@grafloria/element 0.4.54 → 0.4.56

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.
@@ -51,51 +51,21 @@
51
51
  */
52
52
  import { __awaiter } from "tslib";
53
53
  import { AddToGroupCommand, BatchCommand, GridPackEngine, RemoveFromGroupCommand, } from '@grafloria/engine';
54
- import { LiveRegionController, registerTool } from '@grafloria/renderer';
54
+ import { registerTool } from '@grafloria/renderer';
55
55
  import { buildCommitCommands, cellFromGridItem, cellToRect, columnUnitFor, gridItemFromCell, pointToCell, rowHeightFor, sizeToSpan, } from './grid-mapping.js';
56
56
  import { ensureDashboardKitStyles } from './styles.js';
57
- import { captionOfGroup, captionPainted, captionPassThrough, captionKey, paintCaptionBand, sectionCaptionReserve, sizeCaptionBand } from './caption.js';
57
+ import { captionOfGroup, captionPassThrough, sectionCaptionReserve } from './caption.js';
58
58
  import { TAB_STRIP_HEIGHT } from './tabs.js';
59
59
  import { BESIDE_BAND, resolve as resolveZone, resolveTabZone } from './zones.js';
60
60
  import { SequenceCommand, SetGroupCellCommand, tileCommands } from './commit.js';
61
+ import { createProjection } from './project.js';
62
+ import { cursorFor, edgesNear, NO_EDGES, anyEdge } from './edges.js';
63
+ import { DRAG_HANDLE_CLASS, dragHandleSelector, gripHostOf, normalizeDragHandle, pressOnDragHandle, sameDragHandle } from './grip.js';
64
+ import { createChrome, edgeGripFor, isTabsGroup } from './chrome.js';
65
+ import { createKeyboard, describeCell, liveRegionFor } from './keyboard.js';
66
+ export { EDGE_GRIP, anyEdge } from './edges.js';
67
+ export { GRIP_CLASS, DRAG_HANDLE_CLASS, CAPTION_BAND, normalizeDragHandle, dragHandleSelector, gripOf, syncGrip, gripHostOf, pressOnDragHandle } from './grip.js';
61
68
  export { SequenceCommand, SetGroupCellCommand } from './commit.js';
62
- /** The class of the painted grip element (a child of the node host). */
63
- export const GRIP_CLASS = 'axdb-grip';
64
- /** The container class that turns the caption strip's grip dots on. */
65
- export const DRAG_HANDLE_CLASS = 'axdb-drag-handle';
66
- /** A `dragHandle` value with its defaults filled in — the form the handle reports. */
67
- export const normalizeDragHandle = (v) => {
68
- var _a, _b;
69
- return typeof v === 'object' && v !== null && v.grip
70
- ? { grip: true, position: (_a = v.position) !== null && _a !== void 0 ? _a : 'left', placement: (_b = v.placement) !== null && _b !== void 0 ? _b : 'inside' }
71
- : v === true ? true : typeof v === 'string' && v.length > 0 ? v : false;
72
- };
73
- /** The selector a `dragHandle` value names — `null` when the whole card is the handle. */
74
- export const dragHandleSelector = (v) => v === true ? '.axdb-widget-h' : typeof v === 'string' ? v : typeof v === 'object' ? '.' + GRIP_CLASS : null;
75
- /** The grip config of a value, or null when it paints no grip. */
76
- export const gripOf = (v) => (typeof v === 'object' ? v : null);
77
- const sameDragHandle = (a, b) => JSON.stringify(a) === JSON.stringify(b);
78
- /**
79
- * Paint (or remove) the grip on one host, and stamp the host with the grip's
80
- * placement so the header can make room for it. `movable` false = no grip.
81
- */
82
- export function syncGrip(host, cfg, movable) {
83
- var _a, _b, _c, _d, _e;
84
- const existing = host.querySelector(':scope > .' + GRIP_CLASS);
85
- host.classList.remove('axdb-gp-inside', 'axdb-gp-outside', 'axdb-gp-left', 'axdb-gp-center', 'axdb-gp-right');
86
- if (!cfg || !movable) {
87
- existing === null || existing === void 0 ? void 0 : existing.remove();
88
- return;
89
- }
90
- const el = (_a = existing) !== null && _a !== void 0 ? _a : host.ownerDocument.createElement('div');
91
- if (!existing) {
92
- el.setAttribute('aria-hidden', 'true');
93
- el.setAttribute('title', 'Drag');
94
- host.appendChild(el);
95
- }
96
- el.className = `${GRIP_CLASS} ${GRIP_CLASS}--${(_b = cfg.position) !== null && _b !== void 0 ? _b : 'left'} ${GRIP_CLASS}--${(_c = cfg.placement) !== null && _c !== void 0 ? _c : 'inside'}`;
97
- host.classList.add(`axdb-gp-${(_d = cfg.placement) !== null && _d !== void 0 ? _d : 'inside'}`, `axdb-gp-${(_e = cfg.position) !== null && _e !== void 0 ? _e : 'left'}`);
98
- }
99
69
  /** The member host a press on a painted grip belongs to (the grip may sit OUTSIDE the host's box). */
100
70
  /**
101
71
  * Is this press OURS? The renderer's tool registry is PAGE-GLOBAL: every
@@ -131,33 +101,6 @@ export function ownsPress(container, diagram, ev, hit) {
131
101
  }
132
102
  return true;
133
103
  }
134
- export function gripHostOf(target) {
135
- var _a, _b;
136
- const grip = (_a = target === null || target === void 0 ? void 0 : target.closest) === null || _a === void 0 ? void 0 : _a.call(target, '.' + GRIP_CLASS);
137
- return (_b = grip === null || grip === void 0 ? void 0 : grip.closest('.grafloria-node-host')) !== null && _b !== void 0 ? _b : null;
138
- }
139
- /**
140
- * Did a press land on the drag handle? A press INSIDE the handle element
141
- * always does. The default handle is a CAPTION BAR the DevExpress way: the
142
- * strip from the card's top edge down to the header's bottom, padding
143
- * included — so the pointer need not hit the header's text to grab the tile.
144
- */
145
- export function pressOnDragHandle(sel, target, hostEl, clientX, clientY) {
146
- var _a, _b;
147
- const grip = (_b = (_a = target === null || target === void 0 ? void 0 : target.closest) === null || _a === void 0 ? void 0 : _a.call(target, sel)) !== null && _b !== void 0 ? _b : null;
148
- if (grip && (!hostEl || hostEl.contains(grip)))
149
- return true;
150
- if (sel !== '.axdb-widget-h' || !hostEl)
151
- return false;
152
- const hr = hostEl.getBoundingClientRect();
153
- const header = hostEl.querySelector('.axdb-widget-h');
154
- // A host painting its own content has no kit header: its caption is the top
155
- // band of the card, so `dragHandle: true` still means something there.
156
- const bottom = header ? header.getBoundingClientRect().bottom : hr.top + CAPTION_BAND;
157
- return clientX >= hr.left && clientX <= hr.right && clientY >= hr.top && clientY <= bottom;
158
- }
159
- /** The caption strip of a host with no kit header, px from the card's top. */
160
- export const CAPTION_BAND = 28;
161
104
  /** A torn-out page never arrives shorter than this: a strip with no room under it is not a group. */
162
105
  export const TEAR_OUT_MIN_ROWS = 2;
163
106
  /**
@@ -204,58 +147,9 @@ export function registerBoardPeer(container, peer) {
204
147
  s.delete(peer);
205
148
  };
206
149
  }
207
- /**
208
- * ONE aria-live region per canvas, shared by every board on it — the
209
- * renderer's own controller (coalescing, de-duplicating), so a dashboard
210
- * announces through the same channel a diagram does. WeakMap: the region
211
- * follows the container out of memory.
212
- */
213
- const LIVE_REGIONS = new WeakMap();
214
- function liveRegionFor(container) {
215
- let live = LIVE_REGIONS.get(container);
216
- if (!live) {
217
- live = new LiveRegionController(container);
218
- LIVE_REGIONS.set(container, live);
219
- }
220
- return live;
221
- }
222
- function directionName(dx, dy) {
223
- return dx < 0 ? 'left' : dx > 0 ? 'right' : dy < 0 ? 'up' : 'down';
224
- }
225
- /** "column 4, row 2, 3 by 1" — the cell as a person hears it (1-based). */
226
- function describeCell(c) {
227
- return `column ${c.x + 1}, row ${c.y + 1}, ${c.w} by ${c.h}`;
228
- }
229
150
  const DRAG_THRESHOLD = 4;
230
- const GLIDE_OFF_DELAY = 400;
231
- /** A press this close (CSS px) to a tile's border takes that edge for a resize. */
232
- export const EDGE_GRIP = 7;
233
- const NO_EDGES = { n: false, e: false, s: false, w: false };
234
- /** Which of a host's edges a client point is within EDGE_GRIP of (none when outside). */
235
- function edgesNear(host, cx, cy) {
236
- const r = host.getBoundingClientRect();
237
- if (cx < r.left - 2 || cx > r.right + 2 || cy < r.top - 2 || cy > r.bottom + 2)
238
- return NO_EDGES;
239
- return {
240
- n: cy - r.top <= EDGE_GRIP,
241
- s: r.bottom - cy <= EDGE_GRIP,
242
- w: cx - r.left <= EDGE_GRIP,
243
- e: r.right - cx <= EDGE_GRIP,
244
- };
245
- }
246
- export const anyEdge = (E) => E.n || E.e || E.s || E.w;
247
- /** The resize cursor for a set of edges ('' when none). */
248
- function cursorFor(E) {
249
- const v = E.n || E.s;
250
- const h = E.e || E.w;
251
- if (v && h)
252
- return (E.n && E.w) || (E.s && E.e) ? 'nwse-resize' : 'nesw-resize';
253
- if (v)
254
- return 'ns-resize';
255
- if (h)
256
- return 'ew-resize';
257
- return '';
258
- }
151
+ /** The node a gesture holds — node-only paths (the pixel ghost, a resize, a strip drop, a removal) ask through this. */
152
+ const nodeOf = (g) => g.entity;
259
153
  let binderSeq = 0;
260
154
  export function bindDashboardGrid(api, group, options = {}) {
261
155
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
@@ -442,13 +336,10 @@ export function bindDashboardGrid(api, group, options = {}) {
442
336
  let disposed = false;
443
337
  /** Reentrancy guard: our own derived frame writes must not re-project. */
444
338
  let writing = false;
445
- let placeholder = null;
446
339
  /** Foreign tile currently adopted from another binder's gesture. */
447
340
  let adoptedGhostId = null;
448
341
  /** The tab page this board is currently tearing out of a container, if any. */
449
342
  let tearing = null;
450
- let glideTimer = null;
451
- let ghostTimer = null;
452
343
  /**
453
344
  * OUR OWN CAPTION RESERVE: a section carrying a caption gives the band's
454
345
  * pixels up at the top of its frame, so no child may take them — and the
@@ -574,172 +465,57 @@ export function bindDashboardGrid(api, group, options = {}) {
574
465
  grp.setMetadata('gridItem', gridItemFromCell(cell));
575
466
  }
576
467
  };
577
- // -- projection: cells -> pixels -------------------------------------------
578
- /** Write one member's projected rect (derived state → system write). */
579
- const writeRect = (id, r) => {
580
- const node = diagram.getNode(id);
581
- if (node) {
582
- if (Math.abs(node.position.x - r.x) > 0.25 ||
583
- Math.abs(node.position.y - r.y) > 0.25 ||
584
- Math.abs(node.size.width - r.width) > 0.25 ||
585
- Math.abs(node.size.height - r.height) > 0.25) {
586
- diagram.runSystemWrite(() => {
587
- var _a;
588
- node.setPosition(r.x, r.y);
589
- node.setSize(r.width, r.height, (_a = node.size.depth) !== null && _a !== void 0 ? _a : 0);
590
- });
591
- }
592
- return;
593
- }
594
- const grp = diagram.getGroup(id);
595
- if (grp) {
596
- const p = grp.position;
597
- const s = sizeOf(grp);
598
- if (Math.abs(p.x - r.x) > 0.25 ||
599
- Math.abs(p.y - r.y) > 0.25 ||
600
- Math.abs(s.width - r.width) > 0.25 ||
601
- Math.abs(s.height - r.height) > 0.25) {
602
- diagram.runSystemWrite(() => grp.setFrame(Object.assign({}, r)));
603
- }
604
- }
605
- };
606
- /** Enforce the board-frame height the sizing mode implies. */
607
- const enforceBoardHeight = () => {
608
- if (designH <= 0)
609
- return;
610
- const r = rows();
611
- // FIT keeps its design height, full stop — the user's rule: "the board
612
- // stays the same and the widgets change size so all of them fit". Only
613
- // overflow:'scroll' lets the frame EXTEND to hold the rows at the floor
614
- // height (and the canvas pan). Grow extends at the base row height.
615
- const target = sizing === 'fit'
616
- ? overflow === 'scroll'
617
- ? Math.max(designH, 2 * padding + r * minRowHeight + (r - 1) * gap)
618
- : designH
619
- : Math.max(designH, 2 * padding + r * baseRowHeight + (r - 1) * gap);
620
- const f = frame();
621
- if (Math.abs(f.height - target) > 0.5) {
468
+ // -- the board, as its modules see it (tile first, step 4b-i) --------------
469
+ const ctx = {
470
+ api,
471
+ group,
472
+ diagram,
473
+ options,
474
+ gap,
475
+ padding,
476
+ baseRowHeight,
477
+ minRowHeight,
478
+ overflow,
479
+ engine: () => engine,
480
+ frame,
481
+ geom,
482
+ rows,
483
+ sizing: () => sizing,
484
+ designH: () => designH,
485
+ rtl: () => rtl,
486
+ isStatic: () => isStatic,
487
+ disposed: () => disposed,
488
+ htmlLayer,
489
+ hostOf,
490
+ memberEntity,
491
+ sizeOf,
492
+ // A NODE ghost follows the hand by pixels and gets the placeholder; a GROUP ghost (a container adopted here) is projected to its cell like any tile, carried.
493
+ ghostId: () => (adoptedGhostId && !diagram.getGroup(adoptedGhostId) ? adoptedGhostId : (gesture === null || gesture === void 0 ? void 0 : gesture.started) && gesture.subject === 'node' ? gesture.id : null),
494
+ write: (fn) => {
622
495
  writing = true;
623
496
  try {
624
- diagram.runSystemWrite(() => group.setFrame({ x: f.x, y: f.y, width: f.width, height: target }));
497
+ fn();
625
498
  }
626
499
  finally {
627
500
  writing = false;
628
501
  }
629
- }
630
- };
631
- /** Project every member from its engine cells (the ghost is exempt). */
632
- const project = () => {
633
- enforceBoardHeight();
634
- writing = true;
635
- try {
636
- const f = frame();
637
- const g = geom();
638
- const r = rows();
639
- for (const item of engine.getItems()) {
640
- if ((gesture === null || gesture === void 0 ? void 0 : gesture.started) && item.id === gesture.id)
641
- continue; // the ghost
642
- if (item.id === adoptedGhostId)
643
- continue; // a ghost adopted from another binder
644
- writeRect(item.id, cellToRect(item, f, g, r));
645
- }
646
- }
647
- finally {
648
- writing = false;
649
- }
650
- syncPlaceholder();
651
- // The section overlays are projected chrome like the placeholder: they
652
- // follow every frame write, not only a rebuild. Painted at bind time only,
653
- // a section two levels down kept the geometry of its parent's placeholder
654
- // frame (100 × 34) after the view board had laid the parent out (the kit
655
- // lab's L47, 2026-09-08).
656
- syncSlabs();
657
- };
658
- // -- placeholder / ghost chrome --------------------------------------------
659
- /** The placeholder exists ONLY while a gesture is live — so at any moment
660
- * the DOM holds at most one `.axdb-ph` per active gesture, not one idle
661
- * div per bound board. */
662
- const syncPlaceholder = () => {
663
- const ghostId = adoptedGhostId !== null && adoptedGhostId !== void 0 ? adoptedGhostId : ((gesture === null || gesture === void 0 ? void 0 : gesture.started) && !gesture.removedFromBoard ? gesture.id : null);
664
- const item = ghostId ? engine.getItem(ghostId) : undefined;
665
- const live = !!item;
666
- if (!live || !item) {
667
- placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
668
- placeholder = null;
669
- return;
670
- }
671
- const layer = htmlLayer();
672
- if (!layer)
673
- return;
674
- if (!placeholder || placeholder.parentElement !== layer) {
675
- placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
676
- placeholder = document.createElement('div');
677
- placeholder.className = 'axdb-ph';
678
- layer.prepend(placeholder);
679
- }
680
- const r = cellToRect(item, frame(), geom(), rows());
681
- placeholder.style.display = 'block';
682
- placeholder.style.left = `${r.x}px`;
683
- placeholder.style.top = `${r.y}px`;
684
- placeholder.style.width = `${r.width}px`;
685
- placeholder.style.height = `${r.height}px`;
686
- };
687
- const armGlide = () => {
688
- var _a;
689
- (_a = htmlLayer()) === null || _a === void 0 ? void 0 : _a.classList.add('axdb-glide');
690
- if (glideTimer)
691
- clearTimeout(glideTimer);
692
- };
693
- const disarmGlideSoon = () => {
694
- if (glideTimer)
695
- clearTimeout(glideTimer);
696
- glideTimer = setTimeout(() => { var _a; return (_a = htmlLayer()) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-glide'); }, GLIDE_OFF_DELAY);
697
- };
698
- /** The host whose ghost class the pending timer will lift. */
699
- let ghostHost = null;
700
- /**
701
- * Lift a pending ghost NOW. One timer serves every host, so superseding it
702
- * (a new gesture within 60 ms of the last drop — ③ then ④ in the
703
- * nested-containers checks — or a dispose on a rebind) used to clear the
704
- * timer and leave the previous tile lifted for good: a permanent drop
705
- * shadow the visual gate finally caught.
706
- */
707
- const flushGhost = () => {
708
- if (ghostTimer)
709
- clearTimeout(ghostTimer);
710
- ghostTimer = null;
711
- ghostHost === null || ghostHost === void 0 ? void 0 : ghostHost.classList.remove('axdb-ghost', 'axdb-out');
712
- ghostHost = null;
713
- };
714
- const setGhost = (id, on) => {
715
- const host = hostOf(id);
716
- if (!host)
717
- return;
718
- if (ghostHost && ghostHost !== host)
719
- flushGhost();
720
- if (on) {
721
- if (ghostTimer)
722
- clearTimeout(ghostTimer);
723
- ghostTimer = null;
724
- host.classList.add('axdb-ghost');
725
- host.classList.remove('axdb-out');
726
- ghostHost = host;
727
- }
728
- else {
729
- host.classList.remove('axdb-out');
730
- // Keep transition-exemption through the drop write so the snap into the
731
- // placeholder is INSTANT (gridstack-style), then let glides resume.
732
- if (ghostTimer)
733
- clearTimeout(ghostTimer);
734
- ghostHost = host;
735
- ghostTimer = setTimeout(() => {
736
- host.classList.remove('axdb-ghost');
737
- ghostTimer = null;
738
- if (ghostHost === host)
739
- ghostHost = null;
740
- }, 60);
741
- }
502
+ },
742
503
  };
504
+ // -- projection: cells -> pixels (project.ts) ------------------------------
505
+ const projection = createProjection(ctx, { afterProject: () => syncSlabs() });
506
+ const { writeRect, enforceBoardHeight, project, syncPlaceholder, hidePlaceholder, armGlide, disarmGlideSoon, flushGhost, setGhost } = projection;
507
+ // -- chrome: slabs, frames, captions, handles, cursors (chrome.ts) ---------
508
+ const chrome = createChrome(ctx, {
509
+ selectedId: () => selectedId,
510
+ syncA11y: (only) => syncA11y(only),
511
+ grabbing: () => !!slabGesture,
512
+ gestureRunning: () => !!gesture,
513
+ memberGroupAt: (x, y) => memberGroupAt(x, y),
514
+ slabEdgesNear: (grp, x, y) => slabEdgesNear(grp, x, y),
515
+ dragHandle: () => dragHandle,
516
+ wantHandles,
517
+ });
518
+ const { syncSlabs, syncHandles, setCarried, flushCarried, showRefusal, ensureStaticGuard } = chrome;
743
519
  // -- resize handles ---------------------------------------------------------
744
520
  /** The accessible name of a widget: its title, else its kind, else its id. */
745
521
  const nameOf = (node) => {
@@ -813,82 +589,6 @@ export function bindDashboardGrid(api, group, options = {}) {
813
589
  host.classList.toggle('axdb-selected', id === selectedId);
814
590
  }
815
591
  };
816
- /**
817
- * The chrome on every member host: the painted grip (or none) and the corner
818
- * resize handle — ONE host lookup per member, which the host observer's
819
- * budget counts (a repaint of one host must cost that host's lookup, not a
820
- * second pass).
821
- */
822
- const syncHandles = (only) => {
823
- var _a, _b, _c, _d, _e;
824
- syncA11y(only);
825
- if (disposed)
826
- return;
827
- ensureStaticGuard();
828
- syncSlabs();
829
- const grip = gripOf(dragHandle);
830
- for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
831
- if (only && !only.has(id))
832
- continue;
833
- const node = diagram.getNode(id);
834
- if (!node)
835
- continue;
836
- const host = hostOf(id);
837
- if (!host)
838
- continue;
839
- syncGrip(host, grip, ((_b = node.state) === null || _b === void 0 ? void 0 : _b.locked) !== true && !isStatic && ((_c = node.getMetadata) === null || _c === void 0 ? void 0 : _c.call(node, 'widgetMovable')) !== false);
840
- if (!wantHandles)
841
- continue;
842
- const existing = host.querySelector(':scope > .axdb-rs');
843
- if (((_d = node.state) === null || _d === void 0 ? void 0 : _d.locked) === true || isStatic || ((_e = node.getMetadata) === null || _e === void 0 ? void 0 : _e.call(node, 'widgetResizable')) === false) {
844
- existing === null || existing === void 0 ? void 0 : existing.remove();
845
- continue;
846
- }
847
- const rs = existing !== null && existing !== void 0 ? existing : document.createElement('div');
848
- if (!existing) {
849
- rs.className = 'axdb-rs';
850
- rs.setAttribute('title', 'Resize');
851
- host.appendChild(rs);
852
- }
853
- // The grab corner mirrors with the board: bottom-right LTR, bottom-left
854
- // RTL — the same corner the tile actually grows from in each direction.
855
- rs.classList.toggle('axdb-rs--rtl', rtl);
856
- }
857
- };
858
- /**
859
- * Only a HOST-LEVEL change matters: a host arriving (a mount) or a host's
860
- * own children changing (a repaint that wiped the injected handle). A
861
- * chart's internal churn — most of what a live dashboard mutates — targets
862
- * deeper nodes and is ignored, and the hosts the records DO name are the
863
- * only ones re-synced. This was members × repaints `querySelector` calls
864
- * per wave (9,216 at 96 widgets, review D10); it is now proportional to the
865
- * hosts that actually changed.
866
- */
867
- const hostObserver = new MutationObserver((records) => {
868
- const touched = new Set();
869
- const noteHost = (el) => {
870
- var _a;
871
- const e = el;
872
- if ((_a = e === null || e === void 0 ? void 0 : e.classList) === null || _a === void 0 ? void 0 : _a.contains('grafloria-node-host')) {
873
- const id = e.getAttribute('data-node-id');
874
- if (id)
875
- touched.add(id);
876
- }
877
- };
878
- for (const r of records) {
879
- // Our own re-injected handle arriving is not a change to answer — it
880
- // would echo one more pass per repaint.
881
- const ownEcho = r.removedNodes.length === 0 &&
882
- r.addedNodes.length > 0 &&
883
- Array.from(r.addedNodes).every((n) => { var _a; return (_a = n.classList) === null || _a === void 0 ? void 0 : _a.contains('axdb-rs'); });
884
- if (ownEcho)
885
- continue;
886
- noteHost(r.target);
887
- r.addedNodes.forEach((n) => noteHost(n));
888
- }
889
- if (touched.size)
890
- syncHandles(touched);
891
- });
892
592
  // -- gesture snapshot / commit ---------------------------------------------
893
593
  const snapshotAll = () => {
894
594
  const cells = new Map();
@@ -1165,54 +865,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1165
865
  const s = sizeOf(grp);
1166
866
  return { n: y - p.y <= grip, s: p.y + s.height - y <= grip, w: x - p.x <= grip, e: p.x + s.width - x <= grip };
1167
867
  };
1168
- /**
1169
- * SECTION CHROME. A section (member group) paints no card of its own, so it
1170
- * had nothing to press: a click on its empty band cleared the selection and
1171
- * its frame had no handle and no edge — a section with many children could
1172
- * not be selected at all, and could only be resized by pulling a child
1173
- * (Quantia, Groups page). Every section gets a pointer-transparent overlay
1174
- * in the HTML layer that wears the selection ring and, while selected, the
1175
- * corner handle; its frame edges answer the resize cursor.
1176
- */
1177
- const slabEls = new Map();
1178
- /**
1179
- * GROUP FRAME (0.4.43): a TAB CONTAINER wears a frame by default — its slab
1180
- * is bordered and a tinted surface lies under its pages, first in the layer
1181
- * so the tiles paint over it. A page torn out with two widgets under its tab
1182
- * read as a strip floating over two loose cards: nothing said the second
1183
- * card was the tab's. A plain section keeps its invisible slab.
1184
- */
1185
- const groupBgs = new Map();
1186
- const syncGroupBg = (layer, id, on, x, y, w, h) => {
1187
- var _a;
1188
- let bg = (_a = groupBgs.get(id)) !== null && _a !== void 0 ? _a : null;
1189
- if (!on) {
1190
- bg === null || bg === void 0 ? void 0 : bg.remove();
1191
- groupBgs.delete(id);
1192
- return;
1193
- }
1194
- if (!bg || bg.parentElement !== layer) {
1195
- bg === null || bg === void 0 ? void 0 : bg.remove();
1196
- bg = document.createElement('div');
1197
- bg.className = 'axdb-group-bg';
1198
- bg.setAttribute('data-group-bg', id);
1199
- layer.prepend(bg);
1200
- groupBgs.set(id, bg);
1201
- }
1202
- bg.style.left = `${x}px`;
1203
- bg.style.top = `${y}px`;
1204
- bg.style.width = `${w}px`;
1205
- bg.style.height = `${h}px`;
1206
- };
1207
- const isTabsGroup = (grp) => { var _a; return ((_a = grp.getMetadata('containerWidget')) === null || _a === void 0 ? void 0 : _a.layout) === 'tabs'; };
1208
- /**
1209
- * A TAB CONTAINER's frame is its drag handle (0.4.43): its 8-px margin —
1210
- * under the strip, beside the pages — moves the group, so the edge-resize
1211
- * zone shrinks to 3 px there (the corner handle still resizes). A section's
1212
- * edges keep the full grip: its empty band is a drop target, not a handle.
1213
- */
1214
- const TAB_FRAME_GRIP = 3;
1215
- const edgeGripFor = (grp) => (isTabsGroup(grp) ? TAB_FRAME_GRIP : EDGE_GRIP);
1216
868
  /**
1217
869
  * BESIDE (0.4.45): a WIDGET dragged onto a tab container's outer band — a
1218
870
  * fifth of its body, the same bands a tab's split uses — lands next to it
@@ -1298,6 +950,12 @@ export function bindDashboardGrid(api, group, options = {}) {
1298
950
  g.leg.adopted.abort();
1299
951
  g.leg = null;
1300
952
  }
953
+ if (g.subject === 'group' && !g.removedFromBoard && !beside) {
954
+ // A group ghost pushed its way here with intent: the containers it moved
955
+ // come home (the engine's memory) before the beside is measured on them.
956
+ engine.remove(g.id);
957
+ g.removedFromBoard = true;
958
+ }
1301
959
  if (g.removedFromBoard) {
1302
960
  g.removedFromBoard = false;
1303
961
  setDim(g, false);
@@ -1305,180 +963,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1305
963
  }
1306
964
  besideOn(g.id, g.spans, z, row);
1307
965
  };
1308
- /**
1309
- * CARRIED (0.4.43): a group dragged by its strip or band moves as ONE thing.
1310
- * The held TILE is transition-exempt (the ghost), but a group has no host of
1311
- * its own: its strip jumped to the pointer while its pages' tiles GLIDED
1312
- * after it — on the live demo the content trailed the strip by up to 140 px
1313
- * at every step. Everything in the group's subtree — tiles, strips, slabs,
1314
- * the surface — is exempt for the gesture and, like the ghost, through the
1315
- * drop write; then the glides resume.
1316
- */
1317
- const carriedEls = new Set();
1318
- let carriedTimer = null;
1319
- const subtreeIds = (id) => {
1320
- var _a;
1321
- const groups = [];
1322
- const nodes = [];
1323
- const queue = [id];
1324
- const seen = new Set();
1325
- while (queue.length) {
1326
- const cur = queue.shift();
1327
- if (seen.has(cur))
1328
- continue;
1329
- seen.add(cur);
1330
- const grp = diagram.getGroup(cur);
1331
- if (grp) {
1332
- groups.push(cur);
1333
- for (const m of (_a = grp.members) !== null && _a !== void 0 ? _a : [])
1334
- queue.push(m);
1335
- }
1336
- else if (diagram.getNode(cur))
1337
- nodes.push(cur);
1338
- }
1339
- return { groups, nodes };
1340
- };
1341
- const cssId = (id) => (typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(id) : id.replace(/"/g, '\\"'));
1342
- const setCarried = (id, on) => {
1343
- const layer = htmlLayer();
1344
- if (!layer)
1345
- return;
1346
- if (carriedTimer) {
1347
- clearTimeout(carriedTimer);
1348
- carriedTimer = null;
1349
- }
1350
- if (on) {
1351
- const { groups, nodes } = subtreeIds(id);
1352
- const els = [];
1353
- for (const n of nodes) {
1354
- const h = hostOf(n);
1355
- if (h)
1356
- els.push(h);
1357
- }
1358
- for (const g of groups) {
1359
- els.push(...Array.from(layer.querySelectorAll(`:scope > .axdb-tabs[data-tabs-id="${cssId(g)}"], :scope > .axdb-slab[data-slab-id="${cssId(g)}"], :scope > .axdb-group-bg[data-group-bg="${cssId(g)}"]`)));
1360
- }
1361
- for (const el of els) {
1362
- el.classList.add('axdb-carried');
1363
- carriedEls.add(el);
1364
- }
1365
- return;
1366
- }
1367
- carriedTimer = setTimeout(() => {
1368
- for (const el of carriedEls)
1369
- el.classList.remove('axdb-carried');
1370
- carriedEls.clear();
1371
- carriedTimer = null;
1372
- }, 60);
1373
- };
1374
- const flushCarried = () => {
1375
- if (carriedTimer)
1376
- clearTimeout(carriedTimer);
1377
- carriedTimer = null;
1378
- for (const el of carriedEls)
1379
- el.classList.remove('axdb-carried');
1380
- carriedEls.clear();
1381
- };
1382
- let slabLayer = null;
1383
- const syncSlabs = () => {
1384
- var _a, _b, _c;
1385
- if (disposed)
1386
- return;
1387
- const layer = (slabLayer === null || slabLayer === void 0 ? void 0 : slabLayer.isConnected) ? slabLayer : (slabLayer = htmlLayer());
1388
- if (!layer)
1389
- return;
1390
- const seen = new Set();
1391
- for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
1392
- const grp = diagram.getGroup(id);
1393
- if (!grp || diagram.getNode(id))
1394
- continue;
1395
- seen.add(id);
1396
- let el = slabEls.get(id);
1397
- if (!el || el.parentElement !== layer) {
1398
- el === null || el === void 0 ? void 0 : el.remove();
1399
- el = document.createElement('div');
1400
- el.className = 'axdb-slab';
1401
- el.setAttribute('data-slab-id', id);
1402
- const rs = document.createElement('div');
1403
- rs.className = 'axdb-rs';
1404
- rs.setAttribute('title', 'Resize section');
1405
- el.appendChild(rs);
1406
- layer.appendChild(el);
1407
- slabEls.set(id, el);
1408
- }
1409
- const p = grp.position;
1410
- const sz = sizeOf(grp);
1411
- el.style.left = `${p.x}px`;
1412
- el.style.top = `${p.y}px`;
1413
- el.style.width = `${sz.width}px`;
1414
- el.style.height = `${sz.height}px`;
1415
- el.classList.toggle('axdb-slab--selected', selectedId === id);
1416
- el.classList.toggle('axdb-slab--static', isStatic);
1417
- (_b = el.querySelector(':scope > .axdb-rs')) === null || _b === void 0 ? void 0 : _b.classList.toggle('axdb-rs--rtl', rtl);
1418
- const tabs = isTabsGroup(grp);
1419
- el.classList.toggle('axdb-slab--tabs', tabs);
1420
- syncGroupBg(layer, id, tabs, p.x, p.y, sz.width, sz.height);
1421
- syncCaption(el, id, grp, sz.height);
1422
- }
1423
- for (const [id, el] of slabEls) {
1424
- if (!seen.has(id)) {
1425
- el.remove();
1426
- hoverSlabs.delete(el);
1427
- slabEls.delete(id);
1428
- (_c = groupBgs.get(id)) === null || _c === void 0 ? void 0 : _c.remove();
1429
- groupBgs.delete(id);
1430
- }
1431
- }
1432
- };
1433
- /**
1434
- * THE CAPTION BAND of a section, on its slab overlay. Painted from the
1435
- * group's persisted caption; repainted only when its identity changes (the
1436
- * options, RTL, static, the tier) so a custom `renderCaption` is not run
1437
- * per frame. The band takes the pointer (the slab itself does not): a press
1438
- * on it selects the section, an action fires, pass-through reaches content.
1439
- */
1440
- const syncCaption = (el, id, grp, sectionH) => {
1441
- const cap = captionOfGroup(grp);
1442
- let band = el.querySelector(':scope > .axdb-slab-h');
1443
- if (!cap || !captionPainted(cap, isStatic)) {
1444
- band === null || band === void 0 ? void 0 : band.remove();
1445
- hoverSlabs.delete(el);
1446
- el.classList.remove('axdb-slab--hot');
1447
- el.removeAttribute('aria-label');
1448
- el.removeAttribute('role');
1449
- return;
1450
- }
1451
- if (cap.show === 'hover')
1452
- hoverSlabs.add(el);
1453
- else {
1454
- hoverSlabs.delete(el);
1455
- el.classList.remove('axdb-slab--hot');
1456
- }
1457
- const ctx = { rtl, static: isStatic, sectionH };
1458
- const key = captionKey(cap, ctx);
1459
- if (band && band.getAttribute('data-key') === key) {
1460
- sizeCaptionBand(band, cap, sectionH); // the tier follows the live size
1461
- return;
1462
- }
1463
- band === null || band === void 0 ? void 0 : band.remove();
1464
- band = document.createElement('div');
1465
- el.prepend(band);
1466
- const render = options.renderCaption;
1467
- paintCaptionBand(band, cap, Object.assign(Object.assign(Object.assign({}, ctx), (render ? { render: (host) => render(id, host) } : {})), { onAction: (actionId) => { var _a; return (_a = options.onCaptionAction) === null || _a === void 0 ? void 0 : _a.call(options, id, actionId); } }));
1468
- band.setAttribute('data-key', key);
1469
- // A named group, so the caption text is the section's name in the
1470
- // accessibility tree rather than a stray label on an unnamed div. (The
1471
- // band is not a tab stop yet — the actions are deliberately out of the
1472
- // tab order so a board keeps exactly ONE stop; see the plan.)
1473
- if (cap.text) {
1474
- el.setAttribute('role', 'group');
1475
- el.setAttribute('aria-label', cap.text);
1476
- }
1477
- else {
1478
- el.removeAttribute('role');
1479
- el.removeAttribute('aria-label');
1480
- }
1481
- };
1482
966
  const insideMemberGroupFrame = (x, y) => {
1483
967
  var _a;
1484
968
  for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
@@ -1493,8 +977,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1493
977
  return false;
1494
978
  };
1495
979
  let slabGesture = null;
1496
- /** The slab gesture as it is NOW read through a call so a guard's narrowing does not stick. */
1497
- const currentSlab = () => slabGesture;
980
+ /** The gesture as it is NOW, for the same reason. */
981
+ const currentGesture = () => gesture;
1498
982
  /** The PARENT running a resize of OUR section from a press this tool claimed. */
1499
983
  let forwardSlab = null;
1500
984
  const frameOfGroup = (grp) => ({ x: grp.position.x, y: grp.position.y, width: sizeOf(grp).width, height: sizeOf(grp).height });
@@ -1542,48 +1026,45 @@ export function bindDashboardGrid(api, group, options = {}) {
1542
1026
  const it = engine.getItem(id);
1543
1027
  if (!grp || !it || gesture || slabGesture || isStatic)
1544
1028
  return;
1545
- engine.beginGesture();
1546
- const snap = snapshotAll();
1547
- slabGesture = {
1029
+ // A GROUP MOVE IS A GESTURE OF THE ONE MACHINE (tile first, 4b-ii): the
1030
+ // same threshold, snapshot, zone walk, legs, beside and commit a widget
1031
+ // gets — the group being the subject. The frame follows its cell.
1032
+ //
1033
+ // THE PRESS ARMS NOTHING. `beginGestureVisuals` arms the glide when the
1034
+ // threshold is crossed, as it does for a widget: arming it here left a
1035
+ // plain CLICK on a caption band gliding for 400 ms, and everything the
1036
+ // page did next — a caption change, a layout switch — animated instead of
1037
+ // landing (the gallery's fluid-board: "and the rows are back" measured a
1038
+ // tile still travelling).
1039
+ gesture = {
1040
+ kind: 'move',
1548
1041
  id,
1549
- edges: NO_EDGES,
1042
+ subject: 'group',
1043
+ entity: grp,
1550
1044
  pointerId: typeof PointerEvent !== 'undefined' && ev.source instanceof PointerEvent ? ev.source.pointerId : null,
1551
1045
  started: false,
1552
- downScreen: { x: ev.screen.x, y: ev.screen.y },
1553
- startCells: snap.cells,
1554
- startGeom: snap.geoms,
1555
- cellBefore: { x: it.x, y: it.y, w: it.w, h: it.h },
1556
- frameBefore: frameOfGroup(grp),
1557
- grab: { dx: grp.position.x - ev.world.x, dy: grp.position.y - ev.world.y },
1558
- move: true,
1046
+ downClient: { x: ev.screen.x, y: ev.screen.y },
1047
+ downWorld: { x: ev.world.x, y: ev.world.y },
1048
+ grab: { dx: ev.world.x - grp.position.x, dy: ev.world.y - grp.position.y },
1049
+ startCells: new Map(),
1050
+ startGeom: new Map(),
1051
+ startSize: { width: sizeOf(grp).width, height: sizeOf(grp).height },
1052
+ startPos: { x: grp.position.x, y: grp.position.y },
1053
+ edges: NO_EDGES,
1054
+ spans: { w: it.w, h: it.h },
1055
+ removedFromBoard: false,
1056
+ leg: null,
1057
+ strip: null,
1058
+ lastWorld: null,
1059
+ lastScreen: null,
1060
+ hostEl: null,
1061
+ esc: null,
1062
+ chip: null,
1559
1063
  };
1560
- capturePointer(slabGesture.pointerId);
1561
- api.container.style.cursor = 'grabbing';
1562
- };
1563
- /** The cell a slab move asked for and could not have — painted so the refusal is visible; null clears it. */
1564
- let refusal = null;
1565
- const showRefusal = (cell, w, h) => {
1566
- const layer = htmlLayer();
1567
- if (!cell || !layer) {
1568
- refusal === null || refusal === void 0 ? void 0 : refusal.remove();
1569
- refusal = null;
1570
- api.container.style.cursor = slabGesture ? 'grabbing' : '';
1571
- return;
1572
- }
1573
- if (!refusal || refusal.parentElement !== layer) {
1574
- refusal === null || refusal === void 0 ? void 0 : refusal.remove();
1575
- refusal = document.createElement('div');
1576
- refusal.className = 'axdb-ph axdb-ph--no';
1577
- layer.prepend(refusal);
1578
- }
1579
- const r = cellToRect({ x: cell.x, y: cell.y, w, h }, frame(), geom(), rows());
1580
- refusal.style.left = `${r.x}px`;
1581
- refusal.style.top = `${r.y}px`;
1582
- refusal.style.width = `${r.width}px`;
1583
- refusal.style.height = `${r.height}px`;
1584
- api.container.style.cursor = 'not-allowed';
1585
1064
  };
1586
1065
  const slabMove = (ev) => {
1066
+ if ((gesture === null || gesture === void 0 ? void 0 : gesture.subject) === 'group')
1067
+ return onToolMove(ev);
1587
1068
  const g = slabGesture;
1588
1069
  if (!g)
1589
1070
  return;
@@ -1668,6 +1149,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1668
1149
  };
1669
1150
  const slabUp = () => {
1670
1151
  var _a;
1152
+ if ((gesture === null || gesture === void 0 ? void 0 : gesture.subject) === 'group')
1153
+ return onToolUp();
1671
1154
  const g = slabGesture;
1672
1155
  if (!g)
1673
1156
  return;
@@ -1694,6 +1177,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1694
1177
  };
1695
1178
  const slabCancel = () => {
1696
1179
  var _a;
1180
+ if ((gesture === null || gesture === void 0 ? void 0 : gesture.subject) === 'group')
1181
+ return cancelActiveGesture();
1697
1182
  const g = slabGesture;
1698
1183
  if (!g)
1699
1184
  return;
@@ -1741,8 +1226,12 @@ export function bindDashboardGrid(api, group, options = {}) {
1741
1226
  g.started = true;
1742
1227
  armGlide();
1743
1228
  if (g.kind !== 'palette') {
1744
- setGhost(g.id, true);
1745
- g.hostEl = hostOf(g.id);
1229
+ if (g.subject === 'node') {
1230
+ setGhost(g.id, true);
1231
+ g.hostEl = hostOf(g.id);
1232
+ }
1233
+ else
1234
+ setCarried(g.id, true); // a group moves as one thing: its whole subtree is exempt from the glide
1746
1235
  capturePointer(g.pointerId);
1747
1236
  api.container.style.cursor = g.kind === 'resize' ? 'nwse-resize' : 'grabbing';
1748
1237
  }
@@ -1778,14 +1267,19 @@ export function bindDashboardGrid(api, group, options = {}) {
1778
1267
  };
1779
1268
  const cleanupGestureVisuals = (g) => {
1780
1269
  var _a;
1781
- if (g.kind !== 'palette')
1782
- setGhost(g.id, false);
1270
+ if (g.kind !== 'palette') {
1271
+ if (g.subject === 'node')
1272
+ setGhost(g.id, false);
1273
+ else {
1274
+ setCarried(g.id, false); // exempt through the drop write, then the glides resume
1275
+ showRefusal(null, 0, 0);
1276
+ }
1277
+ }
1783
1278
  disarmGlideSoon();
1784
1279
  releasePointer(g.pointerId);
1785
1280
  api.container.style.cursor = '';
1786
1281
  (_a = g.chip) === null || _a === void 0 ? void 0 : _a.remove();
1787
- placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
1788
- placeholder = null;
1282
+ hidePlaceholder();
1789
1283
  };
1790
1284
  const commitGesture = (g) => {
1791
1285
  var _a;
@@ -1815,8 +1309,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1815
1309
  persistLayouts(); // an edit at a narrow count propagated into the wide cache
1816
1310
  if (changed) {
1817
1311
  const it = engine.getItem(g.id);
1818
- if (it)
1819
- live.announce(`${nameOf(g.node)} ${g.kind === 'resize' ? 'resized' : 'moved'} to ${describeCell(it)}`);
1312
+ if (it && g.subject === 'node')
1313
+ live.announce(`${nameOf(nodeOf(g))} ${g.kind === 'resize' ? 'resized' : 'moved'} to ${describeCell(it)}`);
1820
1314
  }
1821
1315
  syncA11y();
1822
1316
  api.renderNow();
@@ -1916,14 +1410,17 @@ export function bindDashboardGrid(api, group, options = {}) {
1916
1410
  const moveGhost = (g, ev) => {
1917
1411
  var _a;
1918
1412
  const desired = { x: ev.world.x - g.grab.dx, y: ev.world.y - g.grab.dy };
1919
- g.node.setPosition(desired.x, desired.y);
1920
- ghostStyleFastPath(g, desired);
1413
+ if (g.subject === 'node') {
1414
+ // The pixel ghost follows the hand; a group's frame is projected from its cell, carried.
1415
+ nodeOf(g).setPosition(desired.x, desired.y);
1416
+ ghostStyleFastPath(g, desired);
1417
+ }
1921
1418
  g.lastWorld = { x: ev.world.x, y: ev.world.y };
1922
1419
  g.lastScreen = { x: ev.screen.x, y: ev.screen.y };
1923
1420
  /** The ghost's pixel size on THIS board — a palette chip has spans, not a size. */
1924
1421
  const pxSize = () => {
1925
1422
  if (g.kind !== 'palette')
1926
- return { width: g.node.size.width, height: g.node.size.height };
1423
+ return sizeOf(g.entity);
1927
1424
  const f = frame();
1928
1425
  const gg = geom();
1929
1426
  return { width: g.spans.w * (columnUnitFor(gg, f.width) + gap) - gap, height: g.spans.h * (rowHeightFor(gg, rows()) + gap) - gap };
@@ -1952,7 +1449,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1952
1449
  project();
1953
1450
  }
1954
1451
  if (!g.leg) {
1955
- const adopted = peer.adopt(g.node, ev.world, pxSize(), opts);
1452
+ const adopted = peer.adopt(g.entity, ev.world, pxSize(), opts);
1956
1453
  if (!adopted)
1957
1454
  return false;
1958
1455
  g.leg = { peer, adopted };
@@ -1965,7 +1462,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1965
1462
  // BEFORE any engine is asked anything. The strip still wins over every
1966
1463
  // board; a band's stickiness and the vacated cell still hold a beside.
1967
1464
  const z = resolveTileZone(g, ev);
1968
- if (z.kind === 'strip' && options.tabDrop && !isStatic && g.kind !== 'palette') {
1465
+ if (z.kind === 'strip' && options.tabDrop && !isStatic && g.kind !== 'palette' && g.subject === 'node') {
1969
1466
  // -- INTO A STRIP: the widget becomes a new tab there, so it leaves
1970
1467
  // this board (survivors settle home) and the strip marks the slot.
1971
1468
  endBeside(true); // a band's shift gives way to the strip: the container comes back
@@ -2063,7 +1560,10 @@ export function bindDashboardGrid(api, group, options = {}) {
2063
1560
  g.leg = null;
2064
1561
  }
2065
1562
  setDim(g, false);
2066
- placeOnSelf(g, desired);
1563
+ // A group moved by hand takes the cell under the hand and PUSHES what
1564
+ // stands in its way (0.4.44) — and the zone walk reads the containers
1565
+ // it pushed at REST, so the hand still finds their zones where they were.
1566
+ placeOnSelf(g, desired, g.subject === 'group');
2067
1567
  }
2068
1568
  else {
2069
1569
  // -- outside every board ------------------------------------------
@@ -2071,6 +1571,10 @@ export function bindDashboardGrid(api, group, options = {}) {
2071
1571
  setDim(g, true);
2072
1572
  }
2073
1573
  syncPlaceholder();
1574
+ // A carried group's slab and frame are this board's chrome, but its frame
1575
+ // is written by whichever board holds it now: re-sync so they follow.
1576
+ if (g.subject === 'group')
1577
+ syncSlabs();
2074
1578
  };
2075
1579
  const onToolMove = (ev) => {
2076
1580
  var _a, _b, _c;
@@ -2355,8 +1859,8 @@ export function bindDashboardGrid(api, group, options = {}) {
2355
1859
  const anchorBottom = liveRect ? liveRect.y + liveRect.height : bottom;
2356
1860
  const px = E.w ? anchorRight - w : anchorLeft;
2357
1861
  const py = E.n ? anchorBottom - h : anchorTop;
2358
- g.node.setSize(w, h, (_c = g.node.size.depth) !== null && _c !== void 0 ? _c : 0);
2359
- g.node.setPosition(px, py);
1862
+ nodeOf(g).setSize(w, h, (_c = nodeOf(g).size.depth) !== null && _c !== void 0 ? _c : 0);
1863
+ nodeOf(g).setPosition(px, py);
2360
1864
  ghostStyleFastPath(g, { x: px, y: py, width: w, height: h });
2361
1865
  const spanF = bound() !== undefined ? frame() : f;
2362
1866
  const spanG = bound() !== undefined ? geom() : gg;
@@ -2396,7 +1900,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2396
1900
  syncPlaceholder();
2397
1901
  };
2398
1902
  const onToolUp = () => {
2399
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1903
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
2400
1904
  const g = gesture;
2401
1905
  if (!g || g.kind === 'palette')
2402
1906
  return;
@@ -2416,8 +1920,8 @@ export function bindDashboardGrid(api, group, options = {}) {
2416
1920
  const displaced = tileCommands(deltasSince(g.startCells, g.startGeom, g.id));
2417
1921
  const snap = g.startGeom.get(g.id);
2418
1922
  if (snap) {
2419
- g.node.setPosition(snap.pos.x, snap.pos.y);
2420
- g.node.setSize(snap.size.width, snap.size.height, (_a = snap.size.depth) !== null && _a !== void 0 ? _a : 0);
1923
+ nodeOf(g).setPosition(snap.pos.x, snap.pos.y);
1924
+ nodeOf(g).setSize(snap.size.width, snap.size.height, (_a = snap.size.depth) !== null && _a !== void 0 ? _a : 0);
2421
1925
  }
2422
1926
  const cmds = options.tabDrop.dropIntoStrip(g.id, target.containerId, target.index, group.id, displaced);
2423
1927
  if (cmds.length === 0) {
@@ -2447,8 +1951,12 @@ export function bindDashboardGrid(api, group, options = {}) {
2447
1951
  try {
2448
1952
  diagram.runSystemWrite(() => {
2449
1953
  var _a;
2450
- g.node.setPosition(fin.rect.x, fin.rect.y);
2451
- g.node.setSize(fin.rect.width, fin.rect.height, (_a = g.node.size.depth) !== null && _a !== void 0 ? _a : 0);
1954
+ if (g.subject === 'node') {
1955
+ nodeOf(g).setPosition(fin.rect.x, fin.rect.y);
1956
+ nodeOf(g).setSize(fin.rect.width, fin.rect.height, (_a = nodeOf(g).size.depth) !== null && _a !== void 0 ? _a : 0);
1957
+ }
1958
+ else
1959
+ g.entity.setFrame(Object.assign({}, fin.rect));
2452
1960
  });
2453
1961
  }
2454
1962
  finally {
@@ -2457,6 +1965,27 @@ export function bindDashboardGrid(api, group, options = {}) {
2457
1965
  const sourceDisplaced = tileCommands(deltasSince(g.startCells, g.startGeom, g.id));
2458
1966
  const before = g.startCells.get(g.id);
2459
1967
  const geomBefore = g.startGeom.get(g.id);
1968
+ // A GROUP crossing boards: its cell-and-frame command, and the host's
1969
+ // bookkeeping for a container that changed boards (the spec entry, the
1970
+ // registry) rides along through `onMemberMoving` (tile first, 4b-ii).
1971
+ const own = g.subject === 'group'
1972
+ ? [
1973
+ new SetGroupCellCommand(g.id, before !== null && before !== void 0 ? before : fin.cell, fin.cell, geomBefore ? { x: geomBefore.pos.x, y: geomBefore.pos.y, width: geomBefore.size.width, height: geomBefore.size.height } : fin.rect, fin.rect),
1974
+ ...((_d = (_c = options.onMemberMoving) === null || _c === void 0 ? void 0 : _c.call(options, g.id, group.id, targetGroupId)) !== null && _d !== void 0 ? _d : []),
1975
+ ]
1976
+ : buildCommitCommands([
1977
+ {
1978
+ id: g.id,
1979
+ locked: false,
1980
+ isGroup: false,
1981
+ cellBefore: before !== null && before !== void 0 ? before : fin.cell,
1982
+ cellAfter: fin.cell,
1983
+ posBefore: (_e = geomBefore === null || geomBefore === void 0 ? void 0 : geomBefore.pos) !== null && _e !== void 0 ? _e : { x: fin.rect.x, y: fin.rect.y },
1984
+ posAfter: { x: fin.rect.x, y: fin.rect.y },
1985
+ sizeBefore: (_f = geomBefore === null || geomBefore === void 0 ? void 0 : geomBefore.size) !== null && _f !== void 0 ? _f : { width: fin.rect.width, height: fin.rect.height },
1986
+ sizeAfter: { width: fin.rect.width, height: fin.rect.height },
1987
+ },
1988
+ ]);
2460
1989
  const crossing = [
2461
1990
  ...sourceDisplaced,
2462
1991
  // …and the TARGET board's own displaced tiles. Dropping onto an
@@ -2469,24 +1998,12 @@ export function bindDashboardGrid(api, group, options = {}) {
2469
1998
  ...fin.commands,
2470
1999
  new RemoveFromGroupCommand(group.id, g.id),
2471
2000
  new AddToGroupCommand(targetGroupId, g.id),
2472
- ...buildCommitCommands([
2473
- {
2474
- id: g.id,
2475
- locked: false,
2476
- isGroup: false,
2477
- cellBefore: before !== null && before !== void 0 ? before : fin.cell,
2478
- cellAfter: fin.cell,
2479
- posBefore: (_c = geomBefore === null || geomBefore === void 0 ? void 0 : geomBefore.pos) !== null && _c !== void 0 ? _c : { x: fin.rect.x, y: fin.rect.y },
2480
- posAfter: { x: fin.rect.x, y: fin.rect.y },
2481
- sizeBefore: (_d = geomBefore === null || geomBefore === void 0 ? void 0 : geomBefore.size) !== null && _d !== void 0 ? _d : { width: fin.rect.width, height: fin.rect.height },
2482
- sizeAfter: { width: fin.rect.width, height: fin.rect.height },
2483
- },
2484
- ]),
2001
+ ...own,
2485
2002
  ];
2486
2003
  // …and whatever follows a member out of this board: an emptied page
2487
2004
  // closes — in which case the membership commands ride INSIDE one
2488
2005
  // sequence with it, or the batch could never undo (its group is gone).
2489
- const leaving = (_f = (_e = options.onMemberLeaving) === null || _e === void 0 ? void 0 : _e.call(options, g.id)) !== null && _f !== void 0 ? _f : [];
2006
+ const leaving = (_h = (_g = options.onMemberLeaving) === null || _g === void 0 ? void 0 : _g.call(options, g.id)) !== null && _h !== void 0 ? _h : [];
2490
2007
  execute('Move widget', leaving.length > 0 ? [new SequenceCommand('Move widget', [...crossing, ...leaving])] : crossing);
2491
2008
  engine.endGesture();
2492
2009
  cleanupGestureVisuals(g);
@@ -2494,10 +2011,10 @@ export function bindDashboardGrid(api, group, options = {}) {
2494
2011
  enforceBoardHeight();
2495
2012
  persistLayouts();
2496
2013
  api.renderNow();
2497
- (_g = options.onGesture) === null || _g === void 0 ? void 0 : _g.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed: true });
2014
+ (_j = options.onGesture) === null || _j === void 0 ? void 0 : _j.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed: true });
2498
2015
  return;
2499
2016
  }
2500
- if (g.removedFromBoard && dragOut === 'cancel') {
2017
+ if (g.removedFromBoard && (dragOut === 'cancel' || g.subject === 'group')) {
2501
2018
  // Released outside every board on a snap-home board: full restore,
2502
2019
  // nothing committed (the parked-outside release).
2503
2020
  cancelActiveGesture();
@@ -2524,16 +2041,16 @@ export function bindDashboardGrid(api, group, options = {}) {
2524
2041
  if (snap) {
2525
2042
  // Park the node on its start rect so the page's RemoveNodeCommand
2526
2043
  // captures sane geometry for undo.
2527
- g.node.setPosition(snap.pos.x, snap.pos.y);
2528
- g.node.setSize(snap.size.width, snap.size.height, (_h = snap.size.depth) !== null && _h !== void 0 ? _h : 0);
2044
+ nodeOf(g).setPosition(snap.pos.x, snap.pos.y);
2045
+ nodeOf(g).setSize(snap.size.width, snap.size.height, (_k = snap.size.depth) !== null && _k !== void 0 ? _k : 0);
2529
2046
  }
2530
2047
  engine.endGesture();
2531
2048
  cleanupGestureVisuals(g);
2532
2049
  gesture = null;
2533
2050
  enforceBoardHeight();
2534
2051
  api.renderNow();
2535
- void ((_j = options.onRemoveRequest) === null || _j === void 0 ? void 0 : _j.call(options, g.id, displaced));
2536
- (_k = options.onGesture) === null || _k === void 0 ? void 0 : _k.call(options, { type: 'remove', kind: g.kind, nodeId: g.id, changed: true });
2052
+ void ((_l = options.onRemoveRequest) === null || _l === void 0 ? void 0 : _l.call(options, g.id, displaced));
2053
+ (_m = options.onGesture) === null || _m === void 0 ? void 0 : _m.call(options, { type: 'remove', kind: g.kind, nodeId: g.id, changed: true });
2537
2054
  return;
2538
2055
  }
2539
2056
  commitGesture(g);
@@ -2611,6 +2128,27 @@ export function bindDashboardGrid(api, group, options = {}) {
2611
2128
  placeNear(g.id, cell.x, cell.y, g.spans.w, pushSolid);
2612
2129
  project();
2613
2130
  }
2131
+ else if (g.subject === 'group') {
2132
+ // The cell under the pointer, else the nearest legal one ALONG ITS ROW
2133
+ // (a section dragged left that wandered down its column read as the
2134
+ // wrong tile moving); when even that fails the wanted cell is painted
2135
+ // as refused (identification round, D6/D7).
2136
+ const it = engine.getItem(g.id);
2137
+ if (!it)
2138
+ return;
2139
+ const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), it.w);
2140
+ if (cell.x === it.x && cell.y === it.y)
2141
+ return;
2142
+ const was = { x: it.x, y: it.y };
2143
+ const moved = engine.moveCheck(g.id, cell.x, cell.y, { gate: false, pushSolid: true }).changed || placeOnRow(g.id, cell.x, cell.y, it.w, true);
2144
+ if (moved) {
2145
+ project();
2146
+ setCarried(g.id, true); // chrome repainted by the projection is carried too
2147
+ }
2148
+ const now = engine.getItem(g.id);
2149
+ const stuck = !!now && now.x === was.x && now.y === was.y;
2150
+ showRefusal(stuck ? cell : null, it.w, it.h);
2151
+ }
2614
2152
  else {
2615
2153
  const spanW = (_b = (_a = engine.getItem(g.id)) === null || _a === void 0 ? void 0 : _a.w) !== null && _b !== void 0 ? _b : g.spans.w;
2616
2154
  const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), spanW);
@@ -2622,24 +2160,65 @@ export function bindDashboardGrid(api, group, options = {}) {
2622
2160
  const resolveTileZone = (g, ev) => {
2623
2161
  var _a, _b;
2624
2162
  let strip = null;
2625
- if (options.tabDrop && !isStatic && g.kind !== 'palette') {
2163
+ if (options.tabDrop && !isStatic && g.kind !== 'palette' && g.subject === 'node') {
2164
+ // A group never becomes a tab: over a container's strip it is over the
2165
+ // container's margin — a cell on the parent board, pushing with intent.
2626
2166
  const crect = api.container.getBoundingClientRect();
2627
2167
  strip = options.tabDrop.stripAt(crect.left + ev.screen.x, crect.top + ev.screen.y);
2628
2168
  }
2629
- return resolveZone({
2630
- x: ev.world.x,
2631
- y: ev.world.y,
2632
- roots: zoneRoots(),
2633
- strip,
2634
- prev: beside
2169
+ return resolveZone(Object.assign(Object.assign({ x: ev.world.x, y: ev.world.y, roots: zoneRoots(), strip, prev: beside
2635
2170
  ? { containerId: beside.id, side: beside.side, frame0: beside.frame0, vacated: cellToRect({ x: beside.vacated.x, y: beside.vacated.y, w: g.spans.w, h: g.spans.h }, frame(), geom(), rows()) }
2636
- : ((_b = (_a = g.leg) === null || _a === void 0 ? void 0 : _a.adopted.besideState()) !== null && _b !== void 0 ? _b : null), // a beside another board holds for the ghost, through its leg
2637
- maxDepth: nesting,
2638
- ghostDepth: 0,
2639
- ghostSubtree: EMPTY_SUBTREE,
2640
- gap,
2641
- homeChain: homeChain(),
2642
- });
2171
+ : ((_b = (_a = g.leg) === null || _a === void 0 ? void 0 : _a.adopted.besideState()) !== null && _b !== void 0 ? _b : null), maxDepth: nesting, ghostDepth: g.subject === 'group' ? 1 + levelsInside(g.id) : 0 }, (g.subject === 'group' ? { restFrames: restFramesOf(g) } : {})), { ghostSubtree: g.subject === 'group' ? descendantGroups(g.id) : EMPTY_SUBTREE, gap, homeChain: homeChain() }));
2172
+ };
2173
+ /** This board's containers as they stood at the press: a group ghost's intent pushes never change what the hand means. */
2174
+ const restFramesOf = (g) => {
2175
+ const out = new Map();
2176
+ for (const [id, snap] of g.startGeom) {
2177
+ if (id === g.id || !isGroupMember(id))
2178
+ continue;
2179
+ out.set(id, { x: snap.pos.x, y: snap.pos.y, width: snap.size.width, height: snap.size.height });
2180
+ }
2181
+ return out;
2182
+ };
2183
+ /** Every group under `id`, itself included — the boards a group ghost may never enter. */
2184
+ const descendantGroups = (id) => {
2185
+ var _a;
2186
+ const out = new Set();
2187
+ const queue = [id];
2188
+ while (queue.length) {
2189
+ const cur = queue.shift();
2190
+ if (out.has(cur))
2191
+ continue;
2192
+ const grp = diagram.getGroup(cur);
2193
+ if (!grp)
2194
+ continue;
2195
+ out.add(cur);
2196
+ for (const m of (_a = grp.members) !== null && _a !== void 0 ? _a : [])
2197
+ if (diagram.getGroup(m))
2198
+ queue.push(m);
2199
+ }
2200
+ return out;
2201
+ };
2202
+ /**
2203
+ * The container LEVELS a group carries INSIDE it: 0 for a section of
2204
+ * widgets or a tab container of plain pages (a container and its page are
2205
+ * one level), 1 for a section holding a section, and so on. The group's own
2206
+ * level is the +1 above: its widgets sit one board deeper than the board it
2207
+ * lands on. The nesting policy counts both.
2208
+ */
2209
+ const levelsInside = (id) => {
2210
+ var _a;
2211
+ const grp = diagram.getGroup(id);
2212
+ if (!grp)
2213
+ return 0;
2214
+ const tabs = isTabsGroup(grp);
2215
+ let deepest = 0;
2216
+ for (const m of (_a = grp.members) !== null && _a !== void 0 ? _a : []) {
2217
+ if (!diagram.getGroup(m))
2218
+ continue;
2219
+ deepest = Math.max(deepest, tabs ? levelsInside(m) : 1 + levelsInside(m));
2220
+ }
2221
+ return deepest;
2643
2222
  };
2644
2223
  /** The groups this board sits in, all the way up: their bands never apply to a tile of this board. */
2645
2224
  const homeChain = () => {
@@ -3021,7 +2600,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3021
2600
  if (isStatic)
3022
2601
  return false;
3023
2602
  beginSlabMove(id, ev);
3024
- return (slabGesture === null || slabGesture === void 0 ? void 0 : slabGesture.id) === id;
2603
+ return (gesture === null || gesture === void 0 ? void 0 : gesture.id) === id;
3025
2604
  },
3026
2605
  dragMember: (id, ev) => {
3027
2606
  var _a;
@@ -3034,7 +2613,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3034
2613
  return { world, screen: { x: e.clientX - rect.left, y: e.clientY - rect.top }, source: e };
3035
2614
  };
3036
2615
  beginSlabMove(id, toTool(ev));
3037
- if (((_a = currentSlab()) === null || _a === void 0 ? void 0 : _a.id) !== id)
2616
+ if (((_a = currentGesture()) === null || _a === void 0 ? void 0 : _a.id) !== id)
3038
2617
  return false;
3039
2618
  const detachAll = () => {
3040
2619
  window.removeEventListener('pointermove', onMove, true);
@@ -3044,7 +2623,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3044
2623
  };
3045
2624
  const onMove = (e) => {
3046
2625
  var _a;
3047
- if (disposed || ((_a = currentSlab()) === null || _a === void 0 ? void 0 : _a.id) !== id)
2626
+ if (disposed || ((_a = currentGesture()) === null || _a === void 0 ? void 0 : _a.id) !== id)
3048
2627
  return detachAll();
3049
2628
  slabMove(toTool(e));
3050
2629
  api.render();
@@ -3314,7 +2893,8 @@ export function bindDashboardGrid(api, group, options = {}) {
3314
2893
  gesture = {
3315
2894
  kind: isResize ? 'resize' : 'move',
3316
2895
  id: node.id,
3317
- node,
2896
+ subject: 'node',
2897
+ entity: node,
3318
2898
  pointerId: typeof PointerEvent !== 'undefined' && ev.source instanceof PointerEvent
3319
2899
  ? ev.source.pointerId
3320
2900
  : null,
@@ -3390,253 +2970,24 @@ export function bindDashboardGrid(api, group, options = {}) {
3390
2970
  onToolUp();
3391
2971
  };
3392
2972
  const unregisterTool = registerTool(tool);
3393
- /**
3394
- * Edge affordance: the cursor says which border a press would take, the
3395
- * way gridstack's invisible edge handles do. One passive listener on the
3396
- * container; the corner handle keeps its own cursor from the stylesheet.
3397
- */
3398
- let hoverHost = null;
3399
- /**
3400
- * A `show: 'hover'` caption cannot ride CSS `:hover`: the slab overlay takes
3401
- * no pointer (by design — it must never steal a press), and while the band
3402
- * is hidden it takes none either, so nothing in the section is ever hovered
3403
- * in CSS terms. The binder already tracks the pointer; it marks the section
3404
- * under it instead.
3405
- */
3406
- /** Only the sections carrying a `show: 'hover'` band — usually none, so the
3407
- * pointer handler costs nothing on a board that has no hover caption. */
3408
- const hoverSlabs = new Set();
3409
- const markHotSection = (clientX, clientY) => {
3410
- if (!hoverSlabs.size)
3411
- return;
3412
- for (const el of hoverSlabs) {
3413
- const r = el.getBoundingClientRect();
3414
- el.classList.toggle('axdb-slab--hot', clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom);
3415
- }
3416
- };
3417
- const onHoverLeave = () => {
3418
- for (const el of hoverSlabs)
3419
- el.classList.remove('axdb-slab--hot');
3420
- };
3421
- const onHover = (e) => {
3422
- var _a, _b, _c, _d, _e, _f, _g, _h;
3423
- if (disposed || gesture)
3424
- return;
3425
- markHotSection(e.clientX, e.clientY);
3426
- // The event may target the host, its content, or (when a host's content
3427
- // is pointer-transparent) the canvas under it — find the member host by
3428
- // the pointer's position in that case.
3429
- let host = (_b = (_a = e.target) === null || _a === void 0 ? void 0 : _a.closest) === null || _b === void 0 ? void 0 : _b.call(_a, '.grafloria-node-host');
3430
- if (!host) {
3431
- for (const id of (_c = group.members) !== null && _c !== void 0 ? _c : []) {
3432
- const h = hostOf(id);
3433
- if (!h)
3434
- continue;
3435
- const r = h.getBoundingClientRect();
3436
- if (e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom) {
3437
- host = h;
3438
- break;
3439
- }
3440
- }
3441
- }
3442
- if (hoverHost && hoverHost !== host) {
3443
- hoverHost.style.cursor = '';
3444
- hoverHost.removeAttribute('data-axdb-edge'); // the affordance follows the pointer off a tile
3445
- }
3446
- hoverHost = host;
3447
- if (!host) {
3448
- // No tile under the pointer: a SECTION's frame edge still says resize.
3449
- const wpt = ((_d = api.viewport) === null || _d === void 0 ? void 0 : _d.clientToWorld) ? api.viewport.clientToWorld(e.clientX, e.clientY, api.container.getBoundingClientRect()) : null;
3450
- const sid = wpt ? memberGroupAt(wpt.x, wpt.y) : null;
3451
- const grp = sid ? diagram.getGroup(sid) : undefined;
3452
- const c = grp && wpt && !isStatic ? cursorFor(slabEdgesNear(grp, wpt.x, wpt.y)) : '';
3453
- if (!slabGesture)
3454
- api.container.style.cursor = c;
3455
- return;
3456
- }
3457
- if (!slabGesture && api.container.style.cursor)
3458
- api.container.style.cursor = '';
3459
- const id = (_e = host.getAttribute('data-node-id')) !== null && _e !== void 0 ? _e : '';
3460
- if (!((_f = group.members) !== null && _f !== void 0 ? _f : new Set()).has(id))
3461
- return;
3462
- const node = diagram.getNode(id);
3463
- const resizable = !!node && !isStatic && ((_g = node.state) === null || _g === void 0 ? void 0 : _g.locked) !== true && ((_h = node.getMetadata) === null || _h === void 0 ? void 0 : _h.call(node, 'widgetResizable')) !== false;
3464
- const cursor = resizable ? cursorFor(edgesNear(host, e.clientX, e.clientY)) : '';
3465
- // The affordance rides on an ATTRIBUTE, not the inline style: a repaint
3466
- // rewrites the host's style and used to clear the cursor mid-hover, and
3467
- // content that sets its own cursor (a chart canvas) hid it — the
3468
- // stylesheet applies the attribute's cursor to the host and everything in it.
3469
- if (cursor)
3470
- host.setAttribute('data-axdb-edge', cursor);
3471
- else
3472
- host.removeAttribute('data-axdb-edge');
3473
- };
3474
- api.container.addEventListener('pointermove', onHover, { passive: true });
3475
- api.container.addEventListener('pointerleave', onHoverLeave, { passive: true });
3476
- /**
3477
- * STATIC BOARDS LET CONTENT BE CLICKED. The renderer prevents the default of
3478
- * every press a tool claims, which cancels the compatibility mouse events —
3479
- * a chart inside a read-only board could not be clicked. So under `static`
3480
- * a press inside a member's CONTENT (not on kit chrome) is stopped on the
3481
- * HTML layer, in the bubble phase: the content has already received it, the
3482
- * renderer never does, nothing is prevented.
3483
- */
3484
- const staticGuard = (e) => {
3485
- var _a, _b, _c, _d;
3486
- if (!isStatic || disposed)
3487
- return;
3488
- const t = e.target;
3489
- const host = (_a = t === null || t === void 0 ? void 0 : t.closest) === null || _a === void 0 ? void 0 : _a.call(t, '.grafloria-node-host');
3490
- if (!host || !((_b = group.members) !== null && _b !== void 0 ? _b : new Set()).has((_c = host.getAttribute('data-node-id')) !== null && _c !== void 0 ? _c : ''))
3491
- return;
3492
- if ((_d = t === null || t === void 0 ? void 0 : t.closest) === null || _d === void 0 ? void 0 : _d.call(t, '.axdb-rs, .axdb-grip, .axdb-div'))
3493
- return;
3494
- e.stopPropagation();
3495
- };
3496
- let guardedLayer = null;
3497
- const ensureStaticGuard = () => {
3498
- // One lookup, ever: the host observer budgets container lookups per
3499
- // repaint, and the layer element lives as long as the instance.
3500
- if (guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.isConnected)
3501
- return;
3502
- const layer = htmlLayer();
3503
- if (!layer)
3504
- return;
3505
- guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
3506
- guardedLayer = layer;
3507
- layer.addEventListener('pointerdown', staticGuard);
3508
- };
3509
- /**
3510
- * KEYBOARD OPERATION (WCAG 2.1.1, and the non-drag alternative 2.5.7 asks
3511
- * for): on a focused member, arrows move it one cell, Shift+arrows resize it
3512
- * one cell, Home/End jump to the first/last member. Every move and resize is
3513
- * the same programmatic gesture the API uses — one undoable step, reported
3514
- * through onLayoutChange — and every outcome is spoken: the tile's new
3515
- * cell, each neighbour it displaced, or why it was refused. Handled keys
3516
- * stop here so the renderer's own pixel nudge never fights the grid.
3517
- */
3518
- const memberHostAt = (target) => {
3519
- var _a, _b, _c;
3520
- const host = (_a = target === null || target === void 0 ? void 0 : target.closest) === null || _a === void 0 ? void 0 : _a.call(target, '.grafloria-node-host');
3521
- if (!host)
3522
- return null;
3523
- const id = (_b = host.getAttribute('data-node-id')) !== null && _b !== void 0 ? _b : '';
3524
- if (!((_c = group.members) !== null && _c !== void 0 ? _c : new Set()).has(id) || !diagram.getNode(id))
3525
- return null;
3526
- return { id, host };
3527
- };
3528
- const onFocusIn = (e) => {
3529
- const hit = memberHostAt(e.target);
3530
- if (!hit || disposed)
3531
- return;
3532
- if (focusedId !== hit.id || selectedId !== hit.id) {
3533
- focusedId = hit.id;
3534
- selectWidget(hit.id);
3535
- syncA11y();
3536
- }
3537
- };
3538
- const onKey = (e) => {
3539
- var _a, _b, _c, _d, _e, _f, _g;
3540
- if (disposed || gesture)
3541
- return;
3542
- const hit = memberHostAt(e.target);
3543
- if (!hit) {
3544
- // Tab reaches the diagram's own root (the svg) before any widget. An
3545
- // arrow or Enter there hands focus to the board's tab stop, so a
3546
- // keyboard user is never parked on "Diagram, 8 nodes" with nowhere to go.
3547
- const el = e.target;
3548
- 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'));
3549
- if (onRoot && (e.key.startsWith('Arrow') || e.key === 'Enter' || e.key === ' ')) {
3550
- const members = [...((_c = group.members) !== null && _c !== void 0 ? _c : [])].filter((id) => !!diagram.getNode(id) && !!hostOf(id));
3551
- const target = focusedId && members.includes(focusedId) ? focusedId : members[0];
3552
- if (target && handle.focusWidget(target)) {
3553
- e.preventDefault();
3554
- e.stopPropagation();
3555
- }
3556
- }
3557
- return;
3558
- }
3559
- const members = [...((_d = group.members) !== null && _d !== void 0 ? _d : [])].filter((id) => !!diagram.getNode(id) && !!hostOf(id));
3560
- if (e.key === 'Home' || e.key === 'End') {
3561
- const id = e.key === 'Home' ? members[0] : members[members.length - 1];
3562
- if (id)
3563
- handle.focusWidget(id);
3564
- e.preventDefault();
3565
- e.stopPropagation();
3566
- return;
3567
- }
3568
- const arrow = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[e.key];
3569
- if (!arrow)
3570
- return;
3571
- e.preventDefault();
3572
- e.stopPropagation();
3573
- if (isStatic)
3574
- return; // readable, not editable
3575
- const node = diagram.getNode(hit.id);
3576
- const item = engine.getItem(hit.id);
3577
- if (!node || !item)
3578
- return;
3579
- const name = nameOf(node);
3580
- if (((_e = node.state) === null || _e === void 0 ? void 0 : _e.locked) === true) {
3581
- live.announceError(`${name} is pinned`);
3582
- return;
3583
- }
3584
- const [dx, dy] = arrow;
3585
- const before = new Map(engine.getItems().map((i) => [i.id, { x: i.x, y: i.y, w: i.w, h: i.h }]));
3586
- const resize = e.shiftKey;
3587
- if (resize && ((_f = node.getMetadata) === null || _f === void 0 ? void 0 : _f.call(node, 'widgetResizable')) === false) {
3588
- live.announceError(`${name} cannot be resized`);
3589
- return;
3590
- }
3591
- if (!resize && ((_g = node.getMetadata) === null || _g === void 0 ? void 0 : _g.call(node, 'widgetMovable')) === false) {
3592
- live.announceError(`${name} cannot be moved`);
3593
- return;
3594
- }
3595
- // A one-cell keyboard step onto a neighbour would be refused by the
3596
- // pointer's anti-jitter gate (a 3-wide covering a third of its neighbour
3597
- // is not "more than half"). A key press is deliberate, so when the step
3598
- // is refused and a neighbour sits there, aim at the neighbour's far edge —
3599
- // the same swap a full drag lands on.
3600
- const stepOrSwap = () => __awaiter(this, void 0, void 0, function* () {
3601
- if (yield handle.moveTo(hit.id, item.x + dx, item.y + dy))
3602
- return true;
3603
- const probe = { x: item.x + dx, y: item.y + dy, w: item.w, h: item.h };
3604
- const c = engine
3605
- .getItems()
3606
- .find((o) => o.id !== hit.id && probe.x < o.x + o.w && o.x < probe.x + probe.w && probe.y < o.y + o.h && o.y < probe.y + probe.h);
3607
- if (!c)
3608
- return false;
3609
- const tx = dx > 0 ? c.x + c.w - item.w : dx < 0 ? c.x : item.x;
3610
- const ty = dy > 0 ? c.y + c.h - item.h : dy < 0 ? c.y : item.y;
3611
- return handle.moveTo(hit.id, tx, ty);
3612
- });
3613
- const op = resize ? handle.resizeTo(hit.id, item.w + dx, item.h + dy) : stepOrSwap();
3614
- void op.then((ok) => {
3615
- var _a, _b;
3616
- if (disposed)
3617
- return;
3618
- const after = engine.getItem(hit.id);
3619
- if (!ok || !after) {
3620
- live.announceError(resize ? `Cannot resize ${name} that way` : `Cannot move ${name} ${directionName(dx, dy)}`);
3621
- return;
3622
- }
3623
- const parts = [`${name} ${resize ? 'resized' : 'moved'} to ${describeCell(after)}`];
3624
- for (const other of engine.getItems()) {
3625
- if (other.id === hit.id)
3626
- continue;
3627
- const was = before.get(other.id);
3628
- if (!was || (was.x === other.x && was.y === other.y))
3629
- continue;
3630
- const o = diagram.getNode(other.id);
3631
- parts.push(`${o ? nameOf(o) : other.id} moved to ${describeCell(other)}`);
3632
- }
3633
- live.announce(parts.join('. '), 'polite', true);
3634
- syncA11y();
3635
- (_b = (_a = hostOf(hit.id)) === null || _a === void 0 ? void 0 : _a.focus) === null || _b === void 0 ? void 0 : _b.call(_a, { preventScroll: true });
3636
- });
3637
- };
3638
- api.container.addEventListener('focusin', onFocusIn);
3639
- api.container.addEventListener('keydown', onKey);
2973
+ api.container.addEventListener('pointermove', chrome.onHover, { passive: true });
2974
+ api.container.addEventListener('pointerleave', chrome.onHoverLeave, { passive: true });
2975
+ // -- keyboard operation (keyboard.ts) --------------------------------------
2976
+ const keyboard = createKeyboard(ctx, {
2977
+ handle: () => handle,
2978
+ live,
2979
+ nameOf,
2980
+ focusedId: () => focusedId,
2981
+ setFocusedId: (id) => {
2982
+ focusedId = id;
2983
+ },
2984
+ selectedId: () => selectedId,
2985
+ selectWidget,
2986
+ syncA11y: () => syncA11y(),
2987
+ gestureRunning: () => !!gesture,
2988
+ });
2989
+ api.container.addEventListener('focusin', keyboard.onFocusIn);
2990
+ api.container.addEventListener('keydown', keyboard.onKey);
3640
2991
  // -- tab tear-out -----------------------------------------------------------
3641
2992
  /**
3642
2993
  * A press that TRAVELLED on a tab drags the whole page onto this board, the
@@ -4039,8 +3390,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4039
3390
  engine.moveCheck(z.target.id, z.keep.x, z.keep.y, { gate: false });
4040
3391
  const ok = l.place(z.born);
4041
3392
  project();
4042
- placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove(); // the accent overlay says which half; the grey placeholder under it is noise
4043
- placeholder = null;
3393
+ hidePlaceholder(); // the accent overlay says which half; the grey placeholder under it is noise
4044
3394
  return ok;
4045
3395
  };
4046
3396
  /** The band a dock is promised: the cell it takes — a BOTTOM dock's lies past the last row, so its tint sits on the board's last rows instead. */
@@ -4056,8 +3406,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4056
3406
  l.place(z.cell, true); // DOCKING PUSHES SECTIONS: everything below the band moves down, solid or not
4057
3407
  insertRows(z.cell, l);
4058
3408
  project();
4059
- placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
4060
- placeholder = null;
3409
+ hidePlaceholder();
4061
3410
  return true;
4062
3411
  };
4063
3412
  let zone = { kind: 'board' };
@@ -4255,7 +3604,8 @@ export function bindDashboardGrid(api, group, options = {}) {
4255
3604
  const g = {
4256
3605
  kind: 'palette',
4257
3606
  id: node.id,
4258
- node,
3607
+ subject: 'node',
3608
+ entity: node,
4259
3609
  pointerId: (_b = event.pointerId) !== null && _b !== void 0 ? _b : null,
4260
3610
  started: false,
4261
3611
  downClient: { x: event.clientX, y: event.clientY },
@@ -4667,19 +4017,17 @@ export function bindDashboardGrid(api, group, options = {}) {
4667
4017
  },
4668
4018
  beginPaletteDrag,
4669
4019
  dispose() {
4670
- var _a, _b, _c, _d;
4020
+ var _a, _b, _c;
4671
4021
  if (disposed)
4672
4022
  return;
4673
4023
  cancelActiveGesture(false);
4674
4024
  disposed = true;
4675
4025
  peersOnCanvas().delete(selfPeer);
4676
4026
  unregisterTool();
4677
- api.container.removeEventListener('pointermove', onHover);
4678
- api.container.removeEventListener('pointerleave', onHoverLeave);
4679
- guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
4680
- api.container.removeEventListener('focusin', onFocusIn);
4681
- api.container.removeEventListener('keydown', onKey);
4682
- hostObserver.disconnect();
4027
+ api.container.removeEventListener('pointermove', chrome.onHover);
4028
+ api.container.removeEventListener('pointerleave', chrome.onHoverLeave);
4029
+ api.container.removeEventListener('focusin', keyboard.onFocusIn);
4030
+ api.container.removeEventListener('keydown', keyboard.onKey);
4683
4031
  containerObserver === null || containerObserver === void 0 ? void 0 : containerObserver.disconnect();
4684
4032
  tearing = null;
4685
4033
  for (const off of subs)
@@ -4688,23 +4036,8 @@ export function bindDashboardGrid(api, group, options = {}) {
4688
4036
  // split layout must not keep showing a resize corner it cannot act on.
4689
4037
  for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : [])
4690
4038
  (_c = (_b = hostOf(id)) === null || _b === void 0 ? void 0 : _b.querySelector(':scope > .axdb-rs')) === null || _c === void 0 ? void 0 : _c.remove();
4691
- placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
4692
- placeholder = null;
4693
- if (glideTimer)
4694
- clearTimeout(glideTimer);
4695
- for (const el of slabEls.values())
4696
- el.remove();
4697
- slabEls.clear();
4698
- hoverSlabs.clear();
4699
- for (const bg of groupBgs.values())
4700
- bg.remove();
4701
- groupBgs.clear();
4702
- // A rebind inside the 60 ms window (a layout switch right after an undo)
4703
- // disposed this binder with the timer pending — the host kept its
4704
- // lifted ghost for good (visual gate, nested-containers ⑤).
4705
- flushGhost();
4706
- flushCarried();
4707
- (_d = htmlLayer()) === null || _d === void 0 ? void 0 : _d.classList.remove('axdb-glide');
4039
+ chrome.dispose(); // slabs, frames, the refused cell, the carried subtree, the host observer, the static guard
4040
+ projection.dispose(); // the placeholder, the glide timer, a pending ghost
4708
4041
  api.container.style.cursor = '';
4709
4042
  },
4710
4043
  };
@@ -4716,7 +4049,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4716
4049
  rebuild(true);
4717
4050
  const layer = htmlLayer();
4718
4051
  if (layer)
4719
- hostObserver.observe(layer, { childList: true, subtree: true });
4052
+ chrome.observe(layer);
4720
4053
  containerObserver === null || containerObserver === void 0 ? void 0 : containerObserver.observe(api.container);
4721
4054
  return handle;
4722
4055
  }