@bsuite/page-builder 0.8.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.
- package/dist/CanvasCard.d.ts +52 -0
- package/dist/CanvasCard.d.ts.map +1 -0
- package/dist/CanvasCard.js +18 -0
- package/dist/DraggableCardPage.d.ts +118 -0
- package/dist/DraggableCardPage.d.ts.map +1 -0
- package/dist/DraggableCardPage.js +110 -0
- package/dist/canvasCardLayout.d.ts +62 -0
- package/dist/canvasCardLayout.d.ts.map +1 -0
- package/dist/canvasCardLayout.js +120 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/scanner/cardSurfaceScanner.d.ts +120 -0
- package/dist/scanner/cardSurfaceScanner.d.ts.map +1 -0
- package/dist/scanner/cardSurfaceScanner.js +289 -0
- package/dist/scanner/index.d.ts +3 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +1 -0
- package/package.json +8 -3
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface CanvasCardProps {
|
|
3
|
+
/** Stable widget key used by PageGridLayout for layout persistence. Required. */
|
|
4
|
+
cardKey: string;
|
|
5
|
+
/** Default grid width in columns (12-col grid). Defaults to 12. */
|
|
6
|
+
w?: 2 | 3 | 4 | 6 | 8 | 12;
|
|
7
|
+
/** Minimum width in columns when the user resizes. Defaults to 4. */
|
|
8
|
+
minW?: 2 | 3 | 4 | 6 | 8 | 12;
|
|
9
|
+
/** Default grid height in 32px row units (seed before first measurement). Defaults to 6. */
|
|
10
|
+
h?: number;
|
|
11
|
+
/** Minimum height in row units when the user resizes. Defaults to 2. */
|
|
12
|
+
minH?: number;
|
|
13
|
+
/**
|
|
14
|
+
* When true, this card's height tracks its measured content height instead
|
|
15
|
+
* of a fixed manual size. `h` is only the seed row count used before the
|
|
16
|
+
* first ResizeObserver measurement lands. Threaded through to the underlying
|
|
17
|
+
* {@link GridLayoutItem.autoHeight} by `DraggableCardPage`.
|
|
18
|
+
*
|
|
19
|
+
* Defaults to TRUE. Cards grow to fit their content so forms and sections
|
|
20
|
+
* never render clipped.
|
|
21
|
+
*
|
|
22
|
+
* `autoHeight={false}` is RESERVED FOR VIRTUALIZED / WINDOWED LISTS ONLY
|
|
23
|
+
* (operator card-surfaces doctrine, 2026-07-23): a card whose body renders a
|
|
24
|
+
* windowed list that owns its own internal scroll and must NOT balloon to
|
|
25
|
+
* the full row count. An ordinary data table must NOT opt out — the doctrine
|
|
26
|
+
* is expand-by-default so every row is visible without an internal
|
|
27
|
+
* scrollbar. A blanket opt-out across ~120 crm7 pages was the 192px clip bug
|
|
28
|
+
* and was swept. Do not reintroduce it on a plain card or table.
|
|
29
|
+
*/
|
|
30
|
+
autoHeight?: boolean;
|
|
31
|
+
/** Card body — typically a single card or page-header element. */
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* CanvasCard — marker component consumed by `DraggableCardPage`.
|
|
36
|
+
*
|
|
37
|
+
* It renders `null` by design: `DraggableCardPage` reads its props off the JSX
|
|
38
|
+
* tree rather than rendering it, so accidental flat usage outside a
|
|
39
|
+
* `DraggableCardPage` is harmless rather than broken.
|
|
40
|
+
*
|
|
41
|
+
* Kept in its own module so a consumer can import the marker without pulling
|
|
42
|
+
* in `DraggableCardPage`'s transitive dependency on `react-grid-layout`. The
|
|
43
|
+
* `displayName` check inside `DraggableCardPage` compares against the literal
|
|
44
|
+
* string `'CanvasCard'`, so it keeps working across module boundaries, bundler
|
|
45
|
+
* chunk splits and duplicated package instances — an `instanceof`/identity
|
|
46
|
+
* check would not.
|
|
47
|
+
*/
|
|
48
|
+
export declare function CanvasCard(_props: CanvasCardProps): null;
|
|
49
|
+
export declare namespace CanvasCard {
|
|
50
|
+
var displayName: string;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=CanvasCard.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CanvasCard — marker component consumed by `DraggableCardPage`.
|
|
3
|
+
*
|
|
4
|
+
* It renders `null` by design: `DraggableCardPage` reads its props off the JSX
|
|
5
|
+
* tree rather than rendering it, so accidental flat usage outside a
|
|
6
|
+
* `DraggableCardPage` is harmless rather than broken.
|
|
7
|
+
*
|
|
8
|
+
* Kept in its own module so a consumer can import the marker without pulling
|
|
9
|
+
* in `DraggableCardPage`'s transitive dependency on `react-grid-layout`. The
|
|
10
|
+
* `displayName` check inside `DraggableCardPage` compares against the literal
|
|
11
|
+
* string `'CanvasCard'`, so it keeps working across module boundaries, bundler
|
|
12
|
+
* chunk splits and duplicated package instances — an `instanceof`/identity
|
|
13
|
+
* check would not.
|
|
14
|
+
*/
|
|
15
|
+
export function CanvasCard(_props) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
CanvasCard.displayName = 'CanvasCard';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DraggableCardPage — declarative wrapper around `PageGridLayout` that gives
|
|
3
|
+
* each child card its own draggable, independently-resizable widget slot.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists
|
|
6
|
+
* ---------------
|
|
7
|
+
* The default way to reach `PageGridLayout` is
|
|
8
|
+
* `widgets={{ content: <Card/><Card/>… }}`, which packs an entire page into
|
|
9
|
+
* ONE widget. The canvas editor then drags the whole page as a single block
|
|
10
|
+
* instead of letting the user rearrange individual cards, and the page's
|
|
11
|
+
* height is capped at that one grid item's pixel height, so long forms render
|
|
12
|
+
* truncated. That is the operator's most-repeated complaint about this estate
|
|
13
|
+
* ("cards move as one block"), and it is a property of how the page calls the
|
|
14
|
+
* grid, not of the grid itself.
|
|
15
|
+
*
|
|
16
|
+
* Why it lives HERE rather than in an app
|
|
17
|
+
* ---------------------------------------
|
|
18
|
+
* It previously existed as three hand-copied forms (crm7, braden, throughput)
|
|
19
|
+
* and was ABSENT from two apps (BSU, conduit). The absence had a cost beyond
|
|
20
|
+
* duplication: BSU's exclusion ledger recorded that its dynamic card lists
|
|
21
|
+
* "cannot be split into static widget keys without a dynamic-widget
|
|
22
|
+
* registration mechanism", which is FALSE — `buildCanvasCardLayout` derives
|
|
23
|
+
* the widget dict from the children at render time, so a `.map()` over a
|
|
24
|
+
* runtime-variable list already produces N independent grid items. crm7 ships
|
|
25
|
+
* exactly that against a Postgres-backed 0..N array. The objection was an
|
|
26
|
+
* artifact of the primitive not being present, not a real constraint.
|
|
27
|
+
* Tracked as crm7#412 / bsuite#1995.
|
|
28
|
+
*
|
|
29
|
+
* Usage
|
|
30
|
+
* -----
|
|
31
|
+
* <DraggableCardPage pageKey="/people/:id" layoutVersion={3}>
|
|
32
|
+
* <CanvasCard cardKey="header" h={4}>
|
|
33
|
+
* <PageHeader heading="Person" />
|
|
34
|
+
* </CanvasCard>
|
|
35
|
+
* <CanvasCard cardKey="personalInfo" h={12}>
|
|
36
|
+
* <Card>…</Card>
|
|
37
|
+
* </CanvasCard>
|
|
38
|
+
* {isTraining && (
|
|
39
|
+
* <CanvasCard cardKey="trainingContract" h={16}>
|
|
40
|
+
* <Card>…</Card>
|
|
41
|
+
* </CanvasCard>
|
|
42
|
+
* )}
|
|
43
|
+
* </DraggableCardPage>
|
|
44
|
+
*
|
|
45
|
+
* Heights are 32px row units. Falsy children are skipped, so conditional cards
|
|
46
|
+
* collapse cleanly without leaving empty slots. Fragments are flattened.
|
|
47
|
+
*
|
|
48
|
+
* What this component deliberately does NOT do
|
|
49
|
+
* --------------------------------------------
|
|
50
|
+
* - **Permissions.** Gating is an app concern with an app-specific permission
|
|
51
|
+
* vocabulary. Wrap this component in the app's own gate.
|
|
52
|
+
* - **`compactType`.** It is NOT accepted, on purpose. `usePageGridLayout`
|
|
53
|
+
* selects `verticalCompactor` unconditionally for BOTH view and edit mode,
|
|
54
|
+
* and that is load-bearing: the previous edit-mode config
|
|
55
|
+
* (`preventCollision: true` with `compactType: null`) made react-grid-layout
|
|
56
|
+
* full-revert any gesture landing on an occupied cell, and `onDragStop` only
|
|
57
|
+
* emits when the layout changed — so a reverted gesture emitted nothing and
|
|
58
|
+
* NOTHING COULD EVER SAVE (bsuite#1588). Accepting a `compactType` prop here
|
|
59
|
+
* would re-open that hole. crm7's local wrapper passed `compactType ?? null`
|
|
60
|
+
* through a cast to a component that does not read it; the prop was dead,
|
|
61
|
+
* and dead is the correct state for it.
|
|
62
|
+
*/
|
|
63
|
+
import { type ComponentType, type ReactNode } from 'react';
|
|
64
|
+
import { CanvasCard, type CanvasCardProps } from './CanvasCard.js';
|
|
65
|
+
import type { PageGridLayoutProps } from './types.js';
|
|
66
|
+
export { CanvasCard };
|
|
67
|
+
export type { CanvasCardProps };
|
|
68
|
+
export type DraggableCardPageProps = Omit<PageGridLayoutProps, 'widgets' | 'defaultLayouts'> & {
|
|
69
|
+
/**
|
|
70
|
+
* App-wide layout epoch, added to this page's `layoutVersion` before it
|
|
71
|
+
* reaches `PageGridLayout`. Defaults to 0.
|
|
72
|
+
*
|
|
73
|
+
* This is the lever for "a DraggableCardPage-level default changed for EVERY
|
|
74
|
+
* page in this app" — bump it once instead of hand-bumping `layoutVersion`
|
|
75
|
+
* at hundreds of call sites, which is guaranteed to miss some. Per-page
|
|
76
|
+
* `layoutVersion` bumps keep working on top of it.
|
|
77
|
+
*
|
|
78
|
+
* It is deliberately a PROP rather than a package constant, because each app
|
|
79
|
+
* has its own history of saved layouts: crm7 is at 101 (E5a autoHeight flip,
|
|
80
|
+
* then the 2026-07-23 card-surfaces sweep), braden and throughput at 100
|
|
81
|
+
* (the D-74 sweep), and a fresh adopter starts at 0. Baking a single number
|
|
82
|
+
* into the package would silently reset or fail to reset saved layouts
|
|
83
|
+
* depending on which app imported it.
|
|
84
|
+
*
|
|
85
|
+
* Distinct from the package-level `PACKAGE_LAYOUT_EPOCH`, which is a
|
|
86
|
+
* one-time reset applied by `usePageGridLayout` to every consumer at once
|
|
87
|
+
* when a package behaviour change poisons stored layouts suite-wide.
|
|
88
|
+
*/
|
|
89
|
+
layoutEpoch?: number;
|
|
90
|
+
/**
|
|
91
|
+
* The grid component to render. Defaults to this package's `PageGridLayout`.
|
|
92
|
+
*
|
|
93
|
+
* Apps that wrap `PageGridLayout` in a local adapter (to inject permissions,
|
|
94
|
+
* a preference adapter, tenant scoping or entity-widget factories) pass that
|
|
95
|
+
* adapter here, so they get the shared card algorithm without giving up
|
|
96
|
+
* their adapter. An app with no adapter passes nothing.
|
|
97
|
+
*/
|
|
98
|
+
gridComponent?: ComponentType<PageGridLayoutProps>;
|
|
99
|
+
/**
|
|
100
|
+
* Called with the display names of children that occupied no grid slot and
|
|
101
|
+
* were discarded. Defaults to a development-only `console.error`.
|
|
102
|
+
*
|
|
103
|
+
* A dropped child is a defect, not a style note: if it was a dialog, its
|
|
104
|
+
* trigger appears to do nothing — the button renders, the handler runs, and
|
|
105
|
+
* the element it toggles was never mounted.
|
|
106
|
+
*/
|
|
107
|
+
onDroppedChildren?: (dropped: string[], pageKey: string) => void;
|
|
108
|
+
/**
|
|
109
|
+
* CanvasCard children. Falsy entries are ignored. Fragments are flattened so
|
|
110
|
+
* a conditional multi-card branch still registers each CanvasCard.
|
|
111
|
+
*/
|
|
112
|
+
children: ReactNode;
|
|
113
|
+
};
|
|
114
|
+
export declare function DraggableCardPage({ layoutEpoch, gridComponent, onDroppedChildren, children, ...pageProps }: DraggableCardPageProps): import("react").JSX.Element;
|
|
115
|
+
export declare namespace DraggableCardPage {
|
|
116
|
+
var displayName: string;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=DraggableCardPage.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* DraggableCardPage — declarative wrapper around `PageGridLayout` that gives
|
|
4
|
+
* each child card its own draggable, independently-resizable widget slot.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists
|
|
7
|
+
* ---------------
|
|
8
|
+
* The default way to reach `PageGridLayout` is
|
|
9
|
+
* `widgets={{ content: <Card/><Card/>… }}`, which packs an entire page into
|
|
10
|
+
* ONE widget. The canvas editor then drags the whole page as a single block
|
|
11
|
+
* instead of letting the user rearrange individual cards, and the page's
|
|
12
|
+
* height is capped at that one grid item's pixel height, so long forms render
|
|
13
|
+
* truncated. That is the operator's most-repeated complaint about this estate
|
|
14
|
+
* ("cards move as one block"), and it is a property of how the page calls the
|
|
15
|
+
* grid, not of the grid itself.
|
|
16
|
+
*
|
|
17
|
+
* Why it lives HERE rather than in an app
|
|
18
|
+
* ---------------------------------------
|
|
19
|
+
* It previously existed as three hand-copied forms (crm7, braden, throughput)
|
|
20
|
+
* and was ABSENT from two apps (BSU, conduit). The absence had a cost beyond
|
|
21
|
+
* duplication: BSU's exclusion ledger recorded that its dynamic card lists
|
|
22
|
+
* "cannot be split into static widget keys without a dynamic-widget
|
|
23
|
+
* registration mechanism", which is FALSE — `buildCanvasCardLayout` derives
|
|
24
|
+
* the widget dict from the children at render time, so a `.map()` over a
|
|
25
|
+
* runtime-variable list already produces N independent grid items. crm7 ships
|
|
26
|
+
* exactly that against a Postgres-backed 0..N array. The objection was an
|
|
27
|
+
* artifact of the primitive not being present, not a real constraint.
|
|
28
|
+
* Tracked as crm7#412 / bsuite#1995.
|
|
29
|
+
*
|
|
30
|
+
* Usage
|
|
31
|
+
* -----
|
|
32
|
+
* <DraggableCardPage pageKey="/people/:id" layoutVersion={3}>
|
|
33
|
+
* <CanvasCard cardKey="header" h={4}>
|
|
34
|
+
* <PageHeader heading="Person" />
|
|
35
|
+
* </CanvasCard>
|
|
36
|
+
* <CanvasCard cardKey="personalInfo" h={12}>
|
|
37
|
+
* <Card>…</Card>
|
|
38
|
+
* </CanvasCard>
|
|
39
|
+
* {isTraining && (
|
|
40
|
+
* <CanvasCard cardKey="trainingContract" h={16}>
|
|
41
|
+
* <Card>…</Card>
|
|
42
|
+
* </CanvasCard>
|
|
43
|
+
* )}
|
|
44
|
+
* </DraggableCardPage>
|
|
45
|
+
*
|
|
46
|
+
* Heights are 32px row units. Falsy children are skipped, so conditional cards
|
|
47
|
+
* collapse cleanly without leaving empty slots. Fragments are flattened.
|
|
48
|
+
*
|
|
49
|
+
* What this component deliberately does NOT do
|
|
50
|
+
* --------------------------------------------
|
|
51
|
+
* - **Permissions.** Gating is an app concern with an app-specific permission
|
|
52
|
+
* vocabulary. Wrap this component in the app's own gate.
|
|
53
|
+
* - **`compactType`.** It is NOT accepted, on purpose. `usePageGridLayout`
|
|
54
|
+
* selects `verticalCompactor` unconditionally for BOTH view and edit mode,
|
|
55
|
+
* and that is load-bearing: the previous edit-mode config
|
|
56
|
+
* (`preventCollision: true` with `compactType: null`) made react-grid-layout
|
|
57
|
+
* full-revert any gesture landing on an occupied cell, and `onDragStop` only
|
|
58
|
+
* emits when the layout changed — so a reverted gesture emitted nothing and
|
|
59
|
+
* NOTHING COULD EVER SAVE (bsuite#1588). Accepting a `compactType` prop here
|
|
60
|
+
* would re-open that hole. crm7's local wrapper passed `compactType ?? null`
|
|
61
|
+
* through a cast to a component that does not read it; the prop was dead,
|
|
62
|
+
* and dead is the correct state for it.
|
|
63
|
+
*/
|
|
64
|
+
import { useMemo } from 'react';
|
|
65
|
+
import { PageGridLayout as DefaultPageGridLayout } from './PageGridLayout.js';
|
|
66
|
+
import { buildCanvasCardLayout } from './canvasCardLayout.js';
|
|
67
|
+
import { CanvasCard } from './CanvasCard.js';
|
|
68
|
+
// Re-exported so a consumer can import both from one module, matching the
|
|
69
|
+
// long-standing crm7 import shape used by ~284 call sites.
|
|
70
|
+
export { CanvasCard };
|
|
71
|
+
/**
|
|
72
|
+
* `process.env.NODE_ENV` rather than `import.meta.env`: every bundler in the
|
|
73
|
+
* estate (Vite, Next, Rollup) statically replaces it, and the `typeof` guard
|
|
74
|
+
* keeps the expression safe in a raw browser ESM context where `process` does
|
|
75
|
+
* not exist. This package must not assume a Vite consumer.
|
|
76
|
+
*/
|
|
77
|
+
function isDevelopment() {
|
|
78
|
+
return (typeof process !== 'undefined' &&
|
|
79
|
+
process.env?.NODE_ENV !== 'production');
|
|
80
|
+
}
|
|
81
|
+
function defaultOnDroppedChildren(dropped, pageKey) {
|
|
82
|
+
if (!isDevelopment())
|
|
83
|
+
return;
|
|
84
|
+
// console.error, not warn: React uses error for "you have written something
|
|
85
|
+
// that will not work", and this is that.
|
|
86
|
+
//
|
|
87
|
+
// Deliberately NOT a thrown error. Hundreds of pages use this component and
|
|
88
|
+
// a throw would turn a missing dialog into a blank page for a shape that has
|
|
89
|
+
// been shipping for months — trading a partial page for no page at all is
|
|
90
|
+
// not an improvement for the user in front of it.
|
|
91
|
+
console.error(`[DraggableCardPage] ${dropped.length} child(ren) of "${pageKey}" were DISCARDED and will not render: ${dropped.join(', ')}. ` +
|
|
92
|
+
`Only <CanvasCard> children (optionally inside a Fragment) occupy a grid slot — everything else is dropped. ` +
|
|
93
|
+
`If this is a dialog or a modal, its trigger will appear to do nothing: the button renders, the handler runs, and the element it toggles was never mounted. ` +
|
|
94
|
+
`Wrap it in <CanvasCard cardKey="…"> or move it outside <DraggableCardPage>.`);
|
|
95
|
+
}
|
|
96
|
+
export function DraggableCardPage({ layoutEpoch = 0, gridComponent, onDroppedChildren, children, ...pageProps }) {
|
|
97
|
+
const { widgets, layouts, dropped } = useMemo(() => buildCanvasCardLayout(children), [children]);
|
|
98
|
+
if (dropped.length > 0) {
|
|
99
|
+
(onDroppedChildren ?? defaultOnDroppedChildren)(dropped, pageProps.pageKey);
|
|
100
|
+
}
|
|
101
|
+
// Ensure a sensible default 12-column grid when the consumer does not
|
|
102
|
+
// override. Without this, PageGridLayout's fallback collapses to cols=1 at
|
|
103
|
+
// every breakpoint and the page renders as a single vertical column with no
|
|
104
|
+
// horizontal canvas area — the user-reported "drag cards into these white
|
|
105
|
+
// spaces" surface had no white spaces because the grid was 1 column wide.
|
|
106
|
+
const defaultCols = pageProps.defaultCols ?? 12;
|
|
107
|
+
const Grid = gridComponent ?? DefaultPageGridLayout;
|
|
108
|
+
return (_jsx(Grid, { ...pageProps, layoutVersion: (pageProps.layoutVersion ?? 1) + layoutEpoch, defaultCols: defaultCols, widgets: widgets, defaultLayouts: layouts }));
|
|
109
|
+
}
|
|
110
|
+
DraggableCardPage.displayName = 'DraggableCardPage';
|
|
@@ -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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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"}
|
|
@@ -0,0 +1,289 @@
|
|
|
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
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
40
|
+
import { join, relative, sep } from 'node:path';
|
|
41
|
+
const DEFAULT_GRID_PRIMITIVES = [
|
|
42
|
+
'CanvasCard',
|
|
43
|
+
'DraggableCardPage',
|
|
44
|
+
'PageGridLayout',
|
|
45
|
+
'usePageGridLayout',
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Strip block comments, line comments and JSX comments before matching.
|
|
49
|
+
*
|
|
50
|
+
* Three of the five hand-rolled copies matched against raw file content, which
|
|
51
|
+
* produced two opposite errors at once: a commented-out `<Card/><Card/>`
|
|
52
|
+
* example false-POSITIVES as glue, and a file that merely MENTIONS
|
|
53
|
+
* `CanvasCard` in a TODO comment false-NEGATIVES as "already gridded" and is
|
|
54
|
+
* skipped entirely. Stripping first fixes both.
|
|
55
|
+
*/
|
|
56
|
+
export function stripComments(source) {
|
|
57
|
+
return source
|
|
58
|
+
.replace(/\{\s*\/\*[\s\S]*?\*\/\s*\}/g, '') // {/* JSX comment */}
|
|
59
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // /* block */
|
|
60
|
+
.replace(/(^|[^:])\/\/[^\n]*/g, '$1'); // // line, but not http://
|
|
61
|
+
}
|
|
62
|
+
function tagPattern(tags) {
|
|
63
|
+
// `[\s/>]` not `[\s>]`: `<Card/>` with no space before the slash was missed
|
|
64
|
+
// by three of the five copies. The trailing alternation keeps `<Card>` and
|
|
65
|
+
// `<Card ...>` matching too.
|
|
66
|
+
return new RegExp(`<(${tags.join('|')})[\\s/>]`, 'g');
|
|
67
|
+
}
|
|
68
|
+
function countCardTags(source, config) {
|
|
69
|
+
let count = (source.match(tagPattern(config.cardTags)) ?? []).length;
|
|
70
|
+
for (const re of config.classSurfaces ?? []) {
|
|
71
|
+
const global = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g');
|
|
72
|
+
count += (source.match(global) ?? []).length;
|
|
73
|
+
}
|
|
74
|
+
return count;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Does this source render cards from a `.map()`?
|
|
78
|
+
*
|
|
79
|
+
* The non-greedy `[\s\S]*?` form is deliberate. Three copies used
|
|
80
|
+
* `\.map\s*\([^)]*\)\s*=>` which requires a PARENTHESISED parameter, so
|
|
81
|
+
* `items.map(item => <Card/>)` — a bare param with an expression body — was
|
|
82
|
+
* missed. That gap is currently dormant across the estate but it is a landmine,
|
|
83
|
+
* not a theoretical concern.
|
|
84
|
+
*/
|
|
85
|
+
function hasMappedCards(source, config) {
|
|
86
|
+
const tags = config.cardTags.join('|');
|
|
87
|
+
if (new RegExp(`\\.map\\s*\\([\\s\\S]*?=>\\s*\\(?\\s*<(${tags})[\\s/>]`).test(source) ||
|
|
88
|
+
new RegExp(`\\.map\\s*\\([\\s\\S]*?\\{[\\s\\S]*?return\\s*\\(?\\s*<(${tags})[\\s/>]`).test(source))
|
|
89
|
+
return true;
|
|
90
|
+
// A mapped CLASS surface is still a mapped card.
|
|
91
|
+
//
|
|
92
|
+
// This was missed on the first pass: the tag alternation above is built from
|
|
93
|
+
// `cardTags` only, so `.map(api => <div className="glass-card">…)` produced N
|
|
94
|
+
// cards inside one grid slot and read as clean. BSU's `Government.tsx#apis`
|
|
95
|
+
// is exactly that shape, and only its hand-maintained ledger knew — which is
|
|
96
|
+
// the failure mode this whole module exists to end. `countCardTags` already
|
|
97
|
+
// counts class surfaces; the mapped check has to as well or the two disagree.
|
|
98
|
+
for (const re of config.classSurfaces ?? []) {
|
|
99
|
+
const src = re.source;
|
|
100
|
+
if (new RegExp(`\\.map\\s*\\([\\s\\S]*?=>\\s*\\(?\\s*<[^>]*?${src}`).test(source) ||
|
|
101
|
+
new RegExp(`\\.map\\s*\\([\\s\\S]*?\\{[\\s\\S]*?return\\s*\\(?\\s*<[^>]*?${src}`).test(source))
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
/** Extract the body of every `<CanvasCard …> … </CanvasCard>` block. */
|
|
107
|
+
function extractCanvasCardBlocks(source) {
|
|
108
|
+
const blocks = [];
|
|
109
|
+
const re = /<CanvasCard[\s>][\s\S]*?<\/CanvasCard>/g;
|
|
110
|
+
let m;
|
|
111
|
+
while ((m = re.exec(source)) !== null)
|
|
112
|
+
blocks.push(m[0]);
|
|
113
|
+
return blocks;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Extract each value of an inline `widgets={{ key: <jsx/> }}` object literal.
|
|
117
|
+
* Brace-balanced rather than regex-terminated, because a widget body contains
|
|
118
|
+
* arbitrary nested JSX and braces.
|
|
119
|
+
*/
|
|
120
|
+
function extractWidgetEntries(source) {
|
|
121
|
+
const out = [];
|
|
122
|
+
const start = /widgets=\{\{/g;
|
|
123
|
+
let m;
|
|
124
|
+
while ((m = start.exec(source)) !== null) {
|
|
125
|
+
let depth = 2;
|
|
126
|
+
let i = m.index + m[0].length;
|
|
127
|
+
const objStart = i;
|
|
128
|
+
while (i < source.length && depth > 0) {
|
|
129
|
+
if (source[i] === '{')
|
|
130
|
+
depth++;
|
|
131
|
+
else if (source[i] === '}')
|
|
132
|
+
depth--;
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
135
|
+
const obj = source.slice(objStart, i - 2);
|
|
136
|
+
// Split top-level `key:` entries at depth 0.
|
|
137
|
+
let d = 0;
|
|
138
|
+
let keyStart = 0;
|
|
139
|
+
let currentKey = null;
|
|
140
|
+
let bodyStart = 0;
|
|
141
|
+
for (let j = 0; j < obj.length; j++) {
|
|
142
|
+
const c = obj[j];
|
|
143
|
+
if (c === '{' || c === '(' || c === '[')
|
|
144
|
+
d++;
|
|
145
|
+
else if (c === '}' || c === ')' || c === ']')
|
|
146
|
+
d--;
|
|
147
|
+
else if (c === ':' && d === 0 && currentKey === null) {
|
|
148
|
+
currentKey = obj.slice(keyStart, j).trim().replace(/['"]/g, '');
|
|
149
|
+
bodyStart = j + 1;
|
|
150
|
+
}
|
|
151
|
+
else if (c === ',' && d === 0 && currentKey !== null) {
|
|
152
|
+
out.push({ key: currentKey, body: obj.slice(bodyStart, j) });
|
|
153
|
+
currentKey = null;
|
|
154
|
+
keyStart = j + 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (currentKey !== null)
|
|
158
|
+
out.push({ key: currentKey, body: obj.slice(bodyStart) });
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
function walk(dir, extensions, acc) {
|
|
163
|
+
for (const entry of readdirSync(dir)) {
|
|
164
|
+
if (entry === 'node_modules' || entry === 'dist' || entry === '.git')
|
|
165
|
+
continue;
|
|
166
|
+
const full = join(dir, entry);
|
|
167
|
+
const st = statSync(full);
|
|
168
|
+
if (st.isDirectory())
|
|
169
|
+
walk(full, extensions, acc);
|
|
170
|
+
else if (extensions.some((e) => entry.endsWith(e)) && !/\.(test|spec)\./.test(entry))
|
|
171
|
+
acc.push(full);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function scanCardSurfaces(config) {
|
|
175
|
+
const extensions = config.extensions ?? ['.tsx'];
|
|
176
|
+
const gridPrimitives = config.gridPrimitives ?? DEFAULT_GRID_PRIMITIVES;
|
|
177
|
+
const findings = [];
|
|
178
|
+
const files = [];
|
|
179
|
+
const scanRootsResolved = [];
|
|
180
|
+
// D-92: assert the inputs were present BEFORE reporting any verdict. A
|
|
181
|
+
// missing scan root must fail loudly, not produce a confident zero.
|
|
182
|
+
for (const root of config.scanRoots) {
|
|
183
|
+
const abs = join(config.projectRoot, root);
|
|
184
|
+
if (!existsSync(abs))
|
|
185
|
+
throw new Error(`[cardSurfaceScanner] scan root "${root}" does not exist at ${abs}. ` +
|
|
186
|
+
`A scanner that silently skips a missing root reports a false pass, which is worse than a failure.`);
|
|
187
|
+
const before = files.length;
|
|
188
|
+
walk(abs, extensions, files);
|
|
189
|
+
if (files.length === before)
|
|
190
|
+
throw new Error(`[cardSurfaceScanner] scan root "${root}" matched ZERO files with extensions ${extensions.join(', ')}. ` +
|
|
191
|
+
`Refusing to report a verdict from an empty scan.`);
|
|
192
|
+
scanRootsResolved.push(root);
|
|
193
|
+
}
|
|
194
|
+
// Retired single-widget primitives must not exist and must not be imported.
|
|
195
|
+
for (const retired of config.retiredPrimitivePaths ?? []) {
|
|
196
|
+
const abs = join(config.projectRoot, retired);
|
|
197
|
+
if (existsSync(abs))
|
|
198
|
+
findings.push({
|
|
199
|
+
file: retired,
|
|
200
|
+
idiom: 'retired-primitive',
|
|
201
|
+
detail: `${retired} still exists. It builds ONE grid item for the whole page, so every card on it ` +
|
|
202
|
+
`moves as a single block — the exact defect this contract exists to prevent.`,
|
|
203
|
+
});
|
|
204
|
+
const base = retired.split(sep).pop()?.replace(/\.tsx?$/, '');
|
|
205
|
+
if (base) {
|
|
206
|
+
for (const file of files) {
|
|
207
|
+
const src = stripComments(readFileSync(file, 'utf8'));
|
|
208
|
+
if (new RegExp(`\\b${base}\\b`).test(src) && !file.endsWith(retired))
|
|
209
|
+
findings.push({
|
|
210
|
+
file: relative(config.projectRoot, file),
|
|
211
|
+
idiom: 'retired-primitive',
|
|
212
|
+
detail: `imports or references the retired primitive ${base}`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (const file of files) {
|
|
218
|
+
const rel = relative(config.projectRoot, file);
|
|
219
|
+
const raw = readFileSync(file, 'utf8');
|
|
220
|
+
const src = stripComments(raw);
|
|
221
|
+
// --- glued widget: 2+ cards inside ONE grid slot -----------------------
|
|
222
|
+
const slots = [
|
|
223
|
+
...extractCanvasCardBlocks(src).map((b, i) => ({ label: `CanvasCard#${i + 1}`, body: b })),
|
|
224
|
+
...extractWidgetEntries(src).map((e) => ({ label: `widgets.${e.key}`, body: e.body })),
|
|
225
|
+
];
|
|
226
|
+
for (const slot of slots) {
|
|
227
|
+
const n = countCardTags(slot.body, config);
|
|
228
|
+
const mapped = hasMappedCards(slot.body, config);
|
|
229
|
+
if (n > 1 || mapped) {
|
|
230
|
+
if (config.gluedExclusions?.[rel])
|
|
231
|
+
continue;
|
|
232
|
+
findings.push({
|
|
233
|
+
file: rel,
|
|
234
|
+
idiom: 'glued-widget',
|
|
235
|
+
cardCount: n,
|
|
236
|
+
detail: `${slot.label} packs ${mapped ? 'a .map() of cards' : `${n} card-like components`} into one grid slot. ` +
|
|
237
|
+
`Dragging any one of them moves them all.`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// --- ungridded multi-card: the check three of five ports DROPPED -------
|
|
242
|
+
// Gate on the COMMENT-STRIPPED source so a TODO mentioning CanvasCard does
|
|
243
|
+
// not mask a page that never actually reaches the grid.
|
|
244
|
+
const usesGrid = gridPrimitives.some((p) => new RegExp(`\\b${p}\\b`).test(src));
|
|
245
|
+
if (!usesGrid) {
|
|
246
|
+
const n = countCardTags(src, config);
|
|
247
|
+
if (n > 1 && !config.ungriddedExclusions?.[rel]) {
|
|
248
|
+
findings.push({
|
|
249
|
+
file: rel,
|
|
250
|
+
idiom: 'ungridded-multi-card',
|
|
251
|
+
cardCount: n,
|
|
252
|
+
detail: `renders ${n} card-like surfaces and never reaches a grid primitive, so none of them is draggable at all. ` +
|
|
253
|
+
`This is the idiom crm7's scanner calls "the actual root cause of the operator's platform-wide complaint".`,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// --- autoHeight opt-out + clip invariant -------------------------------
|
|
258
|
+
const optOut = /autoHeight\s*[:=]\s*\{?\s*false/g;
|
|
259
|
+
if (optOut.test(src) && !(config.autoHeightEscapeHatch?.test(src) ?? false)) {
|
|
260
|
+
findings.push({
|
|
261
|
+
file: rel,
|
|
262
|
+
idiom: 'autoheight-optout',
|
|
263
|
+
detail: `opts out of autoHeight. That is reserved for genuinely virtualized/windowed lists that own their own scroll; ` +
|
|
264
|
+
`an ordinary table must expand so every row is visible.`,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// A card that opts out AND seeds h<=2 (64px) cannot show its content.
|
|
268
|
+
if (/autoHeight\s*[:=]\s*\{?\s*false/.test(src) && /\bh=\{?[12]\}?/.test(src)) {
|
|
269
|
+
findings.push({
|
|
270
|
+
file: rel,
|
|
271
|
+
idiom: 'clipped-card',
|
|
272
|
+
detail: `autoHeight={false} with h<=2 (<=64px) — the card is guaranteed to clip its content.`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Stale ledger entries: a claim that is no longer true is a claim that will
|
|
277
|
+
// be trusted next time. An exclusions ledger is a claim, not a fact.
|
|
278
|
+
const hit = new Set(findings.map((f) => `${f.idiom}:${f.file}`));
|
|
279
|
+
const staleExclusions = [
|
|
280
|
+
...Object.keys(config.gluedExclusions ?? {}).filter((f) => !hit.has(`glued-widget:${f}`) && !hit.has(`autoheight-optout:${f}`)),
|
|
281
|
+
...Object.keys(config.ungriddedExclusions ?? {}).filter((f) => !hit.has(`ungridded-multi-card:${f}`)),
|
|
282
|
+
];
|
|
283
|
+
const summary = `scanned ${files.length} files across ${scanRootsResolved.length} root(s) [${scanRootsResolved.join(', ')}] ` +
|
|
284
|
+
`with cardTags [${config.cardTags.join(', ')}]` +
|
|
285
|
+
`${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 };
|
|
289
|
+
}
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { scanCardSurfaces, stripComments } from './cardSurfaceScanner.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsuite/page-builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Shared BSuite responsive page-builder grid and layout persistence primitives",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
"import": "./dist/index.js",
|
|
24
24
|
"types": "./dist/index.d.ts"
|
|
25
25
|
},
|
|
26
|
-
"./styles.css": "./src/styles/react-grid-layout-overrides.css"
|
|
26
|
+
"./styles.css": "./src/styles/react-grid-layout-overrides.css",
|
|
27
|
+
"./scanner": {
|
|
28
|
+
"import": "./dist/scanner/index.js",
|
|
29
|
+
"types": "./dist/scanner/index.d.ts"
|
|
30
|
+
}
|
|
27
31
|
},
|
|
28
32
|
"scripts": {
|
|
29
33
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -70,6 +74,7 @@
|
|
|
70
74
|
"react-resizable": "^4.0.1",
|
|
71
75
|
"typescript": "~6.0.3",
|
|
72
76
|
"vite": "^8.0.16",
|
|
73
|
-
"vitest": "^4.1.8"
|
|
77
|
+
"vitest": "^4.1.8",
|
|
78
|
+
"@types/node": "^25.9.2"
|
|
74
79
|
}
|
|
75
80
|
}
|