@bsuite/page-builder 0.7.0 → 0.9.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.
@@ -1,4 +1,71 @@
1
1
  export const RESPONSIVE_BREAKPOINTS = ['lg', 'md', 'sm', 'xs', 'xxs'];
2
+ /**
3
+ * Breakpoints whose layout MIRRORS `lg` verbatim.
4
+ *
5
+ * D-75 root cause (operator directive 2026-08-13: "columns available to move
6
+ * cards into do not respect the columns slider").
7
+ * ---------------------------------------------------------------------------
8
+ * react-grid-layout picks its breakpoint from the **container** width, not the
9
+ * viewport — `<Responsive width={containerWidth}>` in `PageGridLayout`. Inside
10
+ * an app shell with a sidebar and page padding, a 1280px laptop presents a
11
+ * container around 950px and a 1440px desktop around 1100px. Both land BELOW
12
+ * the `md: 996` / `lg: 1200` thresholds, so the great majority of real desktop
13
+ * sessions render at `sm` or `md`, not `lg`.
14
+ *
15
+ * `sm` used to be derived by `stackToSingleColumn`, which sets `w: cols` on
16
+ * every item — one card per row at full width, whatever the column count. So
17
+ * on an ordinary laptop the columns slider changed `activeCols` while every
18
+ * card stayed stretched across all of them: the control provably did nothing,
19
+ * and the SE resize handle could not widen a card that was already at the grid
20
+ * bound nor narrow one whose `minW` had been clamped up to `cols`.
21
+ *
22
+ * `md` and `sm` therefore mirror `lg`: at a container width of 768px and above
23
+ * there is room for the arrangement the user chose, and honouring their choice
24
+ * is the whole point of the control. Genuine phone widths (`xs` < 768px
25
+ * container, `xxs` < 480px) still stack, which is what that code was for.
26
+ */
27
+ export const MIRRORED_BREAKPOINTS = ['md', 'sm'];
28
+ /**
29
+ * Breakpoints derived as a single full-width stack — narrow enough that a
30
+ * multi-column arrangement cannot be read.
31
+ */
32
+ export const STACKED_BREAKPOINTS = ['xs', 'xxs'];
33
+ /**
34
+ * Every breakpoint this module DERIVES from `lg`.
35
+ *
36
+ * A derived breakpoint is a render-time projection, never a stored fact. It
37
+ * must not reach the preference adapter: react-grid-layout echoes ALL
38
+ * breakpoints back through `onLayoutChange` on every gesture, so persisting
39
+ * them verbatim froze the derivation — a layout derived once at 12 columns was
40
+ * then "consumer-supplied" forever and stopped tracking `lg`. See
41
+ * `canonicaliseLayoutForPersist` in `usePageGridLayout.ts`.
42
+ */
43
+ export const DERIVED_BREAKPOINTS = [
44
+ ...MIRRORED_BREAKPOINTS,
45
+ ...STACKED_BREAKPOINTS,
46
+ ];
47
+ export function isDerivedBreakpoint(breakpoint) {
48
+ return DERIVED_BREAKPOINTS.includes(breakpoint);
49
+ }
50
+ /**
51
+ * True when a gesture at `breakpoint` can be written straight back onto `lg`.
52
+ *
53
+ * `lg`, `md` and `sm` all render the same array against the same column count,
54
+ * so an edit made at any of them is expressible in `lg` with no loss. `xs` and
55
+ * `xxs` render a full-width stack, where an edit carries only a vertical order
56
+ * and would flatten a multi-column desktop arrangement if applied to `lg`.
57
+ */
58
+ export function isCanonicalisableBreakpoint(breakpoint) {
59
+ return breakpoint === 'lg' || MIRRORED_BREAKPOINTS.includes(breakpoint);
60
+ }
61
+ /**
62
+ * Stack items into one full-width column, preserving reading order.
63
+ *
64
+ * `w: cols` is deliberate at the breakpoints this is used for (`xs`/`xxs`):
65
+ * the container is narrower than 768px, so a card occupies the whole grid
66
+ * whatever the column count. Do NOT reach for this at wider breakpoints — that
67
+ * was D-75.
68
+ */
2
69
  function stackToSingleColumn(items, cols) {
3
70
  const sorted = [...items].sort((a, b) => (a.y !== b.y ? a.y - b.y : a.x - b.x));
4
71
  let nextY = 0;
@@ -16,10 +83,15 @@ function stackToSingleColumn(items, cols) {
16
83
  }
17
84
  /**
18
85
  * Ensure a GridLayouts object has entries for all responsive breakpoints
19
- * (lg, md, sm, xs, xxs). Missing smaller breakpoints (sm/xs/xxs) are derived
20
- * from `lg` as a single-column vertical stack so that consumer-supplied
21
- * lg-only layouts no longer squash on phone viewports. Missing `md` is
22
- * mirrored from `lg`. Consumer-supplied breakpoints are preserved verbatim.
86
+ * (lg, md, sm, xs, xxs).
87
+ *
88
+ * - `md` and `sm` mirror `lg` so the user's chosen column arrangement survives
89
+ * every desktop and tablet container width (see {@link MIRRORED_BREAKPOINTS}).
90
+ * - `xs` and `xxs` are derived as a single-column stack so a phone viewport
91
+ * never squashes cards into unreadable slivers.
92
+ * - Consumer-supplied breakpoints are preserved verbatim. Supplying one opts
93
+ * that breakpoint OUT of tracking `lg`, including out of the columns slider —
94
+ * prefer not to.
23
95
  */
24
96
  export function buildResponsiveLayouts(layouts, { cols = 12 } = {}) {
25
97
  const lg = layouts.lg ?? [];
@@ -32,7 +104,7 @@ export function buildResponsiveLayouts(layouts, { cols = 12 } = {}) {
32
104
  result[bp] = supplied;
33
105
  continue;
34
106
  }
35
- if (bp === 'md') {
107
+ if (MIRRORED_BREAKPOINTS.includes(bp)) {
36
108
  result[bp] = lg;
37
109
  continue;
38
110
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The CanvasCard → grid-layout algorithm, extracted from the component so it
3
+ * can be unit-tested and reused without rendering anything.
4
+ *
5
+ * This is the half of `DraggableCardPage` that was copied verbatim into three
6
+ * apps (crm7, braden, throughput) and was absent from two more (BSU, conduit).
7
+ * It has no app dependencies, no permission model and no adapter — those are
8
+ * the parts that legitimately differ per app and stay there.
9
+ */
10
+ import { type ReactElement, type ReactNode } from 'react';
11
+ import type { CanvasCardProps } from './CanvasCard.js';
12
+ import type { GridLayouts } from './types.js';
13
+ /** The 12-column grid every BSuite page canvas is laid out against. */
14
+ export declare const CANVAS_GRID_COLUMNS = 12;
15
+ export declare function isCanvasCardElement(node: ReactNode): node is ReactElement<CanvasCardProps>;
16
+ /**
17
+ * Best-effort display name for a node that is about to be discarded.
18
+ * "Something was dropped" is not actionable across hundreds of pages; the
19
+ * component's own name is what makes it findable.
20
+ */
21
+ export declare function describeNode(node: ReactNode): string;
22
+ /**
23
+ * Depth-first flatten: CanvasCards kept, Fragments expanded, everything else
24
+ * dropped — and RECORDED rather than vanishing.
25
+ *
26
+ * The dropping itself is correct and deliberate: this maps children onto grid
27
+ * slots, and a node with no `cardKey` has no slot to occupy. What was wrong is
28
+ * that it happened in total silence, so a page could render with a confirm
29
+ * dialog missing and look completely fine — the button appears, the click
30
+ * handler runs, state flips, and the element it toggles was never mounted.
31
+ *
32
+ * Fragments must be expanded explicitly because `Children.forEach` is shallow:
33
+ * it sees one Fragment node, not the CanvasCards inside it. A Fragment-wrapped
34
+ * conditional branch used to drop every nested card silently (the crm7 burn-7
35
+ * CI class, `leave/[id]`).
36
+ */
37
+ export declare function flattenCanvasCards(nodes: ReactNode): {
38
+ cards: ReactElement<CanvasCardProps>[];
39
+ dropped: string[];
40
+ };
41
+ export declare function clampColumns(value: number, min?: number, max?: number): number;
42
+ export interface CanvasCardLayoutResult {
43
+ /** Widget dict keyed by `cardKey`, ready for `PageGridLayout.widgets`. */
44
+ widgets: Record<string, ReactNode>;
45
+ /** `lg` breakpoint layout, ready for `PageGridLayout.defaultLayouts`. */
46
+ layouts: GridLayouts;
47
+ /** Display names of children that occupied no grid slot and were discarded. */
48
+ dropped: string[];
49
+ }
50
+ /**
51
+ * Build the `widgets` dict and `defaultLayouts.lg` array from CanvasCard
52
+ * children.
53
+ *
54
+ * Cards flow left-to-right and wrap to a new row when the next card would
55
+ * overflow the 12-column grid. Because the dict is built FROM THE CHILDREN AT
56
+ * RENDER TIME, a `.map()` producing N cards is fully supported and needs no
57
+ * registration mechanism — each mapped card becomes its own independent grid
58
+ * item. (This is the fact that disproves the "dynamic card lists cannot be
59
+ * split into static widget keys" objection recorded in several app ledgers.)
60
+ */
61
+ export declare function buildCanvasCardLayout(children: ReactNode): CanvasCardLayoutResult;
62
+ //# sourceMappingURL=canvasCardLayout.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * The CanvasCard → grid-layout algorithm, extracted from the component so it
4
+ * can be unit-tested and reused without rendering anything.
5
+ *
6
+ * This is the half of `DraggableCardPage` that was copied verbatim into three
7
+ * apps (crm7, braden, throughput) and was absent from two more (BSU, conduit).
8
+ * It has no app dependencies, no permission model and no adapter — those are
9
+ * the parts that legitimately differ per app and stay there.
10
+ */
11
+ import { Children, Fragment, isValidElement, } from 'react';
12
+ /** The 12-column grid every BSuite page canvas is laid out against. */
13
+ export const CANVAS_GRID_COLUMNS = 12;
14
+ export function isCanvasCardElement(node) {
15
+ return (isValidElement(node) &&
16
+ node.type.displayName === 'CanvasCard');
17
+ }
18
+ function isFragmentElement(node) {
19
+ return isValidElement(node) && node.type === Fragment;
20
+ }
21
+ /**
22
+ * Best-effort display name for a node that is about to be discarded.
23
+ * "Something was dropped" is not actionable across hundreds of pages; the
24
+ * component's own name is what makes it findable.
25
+ */
26
+ export function describeNode(node) {
27
+ if (!isValidElement(node))
28
+ return typeof node === 'string'
29
+ ? `text ${JSON.stringify(node)}`
30
+ : String(node);
31
+ const t = node.type;
32
+ if (typeof t === 'string')
33
+ return `<${t}>`;
34
+ return `<${t?.displayName ?? t?.name ?? 'Unknown'}>`;
35
+ }
36
+ /**
37
+ * Depth-first flatten: CanvasCards kept, Fragments expanded, everything else
38
+ * dropped — and RECORDED rather than vanishing.
39
+ *
40
+ * The dropping itself is correct and deliberate: this maps children onto grid
41
+ * slots, and a node with no `cardKey` has no slot to occupy. What was wrong is
42
+ * that it happened in total silence, so a page could render with a confirm
43
+ * dialog missing and look completely fine — the button appears, the click
44
+ * handler runs, state flips, and the element it toggles was never mounted.
45
+ *
46
+ * Fragments must be expanded explicitly because `Children.forEach` is shallow:
47
+ * it sees one Fragment node, not the CanvasCards inside it. A Fragment-wrapped
48
+ * conditional branch used to drop every nested card silently (the crm7 burn-7
49
+ * CI class, `leave/[id]`).
50
+ */
51
+ export function flattenCanvasCards(nodes) {
52
+ const cards = [];
53
+ const dropped = [];
54
+ Children.forEach(nodes, (child) => {
55
+ // Falsy entries are the documented conditional shape — `{cond && <CanvasCard/>}`
56
+ // collapses to `false`, which is intentional and must stay silent.
57
+ if (child == null || child === false || child === true || child === '')
58
+ return;
59
+ if (isCanvasCardElement(child)) {
60
+ cards.push(child);
61
+ return;
62
+ }
63
+ if (isFragmentElement(child)) {
64
+ const inner = flattenCanvasCards(child.props.children);
65
+ cards.push(...inner.cards);
66
+ dropped.push(...inner.dropped);
67
+ return;
68
+ }
69
+ dropped.push(describeNode(child));
70
+ });
71
+ return { cards, dropped };
72
+ }
73
+ export function clampColumns(value, min = 1, max = CANVAS_GRID_COLUMNS) {
74
+ return Math.min(Math.max(value, min), max);
75
+ }
76
+ /**
77
+ * Build the `widgets` dict and `defaultLayouts.lg` array from CanvasCard
78
+ * children.
79
+ *
80
+ * Cards flow left-to-right and wrap to a new row when the next card would
81
+ * overflow the 12-column grid. Because the dict is built FROM THE CHILDREN AT
82
+ * RENDER TIME, a `.map()` producing N cards is fully supported and needs no
83
+ * registration mechanism — each mapped card becomes its own independent grid
84
+ * item. (This is the fact that disproves the "dynamic card lists cannot be
85
+ * split into static widget keys" objection recorded in several app ledgers.)
86
+ */
87
+ export function buildCanvasCardLayout(children) {
88
+ const widgets = {};
89
+ const lg = [];
90
+ let x = 0;
91
+ let y = 0;
92
+ let rowHeight = 0;
93
+ const { cards, dropped } = flattenCanvasCards(children);
94
+ for (const child of cards) {
95
+ // autoHeight defaults to TRUE — cards track measured content height unless
96
+ // a card explicitly opts out for a virtualized list. See CanvasCardProps.
97
+ const { cardKey, h = 6, minH = 2, minW = 4, autoHeight = true, children: body, } = child.props;
98
+ const normalizedMinW = clampColumns(minW);
99
+ const width = clampColumns(child.props.w ?? CANVAS_GRID_COLUMNS, normalizedMinW);
100
+ if (x + width > CANVAS_GRID_COLUMNS) {
101
+ y += rowHeight;
102
+ x = 0;
103
+ rowHeight = 0;
104
+ }
105
+ widgets[cardKey] = (_jsx("div", { className: "relative h-full group/canvas-card", children: body }));
106
+ lg.push({
107
+ i: cardKey,
108
+ x,
109
+ y,
110
+ w: width,
111
+ h,
112
+ minW: normalizedMinW,
113
+ minH,
114
+ ...(autoHeight ? { autoHeight: true } : {}),
115
+ });
116
+ x += width;
117
+ rowHeight = Math.max(rowHeight, h);
118
+ }
119
+ return { widgets, layouts: { lg }, dropped };
120
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  export { PageGridLayout } from './PageGridLayout.js';
2
2
  export { PageEditorLauncher } from './PageEditorLauncher.js';
3
+ export { CanvasCard } from './CanvasCard.js';
4
+ export type { CanvasCardProps } from './CanvasCard.js';
5
+ export { DraggableCardPage } from './DraggableCardPage.js';
6
+ export type { DraggableCardPageProps } from './DraggableCardPage.js';
7
+ export { buildCanvasCardLayout, flattenCanvasCards, isCanvasCardElement, describeNode, clampColumns, CANVAS_GRID_COLUMNS, } from './canvasCardLayout.js';
8
+ export type { CanvasCardLayoutResult } from './canvasCardLayout.js';
3
9
  export type { WidgetConfig, PageEditorLauncherProps } from './PageEditorLauncher.js';
4
10
  export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.js';
5
11
  export type { PageGridEditingEventDetail } 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,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,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
@@ -1,5 +1,8 @@
1
1
  export { PageGridLayout } from './PageGridLayout.js';
2
2
  export { PageEditorLauncher } from './PageEditorLauncher.js';
3
+ export { CanvasCard } from './CanvasCard.js';
4
+ export { DraggableCardPage } from './DraggableCardPage.js';
5
+ export { buildCanvasCardLayout, flattenCanvasCards, isCanvasCardElement, describeNode, clampColumns, CANVAS_GRID_COLUMNS, } from './canvasCardLayout.js';
3
6
  export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.js';
4
7
  export { rescaleLayout } from './rescaleLayout.js';
5
8
  export { computeAutoHeightRows } from './autoHeight.js';
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The card-surface contract scanner — ONE implementation, shipped from the
3
+ * package, replacing five divergent hand-rolled copies.
4
+ *
5
+ * Why this exists
6
+ * ---------------
7
+ * crm7 wrote a card-unglue contract test. BSU, conduit, throughput and braden
8
+ * each PORTED it, and each silently dropped checks during the port. The
9
+ * divergence was not cosmetic:
10
+ *
11
+ * - `findUngriddedMultiCard` — the check crm7's own comment calls "the actual
12
+ * root cause of the operator's platform-wide complaint" — was ABSENT from
13
+ * BSU, throughput and braden, and present only in a narrowed form in
14
+ * conduit. In throughput and braden it was absent structurally: their glue
15
+ * scan is gated on the file containing `CanvasCard` at all, so a page with
16
+ * two cards and no grid primitive is invisible by construction.
17
+ * - conduit's header documents THREE card idioms and its regexes detect TWO.
18
+ * The third (`rounded-lg border p-4` section panels) has no detector, which
19
+ * is how five known-glued conduit pages pass as an empty ledger.
20
+ * - BSU's scan only walks files containing the literal string
21
+ * `PageGridLayout`, so every BSU page that never adopted the grid is
22
+ * structurally invisible to BSU's own gate.
23
+ *
24
+ * A gate that cannot tell "checked nothing" from "found nothing" is not a gate
25
+ * (D-92). This module therefore ASSERTS ITS INPUTS: it fails closed if a scan
26
+ * root is missing or matches zero files, and it reports how it reached its
27
+ * verdict rather than only the verdict.
28
+ *
29
+ * What stays per-app
30
+ * ------------------
31
+ * Scan roots, card vocabulary, class-based surface patterns, and the exclusion
32
+ * ledgers. Those are operational truth about a specific app. Everything else —
33
+ * the detection logic — lives here so it can only be fixed once.
34
+ *
35
+ * Node-only: this module touches the filesystem and is exported from
36
+ * `@bsuite/page-builder/scanner`, NOT from the package root, so it never
37
+ * reaches a browser bundle.
38
+ */
39
+ export type CardSurfaceIdiom =
40
+ /** 2+ card-like components packed inside ONE grid slot. */
41
+ 'glued-widget'
42
+ /** 2+ card-like components on a page that never reaches a grid primitive. */
43
+ | 'ungridded-multi-card'
44
+ /** A retired single-widget primitive still present or still imported. */
45
+ | 'retired-primitive'
46
+ /** autoHeight opt-out on a card that is not a virtualized list. */
47
+ | 'autoheight-optout'
48
+ /** autoHeight={false} with a seed height so small the card must clip. */
49
+ | 'clipped-card';
50
+ export interface CardSurfaceFinding {
51
+ file: string;
52
+ idiom: CardSurfaceIdiom;
53
+ detail: string;
54
+ /** Number of card-like components involved, where meaningful. */
55
+ cardCount?: number;
56
+ }
57
+ export interface CardSurfaceScannerConfig {
58
+ /** Absolute path to the repo root. */
59
+ projectRoot: string;
60
+ /**
61
+ * Directories to walk, repo-relative. Every one MUST exist and MUST contain
62
+ * at least one scannable file, or the scan fails closed.
63
+ */
64
+ scanRoots: string[];
65
+ /**
66
+ * Component names that count as a card. Apps differ materially here: crm7
67
+ * uses `Card`/`StatCard`/`SummaryCard`, BSU adds `ServiceCard`, conduit uses
68
+ * `SummaryCard`/`CounterCard`.
69
+ */
70
+ cardTags: string[];
71
+ /**
72
+ * Class-based card surfaces — a card that is a styled `div`, not a
73
+ * component. This is the idiom conduit documented and never implemented, and
74
+ * the one BSU's `glass-card` tiles use. Supply e.g.
75
+ * `[/glass-card/, /rounded-lg border p-4/]`.
76
+ */
77
+ classSurfaces?: RegExp[];
78
+ /** Identifiers that prove a file reached the grid. */
79
+ gridPrimitives?: string[];
80
+ /** Repo-relative path of a retired single-widget primitive that must not exist. */
81
+ retiredPrimitivePaths?: string[];
82
+ /** file -> reason. A glued widget listed here is allowed. */
83
+ gluedExclusions?: Record<string, string>;
84
+ /** file -> reason. An ungridded multi-card page listed here is allowed. */
85
+ ungriddedExclusions?: Record<string, string>;
86
+ /**
87
+ * Directories deliberately NOT scanned, each with a reason. Declaring a
88
+ * blind spot is required; having an undeclared one is the defect.
89
+ */
90
+ blindSpots?: Record<string, string>;
91
+ /**
92
+ * If a file matches this, an `autoHeight={false}` in it is permitted — the
93
+ * opt-out is reserved for genuinely virtualized/windowed lists.
94
+ */
95
+ autoHeightEscapeHatch?: RegExp;
96
+ /** File extensions to scan. Defaults to .tsx. */
97
+ extensions?: string[];
98
+ }
99
+ export interface CardSurfaceScanResult {
100
+ findings: CardSurfaceFinding[];
101
+ /** Files actually read. If this is 0 the scan proved nothing. */
102
+ filesScanned: number;
103
+ scanRootsResolved: string[];
104
+ /** Exclusion-ledger keys that no longer match any finding — stale entries. */
105
+ staleExclusions: string[];
106
+ /** Human-readable account of how the verdict was reached. */
107
+ summary: string;
108
+ }
109
+ /**
110
+ * Strip block comments, line comments and JSX comments before matching.
111
+ *
112
+ * Three of the five hand-rolled copies matched against raw file content, which
113
+ * produced two opposite errors at once: a commented-out `<Card/><Card/>`
114
+ * example false-POSITIVES as glue, and a file that merely MENTIONS
115
+ * `CanvasCard` in a TODO comment false-NEGATIVES as "already gridded" and is
116
+ * skipped entirely. Stripping first fixes both.
117
+ */
118
+ export declare function stripComments(source: string): string;
119
+ export declare function scanCardSurfaces(config: CardSurfaceScannerConfig): CardSurfaceScanResult;
120
+ //# sourceMappingURL=cardSurfaceScanner.d.ts.map
@@ -0,0 +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"}