From 6246d7c3a4cdd5b437ee719817829ea01c1d61ce Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Sat, 25 Apr 2026 10:32:37 +0200 Subject: [PATCH] feat(gallery): grid variant with hover labels + download modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ImageGalleryBlock now branches on `block.variant`: - "standard" (default) → existing carousel. - "grid" → 2/3/4-column thumbnail grid (1:1 squares, w=400/q=75 through /cms-images). Each tile scales 5% on hover and surfaces the image's title/alt/caption/description as a gradient overlay caption that fades in on hover and keyboard focus. Click any tile → fullscreen lightbox (z-100 overlay + 90vh image). The lightbox carries: - A download button using the asset's filename (extracted from the src URL). - Prev/Next arrows when there's more than one image (keyboard: ←/→), Esc to close, click on the backdrop closes too. - Position counter (n/total) when applicable. Type updates in block-types.ts: ImageGalleryBlockData gains `variant?: "standard"|"grid"` and `title?`. ImageGalleryImage gains `alt`/`caption`/`title` so the schema's `image` field shape is honoured for the hover label. --- src/lib/block-types.ts | 8 + .../blocks/ImageGalleryBlock.svelte | 152 ++++++++++++++++++ 2 files changed, 160 insertions(+) diff --git a/src/lib/block-types.ts b/src/lib/block-types.ts index 5f8667b..c2f2b53 100644 --- a/src/lib/block-types.ts +++ b/src/lib/block-types.ts @@ -104,6 +104,10 @@ export interface ImageGalleryImage { _slug?: string; src?: string; description?: string; + /** Optionaler Hover-Titel (image-Type: alt/caption/title). */ + alt?: string; + caption?: string; + title?: string; file?: { url?: string }; /** Nach resolveContentImages: Proxy-URL zum transformierten Bild (16:9). */ resolvedSrc?: string; @@ -115,6 +119,10 @@ export interface ImageGalleryBlockData { _slug?: string; /** Optionaler Einleitungstext (Markdown). */ description?: string; + /** Optional: oberhalb des Galerie-Modals als Header. */ + title?: string; + /** "standard" = Carousel (Default), "grid" = Thumbnail-Grid mit Modal. */ + variant?: "standard" | "grid"; images?: ImageGalleryImage[]; layout?: BlockLayout; } diff --git a/src/lib/components/blocks/ImageGalleryBlock.svelte b/src/lib/components/blocks/ImageGalleryBlock.svelte index 2ae2e6f..3f0f281 100644 --- a/src/lib/components/blocks/ImageGalleryBlock.svelte +++ b/src/lib/components/blocks/ImageGalleryBlock.svelte @@ -13,6 +13,22 @@ const t = useTranslate(); let { block }: { block: ImageGalleryBlockData } = $props(); + /** Hover-Label aus image-Field — title|alt|caption|description, in dieser Reihenfolge. */ + function imageLabel(img: ImageGalleryImage): string { + return (img.title ?? img.alt ?? img.caption ?? img.description ?? "").trim(); + } + + /** Dateiname für Download aus Asset-URL ableiten. */ + function downloadFilename(url: string): string { + try { + const u = new URL(url); + const last = u.pathname.split("/").filter(Boolean).pop(); + return last ? decodeURIComponent(last) : "image"; + } catch { + return "image"; + } + } + marked.setOptions({ gfm: true }); const layoutClasses = $derived(getBlockLayoutClasses(block.layout)); @@ -45,6 +61,36 @@ let currentIndex = $state(0); + // Grid-Modal-State: -1 = closed, 0..n-1 = open at index. + let modalIndex = $state(-1); + const modalOpen = $derived(modalIndex >= 0); + + function openModal(i: number) { + modalIndex = i; + } + function closeModal() { + modalIndex = -1; + } + function modalPrev() { + if (modalIndex < 0) return; + modalIndex = (modalIndex - 1 + withUrl.length) % withUrl.length; + } + function modalNext() { + if (modalIndex < 0) return; + modalIndex = (modalIndex + 1) % withUrl.length; + } + function onModalKey(e: KeyboardEvent) { + if (!modalOpen) return; + if (e.key === "Escape") { + e.preventDefault(); + closeModal(); + } else if (e.key === "ArrowLeft") { + modalPrev(); + } else if (e.key === "ArrowRight") { + modalNext(); + } + } + function prev() { currentIndex = (currentIndex - 1 + withUrl.length) % withUrl.length; } @@ -109,6 +155,8 @@ } + +
{#if descriptionHtml}
@@ -118,6 +166,110 @@ {#if withUrl.length === 0}

{t(T.image_gallery_empty)}

+ {:else if block.variant === "grid"} +
    + {#each withUrl as item, i} + {@const label = imageLabel(item.img)} +
  • + +
  • + {/each} +
+ + {#if modalOpen} + {@const cur = withUrl[modalIndex]} + {@const curLabel = imageLabel(cur.img)} + + {/if} {:else if withUrl.length === 1}