feat(dashboard): auf jedem Bildschirm dasselbe Bild – Leinwand je Reiter, massstaeblich skaliert
Je Reiter wird die Flaeche des ersten Desktop-Bildschirms als __canvas im Layout-JSON gespeichert (ohne API/DB-Aenderung, GRID_VERSION bleibt 3). Das Raster rendert in Leinwandbreite und wird per transform: scale(min(bw/cw, bh/ch)) eingepasst, Schrift eingeschlossen, ohne Scrollen; unter 768 px wie bisher. Ziehen/Groesse aendern unter Skalierung ueber eine eigene Positionsstrategie (createScaledStrategy aus react-grid-layout 2.2.3 rechnet den Rasterversatz falsch). Im Browser nachgewiesen: 1920x1080 -> 1366x768 (Faktor 0,66), Ziehen +200 px folgt der Maus, 700 px ohne Skalierung. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,19 @@
|
||||
* wuerde sie mit der alten Marke zurueckschreiben, und der neue Stand wuerde
|
||||
* sie beim naechsten Laden ein zweites Mal skalieren.
|
||||
*
|
||||
* Leinwand (Bezugsgroesse, "wie ein Bild mitskalieren"): Zusaetzlich kann das
|
||||
* JSON den Schluessel `__canvas: { w, h }` tragen — die verfuegbare Flaeche
|
||||
* (Pixel), auf der die Anordnung eingerichtet wurde. Das Raster zeichnet
|
||||
* sich auf JEDEM Desktop-Bildschirm mit genau dieser Breite und skaliert das
|
||||
* Ergebnis per `transform: scale` in die dortige Flaeche (siehe
|
||||
* dashboard-grid.tsx). Wie der Marker lebt `__canvas` nur im JSON, nicht in
|
||||
* `layouts`: `migrateGridLayouts` liefert ihn getrennt als `canvas`,
|
||||
* `withGridVersion(layouts, canvas)` schreibt ihn zurueck — JEDER
|
||||
* Speicherpfad muss ihn mitgeben, sonst geht er verloren. Kein neuer
|
||||
* `GRID_VERSION`: aeltere Programmstaende ueberspringen den Schluessel, weil
|
||||
* sein Wert kein Array ist (`!Array.isArray(value)` unten) — und schreiben
|
||||
* ihn beim Speichern nicht zurueck; dann wird er einfach neu erfasst.
|
||||
*
|
||||
* Ort: Frontend, weil die Raster-Einheiten Frontend-Konstanten sind, die API
|
||||
* das JSON nur durchreicht (`@IsObject()`) und so kein Schreiben auf einem
|
||||
* GET und keine Aenderung am API-Dienst noetig ist. Reine Funktionen ohne
|
||||
@@ -50,6 +63,17 @@
|
||||
export const GRID_VERSION = 3;
|
||||
export const GRID_VERSION_KEY = '__gridVersion';
|
||||
export const GRID_SCALE_FACTOR = 2;
|
||||
export const GRID_CANVAS_KEY = '__canvas';
|
||||
/** Unter dieser Breite gilt die Handy-/Schmalansicht, dort wird nicht skaliert. */
|
||||
export const CANVAS_MIN_WIDTH = 768;
|
||||
/** Kleinere Hoehen sind Messfehler (Fenster kurz zusammengeklappt o. ae.). */
|
||||
export const CANVAS_MIN_HEIGHT = 200;
|
||||
|
||||
/** Bezugsflaeche in Pixeln, auf der die Anordnung eingerichtet wurde. */
|
||||
export interface GridCanvas {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface GridLayoutItem {
|
||||
i: string;
|
||||
@@ -74,6 +98,19 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prueft eine Leinwand: endliche Zahlen, w >= CANVAS_MIN_WIDTH,
|
||||
* h >= CANVAS_MIN_HEIGHT. Alles andere -> null (wird dann neu erfasst).
|
||||
*/
|
||||
export function parseGridCanvas(value: unknown): GridCanvas | null {
|
||||
if (!isPlainObject(value)) return null;
|
||||
const { w, h } = value;
|
||||
if (typeof w !== 'number' || typeof h !== 'number') return null;
|
||||
if (!Number.isFinite(w) || !Number.isFinite(h)) return null;
|
||||
if (w < CANVAS_MIN_WIDTH || h < CANVAS_MIN_HEIGHT) return null;
|
||||
return { w, h };
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechnet eine rohe (aus der API geladene) Anordnung in die aktuellen
|
||||
* Raster-Einheiten um. Liefert die Anordnung OHNE Marker und die Angabe, ob
|
||||
@@ -84,9 +121,11 @@ export function migrateGridLayouts(raw: unknown): {
|
||||
migrated: boolean;
|
||||
/** Marke hoeher als `GRID_VERSION` — Anordnung stammt von einem neueren Programmstand. */
|
||||
newer: boolean;
|
||||
/** Gueltige Leinwand aus `__canvas`, sonst null. */
|
||||
canvas: GridCanvas | null;
|
||||
} {
|
||||
if (!isPlainObject(raw)) {
|
||||
return { layouts: {}, migrated: false, newer: false };
|
||||
return { layouts: {}, migrated: false, newer: false, canvas: null };
|
||||
}
|
||||
|
||||
const markerValue = raw[GRID_VERSION_KEY];
|
||||
@@ -100,7 +139,7 @@ export function migrateGridLayouts(raw: unknown): {
|
||||
let migrated = false;
|
||||
|
||||
for (const key of Object.keys(raw)) {
|
||||
if (key === GRID_VERSION_KEY) continue;
|
||||
if (key === GRID_VERSION_KEY || key === GRID_CANVAS_KEY) continue;
|
||||
const value = raw[key];
|
||||
if (!Array.isArray(value)) continue;
|
||||
|
||||
@@ -121,12 +160,18 @@ export function migrateGridLayouts(raw: unknown): {
|
||||
});
|
||||
}
|
||||
|
||||
return { layouts, migrated, newer };
|
||||
return { layouts, migrated, newer, canvas: parseGridCanvas(raw[GRID_CANVAS_KEY]) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Haengt den Marker fuer das Speichern an, ohne die Eingabe zu veraendern.
|
||||
* Haengt den Marker (und, falls vorhanden, die Leinwand) fuer das Speichern
|
||||
* an, ohne die Eingabe zu veraendern.
|
||||
*/
|
||||
export function withGridVersion(layouts: GridLayouts): Record<string, unknown> {
|
||||
return { ...layouts, [GRID_VERSION_KEY]: GRID_VERSION };
|
||||
export function withGridVersion(
|
||||
layouts: GridLayouts,
|
||||
canvas?: GridCanvas | null,
|
||||
): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = { ...layouts, [GRID_VERSION_KEY]: GRID_VERSION };
|
||||
if (canvas) result[GRID_CANVAS_KEY] = { w: canvas.w, h: canvas.h };
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user