@pinagent/react-native 0.2.2 → 0.2.4
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/dist/native/inspector.d.ts +79 -1
- package/package.json +1 -1
- package/src/native/inspector.ts +252 -39
|
@@ -57,10 +57,46 @@ interface RawInspectorData {
|
|
|
57
57
|
} | null;
|
|
58
58
|
/** Some versions surface the resolved source straight on the payload. */
|
|
59
59
|
source?: RawFiberLike | null;
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* NOT the tapped leaf's props. RN fills this from the nearest authored
|
|
62
|
+
* *composite owner's* FIRST host descendant (`getHostProps` →
|
|
63
|
+
* `findCurrentHostFiber`) — i.e. that component's outermost view — so a tap
|
|
64
|
+
* on a nested child surfaces its container here, not the element under the
|
|
65
|
+
* finger. Used only as a fallback; `closestPublicInstance` pins the leaf.
|
|
66
|
+
*/
|
|
61
67
|
props?: RawProps;
|
|
68
|
+
/**
|
|
69
|
+
* The public instance of the host view actually under the finger — the
|
|
70
|
+
* deepest hit-tested node. Fabric only; Paper passes a numeric view tag we
|
|
71
|
+
* can't bridge, so we degrade to `props`. Its fiber's `memoizedProps` carry
|
|
72
|
+
* the leaf's own `data-pa-loc`. See {@link tappedLeafLoc}.
|
|
73
|
+
*/
|
|
74
|
+
closestPublicInstance?: unknown;
|
|
75
|
+
}
|
|
76
|
+
interface FiberLike {
|
|
77
|
+
tag?: number;
|
|
78
|
+
return?: FiberLike | null;
|
|
79
|
+
/** First child / next sibling — the render-tree walk for the measure fallback. */
|
|
80
|
+
child?: FiberLike | null;
|
|
81
|
+
sibling?: FiberLike | null;
|
|
82
|
+
stateNode?: {
|
|
83
|
+
canonical?: {
|
|
84
|
+
publicInstance?: unknown;
|
|
85
|
+
};
|
|
86
|
+
} | null;
|
|
87
|
+
/** Host fibers carry the committed props — where `data-pa-loc` rides. */
|
|
88
|
+
memoizedProps?: RawProps;
|
|
62
89
|
}
|
|
63
90
|
type Loc = NonNullable<PickResult['loc']>;
|
|
91
|
+
/**
|
|
92
|
+
* Walk a host fiber's render-tree parent (`return`) chain, returning the first
|
|
93
|
+
* `data-pa-loc` found on a fiber's `memoizedProps` — the tapped element itself,
|
|
94
|
+
* or, when that exact host is untagged (a 3rd-party / RN-internal view), the
|
|
95
|
+
* nearest authored element enclosing it. Capped against a malformed `return`
|
|
96
|
+
* cycle. Pure over a fiber-like chain; exported for unit testing without an RN
|
|
97
|
+
* runtime.
|
|
98
|
+
*/
|
|
99
|
+
export declare function nearestPaLocUp(fiber: FiberLike | null): Loc | null;
|
|
64
100
|
/**
|
|
65
101
|
* Keep only authored React components in the breadcrumb. Two kinds of noise
|
|
66
102
|
* are hidden because clicking them is meaningless — they map to no source the
|
|
@@ -104,6 +140,48 @@ export declare function crumbsOf(data: RawInspectorData): RawCrumb[];
|
|
|
104
140
|
* stuck measure can't hang the pick).
|
|
105
141
|
*/
|
|
106
142
|
export declare function measureFrame(measure?: (cb: RawMeasureCb) => void): Promise<Frame | null>;
|
|
143
|
+
/** True when window-coordinate point (x, y) lies within frame `f`. */
|
|
144
|
+
export declare function frameContains(f: Frame, x: number, y: number): boolean;
|
|
145
|
+
/** A measure-resolved hit: the deepest tagged host whose frame holds the tap. */
|
|
146
|
+
interface MeasuredHit {
|
|
147
|
+
fiber: FiberLike;
|
|
148
|
+
loc: Loc;
|
|
149
|
+
name: string | null;
|
|
150
|
+
frame: Frame;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Measure-based hit-test — the fallback for when RN's geometric
|
|
154
|
+
* `findNodeAtPoint` can't descend to the tapped element.
|
|
155
|
+
*
|
|
156
|
+
* `react-native-pager-view` (and anything that hosts content in a native
|
|
157
|
+
* container) detaches a page's *native* views from the Fabric shadow tree
|
|
158
|
+
* `findNodeAtPoint` walks, so the native hit-test bottoms out at the page
|
|
159
|
+
* wrapper and never reaches the widget. But the React **fiber** tree is intact
|
|
160
|
+
* and the widgets are on-screen, hence measurable — so we hit-test ourselves:
|
|
161
|
+
* DFS the fiber subtree under `root`, measuring each host, and keep the DEEPEST
|
|
162
|
+
* tagged host whose window frame contains the tap.
|
|
163
|
+
*
|
|
164
|
+
* Pruned like the browser's `elementFromPoint`: a host whose frame misses the
|
|
165
|
+
* point can't have a (non-overflowing) descendant that hits, so its subtree is
|
|
166
|
+
* skipped. Composite / fragment fibers carry no frame, so we always descend
|
|
167
|
+
* through them to reach their hosts. `measure` is injected so the traversal is
|
|
168
|
+
* unit-testable without an RN runtime.
|
|
169
|
+
*/
|
|
170
|
+
export declare function measureHitTest(root: FiberLike | null, x: number, y: number, measure: (fiber: FiberLike) => Promise<Frame | null>): Promise<MeasuredHit | null>;
|
|
171
|
+
/**
|
|
172
|
+
* The chain of distinctly-located tagged hosts from `leaf` up to the root,
|
|
173
|
+
* root-first (matching {@link crumbsOf}'s order). Built from the fiber `return`
|
|
174
|
+
* chain for the measure-fallback breadcrumb. Consecutive hosts sharing a
|
|
175
|
+
* `data-pa-loc` (a forwarding wrapper re-emitting the call site) collapse to a
|
|
176
|
+
* single crumb; names come from the babel plugin's `data-pa-comp`. Pure over a
|
|
177
|
+
* fiber-like chain (capped against a malformed `return` cycle); exported for
|
|
178
|
+
* unit testing.
|
|
179
|
+
*/
|
|
180
|
+
export declare function taggedAncestors(leaf: FiberLike | null): {
|
|
181
|
+
name: string;
|
|
182
|
+
loc: Loc;
|
|
183
|
+
fiber: FiberLike;
|
|
184
|
+
}[];
|
|
107
185
|
/**
|
|
108
186
|
* Resolve a tap (in window coordinates) to a {@link PickResult}.
|
|
109
187
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinagent/react-native",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "React Native & Expo plugin for Pinagent — tap a view, leave a comment, and a coding agent fixes it with file:line + a screenshot. The native widget ships as source for Metro; the dev-server middleware and Babel source-tagging plugin are built for Node.",
|
|
6
6
|
"keywords": [
|
package/src/native/inspector.ts
CHANGED
|
@@ -80,8 +80,21 @@ interface RawInspectorData {
|
|
|
80
80
|
closestInstance?: { _debugSource?: RawFiberLike } | null;
|
|
81
81
|
/** Some versions surface the resolved source straight on the payload. */
|
|
82
82
|
source?: RawFiberLike | null;
|
|
83
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* NOT the tapped leaf's props. RN fills this from the nearest authored
|
|
85
|
+
* *composite owner's* FIRST host descendant (`getHostProps` →
|
|
86
|
+
* `findCurrentHostFiber`) — i.e. that component's outermost view — so a tap
|
|
87
|
+
* on a nested child surfaces its container here, not the element under the
|
|
88
|
+
* finger. Used only as a fallback; `closestPublicInstance` pins the leaf.
|
|
89
|
+
*/
|
|
84
90
|
props?: RawProps;
|
|
91
|
+
/**
|
|
92
|
+
* The public instance of the host view actually under the finger — the
|
|
93
|
+
* deepest hit-tested node. Fabric only; Paper passes a numeric view tag we
|
|
94
|
+
* can't bridge, so we degrade to `props`. Its fiber's `memoizedProps` carry
|
|
95
|
+
* the leaf's own `data-pa-loc`. See {@link tappedLeafLoc}.
|
|
96
|
+
*/
|
|
97
|
+
closestPublicInstance?: unknown;
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
let cachedFn: InspectorFn | null | undefined;
|
|
@@ -127,7 +140,12 @@ const HOST_COMPONENT = 5;
|
|
|
127
140
|
interface FiberLike {
|
|
128
141
|
tag?: number;
|
|
129
142
|
return?: FiberLike | null;
|
|
143
|
+
/** First child / next sibling — the render-tree walk for the measure fallback. */
|
|
144
|
+
child?: FiberLike | null;
|
|
145
|
+
sibling?: FiberLike | null;
|
|
130
146
|
stateNode?: { canonical?: { publicInstance?: unknown } } | null;
|
|
147
|
+
/** Host fibers carry the committed props — where `data-pa-loc` rides. */
|
|
148
|
+
memoizedProps?: RawProps;
|
|
131
149
|
}
|
|
132
150
|
|
|
133
151
|
let cachedGetHandle: ((instance: unknown) => FiberLike | null) | null | undefined;
|
|
@@ -209,14 +227,63 @@ function paLocOf(props: RawProps): Loc | null {
|
|
|
209
227
|
return parsePaLoc(props?.['data-pa-loc']);
|
|
210
228
|
}
|
|
211
229
|
|
|
230
|
+
/** The enclosing component name the babel plugin records as `data-pa-comp`. */
|
|
231
|
+
function compOf(props: RawProps): string | null {
|
|
232
|
+
const c = props?.['data-pa-comp'];
|
|
233
|
+
return typeof c === 'string' && c.length > 0 ? c : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Walk a host fiber's render-tree parent (`return`) chain, returning the first
|
|
238
|
+
* `data-pa-loc` found on a fiber's `memoizedProps` — the tapped element itself,
|
|
239
|
+
* or, when that exact host is untagged (a 3rd-party / RN-internal view), the
|
|
240
|
+
* nearest authored element enclosing it. Capped against a malformed `return`
|
|
241
|
+
* cycle. Pure over a fiber-like chain; exported for unit testing without an RN
|
|
242
|
+
* runtime.
|
|
243
|
+
*/
|
|
244
|
+
export function nearestPaLocUp(fiber: FiberLike | null): Loc | null {
|
|
245
|
+
for (let i = 0; fiber && i < 10_000; i++) {
|
|
246
|
+
const loc = paLocOf(fiber.memoizedProps);
|
|
247
|
+
if (loc) return loc;
|
|
248
|
+
fiber = fiber.return ?? null;
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The source of the host view actually under the finger.
|
|
255
|
+
*
|
|
256
|
+
* RN's `data.props` is NOT the tapped leaf — it's the nearest authored
|
|
257
|
+
* composite owner's first host descendant (its outer container), so a tap on a
|
|
258
|
+
* nested child resolves to its parent. To land on the leaf, start from the
|
|
259
|
+
* hit-tested host (`data.closestPublicInstance`), bridge it to its fiber, and
|
|
260
|
+
* walk the render-tree parents for the nearest `data-pa-loc`. This mirrors the
|
|
261
|
+
* web widget walking up the DOM from the clicked node.
|
|
262
|
+
*
|
|
263
|
+
* Returns null when the instance can't be bridged (Paper, which surfaces only a
|
|
264
|
+
* numeric view tag) — `pickLoc` then falls back to `data.props`.
|
|
265
|
+
*/
|
|
266
|
+
function tappedLeafLoc(data: RawInspectorData): Loc | null {
|
|
267
|
+
return nearestPaLocUp(getHandleFromPublicInstance(data.closestPublicInstance));
|
|
268
|
+
}
|
|
269
|
+
|
|
212
270
|
/**
|
|
213
271
|
* Resolve the source location. Preferred path: the build-time `data-pa-loc`
|
|
214
|
-
* prop our babel plugin splices on
|
|
215
|
-
*
|
|
216
|
-
*
|
|
272
|
+
* prop our babel plugin splices on — read first from the host view actually
|
|
273
|
+
* under the finger ({@link tappedLeafLoc}), then from `data.props` and each
|
|
274
|
+
* owner outward. Fallback: RN's legacy `_debugSource` / inspector `source`
|
|
275
|
+
* field, for older RN/React where they still exist.
|
|
217
276
|
*/
|
|
218
277
|
function pickLoc(data: RawInspectorData, projectRoot: string): Loc | null {
|
|
219
|
-
//
|
|
278
|
+
// 0. The host view actually under the finger. MUST come first: `data.props`
|
|
279
|
+
// (step 1) is the nearest composite owner's first host — an outer
|
|
280
|
+
// container — so without this a tap on a nested child resolves to its
|
|
281
|
+
// parent (e.g. a card's content tapped, but its screen layout returned).
|
|
282
|
+
const leaf = tappedLeafLoc(data);
|
|
283
|
+
if (leaf) return leaf;
|
|
284
|
+
|
|
285
|
+
// 1. `data-pa-loc` on `data.props` — the nearest authored owner's first host.
|
|
286
|
+
// Fallback for when the touched host can't be bridged to a fiber (Paper).
|
|
220
287
|
const direct = paLocOf(data.props);
|
|
221
288
|
if (direct) return direct;
|
|
222
289
|
|
|
@@ -371,6 +438,132 @@ export function measureFrame(measure?: (cb: RawMeasureCb) => void): Promise<Fram
|
|
|
371
438
|
});
|
|
372
439
|
}
|
|
373
440
|
|
|
441
|
+
/** True when window-coordinate point (x, y) lies within frame `f`. */
|
|
442
|
+
export function frameContains(f: Frame, x: number, y: number): boolean {
|
|
443
|
+
return x >= f.x && y >= f.y && x <= f.x + f.width && y <= f.y + f.height;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** A measure-resolved hit: the deepest tagged host whose frame holds the tap. */
|
|
447
|
+
interface MeasuredHit {
|
|
448
|
+
fiber: FiberLike;
|
|
449
|
+
loc: Loc;
|
|
450
|
+
name: string | null;
|
|
451
|
+
frame: Frame;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Measure a host fiber's on-screen rect via its public instance's
|
|
456
|
+
* `measureInWindow` (window coordinates). Guarded + timed-out like
|
|
457
|
+
* {@link measureFrame}; resolves null for a non-host fiber, a missing instance,
|
|
458
|
+
* or a zero-size / never-firing measure. The RN-runtime half of the measure
|
|
459
|
+
* fallback — not unit-tested (see {@link measureHitTest}, which injects this).
|
|
460
|
+
*/
|
|
461
|
+
function measureFiberInWindow(fiber: FiberLike): Promise<Frame | null> {
|
|
462
|
+
const inst = fiber.stateNode?.canonical?.publicInstance as
|
|
463
|
+
| { measureInWindow?: (cb: (x: number, y: number, w: number, h: number) => void) => void }
|
|
464
|
+
| undefined;
|
|
465
|
+
const measure = inst?.measureInWindow;
|
|
466
|
+
if (typeof measure !== 'function') return Promise.resolve(null);
|
|
467
|
+
return new Promise((resolve) => {
|
|
468
|
+
let settled = false;
|
|
469
|
+
const finish = (f: Frame | null) => {
|
|
470
|
+
if (!settled) {
|
|
471
|
+
settled = true;
|
|
472
|
+
resolve(f);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
const timer = setTimeout(() => finish(null), 120);
|
|
476
|
+
try {
|
|
477
|
+
measure.call(inst, (x, y, width, height) => {
|
|
478
|
+
clearTimeout(timer);
|
|
479
|
+
finish(width > 0 || height > 0 ? { x, y, width, height } : null);
|
|
480
|
+
});
|
|
481
|
+
} catch {
|
|
482
|
+
clearTimeout(timer);
|
|
483
|
+
finish(null);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Measure-based hit-test — the fallback for when RN's geometric
|
|
490
|
+
* `findNodeAtPoint` can't descend to the tapped element.
|
|
491
|
+
*
|
|
492
|
+
* `react-native-pager-view` (and anything that hosts content in a native
|
|
493
|
+
* container) detaches a page's *native* views from the Fabric shadow tree
|
|
494
|
+
* `findNodeAtPoint` walks, so the native hit-test bottoms out at the page
|
|
495
|
+
* wrapper and never reaches the widget. But the React **fiber** tree is intact
|
|
496
|
+
* and the widgets are on-screen, hence measurable — so we hit-test ourselves:
|
|
497
|
+
* DFS the fiber subtree under `root`, measuring each host, and keep the DEEPEST
|
|
498
|
+
* tagged host whose window frame contains the tap.
|
|
499
|
+
*
|
|
500
|
+
* Pruned like the browser's `elementFromPoint`: a host whose frame misses the
|
|
501
|
+
* point can't have a (non-overflowing) descendant that hits, so its subtree is
|
|
502
|
+
* skipped. Composite / fragment fibers carry no frame, so we always descend
|
|
503
|
+
* through them to reach their hosts. `measure` is injected so the traversal is
|
|
504
|
+
* unit-testable without an RN runtime.
|
|
505
|
+
*/
|
|
506
|
+
export async function measureHitTest(
|
|
507
|
+
root: FiberLike | null,
|
|
508
|
+
x: number,
|
|
509
|
+
y: number,
|
|
510
|
+
measure: (fiber: FiberLike) => Promise<Frame | null>,
|
|
511
|
+
): Promise<MeasuredHit | null> {
|
|
512
|
+
let best: MeasuredHit | null = null;
|
|
513
|
+
let bestDepth = -1;
|
|
514
|
+
|
|
515
|
+
async function visitChildren(parent: FiberLike, depth: number): Promise<void> {
|
|
516
|
+
let n = 0;
|
|
517
|
+
for (let node = parent.child ?? null; node && n < 100_000; node = node.sibling ?? null, n++) {
|
|
518
|
+
await visitNode(node, depth);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function visitNode(node: FiberLike, depth: number): Promise<void> {
|
|
523
|
+
if (node.tag === HOST_COMPONENT) {
|
|
524
|
+
const frame = await measure(node);
|
|
525
|
+
// A host that doesn't contain the point prunes its whole subtree.
|
|
526
|
+
if (!frame || !frameContains(frame, x, y)) return;
|
|
527
|
+
const loc = paLocOf(node.memoizedProps);
|
|
528
|
+
// Deepest tagged host wins; on a tie the later (over-painted) sibling does.
|
|
529
|
+
if (loc && depth >= bestDepth) {
|
|
530
|
+
best = { fiber: node, loc, name: compOf(node.memoizedProps), frame };
|
|
531
|
+
bestDepth = depth;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
await visitChildren(node, depth + 1);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (root) await visitNode(root, 0);
|
|
538
|
+
return best;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The chain of distinctly-located tagged hosts from `leaf` up to the root,
|
|
543
|
+
* root-first (matching {@link crumbsOf}'s order). Built from the fiber `return`
|
|
544
|
+
* chain for the measure-fallback breadcrumb. Consecutive hosts sharing a
|
|
545
|
+
* `data-pa-loc` (a forwarding wrapper re-emitting the call site) collapse to a
|
|
546
|
+
* single crumb; names come from the babel plugin's `data-pa-comp`. Pure over a
|
|
547
|
+
* fiber-like chain (capped against a malformed `return` cycle); exported for
|
|
548
|
+
* unit testing.
|
|
549
|
+
*/
|
|
550
|
+
export function taggedAncestors(
|
|
551
|
+
leaf: FiberLike | null,
|
|
552
|
+
): { name: string; loc: Loc; fiber: FiberLike }[] {
|
|
553
|
+
const out: { name: string; loc: Loc; fiber: FiberLike }[] = [];
|
|
554
|
+
const seen = new Set<string>();
|
|
555
|
+
for (let f = leaf, i = 0; f && i < 10_000; f = f.return ?? null, i++) {
|
|
556
|
+
if (f.tag !== HOST_COMPONENT) continue;
|
|
557
|
+
const loc = paLocOf(f.memoizedProps);
|
|
558
|
+
if (!loc) continue;
|
|
559
|
+
const key = `${loc.file}:${loc.line}:${loc.col}`;
|
|
560
|
+
if (seen.has(key)) continue;
|
|
561
|
+
seen.add(key);
|
|
562
|
+
out.push({ name: compOf(f.memoizedProps) ?? 'View', loc, fiber: f });
|
|
563
|
+
}
|
|
564
|
+
return out.reverse();
|
|
565
|
+
}
|
|
566
|
+
|
|
374
567
|
/**
|
|
375
568
|
* Resolve a tap (in window coordinates) to a {@link PickResult}.
|
|
376
569
|
*
|
|
@@ -408,42 +601,62 @@ export function resolvePick(
|
|
|
408
601
|
try {
|
|
409
602
|
fn(inspectedView, x, y, (data) => {
|
|
410
603
|
clearTimeout(timer);
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
604
|
+
void (async () => {
|
|
605
|
+
const frame = data.frame
|
|
606
|
+
? {
|
|
607
|
+
x: data.frame.left,
|
|
608
|
+
y: data.frame.top,
|
|
609
|
+
width: data.frame.width,
|
|
610
|
+
height: data.frame.height,
|
|
611
|
+
}
|
|
612
|
+
: null;
|
|
613
|
+
const rawCrumbs = crumbsOf(data);
|
|
614
|
+
const loc = pickLoc(data, projectRoot);
|
|
615
|
+
const nameChain = nameChainOf(data);
|
|
616
|
+
|
|
617
|
+
// Measure fallback: when RN's native `findNodeAtPoint` couldn't
|
|
618
|
+
// descend to a tagged element (`tappedLeafLoc` is null — e.g.
|
|
619
|
+
// react-native-pager-view detaches its pages from the Fabric shadow
|
|
620
|
+
// tree the hit-test walks, so it bottoms out at the page wrapper),
|
|
621
|
+
// resolve the tap by measuring the still-intact fiber subtree under
|
|
622
|
+
// the touched host. Skipped entirely when the native pick already
|
|
623
|
+
// found the leaf, so every non-pager screen keeps its existing path.
|
|
624
|
+
if (!tappedLeafLoc(data)) {
|
|
625
|
+
const root = getHandleFromPublicInstance(data.closestPublicInstance);
|
|
626
|
+
const hit = root ? await measureHitTest(root, x, y, measureFiberInWindow) : null;
|
|
627
|
+
if (hit) {
|
|
628
|
+
const ancestors = taggedAncestors(hit.fiber);
|
|
629
|
+
const crumbFrames = await Promise.all(
|
|
630
|
+
ancestors.map((a) => measureFiberInWindow(a.fiber)),
|
|
631
|
+
);
|
|
632
|
+
done({
|
|
633
|
+
loc: hit.loc,
|
|
634
|
+
nameChain: ancestors.map((a) => a.name),
|
|
635
|
+
chain: ancestors.map((a, i) => ({
|
|
636
|
+
name: a.name,
|
|
637
|
+
loc: a.loc,
|
|
638
|
+
frame: crumbFrames[i] ?? null,
|
|
639
|
+
})),
|
|
640
|
+
frame: hit.frame,
|
|
641
|
+
});
|
|
642
|
+
return;
|
|
417
643
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
frame: frames[i] ?? null,
|
|
433
|
-
})),
|
|
434
|
-
frame,
|
|
435
|
-
});
|
|
436
|
-
})
|
|
437
|
-
.catch(() => {
|
|
438
|
-
// Measuring failed wholesale — still return the pick without
|
|
439
|
-
// per-crumb frames rather than hang.
|
|
440
|
-
done({
|
|
441
|
-
loc,
|
|
442
|
-
nameChain,
|
|
443
|
-
chain: rawCrumbs.map((c) => ({ name: c.name, loc: c.loc, frame: null })),
|
|
444
|
-
frame,
|
|
445
|
-
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Native pick: measure each crumb so pressing one can move the
|
|
647
|
+
// on-screen highlight to that ancestor. Concurrent, each guarded.
|
|
648
|
+
const frames = await Promise.all(rawCrumbs.map((c) => measureFrame(c.measure)));
|
|
649
|
+
done({
|
|
650
|
+
loc,
|
|
651
|
+
nameChain,
|
|
652
|
+
chain: rawCrumbs.map((c, i) => ({
|
|
653
|
+
name: c.name,
|
|
654
|
+
loc: c.loc,
|
|
655
|
+
frame: frames[i] ?? null,
|
|
656
|
+
})),
|
|
657
|
+
frame,
|
|
446
658
|
});
|
|
659
|
+
})().catch(() => done({ loc: null, nameChain: [], chain: [], frame: null }));
|
|
447
660
|
});
|
|
448
661
|
} catch {
|
|
449
662
|
clearTimeout(timer);
|