@grafloria/element 0.4.49 → 0.4.50

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.49",
3
+ "version": "0.4.50",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -227,6 +227,8 @@ export interface DashboardOptions {
227
227
  * so a designer/viewer pair is one flag apart. Live: `handle.setStatic()`.
228
228
  */
229
229
  static?: boolean;
230
+ /** The deepest board a drop may enter (a view is 0; default 2). See the grid binder's `nesting`. */
231
+ nesting?: number;
230
232
  /**
231
233
  * DRAG HANDLE — DevExpress drags an item by its caption. `true`: the caption
232
234
  * strip is the only handle (the header shows grip dots); a selector string:
@@ -2293,7 +2293,7 @@ export function dashboard(options) {
2293
2293
  }
2294
2294
  function bindView(v, g, viewLayout, live) {
2295
2295
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
2296
- const common = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ gap, padding: gap, rtl: (_b = (_a = live === null || live === void 0 ? void 0 : live.rtl) !== null && _a !== void 0 ? _a : options.rtl) !== null && _b !== void 0 ? _b : false, fluid: mode === 'fluid', static: (_d = (_c = live === null || live === void 0 ? void 0 : live.static) !== null && _c !== void 0 ? _c : options.static) !== null && _d !== void 0 ? _d : false, dragHandle: (_f = (_e = live === null || live === void 0 ? void 0 : live.dragHandle) !== null && _e !== void 0 ? _e : options.dragHandle) !== null && _f !== void 0 ? _f : false }, (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), ((_g = options.binder) !== null && _g !== void 0 ? _g : {})), { onGesture: (e) => {
2296
+ const common = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ gap, padding: gap, rtl: (_b = (_a = live === null || live === void 0 ? void 0 : live.rtl) !== null && _a !== void 0 ? _a : options.rtl) !== null && _b !== void 0 ? _b : false, fluid: mode === 'fluid', static: (_d = (_c = live === null || live === void 0 ? void 0 : live.static) !== null && _c !== void 0 ? _c : options.static) !== null && _d !== void 0 ? _d : false }, (options.nesting !== undefined ? { nesting: options.nesting } : {})), { dragHandle: (_f = (_e = live === null || live === void 0 ? void 0 : live.dragHandle) !== null && _e !== void 0 ? _e : options.dragHandle) !== null && _f !== void 0 ? _f : false }), (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), ((_g = options.binder) !== null && _g !== void 0 ? _g : {})), { onGesture: (e) => {
2297
2297
  var _a, _b;
2298
2298
  if (e.type === 'commit')
2299
2299
  reportChanged();
@@ -2311,7 +2311,7 @@ export function dashboard(options) {
2311
2311
  // The LIVE switches ride on the view's binder when it exists (a
2312
2312
  // container bound after a setStatic/setRtl/setDragHandle must match).
2313
2313
  const vb = binders.get((_a = ctx.viewOfBoard.get(w.id)) !== null && _a !== void 0 ? _a : ctx.active);
2314
- const inner = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ columns: innerColumnsOf(w), gap, padding: 0, baseRowHeight: rowHeight, rtl: (_c = (_b = vb === null || vb === void 0 ? void 0 : vb.getRtl()) !== null && _b !== void 0 ? _b : options.rtl) !== null && _c !== void 0 ? _c : false, static: (_e = (_d = vb === null || vb === void 0 ? void 0 : vb.getStatic()) !== null && _d !== void 0 ? _d : options.static) !== null && _e !== void 0 ? _e : false, dragHandle: (_g = (_f = vb === null || vb === void 0 ? void 0 : vb.getDragHandle()) !== null && _f !== void 0 ? _f : options.dragHandle) !== null && _g !== void 0 ? _g : false }, (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), { onGesture: (e) => {
2314
+ const inner = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ columns: innerColumnsOf(w), gap, padding: 0, baseRowHeight: rowHeight, rtl: (_c = (_b = vb === null || vb === void 0 ? void 0 : vb.getRtl()) !== null && _b !== void 0 ? _b : options.rtl) !== null && _c !== void 0 ? _c : false, static: (_e = (_d = vb === null || vb === void 0 ? void 0 : vb.getStatic()) !== null && _d !== void 0 ? _d : options.static) !== null && _e !== void 0 ? _e : false, dragHandle: (_g = (_f = vb === null || vb === void 0 ? void 0 : vb.getDragHandle()) !== null && _f !== void 0 ? _f : options.dragHandle) !== null && _g !== void 0 ? _g : false }, (options.nesting !== undefined ? { nesting: options.nesting } : {})), (options.squeeze !== undefined ? { squeeze: options.squeeze } : {})), { onGesture: (e) => {
2315
2315
  var _a, _b;
2316
2316
  if (e.type === 'commit')
2317
2317
  reportChanged();
@@ -238,6 +238,14 @@ export interface DashboardGridOptions {
238
238
  * still works. The viewer's mode — see `setStatic` for the live switch.
239
239
  */
240
240
  static?: boolean;
241
+ /**
242
+ * NESTING POLICY (tile first, step 2): the deepest board a DROP may enter,
243
+ * counting a view as 0 — a widget into a section is 1, into a section
244
+ * inside a tab page is 2. Deeper containers are opaque to a drag: the
245
+ * pointer over them means a cell on their parent board (or beside them).
246
+ * Default 2, the depth the gates exercise.
247
+ */
248
+ nesting?: number;
241
249
  /**
242
250
  * DRAG HANDLE (DevExpress drags an item by its caption; gridstack's
243
251
  * `handle`). `true` — the CAPTION STRIP is the handle (the header shows grip
@@ -493,6 +501,8 @@ interface BinderPeer {
493
501
  */
494
502
  containsWorldExtended(x: number, y: number): boolean;
495
503
  frameArea(): number;
504
+ /** A static board is opaque to the zone walk: never entered (tile first, step 2). */
505
+ isStatic?(): boolean;
496
506
  adopt(node: {
497
507
  id: string;
498
508
  }, world: {
@@ -56,6 +56,7 @@ import { buildCommitCommands, cellFromGridItem, cellToRect, columnUnitFor, gridI
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
+ import { BESIDE_BAND, resolve as resolveZone } from './zones.js';
59
60
  /** The class of the painted grip element (a child of the node host). */
60
61
  export const GRIP_CLASS = 'axdb-grip';
61
62
  /** The container class that turns the caption strip's grip dots on. */
@@ -196,6 +197,8 @@ export class SequenceCommand extends Command {
196
197
  * page (review D12).
197
198
  */
198
199
  const BOARD_REGISTRY = new WeakMap();
200
+ /** A widget carries no boards: nothing for the zone walk to refuse as its own descendant. */
201
+ const EMPTY_SUBTREE = new Set();
199
202
  /**
200
203
  * Register a board that is NOT a grid (the split binder on a container) as a
201
204
  * peer on its canvas, so the parent grid's hitTest defers a press on one of
@@ -329,7 +332,7 @@ function cursorFor(E) {
329
332
  }
330
333
  let binderSeq = 0;
331
334
  export function bindDashboardGrid(api, group, options = {}) {
332
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
335
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
333
336
  ensureDashboardKitStyles();
334
337
  const diagram = api.getModel();
335
338
  /** The DECLARED count — the board's authored width, and the responsive cap. */
@@ -364,6 +367,8 @@ export function bindDashboardGrid(api, group, options = {}) {
364
367
  const fluid = options.fluid === true;
365
368
  const overflow = (_h = options.overflow) !== null && _h !== void 0 ? _h : 'bounded';
366
369
  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;
367
372
  let dragHandle = normalizeDragHandle(options.dragHandle);
368
373
  let dragSel = dragHandleSelector(dragHandle);
369
374
  api.container.classList.toggle(DRAG_HANDLE_CLASS, dragHandle === true);
@@ -371,10 +376,10 @@ export function bindDashboardGrid(api, group, options = {}) {
371
376
  let focusedId;
372
377
  const live = liveRegionFor(api.container);
373
378
  /** The design height. Fluid boards re-read it from the container. */
374
- let designH = (_l = (_j = options.designHeight) !== null && _j !== void 0 ? _j : (_k = group.size) === null || _k === void 0 ? void 0 : _k.height) !== null && _l !== void 0 ? _l : 0;
379
+ let designH = (_m = (_k = options.designHeight) !== null && _k !== void 0 ? _k : (_l = group.size) === null || _l === void 0 ? void 0 : _l.height) !== null && _m !== void 0 ? _m : 0;
375
380
  /** Fit-mode CAPACITY in rows (see `overflow`). Undefined = unbounded. */
376
381
  let capacity;
377
- let sizing = (_m = options.sizing) !== null && _m !== void 0 ? _m : 'fit';
382
+ let sizing = (_o = options.sizing) !== null && _o !== void 0 ? _o : 'fit';
378
383
  /**
379
384
  * A fresh engine holding `items` VERBATIM. The constructor add()s each item
380
385
  * and settles after every one, so in gravity mode a legal layout with a gap
@@ -1263,41 +1268,26 @@ export function bindDashboardGrid(api, group, options = {}) {
1263
1268
  */
1264
1269
  const TAB_FRAME_GRIP = 3;
1265
1270
  const edgeGripFor = (grp) => (isTabsGroup(grp) ? TAB_FRAME_GRIP : EDGE_GRIP);
1266
- const BESIDE_BAND = 0.2;
1267
- /** How much wider the band is for a pointer ALREADY in it — a hand resting on the boundary must not flicker between beside and into. */
1268
- const BESIDE_STAY = 0.05;
1269
- /** The outer band of a container FRAME the point is in — null in the strip, in the middle, or outside. */
1270
- const bandOf = (f, wx, wy, band = BESIDE_BAND) => {
1271
- if (wx < f.x || wx > f.x + f.width || wy < f.y || wy > f.y + f.height)
1272
- return null;
1273
- const bodyY = f.y + TAB_STRIP_HEIGHT;
1274
- const bodyH = Math.max(1, f.height - TAB_STRIP_HEIGHT);
1275
- const rx = (wx - f.x) / Math.max(1, f.width);
1276
- const ry = (wy - bodyY) / bodyH;
1277
- if (ry < 0)
1278
- return null; // the strip: a new tab, the strip's own business
1279
- if (rx < band)
1280
- return 'left'; // the sides first: they take the corners
1281
- if (rx > 1 - band)
1282
- return 'right';
1283
- if (ry < band)
1284
- return 'top';
1285
- if (ry > 1 - band)
1286
- return 'bottom';
1287
- return null; // the middle: into the page
1288
- };
1289
- const besideZoneAt = (wx, wy) => {
1290
- var _a;
1291
- for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
1292
- const grp = diagram.getGroup(id);
1293
- if (!grp || diagram.getNode(id) || !isTabsGroup(grp))
1294
- continue;
1295
- const side = bandOf(frameOfGroup(grp), wx, wy);
1296
- if (side)
1297
- return { id, side };
1298
- }
1299
- return null;
1300
- };
1271
+ /**
1272
+ * BESIDE (0.4.45): a WIDGET dragged onto a tab container's outer band — a
1273
+ * fifth of its body, the same bands a tab's split uses — lands next to it
1274
+ * on that side; the middle still goes INTO its page and the strip still
1275
+ * makes a tab. At the board's edge, where there is no room on that side,
1276
+ * the container shifts over by the widget's span and the widget takes the
1277
+ * edge: the fluid demo's side panel sits at the right edge, and "after it"
1278
+ * was nowhere (the user: "if the tab is at an edge I can't add something
1279
+ * after it").
1280
+ *
1281
+ * The container gives way LIVE, with the glide every pushed widget gets
1282
+ * (0.4.48): a group is a widget first, and its tab behaviours sit on top of
1283
+ * that default. 0.4.47 had frozen the hover behind an overlay, the way a
1284
+ * tab's split preview works, and the user missed the slide at once. What
1285
+ * his 3440-px frame had caught was the corner: the panel was pushed DOWN
1286
+ * as a widget crossed its top band on the way to its right — so the left
1287
+ * and right bands win the corners (VS Code's precedence): a one-row widget
1288
+ * carried along the top of a tall panel to its far right means "after it",
1289
+ * not "above it". The shift is undone if the pointer leaves the band.
1290
+ */
1301
1291
  /** The container a BESIDE drop unlocked (and maybe shifted): relocked, and put back if asked, when the zone or the gesture ends. */
1302
1292
  let beside = null;
1303
1293
  const endBeside = (restore) => {
@@ -2058,7 +2048,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2058
2048
  };
2059
2049
  };
2060
2050
  const onToolMove = (ev) => {
2061
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
2051
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
2062
2052
  const g = gesture;
2063
2053
  if (!g || g.kind === 'palette')
2064
2054
  return;
@@ -2075,113 +2065,57 @@ export function bindDashboardGrid(api, group, options = {}) {
2075
2065
  ghostStyleFastPath(g, desired);
2076
2066
  g.lastWorld = { x: ev.world.x, y: ev.world.y };
2077
2067
  g.lastScreen = { x: ev.screen.x, y: ev.screen.y };
2078
- // A TAB STRIP under the pointer wins over every board: the widget will
2079
- // become a new tab there, so it leaves this board (survivors settle
2080
- // home) and the strip marks the slot.
2081
- if (options.tabDrop && !isStatic) {
2082
- const crect = api.container.getBoundingClientRect();
2083
- const hitStrip = options.tabDrop.stripAt(crect.left + ev.screen.x, crect.top + ev.screen.y);
2084
- if (hitStrip) {
2085
- if (g.leg) {
2086
- g.leg.adopted.abort();
2087
- g.leg = null;
2088
- }
2089
- endBeside(true); // a band's shift gives way to the strip: the container comes back
2090
- if (!g.removedFromBoard) {
2091
- g.removedFromBoard = true;
2092
- engine.remove(g.id);
2093
- project();
2094
- }
2095
- (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-out');
2096
- if (!g.strip || g.strip.containerId !== hitStrip.containerId || g.strip.index !== hitStrip.index) {
2097
- options.tabDrop.markDrop(hitStrip.containerId, hitStrip.index);
2098
- }
2099
- g.strip = hitStrip;
2100
- syncPlaceholder();
2101
- api.render();
2102
- return;
2103
- }
2104
- if (g.strip) {
2105
- options.tabDrop.markDrop(null, null);
2106
- g.strip = null;
2107
- }
2108
- }
2109
- // BESIDE a tab container (0.4.45): its outer band puts the widget next
2110
- // to it — at the board's edge the container shifts over to make room,
2111
- // live and gliding, like any widget gives way (0.4.48).
2112
- if (!isStatic) {
2113
- const z = besideZoneAt(ev.world.x, ev.world.y);
2114
- if (z) {
2115
- applyBeside(g, z);
2116
- syncPlaceholder();
2117
- return;
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.
2077
+ if (g.leg) {
2078
+ g.leg.adopted.abort();
2079
+ g.leg = null;
2118
2080
  }
2119
- let z2 = null;
2120
- if (beside) {
2121
- // Off the band: the container comes back unless the POINTER is
2122
- // still over the cell it vacated — the widget's own landing, which
2123
- // is what "beside" leaves under the pointer once the container has
2124
- // shifted away (the ghost's wanted cell carries the grab offset and
2125
- // read as "elsewhere" a move later, and the shift undid itself).
2126
- // "Still here" is the pointer in the SAME band of the container's
2127
- // ORIGINAL frame (it asked for beside from there, and the container
2128
- // is what moved; the band is a little wider for a hand already in
2129
- // it, so it does not flicker against "into the page" at the line)
2130
- // or over the cell the widget took: a one-row widget's cell is not
2131
- // where a hand hovering the band sits, and testing only that undid
2132
- // the shift on the next move, which pushed the widget to the bottom
2133
- // as the panel came back (live walk N). "Anywhere inside the
2134
- // original frame" is too much: a hand crossing the bottom band on
2135
- // its way to the middle never reached the page (lab L73) — the
2136
- // middle of the original frame is a zone change.
2137
- const r = cellToRect({ x: beside.vacated.x, y: beside.vacated.y, w: g.spans.w, h: g.spans.h }, frame(), geom(), rows());
2138
- const inR = (q) => ev.world.x >= q.x - gap && ev.world.x <= q.x + q.width + gap && ev.world.y >= q.y - gap && ev.world.y <= q.y + q.height + gap;
2139
- // A DIFFERENT band of the original frame wins over the vacated
2140
- // cell: a widget as wide as the container took the container's
2141
- // whole frame as its cell on a top-band push, so "over the cell"
2142
- // held "above" all the way into the right corner (lab L95).
2143
- const stay = bandOf(beside.frame0, ev.world.x, ev.world.y, BESIDE_BAND + BESIDE_STAY);
2144
- const other = bandOf(beside.frame0, ev.world.x, ev.world.y);
2145
- const over = stay === beside.side || (!(other !== null && other !== beside.side) && inR(r));
2146
- if (!over) {
2147
- endBeside(true);
2148
- // The container is back where it was: the pointer may be in
2149
- // ANOTHER of its bands now (the top band's push had carried the
2150
- // frame away from under a hand heading for the corner) — resolve
2151
- // again in this same move, or the hand rests on a stale preview.
2152
- z2 = besideZoneAt(ev.world.x, ev.world.y);
2153
- }
2154
- else {
2155
- syncPlaceholder();
2156
- return;
2157
- }
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();
2158
2086
  }
2159
- if (z2) {
2160
- applyBeside(g, z2);
2161
- syncPlaceholder();
2162
- return;
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);
2163
2090
  }
2091
+ g.strip = { containerId: z.containerId, index: z.index };
2092
+ syncPlaceholder();
2093
+ api.render();
2094
+ return;
2164
2095
  }
2165
- // Deepest board under the pointer wins: the nested KPI strip beats the
2166
- // tab that contains it; a foreign board beats "outside". Strict frames
2167
- // first; the one-row grace band below each board (gridstack's extra
2168
- // drag row) is consulted only when NO strict frame matched — so "under
2169
- // the last row" appends instead of reading as off-board.
2170
- const strictSelf = worldInsideBoard(ev.world.x, ev.world.y);
2171
- let peer = peerAt(ev.world.x, ev.world.y);
2172
- let inside = strictSelf;
2173
- if (!strictSelf && !peer) {
2174
- if (worldInsideBoardExtended(ev.world.x, ev.world.y))
2175
- inside = true;
2176
- else
2177
- peer = peerAt(ev.world.x, ev.world.y, true);
2178
- // Last resort: the grace band — a small slip past the edge stays ON
2179
- // this board (the engine clamps the cell; prototype parity).
2180
- if (!inside && !peer && worldInsideBoardGrace(ev.world.x, ev.world.y))
2181
- inside = true;
2096
+ if (g.strip) {
2097
+ (_b = options.tabDrop) === null || _b === void 0 ? void 0 : _b.markDrop(null, null);
2098
+ g.strip = null;
2182
2099
  }
2183
- const selfWins = inside && (!peer || boardArea() <= peer.frameArea());
2184
- if (peer && !selfWins) {
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;
2110
+ }
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) {
2185
2119
  // -- HANDOFF: the pointer is over another board -------------------
2186
2120
  if (g.leg && g.leg.peer === peer) {
2187
2121
  g.leg.adopted.move(ev.world);
@@ -2201,13 +2135,13 @@ export function bindDashboardGrid(api, group, options = {}) {
2201
2135
  height: g.node.size.height,
2202
2136
  });
2203
2137
  if (adopted) {
2204
- (_b = hostOf(g.id)) === null || _b === void 0 ? void 0 : _b.classList.remove('axdb-out');
2138
+ (_c = hostOf(g.id)) === null || _c === void 0 ? void 0 : _c.classList.remove('axdb-out');
2205
2139
  g.leg = { peer, adopted };
2206
2140
  g.leg.adopted.move(ev.world);
2207
2141
  }
2208
2142
  else {
2209
2143
  // Bounded/full board refused the adoption: dim = will snap home.
2210
- (_c = hostOf(g.id)) === null || _c === void 0 ? void 0 : _c.classList.add('axdb-out');
2144
+ (_d = hostOf(g.id)) === null || _d === void 0 ? void 0 : _d.classList.add('axdb-out');
2211
2145
  }
2212
2146
  }
2213
2147
  }
@@ -2219,7 +2153,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2219
2153
  }
2220
2154
  if (g.removedFromBoard) {
2221
2155
  g.removedFromBoard = false;
2222
- (_d = hostOf(g.id)) === null || _d === void 0 ? void 0 : _d.classList.remove('axdb-out');
2156
+ (_e = hostOf(g.id)) === null || _e === void 0 ? void 0 : _e.classList.remove('axdb-out');
2223
2157
  const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), g.spans.w);
2224
2158
  // Re-enter at the bottom edge (collision-free), then take the cursor
2225
2159
  // cell GATELESSLY — a first placement skips the anti-jitter gate.
@@ -2228,7 +2162,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2228
2162
  project();
2229
2163
  }
2230
2164
  else {
2231
- const spanW = (_f = (_e = engine.getItem(g.id)) === null || _e === void 0 ? void 0 : _e.w) !== null && _f !== void 0 ? _f : g.spans.w;
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;
2232
2166
  const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), spanW);
2233
2167
  if (engine.moveCheck(g.id, cell.x, cell.y).changed)
2234
2168
  project();
@@ -2243,7 +2177,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2243
2177
  if (!g.removedFromBoard) {
2244
2178
  g.removedFromBoard = true;
2245
2179
  engine.remove(g.id); // survivors settle home; cells minted nowhere
2246
- (_g = hostOf(g.id)) === null || _g === void 0 ? void 0 : _g.classList.add('axdb-out');
2180
+ (_h = hostOf(g.id)) === null || _h === void 0 ? void 0 : _h.classList.add('axdb-out');
2247
2181
  project();
2248
2182
  }
2249
2183
  }
@@ -2353,7 +2287,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2353
2287
  maxRows = n;
2354
2288
  engine.maxRows = n;
2355
2289
  };
2356
- const slabRows = (_j = (_h = parent.memberCell(group.id)) === null || _h === void 0 ? void 0 : _h.h) !== null && _j !== void 0 ? _j : maxRows;
2290
+ const slabRows = (_k = (_j = parent.memberCell(group.id)) === null || _j === void 0 ? void 0 : _j.h) !== null && _k !== void 0 ? _k : maxRows;
2357
2291
  const inner = maxRows;
2358
2292
  const rhNow = rowHeightFor(geom(), rows());
2359
2293
  const rowPx = rhNow + gap;
@@ -2517,7 +2451,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2517
2451
  const anchorBottom = liveRect ? liveRect.y + liveRect.height : bottom;
2518
2452
  const px = E.w ? anchorRight - w : anchorLeft;
2519
2453
  const py = E.n ? anchorBottom - h : anchorTop;
2520
- g.node.setSize(w, h, (_k = g.node.size.depth) !== null && _k !== void 0 ? _k : 0);
2454
+ g.node.setSize(w, h, (_l = g.node.size.depth) !== null && _l !== void 0 ? _l : 0);
2521
2455
  g.node.setPosition(px, py);
2522
2456
  ghostStyleFastPath(g, { x: px, y: py, width: w, height: h });
2523
2457
  const spanF = bound() !== undefined ? frame() : f;
@@ -2713,6 +2647,73 @@ export function bindDashboardGrid(api, group, options = {}) {
2713
2647
  }
2714
2648
  return set;
2715
2649
  };
2650
+ /**
2651
+ * The boards of this canvas as the zone walk sees them (tile first, step
2652
+ * 2): a ROOT is a board whose group has no parent group (a view); a board's
2653
+ * children are its member groups that are containers, each with the board a
2654
+ * descent enters — a tab container's ACTIVE page, a section's own board —
2655
+ * one level deeper. Built from the peers and the model on every move: a
2656
+ * handful of groups, and the frames are live.
2657
+ */
2658
+ const zoneRoots = () => {
2659
+ const peers = [...peersOnCanvas()];
2660
+ const byGroup = new Map(peers.map((p) => [p.group.id, p]));
2661
+ const boardRef = (p, depth) => ({
2662
+ id: p.group.id,
2663
+ depth,
2664
+ ref: p,
2665
+ contains: (x, y) => p.containsWorld(x, y),
2666
+ containsExtended: (x, y) => p.containsWorldExtended(x, y),
2667
+ children: () => {
2668
+ var _a, _b, _c, _d, _e;
2669
+ const out = [];
2670
+ for (const id of (_a = p.group.members) !== null && _a !== void 0 ? _a : []) {
2671
+ const grp = diagram.getGroup(id);
2672
+ if (!grp || diagram.getNode(id))
2673
+ continue;
2674
+ const cw = ((_b = grp.getMetadata('containerWidget')) !== null && _b !== void 0 ? _b : {});
2675
+ const layout = cw.layout === 'tabs' ? 'tabs' : cw.layout === 'split' ? 'split' : 'grid';
2676
+ let innerPeer;
2677
+ if (layout === 'tabs') {
2678
+ const pageId = cw.active && byGroup.has(cw.active) ? cw.active : [...((_c = grp.members) !== null && _c !== void 0 ? _c : [])].find((m) => byGroup.has(m));
2679
+ innerPeer = pageId ? byGroup.get(pageId) : undefined;
2680
+ }
2681
+ else
2682
+ innerPeer = byGroup.get(id);
2683
+ out.push({
2684
+ id,
2685
+ layout,
2686
+ 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,
2687
+ frame: frameOfGroup(grp),
2688
+ stripHeight: layout === 'tabs' ? TAB_STRIP_HEIGHT : 0,
2689
+ band: layout === 'tabs' ? BESIDE_BAND : 0, // a section's whole body is "into" (Quantia's Groups page)
2690
+ inner: innerPeer ? boardRef(innerPeer, depth + 1) : null,
2691
+ });
2692
+ }
2693
+ return out;
2694
+ },
2695
+ });
2696
+ return peers.filter((p) => !p.group.parentGroupId).map((p) => boardRef(p, 0));
2697
+ };
2698
+ /** What the pointer means for the dragged tile: the resolve over the live tree, with the beside the hand holds. */
2699
+ const resolveTileZone = (g, ev) => {
2700
+ let strip = null;
2701
+ if (options.tabDrop && !isStatic) {
2702
+ const crect = api.container.getBoundingClientRect();
2703
+ strip = options.tabDrop.stripAt(crect.left + ev.screen.x, crect.top + ev.screen.y);
2704
+ }
2705
+ return resolveZone({
2706
+ x: ev.world.x,
2707
+ y: ev.world.y,
2708
+ roots: zoneRoots(),
2709
+ 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,
2711
+ maxDepth: nesting,
2712
+ ghostDepth: 0,
2713
+ ghostSubtree: EMPTY_SUBTREE,
2714
+ gap,
2715
+ });
2716
+ };
2716
2717
  /** The board whose engine holds OUR group as an item (nesting parent). */
2717
2718
  const parentPeer = () => {
2718
2719
  for (const p of peersOnCanvas()) {
@@ -3048,6 +3049,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3048
3049
  };
3049
3050
  const selfPeer = {
3050
3051
  group,
3052
+ isStatic: () => isStatic,
3051
3053
  tearOutMember: (pageId, fromGroupId, ev, plan) => beginTearOut(pageId, fromGroupId, ev, plan),
3052
3054
  clearSelection: () => {
3053
3055
  var _a;
@@ -1744,6 +1744,7 @@ export function bindDashboardSplit(api, group, options = {}) {
1744
1744
  // as a peer so the parent's hitTest hands presses on our tiles to us.
1745
1745
  const selfPeer = {
1746
1746
  group,
1747
+ isStatic: () => isStatic,
1747
1748
  clearSelection: () => {
1748
1749
  var _a;
1749
1750
  if (selectedId === undefined)
@@ -0,0 +1,113 @@
1
+ /**
2
+ * zones — where a dragged tile lands, decided once, recursively (tile first,
3
+ * step 2).
4
+ *
5
+ * Every board is an engine; every container is a tile of its parent's board
6
+ * and a board for its own children. `resolve` walks that tree from the roots
7
+ * inward and answers, for a pointer, one of four things:
8
+ *
9
+ * - `strip` — a tab slot in a container's strip (the client-space hit the
10
+ * binder supplies wins over every board, as it always has);
11
+ * - `beside` — the outer fifth of a container: a cell next to it on the
12
+ * board the container itself sits on;
13
+ * - `plain` — a cell on the deepest board under the pointer that can be
14
+ * entered; "into a container" is nothing more than descending
15
+ * one level, so it needs no kind of its own;
16
+ * - `off` — outside every board.
17
+ *
18
+ * A container is OPAQUE — never entered, its bands still answering — when it
19
+ * is static, when entering it would exceed the depth policy, or when it is
20
+ * part of the dragged subtree (a container cannot be dropped into its own
21
+ * descendant). The strip rows of a tab container are nobody's band, and its
22
+ * margin (inside the frame, outside the page) is a plain cell on the parent.
23
+ *
24
+ * This module is pure geometry: no DOM, no engine, no model. The binder
25
+ * builds the tree for each move from its peers and the model, and acts on
26
+ * the answer.
27
+ */
28
+ export interface ZoneRect {
29
+ x: number;
30
+ y: number;
31
+ width: number;
32
+ height: number;
33
+ }
34
+ export type BesideSide = 'left' | 'right' | 'top' | 'bottom';
35
+ /** A board in the walk: the binder's peer behind it rides in `ref`. */
36
+ export interface ZoneBoard {
37
+ id: string;
38
+ /** Roots are 0; a container's inner board is its board's depth + 1. */
39
+ depth: number;
40
+ ref?: unknown;
41
+ contains(x: number, y: number): boolean;
42
+ /** Containment plus the one-row grace under the board (gridstack's extra drag row). */
43
+ containsExtended(x: number, y: number): boolean;
44
+ children(): ZoneContainer[];
45
+ }
46
+ /** A container as the walk sees it: a frame on its board, a layout, and the board inside it. */
47
+ export interface ZoneContainer {
48
+ id: string;
49
+ layout: 'grid' | 'split' | 'tabs';
50
+ static: boolean;
51
+ frame: ZoneRect;
52
+ /** The rows at the top that belong to the strip: never a band. 0 for a section. */
53
+ stripHeight: number;
54
+ /** The outer fraction of the body that means "beside"; 0 for a container whose whole body is "into" (a section). */
55
+ band: number;
56
+ /** The board a descent enters: a tab container's ACTIVE page, a section's own board. Null: nothing to enter. */
57
+ inner: ZoneBoard | null;
58
+ }
59
+ export interface StripHit {
60
+ containerId: string;
61
+ index: number;
62
+ }
63
+ /** The beside the hand already holds, with the container's frame AT REST and the cell the widget took. */
64
+ export interface BesideMemory {
65
+ containerId: string;
66
+ side: BesideSide;
67
+ frame0: ZoneRect;
68
+ vacated: ZoneRect;
69
+ }
70
+ export interface ResolveInput {
71
+ x: number;
72
+ y: number;
73
+ roots: ZoneBoard[];
74
+ strip: StripHit | null;
75
+ prev: BesideMemory | null;
76
+ /** The deepest board a drop may enter (a root is 0). */
77
+ maxDepth: number;
78
+ /** The depth of the dragged subtree: 0 for a widget, 1 for a section, 2 for a section holding a section. */
79
+ ghostDepth: number;
80
+ /** The groups inside the dragged tile: never entered. */
81
+ ghostSubtree: ReadonlySet<string>;
82
+ /** The board's gap, the tolerance around the vacated cell. */
83
+ gap: number;
84
+ }
85
+ export type Zone = {
86
+ kind: 'off';
87
+ } | {
88
+ kind: 'plain';
89
+ board: ZoneBoard;
90
+ grace: boolean;
91
+ } | {
92
+ kind: 'strip';
93
+ containerId: string;
94
+ index: number;
95
+ } | {
96
+ kind: 'beside';
97
+ board: ZoneBoard;
98
+ containerId: string;
99
+ side: BesideSide;
100
+ kept: boolean;
101
+ };
102
+ /** The outer fifth of a container's body is beside it — the bands a tab's split uses. */
103
+ export declare const BESIDE_BAND = 0.2;
104
+ /** How much wider a band is for a hand ALREADY in it: a hand resting on the line must not flicker. */
105
+ export declare const BESIDE_STAY = 0.05;
106
+ /**
107
+ * The band of a container frame a point is in: null in the strip rows, in
108
+ * the middle, or outside. The left and right bands take the corners (VS
109
+ * Code's precedence): a one-row widget carried along the top of a tall panel
110
+ * to its far right means "after it", not "above it".
111
+ */
112
+ export declare function bandOf(f: ZoneRect, stripHeight: number, x: number, y: number, band?: number): BesideSide | null;
113
+ export declare function resolve(input: ResolveInput): Zone;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * zones — where a dragged tile lands, decided once, recursively (tile first,
3
+ * step 2).
4
+ *
5
+ * Every board is an engine; every container is a tile of its parent's board
6
+ * and a board for its own children. `resolve` walks that tree from the roots
7
+ * inward and answers, for a pointer, one of four things:
8
+ *
9
+ * - `strip` — a tab slot in a container's strip (the client-space hit the
10
+ * binder supplies wins over every board, as it always has);
11
+ * - `beside` — the outer fifth of a container: a cell next to it on the
12
+ * board the container itself sits on;
13
+ * - `plain` — a cell on the deepest board under the pointer that can be
14
+ * entered; "into a container" is nothing more than descending
15
+ * one level, so it needs no kind of its own;
16
+ * - `off` — outside every board.
17
+ *
18
+ * A container is OPAQUE — never entered, its bands still answering — when it
19
+ * is static, when entering it would exceed the depth policy, or when it is
20
+ * part of the dragged subtree (a container cannot be dropped into its own
21
+ * descendant). The strip rows of a tab container are nobody's band, and its
22
+ * margin (inside the frame, outside the page) is a plain cell on the parent.
23
+ *
24
+ * This module is pure geometry: no DOM, no engine, no model. The binder
25
+ * builds the tree for each move from its peers and the model, and acts on
26
+ * the answer.
27
+ */
28
+ /** The outer fifth of a container's body is beside it — the bands a tab's split uses. */
29
+ export const BESIDE_BAND = 0.2;
30
+ /** How much wider a band is for a hand ALREADY in it: a hand resting on the line must not flicker. */
31
+ export const BESIDE_STAY = 0.05;
32
+ const inRect = (r, x, y, tol = 0) => x >= r.x - tol && x <= r.x + r.width + tol && y >= r.y - tol && y <= r.y + r.height + tol;
33
+ /**
34
+ * The band of a container frame a point is in: null in the strip rows, in
35
+ * the middle, or outside. The left and right bands take the corners (VS
36
+ * Code's precedence): a one-row widget carried along the top of a tall panel
37
+ * to its far right means "after it", not "above it".
38
+ */
39
+ export function bandOf(f, stripHeight, x, y, band = BESIDE_BAND) {
40
+ if (band <= 0 || !inRect(f, x, y))
41
+ return null;
42
+ const bodyY = f.y + stripHeight;
43
+ const bodyH = Math.max(1, f.height - stripHeight);
44
+ const rx = (x - f.x) / Math.max(1, f.width);
45
+ const ry = (y - bodyY) / bodyH;
46
+ if (ry < 0)
47
+ return null; // the strip: a new tab, the strip's own business
48
+ if (rx < band)
49
+ return 'left';
50
+ if (rx > 1 - band)
51
+ return 'right';
52
+ if (ry < band)
53
+ return 'top';
54
+ if (ry > 1 - band)
55
+ return 'bottom';
56
+ return null;
57
+ }
58
+ /** The board a container sits on, found by id through the tree. */
59
+ function boardOf(roots, containerId) {
60
+ const visit = (b) => {
61
+ for (const c of b.children()) {
62
+ if (c.id === containerId)
63
+ return b;
64
+ const deeper = c.inner ? visit(c.inner) : null;
65
+ if (deeper)
66
+ return deeper;
67
+ }
68
+ return null;
69
+ };
70
+ for (const r of roots) {
71
+ const found = visit(r);
72
+ if (found)
73
+ return found;
74
+ }
75
+ return null;
76
+ }
77
+ export function resolve(input) {
78
+ const { x, y } = input;
79
+ if (input.strip)
80
+ return { kind: 'strip', containerId: input.strip.containerId, index: input.strip.index };
81
+ // The beside the hand already holds: the same band of the container's
82
+ // ORIGINAL frame (a little wider), or the cell the widget took, keeps it —
83
+ // the container is what moved, and its live frame is no guide. A different
84
+ // band of that frame is a zone change and wins over the vacated cell; the
85
+ // middle of it is a zone change too (into the page).
86
+ if (input.prev) {
87
+ const p = input.prev;
88
+ const stripH = stripHeightOf(input.roots, p.containerId);
89
+ const stay = bandOf(p.frame0, stripH, x, y, BESIDE_BAND + BESIDE_STAY);
90
+ const other = bandOf(p.frame0, stripH, x, y);
91
+ const held = stay === p.side || (!(other !== null && other !== p.side) && inRect(p.vacated, x, y, input.gap));
92
+ if (held) {
93
+ const board = boardOf(input.roots, p.containerId);
94
+ if (board)
95
+ return { kind: 'beside', board, containerId: p.containerId, side: p.side, kept: true };
96
+ }
97
+ }
98
+ const opaque = (board, c) => c.static || input.ghostSubtree.has(c.id) || board.depth + 1 + input.ghostDepth > input.maxDepth;
99
+ // The container the hand holds is read at REST: its live frame is where the
100
+ // beside pushed or shifted it, and a hand crossing from its top band to its
101
+ // corner would otherwise find it gone from under the pointer (0.4.48's
102
+ // corner). Its subtree moved with it, so those boards are tested at the
103
+ // same displacement.
104
+ const held = input.prev;
105
+ const descend = (board, dx, dy) => {
106
+ const px = x + dx;
107
+ const py = y + dy;
108
+ for (const c of board.children()) {
109
+ const atRest = held && c.id === held.containerId ? held.frame0 : null;
110
+ const frame = atRest !== null && atRest !== void 0 ? atRest : c.frame;
111
+ const tx = atRest ? x : px;
112
+ const ty = atRest ? y : py;
113
+ if (!inRect(frame, tx, ty))
114
+ continue;
115
+ const side = bandOf(frame, c.stripHeight, tx, ty, c.band);
116
+ if (side)
117
+ return { kind: 'beside', board, containerId: c.id, side, kept: false };
118
+ if (opaque(board, c) || !c.inner)
119
+ return { kind: 'plain', board, grace: false };
120
+ const ndx = atRest ? c.frame.x - atRest.x : dx;
121
+ const ndy = atRest ? c.frame.y - atRest.y : dy;
122
+ if (!c.inner.contains(x + ndx, y + ndy))
123
+ return { kind: 'plain', board, grace: false }; // the margin: the container's own frame
124
+ return descend(c.inner, ndx, ndy);
125
+ }
126
+ return { kind: 'plain', board, grace: false };
127
+ };
128
+ for (const root of input.roots)
129
+ if (root.contains(x, y))
130
+ return descend(root, 0, 0);
131
+ // Nothing strict matched: the deepest board whose grace row holds the point.
132
+ let deepest = null;
133
+ const visit = (b) => {
134
+ if (b.containsExtended(x, y) && (!deepest || b.depth > deepest.depth))
135
+ deepest = b;
136
+ for (const c of b.children())
137
+ if (c.inner)
138
+ visit(c.inner);
139
+ };
140
+ for (const root of input.roots)
141
+ visit(root);
142
+ return deepest ? { kind: 'plain', board: deepest, grace: true } : { kind: 'off' };
143
+ }
144
+ function stripHeightOf(roots, containerId) {
145
+ const visit = (b) => {
146
+ for (const c of b.children()) {
147
+ if (c.id === containerId)
148
+ return c.stripHeight;
149
+ const deeper = c.inner ? visit(c.inner) : null;
150
+ if (deeper !== null)
151
+ return deeper;
152
+ }
153
+ return null;
154
+ };
155
+ for (const r of roots) {
156
+ const h = visit(r);
157
+ if (h !== null)
158
+ return h;
159
+ }
160
+ return 0;
161
+ }
162
+ //# sourceMappingURL=zones.js.map