feat(quick-260921-pi9): Bilderrahmen-Widget - Diashow mit Grossansicht, Bildverwaltung in den Einstellungen
- picture-frame-config.ts: Eintragstyp als Vereinigung (upload | url) in EINER geordneten Liste, resolvePictureFrameConfig laesst alles ausser https weg (T-PI9-07), Intervall 0 oder 5..3600 s, pickNextIndex (Zufall nie dasselbe) - dashboard-images-api.ts: Upload als FormData-Feld image ohne eigenen Content-Type, 413 -> deutsche Meldung, Proxy-Pfad fuer <img src> - PictureFrameWidget: Leerzustand, <img referrerPolicy=no-referrer> (Browser laedt Fremdbilder, Server nie), object-contain/cover, Unterschrift-Streifen, Wechsel per Timer mit Raeumung, kaputte Bilder verlassen den Umlauf, Grossansicht nur ausserhalb des Bearbeitungsmodus mit Fokus-Rueckgabe und Pause des Wechsels; im Bearbeitungsmodus kein Knopf (Karte bleibt Griff) - PictureFrameConfigForm im WidgetSettingsPanel: Ausschnitt, Intervall, Reihenfolge, Liste mit Vorschau/Unterschrift/Pfeilen/Entfernen (Upload wird auch serverseitig geloescht), Datei hochladen, https-Adresse hinzufuegen - Registry (4x4 min, 8x8 Vorgabe), Katalog, Seite, Uebersetzungen de/en (widgets.pictureFrame, 28 Schluessel), bestehende Tests auf acht Typen Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
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).
|
||||
*/
|
||||
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]);
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user