refactor(quick-260922-m1h): Widget-Typen an einer Stelle, Katalog aus der Registry, Kachel kennt ihr Modul

Ein neuer Widget-Typ war an sieben Stellen einzutragen; vergass man eine,
fehlte die Kachel im Katalog oder die API lehnte sie mit 400 ab.

- WIDGET_TYPES/WidgetType/WIDGET_MODULE_SLUGS stehen jetzt einmal in
  packages/shared; Registry, Katalog und die @IsIn-Whitelist der API
  leiten davon ab
- neun wireXWidget()-Funktionen durch ein generisches registerWidget()
  ersetzt (idempotent, unbekannter Typ wirft in der Entwicklung)
- der Katalog fuehrt keine zweite Typliste mehr, sondern leitet sie aus
  der Registry ab und filtert nach Modulzugriff (fail-closed, wenn die
  Modulliste unbekannt ist); der Abruf von /modules/active liegt auf der
  Dashboard-Seite, nicht im Dialog
- widget-module-map.ts liest die geteilte Tabelle statt einer Kopie, die
  oeffentliche Funktion bleibt unveraendert

Der Katalogfilter ist Komfort (T-M1H-01) — verbindlich bleibt der
serverseitige Filter in DashboardService.getWidgets.

Abweichung vom Plan: apps/web hing entgegen der Planannahme noch nicht
von @tessera/shared ab; die Abhaengigkeit wurde ergaenzt (Lockfile). Die
Dockerfiles kopieren packages/shared bereits, der Produktionsbau von
Next.js und der nest build laufen unveraendert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 16:03:58 +02:00
parent ee2b0256b5
commit 56c07c3581
14 changed files with 535 additions and 146 deletions
@@ -2,28 +2,30 @@
import { useEffect, useRef } from 'react';
import { useTranslations } from 'next-intl';
import { WIDGET_REGISTRY, type WidgetType } from './widget-registry';
import { WIDGET_REGISTRY, type WidgetType, visibleWidgetTypes } from './widget-registry';
interface WidgetCatalogModalProps {
isOpen: boolean;
onClose: () => void;
onAddWidget: (type: WidgetType) => void;
/**
* Slugs der Module, die der angemeldete Benutzer nutzen darf — geholt von
* der Dashboard-Seite ueber `GET /modules/active` (quick-260922-m1h).
* `null` heisst "noch unbekannt oder Abruf fehlgeschlagen": dann bleiben
* Kacheln MIT `moduleSlug` ausgeblendet (fail-closed).
*
* Der Abruf steht bewusst NICHT in diesem Dialog, damit er ein reines
* Anzeige-Bauteil bleibt und ohne Netzwerk-Attrappe testbar ist.
*/
accessibleModuleSlugs: readonly string[] | null;
}
const WIDGET_TYPES: WidgetType[] = [
'clock',
'search',
'calendar',
'note',
'calculator',
'favorites',
'stopwatch',
'picture-frame',
'xframe',
];
/**
* Modal dialog showing available widget types as selectable cards.
*
* quick-260922-m1h: Die Liste kommt aus WIDGET_REGISTRY (Reihenfolge der
* Registry-Definition) statt aus einer zweiten, hier gepflegten Liste — eine
* neue Kachel musste sonst an zwei Stellen eingetragen werden.
* Click on a card adds the widget to the dashboard and closes the modal.
* Escape to close, click outside to close, focus trap (D-01 flow).
*/
@@ -31,6 +33,7 @@ export function WidgetCatalogModal({
isOpen,
onClose,
onAddWidget,
accessibleModuleSlugs,
}: WidgetCatalogModalProps) {
const t = useTranslations('widgets');
const tCommon = useTranslations('common');
@@ -107,13 +110,14 @@ export function WidgetCatalogModal({
{/* 2x2 grid of widget type cards */}
<div className="grid grid-cols-2 gap-3">
{WIDGET_TYPES.map((type) => {
{visibleWidgetTypes(WIDGET_REGISTRY, accessibleModuleSlugs).map((type) => {
const def = WIDGET_REGISTRY[type];
const Icon = def.icon;
return (
<button
key={type}
type="button"
data-widget-type={type}
onClick={() => {
onAddWidget(type);
onClose();