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:
2026-09-21 18:56:02 +02:00
parent 737974b653
commit c080580459
18 changed files with 1688 additions and 4 deletions
@@ -6,6 +6,7 @@ import type { ComponentType } from 'react';
* calculator/favorites/stopwatch: Phase 8 additions (der fruehere
* Einzel-Schnellzugriffs-Typ wurde in quick-260916-iex entfernt — Favoriten
* decken den Fall ab).
* picture-frame: Bilderrahmen (quick-260921-pi9).
*/
export type WidgetType =
| 'clock'
@@ -14,7 +15,8 @@ export type WidgetType =
| 'note'
| 'calculator'
| 'favorites'
| 'stopwatch';
| 'stopwatch'
| 'picture-frame';
/**
* Props contract that every widget component must accept.
@@ -53,6 +55,10 @@ 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 },
};
/**
@@ -248,6 +254,28 @@ function StopwatchIcon({ className }: { className?: string }) {
);
}
function PictureFrameIcon({ className }: { className?: string }) {
return (
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
);
}
/**
* Registry of all widget types. Used by the widget catalog modal
* and the grid renderer to look up components and constraints.
@@ -309,6 +337,14 @@ export const WIDGET_REGISTRY: Record<WidgetType, WidgetDefinition> = {
...WIDGET_CONSTRAINTS.stopwatch,
component: PlaceholderWidget, // Replaced via wireStopwatchWidget()
},
'picture-frame': {
type: 'picture-frame',
nameKey: 'pictureFrame.name',
descriptionKey: 'pictureFrame.description',
icon: PictureFrameIcon,
...WIDGET_CONSTRAINTS['picture-frame'],
component: PlaceholderWidget, // Replaced via wirePictureFrameWidget()
},
};
// Wire actual widget components lazily to avoid circular deps
@@ -370,3 +406,11 @@ export function wireStopwatchWidget(component: ComponentType<WidgetProps>) {
stopwatchWired = true;
}
}
let pictureFrameWired = false;
export function wirePictureFrameWidget(component: ComponentType<WidgetProps>) {
if (!pictureFrameWired) {
WIDGET_REGISTRY['picture-frame'].component = component;
pictureFrameWired = true;
}
}