From 4887596b432a87b5ee2f71da8ccf34c341128320 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 27 Jul 2026 14:44:43 +0200 Subject: [PATCH] Web: Artikelbild per Klick gross anzeigen (Lightbox) Klick aufs Artikelbild oeffnet es bildschirmfuellend; Schliessen per Knopf, Klick auf den Hintergrund oder Escape. Neue Lightbox-Komponente (Portal an ), im bestehenden Farbschema. Co-Authored-By: Claude Opus 4.8 --- web/src/components/Lightbox.jsx | 32 ++++++++++++++++++++++++++++++ web/src/components/ProduktBild.jsx | 10 +++++++++- web/src/styles.css | 24 ++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 web/src/components/Lightbox.jsx diff --git a/web/src/components/Lightbox.jsx b/web/src/components/Lightbox.jsx new file mode 100644 index 0000000..fc30866 --- /dev/null +++ b/web/src/components/Lightbox.jsx @@ -0,0 +1,32 @@ +import { useEffect } from "react"; +import { createPortal } from "react-dom"; +import Icon from "./Icon"; + +/** + * Bildschirmfüllende Bildansicht. Schließt per Schließen-Knopf, Klick auf den + * Hintergrund oder Escape. Wird per Portal an den gehängt, damit es über + * allem liegt und nicht von Containern (overflow) abgeschnitten wird. + */ +export default function Lightbox({ src, alt, onClose }) { + useEffect(() => { + function onKey(e) { if (e.key === "Escape") onClose(); } + document.addEventListener("keydown", onKey); + const vorher = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.removeEventListener("keydown", onKey); + document.body.style.overflow = vorher; + }; + }, [onClose]); + + return createPortal( +
+ + {/* Klick aufs Bild soll nicht schließen – nur der Hintergrund. */} + {alt e.stopPropagation()} /> +
, + document.body, + ); +} diff --git a/web/src/components/ProduktBild.jsx b/web/src/components/ProduktBild.jsx index 3a9cdcf..fa4cb2d 100644 --- a/web/src/components/ProduktBild.jsx +++ b/web/src/components/ProduktBild.jsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { authorizedObjectUrl } from "../api"; import Icon from "./Icon"; +import Lightbox from "./Lightbox"; /** * Lädt das Artikelbild aus der eigenen Datenbank. @@ -61,8 +62,15 @@ function useBildUrl(productId, version = 0, onLoaded) { */ export default function ProduktBild({ productId, alt, className = "", version = 0, onLoaded }) { const url = useBildUrl(productId, version, onLoaded); + const [zoom, setZoom] = useState(false); if (!url) return null; - return {alt; + return ( + <> + {alt setZoom(true)} /> + {zoom && setZoom(false)} />} + + ); } /** diff --git a/web/src/styles.css b/web/src/styles.css index 1e5098f..9125c0f 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -209,11 +209,35 @@ input::placeholder { color: var(--muted); opacity: 0.7; } border-radius: var(--radius); padding: var(--sp-2); } +.produkt-bild.clickable { cursor: zoom-in; } /* Auf schmalen Fenstern haben Bild und Felder nebeneinander keinen Platz mehr. */ @media (max-width: 560px) { .produkt-kopf { flex-direction: column-reverse; align-items: center; } } +/* Bild-Lightbox: Klick aufs Artikelbild zeigt es gross (Schliessen per Knopf, + Klick auf den Hintergrund oder Escape). */ +.lightbox { + position: fixed; inset: 0; z-index: 200; + display: flex; align-items: center; justify-content: center; + padding: var(--sp-5); + background: rgba(0, 0, 0, 0.82); +} +.lightbox-img { + max-width: 92vw; max-height: 92vh; object-fit: contain; + border-radius: var(--radius); + box-shadow: 0 16px 56px rgba(0, 0, 0, 0.5); + cursor: default; +} +.lightbox-close { + position: fixed; top: 16px; right: 16px; + display: flex; align-items: center; justify-content: center; + width: 40px; height: 40px; border-radius: 50%; + background: var(--surface); color: var(--text); + border: 1px solid var(--border); cursor: pointer; +} +.lightbox-close:hover { background: var(--surface-2); } + /* Knoepfe neben dem Barcode-Feld: nebeneinander, mit Umbruch auf schmalen Fenstern. Das Eingabefeld daneben fuellt die restliche Breite. */ .knopf-spalte { display: flex; flex-flow: row wrap; gap: var(--sp-2); flex: 0 0 auto; }