'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { Compactor, ResponsiveLayouts } from 'react-grid-layout'; import { noCompactor, Responsive } from 'react-grid-layout'; import { defaultConstraints, defaultPositionStrategy, type LayoutConstraint, type PositionStrategy, transformStrategy, } from 'react-grid-layout/core'; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; import { useTranslations } from 'next-intl'; import { CANVAS_MIN_HEIGHT, CANVAS_MIN_WIDTH, type GridCanvas } from '@/lib/grid-layout-migration'; import { clampWidthToCols, GRID_COLS, WIDGET_CONSTRAINTS, WIDGET_REGISTRY, type WidgetType, } from './widget-registry'; import { WidgetIcon } from './widgets/widget-icon'; import { WidgetWrapper } from './widgets/widget-wrapper'; // 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` markiert. BREAKPOINTS unveraendert. // // quick-260929-dmx (Version 3): Raster in der BREITE noch einmal doppelt so // fein (48 statt 24 Spalten am lg-Breakpoint, uebrige Breakpoints ebenso // verdoppelt). Zeilenhoehe (20 px) und Abstand (12 px) bleiben. Alle // Breitenwerte (x, w, minW, maxW, WIDGET_CONSTRAINTS, Rueckfallwerte) sind // damit in 48stel-Einheiten; gespeicherte Anordnungen rechnet die Migration // einmalig um, jedes Widget behaelt seine Bildschirmgroesse und -position. const BREAKPOINTS = { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }; // Spaltenzahlen stehen seit quick-260930 in widget-registry.tsx (GRID_COLS), // weil auch der Store sie fuer neue Kacheln braucht. const COLS = GRID_COLS; // 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-Widget). 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 }; /** * Groesse aendern trotz Nachbarn (Design „Mosaik“, Runde 3). * * Mit `preventCollision` verwirft react-grid-layout 2.2.3 beim Ziehen am * Groessengriff die GANZE neue Groesse, sobald sie irgendeinen Nachbarn * beruehrt (chunk-WGL5FSZH.mjs, onResize: w UND h zurueck auf alt). Wer eine * Kachel nur schmaler ziehen will und dabei die Maus eine Rasterzeile nach * unten wackelt, stoesst an die Kachel darunter — und die Breite bleibt * stehen. Genau so liess sich die Favoriten-Kachel oft nicht schmaler machen. * * Diese Regel laeuft vor der Kollisionspruefung: kollidiert die gewuenschte * Groesse, wird zuerst nur die Breite (alte Hoehe), dann nur die Hoehe (alte * Breite) uebernommen — was frei ist, gewinnt. Nur fuer Griffe, die x/y nicht * verschieben (se, e, s); unser Raster zeigt nur den se-Griff. */ function overlaps( a: { x: number; y: number; w: number; h: number }, b: { x: number; y: number; w: number; h: number }, ): boolean { return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h; } export const RESIZE_AXIS_FALLBACK: LayoutConstraint = { name: 'resizeAxisFallback', constrainSize(item, w, h, handle, context) { if (handle !== 'se' && handle !== 'e' && handle !== 's') return { w, h }; const current = context.layout.find((l) => l.i === item.i) ?? item; const others = context.layout.filter((l) => l.i !== item.i); const free = (cw: number, ch: number) => !others.some((o) => overlaps({ x: current.x, y: current.y, w: cw, h: ch }, o)); if (free(w, h)) return { w, h }; if (free(w, current.h)) return { w, h: current.h }; if (free(current.w, h)) return { w: current.w, h }; return { w, h }; }, }; const GRID_CONSTRAINTS: LayoutConstraint[] = [...defaultConstraints, RESIZE_AXIS_FALLBACK]; const GRID_MARGIN = 12; /** * Dashboard wie ein Bild mitskalieren (Leinwand). * * Wunsch: an PC 1 eingerichtet, an PC 2 mit anderer Aufloesung GENAU gleich * aussehen — alles (auch die Schrift) gleichmaessig kleiner oder groesser, * nie scrollen; passt die Bildschirmform nicht, bleibt rechts oder unten * Platz frei. * * Umsetzung: Jeder Reiter hat eine Leinwand `{ w, h }` (gespeichert als * `__canvas`, siehe grid-layout-migration.ts) — die verfuegbare Flaeche, auf * der er eingerichtet wurde (erfasst beim ersten Oeffnen am Desktop, danach * fest). Wo die Leinwand erfasst wurde, sieht das Dashboard 1:1 aus. Auf * jedem Desktop-Bildschirm zeichnet react-grid-layout das Raster mit der * VIRTUELLEN Breite `canvas.w` — Breakpoint und Spaltenbreite sind damit * ueberall identisch — und ein umgebendes Element verkleinert/vergroessert * das Ergebnis per `transform: scale(s)` mit * `s = min(Breite / canvas.w, Hoehe / canvas.h)` (computeCanvasScale), * links oben ausgerichtet (nicht zentriert, bewusst entfernt in bc4c011). * * Unter CANVAS_MIN_WIDTH (Handy/schmal) bleibt alles wie zuvor: Breite = * gemessene Containerbreite, kein transform, keine Erfassung. */ /** Abstand unter dem Raster bis zur Fensterkante: main p-3 (12) + Wrapper p-2 (8). */ export const DASHBOARD_BOTTOM_GAP_PX = 20; /** * Skalierungsfaktor der Leinwand fuer die verfuegbare Flaeche: das kleinere * der beiden Verhaeltnisse, damit alles hineinpasst (freier Platz rechts * ODER unten). Eine unbrauchbare Hoehe (<= 0, nicht endlich) begrenzt nicht — * dann entscheidet nur die Breite. */ export function computeCanvasScale(available: GridCanvas, canvas: GridCanvas): number { const byWidth = available.w / canvas.w; const byHeight = available.h / canvas.h; const heightUsable = Number.isFinite(byHeight) && byHeight > 0; const s = heightUsable ? Math.min(byWidth, byHeight) : byWidth; return Number.isFinite(s) && s > 0 ? s : 1; } /** * Leinwand fuer die Einpassung: ist der Inhalt (unskalierte Rasterhoehe) * hoeher als die Leinwand, gilt die Inhaltshoehe, sonst die Leinwand selbst — * so bleibt es am Original-PC bei 1:1, solange alles draufpasst. */ export function fitCanvasToContent(canvas: GridCanvas, contentHeight: number): GridCanvas { if (!Number.isFinite(contentHeight) || contentHeight <= canvas.h) return canvas; return { w: canvas.w, h: contentHeight }; } /** * Positionsstrategie fuer ein per `transform: scale(s)` skaliertes Raster. * * Bewusst NICHT `createScaledStrategy(s)` aus react-grid-layout 2.2.3: deren * `calcDragPosition` rechnet `(clientX - offsetX) / scale` = * `clientRect.left / scale` (chunk-KDANGDDL.mjs:315-320) — eine FENSTER- * Koordinate ohne Abzug der Raster-Oberkante/-Linkskante. Im Portal liegt das * Raster rechts neben der Seitenleiste; die Kachel spraenge beim Ziehstart um * (Seitenleiste + Abstand) / s Pixel nach rechts unten. Ohne * `calcDragPosition` nimmt react-grid-layout den eigenen Weg * (chunk-WGL5FSZH.mjs:146-163): `(clientRect.left - parentRect.left) / * transformScale` — relativ zum Raster und korrekt entskaliert. Die * Mausbewegungen teilen `DraggableCore` (Prop `scale`) bzw. `Resizable` * (`deltaX / transformScale`) selbst durch `scale`; beide lesen ihn aus * `positionStrategy.scale` (chunk-WGL5FSZH.mjs:675, 512, 527). */ export function scaledPositionStrategy(scale: number): PositionStrategy { if (scale === 1) return defaultPositionStrategy; return { type: 'transform', scale, calcStyle: transformStrategy.calcStyle }; } 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; /** Leerer Zustand: oeffnet den Widget-Katalog. */ onOpenCatalog?: () => void; /** Leerer Zustand: fuegt eine vorgeschlagene Kachel direkt hinzu. */ onQuickAdd?: (type: WidgetType) => void; /** Leinwand des aktiven Reiters; null = noch nicht erfasst (dann unskaliert). */ canvas?: GridCanvas | null; /** Wird mit der verfuegbaren Flaeche gerufen, solange `canvas` fehlt (nur Desktop). */ onCaptureCanvas?: (area: GridCanvas) => void; } /** Vorschlaege im leeren Zustand — Plattform-Kacheln, immer verfuegbar. */ const QUICK_ADD_TYPES: WidgetType[] = ['clock', 'calendar', 'note']; /** * 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 10), 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. * * quick-260930: minW je Breakpoint auf dessen Spaltenzahl begrenzt (Suche * minW 12 am xxs-Breakpoint mit 4 Spalten), w ebenso. Die BREITE wird nur so * weit angehoben, wie rechts Platz ist — innerhalb des Rasters und ohne eine * NEUE Ueberlappung mit einem Nachbarn (freie Platzierung ohne Kompaktierung * wuerde sie sonst stehen lassen; Anlass: Kalender-minW 8 -> 11, schmal * gezogene Kalender mit direktem Nachbarn). Bleibt die Kachel dadurch unter * dem Minimum, ist das harmlos: react-grid-layout zeichnet sie woertlich und * klemmt erst beim naechsten Groessenziehen; stoesst das an den Nachbarn, * greift RESIZE_AXIS_FALLBACK (nur die Hoehe aendert sich), bis der Nachbar * verschoben ist. */ 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; const cols = GRID_COLS[key] ?? Number.POSITIVE_INFINITY; // Zielbreite je Eintrag merken; angehoben wird im zweiten Durchgang, // wenn alle Hoehen feststehen (die Kollisionspruefung braucht sie). const targetW = new Map(); const next = entries.map((entry) => { const type = typeById.get(entry.i); const constraints = type ? WIDGET_CONSTRAINTS[type as WidgetType] : undefined; if (!constraints) return { ...entry }; const minW = clampWidthToCols(constraints.minW, key); const w = Math.min(entry.w, cols); if (w < minW) targetW.set(entry.i, minW); return { ...entry, w, h: Math.max(entry.h, constraints.minH), minW, minH: constraints.minH, }; }); for (const item of next) { const target = targetW.get(item.i); if (target === undefined) continue; const others = next.filter((o) => o !== item); // Schon bestehende Ueberlappungen zaehlen nicht als Hindernis — sie // entstehen nicht durch das Anheben. const blockers = others.filter((o) => !overlaps(item, o)); while ( item.w < target && item.x + item.w + 1 <= cols && !blockers.some((o) => overlaps({ ...item, w: item.w + 1 }, o)) ) { item.w += 1; } } result[key] = next; } 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, onOpenCatalog, onQuickAdd, canvas = null, onCaptureCanvas, }: DashboardGridProps) { const t = useTranslations('widgets'); const [width, setWidth] = useState(1200); // Verfuegbare Hoehe bis zur unteren Fensterkante (Leinwand). 0 = unbekannt. const [availableHeight, setAvailableHeight] = useState(0); // Unskalierte Hoehe des Rasters (inneres Element) fuer die sichtbare Hoehe. const [innerHeight, setInnerHeight] = useState(0); // quick-260922-vdk: Messung haengt am eingehaengten Knoten (Ref-Rueckruf), // nicht mehr an einem Effekt mit leerer Abhaengigkeitsliste. // // Warum: der fruehe Ruecksprung in den Leerzustand rendert das gemessene //
gar nicht erst. Haengt DashboardGrid mit null Kacheln ein, sieht der // alte Effekt (leere Abhaengigkeitsliste, laeuft genau einmal beim // Einhaengen) den Ref als leer, bricht ab und laeuft nie wieder — auch nicht, // wenn spaeter die erste Kachel erscheint und das
tatsaechlich // entsteht. Die Breite blieb dann fuer die ganze Sitzung beim Startwert // 1200, react-grid-layout vergleicht den Breakpoint strikt groesser als // (`width > breakpoint`), 1200 ist damit NICHT `lg` sondern `md` -> 20 statt // 24 Spalten, 51,6 statt 50 px Spaltenbreite, ein toter Streifen rechts. // // Der Ref-Rueckruf `measureRef` folgt dem Knoten ueber Aus- und Einhaengen // hinweg (Leerzustand <-> gefuellt) und laeuft in der Commit-Phase — die dort // ausgeloeste Zustandsaenderung wird vor dem Zeichnen abgearbeitet, ein // zusaetzlicher useLayoutEffect ist damit ueberfluessig. Der Startwert 1200 // lebt deshalb nur noch bis zur Commit-Phase desselben Einhaengens; genau // diesen Uebergang macht der Pfad "Neuladen mit vorhandenen Kacheln" heute // schon in Produktion und er ist nachweislich richtig (459 px in 1176 px // gemessen) — ein anderer Startwert wuerde eine bisher unerprobte // Breakpoint-Folge einfuehren, ohne etwas zu verbessern. // // Der Fenster-Horcher ist ein zusaetzliches Netz fuer Faelle, in denen der // ResizeObserver nichts meldet — nicht sein Ersatz. `applyWidth` verwirft 0 // und nicht endliche Werte (T-VDK-01/T-VDK-03), damit eine kurzzeitig // zusammengefallene Flaeche das Raster nicht auf Null setzt und keine // Rueckkopplungsschleife entsteht; React verwirft gleiche Werte selbst. const nodeRef = useRef(null); const observerRef = useRef(null); const applyWidth = useCallback((next: number) => { if (Number.isFinite(next) && next > 0) { setWidth(next); } }, []); // Leinwand: Hoehe = Fensterhoehe minus Oberkante des Rasters im DOKUMENT // (rect.top + scrollY, also unabhaengig vom Scrollstand) minus Abstand // unten. Das aeussere Element ist selbst nicht transformiert, seine // Oberkante ist also die echte. const applyHeight = useCallback((node: HTMLElement) => { const top = node.getBoundingClientRect().top + window.scrollY; const next = window.innerHeight - top - DASHBOARD_BOTTOM_GAP_PX; setAvailableHeight(Number.isFinite(next) && next > 0 ? Math.floor(next) : 0); }, []); const measureRef = useCallback( (node: HTMLDivElement | null) => { // Ein eventuell laufender Beobachter zuerst trennen — auch der // null-Zweig (Aushaengen) durchlaeuft diese Zeilen, das ist hier der // Aufraeumpfad (T-VDK-02). observerRef.current?.disconnect(); observerRef.current = null; nodeRef.current = node; if (!node) return; // Synchrone Erstmessung in der Commit-Phase, vor dem ersten Zeichnen. applyWidth(node.getBoundingClientRect().width); applyHeight(node); const observer = new ResizeObserver((entries) => { applyWidth(entries[0].contentRect.width); applyHeight(node); }); observer.observe(node); observerRef.current = observer; // Bewusst keine Aufraeumfunktion zurueckgeben: React 19 ruft den // Ref-Rueckruf sonst beim Aushaengen nicht mehr mit null auf. }, [applyWidth, applyHeight], ); useEffect(() => { const onResize = () => { if (nodeRef.current) { applyWidth(nodeRef.current.getBoundingClientRect().width); applyHeight(nodeRef.current); } }; window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, [applyWidth, applyHeight]); // Unskalierte Hoehe des inneren Elements (react-grid-layout setzt die // Rasterhoehe selbst). `contentRect`/`offsetHeight` sind Layout-Masse und // vom transform unberuehrt — genau das Gewuenschte. const innerObserverRef = useRef(null); const innerRef = useCallback((node: HTMLDivElement | null) => { innerObserverRef.current?.disconnect(); innerObserverRef.current = null; if (!node) return; if (node.offsetHeight > 0) setInnerHeight(node.offsetHeight); const observer = new ResizeObserver((entries) => { const h = entries[0].contentRect.height; if (Number.isFinite(h) && h > 0) setInnerHeight(h); }); observer.observe(node); innerObserverRef.current = observer; }, []); // Desktop = verfuegbare Breite ab CANVAS_MIN_WIDTH; nur dort Leinwand. const isDesktop = width >= CANVAS_MIN_WIDTH; const scaled = isDesktop && canvas !== null; // "Nie scrollen" (Nutzerentscheidung): ist der Inhalt hoeher als die // Leinwand (schon am Original-PC laenger als der Bildschirm), zaehlt die // Inhaltshoehe — dann passt auch dort alles hinein, und jeder andere // Bildschirm zeigt dasselbe Bild. Im Bearbeitungsmodus bleibt die Hoehe vom // Beginn des Bearbeitens stehen, sonst wuerde das Raster beim Ziehen nach // unten laufend kleiner und die Kachel liefe dem Mauszeiger davon. const editHeightRef = useRef(null); if (isEditMode && editHeightRef.current === null) editHeightRef.current = innerHeight; if (!isEditMode) editHeightRef.current = null; const contentHeight = isEditMode ? (editHeightRef.current ?? innerHeight) : innerHeight; const scale = scaled ? computeCanvasScale({ w: width, h: availableHeight }, fitCanvasToContent(canvas, contentHeight)) : 1; const positionStrategy = useMemo(() => scaledPositionStrategy(scale), [scale]); // Leinwand erfassen: nur mit eingehaengtem Raster (also mit Kacheln), am // Desktop und mit brauchbarer Hoehe. Der Store uebernimmt sie einmal und // ignoriert weitere Aufrufe; danach ist `canvas` gesetzt und dieser // Effekt tut nichts mehr. const hasWidgets = widgets.length > 0; useEffect(() => { if (!onCaptureCanvas || canvas !== null || !hasWidgets) return; if (!isDesktop || availableHeight < CANVAS_MIN_HEIGHT) return; onCaptureCanvas({ w: width, h: availableHeight }); }, [onCaptureCanvas, canvas, hasWidgets, isDesktop, width, availableHeight]); // 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], ); // Leerer Zustand (D-02, Design „Mosaik“ Runde 2): freundlich ohne // Illustration — Titel, ein Satz, drei Vorschlaege zum direkten // Hinzufuegen und der Hauptknopf zum Katalog. if (widgets.length === 0) { return (

{t('emptyState.title')}

{t('emptyState.body')}

{onQuickAdd && (
    {QUICK_ADD_TYPES.map((type) => (
  • ))}
)} {onOpenCatalog && ( )}
); } // Gleiche DOM-Struktur in beiden Modi, damit react-grid-layout beim // Wechsel (etwa direkt nach dem Erfassen, s = 1) nicht neu einhaengt. // Aeusseres Element: volle Breite (Messung), im skalierten Modus die // sichtbare Hoehe = unskalierte Rasterhoehe x s und `overflow: hidden` — // der transform aendert die Layout-Box nicht, ohne beides bliebe toter // Scrollraum (s < 1) bzw. ragte die 1:1-Breite seitlich hinaus. const outerStyle: React.CSSProperties | undefined = scaled ? { overflow: 'hidden', ...(innerHeight > 0 ? { height: innerHeight * scale } : {}) } : undefined; const innerStyle: React.CSSProperties | undefined = scaled ? { width: canvas.w, transform: `scale(${scale})`, transformOrigin: 'top left' } : undefined; return (
onLayoutChange(allLayouts)} > {widgets.map((widget, index) => { const constraints = WIDGET_CONSTRAINTS[widget.widgetType as WidgetType]; return (
l.i === widget.id) ?? { x: 0, y: 0, w: constraints?.defaultW ?? 8, h: constraints?.defaultH ?? 4, }), minW: constraints?.minW ?? 8, minH: constraints?.minH ?? 4, }} >
); })}
); }