fix(quick-260916-dyv): Mindestgroessen inhaltsgetrieben (kleinste bedienbare Kachel je Typ), gespeicherte Minima ueberschrieben, Stoppuhr-Bedienleiste kompakt

- WIDGET_CONSTRAINTS: Minima aus dem Innenaufbau gerechnet (clock 2/2, search 6/2,
  calendar 3/3, note 4/4, calculator 3/9, favorites 3/3, link 3/2, stopwatch 4/3),
  Vorgaben unveraendert; Test A pinnt alle 32 Werte per toEqual
- dashboard-grid.tsx: effectiveLayouts (useMemo) ueberschreibt minW/minH jedes
  gespeicherten Eintrags in jedem Breakpoint aus den Konstanten und hebt zu
  kleine w/h auf das Minimum an — RGL 2.2.3 nimmt gespeicherte Eintraege
  woertlich (synchronizeLayoutWithChildren) und liest data-grid nur ohne Eintrag
- dashboard-grid.test.tsx: Test 5 angepasst, Test 9 (Ueberschreibung je
  Breakpoint, unbekannter Typ unveraendert) und Test 9b (h 8 -> 9, w >= minW bleibt)
- stopwatch-widget.tsx: Bedienleiste px-2 py-1 text-xs, Zeile gap-1 py-1, damit
  Stop + Runde + Reset in 4 Spalten passen und die Kachel auf 4x3 schrumpfen kann

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
2026-09-16 10:37:55 +02:00
parent ec1b0ce582
commit dc992c9bc6
5 changed files with 181 additions and 33 deletions
@@ -1,6 +1,6 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Responsive, noCompactor } from 'react-grid-layout';
import type { ResponsiveLayouts } from 'react-grid-layout';
import 'react-grid-layout/css/styles.css';
@@ -17,14 +17,72 @@ import { TesseraLogo } from '@/components/brand/tessera-logo';
const BREAKPOINTS = { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 };
const COLS = { lg: 24, md: 20, sm: 12, xs: 8, xxs: 2 };
interface LayoutItemShape {
i: string;
x: number;
y: number;
w: number;
h: number;
minW?: number;
minH?: number;
}
interface DashboardGridProps {
layouts: Record<string, Array<{ i: string; x: number; y: number; w: number; h: number }>>;
layouts: Record<string, Array<LayoutItemShape>>;
widgets: Array<{ id: string; widgetType: string; config: Record<string, unknown> }>;
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<string, Array<LayoutItemShape>>,
widgets: Array<{ id: string; widgetType: string }>,
): Record<string, Array<LayoutItemShape>> {
const typeById = new Map<string, string>();
for (const w of widgets) typeById.set(w.id, w.widgetType);
const result: Record<string, Array<LayoutItemShape>> = {};
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.
@@ -53,6 +111,11 @@ export function DashboardGrid({
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 (
@@ -77,7 +140,7 @@ export function DashboardGrid({
width={width}
breakpoints={BREAKPOINTS}
cols={COLS}
layouts={layouts as ResponsiveLayouts}
layouts={effectiveLayouts as ResponsiveLayouts}
rowHeight={20}
margin={[8, 8] as [number, number]}
// containerPadding bewusst nicht gesetzt: react-grid-layout 2.2.3 nimmt
@@ -98,7 +161,7 @@ export function DashboardGrid({
<div
key={widget.id}
data-grid={{
...(layouts.lg?.find((l) => l.i === widget.id) ?? {
...(effectiveLayouts.lg?.find((l) => l.i === widget.id) ?? {
x: 0,
y: 0,
w: constraints?.defaultW ?? 4,