@grafloria/element 0.4.1 → 0.4.3

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.
@@ -43,8 +43,9 @@
43
43
  * round-trips with no extra work — same as every other kit.
44
44
  */
45
45
  import { __awaiter } from "tslib";
46
- import { AddToGroupCommand, BatchCommand, BringNodeToFrontCommand, Command, GroupModel, NodeModel, RemoveFromGroupCommand, RemoveNodeCommand, SendNodeToBackCommand, } from '@grafloria/engine';
46
+ import { BatchCommand, BringNodeToFrontCommand, Command, GroupModel, NodeModel, RemoveFromGroupCommand, RemoveGroupCommand, RemoveNodeCommand, SendNodeToBackCommand, } from '@grafloria/engine';
47
47
  import { bindDashboardGrid, } from './grid-binder.js';
48
+ import { bindDashboardSplit, SPLIT_TREE_KEY } from './split-binder.js';
48
49
  import { gridItemFromCell } from './grid-mapping.js';
49
50
  import { ensureDashboardKitStyles } from './styles.js';
50
51
  import { defaultWidgetRenderer } from './widgets.js';
@@ -60,26 +61,41 @@ import { defaultWidgetRenderer } from './widgets.js';
60
61
  * unwinds both in undo().
61
62
  */
62
63
  class AddWidgetCommand extends Command {
63
- constructor(node, groupId) {
64
+ /**
65
+ * `registry` is the kit's bookkeeping for the widget (see
66
+ * RegisterWidgetCommand): it is applied INSIDE this command rather than in a
67
+ * batch beside it, because a batch runs its members across awaits and the
68
+ * node would reach the model a microtask after the caller's `addWidget()`
69
+ * returned — every consumer that read the model right after would have
70
+ * broken. `nodeWasInModel` covers re-adding a node that already exists
71
+ * (membership only), the case that used to be a separate AddToGroupCommand.
72
+ */
73
+ constructor(node, groupId, registry, nodeWasInModel = false) {
64
74
  super('Add widget');
65
75
  this.node = node;
66
76
  this.groupId = groupId;
77
+ this.registry = registry;
78
+ this.nodeWasInModel = nodeWasInModel;
67
79
  }
68
80
  execute(context) {
69
- var _a;
81
+ var _a, _b;
70
82
  const diagram = context.diagram;
71
83
  if (!diagram)
72
84
  return;
73
- diagram.addNode(this.node);
74
- (_a = diagram.getGroup(this.groupId)) === null || _a === void 0 ? void 0 : _a.addMember(this.node.id);
85
+ (_a = this.registry) === null || _a === void 0 ? void 0 : _a.register();
86
+ if (!this.nodeWasInModel && !diagram.getNode(this.node.id))
87
+ diagram.addNode(this.node);
88
+ (_b = diagram.getGroup(this.groupId)) === null || _b === void 0 ? void 0 : _b.addMember(this.node.id);
75
89
  }
76
90
  undo(context) {
77
- var _a;
91
+ var _a, _b;
78
92
  const diagram = context.diagram;
79
93
  if (!diagram)
80
94
  return;
81
95
  (_a = diagram.getGroup(this.groupId)) === null || _a === void 0 ? void 0 : _a.removeMember(this.node.id);
82
- diagram.removeNode(this.node.id);
96
+ if (!this.nodeWasInModel)
97
+ diagram.removeNode(this.node.id);
98
+ (_b = this.registry) === null || _b === void 0 ? void 0 : _b.unregister();
83
99
  }
84
100
  serialize() {
85
101
  return {
@@ -90,6 +106,81 @@ class AddWidgetCommand extends Command {
90
106
  };
91
107
  }
92
108
  }
109
+ /**
110
+ * The kit's BOOKKEEPING for a widget — its spec, its board, its slot in the
111
+ * authored array — travels through the history WITH the node.
112
+ *
113
+ * It did not, and that was D2 of the 2026-09-06 review: `remove()` deleted the
114
+ * spec synchronously while the removal itself was a command, so undo restored
115
+ * the node and the membership through the model and the painter — asked to
116
+ * paint a node whose id the kit no longer knew — returned without drawing. A
117
+ * blank host where the donut was, `widget(id)` undefined, not listed. The
118
+ * comment on AddWidgetCommand already warns that the painter needs the spec
119
+ * BEFORE the node reaches the model; the same holds on the way back in.
120
+ *
121
+ * `register`/`unregister` are idempotent so the handle may also apply them
122
+ * synchronously (execute() is async, and the caller reads the handle right
123
+ * after) without double-counting when the command runs.
124
+ */
125
+ class RegisterWidgetCommand extends Command {
126
+ constructor(registry, direction) {
127
+ super(direction === 'register' ? 'Register widget' : 'Unregister widget');
128
+ this.registry = registry;
129
+ this.direction = direction;
130
+ }
131
+ execute() {
132
+ if (this.direction === 'register')
133
+ this.registry.register();
134
+ else
135
+ this.registry.unregister();
136
+ }
137
+ undo() {
138
+ if (this.direction === 'register')
139
+ this.registry.unregister();
140
+ else
141
+ this.registry.register();
142
+ }
143
+ serialize() {
144
+ return { id: this.id, name: this.name, timestamp: this.timestamp, data: { direction: this.direction } };
145
+ }
146
+ }
147
+ /**
148
+ * Pin as ONE undoable step. `pin()` wrote the node's lock directly, which made
149
+ * it the only layout mutation outside the history (Ctrl-Z after a pin undid the
150
+ * gesture before it) and the reason toJSON() never saw it — D5. `before` is
151
+ * captured at construction so the handle can apply the lock synchronously and
152
+ * let the command re-apply it idempotently when it runs.
153
+ */
154
+ class SetWidgetLockCommand extends Command {
155
+ constructor(nodeId, before, after) {
156
+ super(after ? 'Pin widget' : 'Unpin widget');
157
+ this.nodeId = nodeId;
158
+ this.before = before;
159
+ this.after = after;
160
+ }
161
+ apply(context, locked) {
162
+ var _a;
163
+ const diagram = context.diagram;
164
+ (_a = diagram === null || diagram === void 0 ? void 0 : diagram.getNode(this.nodeId)) === null || _a === void 0 ? void 0 : _a.setState({ locked });
165
+ }
166
+ execute(context) {
167
+ this.apply(context, this.after);
168
+ }
169
+ undo(context) {
170
+ this.apply(context, this.before);
171
+ }
172
+ serialize() {
173
+ return {
174
+ id: this.id,
175
+ name: this.name,
176
+ timestamp: this.timestamp,
177
+ data: { nodeId: this.nodeId, before: this.before, after: this.after },
178
+ };
179
+ }
180
+ }
181
+ /** The history events a layout can change on — the engine's DiagramEventTypes
182
+ * values, spelled out so the kit needs no import from the engine's type bag. */
183
+ const HISTORY_EVENTS = ['command:executed', 'command:undone', 'command:redone'];
93
184
  const DEFAULTS = { columns: 12, gap: 8, rowHeight: 130, width: 1180, height: 660 };
94
185
  const OFFSCREEN_X = -20000;
95
186
  let autoId = 0;
@@ -115,6 +206,15 @@ function buildWidgetNode(w, rowHeight) {
115
206
  node.setMetadata('widgetTitle', w.title);
116
207
  node.setMetadata('columnSpan', (_c = w.span) !== null && _c !== void 0 ? _c : 3);
117
208
  node.setMetadata('rowSpan', (_d = w.rows) !== null && _d !== void 0 ? _d : 1);
209
+ // Limits and the two pointer flags reach the node too, so the binder reads
210
+ // them and a reload rebuilds them (only when authored — an unconstrained
211
+ // widget serialises exactly as it always did).
212
+ if (w.limits !== undefined)
213
+ node.setMetadata('widgetLimits', Object.assign({}, w.limits));
214
+ if (w.movable === false)
215
+ node.setMetadata('widgetMovable', false);
216
+ if (w.resizable === false)
217
+ node.setMetadata('widgetResizable', false);
118
218
  if (w.x !== undefined && w.y !== undefined) {
119
219
  node.setGridItem({
120
220
  columnStart: w.x + 1,
@@ -129,15 +229,52 @@ function buildWidgetNode(w, rowHeight) {
129
229
  node.removePort(p.id);
130
230
  return node;
131
231
  }
232
+ function cloneWidgets(ws) {
233
+ return ws.map((w) => (Object.assign(Object.assign({}, w), (w.widgets ? { widgets: cloneWidgets(w.widgets) } : {}))));
234
+ }
235
+ /** A container's inner column count: authored, else its own span — inner cells
236
+ * then ride the parent's column rhythm, which is what a section reads as. */
237
+ function innerColumnsOf(w) {
238
+ var _a, _b;
239
+ return Math.max(1, (_b = (_a = w.columns) !== null && _a !== void 0 ? _a : w.span) !== null && _b !== void 0 ? _b : 3);
240
+ }
241
+ /** The row extent of a laid-out widget list (the inner grid's design height). */
242
+ function rowExtentOf(widgets) {
243
+ var _a, _b;
244
+ let max = 1;
245
+ for (const w of widgets)
246
+ max = Math.max(max, ((_a = w.y) !== null && _a !== void 0 ? _a : 0) + ((_b = w.rows) !== null && _b !== void 0 ? _b : 1));
247
+ return max;
248
+ }
249
+ /** Recursive `assignCells`: a container flows in its parent like any widget,
250
+ * and its children flow inside its OWN column count. */
251
+ function assignCellsDeep(widgets, columns) {
252
+ assignCells(widgets, columns);
253
+ for (const w of widgets) {
254
+ if (w.widgets)
255
+ assignCellsDeep(w.widgets, innerColumnsOf(w));
256
+ }
257
+ }
132
258
  /** Flow widgets that declared no cell: left-to-right, wrapping at `columns`. */
133
259
  function assignCells(widgets, columns) {
134
- var _a, _b;
260
+ var _a, _b, _c;
135
261
  let x = 0;
136
262
  let y = 0;
137
263
  let rowMax = 0;
138
264
  for (const w of widgets) {
139
- const span = Math.max(1, Math.min(columns, (_a = w.span) !== null && _a !== void 0 ? _a : 3));
140
- const rows = Math.max(1, (_b = w.rows) !== null && _b !== void 0 ? _b : 1);
265
+ const lim = (_a = w.limits) !== null && _a !== void 0 ? _a : {};
266
+ let span = Math.max(1, Math.min(columns, (_b = w.span) !== null && _b !== void 0 ? _b : 3));
267
+ if (lim.minSpan !== undefined)
268
+ span = Math.max(span, lim.minSpan);
269
+ if (lim.maxSpan !== undefined)
270
+ span = Math.min(span, lim.maxSpan);
271
+ span = Math.max(1, Math.min(columns, span));
272
+ let rows = Math.max(1, (_c = w.rows) !== null && _c !== void 0 ? _c : 1);
273
+ if (lim.minRows !== undefined)
274
+ rows = Math.max(rows, lim.minRows);
275
+ if (lim.maxRows !== undefined)
276
+ rows = Math.min(rows, lim.maxRows);
277
+ rows = Math.max(1, rows);
141
278
  if (w.x === undefined || w.y === undefined) {
142
279
  if (x + span > columns) {
143
280
  x = 0;
@@ -161,6 +298,175 @@ function assignCells(widgets, columns) {
161
298
  export function createDashboardHandle(ctx) {
162
299
  const { views, groups, binders, specById, viewOfWidget } = ctx;
163
300
  const hostOf = (id) => ctx.hosts.get(id);
301
+ /**
302
+ * Put the camera on a view. FLUID: the board IS the container, so the camera
303
+ * sits at zoom 1 with the board's origin at the top-left — never a fit, which
304
+ * is what made a dashboard a scaled picture (D1). FIXED: frame the board.
305
+ */
306
+ const frameView = (g) => {
307
+ var _a, _b, _c;
308
+ const vp = (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.viewport;
309
+ if (!vp)
310
+ return;
311
+ const gs = (_b = g.size) !== null && _b !== void 0 ? _b : { width: ctx.boardW, height: ctx.boardH };
312
+ if (ctx.mode === 'fluid' && vp.setViewport && vp.getViewport) {
313
+ (_c = vp.setZoom) === null || _c === void 0 ? void 0 : _c.call(vp, 1);
314
+ const cur = vp.getViewport();
315
+ vp.setViewport({ x: g.position.x, y: g.position.y, width: cur.width, height: cur.height });
316
+ return;
317
+ }
318
+ vp.fitToBounds({ x: g.position.x, y: g.position.y, width: gs.width, height: gs.height }, 26, { maxZoom: 1 });
319
+ };
320
+ /**
321
+ * FLUID: the camera is a SCROLL POSITION over the board, never a free pan.
322
+ * Bounded to the active board's frame — the page-scroll model of every grid
323
+ * library — so a wheel cannot run past the last row, and a frame that
324
+ * shrinks under a scrolled camera (Fit after a scroll in Grow; an undo that
325
+ * removes rows) pulls the camera back into the board. Found by the user
326
+ * switching Fit and Grow on the live page: Fit shrank the board to the
327
+ * canvas while the camera stayed 600 px down — the top half of the board
328
+ * out of view above an empty canvas. FIXED boards are diagrams and pan free.
329
+ */
330
+ let clamping = false;
331
+ const clampCamera = () => {
332
+ var _a, _b;
333
+ if (ctx.mode !== 'fluid' || clamping)
334
+ return;
335
+ const vp = (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.viewport;
336
+ const g = groups.get(ctx.active);
337
+ if (!(vp === null || vp === void 0 ? void 0 : vp.getViewport) || !vp.setViewport || !g)
338
+ return;
339
+ const cur = vp.getViewport();
340
+ const gs = (_b = g.size) !== null && _b !== void 0 ? _b : { width: ctx.boardW, height: ctx.boardH };
341
+ const x = Math.min(Math.max(cur.x, g.position.x), g.position.x + Math.max(0, gs.width - cur.width));
342
+ const y = Math.min(Math.max(cur.y, g.position.y), g.position.y + Math.max(0, gs.height - cur.height));
343
+ if (Math.abs(x - cur.x) < 0.5 && Math.abs(y - cur.y) < 0.5)
344
+ return;
345
+ clamping = true;
346
+ try {
347
+ vp.setViewport({ x, y, width: cur.width, height: cur.height });
348
+ }
349
+ finally {
350
+ clamping = false;
351
+ }
352
+ };
353
+ /**
354
+ * A board's widgets as a NESTED tree, derived from LIVE membership — not the
355
+ * authored arrays. A cross-boundary drag moves membership through commands
356
+ * (and undo moves it back); the authored arrays do not follow. Deriving from
357
+ * the groups + engines is what makes toJSON() and onLayoutChange report a
358
+ * tile under the container it is actually in, in every one of those states.
359
+ */
360
+ const treeOf = (boardId) => {
361
+ var _a, _b, _c, _d;
362
+ const g = ctx.boardGroups.get(boardId);
363
+ const b = binders.get(boardId);
364
+ if (!g)
365
+ return [];
366
+ // Cells from the binder's serialisation (largest cached layout — the
367
+ // saving-on-a-phone rule), falling back to the live engine cell.
368
+ const saved = b === null || b === void 0 ? void 0 : b.saveLayout();
369
+ 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); };
370
+ const entries = [];
371
+ for (const memberId of (_a = g.members) !== null && _a !== void 0 ? _a : []) {
372
+ const spec = specById.get(memberId);
373
+ const cell = cellOf(memberId);
374
+ const at = cell ? { x: cell.x, y: cell.y, span: cell.w, rows: cell.h } : {};
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) }));
377
+ }
378
+ else if (spec) {
379
+ // `pinned` is read from the NODE's lock, not the authored spec: pin()
380
+ // changes the node, and a saved board must come back pinned the way
381
+ // the user left it (D5). Written only when true, so an unpinned
382
+ // widget serialises exactly as it always did.
383
+ 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)
385
+ entry.pinned = true;
386
+ else
387
+ delete entry.pinned;
388
+ entries.push(entry);
389
+ }
390
+ }
391
+ 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); });
392
+ return entries;
393
+ };
394
+ /** Bookkeeping closures for one widget, shared by the add and remove
395
+ * commands (and applied synchronously by the handle — idempotent). */
396
+ const registryOf = (id, boardId, spec) => {
397
+ let slot = -1;
398
+ return {
399
+ register: () => {
400
+ specById.set(id, spec);
401
+ viewOfWidget.set(id, boardId);
402
+ const arr = ctx.boardWidgets.get(boardId);
403
+ if (arr && !arr.some((w) => w.id === id)) {
404
+ arr.splice(slot < 0 ? arr.length : Math.min(slot, arr.length), 0, spec);
405
+ }
406
+ },
407
+ unregister: () => {
408
+ specById.delete(id);
409
+ viewOfWidget.delete(id);
410
+ const arr = ctx.boardWidgets.get(boardId);
411
+ if (arr) {
412
+ const i = arr.findIndex((w) => w.id === id);
413
+ if (i >= 0) {
414
+ slot = i; // remembered so undo puts it back where it was
415
+ arr.splice(i, 1);
416
+ }
417
+ }
418
+ },
419
+ };
420
+ };
421
+ // ONE reporter for every path that changes a layout (D3). Diff-based: a
422
+ // pointer commit reports synchronously, the history event that follows finds
423
+ // nothing new and stays quiet.
424
+ const lastReported = new Map();
425
+ const reportChanged = () => {
426
+ if (!ctx.onLayoutChange || !ctx.apiRef)
427
+ return;
428
+ for (const v of handle.toJSON().views) {
429
+ const key = JSON.stringify(v.widgets);
430
+ if (lastReported.get(v.id) === key)
431
+ continue;
432
+ lastReported.set(v.id, key);
433
+ ctx.onLayoutChange(v.id, v.widgets);
434
+ }
435
+ };
436
+ ctx.reportChanged = reportChanged;
437
+ // The boards follow the HISTORY, not the consumer's memory of it: after any
438
+ // command lands, is undone or redone, every binder re-reads the model and the
439
+ // reporter runs. This is what retires "call refresh() after undo".
440
+ ctx.attachHistory = () => {
441
+ var _a, _b, _c, _d, _e;
442
+ const bus = (_c = (_b = (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.getEngine) === null || _b === void 0 ? void 0 : _b.call(_a)) === null || _c === void 0 ? void 0 : _c.eventBus;
443
+ if (!bus)
444
+ return;
445
+ // Prime the reporter so the boot layout is never reported as a change.
446
+ for (const v of handle.toJSON().views)
447
+ lastReported.set(v.id, JSON.stringify(v.widgets));
448
+ const onHistory = () => {
449
+ var _a;
450
+ if (!ctx.apiRef)
451
+ return;
452
+ const model = ctx.apiRef.getModel();
453
+ for (const id of [...ctx.boardGroups.keys()]) {
454
+ if (!binders.has(id) && model.getGroup(id))
455
+ (_a = ctx.rebindContainer) === null || _a === void 0 ? void 0 : _a.call(ctx, id);
456
+ }
457
+ for (const b of binders.values())
458
+ b.sync();
459
+ clampCamera();
460
+ ctx.apiRef.renderNow();
461
+ reportChanged();
462
+ };
463
+ ctx.subscriptions = (_d = ctx.subscriptions) !== null && _d !== void 0 ? _d : [];
464
+ for (const ev of HISTORY_EVENTS)
465
+ ctx.subscriptions.push(bus.on(ev, onHistory));
466
+ const vp = (_e = ctx.apiRef) === null || _e === void 0 ? void 0 : _e.viewport;
467
+ if (ctx.mode === 'fluid' && (vp === null || vp === void 0 ? void 0 : vp.onChange))
468
+ ctx.subscriptions.push(vp.onChange(() => clampCamera()));
469
+ };
164
470
  const execCommand = (cmd) => {
165
471
  var _a, _b, _c, _d;
166
472
  try {
@@ -181,7 +487,7 @@ export function createDashboardHandle(ctx) {
181
487
  return ctx.active;
182
488
  },
183
489
  showView(id) {
184
- var _a, _b, _c, _d, _e, _f;
490
+ var _a, _b, _c;
185
491
  if (!groups.has(id))
186
492
  return;
187
493
  ctx.active = id;
@@ -193,9 +499,7 @@ export function createDashboardHandle(ctx) {
193
499
  }
194
500
  (_b = binders.get(id)) === null || _b === void 0 ? void 0 : _b.sync();
195
501
  (_c = ctx.apiRef) === null || _c === void 0 ? void 0 : _c.renderNow();
196
- const g = groups.get(id);
197
- const gs = (_d = g.size) !== null && _d !== void 0 ? _d : { width: ctx.boardW, height: ctx.boardH };
198
- (_f = (_e = ctx.apiRef) === null || _e === void 0 ? void 0 : _e.viewport) === null || _f === void 0 ? void 0 : _f.fitToBounds({ x: g.position.x, y: g.position.y, width: gs.width, height: gs.height }, 26, { maxZoom: 1 });
502
+ frameView(groups.get(id));
199
503
  },
200
504
  widget(id) {
201
505
  return makeWidgetHandle(id);
@@ -205,18 +509,33 @@ export function createDashboardHandle(ctx) {
205
509
  const v = views.find((x) => x.id === (viewId !== null && viewId !== void 0 ? viewId : ctx.active));
206
510
  return ((_a = v === null || v === void 0 ? void 0 : v.widgets) !== null && _a !== void 0 ? _a : []).map((w) => makeWidgetHandle(w.id)).filter(Boolean);
207
511
  },
512
+ setLayout(layout, viewId) {
513
+ var _a, _b, _c;
514
+ const vid = viewId !== null && viewId !== void 0 ? viewId : ctx.active;
515
+ if (!views.some((v) => v.id === vid))
516
+ return;
517
+ if (((_a = ctx.layoutOf.get(vid)) !== null && _a !== void 0 ? _a : 'grid') === layout)
518
+ return;
519
+ (_b = ctx.rebindView) === null || _b === void 0 ? void 0 : _b.call(ctx, vid, layout);
520
+ clampCamera();
521
+ (_c = ctx.apiRef) === null || _c === void 0 ? void 0 : _c.renderNow();
522
+ reportChanged();
523
+ },
524
+ getLayout: (viewId) => { var _a; return (_a = ctx.layoutOf.get(viewId !== null && viewId !== void 0 ? viewId : ctx.active)) !== null && _a !== void 0 ? _a : 'grid'; },
208
525
  setSizing(mode) {
209
526
  var _a;
210
527
  for (const b of binders.values())
211
528
  b.setSizing(mode);
529
+ clampCamera();
212
530
  (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.renderNow();
213
531
  },
214
- getSizing: () => { var _a, _b, _c; return (_b = (_a = binders.get(ctx.active)) === null || _a === void 0 ? void 0 : _a.getSizing()) !== null && _b !== void 0 ? _b : ((_c = ctx.optionsBase.sizing) !== null && _c !== void 0 ? _c : 'fit'); },
532
+ getSizing: () => { var _a, _b, _c; return (_c = (_b = (_a = binders.get(ctx.active)) === null || _a === void 0 ? void 0 : _a.getSizing()) !== null && _b !== void 0 ? _b : ctx.optionsBase.sizing) !== null && _c !== void 0 ? _c : (ctx.mode === 'fluid' ? 'grow' : 'fit'); },
215
533
  setFloat(on) {
216
534
  var _a;
217
535
  for (const b of binders.values())
218
536
  b.setFloat(on);
219
537
  (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.renderNow();
538
+ reportChanged(); // gravity re-packs when float turns off
220
539
  },
221
540
  getFloat: () => { var _a, _b, _c; return (_b = (_a = binders.get(ctx.active)) === null || _a === void 0 ? void 0 : _a.getFloat()) !== null && _b !== void 0 ? _b : ((_c = ctx.optionsBase.float) !== null && _c !== void 0 ? _c : false); },
222
541
  setColumns(n, layout, viewId) {
@@ -225,6 +544,7 @@ export function createDashboardHandle(ctx) {
225
544
  for (const b of targets)
226
545
  b === null || b === void 0 ? void 0 : b.setColumns(n, layout);
227
546
  (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.renderNow();
547
+ reportChanged(); // derived state, never a command — report it here
228
548
  },
229
549
  getColumns: (viewId) => { var _a, _b; return (_b = (_a = binders.get(viewId !== null && viewId !== void 0 ? viewId : ctx.active)) === null || _a === void 0 ? void 0 : _a.getColumns()) !== null && _b !== void 0 ? _b : ctx.columns; },
230
550
  setRtl(on) {
@@ -234,29 +554,43 @@ export function createDashboardHandle(ctx) {
234
554
  (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.renderNow();
235
555
  },
236
556
  getRtl: () => { var _a, _b, _c; return (_b = (_a = binders.get(ctx.active)) === null || _a === void 0 ? void 0 : _a.getRtl()) !== null && _b !== void 0 ? _b : ((_c = ctx.optionsBase.rtl) !== null && _c !== void 0 ? _c : false); },
557
+ setStatic(on) {
558
+ var _a;
559
+ for (const b of binders.values())
560
+ b.setStatic(on);
561
+ (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.renderNow();
562
+ },
563
+ getStatic: () => { var _a, _b, _c; return (_b = (_a = binders.get(ctx.active)) === null || _a === void 0 ? void 0 : _a.getStatic()) !== null && _b !== void 0 ? _b : ((_c = ctx.optionsBase.static) !== null && _c !== void 0 ? _c : false); },
237
564
  addWidget(spec, viewId) {
238
- var _a, _b, _c, _d, _e;
565
+ var _a, _b, _c, _d, _e, _f;
239
566
  const vid = viewId !== null && viewId !== void 0 ? viewId : ctx.active;
240
- const v = views.find((x) => x.id === vid);
567
+ // `vid` may name a view OR a container — both are boards with a group,
568
+ // a binder and an authored array.
569
+ const arr = ctx.boardWidgets.get(vid);
241
570
  const model = (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.getModel();
242
- const group = groups.get(vid);
243
- if (!v || !model || !group)
571
+ const group = ctx.boardGroups.get(vid);
572
+ if (!arr || !model || !group)
244
573
  return undefined;
245
574
  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
575
  // REGISTER FIRST: a custom node mounts exactly once, and the painter
247
576
  // returns early for an id the spec does not know — so the widget must be
248
577
  // known before the node reaches the model, or it paints blank forever.
249
- v.widgets.push(w);
250
- specById.set(w.id, w);
251
- viewOfWidget.set(w.id, vid);
578
+ // The registration ALSO rides in the batch, so undo un-lists the widget
579
+ // and redo lists it again before the node comes back.
580
+ // A bounded fit board with no room says so HERE, before anything is
581
+ // created: undefined, the same answer as an unknown board.
582
+ if (((_d = binders.get(vid)) === null || _d === void 0 ? void 0 : _d.willItFit(w.span, w.rows)) === false)
583
+ return undefined;
584
+ const registry = registryOf(w.id, vid, w);
585
+ registry.register();
252
586
  const existing = model.getNode(w.id);
253
587
  const node = existing !== null && existing !== void 0 ? existing : buildWidgetNode(w, ctx.rowHeight);
254
588
  if (w.pinned)
255
589
  node.setState({ locked: true });
256
- // ONE undoable step (see AddWidgetCommand for why this cannot be a batch).
257
- execCommand(existing ? new AddToGroupCommand(group.id, w.id) : new AddWidgetCommand(node, group.id));
258
- (_d = binders.get(vid)) === null || _d === void 0 ? void 0 : _d.sync();
259
- (_e = ctx.apiRef) === null || _e === void 0 ? void 0 : _e.renderNow();
590
+ // ONE undoable step, registration included (see AddWidgetCommand).
591
+ execCommand(new AddWidgetCommand(node, group.id, registry, !!existing));
592
+ (_e = binders.get(vid)) === null || _e === void 0 ? void 0 : _e.sync();
593
+ (_f = ctx.apiRef) === null || _f === void 0 ? void 0 : _f.renderNow();
260
594
  return makeWidgetHandle(w.id);
261
595
  },
262
596
  refresh() {
@@ -266,12 +600,9 @@ export function createDashboardHandle(ctx) {
266
600
  (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.renderNow();
267
601
  },
268
602
  fit(viewId) {
269
- var _a, _b, _c;
270
603
  const g = groups.get(viewId !== null && viewId !== void 0 ? viewId : ctx.active);
271
- if (!g)
272
- return;
273
- const gs = (_a = g.size) !== null && _a !== void 0 ? _a : { width: ctx.boardW, height: ctx.boardH };
274
- (_c = (_b = ctx.apiRef) === null || _b === void 0 ? void 0 : _b.viewport) === null || _c === void 0 ? void 0 : _c.fitToBounds({ x: g.position.x, y: g.position.y, width: gs.width, height: gs.height }, 26, { maxZoom: 1 });
604
+ if (g)
605
+ frameView(g);
275
606
  },
276
607
  metrics(viewId) {
277
608
  var _a;
@@ -292,9 +623,16 @@ export function createDashboardHandle(ctx) {
292
623
  ids.add(id);
293
624
  // Read the SPEC, not the group's member Set: membership is maintained by
294
625
  // 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);
626
+ // The spec is what the view IS. Containers add themselves AND their
627
+ // subtree an exported container without its children is an empty frame.
628
+ const walk = (ws) => {
629
+ for (const w of ws) {
630
+ ids.add(w.id);
631
+ if (w.widgets)
632
+ walk(w.widgets);
633
+ }
634
+ };
635
+ walk((_b = (_a = views.find((v) => v.id === id)) === null || _a === void 0 ? void 0 : _a.widgets) !== null && _b !== void 0 ? _b : []);
298
636
  return ids;
299
637
  },
300
638
  toJSON() {
@@ -304,31 +642,49 @@ export function createDashboardHandle(ctx) {
304
642
  // layout its user authored — and the view's `columns` is that count, so
305
643
  // feeding this straight back into dashboard() rebuilds the wide board.
306
644
  const savedViews = views.map((v) => {
307
- var _a;
645
+ var _a, _b, _c, _d, _e;
308
646
  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
- }) });
647
+ const live = treeOf(v.id);
648
+ const layout = (_b = ctx.layoutOf.get(v.id)) !== null && _b !== void 0 ? _b : 'grid';
649
+ const split = binders.get(v.id);
650
+ const tree = layout === 'split' && (split === null || split === void 0 ? void 0 : split.getSplitTree) ? split.getSplitTree() : undefined;
651
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, v), (saved ? { columns: saved.columns } : {})), { layout }), (tree !== undefined ? { tree } : {})), {
652
+ // Live membership when the view is mounted; the authored tree before
653
+ // finalize (a spec serialised without ever rendering keeps its shape).
654
+ widgets: live.length > 0 || ((_e = (_d = (_c = ctx.boardGroups.get(v.id)) === null || _c === void 0 ? void 0 : _c.members) === null || _d === void 0 ? void 0 : _d.size) !== null && _e !== void 0 ? _e : 0) > 0 ? live : v.widgets.map((w) => (Object.assign({}, w))) });
314
655
  });
315
656
  // Board options come off the LIVE board wherever the handle can see it —
316
657
  // `sizing` and `float` are the two a user changes from the toolbar, and
317
658
  // reading them from the authored literal would restore the board they
318
659
  // started with rather than the one they are looking at.
319
- 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 });
660
+ return Object.assign(Object.assign({}, ctx.optionsBase), { renderWidget: undefined, onLayoutChange: undefined, columns: ctx.columns, gap: ctx.gap, rowHeight: ctx.rowHeight, mode: ctx.mode, overflow: ctx.overflow, sizing: handle.getSizing(), float: handle.getFloat(), rtl: handle.getRtl(), static: handle.getStatic(), layout: handle.getLayout(), views: savedViews });
320
661
  },
321
662
  dispose() {
322
- var _a, _b;
663
+ var _a, _b, _c, _d, _e;
664
+ for (const off of (_a = ctx.subscriptions) !== null && _a !== void 0 ? _a : [])
665
+ off();
666
+ ctx.subscriptions = [];
323
667
  for (const b of binders.values())
324
668
  b.dispose();
325
669
  binders.clear();
326
670
  // The groups finalize() created are ours to clean up — leaving them
327
671
  // behind made a rebuild stack a second set of boards on the first.
328
- const model = (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.getModel();
672
+ const model = (_b = ctx.apiRef) === null || _b === void 0 ? void 0 : _b.getModel();
673
+ // …and so are the widget NODES (D9): they used to stay behind with live
674
+ // hosts, so a consumer switching dashboards in one canvas accumulated
675
+ // orphans, and a rebuild with the same ids re-added nodes that existed.
676
+ for (const id of specById.keys()) {
677
+ if (!ctx.boardGroups.has(id))
678
+ (_c = model === null || model === void 0 ? void 0 : model.removeNode) === null || _c === void 0 ? void 0 : _c.call(model, id);
679
+ }
680
+ // Deepest first: a container group removed after its parent is an orphan
681
+ // the model never saw inside a board.
682
+ for (const id of [...ctx.boardGroups.keys()].reverse())
683
+ (_d = model === null || model === void 0 ? void 0 : model.removeGroup) === null || _d === void 0 ? void 0 : _d.call(model, id);
329
684
  for (const id of groups.keys())
330
- (_b = model === null || model === void 0 ? void 0 : model.removeGroup) === null || _b === void 0 ? void 0 : _b.call(model, id);
685
+ (_e = model === null || model === void 0 ? void 0 : model.removeGroup) === null || _e === void 0 ? void 0 : _e.call(model, id);
331
686
  groups.clear();
687
+ ctx.boardGroups.clear();
332
688
  ctx.hosts.clear();
333
689
  },
334
690
  };
@@ -377,12 +733,20 @@ export function createDashboardHandle(ctx) {
377
733
  const n = node();
378
734
  if (!n)
379
735
  return;
380
- n.setState({ locked: on !== null && on !== void 0 ? on : !(((_a = n.state) === null || _a === void 0 ? void 0 : _a.locked) === true) });
736
+ const before = ((_a = n.state) === null || _a === void 0 ? void 0 : _a.locked) === true;
737
+ const after = on !== null && on !== void 0 ? on : !before;
738
+ if (after === before)
739
+ return;
740
+ // Applied now so the handle reads back correctly at once, AND recorded
741
+ // as one undoable step (D5) — the command re-applies idempotently.
742
+ n.setState({ locked: after });
743
+ execCommand(new SetWidgetLockCommand(id, before, after));
381
744
  // Re-sync so the ENGINE's locked flag (never pushed, drags refused)
382
745
  // and the hidden corner handle take effect on this frame, not the next
383
746
  // gesture.
384
747
  (_b = binder()) === null || _b === void 0 ? void 0 : _b.sync();
385
748
  (_c = ctx.apiRef) === null || _c === void 0 ? void 0 : _c.renderNow();
749
+ reportChanged();
386
750
  },
387
751
  bringToFront() {
388
752
  var _a;
@@ -406,9 +770,59 @@ export function createDashboardHandle(ctx) {
406
770
  ctx.renderWidget(spec, host);
407
771
  },
408
772
  remove(displaced) {
409
- var _a, _b;
773
+ var _a, _b, _c, _d, _e, _f;
774
+ if (spec.widgets) {
775
+ // A CONTAINER (review D12): its subtree, its own group, its slab in
776
+ // the parent board and the parent's re-pack, as ONE undoable batch.
777
+ // Undo restores the groups as fresh GroupModels, so the history
778
+ // handler re-binds the container's grid (ctx.rebindContainer).
779
+ const parentGroup = ctx.boardGroups.get(viewId);
780
+ const parentBinder = binders.get(viewId);
781
+ const model = (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.getModel();
782
+ if (!parentGroup || !parentBinder || !model)
783
+ return;
784
+ // ORDER MATTERS FOR UNDO. A batch may only undo when EVERY member
785
+ // can, checked before any of them runs — and RemoveFromGroupCommand
786
+ // can undo only while its group exists. So no membership commands
787
+ // for the members of a group that is going: the group is removed
788
+ // FIRST (its serialized members ride with it and come back on undo),
789
+ // its nodes after; on undo the nodes return, then the group with its
790
+ // membership, then the slab's membership in the parent.
791
+ const cmds = [
792
+ ...((_b = displaced) !== null && _b !== void 0 ? _b : parentBinder.planRemoval(id)),
793
+ new RemoveFromGroupCommand(parentGroup.id, id),
794
+ ];
795
+ const nodeRemovals = [];
796
+ const unregisters = [];
797
+ const removeSubtree = (boardId) => {
798
+ var _a, _b;
799
+ const g = ctx.boardGroups.get(boardId);
800
+ cmds.push(new RemoveGroupCommand(boardId));
801
+ for (const m of [...((_a = g === null || g === void 0 ? void 0 : g.members) !== null && _a !== void 0 ? _a : [])]) {
802
+ const mSpec = specById.get(m);
803
+ if (mSpec)
804
+ unregisters.push(new RegisterWidgetCommand(registryOf(m, boardId, mSpec), 'unregister'));
805
+ if (ctx.boardGroups.has(m))
806
+ removeSubtree(m);
807
+ else
808
+ nodeRemovals.push(new RemoveNodeCommand(m));
809
+ (_b = binders.get(m)) === null || _b === void 0 ? void 0 : _b.dispose();
810
+ binders.delete(m);
811
+ }
812
+ };
813
+ removeSubtree(id);
814
+ const registry = registryOf(id, viewId, spec);
815
+ cmds.push(...nodeRemovals, ...unregisters, new RegisterWidgetCommand(registry, 'unregister'));
816
+ (_c = binders.get(id)) === null || _c === void 0 ? void 0 : _c.dispose();
817
+ binders.delete(id);
818
+ void execCommand(new BatchCommand('Remove section', cmds));
819
+ registry.unregister();
820
+ parentBinder.sync();
821
+ (_d = ctx.apiRef) === null || _d === void 0 ? void 0 : _d.renderNow();
822
+ return;
823
+ }
410
824
  const n = node();
411
- const group = groups.get(viewId);
825
+ const group = ctx.boardGroups.get(viewId);
412
826
  const b = binder();
413
827
  if (!n || !group || !b)
414
828
  return;
@@ -417,16 +831,21 @@ export function createDashboardHandle(ctx) {
417
831
  // computed the survivors passes them in: after a drag-out the tile is
418
832
  // gone from the engine, so planRemoval() would return [] and the
419
833
  // survivors' cells would never commit.
420
- const survivors = (_a = displaced) !== null && _a !== void 0 ? _a : b.planRemoval(id);
421
- const cmds = [...survivors, new RemoveFromGroupCommand(group.id, id), new RemoveNodeCommand(id)];
834
+ const survivors = (_e = displaced) !== null && _e !== void 0 ? _e : b.planRemoval(id);
835
+ // The un-registration is the LAST command so that undo — which runs
836
+ // the batch in reverse — re-registers the spec BEFORE the node and its
837
+ // membership come back and the painter is asked to paint it (D2).
838
+ const registry = registryOf(id, viewId, spec);
839
+ const cmds = [
840
+ ...survivors,
841
+ new RemoveFromGroupCommand(group.id, id),
842
+ new RemoveNodeCommand(id),
843
+ new RegisterWidgetCommand(registry, 'unregister'),
844
+ ];
422
845
  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);
426
- specById.delete(id);
427
- viewOfWidget.delete(id);
846
+ registry.unregister(); // now, for the caller reading the handle next
428
847
  b.sync();
429
- (_b = ctx.apiRef) === null || _b === void 0 ? void 0 : _b.renderNow();
848
+ (_f = ctx.apiRef) === null || _f === void 0 ? void 0 : _f.renderNow();
430
849
  },
431
850
  repaint() {
432
851
  const host = hostOf(id);
@@ -440,40 +859,69 @@ export function createDashboardHandle(ctx) {
440
859
  return handle;
441
860
  }
442
861
  export function dashboard(options) {
443
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
862
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
444
863
  ensureDashboardKitStyles();
445
864
  const columns = (_a = options.columns) !== null && _a !== void 0 ? _a : DEFAULTS.columns;
446
865
  const gap = (_b = options.gap) !== null && _b !== void 0 ? _b : DEFAULTS.gap;
447
866
  const rowHeight = (_c = options.rowHeight) !== null && _c !== void 0 ? _c : DEFAULTS.rowHeight;
448
867
  const boardW = (_d = options.width) !== null && _d !== void 0 ? _d : DEFAULTS.width;
449
868
  const boardH = (_e = options.height) !== null && _e !== void 0 ? _e : DEFAULTS.height;
869
+ // An explicit width is a fixed world; everything else lays out fluid.
870
+ const mode = (_f = options.mode) !== null && _f !== void 0 ? _f : (options.width !== undefined ? 'fixed' : 'fluid');
871
+ const overflow = (_g = options.overflow) !== null && _g !== void 0 ? _g : 'bounded';
872
+ // A fluid board grows (fixed row heights, the board extends); a fixed board
873
+ // fits (its authored height is the picture).
874
+ const sizing = (_h = options.sizing) !== null && _h !== void 0 ? _h : (mode === 'fluid' ? 'grow' : 'fit');
875
+ const layout = (_j = options.layout) !== null && _j !== void 0 ? _j : 'grid';
450
876
  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))) }];
877
+ ? options.views.map((v) => (Object.assign(Object.assign({}, v), { widgets: cloneWidgets(v.widgets) })))
878
+ : [{ id: 'main', widgets: cloneWidgets((_k = options.widgets) !== null && _k !== void 0 ? _k : []) }];
453
879
  for (const v of views)
454
- assignCells(v.widgets, (_g = v.columns) !== null && _g !== void 0 ? _g : columns);
880
+ assignCellsDeep(v.widgets, (_l = v.columns) !== null && _l !== void 0 ? _l : columns);
455
881
  // -- the render spec: one custom-HTML node per widget ----------------------
456
882
  const nodes = [];
457
883
  const specById = new Map();
458
884
  const viewOfWidget = new Map();
459
- for (const v of views) {
460
- for (const w of v.widgets) {
885
+ const boardWidgets = new Map();
886
+ const viewOfBoard = new Map();
887
+ // Recursive walk: a CONTAINER contributes no node (it becomes a group in
888
+ // finalize) but registers like a widget, and its children flatten into the
889
+ // render spec with the container as their board.
890
+ const flatten = (boardId, viewId, widgets) => {
891
+ for (const w of widgets) {
461
892
  specById.set(w.id, w);
462
- viewOfWidget.set(w.id, v.id);
893
+ viewOfWidget.set(w.id, boardId);
894
+ if (w.widgets) {
895
+ boardWidgets.set(w.id, w.widgets);
896
+ viewOfBoard.set(w.id, viewId);
897
+ flatten(w.id, viewId, w.widgets);
898
+ continue;
899
+ }
900
+ pushWidgetNode(w);
901
+ }
902
+ };
903
+ const pushWidgetNode = (w) => {
904
+ var _a, _b;
905
+ {
463
906
  nodes.push({
464
907
  id: w.id,
465
908
  type: 'widget',
466
909
  position: { x: 0, y: 0 },
467
910
  size: { width: 100, height: rowHeight },
468
911
  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 } }),
912
+ metadata: Object.assign(Object.assign(Object.assign(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 } : {})), (w.limits !== undefined ? { widgetLimits: Object.assign({}, w.limits) } : {})), (w.movable === false ? { widgetMovable: false } : {})), (w.resizable === false ? { widgetResizable: false } : {})), { 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
913
  });
471
914
  }
915
+ };
916
+ for (const v of views) {
917
+ boardWidgets.set(v.id, v.widgets);
918
+ viewOfBoard.set(v.id, v.id);
919
+ flatten(v.id, v.id, v.widgets);
472
920
  }
473
921
  // No renderWidget → the built-in renderers draw the declared `kind` from the
474
922
  // developer's own `data` (widgets.ts), unknown kinds landing on the titled
475
923
  // frame they always did.
476
- const renderWidget = (_k = options.renderWidget) !== null && _k !== void 0 ? _k : defaultWidgetRenderer;
924
+ const renderWidget = (_m = options.renderWidget) !== null && _m !== void 0 ? _m : defaultWidgetRenderer;
477
925
  // -- runtime: ONE shared handle over a boxed context -----------------------
478
926
  // The two MUTABLE cells the handle used to close over as free `let`s —
479
927
  // `active` (showView reassigns it) and `apiRef` (finalize sets it) — are boxed
@@ -486,6 +934,9 @@ export function dashboard(options) {
486
934
  binders: new Map(),
487
935
  specById,
488
936
  viewOfWidget,
937
+ boardGroups: new Map(),
938
+ boardWidgets,
939
+ viewOfBoard,
489
940
  hosts: new Map(),
490
941
  renderWidget,
491
942
  columns,
@@ -493,37 +944,44 @@ export function dashboard(options) {
493
944
  rowHeight,
494
945
  boardW,
495
946
  boardH,
947
+ mode,
948
+ overflow,
949
+ layoutOf: new Map(views.map((v) => { var _a; return [v.id, (_a = v.layout) !== null && _a !== void 0 ? _a : layout]; })),
496
950
  optionsBase: options,
497
- active: (_m = (_l = views[0]) === null || _l === void 0 ? void 0 : _l.id) !== null && _m !== void 0 ? _m : 'main',
951
+ active: (_p = (_o = views[0]) === null || _o === void 0 ? void 0 : _o.id) !== null && _p !== void 0 ? _p : 'main',
498
952
  apiRef: null,
953
+ onLayoutChange: options.onLayoutChange,
499
954
  };
500
955
  const { binders, groups } = ctx;
501
956
  const handle = createDashboardHandle(ctx);
502
- return {
503
- nodes,
504
- edges: [],
505
- renderCustomNode: (node, host) => {
957
+ return Object.assign(Object.assign({ nodes, edges: [] }, (mode === 'fluid' ? { renderOptions: { minZoom: 1, maxZoom: 1 } } : {})), { renderCustomNode: (node, host) => {
506
958
  const n = node;
507
959
  const spec = specById.get(n.id);
508
960
  if (!spec)
509
961
  return;
510
962
  ctx.hosts.set(n.id, host);
511
963
  renderWidget(spec, host);
512
- },
513
- get handle() {
964
+ }, get handle() {
514
965
  return handle;
515
- },
516
- finalize: (api) => {
517
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
966
+ }, finalize: (api) => {
967
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
518
968
  const a = api;
519
969
  if (!a)
520
970
  return;
521
971
  ctx.apiRef = a;
522
972
  const model = a.getModel();
973
+ // FLUID: the board starts at the container's box when it can be measured
974
+ // (the binder keeps it there); the authored defaults only fill in for a
975
+ // container with no size yet.
976
+ const box = mode === 'fluid'
977
+ ? { w: ((_a = a.container) === null || _a === void 0 ? void 0 : _a.clientWidth) || 0, h: ((_b = a.container) === null || _b === void 0 ? void 0 : _b.clientHeight) || 0 }
978
+ : { w: 0, h: 0 };
979
+ const viewW = (v) => { var _a; return (mode === 'fluid' && box.w > 0 ? box.w : (_a = v.width) !== null && _a !== void 0 ? _a : boardW); };
980
+ const viewH = (v) => { var _a; return (mode === 'fluid' && box.h > 0 ? box.h : (_a = v.height) !== null && _a !== void 0 ? _a : boardH); };
523
981
  for (const v of views) {
524
982
  // One board per view; the group is a pure LAYOUT CONTAINER, so its
525
983
  // chrome is suppressed (frameChrome) exactly as a dashboard needs.
526
- const g = new GroupModel({ id: v.id, name: (_a = v.name) !== null && _a !== void 0 ? _a : v.id });
984
+ const g = new GroupModel({ id: v.id, name: (_c = v.name) !== null && _c !== void 0 ? _c : v.id });
527
985
  model.addGroup(g);
528
986
  g.setMetadata('frameChrome', 'none');
529
987
  // THE BOARD'S GEOMETRY, PERSISTED. Cells alone do not describe a board:
@@ -533,19 +991,117 @@ export function dashboard(options) {
533
991
  // reload would be a picture of a dashboard rather than a dashboard.
534
992
  // Serializable values only: the binder's callbacks belong to the app.
535
993
  g.setMetadata('dashboardBoard', {
536
- columns: (_b = v.columns) !== null && _b !== void 0 ? _b : columns,
994
+ columns: (_d = v.columns) !== null && _d !== void 0 ? _d : columns,
537
995
  gap,
538
996
  padding: gap,
539
- sizing: (_c = options.sizing) !== null && _c !== void 0 ? _c : 'fit',
997
+ sizing,
540
998
  baseRowHeight: rowHeight,
541
- designHeight: (_d = v.height) !== null && _d !== void 0 ? _d : boardH,
999
+ designHeight: viewH(v),
542
1000
  float: (_e = options.float) !== null && _e !== void 0 ? _e : false,
543
1001
  rtl: (_f = options.rtl) !== null && _f !== void 0 ? _f : false,
1002
+ fluid: mode === 'fluid',
1003
+ overflow,
1004
+ static: (_g = options.static) !== null && _g !== void 0 ? _g : false,
1005
+ layout: (_h = ctx.layoutOf.get(v.id)) !== null && _h !== void 0 ? _h : layout,
544
1006
  });
545
- g.size = { width: (_g = v.width) !== null && _g !== void 0 ? _g : boardW, height: (_h = v.height) !== null && _h !== void 0 ? _h : boardH, depth: 0 };
1007
+ if (((_j = ctx.layoutOf.get(v.id)) !== null && _j !== void 0 ? _j : layout) === 'split' && v.tree !== undefined)
1008
+ g.setMetadata(SPLIT_TREE_KEY, v.tree);
1009
+ g.size = { width: viewW(v), height: viewH(v), depth: 0 };
546
1010
  g.position = { x: v.id === ctx.active ? 0 : OFFSCREEN_X, y: 0 };
547
1011
  groups.set(v.id, g);
548
- for (const w of v.widgets) {
1012
+ ctx.boardGroups.set(v.id, g);
1013
+ mountBoard(v.id, v.id, v.widgets, g);
1014
+ binders.set(v.id, bindView(v, g, (_k = ctx.layoutOf.get(v.id)) !== null && _k !== void 0 ? _k : layout));
1015
+ }
1016
+ // LIVE LAYOUT SWITCH (setLayout): keep the picture, swap the binder.
1017
+ // Either way the outgoing binder's cells are written where the grid
1018
+ // binder reads members' cells (gridItem metadata): grid → split derives
1019
+ // its tree from exactly those, so any stale tree is cleared first;
1020
+ // split → grid rebuilds from them, so the column cache goes too.
1021
+ ctx.rebindView = (viewId, next) => {
1022
+ var _a;
1023
+ const v = views.find((x) => x.id === viewId);
1024
+ const g = groups.get(viewId);
1025
+ const b = binders.get(viewId);
1026
+ if (!v || !g || !b)
1027
+ return;
1028
+ const cells = b.saveLayout().cells;
1029
+ b.dispose();
1030
+ const write = (fn) => (model.runSystemWrite ? model.runSystemWrite(fn) : fn());
1031
+ write(() => {
1032
+ var _a, _b;
1033
+ for (const [id, cell] of cells) {
1034
+ const n = model.getNode(id);
1035
+ if (n)
1036
+ n.setMetadata('gridItem', gridItemFromCell(cell));
1037
+ else
1038
+ (_a = model.getGroup(id)) === null || _a === void 0 ? void 0 : _a.setMetadata('gridItem', gridItemFromCell(cell));
1039
+ }
1040
+ g.setMetadata(SPLIT_TREE_KEY, undefined);
1041
+ g.setMetadata('dashboardLayouts', undefined);
1042
+ const board = (_b = g.getMetadata('dashboardBoard')) !== null && _b !== void 0 ? _b : {};
1043
+ g.setMetadata('dashboardBoard', Object.assign(Object.assign({}, board), { layout: next }));
1044
+ });
1045
+ ctx.layoutOf.set(viewId, next);
1046
+ binders.set(viewId, bindView(v, g, next));
1047
+ (_a = binders.get(viewId)) === null || _a === void 0 ? void 0 : _a.sync();
1048
+ };
1049
+ handle.showView(ctx.active);
1050
+ ctx.rebindContainer = (id) => {
1051
+ var _a;
1052
+ const g = model.getGroup(id);
1053
+ const w = specById.get(id);
1054
+ if (!g || !w || !w.widgets)
1055
+ return;
1056
+ ctx.boardGroups.set(id, g);
1057
+ bindContainer(g, w, (_a = ctx.viewOfBoard.get(id)) !== null && _a !== void 0 ? _a : ctx.active);
1058
+ };
1059
+ (_l = ctx.attachHistory) === null || _l === void 0 ? void 0 : _l.call(ctx);
1060
+ return;
1061
+ /**
1062
+ * Mount one board's widgets into its group — and recurse for CONTAINERS.
1063
+ * A container is a view's construction one level down: a frameless
1064
+ * member group with a slab cell in the PARENT's grid, its own
1065
+ * `dashboardBoard` metadata (so `fromDocument()` rebinds it like any
1066
+ * board), and a second `bindDashboardGrid` on the same canvas — which
1067
+ * registers it as a BinderPeer, so cross-boundary drag, deepest-wins
1068
+ * hit-testing and the height-escalation ratchet all apply unchanged.
1069
+ */
1070
+ function mountBoard(boardId, viewId, widgets, boardGroup) {
1071
+ var _a, _b, _c, _d, _e;
1072
+ for (const w of widgets) {
1073
+ if (w.widgets) {
1074
+ const innerColumns = innerColumnsOf(w);
1075
+ const innerRows = (_a = w.maxRows) !== null && _a !== void 0 ? _a : rowExtentOf(w.widgets);
1076
+ const cg = new GroupModel({ id: w.id, name: (_b = w.title) !== null && _b !== void 0 ? _b : w.id });
1077
+ model.addGroup(cg);
1078
+ cg.setMetadata('frameChrome', 'none');
1079
+ // Slab cells live in GROUP metadata (groups carry no GridItemConfig).
1080
+ cg.setMetadata('gridItem', gridItemFromCell({ x: w.x, y: w.y, w: w.span, h: w.rows }));
1081
+ // The container's own spec fields, persisted ON the group — a
1082
+ // reloaded document has no authored literal to read them from.
1083
+ 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 } : {})));
1084
+ cg.setMetadata('dashboardBoard', {
1085
+ columns: innerColumns,
1086
+ gap,
1087
+ padding: 0,
1088
+ sizing: 'fit',
1089
+ baseRowHeight: rowHeight,
1090
+ // The slab's height is the PARENT's business — 0 hands it over,
1091
+ // which is what makes escalation grow the slab instead of the
1092
+ // container fighting its own frame.
1093
+ designHeight: 0,
1094
+ maxRows: innerRows,
1095
+ float: false,
1096
+ rtl: (_c = options.rtl) !== null && _c !== void 0 ? _c : false,
1097
+ });
1098
+ cg.size = { width: 100, height: rowHeight, depth: 0 };
1099
+ boardGroup.addMember(w.id);
1100
+ ctx.boardGroups.set(w.id, cg);
1101
+ mountBoard(w.id, viewId, w.widgets, cg);
1102
+ bindContainer(cg, w, viewId);
1103
+ continue;
1104
+ }
549
1105
  const n = model.getNode(w.id);
550
1106
  if (!n)
551
1107
  continue;
@@ -557,7 +1113,7 @@ export function dashboard(options) {
557
1113
  // and made toJSON() → dashboard() NOT round-trip (a saved layout
558
1114
  // rebuilt back into its declaration order rather than its cells).
559
1115
  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 }));
1116
+ 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 }));
561
1117
  }
562
1118
  if (w.pinned)
563
1119
  n.setState({ locked: true });
@@ -571,20 +1127,57 @@ export function dashboard(options) {
571
1127
  n.setBehavior({ connectable: false });
572
1128
  for (const p of [...n.getPorts().values()])
573
1129
  n.removePort(p.id);
574
- g.addMember(w.id);
1130
+ boardGroup.addMember(w.id);
575
1131
  }
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) => {
1132
+ }
1133
+ /** Bind a VIEW's board on its group, under the given layout. */
1134
+ function bindView(v, g, viewLayout) {
1135
+ var _a, _b, _c, _d, _e, _f;
1136
+ const common = Object.assign(Object.assign({ gap, padding: gap, rtl: (_a = options.rtl) !== null && _a !== void 0 ? _a : false, fluid: mode === 'fluid', static: (_b = options.static) !== null && _b !== void 0 ? _b : false }, ((_c = options.binder) !== null && _c !== void 0 ? _c : {})), { onGesture: (e) => {
577
1137
  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
- }
1138
+ if (e.type === 'commit')
1139
+ reportChanged();
583
1140
  (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
584
- } })));
1141
+ } });
1142
+ if (viewLayout === 'split') {
1143
+ return bindDashboardSplit(a, g, Object.assign(Object.assign(Object.assign({}, common), { columns: (_d = v.columns) !== null && _d !== void 0 ? _d : columns, baseRowHeight: rowHeight, designHeight: viewH(v) }), (v.tree !== undefined ? { tree: v.tree } : {})));
1144
+ }
1145
+ return bindDashboardGrid(a, g, Object.assign(Object.assign(Object.assign({}, common), { columns: (_e = v.columns) !== null && _e !== void 0 ? _e : columns, sizing, baseRowHeight: rowHeight, designHeight: viewH(v), float: (_f = options.float) !== null && _f !== void 0 ? _f : false, overflow }), (options.responsive ? { responsive: options.responsive } : {})));
585
1146
  }
586
- handle.showView(ctx.active);
587
- },
588
- };
1147
+ /** Bind (or re-bind) a container's inner grid on its group. */
1148
+ function bindContainer(cg, w, viewId) {
1149
+ var _a, _b, _c, _d;
1150
+ binders.set(w.id, bindDashboardGrid(a, cg, {
1151
+ columns: innerColumnsOf(w),
1152
+ gap,
1153
+ padding: 0,
1154
+ sizing: 'fit',
1155
+ baseRowHeight: rowHeight,
1156
+ designHeight: 0,
1157
+ maxRows: (_a = w.maxRows) !== null && _a !== void 0 ? _a : rowExtentOf((_b = w.widgets) !== null && _b !== void 0 ? _b : []),
1158
+ float: false,
1159
+ rtl: (_c = options.rtl) !== null && _c !== void 0 ? _c : false,
1160
+ static: (_d = options.static) !== null && _d !== void 0 ? _d : false,
1161
+ onGesture: (e) => {
1162
+ var _a, _b;
1163
+ if (e.type === 'commit')
1164
+ reportChanged();
1165
+ (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
1166
+ },
1167
+ }));
1168
+ }
1169
+ /**
1170
+ * One reporter for every binder on a view — the view's own and each
1171
+ * container's — and for every API call, undo and redo (D3): the handle's
1172
+ * diff-based `reportChanged`. The payload is the view's FULL NESTED TREE
1173
+ * derived from live membership (handle.toJSON()), so an inner commit
1174
+ * reports the same truth an outer one does, and a tile that crossed a
1175
+ * boundary shows up under its NEW parent.
1176
+ */
1177
+ function reportChanged() {
1178
+ var _a;
1179
+ (_a = ctx.reportChanged) === null || _a === void 0 ? void 0 : _a.call(ctx);
1180
+ }
1181
+ } });
589
1182
  }
590
1183
  //# sourceMappingURL=dashboard.js.map