@bsuite/page-builder 2.5.0 → 2.6.1
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/CustomPageView.d.ts +68 -0
- package/dist/CustomPageView.d.ts.map +1 -0
- package/dist/CustomPageView.js +76 -0
- package/dist/PageEditorLauncher.js +1 -1
- package/dist/PageGridLayout.d.ts.map +1 -1
- package/dist/PageGridLayout.js +18 -7
- package/dist/RelationshipField.js +1 -1
- package/dist/elementScope.d.ts +83 -0
- package/dist/elementScope.d.ts.map +1 -0
- package/dist/elementScope.js +66 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/package.json +16 -16
|
@@ -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
|
+
}
|
|
@@ -112,5 +112,5 @@ export function PageEditorLauncher({ pageId, tenantId, widgets, onAddWidget, onR
|
|
|
112
112
|
// reference it here to avoid an unused-variable lint at the call site.
|
|
113
113
|
void tenantId;
|
|
114
114
|
const effectiveDefaultLayouts = defaultLayouts ?? { lg: [] };
|
|
115
|
-
return (_jsxs("div", { className: "relative isolate", children: [_jsx("div", { className: "sticky top-0 z-40 isolate flex flex-col gap-3 rounded-xl shadow-lg border-2 mb-4 p-4 bg-card/95 supports-[backdrop-filter]:bg-card/80 backdrop-blur border-primary", role: "region", "aria-label": "Page editor toolbar", 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: "rounded-full flex items-center justify-center bg-primary/10 h-10 w-10 animate-pulse", children: _jsx(LayoutGrid, { className: "h-5 w-5 text-primary" }) }), _jsxs("div", { children: [_jsx("h3", { className: "font-semibold text-foreground text-lg", children: "Page Editor" }), _jsx("span", { className: "text-sm text-muted-foreground", children: "Drag cards to rearrange. 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-3 py-2 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", onClick: () => setPaletteOpen(true), children: [_jsx(Plus, { className: "h-4 w-4" }), "Add Widget"] }), _jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded-md border border-border px-3 py-2 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", onClick: handleCancel, children: [_jsx(X, { className: "h-4 w-4" }), "Cancel"] }), _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: handleSave, children: [_jsx(Save, { className: "h-4 w-4 mr-2" }), "Save"] })] })] }) }), _jsx(PageGridLayout, { pageKey: pageId, defaultLayouts: effectiveDefaultLayouts, canEditPage: true, preferenceAdapter: capturedAdapter, widgets: gridWidgets, widgetMeta: gridWidgetMeta }), paletteOpen && (_jsxs("div", { className: "fixed inset-0 z-50 flex", children: [_jsx("div", { className: "absolute inset-0 bg-overlay/50", onClick: () => setPaletteOpen(false), "aria-hidden": "true" }), _jsxs("div", { className: "relative ml-auto h-full w-80 max-w-[85vw] flex flex-col bg-card border-l border-border shadow-xl", children: [_jsxs("div", { className: "flex items-center justify-between px-4 pt-4 pb-3 border-b", children: [_jsx("h2", { className: "text-base font-semibold", children: "Add Widget" }), _jsx("button", { type: "button", className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", onClick: () => setPaletteOpen(false), "aria-label": "Close widget palette", children: _jsx(X, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-4", children: [widgets.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground text-center py-8", children: "No widgets available. Add widgets via your app's widget catalogue." })) : (_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground px-1", children: "Available Widgets" }), widgets.map((w) => (_jsxs("div", { className: "flex items-center justify-between rounded-md border bg-card p-3 hover:bg-accent/50 transition-colors", children: [_jsx("span", { className: "text-sm font-medium truncate", children: w.meta?.label ?? w.key }), _jsx("button", { type: "button", className: "ml-2 shrink-0 inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-secondary text-secondary-foreground transition-colors hover:bg-secondary/80", onClick: () => handlePaletteAdd(w.key), children: "Add" })] }, w.key)))] })), _jsxs("div", { className: "border-t border-border pt-4 space-y-2", children: [_jsx("p", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground px-1", children: "Remove Widget" }), widgets.length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground px-1", children: "No widgets on the canvas to remove." })) : (widgets.map((w) => (_jsxs("div", { className: "flex items-center justify-between rounded-md border bg-card p-3", children: [_jsx("span", { className: "text-sm font-medium truncate", children: w.meta?.label ?? w.key }), _jsx("button", { type: "button", className: "ml-2 shrink-0 inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-destructive/10 text-destructive transition-colors hover:bg-destructive/20", onClick: () => onRemoveWidget(w.key), children: "Remove" })] }, `remove-${w.key}`))))] })] })] })] }))] }));
|
|
115
|
+
return (_jsxs("div", { className: "relative isolate", children: [_jsx("div", { className: "sticky top-0 z-40 isolate flex flex-col gap-3 rounded-xl shadow-lg border-2 mb-4 p-4 bg-card/95 supports-[backdrop-filter]:bg-card/80 backdrop-blur border-primary", role: "region", "aria-label": "Page editor toolbar", 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: "rounded-full flex items-center justify-center bg-primary/10 h-10 w-10 animate-pulse", children: _jsx(LayoutGrid, { className: "h-5 w-5 text-primary" }) }), _jsxs("div", { children: [_jsx("h3", { className: "font-semibold text-foreground text-lg", children: "Page Editor" }), _jsx("span", { className: "text-sm text-muted-foreground", children: "Drag cards to rearrange. 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-interactive px-3 py-2 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", onClick: () => setPaletteOpen(true), children: [_jsx(Plus, { className: "h-4 w-4" }), "Add Widget"] }), _jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded-md border border-border-interactive px-3 py-2 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", onClick: handleCancel, children: [_jsx(X, { className: "h-4 w-4" }), "Cancel"] }), _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: handleSave, children: [_jsx(Save, { className: "h-4 w-4 mr-2" }), "Save"] })] })] }) }), _jsx(PageGridLayout, { pageKey: pageId, defaultLayouts: effectiveDefaultLayouts, canEditPage: true, preferenceAdapter: capturedAdapter, widgets: gridWidgets, widgetMeta: gridWidgetMeta }), paletteOpen && (_jsxs("div", { className: "fixed inset-0 z-50 flex", children: [_jsx("div", { className: "absolute inset-0 bg-overlay/50", onClick: () => setPaletteOpen(false), "aria-hidden": "true" }), _jsxs("div", { className: "relative ml-auto h-full w-80 max-w-[85vw] flex flex-col bg-card border-l border-border shadow-xl", children: [_jsxs("div", { className: "flex items-center justify-between px-4 pt-4 pb-3 border-b", children: [_jsx("h2", { className: "text-base font-semibold", children: "Add Widget" }), _jsx("button", { type: "button", className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", onClick: () => setPaletteOpen(false), "aria-label": "Close widget palette", children: _jsx(X, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-4", children: [widgets.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground text-center py-8", children: "No widgets available. Add widgets via your app's widget catalogue." })) : (_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground px-1", children: "Available Widgets" }), widgets.map((w) => (_jsxs("div", { className: "flex items-center justify-between rounded-md border bg-card p-3 hover:bg-accent/50 transition-colors", children: [_jsx("span", { className: "text-sm font-medium truncate", children: w.meta?.label ?? w.key }), _jsx("button", { type: "button", className: "ml-2 shrink-0 inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-secondary text-secondary-foreground transition-colors hover:bg-secondary/80", onClick: () => handlePaletteAdd(w.key), children: "Add" })] }, w.key)))] })), _jsxs("div", { className: "border-t border-border pt-4 space-y-2", children: [_jsx("p", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground px-1", children: "Remove Widget" }), widgets.length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground px-1", children: "No widgets on the canvas to remove." })) : (widgets.map((w) => (_jsxs("div", { className: "flex items-center justify-between rounded-md border bg-card p-3", children: [_jsx("span", { className: "text-sm font-medium truncate", children: w.meta?.label ?? w.key }), _jsx("button", { type: "button", className: "ml-2 shrink-0 inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-destructive/10 text-destructive transition-colors hover:bg-destructive/20", onClick: () => onRemoveWidget(w.key), children: "Remove" })] }, `remove-${w.key}`))))] })] })] })] }))] }));
|
|
116
116
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageGridLayout.d.ts","sourceRoot":"","sources":["../src/PageGridLayout.tsx"],"names":[],"mappings":"
|
|
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"}
|
package/dist/PageGridLayout.js
CHANGED
|
@@ -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';
|
|
@@ -140,7 +141,7 @@ function warnOnceAboutCollapsedContent(el) {
|
|
|
140
141
|
+ '(an aspect ratio, a px/rem height, or min-height), or pass autoHeight={false} on '
|
|
141
142
|
+ 'this card so it renders into the constrained scrollable wrapper instead.');
|
|
142
143
|
}
|
|
143
|
-
const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, isEditing, label, onHide, autoHeight, onAutoHeightChange, chrome = false, cardStyleVars, 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) {
|
|
144
145
|
// Auto-height measurement (blueprint amendment A1). `measureRef` wraps
|
|
145
146
|
// `content` with NO height constraint of its own — its parent has
|
|
146
147
|
// `overflow-hidden` + `flex-1 min-h-0` (clips *painting* to the current
|
|
@@ -150,6 +151,16 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
|
|
|
150
151
|
// intrinsic size, independent of however many rows the item currently
|
|
151
152
|
// occupies — the property that makes the auto-height loop converge to
|
|
152
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 }));
|
|
153
164
|
const measureRef = useRef(null);
|
|
154
165
|
const lastReportedRowsRef = useRef(null);
|
|
155
166
|
const measureRafRef = useRef(null);
|
|
@@ -335,7 +346,7 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
|
|
|
335
346
|
...(cardStyleVars && '--card-padding' in cardStyleVars
|
|
336
347
|
? { padding: cardStyleVars['--card-padding'] }
|
|
337
348
|
: null),
|
|
338
|
-
}, 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:
|
|
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] }));
|
|
339
350
|
}));
|
|
340
351
|
export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, widgets, widgetMeta, className,
|
|
341
352
|
// Resize default flipped 2026-05-12: rgl v2 dropped auto-handle rendering
|
|
@@ -796,13 +807,13 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
796
807
|
// partially visible — mitigates Issue 2 (banner consuming
|
|
797
808
|
// vertical space). `bg-card/95` + `backdrop-blur` keeps text
|
|
798
809
|
// legible while showing form context behind the banner.
|
|
799
|
-
'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({
|
|
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-interactive 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-interactive', '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-interactive 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-interactive 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({
|
|
800
811
|
elevation: event.target.value === 'default' ? null : Number(event.target.value),
|
|
801
|
-
}), 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) => {
|
|
812
|
+
}), className: "rounded-md border border-border-interactive 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-interactive 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) => {
|
|
802
813
|
const meta = widgetMeta?.[key];
|
|
803
814
|
const Icon = meta?.icon;
|
|
804
|
-
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));
|
|
805
|
-
}) })] })), layerItems.length > 0 && (_jsxs("div", { className: "basis-full rounded-md border border-border bg-background/80 p-2", children: [_jsxs("div", { className: "mb-2 flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), "Layers"] }), _jsx("div", { className: "flex flex-col gap-1", children: layerItems.map((layer) => (_jsxs("div", { className: "flex items-center gap-1 rounded border border-border bg-card px-2 py-1", children: [_jsx("input", { type: "text", value: layer.label, onChange: (event) => handleLayerRename(layer.id, event.target.value), className: "min-w-0 flex-1 bg-transparent text-xs text-foreground outline-none", "aria-label": `Rename ${layer.label}` }), _jsx("button", { type: "button", onClick: () => moveWidget(layer.id, 'up'), disabled: !layer.canMoveUp, "aria-label": `Move ${layer.label} up`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40", children: _jsx(ArrowUp, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => moveWidget(layer.id, 'down'), disabled: !layer.canMoveDown, "aria-label": `Move ${layer.label} down`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40", children: _jsx(ArrowDown, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => setWidgetLocked(layer.id, !layer.locked), "aria-label": `${layer.locked ? 'Unlock' : 'Lock'} ${layer.label}`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", children: layer.locked ? _jsx(Lock, { className: "h-3.5 w-3.5" }) : _jsx(Unlock, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => hideLayer(layer.id), "aria-label": `Hide ${layer.label}`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", children: _jsx(EyeOff, { className: "h-3.5 w-3.5" }) })] }, layer.id))) })] })), _jsxs("button", { type: "button", onClick: handleCompact, className: cn('ml-auto inline-flex items-center rounded-md border border-border px-2 py-1 text-sm transition-colors', 'text-muted-foreground hover:bg-muted hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: [_jsx(Layers, { className: "h-4 w-4 mr-1" }), "Compact Layout"] }), _jsxs("button", { type: "button", onClick: () => setResetConfirmOpen(true), className: cn('inline-flex items-center rounded-md border border-border px-2 py-1 text-sm transition-colors', 'text-muted-foreground hover:bg-muted hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), "Reset to Default"] })] })] })), resetConfirmOpen && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-overlay/50 p-4", role: "dialog", "aria-modal": "true", "aria-labelledby": "page-grid-reset-title", "aria-describedby": "page-grid-reset-description", children: _jsxs("div", { className: "max-w-md rounded-lg bg-card p-5 shadow-lg border border-border", children: [_jsx("h2", { id: "page-grid-reset-title", className: "text-lg font-semibold", children: "Reset page layout?" }), _jsx("p", { id: "page-grid-reset-description", className: "mt-2 text-sm text-muted-foreground", children: "This will discard your current layout and restore all widgets to their default positions." }), _jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [_jsx("button", { type: "button", ref: resetCancelButtonRef, onClick: () => setResetConfirmOpen(false), className: cn('inline-flex items-center rounded-md border border-border px-3 py-2 text-sm transition-colors', 'bg-card text-foreground hover:bg-muted hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: "Cancel" }), _jsx("button", { type: "button", ref: resetConfirmButtonRef, onClick: () => {
|
|
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-interactive', '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));
|
|
816
|
+
}) })] })), layerItems.length > 0 && (_jsxs("div", { className: "basis-full rounded-md border border-border bg-background/80 p-2", children: [_jsxs("div", { className: "mb-2 flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), "Layers"] }), _jsx("div", { className: "flex flex-col gap-1", children: layerItems.map((layer) => (_jsxs("div", { className: "flex items-center gap-1 rounded border border-border bg-card px-2 py-1", children: [_jsx("input", { type: "text", value: layer.label, onChange: (event) => handleLayerRename(layer.id, event.target.value), className: "min-w-0 flex-1 bg-transparent text-xs text-foreground outline-none", "aria-label": `Rename ${layer.label}` }), _jsx("button", { type: "button", onClick: () => moveWidget(layer.id, 'up'), disabled: !layer.canMoveUp, "aria-label": `Move ${layer.label} up`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40", children: _jsx(ArrowUp, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => moveWidget(layer.id, 'down'), disabled: !layer.canMoveDown, "aria-label": `Move ${layer.label} down`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40", children: _jsx(ArrowDown, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => setWidgetLocked(layer.id, !layer.locked), "aria-label": `${layer.locked ? 'Unlock' : 'Lock'} ${layer.label}`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", children: layer.locked ? _jsx(Lock, { className: "h-3.5 w-3.5" }) : _jsx(Unlock, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => hideLayer(layer.id), "aria-label": `Hide ${layer.label}`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", children: _jsx(EyeOff, { className: "h-3.5 w-3.5" }) })] }, layer.id))) })] })), _jsxs("button", { type: "button", onClick: handleCompact, className: cn('ml-auto inline-flex items-center rounded-md border border-border-interactive px-2 py-1 text-sm transition-colors', 'text-muted-foreground hover:bg-muted hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: [_jsx(Layers, { className: "h-4 w-4 mr-1" }), "Compact Layout"] }), _jsxs("button", { type: "button", onClick: () => setResetConfirmOpen(true), className: cn('inline-flex items-center rounded-md border border-border-interactive px-2 py-1 text-sm transition-colors', 'text-muted-foreground hover:bg-muted hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), "Reset to Default"] })] })] })), resetConfirmOpen && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-overlay/50 p-4", role: "dialog", "aria-modal": "true", "aria-labelledby": "page-grid-reset-title", "aria-describedby": "page-grid-reset-description", children: _jsxs("div", { className: "max-w-md rounded-lg bg-card p-5 shadow-lg border border-border", children: [_jsx("h2", { id: "page-grid-reset-title", className: "text-lg font-semibold", children: "Reset page layout?" }), _jsx("p", { id: "page-grid-reset-description", className: "mt-2 text-sm text-muted-foreground", children: "This will discard your current layout and restore all widgets to their default positions." }), _jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [_jsx("button", { type: "button", ref: resetCancelButtonRef, onClick: () => setResetConfirmOpen(false), className: cn('inline-flex items-center rounded-md border border-border-interactive px-3 py-2 text-sm transition-colors', 'bg-card text-foreground hover:bg-muted hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: "Cancel" }), _jsx("button", { type: "button", ref: resetConfirmButtonRef, onClick: () => {
|
|
806
817
|
handleReset();
|
|
807
818
|
setResetConfirmOpen(false);
|
|
808
819
|
}, className: cn('inline-flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors', 'bg-destructive text-destructive-foreground shadow hover:bg-destructive/90', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/40 focus-visible:ring-offset-1'), children: "Reset to Default" })] })] }) })), _jsx("div", { ref: containerRef, className: cn('relative z-0 overflow-visible', isEditing && 'min-h-[200px] bg-muted/30'), "data-page-grid-editing": isEditing || undefined, children: _jsx("div", { className: "relative z-0", style: { opacity: containerWidth > 0 ? 1 : 0 }, "aria-busy": containerWidth <= 0, children: _jsx(Responsive, { width: Math.max(containerWidth, 1), className: "layout page-grid-canvas", layouts: activeLayouts, breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }, rowHeight: DEFAULT_ROW_HEIGHT, onLayoutChange: (layout, layouts) => {
|
|
@@ -832,6 +843,6 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
832
843
|
const content = allWidgets[layoutItem.i];
|
|
833
844
|
if (content === undefined || content === null)
|
|
834
845
|
return null;
|
|
835
|
-
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, cardStyleVars: cardStyleVars }, 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));
|
|
836
847
|
}) }) }) })] }));
|
|
837
848
|
}
|
|
@@ -149,7 +149,7 @@ export function RelationshipField({ value, tenantId, label, placeholder = 'Searc
|
|
|
149
149
|
};
|
|
150
150
|
return (_jsxs("div", { className: cn('relative w-full', className), children: [label && (_jsx("label", { htmlFor: inputId, className: "mb-1 block text-xs font-medium text-muted-foreground", children: label })), _jsxs("div", { className: "relative flex items-center gap-1", children: [_jsx("input", { id: inputId, role: "combobox", type: "text", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-autocomplete": "list", "aria-activedescendant": isOpen && highlightedIndex >= 0 && options[highlightedIndex]
|
|
151
151
|
? `${listboxId}-option-${highlightedIndex}`
|
|
152
|
-
: undefined, "aria-label": ariaLabel ?? label, className: "w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm text-foreground disabled:opacity-60", value: query, placeholder: placeholder, disabled: disabled, autoComplete: "off", onChange: handleInputChange, onFocus: () => {
|
|
152
|
+
: undefined, "aria-label": ariaLabel ?? label, className: "w-full rounded-md border border-border-interactive bg-background px-2 py-1.5 text-sm text-foreground disabled:opacity-60", value: query, placeholder: placeholder, disabled: disabled, autoComplete: "off", onChange: handleInputChange, onFocus: () => {
|
|
153
153
|
setIsOpen(true);
|
|
154
154
|
if (options.length === 0)
|
|
155
155
|
runSearch(query);
|
|
@@ -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
|
@@ -23,4 +23,6 @@ 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
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
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';
|
|
26
28
|
//# sourceMappingURL=index.d.ts.map
|
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,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,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
|
@@ -12,3 +12,10 @@ export { useLocalPreference, defaultPreferenceAdapter } from './preferences.js';
|
|
|
12
12
|
export { RelationshipField } from './RelationshipField.js';
|
|
13
13
|
export { isRelationshipWritable } from './relationshipCatalog.js';
|
|
14
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';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsuite/page-builder",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
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": "^
|
|
67
|
-
"@testing-library/react": "^16.3.
|
|
68
|
-
"@types/node": "^
|
|
69
|
-
"@types/react": "^19.2.
|
|
70
|
-
"@types/react-dom": "^19.2.
|
|
71
|
-
"@vitejs/plugin-react": "^6.
|
|
72
|
-
"jsdom": "^
|
|
73
|
-
"lucide-react": "^1.
|
|
74
|
-
"react": "^19.2.
|
|
75
|
-
"react-dom": "^19.2.
|
|
76
|
-
"react-grid-layout": "^2.2.
|
|
77
|
-
"react-resizable": "^4.0.
|
|
78
|
-
"typescript": "
|
|
79
|
-
"vite": "^8.
|
|
80
|
-
"vitest": "^4.1.
|
|
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
|
}
|