@bsuite/page-builder 1.0.6 → 1.1.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,7 +2,14 @@ import type { ReactNode } from 'react';
2
2
  export interface CanvasCardProps {
3
3
  /** Stable widget key used by PageGridLayout for layout persistence. Required. */
4
4
  cardKey: string;
5
- /** Default grid width in columns (12-col grid). Defaults to 12. */
5
+ /**
6
+ * Grid width in columns on the 12-column grid.
7
+ *
8
+ * Defaults to **6** — half the grid, two cards per row. It defaulted to 12
9
+ * up to 1.0.7, which meant every card that omitted `w` rendered full width
10
+ * and stacked; 1,068 of 1,729 usages across the estate omit it. Say `w={12}`
11
+ * when a card genuinely wants the full row.
12
+ */
6
13
  w?: 2 | 3 | 4 | 6 | 8 | 12;
7
14
  /** Minimum width in columns when the user resizes. Defaults to 4. */
8
15
  minW?: 2 | 3 | 4 | 6 | 8 | 12;
@@ -1 +1 @@
1
- {"version":3,"file":"CanvasCard.d.ts","sourceRoot":"","sources":["../src/CanvasCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;IAC3B,qEAAqE;IACrE,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;IAC9B,4FAA4F;IAC5F,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;;;OAgBG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,kEAAkE;IAClE,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAExD;yBAFe,UAAU"}
1
+ {"version":3,"file":"CanvasCard.d.ts","sourceRoot":"","sources":["../src/CanvasCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;IAC3B,qEAAqE;IACrE,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;IAC9B,4FAA4F;IAC5F,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;;;OAgBG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,kEAAkE;IAClE,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAExD;yBAFe,UAAU"}
@@ -105,6 +105,14 @@ export type DraggableCardPageProps = Omit<PageGridLayoutProps, 'widgets' | 'defa
105
105
  * the element it toggles was never mounted.
106
106
  */
107
107
  onDroppedChildren?: (dropped: string[], pageKey: string) => void;
108
+ /**
109
+ * Width, in grid columns, for cards that do not set `w` themselves.
110
+ *
111
+ * Defaults to `DEFAULT_CANVAS_CARD_WIDTH` (6 — half the grid, two cards per
112
+ * row) as of 1.1.0. Pass 12 to restore the pre-1.1.0 full-width stack while
113
+ * an app migrates its pages.
114
+ */
115
+ defaultCardWidth?: number;
108
116
  /**
109
117
  * CanvasCard children. Falsy entries are ignored. Fragments are flattened so
110
118
  * a conditional multi-card branch still registers each CanvasCard.
@@ -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;;;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,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"}
@@ -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, 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, 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":"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,qBAwzBrB"}
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;AA4W7F,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,qBAyzBrB"}
@@ -84,7 +84,7 @@ const DEFAULT_ADD_RELATIONSHIP_WIDGET_EVENT_NAMES = [
84
84
  const RGLResizeHandle = React.forwardRef(function RGLResizeHandle({ axis = 'se', className: injectedClassName, ...rest }, ref) {
85
85
  return (_jsx("span", { ref: ref, "data-no-drag": true, className: `react-resizable-handle react-resizable-handle-${axis}${injectedClassName ? ' ' + injectedClassName : ''}`, ...rest }));
86
86
  });
87
- const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, isEditing, label, onHide, autoHeight, onAutoHeightChange, children: injectedChildren, className: injectedClassName, style: injectedStyle, ...rest }, ref) {
87
+ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, isEditing, label, onHide, autoHeight, onAutoHeightChange, chrome = false, children: injectedChildren, className: injectedClassName, style: injectedStyle, ...rest }, ref) {
88
88
  // Auto-height measurement (blueprint amendment A1). `measureRef` wraps
89
89
  // `content` with NO height constraint of its own — its parent has
90
90
  // `overflow-hidden` + `flex-1 min-h-0` (clips *painting* to the current
@@ -214,7 +214,18 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
214
214
  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) => {
215
215
  event.stopPropagation();
216
216
  onHide(id);
217
- }, title: `Hide ${label}`, "aria-label": `Hide ${label}`, children: _jsx(EyeOff, { className: "h-3 w-3", "aria-hidden": "true" }) })), _jsx("div", { className: "h-full w-full rounded-3xl transition-all flex flex-col bg-card border border-border shadow-sm dark:shadow-[var(--glow-card,none)]", style: { contain: 'layout style' }, children: _jsx("div", { className: cn('flex-1 min-h-0', autoHeight ? 'overflow-hidden' : 'overflow-auto'), children: autoHeight ? _jsx("div", { ref: measureRef, children: content }) : content }) })] }), injectedChildren] }));
217
+ }, 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', className: chrome
218
+ ? // ONE radius token, read by the grid item AND available to any
219
+ // app card that opts into the same surface. Two surfaces
220
+ // reading the SAME var is what makes their corners align, and
221
+ // it is also what keeps the radius tenant-configurable —
222
+ // a literal here would align them and lock white-label out.
223
+ // `--radius-card` defaults to today's value (1.5rem =
224
+ // rounded-3xl), so this is byte-identical until a tenant sets
225
+ // one.
226
+ 'h-full w-full rounded-[var(--radius-card,1.5rem)] transition-all flex flex-col bg-card border border-border shadow-sm dark:shadow-[var(--glow-card,none)]'
227
+ : // Chrome OFF: layout only. Identical box, no paint.
228
+ 'h-full w-full transition-all flex flex-col', style: { contain: 'layout style' }, children: _jsx("div", { className: cn('flex-1 min-h-0', autoHeight ? 'overflow-hidden' : 'overflow-auto'), children: autoHeight ? _jsx("div", { ref: measureRef, children: content }) : content }) })] }), injectedChildren] }));
218
229
  }));
219
230
  export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, widgets, widgetMeta, className,
220
231
  // Resize default flipped 2026-05-12: rgl v2 dropped auto-handle rendering
@@ -222,7 +233,12 @@ export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVer
222
233
  // adapters then defaulted it back to true. With v2's handle gap fixed,
223
234
  // the safe default is true so any consumer who forgets to set it still
224
235
  // gets a usable canvas. Opt out per-page with isResizable={false}.
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, }) {
236
+ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, tenantId, defaultAutoHeight,
237
+ // CHROME OFF BY DEFAULT — the inversion. See `GridItemProps.chrome`.
238
+ // An app that is not ready to migrate its own card surfaces passes
239
+ // `itemChrome` to get the pre-1.1.0 behaviour back for every slot, and
240
+ // migrates page by page with the per-item `chrome` flag.
241
+ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_NAMES, createEntityWidget, onRegisterEntityWidget, relationshipCatalog, addRelationshipWidgetEventNames = DEFAULT_ADD_RELATIONSHIP_WIDGET_EVENT_NAMES, createRelationshipWidget, onRegisterRelationshipWidget, onRelationshipWidgetRejected, }) {
226
242
  const { currentLayouts, layoutCols, isEditing, setIsEditing, activeCols, activeCompactor, onLayoutChange, handleColumnChange, handleBreakpointChange, handleCompact, handleReset, addWidget, moveWidget, setWidgetLocked, applyAutoHeightRows, autoHeightRows, resetConfirmOpen, setResetConfirmOpen, containerRef, containerWidth, } = usePageGridLayout({
227
243
  pageKey,
228
244
  defaultLayouts,
@@ -693,6 +709,6 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, tenantId, defaultAut
693
709
  const content = allWidgets[layoutItem.i];
694
710
  if (content === undefined || content === null)
695
711
  return null;
696
- return (_jsx(GridItem, { id: layoutItem.i, content: content, isEditing: isEditing, label: layerNames[layoutItem.i] || widgetMeta?.[layoutItem.i]?.label || layoutItem.i, onHide: hideLayer, autoHeight: layoutItem.autoHeight, onAutoHeightChange: handleAutoHeightChange }, layoutItem.i));
712
+ return (_jsx(GridItem, { id: layoutItem.i, content: content, isEditing: isEditing, label: layerNames[layoutItem.i] || widgetMeta?.[layoutItem.i]?.label || layoutItem.i, onHide: hideLayer, autoHeight: layoutItem.autoHeight, onAutoHeightChange: handleAutoHeightChange, chrome: layoutItem.chrome ?? itemChrome }, layoutItem.i));
697
713
  }) }) }) })] }));
698
714
  }
@@ -12,6 +12,26 @@ import type { CanvasCardProps } from './CanvasCard.js';
12
12
  import type { GridLayouts } from './types.js';
13
13
  /** The 12-column grid every BSuite page canvas is laid out against. */
14
14
  export declare const CANVAS_GRID_COLUMNS = 12;
15
+ /**
16
+ * The width a `CanvasCard` gets when its author does not say.
17
+ *
18
+ * SIX, i.e. half the grid — changed from twelve in 1.1.0. Twelve on a
19
+ * twelve-column grid means every card that omits `w` renders full width and
20
+ * the page becomes a single vertical stack. Measured across the estate on
21
+ * 2026-08-25: **1,068 of 1,729 CanvasCard usages omit `w`** (BSU and conduit
22
+ * omit it 100% of the time), so 62% of every card surface in BSuite was
23
+ * stacked by a DEFAULT, not by a layout decision. That is the operator's
24
+ * long-standing "the cards don't use the available space" report, and it is
25
+ * one number in one file rather than 1,068 page edits.
26
+ *
27
+ * Six rather than four: at a 1680px container half the grid is still ~820px,
28
+ * which comfortably holds a data table. Four would have traded a stacking
29
+ * complaint for a cramping one.
30
+ *
31
+ * A page that genuinely wants a full-width card still says `w={12}` — an
32
+ * explicit width always wins, and 661 usages already do exactly that.
33
+ */
34
+ export declare const DEFAULT_CANVAS_CARD_WIDTH = 6;
15
35
  export declare function isCanvasCardElement(node: ReactNode): node is ReactElement<CanvasCardProps>;
16
36
  /**
17
37
  * Best-effort display name for a node that is about to be discarded.
@@ -58,5 +78,7 @@ export interface CanvasCardLayoutResult {
58
78
  * item. (This is the fact that disproves the "dynamic card lists cannot be
59
79
  * split into static widget keys" objection recorded in several app ledgers.)
60
80
  */
61
- export declare function buildCanvasCardLayout(children: ReactNode): CanvasCardLayoutResult;
81
+ export declare function buildCanvasCardLayout(children: ReactNode, options?: {
82
+ defaultWidth?: number;
83
+ }): CanvasCardLayoutResult;
62
84
  //# sourceMappingURL=canvasCardLayout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"canvasCardLayout.d.ts","sourceRoot":"","sources":["../src/canvasCardLayout.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,SAAS,GACd,IAAI,IAAI,YAAY,CAAC,eAAe,CAAC,CAKvC;AAQD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAQpD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAG;IACpD,KAAK,EAAE,YAAY,CAAC,eAAe,CAAC,EAAE,CAAC;IACvC,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAqBA;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAI,EAAE,GAAG,SAAsB,UAE7E;AAED,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnC,yEAAyE;IACzE,OAAO,EAAE,WAAW,CAAC;IACrB,+EAA+E;IAC/E,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,SAAS,GAClB,sBAAsB,CA0CxB"}
1
+ {"version":3,"file":"canvasCardLayout.d.ts","sourceRoot":"","sources":["../src/canvasCardLayout.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,SAAS,GACd,IAAI,IAAI,YAAY,CAAC,eAAe,CAAC,CAKvC;AAQD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAQpD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAG;IACpD,KAAK,EAAE,YAAY,CAAC,eAAe,CAAC,EAAE,CAAC;IACvC,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAqBA;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAI,EAAE,GAAG,SAAsB,UAE7E;AAED,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnC,yEAAyE;IACzE,OAAO,EAAE,WAAW,CAAC;IACrB,+EAA+E;IAC/E,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,SAAS,EACnB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAO,GACtC,sBAAsB,CA2CxB"}
@@ -11,6 +11,26 @@ import { jsx as _jsx } from "react/jsx-runtime";
11
11
  import { Children, Fragment, isValidElement, } from 'react';
12
12
  /** The 12-column grid every BSuite page canvas is laid out against. */
13
13
  export const CANVAS_GRID_COLUMNS = 12;
14
+ /**
15
+ * The width a `CanvasCard` gets when its author does not say.
16
+ *
17
+ * SIX, i.e. half the grid — changed from twelve in 1.1.0. Twelve on a
18
+ * twelve-column grid means every card that omits `w` renders full width and
19
+ * the page becomes a single vertical stack. Measured across the estate on
20
+ * 2026-08-25: **1,068 of 1,729 CanvasCard usages omit `w`** (BSU and conduit
21
+ * omit it 100% of the time), so 62% of every card surface in BSuite was
22
+ * stacked by a DEFAULT, not by a layout decision. That is the operator's
23
+ * long-standing "the cards don't use the available space" report, and it is
24
+ * one number in one file rather than 1,068 page edits.
25
+ *
26
+ * Six rather than four: at a 1680px container half the grid is still ~820px,
27
+ * which comfortably holds a data table. Four would have traded a stacking
28
+ * complaint for a cramping one.
29
+ *
30
+ * A page that genuinely wants a full-width card still says `w={12}` — an
31
+ * explicit width always wins, and 661 usages already do exactly that.
32
+ */
33
+ export const DEFAULT_CANVAS_CARD_WIDTH = 6;
14
34
  export function isCanvasCardElement(node) {
15
35
  return (isValidElement(node) &&
16
36
  node.type.displayName === 'CanvasCard');
@@ -84,7 +104,8 @@ export function clampColumns(value, min = 1, max = CANVAS_GRID_COLUMNS) {
84
104
  * item. (This is the fact that disproves the "dynamic card lists cannot be
85
105
  * split into static widget keys" objection recorded in several app ledgers.)
86
106
  */
87
- export function buildCanvasCardLayout(children) {
107
+ export function buildCanvasCardLayout(children, options = {}) {
108
+ const defaultWidth = clampColumns(options.defaultWidth ?? DEFAULT_CANVAS_CARD_WIDTH);
88
109
  const widgets = {};
89
110
  const lg = [];
90
111
  let x = 0;
@@ -96,7 +117,7 @@ export function buildCanvasCardLayout(children) {
96
117
  // a card explicitly opts out for a virtualized list. See CanvasCardProps.
97
118
  const { cardKey, h = 6, minH = 2, minW = 4, autoHeight = true, children: body, } = child.props;
98
119
  const normalizedMinW = clampColumns(minW);
99
- const width = clampColumns(child.props.w ?? CANVAS_GRID_COLUMNS, normalizedMinW);
120
+ const width = clampColumns(child.props.w ?? defaultWidth, normalizedMinW);
100
121
  if (x + width > CANVAS_GRID_COLUMNS) {
101
122
  y += rowHeight;
102
123
  x = 0;
@@ -0,0 +1,62 @@
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 1.1.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 1.1.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
+ //# sourceMappingURL=cardSurfaces.stories.d.ts.map
@@ -0,0 +1 @@
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;AAItC;;;;;;;;;;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"}
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PageGridLayout } from './PageGridLayout.js';
3
+ export default {
4
+ title: 'Page Builder/Card surfaces',
5
+ parameters: { layout: 'fullscreen' },
6
+ };
7
+ /** What an app renders inside a slot: its own card. Border, radius, background. */
8
+ function AppCard({ title, children }) {
9
+ 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 })] }));
10
+ }
11
+ /** Bare content: no surface of its own. This is the 10% that NEEDS the chrome. */
12
+ function BareContent({ title }) {
13
+ 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." })] }));
14
+ }
15
+ const oneSlot = { lg: [{ i: 'a', x: 0, y: 0, w: 6, h: 6 }] };
16
+ /**
17
+ * THE DEFECT, exactly as it shipped. A card inside the grid item's card:
18
+ * two radii, two 1px borders, two backgrounds. 316+ files render this.
19
+ */
20
+ export const NestedCardDoubleFrame = {
21
+ name: 'Nested card — DOUBLE FRAME (the defect)',
22
+ 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." }) } }) })),
23
+ };
24
+ /** The same content on 1.1.0 defaults: ONE border, ONE radius. */
25
+ export const NestedCardSingleFrame = {
26
+ name: 'Nested card — ONE frame (1.1.0 default)',
27
+ 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." }) } }) })),
28
+ };
29
+ /** Bare content with chrome OFF — the regression the inversion could cause. */
30
+ export const BareContentNoChrome = {
31
+ name: 'Bare content, chrome OFF — the regression to watch for',
32
+ render: () => (_jsx("div", { className: "p-8", children: _jsx(PageGridLayout, { pageKey: "sb/bare-off", defaultLayouts: oneSlot, widgets: { a: _jsx(BareContent, { title: "Pipeline Overview" }) } }) })),
33
+ };
34
+ /** The same bare content opting IN per item. This is the migration for the 10%. */
35
+ export const BareContentOptsIn = {
36
+ name: 'Bare content, per-item chrome ON — the migration',
37
+ 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" }) } }) })),
38
+ };
39
+ /**
40
+ * Every card width side by side. `w` defaulted to 12, so 1,068 of 1,729 usages
41
+ * rendered like the last row here — one card per row, the rest of the screen
42
+ * empty. That is the "cards don't use the available space" report.
43
+ */
44
+ export const WidthLadder = {
45
+ name: 'Width ladder — w = 2 / 3 / 4 / 6 / 12',
46
+ render: () => (_jsx("div", { className: "p-8", children: _jsx(PageGridLayout, { pageKey: "sb/widths", defaultLayouts: {
47
+ lg: [
48
+ { i: 'w2', x: 0, y: 0, w: 2, h: 4 },
49
+ { i: 'w3', x: 2, y: 0, w: 3, h: 4 },
50
+ { i: 'w4', x: 5, y: 0, w: 4, h: 4 },
51
+ { i: 'w6', x: 0, y: 4, w: 6, h: 4 },
52
+ { i: 'w12', x: 0, y: 8, w: 12, h: 4 },
53
+ ],
54
+ }, widgets: {
55
+ w2: _jsx(AppCard, { title: "w=2" }),
56
+ w3: _jsx(AppCard, { title: "w=3" }),
57
+ w4: _jsx(AppCard, { title: "w=4" }),
58
+ w6: _jsx(AppCard, { title: "w=6 \u2014 the new default" }),
59
+ w12: _jsx(AppCard, { title: "w=12 \u2014 the old default, one card per row" }),
60
+ } }) })),
61
+ };
62
+ /**
63
+ * Card headings. `text-gradient-accent` is on 183 of 235 crm7 h1 page titles
64
+ * and 0 of 310 crm7 h2/h3 CARD headings — the element the operator keeps
65
+ * asking about. The pairing matters: the solid `text-(--role-text-heading)`
66
+ * sits UNDER the gradient as the fallback layer.
67
+ *
68
+ * `width: fit-content` inside the utility is load-bearing. Anything that
69
+ * overrides width makes a short heading sample only the first ~15% of the
70
+ * gradient and render flat — indistinguishable from having no gradient at all,
71
+ * which is why "is the class present" is not a check.
72
+ */
73
+ export const CardHeadingGradient = {
74
+ name: 'Card headings — with and without the gradient',
75
+ 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." })] })] })),
76
+ };
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { CanvasCard } from './CanvasCard.js';
4
4
  export type { CanvasCardProps } from './CanvasCard.js';
5
5
  export { DraggableCardPage } from './DraggableCardPage.js';
6
6
  export type { DraggableCardPageProps } from './DraggableCardPage.js';
7
- export { buildCanvasCardLayout, flattenCanvasCards, isCanvasCardElement, describeNode, clampColumns, CANVAS_GRID_COLUMNS, } from './canvasCardLayout.js';
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
10
  export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.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;AAC7C,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,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"}
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;AAC7C,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"}
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ export { PageGridLayout } from './PageGridLayout.js';
2
2
  export { PageEditorLauncher } from './PageEditorLauncher.js';
3
3
  export { CanvasCard } from './CanvasCard.js';
4
4
  export { DraggableCardPage } from './DraggableCardPage.js';
5
- export { buildCanvasCardLayout, flattenCanvasCards, isCanvasCardElement, describeNode, clampColumns, CANVAS_GRID_COLUMNS, } from './canvasCardLayout.js';
5
+ export { buildCanvasCardLayout, DEFAULT_CANVAS_CARD_WIDTH, flattenCanvasCards, isCanvasCardElement, describeNode, clampColumns, CANVAS_GRID_COLUMNS, } from './canvasCardLayout.js';
6
6
  export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.js';
7
7
  export { rescaleLayout } from './rescaleLayout.js';
8
8
  export { computeAutoHeightRows } from './autoHeight.js';
@@ -46,7 +46,20 @@ export type CardSurfaceIdiom =
46
46
  /** autoHeight opt-out on a card that is not a virtualized list. */
47
47
  | 'autoheight-optout'
48
48
  /** autoHeight={false} with a seed height so small the card must clip. */
49
- | 'clipped-card';
49
+ | 'clipped-card'
50
+ /**
51
+ * V-C5. A grid item whose child re-declares card chrome — border + radius +
52
+ * background — inside the chrome the grid item already paints. 28px inside
53
+ * 24px, two 1px borders. This is the idiom V-C4 (`glued-widget`) cannot see:
54
+ * ONE card in ONE slot is not glue, and it is still a double frame.
55
+ */
56
+ | 'nested-chrome'
57
+ /**
58
+ * The file could not be parsed with confidence. NOT a clean verdict. A gate
59
+ * that cannot tell "checked nothing" from "found nothing" is not a gate
60
+ * (D-92), so an unparseable file gets its own idiom and its own exit path.
61
+ */
62
+ | 'unparseable';
50
63
  export interface CardSurfaceFinding {
51
64
  file: string;
52
65
  idiom: CardSurfaceIdiom;
@@ -95,6 +108,36 @@ export interface CardSurfaceScannerConfig {
95
108
  autoHeightEscapeHatch?: RegExp;
96
109
  /** File extensions to scan. Defaults to .tsx. */
97
110
  extensions?: string[];
111
+ /**
112
+ * Extra component names that count as CARD CHROME when they appear inside a
113
+ * grid slot — a component that paints its own border + radius + background.
114
+ * `cardTags` is always included; this is for app-local chrome components the
115
+ * glue check does not need (BSU `MagicCard`, conduit `CounterCard`, …).
116
+ */
117
+ nestedChromeTags?: string[];
118
+ /** file -> reason. A nested-chrome file listed here is allowed. */
119
+ nestedChromeExclusions?: Record<string, string>;
120
+ /**
121
+ * Does the GRID ITEM paint card chrome in the version this app resolves?
122
+ *
123
+ * TRUE for `@bsuite/page-builder` <= 1.0.7, where every grid item painted
124
+ * `rounded-3xl bg-card border border-border` unconditionally and there was
125
+ * no way to turn it off. On those versions a nested card is ALWAYS a double
126
+ * frame, so every nesting slot is a finding.
127
+ *
128
+ * FALSE from 1.1.0, where chrome is opt-in. A nested card then becomes the
129
+ * CORRECT shape — it is the only surface — and the defect narrows to a slot
130
+ * that opts back INTO chrome (`itemChrome`, or `chrome` on the item or the
131
+ * CanvasCard) AND still nests a card.
132
+ *
133
+ * DEFAULTS TO TRUE, i.e. it assumes the pre-inversion package until an app
134
+ * says otherwise. A shared-package fix that four consumers receive and one
135
+ * does not is this estate's recorded failure mode (conduit sat on a
136
+ * minor-locked `^0.6.3` while the fix shipped in 0.8.0, and nothing
137
+ * reported it), so each app must DECLARE which side of the inversion it is
138
+ * on rather than inherit an optimistic default.
139
+ */
140
+ gridItemPaintsChrome?: boolean;
98
141
  }
99
142
  export interface CardSurfaceScanResult {
100
143
  findings: CardSurfaceFinding[];
@@ -103,6 +146,14 @@ export interface CardSurfaceScanResult {
103
146
  scanRootsResolved: string[];
104
147
  /** Exclusion-ledger keys that no longer match any finding — stale entries. */
105
148
  staleExclusions: string[];
149
+ /**
150
+ * Files the scanner could NOT parse with confidence. These are UNKNOWN, not
151
+ * clean. A caller that treats an empty `findings` array as a pass while this
152
+ * array is non-empty has reproduced the exact defect D-92 names.
153
+ */
154
+ unknownFiles: string[];
155
+ /** Distinct files carrying at least one `nested-chrome` finding. */
156
+ nestedChromeFiles: string[];
106
157
  /** Human-readable account of how the verdict was reached. */
107
158
  summary: string;
108
159
  }
@@ -116,5 +167,54 @@ export interface CardSurfaceScanResult {
116
167
  * skipped entirely. Stripping first fixes both.
117
168
  */
118
169
  export declare function stripComments(source: string): string;
170
+ /**
171
+ * Every opening tag in a JSX body, with its raw attribute text.
172
+ *
173
+ * Hand-rolled rather than regex-only because an attribute value can contain
174
+ * `>` inside a string, a template literal or a `{…}` expression — three of the
175
+ * five hand-rolled copies this module replaced were defeated by exactly that.
176
+ */
177
+ export declare function openingTags(body: string): {
178
+ tag: string;
179
+ attrs: string;
180
+ }[];
181
+ /**
182
+ * Does this element paint its OWN card chrome?
183
+ *
184
+ * Chrome means all three of border, radius and background, because that is
185
+ * exactly what `PageGridLayout` already paints on the slot. Two of the three is
186
+ * a tinted panel or a divider, not a second card — requiring all three is what
187
+ * separates BSU's `glass-card rounded-2xl border-primary/30` (a real double
188
+ * frame) from a `rounded-lg bg-muted` inline chip (not one).
189
+ *
190
+ * Chrome declared through `style={{ background, border, borderRadius }}` counts
191
+ * the same as chrome declared through classes. BSU's `Settings.tsx` is chrome
192
+ * entirely in inline style; a class-only detector calls that file clean, which
193
+ * is how a nested card hides from a gate that only reads Tailwind.
194
+ */
195
+ export declare function elementDeclaresCardChrome(el: {
196
+ tag: string;
197
+ attrs: string;
198
+ }, chromeTags: Set<string>): string | null;
199
+ /**
200
+ * Does this grid slot paint chrome of its OWN, post-inversion?
201
+ *
202
+ * Three ways to opt in, and all three have to be visible to the scanner or a
203
+ * re-chromed slot goes unnoticed: `itemChrome` on the grid component (whole
204
+ * app/page), `chrome: true` on the layout item, and `chrome` on the CanvasCard.
205
+ */
206
+ export declare function slotOptsIntoChrome(fileSource: string, slotBody: string): boolean;
207
+ /** The first nested-chrome element inside one grid slot, or null. */
208
+ export declare function findNestedChrome(slotBody: string, config: Pick<CardSurfaceScannerConfig, 'cardTags' | 'nestedChromeTags'>): string | null;
209
+ /**
210
+ * Can this file be parsed with confidence?
211
+ *
212
+ * Returns a reason when it CANNOT. Every `<CanvasCard` must pair with a
213
+ * `</CanvasCard>` the block regex can reach, and every `widgets={{` must close
214
+ * before EOF. A file that fails either is UNKNOWN — it is not reported clean,
215
+ * and it is not silently skipped. "Checked nothing" and "found nothing" do not
216
+ * share an exit code here.
217
+ */
218
+ export declare function parseConfidence(src: string): string | null;
119
219
  export declare function scanCardSurfaces(config: CardSurfaceScannerConfig): CardSurfaceScanResult;
120
220
  //# sourceMappingURL=cardSurfaceScanner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cardSurfaceScanner.d.ts","sourceRoot":"","sources":["../../src/scanner/cardSurfaceScanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAKH,MAAM,MAAM,gBAAgB;AAC1B,2DAA2D;AACzD,cAAc;AAChB,6EAA6E;GAC3E,sBAAsB;AACxB,yEAAyE;GACvE,mBAAmB;AACrB,mEAAmE;GACjE,mBAAmB;AACrB,yEAAyE;GACvE,cAAc,CAAC;AAEnB,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,sDAAsD;IACtD,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,mFAAmF;IACnF,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,6DAA6D;IAC7D,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,2EAA2E;IAC3E,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,8EAA8E;IAC9E,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;CACjB;AASD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKpD;AAwHD,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,wBAAwB,GAC/B,qBAAqB,CAwIvB"}
1
+ {"version":3,"file":"cardSurfaceScanner.d.ts","sourceRoot":"","sources":["../../src/scanner/cardSurfaceScanner.ts"],"names":[],"mappings":"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"}
@@ -55,8 +55,17 @@ const DEFAULT_GRID_PRIMITIVES = [
55
55
  */
56
56
  export function stripComments(source) {
57
57
  return source
58
- .replace(/\{\s*\/\*[\s\S]*?\*\/\s*\}/g, '') // {/* JSX comment */}
59
- .replace(/\/\*[\s\S]*?\*\//g, '') // /* block */
58
+ // {/* JSX comment */} — TEMPERED so the match cannot span a `*​/` it did
59
+ // not open. The naive lazy form `\{\s*\/\*[\s\S]*?\*\/\s*\}` backtracks
60
+ // past the comment's own terminator whenever the next character is not
61
+ // `}` — e.g. an ordinary block comment sitting at the head of an object
62
+ // literal — and then matches the NEXT `*​/}` anywhere later in the file,
63
+ // DELETING every line between. crm7 `src/pages/people/new/worker.tsx` lost
64
+ // 37 lines and two whole `<CanvasCard>` blocks to exactly that, which the
65
+ // UNKNOWN gate below caught as unbalanced tags. Code silently deleted
66
+ // before matching is a scanner that under-reports and calls it clean.
67
+ .replace(/\{\s*\/\*(?:(?!\*\/)[\s\S])*\*\/\s*\}/g, '')
68
+ .replace(/\/\*(?:(?!\*\/)[\s\S])*\*\//g, '') // /* block */
60
69
  .replace(/(^|[^:])\/\/[^\n]*/g, '$1'); // // line, but not http://
61
70
  }
62
71
  function tagPattern(tags) {
@@ -159,6 +168,293 @@ function extractWidgetEntries(source) {
159
168
  }
160
169
  return out;
161
170
  }
171
+ /* ==========================================================================
172
+ * V-C5 — NESTED CHROME
173
+ *
174
+ * `PageGridLayout` paints EVERY grid item as a card:
175
+ *
176
+ * h-full w-full rounded-3xl transition-all flex flex-col
177
+ * bg-card border border-border shadow-sm dark:shadow-[var(--glow-card,none)]
178
+ *
179
+ * When the slot's own content re-declares that surface — border + radius +
180
+ * background — the user sees 28px inside 24px and two 1px borders. V-C4
181
+ * (`glued-widget`) cannot see this: ONE card in ONE slot is not glue, and it
182
+ * is still a double frame. That is why 316 files across the estate render a
183
+ * card inside a card and every existing gate reads green.
184
+ * ========================================================================== */
185
+ /** Components that paint their own card chrome. */
186
+ const DEFAULT_NESTED_CHROME_TAGS = [
187
+ 'Card',
188
+ 'MagicCard',
189
+ 'GlassCard',
190
+ 'NeonCard',
191
+ 'StatCard',
192
+ 'SummaryCard',
193
+ 'ServiceCard',
194
+ 'CounterCard',
195
+ 'MetricCard',
196
+ 'InfoCard',
197
+ ];
198
+ /**
199
+ * Elements that are CONTROLS, not surfaces.
200
+ *
201
+ * A bordered, rounded, tinted `<button>` is a button. A `<Badge>` is a pill.
202
+ * Counting them as nested chrome is how a detector inflates: throughput's
203
+ * `Teams.tsx` carries `border … rounded … text-…` on a filter button, which is
204
+ * correct styling and not a second card. Chrome on a control is never the
205
+ * double-frame defect, so controls are excluded BY TAG rather than by tuning
206
+ * the class thresholds until the number looks right.
207
+ */
208
+ const CONTROL_TAGS = new Set([
209
+ 'button',
210
+ 'a',
211
+ 'input',
212
+ 'select',
213
+ 'textarea',
214
+ 'label',
215
+ 'summary',
216
+ 'Button',
217
+ 'Input',
218
+ 'Select',
219
+ 'SelectTrigger',
220
+ 'SelectContent',
221
+ 'Textarea',
222
+ 'Badge',
223
+ 'Avatar',
224
+ 'AvatarFallback',
225
+ 'Switch',
226
+ 'Checkbox',
227
+ 'Progress',
228
+ 'Slider',
229
+ 'Toggle',
230
+ 'ToggleGroup',
231
+ 'ToggleGroupItem',
232
+ 'TabsList',
233
+ 'TabsTrigger',
234
+ 'Skeleton',
235
+ 'Tooltip',
236
+ 'TooltipContent',
237
+ 'DropdownMenuContent',
238
+ 'PopoverContent',
239
+ ]);
240
+ const RADIUS_CLASS = /(?:^|[\s'"`])rounded(?:-[a-z0-9[\]().\-/]+)?(?=$|[\s'"`])/;
241
+ const BORDER_CLASS = /(?:^|[\s'"`])border(?:-[a-z0-9[\]().\-/]+)?(?=$|[\s'"`])/;
242
+ const BG_CLASS = /(?:^|[\s'"`])(?:bg-[a-z0-9[\]().\-/]+|glass-card|neon-card|card-surface)(?=$|[\s'"`])/;
243
+ /**
244
+ * Composite classes that are a WHOLE card surface on their own.
245
+ *
246
+ * BSU's `.glass-card` (business-suite-unified/src/index.css:760) declares
247
+ * `background-color` AND `border: 1px solid` AND a shadow. `glass-card
248
+ * rounded-xl` is therefore already border + radius + background — the full
249
+ * double frame — while carrying no `border-*` utility for a class matcher to
250
+ * find. Requiring three separate Tailwind utilities makes every composite-class
251
+ * card invisible; BSU `Government.tsx` is exactly that shape.
252
+ */
253
+ const COMPOSITE_CARD_CLASS = /(?:^|[\s'"`])(?:glass-card|neon-card|card-surface)(?=$|[\s'"`])/;
254
+ const RADIUS_STYLE = /\bborder-?[Rr]adius\b/;
255
+ const BORDER_STYLE = /\bborder(?:Color|Width|Style)?\s*:/;
256
+ const BG_STYLE = /\b(?:background|backgroundColor|backgroundImage)\s*:/;
257
+ /**
258
+ * Every opening tag in a JSX body, with its raw attribute text.
259
+ *
260
+ * Hand-rolled rather than regex-only because an attribute value can contain
261
+ * `>` inside a string, a template literal or a `{…}` expression — three of the
262
+ * five hand-rolled copies this module replaced were defeated by exactly that.
263
+ */
264
+ export function openingTags(body) {
265
+ const out = [];
266
+ for (let i = 0; i < body.length; i++) {
267
+ if (body[i] !== '<')
268
+ continue;
269
+ const nameMatch = /^<([A-Za-z][A-Za-z0-9._-]*)/.exec(body.slice(i, i + 64));
270
+ if (!nameMatch)
271
+ continue;
272
+ let j = i + nameMatch[0].length;
273
+ let depth = 0;
274
+ let quote = null;
275
+ let closed = false;
276
+ for (; j < body.length; j++) {
277
+ const c = body[j];
278
+ if (quote) {
279
+ if (c === quote && body[j - 1] !== '\\')
280
+ quote = null;
281
+ continue;
282
+ }
283
+ if (c === '"' || c === "'" || c === '`') {
284
+ quote = c;
285
+ continue;
286
+ }
287
+ if (c === '{')
288
+ depth++;
289
+ else if (c === '}')
290
+ depth--;
291
+ else if (c === '>' && depth === 0) {
292
+ closed = true;
293
+ break;
294
+ }
295
+ }
296
+ if (!closed)
297
+ continue;
298
+ out.push({ tag: nameMatch[1], attrs: body.slice(i + nameMatch[0].length, j) });
299
+ i = j;
300
+ }
301
+ return out;
302
+ }
303
+ /** Every className value on one element — string, template literal or `cn(...)`. */
304
+ function classValues(attrs) {
305
+ const out = [];
306
+ const re = /class(?:Name)?\s*=\s*(?:"([^"]*)"|'([^']*)'|\{([\s\S]*)\})/;
307
+ const m = re.exec(attrs);
308
+ if (!m)
309
+ return out;
310
+ if (m[1] !== undefined)
311
+ out.push(m[1]);
312
+ else if (m[2] !== undefined)
313
+ out.push(m[2]);
314
+ else if (m[3] !== undefined) {
315
+ // `{cn('a', cond && 'b')}` / `{`a ${x}`}` — take every literal inside.
316
+ const lit = /(?:"([^"]*)"|'([^']*)'|`([^`]*)`)/g;
317
+ let l;
318
+ while ((l = lit.exec(m[3])) !== null)
319
+ out.push(l[1] ?? l[2] ?? l[3] ?? '');
320
+ }
321
+ return out;
322
+ }
323
+ /** The raw text of an inline `style={{ … }}` attribute, if present. */
324
+ function styleValue(attrs) {
325
+ const i = attrs.indexOf('style');
326
+ if (i < 0)
327
+ return null;
328
+ const m = /style\s*=\s*\{/.exec(attrs.slice(i));
329
+ if (!m)
330
+ return null;
331
+ let depth = 0;
332
+ const start = i + m.index + m[0].length - 1;
333
+ for (let j = start; j < attrs.length; j++) {
334
+ if (attrs[j] === '{')
335
+ depth++;
336
+ else if (attrs[j] === '}') {
337
+ depth--;
338
+ if (depth === 0)
339
+ return attrs.slice(start, j + 1);
340
+ }
341
+ }
342
+ return null;
343
+ }
344
+ /**
345
+ * Does this element paint its OWN card chrome?
346
+ *
347
+ * Chrome means all three of border, radius and background, because that is
348
+ * exactly what `PageGridLayout` already paints on the slot. Two of the three is
349
+ * a tinted panel or a divider, not a second card — requiring all three is what
350
+ * separates BSU's `glass-card rounded-2xl border-primary/30` (a real double
351
+ * frame) from a `rounded-lg bg-muted` inline chip (not one).
352
+ *
353
+ * Chrome declared through `style={{ background, border, borderRadius }}` counts
354
+ * the same as chrome declared through classes. BSU's `Settings.tsx` is chrome
355
+ * entirely in inline style; a class-only detector calls that file clean, which
356
+ * is how a nested card hides from a gate that only reads Tailwind.
357
+ */
358
+ export function elementDeclaresCardChrome(el, chromeTags) {
359
+ if (chromeTags.has(el.tag))
360
+ return `<${el.tag}> paints its own card chrome`;
361
+ if (CONTROL_TAGS.has(el.tag))
362
+ return null;
363
+ for (const value of classValues(el.attrs)) {
364
+ const composite = COMPOSITE_CARD_CLASS.test(value);
365
+ const hasBorder = composite || BORDER_CLASS.test(value);
366
+ const hasBg = composite || BG_CLASS.test(value);
367
+ if (RADIUS_CLASS.test(value) && hasBorder && hasBg)
368
+ return `<${el.tag}> className declares border + radius + background ("${value.replace(/\s+/g, ' ').trim().slice(0, 80)}")`;
369
+ }
370
+ const style = styleValue(el.attrs);
371
+ if (style && RADIUS_STYLE.test(style) && BORDER_STYLE.test(style) && BG_STYLE.test(style))
372
+ return `<${el.tag}> inline style declares border + radius + background`;
373
+ // Mixed: radius as a class, border+background as inline style. BSU
374
+ // `Settings.tsx` is exactly this shape.
375
+ if (style) {
376
+ const classText = classValues(el.attrs).join(' ');
377
+ const composite = COMPOSITE_CARD_CLASS.test(classText);
378
+ const hasRadius = RADIUS_CLASS.test(classText) || RADIUS_STYLE.test(style);
379
+ const hasBorder = composite || BORDER_CLASS.test(classText) || BORDER_STYLE.test(style);
380
+ const hasBg = composite || BG_CLASS.test(classText) || BG_STYLE.test(style);
381
+ if (hasRadius && hasBorder && hasBg)
382
+ return `<${el.tag}> declares border + radius + background across className and inline style`;
383
+ }
384
+ return null;
385
+ }
386
+ /**
387
+ * Does this grid slot paint chrome of its OWN, post-inversion?
388
+ *
389
+ * Three ways to opt in, and all three have to be visible to the scanner or a
390
+ * re-chromed slot goes unnoticed: `itemChrome` on the grid component (whole
391
+ * app/page), `chrome: true` on the layout item, and `chrome` on the CanvasCard.
392
+ */
393
+ export function slotOptsIntoChrome(fileSource, slotBody) {
394
+ const openTag = /^<CanvasCard[\s\S]*?>/.exec(slotBody)?.[0] ?? '';
395
+ if (/\bchrome\s*(?:=\s*\{?\s*true\b|\}|[\s/>])/.test(openTag) &&
396
+ !/\bchrome\s*=\s*\{\s*false\s*\}/.test(openTag))
397
+ return true;
398
+ if (/\bitemChrome\b(?!\s*=\s*\{\s*false\s*\})/.test(fileSource))
399
+ return true;
400
+ if (/\bchrome\s*:\s*true\b/.test(fileSource))
401
+ return true;
402
+ return false;
403
+ }
404
+ /** The first nested-chrome element inside one grid slot, or null. */
405
+ export function findNestedChrome(slotBody, config) {
406
+ const chromeTags = new Set([
407
+ ...config.cardTags,
408
+ ...(config.nestedChromeTags ?? DEFAULT_NESTED_CHROME_TAGS),
409
+ ]);
410
+ // Skip the slot's own opening tag — `<CanvasCard>` is the grid item, not a
411
+ // child of it.
412
+ const inner = slotBody.replace(/^<CanvasCard[\s\S]*?>/, '');
413
+ for (const el of openingTags(inner)) {
414
+ const hit = elementDeclaresCardChrome(el, chromeTags);
415
+ if (hit)
416
+ return hit;
417
+ }
418
+ return null;
419
+ }
420
+ /**
421
+ * Can this file be parsed with confidence?
422
+ *
423
+ * Returns a reason when it CANNOT. Every `<CanvasCard` must pair with a
424
+ * `</CanvasCard>` the block regex can reach, and every `widgets={{` must close
425
+ * before EOF. A file that fails either is UNKNOWN — it is not reported clean,
426
+ * and it is not silently skipped. "Checked nothing" and "found nothing" do not
427
+ * share an exit code here.
428
+ */
429
+ export function parseConfidence(src) {
430
+ const opens = (src.match(/<CanvasCard[\s/>]/g) ?? []).length;
431
+ const selfClosing = (src.match(/<CanvasCard[^<>]*\/>/g) ?? []).length;
432
+ const closes = (src.match(/<\/CanvasCard>/g) ?? []).length;
433
+ if (opens - selfClosing !== closes)
434
+ return `unbalanced CanvasCard tags: ${opens} opening (${selfClosing} self-closing) vs ${closes} closing`;
435
+ const blocks = extractCanvasCardBlocks(src).length;
436
+ if (blocks !== closes)
437
+ return `CanvasCard block extraction recovered ${blocks} of ${closes} blocks (nested or dynamically composed CanvasCards)`;
438
+ const widgetStarts = (src.match(/widgets=\{\{/g) ?? []).length;
439
+ if (widgetStarts > 0) {
440
+ const re = /widgets=\{\{/g;
441
+ let m;
442
+ while ((m = re.exec(src)) !== null) {
443
+ let depth = 2;
444
+ let i = m.index + m[0].length;
445
+ while (i < src.length && depth > 0) {
446
+ if (src[i] === '{')
447
+ depth++;
448
+ else if (src[i] === '}')
449
+ depth--;
450
+ i++;
451
+ }
452
+ if (depth !== 0)
453
+ return 'widgets={{ … }} object literal never closes before EOF';
454
+ }
455
+ }
456
+ return null;
457
+ }
162
458
  function walk(dir, extensions, acc) {
163
459
  for (const entry of readdirSync(dir)) {
164
460
  if (entry === 'node_modules' || entry === 'dist' || entry === '.git')
@@ -177,6 +473,7 @@ export function scanCardSurfaces(config) {
177
473
  const findings = [];
178
474
  const files = [];
179
475
  const scanRootsResolved = [];
476
+ const unknownFiles = [];
180
477
  // D-92: assert the inputs were present BEFORE reporting any verdict. A
181
478
  // missing scan root must fail loudly, not produce a confident zero.
182
479
  for (const root of config.scanRoots) {
@@ -216,14 +513,53 @@ export function scanCardSurfaces(config) {
216
513
  }
217
514
  for (const file of files) {
218
515
  const rel = relative(config.projectRoot, file);
219
- const raw = readFileSync(file, 'utf8');
516
+ let raw;
517
+ try {
518
+ raw = readFileSync(file, 'utf8');
519
+ }
520
+ catch (err) {
521
+ // A file we could not READ is UNKNOWN, never clean.
522
+ unknownFiles.push(rel);
523
+ findings.push({
524
+ file: rel,
525
+ idiom: 'unparseable',
526
+ detail: `could not be read: ${err.message}`,
527
+ });
528
+ continue;
529
+ }
220
530
  const src = stripComments(raw);
531
+ // D-92 fail-closed: decide whether this file can be parsed with confidence
532
+ // BEFORE reporting any verdict about it. An UNKNOWN file is recorded as
533
+ // UNKNOWN and still scanned on a best-effort basis — what it must never do
534
+ // is contribute a silent "clean".
535
+ const unparseable = parseConfidence(src);
536
+ if (unparseable) {
537
+ unknownFiles.push(rel);
538
+ findings.push({ file: rel, idiom: 'unparseable', detail: unparseable });
539
+ }
221
540
  // --- glued widget: 2+ cards inside ONE grid slot -----------------------
222
541
  const slots = [
223
542
  ...extractCanvasCardBlocks(src).map((b, i) => ({ label: `CanvasCard#${i + 1}`, body: b })),
224
543
  ...extractWidgetEntries(src).map((e) => ({ label: `widgets.${e.key}`, body: e.body })),
225
544
  ];
545
+ let nestedChromeReported = false;
226
546
  for (const slot of slots) {
547
+ // --- V-C5 nested chrome: ONE card inside ONE slot is still a double
548
+ // frame, and V-C4 below cannot see it (it needs 2+).
549
+ const gridItemChromed = (config.gridItemPaintsChrome ?? true) || slotOptsIntoChrome(src, slot.body);
550
+ if (gridItemChromed && !nestedChromeReported && !config.nestedChromeExclusions?.[rel]) {
551
+ const chrome = findNestedChrome(slot.body, config);
552
+ if (chrome) {
553
+ nestedChromeReported = true;
554
+ findings.push({
555
+ file: rel,
556
+ idiom: 'nested-chrome',
557
+ detail: `${slot.label} ${chrome}. The grid item already paints ` +
558
+ `rounded-3xl + bg-card + border-border, so this renders a card inside a card — ` +
559
+ `mismatched corners and a doubled 1px edge.`,
560
+ });
561
+ }
562
+ }
227
563
  const n = countCardTags(slot.body, config);
228
564
  const mapped = hasMappedCards(slot.body, config);
229
565
  if (n > 1 || mapped) {
@@ -280,10 +616,23 @@ export function scanCardSurfaces(config) {
280
616
  ...Object.keys(config.gluedExclusions ?? {}).filter((f) => !hit.has(`glued-widget:${f}`) && !hit.has(`autoheight-optout:${f}`)),
281
617
  ...Object.keys(config.ungriddedExclusions ?? {}).filter((f) => !hit.has(`ungridded-multi-card:${f}`)),
282
618
  ];
619
+ const nestedChromeFiles = [
620
+ ...new Set(findings.filter((f) => f.idiom === 'nested-chrome').map((f) => f.file)),
621
+ ];
283
622
  const summary = `scanned ${files.length} files across ${scanRootsResolved.length} root(s) [${scanRootsResolved.join(', ')}] ` +
284
623
  `with cardTags [${config.cardTags.join(', ')}]` +
285
624
  `${config.classSurfaces?.length ? ` + ${config.classSurfaces.length} class surface pattern(s)` : ''}; ` +
286
- `${findings.length} finding(s); ${Object.keys(config.blindSpots ?? {}).length} declared blind spot(s); ` +
287
- `${staleExclusions.length} stale exclusion(s).`;
288
- return { findings, filesScanned: files.length, scanRootsResolved, staleExclusions, summary };
625
+ `${findings.length} finding(s) incl. ${nestedChromeFiles.length} nested-chrome file(s); ` +
626
+ `${Object.keys(config.blindSpots ?? {}).length} declared blind spot(s); ` +
627
+ `${staleExclusions.length} stale exclusion(s); ` +
628
+ `${unknownFiles.length} UNKNOWN file(s) — an UNKNOWN is not a clean file.`;
629
+ return {
630
+ findings,
631
+ filesScanned: files.length,
632
+ scanRootsResolved,
633
+ staleExclusions,
634
+ unknownFiles,
635
+ nestedChromeFiles,
636
+ summary,
637
+ };
289
638
  }
@@ -1,3 +1,3 @@
1
- export { scanCardSurfaces, stripComments } from './cardSurfaceScanner.js';
1
+ export { scanCardSurfaces, stripComments, openingTags, elementDeclaresCardChrome, findNestedChrome, parseConfidence, slotOptsIntoChrome, } from './cardSurfaceScanner.js';
2
2
  export type { CardSurfaceScannerConfig, CardSurfaceScanResult, CardSurfaceFinding, CardSurfaceIdiom, } from './cardSurfaceScanner.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scanner/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC1E,YAAY,EACV,wBAAwB,EACxB,qBAAqB,EACrB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scanner/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,yBAAyB,EACzB,gBAAgB,EAChB,eAAe,EACf,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,wBAAwB,EACxB,qBAAqB,EACrB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,yBAAyB,CAAC"}
@@ -1 +1 @@
1
- export { scanCardSurfaces, stripComments } from './cardSurfaceScanner.js';
1
+ export { scanCardSurfaces, stripComments, openingTags, elementDeclaresCardChrome, findNestedChrome, parseConfidence, slotOptsIntoChrome, } from './cardSurfaceScanner.js';
package/dist/types.d.ts CHANGED
@@ -15,6 +15,17 @@ export interface GridLayoutItem extends LayoutItem {
15
15
  * operator-mandated capability. See the `activeLayouts` memo.
16
16
  */
17
17
  autoHeight?: boolean;
18
+ /**
19
+ * Paint THIS item as a card — border, radius, background, shadow.
20
+ *
21
+ * Undefined falls back to `PageGridLayout`'s `itemChrome`, which is FALSE.
22
+ * Up to 1.0.7 every grid item painted chrome unconditionally and there was
23
+ * no way to turn it off; 332 files across six apps render their own card
24
+ * inside it. Set this true on a slot whose content is BARE — a heading, a
25
+ * chart, a list with no surface of its own — and leave it alone everywhere
26
+ * the content already is a card.
27
+ */
28
+ chrome?: boolean;
18
29
  /**
19
30
  * True once the user has resized this item with the SE handle.
20
31
  *
@@ -147,6 +158,15 @@ export interface PageGridLayoutProps extends UsePageGridLayoutOptions {
147
158
  widgetMeta?: Record<string, WidgetMeta>;
148
159
  className?: string;
149
160
  isResizable?: boolean;
161
+ /**
162
+ * Paint every grid item as a card unless the item says otherwise.
163
+ *
164
+ * DEFAULT FALSE. This is the app-level migration lever for the 1.1.0 chrome
165
+ * inversion: an app that has not yet migrated its bare slots passes
166
+ * `itemChrome` and gets the pre-1.1.0 look back exactly, then flips slot by
167
+ * slot with `GridLayoutItem.chrome`. Per-item always wins.
168
+ */
169
+ itemChrome?: boolean;
150
170
  resizeHandles?: readonly ResizeHandleAxis[];
151
171
  /**
152
172
  * The signed-in caller's tenant. Threaded into every widget-factory call
@@ -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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"}
@@ -27,10 +27,17 @@ export declare const PAGE_GRID_EDITING_EVENT = "bsuite-page-grid-editing";
27
27
  * was additionally polluted with frozen derived breakpoints, so all of them
28
28
  * reset once to the corrected defaults.
29
29
  *
30
+ * 1000 -> 2000 (2026-08-25, V-C5/G1): two more all-grids defaults changed in
31
+ * one release — the grid item no longer paints card chrome, and a `CanvasCard`
32
+ * that omits `w` is now half the grid rather than all of it. Every stored
33
+ * layout was written under the old width default, so a saved `w: 12` would
34
+ * fight the new default forever and the page would keep stacking. An epoch
35
+ * bump is the only lever that reaches every consumer at once.
36
+ *
30
37
  * Per-page `layoutVersion` bumps keep working on top; this only moves the
31
38
  * floor.
32
39
  */
33
- export declare const PACKAGE_LAYOUT_EPOCH = 1000;
40
+ export declare const PACKAGE_LAYOUT_EPOCH = 2000;
34
41
  export interface PageGridEditingEventDetail {
35
42
  pageKey: string;
36
43
  editing: boolean;
@@ -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;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAiBzC,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAyDD,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,cAAc,EACd,WAAgB,EAChB,aAAiB,EACjB,WAAkB,EAClB,gBAA6C,EAC7C,iBAA4C,EAC5C,iBAAiB,GAClB,EAAE,wBAAwB,GAAG,uBAAuB,CAusBpD"}
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,CA2sBpD"}
@@ -37,10 +37,17 @@ export const PAGE_GRID_EDITING_EVENT = 'bsuite-page-grid-editing';
37
37
  * was additionally polluted with frozen derived breakpoints, so all of them
38
38
  * reset once to the corrected defaults.
39
39
  *
40
+ * 1000 -> 2000 (2026-08-25, V-C5/G1): two more all-grids defaults changed in
41
+ * one release — the grid item no longer paints card chrome, and a `CanvasCard`
42
+ * that omits `w` is now half the grid rather than all of it. Every stored
43
+ * layout was written under the old width default, so a saved `w: 12` would
44
+ * fight the new default forever and the page would keep stacking. An epoch
45
+ * bump is the only lever that reaches every consumer at once.
46
+ *
40
47
  * Per-page `layoutVersion` bumps keep working on top; this only moves the
41
48
  * floor.
42
49
  */
43
- export const PACKAGE_LAYOUT_EPOCH = 1000;
50
+ export const PACKAGE_LAYOUT_EPOCH = 2000;
44
51
  /**
45
52
  * Default for `GridLayoutItem.autoHeight` when an item does not state one.
46
53
  *
@@ -55,57 +62,6 @@ export const PACKAGE_LAYOUT_EPOCH = 1000;
55
62
  * opts out with an explicit `autoHeight: false`.
56
63
  */
57
64
  const DEFAULT_ITEM_AUTO_HEIGHT = true;
58
- /**
59
- * Do two layout SETS describe the same arrangement, at every breakpoint?
60
- *
61
- * Compares only the five fields that place an item. Anything else react-grid-layout
62
- * attaches (`moved`, `static`, `isDraggable`, measured heights) is noise here, and
63
- * comparing it would make every reflow look like a change.
64
- *
65
- * EVERY breakpoint, not just `lg`. A gesture made while the canvas is at `md`
66
- * leaves `lg` untouched, so an `lg`-only comparison reads that real gesture as a
67
- * reflow and silently drops it. That is not hypothetical — it broke the D-75
68
- * "folds a gesture made at md back onto lg" test the moment the comparison was
69
- * introduced.
70
- */
71
- function sameArrangement(a, b) {
72
- return compare(a, b, false);
73
- }
74
- /** Same, but blind to the height of items whose height is MEASURED, not authored. */
75
- function samePlacement(a, b) {
76
- return compare(a, b, true);
77
- }
78
- function compare(a, b, ignoreDerivedHeight) {
79
- if (!a || !b)
80
- return false;
81
- const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
82
- /*
83
- * `h` is EXCLUDED for an autoHeight item, and that exclusion is the whole point.
84
- *
85
- * An autoHeight card's height is MEASURED, not authored: change the column count,
86
- * the card gets narrower, its content rewraps taller, and the ResizeObserver
87
- * emits a new `h`. That emission differs from what was rendered, so a comparison
88
- * including `h` calls it a gesture — and commits the RESCALED WIDTHS riding along
89
- * with it. Measured: three cards authored w=4,3,4 came back w=1,1,1 after nothing
90
- * but a height re-measure.
91
- *
92
- * NARROWED, because the first version was too broad and two existing tests said
93
- * so: a user CAN drag an autoHeight card's height, and that stamps `hUserSet`.
94
- * Once stamped, the height IS a choice and must be compared like any other. So
95
- * `h` is excluded only where it is purely derived — autoHeight AND not user-set.
96
- * A real resize changes `w`, or stamps `hUserSet`, or both; none of those are
97
- * hidden by this.
98
- */
99
- const fingerprint = (items) => (items ?? [])
100
- .map((i) => `${i.i}:${i.x}:${i.y}:${i.w}:${ignoreDerivedHeight && i.autoHeight ? 'auto' : i.h}`)
101
- .sort()
102
- .join('|');
103
- for (const k of keys) {
104
- if (fingerprint(a[k]) !== fingerprint(b[k]))
105
- return false;
106
- }
107
- return true;
108
- }
109
65
  export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, layoutVersion = 1, canEditPage = true, editorEventNames = DEFAULT_EDITOR_EVENT_NAMES, preferenceAdapter = defaultPreferenceAdapter, defaultAutoHeight, }) {
110
66
  const effectiveLayoutVersion = layoutVersion + PACKAGE_LAYOUT_EPOCH;
111
67
  const containerRef = useRef(null);
@@ -512,28 +468,33 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
512
468
  * Identified by comparison rather than by a flag, so it needs no event
513
469
  * ordering to be correct.
514
470
  */
515
- const emitted = layouts;
516
- /* Identical to what was just rendered: a pure reflow, nothing to commit. */
517
- if (sameArrangement(emitted, currentLayouts))
518
- return;
519
471
  /*
520
- * DIFFERS ONLY IN THE HEIGHT OF AN autoHeight ITEM — and that is ambiguous
521
- * by shape alone. A user CAN drag such a card taller (crm7#744), and the
522
- * ResizeObserver ALSO emits a new height when the card is narrowed by a
523
- * column change, because the content rewraps. Both arrive as a bare `h`.
472
+ * ONLY A POINTER GESTURE MAY COMMIT A LAYOUT. Nothing else.
473
+ *
474
+ * Three attempts tried to classify the emission by SHAPE and all three
475
+ * shipped and all three failed on the deployed app:
476
+ *
477
+ * 1.0.4 removed the writes in handleColumnChange — the grid still emitted
478
+ * 1.0.5 skipped an emission equal to what we rendered
479
+ * 1.0.6 ...ignoring measured heights as well
480
+ *
481
+ * Shape can never work, and the reason is structural: react-grid-layout
482
+ * COMPACTS after a reflow. Its emission is therefore legitimately different
483
+ * from `currentLayouts` in x and y, so every comparison against what we
484
+ * rendered says "this is a gesture" and commits the rescaled arrangement.
485
+ * Measured on d.crm.crm7.app with 1.0.6 confirmed in the served bundle: a
486
+ * 12 -> 4 -> 8 -> 2 -> 12 round trip produced THIRTEEN writes and turned
487
+ * widths 4,4,4,7,5,5,12 into 6,6,6,6,6,6,6.
524
488
  *
525
- * Nothing in the layout distinguishes them, so the caller must: the grid
526
- * knows whether a pointer gesture just ended. Default TRUE keeps every
527
- * existing caller behaving exactly as before; `PageGridLayout` passes the
528
- * real answer.
489
+ * The grid knows what we cannot infer: whether a pointer gesture produced
490
+ * this emission. That is the only sound signal, so it is the whole rule.
529
491
  *
530
- * Getting this wrong in either direction is expensive. Treat a re-measure
531
- * as a gesture and the RESCALED WIDTHS ride along into storage — measured:
532
- * three cards authored w=4,3,4 came back w=1,1,1 after nothing but a height
533
- * re-measure. Treat a gesture as a re-measure and a deliberate resize is
534
- * silently dropped, which is crm7#744 reopened.
492
+ * Every other mutation — add, remove, compact, reset — calls `commitItems`
493
+ * directly and is unaffected. A deliberate resize of an auto-height card
494
+ * (crm7#744) IS a gesture and still commits. `wasGesture` defaults to true,
495
+ * so any caller that does not pass it behaves exactly as before.
535
496
  */
536
- if (!wasGesture && samePlacement(emitted, currentLayouts))
497
+ if (!wasGesture)
537
498
  return;
538
499
  pendingLayoutRef.current = layouts;
539
500
  if (layoutCommitFrameRef.current !== null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsuite/page-builder",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "Shared BSuite responsive page-builder grid and layout persistence primitives",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",