Compare commits
5 Commits
6c5946ca6e
...
v1.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
| ad004b286d | |||
| 39b1f74b56 | |||
| cf67c8a389 | |||
| d9f2af32d6 | |||
| 3e8c0f4ef5 |
+4
-3
@@ -5,9 +5,9 @@ current_phase: 18
|
||||
current_phase_name: desktop-client-fertigstellen
|
||||
status: verified
|
||||
stopped_at: "22.09.2026: 1.3.0 freigegeben; danach quick-260922-hk4 — Bilderrahmen-Bilder liegen jetzt im Dateibereich (user-files) statt in der Datenbank, Umzug laeuft automatisch beim Start, Selbstheilung aus der alten data-Spalte eingebaut; im Browser nachgewiesen. NAECHSTER SCHRITT, vom Nutzer noch nicht bestaetigt: (1) einmaliges Aufraeumen, damit ein Modul seine Dashboard-Kachel selbst mitbringt (heute sieben Hartkodierungen je Kachel; Katalog zeigt auch Kacheln gesperrter Module; gesperrte Kachel bleibt leer statt zu erklaeren) — das Geruest WIDGET_MODULE_MAP existiert und ist leer; (2) danach das Proxmox-Modul (PVE/PBS/PMG) und seine Kachel. Offen beim Nutzer: Live-Server auf 1.3.0 ziehen, neuen Client per Browser installieren."
|
||||
last_updated: "2026-09-22T14:15:00.000Z"
|
||||
last_activity: 2026-09-21
|
||||
last_activity_desc: Quick 260922-m1h — Widget-Typen an einer Stelle, Katalog filtert nach Modulzugriff, Kachel kennt ihr Modul (Vorarbeit Proxmox); im Browser als verhaltensneutral bestaetigt
|
||||
last_updated: "2026-09-22T21:10:00.000Z"
|
||||
last_activity: 2026-09-22
|
||||
last_activity_desc: Quick 260922-vdk — Dashboard-Raster misst seine Breite auch aus dem Leerzustand; im echten Linux-Client gegengemessen (469 -> 389 px, Ziehen erreicht den rechten Rand)
|
||||
state_head: 4d485432c003a6caf68f6d85aff7de0bd27794e2
|
||||
progress:
|
||||
total_phases: 18
|
||||
@@ -463,6 +463,7 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
|
||||
| 260922-ge2 | **XFrame: Ausschnitt der Seite waehlen und einpassen, Zoom, „Nur anzeigen“.** Wunsch des Nutzers: nur einen bestimmten Ausschnitt der eingebetteten Seite zeigen, und die Groesse soll skalieren. Config: `crop {x,y,w,h}` in Seitenpixeln bei fester Layoutbreite 1280 (`XFRAME_PAGE_WIDTH`, keine UI), Klemmung ueber EINE Funktion `clampXframeCrop` (x+w ≤ 1280 verschiebt x; w ≥ 100, h ≥ 60, y+h ≤ 4000); `zoom` (50…150 %, nur Ganzseiten-Modus); `readOnly` (transparente Flaeche ueber dem Rahmen im Ansichtsmodus). Kachel: `computeCropLayout` (contain + Zentrierung, Massstab darf > 1 sein), der `<iframe>` wird selbst verschoben und skaliert (cross-origin — die Seite laesst sich von aussen nicht scrollen), Kachelmass per ResizeObserver. Einstellungen: Vorschau der Seite bei 1280 px (Stage 3000 Seitenpixel hoch, eigener Bildlauf), Rahmen als `<fieldset>` (Biome `useSemanticElements`) mit vier Eckgriffen, Ziehen per Pointer-Events mit lokalem Entwurf und genau einem PATCH beim Loslassen, Zahlenfelder als Tastaturweg; Zoom-Auswahl nur ohne Ausschnitt; Aktivieren setzt `readOnly` mit. **Befund im Browser-Rundgang, behoben (cf70a19):** Kachel und Vorschau hatten verschiedene Rahmenhoehen (max(y+h,720) vs. 3000) — bei vh-relativen Seiten (example.com `margin: 15vh`) lag derselbe Inhalt an verschiedenen Stellen, der gewaehlte Ausschnitt haette in der Kachel daneben gelegen; jetzt dieselbe Layouthoehe. Neun Pruefpunkte bestanden (Verschieben, Ecken mit fester Gegenecke und Mindestbreite, Klemmung der Zahlenfelder, Einpassen und Mitskalieren bei Kachelgroesse, Nur-anzeigen, Zoom 60 %, verweigernde Seite). Playwright kann in einem per `transform` skalierten iframe nicht selbst klicken — per `elementFromPoint` + `mouse.click` umgangen, ist eine Werkzeuggrenze. Test-Helfer `src/test/fake-resize-observer.ts`. **Zahlen:** web 604 → 640, api 1175, type-check 4/4, lint 5/5 (web 53 Warnungen unveraendert), `as unknown as` 27/6, Umlaut-Allowlist + „Ausschnitt“. | 2026-09-22 | 445b1d3,30fdd99,cf70a19 | [260922-ge2-xframe-widget-ausschnitt-der-eingebettet](./quick/260922-ge2-xframe-widget-ausschnitt-der-eingebettet/) |
|
||||
| 260922-hk4 | **Bilderrahmen-Bilder liegen jetzt im Dateibereich statt in der Datenbank.** Frage des Nutzers nach der Freigabe 1.3.0, ob `bytea` auf Dauer sinnvoll ist. Befund: Geschwindigkeit ist NICHT das Argument (ein Bild wird je Browser einmal taeglich geladen), die SICHERUNG ist es — gesichert wird von Hand per `pg_dump`, und 30 Bilder à 5 MiB je Benutzer waeren im Extremfall 150 MB pro Benutzer in jedem Abzug (alpha-DB heute 18 MB). Dazu Einheitlichkeit: Profilbilder (`user-files/avatars`, `User.avatarPath`) und DKV-Exporte liegen laengst im Volume. Umsetzung: Spalte `storagePath`, Ablage `user-files/dashboard-images/<userId>/<uuid>.<ext>` — Dateiname IMMER vom Server (UUID + Endung aus dem erkannten Mime-Typ), `originalName` nie im Pfad; ein eigener Ordner je Benutzer ist ausdruecklich KEIN Schutz, es entscheidet weiterhin die Besitzpruefung im Dienst. Umzug laeuft automatisch beim Start (`onApplicationBootstrap` ueber `forSystem()`), idempotent; die Spalte `data` bleibt bewusst vorerst stehen (Todo fuer den DROP, erst wenn alpha und live einmal gelaufen sind). **Befund im Rundgang, eigener Commit:** eine Zeile zeigte auf eine fehlende Datei (lokal Host vs. Container-Volume; im Betrieb: alter `pg_dump` + leeres Volume) — `getBytes` stellt die Datei jetzt aus der noch vorhandenen Spalte `data` wieder her, statt 404 zu melden. **Zahlen:** api 1175 → 1188, web 640, type-check 4/4, lint 5/5, RLS-Waechter 78/78. | 2026-09-22 | 9039cea,8cbfb8b,82472ee | [260922-hk4-bilderrahmen-bilder-auf-die-festplatte](./quick/260922-hk4-bilderrahmen-bilder-auf-die-festplatte/) |
|
||||
| 260922-m1h | **Ein Modul bringt seine Dashboard-Kachel jetzt selbst mit (Vorarbeit fuer Proxmox).** Bestandsaufnahme (lesend) hatte ergeben: ein neuer Widget-Typ war an SIEBEN Stellen hartkodiert (Union-Typ, Constraints, Registry, eigene `wireXWidget()` je Typ, Aufruf in page.tsx, zweite Liste im Katalogfenster, `@IsIn` im API-DTO); die Verbindung Kachel↔Modul existierte als `WIDGET_MODULE_MAP` in `dashboard.service.ts` (filtert fail-closed), war aber nie befuellt; der Katalog zeigte jedem alle Kacheln, auch die gesperrter Module. Umbau: `WIDGET_TYPES`/`WidgetType`/`WIDGET_MODULE_SLUGS` in `packages/shared` als EINE Quelle (API validiert per `@IsIn` gegen genau sie), ein generisches `registerWidget()` statt neun Funktionen, Katalog leitet seine Liste aus der Registry ab und filtert ueber `/modules/active` (fail-closed bei Fehler, reine Funktion `visibleWidgetTypes`), nicht verfuegbare Kachel zeigt `widgets.unavailable` statt leer zu bleiben. Deckungsgleichheits-Test faengt kuenftig jede vergessene Stelle. **Befund des Executors, geprueft statt vermutet:** `apps/web` hatte KEINE Abhaengigkeit auf `@tessera/shared` (frueher bewusst) — vor der Umsetzung nachgemessen, dass Bau und Produktions-Abbild das tragen (node:24-alpine strippt die Typen nativ); Folgeregel „nur loeschbare Syntax in shared“ steht als Warnung in der Datei. Verhalten der neun Kacheln unveraendert, im Browser bestaetigt (Reihenfolge, Anlegen, Entfernen, keine rohen Schluessel). Bewusst offen: der Einstellungs-Zweig je Typ in `widget-settings-panel.tsx` und die Live-Aktualisierung des Katalogs. **Zahlen:** api 1188 → 1202, web 640 → 659, type-check 4/4, lint 5/5 (74/53 wie Basis). | 2026-09-22 | 56c07c3,8be0725 | [260922-m1h-dashboard-widgets-ein-modul-bringt-seine](./quick/260922-m1h-dashboard-widgets-ein-modul-bringt-seine/) |
|
||||
| 260922-vdk | **Dashboard-Raster misst seine Breite auch aus dem Leerzustand heraus.** Meldung des Nutzers aus dem **Linux-Client**: rechts neben dem Kalender freie Flaeche, in die sich keine Kachel schieben laesst — „als ob es keinen Anker gibt“. Aus dem Bildschirmfoto zurueckgerechnet (Spaltenbreite 51,5 px, Platzhalter auf Spalte 13 = letzte moegliche, Rasterende bei x=1459 bei ~1660 px Inhaltsbreite): das Raster rechnete mit **1200 px** statt mit der echten Breite, rechts blieben ~460 px totes Feld. Ursache: die Breitenmessung hing in `useEffect(..., [])` mit `if (!containerRef.current) return` — haengt `DashboardGrid` mit NULL Kacheln ein, rendert der fruehe Ruecksprung in den Leerzustand den gemessenen `<div>` gar nicht, der Effekt bricht ab und laeuft nie wieder, auch nicht wenn spaeter die erste Kachel entsteht. `width` blieb die ganze Sitzung auf dem Startwert 1200; react-grid-layout vergleicht strikt (`width > breakpoint`), 1200 ist damit `md` (20 Spalten, 51,6 px) statt `lg`. Fix: Ref-Rueckruf `measureRef` statt Einmal-Effekt — folgt dem Knoten ueber den Wechsel Leerzustand ↔ gefuellt, misst synchron in der Commit-Phase, haengt den ResizeObserver dort an; Fenster-Horcher als zusaetzliches Netz; `applyWidth` verwirft 0 und nicht endliche Werte. **Verhalten sonst unveraendert** — belegte Plaetze bleiben gesperrt, nichts weicht aus (Ansage des Nutzers). **Geprueft im echten Client**, nicht im Browser: `Tessera-1.3.0.AppImage` auf `DISPLAY=:10` ueber den WebKit-Remote-Inspektor gesteuert. Gleicher Fehlerfall vorher/nachher: Kachel 469 px → **389 px** bei 1000 px Bereich, Ziehen endet jetzt bei 603 px = `1000 − 8 − 389`, exakt der rechte Rand. **Messfalle notiert:** im Client gegen `style.width`/`style.transform` messen, nie gegen `getBoundingClientRect()` — bei Fenster im Hintergrund friert WebKitGTK die Animationsuhr ein und der `width`-Uebergang bleibt auf dem alten Wert stehen. **Zahlen:** web 659 → 661 Tests, type-check 4/4, lint 5/5, Biome web 53 Warnungen unveraendert. | 2026-09-22 | d9f2af3,cf67c8a | [260922-vdk-dashboard-raster-misst-seine-breite-nich](./quick/260922-vdk-dashboard-raster-misst-seine-breite-nich/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
---
|
||||
phase: quick-260922-vdk
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
autonomous: true
|
||||
requirements: [QUICK-260922-VDK]
|
||||
|
||||
files_modified:
|
||||
- apps/web/src/components/dashboard/dashboard-grid.tsx
|
||||
- apps/web/src/components/dashboard/dashboard-grid.test.tsx
|
||||
- CHANGELOG.md
|
||||
|
||||
estimate:
|
||||
tokens: 55000
|
||||
raw_tokens: 55000
|
||||
tasks: 2
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Ein Dashboard, das beim Öffnen leer ist, misst die verfügbare Breite, sobald die erste Kachel erscheint: das Raster füllt den Inhaltsbereich bis zum rechten Rand, rechts bleibt kein toter Streifen, in den sich keine Kachel ziehen lässt."
|
||||
- "Die Messung überlebt den Wechsel Leerzustand → gefüllt, weil sie am eingehängten Knoten selbst hängt (Ref-Rückruf) und nicht an einem Effekt, der nur beim ersten Einhängen läuft."
|
||||
- "Gemessen wird synchron in der Commit-Phase, bevor gezeichnet wird — der bisherige Zwischenzustand mit dem angenommenen Startwert wird nie sichtbar (auch nicht auf dem heute schon funktionierenden Pfad „Neuladen mit Kacheln“)."
|
||||
- "Ändert sich die Fenstergröße, folgt die Rasterbreite; ein Fenster-Horcher ist das Sicherheitsnetz für den Fall, dass der ResizeObserver nichts meldet."
|
||||
- "Das Ziehverhalten bleibt exakt wie heute: FREE_PLACEMENT_COMPACTOR mit preventCollision, ein belegtes Feld bleibt blockiert, nichts wird zur Seite geschoben (Nutzerentscheidung 22.09.2026); Leerzustand, BREAKPOINTS, COLS, rowHeight, margin und applyConstraintMinima sind unverändert."
|
||||
- "Alle Tore grün: 12 Tests in dashboard-grid.test.tsx (10 alte unverändert + 2 neue), Web gesamt ≥ 661 Tests in 81 Dateien, `tsc --noEmit` 4/4, Biome 5/5 mit weiterhin genau 53 Warnungen in web, `as unknown as` in web weiterhin 6."
|
||||
artifacts:
|
||||
- "apps/web/src/components/dashboard/dashboard-grid.tsx — Messung über den Ref-Rückruf `measureRef` (synchrone Erstmessung + ResizeObserver am jeweils eingehängten Knoten, Trennen im null-Zweig), `applyWidth`-Wächter, Fenster-Horcher; deutscher Kommentarblock `quick-260922-vdk` mit dem Warum"
|
||||
- "apps/web/src/components/dashboard/dashboard-grid.test.tsx — zwei neue Fälle (Leerzustand → gefüllt misst 1000; resize-Ereignis misst 1600) und `vi.unstubAllGlobals()` im bestehenden `afterEach`"
|
||||
- "CHANGELOG.md — neuer Abschnitt „### Behoben“ unter „Unveröffentlicht“ mit einem Stichpunkt in Alltagssprache"
|
||||
key_links:
|
||||
- "Kachel hinzufügen → `widgets.length` wird 1 → der Zweig mit dem Raster rendert → React hängt den `<div>` ein → `measureRef(node)` → synchrone Messung + `observer.observe(node)` → `setWidth` noch vor dem Zeichnen → `Responsive width` → Spaltenbreite und Breakpoint stimmen"
|
||||
- "Letzte Kachel entfernt → Knoten wird ausgehängt → `measureRef(null)` → `observerRef.current.disconnect()` (kein zurückgegebener Aufräum-Rückgabewert, damit React den null-Aufruf beibehält) → kein weiterlaufender Beobachter"
|
||||
- "`window` resize → Horcher → `nodeRef.current.getBoundingClientRect().width` → `applyWidth` (verwirft 0 und nicht endliche Werte) → `setWidth`"
|
||||
---
|
||||
|
||||
# Quick-Aufgabe 260922-vdk: Das Dashboard-Raster misst seine Breite auch aus dem Leerzustand heraus
|
||||
|
||||
<objective>
|
||||
Auf einem Dashboard, das beim Öffnen leer ist, bleibt das Raster für die ganze Sitzung bei der angenommenen Breite von 1200 Pixeln stehen. Die erste hinzugefügte Kachel rechnet deshalb mit 20 statt 24 Spalten und 51,6 px Spaltenbreite, das Raster endet bei 1200 px und rechts davon liegt ein toter Bereich (gemessen: ~460 px bei 1920 px Bildschirmbreite), in den sich keine Kachel ziehen lässt. Diese Aufgabe hängt die Messung an den Knoten statt an den ersten Einhäng-Zeitpunkt, misst vor dem ersten Zeichnen und ergänzt einen Fenster-Horcher als Netz.
|
||||
|
||||
Purpose: Fehlerbehebung aus einer bereits abgeschlossenen Messung — die Ursache steht fest, dieser Plan setzt nur noch um und sichert sie mit einem Regressionstest ab.
|
||||
Output: geänderte `dashboard-grid.tsx` mit deutschem Warum-Kommentar, zwei neue Tests (zuerst rot), ein Changelog-Stichpunkt, alle Tore grün, Prüfliste für den Browser-Rundgang im SUMMARY.
|
||||
</objective>
|
||||
|
||||
## Befund (gemessen, nicht neu zu untersuchen)
|
||||
|
||||
Der bisherige Code legt den Beobachter in einem Effekt mit leerer Abhängigkeitsliste an und bricht ab, wenn der Ref noch leer ist. Hängt `DashboardGrid` ein, während das Dashboard null Kacheln hat, greift der frühe Rücksprung in den Leerzustand **vor** dem `<div>` mit dem Ref: der Ref ist leer, der Effekt bricht ab — und läuft wegen der leeren Abhängigkeitsliste nie wieder, auch nicht, wenn später Kacheln erscheinen und der `<div>` tatsächlich entsteht. Die Breite bleibt für die ganze Sitzung beim Startwert.
|
||||
|
||||
Belege (bestätigt, nicht zu wiederholen):
|
||||
|
||||
- Echter Linux-Client (Tessera-1.3.0.AppImage, WebKitGTK), leeres Dashboard, eine Kalender-Kachel hinzugefügt: Kachel 469 px breit in einem 1000 px breiten Container — das ist die Rechnung für 1200.
|
||||
- Derselbe Client nach einem Neuladen mit vorhandener Kachel: 459 px in 1176 px, also richtig — weil die Ladeschranke in `(portal)/page.tsx` das Raster aus- und wieder einhängt, der Ref beim Einhängen also existiert.
|
||||
- Screenshot des Nutzers (1920×1045): Spaltenbreite 51,5 px, Platzhalter klebt an Spalte 13, rechte Rasterkante bei x = 1459 bei einem Inhaltsbereich bis ~1920.
|
||||
|
||||
`react-grid-layout` vergleicht den Breakpoint strikt größer als (`width > breakpoint`), 1200 ist damit **nicht** `lg`, sondern `md` → 20 Spalten.
|
||||
|
||||
## Gebundene Entscheidungen (nicht neu verhandeln)
|
||||
|
||||
1. **Ref-Rückruf statt Einmal-Effekt.** Die Messung hängt am jeweils eingehängten Knoten und überlebt Aus- und Einhängen.
|
||||
2. **Synchron vor dem ersten Zeichnen.** Der Ref-Rückruf läuft in der Commit-Phase; die dort ausgelöste Zustandsänderung wird vor dem Zeichnen abgearbeitet. Ein zusätzlicher `useLayoutEffect` ist damit überflüssig.
|
||||
3. **Fenster-Horcher als Netz**, zusätzlich zum ResizeObserver, nicht statt seiner.
|
||||
4. **Verhalten sonst unverändert.** `FREE_PLACEMENT_COMPACTOR` mit `preventCollision` bleibt (Nutzerentscheidung 22.09.2026: ein belegtes Feld bleibt blockiert, nichts wird zur Seite geschoben). Leerzustand, `BREAKPOINTS`, `COLS`, `rowHeight`, `margin`, `applyConstraintMinima` bleiben wortgleich.
|
||||
5. **Startwert 1200 bleibt.** Er lebt nur noch bis zur Commit-Phase desselben Einhängens. Genau diesen Übergang 1200 → gemessen macht der Pfad „Neuladen mit Kacheln“ heute schon in Produktion, und er ist nachweislich richtig (459 px in 1176 px) — ein anderer Startwert würde eine bisher unerprobte Breakpoint-Folge einführen, ohne etwas zu verbessern.
|
||||
6. **Nur `apps/web`.** Keine API, kein Prisma, kein Docker, keine neuen Pakete (ResizeObserver und resize sind Browser-Schnittstellen).
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||
@~/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/STATE.md
|
||||
@apps/web/src/components/dashboard/dashboard-grid.tsx
|
||||
@apps/web/src/components/dashboard/dashboard-grid.test.tsx
|
||||
@apps/web/src/test/fake-resize-observer.ts
|
||||
@apps/web/src/test/setup.ts
|
||||
</context>
|
||||
|
||||
## Schnittstellen, die der Executor kennen muss
|
||||
|
||||
- `apps/web/src/test/fake-resize-observer.ts` exportiert `stubResizeObserver({ width, height }): void`; es ersetzt den globalen ResizeObserver per `vi.stubGlobal` durch eine Klasse, deren `observe()` den Rückruf **sofort und synchron** mit `new DOMRectReadOnly(0, 0, width, height)` aufruft. Zurückgesetzt wird nur durch `vi.unstubAllGlobals()` — `vi.restoreAllMocks()` reicht dafür nicht.
|
||||
- `apps/web/src/test/setup.ts` legt global einen ResizeObserver-Ersatz an, der immer 1200×800 meldet. Ohne `stubResizeObserver` misst jeder Test also 1200 — der neue Test wäre damit blind für genau diesen Fehler.
|
||||
- `dashboard-grid.test.tsx` ersetzt `Responsive` durch einen Durchreicher, der die Props in `captured.props` ablegt (`vi.hoisted`, Mock per `importOriginal`, damit `noCompactor` echt bleibt). Die gemessene Breite ist dadurch als `captured.props?.width` prüfbar.
|
||||
- jsdom liefert für `getBoundingClientRect()` ohne Zutun 0 — die synchrone Erstmessung schlägt im Test also nicht durch, der gestubbte Beobachter liefert den Wert. Für den Fenster-Test wird `Element.prototype.getBoundingClientRect` gezielt überschrieben.
|
||||
- `new DOMRect(0, 0, w, h)` gibt es in jsdom (die Datei `fake-resize-observer.ts` nutzt bereits `DOMRectReadOnly`) — damit braucht der Test **keinen** Cast, und der Zähler `as unknown as` bleibt bei 6.
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer" tdd="true">
|
||||
<name>Aufgabe 1: Messung an den Knoten hängen (Ref-Rückruf, synchrone Erstmessung, Fenster-Horcher) — Ende-zu-Ende „leeres Dashboard → erste Kachel füllt die volle Breite“, zuerst rot</name>
|
||||
<files>apps/web/src/components/dashboard/dashboard-grid.test.tsx, apps/web/src/components/dashboard/dashboard-grid.tsx</files>
|
||||
<behavior>
|
||||
Zuerst die Tests, beide müssen vor der Änderung rot sein:
|
||||
|
||||
- **Test 10 — Leerzustand → gefüllt:** `stubResizeObserver({ width: 1000, height: 800 })`; `captured.props` auf null setzen; mit `widgets: []` und leeren Layouts rendern → die Überschrift des Leerzustands steht da und `captured.props` ist weiterhin null (das Raster ist nicht eingehängt); danach **`rerender`** derselben Instanz mit einer Uhr-Kachel und einem passenden `lg`-Eintrag → `captured.props?.width` ist 1000. Rot vorher: der Wert bleibt 1200.
|
||||
- **Test 11 — Fenstergröße:** `stubResizeObserver({ width: 1000, height: 800 })`; mit einer Uhr-Kachel rendern → Breite 1000; danach `Element.prototype.getBoundingClientRect` per `vi.spyOn(...).mockReturnValue(new DOMRect(0, 0, 1600, 800))` überschreiben und `window.dispatchEvent(new Event('resize'))` in `act(...)` auslösen → `captured.props?.width` ist 1600. Rot vorher: der Wert bleibt 1000, weil es keinen Fenster-Horcher gibt.
|
||||
|
||||
Die zehn bestehenden Fälle bleiben unverändert und grün — insbesondere Test 4 (Raster-Konstanten), Test 7 (Compactor-Pin mit preventCollision) und Test 9/9b (Minima).
|
||||
</behavior>
|
||||
<action>
|
||||
1. **`dashboard-grid.test.tsx`** (zuerst, rot): `act` zusätzlich aus `@testing-library/react` importieren, `stubResizeObserver` aus `@/test/fake-resize-observer`. Im bestehenden `afterEach` nach `vi.restoreAllMocks()` eine Zeile `vi.unstubAllGlobals()` ergänzen (ohne sie bliebe der gestubbte Beobachter für alle folgenden Dateien stehen). Am Ende des bestehenden `describe('DashboardGrid', …)` die zwei Fälle aus dem `<behavior>`-Block anfügen, benannt „quick-260922-vdk Test 10: …“ und „quick-260922-vdk Test 11: …“, Stil und Kommentarsprache wie die Nachbarfälle (deutsch, mit dem Warum in einer Zeile). Rot-Lauf ausführen und die Fehlermeldungen für das SUMMARY festhalten.
|
||||
|
||||
2. **`dashboard-grid.tsx`**: `useCallback` zum Import aus `react` hinzufügen. Die bisherige Kombination aus `containerRef` und dem Effekt mit leerer Abhängigkeitsliste ersetzen durch:
|
||||
- zwei Refs: `nodeRef` (Typ `HTMLDivElement | null`, hält den gerade eingehängten Knoten für den Fenster-Horcher) und `observerRef` (Typ `ResizeObserver | null`, hält den laufenden Beobachter);
|
||||
- `applyWidth` als `useCallback` mit leerer Abhängigkeitsliste: nimmt eine Zahl, ruft `setWidth` nur bei endlichem Wert größer 0 auf. React verwirft gleiche Werte selbst, ein zusätzlicher Vergleich ist unnötig und eine Rückkopplungsschleife damit ausgeschlossen (T-VDK-01);
|
||||
- `measureRef` als `useCallback` über `applyWidth`, Signatur nimmt `HTMLDivElement | null` und gibt **nichts** zurück: erst einen eventuell laufenden Beobachter trennen und `observerRef` leeren, dann `nodeRef` auf den Knoten setzen, bei `null` zurückspringen, sonst `applyWidth(node.getBoundingClientRect().width)` (das ist die synchrone Erstmessung in der Commit-Phase), dann einen neuen `ResizeObserver` anlegen, der `entries[0].contentRect.width` an `applyWidth` weitergibt, ihn auf den Knoten setzen und in `observerRef` merken. Wichtig: keine Aufräumfunktion zurückgeben — React 19 ruft den Rückruf sonst beim Aushängen nicht mehr mit `null` auf, und genau dieser Zweig ist hier der Aufräumpfad;
|
||||
- einen `useEffect` über `applyWidth`, der `resize` am `window` anmeldet und im Aufräumschritt wieder abmeldet; der Horcher misst `nodeRef` erneut, wenn dort ein Knoten liegt;
|
||||
- am Raster-`<div>` `ref={measureRef}` setzen (exakt dieser Name, ein Tor prüft ihn).
|
||||
|
||||
Alle vier Hooks stehen **vor** dem frühen Rücksprung in den Leerzustand, damit die Hook-Reihenfolge stabil bleibt — derselbe Grund, den der Kommentar bei `effectiveLayouts` bereits festhält.
|
||||
|
||||
3. **Warum-Kommentar** über der Messung, deutsch, im Stil der vorhandenen Blöcke (`quick-260916-dyv`, `quick-260916-bwo`), Präfix `quick-260922-vdk:`. Inhalt in eigenen Worten: der frühe Rücksprung in den Leerzustand rendert den gemessenen Knoten gar nicht erst, ein Effekt mit leerer Abhängigkeitsliste sieht ihn deshalb nie wieder; der Ref-Rückruf folgt dem Knoten über Aus- und Einhängen hinweg und misst in der Commit-Phase, also vor dem Zeichnen; die Folge der alten Annahme war ein `md`-Breakpoint mit 20 Spalten (strikter Größer-Vergleich in RGL), 51,6 px Spaltenbreite und ein toter Streifen rechts; der Fenster-Horcher ist ein Netz für Fälle, in denen der Beobachter nichts meldet; der Wächter verwirft 0 und nicht endliche Werte, damit eine kurzzeitig zusammengefallene Fläche das Raster nicht auf Null setzt. Den Startwert-Absatz (Entscheidung 5 oben) mit aufnehmen.
|
||||
|
||||
4. Nichts anderes anfassen: Leerzustand, `BREAKPOINTS`, `COLS`, `rowHeight`, `margin`, das fehlende `containerPadding`, `dragConfig`, `resizeConfig`, `FREE_PLACEMENT_COMPACTOR`, `applyConstraintMinima` und die `data-grid`-Erzeugung bleiben wortgleich.
|
||||
|
||||
Commit: `fix(quick-260922-vdk): Dashboard-Raster misst seine Breite auch aus dem Leerzustand heraus` (Wortlaut frei, Stil der Nachbarcommits, Co-Authored-By-Zeile).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/components/dashboard/dashboard-grid.test.tsx && pnpm --filter @tessera/web exec tsc --noEmit && grep -q "ref={measureRef}" apps/web/src/components/dashboard/dashboard-grid.tsx && grep -q "addEventListener('resize'" apps/web/src/components/dashboard/dashboard-grid.tsx && grep -q "unstubAllGlobals" apps/web/src/components/dashboard/dashboard-grid.test.tsx && grep -q "quick-260922-vdk" apps/web/src/components/dashboard/dashboard-grid.tsx</automated>
|
||||
</verify>
|
||||
<done>`dashboard-grid.test.tsx` hat 12 grüne Fälle (10 alte wortgleich, 2 neue); die zwei neuen waren nachweislich zuerst rot, die Rot-Meldungen („1200 statt 1000“ bzw. „1000 statt 1600“) stehen im SUMMARY. `tsc --noEmit` ohne Befund. Die Messung hängt am Ref-Rückruf `measureRef`, trennt den Beobachter im null-Zweig, misst synchron bei jedem Einhängen und hat einen Fenster-Horcher. Compactor, Konstanten, Leerzustand und `applyConstraintMinima` sind unverändert (Tests 4/7/9/9b belegen es). Ein Commit mit Scope `quick-260922-vdk`.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Aufgabe 2: Changelog-Stichpunkt, volle Tore, Zähler, Prüfliste für den Browser-Rundgang</name>
|
||||
<files>CHANGELOG.md</files>
|
||||
<action>
|
||||
1. **`CHANGELOG.md`**: unter „## Unveröffentlicht“ nach der bestehenden Liste „### Geändert“ einen neuen Abschnitt „### Behoben“ anlegen (Reihenfolge wie in 1.3.0: Neu, Geändert, Behoben) mit genau einem Stichpunkt in Alltagssprache, Tonlage der Nachbarzeilen, ohne Fachbegriffe: „Dashboard: war das Dashboard beim Öffnen leer, nutzte die erste hinzugefügte Kachel nur einen Teil der Breite — rechts blieb ein toter Streifen, in den sich keine Kachel ziehen ließ; das Raster misst die verfügbare Breite jetzt in jedem Fall und folgt auch einer Änderung der Fenstergröße“.
|
||||
|
||||
2. **Volle Tore** ausführen und die Zahlen ins SUMMARY schreiben: `pnpm type-check` (4/4), `pnpm lint` (5/5), `pnpm --filter @tessera/web test`. Ausgangsmessung dieses Plans (22.09., vor der Änderung): Web 81 Dateien / 659 Tests grün, Biome web genau 53 Warnungen, `as unknown as` in web 6. Erwartet nachher: 81 Dateien / 661 Tests, Warnungen und Zähler unverändert. Weicht eine Zahl ab, im SUMMARY benennen statt stillschweigend anpassen.
|
||||
|
||||
3. **Prüfliste** für den Orchestrator ins SUMMARY schreiben (Browser, lokal, Playwright-MCP — **nicht** auf dem Testserver), Punkt für Punkt abhakbar; ausdrücklich dazuschreiben, dass Breiten am DOM gemessen werden (`getBoundingClientRect` der Elemente), **nie** per `fetch` aus der Seite heraus:
|
||||
(a) Dashboard eines Benutzers ohne Kacheln öffnen (oder alle Kacheln entfernen und neu laden) → Leerzustand mit Bildmarke;
|
||||
(b) Bearbeiten → „Widget hinzufügen“ → Uhr: die Kachel erscheint, und beim Ziehen reicht der Platzhalter bis an den rechten Rand des Inhaltsbereichs — kein toter Streifen;
|
||||
(c) messen: Breite von `.react-grid-layout` gleicht der Breite des umgebenden Inhaltsbereichs (Abweichung höchstens der Rand von 8 px); vorher lag sie bei 1200 px unabhängig von der Fensterbreite;
|
||||
(d) eine zweite Kachel auf ein belegtes Feld ziehen → sie springt zurück, nichts wird zur Seite geschoben (unverändert gewollt); Größe ziehen stoppt am Nachbarn;
|
||||
(e) Fenster schmaler und wieder breiter ziehen → das Raster folgt, Kacheln bleiben heil;
|
||||
(f) Seite mit vorhandenen Kacheln neu laden → weiterhin richtig (der bisher schon funktionierende Pfad), und beim ersten Zeichnen ist kein Sprung von schmal auf breit zu sehen;
|
||||
(g) letzte Kachel entfernen → Leerzustand erscheint, danach eine neue Kachel hinzufügen → wieder volle Breite (der Beobachter wurde sauber getrennt und neu angehängt).
|
||||
|
||||
Commit: `docs(quick-260922-vdk): Changelog - Dashboard-Raster misst seine Breite auch aus dem Leerzustand` (nur CHANGELOG.md; Akte und STATE macht der Orchestrator; Co-Authored-By-Zeile).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && grep -q 'rechts blieb ein toter Streifen' CHANGELOG.md && awk '/^## Unveröffentlicht/{f=1} /^## 1\.3\.0/{f=0} f&&/^### Behoben/{n++} END{exit !(n==1)}' CHANGELOG.md && pnpm type-check && pnpm lint && pnpm --filter @tessera/web lint 2>&1 | grep -q 'Found 53 warnings' && test "$(grep -rn 'as unknown as' apps/web/src --include=*.ts --include=*.tsx | wc -l)" -eq 6 && pnpm --filter @tessera/web test</automated>
|
||||
<human-check>Am Ende der Aufgabe: der Orchestrator geht die siebenpunktige Prüfliste im Browser durch (lokal, Playwright-MCP). Entscheidend ist Punkt (b)/(c): auf einem beim Öffnen leeren Dashboard füllt die erste Kachel den Inhaltsbereich bis zum rechten Rand, und die gemessene Rasterbreite stimmt mit der Breite des Inhaltsbereichs überein.</human-check>
|
||||
</verify>
|
||||
<done>Der Changelog trägt unter „Unveröffentlicht“ genau einen neuen Abschnitt „### Behoben“ mit dem einen Stichpunkt. `pnpm type-check` 4/4 und `pnpm lint` 5/5 ohne Befund der Stufe `error`, Biome web weiterhin genau 53 Warnungen, `as unknown as` in web weiterhin 6, Web-Tests 81 Dateien / 661 grün. Die siebenpunktige Prüfliste steht im SUMMARY. Insgesamt genau zwei Commits mit Scope `quick-260922-vdk` (`git log --oneline -2`).</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
ASVS-Stufe 1, Blockschwelle `high` (jede `high`-Bedrohung MUSS mitigiert sein).
|
||||
|
||||
## Vertrauensgrenzen
|
||||
|
||||
| Grenze | Beschreibung |
|
||||
|---|---|
|
||||
| Browser-Layout → React-Zustand | Gemessene Breiten (ResizeObserver, `getBoundingClientRect`, `resize`) werden zu Zustand und steuern die Rastergeometrie; die Werte sind nicht vertrauenswürdig im Sinne von „immer sinnvoll“ (0 während eines Übergangs, nicht endlich bei zusammengefallener Fläche) |
|
||||
| Knoten-Lebensdauer → Beobachter-Lebensdauer | Der ResizeObserver hängt an einem Knoten, der beim Wechsel Leerzustand ↔ gefüllt aus- und eingehängt wird |
|
||||
| Neue Vertrauensgrenze | Keine. Kein Netzverkehr, keine API, keine Benutzereingabe, keine Persistenz in diesem Plan |
|
||||
|
||||
## STRIDE-Register
|
||||
|
||||
| ID | Kategorie | Komponente | Schwere | Disposition | Maßnahme |
|
||||
|---|---|---|---|---|---|
|
||||
| T-VDK-01 | Denial of Service (Rückkopplung Messung → Zustand → Layout → Messung, bis der Tab steht) | `applyWidth` / ResizeObserver-Rückruf in `dashboard-grid.tsx` | medium | mitigate | `applyWidth` ruft `setWidth` nur bei endlichem Wert größer 0; React verwirft gleiche Werte (Object.is) und rendert dann gar nicht neu — der Beobachter meldet nach dem Einschwingen denselben Wert und die Kette endet. Zusätzlich beobachtet der Beobachter den äußeren `<div>`, dessen Breite nicht von der Rastergeometrie abhängt. |
|
||||
| T-VDK-02 | Denial of Service (Ressourcenleck: Beobachter oder Fenster-Horcher überleben das Aushängen, bei jedem Wechsel Leerzustand ↔ gefüllt einer mehr) | `measureRef` null-Zweig, `useEffect`-Aufräumschritt | medium | mitigate | Der Ref-Rückruf trennt den laufenden Beobachter als erste Handlung und gibt bewusst **keine** Aufräumfunktion zurück, damit React ihn beim Aushängen mit `null` aufruft; der Fenster-Horcher meldet sich im Aufräumschritt des Effekts ab. Punkt (g) der Prüfliste geht den Wechsel im Browser durch. |
|
||||
| T-VDK-03 | Denial of Service (Messung auf einer zusammengefallenen Fläche setzt die Breite auf 0 → Raster unbedienbar, Kacheln unerreichbar) | Synchrone Erstmessung in jsdom-losen Übergängen, `getBoundingClientRect` | low | mitigate | Wächter „größer 0 und endlich“; bleibt eine Messung aus, gilt weiter der letzte gültige Wert statt 0. |
|
||||
| T-VDK-04 | Tampering (stille Verhaltensänderung beim Ziehen: ein belegtes Feld würde plötzlich nachgeben) | `FREE_PLACEMENT_COMPACTOR`, `dragConfig`, `resizeConfig` | medium | mitigate | Diese Stellen werden nicht angefasst; Test 7 pinnt den Compactor als echten `noCompactor` plus `preventCollision: true`, Test 6 die Zieh-Konfiguration, Test 4 die Raster-Konstanten — alle laufen im Tor der Aufgabe 1 mit. Prüfliste (d) belegt es zusätzlich im Browser. |
|
||||
| T-VDK-05 | Information Disclosure | — | low | accept | Es werden nur Layout-Maße des eigenen Fensters gelesen; nichts verlässt den Browser, nichts wird geloggt oder gespeichert. |
|
||||
| T-VDK-06 | Repudiation | — | low | accept | Reine Anzeige-Geometrie ohne Fremdwirkung; kein Audit-Log nötig, ASVS 1 genügt. |
|
||||
| T-VDK-SC | Tampering (Lieferkette) | npm-Installationen | high | mitigate | Nicht ausgelöst: KEINE neuen Pakete — `ResizeObserver`, `getBoundingClientRect`, `window`-Ereignisse und `DOMRect` sind Browser-Schnittstellen, `act` und `vi.spyOn` sind bereits vorhanden. Will der Executor doch etwas installieren: Stopp und Rückfrage an den Orchestrator, keine Installation ohne ausdrückliche Freigabe. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Automatisch (Executor, in den `<verify>`-Blöcken): gezielter Testlauf von `dashboard-grid.test.tsx` (12 Fälle), `tsc --noEmit`, Struktur-Tore auf Ref-Rückruf, Fenster-Horcher, `unstubAllGlobals` und den Warum-Kommentar; danach Changelog-Tore, `pnpm type-check` 4/4, `pnpm lint` 5/5, Biome web genau 53 Warnungen, `as unknown as` web 6, voller Web-Testlauf.
|
||||
|
||||
Rot-Nachweis (Aufgabe 1): beide neuen Fälle laufen vor der Änderung rot; die Meldungen gehören ins SUMMARY.
|
||||
|
||||
Manuell (Orchestrator, Prüfliste aus Aufgabe 2 Punkt 3, lokal im Browser, nicht auf dem Testserver): leeres Dashboard → erste Kachel füllt die Breite; gemessene Rasterbreite gleicht der Breite des Inhaltsbereichs; belegtes Feld bleibt blockiert; Fenstergröße; Neuladen mit Kacheln unverändert; Leerzustand → gefüllt → Leerzustand → gefüllt.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] Auf einem beim Öffnen leeren Dashboard füllt die erste hinzugefügte Kachel den Inhaltsbereich; rechts kein toter Streifen (Test 10 + Prüfliste b/c)
|
||||
- [ ] Die Messung folgt dem Knoten über Aus- und Einhängen und läuft synchron vor dem Zeichnen (Ref-Rückruf `measureRef`, Struktur-Tor + Prüfliste f/g)
|
||||
- [ ] Fenstergröße ändern misst neu (Test 11 + Prüfliste e)
|
||||
- [ ] Ziehverhalten unverändert: belegtes Feld bleibt blockiert, nichts wird verschoben (Tests 4/6/7/9/9b grün, Prüfliste d)
|
||||
- [ ] 12 Fälle in `dashboard-grid.test.tsx`, Web gesamt 81 Dateien / 661 Tests grün
|
||||
- [ ] `pnpm type-check` 4/4, `pnpm lint` 5/5, Biome web 53 Warnungen, `as unknown as` web 6
|
||||
- [ ] Changelog: genau ein Stichpunkt unter „Unveröffentlicht → Behoben“
|
||||
- [ ] Genau zwei Commits mit Scope `quick-260922-vdk`
|
||||
- [ ] Nur `apps/web` und `CHANGELOG.md` angefasst; keine neuen Pakete
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
`.planning/quick/260922-vdk-dashboard-raster-misst-seine-breite-nich/260922-vdk-SUMMARY.md` schreiben, wenn beide Aufgaben fertig sind — mit Rot-Meldungen der zwei neuen Tests, den gemessenen Zahlen (Tests, Warnungen, Zähler) und der siebenpunktigen Prüfliste für den Browser-Rundgang.
|
||||
</output>
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
---
|
||||
phase: quick-260922-vdk
|
||||
plan: 01
|
||||
subsystem: ui
|
||||
tags: [react, react-grid-layout, ref-callback, resize-observer, dashboard, vitest]
|
||||
|
||||
requires: []
|
||||
provides:
|
||||
- "Dashboard-Raster misst seine Breite auch beim Wechsel Leerzustand -> gefuellt (Ref-Rueckruf statt Einmal-Effekt)"
|
||||
- "Fenster-Horcher als Netz zusaetzlich zum ResizeObserver"
|
||||
affects: [dashboard-grid, dashboard]
|
||||
|
||||
actuals:
|
||||
tokens: 2392
|
||||
tasks: 2
|
||||
commits: 2
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Ref-Rueckruf (callback ref) statt useEffect mit leerer Abhaengigkeitsliste, wenn eine Messung den tatsaechlich eingehaengten Knoten ueber Aus-/Einhaengen hinweg verfolgen muss"
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- apps/web/src/components/dashboard/dashboard-grid.tsx
|
||||
- apps/web/src/components/dashboard/dashboard-grid.test.tsx
|
||||
- CHANGELOG.md
|
||||
|
||||
key-decisions:
|
||||
- "Ref-Rueckruf measureRef ersetzt containerRef + Einmal-Effekt; misst synchron in der Commit-Phase, kein useLayoutEffect noetig"
|
||||
- "Kein Aufraeum-Rueckgabewert aus measureRef, damit React 19 den Rueckruf beim Aushaengen mit null aufruft (das ist der Aufraeumpfad)"
|
||||
- "Fenster-Horcher zusaetzlich zum ResizeObserver, nicht als Ersatz"
|
||||
- "Startwert bleibt 1200 (nur bis zur ersten Commit-Phase relevant)"
|
||||
- "FREE_PLACEMENT_COMPACTOR/preventCollision, BREAKPOINTS, COLS, rowHeight, margin, applyConstraintMinima unveraendert"
|
||||
|
||||
patterns-established:
|
||||
- "quick-260922-vdk Kommentarblock in dashboard-grid.tsx erklaert das Warum der Ref-Rueckruf-Messung"
|
||||
|
||||
requirements-completed: [QUICK-260922-VDK]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "Dashboard-Raster misst seine Breite auch aus dem Leerzustand heraus (Ref-Rueckruf, synchrone Erstmessung, Fenster-Horcher)"
|
||||
requirement: "QUICK-260922-VDK"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260922-vdk Test 10: Leerzustand -> gefuellt misst die tatsaechliche Breite statt beim Startwert 1200 stehenzubleiben"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260922-vdk Test 11: Fenstergroesse aendert sich -> der Fenster-Horcher misst neu"
|
||||
status: pass
|
||||
human_judgment: true
|
||||
rationale: "Die Unit-Tests beweisen die gemessene Breite in jsdom; ob im echten Browser rechts kein toter Streifen mehr bleibt und die Rasterbreite sichtbar der Breite des Inhaltsbereichs entspricht, verlangt einen Browser-Rundgang (Prueflliste unten)."
|
||||
- id: D2
|
||||
description: "Ziehverhalten unveraendert: belegtes Feld bleibt blockiert, nichts wird zur Seite geschoben"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260916-dyv Test 7: Compactor-Pin"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260916-dyv Test 6: dragConfig-Pin"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
|
||||
duration: ~9min
|
||||
completed: 2026-09-22
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Quick-Aufgabe 260922-vdk: Dashboard-Raster misst seine Breite auch aus dem Leerzustand heraus Summary
|
||||
|
||||
**Messung an einem Ref-Rueckruf (`measureRef`) statt an einem Einmal-Effekt: der Beobachter folgt dem eingehaengten `<div>` ueber Leerzustand <-> gefuellt hinweg, misst synchron in der Commit-Phase, plus Fenster-Horcher als Netz — 20-Spalten/`md`-Fehlmessung nach 1200 px behoben.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~9 min
|
||||
- **Completed:** 2026-09-22
|
||||
- **Tasks:** 2/2
|
||||
- **Files modified:** 3
|
||||
|
||||
## Accomplishments
|
||||
- Dashboard-Raster misst jetzt in jedem Fall die tatsaechliche Breite des Inhaltsbereichs, auch wenn `DashboardGrid` mit null Kacheln einhaengt und die erste Kachel erst spaeter erscheint
|
||||
- Fenster-Horcher als Sicherheitsnetz zusaetzlich zum ResizeObserver
|
||||
- Zwei neue Regressionstests, nachweislich zuerst rot
|
||||
- Ziehverhalten (`FREE_PLACEMENT_COMPACTOR` mit `preventCollision`), Raster-Konstanten und Leerzustand unveraendert (per Test 4/6/7/9/9b bestaetigt)
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Aufgabe 1: Messung an den Knoten haengen (Ref-Rueckruf, synchrone Erstmessung, Fenster-Horcher)** - `d9f2af3` (fix)
|
||||
2. **Aufgabe 2: Changelog-Stichpunkt, volle Tore, Zaehler, Pruefliste** - `cf67c8a` (docs)
|
||||
|
||||
_Ledger: `plan_head_before` = `3e8c0f4` (letzter Commit vor diesem Plan — die bereits committete Akte). `git rev-list --count 3e8c0f4..HEAD` = 2, deckt sich mit den zwei oben genannten Commits._
|
||||
|
||||
**Plan metadata:** wird vom Orchestrator nach diesem SUMMARY committet (STATE.md/ROADMAP.md ausserhalb dieses Plans).
|
||||
|
||||
## Rot-Nachweis (Aufgabe 1, vor der Aenderung)
|
||||
|
||||
Testlauf vor dem Umbau von `dashboard-grid.tsx` (nur die Test-Datei war schon geaendert):
|
||||
|
||||
```
|
||||
FAIL src/components/dashboard/dashboard-grid.test.tsx > DashboardGrid > quick-260922-vdk Test 10: ...
|
||||
AssertionError: expected 1200 to be 1000 // Object.is equality
|
||||
|
||||
FAIL src/components/dashboard/dashboard-grid.test.tsx > DashboardGrid > quick-260922-vdk Test 11: ...
|
||||
AssertionError: expected 1000 to be 1600 // Object.is equality
|
||||
```
|
||||
|
||||
10 von 12 Faellen gruen, genau die zwei neuen rot — wie erwartet: Test 10 blieb beim Startwert 1200 (der frueh zurueckspringende Leerzustand rendert den gemessenen Knoten nie, der Effekt mit leerer Abhaengigkeitsliste sieht ihn also nie), Test 11 blieb bei 1000, weil es vorher keinen Fenster-Horcher gab.
|
||||
|
||||
Nach dem Umbau: alle 12 Faelle gruen (`pnpm --filter @tessera/web exec vitest run src/components/dashboard/dashboard-grid.test.tsx`).
|
||||
|
||||
## Gemessene Zahlen (nachher, alle Tore)
|
||||
|
||||
| Tor | Ausgangsmessung (22.09., vor der Aenderung) | Nachher (gemessen) |
|
||||
|---|---|---|
|
||||
| `dashboard-grid.test.tsx` | 10 Faelle | 12 Faelle, alle gruen |
|
||||
| Web Tests gesamt | 81 Dateien / 659 Tests | 81 Dateien / 661 Tests, alle gruen |
|
||||
| `pnpm type-check` | — | 4/4 ohne Befund |
|
||||
| `pnpm lint` | — | 5/5 ohne Befund der Stufe `error` |
|
||||
| Biome web Warnungen | 53 | 53 (unveraendert) |
|
||||
| `as unknown as` in web | 6 | 6 (unveraendert) |
|
||||
|
||||
Keine Abweichung — alle erwarteten Zahlen aus dem Plan treffen exakt zu.
|
||||
|
||||
## Files Created/Modified
|
||||
- `apps/web/src/components/dashboard/dashboard-grid.tsx` - `measureRef`-Ref-Rueckruf ersetzt `containerRef` + Einmal-Effekt; `applyWidth`-Wächter (verwirft 0/nicht-endliche Werte); Fenster-Horcher (`resize`-Listener); deutscher Warum-Kommentarblock `quick-260922-vdk`
|
||||
- `apps/web/src/components/dashboard/dashboard-grid.test.tsx` - zwei neue Faelle (Test 10: Leerzustand -> gefuellt misst 1000; Test 11: resize-Ereignis misst 1600), `stubResizeObserver`-Import, `vi.unstubAllGlobals()` im `afterEach`
|
||||
- `CHANGELOG.md` - neuer Abschnitt „### Behoben" unter „## Unveroeffentlicht" mit einem Stichpunkt
|
||||
|
||||
## Decisions Made
|
||||
Keine neuen Entscheidungen — die im Plan gebundenen Entscheidungen (Ref-Rueckruf statt Einmal-Effekt, synchron vor dem ersten Zeichnen, Fenster-Horcher als Netz, Ziehverhalten unveraendert, Startwert 1200 bleibt, nur `apps/web`) wurden wortgleich umgesetzt.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written.
|
||||
|
||||
## Issues Encountered
|
||||
None.
|
||||
|
||||
## Pruefliste fuer den Browser-Rundgang (Orchestrator, lokal, Playwright-MCP, nicht Testserver)
|
||||
|
||||
Breiten werden am DOM gemessen (`getBoundingClientRect` der Elemente), **nie** per `fetch` aus der Seite heraus — das taeuscht in beide Richtungen.
|
||||
|
||||
- [ ] (a) Dashboard eines Benutzers ohne Kacheln oeffnen (oder alle Kacheln entfernen und neu laden) -> Leerzustand mit Bildmarke erscheint
|
||||
- [ ] (b) Bearbeiten -> „Widget hinzufuegen" -> Uhr: die Kachel erscheint, und beim Ziehen reicht der Platzhalter bis an den rechten Rand des Inhaltsbereichs — kein toter Streifen
|
||||
- [ ] (c) messen: Breite von `.react-grid-layout` gleicht der Breite des umgebenden Inhaltsbereichs (Abweichung hoechstens der Rand von 8 px); vorher lag sie bei 1200 px unabhaengig von der Fensterbreite
|
||||
- [ ] (d) eine zweite Kachel auf ein belegtes Feld ziehen -> sie springt zurueck, nichts wird zur Seite geschoben (unveraendert gewollt); Groesse ziehen stoppt am Nachbarn
|
||||
- [ ] (e) Fenster schmaler und wieder breiter ziehen -> das Raster folgt, Kacheln bleiben heil
|
||||
- [ ] (f) Seite mit vorhandenen Kacheln neu laden -> weiterhin richtig (der bisher schon funktionierende Pfad), und beim ersten Zeichnen ist kein Sprung von schmal auf breit zu sehen
|
||||
- [ ] (g) letzte Kachel entfernen -> Leerzustand erscheint, danach eine neue Kachel hinzufuegen -> wieder volle Breite (der Beobachter wurde sauber getrennt und neu angehaengt)
|
||||
|
||||
Entscheidend: Punkt (b)/(c) — auf einem beim Oeffnen leeren Dashboard fuellt die erste Kachel den Inhaltsbereich bis zum rechten Rand, und die gemessene Rasterbreite stimmt mit der Breite des Inhaltsbereichs ueberein.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
Keine neue Vertrauensgrenze, keine neuen Pakete. Alle sechs T-VDK-Punkte aus dem Plan-Threat-Model sind mit Tests bzw. Struktur-Toren abgedeckt (siehe Rot-Nachweis und gemessene Zahlen oben); nichts Neues gefunden.
|
||||
|
||||
## Next Phase Readiness
|
||||
Kein laufender Meilenstein, keine Folge-Phase direkt abhaengig. Naechster Schritt laut STATE.md bleibt: Widget-Modul-Kopplung (`WIDGET_MODULE_MAP`) und danach das Proxmox-Modul — unabhaengig von dieser Quick-Aufgabe.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: apps/web/src/components/dashboard/dashboard-grid.tsx
|
||||
- FOUND: apps/web/src/components/dashboard/dashboard-grid.test.tsx
|
||||
- FOUND: CHANGELOG.md
|
||||
- FOUND: .planning/quick/260922-vdk-dashboard-raster-misst-seine-breite-nich/260922-vdk-SUMMARY.md
|
||||
- FOUND commit: d9f2af3
|
||||
- FOUND commit: cf67c8a
|
||||
|
||||
---
|
||||
*Phase: quick-260922-vdk*
|
||||
*Completed: 2026-09-22*
|
||||
|
||||
## Rundgang durch den Orchestrator (22.09.2026, echter Linux-Client, nicht der Browser)
|
||||
|
||||
Der Nutzer hat den Fehler im **Linux-Client** gemeldet und ausdruecklich gesagt, ein
|
||||
Browser-Test bringe nichts. Geprueft wurde deshalb im echten Paket: `Tessera-1.3.0.AppImage`
|
||||
aus dem Gitea-Release, auf dem Entwicklungsrechner gestartet (`DISPLAY=:10`,
|
||||
`WEBKIT_INSPECTOR_HTTP_SERVER=127.0.0.1:9230`), gesteuert ueber den WebKit-Remote-Inspektor
|
||||
(Treiber `scratchpad/wk.mjs`, Target-Protokoll: `Target.sendMessageToTarget` +
|
||||
`Runtime.evaluate`). Server: lokaler Stack.
|
||||
|
||||
**Ausgangsmessung VOR dem Fix** (gleiche Sitzung, gleicher Client):
|
||||
|
||||
| Weg | Bereich | Kachel | Rasterbreite laut Rechnung |
|
||||
|-----|---------|--------|----------------------------|
|
||||
| Neuladen MIT Kachel | 1176 px | 459 px | 1176 px — richtig |
|
||||
| Seitenleiste auf/zu | 1000 ↔ 1176 px | folgt | richtig |
|
||||
| **Leeres Dashboard, dann Kachel hinzufuegen** | 1000 px | **469 px** | **1200 px — falsch** |
|
||||
|
||||
Der dritte Weg ist der Fehlerfall: `DashboardGrid` haengt mit null Kacheln ein, der
|
||||
gemessene `<div>` existiert nicht, der Einmal-Effekt bricht ab und laeuft nie wieder.
|
||||
|
||||
**Nach dem Fix, derselbe Weg** (Kachel entfernt, gespeichert, neu geladen -> leeres
|
||||
Dashboard -> Kalender hinzugefuegt):
|
||||
|
||||
- `width`-Eigenschaft an `Responsive`: **1000** (= echte Bereichsbreite), Breakpoint `md`,
|
||||
20 Spalten — aus dem React-Fiber ausgelesen, nicht geraten.
|
||||
- Kachel `style.width`: **389 px** = `8 × 41,6 + 56`, exakt der Sollwert fuer 1000 px.
|
||||
- Ziehen nach rechts (synthetische Maus-Ereignisse, jeweils mit Wartezeit, damit React
|
||||
dazwischen rendert): Platzhalter laeuft 8 → 107 → 256 → 405 → 554 → **603** und bleibt
|
||||
dort. `1000 − 8 − 389 = 603` — **der Platzhalter erreicht jetzt exakt den rechten Rand**.
|
||||
Die Kachel wird auch dort abgelegt (`tileX 603`).
|
||||
|
||||
**Messfalle, dokumentiert damit sie niemanden noch einmal kostet:** `getBoundingClientRect()`
|
||||
und `getComputedStyle().width` lieferten im Client weiter 469 px, obwohl `style.width`
|
||||
bereits 389 px war. Grund: `.react-grid-item` hat `transition: width .2s`, und das
|
||||
Client-Fenster lag im Hintergrund — WebKitGTK friert die Animationsuhr dann ein, der
|
||||
Uebergang bleibt auf dem Startwert stehen (`getAnimations()` meldete eine laufende
|
||||
`CSSTransition` auf `width`). **Im Client gegen die gesetzten Werte messen
|
||||
(`style.width`, `style.transform`) oder gegen die React-Eigenschaften, nie gegen die
|
||||
gemalte Box** — sonst misst man die eingefrorene Animation statt des Ergebnisses.
|
||||
|
||||
**Nicht angefasst, wie zugesagt:** belegte Plaetze bleiben gesperrt, nichts weicht aus
|
||||
(`FREE_PLACEMENT_COMPACTOR` mit `preventCollision` unveraendert) — ausdrueckliche Ansage des
|
||||
Nutzers am 22.09.
|
||||
|
||||
**Testreste der Pruefung:** lokaler Testbenutzer `clienttest` und die zeitweise auf
|
||||
`NODE_ENV=development` gesetzte lokale API (WebKitGTK nimmt `secure`-Kekse ueber `http` nicht
|
||||
an, Chromium macht fuer `localhost` eine Ausnahme) — beides nach der Pruefung wieder
|
||||
zurueckgebaut.
|
||||
@@ -4,11 +4,17 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
|
||||
|
||||
## Unveröffentlicht
|
||||
|
||||
## 1.3.1 – 2026-09-23
|
||||
|
||||
### Geändert
|
||||
|
||||
- Bilderrahmen: hochgeladene Bilder liegen jetzt im Dateibereich des Servers statt in der Datenbank — die Datenbanksicherung bleibt dadurch klein; vorhandene Bilder ziehen beim ersten Start automatisch um
|
||||
- Dashboard: Kacheln, die zu einem Modul gehören, erscheinen nur noch für Benutzer, die dieses Modul nutzen dürfen; eine nicht mehr freigegebene Kachel erklärt das jetzt, statt leer zu bleiben
|
||||
|
||||
### Behoben
|
||||
|
||||
- Dashboard: war das Dashboard beim Öffnen leer, nutzte die erste hinzugefügte Kachel nur einen Teil der Breite — rechts blieb ein toter Streifen, in den sich keine Kachel ziehen ließ; das Raster misst die verfügbare Breite jetzt in jedem Fall und folgt auch einer Änderung der Fenstergröße
|
||||
|
||||
## 1.3.0 – 2026-09-22
|
||||
|
||||
### Neu
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Children, isValidElement } from 'react';
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
import { act, cleanup, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { stubResizeObserver } from '@/test/fake-resize-observer';
|
||||
|
||||
// Mock CSS imports that vitest cannot resolve
|
||||
vi.mock('react-grid-layout/css/styles.css', () => ({}));
|
||||
@@ -84,6 +85,10 @@ vi.mock('@/lib/stores/dashboard-store', () => ({
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
// quick-260922-vdk: stubResizeObserver ersetzt den globalen ResizeObserver
|
||||
// per vi.stubGlobal — vi.restoreAllMocks() setzt das nicht zurueck, ohne
|
||||
// diese Zeile bliebe der gestubbte Beobachter fuer alle folgenden Dateien stehen.
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('DashboardGrid', () => {
|
||||
@@ -387,4 +392,64 @@ describe('DashboardGrid', () => {
|
||||
expect(screen.queryByTitle('Drag the tile to move it')).toBeNull();
|
||||
expect(document.querySelector('.widget-drag-handle')).toBeNull();
|
||||
});
|
||||
|
||||
it('quick-260922-vdk Test 10: Leerzustand -> gefuellt misst die tatsaechliche Breite statt beim Startwert 1200 stehenzubleiben', async () => {
|
||||
stubResizeObserver({ width: 1000, height: 800 });
|
||||
captured.props = null;
|
||||
const { DashboardGrid } = await import('./dashboard-grid');
|
||||
|
||||
const { rerender } = render(
|
||||
<DashboardGrid
|
||||
layouts={{ lg: [], md: [], sm: [], xs: [], xxs: [] }}
|
||||
widgets={[]}
|
||||
isEditMode={false}
|
||||
onLayoutChange={vi.fn()}
|
||||
onRemoveWidget={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Leerzustand: der gemessene Knoten ist gar nicht eingehaengt.
|
||||
expect(screen.getByText('No active widgets')).toBeInTheDocument();
|
||||
expect(captured.props).toBeNull();
|
||||
|
||||
// Erste Kachel erscheint -> der Raster-<div> wird eingehaengt -> measureRef
|
||||
// misst synchron vor dem Zeichnen. Vor der Aenderung bleibt width bei 1200.
|
||||
rerender(
|
||||
<DashboardGrid
|
||||
layouts={{ lg: [{ i: 'inst-1', x: 0, y: 0, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [] }}
|
||||
widgets={[{ id: 'inst-1', widgetType: 'clock', config: {} }]}
|
||||
isEditMode={false}
|
||||
onLayoutChange={vi.fn()}
|
||||
onRemoveWidget={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(captured.props?.width).toBe(1000);
|
||||
});
|
||||
|
||||
it('quick-260922-vdk Test 11: Fenstergroesse aendert sich -> der Fenster-Horcher misst neu', async () => {
|
||||
stubResizeObserver({ width: 1000, height: 800 });
|
||||
captured.props = null;
|
||||
const { DashboardGrid } = await import('./dashboard-grid');
|
||||
|
||||
render(
|
||||
<DashboardGrid
|
||||
layouts={{ lg: [{ i: 'inst-1', x: 0, y: 0, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [] }}
|
||||
widgets={[{ id: 'inst-1', widgetType: 'clock', config: {} }]}
|
||||
isEditMode={false}
|
||||
onLayoutChange={vi.fn()}
|
||||
onRemoveWidget={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(captured.props?.width).toBe(1000);
|
||||
|
||||
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 1600, 800));
|
||||
act(() => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
// Vor der Aenderung gibt es keinen Fenster-Horcher: width bliebe bei 1000.
|
||||
expect(captured.props?.width).toBe(1600);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Responsive, noCompactor } from 'react-grid-layout';
|
||||
import type { Compactor, ResponsiveLayouts } from 'react-grid-layout';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
@@ -125,19 +125,79 @@ export function DashboardGrid({
|
||||
onRemoveWidget,
|
||||
}: DashboardGridProps) {
|
||||
const t = useTranslations('widgets');
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState(1200);
|
||||
|
||||
// Measure container width (v2 requires explicit width — Pitfall 1)
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
setWidth(entries[0].contentRect.width);
|
||||
});
|
||||
observer.observe(containerRef.current);
|
||||
return () => observer.disconnect();
|
||||
// quick-260922-vdk: Messung haengt am eingehaengten Knoten (Ref-Rueckruf),
|
||||
// nicht mehr an einem Effekt mit leerer Abhaengigkeitsliste.
|
||||
//
|
||||
// Warum: der fruehe Ruecksprung in den Leerzustand rendert das gemessene
|
||||
// <div> gar nicht erst. Haengt DashboardGrid mit null Kacheln ein, sieht der
|
||||
// alte Effekt (leere Abhaengigkeitsliste, laeuft genau einmal beim
|
||||
// Einhaengen) den Ref als leer, bricht ab und laeuft nie wieder — auch nicht,
|
||||
// wenn spaeter die erste Kachel erscheint und das <div> tatsaechlich
|
||||
// entsteht. Die Breite blieb dann fuer die ganze Sitzung beim Startwert
|
||||
// 1200, react-grid-layout vergleicht den Breakpoint strikt groesser als
|
||||
// (`width > breakpoint`), 1200 ist damit NICHT `lg` sondern `md` -> 20 statt
|
||||
// 24 Spalten, 51,6 statt 50 px Spaltenbreite, ein toter Streifen rechts.
|
||||
//
|
||||
// Der Ref-Rueckruf `measureRef` folgt dem Knoten ueber Aus- und Einhaengen
|
||||
// hinweg (Leerzustand <-> gefuellt) und laeuft in der Commit-Phase — die dort
|
||||
// ausgeloeste Zustandsaenderung wird vor dem Zeichnen abgearbeitet, ein
|
||||
// zusaetzlicher useLayoutEffect ist damit ueberfluessig. Der Startwert 1200
|
||||
// lebt deshalb nur noch bis zur Commit-Phase desselben Einhaengens; genau
|
||||
// diesen Uebergang macht der Pfad "Neuladen mit vorhandenen Kacheln" heute
|
||||
// schon in Produktion und er ist nachweislich richtig (459 px in 1176 px
|
||||
// gemessen) — ein anderer Startwert wuerde eine bisher unerprobte
|
||||
// Breakpoint-Folge einfuehren, ohne etwas zu verbessern.
|
||||
//
|
||||
// Der Fenster-Horcher ist ein zusaetzliches Netz fuer Faelle, in denen der
|
||||
// ResizeObserver nichts meldet — nicht sein Ersatz. `applyWidth` verwirft 0
|
||||
// und nicht endliche Werte (T-VDK-01/T-VDK-03), damit eine kurzzeitig
|
||||
// zusammengefallene Flaeche das Raster nicht auf Null setzt und keine
|
||||
// Rueckkopplungsschleife entsteht; React verwirft gleiche Werte selbst.
|
||||
const nodeRef = useRef<HTMLDivElement | null>(null);
|
||||
const observerRef = useRef<ResizeObserver | null>(null);
|
||||
|
||||
const applyWidth = useCallback((next: number) => {
|
||||
if (Number.isFinite(next) && next > 0) {
|
||||
setWidth(next);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const measureRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
// Ein eventuell laufender Beobachter zuerst trennen — auch der
|
||||
// null-Zweig (Aushaengen) durchlaeuft diese Zeilen, das ist hier der
|
||||
// Aufraeumpfad (T-VDK-02).
|
||||
observerRef.current?.disconnect();
|
||||
observerRef.current = null;
|
||||
nodeRef.current = node;
|
||||
if (!node) return;
|
||||
|
||||
// Synchrone Erstmessung in der Commit-Phase, vor dem ersten Zeichnen.
|
||||
applyWidth(node.getBoundingClientRect().width);
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
applyWidth(entries[0].contentRect.width);
|
||||
});
|
||||
observer.observe(node);
|
||||
observerRef.current = observer;
|
||||
// Bewusst keine Aufraeumfunktion zurueckgeben: React 19 ruft den
|
||||
// Ref-Rueckruf sonst beim Aushaengen nicht mehr mit null auf.
|
||||
},
|
||||
[applyWidth],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onResize = () => {
|
||||
if (nodeRef.current) {
|
||||
applyWidth(nodeRef.current.getBoundingClientRect().width);
|
||||
}
|
||||
};
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => window.removeEventListener('resize', onResize);
|
||||
}, [applyWidth]);
|
||||
|
||||
// quick-260916-dyv: minW/minH (und zu kleine w/h) aus WIDGET_CONSTRAINTS —
|
||||
// siehe applyConstraintMinima. Vor dem Leerzustand, damit die Hook-Reihenfolge
|
||||
// stabil bleibt.
|
||||
@@ -162,7 +222,7 @@ export function DashboardGrid({
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<div ref={measureRef}>
|
||||
<Responsive
|
||||
width={width}
|
||||
breakpoints={BREAKPOINTS}
|
||||
|
||||
Reference in New Issue
Block a user