From 8bf3601a1807ec82d43d9f1646492b609623e19e Mon Sep 17 00:00:00 2001 From: Schalli Date: Mon, 21 Sep 2026 19:10:52 +0200 Subject: [PATCH] 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) --- .../dashboard/widget-registry.test.tsx | 2 +- .../components/dashboard/widget-registry.tsx | 9 +++++---- .../widgets/picture-frame-lightbox.tsx | 14 +++++++++++-- .../picture-frame-config-form.test.tsx | 20 ++++++++----------- apps/web/src/messages/de.json | 2 +- apps/web/src/messages/en.json | 2 +- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/apps/web/src/components/dashboard/widget-registry.test.tsx b/apps/web/src/components/dashboard/widget-registry.test.tsx index 1d5a03a..fe5822a 100644 --- a/apps/web/src/components/dashboard/widget-registry.test.tsx +++ b/apps/web/src/components/dashboard/widget-registry.test.tsx @@ -66,7 +66,7 @@ describe('WIDGET_CONSTRAINTS (DASH-11)', () => { calculator: { minW: 3, minH: 10, defaultW: 6, defaultH: 10 }, favorites: { minW: 3, minH: 3, defaultW: 6, defaultH: 10 }, stopwatch: { minW: 4, minH: 3, defaultW: 6, defaultH: 6 }, - 'picture-frame': { minW: 4, minH: 4, defaultW: 8, defaultH: 8 }, + 'picture-frame': { minW: 4, minH: 4, defaultW: 8, defaultH: 12 }, }); let counted = 0; diff --git a/apps/web/src/components/dashboard/widget-registry.tsx b/apps/web/src/components/dashboard/widget-registry.tsx index 41bbf56..63bc7da 100644 --- a/apps/web/src/components/dashboard/widget-registry.tsx +++ b/apps/web/src/components/dashboard/widget-registry.tsx @@ -55,10 +55,11 @@ export const WIDGET_CONSTRAINTS: Record< calculator: { minW: 3, minH: 10, defaultW: 6, defaultH: 10 }, favorites: { minW: 3, minH: 3, defaultW: 6, defaultH: 10 }, stopwatch: { minW: 4, minH: 3, defaultW: 6, defaultH: 6 }, - // quick-260921-pi9: ein Bild braucht Flaeche — 8x8 ist ein Quadrat von der - // Groesse einer halben Notiz (6x8 -> 8x8), 4x4 die kleinste Kachel, in der - // Bild und Unterschrift-Streifen noch erkennbar sind. - 'picture-frame': { minW: 4, minH: 4, defaultW: 8, defaultH: 8 }, + // quick-260921-pi9: ein Bild braucht Flaeche — 8x12 entspricht der + // Kalender-Vorgabe (rund 530x330 px, Querformat wie ein Foto); 8x8 war im + // Browser-Rundgang mit 216 px Hoehe zu flach. 4x4 ist die kleinste Kachel, + // in der Bild und Unterschrift-Streifen noch erkennbar sind. + 'picture-frame': { minW: 4, minH: 4, defaultW: 8, defaultH: 12 }, }; /** diff --git a/apps/web/src/components/dashboard/widgets/picture-frame-lightbox.tsx b/apps/web/src/components/dashboard/widgets/picture-frame-lightbox.tsx index f3d337b..7ec5a07 100644 --- a/apps/web/src/components/dashboard/widgets/picture-frame-lightbox.tsx +++ b/apps/web/src/components/dashboard/widgets/picture-frame-lightbox.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; import { useTranslations } from 'next-intl'; interface PictureFrameLightboxProps { @@ -21,6 +22,12 @@ interface PictureFrameLightboxProps { * `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'); @@ -38,7 +45,9 @@ export function PictureFrameLightbox({ src, caption, onClose }: PictureFrameLigh return () => document.removeEventListener('keydown', handleKeyDown); }, [onClose]); - return ( + if (typeof document === 'undefined') return null; + + return createPortal(
- + , + document.body, ); } diff --git a/apps/web/src/components/settings/picture-frame-config-form.test.tsx b/apps/web/src/components/settings/picture-frame-config-form.test.tsx index d68fdef..508a12b 100644 --- a/apps/web/src/components/settings/picture-frame-config-form.test.tsx +++ b/apps/web/src/components/settings/picture-frame-config-form.test.tsx @@ -8,19 +8,14 @@ import de from '@/messages/de.json'; * Eintragsliste mit Vorschau/Unterschrift/Pfeilen/Entfernen, Upload-Kette, * https-Pruefung, Grenze 30. */ -vi.mock('next-intl', async () => { - const messages = (await import('@/messages/de.json')).default as Record; - const lookup = (path: string): string | undefined => - path.split('.').reduce((o, k) => (o && typeof o === 'object' ? (o as Record)[k] : undefined), messages) as - | string - | undefined; +vi.mock('next-intl', async (importOriginal) => { + // Echter ICU-Uebersetzer von next-intl auf der echten de.json: die + // Intervall-Beschriftung nutzt `{n, plural, one {…} other {…}}`, ein + // handgeschriebener `{n}`-Ersatz wuerde das Muster nicht verstehen. + const actual = await importOriginal(); + const messages = (await import('@/messages/de.json')).default; return { - useTranslations: - (ns?: string) => - (key: string, values?: Record) => { - const raw = lookup(ns ? `${ns}.${key}` : key) ?? key; - return values ? raw.replace(/\{(\w+)\}/g, (_: string, n: string) => String(values[n] ?? '')) : raw; - }, + useTranslations: (ns?: string) => actual.createTranslator({ locale: 'de', messages, namespace: ns }), }; }); @@ -70,6 +65,7 @@ describe('PictureFrameConfigForm (quick-260921-pi9)', () => { expect(Array.from(interval.options).map((o) => o.textContent)).toContain('Kein Wechsel'); expect(Array.from(interval.options).map((o) => o.textContent)).toContain('5 Sekunden'); expect(Array.from(interval.options).map((o) => o.textContent)).toContain('30 Minuten'); + expect(Array.from(interval.options).map((o) => o.textContent)).toContain('1 Minute'); fireEvent.change(fit, { target: { value: 'cover' } }); expect(onChange).toHaveBeenLastCalledWith({ fit: 'cover' }); diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index 7e83e09..e485422 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -347,7 +347,7 @@ "intervalLabel": "Wechselintervall", "intervalOff": "Kein Wechsel", "intervalSeconds": "{n} Sekunden", - "intervalMinutes": "{n} Minuten", + "intervalMinutes": "{n, plural, one {# Minute} other {# Minuten}}", "orderLabel": "Reihenfolge der Bilder", "orderSequence": "Reihenfolge", "orderRandom": "Zufall", diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 659a898..4e47256 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -347,7 +347,7 @@ "intervalLabel": "Change interval", "intervalOff": "No change", "intervalSeconds": "{n} seconds", - "intervalMinutes": "{n} minutes", + "intervalMinutes": "{n, plural, one {# minute} other {# minutes}}", "orderLabel": "Image order", "orderSequence": "In order", "orderRandom": "Random",