feat(260929-dmx): Widget-Breiten im 48er-Raster, Kalender schmaler ziehbar

- minW/defaultW aller Widgets verdoppelt (gleiche Bildschirmbreite)
- Kalender minW 8 (rund 250 px), defaultW 16
- Test: bestehender Kalender bekommt das neue Minimum in jedem Breakpoint

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 09:53:08 +02:00
parent 97744b59cd
commit 9c9e1420fe
4 changed files with 100 additions and 51 deletions
@@ -233,8 +233,9 @@ describe('DashboardGrid', () => {
const first = children[0];
expect(isValidElement(first)).toBe(true);
const props = (first as React.ReactElement<Record<string, unknown>>).props;
// quick-260916-dyv: Uhr-Minimum 2x2 (vorher verdoppelt 4x4), Vorgabe 4x4 unveraendert.
expect(props['data-grid']).toEqual({ x: 0, y: 0, w: 4, h: 4, minW: 2, minH: 2 });
// quick-260916-dyv: Uhr-Minimum 2 Zeilen hoch; quick-260929-dmx: Breiten im
// 48er-Raster verdoppelt (Minimum 4, Vorgabe 8), Hoehen unveraendert.
expect(props['data-grid']).toEqual({ x: 0, y: 0, w: 8, h: 4, minW: 4, minH: 2 });
});
it('quick-260916-dyv Test 9: gespeicherte minW/minH werden in JEDEM Breakpoint aus WIDGET_CONSTRAINTS ueberschrieben, x/y/w/h bleiben, unbekannte Typen und Schluessel unveraendert', async () => {
@@ -265,9 +266,9 @@ describe('DashboardGrid', () => {
const passed = captured.props?.layouts as Record<string, Array<Record<string, unknown>>>;
expect(Object.keys(passed)).toEqual(Object.keys(layouts));
expect(passed.lg[0]).toEqual({ i: 'inst-1', x: 2, y: 4, w: 6, h: 6, minW: 2, minH: 2 });
expect(passed.md[0]).toEqual({ i: 'inst-1', x: 0, y: 0, w: 4, h: 4, minW: 2, minH: 2 });
expect(passed.md[0].minW).toBe(2);
expect(passed.lg[0]).toEqual({ i: 'inst-1', x: 2, y: 4, w: 6, h: 6, minW: 4, minH: 2 });
expect(passed.md[0]).toEqual({ i: 'inst-1', x: 0, y: 0, w: 4, h: 4, minW: 4, minH: 2 });
expect(passed.md[0].minW).toBe(4);
// Unbekannter Typ: Eintrag bleibt woertlich erhalten (kein Absturz, T-DYV-01).
expect(passed.lg[1]).toEqual({ i: 'inst-x', x: 10, y: 0, w: 5, h: 5, minW: 7, minH: 7 });
expect(passed.sm).toEqual([]);
@@ -278,9 +279,9 @@ describe('DashboardGrid', () => {
const { DashboardGrid } = await import('./dashboard-grid');
const layouts = {
lg: [
// Rechner: gespeichert mit h 8 (bwo-Minimum), neues Minimum 3x9 -> h wird 9, w 6 bleibt.
// Rechner: gespeichert mit h 8 (bwo-Minimum), neues Minimum 6x10 -> h wird 10, w 6 bleibt.
{ i: 'calc-1', x: 0, y: 0, w: 6, h: 8, minW: 4, minH: 8 },
// Suche: gespeichert mit w 4 (unter minW 6) -> w wird 6, h 4 bleibt.
// Suche: gespeichert mit w 4 (unter minW 12) -> w wird 12, h 4 bleibt.
{ i: 'srch-1', x: 6, y: 0, w: 4, h: 4, minW: 6, minH: 4 },
],
md: [{ i: 'calc-1', x: 0, y: 0, w: 3, h: 7, minW: 4, minH: 8 }],
@@ -302,15 +303,53 @@ describe('DashboardGrid', () => {
);
const passed = captured.props?.layouts as Record<string, Array<Record<string, unknown>>>;
expect(passed.lg[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 6, h: 10, minW: 3, minH: 10 });
expect(passed.lg[1]).toEqual({ i: 'srch-1', x: 6, y: 0, w: 6, h: 4, minW: 6, minH: 2 });
// Jeder Breakpoint: md-Rechner 3x7 -> 3x10 (w schon = minW, h angehoben).
expect(passed.md[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 3, h: 10, minW: 3, minH: 10 });
expect(passed.lg[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 6, h: 10, minW: 6, minH: 10 });
expect(passed.lg[1]).toEqual({ i: 'srch-1', x: 6, y: 0, w: 12, h: 4, minW: 12, minH: 2 });
// Jeder Breakpoint: md-Rechner 3x7 -> 6x10 (w und h auf das Minimum angehoben).
expect(passed.md[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 6, h: 10, minW: 6, minH: 10 });
// Eingabe unveraendert (keine Mutation des Store-Objekts).
expect(layouts.lg[0].h).toBe(8);
expect(layouts.lg[0].minH).toBe(8);
});
it('quick-260929-dmx Test 9c: bestehender Kalender (migrierte Werte minW 12, w 16) bekommt minW 8 in JEDEM Breakpoint und kann schmaler gezogen werden; zu schmale Breite wird auf 8 angehoben', async () => {
captured.props = null;
const { DashboardGrid } = await import('./dashboard-grid');
// So sieht ein aus v2 (minW 6 x2) migrierter Kalender im Zustand aus.
const layouts = {
lg: [
{ i: 'cal-1', x: 4, y: 0, w: 16, h: 16, minW: 12, minH: 8 },
{ i: 'cal-2', x: 24, y: 0, w: 6, h: 16, minW: 12, minH: 8 },
],
md: [{ i: 'cal-1', x: 0, y: 0, w: 16, h: 16, minW: 12, minH: 8 }],
sm: [],
xs: [],
xxs: [],
};
render(
<DashboardGrid
layouts={layouts}
widgets={[
{ id: 'cal-1', widgetType: 'calendar', config: {} },
{ id: 'cal-2', widgetType: 'calendar', config: {} },
]}
isEditMode
onLayoutChange={vi.fn()}
onRemoveWidget={vi.fn()}
/>,
);
const passed = captured.props?.layouts as Record<string, Array<Record<string, unknown>>>;
// Breite bleibt (Bildschirmgroesse unveraendert), nur das Minimum sinkt.
expect(passed.lg[0]).toEqual({ i: 'cal-1', x: 4, y: 0, w: 16, h: 16, minW: 8, minH: 8 });
expect(passed.md[0]).toEqual({ i: 'cal-1', x: 0, y: 0, w: 16, h: 16, minW: 8, minH: 8 });
expect(passed.lg[1]).toEqual({ i: 'cal-2', x: 24, y: 0, w: 8, h: 16, minW: 8, minH: 8 });
// Auch der data-grid-Wert der Kinder traegt das neue Minimum.
const children = Children.toArray(captured.props?.children as React.ReactNode);
const grid = (children[0] as React.ReactElement<Record<string, unknown>>).props['data-grid'] as Record<string, unknown>;
expect(grid.minW).toBe(8);
});
const CANCEL = 'input, textarea, select, button, a, [contenteditable], [data-no-drag], .widgetNoDrag';
it('quick-260916-dyv Test 6: dragConfig-Pin — handle Karte, cancel fuer Interaktives, threshold 3; resizeConfig folgt dem Bearbeitungsmodus', async () => {
@@ -67,21 +67,21 @@ describe('WIDGET_CONSTRAINTS (DASH-11)', () => {
});
it('Test A (quick-260916-dyv): Minima = kleinste bedienbare Kachel je Typ, Vorgaben unveraendert (quick-260916-iex: Link-Widget entfernt; quick-260921-pi9: Bilderrahmen dazu; quick-260921-qd3: XFrame dazu; quick-260924-i8v: Proxmox dazu, zehn Typen)', () => {
// Raster 24 Spalten / 20 px (quick-260916-bwo). Die Minima sind seit
// quick-260916-dyv inhaltsgetrieben (Rechnung im Plan), die Vorgaben
// (defaultW/defaultH) bleiben das Doppelte der alten 12-Spalten-Werte.
// 3 und 9 sind ungerade — die bwo-Pruefung "jeder Wert ist gerade" entfaellt.
// Raster 48 Spalten / 20 px (quick-260929-dmx, vorher 24 Spalten). Alle
// Breitenwerte sind gegenueber dem 24er-Raster verdoppelt (gleiche
// Bildschirmbreite), Hoehen unveraendert; einzige Ausnahme Kalender-minW 8
// (rund 250 px, gemessene kleinste benutzbare Breite; vorher 6 von 24).
expect(WIDGET_CONSTRAINTS).toEqual({
clock: { minW: 2, minH: 2, defaultW: 4, defaultH: 4 },
search: { minW: 6, minH: 2, defaultW: 12, defaultH: 4 },
calendar: { minW: 6, minH: 8, defaultW: 8, defaultH: 16 },
note: { minW: 4, minH: 4, defaultW: 6, defaultH: 8 },
calculator: { minW: 3, minH: 10, defaultW: 6, defaultH: 10 },
favorites: { minW: 1, minH: 3, defaultW: 6, defaultH: 10 },
stopwatch: { minW: 4, minH: 3, defaultW: 6, defaultH: 6 },
'picture-frame': { minW: 4, minH: 4, defaultW: 8, defaultH: 12 },
xframe: { minW: 4, minH: 4, defaultW: 12, defaultH: 12 },
proxmox: { minW: 3, minH: 4, defaultW: 8, defaultH: 8 },
clock: { minW: 4, minH: 2, defaultW: 8, defaultH: 4 },
search: { minW: 12, minH: 2, defaultW: 24, defaultH: 4 },
calendar: { minW: 8, minH: 8, defaultW: 16, defaultH: 16 },
note: { minW: 8, minH: 4, defaultW: 12, defaultH: 8 },
calculator: { minW: 6, minH: 10, defaultW: 12, defaultH: 10 },
favorites: { minW: 2, minH: 3, defaultW: 12, defaultH: 10 },
stopwatch: { minW: 8, minH: 3, defaultW: 12, defaultH: 6 },
'picture-frame': { minW: 8, minH: 4, defaultW: 16, defaultH: 12 },
xframe: { minW: 8, minH: 4, defaultW: 24, defaultH: 12 },
proxmox: { minW: 6, minH: 4, defaultW: 16, defaultH: 8 },
});
let counted = 0;
@@ -40,45 +40,55 @@ export const WIDGET_CONSTRAINTS: Record<
{ minW: number; minH: number; defaultW: number; defaultH: number }
> = {
// quick-260916-dyv: minW/minH = kleinste noch bedienbare Kachel je Typ im
// 24-Spalten/20-px-Raster, aus dem Innenaufbau gerechnet (Suche: Auswahl 120 +
// Raster, aus dem Innenaufbau gerechnet (Suche: Auswahl 120 +
// Eingabe + Knopf; Rechner: Anzeige 40 + Speicherzeile 28 + 5 Tastenreihen 28 =
// 268 px -> 10 Zeilen; Stoppuhr: kompakte Bedienleiste). defaultW/defaultH =
// altes 12-Spalten-Mass x2, unveraendert. Gespeicherte minW/minH werden in
// dashboard-grid.tsx aus dieser Tabelle ueberschrieben.
// quick-260929-dmx: Raster in der Breite doppelt so fein (48 Spalten am
// lg-Breakpoint). Alle Breitenwerte (minW, defaultW) sind gegenueber dem
// 24-Spalten-Raster verdoppelt, die Kacheln behalten ihre Bildschirmbreite;
// Hoehen (minH, defaultH) unveraendert. Einzige Ausnahme: Kalender-minW.
// Existing widgets
clock: { minW: 2, minH: 2, defaultW: 4, defaultH: 4 },
search: { minW: 6, minH: 2, defaultW: 12, defaultH: 4 },
clock: { minW: 4, minH: 2, defaultW: 8, defaultH: 4 },
search: { minW: 12, minH: 2, defaultW: 24, defaultH: 4 },
// quick-260916-htc: Monatsraster braucht Breite fuer 7 Spalten und Hoehe
// fuer Nav-Zeile + Wochentagskopf + 6 Rasterzeilen + Naechste-Termine-Block.
// Runde 3 (Design „Mosaik“): defaultH 16 statt 12 — mit 12 passte unter
// den Monat nur ein Termin.
calendar: { minW: 6, minH: 8, defaultW: 8, defaultH: 16 },
note: { minW: 4, minH: 4, defaultW: 6, defaultH: 8 },
// quick-260929-dmx: auf Nutzerwunsch (29.09.) schmaler ziehbar — minW 8 von
// 48 Spalten = rund 250 px am lg-Breakpoint (gemessen: Monatsraster,
// Ueberschrift und Terminliste sind dort noch benutzbar; bei rund 185 px
// wird die Ueberschrift abgeschnitten). defaultW 16 = unveraenderte Breite.
calendar: { minW: 8, minH: 8, defaultW: 16, defaultH: 16 },
note: { minW: 8, minH: 4, defaultW: 12, defaultH: 8 },
// Phase 8 new widgets (D-01)
// minH 10, nicht 9 (260916-dyv, Browser-Messung des Orchestrators): der Rechner hat
// SECHS Tastenreihen (%/CE/C/Loeschen, 1/x/x2/Wurzel/Division, 7-9, 4-6, 1-3, 0/Komma/=),
// Inhalt 268 px; bei 9 Zeilen (244 px) fehlte die unterste Reihe um 25 px.
calculator: { minW: 3, minH: 10, defaultW: 6, defaultH: 10 },
// Favoriten: minW 1 statt 3 (Nutzerwunsch 23.09.) — bei kurzen Linknamen
// blieb rechts viel Leerraum. Der Titel kuerzt mit Auslassungszeichen,
// in der schmalsten Stufe bleibt das Symbol als Leiste stehen.
favorites: { minW: 1, minH: 3, defaultW: 6, defaultH: 10 },
stopwatch: { minW: 4, minH: 3, defaultW: 6, defaultH: 6 },
// quick-260921-pi9: ein Bild braucht Flaeche — 8x12 entspricht der
// Kalender-Vorgabe (rund 530x330 px, Querformat wie ein Foto); 8x8 war im
// Browser-Rundgang mit 216 px Hoehe zu flach. 4x4 ist die kleinste Kachel,
// in der Bild und Unterschrift-Streifen noch erkennbar sind.
'picture-frame': { minW: 4, minH: 4, defaultW: 8, defaultH: 12 },
// quick-260921-qd3: eine Webseite braucht Breite UND Hoehe — 12x12 = halbe
// Rasterbreite bei rund 330 px Hoehe (8 Zeilen waeren nur 216 px, zu flach
// fuer eine Seite; vgl. Bilderrahmen 8x12). 4x4 kleinste Kachel, in der ein
// Rahmen noch Sinn hat.
xframe: { minW: 4, minH: 4, defaultW: 12, defaultH: 12 },
calculator: { minW: 6, minH: 10, defaultW: 12, defaultH: 10 },
// Favoriten: minW 1 (im 24er-Raster, jetzt 2 von 48) statt 3 (Nutzerwunsch
// 23.09.) — bei kurzen Linknamen blieb rechts viel Leerraum. Der Titel
// kuerzt mit Auslassungszeichen, in der schmalsten Stufe bleibt das Symbol
// als Leiste stehen.
favorites: { minW: 2, minH: 3, defaultW: 12, defaultH: 10 },
stopwatch: { minW: 8, minH: 3, defaultW: 12, defaultH: 6 },
// quick-260921-pi9: ein Bild braucht Flaeche — 16x12 (im 48er-Raster)
// entspricht der Kalender-Vorgabe (rund 530x330 px, Querformat wie ein
// Foto); Hoehe 8 war im Browser-Rundgang mit 216 px zu flach. 8x4 ist die
// kleinste Kachel, in der Bild und Unterschrift-Streifen noch erkennbar sind.
'picture-frame': { minW: 8, minH: 4, defaultW: 16, defaultH: 12 },
// quick-260921-qd3: eine Webseite braucht Breite UND Hoehe — 24x12 im
// 48er-Raster = halbe Rasterbreite bei rund 330 px Hoehe (8 Zeilen waeren
// nur 216 px, zu flach fuer eine Seite; vgl. Bilderrahmen). 8x4 kleinste
// Kachel, in der ein Rahmen noch Sinn hat.
xframe: { minW: 8, minH: 4, defaultW: 24, defaultH: 12 },
// quick-260924-i8v: 4 Zeilen = 104 px reichen genau fuer Balken und
// Zusammenfassung (die Serverliste blendet sich darunter per Container-
// Query aus); 8x8 = rund 456x216 px bei 1400 px Breite zeigt etwa sechs
// Serverzeilen; 3 Spalten = rund 166 px zeigen nur Punkte und Namen.
proxmox: { minW: 3, minH: 4, defaultW: 8, defaultH: 8 },
// Query aus); 16x8 (im 48er-Raster) = rund 456x216 px bei 1400 px Breite
// zeigt etwa sechs Serverzeilen; 6 Spalten = rund 166 px zeigen nur Punkte
// und Namen.
proxmox: { minW: 6, minH: 4, defaultW: 16, defaultH: 8 },
};
/**
@@ -143,7 +143,7 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
expect(errorSpy).toHaveBeenCalledTimes(1);
});
it('Test 6: neues Widget wird in den neuen (verdoppelten) Vorgabegroessen fuer den aktiven Reiter angelegt', async () => {
it('Test 6: neues Widget wird in den Vorgabegroessen des 48er-Rasters (quick-260929-dmx) fuer den aktiven Reiter angelegt', async () => {
await useDashboardStore.getState().loadDashboard();
vi.mocked(api.addWidget).mockResolvedValue({ id: 'n1', widgetType: 'clock', config: {} });
@@ -151,7 +151,7 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
const state = useDashboardStore.getState();
expect(api.addWidget).toHaveBeenCalledWith('dash-1', 'clock');
expect(state.layouts.lg).toContainEqual({ i: 'n1', x: 0, y: 0, w: 4, h: 4 });
expect(state.layouts.lg).toContainEqual({ i: 'n1', x: 0, y: 0, w: 8, h: 4 });
expect(state.isDirty).toBe(true);
});
});