@grafloria/element 0.4.55 → 0.4.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafloria/element",
3
- "version": "0.4.55",
3
+ "version": "0.4.56",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -619,6 +619,8 @@ export interface DashboardHandleContext {
619
619
  tearOutPlan?: (containerId: string, pageId: string) => TearOutPlan | null;
620
620
  /** The commands that close `boardId` when it is a tab PAGE about to lose its last member `leaving` — and its container when that was its last page. */
621
621
  closePageIfEmptied?: (boardId: string, leaving: string) => Command[];
622
+ /** The bookkeeping for a CONTAINER a gesture moved from one board to another (tile first, 4b-ii): its spec entry, its registry. */
623
+ moveContainerCommands?: (containerId: string, fromBoardId: string, toBoardId: string) => Command[];
622
624
  /** A widget dragged over a strip: hit-test, marks, and the drop that makes it a new tab. Handed to every binder. */
623
625
  tabDrop?: TabDropHooks;
624
626
  /** Set by finalize: the user's `onTabChange`, so the handle can fire it. */
@@ -790,6 +790,54 @@ export function createDashboardHandle(ctx) {
790
790
  * looked like. Undo reopens the page (its grid re-binds on the history event)
791
791
  * and then the widget comes back into it.
792
792
  */
793
+ /**
794
+ * A CONTAINER CROSSES BOARDS by hand (tile first, 4b-ii): a section carried
795
+ * into a page, a tab container into a section. The membership commands
796
+ * move it; this moves what the kit keeps beside the membership — the
797
+ * authored entry from the old board's list to the new one's, and which
798
+ * board the container is filed under — as a register/unregister pair, the
799
+ * way the tear-out plan files a group it creates. `toJSON()` reads the
800
+ * membership, so the document is right either way; the registry is what
801
+ * `addWidget` and the page-closing rule read.
802
+ */
803
+ ctx.moveContainerCommands = (containerId, fromBoardId, toBoardId) => {
804
+ if (!ctx.boardGroups.has(containerId))
805
+ return [];
806
+ const spec = specById.get(containerId);
807
+ const fromArr = ctx.boardWidgets.get(fromBoardId);
808
+ const toArr = ctx.boardWidgets.get(toBoardId);
809
+ const prevFiled = viewOfWidget.get(containerId);
810
+ let slot = -1;
811
+ const moved = {
812
+ register: () => {
813
+ viewOfWidget.set(containerId, toBoardId);
814
+ if (spec && fromArr) {
815
+ const i = fromArr.findIndex((w) => w.id === containerId);
816
+ if (i >= 0) {
817
+ slot = i;
818
+ fromArr.splice(i, 1);
819
+ }
820
+ }
821
+ if (spec && toArr && !toArr.some((w) => w.id === containerId))
822
+ toArr.push(spec);
823
+ },
824
+ unregister: () => {
825
+ if (prevFiled !== undefined)
826
+ viewOfWidget.set(containerId, prevFiled);
827
+ else
828
+ viewOfWidget.delete(containerId);
829
+ if (spec && toArr) {
830
+ const i = toArr.findIndex((w) => w.id === containerId);
831
+ if (i >= 0)
832
+ toArr.splice(i, 1);
833
+ }
834
+ if (spec && fromArr && !fromArr.some((w) => w.id === containerId)) {
835
+ fromArr.splice(slot < 0 ? fromArr.length : Math.min(slot, fromArr.length), 0, spec);
836
+ }
837
+ },
838
+ };
839
+ return [new RegisterWidgetCommand(moved, 'register')];
840
+ };
793
841
  ctx.closePageIfEmptied = (boardId, leaving) => {
794
842
  var _a, _b, _c, _d, _e, _f;
795
843
  const model = (_a = ctx.apiRef) === null || _a === void 0 ? void 0 : _a.getModel();
@@ -2303,7 +2351,7 @@ export function dashboard(options) {
2303
2351
  if (e.type === 'commit')
2304
2352
  reportChanged();
2305
2353
  (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
2306
- }, onSelect: (id) => { var _a; return (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id, v.id); } }), captionHooks(v.id)), { onMemberLeaving: (memberId) => { var _a, _b; return (_b = (_a = ctx.closePageIfEmptied) === null || _a === void 0 ? void 0 : _a.call(ctx, v.id, memberId)) !== null && _b !== void 0 ? _b : []; } }), (ctx.tabDrop ? { tabDrop: ctx.tabDrop } : {}));
2354
+ }, onSelect: (id) => { var _a; return (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id, v.id); } }), captionHooks(v.id)), { onMemberLeaving: (memberId) => { var _a, _b; return (_b = (_a = ctx.closePageIfEmptied) === null || _a === void 0 ? void 0 : _a.call(ctx, v.id, memberId)) !== null && _b !== void 0 ? _b : []; }, onMemberMoving: (memberId, from, to) => { var _a, _b; return (_b = (_a = ctx.moveContainerCommands) === null || _a === void 0 ? void 0 : _a.call(ctx, memberId, from, to)) !== null && _b !== void 0 ? _b : []; } }), (ctx.tabDrop ? { tabDrop: ctx.tabDrop } : {}));
2307
2355
  if (viewLayout === 'split') {
2308
2356
  return bindDashboardSplit(a, g, Object.assign(Object.assign(Object.assign({}, common), { columns: (_h = v.columns) !== null && _h !== void 0 ? _h : columns, baseRowHeight: rowHeight, designHeight: viewH(v) }), (v.tree !== undefined ? { tree: v.tree } : {})));
2309
2357
  }
@@ -2321,7 +2369,7 @@ export function dashboard(options) {
2321
2369
  if (e.type === 'commit')
2322
2370
  reportChanged();
2323
2371
  (_b = (_a = options.binder) === null || _a === void 0 ? void 0 : _a.onGesture) === null || _b === void 0 ? void 0 : _b.call(_a, e);
2324
- }, onSelect: (id) => { var _a, _b; return (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id, (_b = ctx.viewOfBoard.get(w.id)) !== null && _b !== void 0 ? _b : ctx.active); } }), captionHooks((_h = ctx.viewOfBoard.get(w.id)) !== null && _h !== void 0 ? _h : ctx.active)), { onMemberLeaving: (memberId) => { var _a, _b; return (_b = (_a = ctx.closePageIfEmptied) === null || _a === void 0 ? void 0 : _a.call(ctx, w.id, memberId)) !== null && _b !== void 0 ? _b : []; } }), (ctx.tabDrop ? { tabDrop: ctx.tabDrop } : {}));
2372
+ }, onSelect: (id) => { var _a, _b; return (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id, (_b = ctx.viewOfBoard.get(w.id)) !== null && _b !== void 0 ? _b : ctx.active); } }), captionHooks((_h = ctx.viewOfBoard.get(w.id)) !== null && _h !== void 0 ? _h : ctx.active)), { onMemberLeaving: (memberId) => { var _a, _b; return (_b = (_a = ctx.closePageIfEmptied) === null || _a === void 0 ? void 0 : _a.call(ctx, w.id, memberId)) !== null && _b !== void 0 ? _b : []; }, onMemberMoving: (memberId, from, to) => { var _a, _b; return (_b = (_a = ctx.moveContainerCommands) === null || _a === void 0 ? void 0 : _a.call(ctx, memberId, from, to)) !== null && _b !== void 0 ? _b : []; } }), (ctx.tabDrop ? { tabDrop: ctx.tabDrop } : {}));
2325
2373
  if (((_j = ctx.layoutOf.get(w.id)) !== null && _j !== void 0 ? _j : w.layout) === 'split') {
2326
2374
  // A splitter tree covering the pane; the pane's frame is the parent's
2327
2375
  // slab, so no design height of its own.
@@ -203,6 +203,12 @@ export interface DashboardGridOptions {
203
203
  * it in the same batch — a tab page emptied by the move closes.
204
204
  */
205
205
  onMemberLeaving?: (memberId: string) => Command[];
206
+ /**
207
+ * A member CONTAINER is moving from this board to another by a gesture
208
+ * (tile first, 4b-ii). Answers the commands that carry the host's own
209
+ * bookkeeping for it — the spec entry, the registry — in the same batch.
210
+ */
211
+ onMemberMoving?: (memberId: string, fromBoardId: string, toBoardId: string) => Command[];
206
212
  /** A widget dragged over a tab STRIP becomes a new tab there: the hooks that hit-test, mark and commit it. */
207
213
  tabDrop?: TabDropHooks;
208
214
  /** Fires after commits/cancels/removals so the page can refocus/refit/flash. */
@@ -148,6 +148,8 @@ export function registerBoardPeer(container, peer) {
148
148
  };
149
149
  }
150
150
  const DRAG_THRESHOLD = 4;
151
+ /** The node a gesture holds — node-only paths (the pixel ghost, a resize, a strip drop, a removal) ask through this. */
152
+ const nodeOf = (g) => g.entity;
151
153
  let binderSeq = 0;
152
154
  export function bindDashboardGrid(api, group, options = {}) {
153
155
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
@@ -487,7 +489,8 @@ export function bindDashboardGrid(api, group, options = {}) {
487
489
  hostOf,
488
490
  memberEntity,
489
491
  sizeOf,
490
- ghostId: () => adoptedGhostId !== null && adoptedGhostId !== void 0 ? adoptedGhostId : ((gesture === null || gesture === void 0 ? void 0 : gesture.started) ? gesture.id : null),
492
+ // A NODE ghost follows the hand by pixels and gets the placeholder; a GROUP ghost (a container adopted here) is projected to its cell like any tile, carried.
493
+ ghostId: () => (adoptedGhostId && !diagram.getGroup(adoptedGhostId) ? adoptedGhostId : (gesture === null || gesture === void 0 ? void 0 : gesture.started) && gesture.subject === 'node' ? gesture.id : null),
491
494
  write: (fn) => {
492
495
  writing = true;
493
496
  try {
@@ -947,6 +950,12 @@ export function bindDashboardGrid(api, group, options = {}) {
947
950
  g.leg.adopted.abort();
948
951
  g.leg = null;
949
952
  }
953
+ if (g.subject === 'group' && !g.removedFromBoard && !beside) {
954
+ // A group ghost pushed its way here with intent: the containers it moved
955
+ // come home (the engine's memory) before the beside is measured on them.
956
+ engine.remove(g.id);
957
+ g.removedFromBoard = true;
958
+ }
950
959
  if (g.removedFromBoard) {
951
960
  g.removedFromBoard = false;
952
961
  setDim(g, false);
@@ -968,8 +977,8 @@ export function bindDashboardGrid(api, group, options = {}) {
968
977
  return false;
969
978
  };
970
979
  let slabGesture = null;
971
- /** The slab gesture as it is NOW read through a call so a guard's narrowing does not stick. */
972
- const currentSlab = () => slabGesture;
980
+ /** The gesture as it is NOW, for the same reason. */
981
+ const currentGesture = () => gesture;
973
982
  /** The PARENT running a resize of OUR section from a press this tool claimed. */
974
983
  let forwardSlab = null;
975
984
  const frameOfGroup = (grp) => ({ x: grp.position.x, y: grp.position.y, width: sizeOf(grp).width, height: sizeOf(grp).height });
@@ -1017,25 +1026,45 @@ export function bindDashboardGrid(api, group, options = {}) {
1017
1026
  const it = engine.getItem(id);
1018
1027
  if (!grp || !it || gesture || slabGesture || isStatic)
1019
1028
  return;
1020
- engine.beginGesture();
1021
- const snap = snapshotAll();
1022
- slabGesture = {
1029
+ // A GROUP MOVE IS A GESTURE OF THE ONE MACHINE (tile first, 4b-ii): the
1030
+ // same threshold, snapshot, zone walk, legs, beside and commit a widget
1031
+ // gets — the group being the subject. The frame follows its cell.
1032
+ //
1033
+ // THE PRESS ARMS NOTHING. `beginGestureVisuals` arms the glide when the
1034
+ // threshold is crossed, as it does for a widget: arming it here left a
1035
+ // plain CLICK on a caption band gliding for 400 ms, and everything the
1036
+ // page did next — a caption change, a layout switch — animated instead of
1037
+ // landing (the gallery's fluid-board: "and the rows are back" measured a
1038
+ // tile still travelling).
1039
+ gesture = {
1040
+ kind: 'move',
1023
1041
  id,
1024
- edges: NO_EDGES,
1042
+ subject: 'group',
1043
+ entity: grp,
1025
1044
  pointerId: typeof PointerEvent !== 'undefined' && ev.source instanceof PointerEvent ? ev.source.pointerId : null,
1026
1045
  started: false,
1027
- downScreen: { x: ev.screen.x, y: ev.screen.y },
1028
- startCells: snap.cells,
1029
- startGeom: snap.geoms,
1030
- cellBefore: { x: it.x, y: it.y, w: it.w, h: it.h },
1031
- frameBefore: frameOfGroup(grp),
1032
- grab: { dx: grp.position.x - ev.world.x, dy: grp.position.y - ev.world.y },
1033
- move: true,
1046
+ downClient: { x: ev.screen.x, y: ev.screen.y },
1047
+ downWorld: { x: ev.world.x, y: ev.world.y },
1048
+ grab: { dx: ev.world.x - grp.position.x, dy: ev.world.y - grp.position.y },
1049
+ startCells: new Map(),
1050
+ startGeom: new Map(),
1051
+ startSize: { width: sizeOf(grp).width, height: sizeOf(grp).height },
1052
+ startPos: { x: grp.position.x, y: grp.position.y },
1053
+ edges: NO_EDGES,
1054
+ spans: { w: it.w, h: it.h },
1055
+ removedFromBoard: false,
1056
+ leg: null,
1057
+ strip: null,
1058
+ lastWorld: null,
1059
+ lastScreen: null,
1060
+ hostEl: null,
1061
+ esc: null,
1062
+ chip: null,
1034
1063
  };
1035
- capturePointer(slabGesture.pointerId);
1036
- api.container.style.cursor = 'grabbing';
1037
1064
  };
1038
1065
  const slabMove = (ev) => {
1066
+ if ((gesture === null || gesture === void 0 ? void 0 : gesture.subject) === 'group')
1067
+ return onToolMove(ev);
1039
1068
  const g = slabGesture;
1040
1069
  if (!g)
1041
1070
  return;
@@ -1120,6 +1149,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1120
1149
  };
1121
1150
  const slabUp = () => {
1122
1151
  var _a;
1152
+ if ((gesture === null || gesture === void 0 ? void 0 : gesture.subject) === 'group')
1153
+ return onToolUp();
1123
1154
  const g = slabGesture;
1124
1155
  if (!g)
1125
1156
  return;
@@ -1146,6 +1177,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1146
1177
  };
1147
1178
  const slabCancel = () => {
1148
1179
  var _a;
1180
+ if ((gesture === null || gesture === void 0 ? void 0 : gesture.subject) === 'group')
1181
+ return cancelActiveGesture();
1149
1182
  const g = slabGesture;
1150
1183
  if (!g)
1151
1184
  return;
@@ -1193,8 +1226,12 @@ export function bindDashboardGrid(api, group, options = {}) {
1193
1226
  g.started = true;
1194
1227
  armGlide();
1195
1228
  if (g.kind !== 'palette') {
1196
- setGhost(g.id, true);
1197
- g.hostEl = hostOf(g.id);
1229
+ if (g.subject === 'node') {
1230
+ setGhost(g.id, true);
1231
+ g.hostEl = hostOf(g.id);
1232
+ }
1233
+ else
1234
+ setCarried(g.id, true); // a group moves as one thing: its whole subtree is exempt from the glide
1198
1235
  capturePointer(g.pointerId);
1199
1236
  api.container.style.cursor = g.kind === 'resize' ? 'nwse-resize' : 'grabbing';
1200
1237
  }
@@ -1230,8 +1267,14 @@ export function bindDashboardGrid(api, group, options = {}) {
1230
1267
  };
1231
1268
  const cleanupGestureVisuals = (g) => {
1232
1269
  var _a;
1233
- if (g.kind !== 'palette')
1234
- setGhost(g.id, false);
1270
+ if (g.kind !== 'palette') {
1271
+ if (g.subject === 'node')
1272
+ setGhost(g.id, false);
1273
+ else {
1274
+ setCarried(g.id, false); // exempt through the drop write, then the glides resume
1275
+ showRefusal(null, 0, 0);
1276
+ }
1277
+ }
1235
1278
  disarmGlideSoon();
1236
1279
  releasePointer(g.pointerId);
1237
1280
  api.container.style.cursor = '';
@@ -1266,8 +1309,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1266
1309
  persistLayouts(); // an edit at a narrow count propagated into the wide cache
1267
1310
  if (changed) {
1268
1311
  const it = engine.getItem(g.id);
1269
- if (it)
1270
- live.announce(`${nameOf(g.node)} ${g.kind === 'resize' ? 'resized' : 'moved'} to ${describeCell(it)}`);
1312
+ if (it && g.subject === 'node')
1313
+ live.announce(`${nameOf(nodeOf(g))} ${g.kind === 'resize' ? 'resized' : 'moved'} to ${describeCell(it)}`);
1271
1314
  }
1272
1315
  syncA11y();
1273
1316
  api.renderNow();
@@ -1367,14 +1410,17 @@ export function bindDashboardGrid(api, group, options = {}) {
1367
1410
  const moveGhost = (g, ev) => {
1368
1411
  var _a;
1369
1412
  const desired = { x: ev.world.x - g.grab.dx, y: ev.world.y - g.grab.dy };
1370
- g.node.setPosition(desired.x, desired.y);
1371
- ghostStyleFastPath(g, desired);
1413
+ if (g.subject === 'node') {
1414
+ // The pixel ghost follows the hand; a group's frame is projected from its cell, carried.
1415
+ nodeOf(g).setPosition(desired.x, desired.y);
1416
+ ghostStyleFastPath(g, desired);
1417
+ }
1372
1418
  g.lastWorld = { x: ev.world.x, y: ev.world.y };
1373
1419
  g.lastScreen = { x: ev.screen.x, y: ev.screen.y };
1374
1420
  /** The ghost's pixel size on THIS board — a palette chip has spans, not a size. */
1375
1421
  const pxSize = () => {
1376
1422
  if (g.kind !== 'palette')
1377
- return { width: g.node.size.width, height: g.node.size.height };
1423
+ return sizeOf(g.entity);
1378
1424
  const f = frame();
1379
1425
  const gg = geom();
1380
1426
  return { width: g.spans.w * (columnUnitFor(gg, f.width) + gap) - gap, height: g.spans.h * (rowHeightFor(gg, rows()) + gap) - gap };
@@ -1403,7 +1449,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1403
1449
  project();
1404
1450
  }
1405
1451
  if (!g.leg) {
1406
- const adopted = peer.adopt(g.node, ev.world, pxSize(), opts);
1452
+ const adopted = peer.adopt(g.entity, ev.world, pxSize(), opts);
1407
1453
  if (!adopted)
1408
1454
  return false;
1409
1455
  g.leg = { peer, adopted };
@@ -1416,7 +1462,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1416
1462
  // BEFORE any engine is asked anything. The strip still wins over every
1417
1463
  // board; a band's stickiness and the vacated cell still hold a beside.
1418
1464
  const z = resolveTileZone(g, ev);
1419
- if (z.kind === 'strip' && options.tabDrop && !isStatic && g.kind !== 'palette') {
1465
+ if (z.kind === 'strip' && options.tabDrop && !isStatic && g.kind !== 'palette' && g.subject === 'node') {
1420
1466
  // -- INTO A STRIP: the widget becomes a new tab there, so it leaves
1421
1467
  // this board (survivors settle home) and the strip marks the slot.
1422
1468
  endBeside(true); // a band's shift gives way to the strip: the container comes back
@@ -1514,7 +1560,10 @@ export function bindDashboardGrid(api, group, options = {}) {
1514
1560
  g.leg = null;
1515
1561
  }
1516
1562
  setDim(g, false);
1517
- placeOnSelf(g, desired);
1563
+ // A group moved by hand takes the cell under the hand and PUSHES what
1564
+ // stands in its way (0.4.44) — and the zone walk reads the containers
1565
+ // it pushed at REST, so the hand still finds their zones where they were.
1566
+ placeOnSelf(g, desired, g.subject === 'group');
1518
1567
  }
1519
1568
  else {
1520
1569
  // -- outside every board ------------------------------------------
@@ -1522,6 +1571,10 @@ export function bindDashboardGrid(api, group, options = {}) {
1522
1571
  setDim(g, true);
1523
1572
  }
1524
1573
  syncPlaceholder();
1574
+ // A carried group's slab and frame are this board's chrome, but its frame
1575
+ // is written by whichever board holds it now: re-sync so they follow.
1576
+ if (g.subject === 'group')
1577
+ syncSlabs();
1525
1578
  };
1526
1579
  const onToolMove = (ev) => {
1527
1580
  var _a, _b, _c;
@@ -1806,8 +1859,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1806
1859
  const anchorBottom = liveRect ? liveRect.y + liveRect.height : bottom;
1807
1860
  const px = E.w ? anchorRight - w : anchorLeft;
1808
1861
  const py = E.n ? anchorBottom - h : anchorTop;
1809
- g.node.setSize(w, h, (_c = g.node.size.depth) !== null && _c !== void 0 ? _c : 0);
1810
- g.node.setPosition(px, py);
1862
+ nodeOf(g).setSize(w, h, (_c = nodeOf(g).size.depth) !== null && _c !== void 0 ? _c : 0);
1863
+ nodeOf(g).setPosition(px, py);
1811
1864
  ghostStyleFastPath(g, { x: px, y: py, width: w, height: h });
1812
1865
  const spanF = bound() !== undefined ? frame() : f;
1813
1866
  const spanG = bound() !== undefined ? geom() : gg;
@@ -1847,7 +1900,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1847
1900
  syncPlaceholder();
1848
1901
  };
1849
1902
  const onToolUp = () => {
1850
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1903
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
1851
1904
  const g = gesture;
1852
1905
  if (!g || g.kind === 'palette')
1853
1906
  return;
@@ -1867,8 +1920,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1867
1920
  const displaced = tileCommands(deltasSince(g.startCells, g.startGeom, g.id));
1868
1921
  const snap = g.startGeom.get(g.id);
1869
1922
  if (snap) {
1870
- g.node.setPosition(snap.pos.x, snap.pos.y);
1871
- g.node.setSize(snap.size.width, snap.size.height, (_a = snap.size.depth) !== null && _a !== void 0 ? _a : 0);
1923
+ nodeOf(g).setPosition(snap.pos.x, snap.pos.y);
1924
+ nodeOf(g).setSize(snap.size.width, snap.size.height, (_a = snap.size.depth) !== null && _a !== void 0 ? _a : 0);
1872
1925
  }
1873
1926
  const cmds = options.tabDrop.dropIntoStrip(g.id, target.containerId, target.index, group.id, displaced);
1874
1927
  if (cmds.length === 0) {
@@ -1898,8 +1951,12 @@ export function bindDashboardGrid(api, group, options = {}) {
1898
1951
  try {
1899
1952
  diagram.runSystemWrite(() => {
1900
1953
  var _a;
1901
- g.node.setPosition(fin.rect.x, fin.rect.y);
1902
- g.node.setSize(fin.rect.width, fin.rect.height, (_a = g.node.size.depth) !== null && _a !== void 0 ? _a : 0);
1954
+ if (g.subject === 'node') {
1955
+ nodeOf(g).setPosition(fin.rect.x, fin.rect.y);
1956
+ nodeOf(g).setSize(fin.rect.width, fin.rect.height, (_a = nodeOf(g).size.depth) !== null && _a !== void 0 ? _a : 0);
1957
+ }
1958
+ else
1959
+ g.entity.setFrame(Object.assign({}, fin.rect));
1903
1960
  });
1904
1961
  }
1905
1962
  finally {
@@ -1908,6 +1965,27 @@ export function bindDashboardGrid(api, group, options = {}) {
1908
1965
  const sourceDisplaced = tileCommands(deltasSince(g.startCells, g.startGeom, g.id));
1909
1966
  const before = g.startCells.get(g.id);
1910
1967
  const geomBefore = g.startGeom.get(g.id);
1968
+ // A GROUP crossing boards: its cell-and-frame command, and the host's
1969
+ // bookkeeping for a container that changed boards (the spec entry, the
1970
+ // registry) rides along through `onMemberMoving` (tile first, 4b-ii).
1971
+ const own = g.subject === 'group'
1972
+ ? [
1973
+ new SetGroupCellCommand(g.id, before !== null && before !== void 0 ? before : fin.cell, fin.cell, geomBefore ? { x: geomBefore.pos.x, y: geomBefore.pos.y, width: geomBefore.size.width, height: geomBefore.size.height } : fin.rect, fin.rect),
1974
+ ...((_d = (_c = options.onMemberMoving) === null || _c === void 0 ? void 0 : _c.call(options, g.id, group.id, targetGroupId)) !== null && _d !== void 0 ? _d : []),
1975
+ ]
1976
+ : buildCommitCommands([
1977
+ {
1978
+ id: g.id,
1979
+ locked: false,
1980
+ isGroup: false,
1981
+ cellBefore: before !== null && before !== void 0 ? before : fin.cell,
1982
+ cellAfter: fin.cell,
1983
+ posBefore: (_e = geomBefore === null || geomBefore === void 0 ? void 0 : geomBefore.pos) !== null && _e !== void 0 ? _e : { x: fin.rect.x, y: fin.rect.y },
1984
+ posAfter: { x: fin.rect.x, y: fin.rect.y },
1985
+ sizeBefore: (_f = geomBefore === null || geomBefore === void 0 ? void 0 : geomBefore.size) !== null && _f !== void 0 ? _f : { width: fin.rect.width, height: fin.rect.height },
1986
+ sizeAfter: { width: fin.rect.width, height: fin.rect.height },
1987
+ },
1988
+ ]);
1911
1989
  const crossing = [
1912
1990
  ...sourceDisplaced,
1913
1991
  // …and the TARGET board's own displaced tiles. Dropping onto an
@@ -1920,24 +1998,12 @@ export function bindDashboardGrid(api, group, options = {}) {
1920
1998
  ...fin.commands,
1921
1999
  new RemoveFromGroupCommand(group.id, g.id),
1922
2000
  new AddToGroupCommand(targetGroupId, g.id),
1923
- ...buildCommitCommands([
1924
- {
1925
- id: g.id,
1926
- locked: false,
1927
- isGroup: false,
1928
- cellBefore: before !== null && before !== void 0 ? before : fin.cell,
1929
- cellAfter: fin.cell,
1930
- posBefore: (_c = geomBefore === null || geomBefore === void 0 ? void 0 : geomBefore.pos) !== null && _c !== void 0 ? _c : { x: fin.rect.x, y: fin.rect.y },
1931
- posAfter: { x: fin.rect.x, y: fin.rect.y },
1932
- sizeBefore: (_d = geomBefore === null || geomBefore === void 0 ? void 0 : geomBefore.size) !== null && _d !== void 0 ? _d : { width: fin.rect.width, height: fin.rect.height },
1933
- sizeAfter: { width: fin.rect.width, height: fin.rect.height },
1934
- },
1935
- ]),
2001
+ ...own,
1936
2002
  ];
1937
2003
  // …and whatever follows a member out of this board: an emptied page
1938
2004
  // closes — in which case the membership commands ride INSIDE one
1939
2005
  // sequence with it, or the batch could never undo (its group is gone).
1940
- const leaving = (_f = (_e = options.onMemberLeaving) === null || _e === void 0 ? void 0 : _e.call(options, g.id)) !== null && _f !== void 0 ? _f : [];
2006
+ const leaving = (_h = (_g = options.onMemberLeaving) === null || _g === void 0 ? void 0 : _g.call(options, g.id)) !== null && _h !== void 0 ? _h : [];
1941
2007
  execute('Move widget', leaving.length > 0 ? [new SequenceCommand('Move widget', [...crossing, ...leaving])] : crossing);
1942
2008
  engine.endGesture();
1943
2009
  cleanupGestureVisuals(g);
@@ -1945,10 +2011,10 @@ export function bindDashboardGrid(api, group, options = {}) {
1945
2011
  enforceBoardHeight();
1946
2012
  persistLayouts();
1947
2013
  api.renderNow();
1948
- (_g = options.onGesture) === null || _g === void 0 ? void 0 : _g.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed: true });
2014
+ (_j = options.onGesture) === null || _j === void 0 ? void 0 : _j.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed: true });
1949
2015
  return;
1950
2016
  }
1951
- if (g.removedFromBoard && dragOut === 'cancel') {
2017
+ if (g.removedFromBoard && (dragOut === 'cancel' || g.subject === 'group')) {
1952
2018
  // Released outside every board on a snap-home board: full restore,
1953
2019
  // nothing committed (the parked-outside release).
1954
2020
  cancelActiveGesture();
@@ -1975,16 +2041,16 @@ export function bindDashboardGrid(api, group, options = {}) {
1975
2041
  if (snap) {
1976
2042
  // Park the node on its start rect so the page's RemoveNodeCommand
1977
2043
  // captures sane geometry for undo.
1978
- g.node.setPosition(snap.pos.x, snap.pos.y);
1979
- g.node.setSize(snap.size.width, snap.size.height, (_h = snap.size.depth) !== null && _h !== void 0 ? _h : 0);
2044
+ nodeOf(g).setPosition(snap.pos.x, snap.pos.y);
2045
+ nodeOf(g).setSize(snap.size.width, snap.size.height, (_k = snap.size.depth) !== null && _k !== void 0 ? _k : 0);
1980
2046
  }
1981
2047
  engine.endGesture();
1982
2048
  cleanupGestureVisuals(g);
1983
2049
  gesture = null;
1984
2050
  enforceBoardHeight();
1985
2051
  api.renderNow();
1986
- void ((_j = options.onRemoveRequest) === null || _j === void 0 ? void 0 : _j.call(options, g.id, displaced));
1987
- (_k = options.onGesture) === null || _k === void 0 ? void 0 : _k.call(options, { type: 'remove', kind: g.kind, nodeId: g.id, changed: true });
2052
+ void ((_l = options.onRemoveRequest) === null || _l === void 0 ? void 0 : _l.call(options, g.id, displaced));
2053
+ (_m = options.onGesture) === null || _m === void 0 ? void 0 : _m.call(options, { type: 'remove', kind: g.kind, nodeId: g.id, changed: true });
1988
2054
  return;
1989
2055
  }
1990
2056
  commitGesture(g);
@@ -2062,6 +2128,27 @@ export function bindDashboardGrid(api, group, options = {}) {
2062
2128
  placeNear(g.id, cell.x, cell.y, g.spans.w, pushSolid);
2063
2129
  project();
2064
2130
  }
2131
+ else if (g.subject === 'group') {
2132
+ // The cell under the pointer, else the nearest legal one ALONG ITS ROW
2133
+ // (a section dragged left that wandered down its column read as the
2134
+ // wrong tile moving); when even that fails the wanted cell is painted
2135
+ // as refused (identification round, D6/D7).
2136
+ const it = engine.getItem(g.id);
2137
+ if (!it)
2138
+ return;
2139
+ const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), it.w);
2140
+ if (cell.x === it.x && cell.y === it.y)
2141
+ return;
2142
+ const was = { x: it.x, y: it.y };
2143
+ 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);
2144
+ if (moved) {
2145
+ project();
2146
+ setCarried(g.id, true); // chrome repainted by the projection is carried too
2147
+ }
2148
+ const now = engine.getItem(g.id);
2149
+ const stuck = !!now && now.x === was.x && now.y === was.y;
2150
+ showRefusal(stuck ? cell : null, it.w, it.h);
2151
+ }
2065
2152
  else {
2066
2153
  const spanW = (_b = (_a = engine.getItem(g.id)) === null || _a === void 0 ? void 0 : _a.w) !== null && _b !== void 0 ? _b : g.spans.w;
2067
2154
  const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), spanW);
@@ -2073,24 +2160,65 @@ export function bindDashboardGrid(api, group, options = {}) {
2073
2160
  const resolveTileZone = (g, ev) => {
2074
2161
  var _a, _b;
2075
2162
  let strip = null;
2076
- if (options.tabDrop && !isStatic && g.kind !== 'palette') {
2163
+ if (options.tabDrop && !isStatic && g.kind !== 'palette' && g.subject === 'node') {
2164
+ // A group never becomes a tab: over a container's strip it is over the
2165
+ // container's margin — a cell on the parent board, pushing with intent.
2077
2166
  const crect = api.container.getBoundingClientRect();
2078
2167
  strip = options.tabDrop.stripAt(crect.left + ev.screen.x, crect.top + ev.screen.y);
2079
2168
  }
2080
- return resolveZone({
2081
- x: ev.world.x,
2082
- y: ev.world.y,
2083
- roots: zoneRoots(),
2084
- strip,
2085
- prev: beside
2169
+ return resolveZone(Object.assign(Object.assign({ x: ev.world.x, y: ev.world.y, roots: zoneRoots(), strip, prev: beside
2086
2170
  ? { 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()) }
2087
- : ((_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
2088
- maxDepth: nesting,
2089
- ghostDepth: 0,
2090
- ghostSubtree: EMPTY_SUBTREE,
2091
- gap,
2092
- homeChain: homeChain(),
2093
- });
2171
+ : ((_b = (_a = g.leg) === null || _a === void 0 ? void 0 : _a.adopted.besideState()) !== null && _b !== void 0 ? _b : null), maxDepth: nesting, ghostDepth: g.subject === 'group' ? 1 + levelsInside(g.id) : 0 }, (g.subject === 'group' ? { restFrames: restFramesOf(g) } : {})), { ghostSubtree: g.subject === 'group' ? descendantGroups(g.id) : EMPTY_SUBTREE, gap, homeChain: homeChain() }));
2172
+ };
2173
+ /** This board's containers as they stood at the press: a group ghost's intent pushes never change what the hand means. */
2174
+ const restFramesOf = (g) => {
2175
+ const out = new Map();
2176
+ for (const [id, snap] of g.startGeom) {
2177
+ if (id === g.id || !isGroupMember(id))
2178
+ continue;
2179
+ out.set(id, { x: snap.pos.x, y: snap.pos.y, width: snap.size.width, height: snap.size.height });
2180
+ }
2181
+ return out;
2182
+ };
2183
+ /** Every group under `id`, itself included — the boards a group ghost may never enter. */
2184
+ const descendantGroups = (id) => {
2185
+ var _a;
2186
+ const out = new Set();
2187
+ const queue = [id];
2188
+ while (queue.length) {
2189
+ const cur = queue.shift();
2190
+ if (out.has(cur))
2191
+ continue;
2192
+ const grp = diagram.getGroup(cur);
2193
+ if (!grp)
2194
+ continue;
2195
+ out.add(cur);
2196
+ for (const m of (_a = grp.members) !== null && _a !== void 0 ? _a : [])
2197
+ if (diagram.getGroup(m))
2198
+ queue.push(m);
2199
+ }
2200
+ return out;
2201
+ };
2202
+ /**
2203
+ * The container LEVELS a group carries INSIDE it: 0 for a section of
2204
+ * widgets or a tab container of plain pages (a container and its page are
2205
+ * one level), 1 for a section holding a section, and so on. The group's own
2206
+ * level is the +1 above: its widgets sit one board deeper than the board it
2207
+ * lands on. The nesting policy counts both.
2208
+ */
2209
+ const levelsInside = (id) => {
2210
+ var _a;
2211
+ const grp = diagram.getGroup(id);
2212
+ if (!grp)
2213
+ return 0;
2214
+ const tabs = isTabsGroup(grp);
2215
+ let deepest = 0;
2216
+ for (const m of (_a = grp.members) !== null && _a !== void 0 ? _a : []) {
2217
+ if (!diagram.getGroup(m))
2218
+ continue;
2219
+ deepest = Math.max(deepest, tabs ? levelsInside(m) : 1 + levelsInside(m));
2220
+ }
2221
+ return deepest;
2094
2222
  };
2095
2223
  /** The groups this board sits in, all the way up: their bands never apply to a tile of this board. */
2096
2224
  const homeChain = () => {
@@ -2472,7 +2600,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2472
2600
  if (isStatic)
2473
2601
  return false;
2474
2602
  beginSlabMove(id, ev);
2475
- return (slabGesture === null || slabGesture === void 0 ? void 0 : slabGesture.id) === id;
2603
+ return (gesture === null || gesture === void 0 ? void 0 : gesture.id) === id;
2476
2604
  },
2477
2605
  dragMember: (id, ev) => {
2478
2606
  var _a;
@@ -2485,7 +2613,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2485
2613
  return { world, screen: { x: e.clientX - rect.left, y: e.clientY - rect.top }, source: e };
2486
2614
  };
2487
2615
  beginSlabMove(id, toTool(ev));
2488
- if (((_a = currentSlab()) === null || _a === void 0 ? void 0 : _a.id) !== id)
2616
+ if (((_a = currentGesture()) === null || _a === void 0 ? void 0 : _a.id) !== id)
2489
2617
  return false;
2490
2618
  const detachAll = () => {
2491
2619
  window.removeEventListener('pointermove', onMove, true);
@@ -2495,7 +2623,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2495
2623
  };
2496
2624
  const onMove = (e) => {
2497
2625
  var _a;
2498
- if (disposed || ((_a = currentSlab()) === null || _a === void 0 ? void 0 : _a.id) !== id)
2626
+ if (disposed || ((_a = currentGesture()) === null || _a === void 0 ? void 0 : _a.id) !== id)
2499
2627
  return detachAll();
2500
2628
  slabMove(toTool(e));
2501
2629
  api.render();
@@ -2765,7 +2893,8 @@ export function bindDashboardGrid(api, group, options = {}) {
2765
2893
  gesture = {
2766
2894
  kind: isResize ? 'resize' : 'move',
2767
2895
  id: node.id,
2768
- node,
2896
+ subject: 'node',
2897
+ entity: node,
2769
2898
  pointerId: typeof PointerEvent !== 'undefined' && ev.source instanceof PointerEvent
2770
2899
  ? ev.source.pointerId
2771
2900
  : null,
@@ -3475,7 +3604,8 @@ export function bindDashboardGrid(api, group, options = {}) {
3475
3604
  const g = {
3476
3605
  kind: 'palette',
3477
3606
  id: node.id,
3478
- node,
3607
+ subject: 'node',
3608
+ entity: node,
3479
3609
  pointerId: (_b = event.pointerId) !== null && _b !== void 0 ? _b : null,
3480
3610
  started: false,
3481
3611
  downClient: { x: event.clientX, y: event.clientY },
@@ -88,6 +88,16 @@ export interface ResolveInput {
88
88
  * be put beside the container it is still inside.
89
89
  */
90
90
  homeChain: ReadonlySet<string>;
91
+ /**
92
+ * Containers of the source board as they stood when the gesture began (a
93
+ * GROUP ghost pushes solids with intent, 0.4.44). The hand over a pushed
94
+ * container's rest frame still means that container — its zones, its
95
+ * page tested at the displacement — or a group could never enter a
96
+ * container its own body reaches before the hand does: the target fled.
97
+ * The engine's memory brings the pushed container home when the ghost
98
+ * leaves the board.
99
+ */
100
+ restFrames?: ReadonlyMap<string, ZoneRect>;
91
101
  }
92
102
  export type Zone = {
93
103
  kind: 'off';
@@ -95,7 +95,12 @@ export function resolve(input) {
95
95
  return { kind: 'beside', board, containerId: p.containerId, side: p.side, kept: true };
96
96
  }
97
97
  }
98
- const opaque = (board, c) => c.static || input.ghostSubtree.has(c.id) || board.depth + 1 + input.ghostDepth > input.maxDepth;
98
+ // A static container and a container too deep for the ghost are obstacles:
99
+ // the hand over them means a cell on their parent board. The GHOST'S OWN
100
+ // subtree is not an obstacle but glass: a container carried by hand has its
101
+ // frame under the pointer at every step, and reading it as a hit answered
102
+ // "plain on the parent" before the walk ever looked at the page beneath.
103
+ const opaque = (board, c) => c.static || board.depth + 1 + input.ghostDepth > input.maxDepth;
99
104
  // The container the hand holds is read at REST: its live frame is where the
100
105
  // beside pushed or shifted it, and a hand crossing from its top band to its
101
106
  // corner would otherwise find it gone from under the pointer (0.4.48's
@@ -103,11 +108,16 @@ export function resolve(input) {
103
108
  // same displacement.
104
109
  const held = input.prev;
105
110
  const descend = (board, dx, dy) => {
111
+ var _a;
106
112
  const px = x + dx;
107
113
  const py = y + dy;
108
114
  for (const c of board.children()) {
109
- // the held container at rest — unless the hand has left that frame for the live one
110
- const atRest = held && c.id === held.containerId && inRect(held.frame0, x, y) ? held.frame0 : null;
115
+ if (input.ghostSubtree.has(c.id))
116
+ continue; // the carried container itself, and the boards inside it: glass
117
+ // the held container at rest — unless the hand has left that frame for the live one;
118
+ // and any container the ghost pushed, at rest the same way
119
+ const rest = (_a = input.restFrames) === null || _a === void 0 ? void 0 : _a.get(c.id);
120
+ const atRest = rest && inRect(rest, x, y) ? rest : held && c.id === held.containerId && inRect(held.frame0, x, y) ? held.frame0 : null;
111
121
  const frame = atRest !== null && atRest !== void 0 ? atRest : c.frame;
112
122
  const tx = atRest ? x : px;
113
123
  const ty = atRest ? y : py;