@grafloria/element 0.4.51 → 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. */
@@ -367,8 +293,8 @@ export function bindDashboardGrid(api, group, options = {}) {
367
293
  const fluid = options.fluid === true;
368
294
  const overflow = (_h = options.overflow) !== null && _h !== void 0 ? _h : 'bounded';
369
295
  let isStatic = options.static === true;
370
- /** The deepest board a drop may enter — a root is 0; two levels are what the gates exercise (tile first, step 2). */
371
- const nesting = (_j = options.nesting) !== null && _j !== void 0 ? _j : 2;
296
+ /** The deepest board a drop may enter — a root is 0. Unbounded unless asked for: three-level drops are in the specs today (tile first, step 2). */
297
+ const nesting = (_j = options.nesting) !== null && _j !== void 0 ? _j : Number.POSITIVE_INFINITY;
372
298
  let dragHandle = normalizeDragHandle(options.dragHandle);
373
299
  let dragSel = dragHandleSelector(dragHandle);
374
300
  api.container.classList.toggle(DRAG_HANDLE_CLASS, dragHandle === true);
@@ -624,10 +550,14 @@ export function bindDashboardGrid(api, group, options = {}) {
624
550
  const cell = grp
625
551
  ? cellFromGridItem((_g = grp.getMetadata) === null || _g === void 0 ? void 0 : _g.call(grp, 'gridItem'))
626
552
  : null;
627
- // Member groups are LOCKED slabs (see the module doc).
553
+ // A member group is a SOLID tile (tile first, step 3): a widget carried
554
+ // over it slides aside and it never packs — the board under the hand
555
+ // holds still, so its zones (into, a tab, beside) stay reachable — and
556
+ // it is moved by INTENT: placed beside, pushed by a moved section, a
557
+ // dock or a widget it refused, moved by its own gesture.
628
558
  if (cell)
629
- return Object.assign(Object.assign({ id }, cell), { locked: true });
630
- return { id, x: 0, y: 0, w: columns, h: 1, locked: true, autoPosition: true };
559
+ return Object.assign(Object.assign({ id }, cell), { solid: true });
560
+ return { id, x: 0, y: 0, w: columns, h: 1, solid: true, autoPosition: true };
631
561
  };
632
562
  /** Persist adopted cells so save/undo round-trips them. */
633
563
  const persistAdoptedCell = (id, item) => {
@@ -1000,19 +930,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1000
930
  }
1001
931
  return out;
1002
932
  };
1003
- /** The member groups a gesture displaced, as their own cell commands — `buildCommitCommands` skips groups by design. */
1004
- const groupCellCommands = (deltas) => {
1005
- const out = [];
1006
- for (const d of deltas) {
1007
- 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))
1008
- continue;
1009
- const og = diagram.getGroup(d.id);
1010
- if (!og)
1011
- continue;
1012
- 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)));
1013
- }
1014
- return out;
1015
- };
1016
933
  const execute = (name, commands) => {
1017
934
  if (commands.length === 0)
1018
935
  return false;
@@ -1020,10 +937,23 @@ export function bindDashboardGrid(api, group, options = {}) {
1020
937
  return true;
1021
938
  };
1022
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;
1023
949
  const onMemberAdded = (id) => {
1024
- var _a;
950
+ var _a, _b;
1025
951
  if (disposed)
1026
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;
1027
957
  if (!engine.getItem(id)) {
1028
958
  const item = itemFor(id);
1029
959
  let placed = engine.add(item);
@@ -1033,7 +963,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1033
963
  // for this adoption — it floors at the content on the next refresh.
1034
964
  capacity = undefined;
1035
965
  engine = engineFrom([...engine.getItems().map((i) => (Object.assign({}, i))), item]);
1036
- placed = (_a = engine.getItem(id)) !== null && _a !== void 0 ? _a : null;
966
+ placed = (_b = engine.getItem(id)) !== null && _b !== void 0 ? _b : null;
1037
967
  refreshCapacity();
1038
968
  }
1039
969
  if (placed)
@@ -1288,7 +1218,13 @@ export function bindDashboardGrid(api, group, options = {}) {
1288
1218
  * carried along the top of a tall panel to its far right means "after it",
1289
1219
  * not "above it". The shift is undone if the pointer leaves the band.
1290
1220
  */
1291
- /** The container a BESIDE drop unlocked (and maybe shifted): relocked, and put back if asked, when the zone or the gesture ends. */
1221
+ /**
1222
+ * The beside the hand holds (tile first, step 3): the container's cell and
1223
+ * frame AT REST (the resolve reads the held container at rest), the cell
1224
+ * the widget took, and the row it took it at. The shift itself is the
1225
+ * engine's `placeBeside`; the sections it pushed remember their cells and
1226
+ * come back by themselves when the widget leaves (S2).
1227
+ */
1292
1228
  let beside = null;
1293
1229
  const endBeside = (restore) => {
1294
1230
  if (!beside)
@@ -1296,104 +1232,64 @@ export function bindDashboardGrid(api, group, options = {}) {
1296
1232
  const it = engine.getItem(beside.id);
1297
1233
  let moved = false;
1298
1234
  if (it && restore && (it.x !== beside.from.x || it.y !== beside.from.y))
1299
- moved = engine.moveCheck(beside.id, beside.from.x, beside.from.y, { gate: false }).changed || moved;
1300
- if (restore) {
1301
- // The sections the shift pushed come back with it.
1302
- for (const [oid, c] of beside.others) {
1303
- const o = engine.getItem(oid);
1304
- if (o && (o.x !== c.x || o.y !== c.y))
1305
- moved = engine.moveCheck(oid, c.x, c.y, { gate: false }).changed || moved;
1306
- }
1307
- }
1308
- if (it)
1309
- it.locked = true;
1310
- relockOthersForSlab();
1235
+ moved = engine.moveCheck(beside.id, beside.from.x, beside.from.y, { gate: false }).changed;
1311
1236
  beside = null;
1312
1237
  // The frames the zone test reads are the MODEL's: a restore that moved
1313
1238
  // the container without projecting left its frame where the push had
1314
- // put it, so the next test read the pointer as OUTSIDE it (the user's
1315
- // 3440-px corner on 0.4.48: along the top band the panel was pushed
1316
- // down, and the corner never turned into "right").
1239
+ // put it (the user's 3440-px corner on 0.4.48).
1317
1240
  if (moved)
1318
1241
  project();
1319
1242
  };
1320
- const applyBeside = (g, z) => {
1321
- var _a;
1243
+ /** The row of this board's grid under a world y — where a beside lands (D3: the pointer's row, not the container's top). */
1244
+ const rowOfPoint = (y) => {
1245
+ const r0 = cellToRect({ x: 0, y: 0, w: 1, h: 1 }, frame(), geom(), rows());
1246
+ const r1 = cellToRect({ x: 0, y: 1, w: 1, h: 1 }, frame(), geom(), rows());
1247
+ const pitch = Math.max(1, r1.y - r0.y);
1248
+ return Math.max(0, Math.floor((y - r0.y) / pitch));
1249
+ };
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) => {
1322
1258
  const tc = engine.getItem(z.id);
1323
1259
  if (!tc)
1324
1260
  return;
1325
- if (g.leg) {
1326
- g.leg.adopted.abort();
1327
- g.leg = null;
1328
- }
1329
- const w = g.spans.w;
1330
- const h = g.spans.h;
1331
- if (g.removedFromBoard) {
1332
- g.removedFromBoard = false;
1333
- (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-out');
1334
- engine.add({ id: g.id, x: 0, y: engine.rows(), w, h });
1335
- }
1336
1261
  if (beside && (beside.id !== z.id || beside.side !== z.side))
1337
- endBeside(true); // another container, or another side of it: start over from the rest layout
1338
- const from = beside ? beside.from : { x: tc.x, y: tc.y };
1339
- let cell;
1340
- let shiftTo = null;
1341
- switch (z.side) {
1342
- case 'right':
1343
- cell = { x: from.x + tc.w, y: from.y };
1344
- if (cell.x + w > columns) {
1345
- shiftTo = { x: columns - w - tc.w, y: from.y };
1346
- cell = { x: columns - w, y: from.y };
1347
- }
1348
- break;
1349
- case 'left':
1350
- cell = { x: from.x - w, y: from.y };
1351
- if (cell.x < 0) {
1352
- shiftTo = { x: w, y: from.y };
1353
- cell = { x: 0, y: from.y };
1354
- }
1355
- break;
1356
- case 'top':
1357
- cell = { x: Math.max(0, Math.min(from.x, columns - w)), y: from.y }; // the ghost takes the container's rows; the container is pushed down under it
1358
- break;
1359
- default:
1360
- cell = { x: Math.max(0, Math.min(from.x, columns - w)), y: from.y + tc.h };
1361
- }
1362
- if (shiftTo && shiftTo.x < 0)
1363
- shiftTo = null; // no room even shifted: the widget goes where it can
1262
+ endBeside(true); // another container, or another side of it: from the rest layout
1364
1263
  if (!beside) {
1365
- // The other sections give way to the shift the way they give way to a
1366
- // moved group (0.4.44): on the demo the Operations section spans the
1367
- // row under the panel and a locked tile refused the shift outright.
1368
- // Their cells are kept so they come back if the pointer leaves.
1369
- const others = new Map();
1370
- for (const o of engine.getItems())
1371
- if (o.id !== z.id && o.id !== g.id && isGroupMember(o.id))
1372
- others.set(o.id, { x: o.x, y: o.y });
1373
1264
  const grp0 = diagram.getGroup(z.id);
1374
- beside = { id: z.id, side: z.side, from, frame0: grp0 ? frameOfGroup(grp0) : cellToRect({ x: from.x, y: from.y, w: tc.w, h: tc.h }, frame(), geom(), rows()), vacated: cell, others };
1375
- unlockOthersForSlab(z.id);
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 };
1376
1266
  }
1377
- tc.locked = false; // for the gesture: it shifts, or the ghost pushes it
1378
- // The ghost steps off the board while the container shifts: the engine
1379
- // will not push the dragged tile, so a chart as wide as the panel had its
1380
- // own cell as the panel's target and the shift was refused (lab L94).
1381
- // Then it takes the cell the shift vacated.
1382
- if (shiftTo && (tc.x !== shiftTo.x || tc.y !== shiftTo.y)) {
1383
- if (engine.getItem(g.id))
1384
- engine.remove(g.id);
1385
- engine.moveCheck(z.id, shiftTo.x, shiftTo.y, { gate: false });
1267
+ const r = engine.placeBeside(id, z.id, z.side, row);
1268
+ const at = engine.getItem(id);
1269
+ if (r.changed && at) {
1270
+ beside.vacated = { x: at.x, y: at.y };
1271
+ beside.row = row;
1386
1272
  }
1387
- beside.vacated = cell;
1388
- if (!engine.getItem(g.id))
1389
- engine.add({ id: g.id, x: 0, y: engine.rows(), w, h });
1390
- if (!engine.moveCheck(g.id, cell.x, cell.y, { gate: false }).changed) {
1391
- const at = engine.getItem(g.id);
1392
- if (!at || at.x !== cell.x || at.y !== cell.y)
1393
- placeNear(g.id, cell.x, cell.y, w);
1273
+ else if (at) {
1274
+ // Refused (a bound): the widget goes where it can, as any refused cell does.
1275
+ placeNear(id, at.x, at.y, spans.w);
1394
1276
  }
1395
1277
  project();
1396
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
+ };
1397
1293
  /**
1398
1294
  * CARRIED (0.4.43): a group dragged by its strip or band moves as ONE thing.
1399
1295
  * The held TILE is transition-exempt (the ghost), but a group has no host of
@@ -1610,13 +1506,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1610
1506
  },
1611
1507
  move: false,
1612
1508
  };
1613
- // A TOP or LEFT edge moves the slab's origin, and the engine refuses to
1614
- // move a locked tile: the move never landed, only the resize did, and a
1615
- // section pulled up by its caption grew DOWNWARD a row per pointer step
1616
- // — 18 rows for a 2-row pull (identification round, F16). Unlocked for
1617
- // its own gesture, as a move is; relocked on release.
1618
- if (edges.n || edges.w)
1619
- it.locked = false;
1620
1509
  capturePointer(slabGesture.pointerId);
1621
1510
  api.container.style.cursor = cursorFor(edges);
1622
1511
  };
@@ -1633,24 +1522,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1633
1522
  * pushes it; a group's children ride along: the frame moves, the nested
1634
1523
  * board re-projects. The others relock on release.
1635
1524
  */
1636
- let slabUnlocked = [];
1637
- const unlockOthersForSlab = (id) => {
1638
- slabUnlocked = [];
1639
- for (const o of engine.getItems()) {
1640
- if (o.id !== id && o.locked && isGroupMember(o.id)) {
1641
- o.locked = false;
1642
- slabUnlocked.push(o.id);
1643
- }
1644
- }
1645
- };
1646
- const relockOthersForSlab = () => {
1647
- for (const oid of slabUnlocked) {
1648
- const o = engine.getItem(oid);
1649
- if (o)
1650
- o.locked = true;
1651
- }
1652
- slabUnlocked = [];
1653
- };
1654
1525
  const beginSlabMove = (id, ev) => {
1655
1526
  const grp = diagram.getGroup(id);
1656
1527
  const it = engine.getItem(id);
@@ -1658,8 +1529,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1658
1529
  return;
1659
1530
  engine.beginGesture();
1660
1531
  const snap = snapshotAll();
1661
- it.locked = false;
1662
- unlockOthersForSlab(id);
1663
1532
  slabGesture = {
1664
1533
  id,
1665
1534
  edges: NO_EDGES,
@@ -1676,11 +1545,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1676
1545
  capturePointer(slabGesture.pointerId);
1677
1546
  api.container.style.cursor = 'grabbing';
1678
1547
  };
1679
- const relockSlab = (id) => {
1680
- const it = engine.getItem(id);
1681
- if (it)
1682
- it.locked = true;
1683
- };
1684
1548
  /** The cell a slab move asked for and could not have — painted so the refusal is visible; null clears it. */
1685
1549
  let refusal = null;
1686
1550
  const showRefusal = (cell, w, h) => {
@@ -1734,7 +1598,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1734
1598
  // Along its ROW only: a section dragged LEFT that wandered DOWN its
1735
1599
  // column (the row search) read as the wrong tile moving.
1736
1600
  const was = { x: it.x, y: it.y };
1737
- const moved = engine.moveCheck(g.id, cell.x, cell.y, { gate: false }).changed || placeOnRow(g.id, cell.x, cell.y, it.w);
1601
+ const moved = engine.moveCheck(g.id, cell.x, cell.y, { gate: false, pushSolid: true }).changed || placeOnRow(g.id, cell.x, cell.y, it.w, true); // a moved section pushes the sections in its way (0.4.44)
1738
1602
  if (moved) {
1739
1603
  project();
1740
1604
  setCarried(g.id, true); // chrome repainted by the projection is carried too
@@ -1781,7 +1645,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1781
1645
  }
1782
1646
  let changed = false;
1783
1647
  if (x !== it.x || y !== it.y)
1784
- changed = engine.moveCheck(g.id, x, y, { gate: false }).changed || changed;
1648
+ changed = engine.moveCheck(g.id, x, y, { gate: false, pushSolid: true }).changed || changed;
1785
1649
  if (w !== it.w || h !== it.h)
1786
1650
  changed = engine.resizeCheck(g.id, w, h).changed || changed;
1787
1651
  if (changed)
@@ -1796,8 +1660,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1796
1660
  showRefusal(null, 0, 0);
1797
1661
  releasePointer(g.pointerId);
1798
1662
  api.container.style.cursor = '';
1799
- relockSlab(g.id);
1800
- relockOthersForSlab();
1801
1663
  if (g.move && g.started)
1802
1664
  setCarried(g.id, false); // exempt through the drop write, then the glides resume
1803
1665
  if (!g.started) {
@@ -1806,19 +1668,9 @@ export function bindDashboardGrid(api, group, options = {}) {
1806
1668
  }
1807
1669
  engine.endGesture();
1808
1670
  project();
1809
- const it = engine.getItem(g.id);
1810
- const grp = diagram.getGroup(g.id);
1811
- const deltas = deltasSince(g.startCells, g.startGeom, g.id);
1812
- const commands = buildCommitCommands(deltas);
1813
- // The sections and groups the move PUSHED (0.4.44): the tile commit skips
1814
- // groups by design (a group's cell is its own command), so they are
1815
- // committed here the way a dock commits the groups it displaced — or the
1816
- // model snapped every one of them, and the mover, straight back.
1817
- commands.push(...groupCellCommands(deltas));
1818
- const b = g.cellBefore;
1819
- if (it && grp && (b.x !== it.x || b.y !== it.y || b.w !== it.w || b.h !== it.h)) {
1820
- commands.push(new SetGroupCellCommand(g.id, b, { x: it.x, y: it.y, w: it.w, h: it.h }, g.frameBefore, frameOfGroup(grp)));
1821
- }
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));
1822
1674
  const changed = execute(g.move ? 'Move section' : 'Resize section', commands);
1823
1675
  disarmGlideSoon();
1824
1676
  syncHandles();
@@ -1833,8 +1685,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1833
1685
  slabGesture = null;
1834
1686
  releasePointer(g.pointerId);
1835
1687
  api.container.style.cursor = '';
1836
- relockSlab(g.id);
1837
- relockOthersForSlab();
1838
1688
  if (g.move && g.started)
1839
1689
  setCarried(g.id, false);
1840
1690
  if (g.started)
@@ -1905,6 +1755,12 @@ export function bindDashboardGrid(api, group, options = {}) {
1905
1755
  if (rect.height !== undefined)
1906
1756
  el.style.height = `${rect.height}px`;
1907
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
+ };
1908
1764
  const cleanupGestureVisuals = (g) => {
1909
1765
  var _a;
1910
1766
  if (g.kind !== 'palette')
@@ -1935,8 +1791,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1935
1791
  gesture = null;
1936
1792
  project();
1937
1793
  const deltas = deltasSince(g.startCells, g.startGeom);
1938
- const commands = buildCommitCommands(deltas);
1939
- 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)
1940
1795
  endBeside(false);
1941
1796
  if (g.esc && g.esc.rowsAdded !== 0) {
1942
1797
  commands.push(new SetGroupCellCommand(group.id, g.esc.cellBefore, g.esc.cellAfter, g.esc.frameBefore, g.esc.frameAfter));
@@ -1953,7 +1808,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1953
1808
  (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed });
1954
1809
  };
1955
1810
  const cancelActiveGesture = (notify = true) => {
1956
- var _a, _b, _c, _d, _e;
1811
+ var _a, _b, _c;
1957
1812
  if (gesture === null || gesture === void 0 ? void 0 : gesture.strip) {
1958
1813
  (_a = options.tabDrop) === null || _a === void 0 ? void 0 : _a.markDrop(null, null);
1959
1814
  gesture.strip = null;
@@ -1968,31 +1823,20 @@ export function bindDashboardGrid(api, group, options = {}) {
1968
1823
  if (!g)
1969
1824
  return;
1970
1825
  gesture = null;
1971
- if (g.kind !== 'palette' && g.leg) {
1826
+ if (g.leg) {
1972
1827
  g.leg.adopted.abort(); // target board back to its pre-entry layout
1973
1828
  g.leg = null;
1974
1829
  }
1975
- if (g.kind !== 'palette' && g.esc && g.esc.rowsAdded !== 0) {
1830
+ if (g.esc && g.esc.rowsAdded !== 0) {
1976
1831
  g.esc.peer.resizeMemberBy(group.id, -g.esc.rowsAdded); // slab back down
1977
1832
  g.esc = null;
1978
1833
  }
1979
1834
  endBeside(true);
1980
1835
  if (g.started) {
1981
- if (g.removedFromBoard || g.kind === 'palette') {
1982
- // The engine cannot resurrect a removed item rebuild from the
1983
- // gesture-start snapshot (cells are pure data; the constructor
1984
- // honours legal layouts verbatim).
1985
- engine.endGesture();
1986
- const items = [];
1987
- for (const [id, c] of g.startCells) {
1988
- const lockedNode = ((_d = (_c = diagram.getNode(id)) === null || _c === void 0 ? void 0 : _c.state) === null || _d === void 0 ? void 0 : _d.locked) === true;
1989
- items.push(Object.assign(Object.assign({ id }, c), { locked: lockedNode || isGroupMember(id) }));
1990
- }
1991
- engine = engineFrom(items);
1992
- }
1993
- else {
1994
- engine.cancelGesture();
1995
- }
1836
+ // The engine's gesture snapshot restores cells, sizes AND membership
1837
+ // (engine 0.3.8): a ghost removed mid-gesture comes back at its start
1838
+ // cell, a palette tile added mid-gesture is gone.
1839
+ engine.cancelGesture();
1996
1840
  if (designRows !== undefined) {
1997
1841
  maxRows = liveBound(engine.getItems());
1998
1842
  engine.maxRows = maxRows;
@@ -2033,7 +1877,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2033
1877
  enforceBoardHeight();
2034
1878
  api.renderNow();
2035
1879
  if (notify) {
2036
- (_e = options.onGesture) === null || _e === void 0 ? void 0 : _e.call(options, { type: 'cancel', kind: g.kind, nodeId: g.id, changed: false });
1880
+ (_c = options.onGesture) === null || _c === void 0 ? void 0 : _c.call(options, { type: 'cancel', kind: g.kind, nodeId: g.id, changed: false });
2037
1881
  }
2038
1882
  };
2039
1883
  /** Centre a w×h-span tile's top-left under the cursor, in world px. */
@@ -2047,141 +1891,186 @@ export function bindDashboardGrid(api, group, options = {}) {
2047
1891
  y: worldY - (spans.h * (rh + gap) - gap) / 2,
2048
1892
  };
2049
1893
  };
2050
- const onToolMove = (ev) => {
2051
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
2052
- const g = gesture;
2053
- 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();
2054
1965
  return;
2055
- if (!g.started) {
2056
- const dx = ev.screen.x - g.downClient.x;
2057
- const dy = ev.screen.y - g.downClient.y;
2058
- if (Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD)
2059
- return;
2060
- beginGestureVisuals(g);
2061
1966
  }
2062
- if (g.kind === 'move') {
2063
- const desired = { x: ev.world.x - g.grab.dx, y: ev.world.y - g.grab.dy };
2064
- g.node.setPosition(desired.x, desired.y);
2065
- ghostStyleFastPath(g, desired);
2066
- g.lastWorld = { x: ev.world.x, y: ev.world.y };
2067
- g.lastScreen = { x: ev.screen.x, y: ev.screen.y };
2068
- // THE ZONE (tile first, step 2): one recursive resolve over the boards'
2069
- // membership tree a strip slot, a cell beside a container, a plain
2070
- // cell on the deepest board the pointer may enter, or off — decided
2071
- // BEFORE any engine is asked anything. The strip still wins over every
2072
- // board; a band's stickiness and the vacated cell still hold a beside.
2073
- const z = resolveTileZone(g, ev);
2074
- if (z.kind === 'strip' && options.tabDrop && !isStatic) {
2075
- // -- INTO A STRIP: the widget becomes a new tab there, so it leaves
2076
- // 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) {
2077
2003
  if (g.leg) {
2078
2004
  g.leg.adopted.abort();
2079
2005
  g.leg = null;
2080
2006
  }
2081
- endBeside(true); // a band's shift gives way to the strip: the container comes back
2082
- if (!g.removedFromBoard) {
2083
- g.removedFromBoard = true;
2084
- engine.remove(g.id);
2085
- project();
2086
- }
2087
- (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-out');
2088
- if (!g.strip || g.strip.containerId !== z.containerId || g.strip.index !== z.index) {
2089
- options.tabDrop.markDrop(z.containerId, z.index);
2090
- }
2091
- g.strip = { containerId: z.containerId, index: z.index };
2092
- syncPlaceholder();
2093
- api.render();
2094
- return;
2007
+ placeOnSelf(g, desired, true);
2008
+ setDim(g, false);
2095
2009
  }
2096
- if (g.strip) {
2097
- (_b = options.tabDrop) === null || _b === void 0 ? void 0 : _b.markDrop(null, null);
2098
- 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);
2099
2013
  }
2100
- // -- BESIDE a tab container of THIS board: its outer band puts the
2101
- // widget next to it — at the board's edge the container shifts over
2102
- // to make room, live and gliding, like any widget gives way (0.4.48).
2103
- // (A container on another board still resolves as a plain cell there
2104
- // until the gesture scope spans boards — step 3.)
2105
- if (z.kind === 'beside' && !isStatic && z.board.ref === selfPeer) {
2106
- if (!z.kept)
2107
- applyBeside(g, { id: z.containerId, side: z.side });
2108
- syncPlaceholder();
2109
- return;
2014
+ else if (parent && enterLeg(parent, { push: true })) {
2015
+ setDim(g, false);
2110
2016
  }
2111
- if (beside)
2112
- endBeside(true); // the hand left the band: the container comes back
2113
- // Last resort: the grace band — a small slip past the edge stays ON
2114
- // this board (the engine clamps the cell; prototype parity).
2115
- const onSelf = z.kind !== 'off' && z.kind !== 'strip' && z.board.ref === selfPeer;
2116
- const inside = onSelf || (z.kind === 'off' && worldInsideBoardGrace(ev.world.x, ev.world.y));
2117
- const peer = !onSelf && z.kind !== 'off' && z.kind !== 'strip' ? z.board.ref : null;
2118
- if (peer) {
2119
- // -- HANDOFF: the pointer is over another board -------------------
2120
- if (g.leg && g.leg.peer === peer) {
2121
- g.leg.adopted.move(ev.world);
2122
- }
2123
- else {
2124
- if (g.leg) {
2125
- g.leg.adopted.abort();
2126
- g.leg = null;
2127
- }
2128
- if (!g.removedFromBoard) {
2129
- g.removedFromBoard = true;
2130
- engine.remove(g.id); // survivors settle home (gesture memory intact)
2131
- project();
2132
- }
2133
- const adopted = peer.adopt(g.node, ev.world, {
2134
- width: g.node.size.width,
2135
- height: g.node.size.height,
2136
- });
2137
- if (adopted) {
2138
- (_c = hostOf(g.id)) === null || _c === void 0 ? void 0 : _c.classList.remove('axdb-out');
2139
- g.leg = { peer, adopted };
2140
- g.leg.adopted.move(ev.world);
2141
- }
2142
- else {
2143
- // Bounded/full board refused the adoption: dim = will snap home.
2144
- (_d = hostOf(g.id)) === null || _d === void 0 ? void 0 : _d.classList.add('axdb-out');
2145
- }
2146
- }
2017
+ else {
2018
+ // No board holds the refusing section (a view): dim = will snap home.
2019
+ leaveSelf();
2020
+ setDim(g, true);
2147
2021
  }
2148
- else if (inside) {
2149
- // -- back on (or still on) our own board --------------------------
2150
- if (g.leg) {
2151
- g.leg.adopted.abort();
2152
- g.leg = null;
2153
- }
2154
- if (g.removedFromBoard) {
2155
- g.removedFromBoard = false;
2156
- (_e = hostOf(g.id)) === null || _e === void 0 ? void 0 : _e.classList.remove('axdb-out');
2157
- const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), g.spans.w);
2158
- // Re-enter at the bottom edge (collision-free), then take the cursor
2159
- // cell GATELESSLY — a first placement skips the anti-jitter gate.
2160
- engine.add({ id: g.id, x: 0, y: engine.rows(), w: g.spans.w, h: g.spans.h });
2161
- engine.moveCheck(g.id, cell.x, cell.y, { gate: false });
2162
- project();
2163
- }
2164
- else {
2165
- const spanW = (_g = (_f = engine.getItem(g.id)) === null || _f === void 0 ? void 0 : _f.w) !== null && _g !== void 0 ? _g : g.spans.w;
2166
- const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), spanW);
2167
- if (engine.moveCheck(g.id, cell.x, cell.y).changed)
2168
- project();
2169
- }
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);
2031
+ }
2032
+ else if (enterLeg(peer, {})) {
2033
+ g.refusedPeer = null;
2034
+ setDim(g, false);
2035
+ g.leg.adopted.move(ev.world);
2170
2036
  }
2171
2037
  else {
2172
- // -- outside every board ------------------------------------------
2173
- if (g.leg) {
2174
- g.leg.adopted.abort();
2175
- g.leg = null;
2176
- }
2177
- if (!g.removedFromBoard) {
2178
- g.removedFromBoard = true;
2179
- engine.remove(g.id); // survivors settle home; cells minted nowhere
2180
- (_h = hostOf(g.id)) === null || _h === void 0 ? void 0 : _h.classList.add('axdb-out');
2181
- project();
2182
- }
2038
+ // The board refused (full, fit): the widget pushes it on its parent (D2).
2039
+ g.refusedPeer = peer;
2040
+ pushRefused(peer);
2183
2041
  }
2184
- 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);
2185
2074
  return;
2186
2075
  }
2187
2076
  // resize: fluid pixel preview on the ghost, cell-stepped live push.
@@ -2287,7 +2176,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2287
2176
  maxRows = n;
2288
2177
  engine.maxRows = n;
2289
2178
  };
2290
- const slabRows = (_k = (_j = parent.memberCell(group.id)) === null || _j === void 0 ? void 0 : _j.h) !== null && _k !== void 0 ? _k : maxRows;
2179
+ const slabRows = (_b = (_a = parent.memberCell(group.id)) === null || _a === void 0 ? void 0 : _a.h) !== null && _b !== void 0 ? _b : maxRows;
2291
2180
  const inner = maxRows;
2292
2181
  const rhNow = rowHeightFor(geom(), rows());
2293
2182
  const rowPx = rhNow + gap;
@@ -2451,7 +2340,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2451
2340
  const anchorBottom = liveRect ? liveRect.y + liveRect.height : bottom;
2452
2341
  const px = E.w ? anchorRight - w : anchorLeft;
2453
2342
  const py = E.n ? anchorBottom - h : anchorTop;
2454
- g.node.setSize(w, h, (_l = g.node.size.depth) !== null && _l !== void 0 ? _l : 0);
2343
+ g.node.setSize(w, h, (_c = g.node.size.depth) !== null && _c !== void 0 ? _c : 0);
2455
2344
  g.node.setPosition(px, py);
2456
2345
  ghostStyleFastPath(g, { x: px, y: py, width: w, height: h });
2457
2346
  const spanF = bound() !== undefined ? frame() : f;
@@ -2509,7 +2398,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2509
2398
  g.leg.adopted.abort();
2510
2399
  g.leg = null;
2511
2400
  }
2512
- const displaced = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
2401
+ const displaced = tileCommands(deltasSince(g.startCells, g.startGeom, g.id));
2513
2402
  const snap = g.startGeom.get(g.id);
2514
2403
  if (snap) {
2515
2404
  g.node.setPosition(snap.pos.x, snap.pos.y);
@@ -2550,7 +2439,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2550
2439
  finally {
2551
2440
  writing = false;
2552
2441
  }
2553
- const sourceDisplaced = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
2442
+ const sourceDisplaced = tileCommands(deltasSince(g.startCells, g.startGeom, g.id));
2554
2443
  const before = g.startCells.get(g.id);
2555
2444
  const geomBefore = g.startGeom.get(g.id);
2556
2445
  const crossing = [
@@ -2615,7 +2504,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2615
2504
  }
2616
2505
  if (g.removedFromBoard && dragOut === 'remove') {
2617
2506
  // Release OUTSIDE the board → remove via the page's atomic command path.
2618
- const displaced = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
2507
+ const displaced = tileCommands(deltasSince(g.startCells, g.startGeom, g.id));
2619
2508
  const snap = g.startGeom.get(g.id);
2620
2509
  if (snap) {
2621
2510
  // Park the node on its start rect so the page's RemoveNodeCommand
@@ -2695,10 +2584,30 @@ export function bindDashboardGrid(api, group, options = {}) {
2695
2584
  });
2696
2585
  return peers.filter((p) => !p.group.parentGroupId).map((p) => boardRef(p, 0));
2697
2586
  };
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. */
2588
+ const placeOnSelf = (g, desired, pushSolid = false) => {
2589
+ var _a, _b;
2590
+ if (g.removedFromBoard) {
2591
+ g.removedFromBoard = false;
2592
+ setDim(g, false);
2593
+ const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), g.spans.w);
2594
+ engine.add({ id: g.id, x: 0, y: engine.rows(), w: g.spans.w, h: g.spans.h });
2595
+ if (!engine.moveCheck(g.id, cell.x, cell.y, { gate: false, pushSolid }).changed)
2596
+ placeNear(g.id, cell.x, cell.y, g.spans.w, pushSolid);
2597
+ project();
2598
+ }
2599
+ else {
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;
2601
+ const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), spanW);
2602
+ if (engine.moveCheck(g.id, cell.x, cell.y, { pushSolid }).changed)
2603
+ project();
2604
+ }
2605
+ };
2698
2606
  /** What the pointer means for the dragged tile: the resolve over the live tree, with the beside the hand holds. */
2699
2607
  const resolveTileZone = (g, ev) => {
2608
+ var _a, _b;
2700
2609
  let strip = null;
2701
- if (options.tabDrop && !isStatic) {
2610
+ if (options.tabDrop && !isStatic && g.kind !== 'palette') {
2702
2611
  const crect = api.container.getBoundingClientRect();
2703
2612
  strip = options.tabDrop.stripAt(crect.left + ev.screen.x, crect.top + ev.screen.y);
2704
2613
  }
@@ -2707,13 +2616,26 @@ export function bindDashboardGrid(api, group, options = {}) {
2707
2616
  y: ev.world.y,
2708
2617
  roots: zoneRoots(),
2709
2618
  strip,
2710
- 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
2711
2622
  maxDepth: nesting,
2712
2623
  ghostDepth: 0,
2713
2624
  ghostSubtree: EMPTY_SUBTREE,
2714
2625
  gap,
2626
+ homeChain: homeChain(),
2715
2627
  });
2716
2628
  };
2629
+ /** The groups this board sits in, all the way up: their bands never apply to a tile of this board. */
2630
+ const homeChain = () => {
2631
+ const out = new Set();
2632
+ let cur = group;
2633
+ for (let i = 0; cur && i < 32; i++) {
2634
+ out.add(cur.id);
2635
+ cur = cur.parentGroupId ? diagram.getGroup(cur.parentGroupId) : undefined;
2636
+ }
2637
+ return out;
2638
+ };
2717
2639
  /** The board whose engine holds OUR group as an item (nesting parent). */
2718
2640
  const parentPeer = () => {
2719
2641
  for (const p of peersOnCanvas()) {
@@ -2756,7 +2678,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2756
2678
  * wherever the last swing left it. Ask where the tile IS, not whether the
2757
2679
  * call moved it.
2758
2680
  */
2759
- const placeOnRow = (id, x, y, w) => {
2681
+ const placeOnRow = (id, x, y, w, pushSolid = false) => {
2760
2682
  const at = (cx) => {
2761
2683
  const i = engine.getItem(id);
2762
2684
  return !!i && i.x === cx && i.y === y;
@@ -2764,7 +2686,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2764
2686
  const maxX = Math.max(0, columns - w);
2765
2687
  if (at(x))
2766
2688
  return true;
2767
- if (engine.moveCheck(id, x, y, { gate: false }).changed)
2689
+ if (engine.moveCheck(id, x, y, { gate: false, pushSolid }).changed)
2768
2690
  return true;
2769
2691
  for (let d = 1; d <= columns; d++) {
2770
2692
  for (const cx of [x - d, x + d]) {
@@ -2772,14 +2694,14 @@ export function bindDashboardGrid(api, group, options = {}) {
2772
2694
  continue;
2773
2695
  if (at(cx))
2774
2696
  return true;
2775
- if (engine.moveCheck(id, cx, y, { gate: false }).changed)
2697
+ if (engine.moveCheck(id, cx, y, { gate: false, pushSolid }).changed)
2776
2698
  return true;
2777
2699
  }
2778
2700
  }
2779
2701
  return false;
2780
2702
  };
2781
- const placeNear = (id, x, y, w) => {
2782
- if (placeOnRow(id, x, y, w))
2703
+ const placeNear = (id, x, y, w, pushSolid = false) => {
2704
+ if (placeOnRow(id, x, y, w, pushSolid))
2783
2705
  return true;
2784
2706
  // EVERY column on that row refused — which is what a locked section
2785
2707
  // spanning the full width does, and there are plenty of those. Sliding
@@ -2791,7 +2713,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2791
2713
  for (const cy of [y - d, y + d]) {
2792
2714
  if (cy < 0)
2793
2715
  continue;
2794
- if (placeOnRow(id, x, cy, w))
2716
+ if (placeOnRow(id, x, cy, w, pushSolid))
2795
2717
  return true;
2796
2718
  }
2797
2719
  }
@@ -2806,7 +2728,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2806
2728
  const fitHeightAt = (id, x, y, w, hNatural) => {
2807
2729
  let limit = hNatural;
2808
2730
  for (const it of engine.getItems()) {
2809
- if (it.id === id || !it.locked)
2731
+ if (it.id === id || !(it.locked || it.solid))
2810
2732
  continue;
2811
2733
  if (!(it.x < x + w && x < it.x + it.w))
2812
2734
  continue;
@@ -2916,11 +2838,16 @@ export function bindDashboardGrid(api, group, options = {}) {
2916
2838
  const tl = centredTopLeft(wx, wy, { w: itemW, h: opts.anchor === 'top' ? 0 : itemH });
2917
2839
  return pointToCell(tl.x, tl.y, frame(), geom(), rows(), itemW);
2918
2840
  };
2919
- 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);
2920
2842
  let lastWant = null;
2921
- const cell0 = wantedCell(world.x, world.y, span.w, span.h);
2922
- lastWant = cell0;
2923
- 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
+ }
2924
2851
  armGlide();
2925
2852
  project();
2926
2853
  syncPlaceholder();
@@ -2935,7 +2862,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2935
2862
  const it = engine.getItem(node.id);
2936
2863
  return it ? cellToRect(it, frame(), geom(), rows()) : null;
2937
2864
  },
2938
- place: (cell) => {
2865
+ place: (cell, pushSolid = false) => {
2939
2866
  if (!engine.getItem(node.id) && !engine.add({ id: node.id, x: 0, y: engine.rows(), w: cell.w, h: cell.h }))
2940
2867
  return false;
2941
2868
  const it = engine.getItem(node.id);
@@ -2952,32 +2879,46 @@ export function bindDashboardGrid(api, group, options = {}) {
2952
2879
  engine.resizeCheck(node.id, w0, h0);
2953
2880
  const moved = engine.getItem(node.id);
2954
2881
  if (moved && (moved.x !== cell.x || moved.y !== cell.y))
2955
- engine.moveCheck(node.id, cell.x, cell.y, { gate: false });
2882
+ engine.moveCheck(node.id, cell.x, cell.y, { gate: false, pushSolid });
2956
2883
  const now = engine.getItem(node.id);
2957
2884
  if (now && (now.w !== cell.w || now.h !== cell.h))
2958
- engine.resizeCheck(node.id, cell.w, cell.h);
2885
+ engine.resizeCheck(node.id, cell.w, cell.h, { pushSolid });
2959
2886
  lastWant = null;
2960
2887
  project();
2961
2888
  syncPlaceholder();
2962
2889
  const at = engine.getItem(node.id);
2963
2890
  return !!at && at.x === cell.x && at.y === cell.y && at.w === cell.w && at.h === cell.h;
2964
2891
  },
2965
- move: (w) => {
2892
+ move: (w, o) => {
2966
2893
  const item = engine.getItem(node.id);
2967
2894
  if (!item)
2968
2895
  return;
2896
+ if (beside)
2897
+ endBeside(true); // the hand left the band: the container comes back before the tile takes a plain cell
2969
2898
  const cell = wantedCell(w.x, w.y, item.w, opts.fit === 'shrink' ? hNatural : item.h);
2970
2899
  // The search is worth running once per wanted cell, not per pixel.
2971
2900
  if (lastWant && lastWant.x === cell.x && lastWant.y === cell.y)
2972
2901
  return;
2973
2902
  lastWant = cell;
2974
- if (place(cell, item.w))
2903
+ if (place(cell, item.w, !!(o === null || o === void 0 ? void 0 : o.push)))
2975
2904
  project();
2976
2905
  syncPlaceholder();
2977
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,
2978
2918
  leave: () => {
2979
2919
  if (!engine.getItem(node.id))
2980
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
2981
2922
  engine.remove(node.id); // displaced tiles come home (gesture memory)
2982
2923
  lastWant = null;
2983
2924
  project();
@@ -2996,6 +2937,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2996
2937
  syncPlaceholder();
2997
2938
  },
2998
2939
  abort: () => {
2940
+ beside = null; // the snapshot restores the container with everything else
2999
2941
  if (engine.getItem(node.id))
3000
2942
  engine.remove(node.id);
3001
2943
  engine.cancelGesture(); // pre-entry layout, memory cleared
@@ -3006,6 +2948,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3006
2948
  syncPlaceholder();
3007
2949
  },
3008
2950
  finalize: () => {
2951
+ endBeside(false); // the shift stays: the deltas below carry it
3009
2952
  const item = engine.getItem(node.id);
3010
2953
  if (!item) {
3011
2954
  engine.endGesture();
@@ -3015,26 +2958,11 @@ export function bindDashboardGrid(api, group, options = {}) {
3015
2958
  }
3016
2959
  const cell = { x: item.x, y: item.y, w: item.w, h: item.h };
3017
2960
  const rect = cellToRect(item, frame(), geom(), rows());
3018
- const commands = buildCommitCommands(deltasSince(startCells, startGeom, node.id));
3019
- const groups = [];
3020
- for (const [id, before] of startCells) {
3021
- if (!isGroupMember(id))
3022
- continue;
3023
- const it = engine.getItem(id);
3024
- const g0 = startGeom.get(id);
3025
- if (!it || !g0)
3026
- continue;
3027
- if (it.x === before.x && it.y === before.y && it.w === before.w && it.h === before.h)
3028
- continue;
3029
- groups.push({
3030
- id,
3031
- cellBefore: before,
3032
- cellAfter: { x: it.x, y: it.y, w: it.w, h: it.h },
3033
- frameBefore: { x: g0.pos.x, y: g0.pos.y, width: g0.size.width, height: g0.size.height },
3034
- frameAfter: cellToRect(it, frame(), geom(), rows()),
3035
- });
3036
- }
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));
3037
2964
  engine.endGesture();
2965
+ pendingDrop = node.id; // the tile stays for the member the commit (or the host's drop-in) adds
3038
2966
  if (squeezeRoom !== undefined) {
3039
2967
  // What the board now HOLDS is its bound, not the squeeze's room.
3040
2968
  squeezeRoom = undefined;
@@ -3043,7 +2971,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3043
2971
  adoptedGhostId = null;
3044
2972
  disarmGlideSoon();
3045
2973
  syncPlaceholder();
3046
- return { commands, cell, rect, groups };
2974
+ return { commands, cell, rect };
3047
2975
  },
3048
2976
  };
3049
2977
  };
@@ -4005,32 +3933,6 @@ export function bindDashboardGrid(api, group, options = {}) {
4005
3933
  }
4006
3934
  };
4007
3935
  const zoneKey = (z) => JSON.stringify(z, (k, v) => (k === 'target' ? v.id : k === 'peer' ? v.group.id : v));
4008
- // DOCKING PUSHES SECTIONS. A section is a locked tile so that a widget
4009
- // never pushes it; a group docked against the board's edge is the one
4010
- // gesture that must — everything below the top band moves down. For the
4011
- // preview every member group is unlocked; they are relocked when the
4012
- // pointer leaves the band, and their moved cells are committed with the
4013
- // dock.
4014
- let unlockedGroups = [];
4015
- const unlockGroups = () => {
4016
- if (unlockedGroups.length > 0)
4017
- return;
4018
- for (const it of engine.getItems()) {
4019
- if (it.id !== plan.arrivingId && it.locked && isGroupMember(it.id)) {
4020
- it.locked = false;
4021
- unlockedGroups.push(it.id);
4022
- }
4023
- }
4024
- };
4025
- const relockGroups = () => {
4026
- for (const id of unlockedGroups) {
4027
- const it = engine.getItem(id);
4028
- if (it)
4029
- it.locked = true;
4030
- }
4031
- unlockedGroups = [];
4032
- };
4033
- 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));
4034
3936
  // A TOP DOCK INSERTS ROWS. The engine's push cascade resolves the band's
4035
3937
  // collisions one tile at a time and scrambles what stood beneath it (the
4036
3938
  // demo's KPI row came apart, two of its tiles under the chart). VS Code
@@ -4097,18 +3999,24 @@ export function bindDashboardGrid(api, group, options = {}) {
4097
3999
  engine.moveCheck(split.id, split.before.x, split.before.y, { gate: false });
4098
4000
  if (it.w !== split.before.w || it.h !== split.before.h)
4099
4001
  engine.resizeCheck(split.id, split.before.w, split.before.h);
4100
- it.locked = true;
4101
4002
  }
4102
4003
  split = null;
4103
4004
  project();
4104
4005
  };
4105
4006
  const previewSplit = (z, world) => {
4106
4007
  const it = engine.getItem(z.target.id);
4008
+ if (!it)
4009
+ return false;
4010
+ // The target's cell AT REST is the one the commit restores — read it
4011
+ // BEFORE the leg enters: the arriving ghost is placed under the pointer
4012
+ // first, and a target that is a tile like any other is pushed by it
4013
+ // for a moment (tile first, step 3) until it takes its half below.
4014
+ const before = { x: it.x, y: it.y, w: it.w, h: it.h };
4015
+ const frameBefore = frameOfGroup(z.target);
4107
4016
  const l = ensureLeg(world, null);
4108
- if (!it || !l)
4017
+ if (!l)
4109
4018
  return false;
4110
- split = { id: z.target.id, before: { x: it.x, y: it.y, w: it.w, h: it.h }, frameBefore: frameOfGroup(z.target), keep: z.keep };
4111
- it.locked = false; // its own gesture for the moment: it may shrink and shift
4019
+ split = { id: z.target.id, before, frameBefore, keep: z.keep };
4112
4020
  if (it.w !== z.keep.w || it.h !== z.keep.h)
4113
4021
  engine.resizeCheck(z.target.id, z.keep.w, z.keep.h);
4114
4022
  const now = engine.getItem(z.target.id);
@@ -4130,8 +4038,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4130
4038
  const l = ensureLeg(world, null);
4131
4039
  if (!l)
4132
4040
  return false;
4133
- unlockGroups();
4134
- l.place(z.cell);
4041
+ l.place(z.cell, true); // DOCKING PUSHES SECTIONS: everything below the band moves down, solid or not
4135
4042
  insertRows(z.cell, l);
4136
4043
  project();
4137
4044
  placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
@@ -4153,7 +4060,6 @@ export function bindDashboardGrid(api, group, options = {}) {
4153
4060
  }
4154
4061
  undoSplitPreview();
4155
4062
  undoInsertRows();
4156
- relockGroups();
4157
4063
  plan.markDrop(null, null);
4158
4064
  hideOverlay();
4159
4065
  // A PREVIEW IS AN OVERLAY. A split and a dock used to be applied LIVE
@@ -4241,7 +4147,6 @@ export function bindDashboardGrid(api, group, options = {}) {
4241
4147
  if (!commit || z.kind === 'home' || z.kind === 'off' || (z.kind === 'root' && !ensureLeg(world, null)) || (z.kind === 'board' && (!ensureLeg(world, (_a = z.peer) !== null && _a !== void 0 ? _a : null) || landingHidden()))) {
4242
4148
  undoSplitPreview();
4243
4149
  leg === null || leg === void 0 ? void 0 : leg.abort();
4244
- relockGroups();
4245
4150
  done(false, 'cancel');
4246
4151
  return;
4247
4152
  }
@@ -4278,25 +4183,17 @@ export function bindDashboardGrid(api, group, options = {}) {
4278
4183
  return;
4279
4184
  }
4280
4185
  const fin = (_b = leg === null || leg === void 0 ? void 0 : leg.finalize()) !== null && _b !== void 0 ? _b : null;
4281
- const it = engine.getItem(z.target.id);
4282
- const before = split;
4283
- if (it)
4284
- it.locked = true;
4186
+ const halved = !!split && !!engine.getItem(z.target.id);
4285
4187
  split = null;
4286
- if (!fin || !before || !it) {
4188
+ if (!fin || !halved) {
4287
4189
  leg === null || leg === void 0 ? void 0 : leg.abort();
4288
4190
  done(false, 'cancel');
4289
4191
  return;
4290
4192
  }
4291
- const keep = { x: it.x, y: it.y, w: it.w, h: it.h };
4292
4193
  const planned = plan.commands(fin.cell, fin.rect, group.id);
4293
- const changed = execute('Split group', [
4294
- ...fin.commands,
4295
- ...groupCommands(fin, z.target.id),
4296
- new SetGroupCellCommand(z.target.id, before.before, keep, before.frameBefore, cellToRect(keep, frame(), geom(), rows())),
4297
- ...planned.move,
4298
- ...planned.collapse,
4299
- ]);
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]);
4300
4197
  done(changed, 'commit');
4301
4198
  return;
4302
4199
  }
@@ -4309,13 +4206,12 @@ export function bindDashboardGrid(api, group, options = {}) {
4309
4206
  hideOverlay(); // a re-applied zone repaints its preview; the release is not a preview
4310
4207
  plan.markDrop(null, null);
4311
4208
  const fin = (_c = leg === null || leg === void 0 ? void 0 : leg.finalize()) !== null && _c !== void 0 ? _c : null;
4312
- relockGroups();
4313
4209
  if (!fin) {
4314
4210
  done(false, 'cancel');
4315
4211
  return;
4316
4212
  }
4317
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);
4318
- 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');
4319
4215
  };
4320
4216
  const onUp = () => finish(true);
4321
4217
  const onCancel = () => finish(false);
@@ -4387,43 +4283,39 @@ export function bindDashboardGrid(api, group, options = {}) {
4387
4283
  return;
4388
4284
  }
4389
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 };
4390
4290
  }
4391
4291
  if (chip) {
4392
4292
  chip.style.left = `${e.clientX + 6}px`;
4393
4293
  chip.style.top = `${e.clientY + 6}px`;
4394
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();
4395
4300
  const world = toWorld(e.clientX, e.clientY);
4396
- const inside = worldInsideBoard(world.x, world.y);
4397
- if (inside) {
4398
- const tl = centredTopLeft(world.x, world.y, g.spans);
4399
- const cell = pointToCell(tl.x, tl.y, frame(), geom(), rows(), g.spans.w);
4400
- if (g.removedFromBoard) {
4401
- g.removedFromBoard = false;
4402
- // Enter at the bottom edge (collision-free), then take the cursor
4403
- // cell GATELESSLY — gridstack's drag-in skips the gate on entry.
4404
- // A bounded board with no room refuses the entry: the chip dims to
4405
- // say so, and the release will snap it home.
4406
- const entered = engine.add({ id: g.id, x: 0, y: engine.rows(), w: g.spans.w, h: g.spans.h });
4407
- chip === null || chip === void 0 ? void 0 : chip.classList.toggle('axdb-out', !entered);
4408
- if (entered)
4409
- engine.moveCheck(g.id, cell.x, cell.y, { gate: false });
4410
- project();
4411
- }
4412
- else if (engine.moveCheck(g.id, cell.x, cell.y).changed) {
4413
- project();
4414
- }
4415
- }
4416
- else if (!g.removedFromBoard) {
4417
- g.removedFromBoard = true;
4418
- chip === null || chip === void 0 ? void 0 : chip.classList.remove('axdb-out');
4419
- engine.remove(g.id); // displaced tiles come home (gesture memory)
4420
- project();
4421
- }
4422
- 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 } });
4423
4302
  api.render();
4424
4303
  };
4425
- 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) => {
4426
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) => {
4427
4319
  detach();
4428
4320
  if (gesture !== g)
4429
4321
  return;
@@ -4434,18 +4326,21 @@ export function bindDashboardGrid(api, group, options = {}) {
4434
4326
  chip === null || chip === void 0 ? void 0 : chip.remove();
4435
4327
  return;
4436
4328
  }
4437
- 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)) {
4438
4341
  const item = engine.getItem(g.id);
4439
- const cell = { x: item.x, y: item.y, w: item.w, h: item.h };
4440
- node.setGridItem(gridItemFromCell(cell));
4441
- const displaced = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
4442
- engine.endGesture();
4443
- cleanupGestureVisuals(g);
4444
- gesture = null;
4445
- void ((_a = options.onDropIn) === null || _a === void 0 ? void 0 : _a.call(options, node, cell, displaced));
4446
- persistLayouts();
4447
- (_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'drop-in', kind: 'palette', nodeId: g.id, changed: true });
4448
- 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)));
4449
4344
  return;
4450
4345
  }
4451
4346
  // Abort (released outside, or Escape): restore the board.
@@ -4472,14 +4367,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4472
4367
  return false;
4473
4368
  engine.beginGesture();
4474
4369
  const snap = snapshotAll();
4475
- // A section is a LOCKED tile; for ITS OWN move it is the one moving.
4476
- const self = engine.getItem(id);
4477
- const wasLocked = !!(self === null || self === void 0 ? void 0 : self.locked);
4478
- if (self && isGroupMember(id))
4479
- self.locked = false;
4480
4370
  const ok = op();
4481
- if (self && isGroupMember(id))
4482
- self.locked = wasLocked;
4483
4371
  if (!ok) {
4484
4372
  engine.endGesture();
4485
4373
  return false;
@@ -4495,18 +4383,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4495
4383
  finally {
4496
4384
  writing = false;
4497
4385
  }
4498
- const commands = buildCommitCommands(deltasSince(snap.cells, snap.geoms));
4499
- if (isGroupMember(id)) {
4500
- // A SECTION: its cell lives in group metadata and its frame is written
4501
- // by project(); the node commands above skip groups (D5 of the review).
4502
- const it = engine.getItem(id);
4503
- const grp = diagram.getGroup(id);
4504
- const cb = snap.cells.get(id);
4505
- const gb = snap.geoms.get(id);
4506
- if (it && grp && cb && gb && (cb.x !== it.x || cb.y !== it.y || cb.w !== it.w || cb.h !== it.h)) {
4507
- 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)));
4508
- }
4509
- }
4386
+ const commands = tileCommands(deltasSince(snap.cells, snap.geoms)); // a section's cell-and-frame command rides with the nodes'
4510
4387
  engine.endGesture();
4511
4388
  disarmGlideSoon();
4512
4389
  enforceBoardHeight();
@@ -4547,6 +4424,7 @@ export function bindDashboardGrid(api, group, options = {}) {
4547
4424
  return;
4548
4425
  }
4549
4426
  applyFluidFrame();
4427
+ pendingDrop = null; // rebuilt from the members: a phantom is gone with the engine
4550
4428
  const items = [];
4551
4429
  for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
4552
4430
  if (!memberEntity(id))
@@ -4763,10 +4641,10 @@ export function bindDashboardGrid(api, group, options = {}) {
4763
4641
  sizeAfter: { width: target.width, height: target.height },
4764
4642
  });
4765
4643
  }
4766
- return buildCommitCommands(deltas);
4644
+ return tileCommands(deltas);
4767
4645
  },
4768
4646
  moveTo(id, x, y) {
4769
- return programmatic('Move widget', id, () => engine.moveCheck(id, x, y).changed);
4647
+ return programmatic('Move widget', id, () => engine.moveCheck(id, x, y, { pushSolid: isGroupMember(id) }).changed);
4770
4648
  },
4771
4649
  resizeTo(id, w, h) {
4772
4650
  const hh = isGroupMember(id) ? Math.max(h, innerRowsOf(id)) : h; // a section: never below its children