@grafloria/element 0.4.2 → 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.
@@ -0,0 +1,1042 @@
1
+ /**
2
+ * SPLIT BINDER — a board laid out as a splitter tree (see split-layout.ts),
3
+ * bound to a group the way `bindDashboardGrid` binds a grid: it implements the
4
+ * same `DashboardGridHandle`, so `dashboard()`, `fromDocument()` and the
5
+ * history plumbing drive it without knowing which layout a view runs.
6
+ *
7
+ * THE MODEL (measured on the DevExpress designer, 6 Sep 2026):
8
+ * - the board is always covered: one widget fills it, the next halves the
9
+ * largest one along its longer axis, a removed widget's slot goes to its
10
+ * siblings;
11
+ * - a DIVIDER between two siblings drags as a PERCENTAGE of their group; a
12
+ * widget alone in its group has no divider on that axis;
13
+ * - a DRAG lifts the widget out at once (its siblings take the slot live), an
14
+ * INSERTION LINE on the nearest edge of the widget under the pointer says
15
+ * where it will land, and the drop splits that widget's slot on that side.
16
+ *
17
+ * THE UNDO STORY is one command: `SetSplitTreeCommand` swaps the whole tree
18
+ * (plain JSON on the group as `dashboardTree`). A gesture, a keyboard step, an
19
+ * add and a removal are each one tree write, so one undo is always one thing.
20
+ *
21
+ * Sizing is always 'fit': the tree is a division of the frame. On a fluid
22
+ * board the frame follows the container (both axes); on a fixed board it is
23
+ * the authored size.
24
+ */
25
+ import { __awaiter } from "tslib";
26
+ import { Command } from '@grafloria/engine';
27
+ import { LiveRegionController, registerTool } from '@grafloria/renderer';
28
+ import { cellFromGridItem } from './grid-mapping.js';
29
+ import { addSplitLeaf, cellsFromSplit, cloneSplit, dividersOf, groupRectsOf, insertSplitLeaf, moveSplitDivider, normalizeSplit, pathToLeaf, projectSplit, removeSplitLeaf, splitFromCells, splitLeaves, } from './split-layout.js';
30
+ import { ensureDashboardKitStyles } from './styles.js';
31
+ /** Group metadata key the tree persists under. */
32
+ export const SPLIT_TREE_KEY = 'dashboardTree';
33
+ /**
34
+ * Undoable whole-tree write. Everything a split board does to its layout is
35
+ * one of these — a gesture, a keyboard step, an add, a removal — so one undo
36
+ * is always exactly one thing, and a batch that removes a node can fold the
37
+ * tree-without-it in beside the node removal.
38
+ */
39
+ export class SetSplitTreeCommand extends Command {
40
+ constructor(groupId, before, after) {
41
+ super('Lay out board');
42
+ this.groupId = groupId;
43
+ this.before = before;
44
+ this.after = after;
45
+ }
46
+ apply(context, tree) {
47
+ const diagram = context.diagram;
48
+ const grp = diagram === null || diagram === void 0 ? void 0 : diagram.getGroup(this.groupId);
49
+ if (!grp)
50
+ return;
51
+ grp.setMetadata(SPLIT_TREE_KEY, tree ? cloneSplit(tree) : null);
52
+ }
53
+ execute(context) {
54
+ this.apply(context, this.after);
55
+ }
56
+ undo(context) {
57
+ this.apply(context, this.before);
58
+ }
59
+ serialize() {
60
+ return {
61
+ id: this.id,
62
+ name: this.name,
63
+ timestamp: this.timestamp,
64
+ data: { groupId: this.groupId, before: this.before, after: this.after },
65
+ };
66
+ }
67
+ }
68
+ const LIVE_REGIONS = new WeakMap();
69
+ function liveRegionFor(container) {
70
+ let live = LIVE_REGIONS.get(container);
71
+ if (!live) {
72
+ live = new LiveRegionController(container);
73
+ LIVE_REGIONS.set(container, live);
74
+ }
75
+ return live;
76
+ }
77
+ const DRAG_THRESHOLD = 4;
78
+ /** The divider's hit zone, px — the gap is 10; a 24-px target is what WCAG 2.5.8 asks. */
79
+ const DIVIDER_HIT = 24;
80
+ /** A keyboard divider step, as a fraction of the group. */
81
+ const KEY_STEP = 0.05;
82
+ let binderSeq = 0;
83
+ export function bindDashboardSplit(api, group, options = {}) {
84
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
85
+ const diagram = api.getModel();
86
+ ensureDashboardKitStyles((_a = api.container.ownerDocument) !== null && _a !== void 0 ? _a : document);
87
+ const columns = (_b = options.columns) !== null && _b !== void 0 ? _b : 12;
88
+ const gap = (_c = options.gap) !== null && _c !== void 0 ? _c : 12;
89
+ const padding = (_d = options.padding) !== null && _d !== void 0 ? _d : gap;
90
+ const baseRowHeight = (_e = options.baseRowHeight) !== null && _e !== void 0 ? _e : 110;
91
+ const fluid = options.fluid === true;
92
+ let rtl = options.rtl === true;
93
+ let isStatic = options.static === true;
94
+ let designH = (_h = (_f = options.designHeight) !== null && _f !== void 0 ? _f : (_g = group.size) === null || _g === void 0 ? void 0 : _g.height) !== null && _h !== void 0 ? _h : 0;
95
+ let designW = (_k = (_j = group.size) === null || _j === void 0 ? void 0 : _j.width) !== null && _k !== void 0 ? _k : 0;
96
+ let disposed = false;
97
+ let gesture = null;
98
+ let focusedId;
99
+ const live = liveRegionFor(api.container);
100
+ // -- geometry ---------------------------------------------------------------
101
+ const frame = () => {
102
+ var _a, _b, _c, _d;
103
+ return ({
104
+ x: group.position.x,
105
+ y: group.position.y,
106
+ width: (_b = (_a = group.size) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : designW,
107
+ height: (_d = (_c = group.size) === null || _c === void 0 ? void 0 : _c.height) !== null && _d !== void 0 ? _d : designH,
108
+ });
109
+ };
110
+ const containerBox = () => ({
111
+ w: api.container.clientWidth || 0,
112
+ h: api.container.clientHeight || 0,
113
+ });
114
+ /** FLUID: the frame is the container, both axes. Returns true when it changed. */
115
+ const applyFluidFrame = () => {
116
+ if (!fluid || disposed)
117
+ return false;
118
+ const box = containerBox();
119
+ if (box.w <= 0)
120
+ return false;
121
+ const f = frame();
122
+ const height = box.h > 0 ? box.h : f.height;
123
+ if (Math.abs(f.width - box.w) < 0.5 && Math.abs(f.height - height) < 0.5)
124
+ return false;
125
+ designW = box.w;
126
+ designH = height;
127
+ diagram.runSystemWrite(() => group.setFrame({ x: f.x, y: f.y, width: box.w, height }));
128
+ return true;
129
+ };
130
+ // -- the tree ---------------------------------------------------------------
131
+ const members = () => { var _a; return [...((_a = group.members) !== null && _a !== void 0 ? _a : [])].filter((id) => !!diagram.getNode(id) || !!diagram.getGroup(id)); };
132
+ const readTree = () => {
133
+ const t = group.getMetadata(SPLIT_TREE_KEY);
134
+ return t ? cloneSplit(t) : null;
135
+ };
136
+ const writeTree = (tree) => {
137
+ diagram.runSystemWrite(() => group.setMetadata(SPLIT_TREE_KEY, tree ? cloneSplit(tree) : null));
138
+ };
139
+ /** A member's cell from its persisted gridItem, for the first tree of a grid-authored board. */
140
+ const persistedCell = (id) => {
141
+ var _a, _b, _c;
142
+ const n = diagram.getNode(id);
143
+ const raw = n
144
+ ? (_a = n.getMetadata) === null || _a === void 0 ? void 0 : _a.call(n, 'gridItem')
145
+ : (_b = diagram.getGroup(id)) === null || _b === void 0 ? void 0 : _b.getMetadata('gridItem');
146
+ return (_c = (raw ? cellFromGridItem(raw) : undefined)) !== null && _c !== void 0 ? _c : undefined;
147
+ };
148
+ /**
149
+ * The tree the board should show: the persisted one, reconciled with LIVE
150
+ * membership — a member the tree does not know is added (halving the
151
+ * largest leaf, the DevExpress rule), a leaf whose member is gone is removed.
152
+ * A board with no tree yet gets one from its members' cells (a grid-authored
153
+ * spec keeps its proportions) or, failing that, by adding them in order.
154
+ */
155
+ const reconcile = () => {
156
+ const ids = members();
157
+ let tree = readTree();
158
+ const known = new Set(splitLeaves(tree));
159
+ let changed = false;
160
+ if (!tree && ids.length) {
161
+ const cells = new Map();
162
+ for (const id of ids) {
163
+ const c = persistedCell(id);
164
+ if (c)
165
+ cells.set(id, c);
166
+ }
167
+ tree = cells.size === ids.length ? splitFromCells(cells) : null;
168
+ if (!tree)
169
+ for (const id of ids)
170
+ tree = addSplitLeaf(tree, id, frame(), gap, padding);
171
+ changed = true;
172
+ for (const id of ids)
173
+ known.add(id);
174
+ }
175
+ for (const id of [...known]) {
176
+ if (!ids.includes(id)) {
177
+ tree = removeSplitLeaf(tree, id);
178
+ known.delete(id);
179
+ changed = true;
180
+ }
181
+ }
182
+ for (const id of ids) {
183
+ if (!known.has(id)) {
184
+ tree = addSplitLeaf(tree, id, frame(), gap, padding);
185
+ known.add(id);
186
+ changed = true;
187
+ }
188
+ }
189
+ if (changed)
190
+ writeTree(tree);
191
+ return tree;
192
+ };
193
+ const rectsOf = (tree) => projectSplit(tree, frame(), gap, padding, rtl);
194
+ // -- projection -------------------------------------------------------------
195
+ const htmlLayer = () => api.container.querySelector('.grafloria-html-layer');
196
+ const hostOf = (id) => {
197
+ const esc = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(id) : id.replace(/"/g, '\\"');
198
+ return api.container.querySelector(`.grafloria-node-host[data-node-id="${esc}"]`);
199
+ };
200
+ const writeRect = (id, r) => {
201
+ const node = diagram.getNode(id);
202
+ if (node) {
203
+ if (Math.abs(node.position.x - r.x) > 0.25 ||
204
+ Math.abs(node.position.y - r.y) > 0.25 ||
205
+ Math.abs(node.size.width - r.width) > 0.25 ||
206
+ Math.abs(node.size.height - r.height) > 0.25) {
207
+ diagram.runSystemWrite(() => {
208
+ var _a;
209
+ node.setPosition(r.x, r.y);
210
+ node.setSize(r.width, r.height, (_a = node.size.depth) !== null && _a !== void 0 ? _a : 0);
211
+ });
212
+ }
213
+ return;
214
+ }
215
+ const grp = diagram.getGroup(id);
216
+ if (grp)
217
+ diagram.runSystemWrite(() => grp.setFrame(Object.assign({}, r)));
218
+ };
219
+ /** The tree currently PAINTED — the gesture's live tree while one runs. */
220
+ const paintedTree = () => ((gesture === null || gesture === void 0 ? void 0 : gesture.started) ? gesture.liveTree : readTree());
221
+ const project = (tree = paintedTree()) => {
222
+ const rects = rectsOf(tree);
223
+ for (const [id, r] of rects) {
224
+ if ((gesture === null || gesture === void 0 ? void 0 : gesture.started) && gesture.kind !== 'divider' && id === gesture.id)
225
+ continue; // the ghost follows the pointer
226
+ writeRect(id, r);
227
+ }
228
+ syncDividers(tree);
229
+ syncA11y();
230
+ };
231
+ // -- chrome: dividers + insertion line -------------------------------------
232
+ const dividerEls = [];
233
+ let insertion = null;
234
+ const syncDividers = (tree) => {
235
+ const layer = htmlLayer();
236
+ for (const el of dividerEls)
237
+ el.remove();
238
+ dividerEls.length = 0;
239
+ if (!layer || isStatic || disposed)
240
+ return;
241
+ const divs = dividersOf(tree, frame(), gap, padding, rtl);
242
+ divs.forEach((d, i) => {
243
+ const el = document.createElement('div');
244
+ el.className = `axdb-div axdb-div--${d.dir}`;
245
+ el.setAttribute('data-divider', String(i));
246
+ const grow = Math.max(0, DIVIDER_HIT - (d.dir === 'row' ? d.rect.width : d.rect.height)) / 2;
247
+ const x = d.dir === 'row' ? d.rect.x - grow : d.rect.x;
248
+ const y = d.dir === 'row' ? d.rect.y : d.rect.y - grow;
249
+ const w = d.dir === 'row' ? d.rect.width + 2 * grow : d.rect.width;
250
+ const h = d.dir === 'row' ? d.rect.height : d.rect.height + 2 * grow;
251
+ el.style.left = `${x}px`;
252
+ el.style.top = `${y}px`;
253
+ el.style.width = `${w}px`;
254
+ el.style.height = `${h}px`;
255
+ el.setAttribute('aria-hidden', 'true');
256
+ layer.appendChild(el);
257
+ dividerEls.push(el);
258
+ });
259
+ liveDividers = divs;
260
+ };
261
+ let liveDividers = [];
262
+ const showInsertion = (rect) => {
263
+ const layer = htmlLayer();
264
+ if (!rect || !layer) {
265
+ insertion === null || insertion === void 0 ? void 0 : insertion.remove();
266
+ insertion = null;
267
+ return;
268
+ }
269
+ if (!insertion || insertion.parentElement !== layer) {
270
+ insertion === null || insertion === void 0 ? void 0 : insertion.remove();
271
+ insertion = document.createElement('div');
272
+ insertion.className = 'axdb-ins';
273
+ layer.appendChild(insertion);
274
+ }
275
+ insertion.style.left = `${rect.x}px`;
276
+ insertion.style.top = `${rect.y}px`;
277
+ insertion.style.width = `${rect.width}px`;
278
+ insertion.style.height = `${rect.height}px`;
279
+ };
280
+ /** The insertion line for a drop on `side` of the leaf painted at `r`. */
281
+ const insertionRect = (r, side) => {
282
+ const t = 4;
283
+ switch (side) {
284
+ case 'left':
285
+ return { x: r.x - t / 2, y: r.y, width: t, height: r.height };
286
+ case 'right':
287
+ return { x: r.x + r.width - t / 2, y: r.y, width: t, height: r.height };
288
+ case 'top':
289
+ return { x: r.x, y: r.y - t / 2, width: r.width, height: t };
290
+ default:
291
+ return { x: r.x, y: r.y + r.height - t / 2, width: r.width, height: t };
292
+ }
293
+ };
294
+ /**
295
+ * Where a drop at the world point lands. Near the OUTER edge of a group —
296
+ * within GROUP_BAND px, the outermost such group winning — the target is the
297
+ * whole group ("under all of these columns", DevExpress's group indicator);
298
+ * otherwise it is the widget under the pointer, on its nearest edge.
299
+ */
300
+ const GROUP_BAND = 18;
301
+ const dropTargetAt = (tree, wx, wy, exclude) => {
302
+ var _a;
303
+ let leaf = null;
304
+ for (const [id, r] of rectsOf(tree)) {
305
+ if (id === exclude)
306
+ continue;
307
+ if (wx >= r.x && wx <= r.x + r.width && wy >= r.y && wy <= r.y + r.height) {
308
+ leaf = { id, rect: r };
309
+ break;
310
+ }
311
+ }
312
+ if (!leaf)
313
+ return null;
314
+ const leafPath = (_a = pathToLeaf(tree, leaf.id)) !== null && _a !== void 0 ? _a : [];
315
+ // Ancestors first (the root is path []), outermost wins.
316
+ const groups = groupRectsOf(tree, frame(), gap, padding, rtl)
317
+ .filter((g) => g.path.length < leafPath.length && g.path.every((i, k) => leafPath[k] === i))
318
+ .sort((a, b) => a.path.length - b.path.length);
319
+ for (const g of groups) {
320
+ const r = g.rect;
321
+ const d = { left: wx - r.x, right: r.x + r.width - wx, top: wy - r.y, bottom: r.y + r.height - wy };
322
+ const side = Object.keys(d).reduce((a, b) => (d[b] < d[a] ? b : a));
323
+ if (d[side] <= GROUP_BAND)
324
+ return { path: g.path, side, rect: r };
325
+ }
326
+ const r = leaf.rect;
327
+ const d = { left: wx - r.x, right: r.x + r.width - wx, top: wy - r.y, bottom: r.y + r.height - wy };
328
+ // Normalise by the axis length so a wide, short tile still has a usable top / bottom band.
329
+ const n = { left: d.left / r.width, right: d.right / r.width, top: d.top / r.height, bottom: d.bottom / r.height };
330
+ const side = Object.keys(n).reduce((a, b) => (n[b] < n[a] ? b : a));
331
+ return { id: leaf.id, side, rect: r };
332
+ };
333
+ const targetOf = (t) => 'id' in t ? { id: t.id, side: t.side } : { path: t.path, side: t.side };
334
+ const targetRef = (t) => ('id' in t ? t.id : { path: t.path });
335
+ const targetName = (t) => ('id' in t ? nameOf(t.id) : 'the group');
336
+ const worldInsideBoard = (x, y) => {
337
+ const f = frame();
338
+ return x >= f.x && x <= f.x + f.width && y >= f.y && y <= f.y + f.height;
339
+ };
340
+ // -- a11y -------------------------------------------------------------------
341
+ const nameOf = (id) => {
342
+ var _a, _b, _c, _d;
343
+ const node = diagram.getNode(id);
344
+ const title = (_a = node === null || node === void 0 ? void 0 : node.getMetadata) === null || _a === void 0 ? void 0 : _a.call(node, 'widgetTitle');
345
+ if (typeof title === 'string' && title)
346
+ return title;
347
+ const label = (_c = (_b = node === null || node === void 0 ? void 0 : node.getMetadata) === null || _b === void 0 ? void 0 : _b.call(node, 'widgetData')) === null || _c === void 0 ? void 0 : _c.label;
348
+ if (typeof label === 'string' && label)
349
+ return label;
350
+ const kind = (_d = node === null || node === void 0 ? void 0 : node.getMetadata) === null || _d === void 0 ? void 0 : _d.call(node, 'widgetKind');
351
+ return typeof kind === 'string' && kind ? `${kind} widget` : id;
352
+ };
353
+ const describeSlot = (id, tree) => {
354
+ const r = rectsOf(tree).get(id);
355
+ const f = frame();
356
+ if (!r || f.width <= 0 || f.height <= 0)
357
+ return '';
358
+ return `${Math.round((r.width / f.width) * 100)} percent wide, ${Math.round((r.height / f.height) * 100)} percent tall`;
359
+ };
360
+ const syncA11y = () => {
361
+ var _a, _b;
362
+ if (disposed)
363
+ return;
364
+ const order = splitLeaves(paintedTree()).filter((id) => !!diagram.getNode(id));
365
+ // No corner handles on a split board: size comes from the dividers. A host
366
+ // that carried the grid's handle (a board switched live) sheds it here.
367
+ for (const id of order)
368
+ (_b = (_a = hostOf(id)) === null || _a === void 0 ? void 0 : _a.querySelector(':scope > .axdb-rs')) === null || _b === void 0 ? void 0 : _b.remove();
369
+ if (focusedId && !order.includes(focusedId))
370
+ focusedId = undefined;
371
+ const stop = focusedId !== null && focusedId !== void 0 ? focusedId : order[0];
372
+ order.forEach((id, i) => {
373
+ var _a;
374
+ const node = diagram.getNode(id);
375
+ const host = hostOf(id);
376
+ if (!node || !host)
377
+ return;
378
+ const bits = [nameOf(id), `${i + 1} of ${order.length}`, describeSlot(id, paintedTree())];
379
+ if (((_a = node.state) === null || _a === void 0 ? void 0 : _a.locked) === true)
380
+ bits.push('pinned');
381
+ host.setAttribute('role', 'group');
382
+ host.setAttribute('aria-roledescription', 'dashboard widget');
383
+ host.setAttribute('aria-label', bits.filter(Boolean).join(', '));
384
+ host.setAttribute('tabindex', id === stop ? '0' : '-1');
385
+ });
386
+ };
387
+ // -- commit -----------------------------------------------------------------
388
+ const execCommand = (cmd) => {
389
+ var _a;
390
+ try {
391
+ const r = api.getEngine().commandManager.execute(cmd);
392
+ (_a = r === null || r === void 0 ? void 0 : r.catch) === null || _a === void 0 ? void 0 : _a.call(r, () => undefined);
393
+ return r;
394
+ }
395
+ catch (_b) {
396
+ return undefined;
397
+ }
398
+ };
399
+ const commitTree = (before, after) => {
400
+ const norm = normalizeSplit(after);
401
+ return execCommand(new SetSplitTreeCommand(group.id, before, norm));
402
+ };
403
+ const fire = (e) => {
404
+ var _a;
405
+ try {
406
+ (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, e);
407
+ }
408
+ catch (_b) {
409
+ /* a page hook must not break the board */
410
+ }
411
+ };
412
+ // -- gestures ---------------------------------------------------------------
413
+ const toWorld = (cx, cy) => {
414
+ var _a;
415
+ const rect = api.container.getBoundingClientRect();
416
+ return ((_a = api.viewport) === null || _a === void 0 ? void 0 : _a.clientToWorld) ? api.viewport.clientToWorld(cx, cy, rect) : { x: cx - rect.left, y: cy - rect.top };
417
+ };
418
+ const armEscape = (g) => {
419
+ g.esc = (e) => {
420
+ if (e.key === 'Escape' && gesture === g) {
421
+ e.preventDefault();
422
+ cancelActiveGesture();
423
+ }
424
+ };
425
+ window.addEventListener('keydown', g.esc, true);
426
+ };
427
+ const capturePointer = (g) => {
428
+ var _a, _b;
429
+ if (g.pointerId === null)
430
+ return;
431
+ try {
432
+ (_b = (_a = api.container).setPointerCapture) === null || _b === void 0 ? void 0 : _b.call(_a, g.pointerId);
433
+ }
434
+ catch (_c) {
435
+ /* a pointer that already ended cannot be captured — the release still arrives on the canvas */
436
+ }
437
+ };
438
+ const teardownGesture = (g) => {
439
+ var _a, _b, _c, _d;
440
+ if (g.pointerId !== null) {
441
+ try {
442
+ (_b = (_a = api.container).releasePointerCapture) === null || _b === void 0 ? void 0 : _b.call(_a, g.pointerId);
443
+ }
444
+ catch (_e) {
445
+ /* already released */
446
+ }
447
+ }
448
+ if (g.esc)
449
+ window.removeEventListener('keydown', g.esc, true);
450
+ (_c = g.chip) === null || _c === void 0 ? void 0 : _c.remove();
451
+ showInsertion(null);
452
+ (_d = g.hostEl) === null || _d === void 0 ? void 0 : _d.classList.remove('axdb-ghost', 'axdb-out');
453
+ for (const el of dividerEls)
454
+ el.classList.remove('axdb-active');
455
+ api.container.style.cursor = '';
456
+ };
457
+ const beginMoveVisuals = (g) => {
458
+ var _a;
459
+ g.started = true;
460
+ g.liveTree = removeSplitLeaf(g.startTree, g.id); // the siblings take the slot at once
461
+ g.hostEl = hostOf(g.id);
462
+ (_a = g.hostEl) === null || _a === void 0 ? void 0 : _a.classList.add('axdb-ghost');
463
+ project(g.liveTree);
464
+ api.render();
465
+ };
466
+ const onToolMove = (ev) => {
467
+ var _a, _b;
468
+ const g = gesture;
469
+ if (!g || g.kind === 'palette')
470
+ return;
471
+ if (!g.started) {
472
+ if (Math.abs(ev.screen.x - g.downClient.x) + Math.abs(ev.screen.y - g.downClient.y) < DRAG_THRESHOLD)
473
+ return;
474
+ if (g.kind === 'move')
475
+ beginMoveVisuals(g);
476
+ else {
477
+ g.started = true;
478
+ (_a = dividerEls[liveDividers.indexOf(g.divider)]) === null || _a === void 0 ? void 0 : _a.classList.add('axdb-active');
479
+ }
480
+ capturePointer(g);
481
+ armEscape(g);
482
+ }
483
+ if (g.kind === 'divider' && g.divider) {
484
+ const d = g.divider;
485
+ const delta = d.dir === 'row' ? ev.world.x - g.downWorld.x : ev.world.y - g.downWorld.y;
486
+ const signed = d.dir === 'row' && rtl ? -delta : delta;
487
+ g.liveTree = moveSplitDivider(g.startTree, d.path, d.index, d.length > 0 ? signed / d.length : 0);
488
+ project(g.liveTree);
489
+ api.render();
490
+ return;
491
+ }
492
+ // MOVE: the ghost follows the pointer; the target is the nearest edge under it.
493
+ if (g.node) {
494
+ const x = ev.world.x - g.grab.dx;
495
+ const y = ev.world.y - g.grab.dy;
496
+ diagram.runSystemWrite(() => g.node.setPosition(x, y));
497
+ }
498
+ const inside = worldInsideBoard(ev.world.x, ev.world.y);
499
+ const t = inside ? dropTargetAt(g.liveTree, ev.world.x, ev.world.y, g.id) : null;
500
+ g.target = t ? targetOf(t) : null;
501
+ showInsertion(t ? insertionRect(t.rect, t.side) : null);
502
+ const out = !inside &&
503
+ options.dragOut === 'remove' &&
504
+ (!options.removeZone || options.removeZone({ x: ev.screen.x, y: ev.screen.y }, { x: ev.world.x, y: ev.world.y }));
505
+ g.out = out;
506
+ (_b = g.hostEl) === null || _b === void 0 ? void 0 : _b.classList.toggle('axdb-out', out);
507
+ api.render();
508
+ };
509
+ const onToolUp = () => {
510
+ const g = gesture;
511
+ if (!g || g.kind === 'palette')
512
+ return;
513
+ gesture = null;
514
+ teardownGesture(g);
515
+ if (!g.started) {
516
+ api.render();
517
+ return;
518
+ }
519
+ if (g.kind === 'divider') {
520
+ const changed = JSON.stringify(g.liveTree) !== JSON.stringify(g.startTree);
521
+ if (changed)
522
+ commitTree(g.startTree, g.liveTree);
523
+ project(readTree());
524
+ api.renderNow();
525
+ fire({ type: changed ? 'commit' : 'cancel', kind: 'resize', nodeId: g.id, changed });
526
+ return;
527
+ }
528
+ if (g.out && options.onRemoveRequest) {
529
+ const without = g.liveTree;
530
+ void options.onRemoveRequest(g.id, [new SetSplitTreeCommand(group.id, g.startTree, normalizeSplit(without))]);
531
+ fire({ type: 'remove', kind: 'move', nodeId: g.id, changed: true });
532
+ return;
533
+ }
534
+ if (g.target) {
535
+ const side = rtl && (g.target.side === 'left' || g.target.side === 'right') ? (g.target.side === 'left' ? 'right' : 'left') : g.target.side;
536
+ const after = insertSplitLeaf(g.liveTree, g.id, targetRef(g.target), side);
537
+ const changed = JSON.stringify(normalizeSplit(after)) !== JSON.stringify(normalizeSplit(g.startTree));
538
+ if (changed)
539
+ commitTree(g.startTree, after);
540
+ else
541
+ project(g.startTree);
542
+ project(readTree());
543
+ api.renderNow();
544
+ if (changed)
545
+ live.announce(`${nameOf(g.id)} moved ${side === 'left' || side === 'top' ? 'before' : 'after'} ${targetName(g.target)}`, 'polite', true);
546
+ fire({ type: changed ? 'commit' : 'cancel', kind: 'move', nodeId: g.id, changed });
547
+ return;
548
+ }
549
+ // Released nowhere: the widget snaps home.
550
+ project(g.startTree);
551
+ api.renderNow();
552
+ fire({ type: 'cancel', kind: 'move', nodeId: g.id, changed: false });
553
+ };
554
+ const cancelActiveGesture = () => {
555
+ const g = gesture;
556
+ if (!g)
557
+ return;
558
+ gesture = null;
559
+ teardownGesture(g);
560
+ if (g.kind === 'palette') {
561
+ api.renderNow();
562
+ fire({ type: 'cancel', kind: 'palette', nodeId: g.id, changed: false });
563
+ return;
564
+ }
565
+ project(g.startTree);
566
+ api.renderNow();
567
+ if (g.started)
568
+ fire({ type: 'cancel', kind: g.kind === 'divider' ? 'resize' : 'move', nodeId: g.id, changed: false });
569
+ };
570
+ const tool = {
571
+ id: `dashboard-split:${group.id}:${++binderSeq}`,
572
+ priority: 2,
573
+ hitTest(ev, hit) {
574
+ var _a;
575
+ if (disposed)
576
+ return false;
577
+ if (gesture)
578
+ return true;
579
+ if (hit.node)
580
+ return ((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(hit.node.id);
581
+ return worldInsideBoard(ev.world.x, ev.world.y);
582
+ },
583
+ onPointerDown(ev, hit) {
584
+ var _a, _b, _c, _d, _e, _f, _g;
585
+ if (gesture)
586
+ return;
587
+ const target = ((_b = (_a = ev.source) === null || _a === void 0 ? void 0 : _a.target) !== null && _b !== void 0 ? _b : null);
588
+ const dividerEl = (_c = target === null || target === void 0 ? void 0 : target.closest) === null || _c === void 0 ? void 0 : _c.call(target, '.axdb-div');
589
+ if (dividerEl && !isStatic) {
590
+ const d = liveDividers[Number(dividerEl.getAttribute('data-divider'))];
591
+ if (!d)
592
+ return;
593
+ const tree = readTree();
594
+ gesture = {
595
+ kind: 'divider',
596
+ id: `divider:${d.path.join('.')}:${d.index}`,
597
+ node: null,
598
+ started: false,
599
+ downClient: { x: ev.screen.x, y: ev.screen.y },
600
+ downWorld: { x: ev.world.x, y: ev.world.y },
601
+ grab: { dx: 0, dy: 0 },
602
+ startTree: tree,
603
+ liveTree: tree,
604
+ divider: d,
605
+ target: null,
606
+ out: false,
607
+ chip: null,
608
+ esc: null,
609
+ hostEl: null,
610
+ pointerId: typeof PointerEvent !== 'undefined' && ev.source instanceof PointerEvent ? ev.source.pointerId : null,
611
+ };
612
+ api.container.style.cursor = d.dir === 'row' ? 'col-resize' : 'row-resize';
613
+ return;
614
+ }
615
+ if (!hit.node) {
616
+ (_e = (_d = diagram).clearSelection) === null || _e === void 0 ? void 0 : _e.call(_d);
617
+ api.render();
618
+ return;
619
+ }
620
+ const node = diagram.getNode(hit.node.id);
621
+ if (!node || ((_f = node.state) === null || _f === void 0 ? void 0 : _f.locked) === true || isStatic)
622
+ return;
623
+ if (((_g = node.getMetadata) === null || _g === void 0 ? void 0 : _g.call(node, 'widgetMovable')) === false)
624
+ return;
625
+ const tree = readTree();
626
+ gesture = {
627
+ kind: 'move',
628
+ id: node.id,
629
+ node,
630
+ started: false,
631
+ downClient: { x: ev.screen.x, y: ev.screen.y },
632
+ downWorld: { x: ev.world.x, y: ev.world.y },
633
+ grab: { dx: ev.world.x - node.position.x, dy: ev.world.y - node.position.y },
634
+ startTree: tree,
635
+ liveTree: tree,
636
+ divider: null,
637
+ target: null,
638
+ out: false,
639
+ chip: null,
640
+ esc: null,
641
+ hostEl: null,
642
+ pointerId: typeof PointerEvent !== 'undefined' && ev.source instanceof PointerEvent ? ev.source.pointerId : null,
643
+ };
644
+ },
645
+ onPointerMove(ev) {
646
+ onToolMove(ev);
647
+ },
648
+ onPointerUp() {
649
+ onToolUp();
650
+ },
651
+ onCancel() {
652
+ cancelActiveGesture();
653
+ },
654
+ };
655
+ const unregisterTool = registerTool(tool);
656
+ // -- palette drag-in --------------------------------------------------------
657
+ const beginPaletteDrag = (node, spec, event) => {
658
+ var _a;
659
+ if (disposed || gesture || isStatic)
660
+ return;
661
+ const chip = (_a = spec.chip) !== null && _a !== void 0 ? _a : null;
662
+ if (chip) {
663
+ chip.classList.add('axdb-drag-chip');
664
+ document.body.appendChild(chip);
665
+ chip.style.left = `${event.clientX + 6}px`;
666
+ chip.style.top = `${event.clientY + 6}px`;
667
+ }
668
+ const tree = readTree();
669
+ const g = {
670
+ kind: 'palette',
671
+ id: node.id,
672
+ node,
673
+ started: true,
674
+ downClient: { x: event.clientX, y: event.clientY },
675
+ downWorld: { x: 0, y: 0 },
676
+ grab: { dx: 0, dy: 0 },
677
+ startTree: tree,
678
+ liveTree: tree,
679
+ divider: null,
680
+ target: null,
681
+ out: false,
682
+ chip,
683
+ esc: null,
684
+ hostEl: null,
685
+ pointerId: null,
686
+ };
687
+ gesture = g;
688
+ armEscape(g);
689
+ const detach = () => {
690
+ window.removeEventListener('pointermove', onMove, true);
691
+ window.removeEventListener('pointerup', onUp, true);
692
+ };
693
+ const onMove = (e) => {
694
+ if (gesture !== g)
695
+ return detach();
696
+ if (chip) {
697
+ chip.style.left = `${e.clientX + 6}px`;
698
+ chip.style.top = `${e.clientY + 6}px`;
699
+ }
700
+ const w = toWorld(e.clientX, e.clientY);
701
+ const t = worldInsideBoard(w.x, w.y) ? dropTargetAt(g.liveTree, w.x, w.y) : null;
702
+ g.target = t ? targetOf(t) : null;
703
+ showInsertion(t ? insertionRect(t.rect, t.side) : null);
704
+ chip === null || chip === void 0 ? void 0 : chip.classList.toggle('axdb-out', !t && !!g.liveTree);
705
+ api.render();
706
+ };
707
+ const onUp = () => {
708
+ var _a, _b;
709
+ detach();
710
+ if (gesture !== g)
711
+ return;
712
+ gesture = null;
713
+ teardownGesture(g);
714
+ const tree = g.liveTree;
715
+ const after = g.target
716
+ ? insertSplitLeaf(tree, node.id, targetRef(g.target), g.target.side)
717
+ : tree
718
+ ? null
719
+ : addSplitLeaf(null, node.id, frame(), gap, padding);
720
+ if (!after) {
721
+ api.renderNow();
722
+ fire({ type: 'cancel', kind: 'palette', nodeId: node.id, changed: false });
723
+ return;
724
+ }
725
+ const cells = cellsFromSplit(after, columns, rowsGuess());
726
+ const cell = (_a = cells.get(node.id)) !== null && _a !== void 0 ? _a : { x: 0, y: 0, w: Math.max(1, spec.w), h: Math.max(1, spec.h) };
727
+ const displaced = [new SetSplitTreeCommand(group.id, tree, normalizeSplit(after))];
728
+ void ((_b = options.onDropIn) === null || _b === void 0 ? void 0 : _b.call(options, node, cell, displaced));
729
+ fire({ type: 'drop-in', kind: 'palette', nodeId: node.id, changed: true });
730
+ };
731
+ window.addEventListener('pointermove', onMove, true);
732
+ window.addEventListener('pointerup', onUp, true);
733
+ };
734
+ // -- keyboard ---------------------------------------------------------------
735
+ const memberHostAt = (target) => {
736
+ var _a, _b, _c;
737
+ const host = (_a = target === null || target === void 0 ? void 0 : target.closest) === null || _a === void 0 ? void 0 : _a.call(target, '.grafloria-node-host');
738
+ if (!host)
739
+ return null;
740
+ const id = (_b = host.getAttribute('data-node-id')) !== null && _b !== void 0 ? _b : '';
741
+ if (!((_c = group.members) !== null && _c !== void 0 ? _c : new Set()).has(id) || !diagram.getNode(id))
742
+ return null;
743
+ return { id, host };
744
+ };
745
+ const onFocusIn = (e) => {
746
+ const hit = memberHostAt(e.target);
747
+ if (!hit || disposed)
748
+ return;
749
+ if (focusedId !== hit.id) {
750
+ focusedId = hit.id;
751
+ syncA11y();
752
+ }
753
+ };
754
+ /** The divider on `side` of leaf `id`: the nearest ancestor group running that way. */
755
+ const dividerBeside = (tree, id, side) => {
756
+ const path = pathToLeaf(tree, id);
757
+ if (!path || !tree)
758
+ return null;
759
+ const dir = side === 'left' || side === 'right' ? 'row' : 'column';
760
+ for (let depth = path.length - 1; depth >= 0; depth--) {
761
+ const groupPath = path.slice(0, depth);
762
+ let node = tree;
763
+ for (const i of groupPath)
764
+ node = node.children[i];
765
+ const grp = node;
766
+ if (grp.dir !== dir)
767
+ continue;
768
+ const i = path[depth];
769
+ const after = side === 'right' || side === 'bottom';
770
+ const index = after ? i : i - 1;
771
+ if (index >= 0 && index < grp.children.length - 1)
772
+ return { path: groupPath, index };
773
+ return null;
774
+ }
775
+ return null;
776
+ };
777
+ const onKey = (e) => {
778
+ var _a, _b;
779
+ if (disposed || gesture)
780
+ return;
781
+ const hit = memberHostAt(e.target);
782
+ const order = splitLeaves(readTree()).filter((id) => !!diagram.getNode(id) && !!hostOf(id));
783
+ if (!hit) {
784
+ const el = e.target;
785
+ const onRoot = !!el && ((_a = el.tagName) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'svg' && ((_b = el.classList) === null || _b === void 0 ? void 0 : _b.contains('grafloria-diagram'));
786
+ if (onRoot && (e.key.startsWith('Arrow') || e.key === 'Enter' || e.key === ' ')) {
787
+ const target = focusedId && order.includes(focusedId) ? focusedId : order[0];
788
+ if (target && handle.focusWidget(target)) {
789
+ e.preventDefault();
790
+ e.stopPropagation();
791
+ }
792
+ }
793
+ return;
794
+ }
795
+ if (e.key === 'Home' || e.key === 'End') {
796
+ const id = e.key === 'Home' ? order[0] : order[order.length - 1];
797
+ if (id)
798
+ handle.focusWidget(id);
799
+ e.preventDefault();
800
+ e.stopPropagation();
801
+ return;
802
+ }
803
+ const side = { ArrowLeft: 'left', ArrowRight: 'right', ArrowUp: 'top', ArrowDown: 'bottom' }[e.key];
804
+ if (!side)
805
+ return;
806
+ e.preventDefault();
807
+ e.stopPropagation();
808
+ if (!e.shiftKey) {
809
+ // Plain arrows walk the widgets in reading order.
810
+ const i = order.indexOf(hit.id);
811
+ const next = order[side === 'left' || side === 'top' ? i - 1 : i + 1];
812
+ if (next)
813
+ handle.focusWidget(next);
814
+ return;
815
+ }
816
+ if (isStatic)
817
+ return;
818
+ const name = nameOf(hit.id);
819
+ const tree = readTree();
820
+ const d = dividerBeside(tree, hit.id, side);
821
+ if (!d || !tree) {
822
+ live.announceError(`${name} has no divider on its ${side === 'top' ? 'top' : side === 'bottom' ? 'bottom' : side} side`);
823
+ return;
824
+ }
825
+ // Shift+arrow grows the widget towards that side: the divider moves away from it.
826
+ const grows = side === 'right' || side === 'bottom' ? KEY_STEP : -KEY_STEP;
827
+ const after = moveSplitDivider(tree, d.path, d.index, grows);
828
+ const r = commitTree(tree, after);
829
+ void Promise.resolve(r).then(() => {
830
+ var _a, _b;
831
+ if (disposed)
832
+ return;
833
+ project(readTree());
834
+ api.renderNow();
835
+ live.announce(`${name} resized to ${describeSlot(hit.id, readTree())}`, 'polite', true);
836
+ syncA11y();
837
+ (_b = (_a = hostOf(hit.id)) === null || _a === void 0 ? void 0 : _a.focus) === null || _b === void 0 ? void 0 : _b.call(_a, { preventScroll: true });
838
+ });
839
+ };
840
+ api.container.addEventListener('focusin', onFocusIn);
841
+ api.container.addEventListener('keydown', onKey);
842
+ // -- observers --------------------------------------------------------------
843
+ const containerObserver = fluid && typeof ResizeObserver !== 'undefined'
844
+ ? new ResizeObserver(() => {
845
+ if (disposed)
846
+ return;
847
+ if (applyFluidFrame()) {
848
+ project();
849
+ api.renderNow();
850
+ }
851
+ })
852
+ : null;
853
+ containerObserver === null || containerObserver === void 0 ? void 0 : containerObserver.observe(api.container);
854
+ // A repainted host loses its a11y attributes: put them back when hosts land.
855
+ const hostObserver = typeof MutationObserver !== 'undefined'
856
+ ? new MutationObserver((records) => {
857
+ if (disposed)
858
+ return;
859
+ const landed = records.some((r) => Array.from(r.addedNodes).some((n) => { var _a; return (_a = n.classList) === null || _a === void 0 ? void 0 : _a.contains('grafloria-node-host'); }));
860
+ if (landed)
861
+ syncA11y();
862
+ })
863
+ : null;
864
+ const layerEl = htmlLayer();
865
+ if (layerEl && hostObserver)
866
+ hostObserver.observe(layerEl, { childList: true });
867
+ // -- the handle -------------------------------------------------------------
868
+ const rowsGuess = () => Math.max(1, Math.round(frame().height / (baseRowHeight + gap)));
869
+ const handle = {
870
+ sync() {
871
+ if (disposed)
872
+ return;
873
+ applyFluidFrame();
874
+ const tree = reconcile();
875
+ project(tree);
876
+ api.renderNow();
877
+ },
878
+ setSizing() {
879
+ /* a split board is always 'fit' — the tree divides the frame */
880
+ },
881
+ getSizing: () => 'fit',
882
+ setFloat() {
883
+ /* no gravity in a tree */
884
+ },
885
+ getFloat: () => false,
886
+ setColumns: () => false,
887
+ getColumns: () => columns,
888
+ setRtl(on) {
889
+ if (on === rtl)
890
+ return;
891
+ rtl = on;
892
+ project();
893
+ api.renderNow();
894
+ },
895
+ getRtl: () => rtl,
896
+ setStatic(on) {
897
+ if (on === isStatic)
898
+ return;
899
+ cancelActiveGesture();
900
+ isStatic = on;
901
+ project();
902
+ api.renderNow();
903
+ },
904
+ getStatic: () => isStatic,
905
+ focusWidget(id) {
906
+ var _a, _b, _c;
907
+ if (!((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(id) || !diagram.getNode(id))
908
+ return false;
909
+ focusedId = id;
910
+ syncA11y();
911
+ (_c = (_b = hostOf(id)) === null || _b === void 0 ? void 0 : _b.focus) === null || _c === void 0 ? void 0 : _c.call(_b, { preventScroll: true });
912
+ return true;
913
+ },
914
+ getFocusedWidget: () => focusedId,
915
+ saveLayout() {
916
+ return { columns, cells: cellsFromSplit(readTree(), columns, rowsGuess()) };
917
+ },
918
+ metrics() {
919
+ const f = frame();
920
+ return {
921
+ columns,
922
+ maxColumns: columns,
923
+ rtl,
924
+ responsive: false,
925
+ fluid,
926
+ static: isStatic,
927
+ capacity: undefined,
928
+ gap,
929
+ padding,
930
+ sizing: 'fit',
931
+ rows: rowsGuess(),
932
+ rowHeight: rowsGuess() > 0 ? (f.height - 2 * padding - (rowsGuess() - 1) * gap) / rowsGuess() : 0,
933
+ columnUnit: columns > 0 ? (f.width - 2 * padding - (columns - 1) * gap) / columns : 0,
934
+ boardHeight: f.height,
935
+ frame: f,
936
+ };
937
+ },
938
+ willItFit: () => true,
939
+ cellOf(id) {
940
+ return cellsFromSplit(readTree(), columns, rowsGuess()).get(id);
941
+ },
942
+ cellRectOf(id) {
943
+ // The PAINTED tree: mid-gesture a caller sees what the user sees — and
944
+ // the lifted widget, which has no slot while in flight, is its ghost.
945
+ const r = rectsOf(paintedTree()).get(id);
946
+ if (r)
947
+ return r;
948
+ const n = diagram.getNode(id);
949
+ return n ? { x: n.position.x, y: n.position.y, width: n.size.width, height: n.size.height } : undefined;
950
+ },
951
+ planRemoval(id) {
952
+ const tree = readTree();
953
+ if (!pathToLeaf(tree, id))
954
+ return [];
955
+ return [new SetSplitTreeCommand(group.id, tree, normalizeSplit(removeSplitLeaf(tree, id)))];
956
+ },
957
+ moveTo(id, x, y) {
958
+ return __awaiter(this, void 0, void 0, function* () {
959
+ // A cell address on a split board: drop `id` on the side of the leaf whose
960
+ // cell holds (x, y) that the address points at.
961
+ const tree = readTree();
962
+ if (!pathToLeaf(tree, id))
963
+ return false;
964
+ const cells = cellsFromSplit(tree, columns, rowsGuess());
965
+ let targetId;
966
+ for (const [tid, c] of cells) {
967
+ if (tid !== id && x >= c.x && x < c.x + c.w && y >= c.y && y < c.y + c.h)
968
+ targetId = tid;
969
+ }
970
+ if (!targetId)
971
+ return false;
972
+ const mine = cells.get(id);
973
+ const side = mine.x + mine.w <= cells.get(targetId).x ? 'left' : mine.x >= cells.get(targetId).x + cells.get(targetId).w ? 'right' : mine.y < cells.get(targetId).y ? 'top' : 'bottom';
974
+ const after = insertSplitLeaf(tree, id, targetId, side);
975
+ yield commitTree(tree, after);
976
+ project(readTree());
977
+ api.renderNow();
978
+ return true;
979
+ });
980
+ },
981
+ resizeTo(id, w, h) {
982
+ return __awaiter(this, void 0, void 0, function* () {
983
+ // Grow / shrink towards the right, then the bottom, by whole-cell fractions.
984
+ const tree = readTree();
985
+ const cells = cellsFromSplit(tree, columns, rowsGuess());
986
+ const mine = cells.get(id);
987
+ if (!tree || !mine)
988
+ return false;
989
+ let next = tree;
990
+ const dw = w - mine.w;
991
+ const dh = h - mine.h;
992
+ const dx = dividerBeside(next, id, 'right');
993
+ if (dw && dx)
994
+ next = moveSplitDivider(next, dx.path, dx.index, dw / columns);
995
+ const dy = dividerBeside(next, id, 'bottom');
996
+ if (dh && dy)
997
+ next = moveSplitDivider(next, dy.path, dy.index, dh / rowsGuess());
998
+ if (JSON.stringify(next) === JSON.stringify(tree))
999
+ return false;
1000
+ yield commitTree(tree, next);
1001
+ project(readTree());
1002
+ api.renderNow();
1003
+ return true;
1004
+ });
1005
+ },
1006
+ beginPaletteDrag,
1007
+ getSplitTree: () => readTree(),
1008
+ setSplitTree(tree) {
1009
+ return __awaiter(this, void 0, void 0, function* () {
1010
+ const before = readTree();
1011
+ yield commitTree(before, tree);
1012
+ project(readTree());
1013
+ api.renderNow();
1014
+ });
1015
+ },
1016
+ dispose() {
1017
+ if (disposed)
1018
+ return;
1019
+ cancelActiveGesture();
1020
+ disposed = true;
1021
+ unregisterTool();
1022
+ api.container.removeEventListener('focusin', onFocusIn);
1023
+ api.container.removeEventListener('keydown', onKey);
1024
+ containerObserver === null || containerObserver === void 0 ? void 0 : containerObserver.disconnect();
1025
+ hostObserver === null || hostObserver === void 0 ? void 0 : hostObserver.disconnect();
1026
+ for (const el of dividerEls)
1027
+ el.remove();
1028
+ dividerEls.length = 0;
1029
+ insertion === null || insertion === void 0 ? void 0 : insertion.remove();
1030
+ insertion = null;
1031
+ api.container.style.cursor = '';
1032
+ },
1033
+ };
1034
+ // Boot: an authored tree wins, then the persisted one, then the members' cells.
1035
+ if (options.tree !== undefined)
1036
+ writeTree(options.tree);
1037
+ applyFluidFrame();
1038
+ project(reconcile());
1039
+ api.renderNow();
1040
+ return handle;
1041
+ }
1042
+ //# sourceMappingURL=split-binder.js.map