@grafloria/element 0.4.2 → 0.4.4

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,
@@ -157,13 +257,24 @@ function assignCellsDeep(widgets, columns) {
157
257
  }
158
258
  /** Flow widgets that declared no cell: left-to-right, wrapping at `columns`. */
159
259
  function assignCells(widgets, columns) {
160
- var _a, _b;
260
+ var _a, _b, _c;
161
261
  let x = 0;
162
262
  let y = 0;
163
263
  let rowMax = 0;
164
264
  for (const w of widgets) {
165
- const span = Math.max(1, Math.min(columns, (_a = w.span) !== null && _a !== void 0 ? _a : 3));
166
- 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);
167
278
  if (w.x === undefined || w.y === undefined) {
168
279
  if (x + span > columns) {
169
280
  x = 0;
@@ -187,6 +298,58 @@ function assignCells(widgets, columns) {
187
298
  export function createDashboardHandle(ctx) {
188
299
  const { views, groups, binders, specById, viewOfWidget } = ctx;
189
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
+ };
190
353
  /**
191
354
  * A board's widgets as a NESTED tree, derived from LIVE membership — not the
192
355
  * authored arrays. A cross-boundary drag moves membership through commands
@@ -195,7 +358,7 @@ export function createDashboardHandle(ctx) {
195
358
  * tile under the container it is actually in, in every one of those states.
196
359
  */
197
360
  const treeOf = (boardId) => {
198
- var _a;
361
+ var _a, _b, _c, _d;
199
362
  const g = ctx.boardGroups.get(boardId);
200
363
  const b = binders.get(boardId);
201
364
  if (!g)
@@ -213,12 +376,97 @@ export function createDashboardHandle(ctx) {
213
376
  entries.push(Object.assign(Object.assign(Object.assign({ id: memberId }, (spec !== null && spec !== void 0 ? spec : {})), at), { widgets: treeOf(memberId) }));
214
377
  }
215
378
  else if (spec) {
216
- entries.push(Object.assign(Object.assign({}, spec), at));
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);
217
389
  }
218
390
  }
219
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); });
220
392
  return entries;
221
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
+ };
222
470
  const execCommand = (cmd) => {
223
471
  var _a, _b, _c, _d;
224
472
  try {
@@ -239,7 +487,7 @@ export function createDashboardHandle(ctx) {
239
487
  return ctx.active;
240
488
  },
241
489
  showView(id) {
242
- var _a, _b, _c, _d, _e, _f;
490
+ var _a, _b, _c;
243
491
  if (!groups.has(id))
244
492
  return;
245
493
  ctx.active = id;
@@ -251,9 +499,7 @@ export function createDashboardHandle(ctx) {
251
499
  }
252
500
  (_b = binders.get(id)) === null || _b === void 0 ? void 0 : _b.sync();
253
501
  (_c = ctx.apiRef) === null || _c === void 0 ? void 0 : _c.renderNow();
254
- const g = groups.get(id);
255
- const gs = (_d = g.size) !== null && _d !== void 0 ? _d : { width: ctx.boardW, height: ctx.boardH };
256
- (_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));
257
503
  },
258
504
  widget(id) {
259
505
  return makeWidgetHandle(id);
@@ -263,18 +509,33 @@ export function createDashboardHandle(ctx) {
263
509
  const v = views.find((x) => x.id === (viewId !== null && viewId !== void 0 ? viewId : ctx.active));
264
510
  return ((_a = v === null || v === void 0 ? void 0 : v.widgets) !== null && _a !== void 0 ? _a : []).map((w) => makeWidgetHandle(w.id)).filter(Boolean);
265
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'; },
266
525
  setSizing(mode) {
267
526
  var _a;
268
527
  for (const b of binders.values())
269
528
  b.setSizing(mode);
529
+ clampCamera();
270
530
  (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.renderNow();
271
531
  },
272
- 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'); },
273
533
  setFloat(on) {
274
534
  var _a;
275
535
  for (const b of binders.values())
276
536
  b.setFloat(on);
277
537
  (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.renderNow();
538
+ reportChanged(); // gravity re-packs when float turns off
278
539
  },
279
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); },
280
541
  setColumns(n, layout, viewId) {
@@ -283,6 +544,7 @@ export function createDashboardHandle(ctx) {
283
544
  for (const b of targets)
284
545
  b === null || b === void 0 ? void 0 : b.setColumns(n, layout);
285
546
  (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.renderNow();
547
+ reportChanged(); // derived state, never a command — report it here
286
548
  },
287
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; },
288
550
  setRtl(on) {
@@ -292,8 +554,15 @@ export function createDashboardHandle(ctx) {
292
554
  (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.renderNow();
293
555
  },
294
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); },
295
564
  addWidget(spec, viewId) {
296
- var _a, _b, _c, _d, _e;
565
+ var _a, _b, _c, _d, _e, _f;
297
566
  const vid = viewId !== null && viewId !== void 0 ? viewId : ctx.active;
298
567
  // `vid` may name a view OR a container — both are boards with a group,
299
568
  // a binder and an authored array.
@@ -306,17 +575,22 @@ export function createDashboardHandle(ctx) {
306
575
  // REGISTER FIRST: a custom node mounts exactly once, and the painter
307
576
  // returns early for an id the spec does not know — so the widget must be
308
577
  // known before the node reaches the model, or it paints blank forever.
309
- arr.push(w);
310
- specById.set(w.id, w);
311
- 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();
312
586
  const existing = model.getNode(w.id);
313
587
  const node = existing !== null && existing !== void 0 ? existing : buildWidgetNode(w, ctx.rowHeight);
314
588
  if (w.pinned)
315
589
  node.setState({ locked: true });
316
- // ONE undoable step (see AddWidgetCommand for why this cannot be a batch).
317
- execCommand(existing ? new AddToGroupCommand(group.id, w.id) : new AddWidgetCommand(node, group.id));
318
- (_d = binders.get(vid)) === null || _d === void 0 ? void 0 : _d.sync();
319
- (_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();
320
594
  return makeWidgetHandle(w.id);
321
595
  },
322
596
  refresh() {
@@ -326,12 +600,9 @@ export function createDashboardHandle(ctx) {
326
600
  (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.renderNow();
327
601
  },
328
602
  fit(viewId) {
329
- var _a, _b, _c;
330
603
  const g = groups.get(viewId !== null && viewId !== void 0 ? viewId : ctx.active);
331
- if (!g)
332
- return;
333
- const gs = (_a = g.size) !== null && _a !== void 0 ? _a : { width: ctx.boardW, height: ctx.boardH };
334
- (_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);
335
606
  },
336
607
  metrics(viewId) {
337
608
  var _a;
@@ -371,34 +642,47 @@ export function createDashboardHandle(ctx) {
371
642
  // layout its user authored — and the view's `columns` is that count, so
372
643
  // feeding this straight back into dashboard() rebuilds the wide board.
373
644
  const savedViews = views.map((v) => {
374
- var _a, _b, _c, _d;
645
+ var _a, _b, _c, _d, _e;
375
646
  const saved = (_a = binders.get(v.id)) === null || _a === void 0 ? void 0 : _a.saveLayout();
376
647
  const live = treeOf(v.id);
377
- return Object.assign(Object.assign(Object.assign({}, v), (saved ? { columns: saved.columns } : {})), {
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 } : {})), {
378
652
  // Live membership when the view is mounted; the authored tree before
379
653
  // 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))) });
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))) });
381
655
  });
382
656
  // Board options come off the LIVE board wherever the handle can see it —
383
657
  // `sizing` and `float` are the two a user changes from the toolbar, and
384
658
  // reading them from the authored literal would restore the board they
385
659
  // started with rather than the one they are looking at.
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 });
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 });
387
661
  },
388
662
  dispose() {
389
- var _a, _b, _c;
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 = [];
390
667
  for (const b of binders.values())
391
668
  b.dispose();
392
669
  binders.clear();
393
670
  // The groups finalize() created are ours to clean up — leaving them
394
671
  // behind made a rebuild stack a second set of boards on the first.
395
- 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
+ }
396
680
  // Deepest first: a container group removed after its parent is an orphan
397
681
  // the model never saw inside a board.
398
682
  for (const id of [...ctx.boardGroups.keys()].reverse())
399
- (_b = model === null || model === void 0 ? void 0 : model.removeGroup) === null || _b === void 0 ? void 0 : _b.call(model, id);
683
+ (_d = model === null || model === void 0 ? void 0 : model.removeGroup) === null || _d === void 0 ? void 0 : _d.call(model, id);
400
684
  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);
685
+ (_e = model === null || model === void 0 ? void 0 : model.removeGroup) === null || _e === void 0 ? void 0 : _e.call(model, id);
402
686
  groups.clear();
403
687
  ctx.boardGroups.clear();
404
688
  ctx.hosts.clear();
@@ -449,12 +733,20 @@ export function createDashboardHandle(ctx) {
449
733
  const n = node();
450
734
  if (!n)
451
735
  return;
452
- 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));
453
744
  // Re-sync so the ENGINE's locked flag (never pushed, drags refused)
454
745
  // and the hidden corner handle take effect on this frame, not the next
455
746
  // gesture.
456
747
  (_b = binder()) === null || _b === void 0 ? void 0 : _b.sync();
457
748
  (_c = ctx.apiRef) === null || _c === void 0 ? void 0 : _c.renderNow();
749
+ reportChanged();
458
750
  },
459
751
  bringToFront() {
460
752
  var _a;
@@ -478,12 +770,55 @@ export function createDashboardHandle(ctx) {
478
770
  ctx.renderWidget(spec, host);
479
771
  },
480
772
  remove(displaced) {
481
- var _a, _b;
773
+ var _a, _b, _c, _d, _e, _f;
482
774
  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');
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();
487
822
  return;
488
823
  }
489
824
  const n = node();
@@ -496,19 +831,21 @@ export function createDashboardHandle(ctx) {
496
831
  // computed the survivors passes them in: after a drag-out the tile is
497
832
  // gone from the engine, so planRemoval() would return [] and the
498
833
  // survivors' cells would never commit.
499
- const survivors = (_a = displaced) !== null && _a !== void 0 ? _a : b.planRemoval(id);
500
- 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
+ ];
501
845
  void execCommand(new BatchCommand('Remove widget', cmds));
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
- }
508
- specById.delete(id);
509
- viewOfWidget.delete(id);
846
+ registry.unregister(); // now, for the caller reading the handle next
510
847
  b.sync();
511
- (_b = ctx.apiRef) === null || _b === void 0 ? void 0 : _b.renderNow();
848
+ (_f = ctx.apiRef) === null || _f === void 0 ? void 0 : _f.renderNow();
512
849
  },
513
850
  repaint() {
514
851
  const host = hostOf(id);
@@ -522,18 +859,25 @@ export function createDashboardHandle(ctx) {
522
859
  return handle;
523
860
  }
524
861
  export function dashboard(options) {
525
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
862
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
526
863
  ensureDashboardKitStyles();
527
864
  const columns = (_a = options.columns) !== null && _a !== void 0 ? _a : DEFAULTS.columns;
528
865
  const gap = (_b = options.gap) !== null && _b !== void 0 ? _b : DEFAULTS.gap;
529
866
  const rowHeight = (_c = options.rowHeight) !== null && _c !== void 0 ? _c : DEFAULTS.rowHeight;
530
867
  const boardW = (_d = options.width) !== null && _d !== void 0 ? _d : DEFAULTS.width;
531
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';
532
876
  const views = options.views
533
877
  ? 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 : []) }];
878
+ : [{ id: 'main', widgets: cloneWidgets((_k = options.widgets) !== null && _k !== void 0 ? _k : []) }];
535
879
  for (const v of views)
536
- assignCellsDeep(v.widgets, (_g = v.columns) !== null && _g !== void 0 ? _g : columns);
880
+ assignCellsDeep(v.widgets, (_l = v.columns) !== null && _l !== void 0 ? _l : columns);
537
881
  // -- the render spec: one custom-HTML node per widget ----------------------
538
882
  const nodes = [];
539
883
  const specById = new Map();
@@ -565,7 +909,7 @@ export function dashboard(options) {
565
909
  position: { x: 0, y: 0 },
566
910
  size: { width: 100, height: rowHeight },
567
911
  custom: true,
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 } }),
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 } }),
569
913
  });
570
914
  }
571
915
  };
@@ -577,7 +921,7 @@ export function dashboard(options) {
577
921
  // No renderWidget → the built-in renderers draw the declared `kind` from the
578
922
  // developer's own `data` (widgets.ts), unknown kinds landing on the titled
579
923
  // frame they always did.
580
- const renderWidget = (_h = options.renderWidget) !== null && _h !== void 0 ? _h : defaultWidgetRenderer;
924
+ const renderWidget = (_m = options.renderWidget) !== null && _m !== void 0 ? _m : defaultWidgetRenderer;
581
925
  // -- runtime: ONE shared handle over a boxed context -----------------------
582
926
  // The two MUTABLE cells the handle used to close over as free `let`s —
583
927
  // `active` (showView reassigns it) and `apiRef` (finalize sets it) — are boxed
@@ -600,37 +944,44 @@ export function dashboard(options) {
600
944
  rowHeight,
601
945
  boardW,
602
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]; })),
603
950
  optionsBase: options,
604
- active: (_k = (_j = views[0]) === null || _j === void 0 ? void 0 : _j.id) !== null && _k !== void 0 ? _k : 'main',
951
+ active: (_p = (_o = views[0]) === null || _o === void 0 ? void 0 : _o.id) !== null && _p !== void 0 ? _p : 'main',
605
952
  apiRef: null,
953
+ onLayoutChange: options.onLayoutChange,
606
954
  };
607
955
  const { binders, groups } = ctx;
608
956
  const handle = createDashboardHandle(ctx);
609
- return {
610
- nodes,
611
- edges: [],
612
- renderCustomNode: (node, host) => {
957
+ return Object.assign(Object.assign({ nodes, edges: [] }, (mode === 'fluid' ? { renderOptions: { minZoom: 1, maxZoom: 1 } } : {})), { renderCustomNode: (node, host) => {
613
958
  const n = node;
614
959
  const spec = specById.get(n.id);
615
960
  if (!spec)
616
961
  return;
617
962
  ctx.hosts.set(n.id, host);
618
963
  renderWidget(spec, host);
619
- },
620
- get handle() {
964
+ }, get handle() {
621
965
  return handle;
622
- },
623
- finalize: (api) => {
624
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
966
+ }, finalize: (api) => {
967
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
625
968
  const a = api;
626
969
  if (!a)
627
970
  return;
628
971
  ctx.apiRef = a;
629
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); };
630
981
  for (const v of views) {
631
982
  // One board per view; the group is a pure LAYOUT CONTAINER, so its
632
983
  // chrome is suppressed (frameChrome) exactly as a dashboard needs.
633
- 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 });
634
985
  model.addGroup(g);
635
986
  g.setMetadata('frameChrome', 'none');
636
987
  // THE BOARD'S GEOMETRY, PERSISTED. Cells alone do not describe a board:
@@ -640,28 +991,72 @@ export function dashboard(options) {
640
991
  // reload would be a picture of a dashboard rather than a dashboard.
641
992
  // Serializable values only: the binder's callbacks belong to the app.
642
993
  g.setMetadata('dashboardBoard', {
643
- columns: (_b = v.columns) !== null && _b !== void 0 ? _b : columns,
994
+ columns: (_d = v.columns) !== null && _d !== void 0 ? _d : columns,
644
995
  gap,
645
996
  padding: gap,
646
- sizing: (_c = options.sizing) !== null && _c !== void 0 ? _c : 'fit',
997
+ sizing,
647
998
  baseRowHeight: rowHeight,
648
- designHeight: (_d = v.height) !== null && _d !== void 0 ? _d : boardH,
999
+ designHeight: viewH(v),
649
1000
  float: (_e = options.float) !== null && _e !== void 0 ? _e : false,
650
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,
651
1006
  });
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 };
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 };
653
1010
  g.position = { x: v.id === ctx.active ? 0 : OFFSCREEN_X, y: 0 };
654
1011
  groups.set(v.id, g);
655
1012
  ctx.boardGroups.set(v.id, g);
656
1013
  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
- } })));
1014
+ binders.set(v.id, bindView(v, g, (_k = ctx.layoutOf.get(v.id)) !== null && _k !== void 0 ? _k : layout));
663
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
+ };
664
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);
665
1060
  return;
666
1061
  /**
667
1062
  * Mount one board's widgets into its group — and recurse for CONTAINERS.
@@ -673,7 +1068,7 @@ export function dashboard(options) {
673
1068
  * hit-testing and the height-escalation ratchet all apply unchanged.
674
1069
  */
675
1070
  function mountBoard(boardId, viewId, widgets, boardGroup) {
676
- var _a, _b, _c, _d, _e, _f;
1071
+ var _a, _b, _c, _d, _e;
677
1072
  for (const w of widgets) {
678
1073
  if (w.widgets) {
679
1074
  const innerColumns = innerColumnsOf(w);
@@ -704,23 +1099,7 @@ export function dashboard(options) {
704
1099
  boardGroup.addMember(w.id);
705
1100
  ctx.boardGroups.set(w.id, cg);
706
1101
  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
- }));
1102
+ bindContainer(cg, w, viewId);
724
1103
  continue;
725
1104
  }
726
1105
  const n = model.getNode(w.id);
@@ -734,7 +1113,7 @@ export function dashboard(options) {
734
1113
  // and made toJSON() → dashboard() NOT round-trip (a saved layout
735
1114
  // rebuilt back into its declaration order rather than its cells).
736
1115
  if (w.x !== undefined && w.y !== undefined) {
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 }));
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 }));
738
1117
  }
739
1118
  if (w.pinned)
740
1119
  n.setState({ locked: true });
@@ -751,22 +1130,54 @@ export function dashboard(options) {
751
1130
  boardGroup.addMember(w.id);
752
1131
  }
753
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) => {
1137
+ var _a, _b;
1138
+ if (e.type === 'commit')
1139
+ reportChanged();
1140
+ (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
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 } : {})));
1146
+ }
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
+ }
754
1169
  /**
755
1170
  * 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.)
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.
761
1176
  */
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);
1177
+ function reportChanged() {
1178
+ var _a;
1179
+ (_a = ctx.reportChanged) === null || _a === void 0 ? void 0 : _a.call(ctx);
768
1180
  }
769
- },
770
- };
1181
+ } });
771
1182
  }
772
1183
  //# sourceMappingURL=dashboard.js.map