@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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/android/build.gradle +24 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/GestureExclusionModule.kt +55 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/NativeMotionPackage.kt +27 -0
- package/package.json +57 -0
- package/react-native.config.js +14 -0
- package/src/native/driver/instantWindow.ts +31 -0
- package/src/native/driver/strictModeReplay.ts +20 -0
- package/src/native/driver/uiLayoutEngine.ts +845 -0
- package/src/native/driver/uiLayoutGraph.ts +338 -0
- package/src/native/driver/uiValueChannel.ts +4640 -0
- package/src/native/driver/workletDriver.ts +3860 -0
- package/src/native/motion/AnimatePresence.tsx +1605 -0
- package/src/native/motion/LayoutGroup.tsx +163 -0
- package/src/native/motion/MotionConfig.tsx +400 -0
- package/src/native/motion/MotionRoot.tsx +250 -0
- package/src/native/motion/MotionView.tsx +6761 -0
- package/src/native/motion/addScaleCorrector.ts +97 -0
- package/src/native/motion/colorEndpointUiFeed.ts +32 -0
- package/src/native/motion/colorProjection.ts +39 -0
- package/src/native/motion/composeTransform.ts +271 -0
- package/src/native/motion/constraintMeasure.ts +157 -0
- package/src/native/motion/deferredPendingSnapshots.ts +25 -0
- package/src/native/motion/discreteProjection.ts +36 -0
- package/src/native/motion/dragAncestorPanContext.ts +28 -0
- package/src/native/motion/dragControls.ts +152 -0
- package/src/native/motion/dragGestureWiring.ts +1168 -0
- package/src/native/motion/dragHandoffBinding.ts +179 -0
- package/src/native/motion/dragHubStream.ts +756 -0
- package/src/native/motion/dragPropagationLock.ts +57 -0
- package/src/native/motion/driverValueChannel.ts +3288 -0
- package/src/native/motion/externalDragDriver.ts +214 -0
- package/src/native/motion/frameData.ts +31 -0
- package/src/native/motion/gestureBinding.ts +107 -0
- package/src/native/motion/gestureStateGate.ts +563 -0
- package/src/native/motion/gestureStateResolver.ts +286 -0
- package/src/native/motion/identityValueChannelControllerAdapter.ts +2043 -0
- package/src/native/motion/identityValueChannelControllerReconciliation.ts +176 -0
- package/src/native/motion/identityValueChannelLaneMarker.ts +14 -0
- package/src/native/motion/imperativeAnimate.ts +816 -0
- package/src/native/motion/keyframeTiming.ts +7 -0
- package/src/native/motion/layoutIdBinding.ts +261 -0
- package/src/native/motion/layoutIdFlightConfig.ts +51 -0
- package/src/native/motion/layoutProjection.ts +89 -0
- package/src/native/motion/layoutScroll.ts +204 -0
- package/src/native/motion/layoutTransition.ts +1302 -0
- package/src/native/motion/lengthLayoutContext.tsx +100 -0
- package/src/native/motion/lengthLayoutHost.ts +44 -0
- package/src/native/motion/mappedKeys.ts +184 -0
- package/src/native/motion/motionViewController.ts +3180 -0
- package/src/native/motion/nativeHostMarker.ts +22 -0
- package/src/native/motion/panSessionWiring.ts +127 -0
- package/src/native/motion/pathTransition.ts +101 -0
- package/src/native/motion/popLayout.ts +176 -0
- package/src/native/motion/presenceBinding.ts +382 -0
- package/src/native/motion/scaleCorrectorRegistry.ts +123 -0
- package/src/native/motion/scrollValues.ts +260 -0
- package/src/native/motion/serializablePayload.ts +151 -0
- package/src/native/motion/severity.ts +16 -0
- package/src/native/motion/shippedSurface.ts +843 -0
- package/src/native/motion/staticLengthGate.ts +86 -0
- package/src/native/motion/styleBaseGate.ts +72 -0
- package/src/native/motion/styleValueBinding.ts +657 -0
- package/src/native/motion/systemGestureExclusion.ts +61 -0
- package/src/native/motion/tapGestureWiring.ts +215 -0
- package/src/native/motion/transformOrder.ts +38 -0
- package/src/native/motion/transformStringBinding.ts +200 -0
- package/src/native/motion/transformTemplateGate.ts +45 -0
- package/src/native/motion/transitionGate.ts +435 -0
- package/src/native/motion/useAnimate.ts +52 -0
- package/src/native/motion/useCycle.ts +44 -0
- package/src/native/motion/useInstantTransition.ts +68 -0
- package/src/native/motion/useReducedMotion.ts +58 -0
- package/src/native/motion/useScroll.ts +162 -0
- package/src/native/motion/useViewportScroll.ts +29 -0
- package/src/native/motion/valueChannel.ts +7121 -0
- package/src/native/motion/valueHooks.ts +901 -0
- package/src/native/motion/variantChildRegistry.ts +67 -0
- package/src/native/motion/variantContext.tsx +172 -0
- package/src/native/motion/variantProps.ts +598 -0
- package/src/native.ts +159 -0
- package/src/verification/harnessMetrics.ts +78 -0
- package/src/verification/probe/LayoutIdentityWorkletProbe.tsx +251 -0
- package/src/verification/probe/WorkletParityProbe.tsx +162 -0
- package/src/verification/probe/layoutIdentityProbeEngine.ts +429 -0
- package/src/verification/screens/ArcPathChecksScreen.tsx +273 -0
- package/src/verification/screens/BooleanAnimateChecksScreen.tsx +358 -0
- package/src/verification/screens/ChoreographyGalleryScreen.tsx +1010 -0
- package/src/verification/screens/ColorBindingChecksScreen.tsx +568 -0
- package/src/verification/screens/CompletionChecksScreen.tsx +294 -0
- package/src/verification/screens/ConformanceScreen.tsx +573 -0
- package/src/verification/screens/ContentionProbeScreen.tsx +224 -0
- package/src/verification/screens/DriverSmokeScreen.tsx +99 -0
- package/src/verification/screens/DurationOnlyTweenChecksScreen.tsx +294 -0
- package/src/verification/screens/DynamicDragConfigChecksScreen.tsx +237 -0
- package/src/verification/screens/FrameDataChecksScreen.tsx +252 -0
- package/src/verification/screens/GestureChecksScreen.tsx +685 -0
- package/src/verification/screens/InstantTransitionChecksScreen.tsx +839 -0
- package/src/verification/screens/LayoutAnimationStartChecksScreen.tsx +648 -0
- package/src/verification/screens/LayoutChecksScreen.tsx +822 -0
- package/src/verification/screens/LayoutCommitSpikeScreen.tsx +142 -0
- package/src/verification/screens/MotionViewChecksScreen.tsx +930 -0
- package/src/verification/screens/PresenceChecksScreen.tsx +614 -0
- package/src/verification/screens/ReducedMotionChecksScreen.tsx +614 -0
- package/src/verification/screens/RestThresholdChecksScreen.tsx +634 -0
- package/src/verification/screens/ScaleCorrectorChecksScreen.tsx +425 -0
- package/src/verification/screens/SharedLayoutContinuityChecksScreen.tsx +338 -0
- package/src/verification/screens/SharedLayoutCrossfadeChecksScreen.tsx +425 -0
- package/src/verification/screens/TransitionDefaultSelectionChecksScreen.tsx +514 -0
- package/src/verification/screens/ViewportScrollAliasChecksScreen.tsx +303 -0
- package/src/verification/screens/conformanceBanner.ts +21 -0
- package/src/verification/screens/proofConsoleTap.ts +18 -0
- package/src/verification.ts +47 -0
- package/src/web.ts +98 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// U6 continuity device checks (REQ-VALUETYPE-013(f) — the packet dissolved into it at the U6
|
|
2
|
+
// terminal): the PUBLIC catalog Shared-layout-continuity.tsx shape on device — a layoutId="u6-box"
|
|
3
|
+
// box keyed `shape-${count}` cycling the pin's styles table, where styles[2] is the pin-verbatim
|
|
4
|
+
// calc seed `{ width: 100, height: 100, left: 'calc(100vw - 100px)' }` and styles[1] is a plain
|
|
5
|
+
// px position, flying on the pin's authored 3s transition. Three claims, self-driven:
|
|
6
|
+
//
|
|
7
|
+
// 1. FIRST MOUNT ON THE CALC SHAPE — the box boots on styles[2]; its host-measured window x is
|
|
8
|
+
// viewportWidth − 100 (±2). The pre-U6 defect: Yoga silently dropped the calc string and the
|
|
9
|
+
// box laid out at x≈0/auto — half a viewport away from the asserted band. settles must be 0:
|
|
10
|
+
// the seed is a static-style layout, not an animation.
|
|
11
|
+
// 2. KEYED REMOUNT FLIGHT — cycling 2 → 1 remounts the box (no AnimatePresence on this card:
|
|
12
|
+
// the old key unmounts instantly, the exclusive-handoff flies the successor). Sampled at a
|
|
13
|
+
// stamped ~1.4s into the 3s flight the measured x sits STRICTLY between origin and target (a
|
|
14
|
+
// snap fails betweenness); a second sample 400ms later has moved (an unstarted flight fails);
|
|
15
|
+
// the settle lands at left:100 exactly (a wrong duration class never arrives in time).
|
|
16
|
+
// 3. THE CALC TARGET LEG — cycling 1 → 2 makes the calc shape the flight DESTINATION: the
|
|
17
|
+
// projection measures the resolved rect exactly as the pin measures browser-resolved px, and
|
|
18
|
+
// the settle lands at viewportWidth − 100 measured on the host.
|
|
19
|
+
//
|
|
20
|
+
// Readback honesty: every position is measureInWindow against the host (the value the substrate
|
|
21
|
+
// actually paints, flight transform included). The stage is full-bleed (stage left = window x 0,
|
|
22
|
+
// width = vw) so the viewport-resolved seed reads directly in window coordinates. A screenshot is
|
|
23
|
+
// the assertion; try/catch is evidence capture, never suppression.
|
|
24
|
+
|
|
25
|
+
import { useEffect, useRef, useState, type ComponentRef } from 'react'
|
|
26
|
+
import { Dimensions, ScrollView, StyleSheet, Text, View } from 'react-native'
|
|
27
|
+
import { MotionRoot } from '../../native/motion/MotionRoot'
|
|
28
|
+
import { MotionView, type MotionViewStyle } from '../../native/motion/MotionView'
|
|
29
|
+
import { ambientNativeSeverity } from '../../native/motion/severity'
|
|
30
|
+
|
|
31
|
+
// The pin's styles table verbatim (Shared-layout-continuity.tsx:69-89); the card cycles 2 → 1 → 2.
|
|
32
|
+
// The pin's animate-table background colors ride as statics so the swap reads on the capture.
|
|
33
|
+
const SHAPE_STYLES: readonly MotionViewStyle[] = [
|
|
34
|
+
{ width: 100, height: 100, top: 100 },
|
|
35
|
+
{ width: 200, height: 200, left: 100 },
|
|
36
|
+
{ width: 100, height: 100, left: 'calc(100vw - 100px)' },
|
|
37
|
+
{ width: 200, height: 200 },
|
|
38
|
+
]
|
|
39
|
+
const SHAPE_COLORS = ['#09f', '#90f', '#f09', '#9f0'] as const
|
|
40
|
+
const CALC_SHAPE = 2
|
|
41
|
+
const PX_SHAPE = 1
|
|
42
|
+
const PX_TARGET_X = 100
|
|
43
|
+
|
|
44
|
+
const ROW_MOUNT = 'first mount ON the calc shape: host x = vw − 100 (±2), no flight'
|
|
45
|
+
const ROW_FLIGHT = 'keyed remount flight: mid-flight between + moving, settles at left:100'
|
|
46
|
+
const ROW_CALC_TARGET = 'calc TARGET leg: remount onto the calc shape settles at vw − 100'
|
|
47
|
+
const ROW_ORDER = [ROW_MOUNT, ROW_FLIGHT, ROW_CALC_TARGET] as const
|
|
48
|
+
|
|
49
|
+
// Timeline (ms). Dwells make every staged state capturable: the boot frame holds the calc shape
|
|
50
|
+
// (0–1600), the mid-flight window is ~3000–3400, the settled px shape dwells 5100–5700, and the
|
|
51
|
+
// final verdict holds from ~9500 on.
|
|
52
|
+
const T_MOUNT = 1200
|
|
53
|
+
const T_CYCLE_1 = 1600
|
|
54
|
+
const T_MID_1 = 3000
|
|
55
|
+
const T_MID_2 = 3400
|
|
56
|
+
const T_SETTLE_1 = 5100
|
|
57
|
+
const T_CYCLE_2 = 5700
|
|
58
|
+
const T_SETTLE_2 = 9200
|
|
59
|
+
const T_DONE = 9500
|
|
60
|
+
|
|
61
|
+
interface RowResult {
|
|
62
|
+
readonly pass: boolean
|
|
63
|
+
readonly detail: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ContinuityChecks() {
|
|
67
|
+
const severity = ambientNativeSeverity()
|
|
68
|
+
const [count, setCount] = useState(CALC_SHAPE)
|
|
69
|
+
const [stage, setStage] = useState('boot: first mount ON the calc shape (styles[2])')
|
|
70
|
+
const [liveX, setLiveX] = useState('—')
|
|
71
|
+
const [rows, setRows] = useState<Partial<Record<(typeof ROW_ORDER)[number], RowResult>>>({})
|
|
72
|
+
|
|
73
|
+
const boxRef = useRef<ComponentRef<typeof MotionView> | null>(null)
|
|
74
|
+
const vwRef = useRef(Dimensions.get('window').width)
|
|
75
|
+
const cycleStampRef = useRef(0)
|
|
76
|
+
const settleLatenciesRef = useRef<number[]>([])
|
|
77
|
+
const mid1Ref = useRef<{ x: number; at: number } | null>(null)
|
|
78
|
+
const mid2Ref = useRef<{ x: number; at: number } | null>(null)
|
|
79
|
+
|
|
80
|
+
const finalize = (id: (typeof ROW_ORDER)[number], pass: boolean, detail: string): void => {
|
|
81
|
+
setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const vw = vwRef.current
|
|
86
|
+
const calcX = vw - 100
|
|
87
|
+
const measureBox = (done: (x: number | null) => void): void => {
|
|
88
|
+
const node = boxRef.current
|
|
89
|
+
if (node === null) {
|
|
90
|
+
done(null)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
node.measureInWindow((x, _y, width) => done(width > 0 ? x : null))
|
|
94
|
+
}
|
|
95
|
+
const fmt = (x: number | null): string => (x === null ? 'unavailable' : x.toFixed(1))
|
|
96
|
+
|
|
97
|
+
const timers: ReturnType<typeof setTimeout>[] = []
|
|
98
|
+
const at = (ms: number, run: () => void): void => {
|
|
99
|
+
timers.push(setTimeout(run, ms))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 1. FIRST MOUNT — the calc seed must be resolved to px BEFORE Yoga ever sees the style.
|
|
103
|
+
at(T_MOUNT, () => {
|
|
104
|
+
measureBox((x) => {
|
|
105
|
+
setLiveX(fmt(x))
|
|
106
|
+
const settles = settleLatenciesRef.current.length
|
|
107
|
+
const pass = x !== null && Math.abs(x - calcX) <= 2 && settles === 0
|
|
108
|
+
finalize(
|
|
109
|
+
ROW_MOUNT,
|
|
110
|
+
pass,
|
|
111
|
+
`measured x=${fmt(x)}, vw=${vw.toFixed(0)} (want ${calcX.toFixed(0)}±2); settles=${settles} ` +
|
|
112
|
+
'(a silent Yoga drop lays out at x≈0; an entrance animation fires a settle)',
|
|
113
|
+
)
|
|
114
|
+
setStage('first mount measured — cycling to the plain px shape')
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// 2. KEYED REMOUNT FLIGHT — calc shape → styles[1] (left:100, 200×200) on the authored 3s.
|
|
119
|
+
at(T_CYCLE_1, () => {
|
|
120
|
+
cycleStampRef.current = Date.now()
|
|
121
|
+
setCount(PX_SHAPE)
|
|
122
|
+
setStage('flight 1: calc shape → left:100 (keyed remount, 3s tween)')
|
|
123
|
+
})
|
|
124
|
+
at(T_MID_1, () => {
|
|
125
|
+
measureBox((x) => {
|
|
126
|
+
if (x !== null) mid1Ref.current = { x, at: Date.now() - cycleStampRef.current }
|
|
127
|
+
setLiveX(fmt(x))
|
|
128
|
+
setStage('flight 1 mid-flight — first sample stamped')
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
at(T_MID_2, () => {
|
|
132
|
+
measureBox((x) => {
|
|
133
|
+
if (x !== null) mid2Ref.current = { x, at: Date.now() - cycleStampRef.current }
|
|
134
|
+
setLiveX(fmt(x))
|
|
135
|
+
setStage('flight 1 mid-flight — second sample stamped')
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
at(T_SETTLE_1, () => {
|
|
139
|
+
measureBox((settled) => {
|
|
140
|
+
setLiveX(fmt(settled))
|
|
141
|
+
const mid1 = mid1Ref.current
|
|
142
|
+
const mid2 = mid2Ref.current
|
|
143
|
+
let bad: string | null = null
|
|
144
|
+
if (mid1 === null || mid2 === null) {
|
|
145
|
+
bad = 'a mid-flight sample never measured (the box was not mounted mid-flight)'
|
|
146
|
+
} else {
|
|
147
|
+
// The flight runs origin (vw − 100) → target (100): strictly between with 15px
|
|
148
|
+
// floors, then still moving 400ms later, then exactly on the target at settle.
|
|
149
|
+
if (!(mid1.x < calcX - 15 && mid1.x > PX_TARGET_X + 15)) {
|
|
150
|
+
bad = `mid-flight sample x=${mid1.x.toFixed(1)} not strictly between ${PX_TARGET_X} and ${calcX.toFixed(0)} (a snap fails one side)`
|
|
151
|
+
} else if (!(mid2.x < mid1.x - 2)) {
|
|
152
|
+
bad = `the box did not move between the samples (${mid1.x.toFixed(1)} → ${mid2.x.toFixed(1)}) — the flight never started`
|
|
153
|
+
} else if (settled === null || Math.abs(settled - PX_TARGET_X) > 2) {
|
|
154
|
+
bad = `settled x=${fmt(settled)}, want ${PX_TARGET_X}±2`
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const latency = settleLatenciesRef.current[0]
|
|
158
|
+
finalize(
|
|
159
|
+
ROW_FLIGHT,
|
|
160
|
+
bad === null,
|
|
161
|
+
bad ??
|
|
162
|
+
`mid1 x=${mid1!.x.toFixed(1)} @+${(mid1!.at / 1000).toFixed(2)}s, mid2 x=${mid2!.x.toFixed(1)} ` +
|
|
163
|
+
`@+${(mid2!.at / 1000).toFixed(2)}s (moved ${(mid1!.x - mid2!.x).toFixed(1)}), settled x=${fmt(settled)}` +
|
|
164
|
+
(latency === undefined
|
|
165
|
+
? ''
|
|
166
|
+
: `; settle callback at +${(latency / 1000).toFixed(2)}s (3s class)`),
|
|
167
|
+
)
|
|
168
|
+
setStage('flight 1 settled at left:100 — dwelling for the capture')
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// 3. THE CALC TARGET LEG — the flight destination is the calc shape itself.
|
|
173
|
+
at(T_CYCLE_2, () => {
|
|
174
|
+
cycleStampRef.current = Date.now()
|
|
175
|
+
setCount(CALC_SHAPE)
|
|
176
|
+
setStage('flight 2: left:100 → calc(100vw - 100px) TARGET (keyed remount, 3s tween)')
|
|
177
|
+
})
|
|
178
|
+
at(T_SETTLE_2, () => {
|
|
179
|
+
measureBox((x) => {
|
|
180
|
+
setLiveX(fmt(x))
|
|
181
|
+
const pass = x !== null && Math.abs(x - calcX) <= 2
|
|
182
|
+
const latency = settleLatenciesRef.current[1]
|
|
183
|
+
finalize(
|
|
184
|
+
ROW_CALC_TARGET,
|
|
185
|
+
pass,
|
|
186
|
+
(pass
|
|
187
|
+
? `settled x=${fmt(x)}, vw=${vw.toFixed(0)} (want ${calcX.toFixed(0)}±2) — the projection measured the resolved calc rect`
|
|
188
|
+
: `settled x=${fmt(x)}, want ${calcX.toFixed(0)}±2 (a dropped calc seed at the target lays out at x≈0)`) +
|
|
189
|
+
(latency === undefined ? '' : `; settle callback at +${(latency / 1000).toFixed(2)}s`),
|
|
190
|
+
)
|
|
191
|
+
setStage('flight 2 settled on the calc shape')
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
at(T_DONE, () => setStage('done'))
|
|
195
|
+
|
|
196
|
+
return () => {
|
|
197
|
+
for (const timer of timers) clearTimeout(timer)
|
|
198
|
+
}
|
|
199
|
+
// All captured values are refs or stable setters; the timeline runs once per mount.
|
|
200
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
201
|
+
}, [])
|
|
202
|
+
|
|
203
|
+
const done = ROW_ORDER.every((id) => rows[id] !== undefined)
|
|
204
|
+
const failing = ROW_ORDER.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
|
|
205
|
+
const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
|
|
206
|
+
const passed = Object.keys(rows).length - failing.length
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<ScrollView style={styles.scroll} contentContainerStyle={styles.container}>
|
|
210
|
+
<View style={styles.padded}>
|
|
211
|
+
<Text style={styles.title}>U6 shared-layout continuity checks</Text>
|
|
212
|
+
<Text style={styles.subtitle}>
|
|
213
|
+
layoutId="u6-box" keyed remount, calc-seeded static style ({severity} severity)
|
|
214
|
+
</Text>
|
|
215
|
+
<View
|
|
216
|
+
style={[
|
|
217
|
+
styles.banner,
|
|
218
|
+
verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
|
|
219
|
+
]}
|
|
220
|
+
>
|
|
221
|
+
<Text style={styles.bannerText}>{verdict}</Text>
|
|
222
|
+
</View>
|
|
223
|
+
<Text style={styles.tally}>
|
|
224
|
+
{done
|
|
225
|
+
? `${passed} PASS / ${failing.length} FAIL of ${ROW_ORDER.length} rows`
|
|
226
|
+
: `${Object.keys(rows).length}/${ROW_ORDER.length} rows complete — RUNNING`}
|
|
227
|
+
</Text>
|
|
228
|
+
<Text style={styles.tallyDetail}>
|
|
229
|
+
{done
|
|
230
|
+
? failing.length === 0
|
|
231
|
+
? 'no failing rows'
|
|
232
|
+
: `FAILING: ${failing.join(' | ')}`
|
|
233
|
+
: `stage: ${stage}`}
|
|
234
|
+
</Text>
|
|
235
|
+
<Text style={styles.status}>
|
|
236
|
+
stage: {stage} · count={count} · measured x={liveX} · vw={vwRef.current.toFixed(0)}
|
|
237
|
+
</Text>
|
|
238
|
+
</View>
|
|
239
|
+
|
|
240
|
+
{/* The full-bleed stage: stage left = window x 0 and width = vw, so the viewport-resolved
|
|
241
|
+
calc seed reads directly in window coordinates. */}
|
|
242
|
+
<View style={styles.stage} accessibilityLabel="u6-continuity-stage">
|
|
243
|
+
<MotionRoot>
|
|
244
|
+
<MotionView
|
|
245
|
+
key={`shape-${count}`}
|
|
246
|
+
layoutId="u6-box"
|
|
247
|
+
ref={boxRef}
|
|
248
|
+
transition={{ duration: 3 }}
|
|
249
|
+
onLayoutAnimationComplete={() => {
|
|
250
|
+
settleLatenciesRef.current.push(Date.now() - cycleStampRef.current)
|
|
251
|
+
}}
|
|
252
|
+
style={[styles.box, SHAPE_STYLES[count], { backgroundColor: SHAPE_COLORS[count]! }]}
|
|
253
|
+
accessibilityLabel={`u6-continuity-shape-${count}`}
|
|
254
|
+
/>
|
|
255
|
+
</MotionRoot>
|
|
256
|
+
</View>
|
|
257
|
+
|
|
258
|
+
<View style={styles.padded}>
|
|
259
|
+
{ROW_ORDER.map((id) => {
|
|
260
|
+
const row = rows[id]
|
|
261
|
+
return (
|
|
262
|
+
<View key={id} style={styles.row}>
|
|
263
|
+
<Text
|
|
264
|
+
style={[
|
|
265
|
+
styles.verdict,
|
|
266
|
+
row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
|
|
267
|
+
]}
|
|
268
|
+
>
|
|
269
|
+
{row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
|
|
270
|
+
</Text>
|
|
271
|
+
<View style={styles.rowBody}>
|
|
272
|
+
<Text style={styles.rowId}>{id}</Text>
|
|
273
|
+
<Text style={styles.rowDetail} numberOfLines={3}>
|
|
274
|
+
{row === undefined ? 'pending' : row.detail}
|
|
275
|
+
</Text>
|
|
276
|
+
</View>
|
|
277
|
+
</View>
|
|
278
|
+
)
|
|
279
|
+
})}
|
|
280
|
+
</View>
|
|
281
|
+
</ScrollView>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function SharedLayoutContinuityChecksScreen() {
|
|
286
|
+
// layoutId requires a MotionRoot above it (REQ-LAYOUT-003): the root owns the shared
|
|
287
|
+
// layout-identity registry the keyed remount exchanges through.
|
|
288
|
+
return (
|
|
289
|
+
<MotionRoot>
|
|
290
|
+
<ContinuityChecks />
|
|
291
|
+
</MotionRoot>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const styles = StyleSheet.create({
|
|
296
|
+
scroll: { flex: 1, backgroundColor: '#0b0b12' },
|
|
297
|
+
container: { paddingVertical: 72 },
|
|
298
|
+
padded: { paddingHorizontal: 12 },
|
|
299
|
+
title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
|
|
300
|
+
subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
|
|
301
|
+
banner: {
|
|
302
|
+
alignSelf: 'center',
|
|
303
|
+
paddingHorizontal: 36,
|
|
304
|
+
paddingVertical: 10,
|
|
305
|
+
borderRadius: 10,
|
|
306
|
+
marginTop: 18,
|
|
307
|
+
marginBottom: 10,
|
|
308
|
+
},
|
|
309
|
+
pass: { backgroundColor: '#1d7a3e' },
|
|
310
|
+
fail: { backgroundColor: '#a32633' },
|
|
311
|
+
running: { backgroundColor: '#7a641d' },
|
|
312
|
+
bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
|
|
313
|
+
tally: { color: '#e6e6f0', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
314
|
+
tallyDetail: { color: '#f87171', fontSize: 11, marginTop: 3, textAlign: 'center' },
|
|
315
|
+
status: {
|
|
316
|
+
color: '#c9c9d8',
|
|
317
|
+
fontSize: 11,
|
|
318
|
+
marginTop: 8,
|
|
319
|
+
textAlign: 'center',
|
|
320
|
+
fontFamily: 'Menlo',
|
|
321
|
+
},
|
|
322
|
+
stage: {
|
|
323
|
+
height: 230,
|
|
324
|
+
marginTop: 14,
|
|
325
|
+
backgroundColor: '#14141d',
|
|
326
|
+
borderRadius: 8,
|
|
327
|
+
overflow: 'hidden',
|
|
328
|
+
},
|
|
329
|
+
box: { position: 'absolute', borderRadius: 10 },
|
|
330
|
+
row: { flexDirection: 'row', marginTop: 10 },
|
|
331
|
+
verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
|
|
332
|
+
ok: { color: '#4ade80' },
|
|
333
|
+
bad: { color: '#f87171' },
|
|
334
|
+
pending: { color: '#8a8aa0' },
|
|
335
|
+
rowBody: { flex: 1 },
|
|
336
|
+
rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
|
|
337
|
+
rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
|
|
338
|
+
})
|