From 74ed250ad7663c61fe756aefa993ec0784f87469 Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 7 Sep 2026 13:41:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20echtes=20freies=20Grid-Layou?= =?UTF-8?q?t=20mit=20Resize=20statt=20Sortable+Gr=C3=B6=C3=9Fenstufen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @dnd-kit (nur Reihenfolge tauschen) durch react-grid-layout ersetzt - Kacheln lassen sich jetzt frei auf einem 12-Spalten-Raster positionieren (nicht nur umsortieren) UND per Ecke ziehen dynamisch in der Größe ändern (nicht nur zwischen 4 festen Stufen wechseln). Layout-Modell in layout.ts auf x/y/w/h pro Kachel umgestellt (vorher Reihenfolge-Array + Größen-Enum). TileFrame vereinfacht (kein Drag-Griff/Größen-Button mehr nötig, das übernimmt die Grid-Lib direkt an der Kachel). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV --- DEVLOG.md | 13 ++ frontend/package-lock.json | 145 +++++++++++------- frontend/package.json | 4 +- .../src/pages/dashboard/DashboardPage.tsx | 119 +++++++------- frontend/src/pages/dashboard/TileFrame.tsx | 104 ++++--------- frontend/src/pages/dashboard/layout.ts | 74 +++++---- 6 files changed, 228 insertions(+), 231 deletions(-) diff --git a/DEVLOG.md b/DEVLOG.md index acf97e0..3f385bd 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -6573,3 +6573,16 @@ Keine Commits in dieser Session. - frontend/src/pages/dashboard/tiles/QualifikationTile.tsx | 6 ++- --- +## 2026-09-07 13:33 – 13:35 (2m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +- 85df756 fix(frontend): Tailwind-Utilities griffen nicht - global.css war unlayered + +### Geänderte Dateien +- DEVLOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ +- frontend/src/main.tsx | 1 - +- frontend/src/styles/tailwind.css | 9 ++++++++- + +--- diff --git a/frontend/package-lock.json b/frontend/package-lock.json index af5f9b7..2280eaf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,9 +8,6 @@ "name": "mabea-frontend", "version": "0.1.0", "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-slot": "^1.3.3", "@zxing/browser": "^0.1.5", "class-variance-authority": "^0.7.1", @@ -19,6 +16,7 @@ "lucide-react": "^1.42.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-grid-layout": "^2.2.4", "react-router-dom": "^6.26.2", "tailwind-merge": "^3.6.0" }, @@ -1625,59 +1623,6 @@ "node": ">=6.9.0" } }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "license": "MIT", - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/sortable": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", - "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", - "license": "MIT", - "dependencies": { - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.3.0", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -3950,6 +3895,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -5324,6 +5275,15 @@ "node": ">=18" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -5539,6 +5499,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -5564,6 +5535,44 @@ "react": "^18.3.1" } }, + "node_modules/react-draggable": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.7.1.tgz", + "integrity": "sha512-wa3tzfFnYt3yaZLuyU58fl1TNunfWfBekDgWhZA1+gb2jnp42wZ0ymuopR6M5kqDYmm4hKmzGlcKWjZf3Zb6RQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-grid-layout": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.4.tgz", + "integrity": "sha512-Eb57FsgOMYOfsUGrMI1ku/FFR+dPNPrE8qo+3hwZubpqVSy4GO9v52DeX50Tl3JDYAlCypP4rmw7Vrqk/zOIvA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "fast-equals": "^4.0.3", + "prop-types": "^15.8.1", + "react-draggable": "^4.4.6", + "react-resizable": "^3.1.3", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -5574,6 +5583,20 @@ "node": ">=0.10.0" } }, + "node_modules/react-resizable": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.2.0.tgz", + "integrity": "sha512-3NKQ0SLZV7rs3LQHeXlOzDSRQfFrkX6TVet77/Qk03zqiZyee37b7N8/gwDJAA8UUjRz7PdWCCy49hcso45SMQ==", + "license": "MIT", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.5.0" + }, + "peerDependencies": { + "react": ">= 16.3", + "react-dom": ">= 16.3" + } + }, "node_modules/react-router": { "version": "6.30.6", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.6.tgz", @@ -5718,6 +5741,12 @@ "node": ">=0.10.0" } }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -6332,7 +6361,9 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "dev": true, + "license": "0BSD", + "optional": true }, "node_modules/type-fest": { "version": "0.16.0", diff --git a/frontend/package.json b/frontend/package.json index 3df0193..42820dc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,9 +11,6 @@ "test:e2e:ui": "playwright test --ui" }, "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-slot": "^1.3.3", "@zxing/browser": "^0.1.5", "class-variance-authority": "^0.7.1", @@ -22,6 +19,7 @@ "lucide-react": "^1.42.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-grid-layout": "^2.2.4", "react-router-dom": "^6.26.2", "tailwind-merge": "^3.6.0" }, diff --git a/frontend/src/pages/dashboard/DashboardPage.tsx b/frontend/src/pages/dashboard/DashboardPage.tsx index bc475da..388a404 100644 --- a/frontend/src/pages/dashboard/DashboardPage.tsx +++ b/frontend/src/pages/dashboard/DashboardPage.tsx @@ -1,14 +1,14 @@ -import type { DragEndEvent } from "@dnd-kit/core"; -import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; -import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; -import { useCallback, useEffect, useState } from "react"; +import { GridLayout, useContainerWidth } from "react-grid-layout"; +import type { Layout } from "react-grid-layout"; +import "react-grid-layout/css/styles.css"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { apiRequest } from "../../api/client"; import type { HistorieEintrag, Mangel } from "../../api/types"; import { Button } from "../../components/ui/button"; import { cn } from "../../lib/utils"; -import type { DashboardTileGroesse, DashboardTileId } from "./layout"; -import { flexBasisFuerGroesse, GROESSEN_REIHENFOLGE, layoutLaden, layoutSpeichern, STANDARD_REIHENFOLGE } from "./layout"; +import type { DashboardTileId } from "./layout"; +import { ALLE_TILE_IDS, GRID_SPALTEN, GRID_ZEILENHOEHE, layoutLaden, layoutSpeichern, STANDARD_RASTER } from "./layout"; import type { TileAkzent } from "./TileFrame"; import { TileFrame } from "./TileFrame"; import { AblaufdatenTile } from "./tiles/AblaufdatenTile"; @@ -48,6 +48,7 @@ export function DashboardPage() { const [bearbeitenModus, setBearbeitenModus] = useState(false); const [monitorModus, setMonitorModus] = useState(false); const [zuletztAktualisiert, setZuletztAktualisiert] = useState(null); + const { width: gridBreite, containerRef, mounted } = useContainerWidth(); const ladeDaten = useCallback(() => { return Promise.all([ @@ -89,11 +90,6 @@ export function DashboardPage() { return () => document.removeEventListener("fullscreenchange", fullscreenGeaendert); }, []); - const sensoren = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), - useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) - ); - const aktualisiereLayout = useCallback((aendern: (bisherig: typeof layout) => typeof layout) => { setLayout((bisherig) => { const neuesLayout = aendern(bisherig); @@ -102,15 +98,15 @@ export function DashboardPage() { }); }, []); - const dragEnde = useCallback( - (event: DragEndEvent) => { - const { active, over } = event; - if (!over || active.id === over.id) return; + const rasterGeaendert = useCallback( + (neuesRaster: Layout) => { aktualisiereLayout((bisherig) => { - const von = bisherig.reihenfolge.indexOf(active.id as DashboardTileId); - const nach = bisherig.reihenfolge.indexOf(over.id as DashboardTileId); - if (von === -1 || nach === -1) return bisherig; - return { ...bisherig, reihenfolge: arrayMove(bisherig.reihenfolge, von, nach) }; + const raster = { ...bisherig.raster }; + for (const item of neuesRaster) { + const id = item.i as DashboardTileId; + raster[id] = { x: item.x, y: item.y, w: item.w, h: item.h }; + } + return { ...bisherig, raster }; }); }, [aktualisiereLayout] @@ -129,19 +125,8 @@ export function DashboardPage() { [aktualisiereLayout] ); - const groesseAendern = useCallback( - (id: DashboardTileId) => { - aktualisiereLayout((bisherig) => { - const aktuelle = bisherig.groessen[id] ?? "normal"; - const naechsteIndex = (GROESSEN_REIHENFOLGE.indexOf(aktuelle) + 1) % GROESSEN_REIHENFOLGE.length; - return { ...bisherig, groessen: { ...bisherig.groessen, [id]: GROESSEN_REIHENFOLGE[naechsteIndex] } }; - }); - }, - [aktualisiereLayout] - ); - const layoutZuruecksetzen = useCallback(() => { - aktualisiereLayout(() => ({ reihenfolge: STANDARD_REIHENFOLGE, ausgeblendet: [], groessen: {} })); + aktualisiereLayout(() => ({ raster: { ...STANDARD_RASTER }, ausgeblendet: [] })); }, [aktualisiereLayout]); async function monitorUmschalten() { @@ -217,6 +202,13 @@ export function DashboardPage() { } } + const sichtbareIds = ALLE_TILE_IDS.filter((id) => bearbeitenModus || !layout.ausgeblendet.includes(id)); + + const rasterLayout: Layout = useMemo( + () => sichtbareIds.map((id) => ({ i: id, ...layout.raster[id], minW: 2, minH: 2 })), + [sichtbareIds, layout.raster] + ); + return (
{!monitorModus && ( @@ -271,37 +263,42 @@ export function DashboardPage() { ))} )} - {!laedt && bereitschaft && ( - - -
- {layout.reihenfolge.map((id) => { - const ausgeblendet = layout.ausgeblendet.includes(id); - if (ausgeblendet && !bearbeitenModus) return null; - const inhalt = tileInhalt(id); - if (!inhalt) return null; - const groesse: DashboardTileGroesse = layout.groessen[id] ?? (id === "aufgaben" ? "voll" : "normal"); - return ( - ausblendenUmschalten(id)} - onGroesseAendern={() => groesseAendern(id)} - > - {inhalt} - - ); - })} -
-
-
+ {bearbeitenModus && ( +

+ Kachel ziehen zum Verschieben, an der unteren rechten Ecke ziehen zum Vergrößern/Verkleinern. +

)} +
} className="mt-4"> + {!laedt && bereitschaft && mounted && ( + + {sichtbareIds.map((id) => { + const inhalt = tileInhalt(id); + if (!inhalt) return null; + const ausgeblendet = layout.ausgeblendet.includes(id); + return ( + ausblendenUmschalten(id)} + > + {inhalt} + + ); + })} + + )} +
); } diff --git a/frontend/src/pages/dashboard/TileFrame.tsx b/frontend/src/pages/dashboard/TileFrame.tsx index 858c65c..95ea779 100644 --- a/frontend/src/pages/dashboard/TileFrame.tsx +++ b/frontend/src/pages/dashboard/TileFrame.tsx @@ -1,12 +1,8 @@ -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { memo } from "react"; -import type { ReactNode } from "react"; +import { forwardRef, memo, type HTMLAttributes, type ReactNode } from "react"; import { Button } from "../../components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"; import { cn } from "../../lib/utils"; -import { GROESSE_LABEL, type DashboardTileGroesse, type DashboardTileId } from "./layout"; export type TileAkzent = "neutral" | "warning" | "danger"; @@ -16,84 +12,50 @@ const AKZENT_KLASSE: Record = { danger: "border-l-danger", }; -interface Props { - id: DashboardTileId; +interface Props extends HTMLAttributes { title: string; - flexBasis: string; - groesse: DashboardTileGroesse; akzent?: TileAkzent; bearbeitenAktiv: boolean; ausgeblendet: boolean; - onAusblendenUmschalten: (id: DashboardTileId) => void; - onGroesseAendern: (id: DashboardTileId) => void; + onAusblendenUmschalten: () => void; children: ReactNode; } -function TileFrameKomponente({ - id, - title, - flexBasis, - groesse, - akzent = "neutral", - bearbeitenAktiv, - ausgeblendet, - onAusblendenUmschalten, - onGroesseAendern, - children, -}: Props) { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id, - disabled: !bearbeitenAktiv, - }); - - return ( - - -
- {bearbeitenAktiv && ( - - )} +// forwardRef + generische DOM-Props nötig, react-grid-layout reicht Style/Events +// (Drag/Resize-Handler) direkt an die Kachel-Wurzel durch. +export const TileFrame = memo( + forwardRef(function TileFrame( + { title, akzent = "neutral", bearbeitenAktiv, ausgeblendet, onAusblendenUmschalten, children, className, ...rest }, + ref + ) { + return ( + + {title} -
- {bearbeitenAktiv && ( -
+ {bearbeitenAktiv && ( - -
- )} -
- {!ausgeblendet && {children}} -
- ); -} - -export const TileFrame = memo(TileFrameKomponente); + )} + + {!ausgeblendet && {children}} + + ); + }) +); diff --git a/frontend/src/pages/dashboard/layout.ts b/frontend/src/pages/dashboard/layout.ts index b3b66da..eb52bff 100644 --- a/frontend/src/pages/dashboard/layout.ts +++ b/frontend/src/pages/dashboard/layout.ts @@ -8,7 +8,7 @@ export type DashboardTileId = | "qualifikation" | "aktivitaet"; -export const STANDARD_REIHENFOLGE: DashboardTileId[] = [ +export const ALLE_TILE_IDS: DashboardTileId[] = [ "aufgaben", "einsatzbereitschaft", "prueftermine", @@ -19,65 +19,61 @@ export const STANDARD_REIHENFOLGE: DashboardTileId[] = [ "aktivitaet", ]; -export type DashboardTileGroesse = "schmal" | "normal" | "breit" | "voll"; +export const GRID_SPALTEN = 12; +export const GRID_ZEILENHOEHE = 56; -export const GROESSEN_REIHENFOLGE: DashboardTileGroesse[] = ["schmal", "normal", "breit", "voll"]; +export interface RasterPosition { + x: number; + y: number; + w: number; + h: number; +} -export const GROESSE_LABEL: Record = { - schmal: "Schmal", - normal: "Normal", - breit: "Breit", - voll: "Volle Breite", +// Freie Grid-Positionen als Startzustand - grob nach Wichtigkeit sortiert, +// Aufgaben-Kachel volle Breite oben. +export const STANDARD_RASTER: Record = { + aufgaben: { x: 0, y: 0, w: 12, h: 3 }, + einsatzbereitschaft: { x: 0, y: 3, w: 4, h: 5 }, + prueftermine: { x: 4, y: 3, w: 4, h: 5 }, + ablaufdaten: { x: 8, y: 3, w: 4, h: 5 }, + kennzahlen: { x: 0, y: 8, w: 4, h: 5 }, + maengel: { x: 4, y: 8, w: 4, h: 5 }, + qualifikation: { x: 8, y: 8, w: 4, h: 4 }, + aktivitaet: { x: 0, y: 13, w: 8, h: 5 }, }; export interface DashboardLayout { - reihenfolge: DashboardTileId[]; + raster: Record; ausgeblendet: DashboardTileId[]; - groessen: Partial>; } -const SPEICHER_KEY = "mabea-dashboard-layout"; +const SPEICHER_KEY = "mabea-dashboard-layout-v2"; function istGueltigeId(wert: unknown): wert is DashboardTileId { - return typeof wert === "string" && (STANDARD_REIHENFOLGE as string[]).includes(wert); + return typeof wert === "string" && (ALLE_TILE_IDS as string[]).includes(wert); } -function istGueltigeGroesse(wert: unknown): wert is DashboardTileGroesse { - return typeof wert === "string" && (GROESSEN_REIHENFOLGE as string[]).includes(wert); +function istGueltigePosition(wert: unknown): wert is RasterPosition { + if (!wert || typeof wert !== "object") return false; + const p = wert as Record; + return typeof p.x === "number" && typeof p.y === "number" && typeof p.w === "number" && typeof p.h === "number"; } export function layoutLaden(): DashboardLayout { try { const roh = localStorage.getItem(SPEICHER_KEY); - if (!roh) return { reihenfolge: STANDARD_REIHENFOLGE, ausgeblendet: [], groessen: {} }; + if (!roh) return { raster: { ...STANDARD_RASTER }, ausgeblendet: [] }; const geparst = JSON.parse(roh); - const gespeicherteReihenfolge = Array.isArray(geparst.reihenfolge) - ? geparst.reihenfolge.filter(istGueltigeId) - : []; - const fehlende = STANDARD_REIHENFOLGE.filter((id) => !gespeicherteReihenfolge.includes(id)); - const ausgeblendet = Array.isArray(geparst.ausgeblendet) ? geparst.ausgeblendet.filter(istGueltigeId) : []; - const groessen: Partial> = {}; - if (geparst.groessen && typeof geparst.groessen === "object") { - for (const [id, groesse] of Object.entries(geparst.groessen)) { - if (istGueltigeId(id) && istGueltigeGroesse(groesse)) groessen[id] = groesse; + const raster: Record = { ...STANDARD_RASTER }; + if (geparst.raster && typeof geparst.raster === "object") { + for (const [id, position] of Object.entries(geparst.raster)) { + if (istGueltigeId(id) && istGueltigePosition(position)) raster[id] = position; } } - return { reihenfolge: [...gespeicherteReihenfolge, ...fehlende], ausgeblendet, groessen }; + const ausgeblendet = Array.isArray(geparst.ausgeblendet) ? geparst.ausgeblendet.filter(istGueltigeId) : []; + return { raster, ausgeblendet }; } catch { - return { reihenfolge: STANDARD_REIHENFOLGE, ausgeblendet: [], groessen: {} }; - } -} - -export function flexBasisFuerGroesse(groesse: DashboardTileGroesse): string { - switch (groesse) { - case "schmal": - return "14rem"; - case "breit": - return "28rem"; - case "voll": - return "100%"; - default: - return "18rem"; + return { raster: { ...STANDARD_RASTER }, ausgeblendet: [] }; } }