@bsuite/page-builder 0.7.0 → 0.8.0
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/dist/PageGridLayout.d.ts +1 -1
- package/dist/PageGridLayout.d.ts.map +1 -1
- package/dist/PageGridLayout.js +102 -19
- package/dist/buildResponsiveLayouts.d.ts +61 -4
- package/dist/buildResponsiveLayouts.d.ts.map +1 -1
- package/dist/buildResponsiveLayouts.js +77 -5
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/usePageGridLayout.d.ts +22 -1
- package/dist/usePageGridLayout.d.ts.map +1 -1
- package/dist/usePageGridLayout.js +197 -66
- package/package.json +1 -1
package/dist/PageGridLayout.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ import React from 'react';
|
|
|
2
2
|
import 'react-grid-layout/css/styles.css';
|
|
3
3
|
import 'react-resizable/css/styles.css';
|
|
4
4
|
import type { PageGridLayoutProps } from './types.js';
|
|
5
|
-
export declare function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, widgets, widgetMeta, className, isResizable, resizeHandles, tenantId, addEntityWidgetEventNames, createEntityWidget, onRegisterEntityWidget, relationshipCatalog, addRelationshipWidgetEventNames, createRelationshipWidget, onRegisterRelationshipWidget, onRelationshipWidgetRejected, }: PageGridLayoutProps): React.JSX.Element;
|
|
5
|
+
export declare function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, widgets, widgetMeta, className, isResizable, resizeHandles, tenantId, defaultAutoHeight, addEntityWidgetEventNames, createEntityWidget, onRegisterEntityWidget, relationshipCatalog, addRelationshipWidgetEventNames, createRelationshipWidget, onRegisterRelationshipWidget, onRelationshipWidgetRejected, }: PageGridLayoutProps): React.JSX.Element;
|
|
6
6
|
//# sourceMappingURL=PageGridLayout.d.ts.map
|
|
@@ -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,KAQN,MAAM,OAAO,CAAC;AAGf,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAMxC,OAAO,KAAK,EAAe,mBAAmB,EAA4B,MAAM,YAAY,CAAC;AA8U7F,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,QAAQ,EACR,iBAAiB,EACjB,yBAAiE,EACjE,kBAAkB,EAClB,sBAAsB,EACtB,mBAAmB,EACnB,+BAA6E,EAC7E,wBAAwB,EACxB,4BAA4B,EAC5B,4BAA4B,GAC7B,EAAE,mBAAmB,qBA6xBrB"}
|
package/dist/PageGridLayout.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import React, { startTransition, useCallback, useEffect, useLayoutEffect, 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';
|
|
@@ -97,6 +97,71 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
|
|
|
97
97
|
const measureRef = useRef(null);
|
|
98
98
|
const lastReportedRowsRef = useRef(null);
|
|
99
99
|
const measureRafRef = useRef(null);
|
|
100
|
+
/**
|
|
101
|
+
* Latest measured content height, updated on EVERY ResizeObserver entry.
|
|
102
|
+
*
|
|
103
|
+
* D-76 root cause ("cards render half cut off on page open"). The previous
|
|
104
|
+
* implementation captured `contentPx` in the rAF closure and then bailed
|
|
105
|
+
* out of any further entry that arrived before that frame ran:
|
|
106
|
+
*
|
|
107
|
+
* if (measureRafRef.current !== null) return; // <- measurement lost
|
|
108
|
+
*
|
|
109
|
+
* That is first-writer-wins inside a frame, and the first writer is
|
|
110
|
+
* routinely the WRONG one: a card mounts empty or skeletonised, the
|
|
111
|
+
* observer fires at (say) 90px and schedules the frame, the query resolves
|
|
112
|
+
* and the observer fires again at 760px — and that second entry returned
|
|
113
|
+
* early, discarding the real height. The frame then converged the card to
|
|
114
|
+
* ~3 rows. Because `computeAutoHeightRows` is a pure function of the height
|
|
115
|
+
* it is given, nothing re-triggered: the card stayed at the empty-state
|
|
116
|
+
* height with its content clipped by the `overflow-hidden` autoHeight
|
|
117
|
+
* wrapper until something else happened to resize it.
|
|
118
|
+
*
|
|
119
|
+
* Storing the height in a ref and READING IT INSIDE the frame makes it
|
|
120
|
+
* last-writer-wins, which is the only correct policy for "how tall is this
|
|
121
|
+
* content right now".
|
|
122
|
+
*/
|
|
123
|
+
const latestContentPxRef = useRef(null);
|
|
124
|
+
const reportRows = useCallback((contentPx) => {
|
|
125
|
+
if (!onAutoHeightChange)
|
|
126
|
+
return;
|
|
127
|
+
const rows = computeAutoHeightRows({
|
|
128
|
+
contentPx,
|
|
129
|
+
cardChromePx: DEFAULT_CARD_CHROME_PX,
|
|
130
|
+
rowHeightPx: DEFAULT_ROW_HEIGHT,
|
|
131
|
+
marginYPx: DEFAULT_MARGIN[1],
|
|
132
|
+
});
|
|
133
|
+
if (rows === lastReportedRowsRef.current)
|
|
134
|
+
return;
|
|
135
|
+
lastReportedRowsRef.current = rows;
|
|
136
|
+
onAutoHeightChange(id, rows);
|
|
137
|
+
}, [id, onAutoHeightChange]);
|
|
138
|
+
/**
|
|
139
|
+
* First measurement BEFORE paint (second half of D-76).
|
|
140
|
+
*
|
|
141
|
+
* A ResizeObserver's initial observation is delivered asynchronously, and
|
|
142
|
+
* the rAF hop below adds another frame. Until it lands the card renders at
|
|
143
|
+
* its seed `h` — 6 rows / 192px for a `DraggableCardPage` card — with
|
|
144
|
+
* `overflow-hidden`, so the first painted frame of a taller card is
|
|
145
|
+
* literally cut in half. That is what the operator sees "on page open",
|
|
146
|
+
* every open, on every surface with content taller than its seed.
|
|
147
|
+
*
|
|
148
|
+
* Measuring in a layout effect and reporting synchronously puts the correct
|
|
149
|
+
* row count into state within the same commit, so the browser never paints
|
|
150
|
+
* the clipped frame. React batches these updates across all GridItems
|
|
151
|
+
* mounting together, and `applyAutoHeightRows` is a functional update, so N
|
|
152
|
+
* cards settling at once cost one re-render and cannot clobber each other.
|
|
153
|
+
*/
|
|
154
|
+
useLayoutEffect(() => {
|
|
155
|
+
if (!autoHeight || !onAutoHeightChange)
|
|
156
|
+
return;
|
|
157
|
+
const el = measureRef.current;
|
|
158
|
+
if (!el)
|
|
159
|
+
return;
|
|
160
|
+
const contentPx = el.getBoundingClientRect().height;
|
|
161
|
+
latestContentPxRef.current = contentPx;
|
|
162
|
+
if (contentPx > 0)
|
|
163
|
+
reportRows(contentPx);
|
|
164
|
+
}, [autoHeight, onAutoHeightChange, reportRows]);
|
|
100
165
|
useEffect(() => {
|
|
101
166
|
if (!autoHeight || !onAutoHeightChange)
|
|
102
167
|
return;
|
|
@@ -107,7 +172,7 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
|
|
|
107
172
|
const entry = entries[0];
|
|
108
173
|
if (!entry)
|
|
109
174
|
return;
|
|
110
|
-
|
|
175
|
+
latestContentPxRef.current = entry.contentRect.height;
|
|
111
176
|
if (measureRafRef.current !== null)
|
|
112
177
|
return;
|
|
113
178
|
const schedule = typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function'
|
|
@@ -115,16 +180,10 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
|
|
|
115
180
|
: (cb) => setTimeout(cb, 0);
|
|
116
181
|
measureRafRef.current = schedule(() => {
|
|
117
182
|
measureRafRef.current = null;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
marginYPx: DEFAULT_MARGIN[1],
|
|
123
|
-
});
|
|
124
|
-
if (rows !== lastReportedRowsRef.current) {
|
|
125
|
-
lastReportedRowsRef.current = rows;
|
|
126
|
-
onAutoHeightChange(id, rows);
|
|
127
|
-
}
|
|
183
|
+
const contentPx = latestContentPxRef.current;
|
|
184
|
+
if (contentPx === null)
|
|
185
|
+
return;
|
|
186
|
+
reportRows(contentPx);
|
|
128
187
|
});
|
|
129
188
|
});
|
|
130
189
|
ro.observe(el);
|
|
@@ -137,7 +196,7 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
|
|
|
137
196
|
}
|
|
138
197
|
measureRafRef.current = null;
|
|
139
198
|
};
|
|
140
|
-
}, [autoHeight,
|
|
199
|
+
}, [autoHeight, onAutoHeightChange, reportRows]);
|
|
141
200
|
// DnD root-cause fix (2026-05-07): react-draggable@4 (used internally by
|
|
142
201
|
// react-grid-layout@2) checks the drag handle via
|
|
143
202
|
// `matchesSelectorAndParentsTo(target, handle, baseNode)` — it walks UP
|
|
@@ -163,8 +222,8 @@ export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVer
|
|
|
163
222
|
// adapters then defaulted it back to true. With v2's handle gap fixed,
|
|
164
223
|
// the safe default is true so any consumer who forgets to set it still
|
|
165
224
|
// gets a usable canvas. Opt out per-page with isResizable={false}.
|
|
166
|
-
isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, tenantId, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_NAMES, createEntityWidget, onRegisterEntityWidget, relationshipCatalog, addRelationshipWidgetEventNames = DEFAULT_ADD_RELATIONSHIP_WIDGET_EVENT_NAMES, createRelationshipWidget, onRegisterRelationshipWidget, onRelationshipWidgetRejected, }) {
|
|
167
|
-
const { currentLayouts, layoutCols, isEditing, setIsEditing, activeCols, activeCompactor, onLayoutChange, handleColumnChange, handleCompact, handleReset, addWidget, moveWidget, setWidgetLocked, applyAutoHeightRows, autoHeightRows, resetConfirmOpen, setResetConfirmOpen, containerRef, containerWidth, } = usePageGridLayout({
|
|
225
|
+
isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, tenantId, defaultAutoHeight, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_NAMES, createEntityWidget, onRegisterEntityWidget, relationshipCatalog, addRelationshipWidgetEventNames = DEFAULT_ADD_RELATIONSHIP_WIDGET_EVENT_NAMES, createRelationshipWidget, onRegisterRelationshipWidget, onRelationshipWidgetRejected, }) {
|
|
226
|
+
const { currentLayouts, layoutCols, isEditing, setIsEditing, activeCols, activeCompactor, onLayoutChange, handleColumnChange, handleBreakpointChange, handleCompact, handleReset, addWidget, moveWidget, setWidgetLocked, applyAutoHeightRows, autoHeightRows, resetConfirmOpen, setResetConfirmOpen, containerRef, containerWidth, } = usePageGridLayout({
|
|
168
227
|
pageKey,
|
|
169
228
|
defaultLayouts,
|
|
170
229
|
defaultCols,
|
|
@@ -172,6 +231,7 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, tenantId, addEntityW
|
|
|
172
231
|
canEditPage,
|
|
173
232
|
editorEventNames,
|
|
174
233
|
preferenceAdapter,
|
|
234
|
+
defaultAutoHeight,
|
|
175
235
|
});
|
|
176
236
|
// Auto-height dispatcher (blueprint amendment A1, hardened per quality
|
|
177
237
|
// review 2026-07-14). `GridItem`'s ResizeObserver calls this per-widget
|
|
@@ -309,9 +369,28 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, tenantId, addEntityW
|
|
|
309
369
|
// Until the first measurement lands, `autoHeightRows` has no entry and
|
|
310
370
|
// the item's seed `h` renders as-is.
|
|
311
371
|
.map((item) => {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
//
|
|
372
|
+
// HEAL a persisted `isResizable: false` on ANY item, not just an
|
|
373
|
+
// autoHeight one (D-75: "individual cards can no longer be resized").
|
|
374
|
+
//
|
|
375
|
+
// The 0.6.0 heal was scoped to `item.autoHeight`, which left the
|
|
376
|
+
// whole non-autoHeight population — every card that opted out, and
|
|
377
|
+
// every layout persisted before `autoHeight` existed as a concept —
|
|
378
|
+
// permanently unresizable with no way back: the flag is re-persisted
|
|
379
|
+
// on each load, so clearing it by hand does not survive a refresh.
|
|
380
|
+
//
|
|
381
|
+
// The one legitimate source of a per-item `isResizable: false` is the
|
|
382
|
+
// Layers "lock" control, which writes it together with `static: true`
|
|
383
|
+
// and `isDraggable: false`. Requiring that full signature separates a
|
|
384
|
+
// deliberate lock (kept) from the 0.5.2 leak (dropped), so the heal
|
|
385
|
+
// can be widened without silently unlocking anything a user locked.
|
|
386
|
+
const lockedDeliberately = item.static === true || item.isDraggable === false;
|
|
387
|
+
const needsResizeHeal = item.isResizable === false && !lockedDeliberately;
|
|
388
|
+
if (!item.autoHeight) {
|
|
389
|
+
if (!needsResizeHeal)
|
|
390
|
+
return item;
|
|
391
|
+
const { isResizable: _dropped, ...unlocked } = item;
|
|
392
|
+
return unlocked;
|
|
393
|
+
}
|
|
315
394
|
//
|
|
316
395
|
// 0.5.2 set this at the render layer, and it leaked out through
|
|
317
396
|
// `onLayoutChange` into the SAVED layout. Removing the override in
|
|
@@ -565,7 +644,11 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, tenantId, addEntityW
|
|
|
565
644
|
}) })] })), 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-overlay/50 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: () => {
|
|
566
645
|
handleReset();
|
|
567
646
|
setResetConfirmOpen(false);
|
|
568
|
-
}, className: cn('inline-flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors', 'bg-destructive text-destructive-foreground 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,
|
|
647
|
+
}, className: cn('inline-flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors', 'bg-destructive text-destructive-foreground 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,
|
|
648
|
+
// Tells the hook which breakpoint a gesture belongs to, so the edit
|
|
649
|
+
// is folded back onto `lg` instead of into a derived breakpoint
|
|
650
|
+
// that is regenerated (and therefore discarded) on the next render.
|
|
651
|
+
onBreakpointChange: handleBreakpointChange, dragConfig: {
|
|
569
652
|
enabled: isEditing,
|
|
570
653
|
handle: '.drag-handle',
|
|
571
654
|
bounded: false,
|
|
@@ -1,15 +1,72 @@
|
|
|
1
1
|
import type { GridLayouts } from './types.js';
|
|
2
2
|
export declare const RESPONSIVE_BREAKPOINTS: readonly ["lg", "md", "sm", "xs", "xxs"];
|
|
3
3
|
export type ResponsiveBreakpoint = (typeof RESPONSIVE_BREAKPOINTS)[number];
|
|
4
|
+
/**
|
|
5
|
+
* Breakpoints whose layout MIRRORS `lg` verbatim.
|
|
6
|
+
*
|
|
7
|
+
* D-75 root cause (operator directive 2026-08-13: "columns available to move
|
|
8
|
+
* cards into do not respect the columns slider").
|
|
9
|
+
* ---------------------------------------------------------------------------
|
|
10
|
+
* react-grid-layout picks its breakpoint from the **container** width, not the
|
|
11
|
+
* viewport — `<Responsive width={containerWidth}>` in `PageGridLayout`. Inside
|
|
12
|
+
* an app shell with a sidebar and page padding, a 1280px laptop presents a
|
|
13
|
+
* container around 950px and a 1440px desktop around 1100px. Both land BELOW
|
|
14
|
+
* the `md: 996` / `lg: 1200` thresholds, so the great majority of real desktop
|
|
15
|
+
* sessions render at `sm` or `md`, not `lg`.
|
|
16
|
+
*
|
|
17
|
+
* `sm` used to be derived by `stackToSingleColumn`, which sets `w: cols` on
|
|
18
|
+
* every item — one card per row at full width, whatever the column count. So
|
|
19
|
+
* on an ordinary laptop the columns slider changed `activeCols` while every
|
|
20
|
+
* card stayed stretched across all of them: the control provably did nothing,
|
|
21
|
+
* and the SE resize handle could not widen a card that was already at the grid
|
|
22
|
+
* bound nor narrow one whose `minW` had been clamped up to `cols`.
|
|
23
|
+
*
|
|
24
|
+
* `md` and `sm` therefore mirror `lg`: at a container width of 768px and above
|
|
25
|
+
* there is room for the arrangement the user chose, and honouring their choice
|
|
26
|
+
* is the whole point of the control. Genuine phone widths (`xs` < 768px
|
|
27
|
+
* container, `xxs` < 480px) still stack, which is what that code was for.
|
|
28
|
+
*/
|
|
29
|
+
export declare const MIRRORED_BREAKPOINTS: readonly ["md", "sm"];
|
|
30
|
+
/**
|
|
31
|
+
* Breakpoints derived as a single full-width stack — narrow enough that a
|
|
32
|
+
* multi-column arrangement cannot be read.
|
|
33
|
+
*/
|
|
34
|
+
export declare const STACKED_BREAKPOINTS: readonly ["xs", "xxs"];
|
|
35
|
+
/**
|
|
36
|
+
* Every breakpoint this module DERIVES from `lg`.
|
|
37
|
+
*
|
|
38
|
+
* A derived breakpoint is a render-time projection, never a stored fact. It
|
|
39
|
+
* must not reach the preference adapter: react-grid-layout echoes ALL
|
|
40
|
+
* breakpoints back through `onLayoutChange` on every gesture, so persisting
|
|
41
|
+
* them verbatim froze the derivation — a layout derived once at 12 columns was
|
|
42
|
+
* then "consumer-supplied" forever and stopped tracking `lg`. See
|
|
43
|
+
* `canonicaliseLayoutForPersist` in `usePageGridLayout.ts`.
|
|
44
|
+
*/
|
|
45
|
+
export declare const DERIVED_BREAKPOINTS: readonly ResponsiveBreakpoint[];
|
|
46
|
+
export declare function isDerivedBreakpoint(breakpoint: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* True when a gesture at `breakpoint` can be written straight back onto `lg`.
|
|
49
|
+
*
|
|
50
|
+
* `lg`, `md` and `sm` all render the same array against the same column count,
|
|
51
|
+
* so an edit made at any of them is expressible in `lg` with no loss. `xs` and
|
|
52
|
+
* `xxs` render a full-width stack, where an edit carries only a vertical order
|
|
53
|
+
* and would flatten a multi-column desktop arrangement if applied to `lg`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isCanonicalisableBreakpoint(breakpoint: string): boolean;
|
|
4
56
|
export interface BuildResponsiveLayoutsOptions {
|
|
5
57
|
cols?: number;
|
|
6
58
|
}
|
|
7
59
|
/**
|
|
8
60
|
* Ensure a GridLayouts object has entries for all responsive breakpoints
|
|
9
|
-
* (lg, md, sm, xs, xxs).
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
61
|
+
* (lg, md, sm, xs, xxs).
|
|
62
|
+
*
|
|
63
|
+
* - `md` and `sm` mirror `lg` so the user's chosen column arrangement survives
|
|
64
|
+
* every desktop and tablet container width (see {@link MIRRORED_BREAKPOINTS}).
|
|
65
|
+
* - `xs` and `xxs` are derived as a single-column stack so a phone viewport
|
|
66
|
+
* never squashes cards into unreadable slivers.
|
|
67
|
+
* - Consumer-supplied breakpoints are preserved verbatim. Supplying one opts
|
|
68
|
+
* that breakpoint OUT of tracking `lg`, including out of the columns slider —
|
|
69
|
+
* prefer not to.
|
|
13
70
|
*/
|
|
14
71
|
export declare function buildResponsiveLayouts(layouts: GridLayouts, { cols }?: BuildResponsiveLayoutsOptions): GridLayouts;
|
|
15
72
|
//# sourceMappingURL=buildResponsiveLayouts.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildResponsiveLayouts.d.ts","sourceRoot":"","sources":["../src/buildResponsiveLayouts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9D,eAAO,MAAM,sBAAsB,0CAA2C,CAAC;AAC/E,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3E,MAAM,WAAW,6BAA6B;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;
|
|
1
|
+
{"version":3,"file":"buildResponsiveLayouts.d.ts","sourceRoot":"","sources":["../src/buildResponsiveLayouts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9D,eAAO,MAAM,sBAAsB,0CAA2C,CAAC;AAC/E,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB,uBAAwB,CAAC;AAE1D;;;GAGG;AACH,eAAO,MAAM,mBAAmB,wBAAyB,CAAC;AAE1D;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,oBAAoB,EAG9D,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEvE;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,WAAW,EACpB,EAAE,IAAS,EAAE,GAAE,6BAAkC,GAChD,WAAW,CAyBb"}
|
|
@@ -1,4 +1,71 @@
|
|
|
1
1
|
export const RESPONSIVE_BREAKPOINTS = ['lg', 'md', 'sm', 'xs', 'xxs'];
|
|
2
|
+
/**
|
|
3
|
+
* Breakpoints whose layout MIRRORS `lg` verbatim.
|
|
4
|
+
*
|
|
5
|
+
* D-75 root cause (operator directive 2026-08-13: "columns available to move
|
|
6
|
+
* cards into do not respect the columns slider").
|
|
7
|
+
* ---------------------------------------------------------------------------
|
|
8
|
+
* react-grid-layout picks its breakpoint from the **container** width, not the
|
|
9
|
+
* viewport — `<Responsive width={containerWidth}>` in `PageGridLayout`. Inside
|
|
10
|
+
* an app shell with a sidebar and page padding, a 1280px laptop presents a
|
|
11
|
+
* container around 950px and a 1440px desktop around 1100px. Both land BELOW
|
|
12
|
+
* the `md: 996` / `lg: 1200` thresholds, so the great majority of real desktop
|
|
13
|
+
* sessions render at `sm` or `md`, not `lg`.
|
|
14
|
+
*
|
|
15
|
+
* `sm` used to be derived by `stackToSingleColumn`, which sets `w: cols` on
|
|
16
|
+
* every item — one card per row at full width, whatever the column count. So
|
|
17
|
+
* on an ordinary laptop the columns slider changed `activeCols` while every
|
|
18
|
+
* card stayed stretched across all of them: the control provably did nothing,
|
|
19
|
+
* and the SE resize handle could not widen a card that was already at the grid
|
|
20
|
+
* bound nor narrow one whose `minW` had been clamped up to `cols`.
|
|
21
|
+
*
|
|
22
|
+
* `md` and `sm` therefore mirror `lg`: at a container width of 768px and above
|
|
23
|
+
* there is room for the arrangement the user chose, and honouring their choice
|
|
24
|
+
* is the whole point of the control. Genuine phone widths (`xs` < 768px
|
|
25
|
+
* container, `xxs` < 480px) still stack, which is what that code was for.
|
|
26
|
+
*/
|
|
27
|
+
export const MIRRORED_BREAKPOINTS = ['md', 'sm'];
|
|
28
|
+
/**
|
|
29
|
+
* Breakpoints derived as a single full-width stack — narrow enough that a
|
|
30
|
+
* multi-column arrangement cannot be read.
|
|
31
|
+
*/
|
|
32
|
+
export const STACKED_BREAKPOINTS = ['xs', 'xxs'];
|
|
33
|
+
/**
|
|
34
|
+
* Every breakpoint this module DERIVES from `lg`.
|
|
35
|
+
*
|
|
36
|
+
* A derived breakpoint is a render-time projection, never a stored fact. It
|
|
37
|
+
* must not reach the preference adapter: react-grid-layout echoes ALL
|
|
38
|
+
* breakpoints back through `onLayoutChange` on every gesture, so persisting
|
|
39
|
+
* them verbatim froze the derivation — a layout derived once at 12 columns was
|
|
40
|
+
* then "consumer-supplied" forever and stopped tracking `lg`. See
|
|
41
|
+
* `canonicaliseLayoutForPersist` in `usePageGridLayout.ts`.
|
|
42
|
+
*/
|
|
43
|
+
export const DERIVED_BREAKPOINTS = [
|
|
44
|
+
...MIRRORED_BREAKPOINTS,
|
|
45
|
+
...STACKED_BREAKPOINTS,
|
|
46
|
+
];
|
|
47
|
+
export function isDerivedBreakpoint(breakpoint) {
|
|
48
|
+
return DERIVED_BREAKPOINTS.includes(breakpoint);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* True when a gesture at `breakpoint` can be written straight back onto `lg`.
|
|
52
|
+
*
|
|
53
|
+
* `lg`, `md` and `sm` all render the same array against the same column count,
|
|
54
|
+
* so an edit made at any of them is expressible in `lg` with no loss. `xs` and
|
|
55
|
+
* `xxs` render a full-width stack, where an edit carries only a vertical order
|
|
56
|
+
* and would flatten a multi-column desktop arrangement if applied to `lg`.
|
|
57
|
+
*/
|
|
58
|
+
export function isCanonicalisableBreakpoint(breakpoint) {
|
|
59
|
+
return breakpoint === 'lg' || MIRRORED_BREAKPOINTS.includes(breakpoint);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Stack items into one full-width column, preserving reading order.
|
|
63
|
+
*
|
|
64
|
+
* `w: cols` is deliberate at the breakpoints this is used for (`xs`/`xxs`):
|
|
65
|
+
* the container is narrower than 768px, so a card occupies the whole grid
|
|
66
|
+
* whatever the column count. Do NOT reach for this at wider breakpoints — that
|
|
67
|
+
* was D-75.
|
|
68
|
+
*/
|
|
2
69
|
function stackToSingleColumn(items, cols) {
|
|
3
70
|
const sorted = [...items].sort((a, b) => (a.y !== b.y ? a.y - b.y : a.x - b.x));
|
|
4
71
|
let nextY = 0;
|
|
@@ -16,10 +83,15 @@ function stackToSingleColumn(items, cols) {
|
|
|
16
83
|
}
|
|
17
84
|
/**
|
|
18
85
|
* Ensure a GridLayouts object has entries for all responsive breakpoints
|
|
19
|
-
* (lg, md, sm, xs, xxs).
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
86
|
+
* (lg, md, sm, xs, xxs).
|
|
87
|
+
*
|
|
88
|
+
* - `md` and `sm` mirror `lg` so the user's chosen column arrangement survives
|
|
89
|
+
* every desktop and tablet container width (see {@link MIRRORED_BREAKPOINTS}).
|
|
90
|
+
* - `xs` and `xxs` are derived as a single-column stack so a phone viewport
|
|
91
|
+
* never squashes cards into unreadable slivers.
|
|
92
|
+
* - Consumer-supplied breakpoints are preserved verbatim. Supplying one opts
|
|
93
|
+
* that breakpoint OUT of tracking `lg`, including out of the columns slider —
|
|
94
|
+
* prefer not to.
|
|
23
95
|
*/
|
|
24
96
|
export function buildResponsiveLayouts(layouts, { cols = 12 } = {}) {
|
|
25
97
|
const lg = layouts.lg ?? [];
|
|
@@ -32,7 +104,7 @@ export function buildResponsiveLayouts(layouts, { cols = 12 } = {}) {
|
|
|
32
104
|
result[bp] = supplied;
|
|
33
105
|
continue;
|
|
34
106
|
}
|
|
35
|
-
if (bp
|
|
107
|
+
if (MIRRORED_BREAKPOINTS.includes(bp)) {
|
|
36
108
|
result[bp] = lg;
|
|
37
109
|
continue;
|
|
38
110
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -34,6 +34,14 @@ export interface UsePageGridLayoutOptions {
|
|
|
34
34
|
canEditPage?: boolean;
|
|
35
35
|
editorEventNames?: readonly string[];
|
|
36
36
|
preferenceAdapter?: PageGridPreferenceFactory;
|
|
37
|
+
/**
|
|
38
|
+
* Value used for `GridLayoutItem.autoHeight` on any item that does not state
|
|
39
|
+
* one. Defaults to `true` — cards grow to fit their content rather than
|
|
40
|
+
* clipping it behind an inner scrollbar (D-76). Pass `false` for a surface
|
|
41
|
+
* whose items own their own scroll (a virtualized list); an individual item
|
|
42
|
+
* can always opt out with an explicit `autoHeight: false`.
|
|
43
|
+
*/
|
|
44
|
+
defaultAutoHeight?: boolean;
|
|
37
45
|
}
|
|
38
46
|
export interface WidgetMeta {
|
|
39
47
|
label: string;
|
|
@@ -162,6 +170,14 @@ export interface UsePageGridLayoutResult {
|
|
|
162
170
|
activeCompactor: Compactor;
|
|
163
171
|
onLayoutChange: (_layout: unknown, layouts: unknown) => void;
|
|
164
172
|
handleColumnChange: (newCols: number) => void;
|
|
173
|
+
/**
|
|
174
|
+
* Wire to `<Responsive onBreakpointChange>`. The hook needs to know which
|
|
175
|
+
* breakpoint react-grid-layout is rendering so a gesture is written back to
|
|
176
|
+
* the breakpoint that owns it — see `canonicaliseLayoutForPersist`. Without
|
|
177
|
+
* this the hook assumes `lg`, and every edit made at a narrower container
|
|
178
|
+
* width lands in a derived breakpoint that is then discarded (D-75).
|
|
179
|
+
*/
|
|
180
|
+
handleBreakpointChange: (breakpoint: string) => void;
|
|
165
181
|
handleCompact: () => void;
|
|
166
182
|
handleReset: () => void;
|
|
167
183
|
addWidget: (widgetKey: string, initialSize?: Partial<Pick<GridLayoutItem, 'w' | 'h' | 'minW' | 'minH'>>) => void;
|
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,WAAW,cAAe,SAAQ,UAAU;IAChD;;;;;;;;;;;;OAYG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,cAAc,EAAE,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,yBAAyB,CAAC,CAAC;IAC1C,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC;IACpD,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,EACxC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,CAAC,KACR,yBAAyB,CAAC,CAAC,CAAC,CAAC;AAElC,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,WAAW,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,iBAAiB,CAAC,EAAE,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,9 +10,30 @@ export declare const DEFAULT_EDITOR_EVENT_NAMES: readonly ["bsuite-open-page-edi
|
|
|
10
10
|
* launchers scope by page if they handle multiple grids on one screen.
|
|
11
11
|
*/
|
|
12
12
|
export declare const PAGE_GRID_EDITING_EVENT = "bsuite-page-grid-editing";
|
|
13
|
+
/**
|
|
14
|
+
* Package-wide layout epoch, added to EVERY consumer's `layoutVersion`.
|
|
15
|
+
*
|
|
16
|
+
* When a default that governs all grids changes, a saved layout produced under
|
|
17
|
+
* the old default has to be invalidated — and there is no way to reach ~330
|
|
18
|
+
* call sites across six apps to hand-bump each one. crm7's `DraggableCardPage`
|
|
19
|
+
* already solved this locally with its own `LAYOUT_EPOCH`; this is the same
|
|
20
|
+
* lever one level down, so it also covers the raw `PageGridLayout` consumers
|
|
21
|
+
* (Dashboard, Billing, GTO, Analytics, the throughput and braden pages) that
|
|
22
|
+
* no app-level epoch can see.
|
|
23
|
+
*
|
|
24
|
+
* 0 -> 1000 (2026-08-13, D-75/D-76): two all-grids defaults changed in the same
|
|
25
|
+
* release — `autoHeight` now defaults to true for raw consumers, and md/sm no
|
|
26
|
+
* longer collapse to a full-width stack. Every stored layout predates both and
|
|
27
|
+
* was additionally polluted with frozen derived breakpoints, so all of them
|
|
28
|
+
* reset once to the corrected defaults.
|
|
29
|
+
*
|
|
30
|
+
* Per-page `layoutVersion` bumps keep working on top; this only moves the
|
|
31
|
+
* floor.
|
|
32
|
+
*/
|
|
33
|
+
export declare const PACKAGE_LAYOUT_EPOCH = 1000;
|
|
13
34
|
export interface PageGridEditingEventDetail {
|
|
14
35
|
pageKey: string;
|
|
15
36
|
editing: boolean;
|
|
16
37
|
}
|
|
17
|
-
export declare function usePageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, }: UsePageGridLayoutOptions): UsePageGridLayoutResult;
|
|
38
|
+
export declare function usePageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, defaultAutoHeight, }: UsePageGridLayoutOptions): UsePageGridLayoutResult;
|
|
18
39
|
//# sourceMappingURL=usePageGridLayout.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePageGridLayout.d.ts","sourceRoot":"","sources":["../src/usePageGridLayout.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"usePageGridLayout.d.ts","sourceRoot":"","sources":["../src/usePageGridLayout.ts"],"names":[],"mappings":"AAiBA,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;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAiBzC,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,EAC5C,iBAAiB,GAClB,EAAE,wBAAwB,GAAG,uBAAuB,CAkmBpD"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { startTransition, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
2
2
|
import { verticalCompactor } from 'react-grid-layout';
|
|
3
|
-
import { buildResponsiveLayouts } from './buildResponsiveLayouts.js';
|
|
3
|
+
import { buildResponsiveLayouts, isCanonicalisableBreakpoint, isDerivedBreakpoint, } from './buildResponsiveLayouts.js';
|
|
4
4
|
import { defaultPreferenceAdapter } from './preferences.js';
|
|
5
5
|
import { rescaleLayout } from './rescaleLayout.js';
|
|
6
6
|
export const DEFAULT_EDITOR_EVENT_NAMES = [
|
|
@@ -20,7 +20,43 @@ export const DEFAULT_EDITOR_EVENT_NAMES = [
|
|
|
20
20
|
* launchers scope by page if they handle multiple grids on one screen.
|
|
21
21
|
*/
|
|
22
22
|
export const PAGE_GRID_EDITING_EVENT = 'bsuite-page-grid-editing';
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Package-wide layout epoch, added to EVERY consumer's `layoutVersion`.
|
|
25
|
+
*
|
|
26
|
+
* When a default that governs all grids changes, a saved layout produced under
|
|
27
|
+
* the old default has to be invalidated — and there is no way to reach ~330
|
|
28
|
+
* call sites across six apps to hand-bump each one. crm7's `DraggableCardPage`
|
|
29
|
+
* already solved this locally with its own `LAYOUT_EPOCH`; this is the same
|
|
30
|
+
* lever one level down, so it also covers the raw `PageGridLayout` consumers
|
|
31
|
+
* (Dashboard, Billing, GTO, Analytics, the throughput and braden pages) that
|
|
32
|
+
* no app-level epoch can see.
|
|
33
|
+
*
|
|
34
|
+
* 0 -> 1000 (2026-08-13, D-75/D-76): two all-grids defaults changed in the same
|
|
35
|
+
* release — `autoHeight` now defaults to true for raw consumers, and md/sm no
|
|
36
|
+
* longer collapse to a full-width stack. Every stored layout predates both and
|
|
37
|
+
* was additionally polluted with frozen derived breakpoints, so all of them
|
|
38
|
+
* reset once to the corrected defaults.
|
|
39
|
+
*
|
|
40
|
+
* Per-page `layoutVersion` bumps keep working on top; this only moves the
|
|
41
|
+
* floor.
|
|
42
|
+
*/
|
|
43
|
+
export const PACKAGE_LAYOUT_EPOCH = 1000;
|
|
44
|
+
/**
|
|
45
|
+
* Default for `GridLayoutItem.autoHeight` when an item does not state one.
|
|
46
|
+
*
|
|
47
|
+
* D-76 ("cards render half cut off on page open"). `DraggableCardPage` has
|
|
48
|
+
* defaulted this to true since the E5a flip, but a RAW `PageGridLayout`
|
|
49
|
+
* consumer builds its layout array by hand and inherits nothing — the flag had
|
|
50
|
+
* to be written on every item. Sixteen of the eighteen raw consumers across the
|
|
51
|
+
* suite never did, so their cards were fixed at whatever seed `h` the author
|
|
52
|
+
* guessed and clipped everything past it behind `overflow-auto`. That is not a
|
|
53
|
+
* per-page authoring mistake repeated sixteen times; it is a default in the
|
|
54
|
+
* wrong place. An item that genuinely owns its own scroll (a virtualized list)
|
|
55
|
+
* opts out with an explicit `autoHeight: false`.
|
|
56
|
+
*/
|
|
57
|
+
const DEFAULT_ITEM_AUTO_HEIGHT = true;
|
|
58
|
+
export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, layoutVersion = 1, canEditPage = true, editorEventNames = DEFAULT_EDITOR_EVENT_NAMES, preferenceAdapter = defaultPreferenceAdapter, defaultAutoHeight, }) {
|
|
59
|
+
const effectiveLayoutVersion = layoutVersion + PACKAGE_LAYOUT_EPOCH;
|
|
24
60
|
const containerRef = useRef(null);
|
|
25
61
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
26
62
|
const [isEditing, setIsEditingState] = useState(false);
|
|
@@ -65,18 +101,18 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
65
101
|
useEffect(() => {
|
|
66
102
|
if (!prefsLoaded)
|
|
67
103
|
return;
|
|
68
|
-
if ((savedLayoutVersion ?? 0) <
|
|
104
|
+
if ((savedLayoutVersion ?? 0) < effectiveLayoutVersion) {
|
|
69
105
|
startTransition(() => {
|
|
70
106
|
setSavedLayout(defaultLayouts);
|
|
71
107
|
setLayoutCols(defaultCols);
|
|
72
108
|
setBaseCols(defaultCols);
|
|
73
|
-
setSavedLayoutVersion(
|
|
109
|
+
setSavedLayoutVersion(effectiveLayoutVersion);
|
|
74
110
|
});
|
|
75
111
|
}
|
|
76
112
|
}, [
|
|
77
113
|
defaultCols,
|
|
78
114
|
defaultLayouts,
|
|
79
|
-
|
|
115
|
+
effectiveLayoutVersion,
|
|
80
116
|
prefsLoaded,
|
|
81
117
|
savedLayoutVersion,
|
|
82
118
|
setBaseCols,
|
|
@@ -89,7 +125,26 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
89
125
|
const rawLayouts = useMemo(() => {
|
|
90
126
|
if (Object.keys(savedLayout || {}).length === 0)
|
|
91
127
|
return defaultLayouts;
|
|
92
|
-
|
|
128
|
+
// Drop DERIVED breakpoints out of a stored layout before anything reads it.
|
|
129
|
+
//
|
|
130
|
+
// Until 0.8.0 react-grid-layout's `onLayoutChange` echo persisted md/sm/xs/
|
|
131
|
+
// xxs alongside lg. `buildResponsiveLayouts` then treated them as
|
|
132
|
+
// consumer-supplied and preserved them verbatim, so a layout derived once —
|
|
133
|
+
// at whatever column count happened to be active that day — outlived every
|
|
134
|
+
// later change to `lg` and to the columns slider (D-75). Every user who has
|
|
135
|
+
// ever dragged a card on any page has that pollution stored, so the fix has
|
|
136
|
+
// to heal on read rather than wait for a layoutVersion bump; `lg` (the only
|
|
137
|
+
// breakpoint that was ever authoritative) is preserved untouched, so nobody
|
|
138
|
+
// loses the arrangement they made.
|
|
139
|
+
const stored = savedLayout;
|
|
140
|
+
const saved = {};
|
|
141
|
+
for (const bp of Object.keys(stored)) {
|
|
142
|
+
if (isDerivedBreakpoint(bp))
|
|
143
|
+
continue;
|
|
144
|
+
saved[bp] = stored[bp];
|
|
145
|
+
}
|
|
146
|
+
if (!saved.lg)
|
|
147
|
+
saved.lg = stored.lg ?? defaultLayouts.lg ?? [];
|
|
93
148
|
let changed = false;
|
|
94
149
|
const merged = {};
|
|
95
150
|
for (const bp of Object.keys(saved)) {
|
|
@@ -107,12 +162,28 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
107
162
|
merged[bp] = savedItems;
|
|
108
163
|
}
|
|
109
164
|
}
|
|
110
|
-
|
|
165
|
+
// `merged` is always returned (never the raw `savedLayout`) because the
|
|
166
|
+
// derived-breakpoint filter above may have removed keys even when no
|
|
167
|
+
// default item was missing — handing back `savedLayout` would smuggle the
|
|
168
|
+
// stale derived breakpoints straight past the heal.
|
|
169
|
+
void changed;
|
|
170
|
+
return merged;
|
|
111
171
|
}, [savedLayout, defaultLayouts]);
|
|
112
172
|
const currentLayouts = useMemo(() => {
|
|
113
173
|
const rescaled = baseCols !== layoutCols ? rescaleLayout(rawLayouts, baseCols, layoutCols) : rawLayouts;
|
|
114
|
-
|
|
115
|
-
|
|
174
|
+
const responsive = buildResponsiveLayouts(rescaled, { cols: layoutCols });
|
|
175
|
+
// Resolve the autoHeight default HERE rather than at the render layer, so
|
|
176
|
+
// `stripAutoHeightRows` (which reads this object through
|
|
177
|
+
// `currentLayoutsForStripRef`) and `PageGridLayout`'s `activeLayouts` agree
|
|
178
|
+
// on which items are auto-height. They disagreed once before and the
|
|
179
|
+
// measured height leaked into storage.
|
|
180
|
+
const resolvedAutoHeight = defaultAutoHeight ?? DEFAULT_ITEM_AUTO_HEIGHT;
|
|
181
|
+
const resolved = { lg: [] };
|
|
182
|
+
for (const bp of Object.keys(responsive)) {
|
|
183
|
+
resolved[bp] = (responsive[bp] ?? []).map((item) => item.autoHeight === undefined ? { ...item, autoHeight: resolvedAutoHeight } : item);
|
|
184
|
+
}
|
|
185
|
+
return resolved;
|
|
186
|
+
}, [rawLayouts, baseCols, layoutCols, defaultAutoHeight]);
|
|
116
187
|
useEffect(() => {
|
|
117
188
|
if (baseCols !== layoutCols) {
|
|
118
189
|
const rescaled = rescaleLayout(rawLayouts, baseCols, layoutCols);
|
|
@@ -265,6 +336,67 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
265
336
|
useEffect(() => {
|
|
266
337
|
currentLayoutsForStripRef.current = currentLayouts;
|
|
267
338
|
}, [currentLayouts]);
|
|
339
|
+
/**
|
|
340
|
+
* The breakpoint react-grid-layout is currently rendering, tracked so a
|
|
341
|
+
* gesture can be written back to the breakpoint that OWNS it.
|
|
342
|
+
*
|
|
343
|
+
* Seeded to `lg` and corrected by `<Responsive onBreakpointChange>`, which
|
|
344
|
+
* fires on mount once the container width is known.
|
|
345
|
+
*/
|
|
346
|
+
const activeBreakpointRef = useRef('lg');
|
|
347
|
+
const handleBreakpointChange = useCallback((breakpoint) => {
|
|
348
|
+
activeBreakpointRef.current = breakpoint;
|
|
349
|
+
}, []);
|
|
350
|
+
/**
|
|
351
|
+
* Reduce react-grid-layout's all-breakpoints echo down to the ONE breakpoint
|
|
352
|
+
* that is authoritative — `lg` — or refuse the write.
|
|
353
|
+
*
|
|
354
|
+
* Two defects this closes, both reported as D-75:
|
|
355
|
+
*
|
|
356
|
+
* 1. **The derived breakpoints froze.** RGL echoes every breakpoint on every
|
|
357
|
+
* gesture. Persisting them made them consumer-supplied, so
|
|
358
|
+
* `buildResponsiveLayouts` stopped deriving them and the columns slider
|
|
359
|
+
* (which rescales `lg`) could no longer reach the breakpoint actually on
|
|
360
|
+
* screen. Below a 1200px container — i.e. most real desktop sessions once
|
|
361
|
+
* the sidebar is subtracted — moving the slider changed nothing at all.
|
|
362
|
+
*
|
|
363
|
+
* 2. **An edit made at `md`/`sm` was invisible at `lg`.** The gesture landed
|
|
364
|
+
* in `layouts.md`; `layouts.lg` kept its old values; the same user on a
|
|
365
|
+
* wider monitor saw their arrangement revert. `md` and `sm` render the
|
|
366
|
+
* same array against the same column count as `lg`, so folding the
|
|
367
|
+
* gesture onto `lg` is lossless and makes one arrangement follow the user
|
|
368
|
+
* across every window size.
|
|
369
|
+
*
|
|
370
|
+
* `xs`/`xxs` return `null` (no write). Those breakpoints render a full-width
|
|
371
|
+
* stack, so a gesture there carries only a vertical order; applying it to
|
|
372
|
+
* `lg` would flatten a multi-column desktop arrangement that the user cannot
|
|
373
|
+
* even see on that device. Refusing the write is the conservative half of
|
|
374
|
+
* the trade: a phone visit can no longer destroy the desktop layout, at the
|
|
375
|
+
* cost of phone-only reordering not persisting. Stated in the PR, not hidden.
|
|
376
|
+
*/
|
|
377
|
+
const canonicaliseLayoutForPersist = useCallback((layouts) => {
|
|
378
|
+
const breakpoint = activeBreakpointRef.current;
|
|
379
|
+
if (!isCanonicalisableBreakpoint(breakpoint))
|
|
380
|
+
return null;
|
|
381
|
+
const items = layouts[breakpoint] ?? layouts.lg;
|
|
382
|
+
if (!items)
|
|
383
|
+
return null;
|
|
384
|
+
const next = { lg: items };
|
|
385
|
+
// Preserve any breakpoint a CONSUMER supplied deliberately (never a
|
|
386
|
+
// derived one) so an opt-out layout is not silently discarded.
|
|
387
|
+
for (const bp of Object.keys(layouts)) {
|
|
388
|
+
if (bp === 'lg' || isDerivedBreakpoint(bp))
|
|
389
|
+
continue;
|
|
390
|
+
next[bp] = layouts[bp] ?? [];
|
|
391
|
+
}
|
|
392
|
+
return next;
|
|
393
|
+
}, []);
|
|
394
|
+
const commitLayout = useCallback((layouts) => {
|
|
395
|
+
const canonical = canonicaliseLayoutForPersist(layouts);
|
|
396
|
+
if (!canonical)
|
|
397
|
+
return;
|
|
398
|
+
setSavedLayout(stripAutoHeightRows(canonical));
|
|
399
|
+
}, [canonicaliseLayoutForPersist, setSavedLayout, stripAutoHeightRows]);
|
|
268
400
|
const onLayoutChange = useCallback((_layout, layouts) => {
|
|
269
401
|
if (!isEditing)
|
|
270
402
|
return;
|
|
@@ -272,7 +404,7 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
272
404
|
if (layoutCommitFrameRef.current !== null)
|
|
273
405
|
return;
|
|
274
406
|
if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') {
|
|
275
|
-
|
|
407
|
+
commitLayout(layouts);
|
|
276
408
|
return;
|
|
277
409
|
}
|
|
278
410
|
// Trailing rAF — lets a burst of resize ticks coalesce into one commit.
|
|
@@ -281,10 +413,10 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
281
413
|
const pending = pendingLayoutRef.current;
|
|
282
414
|
if (pending) {
|
|
283
415
|
pendingLayoutRef.current = null;
|
|
284
|
-
|
|
416
|
+
commitLayout(pending);
|
|
285
417
|
}
|
|
286
418
|
});
|
|
287
|
-
}, [
|
|
419
|
+
}, [commitLayout, isEditing]);
|
|
288
420
|
useEffect(() => {
|
|
289
421
|
return () => {
|
|
290
422
|
if (layoutCommitFrameRef.current !== null &&
|
|
@@ -300,6 +432,23 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
300
432
|
setLayoutCols(newCols);
|
|
301
433
|
setBaseCols(newCols);
|
|
302
434
|
}, [currentLayouts, layoutCols, setBaseCols, setLayoutCols, setSavedLayout]);
|
|
435
|
+
/**
|
|
436
|
+
* Build the object handed to the preference adapter from a new `lg` array.
|
|
437
|
+
*
|
|
438
|
+
* Only `lg` and consumer-supplied breakpoints are stored; md/sm/xs/xxs are
|
|
439
|
+
* re-derived on every render. Before 0.8.0 every mutator below spread
|
|
440
|
+
* `currentLayouts` (which contains the derived breakpoints) into its write,
|
|
441
|
+
* which re-froze them one gesture after `rawLayouts` had healed them.
|
|
442
|
+
*/
|
|
443
|
+
const layoutsWithLg = useCallback((items) => {
|
|
444
|
+
const next = { lg: items };
|
|
445
|
+
for (const bp of Object.keys(currentLayouts)) {
|
|
446
|
+
if (bp === 'lg' || isDerivedBreakpoint(bp))
|
|
447
|
+
continue;
|
|
448
|
+
next[bp] = currentLayouts[bp] ?? [];
|
|
449
|
+
}
|
|
450
|
+
return next;
|
|
451
|
+
}, [currentLayouts]);
|
|
303
452
|
const handleCompact = useCallback(() => {
|
|
304
453
|
if (!currentLayouts.lg)
|
|
305
454
|
return;
|
|
@@ -316,8 +465,8 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
316
465
|
placed.push(compacted);
|
|
317
466
|
return compacted;
|
|
318
467
|
});
|
|
319
|
-
startTransition(() => setSavedLayout(
|
|
320
|
-
}, [currentLayouts, setSavedLayout]);
|
|
468
|
+
startTransition(() => setSavedLayout(layoutsWithLg(result)));
|
|
469
|
+
}, [currentLayouts, layoutsWithLg, setSavedLayout]);
|
|
321
470
|
const handleReset = useCallback(() => {
|
|
322
471
|
startTransition(() => {
|
|
323
472
|
setSavedLayout(defaultLayouts);
|
|
@@ -326,7 +475,6 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
326
475
|
});
|
|
327
476
|
}, [defaultCols, defaultLayouts, setBaseCols, setLayoutCols, setSavedLayout]);
|
|
328
477
|
const addWidget = useCallback((widgetKey, initialSize) => {
|
|
329
|
-
const bps = Object.keys(currentLayouts);
|
|
330
478
|
const maxY = (currentLayouts.lg ?? []).reduce((max, item) => Math.max(max, item.y + item.h), 0);
|
|
331
479
|
const newItem = {
|
|
332
480
|
i: widgetKey,
|
|
@@ -337,60 +485,42 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
337
485
|
minW: initialSize?.minW,
|
|
338
486
|
minH: initialSize?.minH,
|
|
339
487
|
};
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
updated[bp] = [...(currentLayouts[bp] ?? []), newItem];
|
|
343
|
-
}
|
|
344
|
-
startTransition(() => setSavedLayout(updated));
|
|
345
|
-
}, [currentLayouts, layoutCols, setSavedLayout]);
|
|
488
|
+
startTransition(() => setSavedLayout(layoutsWithLg([...(currentLayouts.lg ?? []), newItem])));
|
|
489
|
+
}, [currentLayouts, layoutCols, layoutsWithLg, setSavedLayout]);
|
|
346
490
|
const moveWidget = useCallback((widgetKey, direction) => {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
...withoutMoved.slice(0, targetIndex),
|
|
364
|
-
moved,
|
|
365
|
-
...withoutMoved.slice(targetIndex),
|
|
366
|
-
];
|
|
367
|
-
updated[bp] = nextItems;
|
|
368
|
-
}
|
|
369
|
-
startTransition(() => setSavedLayout(updated));
|
|
370
|
-
}, [currentLayouts, setSavedLayout]);
|
|
491
|
+
const items = [...(currentLayouts.lg ?? [])];
|
|
492
|
+
const index = items.findIndex((item) => item.i === widgetKey);
|
|
493
|
+
if (index === -1)
|
|
494
|
+
return;
|
|
495
|
+
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
|
496
|
+
if (targetIndex < 0 || targetIndex >= items.length)
|
|
497
|
+
return;
|
|
498
|
+
const moved = items[index];
|
|
499
|
+
const withoutMoved = [...items.slice(0, index), ...items.slice(index + 1)];
|
|
500
|
+
const nextItems = [
|
|
501
|
+
...withoutMoved.slice(0, targetIndex),
|
|
502
|
+
moved,
|
|
503
|
+
...withoutMoved.slice(targetIndex),
|
|
504
|
+
];
|
|
505
|
+
startTransition(() => setSavedLayout(layoutsWithLg(nextItems)));
|
|
506
|
+
}, [currentLayouts, layoutsWithLg, setSavedLayout]);
|
|
371
507
|
const setWidgetLocked = useCallback((widgetKey, locked) => {
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
startTransition(() => setSavedLayout(updated));
|
|
386
|
-
}, [currentLayouts, setSavedLayout]);
|
|
508
|
+
const nextItems = (currentLayouts.lg ?? []).map((item) => {
|
|
509
|
+
if (item.i !== widgetKey)
|
|
510
|
+
return item;
|
|
511
|
+
return {
|
|
512
|
+
...item,
|
|
513
|
+
static: locked,
|
|
514
|
+
isDraggable: !locked,
|
|
515
|
+
isResizable: !locked,
|
|
516
|
+
};
|
|
517
|
+
});
|
|
518
|
+
startTransition(() => setSavedLayout(layoutsWithLg(nextItems)));
|
|
519
|
+
}, [currentLayouts, layoutsWithLg, setSavedLayout]);
|
|
387
520
|
const removeWidget = useCallback((widgetKey) => {
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
startTransition(() => setSavedLayout(updated));
|
|
393
|
-
}, [currentLayouts, setSavedLayout]);
|
|
521
|
+
const nextItems = (currentLayouts.lg ?? []).filter((item) => item.i !== widgetKey);
|
|
522
|
+
startTransition(() => setSavedLayout(layoutsWithLg(nextItems)));
|
|
523
|
+
}, [currentLayouts, layoutsWithLg, setSavedLayout]);
|
|
394
524
|
/**
|
|
395
525
|
* Measured auto-height overrides (widgetKey -> rows). DERIVED, in-memory-
|
|
396
526
|
* only state (quality-review design ruling, 2026-07-14):
|
|
@@ -467,6 +597,7 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
467
597
|
activeCompactor,
|
|
468
598
|
onLayoutChange,
|
|
469
599
|
handleColumnChange,
|
|
600
|
+
handleBreakpointChange,
|
|
470
601
|
handleCompact,
|
|
471
602
|
handleReset,
|
|
472
603
|
addWidget,
|