@grafloria/element 0.4.57 → 0.4.59

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.57",
3
+ "version": "0.4.59",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -1067,16 +1067,41 @@ export function createDashboardHandle(ctx) {
1067
1067
  return [new RegisterWidgetCommand({ register: () => apply(after), unregister: () => apply(before) }, 'register')];
1068
1068
  };
1069
1069
  ctx.tabDrop = {
1070
- stripAt: (cx, cy) => {
1070
+ stripAt: (cx, cy, grace) => {
1071
+ // The strip the drag already holds is tested with its box widened by
1072
+ // `grace.px` (see STRIP_STAY): a small overshoot still means the tabs.
1073
+ // Its own hit is checked FIRST, so a neighbour never steals it back.
1074
+ const hit = (id, el, pad) => {
1075
+ const r = el.getBoundingClientRect();
1076
+ if (r.width <= 0)
1077
+ return null;
1078
+ return cx >= r.left - pad && cx <= r.right + pad && cy >= r.top - pad && cy <= r.bottom + pad
1079
+ ? { containerId: id, index: indexInStrip(el, cx) }
1080
+ : null;
1081
+ };
1082
+ if (grace && grace.px > 0) {
1083
+ const el = ctx.tabStrips.get(grace.containerId);
1084
+ if (el && ctx.boardGroups.has(grace.containerId)) {
1085
+ const held = hit(grace.containerId, el, grace.px);
1086
+ if (held)
1087
+ return held;
1088
+ }
1089
+ }
1071
1090
  for (const [id, el] of ctx.tabStrips) {
1072
1091
  if (!ctx.boardGroups.has(id))
1073
1092
  continue;
1074
- const r = el.getBoundingClientRect();
1075
- if (r.width > 0 && cx >= r.left && cx <= r.right && cy >= r.top && cy <= r.bottom)
1076
- return { containerId: id, index: indexInStrip(el, cx) };
1093
+ const h = hit(id, el, 0);
1094
+ if (h)
1095
+ return h;
1077
1096
  }
1078
1097
  return null;
1079
1098
  },
1099
+ tabIndexAt: (containerId, cx) => {
1100
+ const el = ctx.tabStrips.get(containerId);
1101
+ if (!el || !ctx.boardGroups.has(containerId))
1102
+ return null;
1103
+ return indexInStrip(el, cx);
1104
+ },
1080
1105
  markDrop: markTabDrop,
1081
1106
  dropIntoStrip: (widgetId, containerId, index, sourceBoardId, displaced) => moveWidgetToTabCommands(widgetId, containerId, index, sourceBoardId, displaced),
1082
1107
  };
@@ -1211,12 +1236,12 @@ export function createDashboardHandle(ctx) {
1211
1236
  joinTargets: [...ctx.layoutOf.keys()].filter((id) => { var _a, _b; return id !== containerId && id !== pageId && ((_a = ctx.layoutOf.get(id)) !== null && _a !== void 0 ? _a : (_b = specById.get(id)) === null || _b === void 0 ? void 0 : _b.layout) === 'tabs' && !!model.getGroup(id) && !insidePage(id); }),
1212
1237
  stripHeight: (targetId) => tabStripReserve(ctx.tabsOf.get(targetId), Math.max(1, liveCount(targetId))),
1213
1238
  ownBoards: [pageId, ...[...ctx.boardGroups.keys()].filter((id) => insidePage(id))],
1214
- stripIndex: (targetId, cx, cy) => {
1239
+ stripIndex: (targetId, cx, cy, grace = 0) => {
1215
1240
  const strip = ctx.tabStrips.get(targetId);
1216
1241
  if (!strip)
1217
1242
  return null;
1218
1243
  const r = strip.getBoundingClientRect();
1219
- if (r.width === 0 || cx < r.left || cx > r.right || cy < r.top || cy > r.bottom)
1244
+ if (r.width === 0 || cx < r.left - grace || cx > r.right + grace || cy < r.top - grace || cy > r.bottom + grace)
1220
1245
  return null;
1221
1246
  return indexInStrip(strip, cx);
1222
1247
  },
@@ -558,7 +558,7 @@ export interface TearOutPlan {
558
558
  /** Paint (or, with null, clear) the insertion mark on `targetId`'s strip. */
559
559
  markDrop(targetId: string | null, index: number | null): void;
560
560
  /** The tab index a CLIENT point means along `targetId`'s strip — null when the point is not over that strip. */
561
- stripIndex(targetId: string, clientX: number, clientY: number): number | null;
561
+ stripIndex(targetId: string, clientX: number, clientY: number, grace?: number): number | null;
562
562
  /** The strip's height on `targetId`, px — the band above its body. */
563
563
  stripHeight(targetId: string): number;
564
564
  /** The page itself and every board inside it — never a board the page can land on. */
@@ -581,13 +581,30 @@ export interface TearOutPlan {
581
581
  * handle owns the strips and the registry; the binder owns the gesture.
582
582
  */
583
583
  export interface TabDropHooks {
584
- /** The strip under a CLIENT point, and the tab index that point means along it. */
585
- stripAt(clientX: number, clientY: number): {
584
+ /**
585
+ * The strip under a CLIENT point, and the tab index that point means along
586
+ * it. `grace` widens ONE strip's box by that many pixels on every side —
587
+ * the strip a drag is already aiming at keeps the hand through a small
588
+ * overshoot, instead of handing it to the zone underneath on one pixel.
589
+ */
590
+ stripAt(clientX: number, clientY: number, grace?: {
591
+ containerId: string;
592
+ px: number;
593
+ }): {
586
594
  containerId: string;
587
595
  index: number;
588
596
  } | null;
589
597
  /** Paint (or, with null, clear) the insertion mark on a strip. */
590
598
  markDrop(containerId: string | null, index: number | null): void;
599
+ /**
600
+ * Which tab slot a CLIENT x means along one named strip — the x alone, with
601
+ * no hit test. The binder decides WHETHER the hand is on a strip from the
602
+ * container's own frame (the model), because a strip element that is gliding
603
+ * — home after a push, or away under one — reports a box the hand is not in
604
+ * yet. Optional: a host with its own hooks and no such method keeps the
605
+ * live hit test for everything.
606
+ */
607
+ tabIndexAt?(containerId: string, clientX: number): number | null;
591
608
  /** The commands that make `widgetId` a new tab of `containerId` at `index`; `displaced` are this board's survivors. Empty = refused. */
592
609
  dropIntoStrip(widgetId: string, containerId: string, index: number, sourceBoardId: string, displaced: Command[]): Command[];
593
610
  }
@@ -680,4 +697,14 @@ export declare function parentPeerOf(container: HTMLElement, groupId: string): B
680
697
  export declare function clearOtherSelections(container: HTMLElement, self: BinderPeer | null): void;
681
698
  export declare function registerBoardPeer(container: HTMLElement, peer: BinderPeer): () => void;
682
699
  export type { BinderPeer };
700
+ /**
701
+ * THE STRIP KEEPS THE HAND IT HAS (CSS px). A tab strip is ~30 px tall and
702
+ * the zone right under it moves the whole container out of the way, so at the
703
+ * seam a wobble of a pixel or two toggled a 90-px animated push — the user,
704
+ * aiming a widget at the tabs: "it's switching so fast between having it above
705
+ * the entire tab group or inside as a tab". Once a drag is over a strip, that
706
+ * strip's box counts as this much larger until the hand really leaves. A strip
707
+ * the drag has NOT reached gets no extra room: it never grows under the hand.
708
+ */
709
+ export declare const STRIP_STAY = 9;
683
710
  export declare function bindDashboardGrid(api: DashboardGridApi, group: GroupModel, options?: DashboardGridOptions): DashboardGridHandle;
@@ -149,6 +149,16 @@ export function registerBoardPeer(container, peer) {
149
149
  };
150
150
  }
151
151
  const DRAG_THRESHOLD = 4;
152
+ /**
153
+ * THE STRIP KEEPS THE HAND IT HAS (CSS px). A tab strip is ~30 px tall and
154
+ * the zone right under it moves the whole container out of the way, so at the
155
+ * seam a wobble of a pixel or two toggled a 90-px animated push — the user,
156
+ * aiming a widget at the tabs: "it's switching so fast between having it above
157
+ * the entire tab group or inside as a tab". Once a drag is over a strip, that
158
+ * strip's box counts as this much larger until the hand really leaves. A strip
159
+ * the drag has NOT reached gets no extra room: it never grows under the hand.
160
+ */
161
+ export const STRIP_STAY = 9;
152
162
  /** The node a gesture holds — node-only paths (the pixel ghost, a resize, a strip drop, a removal) ask through this. */
153
163
  const nodeOf = (g) => g.entity;
154
164
  let binderSeq = 0;
@@ -697,6 +707,13 @@ export function bindDashboardGrid(api, group, options = {}) {
697
707
  if (!engine.getItem(id))
698
708
  return;
699
709
  engine.remove(id);
710
+ // THE LIVE BOUND FOLLOWS THE MEMBERS. A board that took rows from its
711
+ // parent to hold an arrival (D4) keeps that bound while the tile is here;
712
+ // when the tile leaves — an undo of the drop, a removal — the bound goes
713
+ // back to what the remaining tiles need, or the board stays a row taller
714
+ // than anything in it.
715
+ if (designRows !== undefined)
716
+ setLiveBound(liveBound(engine.getItems()));
700
717
  project();
701
718
  api.render();
702
719
  };
@@ -2158,17 +2175,45 @@ export function bindDashboardGrid(api, group, options = {}) {
2158
2175
  };
2159
2176
  /** What the pointer means for the dragged tile: the resolve over the live tree, with the beside the hand holds. */
2160
2177
  const resolveTileZone = (g, ev) => {
2161
- var _a, _b;
2178
+ var _a, _b, _c, _d, _e;
2162
2179
  let strip = null;
2163
2180
  if (options.tabDrop && !isStatic && g.kind !== 'palette' && g.subject === 'node') {
2164
2181
  // A group never becomes a tab: over a container's strip it is over the
2165
2182
  // container's margin — a cell on the parent board, pushing with intent.
2166
2183
  const crect = api.container.getBoundingClientRect();
2167
- strip = options.tabDrop.stripAt(crect.left + ev.screen.x, crect.top + ev.screen.y);
2184
+ const cx = crect.left + ev.screen.x;
2185
+ const cy = crect.top + ev.screen.y;
2186
+ const stay = g.strip ? STRIP_STAY : 0;
2187
+ // THE HELD CONTAINER'S STRIP IS READ FROM THE MODEL, AT REST — as its
2188
+ // bands are (0.4.49). Two reasons, both measured on the live demo with
2189
+ // a widget carried down the panel's strip: a beside SHIFTS or PUSHES the
2190
+ // container and its strip travels with it, so tested where the DOM says
2191
+ // it is, the tab zone runs away from the hand aiming at it (the tabs
2192
+ // became unreachable at any height); and the DOM box GLIDES, so for a
2193
+ // frame or two after the container is sent home the strip is still in
2194
+ // the air and the hand falls through it.
2195
+ const held = (_c = (_a = beside === null || beside === void 0 ? void 0 : beside.id) !== null && _a !== void 0 ? _a : (_b = g.strip) === null || _b === void 0 ? void 0 : _b.containerId) !== null && _c !== void 0 ? _c : null;
2196
+ if (held && options.tabDrop.tabIndexAt) {
2197
+ const grp = diagram.getGroup(held);
2198
+ const f = beside && beside.id === held ? beside.frame0 : grp ? frameOfGroup(grp) : null;
2199
+ if (f) {
2200
+ const s = clientPerWorld();
2201
+ const tol = stay / (s.y || 1);
2202
+ const inStrip = ev.world.x >= f.x - stay / (s.x || 1) &&
2203
+ ev.world.x <= f.x + f.width + stay / (s.x || 1) &&
2204
+ ev.world.y >= f.y - tol &&
2205
+ ev.world.y <= f.y + TAB_STRIP_HEIGHT + tol;
2206
+ const idx = inStrip ? options.tabDrop.tabIndexAt(held, cx) : null;
2207
+ if (idx !== null)
2208
+ strip = { containerId: held, index: idx };
2209
+ }
2210
+ }
2211
+ if (!strip)
2212
+ strip = options.tabDrop.stripAt(cx, cy, g.strip ? { containerId: g.strip.containerId, px: stay } : undefined);
2168
2213
  }
2169
2214
  return resolveZone(Object.assign(Object.assign({ x: ev.world.x, y: ev.world.y, roots: zoneRoots(), strip, prev: beside
2170
2215
  ? { 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()) }
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() }));
2216
+ : ((_e = (_d = g.leg) === null || _d === void 0 ? void 0 : _d.adopted.besideState()) !== null && _e !== void 0 ? _e : 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
2217
  };
2173
2218
  /** This board's containers as they stood at the press: a group ghost's intent pushes never change what the hand means. */
2174
2219
  const restFramesOf = (g) => {
@@ -2220,6 +2265,14 @@ export function bindDashboardGrid(api, group, options = {}) {
2220
2265
  }
2221
2266
  return deepest;
2222
2267
  };
2268
+ /** Client pixels per world unit — the camera's scale, measured the way the tear-out measures its bands. */
2269
+ const clientPerWorld = () => {
2270
+ const rect = api.container.getBoundingClientRect();
2271
+ 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 }; };
2272
+ const o = toWorld(rect.left, rect.top);
2273
+ const u = toWorld(rect.left + 100, rect.top + 100);
2274
+ return { x: 100 / (u.x - o.x || 100), y: 100 / (u.y - o.y || 100) };
2275
+ };
2223
2276
  /** The groups this board sits in, all the way up: their bands never apply to a tile of this board. */
2224
2277
  const homeChain = () => {
2225
2278
  const out = new Set();
@@ -2394,9 +2447,8 @@ export function bindDashboardGrid(api, group, options = {}) {
2394
2447
  const squeezeBefore = squeezeRoom;
2395
2448
  let entered = engine.add({ id: node.id, x: 0, y: engine.rows(), w: span.w, h: span.h });
2396
2449
  // Only a board with NO parent tile to grow through squeezes — a page. A
2397
- // full SECTION keeps refusing (grid-options s05: refused, then joined once
2398
- // a strip was removed); growing its slab for a dropped tile is the
2399
- // escalation path's, not a squeeze's, and is not built yet.
2450
+ // full SECTION grows instead (D4, below): its height is a tile of its
2451
+ // parent, and the parent is what has the rows to give.
2400
2452
  if (!entered && !parentPeer()) {
2401
2453
  const room = elasticRows();
2402
2454
  if (room !== undefined && room > ((_a = bound()) !== null && _a !== void 0 ? _a : 0)) {
@@ -2405,7 +2457,38 @@ export function bindDashboardGrid(api, group, options = {}) {
2405
2457
  entered = engine.add({ id: node.id, x: 0, y: engine.rows(), w: span.w, h: span.h });
2406
2458
  }
2407
2459
  }
2460
+ let grown = null;
2461
+ const ungrow = () => {
2462
+ const g = grown;
2463
+ if (!g)
2464
+ return;
2465
+ g.peer.resizeMemberBy(group.id, -g.rows);
2466
+ setLiveBound(Math.max(1, (maxRows !== null && maxRows !== void 0 ? maxRows : 1) - g.rows));
2467
+ grown = null;
2468
+ };
2469
+ if (!entered && escalate) {
2470
+ const parent = parentPeer();
2471
+ let rowsAdded = 0;
2472
+ let firstBefore = null;
2473
+ let lastAfter = null;
2474
+ // At most the tile's own height in rows: past that the board is not
2475
+ // "full", it is smaller than the thing being dropped into it.
2476
+ for (let i = 0; parent && !entered && i < Math.max(1, span.h); i++) {
2477
+ const res = parent.resizeMemberBy(group.id, +1);
2478
+ if (!res.changed || !res.cellBefore || !res.cellAfter || !res.frameBefore || !res.frameAfter)
2479
+ break;
2480
+ rowsAdded += 1;
2481
+ firstBefore = firstBefore !== null && firstBefore !== void 0 ? firstBefore : { cell: res.cellBefore, frame: res.frameBefore };
2482
+ lastAfter = { cell: res.cellAfter, frame: res.frameAfter };
2483
+ setLiveBound((maxRows !== null && maxRows !== void 0 ? maxRows : 0) + 1);
2484
+ entered = engine.add({ id: node.id, x: 0, y: engine.rows(), w: span.w, h: span.h });
2485
+ }
2486
+ if (parent && rowsAdded > 0 && firstBefore && lastAfter) {
2487
+ grown = { peer: parent, rows: rowsAdded, cellBefore: firstBefore.cell, frameBefore: firstBefore.frame, cellAfter: lastAfter.cell, frameAfter: lastAfter.frame };
2488
+ }
2489
+ }
2408
2490
  if (!entered) {
2491
+ ungrow();
2409
2492
  setSqueeze(squeezeBefore);
2410
2493
  engine.endGesture();
2411
2494
  return null; // a bounded, full board refuses the adoption
@@ -2535,6 +2618,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2535
2618
  if (engine.getItem(node.id))
2536
2619
  engine.remove(node.id);
2537
2620
  engine.cancelGesture(); // pre-entry layout, memory cleared
2621
+ ungrow(); // the rows this board took from its parent for the arrival go back (D4)
2538
2622
  setSqueeze(squeezeBefore);
2539
2623
  adoptedGhostId = null;
2540
2624
  disarmGlideSoon();
@@ -2555,6 +2639,11 @@ export function bindDashboardGrid(api, group, options = {}) {
2555
2639
  // Widgets AND sections this board's adoption displaced, as one list: a
2556
2640
  // dock or a push that moved a section commits it with the drop.
2557
2641
  const commands = tileCommands(deltasSince(startCells, startGeom, node.id));
2642
+ // …and the rows this board took from its parent to hold the arrival (D4).
2643
+ if (grown) {
2644
+ commands.push(new SetGroupCellCommand(group.id, grown.cellBefore, grown.cellAfter, grown.frameBefore, grown.frameAfter));
2645
+ grown = null;
2646
+ }
2558
2647
  engine.endGesture();
2559
2648
  pendingDrop = node.id; // the tile stays for the member the commit (or the host's drop-in) adds
2560
2649
  if (squeezeRoom !== undefined) {
@@ -1,6 +1,7 @@
1
1
  import { cellToRect, rowHeightFor, sizeToSpan } from './grid-mapping.js';
2
2
  import { BESIDE_BAND, resolveTabZone } from './zones.js';
3
3
  import { EDGE_GRACE } from './board-ctx.js';
4
+ import { STRIP_STAY } from './grid-binder.js';
4
5
  /** A torn-out page never arrives shorter than this: a strip with no room under it is not a group. */
5
6
  export const TEAR_OUT_MIN_ROWS = 2;
6
7
  export function createTearOut(ctx, deps) {
@@ -278,8 +279,11 @@ export function createTearOut(ctx, deps) {
278
279
  x: world.x,
279
280
  y: world.y,
280
281
  clientInside,
281
- ownStrip: plan.stripIndex(fromGroupId, cx, cy),
282
- stripOf: (id) => plan.stripIndex(id, cx, cy),
282
+ // A page dragged along its own strip, or over another's, keeps that
283
+ // strip through a small overshoot too (STRIP_STAY): the zone under a
284
+ // strip moves the container, and a pixel must not toggle it.
285
+ ownStrip: plan.stripIndex(fromGroupId, cx, cy, zone.kind === 'reorder' ? STRIP_STAY : 0),
286
+ stripOf: (id) => plan.stripIndex(id, cx, cy, zone.kind === 'strip' && zone.target.id === id ? STRIP_STAY : 0),
283
287
  root: root && root.kind === 'root' ? { side: root.side } : null,
284
288
  target: target
285
289
  ? {