@bsuite/page-builder 2.6.1 → 2.7.1-next.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/README.md CHANGED
@@ -48,3 +48,19 @@ Behaviour and guarantees:
48
48
 
49
49
  When adding `autoHeight` to an existing page's layout, bump that page's
50
50
  `layoutVersion` so stale persisted layouts (without the flag) are reset.
51
+
52
+ ## Edit-mode chrome (2.7.1-next.0+)
53
+
54
+ While the canvas editor is open, each grid item renders an in-flow strip
55
+ (`[data-slot="grid-item-editor-chrome"]`) above the card content: the widget
56
+ name and a Hide control. The strip is **not** absolutely positioned over the
57
+ card heading. AutoHeight measures it with the content so the grid grows;
58
+ leaving edit mode removes it and restores the content-only measurement.
59
+ Do not restyle this in a consumer — it is the shared contract.
60
+
61
+ The label carries `min-w-0 truncate` and a `title` with the full name. That is
62
+ load-bearing on a narrow card, not decoration: without it the label's automatic
63
+ minimum size is its min-content, and a long unbroken widget name pushes the
64
+ `ml-auto` hide button outside the card box. Measured both ways in
65
+ `scripts/editor-chrome-geometry.mjs`, which reproduces the defect with the two
66
+ classes removed at run time.
@@ -1 +1 @@
1
- {"version":3,"file":"DraggableCardPage.d.ts","sourceRoot":"","sources":["../src/DraggableCardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAEH,OAAO,EAAW,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGpE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAItD,OAAO,EAAE,UAAU,EAAE,CAAC;AACtB,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC,MAAM,MAAM,sBAAsB,GAAG,IAAI,CACvC,mBAAmB,EACnB,SAAS,GAAG,gBAAgB,CAC7B,GAAG;IACF;;;;;;;;;;;;;;;;;;;OAmBG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC,mBAAmB,CAAC,CAAC;IACnD;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAgCF,wBAAgB,iBAAiB,CAAC,EAChC,WAAe,EACf,aAAa,EACb,iBAAiB,EACjB,QAAQ,EACR,GAAG,SAAS,EACb,EAAE,sBAAsB,+BA2BxB;yBAjCe,iBAAiB"}
1
+ {"version":3,"file":"DraggableCardPage.d.ts","sourceRoot":"","sources":["../src/DraggableCardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAEH,OAAO,EAAW,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGpE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,KAAK,EAAmB,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAIvE,OAAO,EAAE,UAAU,EAAE,CAAC;AACtB,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC,MAAM,MAAM,sBAAsB,GAAG,IAAI,CACvC,mBAAmB,EACnB,SAAS,GAAG,gBAAgB,CAC7B,GAAG;IACF;;;;;;;;;;;;;;;;;;;OAmBG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC,mBAAmB,CAAC,CAAC;IACnD;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAgCF,wBAAgB,iBAAiB,CAAC,EAChC,WAAe,EACf,aAAa,EACb,iBAAiB,EACjB,QAAQ,EACR,GAAG,SAAS,EACb,EAAE,sBAAsB,+BAyDxB;yBA/De,iBAAiB"}
@@ -105,6 +105,36 @@ export function DraggableCardPage({ layoutEpoch = 0, gridComponent, onDroppedChi
105
105
  // spaces" surface had no white spaces because the grid was 1 column wide.
106
106
  const defaultCols = pageProps.defaultCols ?? 12;
107
107
  const Grid = gridComponent ?? DefaultPageGridLayout;
108
- return (_jsx(Grid, { ...pageProps, layoutVersion: (pageProps.layoutVersion ?? 1) + layoutEpoch, defaultCols: defaultCols, widgets: widgets, defaultLayouts: layouts }));
108
+ /*
109
+ * `layoutMigrations` keys live in the same version space as `layoutVersion`,
110
+ * so they cross the epoch boundary with it. A page writes the number it
111
+ * bumped — `layoutVersion={2}` with `{ 2: … }` — and never has to know this
112
+ * app's epoch, or the package's.
113
+ *
114
+ * Getting this wrong fails SILENTLY and in the worst direction: an unshifted
115
+ * key simply never matches the step being migrated, the version gate finds
116
+ * nothing registered, and it falls back to the wholesale discard the
117
+ * migration existed to prevent. Pinned by
118
+ * `DraggableCardPage.layoutMigrations.test.tsx`.
119
+ *
120
+ * Memoised because the shifted map is an effect dependency inside
121
+ * `usePageGridLayout`; a fresh object per render would re-run an effect that
122
+ * writes state.
123
+ */
124
+ const layoutMigrations = useMemo(() => {
125
+ const declared = pageProps.layoutMigrations;
126
+ if (!declared || layoutEpoch === 0)
127
+ return declared;
128
+ const shifted = {};
129
+ for (const [version, migration] of Object.entries(declared)) {
130
+ shifted[Number(version) + layoutEpoch] = migration;
131
+ }
132
+ return shifted;
133
+ }, [pageProps.layoutMigrations, layoutEpoch]);
134
+ return (_jsx(Grid, { ...pageProps, layoutVersion: (pageProps.layoutVersion ?? 1) + layoutEpoch,
135
+ // AFTER the spread, deliberately — `pageProps` still carries the
136
+ // consumer's unshifted map, and letting the spread lay it back over this
137
+ // would look wired and silently discard instead of migrate.
138
+ layoutMigrations: layoutMigrations, defaultCols: defaultCols, widgets: widgets, defaultLayouts: layouts }));
109
139
  }
110
140
  DraggableCardPage.displayName = 'DraggableCardPage';
@@ -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, defaultAutoHeight, itemChrome, 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, layoutMigrations, itemChrome, 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":"AAEA,OAAO,KAQN,MAAM,OAAO,CAAC;AAGf,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAsBxC,OAAO,KAAK,EAAe,mBAAmB,EAA4B,MAAM,YAAY,CAAC;AAmmB7F,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,EAKjB,UAAkB,EAClB,yBAAiE,EACjE,kBAAkB,EAClB,sBAAsB,EACtB,mBAAmB,EACnB,+BAA6E,EAC7E,wBAAwB,EACxB,4BAA4B,EAC5B,4BAA4B,GAC7B,EAAE,mBAAmB,qBAs+BrB"}
1
+ {"version":3,"file":"PageGridLayout.d.ts","sourceRoot":"","sources":["../src/PageGridLayout.tsx"],"names":[],"mappings":"AAEA,OAAO,KAQN,MAAM,OAAO,CAAC;AAGf,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAsBxC,OAAO,KAAK,EAAe,mBAAmB,EAA4B,MAAM,YAAY,CAAC;AA2pB7F,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,gBAAgB,EAKhB,UAAkB,EAClB,yBAAiE,EACjE,kBAAkB,EAClB,sBAAsB,EACtB,mBAAmB,EACnB,+BAA6E,EAC7E,wBAAwB,EACxB,4BAA4B,EAC5B,4BAA4B,GAC7B,EAAE,mBAAmB,qBAu+BrB"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ElementScopeProvider } from './elementScope.js';
3
3
  import { ArrowDown, ArrowUp, ChevronDown, ChevronUp, Eye, EyeOff, Layers, LayoutGrid, Lock, Plus, RotateCcw, Save, Settings2, Unlock } from 'lucide-react';
4
4
  import React, { startTransition, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
@@ -109,6 +109,31 @@ const RGLResizeHandle = React.forwardRef(function RGLResizeHandle({ axis = 'se',
109
109
  * the silent case audible in development and leaves production untouched, which
110
110
  * is the same trade @bsuite/theme 1.3.0 made for its missing-provider warning.
111
111
  */
112
+ /**
113
+ * Edit-mode label + hide. These used to be `absolute top-2 left-2` /
114
+ * `top-2 right-2` overlays on the card (PageGridLayout GridItem, historically
115
+ * around the 407/415 marks). That painted the widget name and the hide control
116
+ * on top of the card heading — measured on the Candidates card at 1440.
117
+ *
118
+ * An earlier draft of this comment also claimed 390. Withdrawn: the instrument
119
+ * that produced that number never asserted its own `innerWidth`, and the cell
120
+ * labelled "390" reported 1560. What 390 actually looks like is now measured
121
+ * and recorded in `editor-chrome-geometry-390.json` — and it is a different
122
+ * fact: below a 480px container `buildResponsiveLayouts` stacks every card to
123
+ * full width, so 390 has no narrow card at all.
124
+ *
125
+ * In-flow, measured: the strip is a flex-none row. AutoHeight includes it in
126
+ * the unconstrained measure wrapper so the grid grows rather than overlapping.
127
+ * Fixed-height cards keep it *above* the overflow-auto body so hide cannot
128
+ * scroll away. Drag still starts from the strip (outer `.drag-handle`); hide
129
+ * is a `button` + `data-no-drag` so the existing cancel selector ignores it.
130
+ */
131
+ function GridItemEditorChrome({ id, label, onHide, }) {
132
+ return (_jsxs("div", { "data-slot": "grid-item-editor-chrome", className: "flex min-w-0 flex-none items-center gap-1 px-2 py-1 bg-muted text-muted-foreground", children: [_jsx("span", { className: "min-w-0 truncate text-[10px] px-1.5 py-0.5 rounded-md font-medium", title: label, children: label }), _jsx("button", { type: "button", className: "ml-auto h-6 w-6 shrink-0 rounded-full flex items-center justify-center bg-destructive hover:bg-destructive text-destructive-foreground shadow transition-colors", "data-no-drag": true, onPointerDown: (event) => event.stopPropagation(), onClick: (event) => {
133
+ event.stopPropagation();
134
+ onHide(id);
135
+ }, title: `Hide ${label}`, "aria-label": `Hide ${label}`, children: _jsx(EyeOff, { className: "h-3 w-3", "aria-hidden": "true" }) })] }));
136
+ }
112
137
  let warnedAboutCollapsedContent = false;
113
138
  function warnOnceAboutCollapsedContent(el) {
114
139
  if (warnedAboutCollapsedContent)
@@ -230,7 +255,9 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
230
255
  reportRows(contentPx);
231
256
  else
232
257
  warnOnceAboutCollapsedContent(el);
233
- }, [autoHeight, onAutoHeightChange, reportRows]);
258
+ // Re-measure when edit chrome mounts/unmounts so the strip is in the
259
+ // row count in edit mode and gone again on lossless return.
260
+ }, [autoHeight, onAutoHeightChange, reportRows, isEditing]);
234
261
  useEffect(() => {
235
262
  if (!autoHeight || !onAutoHeightChange)
236
263
  return;
@@ -280,10 +307,7 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
280
307
  // (buttons, inputs, textarea, select, links, [data-no-drag]) are still
281
308
  // protected via the `cancel` selector below in the <Responsive> render.
282
309
  const outerClass = cn('relative group overflow-visible', isEditing && 'drag-handle cursor-move', injectedClassName);
283
- return (_jsxs("div", { ref: ref, className: outerClass, style: injectedStyle, ...rest, children: [_jsxs("div", { className: "h-full w-full relative", children: [isEditing && (_jsx("div", { className: "absolute inset-0 z-10 pointer-events-none rounded-3xl border-2 border-transparent group-hover:border-primary/50 transition-colors bg-primary/5" })), isEditing && (_jsx("div", { className: "absolute top-2 left-2 z-30 flex items-center gap-1 pointer-events-none", children: _jsx("span", { className: "text-[10px] px-1.5 py-0.5 rounded-md opacity-80 font-medium bg-muted text-muted-foreground", children: label }) })), isEditing && (_jsx("button", { className: "absolute top-2 right-2 z-30 h-6 w-6 rounded-full flex items-center justify-center bg-destructive/80 hover:bg-destructive text-destructive-foreground shadow transition-colors", "data-no-drag": true, onPointerDown: (event) => event.stopPropagation(), onClick: (event) => {
284
- event.stopPropagation();
285
- onHide(id);
286
- }, title: `Hide ${label}`, "aria-label": `Hide ${label}`, children: _jsx(EyeOff, { className: "h-3 w-3", "aria-hidden": "true" }) })), _jsx("div", { "data-slot": "grid-item-surface", "data-chrome": chrome ? 'on' : 'off', "data-grid-slot-key": id, className: chrome
310
+ return (_jsxs("div", { ref: ref, className: outerClass, style: injectedStyle, ...rest, children: [_jsxs("div", { className: "h-full w-full relative", children: [isEditing && (_jsx("div", { className: "absolute inset-0 z-10 pointer-events-none rounded-3xl border-2 border-transparent group-hover:border-primary/50 transition-colors bg-primary/5" })), _jsx("div", { "data-slot": "grid-item-surface", "data-chrome": chrome ? 'on' : 'off', "data-grid-slot-key": id, className: chrome
287
311
  ? // ONE radius token, read by the grid item AND available to any
288
312
  // app card that opts into the same surface. Two surfaces
289
313
  // reading the SAME var is what makes their corners align, and
@@ -346,7 +370,7 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
346
370
  ...(cardStyleVars && '--card-padding' in cardStyleVars
347
371
  ? { padding: cardStyleVars['--card-padding'] }
348
372
  : null),
349
- }, children: _jsx("div", { className: cn('min-h-0', autoHeight ? 'flex-none overflow-visible' : 'flex-1 overflow-auto'), children: autoHeight ? (_jsx("div", { ref: measureRef, className: "flow-root", children: scopedContent })) : (scopedContent) }) })] }), injectedChildren] }));
373
+ }, children: autoHeight ? (_jsx("div", { "data-slot": "grid-item-body", className: "min-h-0 flex-none overflow-visible", children: _jsxs("div", { ref: measureRef, className: "flow-root", children: [isEditing ? (_jsx(GridItemEditorChrome, { id: id, label: label, onHide: onHide })) : null, scopedContent] }) })) : (_jsxs(_Fragment, { children: [isEditing ? (_jsx(GridItemEditorChrome, { id: id, label: label, onHide: onHide })) : null, _jsx("div", { "data-slot": "grid-item-body", className: "min-h-0 flex-1 overflow-auto", children: scopedContent })] })) })] }), injectedChildren] }));
350
374
  }));
351
375
  export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, widgets, widgetMeta, className,
352
376
  // Resize default flipped 2026-05-12: rgl v2 dropped auto-handle rendering
@@ -354,7 +378,7 @@ export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVer
354
378
  // adapters then defaulted it back to true. With v2's handle gap fixed,
355
379
  // the safe default is true so any consumer who forgets to set it still
356
380
  // gets a usable canvas. Opt out per-page with isResizable={false}.
357
- isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, tenantId, defaultAutoHeight,
381
+ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, tenantId, defaultAutoHeight, layoutMigrations,
358
382
  // CHROME OFF BY DEFAULT — the inversion. See `GridItemProps.chrome`.
359
383
  // An app that is not ready to migrate its own card surfaces passes
360
384
  // `itemChrome` to get the pre-2.0.0 behaviour back for every slot, and
@@ -369,6 +393,7 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
369
393
  editorEventNames,
370
394
  preferenceAdapter,
371
395
  defaultAutoHeight,
396
+ layoutMigrations,
372
397
  });
373
398
  // Auto-height dispatcher (blueprint amendment A1, hardened per quality
374
399
  // review 2026-07-14). `GridItem`'s ResizeObserver calls this per-widget
package/dist/index.d.ts CHANGED
@@ -7,8 +7,10 @@ export type { DraggableCardPageProps } from './DraggableCardPage.js';
7
7
  export { buildCanvasCardLayout, DEFAULT_CANVAS_CARD_WIDTH, flattenCanvasCards, isCanvasCardElement, describeNode, clampColumns, CANVAS_GRID_COLUMNS, } from './canvasCardLayout.js';
8
8
  export type { CanvasCardLayoutResult } from './canvasCardLayout.js';
9
9
  export type { WidgetConfig, PageEditorLauncherProps } from './PageEditorLauncher.js';
10
- export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.js';
10
+ export { usePageGridLayout, migrateSavedLayout, DEFAULT_EDITOR_EVENT_NAMES, PACKAGE_LAYOUT_EPOCH, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.js';
11
11
  export type { PageGridEditingEventDetail } from './usePageGridLayout.js';
12
+ export { adoptUnchangedDefaults } from './layoutMigrations.js';
13
+ export type { PreviousItemDefault } from './layoutMigrations.js';
12
14
  export { rescaleLayout } from './rescaleLayout.js';
13
15
  export { computeAutoHeightRows } from './autoHeight.js';
14
16
  export type { ComputeAutoHeightRowsOptions } from './autoHeight.js';
@@ -20,7 +22,7 @@ export { useLocalPreference, defaultPreferenceAdapter } from './preferences.js';
20
22
  export { RelationshipField } from './RelationshipField.js';
21
23
  export type { RelationshipFieldProps } from './RelationshipField.js';
22
24
  export { isRelationshipWritable } from './relationshipCatalog.js';
23
- export type { EntityWidgetDetail, EntityWidgetFactoryOptions, GridLayoutItem, GridLayouts, PageGridLayoutProps, PageGridPreferenceAdapter, PageGridPreferenceFactory, RelationshipCatalog, RelationshipCatalogEntry, RelationshipFieldOption, RelationshipWidgetDetail, RelationshipWidgetFactoryOptions, UsePageGridLayoutOptions, UsePageGridLayoutResult, WidgetMeta, } from './types.js';
25
+ export type { EntityWidgetDetail, EntityWidgetFactoryOptions, GridLayoutItem, GridLayouts, PageGridLayoutProps, PageGridPreferenceAdapter, PageGridPreferenceFactory, RelationshipCatalog, RelationshipCatalogEntry, RelationshipFieldOption, RelationshipWidgetDetail, RelationshipWidgetFactoryOptions, LayoutMigration, UsePageGridLayoutOptions, UsePageGridLayoutResult, WidgetMeta, } from './types.js';
24
26
  export { BORDER_TONES, BORDER_STYLES, DEFAULT_CARD_STYLE, RADIUS_RANGE, BORDER_WIDTH_RANGE, PADDING_RANGE, normaliseCardStyle, isDefaultCardStyle, toCssVars as cardStyleToCssVars, describeCardStyle, } from './cardStyle.js';
25
27
  export type { CardStyle, BorderTone, BorderStyle, Elevation } from './cardStyle.js';
26
28
  export { ElementScopeProvider, useElementScope, elementRef, describeElement, parseElementRef, } from './elementScope.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAwB7C,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AACrE,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,YAAY,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,EACL,iBAAiB,EACjB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxD,YAAY,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EACzB,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,6BAA6B,EAC7B,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,YAAY,EACV,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,yBAAyB,EACzB,yBAAyB,EACzB,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,gCAAgC,EAChC,wBAAwB,EACxB,uBAAuB,EACvB,UAAU,GACX,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,SAAS,IAAI,kBAAkB,EAC/B,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAQpF,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,UAAU,EACV,eAAe,EACf,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAwB7C,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AACrE,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,YAAY,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,0BAA0B,EAC1B,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,YAAY,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxD,YAAY,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EACzB,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,6BAA6B,EAC7B,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,YAAY,EACV,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,yBAAyB,EACzB,yBAAyB,EACzB,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,gCAAgC,EAChC,eAAe,EACf,wBAAwB,EACxB,uBAAuB,EACvB,UAAU,GACX,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,SAAS,IAAI,kBAAkB,EAC/B,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAQpF,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,UAAU,EACV,eAAe,EACf,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -3,7 +3,8 @@ export { PageEditorLauncher } from './PageEditorLauncher.js';
3
3
  export { CanvasCard } from './CanvasCard.js';
4
4
  export { DraggableCardPage } from './DraggableCardPage.js';
5
5
  export { buildCanvasCardLayout, DEFAULT_CANVAS_CARD_WIDTH, flattenCanvasCards, isCanvasCardElement, describeNode, clampColumns, CANVAS_GRID_COLUMNS, } from './canvasCardLayout.js';
6
- export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.js';
6
+ export { usePageGridLayout, migrateSavedLayout, DEFAULT_EDITOR_EVENT_NAMES, PACKAGE_LAYOUT_EPOCH, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.js';
7
+ export { adoptUnchangedDefaults } from './layoutMigrations.js';
7
8
  export { rescaleLayout } from './rescaleLayout.js';
8
9
  export { computeAutoHeightRows } from './autoHeight.js';
9
10
  export { WYSIWYG_REQUIRED_PRIMITIVES, WYSIWYG_SURFACE_CONTRACTS, assertWysiwygPrimitiveCoverage, getWysiwygSurfaceContract, } from './wysiwygContract.js';
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Migrations for a `layoutVersion` bump — the alternative to throwing the
3
+ * user's arrangement away.
4
+ *
5
+ * `usePageGridLayout` had exactly one response to a version bump: overwrite
6
+ * the stored layout with `defaultLayouts`. That is right for the reason
7
+ * crm7's dashboard epoch 8 was raised — a saved layout referencing widgets
8
+ * that no longer exist cannot be repaired — and wrong for the reason a
9
+ * version usually gets bumped, which is that a DEFAULT changed. crm7#2490
10
+ * bumped four routes purely to change default card widths; measured on one
11
+ * account, that discarded 14 stored layouts.
12
+ *
13
+ * The doctrine is already in `usePageGridLayout.ts`, in the derived-breakpoint
14
+ * heal: "`lg` … is preserved untouched, so nobody loses the arrangement they
15
+ * made." A migration is how a version bump keeps that promise.
16
+ */
17
+ import type { LayoutMigration } from './types.js';
18
+ /**
19
+ * The subset of an item's OLD default a migration compares against.
20
+ *
21
+ * `w` is required, because the whole test is "does the stored value still equal
22
+ * what the page used to author?". `minW` and `position` are optional: state
23
+ * them only where the bump actually moved them, because stating a value is what
24
+ * makes it a candidate for replacement.
25
+ *
26
+ * `position` is `{x, y}` and not two loose fields ON PURPOSE. Half a match is
27
+ * not a match: an item whose `x` still equals the old default but whose `y` does
28
+ * not was moved, and adopting only `x` would invent a third position that
29
+ * neither the user nor the page ever chose. Making the pair a single object
30
+ * means a caller cannot record half of it by accident.
31
+ */
32
+ export interface PreviousItemDefault {
33
+ w: number;
34
+ minW?: number;
35
+ position?: {
36
+ x: number;
37
+ y: number;
38
+ };
39
+ }
40
+ /**
41
+ * A migration for a bump that changed DEFAULTS — widths, positions, or both.
42
+ *
43
+ * One rule, applied per item: **adopt what the user never chose, keep what they
44
+ * did.** A stored value still equal to the old default was never a decision; it
45
+ * is inherited furniture.
46
+ *
47
+ * saved position === the OLD default position
48
+ * -> the user never placed this card. Adopt the new default `x` and `y`
49
+ * (atomically — never one without the other), and go on to consider
50
+ * its width.
51
+ * saved position !== the OLD default position
52
+ * -> the user placed this card. It is IMMUNE: position and width are both
53
+ * left alone. A card someone put somewhere by hand must not silently
54
+ * change size underneath them either, and reflowing around a
55
+ * hand-placed card is how a deliberate arrangement stops making sense.
56
+ *
57
+ * saved w === the OLD default w -> adopt the new default width
58
+ * saved w !== the OLD default w -> the user sized it; keep it
59
+ *
60
+ * and the same comparison for `minW`. Height, `autoHeight`, `chrome`,
61
+ * `hUserSet` and everything else are never touched.
62
+ *
63
+ * An entry that records no `position` is a WIDTH-ONLY migration: nothing moves.
64
+ * That is the pre-existing behaviour and is kept so a migration written before
65
+ * the position rule existed cannot start relocating cards.
66
+ *
67
+ * Why position is in scope at all, since it did not start that way: treating
68
+ * width as inherited while treating position as sacred splits one rule into two,
69
+ * and produces a layout nobody authored. Measured on `/clients` — the four stat
70
+ * cards asked for `w={3}` and stored 4 (the default `minW` clamps `w` up), so
71
+ * three filled the row and the fourth wrapped. Adopting only widths narrowed
72
+ * them to 3 but left them at x 0/4/8 with the fourth still wrapped: neither the
73
+ * arrangement the user had nor the 0/3/6/9 the page now authors. Pinned in
74
+ * `layoutVersionMigration.test.tsx`.
75
+ *
76
+ * @param previousDefaults keyed by layout item id (`i` / `cardKey`). An item
77
+ * with no entry is passed through untouched, so a bump only has to name the
78
+ * items whose defaults actually moved.
79
+ */
80
+ export declare function adoptUnchangedDefaults(previousDefaults: Readonly<Record<string, PreviousItemDefault>>): LayoutMigration;
81
+ //# sourceMappingURL=layoutMigrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layoutMigrations.d.ts","sourceRoot":"","sources":["../src/layoutMigrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAA+B,eAAe,EAAE,MAAM,YAAY,CAAC;AAE/E;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,mBAAmB;IAClC,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,sBAAsB,CACpC,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,GAC9D,eAAe,CA2CjB"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Migrations for a `layoutVersion` bump — the alternative to throwing the
3
+ * user's arrangement away.
4
+ *
5
+ * `usePageGridLayout` had exactly one response to a version bump: overwrite
6
+ * the stored layout with `defaultLayouts`. That is right for the reason
7
+ * crm7's dashboard epoch 8 was raised — a saved layout referencing widgets
8
+ * that no longer exist cannot be repaired — and wrong for the reason a
9
+ * version usually gets bumped, which is that a DEFAULT changed. crm7#2490
10
+ * bumped four routes purely to change default card widths; measured on one
11
+ * account, that discarded 14 stored layouts.
12
+ *
13
+ * The doctrine is already in `usePageGridLayout.ts`, in the derived-breakpoint
14
+ * heal: "`lg` … is preserved untouched, so nobody loses the arrangement they
15
+ * made." A migration is how a version bump keeps that promise.
16
+ */
17
+ /**
18
+ * A migration for a bump that changed DEFAULTS — widths, positions, or both.
19
+ *
20
+ * One rule, applied per item: **adopt what the user never chose, keep what they
21
+ * did.** A stored value still equal to the old default was never a decision; it
22
+ * is inherited furniture.
23
+ *
24
+ * saved position === the OLD default position
25
+ * -> the user never placed this card. Adopt the new default `x` and `y`
26
+ * (atomically — never one without the other), and go on to consider
27
+ * its width.
28
+ * saved position !== the OLD default position
29
+ * -> the user placed this card. It is IMMUNE: position and width are both
30
+ * left alone. A card someone put somewhere by hand must not silently
31
+ * change size underneath them either, and reflowing around a
32
+ * hand-placed card is how a deliberate arrangement stops making sense.
33
+ *
34
+ * saved w === the OLD default w -> adopt the new default width
35
+ * saved w !== the OLD default w -> the user sized it; keep it
36
+ *
37
+ * and the same comparison for `minW`. Height, `autoHeight`, `chrome`,
38
+ * `hUserSet` and everything else are never touched.
39
+ *
40
+ * An entry that records no `position` is a WIDTH-ONLY migration: nothing moves.
41
+ * That is the pre-existing behaviour and is kept so a migration written before
42
+ * the position rule existed cannot start relocating cards.
43
+ *
44
+ * Why position is in scope at all, since it did not start that way: treating
45
+ * width as inherited while treating position as sacred splits one rule into two,
46
+ * and produces a layout nobody authored. Measured on `/clients` — the four stat
47
+ * cards asked for `w={3}` and stored 4 (the default `minW` clamps `w` up), so
48
+ * three filled the row and the fourth wrapped. Adopting only widths narrowed
49
+ * them to 3 but left them at x 0/4/8 with the fourth still wrapped: neither the
50
+ * arrangement the user had nor the 0/3/6/9 the page now authors. Pinned in
51
+ * `layoutVersionMigration.test.tsx`.
52
+ *
53
+ * @param previousDefaults keyed by layout item id (`i` / `cardKey`). An item
54
+ * with no entry is passed through untouched, so a bump only has to name the
55
+ * items whose defaults actually moved.
56
+ */
57
+ export function adoptUnchangedDefaults(previousDefaults) {
58
+ return (saved, defaults) => {
59
+ const migrated = { lg: [] };
60
+ for (const breakpoint of Object.keys(saved)) {
61
+ const defaultItems = defaults[breakpoint] ??
62
+ defaults.lg ??
63
+ [];
64
+ const defaultByKey = new Map(defaultItems.map((item) => [item.i, item]));
65
+ migrated[breakpoint] = (saved[breakpoint] ?? []).map((item) => {
66
+ const previous = previousDefaults[item.i];
67
+ const current = defaultByKey.get(item.i);
68
+ // No recorded old default, or the item is gone from the new defaults:
69
+ // there is nothing to compare against, so nothing is a candidate.
70
+ if (!previous || !current)
71
+ return item;
72
+ // Position is decided FIRST, because a moved item is immune to the
73
+ // width rule as well — returning the item itself, not a copy, so the
74
+ // immunity is visible in the object identity too.
75
+ const placed = previous.position !== undefined &&
76
+ (item.x !== previous.position.x || item.y !== previous.position.y);
77
+ if (placed)
78
+ return item;
79
+ // A fresh object every time — the caller's array is state that other
80
+ // renders still hold, and mutating it in place would edit the "before"
81
+ // out from under anyone comparing the two.
82
+ const next = { ...item };
83
+ if (previous.position !== undefined) {
84
+ next.x = current.x;
85
+ next.y = current.y;
86
+ }
87
+ if (item.w === previous.w)
88
+ next.w = current.w;
89
+ if (previous.minW !== undefined && item.minW === previous.minW) {
90
+ if (current.minW === undefined)
91
+ delete next.minW;
92
+ else
93
+ next.minW = current.minW;
94
+ }
95
+ return next;
96
+ });
97
+ }
98
+ if (!migrated.lg)
99
+ migrated.lg = saved.lg ?? [];
100
+ return migrated;
101
+ };
102
+ }
package/dist/types.d.ts CHANGED
@@ -53,6 +53,17 @@ export interface GridLayouts {
53
53
  lg: GridLayoutItem[];
54
54
  [key: string]: GridLayoutItem[];
55
55
  }
56
+ /**
57
+ * Transforms a saved layout from the version immediately below a bump to the
58
+ * version at it. Pure: it is handed the stored layout and the NEW authored
59
+ * defaults, and returns the layout to store.
60
+ *
61
+ * Registered per target version in {@link UsePageGridLayoutOptions.layoutMigrations}.
62
+ * A step with no registered migration falls back to the wholesale discard,
63
+ * which is the correct answer when a bump's reason is that saved layouts
64
+ * reference widgets that no longer exist.
65
+ */
66
+ export type LayoutMigration = (saved: GridLayouts, defaults: GridLayouts) => GridLayouts;
56
67
  export interface PageGridPreferenceAdapter<T> {
57
68
  value: T;
58
69
  setValue: (value: T | ((previous: T) => T)) => void;
@@ -75,6 +86,40 @@ export interface UsePageGridLayoutOptions {
75
86
  * can always opt out with an explicit `autoHeight: false`.
76
87
  */
77
88
  defaultAutoHeight?: boolean;
89
+ /**
90
+ * Migrations that TRANSFORM a stored layout across a `layoutVersion` bump
91
+ * instead of destroying it, keyed by the version each one produces.
92
+ *
93
+ * Without this, every bump has exactly one outcome: the stored layout is
94
+ * overwritten with `defaultLayouts` and every card position the user ever
95
+ * dragged on that page is gone, for every user. That is the right answer
96
+ * when the bump's reason is that saved layouts reference widgets which no
97
+ * longer exist (crm7's dashboard epoch 8), and the wrong one for the reason
98
+ * a version usually moves — a DEFAULT changed. crm7#2490 bumped four routes
99
+ * purely to change default card widths and, measured on one account, cost 14
100
+ * stored layouts.
101
+ *
102
+ * Keys are in the SAME version space as `layoutVersion` on the component you
103
+ * pass them to — `layoutVersion={2}` pairs with `{ 2: … }`. Any app-level
104
+ * epoch (`DraggableCardPage`'s `layoutEpoch`) and the package-level
105
+ * `PACKAGE_LAYOUT_EPOCH` are applied to these keys exactly as they are
106
+ * applied to `layoutVersion`, so a page author never writes an epoch down.
107
+ *
108
+ * Every step from the stored version up to the current one must have a
109
+ * registered migration; they are applied in ascending order. If ANY step in
110
+ * that span has none, the whole span falls back to the wholesale discard —
111
+ * a missing migration means "nobody has said this transition is safe", and
112
+ * guessing is how a layout gets silently corrupted rather than reset. A
113
+ * `PACKAGE_LAYOUT_EPOCH` bump therefore discards by construction, which is
114
+ * what that lever is for.
115
+ *
116
+ * Use a module-level constant, not an object literal in the render body:
117
+ * this is an effect dependency.
118
+ *
119
+ * @see adoptUnchangedDefaults for the common case — a bump that only moved
120
+ * default widths and/or default positions.
121
+ */
122
+ layoutMigrations?: Readonly<Record<number, LayoutMigration>>;
78
123
  }
79
124
  export interface WidgetMeta {
80
125
  label: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { UsePageGridLayoutOptions, UsePageGridLayoutResult } from './types.js';
1
+ import type { GridLayouts, LayoutMigration, UsePageGridLayoutOptions, UsePageGridLayoutResult } from './types.js';
2
2
  export declare const DEFAULT_EDITOR_EVENT_NAMES: readonly ["bsuite-open-page-editor", "bsu-open-page-editor", "crm7-open-page-editor", "conduit-open-page-editor", "r80-open-page-editor"];
3
3
  /**
4
4
  * Event name broadcast on `window` whenever any PageGridLayout transitions
@@ -38,9 +38,38 @@ export declare const PAGE_GRID_EDITING_EVENT = "bsuite-page-grid-editing";
38
38
  * floor.
39
39
  */
40
40
  export declare const PACKAGE_LAYOUT_EPOCH = 2000;
41
+ /**
42
+ * Walk a stored layout from the version it was written at up to the version
43
+ * the page now declares, applying one registered migration per step.
44
+ *
45
+ * Returns the migrated layout, or `null` meaning "this span cannot be
46
+ * migrated — discard". Discarding is not a failure mode here; it is the
47
+ * pre-existing behaviour, kept deliberately as the fallback. crm7's dashboard
48
+ * epoch 8 was raised because saved layouts referenced widgets that no longer
49
+ * existed, and no transformation can repair that. A step nobody registered a
50
+ * migration for means nobody has said the transition is safe, and inventing
51
+ * one silently corrupts a layout instead of resetting it.
52
+ *
53
+ * Consequences that fall out of this rule, all of them wanted:
54
+ * - A first visit (`from` 0) has no registered step and no stored layout to
55
+ * preserve, so it discards onto the defaults exactly as before.
56
+ * - A `PACKAGE_LAYOUT_EPOCH` bump moves the target by a thousand, so the span
57
+ * cannot be covered and every consumer resets — which is what that lever
58
+ * exists to do.
59
+ *
60
+ * Exported for direct unit testing: this is the decision, and it should be
61
+ * assertable without a React tree around it.
62
+ */
63
+ export declare function migrateSavedLayout({ saved, defaults, from, to, migrations, }: {
64
+ saved: GridLayouts | undefined;
65
+ defaults: GridLayouts;
66
+ from: number;
67
+ to: number;
68
+ migrations: ReadonlyMap<number, LayoutMigration> | null;
69
+ }): GridLayouts | null;
41
70
  export interface PageGridEditingEventDetail {
42
71
  pageKey: string;
43
72
  editing: boolean;
44
73
  }
45
- export declare function usePageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, defaultAutoHeight, }: UsePageGridLayoutOptions): UsePageGridLayoutResult;
74
+ export declare function usePageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, defaultAutoHeight, layoutMigrations, }: UsePageGridLayoutOptions): UsePageGridLayoutResult;
46
75
  //# sourceMappingURL=usePageGridLayout.d.ts.map
@@ -1 +1 @@
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;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;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,CAsuBpD"}
1
+ {"version":3,"file":"usePageGridLayout.d.ts","sourceRoot":"","sources":["../src/usePageGridLayout.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAEV,WAAW,EACX,eAAe,EACf,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,0BAA0B,2IAM7B,CAAC;AAEX;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAiBzC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,IAAI,EACJ,EAAE,EACF,UAAU,GACX,EAAE;IACD,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,IAAI,CAAC;CACzD,GAAG,WAAW,GAAG,IAAI,CA+BrB;AAED,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,EACjB,gBAAgB,GACjB,EAAE,wBAAwB,GAAG,uBAAuB,CA8yBpD"}
@@ -62,7 +62,65 @@ export const PACKAGE_LAYOUT_EPOCH = 2000;
62
62
  * opts out with an explicit `autoHeight: false`.
63
63
  */
64
64
  const DEFAULT_ITEM_AUTO_HEIGHT = true;
65
- export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, layoutVersion = 1, canEditPage = true, editorEventNames = DEFAULT_EDITOR_EVENT_NAMES, preferenceAdapter = defaultPreferenceAdapter, defaultAutoHeight, }) {
65
+ /**
66
+ * Walk a stored layout from the version it was written at up to the version
67
+ * the page now declares, applying one registered migration per step.
68
+ *
69
+ * Returns the migrated layout, or `null` meaning "this span cannot be
70
+ * migrated — discard". Discarding is not a failure mode here; it is the
71
+ * pre-existing behaviour, kept deliberately as the fallback. crm7's dashboard
72
+ * epoch 8 was raised because saved layouts referenced widgets that no longer
73
+ * existed, and no transformation can repair that. A step nobody registered a
74
+ * migration for means nobody has said the transition is safe, and inventing
75
+ * one silently corrupts a layout instead of resetting it.
76
+ *
77
+ * Consequences that fall out of this rule, all of them wanted:
78
+ * - A first visit (`from` 0) has no registered step and no stored layout to
79
+ * preserve, so it discards onto the defaults exactly as before.
80
+ * - A `PACKAGE_LAYOUT_EPOCH` bump moves the target by a thousand, so the span
81
+ * cannot be covered and every consumer resets — which is what that lever
82
+ * exists to do.
83
+ *
84
+ * Exported for direct unit testing: this is the decision, and it should be
85
+ * assertable without a React tree around it.
86
+ */
87
+ export function migrateSavedLayout({ saved, defaults, from, to, migrations, }) {
88
+ if (!migrations || migrations.size === 0)
89
+ return null;
90
+ if (from >= to)
91
+ return null;
92
+ // Nothing stored (or an empty object) is not an arrangement worth carrying
93
+ // across — and `rawLayouts` already treats an empty stored layout as "use
94
+ // the defaults", so migrating it would be a no-op wearing a cost.
95
+ if (!saved || Object.keys(saved).length === 0)
96
+ return null;
97
+ // O(1) refusal before the loop: a span wider than the number of registered
98
+ // migrations must contain a step with none.
99
+ //
100
+ // This is a COST guard, not a correctness one — the loop below reaches the
101
+ // same `null` without it, because it bails at the first version with no
102
+ // migration. What it saves is AT MOST `migrations.size + 1` registry lookups,
103
+ // reached only when the registry happens to be contiguous from `from + 1`;
104
+ // measured, size 1 -> 2, size 5 -> 6, size 500 -> 501. For crm7#2490's shape
105
+ // — one migration keyed 2103, a first visit from 0 — it is ONE lookup, since
106
+ // the very first miss ends the walk.
107
+ //
108
+ // An earlier version of this comment claimed a first visit would "spin two
109
+ // thousand times". That was false: the work is bounded by the REGISTRY, never
110
+ // by the span. The guard is still worth keeping, and it is worth keeping for
111
+ // the size it actually has.
112
+ if (to - from > migrations.size)
113
+ return null;
114
+ let working = saved;
115
+ for (let version = from + 1; version <= to; version += 1) {
116
+ const migration = migrations.get(version);
117
+ if (!migration)
118
+ return null;
119
+ working = migration(working, defaults);
120
+ }
121
+ return working;
122
+ }
123
+ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, layoutVersion = 1, canEditPage = true, editorEventNames = DEFAULT_EDITOR_EVENT_NAMES, preferenceAdapter = defaultPreferenceAdapter, defaultAutoHeight, layoutMigrations, }) {
66
124
  const effectiveLayoutVersion = layoutVersion + PACKAGE_LAYOUT_EPOCH;
67
125
  const containerRef = useRef(null);
68
126
  const [containerWidth, setContainerWidth] = useState(0);
@@ -105,21 +163,91 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
105
163
  const { value: savedLayoutCols, setValue: setLayoutCols } = preferenceAdapter(`page:${pageKey}_grid_cols`, defaultCols);
106
164
  const { value: savedBaseCols, setValue: setBaseCols } = preferenceAdapter(`page:${pageKey}_grid_base_cols`, defaultCols);
107
165
  const prefsLoaded = layoutLoaded && versionLoaded;
166
+ /*
167
+ * Migration keys arrive in the CONSUMER's version space — `layoutVersion={2}`
168
+ * pairs with `{ 2: … }` — and are shifted onto the stored (effective) space
169
+ * by exactly the same epoch that shifts `layoutVersion`. That symmetry is the
170
+ * point: a page author writes the number they bumped, and never has to know
171
+ * `PACKAGE_LAYOUT_EPOCH` exists. `DraggableCardPage` applies its own
172
+ * `layoutEpoch` to both in the same way, one level up.
173
+ */
174
+ const effectiveMigrations = useMemo(() => {
175
+ if (!layoutMigrations)
176
+ return null;
177
+ const byEffectiveVersion = new Map();
178
+ for (const [version, migration] of Object.entries(layoutMigrations)) {
179
+ byEffectiveVersion.set(Number(version) + PACKAGE_LAYOUT_EPOCH, migration);
180
+ }
181
+ return byEffectiveVersion;
182
+ }, [layoutMigrations]);
183
+ /*
184
+ * The stored layout, mirrored for the version gate below — which must READ it
185
+ * without DEPENDING on it.
186
+ *
187
+ * Depending on it would re-run the gate on every drag that saves a layout,
188
+ * and worse, would re-run it in the window between the gate writing the
189
+ * migrated layout and the new version landing: an adapter that surfaces those
190
+ * two writes in separate renders would then apply the same migration twice.
191
+ * `adoptUnchangedDefaults` happens to be idempotent; a migration in general is
192
+ * not, and a contract that only holds for the migrations that exist today is
193
+ * not a contract.
194
+ *
195
+ * `useLayoutEffect`, not a render-phase assignment and not `useEffect`: all
196
+ * layout effects for a commit run before any passive effect, so on mount this
197
+ * is populated before the gate's `useEffect` reads it. Declared here, above
198
+ * the gate, so that ordering is visible rather than inferred.
199
+ */
200
+ const savedLayoutForVersionGateRef = useRef(savedLayout);
201
+ useLayoutEffect(() => {
202
+ savedLayoutForVersionGateRef.current = savedLayout;
203
+ }, [savedLayout]);
108
204
  useEffect(() => {
109
205
  if (!prefsLoaded)
110
206
  return;
111
- if ((savedLayoutVersion ?? 0) < effectiveLayoutVersion) {
112
- startTransition(() => {
113
- setSavedLayout(defaultLayouts);
207
+ const storedVersion = savedLayoutVersion ?? 0;
208
+ if (storedVersion >= effectiveLayoutVersion)
209
+ return;
210
+ /*
211
+ * MIGRATE, DO NOT DISCARD.
212
+ *
213
+ * This used to have one branch: overwrite the stored layout with
214
+ * `defaultLayouts`. So ANY `layoutVersion` bump threw away every card
215
+ * position the user had ever dragged on that page, for every user — and
216
+ * the commonest reason to bump a version is that a DEFAULT WIDTH changed,
217
+ * which is a transformation, not a reason to lose an arrangement.
218
+ * crm7#2490 bumped four routes purely to change default widths; measured on
219
+ * one account (braden@braden.com.au), that was 14 layouts.
220
+ *
221
+ * The doctrine is forty lines below, in the derived-breakpoint heal: "`lg`
222
+ * … is preserved untouched, so nobody loses the arrangement they made."
223
+ * `migrateSavedLayout` returning null keeps the discard for the case that
224
+ * genuinely needs it.
225
+ */
226
+ const migrated = migrateSavedLayout({
227
+ saved: savedLayoutForVersionGateRef.current,
228
+ defaults: defaultLayouts,
229
+ from: storedVersion,
230
+ to: effectiveLayoutVersion,
231
+ migrations: effectiveMigrations,
232
+ });
233
+ startTransition(() => {
234
+ setSavedLayout(migrated ?? defaultLayouts);
235
+ // Column choice is part of the arrangement, not part of the layout array:
236
+ // a migration preserves what the user set up, so it must not reset these
237
+ // either. A discard still does — the layout it is restoring is authored
238
+ // at `defaultCols`, and leaving a stale `baseCols` behind would rescale
239
+ // the fresh defaults against a column count nothing authored them at.
240
+ if (migrated === null) {
114
241
  setLayoutCols(defaultCols);
115
242
  setBaseCols(defaultCols);
116
- setSavedLayoutVersion(effectiveLayoutVersion);
117
- });
118
- }
243
+ }
244
+ setSavedLayoutVersion(effectiveLayoutVersion);
245
+ });
119
246
  }, [
120
247
  defaultCols,
121
248
  defaultLayouts,
122
249
  effectiveLayoutVersion,
250
+ effectiveMigrations,
123
251
  prefsLoaded,
124
252
  savedLayoutVersion,
125
253
  setBaseCols,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsuite/page-builder",
3
- "version": "2.6.1",
3
+ "version": "2.7.1-next.0",
4
4
  "description": "Shared BSuite responsive page-builder grid and layout persistence primitives",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -33,6 +33,7 @@
33
33
  "build": "tsc -p tsconfig.build.json",
34
34
  "typecheck": "tsc --noEmit",
35
35
  "test": "vitest run",
36
+ "test:geometry": "node scripts/editor-chrome-geometry.mjs",
36
37
  "prepublishOnly": "pnpm build"
37
38
  },
38
39
  "peerDependencies": {
@@ -1,68 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- /**
3
- * The card-surface defect classes, made VISIBLE and DIFFABLE.
4
- *
5
- * `@bsuite/page-builder` 1.0.4, 1.0.5 and 1.0.6 each shipped GREEN AND BROKEN.
6
- * Types, lint, unit tests and CI cannot see a 28px card sitting inside a 24px
7
- * frame; only a rendered surface can. These stories exist so the chrome
8
- * inversion has something to be checked against that is not a number.
9
- *
10
- * Read them in pairs: the story named "…(the defect)" is what shipped up to
11
- * 1.0.7, and the one beside it is what 2.0.0 renders.
12
- */
13
- /**
14
- * Plain CSF, no `Meta`/`StoryObj` type imports.
15
- *
16
- * `@storybook/react-vite` is a devDependency of `@bsuite/ui`, which owns the
17
- * Storybook config; this package does not depend on Storybook and must not
18
- * start to, or `pnpm typecheck` here fails on a type-only import for a tool
19
- * that is not installed. CSF is a plain-object format — the indexer reads the
20
- * default export and the named exports, and the types are optional sugar.
21
- */
22
- type Story = {
23
- name?: string;
24
- render: () => ReactNode;
25
- };
26
- declare const _default: {
27
- title: string;
28
- parameters: {
29
- layout: string;
30
- };
31
- };
32
- export default _default;
33
- /**
34
- * THE DEFECT, exactly as it shipped. A card inside the grid item's card:
35
- * two radii, two 1px borders, two backgrounds. 316+ files render this.
36
- */
37
- export declare const NestedCardDoubleFrame: Story;
38
- /** The same content on 2.0.0 defaults: ONE border, ONE radius. */
39
- export declare const NestedCardSingleFrame: Story;
40
- /** Bare content with chrome OFF — the regression the inversion could cause. */
41
- export declare const BareContentNoChrome: Story;
42
- /** The same bare content opting IN per item. This is the migration for the 10%. */
43
- export declare const BareContentOptsIn: Story;
44
- /**
45
- * Every card width side by side. `w` defaulted to 12, so 1,068 of 1,729 usages
46
- * rendered like the last row here — one card per row, the rest of the screen
47
- * empty. That is the "cards don't use the available space" report.
48
- */
49
- export declare const WidthLadder: Story;
50
- /**
51
- * Card headings. `text-gradient-accent` is on 183 of 235 crm7 h1 page titles
52
- * and 0 of 310 crm7 h2/h3 CARD headings — the element the operator keeps
53
- * asking about. The pairing matters: the solid `text-(--role-text-heading)`
54
- * sits UNDER the gradient as the fallback layer.
55
- *
56
- * `width: fit-content` inside the utility is load-bearing. Anything that
57
- * overrides width makes a short heading sample only the first ~15% of the
58
- * gradient and render flat — indistinguishable from having no gradient at all,
59
- * which is why "is the class present" is not a check.
60
- */
61
- export declare const CardHeadingGradient: Story;
62
- export declare const CardStyleDefault: Story;
63
- export declare const CardStyleThickBorder: Story;
64
- export declare const CardStyleDashedAccent: Story;
65
- export declare const CardStyleSquare: Story;
66
- export declare const CardStyleBorderless: Story;
67
- export declare const CardStyleElevationLadder: Story;
68
- //# sourceMappingURL=cardSurfaces.stories.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cardSurfaces.stories.d.ts","sourceRoot":"","sources":["../src/cardSurfaces.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC;;;;;;;;;;GAUG;AACH;;;;;;;;GAQG;AACH,KAAK,KAAK,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,SAAS,CAAA;CAAE,CAAA;;;;;;;AAEvD,wBAGC;AA0BD;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAYnC,CAAA;AAED,kEAAkE;AAClE,eAAO,MAAM,qBAAqB,EAAE,KAWnC,CAAA;AAED,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,EAAE,KAWjC,CAAA;AAED,mFAAmF;AACnF,eAAO,MAAM,iBAAiB,EAAE,KAW/B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,KAyBzB,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,EAAE,KAoBjC,CAAA;AA2CD,eAAO,MAAM,gBAAgB,EAAE,KAK9B,CAAA;AAED,eAAO,MAAM,oBAAoB,EAAE,KAGlC,CAAA;AAED,eAAO,MAAM,qBAAqB,EAAE,KAQnC,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,KAG7B,CAAA;AAED,eAAO,MAAM,mBAAmB,EAAE,KAQjC,CAAA;AAED,eAAO,MAAM,wBAAwB,EAAE,KAatC,CAAA"}
@@ -1,126 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { PageGridLayout } from './PageGridLayout.js';
3
- import { DEFAULT_CARD_STYLE, describeCardStyle, toCssVars } from './cardStyle.js';
4
- export default {
5
- title: 'Page Builder/Card surfaces',
6
- parameters: { layout: 'fullscreen' },
7
- };
8
- /** What an app renders inside a slot: its own card. Border, radius, background. */
9
- function AppCard({ title, children }) {
10
- return (_jsxs("div", { className: "h-full rounded-[var(--radius-card,1.5rem)] border border-border bg-card p-6 shadow-sm", children: [_jsx("h3", { className: "mb-2 text-lg font-semibold text-(--role-text-heading)", children: title }), _jsx("div", { className: "text-sm text-muted-foreground", children: children })] }));
11
- }
12
- /** Bare content: no surface of its own. This is the 10% that NEEDS the chrome. */
13
- function BareContent({ title }) {
14
- return (_jsxs("div", { className: "p-6", children: [_jsx("h3", { className: "mb-2 text-lg font-semibold text-(--role-text-heading)", children: title }), _jsx("p", { className: "text-sm text-muted-foreground", children: "No surface of its own \u2014 this slot is the one that should opt IN to chrome." })] }));
15
- }
16
- const oneSlot = { lg: [{ i: 'a', x: 0, y: 0, w: 6, h: 6 }] };
17
- /**
18
- * THE DEFECT, exactly as it shipped. A card inside the grid item's card:
19
- * two radii, two 1px borders, two backgrounds. 316+ files render this.
20
- */
21
- export const NestedCardDoubleFrame = {
22
- name: 'Nested card — DOUBLE FRAME (the defect)',
23
- render: () => (_jsx("div", { className: "p-8", children: _jsx(PageGridLayout, { pageKey: "sb/nested-defect", defaultLayouts: oneSlot, itemChrome: true, widgets: { a: _jsx(AppCard, { title: "Quick Actions", children: "Grid item chrome ON + app card." }) } }) })),
24
- };
25
- /** The same content on 2.0.0 defaults: ONE border, ONE radius. */
26
- export const NestedCardSingleFrame = {
27
- name: 'Nested card — ONE frame (2.0.0 default)',
28
- render: () => (_jsx("div", { className: "p-8", children: _jsx(PageGridLayout, { pageKey: "sb/nested-fixed", defaultLayouts: oneSlot, widgets: { a: _jsx(AppCard, { title: "Quick Actions", children: "Grid item chrome OFF; the app card is the only surface." }) } }) })),
29
- };
30
- /** Bare content with chrome OFF — the regression the inversion could cause. */
31
- export const BareContentNoChrome = {
32
- name: 'Bare content, chrome OFF — the regression to watch for',
33
- render: () => (_jsx("div", { className: "p-8", children: _jsx(PageGridLayout, { pageKey: "sb/bare-off", defaultLayouts: oneSlot, widgets: { a: _jsx(BareContent, { title: "Pipeline Overview" }) } }) })),
34
- };
35
- /** The same bare content opting IN per item. This is the migration for the 10%. */
36
- export const BareContentOptsIn = {
37
- name: 'Bare content, per-item chrome ON — the migration',
38
- render: () => (_jsx("div", { className: "p-8", children: _jsx(PageGridLayout, { pageKey: "sb/bare-on", defaultLayouts: { lg: [{ i: 'a', x: 0, y: 0, w: 6, h: 6, chrome: true }] }, widgets: { a: _jsx(BareContent, { title: "Pipeline Overview" }) } }) })),
39
- };
40
- /**
41
- * Every card width side by side. `w` defaulted to 12, so 1,068 of 1,729 usages
42
- * rendered like the last row here — one card per row, the rest of the screen
43
- * empty. That is the "cards don't use the available space" report.
44
- */
45
- export const WidthLadder = {
46
- name: 'Width ladder — w = 2 / 3 / 4 / 6 / 12',
47
- render: () => (_jsx("div", { className: "p-8", children: _jsx(PageGridLayout, { pageKey: "sb/widths", defaultLayouts: {
48
- lg: [
49
- { i: 'w2', x: 0, y: 0, w: 2, h: 4 },
50
- { i: 'w3', x: 2, y: 0, w: 3, h: 4 },
51
- { i: 'w4', x: 5, y: 0, w: 4, h: 4 },
52
- { i: 'w6', x: 0, y: 4, w: 6, h: 4 },
53
- { i: 'w12', x: 0, y: 8, w: 12, h: 4 },
54
- ],
55
- }, widgets: {
56
- w2: _jsx(AppCard, { title: "w=2" }),
57
- w3: _jsx(AppCard, { title: "w=3" }),
58
- w4: _jsx(AppCard, { title: "w=4" }),
59
- w6: _jsx(AppCard, { title: "w=6 \u2014 the new default" }),
60
- w12: _jsx(AppCard, { title: "w=12 \u2014 the old default, one card per row" }),
61
- } }) })),
62
- };
63
- /**
64
- * Card headings. `text-gradient-accent` is on 183 of 235 crm7 h1 page titles
65
- * and 0 of 310 crm7 h2/h3 CARD headings — the element the operator keeps
66
- * asking about. The pairing matters: the solid `text-(--role-text-heading)`
67
- * sits UNDER the gradient as the fallback layer.
68
- *
69
- * `width: fit-content` inside the utility is load-bearing. Anything that
70
- * overrides width makes a short heading sample only the first ~15% of the
71
- * gradient and render flat — indistinguishable from having no gradient at all,
72
- * which is why "is the class present" is not a check.
73
- */
74
- export const CardHeadingGradient = {
75
- name: 'Card headings — with and without the gradient',
76
- render: () => (_jsxs("div", { className: "grid gap-8 p-8 md:grid-cols-2", children: [_jsxs("div", { className: "rounded-[var(--radius-card,1.5rem)] border border-border bg-card p-6", children: [_jsx("h3", { className: "mb-2 text-lg font-semibold text-(--role-text-heading)", children: "Quick Actions \u2014 flat (today, 0 of 310)" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "No gradient." })] }), _jsxs("div", { className: "rounded-[var(--radius-card,1.5rem)] border border-border bg-card p-6", children: [_jsx("h3", { className: "text-gradient-accent mb-2 text-lg font-semibold text-(--role-text-heading)", children: "Quick Actions \u2014 gradient (G2 target)" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "`text-gradient-accent` over the solid heading token." })] })] })),
77
- };
78
- /* ------------------------------------------------------------------ *
79
- * OPERATOR-EDITABLE CARD APPEARANCE
80
- *
81
- * The border, corners, shadow and padding are set by the operator in the
82
- * canvas editor and persisted per page (see cardStyle.ts). These stories are
83
- * the reference for what each setting actually paints, so a change to the
84
- * chrome can be diffed against a picture rather than argued about.
85
- *
86
- * `toCssVars` returns ONLY the properties that differ from the default, so
87
- * `Default` below emits no custom properties at all — it is the control, and
88
- * it must look exactly like `BareContentOptsIn`. If those two ever diverge,
89
- * the "costs nothing until you touch it" guarantee has been broken.
90
- * ------------------------------------------------------------------ */
91
- /** Renders one slot with chrome on, under a given operator card style. */
92
- function StyledSurface({ style, note }) {
93
- const resolved = { ...DEFAULT_CARD_STYLE, ...style };
94
- const vars = toCssVars(resolved);
95
- return (_jsxs("div", { className: "p-8", style: {
96
- ...vars,
97
- // The editor applies elevation and padding as real declarations,
98
- // because each has to beat a class. Mirrored here so the story shows
99
- // what the page shows.
100
- ...('--card-shadow' in vars ? { ['--story-shadow']: vars['--card-shadow'] } : null),
101
- }, children: [_jsx("p", { className: "mb-3 text-sm text-muted-foreground", children: note }), _jsx("p", { className: "mb-3 text-sm font-medium text-foreground", children: describeCardStyle(resolved) }), _jsx(PageGridLayout, { pageKey: `card-style-${note.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}`, defaultLayouts: oneSlot, itemChrome: true, widgets: { a: _jsx(BareContent, { title: "Operator-styled card" }) } })] }));
102
- }
103
- export const CardStyleDefault = {
104
- name: 'Card style — default (emits no CSS)',
105
- render: () => (_jsx(StyledSurface, { style: {}, note: "The control. No custom properties are emitted at all." })),
106
- };
107
- export const CardStyleThickBorder = {
108
- name: 'Card style — 4px border',
109
- render: () => _jsx(StyledSurface, { style: { borderWidth: 4 }, note: "Border width is a slider, 0-8px." }),
110
- };
111
- export const CardStyleDashedAccent = {
112
- name: 'Card style — dashed accent border',
113
- render: () => (_jsx(StyledSurface, { style: { borderWidth: 2, borderStyle: 'dashed', borderTone: 'accent' }, note: "Colour comes from a THEME TOKEN, never a literal, so white-labelling still applies." })),
114
- };
115
- export const CardStyleSquare = {
116
- name: 'Card style — square corners',
117
- render: () => _jsx(StyledSurface, { style: { radius: 0 }, note: "Corner radius is a slider, 0-48px." }),
118
- };
119
- export const CardStyleBorderless = {
120
- name: 'Card style — no border',
121
- render: () => (_jsx(StyledSurface, { style: { borderStyle: 'none' }, note: "A borderless card still keeps its background and radius." })),
122
- };
123
- export const CardStyleElevationLadder = {
124
- name: 'Card style — elevation 0 to 4',
125
- render: () => (_jsx("div", { className: "space-y-2", children: [0, 1, 2, 3, 4].map((level) => (_jsx(StyledSurface, { style: { elevation: level }, note: `--shadow-elev-${level}, from @bsuite/theme.` }, level))) })),
126
- };