@grafloria/element 0.4.68 → 0.4.70

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.68",
3
+ "version": "0.4.70",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -52,7 +52,7 @@
52
52
  import { Command, type DiagramModel, type GridColumnLayout, type GroupModel, type NodeModel } from '@grafloria/engine';
53
53
  import { type ToolPointerEvent } from '@grafloria/renderer';
54
54
  import { type CellRect, type WorldRect } from './grid-mapping.js';
55
- import { type BesideSide } from './zones.js';
55
+ import { type BesideSide, type ZoneBoard } from './zones.js';
56
56
  import { type ResizeEdges } from './edges.js';
57
57
  import { type DragHandleOption } from './grip.js';
58
58
  export { TEAR_OUT_MIN_ROWS } from './tear-out.js';
@@ -631,7 +631,7 @@ export interface AdoptedLeg {
631
631
  containerId: string;
632
632
  side: BesideSide;
633
633
  frame0: WorldRect;
634
- vacated: WorldRect;
634
+ vacated?: WorldRect;
635
635
  } | null;
636
636
  /**
637
637
  * Take the tile OFF the board while the pointer is somewhere the board is
@@ -683,6 +683,24 @@ export declare function parentPeerOf(container: HTMLElement, groupId: string): B
683
683
  export declare function clearOtherSelections(container: HTMLElement, self: BinderPeer | null): void;
684
684
  export declare function registerBoardPeer(container: HTMLElement, peer: BinderPeer): () => void;
685
685
  export type { BinderPeer };
686
+ /** The peers registered on a canvas (the set is created on first ask): the boards the zone walk sees. */
687
+ export declare function peersOnCanvasOf(container: HTMLElement): Set<BinderPeer>;
688
+ /** Client pixels per world unit on a canvas: a zoomed camera makes a 9 px stay fewer world units. */
689
+ export declare function clientPerWorldOf(api: Pick<DashboardGridApi, 'container' | 'viewport'>): {
690
+ x: number;
691
+ y: number;
692
+ };
693
+ /**
694
+ * The boards of a canvas as the zone walk sees them (tile first, step 2): a
695
+ * ROOT is a board whose group has no parent group (a view); a board's children
696
+ * are its member groups that are containers, each with the board a descent
697
+ * enters — a tab container's ACTIVE page, a section's own board — one level
698
+ * deeper. Built from the peers and the model on every move: a handful of
699
+ * groups, and the frames are live. One builder for the grid AND the split
700
+ * board (0.4.69): what a hand means over a tab group must not depend on which
701
+ * layout the board under it runs.
702
+ */
703
+ export declare function zoneRootsOf(peers: Iterable<BinderPeer>, diagram: DiagramModel): ZoneBoard[];
686
704
  /**
687
705
  * THE STRIP KEEPS THE HAND IT HAS (CSS px). A tab strip is ~30 px tall and
688
706
  * the zone right under it moves the whole container out of the way, so at the
@@ -56,7 +56,7 @@ import { buildCommitCommands, cellFromGridItem, cellToRect, columnUnitFor, gridI
56
56
  import { ensureDashboardKitStyles } from './styles.js';
57
57
  import { captionOfGroup, captionPassThrough, sectionCaptionReserve } from './caption.js';
58
58
  import { TAB_STRIP_HEIGHT } from './tabs.js';
59
- import { BESIDE_BAND, resolve as resolveZone, stripUnder } from './zones.js';
59
+ import { BESIDE_BAND, resolve as resolveZone, stripCrossing, stripUnder } from './zones.js';
60
60
  import { SequenceCommand, SetGroupCellCommand, tileCommands } from './commit.js';
61
61
  import { EDGE_GRACE } from './board-ctx.js';
62
62
  import { createProjection } from './project.js';
@@ -148,6 +148,84 @@ export function registerBoardPeer(container, peer) {
148
148
  s.delete(peer);
149
149
  };
150
150
  }
151
+ /** The peers registered on a canvas (the set is created on first ask): the boards the zone walk sees. */
152
+ export function peersOnCanvasOf(container) {
153
+ let set = BOARD_REGISTRY.get(container);
154
+ if (!set) {
155
+ set = new Set();
156
+ BOARD_REGISTRY.set(container, set);
157
+ }
158
+ return set;
159
+ }
160
+ /** Client pixels per world unit on a canvas: a zoomed camera makes a 9 px stay fewer world units. */
161
+ export function clientPerWorldOf(api) {
162
+ const rect = api.container.getBoundingClientRect();
163
+ const toWorld = (cx, cy) => { var _a; return ((_a = api.viewport) === null || _a === void 0 ? void 0 : _a.clientToWorld) ? api.viewport.clientToWorld(cx, cy, rect) : { x: cx - rect.left, y: cy - rect.top }; };
164
+ const o = toWorld(rect.left, rect.top);
165
+ const u = toWorld(rect.left + 100, rect.top + 100);
166
+ return { x: 100 / (u.x - o.x || 100), y: 100 / (u.y - o.y || 100) };
167
+ }
168
+ /**
169
+ * The boards of a canvas as the zone walk sees them (tile first, step 2): a
170
+ * ROOT is a board whose group has no parent group (a view); a board's children
171
+ * are its member groups that are containers, each with the board a descent
172
+ * enters — a tab container's ACTIVE page, a section's own board — one level
173
+ * deeper. Built from the peers and the model on every move: a handful of
174
+ * groups, and the frames are live. One builder for the grid AND the split
175
+ * board (0.4.69): what a hand means over a tab group must not depend on which
176
+ * layout the board under it runs.
177
+ */
178
+ export function zoneRootsOf(peers, diagram) {
179
+ const list = [...peers];
180
+ const byGroup = new Map(list.map((p) => [p.group.id, p]));
181
+ const frameOf = (grp) => { var _a, _b, _c, _d; return ({ x: grp.position.x, y: grp.position.y, width: (_b = (_a = grp.size) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : 0, height: (_d = (_c = grp.size) === null || _c === void 0 ? void 0 : _c.height) !== null && _d !== void 0 ? _d : 0 }); };
182
+ const boardRef = (p, depth) => ({
183
+ id: p.group.id,
184
+ depth,
185
+ ref: p,
186
+ contains: (x, y) => p.containsWorld(x, y),
187
+ containsExtended: (x, y) => p.containsWorldExtended(x, y),
188
+ children: () => {
189
+ var _a, _b, _c, _d, _e;
190
+ const out = [];
191
+ for (const id of (_a = p.group.members) !== null && _a !== void 0 ? _a : []) {
192
+ const grp = diagram.getGroup(id);
193
+ if (!grp || diagram.getNode(id))
194
+ continue;
195
+ const cw = ((_b = grp.getMetadata('containerWidget')) !== null && _b !== void 0 ? _b : {});
196
+ const layout = cw.layout === 'tabs' ? 'tabs' : cw.layout === 'split' ? 'split' : 'grid';
197
+ let innerPeer;
198
+ if (layout === 'tabs') {
199
+ const pageId = cw.active && byGroup.has(cw.active) ? cw.active : [...((_c = grp.members) !== null && _c !== void 0 ? _c : [])].find((m) => byGroup.has(m));
200
+ innerPeer = pageId ? byGroup.get(pageId) : undefined;
201
+ }
202
+ else
203
+ innerPeer = byGroup.get(id);
204
+ out.push({
205
+ id,
206
+ layout,
207
+ static: (_e = (_d = innerPeer === null || innerPeer === void 0 ? void 0 : innerPeer.isStatic) === null || _d === void 0 ? void 0 : _d.call(innerPeer)) !== null && _e !== void 0 ? _e : false,
208
+ frame: frameOf(grp),
209
+ stripHeight: layout === 'tabs' ? TAB_STRIP_HEIGHT : 0,
210
+ band: layout === 'tabs' ? BESIDE_BAND : 0, // a section's whole body is "into" (Quantia's Groups page)
211
+ // The top and bottom are a FIXED depth — one strip's worth, under
212
+ // the strip — not a fifth of the body, which grew with the panel
213
+ // until 216 px of the fluid demo's page meant "above the whole
214
+ // panel" (0.4.62). The sides keep the fifth.
215
+ bandY: layout === 'tabs' ? TAB_STRIP_HEIGHT : 0,
216
+ // …and the TOP band hangs ABOVE the frame, where a hand looking
217
+ // for "above this panel" actually goes (0.4.65). A panel holding
218
+ // the board's first row has no row above it to point at, and the
219
+ // lane under its header is the last place anyone would try.
220
+ topOutside: layout === 'tabs' ? TAB_STRIP_HEIGHT : 0,
221
+ inner: innerPeer ? boardRef(innerPeer, depth + 1) : null,
222
+ });
223
+ }
224
+ return out;
225
+ },
226
+ });
227
+ return list.filter((p) => !p.group.parentGroupId).map((p) => boardRef(p, 0));
228
+ }
151
229
  const DRAG_THRESHOLD = 4;
152
230
  /**
153
231
  * THE STRIP KEEPS THE HAND IT HAS (CSS px). A tab strip is ~30 px tall and
@@ -2081,72 +2159,9 @@ export function bindDashboardGrid(api, group, options = {}) {
2081
2159
  const f = frame();
2082
2160
  return f.width * boardVisualHeight();
2083
2161
  };
2084
- const peersOnCanvas = () => {
2085
- let set = BOARD_REGISTRY.get(api.container);
2086
- if (!set) {
2087
- set = new Set();
2088
- BOARD_REGISTRY.set(api.container, set);
2089
- }
2090
- return set;
2091
- };
2092
- /**
2093
- * The boards of this canvas as the zone walk sees them (tile first, step
2094
- * 2): a ROOT is a board whose group has no parent group (a view); a board's
2095
- * children are its member groups that are containers, each with the board a
2096
- * descent enters — a tab container's ACTIVE page, a section's own board —
2097
- * one level deeper. Built from the peers and the model on every move: a
2098
- * handful of groups, and the frames are live.
2099
- */
2100
- const zoneRoots = () => {
2101
- const peers = [...peersOnCanvas()];
2102
- const byGroup = new Map(peers.map((p) => [p.group.id, p]));
2103
- const boardRef = (p, depth) => ({
2104
- id: p.group.id,
2105
- depth,
2106
- ref: p,
2107
- contains: (x, y) => p.containsWorld(x, y),
2108
- containsExtended: (x, y) => p.containsWorldExtended(x, y),
2109
- children: () => {
2110
- var _a, _b, _c, _d, _e;
2111
- const out = [];
2112
- for (const id of (_a = p.group.members) !== null && _a !== void 0 ? _a : []) {
2113
- const grp = diagram.getGroup(id);
2114
- if (!grp || diagram.getNode(id))
2115
- continue;
2116
- const cw = ((_b = grp.getMetadata('containerWidget')) !== null && _b !== void 0 ? _b : {});
2117
- const layout = cw.layout === 'tabs' ? 'tabs' : cw.layout === 'split' ? 'split' : 'grid';
2118
- let innerPeer;
2119
- if (layout === 'tabs') {
2120
- const pageId = cw.active && byGroup.has(cw.active) ? cw.active : [...((_c = grp.members) !== null && _c !== void 0 ? _c : [])].find((m) => byGroup.has(m));
2121
- innerPeer = pageId ? byGroup.get(pageId) : undefined;
2122
- }
2123
- else
2124
- innerPeer = byGroup.get(id);
2125
- out.push({
2126
- id,
2127
- layout,
2128
- static: (_e = (_d = innerPeer === null || innerPeer === void 0 ? void 0 : innerPeer.isStatic) === null || _d === void 0 ? void 0 : _d.call(innerPeer)) !== null && _e !== void 0 ? _e : false,
2129
- frame: frameOfGroup(grp),
2130
- stripHeight: layout === 'tabs' ? TAB_STRIP_HEIGHT : 0,
2131
- band: layout === 'tabs' ? BESIDE_BAND : 0, // a section's whole body is "into" (Quantia's Groups page)
2132
- // The top and bottom are a FIXED depth — one strip's worth, under
2133
- // the strip — not a fifth of the body, which grew with the panel
2134
- // until 216 px of the fluid demo's page meant "above the whole
2135
- // panel" (0.4.62). The sides keep the fifth.
2136
- bandY: layout === 'tabs' ? TAB_STRIP_HEIGHT : 0,
2137
- // …and the TOP band hangs ABOVE the frame, where a hand looking
2138
- // for "above this panel" actually goes (0.4.65). A panel holding
2139
- // the board's first row has no row above it to point at, and the
2140
- // lane under its header is the last place anyone would try.
2141
- topOutside: layout === 'tabs' ? TAB_STRIP_HEIGHT : 0,
2142
- inner: innerPeer ? boardRef(innerPeer, depth + 1) : null,
2143
- });
2144
- }
2145
- return out;
2146
- },
2147
- });
2148
- return peers.filter((p) => !p.group.parentGroupId).map((p) => boardRef(p, 0));
2149
- };
2162
+ const peersOnCanvas = () => peersOnCanvasOf(api.container);
2163
+ /** The boards of this canvas as the zone walk sees them — see `zoneRootsOf`. */
2164
+ const zoneRoots = () => zoneRootsOf(peersOnCanvas(), diagram);
2150
2165
  /** 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. */
2151
2166
  const placeOnSelf = (g, desired, pushSolid = false) => {
2152
2167
  var _a, _b;
@@ -2198,7 +2213,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2198
2213
  };
2199
2214
  /** What the pointer means for the dragged tile: the resolve over the live tree, with the beside the hand holds. */
2200
2215
  const resolveTileZone = (g, ev) => {
2201
- var _a, _b, _c, _d, _e, _f, _g, _h;
2216
+ var _a, _b, _c, _d, _e, _f;
2202
2217
  const roots = zoneRoots();
2203
2218
  // THE CONTAINERS THIS GESTURE HAS MOVED, at the frames they rest in. A
2204
2219
  // drag's own pushes must never change what the hand means — settled for a
@@ -2212,44 +2227,8 @@ export function bindDashboardGrid(api, group, options = {}) {
2212
2227
  // A group never becomes a tab: over a container's strip it is over the
2213
2228
  // container's margin — a cell on the parent board, pushing with intent.
2214
2229
  const scaleY = clientPerWorld().y || 1;
2215
- let hit = stripUnder({ x: ev.world.x, y: ev.world.y, roots, held: (_c = (_b = g.strip) === null || _b === void 0 ? void 0 : _b.containerId) !== null && _c !== void 0 ? _c : null, stay: STRIP_STAY / scaleY, restFrames: rests });
2216
- if (!hit && prevWorld && !g.strip) {
2217
- // A HAND MOVES FASTER THAN A STRIP IS TALL. (Only for a hand ARRIVING:
2218
- // one already holding the strip leaves it by the stay, which reaches
2219
- // down and never up — the band above must stay reachable.) The strip is 30 px and a
2220
- // hand covers 40 to 80 px between events, so testing only where the
2221
- // pointer LANDS skips it — the user, coming down from above the panel:
2222
- // "it's not passing by the tab header, it drops directly to inside or
2223
- // outside." The segment it travelled is tested too, in steps of half a
2224
- // strip: crossing the rows and landing within one strip's height of
2225
- // them means the tabs. Flying far past them does not — a fast drag
2226
- // into the page must never snag on the header.
2227
- const dx = ev.world.x - prevWorld.x;
2228
- const dy = ev.world.y - prevWorld.y;
2229
- const n = Math.ceil(Math.hypot(dx, dy) / (TAB_STRIP_HEIGHT / 2));
2230
- let crossed = null;
2231
- for (let i = 1; i < n && !crossed; i++)
2232
- crossed = (_e = (_d = stripUnder({ x: prevWorld.x + (dx * i) / n, y: prevWorld.y + (dy * i) / n, roots, held: null, stay: 0, restFrames: rests })) === null || _d === void 0 ? void 0 : _d.containerId) !== null && _e !== void 0 ? _e : null;
2233
- if (crossed) {
2234
- const grp = diagram.getGroup(crossed);
2235
- const f = (_f = rests.get(crossed)) !== null && _f !== void 0 ? _f : (grp ? frameOfGroup(grp) : null);
2236
- if (f) {
2237
- const top = f.y;
2238
- const bottom = f.y + TAB_STRIP_HEIGHT;
2239
- // A CROSSING is the two events on OPPOSITE sides of the rows — a
2240
- // hand that swept along the tabs from beside the panel and ended
2241
- // above it did not cross them, it went past them. And the sides
2242
- // still take the corners (0.4.47): a hand landing in the outer
2243
- // fifth meant "after it", whatever it crossed on the way.
2244
- const through = (prevWorld.y < top && ev.world.y > bottom) || (prevWorld.y > bottom && ev.world.y < top);
2245
- const away = ev.world.y < top ? top - ev.world.y : ev.world.y > bottom ? ev.world.y - bottom : 0;
2246
- const rx = (ev.world.x - f.x) / Math.max(1, f.width);
2247
- const inSideBand = rx < BESIDE_BAND || rx > 1 - BESIDE_BAND;
2248
- if (through && !inSideBand && ev.world.x >= f.x && ev.world.x <= f.x + f.width && away <= TAB_STRIP_HEIGHT / scaleY)
2249
- hit = { containerId: crossed };
2250
- }
2251
- }
2252
- }
2230
+ // …and the segment the hand TRAVELLED, for a hand arriving faster than a strip is tall (0.4.67; `stripCrossing`).
2231
+ const hit = (_d = stripUnder({ x: ev.world.x, y: ev.world.y, roots, held: (_c = (_b = g.strip) === null || _b === void 0 ? void 0 : _b.containerId) !== null && _c !== void 0 ? _c : null, stay: STRIP_STAY / scaleY, restFrames: rests })) !== null && _d !== void 0 ? _d : (prevWorld && !g.strip ? stripCrossing({ prev: prevWorld, cur: ev.world, roots, restFrames: rests, stripHeight: TAB_STRIP_HEIGHT, band: BESIDE_BAND, reach: TAB_STRIP_HEIGHT / scaleY }) : null);
2253
2232
  if (hit) {
2254
2233
  // The SLOT is the painted strip's business — its tabs are laid out by
2255
2234
  // the browser. A container the gesture shifted sideways is painted
@@ -2270,7 +2249,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2270
2249
  strip,
2271
2250
  prev: beside
2272
2251
  ? { 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()) }
2273
- : ((_h = (_g = g.leg) === null || _g === void 0 ? void 0 : _g.adopted.besideState()) !== null && _h !== void 0 ? _h : null), // a beside another board holds for the ghost, through its leg
2252
+ : ((_f = (_e = g.leg) === null || _e === void 0 ? void 0 : _e.adopted.besideState()) !== null && _f !== void 0 ? _f : null), // a beside another board holds for the ghost, through its leg
2274
2253
  maxDepth: nesting,
2275
2254
  ghostDepth: g.subject === 'group' ? 1 + levelsInside(g.id) : 0, // a group's widgets sit one board deeper than wherever it lands
2276
2255
  restFrames: rests,
@@ -2330,13 +2309,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2330
2309
  return deepest;
2331
2310
  };
2332
2311
  /** Client pixels per world unit — the camera's scale, measured the way the tear-out measures its bands. */
2333
- const clientPerWorld = () => {
2334
- const rect = api.container.getBoundingClientRect();
2335
- const toWorld = (cx, cy) => { var _a; return ((_a = api.viewport) === null || _a === void 0 ? void 0 : _a.clientToWorld) ? api.viewport.clientToWorld(cx, cy, rect) : { x: cx - rect.left, y: cy - rect.top }; };
2336
- const o = toWorld(rect.left, rect.top);
2337
- const u = toWorld(rect.left + 100, rect.top + 100);
2338
- return { x: 100 / (u.x - o.x || 100), y: 100 / (u.y - o.y || 100) };
2339
- };
2312
+ const clientPerWorld = () => clientPerWorldOf(api);
2340
2313
  /** The groups this board sits in, all the way up: their bands never apply to a tile of this board. */
2341
2314
  const homeChain = () => {
2342
2315
  const out = new Set();
@@ -27,7 +27,7 @@ import type { DashboardGridApi, DashboardGridHandle, DashboardGridOptions } from
27
27
  import { type SplitNode } from './split-layout.js';
28
28
  /** Group metadata key the tree persists under. */
29
29
  export declare const SPLIT_TREE_KEY = "dashboardTree";
30
- export interface DashboardSplitOptions extends Pick<DashboardGridOptions, 'columns' | 'gap' | 'padding' | 'rtl' | 'fluid' | 'static' | 'dragHandle' | 'squeeze' | 'designHeight' | 'baseRowHeight' | 'dragOut' | 'removeZone' | 'onRemoveRequest' | 'onDropIn' | 'onGesture' | 'onSelect' | 'renderCaption' | 'onCaptionAction'> {
30
+ export interface DashboardSplitOptions extends Pick<DashboardGridOptions, 'columns' | 'gap' | 'padding' | 'rtl' | 'fluid' | 'static' | 'dragHandle' | 'squeeze' | 'designHeight' | 'baseRowHeight' | 'dragOut' | 'removeZone' | 'onRemoveRequest' | 'onDropIn' | 'tabDrop' | 'onMemberLeaving' | 'nesting' | 'onGesture' | 'onSelect' | 'renderCaption' | 'onCaptionAction'> {
31
31
  /** An authored tree. Default: the persisted one, else derived from the members' cells. */
32
32
  tree?: SplitNode | null;
33
33
  }
@@ -23,11 +23,13 @@
23
23
  * the authored size.
24
24
  */
25
25
  import { __awaiter } from "tslib";
26
- import { BESIDE_BAND, resolveTabZone } from './zones.js';
27
- import { BatchCommand, Command } from '@grafloria/engine';
26
+ import { BESIDE_BAND, resolveTabZone, resolve as resolveZone, stripCrossing, stripUnder } from './zones.js';
27
+ import { AddToGroupCommand, BatchCommand, Command, RemoveFromGroupCommand } from '@grafloria/engine';
28
28
  import { LiveRegionController, registerTool } from '@grafloria/renderer';
29
- import { anyEdge, clearOtherSelections, dragHandleSelector, gripHostOf, gripOf, normalizeDragHandle, ownsPress, parentPeerOf, pressOnDragHandle, registerBoardPeer, syncGrip, DRAG_HANDLE_CLASS, EDGE_GRIP } from './grid-binder.js';
30
- import { cellFromGridItem } from './grid-mapping.js';
29
+ import { anyEdge, clearOtherSelections, clientPerWorldOf, dragHandleSelector, gripHostOf, gripOf, normalizeDragHandle, ownsPress, parentPeerOf, peersOnCanvasOf, pressOnDragHandle, registerBoardPeer, syncGrip, zoneRootsOf, DRAG_HANDLE_CLASS, EDGE_GRIP, STRIP_STAY } from './grid-binder.js';
30
+ import { buildCommitCommands, cellFromGridItem } from './grid-mapping.js';
31
+ import { SequenceCommand } from './commit.js';
32
+ import { TAB_STRIP_HEIGHT } from './tabs.js';
31
33
  import { addSplitLeaf, cellsFromSplit, cloneSplit, dividersOf, groupRectsOf, insertSplitLeaf, moveSplitDivider, normalizeSplit, pathToLeaf, projectSplit, removeSplitLeaf, splitFromCells, splitLeaves, } from './split-layout.js';
32
34
  import { ensureDashboardKitStyles } from './styles.js';
33
35
  import { captionKey, captionOfGroup, captionPainted, paintCaptionBand, sectionCaptionReserve, sizeCaptionBand } from './caption.js';
@@ -509,6 +511,224 @@ export function bindDashboardSplit(api, group, options = {}) {
509
511
  const f = frame();
510
512
  return x >= f.x && x <= f.x + f.width && y >= f.y && y <= f.y + f.height;
511
513
  };
514
+ // -- the tab-group zones on a split board (0.4.69) ----------------------------
515
+ const frameOfGroupW = (grp) => { var _a, _b, _c, _d; return ({ x: grp.position.x, y: grp.position.y, width: (_b = (_a = grp.size) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : 0, height: (_d = (_c = grp.size) === null || _c === void 0 ? void 0 : _c.height) !== null && _d !== void 0 ? _d : 0 }); };
516
+ const EMPTY_SUBTREE = new Set();
517
+ /** This board's group and its ancestors: a container's band never applies to a widget that lives inside it. */
518
+ const homeChain = () => {
519
+ const out = new Set();
520
+ let cur = group;
521
+ for (let i = 0; cur && i < 32; i++) {
522
+ out.add(cur.id);
523
+ cur = cur.parentGroupId ? diagram.getGroup(cur.parentGroupId) : undefined;
524
+ }
525
+ return out;
526
+ };
527
+ /**
528
+ * The zone under the hand for a widget of this board — the SAME walk the
529
+ * grid board runs (zones.ts), over the same tree of boards: a strip slot
530
+ * (with the crossing rule for a hand arriving faster than a strip is
531
+ * tall), a band beside a container, a plain cell on the deepest board the
532
+ * pointer may enter, or off. Nothing on a split board is pushed, so no
533
+ * container is read at rest.
534
+ */
535
+ const tileZone = (g, ev, prev) => {
536
+ var _a, _b, _c, _d, _e, _f, _g, _h;
537
+ const roots = zoneRootsOf(peersOnCanvasOf(api.container), diagram);
538
+ let strip = null;
539
+ if (((_a = options.tabDrop) === null || _a === void 0 ? void 0 : _a.tabIndexAt) && !isStatic && g.kind === 'move' && g.node) {
540
+ const scaleY = clientPerWorldOf(api).y || 1;
541
+ const hit = (_d = stripUnder({ x: ev.world.x, y: ev.world.y, roots, held: (_c = (_b = g.strip) === null || _b === void 0 ? void 0 : _b.containerId) !== null && _c !== void 0 ? _c : null, stay: STRIP_STAY / scaleY })) !== null && _d !== void 0 ? _d : (prev && !g.strip ? stripCrossing({ prev, cur: ev.world, roots, stripHeight: TAB_STRIP_HEIGHT, band: BESIDE_BAND, reach: TAB_STRIP_HEIGHT / scaleY }) : null);
542
+ if (hit) {
543
+ const idx = options.tabDrop.tabIndexAt(hit.containerId, api.container.getBoundingClientRect().left + ev.screen.x);
544
+ if (idx !== null)
545
+ strip = { containerId: hit.containerId, index: idx };
546
+ }
547
+ }
548
+ return resolveZone({
549
+ x: ev.world.x,
550
+ y: ev.world.y,
551
+ roots,
552
+ strip,
553
+ prev: (_g = (_e = g.beside) !== null && _e !== void 0 ? _e : (_f = g.leg) === null || _f === void 0 ? void 0 : _f.adopted.besideState()) !== null && _g !== void 0 ? _g : null,
554
+ maxDepth: (_h = options.nesting) !== null && _h !== void 0 ? _h : 2,
555
+ ghostDepth: 0,
556
+ ghostSubtree: EMPTY_SUBTREE,
557
+ gap,
558
+ homeChain: homeChain(),
559
+ });
560
+ };
561
+ /** The widget's NATURAL size for a board that adopts it: its authored cell at this board's units, not the pane it was stretched over. */
562
+ const pxSizeOf = (g) => {
563
+ const cell = persistedCell(g.id);
564
+ const f = frame();
565
+ const colW = Math.max(1, (f.width - 2 * padding - (columns - 1) * gap) / columns);
566
+ if (cell)
567
+ return { width: Math.max(1, cell.w * (colW + gap) - gap), height: Math.max(1, cell.h * (baseRowHeight + gap) - gap) };
568
+ return g.node ? { width: g.node.size.width, height: g.node.size.height } : { width: colW, height: baseRowHeight };
569
+ };
570
+ const endStrip = (g) => {
571
+ var _a;
572
+ if (!g.strip)
573
+ return;
574
+ (_a = options.tabDrop) === null || _a === void 0 ? void 0 : _a.markDrop(null, null);
575
+ g.strip = null;
576
+ };
577
+ const endLeg = (g) => {
578
+ if (!g.leg)
579
+ return;
580
+ g.leg.adopted.abort();
581
+ g.leg = null;
582
+ };
583
+ /**
584
+ * What the zone means for the pane under the hand — the one step the widget
585
+ * drag and the palette drag share (0.4.70): a strip marks its slot (a
586
+ * widget's, never a chip's — the walk answers no strip for a chip); a band
587
+ * beside a container is a pane on that side, this board's line; a plain
588
+ * cell on a NESTED board, or on the board holding this one, hands the tile
589
+ * to that board through a leg (its own placeholder), a board that refuses
590
+ * leaving the split rule to answer; anything else is the nearest edge of
591
+ * the pane under the pointer. Answers the insertion target, or null.
592
+ */
593
+ const zoneTarget = (g, z, world, inside, px) => {
594
+ var _a, _b, _c;
595
+ if (z.kind === 'strip') {
596
+ endLeg(g);
597
+ g.beside = null;
598
+ if (!g.strip || g.strip.containerId !== z.containerId || g.strip.index !== z.index)
599
+ (_a = options.tabDrop) === null || _a === void 0 ? void 0 : _a.markDrop(z.containerId, z.index);
600
+ g.strip = { containerId: z.containerId, index: z.index };
601
+ return null;
602
+ }
603
+ endStrip(g);
604
+ const peer = z.kind === 'plain' ? ((_b = z.board.ref) !== null && _b !== void 0 ? _b : null) : null;
605
+ if (z.kind === 'beside') {
606
+ endLeg(g);
607
+ const grp = diagram.getGroup(z.containerId);
608
+ const rect = (_c = rectsOf(g.liveTree).get(z.containerId)) !== null && _c !== void 0 ? _c : (grp ? frameOfGroupW(grp) : null);
609
+ if (!rect)
610
+ return null;
611
+ g.beside = z.kept && g.beside ? g.beside : { containerId: z.containerId, side: z.side, frame0: rect };
612
+ return { id: z.containerId, side: z.side, rect };
613
+ }
614
+ g.beside = null;
615
+ if (peer && peer !== selfPeerRef) {
616
+ if (g.leg && g.leg.peer !== peer)
617
+ endLeg(g);
618
+ if (!g.leg) {
619
+ const adopted = peer.adopt({ id: g.id }, world, px, {});
620
+ if (adopted)
621
+ g.leg = { peer, adopted };
622
+ }
623
+ if (g.leg) {
624
+ g.leg.adopted.move(world);
625
+ return null;
626
+ }
627
+ }
628
+ else
629
+ endLeg(g);
630
+ return inside ? dropTargetAt(g.liveTree, world.x, world.y, g.id) : null;
631
+ };
632
+ // -- the split board as an adoption TARGET (0.4.70) ----------------------------
633
+ /**
634
+ * A tile from ANOTHER board — a widget leaving a page or a section, a
635
+ * palette chip — held over this board through a leg. Measured live on
636
+ * 0.4.69: a widget dragged out of the Filters page in split mode got no
637
+ * line and no placeholder anywhere, the ghost dimmed, every release
638
+ * snapped home — this peer's `adopt` answered null. The leg's promise is
639
+ * the insertion line on the nearest edge of the pane under the pointer;
640
+ * nothing moves until the release (the split model's rule); `finalize`
641
+ * answers the tree with the new pane, so the source's commit carries it. A
642
+ * beside on a container of this board is a pane on that side of it. An
643
+ * empty board takes the tile as its whole.
644
+ */
645
+ const adoptPane = (node, world, opts = {}) => {
646
+ if (disposed || isStatic)
647
+ return null;
648
+ const tree = readTree();
649
+ if (tree && splitLeaves(tree).includes(node.id))
650
+ return null; // a pane of this board never adopts itself
651
+ const whole = { path: [], side: 'top', rect: frame() };
652
+ const state = { target: null };
653
+ const targetAt = (w) => (!worldInsideBoard(w.x, w.y) ? null : tree ? dropTargetAt(tree, w.x, w.y) : whole);
654
+ const paint = () => {
655
+ showInsertion(state.target ? insertionRect(state.target.rect, state.target.side) : null);
656
+ api.render();
657
+ };
658
+ const after = () => {
659
+ if (!state.target)
660
+ return null;
661
+ if (!tree)
662
+ return normalizeSplit(addSplitLeaf(null, node.id, frame(), gap, padding));
663
+ const t = state.target;
664
+ const side = rtl && (t.side === 'left' || t.side === 'right') ? (t.side === 'left' ? 'right' : 'left') : t.side;
665
+ return normalizeSplit(insertSplitLeaf(tree, node.id, targetRef(t), side));
666
+ };
667
+ const cellOf = () => {
668
+ var _a;
669
+ const t = after();
670
+ return t ? ((_a = cellsFromSplit(t, columns, rowsGuess()).get(node.id)) !== null && _a !== void 0 ? _a : null) : null;
671
+ };
672
+ const rectOf = () => {
673
+ var _a;
674
+ const t = after();
675
+ return t ? ((_a = rectsOf(t).get(node.id)) !== null && _a !== void 0 ? _a : null) : null;
676
+ };
677
+ const besideOn = (containerId, side) => {
678
+ const rect = rectsOf(tree).get(containerId);
679
+ if (!rect)
680
+ return;
681
+ state.target = { id: containerId, side, rect };
682
+ paint();
683
+ };
684
+ if (opts.beside)
685
+ besideOn(opts.beside.containerId, opts.beside.side);
686
+ else {
687
+ state.target = targetAt(world);
688
+ paint();
689
+ }
690
+ return {
691
+ groupId: group.id,
692
+ move: (w) => {
693
+ state.target = targetAt(w);
694
+ paint();
695
+ },
696
+ beside: (containerId, side) => besideOn(containerId, side),
697
+ besideState: () => {
698
+ const t = state.target;
699
+ if (!t || !('id' in t) || !diagram.getGroup(t.id))
700
+ return null;
701
+ return { containerId: t.id, side: t.side, frame0: t.rect };
702
+ },
703
+ leave: () => {
704
+ state.target = null;
705
+ paint();
706
+ },
707
+ enter: (w) => {
708
+ state.target = targetAt(w);
709
+ paint();
710
+ },
711
+ place: () => false, // a pane is the tree's to size: no prescribed cell here
712
+ baseline: () => (tree ? cellsFromSplit(tree, columns, rowsGuess()) : new Map()),
713
+ cell: () => cellOf(),
714
+ rect: () => rectOf(),
715
+ abort: () => {
716
+ state.target = null;
717
+ showInsertion(null);
718
+ api.render();
719
+ },
720
+ finalize: () => {
721
+ const t = after();
722
+ const cell = cellOf();
723
+ const rect = rectOf();
724
+ state.target = null;
725
+ showInsertion(null);
726
+ if (!t || !cell || !rect)
727
+ return null;
728
+ return { commands: [new SetSplitTreeCommand(group.id, tree, t)], cell, rect };
729
+ },
730
+ };
731
+ };
512
732
  // -- a11y -------------------------------------------------------------------
513
733
  const nameOf = (id) => {
514
734
  var _a, _b, _c, _d;
@@ -543,6 +763,8 @@ export function bindDashboardSplit(api, group, options = {}) {
543
763
  (_a = options.onSelect) === null || _a === void 0 ? void 0 : _a.call(options, id);
544
764
  };
545
765
  let selfPeerRef = null;
766
+ /** The previous move event's world point: the segment a hand travelled, for a strip it stepped over. */
767
+ let prevWorld = null;
546
768
  // Static boards let content be clicked — see grid-binder's staticGuard.
547
769
  const staticGuard = (e) => {
548
770
  var _a, _b, _c, _d;
@@ -679,6 +901,7 @@ export function bindDashboardSplit(api, group, options = {}) {
679
901
  const beginMoveVisuals = (g) => {
680
902
  var _a;
681
903
  g.started = true;
904
+ prevWorld = null;
682
905
  g.liveTree = removeSplitLeaf(g.startTree, g.id); // the siblings take the slot at once
683
906
  g.hostEl = hostOf(g.id);
684
907
  (_a = g.hostEl) === null || _a === void 0 ? void 0 : _a.classList.add('axdb-ghost');
@@ -711,17 +934,29 @@ export function bindDashboardSplit(api, group, options = {}) {
711
934
  api.render();
712
935
  return;
713
936
  }
714
- // MOVE: the ghost follows the pointer; the target is the nearest edge under it.
937
+ // MOVE: the ghost follows the pointer. What the hand means is the ZONE
938
+ // WALK's answer first (0.4.69, the grid board's since 0.4.50): a tab
939
+ // container's strip makes the widget a tab, its page takes the widget
940
+ // through a leg, its outer fifth means a pane beside it — and only then
941
+ // the split board's own rule, the nearest edge of the pane under the
942
+ // pointer. Measured before this on the fluid demo in split mode: the
943
+ // header was never a tab target, the page never took a widget, and
944
+ // "above" claimed the upper half of the panel's body.
715
945
  if (g.node) {
716
946
  const x = ev.world.x - g.grab.dx;
717
947
  const y = ev.world.y - g.grab.dy;
718
948
  diagram.runSystemWrite(() => g.node.setPosition(x, y));
719
949
  }
720
950
  const inside = worldInsideBoard(ev.world.x, ev.world.y);
721
- const t = inside ? dropTargetAt(g.liveTree, ev.world.x, ev.world.y, g.id) : null;
951
+ const prev = prevWorld;
952
+ prevWorld = { x: ev.world.x, y: ev.world.y };
953
+ const t = zoneTarget(g, tileZone(g, ev, prev), ev.world, inside, pxSizeOf(g));
722
954
  g.target = t ? targetOf(t) : null;
723
955
  showInsertion(t ? insertionRect(t.rect, t.side) : null);
724
956
  const out = !inside &&
957
+ !t &&
958
+ !g.strip &&
959
+ !g.leg &&
725
960
  options.dragOut === 'remove' &&
726
961
  (!options.removeZone || options.removeZone({ x: ev.screen.x, y: ev.screen.y }, { x: ev.world.x, y: ev.world.y }));
727
962
  g.out = out;
@@ -729,6 +964,7 @@ export function bindDashboardSplit(api, group, options = {}) {
729
964
  api.render();
730
965
  };
731
966
  const onToolUp = () => {
967
+ var _a, _b, _c, _d;
732
968
  const g = gesture;
733
969
  if (!g || g.kind === 'palette')
734
970
  return;
@@ -747,6 +983,89 @@ export function bindDashboardSplit(api, group, options = {}) {
747
983
  fire({ type: changed ? 'commit' : 'cancel', kind: 'resize', nodeId: g.id, changed });
748
984
  return;
749
985
  }
986
+ const paint = () => {
987
+ if (disposed)
988
+ return;
989
+ project(readTree());
990
+ api.renderNow();
991
+ };
992
+ const settle = () => {
993
+ paint();
994
+ void Promise.resolve(pendingBatch).then(paint, () => undefined);
995
+ };
996
+ if (g.strip && options.tabDrop) {
997
+ // -- INTO A STRIP: the widget becomes a new tab of that container and
998
+ // its pane leaves this tree — the tree-without-it rides as the source's
999
+ // "displaced", the way a grid's survivors do (0.4.69). One history step.
1000
+ const target = g.strip;
1001
+ g.strip = null;
1002
+ options.tabDrop.markDrop(null, null);
1003
+ const without = new SetSplitTreeCommand(group.id, g.startTree, normalizeSplit(g.liveTree));
1004
+ const cmds = options.tabDrop.dropIntoStrip(g.id, target.containerId, target.index, group.id, [without]);
1005
+ if (cmds.length === 0) {
1006
+ project(g.startTree);
1007
+ api.renderNow();
1008
+ fire({ type: 'cancel', kind: 'move', nodeId: g.id, changed: false });
1009
+ return;
1010
+ }
1011
+ execute('Move widget into a new tab', cmds);
1012
+ settle();
1013
+ live.announce(`${nameOf(g.id)} became a tab of ${nameOf(target.containerId)}`, 'polite', true);
1014
+ fire({ type: 'commit', kind: 'move', nodeId: g.id, changed: true });
1015
+ return;
1016
+ }
1017
+ if (g.leg) {
1018
+ // -- ONTO ANOTHER BOARD (a page, a section, the board holding this one):
1019
+ // one batch across both — this tree without the pane, the target's
1020
+ // displaced tiles, the membership, the widget's cell and frame there.
1021
+ const leg = g.leg;
1022
+ g.leg = null;
1023
+ const fin = leg.adopted.finalize();
1024
+ if (!fin) {
1025
+ project(g.startTree);
1026
+ api.renderNow();
1027
+ fire({ type: 'cancel', kind: 'move', nodeId: g.id, changed: false });
1028
+ return;
1029
+ }
1030
+ const node = g.node;
1031
+ if (node) {
1032
+ diagram.runSystemWrite(() => {
1033
+ var _a;
1034
+ node.setPosition(fin.rect.x, fin.rect.y);
1035
+ node.setSize(fin.rect.width, fin.rect.height, (_a = node.size.depth) !== null && _a !== void 0 ? _a : 0);
1036
+ });
1037
+ }
1038
+ const geom0 = (_a = g.startGeom) !== null && _a !== void 0 ? _a : { pos: { x: fin.rect.x, y: fin.rect.y }, size: { width: fin.rect.width, height: fin.rect.height } };
1039
+ const own = buildCommitCommands([
1040
+ {
1041
+ id: g.id,
1042
+ locked: false,
1043
+ isGroup: false,
1044
+ cellBefore: (_b = persistedCell(g.id)) !== null && _b !== void 0 ? _b : fin.cell,
1045
+ cellAfter: fin.cell,
1046
+ posBefore: geom0.pos,
1047
+ posAfter: { x: fin.rect.x, y: fin.rect.y },
1048
+ sizeBefore: geom0.size,
1049
+ sizeAfter: { width: fin.rect.width, height: fin.rect.height },
1050
+ },
1051
+ ]);
1052
+ const crossing = [
1053
+ new SetSplitTreeCommand(group.id, g.startTree, normalizeSplit(g.liveTree)),
1054
+ ...fin.commands,
1055
+ new RemoveFromGroupCommand(group.id, g.id),
1056
+ new AddToGroupCommand(leg.adopted.groupId, g.id),
1057
+ ...own,
1058
+ ];
1059
+ // …and whatever follows a member out of this board: an emptied split
1060
+ // page closes — inside one sequence with the move, or the batch could
1061
+ // never undo (its group is gone).
1062
+ const leaving = (_d = (_c = options.onMemberLeaving) === null || _c === void 0 ? void 0 : _c.call(options, g.id)) !== null && _d !== void 0 ? _d : [];
1063
+ execute('Move widget', leaving.length > 0 ? [new SequenceCommand('Move widget', [...crossing, ...leaving])] : crossing);
1064
+ settle();
1065
+ live.announce(`${nameOf(g.id)} moved into ${nameOf(leg.adopted.groupId)}`, 'polite', true);
1066
+ fire({ type: 'commit', kind: 'move', nodeId: g.id, changed: true });
1067
+ return;
1068
+ }
750
1069
  if (g.out && options.onRemoveRequest) {
751
1070
  const without = g.liveTree;
752
1071
  void options.onRemoveRequest(g.id, [new SetSplitTreeCommand(group.id, g.startTree, normalizeSplit(without))]);
@@ -779,6 +1098,8 @@ export function bindDashboardSplit(api, group, options = {}) {
779
1098
  return;
780
1099
  gesture = null;
781
1100
  teardownGesture(g);
1101
+ endStrip(g);
1102
+ endLeg(g);
782
1103
  if (g.kind === 'palette') {
783
1104
  api.renderNow();
784
1105
  fire({ type: 'cancel', kind: 'palette', nodeId: g.id, changed: false });
@@ -835,6 +1156,10 @@ export function bindDashboardSplit(api, group, options = {}) {
835
1156
  out: false,
836
1157
  chip: null,
837
1158
  esc: null,
1159
+ strip: null,
1160
+ leg: null,
1161
+ beside: null,
1162
+ startGeom: null,
838
1163
  hostEl: null,
839
1164
  pointerId: typeof PointerEvent !== 'undefined' && ev.source instanceof PointerEvent ? ev.source.pointerId : null,
840
1165
  };
@@ -934,6 +1259,10 @@ export function bindDashboardSplit(api, group, options = {}) {
934
1259
  out: false,
935
1260
  chip: null,
936
1261
  esc: null,
1262
+ strip: null,
1263
+ leg: null,
1264
+ beside: null,
1265
+ startGeom: { pos: { x: node.position.x, y: node.position.y }, size: { width: node.size.width, height: node.size.height } },
937
1266
  hostEl: null,
938
1267
  pointerId: typeof PointerEvent !== 'undefined' && ev.source instanceof PointerEvent ? ev.source.pointerId : null,
939
1268
  };
@@ -1314,6 +1643,10 @@ export function bindDashboardSplit(api, group, options = {}) {
1314
1643
  out: false,
1315
1644
  chip,
1316
1645
  esc: null,
1646
+ strip: null,
1647
+ leg: null,
1648
+ beside: null,
1649
+ startGeom: null,
1317
1650
  hostEl: null,
1318
1651
  pointerId: null,
1319
1652
  };
@@ -1323,6 +1656,11 @@ export function bindDashboardSplit(api, group, options = {}) {
1323
1656
  window.removeEventListener('pointermove', onMove, true);
1324
1657
  window.removeEventListener('pointerup', onUp, true);
1325
1658
  };
1659
+ // The chip's size in this board's units — what a page or a section that
1660
+ // adopts it makes its tile from (the grid board does the same, 0.4.53).
1661
+ const f0 = frame();
1662
+ const colW0 = Math.max(1, (f0.width - 2 * padding - (columns - 1) * gap) / columns);
1663
+ const chipPx = { width: Math.max(1, spec.w * (colW0 + gap) - gap), height: Math.max(1, spec.h * (baseRowHeight + gap) - gap) };
1326
1664
  const onMove = (e) => {
1327
1665
  if (gesture !== g)
1328
1666
  return detach();
@@ -1331,19 +1669,39 @@ export function bindDashboardSplit(api, group, options = {}) {
1331
1669
  chip.style.top = `${e.clientY + 6}px`;
1332
1670
  }
1333
1671
  const w = toWorld(e.clientX, e.clientY);
1334
- const t = worldInsideBoard(w.x, w.y) ? dropTargetAt(g.liveTree, w.x, w.y) : null;
1672
+ const inside = worldInsideBoard(w.x, w.y);
1673
+ // Through the zones (0.4.70): a page or a section takes the chip through
1674
+ // a leg and shows its own placeholder; a container's band means a pane
1675
+ // beside it; a chip never becomes a tab (the grid's rule too).
1676
+ const ev = { type: 'move', world: w, screen: { x: e.clientX, y: e.clientY }, modifiers: { shift: false, ctrl: false, alt: false, meta: false } };
1677
+ const t = zoneTarget(g, tileZone(g, ev, null), w, inside, chipPx);
1335
1678
  g.target = t ? targetOf(t) : null;
1336
1679
  showInsertion(t ? insertionRect(t.rect, t.side) : null);
1337
- chip === null || chip === void 0 ? void 0 : chip.classList.toggle('axdb-out', !t && !!g.liveTree);
1680
+ chip === null || chip === void 0 ? void 0 : chip.classList.toggle('axdb-out', !t && !g.leg && !!g.liveTree);
1338
1681
  api.render();
1339
1682
  };
1340
1683
  const onUp = () => {
1341
- var _a, _b;
1684
+ var _a, _b, _c;
1342
1685
  detach();
1343
1686
  if (gesture !== g)
1344
1687
  return;
1345
1688
  gesture = null;
1346
1689
  teardownGesture(g);
1690
+ if (g.leg) {
1691
+ // Dropped into another board (a page, a section): its leg closes with
1692
+ // that board's displaced tiles; this tree is untouched.
1693
+ const leg = g.leg;
1694
+ g.leg = null;
1695
+ const fin = leg.adopted.finalize();
1696
+ if (!fin) {
1697
+ api.renderNow();
1698
+ fire({ type: 'cancel', kind: 'palette', nodeId: node.id, changed: false });
1699
+ return;
1700
+ }
1701
+ void ((_a = options.onDropIn) === null || _a === void 0 ? void 0 : _a.call(options, node, fin.cell, fin.commands, { boardId: leg.adopted.groupId }));
1702
+ fire({ type: 'drop-in', kind: 'palette', nodeId: node.id, changed: true });
1703
+ return;
1704
+ }
1347
1705
  const tree = g.liveTree;
1348
1706
  const after = g.target
1349
1707
  ? insertSplitLeaf(tree, node.id, targetRef(g.target), g.target.side)
@@ -1356,9 +1714,9 @@ export function bindDashboardSplit(api, group, options = {}) {
1356
1714
  return;
1357
1715
  }
1358
1716
  const cells = cellsFromSplit(after, columns, rowsGuess());
1359
- const cell = (_a = cells.get(node.id)) !== null && _a !== void 0 ? _a : { x: 0, y: 0, w: Math.max(1, spec.w), h: Math.max(1, spec.h) };
1717
+ const cell = (_b = cells.get(node.id)) !== null && _b !== void 0 ? _b : { x: 0, y: 0, w: Math.max(1, spec.w), h: Math.max(1, spec.h) };
1360
1718
  const displaced = [new SetSplitTreeCommand(group.id, tree, normalizeSplit(after))];
1361
- void ((_b = options.onDropIn) === null || _b === void 0 ? void 0 : _b.call(options, node, cell, displaced, { boardId: group.id }));
1719
+ void ((_c = options.onDropIn) === null || _c === void 0 ? void 0 : _c.call(options, node, cell, displaced, { boardId: group.id }));
1362
1720
  fire({ type: 'drop-in', kind: 'palette', nodeId: node.id, changed: true });
1363
1721
  };
1364
1722
  window.addEventListener('pointermove', onMove, true);
@@ -1779,7 +2137,8 @@ export function bindDashboardSplit(api, group, options = {}) {
1779
2137
  const f = frame();
1780
2138
  return f.width * f.height;
1781
2139
  },
1782
- adopt: () => null,
2140
+ // A tile from another board lands as a PANE where the line shows (0.4.70) see adoptPane.
2141
+ adopt: (node, world, _px, opts) => adoptPane(node, world, opts),
1783
2142
  };
1784
2143
  const unregisterPeer = registerBoardPeer(api.container, selfPeer);
1785
2144
  selfPeerRef = selfPeer;
@@ -190,6 +190,44 @@ export interface StripProbe {
190
190
  export declare function stripUnder(p: StripProbe): {
191
191
  containerId: string;
192
192
  } | null;
193
+ export interface CrossingProbe {
194
+ /** The previous pointer event's point and this one's, in world units. */
195
+ prev: {
196
+ x: number;
197
+ y: number;
198
+ };
199
+ cur: {
200
+ x: number;
201
+ y: number;
202
+ };
203
+ roots: ZoneBoard[];
204
+ /** Containers the gesture has displaced, at the frames they rest in. */
205
+ restFrames?: ReadonlyMap<string, ZoneRect>;
206
+ /** A strip's rows, in world units. */
207
+ stripHeight: number;
208
+ /** The outer fraction of a container that is its side bands: those keep their corners. Default BESIDE_BAND. */
209
+ band?: number;
210
+ /** How far past the rows a landing may be and still mean the tabs, in world units. Default: one strip's height. */
211
+ reach?: number;
212
+ }
213
+ /**
214
+ * A HAND MOVES FASTER THAN A STRIP IS TALL. The strip is 30 px and a hand
215
+ * covers 40 to 80 px between pointer events, so testing only where the pointer
216
+ * LANDS skips it — the user, coming down from above the panel: "it's not
217
+ * passing by the tab header, it drops directly to inside or outside." The
218
+ * segment the hand travelled is tested too, in steps of half a strip: a
219
+ * CROSSING — the two events on opposite sides of a strip's rows — that lands
220
+ * within one strip's height of them means the tabs. Flying far past them does
221
+ * not (a fast drag into the page must never snag on the header), a sweep
222
+ * ALONG the rows is not a crossing, and the sides still take the corners
223
+ * (0.4.47): a hand landing in the outer fifth meant "after it", whatever it
224
+ * crossed on the way. Only for a hand ARRIVING: one already holding a strip
225
+ * leaves it by the stay. The grid board had this inline (0.4.67); the split
226
+ * board asks the same question, so it is one helper (0.4.69).
227
+ */
228
+ export declare function stripCrossing(p: CrossingProbe): {
229
+ containerId: string;
230
+ } | null;
193
231
  export declare function resolve(input: ResolveInput): Zone;
194
232
  /** A join target as the walk sees it: another tab container, its frame anchored by the binder while the pointer is inside it. */
195
233
  export interface TabTarget {
@@ -125,6 +125,45 @@ export function stripUnder(p) {
125
125
  visit(r, 0, 0);
126
126
  return bestId === null ? null : { containerId: bestId };
127
127
  }
128
+ /**
129
+ * A HAND MOVES FASTER THAN A STRIP IS TALL. The strip is 30 px and a hand
130
+ * covers 40 to 80 px between pointer events, so testing only where the pointer
131
+ * LANDS skips it — the user, coming down from above the panel: "it's not
132
+ * passing by the tab header, it drops directly to inside or outside." The
133
+ * segment the hand travelled is tested too, in steps of half a strip: a
134
+ * CROSSING — the two events on opposite sides of a strip's rows — that lands
135
+ * within one strip's height of them means the tabs. Flying far past them does
136
+ * not (a fast drag into the page must never snag on the header), a sweep
137
+ * ALONG the rows is not a crossing, and the sides still take the corners
138
+ * (0.4.47): a hand landing in the outer fifth meant "after it", whatever it
139
+ * crossed on the way. Only for a hand ARRIVING: one already holding a strip
140
+ * leaves it by the stay. The grid board had this inline (0.4.67); the split
141
+ * board asks the same question, so it is one helper (0.4.69).
142
+ */
143
+ export function stripCrossing(p) {
144
+ var _a, _b, _c, _d, _e, _f, _g;
145
+ const dx = p.cur.x - p.prev.x;
146
+ const dy = p.cur.y - p.prev.y;
147
+ const n = Math.ceil(Math.hypot(dx, dy) / Math.max(1, p.stripHeight / 2));
148
+ let crossed = null;
149
+ for (let i = 1; i < n && !crossed; i++) {
150
+ crossed = (_b = (_a = stripUnder({ x: p.prev.x + (dx * i) / n, y: p.prev.y + (dy * i) / n, roots: p.roots, held: null, stay: 0, restFrames: p.restFrames })) === null || _a === void 0 ? void 0 : _a.containerId) !== null && _b !== void 0 ? _b : null;
151
+ }
152
+ if (!crossed)
153
+ return null;
154
+ const f = (_d = (_c = p.restFrames) === null || _c === void 0 ? void 0 : _c.get(crossed)) !== null && _d !== void 0 ? _d : (_e = containerOf(p.roots, crossed)) === null || _e === void 0 ? void 0 : _e.frame;
155
+ if (!f)
156
+ return null;
157
+ const top = f.y;
158
+ const bottom = f.y + p.stripHeight;
159
+ const through = (p.prev.y < top && p.cur.y > bottom) || (p.prev.y > bottom && p.cur.y < top);
160
+ const away = p.cur.y < top ? top - p.cur.y : p.cur.y > bottom ? p.cur.y - bottom : 0;
161
+ const band = (_f = p.band) !== null && _f !== void 0 ? _f : BESIDE_BAND;
162
+ const rx = (p.cur.x - f.x) / Math.max(1, f.width);
163
+ const inSideBand = rx < band || rx > 1 - band;
164
+ const inside = p.cur.x >= f.x && p.cur.x <= f.x + f.width;
165
+ return through && !inSideBand && inside && away <= ((_g = p.reach) !== null && _g !== void 0 ? _g : p.stripHeight) ? { containerId: crossed } : null;
166
+ }
128
167
  /**
129
168
  * The band a point is in for a TILE drag: the sides and the bottom as
130
169
  * `bandOf` gives them, and "top" ONLY from the band hanging above the frame.