@grafloria/element 0.4.34 → 0.4.36

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.
@@ -1348,6 +1348,13 @@ export function bindDashboardGrid(api, group, options = {}) {
1348
1348
  },
1349
1349
  move: false,
1350
1350
  };
1351
+ // A TOP or LEFT edge moves the slab's origin, and the engine refuses to
1352
+ // move a locked tile: the move never landed, only the resize did, and a
1353
+ // section pulled up by its caption grew DOWNWARD a row per pointer step
1354
+ // — 18 rows for a 2-row pull (identification round, F16). Unlocked for
1355
+ // its own gesture, as a move is; relocked on release.
1356
+ if (edges.n || edges.w)
1357
+ it.locked = false;
1351
1358
  capturePointer(slabGesture.pointerId);
1352
1359
  api.container.style.cursor = cursorFor(edges);
1353
1360
  };
@@ -1388,6 +1395,29 @@ export function bindDashboardGrid(api, group, options = {}) {
1388
1395
  if (it)
1389
1396
  it.locked = true;
1390
1397
  };
1398
+ /** The cell a slab move asked for and could not have — painted so the refusal is visible; null clears it. */
1399
+ let refusal = null;
1400
+ const showRefusal = (cell, w, h) => {
1401
+ const layer = htmlLayer();
1402
+ if (!cell || !layer) {
1403
+ refusal === null || refusal === void 0 ? void 0 : refusal.remove();
1404
+ refusal = null;
1405
+ api.container.style.cursor = slabGesture ? 'grabbing' : '';
1406
+ return;
1407
+ }
1408
+ if (!refusal || refusal.parentElement !== layer) {
1409
+ refusal === null || refusal === void 0 ? void 0 : refusal.remove();
1410
+ refusal = document.createElement('div');
1411
+ refusal.className = 'axdb-ph axdb-ph--no';
1412
+ layer.prepend(refusal);
1413
+ }
1414
+ const r = cellToRect({ x: cell.x, y: cell.y, w, h }, frame(), geom(), rows());
1415
+ refusal.style.left = `${r.x}px`;
1416
+ refusal.style.top = `${r.y}px`;
1417
+ refusal.style.width = `${r.width}px`;
1418
+ refusal.style.height = `${r.height}px`;
1419
+ api.container.style.cursor = 'not-allowed';
1420
+ };
1391
1421
  const slabMove = (ev) => {
1392
1422
  const g = slabGesture;
1393
1423
  if (!g)
@@ -1406,8 +1436,26 @@ export function bindDashboardGrid(api, group, options = {}) {
1406
1436
  if (g.move) {
1407
1437
  // The section's top-left follows the pointer by the offset it was grabbed at.
1408
1438
  const cell = pointToCell(ev.world.x + g.grab.dx, ev.world.y + g.grab.dy, f, gg, rows(), it.w);
1409
- if ((cell.x !== it.x || cell.y !== it.y) && engine.moveCheck(g.id, cell.x, cell.y, { gate: false }).changed)
1410
- project();
1439
+ if (cell.x !== it.x || cell.y !== it.y) {
1440
+ // The cell under the pointer, else the nearest legal one — a widget
1441
+ // slides along its row past a locked tile; a section grabbed 60 px
1442
+ // from its left edge used to have its right edge refused by the
1443
+ // locked panel at every cell and simply not move, with nothing on
1444
+ // screen to say why (identification round, D6/D7). When even that
1445
+ // fails the wanted cell is painted as refused.
1446
+ // Along its ROW only: a section dragged LEFT that wandered DOWN its
1447
+ // column (the row search) read as the wrong tile moving.
1448
+ const was = { x: it.x, y: it.y };
1449
+ const moved = engine.moveCheck(g.id, cell.x, cell.y, { gate: false }).changed || placeOnRow(g.id, cell.x, cell.y, it.w);
1450
+ if (moved)
1451
+ project();
1452
+ // STUCK: the pointer asks for another cell and the slab did not budge
1453
+ // (placeOnRow counts "already on a legal cell" as placed) — paint what
1454
+ // was asked for as refused.
1455
+ const now = engine.getItem(g.id);
1456
+ const stuck = !!now && now.x === was.x && now.y === was.y && (cell.x !== was.x || cell.y !== was.y);
1457
+ showRefusal(stuck ? cell : null, it.w, it.h);
1458
+ }
1411
1459
  syncPlaceholder();
1412
1460
  return;
1413
1461
  }
@@ -1455,10 +1503,10 @@ export function bindDashboardGrid(api, group, options = {}) {
1455
1503
  if (!g)
1456
1504
  return;
1457
1505
  slabGesture = null;
1506
+ showRefusal(null, 0, 0);
1458
1507
  releasePointer(g.pointerId);
1459
1508
  api.container.style.cursor = '';
1460
- if (g.move)
1461
- relockSlab(g.id);
1509
+ relockSlab(g.id);
1462
1510
  if (!g.started) {
1463
1511
  engine.endGesture();
1464
1512
  return;
@@ -1486,8 +1534,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1486
1534
  slabGesture = null;
1487
1535
  releasePointer(g.pointerId);
1488
1536
  api.container.style.cursor = '';
1489
- if (g.move)
1490
- relockSlab(g.id);
1537
+ relockSlab(g.id);
1491
1538
  if (g.started)
1492
1539
  engine.cancelGesture();
1493
1540
  else
@@ -2507,6 +2554,14 @@ export function bindDashboardGrid(api, group, options = {}) {
2507
2554
  return {
2508
2555
  groupId: group.id,
2509
2556
  baseline: () => startCells,
2557
+ cell: () => {
2558
+ const it = engine.getItem(node.id);
2559
+ return it ? { x: it.x, y: it.y, w: it.w, h: it.h } : null;
2560
+ },
2561
+ rect: () => {
2562
+ const it = engine.getItem(node.id);
2563
+ return it ? cellToRect(it, frame(), geom(), rows()) : null;
2564
+ },
2510
2565
  place: (cell) => {
2511
2566
  if (!engine.getItem(node.id) && !engine.add({ id: node.id, x: 0, y: engine.rows(), w: cell.w, h: cell.h }))
2512
2567
  return false;
@@ -2737,23 +2792,37 @@ export function bindDashboardGrid(api, group, options = {}) {
2737
2792
  selfPeerRef = selfPeer;
2738
2793
  // The board's gap, for chrome that must fit BETWEEN tiles (the outside grip tab).
2739
2794
  api.container.style.setProperty('--axdb-gap', `${gap}px`);
2795
+ /** A press on one of our containers' strips, claimed so the renderer stays out of it. */
2796
+ let stripPress = false;
2740
2797
  const tool = {
2741
2798
  id: `dashboard-grid:${group.id}:${++binderSeq}`,
2742
2799
  priority: 2, // point-specific claim — outranks mode-style tools (see ext/tools.ts)
2743
2800
  hitTest(ev, hit) {
2744
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
2801
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
2745
2802
  if (disposed)
2746
2803
  return false;
2747
- if (gesture || slabGesture || forwardSlab)
2804
+ if (gesture || slabGesture || forwardSlab || stripPress)
2748
2805
  return true; // own the rest of an in-flight gesture
2806
+ // A press on one of OUR containers' TAB STRIPS is ours to CLAIM and then
2807
+ // leave alone: the strip's own listeners run the click, the tab drag and
2808
+ // the container move. Left unclaimed (ownsPress calls a strip "content")
2809
+ // the renderer's ladder cleared the selection and armed its empty-canvas
2810
+ // PAN, and the camera slid 10–20 px under every tab drag — the reorder
2811
+ // mark lost after a detour, a release outside the canvas that committed,
2812
+ // the strip end reading as the right band (identification round, 2026-09-09).
2813
+ const stripEl = (_d = (_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-tabs')) !== null && _d !== void 0 ? _d : null;
2814
+ if (stripEl) {
2815
+ const cid = stripEl.getAttribute('data-tabs-id');
2816
+ return !!cid && ((_e = group.members) !== null && _e !== void 0 ? _e : new Set()).has(cid) && api.container.contains(stripEl);
2817
+ }
2749
2818
  if (!ownsPress(api.container, diagram, ev, hit))
2750
2819
  return false;
2751
2820
  // A press on one of OUR sections' caption bands is ours by the DOM: a
2752
2821
  // 'tab' band sits above the frame, over the gap or the tile above,
2753
2822
  // where the geometry says otherwise.
2754
- 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, .axdb-slab > .axdb-rs');
2755
- const chromeId = (_d = chrome === null || chrome === void 0 ? void 0 : chrome.parentElement) === null || _d === void 0 ? void 0 : _d.getAttribute('data-slab-id');
2756
- const mine = !!chromeId && ((_e = group.members) !== null && _e !== void 0 ? _e : new Set()).has(chromeId);
2823
+ const chrome = (_h = (_g = (_f = ev.source) === null || _f === void 0 ? void 0 : _f.target) === null || _g === void 0 ? void 0 : _g.closest) === null || _h === void 0 ? void 0 : _h.call(_g, '.axdb-slab > .axdb-slab-h, .axdb-slab > .axdb-rs');
2824
+ const chromeId = (_j = chrome === null || chrome === void 0 ? void 0 : chrome.parentElement) === null || _j === void 0 ? void 0 : _j.getAttribute('data-slab-id');
2825
+ const mine = !!chromeId && ((_k = group.members) !== null && _k !== void 0 ? _k : new Set()).has(chromeId);
2757
2826
  if (mine)
2758
2827
  return true;
2759
2828
  // Someone else's SECTION HANDLE is never ours to claim. A tab
@@ -2766,7 +2835,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2766
2835
  if (chromeId && (chrome === null || chrome === void 0 ? void 0 : chrome.classList.contains('axdb-rs')))
2767
2836
  return false;
2768
2837
  if (hit.node) {
2769
- if (((_f = group.members) !== null && _f !== void 0 ? _f : new Set()).has(hit.node.id))
2838
+ if (((_l = group.members) !== null && _l !== void 0 ? _l : new Set()).has(hit.node.id))
2770
2839
  return true;
2771
2840
  // A press on a tile that belongs to a NESTED board must reach that
2772
2841
  // board's tool. The dead-zone claim below deadens the slab's EMPTY
@@ -2775,7 +2844,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2775
2844
  // parent's tool won the registration-order tie and the child's resize
2776
2845
  // never armed; grid-options binds parent-first and worked by
2777
2846
  // accident).
2778
- for (const p of (_g = BOARD_REGISTRY.get(api.container)) !== null && _g !== void 0 ? _g : []) {
2847
+ for (const p of (_m = BOARD_REGISTRY.get(api.container)) !== null && _m !== void 0 ? _m : []) {
2779
2848
  if (p !== selfPeer && p.hasItem(hit.node.id))
2780
2849
  return false;
2781
2850
  }
@@ -2788,8 +2857,8 @@ export function bindDashboardGrid(api, group, options = {}) {
2788
2857
  // divider press in a split section (dead dividers) and, near the
2789
2858
  // section's edge, turned it into a section resize (the width changed
2790
2859
  // while a control's height was being dragged — Quantia, Groups page).
2791
- for (const p of (_h = BOARD_REGISTRY.get(api.container)) !== null && _h !== void 0 ? _h : []) {
2792
- if (p !== selfPeer && ((_j = group.members) !== null && _j !== void 0 ? _j : new Set()).has(p.group.id) && p.containsWorld(ev.world.x, ev.world.y))
2860
+ for (const p of (_o = BOARD_REGISTRY.get(api.container)) !== null && _o !== void 0 ? _o : []) {
2861
+ if (p !== selfPeer && ((_p = group.members) !== null && _p !== void 0 ? _p : new Set()).has(p.group.id) && p.containsWorld(ev.world.x, ev.world.y))
2793
2862
  return false;
2794
2863
  }
2795
2864
  // Claim (and deaden) empty presses inside a member group's frame so the
@@ -2801,34 +2870,38 @@ export function bindDashboardGrid(api, group, options = {}) {
2801
2870
  return insideMemberGroupFrame(ev.world.x, ev.world.y) || worldInsideBoard(ev.world.x, ev.world.y);
2802
2871
  },
2803
2872
  onPointerDown(ev, hit) {
2804
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
2873
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
2805
2874
  if (gesture)
2806
2875
  return; // mid-palette
2807
2876
  const target = ((_b = (_a = ev.source) === null || _a === void 0 ? void 0 : _a.target) !== null && _b !== void 0 ? _b : null);
2877
+ if ((_c = target === null || target === void 0 ? void 0 : target.closest) === null || _c === void 0 ? void 0 : _c.call(target, '.axdb-tabs')) {
2878
+ stripPress = true; // claimed for the strip: nothing of ours starts
2879
+ return;
2880
+ }
2808
2881
  // A press on a painted grip names its widget by the DOM: an OUTSIDE tab
2809
2882
  // sits above the card's box, where the hit test sees the gap or the
2810
2883
  // neighbour above.
2811
2884
  const gripHost = gripHostOf(target);
2812
- const gripId = (_c = gripHost === null || gripHost === void 0 ? void 0 : gripHost.getAttribute('data-node-id')) !== null && _c !== void 0 ? _c : null;
2813
- const onGrip = !!gripId && ((_d = group.members) !== null && _d !== void 0 ? _d : new Set()).has(gripId);
2885
+ const gripId = (_d = gripHost === null || gripHost === void 0 ? void 0 : gripHost.getAttribute('data-node-id')) !== null && _d !== void 0 ? _d : null;
2886
+ const onGrip = !!gripId && ((_e = group.members) !== null && _e !== void 0 ? _e : new Set()).has(gripId);
2814
2887
  // A SECTION's corner handle sits on top of whatever tile shares that
2815
2888
  // corner; the DOM target names the section, the hit test the tile.
2816
- const sectionHandle = (_e = target === null || target === void 0 ? void 0 : target.closest) === null || _e === void 0 ? void 0 : _e.call(target, '.axdb-slab > .axdb-rs');
2889
+ const sectionHandle = (_f = target === null || target === void 0 ? void 0 : target.closest) === null || _f === void 0 ? void 0 : _f.call(target, '.axdb-slab > .axdb-rs');
2817
2890
  // A SECTION's CAPTION BAND names its section the same way — a 'tab'
2818
2891
  // band sits above the frame, over the gap or the tile above. An action
2819
2892
  // button in it fires and does nothing else.
2820
- const captionBand = (_f = target === null || target === void 0 ? void 0 : target.closest) === null || _f === void 0 ? void 0 : _f.call(target, '.axdb-slab > .axdb-slab-h');
2821
- const captionId = (_h = (_g = captionBand === null || captionBand === void 0 ? void 0 : captionBand.parentElement) === null || _g === void 0 ? void 0 : _g.getAttribute('data-slab-id')) !== null && _h !== void 0 ? _h : null;
2822
- const ownCaption = !!captionId && ((_j = group.members) !== null && _j !== void 0 ? _j : new Set()).has(captionId);
2893
+ const captionBand = (_g = target === null || target === void 0 ? void 0 : target.closest) === null || _g === void 0 ? void 0 : _g.call(target, '.axdb-slab > .axdb-slab-h');
2894
+ const captionId = (_j = (_h = captionBand === null || captionBand === void 0 ? void 0 : captionBand.parentElement) === null || _h === void 0 ? void 0 : _h.getAttribute('data-slab-id')) !== null && _j !== void 0 ? _j : null;
2895
+ const ownCaption = !!captionId && ((_k = group.members) !== null && _k !== void 0 ? _k : new Set()).has(captionId);
2823
2896
  // (An action button never reaches here: it is pass-through, and its own
2824
2897
  // `click` fires onCaptionAction — see captionPassThrough.)
2825
2898
  if ((!hit.node && !onGrip) || sectionHandle || ownCaption) {
2826
2899
  // A press on a SECTION — its empty band, its caption, its corner
2827
2900
  // handle or its frame edge — selects the section; the handle or an
2828
2901
  // edge resizes it.
2829
- const slabHandle = (_k = target === null || target === void 0 ? void 0 : target.closest) === null || _k === void 0 ? void 0 : _k.call(target, '.axdb-slab > .axdb-rs');
2830
- const slabId = (_o = (_m = (_l = slabHandle === null || slabHandle === void 0 ? void 0 : slabHandle.parentElement) === null || _l === void 0 ? void 0 : _l.getAttribute('data-slab-id')) !== null && _m !== void 0 ? _m : (ownCaption ? captionId : null)) !== null && _o !== void 0 ? _o : memberGroupAt(ev.world.x, ev.world.y);
2831
- const grp = slabId && ((_p = group.members) !== null && _p !== void 0 ? _p : new Set()).has(slabId) ? diagram.getGroup(slabId) : undefined;
2902
+ const slabHandle = (_l = target === null || target === void 0 ? void 0 : target.closest) === null || _l === void 0 ? void 0 : _l.call(target, '.axdb-slab > .axdb-rs');
2903
+ const slabId = (_p = (_o = (_m = slabHandle === null || slabHandle === void 0 ? void 0 : slabHandle.parentElement) === null || _m === void 0 ? void 0 : _m.getAttribute('data-slab-id')) !== null && _o !== void 0 ? _o : (ownCaption ? captionId : null)) !== null && _p !== void 0 ? _p : memberGroupAt(ev.world.x, ev.world.y);
2904
+ const grp = slabId && ((_q = group.members) !== null && _q !== void 0 ? _q : new Set()).has(slabId) ? diagram.getGroup(slabId) : undefined;
2832
2905
  if (slabId && grp) {
2833
2906
  selectWidget(slabId);
2834
2907
  api.render();
@@ -2851,7 +2924,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2851
2924
  // forwards the pointer sequence.
2852
2925
  const parent = parentPeer();
2853
2926
  if ((parent === null || parent === void 0 ? void 0 : parent.selectMember) && worldInsideBoard(ev.world.x, ev.world.y)) {
2854
- const ownHandle = ((_q = slabHandle === null || slabHandle === void 0 ? void 0 : slabHandle.parentElement) === null || _q === void 0 ? void 0 : _q.getAttribute('data-slab-id')) === group.id;
2927
+ const ownHandle = ((_r = slabHandle === null || slabHandle === void 0 ? void 0 : slabHandle.parentElement) === null || _r === void 0 ? void 0 : _r.getAttribute('data-slab-id')) === group.id;
2855
2928
  parent.selectMember(group.id);
2856
2929
  if (!isStatic && parent.beginSlabResize) {
2857
2930
  const edges = ownHandle
@@ -2859,14 +2932,14 @@ export function bindDashboardGrid(api, group, options = {}) {
2859
2932
  : slabEdgesNear(group, ev.world.x, ev.world.y);
2860
2933
  if (anyEdge(edges) && parent.beginSlabResize(group.id, edges, ev))
2861
2934
  forwardSlab = parent;
2862
- else if (!anyEdge(edges) && captionId === group.id && ((_r = parent.beginSlabMove) === null || _r === void 0 ? void 0 : _r.call(parent, group.id, ev)))
2935
+ else if (!anyEdge(edges) && captionId === group.id && ((_s = parent.beginSlabMove) === null || _s === void 0 ? void 0 : _s.call(parent, group.id, ev)))
2863
2936
  forwardSlab = parent;
2864
2937
  }
2865
2938
  return;
2866
2939
  }
2867
2940
  // The board's own empty area: a void click. Nothing to drag, and the
2868
2941
  // selection clears exactly as a click outside any board would.
2869
- (_t = (_s = diagram).clearSelection) === null || _t === void 0 ? void 0 : _t.call(_s);
2942
+ (_u = (_t = diagram).clearSelection) === null || _u === void 0 ? void 0 : _u.call(_t);
2870
2943
  selectWidget(undefined);
2871
2944
  api.render();
2872
2945
  return;
@@ -2875,17 +2948,17 @@ export function bindDashboardGrid(api, group, options = {}) {
2875
2948
  if (!node)
2876
2949
  return;
2877
2950
  selectWidget(node.id); // a press selects, whether or not it starts a gesture
2878
- if (((_u = node.state) === null || _u === void 0 ? void 0 : _u.locked) === true)
2951
+ if (((_v = node.state) === null || _v === void 0 ? void 0 : _v.locked) === true)
2879
2952
  return; // pinned: refuse; click still focuses
2880
2953
  if (isStatic)
2881
2954
  return; // a static board: claimed and deadened, click still focuses
2882
2955
  // Which edges did the press take? The corner handle names its own (s+e,
2883
2956
  // or s+w on RTL); a bare press within EDGE_GRIP of the tile's border
2884
2957
  // takes that border; anywhere else is a move. A grip press is a move.
2885
- const onHandle = !!((_v = target === null || target === void 0 ? void 0 : target.closest) === null || _v === void 0 ? void 0 : _v.call(target, '.axdb-rs'));
2958
+ const onHandle = !!((_w = target === null || target === void 0 ? void 0 : target.closest) === null || _w === void 0 ? void 0 : _w.call(target, '.axdb-rs'));
2886
2959
  const hostEl = hostOf(node.id);
2887
- const resizable = ((_w = node.getMetadata) === null || _w === void 0 ? void 0 : _w.call(node, 'widgetResizable')) !== false;
2888
- const movable = ((_x = node.getMetadata) === null || _x === void 0 ? void 0 : _x.call(node, 'widgetMovable')) !== false;
2960
+ const resizable = ((_x = node.getMetadata) === null || _x === void 0 ? void 0 : _x.call(node, 'widgetResizable')) !== false;
2961
+ const movable = ((_y = node.getMetadata) === null || _y === void 0 ? void 0 : _y.call(node, 'widgetMovable')) !== false;
2889
2962
  // `ev.screen` is ELEMENT-LOCAL px; host rects are in client px. Compare
2890
2963
  // like with like — the source event's clientX/Y when there is one, else
2891
2964
  // the container's origin plus the local offset.
@@ -2933,7 +3006,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2933
3006
  startSize: { width: node.size.width, height: node.size.height },
2934
3007
  startPos: { x: node.position.x, y: node.position.y },
2935
3008
  edges,
2936
- spans: { w: (_y = it === null || it === void 0 ? void 0 : it.w) !== null && _y !== void 0 ? _y : 1, h: (_z = it === null || it === void 0 ? void 0 : it.h) !== null && _z !== void 0 ? _z : 1 },
3009
+ spans: { w: (_z = it === null || it === void 0 ? void 0 : it.w) !== null && _z !== void 0 ? _z : 1, h: (_0 = it === null || it === void 0 ? void 0 : it.h) !== null && _0 !== void 0 ? _0 : 1 },
2937
3010
  removedFromBoard: false,
2938
3011
  leg: null,
2939
3012
  strip: null,
@@ -2946,6 +3019,8 @@ export function bindDashboardGrid(api, group, options = {}) {
2946
3019
  },
2947
3020
  onPointerMove(ev) {
2948
3021
  var _a;
3022
+ if (stripPress)
3023
+ return;
2949
3024
  if (forwardSlab)
2950
3025
  (_a = forwardSlab.slabMove) === null || _a === void 0 ? void 0 : _a.call(forwardSlab, ev);
2951
3026
  else if (slabGesture)
@@ -2954,20 +3029,45 @@ export function bindDashboardGrid(api, group, options = {}) {
2954
3029
  onToolMove(ev);
2955
3030
  },
2956
3031
  onPointerUp() {
2957
- var _a;
2958
- if (forwardSlab) {
2959
- (_a = forwardSlab.slabUp) === null || _a === void 0 ? void 0 : _a.call(forwardSlab);
2960
- forwardSlab = null;
3032
+ if (stripPress) {
3033
+ stripPress = false;
3034
+ return;
3035
+ }
3036
+ try {
3037
+ onPointerUpInner();
3038
+ }
3039
+ finally {
3040
+ flushDeferredRebuild();
3041
+ // EVERY press ends disarmed. The press armed the glide (a task ahead
3042
+ // of any displacement — see onPointerDown); a drop disarms on its own
3043
+ // path, but a press that never travelled — a plain CLICK — left it
3044
+ // armed for good, and every later left/top write eased: a tab switch
3045
+ // slid its page in from 20,000 px off canvas over 280 ms (0.4.36).
3046
+ disarmGlideSoon();
2961
3047
  }
2962
- else if (slabGesture)
2963
- slabUp();
2964
- else
2965
- onToolUp();
2966
3048
  },
2967
3049
  onCancel() {
2968
- cancelActiveGesture();
3050
+ stripPress = false;
3051
+ try {
3052
+ cancelActiveGesture();
3053
+ }
3054
+ finally {
3055
+ flushDeferredRebuild();
3056
+ disarmGlideSoon();
3057
+ }
2969
3058
  },
2970
3059
  };
3060
+ const onPointerUpInner = () => {
3061
+ var _a;
3062
+ if (forwardSlab) {
3063
+ (_a = forwardSlab.slabUp) === null || _a === void 0 ? void 0 : _a.call(forwardSlab);
3064
+ forwardSlab = null;
3065
+ }
3066
+ else if (slabGesture)
3067
+ slabUp();
3068
+ else
3069
+ onToolUp();
3070
+ };
2971
3071
  const unregisterTool = registerTool(tool);
2972
3072
  /**
2973
3073
  * Edge affordance: the cursor says which border a press would take, the
@@ -3250,11 +3350,42 @@ export function bindDashboardGrid(api, group, options = {}) {
3250
3350
  // leaves whenever it is over a strip, a group or an edge. A full board that
3251
3351
  // refuses the ghost still lets the page join, reorder or split.
3252
3352
  let leg = null;
3253
- const ensureLeg = (world) => {
3254
- if (!leg)
3255
- leg = adopt({ id: plan.arrivingId }, world, plan.size, { fit: 'shrink', anchor: 'top' });
3353
+ /** The board holding the leg: null = this one, else the foreign peer under the pointer. */
3354
+ let legPeer = null;
3355
+ const ensureLeg = (world, peer = null) => {
3356
+ // The page TRAVELS at most half the board tall (the dock's measure): at
3357
+ // its natural height — a page fills its container, eight rows on the
3358
+ // demo — the ghost crossing the KPI row tore the whole dashboard apart
3359
+ // on its way to a cell (identification round, D1 03–08).
3360
+ const capPx = dockSpan.h * (rowHeightFor(geom(), rows()) + gap) - gap;
3361
+ const size = { width: plan.size.width, height: Math.min(plan.size.height, capPx) };
3362
+ // A different board under the pointer takes the leg over, the old one
3363
+ // back to its pre-entry layout first. An INNER tab used to know only
3364
+ // the board owning its container — over the main board the chip dimmed
3365
+ // and the release did nothing (identification round, L2).
3366
+ if (leg && legPeer !== peer) {
3367
+ leg.abort();
3368
+ leg = null;
3369
+ }
3370
+ if (!leg) {
3371
+ leg = peer ? peer.adopt({ id: plan.arrivingId }, world, size, { fit: 'shrink', anchor: 'top' }) : adopt({ id: plan.arrivingId }, world, size, { fit: 'shrink', anchor: 'top' });
3372
+ legPeer = leg ? peer : null;
3373
+ }
3256
3374
  return leg;
3257
3375
  };
3376
+ /** The deepest OTHER board under the point that may take the page — never one of the page's own boards. */
3377
+ const foreignAt = (wx, wy) => {
3378
+ let best = null;
3379
+ for (const p of peersOnCanvas()) {
3380
+ if (p === selfPeer || plan.ownBoards.includes(p.group.id))
3381
+ continue;
3382
+ if (!p.containsWorld(wx, wy))
3383
+ continue;
3384
+ if (!best || p.frameArea() < best.frameArea())
3385
+ best = p;
3386
+ }
3387
+ return best;
3388
+ };
3258
3389
  const naturalSpan = (() => {
3259
3390
  const sp = sizeToSpan(plan.size.width, plan.size.height, frame(), geom(), rows());
3260
3391
  return { w: Math.max(1, Math.min(columns, sp.w)), h: Math.max(TEAR_OUT_MIN_ROWS, sp.h) };
@@ -3337,13 +3468,60 @@ export function bindDashboardGrid(api, group, options = {}) {
3337
3468
  return { left, top, right, bottom }; // unlaid-out: nothing to clip to
3338
3469
  return { left: Math.max(left, rect.left), top: Math.max(top, rect.top), right: Math.min(right, rect.right), bottom: Math.min(bottom, rect.bottom) };
3339
3470
  };
3471
+ /** Inside the VISIBLE canvas plus the grace band, on the screen — a camera that moved must not turn a pointer outside the canvas into a world point inside the board. */
3472
+ const clientInsideCanvasGrace = (cx, cy) => {
3473
+ // The CANVAS, not this board's frame: a nested page's binder runs the
3474
+ // tear-out of the tabs inside it, and its pages must still land on the
3475
+ // boards around it. An unmeasurable container (no layout) is never off.
3476
+ const rect = api.container.getBoundingClientRect();
3477
+ if (rect.width <= 0 || rect.height <= 0)
3478
+ return true;
3479
+ return cx >= rect.left - EDGE_GRACE && cx <= rect.right + EDGE_GRACE && cy >= rect.top - EDGE_GRACE && cy <= rect.bottom + EDGE_GRACE;
3480
+ };
3481
+ /**
3482
+ * The board's rows for a dock: as they stood BEFORE the ghost entered.
3483
+ * Read live, the ghost's own pushing inflated them on every re-entry —
3484
+ * side docks 14, 21, 104 rows tall, a bottom dock landing at row 29.
3485
+ */
3486
+ const baseRows = () => {
3487
+ const b = leg === null || leg === void 0 ? void 0 : leg.baseline();
3488
+ if (!b)
3489
+ return rowsWithout(plan.arrivingId);
3490
+ let r = 0;
3491
+ for (const c of b.values())
3492
+ r = Math.max(r, c.y + c.h);
3493
+ return r;
3494
+ };
3495
+ const worldToClient = (wx, wy) => {
3496
+ const rect = api.container.getBoundingClientRect();
3497
+ const o = toWorld(rect.left, rect.top);
3498
+ const u = toWorld(rect.left + 100, rect.top + 100);
3499
+ return { x: rect.left + (wx - o.x) * (100 / (u.x - o.x || 100)), y: rect.top + (wy - o.y) * (100 / (u.y - o.y || 100)) };
3500
+ };
3501
+ /**
3502
+ * Would the page land where the user cannot see it? A cell whose room
3503
+ * lies below the fold (the only cell left under a locked section — D1
3504
+ * hold3) used to take the drop a screen away. Hidden = not one pixel of
3505
+ * the cell inside the visible canvas: a landing whose top edge shows,
3506
+ * the rest below the fold of a small canvas, is a landing the user can
3507
+ * see and a grow board scrolls to (lab L61 dropped at the canvas foot).
3508
+ */
3509
+ const landingHidden = () => {
3510
+ const r = leg === null || leg === void 0 ? void 0 : leg.rect();
3511
+ const rect = api.container.getBoundingClientRect();
3512
+ if (!r || rect.width <= 0 || rect.height <= 0)
3513
+ return false;
3514
+ const tl = worldToClient(r.x, r.y);
3515
+ const br = worldToClient(r.x + r.width, r.y + r.height);
3516
+ return tl.y >= rect.bottom || br.y <= rect.top || tl.x >= rect.right || br.x <= rect.left;
3517
+ };
3340
3518
  const rootAt = (cx, cy) => {
3341
3519
  // The bands reach a little OUTSIDE the frame too: a pointer that
3342
3520
  // overshoots the board's top edge by a few pixels means "the top".
3343
3521
  const v = visibleFrame();
3344
3522
  if (cx < v.left - ROOT_SIDE || cx > v.right + ROOT_SIDE || cy < v.top - ROOT_TOP || cy > v.bottom + ROOT_BOTTOM)
3345
3523
  return null;
3346
- const rows = Math.max(TEAR_OUT_MIN_ROWS, rowsWithout(plan.arrivingId));
3524
+ const rows = Math.max(TEAR_OUT_MIN_ROWS, baseRows());
3347
3525
  const w = dockSpan.w;
3348
3526
  const h = dockSpan.h;
3349
3527
  if (cy - v.top <= ROOT_TOP)
@@ -3390,6 +3568,11 @@ export function bindDashboardGrid(api, group, options = {}) {
3390
3568
  : { keep: { x: it.x, y: it.y + b, w: it.w, h: a }, born: { x: it.x, y: it.y, w: it.w, h: b } };
3391
3569
  };
3392
3570
  const zoneAt = (cx, cy, world) => {
3571
+ // A pointer OUTSIDE the visible canvas is off, full stop: a camera that
3572
+ // moved can map it to a world point inside a group, and that group
3573
+ // took a join or a split from a release in the page header.
3574
+ if (!clientInsideCanvasGrace(cx, cy))
3575
+ return { kind: 'off' };
3393
3576
  const target = targetAt(world.x, world.y);
3394
3577
  const home = !target && worldInsideGroup(from, world.x, world.y);
3395
3578
  // A strip is the most precise target there is: anyone's wins outright.
@@ -3424,9 +3607,12 @@ export function bindDashboardGrid(api, group, options = {}) {
3424
3607
  }
3425
3608
  if (home)
3426
3609
  return { kind: 'home' };
3610
+ const foreign = foreignAt(world.x, world.y);
3611
+ if (foreign && (!worldInsideBoard(world.x, world.y) || foreign.frameArea() < boardArea()))
3612
+ return { kind: 'board', peer: foreign };
3427
3613
  return { kind: 'board' };
3428
3614
  };
3429
- const zoneKey = (z) => JSON.stringify(z, (k, v) => (k === 'target' ? v.id : v));
3615
+ const zoneKey = (z) => JSON.stringify(z, (k, v) => (k === 'target' ? v.id : k === 'peer' ? v.group.id : v));
3430
3616
  // DOCKING PUSHES SECTIONS. A section is a locked tile so that a widget
3431
3617
  // never pushes it; a group docked against the board's edge is the one
3432
3618
  // gesture that must — everything below the top band moves down. For the
@@ -3526,7 +3712,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3526
3712
  };
3527
3713
  const previewSplit = (z, world) => {
3528
3714
  const it = engine.getItem(z.target.id);
3529
- const l = ensureLeg(world);
3715
+ const l = ensureLeg(world, null);
3530
3716
  if (!it || !l)
3531
3717
  return false;
3532
3718
  split = { id: z.target.id, before: { x: it.x, y: it.y, w: it.w, h: it.h }, frameBefore: frameOfGroup(z.target), keep: z.keep };
@@ -3545,14 +3731,14 @@ export function bindDashboardGrid(api, group, options = {}) {
3545
3731
  let zone = { kind: 'board' };
3546
3732
  let key = zoneKey(zone);
3547
3733
  const applyZone = (z, world) => {
3548
- var _a;
3734
+ var _a, _b, _c;
3549
3735
  const k = zoneKey(z);
3550
3736
  const same = k === key;
3551
3737
  key = k;
3552
3738
  zone = z;
3553
3739
  if (same) {
3554
3740
  if (z.kind === 'board')
3555
- (_a = ensureLeg(world)) === null || _a === void 0 ? void 0 : _a.move(world);
3741
+ (_b = ensureLeg(world, (_a = z.peer) !== null && _a !== void 0 ? _a : null)) === null || _b === void 0 ? void 0 : _b.move(world);
3556
3742
  return;
3557
3743
  }
3558
3744
  undoSplitPreview();
@@ -3583,7 +3769,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3583
3769
  break;
3584
3770
  }
3585
3771
  case 'root': {
3586
- const l = ensureLeg(world);
3772
+ const l = ensureLeg(world, null);
3587
3773
  if (l) {
3588
3774
  unlockGroups();
3589
3775
  l.place(z.cell);
@@ -3600,10 +3786,11 @@ export function bindDashboardGrid(api, group, options = {}) {
3600
3786
  plan.markDrop(fromGroupId, z.index);
3601
3787
  break;
3602
3788
  case 'home':
3789
+ case 'off':
3603
3790
  leg === null || leg === void 0 ? void 0 : leg.leave();
3604
3791
  break;
3605
3792
  case 'board': {
3606
- const l = ensureLeg(world);
3793
+ const l = ensureLeg(world, (_c = z.peer) !== null && _c !== void 0 ? _c : null);
3607
3794
  l === null || l === void 0 ? void 0 : l.enter(world);
3608
3795
  break;
3609
3796
  }
@@ -3626,7 +3813,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3626
3813
  applyZone(z, world);
3627
3814
  // Dimmed = a release here does nothing: home, off the board, or a board
3628
3815
  // that refused the ghost (bounded and full).
3629
- chip.classList.toggle('axdb-out', zone.kind === 'home' || (zone.kind === 'root' && !leg) || (zone.kind === 'board' && (!leg || !worldInsideBoardGrace(world.x, world.y))));
3816
+ chip.classList.toggle('axdb-out', zone.kind === 'home' || zone.kind === 'off' || (zone.kind === 'root' && !leg) || (zone.kind === 'board' && (!leg || landingHidden())));
3630
3817
  api.render();
3631
3818
  };
3632
3819
  const done = (changed, kind) => {
@@ -3638,7 +3825,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3638
3825
  (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: kind, kind: 'move', nodeId: pageId, changed });
3639
3826
  };
3640
3827
  const finish = (commit) => {
3641
- var _a, _b;
3828
+ var _a, _b, _c, _d;
3642
3829
  detach();
3643
3830
  chip.remove();
3644
3831
  hideOverlay();
@@ -3650,7 +3837,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3650
3837
  const z = commit ? zoneAt(last.x, last.y, world) : { kind: 'home' };
3651
3838
  if (z.kind !== 'root')
3652
3839
  undoInsertRows();
3653
- if (!commit || z.kind === 'home' || (z.kind === 'root' && !ensureLeg(world)) || (z.kind === 'board' && (!ensureLeg(world) || !worldInsideBoardGrace(world.x, world.y)))) {
3840
+ if (!commit || z.kind === 'home' || z.kind === 'off' || (z.kind === 'root' && !ensureLeg(world, null)) || (z.kind === 'board' && (!ensureLeg(world, (_a = z.peer) !== null && _a !== void 0 ? _a : null) || landingHidden()))) {
3654
3841
  undoSplitPreview();
3655
3842
  leg === null || leg === void 0 ? void 0 : leg.abort();
3656
3843
  relockGroups();
@@ -3680,7 +3867,9 @@ export function bindDashboardGrid(api, group, options = {}) {
3680
3867
  // other. The preview already holds both in the engine; commit them.
3681
3868
  if (!split || split.id !== z.target.id || zoneKey(zone) !== zoneKey(z))
3682
3869
  applyZone(z, world);
3683
- const fin = (_a = leg === null || leg === void 0 ? void 0 : leg.finalize()) !== null && _a !== void 0 ? _a : null;
3870
+ hideOverlay(); // a re-applied zone repaints its preview; the release is not a preview
3871
+ plan.markDrop(null, null);
3872
+ const fin = (_b = leg === null || leg === void 0 ? void 0 : leg.finalize()) !== null && _b !== void 0 ? _b : null;
3684
3873
  const it = engine.getItem(z.target.id);
3685
3874
  const before = split;
3686
3875
  if (it)
@@ -3706,13 +3895,15 @@ export function bindDashboardGrid(api, group, options = {}) {
3706
3895
  // ROOT DOCK or a free cell on the board: the leg sits where it will land.
3707
3896
  if (zoneKey(zone) !== zoneKey(z))
3708
3897
  applyZone(z, world);
3709
- const fin = (_b = leg === null || leg === void 0 ? void 0 : leg.finalize()) !== null && _b !== void 0 ? _b : null;
3898
+ hideOverlay(); // a re-applied zone repaints its preview; the release is not a preview
3899
+ plan.markDrop(null, null);
3900
+ const fin = (_c = leg === null || leg === void 0 ? void 0 : leg.finalize()) !== null && _c !== void 0 ? _c : null;
3710
3901
  relockGroups();
3711
3902
  if (!fin) {
3712
3903
  done(false, 'cancel');
3713
3904
  return;
3714
3905
  }
3715
- const planned = plan.commands(fin.cell, fin.rect, group.id);
3906
+ const planned = plan.commands(fin.cell, fin.rect, (_d = leg === null || leg === void 0 ? void 0 : leg.groupId) !== null && _d !== void 0 ? _d : group.id);
3716
3907
  done(execute(z.kind === 'root' ? 'Dock tab' : 'Move tab out', [...fin.commands, ...groupCommands(fin), ...planned.move, ...planned.collapse]), 'commit');
3717
3908
  };
3718
3909
  const onUp = () => finish(true);
@@ -3917,12 +4108,33 @@ export function bindDashboardGrid(api, group, options = {}) {
3917
4108
  });
3918
4109
  /** Rebuild the engine from the members' persisted cells. `pack` runs gravity
3919
4110
  * over the result (boot, float→off); a plain sync keeps the cells verbatim. */
4111
+ /**
4112
+ * A rebuild asked for while a gesture is live waits for the gesture. The
4113
+ * pointer's own crossing re-lays boards — adopting a widget into the Nested
4114
+ * page moved the inner tabs container, the tabs runtime re-placed its
4115
+ * pages, and the inner page's binder rebuilt from the model: its gesture
4116
+ * cancelled mid-drag, the dragged widget re-added, the ghost class gone
4117
+ * and the target's placeholder leaked through release and undo
4118
+ * (identification round, L5). The frame change itself is answered by
4119
+ * `onBoundsChanged` (a re-projection); the model is rebuilt afterwards.
4120
+ */
4121
+ let rebuildDeferred = null;
4122
+ const flushDeferredRebuild = () => {
4123
+ if (!rebuildDeferred || gesture || slabGesture)
4124
+ return;
4125
+ const { pack } = rebuildDeferred;
4126
+ rebuildDeferred = null;
4127
+ rebuild(pack);
4128
+ };
3920
4129
  const rebuild = (pack) => {
3921
4130
  var _a, _b, _c, _d;
3922
4131
  if (disposed)
3923
4132
  return;
3924
- if (gesture)
3925
- cancelActiveGesture(false);
4133
+ if (gesture || slabGesture) {
4134
+ rebuildDeferred = { pack: pack || (rebuildDeferred === null || rebuildDeferred === void 0 ? void 0 : rebuildDeferred.pack) === true };
4135
+ project();
4136
+ return;
4137
+ }
3926
4138
  applyFluidFrame();
3927
4139
  const items = [];
3928
4140
  for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {