@bsuite/page-builder 2.4.1 → 2.6.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.
@@ -0,0 +1,68 @@
1
+ import type { ReactNode } from 'react';
2
+ /** The subset of a `custom_pages` row this component needs. Apps may pass more. */
3
+ export interface CustomPageLike {
4
+ title: string;
5
+ description?: string | null;
6
+ layout?: unknown;
7
+ }
8
+ /**
9
+ * A stored layout. Deliberately structural and permissive: this package does not
10
+ * own the field vocabulary, and a layout carrying keys it does not recognise must
11
+ * render what it can rather than throw.
12
+ */
13
+ export interface StoredLayoutSection {
14
+ id?: string;
15
+ title?: string;
16
+ description?: string | null;
17
+ columns?: number;
18
+ fields?: Array<{
19
+ fieldKey?: string;
20
+ label?: string;
21
+ [k: string]: unknown;
22
+ }>;
23
+ }
24
+ export interface StoredLayout {
25
+ sections?: StoredLayoutSection[];
26
+ tabs?: Array<{
27
+ id?: string;
28
+ title?: string;
29
+ sections?: StoredLayoutSection[];
30
+ }>;
31
+ }
32
+ export interface CustomPageViewProps {
33
+ /** The already-fetched row. `null` means resolved-and-absent; `undefined` means not yet known. */
34
+ page: CustomPageLike | null | undefined;
35
+ isLoading?: boolean;
36
+ error?: {
37
+ message: string;
38
+ } | null;
39
+ /**
40
+ * What to do when the page resolves to nothing.
41
+ *
42
+ * `silent` (the default) is for an EMBEDDED mount that sits beside a static
43
+ * layout: a miss must render nothing rather than an error card in the middle of
44
+ * a working page.
45
+ *
46
+ * `message` is for a route where the custom page IS the whole page. Silence
47
+ * there is a blank screen, which reads as broken.
48
+ */
49
+ onMissing?: 'silent' | 'message';
50
+ /** The per-app widget catalogue. Omit it and the structural default is used. */
51
+ renderLayout?: (layout: StoredLayout, page: CustomPageLike) => ReactNode;
52
+ renderLoading?: () => ReactNode;
53
+ renderError?: (message: string) => ReactNode;
54
+ renderEmpty?: () => ReactNode;
55
+ className?: string;
56
+ }
57
+ /** A layout with no sections and no tabs has nothing to draw. */
58
+ export declare function layoutIsEmpty(layout: unknown): boolean;
59
+ /** Every section in a layout, whether it sits at the top level or inside a tab. */
60
+ export declare function flattenSections(layout: StoredLayout): StoredLayoutSection[];
61
+ /**
62
+ * The readable default. NOT a widget catalogue and not pretending to be one — it
63
+ * renders the SHAPE a page was given, so an app without its own catalogue shows
64
+ * something a person can read instead of `JSON.stringify`.
65
+ */
66
+ export declare function renderStructuredLayout(layout: StoredLayout): ReactNode;
67
+ export declare function CustomPageView({ page, isLoading, error, onMissing, renderLayout, renderLoading, renderError, renderEmpty, className, }: CustomPageViewProps): import("react").JSX.Element | null;
68
+ //# sourceMappingURL=CustomPageView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomPageView.d.ts","sourceRoot":"","sources":["../src/CustomPageView.tsx"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;CAC7E;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACjC,IAAI,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAA;KAAE,CAAC,CAAC;CACjF;AAED,MAAM,WAAW,mBAAmB;IAClC,kGAAkG;IAClG,IAAI,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,CAAC;IACxC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACnC;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IACjC,gFAAgF;IAChF,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,cAAc,KAAK,SAAS,CAAC;IACzE,aAAa,CAAC,EAAE,MAAM,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC;IAC7C,WAAW,CAAC,EAAE,MAAM,SAAS,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAOtD;AAED,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,mBAAmB,EAAE,CAM3E;AASD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS,CAwCtE;AAED,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,SAAiB,EACjB,KAAY,EACZ,SAAoB,EACpB,YAAY,EACZ,aAAa,EACb,WAAW,EACX,WAAW,EACX,SAAS,GACV,EAAE,mBAAmB,sCAyDrB"}
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /** A layout with no sections and no tabs has nothing to draw. */
3
+ export function layoutIsEmpty(layout) {
4
+ if (!layout || typeof layout !== 'object')
5
+ return true;
6
+ const l = layout;
7
+ const sections = Array.isArray(l.sections) ? l.sections : [];
8
+ const tabs = Array.isArray(l.tabs) ? l.tabs : [];
9
+ if (tabs.length > 0)
10
+ return false;
11
+ return sections.length === 0;
12
+ }
13
+ /** Every section in a layout, whether it sits at the top level or inside a tab. */
14
+ export function flattenSections(layout) {
15
+ const out = Array.isArray(layout.sections) ? [...layout.sections] : [];
16
+ for (const tab of Array.isArray(layout.tabs) ? layout.tabs : []) {
17
+ for (const s of Array.isArray(tab.sections) ? tab.sections : [])
18
+ out.push(s);
19
+ }
20
+ return out;
21
+ }
22
+ /** Turn `given_name` into `Given name` when a field carries no explicit label. */
23
+ function humanise(key) {
24
+ const spaced = key.replace(/[_-]+/g, ' ').trim();
25
+ if (spaced.length === 0)
26
+ return key;
27
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
28
+ }
29
+ /**
30
+ * The readable default. NOT a widget catalogue and not pretending to be one — it
31
+ * renders the SHAPE a page was given, so an app without its own catalogue shows
32
+ * something a person can read instead of `JSON.stringify`.
33
+ */
34
+ export function renderStructuredLayout(layout) {
35
+ const sections = flattenSections(layout);
36
+ return (_jsx("div", { className: "space-y-6", children: sections.map((section, si) => {
37
+ const fields = Array.isArray(section.fields) ? section.fields : [];
38
+ return (_jsxs("section", { className: "space-y-2", children: [section.title ? (_jsx("h2", { className: "text-sm font-semibold tracking-tight text-foreground", children: section.title })) : null, section.description ? (_jsx("p", { className: "text-sm text-muted-foreground", children: section.description })) : null, fields.length > 0 ? (_jsx("dl", { className: "grid gap-x-6 gap-y-2 sm:grid-cols-2", children: fields.map((field, fi) => {
39
+ const key = typeof field.fieldKey === 'string' ? field.fieldKey : `field-${fi}`;
40
+ const label = typeof field.label === 'string' && field.label.length > 0
41
+ ? field.label
42
+ : humanise(key);
43
+ return (_jsxs("div", { className: "min-w-0", children: [_jsx("dt", { className: "truncate text-xs text-muted-foreground", children: label }), _jsx("dd", { className: "truncate text-sm text-foreground", children: "\u2014" })] }, key));
44
+ }) })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "No fields in this section." }))] }, section.id ?? `section-${si}`));
45
+ }) }));
46
+ }
47
+ export function CustomPageView({ page, isLoading = false, error = null, onMissing = 'silent', renderLayout, renderLoading, renderError, renderEmpty, className, }) {
48
+ if (isLoading) {
49
+ if (renderLoading)
50
+ return _jsx(_Fragment, { children: renderLoading() });
51
+ // Silence while loading is right for an embedded mount: a spinner in the
52
+ // middle of a page that has already drawn reads as the page being broken.
53
+ return onMissing === 'message' ? (_jsx("div", { className: "flex items-center justify-center py-16 text-sm text-muted-foreground", children: "Loading\u2026" })) : null;
54
+ }
55
+ if (error) {
56
+ if (onMissing !== 'message')
57
+ return null;
58
+ if (renderError)
59
+ return _jsx(_Fragment, { children: renderError(error.message) });
60
+ return (_jsxs("div", { className: "mx-auto max-w-lg rounded-lg border border-destructive/40 p-4", children: [_jsx("p", { className: "text-sm font-medium text-destructive", children: "This page could not be loaded." }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: error.message })] }));
61
+ }
62
+ if (!page) {
63
+ if (onMissing !== 'message')
64
+ return null;
65
+ return (_jsx("div", { className: "mx-auto max-w-lg rounded-lg border border-dashed p-6 text-center", children: _jsx("p", { className: "text-sm font-medium", children: "Page not found" }) }));
66
+ }
67
+ const layout = (page.layout ?? {});
68
+ const empty = layoutIsEmpty(layout);
69
+ return (_jsxs("section", { className: className ?? 'space-y-4', children: [_jsxs("header", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: page.title }), page.description ? (_jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: page.description })) : null] }), empty
70
+ ? renderEmpty
71
+ ? renderEmpty()
72
+ : (_jsx("div", { className: "rounded-lg border-2 border-dashed py-12 text-center text-muted-foreground", children: _jsx("p", { className: "text-sm", children: "This page has no layout configured yet." }) }))
73
+ : renderLayout
74
+ ? renderLayout(layout, page)
75
+ : renderStructuredLayout(layout)] }));
76
+ }
@@ -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;AAghB7F,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"}
1
+ {"version":3,"file":"PageGridLayout.d.ts","sourceRoot":"","sources":["../src/PageGridLayout.tsx"],"names":[],"mappings":"AAEA,OAAO,KAQN,MAAM,OAAO,CAAC;AAGf,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAsBxC,OAAO,KAAK,EAAe,mBAAmB,EAA4B,MAAM,YAAY,CAAC;AAmmB7F,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,OAAO,EACP,UAAU,EACV,SAAS,EAMT,WAAkB,EAClB,aAAsC,EACtC,QAAQ,EACR,iBAAiB,EAKjB,UAAkB,EAClB,yBAAiE,EACjE,kBAAkB,EAClB,sBAAsB,EACtB,mBAAmB,EACnB,+BAA6E,EAC7E,wBAAwB,EACxB,4BAA4B,EAC5B,4BAA4B,GAC7B,EAAE,mBAAmB,qBAs+BrB"}
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ElementScopeProvider } from './elementScope.js';
2
3
  import { ArrowDown, ArrowUp, ChevronDown, ChevronUp, Eye, EyeOff, Layers, LayoutGrid, Lock, Plus, RotateCcw, Save, Settings2, Unlock } from 'lucide-react';
3
4
  import React, { startTransition, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
4
5
  import { Responsive } from 'react-grid-layout';
@@ -6,6 +7,7 @@ import { gridBounds, minMaxSize, minSize } from 'react-grid-layout/core';
6
7
  import 'react-grid-layout/css/styles.css';
7
8
  import 'react-resizable/css/styles.css';
8
9
  import { computeAutoHeightRows } from './autoHeight.js';
10
+ import { BORDER_STYLES, BORDER_TONES, BORDER_WIDTH_RANGE, DEFAULT_CARD_STYLE, PADDING_RANGE, RADIUS_RANGE, describeCardStyle, isDefaultCardStyle, normaliseCardStyle, toCssVars, } from './cardStyle.js';
9
11
  import { defaultPreferenceAdapter } from './preferences.js';
10
12
  import { isRelationshipWritable } from './relationshipCatalog.js';
11
13
  import { usePageGridLayout } from './usePageGridLayout.js';
@@ -139,7 +141,7 @@ function warnOnceAboutCollapsedContent(el) {
139
141
  + '(an aspect ratio, a px/rem height, or min-height), or pass autoHeight={false} on '
140
142
  + 'this card so it renders into the constrained scrollable wrapper instead.');
141
143
  }
142
- 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) {
144
+ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, isEditing, label, pageKey, onHide, autoHeight, onAutoHeightChange, chrome = false, cardStyleVars, children: injectedChildren, className: injectedClassName, style: injectedStyle, ...rest }, ref) {
143
145
  // Auto-height measurement (blueprint amendment A1). `measureRef` wraps
144
146
  // `content` with NO height constraint of its own — its parent has
145
147
  // `overflow-hidden` + `flex-1 min-h-0` (clips *painting* to the current
@@ -149,6 +151,16 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
149
151
  // intrinsic size, independent of however many rows the item currently
150
152
  // occupies — the property that makes the auto-height loop converge to
151
153
  // a fixed point instead of oscillating (see autoHeight.ts).
154
+ /*
155
+ * The card's identity, handed down so anything inside it can be addressed.
156
+ *
157
+ * A Context.Provider renders NO DOM NODE, so this cannot affect the
158
+ * ResizeObserver measurement below — which matters, because a trailing
159
+ * margin once collapsed THROUGH the autoHeight measure wrapper and cost 32px
160
+ * of card overflow across 70 sites. An extra element here would have been a
161
+ * real risk; a provider is not one.
162
+ */
163
+ const scopedContent = (_jsx(ElementScopeProvider, { scope: { pageKey, cardKey: id, cardLabel: label, isEditing }, children: content }));
152
164
  const measureRef = useRef(null);
153
165
  const lastReportedRowsRef = useRef(null);
154
166
  const measureRafRef = useRef(null);
@@ -280,7 +292,19 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
280
292
  // `--radius-card` defaults to today's value (1.5rem =
281
293
  // rounded-3xl), so this is byte-identical until a tenant sets
282
294
  // one.
283
- cn('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)]',
295
+ cn(
296
+ // Border width, style and colour read custom properties
297
+ // whose fallbacks are byte-identical to the `border
298
+ // border-border` they replace, so an operator who has never
299
+ // opened the card editor sees exactly the previous CSS.
300
+ // The shadow deliberately keeps its ORIGINAL classes. An
301
+ // elevation choice arrives as an inline `boxShadow`, which
302
+ // beats the class without needing a fallback that restates
303
+ // `shadow-sm`. Writing that fallback by hand was the first
304
+ // attempt and it was wrong: elev-1 is CLOSE to shadow-sm
305
+ // but not equal, so it would have silently restyled every
306
+ // card on 305 pages the day this shipped.
307
+ 'w-full rounded-[var(--radius-card,1.5rem)] transition-all flex flex-col bg-card border-[length:var(--card-border-width,1px)] border-[color:var(--card-border-color,var(--border))] [border-style:var(--card-border-style,solid)] shadow-sm dark:shadow-[var(--glow-card,none)]',
284
308
  // THE DOUBLED BOTTOM BORDER, AND WHY THE ARITHMETIC COULD NEVER FIX IT.
285
309
  //
286
310
  // computeAutoHeightRows uses Math.ceil to round content height up to
@@ -305,7 +329,24 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
305
329
  // height and the chrome should fill it.
306
330
  autoHeight ? 'h-fit' : 'h-full')
307
331
  : // Chrome OFF: layout only. Identical box, no paint.
308
- cn('w-full transition-all flex flex-col', autoHeight ? 'h-fit' : 'h-full'), style: { contain: 'layout style' }, children: _jsx("div", { className: cn('min-h-0', autoHeight ? 'flex-none overflow-visible' : 'flex-1 overflow-auto'), children: autoHeight ? _jsx("div", { ref: measureRef, children: content }) : content }) })] }), injectedChildren] }));
332
+ cn('w-full transition-all flex flex-col', autoHeight ? 'h-fit' : 'h-full'),
333
+ /*
334
+ * `cardStyleVars` carries ONLY the properties the operator changed
335
+ * (see cardStyle.ts). An untouched page spreads an empty object, so
336
+ * this attribute is identical to what it was before the card editor
337
+ * existed. `boxShadow` is set as a real declaration rather than a
338
+ * custom property because it has to beat the `shadow-sm` class.
339
+ */
340
+ style: {
341
+ contain: 'layout style',
342
+ ...cardStyleVars,
343
+ ...(cardStyleVars && '--card-shadow' in cardStyleVars
344
+ ? { boxShadow: cardStyleVars['--card-shadow'] }
345
+ : null),
346
+ ...(cardStyleVars && '--card-padding' in cardStyleVars
347
+ ? { padding: cardStyleVars['--card-padding'] }
348
+ : null),
349
+ }, children: _jsx("div", { className: cn('min-h-0', autoHeight ? 'flex-none overflow-visible' : 'flex-1 overflow-auto'), children: autoHeight ? (_jsx("div", { ref: measureRef, className: "flow-root", children: scopedContent })) : (scopedContent) }) })] }), injectedChildren] }));
309
350
  }));
310
351
  export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, widgets, widgetMeta, className,
311
352
  // Resize default flipped 2026-05-12: rgl v2 dropped auto-handle rendering
@@ -398,6 +439,17 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
398
439
  const [extraRelationshipWidgetConfigs, setExtraRelationshipWidgetConfigs] = useState({});
399
440
  const { value: layerNames, setValue: setLayerNames } = (preferenceAdapter ?? defaultPreferenceAdapter)(`page:${pageKey}_grid_layer_names`, {});
400
441
  const { value: hiddenLayerIds, setValue: setHiddenLayerIds } = (preferenceAdapter ?? defaultPreferenceAdapter)(`page:${pageKey}_grid_hidden_layers`, {});
442
+ /*
443
+ * Card appearance, per page, persisted beside the layout.
444
+ *
445
+ * Same adapter and the same key shape as every other page preference, so it
446
+ * survives a reload, follows the operator across devices wherever the app
447
+ * backs the adapter with the database, and needs no new storage concept.
448
+ */
449
+ const { value: storedCardStyle, setValue: setStoredCardStyle } = (preferenceAdapter ?? defaultPreferenceAdapter)(`page:${pageKey}_card_style`, DEFAULT_CARD_STYLE);
450
+ const cardStyle = useMemo(() => normaliseCardStyle(storedCardStyle), [storedCardStyle]);
451
+ const cardStyleVars = useMemo(() => toCssVars(cardStyle), [cardStyle]);
452
+ const updateCardStyle = useCallback((patch) => setStoredCardStyle({ ...cardStyle, ...patch }), [cardStyle, setStoredCardStyle]);
401
453
  const extraWidgets = useMemo(() => {
402
454
  const rendered = {};
403
455
  if (!createEntityWidget)
@@ -755,7 +807,9 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
755
807
  // partially visible — mitigates Issue 2 (banner consuming
756
808
  // vertical space). `bg-card/95` + `backdrop-blur` keeps text
757
809
  // legible while showing form context behind the banner.
758
- 'bg-card/95 supports-[backdrop-filter]:bg-card/80 backdrop-blur border-primary'), role: "region", "aria-label": "Canvas editor controls", children: [_jsxs("div", { className: "flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('rounded-full flex items-center justify-center bg-primary/10', controlsCollapsed ? 'h-8 w-8' : 'h-10 w-10 animate-pulse'), children: _jsx(Settings2, { className: cn('text-primary', controlsCollapsed ? 'h-4 w-4' : 'h-5 w-5') }) }), _jsxs("div", { children: [_jsx("h3", { className: cn('font-semibold text-foreground', controlsCollapsed ? 'text-sm' : 'text-lg'), children: "Canvas Editor Active" }), !controlsCollapsed && (_jsx("span", { className: "text-sm text-muted-foreground", children: "Drag anywhere on a card to move it. Resize with the bottom-right handle." }))] })] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded-md border border-border px-2 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", "aria-expanded": !controlsCollapsed, "aria-controls": "page-grid-editor-controls-body", onClick: () => setControlsCollapsed((previous) => !previous), children: [controlsCollapsed ? _jsx(ChevronDown, { className: "h-4 w-4" }) : _jsx(ChevronUp, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only sm:not-sr-only", children: controlsCollapsed ? 'Expand' : 'Collapse' })] }), _jsxs("button", { type: "button", className: "inline-flex items-center rounded-md px-3 py-2 text-sm font-medium bg-primary text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", onClick: () => startTransition(() => setIsEditing(false)), children: [_jsx(Save, { className: "h-4 w-4 mr-2" }), "Save & Exit"] })] })] }), _jsxs("div", { id: "page-grid-editor-controls-body", hidden: controlsCollapsed, className: "flex flex-wrap items-center gap-x-4 gap-y-2 pt-2 border-t border-border max-h-[60vh] overflow-y-auto", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(LayoutGrid, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "text-sm shrink-0 text-muted-foreground", children: "Columns:" }), _jsx("input", { type: "range", min: 1, max: 24, value: layoutCols, onChange: (event) => handleColumnChange(Number(event.target.value)), className: "w-24 cursor-pointer accent-primary", "aria-label": "Column count" }), _jsx("span", { className: "text-sm font-mono w-5 text-center tabular-nums", children: layoutCols })] }), _jsx("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Column presets", children: [1, 2, 3, 4, 6, 12].map((columnCount) => (_jsx("button", { type: "button", onClick: () => handleColumnChange(columnCount), "data-active": layoutCols === columnCount || undefined, "aria-pressed": layoutCols === columnCount, className: cn('px-2 py-0.5 rounded text-xs font-medium transition-colors border', 'bg-muted text-muted-foreground border-border', 'hover:bg-muted/80 hover:text-foreground', 'data-[active]:bg-primary data-[active]:text-primary-foreground data-[active]:border-primary', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: columnCount }, columnCount))) }), hiddenWidgetKeys.length > 0 && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Eye, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "text-sm shrink-0 text-muted-foreground", children: "Add widget:" }), _jsx("div", { className: "flex flex-wrap gap-1", children: hiddenWidgetKeys.map((key) => {
810
+ 'bg-card/95 supports-[backdrop-filter]:bg-card/80 backdrop-blur border-primary'), role: "region", "aria-label": "Canvas editor controls", children: [_jsxs("div", { className: "flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('rounded-full flex items-center justify-center bg-primary/10', controlsCollapsed ? 'h-8 w-8' : 'h-10 w-10 animate-pulse'), children: _jsx(Settings2, { className: cn('text-primary', controlsCollapsed ? 'h-4 w-4' : 'h-5 w-5') }) }), _jsxs("div", { children: [_jsx("h3", { className: cn('font-semibold text-foreground', controlsCollapsed ? 'text-sm' : 'text-lg'), children: "Canvas Editor Active" }), !controlsCollapsed && (_jsx("span", { className: "text-sm text-muted-foreground", children: "Drag anywhere on a card to move it. Resize with the bottom-right handle." }))] })] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded-md border border-border px-2 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", "aria-expanded": !controlsCollapsed, "aria-controls": "page-grid-editor-controls-body", "aria-label": controlsCollapsed ? 'Expand controls: columns, cards and layers' : 'Collapse controls', onClick: () => setControlsCollapsed((previous) => !previous), children: [controlsCollapsed ? _jsx(ChevronDown, { className: "h-4 w-4" }) : _jsx(ChevronUp, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only sm:not-sr-only", children: controlsCollapsed ? 'Columns, cards & layers' : 'Collapse' })] }), _jsxs("button", { type: "button", className: "inline-flex items-center rounded-md px-3 py-2 text-sm font-medium bg-primary text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", onClick: () => startTransition(() => setIsEditing(false)), children: [_jsx(Save, { className: "h-4 w-4 mr-2" }), "Save & Exit"] })] })] }), _jsxs("div", { id: "page-grid-editor-controls-body", hidden: controlsCollapsed, className: "flex flex-wrap items-center gap-x-4 gap-y-2 pt-2 border-t border-border max-h-[60vh] overflow-y-auto", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(LayoutGrid, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "text-sm shrink-0 text-muted-foreground", children: "Columns:" }), _jsx("input", { type: "range", min: 1, max: 24, value: layoutCols, onChange: (event) => handleColumnChange(Number(event.target.value)), className: "w-24 cursor-pointer accent-primary", "aria-label": "Column count" }), _jsx("span", { className: "text-sm font-mono w-5 text-center tabular-nums", children: layoutCols })] }), _jsx("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Column presets", children: [1, 2, 3, 4, 6, 12].map((columnCount) => (_jsx("button", { type: "button", onClick: () => handleColumnChange(columnCount), "data-active": layoutCols === columnCount || undefined, "aria-pressed": layoutCols === columnCount, className: cn('px-2 py-0.5 rounded text-xs font-medium transition-colors border', 'bg-muted text-muted-foreground border-border', 'hover:bg-muted/80 hover:text-foreground', 'data-[active]:bg-primary data-[active]:text-primary-foreground data-[active]:border-primary', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: columnCount }, columnCount))) }), _jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 w-full pt-2 border-t border-border", role: "group", "aria-label": "Card appearance", children: [_jsx("span", { className: "text-sm shrink-0 font-medium text-foreground", children: "Card:" }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Corners" }), _jsx("input", { type: "range", min: RADIUS_RANGE.min, max: RADIUS_RANGE.max, value: cardStyle.radius, onChange: (event) => updateCardStyle({ radius: Number(event.target.value) }), className: "w-20 cursor-pointer accent-primary", "aria-label": "Card corner radius in pixels" }), _jsx("span", { className: "text-sm font-mono w-8 text-right tabular-nums", children: cardStyle.radius })] }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Border" }), _jsx("input", { type: "range", min: BORDER_WIDTH_RANGE.min, max: BORDER_WIDTH_RANGE.max, value: cardStyle.borderWidth, onChange: (event) => updateCardStyle({ borderWidth: Number(event.target.value) }), className: "w-16 cursor-pointer accent-primary", "aria-label": "Card border width in pixels" }), _jsx("span", { className: "text-sm font-mono w-6 text-right tabular-nums", children: cardStyle.borderWidth })] }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "sr-only", children: "Border colour" }), _jsx("select", { value: cardStyle.borderTone, onChange: (event) => updateCardStyle({ borderTone: event.target.value }), className: "rounded-md border border-border bg-background px-2 py-1 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "aria-label": "Card border colour", children: BORDER_TONES.map((tone) => (_jsx("option", { value: tone, children: tone === 'border' ? 'default colour' : tone }, tone))) })] }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "sr-only", children: "Border style" }), _jsx("select", { value: cardStyle.borderStyle, onChange: (event) => updateCardStyle({ borderStyle: event.target.value }), className: "rounded-md border border-border bg-background px-2 py-1 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "aria-label": "Card border style", children: BORDER_STYLES.map((style) => (_jsx("option", { value: style, children: style }, style))) })] }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "sr-only", children: "Card shadow" }), _jsxs("select", { value: cardStyle.elevation === null ? 'default' : String(cardStyle.elevation), onChange: (event) => updateCardStyle({
811
+ elevation: event.target.value === 'default' ? null : Number(event.target.value),
812
+ }), className: "rounded-md border border-border bg-background px-2 py-1 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "aria-label": "Card shadow depth", children: [_jsx("option", { value: "default", children: "default shadow" }), [0, 1, 2, 3, 4].map((level) => (_jsx("option", { value: level, children: level === 0 ? 'no shadow' : `shadow ${level}` }, level)))] })] }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Padding" }), _jsx("input", { type: "range", min: PADDING_RANGE.min, max: PADDING_RANGE.max, value: cardStyle.padding ?? 0, onChange: (event) => updateCardStyle({ padding: Number(event.target.value) }), className: "w-16 cursor-pointer accent-primary", "aria-label": "Card inner padding in pixels" }), _jsx("span", { className: "text-sm font-mono w-8 text-right tabular-nums", children: cardStyle.padding === null ? '\u2014' : cardStyle.padding })] }), _jsxs("button", { type: "button", onClick: () => setStoredCardStyle({ ...DEFAULT_CARD_STYLE }), disabled: isDefaultCardStyle(cardStyle), className: "inline-flex items-center gap-1 rounded-md border border-border px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50 disabled:pointer-events-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx(RotateCcw, { className: "h-3.5 w-3.5" }), "Reset cards"] }), _jsx("output", { className: "text-sm text-muted-foreground", "aria-live": "polite", children: describeCardStyle(cardStyle) })] }), hiddenWidgetKeys.length > 0 && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Eye, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "text-sm shrink-0 text-muted-foreground", children: "Add widget:" }), _jsx("div", { className: "flex flex-wrap gap-1", children: hiddenWidgetKeys.map((key) => {
759
813
  const meta = widgetMeta?.[key];
760
814
  const Icon = meta?.icon;
761
815
  return (_jsxs("button", { type: "button", onClick: () => showLayer(key, meta?.defaultSize), className: cn('flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium border transition-colors', 'bg-muted text-muted-foreground border-border', 'hover:bg-muted/80 hover:text-foreground hover:border-primary/60', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: [_jsx(Plus, { className: "h-3 w-3" }), Icon && _jsx(Icon, { className: "h-3 w-3" }), layerNames[key] || meta?.label || key] }, key));
@@ -789,6 +843,6 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
789
843
  const content = allWidgets[layoutItem.i];
790
844
  if (content === undefined || content === null)
791
845
  return null;
792
- 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));
846
+ return (_jsx(GridItem, { id: layoutItem.i, content: content, isEditing: isEditing, label: layerNames[layoutItem.i] || widgetMeta?.[layoutItem.i]?.label || layoutItem.i, pageKey: pageKey, onHide: hideLayer, autoHeight: layoutItem.autoHeight, onAutoHeightChange: handleAutoHeightChange, chrome: layoutItem.chrome ?? itemChrome, cardStyleVars: cardStyleVars }, layoutItem.i));
793
847
  }) }) }) })] }));
794
848
  }
@@ -0,0 +1,68 @@
1
+ /** Theme tokens a border may reference. Closed set — never a raw colour. */
2
+ export declare const BORDER_TONES: readonly ["border", "primary", "accent", "muted", "destructive", "transparent"];
3
+ export type BorderTone = (typeof BORDER_TONES)[number];
4
+ export declare const BORDER_STYLES: readonly ["solid", "dashed", "dotted", "none"];
5
+ export type BorderStyle = (typeof BORDER_STYLES)[number];
6
+ /** `--shadow-elev-0..4`, shipped by @bsuite/theme >= 0.11.0. */
7
+ export type Elevation = 0 | 1 | 2 | 3 | 4;
8
+ export interface CardStyle {
9
+ /** Corner radius in px. */
10
+ radius: number;
11
+ /** Border thickness in px. */
12
+ borderWidth: number;
13
+ /** Theme token the border colour reads from. */
14
+ borderTone: BorderTone;
15
+ borderStyle: BorderStyle;
16
+ /**
17
+ * `null` means "leave the class-based shadow alone" — which is what every
18
+ * card did before this file. A number selects `--shadow-elev-N`.
19
+ */
20
+ elevation: Elevation | null;
21
+ /** Inner padding in px. `null` leaves the card's own padding untouched. */
22
+ padding: number | null;
23
+ }
24
+ /**
25
+ * The values the chrome element already rendered.
26
+ *
27
+ * `radius: 24` is `1.5rem`, the existing `--radius-card` fallback.
28
+ * `borderWidth: 1` + `borderTone: 'border'` is the existing `border
29
+ * border-border`. `elevation: null` preserves `shadow-sm` and the dark-mode
30
+ * `--glow-card` rule rather than replacing them with an elevation token that
31
+ * is close but not equal.
32
+ */
33
+ export declare const DEFAULT_CARD_STYLE: CardStyle;
34
+ export declare const RADIUS_RANGE: {
35
+ readonly min: 0;
36
+ readonly max: 48;
37
+ };
38
+ export declare const BORDER_WIDTH_RANGE: {
39
+ readonly min: 0;
40
+ readonly max: 8;
41
+ };
42
+ export declare const PADDING_RANGE: {
43
+ readonly min: 0;
44
+ readonly max: 48;
45
+ };
46
+ /**
47
+ * Coerce anything read back out of a preference store into a valid style.
48
+ *
49
+ * Preferences are user-writable JSON that survives releases, so this has to
50
+ * treat its input as untrusted: a stored `borderTone` naming a token that no
51
+ * longer exists must not paint an invalid border, it must fall back. Every
52
+ * field is validated independently so one bad key cannot discard the rest of
53
+ * the operator's choices.
54
+ */
55
+ export declare function normaliseCardStyle(input: unknown): CardStyle;
56
+ export declare function isDefaultCardStyle(style: CardStyle): boolean;
57
+ /** The CSS custom properties a non-default style needs. Empty when default. */
58
+ export declare function toCssVars(style: CardStyle): React.CSSProperties;
59
+ /**
60
+ * A human sentence describing the current card, for the editor's live readout.
61
+ *
62
+ * The operator is adjusting something they can see, so the readout names the
63
+ * change in their words rather than echoing CSS: "2px dashed accent border,
64
+ * 8px corners". D8.3 -- a control whose effect you cannot predict from the
65
+ * screen is not finished.
66
+ */
67
+ export declare function describeCardStyle(style: CardStyle): string;
68
+ //# sourceMappingURL=cardStyle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cardStyle.d.ts","sourceRoot":"","sources":["../src/cardStyle.ts"],"names":[],"mappings":"AA0BA,4EAA4E;AAC5E,eAAO,MAAM,YAAY,iFAOf,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,eAAO,MAAM,aAAa,gDAAiD,CAAC;AAC5E,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD,gEAAgE;AAChE,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAE1C,MAAM,WAAW,SAAS;IACxB,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,UAAU,EAAE,UAAU,CAAC;IACvB,WAAW,EAAE,WAAW,CAAC;IACzB;;;OAGG;IACH,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;IAC5B,2EAA2E;IAC3E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAOhC,CAAC;AAEF,eAAO,MAAM,YAAY;;;CAA+B,CAAC;AACzD,eAAO,MAAM,kBAAkB;;;CAA8B,CAAC;AAC9D,eAAO,MAAM,aAAa;;;CAA+B,CAAC;AAO1D;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CA8B5D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAS5D;AAED,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK,CAAC,aAAa,CAuB/D;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAc1D"}
@@ -0,0 +1,155 @@
1
+ /*
2
+ * cardStyle.ts — the card's appearance, as something the operator edits.
3
+ *
4
+ * WHY THIS EXISTS. Card chrome — the border, the corner radius, the shadow,
5
+ * the padding — was a string of Tailwind classes compiled into
6
+ * `PageGridLayout`. Changing any of it meant asking an agent to edit the
7
+ * package, publish it, and roll it through five apps. The operator has asked
8
+ * for the border to be corrected repeatedly, and each ask cost a full release
9
+ * cycle to answer. The remedy is not another correct border; it is to stop
10
+ * being the only one who can set it.
11
+ *
12
+ * THE DEFAULT IS "DON'T EMIT ANYTHING". `toCssVars` returns only the
13
+ * properties that DIFFER from `DEFAULT_CARD_STYLE`, and the chrome element
14
+ * falls back to exactly the values it used before this file existed. A page
15
+ * whose operator has never opened the editor therefore renders byte-identical
16
+ * CSS — this is a capability, not a restyle, and it must not show up as a
17
+ * visual diff on 305 pages the day it ships.
18
+ *
19
+ * TOKENS, NOT COLOURS. `borderTone` names a theme token, never a literal.
20
+ * The estate bans hex/rgb in D2C apps (`bsuite/no-hardcoded-colours`) and the
21
+ * whole point of `--border` is that a tenant can rebrand it. An editor that
22
+ * wrote a raw hex value into a preference would be a white-label regression wearing
23
+ * a feature's clothes, so the palette below is a closed set of token names and
24
+ * the type makes an arbitrary string unrepresentable.
25
+ */
26
+ /** Theme tokens a border may reference. Closed set — never a raw colour. */
27
+ export const BORDER_TONES = [
28
+ 'border',
29
+ 'primary',
30
+ 'accent',
31
+ 'muted',
32
+ 'destructive',
33
+ 'transparent',
34
+ ];
35
+ export const BORDER_STYLES = ['solid', 'dashed', 'dotted', 'none'];
36
+ /**
37
+ * The values the chrome element already rendered.
38
+ *
39
+ * `radius: 24` is `1.5rem`, the existing `--radius-card` fallback.
40
+ * `borderWidth: 1` + `borderTone: 'border'` is the existing `border
41
+ * border-border`. `elevation: null` preserves `shadow-sm` and the dark-mode
42
+ * `--glow-card` rule rather than replacing them with an elevation token that
43
+ * is close but not equal.
44
+ */
45
+ export const DEFAULT_CARD_STYLE = {
46
+ radius: 24,
47
+ borderWidth: 1,
48
+ borderTone: 'border',
49
+ borderStyle: 'solid',
50
+ elevation: null,
51
+ padding: null,
52
+ };
53
+ export const RADIUS_RANGE = { min: 0, max: 48 };
54
+ export const BORDER_WIDTH_RANGE = { min: 0, max: 8 };
55
+ export const PADDING_RANGE = { min: 0, max: 48 };
56
+ function clamp(value, min, max) {
57
+ if (!Number.isFinite(value))
58
+ return min;
59
+ return Math.min(max, Math.max(min, Math.round(value)));
60
+ }
61
+ /**
62
+ * Coerce anything read back out of a preference store into a valid style.
63
+ *
64
+ * Preferences are user-writable JSON that survives releases, so this has to
65
+ * treat its input as untrusted: a stored `borderTone` naming a token that no
66
+ * longer exists must not paint an invalid border, it must fall back. Every
67
+ * field is validated independently so one bad key cannot discard the rest of
68
+ * the operator's choices.
69
+ */
70
+ export function normaliseCardStyle(input) {
71
+ if (input === null || typeof input !== 'object')
72
+ return { ...DEFAULT_CARD_STYLE };
73
+ const raw = input;
74
+ const tone = BORDER_TONES.includes(raw.borderTone)
75
+ ? raw.borderTone
76
+ : DEFAULT_CARD_STYLE.borderTone;
77
+ const style = BORDER_STYLES.includes(raw.borderStyle)
78
+ ? raw.borderStyle
79
+ : DEFAULT_CARD_STYLE.borderStyle;
80
+ const elevation = typeof raw.elevation === 'number' && raw.elevation >= 0 && raw.elevation <= 4
81
+ ? Math.round(raw.elevation)
82
+ : null;
83
+ return {
84
+ radius: typeof raw.radius === 'number'
85
+ ? clamp(raw.radius, RADIUS_RANGE.min, RADIUS_RANGE.max)
86
+ : DEFAULT_CARD_STYLE.radius,
87
+ borderWidth: typeof raw.borderWidth === 'number'
88
+ ? clamp(raw.borderWidth, BORDER_WIDTH_RANGE.min, BORDER_WIDTH_RANGE.max)
89
+ : DEFAULT_CARD_STYLE.borderWidth,
90
+ borderTone: tone,
91
+ borderStyle: style,
92
+ elevation,
93
+ padding: typeof raw.padding === 'number'
94
+ ? clamp(raw.padding, PADDING_RANGE.min, PADDING_RANGE.max)
95
+ : null,
96
+ };
97
+ }
98
+ export function isDefaultCardStyle(style) {
99
+ return (style.radius === DEFAULT_CARD_STYLE.radius &&
100
+ style.borderWidth === DEFAULT_CARD_STYLE.borderWidth &&
101
+ style.borderTone === DEFAULT_CARD_STYLE.borderTone &&
102
+ style.borderStyle === DEFAULT_CARD_STYLE.borderStyle &&
103
+ style.elevation === DEFAULT_CARD_STYLE.elevation &&
104
+ style.padding === DEFAULT_CARD_STYLE.padding);
105
+ }
106
+ /** The CSS custom properties a non-default style needs. Empty when default. */
107
+ export function toCssVars(style) {
108
+ const vars = {};
109
+ if (style.radius !== DEFAULT_CARD_STYLE.radius) {
110
+ vars['--radius-card'] = `${style.radius}px`;
111
+ }
112
+ if (style.borderWidth !== DEFAULT_CARD_STYLE.borderWidth) {
113
+ vars['--card-border-width'] = `${style.borderWidth}px`;
114
+ }
115
+ if (style.borderTone !== DEFAULT_CARD_STYLE.borderTone) {
116
+ // `transparent` is a CSS-wide keyword, not a token, so it is not wrapped.
117
+ vars['--card-border-color'] =
118
+ style.borderTone === 'transparent' ? 'transparent' : `var(--${style.borderTone})`;
119
+ }
120
+ if (style.borderStyle !== DEFAULT_CARD_STYLE.borderStyle) {
121
+ vars['--card-border-style'] = style.borderStyle;
122
+ }
123
+ if (style.elevation !== null) {
124
+ vars['--card-shadow'] = `var(--shadow-elev-${style.elevation})`;
125
+ }
126
+ if (style.padding !== null) {
127
+ vars['--card-padding'] = `${style.padding}px`;
128
+ }
129
+ return vars;
130
+ }
131
+ /**
132
+ * A human sentence describing the current card, for the editor's live readout.
133
+ *
134
+ * The operator is adjusting something they can see, so the readout names the
135
+ * change in their words rather than echoing CSS: "2px dashed accent border,
136
+ * 8px corners". D8.3 -- a control whose effect you cannot predict from the
137
+ * screen is not finished.
138
+ */
139
+ export function describeCardStyle(style) {
140
+ const parts = [];
141
+ if (style.borderStyle === 'none' || style.borderWidth === 0) {
142
+ parts.push('no border');
143
+ }
144
+ else {
145
+ const tone = style.borderTone === 'border' ? 'default' : style.borderTone;
146
+ parts.push(`${style.borderWidth}px ${style.borderStyle} ${tone} border`);
147
+ }
148
+ parts.push(style.radius === 0 ? 'square corners' : `${style.radius}px corners`);
149
+ if (style.elevation !== null) {
150
+ parts.push(style.elevation === 0 ? 'no shadow' : `elevation ${style.elevation}`);
151
+ }
152
+ if (style.padding !== null)
153
+ parts.push(`${style.padding}px padding`);
154
+ return parts.join(', ');
155
+ }
@@ -59,4 +59,10 @@ export declare const WidthLadder: Story;
59
59
  * which is why "is the class present" is not a check.
60
60
  */
61
61
  export declare const CardHeadingGradient: Story;
62
+ export declare const CardStyleDefault: Story;
63
+ export declare const CardStyleThickBorder: Story;
64
+ export declare const CardStyleDashedAccent: Story;
65
+ export declare const CardStyleSquare: Story;
66
+ export declare const CardStyleBorderless: Story;
67
+ export declare const CardStyleElevationLadder: Story;
62
68
  //# sourceMappingURL=cardSurfaces.stories.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"cardSurfaces.stories.d.ts","sourceRoot":"","sources":["../src/cardSurfaces.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC;;;;;;;;;;GAUG;AACH;;;;;;;;GAQG;AACH,KAAK,KAAK,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,SAAS,CAAA;CAAE,CAAA;;;;;;;AAEvD,wBAGC;AA0BD;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAYnC,CAAA;AAED,kEAAkE;AAClE,eAAO,MAAM,qBAAqB,EAAE,KAWnC,CAAA;AAED,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,EAAE,KAWjC,CAAA;AAED,mFAAmF;AACnF,eAAO,MAAM,iBAAiB,EAAE,KAW/B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,KAyBzB,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,EAAE,KAoBjC,CAAA;AA2CD,eAAO,MAAM,gBAAgB,EAAE,KAK9B,CAAA;AAED,eAAO,MAAM,oBAAoB,EAAE,KAGlC,CAAA;AAED,eAAO,MAAM,qBAAqB,EAAE,KAQnC,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,KAG7B,CAAA;AAED,eAAO,MAAM,mBAAmB,EAAE,KAQjC,CAAA;AAED,eAAO,MAAM,wBAAwB,EAAE,KAatC,CAAA"}
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PageGridLayout } from './PageGridLayout.js';
3
+ import { DEFAULT_CARD_STYLE, describeCardStyle, toCssVars } from './cardStyle.js';
3
4
  export default {
4
5
  title: 'Page Builder/Card surfaces',
5
6
  parameters: { layout: 'fullscreen' },
@@ -74,3 +75,52 @@ export const CardHeadingGradient = {
74
75
  name: 'Card headings — with and without the gradient',
75
76
  render: () => (_jsxs("div", { className: "grid gap-8 p-8 md:grid-cols-2", children: [_jsxs("div", { className: "rounded-[var(--radius-card,1.5rem)] border border-border bg-card p-6", children: [_jsx("h3", { className: "mb-2 text-lg font-semibold text-(--role-text-heading)", children: "Quick Actions \u2014 flat (today, 0 of 310)" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "No gradient." })] }), _jsxs("div", { className: "rounded-[var(--radius-card,1.5rem)] border border-border bg-card p-6", children: [_jsx("h3", { className: "text-gradient-accent mb-2 text-lg font-semibold text-(--role-text-heading)", children: "Quick Actions \u2014 gradient (G2 target)" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "`text-gradient-accent` over the solid heading token." })] })] })),
76
77
  };
78
+ /* ------------------------------------------------------------------ *
79
+ * OPERATOR-EDITABLE CARD APPEARANCE
80
+ *
81
+ * The border, corners, shadow and padding are set by the operator in the
82
+ * canvas editor and persisted per page (see cardStyle.ts). These stories are
83
+ * the reference for what each setting actually paints, so a change to the
84
+ * chrome can be diffed against a picture rather than argued about.
85
+ *
86
+ * `toCssVars` returns ONLY the properties that differ from the default, so
87
+ * `Default` below emits no custom properties at all — it is the control, and
88
+ * it must look exactly like `BareContentOptsIn`. If those two ever diverge,
89
+ * the "costs nothing until you touch it" guarantee has been broken.
90
+ * ------------------------------------------------------------------ */
91
+ /** Renders one slot with chrome on, under a given operator card style. */
92
+ function StyledSurface({ style, note }) {
93
+ const resolved = { ...DEFAULT_CARD_STYLE, ...style };
94
+ const vars = toCssVars(resolved);
95
+ return (_jsxs("div", { className: "p-8", style: {
96
+ ...vars,
97
+ // The editor applies elevation and padding as real declarations,
98
+ // because each has to beat a class. Mirrored here so the story shows
99
+ // what the page shows.
100
+ ...('--card-shadow' in vars ? { ['--story-shadow']: vars['--card-shadow'] } : null),
101
+ }, children: [_jsx("p", { className: "mb-3 text-sm text-muted-foreground", children: note }), _jsx("p", { className: "mb-3 text-sm font-medium text-foreground", children: describeCardStyle(resolved) }), _jsx(PageGridLayout, { pageKey: `card-style-${note.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}`, defaultLayouts: oneSlot, itemChrome: true, widgets: { a: _jsx(BareContent, { title: "Operator-styled card" }) } })] }));
102
+ }
103
+ export const CardStyleDefault = {
104
+ name: 'Card style — default (emits no CSS)',
105
+ render: () => (_jsx(StyledSurface, { style: {}, note: "The control. No custom properties are emitted at all." })),
106
+ };
107
+ export const CardStyleThickBorder = {
108
+ name: 'Card style — 4px border',
109
+ render: () => _jsx(StyledSurface, { style: { borderWidth: 4 }, note: "Border width is a slider, 0-8px." }),
110
+ };
111
+ export const CardStyleDashedAccent = {
112
+ name: 'Card style — dashed accent border',
113
+ render: () => (_jsx(StyledSurface, { style: { borderWidth: 2, borderStyle: 'dashed', borderTone: 'accent' }, note: "Colour comes from a THEME TOKEN, never a literal, so white-labelling still applies." })),
114
+ };
115
+ export const CardStyleSquare = {
116
+ name: 'Card style — square corners',
117
+ render: () => _jsx(StyledSurface, { style: { radius: 0 }, note: "Corner radius is a slider, 0-48px." }),
118
+ };
119
+ export const CardStyleBorderless = {
120
+ name: 'Card style — no border',
121
+ render: () => (_jsx(StyledSurface, { style: { borderStyle: 'none' }, note: "A borderless card still keeps its background and radius." })),
122
+ };
123
+ export const CardStyleElevationLadder = {
124
+ name: 'Card style — elevation 0 to 4',
125
+ render: () => (_jsx("div", { className: "space-y-2", children: [0, 1, 2, 3, 4].map((level) => (_jsx(StyledSurface, { style: { elevation: level }, note: `--shadow-elev-${level}, from @bsuite/theme.` }, level))) })),
126
+ };
@@ -0,0 +1,83 @@
1
+ import * as React from 'react';
2
+ /**
3
+ * EVERY ELEMENT NEEDS A NAME BEFORE ANYTHING CAN STYLE IT.
4
+ *
5
+ * The operator's complaint: "I'm sick of seeing buttons that go the full width of
6
+ * cards and not being able to modify it visually." Measured 2026-09-02: 270
7
+ * full-width buttons, 236 of them on pages he can already rearrange — so the
8
+ * mouse obeys him on a card's outside edge and ignores him on its inside.
9
+ *
10
+ * The blocker is not styling. It is IDENTITY. Nothing in the estate has ever
11
+ * stored a per-element property (except `colSpan` on a form field), because
12
+ * there has been no way to say WHICH element a stored value belongs to. A card
13
+ * has a `cardKey`; the button inside it has nothing.
14
+ *
15
+ * This module is that identity, and nothing else. No stored styles, no visual
16
+ * change at rest — a deliberate first step, so the thing everything else hangs
17
+ * off can be reviewed on its own.
18
+ *
19
+ * THE HOT PATH IS THE DESIGN CONSTRAINT. `Button` renders 2,097 times in crm7,
20
+ * and on the 34 EnhancedDataTable pages that is once per row per action column.
21
+ * So:
22
+ * - the context default is `null`, and a `useContext` returning null is about
23
+ * the cheapest hook there is;
24
+ * - NOTHING is computed unless `isEditing` is true. A consumer's whole cost
25
+ * outside edit mode is one null check.
26
+ * Building the reference string on every render would have been a real
27
+ * regression on the tables, which is why `elementRef` is a function the consumer
28
+ * calls rather than a value this provider computes.
29
+ */
30
+ export interface ElementScope {
31
+ /** The page this element lives on — `PageGridLayout`'s own `pageKey`. */
32
+ pageKey: string;
33
+ /** The card it lives in — `GridItem`'s `id`. */
34
+ cardKey: string;
35
+ /** What the card calls itself, for a sentence a person can read. */
36
+ cardLabel?: string;
37
+ /**
38
+ * Whether the page editor is open. Gates every cost above; a consumer that
39
+ * reads this and returns early pays one boolean.
40
+ */
41
+ isEditing: boolean;
42
+ }
43
+ /**
44
+ * Provided by `GridItem`, which is the only component that knows both the page
45
+ * and the card. Consumers never construct one.
46
+ */
47
+ export declare function ElementScopeProvider({ scope, children, }: {
48
+ scope: ElementScope;
49
+ children: React.ReactNode;
50
+ }): React.JSX.Element;
51
+ /**
52
+ * The scope this element sits in, or `null` outside a grid page.
53
+ *
54
+ * `null` is a normal answer, not an error: most of the estate renders outside a
55
+ * `PageGridLayout`, and an element with no scope simply cannot be addressed yet.
56
+ */
57
+ export declare function useElementScope(): ElementScope | null;
58
+ /**
59
+ * A stable address for one element.
60
+ *
61
+ * SHAPE: `page:<pageKey>|card:<cardKey>|el:<name>`. Chosen so it is greppable,
62
+ * sorts by page then card, and survives being read by a human in a database row.
63
+ *
64
+ * `name` is the element's own stable identifier — NOT its visible label. A label
65
+ * is a moving target: rename "Add contact" to "New contact" and every stored
66
+ * override keyed on it orphans silently. Callers pass something that does not
67
+ * change when the copy does.
68
+ */
69
+ export declare function elementRef(scope: ElementScope, name: string): string;
70
+ /**
71
+ * The same address as a sentence, for a tooltip or a property panel.
72
+ *
73
+ * Uses the card's LABEL rather than its key, because "the Contacts card" is what
74
+ * a person sees and `card3` is not.
75
+ */
76
+ export declare function describeElement(scope: ElementScope, name: string): string;
77
+ /** Parses an address back into its parts, or `null` if it is not one. */
78
+ export declare function parseElementRef(ref: string): {
79
+ pageKey: string;
80
+ cardKey: string;
81
+ name: string;
82
+ } | null;
83
+ //# sourceMappingURL=elementScope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elementScope.d.ts","sourceRoot":"","sources":["../src/elementScope.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,YAAY;IAC3B,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,EAAE,OAAO,CAAC;CACpB;AAID;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAapB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,YAAY,GAAG,IAAI,CAErD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGzE;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,GACV;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAU3D"}
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ const ElementScopeContext = React.createContext(null);
4
+ /**
5
+ * Provided by `GridItem`, which is the only component that knows both the page
6
+ * and the card. Consumers never construct one.
7
+ */
8
+ export function ElementScopeProvider({ scope, children, }) {
9
+ /* Destructured FIRST, then memoised on the primitives.
10
+ Memoising on `scope` itself would hand the subtree a new context value on
11
+ every GridItem render and re-render every button in the card for nothing;
12
+ memoising on `scope.pageKey` and friends would need an exhaustive-deps
13
+ suppression, and the estate ratchets those. Pulling the fields out gets the
14
+ correct behaviour with no suppression at all — the lint rule was right. */
15
+ const { pageKey, cardKey, cardLabel, isEditing } = scope;
16
+ const value = React.useMemo(() => ({ pageKey, cardKey, cardLabel, isEditing }), [pageKey, cardKey, cardLabel, isEditing]);
17
+ return _jsx(ElementScopeContext.Provider, { value: value, children: children });
18
+ }
19
+ /**
20
+ * The scope this element sits in, or `null` outside a grid page.
21
+ *
22
+ * `null` is a normal answer, not an error: most of the estate renders outside a
23
+ * `PageGridLayout`, and an element with no scope simply cannot be addressed yet.
24
+ */
25
+ export function useElementScope() {
26
+ return React.useContext(ElementScopeContext);
27
+ }
28
+ /**
29
+ * A stable address for one element.
30
+ *
31
+ * SHAPE: `page:<pageKey>|card:<cardKey>|el:<name>`. Chosen so it is greppable,
32
+ * sorts by page then card, and survives being read by a human in a database row.
33
+ *
34
+ * `name` is the element's own stable identifier — NOT its visible label. A label
35
+ * is a moving target: rename "Add contact" to "New contact" and every stored
36
+ * override keyed on it orphans silently. Callers pass something that does not
37
+ * change when the copy does.
38
+ */
39
+ export function elementRef(scope, name) {
40
+ return `page:${scope.pageKey}|card:${scope.cardKey}|el:${name}`;
41
+ }
42
+ /**
43
+ * The same address as a sentence, for a tooltip or a property panel.
44
+ *
45
+ * Uses the card's LABEL rather than its key, because "the Contacts card" is what
46
+ * a person sees and `card3` is not.
47
+ */
48
+ export function describeElement(scope, name) {
49
+ const where = scope.cardLabel?.trim() ? `the ${scope.cardLabel.trim()} card` : 'this card';
50
+ return `${name}, in ${where}`;
51
+ }
52
+ /** Parses an address back into its parts, or `null` if it is not one. */
53
+ export function parseElementRef(ref) {
54
+ const parts = ref.split('|');
55
+ if (parts.length !== 3)
56
+ return null;
57
+ const [p, c, e] = parts;
58
+ if (!p.startsWith('page:') || !c.startsWith('card:') || !e.startsWith('el:'))
59
+ return null;
60
+ const pageKey = p.slice('page:'.length);
61
+ const cardKey = c.slice('card:'.length);
62
+ const name = e.slice('el:'.length);
63
+ if (!pageKey || !cardKey || !name)
64
+ return null;
65
+ return { pageKey, cardKey, name };
66
+ }
package/dist/index.d.ts CHANGED
@@ -21,4 +21,8 @@ export { RelationshipField } from './RelationshipField.js';
21
21
  export type { RelationshipFieldProps } from './RelationshipField.js';
22
22
  export { isRelationshipWritable } from './relationshipCatalog.js';
23
23
  export type { EntityWidgetDetail, EntityWidgetFactoryOptions, GridLayoutItem, GridLayouts, PageGridLayoutProps, PageGridPreferenceAdapter, PageGridPreferenceFactory, RelationshipCatalog, RelationshipCatalogEntry, RelationshipFieldOption, RelationshipWidgetDetail, RelationshipWidgetFactoryOptions, UsePageGridLayoutOptions, UsePageGridLayoutResult, WidgetMeta, } from './types.js';
24
+ export { BORDER_TONES, BORDER_STYLES, DEFAULT_CARD_STYLE, RADIUS_RANGE, BORDER_WIDTH_RANGE, PADDING_RANGE, normaliseCardStyle, isDefaultCardStyle, toCssVars as cardStyleToCssVars, describeCardStyle, } from './cardStyle.js';
25
+ export type { CardStyle, BorderTone, BorderStyle, Elevation } from './cardStyle.js';
26
+ export { ElementScopeProvider, useElementScope, elementRef, describeElement, parseElementRef, } from './elementScope.js';
27
+ export type { ElementScope } from './elementScope.js';
24
28
  //# sourceMappingURL=index.d.ts.map
@@ -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,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAwB7C,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AACrE,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,YAAY,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,EACL,iBAAiB,EACjB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxD,YAAY,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EACzB,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,6BAA6B,EAC7B,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,YAAY,EACV,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,yBAAyB,EACzB,yBAAyB,EACzB,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,gCAAgC,EAChC,wBAAwB,EACxB,uBAAuB,EACvB,UAAU,GACX,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,SAAS,IAAI,kBAAkB,EAC/B,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAQpF,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,UAAU,EACV,eAAe,EACf,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -11,3 +11,11 @@ export { buildResponsiveLayouts, RESPONSIVE_BREAKPOINTS, } from './buildResponsi
11
11
  export { useLocalPreference, defaultPreferenceAdapter } from './preferences.js';
12
12
  export { RelationshipField } from './RelationshipField.js';
13
13
  export { isRelationshipWritable } from './relationshipCatalog.js';
14
+ export { BORDER_TONES, BORDER_STYLES, DEFAULT_CARD_STYLE, RADIUS_RANGE, BORDER_WIDTH_RANGE, PADDING_RANGE, normaliseCardStyle, isDefaultCardStyle, toCssVars as cardStyleToCssVars, describeCardStyle, } from './cardStyle.js';
15
+ /*
16
+ * ELEMENT IDENTITY. Exported so a consumer can address one control inside a
17
+ * card — the prerequisite for storing any per-element property, and the reason
18
+ * none has ever been storable: a card has a key, the button inside it had
19
+ * nothing. No styling here, only the name.
20
+ */
21
+ export { ElementScopeProvider, useElementScope, elementRef, describeElement, parseElementRef, } from './elementScope.js';
@@ -1 +1 @@
1
- {"version":3,"file":"cardHeadingScanner.d.ts","sourceRoot":"","sources":["../../src/scanner/cardHeadingScanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAMH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,gBAAgB,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,WAAW,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;OAUG;IACH,qBAAqB,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,iEAAiE;IACjE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,2DAA2D;IAC3D,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,qDAAqD;IACrD,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAkDD,UAAU,MAAM;IACd,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAqDhD;AAsCD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,wBAAwB,GAAG,qBAAqB,CA+FxF"}
1
+ {"version":3,"file":"cardHeadingScanner.d.ts","sourceRoot":"","sources":["../../src/scanner/cardHeadingScanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAMH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,gBAAgB,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,WAAW,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;OAUG;IACH,qBAAqB,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,iEAAiE;IACjE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,2DAA2D;IAC3D,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,qDAAqD;IACrD,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAsDD,UAAU,MAAM;IACd,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAqDhD;AAsCD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,wBAAwB,GAAG,qBAAqB,CA+FxF"}
@@ -60,12 +60,16 @@ const DEFAULT_GRADIENT_CLASSES = ['text-gradient-accent', 'gradient-text'];
60
60
  * the full hazard set an order of magnitude higher. The loose number was the
61
61
  * kind that makes a codemod look safe.
62
62
  */
63
+ // theme-audit-ok: this is the scanner's VOCABULARY — class names it looks FOR in
64
+ // other people's markup, not classes applied here. `flex-grow` is deliberately
65
+ // listed alongside `grow` because the scanner must recognise both spellings in
66
+ // source it did not write.
63
67
  const WIDTH_DEPENDENT = [
64
68
  'truncate',
65
69
  'flex-1',
66
70
  'w-full',
67
71
  'grow',
68
- 'flex-grow',
72
+ 'flex-grow', // theme-audit-ok: vocabulary the scanner looks FOR, never applied
69
73
  'text-center',
70
74
  'mx-auto',
71
75
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsuite/page-builder",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "description": "Shared BSuite responsive page-builder grid and layout persistence primitives",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -63,21 +63,21 @@
63
63
  "devDependencies": {
64
64
  "@bsuite/nav-core": "workspace:^",
65
65
  "@bsuite/theme": "workspace:^",
66
- "@testing-library/jest-dom": "^6.9.1",
67
- "@testing-library/react": "^16.3.2",
68
- "@types/node": "^25.9.2",
69
- "@types/react": "^19.2.17",
70
- "@types/react-dom": "^19.2.3",
71
- "@vitejs/plugin-react": "^6.0.2",
72
- "jsdom": "^29.1.1",
73
- "lucide-react": "^1.17.0",
74
- "react": "^19.2.7",
75
- "react-dom": "^19.2.7",
76
- "react-grid-layout": "^2.2.3",
77
- "react-resizable": "^4.0.1",
78
- "typescript": "~6.0.3",
79
- "vite": "^8.0.16",
80
- "vitest": "^4.1.8"
66
+ "@testing-library/jest-dom": "^7.0.1",
67
+ "@testing-library/react": "^16.3.3",
68
+ "@types/node": "^26.4.0",
69
+ "@types/react": "^19.2.18",
70
+ "@types/react-dom": "^19.2.5",
71
+ "@vitejs/plugin-react": "^6.1.1",
72
+ "jsdom": "^30.0.1",
73
+ "lucide-react": "^1.37.0",
74
+ "react": "^19.2.8",
75
+ "react-dom": "^19.2.8",
76
+ "react-grid-layout": "^2.2.4",
77
+ "react-resizable": "^4.0.2",
78
+ "typescript": "^6.0.3",
79
+ "vite": "^8.2.2",
80
+ "vitest": "^4.1.11"
81
81
  },
82
82
  "packageManager": "pnpm@10.33.3"
83
83
  }