@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,294 @@
1
+ // REQ-API-060 device checks (finding-B resolution): the property-lane onAnimationComplete
2
+ // delivery chain end to end on device. Leg A: an un-raced single flight completes. Leg B: a
3
+ // deliberately RACED label retarget — the superseded outgoing flight NEVER completes (pin
4
+ // MotionValue.start stops it) and the incoming flight completes with the BARE label string
5
+ // (the inherited-lane array fix, F5). Leg C: a typed-spring settle completes. iOS has no tap
6
+ // injection, so the screen self-drives. The completion ledger is the discriminator (Decision 94
7
+ // analog): every row judges the callback-sourced ledger — a missing or mis-shaped completion
8
+ // fails the row, so a late-but-healthy landing fails CLOSED rather than passing vacated.
9
+
10
+ import { useCallback, useEffect, useRef, useState } from 'react'
11
+ import { StyleSheet, Text, View } from 'react-native'
12
+ import { MotionRoot } from '../../native/motion/MotionRoot'
13
+ import { MotionView } from '../../native/motion/MotionView'
14
+ import { ambientNativeSeverity } from '../../native/motion/severity'
15
+ import { armProofConsoleTap } from './proofConsoleTap'
16
+
17
+ const ROW_A = 'leg A: un-raced single tween flight completes'
18
+ const ROW_B = 'leg B: raced retarget — only the incoming flight completes, with the bare label'
19
+ const ROW_C = 'leg C: typed-spring settle completes'
20
+ const ROW_CLEAN = 'clean run: zero unexpected reports'
21
+ const ROWS = [ROW_A, ROW_B, ROW_C, ROW_CLEAN] as const
22
+
23
+ const T_A_START = 80
24
+ const T_A_JUDGE = 1400
25
+ const T_B_HIDDEN = 1900
26
+ const T_B_JUDGE = 3200
27
+ const T_C_JUDGE = 4800
28
+ const T_CLEAN = 4900
29
+
30
+ interface RowResult {
31
+ readonly pass: boolean
32
+ readonly detail: string
33
+ }
34
+
35
+ interface Completion {
36
+ readonly at: number
37
+ readonly definition: string
38
+ }
39
+
40
+ function describe(definition: unknown): string {
41
+ if (typeof definition === 'string') return definition
42
+ if (Array.isArray(definition)) return `array(${definition.map(String).join(',')})`
43
+ return typeof definition
44
+ }
45
+
46
+ const VARIANTS = {
47
+ visible: { opacity: 1, transition: { duration: 0.8 } },
48
+ hidden: { opacity: 0, transition: { duration: 0.8 } },
49
+ } as const
50
+
51
+ export function CompletionChecksScreen() {
52
+ const severity = ambientNativeSeverity()
53
+ const [targetA, setTargetA] = useState(0)
54
+ const [labelB, setLabelB] = useState<'hidden' | 'visible'>('hidden')
55
+ const [targetC, setTargetC] = useState(0)
56
+ const [stage, setStage] = useState('boot')
57
+ const [rows, setRows] = useState<Partial<Record<(typeof ROWS)[number], RowResult>>>({})
58
+ const completionsA = useRef<Completion[]>([])
59
+ const completionsB = useRef<Completion[]>([])
60
+ const completionsC = useRef<Completion[]>([])
61
+ const interiorA = useRef(0)
62
+ const interiorB = useRef(0)
63
+ const t0 = useRef(Date.now())
64
+ const reportsRef = useRef<string[]>([])
65
+ const restoreTapRef = useRef<null | (() => void)>(null)
66
+ if (restoreTapRef.current === null) {
67
+ restoreTapRef.current = armProofConsoleTap(reportsRef.current, (message) =>
68
+ /motion|transition|settle|complete/i.test(message),
69
+ )
70
+ }
71
+
72
+ const finalize = (id: (typeof ROWS)[number], pass: boolean, detail: string): void => {
73
+ setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
74
+ }
75
+ const stamp = (): number => Date.now() - t0.current
76
+
77
+ const onCompleteA = useCallback((definition: unknown): void => {
78
+ completionsA.current.push({ at: stamp(), definition: describe(definition) })
79
+ }, [])
80
+ const onCompleteB = useCallback((definition: unknown): void => {
81
+ completionsB.current.push({ at: stamp(), definition: describe(definition) })
82
+ }, [])
83
+ const onCompleteC = useCallback((definition: unknown): void => {
84
+ completionsC.current.push({ at: stamp(), definition: describe(definition) })
85
+ }, [])
86
+ const onUpdateA = useCallback((latest: Readonly<Record<string, string | number>>): void => {
87
+ const value = latest.opacity
88
+ if (typeof value === 'number' && value > 0.02 && value < 0.98) interiorA.current += 1
89
+ }, [])
90
+ const onUpdateB = useCallback((latest: Readonly<Record<string, string | number>>): void => {
91
+ const value = latest.opacity
92
+ if (typeof value === 'number' && value > 0.02 && value < 0.98) interiorB.current += 1
93
+ }, [])
94
+
95
+ useEffect(() => {
96
+ const timers: ReturnType<typeof setTimeout>[] = []
97
+ const at = (ms: number, run: () => void): void => {
98
+ timers.push(setTimeout(run, ms))
99
+ }
100
+
101
+ at(T_A_START, () => {
102
+ setTargetA(1)
103
+ setStage('leg A: single tween flight')
104
+ })
105
+ at(T_A_JUDGE, () => {
106
+ const completions = completionsA.current
107
+ const pass = completions.length === 1 && interiorA.current > 3
108
+ finalize(
109
+ ROW_A,
110
+ pass,
111
+ `completions=${completions.length} (${
112
+ completions.map((c) => `+${c.at}ms ${c.definition}`).join(' | ') || 'none'
113
+ }), ${interiorA.current} interior samples (the flight ran)`,
114
+ )
115
+ setLabelB('visible')
116
+ setStage('leg B: label flight started — retarget races it at +400ms')
117
+ })
118
+ at(T_B_HIDDEN, () => {
119
+ setLabelB('hidden')
120
+ })
121
+ at(T_B_JUDGE, () => {
122
+ const completions = completionsB.current
123
+ const pass =
124
+ completions.length === 1 && completions[0]!.definition === 'hidden' && interiorB.current > 0
125
+ finalize(
126
+ ROW_B,
127
+ pass,
128
+ `completions=${completions.length} (${
129
+ completions.map((c) => `+${c.at}ms ${c.definition}`).join(' | ') || 'none'
130
+ }) — the pin fires only the incoming flight, with the bare ` +
131
+ 'label string; the outgoing (superseded) flight never completes',
132
+ )
133
+ setTargetC(100)
134
+ setStage('leg C: typed-spring settle')
135
+ })
136
+ at(T_C_JUDGE, () => {
137
+ const completions = completionsC.current
138
+ const pass = completions.length === 1
139
+ finalize(
140
+ ROW_C,
141
+ pass,
142
+ `completions=${completions.length} (${
143
+ completions.map((c) => `+${c.at}ms ${c.definition}`).join(' | ') || 'none'
144
+ })`,
145
+ )
146
+ setStage('legs judged')
147
+ })
148
+ at(T_CLEAN, () => {
149
+ const pass = reportsRef.current.length === 0
150
+ finalize(
151
+ ROW_CLEAN,
152
+ pass,
153
+ pass
154
+ ? '0 unexpected reports'
155
+ : `${reportsRef.current.length} unexpected: ${reportsRef.current
156
+ .map((message) => message.slice(0, 80))
157
+ .join(' | ')}`,
158
+ )
159
+ setStage('done')
160
+ })
161
+
162
+ return () => {
163
+ for (const timer of timers) clearTimeout(timer)
164
+ restoreTapRef.current?.()
165
+ restoreTapRef.current = null
166
+ }
167
+ // The timeline runs once per mount; all captured values are refs or stable setters.
168
+ // eslint-disable-next-line react-hooks/exhaustive-deps
169
+ }, [])
170
+
171
+ const done = ROWS.every((id) => rows[id] !== undefined)
172
+ const failing = ROWS.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
173
+ const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
174
+ const passed = Object.keys(rows).length - failing.length
175
+
176
+ return (
177
+ <MotionRoot>
178
+ <View style={styles.scroll}>
179
+ <View style={styles.container}>
180
+ <Text style={styles.title}>onAnimationComplete checks</Text>
181
+ <Text style={styles.subtitle}>delivery + supersession law ({severity} severity)</Text>
182
+ <View
183
+ style={[
184
+ styles.banner,
185
+ verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
186
+ ]}
187
+ >
188
+ <Text style={styles.bannerText}>{verdict}</Text>
189
+ </View>
190
+ <Text style={styles.tally}>
191
+ {done
192
+ ? `${passed} PASS / ${failing.length} FAIL of ${ROWS.length} rows`
193
+ : `${Object.keys(rows).length}/${ROWS.length} rows complete — RUNNING`}
194
+ </Text>
195
+ <Text style={styles.tallyDetail}>
196
+ {done
197
+ ? failing.length === 0
198
+ ? 'no failing rows'
199
+ : `FAILING: ${failing.join(' | ')}`
200
+ : `stage: ${stage}`}
201
+ </Text>
202
+
203
+ <MotionView
204
+ animate={{ opacity: targetA }}
205
+ initial={false}
206
+ transition={{ duration: 0.8 }}
207
+ onUpdate={onUpdateA}
208
+ onAnimationComplete={onCompleteA}
209
+ style={styles.box}
210
+ accessibilityLabel="completion-legA-box"
211
+ />
212
+ <MotionView animate={labelB} initial={false}>
213
+ <MotionView
214
+ variants={VARIANTS}
215
+ onUpdate={onUpdateB}
216
+ onAnimationComplete={onCompleteB}
217
+ style={[styles.box, styles.boxB]}
218
+ accessibilityLabel="completion-legB-box"
219
+ />
220
+ </MotionView>
221
+ <MotionView
222
+ animate={{ x: targetC }}
223
+ initial={false}
224
+ transition={{ type: 'spring', duration: 1 }}
225
+ onAnimationComplete={onCompleteC}
226
+ style={[styles.box, styles.boxC]}
227
+ accessibilityLabel="completion-legC-box"
228
+ />
229
+
230
+ {ROWS.map((id) => {
231
+ const row = rows[id]
232
+ return (
233
+ <View key={id} style={styles.row}>
234
+ <Text
235
+ style={[
236
+ styles.verdict,
237
+ row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
238
+ ]}
239
+ >
240
+ {row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
241
+ </Text>
242
+ <View style={styles.rowBody}>
243
+ <Text style={styles.rowId}>{id}</Text>
244
+ <Text style={styles.rowDetail} numberOfLines={3}>
245
+ {row === undefined ? 'pending' : row.detail}
246
+ </Text>
247
+ </View>
248
+ </View>
249
+ )
250
+ })}
251
+ </View>
252
+ </View>
253
+ </MotionRoot>
254
+ )
255
+ }
256
+
257
+ const styles = StyleSheet.create({
258
+ scroll: { flex: 1, backgroundColor: '#0b0b12' },
259
+ container: { paddingVertical: 72, paddingHorizontal: 8 },
260
+ title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
261
+ subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
262
+ banner: {
263
+ alignSelf: 'center',
264
+ paddingHorizontal: 36,
265
+ paddingVertical: 10,
266
+ borderRadius: 10,
267
+ marginTop: 18,
268
+ marginBottom: 10,
269
+ },
270
+ pass: { backgroundColor: '#1d7a3e' },
271
+ fail: { backgroundColor: '#a32633' },
272
+ running: { backgroundColor: '#7a641d' },
273
+ bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
274
+ tally: { color: '#e6e6f0', fontSize: 14, fontWeight: '700', textAlign: 'center' },
275
+ tallyDetail: { color: '#f87171', fontSize: 11, marginTop: 3, textAlign: 'center' },
276
+ box: {
277
+ width: 40,
278
+ height: 40,
279
+ marginTop: 12,
280
+ backgroundColor: '#ef4444',
281
+ borderRadius: 8,
282
+ opacity: 0,
283
+ },
284
+ boxB: { backgroundColor: '#38bdf8' },
285
+ boxC: { backgroundColor: '#f97316', opacity: 1 },
286
+ row: { flexDirection: 'row', marginTop: 10 },
287
+ verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
288
+ ok: { color: '#4ade80' },
289
+ bad: { color: '#f87171' },
290
+ pending: { color: '#8a8aa0' },
291
+ rowBody: { flex: 1 },
292
+ rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
293
+ rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
294
+ })