@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.
@@ -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,KAA6E,MAAM,OAAO,CAAC;AAGlG,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAMxC,OAAO,KAAK,EAAe,mBAAmB,EAA4B,MAAM,YAAY,CAAC;AAmR7F,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,yBAAiE,EACjE,kBAAkB,EAClB,sBAAsB,EACtB,mBAAmB,EACnB,+BAA6E,EAC7E,wBAAwB,EACxB,4BAA4B,EAC5B,4BAA4B,GAC7B,EAAE,mBAAmB,qBAowBrB"}
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"}
@@ -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
- const contentPx = entry.contentRect.height;
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 rows = computeAutoHeightRows({
119
- contentPx,
120
- cardChromePx: DEFAULT_CARD_CHROME_PX,
121
- rowHeightPx: DEFAULT_ROW_HEIGHT,
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, id, onAutoHeightChange]);
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
- if (!item.autoHeight)
313
- return item;
314
- // HEAL a persisted `isResizable: false`.
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, dragConfig: {
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). Missing smaller breakpoints (sm/xs/xxs) are derived
10
- * from `lg` as a single-column vertical stack so that consumer-supplied
11
- * lg-only layouts no longer squash on phone viewports. Missing `md` is
12
- * mirrored from `lg`. Consumer-supplied breakpoints are preserved verbatim.
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;AAkBD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,WAAW,EACpB,EAAE,IAAS,EAAE,GAAE,6BAAkC,GAChD,WAAW,CAyBb"}
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). Missing smaller breakpoints (sm/xs/xxs) are derived
20
- * from `lg` as a single-column vertical stack so that consumer-supplied
21
- * lg-only layouts no longer squash on phone viewports. Missing `md` is
22
- * mirrored from `lg`. Consumer-supplied breakpoints are preserved verbatim.
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 === 'md') {
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;
@@ -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;CAC/C;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,WAAW,CAAC,EAAE;QAAE,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,0BAA2B,SAAQ,kBAAkB;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAYD;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,wBAAwB;IACvC,6EAA6E;IAC7E,cAAc,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gCAAiC,SAAQ,wBAAwB;IAChF,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,wBAAwB;IACvC,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,mBAAmB,GAAG,SAAS,wBAAwB,EAAE,CAAC;AAEtE,MAAM,WAAW,mBAAoB,SAAQ,wBAAwB;IACnE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC5C;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yBAAyB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,0BAA0B,KAAK,KAAK,CAAC,SAAS,CAAC;IAC9E,sBAAsB,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACtF;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,+BAA+B,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpD,wBAAwB,CAAC,EAAE,CAAC,OAAO,EAAE,gCAAgC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC1F,4BAA4B,CAAC,EAAE,CAAC,OAAO,EAAE,wBAAwB,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAClG;;;;;OAKG;IACH,4BAA4B,CAAC,EAAE,CAAC,MAAM,EAAE,wBAAwB,KAAK,IAAI,CAAC;CAC3E;AAED,MAAM,WAAW,uBAAuB;IACtC,cAAc,EAAE,WAAW,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5D,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,eAAe,EAAE,SAAS,CAAC;IAC3B,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7D,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,SAAS,EAAE,CACT,SAAS,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,GAAG,GAAG,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC,KACrE,IAAI,CAAC;IACV,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,GAAG,MAAM,KAAK,IAAI,CAAC;IAClE,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9D,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C;;;;;;;;;;OAUG;IACH,mBAAmB,EAAE,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IACpE;0CACsC;IACtC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,mBAAmB,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IACnE,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAClD,cAAc,EAAE,MAAM,CAAC;CACxB"}
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":"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,CA+fpD"}
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
- export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, layoutVersion = 1, canEditPage = true, editorEventNames = DEFAULT_EDITOR_EVENT_NAMES, preferenceAdapter = defaultPreferenceAdapter, }) {
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) < layoutVersion) {
104
+ if ((savedLayoutVersion ?? 0) < effectiveLayoutVersion) {
69
105
  startTransition(() => {
70
106
  setSavedLayout(defaultLayouts);
71
107
  setLayoutCols(defaultCols);
72
108
  setBaseCols(defaultCols);
73
- setSavedLayoutVersion(layoutVersion);
109
+ setSavedLayoutVersion(effectiveLayoutVersion);
74
110
  });
75
111
  }
76
112
  }, [
77
113
  defaultCols,
78
114
  defaultLayouts,
79
- layoutVersion,
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
- const saved = savedLayout;
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
- return changed ? merged : savedLayout;
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
- return buildResponsiveLayouts(rescaled, { cols: layoutCols });
115
- }, [rawLayouts, baseCols, layoutCols]);
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
- setSavedLayout(stripAutoHeightRows(layouts));
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
- setSavedLayout(stripAutoHeightRows(pending));
416
+ commitLayout(pending);
285
417
  }
286
418
  });
287
- }, [isEditing, setSavedLayout, stripAutoHeightRows]);
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({ ...currentLayouts, lg: result }));
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
- const updated = { lg: [] };
341
- for (const bp of bps) {
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 updated = { lg: [] };
348
- for (const bp of Object.keys(currentLayouts)) {
349
- const items = [...(currentLayouts[bp] ?? [])];
350
- const index = items.findIndex((item) => item.i === widgetKey);
351
- if (index === -1) {
352
- updated[bp] = items;
353
- continue;
354
- }
355
- const targetIndex = direction === 'up' ? index - 1 : index + 1;
356
- if (targetIndex < 0 || targetIndex >= items.length) {
357
- updated[bp] = items;
358
- continue;
359
- }
360
- const moved = items[index];
361
- const withoutMoved = [...items.slice(0, index), ...items.slice(index + 1)];
362
- const nextItems = [
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 updated = { lg: [] };
373
- for (const bp of Object.keys(currentLayouts)) {
374
- updated[bp] = (currentLayouts[bp] ?? []).map((item) => {
375
- if (item.i !== widgetKey)
376
- return item;
377
- return {
378
- ...item,
379
- static: locked,
380
- isDraggable: !locked,
381
- isResizable: !locked,
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 updated = { lg: [] };
389
- for (const bp of Object.keys(currentLayouts)) {
390
- updated[bp] = (currentLayouts[bp] ?? []).filter((item) => item.i !== widgetKey);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsuite/page-builder",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Shared BSuite responsive page-builder grid and layout persistence primitives",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",