@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,358 @@
1
+ // U7h boolean-animate device checks: catalog Shared-layout-sibling-to-child on the public
2
+ // MotionView path. iOS has no tap injection, so the screen self-drives. Discriminators are
3
+ // acceptance (Decision 82 analog of 77): bare / true / false `animate` mount without a typed
4
+ // InvalidTargetError, and the sibling→child layoutId exclusive remount still completes
5
+ // (inert — boolean is not a disable switch). Distinct Child/Sibling types (R13: inlining
6
+ // the ternary reuses the first MotionView fiber and leaves settles at 0). Host paint of
7
+ // the crossfade is the web e2e.
8
+
9
+ import { useCallback, useEffect, useRef, useState } from 'react'
10
+ import { ScrollView, StyleSheet, Text, View } from 'react-native'
11
+ import { LayoutGroup } from '../../native/motion/LayoutGroup'
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_BARE = 'bare animate mounts without InvalidTargetError'
18
+ const ROW_TRUE = 'animate={true} mounts without InvalidTargetError'
19
+ const ROW_FALSE = 'animate={false} mounts without InvalidTargetError'
20
+ const ROW_FLIGHT = 'sibling→child layoutId flight: exclusive remount completes'
21
+ const ROW_CLEAN = 'clean run: zero InvalidTargetError reports'
22
+ const ROWS = [ROW_BARE, ROW_TRUE, ROW_FALSE, ROW_FLIGHT, ROW_CLEAN] as const
23
+
24
+ // Family-3 / REQ-LAYOUT-023: layoutId identity flights use duration/ease, not a spring.
25
+ const TRANSITION = { duration: 0.4, ease: 'easeOut' } as const
26
+ const T_ACCEPT = 400
27
+ const T_FLIGHT = 1200
28
+ const SETTLE_DEADLINE_MS = 4000
29
+ const STABILITY_MS = 400
30
+
31
+ function isKnownProjectionRadiusDebt(message: string): boolean {
32
+ return /borderRadius correction is anisotropic/i.test(message)
33
+ }
34
+
35
+ interface RowResult {
36
+ readonly pass: boolean
37
+ readonly detail: string
38
+ }
39
+
40
+ function isAnimateRefusal(message: string): boolean {
41
+ return /InvalidTargetError|malformed animate target|animate-target/i.test(message)
42
+ }
43
+
44
+ function U7hChild(props: { onComplete: () => void }) {
45
+ return (
46
+ <MotionView
47
+ animate
48
+ layoutId="u7h-big"
49
+ transition={TRANSITION}
50
+ onLayoutAnimationComplete={props.onComplete}
51
+ style={styles.childBig}
52
+ accessibilityLabel="u7h-boolean-animate-big"
53
+ >
54
+ <MotionView
55
+ animate
56
+ layoutId="u7h-small"
57
+ transition={TRANSITION}
58
+ style={styles.childSmall}
59
+ accessibilityLabel="u7h-boolean-animate-small"
60
+ />
61
+ </MotionView>
62
+ )
63
+ }
64
+
65
+ function U7hSibling(props: { onComplete: () => void }) {
66
+ return (
67
+ <>
68
+ <MotionView
69
+ animate
70
+ layoutId="u7h-big"
71
+ transition={TRANSITION}
72
+ onLayoutAnimationComplete={props.onComplete}
73
+ style={styles.siblingBig}
74
+ accessibilityLabel="u7h-boolean-animate-big"
75
+ />
76
+ <MotionView
77
+ animate
78
+ layoutId="u7h-small"
79
+ transition={TRANSITION}
80
+ style={styles.siblingSmall}
81
+ accessibilityLabel="u7h-boolean-animate-small"
82
+ />
83
+ </>
84
+ )
85
+ }
86
+
87
+ export function BooleanAnimateChecksScreen() {
88
+ const severity = ambientNativeSeverity()
89
+ const [isOn, setOn] = useState(false)
90
+ const [stage, setStage] = useState('boot')
91
+ const [rows, setRows] = useState<Partial<Record<(typeof ROWS)[number], RowResult>>>({})
92
+ const [completes, setCompletes] = useState(0)
93
+
94
+ const reportsRef = useRef<string[]>([])
95
+ const completesRef = useRef(0)
96
+ const rafsRef = useRef<ReturnType<typeof requestAnimationFrame>[]>([])
97
+ const mountStampRef = useRef(Date.now())
98
+ const restoreTapRef = useRef<null | (() => void)>(null)
99
+ // Arm BEFORE children render. Production resolveAnimateProp reports via console.error
100
+ // on the child's render (review r1 major, agent-2026-08-20-304390). A parent useEffect
101
+ // tap would miss the first-mount InvalidTargetError and 5/5 the pre-fix tree.
102
+ if (restoreTapRef.current === null) {
103
+ restoreTapRef.current = armProofConsoleTap(reportsRef.current, (message) =>
104
+ /motion|InvalidTargetError|animate/i.test(message),
105
+ )
106
+ }
107
+
108
+ const now = (): number => Date.now() - mountStampRef.current
109
+
110
+ const finalize = (id: (typeof ROWS)[number], pass: boolean, detail: string): void => {
111
+ setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
112
+ }
113
+
114
+ const onComplete = useCallback((): void => {
115
+ completesRef.current += 1
116
+ setCompletes(completesRef.current)
117
+ }, [])
118
+
119
+ useEffect(() => {
120
+ const timers: ReturnType<typeof setTimeout>[] = []
121
+ const at = (ms: number, run: () => void): void => {
122
+ timers.push(setTimeout(run, ms))
123
+ }
124
+ const trackRaf = (id: ReturnType<typeof requestAnimationFrame>): void => {
125
+ rafsRef.current.push(id)
126
+ }
127
+ const pollUntil = (
128
+ condition: () => boolean,
129
+ deadlineAt: number,
130
+ onResolve: (ok: boolean) => void,
131
+ ): void => {
132
+ const step = (): void => {
133
+ if (condition()) {
134
+ onResolve(true)
135
+ return
136
+ }
137
+ if (now() >= deadlineAt) {
138
+ onResolve(false)
139
+ return
140
+ }
141
+ trackRaf(requestAnimationFrame(step))
142
+ }
143
+ trackRaf(requestAnimationFrame(step))
144
+ }
145
+
146
+ at(T_ACCEPT, () => {
147
+ const refusals = reportsRef.current.filter(isAnimateRefusal)
148
+ const pass = refusals.length === 0
149
+ const detail = pass
150
+ ? 'bare/true/false mounted with zero animate-target refusals'
151
+ : `${refusals.length} animate refusal(s): ${refusals.map((message) => message.slice(0, 80)).join(' | ')}`
152
+ finalize(ROW_BARE, pass, pass ? 'catalog sibling tree mounted with bare animate' : detail)
153
+ finalize(ROW_TRUE, pass, pass ? 'true-peer mounted beside the catalog tree' : detail)
154
+ finalize(ROW_FALSE, pass, pass ? 'false-peer mounted beside the catalog tree' : detail)
155
+ setStage('boolean mounts sampled')
156
+ })
157
+
158
+ at(T_FLIGHT, () => {
159
+ setOn(true)
160
+ setStage('layoutId flight pressed (sibling → child)')
161
+ pollUntil(
162
+ () => completesRef.current >= 1,
163
+ now() + SETTLE_DEADLINE_MS,
164
+ (okSettle) => {
165
+ finalize(
166
+ ROW_FLIGHT,
167
+ okSettle,
168
+ okSettle
169
+ ? `exclusive remount complete ${completesRef.current} on layoutId big`
170
+ : `layoutId complete never fired within ${SETTLE_DEADLINE_MS}ms (completes ${completesRef.current})`,
171
+ )
172
+ setStage(okSettle ? 'flight row finalized' : 'flight row finalized (FAIL)')
173
+ at(STABILITY_MS, () => finishClean())
174
+ },
175
+ )
176
+ })
177
+
178
+ const finishClean = (): void => {
179
+ const refusals = reportsRef.current.filter(isAnimateRefusal)
180
+ const known = reportsRef.current.filter(isKnownProjectionRadiusDebt)
181
+ const unexpected = reportsRef.current.filter(
182
+ (message) => !isAnimateRefusal(message) && !isKnownProjectionRadiusDebt(message),
183
+ )
184
+ finalize(
185
+ ROW_CLEAN,
186
+ refusals.length === 0 && unexpected.length === 0,
187
+ refusals.length === 0 && unexpected.length === 0
188
+ ? `0 InvalidTargetError reports; ${known.length} T19 anisotropic radius disclosure(s)`
189
+ : `${refusals.length} animate + ${unexpected.length} other: ${reportsRef.current
190
+ .map((message) => message.slice(0, 80))
191
+ .join(' | ')}`,
192
+ )
193
+ setStage('done')
194
+ }
195
+
196
+ return () => {
197
+ for (const timer of timers) clearTimeout(timer)
198
+ for (const raf of rafsRef.current) cancelAnimationFrame(raf)
199
+ restoreTapRef.current?.()
200
+ restoreTapRef.current = null
201
+ }
202
+ }, [])
203
+
204
+ const done = ROWS.every((id) => rows[id] !== undefined)
205
+ const failing = ROWS.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
206
+ const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
207
+ const passed = Object.keys(rows).length - failing.length
208
+
209
+ return (
210
+ <MotionRoot>
211
+ <ScrollView style={styles.scroll} contentContainerStyle={styles.container}>
212
+ <Text style={styles.title}>U7h boolean-animate checks</Text>
213
+ <Text style={styles.subtitle}>
214
+ catalog sibling↔child + true/false peers ({severity} severity)
215
+ </Text>
216
+ <View
217
+ style={[
218
+ styles.banner,
219
+ verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
220
+ ]}
221
+ >
222
+ <Text style={styles.bannerText}>{verdict}</Text>
223
+ </View>
224
+ <Text style={styles.tally}>
225
+ {done
226
+ ? `${passed} PASS / ${failing.length} FAIL of ${ROWS.length} rows`
227
+ : `${Object.keys(rows).length}/${ROWS.length} rows complete — RUNNING`}
228
+ </Text>
229
+ <Text style={styles.tallyDetail}>
230
+ {done
231
+ ? failing.length === 0
232
+ ? 'no failing rows'
233
+ : `FAILING: ${failing.join(' | ')}`
234
+ : `stage: ${stage}`}
235
+ </Text>
236
+ <Text style={styles.status}>
237
+ on={isOn ? 1 : 0} · completes={completes} · {severity}
238
+ </Text>
239
+
240
+ <LayoutGroup>
241
+ <View style={styles.stage} accessibilityLabel="u7h-boolean-animate-stage">
242
+ {isOn ? <U7hChild onComplete={onComplete} /> : <U7hSibling onComplete={onComplete} />}
243
+ </View>
244
+ </LayoutGroup>
245
+
246
+ <View style={styles.peers}>
247
+ <MotionView animate={true} style={[styles.peer, styles.peerTrue]} />
248
+ <MotionView animate={false} style={[styles.peer, styles.peerFalse]} />
249
+ </View>
250
+
251
+ {ROWS.map((id) => {
252
+ const row = rows[id]
253
+ return (
254
+ <View key={id} style={styles.row}>
255
+ <Text
256
+ style={[
257
+ styles.verdict,
258
+ row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
259
+ ]}
260
+ >
261
+ {row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
262
+ </Text>
263
+ <View style={styles.rowBody}>
264
+ <Text style={styles.rowId}>{id}</Text>
265
+ <Text style={styles.rowDetail} numberOfLines={3}>
266
+ {row === undefined ? 'pending' : row.detail}
267
+ </Text>
268
+ </View>
269
+ </View>
270
+ )
271
+ })}
272
+ </ScrollView>
273
+ </MotionRoot>
274
+ )
275
+ }
276
+
277
+ const styles = StyleSheet.create({
278
+ scroll: { flex: 1, backgroundColor: '#0b0b12' },
279
+ container: { paddingVertical: 72, paddingHorizontal: 8 },
280
+ title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
281
+ subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
282
+ banner: {
283
+ alignSelf: 'center',
284
+ paddingHorizontal: 36,
285
+ paddingVertical: 10,
286
+ borderRadius: 10,
287
+ marginTop: 18,
288
+ marginBottom: 10,
289
+ },
290
+ pass: { backgroundColor: '#1d7a3e' },
291
+ fail: { backgroundColor: '#a32633' },
292
+ running: { backgroundColor: '#7a641d' },
293
+ bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
294
+ tally: { color: '#e6e6f0', fontSize: 14, fontWeight: '700', textAlign: 'center' },
295
+ tallyDetail: { color: '#f87171', fontSize: 11, marginTop: 3, textAlign: 'center' },
296
+ status: {
297
+ color: '#c9c9d8',
298
+ fontSize: 11,
299
+ marginTop: 8,
300
+ textAlign: 'center',
301
+ fontFamily: 'Menlo',
302
+ },
303
+ stage: {
304
+ height: 280,
305
+ marginTop: 14,
306
+ backgroundColor: '#f3f3f3',
307
+ borderRadius: 16,
308
+ overflow: 'visible',
309
+ },
310
+ childBig: {
311
+ position: 'absolute',
312
+ top: 97,
313
+ left: 26,
314
+ width: 120,
315
+ height: 120,
316
+ borderRadius: 20,
317
+ backgroundColor: 'rgba(136, 85, 255, 0.3)',
318
+ },
319
+ childSmall: {
320
+ position: 'absolute',
321
+ top: 30,
322
+ left: 30,
323
+ width: 60,
324
+ height: 60,
325
+ borderRadius: 10,
326
+ backgroundColor: '#8855ff',
327
+ },
328
+ siblingBig: {
329
+ position: 'absolute',
330
+ top: 70,
331
+ left: 40,
332
+ width: 60,
333
+ height: 60,
334
+ borderRadius: 10,
335
+ backgroundColor: 'rgba(0, 153, 255, 0.3)',
336
+ },
337
+ siblingSmall: {
338
+ position: 'absolute',
339
+ top: 132,
340
+ left: 102,
341
+ width: 60,
342
+ height: 60,
343
+ borderRadius: 10,
344
+ backgroundColor: '#0099ff',
345
+ },
346
+ peers: { flexDirection: 'row', justifyContent: 'center', marginTop: 12 },
347
+ peer: { width: 32, height: 32, borderRadius: 6 },
348
+ peerTrue: { backgroundColor: '#16a34a', marginRight: 12 },
349
+ peerFalse: { backgroundColor: '#eab308' },
350
+ row: { flexDirection: 'row', marginTop: 10 },
351
+ verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
352
+ ok: { color: '#4ade80' },
353
+ bad: { color: '#f87171' },
354
+ pending: { color: '#8a8aa0' },
355
+ rowBody: { flex: 1 },
356
+ rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
357
+ rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
358
+ })