@grafloria/element 0.4.52 → 0.4.54

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.
@@ -50,13 +50,15 @@
50
50
  * second `bindDashboardGrid` on the section for a nested pack grid.
51
51
  */
52
52
  import { __awaiter } from "tslib";
53
- import { AddToGroupCommand, BatchCommand, Command, GridPackEngine, RemoveFromGroupCommand, } from '@grafloria/engine';
53
+ import { AddToGroupCommand, BatchCommand, GridPackEngine, RemoveFromGroupCommand, } from '@grafloria/engine';
54
54
  import { LiveRegionController, registerTool } from '@grafloria/renderer';
55
55
  import { buildCommitCommands, cellFromGridItem, cellToRect, columnUnitFor, gridItemFromCell, pointToCell, rowHeightFor, sizeToSpan, } from './grid-mapping.js';
56
56
  import { ensureDashboardKitStyles } from './styles.js';
57
57
  import { captionOfGroup, captionPainted, captionPassThrough, captionKey, paintCaptionBand, sectionCaptionReserve, sizeCaptionBand } from './caption.js';
58
58
  import { TAB_STRIP_HEIGHT } from './tabs.js';
59
59
  import { BESIDE_BAND, resolve as resolveZone, resolveTabZone } from './zones.js';
60
+ import { SequenceCommand, SetGroupCellCommand, tileCommands } from './commit.js';
61
+ export { SequenceCommand, SetGroupCellCommand } from './commit.js';
60
62
  /** The class of the painted grip element (a child of the node host). */
61
63
  export const GRIP_CLASS = 'axdb-grip';
62
64
  /** The container class that turns the caption strip's grip dots on. */
@@ -158,38 +160,6 @@ export function pressOnDragHandle(sel, target, hostEl, clientX, clientY) {
158
160
  export const CAPTION_BAND = 28;
159
161
  /** A torn-out page never arrives shorter than this: a strip with no room under it is not a group. */
160
162
  export const TEAR_OUT_MIN_ROWS = 2;
161
- /**
162
- * Steps that depend on each other's RESULT — create a group, then move a page
163
- * into it, then place the group on the board; or remove a member and then the
164
- * group it emptied — run as one history step. A batch checks every member's
165
- * `canExecute` before running any of them and every member's `canUndo` before
166
- * undoing any, and a membership command's precondition (its group exists) is
167
- * exactly what a neighbouring step creates or removes. This runs the chain in
168
- * order and reverses it on undo, judging nothing up front.
169
- */
170
- export class SequenceCommand extends Command {
171
- constructor(name, steps) {
172
- super(name);
173
- this.steps = steps;
174
- }
175
- execute(context) {
176
- for (const c of this.steps)
177
- c.execute(context);
178
- }
179
- undo(context) {
180
- for (let i = this.steps.length - 1; i >= 0; i--)
181
- this.steps[i].undo(context);
182
- }
183
- canExecute() {
184
- return true;
185
- }
186
- canUndo() {
187
- return true;
188
- }
189
- serialize() {
190
- return { id: this.id, name: this.name, timestamp: this.timestamp, data: { steps: this.steps.map((c) => c.serialize()) } };
191
- }
192
- }
193
163
  /**
194
164
  * Binders on the same canvas find each other here. A WeakMap: a canvas that
195
165
  * is gone takes its (empty) peer set with it — the strong Map it replaced kept
@@ -256,50 +226,6 @@ function directionName(dx, dy) {
256
226
  function describeCell(c) {
257
227
  return `column ${c.x + 1}, row ${c.y + 1}, ${c.w} by ${c.h}`;
258
228
  }
259
- /**
260
- * Undoable cell+frame write for a GROUP member (the strip's slab). The engine
261
- * has Move/Resize commands for nodes but none for a group's frame, and slab
262
- * cells live in group metadata — this closes nested height escalation into
263
- * the gesture's single BatchCommand so one undo restores the strip too.
264
- */
265
- class SetGroupCellCommand extends Command {
266
- constructor(groupId, cellBefore, cellAfter, frameBefore, frameAfter) {
267
- super('Resize section');
268
- this.groupId = groupId;
269
- this.cellBefore = cellBefore;
270
- this.cellAfter = cellAfter;
271
- this.frameBefore = frameBefore;
272
- this.frameAfter = frameAfter;
273
- }
274
- apply(context, cell, frame) {
275
- const diagram = context.diagram;
276
- const grp = diagram === null || diagram === void 0 ? void 0 : diagram.getGroup(this.groupId);
277
- if (!grp)
278
- return;
279
- grp.setMetadata('gridItem', gridItemFromCell(cell));
280
- grp.setFrame(Object.assign({}, frame));
281
- }
282
- execute(context) {
283
- this.apply(context, this.cellAfter, this.frameAfter);
284
- }
285
- undo(context) {
286
- this.apply(context, this.cellBefore, this.frameBefore);
287
- }
288
- serialize() {
289
- return {
290
- id: this.id,
291
- name: this.name,
292
- timestamp: this.timestamp,
293
- data: {
294
- groupId: this.groupId,
295
- cellBefore: this.cellBefore,
296
- cellAfter: this.cellAfter,
297
- frameBefore: this.frameBefore,
298
- frameAfter: this.frameAfter,
299
- },
300
- };
301
- }
302
- }
303
229
  const DRAG_THRESHOLD = 4;
304
230
  const GLIDE_OFF_DELAY = 400;
305
231
  /** A press this close (CSS px) to a tile's border takes that edge for a resize. */
@@ -1004,19 +930,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1004
930
  }
1005
931
  return out;
1006
932
  };
1007
- /** The member groups a gesture displaced, as their own cell commands — `buildCommitCommands` skips groups by design. */
1008
- const groupCellCommands = (deltas) => {
1009
- const out = [];
1010
- for (const d of deltas) {
1011
- if (!d.isGroup || (d.cellBefore.x === d.cellAfter.x && d.cellBefore.y === d.cellAfter.y && d.cellBefore.w === d.cellAfter.w && d.cellBefore.h === d.cellAfter.h))
1012
- continue;
1013
- const og = diagram.getGroup(d.id);
1014
- if (!og)
1015
- continue;
1016
- out.push(new SetGroupCellCommand(d.id, d.cellBefore, d.cellAfter, { x: d.posBefore.x, y: d.posBefore.y, width: d.sizeBefore.width, height: d.sizeBefore.height }, frameOfGroup(og)));
1017
- }
1018
- return out;
1019
- };
1020
933
  const execute = (name, commands) => {
1021
934
  if (commands.length === 0)
1022
935
  return false;
@@ -1024,20 +937,48 @@ export function bindDashboardGrid(api, group, options = {}) {
1024
937
  return true;
1025
938
  };
1026
939
  // -- membership + bounds sync ----------------------------------------------
940
+ /**
941
+ * A DROP LEAVES ITS TILE IN THE ENGINE, waiting for the member the commit
942
+ * adds under the same id — or, for a palette drop, for whatever the host's
943
+ * command adds: a host that mints its own id would otherwise leave the
944
+ * ghost behind as a phantom holding the cell. The next member to arrive
945
+ * claims the phantom's place: the same id simply takes it over, another id
946
+ * removes it first and lands where the drop settled.
947
+ */
948
+ let pendingDrop = null;
1027
949
  const onMemberAdded = (id) => {
1028
- var _a;
950
+ var _a, _b, _c;
1029
951
  if (disposed)
1030
952
  return;
953
+ // The cell a waiting tile held for an arriving member of ANOTHER id. The
954
+ // removal settles the board — the tiles the drop pushed float back into
955
+ // the hole — so the member cannot simply be added there: it collided and
956
+ // auto-positioned to the left edge (Quantia's "lands on the cell it was
957
+ // aimed at"). It enters at the bottom edge instead and takes the cell
958
+ // gatelessly, pushing them down again, all inside this one call.
959
+ let claim = null;
960
+ if (pendingDrop && pendingDrop !== id) {
961
+ const ph = engine.getItem(pendingDrop);
962
+ if (ph && !((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(pendingDrop)) {
963
+ claim = { x: ph.x, y: ph.y };
964
+ engine.remove(pendingDrop);
965
+ }
966
+ }
967
+ pendingDrop = null;
1031
968
  if (!engine.getItem(id)) {
1032
969
  const item = itemFor(id);
1033
- let placed = engine.add(item);
970
+ let placed = claim ? engine.add(Object.assign(Object.assign({}, item), { x: 0, y: engine.rows(), autoPosition: false })) : engine.add(item);
971
+ if (placed && claim) {
972
+ engine.moveCheck(id, claim.x, claim.y, { gate: false, pushSolid: !!placed.solid });
973
+ placed = (_b = engine.getItem(id)) !== null && _b !== void 0 ? _b : placed;
974
+ }
1034
975
  if (!placed && capacity !== undefined) {
1035
976
  // Membership is a document fact (an undo just restored it, say); a
1036
977
  // bounded board must not strand the node invisible. Lift the capacity
1037
978
  // for this adoption — it floors at the content on the next refresh.
1038
979
  capacity = undefined;
1039
980
  engine = engineFrom([...engine.getItems().map((i) => (Object.assign({}, i))), item]);
1040
- placed = (_a = engine.getItem(id)) !== null && _a !== void 0 ? _a : null;
981
+ placed = (_c = engine.getItem(id)) !== null && _c !== void 0 ? _c : null;
1041
982
  refreshCapacity();
1042
983
  }
1043
984
  if (placed)
@@ -1321,38 +1262,49 @@ export function bindDashboardGrid(api, group, options = {}) {
1321
1262
  const pitch = Math.max(1, r1.y - r0.y);
1322
1263
  return Math.max(0, Math.floor((y - r0.y) / pitch));
1323
1264
  };
1324
- const applyBeside = (g, z, row) => {
1325
- var _a;
1265
+ /**
1266
+ * The ghost `id` (spanning `spans`) beside container `z.id` on THIS board at
1267
+ * `row`: the engine shifts the container or pushes what stands behind it,
1268
+ * and the beside is remembered for the resolve's stickiness. One function
1269
+ * for a tile of this board and for a tile another board's gesture placed
1270
+ * here through its leg.
1271
+ */
1272
+ const besideOn = (id, spans, z, row) => {
1326
1273
  const tc = engine.getItem(z.id);
1327
1274
  if (!tc)
1328
1275
  return;
1329
- if (g.leg) {
1330
- g.leg.adopted.abort();
1331
- g.leg = null;
1332
- }
1333
- if (g.removedFromBoard) {
1334
- g.removedFromBoard = false;
1335
- (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-out');
1336
- engine.add({ id: g.id, x: 0, y: engine.rows(), w: g.spans.w, h: g.spans.h });
1337
- }
1338
1276
  if (beside && (beside.id !== z.id || beside.side !== z.side))
1339
1277
  endBeside(true); // another container, or another side of it: from the rest layout
1340
1278
  if (!beside) {
1341
1279
  const grp0 = diagram.getGroup(z.id);
1342
1280
  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 };
1343
1281
  }
1344
- const r = engine.placeBeside(g.id, z.id, z.side, row);
1345
- const at = engine.getItem(g.id);
1282
+ const r = engine.placeBeside(id, z.id, z.side, row);
1283
+ const at = engine.getItem(id);
1346
1284
  if (r.changed && at) {
1347
1285
  beside.vacated = { x: at.x, y: at.y };
1348
1286
  beside.row = row;
1349
1287
  }
1350
1288
  else if (at) {
1351
1289
  // 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);
1290
+ placeNear(id, at.x, at.y, spans.w);
1353
1291
  }
1354
1292
  project();
1355
1293
  };
1294
+ const applyBeside = (g, z, row) => {
1295
+ if (!engine.getItem(z.id))
1296
+ return;
1297
+ if (g.leg) {
1298
+ g.leg.adopted.abort();
1299
+ g.leg = null;
1300
+ }
1301
+ if (g.removedFromBoard) {
1302
+ g.removedFromBoard = false;
1303
+ setDim(g, false);
1304
+ engine.add({ id: g.id, x: 0, y: engine.rows(), w: g.spans.w, h: g.spans.h });
1305
+ }
1306
+ besideOn(g.id, g.spans, z, row);
1307
+ };
1356
1308
  /**
1357
1309
  * CARRIED (0.4.43): a group dragged by its strip or band moves as ONE thing.
1358
1310
  * The held TILE is transition-exempt (the ghost), but a group has no host of
@@ -1731,19 +1683,9 @@ export function bindDashboardGrid(api, group, options = {}) {
1731
1683
  }
1732
1684
  engine.endGesture();
1733
1685
  project();
1734
- const it = engine.getItem(g.id);
1735
- const grp = diagram.getGroup(g.id);
1736
- const deltas = deltasSince(g.startCells, g.startGeom, g.id);
1737
- const commands = buildCommitCommands(deltas);
1738
- // The sections and groups the move PUSHED (0.4.44): the tile commit skips
1739
- // groups by design (a group's cell is its own command), so they are
1740
- // committed here the way a dock commits the groups it displaced — or the
1741
- // model snapped every one of them, and the mover, straight back.
1742
- commands.push(...groupCellCommands(deltas));
1743
- const b = g.cellBefore;
1744
- if (it && grp && (b.x !== it.x || b.y !== it.y || b.w !== it.w || b.h !== it.h)) {
1745
- commands.push(new SetGroupCellCommand(g.id, b, { x: it.x, y: it.y, w: it.w, h: it.h }, g.frameBefore, frameOfGroup(grp)));
1746
- }
1686
+ // The mover and the sections it PUSHED (0.4.44) commit alike: a group's
1687
+ // cell-and-frame command comes out of the same deltas as a node's.
1688
+ const commands = tileCommands(deltasSince(g.startCells, g.startGeom));
1747
1689
  const changed = execute(g.move ? 'Move section' : 'Resize section', commands);
1748
1690
  disarmGlideSoon();
1749
1691
  syncHandles();
@@ -1828,6 +1770,12 @@ export function bindDashboardGrid(api, group, options = {}) {
1828
1770
  if (rect.height !== undefined)
1829
1771
  el.style.height = `${rect.height}px`;
1830
1772
  };
1773
+ /** "A release here lands nowhere": the ghost's host dims, and so does a palette chip (which has no host). */
1774
+ const setDim = (g, on) => {
1775
+ var _a, _b;
1776
+ (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.toggle('axdb-out', on);
1777
+ (_b = g.chip) === null || _b === void 0 ? void 0 : _b.classList.toggle('axdb-out', on);
1778
+ };
1831
1779
  const cleanupGestureVisuals = (g) => {
1832
1780
  var _a;
1833
1781
  if (g.kind !== 'palette')
@@ -1858,8 +1806,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1858
1806
  gesture = null;
1859
1807
  project();
1860
1808
  const deltas = deltasSince(g.startCells, g.startGeom);
1861
- const commands = buildCommitCommands(deltas);
1862
- commands.push(...groupCellCommands(deltas)); // a container a BESIDE drop shifted (0.4.45)
1809
+ const commands = tileCommands(deltas); // a container a BESIDE drop shifted commits with the widgets (0.4.45)
1863
1810
  endBeside(false);
1864
1811
  if (g.esc && g.esc.rowsAdded !== 0) {
1865
1812
  commands.push(new SetGroupCellCommand(group.id, g.esc.cellBefore, g.esc.cellAfter, g.esc.frameBefore, g.esc.frameAfter));
@@ -1891,11 +1838,11 @@ export function bindDashboardGrid(api, group, options = {}) {
1891
1838
  if (!g)
1892
1839
  return;
1893
1840
  gesture = null;
1894
- if (g.kind !== 'palette' && g.leg) {
1841
+ if (g.leg) {
1895
1842
  g.leg.adopted.abort(); // target board back to its pre-entry layout
1896
1843
  g.leg = null;
1897
1844
  }
1898
- if (g.kind !== 'palette' && g.esc && g.esc.rowsAdded !== 0) {
1845
+ if (g.esc && g.esc.rowsAdded !== 0) {
1899
1846
  g.esc.peer.resizeMemberBy(group.id, -g.esc.rowsAdded); // slab back down
1900
1847
  g.esc = null;
1901
1848
  }
@@ -1959,141 +1906,186 @@ export function bindDashboardGrid(api, group, options = {}) {
1959
1906
  y: worldY - (spans.h * (rh + gap) - gap) / 2,
1960
1907
  };
1961
1908
  };
1962
- const onToolMove = (ev) => {
1963
- var _a, _b, _c, _d, _e, _f, _g, _h;
1964
- const g = gesture;
1965
- if (!g || g.kind === 'palette')
1909
+ /**
1910
+ * THE GHOST FOLLOWS THE HAND for a tile of this board and for a palette
1911
+ * chip alike (tile first, step 4a): the zone under the pointer is resolved
1912
+ * once over the boards' membership tree, and whichever board it names
1913
+ * takes the ghost through a leg — beside a container of that board, with
1914
+ * intent where a section of that board refused it (D2), or on a plain cell.
1915
+ */
1916
+ const moveGhost = (g, ev) => {
1917
+ var _a;
1918
+ const desired = { x: ev.world.x - g.grab.dx, y: ev.world.y - g.grab.dy };
1919
+ g.node.setPosition(desired.x, desired.y);
1920
+ ghostStyleFastPath(g, desired);
1921
+ g.lastWorld = { x: ev.world.x, y: ev.world.y };
1922
+ g.lastScreen = { x: ev.screen.x, y: ev.screen.y };
1923
+ /** The ghost's pixel size on THIS board — a palette chip has spans, not a size. */
1924
+ const pxSize = () => {
1925
+ if (g.kind !== 'palette')
1926
+ return { width: g.node.size.width, height: g.node.size.height };
1927
+ const f = frame();
1928
+ const gg = geom();
1929
+ return { width: g.spans.w * (columnUnitFor(gg, f.width) + gap) - gap, height: g.spans.h * (rowHeightFor(gg, rows()) + gap) - gap };
1930
+ };
1931
+ /** The ghost leaves THIS board's engine (survivors settle home); the old leg is closed first. */
1932
+ const leaveSelf = () => {
1933
+ if (g.leg) {
1934
+ g.leg.adopted.abort();
1935
+ g.leg = null;
1936
+ }
1937
+ if (!g.removedFromBoard) {
1938
+ g.removedFromBoard = true;
1939
+ engine.remove(g.id);
1940
+ project();
1941
+ }
1942
+ };
1943
+ /** A leg on `peer`, entered with `opts`; the old leg (another board's) is closed first. Answers whether the peer took the ghost. */
1944
+ const enterLeg = (peer, opts) => {
1945
+ if (g.leg && g.leg.peer !== peer) {
1946
+ g.leg.adopted.abort();
1947
+ g.leg = null;
1948
+ }
1949
+ if (!g.removedFromBoard) {
1950
+ g.removedFromBoard = true;
1951
+ engine.remove(g.id); // survivors settle home (gesture memory intact)
1952
+ project();
1953
+ }
1954
+ if (!g.leg) {
1955
+ const adopted = peer.adopt(g.node, ev.world, pxSize(), opts);
1956
+ if (!adopted)
1957
+ return false;
1958
+ g.leg = { peer, adopted };
1959
+ }
1960
+ return true;
1961
+ };
1962
+ // THE ZONE (tile first, step 2): one recursive resolve over the boards'
1963
+ // membership tree — a strip slot, a cell beside a container, a plain
1964
+ // cell on the deepest board the pointer may enter, or off — decided
1965
+ // BEFORE any engine is asked anything. The strip still wins over every
1966
+ // board; a band's stickiness and the vacated cell still hold a beside.
1967
+ const z = resolveTileZone(g, ev);
1968
+ if (z.kind === 'strip' && options.tabDrop && !isStatic && g.kind !== 'palette') {
1969
+ // -- INTO A STRIP: the widget becomes a new tab there, so it leaves
1970
+ // this board (survivors settle home) and the strip marks the slot.
1971
+ endBeside(true); // a band's shift gives way to the strip: the container comes back
1972
+ leaveSelf();
1973
+ setDim(g, false);
1974
+ if (!g.strip || g.strip.containerId !== z.containerId || g.strip.index !== z.index) {
1975
+ options.tabDrop.markDrop(z.containerId, z.index);
1976
+ }
1977
+ g.strip = { containerId: z.containerId, index: z.index };
1978
+ syncPlaceholder();
1979
+ api.render();
1966
1980
  return;
1967
- if (!g.started) {
1968
- const dx = ev.screen.x - g.downClient.x;
1969
- const dy = ev.screen.y - g.downClient.y;
1970
- if (Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD)
1971
- return;
1972
- beginGestureVisuals(g);
1973
1981
  }
1974
- if (g.kind === 'move') {
1975
- const desired = { x: ev.world.x - g.grab.dx, y: ev.world.y - g.grab.dy };
1976
- g.node.setPosition(desired.x, desired.y);
1977
- ghostStyleFastPath(g, desired);
1978
- g.lastWorld = { x: ev.world.x, y: ev.world.y };
1979
- g.lastScreen = { x: ev.screen.x, y: ev.screen.y };
1980
- // THE ZONE (tile first, step 2): one recursive resolve over the boards'
1981
- // membership tree a strip slot, a cell beside a container, a plain
1982
- // cell on the deepest board the pointer may enter, or off — decided
1983
- // BEFORE any engine is asked anything. The strip still wins over every
1984
- // board; a band's stickiness and the vacated cell still hold a beside.
1985
- const z = resolveTileZone(g, ev);
1986
- if (z.kind === 'strip' && options.tabDrop && !isStatic) {
1987
- // -- INTO A STRIP: the widget becomes a new tab there, so it leaves
1988
- // this board (survivors settle home) and the strip marks the slot.
1982
+ if (g.strip) {
1983
+ (_a = options.tabDrop) === null || _a === void 0 ? void 0 : _a.markDrop(null, null);
1984
+ g.strip = null;
1985
+ }
1986
+ // -- BESIDE a tab container: its outer band puts the widget next to it
1987
+ // at the board's edge the container shifts over to make room, live
1988
+ // and gliding, like any widget gives way (0.4.48). On THIS board the
1989
+ // engine is driven directly; on another board its leg drives it (4a).
1990
+ if (z.kind === 'beside' && !isStatic && z.board.ref === selfPeer) {
1991
+ const row = rowOfPoint(ev.world.y);
1992
+ if (!z.kept || !beside || beside.row !== row)
1993
+ applyBeside(g, { id: z.containerId, side: z.side }, row);
1994
+ syncPlaceholder();
1995
+ return;
1996
+ }
1997
+ if (beside)
1998
+ endBeside(true); // the hand left the band: the container comes back
1999
+ if (z.kind === 'beside' && !isStatic) {
2000
+ const peer = z.board.ref;
2001
+ if (g.leg && g.leg.peer === peer)
2002
+ g.leg.adopted.beside(z.containerId, z.side, ev.world);
2003
+ else if (enterLeg(peer, { beside: { containerId: z.containerId, side: z.side } }))
2004
+ g.refusedPeer = null;
2005
+ setDim(g, !g.leg);
2006
+ syncPlaceholder();
2007
+ return;
2008
+ }
2009
+ // Last resort: the grace band — a small slip past the edge stays ON
2010
+ // this board (the engine clamps the cell; prototype parity).
2011
+ const onSelf = z.kind !== 'off' && z.kind !== 'strip' && z.board.ref === selfPeer;
2012
+ const inside = onSelf || (z.kind === 'off' && worldInsideBoardGrace(ev.world.x, ev.world.y));
2013
+ const peer = !onSelf && z.kind !== 'off' && z.kind !== 'strip' ? z.board.ref : null;
2014
+ /** A section that refused the widget is PUSHED by it on the board that holds the section (D2): this board directly, another through its leg. */
2015
+ const pushRefused = (refused) => {
2016
+ const parent = parentPeerOf(api.container, refused.group.id);
2017
+ if (parent === selfPeer) {
1989
2018
  if (g.leg) {
1990
2019
  g.leg.adopted.abort();
1991
2020
  g.leg = null;
1992
2021
  }
1993
- endBeside(true); // a band's shift gives way to the strip: the container comes back
1994
- if (!g.removedFromBoard) {
1995
- g.removedFromBoard = true;
1996
- engine.remove(g.id);
1997
- project();
1998
- }
1999
- (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-out');
2000
- if (!g.strip || g.strip.containerId !== z.containerId || g.strip.index !== z.index) {
2001
- options.tabDrop.markDrop(z.containerId, z.index);
2002
- }
2003
- g.strip = { containerId: z.containerId, index: z.index };
2004
- syncPlaceholder();
2005
- api.render();
2006
- return;
2022
+ placeOnSelf(g, desired, true);
2023
+ setDim(g, false);
2007
2024
  }
2008
- if (g.strip) {
2009
- (_b = options.tabDrop) === null || _b === void 0 ? void 0 : _b.markDrop(null, null);
2010
- g.strip = null;
2025
+ else if (parent && g.leg && g.leg.peer === parent) {
2026
+ g.leg.adopted.move(ev.world, { push: true });
2027
+ setDim(g, false);
2011
2028
  }
2012
- // -- BESIDE a tab container of THIS board: its outer band puts the
2013
- // widget next to it — at the board's edge the container shifts over
2014
- // to make room, live and gliding, like any widget gives way (0.4.48).
2015
- // (A container on another board still resolves as a plain cell there
2016
- // until the gesture scope spans boards — step 3.)
2017
- if (z.kind === 'beside' && !isStatic && z.board.ref === selfPeer) {
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);
2021
- syncPlaceholder();
2022
- return;
2029
+ else if (parent && enterLeg(parent, { push: true })) {
2030
+ setDim(g, false);
2023
2031
  }
2024
- if (beside)
2025
- endBeside(true); // the hand left the band: the container comes back
2026
- // Last resort: the grace band — a small slip past the edge stays ON
2027
- // this board (the engine clamps the cell; prototype parity).
2028
- const onSelf = z.kind !== 'off' && z.kind !== 'strip' && z.board.ref === selfPeer;
2029
- const inside = onSelf || (z.kind === 'off' && worldInsideBoardGrace(ev.world.x, ev.world.y));
2030
- const peer = !onSelf && z.kind !== 'off' && z.kind !== 'strip' ? z.board.ref : null;
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);
2032
+ else {
2033
+ // No board holds the refusing section (a view): dim = will snap home.
2034
+ leaveSelf();
2035
+ setDim(g, true);
2036
2036
  }
2037
- else if (peer) {
2038
- // -- HANDOFF: the pointer is over another board -------------------
2039
- if (g.leg && g.leg.peer === peer) {
2040
- g.leg.adopted.move(ev.world);
2041
- }
2042
- else {
2043
- if (g.leg) {
2044
- g.leg.adopted.abort();
2045
- g.leg = null;
2046
- }
2047
- if (!g.removedFromBoard) {
2048
- g.removedFromBoard = true;
2049
- engine.remove(g.id); // survivors settle home (gesture memory intact)
2050
- project();
2051
- }
2052
- const adopted = peer.adopt(g.node, ev.world, {
2053
- width: g.node.size.width,
2054
- height: g.node.size.height,
2055
- });
2056
- if (adopted) {
2057
- g.refusedPeer = null;
2058
- (_c = hostOf(g.id)) === null || _c === void 0 ? void 0 : _c.classList.remove('axdb-out');
2059
- g.leg = { peer, adopted };
2060
- g.leg.adopted.move(ev.world);
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
- }
2067
- else {
2068
- // A board deeper down refused the adoption: dim = will snap home (the N-board scope is step 4).
2069
- g.refusedPeer = peer;
2070
- (_d = hostOf(g.id)) === null || _d === void 0 ? void 0 : _d.classList.add('axdb-out');
2071
- }
2072
- }
2037
+ };
2038
+ if (peer && g.refusedPeer === peer) {
2039
+ // -- REFUSED, PUSHED (D2): asked once; every move over it pushes.
2040
+ pushRefused(peer);
2041
+ }
2042
+ else if (peer) {
2043
+ // -- HANDOFF: the pointer is over another board -------------------
2044
+ if (g.leg && g.leg.peer === peer) {
2045
+ g.leg.adopted.move(ev.world);
2073
2046
  }
2074
- else if (inside) {
2075
- // -- back on (or still on) our own board --------------------------
2047
+ else if (enterLeg(peer, {})) {
2076
2048
  g.refusedPeer = null;
2077
- if (g.leg) {
2078
- g.leg.adopted.abort();
2079
- g.leg = null;
2080
- }
2081
- placeOnSelf(g, desired);
2049
+ setDim(g, false);
2050
+ g.leg.adopted.move(ev.world);
2082
2051
  }
2083
2052
  else {
2084
- // -- outside every board ------------------------------------------
2085
- if (g.leg) {
2086
- g.leg.adopted.abort();
2087
- g.leg = null;
2088
- }
2089
- if (!g.removedFromBoard) {
2090
- g.removedFromBoard = true;
2091
- engine.remove(g.id); // survivors settle home; cells minted nowhere
2092
- (_e = hostOf(g.id)) === null || _e === void 0 ? void 0 : _e.classList.add('axdb-out');
2093
- project();
2094
- }
2053
+ // The board refused (full, fit): the widget pushes it on its parent (D2).
2054
+ g.refusedPeer = peer;
2055
+ pushRefused(peer);
2095
2056
  }
2096
- syncPlaceholder();
2057
+ }
2058
+ else if (inside) {
2059
+ // -- back on (or still on) our own board --------------------------
2060
+ g.refusedPeer = null;
2061
+ if (g.leg) {
2062
+ g.leg.adopted.abort();
2063
+ g.leg = null;
2064
+ }
2065
+ setDim(g, false);
2066
+ placeOnSelf(g, desired);
2067
+ }
2068
+ else {
2069
+ // -- outside every board ------------------------------------------
2070
+ leaveSelf();
2071
+ setDim(g, true);
2072
+ }
2073
+ syncPlaceholder();
2074
+ };
2075
+ const onToolMove = (ev) => {
2076
+ var _a, _b, _c;
2077
+ const g = gesture;
2078
+ if (!g || g.kind === 'palette')
2079
+ return;
2080
+ if (!g.started) {
2081
+ const dx = ev.screen.x - g.downClient.x;
2082
+ const dy = ev.screen.y - g.downClient.y;
2083
+ if (Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD)
2084
+ return;
2085
+ beginGestureVisuals(g);
2086
+ }
2087
+ if (g.kind === 'move') {
2088
+ moveGhost(g, ev);
2097
2089
  return;
2098
2090
  }
2099
2091
  // resize: fluid pixel preview on the ghost, cell-stepped live push.
@@ -2199,7 +2191,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2199
2191
  maxRows = n;
2200
2192
  engine.maxRows = n;
2201
2193
  };
2202
- const slabRows = (_g = (_f = parent.memberCell(group.id)) === null || _f === void 0 ? void 0 : _f.h) !== null && _g !== void 0 ? _g : maxRows;
2194
+ const slabRows = (_b = (_a = parent.memberCell(group.id)) === null || _a === void 0 ? void 0 : _a.h) !== null && _b !== void 0 ? _b : maxRows;
2203
2195
  const inner = maxRows;
2204
2196
  const rhNow = rowHeightFor(geom(), rows());
2205
2197
  const rowPx = rhNow + gap;
@@ -2363,7 +2355,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2363
2355
  const anchorBottom = liveRect ? liveRect.y + liveRect.height : bottom;
2364
2356
  const px = E.w ? anchorRight - w : anchorLeft;
2365
2357
  const py = E.n ? anchorBottom - h : anchorTop;
2366
- g.node.setSize(w, h, (_h = g.node.size.depth) !== null && _h !== void 0 ? _h : 0);
2358
+ g.node.setSize(w, h, (_c = g.node.size.depth) !== null && _c !== void 0 ? _c : 0);
2367
2359
  g.node.setPosition(px, py);
2368
2360
  ghostStyleFastPath(g, { x: px, y: py, width: w, height: h });
2369
2361
  const spanF = bound() !== undefined ? frame() : f;
@@ -2421,7 +2413,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2421
2413
  g.leg.adopted.abort();
2422
2414
  g.leg = null;
2423
2415
  }
2424
- const displaced = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
2416
+ const displaced = tileCommands(deltasSince(g.startCells, g.startGeom, g.id));
2425
2417
  const snap = g.startGeom.get(g.id);
2426
2418
  if (snap) {
2427
2419
  g.node.setPosition(snap.pos.x, snap.pos.y);
@@ -2462,7 +2454,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2462
2454
  finally {
2463
2455
  writing = false;
2464
2456
  }
2465
- const sourceDisplaced = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
2457
+ const sourceDisplaced = tileCommands(deltasSince(g.startCells, g.startGeom, g.id));
2466
2458
  const before = g.startCells.get(g.id);
2467
2459
  const geomBefore = g.startGeom.get(g.id);
2468
2460
  const crossing = [
@@ -2527,7 +2519,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2527
2519
  }
2528
2520
  if (g.removedFromBoard && dragOut === 'remove') {
2529
2521
  // Release OUTSIDE the board → remove via the page's atomic command path.
2530
- const displaced = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
2522
+ const displaced = tileCommands(deltasSince(g.startCells, g.startGeom, g.id));
2531
2523
  const snap = g.startGeom.get(g.id);
2532
2524
  if (snap) {
2533
2525
  // Park the node on its start rect so the page's RemoveNodeCommand
@@ -2609,10 +2601,10 @@ export function bindDashboardGrid(api, group, options = {}) {
2609
2601
  };
2610
2602
  /** 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
2603
  const placeOnSelf = (g, desired, pushSolid = false) => {
2612
- var _a, _b, _c;
2604
+ var _a, _b;
2613
2605
  if (g.removedFromBoard) {
2614
2606
  g.removedFromBoard = false;
2615
- (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-out');
2607
+ setDim(g, false);
2616
2608
  const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), g.spans.w);
2617
2609
  engine.add({ id: g.id, x: 0, y: engine.rows(), w: g.spans.w, h: g.spans.h });
2618
2610
  if (!engine.moveCheck(g.id, cell.x, cell.y, { gate: false, pushSolid }).changed)
@@ -2620,7 +2612,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2620
2612
  project();
2621
2613
  }
2622
2614
  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;
2615
+ const spanW = (_b = (_a = engine.getItem(g.id)) === null || _a === void 0 ? void 0 : _a.w) !== null && _b !== void 0 ? _b : g.spans.w;
2624
2616
  const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), spanW);
2625
2617
  if (engine.moveCheck(g.id, cell.x, cell.y, { pushSolid }).changed)
2626
2618
  project();
@@ -2628,8 +2620,9 @@ export function bindDashboardGrid(api, group, options = {}) {
2628
2620
  };
2629
2621
  /** What the pointer means for the dragged tile: the resolve over the live tree, with the beside the hand holds. */
2630
2622
  const resolveTileZone = (g, ev) => {
2623
+ var _a, _b;
2631
2624
  let strip = null;
2632
- if (options.tabDrop && !isStatic) {
2625
+ if (options.tabDrop && !isStatic && g.kind !== 'palette') {
2633
2626
  const crect = api.container.getBoundingClientRect();
2634
2627
  strip = options.tabDrop.stripAt(crect.left + ev.screen.x, crect.top + ev.screen.y);
2635
2628
  }
@@ -2638,7 +2631,9 @@ export function bindDashboardGrid(api, group, options = {}) {
2638
2631
  y: ev.world.y,
2639
2632
  roots: zoneRoots(),
2640
2633
  strip,
2641
- prev: beside ? { containerId: beside.id, side: beside.side, frame0: beside.frame0, vacated: cellToRect({ x: beside.vacated.x, y: beside.vacated.y, w: g.spans.w, h: g.spans.h }, frame(), geom(), rows()) } : null,
2634
+ prev: beside
2635
+ ? { containerId: beside.id, side: beside.side, frame0: beside.frame0, vacated: cellToRect({ x: beside.vacated.x, y: beside.vacated.y, w: g.spans.w, h: g.spans.h }, frame(), geom(), rows()) }
2636
+ : ((_b = (_a = g.leg) === null || _a === void 0 ? void 0 : _a.adopted.besideState()) !== null && _b !== void 0 ? _b : null), // a beside another board holds for the ghost, through its leg
2642
2637
  maxDepth: nesting,
2643
2638
  ghostDepth: 0,
2644
2639
  ghostSubtree: EMPTY_SUBTREE,
@@ -2858,11 +2853,16 @@ export function bindDashboardGrid(api, group, options = {}) {
2858
2853
  const tl = centredTopLeft(wx, wy, { w: itemW, h: opts.anchor === 'top' ? 0 : itemH });
2859
2854
  return pointToCell(tl.x, tl.y, frame(), geom(), rows(), itemW);
2860
2855
  };
2861
- const place = (cell, itemW) => opts.fit === 'shrink' ? placeFitting(node.id, cell.x, cell.y, itemW, hNatural) : placeNear(node.id, cell.x, cell.y, itemW);
2856
+ const place = (cell, itemW, push = false) => opts.fit === 'shrink' ? placeFitting(node.id, cell.x, cell.y, itemW, hNatural) : placeNear(node.id, cell.x, cell.y, itemW, push);
2862
2857
  let lastWant = null;
2863
- const cell0 = wantedCell(world.x, world.y, span.w, span.h);
2864
- lastWant = cell0;
2865
- place(cell0, span.w);
2858
+ if (opts.beside) {
2859
+ besideOn(node.id, span, { id: opts.beside.containerId, side: opts.beside.side }, rowOfPoint(world.y));
2860
+ }
2861
+ else {
2862
+ const cell0 = wantedCell(world.x, world.y, span.w, span.h);
2863
+ lastWant = cell0;
2864
+ place(cell0, span.w, opts.push);
2865
+ }
2866
2866
  armGlide();
2867
2867
  project();
2868
2868
  syncPlaceholder();
@@ -2904,22 +2904,36 @@ export function bindDashboardGrid(api, group, options = {}) {
2904
2904
  const at = engine.getItem(node.id);
2905
2905
  return !!at && at.x === cell.x && at.y === cell.y && at.w === cell.w && at.h === cell.h;
2906
2906
  },
2907
- move: (w) => {
2907
+ move: (w, o) => {
2908
2908
  const item = engine.getItem(node.id);
2909
2909
  if (!item)
2910
2910
  return;
2911
+ if (beside)
2912
+ endBeside(true); // the hand left the band: the container comes back before the tile takes a plain cell
2911
2913
  const cell = wantedCell(w.x, w.y, item.w, opts.fit === 'shrink' ? hNatural : item.h);
2912
2914
  // The search is worth running once per wanted cell, not per pixel.
2913
2915
  if (lastWant && lastWant.x === cell.x && lastWant.y === cell.y)
2914
2916
  return;
2915
2917
  lastWant = cell;
2916
- if (place(cell, item.w))
2918
+ if (place(cell, item.w, !!(o === null || o === void 0 ? void 0 : o.push)))
2917
2919
  project();
2918
2920
  syncPlaceholder();
2919
2921
  },
2922
+ beside: (containerId, side, w) => {
2923
+ if (!engine.getItem(node.id) && !engine.add({ id: node.id, x: 0, y: engine.rows(), w: span.w, h: hNatural }))
2924
+ return;
2925
+ const row = rowOfPoint(w.y);
2926
+ if (beside && beside.id === containerId && beside.side === side && beside.row === row)
2927
+ return;
2928
+ lastWant = null;
2929
+ besideOn(node.id, span, { id: containerId, side }, row);
2930
+ syncPlaceholder();
2931
+ },
2932
+ besideState: () => beside ? { containerId: beside.id, side: beside.side, frame0: beside.frame0, vacated: cellToRect({ x: beside.vacated.x, y: beside.vacated.y, w: span.w, h: hNatural }, frame(), geom(), rows()) } : null,
2920
2933
  leave: () => {
2921
2934
  if (!engine.getItem(node.id))
2922
2935
  return;
2936
+ endBeside(true); // a container shifted for the tile comes back by itself: the shift was deliberate, so the memory never brings it
2923
2937
  engine.remove(node.id); // displaced tiles come home (gesture memory)
2924
2938
  lastWant = null;
2925
2939
  project();
@@ -2938,6 +2952,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2938
2952
  syncPlaceholder();
2939
2953
  },
2940
2954
  abort: () => {
2955
+ beside = null; // the snapshot restores the container with everything else
2941
2956
  if (engine.getItem(node.id))
2942
2957
  engine.remove(node.id);
2943
2958
  engine.cancelGesture(); // pre-entry layout, memory cleared
@@ -2948,6 +2963,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2948
2963
  syncPlaceholder();
2949
2964
  },
2950
2965
  finalize: () => {
2966
+ endBeside(false); // the shift stays: the deltas below carry it
2951
2967
  const item = engine.getItem(node.id);
2952
2968
  if (!item) {
2953
2969
  engine.endGesture();
@@ -2957,26 +2973,11 @@ export function bindDashboardGrid(api, group, options = {}) {
2957
2973
  }
2958
2974
  const cell = { x: item.x, y: item.y, w: item.w, h: item.h };
2959
2975
  const rect = cellToRect(item, frame(), geom(), rows());
2960
- const commands = buildCommitCommands(deltasSince(startCells, startGeom, node.id));
2961
- const groups = [];
2962
- for (const [id, before] of startCells) {
2963
- if (!isGroupMember(id))
2964
- continue;
2965
- const it = engine.getItem(id);
2966
- const g0 = startGeom.get(id);
2967
- if (!it || !g0)
2968
- continue;
2969
- if (it.x === before.x && it.y === before.y && it.w === before.w && it.h === before.h)
2970
- continue;
2971
- groups.push({
2972
- id,
2973
- cellBefore: before,
2974
- cellAfter: { x: it.x, y: it.y, w: it.w, h: it.h },
2975
- frameBefore: { x: g0.pos.x, y: g0.pos.y, width: g0.size.width, height: g0.size.height },
2976
- frameAfter: cellToRect(it, frame(), geom(), rows()),
2977
- });
2978
- }
2976
+ // Widgets AND sections this board's adoption displaced, as one list: a
2977
+ // dock or a push that moved a section commits it with the drop.
2978
+ const commands = tileCommands(deltasSince(startCells, startGeom, node.id));
2979
2979
  engine.endGesture();
2980
+ pendingDrop = node.id; // the tile stays for the member the commit (or the host's drop-in) adds
2980
2981
  if (squeezeRoom !== undefined) {
2981
2982
  // What the board now HOLDS is its bound, not the squeeze's room.
2982
2983
  squeezeRoom = undefined;
@@ -2985,7 +2986,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2985
2986
  adoptedGhostId = null;
2986
2987
  disarmGlideSoon();
2987
2988
  syncPlaceholder();
2988
- return { commands, cell, rect, groups };
2989
+ return { commands, cell, rect };
2989
2990
  },
2990
2991
  };
2991
2992
  };
@@ -3947,13 +3948,6 @@ export function bindDashboardGrid(api, group, options = {}) {
3947
3948
  }
3948
3949
  };
3949
3950
  const zoneKey = (z) => JSON.stringify(z, (k, v) => (k === 'target' ? v.id : k === 'peer' ? v.group.id : v));
3950
- // DOCKING PUSHES SECTIONS. A section is a locked tile so that a widget
3951
- // never pushes it; a group docked against the board's edge is the one
3952
- // gesture that must — everything below the top band moves down. For the
3953
- // preview every member group is unlocked; they are relocked when the
3954
- // pointer leaves the band, and their moved cells are committed with the
3955
- // dock.
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));
3957
3951
  // A TOP DOCK INSERTS ROWS. The engine's push cascade resolves the band's
3958
3952
  // collisions one tile at a time and scrambles what stood beneath it (the
3959
3953
  // demo's KPI row came apart, two of its tiles under the chart). VS Code
@@ -4204,23 +4198,17 @@ export function bindDashboardGrid(api, group, options = {}) {
4204
4198
  return;
4205
4199
  }
4206
4200
  const fin = (_b = leg === null || leg === void 0 ? void 0 : leg.finalize()) !== null && _b !== void 0 ? _b : null;
4207
- const it = engine.getItem(z.target.id);
4208
- const before = split;
4201
+ const halved = !!split && !!engine.getItem(z.target.id);
4209
4202
  split = null;
4210
- if (!fin || !before || !it) {
4203
+ if (!fin || !halved) {
4211
4204
  leg === null || leg === void 0 ? void 0 : leg.abort();
4212
4205
  done(false, 'cancel');
4213
4206
  return;
4214
4207
  }
4215
- const keep = { x: it.x, y: it.y, w: it.w, h: it.h };
4216
4208
  const planned = plan.commands(fin.cell, fin.rect, group.id);
4217
- const changed = execute('Split group', [
4218
- ...fin.commands,
4219
- ...groupCommands(fin, z.target.id),
4220
- new SetGroupCellCommand(z.target.id, before.before, keep, before.frameBefore, cellToRect(keep, frame(), geom(), rows())),
4221
- ...planned.move,
4222
- ...planned.collapse,
4223
- ]);
4209
+ // The target's halving is in the leg's own deltas: its cell at rest was
4210
+ // snapshotted when the ghost entered, and its half is where it stands now.
4211
+ const changed = execute('Split group', [...fin.commands, ...planned.move, ...planned.collapse]);
4224
4212
  done(changed, 'commit');
4225
4213
  return;
4226
4214
  }
@@ -4238,7 +4226,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4238
4226
  return;
4239
4227
  }
4240
4228
  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);
4241
- done(execute(z.kind === 'root' ? 'Dock tab' : 'Move tab out', [...fin.commands, ...groupCommands(fin), ...planned.move, ...planned.collapse]), 'commit');
4229
+ done(execute(z.kind === 'root' ? 'Dock tab' : 'Move tab out', [...fin.commands, ...planned.move, ...planned.collapse]), 'commit');
4242
4230
  };
4243
4231
  const onUp = () => finish(true);
4244
4232
  const onCancel = () => finish(false);
@@ -4310,43 +4298,39 @@ export function bindDashboardGrid(api, group, options = {}) {
4310
4298
  return;
4311
4299
  }
4312
4300
  beginGestureVisuals(g);
4301
+ // The chip is held by its middle: the ghost centres under the hand.
4302
+ const f = frame();
4303
+ const gg = geom();
4304
+ g.grab = { dx: (g.spans.w * (columnUnitFor(gg, f.width) + gap) - gap) / 2, dy: (g.spans.h * (rowHeightFor(gg, rows()) + gap) - gap) / 2 };
4313
4305
  }
4314
4306
  if (chip) {
4315
4307
  chip.style.left = `${e.clientX + 6}px`;
4316
4308
  chip.style.top = `${e.clientY + 6}px`;
4317
4309
  }
4310
+ // THE SAME PATH AS A TILE OF THIS BOARD (tile first, step 4a): the zone
4311
+ // under the hand names the board — this one, a page, a section — and
4312
+ // that board takes the ghost. The palette used to test "inside this
4313
+ // board" and nothing else, so a chip over a page landed under it.
4314
+ const rect = api.container.getBoundingClientRect();
4318
4315
  const world = toWorld(e.clientX, e.clientY);
4319
- const inside = worldInsideBoard(world.x, world.y);
4320
- if (inside) {
4321
- const tl = centredTopLeft(world.x, world.y, g.spans);
4322
- const cell = pointToCell(tl.x, tl.y, frame(), geom(), rows(), g.spans.w);
4323
- if (g.removedFromBoard) {
4324
- g.removedFromBoard = false;
4325
- // Enter at the bottom edge (collision-free), then take the cursor
4326
- // cell GATELESSLY — gridstack's drag-in skips the gate on entry.
4327
- // A bounded board with no room refuses the entry: the chip dims to
4328
- // say so, and the release will snap it home.
4329
- const entered = engine.add({ id: g.id, x: 0, y: engine.rows(), w: g.spans.w, h: g.spans.h });
4330
- chip === null || chip === void 0 ? void 0 : chip.classList.toggle('axdb-out', !entered);
4331
- if (entered)
4332
- engine.moveCheck(g.id, cell.x, cell.y, { gate: false });
4333
- project();
4334
- }
4335
- else if (engine.moveCheck(g.id, cell.x, cell.y).changed) {
4336
- project();
4337
- }
4338
- }
4339
- else if (!g.removedFromBoard) {
4340
- g.removedFromBoard = true;
4341
- chip === null || chip === void 0 ? void 0 : chip.classList.remove('axdb-out');
4342
- engine.remove(g.id); // displaced tiles come home (gesture memory)
4343
- project();
4344
- }
4345
- syncPlaceholder();
4316
+ moveGhost(g, { type: 'move', world, screen: { x: e.clientX - rect.left, y: e.clientY - rect.top }, modifiers: { shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey, meta: e.metaKey } });
4346
4317
  api.render();
4347
4318
  };
4348
- const finish = (commit) => {
4319
+ /** The drop, on whichever board holds the ghost: the node carries its cell, the host's command adds it there. */
4320
+ const dropOn = (boardId, cell, displaced) => {
4349
4321
  var _a, _b;
4322
+ node.setGridItem(gridItemFromCell(cell));
4323
+ if (boardId === group.id)
4324
+ pendingDrop = g.id; // on another board its own finalize() pended it
4325
+ engine.endGesture();
4326
+ cleanupGestureVisuals(g);
4327
+ gesture = null;
4328
+ void ((_a = options.onDropIn) === null || _a === void 0 ? void 0 : _a.call(options, node, cell, displaced, { boardId }));
4329
+ persistLayouts();
4330
+ (_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'drop-in', kind: 'palette', nodeId: g.id, changed: true });
4331
+ api.renderNow();
4332
+ };
4333
+ const finish = (commit) => {
4350
4334
  detach();
4351
4335
  if (gesture !== g)
4352
4336
  return;
@@ -4357,18 +4341,21 @@ export function bindDashboardGrid(api, group, options = {}) {
4357
4341
  chip === null || chip === void 0 ? void 0 : chip.remove();
4358
4342
  return;
4359
4343
  }
4360
- if (commit && !g.removedFromBoard && engine.getItem(g.id)) {
4344
+ if (commit && g.leg) {
4345
+ // Dropped into another board (a page, a section): its leg closes with
4346
+ // that board's displaced tiles; this board displaced nothing.
4347
+ const fin = g.leg.adopted.finalize();
4348
+ const boardId = g.leg.adopted.groupId;
4349
+ g.leg = null;
4350
+ if (fin) {
4351
+ dropOn(boardId, fin.cell, [...tileCommands(deltasSince(g.startCells, g.startGeom, g.id)), ...fin.commands]);
4352
+ return;
4353
+ }
4354
+ }
4355
+ else if (commit && !g.removedFromBoard && engine.getItem(g.id)) {
4361
4356
  const item = engine.getItem(g.id);
4362
- const cell = { x: item.x, y: item.y, w: item.w, h: item.h };
4363
- node.setGridItem(gridItemFromCell(cell));
4364
- const displaced = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
4365
- engine.endGesture();
4366
- cleanupGestureVisuals(g);
4367
- gesture = null;
4368
- void ((_a = options.onDropIn) === null || _a === void 0 ? void 0 : _a.call(options, node, cell, displaced));
4369
- persistLayouts();
4370
- (_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'drop-in', kind: 'palette', nodeId: g.id, changed: true });
4371
- api.renderNow();
4357
+ endBeside(false); // a container that shifted for the chip stays shifted: its delta commits with the drop
4358
+ dropOn(group.id, { x: item.x, y: item.y, w: item.w, h: item.h }, tileCommands(deltasSince(g.startCells, g.startGeom, g.id)));
4372
4359
  return;
4373
4360
  }
4374
4361
  // Abort (released outside, or Escape): restore the board.
@@ -4411,18 +4398,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4411
4398
  finally {
4412
4399
  writing = false;
4413
4400
  }
4414
- const commands = buildCommitCommands(deltasSince(snap.cells, snap.geoms));
4415
- if (isGroupMember(id)) {
4416
- // A SECTION: its cell lives in group metadata and its frame is written
4417
- // by project(); the node commands above skip groups (D5 of the review).
4418
- const it = engine.getItem(id);
4419
- const grp = diagram.getGroup(id);
4420
- const cb = snap.cells.get(id);
4421
- const gb = snap.geoms.get(id);
4422
- if (it && grp && cb && gb && (cb.x !== it.x || cb.y !== it.y || cb.w !== it.w || cb.h !== it.h)) {
4423
- commands.push(new SetGroupCellCommand(id, cb, { x: it.x, y: it.y, w: it.w, h: it.h }, { x: gb.pos.x, y: gb.pos.y, width: gb.size.width, height: gb.size.height }, frameOfGroup(grp)));
4424
- }
4425
- }
4401
+ const commands = tileCommands(deltasSince(snap.cells, snap.geoms)); // a section's cell-and-frame command rides with the nodes'
4426
4402
  engine.endGesture();
4427
4403
  disarmGlideSoon();
4428
4404
  enforceBoardHeight();
@@ -4463,6 +4439,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4463
4439
  return;
4464
4440
  }
4465
4441
  applyFluidFrame();
4442
+ pendingDrop = null; // rebuilt from the members: a phantom is gone with the engine
4466
4443
  const items = [];
4467
4444
  for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
4468
4445
  if (!memberEntity(id))
@@ -4679,7 +4656,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4679
4656
  sizeAfter: { width: target.width, height: target.height },
4680
4657
  });
4681
4658
  }
4682
- return buildCommitCommands(deltas);
4659
+ return tileCommands(deltas);
4683
4660
  },
4684
4661
  moveTo(id, x, y) {
4685
4662
  return programmatic('Move widget', id, () => engine.moveCheck(id, x, y, { pushSolid: isGroupMember(id) }).changed);