@bsuite/page-builder 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/PageGridLayout.d.ts.map +1 -1
- package/dist/PageGridLayout.js +154 -9
- package/dist/autoHeight.d.ts +49 -0
- package/dist/autoHeight.d.ts.map +1 -0
- package/dist/autoHeight.js +16 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/types.d.ts +27 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/usePageGridLayout.d.ts.map +1 -1
- package/dist/usePageGridLayout.js +81 -5
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -7,7 +7,44 @@ Exports:
|
|
|
7
7
|
- `PageGridLayout`
|
|
8
8
|
- `usePageGridLayout`
|
|
9
9
|
- `rescaleLayout`
|
|
10
|
+
- `computeAutoHeightRows`
|
|
10
11
|
- `GridLayouts`, `GridLayoutItem`, `WidgetMeta`, and related types
|
|
11
12
|
|
|
12
13
|
Consumer apps must depend on the published npm package, not `workspace:*` or
|
|
13
14
|
`file:` links, because each app deploys independently.
|
|
15
|
+
|
|
16
|
+
## Measured auto-height (`autoHeight`, 0.5.0+)
|
|
17
|
+
|
|
18
|
+
Opt a widget into content-driven height by setting `autoHeight: true` on its
|
|
19
|
+
`GridLayoutItem`:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const defaultLayouts: GridLayouts = {
|
|
23
|
+
lg: [
|
|
24
|
+
// Fixed card — user resizes it manually, height persists.
|
|
25
|
+
{ i: 'summary', x: 0, y: 0, w: 6, h: 8 },
|
|
26
|
+
// Measured card — height tracks rendered content; `h` is only the
|
|
27
|
+
// initial seed used until the first measurement lands.
|
|
28
|
+
{ i: 'details', x: 6, y: 0, w: 6, h: 20, autoHeight: true },
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Behaviour and guarantees:
|
|
34
|
+
|
|
35
|
+
- A `ResizeObserver` on an unconstrained measure wrapper reads the content's
|
|
36
|
+
intrinsic height; `computeAutoHeightRows` converts px → rows using the
|
|
37
|
+
same `rowHeight`/`margin` the grid renders with, so the loop converges to
|
|
38
|
+
a fixed point instead of oscillating.
|
|
39
|
+
- Auto-height items render with `isResizable: false` automatically — do not
|
|
40
|
+
also wire a manual resize path for them.
|
|
41
|
+
- Measured rows are **derived, in-memory-only state**: merged over the saved
|
|
42
|
+
layout at render time for every viewer, and **never persisted**. A
|
|
43
|
+
read-only viewer switching tabs inside a card produces zero preference
|
|
44
|
+
writes. User-driven `x`/`y`/`w` (and `h` for non-autoHeight items)
|
|
45
|
+
persistence is unchanged.
|
|
46
|
+
- Updates are rAF-batched (all cards settling in a frame apply as one state
|
|
47
|
+
update) and suppressed mid-drag/mid-resize, then flushed on gesture end.
|
|
48
|
+
|
|
49
|
+
When adding `autoHeight` to an existing page's layout, bump that page's
|
|
50
|
+
`layoutVersion` so stale persisted layouts (without the flag) are reset.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageGridLayout.d.ts","sourceRoot":"","sources":["../src/PageGridLayout.tsx"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"PageGridLayout.d.ts","sourceRoot":"","sources":["../src/PageGridLayout.tsx"],"names":[],"mappings":"AACA,OAAO,KAA6E,MAAM,OAAO,CAAC;AAGlG,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAKxC,OAAO,KAAK,EAAe,mBAAmB,EAAE,MAAM,YAAY,CAAC;AA0QnE,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,OAAO,EACP,UAAU,EACV,SAAS,EAMT,WAAkB,EAClB,aAAsC,EACtC,yBAAiE,EACjE,kBAAkB,EAClB,sBAAsB,GACvB,EAAE,mBAAmB,qBAooBrB"}
|
package/dist/PageGridLayout.js
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ArrowDown, ArrowUp, ChevronDown, ChevronUp, Eye, EyeOff, Layers, LayoutGrid, Lock, Plus, RotateCcw, Save, Settings2, Unlock } from 'lucide-react';
|
|
3
|
-
import React, { startTransition, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import React, { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { Responsive } from 'react-grid-layout';
|
|
5
5
|
import { gridBounds, minMaxSize, minSize } from 'react-grid-layout/core';
|
|
6
6
|
import 'react-grid-layout/css/styles.css';
|
|
7
7
|
import 'react-resizable/css/styles.css';
|
|
8
|
+
import { computeAutoHeightRows } from './autoHeight.js';
|
|
8
9
|
import { defaultPreferenceAdapter } from './preferences.js';
|
|
9
10
|
import { usePageGridLayout } from './usePageGridLayout.js';
|
|
10
11
|
import { cn } from './utils.js';
|
|
12
|
+
/**
|
|
13
|
+
* Single source of truth for the grid's pixel geometry — read by both the
|
|
14
|
+
* `<Responsive>` props below AND `GridItem`'s auto-height measurement
|
|
15
|
+
* (blueprint amendment A1). Previously these were separate inline literals
|
|
16
|
+
* (`rowHeight={32}`, `margin={[6, 6]}`) with no shared reference, which is
|
|
17
|
+
* exactly the kind of drift the auto-height px→row conversion cannot
|
|
18
|
+
* tolerate — `computeAutoHeightRows` must use the *same* rowHeight/margin
|
|
19
|
+
* the grid itself renders with, or the computed row count would target the
|
|
20
|
+
* wrong pixel size.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_ROW_HEIGHT = 32;
|
|
23
|
+
const DEFAULT_MARGIN = [6, 6];
|
|
24
|
+
/**
|
|
25
|
+
* Fixed non-content chrome added before converting measured content px to
|
|
26
|
+
* rows — currently just the unconditional `border border-border` (1px top
|
|
27
|
+
* + 1px bottom) added to every card surface (blueprint item 1).
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_CARD_CHROME_PX = 2;
|
|
11
30
|
/**
|
|
12
31
|
* Default resize bounds applied as a *global* constraint to ALL grid items.
|
|
13
32
|
*
|
|
@@ -55,7 +74,60 @@ const DEFAULT_ADD_ENTITY_WIDGET_EVENT_NAMES = ['bsu-add-entity-widget', 'crm7-ad
|
|
|
55
74
|
const RGLResizeHandle = React.forwardRef(function RGLResizeHandle({ axis = 'se', className: injectedClassName, ...rest }, ref) {
|
|
56
75
|
return (_jsx("span", { ref: ref, "data-no-drag": true, className: `react-resizable-handle react-resizable-handle-${axis}${injectedClassName ? ' ' + injectedClassName : ''}`, ...rest }));
|
|
57
76
|
});
|
|
58
|
-
const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, isEditing, label, onHide, children: injectedChildren, className: injectedClassName, style: injectedStyle, ...rest }, ref) {
|
|
77
|
+
const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, isEditing, label, onHide, autoHeight, onAutoHeightChange, children: injectedChildren, className: injectedClassName, style: injectedStyle, ...rest }, ref) {
|
|
78
|
+
// Auto-height measurement (blueprint amendment A1). `measureRef` wraps
|
|
79
|
+
// `content` with NO height constraint of its own — its parent has
|
|
80
|
+
// `overflow-hidden` + `flex-1 min-h-0` (clips *painting* to the current
|
|
81
|
+
// grid cell height, per MDN's overflow semantics), but that never
|
|
82
|
+
// shrinks this child's own computed box height. So
|
|
83
|
+
// `entry.contentRect.height` here is always the content's true
|
|
84
|
+
// intrinsic size, independent of however many rows the item currently
|
|
85
|
+
// occupies — the property that makes the auto-height loop converge to
|
|
86
|
+
// a fixed point instead of oscillating (see autoHeight.ts).
|
|
87
|
+
const measureRef = useRef(null);
|
|
88
|
+
const lastReportedRowsRef = useRef(null);
|
|
89
|
+
const measureRafRef = useRef(null);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!autoHeight || !onAutoHeightChange)
|
|
92
|
+
return;
|
|
93
|
+
const el = measureRef.current;
|
|
94
|
+
if (!el || typeof ResizeObserver === 'undefined')
|
|
95
|
+
return;
|
|
96
|
+
const ro = new ResizeObserver((entries) => {
|
|
97
|
+
const entry = entries[0];
|
|
98
|
+
if (!entry)
|
|
99
|
+
return;
|
|
100
|
+
const contentPx = entry.contentRect.height;
|
|
101
|
+
if (measureRafRef.current !== null)
|
|
102
|
+
return;
|
|
103
|
+
const schedule = typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function'
|
|
104
|
+
? window.requestAnimationFrame.bind(window)
|
|
105
|
+
: (cb) => setTimeout(cb, 0);
|
|
106
|
+
measureRafRef.current = schedule(() => {
|
|
107
|
+
measureRafRef.current = null;
|
|
108
|
+
const rows = computeAutoHeightRows({
|
|
109
|
+
contentPx,
|
|
110
|
+
cardChromePx: DEFAULT_CARD_CHROME_PX,
|
|
111
|
+
rowHeightPx: DEFAULT_ROW_HEIGHT,
|
|
112
|
+
marginYPx: DEFAULT_MARGIN[1],
|
|
113
|
+
});
|
|
114
|
+
if (rows !== lastReportedRowsRef.current) {
|
|
115
|
+
lastReportedRowsRef.current = rows;
|
|
116
|
+
onAutoHeightChange(id, rows);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
ro.observe(el);
|
|
121
|
+
return () => {
|
|
122
|
+
ro.disconnect();
|
|
123
|
+
if (measureRafRef.current !== null &&
|
|
124
|
+
typeof window !== 'undefined' &&
|
|
125
|
+
typeof window.cancelAnimationFrame === 'function') {
|
|
126
|
+
window.cancelAnimationFrame(measureRafRef.current);
|
|
127
|
+
}
|
|
128
|
+
measureRafRef.current = null;
|
|
129
|
+
};
|
|
130
|
+
}, [autoHeight, id, onAutoHeightChange]);
|
|
59
131
|
// DnD root-cause fix (2026-05-07): react-draggable@4 (used internally by
|
|
60
132
|
// react-grid-layout@2) checks the drag handle via
|
|
61
133
|
// `matchesSelectorAndParentsTo(target, handle, baseNode)` — it walks UP
|
|
@@ -73,7 +145,7 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
|
|
|
73
145
|
return (_jsxs("div", { ref: ref, className: outerClass, style: injectedStyle, ...rest, children: [_jsxs("div", { className: "h-full w-full relative", children: [isEditing && (_jsx("div", { className: "absolute inset-0 z-10 pointer-events-none rounded-3xl border-2 border-transparent group-hover:border-primary/50 transition-colors bg-black/5" })), isEditing && (_jsx("div", { className: "absolute top-2 left-2 z-30 flex items-center gap-1 pointer-events-none", children: _jsx("span", { className: "text-[10px] px-1.5 py-0.5 rounded-md opacity-80 font-medium bg-muted text-muted-foreground", children: label }) })), isEditing && (_jsx("button", { className: "absolute top-2 right-2 z-30 h-6 w-6 rounded-full flex items-center justify-center bg-destructive/80 hover:bg-destructive text-white shadow transition-colors", "data-no-drag": true, onPointerDown: (event) => event.stopPropagation(), onClick: (event) => {
|
|
74
146
|
event.stopPropagation();
|
|
75
147
|
onHide(id);
|
|
76
|
-
}, title: `Hide ${label}`, "aria-label": `Hide ${label}`, children: _jsx(EyeOff, { className: "h-3 w-3", "aria-hidden": "true" }) })), _jsx("div", { className: "h-full w-full rounded-3xl transition-all flex flex-col bg-card shadow-sm", style: { contain: 'layout style' }, children: _jsx("div", { className:
|
|
148
|
+
}, title: `Hide ${label}`, "aria-label": `Hide ${label}`, children: _jsx(EyeOff, { className: "h-3 w-3", "aria-hidden": "true" }) })), _jsx("div", { className: "h-full w-full rounded-3xl transition-all flex flex-col bg-card border border-border shadow-sm dark:shadow-[var(--glow-card,none)]", style: { contain: 'layout style' }, children: _jsx("div", { className: cn('flex-1 min-h-0', autoHeight ? 'overflow-hidden' : 'overflow-auto'), children: autoHeight ? _jsx("div", { ref: measureRef, children: content }) : content }) })] }), injectedChildren] }));
|
|
77
149
|
}));
|
|
78
150
|
export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, widgets, widgetMeta, className,
|
|
79
151
|
// Resize default flipped 2026-05-12: rgl v2 dropped auto-handle rendering
|
|
@@ -82,7 +154,7 @@ export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVer
|
|
|
82
154
|
// the safe default is true so any consumer who forgets to set it still
|
|
83
155
|
// gets a usable canvas. Opt out per-page with isResizable={false}.
|
|
84
156
|
isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_NAMES, createEntityWidget, onRegisterEntityWidget, }) {
|
|
85
|
-
const { currentLayouts, layoutCols, isEditing, setIsEditing, activeCols, activeCompactor, onLayoutChange, handleColumnChange, handleCompact, handleReset, addWidget, moveWidget, setWidgetLocked, resetConfirmOpen, setResetConfirmOpen, containerRef, containerWidth, } = usePageGridLayout({
|
|
157
|
+
const { currentLayouts, layoutCols, isEditing, setIsEditing, activeCols, activeCompactor, onLayoutChange, handleColumnChange, handleCompact, handleReset, addWidget, moveWidget, setWidgetLocked, applyAutoHeightRows, autoHeightRows, resetConfirmOpen, setResetConfirmOpen, containerRef, containerWidth, } = usePageGridLayout({
|
|
86
158
|
pageKey,
|
|
87
159
|
defaultLayouts,
|
|
88
160
|
defaultCols,
|
|
@@ -91,6 +163,52 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
|
|
|
91
163
|
editorEventNames,
|
|
92
164
|
preferenceAdapter,
|
|
93
165
|
});
|
|
166
|
+
// Auto-height dispatcher (blueprint amendment A1, hardened per quality
|
|
167
|
+
// review 2026-07-14). `GridItem`'s ResizeObserver calls this per-widget
|
|
168
|
+
// whenever its measured content height changes; we rAF-batch and flush ALL
|
|
169
|
+
// pending widgets in ONE `applyAutoHeightRows` call — a single functional
|
|
170
|
+
// state update, so two cards settling in the same frame (e.g. card2+card3
|
|
171
|
+
// on /people/:id mount) can never last-writer-wins each other. Measured
|
|
172
|
+
// rows are DERIVED, in-memory-only state: they merge into `activeLayouts`
|
|
173
|
+
// below for ALL viewers and are never persisted (see usePageGridLayout).
|
|
174
|
+
// Updates are suppressed entirely while the user is mid-drag/mid-resize so
|
|
175
|
+
// auto-height never fights a manual gesture. A suppressed update is
|
|
176
|
+
// queued, not dropped — the drag/resize *Stop handlers flush whatever's
|
|
177
|
+
// pending the moment the gesture ends.
|
|
178
|
+
const isInteractingRef = useRef(false);
|
|
179
|
+
const pendingAutoHeightRef = useRef(new Map());
|
|
180
|
+
const autoHeightFrameRef = useRef(null);
|
|
181
|
+
const scheduleFrame = useCallback((callback) => {
|
|
182
|
+
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
|
|
183
|
+
return window.requestAnimationFrame(callback);
|
|
184
|
+
}
|
|
185
|
+
return setTimeout(callback, 0);
|
|
186
|
+
}, []);
|
|
187
|
+
const flushAutoHeightUpdates = useCallback(() => {
|
|
188
|
+
autoHeightFrameRef.current = null;
|
|
189
|
+
if (isInteractingRef.current)
|
|
190
|
+
return;
|
|
191
|
+
const pending = pendingAutoHeightRef.current;
|
|
192
|
+
if (pending.size === 0)
|
|
193
|
+
return;
|
|
194
|
+
pendingAutoHeightRef.current = new Map();
|
|
195
|
+
applyAutoHeightRows(Object.fromEntries(pending));
|
|
196
|
+
}, [applyAutoHeightRows]);
|
|
197
|
+
const handleAutoHeightChange = useCallback((widgetKey, rows) => {
|
|
198
|
+
pendingAutoHeightRef.current.set(widgetKey, rows);
|
|
199
|
+
if (isInteractingRef.current || autoHeightFrameRef.current !== null)
|
|
200
|
+
return;
|
|
201
|
+
autoHeightFrameRef.current = scheduleFrame(flushAutoHeightUpdates);
|
|
202
|
+
}, [flushAutoHeightUpdates, scheduleFrame]);
|
|
203
|
+
const handleInteractionStart = useCallback(() => {
|
|
204
|
+
isInteractingRef.current = true;
|
|
205
|
+
}, []);
|
|
206
|
+
const handleInteractionStop = useCallback(() => {
|
|
207
|
+
isInteractingRef.current = false;
|
|
208
|
+
if (pendingAutoHeightRef.current.size > 0 && autoHeightFrameRef.current === null) {
|
|
209
|
+
autoHeightFrameRef.current = scheduleFrame(flushAutoHeightUpdates);
|
|
210
|
+
}
|
|
211
|
+
}, [flushAutoHeightUpdates, scheduleFrame]);
|
|
94
212
|
const resizeEnabled = isEditing && isResizable;
|
|
95
213
|
const resizeConstraints = useMemo(() => [
|
|
96
214
|
// `gridBounds` caps width at the active column count, so we don't need
|
|
@@ -123,10 +241,32 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
|
|
|
123
241
|
const activeLayouts = useMemo(() => {
|
|
124
242
|
const filtered = { lg: [] };
|
|
125
243
|
for (const bp in currentLayouts) {
|
|
126
|
-
filtered[bp] = (currentLayouts[bp] ?? [])
|
|
244
|
+
filtered[bp] = (currentLayouts[bp] ?? [])
|
|
245
|
+
.filter((item) => renderableWidgetKeys.has(item.i) && !hiddenLayerIds[item.i])
|
|
246
|
+
// autoHeight items are measured, not manually resized (blueprint
|
|
247
|
+
// amendment A1) — force isResizable: false centrally here so every
|
|
248
|
+
// call site that sets autoHeight: true gets this for free, rather
|
|
249
|
+
// than needing to remember to also set isResizable itself.
|
|
250
|
+
//
|
|
251
|
+
// Measured rows (quality-review design ruling, 2026-07-14) merge
|
|
252
|
+
// over the saved/base `h`/`minH` HERE — the render layer — so the
|
|
253
|
+
// grid renders full-height cards for every viewer while the saved
|
|
254
|
+
// layout (and thus the preference adapter) never sees a measured
|
|
255
|
+
// height. Until the first measurement lands, `autoHeightRows` has
|
|
256
|
+
// no entry and the item's seed `h` renders as-is.
|
|
257
|
+
.map((item) => {
|
|
258
|
+
if (!item.autoHeight)
|
|
259
|
+
return item;
|
|
260
|
+
const measuredRows = autoHeightRows[item.i];
|
|
261
|
+
return {
|
|
262
|
+
...item,
|
|
263
|
+
isResizable: false,
|
|
264
|
+
...(measuredRows !== undefined ? { h: measuredRows, minH: measuredRows } : {}),
|
|
265
|
+
};
|
|
266
|
+
});
|
|
127
267
|
}
|
|
128
268
|
return filtered;
|
|
129
|
-
}, [currentLayouts, hiddenLayerIds, renderableWidgetKeys]);
|
|
269
|
+
}, [autoHeightRows, currentLayouts, hiddenLayerIds, renderableWidgetKeys]);
|
|
130
270
|
const hideLayer = (layerId) => {
|
|
131
271
|
setHiddenLayerIds((previous) => ({ ...previous, [layerId]: true }));
|
|
132
272
|
};
|
|
@@ -295,7 +435,7 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
|
|
|
295
435
|
}) })] })), layerItems.length > 0 && (_jsxs("div", { className: "basis-full rounded-md border border-border bg-background/80 p-2", children: [_jsxs("div", { className: "mb-2 flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), "Layers"] }), _jsx("div", { className: "flex flex-col gap-1", children: layerItems.map((layer) => (_jsxs("div", { className: "flex items-center gap-1 rounded border border-border bg-card px-2 py-1", children: [_jsx("input", { type: "text", value: layer.label, onChange: (event) => handleLayerRename(layer.id, event.target.value), className: "min-w-0 flex-1 bg-transparent text-xs text-foreground outline-none", "aria-label": `Rename ${layer.label}` }), _jsx("button", { type: "button", onClick: () => moveWidget(layer.id, 'up'), disabled: !layer.canMoveUp, "aria-label": `Move ${layer.label} up`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40", children: _jsx(ArrowUp, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => moveWidget(layer.id, 'down'), disabled: !layer.canMoveDown, "aria-label": `Move ${layer.label} down`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40", children: _jsx(ArrowDown, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => setWidgetLocked(layer.id, !layer.locked), "aria-label": `${layer.locked ? 'Unlock' : 'Lock'} ${layer.label}`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", children: layer.locked ? _jsx(Lock, { className: "h-3.5 w-3.5" }) : _jsx(Unlock, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => hideLayer(layer.id), "aria-label": `Hide ${layer.label}`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", children: _jsx(EyeOff, { className: "h-3.5 w-3.5" }) })] }, layer.id))) })] })), _jsxs("button", { type: "button", onClick: handleCompact, className: cn('ml-auto inline-flex items-center rounded-md border border-border px-2 py-1 text-sm transition-colors', 'text-muted-foreground hover:bg-muted hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: [_jsx(Layers, { className: "h-4 w-4 mr-1" }), "Compact Layout"] }), _jsxs("button", { type: "button", onClick: () => setResetConfirmOpen(true), className: cn('inline-flex items-center rounded-md border border-border px-2 py-1 text-sm transition-colors', 'text-muted-foreground hover:bg-muted hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), "Reset to Default"] })] })] })), resetConfirmOpen && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4", role: "dialog", "aria-modal": "true", "aria-labelledby": "page-grid-reset-title", "aria-describedby": "page-grid-reset-description", children: _jsxs("div", { className: "max-w-md rounded-lg bg-card p-5 shadow-lg border border-border", children: [_jsx("h2", { id: "page-grid-reset-title", className: "text-lg font-semibold", children: "Reset page layout?" }), _jsx("p", { id: "page-grid-reset-description", className: "mt-2 text-sm text-muted-foreground", children: "This will discard your current layout and restore all widgets to their default positions." }), _jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [_jsx("button", { type: "button", ref: resetCancelButtonRef, onClick: () => setResetConfirmOpen(false), className: cn('inline-flex items-center rounded-md border border-border px-3 py-2 text-sm transition-colors', 'bg-card text-foreground hover:bg-muted hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: "Cancel" }), _jsx("button", { type: "button", ref: resetConfirmButtonRef, onClick: () => {
|
|
296
436
|
handleReset();
|
|
297
437
|
setResetConfirmOpen(false);
|
|
298
|
-
}, className: cn('inline-flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors', 'bg-destructive text-white shadow hover:bg-destructive/90', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/40 focus-visible:ring-offset-1'), children: "Reset to Default" })] })] }) })), _jsx("div", { ref: containerRef, className: cn('relative z-0 overflow-visible', isEditing && 'min-h-[200px] bg-muted/30'), "data-page-grid-editing": isEditing || undefined, children: _jsx("div", { className: "relative z-0", style: { opacity: containerWidth > 0 ? 1 : 0 }, "aria-busy": containerWidth <= 0, children: _jsx(Responsive, { width: Math.max(containerWidth, 1), className: "layout page-grid-canvas", layouts: activeLayouts, breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }, rowHeight:
|
|
438
|
+
}, className: cn('inline-flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors', 'bg-destructive text-white shadow hover:bg-destructive/90', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/40 focus-visible:ring-offset-1'), children: "Reset to Default" })] })] }) })), _jsx("div", { ref: containerRef, className: cn('relative z-0 overflow-visible', isEditing && 'min-h-[200px] bg-muted/30'), "data-page-grid-editing": isEditing || undefined, children: _jsx("div", { className: "relative z-0", style: { opacity: containerWidth > 0 ? 1 : 0 }, "aria-busy": containerWidth <= 0, children: _jsx(Responsive, { width: Math.max(containerWidth, 1), className: "layout page-grid-canvas", layouts: activeLayouts, breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }, rowHeight: DEFAULT_ROW_HEIGHT, onLayoutChange: onLayoutChange, dragConfig: {
|
|
299
439
|
enabled: isEditing,
|
|
300
440
|
handle: '.drag-handle',
|
|
301
441
|
bounded: false,
|
|
@@ -305,10 +445,15 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
|
|
|
305
445
|
handles: resizeHandles,
|
|
306
446
|
// Required for v2 — see RGLResizeHandle comment above.
|
|
307
447
|
handleComponent: (axis, ref) => (_jsx(RGLResizeHandle, { axis: axis, ref: ref })),
|
|
308
|
-
}, constraints: resizeEnabled ? resizeConstraints : undefined, compactor: activeCompactor, cols: activeCols, margin:
|
|
448
|
+
}, constraints: resizeEnabled ? resizeConstraints : undefined, compactor: activeCompactor, cols: activeCols, margin: DEFAULT_MARGIN,
|
|
449
|
+
// Suppress auto-height writes mid-gesture (blueprint amendment
|
|
450
|
+
// A1) — coexists with dragConfig/resizeConfig above; RGL v2's
|
|
451
|
+
// grouped config props and top-level *Start/*Stop callbacks are
|
|
452
|
+
// independent, not mutually exclusive.
|
|
453
|
+
onDragStart: handleInteractionStart, onDragStop: handleInteractionStop, onResizeStart: handleInteractionStart, onResizeStop: handleInteractionStop, children: activeLayouts.lg.map((layoutItem) => {
|
|
309
454
|
const content = allWidgets[layoutItem.i];
|
|
310
455
|
if (content === undefined || content === null)
|
|
311
456
|
return null;
|
|
312
|
-
return (_jsx(GridItem, { id: layoutItem.i, content: content, isEditing: isEditing, label: layerNames[layoutItem.i] || widgetMeta?.[layoutItem.i]?.label || layoutItem.i, onHide: hideLayer }, layoutItem.i));
|
|
457
|
+
return (_jsx(GridItem, { id: layoutItem.i, content: content, isEditing: isEditing, label: layerNames[layoutItem.i] || widgetMeta?.[layoutItem.i]?.label || layoutItem.i, onHide: hideLayer, autoHeight: layoutItem.autoHeight, onAutoHeightChange: handleAutoHeightChange }, layoutItem.i));
|
|
313
458
|
}) }) }) })] }));
|
|
314
459
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface ComputeAutoHeightRowsOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Measured intrinsic content height in pixels. Callers MUST measure an
|
|
4
|
+
* UNCONSTRAINED wrapper around the card content (see `GridItem`'s
|
|
5
|
+
* ResizeObserver in `PageGridLayout.tsx`) — `overflow: hidden` on an
|
|
6
|
+
* ancestor clips *painting* only, it does not shrink a descendant's own
|
|
7
|
+
* computed box height, so an unconstrained wrapper reports the content's
|
|
8
|
+
* true size regardless of the grid item's current allocated pixel height.
|
|
9
|
+
* That property is what makes this function converge to a fixed point
|
|
10
|
+
* (see the "idempotent" test below) instead of oscillating: the same
|
|
11
|
+
* content always measures the same, no matter how many rows the item
|
|
12
|
+
* currently occupies.
|
|
13
|
+
*/
|
|
14
|
+
contentPx: number;
|
|
15
|
+
/**
|
|
16
|
+
* Fixed non-content chrome (e.g. the card's top+bottom border added by
|
|
17
|
+
* the unconditional `border border-border` — blueprint item 1) added
|
|
18
|
+
* before converting to rows, so rounding/border pixels never clip
|
|
19
|
+
* content by a sub-row amount.
|
|
20
|
+
*/
|
|
21
|
+
cardChromePx: number;
|
|
22
|
+
/**
|
|
23
|
+
* Must match the `rowHeight` passed to `<Responsive>`. Single source of
|
|
24
|
+
* truth: `DEFAULT_ROW_HEIGHT` in `PageGridLayout.tsx`.
|
|
25
|
+
*/
|
|
26
|
+
rowHeightPx: number;
|
|
27
|
+
/**
|
|
28
|
+
* Must match the vertical component of the `margin` tuple passed to
|
|
29
|
+
* `<Responsive>`. Single source of truth: `DEFAULT_MARGIN` in
|
|
30
|
+
* `PageGridLayout.tsx`.
|
|
31
|
+
*/
|
|
32
|
+
marginYPx: number;
|
|
33
|
+
/**
|
|
34
|
+
* Row-count floor. Defaults to 1 — a widget can never auto-collapse to 0
|
|
35
|
+
* rows even if content momentarily measures 0px (e.g. before first paint).
|
|
36
|
+
*/
|
|
37
|
+
minRows?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Pure px→row conversion for auto-height grid items (blueprint amendment
|
|
41
|
+
* A1). Deliberately uses `Math.ceil`, NOT react-grid-layout's own internal
|
|
42
|
+
* `Math.round` convention (see `calcWH` in react-grid-layout's compiled
|
|
43
|
+
* source) — rounding down would silently clip content roughly half the
|
|
44
|
+
* time. Ceiling always over-allocates rather than under-allocates, trading
|
|
45
|
+
* a few pixels of empty card padding for a guarantee that content is never
|
|
46
|
+
* cut off.
|
|
47
|
+
*/
|
|
48
|
+
export declare function computeAutoHeightRows({ contentPx, cardChromePx, rowHeightPx, marginYPx, minRows, }: ComputeAutoHeightRowsOptions): number;
|
|
49
|
+
//# sourceMappingURL=autoHeight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autoHeight.d.ts","sourceRoot":"","sources":["../src/autoHeight.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,4BAA4B;IAC3C;;;;;;;;;;;OAWG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,YAAY,EACZ,WAAW,EACX,SAAS,EACT,OAAW,GACZ,EAAE,4BAA4B,GAAG,MAAM,CAKvC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure px→row conversion for auto-height grid items (blueprint amendment
|
|
3
|
+
* A1). Deliberately uses `Math.ceil`, NOT react-grid-layout's own internal
|
|
4
|
+
* `Math.round` convention (see `calcWH` in react-grid-layout's compiled
|
|
5
|
+
* source) — rounding down would silently clip content roughly half the
|
|
6
|
+
* time. Ceiling always over-allocates rather than under-allocates, trading
|
|
7
|
+
* a few pixels of empty card padding for a guarantee that content is never
|
|
8
|
+
* cut off.
|
|
9
|
+
*/
|
|
10
|
+
export function computeAutoHeightRows({ contentPx, cardChromePx, rowHeightPx, marginYPx, minRows = 1, }) {
|
|
11
|
+
const rowUnitPx = rowHeightPx + marginYPx;
|
|
12
|
+
if (rowUnitPx <= 0)
|
|
13
|
+
return minRows;
|
|
14
|
+
const raw = Math.ceil((contentPx + cardChromePx) / rowUnitPx);
|
|
15
|
+
return Math.max(minRows, raw);
|
|
16
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { PageGridLayout } from './PageGridLayout.js';
|
|
|
2
2
|
export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.js';
|
|
3
3
|
export type { PageGridEditingEventDetail } from './usePageGridLayout.js';
|
|
4
4
|
export { rescaleLayout } from './rescaleLayout.js';
|
|
5
|
+
export { computeAutoHeightRows } from './autoHeight.js';
|
|
6
|
+
export type { ComputeAutoHeightRowsOptions } from './autoHeight.js';
|
|
5
7
|
export { WYSIWYG_REQUIRED_PRIMITIVES, WYSIWYG_SURFACE_CONTRACTS, assertWysiwygPrimitiveCoverage, getWysiwygSurfaceContract, } from './wysiwygContract.js';
|
|
6
8
|
export type { WysiwygPrimitive, WysiwygSurfaceContract, WysiwygSurfaceId, } from './wysiwygContract.js';
|
|
7
9
|
export { buildResponsiveLayouts, RESPONSIVE_BREAKPOINTS, } from './buildResponsiveLayouts.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EACL,iBAAiB,EACjB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EACzB,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,6BAA6B,EAC7B,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAChF,YAAY,EACV,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,uBAAuB,EACvB,UAAU,GACX,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EACL,iBAAiB,EACjB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxD,YAAY,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EACzB,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,6BAA6B,EAC7B,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAChF,YAAY,EACV,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,uBAAuB,EACvB,UAAU,GACX,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { PageGridLayout } from './PageGridLayout.js';
|
|
2
2
|
export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.js';
|
|
3
3
|
export { rescaleLayout } from './rescaleLayout.js';
|
|
4
|
+
export { computeAutoHeightRows } from './autoHeight.js';
|
|
4
5
|
export { WYSIWYG_REQUIRED_PRIMITIVES, WYSIWYG_SURFACE_CONTRACTS, assertWysiwygPrimitiveCoverage, getWysiwygSurfaceContract, } from './wysiwygContract.js';
|
|
5
6
|
export { buildResponsiveLayouts, RESPONSIVE_BREAKPOINTS, } from './buildResponsiveLayouts.js';
|
|
6
7
|
export { useLocalPreference, defaultPreferenceAdapter } from './preferences.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
import type { Compactor, LayoutItem, ResizeHandleAxis } from 'react-grid-layout';
|
|
3
|
-
export
|
|
3
|
+
export interface GridLayoutItem extends LayoutItem {
|
|
4
|
+
/**
|
|
5
|
+
* When true, this item's height is driven by its own measured content
|
|
6
|
+
* height rather than manual resize (blueprint amendment A1). `GridItem`
|
|
7
|
+
* mounts a `ResizeObserver` on an unconstrained content wrapper and
|
|
8
|
+
* reports the computed row count upward via `computeAutoHeightRows`;
|
|
9
|
+
* `PageGridLayout` force-sets `isResizable: false` for any item with
|
|
10
|
+
* `autoHeight: true` (see the `activeLayouts` memo) so callers don't need
|
|
11
|
+
* to remember to set both fields.
|
|
12
|
+
*/
|
|
13
|
+
autoHeight?: boolean;
|
|
14
|
+
}
|
|
4
15
|
export interface GridLayouts {
|
|
5
16
|
lg: GridLayoutItem[];
|
|
6
17
|
[key: string]: GridLayoutItem[];
|
|
@@ -67,6 +78,21 @@ export interface UsePageGridLayoutResult {
|
|
|
67
78
|
moveWidget: (widgetKey: string, direction: 'up' | 'down') => void;
|
|
68
79
|
setWidgetLocked: (widgetKey: string, locked: boolean) => void;
|
|
69
80
|
removeWidget: (widgetKey: string) => void;
|
|
81
|
+
/**
|
|
82
|
+
* Applies a batch of measured auto-height row counts (widgetKey -> rows)
|
|
83
|
+
* in a SINGLE functional state update. Measured heights are DERIVED,
|
|
84
|
+
* in-memory-only state: they are merged over the saved layout when
|
|
85
|
+
* producing the layouts handed to react-grid-layout (so every viewer
|
|
86
|
+
* renders full-height cards) and are NEVER written to the preference
|
|
87
|
+
* adapter — measurements re-derive on every mount, so persisting them
|
|
88
|
+
* would be redundant and would cause storage writes from mere viewing
|
|
89
|
+
* (e.g. switching tabs inside a card). Diff-guarded: returns the previous
|
|
90
|
+
* map identity when nothing changed, so no re-render occurs.
|
|
91
|
+
*/
|
|
92
|
+
applyAutoHeightRows: (rowsByWidget: Record<string, number>) => void;
|
|
93
|
+
/** Current measured auto-height overrides (widgetKey -> rows). In-memory
|
|
94
|
+
* only; see `applyAutoHeightRows`. */
|
|
95
|
+
autoHeightRows: Record<string, number>;
|
|
70
96
|
resetConfirmOpen: boolean;
|
|
71
97
|
setResetConfirmOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
72
98
|
canEditPage: boolean;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAEjF,MAAM,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePageGridLayout.d.ts","sourceRoot":"","sources":["../src/usePageGridLayout.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAGV,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,0BAA0B,2IAM7B,CAAC;AAEX;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;AAElE,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,cAAc,EACd,WAAgB,EAChB,aAAiB,EACjB,WAAkB,EAClB,gBAA6C,EAC7C,iBAA4C,GAC7C,EAAE,wBAAwB,GAAG,uBAAuB,
|
|
1
|
+
{"version":3,"file":"usePageGridLayout.d.ts","sourceRoot":"","sources":["../src/usePageGridLayout.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAGV,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,0BAA0B,2IAM7B,CAAC;AAEX;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;AAElE,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,cAAc,EACd,WAAgB,EAChB,aAAiB,EACjB,WAAkB,EAClB,gBAA6C,EAC7C,iBAA4C,GAC7C,EAAE,wBAAwB,GAAG,uBAAuB,CA8ZpD"}
|
|
@@ -141,6 +141,40 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
141
141
|
*/
|
|
142
142
|
const pendingLayoutRef = useRef(null);
|
|
143
143
|
const layoutCommitFrameRef = useRef(null);
|
|
144
|
+
/**
|
|
145
|
+
* Strips DERIVED measured heights out of a layout before it reaches the
|
|
146
|
+
* preference adapter. react-grid-layout is rendered with `autoHeightRows`
|
|
147
|
+
* merged over the saved layout (see `applyAutoHeightRows`), so the layouts
|
|
148
|
+
* it echoes back through `onLayoutChange` carry the MEASURED `h` for
|
|
149
|
+
* autoHeight items — persisting that verbatim would smuggle measured
|
|
150
|
+
* heights into user_preferences through the drag/resize path and drop the
|
|
151
|
+
* `autoHeight` flag (react-grid-layout does not round-trip custom item
|
|
152
|
+
* props). Restore `h`/`minH`/`autoHeight` from the un-merged base layout;
|
|
153
|
+
* user-driven `x`/`y`/`w` (and `h` for non-autoHeight items) pass through
|
|
154
|
+
* untouched. Reads the base via a ref so the trailing-rAF commit below
|
|
155
|
+
* never closes over a stale snapshot.
|
|
156
|
+
*/
|
|
157
|
+
const currentLayoutsForStripRef = useRef(null);
|
|
158
|
+
const stripAutoHeightRows = useCallback((layouts) => {
|
|
159
|
+
const base = currentLayoutsForStripRef.current;
|
|
160
|
+
if (!base)
|
|
161
|
+
return layouts;
|
|
162
|
+
const result = { lg: [] };
|
|
163
|
+
for (const bp of Object.keys(layouts)) {
|
|
164
|
+
const baseItems = base[bp] ?? base.lg ?? [];
|
|
165
|
+
const baseByKey = new Map(baseItems.map((item) => [item.i, item]));
|
|
166
|
+
result[bp] = (layouts[bp] ?? []).map((item) => {
|
|
167
|
+
const baseItem = baseByKey.get(item.i);
|
|
168
|
+
if (!baseItem?.autoHeight)
|
|
169
|
+
return item;
|
|
170
|
+
return { ...item, autoHeight: true, h: baseItem.h, minH: baseItem.minH };
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}, []);
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
currentLayoutsForStripRef.current = currentLayouts;
|
|
177
|
+
}, [currentLayouts]);
|
|
144
178
|
const onLayoutChange = useCallback((_layout, layouts) => {
|
|
145
179
|
if (!isEditing)
|
|
146
180
|
return;
|
|
@@ -148,7 +182,7 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
148
182
|
if (layoutCommitFrameRef.current !== null)
|
|
149
183
|
return;
|
|
150
184
|
if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') {
|
|
151
|
-
setSavedLayout(layouts);
|
|
185
|
+
setSavedLayout(stripAutoHeightRows(layouts));
|
|
152
186
|
return;
|
|
153
187
|
}
|
|
154
188
|
// Trailing rAF — lets a burst of resize ticks coalesce into one commit.
|
|
@@ -157,10 +191,10 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
157
191
|
const pending = pendingLayoutRef.current;
|
|
158
192
|
if (pending) {
|
|
159
193
|
pendingLayoutRef.current = null;
|
|
160
|
-
setSavedLayout(pending);
|
|
194
|
+
setSavedLayout(stripAutoHeightRows(pending));
|
|
161
195
|
}
|
|
162
196
|
});
|
|
163
|
-
}, [isEditing, setSavedLayout]);
|
|
197
|
+
}, [isEditing, setSavedLayout, stripAutoHeightRows]);
|
|
164
198
|
useEffect(() => {
|
|
165
199
|
return () => {
|
|
166
200
|
if (layoutCommitFrameRef.current !== null &&
|
|
@@ -267,6 +301,37 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
267
301
|
}
|
|
268
302
|
startTransition(() => setSavedLayout(updated));
|
|
269
303
|
}, [currentLayouts, setSavedLayout]);
|
|
304
|
+
/**
|
|
305
|
+
* Measured auto-height overrides (widgetKey -> rows). DERIVED, in-memory-
|
|
306
|
+
* only state (quality-review design ruling, 2026-07-14):
|
|
307
|
+
*
|
|
308
|
+
* - Applied for ALL viewers — the map is merged over the saved layout when
|
|
309
|
+
* producing the layouts handed to react-grid-layout (see
|
|
310
|
+
* `PageGridLayout`'s `activeLayouts` memo), so read-only users get
|
|
311
|
+
* full-height cards too.
|
|
312
|
+
* - NEVER persisted — not even while editing. Measurements re-derive on
|
|
313
|
+
* every mount; writing them to the preference adapter would be redundant
|
|
314
|
+
* AND would turn mere viewing (e.g. Radix tab switches inside a card,
|
|
315
|
+
* which unmount/remount panel content and fire the ResizeObserver) into
|
|
316
|
+
* storage upserts for any authenticated viewer.
|
|
317
|
+
* - Updated FUNCTIONALLY in one combined state update per flush, so two
|
|
318
|
+
* cards settling in the same animation frame can never last-writer-wins
|
|
319
|
+
* each other through a stale closure.
|
|
320
|
+
*/
|
|
321
|
+
const [autoHeightRows, setAutoHeightRows] = useState({});
|
|
322
|
+
const applyAutoHeightRows = useCallback((rowsByWidget) => {
|
|
323
|
+
setAutoHeightRows((previous) => {
|
|
324
|
+
let changed = false;
|
|
325
|
+
const next = { ...previous };
|
|
326
|
+
for (const [widgetKey, rows] of Object.entries(rowsByWidget)) {
|
|
327
|
+
if (next[widgetKey] !== rows) {
|
|
328
|
+
next[widgetKey] = rows;
|
|
329
|
+
changed = true;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return changed ? next : previous;
|
|
333
|
+
});
|
|
334
|
+
}, []);
|
|
270
335
|
useEffect(() => {
|
|
271
336
|
if (!canEditPage || typeof window === 'undefined')
|
|
272
337
|
return;
|
|
@@ -274,7 +339,16 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
274
339
|
const detail = event.detail;
|
|
275
340
|
if (!detail?.page || detail.page === pageKey) {
|
|
276
341
|
detail?.ack?.();
|
|
277
|
-
|
|
342
|
+
// `detail.editing` defaults to `true` when omitted — every existing
|
|
343
|
+
// dispatcher across all 6 apps only ever sent `{ page, path }` to
|
|
344
|
+
// OPEN the editor, so this preserves 100% backward compatibility.
|
|
345
|
+
// An explicit `editing: false` lets a launcher force-close a grid
|
|
346
|
+
// instance that's still mounted after an in-route navigation (e.g.
|
|
347
|
+
// wouter's `/people/:id` not remounting between different ids) —
|
|
348
|
+
// see crm7's `PageEditorLauncher.tsx` navigation-close effect
|
|
349
|
+
// (blueprint amendment A3). Reuses this exact event channel rather
|
|
350
|
+
// than introducing a second one.
|
|
351
|
+
setIsEditing(detail?.editing ?? true);
|
|
278
352
|
}
|
|
279
353
|
};
|
|
280
354
|
for (const eventName of editorEventNames) {
|
|
@@ -285,7 +359,7 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
285
359
|
window.removeEventListener(eventName, handler);
|
|
286
360
|
}
|
|
287
361
|
};
|
|
288
|
-
}, [canEditPage, editorEventNames, pageKey]);
|
|
362
|
+
}, [canEditPage, editorEventNames, pageKey, setIsEditing]);
|
|
289
363
|
return {
|
|
290
364
|
currentLayouts,
|
|
291
365
|
layoutCols,
|
|
@@ -301,6 +375,8 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
301
375
|
moveWidget,
|
|
302
376
|
setWidgetLocked,
|
|
303
377
|
removeWidget,
|
|
378
|
+
applyAutoHeightRows,
|
|
379
|
+
autoHeightRows,
|
|
304
380
|
resetConfirmOpen,
|
|
305
381
|
setResetConfirmOpen,
|
|
306
382
|
canEditPage,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsuite/page-builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Shared BSuite responsive page-builder grid and layout persistence primitives",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"prepublishOnly": "pnpm build"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
+
"@bsuite/theme": ">=0.5.0",
|
|
35
36
|
"@dnd-kit/core": "^6",
|
|
36
37
|
"@dnd-kit/sortable": "^10",
|
|
37
38
|
"@dnd-kit/utilities": "^3",
|
|
@@ -42,6 +43,9 @@
|
|
|
42
43
|
"react-resizable": "^3 || ^4"
|
|
43
44
|
},
|
|
44
45
|
"peerDependenciesMeta": {
|
|
46
|
+
"@bsuite/theme": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
45
49
|
"@dnd-kit/core": {
|
|
46
50
|
"optional": true
|
|
47
51
|
},
|