@bsuite/page-builder 2.7.0 → 2.7.1-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,3 +48,26 @@ Behaviour and guarantees:
48
48
 
49
49
  When adding `autoHeight` to an existing page's layout, bump that page's
50
50
  `layoutVersion` so stale persisted layouts (without the flag) are reset.
51
+
52
+ ## Edit-mode chrome (2.7.1-next.0+)
53
+
54
+ `PAGE_GRID_EDITING_EVENT` reports committed changes from `2.7.1-next.1`.
55
+ Its `{ pageKey, editing }` payload and event name are unchanged. The matching
56
+ canvas DOM is updated before listeners run, including when Save & Exit uses
57
+ a React transition. Initial mounting, unchanged state and abandoned transitions
58
+ do not emit notifications. Consumers must match the page key and preserve
59
+ focus on surviving inputs when synchronizing their header controls.
60
+
61
+ While the canvas editor is open, each grid item renders an in-flow strip
62
+ (`[data-slot="grid-item-editor-chrome"]`) above the card content: the widget
63
+ name and a Hide control. The strip is **not** absolutely positioned over the
64
+ card heading. AutoHeight measures it with the content so the grid grows;
65
+ leaving edit mode removes it and restores the content-only measurement.
66
+ Do not restyle this in a consumer — it is the shared contract.
67
+
68
+ The label carries `min-w-0 truncate` and a `title` with the full name. That is
69
+ load-bearing on a narrow card, not decoration: without it the label's automatic
70
+ minimum size is its min-content, and a long unbroken widget name pushes the
71
+ `ml-auto` hide button outside the card box. Measured both ways in
72
+ `scripts/editor-chrome-geometry.mjs`, which reproduces the defect with the two
73
+ classes removed at run time.
@@ -1 +1 @@
1
- {"version":3,"file":"PageGridLayout.d.ts","sourceRoot":"","sources":["../src/PageGridLayout.tsx"],"names":[],"mappings":"AAEA,OAAO,KAQN,MAAM,OAAO,CAAC;AAGf,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAsBxC,OAAO,KAAK,EAAe,mBAAmB,EAA4B,MAAM,YAAY,CAAC;AAmmB7F,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,OAAO,EACP,UAAU,EACV,SAAS,EAMT,WAAkB,EAClB,aAAsC,EACtC,QAAQ,EACR,iBAAiB,EACjB,gBAAgB,EAKhB,UAAkB,EAClB,yBAAiE,EACjE,kBAAkB,EAClB,sBAAsB,EACtB,mBAAmB,EACnB,+BAA6E,EAC7E,wBAAwB,EACxB,4BAA4B,EAC5B,4BAA4B,GAC7B,EAAE,mBAAmB,qBAu+BrB"}
1
+ {"version":3,"file":"PageGridLayout.d.ts","sourceRoot":"","sources":["../src/PageGridLayout.tsx"],"names":[],"mappings":"AAEA,OAAO,KAQN,MAAM,OAAO,CAAC;AAGf,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAsBxC,OAAO,KAAK,EAAe,mBAAmB,EAA4B,MAAM,YAAY,CAAC;AA2pB7F,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,OAAO,EACP,UAAU,EACV,SAAS,EAMT,WAAkB,EAClB,aAAsC,EACtC,QAAQ,EACR,iBAAiB,EACjB,gBAAgB,EAKhB,UAAkB,EAClB,yBAAiE,EACjE,kBAAkB,EAClB,sBAAsB,EACtB,mBAAmB,EACnB,+BAA6E,EAC7E,wBAAwB,EACxB,4BAA4B,EAC5B,4BAA4B,GAC7B,EAAE,mBAAmB,qBAu+BrB"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ElementScopeProvider } from './elementScope.js';
3
3
  import { ArrowDown, ArrowUp, ChevronDown, ChevronUp, Eye, EyeOff, Layers, LayoutGrid, Lock, Plus, RotateCcw, Save, Settings2, Unlock } from 'lucide-react';
4
4
  import React, { startTransition, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
@@ -109,6 +109,31 @@ const RGLResizeHandle = React.forwardRef(function RGLResizeHandle({ axis = 'se',
109
109
  * the silent case audible in development and leaves production untouched, which
110
110
  * is the same trade @bsuite/theme 1.3.0 made for its missing-provider warning.
111
111
  */
112
+ /**
113
+ * Edit-mode label + hide. These used to be `absolute top-2 left-2` /
114
+ * `top-2 right-2` overlays on the card (PageGridLayout GridItem, historically
115
+ * around the 407/415 marks). That painted the widget name and the hide control
116
+ * on top of the card heading — measured on the Candidates card at 1440.
117
+ *
118
+ * An earlier draft of this comment also claimed 390. Withdrawn: the instrument
119
+ * that produced that number never asserted its own `innerWidth`, and the cell
120
+ * labelled "390" reported 1560. What 390 actually looks like is now measured
121
+ * and recorded in `editor-chrome-geometry-390.json` — and it is a different
122
+ * fact: below a 480px container `buildResponsiveLayouts` stacks every card to
123
+ * full width, so 390 has no narrow card at all.
124
+ *
125
+ * In-flow, measured: the strip is a flex-none row. AutoHeight includes it in
126
+ * the unconstrained measure wrapper so the grid grows rather than overlapping.
127
+ * Fixed-height cards keep it *above* the overflow-auto body so hide cannot
128
+ * scroll away. Drag still starts from the strip (outer `.drag-handle`); hide
129
+ * is a `button` + `data-no-drag` so the existing cancel selector ignores it.
130
+ */
131
+ function GridItemEditorChrome({ id, label, onHide, }) {
132
+ return (_jsxs("div", { "data-slot": "grid-item-editor-chrome", className: "flex min-w-0 flex-none items-center gap-1 px-2 py-1 bg-muted text-muted-foreground", children: [_jsx("span", { className: "min-w-0 truncate text-[10px] px-1.5 py-0.5 rounded-md font-medium", title: label, children: label }), _jsx("button", { type: "button", className: "ml-auto h-6 w-6 shrink-0 rounded-full flex items-center justify-center bg-destructive hover:bg-destructive text-destructive-foreground shadow transition-colors", "data-no-drag": true, onPointerDown: (event) => event.stopPropagation(), onClick: (event) => {
133
+ event.stopPropagation();
134
+ onHide(id);
135
+ }, title: `Hide ${label}`, "aria-label": `Hide ${label}`, children: _jsx(EyeOff, { className: "h-3 w-3", "aria-hidden": "true" }) })] }));
136
+ }
112
137
  let warnedAboutCollapsedContent = false;
113
138
  function warnOnceAboutCollapsedContent(el) {
114
139
  if (warnedAboutCollapsedContent)
@@ -230,7 +255,9 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
230
255
  reportRows(contentPx);
231
256
  else
232
257
  warnOnceAboutCollapsedContent(el);
233
- }, [autoHeight, onAutoHeightChange, reportRows]);
258
+ // Re-measure when edit chrome mounts/unmounts so the strip is in the
259
+ // row count in edit mode and gone again on lossless return.
260
+ }, [autoHeight, onAutoHeightChange, reportRows, isEditing]);
234
261
  useEffect(() => {
235
262
  if (!autoHeight || !onAutoHeightChange)
236
263
  return;
@@ -280,10 +307,7 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
280
307
  // (buttons, inputs, textarea, select, links, [data-no-drag]) are still
281
308
  // protected via the `cancel` selector below in the <Responsive> render.
282
309
  const outerClass = cn('relative group overflow-visible', isEditing && 'drag-handle cursor-move', injectedClassName);
283
- 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-primary/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-destructive-foreground shadow transition-colors", "data-no-drag": true, onPointerDown: (event) => event.stopPropagation(), onClick: (event) => {
284
- event.stopPropagation();
285
- onHide(id);
286
- }, title: `Hide ${label}`, "aria-label": `Hide ${label}`, children: _jsx(EyeOff, { className: "h-3 w-3", "aria-hidden": "true" }) })), _jsx("div", { "data-slot": "grid-item-surface", "data-chrome": chrome ? 'on' : 'off', "data-grid-slot-key": id, className: chrome
310
+ 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-primary/5" })), _jsx("div", { "data-slot": "grid-item-surface", "data-chrome": chrome ? 'on' : 'off', "data-grid-slot-key": id, className: chrome
287
311
  ? // ONE radius token, read by the grid item AND available to any
288
312
  // app card that opts into the same surface. Two surfaces
289
313
  // reading the SAME var is what makes their corners align, and
@@ -346,7 +370,7 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
346
370
  ...(cardStyleVars && '--card-padding' in cardStyleVars
347
371
  ? { padding: cardStyleVars['--card-padding'] }
348
372
  : null),
349
- }, children: _jsx("div", { className: cn('min-h-0', autoHeight ? 'flex-none overflow-visible' : 'flex-1 overflow-auto'), children: autoHeight ? (_jsx("div", { ref: measureRef, className: "flow-root", children: scopedContent })) : (scopedContent) }) })] }), injectedChildren] }));
373
+ }, children: autoHeight ? (_jsx("div", { "data-slot": "grid-item-body", className: "min-h-0 flex-none overflow-visible", children: _jsxs("div", { ref: measureRef, className: "flow-root", children: [isEditing ? (_jsx(GridItemEditorChrome, { id: id, label: label, onHide: onHide })) : null, scopedContent] }) })) : (_jsxs(_Fragment, { children: [isEditing ? (_jsx(GridItemEditorChrome, { id: id, label: label, onHide: onHide })) : null, _jsx("div", { "data-slot": "grid-item-body", className: "min-h-0 flex-1 overflow-auto", children: scopedContent })] })) })] }), injectedChildren] }));
350
374
  }));
351
375
  export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, widgets, widgetMeta, className,
352
376
  // Resize default flipped 2026-05-12: rgl v2 dropped auto-handle rendering
@@ -1,13 +1,10 @@
1
1
  import type { GridLayouts, LayoutMigration, 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
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.
4
+ * Broadcast after a PageGridLayout commits an editing-state change. Header
5
+ * launchers use this to synchronize their label and restore keyboard focus
6
+ * after canvas controls disappear. Initial state and abandoned transitions do
7
+ * not publish an event. `detail.pageKey` identifies the changed canvas.
11
8
  */
12
9
  export declare const PAGE_GRID_EDITING_EVENT = "bsuite-page-grid-editing";
13
10
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"usePageGridLayout.d.ts","sourceRoot":"","sources":["../src/usePageGridLayout.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAEV,WAAW,EACX,eAAe,EACf,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,0BAA0B,2IAM7B,CAAC;AAEX;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAiBzC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,IAAI,EACJ,EAAE,EACF,UAAU,GACX,EAAE;IACD,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,IAAI,CAAC;CACzD,GAAG,WAAW,GAAG,IAAI,CA+BrB;AAED,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,EAC5C,iBAAiB,EACjB,gBAAgB,GACjB,EAAE,wBAAwB,GAAG,uBAAuB,CA8yBpD"}
1
+ {"version":3,"file":"usePageGridLayout.d.ts","sourceRoot":"","sources":["../src/usePageGridLayout.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAEV,WAAW,EACX,eAAe,EACf,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,0BAA0B,2IAM7B,CAAC;AAEX;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAiBzC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,IAAI,EACJ,EAAE,EACF,UAAU,GACX,EAAE;IACD,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,IAAI,CAAC;CACzD,GAAG,WAAW,GAAG,IAAI,CA+BrB;AAED,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,EAC5C,iBAAiB,EACjB,gBAAgB,GACjB,EAAE,wBAAwB,GAAG,uBAAuB,CAuyBpD"}
@@ -11,13 +11,10 @@ export const DEFAULT_EDITOR_EVENT_NAMES = [
11
11
  'r80-open-page-editor',
12
12
  ];
13
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.
14
+ * Broadcast after a PageGridLayout commits an editing-state change. Header
15
+ * launchers use this to synchronize their label and restore keyboard focus
16
+ * after canvas controls disappear. Initial state and abandoned transitions do
17
+ * not publish an event. `detail.pageKey` identifies the changed canvas.
21
18
  */
22
19
  export const PAGE_GRID_EDITING_EVENT = 'bsuite-page-grid-editing';
23
20
  /**
@@ -124,19 +121,16 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
124
121
  const effectiveLayoutVersion = layoutVersion + PACKAGE_LAYOUT_EPOCH;
125
122
  const containerRef = useRef(null);
126
123
  const [containerWidth, setContainerWidth] = useState(0);
127
- const [isEditing, setIsEditingState] = useState(false);
128
- const setIsEditing = useCallback((next) => {
129
- setIsEditingState((previous) => {
130
- const resolved = typeof next === 'function' ? next(previous) : next;
131
- if (resolved !== previous && typeof window !== 'undefined') {
132
- // Broadcast to FAB launchers so they can hide while the editor is open.
133
- window.dispatchEvent(new CustomEvent(PAGE_GRID_EDITING_EVENT, {
134
- detail: { pageKey, editing: resolved },
135
- }));
136
- }
137
- return resolved;
138
- });
139
- }, [pageKey]);
124
+ const [isEditing, setIsEditing] = useState(false);
125
+ const publishedEditing = useRef(false);
126
+ useEffect(() => {
127
+ if (publishedEditing.current === isEditing)
128
+ return;
129
+ publishedEditing.current = isEditing;
130
+ window.dispatchEvent(new CustomEvent(PAGE_GRID_EDITING_EVENT, {
131
+ detail: { pageKey, editing: isEditing },
132
+ }));
133
+ }, [isEditing, pageKey]);
140
134
  const [resetConfirmOpen, setResetConfirmOpen] = useState(false);
141
135
  useLayoutEffect(() => {
142
136
  const el = containerRef.current;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsuite/page-builder",
3
- "version": "2.7.0",
3
+ "version": "2.7.1-next.1",
4
4
  "description": "Shared BSuite responsive page-builder grid and layout persistence primitives",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -33,6 +33,7 @@
33
33
  "build": "tsc -p tsconfig.build.json",
34
34
  "typecheck": "tsc --noEmit",
35
35
  "test": "vitest run",
36
+ "test:geometry": "node scripts/editor-chrome-geometry.mjs",
36
37
  "prepublishOnly": "pnpm build"
37
38
  },
38
39
  "peerDependencies": {