@unrulysystems/native-motion 0.1.0-alpha.0

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 (117) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +55 -0
  4. package/android/build.gradle +24 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/com/unrulysystems/nativemotion/GestureExclusionModule.kt +55 -0
  7. package/android/src/main/java/com/unrulysystems/nativemotion/NativeMotionPackage.kt +27 -0
  8. package/package.json +57 -0
  9. package/react-native.config.js +14 -0
  10. package/src/native/driver/instantWindow.ts +31 -0
  11. package/src/native/driver/strictModeReplay.ts +20 -0
  12. package/src/native/driver/uiLayoutEngine.ts +845 -0
  13. package/src/native/driver/uiLayoutGraph.ts +338 -0
  14. package/src/native/driver/uiValueChannel.ts +4640 -0
  15. package/src/native/driver/workletDriver.ts +3860 -0
  16. package/src/native/motion/AnimatePresence.tsx +1605 -0
  17. package/src/native/motion/LayoutGroup.tsx +163 -0
  18. package/src/native/motion/MotionConfig.tsx +400 -0
  19. package/src/native/motion/MotionRoot.tsx +250 -0
  20. package/src/native/motion/MotionView.tsx +6761 -0
  21. package/src/native/motion/addScaleCorrector.ts +97 -0
  22. package/src/native/motion/colorEndpointUiFeed.ts +32 -0
  23. package/src/native/motion/colorProjection.ts +39 -0
  24. package/src/native/motion/composeTransform.ts +271 -0
  25. package/src/native/motion/constraintMeasure.ts +157 -0
  26. package/src/native/motion/deferredPendingSnapshots.ts +25 -0
  27. package/src/native/motion/discreteProjection.ts +36 -0
  28. package/src/native/motion/dragAncestorPanContext.ts +28 -0
  29. package/src/native/motion/dragControls.ts +152 -0
  30. package/src/native/motion/dragGestureWiring.ts +1168 -0
  31. package/src/native/motion/dragHandoffBinding.ts +179 -0
  32. package/src/native/motion/dragHubStream.ts +756 -0
  33. package/src/native/motion/dragPropagationLock.ts +57 -0
  34. package/src/native/motion/driverValueChannel.ts +3288 -0
  35. package/src/native/motion/externalDragDriver.ts +214 -0
  36. package/src/native/motion/frameData.ts +31 -0
  37. package/src/native/motion/gestureBinding.ts +107 -0
  38. package/src/native/motion/gestureStateGate.ts +563 -0
  39. package/src/native/motion/gestureStateResolver.ts +286 -0
  40. package/src/native/motion/identityValueChannelControllerAdapter.ts +2043 -0
  41. package/src/native/motion/identityValueChannelControllerReconciliation.ts +176 -0
  42. package/src/native/motion/identityValueChannelLaneMarker.ts +14 -0
  43. package/src/native/motion/imperativeAnimate.ts +816 -0
  44. package/src/native/motion/keyframeTiming.ts +7 -0
  45. package/src/native/motion/layoutIdBinding.ts +261 -0
  46. package/src/native/motion/layoutIdFlightConfig.ts +51 -0
  47. package/src/native/motion/layoutProjection.ts +89 -0
  48. package/src/native/motion/layoutScroll.ts +204 -0
  49. package/src/native/motion/layoutTransition.ts +1302 -0
  50. package/src/native/motion/lengthLayoutContext.tsx +100 -0
  51. package/src/native/motion/lengthLayoutHost.ts +44 -0
  52. package/src/native/motion/mappedKeys.ts +184 -0
  53. package/src/native/motion/motionViewController.ts +3180 -0
  54. package/src/native/motion/nativeHostMarker.ts +22 -0
  55. package/src/native/motion/panSessionWiring.ts +127 -0
  56. package/src/native/motion/pathTransition.ts +101 -0
  57. package/src/native/motion/popLayout.ts +176 -0
  58. package/src/native/motion/presenceBinding.ts +382 -0
  59. package/src/native/motion/scaleCorrectorRegistry.ts +123 -0
  60. package/src/native/motion/scrollValues.ts +260 -0
  61. package/src/native/motion/serializablePayload.ts +151 -0
  62. package/src/native/motion/severity.ts +16 -0
  63. package/src/native/motion/shippedSurface.ts +843 -0
  64. package/src/native/motion/staticLengthGate.ts +86 -0
  65. package/src/native/motion/styleBaseGate.ts +72 -0
  66. package/src/native/motion/styleValueBinding.ts +657 -0
  67. package/src/native/motion/systemGestureExclusion.ts +61 -0
  68. package/src/native/motion/tapGestureWiring.ts +215 -0
  69. package/src/native/motion/transformOrder.ts +38 -0
  70. package/src/native/motion/transformStringBinding.ts +200 -0
  71. package/src/native/motion/transformTemplateGate.ts +45 -0
  72. package/src/native/motion/transitionGate.ts +435 -0
  73. package/src/native/motion/useAnimate.ts +52 -0
  74. package/src/native/motion/useCycle.ts +44 -0
  75. package/src/native/motion/useInstantTransition.ts +68 -0
  76. package/src/native/motion/useReducedMotion.ts +58 -0
  77. package/src/native/motion/useScroll.ts +162 -0
  78. package/src/native/motion/useViewportScroll.ts +29 -0
  79. package/src/native/motion/valueChannel.ts +7121 -0
  80. package/src/native/motion/valueHooks.ts +901 -0
  81. package/src/native/motion/variantChildRegistry.ts +67 -0
  82. package/src/native/motion/variantContext.tsx +172 -0
  83. package/src/native/motion/variantProps.ts +598 -0
  84. package/src/native.ts +159 -0
  85. package/src/verification/harnessMetrics.ts +78 -0
  86. package/src/verification/probe/LayoutIdentityWorkletProbe.tsx +251 -0
  87. package/src/verification/probe/WorkletParityProbe.tsx +162 -0
  88. package/src/verification/probe/layoutIdentityProbeEngine.ts +429 -0
  89. package/src/verification/screens/ArcPathChecksScreen.tsx +273 -0
  90. package/src/verification/screens/BooleanAnimateChecksScreen.tsx +358 -0
  91. package/src/verification/screens/ChoreographyGalleryScreen.tsx +1010 -0
  92. package/src/verification/screens/ColorBindingChecksScreen.tsx +568 -0
  93. package/src/verification/screens/CompletionChecksScreen.tsx +294 -0
  94. package/src/verification/screens/ConformanceScreen.tsx +573 -0
  95. package/src/verification/screens/ContentionProbeScreen.tsx +224 -0
  96. package/src/verification/screens/DriverSmokeScreen.tsx +99 -0
  97. package/src/verification/screens/DurationOnlyTweenChecksScreen.tsx +294 -0
  98. package/src/verification/screens/DynamicDragConfigChecksScreen.tsx +237 -0
  99. package/src/verification/screens/FrameDataChecksScreen.tsx +252 -0
  100. package/src/verification/screens/GestureChecksScreen.tsx +685 -0
  101. package/src/verification/screens/InstantTransitionChecksScreen.tsx +839 -0
  102. package/src/verification/screens/LayoutAnimationStartChecksScreen.tsx +648 -0
  103. package/src/verification/screens/LayoutChecksScreen.tsx +822 -0
  104. package/src/verification/screens/LayoutCommitSpikeScreen.tsx +142 -0
  105. package/src/verification/screens/MotionViewChecksScreen.tsx +930 -0
  106. package/src/verification/screens/PresenceChecksScreen.tsx +614 -0
  107. package/src/verification/screens/ReducedMotionChecksScreen.tsx +614 -0
  108. package/src/verification/screens/RestThresholdChecksScreen.tsx +634 -0
  109. package/src/verification/screens/ScaleCorrectorChecksScreen.tsx +425 -0
  110. package/src/verification/screens/SharedLayoutContinuityChecksScreen.tsx +338 -0
  111. package/src/verification/screens/SharedLayoutCrossfadeChecksScreen.tsx +425 -0
  112. package/src/verification/screens/TransitionDefaultSelectionChecksScreen.tsx +514 -0
  113. package/src/verification/screens/ViewportScrollAliasChecksScreen.tsx +303 -0
  114. package/src/verification/screens/conformanceBanner.ts +21 -0
  115. package/src/verification/screens/proofConsoleTap.ts +18 -0
  116. package/src/verification.ts +47 -0
  117. package/src/web.ts +98 -0
@@ -0,0 +1,237 @@
1
+ // U7f dynamic-drag-config device checks: catalog _conditionalDraggable +
2
+ // _dragConstraintChanges on the public MotionView path. iOS has no tap injection, so
3
+ // the screen self-drives. Discriminators are the freeze/refusal repeal (the catalog
4
+ // blockers): toggling drag and rebuilding numeric constraints must not refuse, and
5
+ // bare dragElastic without constraints must not refuse. Pan follow is unit-proven on
6
+ // the MotionView host path (dynamicDragConfig.host.test.ts).
7
+
8
+ import { useEffect, useRef, useState } from 'react'
9
+ import { ScrollView, StyleSheet, Text, View } from 'react-native'
10
+ import { MotionView } from '../../native/motion/MotionView'
11
+ import { MotionRoot } from '../../native/motion/MotionRoot'
12
+ import { ambientNativeSeverity } from '../../native/motion/severity'
13
+
14
+ const ROW_FALSE = 'drag={false} mounts without a typed refusal'
15
+ const ROW_TOGGLE = 'toggling drag true/false does not freeze-refuse (catalog _conditionalDraggable)'
16
+ const ROW_CONSTRAINTS = 'rebuilding numeric constraints does not freeze-refuse'
17
+ const ROW_ELASTIC = 'bare dragElastic without constraints is a no-op (no refusal)'
18
+ const ROW_CLEAN = 'clean run: zero refusal reports through the console tap'
19
+ const ROWS = [ROW_FALSE, ROW_TOGGLE, ROW_CONSTRAINTS, ROW_ELASTIC, ROW_CLEAN] as const
20
+
21
+ const T_FALSE = 400
22
+ const T_TOGGLE_ON = 900
23
+ const T_TOGGLE_OFF = 1400
24
+ const T_CONSTRAINTS = 1900
25
+ const T_CONSTRAINTS_SAMPLE = 2300
26
+ const T_FINISH = 2800
27
+
28
+ interface RowResult {
29
+ readonly pass: boolean
30
+ readonly detail: string
31
+ }
32
+
33
+ export function DynamicDragConfigChecksScreen() {
34
+ const severity = ambientNativeSeverity()
35
+ const [dragOn, setDragOn] = useState(false)
36
+ const [bound, setBound] = useState(200)
37
+ const [stage, setStage] = useState('boot')
38
+ const [rows, setRows] = useState<Partial<Record<(typeof ROWS)[number], RowResult>>>({})
39
+ const reportsRef = useRef<string[]>([])
40
+
41
+ const finalize = (id: (typeof ROWS)[number], pass: boolean, detail: string): void => {
42
+ setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
43
+ }
44
+
45
+ useEffect(() => {
46
+ const originalError = console.error
47
+ console.error = (...args: unknown[]) => {
48
+ for (const arg of args) {
49
+ const message = arg instanceof Error ? arg.message : typeof arg === 'string' ? arg : null
50
+ if (message !== null && /motion|drag/i.test(message)) reportsRef.current.push(message)
51
+ }
52
+ originalError(...args)
53
+ }
54
+ const timers: ReturnType<typeof setTimeout>[] = []
55
+ const at = (ms: number, run: () => void): void => {
56
+ timers.push(setTimeout(run, ms))
57
+ }
58
+
59
+ at(T_FALSE, () => {
60
+ const reports = reportsRef.current.length
61
+ finalize(
62
+ ROW_FALSE,
63
+ reports === 0,
64
+ reports === 0
65
+ ? 'drag={false} mounted with zero motion/drag reports'
66
+ : `${reports} report(s) at disabled mount`,
67
+ )
68
+ setStage('disabled mount sampled')
69
+ })
70
+ at(T_TOGGLE_ON, () => {
71
+ setDragOn(true)
72
+ setStage('toggled drag on')
73
+ })
74
+ at(T_TOGGLE_OFF, () => {
75
+ setDragOn(false)
76
+ const freeze = reportsRef.current.some((message) => /fixed at mount/i.test(message))
77
+ finalize(
78
+ ROW_TOGGLE,
79
+ !freeze,
80
+ freeze
81
+ ? 'freeze-refusal fired on toggle (the pre-U7f mount-frozen law)'
82
+ : 'toggle true→false with no fixed-at-mount refusal',
83
+ )
84
+ setStage('toggled drag off')
85
+ })
86
+ at(T_CONSTRAINTS, () => {
87
+ setBound(20)
88
+ setStage('constraints rebuilt')
89
+ })
90
+ at(T_CONSTRAINTS_SAMPLE, () => {
91
+ const freeze = reportsRef.current.some((message) => /fixed at mount/i.test(message))
92
+ const elastic = reportsRef.current.some((message) => /meaningless/i.test(message))
93
+ finalize(
94
+ ROW_CONSTRAINTS,
95
+ !freeze,
96
+ freeze
97
+ ? 'freeze-refusal fired on constraint rebuild'
98
+ : 'constraint bound 200→20 with no fixed-at-mount refusal',
99
+ )
100
+ finalize(
101
+ ROW_ELASTIC,
102
+ !elastic,
103
+ elastic
104
+ ? 'dragElastic-without-constraints still refused'
105
+ : 'bare dragElastic peer mounted with no meaningless-elastic refusal',
106
+ )
107
+ setStage('constraint rebuild sampled')
108
+ })
109
+ at(T_FINISH, () => {
110
+ const reports = reportsRef.current
111
+ finalize(
112
+ ROW_CLEAN,
113
+ reports.length === 0,
114
+ reports.length === 0
115
+ ? '0 refusal reports across disabled mount, toggle, constraint rebuild, elastic peer'
116
+ : `${reports.length} report(s): ${reports.map((message) => message.slice(0, 80)).join(' | ')}`,
117
+ )
118
+ setStage('done')
119
+ })
120
+
121
+ return () => {
122
+ for (const timer of timers) clearTimeout(timer)
123
+ console.error = originalError
124
+ }
125
+ }, [])
126
+
127
+ const done = ROWS.every((id) => rows[id] !== undefined)
128
+ const failing = ROWS.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
129
+ const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
130
+ const passed = Object.keys(rows).length - failing.length
131
+
132
+ return (
133
+ <MotionRoot>
134
+ <ScrollView style={styles.scroll} contentContainerStyle={styles.container}>
135
+ <Text style={styles.title}>U7f dynamic-drag-config checks</Text>
136
+ <Text style={styles.subtitle}>
137
+ catalog toggle + live constraints + elastic no-op ({severity} severity)
138
+ </Text>
139
+ <View
140
+ style={[
141
+ styles.banner,
142
+ verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
143
+ ]}
144
+ >
145
+ <Text style={styles.bannerText}>{verdict}</Text>
146
+ </View>
147
+ <Text style={styles.tally}>
148
+ {done
149
+ ? `${passed} PASS / ${failing.length} FAIL of ${ROWS.length} rows`
150
+ : `${Object.keys(rows).length}/${ROWS.length} rows complete — RUNNING`}
151
+ </Text>
152
+ <Text style={styles.tallyDetail}>
153
+ {done
154
+ ? failing.length === 0
155
+ ? 'no failing rows'
156
+ : `FAILING: ${failing.join(' | ')}`
157
+ : `stage: ${stage}`}
158
+ </Text>
159
+ <Text style={styles.status}>
160
+ drag={dragOn ? 'true' : 'false'} · bound={bound}
161
+ </Text>
162
+
163
+ {dragOn ? (
164
+ <MotionView drag style={[styles.box, { backgroundColor: '#16a34a' }]} />
165
+ ) : (
166
+ <MotionView drag={false} style={[styles.box, { backgroundColor: '#eab308' }]} />
167
+ )}
168
+ <MotionView
169
+ drag="x"
170
+ dragConstraints={{ left: 0, right: bound }}
171
+ dragElastic={0}
172
+ style={[styles.box, { backgroundColor: '#3b82f6' }]}
173
+ />
174
+ <MotionView drag="x" dragElastic style={[styles.box, { backgroundColor: '#f97316' }]} />
175
+
176
+ {ROWS.map((id) => {
177
+ const row = rows[id]
178
+ return (
179
+ <View key={id} style={styles.row}>
180
+ <Text
181
+ style={[
182
+ styles.verdict,
183
+ row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
184
+ ]}
185
+ >
186
+ {row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
187
+ </Text>
188
+ <View style={styles.rowBody}>
189
+ <Text style={styles.rowId}>{id}</Text>
190
+ <Text style={styles.rowDetail} numberOfLines={3}>
191
+ {row === undefined ? 'pending' : row.detail}
192
+ </Text>
193
+ </View>
194
+ </View>
195
+ )
196
+ })}
197
+ </ScrollView>
198
+ </MotionRoot>
199
+ )
200
+ }
201
+
202
+ const styles = StyleSheet.create({
203
+ scroll: { flex: 1, backgroundColor: '#0b0b12' },
204
+ container: { paddingVertical: 72, paddingHorizontal: 8 },
205
+ title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
206
+ subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
207
+ banner: {
208
+ alignSelf: 'center',
209
+ paddingHorizontal: 36,
210
+ paddingVertical: 10,
211
+ borderRadius: 10,
212
+ marginTop: 18,
213
+ marginBottom: 10,
214
+ },
215
+ pass: { backgroundColor: '#1d7a3e' },
216
+ fail: { backgroundColor: '#a32633' },
217
+ running: { backgroundColor: '#7a641d' },
218
+ bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
219
+ tally: { color: '#e6e6f0', fontSize: 14, fontWeight: '700', textAlign: 'center' },
220
+ tallyDetail: { color: '#f87171', fontSize: 11, marginTop: 3, textAlign: 'center' },
221
+ status: {
222
+ color: '#c9c9d8',
223
+ fontSize: 11,
224
+ marginTop: 8,
225
+ textAlign: 'center',
226
+ fontFamily: 'Menlo',
227
+ },
228
+ box: { width: 72, height: 72, marginTop: 12, alignSelf: 'center', borderRadius: 8 },
229
+ row: { flexDirection: 'row', marginTop: 10 },
230
+ verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
231
+ ok: { color: '#4ade80' },
232
+ bad: { color: '#f87171' },
233
+ pending: { color: '#8a8aa0' },
234
+ rowBody: { flex: 1 },
235
+ rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
236
+ rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
237
+ })
@@ -0,0 +1,252 @@
1
+ // U7j frameData device checks: catalog useVelocity.tsx on the public path. iOS has no
2
+ // tap injection, so the screen self-drives. Discriminators (Decision 90 analog of 77/82/86):
3
+ // frameData is the JS-thread clock (F1 — not the UI driver). Host paint of x is the web e2e.
4
+ // Native stamps frameData.timestamp while a JS-channel animate() of a MotionValue runs, then
5
+ // proves idle freeze. The `frame` scheduler is not this unit.
6
+
7
+ import { useCallback, useEffect, useRef, useState } from 'react'
8
+ import { StyleSheet, Text, View } from 'react-native'
9
+ import { MotionRoot } from '../../native/motion/MotionRoot'
10
+ import { animate } from '../../native/motion/imperativeAnimate'
11
+ import { ambientNativeSeverity } from '../../native/motion/severity'
12
+ import { frameData } from '../../native/motion/frameData'
13
+ import { useMotionValue, useMotionValueEvent, useVelocity } from '../../native/motion/valueHooks'
14
+ import { armProofConsoleTap } from './proofConsoleTap'
15
+
16
+ const ROW_SHAPE = 'frameData shape: delta, timestamp, isProcessing'
17
+ const ROW_ADVANCE = 'timestamp advances while the JS channel is kicked'
18
+ const ROW_FREEZE = 'idle freeze: two reads after settle are identical'
19
+ const ROW_CLEAN = 'clean run: zero unexpected reports'
20
+ const ROWS = [ROW_SHAPE, ROW_ADVANCE, ROW_FREEZE, ROW_CLEAN] as const
21
+
22
+ const T_SHAPE = 200
23
+ const T_START = 400
24
+ const T_MID = 700
25
+ const T_IDLE_A = 1400
26
+ const T_IDLE_B = 1800
27
+ const T_CLEAN = 2100
28
+ const DURATION_S = 0.45
29
+
30
+ interface RowResult {
31
+ readonly pass: boolean
32
+ readonly detail: string
33
+ }
34
+
35
+ export function FrameDataChecksScreen() {
36
+ const severity = ambientNativeSeverity()
37
+ const x = useMotionValue(0)
38
+ const xVelocity = useVelocity(x)
39
+ const [stage, setStage] = useState('boot')
40
+ const [rows, setRows] = useState<Partial<Record<(typeof ROWS)[number], RowResult>>>({})
41
+ const [xNow, setXNow] = useState(0)
42
+ const [stampNow, setStampNow] = useState(() => frameData.timestamp)
43
+ const stampsRef = useRef<number[]>([])
44
+ const idleARef = useRef<number | null>(null)
45
+ const reportsRef = useRef<string[]>([])
46
+ const restoreTapRef = useRef<null | (() => void)>(null)
47
+ if (restoreTapRef.current === null) {
48
+ restoreTapRef.current = armProofConsoleTap(reportsRef.current, (message) =>
49
+ /motion|frameData|animate|velocity/i.test(message),
50
+ )
51
+ }
52
+
53
+ const finalize = (id: (typeof ROWS)[number], pass: boolean, detail: string): void => {
54
+ setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
55
+ }
56
+
57
+ const onX = useCallback((value: number): void => {
58
+ setXNow(value)
59
+ const stamp = frameData.timestamp
60
+ stampsRef.current.push(stamp)
61
+ setStampNow(stamp)
62
+ }, [])
63
+ useMotionValueEvent(x, 'change', onX)
64
+ useMotionValueEvent(xVelocity, 'change', () => {
65
+ setStampNow(frameData.timestamp)
66
+ })
67
+
68
+ useEffect(() => {
69
+ const timers: ReturnType<typeof setTimeout>[] = []
70
+ const at = (ms: number, run: () => void): void => {
71
+ timers.push(setTimeout(run, ms))
72
+ }
73
+
74
+ at(T_SHAPE, () => {
75
+ const pass =
76
+ typeof frameData.delta === 'number' &&
77
+ typeof frameData.timestamp === 'number' &&
78
+ typeof frameData.isProcessing === 'boolean'
79
+ finalize(
80
+ ROW_SHAPE,
81
+ pass,
82
+ pass
83
+ ? `delta=${frameData.delta.toFixed(2)} ts=${frameData.timestamp.toFixed(1)} processing=${frameData.isProcessing}`
84
+ : 'shape missing',
85
+ )
86
+ setStage('shape sampled')
87
+ })
88
+
89
+ at(T_START, () => {
90
+ animate(x, 100, { duration: DURATION_S, ease: 'linear' })
91
+ setStage('js-channel animate started')
92
+ })
93
+
94
+ at(T_MID, () => {
95
+ const stamps = stampsRef.current
96
+ const pass = stamps.length >= 2 && stamps[stamps.length - 1]! > stamps[0]!
97
+ finalize(
98
+ ROW_ADVANCE,
99
+ pass,
100
+ pass
101
+ ? `stamps=${stamps.length} first=${stamps[0]!.toFixed(1)} last=${stamps[stamps.length - 1]!.toFixed(1)}`
102
+ : `stamps=${stamps.length} — expected advancing timestamps`,
103
+ )
104
+ setStage(pass ? 'advance row finalized' : 'advance row finalized (FAIL)')
105
+ })
106
+
107
+ at(T_IDLE_A, () => {
108
+ idleARef.current = frameData.timestamp
109
+ setStage(`idle-a ts=${frameData.timestamp.toFixed(1)}`)
110
+ })
111
+
112
+ at(T_IDLE_B, () => {
113
+ const a = idleARef.current
114
+ const b = frameData.timestamp
115
+ const pass = a !== null && a === b
116
+ finalize(
117
+ ROW_FREEZE,
118
+ pass,
119
+ pass
120
+ ? `idle ${a!.toFixed(1)} === ${b.toFixed(1)}`
121
+ : `idle-a=${a === null ? 'n/a' : a.toFixed(1)} idle-b=${b.toFixed(1)}`,
122
+ )
123
+ setStage(pass ? 'freeze row finalized' : 'freeze row finalized (FAIL)')
124
+ })
125
+
126
+ at(T_CLEAN, () => {
127
+ finalize(
128
+ ROW_CLEAN,
129
+ reportsRef.current.length === 0,
130
+ reportsRef.current.length === 0
131
+ ? '0 unexpected reports'
132
+ : `${reportsRef.current.length} unexpected: ${reportsRef.current
133
+ .map((message) => message.slice(0, 80))
134
+ .join(' | ')}`,
135
+ )
136
+ setStage('done')
137
+ })
138
+
139
+ return () => {
140
+ for (const timer of timers) clearTimeout(timer)
141
+ restoreTapRef.current?.()
142
+ restoreTapRef.current = null
143
+ }
144
+ }, [x])
145
+
146
+ const done = ROWS.every((id) => rows[id] !== undefined)
147
+ const failing = ROWS.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
148
+ const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
149
+ const passed = Object.keys(rows).length - failing.length
150
+
151
+ return (
152
+ <MotionRoot>
153
+ <View style={styles.scroll}>
154
+ <View style={styles.container}>
155
+ <Text style={styles.title}>U7j frameData checks</Text>
156
+ <Text style={styles.subtitle}>
157
+ catalog useVelocity timestamps + JS-channel kick ({severity} severity)
158
+ </Text>
159
+ <View
160
+ style={[
161
+ styles.banner,
162
+ verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
163
+ ]}
164
+ >
165
+ <Text style={styles.bannerText}>{verdict}</Text>
166
+ </View>
167
+ <Text style={styles.tally}>
168
+ {done
169
+ ? `${passed} PASS / ${failing.length} FAIL of ${ROWS.length} rows`
170
+ : `${Object.keys(rows).length}/${ROWS.length} rows complete — RUNNING`}
171
+ </Text>
172
+ <Text style={styles.tallyDetail}>
173
+ {done
174
+ ? failing.length === 0
175
+ ? 'no failing rows'
176
+ : `FAILING: ${failing.join(' | ')}`
177
+ : `stage: ${stage}`}
178
+ </Text>
179
+ <Text style={styles.status}>
180
+ x={xNow.toFixed(1)} · ts={stampNow.toFixed(1)} · {severity}
181
+ </Text>
182
+ <View style={styles.box} accessibilityLabel="u7j-frame-data-box" />
183
+
184
+ {ROWS.map((id) => {
185
+ const row = rows[id]
186
+ return (
187
+ <View key={id} style={styles.row}>
188
+ <Text
189
+ style={[
190
+ styles.verdict,
191
+ row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
192
+ ]}
193
+ >
194
+ {row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
195
+ </Text>
196
+ <View style={styles.rowBody}>
197
+ <Text style={styles.rowId}>{id}</Text>
198
+ <Text style={styles.rowDetail} numberOfLines={3}>
199
+ {row === undefined ? 'pending' : row.detail}
200
+ </Text>
201
+ </View>
202
+ </View>
203
+ )
204
+ })}
205
+ </View>
206
+ </View>
207
+ </MotionRoot>
208
+ )
209
+ }
210
+
211
+ const styles = StyleSheet.create({
212
+ scroll: { flex: 1, backgroundColor: '#0b0b12' },
213
+ container: { paddingVertical: 72, paddingHorizontal: 8 },
214
+ title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
215
+ subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
216
+ banner: {
217
+ alignSelf: 'center',
218
+ paddingHorizontal: 36,
219
+ paddingVertical: 10,
220
+ borderRadius: 10,
221
+ marginTop: 18,
222
+ marginBottom: 10,
223
+ },
224
+ pass: { backgroundColor: '#1d7a3e' },
225
+ fail: { backgroundColor: '#a32633' },
226
+ running: { backgroundColor: '#7a641d' },
227
+ bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
228
+ tally: { color: '#e6e6f0', fontSize: 14, fontWeight: '700', textAlign: 'center' },
229
+ tallyDetail: { color: '#f87171', fontSize: 11, marginTop: 3, textAlign: 'center' },
230
+ status: {
231
+ color: '#c9c9d8',
232
+ fontSize: 11,
233
+ marginTop: 8,
234
+ textAlign: 'center',
235
+ fontFamily: 'Menlo',
236
+ },
237
+ box: {
238
+ width: 100,
239
+ height: 100,
240
+ marginTop: 14,
241
+ backgroundColor: '#ef4444',
242
+ borderRadius: 8,
243
+ },
244
+ row: { flexDirection: 'row', marginTop: 10 },
245
+ verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
246
+ ok: { color: '#4ade80' },
247
+ bad: { color: '#f87171' },
248
+ pending: { color: '#8a8aa0' },
249
+ rowBody: { flex: 1 },
250
+ rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
251
+ rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
252
+ })