'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; import { Responsive, noCompactor } from 'react-grid-layout'; import type { Compactor, ResponsiveLayouts } from 'react-grid-layout'; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; import { useTranslations } from 'next-intl'; import { WIDGET_CONSTRAINTS, type WidgetType } from './widget-registry'; import { WidgetWrapper } from './widgets/widget-wrapper'; import { TesseraLogo } from '@/components/brand/tessera-logo'; // quick-260916-bwo: Raster doppelt so fein wie zuvor (24 statt 12 Spalten, // 20 statt 40 px Zeilenhoehe, 8 statt 16 px Abstand). Gespeicherte Anordnungen // in alten Einheiten werden in `@/lib/grid-layout-migration` einmalig // umgerechnet und mit `__gridVersion: 2` markiert. BREAKPOINTS unveraendert. const BREAKPOINTS = { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }; const COLS = { lg: 24, md: 20, sm: 12, xs: 8, xxs: 2 }; // quick-260916-dyv: Ziehen zuverlaessig. // - Griff ist die GANZE Karte (`widget-drag-handle` in widget-wrapper.tsx). // - `cancel` verhindert den Drag-Start an Eingabefeldern, Knoepfen, Links, // [contenteditable], [data-no-drag] und der Klasse `widgetNoDrag` (Favoriten/ // Link-Widget, bisher nirgends verdrahtet). react-draggable 4.7.0 prueft // `cancel` NACH `handle` und vom Ereignisziel aufwaerts bis zum RGL-Element // (Draggable.js:417 + matchesSelectorAndParentsTo) — cancel gewinnt also auch // INNERHALB des Griffs. RGL haengt `.react-resizable-handle` selbst voran // (chunk-WGL5FSZH.mjs:526), der Groessen-Griff funktioniert weiter. // - `threshold: 3` = RGL-Standard (Klick vs. Ziehen). export const WIDGET_DRAG_HANDLE_SELECTOR = '.widget-drag-handle'; export const WIDGET_DRAG_CANCEL_SELECTOR = 'input, textarea, select, button, a, [contenteditable], [data-no-drag], .widgetNoDrag'; // quick-260916-dyv: Kein Ueberlappen beim Ablegen, freie Platzierung bleibt. // `noCompactor` (Commit c8f3361, "prevent auto-compaction on drag") ist gewollt: // Luecken werden nicht geschlossen. OHNE `preventCollision` springt beim Ziehen // auf ein belegtes Feld das gezogene Widget auf die Zeile des getroffenen und // das getroffene rutscht um seine EIGENE Hoehe nach unten — ohne Kaskade und // ohne Aufloesung, Ueberlappungen bleiben, weil `noCompactor.compact` die // Identitaet ist (gemessen chunk-76RTO6EO.mjs:279-328); beim Vergroessern in // einen Nachbarn entsteht die Ueberlappung stumm (chunk-WGL5FSZH.mjs:872-885). // MIT `preventCollision: true` bleibt das gezogene Widget am Ausgangsort // (`l.x = oldX; l.y = oldY`), und Vergroessern stoppt am Nachbarn. // `preventCollision` lebt am Compactor-Objekt (chunk-WGL5FSZH.mjs:666). export const FREE_PLACEMENT_COMPACTOR: Compactor = { ...noCompactor, preventCollision: true }; interface LayoutItemShape { i: string; x: number; y: number; w: number; h: number; minW?: number; minH?: number; } interface DashboardGridProps { layouts: Record>; widgets: Array<{ id: string; widgetType: string; config: Record }>; isEditMode: boolean; onLayoutChange: (allLayouts: ResponsiveLayouts) => void; onRemoveWidget: (id: string) => void; } /** * quick-260916-dyv: Gespeicherte minW/minH aus den Konstanten ueberschreiben. * * Warum: react-grid-layout 2.2.3 nimmt in `synchronizeLayoutWithChildren` * (chunk-WGL5FSZH.mjs:559-562) fuer jedes Kind MIT Eintrag im `layout` den * Eintrag per `cloneLayoutItem` woertlich — inklusive minW/minH — und liest * `data-grid` nur fuer Kinder OHNE Eintrag. Gespeicherte Anordnungen tragen * minW/minH (RGL liefert sie in `onLayoutChange`, 260916-bwo hat sie verdoppelt * persistiert). Eine Aenderung von WIDGET_CONSTRAINTS allein aendert fuer * bestehende Widgets also NICHTS. Deshalb: in JEDEM Breakpoint minW/minH jedes * Eintrags eines bekannten Typs aus der Tabelle setzen; die Konstanten sind * die einzige Quelle (T-DYV-01: manipulierte Minima wirken nicht). * * Liegt die gespeicherte Groesse UNTER dem neuen Minimum (z. B. Rechner mit * h 8, neues minH 9), wird w/h auf das Minimum angehoben: RGL selbst klemmt * w/h nur beim Vergroessern/Verkleinern (`minMaxSize.constrainSize`, * chunk-KDANGDDL.mjs:26-31; `minConstraints` an `Resizable`, * chunk-WGL5FSZH.mjs:472-475), rendert die zu kleine Kachel aber woertlich — * der Rechner bliebe bis zum ersten Anfassen unten abgeschnitten. * Unbekannte Typen bleiben unveraendert; das Eingabeobjekt wird nicht mutiert. */ function applyConstraintMinima( layouts: Record>, widgets: Array<{ id: string; widgetType: string }>, ): Record> { const typeById = new Map(); for (const w of widgets) typeById.set(w.id, w.widgetType); const result: Record> = {}; for (const key of Object.keys(layouts)) { const entries = layouts[key]; if (!Array.isArray(entries)) continue; result[key] = entries.map((entry) => { const type = typeById.get(entry.i); const constraints = type ? WIDGET_CONSTRAINTS[type as WidgetType] : undefined; if (!constraints) return { ...entry }; return { ...entry, w: Math.max(entry.w, constraints.minW), h: Math.max(entry.h, constraints.minH), minW: constraints.minW, minH: constraints.minH, }; }); } return result; } /** * Dashboard grid wrapper using react-grid-layout v2 Responsive component. * Renders widgets in a draggable/resizable grid when in edit mode. * Shows empty state when no widgets exist. * * v2 API: uses dragConfig/resizeConfig instead of isDraggable/isResizable. */ export function DashboardGrid({ layouts, widgets, isEditMode, onLayoutChange, onRemoveWidget, }: DashboardGridProps) { const t = useTranslations('widgets'); const containerRef = useRef(null); const [width, setWidth] = useState(1200); // Measure container width (v2 requires explicit width — Pitfall 1) useEffect(() => { if (!containerRef.current) return; const observer = new ResizeObserver((entries) => { setWidth(entries[0].contentRect.width); }); observer.observe(containerRef.current); return () => observer.disconnect(); }, []); // quick-260916-dyv: minW/minH (und zu kleine w/h) aus WIDGET_CONSTRAINTS — // siehe applyConstraintMinima. Vor dem Leerzustand, damit die Hook-Reihenfolge // stabil bleibt. const effectiveLayouts = useMemo(() => applyConstraintMinima(layouts, widgets), [layouts, widgets]); // Empty state (D-02) if (widgets.length === 0) { return (
{/* Bildmarke statt des frueheren handgezeichneten Vier-Quadrate-Symbols. Ohne Rahmen und ohne Kontur: die Marke steht hier frei auf dem Seitenhintergrund, nicht auf einer eigenen Flaeche. */}

{t('emptyHeading')}

{t('emptyBody')}

); } return (
onLayoutChange(allLayouts)} > {widgets.map((widget) => { const constraints = WIDGET_CONSTRAINTS[widget.widgetType as WidgetType]; return (
l.i === widget.id) ?? { x: 0, y: 0, w: constraints?.defaultW ?? 4, h: constraints?.defaultH ?? 4, }), minW: constraints?.minW ?? 4, minH: constraints?.minH ?? 4, }} >
); })}
); }