@grafloria/element 0.4.1 → 0.4.2

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.1",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -64,6 +64,30 @@ export interface DashboardWidgetSpec {
64
64
  data?: Record<string, unknown>;
65
65
  /** Optional title used by the built-in fallback renderer. */
66
66
  title?: string;
67
+ /**
68
+ * CONTAINMENT. A widget carrying `widgets` is a CONTAINER: it mounts as a
69
+ * member group (a locked slab in its parent's grid, exactly like a view's
70
+ * board one level down) with its own nested pack grid bound on it. Children
71
+ * lay out inside its frame; dragging a tile across the boundary adopts it
72
+ * live in either direction, and one undo restores the whole gesture.
73
+ * Containers may nest — tested to TWO levels; deeper is not exercised by
74
+ * the gates and rides at your own risk. A container renders no card of its
75
+ * own (`kind`/`data` are carried for your bookkeeping and serialization,
76
+ * not painted).
77
+ */
78
+ widgets?: DashboardWidgetSpec[];
79
+ /**
80
+ * Container only: column count of the INNER grid (default: the parent
81
+ * board's column count).
82
+ */
83
+ columns?: number;
84
+ /**
85
+ * Container only: the inner grid's designed row count. A child resized past
86
+ * it ESCALATES — the container's slab grows a row in the parent board (the
87
+ * ratchet), instead of the child overflowing the frame. Default: the row
88
+ * extent of the declared children.
89
+ */
90
+ maxRows?: number;
67
91
  }
68
92
  /** One board. Multiple views are the tab pattern: only one is on-camera. */
69
93
  export interface DashboardViewSpec {
@@ -316,7 +340,17 @@ export interface DashboardHandleContext {
316
340
  groups: Map<string, GroupModel>;
317
341
  binders: Map<string, DashboardGridHandle>;
318
342
  specById: Map<string, DashboardWidgetSpec>;
343
+ /** Widget id → the BOARD that owns it (a view id, or a container id). */
319
344
  viewOfWidget: Map<string, string>;
345
+ /** Every board group — the views PLUS every container. Views also live in
346
+ * `groups` (the parking map showView drives); containers deliberately do
347
+ * NOT — parking flings a group to OFFSCREEN_X, and a container must follow
348
+ * its parent, not travel on its own. */
349
+ boardGroups: Map<string, GroupModel>;
350
+ /** Board id → its authored widgets array (views and containers alike). */
351
+ boardWidgets: Map<string, DashboardWidgetSpec[]>;
352
+ /** Board id → the VIEW it belongs to (identity for views). */
353
+ viewOfBoard: Map<string, string>;
320
354
  hosts: Map<string, HTMLElement>;
321
355
  renderWidget: (widget: DashboardWidgetSpec, host: HTMLElement) => void;
322
356
  columns: number;
@@ -129,6 +129,32 @@ function buildWidgetNode(w, rowHeight) {
129
129
  node.removePort(p.id);
130
130
  return node;
131
131
  }
132
+ function cloneWidgets(ws) {
133
+ return ws.map((w) => (Object.assign(Object.assign({}, w), (w.widgets ? { widgets: cloneWidgets(w.widgets) } : {}))));
134
+ }
135
+ /** A container's inner column count: authored, else its own span — inner cells
136
+ * then ride the parent's column rhythm, which is what a section reads as. */
137
+ function innerColumnsOf(w) {
138
+ var _a, _b;
139
+ return Math.max(1, (_b = (_a = w.columns) !== null && _a !== void 0 ? _a : w.span) !== null && _b !== void 0 ? _b : 3);
140
+ }
141
+ /** The row extent of a laid-out widget list (the inner grid's design height). */
142
+ function rowExtentOf(widgets) {
143
+ var _a, _b;
144
+ let max = 1;
145
+ for (const w of widgets)
146
+ max = Math.max(max, ((_a = w.y) !== null && _a !== void 0 ? _a : 0) + ((_b = w.rows) !== null && _b !== void 0 ? _b : 1));
147
+ return max;
148
+ }
149
+ /** Recursive `assignCells`: a container flows in its parent like any widget,
150
+ * and its children flow inside its OWN column count. */
151
+ function assignCellsDeep(widgets, columns) {
152
+ assignCells(widgets, columns);
153
+ for (const w of widgets) {
154
+ if (w.widgets)
155
+ assignCellsDeep(w.widgets, innerColumnsOf(w));
156
+ }
157
+ }
132
158
  /** Flow widgets that declared no cell: left-to-right, wrapping at `columns`. */
133
159
  function assignCells(widgets, columns) {
134
160
  var _a, _b;
@@ -161,6 +187,38 @@ function assignCells(widgets, columns) {
161
187
  export function createDashboardHandle(ctx) {
162
188
  const { views, groups, binders, specById, viewOfWidget } = ctx;
163
189
  const hostOf = (id) => ctx.hosts.get(id);
190
+ /**
191
+ * A board's widgets as a NESTED tree, derived from LIVE membership — not the
192
+ * authored arrays. A cross-boundary drag moves membership through commands
193
+ * (and undo moves it back); the authored arrays do not follow. Deriving from
194
+ * the groups + engines is what makes toJSON() and onLayoutChange report a
195
+ * tile under the container it is actually in, in every one of those states.
196
+ */
197
+ const treeOf = (boardId) => {
198
+ var _a;
199
+ const g = ctx.boardGroups.get(boardId);
200
+ const b = binders.get(boardId);
201
+ if (!g)
202
+ return [];
203
+ // Cells from the binder's serialisation (largest cached layout — the
204
+ // saving-on-a-phone rule), falling back to the live engine cell.
205
+ const saved = b === null || b === void 0 ? void 0 : b.saveLayout();
206
+ const cellOf = (id) => { var _a; return (_a = saved === null || saved === void 0 ? void 0 : saved.cells.get(id)) !== null && _a !== void 0 ? _a : b === null || b === void 0 ? void 0 : b.cellOf(id); };
207
+ const entries = [];
208
+ for (const memberId of (_a = g.members) !== null && _a !== void 0 ? _a : []) {
209
+ const spec = specById.get(memberId);
210
+ const cell = cellOf(memberId);
211
+ const at = cell ? { x: cell.x, y: cell.y, span: cell.w, rows: cell.h } : {};
212
+ if (ctx.boardGroups.has(memberId)) {
213
+ entries.push(Object.assign(Object.assign(Object.assign({ id: memberId }, (spec !== null && spec !== void 0 ? spec : {})), at), { widgets: treeOf(memberId) }));
214
+ }
215
+ else if (spec) {
216
+ entries.push(Object.assign(Object.assign({}, spec), at));
217
+ }
218
+ }
219
+ entries.sort((p1, p2) => { var _a, _b, _c, _d; return ((_a = p1.y) !== null && _a !== void 0 ? _a : 0) - ((_b = p2.y) !== null && _b !== void 0 ? _b : 0) || ((_c = p1.x) !== null && _c !== void 0 ? _c : 0) - ((_d = p2.x) !== null && _d !== void 0 ? _d : 0); });
220
+ return entries;
221
+ };
164
222
  const execCommand = (cmd) => {
165
223
  var _a, _b, _c, _d;
166
224
  try {
@@ -237,16 +295,18 @@ export function createDashboardHandle(ctx) {
237
295
  addWidget(spec, viewId) {
238
296
  var _a, _b, _c, _d, _e;
239
297
  const vid = viewId !== null && viewId !== void 0 ? viewId : ctx.active;
240
- const v = views.find((x) => x.id === vid);
298
+ // `vid` may name a view OR a container — both are boards with a group,
299
+ // a binder and an authored array.
300
+ const arr = ctx.boardWidgets.get(vid);
241
301
  const model = (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.getModel();
242
- const group = groups.get(vid);
243
- if (!v || !model || !group)
302
+ const group = ctx.boardGroups.get(vid);
303
+ if (!arr || !model || !group)
244
304
  return undefined;
245
305
  const w = Object.assign(Object.assign({}, spec), { id: spec.id || `w-${++autoId}`, span: (_b = spec.span) !== null && _b !== void 0 ? _b : 3, rows: (_c = spec.rows) !== null && _c !== void 0 ? _c : 1 });
246
306
  // REGISTER FIRST: a custom node mounts exactly once, and the painter
247
307
  // returns early for an id the spec does not know — so the widget must be
248
308
  // known before the node reaches the model, or it paints blank forever.
249
- v.widgets.push(w);
309
+ arr.push(w);
250
310
  specById.set(w.id, w);
251
311
  viewOfWidget.set(w.id, vid);
252
312
  const existing = model.getNode(w.id);
@@ -292,9 +352,16 @@ export function createDashboardHandle(ctx) {
292
352
  ids.add(id);
293
353
  // Read the SPEC, not the group's member Set: membership is maintained by
294
354
  // commands and an in-flight drag can have a widget momentarily reparented.
295
- // The spec is what the view IS.
296
- for (const w of (_b = (_a = views.find((v) => v.id === id)) === null || _a === void 0 ? void 0 : _a.widgets) !== null && _b !== void 0 ? _b : [])
297
- ids.add(w.id);
355
+ // The spec is what the view IS. Containers add themselves AND their
356
+ // subtree an exported container without its children is an empty frame.
357
+ const walk = (ws) => {
358
+ for (const w of ws) {
359
+ ids.add(w.id);
360
+ if (w.widgets)
361
+ walk(w.widgets);
362
+ }
363
+ };
364
+ walk((_b = (_a = views.find((v) => v.id === id)) === null || _a === void 0 ? void 0 : _a.widgets) !== null && _b !== void 0 ? _b : []);
298
365
  return ids;
299
366
  },
300
367
  toJSON() {
@@ -304,13 +371,13 @@ export function createDashboardHandle(ctx) {
304
371
  // layout its user authored — and the view's `columns` is that count, so
305
372
  // feeding this straight back into dashboard() rebuilds the wide board.
306
373
  const savedViews = views.map((v) => {
307
- var _a;
374
+ var _a, _b, _c, _d;
308
375
  const saved = (_a = binders.get(v.id)) === null || _a === void 0 ? void 0 : _a.saveLayout();
309
- return Object.assign(Object.assign(Object.assign({}, v), (saved ? { columns: saved.columns } : {})), { widgets: v.widgets.map((w) => {
310
- var _a, _b;
311
- const cell = (_a = saved === null || saved === void 0 ? void 0 : saved.cells.get(w.id)) !== null && _a !== void 0 ? _a : (_b = binders.get(v.id)) === null || _b === void 0 ? void 0 : _b.cellOf(w.id);
312
- return cell ? Object.assign(Object.assign({}, w), { x: cell.x, y: cell.y, span: cell.w, rows: cell.h }) : Object.assign({}, w);
313
- }) });
376
+ const live = treeOf(v.id);
377
+ return Object.assign(Object.assign(Object.assign({}, v), (saved ? { columns: saved.columns } : {})), {
378
+ // Live membership when the view is mounted; the authored tree before
379
+ // finalize (a spec serialised without ever rendering keeps its shape).
380
+ widgets: live.length > 0 || ((_d = (_c = (_b = ctx.boardGroups.get(v.id)) === null || _b === void 0 ? void 0 : _b.members) === null || _c === void 0 ? void 0 : _c.size) !== null && _d !== void 0 ? _d : 0) > 0 ? live : v.widgets.map((w) => (Object.assign({}, w))) });
314
381
  });
315
382
  // Board options come off the LIVE board wherever the handle can see it —
316
383
  // `sizing` and `float` are the two a user changes from the toolbar, and
@@ -319,16 +386,21 @@ export function createDashboardHandle(ctx) {
319
386
  return Object.assign(Object.assign({}, ctx.optionsBase), { renderWidget: undefined, onLayoutChange: undefined, columns: ctx.columns, gap: ctx.gap, rowHeight: ctx.rowHeight, sizing: handle.getSizing(), float: handle.getFloat(), rtl: handle.getRtl(), views: savedViews });
320
387
  },
321
388
  dispose() {
322
- var _a, _b;
389
+ var _a, _b, _c;
323
390
  for (const b of binders.values())
324
391
  b.dispose();
325
392
  binders.clear();
326
393
  // The groups finalize() created are ours to clean up — leaving them
327
394
  // behind made a rebuild stack a second set of boards on the first.
328
395
  const model = (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.getModel();
329
- for (const id of groups.keys())
396
+ // Deepest first: a container group removed after its parent is an orphan
397
+ // the model never saw inside a board.
398
+ for (const id of [...ctx.boardGroups.keys()].reverse())
330
399
  (_b = model === null || model === void 0 ? void 0 : model.removeGroup) === null || _b === void 0 ? void 0 : _b.call(model, id);
400
+ for (const id of groups.keys())
401
+ (_c = model === null || model === void 0 ? void 0 : model.removeGroup) === null || _c === void 0 ? void 0 : _c.call(model, id);
331
402
  groups.clear();
403
+ ctx.boardGroups.clear();
332
404
  ctx.hosts.clear();
333
405
  },
334
406
  };
@@ -407,8 +479,15 @@ export function createDashboardHandle(ctx) {
407
479
  },
408
480
  remove(displaced) {
409
481
  var _a, _b;
482
+ if (spec.widgets) {
483
+ // A container is a GROUP, not a node — RemoveNodeCommand would
484
+ // silently no-op and strand the children. Container removal is not
485
+ // supported through the widget handle (yet); say so loudly.
486
+ console.warn('[dashboard] remove() on a container is not supported');
487
+ return;
488
+ }
410
489
  const n = node();
411
- const group = groups.get(viewId);
490
+ const group = ctx.boardGroups.get(viewId);
412
491
  const b = binder();
413
492
  if (!n || !group || !b)
414
493
  return;
@@ -420,9 +499,12 @@ export function createDashboardHandle(ctx) {
420
499
  const survivors = (_a = displaced) !== null && _a !== void 0 ? _a : b.planRemoval(id);
421
500
  const cmds = [...survivors, new RemoveFromGroupCommand(group.id, id), new RemoveNodeCommand(id)];
422
501
  void execCommand(new BatchCommand('Remove widget', cmds));
423
- const v = views.find((x) => x.id === viewId);
424
- if (v)
425
- v.widgets = v.widgets.filter((w) => w.id !== id);
502
+ const arr = ctx.boardWidgets.get(viewId);
503
+ if (arr) {
504
+ const i = arr.findIndex((w) => w.id === id);
505
+ if (i >= 0)
506
+ arr.splice(i, 1);
507
+ }
426
508
  specById.delete(id);
427
509
  viewOfWidget.delete(id);
428
510
  b.sync();
@@ -440,7 +522,7 @@ export function createDashboardHandle(ctx) {
440
522
  return handle;
441
523
  }
442
524
  export function dashboard(options) {
443
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
525
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
444
526
  ensureDashboardKitStyles();
445
527
  const columns = (_a = options.columns) !== null && _a !== void 0 ? _a : DEFAULTS.columns;
446
528
  const gap = (_b = options.gap) !== null && _b !== void 0 ? _b : DEFAULTS.gap;
@@ -448,32 +530,54 @@ export function dashboard(options) {
448
530
  const boardW = (_d = options.width) !== null && _d !== void 0 ? _d : DEFAULTS.width;
449
531
  const boardH = (_e = options.height) !== null && _e !== void 0 ? _e : DEFAULTS.height;
450
532
  const views = options.views
451
- ? options.views.map((v) => (Object.assign(Object.assign({}, v), { widgets: v.widgets.map((w) => (Object.assign({}, w))) })))
452
- : [{ id: 'main', widgets: ((_f = options.widgets) !== null && _f !== void 0 ? _f : []).map((w) => (Object.assign({}, w))) }];
533
+ ? options.views.map((v) => (Object.assign(Object.assign({}, v), { widgets: cloneWidgets(v.widgets) })))
534
+ : [{ id: 'main', widgets: cloneWidgets((_f = options.widgets) !== null && _f !== void 0 ? _f : []) }];
453
535
  for (const v of views)
454
- assignCells(v.widgets, (_g = v.columns) !== null && _g !== void 0 ? _g : columns);
536
+ assignCellsDeep(v.widgets, (_g = v.columns) !== null && _g !== void 0 ? _g : columns);
455
537
  // -- the render spec: one custom-HTML node per widget ----------------------
456
538
  const nodes = [];
457
539
  const specById = new Map();
458
540
  const viewOfWidget = new Map();
459
- for (const v of views) {
460
- for (const w of v.widgets) {
541
+ const boardWidgets = new Map();
542
+ const viewOfBoard = new Map();
543
+ // Recursive walk: a CONTAINER contributes no node (it becomes a group in
544
+ // finalize) but registers like a widget, and its children flatten into the
545
+ // render spec with the container as their board.
546
+ const flatten = (boardId, viewId, widgets) => {
547
+ for (const w of widgets) {
461
548
  specById.set(w.id, w);
462
- viewOfWidget.set(w.id, v.id);
549
+ viewOfWidget.set(w.id, boardId);
550
+ if (w.widgets) {
551
+ boardWidgets.set(w.id, w.widgets);
552
+ viewOfBoard.set(w.id, viewId);
553
+ flatten(w.id, viewId, w.widgets);
554
+ continue;
555
+ }
556
+ pushWidgetNode(w);
557
+ }
558
+ };
559
+ const pushWidgetNode = (w) => {
560
+ var _a, _b;
561
+ {
463
562
  nodes.push({
464
563
  id: w.id,
465
564
  type: 'widget',
466
565
  position: { x: 0, y: 0 },
467
566
  size: { width: 100, height: rowHeight },
468
567
  custom: true,
469
- metadata: Object.assign(Object.assign({ useHTMLLayer: true, widgetKind: (_h = w.kind) !== null && _h !== void 0 ? _h : 'widget', widgetSpec: (_j = w.data) !== null && _j !== void 0 ? _j : {} }, (w.title !== undefined ? { widgetTitle: w.title } : {})), { columnSpan: w.span, rowSpan: w.rows, gridItem: { columnStart: w.x + 1, columnEnd: w.x + 1 + w.span, rowStart: w.y + 1, rowEnd: w.y + 1 + w.rows } }),
568
+ metadata: Object.assign(Object.assign({ useHTMLLayer: true, widgetKind: (_a = w.kind) !== null && _a !== void 0 ? _a : 'widget', widgetSpec: (_b = w.data) !== null && _b !== void 0 ? _b : {} }, (w.title !== undefined ? { widgetTitle: w.title } : {})), { columnSpan: w.span, rowSpan: w.rows, gridItem: { columnStart: w.x + 1, columnEnd: w.x + 1 + w.span, rowStart: w.y + 1, rowEnd: w.y + 1 + w.rows } }),
470
569
  });
471
570
  }
571
+ };
572
+ for (const v of views) {
573
+ boardWidgets.set(v.id, v.widgets);
574
+ viewOfBoard.set(v.id, v.id);
575
+ flatten(v.id, v.id, v.widgets);
472
576
  }
473
577
  // No renderWidget → the built-in renderers draw the declared `kind` from the
474
578
  // developer's own `data` (widgets.ts), unknown kinds landing on the titled
475
579
  // frame they always did.
476
- const renderWidget = (_k = options.renderWidget) !== null && _k !== void 0 ? _k : defaultWidgetRenderer;
580
+ const renderWidget = (_h = options.renderWidget) !== null && _h !== void 0 ? _h : defaultWidgetRenderer;
477
581
  // -- runtime: ONE shared handle over a boxed context -----------------------
478
582
  // The two MUTABLE cells the handle used to close over as free `let`s —
479
583
  // `active` (showView reassigns it) and `apiRef` (finalize sets it) — are boxed
@@ -486,6 +590,9 @@ export function dashboard(options) {
486
590
  binders: new Map(),
487
591
  specById,
488
592
  viewOfWidget,
593
+ boardGroups: new Map(),
594
+ boardWidgets,
595
+ viewOfBoard,
489
596
  hosts: new Map(),
490
597
  renderWidget,
491
598
  columns,
@@ -494,7 +601,7 @@ export function dashboard(options) {
494
601
  boardW,
495
602
  boardH,
496
603
  optionsBase: options,
497
- active: (_m = (_l = views[0]) === null || _l === void 0 ? void 0 : _l.id) !== null && _m !== void 0 ? _m : 'main',
604
+ active: (_k = (_j = views[0]) === null || _j === void 0 ? void 0 : _j.id) !== null && _k !== void 0 ? _k : 'main',
498
605
  apiRef: null,
499
606
  };
500
607
  const { binders, groups } = ctx;
@@ -514,7 +621,7 @@ export function dashboard(options) {
514
621
  return handle;
515
622
  },
516
623
  finalize: (api) => {
517
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
624
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
518
625
  const a = api;
519
626
  if (!a)
520
627
  return;
@@ -545,7 +652,77 @@ export function dashboard(options) {
545
652
  g.size = { width: (_g = v.width) !== null && _g !== void 0 ? _g : boardW, height: (_h = v.height) !== null && _h !== void 0 ? _h : boardH, depth: 0 };
546
653
  g.position = { x: v.id === ctx.active ? 0 : OFFSCREEN_X, y: 0 };
547
654
  groups.set(v.id, g);
548
- for (const w of v.widgets) {
655
+ ctx.boardGroups.set(v.id, g);
656
+ mountBoard(v.id, v.id, v.widgets, g);
657
+ binders.set(v.id, bindDashboardGrid(a, g, Object.assign(Object.assign(Object.assign({ columns: (_j = v.columns) !== null && _j !== void 0 ? _j : columns, gap, padding: gap, sizing: (_k = options.sizing) !== null && _k !== void 0 ? _k : 'fit', baseRowHeight: rowHeight, designHeight: (_l = v.height) !== null && _l !== void 0 ? _l : boardH, float: (_m = options.float) !== null && _m !== void 0 ? _m : false, rtl: (_o = options.rtl) !== null && _o !== void 0 ? _o : false }, (options.responsive ? { responsive: options.responsive } : {})), ((_p = options.binder) !== null && _p !== void 0 ? _p : {})), { onGesture: (e) => {
658
+ var _a, _b;
659
+ if (e.type === 'commit')
660
+ reportLayoutChange(v.id);
661
+ (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
662
+ } })));
663
+ }
664
+ handle.showView(ctx.active);
665
+ return;
666
+ /**
667
+ * Mount one board's widgets into its group — and recurse for CONTAINERS.
668
+ * A container is a view's construction one level down: a frameless
669
+ * member group with a slab cell in the PARENT's grid, its own
670
+ * `dashboardBoard` metadata (so `fromDocument()` rebinds it like any
671
+ * board), and a second `bindDashboardGrid` on the same canvas — which
672
+ * registers it as a BinderPeer, so cross-boundary drag, deepest-wins
673
+ * hit-testing and the height-escalation ratchet all apply unchanged.
674
+ */
675
+ function mountBoard(boardId, viewId, widgets, boardGroup) {
676
+ var _a, _b, _c, _d, _e, _f;
677
+ for (const w of widgets) {
678
+ if (w.widgets) {
679
+ const innerColumns = innerColumnsOf(w);
680
+ const innerRows = (_a = w.maxRows) !== null && _a !== void 0 ? _a : rowExtentOf(w.widgets);
681
+ const cg = new GroupModel({ id: w.id, name: (_b = w.title) !== null && _b !== void 0 ? _b : w.id });
682
+ model.addGroup(cg);
683
+ cg.setMetadata('frameChrome', 'none');
684
+ // Slab cells live in GROUP metadata (groups carry no GridItemConfig).
685
+ cg.setMetadata('gridItem', gridItemFromCell({ x: w.x, y: w.y, w: w.span, h: w.rows }));
686
+ // The container's own spec fields, persisted ON the group — a
687
+ // reloaded document has no authored literal to read them from.
688
+ 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 } : {})));
689
+ cg.setMetadata('dashboardBoard', {
690
+ columns: innerColumns,
691
+ gap,
692
+ padding: 0,
693
+ sizing: 'fit',
694
+ baseRowHeight: rowHeight,
695
+ // The slab's height is the PARENT's business — 0 hands it over,
696
+ // which is what makes escalation grow the slab instead of the
697
+ // container fighting its own frame.
698
+ designHeight: 0,
699
+ maxRows: innerRows,
700
+ float: false,
701
+ rtl: (_c = options.rtl) !== null && _c !== void 0 ? _c : false,
702
+ });
703
+ cg.size = { width: 100, height: rowHeight, depth: 0 };
704
+ boardGroup.addMember(w.id);
705
+ ctx.boardGroups.set(w.id, cg);
706
+ mountBoard(w.id, viewId, w.widgets, cg);
707
+ binders.set(w.id, bindDashboardGrid(a, cg, {
708
+ columns: innerColumns,
709
+ gap,
710
+ padding: 0,
711
+ sizing: 'fit',
712
+ baseRowHeight: rowHeight,
713
+ designHeight: 0,
714
+ maxRows: innerRows,
715
+ float: false,
716
+ rtl: (_d = options.rtl) !== null && _d !== void 0 ? _d : false,
717
+ onGesture: (e) => {
718
+ var _a, _b;
719
+ if (e.type === 'commit')
720
+ reportLayoutChange(viewId);
721
+ (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
722
+ },
723
+ }));
724
+ continue;
725
+ }
549
726
  const n = model.getNode(w.id);
550
727
  if (!n)
551
728
  continue;
@@ -557,7 +734,7 @@ export function dashboard(options) {
557
734
  // and made toJSON() → dashboard() NOT round-trip (a saved layout
558
735
  // rebuilt back into its declaration order rather than its cells).
559
736
  if (w.x !== undefined && w.y !== undefined) {
560
- n.setGridItem(gridItemFromCell({ x: w.x, y: w.y, w: (_j = w.span) !== null && _j !== void 0 ? _j : 3, h: (_k = w.rows) !== null && _k !== void 0 ? _k : 1 }));
737
+ n.setGridItem(gridItemFromCell({ x: w.x, y: w.y, w: (_e = w.span) !== null && _e !== void 0 ? _e : 3, h: (_f = w.rows) !== null && _f !== void 0 ? _f : 1 }));
561
738
  }
562
739
  if (w.pinned)
563
740
  n.setState({ locked: true });
@@ -571,19 +748,24 @@ export function dashboard(options) {
571
748
  n.setBehavior({ connectable: false });
572
749
  for (const p of [...n.getPorts().values()])
573
750
  n.removePort(p.id);
574
- g.addMember(w.id);
751
+ boardGroup.addMember(w.id);
575
752
  }
576
- binders.set(v.id, bindDashboardGrid(a, g, Object.assign(Object.assign(Object.assign({ columns: (_l = v.columns) !== null && _l !== void 0 ? _l : columns, gap, padding: gap, sizing: (_m = options.sizing) !== null && _m !== void 0 ? _m : 'fit', baseRowHeight: rowHeight, designHeight: (_o = v.height) !== null && _o !== void 0 ? _o : boardH, float: (_p = options.float) !== null && _p !== void 0 ? _p : false, rtl: (_q = options.rtl) !== null && _q !== void 0 ? _q : false }, (options.responsive ? { responsive: options.responsive } : {})), ((_r = options.binder) !== null && _r !== void 0 ? _r : {})), { onGesture: (e) => {
577
- var _a, _b;
578
- if (e.type === 'commit' && options.onLayoutChange) {
579
- const snapshot = handle.toJSON().views.find((x) => x.id === v.id);
580
- if (snapshot)
581
- options.onLayoutChange(v.id, snapshot.widgets);
582
- }
583
- (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
584
- } })));
585
753
  }
586
- handle.showView(ctx.active);
754
+ /**
755
+ * One reporter for every binder on a view — the view's own and each
756
+ * container's. The payload is the view's FULL NESTED TREE derived from
757
+ * live membership (handle.toJSON()), so an inner commit reports the same
758
+ * truth an outer one does, and a tile that crossed a boundary shows up
759
+ * under its NEW parent. (The old per-view lookup found nothing for a
760
+ * container binder and silently reported nothing.)
761
+ */
762
+ function reportLayoutChange(viewId) {
763
+ if (!options.onLayoutChange)
764
+ return;
765
+ const snapshot = handle.toJSON().views.find((x) => x.id === viewId);
766
+ if (snapshot)
767
+ options.onLayoutChange(viewId, snapshot.widgets);
768
+ }
587
769
  },
588
770
  };
589
771
  }
@@ -1240,13 +1240,27 @@ export function bindDashboardGrid(api, group, options = {}) {
1240
1240
  id: `dashboard-grid:${group.id}:${++binderSeq}`,
1241
1241
  priority: 2, // point-specific claim — outranks mode-style tools (see ext/tools.ts)
1242
1242
  hitTest(ev, hit) {
1243
- var _a;
1243
+ var _a, _b;
1244
1244
  if (disposed)
1245
1245
  return false;
1246
1246
  if (gesture)
1247
1247
  return true; // own the rest of an in-flight gesture
1248
- if (hit.node)
1249
- return ((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(hit.node.id);
1248
+ if (hit.node) {
1249
+ if (((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(hit.node.id))
1250
+ return true;
1251
+ // A press on a tile that belongs to a NESTED board must reach that
1252
+ // board's tool. The dead-zone claim below deadens the slab's EMPTY
1253
+ // band — claiming a peer's tile with it swallowed every resize inside
1254
+ // an API-built container (which binds child-before-parent, so the
1255
+ // parent's tool won the registration-order tie and the child's resize
1256
+ // never armed; grid-options binds parent-first and worked by
1257
+ // accident).
1258
+ for (const p of (_b = BOARD_REGISTRY.get(api.container)) !== null && _b !== void 0 ? _b : []) {
1259
+ if (p !== selfPeer && p.hasItem(hit.node.id))
1260
+ return false;
1261
+ }
1262
+ return insideMemberGroupFrame(ev.world.x, ev.world.y);
1263
+ }
1250
1264
  // Claim (and deaden) empty presses inside a member group's frame so the
1251
1265
  // built-in group-drag cannot fight the pack layout for the KPI slab.
1252
1266
  return insideMemberGroupFrame(ev.world.x, ev.world.y);
package/src/lib/load.js CHANGED
@@ -67,6 +67,7 @@ import { bindRowInteractions } from './diagram-kit/rows.js';
67
67
  import { bindCardEditing } from './diagram-kit/editing.js';
68
68
  import { ensureDiagramKitStyles } from './diagram-kit/styles.js';
69
69
  import { bindDashboardGrid } from './dashboard-kit/grid-binder.js';
70
+ import { cellFromGridItem } from './dashboard-kit/grid-mapping.js';
70
71
  import { ensureDashboardKitStyles } from './dashboard-kit/styles.js';
71
72
  import { defaultWidgetRenderer } from './dashboard-kit/widgets.js';
72
73
  import { createDashboardHandle, } from './dashboard-kit/dashboard.js';
@@ -100,7 +101,7 @@ function widgetSpecOf(node) {
100
101
  * of either.
101
102
  */
102
103
  export function fromDocument(document, options = {}) {
103
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
104
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
104
105
  const parsed = typeof document === 'string' ? parseDocument(document) : document;
105
106
  const model = new DiagramSerializer().deserialize(parsed);
106
107
  const nodes = model.getNodes();
@@ -124,40 +125,79 @@ export function fromDocument(document, options = {}) {
124
125
  const dashGroups = groups.filter((g) => g.getMetadata('dashboardBoard') !== undefined);
125
126
  const specById = new Map();
126
127
  const viewOfWidget = new Map();
127
- const ctxViews = dashGroups.map((g) => {
128
- var _a, _b, _c;
129
- const board = g.getMetadata('dashboardBoard');
128
+ const boardWidgets = new Map();
129
+ const viewOfBoard = new Map();
130
+ const boardGroups = new Map(dashGroups.map((g) => [g.id, g]));
131
+ // A CONTAINER is a dashboard group that is itself a MEMBER of another
132
+ // dashboard group; the rest are views. This is the whole nesting test —
133
+ // containment IS membership.
134
+ const containerIds = new Set();
135
+ for (const g of dashGroups) {
136
+ for (const m of (_b = g.members) !== null && _b !== void 0 ? _b : [])
137
+ if (boardGroups.has(m))
138
+ containerIds.add(m);
139
+ }
140
+ const rebuildBoard = (g, viewId) => {
141
+ var _a, _b;
130
142
  const widgets = [];
131
143
  for (const memberId of (_a = g.members) !== null && _a !== void 0 ? _a : []) {
144
+ const childGroup = boardGroups.get(memberId);
145
+ if (childGroup) {
146
+ // A nested container: its spec fields ride on the group, its cell in
147
+ // the group's slab `gridItem`, its children in its own membership.
148
+ const meta = ((_b = childGroup.getMetadata('containerWidget')) !== null && _b !== void 0 ? _b : {});
149
+ const cell = cellFromGridItem(childGroup.getMetadata('gridItem'));
150
+ const ws = Object.assign(Object.assign(Object.assign({ id: childGroup.id }, meta), (cell ? { x: cell.x, y: cell.y, span: cell.w, rows: cell.h } : {})), { widgets: rebuildBoard(childGroup, viewId) });
151
+ specById.set(ws.id, ws);
152
+ viewOfWidget.set(ws.id, g.id);
153
+ boardWidgets.set(ws.id, ws.widgets);
154
+ viewOfBoard.set(ws.id, viewId);
155
+ widgets.push(ws);
156
+ continue;
157
+ }
132
158
  const node = model.getNode(memberId);
133
159
  const ws = node ? widgetSpecOf(node) : null;
134
160
  if (!ws)
135
- continue; // non-widget members (e.g. a nested slab group) are not widgets
161
+ continue;
136
162
  specById.set(ws.id, ws);
137
163
  viewOfWidget.set(ws.id, g.id);
138
164
  widgets.push(ws);
139
165
  }
140
- return { id: g.id, name: g.name, widgets, columns: board.columns, width: (_b = g.size) === null || _b === void 0 ? void 0 : _b.width, height: (_c = g.size) === null || _c === void 0 ? void 0 : _c.height };
166
+ return widgets;
167
+ };
168
+ const viewGroups = dashGroups.filter((g) => !containerIds.has(g.id));
169
+ const ctxViews = viewGroups.map((g) => {
170
+ var _a, _b;
171
+ const board = g.getMetadata('dashboardBoard');
172
+ const widgets = rebuildBoard(g, g.id);
173
+ boardWidgets.set(g.id, widgets);
174
+ viewOfBoard.set(g.id, g.id);
175
+ return { id: g.id, name: g.name, widgets, columns: board.columns, width: (_a = g.size) === null || _a === void 0 ? void 0 : _a.width, height: (_b = g.size) === null || _b === void 0 ? void 0 : _b.height };
141
176
  });
142
- const firstBoard = (_b = dashGroups[0]) === null || _b === void 0 ? void 0 : _b.getMetadata('dashboardBoard');
177
+ const firstBoard = (_c = viewGroups[0]) === null || _c === void 0 ? void 0 : _c.getMetadata('dashboardBoard');
143
178
  // The active view is the one the save left ON camera (x≈0); the others were
144
179
  // parked far off-screen by showView. Falls back to the first board when
145
180
  // positions are ambiguous (e.g. a single view, or positions not restored).
146
- const activeGroup = (_c = dashGroups.find((g) => g.position.x > -1000)) !== null && _c !== void 0 ? _c : dashGroups[0];
181
+ const activeGroup = (_d = viewGroups.find((g) => g.position.x > -1000)) !== null && _d !== void 0 ? _d : viewGroups[0];
147
182
  const ctx = {
148
183
  views: ctxViews,
149
- groups: new Map(dashGroups.map((g) => [g.id, g])),
184
+ // PARKING map: views only. A container must follow its parent when a view
185
+ // parks, not travel to OFFSCREEN_X on its own.
186
+ groups: new Map(viewGroups.map((g) => [g.id, g])),
150
187
  // The SAME map the LoadedDiagramSpec exposes as `boards` — derived, not a copy.
151
188
  binders: boards,
152
189
  specById,
153
190
  viewOfWidget,
191
+ boardGroups,
192
+ boardWidgets,
193
+ viewOfBoard,
154
194
  hosts: new Map(),
155
195
  renderWidget: paintWidget,
156
- columns: (_d = firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.columns) !== null && _d !== void 0 ? _d : 12,
157
- gap: (_e = firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.gap) !== null && _e !== void 0 ? _e : 8,
158
- rowHeight: (_f = firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.baseRowHeight) !== null && _f !== void 0 ? _f : 130,
159
- boardW: (_j = (_h = (_g = dashGroups[0]) === null || _g === void 0 ? void 0 : _g.size) === null || _h === void 0 ? void 0 : _h.width) !== null && _j !== void 0 ? _j : 1180,
160
- boardH: (_m = (_l = (_k = dashGroups[0]) === null || _k === void 0 ? void 0 : _k.size) === null || _l === void 0 ? void 0 : _l.height) !== null && _m !== void 0 ? _m : 660,
196
+ columns: (_e = firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.columns) !== null && _e !== void 0 ? _e : 12,
197
+ gap: (_f = firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.gap) !== null && _f !== void 0 ? _f : 8,
198
+ rowHeight: (_g = firstBoard === null || firstBoard === void 0 ? void 0 : firstBoard.baseRowHeight) !== null && _g !== void 0 ? _g : 130,
199
+ boardW: (_k = (_j = (_h = viewGroups[0]) === null || _h === void 0 ? void 0 : _h.size) === null || _j === void 0 ? void 0 : _j.width) !== null && _k !== void 0 ? _k : 1180,
200
+ boardH: (_o = (_m = (_l = viewGroups[0]) === null || _l === void 0 ? void 0 : _l.size) === null || _m === void 0 ? void 0 : _m.height) !== null && _o !== void 0 ? _o : 660,
161
201
  // responsive is NOT in the document (a runtime seam), so it is deliberately
162
202
  // absent from the round-trip; width/height/columns/gap/sizing/float/rtl are.
163
203
  optionsBase: firstBoard
@@ -168,11 +208,11 @@ export function fromDocument(document, options = {}) {
168
208
  sizing: firstBoard.sizing,
169
209
  float: firstBoard.float,
170
210
  rtl: firstBoard.rtl,
171
- width: (_p = (_o = dashGroups[0]) === null || _o === void 0 ? void 0 : _o.size) === null || _p === void 0 ? void 0 : _p.width,
172
- height: (_r = (_q = dashGroups[0]) === null || _q === void 0 ? void 0 : _q.size) === null || _r === void 0 ? void 0 : _r.height,
211
+ width: (_q = (_p = viewGroups[0]) === null || _p === void 0 ? void 0 : _p.size) === null || _q === void 0 ? void 0 : _q.width,
212
+ height: (_s = (_r = viewGroups[0]) === null || _r === void 0 ? void 0 : _r.size) === null || _s === void 0 ? void 0 : _s.height,
173
213
  }
174
214
  : {},
175
- active: (_s = activeGroup === null || activeGroup === void 0 ? void 0 : activeGroup.id) !== null && _s !== void 0 ? _s : 'main',
215
+ active: (_t = activeGroup === null || activeGroup === void 0 ? void 0 : activeGroup.id) !== null && _t !== void 0 ? _t : 'main',
176
216
  apiRef: null,
177
217
  };
178
218
  const handle = createDashboardHandle(ctx);