@grafloria/element 0.4.52 → 0.4.53

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,10 +937,23 @@ 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;
1029
951
  if (disposed)
1030
952
  return;
953
+ if (pendingDrop && pendingDrop !== id && engine.getItem(pendingDrop) && !((_a = group.members) !== null && _a !== void 0 ? _a : new Set()).has(pendingDrop)) {
954
+ engine.remove(pendingDrop);
955
+ }
956
+ pendingDrop = null;
1031
957
  if (!engine.getItem(id)) {
1032
958
  const item = itemFor(id);
1033
959
  let placed = engine.add(item);
@@ -1037,7 +963,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1037
963
  // for this adoption — it floors at the content on the next refresh.
1038
964
  capacity = undefined;
1039
965
  engine = engineFrom([...engine.getItems().map((i) => (Object.assign({}, i))), item]);
1040
- placed = (_a = engine.getItem(id)) !== null && _a !== void 0 ? _a : null;
966
+ placed = (_b = engine.getItem(id)) !== null && _b !== void 0 ? _b : null;
1041
967
  refreshCapacity();
1042
968
  }
1043
969
  if (placed)
@@ -1321,38 +1247,49 @@ export function bindDashboardGrid(api, group, options = {}) {
1321
1247
  const pitch = Math.max(1, r1.y - r0.y);
1322
1248
  return Math.max(0, Math.floor((y - r0.y) / pitch));
1323
1249
  };
1324
- const applyBeside = (g, z, row) => {
1325
- var _a;
1250
+ /**
1251
+ * The ghost `id` (spanning `spans`) beside container `z.id` on THIS board at
1252
+ * `row`: the engine shifts the container or pushes what stands behind it,
1253
+ * and the beside is remembered for the resolve's stickiness. One function
1254
+ * for a tile of this board and for a tile another board's gesture placed
1255
+ * here through its leg.
1256
+ */
1257
+ const besideOn = (id, spans, z, row) => {
1326
1258
  const tc = engine.getItem(z.id);
1327
1259
  if (!tc)
1328
1260
  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
1261
  if (beside && (beside.id !== z.id || beside.side !== z.side))
1339
1262
  endBeside(true); // another container, or another side of it: from the rest layout
1340
1263
  if (!beside) {
1341
1264
  const grp0 = diagram.getGroup(z.id);
1342
1265
  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
1266
  }
1344
- const r = engine.placeBeside(g.id, z.id, z.side, row);
1345
- const at = engine.getItem(g.id);
1267
+ const r = engine.placeBeside(id, z.id, z.side, row);
1268
+ const at = engine.getItem(id);
1346
1269
  if (r.changed && at) {
1347
1270
  beside.vacated = { x: at.x, y: at.y };
1348
1271
  beside.row = row;
1349
1272
  }
1350
1273
  else if (at) {
1351
1274
  // 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);
1275
+ placeNear(id, at.x, at.y, spans.w);
1353
1276
  }
1354
1277
  project();
1355
1278
  };
1279
+ const applyBeside = (g, z, row) => {
1280
+ if (!engine.getItem(z.id))
1281
+ return;
1282
+ if (g.leg) {
1283
+ g.leg.adopted.abort();
1284
+ g.leg = null;
1285
+ }
1286
+ if (g.removedFromBoard) {
1287
+ g.removedFromBoard = false;
1288
+ setDim(g, false);
1289
+ engine.add({ id: g.id, x: 0, y: engine.rows(), w: g.spans.w, h: g.spans.h });
1290
+ }
1291
+ besideOn(g.id, g.spans, z, row);
1292
+ };
1356
1293
  /**
1357
1294
  * CARRIED (0.4.43): a group dragged by its strip or band moves as ONE thing.
1358
1295
  * The held TILE is transition-exempt (the ghost), but a group has no host of
@@ -1731,19 +1668,9 @@ export function bindDashboardGrid(api, group, options = {}) {
1731
1668
  }
1732
1669
  engine.endGesture();
1733
1670
  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
- }
1671
+ // The mover and the sections it PUSHED (0.4.44) commit alike: a group's
1672
+ // cell-and-frame command comes out of the same deltas as a node's.
1673
+ const commands = tileCommands(deltasSince(g.startCells, g.startGeom));
1747
1674
  const changed = execute(g.move ? 'Move section' : 'Resize section', commands);
1748
1675
  disarmGlideSoon();
1749
1676
  syncHandles();
@@ -1828,6 +1755,12 @@ export function bindDashboardGrid(api, group, options = {}) {
1828
1755
  if (rect.height !== undefined)
1829
1756
  el.style.height = `${rect.height}px`;
1830
1757
  };
1758
+ /** "A release here lands nowhere": the ghost's host dims, and so does a palette chip (which has no host). */
1759
+ const setDim = (g, on) => {
1760
+ var _a, _b;
1761
+ (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.toggle('axdb-out', on);
1762
+ (_b = g.chip) === null || _b === void 0 ? void 0 : _b.classList.toggle('axdb-out', on);
1763
+ };
1831
1764
  const cleanupGestureVisuals = (g) => {
1832
1765
  var _a;
1833
1766
  if (g.kind !== 'palette')
@@ -1858,8 +1791,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1858
1791
  gesture = null;
1859
1792
  project();
1860
1793
  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)
1794
+ const commands = tileCommands(deltas); // a container a BESIDE drop shifted commits with the widgets (0.4.45)
1863
1795
  endBeside(false);
1864
1796
  if (g.esc && g.esc.rowsAdded !== 0) {
1865
1797
  commands.push(new SetGroupCellCommand(group.id, g.esc.cellBefore, g.esc.cellAfter, g.esc.frameBefore, g.esc.frameAfter));
@@ -1891,11 +1823,11 @@ export function bindDashboardGrid(api, group, options = {}) {
1891
1823
  if (!g)
1892
1824
  return;
1893
1825
  gesture = null;
1894
- if (g.kind !== 'palette' && g.leg) {
1826
+ if (g.leg) {
1895
1827
  g.leg.adopted.abort(); // target board back to its pre-entry layout
1896
1828
  g.leg = null;
1897
1829
  }
1898
- if (g.kind !== 'palette' && g.esc && g.esc.rowsAdded !== 0) {
1830
+ if (g.esc && g.esc.rowsAdded !== 0) {
1899
1831
  g.esc.peer.resizeMemberBy(group.id, -g.esc.rowsAdded); // slab back down
1900
1832
  g.esc = null;
1901
1833
  }
@@ -1959,141 +1891,186 @@ export function bindDashboardGrid(api, group, options = {}) {
1959
1891
  y: worldY - (spans.h * (rh + gap) - gap) / 2,
1960
1892
  };
1961
1893
  };
1962
- const onToolMove = (ev) => {
1963
- var _a, _b, _c, _d, _e, _f, _g, _h;
1964
- const g = gesture;
1965
- if (!g || g.kind === 'palette')
1894
+ /**
1895
+ * THE GHOST FOLLOWS THE HAND for a tile of this board and for a palette
1896
+ * chip alike (tile first, step 4a): the zone under the pointer is resolved
1897
+ * once over the boards' membership tree, and whichever board it names
1898
+ * takes the ghost through a leg — beside a container of that board, with
1899
+ * intent where a section of that board refused it (D2), or on a plain cell.
1900
+ */
1901
+ const moveGhost = (g, ev) => {
1902
+ var _a;
1903
+ const desired = { x: ev.world.x - g.grab.dx, y: ev.world.y - g.grab.dy };
1904
+ g.node.setPosition(desired.x, desired.y);
1905
+ ghostStyleFastPath(g, desired);
1906
+ g.lastWorld = { x: ev.world.x, y: ev.world.y };
1907
+ g.lastScreen = { x: ev.screen.x, y: ev.screen.y };
1908
+ /** The ghost's pixel size on THIS board — a palette chip has spans, not a size. */
1909
+ const pxSize = () => {
1910
+ if (g.kind !== 'palette')
1911
+ return { width: g.node.size.width, height: g.node.size.height };
1912
+ const f = frame();
1913
+ const gg = geom();
1914
+ return { width: g.spans.w * (columnUnitFor(gg, f.width) + gap) - gap, height: g.spans.h * (rowHeightFor(gg, rows()) + gap) - gap };
1915
+ };
1916
+ /** The ghost leaves THIS board's engine (survivors settle home); the old leg is closed first. */
1917
+ const leaveSelf = () => {
1918
+ if (g.leg) {
1919
+ g.leg.adopted.abort();
1920
+ g.leg = null;
1921
+ }
1922
+ if (!g.removedFromBoard) {
1923
+ g.removedFromBoard = true;
1924
+ engine.remove(g.id);
1925
+ project();
1926
+ }
1927
+ };
1928
+ /** A leg on `peer`, entered with `opts`; the old leg (another board's) is closed first. Answers whether the peer took the ghost. */
1929
+ const enterLeg = (peer, opts) => {
1930
+ if (g.leg && g.leg.peer !== peer) {
1931
+ g.leg.adopted.abort();
1932
+ g.leg = null;
1933
+ }
1934
+ if (!g.removedFromBoard) {
1935
+ g.removedFromBoard = true;
1936
+ engine.remove(g.id); // survivors settle home (gesture memory intact)
1937
+ project();
1938
+ }
1939
+ if (!g.leg) {
1940
+ const adopted = peer.adopt(g.node, ev.world, pxSize(), opts);
1941
+ if (!adopted)
1942
+ return false;
1943
+ g.leg = { peer, adopted };
1944
+ }
1945
+ return true;
1946
+ };
1947
+ // THE ZONE (tile first, step 2): one recursive resolve over the boards'
1948
+ // membership tree — a strip slot, a cell beside a container, a plain
1949
+ // cell on the deepest board the pointer may enter, or off — decided
1950
+ // BEFORE any engine is asked anything. The strip still wins over every
1951
+ // board; a band's stickiness and the vacated cell still hold a beside.
1952
+ const z = resolveTileZone(g, ev);
1953
+ if (z.kind === 'strip' && options.tabDrop && !isStatic && g.kind !== 'palette') {
1954
+ // -- INTO A STRIP: the widget becomes a new tab there, so it leaves
1955
+ // this board (survivors settle home) and the strip marks the slot.
1956
+ endBeside(true); // a band's shift gives way to the strip: the container comes back
1957
+ leaveSelf();
1958
+ setDim(g, false);
1959
+ if (!g.strip || g.strip.containerId !== z.containerId || g.strip.index !== z.index) {
1960
+ options.tabDrop.markDrop(z.containerId, z.index);
1961
+ }
1962
+ g.strip = { containerId: z.containerId, index: z.index };
1963
+ syncPlaceholder();
1964
+ api.render();
1966
1965
  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
1966
  }
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.
1967
+ if (g.strip) {
1968
+ (_a = options.tabDrop) === null || _a === void 0 ? void 0 : _a.markDrop(null, null);
1969
+ g.strip = null;
1970
+ }
1971
+ // -- BESIDE a tab container: its outer band puts the widget next to it
1972
+ // at the board's edge the container shifts over to make room, live
1973
+ // and gliding, like any widget gives way (0.4.48). On THIS board the
1974
+ // engine is driven directly; on another board its leg drives it (4a).
1975
+ if (z.kind === 'beside' && !isStatic && z.board.ref === selfPeer) {
1976
+ const row = rowOfPoint(ev.world.y);
1977
+ if (!z.kept || !beside || beside.row !== row)
1978
+ applyBeside(g, { id: z.containerId, side: z.side }, row);
1979
+ syncPlaceholder();
1980
+ return;
1981
+ }
1982
+ if (beside)
1983
+ endBeside(true); // the hand left the band: the container comes back
1984
+ if (z.kind === 'beside' && !isStatic) {
1985
+ const peer = z.board.ref;
1986
+ if (g.leg && g.leg.peer === peer)
1987
+ g.leg.adopted.beside(z.containerId, z.side, ev.world);
1988
+ else if (enterLeg(peer, { beside: { containerId: z.containerId, side: z.side } }))
1989
+ g.refusedPeer = null;
1990
+ setDim(g, !g.leg);
1991
+ syncPlaceholder();
1992
+ return;
1993
+ }
1994
+ // Last resort: the grace band — a small slip past the edge stays ON
1995
+ // this board (the engine clamps the cell; prototype parity).
1996
+ const onSelf = z.kind !== 'off' && z.kind !== 'strip' && z.board.ref === selfPeer;
1997
+ const inside = onSelf || (z.kind === 'off' && worldInsideBoardGrace(ev.world.x, ev.world.y));
1998
+ const peer = !onSelf && z.kind !== 'off' && z.kind !== 'strip' ? z.board.ref : null;
1999
+ /** 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. */
2000
+ const pushRefused = (refused) => {
2001
+ const parent = parentPeerOf(api.container, refused.group.id);
2002
+ if (parent === selfPeer) {
1989
2003
  if (g.leg) {
1990
2004
  g.leg.adopted.abort();
1991
2005
  g.leg = null;
1992
2006
  }
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;
2007
+ placeOnSelf(g, desired, true);
2008
+ setDim(g, false);
2007
2009
  }
2008
- if (g.strip) {
2009
- (_b = options.tabDrop) === null || _b === void 0 ? void 0 : _b.markDrop(null, null);
2010
- g.strip = null;
2010
+ else if (parent && g.leg && g.leg.peer === parent) {
2011
+ g.leg.adopted.move(ev.world, { push: true });
2012
+ setDim(g, false);
2011
2013
  }
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;
2014
+ else if (parent && enterLeg(parent, { push: true })) {
2015
+ setDim(g, false);
2023
2016
  }
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);
2017
+ else {
2018
+ // No board holds the refusing section (a view): dim = will snap home.
2019
+ leaveSelf();
2020
+ setDim(g, true);
2036
2021
  }
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
- }
2022
+ };
2023
+ if (peer && g.refusedPeer === peer) {
2024
+ // -- REFUSED, PUSHED (D2): asked once; every move over it pushes.
2025
+ pushRefused(peer);
2026
+ }
2027
+ else if (peer) {
2028
+ // -- HANDOFF: the pointer is over another board -------------------
2029
+ if (g.leg && g.leg.peer === peer) {
2030
+ g.leg.adopted.move(ev.world);
2073
2031
  }
2074
- else if (inside) {
2075
- // -- back on (or still on) our own board --------------------------
2032
+ else if (enterLeg(peer, {})) {
2076
2033
  g.refusedPeer = null;
2077
- if (g.leg) {
2078
- g.leg.adopted.abort();
2079
- g.leg = null;
2080
- }
2081
- placeOnSelf(g, desired);
2034
+ setDim(g, false);
2035
+ g.leg.adopted.move(ev.world);
2082
2036
  }
2083
2037
  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
- }
2038
+ // The board refused (full, fit): the widget pushes it on its parent (D2).
2039
+ g.refusedPeer = peer;
2040
+ pushRefused(peer);
2095
2041
  }
2096
- syncPlaceholder();
2042
+ }
2043
+ else if (inside) {
2044
+ // -- back on (or still on) our own board --------------------------
2045
+ g.refusedPeer = null;
2046
+ if (g.leg) {
2047
+ g.leg.adopted.abort();
2048
+ g.leg = null;
2049
+ }
2050
+ setDim(g, false);
2051
+ placeOnSelf(g, desired);
2052
+ }
2053
+ else {
2054
+ // -- outside every board ------------------------------------------
2055
+ leaveSelf();
2056
+ setDim(g, true);
2057
+ }
2058
+ syncPlaceholder();
2059
+ };
2060
+ const onToolMove = (ev) => {
2061
+ var _a, _b, _c;
2062
+ const g = gesture;
2063
+ if (!g || g.kind === 'palette')
2064
+ return;
2065
+ if (!g.started) {
2066
+ const dx = ev.screen.x - g.downClient.x;
2067
+ const dy = ev.screen.y - g.downClient.y;
2068
+ if (Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD)
2069
+ return;
2070
+ beginGestureVisuals(g);
2071
+ }
2072
+ if (g.kind === 'move') {
2073
+ moveGhost(g, ev);
2097
2074
  return;
2098
2075
  }
2099
2076
  // resize: fluid pixel preview on the ghost, cell-stepped live push.
@@ -2199,7 +2176,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2199
2176
  maxRows = n;
2200
2177
  engine.maxRows = n;
2201
2178
  };
2202
- const slabRows = (_g = (_f = parent.memberCell(group.id)) === null || _f === void 0 ? void 0 : _f.h) !== null && _g !== void 0 ? _g : maxRows;
2179
+ const slabRows = (_b = (_a = parent.memberCell(group.id)) === null || _a === void 0 ? void 0 : _a.h) !== null && _b !== void 0 ? _b : maxRows;
2203
2180
  const inner = maxRows;
2204
2181
  const rhNow = rowHeightFor(geom(), rows());
2205
2182
  const rowPx = rhNow + gap;
@@ -2363,7 +2340,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2363
2340
  const anchorBottom = liveRect ? liveRect.y + liveRect.height : bottom;
2364
2341
  const px = E.w ? anchorRight - w : anchorLeft;
2365
2342
  const py = E.n ? anchorBottom - h : anchorTop;
2366
- g.node.setSize(w, h, (_h = g.node.size.depth) !== null && _h !== void 0 ? _h : 0);
2343
+ g.node.setSize(w, h, (_c = g.node.size.depth) !== null && _c !== void 0 ? _c : 0);
2367
2344
  g.node.setPosition(px, py);
2368
2345
  ghostStyleFastPath(g, { x: px, y: py, width: w, height: h });
2369
2346
  const spanF = bound() !== undefined ? frame() : f;
@@ -2421,7 +2398,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2421
2398
  g.leg.adopted.abort();
2422
2399
  g.leg = null;
2423
2400
  }
2424
- const displaced = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
2401
+ const displaced = tileCommands(deltasSince(g.startCells, g.startGeom, g.id));
2425
2402
  const snap = g.startGeom.get(g.id);
2426
2403
  if (snap) {
2427
2404
  g.node.setPosition(snap.pos.x, snap.pos.y);
@@ -2462,7 +2439,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2462
2439
  finally {
2463
2440
  writing = false;
2464
2441
  }
2465
- const sourceDisplaced = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
2442
+ const sourceDisplaced = tileCommands(deltasSince(g.startCells, g.startGeom, g.id));
2466
2443
  const before = g.startCells.get(g.id);
2467
2444
  const geomBefore = g.startGeom.get(g.id);
2468
2445
  const crossing = [
@@ -2527,7 +2504,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2527
2504
  }
2528
2505
  if (g.removedFromBoard && dragOut === 'remove') {
2529
2506
  // Release OUTSIDE the board → remove via the page's atomic command path.
2530
- const displaced = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
2507
+ const displaced = tileCommands(deltasSince(g.startCells, g.startGeom, g.id));
2531
2508
  const snap = g.startGeom.get(g.id);
2532
2509
  if (snap) {
2533
2510
  // Park the node on its start rect so the page's RemoveNodeCommand
@@ -2609,10 +2586,10 @@ export function bindDashboardGrid(api, group, options = {}) {
2609
2586
  };
2610
2587
  /** 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
2588
  const placeOnSelf = (g, desired, pushSolid = false) => {
2612
- var _a, _b, _c;
2589
+ var _a, _b;
2613
2590
  if (g.removedFromBoard) {
2614
2591
  g.removedFromBoard = false;
2615
- (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-out');
2592
+ setDim(g, false);
2616
2593
  const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), g.spans.w);
2617
2594
  engine.add({ id: g.id, x: 0, y: engine.rows(), w: g.spans.w, h: g.spans.h });
2618
2595
  if (!engine.moveCheck(g.id, cell.x, cell.y, { gate: false, pushSolid }).changed)
@@ -2620,7 +2597,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2620
2597
  project();
2621
2598
  }
2622
2599
  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;
2600
+ 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
2601
  const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), spanW);
2625
2602
  if (engine.moveCheck(g.id, cell.x, cell.y, { pushSolid }).changed)
2626
2603
  project();
@@ -2628,8 +2605,9 @@ export function bindDashboardGrid(api, group, options = {}) {
2628
2605
  };
2629
2606
  /** What the pointer means for the dragged tile: the resolve over the live tree, with the beside the hand holds. */
2630
2607
  const resolveTileZone = (g, ev) => {
2608
+ var _a, _b;
2631
2609
  let strip = null;
2632
- if (options.tabDrop && !isStatic) {
2610
+ if (options.tabDrop && !isStatic && g.kind !== 'palette') {
2633
2611
  const crect = api.container.getBoundingClientRect();
2634
2612
  strip = options.tabDrop.stripAt(crect.left + ev.screen.x, crect.top + ev.screen.y);
2635
2613
  }
@@ -2638,7 +2616,9 @@ export function bindDashboardGrid(api, group, options = {}) {
2638
2616
  y: ev.world.y,
2639
2617
  roots: zoneRoots(),
2640
2618
  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,
2619
+ prev: beside
2620
+ ? { 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()) }
2621
+ : ((_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
2622
  maxDepth: nesting,
2643
2623
  ghostDepth: 0,
2644
2624
  ghostSubtree: EMPTY_SUBTREE,
@@ -2858,11 +2838,16 @@ export function bindDashboardGrid(api, group, options = {}) {
2858
2838
  const tl = centredTopLeft(wx, wy, { w: itemW, h: opts.anchor === 'top' ? 0 : itemH });
2859
2839
  return pointToCell(tl.x, tl.y, frame(), geom(), rows(), itemW);
2860
2840
  };
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);
2841
+ 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
2842
  let lastWant = null;
2863
- const cell0 = wantedCell(world.x, world.y, span.w, span.h);
2864
- lastWant = cell0;
2865
- place(cell0, span.w);
2843
+ if (opts.beside) {
2844
+ besideOn(node.id, span, { id: opts.beside.containerId, side: opts.beside.side }, rowOfPoint(world.y));
2845
+ }
2846
+ else {
2847
+ const cell0 = wantedCell(world.x, world.y, span.w, span.h);
2848
+ lastWant = cell0;
2849
+ place(cell0, span.w, opts.push);
2850
+ }
2866
2851
  armGlide();
2867
2852
  project();
2868
2853
  syncPlaceholder();
@@ -2904,22 +2889,36 @@ export function bindDashboardGrid(api, group, options = {}) {
2904
2889
  const at = engine.getItem(node.id);
2905
2890
  return !!at && at.x === cell.x && at.y === cell.y && at.w === cell.w && at.h === cell.h;
2906
2891
  },
2907
- move: (w) => {
2892
+ move: (w, o) => {
2908
2893
  const item = engine.getItem(node.id);
2909
2894
  if (!item)
2910
2895
  return;
2896
+ if (beside)
2897
+ endBeside(true); // the hand left the band: the container comes back before the tile takes a plain cell
2911
2898
  const cell = wantedCell(w.x, w.y, item.w, opts.fit === 'shrink' ? hNatural : item.h);
2912
2899
  // The search is worth running once per wanted cell, not per pixel.
2913
2900
  if (lastWant && lastWant.x === cell.x && lastWant.y === cell.y)
2914
2901
  return;
2915
2902
  lastWant = cell;
2916
- if (place(cell, item.w))
2903
+ if (place(cell, item.w, !!(o === null || o === void 0 ? void 0 : o.push)))
2917
2904
  project();
2918
2905
  syncPlaceholder();
2919
2906
  },
2907
+ beside: (containerId, side, w) => {
2908
+ if (!engine.getItem(node.id) && !engine.add({ id: node.id, x: 0, y: engine.rows(), w: span.w, h: hNatural }))
2909
+ return;
2910
+ const row = rowOfPoint(w.y);
2911
+ if (beside && beside.id === containerId && beside.side === side && beside.row === row)
2912
+ return;
2913
+ lastWant = null;
2914
+ besideOn(node.id, span, { id: containerId, side }, row);
2915
+ syncPlaceholder();
2916
+ },
2917
+ 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
2918
  leave: () => {
2921
2919
  if (!engine.getItem(node.id))
2922
2920
  return;
2921
+ endBeside(true); // a container shifted for the tile comes back by itself: the shift was deliberate, so the memory never brings it
2923
2922
  engine.remove(node.id); // displaced tiles come home (gesture memory)
2924
2923
  lastWant = null;
2925
2924
  project();
@@ -2938,6 +2937,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2938
2937
  syncPlaceholder();
2939
2938
  },
2940
2939
  abort: () => {
2940
+ beside = null; // the snapshot restores the container with everything else
2941
2941
  if (engine.getItem(node.id))
2942
2942
  engine.remove(node.id);
2943
2943
  engine.cancelGesture(); // pre-entry layout, memory cleared
@@ -2948,6 +2948,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2948
2948
  syncPlaceholder();
2949
2949
  },
2950
2950
  finalize: () => {
2951
+ endBeside(false); // the shift stays: the deltas below carry it
2951
2952
  const item = engine.getItem(node.id);
2952
2953
  if (!item) {
2953
2954
  engine.endGesture();
@@ -2957,26 +2958,11 @@ export function bindDashboardGrid(api, group, options = {}) {
2957
2958
  }
2958
2959
  const cell = { x: item.x, y: item.y, w: item.w, h: item.h };
2959
2960
  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
- }
2961
+ // Widgets AND sections this board's adoption displaced, as one list: a
2962
+ // dock or a push that moved a section commits it with the drop.
2963
+ const commands = tileCommands(deltasSince(startCells, startGeom, node.id));
2979
2964
  engine.endGesture();
2965
+ pendingDrop = node.id; // the tile stays for the member the commit (or the host's drop-in) adds
2980
2966
  if (squeezeRoom !== undefined) {
2981
2967
  // What the board now HOLDS is its bound, not the squeeze's room.
2982
2968
  squeezeRoom = undefined;
@@ -2985,7 +2971,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2985
2971
  adoptedGhostId = null;
2986
2972
  disarmGlideSoon();
2987
2973
  syncPlaceholder();
2988
- return { commands, cell, rect, groups };
2974
+ return { commands, cell, rect };
2989
2975
  },
2990
2976
  };
2991
2977
  };
@@ -3947,13 +3933,6 @@ export function bindDashboardGrid(api, group, options = {}) {
3947
3933
  }
3948
3934
  };
3949
3935
  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
3936
  // A TOP DOCK INSERTS ROWS. The engine's push cascade resolves the band's
3958
3937
  // collisions one tile at a time and scrambles what stood beneath it (the
3959
3938
  // demo's KPI row came apart, two of its tiles under the chart). VS Code
@@ -4204,23 +4183,17 @@ export function bindDashboardGrid(api, group, options = {}) {
4204
4183
  return;
4205
4184
  }
4206
4185
  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;
4186
+ const halved = !!split && !!engine.getItem(z.target.id);
4209
4187
  split = null;
4210
- if (!fin || !before || !it) {
4188
+ if (!fin || !halved) {
4211
4189
  leg === null || leg === void 0 ? void 0 : leg.abort();
4212
4190
  done(false, 'cancel');
4213
4191
  return;
4214
4192
  }
4215
- const keep = { x: it.x, y: it.y, w: it.w, h: it.h };
4216
4193
  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
- ]);
4194
+ // The target's halving is in the leg's own deltas: its cell at rest was
4195
+ // snapshotted when the ghost entered, and its half is where it stands now.
4196
+ const changed = execute('Split group', [...fin.commands, ...planned.move, ...planned.collapse]);
4224
4197
  done(changed, 'commit');
4225
4198
  return;
4226
4199
  }
@@ -4238,7 +4211,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4238
4211
  return;
4239
4212
  }
4240
4213
  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');
4214
+ done(execute(z.kind === 'root' ? 'Dock tab' : 'Move tab out', [...fin.commands, ...planned.move, ...planned.collapse]), 'commit');
4242
4215
  };
4243
4216
  const onUp = () => finish(true);
4244
4217
  const onCancel = () => finish(false);
@@ -4310,43 +4283,39 @@ export function bindDashboardGrid(api, group, options = {}) {
4310
4283
  return;
4311
4284
  }
4312
4285
  beginGestureVisuals(g);
4286
+ // The chip is held by its middle: the ghost centres under the hand.
4287
+ const f = frame();
4288
+ const gg = geom();
4289
+ g.grab = { dx: (g.spans.w * (columnUnitFor(gg, f.width) + gap) - gap) / 2, dy: (g.spans.h * (rowHeightFor(gg, rows()) + gap) - gap) / 2 };
4313
4290
  }
4314
4291
  if (chip) {
4315
4292
  chip.style.left = `${e.clientX + 6}px`;
4316
4293
  chip.style.top = `${e.clientY + 6}px`;
4317
4294
  }
4295
+ // THE SAME PATH AS A TILE OF THIS BOARD (tile first, step 4a): the zone
4296
+ // under the hand names the board — this one, a page, a section — and
4297
+ // that board takes the ghost. The palette used to test "inside this
4298
+ // board" and nothing else, so a chip over a page landed under it.
4299
+ const rect = api.container.getBoundingClientRect();
4318
4300
  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();
4301
+ 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
4302
  api.render();
4347
4303
  };
4348
- const finish = (commit) => {
4304
+ /** The drop, on whichever board holds the ghost: the node carries its cell, the host's command adds it there. */
4305
+ const dropOn = (boardId, cell, displaced) => {
4349
4306
  var _a, _b;
4307
+ node.setGridItem(gridItemFromCell(cell));
4308
+ if (boardId === group.id)
4309
+ pendingDrop = g.id; // on another board its own finalize() pended it
4310
+ engine.endGesture();
4311
+ cleanupGestureVisuals(g);
4312
+ gesture = null;
4313
+ void ((_a = options.onDropIn) === null || _a === void 0 ? void 0 : _a.call(options, node, cell, displaced, { boardId }));
4314
+ persistLayouts();
4315
+ (_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'drop-in', kind: 'palette', nodeId: g.id, changed: true });
4316
+ api.renderNow();
4317
+ };
4318
+ const finish = (commit) => {
4350
4319
  detach();
4351
4320
  if (gesture !== g)
4352
4321
  return;
@@ -4357,18 +4326,21 @@ export function bindDashboardGrid(api, group, options = {}) {
4357
4326
  chip === null || chip === void 0 ? void 0 : chip.remove();
4358
4327
  return;
4359
4328
  }
4360
- if (commit && !g.removedFromBoard && engine.getItem(g.id)) {
4329
+ if (commit && g.leg) {
4330
+ // Dropped into another board (a page, a section): its leg closes with
4331
+ // that board's displaced tiles; this board displaced nothing.
4332
+ const fin = g.leg.adopted.finalize();
4333
+ const boardId = g.leg.adopted.groupId;
4334
+ g.leg = null;
4335
+ if (fin) {
4336
+ dropOn(boardId, fin.cell, [...tileCommands(deltasSince(g.startCells, g.startGeom, g.id)), ...fin.commands]);
4337
+ return;
4338
+ }
4339
+ }
4340
+ else if (commit && !g.removedFromBoard && engine.getItem(g.id)) {
4361
4341
  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();
4342
+ endBeside(false); // a container that shifted for the chip stays shifted: its delta commits with the drop
4343
+ dropOn(group.id, { x: item.x, y: item.y, w: item.w, h: item.h }, tileCommands(deltasSince(g.startCells, g.startGeom, g.id)));
4372
4344
  return;
4373
4345
  }
4374
4346
  // Abort (released outside, or Escape): restore the board.
@@ -4411,18 +4383,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4411
4383
  finally {
4412
4384
  writing = false;
4413
4385
  }
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
- }
4386
+ const commands = tileCommands(deltasSince(snap.cells, snap.geoms)); // a section's cell-and-frame command rides with the nodes'
4426
4387
  engine.endGesture();
4427
4388
  disarmGlideSoon();
4428
4389
  enforceBoardHeight();
@@ -4463,6 +4424,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4463
4424
  return;
4464
4425
  }
4465
4426
  applyFluidFrame();
4427
+ pendingDrop = null; // rebuilt from the members: a phantom is gone with the engine
4466
4428
  const items = [];
4467
4429
  for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
4468
4430
  if (!memberEntity(id))
@@ -4679,7 +4641,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4679
4641
  sizeAfter: { width: target.width, height: target.height },
4680
4642
  });
4681
4643
  }
4682
- return buildCommitCommands(deltas);
4644
+ return tileCommands(deltas);
4683
4645
  },
4684
4646
  moveTo(id, x, y) {
4685
4647
  return programmatic('Move widget', id, () => engine.moveCheck(id, x, y, { pushSolid: isGroupMember(id) }).changed);