@grafloria/element 0.4.53 → 0.4.55

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,7 @@ 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
- }
259
151
  let binderSeq = 0;
260
152
  export function bindDashboardGrid(api, group, options = {}) {
261
153
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
@@ -442,13 +334,10 @@ export function bindDashboardGrid(api, group, options = {}) {
442
334
  let disposed = false;
443
335
  /** Reentrancy guard: our own derived frame writes must not re-project. */
444
336
  let writing = false;
445
- let placeholder = null;
446
337
  /** Foreign tile currently adopted from another binder's gesture. */
447
338
  let adoptedGhostId = null;
448
339
  /** The tab page this board is currently tearing out of a container, if any. */
449
340
  let tearing = null;
450
- let glideTimer = null;
451
- let ghostTimer = null;
452
341
  /**
453
342
  * OUR OWN CAPTION RESERVE: a section carrying a caption gives the band's
454
343
  * pixels up at the top of its frame, so no child may take them — and the
@@ -574,172 +463,56 @@ export function bindDashboardGrid(api, group, options = {}) {
574
463
  grp.setMetadata('gridItem', gridItemFromCell(cell));
575
464
  }
576
465
  };
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) {
466
+ // -- the board, as its modules see it (tile first, step 4b-i) --------------
467
+ const ctx = {
468
+ api,
469
+ group,
470
+ diagram,
471
+ options,
472
+ gap,
473
+ padding,
474
+ baseRowHeight,
475
+ minRowHeight,
476
+ overflow,
477
+ engine: () => engine,
478
+ frame,
479
+ geom,
480
+ rows,
481
+ sizing: () => sizing,
482
+ designH: () => designH,
483
+ rtl: () => rtl,
484
+ isStatic: () => isStatic,
485
+ disposed: () => disposed,
486
+ htmlLayer,
487
+ hostOf,
488
+ memberEntity,
489
+ sizeOf,
490
+ ghostId: () => adoptedGhostId !== null && adoptedGhostId !== void 0 ? adoptedGhostId : ((gesture === null || gesture === void 0 ? void 0 : gesture.started) ? gesture.id : null),
491
+ write: (fn) => {
622
492
  writing = true;
623
493
  try {
624
- diagram.runSystemWrite(() => group.setFrame({ x: f.x, y: f.y, width: f.width, height: target }));
494
+ fn();
625
495
  }
626
496
  finally {
627
497
  writing = false;
628
498
  }
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
- }
499
+ },
742
500
  };
501
+ // -- projection: cells -> pixels (project.ts) ------------------------------
502
+ const projection = createProjection(ctx, { afterProject: () => syncSlabs() });
503
+ const { writeRect, enforceBoardHeight, project, syncPlaceholder, hidePlaceholder, armGlide, disarmGlideSoon, flushGhost, setGhost } = projection;
504
+ // -- chrome: slabs, frames, captions, handles, cursors (chrome.ts) ---------
505
+ const chrome = createChrome(ctx, {
506
+ selectedId: () => selectedId,
507
+ syncA11y: (only) => syncA11y(only),
508
+ grabbing: () => !!slabGesture,
509
+ gestureRunning: () => !!gesture,
510
+ memberGroupAt: (x, y) => memberGroupAt(x, y),
511
+ slabEdgesNear: (grp, x, y) => slabEdgesNear(grp, x, y),
512
+ dragHandle: () => dragHandle,
513
+ wantHandles,
514
+ });
515
+ const { syncSlabs, syncHandles, setCarried, flushCarried, showRefusal, ensureStaticGuard } = chrome;
743
516
  // -- resize handles ---------------------------------------------------------
744
517
  /** The accessible name of a widget: its title, else its kind, else its id. */
745
518
  const nameOf = (node) => {
@@ -813,82 +586,6 @@ export function bindDashboardGrid(api, group, options = {}) {
813
586
  host.classList.toggle('axdb-selected', id === selectedId);
814
587
  }
815
588
  };
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
589
  // -- gesture snapshot / commit ---------------------------------------------
893
590
  const snapshotAll = () => {
894
591
  const cells = new Map();
@@ -947,23 +644,38 @@ export function bindDashboardGrid(api, group, options = {}) {
947
644
  */
948
645
  let pendingDrop = null;
949
646
  const onMemberAdded = (id) => {
950
- var _a, _b;
647
+ var _a, _b, _c;
951
648
  if (disposed)
952
649
  return;
953
- if (pendingDrop && pendingDrop !== id && engine.getItem(pendingDrop) && !((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(pendingDrop)) {
954
- engine.remove(pendingDrop);
650
+ // The cell a waiting tile held for an arriving member of ANOTHER id. The
651
+ // removal settles the board — the tiles the drop pushed float back into
652
+ // the hole — so the member cannot simply be added there: it collided and
653
+ // auto-positioned to the left edge (Quantia's "lands on the cell it was
654
+ // aimed at"). It enters at the bottom edge instead and takes the cell
655
+ // gatelessly, pushing them down again, all inside this one call.
656
+ let claim = null;
657
+ if (pendingDrop && pendingDrop !== id) {
658
+ const ph = engine.getItem(pendingDrop);
659
+ if (ph && !((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(pendingDrop)) {
660
+ claim = { x: ph.x, y: ph.y };
661
+ engine.remove(pendingDrop);
662
+ }
955
663
  }
956
664
  pendingDrop = null;
957
665
  if (!engine.getItem(id)) {
958
666
  const item = itemFor(id);
959
- let placed = engine.add(item);
667
+ let placed = claim ? engine.add(Object.assign(Object.assign({}, item), { x: 0, y: engine.rows(), autoPosition: false })) : engine.add(item);
668
+ if (placed && claim) {
669
+ engine.moveCheck(id, claim.x, claim.y, { gate: false, pushSolid: !!placed.solid });
670
+ placed = (_b = engine.getItem(id)) !== null && _b !== void 0 ? _b : placed;
671
+ }
960
672
  if (!placed && capacity !== undefined) {
961
673
  // Membership is a document fact (an undo just restored it, say); a
962
674
  // bounded board must not strand the node invisible. Lift the capacity
963
675
  // for this adoption — it floors at the content on the next refresh.
964
676
  capacity = undefined;
965
677
  engine = engineFrom([...engine.getItems().map((i) => (Object.assign({}, i))), item]);
966
- placed = (_b = engine.getItem(id)) !== null && _b !== void 0 ? _b : null;
678
+ placed = (_c = engine.getItem(id)) !== null && _c !== void 0 ? _c : null;
967
679
  refreshCapacity();
968
680
  }
969
681
  if (placed)
@@ -1150,54 +862,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1150
862
  const s = sizeOf(grp);
1151
863
  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 };
1152
864
  };
1153
- /**
1154
- * SECTION CHROME. A section (member group) paints no card of its own, so it
1155
- * had nothing to press: a click on its empty band cleared the selection and
1156
- * its frame had no handle and no edge — a section with many children could
1157
- * not be selected at all, and could only be resized by pulling a child
1158
- * (Quantia, Groups page). Every section gets a pointer-transparent overlay
1159
- * in the HTML layer that wears the selection ring and, while selected, the
1160
- * corner handle; its frame edges answer the resize cursor.
1161
- */
1162
- const slabEls = new Map();
1163
- /**
1164
- * GROUP FRAME (0.4.43): a TAB CONTAINER wears a frame by default — its slab
1165
- * is bordered and a tinted surface lies under its pages, first in the layer
1166
- * so the tiles paint over it. A page torn out with two widgets under its tab
1167
- * read as a strip floating over two loose cards: nothing said the second
1168
- * card was the tab's. A plain section keeps its invisible slab.
1169
- */
1170
- const groupBgs = new Map();
1171
- const syncGroupBg = (layer, id, on, x, y, w, h) => {
1172
- var _a;
1173
- let bg = (_a = groupBgs.get(id)) !== null && _a !== void 0 ? _a : null;
1174
- if (!on) {
1175
- bg === null || bg === void 0 ? void 0 : bg.remove();
1176
- groupBgs.delete(id);
1177
- return;
1178
- }
1179
- if (!bg || bg.parentElement !== layer) {
1180
- bg === null || bg === void 0 ? void 0 : bg.remove();
1181
- bg = document.createElement('div');
1182
- bg.className = 'axdb-group-bg';
1183
- bg.setAttribute('data-group-bg', id);
1184
- layer.prepend(bg);
1185
- groupBgs.set(id, bg);
1186
- }
1187
- bg.style.left = `${x}px`;
1188
- bg.style.top = `${y}px`;
1189
- bg.style.width = `${w}px`;
1190
- bg.style.height = `${h}px`;
1191
- };
1192
- const isTabsGroup = (grp) => { var _a; return ((_a = grp.getMetadata('containerWidget')) === null || _a === void 0 ? void 0 : _a.layout) === 'tabs'; };
1193
- /**
1194
- * A TAB CONTAINER's frame is its drag handle (0.4.43): its 8-px margin —
1195
- * under the strip, beside the pages — moves the group, so the edge-resize
1196
- * zone shrinks to 3 px there (the corner handle still resizes). A section's
1197
- * edges keep the full grip: its empty band is a drop target, not a handle.
1198
- */
1199
- const TAB_FRAME_GRIP = 3;
1200
- const edgeGripFor = (grp) => (isTabsGroup(grp) ? TAB_FRAME_GRIP : EDGE_GRIP);
1201
865
  /**
1202
866
  * BESIDE (0.4.45): a WIDGET dragged onto a tab container's outer band — a
1203
867
  * fifth of its body, the same bands a tab's split uses — lands next to it
@@ -1290,180 +954,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1290
954
  }
1291
955
  besideOn(g.id, g.spans, z, row);
1292
956
  };
1293
- /**
1294
- * CARRIED (0.4.43): a group dragged by its strip or band moves as ONE thing.
1295
- * The held TILE is transition-exempt (the ghost), but a group has no host of
1296
- * its own: its strip jumped to the pointer while its pages' tiles GLIDED
1297
- * after it — on the live demo the content trailed the strip by up to 140 px
1298
- * at every step. Everything in the group's subtree — tiles, strips, slabs,
1299
- * the surface — is exempt for the gesture and, like the ghost, through the
1300
- * drop write; then the glides resume.
1301
- */
1302
- const carriedEls = new Set();
1303
- let carriedTimer = null;
1304
- const subtreeIds = (id) => {
1305
- var _a;
1306
- const groups = [];
1307
- const nodes = [];
1308
- const queue = [id];
1309
- const seen = new Set();
1310
- while (queue.length) {
1311
- const cur = queue.shift();
1312
- if (seen.has(cur))
1313
- continue;
1314
- seen.add(cur);
1315
- const grp = diagram.getGroup(cur);
1316
- if (grp) {
1317
- groups.push(cur);
1318
- for (const m of (_a = grp.members) !== null && _a !== void 0 ? _a : [])
1319
- queue.push(m);
1320
- }
1321
- else if (diagram.getNode(cur))
1322
- nodes.push(cur);
1323
- }
1324
- return { groups, nodes };
1325
- };
1326
- const cssId = (id) => (typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(id) : id.replace(/"/g, '\\"'));
1327
- const setCarried = (id, on) => {
1328
- const layer = htmlLayer();
1329
- if (!layer)
1330
- return;
1331
- if (carriedTimer) {
1332
- clearTimeout(carriedTimer);
1333
- carriedTimer = null;
1334
- }
1335
- if (on) {
1336
- const { groups, nodes } = subtreeIds(id);
1337
- const els = [];
1338
- for (const n of nodes) {
1339
- const h = hostOf(n);
1340
- if (h)
1341
- els.push(h);
1342
- }
1343
- for (const g of groups) {
1344
- 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)}"]`)));
1345
- }
1346
- for (const el of els) {
1347
- el.classList.add('axdb-carried');
1348
- carriedEls.add(el);
1349
- }
1350
- return;
1351
- }
1352
- carriedTimer = setTimeout(() => {
1353
- for (const el of carriedEls)
1354
- el.classList.remove('axdb-carried');
1355
- carriedEls.clear();
1356
- carriedTimer = null;
1357
- }, 60);
1358
- };
1359
- const flushCarried = () => {
1360
- if (carriedTimer)
1361
- clearTimeout(carriedTimer);
1362
- carriedTimer = null;
1363
- for (const el of carriedEls)
1364
- el.classList.remove('axdb-carried');
1365
- carriedEls.clear();
1366
- };
1367
- let slabLayer = null;
1368
- const syncSlabs = () => {
1369
- var _a, _b, _c;
1370
- if (disposed)
1371
- return;
1372
- const layer = (slabLayer === null || slabLayer === void 0 ? void 0 : slabLayer.isConnected) ? slabLayer : (slabLayer = htmlLayer());
1373
- if (!layer)
1374
- return;
1375
- const seen = new Set();
1376
- for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
1377
- const grp = diagram.getGroup(id);
1378
- if (!grp || diagram.getNode(id))
1379
- continue;
1380
- seen.add(id);
1381
- let el = slabEls.get(id);
1382
- if (!el || el.parentElement !== layer) {
1383
- el === null || el === void 0 ? void 0 : el.remove();
1384
- el = document.createElement('div');
1385
- el.className = 'axdb-slab';
1386
- el.setAttribute('data-slab-id', id);
1387
- const rs = document.createElement('div');
1388
- rs.className = 'axdb-rs';
1389
- rs.setAttribute('title', 'Resize section');
1390
- el.appendChild(rs);
1391
- layer.appendChild(el);
1392
- slabEls.set(id, el);
1393
- }
1394
- const p = grp.position;
1395
- const sz = sizeOf(grp);
1396
- el.style.left = `${p.x}px`;
1397
- el.style.top = `${p.y}px`;
1398
- el.style.width = `${sz.width}px`;
1399
- el.style.height = `${sz.height}px`;
1400
- el.classList.toggle('axdb-slab--selected', selectedId === id);
1401
- el.classList.toggle('axdb-slab--static', isStatic);
1402
- (_b = el.querySelector(':scope > .axdb-rs')) === null || _b === void 0 ? void 0 : _b.classList.toggle('axdb-rs--rtl', rtl);
1403
- const tabs = isTabsGroup(grp);
1404
- el.classList.toggle('axdb-slab--tabs', tabs);
1405
- syncGroupBg(layer, id, tabs, p.x, p.y, sz.width, sz.height);
1406
- syncCaption(el, id, grp, sz.height);
1407
- }
1408
- for (const [id, el] of slabEls) {
1409
- if (!seen.has(id)) {
1410
- el.remove();
1411
- hoverSlabs.delete(el);
1412
- slabEls.delete(id);
1413
- (_c = groupBgs.get(id)) === null || _c === void 0 ? void 0 : _c.remove();
1414
- groupBgs.delete(id);
1415
- }
1416
- }
1417
- };
1418
- /**
1419
- * THE CAPTION BAND of a section, on its slab overlay. Painted from the
1420
- * group's persisted caption; repainted only when its identity changes (the
1421
- * options, RTL, static, the tier) so a custom `renderCaption` is not run
1422
- * per frame. The band takes the pointer (the slab itself does not): a press
1423
- * on it selects the section, an action fires, pass-through reaches content.
1424
- */
1425
- const syncCaption = (el, id, grp, sectionH) => {
1426
- const cap = captionOfGroup(grp);
1427
- let band = el.querySelector(':scope > .axdb-slab-h');
1428
- if (!cap || !captionPainted(cap, isStatic)) {
1429
- band === null || band === void 0 ? void 0 : band.remove();
1430
- hoverSlabs.delete(el);
1431
- el.classList.remove('axdb-slab--hot');
1432
- el.removeAttribute('aria-label');
1433
- el.removeAttribute('role');
1434
- return;
1435
- }
1436
- if (cap.show === 'hover')
1437
- hoverSlabs.add(el);
1438
- else {
1439
- hoverSlabs.delete(el);
1440
- el.classList.remove('axdb-slab--hot');
1441
- }
1442
- const ctx = { rtl, static: isStatic, sectionH };
1443
- const key = captionKey(cap, ctx);
1444
- if (band && band.getAttribute('data-key') === key) {
1445
- sizeCaptionBand(band, cap, sectionH); // the tier follows the live size
1446
- return;
1447
- }
1448
- band === null || band === void 0 ? void 0 : band.remove();
1449
- band = document.createElement('div');
1450
- el.prepend(band);
1451
- const render = options.renderCaption;
1452
- 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); } }));
1453
- band.setAttribute('data-key', key);
1454
- // A named group, so the caption text is the section's name in the
1455
- // accessibility tree rather than a stray label on an unnamed div. (The
1456
- // band is not a tab stop yet — the actions are deliberately out of the
1457
- // tab order so a board keeps exactly ONE stop; see the plan.)
1458
- if (cap.text) {
1459
- el.setAttribute('role', 'group');
1460
- el.setAttribute('aria-label', cap.text);
1461
- }
1462
- else {
1463
- el.removeAttribute('role');
1464
- el.removeAttribute('aria-label');
1465
- }
1466
- };
1467
957
  const insideMemberGroupFrame = (x, y) => {
1468
958
  var _a;
1469
959
  for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
@@ -1545,29 +1035,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1545
1035
  capturePointer(slabGesture.pointerId);
1546
1036
  api.container.style.cursor = 'grabbing';
1547
1037
  };
1548
- /** The cell a slab move asked for and could not have — painted so the refusal is visible; null clears it. */
1549
- let refusal = null;
1550
- const showRefusal = (cell, w, h) => {
1551
- const layer = htmlLayer();
1552
- if (!cell || !layer) {
1553
- refusal === null || refusal === void 0 ? void 0 : refusal.remove();
1554
- refusal = null;
1555
- api.container.style.cursor = slabGesture ? 'grabbing' : '';
1556
- return;
1557
- }
1558
- if (!refusal || refusal.parentElement !== layer) {
1559
- refusal === null || refusal === void 0 ? void 0 : refusal.remove();
1560
- refusal = document.createElement('div');
1561
- refusal.className = 'axdb-ph axdb-ph--no';
1562
- layer.prepend(refusal);
1563
- }
1564
- const r = cellToRect({ x: cell.x, y: cell.y, w, h }, frame(), geom(), rows());
1565
- refusal.style.left = `${r.x}px`;
1566
- refusal.style.top = `${r.y}px`;
1567
- refusal.style.width = `${r.width}px`;
1568
- refusal.style.height = `${r.height}px`;
1569
- api.container.style.cursor = 'not-allowed';
1570
- };
1571
1038
  const slabMove = (ev) => {
1572
1039
  const g = slabGesture;
1573
1040
  if (!g)
@@ -1769,8 +1236,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1769
1236
  releasePointer(g.pointerId);
1770
1237
  api.container.style.cursor = '';
1771
1238
  (_a = g.chip) === null || _a === void 0 ? void 0 : _a.remove();
1772
- placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
1773
- placeholder = null;
1239
+ hidePlaceholder();
1774
1240
  };
1775
1241
  const commitGesture = (g) => {
1776
1242
  var _a;
@@ -3375,253 +2841,24 @@ export function bindDashboardGrid(api, group, options = {}) {
3375
2841
  onToolUp();
3376
2842
  };
3377
2843
  const unregisterTool = registerTool(tool);
3378
- /**
3379
- * Edge affordance: the cursor says which border a press would take, the
3380
- * way gridstack's invisible edge handles do. One passive listener on the
3381
- * container; the corner handle keeps its own cursor from the stylesheet.
3382
- */
3383
- let hoverHost = null;
3384
- /**
3385
- * A `show: 'hover'` caption cannot ride CSS `:hover`: the slab overlay takes
3386
- * no pointer (by design — it must never steal a press), and while the band
3387
- * is hidden it takes none either, so nothing in the section is ever hovered
3388
- * in CSS terms. The binder already tracks the pointer; it marks the section
3389
- * under it instead.
3390
- */
3391
- /** Only the sections carrying a `show: 'hover'` band — usually none, so the
3392
- * pointer handler costs nothing on a board that has no hover caption. */
3393
- const hoverSlabs = new Set();
3394
- const markHotSection = (clientX, clientY) => {
3395
- if (!hoverSlabs.size)
3396
- return;
3397
- for (const el of hoverSlabs) {
3398
- const r = el.getBoundingClientRect();
3399
- el.classList.toggle('axdb-slab--hot', clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom);
3400
- }
3401
- };
3402
- const onHoverLeave = () => {
3403
- for (const el of hoverSlabs)
3404
- el.classList.remove('axdb-slab--hot');
3405
- };
3406
- const onHover = (e) => {
3407
- var _a, _b, _c, _d, _e, _f, _g, _h;
3408
- if (disposed || gesture)
3409
- return;
3410
- markHotSection(e.clientX, e.clientY);
3411
- // The event may target the host, its content, or (when a host's content
3412
- // is pointer-transparent) the canvas under it — find the member host by
3413
- // the pointer's position in that case.
3414
- 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');
3415
- if (!host) {
3416
- for (const id of (_c = group.members) !== null && _c !== void 0 ? _c : []) {
3417
- const h = hostOf(id);
3418
- if (!h)
3419
- continue;
3420
- const r = h.getBoundingClientRect();
3421
- if (e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom) {
3422
- host = h;
3423
- break;
3424
- }
3425
- }
3426
- }
3427
- if (hoverHost && hoverHost !== host) {
3428
- hoverHost.style.cursor = '';
3429
- hoverHost.removeAttribute('data-axdb-edge'); // the affordance follows the pointer off a tile
3430
- }
3431
- hoverHost = host;
3432
- if (!host) {
3433
- // No tile under the pointer: a SECTION's frame edge still says resize.
3434
- const wpt = ((_d = api.viewport) === null || _d === void 0 ? void 0 : _d.clientToWorld) ? api.viewport.clientToWorld(e.clientX, e.clientY, api.container.getBoundingClientRect()) : null;
3435
- const sid = wpt ? memberGroupAt(wpt.x, wpt.y) : null;
3436
- const grp = sid ? diagram.getGroup(sid) : undefined;
3437
- const c = grp && wpt && !isStatic ? cursorFor(slabEdgesNear(grp, wpt.x, wpt.y)) : '';
3438
- if (!slabGesture)
3439
- api.container.style.cursor = c;
3440
- return;
3441
- }
3442
- if (!slabGesture && api.container.style.cursor)
3443
- api.container.style.cursor = '';
3444
- const id = (_e = host.getAttribute('data-node-id')) !== null && _e !== void 0 ? _e : '';
3445
- if (!((_f = group.members) !== null && _f !== void 0 ? _f : new Set()).has(id))
3446
- return;
3447
- const node = diagram.getNode(id);
3448
- 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;
3449
- const cursor = resizable ? cursorFor(edgesNear(host, e.clientX, e.clientY)) : '';
3450
- // The affordance rides on an ATTRIBUTE, not the inline style: a repaint
3451
- // rewrites the host's style and used to clear the cursor mid-hover, and
3452
- // content that sets its own cursor (a chart canvas) hid it — the
3453
- // stylesheet applies the attribute's cursor to the host and everything in it.
3454
- if (cursor)
3455
- host.setAttribute('data-axdb-edge', cursor);
3456
- else
3457
- host.removeAttribute('data-axdb-edge');
3458
- };
3459
- api.container.addEventListener('pointermove', onHover, { passive: true });
3460
- api.container.addEventListener('pointerleave', onHoverLeave, { passive: true });
3461
- /**
3462
- * STATIC BOARDS LET CONTENT BE CLICKED. The renderer prevents the default of
3463
- * every press a tool claims, which cancels the compatibility mouse events —
3464
- * a chart inside a read-only board could not be clicked. So under `static`
3465
- * a press inside a member's CONTENT (not on kit chrome) is stopped on the
3466
- * HTML layer, in the bubble phase: the content has already received it, the
3467
- * renderer never does, nothing is prevented.
3468
- */
3469
- const staticGuard = (e) => {
3470
- var _a, _b, _c, _d;
3471
- if (!isStatic || disposed)
3472
- return;
3473
- const t = e.target;
3474
- const host = (_a = t === null || t === void 0 ? void 0 : t.closest) === null || _a === void 0 ? void 0 : _a.call(t, '.grafloria-node-host');
3475
- if (!host || !((_b = group.members) !== null && _b !== void 0 ? _b : new Set()).has((_c = host.getAttribute('data-node-id')) !== null && _c !== void 0 ? _c : ''))
3476
- return;
3477
- 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'))
3478
- return;
3479
- e.stopPropagation();
3480
- };
3481
- let guardedLayer = null;
3482
- const ensureStaticGuard = () => {
3483
- // One lookup, ever: the host observer budgets container lookups per
3484
- // repaint, and the layer element lives as long as the instance.
3485
- if (guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.isConnected)
3486
- return;
3487
- const layer = htmlLayer();
3488
- if (!layer)
3489
- return;
3490
- guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
3491
- guardedLayer = layer;
3492
- layer.addEventListener('pointerdown', staticGuard);
3493
- };
3494
- /**
3495
- * KEYBOARD OPERATION (WCAG 2.1.1, and the non-drag alternative 2.5.7 asks
3496
- * for): on a focused member, arrows move it one cell, Shift+arrows resize it
3497
- * one cell, Home/End jump to the first/last member. Every move and resize is
3498
- * the same programmatic gesture the API uses — one undoable step, reported
3499
- * through onLayoutChange — and every outcome is spoken: the tile's new
3500
- * cell, each neighbour it displaced, or why it was refused. Handled keys
3501
- * stop here so the renderer's own pixel nudge never fights the grid.
3502
- */
3503
- const memberHostAt = (target) => {
3504
- var _a, _b, _c;
3505
- const host = (_a = target === null || target === void 0 ? void 0 : target.closest) === null || _a === void 0 ? void 0 : _a.call(target, '.grafloria-node-host');
3506
- if (!host)
3507
- return null;
3508
- const id = (_b = host.getAttribute('data-node-id')) !== null && _b !== void 0 ? _b : '';
3509
- if (!((_c = group.members) !== null && _c !== void 0 ? _c : new Set()).has(id) || !diagram.getNode(id))
3510
- return null;
3511
- return { id, host };
3512
- };
3513
- const onFocusIn = (e) => {
3514
- const hit = memberHostAt(e.target);
3515
- if (!hit || disposed)
3516
- return;
3517
- if (focusedId !== hit.id || selectedId !== hit.id) {
3518
- focusedId = hit.id;
3519
- selectWidget(hit.id);
3520
- syncA11y();
3521
- }
3522
- };
3523
- const onKey = (e) => {
3524
- var _a, _b, _c, _d, _e, _f, _g;
3525
- if (disposed || gesture)
3526
- return;
3527
- const hit = memberHostAt(e.target);
3528
- if (!hit) {
3529
- // Tab reaches the diagram's own root (the svg) before any widget. An
3530
- // arrow or Enter there hands focus to the board's tab stop, so a
3531
- // keyboard user is never parked on "Diagram, 8 nodes" with nowhere to go.
3532
- const el = e.target;
3533
- 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'));
3534
- if (onRoot && (e.key.startsWith('Arrow') || e.key === 'Enter' || e.key === ' ')) {
3535
- const members = [...((_c = group.members) !== null && _c !== void 0 ? _c : [])].filter((id) => !!diagram.getNode(id) && !!hostOf(id));
3536
- const target = focusedId && members.includes(focusedId) ? focusedId : members[0];
3537
- if (target && handle.focusWidget(target)) {
3538
- e.preventDefault();
3539
- e.stopPropagation();
3540
- }
3541
- }
3542
- return;
3543
- }
3544
- const members = [...((_d = group.members) !== null && _d !== void 0 ? _d : [])].filter((id) => !!diagram.getNode(id) && !!hostOf(id));
3545
- if (e.key === 'Home' || e.key === 'End') {
3546
- const id = e.key === 'Home' ? members[0] : members[members.length - 1];
3547
- if (id)
3548
- handle.focusWidget(id);
3549
- e.preventDefault();
3550
- e.stopPropagation();
3551
- return;
3552
- }
3553
- const arrow = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[e.key];
3554
- if (!arrow)
3555
- return;
3556
- e.preventDefault();
3557
- e.stopPropagation();
3558
- if (isStatic)
3559
- return; // readable, not editable
3560
- const node = diagram.getNode(hit.id);
3561
- const item = engine.getItem(hit.id);
3562
- if (!node || !item)
3563
- return;
3564
- const name = nameOf(node);
3565
- if (((_e = node.state) === null || _e === void 0 ? void 0 : _e.locked) === true) {
3566
- live.announceError(`${name} is pinned`);
3567
- return;
3568
- }
3569
- const [dx, dy] = arrow;
3570
- const before = new Map(engine.getItems().map((i) => [i.id, { x: i.x, y: i.y, w: i.w, h: i.h }]));
3571
- const resize = e.shiftKey;
3572
- if (resize && ((_f = node.getMetadata) === null || _f === void 0 ? void 0 : _f.call(node, 'widgetResizable')) === false) {
3573
- live.announceError(`${name} cannot be resized`);
3574
- return;
3575
- }
3576
- if (!resize && ((_g = node.getMetadata) === null || _g === void 0 ? void 0 : _g.call(node, 'widgetMovable')) === false) {
3577
- live.announceError(`${name} cannot be moved`);
3578
- return;
3579
- }
3580
- // A one-cell keyboard step onto a neighbour would be refused by the
3581
- // pointer's anti-jitter gate (a 3-wide covering a third of its neighbour
3582
- // is not "more than half"). A key press is deliberate, so when the step
3583
- // is refused and a neighbour sits there, aim at the neighbour's far edge —
3584
- // the same swap a full drag lands on.
3585
- const stepOrSwap = () => __awaiter(this, void 0, void 0, function* () {
3586
- if (yield handle.moveTo(hit.id, item.x + dx, item.y + dy))
3587
- return true;
3588
- const probe = { x: item.x + dx, y: item.y + dy, w: item.w, h: item.h };
3589
- const c = engine
3590
- .getItems()
3591
- .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);
3592
- if (!c)
3593
- return false;
3594
- const tx = dx > 0 ? c.x + c.w - item.w : dx < 0 ? c.x : item.x;
3595
- const ty = dy > 0 ? c.y + c.h - item.h : dy < 0 ? c.y : item.y;
3596
- return handle.moveTo(hit.id, tx, ty);
3597
- });
3598
- const op = resize ? handle.resizeTo(hit.id, item.w + dx, item.h + dy) : stepOrSwap();
3599
- void op.then((ok) => {
3600
- var _a, _b;
3601
- if (disposed)
3602
- return;
3603
- const after = engine.getItem(hit.id);
3604
- if (!ok || !after) {
3605
- live.announceError(resize ? `Cannot resize ${name} that way` : `Cannot move ${name} ${directionName(dx, dy)}`);
3606
- return;
3607
- }
3608
- const parts = [`${name} ${resize ? 'resized' : 'moved'} to ${describeCell(after)}`];
3609
- for (const other of engine.getItems()) {
3610
- if (other.id === hit.id)
3611
- continue;
3612
- const was = before.get(other.id);
3613
- if (!was || (was.x === other.x && was.y === other.y))
3614
- continue;
3615
- const o = diagram.getNode(other.id);
3616
- parts.push(`${o ? nameOf(o) : other.id} moved to ${describeCell(other)}`);
3617
- }
3618
- live.announce(parts.join('. '), 'polite', true);
3619
- syncA11y();
3620
- (_b = (_a = hostOf(hit.id)) === null || _a === void 0 ? void 0 : _a.focus) === null || _b === void 0 ? void 0 : _b.call(_a, { preventScroll: true });
3621
- });
3622
- };
3623
- api.container.addEventListener('focusin', onFocusIn);
3624
- api.container.addEventListener('keydown', onKey);
2844
+ api.container.addEventListener('pointermove', chrome.onHover, { passive: true });
2845
+ api.container.addEventListener('pointerleave', chrome.onHoverLeave, { passive: true });
2846
+ // -- keyboard operation (keyboard.ts) --------------------------------------
2847
+ const keyboard = createKeyboard(ctx, {
2848
+ handle: () => handle,
2849
+ live,
2850
+ nameOf,
2851
+ focusedId: () => focusedId,
2852
+ setFocusedId: (id) => {
2853
+ focusedId = id;
2854
+ },
2855
+ selectedId: () => selectedId,
2856
+ selectWidget,
2857
+ syncA11y: () => syncA11y(),
2858
+ gestureRunning: () => !!gesture,
2859
+ });
2860
+ api.container.addEventListener('focusin', keyboard.onFocusIn);
2861
+ api.container.addEventListener('keydown', keyboard.onKey);
3625
2862
  // -- tab tear-out -----------------------------------------------------------
3626
2863
  /**
3627
2864
  * A press that TRAVELLED on a tab drags the whole page onto this board, the
@@ -4024,8 +3261,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4024
3261
  engine.moveCheck(z.target.id, z.keep.x, z.keep.y, { gate: false });
4025
3262
  const ok = l.place(z.born);
4026
3263
  project();
4027
- placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove(); // the accent overlay says which half; the grey placeholder under it is noise
4028
- placeholder = null;
3264
+ hidePlaceholder(); // the accent overlay says which half; the grey placeholder under it is noise
4029
3265
  return ok;
4030
3266
  };
4031
3267
  /** 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. */
@@ -4041,8 +3277,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4041
3277
  l.place(z.cell, true); // DOCKING PUSHES SECTIONS: everything below the band moves down, solid or not
4042
3278
  insertRows(z.cell, l);
4043
3279
  project();
4044
- placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
4045
- placeholder = null;
3280
+ hidePlaceholder();
4046
3281
  return true;
4047
3282
  };
4048
3283
  let zone = { kind: 'board' };
@@ -4652,19 +3887,17 @@ export function bindDashboardGrid(api, group, options = {}) {
4652
3887
  },
4653
3888
  beginPaletteDrag,
4654
3889
  dispose() {
4655
- var _a, _b, _c, _d;
3890
+ var _a, _b, _c;
4656
3891
  if (disposed)
4657
3892
  return;
4658
3893
  cancelActiveGesture(false);
4659
3894
  disposed = true;
4660
3895
  peersOnCanvas().delete(selfPeer);
4661
3896
  unregisterTool();
4662
- api.container.removeEventListener('pointermove', onHover);
4663
- api.container.removeEventListener('pointerleave', onHoverLeave);
4664
- guardedLayer === null || guardedLayer === void 0 ? void 0 : guardedLayer.removeEventListener('pointerdown', staticGuard);
4665
- api.container.removeEventListener('focusin', onFocusIn);
4666
- api.container.removeEventListener('keydown', onKey);
4667
- hostObserver.disconnect();
3897
+ api.container.removeEventListener('pointermove', chrome.onHover);
3898
+ api.container.removeEventListener('pointerleave', chrome.onHoverLeave);
3899
+ api.container.removeEventListener('focusin', keyboard.onFocusIn);
3900
+ api.container.removeEventListener('keydown', keyboard.onKey);
4668
3901
  containerObserver === null || containerObserver === void 0 ? void 0 : containerObserver.disconnect();
4669
3902
  tearing = null;
4670
3903
  for (const off of subs)
@@ -4673,23 +3906,8 @@ export function bindDashboardGrid(api, group, options = {}) {
4673
3906
  // split layout must not keep showing a resize corner it cannot act on.
4674
3907
  for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : [])
4675
3908
  (_c = (_b = hostOf(id)) === null || _b === void 0 ? void 0 : _b.querySelector(':scope > .axdb-rs')) === null || _c === void 0 ? void 0 : _c.remove();
4676
- placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
4677
- placeholder = null;
4678
- if (glideTimer)
4679
- clearTimeout(glideTimer);
4680
- for (const el of slabEls.values())
4681
- el.remove();
4682
- slabEls.clear();
4683
- hoverSlabs.clear();
4684
- for (const bg of groupBgs.values())
4685
- bg.remove();
4686
- groupBgs.clear();
4687
- // A rebind inside the 60 ms window (a layout switch right after an undo)
4688
- // disposed this binder with the timer pending — the host kept its
4689
- // lifted ghost for good (visual gate, nested-containers ⑤).
4690
- flushGhost();
4691
- flushCarried();
4692
- (_d = htmlLayer()) === null || _d === void 0 ? void 0 : _d.classList.remove('axdb-glide');
3909
+ chrome.dispose(); // slabs, frames, the refused cell, the carried subtree, the host observer, the static guard
3910
+ projection.dispose(); // the placeholder, the glide timer, a pending ghost
4693
3911
  api.container.style.cursor = '';
4694
3912
  },
4695
3913
  };
@@ -4701,7 +3919,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4701
3919
  rebuild(true);
4702
3920
  const layer = htmlLayer();
4703
3921
  if (layer)
4704
- hostObserver.observe(layer, { childList: true, subtree: true });
3922
+ chrome.observe(layer);
4705
3923
  containerObserver === null || containerObserver === void 0 ? void 0 : containerObserver.observe(api.container);
4706
3924
  return handle;
4707
3925
  }