@grafloria/element 0.4.13 → 0.4.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafloria/element",
3
- "version": "0.4.13",
3
+ "version": "0.4.14",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -106,6 +106,25 @@ export interface DashboardWidgetSpec {
106
106
  * extent of the declared children.
107
107
  */
108
108
  maxRows?: number;
109
+ /**
110
+ * Container only: the inner board's layout, exactly as a view's. `'grid'`
111
+ * (default) packs the children in cells; `'split'` is a splitter tree that
112
+ * always covers the pane. Switch live with `setLayout(mode, containerId)`;
113
+ * `toJSON()` writes it per container.
114
+ */
115
+ layout?: 'grid' | 'split';
116
+ /**
117
+ * Container only, split layout: the authored splitter tree. Omit it and the
118
+ * tree is derived from the children's cells. `toJSON()` writes it back.
119
+ */
120
+ tree?: SplitNode | null;
121
+ /**
122
+ * Container only: what a pull past the pane's rows does. `'grow'` (default):
123
+ * the container's slab grows a row in the parent — the ratchet above.
124
+ * `'fit'`: the pane is the bound — a child that needs a row the pane does
125
+ * not hold is refused where it stands, and nothing outside the pane moves.
126
+ */
127
+ sizing?: 'fit' | 'grow';
109
128
  }
110
129
  /** One board. Multiple views are the tab pattern: only one is on-camera. */
111
130
  export interface DashboardViewSpec {
@@ -42,7 +42,7 @@
42
42
  * Cells are the truth and live in the existing `GridItemConfig`, so save/load
43
43
  * round-trips with no extra work — same as every other kit.
44
44
  */
45
- import { __awaiter } from "tslib";
45
+ import { __awaiter, __rest } from "tslib";
46
46
  import { BatchCommand, BringNodeToFrontCommand, Command, GroupModel, NodeModel, RemoveFromGroupCommand, RemoveGroupCommand, RemoveNodeCommand, SendNodeToBackCommand, } from '@grafloria/engine';
47
47
  import { bindDashboardGrid, } from './grid-binder.js';
48
48
  import { bindDashboardSplit, SPLIT_TREE_KEY } from './split-binder.js';
@@ -358,7 +358,7 @@ export function createDashboardHandle(ctx) {
358
358
  * tile under the container it is actually in, in every one of those states.
359
359
  */
360
360
  const treeOf = (boardId) => {
361
- var _a, _b, _c, _d;
361
+ var _a, _b, _c, _d, _e, _f;
362
362
  const g = ctx.boardGroups.get(boardId);
363
363
  const b = binders.get(boardId);
364
364
  if (!g)
@@ -373,7 +373,14 @@ export function createDashboardHandle(ctx) {
373
373
  const cell = cellOf(memberId);
374
374
  const at = cell ? { x: cell.x, y: cell.y, span: cell.w, rows: cell.h } : {};
375
375
  if (ctx.boardGroups.has(memberId)) {
376
- entries.push(Object.assign(Object.assign(Object.assign({ id: memberId }, (spec !== null && spec !== void 0 ? spec : {})), at), { widgets: treeOf(memberId) }));
376
+ // The container's LIVE layout and, under split, its live tree the
377
+ // authored `tree` is stale the moment a divider moves.
378
+ const _g = spec !== null && spec !== void 0 ? spec : {}, { tree: _authored } = _g, rest = __rest(_g, ["tree"]);
379
+ void _authored;
380
+ const clayout = (_c = (_b = ctx.layoutOf.get(memberId)) !== null && _b !== void 0 ? _b : spec === null || spec === void 0 ? void 0 : spec.layout) !== null && _c !== void 0 ? _c : 'grid';
381
+ const cb = binders.get(memberId);
382
+ const ctree = clayout === 'split' && (cb === null || cb === void 0 ? void 0 : cb.getSplitTree) ? cb.getSplitTree() : undefined;
383
+ entries.push(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ id: memberId }, rest), at), { layout: clayout }), (ctree !== undefined ? { tree: ctree } : {})), { widgets: treeOf(memberId) }));
377
384
  }
378
385
  else if (spec) {
379
386
  // `pinned` is read from the NODE's lock, not the authored spec: pin()
@@ -381,7 +388,7 @@ export function createDashboardHandle(ctx) {
381
388
  // the user left it (D5). Written only when true, so an unpinned
382
389
  // widget serialises exactly as it always did.
383
390
  const entry = Object.assign(Object.assign({}, spec), at);
384
- if (((_d = (_c = (_b = ctx.apiRef) === null || _b === void 0 ? void 0 : _b.getModel().getNode(memberId)) === null || _c === void 0 ? void 0 : _c.state) === null || _d === void 0 ? void 0 : _d.locked) === true)
391
+ if (((_f = (_e = (_d = ctx.apiRef) === null || _d === void 0 ? void 0 : _d.getModel().getNode(memberId)) === null || _e === void 0 ? void 0 : _e.state) === null || _f === void 0 ? void 0 : _f.locked) === true)
385
392
  entry.pinned = true;
386
393
  else
387
394
  delete entry.pinned;
@@ -546,7 +553,8 @@ export function createDashboardHandle(ctx) {
546
553
  setLayout(layout, viewId) {
547
554
  var _a, _b, _c;
548
555
  const vid = viewId !== null && viewId !== void 0 ? viewId : ctx.active;
549
- if (!views.some((v) => v.id === vid))
556
+ // A view, or a CONTAINER (item 7): the same switch one level down.
557
+ if (!views.some((v) => v.id === vid) && !ctx.boardGroups.has(vid))
550
558
  return;
551
559
  if (((_a = ctx.layoutOf.get(vid)) !== null && _a !== void 0 ? _a : 'grid') === layout)
552
560
  return;
@@ -1063,6 +1071,10 @@ export function dashboard(options) {
1063
1071
  ctx.rebindView = (viewId, next) => {
1064
1072
  var _a, _b, _c;
1065
1073
  const v = views.find((x) => x.id === viewId);
1074
+ if (!v) {
1075
+ rebindContainerLayout(viewId, next);
1076
+ return;
1077
+ }
1066
1078
  const g = groups.get(viewId);
1067
1079
  const b = binders.get(viewId);
1068
1080
  if (!v || !g || !b)
@@ -1109,6 +1121,56 @@ export function dashboard(options) {
1109
1121
  ctx.boardGroups.set(id, g);
1110
1122
  bindContainer(g, w, (_a = ctx.viewOfBoard.get(id)) !== null && _a !== void 0 ? _a : ctx.active);
1111
1123
  };
1124
+ /**
1125
+ * LIVE LAYOUT SWITCH ON A CONTAINER (item 7) — the view's contract one
1126
+ * level down: cells persisted where the grid reads them, the tree and
1127
+ * column cache cleared, the container's `layout` flipped on its spec and
1128
+ * its metadata, a fresh binder on the same group, selection carried.
1129
+ */
1130
+ function rebindContainerLayout(id, next) {
1131
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1132
+ const cg = ctx.boardGroups.get(id);
1133
+ const w = specById.get(id);
1134
+ const b = binders.get(id);
1135
+ if (!cg || !w || !w.widgets || !b)
1136
+ return;
1137
+ const cells = b.saveLayout().cells;
1138
+ const focused = b.getFocusedWidget();
1139
+ const selected = b.getSelectedWidget();
1140
+ b.dispose();
1141
+ const write = (fn) => (model.runSystemWrite ? model.runSystemWrite(fn) : fn());
1142
+ write(() => {
1143
+ var _a, _b, _c;
1144
+ for (const [cid, cell] of cells) {
1145
+ const n = model.getNode(cid);
1146
+ if (n)
1147
+ n.setMetadata('gridItem', gridItemFromCell(cell));
1148
+ else
1149
+ (_a = model.getGroup(cid)) === null || _a === void 0 ? void 0 : _a.setMetadata('gridItem', gridItemFromCell(cell));
1150
+ }
1151
+ cg.setMetadata(SPLIT_TREE_KEY, undefined);
1152
+ cg.setMetadata('dashboardLayouts', undefined);
1153
+ const board = (_b = cg.getMetadata('dashboardBoard')) !== null && _b !== void 0 ? _b : {};
1154
+ cg.setMetadata('dashboardBoard', Object.assign(Object.assign({}, board), { layout: next }));
1155
+ const cw = (_c = cg.getMetadata('containerWidget')) !== null && _c !== void 0 ? _c : {};
1156
+ cg.setMetadata('containerWidget', Object.assign(Object.assign({}, cw), { layout: next }));
1157
+ });
1158
+ w.layout = next;
1159
+ delete w.tree;
1160
+ ctx.layoutOf.set(id, next);
1161
+ // The inner bound is the slab's LIVE row count, not the authored design:
1162
+ // a child escalation grew to two rows must keep them through a
1163
+ // split → grid round trip (the visual gate caught it collapsing to one
1164
+ // inside a two-row slab).
1165
+ const slabRows = (_c = (_b = binders.get((_a = ctx.viewOfWidget.get(id)) !== null && _a !== void 0 ? _a : '')) === null || _b === void 0 ? void 0 : _b.cellOf(id)) === null || _c === void 0 ? void 0 : _c.h;
1166
+ const authored = (_d = w.maxRows) !== null && _d !== void 0 ? _d : rowExtentOf((_e = w.widgets) !== null && _e !== void 0 ? _e : []);
1167
+ bindContainer(cg, w, (_f = ctx.viewOfBoard.get(id)) !== null && _f !== void 0 ? _f : ctx.active, Math.max(authored, slabRows !== null && slabRows !== void 0 ? slabRows : 0));
1168
+ (_g = binders.get(id)) === null || _g === void 0 ? void 0 : _g.sync();
1169
+ if (focused)
1170
+ (_h = binders.get(id)) === null || _h === void 0 ? void 0 : _h.focusWidget(focused);
1171
+ else if (selected)
1172
+ (_j = binders.get(id)) === null || _j === void 0 ? void 0 : _j.selectWidget(selected);
1173
+ }
1112
1174
  (_m = ctx.attachHistory) === null || _m === void 0 ? void 0 : _m.call(ctx);
1113
1175
  return;
1114
1176
  /**
@@ -1121,7 +1183,7 @@ export function dashboard(options) {
1121
1183
  * hit-testing and the height-escalation ratchet all apply unchanged.
1122
1184
  */
1123
1185
  function mountBoard(boardId, viewId, widgets, boardGroup) {
1124
- var _a, _b, _c, _d, _e;
1186
+ var _a, _b, _c, _d, _e, _f, _g;
1125
1187
  for (const w of widgets) {
1126
1188
  if (w.widgets) {
1127
1189
  const innerColumns = innerColumnsOf(w);
@@ -1133,7 +1195,11 @@ export function dashboard(options) {
1133
1195
  cg.setMetadata('gridItem', gridItemFromCell({ x: w.x, y: w.y, w: w.span, h: w.rows }));
1134
1196
  // The container's own spec fields, persisted ON the group — a
1135
1197
  // reloaded document has no authored literal to read them from.
1136
- cg.setMetadata('containerWidget', Object.assign(Object.assign(Object.assign(Object.assign({}, (w.kind !== undefined ? { kind: w.kind } : {})), (w.title !== undefined ? { title: w.title } : {})), { columns: innerColumns, maxRows: innerRows }), (w.data !== undefined ? { data: w.data } : {})));
1198
+ cg.setMetadata('containerWidget', Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (w.kind !== undefined ? { kind: w.kind } : {})), (w.title !== undefined ? { title: w.title } : {})), { columns: innerColumns, maxRows: innerRows }), (w.data !== undefined ? { data: w.data } : {})), (w.layout !== undefined ? { layout: w.layout } : {})), (w.sizing !== undefined ? { sizing: w.sizing } : {})));
1199
+ // Item 7: the container's own layout and bound, persisted like a view's.
1200
+ ctx.layoutOf.set(w.id, (_c = w.layout) !== null && _c !== void 0 ? _c : 'grid');
1201
+ if (w.layout === 'split' && w.tree !== undefined)
1202
+ cg.setMetadata(SPLIT_TREE_KEY, w.tree);
1137
1203
  cg.setMetadata('dashboardBoard', {
1138
1204
  columns: innerColumns,
1139
1205
  gap,
@@ -1146,7 +1212,9 @@ export function dashboard(options) {
1146
1212
  designHeight: 0,
1147
1213
  maxRows: innerRows,
1148
1214
  float: false,
1149
- rtl: (_c = options.rtl) !== null && _c !== void 0 ? _c : false,
1215
+ rtl: (_d = options.rtl) !== null && _d !== void 0 ? _d : false,
1216
+ layout: (_e = w.layout) !== null && _e !== void 0 ? _e : 'grid',
1217
+ escalate: w.sizing !== 'fit',
1150
1218
  });
1151
1219
  cg.size = { width: 100, height: rowHeight, depth: 0 };
1152
1220
  boardGroup.addMember(w.id);
@@ -1166,7 +1234,7 @@ export function dashboard(options) {
1166
1234
  // and made toJSON() → dashboard() NOT round-trip (a saved layout
1167
1235
  // rebuilt back into its declaration order rather than its cells).
1168
1236
  if (w.x !== undefined && w.y !== undefined) {
1169
- n.setGridItem(gridItemFromCell({ x: w.x, y: w.y, w: (_d = w.span) !== null && _d !== void 0 ? _d : 3, h: (_e = w.rows) !== null && _e !== void 0 ? _e : 1 }));
1237
+ n.setGridItem(gridItemFromCell({ x: w.x, y: w.y, w: (_f = w.span) !== null && _f !== void 0 ? _f : 3, h: (_g = w.rows) !== null && _g !== void 0 ? _g : 1 }));
1170
1238
  }
1171
1239
  if (w.pinned)
1172
1240
  n.setState({ locked: true });
@@ -1204,14 +1272,25 @@ export function dashboard(options) {
1204
1272
  return bindDashboardGrid(a, g, Object.assign(Object.assign(Object.assign({}, common), { columns: (_j = v.columns) !== null && _j !== void 0 ? _j : columns, sizing, baseRowHeight: rowHeight, designHeight: viewH(v), float: (_k = options.float) !== null && _k !== void 0 ? _k : false, overflow }), (options.responsive ? { responsive: options.responsive } : {})));
1205
1273
  }
1206
1274
  /** Bind (or re-bind) a container's inner grid on its group. */
1207
- function bindContainer(cg, w, viewId) {
1208
- var _a, _b, _c, _d, _e;
1209
- binders.set(w.id, bindDashboardGrid(a, cg, Object.assign(Object.assign({ columns: innerColumnsOf(w), gap, padding: 0, sizing: 'fit', baseRowHeight: rowHeight, designHeight: 0, maxRows: (_a = w.maxRows) !== null && _a !== void 0 ? _a : rowExtentOf((_b = w.widgets) !== null && _b !== void 0 ? _b : []), float: false, rtl: (_c = options.rtl) !== null && _c !== void 0 ? _c : false, static: (_d = options.static) !== null && _d !== void 0 ? _d : false, dragHandle: (_e = options.dragHandle) !== null && _e !== void 0 ? _e : false }, (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), { onGesture: (e) => {
1275
+ function bindContainer(cg, w, viewId, innerRows) {
1276
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1277
+ void viewId;
1278
+ // The LIVE switches ride on the view's binder when it exists (a
1279
+ // container bound after a setStatic/setRtl/setDragHandle must match).
1280
+ const vb = binders.get((_a = ctx.viewOfBoard.get(w.id)) !== null && _a !== void 0 ? _a : ctx.active);
1281
+ const inner = Object.assign(Object.assign({ columns: innerColumnsOf(w), gap, padding: 0, baseRowHeight: rowHeight, rtl: (_c = (_b = vb === null || vb === void 0 ? void 0 : vb.getRtl()) !== null && _b !== void 0 ? _b : options.rtl) !== null && _c !== void 0 ? _c : false, static: (_e = (_d = vb === null || vb === void 0 ? void 0 : vb.getStatic()) !== null && _d !== void 0 ? _d : options.static) !== null && _e !== void 0 ? _e : false, dragHandle: (_g = (_f = vb === null || vb === void 0 ? void 0 : vb.getDragHandle()) !== null && _f !== void 0 ? _f : options.dragHandle) !== null && _g !== void 0 ? _g : false }, (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), { onGesture: (e) => {
1210
1282
  var _a, _b;
1211
1283
  if (e.type === 'commit')
1212
1284
  reportChanged();
1213
1285
  (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
1214
- } })));
1286
+ } });
1287
+ if (((_h = ctx.layoutOf.get(w.id)) !== null && _h !== void 0 ? _h : w.layout) === 'split') {
1288
+ // A splitter tree covering the pane; the pane's frame is the parent's
1289
+ // slab, so no design height of its own.
1290
+ binders.set(w.id, bindDashboardSplit(a, cg, Object.assign(Object.assign({}, inner), (w.tree !== undefined ? { tree: w.tree } : {}))));
1291
+ return;
1292
+ }
1293
+ binders.set(w.id, bindDashboardGrid(a, cg, Object.assign(Object.assign({}, inner), { sizing: 'fit', designHeight: 0, maxRows: (_j = innerRows !== null && innerRows !== void 0 ? innerRows : w.maxRows) !== null && _j !== void 0 ? _j : rowExtentOf((_k = w.widgets) !== null && _k !== void 0 ? _k : []), float: false, escalate: w.sizing !== 'fit' })));
1215
1294
  }
1216
1295
  /**
1217
1296
  * One reporter for every binder on a view — the view's own and each
@@ -148,6 +148,13 @@ export interface DashboardGridOptions {
148
148
  * row instead), and the strip can never be squeezed.
149
149
  */
150
150
  maxRows?: number;
151
+ /**
152
+ * Nested boards only. `true` (default): a child pulled clearly past the
153
+ * bound GROWS the container's slab in the parent (height escalation).
154
+ * `false`: the pane is the bound — the pull is refused where it stands, the
155
+ * container's `sizing: 'fit'`.
156
+ */
157
+ escalate?: boolean;
151
158
  /**
152
159
  * What dragging a tile OUT of the board means (default 'cancel' — the tile
153
160
  * snaps back on release). 'remove' dims the ghost outside the board and a
@@ -406,4 +413,83 @@ export interface DashboardGridHandle {
406
413
  }, event: PointerEvent): void;
407
414
  dispose(): void;
408
415
  }
416
+ /**
417
+ * CROSS-CONTAINER HANDOFF. Binders on the same canvas register here; a move
418
+ * gesture whose pointer enters ANOTHER registered board (deepest wins — the
419
+ * nested KPI section beats the tab that contains it) hands the tile off: the
420
+ * source engine drops it (survivors settle home), the target engine ADOPTS it
421
+ * (gateless first placement, then the normal live-push loop), and release
422
+ * commits ONE batch across both boards — displaced tiles on each side,
423
+ * RemoveFromGroup + AddToGroup, the tile's new cells and geometry. This is
424
+ * what makes "drag Total Revenue under Top reps" MOVE the KPI to the main
425
+ * board rather than snapping it home (live review: parking was a guard, not
426
+ * the feature).
427
+ */
428
+ interface BinderPeer {
429
+ group: GroupModel;
430
+ /** True when this board's engine holds `id` as an item (member lookup). */
431
+ hasItem(id: string): boolean;
432
+ /** The member's current cell in this board (undefined when absent). */
433
+ memberCell(id: string): CellRect | undefined;
434
+ /**
435
+ * Grow/shrink a member's row span by `dRows` — the parent half of nested
436
+ * HEIGHT ESCALATION: pulling a KPI taller than its one-row strip grows the
437
+ * STRIP's slab in the board that contains it (live report: "i cant
438
+ * increase height"). Returns the cell+frame before/after when accepted.
439
+ */
440
+ resizeMemberBy(id: string, dRows: number): {
441
+ changed: boolean;
442
+ cellBefore?: CellRect;
443
+ cellAfter?: CellRect;
444
+ frameBefore?: WorldRect;
445
+ frameAfter?: WorldRect;
446
+ };
447
+ containsWorld(x: number, y: number): boolean;
448
+ /**
449
+ * Containment plus ONE extra row of grace below the frame — gridstack's
450
+ * `_extraDragRow`: dropping "under the last row" appends a row rather than
451
+ * counting as off-board. Consulted only when NO strict frame matched, so a
452
+ * nested strip's band can never steal a point that strictly belongs to the
453
+ * board below it.
454
+ */
455
+ containsWorldExtended(x: number, y: number): boolean;
456
+ frameArea(): number;
457
+ adopt(node: NodeModel, world: {
458
+ x: number;
459
+ y: number;
460
+ }, pxSize: {
461
+ width: number;
462
+ height: number;
463
+ }): AdoptedLeg | null;
464
+ }
465
+ interface AdoptedLeg {
466
+ groupId: string;
467
+ /** Drive the target engine from the source binder's pointer stream. */
468
+ move(world: {
469
+ x: number;
470
+ y: number;
471
+ }): void;
472
+ /** Undo the adoption: target board back to its pre-entry layout. */
473
+ abort(): void;
474
+ /**
475
+ * Close the leg for commit: returns the target-side displaced commands, the
476
+ * tile's final cell and its projected rect. Null when the tile is somehow
477
+ * gone (treat as abort).
478
+ */
479
+ finalize(): {
480
+ commands: Command[];
481
+ cell: CellRect;
482
+ rect: WorldRect;
483
+ } | null;
484
+ }
485
+ /**
486
+ * Register a board that is NOT a grid (the split binder on a container) as a
487
+ * peer on its canvas, so the parent grid's hitTest defers a press on one of
488
+ * its tiles to it ("a press on a tile that belongs to a NESTED board must
489
+ * reach that board's tool") whatever the registration order. Returns the
490
+ * unregister. A split board adopts nothing and grows no slab: its `adopt`
491
+ * answers null and `resizeMemberBy` answers unchanged.
492
+ */
493
+ export declare function registerBoardPeer(container: HTMLElement, peer: BinderPeer): () => void;
494
+ export type { BinderPeer };
409
495
  export declare function bindDashboardGrid(api: DashboardGridApi, group: GroupModel, options?: DashboardGridOptions): DashboardGridHandle;
@@ -145,6 +145,26 @@ export const CAPTION_BAND = 28;
145
145
  * page (review D12).
146
146
  */
147
147
  const BOARD_REGISTRY = new WeakMap();
148
+ /**
149
+ * Register a board that is NOT a grid (the split binder on a container) as a
150
+ * peer on its canvas, so the parent grid's hitTest defers a press on one of
151
+ * its tiles to it ("a press on a tile that belongs to a NESTED board must
152
+ * reach that board's tool") whatever the registration order. Returns the
153
+ * unregister. A split board adopts nothing and grows no slab: its `adopt`
154
+ * answers null and `resizeMemberBy` answers unchanged.
155
+ */
156
+ export function registerBoardPeer(container, peer) {
157
+ let set = BOARD_REGISTRY.get(container);
158
+ if (!set) {
159
+ set = new Set();
160
+ BOARD_REGISTRY.set(container, set);
161
+ }
162
+ set.add(peer);
163
+ const s = set;
164
+ return () => {
165
+ s.delete(peer);
166
+ };
167
+ }
148
168
  /**
149
169
  * ONE aria-live region per canvas, shared by every board on it — the
150
170
  * renderer's own controller (coalescing, de-duplicating), so a dashboard
@@ -262,6 +282,8 @@ export function bindDashboardGrid(api, group, options = {}) {
262
282
  let float = (_f = options.float) !== null && _f !== void 0 ? _f : false;
263
283
  /** The AUTHORED bound — a nested strip's design (row-first push, escalation). */
264
284
  const maxRows = options.maxRows;
285
+ /** May a pull past the bound grow the slab in the parent? `false` = the pane is the bound. */
286
+ const escalate = options.escalate !== false;
265
287
  const dragOut = (_g = options.dragOut) !== null && _g !== void 0 ? _g : 'cancel';
266
288
  const wantHandles = options.resizeHandles !== false;
267
289
  const fluid = options.fluid === true;
@@ -594,15 +616,35 @@ export function bindDashboardGrid(api, group, options = {}) {
594
616
  clearTimeout(glideTimer);
595
617
  glideTimer = setTimeout(() => { var _a; return (_a = htmlLayer()) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-glide'); }, GLIDE_OFF_DELAY);
596
618
  };
619
+ /** The host whose ghost class the pending timer will lift. */
620
+ let ghostHost = null;
621
+ /**
622
+ * Lift a pending ghost NOW. One timer serves every host, so superseding it
623
+ * (a new gesture within 60 ms of the last drop — ③ then ④ in the
624
+ * nested-containers checks — or a dispose on a rebind) used to clear the
625
+ * timer and leave the previous tile lifted for good: a permanent drop
626
+ * shadow the visual gate finally caught.
627
+ */
628
+ const flushGhost = () => {
629
+ if (ghostTimer)
630
+ clearTimeout(ghostTimer);
631
+ ghostTimer = null;
632
+ ghostHost === null || ghostHost === void 0 ? void 0 : ghostHost.classList.remove('axdb-ghost', 'axdb-out');
633
+ ghostHost = null;
634
+ };
597
635
  const setGhost = (id, on) => {
598
636
  const host = hostOf(id);
599
637
  if (!host)
600
638
  return;
639
+ if (ghostHost && ghostHost !== host)
640
+ flushGhost();
601
641
  if (on) {
602
642
  if (ghostTimer)
603
643
  clearTimeout(ghostTimer);
644
+ ghostTimer = null;
604
645
  host.classList.add('axdb-ghost');
605
646
  host.classList.remove('axdb-out');
647
+ ghostHost = host;
606
648
  }
607
649
  else {
608
650
  host.classList.remove('axdb-out');
@@ -610,7 +652,13 @@ export function bindDashboardGrid(api, group, options = {}) {
610
652
  // placeholder is INSTANT (gridstack-style), then let glides resume.
611
653
  if (ghostTimer)
612
654
  clearTimeout(ghostTimer);
613
- ghostTimer = setTimeout(() => host.classList.remove('axdb-ghost'), 60);
655
+ ghostHost = host;
656
+ ghostTimer = setTimeout(() => {
657
+ host.classList.remove('axdb-ghost');
658
+ ghostTimer = null;
659
+ if (ghostHost === host)
660
+ ghostHost = null;
661
+ }, 60);
614
662
  }
615
663
  };
616
664
  // -- resize handles ---------------------------------------------------------
@@ -1327,7 +1375,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1327
1375
  // the strip's CURRENT slab rows, whatever gesture created them; the
1328
1376
  // ledger just accumulates this gesture's net change for the one-batch
1329
1377
  // commit and for Escape.
1330
- if (maxRows !== undefined && g.kind === 'resize') {
1378
+ if (maxRows !== undefined && escalate && g.kind === 'resize') {
1331
1379
  const parent = parentPeer();
1332
1380
  if (parent) {
1333
1381
  const visual = boardVisualHeight();
@@ -2445,8 +2493,10 @@ export function bindDashboardGrid(api, group, options = {}) {
2445
2493
  placeholder = null;
2446
2494
  if (glideTimer)
2447
2495
  clearTimeout(glideTimer);
2448
- if (ghostTimer)
2449
- clearTimeout(ghostTimer);
2496
+ // A rebind inside the 60 ms window (a layout switch right after an undo)
2497
+ // disposed this binder with the timer pending — the host kept its
2498
+ // lifted ghost for good (visual gate, nested-containers ⑤).
2499
+ flushGhost();
2450
2500
  (_d = htmlLayer()) === null || _d === void 0 ? void 0 : _d.classList.remove('axdb-glide');
2451
2501
  api.container.style.cursor = '';
2452
2502
  },
@@ -25,7 +25,7 @@
25
25
  import { __awaiter } from "tslib";
26
26
  import { Command } from '@grafloria/engine';
27
27
  import { LiveRegionController, registerTool } from '@grafloria/renderer';
28
- import { dragHandleSelector, gripHostOf, gripOf, normalizeDragHandle, ownsPress, pressOnDragHandle, syncGrip, DRAG_HANDLE_CLASS } from './grid-binder.js';
28
+ import { dragHandleSelector, gripHostOf, gripOf, normalizeDragHandle, ownsPress, pressOnDragHandle, registerBoardPeer, syncGrip, DRAG_HANDLE_CLASS } from './grid-binder.js';
29
29
  import { cellFromGridItem } from './grid-mapping.js';
30
30
  import { addSplitLeaf, cellsFromSplit, cloneSplit, dividersOf, groupRectsOf, insertSplitLeaf, moveSplitDivider, normalizeSplit, pathToLeaf, projectSplit, removeSplitLeaf, splitFromCells, splitLeaves, } from './split-layout.js';
31
31
  import { ensureDashboardKitStyles } from './styles.js';
@@ -1166,6 +1166,26 @@ export function bindDashboardSplit(api, group, options = {}) {
1166
1166
  applyFluidFrame();
1167
1167
  project(reconcile());
1168
1168
  api.renderNow();
1169
+ // A split board on a CONTAINER (item 7) sits inside a parent grid: register
1170
+ // as a peer so the parent's hitTest hands presses on our tiles to us.
1171
+ const unregisterPeer = registerBoardPeer(api.container, {
1172
+ group,
1173
+ hasItem: (id) => { var _a; return ((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id); },
1174
+ memberCell: (id) => handle.cellOf(id),
1175
+ resizeMemberBy: () => ({ changed: false }),
1176
+ containsWorld: (x, y) => worldInsideBoard(x, y),
1177
+ containsWorldExtended: (x, y) => worldInsideBoard(x, y),
1178
+ frameArea: () => {
1179
+ const f = frame();
1180
+ return f.width * f.height;
1181
+ },
1182
+ adopt: () => null,
1183
+ });
1184
+ const disposeHandle = handle.dispose.bind(handle);
1185
+ handle.dispose = () => {
1186
+ unregisterPeer();
1187
+ disposeHandle();
1188
+ };
1169
1189
  return handle;
1170
1190
  }
1171
1191
  //# sourceMappingURL=split-binder.js.map
package/src/lib/load.js CHANGED
@@ -206,7 +206,7 @@ export function fromDocument(document, options = {}) {
206
206
  // a fixed world — it stays one. Fluid is only what was saved fluid.
207
207
  mode: (firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.fluid) === true ? 'fluid' : 'fixed',
208
208
  overflow: (_p = firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.overflow) !== null && _p !== void 0 ? _p : 'bounded',
209
- layoutOf: new Map(viewGroups.map((g) => { var _a, _b; return [g.id, ((_b = (_a = g.getMetadata('dashboardBoard')) === null || _a === void 0 ? void 0 : _a.layout) !== null && _b !== void 0 ? _b : 'grid')]; })),
209
+ layoutOf: new Map(dashGroups.map((g) => { var _a, _b; return [g.id, ((_b = (_a = g.getMetadata('dashboardBoard')) === null || _a === void 0 ? void 0 : _a.layout) !== null && _b !== void 0 ? _b : 'grid')]; })),
210
210
  // responsive is NOT in the document (a runtime seam), so it is deliberately
211
211
  // absent from the round-trip; width/height/columns/gap/sizing/float/rtl are.
212
212
  optionsBase: firstBoard