@grafloria/element 0.4.39 → 0.4.41

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafloria/element",
3
- "version": "0.4.39",
3
+ "version": "0.4.41",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -107,7 +107,7 @@ interface CaptionDiagram {
107
107
  /**
108
108
  * Does the board that HOLDS this section paint section chrome? Only the grid
109
109
  * binder draws the slab overlay the band lives on, so a section inside a
110
- * SPLIT board has no band and must not reserve space for one.
110
+ * SPLIT board paints its bands too since 0.4.40 (it had none, and reserved none).
111
111
  *
112
112
  * Read from the parent group's persisted layout rather than from the live
113
113
  * binder registry: membership and metadata both exist before either binder is
@@ -51,7 +51,7 @@ export function pairOf(v, dflt) {
51
51
  /**
52
52
  * Does the board that HOLDS this section paint section chrome? Only the grid
53
53
  * binder draws the slab overlay the band lives on, so a section inside a
54
- * SPLIT board has no band and must not reserve space for one.
54
+ * SPLIT board paints its bands too since 0.4.40 (it had none, and reserved none).
55
55
  *
56
56
  * Read from the parent group's persisted layout rather than from the live
57
57
  * binder registry: membership and metadata both exist before either binder is
@@ -60,13 +60,15 @@ export function pairOf(v, dflt) {
60
60
  * first, and a child binds BEFORE its parent).
61
61
  */
62
62
  export function parentPaintsSectionChrome(diagram, group) {
63
- var _a, _b, _c, _d;
63
+ var _a, _b, _c;
64
64
  const groups = (_b = (_a = diagram.getGroups) === null || _a === void 0 ? void 0 : _a.call(diagram)) !== null && _b !== void 0 ? _b : [];
65
65
  for (const g of groups) {
66
66
  if (g === group || !((_c = g.members) === null || _c === void 0 ? void 0 : _c.has(group.id)))
67
67
  continue;
68
- const board = g.getMetadata('dashboardBoard');
69
- return ((_d = board === null || board === void 0 ? void 0 : board.layout) !== null && _d !== void 0 ? _d : 'grid') !== 'split';
68
+ // A grid board paints slabs and bands, and since 0.4.40 a split board
69
+ // paints them too (it lost the Operations band the moment the fluid demo
70
+ // switched to Split) — any board parent paints; only a view does not.
71
+ return true;
70
72
  }
71
73
  return false; // not a member of any board: a view, which carries no caption
72
74
  }
@@ -1051,6 +1051,30 @@ export function createDashboardHandle(ctx) {
1051
1051
  const label = (_e = pageSpec.title) !== null && _e !== void 0 ? _e : pageId;
1052
1052
  const size = { width: (_g = (_f = pg.size) === null || _f === void 0 ? void 0 : _f.width) !== null && _g !== void 0 ? _g : 0, height: ((_j = (_h = pg.size) === null || _h === void 0 ? void 0 : _h.height) !== null && _j !== void 0 ? _j : 0) + tabStripReserve(tabsOpts, 1) };
1053
1053
  const remaining = [...((_k = from.members) !== null && _k !== void 0 ? _k : [])].filter((m) => m !== pageId && !!model.getGroup(m));
1054
+ // The page that was showing when the gesture began. Undo puts the tab
1055
+ // back — and shows THAT page again, not the one the container switched to
1056
+ // when the tab left (the live walk: Filters showing, tear out, undo, Alerts
1057
+ // showing — 0.4.41). It goes FIRST in each move sequence so its undo runs
1058
+ // LAST, after the tab is back among the pages: a sync fired in between by
1059
+ // a neighbour's reflow would otherwise reset a page that is not there yet.
1060
+ const showing = ctx.activeTab.get(containerId);
1061
+ const showingAgain = () => new RegisterWidgetCommand({
1062
+ register: () => { },
1063
+ unregister: () => {
1064
+ var _a, _b, _c, _d;
1065
+ const cg = (_a = ctx.boardGroups.get(containerId)) !== null && _a !== void 0 ? _a : model.getGroup(containerId);
1066
+ const spec = specById.get(containerId);
1067
+ if (!showing || !cg || !spec || ctx.activeTab.get(containerId) === showing)
1068
+ return;
1069
+ if (!((_b = spec.widgets) !== null && _b !== void 0 ? _b : []).some((p) => p.id === showing) || !((_c = cg.members) === null || _c === void 0 ? void 0 : _c.has(showing)))
1070
+ return;
1071
+ ctx.activeTab.set(containerId, showing);
1072
+ spec.active = showing;
1073
+ const cw = (_d = cg.getMetadata('containerWidget')) !== null && _d !== void 0 ? _d : {};
1074
+ cg.setMetadata('containerWidget', Object.assign(Object.assign({}, cw), { active: showing }));
1075
+ teleport(ctx.container, () => { var _a; return (_a = ctx.syncTabs) === null || _a === void 0 ? void 0 : _a.call(ctx, containerId); });
1076
+ },
1077
+ }, 'register');
1054
1078
  return {
1055
1079
  arrivingId: W,
1056
1080
  label,
@@ -1120,6 +1144,7 @@ export function createDashboardHandle(ctx) {
1120
1144
  };
1121
1145
  const move = [
1122
1146
  new SequenceCommand('Move tab out', [
1147
+ showingAgain(),
1123
1148
  new AddGroupCommand(g),
1124
1149
  new RemoveFromGroupCommand(containerId, pageId),
1125
1150
  new AddToGroupCommand(W, pageId),
@@ -1202,6 +1227,7 @@ export function createDashboardHandle(ctx) {
1202
1227
  };
1203
1228
  const move = [
1204
1229
  new SequenceCommand('Move tab', [
1230
+ showingAgain(),
1205
1231
  new RemoveFromGroupCommand(containerId, pageId),
1206
1232
  new AddToGroupCommand(targetId, pageId),
1207
1233
  new RegisterWidgetCommand(joined, 'register'),
@@ -27,7 +27,7 @@ import type { DashboardGridApi, DashboardGridHandle, DashboardGridOptions } from
27
27
  import { type SplitNode } from './split-layout.js';
28
28
  /** Group metadata key the tree persists under. */
29
29
  export declare const SPLIT_TREE_KEY = "dashboardTree";
30
- export interface DashboardSplitOptions extends Pick<DashboardGridOptions, 'columns' | 'gap' | 'padding' | 'rtl' | 'fluid' | 'static' | 'dragHandle' | 'squeeze' | 'designHeight' | 'baseRowHeight' | 'dragOut' | 'removeZone' | 'onRemoveRequest' | 'onDropIn' | 'onGesture' | 'onSelect'> {
30
+ export interface DashboardSplitOptions extends Pick<DashboardGridOptions, 'columns' | 'gap' | 'padding' | 'rtl' | 'fluid' | 'static' | 'dragHandle' | 'squeeze' | 'designHeight' | 'baseRowHeight' | 'dragOut' | 'removeZone' | 'onRemoveRequest' | 'onDropIn' | 'onGesture' | 'onSelect' | 'renderCaption' | 'onCaptionAction'> {
31
31
  /** An authored tree. Default: the persisted one, else derived from the members' cells. */
32
32
  tree?: SplitNode | null;
33
33
  }
@@ -29,7 +29,7 @@ import { anyEdge, clearOtherSelections, dragHandleSelector, gripHostOf, gripOf,
29
29
  import { cellFromGridItem } from './grid-mapping.js';
30
30
  import { addSplitLeaf, cellsFromSplit, cloneSplit, dividersOf, groupRectsOf, insertSplitLeaf, moveSplitDivider, normalizeSplit, pathToLeaf, projectSplit, removeSplitLeaf, splitFromCells, splitLeaves, } from './split-layout.js';
31
31
  import { ensureDashboardKitStyles } from './styles.js';
32
- import { sectionCaptionReserve } from './caption.js';
32
+ import { captionKey, captionOfGroup, captionPainted, paintCaptionBand, sectionCaptionReserve, sizeCaptionBand } from './caption.js';
33
33
  /** Group metadata key the tree persists under. */
34
34
  export const SPLIT_TREE_KEY = 'dashboardTree';
35
35
  /**
@@ -250,11 +250,116 @@ export function bindDashboardSplit(api, group, options = {}) {
250
250
  writeRect(id, r);
251
251
  }
252
252
  syncDividers(tree);
253
+ syncSlabs();
253
254
  syncA11y();
254
255
  };
255
256
  // -- chrome: dividers + insertion line -------------------------------------
256
257
  const dividerEls = [];
257
258
  let insertion = null;
259
+ // -- section chrome: slabs and caption bands (0.4.40) ------------------------
260
+ // The grid board paints a slab per member section (the selection ring, the
261
+ // caption band); the split board painted none, so a captioned section lost
262
+ // its band the moment the board switched to split and its children sat bare
263
+ // in the pane. The same slabs here, minus the corner handle (a divider
264
+ // resizes a pane); the band is the section's drag handle (beginMemberDrag).
265
+ const slabEls = new Map();
266
+ const hoverSlabs = new Set();
267
+ const syncCaption = (el, id, grp, sectionH) => {
268
+ const cap = captionOfGroup(grp);
269
+ let band = el.querySelector(':scope > .axdb-slab-h');
270
+ if (!cap || !captionPainted(cap, isStatic)) {
271
+ band === null || band === void 0 ? void 0 : band.remove();
272
+ hoverSlabs.delete(el);
273
+ el.classList.remove('axdb-slab--hot');
274
+ el.removeAttribute('aria-label');
275
+ el.removeAttribute('role');
276
+ return;
277
+ }
278
+ if (cap.show === 'hover')
279
+ hoverSlabs.add(el);
280
+ else {
281
+ hoverSlabs.delete(el);
282
+ el.classList.remove('axdb-slab--hot');
283
+ }
284
+ const cctx = { rtl, static: isStatic, sectionH };
285
+ const key = captionKey(cap, cctx);
286
+ if (band && band.getAttribute('data-key') === key) {
287
+ sizeCaptionBand(band, cap, sectionH);
288
+ return;
289
+ }
290
+ band === null || band === void 0 ? void 0 : band.remove();
291
+ band = document.createElement('div');
292
+ el.prepend(band);
293
+ const render = options.renderCaption;
294
+ paintCaptionBand(band, cap, Object.assign(Object.assign(Object.assign({}, cctx), (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); } }));
295
+ band.setAttribute('data-key', key);
296
+ if (cap.text) {
297
+ el.setAttribute('role', 'group');
298
+ el.setAttribute('aria-label', cap.text);
299
+ }
300
+ else {
301
+ el.removeAttribute('role');
302
+ el.removeAttribute('aria-label');
303
+ }
304
+ };
305
+ const syncSlabs = () => {
306
+ var _a, _b;
307
+ if (disposed)
308
+ return;
309
+ const layer = htmlLayer();
310
+ if (!layer)
311
+ return;
312
+ const seen = new Set();
313
+ for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : new Set()) {
314
+ const grp = diagram.getGroup(id);
315
+ if (!grp || diagram.getNode(id))
316
+ continue;
317
+ seen.add(id);
318
+ let el = slabEls.get(id);
319
+ if (!el || el.parentElement !== layer) {
320
+ el === null || el === void 0 ? void 0 : el.remove();
321
+ el = document.createElement('div');
322
+ el.className = 'axdb-slab';
323
+ el.setAttribute('data-slab-id', id);
324
+ layer.appendChild(el);
325
+ slabEls.set(id, el);
326
+ }
327
+ const p = grp.position;
328
+ const sz = (_b = grp.size) !== null && _b !== void 0 ? _b : { width: 0, height: 0 };
329
+ el.style.left = `${p.x}px`;
330
+ el.style.top = `${p.y}px`;
331
+ el.style.width = `${sz.width}px`;
332
+ el.style.height = `${sz.height}px`;
333
+ el.classList.toggle('axdb-slab--selected', selectedId === id);
334
+ el.classList.toggle('axdb-slab--static', isStatic);
335
+ syncCaption(el, id, grp, sz.height);
336
+ }
337
+ for (const [id, el] of slabEls) {
338
+ if (!seen.has(id)) {
339
+ el.remove();
340
+ hoverSlabs.delete(el);
341
+ slabEls.delete(id);
342
+ }
343
+ }
344
+ };
345
+ /** A `show: 'hover'` band is an overlay that takes no pointer while hidden: the board marks the section under the pointer itself. */
346
+ const markHotSection = (clientX, clientY) => {
347
+ if (!hoverSlabs.size)
348
+ return;
349
+ for (const el of hoverSlabs) {
350
+ const r = el.getBoundingClientRect();
351
+ el.classList.toggle('axdb-slab--hot', clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom);
352
+ }
353
+ };
354
+ const onHoverMove = (e) => {
355
+ if (disposed || gesture)
356
+ return;
357
+ markHotSection(e.clientX, e.clientY);
358
+ };
359
+ const onHoverLeave = () => {
360
+ for (const el of hoverSlabs)
361
+ el.classList.remove('axdb-slab--hot');
362
+ };
258
363
  const syncDividers = (tree) => {
259
364
  const layer = htmlLayer();
260
365
  for (const el of dividerEls)
@@ -391,6 +496,7 @@ export function bindDashboardSplit(api, group, options = {}) {
391
496
  return;
392
497
  selectedId = id;
393
498
  syncA11y();
499
+ syncSlabs();
394
500
  (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id);
395
501
  };
396
502
  let selfPeerRef = null;
@@ -419,23 +525,24 @@ export function bindDashboardSplit(api, group, options = {}) {
419
525
  layer.addEventListener('pointerdown', staticGuard);
420
526
  };
421
527
  const syncA11y = () => {
422
- var _a, _b, _c, _d, _e;
528
+ var _a, _b, _c, _d, _e, _f;
423
529
  if (disposed)
424
530
  return;
425
531
  ensureStaticGuard();
426
532
  const order = splitLeaves(paintedTree()).filter((id) => !!diagram.getNode(id));
427
- if (selectedId && !order.includes(selectedId))
533
+ // A selected SECTION (a member group) is not a tab stop, but it is a selection — keep it (0.4.40).
534
+ if (selectedId && !order.includes(selectedId) && !((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(selectedId))
428
535
  selectedId = undefined;
429
536
  // No corner handles on a split board: size comes from the dividers. A host
430
537
  // that carried the grid's handle (a board switched live) sheds it here.
431
- for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : [])
432
- (_c = (_b = hostOf(id)) === null || _b === void 0 ? void 0 : _b.querySelector(':scope > .axdb-rs')) === null || _c === void 0 ? void 0 : _c.remove();
538
+ for (const id of (_b = group.members) !== null && _b !== void 0 ? _b : [])
539
+ (_d = (_c = hostOf(id)) === null || _c === void 0 ? void 0 : _c.querySelector(':scope > .axdb-rs')) === null || _d === void 0 ? void 0 : _d.remove();
433
540
  // The painted grip (or none), on every leaf host.
434
541
  for (const id of order) {
435
542
  const host = hostOf(id);
436
543
  const node = diagram.getNode(id);
437
544
  if (host && node)
438
- syncGrip(host, gripOf(dragHandle), ((_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, 'widgetMovable')) !== false);
545
+ syncGrip(host, gripOf(dragHandle), ((_e = node.state) === null || _e === void 0 ? void 0 : _e.locked) !== true && !isStatic && ((_f = node.getMetadata) === null || _f === void 0 ? void 0 : _f.call(node, 'widgetMovable')) !== false);
439
546
  }
440
547
  if (focusedId && !order.includes(focusedId))
441
548
  focusedId = undefined;
@@ -643,19 +750,24 @@ export function bindDashboardSplit(api, group, options = {}) {
643
750
  id: `dashboard-split:${group.id}:${++binderSeq}`,
644
751
  priority: 2,
645
752
  hitTest(ev, hit) {
646
- var _a;
753
+ var _a, _b, _c, _d, _e, _f;
647
754
  if (disposed)
648
755
  return false;
649
756
  if (gesture || forwardSlab)
650
757
  return true;
651
758
  if (!ownsPress(api.container, diagram, ev, hit))
652
759
  return false;
760
+ // A press on one of OUR sections' caption bands is ours by the DOM.
761
+ const chrome = (_c = (_b = (_a = ev.source) === null || _a === void 0 ? void 0 : _a.target) === null || _b === void 0 ? void 0 : _b.closest) === null || _c === void 0 ? void 0 : _c.call(_b, '.axdb-slab > .axdb-slab-h');
762
+ const chromeId = (_d = chrome === null || chrome === void 0 ? void 0 : chrome.parentElement) === null || _d === void 0 ? void 0 : _d.getAttribute('data-slab-id');
763
+ if (chromeId && ((_e = group.members) !== null && _e !== void 0 ? _e : new Set()).has(chromeId))
764
+ return true;
653
765
  if (hit.node)
654
- return ((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(hit.node.id);
766
+ return ((_f = group.members) !== null && _f !== void 0 ? _f : new Set()).has(hit.node.id);
655
767
  return worldInsideBoard(ev.world.x, ev.world.y);
656
768
  },
657
769
  onPointerDown(ev, hit) {
658
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
770
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
659
771
  if (gesture)
660
772
  return;
661
773
  const target = ((_b = (_a = ev.source) === null || _a === void 0 ? void 0 : _a.target) !== null && _b !== void 0 ? _b : null);
@@ -691,6 +803,19 @@ export function bindDashboardSplit(api, group, options = {}) {
691
803
  const gripId = (_e = (_d = gripHostOf(target)) === null || _d === void 0 ? void 0 : _d.getAttribute('data-node-id')) !== null && _e !== void 0 ? _e : null;
692
804
  const onGrip = !!gripId && ((_f = group.members) !== null && _f !== void 0 ? _f : new Set()).has(gripId);
693
805
  const sectionHandle = (_g = target === null || target === void 0 ? void 0 : target.closest) === null || _g === void 0 ? void 0 : _g.call(target, '.axdb-slab > .axdb-rs');
806
+ // A SECTION's caption band: the press selects the section, a travel
807
+ // drags it to another pane's edge (beginMemberDrag). An action button
808
+ // never reaches here — it is pass-through (see ownsPress).
809
+ const captionBand = (_h = target === null || target === void 0 ? void 0 : target.closest) === null || _h === void 0 ? void 0 : _h.call(target, '.axdb-slab > .axdb-slab-h');
810
+ const captionId = (_k = (_j = captionBand === null || captionBand === void 0 ? void 0 : captionBand.parentElement) === null || _j === void 0 ? void 0 : _j.getAttribute('data-slab-id')) !== null && _k !== void 0 ? _k : null;
811
+ if (captionId && ((_l = group.members) !== null && _l !== void 0 ? _l : new Set()).has(captionId)) {
812
+ selectWidget(captionId);
813
+ api.render();
814
+ const src = ev.source;
815
+ if (!isStatic && typeof (src === null || src === void 0 ? void 0 : src.clientX) === 'number' && typeof (src === null || src === void 0 ? void 0 : src.clientY) === 'number')
816
+ beginMemberDrag(captionId, src);
817
+ return;
818
+ }
694
819
  if ((!hit.node && !onGrip) || sectionHandle) {
695
820
  // OUR OWN empty band or corner handle, and we are a section of a
696
821
  // parent board: select the section there; an edge or the handle
@@ -698,7 +823,7 @@ export function bindDashboardSplit(api, group, options = {}) {
698
823
  // forwards the pointer sequence (the grid binder does the same).
699
824
  const parent = parentPeerOf(api.container, group.id);
700
825
  if ((parent === null || parent === void 0 ? void 0 : parent.selectMember) && (sectionHandle || worldInsideBoard(ev.world.x, ev.world.y))) {
701
- const own = ((_h = sectionHandle === null || sectionHandle === void 0 ? void 0 : sectionHandle.parentElement) === null || _h === void 0 ? void 0 : _h.getAttribute('data-slab-id')) === group.id;
826
+ const own = ((_m = sectionHandle === null || sectionHandle === void 0 ? void 0 : sectionHandle.parentElement) === null || _m === void 0 ? void 0 : _m.getAttribute('data-slab-id')) === group.id;
702
827
  parent.selectMember(group.id);
703
828
  if (!isStatic && parent.beginSlabResize) {
704
829
  const f = frame();
@@ -710,7 +835,7 @@ export function bindDashboardSplit(api, group, options = {}) {
710
835
  }
711
836
  return;
712
837
  }
713
- (_k = (_j = diagram).clearSelection) === null || _k === void 0 ? void 0 : _k.call(_j);
838
+ (_p = (_o = diagram).clearSelection) === null || _p === void 0 ? void 0 : _p.call(_o);
714
839
  selectWidget(undefined);
715
840
  api.render();
716
841
  return;
@@ -719,9 +844,9 @@ export function bindDashboardSplit(api, group, options = {}) {
719
844
  if (!node)
720
845
  return;
721
846
  selectWidget(node.id); // a press selects, whether or not it starts a gesture
722
- if (((_l = node.state) === null || _l === void 0 ? void 0 : _l.locked) === true || isStatic)
847
+ if (((_q = node.state) === null || _q === void 0 ? void 0 : _q.locked) === true || isStatic)
723
848
  return;
724
- if (((_m = node.getMetadata) === null || _m === void 0 ? void 0 : _m.call(node, 'widgetMovable')) === false)
849
+ if (((_r = node.getMetadata) === null || _r === void 0 ? void 0 : _r.call(node, 'widgetMovable')) === false)
725
850
  return;
726
851
  // Drag-handle mode: only the handle (the caption strip, a custom element
727
852
  // or the painted grip) lifts a widget out — see pressOnDragHandle.
@@ -779,6 +904,8 @@ export function bindDashboardSplit(api, group, options = {}) {
779
904
  },
780
905
  };
781
906
  const unregisterTool = registerTool(tool);
907
+ api.container.addEventListener('pointermove', onHoverMove);
908
+ api.container.addEventListener('pointerleave', onHoverLeave);
782
909
  // -- a tab torn out of a container on THIS board (0.4.37) --------------------
783
910
  // The split board REFUSED every tear-out ("no cells to drop a page into"),
784
911
  // so on a split board a tab press was a dead click — no chip, no reorder,
@@ -799,7 +926,7 @@ export function bindDashboardSplit(api, group, options = {}) {
799
926
  return true;
800
927
  };
801
928
  const beginTearOut = (pageId, fromGroupId, ev, plan) => {
802
- var _a;
929
+ var _a, _b, _c, _d;
803
930
  if (disposed || isStatic || gesture || tearing)
804
931
  return false;
805
932
  const from = diagram.getGroup(fromGroupId);
@@ -808,6 +935,9 @@ export function bindDashboardSplit(api, group, options = {}) {
808
935
  const tree0 = readTree();
809
936
  tearing = true;
810
937
  const doc = (_a = api.container.ownerDocument) !== null && _a !== void 0 ? _a : document;
938
+ const bodyUserSelect = doc.body.style.userSelect;
939
+ doc.body.style.userSelect = 'none';
940
+ (_d = (_c = (_b = doc.getSelection) === null || _b === void 0 ? void 0 : _b.call(doc)) === null || _c === void 0 ? void 0 : _c.removeAllRanges) === null || _d === void 0 ? void 0 : _d.call(_c);
811
941
  const chip = doc.createElement('div');
812
942
  chip.className = 'axdb-drag-chip axdb-tab-chip';
813
943
  chip.textContent = plan.label;
@@ -923,6 +1053,7 @@ export function bindDashboardSplit(api, group, options = {}) {
923
1053
  hideJoin();
924
1054
  showInsertion(null);
925
1055
  plan.markDrop(null, null);
1056
+ doc.body.style.userSelect = bodyUserSelect;
926
1057
  if (disposed) {
927
1058
  tearing = false;
928
1059
  return;
@@ -981,6 +1112,109 @@ export function bindDashboardSplit(api, group, options = {}) {
981
1112
  api.render();
982
1113
  return true;
983
1114
  };
1115
+ // -- a tab GROUP (or a section) moved by its strip's empty space (0.4.40) ----
1116
+ // The peer answered "no cells to move a section across", so on a split
1117
+ // board a tab group could not be moved at all — only its tabs could leave
1118
+ // it. The board's own drop model moves it: a chip carries its name, the
1119
+ // insertion line marks the pane edge under the pointer, release re-inserts
1120
+ // the group there (dropped on a neighbour's far edge, the two swap sides).
1121
+ // Nothing on the canvas moves until the release; one history step.
1122
+ const beginMemberDrag = (id, ev) => {
1123
+ var _a, _b, _c, _d, _e, _f;
1124
+ if (disposed || isStatic || gesture || tearing)
1125
+ return false;
1126
+ const tree0 = readTree();
1127
+ if (!tree0 || !splitLeaves(tree0).includes(id))
1128
+ return false;
1129
+ const grp = diagram.getGroup(id);
1130
+ if (!grp)
1131
+ return false;
1132
+ tearing = true;
1133
+ const doc = (_a = api.container.ownerDocument) !== null && _a !== void 0 ? _a : document;
1134
+ // The press came through the DOM (a strip's empty space) or a caption
1135
+ // band, not through the renderer's guard: without this the travel
1136
+ // selected every label it crossed (tab names, table cells).
1137
+ (_b = ev.preventDefault) === null || _b === void 0 ? void 0 : _b.call(ev);
1138
+ const bodyUserSelect = doc.body.style.userSelect;
1139
+ doc.body.style.userSelect = 'none';
1140
+ (_e = (_d = (_c = doc.getSelection) === null || _c === void 0 ? void 0 : _c.call(doc)) === null || _d === void 0 ? void 0 : _d.removeAllRanges) === null || _e === void 0 ? void 0 : _e.call(_d);
1141
+ const meta = ((_f = grp.getMetadata('containerWidget')) !== null && _f !== void 0 ? _f : {});
1142
+ const label = typeof meta.title === 'string' && meta.title ? meta.title : grp.name || id;
1143
+ const chip = doc.createElement('div');
1144
+ chip.className = 'axdb-drag-chip axdb-tab-chip';
1145
+ chip.textContent = label;
1146
+ const moveChip = (cx, cy) => {
1147
+ chip.style.left = `${cx + 6}px`;
1148
+ chip.style.top = `${cy + 6}px`;
1149
+ };
1150
+ const down = { x: ev.clientX, y: ev.clientY };
1151
+ let started = false;
1152
+ let target = null;
1153
+ const detach = () => {
1154
+ window.removeEventListener('pointermove', onMove, true);
1155
+ window.removeEventListener('pointerup', onUp, true);
1156
+ window.removeEventListener('pointercancel', onCancel, true);
1157
+ window.removeEventListener('keydown', onKey, true);
1158
+ };
1159
+ const finish = (commit) => {
1160
+ detach();
1161
+ chip.remove();
1162
+ showInsertion(null);
1163
+ tearing = false;
1164
+ doc.body.style.userSelect = bodyUserSelect;
1165
+ if (disposed || !started)
1166
+ return;
1167
+ const t = commit ? target : null;
1168
+ if (!t) {
1169
+ api.renderNow();
1170
+ fire({ type: 'cancel', kind: 'move', nodeId: id, changed: false });
1171
+ return;
1172
+ }
1173
+ const side = rtl && (t.side === 'left' || t.side === 'right') ? (t.side === 'left' ? 'right' : 'left') : t.side;
1174
+ const after = normalizeSplit(insertSplitLeaf(tree0, id, targetRef(t), side));
1175
+ const changed = JSON.stringify(after) !== JSON.stringify(normalizeSplit(tree0));
1176
+ const paint = () => {
1177
+ if (disposed)
1178
+ return;
1179
+ project(readTree());
1180
+ api.renderNow();
1181
+ };
1182
+ const p = changed ? commitTree(tree0, after) : undefined;
1183
+ paint();
1184
+ if (p && typeof p.then === 'function')
1185
+ void p.then(paint, () => undefined);
1186
+ if (changed)
1187
+ live.announce(`${label} moved ${side === 'left' || side === 'top' ? 'before' : 'after'} ${targetName(t)}`, 'polite', true);
1188
+ fire({ type: changed ? 'commit' : 'cancel', kind: 'move', nodeId: id, changed });
1189
+ };
1190
+ const onMove = (e) => {
1191
+ if (disposed)
1192
+ return finish(false);
1193
+ if (!started) {
1194
+ if (Math.abs(e.clientX - down.x) + Math.abs(e.clientY - down.y) < DRAG_THRESHOLD)
1195
+ return;
1196
+ started = true;
1197
+ doc.body.appendChild(chip);
1198
+ }
1199
+ moveChip(e.clientX, e.clientY);
1200
+ const w = toWorld(e.clientX, e.clientY);
1201
+ target = worldInsideBoard(w.x, w.y) ? dropTargetAt(tree0, w.x, w.y, id) : null;
1202
+ showInsertion(target ? insertionRect(target.rect, target.side) : null);
1203
+ chip.classList.toggle('axdb-out', !target);
1204
+ api.render();
1205
+ };
1206
+ const onUp = () => finish(true);
1207
+ const onCancel = () => finish(false);
1208
+ const onKey = (e) => {
1209
+ if (e.key === 'Escape')
1210
+ finish(false);
1211
+ };
1212
+ window.addEventListener('pointermove', onMove, true);
1213
+ window.addEventListener('pointerup', onUp, true);
1214
+ window.addEventListener('pointercancel', onCancel, true);
1215
+ window.addEventListener('keydown', onKey, true);
1216
+ return true;
1217
+ };
984
1218
  // -- palette drag-in --------------------------------------------------------
985
1219
  const beginPaletteDrag = (node, spec, event) => {
986
1220
  var _a;
@@ -1427,6 +1661,12 @@ export function bindDashboardSplit(api, group, options = {}) {
1427
1661
  for (const el of dividerEls)
1428
1662
  el.remove();
1429
1663
  dividerEls.length = 0;
1664
+ api.container.removeEventListener('pointermove', onHoverMove);
1665
+ api.container.removeEventListener('pointerleave', onHoverLeave);
1666
+ for (const el of slabEls.values())
1667
+ el.remove();
1668
+ slabEls.clear();
1669
+ hoverSlabs.clear();
1430
1670
  insertion === null || insertion === void 0 ? void 0 : insertion.remove();
1431
1671
  insertion = null;
1432
1672
  api.container.style.cursor = '';
@@ -1455,8 +1695,9 @@ export function bindDashboardSplit(api, group, options = {}) {
1455
1695
  resizeMemberBy: () => ({ changed: false }),
1456
1696
  // A torn-out page becomes a PANE (0.4.37) — see beginTearOut.
1457
1697
  tearOutMember: (pageId, fromGroupId, ev, plan) => beginTearOut(pageId, fromGroupId, ev, plan),
1458
- // …and it has no cells to move a section across: a strip press stays a selection.
1459
- dragMember: () => false,
1698
+ // A tab group (or a section) moved by its strip's empty space becomes a
1699
+ // pane elsewhere (0.4.40) see beginMemberDrag.
1700
+ dragMember: (id, ev) => beginMemberDrag(id, ev),
1460
1701
  containsWorld: (x, y) => worldInsideBoard(x, y),
1461
1702
  containsWorldExtended: (x, y) => worldInsideBoard(x, y),
1462
1703
  frameArea: () => {
@@ -57,12 +57,20 @@ onDrag) {
57
57
  // cover it completely, so without this there is nowhere to press to select
58
58
  // it — and an unselected section shows no corner handle, which made the
59
59
  // container impossible to resize by hand.
60
- strip.onpointerdown = (e) => {
60
+ // A real listener, not the `onpointerdown` IDL attribute: the attribute is
61
+ // not an event handler under jsdom, so the strip's empty-space press — the
62
+ // whole group's drag handle — could never be driven by a spec. Repaints
63
+ // replace the previous listener rather than stacking one per paint.
64
+ const withSelect = strip;
65
+ if (withSelect.__axdbSelect)
66
+ strip.removeEventListener('pointerdown', withSelect.__axdbSelect);
67
+ withSelect.__axdbSelect = (e) => {
61
68
  var _a;
62
69
  if ((_a = e.target) === null || _a === void 0 ? void 0 : _a.closest('.axdb-tab'))
63
70
  return;
64
71
  onSelectContainer === null || onSelectContainer === void 0 ? void 0 : onSelectContainer(e);
65
72
  };
73
+ strip.addEventListener('pointerdown', withSelect.__axdbSelect);
66
74
  for (const p of pages) {
67
75
  const b = doc.createElement('button');
68
76
  b.type = 'button';