@bsuite/page-builder 0.5.1 → 0.5.3

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,73 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { GridLayouts, WidgetMeta } from './types.js';
3
+ /**
4
+ * Widget configuration accepted by {@link PageEditorLauncher}.
5
+ *
6
+ * A widget is a single renderable card on the canvas — its `key` matches the
7
+ * `i` field of a {@link GridLayouts} item, and `content` is the React node
8
+ * rendered inside the card body.
9
+ */
10
+ export interface WidgetConfig {
11
+ /** Unique key — must match the `i` field of the corresponding layout item. */
12
+ key: string;
13
+ /** React node rendered inside the card. */
14
+ content: ReactNode;
15
+ /** Optional metadata (label, icon, default size) for the editor banner. */
16
+ meta?: WidgetMeta;
17
+ }
18
+ export interface PageEditorLauncherProps {
19
+ /** The page identifier — used as the `pageKey` for the grid layout persistence. */
20
+ pageId: string;
21
+ /** The tenant identifier — passed through to the consumer's add-widget handler. */
22
+ tenantId: string;
23
+ /** The current set of widgets to render on the canvas. */
24
+ widgets: WidgetConfig[];
25
+ /** Called when the user adds a widget from the palette. */
26
+ onAddWidget: (widget: WidgetConfig) => void;
27
+ /** Called when the user removes a widget from the canvas. */
28
+ onRemoveWidget: (widgetKey: string) => void;
29
+ /** Called with the final layout when the user clicks Save. */
30
+ onSave: (layout: GridLayouts) => void;
31
+ /** Called when the user clicks Cancel or closes the editor. */
32
+ onClose: () => void;
33
+ /** Default layouts for the grid. If omitted, a single empty `lg` layout is used. */
34
+ defaultLayouts?: GridLayouts;
35
+ }
36
+ /**
37
+ * PageEditorLauncher — framework-agnostic page layout editor.
38
+ *
39
+ * Extracted from crm7's `PageEditorLauncher` (crm7/src/components/platform/
40
+ * PageEditorLauncher.tsx) into the shared `@bsuite/page-builder` package so
41
+ * any consumer app (conduit, BSU, R80) can mount a full drag-and-drop page
42
+ * editor without reimplementing the WidgetPalette drawer, canvas grid, or
43
+ * save/cancel toolbar.
44
+ *
45
+ * Unlike the crm7 original, this component is **headless** with respect to
46
+ * auth/permissions/routing — the consumer app gates mount via its own
47
+ * auth/permission checks and passes the authorised `tenantId`/`pageId`.
48
+ * All crm7-specific imports (supabase client, `useAuth`, `usePermissions`,
49
+ * `useLocation`/wouter, `getCustomPageBySlug`, crm7 stores) have been removed.
50
+ *
51
+ * The editor renders:
52
+ * 1. A **WidgetPalette drawer** (right-side slide-over) for adding widgets.
53
+ * 2. A **canvas grid** (`PageGridLayout` with `canEditPage=true`) for
54
+ * drag-and-drop arrangement.
55
+ * 3. A **save/cancel toolbar** at the top for committing or discarding changes.
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * {editMode && (
60
+ * <PageEditorLauncher
61
+ * pageId={`custom-page:${page.id}`}
62
+ * tenantId={tenantId ?? ''}
63
+ * widgets={widgets}
64
+ * onAddWidget={handleAddWidget}
65
+ * onRemoveWidget={handleRemoveWidget}
66
+ * onSave={handleSaveLayout}
67
+ * onClose={() => setEditMode(false)}
68
+ * />
69
+ * )}
70
+ * ```
71
+ */
72
+ export declare function PageEditorLauncher({ pageId, tenantId, widgets, onAddWidget, onRemoveWidget, onSave, onClose, defaultLayouts, }: PageEditorLauncherProps): import("react").JSX.Element;
73
+ //# sourceMappingURL=PageEditorLauncher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PageEditorLauncher.d.ts","sourceRoot":"","sources":["../src/PageEditorLauncher.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA0C,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAI/E,OAAO,KAAK,EACV,WAAW,EAGX,UAAU,EACX,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,OAAO,EAAE,SAAS,CAAC;IACnB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,2DAA2D;IAC3D,WAAW,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;IAC5C,6DAA6D;IAC7D,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,8DAA8D;IAC9D,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACtC,+DAA+D;IAC/D,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,oFAAoF;IACpF,cAAc,CAAC,EAAE,WAAW,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,QAAQ,EACR,OAAO,EACP,WAAW,EACX,cAAc,EACd,MAAM,EACN,OAAO,EACP,cAAc,GACf,EAAE,uBAAuB,+BAmPzB"}
@@ -0,0 +1,116 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useMemo, useRef, useState } from 'react';
3
+ import { LayoutGrid, Plus, Save, X } from 'lucide-react';
4
+ import { PageGridLayout } from './PageGridLayout.js';
5
+ import { useLocalPreference } from './preferences.js';
6
+ /**
7
+ * PageEditorLauncher — framework-agnostic page layout editor.
8
+ *
9
+ * Extracted from crm7's `PageEditorLauncher` (crm7/src/components/platform/
10
+ * PageEditorLauncher.tsx) into the shared `@bsuite/page-builder` package so
11
+ * any consumer app (conduit, BSU, R80) can mount a full drag-and-drop page
12
+ * editor without reimplementing the WidgetPalette drawer, canvas grid, or
13
+ * save/cancel toolbar.
14
+ *
15
+ * Unlike the crm7 original, this component is **headless** with respect to
16
+ * auth/permissions/routing — the consumer app gates mount via its own
17
+ * auth/permission checks and passes the authorised `tenantId`/`pageId`.
18
+ * All crm7-specific imports (supabase client, `useAuth`, `usePermissions`,
19
+ * `useLocation`/wouter, `getCustomPageBySlug`, crm7 stores) have been removed.
20
+ *
21
+ * The editor renders:
22
+ * 1. A **WidgetPalette drawer** (right-side slide-over) for adding widgets.
23
+ * 2. A **canvas grid** (`PageGridLayout` with `canEditPage=true`) for
24
+ * drag-and-drop arrangement.
25
+ * 3. A **save/cancel toolbar** at the top for committing or discarding changes.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * {editMode && (
30
+ * <PageEditorLauncher
31
+ * pageId={`custom-page:${page.id}`}
32
+ * tenantId={tenantId ?? ''}
33
+ * widgets={widgets}
34
+ * onAddWidget={handleAddWidget}
35
+ * onRemoveWidget={handleRemoveWidget}
36
+ * onSave={handleSaveLayout}
37
+ * onClose={() => setEditMode(false)}
38
+ * />
39
+ * )}
40
+ * ```
41
+ */
42
+ export function PageEditorLauncher({ pageId, tenantId, widgets, onAddWidget, onRemoveWidget, onSave, onClose, defaultLayouts, }) {
43
+ const [paletteOpen, setPaletteOpen] = useState(false);
44
+ // Track the latest layout in a ref so the Save handler can read it.
45
+ // The grid's preference adapter writes to this ref via the custom factory
46
+ // below, and Save reads from it — no localStorage coupling required.
47
+ const layoutRef = useRef(defaultLayouts ?? { lg: [] });
48
+ // Custom preference adapter factory: wraps `useLocalPreference` (so the
49
+ // grid still persists to localStorage for the session) and also captures
50
+ // the layout value in `layoutRef` whenever the `_grid_layouts` key changes.
51
+ // This is the seam that lets `onSave` emit the final layout without
52
+ // reaching into localStorage directly.
53
+ const capturedAdapter = useCallback((key, fallback) => {
54
+ const adapter = useLocalPreference(key, fallback);
55
+ // Track the layouts key so Save can read the ref.
56
+ if (key.endsWith('_grid_layouts')) {
57
+ const originalSetValue = adapter.setValue;
58
+ const wrappedSetValue = useCallback((next) => {
59
+ originalSetValue((previous) => {
60
+ const resolved = typeof next === 'function'
61
+ ? next(previous)
62
+ : next;
63
+ layoutRef.current = resolved;
64
+ return resolved;
65
+ });
66
+ }, [originalSetValue]);
67
+ // Also capture the initial value.
68
+ layoutRef.current = adapter.value;
69
+ return { ...adapter, setValue: wrappedSetValue };
70
+ }
71
+ return adapter;
72
+ }, []);
73
+ // Build the `widgets` record expected by PageGridLayout from the WidgetConfig[].
74
+ const gridWidgets = useMemo(() => {
75
+ const record = {};
76
+ for (const w of widgets) {
77
+ record[w.key] = w.content;
78
+ }
79
+ return record;
80
+ }, [widgets]);
81
+ // Build the `widgetMeta` record for the editor banner labels.
82
+ const gridWidgetMeta = useMemo(() => {
83
+ const record = {};
84
+ for (const w of widgets) {
85
+ if (w.meta)
86
+ record[w.key] = w.meta;
87
+ }
88
+ return record;
89
+ }, [widgets]);
90
+ const handleSave = useCallback(() => {
91
+ onSave(layoutRef.current);
92
+ onClose();
93
+ }, [onSave, onClose]);
94
+ const handleCancel = useCallback(() => {
95
+ onClose();
96
+ }, [onClose]);
97
+ // The palette calls this when the user picks a widget to add.
98
+ const handlePaletteAdd = useCallback((widgetType) => {
99
+ // Delegate to the consumer's onAddWidget — the consumer knows how to
100
+ // resolve a widget type string into a WidgetConfig (entity table,
101
+ // built-in, custom, etc.). This is the framework-agnostic seam: crm7
102
+ // resolved this via its widget registry + supabase; conduit resolves it
103
+ // via its own widget catalogue.
104
+ onAddWidget({
105
+ key: widgetType,
106
+ content: null,
107
+ meta: { label: widgetType },
108
+ });
109
+ setPaletteOpen(false);
110
+ }, [onAddWidget]);
111
+ // tenantId is passed through to onAddWidget via the consumer's handler;
112
+ // reference it here to avoid an unused-variable lint at the call site.
113
+ void tenantId;
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-black/40", 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
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { PageGridLayout } from './PageGridLayout.js';
2
+ export { PageEditorLauncher } from './PageEditorLauncher.js';
3
+ export type { WidgetConfig, PageEditorLauncherProps } from './PageEditorLauncher.js';
2
4
  export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.js';
3
5
  export type { PageGridEditingEventDetail } from './usePageGridLayout.js';
4
6
  export { rescaleLayout } from './rescaleLayout.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,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,YAAY,EACV,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,yBAAyB,EACzB,yBAAyB,EACzB,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,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,YAAY,EACV,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,uBAAuB,EACvB,UAAU,GACX,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { PageGridLayout } from './PageGridLayout.js';
2
+ export { PageEditorLauncher } from './PageEditorLauncher.js';
2
3
  export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.js';
3
4
  export { rescaleLayout } from './rescaleLayout.js';
4
5
  export { computeAutoHeightRows } from './autoHeight.js';
@@ -1 +1 @@
1
- {"version":3,"file":"usePageGridLayout.d.ts","sourceRoot":"","sources":["../src/usePageGridLayout.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAGV,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,0BAA0B,2IAM7B,CAAC;AAEX;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;AAElE,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,cAAc,EACd,WAAgB,EAChB,aAAiB,EACjB,WAAkB,EAClB,gBAA6C,EAC7C,iBAA4C,GAC7C,EAAE,wBAAwB,GAAG,uBAAuB,CA8ZpD"}
1
+ {"version":3,"file":"usePageGridLayout.d.ts","sourceRoot":"","sources":["../src/usePageGridLayout.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAGV,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,0BAA0B,2IAM7B,CAAC;AAEX;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;AAElE,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,cAAc,EACd,WAAgB,EAChB,aAAiB,EACjB,WAAkB,EAClB,gBAA6C,EAC7C,iBAA4C,GAC7C,EAAE,wBAAwB,GAAG,uBAAuB,CA2cpD"}
@@ -1,5 +1,5 @@
1
1
  import { startTransition, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
2
- import { noCompactor, verticalCompactor } from 'react-grid-layout';
2
+ import { verticalCompactor } from 'react-grid-layout';
3
3
  import { buildResponsiveLayouts } from './buildResponsiveLayouts.js';
4
4
  import { defaultPreferenceAdapter } from './preferences.js';
5
5
  import { rescaleLayout } from './rescaleLayout.js';
@@ -121,7 +121,55 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
121
121
  }
122
122
  }, [baseCols, layoutCols, rawLayouts, setBaseCols, setSavedLayout]);
123
123
  const activeCols = useMemo(() => ({ lg: layoutCols, md: layoutCols, sm: layoutCols, xs: layoutCols, xxs: layoutCols }), [layoutCols]);
124
- const activeCompactor = useMemo(() => (isEditing ? { ...noCompactor, preventCollision: true } : verticalCompactor), [isEditing]);
124
+ /**
125
+ * The compactor handed to react-grid-layout — the SAME one in edit mode and
126
+ * view mode (bsuite#1588).
127
+ *
128
+ * Why this is NOT `{ ...noCompactor, preventCollision: true }` while editing
129
+ * -------------------------------------------------------------------------
130
+ * That was the original edit-mode config, present since this package's first
131
+ * commit, and it silently made drag/resize gestures unable to persist for
132
+ * any layout with adjacent cards. react-grid-layout derives three knobs
133
+ * straight off the compactor object:
134
+ *
135
+ * preventCollision = compactor.preventCollision ?? false // was true
136
+ * allowOverlap = compactor.allowOverlap // false
137
+ * compactType = compactor.type // was null
138
+ *
139
+ * and feeds them to `moveElement`, whose colliding-move branch is:
140
+ *
141
+ * if (hasCollisions && preventCollision) {
142
+ * l.x = oldX; l.y = oldY; l.moved = false; return layout; // full revert
143
+ * }
144
+ *
145
+ * With `compactType: null` there is NO compaction to displace the colliding
146
+ * neighbour, so the only outcome for a gesture landing on an occupied cell
147
+ * is a total revert. `onDragStop` then guards its emit with
148
+ * `if (oldLayout && !deepEqual(oldLayout, finalLayout)) onLayoutChange(...)`
149
+ * — a reverted gesture emits NOTHING, so the persisted layout could never
150
+ * change. Resize is rejected by the same lever (`if (preventCollision &&
151
+ * !allowOverlap)` restores the old w/h/x/y). The card still tracked the
152
+ * cursor because react-draggable/react-resizable transform the DOM node
153
+ * directly, independent of whether RGL accepted the move — which is why the
154
+ * gesture looked live while nothing ever saved.
155
+ *
156
+ * In a full-width stack (every card `w: cols` — the dominant archetype on
157
+ * /dashboard and /people/:id) EVERY reorder target is occupied, so EVERY
158
+ * gesture reverted.
159
+ *
160
+ * Why `verticalCompactor` in BOTH modes rather than another combination
161
+ * --------------------------------------------------------------------
162
+ * View mode always compacted vertically. An edit-mode compactor that allows
163
+ * free/overlapping placement therefore produces a layout the viewer
164
+ * immediately re-compacts away — the editor/viewer mismatch meant "free
165
+ * placement" was never deliverable, and Save & Exit re-compacted regardless.
166
+ * One compactor for both modes makes what the user arranges exactly what
167
+ * they get, lets `moveElement` displace neighbours (so reordering a stack
168
+ * works at all), and removes a spurious `onLayoutChange` that RGL fired on
169
+ * every edit-mode toggle purely because `compactType` changed — that echo
170
+ * carried the pre-gesture layout and was the only write reaching storage.
171
+ */
172
+ const activeCompactor = verticalCompactor;
125
173
  /**
126
174
  * Throttled layout-change handler.
127
175
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsuite/page-builder",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Shared BSuite responsive page-builder grid and layout persistence primitives",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",