fix(dashboard): Rasterversion schuetzen, Kalender-Mindestbreite, Breiten je Breakpoint, Hintergrund
- Layout mit neuerer Rasterversion wird nie gespeichert, Bearbeiten gesperrt mit Hinweis - Kalender minW 11 (~260 px bei lg), Breiten je Breakpoint auf Spaltenzahl begrenzt - optimistische Ruecksetzung nur, wenn noch der gesetzte Wert steht - Titel-Schalter mit fester Beschriftung + aria-pressed - Hintergrund-Dialog: Fokus rein/zurueck, Tab bleibt im Dialog - Loeschen eines Bildes setzt eine darauf zeigende Hintergrund-Wahl zurueck - Uebersetzungen und CHANGELOG Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Responsive, noCompactor } from 'react-grid-layout';
|
||||
import type { Compactor, ResponsiveLayouts } from 'react-grid-layout';
|
||||
import { noCompactor, Responsive } from 'react-grid-layout';
|
||||
import { defaultConstraints, type LayoutConstraint } from 'react-grid-layout/core';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { WIDGET_CONSTRAINTS, WIDGET_REGISTRY, type WidgetType } from './widget-registry';
|
||||
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';
|
||||
|
||||
@@ -23,7 +29,9 @@ import { WidgetWrapper } from './widgets/widget-wrapper';
|
||||
// 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 };
|
||||
const COLS = { lg: 48, md: 40, sm: 24, xs: 16, xxs: 4 };
|
||||
// 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).
|
||||
@@ -138,6 +146,17 @@ const QUICK_ADD_TYPES: WidgetType[] = ['clock', 'calendar', 'note'];
|
||||
* 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<string, Array<LayoutItemShape>>,
|
||||
@@ -150,18 +169,42 @@ function applyConstraintMinima(
|
||||
for (const key of Object.keys(layouts)) {
|
||||
const entries = layouts[key];
|
||||
if (!Array.isArray(entries)) continue;
|
||||
result[key] = entries.map((entry) => {
|
||||
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<string, number>();
|
||||
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: Math.max(entry.w, constraints.minW),
|
||||
w,
|
||||
h: Math.max(entry.h, constraints.minH),
|
||||
minW: constraints.minW,
|
||||
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;
|
||||
}
|
||||
@@ -259,7 +302,10 @@ export function DashboardGrid({
|
||||
// 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]);
|
||||
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
|
||||
@@ -269,7 +315,18 @@ export function DashboardGrid({
|
||||
<div className="flex min-h-[55vh] flex-col items-center justify-center px-4 text-center">
|
||||
<div className="dashboard-chrome flex max-w-md flex-col items-center rounded-xl px-6 py-8">
|
||||
<span className="mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.75"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="9" rx="1.5" />
|
||||
<rect x="14" y="3" width="7" height="5" rx="1.5" />
|
||||
<rect x="14" y="12" width="7" height="9" rx="1.5" />
|
||||
@@ -296,7 +353,19 @@ export function DashboardGrid({
|
||||
)}
|
||||
{onOpenCatalog && (
|
||||
<button type="button" onClick={onOpenCatalog} className="btn btn-primary mt-5">
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M12 5v14M5 12h14" /></svg>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
>
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
{t('addWidget')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user