diff --git a/CLAUDE.md b/CLAUDE.md index ce6b462..c4a5429 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,6 +32,8 @@ Vaadin Flow (server-side Java UI, no hand-written HTML/JS for views) on Spring B Since `apex-chart.ts` renders into light DOM (`createRenderRoot()` returns `this`), global CSS can reach into the chart markup — but series/legend/grid colors are driven entirely by the options JSON, not CSS, because ApexCharts renders its own SVG/canvas. +**`GridStackLayout`/`GridStackItem`** (`components/`) wrap [gridstack.js](https://gridstack.js.org) 13.1.0 for draggable/resizable grids. Unlike `ApexChart`, the bridge (`frontend/components/grid-stack.ts`) is a plain `HTMLElement`, not a Lit component — its children are server-rendered `GridStackItem`s living in light DOM, and a Lit render root would fight gridstack for ownership of them. A `MutationObserver` calls `makeWidget`/`removeWidget` as Flow adds/removes children, so there's no explicit add/remove protocol to the client. Every `GridStackItem` needs a stable `gs-id` (auto-generated if not given) — `GridStackLayout.setStorageKey(...)` persists drag/resize state to browser `localStorage` keyed on it and restores by matching ids, so items lose their saved position if their id changes between reloads. Item styling (`.grid-stack-item-content`) extends the `--dialect-*` alias layer in `styles.css`, same as `.dialect-card`. + **Views** (`views/`): `MainLayout` (`@Layout`, applies to all routes) is the `AppLayout` shell — navbar + `SideNav` drawer, with one `SideNavItem` per route. Routes: `DashboardView` (`@Route("")`) wraps each chart in a `Card` (`components/Card.java`); `FormView` (`@Route("formular")`) demonstrates form controls bound via `Binder`; `TableView` (`@Route("tabelle")`) demonstrates a `Grid` over dummy data with a live text filter (`GridListDataView.addFilter`, `TextField` in `ValueChangeMode.EAGER`). New views should reuse `Card` to wrap their content rather than adding components directly, and get a matching `SideNavItem` in `MainLayout`. **Styling**: `src/main/resources/META-INF/resources/styles.css` is the one project-level stylesheet (loaded via `@StyleSheet("styles.css")` in `Application.java`). It defines `--dialect-*` design tokens (Dialect design system: primary orange `#E86C00`, cool-gray background, card radius/shadow) and aliases them onto Aura's own CSS custom properties (`--aura-accent-color-*`, `--aura-background-color-*`, `--aura-orange`, `--aura-yellow`) rather than fighting the theme. Aura tokens use OKLCH + relative-color syntax and accept plain hex overrides. When restyling, prefer extending this alias layer over hardcoding new colors in components. diff --git a/package-lock.json b/package-lock.json index 843d979..6068953 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@vaadin/vaadin-usage-statistics": "2.1.3", "apexcharts": "5.15.2", "date-fns": "4.1.0", + "gridstack": "13.1.0", "lit": "3.3.3", "ol": "10.6.1", "proj4": "2.17.0", @@ -5304,6 +5305,22 @@ "dev": true, "license": "ISC" }, + "node_modules/gridstack": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/gridstack/-/gridstack-13.1.0.tgz", + "integrity": "sha512-cOGK9Ksy0mzTOTbEI32vpZdCby2WbzbnZcYxgASopkJWg8beBpp7gRhiTNROUtTHbDansXNOxoCfkyRTlx5DNw==", + "funding": [ + { + "type": "paypal", + "url": "https://www.paypal.me/alaind831" + }, + { + "type": "venmo", + "url": "https://www.venmo.com/adumesny" + } + ], + "license": "MIT" + }, "node_modules/has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", diff --git a/package.json b/package.json index d5ab112..17b242f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@vaadin/vaadin-usage-statistics": "2.1.3", "apexcharts": "5.15.2", "date-fns": "4.1.0", + "gridstack": "13.1.0", "lit": "3.3.3", "ol": "10.6.1", "proj4": "2.17.0", @@ -61,6 +62,7 @@ "@vaadin/vaadin-usage-statistics": "2.1.3", "apexcharts": "5.15.2", "date-fns": "4.1.0", + "gridstack": "13.1.0", "lit": "3.3.3", "ol": "10.6.1", "proj4": "2.17.0", @@ -95,7 +97,7 @@ "workbox-routing": "7.4.1", "workbox-strategies": "7.4.1" }, - "hash": "54350847a380a8c1b6cc526f6f7eff8dd9253d848e38919e7e1b633cec9f3aca", + "hash": "a8466446482b2201dfcaacaf427dabc79f870e7716e2d8f65cf31eca3870ef6a", "overrides": { "@fortawesome/fontawesome-free": "$@fortawesome/fontawesome-free", "@vaadin/a11y-base": "25.2.0", @@ -177,6 +179,7 @@ "@vaadin/virtual-list": "25.2.0", "apexcharts": "$apexcharts", "date-fns": "$date-fns", + "gridstack": "$gridstack", "lit": "$lit", "ol": "$ol", "proj4": "$proj4", @@ -277,6 +280,7 @@ "glob": "13.0.6" }, "apexcharts": "$apexcharts", - "@fortawesome/fontawesome-free": "$@fortawesome/fontawesome-free" + "@fortawesome/fontawesome-free": "$@fortawesome/fontawesome-free", + "gridstack": "$gridstack" } } \ No newline at end of file diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle index 459dfed..c6f3e12 100644 Binary files a/src/main/bundles/dev.bundle and b/src/main/bundles/dev.bundle differ diff --git a/src/main/frontend/components/grid-stack.ts b/src/main/frontend/components/grid-stack.ts index 8d2f893..503803a 100644 --- a/src/main/frontend/components/grid-stack.ts +++ b/src/main/frontend/components/grid-stack.ts @@ -34,13 +34,15 @@ class GridStackLayout extends HTMLElement { this.classList.add('grid-stack'); const options = JSON.parse(optionsJson); - this.grid = GridStack.init(options, this); + const grid = GridStack.init(options, this); + if (!grid) return; + this.grid = grid; this.registerChildren(); this.restore(); - this.grid.on('change added removed', () => this.schedulePersist()); - this.grid.on('resizestop', () => window.dispatchEvent(new Event('resize'))); + grid.on('change added removed', () => this.schedulePersist()); + grid.on('resizestop', () => window.dispatchEvent(new Event('resize'))); } /** Drops any saved layout for this grid and re-applies the positions @@ -73,6 +75,15 @@ class GridStackLayout extends HTMLElement { mutation.removedNodes.forEach((node) => { if (!(node instanceof HTMLElement)) return; if (!(node as any).gridstackNode) return; + // gridstack's own _sortDom() re-appends children (within this + // same element) to match layout order after every + // move/resize, which fires childList mutations too. A + // reordered node is back in the (still connected) DOM by the + // time this callback runs; only a genuinely removed node is + // disconnected. Without this check, removeWidget() here + // would fire on gridstack's own reorder, re-triggering + // _sortDom() and looping forever. + if (node.isConnected) return; this.grid!.removeWidget(node, false); }); } diff --git a/src/main/java/com/example/components/GridStackItem.java b/src/main/java/com/example/components/GridStackItem.java index a9f180c..723a9a9 100644 --- a/src/main/java/com/example/components/GridStackItem.java +++ b/src/main/java/com/example/components/GridStackItem.java @@ -27,7 +27,11 @@ public class GridStackItem extends Div { getElement().setAttribute("gs-h", String.valueOf(h)); this.content.addClassName("grid-stack-item-content"); - add(this.content); + // Attach the wrapper div directly through the element API — calling + // the overridden add(Component...) below would route it through + // `content.add(...)`, making the wrapper try to add itself as its + // own child. + getElement().appendChild(this.content.getElement()); this.content.add(content); } @@ -80,7 +84,7 @@ public class GridStackItem extends Div { } /** Snapshot of a widget's grid placement, as reported by the client after a - * drag/resize (see {@link GridStackLayout#onLayoutChange(String)}). */ + * drag/resize. */ public record Position(String id, int x, int y, int w, int h) { } } diff --git a/src/main/resources/META-INF/resources/styles.css b/src/main/resources/META-INF/resources/styles.css index fd72668..2feed68 100644 --- a/src/main/resources/META-INF/resources/styles.css +++ b/src/main/resources/META-INF/resources/styles.css @@ -121,4 +121,32 @@ vaadin-app-layout::part(content) { font-weight: 600; color: var(--dialect-ink); margin: 0 0 12px 0; +} + +/* GridStackLayout (components/GridStackLayout.java): extend the same alias + layer used by .dialect-card rather than hardcoding new colors. */ +.grid-stack-item-content { + background: var(--dialect-surface); + border-radius: var(--dialect-radius); + box-shadow: var(--dialect-shadow); + overflow: auto; +} + +/* A Card placed inside a grid item should fill the item instead of + stacking its own surface/shadow on top of grid-stack-item-content's. */ +.grid-stack-item-content > .dialect-card { + height: 100%; + box-shadow: none; +} + +.grid-stack-item-content .ui-resizable-handle { + background: var(--dialect-primary); + opacity: 0.6; + border-radius: 2px; +} + +.grid-stack-placeholder > .placeholder-content { + background: var(--dialect-bg); + border: 2px dashed var(--dialect-border); + border-radius: var(--dialect-radius); } \ No newline at end of file