Files
tessera-ctl/apps/web/src/components/dashboard/widgets/picture-frame-lightbox.tsx
T
schalli 8bf3601a18 fix(quick-260921-pi9): Grossansicht per Portal, "1 Minute" statt "1 Minuten", Kachel-Vorgabe 8x12
Befunde aus dem Browser-Rundgang am 21.09.2026:

- Die Grossansicht lag in einem `react-grid-item` mit CSS-`transform`; ein
  transformierter Vorfahr wird fuer `position: fixed` zum Bezugsrahmen, der
  Dialog war deshalb auf die Kachelflaeche (531x216) beschraenkt statt den
  Viewport zu fuellen. Jetzt per `createPortal` in `document.body`, wie der
  Kalender-Tooltip.
- Wechselintervall "1 Minuten" -> ICU-Plural (`one {# Minute}`), de + en; der
  Formular-Test nutzt dafuer den echten `createTranslator` von next-intl auf
  der echten de.json statt eines `{n}`-Ersatzes.
- Standardgroesse 8x8 (216 px hoch) war zu flach fuer ein Foto -> 8x12 wie die
  Kalender-Vorgabe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 19:10:52 +02:00

109 lines
3.6 KiB
TypeScript

'use client';
import { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { useTranslations } from 'next-intl';
interface PictureFrameLightboxProps {
src: string;
caption?: string;
onClose: () => void;
}
/**
* Grossansicht des Bilderrahmen-Widgets (quick-260921-pi9).
*
* Aufbau wie widget-catalog-modal.tsx: Hintergrund als echte, benannte
* Schaltflaeche (schliesst), Dialog `role="dialog" aria-modal` mit
* `tabIndex={-1}` und Fokus beim Einhaengen, Escape-Listener mit Aufraeumung,
* Schliessen-Knopf oben rechts. Die Fokus-RUECKGABE an den Bild-Knopf macht
* das Widget selbst (es kennt den Ausloeser), nicht diese Komponente.
*
* `referrerPolicy="no-referrer"` auch hier (T-PI9-09): ein Fremdhost erfaehrt
* die Tessera-Adresse nicht. Die Unterschrift ist ein React-Textknoten,
* kein HTML (T-PI9-06).
*
* Gerendert per `createPortal` in `document.body` (wie der Kalender-Tooltip):
* die Kachel liegt in einem `react-grid-item` mit CSS-`transform`, und ein
* transformierter Vorfahr macht sich fuer `position: fixed` zum Bezugsrahmen —
* ohne Portal waere die Grossansicht auf die Kachelflaeche beschraenkt
* (im Browser-Rundgang am 21.09.2026 so gesehen: Dialog 531x216 statt Viewport).
*/
export function PictureFrameLightbox({ src, caption, onClose }: PictureFrameLightboxProps) {
const t = useTranslations('widgets');
const tCommon = useTranslations('common');
const dialogRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onClose();
}
}
document.addEventListener('keydown', handleKeyDown);
dialogRef.current?.focus();
return () => document.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
if (typeof document === 'undefined') return null;
return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center">
<button
type="button"
onClick={onClose}
aria-label={t('pictureFrame.close')}
data-testid="picture-frame-lightbox-backdrop"
className="fixed inset-0 bg-black/80"
/>
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-label={caption ?? t('pictureFrame.open')}
className="relative z-50 flex max-h-[95vh] max-w-[95vw] flex-col items-center gap-2 p-4"
tabIndex={-1}
>
<button
type="button"
onClick={onClose}
data-testid="picture-frame-lightbox-close"
className="absolute -right-1 -top-1 z-10 rounded-full bg-black/60 p-1.5 text-white transition-colors hover:bg-black/80"
aria-label={tCommon('close')}
>
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
<img
src={src}
alt={caption ?? ''}
referrerPolicy="no-referrer"
data-testid="picture-frame-lightbox-image"
className="max-h-[85vh] max-w-[90vw] rounded object-contain shadow-2xl"
/>
{caption && (
<p className="max-w-[90vw] truncate text-center text-sm text-white" data-testid="picture-frame-lightbox-caption">
{caption}
</p>
)}
</div>
</div>,
document.body,
);
}