@grafloria/element 0.4.30 → 0.4.32

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.
@@ -156,6 +156,38 @@ export function pressOnDragHandle(sel, target, hostEl, clientX, clientY) {
156
156
  export const CAPTION_BAND = 28;
157
157
  /** A torn-out page never arrives shorter than this: a strip with no room under it is not a group. */
158
158
  export const TEAR_OUT_MIN_ROWS = 2;
159
+ /**
160
+ * Steps that depend on each other's RESULT — create a group, then move a page
161
+ * into it, then place the group on the board; or remove a member and then the
162
+ * group it emptied — run as one history step. A batch checks every member's
163
+ * `canExecute` before running any of them and every member's `canUndo` before
164
+ * undoing any, and a membership command's precondition (its group exists) is
165
+ * exactly what a neighbouring step creates or removes. This runs the chain in
166
+ * order and reverses it on undo, judging nothing up front.
167
+ */
168
+ export class SequenceCommand extends Command {
169
+ constructor(name, steps) {
170
+ super(name);
171
+ this.steps = steps;
172
+ }
173
+ execute(context) {
174
+ for (const c of this.steps)
175
+ c.execute(context);
176
+ }
177
+ undo(context) {
178
+ for (let i = this.steps.length - 1; i >= 0; i--)
179
+ this.steps[i].undo(context);
180
+ }
181
+ canExecute() {
182
+ return true;
183
+ }
184
+ canUndo() {
185
+ return true;
186
+ }
187
+ serialize() {
188
+ return { id: this.id, name: this.name, timestamp: this.timestamp, data: { steps: this.steps.map((c) => c.serialize()) } };
189
+ }
190
+ }
159
191
  /**
160
192
  * Binders on the same canvas find each other here. A WeakMap: a canvas that
161
193
  * is gone takes its (empty) peer set with it — the strong Map it replaced kept
@@ -1257,6 +1289,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1257
1289
  return false;
1258
1290
  };
1259
1291
  let slabGesture = null;
1292
+ /** The slab gesture as it is NOW — read through a call so a guard's narrowing does not stick. */
1293
+ const currentSlab = () => slabGesture;
1260
1294
  /** The PARENT running a resize of OUR section from a press this tool claimed. */
1261
1295
  let forwardSlab = null;
1262
1296
  const frameOfGroup = (grp) => ({ x: grp.position.x, y: grp.position.y, width: sizeOf(grp).width, height: sizeOf(grp).height });
@@ -1281,10 +1315,48 @@ export function bindDashboardGrid(api, group, options = {}) {
1281
1315
  dx: edges.e ? grp.position.x + sizeOf(grp).width - ev.world.x : edges.w ? grp.position.x - ev.world.x : 0,
1282
1316
  dy: edges.s ? grp.position.y + sizeOf(grp).height - ev.world.y : edges.n ? grp.position.y - ev.world.y : 0,
1283
1317
  },
1318
+ move: false,
1284
1319
  };
1285
1320
  capturePointer(slabGesture.pointerId);
1286
1321
  api.container.style.cursor = cursorFor(edges);
1287
1322
  };
1323
+ /**
1324
+ * MOVE a section by its caption band or its strip's empty space. A section
1325
+ * is a LOCKED tile so that nothing pushes it; for the gesture's duration the
1326
+ * tile is unlocked (it is the one moving), other sections stay locked and
1327
+ * still refuse it (E4b), and every unlocked tile gets pushed the way a
1328
+ * widget pushes it. Its children ride along: the frame moves, the nested
1329
+ * board re-projects.
1330
+ */
1331
+ const beginSlabMove = (id, ev) => {
1332
+ const grp = diagram.getGroup(id);
1333
+ const it = engine.getItem(id);
1334
+ if (!grp || !it || gesture || slabGesture || isStatic)
1335
+ return;
1336
+ engine.beginGesture();
1337
+ const snap = snapshotAll();
1338
+ it.locked = false;
1339
+ slabGesture = {
1340
+ id,
1341
+ edges: NO_EDGES,
1342
+ pointerId: typeof PointerEvent !== 'undefined' && ev.source instanceof PointerEvent ? ev.source.pointerId : null,
1343
+ started: false,
1344
+ downScreen: { x: ev.screen.x, y: ev.screen.y },
1345
+ startCells: snap.cells,
1346
+ startGeom: snap.geoms,
1347
+ cellBefore: { x: it.x, y: it.y, w: it.w, h: it.h },
1348
+ frameBefore: frameOfGroup(grp),
1349
+ grab: { dx: grp.position.x - ev.world.x, dy: grp.position.y - ev.world.y },
1350
+ move: true,
1351
+ };
1352
+ capturePointer(slabGesture.pointerId);
1353
+ api.container.style.cursor = 'grabbing';
1354
+ };
1355
+ const relockSlab = (id) => {
1356
+ const it = engine.getItem(id);
1357
+ if (it)
1358
+ it.locked = true;
1359
+ };
1288
1360
  const slabMove = (ev) => {
1289
1361
  const g = slabGesture;
1290
1362
  if (!g)
@@ -1300,6 +1372,14 @@ export function bindDashboardGrid(api, group, options = {}) {
1300
1372
  return;
1301
1373
  const f = frame();
1302
1374
  const gg = geom();
1375
+ if (g.move) {
1376
+ // The section's top-left follows the pointer by the offset it was grabbed at.
1377
+ const cell = pointToCell(ev.world.x + g.grab.dx, ev.world.y + g.grab.dy, f, gg, rows(), it.w);
1378
+ if ((cell.x !== it.x || cell.y !== it.y) && engine.moveCheck(g.id, cell.x, cell.y, { gate: false }).changed)
1379
+ project();
1380
+ syncPlaceholder();
1381
+ return;
1382
+ }
1303
1383
  const cu = columnUnitFor(gg, f.width);
1304
1384
  const rh = rowHeightFor(gg, rows());
1305
1385
  // The grid line nearest the pointer, in cells (mirrored on RTL).
@@ -1346,6 +1426,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1346
1426
  slabGesture = null;
1347
1427
  releasePointer(g.pointerId);
1348
1428
  api.container.style.cursor = '';
1429
+ if (g.move)
1430
+ relockSlab(g.id);
1349
1431
  if (!g.started) {
1350
1432
  engine.endGesture();
1351
1433
  return;
@@ -1359,11 +1441,11 @@ export function bindDashboardGrid(api, group, options = {}) {
1359
1441
  if (it && grp && (b.x !== it.x || b.y !== it.y || b.w !== it.w || b.h !== it.h)) {
1360
1442
  commands.push(new SetGroupCellCommand(g.id, b, { x: it.x, y: it.y, w: it.w, h: it.h }, g.frameBefore, frameOfGroup(grp)));
1361
1443
  }
1362
- const changed = execute('Resize section', commands);
1444
+ const changed = execute(g.move ? 'Move section' : 'Resize section', commands);
1363
1445
  disarmGlideSoon();
1364
1446
  syncHandles();
1365
1447
  api.renderNow();
1366
- (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'commit', kind: 'resize', nodeId: g.id, changed });
1448
+ (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'commit', kind: g.move ? 'move' : 'resize', nodeId: g.id, changed });
1367
1449
  };
1368
1450
  const slabCancel = () => {
1369
1451
  var _a;
@@ -1373,6 +1455,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1373
1455
  slabGesture = null;
1374
1456
  releasePointer(g.pointerId);
1375
1457
  api.container.style.cursor = '';
1458
+ if (g.move)
1459
+ relockSlab(g.id);
1376
1460
  if (g.started)
1377
1461
  engine.cancelGesture();
1378
1462
  else
@@ -1487,9 +1571,13 @@ export function bindDashboardGrid(api, group, options = {}) {
1487
1571
  (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed });
1488
1572
  };
1489
1573
  const cancelActiveGesture = (notify = true) => {
1490
- var _a, _b, _c, _d;
1574
+ var _a, _b, _c, _d, _e;
1575
+ if (gesture === null || gesture === void 0 ? void 0 : gesture.strip) {
1576
+ (_a = options.tabDrop) === null || _a === void 0 ? void 0 : _a.markDrop(null, null);
1577
+ gesture.strip = null;
1578
+ }
1491
1579
  if (forwardSlab) {
1492
- (_a = forwardSlab.slabCancel) === null || _a === void 0 ? void 0 : _a.call(forwardSlab);
1580
+ (_b = forwardSlab.slabCancel) === null || _b === void 0 ? void 0 : _b.call(forwardSlab);
1493
1581
  forwardSlab = null;
1494
1582
  }
1495
1583
  if (slabGesture)
@@ -1514,7 +1602,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1514
1602
  engine.endGesture();
1515
1603
  const items = [];
1516
1604
  for (const [id, c] of g.startCells) {
1517
- const lockedNode = ((_c = (_b = diagram.getNode(id)) === null || _b === void 0 ? void 0 : _b.state) === null || _c === void 0 ? void 0 : _c.locked) === true;
1605
+ const lockedNode = ((_d = (_c = diagram.getNode(id)) === null || _c === void 0 ? void 0 : _c.state) === null || _d === void 0 ? void 0 : _d.locked) === true;
1518
1606
  items.push(Object.assign(Object.assign({ id }, c), { locked: lockedNode || isGroupMember(id) }));
1519
1607
  }
1520
1608
  engine = engineFrom(items);
@@ -1562,7 +1650,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1562
1650
  enforceBoardHeight();
1563
1651
  api.renderNow();
1564
1652
  if (notify) {
1565
- (_d = options.onGesture) === null || _d === void 0 ? void 0 : _d.call(options, { type: 'cancel', kind: g.kind, nodeId: g.id, changed: false });
1653
+ (_e = options.onGesture) === null || _e === void 0 ? void 0 : _e.call(options, { type: 'cancel', kind: g.kind, nodeId: g.id, changed: false });
1566
1654
  }
1567
1655
  };
1568
1656
  /** Centre a w×h-span tile's top-left under the cursor, in world px. */
@@ -1577,7 +1665,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1577
1665
  };
1578
1666
  };
1579
1667
  const onToolMove = (ev) => {
1580
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1668
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1581
1669
  const g = gesture;
1582
1670
  if (!g || g.kind === 'palette')
1583
1671
  return;
@@ -1594,6 +1682,36 @@ export function bindDashboardGrid(api, group, options = {}) {
1594
1682
  ghostStyleFastPath(g, desired);
1595
1683
  g.lastWorld = { x: ev.world.x, y: ev.world.y };
1596
1684
  g.lastScreen = { x: ev.screen.x, y: ev.screen.y };
1685
+ // A TAB STRIP under the pointer wins over every board: the widget will
1686
+ // become a new tab there, so it leaves this board (survivors settle
1687
+ // home) and the strip marks the slot.
1688
+ if (options.tabDrop && !isStatic) {
1689
+ const crect = api.container.getBoundingClientRect();
1690
+ const hitStrip = options.tabDrop.stripAt(crect.left + ev.screen.x, crect.top + ev.screen.y);
1691
+ if (hitStrip) {
1692
+ if (g.leg) {
1693
+ g.leg.adopted.abort();
1694
+ g.leg = null;
1695
+ }
1696
+ if (!g.removedFromBoard) {
1697
+ g.removedFromBoard = true;
1698
+ engine.remove(g.id);
1699
+ project();
1700
+ }
1701
+ (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-out');
1702
+ if (!g.strip || g.strip.containerId !== hitStrip.containerId || g.strip.index !== hitStrip.index) {
1703
+ options.tabDrop.markDrop(hitStrip.containerId, hitStrip.index);
1704
+ }
1705
+ g.strip = hitStrip;
1706
+ syncPlaceholder();
1707
+ api.render();
1708
+ return;
1709
+ }
1710
+ if (g.strip) {
1711
+ options.tabDrop.markDrop(null, null);
1712
+ g.strip = null;
1713
+ }
1714
+ }
1597
1715
  // Deepest board under the pointer wins: the nested KPI strip beats the
1598
1716
  // tab that contains it; a foreign board beats "outside". Strict frames
1599
1717
  // first; the one-row grace band below each board (gridstack's extra
@@ -1633,13 +1751,13 @@ export function bindDashboardGrid(api, group, options = {}) {
1633
1751
  height: g.node.size.height,
1634
1752
  });
1635
1753
  if (adopted) {
1636
- (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-out');
1754
+ (_b = hostOf(g.id)) === null || _b === void 0 ? void 0 : _b.classList.remove('axdb-out');
1637
1755
  g.leg = { peer, adopted };
1638
1756
  g.leg.adopted.move(ev.world);
1639
1757
  }
1640
1758
  else {
1641
1759
  // Bounded/full board refused the adoption: dim = will snap home.
1642
- (_b = hostOf(g.id)) === null || _b === void 0 ? void 0 : _b.classList.add('axdb-out');
1760
+ (_c = hostOf(g.id)) === null || _c === void 0 ? void 0 : _c.classList.add('axdb-out');
1643
1761
  }
1644
1762
  }
1645
1763
  }
@@ -1651,7 +1769,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1651
1769
  }
1652
1770
  if (g.removedFromBoard) {
1653
1771
  g.removedFromBoard = false;
1654
- (_c = hostOf(g.id)) === null || _c === void 0 ? void 0 : _c.classList.remove('axdb-out');
1772
+ (_d = hostOf(g.id)) === null || _d === void 0 ? void 0 : _d.classList.remove('axdb-out');
1655
1773
  const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), g.spans.w);
1656
1774
  // Re-enter at the bottom edge (collision-free), then take the cursor
1657
1775
  // cell GATELESSLY — a first placement skips the anti-jitter gate.
@@ -1660,7 +1778,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1660
1778
  project();
1661
1779
  }
1662
1780
  else {
1663
- const spanW = (_e = (_d = engine.getItem(g.id)) === null || _d === void 0 ? void 0 : _d.w) !== null && _e !== void 0 ? _e : g.spans.w;
1781
+ const spanW = (_f = (_e = engine.getItem(g.id)) === null || _e === void 0 ? void 0 : _e.w) !== null && _f !== void 0 ? _f : g.spans.w;
1664
1782
  const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), spanW);
1665
1783
  if (engine.moveCheck(g.id, cell.x, cell.y).changed)
1666
1784
  project();
@@ -1675,7 +1793,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1675
1793
  if (!g.removedFromBoard) {
1676
1794
  g.removedFromBoard = true;
1677
1795
  engine.remove(g.id); // survivors settle home; cells minted nowhere
1678
- (_f = hostOf(g.id)) === null || _f === void 0 ? void 0 : _f.classList.add('axdb-out');
1796
+ (_g = hostOf(g.id)) === null || _g === void 0 ? void 0 : _g.classList.add('axdb-out');
1679
1797
  project();
1680
1798
  }
1681
1799
  }
@@ -1785,7 +1903,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1785
1903
  maxRows = n;
1786
1904
  engine.maxRows = n;
1787
1905
  };
1788
- const slabRows = (_h = (_g = parent.memberCell(group.id)) === null || _g === void 0 ? void 0 : _g.h) !== null && _h !== void 0 ? _h : maxRows;
1906
+ const slabRows = (_j = (_h = parent.memberCell(group.id)) === null || _h === void 0 ? void 0 : _h.h) !== null && _j !== void 0 ? _j : maxRows;
1789
1907
  const inner = maxRows;
1790
1908
  const rhNow = rowHeightFor(geom(), rows());
1791
1909
  const rowPx = rhNow + gap;
@@ -1949,7 +2067,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1949
2067
  const anchorBottom = liveRect ? liveRect.y + liveRect.height : bottom;
1950
2068
  const px = E.w ? anchorRight - w : anchorLeft;
1951
2069
  const py = E.n ? anchorBottom - h : anchorTop;
1952
- g.node.setSize(w, h, (_j = g.node.size.depth) !== null && _j !== void 0 ? _j : 0);
2070
+ g.node.setSize(w, h, (_k = g.node.size.depth) !== null && _k !== void 0 ? _k : 0);
1953
2071
  g.node.setPosition(px, py);
1954
2072
  ghostStyleFastPath(g, { x: px, y: py, width: w, height: h });
1955
2073
  const spanF = bound() !== undefined ? frame() : f;
@@ -1990,7 +2108,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1990
2108
  syncPlaceholder();
1991
2109
  };
1992
2110
  const onToolUp = () => {
1993
- var _a, _b, _c, _d, _e, _f;
2111
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1994
2112
  const g = gesture;
1995
2113
  if (!g || g.kind === 'palette')
1996
2114
  return;
@@ -1998,6 +2116,36 @@ export function bindDashboardGrid(api, group, options = {}) {
1998
2116
  gesture = null; // a plain click — the page's own click-to-focus handles it
1999
2117
  return;
2000
2118
  }
2119
+ if (g.strip && options.tabDrop) {
2120
+ // -- INTO A STRIP: the widget becomes a new tab of that container ------
2121
+ const target = g.strip;
2122
+ g.strip = null;
2123
+ options.tabDrop.markDrop(null, null);
2124
+ if (g.leg) {
2125
+ g.leg.adopted.abort();
2126
+ g.leg = null;
2127
+ }
2128
+ const displaced = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
2129
+ const snap = g.startGeom.get(g.id);
2130
+ if (snap) {
2131
+ g.node.setPosition(snap.pos.x, snap.pos.y);
2132
+ g.node.setSize(snap.size.width, snap.size.height, (_a = snap.size.depth) !== null && _a !== void 0 ? _a : 0);
2133
+ }
2134
+ const cmds = options.tabDrop.dropIntoStrip(g.id, target.containerId, target.index, group.id, displaced);
2135
+ if (cmds.length === 0) {
2136
+ cancelActiveGesture();
2137
+ return;
2138
+ }
2139
+ engine.endGesture();
2140
+ cleanupGestureVisuals(g);
2141
+ gesture = null;
2142
+ execute('Move widget into a new tab', cmds);
2143
+ enforceBoardHeight();
2144
+ persistLayouts();
2145
+ api.renderNow();
2146
+ (_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed: true });
2147
+ return;
2148
+ }
2001
2149
  if (g.leg) {
2002
2150
  // -- CROSS-CONTAINER COMMIT: one batch across both boards -----------
2003
2151
  const fin = g.leg.adopted.finalize();
@@ -2040,21 +2188,25 @@ export function bindDashboardGrid(api, group, options = {}) {
2040
2188
  isGroup: false,
2041
2189
  cellBefore: before !== null && before !== void 0 ? before : fin.cell,
2042
2190
  cellAfter: fin.cell,
2043
- posBefore: (_a = geomBefore === null || geomBefore === void 0 ? void 0 : geomBefore.pos) !== null && _a !== void 0 ? _a : { x: fin.rect.x, y: fin.rect.y },
2191
+ posBefore: (_c = geomBefore === null || geomBefore === void 0 ? void 0 : geomBefore.pos) !== null && _c !== void 0 ? _c : { x: fin.rect.x, y: fin.rect.y },
2044
2192
  posAfter: { x: fin.rect.x, y: fin.rect.y },
2045
- sizeBefore: (_b = geomBefore === null || geomBefore === void 0 ? void 0 : geomBefore.size) !== null && _b !== void 0 ? _b : { width: fin.rect.width, height: fin.rect.height },
2193
+ sizeBefore: (_d = geomBefore === null || geomBefore === void 0 ? void 0 : geomBefore.size) !== null && _d !== void 0 ? _d : { width: fin.rect.width, height: fin.rect.height },
2046
2194
  sizeAfter: { width: fin.rect.width, height: fin.rect.height },
2047
2195
  },
2048
2196
  ]),
2049
2197
  ];
2050
- execute('Move widget', crossing);
2198
+ // …and whatever follows a member out of this board: an emptied page
2199
+ // closes — in which case the membership commands ride INSIDE one
2200
+ // sequence with it, or the batch could never undo (its group is gone).
2201
+ const leaving = (_f = (_e = options.onMemberLeaving) === null || _e === void 0 ? void 0 : _e.call(options, g.id)) !== null && _f !== void 0 ? _f : [];
2202
+ execute('Move widget', leaving.length > 0 ? [new SequenceCommand('Move widget', [...crossing, ...leaving])] : crossing);
2051
2203
  engine.endGesture();
2052
2204
  cleanupGestureVisuals(g);
2053
2205
  gesture = null;
2054
2206
  enforceBoardHeight();
2055
2207
  persistLayouts();
2056
2208
  api.renderNow();
2057
- (_c = options.onGesture) === null || _c === void 0 ? void 0 : _c.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed: true });
2209
+ (_g = options.onGesture) === null || _g === void 0 ? void 0 : _g.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed: true });
2058
2210
  return;
2059
2211
  }
2060
2212
  if (g.removedFromBoard && dragOut === 'cancel') {
@@ -2085,15 +2237,15 @@ export function bindDashboardGrid(api, group, options = {}) {
2085
2237
  // Park the node on its start rect so the page's RemoveNodeCommand
2086
2238
  // captures sane geometry for undo.
2087
2239
  g.node.setPosition(snap.pos.x, snap.pos.y);
2088
- g.node.setSize(snap.size.width, snap.size.height, (_d = snap.size.depth) !== null && _d !== void 0 ? _d : 0);
2240
+ g.node.setSize(snap.size.width, snap.size.height, (_h = snap.size.depth) !== null && _h !== void 0 ? _h : 0);
2089
2241
  }
2090
2242
  engine.endGesture();
2091
2243
  cleanupGestureVisuals(g);
2092
2244
  gesture = null;
2093
2245
  enforceBoardHeight();
2094
2246
  api.renderNow();
2095
- void ((_e = options.onRemoveRequest) === null || _e === void 0 ? void 0 : _e.call(options, g.id, displaced));
2096
- (_f = options.onGesture) === null || _f === void 0 ? void 0 : _f.call(options, { type: 'remove', kind: g.kind, nodeId: g.id, changed: true });
2247
+ void ((_j = options.onRemoveRequest) === null || _j === void 0 ? void 0 : _j.call(options, g.id, displaced));
2248
+ (_k = options.onGesture) === null || _k === void 0 ? void 0 : _k.call(options, { type: 'remove', kind: g.kind, nodeId: g.id, changed: true });
2097
2249
  return;
2098
2250
  }
2099
2251
  commitGesture(g);
@@ -2314,8 +2466,29 @@ export function bindDashboardGrid(api, group, options = {}) {
2314
2466
  project();
2315
2467
  syncPlaceholder();
2316
2468
  },
2469
+ leave: () => {
2470
+ if (!engine.getItem(node.id))
2471
+ return;
2472
+ engine.remove(node.id); // displaced tiles come home (gesture memory)
2473
+ lastWant = null;
2474
+ project();
2475
+ syncPlaceholder();
2476
+ },
2477
+ enter: (w) => {
2478
+ if (!engine.getItem(node.id) && !engine.add({ id: node.id, x: 0, y: engine.rows(), w: span.w, h: hNatural }))
2479
+ return;
2480
+ const item = engine.getItem(node.id);
2481
+ if (!item)
2482
+ return;
2483
+ const cell = wantedCell(w.x, w.y, item.w, opts.fit === 'shrink' ? hNatural : item.h);
2484
+ lastWant = cell;
2485
+ place(cell, item.w);
2486
+ project();
2487
+ syncPlaceholder();
2488
+ },
2317
2489
  abort: () => {
2318
- engine.remove(node.id);
2490
+ if (engine.getItem(node.id))
2491
+ engine.remove(node.id);
2319
2492
  engine.cancelGesture(); // pre-entry layout, memory cleared
2320
2493
  adoptedGhostId = null;
2321
2494
  disarmGlideSoon();
@@ -2367,6 +2540,56 @@ export function bindDashboardGrid(api, group, options = {}) {
2367
2540
  beginSlabResize(id, edges, ev);
2368
2541
  return (slabGesture === null || slabGesture === void 0 ? void 0 : slabGesture.id) === id;
2369
2542
  },
2543
+ beginSlabMove: (id, ev) => {
2544
+ if (isStatic)
2545
+ return false;
2546
+ beginSlabMove(id, ev);
2547
+ return (slabGesture === null || slabGesture === void 0 ? void 0 : slabGesture.id) === id;
2548
+ },
2549
+ dragMember: (id, ev) => {
2550
+ var _a;
2551
+ if (isStatic || disposed || !engine.getItem(id) || gesture || slabGesture)
2552
+ return false;
2553
+ const toTool = (e) => {
2554
+ var _a;
2555
+ const rect = api.container.getBoundingClientRect();
2556
+ const world = ((_a = api.viewport) === null || _a === void 0 ? void 0 : _a.clientToWorld) ? api.viewport.clientToWorld(e.clientX, e.clientY, rect) : { x: e.clientX - rect.left, y: e.clientY - rect.top };
2557
+ return { world, screen: { x: e.clientX - rect.left, y: e.clientY - rect.top }, source: e };
2558
+ };
2559
+ beginSlabMove(id, toTool(ev));
2560
+ if (((_a = currentSlab()) === null || _a === void 0 ? void 0 : _a.id) !== id)
2561
+ return false;
2562
+ const detachAll = () => {
2563
+ window.removeEventListener('pointermove', onMove, true);
2564
+ window.removeEventListener('pointerup', onUp, true);
2565
+ window.removeEventListener('pointercancel', onCancel, true);
2566
+ window.removeEventListener('keydown', onKey, true);
2567
+ };
2568
+ const onMove = (e) => {
2569
+ var _a;
2570
+ if (disposed || ((_a = currentSlab()) === null || _a === void 0 ? void 0 : _a.id) !== id)
2571
+ return detachAll();
2572
+ slabMove(toTool(e));
2573
+ api.render();
2574
+ };
2575
+ const onUp = () => {
2576
+ detachAll();
2577
+ slabUp();
2578
+ };
2579
+ const onCancel = () => {
2580
+ detachAll();
2581
+ slabCancel();
2582
+ };
2583
+ const onKey = (e) => {
2584
+ if (e.key === 'Escape')
2585
+ onCancel();
2586
+ };
2587
+ window.addEventListener('pointermove', onMove, true);
2588
+ window.addEventListener('pointerup', onUp, true);
2589
+ window.addEventListener('pointercancel', onCancel, true);
2590
+ window.addEventListener('keydown', onKey, true);
2591
+ return true;
2592
+ },
2370
2593
  slabMove: (ev) => slabMove(ev),
2371
2594
  slabUp: () => slabUp(),
2372
2595
  slabCancel: () => slabCancel(),
@@ -2473,7 +2696,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2473
2696
  return insideMemberGroupFrame(ev.world.x, ev.world.y) || worldInsideBoard(ev.world.x, ev.world.y);
2474
2697
  },
2475
2698
  onPointerDown(ev, hit) {
2476
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
2699
+ 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;
2477
2700
  if (gesture)
2478
2701
  return; // mid-palette
2479
2702
  const target = ((_b = (_a = ev.source) === null || _a === void 0 ? void 0 : _a.target) !== null && _b !== void 0 ? _b : null);
@@ -2511,6 +2734,10 @@ export function bindDashboardGrid(api, group, options = {}) {
2511
2734
  : slabEdgesNear(grp, ev.world.x, ev.world.y);
2512
2735
  if (anyEdge(edges))
2513
2736
  beginSlabResize(slabId, edges, ev);
2737
+ // The caption band is the section's handle: pressed and travelled,
2738
+ // it moves the whole section (its inner empty space only selects).
2739
+ else if (ownCaption)
2740
+ beginSlabMove(slabId, ev);
2514
2741
  return;
2515
2742
  }
2516
2743
  // OUR OWN empty band, and we are a section of a parent board: the
@@ -2527,12 +2754,14 @@ export function bindDashboardGrid(api, group, options = {}) {
2527
2754
  : slabEdgesNear(group, ev.world.x, ev.world.y);
2528
2755
  if (anyEdge(edges) && parent.beginSlabResize(group.id, edges, ev))
2529
2756
  forwardSlab = parent;
2757
+ else if (!anyEdge(edges) && captionId === group.id && ((_r = parent.beginSlabMove) === null || _r === void 0 ? void 0 : _r.call(parent, group.id, ev)))
2758
+ forwardSlab = parent;
2530
2759
  }
2531
2760
  return;
2532
2761
  }
2533
2762
  // The board's own empty area: a void click. Nothing to drag, and the
2534
2763
  // selection clears exactly as a click outside any board would.
2535
- (_s = (_r = diagram).clearSelection) === null || _s === void 0 ? void 0 : _s.call(_r);
2764
+ (_t = (_s = diagram).clearSelection) === null || _t === void 0 ? void 0 : _t.call(_s);
2536
2765
  selectWidget(undefined);
2537
2766
  api.render();
2538
2767
  return;
@@ -2541,17 +2770,17 @@ export function bindDashboardGrid(api, group, options = {}) {
2541
2770
  if (!node)
2542
2771
  return;
2543
2772
  selectWidget(node.id); // a press selects, whether or not it starts a gesture
2544
- if (((_t = node.state) === null || _t === void 0 ? void 0 : _t.locked) === true)
2773
+ if (((_u = node.state) === null || _u === void 0 ? void 0 : _u.locked) === true)
2545
2774
  return; // pinned: refuse; click still focuses
2546
2775
  if (isStatic)
2547
2776
  return; // a static board: claimed and deadened, click still focuses
2548
2777
  // Which edges did the press take? The corner handle names its own (s+e,
2549
2778
  // or s+w on RTL); a bare press within EDGE_GRIP of the tile's border
2550
2779
  // takes that border; anywhere else is a move. A grip press is a move.
2551
- const onHandle = !!((_u = target === null || target === void 0 ? void 0 : target.closest) === null || _u === void 0 ? void 0 : _u.call(target, '.axdb-rs'));
2780
+ const onHandle = !!((_v = target === null || target === void 0 ? void 0 : target.closest) === null || _v === void 0 ? void 0 : _v.call(target, '.axdb-rs'));
2552
2781
  const hostEl = hostOf(node.id);
2553
- const resizable = ((_v = node.getMetadata) === null || _v === void 0 ? void 0 : _v.call(node, 'widgetResizable')) !== false;
2554
- const movable = ((_w = node.getMetadata) === null || _w === void 0 ? void 0 : _w.call(node, 'widgetMovable')) !== false;
2782
+ const resizable = ((_w = node.getMetadata) === null || _w === void 0 ? void 0 : _w.call(node, 'widgetResizable')) !== false;
2783
+ const movable = ((_x = node.getMetadata) === null || _x === void 0 ? void 0 : _x.call(node, 'widgetMovable')) !== false;
2555
2784
  // `ev.screen` is ELEMENT-LOCAL px; host rects are in client px. Compare
2556
2785
  // like with like — the source event's clientX/Y when there is one, else
2557
2786
  // the container's origin plus the local offset.
@@ -2599,9 +2828,10 @@ export function bindDashboardGrid(api, group, options = {}) {
2599
2828
  startSize: { width: node.size.width, height: node.size.height },
2600
2829
  startPos: { x: node.position.x, y: node.position.y },
2601
2830
  edges,
2602
- spans: { w: (_x = it === null || it === void 0 ? void 0 : it.w) !== null && _x !== void 0 ? _x : 1, h: (_y = it === null || it === void 0 ? void 0 : it.h) !== null && _y !== void 0 ? _y : 1 },
2831
+ 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 },
2603
2832
  removedFromBoard: false,
2604
2833
  leg: null,
2834
+ strip: null,
2605
2835
  lastWorld: null,
2606
2836
  lastScreen: null,
2607
2837
  hostEl: null,
@@ -2920,6 +3150,48 @@ export function bindDashboardGrid(api, group, options = {}) {
2920
3150
  chip.className = 'axdb-drag-chip axdb-tab-chip';
2921
3151
  chip.textContent = plan.label;
2922
3152
  doc.body.appendChild(chip);
3153
+ // Over ANOTHER tab container the page joins it instead of taking a cell:
3154
+ // the tile leaves the board (its displaced tiles come home), the target's
3155
+ // frame lights up and its strip marks where the tab will go.
3156
+ const layer = htmlLayer();
3157
+ const targets = plan.joinTargets
3158
+ .map((id) => diagram.getGroup(id))
3159
+ .filter((g) => !!g && g.id !== fromGroupId);
3160
+ const targetAt = (wx, wy) => { var _a; return (_a = targets.find((t) => worldInsideGroup(t, wx, wy))) !== null && _a !== void 0 ? _a : null; };
3161
+ let over = null;
3162
+ let joinEl = null;
3163
+ const showJoin = (g) => {
3164
+ if (!layer || g.id === fromGroupId)
3165
+ return; // reordering along its own strip: the mark is enough
3166
+ if (!joinEl) {
3167
+ joinEl = doc.createElement('div');
3168
+ joinEl.className = 'axdb-join';
3169
+ layer.prepend(joinEl);
3170
+ }
3171
+ const sz = sizeOf(g);
3172
+ joinEl.style.left = `${g.position.x}px`;
3173
+ joinEl.style.top = `${g.position.y}px`;
3174
+ joinEl.style.width = `${sz.width}px`;
3175
+ joinEl.style.height = `${sz.height}px`;
3176
+ };
3177
+ const hideJoin = () => {
3178
+ joinEl === null || joinEl === void 0 ? void 0 : joinEl.remove();
3179
+ joinEl = null;
3180
+ plan.markDrop(null, null);
3181
+ };
3182
+ const setOver = (g, world) => {
3183
+ if (g === over)
3184
+ return;
3185
+ over = g;
3186
+ if (g) {
3187
+ leg.leave();
3188
+ showJoin(g);
3189
+ }
3190
+ else {
3191
+ hideJoin();
3192
+ leg.enter(world);
3193
+ }
3194
+ };
2923
3195
  const moveChip = (cx, cy) => {
2924
3196
  chip.style.left = `${cx + 6}px`;
2925
3197
  chip.style.top = `${cy + 6}px`;
@@ -2934,6 +3206,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2934
3206
  window.removeEventListener('pointercancel', onCancel, true);
2935
3207
  window.removeEventListener('keydown', onKey, true);
2936
3208
  };
3209
+ let homeIndex = null;
2937
3210
  const onMove = (e) => {
2938
3211
  if (disposed)
2939
3212
  return detach();
@@ -2941,27 +3214,60 @@ export function bindDashboardGrid(api, group, options = {}) {
2941
3214
  moveChip(e.clientX, e.clientY);
2942
3215
  const world = toWorld(e.clientX, e.clientY);
2943
3216
  const home = worldInsideGroup(from, world.x, world.y);
2944
- // Over its own container it goes home: a tab dragged around its own
2945
- // strip must not tear itself out.
2946
- chip.classList.toggle('axdb-out', home || !worldInsideBoardGrace(world.x, world.y));
2947
- if (!home)
3217
+ // Over its OWN strip the tab reorders: the strip marks the slot and the
3218
+ // tile leaves the board. Over its own body it goes home (a cancel).
3219
+ homeIndex = home ? plan.stripIndex(fromGroupId, e.clientX, e.clientY) : null;
3220
+ const target = home ? null : targetAt(world.x, world.y);
3221
+ setOver(target !== null && target !== void 0 ? target : (homeIndex !== null ? from : null), world);
3222
+ chip.classList.toggle('axdb-out', (home && homeIndex === null) || (!target && !home && !worldInsideBoardGrace(world.x, world.y)));
3223
+ if (target)
3224
+ plan.markDrop(target.id, plan.dropIndex(target.id, e.clientX, e.clientY));
3225
+ else if (homeIndex !== null)
3226
+ plan.markDrop(fromGroupId, homeIndex);
3227
+ else if (!home)
2948
3228
  leg.move(world);
2949
3229
  api.render();
2950
3230
  };
2951
3231
  const finish = (commit) => {
2952
- var _a, _b;
3232
+ var _a, _b, _c, _d;
2953
3233
  detach();
2954
3234
  chip.remove();
3235
+ hideJoin();
2955
3236
  tearing = null;
2956
3237
  if (disposed)
2957
3238
  return;
2958
3239
  const world = toWorld(last.x, last.y);
2959
3240
  const home = worldInsideGroup(from, world.x, world.y);
3241
+ if (commit && home && homeIndex !== null) {
3242
+ // REORDER along its own strip.
3243
+ leg.abort();
3244
+ const cmds = plan.reorder(homeIndex);
3245
+ const changed = cmds.length > 0 ? execute('Reorder tab', cmds) : false;
3246
+ disarmGlideSoon();
3247
+ api.renderNow();
3248
+ (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: changed ? 'commit' : 'cancel', kind: 'move', nodeId: pageId, changed });
3249
+ return;
3250
+ }
3251
+ const target = commit && !home ? targetAt(world.x, world.y) : null;
3252
+ if (target) {
3253
+ // JOIN: no cell on this board — the leg is abandoned (its displaced
3254
+ // tiles are already home) and the page becomes the target's tab.
3255
+ const index = plan.dropIndex(target.id, last.x, last.y);
3256
+ leg.abort();
3257
+ const planned = plan.join(target.id, index, group.id);
3258
+ const changed = execute('Move tab', [...planned.move, ...planned.collapse]);
3259
+ disarmGlideSoon();
3260
+ enforceBoardHeight();
3261
+ persistLayouts();
3262
+ api.renderNow();
3263
+ (_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'commit', kind: 'move', nodeId: pageId, changed });
3264
+ return;
3265
+ }
2960
3266
  if (!commit || home || !worldInsideBoardGrace(world.x, world.y)) {
2961
3267
  leg.abort();
2962
3268
  disarmGlideSoon();
2963
3269
  api.renderNow();
2964
- (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'cancel', kind: 'move', nodeId: pageId, changed: false });
3270
+ (_c = options.onGesture) === null || _c === void 0 ? void 0 : _c.call(options, { type: 'cancel', kind: 'move', nodeId: pageId, changed: false });
2965
3271
  return;
2966
3272
  }
2967
3273
  const fin = leg.finalize();
@@ -2972,15 +3278,13 @@ export function bindDashboardGrid(api, group, options = {}) {
2972
3278
  }
2973
3279
  const planned = plan.commands(fin.cell, fin.rect, group.id);
2974
3280
  // The container the page left may have been holding its last page: it
2975
- // goes too, and the tiles around its slab settle the way a removal
2976
- // settles them (VS Code closes a group whose last editor leaves).
2977
- const collapse = planned.collapse.length > 0 ? [...planRemovalOf(fromGroupId), ...planned.collapse] : [];
2978
- const changed = execute('Move tab out', [...fin.commands, ...planned.move, ...collapse]);
3281
+ // goes too (the plan carries the removal's settle).
3282
+ const changed = execute('Move tab out', [...fin.commands, ...planned.move, ...planned.collapse]);
2979
3283
  disarmGlideSoon();
2980
3284
  enforceBoardHeight();
2981
3285
  persistLayouts();
2982
3286
  api.renderNow();
2983
- (_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'commit', kind: 'move', nodeId: pageId, changed });
3287
+ (_d = options.onGesture) === null || _d === void 0 ? void 0 : _d.call(options, { type: 'commit', kind: 'move', nodeId: pageId, changed });
2984
3288
  };
2985
3289
  const onUp = () => finish(true);
2986
3290
  const onCancel = () => finish(false);
@@ -3023,6 +3327,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3023
3327
  spans: { w: Math.max(1, spec.w), h: Math.max(1, spec.h) },
3024
3328
  removedFromBoard: true,
3025
3329
  leg: null,
3330
+ strip: null,
3026
3331
  lastWorld: null,
3027
3332
  lastScreen: null,
3028
3333
  hostEl: null,
@@ -3136,7 +3441,15 @@ export function bindDashboardGrid(api, group, options = {}) {
3136
3441
  return false;
3137
3442
  engine.beginGesture();
3138
3443
  const snap = snapshotAll();
3139
- if (!op()) {
3444
+ // A section is a LOCKED tile; for ITS OWN move it is the one moving.
3445
+ const self = engine.getItem(id);
3446
+ const wasLocked = !!(self === null || self === void 0 ? void 0 : self.locked);
3447
+ if (self && isGroupMember(id))
3448
+ self.locked = false;
3449
+ const ok = op();
3450
+ if (self && isGroupMember(id))
3451
+ self.locked = wasLocked;
3452
+ if (!ok) {
3140
3453
  engine.endGesture();
3141
3454
  return false;
3142
3455
  }