@bsuite/page-builder 0.2.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"PageGridLayout.d.ts","sourceRoot":"","sources":["../src/PageGridLayout.tsx"],"names":[],"mappings":"AAIA,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAGxC,OAAO,KAAK,EAAe,mBAAmB,EAAE,MAAM,YAAY,CAAC;AA0InE,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,yBAAiE,EACjE,kBAAkB,EAClB,sBAAsB,GACvB,EAAE,mBAAmB,2CA6XrB"}
1
+ {"version":3,"file":"PageGridLayout.d.ts","sourceRoot":"","sources":["../src/PageGridLayout.tsx"],"names":[],"mappings":"AAIA,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAIxC,OAAO,KAAK,EAAe,mBAAmB,EAAE,MAAM,YAAY,CAAC;AA+KnE,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,yBAAiE,EACjE,kBAAkB,EAClB,sBAAsB,GACvB,EAAE,mBAAmB,2CA+frB"}
@@ -1,13 +1,32 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Eye, EyeOff, Layers, LayoutGrid, Plus, RotateCcw, Save, Settings2 } from 'lucide-react';
2
+ import { ArrowDown, ArrowUp, Eye, EyeOff, Layers, LayoutGrid, Lock, Plus, RotateCcw, Save, Settings2, Unlock } from 'lucide-react';
3
3
  import React, { startTransition, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { Responsive } from 'react-grid-layout';
5
- import { gridBounds, maxSize, minMaxSize, minSize } from 'react-grid-layout/core';
5
+ import { gridBounds, minMaxSize, minSize } from 'react-grid-layout/core';
6
6
  import 'react-grid-layout/css/styles.css';
7
7
  import 'react-resizable/css/styles.css';
8
+ import { defaultPreferenceAdapter } from './preferences.js';
8
9
  import { usePageGridLayout } from './usePageGridLayout.js';
9
10
  import { cn } from './utils.js';
10
- const DEFAULT_RESIZE_BOUNDS = { minW: 2, minH: 1, maxW: 12, maxH: 16 };
11
+ /**
12
+ * Default resize bounds applied as a *global* constraint to ALL grid items.
13
+ *
14
+ * History
15
+ * -------
16
+ * - 0.2.x: maxH=16 (≈512px) — clipped data tables.
17
+ * - 0.3.0: bumped to maxH=48 (≈1536px) — still a hard ceiling. Operators
18
+ * reported "the handle hits a wall before all my data fits" on tall
19
+ * data-table cards even after the bump.
20
+ * - 0.4.0 (this version): **removed the global maxW and maxH ceilings
21
+ * entirely.** Width is bounded by `gridBounds` (the active column count)
22
+ * so no separate global `maxSize(...)` is needed; per-item
23
+ * `LayoutItem.maxW` / `maxH` is honoured via `minMaxSize`. Combined
24
+ * with the new internal scroll wrapper around `{content}` (see GridItem
25
+ * render below), cards now grow to their bounds and content scrolls
26
+ * inside. Per-item `maxH` on `LayoutItem` remains the right knob for
27
+ * "this widget shouldn't grow past N rows".
28
+ */
29
+ const DEFAULT_RESIZE_BOUNDS = { minW: 2, minH: 1 };
11
30
  const DEFAULT_RESIZE_HANDLES = ['se'];
12
31
  const DEFAULT_ADD_ENTITY_WIDGET_EVENT_NAMES = ['bsu-add-entity-widget', 'crm7-add-entity-widget'];
13
32
  /**
@@ -36,7 +55,7 @@ const DEFAULT_ADD_ENTITY_WIDGET_EVENT_NAMES = ['bsu-add-entity-widget', 'crm7-ad
36
55
  const RGLResizeHandle = React.forwardRef(function RGLResizeHandle({ axis = 'se', className: injectedClassName, ...rest }, ref) {
37
56
  return (_jsx("span", { ref: ref, "data-no-drag": true, className: `react-resizable-handle react-resizable-handle-${axis}${injectedClassName ? ' ' + injectedClassName : ''}`, ...rest }));
38
57
  });
39
- const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, isEditing, label, onRemove, children: injectedChildren, className: injectedClassName, style: injectedStyle, ...rest }, ref) {
58
+ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, isEditing, label, onHide, children: injectedChildren, className: injectedClassName, style: injectedStyle, ...rest }, ref) {
40
59
  // DnD root-cause fix (2026-05-07): react-draggable@4 (used internally by
41
60
  // react-grid-layout@2) checks the drag handle via
42
61
  // `matchesSelectorAndParentsTo(target, handle, baseNode)` — it walks UP
@@ -53,8 +72,8 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
53
72
  const outerClass = cn('relative group', isEditing && 'drag-handle cursor-move', injectedClassName);
54
73
  return (_jsxs("div", { ref: ref, className: outerClass, style: injectedStyle, ...rest, children: [_jsxs("div", { className: "h-full w-full relative", children: [isEditing && (_jsx("div", { className: "absolute inset-0 z-10 pointer-events-none rounded-3xl border-2 border-transparent group-hover:border-primary/50 transition-colors bg-black/5" })), isEditing && (_jsx("div", { className: "absolute top-2 left-2 z-30 flex items-center gap-1 pointer-events-none", children: _jsx("span", { className: "text-[10px] px-1.5 py-0.5 rounded-md opacity-80 font-medium bg-muted text-muted-foreground", children: label }) })), isEditing && (_jsx("button", { className: "absolute top-2 right-2 z-30 h-6 w-6 rounded-full flex items-center justify-center bg-destructive/80 hover:bg-destructive text-white shadow transition-colors", "data-no-drag": true, onPointerDown: (event) => event.stopPropagation(), onClick: (event) => {
55
74
  event.stopPropagation();
56
- onRemove(id);
57
- }, title: `Hide ${label}`, "aria-label": `Hide ${label}`, children: _jsx(EyeOff, { className: "h-3 w-3", "aria-hidden": "true" }) })), _jsx("div", { className: "h-full w-full rounded-3xl transition-all flex flex-col bg-card shadow-sm", style: { contain: 'paint' }, children: content })] }), injectedChildren] }));
75
+ onHide(id);
76
+ }, title: `Hide ${label}`, "aria-label": `Hide ${label}`, children: _jsx(EyeOff, { className: "h-3 w-3", "aria-hidden": "true" }) })), _jsx("div", { className: "h-full w-full rounded-3xl transition-all flex flex-col bg-card shadow-sm", style: { contain: 'paint' }, children: _jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: content }) })] }), injectedChildren] }));
58
77
  }));
59
78
  export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, widgets, widgetMeta, className,
60
79
  // Resize default flipped 2026-05-12: rgl v2 dropped auto-handle rendering
@@ -63,7 +82,7 @@ export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVer
63
82
  // the safe default is true so any consumer who forgets to set it still
64
83
  // gets a usable canvas. Opt out per-page with isResizable={false}.
65
84
  isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_NAMES, createEntityWidget, onRegisterEntityWidget, }) {
66
- const { currentLayouts, layoutCols, isEditing, setIsEditing, activeCols, activeCompactor, onLayoutChange, handleColumnChange, handleCompact, handleReset, addWidget, removeWidget, resetConfirmOpen, setResetConfirmOpen, containerRef, containerWidth, } = usePageGridLayout({
85
+ const { currentLayouts, layoutCols, isEditing, setIsEditing, activeCols, activeCompactor, onLayoutChange, handleColumnChange, handleCompact, handleReset, addWidget, moveWidget, setWidgetLocked, resetConfirmOpen, setResetConfirmOpen, containerRef, containerWidth, } = usePageGridLayout({
67
86
  pageKey,
68
87
  defaultLayouts,
69
88
  defaultCols,
@@ -74,12 +93,17 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
74
93
  });
75
94
  const resizeEnabled = isEditing && isResizable;
76
95
  const resizeConstraints = useMemo(() => [
96
+ // `gridBounds` caps width at the active column count, so we don't need
97
+ // a global maxSize(...). Width and height are unbounded by default;
98
+ // consumers cap individual widgets via LayoutItem.maxW / maxH which
99
+ // `minMaxSize` reads per-item.
77
100
  gridBounds,
78
101
  minMaxSize,
79
102
  minSize(DEFAULT_RESIZE_BOUNDS.minW, DEFAULT_RESIZE_BOUNDS.minH),
80
- maxSize(DEFAULT_RESIZE_BOUNDS.maxW, DEFAULT_RESIZE_BOUNDS.maxH),
81
103
  ], []);
82
104
  const [extraWidgetConfigs, setExtraWidgetConfigs] = useState({});
105
+ const { value: layerNames, setValue: setLayerNames } = (preferenceAdapter ?? defaultPreferenceAdapter)(`page:${pageKey}_grid_layer_names`, {});
106
+ const { value: hiddenLayerIds, setValue: setHiddenLayerIds } = (preferenceAdapter ?? defaultPreferenceAdapter)(`page:${pageKey}_grid_hidden_layers`, {});
83
107
  const extraWidgets = useMemo(() => {
84
108
  const rendered = {};
85
109
  if (!createEntityWidget)
@@ -99,10 +123,25 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
99
123
  const activeLayouts = useMemo(() => {
100
124
  const filtered = { lg: [] };
101
125
  for (const bp in currentLayouts) {
102
- filtered[bp] = (currentLayouts[bp] ?? []).filter((item) => renderableWidgetKeys.has(item.i));
126
+ filtered[bp] = (currentLayouts[bp] ?? []).filter((item) => renderableWidgetKeys.has(item.i) && !hiddenLayerIds[item.i]);
103
127
  }
104
128
  return filtered;
105
- }, [currentLayouts, renderableWidgetKeys]);
129
+ }, [currentLayouts, hiddenLayerIds, renderableWidgetKeys]);
130
+ const hideLayer = (layerId) => {
131
+ setHiddenLayerIds((previous) => ({ ...previous, [layerId]: true }));
132
+ };
133
+ const showLayer = (layerId, defaultSize) => {
134
+ setHiddenLayerIds((previous) => {
135
+ if (!previous[layerId])
136
+ return previous;
137
+ const { [layerId]: _removed, ...rest } = previous;
138
+ return rest;
139
+ });
140
+ const existsInLayouts = Object.values(currentLayouts).some((items) => (items ?? []).some((item) => item.i === layerId));
141
+ if (!existsInLayouts) {
142
+ addWidget(layerId, defaultSize);
143
+ }
144
+ };
106
145
  useEffect(() => {
107
146
  if (!createEntityWidget || typeof window === 'undefined')
108
147
  return;
@@ -142,6 +181,30 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
142
181
  ]);
143
182
  const visibleKeys = useMemo(() => new Set((activeLayouts.lg ?? []).map((item) => item.i)), [activeLayouts.lg]);
144
183
  const hiddenWidgetKeys = useMemo(() => [...renderableWidgetKeys].filter((key) => !visibleKeys.has(key)), [renderableWidgetKeys, visibleKeys]);
184
+ const layerItems = useMemo(() => (activeLayouts.lg ?? []).map((item, index, layers) => {
185
+ const fallbackLabel = widgetMeta?.[item.i]?.label ?? item.i;
186
+ return {
187
+ id: item.i,
188
+ label: layerNames[item.i] || fallbackLabel,
189
+ locked: Boolean(item.static || item.isDraggable === false || item.isResizable === false),
190
+ canMoveUp: index > 0,
191
+ canMoveDown: index < layers.length - 1,
192
+ };
193
+ }), [activeLayouts.lg, layerNames, widgetMeta]);
194
+ const handleLayerRename = (layerId, value) => {
195
+ const trimmed = value.trim();
196
+ setLayerNames((previous) => {
197
+ if (trimmed.length === 0) {
198
+ if (!(layerId in previous))
199
+ return previous;
200
+ const { [layerId]: _removed, ...rest } = previous;
201
+ return rest;
202
+ }
203
+ if (previous[layerId] === trimmed)
204
+ return previous;
205
+ return { ...previous, [layerId]: trimmed };
206
+ });
207
+ };
145
208
  // Reset-confirmation dialog: full ARIA APG dialog-modal pattern.
146
209
  // - Escape key closes the dialog.
147
210
  // - Tab / Shift+Tab are trapped within the two dialog buttons (cancel ↔ confirm).
@@ -191,14 +254,24 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
191
254
  previousActiveElement?.focus?.();
192
255
  };
193
256
  }, [resetConfirmOpen]);
194
- return (_jsxs("div", { className: className, children: [isEditing && (_jsxs("div", { className: "flex flex-col gap-3 p-4 rounded-xl shadow-lg border-2 mb-4 bg-card border-primary", 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: "h-10 w-10 rounded-full flex items-center justify-center animate-pulse bg-primary/10", children: _jsx(Settings2, { className: "h-5 w-5 text-primary" }) }), _jsxs("div", { children: [_jsx("h3", { className: "font-semibold text-lg text-foreground", children: "Canvas Editor Active" }), _jsx("span", { className: "text-sm text-muted-foreground", children: "Drag anywhere on a card to move it. Resize with the bottom-right handle." })] })] }), _jsxs("button", { type: "button", className: "shrink-0 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", { className: "flex flex-wrap items-center gap-x-4 gap-y-2 pt-2 border-t border-border", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(LayoutGrid, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "text-sm shrink-0 text-muted-foreground", children: "Columns:" }), _jsx("input", { type: "range", min: 1, max: 24, value: layoutCols, onChange: (event) => handleColumnChange(Number(event.target.value)), className: "w-24 cursor-pointer accent-primary", "aria-label": "Column count" }), _jsx("span", { className: "text-sm font-mono w-5 text-center tabular-nums", children: layoutCols })] }), _jsx("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Column presets", children: [1, 2, 3, 4, 6, 12].map((columnCount) => (_jsx("button", { type: "button", onClick: () => handleColumnChange(columnCount), "data-active": layoutCols === columnCount || undefined, "aria-pressed": layoutCols === columnCount, className: cn('px-2 py-0.5 rounded text-xs font-medium transition-colors border', 'bg-muted text-muted-foreground border-border', 'hover:bg-muted/80 hover:text-foreground', 'data-[active]:bg-primary data-[active]:text-primary-foreground data-[active]:border-primary', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: columnCount }, columnCount))) }), hiddenWidgetKeys.length > 0 && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Eye, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "text-sm shrink-0 text-muted-foreground", children: "Add widget:" }), _jsx("div", { className: "flex flex-wrap gap-1", children: hiddenWidgetKeys.map((key) => {
257
+ return (_jsxs("div", { className: className, children: [isEditing && (_jsxs("div", { className: cn(
258
+ // Sticky overlay banner — sits at the top of the scroll container
259
+ // without pushing the form down. `top-0` anchors to the nearest
260
+ // scrolling ancestor; `z-30` keeps it above grid items but below
261
+ // app-level overlays (toaster, dialogs are z-50+).
262
+ 'sticky top-0 z-30 flex flex-col gap-3 p-4 rounded-xl shadow-lg border-2 mb-4',
263
+ // Subtle translucent background so the form behind it stays
264
+ // partially visible — mitigates Issue 2 (banner consuming
265
+ // vertical space). `bg-card/95` + `backdrop-blur` keeps text
266
+ // legible while showing form context behind the banner.
267
+ '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: "h-10 w-10 rounded-full flex items-center justify-center animate-pulse bg-primary/10", children: _jsx(Settings2, { className: "h-5 w-5 text-primary" }) }), _jsxs("div", { children: [_jsx("h3", { className: "font-semibold text-lg text-foreground", children: "Canvas Editor Active" }), _jsx("span", { className: "text-sm text-muted-foreground", children: "Drag anywhere on a card to move it. Resize with the bottom-right handle." })] })] }), _jsxs("button", { type: "button", className: "shrink-0 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", { className: "flex flex-wrap items-center gap-x-4 gap-y-2 pt-2 border-t border-border", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(LayoutGrid, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "text-sm shrink-0 text-muted-foreground", children: "Columns:" }), _jsx("input", { type: "range", min: 1, max: 24, value: layoutCols, onChange: (event) => handleColumnChange(Number(event.target.value)), className: "w-24 cursor-pointer accent-primary", "aria-label": "Column count" }), _jsx("span", { className: "text-sm font-mono w-5 text-center tabular-nums", children: layoutCols })] }), _jsx("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Column presets", children: [1, 2, 3, 4, 6, 12].map((columnCount) => (_jsx("button", { type: "button", onClick: () => handleColumnChange(columnCount), "data-active": layoutCols === columnCount || undefined, "aria-pressed": layoutCols === columnCount, className: cn('px-2 py-0.5 rounded text-xs font-medium transition-colors border', 'bg-muted text-muted-foreground border-border', 'hover:bg-muted/80 hover:text-foreground', 'data-[active]:bg-primary data-[active]:text-primary-foreground data-[active]:border-primary', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: columnCount }, columnCount))) }), hiddenWidgetKeys.length > 0 && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Eye, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "text-sm shrink-0 text-muted-foreground", children: "Add widget:" }), _jsx("div", { className: "flex flex-wrap gap-1", children: hiddenWidgetKeys.map((key) => {
195
268
  const meta = widgetMeta?.[key];
196
269
  const Icon = meta?.icon;
197
- return (_jsxs("button", { type: "button", onClick: () => addWidget(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" }), meta?.label ?? key] }, key));
198
- }) })] })), _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-black/40 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: () => {
270
+ 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));
271
+ }) })] })), 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-black/40 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: () => {
199
272
  handleReset();
200
273
  setResetConfirmOpen(false);
201
- }, className: cn('inline-flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors', 'bg-destructive text-white 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: isEditing ? 'min-h-[200px]' : '', style: { backgroundColor: isEditing ? 'rgb(0 0 0 / 0.03)' : 'transparent' }, children: _jsx("div", { style: { opacity: containerWidth > 0 ? 1 : 0 }, "aria-busy": containerWidth <= 0, children: _jsx(Responsive, { width: Math.max(containerWidth, 1), className: "layout", layouts: activeLayouts, breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }, rowHeight: 32, onLayoutChange: onLayoutChange, dragConfig: {
274
+ }, className: cn('inline-flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors', 'bg-destructive text-white 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: isEditing ? 'min-h-[200px]' : '', style: { backgroundColor: isEditing ? 'rgb(0 0 0 / 0.03)' : 'transparent' }, "data-page-grid-editing": isEditing || undefined, children: _jsx("div", { style: { opacity: containerWidth > 0 ? 1 : 0 }, "aria-busy": containerWidth <= 0, children: _jsx(Responsive, { width: Math.max(containerWidth, 1), className: "layout", layouts: activeLayouts, breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }, rowHeight: 32, onLayoutChange: onLayoutChange, dragConfig: {
202
275
  enabled: isEditing,
203
276
  handle: '.drag-handle',
204
277
  bounded: false,
@@ -212,6 +285,6 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
212
285
  const content = allWidgets[layoutItem.i];
213
286
  if (content === undefined || content === null)
214
287
  return null;
215
- return (_jsx(GridItem, { id: layoutItem.i, content: content, isEditing: isEditing, label: widgetMeta?.[layoutItem.i]?.label ?? layoutItem.i, onRemove: removeWidget }, layoutItem.i));
288
+ return (_jsx(GridItem, { id: layoutItem.i, content: content, isEditing: isEditing, label: layerNames[layoutItem.i] || widgetMeta?.[layoutItem.i]?.label || layoutItem.i, onHide: hideLayer }, layoutItem.i));
216
289
  }) }) }) })] }));
217
290
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { PageGridLayout } from './PageGridLayout.js';
2
- export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES } from './usePageGridLayout.js';
2
+ export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.js';
3
+ export type { PageGridEditingEventDetail } from './usePageGridLayout.js';
3
4
  export { rescaleLayout } from './rescaleLayout.js';
4
5
  export { WYSIWYG_REQUIRED_PRIMITIVES, WYSIWYG_SURFACE_CONTRACTS, assertWysiwygPrimitiveCoverage, getWysiwygSurfaceContract, } from './wysiwygContract.js';
5
6
  export type { WysiwygPrimitive, WysiwygSurfaceContract, WysiwygSurfaceId, } from './wysiwygContract.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACvF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,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,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,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,5 +1,5 @@
1
1
  export { PageGridLayout } from './PageGridLayout.js';
2
- export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES } from './usePageGridLayout.js';
2
+ export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES, PAGE_GRID_EDITING_EVENT, } from './usePageGridLayout.js';
3
3
  export { rescaleLayout } from './rescaleLayout.js';
4
4
  export { WYSIWYG_REQUIRED_PRIMITIVES, WYSIWYG_SURFACE_CONTRACTS, assertWysiwygPrimitiveCoverage, getWysiwygSurfaceContract, } from './wysiwygContract.js';
5
5
  export { buildResponsiveLayouts, RESPONSIVE_BREAKPOINTS, } from './buildResponsiveLayouts.js';
package/dist/types.d.ts CHANGED
@@ -64,6 +64,8 @@ export interface UsePageGridLayoutResult {
64
64
  handleCompact: () => void;
65
65
  handleReset: () => void;
66
66
  addWidget: (widgetKey: string, initialSize?: Partial<Pick<GridLayoutItem, 'w' | 'h' | 'minW' | 'minH'>>) => void;
67
+ moveWidget: (widgetKey: string, direction: 'up' | 'down') => void;
68
+ setWidgetLocked: (widgetKey: string, locked: boolean) => void;
67
69
  removeWidget: (widgetKey: string) => void;
68
70
  resetConfirmOpen: boolean;
69
71
  setResetConfirmOpen: React.Dispatch<React.SetStateAction<boolean>>;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,18 @@
1
1
  import type { UsePageGridLayoutOptions, UsePageGridLayoutResult } from './types.js';
2
2
  export declare const DEFAULT_EDITOR_EVENT_NAMES: readonly ["bsuite-open-page-editor", "bsu-open-page-editor", "crm7-open-page-editor", "conduit-open-page-editor", "r80-open-page-editor"];
3
+ /**
4
+ * Event name broadcast on `window` whenever any PageGridLayout transitions
5
+ * its `isEditing` state. Launcher widgets (e.g. floating "Edit Page" FABs)
6
+ * subscribe to this so they can hide themselves while the canvas editor is
7
+ * already active — prevents the redundant-affordance UX issue where a
8
+ * "Edit Page" button sits in the corner while the editor banner is visible
9
+ * at the top. `detail.editing` is the new state; `detail.pageKey` lets
10
+ * launchers scope by page if they handle multiple grids on one screen.
11
+ */
12
+ export declare const PAGE_GRID_EDITING_EVENT = "bsuite-page-grid-editing";
13
+ export interface PageGridEditingEventDetail {
14
+ pageKey: string;
15
+ editing: boolean;
16
+ }
3
17
  export declare function usePageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, }: UsePageGridLayoutOptions): UsePageGridLayoutResult;
4
18
  //# sourceMappingURL=usePageGridLayout.d.ts.map
@@ -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,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,cAAc,EACd,WAAgB,EAChB,aAAiB,EACjB,WAAkB,EAClB,gBAA6C,EAC7C,iBAA4C,GAC7C,EAAE,wBAAwB,GAAG,uBAAuB,CA6NpD"}
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,CA8UpD"}
@@ -10,10 +10,32 @@ export const DEFAULT_EDITOR_EVENT_NAMES = [
10
10
  'conduit-open-page-editor',
11
11
  'r80-open-page-editor',
12
12
  ];
13
+ /**
14
+ * Event name broadcast on `window` whenever any PageGridLayout transitions
15
+ * its `isEditing` state. Launcher widgets (e.g. floating "Edit Page" FABs)
16
+ * subscribe to this so they can hide themselves while the canvas editor is
17
+ * already active — prevents the redundant-affordance UX issue where a
18
+ * "Edit Page" button sits in the corner while the editor banner is visible
19
+ * at the top. `detail.editing` is the new state; `detail.pageKey` lets
20
+ * launchers scope by page if they handle multiple grids on one screen.
21
+ */
22
+ export const PAGE_GRID_EDITING_EVENT = 'bsuite-page-grid-editing';
13
23
  export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, layoutVersion = 1, canEditPage = true, editorEventNames = DEFAULT_EDITOR_EVENT_NAMES, preferenceAdapter = defaultPreferenceAdapter, }) {
14
24
  const containerRef = useRef(null);
15
25
  const [containerWidth, setContainerWidth] = useState(0);
16
- const [isEditing, setIsEditing] = useState(false);
26
+ const [isEditing, setIsEditingState] = useState(false);
27
+ const setIsEditing = useCallback((next) => {
28
+ setIsEditingState((previous) => {
29
+ const resolved = typeof next === 'function' ? next(previous) : next;
30
+ if (resolved !== previous && typeof window !== 'undefined') {
31
+ // Broadcast to FAB launchers so they can hide while the editor is open.
32
+ window.dispatchEvent(new CustomEvent(PAGE_GRID_EDITING_EVENT, {
33
+ detail: { pageKey, editing: resolved },
34
+ }));
35
+ }
36
+ return resolved;
37
+ });
38
+ }, [pageKey]);
17
39
  const [resetConfirmOpen, setResetConfirmOpen] = useState(false);
18
40
  useLayoutEffect(() => {
19
41
  const el = containerRef.current;
@@ -100,10 +122,54 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
100
122
  }, [baseCols, layoutCols, rawLayouts, setBaseCols, setSavedLayout]);
101
123
  const activeCols = useMemo(() => ({ lg: layoutCols, md: layoutCols, sm: layoutCols, xs: layoutCols, xxs: layoutCols }), [layoutCols]);
102
124
  const activeCompactor = useMemo(() => (isEditing ? { ...noCompactor, preventCollision: true } : verticalCompactor), [isEditing]);
125
+ /**
126
+ * Throttled layout-change handler.
127
+ *
128
+ * `react-grid-layout` fires `onLayoutChange` on **every** drag/resize tick
129
+ * (≈60Hz). The previous implementation called `setSavedLayout` directly,
130
+ * which delegates to a preference adapter — in apps like crm7 that adapter
131
+ * writes to localStorage and/or queues a Supabase upsert per call. Operators
132
+ * reported 6–7s INP blocks on resize gestures (long-task warnings on
133
+ * `.react-resizable-handle-se` and `.react-grid-layout`).
134
+ *
135
+ * Fix: capture the latest layout in a ref each tick (cheap, no React work)
136
+ * and only commit to the preference adapter on a trailing rAF tick. This
137
+ * cuts adapter calls from ~60/sec to ~1/sec during a typical drag while
138
+ * still persisting the final position when the gesture ends. The component
139
+ * still receives `activeLayouts` from `savedLayout` synchronously, so the
140
+ * grid keeps following the cursor visually.
141
+ */
142
+ const pendingLayoutRef = useRef(null);
143
+ const layoutCommitFrameRef = useRef(null);
103
144
  const onLayoutChange = useCallback((_layout, layouts) => {
104
- if (isEditing)
145
+ if (!isEditing)
146
+ return;
147
+ pendingLayoutRef.current = layouts;
148
+ if (layoutCommitFrameRef.current !== null)
149
+ return;
150
+ if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') {
105
151
  setSavedLayout(layouts);
152
+ return;
153
+ }
154
+ // Trailing rAF — lets a burst of resize ticks coalesce into one commit.
155
+ layoutCommitFrameRef.current = window.requestAnimationFrame(() => {
156
+ layoutCommitFrameRef.current = null;
157
+ const pending = pendingLayoutRef.current;
158
+ if (pending) {
159
+ pendingLayoutRef.current = null;
160
+ setSavedLayout(pending);
161
+ }
162
+ });
106
163
  }, [isEditing, setSavedLayout]);
164
+ useEffect(() => {
165
+ return () => {
166
+ if (layoutCommitFrameRef.current !== null &&
167
+ typeof window !== 'undefined' &&
168
+ typeof window.cancelAnimationFrame === 'function') {
169
+ window.cancelAnimationFrame(layoutCommitFrameRef.current);
170
+ }
171
+ };
172
+ }, []);
107
173
  const handleColumnChange = useCallback((newCols) => {
108
174
  const rescaled = rescaleLayout(currentLayouts, layoutCols, newCols);
109
175
  setSavedLayout(rescaled);
@@ -153,6 +219,47 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
153
219
  }
154
220
  startTransition(() => setSavedLayout(updated));
155
221
  }, [currentLayouts, layoutCols, setSavedLayout]);
222
+ const moveWidget = useCallback((widgetKey, direction) => {
223
+ const updated = { lg: [] };
224
+ for (const bp of Object.keys(currentLayouts)) {
225
+ const items = [...(currentLayouts[bp] ?? [])];
226
+ const index = items.findIndex((item) => item.i === widgetKey);
227
+ if (index === -1) {
228
+ updated[bp] = items;
229
+ continue;
230
+ }
231
+ const targetIndex = direction === 'up' ? index - 1 : index + 1;
232
+ if (targetIndex < 0 || targetIndex >= items.length) {
233
+ updated[bp] = items;
234
+ continue;
235
+ }
236
+ const moved = items[index];
237
+ const withoutMoved = [...items.slice(0, index), ...items.slice(index + 1)];
238
+ const nextItems = [
239
+ ...withoutMoved.slice(0, targetIndex),
240
+ moved,
241
+ ...withoutMoved.slice(targetIndex),
242
+ ];
243
+ updated[bp] = nextItems;
244
+ }
245
+ startTransition(() => setSavedLayout(updated));
246
+ }, [currentLayouts, setSavedLayout]);
247
+ const setWidgetLocked = useCallback((widgetKey, locked) => {
248
+ const updated = { lg: [] };
249
+ for (const bp of Object.keys(currentLayouts)) {
250
+ updated[bp] = (currentLayouts[bp] ?? []).map((item) => {
251
+ if (item.i !== widgetKey)
252
+ return item;
253
+ return {
254
+ ...item,
255
+ static: locked,
256
+ isDraggable: !locked,
257
+ isResizable: !locked,
258
+ };
259
+ });
260
+ }
261
+ startTransition(() => setSavedLayout(updated));
262
+ }, [currentLayouts, setSavedLayout]);
156
263
  const removeWidget = useCallback((widgetKey) => {
157
264
  const updated = { lg: [] };
158
265
  for (const bp of Object.keys(currentLayouts)) {
@@ -191,6 +298,8 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
191
298
  handleCompact,
192
299
  handleReset,
193
300
  addWidget,
301
+ moveWidget,
302
+ setWidgetLocked,
194
303
  removeWidget,
195
304
  resetConfirmOpen,
196
305
  setResetConfirmOpen,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsuite/page-builder",
3
- "version": "0.2.9",
3
+ "version": "0.4.0",
4
4
  "description": "Shared BSuite responsive page-builder grid and layout persistence primitives",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",