@buoy-gg/focus-inspector 7.0.40 → 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 (51) hide show
  1. package/README.md +4 -0
  2. package/lib/commonjs/detect/detectors.js +1 -351
  3. package/lib/commonjs/index.js +1 -106
  4. package/lib/commonjs/observe/focusEventObserver.js +1 -105
  5. package/lib/commonjs/observe/keyEventObserver.js +1 -94
  6. package/lib/commonjs/observe/keyEventObserver.web.js +1 -0
  7. package/lib/commonjs/observe/rawEventEmitter.js +1 -44
  8. package/lib/commonjs/observe/rawEventEmitter.web.js +1 -0
  9. package/lib/commonjs/scan/focusableScanner.js +1 -497
  10. package/lib/commonjs/store/focusStore.js +1 -325
  11. package/lib/commonjs/sync/focusInspectorSyncAdapter.js +1 -247
  12. package/lib/module/detect/detectors.js +1 -341
  13. package/lib/module/index.js +1 -36
  14. package/lib/module/observe/focusEventObserver.js +1 -99
  15. package/lib/module/observe/keyEventObserver.js +1 -86
  16. package/lib/module/observe/keyEventObserver.web.js +1 -0
  17. package/lib/module/observe/rawEventEmitter.js +1 -40
  18. package/lib/module/observe/rawEventEmitter.web.js +1 -0
  19. package/lib/module/scan/focusableScanner.js +1 -492
  20. package/lib/module/store/focusStore.js +1 -312
  21. package/lib/module/sync/focusInspectorSyncAdapter.js +1 -244
  22. package/lib/typescript/detect/detectors.d.ts +1 -1
  23. package/lib/typescript/observe/keyEventObserver.web.d.ts +6 -0
  24. package/lib/typescript/observe/rawEventEmitter.web.d.ts +3 -0
  25. package/lib/typescript/scan/focusableScanner.d.ts +0 -24
  26. package/lib/web/index.mjs +1 -0
  27. package/package.json +39 -5
  28. package/lib/commonjs/detect/detectors.js.map +0 -1
  29. package/lib/commonjs/index.js.map +0 -1
  30. package/lib/commonjs/observe/focusEventObserver.js.map +0 -1
  31. package/lib/commonjs/observe/keyEventObserver.js.map +0 -1
  32. package/lib/commonjs/observe/rawEventEmitter.js.map +0 -1
  33. package/lib/commonjs/scan/focusableScanner.js.map +0 -1
  34. package/lib/commonjs/store/focusStore.js.map +0 -1
  35. package/lib/commonjs/sync/focusInspectorSyncAdapter.js.map +0 -1
  36. package/lib/module/detect/detectors.js.map +0 -1
  37. package/lib/module/index.js.map +0 -1
  38. package/lib/module/observe/focusEventObserver.js.map +0 -1
  39. package/lib/module/observe/keyEventObserver.js.map +0 -1
  40. package/lib/module/observe/rawEventEmitter.js.map +0 -1
  41. package/lib/module/scan/focusableScanner.js.map +0 -1
  42. package/lib/module/store/focusStore.js.map +0 -1
  43. package/lib/module/sync/focusInspectorSyncAdapter.js.map +0 -1
  44. package/lib/typescript/detect/detectors.d.ts.map +0 -1
  45. package/lib/typescript/index.d.ts.map +0 -1
  46. package/lib/typescript/observe/focusEventObserver.d.ts.map +0 -1
  47. package/lib/typescript/observe/keyEventObserver.d.ts.map +0 -1
  48. package/lib/typescript/observe/rawEventEmitter.d.ts.map +0 -1
  49. package/lib/typescript/scan/focusableScanner.d.ts.map +0 -1
  50. package/lib/typescript/store/focusStore.d.ts.map +0 -1
  51. package/lib/typescript/sync/focusInspectorSyncAdapter.d.ts.map +0 -1
@@ -1,312 +1 @@
1
- "use strict";
2
-
3
- /**
4
- * focusStore — the device-side memory of the TV focus inspector.
5
- *
6
- * CONTRACT: this store only ever OBSERVES. It never adds a view, never sets a
7
- * prop, never calls a focus command on its own. On Android TV any focusable
8
- * view we rendered would become a D-pad stop in the host app's focus order, so
9
- * a tool that measures focus by participating in it would measure itself. The
10
- * only thing that ever moves focus is the explicit `focusElement` action a
11
- * human triggers from the dashboard.
12
- *
13
- * It is deliberately react-native-free: observers push events in, so every rule
14
- * below (transition pairing, direction attribution, dead-end probes) is a pure
15
- * function of a timestamped event stream and is unit-testable without a device.
16
- */
17
-
18
- /** The four D-pad directions. Select/menu never move focus, so they're dropped. */
19
-
20
- /**
21
- * One D-pad press, recorded against the element that held focus when it
22
- * happened, plus what it did. A probe with `movedTo: null` is one piece of
23
- * dead-end evidence; N consecutive ones in the same direction is a flag.
24
- */
25
-
26
- /**
27
- * Why nothing is reported as focused. These are NOT the same thing and must
28
- * never be rendered the same way:
29
- *
30
- * - "never-observed": we have not seen a single focus event yet. Focus is
31
- * almost certainly fine — there is no API for "which view has focus", so the
32
- * tool learns the answer only when focus MOVES. Attaching to an app that is
33
- * sitting still looks exactly like this until the first press.
34
- * - "lost": we WATCHED a view blur with nothing taking over. That is the real
35
- * "focus vanished" bug.
36
- */
37
-
38
- /**
39
- * A D-pad press and the focus pair it caused arrive in EITHER order, a few ms
40
- * apart — both `HWEvent → blur → focus` and `blur → focus → HWEvent` were
41
- * observed live on the same session. So direction attribution is a time-window
42
- * join in both directions, never a sequence assumption.
43
- */
44
- export const DIRECTION_WINDOW_MS = 250;
45
-
46
- /**
47
- * The window for crediting a press to a move that was ALREADY recorded — the
48
- * `blur → focus → HWEvent` ordering. Much tighter than the forward window on
49
- * purpose: forward attribution is waiting for a consequence, but backward
50
- * attribution is claiming a cause, and a press 200ms after a move is far more
51
- * likely to be the NEXT press than a late-delivered echo of the last one.
52
- * Delivery races were observed at a few ms.
53
- */
54
- export const BACKFILL_WINDOW_MS = 60;
55
-
56
- /**
57
- * How long a blur waits for its matching focus before we call it a loss. Focus
58
- * pairs land within a frame or two; anything past this really has left the RN
59
- * tree (system alert, another window, an unmounted subtree).
60
- */
61
- export const FOCUS_LOST_MS = 400;
62
- const MAX_TRANSITIONS = 300;
63
- const MAX_PROBES = 400;
64
- let currentTag = null;
65
- let currentSince = null;
66
- let transitions = [];
67
- let probes = [];
68
- let visited = new Set();
69
- let lostCount = 0;
70
- let transitionSeq = 0;
71
- let probeSeq = 0;
72
- let tracking = true;
73
-
74
- /** Blur is always dispatched before the matching focus — this is that half. */
75
- let pendingBlur = null;
76
- let lostTimer = null;
77
- const probeTimers = new Set();
78
- const listeners = new Set();
79
- function emitChange() {
80
- listeners.forEach(listener => {
81
- listener();
82
- });
83
- }
84
- function pushTransition(transition) {
85
- transitions = transitions.length >= MAX_TRANSITIONS ? [...transitions.slice(1), transition] : [...transitions, transition];
86
- }
87
- function pushProbe(probe) {
88
- probes = probes.length >= MAX_PROBES ? [...probes.slice(1), probe] : [...probes, probe];
89
- }
90
-
91
- /**
92
- * Credit a just-recorded transition to a press that arrived BEFORE it, and
93
- * close that press's probe. Returns the direction, if one was found.
94
- */
95
- function claimPendingProbe(fromTag, toTag, at) {
96
- for (let i = probes.length - 1; i >= 0; i--) {
97
- const probe = probes[i];
98
- if (at - probe.at > DIRECTION_WINDOW_MS) break;
99
- if (probe.resolved) continue;
100
- if (probe.tag !== fromTag) continue;
101
- probe.resolved = true;
102
- probe.movedTo = toTag;
103
- return probe.direction;
104
- }
105
- return null;
106
- }
107
- function recordTransition(fromTag, toTag, at) {
108
- transitionSeq += 1;
109
- const dwellMs = fromTag !== null && currentSince !== null ? Math.max(0, at - currentSince) : undefined;
110
- const transition = {
111
- seq: transitionSeq,
112
- fromTag,
113
- toTag,
114
- direction: claimPendingProbe(fromTag, toTag, at),
115
- at,
116
- dwellMs
117
- };
118
- pushTransition(transition);
119
- if (toTag === null) lostCount += 1;
120
- }
121
- function clearLostTimer() {
122
- if (lostTimer !== null) {
123
- clearTimeout(lostTimer);
124
- lostTimer = null;
125
- }
126
- }
127
-
128
- /** A view took focus. `tag` is the native tag from the raw event's target. */
129
- export function recordFocus(tag, at = Date.now()) {
130
- if (!tracking || !Number.isFinite(tag)) return;
131
- clearLostTimer();
132
-
133
- // Prefer the pending blur as the source: on a real move the blur fires first
134
- // and names the element focus actually left. `currentTag` is the fallback for
135
- // the boot case (a first focus with no blur) and for platforms that skip the
136
- // blur when focus moves between windows.
137
- const fromTag = pendingBlur?.tag ?? currentTag;
138
- pendingBlur = null;
139
-
140
- // A focus event for the element that already has focus is a re-notification
141
- // (re-entering the same view, a re-render), not a transition.
142
- if (fromTag === tag && currentTag === tag) return;
143
- recordTransition(fromTag, tag, at);
144
- currentTag = tag;
145
- currentSince = at;
146
- visited.add(tag);
147
- emitChange();
148
- }
149
-
150
- /**
151
- * A view lost focus. Held rather than recorded: the matching focus lands within
152
- * a frame or two and turns this into a normal transition. Only a blur with NO
153
- * follow-up is the "focus vanished" bug, and that is worth reporting.
154
- *
155
- * THE ORDERING TRAP: blur does NOT reliably precede its focus. On tvOS Fabric
156
- * 0.85 the emitter delivers `topFocus(new)` and only THEN `topBlur(old)`, ~1ms
157
- * apart (verified on the Apple TV simulator). A blur for a tag that no longer
158
- * holds focus is therefore a late echo of a move already recorded — swallowing
159
- * it is what keeps the tool from arming a loss timer for an element that was
160
- * never actually orphaned, and then reporting a phantom "focus lost" plus a
161
- * wrong `fromTag` on the next real move. Only a blur for the CURRENT holder can
162
- * mean focus is leaving, and that test is order-agnostic on both platforms.
163
- */
164
- export function recordBlur(tag, at = Date.now()) {
165
- if (!tracking || !Number.isFinite(tag)) return;
166
- if (currentTag !== null && tag !== currentTag) return;
167
- pendingBlur = {
168
- tag,
169
- at
170
- };
171
- clearLostTimer();
172
- lostTimer = setTimeout(() => {
173
- lostTimer = null;
174
- if (!pendingBlur) return;
175
- const lostFrom = pendingBlur.tag;
176
- pendingBlur = null;
177
- recordTransition(lostFrom, null, Date.now());
178
- currentTag = null;
179
- currentSince = null;
180
- emitChange();
181
- }, FOCUS_LOST_MS);
182
- }
183
-
184
- /**
185
- * A D-pad direction was pressed. Opens a probe against whatever holds focus,
186
- * which resolves either to the element focus moved to or to `null` — "this
187
- * direction did nothing from here", the raw material of every dead-end flag.
188
- */
189
- export function recordKey(direction, at = Date.now()) {
190
- if (!tracking) return;
191
-
192
- // The `blur → focus → HWEvent` ordering: the move is already recorded and
193
- // still uncredited, so this press is its cause. Credit it and log the probe
194
- // against the element focus left, NOT against `currentTag` (which the
195
- // transition already advanced past).
196
- //
197
- // `fromTag !== null` is the guard that keeps this from eating the wrong
198
- // press: a transition out of nowhere (boot-time focus, focus regained after a
199
- // loss) has no source element, so no D-pad direction can have caused it —
200
- // without the check, the first press after app launch would be swallowed by
201
- // the launch's own focus event and never open a probe.
202
- const latest = transitions[transitions.length - 1];
203
- if (latest && latest.direction === null && latest.fromTag !== null && at - latest.at <= BACKFILL_WINDOW_MS) {
204
- latest.direction = direction;
205
- probeSeq += 1;
206
- pushProbe({
207
- seq: probeSeq,
208
- tag: latest.fromTag,
209
- direction,
210
- at,
211
- resolved: true,
212
- movedTo: latest.toTag
213
- });
214
- emitChange();
215
- return;
216
- }
217
- probeSeq += 1;
218
- const probe = {
219
- seq: probeSeq,
220
- tag: currentTag,
221
- direction,
222
- at,
223
- resolved: false,
224
- movedTo: null
225
- };
226
- pushProbe(probe);
227
- const timer = setTimeout(() => {
228
- probeTimers.delete(timer);
229
- if (probe.resolved) return;
230
- probe.resolved = true;
231
- probe.movedTo = null;
232
- emitChange();
233
- }, DIRECTION_WINDOW_MS);
234
- probeTimers.add(timer);
235
- emitChange();
236
- }
237
- export function getState() {
238
- return {
239
- currentTag,
240
- // `currentTag` is only ever nulled by the loss path, so a null with no
241
- // transitions behind it means we simply have not seen focus yet.
242
- presence: currentTag !== null ? "focused" : transitionSeq === 0 ? "never-observed" : "lost",
243
- currentSince,
244
- transitions,
245
- probes,
246
- visited: Array.from(visited),
247
- lostCount,
248
- transitionCount: transitionSeq,
249
- tracking
250
- };
251
- }
252
-
253
- /**
254
- * Wipe the history but KEEP where focus currently is — clearing is how a user
255
- * starts a clean traversal run, and forgetting the current element would blank
256
- * the Now card until the next press.
257
- */
258
- export function clearHistory() {
259
- transitions = [];
260
- probes = [];
261
- visited = currentTag !== null ? new Set([currentTag]) : new Set();
262
- lostCount = 0;
263
- transitionSeq = 0;
264
- probeSeq = 0;
265
- emitChange();
266
- }
267
-
268
- /**
269
- * Pause/resume recording without detaching the native listeners. Useful when a
270
- * user is driving the app by hand and doesn't want that traversal scored.
271
- */
272
- export function setTracking(enabled) {
273
- if (tracking === enabled) return;
274
- tracking = enabled;
275
- if (!enabled) {
276
- clearLostTimer();
277
- pendingBlur = null;
278
- }
279
- emitChange();
280
- }
281
-
282
- /** Full reset, including timers — the observers' detach path and tests. */
283
- export function resetStore() {
284
- clearLostTimer();
285
- probeTimers.forEach(timer => {
286
- clearTimeout(timer);
287
- });
288
- probeTimers.clear();
289
- pendingBlur = null;
290
- currentTag = null;
291
- currentSince = null;
292
- transitions = [];
293
- probes = [];
294
- visited = new Set();
295
- lostCount = 0;
296
- transitionSeq = 0;
297
- probeSeq = 0;
298
- tracking = true;
299
- emitChange();
300
- }
301
- export function subscribe(listener) {
302
- listeners.add(listener);
303
- return () => {
304
- listeners.delete(listener);
305
- };
306
- }
307
-
308
- /** Exposed so the scanner can nudge a rescan when focus lands on a stranger. */
309
- export function notifyChange() {
310
- emitChange();
311
- }
312
- //# sourceMappingURL=focusStore.js.map
1
+ "use strict";export const DIRECTION_WINDOW_MS=250;export const BACKFILL_WINDOW_MS=60;export const FOCUS_LOST_MS=400;const g=300;const _=400;let r=null;let d=null;let u=[];let i=[];let S=new Set;let T=0;let a=0;let m=0;let f=true;let c=null;let p=null;const v=new Set;const h=new Set;function s(){h.forEach(e=>{e()})}function C(e){u=u.length>=g?[...u.slice(1),e]:[...u,e]}function w(e){i=i.length>=_?[...i.slice(1),e]:[...i,e]}function D(e,t,n){for(let l=i.length-1;l>=0;l--){const o=i[l];if(n-o.at>DIRECTION_WINDOW_MS)break;if(o.resolved)continue;if(o.tag!==e)continue;o.resolved=true;o.movedTo=t;return o.direction}return null}function b(e,t,n){a+=1;const l=e!==null&&d!==null?Math.max(0,n-d):void 0;const o={seq:a,fromTag:e,toTag:t,direction:D(e,t,n),at:n,dwellMs:l};C(o);if(t===null)T+=1}function x(){if(p!==null){clearTimeout(p);p=null}}export function recordFocus(e,t=Date.now()){if(!f||!Number.isFinite(e))return;x();const n=c?.tag??r;c=null;if(n===e&&r===e)return;b(n,e,t);r=e;d=t;S.add(e);s()}export function recordBlur(e,t=Date.now()){if(!f||!Number.isFinite(e))return;if(r!==null&&e!==r)return;c={tag:e,at:t};x();p=setTimeout(()=>{p=null;if(!c)return;const n=c.tag;c=null;b(n,null,Date.now());r=null;d=null;s()},FOCUS_LOST_MS)}export function recordKey(e,t=Date.now()){if(!f)return;const n=u[u.length-1];if(n&&n.direction===null&&n.fromTag!==null&&t-n.at<=BACKFILL_WINDOW_MS){n.direction=e;m+=1;w({seq:m,tag:n.fromTag,direction:e,at:t,resolved:true,movedTo:n.toTag});s();return}m+=1;const l={seq:m,tag:r,direction:e,at:t,resolved:false,movedTo:null};w(l);const o=setTimeout(()=>{v.delete(o);if(l.resolved)return;l.resolved=true;l.movedTo=null;s()},DIRECTION_WINDOW_MS);v.add(o);s()}export function getState(){return{currentTag:r,presence:r!==null?"focused":a===0?"never-observed":"lost",currentSince:d,transitions:u,probes:i,visited:Array.from(S),lostCount:T,transitionCount:a,tracking:f}}export function clearHistory(){u=[];i=[];S=r!==null?new Set([r]):new Set;T=0;a=0;m=0;s()}export function setTracking(e){if(f===e)return;f=e;if(!e){x();c=null}s()}export function resetStore(){x();v.forEach(e=>{clearTimeout(e)});v.clear();c=null;r=null;d=null;u=[];i=[];S=new Set;T=0;a=0;m=0;f=true;s()}export function subscribe(e){h.add(e);return()=>{h.delete(e)}}export function notifyChange(){s()}
@@ -1,244 +1 @@
1
- "use strict";
2
-
3
- /**
4
- * focusInspectorSyncAdapter — streams the focus picture to Buoy Desktop.
5
- *
6
- * Structurally matches external-sync's `ToolSyncAdapter` WITHOUT importing the
7
- * package (the house rule every tool's adapter follows). Auto-discovery in
8
- * @buoy-gg/core picks it up by export name and announces the "focus-inspector"
9
- * capability.
10
- *
11
- * There is NO on-device UI and there never will be: on Android TV a focusable
12
- * view in an overlay becomes a D-pad stop in the host app's focus order
13
- * (`pointerEvents="box-none"` gates touch, not focus), so an on-device panel
14
- * would corrupt the measurement. Everything device-side therefore lives in
15
- * module scope and in this adapter's `subscribe` — headless FloatingDevTools
16
- * mounts the adapters and NONE of the overlay components, so instrumentation
17
- * inside a component would simply never run on TV.
18
- *
19
- * WORK SPLIT: the two event listeners attach at module load and stay attached;
20
- * the fiber scan only runs while a dashboard is watching.
21
- *
22
- * That asymmetry is deliberate and was learned the hard way. Focus cannot be
23
- * QUERIED on either platform — the tool only ever learns where focus is by
24
- * watching it arrive. So an observer that attached on first `subscribe` knows
25
- * nothing until focus next MOVES, which means opening the panel on a running
26
- * app shows nothing, and, far worse, the tool is blind in precisely the case it
27
- * exists for: focus is stuck, you press a direction, nothing moves, and so
28
- * nothing is ever learned. Listening from the start is what makes "what holds
29
- * focus right now" answerable at all.
30
- *
31
- * The cost is two event-emitter listeners and a bounded ring-buffer append per
32
- * focus change — nothing next to the fiber walk, which stays gated on a
33
- * dashboard actually watching. `setTracking(false)` is the off switch.
34
- */
35
-
36
- import { approxJsonSize } from "@buoy-gg/shared-ui";
37
- import { clearHistory, getState, setTracking, subscribe as subscribeStore, notifyChange } from "../store/focusStore";
38
- import { attachFocusObserver, getFocusObserverStatus } from "../observe/focusEventObserver";
39
- import { attachKeyObserver, isKeyObserverListening, isTV, platformOS } from "../observe/keyEventObserver";
40
- import { requestFocusForTag, scanFocusables } from "../scan/focusableScanner";
41
- import { computeCoverage, detectFlags, observedEdges } from "../detect/detectors";
42
-
43
- /**
44
- * Self-truncation budget. `attachDeviceSync` SILENTLY DROPS any snapshot over
45
- * 2MB — the panel just goes stale with no error — so the adapter trims to fit
46
- * rather than hoping it does.
47
- */
48
- const MAX_SNAPSHOT_BYTES = 1.5 * 1024 * 1024;
49
-
50
- /** A rescan after focus lands on a stranger, debounced so a fast traversal doesn't thrash. */
51
- const RESCAN_DEBOUNCE_MS = 500;
52
- // Attach the moment the package is loaded — which is when @buoy-gg/core's
53
- // adapter discovery requires it, i.e. only in builds that ship Buoy at all.
54
- // Guarded on TV so a phone build pays literally nothing.
55
- if (isTV) {
56
- attachFocusObserver();
57
- attachKeyObserver();
58
- }
59
- let lastScan = null;
60
- let scanning = false;
61
- let rescanTimer = null;
62
- let subscriberCount = 0;
63
- function nodesByTag() {
64
- const map = new Map();
65
- for (const node of lastScan?.nodes ?? []) map.set(node.nativeTag, node);
66
- return map;
67
- }
68
- async function runScan() {
69
- if (scanning) return lastScan;
70
- scanning = true;
71
- try {
72
- lastScan = await scanFocusables();
73
- return lastScan;
74
- } catch {
75
- return lastScan;
76
- } finally {
77
- scanning = false;
78
- notifyChange();
79
- }
80
- }
81
-
82
- /**
83
- * Focus landed on a tag the inventory doesn't know. Either the screen changed
84
- * or focus is on an element we never collected — both are worth a rescan, and
85
- * the second is the invisible-stop case the panel needs a name for.
86
- */
87
- function scheduleRescanIfStranger() {
88
- if (subscriberCount === 0) return;
89
- const {
90
- currentTag
91
- } = getState();
92
- if (currentTag === null) return;
93
- if (nodesByTag().has(currentTag)) return;
94
- if (rescanTimer !== null) return;
95
- rescanTimer = setTimeout(() => {
96
- rescanTimer = null;
97
- void runScan();
98
- }, RESCAN_DEBOUNCE_MS);
99
- }
100
-
101
- /**
102
- * Newest-fitting trim, walking backwards from the most recent transition. A
103
- * dropped snapshot is invisible on the dashboard, so the budget is spent on the
104
- * transitions a person is actually looking at.
105
- */
106
- function fittingTransitions(transitions, budget) {
107
- const out = [];
108
- let bytes = 0;
109
- for (let i = transitions.length - 1; i >= 0; i--) {
110
- const size = approxJsonSize(transitions[i], budget).bytes;
111
- if (bytes + size > budget && out.length > 0) break;
112
- out.push(transitions[i]);
113
- bytes += size;
114
- }
115
- out.reverse();
116
- return out;
117
- }
118
- function buildSnapshot() {
119
- const state = getState();
120
- const nodes = lastScan?.nodes ?? [];
121
- const observer = getFocusObserverStatus();
122
- const flags = detectFlags(state.transitions, state.probes, nodes, state.lostCount, lastScan?.scannedAt ?? 0);
123
- const coverage = computeCoverage(nodes, state.visited, state.transitions, state.probes);
124
- const byTag = nodesByTag();
125
-
126
- // Focusables come first in the budget — they are the smaller, bounded half
127
- // (capped at 400 by the scanner) and the panel is unusable without them.
128
- const focusablesBytes = approxJsonSize(nodes, MAX_SNAPSHOT_BYTES).bytes;
129
- const transitions = fittingTransitions(state.transitions, Math.max(64 * 1024, MAX_SNAPSHOT_BYTES - focusablesBytes));
130
- const flagged = flags.deadEnds.filter(flag => !flag.atEdge).length + flags.traps.length + flags.invisibleStops.length + (flags.lostCount > 0 ? 1 : 0);
131
- return {
132
- supported: isTV,
133
- platform: platformOS,
134
- observer: observer.status,
135
- observerReason: observer.reason,
136
- keysListening: isKeyObserverListening(),
137
- tracking: state.tracking,
138
- presence: state.presence,
139
- current: state.currentTag === null ? null : {
140
- tag: state.currentTag,
141
- node: byTag.get(state.currentTag) ?? null,
142
- since: state.currentSince ?? Date.now(),
143
- edges: observedEdges(state.currentTag, state.transitions)
144
- },
145
- transitions,
146
- focusables: nodes,
147
- screen: lastScan?.screen ?? null,
148
- scannedAt: lastScan?.scannedAt ?? null,
149
- flags,
150
- coverage,
151
- stats: {
152
- focusables: coverage.focusables,
153
- visited: coverage.visited,
154
- transitions: state.transitionCount,
155
- flagged
156
- },
157
- capturedAt: Date.now()
158
- };
159
- }
160
- export const focusInspectorSyncAdapter = {
161
- version: 1,
162
- getSnapshot() {
163
- return buildSnapshot();
164
- },
165
- subscribe(onChange) {
166
- subscriberCount += 1;
167
- if (subscriberCount === 1) {
168
- // Observers are already attached (module load). Only the scan is gated.
169
- void runScan();
170
- }
171
- const unsubscribeStore = subscribeStore(() => {
172
- scheduleRescanIfStranger();
173
- onChange();
174
- });
175
-
176
- // A dashboard can attach before the app's first screen has mounted, in
177
- // which case the first scan finds nothing. Poll briefly so the inventory
178
- // fills in without the user having to press anything.
179
- let retries = 0;
180
- let pollTimer = null;
181
- const poll = () => {
182
- retries += 1;
183
- if ((lastScan?.nodes.length ?? 0) > 0 || retries >= 100) {
184
- pollTimer = null;
185
- return;
186
- }
187
- void runScan();
188
- pollTimer = setTimeout(poll, 100);
189
- };
190
- pollTimer = setTimeout(poll, 100);
191
- return () => {
192
- unsubscribeStore();
193
- if (pollTimer !== null) clearTimeout(pollTimer);
194
- subscriberCount = Math.max(0, subscriberCount - 1);
195
- if (subscriberCount === 0) {
196
- if (rescanTimer !== null) {
197
- clearTimeout(rescanTimer);
198
- rescanTimer = null;
199
- }
200
- // The observers stay attached on purpose — see the note at the top.
201
- // Detaching here would make the tool forget where focus is every time
202
- // the panel is closed, and re-open blind.
203
- }
204
- };
205
- },
206
- actions: {
207
- async rescan() {
208
- const scan = await runScan();
209
- return {
210
- ok: true,
211
- focusables: scan?.nodes.length ?? 0
212
- };
213
- },
214
- clearHistory() {
215
- clearHistory();
216
- return {
217
- ok: true
218
- };
219
- },
220
- setTracking(params) {
221
- setTracking(params?.enabled !== false);
222
- return {
223
- tracking: getState().tracking
224
- };
225
- },
226
- /**
227
- * Put focus on an element and watch what the D-pad does from there — the
228
- * only write this tool ever performs, and only when a human asks for it.
229
- */
230
- focusElement(params) {
231
- const tag = params?.nativeTag;
232
- if (typeof tag !== "number") return {
233
- ok: false,
234
- reason: "nativeTag is required."
235
- };
236
- if (!isTV) return {
237
- ok: false,
238
- reason: "Not a TV build — there is no focus engine."
239
- };
240
- return requestFocusForTag(tag);
241
- }
242
- }
243
- };
244
- //# sourceMappingURL=focusInspectorSyncAdapter.js.map
1
+ "use strict";import{approxJsonSize as d}from"@buoy-gg/shared-ui";import{clearHistory as w,getState as f,setTracking as O,subscribe as A,notifyChange as C}from"../store/focusStore";import{attachFocusObserver as p,detachFocusObserver as E,getFocusObserverStatus as F}from"../observe/focusEventObserver";import{attachKeyObserver as m,detachKeyObserver as M,isKeyObserverListening as x,isTV as g,platformOS as c}from"../observe/keyEventObserver";import{requestFocusForTag as B,scanFocusables as N}from"../scan/focusableScanner";import{computeCoverage as _,detectFlags as D,observedEdges as H}from"../detect/detectors";const T=1.5*1024*1024;const K=500;if(g){p();m()}let o=null;let b=false;let a=null;let i=0;function v(){const e=new Map;for(const t of o?.nodes??[])e.set(t.nativeTag,t);return e}async function l(){if(b)return o;b=true;try{o=await N();return o}catch{return o}finally{b=false;C()}}function R(){if(i===0)return;const{currentTag:e}=f();if(e===null)return;if(v().has(e))return;if(a!==null)return;a=setTimeout(()=>{a=null;void l()},K)}function q(e,t){const r=[];let n=0;for(let s=e.length-1;s>=0;s--){const u=d(e[s],t).bytes;if(n+u>t&&r.length>0)break;r.push(e[s]);n+=u}r.reverse();return r}function z(){const e=f();const t=o?.nodes??[];const r=F();const n=D(e.transitions,e.probes,t,e.lostCount,o?.scannedAt??0);const s=_(t,e.visited,e.transitions,e.probes);const u=v();const h=d(t,T).bytes;const y=q(e.transitions,Math.max(64*1024,T-h));const S=n.deadEnds.filter(k=>!k.atEdge).length+n.traps.length+n.invisibleStops.length+(n.lostCount>0?1:0);return{supported:g||c==="web",platform:c,observer:r.status,observerReason:r.reason,keysListening:x(),tracking:e.tracking,presence:e.presence,current:e.currentTag===null?null:{tag:e.currentTag,node:u.get(e.currentTag)??null,since:e.currentSince??Date.now(),edges:H(e.currentTag,e.transitions,e.probes)},transitions:y,focusables:t,screen:o?.screen??null,scannedAt:o?.scannedAt??null,flags:n,coverage:s,stats:{focusables:s.focusables,visited:s.visited,transitions:e.transitionCount,flagged:S},capturedAt:Date.now()}}export const focusInspectorSyncAdapter={version:1,getSnapshot(){return z()},subscribe(e){i+=1;if(i===1){if(c==="web"){p();m()}void l()}const t=A(()=>{R();e()});let r=0;let n=null;const s=()=>{r+=1;if((o?.nodes.length??0)>0||r>=100){n=null;return}void l();n=setTimeout(s,100)};n=setTimeout(s,100);return()=>{t();if(n!==null)clearTimeout(n);i=Math.max(0,i-1);if(i===0){if(a!==null){clearTimeout(a);a=null}if(c==="web"){E();M()}}}},actions:{async rescan(){const e=await l();return{ok:true,focusables:e?.nodes.length??0}},clearHistory(){w();return{ok:true}},setTracking(e){O(e?.enabled!==false);return{tracking:f().tracking}},focusElement(e){const t=e?.nativeTag;if(typeof t!=="number")return{ok:false,reason:"nativeTag is required."};if(!g&&c!=="web")return{ok:false,reason:"Not a TV build \u2014 there is no focus engine."};return B(t)}}};
@@ -118,5 +118,5 @@ export declare function computeCoverage(nodes: FocusableNode[], visited: number[
118
118
  * renders. A direction with no entry is UNKNOWN (dim), which is a different
119
119
  * statement from a dead end (pressed, went nowhere).
120
120
  */
121
- export declare function observedEdges(tag: number, transitions: FocusTransition[]): Partial<Record<FocusDirection, number | null>>;
121
+ export declare function observedEdges(tag: number, transitions: FocusTransition[], probes?: FocusProbe[]): Partial<Record<FocusDirection, number | null>>;
122
122
  //# sourceMappingURL=detectors.d.ts.map
@@ -0,0 +1,6 @@
1
+ export declare const isTV = false;
2
+ export declare const platformOS = "web";
3
+ export declare function attachKeyObserver(): void;
4
+ export declare function detachKeyObserver(): void;
5
+ export declare function isKeyObserverListening(): boolean;
6
+ //# sourceMappingURL=keyEventObserver.web.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { resolveDOMEventEmitter as resolveRawEventEmitter } from '@buoy-gg/shared-ui/utils';
2
+ export type { RawEventSubscription, RawEventEmitterLike, RawEmitterResolution } from './rawEventEmitter';
3
+ //# sourceMappingURL=rawEventEmitter.web.d.ts.map
@@ -1,27 +1,3 @@
1
- /**
2
- * focusableScanner — a read-only fiber walk that inventories everything on
3
- * screen the TV focus engine could land on.
4
- *
5
- * Provably inert: it reads `memoizedProps` and calls `measureInWindow`. It adds
6
- * no views, sets no props, and calls no focus command — which matters more here
7
- * than anywhere else in Buoy, because on Android TV a single focusable view we
8
- * rendered would insert itself into the host app's focus order and change the
9
- * very navigation this tool measures.
10
- *
11
- * Traversal (multi-renderer roots, the Fabric/Paper/CLI-dev instance triad,
12
- * Offscreen + inactive-`react-native-screens` pruning, measure-with-a-hang-
13
- * guard) follows the house pattern from
14
- * `packages/highlight-updates/src/highlight-updates/utils/screenElements.ts`.
15
- * The repo convention is a per-package copy trimmed to the tool's needs rather
16
- * than a shared walker library — see packages/CLAUDE.md.
17
- *
18
- * There is no JS API for "which view has focus" and none for "if focus is here
19
- * and the user presses RIGHT, where does it go" (Android has
20
- * `FabricUIManager.findNextFocusableElement` natively but never exposes it to
21
- * JS; tvOS's UIKit engine decides at runtime and answers nothing). So this
22
- * inventory is the *candidate set* — the observed transition stream is what
23
- * says which of them focus actually reaches.
24
- */
25
1
  export interface FocusFrame {
26
2
  x: number;
27
3
  y: number;
@@ -0,0 +1 @@
1
+ "use client";var Z=Object.defineProperty;var _e=Object.getOwnPropertyDescriptor;var Be=Object.getOwnPropertyNames;var He=Object.prototype.hasOwnProperty;var ze=(e,n)=>{for(var t in n)Z(e,t,{get:n[t],enumerable:true})},X=(e,n,t,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of Be(n))!He.call(e,i)&&i!==t&&Z(e,i,{get:()=>n[i],enumerable:!(r=_e(n,i))||r.enumerable});return e},We=(e,n,t)=>(X(e,n,"default"),t&&X(t,n,"default"));typeof window<"u"&&typeof globalThis.global>"u"&&(globalThis.global=globalThis);import{approxJsonSize as ee}from"@buoy-gg/shared-ui/web";var b=null,E=null,w=[],T=[],O=new Set,R=0,F=0,P=0,k=true,S=null,C=null,_=new Set,j=new Set;function x(){j.forEach(e=>{e()})}function je(e){w=w.length>=300?[...w.slice(1),e]:[...w,e]}function te(e){T=T.length>=400?[...T.slice(1),e]:[...T,e]}function Ke(e,n,t){for(let r=T.length-1;r>=0;r--){let i=T[r];if(t-i.at>250)break;if(!i.resolved&&i.tag===e)return i.resolved=true,i.movedTo=n,i.direction}return null}function ne(e,n,t){F+=1;let r=e!==null&&E!==null?Math.max(0,t-E):void 0,i={seq:F,fromTag:e,toTag:n,direction:Ke(e,n,t),at:t,dwellMs:r};je(i),n===null&&(R+=1)}function B(){C!==null&&(clearTimeout(C),C=null)}function ae(e,n=Date.now()){if(!k||!Number.isFinite(e))return;B();let t=S?.tag??b;S=null,!(t===e&&b===e)&&(ne(t,e,n),b=e,E=n,O.add(e),x())}function re(e,n=Date.now()){!k||!Number.isFinite(e)||b!==null&&e!==b||(S={tag:e,at:n},B(),C=setTimeout(()=>{if(C=null,!S)return;let t=S.tag;S=null,ne(t,null,Date.now()),b=null,E=null,x()},400))}function ie(e,n=Date.now()){if(!k)return;let t=w[w.length-1];if(t&&t.direction===null&&t.fromTag!==null&&n-t.at<=60){t.direction=e,P+=1,te({seq:P,tag:t.fromTag,direction:e,at:n,resolved:true,movedTo:t.toTag}),x();return}P+=1;let r={seq:P,tag:b,direction:e,at:n,resolved:false,movedTo:null};te(r);let i=setTimeout(()=>{_.delete(i),!r.resolved&&(r.resolved=true,r.movedTo=null,x())},250);_.add(i),x()}function H(){return{currentTag:b,presence:b!==null?"focused":F===0?"never-observed":"lost",currentSince:E,transitions:w,probes:T,visited:Array.from(O),lostCount:R,transitionCount:F,tracking:k}}function oe(){w=[],T=[],O=b!==null?new Set([b]):new Set,R=0,F=0,P=0,x()}function le(e){k!==e&&(k=e,e||(B(),S=null),x())}function Ue(){B(),_.forEach(e=>{clearTimeout(e)}),_.clear(),S=null,b=null,E=null,w=[],T=[],O=new Set,R=0,F=0,P=0,k=true,x()}function se(e){return j.add(e),()=>{j.delete(e)}}function qe(){x()}import{resolveDOMEventEmitter as $e}from"@buoy-gg/shared-ui/web/utils";var K=null,ue=false,D=[],A="idle",U=null;function Ge(){if(ue)return K;ue=true;let e=$e();return K=e.emitter,U=e.reason,K}function ce(e){let n=e?.nativeEvent;if(!n)return null;let t=n.target??n.tag;return typeof t=="number"&&Number.isFinite(t)?t:null}function de(){if(D.length>0)return;let e=Ge();if(!e){A="unavailable";return}try{D=[e.addListener("topFocus",n=>{let t=ce(n);t!==null&&ae(t)}),e.addListener("topBlur",n=>{let t=ce(n);t!==null&&re(t)})],A="listening"}catch(n){D=[],A="unavailable",U=n?.message??"addListener failed"}}function Ye(){D.forEach(e=>{try{e.remove()}catch{}}),D=[],A==="listening"&&(A="idle")}function Je(){return{status:A,reason:A==="unavailable"?U:null}}var q=false,I="web",L=false,Qe={ArrowUp:"up",ArrowDown:"down",ArrowLeft:"left",ArrowRight:"right"};function fe(e){let n=Qe[e.key];n&&ie(n)}function ge(){L||typeof document>"u"||(L=true,document.addEventListener("keydown",fe,true))}function Ze(){L&&(L=false,document.removeEventListener("keydown",fe,true))}function Xe(){return L}import{getDOMNodeHandle as et,getDOMPublicInstance as tt,isDOMElement as nt}from"@buoy-gg/shared-ui/web/utils";var he={};ze(he,{AccessibilityInfo:()=>ot,ActivityIndicator:()=>on,Alert:()=>un,Animated:()=>lt,AppRegistry:()=>ut,AppState:()=>ct,Appearance:()=>st,BackHandler:()=>dt,Button:()=>ln,CheckBox:()=>Ft,Clipboard:()=>ft,DeviceEventEmitter:()=>jt,Dimensions:()=>pe,Easing:()=>gt,FlatList:()=>Pt,I18nManager:()=>ht,Image:()=>Mt,ImageBackground:()=>Vt,InteractionManager:()=>mt,Keyboard:()=>pt,KeyboardAvoidingView:()=>Ct,LayoutAnimation:()=>bt,Linking:()=>vt,LogBox:()=>Wt,Modal:()=>an,NativeEventEmitter:()=>yt,NativeModules:()=>it,PanResponder:()=>wt,Picker:()=>Dt,PixelRatio:()=>Tt,Platform:()=>xt,Pressable:()=>tn,ProgressBar:()=>It,RefreshControl:()=>Lt,SafeAreaView:()=>Nt,ScrollView:()=>nn,SectionList:()=>Ot,Share:()=>St,StatusBar:()=>Rt,StyleSheet:()=>kt,Switch:()=>rn,Text:()=>Jt,TextInput:()=>Qt,Touchable:()=>_t,TouchableHighlight:()=>Xt,TouchableNativeFeedback:()=>Bt,TouchableOpacity:()=>Zt,TouchableWithoutFeedback:()=>en,UIManager:()=>At,Vibration:()=>Et,View:()=>Yt,VirtualizedList:()=>Ht,YellowBox:()=>zt,findNodeHandle:()=>sn,unstable_createElement:()=>rt,useColorScheme:()=>Kt,useLocaleContext:()=>Ut,useWindowDimensions:()=>qt});We(he,at);import*as at from"react-native-web";import{unstable_createElement as rt,NativeModules as it,AccessibilityInfo as ot,Animated as lt,Appearance as st,AppRegistry as ut,AppState as ct,BackHandler as dt,Clipboard as ft,Dimensions as pe,Easing as gt,I18nManager as ht,Keyboard as pt,InteractionManager as mt,LayoutAnimation as bt,Linking as vt,NativeEventEmitter as yt,PanResponder as wt,PixelRatio as Tt,Platform as xt,Share as St,StyleSheet as kt,UIManager as At,Vibration as Et,CheckBox as Ft,FlatList as Pt,Image as Mt,ImageBackground as Vt,KeyboardAvoidingView as Ct,Picker as Dt,ProgressBar as It,RefreshControl as Lt,SafeAreaView as Nt,SectionList as Ot,StatusBar as Rt,Touchable as _t,TouchableNativeFeedback as Bt,VirtualizedList as Ht,YellowBox as zt,LogBox as Wt,DeviceEventEmitter as jt,useColorScheme as Kt,useLocaleContext as Ut,useWindowDimensions as qt}from"react-native-web";import*as h from"react-native-web";import{createElement as $t,forwardRef as Gt}from"react";function v(e){let n=Gt((r,i)=>{let{accessibilityState:a,accessibilityValue:o,...l}=r;for(let s of["busy","checked","disabled","expanded","selected"])a?.[s]!==void 0&&l[`aria-${s}`]===void 0&&(l[`aria-${s}`]=a[s]);for(let[s,u]of Object.entries({min:"valuemin",max:"valuemax",now:"valuenow",text:"valuetext"}))o?.[s]!==void 0&&l[`aria-${u}`]===void 0&&(l[`aria-${u}`]=o[s]);return $t(e,{...l,ref:i})}),t=e.State;return t&&Object.assign(n,{State:t}),n.displayName=e.displayName??e.name??"NativeWebView",n}var Yt=v(h.View),Jt=v(h.Text),Qt=v(h.TextInput),Zt=v(h.TouchableOpacity),Xt=v(h.TouchableHighlight),en=v(h.TouchableWithoutFeedback),tn=v(h.Pressable),nn=v(h.ScrollView),an=v(h.Modal),rn=v(h.Switch),on=v(h.ActivityIndicator),ln=v(h.Button),sn=h.findNodeHandle,un={alert(e,n,t,r){if(typeof document>"u")return;let i=document.createElement("dialog");i.dataset.buoyRoot="",i.setAttribute("aria-label",e),i.style.cssText="position:fixed;inset:0;margin:auto;width:min(440px,90vw);padding:24px;border:1px solid #475569;border-radius:14px;background:#17212e;color:#f1f5f9;font:14px system-ui;box-shadow:0 20px 80px #0008;z-index:2147483647";let a=document.createElement("h2");if(a.textContent=e,a.style.cssText="font-size:18px;margin:0 0 12px",i.appendChild(a),n){let u=document.createElement("p");u.textContent=n,u.style.cssText="line-height:1.5;white-space:pre-wrap;margin:0 0 20px",i.appendChild(u)}let o=document.createElement("div");o.style.cssText="display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px";let l=t?.length?t:[{text:"OK"}],s=()=>{i.close?.(),i.remove()};for(let u of l){let f=document.createElement("button");f.type="button",f.textContent=u.text??"OK",f.style.cssText=`padding:9px 14px;border:1px solid #64748b;border-radius:7px;color:white;background:${u.style==="destructive"?"#a52b38":"#283a4d"};cursor:pointer;font:inherit`,f.onclick=()=>{s(),u.onPress?.()},o.appendChild(f)}i.addEventListener("cancel",u=>{u.preventDefault(),r?.cancelable!==false&&(s(),r?.onDismiss?.())}),i.appendChild(o),document.body.appendChild(i),i.showModal?i.showModal():i.setAttribute("open","")}};var cn=22,me=new Set(["View","RCTView","ScrollView","RCTScrollView","RCTScrollContentView","SafeAreaView","RCTSafeAreaView","AndroidHorizontalScrollContentView","KeyboardAvoidingView","Pressable","PlatformPressable","TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback","AnimatedComponent","Wrapper"]),$="TVFocusGuideView";function dn(){return typeof globalThis>"u"?null:globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__??null}function fn(){let e=dn();if(!e)return[];let n=[];try{if(e.renderers&&typeof e.renderers.forEach=="function"&&e.renderers.forEach((t,r)=>{try{let i=e.getFiberRoots?.(r);i&&i.forEach(a=>n.push(a))}catch{}}),n.length===0&&e.getFiberRoots){let t=e.getFiberRoots(1);t&&t.forEach(r=>n.push(r))}}catch{}return n}function gn(e){return e.tag===cn&&e.memoizedState!==null}function hn(e){let n=e.memoizedProps?.activityState;return n===0||n===1||e.memoizedProps?.isPageFocused===false}function G(e){return e?.memoizedProps??e?.pendingProps??null}function be(e){let n=e?.type;return n?typeof n=="string"?n:n.displayName||n.name||null:null}function ve(e){let n=e?._debugOwner||e?.return,t=0;for(;n&&t<30;){t++;let r=be(n);if(r&&!me.has(r)&&typeof n.type!="string")return r;n=n.return}return null}function ye(e){let n=tt(e);if(n)return n;if(!e)return null;let t=e;return t.canonical?.publicInstance?t.canonical.publicInstance:t.canonical&&typeof t.canonical.measure=="function"?t.canonical:typeof t.measure=="function"||typeof t.measureInWindow=="function"?t:null}function we(e){let n=et(e);if(n!==null)return n;if(e==null)return null;let t=e;if(typeof t.__nativeTag=="number")return t.__nativeTag;if(typeof t._nativeTag=="number")return t._nativeTag;if(typeof t.nativeTag=="number")return t.nativeTag;if(t.canonical){if(typeof t.canonical.__nativeTag=="number")return t.canonical.__nativeTag;if(typeof t.canonical._nativeTag=="number")return t.canonical._nativeTag;if(typeof t.canonical.nativeTag=="number")return t.canonical.nativeTag}return null}function pn(e){let n=ye(e.stateNode);if(n)return{instance:n,nativeTag:we(e.stateNode)};let t=[];e.child&&t.push(e.child);let r=0;for(;t.length>0&&r<4e3;){r++;let i=t.pop();if(!i)continue;let a=ye(i.stateNode);if(a)return{instance:a,nativeTag:we(i.stateNode)};i.child&&t.push(i.child),i.sibling&&t.push(i.sibling)}return null}function mn(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}function bn(e){let n=typeof e.nextFocusUp=="number",t=typeof e.nextFocusDown=="number",r=typeof e.nextFocusLeft=="number",i=typeof e.nextFocusRight=="number";if(!(!n&&!t&&!r&&!i))return{up:n||void 0,down:t||void 0,left:r||void 0,right:i||void 0}}function Te(e){if(!e)return;let n=t=>{if(typeof t=="string")return t.trim()?t.trim().slice(0,60):void 0;if(typeof t=="number"&&Number.isFinite(t))return String(t)};return n(e.children)??n(e.title)??n(e.label)??void 0}function vn(e,n){let t=[],r=(a,o)=>{if(!a||o>n||t.length>=3)return;if(a!==e){let s=G(a);if(s&&typeof s.onPress=="function")return}let l=Te(G(a));l&&!t.includes(l)&&t.push(l),a.child&&r(a.child,o+1),a!==e&&a.sibling&&r(a.sibling,o)};e.child&&r(e.child,1);let i=t.join(" ").trim();return i?i.slice(0,60):void 0}function yn(e){return new Promise(n=>{let t=false,r=i=>{t||(t=true,n(i))};setTimeout(()=>r(null),1e3);try{typeof e.measureInWindow=="function"?e.measureInWindow((i,a,o,l)=>{r(o>0&&l>0?{x:i,y:a,width:o,height:l}:null)}):typeof e.measure=="function"?e.measure((i,a,o,l,s,u)=>{r(o>0&&l>0&&s!=null&&u!=null?{x:s,y:u,width:o,height:l}:null)}):r(null)}catch{r(null)}})}var Y=new Map;function wn(e,n,t){let r=[{fiber:e,context:n,depth:0}],i=new Set;for(;r.length>0;){let a=r.pop();if(!a)continue;let{fiber:o,depth:l}=a;if(!o||i.has(o)||l>500)continue;if(i.add(o),gn(o)||hn(o)){o.sibling&&r.push({fiber:o.sibling,context:a.context,depth:l});continue}let s=t(o,a.context);o.sibling&&r.push({fiber:o.sibling,context:a.context,depth:l}),o.child&&r.push({fiber:o.child,context:s,depth:l+1})}}function Tn(){let e=fn();if(e.length===0)return[];let n=[],t=new Set;for(let r of e)wn(r.current,{guideTag:null},(i,a)=>{let o=G(i);if(!o)return a;let l=be(i),s=l===$,u=mn(o),f=typeof o.onPress=="function"||typeof o.onLongPress=="function",c=o.hasTVPreferredFocus===true,p=bn(o);if(!(s||u===true||c||!!p||f&&u!==false||nt(i.stateNode)&&i.stateNode.matches('a[href],button,input,select,textarea,[tabindex],[contenteditable="true"]')&&!i.stateNode.matches(':disabled,[inert], [tabindex="-1"]')))return a;let d=pn(i),g=d?.nativeTag??null;if(g===null)return a;let N=s?{guideTag:g}:a;if(t.has(g)){let m=n.find(Re=>Re.node.nativeTag===g);return m&&(u!==void 0&&m.node.focusProps.focusable===void 0&&(m.node.focusProps.focusable=u),c&&(m.node.focusProps.hasTVPreferredFocus=true),p&&!m.node.focusProps.nextFocus&&(m.node.focusProps.nextFocus=p),f&&(m.node.pressable=true),!m.node.testID&&o.testID&&(m.node.testID=o.testID),s&&!m.node.guide&&(m.node.guide=xe(o),m.node.name=$)),N}t.add(g);let Q=l??"Unknown";(!l||me.has(l)||typeof i.type=="string")&&(Q=ve(i)??l??"Unknown");let Le=o.accessibilityLabel||o["aria-label"]||void 0,Ne=Te(o)??(f?vn(i,6):void 0),Oe={nativeTag:g,name:s?$:Q,owner:ve(i)??void 0,testID:o.testID||o["data-testid"]||void 0,label:Le,text:Ne,focusProps:{focusable:u,hasTVPreferredFocus:c||void 0,nextFocus:p},guide:s?xe(o):void 0,guideAncestorTag:a.guideTag??void 0,pressable:f||void 0};return d?.instance&&Y.set(g,d.instance),n.push({node:Oe,instance:d?.instance??null}),N});return n}function xe(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 z(e){return Math.round(e*1e3)/1e3}var xn=400;async function Se(){Y.clear();let e=Tn();await Promise.all(e.map(async a=>{if(!a.instance)return;let o=await yn(a.instance);o&&(a.node.frame=o)}));let n=pe.get("window"),t=Math.round(n.width)||1,r=Math.round(n.height)||1,i=[];for(let a of e){let o=a.node.frame;if(o){let l=o.x+o.width/2,s=o.y+o.height/2;a.node.onScreen=s>=0&&s<=r&&l>=0&&l<=t,a.node.frame={x:z(o.x/t),y:z(o.y/r),width:z(o.width/t),height:z(o.height/r)}}if(i.push(a.node),i.length>=xn)break}return i.sort((a,o)=>{let l=a.frame?.y??2,s=o.frame?.y??2;return Math.abs(l-s)>.01?l-s:(a.frame?.x??2)-(o.frame?.x??2)}),{screen:{width:t,height:r},nodes:i,scannedAt:Date.now()}}function Sn(e){let n=Y.get(e);if(!n)return{ok:false,reason:"Unknown tag \u2014 rescan and try again."};if(typeof n.requestTVFocus!="function")return{ok:false,reason:"This element exposes no requestTVFocus()."};try{return n.requestTVFocus(),{ok:true}}catch(t){return{ok:false,reason:t?.message??"requestTVFocus() threw."}}}var ke=["up","down","left","right"];function kn(e,n){return`${e}:${n}`}function Ae(e,n,t=3,r=0){let i=new Map;for(let l of e){if(!l.resolved||l.tag===null||l.at<r)continue;let s=kn(l.tag,l.direction);l.movedTo===null?i.set(s,(i.get(s)??0)+1):i.set(s,0)}let a=new Map;for(let l of n)a.set(l.nativeTag,l);let o=[];return i.forEach((l,s)=>{if(l<t)return;let[u,f]=s.split(":"),c=Number(u),p=f;o.push({tag:c,direction:p,attempts:l,atEdge:An(a.get(c),p,n)})}),o.sort((l,s)=>s.attempts-l.attempts)}function An(e,n,t){let r=e?.frame;if(!r)return false;let i=r.x+r.width/2,a=r.y+r.height/2;for(let o of t){if(o.nativeTag===e.nativeTag||o.guide)continue;let l=o.frame;if(!l)continue;let s=l.x+l.width/2,u=l.y+l.height/2;switch(n){case"up":if(u<r.y)return false;break;case"down":if(u>r.y+r.height)return false;break;case"left":if(s<r.x)return false;break;case"right":if(s>r.x+r.width)return false;break}}return true}function En(e){if(e.length===0)return null;let n=1/0,t=1/0,r=-1/0,i=-1/0;for(let a of e)n=Math.min(n,a.x),t=Math.min(t,a.y),r=Math.max(r,a.x+a.width),i=Math.max(i,a.y+a.height);return{x:n,y:t,width:r-n,height:i-t}}function Fn(e,n,t){switch(t){case"up":return n.y<e.y;case"down":return n.y>e.y+e.height;case"left":return n.x<e.x;case"right":return n.x>e.x+e.width;default:return false}}var Pn=2;function Ee(e,n){let t=new Map;for(let a of n)t.set(a.nativeTag,a);let r=new Map;for(let a of e){let o=t.get(a.tag)?.guideAncestorTag??a.tag,l=r.get(o)??{containerTag:o,tags:new Set,directions:new Set};l.tags.add(a.tag),l.directions.add(a.direction),r.set(o,l)}let i=[];return r.forEach(a=>{if(a.directions.size<Pn)return;let o=new Set(a.tags);for(let d of n)d.guideAncestorTag===a.containerTag&&o.add(d.nativeTag);o.add(a.containerTag);let l=t.get(a.containerTag),s=l?.guide&&l.frame?l.frame:En(Array.from(o).map(d=>t.get(d)?.frame).filter(d=>!!d));if(!s)return;let u=0,f=0;for(let d of ke)n.some(g=>{if(g.guide||o.has(g.nativeTag)||!g.frame)return false;let N={x:g.frame.x+g.frame.width/2,y:g.frame.y+g.frame.height/2};return Fn(s,N,d)})&&(f+=1,a.directions.has(d)||(u+=1));if(f===0||u>0)return;let c=l?.guide,p=c!==void 0&&(c.trap.up||c.trap.down||c.trap.left||c.trap.right);i.push({tags:Array.from(a.tags).sort((d,g)=>d-g),containerTag:a.containerTag,directions:ke.filter(d=>a.directions.has(d)),guideTrapTag:p?a.containerTag:void 0})}),i}function Fe(e,n,t=0){let r=new Map;for(let a of n)r.set(a.nativeTag,a);let i=new Map;for(let a of e){let o=a.toTag;if(o===null||a.at<t)continue;let l=r.get(o),s=l?l.guide?"guide":null:n.length>0?"unknown":null;if(!s)continue;let u=i.get(o);u?(u.count+=1,u.at=a.at):i.set(o,{tag:o,reason:s,at:a.at,count:1})}return Array.from(i.values()).sort((a,o)=>o.at-a.at)}function Pe(e,n,t,r,i=0){let a=Ae(n,t,3,i);return{deadEnds:a,traps:Ee(a,t),invisibleStops:Fe(e,t,i),lostCount:r}}function Me(e,n,t,r){let i=new Set(n),a=e.filter(c=>!c.guide),o=a.filter(c=>!i.has(c.nativeTag)).map(c=>c.nativeTag),l=r.length>=12,s=new Set;for(let c of t)c.toTag!==null&&s.add(c.toTag);let u=new Map;for(let c of e)u.set(c.nativeTag,c);let f=l?o.filter(c=>{if(s.has(c))return false;let p=u.get(c);return p?.frame?a.some(d=>d.nativeTag!==c&&i.has(d.nativeTag)&&Mn(p,d)):false}):[];return{focusables:a.length,visited:a.filter(c=>i.has(c.nativeTag)).length,unvisited:o,suspectedUnreachable:f,meaningful:l}}var Ve=.12;function Mn(e,n){let t=e.frame,r=n.frame;if(!t||!r)return false;let i=t.x<r.x+r.width&&r.x<t.x+t.width,a=t.y<r.y+r.height&&r.y<t.y+t.height;return!!(i&&Math.max(t.y-(r.y+r.height),r.y-(t.y+t.height))<=Ve||a&&Math.max(t.x-(r.x+r.width),r.x-(t.x+t.width))<=Ve)}function Ce(e,n,t=[]){let r=[];for(let a of n)a.fromTag!==e||a.direction===null||r.push({at:a.at,direction:a.direction,to:a.toTag});for(let a of t)a.tag!==e||!a.resolved||a.movedTo!==null||r.push({at:a.at,direction:a.direction,to:null});r.sort((a,o)=>a.at-o.at);let i={};for(let a of r)i[a.direction]=a.to;return i}var De=1.5*1024*1024,Vn=500;q&&(de(),ge());var y=null,J=false,M=null,V=0;function Ie(){let e=new Map;for(let n of y?.nodes??[])e.set(n.nativeTag,n);return e}async function W(){if(J)return y;J=true;try{return y=await Se(),y}catch{return y}finally{J=false,qe()}}function Cn(){if(V===0)return;let{currentTag:e}=H();e!==null&&(Ie().has(e)||M===null&&(M=setTimeout(()=>{M=null,W()},Vn)))}function Dn(e,n){let t=[],r=0;for(let i=e.length-1;i>=0;i--){let a=ee(e[i],n).bytes;if(r+a>n&&t.length>0)break;t.push(e[i]),r+=a}return t.reverse(),t}function In(){let e=H(),n=y?.nodes??[],t=Je(),r=Pe(e.transitions,e.probes,n,e.lostCount,y?.scannedAt??0),i=Me(n,e.visited,e.transitions,e.probes),a=Ie(),o=ee(n,De).bytes,l=Dn(e.transitions,Math.max(64*1024,De-o)),s=r.deadEnds.filter(u=>!u.atEdge).length+r.traps.length+r.invisibleStops.length+(r.lostCount>0?1:0);return{supported:q||I==="web",platform:I,observer:t.status,observerReason:t.reason,keysListening:Xe(),tracking:e.tracking,presence:e.presence,current:e.currentTag===null?null:{tag:e.currentTag,node:a.get(e.currentTag)??null,since:e.currentSince??Date.now(),edges:Ce(e.currentTag,e.transitions,e.probes)},transitions:l,focusables:n,screen:y?.screen??null,scannedAt:y?.scannedAt??null,flags:r,coverage:i,stats:{focusables:i.focusables,visited:i.visited,transitions:e.transitionCount,flagged:s},capturedAt:Date.now()}}var Ln={version:1,getSnapshot(){return In()},subscribe(e){V+=1,V===1&&(I==="web"&&(de(),ge()),W());let n=se(()=>{Cn(),e()}),t=0,r=null,i=()=>{if(t+=1,(y?.nodes.length??0)>0||t>=100){r=null;return}W(),r=setTimeout(i,100)};return r=setTimeout(i,100),()=>{n(),r!==null&&clearTimeout(r),V=Math.max(0,V-1),V===0&&(M!==null&&(clearTimeout(M),M=null),I==="web"&&(Ye(),Ze()))}},actions:{async rescan(){return{ok:true,focusables:(await W())?.nodes.length??0}},clearHistory(){return oe(),{ok:true}},setTracking(e){return le(e?.enabled!==false),{tracking:H().tracking}},focusElement(e){let n=e?.nativeTag;return typeof n!="number"?{ok:false,reason:"nativeTag is required."}:!q&&I!=="web"?{ok:false,reason:"Not a TV build \u2014 there is no focus engine."}:Sn(n)}}};export{oe as clearHistory,Me as computeCoverage,Ae as detectDeadEnds,Pe as detectFlags,Fe as detectInvisibleStops,Ee as detectTraps,Ln as focusInspectorSyncAdapter,H as getState,Ce as observedEdges,re as recordBlur,ae as recordFocus,ie as recordKey,Ue as resetStore,Se as scanFocusables,le as setTracking,se as subscribe};