@buoy-gg/focus-inspector 7.0.41 → 7.0.42

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.
Files changed (52) hide show
  1. package/lib/commonjs/detect/detectors.js +1 -369
  2. package/lib/commonjs/index.js +1 -106
  3. package/lib/commonjs/observe/focusEventObserver.js +1 -105
  4. package/lib/commonjs/observe/keyEventObserver.js +1 -94
  5. package/lib/commonjs/observe/keyEventObserver.web.js +1 -37
  6. package/lib/commonjs/observe/rawEventEmitter.js +1 -44
  7. package/lib/commonjs/observe/rawEventEmitter.web.js +1 -13
  8. package/lib/commonjs/scan/focusableScanner.js +1 -502
  9. package/lib/commonjs/store/focusStore.js +1 -325
  10. package/lib/commonjs/sync/focusInspectorSyncAdapter.js +1 -254
  11. package/lib/module/detect/detectors.js +1 -359
  12. package/lib/module/index.js +1 -36
  13. package/lib/module/observe/focusEventObserver.js +1 -99
  14. package/lib/module/observe/keyEventObserver.js +1 -86
  15. package/lib/module/observe/keyEventObserver.web.js +1 -30
  16. package/lib/module/observe/rawEventEmitter.js +1 -40
  17. package/lib/module/observe/rawEventEmitter.web.js +1 -4
  18. package/lib/module/scan/focusableScanner.js +1 -497
  19. package/lib/module/store/focusStore.js +1 -312
  20. package/lib/module/sync/focusInspectorSyncAdapter.js +1 -251
  21. package/lib/web/index.mjs +1 -2
  22. package/package.json +2 -2
  23. package/lib/commonjs/detect/detectors.js.map +0 -1
  24. package/lib/commonjs/index.js.map +0 -1
  25. package/lib/commonjs/observe/focusEventObserver.js.map +0 -1
  26. package/lib/commonjs/observe/keyEventObserver.js.map +0 -1
  27. package/lib/commonjs/observe/keyEventObserver.web.js.map +0 -1
  28. package/lib/commonjs/observe/rawEventEmitter.js.map +0 -1
  29. package/lib/commonjs/observe/rawEventEmitter.web.js.map +0 -1
  30. package/lib/commonjs/scan/focusableScanner.js.map +0 -1
  31. package/lib/commonjs/store/focusStore.js.map +0 -1
  32. package/lib/commonjs/sync/focusInspectorSyncAdapter.js.map +0 -1
  33. package/lib/module/detect/detectors.js.map +0 -1
  34. package/lib/module/index.js.map +0 -1
  35. package/lib/module/observe/focusEventObserver.js.map +0 -1
  36. package/lib/module/observe/keyEventObserver.js.map +0 -1
  37. package/lib/module/observe/keyEventObserver.web.js.map +0 -1
  38. package/lib/module/observe/rawEventEmitter.js.map +0 -1
  39. package/lib/module/observe/rawEventEmitter.web.js.map +0 -1
  40. package/lib/module/scan/focusableScanner.js.map +0 -1
  41. package/lib/module/store/focusStore.js.map +0 -1
  42. package/lib/module/sync/focusInspectorSyncAdapter.js.map +0 -1
  43. package/lib/typescript/detect/detectors.d.ts.map +0 -1
  44. package/lib/typescript/index.d.ts.map +0 -1
  45. package/lib/typescript/observe/focusEventObserver.d.ts.map +0 -1
  46. package/lib/typescript/observe/keyEventObserver.d.ts.map +0 -1
  47. package/lib/typescript/observe/keyEventObserver.web.d.ts.map +0 -1
  48. package/lib/typescript/observe/rawEventEmitter.d.ts.map +0 -1
  49. package/lib/typescript/observe/rawEventEmitter.web.d.ts.map +0 -1
  50. package/lib/typescript/scan/focusableScanner.d.ts.map +0 -1
  51. package/lib/typescript/store/focusStore.d.ts.map +0 -1
  52. package/lib/typescript/sync/focusInspectorSyncAdapter.d.ts.map +0 -1
@@ -1,497 +1 @@
1
- "use strict";
2
-
3
- import { getDOMNodeHandle, getDOMPublicInstance, isDOMElement } from "@buoy-gg/shared-ui/utils";
4
- /**
5
- * focusableScanner — a read-only fiber walk that inventories everything on
6
- * screen the TV focus engine could land on.
7
- *
8
- * Provably inert: it reads `memoizedProps` and calls `measureInWindow`. It adds
9
- * no views, sets no props, and calls no focus command — which matters more here
10
- * than anywhere else in Buoy, because on Android TV a single focusable view we
11
- * rendered would insert itself into the host app's focus order and change the
12
- * very navigation this tool measures.
13
- *
14
- * Traversal (multi-renderer roots, the Fabric/Paper/CLI-dev instance triad,
15
- * Offscreen + inactive-`react-native-screens` pruning, measure-with-a-hang-
16
- * guard) follows the house pattern from
17
- * `packages/highlight-updates/src/highlight-updates/utils/screenElements.ts`.
18
- * The repo convention is a per-package copy trimmed to the tool's needs rather
19
- * than a shared walker library — see packages/CLAUDE.md.
20
- *
21
- * There is no JS API for "which view has focus" and none for "if focus is here
22
- * and the user presses RIGHT, where does it go" (Android has
23
- * `FabricUIManager.findNextFocusableElement` natively but never exposes it to
24
- * JS; tvOS's UIKit engine decides at runtime and answers nothing). So this
25
- * inventory is the *candidate set* — the observed transition stream is what
26
- * says which of them focus actually reaches.
27
- */
28
-
29
- import { Dimensions } from "react-native";
30
- const OffscreenComponent = 22;
31
-
32
- /** Host wrappers whose own name says nothing — fall through to the owner. */
33
- const INTERNAL_NAMES = new Set(["View", "RCTView", "ScrollView", "RCTScrollView", "RCTScrollContentView", "SafeAreaView", "RCTSafeAreaView", "AndroidHorizontalScrollContentView", "KeyboardAvoidingView", "Pressable", "PlatformPressable", "TouchableOpacity", "TouchableHighlight", "TouchableWithoutFeedback", "TouchableNativeFeedback", "AnimatedComponent", "Wrapper"]);
34
-
35
- /** The composite that renders an invisible focus stop. */
36
- const GUIDE_NAME = "TVFocusGuideView";
37
- // --- fiber plumbing -------------------------------------------------------
38
-
39
- function getReactDevToolsHook() {
40
- if (typeof global === "undefined") return null;
41
- return global.__REACT_DEVTOOLS_GLOBAL_HOOK__ ?? null;
42
- }
43
-
44
- /**
45
- * Roots across ALL renderers. A single renderer id is not safe to assume:
46
- * secondary reconcilers (skia, svg) register their own, sometimes first.
47
- */
48
- function getAllFiberRoots() {
49
- const hook = getReactDevToolsHook();
50
- if (!hook) return [];
51
- const roots = [];
52
- try {
53
- if (hook.renderers && typeof hook.renderers.forEach === "function") {
54
- hook.renderers.forEach((_r, rendererId) => {
55
- try {
56
- const set = hook.getFiberRoots?.(rendererId);
57
- if (set) set.forEach(root => roots.push(root));
58
- } catch {
59
- /* one renderer failing must not lose the others */
60
- }
61
- });
62
- }
63
- if (roots.length === 0 && hook.getFiberRoots) {
64
- const legacy = hook.getFiberRoots(1);
65
- if (legacy) legacy.forEach(root => roots.push(root));
66
- }
67
- } catch {
68
- /* ignore */
69
- }
70
- return roots;
71
- }
72
- function isHiddenOffscreen(fiber) {
73
- return fiber.tag === OffscreenComponent && fiber.memoizedState !== null;
74
- }
75
- function isInactiveScreen(fiber) {
76
- // react-native-screens: activityState 0/1 = inactive/transitioning. A card
77
- // sitting behind the current screen still has focusable props but is not a
78
- // focus target — counting it would tank the coverage number forever.
79
- const activityState = fiber.memoizedProps?.activityState;
80
- if (activityState === 0 || activityState === 1) return true;
81
- if (fiber.memoizedProps?.isPageFocused === false) return true;
82
- return false;
83
- }
84
- function getProps(fiber) {
85
- return fiber?.memoizedProps ?? fiber?.pendingProps ?? null;
86
- }
87
- function getComponentName(fiber) {
88
- const type = fiber?.type;
89
- if (!type) return null;
90
- if (typeof type === "string") return type;
91
- return type.displayName || type.name || null;
92
- }
93
- function getOwningComponentName(fiber) {
94
- let cur = fiber?._debugOwner || fiber?.return;
95
- let depth = 0;
96
- while (cur && depth < 30) {
97
- depth++;
98
- const name = getComponentName(cur);
99
- if (name && !INTERNAL_NAMES.has(name) && typeof cur.type !== "string") return name;
100
- cur = cur.return;
101
- }
102
- return null;
103
- }
104
- /** The Fabric / Paper / RN-CLI-dev instance triad — always check all three. */
105
- function getPublicInstance(stateNode) {
106
- const dom = getDOMPublicInstance(stateNode);
107
- if (dom) return dom;
108
- if (!stateNode) return null;
109
- const node = stateNode;
110
- if (node.canonical?.publicInstance) return node.canonical.publicInstance;
111
- if (node.canonical && typeof node.canonical.measure === "function") return node.canonical;
112
- if (typeof node.measure === "function" || typeof node.measureInWindow === "function") {
113
- return node;
114
- }
115
- return null;
116
- }
117
- function getNativeTag(stateNode) {
118
- const dom = getDOMNodeHandle(stateNode);
119
- if (dom !== null) return dom;
120
- if (stateNode == null) return null;
121
- const n = stateNode;
122
- if (typeof n.__nativeTag === "number") return n.__nativeTag;
123
- if (typeof n._nativeTag === "number") return n._nativeTag;
124
- if (typeof n.nativeTag === "number") return n.nativeTag;
125
- if (n.canonical) {
126
- if (typeof n.canonical.__nativeTag === "number") return n.canonical.__nativeTag;
127
- if (typeof n.canonical._nativeTag === "number") return n.canonical._nativeTag;
128
- if (typeof n.canonical.nativeTag === "number") return n.canonical.nativeTag;
129
- }
130
- return null;
131
- }
132
-
133
- /** First host node at or below a fiber (self first, then a bounded subtree DFS). */
134
- function findHostInstance(fiber) {
135
- const selfInstance = getPublicInstance(fiber.stateNode);
136
- if (selfInstance) {
137
- return {
138
- instance: selfInstance,
139
- nativeTag: getNativeTag(fiber.stateNode)
140
- };
141
- }
142
- const stack = [];
143
- if (fiber.child) stack.push(fiber.child);
144
- let guard = 0;
145
- while (stack.length > 0 && guard < 4000) {
146
- guard++;
147
- const f = stack.pop();
148
- if (!f) continue;
149
- const instance = getPublicInstance(f.stateNode);
150
- if (instance) return {
151
- instance,
152
- nativeTag: getNativeTag(f.stateNode)
153
- };
154
- if (f.child) stack.push(f.child);
155
- if (f.sibling) stack.push(f.sibling);
156
- }
157
- return null;
158
- }
159
-
160
- // --- candidate predicate --------------------------------------------------
161
-
162
- /**
163
- * The props that make a view a focus candidate, per the fork's own mapping:
164
- * on iOS `View` writes `focusable ?? isTVSelectable` into `isTVSelectable` and
165
- * DELETES `focusable`; on Android it writes `focusable` and deletes
166
- * `isTVSelectable`. So both names have to be accepted — the host fiber only
167
- * ever carries one of them, and which one depends on the platform.
168
- */
169
- function focusableFlag(props) {
170
- if (typeof props.isTVSelectable === "boolean") return props.isTVSelectable;
171
- if (typeof props.focusable === "boolean") return props.focusable;
172
- if (typeof props.tvFocusable === "boolean") return props.tvFocusable;
173
- return undefined;
174
- }
175
- function nextFocusFlags(props) {
176
- const up = typeof props.nextFocusUp === "number";
177
- const down = typeof props.nextFocusDown === "number";
178
- const left = typeof props.nextFocusLeft === "number";
179
- const right = typeof props.nextFocusRight === "number";
180
- if (!up && !down && !left && !right) return undefined;
181
- return {
182
- up: up || undefined,
183
- down: down || undefined,
184
- left: left || undefined,
185
- right: right || undefined
186
- };
187
- }
188
- function getVisibleText(props) {
189
- if (!props) return undefined;
190
- const pick = value => {
191
- if (typeof value === "string") return value.trim() ? value.trim().slice(0, 60) : undefined;
192
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
193
- return undefined;
194
- };
195
- return pick(props.children) ?? pick(props.title) ?? pick(props.label) ?? undefined;
196
- }
197
-
198
- /** A focusable's own label, gathered from descendants without crossing into another stop. */
199
- function collectDescendantText(root, maxDepth) {
200
- const parts = [];
201
- const visit = (fiber, depth) => {
202
- if (!fiber || depth > maxDepth || parts.length >= 3) return;
203
- if (fiber !== root) {
204
- const props = getProps(fiber);
205
- if (props && typeof props.onPress === "function") return;
206
- }
207
- const text = getVisibleText(getProps(fiber));
208
- if (text && !parts.includes(text)) parts.push(text);
209
- if (fiber.child) visit(fiber.child, depth + 1);
210
- if (fiber !== root && fiber.sibling) visit(fiber.sibling, depth);
211
- };
212
- if (root.child) visit(root.child, 1);
213
- const joined = parts.join(" ").trim();
214
- return joined ? joined.slice(0, 60) : undefined;
215
- }
216
-
217
- // --- measurement ----------------------------------------------------------
218
-
219
- function measureInstance(instance) {
220
- return new Promise(resolve => {
221
- let settled = false;
222
- const done = rect => {
223
- if (settled) return;
224
- settled = true;
225
- resolve(rect);
226
- };
227
- // Detached / off-screen nodes can fire NEITHER callback — without this the
228
- // whole scan would hang on one stale node.
229
- setTimeout(() => done(null), 1000);
230
- try {
231
- if (typeof instance.measureInWindow === "function") {
232
- instance.measureInWindow((x, y, width, height) => {
233
- done(width > 0 && height > 0 ? {
234
- x,
235
- y,
236
- width,
237
- height
238
- } : null);
239
- });
240
- } else if (typeof instance.measure === "function") {
241
- instance.measure((_x, _y, width, height, pageX, pageY) => {
242
- done(width > 0 && height > 0 && pageX != null && pageY != null ? {
243
- x: pageX,
244
- y: pageY,
245
- width,
246
- height
247
- } : null);
248
- });
249
- } else {
250
- done(null);
251
- }
252
- } catch {
253
- done(null);
254
- }
255
- });
256
- }
257
-
258
- // --- collection -----------------------------------------------------------
259
-
260
- /**
261
- * A live handle on the element behind a native tag, kept out of the wire
262
- * snapshot. `focusElement` needs the public INSTANCE (Commands take instance
263
- * refs, never tags), so the last scan's instances are the lookup table.
264
- */
265
- const instancesByTag = new Map();
266
- function walk(fiber, context, visit) {
267
- const stack = [{
268
- fiber,
269
- context,
270
- depth: 0
271
- }];
272
- const seen = new Set();
273
- while (stack.length > 0) {
274
- const entry = stack.pop();
275
- if (!entry) continue;
276
- const {
277
- fiber: current,
278
- depth
279
- } = entry;
280
- if (!current || seen.has(current) || depth > 500) continue;
281
- seen.add(current);
282
- if (isHiddenOffscreen(current) || isInactiveScreen(current)) {
283
- // Prune the subtree, keep walking siblings.
284
- if (current.sibling) {
285
- stack.push({
286
- fiber: current.sibling,
287
- context: entry.context,
288
- depth
289
- });
290
- }
291
- continue;
292
- }
293
- const childContext = visit(current, entry.context);
294
- if (current.sibling) stack.push({
295
- fiber: current.sibling,
296
- context: entry.context,
297
- depth
298
- });
299
- if (current.child) stack.push({
300
- fiber: current.child,
301
- context: childContext,
302
- depth: depth + 1
303
- });
304
- }
305
- }
306
- function collectCandidates() {
307
- const roots = getAllFiberRoots();
308
- if (roots.length === 0) return [];
309
- const out = [];
310
- const seenTags = new Set();
311
- for (const root of roots) {
312
- walk(root.current, {
313
- guideTag: null
314
- }, (fiber, context) => {
315
- const props = getProps(fiber);
316
- if (!props) return context;
317
- const componentName = getComponentName(fiber);
318
- const isGuide = componentName === GUIDE_NAME;
319
- const flag = focusableFlag(props);
320
- const pressable = typeof props.onPress === "function" || typeof props.onLongPress === "function";
321
- const hasPreferred = props.hasTVPreferredFocus === true;
322
- const nextFocus = nextFocusFlags(props);
323
-
324
- // A composite Pressable and its host View both match; the host is the one
325
- // that carries the native tag focus events report, so dedupe on the tag.
326
- const isCandidate = isGuide || flag === true || hasPreferred || !!nextFocus || pressable && flag !== false || isDOMElement(fiber.stateNode) && fiber.stateNode.matches('a[href],button,input,select,textarea,[tabindex],[contenteditable="true"]') && !fiber.stateNode.matches(':disabled,[inert], [tabindex="-1"]');
327
- if (!isCandidate) return context;
328
- const host = findHostInstance(fiber);
329
- const nativeTag = host?.nativeTag ?? null;
330
- if (nativeTag === null) return context;
331
- const nextContext = isGuide ? {
332
- guideTag: nativeTag
333
- } : context;
334
- if (seenTags.has(nativeTag)) {
335
- // Merge the richer signal onto the row already recorded for this tag —
336
- // the composite knows the name, the host knows the focus props.
337
- const existing = out.find(c => c.node.nativeTag === nativeTag);
338
- if (existing) {
339
- if (flag !== undefined && existing.node.focusProps.focusable === undefined) {
340
- existing.node.focusProps.focusable = flag;
341
- }
342
- if (hasPreferred) existing.node.focusProps.hasTVPreferredFocus = true;
343
- if (nextFocus && !existing.node.focusProps.nextFocus) {
344
- existing.node.focusProps.nextFocus = nextFocus;
345
- }
346
- if (pressable) existing.node.pressable = true;
347
- if (!existing.node.testID && props.testID) existing.node.testID = props.testID;
348
- if (isGuide && !existing.node.guide) {
349
- existing.node.guide = readGuide(props);
350
- existing.node.name = GUIDE_NAME;
351
- }
352
- }
353
- return nextContext;
354
- }
355
- seenTags.add(nativeTag);
356
- let name = componentName ?? "Unknown";
357
- if (!componentName || INTERNAL_NAMES.has(componentName) || typeof fiber.type === "string") {
358
- name = getOwningComponentName(fiber) ?? componentName ?? "Unknown";
359
- }
360
- const label = props.accessibilityLabel || props["aria-label"] || undefined;
361
- const text = getVisibleText(props) ?? (pressable ? collectDescendantText(fiber, 6) : undefined);
362
- const node = {
363
- nativeTag,
364
- name: isGuide ? GUIDE_NAME : name,
365
- owner: getOwningComponentName(fiber) ?? undefined,
366
- testID: props.testID || props["data-testid"] || undefined,
367
- label,
368
- text,
369
- focusProps: {
370
- focusable: flag,
371
- hasTVPreferredFocus: hasPreferred || undefined,
372
- nextFocus
373
- },
374
- guide: isGuide ? readGuide(props) : undefined,
375
- guideAncestorTag: context.guideTag ?? undefined,
376
- pressable: pressable || undefined
377
- };
378
- if (host?.instance) instancesByTag.set(nativeTag, host.instance);
379
- out.push({
380
- node,
381
- instance: host?.instance ?? null
382
- });
383
- return nextContext;
384
- });
385
- }
386
- return out;
387
- }
388
- function readGuide(props) {
389
- return {
390
- autoFocus: props.autoFocus === true,
391
- hasDestinations: Array.isArray(props.destinations) && props.destinations.length > 0,
392
- enabled: props.enabled !== false,
393
- trap: {
394
- up: props.trapFocusUp === true,
395
- down: props.trapFocusDown === true,
396
- left: props.trapFocusLeft === true,
397
- right: props.trapFocusRight === true
398
- }
399
- };
400
- }
401
- function round3(n) {
402
- return Math.round(n * 1000) / 1000;
403
- }
404
-
405
- /** Cap the inventory so a pathological screen can't blow the snapshot budget. */
406
- const MAX_NODES = 400;
407
- export async function scanFocusables() {
408
- instancesByTag.clear();
409
- const candidates = collectCandidates();
410
- await Promise.all(candidates.map(async candidate => {
411
- if (!candidate.instance) return;
412
- const rect = await measureInstance(candidate.instance);
413
- if (!rect) return;
414
- candidate.node.frame = rect;
415
- }));
416
- const window = Dimensions.get("window");
417
- const screenW = Math.round(window.width) || 1;
418
- const screenH = Math.round(window.height) || 1;
419
- const nodes = [];
420
- for (const candidate of candidates) {
421
- const rect = candidate.node.frame;
422
- if (rect) {
423
- // A guide's own box is often a 1x1 sliver (its StyleSheet minWidth), so
424
- // there is no minimum-size filter here — the invisible stop IS the point.
425
- //
426
- // Off-window elements are KEPT, unlike in the describe-screen walker this
427
- // is adapted from. A tile below a ScrollView's fold is still somewhere the
428
- // D-pad goes (the engine scrolls to it), so dropping it would under-report
429
- // the coverage denominator — the tool's headline number — and would make
430
- // rows vanish from the inventory as the user scrolls. Previous router
431
- // screens are already pruned by the inactive-`react-native-screens` check
432
- // during the walk, which is the case the drop existed for.
433
- const cx = rect.x + rect.width / 2;
434
- const cy = rect.y + rect.height / 2;
435
- candidate.node.onScreen = cy >= 0 && cy <= screenH && cx >= 0 && cx <= screenW;
436
- candidate.node.frame = {
437
- x: round3(rect.x / screenW),
438
- y: round3(rect.y / screenH),
439
- width: round3(rect.width / screenW),
440
- height: round3(rect.height / screenH)
441
- };
442
- }
443
- nodes.push(candidate.node);
444
- if (nodes.length >= MAX_NODES) break;
445
- }
446
-
447
- // Reading order, so the inventory table matches how a person scans the screen.
448
- nodes.sort((a, b) => {
449
- const ay = a.frame?.y ?? 2;
450
- const by = b.frame?.y ?? 2;
451
- if (Math.abs(ay - by) > 0.01) return ay - by;
452
- return (a.frame?.x ?? 2) - (b.frame?.x ?? 2);
453
- });
454
- return {
455
- screen: {
456
- width: screenW,
457
- height: screenH
458
- },
459
- nodes,
460
- scannedAt: Date.now()
461
- };
462
- }
463
-
464
- /**
465
- * Move focus to a native tag. Source-verified only until this runs on a device:
466
- * `Commands.requestTVFocus` takes an instance ref, never a tag, so the last
467
- * scan's instance table is the only way to get from one to the other.
468
- */
469
- export function requestFocusForTag(nativeTag) {
470
- const instance = instancesByTag.get(nativeTag);
471
- if (!instance) {
472
- return {
473
- ok: false,
474
- reason: "Unknown tag — rescan and try again."
475
- };
476
- }
477
- if (typeof instance.requestTVFocus !== "function") {
478
- // `requestTVFocus` is attached by RN's View to its instance; a host that
479
- // isn't a View (Text, Image) never gets one.
480
- return {
481
- ok: false,
482
- reason: "This element exposes no requestTVFocus()."
483
- };
484
- }
485
- try {
486
- instance.requestTVFocus();
487
- return {
488
- ok: true
489
- };
490
- } catch (e) {
491
- return {
492
- ok: false,
493
- reason: e?.message ?? "requestTVFocus() threw."
494
- };
495
- }
496
- }
497
- //# sourceMappingURL=focusableScanner.js.map
1
+ "use strict";import{getDOMNodeHandle as O,getDOMPublicInstance as R,isDOMElement as E}from"@buoy-gg/shared-ui/utils";import{Dimensions as M}from"react-native";const H=22;const x=new Set(["View","RCTView","ScrollView","RCTScrollView","RCTScrollContentView","SafeAreaView","RCTSafeAreaView","AndroidHorizontalScrollContentView","KeyboardAvoidingView","Pressable","PlatformPressable","TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback","AnimatedComponent","Wrapper"]);const y="TVFocusGuideView";function L(){if(typeof global==="undefined")return null;return global.__REACT_DEVTOOLS_GLOBAL_HOOK__??null}function U(){const e=L();if(!e)return[];const o=[];try{if(e.renderers&&typeof e.renderers.forEach==="function"){e.renderers.forEach((n,r)=>{try{const s=e.getFiberRoots?.(r);if(s)s.forEach(i=>o.push(i))}catch{}})}if(o.length===0&&e.getFiberRoots){const n=e.getFiberRoots(1);if(n)n.forEach(r=>o.push(r))}}catch{}return o}function W(e){return e.tag===H&&e.memoizedState!==null}function q(e){const o=e.memoizedProps?.activityState;if(o===0||o===1)return true;if(e.memoizedProps?.isPageFocused===false)return true;return false}function b(e){return e?.memoizedProps??e?.pendingProps??null}function P(e){const o=e?.type;if(!o)return null;if(typeof o==="string")return o;return o.displayName||o.name||null}function _(e){let o=e?._debugOwner||e?.return;let n=0;while(o&&n<30){n++;const r=P(o);if(r&&!x.has(r)&&typeof o.type!=="string")return r;o=o.return}return null}function v(e){const o=R(e);if(o)return o;if(!e)return null;const n=e;if(n.canonical?.publicInstance)return n.canonical.publicInstance;if(n.canonical&&typeof n.canonical.measure==="function")return n.canonical;if(typeof n.measure==="function"||typeof n.measureInWindow==="function"){return n}return null}function V(e){const o=O(e);if(o!==null)return o;if(e==null)return null;const n=e;if(typeof n.__nativeTag==="number")return n.__nativeTag;if(typeof n._nativeTag==="number")return n._nativeTag;if(typeof n.nativeTag==="number")return n.nativeTag;if(n.canonical){if(typeof n.canonical.__nativeTag==="number")return n.canonical.__nativeTag;if(typeof n.canonical._nativeTag==="number")return n.canonical._nativeTag;if(typeof n.canonical.nativeTag==="number")return n.canonical.nativeTag}return null}function z(e){const o=v(e.stateNode);if(o){return{instance:o,nativeTag:V(e.stateNode)}}const n=[];if(e.child)n.push(e.child);let r=0;while(n.length>0&&r<4e3){r++;const s=n.pop();if(!s)continue;const i=v(s.stateNode);if(i)return{instance:i,nativeTag:V(s.stateNode)};if(s.child)n.push(s.child);if(s.sibling)n.push(s.sibling)}return null}function G(e){if(typeof e.isTVSelectable==="boolean")return e.isTVSelectable;if(typeof e.focusable==="boolean")return e.focusable;if(typeof e.tvFocusable==="boolean")return e.tvFocusable;return void 0}function j(e){const o=typeof e.nextFocusUp==="number";const n=typeof e.nextFocusDown==="number";const r=typeof e.nextFocusLeft==="number";const s=typeof e.nextFocusRight==="number";if(!o&&!n&&!r&&!s)return void 0;return{up:o||void 0,down:n||void 0,left:r||void 0,right:s||void 0}}function S(e){if(!e)return void 0;const o=n=>{if(typeof n==="string")return n.trim()?n.trim().slice(0,60):void 0;if(typeof n==="number"&&Number.isFinite(n))return String(n);return void 0};return o(e.children)??o(e.title)??o(e.label)??void 0}function B(e,o){const n=[];const r=(i,t)=>{if(!i||t>o||n.length>=3)return;if(i!==e){const c=b(i);if(c&&typeof c.onPress==="function")return}const u=S(b(i));if(u&&!n.includes(u))n.push(u);if(i.child)r(i.child,t+1);if(i!==e&&i.sibling)r(i.sibling,t)};if(e.child)r(e.child,1);const s=n.join(" ").trim();return s?s.slice(0,60):void 0}function K(e){return new Promise(o=>{let n=false;const r=s=>{if(n)return;n=true;o(s)};setTimeout(()=>r(null),1e3);try{if(typeof e.measureInWindow==="function"){e.measureInWindow((s,i,t,u)=>{r(t>0&&u>0?{x:s,y:i,width:t,height:u}:null)})}else if(typeof e.measure==="function"){e.measure((s,i,t,u,c,f)=>{r(t>0&&u>0&&c!=null&&f!=null?{x:c,y:f,width:t,height:u}:null)})}else{r(null)}}catch{r(null)}})}const T=new Map;function J(e,o,n){const r=[{fiber:e,context:o,depth:0}];const s=new Set;while(r.length>0){const i=r.pop();if(!i)continue;const{fiber:t,depth:u}=i;if(!t||s.has(t)||u>500)continue;s.add(t);if(W(t)||q(t)){if(t.sibling){r.push({fiber:t.sibling,context:i.context,depth:u})}continue}const c=n(t,i.context);if(t.sibling)r.push({fiber:t.sibling,context:i.context,depth:u});if(t.child)r.push({fiber:t.child,context:c,depth:u+1})}}function Q(){const e=U();if(e.length===0)return[];const o=[];const n=new Set;for(const r of e){J(r.current,{guideTag:null},(s,i)=>{const t=b(s);if(!t)return i;const u=P(s);const c=u===y;const f=G(t);const d=typeof t.onPress==="function"||typeof t.onLongPress==="function";const p=t.hasTVPreferredFocus===true;const g=j(t);const I=c||f===true||p||!!g||d&&f!==false||E(s.stateNode)&&s.stateNode.matches('a[href],button,input,select,textarea,[tabindex],[contenteditable="true"]')&&!s.stateNode.matches(':disabled,[inert], [tabindex="-1"]');if(!I)return i;const h=z(s);const l=h?.nativeTag??null;if(l===null)return i;const w=c?{guideTag:l}:i;if(n.has(l)){const a=o.find(C=>C.node.nativeTag===l);if(a){if(f!==void 0&&a.node.focusProps.focusable===void 0){a.node.focusProps.focusable=f}if(p)a.node.focusProps.hasTVPreferredFocus=true;if(g&&!a.node.focusProps.nextFocus){a.node.focusProps.nextFocus=g}if(d)a.node.pressable=true;if(!a.node.testID&&t.testID)a.node.testID=t.testID;if(c&&!a.node.guide){a.node.guide=D(t);a.node.name=y}}return w}n.add(l);let F=u??"Unknown";if(!u||x.has(u)||typeof s.type==="string"){F=_(s)??u??"Unknown"}const k=t.accessibilityLabel||t["aria-label"]||void 0;const A=S(t)??(d?B(s,6):void 0);const N={nativeTag:l,name:c?y:F,owner:_(s)??void 0,testID:t.testID||t["data-testid"]||void 0,label:k,text:A,focusProps:{focusable:f,hasTVPreferredFocus:p||void 0,nextFocus:g},guide:c?D(t):void 0,guideAncestorTag:i.guideTag??void 0,pressable:d||void 0};if(h?.instance)T.set(l,h.instance);o.push({node:N,instance:h?.instance??null});return w})}return o}function D(e){return{autoFocus:e.autoFocus===true,hasDestinations:Array.isArray(e.destinations)&&e.destinations.length>0,enabled:e.enabled!==false,trap:{up:e.trapFocusUp===true,down:e.trapFocusDown===true,left:e.trapFocusLeft===true,right:e.trapFocusRight===true}}}function m(e){return Math.round(e*1e3)/1e3}const X=400;export async function scanFocusables(){T.clear();const e=Q();await Promise.all(e.map(async i=>{if(!i.instance)return;const t=await K(i.instance);if(!t)return;i.node.frame=t}));const o=M.get("window");const n=Math.round(o.width)||1;const r=Math.round(o.height)||1;const s=[];for(const i of e){const t=i.node.frame;if(t){const u=t.x+t.width/2;const c=t.y+t.height/2;i.node.onScreen=c>=0&&c<=r&&u>=0&&u<=n;i.node.frame={x:m(t.x/n),y:m(t.y/r),width:m(t.width/n),height:m(t.height/r)}}s.push(i.node);if(s.length>=X)break}s.sort((i,t)=>{const u=i.frame?.y??2;const c=t.frame?.y??2;if(Math.abs(u-c)>.01)return u-c;return(i.frame?.x??2)-(t.frame?.x??2)});return{screen:{width:n,height:r},nodes:s,scannedAt:Date.now()}}export function requestFocusForTag(e){const o=T.get(e);if(!o){return{ok:false,reason:"Unknown tag \u2014 rescan and try again."}}if(typeof o.requestTVFocus!=="function"){return{ok:false,reason:"This element exposes no requestTVFocus()."}}try{o.requestTVFocus();return{ok:true}}catch(n){return{ok:false,reason:n?.message??"requestTVFocus() threw."}}}