@bsuite/page-builder 0.2.9 → 0.3.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;AAqJnE,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,2CA4frB"}
@@ -1,13 +1,25 @@
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
5
  import { gridBounds, maxSize, 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
+ * Resize bounds for the bottom-right handle.
13
+ *
14
+ * `maxH` was 16 (≈512px at rowHeight=32) which clipped data tables holding
15
+ * more than ~7 rows — operators reported "the resize handle hits a wall
16
+ * before all my data fits". Bumped to 48 (≈1536px) which comfortably fits
17
+ * ~20–30 list rows of any density and matches the maximum viewport height
18
+ * on common displays. Cards that still need to show more data should rely
19
+ * on their own internal scroll container (we apply `min-h-0 overflow-auto`
20
+ * to the card body wrapper below) rather than growing without bound.
21
+ */
22
+ const DEFAULT_RESIZE_BOUNDS = { minW: 2, minH: 1, maxW: 12, maxH: 48 };
11
23
  const DEFAULT_RESIZE_HANDLES = ['se'];
12
24
  const DEFAULT_ADD_ENTITY_WIDGET_EVENT_NAMES = ['bsu-add-entity-widget', 'crm7-add-entity-widget'];
13
25
  /**
@@ -36,7 +48,7 @@ const DEFAULT_ADD_ENTITY_WIDGET_EVENT_NAMES = ['bsu-add-entity-widget', 'crm7-ad
36
48
  const RGLResizeHandle = React.forwardRef(function RGLResizeHandle({ axis = 'se', className: injectedClassName, ...rest }, ref) {
37
49
  return (_jsx("span", { ref: ref, "data-no-drag": true, className: `react-resizable-handle react-resizable-handle-${axis}${injectedClassName ? ' ' + injectedClassName : ''}`, ...rest }));
38
50
  });
39
- const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, isEditing, label, onRemove, children: injectedChildren, className: injectedClassName, style: injectedStyle, ...rest }, ref) {
51
+ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, isEditing, label, onHide, children: injectedChildren, className: injectedClassName, style: injectedStyle, ...rest }, ref) {
40
52
  // DnD root-cause fix (2026-05-07): react-draggable@4 (used internally by
41
53
  // react-grid-layout@2) checks the drag handle via
42
54
  // `matchesSelectorAndParentsTo(target, handle, baseNode)` — it walks UP
@@ -53,7 +65,7 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
53
65
  const outerClass = cn('relative group', isEditing && 'drag-handle cursor-move', injectedClassName);
54
66
  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
67
  event.stopPropagation();
56
- onRemove(id);
68
+ onHide(id);
57
69
  }, 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] }));
58
70
  }));
59
71
  export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, widgets, widgetMeta, className,
@@ -63,7 +75,7 @@ export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVer
63
75
  // the safe default is true so any consumer who forgets to set it still
64
76
  // gets a usable canvas. Opt out per-page with isResizable={false}.
65
77
  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({
78
+ const { currentLayouts, layoutCols, isEditing, setIsEditing, activeCols, activeCompactor, onLayoutChange, handleColumnChange, handleCompact, handleReset, addWidget, moveWidget, setWidgetLocked, resetConfirmOpen, setResetConfirmOpen, containerRef, containerWidth, } = usePageGridLayout({
67
79
  pageKey,
68
80
  defaultLayouts,
69
81
  defaultCols,
@@ -80,6 +92,8 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
80
92
  maxSize(DEFAULT_RESIZE_BOUNDS.maxW, DEFAULT_RESIZE_BOUNDS.maxH),
81
93
  ], []);
82
94
  const [extraWidgetConfigs, setExtraWidgetConfigs] = useState({});
95
+ const { value: layerNames, setValue: setLayerNames } = (preferenceAdapter ?? defaultPreferenceAdapter)(`page:${pageKey}_grid_layer_names`, {});
96
+ const { value: hiddenLayerIds, setValue: setHiddenLayerIds } = (preferenceAdapter ?? defaultPreferenceAdapter)(`page:${pageKey}_grid_hidden_layers`, {});
83
97
  const extraWidgets = useMemo(() => {
84
98
  const rendered = {};
85
99
  if (!createEntityWidget)
@@ -99,10 +113,25 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
99
113
  const activeLayouts = useMemo(() => {
100
114
  const filtered = { lg: [] };
101
115
  for (const bp in currentLayouts) {
102
- filtered[bp] = (currentLayouts[bp] ?? []).filter((item) => renderableWidgetKeys.has(item.i));
116
+ filtered[bp] = (currentLayouts[bp] ?? []).filter((item) => renderableWidgetKeys.has(item.i) && !hiddenLayerIds[item.i]);
103
117
  }
104
118
  return filtered;
105
- }, [currentLayouts, renderableWidgetKeys]);
119
+ }, [currentLayouts, hiddenLayerIds, renderableWidgetKeys]);
120
+ const hideLayer = (layerId) => {
121
+ setHiddenLayerIds((previous) => ({ ...previous, [layerId]: true }));
122
+ };
123
+ const showLayer = (layerId, defaultSize) => {
124
+ setHiddenLayerIds((previous) => {
125
+ if (!previous[layerId])
126
+ return previous;
127
+ const { [layerId]: _removed, ...rest } = previous;
128
+ return rest;
129
+ });
130
+ const existsInLayouts = Object.values(currentLayouts).some((items) => (items ?? []).some((item) => item.i === layerId));
131
+ if (!existsInLayouts) {
132
+ addWidget(layerId, defaultSize);
133
+ }
134
+ };
106
135
  useEffect(() => {
107
136
  if (!createEntityWidget || typeof window === 'undefined')
108
137
  return;
@@ -142,6 +171,30 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
142
171
  ]);
143
172
  const visibleKeys = useMemo(() => new Set((activeLayouts.lg ?? []).map((item) => item.i)), [activeLayouts.lg]);
144
173
  const hiddenWidgetKeys = useMemo(() => [...renderableWidgetKeys].filter((key) => !visibleKeys.has(key)), [renderableWidgetKeys, visibleKeys]);
174
+ const layerItems = useMemo(() => (activeLayouts.lg ?? []).map((item, index, layers) => {
175
+ const fallbackLabel = widgetMeta?.[item.i]?.label ?? item.i;
176
+ return {
177
+ id: item.i,
178
+ label: layerNames[item.i] || fallbackLabel,
179
+ locked: Boolean(item.static || item.isDraggable === false || item.isResizable === false),
180
+ canMoveUp: index > 0,
181
+ canMoveDown: index < layers.length - 1,
182
+ };
183
+ }), [activeLayouts.lg, layerNames, widgetMeta]);
184
+ const handleLayerRename = (layerId, value) => {
185
+ const trimmed = value.trim();
186
+ setLayerNames((previous) => {
187
+ if (trimmed.length === 0) {
188
+ if (!(layerId in previous))
189
+ return previous;
190
+ const { [layerId]: _removed, ...rest } = previous;
191
+ return rest;
192
+ }
193
+ if (previous[layerId] === trimmed)
194
+ return previous;
195
+ return { ...previous, [layerId]: trimmed };
196
+ });
197
+ };
145
198
  // Reset-confirmation dialog: full ARIA APG dialog-modal pattern.
146
199
  // - Escape key closes the dialog.
147
200
  // - Tab / Shift+Tab are trapped within the two dialog buttons (cancel ↔ confirm).
@@ -191,14 +244,24 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
191
244
  previousActiveElement?.focus?.();
192
245
  };
193
246
  }, [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) => {
247
+ return (_jsxs("div", { className: className, children: [isEditing && (_jsxs("div", { className: cn(
248
+ // Sticky overlay banner — sits at the top of the scroll container
249
+ // without pushing the form down. `top-0` anchors to the nearest
250
+ // scrolling ancestor; `z-30` keeps it above grid items but below
251
+ // app-level overlays (toaster, dialogs are z-50+).
252
+ 'sticky top-0 z-30 flex flex-col gap-3 p-4 rounded-xl shadow-lg border-2 mb-4',
253
+ // Subtle translucent background so the form behind it stays
254
+ // partially visible — mitigates Issue 2 (banner consuming
255
+ // vertical space). `bg-card/95` + `backdrop-blur` keeps text
256
+ // legible while showing form context behind the banner.
257
+ '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
258
  const meta = widgetMeta?.[key];
196
259
  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: () => {
260
+ 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));
261
+ }) })] })), 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
262
  handleReset();
200
263
  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: {
264
+ }, 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
265
  enabled: isEditing,
203
266
  handle: '.drag-handle',
204
267
  bounded: false,
@@ -212,6 +275,6 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEvent
212
275
  const content = allWidgets[layoutItem.i];
213
276
  if (content === undefined || content === null)
214
277
  return null;
215
- return (_jsx(GridItem, { id: layoutItem.i, content: content, isEditing: isEditing, label: widgetMeta?.[layoutItem.i]?.label ?? layoutItem.i, onRemove: removeWidget }, layoutItem.i));
278
+ 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
279
  }) }) }) })] }));
217
280
  }
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.3.0",
4
4
  "description": "Shared BSuite responsive page-builder grid and layout persistence primitives",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",