@buoy-gg/focus-inspector 7.0.31
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/LICENSE +58 -0
- package/README.md +136 -0
- package/lib/commonjs/detect/detectors.js +351 -0
- package/lib/commonjs/detect/detectors.js.map +1 -0
- package/lib/commonjs/index.js +106 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/observe/focusEventObserver.js +105 -0
- package/lib/commonjs/observe/focusEventObserver.js.map +1 -0
- package/lib/commonjs/observe/keyEventObserver.js +94 -0
- package/lib/commonjs/observe/keyEventObserver.js.map +1 -0
- package/lib/commonjs/observe/rawEventEmitter.js +44 -0
- package/lib/commonjs/observe/rawEventEmitter.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/scan/focusableScanner.js +497 -0
- package/lib/commonjs/scan/focusableScanner.js.map +1 -0
- package/lib/commonjs/store/focusStore.js +325 -0
- package/lib/commonjs/store/focusStore.js.map +1 -0
- package/lib/commonjs/sync/focusInspectorSyncAdapter.js +247 -0
- package/lib/commonjs/sync/focusInspectorSyncAdapter.js.map +1 -0
- package/lib/module/detect/detectors.js +341 -0
- package/lib/module/detect/detectors.js.map +1 -0
- package/lib/module/index.js +36 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/observe/focusEventObserver.js +99 -0
- package/lib/module/observe/focusEventObserver.js.map +1 -0
- package/lib/module/observe/keyEventObserver.js +86 -0
- package/lib/module/observe/keyEventObserver.js.map +1 -0
- package/lib/module/observe/rawEventEmitter.js +40 -0
- package/lib/module/observe/rawEventEmitter.js.map +1 -0
- package/lib/module/scan/focusableScanner.js +492 -0
- package/lib/module/scan/focusableScanner.js.map +1 -0
- package/lib/module/store/focusStore.js +312 -0
- package/lib/module/store/focusStore.js.map +1 -0
- package/lib/module/sync/focusInspectorSyncAdapter.js +244 -0
- package/lib/module/sync/focusInspectorSyncAdapter.js.map +1 -0
- package/lib/typescript/detect/detectors.d.ts +122 -0
- package/lib/typescript/detect/detectors.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +28 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/observe/focusEventObserver.d.ts +34 -0
- package/lib/typescript/observe/focusEventObserver.d.ts.map +1 -0
- package/lib/typescript/observe/keyEventObserver.d.ts +22 -0
- package/lib/typescript/observe/keyEventObserver.d.ts.map +1 -0
- package/lib/typescript/observe/rawEventEmitter.d.ts +29 -0
- package/lib/typescript/observe/rawEventEmitter.d.ts.map +1 -0
- package/lib/typescript/scan/focusableScanner.d.ts +95 -0
- package/lib/typescript/scan/focusableScanner.d.ts.map +1 -0
- package/lib/typescript/store/focusStore.d.ts +135 -0
- package/lib/typescript/store/focusStore.d.ts.map +1 -0
- package/lib/typescript/sync/focusInspectorSyncAdapter.d.ts +107 -0
- package/lib/typescript/sync/focusInspectorSyncAdapter.d.ts.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detectors — pure functions from the recorded stream to the three TV focus bug
|
|
3
|
+
* classes. No timers, no RN, no fiber access, so every rule below is a unit
|
|
4
|
+
* test rather than a device session.
|
|
5
|
+
*
|
|
6
|
+
* The honesty rule that shapes all of this: focus can only be OBSERVED from JS,
|
|
7
|
+
* never computed. There is no "where would RIGHT take me" API on either
|
|
8
|
+
* platform, and a JS re-implementation of Android's FocusFinder geometry would
|
|
9
|
+
* lie in exactly the cases this tool exists for (guides, traps, nextFocus*
|
|
10
|
+
* overrides). So geometry is only ever used to *downgrade* a flag ("that's the
|
|
11
|
+
* screen edge, not a dead end") or to *suspect* one — never to assert what the
|
|
12
|
+
* engine would do.
|
|
13
|
+
*/
|
|
14
|
+
import type { FocusDirection, FocusProbe, FocusTransition } from "../store/focusStore";
|
|
15
|
+
import type { FocusableNode } from "../scan/focusableScanner";
|
|
16
|
+
export declare const DIRECTIONS: FocusDirection[];
|
|
17
|
+
/** Consecutive failed presses in one direction before it counts as a dead end. */
|
|
18
|
+
export declare const DEAD_END_ATTEMPTS = 3;
|
|
19
|
+
/** Presses before "unvisited" is worth reporting at all — below this it's noise. */
|
|
20
|
+
export declare const COVERAGE_MIN_PROBES = 12;
|
|
21
|
+
export interface DeadEndFlag {
|
|
22
|
+
tag: number;
|
|
23
|
+
direction: FocusDirection;
|
|
24
|
+
/** Consecutive presses in this direction that moved nothing. */
|
|
25
|
+
attempts: number;
|
|
26
|
+
/**
|
|
27
|
+
* The element's frame abuts the viewport edge in this direction, so "focus
|
|
28
|
+
* didn't move" is the expected behavior, not a bug.
|
|
29
|
+
*/
|
|
30
|
+
atEdge: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface TrapFlag {
|
|
33
|
+
/** The element(s) focus cannot escape from, as a group. */
|
|
34
|
+
tags: number[];
|
|
35
|
+
/** What holds them: the enclosing guide's tag, or the single element's own. */
|
|
36
|
+
containerTag: number;
|
|
37
|
+
directions: FocusDirection[];
|
|
38
|
+
/**
|
|
39
|
+
* A `trapFocus*` prop on the enclosing guide explains the trap — probably
|
|
40
|
+
* intentional. NOTE: trapFocus does NOT hold on Android TV, so an OBSERVED
|
|
41
|
+
* trap there is always a real bug regardless of this hint.
|
|
42
|
+
*/
|
|
43
|
+
guideTrapTag?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface InvisibleStopFlag {
|
|
46
|
+
tag: number;
|
|
47
|
+
/** "guide" = a TVFocusGuideView (a stop with no highlight). */
|
|
48
|
+
reason: "guide" | "unknown";
|
|
49
|
+
at: number;
|
|
50
|
+
/** How many times focus has landed here. */
|
|
51
|
+
count: number;
|
|
52
|
+
}
|
|
53
|
+
export interface CoverageResult {
|
|
54
|
+
focusables: number;
|
|
55
|
+
visited: number;
|
|
56
|
+
unvisited: number[];
|
|
57
|
+
/**
|
|
58
|
+
* Unvisited AND no observed edge points at it AND a visited neighbour sits
|
|
59
|
+
* next to it geometrically. A SUSPICION, never a proof — say "suspected
|
|
60
|
+
* unreachable" in the UI, never "unreachable".
|
|
61
|
+
*/
|
|
62
|
+
suspectedUnreachable: number[];
|
|
63
|
+
/** False until enough traversal has happened for absence to mean anything. */
|
|
64
|
+
meaningful: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface FocusFlags {
|
|
67
|
+
deadEnds: DeadEndFlag[];
|
|
68
|
+
traps: TrapFlag[];
|
|
69
|
+
invisibleStops: InvisibleStopFlag[];
|
|
70
|
+
lostCount: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A direction can legitimately move focus sometimes and not others (a rail
|
|
74
|
+
* scrolls, then hits its end), so a dead end is the *trailing run* of failures,
|
|
75
|
+
* not a lifetime tally. One successful move clears it.
|
|
76
|
+
*/
|
|
77
|
+
export declare function detectDeadEnds(probes: FocusProbe[], nodes: FocusableNode[], minAttempts?: number, since?: number): DeadEndFlag[];
|
|
78
|
+
/**
|
|
79
|
+
* A trap is a REGION focus cannot get OUT of, which is usually not one element
|
|
80
|
+
* and is not "all four directions are dead".
|
|
81
|
+
*
|
|
82
|
+
* Both of the obvious rules were tried against the armed trap in the TV example
|
|
83
|
+
* (a row of three tiles in one `TVFocusGuideView`) and both missed it. Per
|
|
84
|
+
* element: left/right move freely between the tiles, so no single tile
|
|
85
|
+
* dead-ends four ways. All-four-directions on the group: the box is
|
|
86
|
+
* left-aligned with everything else on the screen, so LEFT is a legitimate
|
|
87
|
+
* screen edge and the union never reaches four.
|
|
88
|
+
*
|
|
89
|
+
* What actually defines the bug is escape: for every direction in which
|
|
90
|
+
* something focusable EXISTS outside the container, focus has to have been
|
|
91
|
+
* observed failing to get there. A direction with nothing beyond it is not
|
|
92
|
+
* evidence either way, and a container with nothing outside it at all is the
|
|
93
|
+
* whole screen, not a trap.
|
|
94
|
+
*/
|
|
95
|
+
export declare function detectTraps(deadEnds: DeadEndFlag[], nodes: FocusableNode[]): TrapFlag[];
|
|
96
|
+
/**
|
|
97
|
+
* Focus that parked somewhere with no visible highlight — the "focus vanished"
|
|
98
|
+
* symptom. Two shapes: it landed on a TVFocusGuideView (a real focus stop the
|
|
99
|
+
* user cannot see), or on a tag the inventory has never heard of (a view
|
|
100
|
+
* outside the walked tree, or one that unmounted since the last scan).
|
|
101
|
+
*/
|
|
102
|
+
export declare function detectInvisibleStops(transitions: FocusTransition[], nodes: FocusableNode[], since?: number): InvisibleStopFlag[];
|
|
103
|
+
/**
|
|
104
|
+
* `since` is the timestamp of the inventory these flags are judged against —
|
|
105
|
+
* normally the last scan. Everything older belonged to a screen that no longer
|
|
106
|
+
* exists, and judging it here produces confident nonsense.
|
|
107
|
+
*/
|
|
108
|
+
export declare function detectFlags(transitions: FocusTransition[], probes: FocusProbe[], nodes: FocusableNode[], lostCount: number, since?: number): FocusFlags;
|
|
109
|
+
/**
|
|
110
|
+
* Coverage. Observed absence is EVIDENCE, not proof: focus is nondeterministic
|
|
111
|
+
* enough on tvOS that "never visited" cannot be promoted to "unreachable"
|
|
112
|
+
* without native support that does not exist. So the strong word is reserved
|
|
113
|
+
* for the case where geometry AND the observed graph agree.
|
|
114
|
+
*/
|
|
115
|
+
export declare function computeCoverage(nodes: FocusableNode[], visited: number[], transitions: FocusTransition[], probes: FocusProbe[]): CoverageResult;
|
|
116
|
+
/**
|
|
117
|
+
* The observed out-edges of one element — what the desktop's D-pad compass
|
|
118
|
+
* renders. A direction with no entry is UNKNOWN (dim), which is a different
|
|
119
|
+
* statement from a dead end (pressed, went nowhere).
|
|
120
|
+
*/
|
|
121
|
+
export declare function observedEdges(tag: number, transitions: FocusTransition[]): Partial<Record<FocusDirection, number | null>>;
|
|
122
|
+
//# sourceMappingURL=detectors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detectors.d.ts","sourceRoot":"","sources":["../../../src/detect/detectors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACvF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,eAAO,MAAM,UAAU,EAAE,cAAc,EAAoC,CAAC;AAE5E,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC,oFAAoF;AACpF,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,cAAc,CAAC;IAC1B,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,+EAA+E;IAC/E,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,+DAA+D;IAC/D,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB;;;;OAIG;IACH,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,8EAA8E;IAC9E,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,cAAc,EAAE,iBAAiB,EAAE,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,UAAU,EAAE,EACpB,KAAK,EAAE,aAAa,EAAE,EACtB,WAAW,GAAE,MAA0B,EACvC,KAAK,SAAI,GACR,WAAW,EAAE,CA2Bf;AA8FD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,QAAQ,EAAE,CA6EvF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,eAAe,EAAE,EAC9B,KAAK,EAAE,aAAa,EAAE,EACtB,KAAK,SAAI,GACR,iBAAiB,EAAE,CAiCrB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,WAAW,EAAE,eAAe,EAAE,EAC9B,MAAM,EAAE,UAAU,EAAE,EACpB,KAAK,EAAE,aAAa,EAAE,EACtB,SAAS,EAAE,MAAM,EACjB,KAAK,SAAI,GACR,UAAU,CAQZ;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,EAAE,EACtB,OAAO,EAAE,MAAM,EAAE,EACjB,WAAW,EAAE,eAAe,EAAE,EAC9B,MAAM,EAAE,UAAU,EAAE,GACnB,cAAc,CAuChB;AAsBD;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,eAAe,EAAE,GAC7B,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAOhD"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @buoy-gg/focus-inspector
|
|
3
|
+
*
|
|
4
|
+
* TV focus debugging for React Native TV apps, from Buoy Desktop: what holds
|
|
5
|
+
* D-pad focus right now, every focus transition with the direction that caused
|
|
6
|
+
* it, the inventory of focusable elements, and automatic flags for the three
|
|
7
|
+
* big TV focus bug classes — focus gets STUCK (dead end / trap), focus
|
|
8
|
+
* DISAPPEARS (parks on an invisible stop or leaves the RN tree), and an element
|
|
9
|
+
* is never REACHED.
|
|
10
|
+
*
|
|
11
|
+
* This package renders NOTHING on the device, and that is a hard constraint
|
|
12
|
+
* rather than a preference: on Android TV any focusable view in an overlay
|
|
13
|
+
* becomes a D-pad stop in the host app's focus order, so an on-device panel
|
|
14
|
+
* would change the navigation it is measuring. See the README.
|
|
15
|
+
*
|
|
16
|
+
* It also ships no native code — the focus stream comes from a JS-only global
|
|
17
|
+
* observer (`RawEventEmitter`'s `topFocus`/`topBlur`), which is why Buoy stays
|
|
18
|
+
* podspec-free.
|
|
19
|
+
*/
|
|
20
|
+
export { focusInspectorSyncAdapter } from "./sync/focusInspectorSyncAdapter";
|
|
21
|
+
export type { FocusInspectorSnapshot } from "./sync/focusInspectorSyncAdapter";
|
|
22
|
+
export { clearHistory, getState, recordBlur, recordFocus, recordKey, resetStore, setTracking, subscribe, } from "./store/focusStore";
|
|
23
|
+
export type { FocusDirection, FocusProbe, FocusStoreState, FocusTransition, } from "./store/focusStore";
|
|
24
|
+
export { scanFocusables } from "./scan/focusableScanner";
|
|
25
|
+
export type { FocusableNode, FocusFrame, FocusGuideInfo, FocusScanResult, } from "./scan/focusableScanner";
|
|
26
|
+
export { computeCoverage, detectDeadEnds, detectFlags, detectInvisibleStops, detectTraps, observedEdges, } from "./detect/detectors";
|
|
27
|
+
export type { CoverageResult, DeadEndFlag, FocusFlags, InvisibleStopFlag, TrapFlag, } from "./detect/detectors";
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH,OAAO,EAAE,yBAAyB,EAAE,MAAM,kCAAkC,CAAC;AAC7E,YAAY,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAK/E,OAAO,EACL,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,WAAW,EACX,SAAS,EACT,UAAU,EACV,WAAW,EACX,SAAS,GACV,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,cAAc,EACd,UAAU,EACV,eAAe,EACf,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,YAAY,EACV,aAAa,EACb,UAAU,EACV,cAAc,EACd,eAAe,GAChB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,eAAe,EACf,cAAc,EACd,WAAW,EACX,oBAAoB,EACpB,WAAW,EACX,aAAa,GACd,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,cAAc,EACd,WAAW,EACX,UAAU,EACV,iBAAiB,EACjB,QAAQ,GACT,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* focusEventObserver — the global, zero-instrumentation focus source.
|
|
3
|
+
*
|
|
4
|
+
* `react-native/Libraries/Core/RawEventEmitter` mirrors every Fabric-dispatched
|
|
5
|
+
* event BEFORE React's plugins or components see it, and on both tvOS and
|
|
6
|
+
* Android TV every focus change arrives there as `topFocus` / `topBlur` with
|
|
7
|
+
* the focused view's native tag. Its own source comment blesses exactly this
|
|
8
|
+
* use ("app-specific perf monitoring … and maybe debugging").
|
|
9
|
+
*
|
|
10
|
+
* Why not the obvious alternatives:
|
|
11
|
+
* - `onFocus`/`onBlur` props would require instrumenting the app's components,
|
|
12
|
+
* and would MISS the interesting cases: focus really does park on views with
|
|
13
|
+
* no handler (a TVFocusGuideView with `destinations` is an invisible focus
|
|
14
|
+
* stop). A per-component observer reports "focus is nowhere" for the exact
|
|
15
|
+
* bug this tool exists to catch.
|
|
16
|
+
* - `TVEventHandler`'s `focus`/`blur` HWEvents are deprecated and 0.85 deleted
|
|
17
|
+
* the Fabric calls that emitted them (fork issue #1037). They still fire on
|
|
18
|
+
* the 0.83 line, so they're folded in as optional enrichment below — never
|
|
19
|
+
* required, or the tool would work on one app and not the other.
|
|
20
|
+
* - There is no root-level capture View to wrap the app in: Buoy mounts as a
|
|
21
|
+
* SIBLING of the app tree, not a parent.
|
|
22
|
+
*
|
|
23
|
+
* The deep import is guarded. If it ever moves, the snapshot degrades to
|
|
24
|
+
* `observer: "unavailable"` and the panel says so, rather than the tool taking
|
|
25
|
+
* the app's bundle down.
|
|
26
|
+
*/
|
|
27
|
+
export type ObserverStatus = "idle" | "listening" | "unavailable";
|
|
28
|
+
export declare function attachFocusObserver(): void;
|
|
29
|
+
export declare function detachFocusObserver(): void;
|
|
30
|
+
export declare function getFocusObserverStatus(): {
|
|
31
|
+
status: ObserverStatus;
|
|
32
|
+
reason: string | null;
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=focusEventObserver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focusEventObserver.d.ts","sourceRoot":"","sources":["../../../src/observe/focusEventObserver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AASH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,WAAW,GAAG,aAAa,CAAC;AA6BlE,wBAAgB,mBAAmB,IAAI,IAAI,CA2B1C;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAU1C;AAED,wBAAgB,sBAAsB,IAAI;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAE1F"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keyEventObserver — the direction half of the stream.
|
|
3
|
+
*
|
|
4
|
+
* D-pad HWEvents carry NO tag on either platform (an Android payload is exactly
|
|
5
|
+
* `{"eventKeyAction":1,"eventType":"right"}`), and focus events carry no
|
|
6
|
+
* direction. So key events tell you *which way the user pushed* and focus
|
|
7
|
+
* events tell you *where focus went*; the store joins them on time.
|
|
8
|
+
*
|
|
9
|
+
* `TVEventHandler.addListener` (not the `useTVEventHandler` hook) because sync
|
|
10
|
+
* adapters are not React components — nothing here mounts.
|
|
11
|
+
*
|
|
12
|
+
* On the 0.83 fork line iOS Fabric ALSO still emits tagged `focus`/`blur`
|
|
13
|
+
* HWEvents here. Those are folded into the same store as enrichment so the
|
|
14
|
+
* older line gets a focus source even if the RawEventEmitter path ever fails —
|
|
15
|
+
* never depended on, because 0.85 deleted them.
|
|
16
|
+
*/
|
|
17
|
+
export declare const isTV: boolean;
|
|
18
|
+
export declare const platformOS: string;
|
|
19
|
+
export declare function attachKeyObserver(): void;
|
|
20
|
+
export declare function detachKeyObserver(): void;
|
|
21
|
+
export declare function isKeyObserverListening(): boolean;
|
|
22
|
+
//# sourceMappingURL=keyEventObserver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyEventObserver.d.ts","sourceRoot":"","sources":["../../../src/observe/keyEventObserver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAuBH,eAAO,MAAM,IAAI,SAA6B,CAAC;AAC/C,eAAO,MAAM,UAAU,QAA+B,CAAC;AAiDvD,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAQxC;AAED,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one guarded require for React Native's RawEventEmitter, kept in its own
|
|
3
|
+
* module for two reasons.
|
|
4
|
+
*
|
|
5
|
+
* 1. `react-native/Libraries/Core/RawEventEmitter` is a DEEP import. It warns
|
|
6
|
+
* under 0.85's export map and could move in any release, so the failure has
|
|
7
|
+
* to degrade to "observer unavailable" in the snapshot rather than taking
|
|
8
|
+
* the host app's bundle down. Metro only treats a require as optional when
|
|
9
|
+
* it sits LEXICALLY inside a try statement — wrapping it in a helper or an
|
|
10
|
+
* arrow makes Metro hard-fail the bundle instead.
|
|
11
|
+
* 2. It gives the unit suite a seam. Node's `require` resolves through the
|
|
12
|
+
* filesystem and ignores Vitest's module mocks, so the emitter can only be
|
|
13
|
+
* faked by mocking a module that is reached with a real `import`.
|
|
14
|
+
*/
|
|
15
|
+
export interface RawEventSubscription {
|
|
16
|
+
remove: () => void;
|
|
17
|
+
}
|
|
18
|
+
export interface RawEventEmitterLike {
|
|
19
|
+
addListener: (eventType: string, listener: (event: {
|
|
20
|
+
eventName?: string;
|
|
21
|
+
nativeEvent?: Record<string, unknown>;
|
|
22
|
+
}) => void) => RawEventSubscription;
|
|
23
|
+
}
|
|
24
|
+
export interface RawEmitterResolution {
|
|
25
|
+
emitter: RawEventEmitterLike | null;
|
|
26
|
+
reason: string | null;
|
|
27
|
+
}
|
|
28
|
+
export declare function resolveRawEventEmitter(): RawEmitterResolution;
|
|
29
|
+
//# sourceMappingURL=rawEventEmitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rawEventEmitter.d.ts","sourceRoot":"","sources":["../../../src/observe/rawEventEmitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,CACX,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,KAAK,IAAI,KACrF,oBAAoB,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACpC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,wBAAgB,sBAAsB,IAAI,oBAAoB,CAc7D"}
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
export interface FocusFrame {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
}
|
|
31
|
+
export interface FocusGuideInfo {
|
|
32
|
+
autoFocus: boolean;
|
|
33
|
+
/** `destinations` is applied via a native command, so only presence is known. */
|
|
34
|
+
hasDestinations: boolean;
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
trap: {
|
|
37
|
+
up: boolean;
|
|
38
|
+
down: boolean;
|
|
39
|
+
left: boolean;
|
|
40
|
+
right: boolean;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface FocusableNode {
|
|
44
|
+
nativeTag: number;
|
|
45
|
+
name: string;
|
|
46
|
+
owner?: string;
|
|
47
|
+
testID?: string;
|
|
48
|
+
label?: string;
|
|
49
|
+
text?: string;
|
|
50
|
+
/** Normalized [0,1] rect. Absent when the node could not be measured. */
|
|
51
|
+
frame?: FocusFrame;
|
|
52
|
+
focusProps: {
|
|
53
|
+
focusable?: boolean;
|
|
54
|
+
hasTVPreferredFocus?: boolean;
|
|
55
|
+
/** Which `nextFocus*` overrides are set — the values are native tags. */
|
|
56
|
+
nextFocus?: {
|
|
57
|
+
up?: boolean;
|
|
58
|
+
down?: boolean;
|
|
59
|
+
left?: boolean;
|
|
60
|
+
right?: boolean;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
/** Present when this node IS a TVFocusGuideView (an invisible focus stop). */
|
|
64
|
+
guide?: FocusGuideInfo;
|
|
65
|
+
/** Native tag of the nearest enclosing guide, for the "intentional trap?" hint. */
|
|
66
|
+
guideAncestorTag?: number;
|
|
67
|
+
/** True when a press handler is reachable — a Pressable/Touchable stop. */
|
|
68
|
+
pressable?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* False when the element's box sits outside the window right now — below the
|
|
71
|
+
* fold of a ScrollView, typically. Still a real D-pad target (the engine
|
|
72
|
+
* scrolls to it), so it stays in the inventory and in the coverage
|
|
73
|
+
* denominator; it is only marked so the panel can say why it is unvisited.
|
|
74
|
+
*/
|
|
75
|
+
onScreen?: boolean;
|
|
76
|
+
}
|
|
77
|
+
export interface FocusScanResult {
|
|
78
|
+
screen: {
|
|
79
|
+
width: number;
|
|
80
|
+
height: number;
|
|
81
|
+
};
|
|
82
|
+
nodes: FocusableNode[];
|
|
83
|
+
scannedAt: number;
|
|
84
|
+
}
|
|
85
|
+
export declare function scanFocusables(): Promise<FocusScanResult>;
|
|
86
|
+
/**
|
|
87
|
+
* Move focus to a native tag. Source-verified only until this runs on a device:
|
|
88
|
+
* `Commands.requestTVFocus` takes an instance ref, never a tag, so the last
|
|
89
|
+
* scan's instance table is the only way to get from one to the other.
|
|
90
|
+
*/
|
|
91
|
+
export declare function requestFocusForTag(nativeTag: number): {
|
|
92
|
+
ok: boolean;
|
|
93
|
+
reason?: string;
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=focusableScanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focusableScanner.d.ts","sourceRoot":"","sources":["../../../src/scan/focusableScanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AA8BH,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,OAAO,CAAC;IACnB,iFAAiF;IACjF,eAAe,EAAE,OAAO,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC;CACrE;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE;QACV,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,mBAAmB,CAAC,EAAE,OAAO,CAAC;QAC9B,yEAAyE;QACzE,SAAS,CAAC,EAAE;YAAE,EAAE,CAAC,EAAE,OAAO,CAAC;YAAC,IAAI,CAAC,EAAE,OAAO,CAAC;YAAC,IAAI,CAAC,EAAE,OAAO,CAAC;YAAC,KAAK,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC;KAC/E,CAAC;IACF,8EAA8E;IAC9E,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AA6XD,wBAAsB,cAAc,IAAI,OAAO,CAAC,eAAe,CAAC,CA0D/D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAgBtF"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* focusStore — the device-side memory of the TV focus inspector.
|
|
3
|
+
*
|
|
4
|
+
* CONTRACT: this store only ever OBSERVES. It never adds a view, never sets a
|
|
5
|
+
* prop, never calls a focus command on its own. On Android TV any focusable
|
|
6
|
+
* view we rendered would become a D-pad stop in the host app's focus order, so
|
|
7
|
+
* a tool that measures focus by participating in it would measure itself. The
|
|
8
|
+
* only thing that ever moves focus is the explicit `focusElement` action a
|
|
9
|
+
* human triggers from the dashboard.
|
|
10
|
+
*
|
|
11
|
+
* It is deliberately react-native-free: observers push events in, so every rule
|
|
12
|
+
* below (transition pairing, direction attribution, dead-end probes) is a pure
|
|
13
|
+
* function of a timestamped event stream and is unit-testable without a device.
|
|
14
|
+
*/
|
|
15
|
+
/** The four D-pad directions. Select/menu never move focus, so they're dropped. */
|
|
16
|
+
export type FocusDirection = "up" | "down" | "left" | "right";
|
|
17
|
+
export interface FocusTransition {
|
|
18
|
+
/** Monotonic counter — survives ring-buffer trimming, so it's a stable key. */
|
|
19
|
+
seq: number;
|
|
20
|
+
/** Where focus was. `null` at boot (first focus) or after focus was lost. */
|
|
21
|
+
fromTag: number | null;
|
|
22
|
+
/** Where focus went. `null` means focus LEFT the RN tree (the "vanished" bug). */
|
|
23
|
+
toTag: number | null;
|
|
24
|
+
/** D-pad direction credited to this move, when one could be attributed. */
|
|
25
|
+
direction: FocusDirection | null;
|
|
26
|
+
at: number;
|
|
27
|
+
/** How long focus sat on `fromTag` before this move. */
|
|
28
|
+
dwellMs?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* One D-pad press, recorded against the element that held focus when it
|
|
32
|
+
* happened, plus what it did. A probe with `movedTo: null` is one piece of
|
|
33
|
+
* dead-end evidence; N consecutive ones in the same direction is a flag.
|
|
34
|
+
*/
|
|
35
|
+
export interface FocusProbe {
|
|
36
|
+
seq: number;
|
|
37
|
+
/** Focused element at press time. `null` = nothing had focus (already lost). */
|
|
38
|
+
tag: number | null;
|
|
39
|
+
direction: FocusDirection;
|
|
40
|
+
at: number;
|
|
41
|
+
/** False while we're still inside the attribution window. */
|
|
42
|
+
resolved: boolean;
|
|
43
|
+
/** Where focus went, or `null` for "focus did not move". */
|
|
44
|
+
movedTo: number | null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Why nothing is reported as focused. These are NOT the same thing and must
|
|
48
|
+
* never be rendered the same way:
|
|
49
|
+
*
|
|
50
|
+
* - "never-observed": we have not seen a single focus event yet. Focus is
|
|
51
|
+
* almost certainly fine — there is no API for "which view has focus", so the
|
|
52
|
+
* tool learns the answer only when focus MOVES. Attaching to an app that is
|
|
53
|
+
* sitting still looks exactly like this until the first press.
|
|
54
|
+
* - "lost": we WATCHED a view blur with nothing taking over. That is the real
|
|
55
|
+
* "focus vanished" bug.
|
|
56
|
+
*/
|
|
57
|
+
export type FocusPresence = "focused" | "never-observed" | "lost";
|
|
58
|
+
export interface FocusStoreState {
|
|
59
|
+
currentTag: number | null;
|
|
60
|
+
presence: FocusPresence;
|
|
61
|
+
/** When focus arrived at `currentTag` (for the dwell timer on the Now card). */
|
|
62
|
+
currentSince: number | null;
|
|
63
|
+
transitions: FocusTransition[];
|
|
64
|
+
probes: FocusProbe[];
|
|
65
|
+
/** Every tag focus has ever landed on this session. */
|
|
66
|
+
visited: number[];
|
|
67
|
+
/** Transitions whose `toTag` was null — focus left the tree. */
|
|
68
|
+
lostCount: number;
|
|
69
|
+
/** Total transitions recorded, including ones trimmed out of the buffer. */
|
|
70
|
+
transitionCount: number;
|
|
71
|
+
tracking: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A D-pad press and the focus pair it caused arrive in EITHER order, a few ms
|
|
75
|
+
* apart — both `HWEvent → blur → focus` and `blur → focus → HWEvent` were
|
|
76
|
+
* observed live on the same session. So direction attribution is a time-window
|
|
77
|
+
* join in both directions, never a sequence assumption.
|
|
78
|
+
*/
|
|
79
|
+
export declare const DIRECTION_WINDOW_MS = 250;
|
|
80
|
+
/**
|
|
81
|
+
* The window for crediting a press to a move that was ALREADY recorded — the
|
|
82
|
+
* `blur → focus → HWEvent` ordering. Much tighter than the forward window on
|
|
83
|
+
* purpose: forward attribution is waiting for a consequence, but backward
|
|
84
|
+
* attribution is claiming a cause, and a press 200ms after a move is far more
|
|
85
|
+
* likely to be the NEXT press than a late-delivered echo of the last one.
|
|
86
|
+
* Delivery races were observed at a few ms.
|
|
87
|
+
*/
|
|
88
|
+
export declare const BACKFILL_WINDOW_MS = 60;
|
|
89
|
+
/**
|
|
90
|
+
* How long a blur waits for its matching focus before we call it a loss. Focus
|
|
91
|
+
* pairs land within a frame or two; anything past this really has left the RN
|
|
92
|
+
* tree (system alert, another window, an unmounted subtree).
|
|
93
|
+
*/
|
|
94
|
+
export declare const FOCUS_LOST_MS = 400;
|
|
95
|
+
/** A view took focus. `tag` is the native tag from the raw event's target. */
|
|
96
|
+
export declare function recordFocus(tag: number, at?: number): void;
|
|
97
|
+
/**
|
|
98
|
+
* A view lost focus. Held rather than recorded: the matching focus lands within
|
|
99
|
+
* a frame or two and turns this into a normal transition. Only a blur with NO
|
|
100
|
+
* follow-up is the "focus vanished" bug, and that is worth reporting.
|
|
101
|
+
*
|
|
102
|
+
* THE ORDERING TRAP: blur does NOT reliably precede its focus. On tvOS Fabric
|
|
103
|
+
* 0.85 the emitter delivers `topFocus(new)` and only THEN `topBlur(old)`, ~1ms
|
|
104
|
+
* apart (verified on the Apple TV simulator). A blur for a tag that no longer
|
|
105
|
+
* holds focus is therefore a late echo of a move already recorded — swallowing
|
|
106
|
+
* it is what keeps the tool from arming a loss timer for an element that was
|
|
107
|
+
* never actually orphaned, and then reporting a phantom "focus lost" plus a
|
|
108
|
+
* wrong `fromTag` on the next real move. Only a blur for the CURRENT holder can
|
|
109
|
+
* mean focus is leaving, and that test is order-agnostic on both platforms.
|
|
110
|
+
*/
|
|
111
|
+
export declare function recordBlur(tag: number, at?: number): void;
|
|
112
|
+
/**
|
|
113
|
+
* A D-pad direction was pressed. Opens a probe against whatever holds focus,
|
|
114
|
+
* which resolves either to the element focus moved to or to `null` — "this
|
|
115
|
+
* direction did nothing from here", the raw material of every dead-end flag.
|
|
116
|
+
*/
|
|
117
|
+
export declare function recordKey(direction: FocusDirection, at?: number): void;
|
|
118
|
+
export declare function getState(): FocusStoreState;
|
|
119
|
+
/**
|
|
120
|
+
* Wipe the history but KEEP where focus currently is — clearing is how a user
|
|
121
|
+
* starts a clean traversal run, and forgetting the current element would blank
|
|
122
|
+
* the Now card until the next press.
|
|
123
|
+
*/
|
|
124
|
+
export declare function clearHistory(): void;
|
|
125
|
+
/**
|
|
126
|
+
* Pause/resume recording without detaching the native listeners. Useful when a
|
|
127
|
+
* user is driving the app by hand and doesn't want that traversal scored.
|
|
128
|
+
*/
|
|
129
|
+
export declare function setTracking(enabled: boolean): void;
|
|
130
|
+
/** Full reset, including timers — the observers' detach path and tests. */
|
|
131
|
+
export declare function resetStore(): void;
|
|
132
|
+
export declare function subscribe(listener: () => void): () => void;
|
|
133
|
+
/** Exposed so the scanner can nudge a rescan when focus lands on a stranger. */
|
|
134
|
+
export declare function notifyChange(): void;
|
|
135
|
+
//# sourceMappingURL=focusStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focusStore.d.ts","sourceRoot":"","sources":["../../../src/store/focusStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,mFAAmF;AACnF,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE9D,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAC;IACZ,6EAA6E;IAC7E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,kFAAkF;IAClF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,2EAA2E;IAC3E,SAAS,EAAE,cAAc,GAAG,IAAI,CAAC;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,gFAAgF;IAChF,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,SAAS,EAAE,cAAc,CAAC;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,6DAA6D;IAC7D,QAAQ,EAAE,OAAO,CAAC;IAClB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAElE,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,aAAa,CAAC;IACxB,gFAAgF;IAChF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;GAIG;AACH,eAAO,MAAM,aAAa,MAAM,CAAC;AA+EjC,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,GAAE,MAAmB,GAAG,IAAI,CAoBtE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,GAAE,MAAmB,GAAG,IAAI,CAerE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,cAAc,EAAE,EAAE,GAAE,MAAmB,GAAG,IAAI,CAsDlF;AAED,wBAAgB,QAAQ,IAAI,eAAe,CAc1C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAQnC;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAQlD;AAED,2EAA2E;AAC3E,wBAAgB,UAAU,IAAI,IAAI,CAiBjC;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAK1D;AAED,gFAAgF;AAChF,wBAAgB,YAAY,IAAI,IAAI,CAEnC"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* focusInspectorSyncAdapter — streams the focus picture to Buoy Desktop.
|
|
3
|
+
*
|
|
4
|
+
* Structurally matches external-sync's `ToolSyncAdapter` WITHOUT importing the
|
|
5
|
+
* package (the house rule every tool's adapter follows). Auto-discovery in
|
|
6
|
+
* @buoy-gg/core picks it up by export name and announces the "focus-inspector"
|
|
7
|
+
* capability.
|
|
8
|
+
*
|
|
9
|
+
* There is NO on-device UI and there never will be: on Android TV a focusable
|
|
10
|
+
* view in an overlay becomes a D-pad stop in the host app's focus order
|
|
11
|
+
* (`pointerEvents="box-none"` gates touch, not focus), so an on-device panel
|
|
12
|
+
* would corrupt the measurement. Everything device-side therefore lives in
|
|
13
|
+
* module scope and in this adapter's `subscribe` — headless FloatingDevTools
|
|
14
|
+
* mounts the adapters and NONE of the overlay components, so instrumentation
|
|
15
|
+
* inside a component would simply never run on TV.
|
|
16
|
+
*
|
|
17
|
+
* WORK SPLIT: the two event listeners attach at module load and stay attached;
|
|
18
|
+
* the fiber scan only runs while a dashboard is watching.
|
|
19
|
+
*
|
|
20
|
+
* That asymmetry is deliberate and was learned the hard way. Focus cannot be
|
|
21
|
+
* QUERIED on either platform — the tool only ever learns where focus is by
|
|
22
|
+
* watching it arrive. So an observer that attached on first `subscribe` knows
|
|
23
|
+
* nothing until focus next MOVES, which means opening the panel on a running
|
|
24
|
+
* app shows nothing, and, far worse, the tool is blind in precisely the case it
|
|
25
|
+
* exists for: focus is stuck, you press a direction, nothing moves, and so
|
|
26
|
+
* nothing is ever learned. Listening from the start is what makes "what holds
|
|
27
|
+
* focus right now" answerable at all.
|
|
28
|
+
*
|
|
29
|
+
* The cost is two event-emitter listeners and a bounded ring-buffer append per
|
|
30
|
+
* focus change — nothing next to the fiber walk, which stays gated on a
|
|
31
|
+
* dashboard actually watching. `setTracking(false)` is the off switch.
|
|
32
|
+
*/
|
|
33
|
+
import { type FocusDirection, type FocusPresence, type FocusTransition } from "../store/focusStore";
|
|
34
|
+
import { type ObserverStatus } from "../observe/focusEventObserver";
|
|
35
|
+
import { type FocusableNode } from "../scan/focusableScanner";
|
|
36
|
+
import { computeCoverage, type FocusFlags } from "../detect/detectors";
|
|
37
|
+
export interface FocusInspectorSnapshot {
|
|
38
|
+
/** False on phones/tablets/web — the panel renders an "not a TV build" state. */
|
|
39
|
+
supported: boolean;
|
|
40
|
+
platform: string;
|
|
41
|
+
/** "unavailable" means the RawEventEmitter deep import moved; nothing is being observed. */
|
|
42
|
+
observer: ObserverStatus;
|
|
43
|
+
observerReason: string | null;
|
|
44
|
+
/** Whether the D-pad direction stream is attached (0.85 TVEventHandler). */
|
|
45
|
+
keysListening: boolean;
|
|
46
|
+
tracking: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Why `current` is null, when it is. "never-observed" is the ordinary
|
|
49
|
+
* cold-start case and must NOT be reported as a bug: focus cannot be queried,
|
|
50
|
+
* only watched arriving, so an app sitting still looks identical to one with
|
|
51
|
+
* no focus until something moves.
|
|
52
|
+
*/
|
|
53
|
+
presence: FocusPresence;
|
|
54
|
+
current: {
|
|
55
|
+
tag: number;
|
|
56
|
+
node: FocusableNode | null;
|
|
57
|
+
since: number;
|
|
58
|
+
/** Where each direction has been OBSERVED to lead from here. */
|
|
59
|
+
edges: Partial<Record<FocusDirection, number | null>>;
|
|
60
|
+
} | null;
|
|
61
|
+
transitions: FocusTransition[];
|
|
62
|
+
focusables: FocusableNode[];
|
|
63
|
+
screen: {
|
|
64
|
+
width: number;
|
|
65
|
+
height: number;
|
|
66
|
+
} | null;
|
|
67
|
+
scannedAt: number | null;
|
|
68
|
+
flags: FocusFlags;
|
|
69
|
+
coverage: ReturnType<typeof computeCoverage>;
|
|
70
|
+
stats: {
|
|
71
|
+
focusables: number;
|
|
72
|
+
visited: number;
|
|
73
|
+
transitions: number;
|
|
74
|
+
flagged: number;
|
|
75
|
+
};
|
|
76
|
+
capturedAt: number;
|
|
77
|
+
}
|
|
78
|
+
export declare const focusInspectorSyncAdapter: {
|
|
79
|
+
version: number;
|
|
80
|
+
getSnapshot(): FocusInspectorSnapshot;
|
|
81
|
+
subscribe(onChange: () => void): () => void;
|
|
82
|
+
actions: {
|
|
83
|
+
rescan(): Promise<{
|
|
84
|
+
ok: true;
|
|
85
|
+
focusables: number;
|
|
86
|
+
}>;
|
|
87
|
+
clearHistory(): {
|
|
88
|
+
ok: true;
|
|
89
|
+
};
|
|
90
|
+
setTracking(params?: {
|
|
91
|
+
enabled?: boolean;
|
|
92
|
+
}): {
|
|
93
|
+
tracking: boolean;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Put focus on an element and watch what the D-pad does from there — the
|
|
97
|
+
* only write this tool ever performs, and only when a human asks for it.
|
|
98
|
+
*/
|
|
99
|
+
focusElement(params?: {
|
|
100
|
+
nativeTag?: number;
|
|
101
|
+
}): {
|
|
102
|
+
ok: boolean;
|
|
103
|
+
reason?: string;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=focusInspectorSyncAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focusInspectorSyncAdapter.d.ts","sourceRoot":"","sources":["../../../src/sync/focusInspectorSyncAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH,OAAO,EAML,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,+BAA+B,CAAC;AAOvC,OAAO,EAGL,KAAK,aAAa,EAEnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,eAAe,EAA8B,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAYnG,MAAM,WAAW,sBAAsB;IACrC,iFAAiF;IACjF,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,4FAA4F;IAC5F,QAAQ,EAAE,cAAc,CAAC;IACzB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,4EAA4E;IAC5E,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,EAAE;QACP,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,aAAa,GAAG,IAAI,CAAC;QAC3B,KAAK,EAAE,MAAM,CAAC;QACd,gEAAgE;QAChE,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;KACvD,GAAG,IAAI,CAAC;IACT,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IAC7C,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,UAAU,EAAE,MAAM,CAAC;CACpB;AAoID,eAAO,MAAM,yBAAyB;;mBAGrB,sBAAsB;wBAIjB,MAAM,IAAI,GAAG,MAAM,IAAI;;kBA6CzB,OAAO,CAAC;YAAE,EAAE,EAAE,IAAI,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAC;wBAKzC;YAAE,EAAE,EAAE,IAAI,CAAA;SAAE;6BAKP;YAAE,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,GAAG;YAAE,QAAQ,EAAE,OAAO,CAAA;SAAE;QAKlE;;;WAGG;8BACmB;YAAE,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG;YAAE,EAAE,EAAE,OAAO,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE;;CAOlF,CAAC"}
|