@grafloria/element 0.4.59 → 0.4.60

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.59",
3
+ "version": "0.4.60",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -1067,35 +1067,6 @@ 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, 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
- }
1090
- for (const [id, el] of ctx.tabStrips) {
1091
- if (!ctx.boardGroups.has(id))
1092
- continue;
1093
- const h = hit(id, el, 0);
1094
- if (h)
1095
- return h;
1096
- }
1097
- return null;
1098
- },
1099
1070
  tabIndexAt: (containerId, cx) => {
1100
1071
  const el = ctx.tabStrips.get(containerId);
1101
1072
  if (!el || !ctx.boardGroups.has(containerId))
@@ -581,28 +581,14 @@ 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
- /**
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
- }): {
594
- containerId: string;
595
- index: number;
596
- } | null;
597
584
  /** Paint (or, with null, clear) the insertion mark on a strip. */
598
585
  markDrop(containerId: string | null, index: number | null): void;
599
586
  /**
600
587
  * 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.
588
+ * no hit test. WHETHER the hand is on a strip at all is the zone walk's
589
+ * answer, from the container's frame in the model at rest: a strip element
590
+ * reports the box it is painted in, and a strip that has been pushed or
591
+ * is gliding home from a push is a box the hand never asked for.
606
592
  */
607
593
  tabIndexAt?(containerId: string, clientX: number): number | null;
608
594
  /** The commands that make `widgetId` a new tab of `containerId` at `index`; `displaced` are this board's survivors. Empty = refused. */
@@ -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 } from './zones.js';
59
+ import { BESIDE_BAND, resolve as resolveZone, 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';
@@ -2176,46 +2176,49 @@ export function bindDashboardGrid(api, group, options = {}) {
2176
2176
  /** What the pointer means for the dragged tile: the resolve over the live tree, with the beside the hand holds. */
2177
2177
  const resolveTileZone = (g, ev) => {
2178
2178
  var _a, _b, _c, _d, _e;
2179
+ const roots = zoneRoots();
2180
+ // THE CONTAINERS THIS GESTURE HAS MOVED, at the frames they rest in. A
2181
+ // drag's own pushes must never change what the hand means — settled for a
2182
+ // group ghost in 0.4.44, and true of a widget for the same reason: the
2183
+ // beside that a band previews SHOVES the container out from under the
2184
+ // pointer, so read live it would answer about a container that is only
2185
+ // there because of the answer.
2186
+ const rests = restFramesOf(g);
2179
2187
  let strip = null;
2180
- if (options.tabDrop && !isStatic && g.kind !== 'palette' && g.subject === 'node') {
2188
+ if (((_a = options.tabDrop) === null || _a === void 0 ? void 0 : _a.tabIndexAt) && !isStatic && g.kind !== 'palette' && g.subject === 'node') {
2181
2189
  // A group never becomes a tab: over a container's strip it is over the
2182
2190
  // container's margin — a cell on the parent board, pushing with intent.
2183
- const crect = api.container.getBoundingClientRect();
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);
2213
- }
2214
- return resolveZone(Object.assign(Object.assign({ x: ev.world.x, y: ev.world.y, roots: zoneRoots(), strip, prev: beside
2191
+ const 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 / (clientPerWorld().y || 1), restFrames: rests });
2192
+ if (hit) {
2193
+ // The SLOT is the painted strip's business — its tabs are laid out by
2194
+ // the browser. A container the gesture shifted sideways is painted
2195
+ // that far from where it was measured, so the client x is carried
2196
+ // across with it.
2197
+ const grp = diagram.getGroup(hit.containerId);
2198
+ const rest = rests.get(hit.containerId);
2199
+ const dx = grp && rest ? (grp.position.x - rest.x) * clientPerWorld().x : 0;
2200
+ const idx = options.tabDrop.tabIndexAt(hit.containerId, api.container.getBoundingClientRect().left + ev.screen.x + dx);
2201
+ if (idx !== null)
2202
+ strip = { containerId: hit.containerId, index: idx };
2203
+ }
2204
+ }
2205
+ return resolveZone({
2206
+ x: ev.world.x,
2207
+ y: ev.world.y,
2208
+ roots,
2209
+ strip,
2210
+ prev: beside
2215
2211
  ? { 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()) }
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() }));
2212
+ : ((_e = (_d = g.leg) === null || _d === void 0 ? void 0 : _d.adopted.besideState()) !== null && _e !== void 0 ? _e : null), // a beside another board holds for the ghost, through its leg
2213
+ maxDepth: nesting,
2214
+ ghostDepth: g.subject === 'group' ? 1 + levelsInside(g.id) : 0, // a group's widgets sit one board deeper than wherever it lands
2215
+ restFrames: rests,
2216
+ ghostSubtree: g.subject === 'group' ? descendantGroups(g.id) : EMPTY_SUBTREE,
2217
+ gap,
2218
+ homeChain: homeChain(),
2219
+ });
2217
2220
  };
2218
- /** This board's containers as they stood at the press: a group ghost's intent pushes never change what the hand means. */
2221
+ /** This board's containers as they stood at the press: a gesture's own pushes never change what the hand means. */
2219
2222
  const restFramesOf = (g) => {
2220
2223
  const out = new Map();
2221
2224
  for (const [id, snap] of g.startGeom) {
@@ -21,6 +21,10 @@
21
21
  * descendant). The strip rows of a tab container are nobody's band, and its
22
22
  * margin (inside the frame, outside the page) is a plain cell on the parent.
23
23
  *
24
+ * `stripUnder` walks the same tree for the strip itself, so a tab slot and a
25
+ * band are decided on one set of frames — the ones the containers REST in,
26
+ * never the ones a preview pushed them to.
27
+ *
24
28
  * This module is pure geometry: no DOM, no engine, no model. The binder
25
29
  * builds the tree for each move from its peers and the model, and acts on
26
30
  * the answer.
@@ -89,13 +93,13 @@ export interface ResolveInput {
89
93
  */
90
94
  homeChain: ReadonlySet<string>;
91
95
  /**
92
- * Containers of the source board as they stood when the gesture began (a
93
- * GROUP ghost pushes solids with intent, 0.4.44). The hand over a pushed
94
- * container's rest frame still means that containerits zones, its
95
- * page tested at the displacement or a group could never enter a
96
- * container its own body reaches before the hand does: the target fled.
97
- * The engine's memory brings the pushed container home when the ghost
98
- * leaves the board.
96
+ * Containers of the source board as they stood when the gesture began. The
97
+ * hand over a pushed container's rest frame still means that container
98
+ * its zones, its page tested at the displacement or the target flees the
99
+ * hand that is aiming at it: a group could never enter a container its own
100
+ * body reaches first (0.4.44), and a widget aimed at a tab strip chased a
101
+ * strip its own preview kept shoving away (0.4.60). The engine's memory
102
+ * brings the pushed container home when the ghost leaves the board.
99
103
  */
100
104
  restFrames?: ReadonlyMap<string, ZoneRect>;
101
105
  }
@@ -127,6 +131,37 @@ export declare const BESIDE_STAY = 0.05;
127
131
  * to its far right means "after it", not "above it".
128
132
  */
129
133
  export declare function bandOf(f: ZoneRect, stripHeight: number, x: number, y: number, band?: number): BesideSide | null;
134
+ export interface StripProbe {
135
+ x: number;
136
+ y: number;
137
+ roots: ZoneBoard[];
138
+ /** The container whose strip the gesture already holds: its rows are widened by `stay`, and it wins over a neighbour's. */
139
+ held: string | null;
140
+ /** The stickiness around the held strip, in world units. */
141
+ stay: number;
142
+ /** Containers the gesture has displaced, at the frames they rest in. */
143
+ restFrames?: ReadonlyMap<string, ZoneRect>;
144
+ }
145
+ /**
146
+ * The tab strip under a pointer — WHERE THE CONTAINER RESTS, read from the
147
+ * same tree at the same frames as `resolve`.
148
+ *
149
+ * A strip is painted at the top of its container, so it travels with it: a
150
+ * beside shifts the container, a refusal pushes it, and it GLIDES home
151
+ * afterwards. Hit-testing the painted box therefore makes the strip a target
152
+ * at wherever the preview happened to put it — and a box in the air sweeps
153
+ * under a hand that is not moving at all and steals the zone. That is a
154
+ * closed loop: the tab claims the hand, the container comes home, the band
155
+ * under the hand claims it back, the container is pushed away, and the strip
156
+ * flies through the pointer again. The user met it as "it flickers between a
157
+ * tab and above the group; I have to go very slowly".
158
+ *
159
+ * So the rows are the container's own, at rest, on the model's frames. What
160
+ * a hand means never depends on what the preview did with it.
161
+ */
162
+ export declare function stripUnder(p: StripProbe): {
163
+ containerId: string;
164
+ } | null;
130
165
  export declare function resolve(input: ResolveInput): Zone;
131
166
  /** A join target as the walk sees it: another tab container, its frame anchored by the binder while the pointer is inside it. */
132
167
  export interface TabTarget {
@@ -21,6 +21,10 @@
21
21
  * descendant). The strip rows of a tab container are nobody's band, and its
22
22
  * margin (inside the frame, outside the page) is a plain cell on the parent.
23
23
  *
24
+ * `stripUnder` walks the same tree for the strip itself, so a tab slot and a
25
+ * band are decided on one set of frames — the ones the containers REST in,
26
+ * never the ones a preview pushed them to.
27
+ *
24
28
  * This module is pure geometry: no DOM, no engine, no model. The binder
25
29
  * builds the tree for each move from its peers and the model, and acts on
26
30
  * the answer.
@@ -55,6 +59,55 @@ export function bandOf(f, stripHeight, x, y, band = BESIDE_BAND) {
55
59
  return 'bottom';
56
60
  return null;
57
61
  }
62
+ /**
63
+ * The tab strip under a pointer — WHERE THE CONTAINER RESTS, read from the
64
+ * same tree at the same frames as `resolve`.
65
+ *
66
+ * A strip is painted at the top of its container, so it travels with it: a
67
+ * beside shifts the container, a refusal pushes it, and it GLIDES home
68
+ * afterwards. Hit-testing the painted box therefore makes the strip a target
69
+ * at wherever the preview happened to put it — and a box in the air sweeps
70
+ * under a hand that is not moving at all and steals the zone. That is a
71
+ * closed loop: the tab claims the hand, the container comes home, the band
72
+ * under the hand claims it back, the container is pushed away, and the strip
73
+ * flies through the pointer again. The user met it as "it flickers between a
74
+ * tab and above the group; I have to go very slowly".
75
+ *
76
+ * So the rows are the container's own, at rest, on the model's frames. What
77
+ * a hand means never depends on what the preview did with it.
78
+ */
79
+ export function stripUnder(p) {
80
+ const { x, y } = p;
81
+ // Plain locals, not one object: a `let` assigned only inside the closure
82
+ // narrows to `never` at the return.
83
+ let bestId = null;
84
+ let bestDepth = -1;
85
+ let bestHeld = false;
86
+ const visit = (b, dx, dy) => {
87
+ var _a;
88
+ for (const c of b.children()) {
89
+ const rest = (_a = p.restFrames) === null || _a === void 0 ? void 0 : _a.get(c.id);
90
+ const atRest = rest && inRect(rest, x, y) ? rest : null;
91
+ const frame = atRest !== null && atRest !== void 0 ? atRest : c.frame;
92
+ const tx = atRest ? x : x + dx;
93
+ const ty = atRest ? y : y + dy;
94
+ const held = c.id === p.held;
95
+ const pad = held ? p.stay : 0;
96
+ if (c.stripHeight > 0 && inRect({ x: frame.x, y: frame.y, width: frame.width, height: c.stripHeight }, tx, ty, pad)) {
97
+ if (bestId === null || held || (!bestHeld && b.depth >= bestDepth)) {
98
+ bestId = c.id;
99
+ bestDepth = b.depth;
100
+ bestHeld = held;
101
+ }
102
+ }
103
+ if (c.inner && inRect(frame, tx, ty, pad))
104
+ visit(c.inner, atRest ? c.frame.x - atRest.x : dx, atRest ? c.frame.y - atRest.y : dy);
105
+ }
106
+ };
107
+ for (const r of p.roots)
108
+ visit(r, 0, 0);
109
+ return bestId === null ? null : { containerId: bestId };
110
+ }
58
111
  /** The board a container sits on, found by id through the tree. */
59
112
  function boardOf(roots, containerId) {
60
113
  const visit = (b) => {