@grafloria/element 0.4.50 → 0.4.52

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,11 +1,11 @@
1
1
  {
2
2
  "name": "@grafloria/element",
3
- "version": "0.4.50",
3
+ "version": "0.4.52",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
7
7
  "peerDependencies": {
8
- "@grafloria/engine": "^0.3.8",
8
+ "@grafloria/engine": "^0.3.9",
9
9
  "@grafloria/renderer": "^0.4.0"
10
10
  },
11
11
  "dependencies": {
@@ -227,7 +227,7 @@ export interface DashboardOptions {
227
227
  * so a designer/viewer pair is one flag apart. Live: `handle.setStatic()`.
228
228
  */
229
229
  static?: boolean;
230
- /** The deepest board a drop may enter (a view is 0; default 2). See the grid binder's `nesting`. */
230
+ /** The deepest board a drop may enter (a view is 0; unbounded by default). See the grid binder's `nesting`. */
231
231
  nesting?: number;
232
232
  /**
233
233
  * DRAG HANDLE — DevExpress drags an item by its caption. `true`: the caption
@@ -243,7 +243,7 @@ export interface DashboardGridOptions {
243
243
  * counting a view as 0 — a widget into a section is 1, into a section
244
244
  * inside a tab page is 2. Deeper containers are opaque to a drag: the
245
245
  * pointer over them means a cell on their parent board (or beside them).
246
- * Default 2, the depth the gates exercise.
246
+ * Unbounded by default.
247
247
  */
248
248
  nesting?: number;
249
249
  /**
@@ -670,7 +670,7 @@ interface AdoptedLeg {
670
670
  y: number;
671
671
  w: number;
672
672
  h: number;
673
- }): boolean;
673
+ }, pushSolid?: boolean): boolean;
674
674
  /** Every other tile's cell before the ghost entered — the layout a row insert translates. */
675
675
  baseline(): Map<string, CellRect>;
676
676
  /** Where the tile sits right now, or null while it is off the board. */
@@ -56,7 +56,7 @@ import { buildCommitCommands, cellFromGridItem, cellToRect, columnUnitFor, gridI
56
56
  import { ensureDashboardKitStyles } from './styles.js';
57
57
  import { captionOfGroup, captionPainted, captionPassThrough, captionKey, paintCaptionBand, sectionCaptionReserve, sizeCaptionBand } from './caption.js';
58
58
  import { TAB_STRIP_HEIGHT } from './tabs.js';
59
- import { BESIDE_BAND, resolve as resolveZone } from './zones.js';
59
+ import { BESIDE_BAND, resolve as resolveZone, resolveTabZone } from './zones.js';
60
60
  /** The class of the painted grip element (a child of the node host). */
61
61
  export const GRIP_CLASS = 'axdb-grip';
62
62
  /** The container class that turns the caption strip's grip dots on. */
@@ -367,8 +367,8 @@ export function bindDashboardGrid(api, group, options = {}) {
367
367
  const fluid = options.fluid === true;
368
368
  const overflow = (_h = options.overflow) !== null && _h !== void 0 ? _h : 'bounded';
369
369
  let isStatic = options.static === true;
370
- /** The deepest board a drop may enter — a root is 0; two levels are what the gates exercise (tile first, step 2). */
371
- const nesting = (_j = options.nesting) !== null && _j !== void 0 ? _j : 2;
370
+ /** The deepest board a drop may enter — a root is 0. Unbounded unless asked for: three-level drops are in the specs today (tile first, step 2). */
371
+ const nesting = (_j = options.nesting) !== null && _j !== void 0 ? _j : Number.POSITIVE_INFINITY;
372
372
  let dragHandle = normalizeDragHandle(options.dragHandle);
373
373
  let dragSel = dragHandleSelector(dragHandle);
374
374
  api.container.classList.toggle(DRAG_HANDLE_CLASS, dragHandle === true);
@@ -624,10 +624,14 @@ export function bindDashboardGrid(api, group, options = {}) {
624
624
  const cell = grp
625
625
  ? cellFromGridItem((_g = grp.getMetadata) === null || _g === void 0 ? void 0 : _g.call(grp, 'gridItem'))
626
626
  : null;
627
- // Member groups are LOCKED slabs (see the module doc).
627
+ // A member group is a SOLID tile (tile first, step 3): a widget carried
628
+ // over it slides aside and it never packs — the board under the hand
629
+ // holds still, so its zones (into, a tab, beside) stay reachable — and
630
+ // it is moved by INTENT: placed beside, pushed by a moved section, a
631
+ // dock or a widget it refused, moved by its own gesture.
628
632
  if (cell)
629
- return Object.assign(Object.assign({ id }, cell), { locked: true });
630
- return { id, x: 0, y: 0, w: columns, h: 1, locked: true, autoPosition: true };
633
+ return Object.assign(Object.assign({ id }, cell), { solid: true });
634
+ return { id, x: 0, y: 0, w: columns, h: 1, solid: true, autoPosition: true };
631
635
  };
632
636
  /** Persist adopted cells so save/undo round-trips them. */
633
637
  const persistAdoptedCell = (id, item) => {
@@ -1288,7 +1292,13 @@ export function bindDashboardGrid(api, group, options = {}) {
1288
1292
  * carried along the top of a tall panel to its far right means "after it",
1289
1293
  * not "above it". The shift is undone if the pointer leaves the band.
1290
1294
  */
1291
- /** The container a BESIDE drop unlocked (and maybe shifted): relocked, and put back if asked, when the zone or the gesture ends. */
1295
+ /**
1296
+ * The beside the hand holds (tile first, step 3): the container's cell and
1297
+ * frame AT REST (the resolve reads the held container at rest), the cell
1298
+ * the widget took, and the row it took it at. The shift itself is the
1299
+ * engine's `placeBeside`; the sections it pushed remember their cells and
1300
+ * come back by themselves when the widget leaves (S2).
1301
+ */
1292
1302
  let beside = null;
1293
1303
  const endBeside = (restore) => {
1294
1304
  if (!beside)
@@ -1296,28 +1306,22 @@ export function bindDashboardGrid(api, group, options = {}) {
1296
1306
  const it = engine.getItem(beside.id);
1297
1307
  let moved = false;
1298
1308
  if (it && restore && (it.x !== beside.from.x || it.y !== beside.from.y))
1299
- moved = engine.moveCheck(beside.id, beside.from.x, beside.from.y, { gate: false }).changed || moved;
1300
- if (restore) {
1301
- // The sections the shift pushed come back with it.
1302
- for (const [oid, c] of beside.others) {
1303
- const o = engine.getItem(oid);
1304
- if (o && (o.x !== c.x || o.y !== c.y))
1305
- moved = engine.moveCheck(oid, c.x, c.y, { gate: false }).changed || moved;
1306
- }
1307
- }
1308
- if (it)
1309
- it.locked = true;
1310
- relockOthersForSlab();
1309
+ moved = engine.moveCheck(beside.id, beside.from.x, beside.from.y, { gate: false }).changed;
1311
1310
  beside = null;
1312
1311
  // The frames the zone test reads are the MODEL's: a restore that moved
1313
1312
  // the container without projecting left its frame where the push had
1314
- // put it, so the next test read the pointer as OUTSIDE it (the user's
1315
- // 3440-px corner on 0.4.48: along the top band the panel was pushed
1316
- // down, and the corner never turned into "right").
1313
+ // put it (the user's 3440-px corner on 0.4.48).
1317
1314
  if (moved)
1318
1315
  project();
1319
1316
  };
1320
- const applyBeside = (g, z) => {
1317
+ /** The row of this board's grid under a world y — where a beside lands (D3: the pointer's row, not the container's top). */
1318
+ const rowOfPoint = (y) => {
1319
+ const r0 = cellToRect({ x: 0, y: 0, w: 1, h: 1 }, frame(), geom(), rows());
1320
+ const r1 = cellToRect({ x: 0, y: 1, w: 1, h: 1 }, frame(), geom(), rows());
1321
+ const pitch = Math.max(1, r1.y - r0.y);
1322
+ return Math.max(0, Math.floor((y - r0.y) / pitch));
1323
+ };
1324
+ const applyBeside = (g, z, row) => {
1321
1325
  var _a;
1322
1326
  const tc = engine.getItem(z.id);
1323
1327
  if (!tc)
@@ -1326,71 +1330,26 @@ export function bindDashboardGrid(api, group, options = {}) {
1326
1330
  g.leg.adopted.abort();
1327
1331
  g.leg = null;
1328
1332
  }
1329
- const w = g.spans.w;
1330
- const h = g.spans.h;
1331
1333
  if (g.removedFromBoard) {
1332
1334
  g.removedFromBoard = false;
1333
1335
  (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-out');
1334
- engine.add({ id: g.id, x: 0, y: engine.rows(), w, h });
1336
+ engine.add({ id: g.id, x: 0, y: engine.rows(), w: g.spans.w, h: g.spans.h });
1335
1337
  }
1336
1338
  if (beside && (beside.id !== z.id || beside.side !== z.side))
1337
- endBeside(true); // another container, or another side of it: start over from the rest layout
1338
- const from = beside ? beside.from : { x: tc.x, y: tc.y };
1339
- let cell;
1340
- let shiftTo = null;
1341
- switch (z.side) {
1342
- case 'right':
1343
- cell = { x: from.x + tc.w, y: from.y };
1344
- if (cell.x + w > columns) {
1345
- shiftTo = { x: columns - w - tc.w, y: from.y };
1346
- cell = { x: columns - w, y: from.y };
1347
- }
1348
- break;
1349
- case 'left':
1350
- cell = { x: from.x - w, y: from.y };
1351
- if (cell.x < 0) {
1352
- shiftTo = { x: w, y: from.y };
1353
- cell = { x: 0, y: from.y };
1354
- }
1355
- break;
1356
- case 'top':
1357
- cell = { x: Math.max(0, Math.min(from.x, columns - w)), y: from.y }; // the ghost takes the container's rows; the container is pushed down under it
1358
- break;
1359
- default:
1360
- cell = { x: Math.max(0, Math.min(from.x, columns - w)), y: from.y + tc.h };
1361
- }
1362
- if (shiftTo && shiftTo.x < 0)
1363
- shiftTo = null; // no room even shifted: the widget goes where it can
1339
+ endBeside(true); // another container, or another side of it: from the rest layout
1364
1340
  if (!beside) {
1365
- // The other sections give way to the shift the way they give way to a
1366
- // moved group (0.4.44): on the demo the Operations section spans the
1367
- // row under the panel and a locked tile refused the shift outright.
1368
- // Their cells are kept so they come back if the pointer leaves.
1369
- const others = new Map();
1370
- for (const o of engine.getItems())
1371
- if (o.id !== z.id && o.id !== g.id && isGroupMember(o.id))
1372
- others.set(o.id, { x: o.x, y: o.y });
1373
1341
  const grp0 = diagram.getGroup(z.id);
1374
- beside = { id: z.id, side: z.side, from, frame0: grp0 ? frameOfGroup(grp0) : cellToRect({ x: from.x, y: from.y, w: tc.w, h: tc.h }, frame(), geom(), rows()), vacated: cell, others };
1375
- unlockOthersForSlab(z.id);
1342
+ beside = { id: z.id, side: z.side, from: { x: tc.x, y: tc.y }, frame0: grp0 ? frameOfGroup(grp0) : cellToRect({ x: tc.x, y: tc.y, w: tc.w, h: tc.h }, frame(), geom(), rows()), vacated: { x: tc.x, y: tc.y }, row };
1376
1343
  }
1377
- tc.locked = false; // for the gesture: it shifts, or the ghost pushes it
1378
- // The ghost steps off the board while the container shifts: the engine
1379
- // will not push the dragged tile, so a chart as wide as the panel had its
1380
- // own cell as the panel's target and the shift was refused (lab L94).
1381
- // Then it takes the cell the shift vacated.
1382
- if (shiftTo && (tc.x !== shiftTo.x || tc.y !== shiftTo.y)) {
1383
- if (engine.getItem(g.id))
1384
- engine.remove(g.id);
1385
- engine.moveCheck(z.id, shiftTo.x, shiftTo.y, { gate: false });
1344
+ const r = engine.placeBeside(g.id, z.id, z.side, row);
1345
+ const at = engine.getItem(g.id);
1346
+ if (r.changed && at) {
1347
+ beside.vacated = { x: at.x, y: at.y };
1348
+ beside.row = row;
1386
1349
  }
1387
- beside.vacated = cell;
1388
- if (!engine.getItem(g.id))
1389
- engine.add({ id: g.id, x: 0, y: engine.rows(), w, h });
1390
- if (!engine.moveCheck(g.id, cell.x, cell.y, { gate: false }).changed) {
1391
- const at = engine.getItem(g.id);
1392
- if (!at || at.x !== cell.x || at.y !== cell.y)
1393
- placeNear(g.id, cell.x, cell.y, w);
1350
+ else if (at) {
1351
+ // Refused (a bound): the widget goes where it can, as any refused cell does.
1352
+ placeNear(g.id, at.x, at.y, g.spans.w);
1394
1353
  }
1395
1354
  project();
1396
1355
  };
@@ -1610,13 +1569,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1610
1569
  },
1611
1570
  move: false,
1612
1571
  };
1613
- // A TOP or LEFT edge moves the slab's origin, and the engine refuses to
1614
- // move a locked tile: the move never landed, only the resize did, and a
1615
- // section pulled up by its caption grew DOWNWARD a row per pointer step
1616
- // — 18 rows for a 2-row pull (identification round, F16). Unlocked for
1617
- // its own gesture, as a move is; relocked on release.
1618
- if (edges.n || edges.w)
1619
- it.locked = false;
1620
1572
  capturePointer(slabGesture.pointerId);
1621
1573
  api.container.style.cursor = cursorFor(edges);
1622
1574
  };
@@ -1633,24 +1585,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1633
1585
  * pushes it; a group's children ride along: the frame moves, the nested
1634
1586
  * board re-projects. The others relock on release.
1635
1587
  */
1636
- let slabUnlocked = [];
1637
- const unlockOthersForSlab = (id) => {
1638
- slabUnlocked = [];
1639
- for (const o of engine.getItems()) {
1640
- if (o.id !== id && o.locked && isGroupMember(o.id)) {
1641
- o.locked = false;
1642
- slabUnlocked.push(o.id);
1643
- }
1644
- }
1645
- };
1646
- const relockOthersForSlab = () => {
1647
- for (const oid of slabUnlocked) {
1648
- const o = engine.getItem(oid);
1649
- if (o)
1650
- o.locked = true;
1651
- }
1652
- slabUnlocked = [];
1653
- };
1654
1588
  const beginSlabMove = (id, ev) => {
1655
1589
  const grp = diagram.getGroup(id);
1656
1590
  const it = engine.getItem(id);
@@ -1658,8 +1592,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1658
1592
  return;
1659
1593
  engine.beginGesture();
1660
1594
  const snap = snapshotAll();
1661
- it.locked = false;
1662
- unlockOthersForSlab(id);
1663
1595
  slabGesture = {
1664
1596
  id,
1665
1597
  edges: NO_EDGES,
@@ -1676,11 +1608,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1676
1608
  capturePointer(slabGesture.pointerId);
1677
1609
  api.container.style.cursor = 'grabbing';
1678
1610
  };
1679
- const relockSlab = (id) => {
1680
- const it = engine.getItem(id);
1681
- if (it)
1682
- it.locked = true;
1683
- };
1684
1611
  /** The cell a slab move asked for and could not have — painted so the refusal is visible; null clears it. */
1685
1612
  let refusal = null;
1686
1613
  const showRefusal = (cell, w, h) => {
@@ -1734,7 +1661,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1734
1661
  // Along its ROW only: a section dragged LEFT that wandered DOWN its
1735
1662
  // column (the row search) read as the wrong tile moving.
1736
1663
  const was = { x: it.x, y: it.y };
1737
- const moved = engine.moveCheck(g.id, cell.x, cell.y, { gate: false }).changed || placeOnRow(g.id, cell.x, cell.y, it.w);
1664
+ const moved = engine.moveCheck(g.id, cell.x, cell.y, { gate: false, pushSolid: true }).changed || placeOnRow(g.id, cell.x, cell.y, it.w, true); // a moved section pushes the sections in its way (0.4.44)
1738
1665
  if (moved) {
1739
1666
  project();
1740
1667
  setCarried(g.id, true); // chrome repainted by the projection is carried too
@@ -1781,7 +1708,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1781
1708
  }
1782
1709
  let changed = false;
1783
1710
  if (x !== it.x || y !== it.y)
1784
- changed = engine.moveCheck(g.id, x, y, { gate: false }).changed || changed;
1711
+ changed = engine.moveCheck(g.id, x, y, { gate: false, pushSolid: true }).changed || changed;
1785
1712
  if (w !== it.w || h !== it.h)
1786
1713
  changed = engine.resizeCheck(g.id, w, h).changed || changed;
1787
1714
  if (changed)
@@ -1796,8 +1723,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1796
1723
  showRefusal(null, 0, 0);
1797
1724
  releasePointer(g.pointerId);
1798
1725
  api.container.style.cursor = '';
1799
- relockSlab(g.id);
1800
- relockOthersForSlab();
1801
1726
  if (g.move && g.started)
1802
1727
  setCarried(g.id, false); // exempt through the drop write, then the glides resume
1803
1728
  if (!g.started) {
@@ -1833,8 +1758,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1833
1758
  slabGesture = null;
1834
1759
  releasePointer(g.pointerId);
1835
1760
  api.container.style.cursor = '';
1836
- relockSlab(g.id);
1837
- relockOthersForSlab();
1838
1761
  if (g.move && g.started)
1839
1762
  setCarried(g.id, false);
1840
1763
  if (g.started)
@@ -1953,7 +1876,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1953
1876
  (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed });
1954
1877
  };
1955
1878
  const cancelActiveGesture = (notify = true) => {
1956
- var _a, _b, _c, _d, _e;
1879
+ var _a, _b, _c;
1957
1880
  if (gesture === null || gesture === void 0 ? void 0 : gesture.strip) {
1958
1881
  (_a = options.tabDrop) === null || _a === void 0 ? void 0 : _a.markDrop(null, null);
1959
1882
  gesture.strip = null;
@@ -1978,21 +1901,10 @@ export function bindDashboardGrid(api, group, options = {}) {
1978
1901
  }
1979
1902
  endBeside(true);
1980
1903
  if (g.started) {
1981
- if (g.removedFromBoard || g.kind === 'palette') {
1982
- // The engine cannot resurrect a removed item rebuild from the
1983
- // gesture-start snapshot (cells are pure data; the constructor
1984
- // honours legal layouts verbatim).
1985
- engine.endGesture();
1986
- const items = [];
1987
- for (const [id, c] of g.startCells) {
1988
- const lockedNode = ((_d = (_c = diagram.getNode(id)) === null || _c === void 0 ? void 0 : _c.state) === null || _d === void 0 ? void 0 : _d.locked) === true;
1989
- items.push(Object.assign(Object.assign({ id }, c), { locked: lockedNode || isGroupMember(id) }));
1990
- }
1991
- engine = engineFrom(items);
1992
- }
1993
- else {
1994
- engine.cancelGesture();
1995
- }
1904
+ // The engine's gesture snapshot restores cells, sizes AND membership
1905
+ // (engine 0.3.8): a ghost removed mid-gesture comes back at its start
1906
+ // cell, a palette tile added mid-gesture is gone.
1907
+ engine.cancelGesture();
1996
1908
  if (designRows !== undefined) {
1997
1909
  maxRows = liveBound(engine.getItems());
1998
1910
  engine.maxRows = maxRows;
@@ -2033,7 +1945,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2033
1945
  enforceBoardHeight();
2034
1946
  api.renderNow();
2035
1947
  if (notify) {
2036
- (_e = options.onGesture) === null || _e === void 0 ? void 0 : _e.call(options, { type: 'cancel', kind: g.kind, nodeId: g.id, changed: false });
1948
+ (_c = options.onGesture) === null || _c === void 0 ? void 0 : _c.call(options, { type: 'cancel', kind: g.kind, nodeId: g.id, changed: false });
2037
1949
  }
2038
1950
  };
2039
1951
  /** Centre a w×h-span tile's top-left under the cursor, in world px. */
@@ -2048,7 +1960,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2048
1960
  };
2049
1961
  };
2050
1962
  const onToolMove = (ev) => {
2051
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
1963
+ var _a, _b, _c, _d, _e, _f, _g, _h;
2052
1964
  const g = gesture;
2053
1965
  if (!g || g.kind === 'palette')
2054
1966
  return;
@@ -2103,8 +2015,9 @@ export function bindDashboardGrid(api, group, options = {}) {
2103
2015
  // (A container on another board still resolves as a plain cell there
2104
2016
  // until the gesture scope spans boards — step 3.)
2105
2017
  if (z.kind === 'beside' && !isStatic && z.board.ref === selfPeer) {
2106
- if (!z.kept)
2107
- applyBeside(g, { id: z.containerId, side: z.side });
2018
+ const row = rowOfPoint(ev.world.y);
2019
+ if (!z.kept || !beside || beside.row !== row)
2020
+ applyBeside(g, { id: z.containerId, side: z.side }, row);
2108
2021
  syncPlaceholder();
2109
2022
  return;
2110
2023
  }
@@ -2115,7 +2028,13 @@ export function bindDashboardGrid(api, group, options = {}) {
2115
2028
  const onSelf = z.kind !== 'off' && z.kind !== 'strip' && z.board.ref === selfPeer;
2116
2029
  const inside = onSelf || (z.kind === 'off' && worldInsideBoardGrace(ev.world.x, ev.world.y));
2117
2030
  const peer = !onSelf && z.kind !== 'off' && z.kind !== 'strip' ? z.board.ref : null;
2118
- if (peer) {
2031
+ if (peer && g.refusedPeer === peer && parentPeerOf(api.container, peer.group.id) === selfPeer) {
2032
+ // -- REFUSED, PUSHED (D2): a section of this board that cannot take
2033
+ // the widget is pushed by it instead — the widget takes the cell
2034
+ // under the hand on THIS board, and means it.
2035
+ placeOnSelf(g, desired, true);
2036
+ }
2037
+ else if (peer) {
2119
2038
  // -- HANDOFF: the pointer is over another board -------------------
2120
2039
  if (g.leg && g.leg.peer === peer) {
2121
2040
  g.leg.adopted.move(ev.world);
@@ -2135,38 +2054,31 @@ export function bindDashboardGrid(api, group, options = {}) {
2135
2054
  height: g.node.size.height,
2136
2055
  });
2137
2056
  if (adopted) {
2057
+ g.refusedPeer = null;
2138
2058
  (_c = hostOf(g.id)) === null || _c === void 0 ? void 0 : _c.classList.remove('axdb-out');
2139
2059
  g.leg = { peer, adopted };
2140
2060
  g.leg.adopted.move(ev.world);
2141
2061
  }
2062
+ else if (parentPeerOf(api.container, peer.group.id) === selfPeer) {
2063
+ // A section of this board refused (full, fit): the widget pushes it (D2).
2064
+ g.refusedPeer = peer;
2065
+ placeOnSelf(g, desired, true);
2066
+ }
2142
2067
  else {
2143
- // Bounded/full board refused the adoption: dim = will snap home.
2068
+ // A board deeper down refused the adoption: dim = will snap home (the N-board scope is step 4).
2069
+ g.refusedPeer = peer;
2144
2070
  (_d = hostOf(g.id)) === null || _d === void 0 ? void 0 : _d.classList.add('axdb-out');
2145
2071
  }
2146
2072
  }
2147
2073
  }
2148
2074
  else if (inside) {
2149
2075
  // -- back on (or still on) our own board --------------------------
2076
+ g.refusedPeer = null;
2150
2077
  if (g.leg) {
2151
2078
  g.leg.adopted.abort();
2152
2079
  g.leg = null;
2153
2080
  }
2154
- if (g.removedFromBoard) {
2155
- g.removedFromBoard = false;
2156
- (_e = hostOf(g.id)) === null || _e === void 0 ? void 0 : _e.classList.remove('axdb-out');
2157
- const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), g.spans.w);
2158
- // Re-enter at the bottom edge (collision-free), then take the cursor
2159
- // cell GATELESSLY — a first placement skips the anti-jitter gate.
2160
- engine.add({ id: g.id, x: 0, y: engine.rows(), w: g.spans.w, h: g.spans.h });
2161
- engine.moveCheck(g.id, cell.x, cell.y, { gate: false });
2162
- project();
2163
- }
2164
- else {
2165
- const spanW = (_g = (_f = engine.getItem(g.id)) === null || _f === void 0 ? void 0 : _f.w) !== null && _g !== void 0 ? _g : g.spans.w;
2166
- const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), spanW);
2167
- if (engine.moveCheck(g.id, cell.x, cell.y).changed)
2168
- project();
2169
- }
2081
+ placeOnSelf(g, desired);
2170
2082
  }
2171
2083
  else {
2172
2084
  // -- outside every board ------------------------------------------
@@ -2177,7 +2089,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2177
2089
  if (!g.removedFromBoard) {
2178
2090
  g.removedFromBoard = true;
2179
2091
  engine.remove(g.id); // survivors settle home; cells minted nowhere
2180
- (_h = hostOf(g.id)) === null || _h === void 0 ? void 0 : _h.classList.add('axdb-out');
2092
+ (_e = hostOf(g.id)) === null || _e === void 0 ? void 0 : _e.classList.add('axdb-out');
2181
2093
  project();
2182
2094
  }
2183
2095
  }
@@ -2287,7 +2199,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2287
2199
  maxRows = n;
2288
2200
  engine.maxRows = n;
2289
2201
  };
2290
- const slabRows = (_k = (_j = parent.memberCell(group.id)) === null || _j === void 0 ? void 0 : _j.h) !== null && _k !== void 0 ? _k : maxRows;
2202
+ const slabRows = (_g = (_f = parent.memberCell(group.id)) === null || _f === void 0 ? void 0 : _f.h) !== null && _g !== void 0 ? _g : maxRows;
2291
2203
  const inner = maxRows;
2292
2204
  const rhNow = rowHeightFor(geom(), rows());
2293
2205
  const rowPx = rhNow + gap;
@@ -2451,7 +2363,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2451
2363
  const anchorBottom = liveRect ? liveRect.y + liveRect.height : bottom;
2452
2364
  const px = E.w ? anchorRight - w : anchorLeft;
2453
2365
  const py = E.n ? anchorBottom - h : anchorTop;
2454
- g.node.setSize(w, h, (_l = g.node.size.depth) !== null && _l !== void 0 ? _l : 0);
2366
+ g.node.setSize(w, h, (_h = g.node.size.depth) !== null && _h !== void 0 ? _h : 0);
2455
2367
  g.node.setPosition(px, py);
2456
2368
  ghostStyleFastPath(g, { x: px, y: py, width: w, height: h });
2457
2369
  const spanF = bound() !== undefined ? frame() : f;
@@ -2695,6 +2607,25 @@ export function bindDashboardGrid(api, group, options = {}) {
2695
2607
  });
2696
2608
  return peers.filter((p) => !p.group.parentGroupId).map((p) => boardRef(p, 0));
2697
2609
  };
2610
+ /** The ghost takes the cell under the hand on THIS board: re-entering at the bottom edge first (collision-free), then gatelessly; a tile already here moves through the gate. */
2611
+ const placeOnSelf = (g, desired, pushSolid = false) => {
2612
+ var _a, _b, _c;
2613
+ if (g.removedFromBoard) {
2614
+ g.removedFromBoard = false;
2615
+ (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-out');
2616
+ const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), g.spans.w);
2617
+ engine.add({ id: g.id, x: 0, y: engine.rows(), w: g.spans.w, h: g.spans.h });
2618
+ if (!engine.moveCheck(g.id, cell.x, cell.y, { gate: false, pushSolid }).changed)
2619
+ placeNear(g.id, cell.x, cell.y, g.spans.w, pushSolid);
2620
+ project();
2621
+ }
2622
+ else {
2623
+ const spanW = (_c = (_b = engine.getItem(g.id)) === null || _b === void 0 ? void 0 : _b.w) !== null && _c !== void 0 ? _c : g.spans.w;
2624
+ const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), spanW);
2625
+ if (engine.moveCheck(g.id, cell.x, cell.y, { pushSolid }).changed)
2626
+ project();
2627
+ }
2628
+ };
2698
2629
  /** What the pointer means for the dragged tile: the resolve over the live tree, with the beside the hand holds. */
2699
2630
  const resolveTileZone = (g, ev) => {
2700
2631
  let strip = null;
@@ -2712,8 +2643,19 @@ export function bindDashboardGrid(api, group, options = {}) {
2712
2643
  ghostDepth: 0,
2713
2644
  ghostSubtree: EMPTY_SUBTREE,
2714
2645
  gap,
2646
+ homeChain: homeChain(),
2715
2647
  });
2716
2648
  };
2649
+ /** The groups this board sits in, all the way up: their bands never apply to a tile of this board. */
2650
+ const homeChain = () => {
2651
+ const out = new Set();
2652
+ let cur = group;
2653
+ for (let i = 0; cur && i < 32; i++) {
2654
+ out.add(cur.id);
2655
+ cur = cur.parentGroupId ? diagram.getGroup(cur.parentGroupId) : undefined;
2656
+ }
2657
+ return out;
2658
+ };
2717
2659
  /** The board whose engine holds OUR group as an item (nesting parent). */
2718
2660
  const parentPeer = () => {
2719
2661
  for (const p of peersOnCanvas()) {
@@ -2756,7 +2698,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2756
2698
  * wherever the last swing left it. Ask where the tile IS, not whether the
2757
2699
  * call moved it.
2758
2700
  */
2759
- const placeOnRow = (id, x, y, w) => {
2701
+ const placeOnRow = (id, x, y, w, pushSolid = false) => {
2760
2702
  const at = (cx) => {
2761
2703
  const i = engine.getItem(id);
2762
2704
  return !!i && i.x === cx && i.y === y;
@@ -2764,7 +2706,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2764
2706
  const maxX = Math.max(0, columns - w);
2765
2707
  if (at(x))
2766
2708
  return true;
2767
- if (engine.moveCheck(id, x, y, { gate: false }).changed)
2709
+ if (engine.moveCheck(id, x, y, { gate: false, pushSolid }).changed)
2768
2710
  return true;
2769
2711
  for (let d = 1; d <= columns; d++) {
2770
2712
  for (const cx of [x - d, x + d]) {
@@ -2772,14 +2714,14 @@ export function bindDashboardGrid(api, group, options = {}) {
2772
2714
  continue;
2773
2715
  if (at(cx))
2774
2716
  return true;
2775
- if (engine.moveCheck(id, cx, y, { gate: false }).changed)
2717
+ if (engine.moveCheck(id, cx, y, { gate: false, pushSolid }).changed)
2776
2718
  return true;
2777
2719
  }
2778
2720
  }
2779
2721
  return false;
2780
2722
  };
2781
- const placeNear = (id, x, y, w) => {
2782
- if (placeOnRow(id, x, y, w))
2723
+ const placeNear = (id, x, y, w, pushSolid = false) => {
2724
+ if (placeOnRow(id, x, y, w, pushSolid))
2783
2725
  return true;
2784
2726
  // EVERY column on that row refused — which is what a locked section
2785
2727
  // spanning the full width does, and there are plenty of those. Sliding
@@ -2791,7 +2733,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2791
2733
  for (const cy of [y - d, y + d]) {
2792
2734
  if (cy < 0)
2793
2735
  continue;
2794
- if (placeOnRow(id, x, cy, w))
2736
+ if (placeOnRow(id, x, cy, w, pushSolid))
2795
2737
  return true;
2796
2738
  }
2797
2739
  }
@@ -2806,7 +2748,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2806
2748
  const fitHeightAt = (id, x, y, w, hNatural) => {
2807
2749
  let limit = hNatural;
2808
2750
  for (const it of engine.getItems()) {
2809
- if (it.id === id || !it.locked)
2751
+ if (it.id === id || !(it.locked || it.solid))
2810
2752
  continue;
2811
2753
  if (!(it.x < x + w && x < it.x + it.w))
2812
2754
  continue;
@@ -2935,7 +2877,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2935
2877
  const it = engine.getItem(node.id);
2936
2878
  return it ? cellToRect(it, frame(), geom(), rows()) : null;
2937
2879
  },
2938
- place: (cell) => {
2880
+ place: (cell, pushSolid = false) => {
2939
2881
  if (!engine.getItem(node.id) && !engine.add({ id: node.id, x: 0, y: engine.rows(), w: cell.w, h: cell.h }))
2940
2882
  return false;
2941
2883
  const it = engine.getItem(node.id);
@@ -2952,10 +2894,10 @@ export function bindDashboardGrid(api, group, options = {}) {
2952
2894
  engine.resizeCheck(node.id, w0, h0);
2953
2895
  const moved = engine.getItem(node.id);
2954
2896
  if (moved && (moved.x !== cell.x || moved.y !== cell.y))
2955
- engine.moveCheck(node.id, cell.x, cell.y, { gate: false });
2897
+ engine.moveCheck(node.id, cell.x, cell.y, { gate: false, pushSolid });
2956
2898
  const now = engine.getItem(node.id);
2957
2899
  if (now && (now.w !== cell.w || now.h !== cell.h))
2958
- engine.resizeCheck(node.id, cell.w, cell.h);
2900
+ engine.resizeCheck(node.id, cell.w, cell.h, { pushSolid });
2959
2901
  lastWant = null;
2960
2902
  project();
2961
2903
  syncPlaceholder();
@@ -3836,7 +3778,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3836
3778
  // thirds, and the user, putting a page back into the panel it came from,
3837
3779
  // never found the "into": "close to the edge means beside it, the content
3838
3780
  // area means inside, the header means a new tab" (0.4.42).
3839
- const EDGE_BAND = 0.2;
3781
+ const EDGE_BAND = BESIDE_BAND; // one band for a tab's split and a widget's beside (zones.ts)
3840
3782
  const visibleFrame = () => {
3841
3783
  const rect = api.container.getBoundingClientRect();
3842
3784
  const o = toWorld(rect.left, rect.top);
@@ -3954,47 +3896,55 @@ export function bindDashboardGrid(api, group, options = {}) {
3954
3896
  const zoneAt = (cx, cy, world) => {
3955
3897
  // A pointer OUTSIDE the visible canvas is off, full stop: a camera that
3956
3898
  // moved can map it to a world point inside a group, and that group
3957
- // took a join or a split from a release in the page header.
3958
- if (!clientInsideCanvasGrace(cx, cy))
3959
- return { kind: 'off' };
3960
- const target = targetAt(world.x, world.y);
3961
- const home = !target && worldInsideGroup(from, world.x, world.y);
3962
- // A strip is the most precise target there is: anyone's wins outright.
3963
- if (target) {
3964
- const idx = plan.stripIndex(target.id, cx, cy);
3965
- if (idx !== null)
3966
- return { kind: 'strip', target, index: idx };
3967
- }
3968
- else if (home) {
3969
- const idx = plan.stripIndex(fromGroupId, cx, cy);
3970
- if (idx !== null)
3971
- return { kind: 'reorder', index: idx };
3972
- }
3973
- // The board's own edges beat whatever sits against them — Dockview's
3974
- // container edges over its groups — so a group at the top of the board
3975
- // still leaves the top band to the board.
3976
- const root = rootAt(cx, cy);
3977
- if (root)
3978
- return root;
3979
- if (target) {
3980
- const f = anchor && anchor.g === target ? anchor.frame : frameOfGroup(target);
3981
- const stripH = anchor && anchor.g === target ? anchor.stripH : plan.stripHeight(target.id);
3982
- const bodyH = Math.max(1, f.height - stripH);
3983
- const rx = Math.min(1, Math.max(0, (world.x - f.x) / Math.max(1, f.width)));
3984
- const ry = Math.min(1, Math.max(0, (world.y - f.y - stripH) / bodyH));
3985
- if (rx >= EDGE_BAND && rx <= 1 - EDGE_BAND && ry >= EDGE_BAND && ry <= 1 - EDGE_BAND)
3986
- return { kind: 'join', target };
3987
- const d = [['left', rx], ['right', 1 - rx], ['top', ry], ['bottom', 1 - ry]];
3988
- d.sort((p, q) => p[1] - q[1]);
3989
- const h = halves(target, d[0][0]);
3990
- return h ? Object.assign({ kind: 'split', target, side: d[0][0] }, h) : { kind: 'join', target };
3991
- }
3992
- if (home)
3993
- return { kind: 'home' };
3899
+ // took a join or a split from a release in the page header. The
3900
+ // target is looked up (and anchored) only inside it.
3901
+ const clientInside = clientInsideCanvasGrace(cx, cy);
3902
+ const target = clientInside ? targetAt(world.x, world.y) : null;
3903
+ const root = clientInside ? rootAt(cx, cy) : null;
3994
3904
  const foreign = foreignAt(world.x, world.y);
3995
- if (foreign && (!worldInsideBoard(world.x, world.y) || foreign.frameArea() < boardArea()))
3996
- return { kind: 'board', peer: foreign };
3997
- return { kind: 'board' };
3905
+ // The ORDER is zones.ts's, shared with the split board: a strip, the
3906
+ // board's own edges, the target's fifth or middle, home, then a board.
3907
+ const tz = resolveTabZone({
3908
+ x: world.x,
3909
+ y: world.y,
3910
+ clientInside,
3911
+ ownStrip: plan.stripIndex(fromGroupId, cx, cy),
3912
+ stripOf: (id) => plan.stripIndex(id, cx, cy),
3913
+ root: root && root.kind === 'root' ? { side: root.side } : null,
3914
+ target: target
3915
+ ? {
3916
+ id: target.id,
3917
+ frame: anchor && anchor.g === target ? anchor.frame : frameOfGroup(target),
3918
+ stripHeight: anchor && anchor.g === target ? anchor.stripH : plan.stripHeight(target.id),
3919
+ }
3920
+ : null,
3921
+ home: worldInsideGroup(from, world.x, world.y) ? frameOfGroup(from) : null,
3922
+ homeBand: 0, // on a grid board the whole source frame is home
3923
+ band: EDGE_BAND,
3924
+ canSplit: (_id, side) => !!target && !!halves(target, side),
3925
+ pane: false,
3926
+ foreign: !!foreign && (!worldInsideBoard(world.x, world.y) || foreign.frameArea() < boardArea()),
3927
+ });
3928
+ switch (tz.kind) {
3929
+ case 'off':
3930
+ return { kind: 'off' };
3931
+ case 'strip':
3932
+ return { kind: 'strip', target: target, index: tz.index };
3933
+ case 'reorder':
3934
+ return { kind: 'reorder', index: tz.index };
3935
+ case 'root':
3936
+ return root;
3937
+ case 'join':
3938
+ return { kind: 'join', target: target };
3939
+ case 'split': {
3940
+ const h = halves(target, tz.side);
3941
+ return h ? Object.assign({ kind: 'split', target: target, side: tz.side }, h) : { kind: 'join', target: target };
3942
+ }
3943
+ case 'home':
3944
+ return { kind: 'home' };
3945
+ default:
3946
+ return tz.kind === 'board' && tz.foreign && foreign ? { kind: 'board', peer: foreign } : { kind: 'board' };
3947
+ }
3998
3948
  };
3999
3949
  const zoneKey = (z) => JSON.stringify(z, (k, v) => (k === 'target' ? v.id : k === 'peer' ? v.group.id : v));
4000
3950
  // DOCKING PUSHES SECTIONS. A section is a locked tile so that a widget
@@ -4003,25 +3953,6 @@ export function bindDashboardGrid(api, group, options = {}) {
4003
3953
  // preview every member group is unlocked; they are relocked when the
4004
3954
  // pointer leaves the band, and their moved cells are committed with the
4005
3955
  // dock.
4006
- let unlockedGroups = [];
4007
- const unlockGroups = () => {
4008
- if (unlockedGroups.length > 0)
4009
- return;
4010
- for (const it of engine.getItems()) {
4011
- if (it.id !== plan.arrivingId && it.locked && isGroupMember(it.id)) {
4012
- it.locked = false;
4013
- unlockedGroups.push(it.id);
4014
- }
4015
- }
4016
- };
4017
- const relockGroups = () => {
4018
- for (const id of unlockedGroups) {
4019
- const it = engine.getItem(id);
4020
- if (it)
4021
- it.locked = true;
4022
- }
4023
- unlockedGroups = [];
4024
- };
4025
3956
  const groupCommands = (fin, except) => fin.groups.filter((g) => g.id !== except).map((g) => new SetGroupCellCommand(g.id, g.cellBefore, g.cellAfter, g.frameBefore, g.frameAfter));
4026
3957
  // A TOP DOCK INSERTS ROWS. The engine's push cascade resolves the band's
4027
3958
  // collisions one tile at a time and scrambles what stood beneath it (the
@@ -4089,18 +4020,24 @@ export function bindDashboardGrid(api, group, options = {}) {
4089
4020
  engine.moveCheck(split.id, split.before.x, split.before.y, { gate: false });
4090
4021
  if (it.w !== split.before.w || it.h !== split.before.h)
4091
4022
  engine.resizeCheck(split.id, split.before.w, split.before.h);
4092
- it.locked = true;
4093
4023
  }
4094
4024
  split = null;
4095
4025
  project();
4096
4026
  };
4097
4027
  const previewSplit = (z, world) => {
4098
4028
  const it = engine.getItem(z.target.id);
4029
+ if (!it)
4030
+ return false;
4031
+ // The target's cell AT REST is the one the commit restores — read it
4032
+ // BEFORE the leg enters: the arriving ghost is placed under the pointer
4033
+ // first, and a target that is a tile like any other is pushed by it
4034
+ // for a moment (tile first, step 3) until it takes its half below.
4035
+ const before = { x: it.x, y: it.y, w: it.w, h: it.h };
4036
+ const frameBefore = frameOfGroup(z.target);
4099
4037
  const l = ensureLeg(world, null);
4100
- if (!it || !l)
4038
+ if (!l)
4101
4039
  return false;
4102
- 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 };
4103
- it.locked = false; // its own gesture for the moment: it may shrink and shift
4040
+ split = { id: z.target.id, before, frameBefore, keep: z.keep };
4104
4041
  if (it.w !== z.keep.w || it.h !== z.keep.h)
4105
4042
  engine.resizeCheck(z.target.id, z.keep.w, z.keep.h);
4106
4043
  const now = engine.getItem(z.target.id);
@@ -4122,8 +4059,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4122
4059
  const l = ensureLeg(world, null);
4123
4060
  if (!l)
4124
4061
  return false;
4125
- unlockGroups();
4126
- l.place(z.cell);
4062
+ l.place(z.cell, true); // DOCKING PUSHES SECTIONS: everything below the band moves down, solid or not
4127
4063
  insertRows(z.cell, l);
4128
4064
  project();
4129
4065
  placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
@@ -4145,7 +4081,6 @@ export function bindDashboardGrid(api, group, options = {}) {
4145
4081
  }
4146
4082
  undoSplitPreview();
4147
4083
  undoInsertRows();
4148
- relockGroups();
4149
4084
  plan.markDrop(null, null);
4150
4085
  hideOverlay();
4151
4086
  // A PREVIEW IS AN OVERLAY. A split and a dock used to be applied LIVE
@@ -4233,7 +4168,6 @@ export function bindDashboardGrid(api, group, options = {}) {
4233
4168
  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()))) {
4234
4169
  undoSplitPreview();
4235
4170
  leg === null || leg === void 0 ? void 0 : leg.abort();
4236
- relockGroups();
4237
4171
  done(false, 'cancel');
4238
4172
  return;
4239
4173
  }
@@ -4272,8 +4206,6 @@ export function bindDashboardGrid(api, group, options = {}) {
4272
4206
  const fin = (_b = leg === null || leg === void 0 ? void 0 : leg.finalize()) !== null && _b !== void 0 ? _b : null;
4273
4207
  const it = engine.getItem(z.target.id);
4274
4208
  const before = split;
4275
- if (it)
4276
- it.locked = true;
4277
4209
  split = null;
4278
4210
  if (!fin || !before || !it) {
4279
4211
  leg === null || leg === void 0 ? void 0 : leg.abort();
@@ -4301,7 +4233,6 @@ export function bindDashboardGrid(api, group, options = {}) {
4301
4233
  hideOverlay(); // a re-applied zone repaints its preview; the release is not a preview
4302
4234
  plan.markDrop(null, null);
4303
4235
  const fin = (_c = leg === null || leg === void 0 ? void 0 : leg.finalize()) !== null && _c !== void 0 ? _c : null;
4304
- relockGroups();
4305
4236
  if (!fin) {
4306
4237
  done(false, 'cancel');
4307
4238
  return;
@@ -4464,14 +4395,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4464
4395
  return false;
4465
4396
  engine.beginGesture();
4466
4397
  const snap = snapshotAll();
4467
- // A section is a LOCKED tile; for ITS OWN move it is the one moving.
4468
- const self = engine.getItem(id);
4469
- const wasLocked = !!(self === null || self === void 0 ? void 0 : self.locked);
4470
- if (self && isGroupMember(id))
4471
- self.locked = false;
4472
4398
  const ok = op();
4473
- if (self && isGroupMember(id))
4474
- self.locked = wasLocked;
4475
4399
  if (!ok) {
4476
4400
  engine.endGesture();
4477
4401
  return false;
@@ -4758,7 +4682,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4758
4682
  return buildCommitCommands(deltas);
4759
4683
  },
4760
4684
  moveTo(id, x, y) {
4761
- return programmatic('Move widget', id, () => engine.moveCheck(id, x, y).changed);
4685
+ return programmatic('Move widget', id, () => engine.moveCheck(id, x, y, { pushSolid: isGroupMember(id) }).changed);
4762
4686
  },
4763
4687
  resizeTo(id, w, h) {
4764
4688
  const hh = isGroupMember(id) ? Math.max(h, innerRowsOf(id)) : h; // a section: never below its children
@@ -23,6 +23,7 @@
23
23
  * the authored size.
24
24
  */
25
25
  import { __awaiter } from "tslib";
26
+ import { BESIDE_BAND, resolveTabZone } from './zones.js';
26
27
  import { BatchCommand, Command } from '@grafloria/engine';
27
28
  import { LiveRegionController, registerTool } from '@grafloria/renderer';
28
29
  import { anyEdge, clearOtherSelections, dragHandleSelector, gripHostOf, gripOf, normalizeDragHandle, ownsPress, parentPeerOf, pressOnDragHandle, registerBoardPeer, syncGrip, DRAG_HANDLE_CLASS, EDGE_GRIP } from './grid-binder.js';
@@ -1037,37 +1038,48 @@ export function bindDashboardSplit(api, group, options = {}) {
1037
1038
  return true;
1038
1039
  return cx >= rect.left - EDGE_GRACE && cx <= rect.right + EDGE_GRACE && cy >= rect.top - EDGE_GRACE && cy <= rect.bottom + EDGE_GRACE;
1039
1040
  };
1040
- /** The centre third of a container's BODY (below its strip), both axes — the grid board's join zone. */
1041
- const centreThird = (g, w) => {
1042
- const f = frameOfGroup(g);
1043
- if (!inRect(f, w.x, w.y))
1044
- return false;
1045
- const top = f.y + plan.stripHeight(g.id);
1046
- const h = Math.max(0, f.y + f.height - top);
1047
- return w.x >= f.x + f.width / 3 && w.x <= f.x + (2 * f.width) / 3 && w.y >= top + h / 3 && w.y <= top + (2 * h) / 3;
1048
- };
1049
1041
  const zoneAt = (cx, cy, w) => {
1050
- if (!clientInsideCanvasGrace(cx, cy))
1051
- return { kind: 'off' };
1052
- const own = plan.stripIndex(fromGroupId, cx, cy);
1053
- if (own !== null)
1054
- return { kind: 'reorder', index: own };
1055
- for (const t of targets) {
1056
- const i = plan.stripIndex(t.id, cx, cy);
1057
- if (i !== null)
1058
- return { kind: 'strip', targetId: t.id, index: i };
1059
- }
1060
- for (const t of targets)
1061
- if (centreThird(t, w))
1062
- return { kind: 'join', targetId: t.id };
1063
- if (centreThird(from, w))
1064
- return { kind: 'home' };
1065
- if (worldInsideBoard(w.x, w.y)) {
1066
- const t = dropTargetAt(tree0, w.x, w.y);
1067
- if (t)
1068
- return { kind: 'pane', target: t };
1042
+ var _a;
1043
+ // The ORDER is zones.ts's, shared with the grid board: a strip, the
1044
+ // target's fifth (a pane beside it on that side, the sides taking the
1045
+ // corners) or its middle (a join), home — the source's middle, its own
1046
+ // edges meaning a pane beside itself — then the pane under the pointer.
1047
+ // The fifth replaced the centre third here too (0.4.42's rule, at last
1048
+ // on both boards).
1049
+ const clientInside = clientInsideCanvasGrace(cx, cy);
1050
+ const target = clientInside ? ((_a = targets.find((t) => inRect(frameOfGroup(t), w.x, w.y))) !== null && _a !== void 0 ? _a : null) : null;
1051
+ const paneTarget = clientInside && worldInsideBoard(w.x, w.y) ? dropTargetAt(tree0, w.x, w.y) : null;
1052
+ const tz = resolveTabZone({
1053
+ x: w.x,
1054
+ y: w.y,
1055
+ clientInside,
1056
+ ownStrip: plan.stripIndex(fromGroupId, cx, cy),
1057
+ stripOf: (id) => plan.stripIndex(id, cx, cy),
1058
+ root: null,
1059
+ target: target ? { id: target.id, frame: frameOfGroup(target), stripHeight: plan.stripHeight(target.id) } : null,
1060
+ home: inRect(frameOfGroup(from), w.x, w.y) ? frameOfGroup(from) : null,
1061
+ homeBand: BESIDE_BAND,
1062
+ band: BESIDE_BAND,
1063
+ canSplit: () => true,
1064
+ pane: !!paneTarget,
1065
+ foreign: false,
1066
+ });
1067
+ switch (tz.kind) {
1068
+ case 'off':
1069
+ return { kind: 'off' };
1070
+ case 'strip':
1071
+ return { kind: 'strip', targetId: tz.targetId, index: tz.index };
1072
+ case 'reorder':
1073
+ return { kind: 'reorder', index: tz.index };
1074
+ case 'join':
1075
+ return { kind: 'join', targetId: tz.targetId };
1076
+ case 'split':
1077
+ return { kind: 'pane', target: { id: tz.targetId, side: tz.side, rect: frameOfGroup(target) } };
1078
+ case 'pane':
1079
+ return { kind: 'pane', target: paneTarget };
1080
+ default:
1081
+ return { kind: 'home' };
1069
1082
  }
1070
- return { kind: 'home' };
1071
1083
  };
1072
1084
  const apply = (z) => {
1073
1085
  plan.markDrop(z.kind === 'reorder' ? fromGroupId : z.kind === 'strip' ? z.targetId : null, z.kind === 'reorder' || z.kind === 'strip' ? z.index : null);
@@ -81,6 +81,13 @@ export interface ResolveInput {
81
81
  ghostSubtree: ReadonlySet<string>;
82
82
  /** The board's gap, the tolerance around the vacated cell. */
83
83
  gap: number;
84
+ /**
85
+ * The containers the dragged tile's OWN board sits in, innermost first or
86
+ * in any order: their bands do not apply to it. A widget leaving its page
87
+ * through the page's left fifth is moving within the page, not asking to
88
+ * be put beside the container it is still inside.
89
+ */
90
+ homeChain: ReadonlySet<string>;
84
91
  }
85
92
  export type Zone = {
86
93
  kind: 'off';
@@ -111,3 +118,74 @@ export declare const BESIDE_STAY = 0.05;
111
118
  */
112
119
  export declare function bandOf(f: ZoneRect, stripHeight: number, x: number, y: number, band?: number): BesideSide | null;
113
120
  export declare function resolve(input: ResolveInput): Zone;
121
+ /** A join target as the walk sees it: another tab container, its frame anchored by the binder while the pointer is inside it. */
122
+ export interface TabTarget {
123
+ id: string;
124
+ frame: ZoneRect;
125
+ stripHeight: number;
126
+ }
127
+ export interface TabZoneInput {
128
+ x: number;
129
+ y: number;
130
+ /** Inside the visible canvas plus its grace, in CLIENT space: off otherwise, whatever the world point says. */
131
+ clientInside: boolean;
132
+ /** The source strip's slot under the pointer (client space), else null. */
133
+ ownStrip: number | null;
134
+ /** A target strip's slot under the pointer (client space), else null. */
135
+ stripOf(targetId: string): number | null;
136
+ /** The board's own edge band under the pointer (client space) — a dock; null on boards that have none. */
137
+ root: {
138
+ side: BesideSide;
139
+ } | null;
140
+ /** The join target whose frame holds the point, or null. */
141
+ target: TabTarget | null;
142
+ /** The source group's frame, when the pointer is inside it. */
143
+ home: ZoneRect | null;
144
+ /** The source group's own band: 0 keeps its whole frame "home", a fifth lets its edges mean "beside myself". */
145
+ homeBand: number;
146
+ /** The outer fraction of a target's body that splits it (Dockview's fifth). */
147
+ band: number;
148
+ /** Whether the target can be halved on that side (a grid target too small refuses; a pane always can). */
149
+ canSplit(targetId: string, side: BesideSide): boolean;
150
+ /** A pane insertion exists under the pointer (split boards). */
151
+ pane: boolean;
152
+ /** A foreign board lies under the pointer and takes the drop (grid boards). */
153
+ foreign: boolean;
154
+ }
155
+ export type TabZone = {
156
+ kind: 'off';
157
+ } | {
158
+ kind: 'reorder';
159
+ index: number;
160
+ } | {
161
+ kind: 'strip';
162
+ targetId: string;
163
+ index: number;
164
+ } | {
165
+ kind: 'root';
166
+ side: BesideSide;
167
+ } | {
168
+ kind: 'join';
169
+ targetId: string;
170
+ } | {
171
+ kind: 'split';
172
+ targetId: string;
173
+ side: BesideSide;
174
+ } | {
175
+ kind: 'home';
176
+ } | {
177
+ kind: 'pane';
178
+ } | {
179
+ kind: 'board';
180
+ foreign: boolean;
181
+ };
182
+ /**
183
+ * What a pointer means for a TORN-OUT PAGE, in the one order both binders
184
+ * use: off the canvas, then a strip (the most precise target there is —
185
+ * a target's, or the source's own for a reorder), then the board's own
186
+ * edges (Dockview's container edges beat the groups against them), then the
187
+ * target under the pointer — its outer fifth splits it on that side, the
188
+ * sides taking the corners, its middle joins it — then home, then a pane, a
189
+ * foreign board, or this board.
190
+ */
191
+ export declare function resolveTabZone(input: TabZoneInput): TabZone;
@@ -106,22 +106,28 @@ export function resolve(input) {
106
106
  const px = x + dx;
107
107
  const py = y + dy;
108
108
  for (const c of board.children()) {
109
- const atRest = held && c.id === held.containerId ? held.frame0 : null;
109
+ // the held container at rest unless the hand has left that frame for the live one
110
+ const atRest = held && c.id === held.containerId && inRect(held.frame0, x, y) ? held.frame0 : null;
110
111
  const frame = atRest !== null && atRest !== void 0 ? atRest : c.frame;
111
112
  const tx = atRest ? x : px;
112
113
  const ty = atRest ? y : py;
113
114
  if (!inRect(frame, tx, ty))
114
115
  continue;
115
- const side = bandOf(frame, c.stripHeight, tx, ty, c.band);
116
+ // A band does not reach THROUGH a nested container: a hand over a
117
+ // section inside the page is inside the page, wherever the outer
118
+ // container's fifth falls (the inset margin is what remains of the
119
+ // band there). The page's plain widgets do not stop it.
120
+ const ndx0 = atRest ? c.frame.x - atRest.x : dx;
121
+ const ndy0 = atRest ? c.frame.y - atRest.y : dy;
122
+ const overNested = !!c.inner && c.inner.children().some((cc) => inRect(cc.frame, x + ndx0, y + ndy0));
123
+ const side = overNested || input.homeChain.has(c.id) ? null : bandOf(frame, c.stripHeight, tx, ty, c.band);
116
124
  if (side)
117
125
  return { kind: 'beside', board, containerId: c.id, side, kept: false };
118
126
  if (opaque(board, c) || !c.inner)
119
127
  return { kind: 'plain', board, grace: false };
120
- const ndx = atRest ? c.frame.x - atRest.x : dx;
121
- const ndy = atRest ? c.frame.y - atRest.y : dy;
122
- if (!c.inner.contains(x + ndx, y + ndy))
128
+ if (!c.inner.contains(x + ndx0, y + ndy0))
123
129
  return { kind: 'plain', board, grace: false }; // the margin: the container's own frame
124
- return descend(c.inner, ndx, ndy);
130
+ return descend(c.inner, ndx0, ndy0);
125
131
  }
126
132
  return { kind: 'plain', board, grace: false };
127
133
  };
@@ -159,4 +165,39 @@ function stripHeightOf(roots, containerId) {
159
165
  }
160
166
  return 0;
161
167
  }
168
+ /**
169
+ * What a pointer means for a TORN-OUT PAGE, in the one order both binders
170
+ * use: off the canvas, then a strip (the most precise target there is —
171
+ * a target's, or the source's own for a reorder), then the board's own
172
+ * edges (Dockview's container edges beat the groups against them), then the
173
+ * target under the pointer — its outer fifth splits it on that side, the
174
+ * sides taking the corners, its middle joins it — then home, then a pane, a
175
+ * foreign board, or this board.
176
+ */
177
+ export function resolveTabZone(input) {
178
+ if (!input.clientInside)
179
+ return { kind: 'off' };
180
+ const { x, y } = input;
181
+ if (input.target) {
182
+ const idx = input.stripOf(input.target.id);
183
+ if (idx !== null)
184
+ return { kind: 'strip', targetId: input.target.id, index: idx };
185
+ }
186
+ else if (input.ownStrip !== null)
187
+ return { kind: 'reorder', index: input.ownStrip };
188
+ if (input.root)
189
+ return { kind: 'root', side: input.root.side };
190
+ if (input.target) {
191
+ const t = input.target;
192
+ const side = bandOf(t.frame, t.stripHeight, x, y, input.band);
193
+ if (side && input.canSplit(t.id, side))
194
+ return { kind: 'split', targetId: t.id, side };
195
+ return { kind: 'join', targetId: t.id };
196
+ }
197
+ if (input.home && inRect(input.home, x, y) && !bandOf(input.home, 0, x, y, input.homeBand))
198
+ return { kind: 'home' };
199
+ if (input.pane)
200
+ return { kind: 'pane' };
201
+ return { kind: 'board', foreign: input.foreign };
202
+ }
162
203
  //# sourceMappingURL=zones.js.map