@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,614 @@
|
|
|
1
|
+
// U7a reduced-motion device checks (the U7a packet's floor 3 — dissolved at the unit terminal
|
|
2
|
+
// into REQ-API-027-adjacent surface text): the
|
|
3
|
+
// native card mirroring the pinned catalog useReducedMotion.tsx example — an outer motion View
|
|
4
|
+
// carrying the animate label with initial={false}, an inner variants-only child whose embedded
|
|
5
|
+
// transition is SELECTED BY THE HOOK SNAPSHOT (`shouldReduceMotion ? { type: false } :
|
|
6
|
+
// { duration: 1 }`), visibility toggling every ~1.5s. Five claims, self-driven:
|
|
7
|
+
//
|
|
8
|
+
// 1. HOOK RESOLVE — the mounted hook's first-render value is false (the pin's matchMedia-absent
|
|
9
|
+
// fallback, packet F2's async transient) and the resolved value equals a DIRECT
|
|
10
|
+
// AccessibilityInfo.isReduceMotionEnabled() read taken at +900ms. On the proof devices the
|
|
11
|
+
// OS preference is OFF, so the resolve is a same-value setState bail; the one-update /
|
|
12
|
+
// no-subscription snapshot law is pinned by the mock-driven useReducedMotion.test.ts — the
|
|
13
|
+
// row says so rather than claiming a device witness it cannot have.
|
|
14
|
+
// 2. SNAPSHOT SELECTS THE TWEEN — with the resolved value false the example's toggle drives
|
|
15
|
+
// opacity through the 1s flight: mid-fade onUpdate samples must land STRICTLY between 0.02
|
|
16
|
+
// and 0.98 in BOTH fades (a broken label propagation animates nothing; an instant commit
|
|
17
|
+
// shows no interior samples), each fade must REACH its target, and the flight-activity
|
|
18
|
+
// span (how long the samples keep moving) must read the 1s class — all measured from the
|
|
19
|
+
// committed samples, never the settle marshal. The fade peak and the onAnimationComplete
|
|
20
|
+
// stamps are recorded as evidence, not gated: the 2026-08-18 proof showed the flight
|
|
21
|
+
// running with a spring OVERSHOOT (peak ≈1.046 — native's omitted-type { duration }
|
|
22
|
+
// mapping, FIXED by REQ-TIMING-006: the fade now tweens monotone, peak ≤ 1.0) and zero
|
|
23
|
+
// completion deliveries (RESOLVED 2026-08-21 by the finding-B unit: the dispatch chain was
|
|
24
|
+
// intact — the fade-in was superseded by the raced fade-out retarget, which the widened
|
|
25
|
+
// toggle gap above removes, and the card read completions once past their landing).
|
|
26
|
+
// 3. THE INSTANT LANE (contract 0's device witness) — a second box on an AUTHORED
|
|
27
|
+
// `{ type: false }` transition: on retarget the final value commits with NO flight. The
|
|
28
|
+
// same-tick sample must still read the origin (the commit is NEXT-FRAME, never synchronous
|
|
29
|
+
// at the retarget call) and the rAF chain must read the target within the 4-frame
|
|
30
|
+
// JS-readback band: the pin's law covers the UI-side commit (the driver's next update
|
|
31
|
+
// tick), while this card measures the JS readback, which adds the command crossing, the
|
|
32
|
+
// t=0 frame (null dt — the pin's frame.update deferral), and the marshal's setTimeout(0)
|
|
33
|
+
// return; the delay leg's +350ms on a 300ms delay corroborates the ~3-frame round trip.
|
|
34
|
+
// Any post-retarget sample strictly between origin and target is a flight and fails.
|
|
35
|
+
// 4. AUTHORED DELAY — the same box retargeted on `{ type: false, delay: 0.3 }`: a +150ms
|
|
36
|
+
// sample still reads the origin, a +600ms sample reads the target (the pin's authored-form
|
|
37
|
+
// asymmetry — the global instant flag zeroes delay, the authored form honors it).
|
|
38
|
+
// 5. CLEAN RUN — zero refusal reports through the console tap (the U5 pattern: console.error
|
|
39
|
+
// is the severity law's production error channel) across the whole timeline.
|
|
40
|
+
//
|
|
41
|
+
// Readback honesty: every opacity sample rides the element's own onUpdate (the driver's
|
|
42
|
+
// per-frame committed-values marshal) — the value the host actually received, never a JS-side
|
|
43
|
+
// mirror. A screenshot is the assertion; the console tap is evidence capture, never suppression.
|
|
44
|
+
|
|
45
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
46
|
+
import { AccessibilityInfo, ScrollView, StyleSheet, Text, View } from 'react-native'
|
|
47
|
+
import type { Transition } from '@unrulysystems/native-motion-core'
|
|
48
|
+
import { MotionView } from '../../native/motion/MotionView'
|
|
49
|
+
import { ambientNativeSeverity } from '../../native/motion/severity'
|
|
50
|
+
import { useReducedMotion } from '../../native/motion/useReducedMotion'
|
|
51
|
+
|
|
52
|
+
const ROW_HOOK = 'hook resolve: first-render false, resolved value = the direct OS read'
|
|
53
|
+
const ROW_TWEEN = 'snapshot selects the 1s tween: mid-fade samples strictly between 0.02 and 0.98'
|
|
54
|
+
const ROW_INSTANT = 'authored { type: false }: no flight — next-frame commit, never synchronous'
|
|
55
|
+
const ROW_DELAY = 'authored { type: false, delay: 0.3 }: the authored delay is honored'
|
|
56
|
+
const ROW_CLEAN = 'clean run: zero refusal reports through the console tap'
|
|
57
|
+
const ROW_ORDER = [ROW_HOOK, ROW_TWEEN, ROW_INSTANT, ROW_DELAY, ROW_CLEAN] as const
|
|
58
|
+
|
|
59
|
+
// The instant box's two legs: 0.15 → 0.85 bare, then 0.85 → 0.15 under the authored 300ms delay.
|
|
60
|
+
const INSTANT_ORIGIN = 0.15
|
|
61
|
+
const INSTANT_TARGET = 0.85
|
|
62
|
+
const EPS = 0.005
|
|
63
|
+
|
|
64
|
+
// Timeline (ms). Dwells make every staged state capturable: the boot frame holds the pin
|
|
65
|
+
// example's hidden mount (0–1500, the initial={false} witness), the fade-in mid-flight window is
|
|
66
|
+
// ~1900–2300 and the fade-out's ~5000–5400, the instant-lane commit dwells 7600–8100, and the
|
|
67
|
+
// final verdict holds from ~9700 on. The 2026-08-21 re-base (finding-B resolution, REQ-API-060):
|
|
68
|
+
// the toggle gap widened 1700 → 3100ms so the 1s fade-in completes BEFORE the fade-out retarget
|
|
69
|
+
// even under the measured ~770ms mount→flight-start latency — at the original 3200 the fade-in
|
|
70
|
+
// was superseded mid-flight (peak 0.858, no completion), a healthy-but-raced flight, not a
|
|
71
|
+
// defect; and row 2's finalize is event-driven on the two completions (a fixed wall-clock
|
|
72
|
+
// finalize raced the stretched flight both ways).
|
|
73
|
+
const T_HOOK = 900
|
|
74
|
+
const T_TOGGLE_1 = 1500
|
|
75
|
+
const T_TOGGLE_2 = 4600
|
|
76
|
+
const T_INSTANT_RETARGET = 7000
|
|
77
|
+
const T_INSTANT_FINALIZE = 7600
|
|
78
|
+
const T_DELAY_RETARGET = 8100
|
|
79
|
+
const T_DELAY_EARLY = 8250
|
|
80
|
+
const T_DELAY_LATE = 8700
|
|
81
|
+
const T_DELAY_FINALIZE = 8900
|
|
82
|
+
const T_CLEAN = 9500
|
|
83
|
+
const T_DONE = 9700
|
|
84
|
+
|
|
85
|
+
interface RowResult {
|
|
86
|
+
readonly pass: boolean
|
|
87
|
+
readonly detail: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface OpacitySample {
|
|
91
|
+
readonly at: number
|
|
92
|
+
readonly value: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readOpacity(latest: Readonly<Record<string, string | number>>): number | null {
|
|
96
|
+
const raw = latest['opacity']
|
|
97
|
+
const value = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number(raw) : NaN
|
|
98
|
+
return Number.isFinite(value) ? value : null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The flight-activity span: how long the committed samples keep MOVING (a later sample differs
|
|
102
|
+
// by > 0.002), measured from the FIRST moving sample to the last. Generator-agnostic by design —
|
|
103
|
+
// the pin's 1s tween and native's 1s duration-spring both move for ~1s; a default-duration
|
|
104
|
+
// fallback (~0.3s) or an instant commit falls short. Latency-agnostic by the same design
|
|
105
|
+
// (2026-08-21, finding-B re-base): measuring from the toggle conflated the ~770ms
|
|
106
|
+
// mount→flight-start latency with the flight class and read 1944ms on a healthy 1s tween. The
|
|
107
|
+
// band tops at 2400ms because the driver's dt CEILING (40ms; owner REQ-CLOCK-003, consumer
|
|
108
|
+
// REQ-DRIVER-012) stretches a 1s
|
|
109
|
+
// flight to ~1.9s under a loaded host — a 3s flight still reads well past the band.
|
|
110
|
+
function activitySpan(w: readonly OpacitySample[]): number | null {
|
|
111
|
+
let firstMoving = -1
|
|
112
|
+
for (let i = 0; i < w.length - 1; i++) {
|
|
113
|
+
if (Math.abs(w[i + 1]!.value - w[i]!.value) > 0.002) {
|
|
114
|
+
firstMoving = i
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (firstMoving === -1) return null
|
|
119
|
+
for (let i = w.length - 2; i >= firstMoving; i--) {
|
|
120
|
+
if (Math.abs(w[i + 1]!.value - w[i]!.value) > 0.002) return w[i + 1]!.at - w[firstMoving]!.at
|
|
121
|
+
}
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function fmtSpan(span: number | null): string {
|
|
126
|
+
return span === null ? 'none' : `~${span.toFixed(0)}ms`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function ReducedMotionChecksScreen() {
|
|
130
|
+
const severity = ambientNativeSeverity()
|
|
131
|
+
const shouldReduceMotion = useReducedMotion()
|
|
132
|
+
|
|
133
|
+
// The pin example's exact structure: the transition is selected by the hook snapshot and rides
|
|
134
|
+
// INSIDE the variant entries. Memoized on the hook value so an unrelated re-render never
|
|
135
|
+
// re-identities the dictionary into the label lane (R7 law b: labels execute on a definition
|
|
136
|
+
// change) — on the proof devices the value never changes after resolve, so this is the pin's
|
|
137
|
+
// `transition` constant, not a reactive re-arm.
|
|
138
|
+
const variants = useMemo(() => {
|
|
139
|
+
const transition: Transition = shouldReduceMotion ? { type: false } : { duration: 1 }
|
|
140
|
+
return {
|
|
141
|
+
visible: { opacity: 1, transition },
|
|
142
|
+
hidden: { opacity: 0, transition },
|
|
143
|
+
}
|
|
144
|
+
}, [shouldReduceMotion])
|
|
145
|
+
|
|
146
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
147
|
+
const [instantTarget, setInstantTarget] = useState(INSTANT_ORIGIN)
|
|
148
|
+
const [instantTransition, setInstantTransition] = useState<Transition>({ type: false })
|
|
149
|
+
const [stage, setStage] = useState('boot: mounted hidden (initial={false}), hook resolving')
|
|
150
|
+
const [liveTween, setLiveTween] = useState('—')
|
|
151
|
+
const [liveInstant, setLiveInstant] = useState(INSTANT_ORIGIN.toFixed(3))
|
|
152
|
+
const [rows, setRows] = useState<Partial<Record<(typeof ROW_ORDER)[number], RowResult>>>({})
|
|
153
|
+
|
|
154
|
+
// Row 1 evidence: the first-render value, the live resolved value (a ref so the single-run
|
|
155
|
+
// timeline never reads a stale closure), and the distinct-value history (the snapshot law).
|
|
156
|
+
const mountStampRef = useRef(Date.now())
|
|
157
|
+
const firstRenderRef = useRef<boolean | null>(null)
|
|
158
|
+
if (firstRenderRef.current === null) firstRenderRef.current = shouldReduceMotion
|
|
159
|
+
const resolvedRef = useRef(shouldReduceMotion)
|
|
160
|
+
resolvedRef.current = shouldReduceMotion
|
|
161
|
+
const valueHistoryRef = useRef<readonly (boolean | null)[]>([])
|
|
162
|
+
|
|
163
|
+
// Row 2 evidence: every committed opacity sample + completion stamps on the tween subject.
|
|
164
|
+
const tweenSamplesRef = useRef<OpacitySample[]>([])
|
|
165
|
+
const tweenCompletionsRef = useRef<number[]>([])
|
|
166
|
+
const toggleStampsRef = useRef<number[]>([])
|
|
167
|
+
|
|
168
|
+
// Rows 3/4 evidence: the instant box's committed samples, the same-tick read, the retarget
|
|
169
|
+
// rAF chain, and the delay leg's early/late reads.
|
|
170
|
+
const instantSamplesRef = useRef<OpacitySample[]>([])
|
|
171
|
+
const instantLatestRef = useRef(INSTANT_ORIGIN)
|
|
172
|
+
const instantStampRef = useRef(0)
|
|
173
|
+
const delayStampRef = useRef(0)
|
|
174
|
+
const sameTickRef = useRef<number | null>(null)
|
|
175
|
+
const frameChainRef = useRef<OpacitySample[]>([])
|
|
176
|
+
const delayEarlyRef = useRef<number | null>(null)
|
|
177
|
+
const delayLateRef = useRef<number | null>(null)
|
|
178
|
+
|
|
179
|
+
const reportsRef = useRef<string[]>([])
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
valueHistoryRef.current = [...valueHistoryRef.current, shouldReduceMotion]
|
|
183
|
+
}, [shouldReduceMotion])
|
|
184
|
+
|
|
185
|
+
const finalize = (id: (typeof ROW_ORDER)[number], pass: boolean, detail: string): void => {
|
|
186
|
+
setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
const mountStamp = mountStampRef.current
|
|
191
|
+
const now = (): number => Date.now() - mountStamp
|
|
192
|
+
|
|
193
|
+
// The production error channel tap (the U5 pattern): refusal reports ride consoleReporter →
|
|
194
|
+
// console.error. Installed for this screen's lifetime only; everything passes through.
|
|
195
|
+
const originalError = console.error
|
|
196
|
+
console.error = (...args: unknown[]) => {
|
|
197
|
+
for (const arg of args) {
|
|
198
|
+
const message = arg instanceof Error ? arg.message : typeof arg === 'string' ? arg : null
|
|
199
|
+
if (message !== null && /motion/i.test(message)) reportsRef.current.push(message)
|
|
200
|
+
}
|
|
201
|
+
originalError(...args)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const timers: ReturnType<typeof setTimeout>[] = []
|
|
205
|
+
const at = (ms: number, run: () => void): void => {
|
|
206
|
+
timers.push(setTimeout(run, ms))
|
|
207
|
+
}
|
|
208
|
+
let raf: ReturnType<typeof requestAnimationFrame> | null = null
|
|
209
|
+
|
|
210
|
+
const fmt = (value: number | null): string =>
|
|
211
|
+
value === null ? 'unavailable' : value.toFixed(3)
|
|
212
|
+
|
|
213
|
+
// 1. HOOK RESOLVE — compare the mounted hook's resolved value against a direct OS read.
|
|
214
|
+
at(T_HOOK, () => {
|
|
215
|
+
void AccessibilityInfo.isReduceMotionEnabled()
|
|
216
|
+
.then((osEnabled) => {
|
|
217
|
+
const resolved = resolvedRef.current
|
|
218
|
+
const changes = valueHistoryRef.current.length - 1
|
|
219
|
+
const pass = firstRenderRef.current === false && resolved === osEnabled && changes <= 1
|
|
220
|
+
finalize(
|
|
221
|
+
ROW_HOOK,
|
|
222
|
+
pass,
|
|
223
|
+
`first render ${String(firstRenderRef.current)} (the pin's matchMedia-absent ` +
|
|
224
|
+
`fallback — packet F2's async transient); resolved ${String(resolved)} = direct ` +
|
|
225
|
+
`AccessibilityInfo read ${String(osEnabled)} at +${T_HOOK}ms; ${changes} value ` +
|
|
226
|
+
'change(s) this mount (OS OFF → the false→false resolve is a same-value setState ' +
|
|
227
|
+
'bail; the one-update/no-subscription snapshot law is pinned by the mock-driven ' +
|
|
228
|
+
'useReducedMotion.test.ts, not claimable on device)',
|
|
229
|
+
)
|
|
230
|
+
setStage('hook resolved — toggle 1: hidden → visible on the selected transition')
|
|
231
|
+
})
|
|
232
|
+
.catch((error: unknown) => {
|
|
233
|
+
finalize(
|
|
234
|
+
ROW_HOOK,
|
|
235
|
+
false,
|
|
236
|
+
`the direct AccessibilityInfo read rejected: ${error instanceof Error ? error.message : String(error)}`,
|
|
237
|
+
)
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// 2. The pin example's toggle: visibility flips ~every 1.5s, the snapshot-selected
|
|
242
|
+
// transition carries it (resolved false on the proof devices → the 1s tween).
|
|
243
|
+
at(T_TOGGLE_1, () => {
|
|
244
|
+
toggleStampsRef.current = [now()]
|
|
245
|
+
setIsVisible(true)
|
|
246
|
+
setStage('fade-in: hidden → visible (mid-flight capture window ~+1.9–2.3s)')
|
|
247
|
+
})
|
|
248
|
+
at(T_TOGGLE_2, () => {
|
|
249
|
+
toggleStampsRef.current = [...toggleStampsRef.current, now()]
|
|
250
|
+
setIsVisible(false)
|
|
251
|
+
setStage('fade-out: visible → hidden (mid-flight capture window ~+5.0–5.4s)')
|
|
252
|
+
})
|
|
253
|
+
const finalizeTweenRow = (): void => {
|
|
254
|
+
const [t1, t2] = toggleStampsRef.current
|
|
255
|
+
const samples = tweenSamplesRef.current
|
|
256
|
+
const completions = tweenCompletionsRef.current
|
|
257
|
+
if (t1 === undefined || t2 === undefined) {
|
|
258
|
+
finalize(ROW_TWEEN, false, 'a toggle never fired — the timeline is broken')
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
// The 2026-08-21 latency-tolerant window (finding-B resolution): 1400ms raced the
|
|
262
|
+
// ~770ms mount→flight-start latency — the flight ends at toggle+~1800ms on the proof
|
|
263
|
+
// devices, so the peak/trough read landed mid-flight (0.844) even with the supersession
|
|
264
|
+
// removed. 2400ms covers flight + latency with margin and never reaches the next toggle.
|
|
265
|
+
const window = (t: number): OpacitySample[] =>
|
|
266
|
+
samples.filter((s) => s.at >= t && s.at <= t + 2400)
|
|
267
|
+
const interior = (t: number): OpacitySample[] =>
|
|
268
|
+
window(t).filter((s) => s.value > 0.02 && s.value < 0.98)
|
|
269
|
+
const in1 = interior(t1)
|
|
270
|
+
const in2 = interior(t2)
|
|
271
|
+
const w1 = window(t1)
|
|
272
|
+
const w2 = window(t2)
|
|
273
|
+
const span1 = activitySpan(w1)
|
|
274
|
+
const span2 = activitySpan(w2)
|
|
275
|
+
const peak1 = w1.reduce((max, s) => Math.max(max, s.value), 0)
|
|
276
|
+
const trough2 = w2.reduce((min, s) => Math.min(min, s.value), 1)
|
|
277
|
+
const preToggle = samples.filter((s) => s.at < t1).length
|
|
278
|
+
let bad: string | null = null
|
|
279
|
+
if (in1.length < 3) {
|
|
280
|
+
bad = `fade-in produced ${in1.length} interior sample(s) — a broken label propagation animates nothing, an instant commit shows none`
|
|
281
|
+
} else if (in1[in1.length - 1]!.value <= in1[0]!.value) {
|
|
282
|
+
bad = `fade-in interior samples do not climb (${in1[0]!.value.toFixed(3)} → ${in1[in1.length - 1]!.value.toFixed(3)})`
|
|
283
|
+
} else if (in2.length < 3) {
|
|
284
|
+
bad = `fade-out produced ${in2.length} interior sample(s)`
|
|
285
|
+
} else if (in2[in2.length - 1]!.value >= in2[0]!.value) {
|
|
286
|
+
bad = `fade-out interior samples do not fall (${in2[0]!.value.toFixed(3)} → ${in2[in2.length - 1]!.value.toFixed(3)})`
|
|
287
|
+
} else if (peak1 < 0.98) {
|
|
288
|
+
bad = `fade-in never reached the target (peak ${peak1.toFixed(3)})`
|
|
289
|
+
} else if (trough2 > 0.02) {
|
|
290
|
+
bad = `fade-out never reached the target (trough ${trough2.toFixed(3)})`
|
|
291
|
+
} else if (span1 === null || span1 < 600 || span1 > 2400) {
|
|
292
|
+
bad = `fade-in flight activity ${fmtSpan(span1)} — want the 1s class (600–2400ms of movement)`
|
|
293
|
+
} else if (span2 === null || span2 < 600 || span2 > 2400) {
|
|
294
|
+
bad = `fade-out flight activity ${fmtSpan(span2)} — want the 1s class`
|
|
295
|
+
}
|
|
296
|
+
// Measured evidence, never gated: the fade peak (peak ≤ 1.0 since REQ-TIMING-006 retired
|
|
297
|
+
// the omitted-type duration-spring mapping; the 2026-08-18 pre-fix run read ≈1.046) and
|
|
298
|
+
// the onAnimationComplete delivery (finding B resolved 2026-08-21: with the widened toggle
|
|
299
|
+
// gap the fade-in is never superseded, so BOTH completions land — the dispatch chain was
|
|
300
|
+
// intact all along, REQ-API-060).
|
|
301
|
+
const completionEvidence =
|
|
302
|
+
completions.length === 0
|
|
303
|
+
? 'none delivered by finalize (finding-B signature — the widened gap should have ' +
|
|
304
|
+
'removed the supersession; investigate before re-proofing)'
|
|
305
|
+
: completions.map((c) => `+${(c - t1).toFixed(0)}ms`).join(', ')
|
|
306
|
+
finalize(
|
|
307
|
+
ROW_TWEEN,
|
|
308
|
+
bad === null,
|
|
309
|
+
bad ??
|
|
310
|
+
`fade-in ${in1.length} interior samples ${in1[0]!.value.toFixed(3)}→${in1[in1.length - 1]!.value.toFixed(3)}, ` +
|
|
311
|
+
`peak ${peak1.toFixed(3)}, activity ${fmtSpan(span1)}; fade-out ${in2.length} interior ` +
|
|
312
|
+
`${in2[0]!.value.toFixed(3)}→${in2[in2.length - 1]!.value.toFixed(3)}, activity ` +
|
|
313
|
+
`${fmtSpan(span2)}; ${preToggle} pre-toggle onUpdate(s) (the initial={false} ` +
|
|
314
|
+
`no-entrance witness); onAnimationComplete: ${completionEvidence}; resolved snapshot ` +
|
|
315
|
+
'false → variants carried { duration: 1 }',
|
|
316
|
+
)
|
|
317
|
+
setStage('tween rows finalized — instant-lane retarget next')
|
|
318
|
+
}
|
|
319
|
+
// Event-driven row-2 finalize (finding-B resolution, the U8 2295fbab analog): the fade
|
|
320
|
+
// completions ARE the settle edges, so the row judges them, never a wall-clock sample — a
|
|
321
|
+
// fixed finalize raced the latency-stretched flight both ways (supersession at 1.7s gap,
|
|
322
|
+
// mid-flight window read at 3.1s). Fail-closed deadline: 3s past the fade-out toggle.
|
|
323
|
+
const tweenRowPoll = setInterval(() => {
|
|
324
|
+
const [, t2] = toggleStampsRef.current
|
|
325
|
+
if (t2 === undefined) return
|
|
326
|
+
if (tweenCompletionsRef.current.length >= 2 || now() - t2 > 3000) {
|
|
327
|
+
clearInterval(tweenRowPoll)
|
|
328
|
+
finalizeTweenRow()
|
|
329
|
+
}
|
|
330
|
+
}, 200)
|
|
331
|
+
|
|
332
|
+
// 3. THE INSTANT LANE — retarget on the authored { type: false }. The same-tick read must
|
|
333
|
+
// still see the origin (the commit is next-frame, not synchronous); the rAF chain must
|
|
334
|
+
// see the target within the 4-frame JS-readback band (the pin's law covers the UI-side
|
|
335
|
+
// commit; the card's readback also spans the command crossing, the null-dt t=0 frame,
|
|
336
|
+
// and the marshal's setTimeout(0) return); no sample may land between origin and target.
|
|
337
|
+
at(T_INSTANT_RETARGET, () => {
|
|
338
|
+
instantStampRef.current = now()
|
|
339
|
+
sameTickRef.current = instantLatestRef.current
|
|
340
|
+
frameChainRef.current = []
|
|
341
|
+
setInstantTarget(INSTANT_TARGET)
|
|
342
|
+
let frame = 0
|
|
343
|
+
const step = (): void => {
|
|
344
|
+
frame += 1
|
|
345
|
+
frameChainRef.current = [
|
|
346
|
+
...frameChainRef.current,
|
|
347
|
+
{ at: now() - instantStampRef.current, value: instantLatestRef.current },
|
|
348
|
+
]
|
|
349
|
+
if (frame < 5) raf = requestAnimationFrame(step)
|
|
350
|
+
}
|
|
351
|
+
raf = requestAnimationFrame(step)
|
|
352
|
+
setStage(`instant retarget: ${INSTANT_ORIGIN} → ${INSTANT_TARGET} on { type: false }`)
|
|
353
|
+
})
|
|
354
|
+
at(T_INSTANT_FINALIZE, () => {
|
|
355
|
+
const stamp = instantStampRef.current
|
|
356
|
+
const post = instantSamplesRef.current.filter((s) => s.at >= stamp)
|
|
357
|
+
const flight = post.find(
|
|
358
|
+
(s) => s.value > INSTANT_ORIGIN + 0.02 && s.value < INSTANT_TARGET - 0.02,
|
|
359
|
+
)
|
|
360
|
+
const sameTick = sameTickRef.current
|
|
361
|
+
const frames = frameChainRef.current
|
|
362
|
+
const targetFrame = frames.findIndex((f) => Math.abs(f.value - INSTANT_TARGET) <= EPS) + 1
|
|
363
|
+
const chain = frames.map((f, i) => `f${i + 1}=${f.value.toFixed(3)}`).join(' ')
|
|
364
|
+
const committed =
|
|
365
|
+
post.length > 0 && Math.abs(post[post.length - 1]!.value - INSTANT_TARGET) <= EPS
|
|
366
|
+
let bad: string | null = null
|
|
367
|
+
if (sameTick === null || Math.abs(sameTick - INSTANT_ORIGIN) > EPS) {
|
|
368
|
+
bad = `the same-tick sample reads ${fmt(sameTick)}, want origin ${INSTANT_ORIGIN} — a synchronous commit at the retarget call`
|
|
369
|
+
} else if (targetFrame === 0 || targetFrame > 4) {
|
|
370
|
+
bad = `the target never landed within the 4-frame JS-readback band of the retarget (chain: ${chain || 'empty'})`
|
|
371
|
+
} else if (flight !== undefined) {
|
|
372
|
+
bad = `a post-retarget sample reads ${flight.value.toFixed(3)} at +${flight.at.toFixed(0)}ms — that is a flight, not an instant commit`
|
|
373
|
+
} else if (!committed) {
|
|
374
|
+
bad = `no post-retarget onUpdate committed the target (samples: ${post.map((s) => s.value.toFixed(3)).join(', ') || 'none'})`
|
|
375
|
+
}
|
|
376
|
+
finalize(
|
|
377
|
+
ROW_INSTANT,
|
|
378
|
+
bad === null,
|
|
379
|
+
bad ??
|
|
380
|
+
`same-tick sample ${sameTick!.toFixed(3)} = origin (the commit is NOT synchronous at ` +
|
|
381
|
+
`the retarget call); target ${INSTANT_TARGET} read at rAF frame ${targetFrame} ` +
|
|
382
|
+
`(chain: ${chain}); post-retarget onUpdates [${post.map((s) => s.value.toFixed(3)).join(', ')}] — ` +
|
|
383
|
+
'no flight (the ≤4-frame band is the JS readback of the UI-side next-frame commit: ' +
|
|
384
|
+
'command crossing + the null-dt t=0 frame + the marshal return)',
|
|
385
|
+
)
|
|
386
|
+
setStage('instant lane committed — authored-delay leg next')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
// 4. AUTHORED DELAY — { type: false, delay: 0.3 }: +150ms still reads the origin, +600ms
|
|
390
|
+
// reads the target. The pin's asymmetry: the authored form honors delay, the global
|
|
391
|
+
// instant flag (7b's window) zeroes it.
|
|
392
|
+
at(T_DELAY_RETARGET, () => {
|
|
393
|
+
delayStampRef.current = now()
|
|
394
|
+
setInstantTransition({ type: false, delay: 0.3 })
|
|
395
|
+
setInstantTarget(INSTANT_ORIGIN)
|
|
396
|
+
setStage(`delay leg: ${INSTANT_TARGET} → ${INSTANT_ORIGIN} on { type: false, delay: 0.3 }`)
|
|
397
|
+
})
|
|
398
|
+
at(T_DELAY_EARLY, () => {
|
|
399
|
+
delayEarlyRef.current = instantLatestRef.current
|
|
400
|
+
setStage('delay leg: +150ms sample taken (must still read the origin)')
|
|
401
|
+
})
|
|
402
|
+
at(T_DELAY_LATE, () => {
|
|
403
|
+
delayLateRef.current = instantLatestRef.current
|
|
404
|
+
setStage('delay leg: +600ms sample taken (must read the target)')
|
|
405
|
+
})
|
|
406
|
+
at(T_DELAY_FINALIZE, () => {
|
|
407
|
+
const stamp = delayStampRef.current
|
|
408
|
+
const post = instantSamplesRef.current.filter((s) => s.at >= stamp)
|
|
409
|
+
const commit = post.find((s) => Math.abs(s.value - INSTANT_ORIGIN) <= EPS)
|
|
410
|
+
const flight = post.find(
|
|
411
|
+
(s) => s.value > INSTANT_ORIGIN + 0.02 && s.value < INSTANT_TARGET - 0.02,
|
|
412
|
+
)
|
|
413
|
+
const early = delayEarlyRef.current
|
|
414
|
+
const late = delayLateRef.current
|
|
415
|
+
const commitAt = commit === undefined ? null : commit.at - stamp
|
|
416
|
+
let bad: string | null = null
|
|
417
|
+
if (early === null || Math.abs(early - INSTANT_TARGET) > EPS) {
|
|
418
|
+
bad = `the +150ms sample reads ${fmt(early)}, want origin ${INSTANT_TARGET} — the authored 300ms delay was not honored`
|
|
419
|
+
} else if (late === null || Math.abs(late - INSTANT_ORIGIN) > EPS) {
|
|
420
|
+
bad = `the +600ms sample reads ${fmt(late)}, want target ${INSTANT_ORIGIN} — the delayed commit never landed`
|
|
421
|
+
} else if (commitAt === null || commitAt < 250 || commitAt > 900) {
|
|
422
|
+
bad = `the commit landed at ${commitAt === null ? 'never' : `+${commitAt.toFixed(0)}ms`} — want the 300ms class (250–900ms)`
|
|
423
|
+
} else if (flight !== undefined) {
|
|
424
|
+
bad = `a post-retarget sample reads ${flight.value.toFixed(3)} — a flight under the delay leg`
|
|
425
|
+
}
|
|
426
|
+
finalize(
|
|
427
|
+
ROW_DELAY,
|
|
428
|
+
bad === null,
|
|
429
|
+
bad ??
|
|
430
|
+
`+150ms sample ${early!.toFixed(3)} = origin (delay holding), +600ms sample ` +
|
|
431
|
+
`${late!.toFixed(3)} = target, commit at +${commitAt!.toFixed(0)}ms — the authored ` +
|
|
432
|
+
'300ms delay is honored (the pin’s authored-form asymmetry: the global instant flag ' +
|
|
433
|
+
'would zero it)',
|
|
434
|
+
)
|
|
435
|
+
setStage('delay leg finalized — clean-run verdict next')
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
// 5. CLEAN RUN — the console tap saw zero refusal reports across the whole timeline. The
|
|
439
|
+
// completion ledger is re-read here as a second witness: before the finding-B
|
|
440
|
+
// resolution the tween row read it once at a wall-clock finalize and a healthy
|
|
441
|
+
// completion landing past the read permanently read "none delivered".
|
|
442
|
+
at(T_CLEAN, () => {
|
|
443
|
+
const reports = reportsRef.current
|
|
444
|
+
const completions = tweenCompletionsRef.current
|
|
445
|
+
finalize(
|
|
446
|
+
ROW_CLEAN,
|
|
447
|
+
reports.length === 0,
|
|
448
|
+
(reports.length === 0
|
|
449
|
+
? `0 refusal reports through the console tap across ${now().toFixed(0)}ms of ` +
|
|
450
|
+
'hook resolve, two tween fades, and both instant-lane legs'
|
|
451
|
+
: `${reports.length} report(s): ${reports
|
|
452
|
+
.map((message) => `"${message.slice(0, 80)}…"`)
|
|
453
|
+
.join(' | ')}`) +
|
|
454
|
+
`; tween completions by T_CLEAN: ${
|
|
455
|
+
completions.length === 0
|
|
456
|
+
? 'none (the finding-B signature)'
|
|
457
|
+
: completions.map((c) => `+${c.toFixed(0)}ms`).join(', ')
|
|
458
|
+
}`,
|
|
459
|
+
)
|
|
460
|
+
})
|
|
461
|
+
at(T_DONE, () => setStage('done'))
|
|
462
|
+
|
|
463
|
+
return () => {
|
|
464
|
+
clearInterval(tweenRowPoll)
|
|
465
|
+
for (const timer of timers) clearTimeout(timer)
|
|
466
|
+
if (raf !== null) cancelAnimationFrame(raf)
|
|
467
|
+
console.error = originalError
|
|
468
|
+
}
|
|
469
|
+
// All captured values are refs or stable setters; the timeline runs once per mount.
|
|
470
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
471
|
+
}, [])
|
|
472
|
+
|
|
473
|
+
const done = ROW_ORDER.every((id) => rows[id] !== undefined)
|
|
474
|
+
const failing = ROW_ORDER.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
|
|
475
|
+
const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
|
|
476
|
+
const passed = Object.keys(rows).length - failing.length
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<ScrollView style={styles.scroll} contentContainerStyle={styles.container}>
|
|
480
|
+
<Text style={styles.title}>U7a reduced-motion checks</Text>
|
|
481
|
+
<Text style={styles.subtitle}>
|
|
482
|
+
useReducedMotion snapshot + authored {'{ type: false }'} instant lane ({severity} severity)
|
|
483
|
+
</Text>
|
|
484
|
+
<View
|
|
485
|
+
style={[
|
|
486
|
+
styles.banner,
|
|
487
|
+
verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
|
|
488
|
+
]}
|
|
489
|
+
>
|
|
490
|
+
<Text style={styles.bannerText}>{verdict}</Text>
|
|
491
|
+
</View>
|
|
492
|
+
<Text style={styles.tally}>
|
|
493
|
+
{done
|
|
494
|
+
? `${passed} PASS / ${failing.length} FAIL of ${ROW_ORDER.length} rows`
|
|
495
|
+
: `${Object.keys(rows).length}/${ROW_ORDER.length} rows complete — RUNNING`}
|
|
496
|
+
</Text>
|
|
497
|
+
<Text style={styles.tallyDetail}>
|
|
498
|
+
{done
|
|
499
|
+
? failing.length === 0
|
|
500
|
+
? 'no failing rows'
|
|
501
|
+
: `FAILING: ${failing.join(' | ')}`
|
|
502
|
+
: `stage: ${stage}`}
|
|
503
|
+
</Text>
|
|
504
|
+
<Text style={styles.status}>
|
|
505
|
+
stage: {stage} · reduced={String(shouldReduceMotion)} · visible={String(isVisible)} · tween
|
|
506
|
+
opacity={liveTween} · instant opacity={liveInstant} → {instantTarget}
|
|
507
|
+
</Text>
|
|
508
|
+
|
|
509
|
+
{/* The subjects. Left: the pin example verbatim — outer carries the animate label with
|
|
510
|
+
initial={false}, the inner variants-only child resolves it (label propagation) with the
|
|
511
|
+
snapshot-selected transition embedded. Right: the authored { type: false } instant-lane
|
|
512
|
+
box. onUpdate on both is the committed-values readback. */}
|
|
513
|
+
<View style={styles.stageRow}>
|
|
514
|
+
<MotionView animate={isVisible ? 'visible' : 'hidden'} initial={false}>
|
|
515
|
+
<MotionView
|
|
516
|
+
variants={variants}
|
|
517
|
+
onUpdate={(latest) => {
|
|
518
|
+
const value = readOpacity(latest)
|
|
519
|
+
if (value === null) return
|
|
520
|
+
tweenSamplesRef.current.push({ at: Date.now() - mountStampRef.current, value })
|
|
521
|
+
setLiveTween(value.toFixed(3))
|
|
522
|
+
}}
|
|
523
|
+
onAnimationComplete={() => {
|
|
524
|
+
tweenCompletionsRef.current.push(Date.now() - mountStampRef.current)
|
|
525
|
+
}}
|
|
526
|
+
style={styles.tweenBox}
|
|
527
|
+
accessibilityLabel="u7a-tween-subject"
|
|
528
|
+
/>
|
|
529
|
+
</MotionView>
|
|
530
|
+
<MotionView
|
|
531
|
+
animate={{ opacity: instantTarget }}
|
|
532
|
+
initial={false}
|
|
533
|
+
transition={instantTransition}
|
|
534
|
+
onUpdate={(latest) => {
|
|
535
|
+
const value = readOpacity(latest)
|
|
536
|
+
if (value === null) return
|
|
537
|
+
instantLatestRef.current = value
|
|
538
|
+
instantSamplesRef.current.push({ at: Date.now() - mountStampRef.current, value })
|
|
539
|
+
setLiveInstant(value.toFixed(3))
|
|
540
|
+
}}
|
|
541
|
+
style={styles.instantBox}
|
|
542
|
+
accessibilityLabel="u7a-instant-subject"
|
|
543
|
+
/>
|
|
544
|
+
</View>
|
|
545
|
+
|
|
546
|
+
{ROW_ORDER.map((id) => {
|
|
547
|
+
const row = rows[id]
|
|
548
|
+
return (
|
|
549
|
+
<View key={id} style={styles.row}>
|
|
550
|
+
<Text
|
|
551
|
+
style={[
|
|
552
|
+
styles.verdict,
|
|
553
|
+
row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
|
|
554
|
+
]}
|
|
555
|
+
>
|
|
556
|
+
{row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
|
|
557
|
+
</Text>
|
|
558
|
+
<View style={styles.rowBody}>
|
|
559
|
+
<Text style={styles.rowId}>{id}</Text>
|
|
560
|
+
<Text style={styles.rowDetail} numberOfLines={4}>
|
|
561
|
+
{row === undefined ? 'pending' : row.detail}
|
|
562
|
+
</Text>
|
|
563
|
+
</View>
|
|
564
|
+
</View>
|
|
565
|
+
)
|
|
566
|
+
})}
|
|
567
|
+
</ScrollView>
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const styles = StyleSheet.create({
|
|
572
|
+
scroll: { flex: 1, backgroundColor: '#0b0b12' },
|
|
573
|
+
container: { paddingVertical: 72, paddingHorizontal: 8 },
|
|
574
|
+
title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
|
|
575
|
+
subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
|
|
576
|
+
banner: {
|
|
577
|
+
alignSelf: 'center',
|
|
578
|
+
paddingHorizontal: 36,
|
|
579
|
+
paddingVertical: 10,
|
|
580
|
+
borderRadius: 10,
|
|
581
|
+
marginTop: 18,
|
|
582
|
+
marginBottom: 10,
|
|
583
|
+
},
|
|
584
|
+
pass: { backgroundColor: '#1d7a3e' },
|
|
585
|
+
fail: { backgroundColor: '#a32633' },
|
|
586
|
+
running: { backgroundColor: '#7a641d' },
|
|
587
|
+
bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
|
|
588
|
+
tally: { color: '#e6e6f0', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
589
|
+
tallyDetail: { color: '#f87171', fontSize: 11, marginTop: 3, textAlign: 'center' },
|
|
590
|
+
status: {
|
|
591
|
+
color: '#c9c9d8',
|
|
592
|
+
fontSize: 11,
|
|
593
|
+
marginTop: 8,
|
|
594
|
+
textAlign: 'center',
|
|
595
|
+
fontFamily: 'Menlo',
|
|
596
|
+
},
|
|
597
|
+
stageRow: {
|
|
598
|
+
flexDirection: 'row',
|
|
599
|
+
marginTop: 18,
|
|
600
|
+
alignItems: 'center',
|
|
601
|
+
justifyContent: 'space-evenly',
|
|
602
|
+
},
|
|
603
|
+
// The pin example's style verbatim (useReducedMotion.tsx:4-9): 100×100 red, base opacity 1.
|
|
604
|
+
tweenBox: { width: 100, height: 100, backgroundColor: 'red', opacity: 1, borderRadius: 12 },
|
|
605
|
+
instantBox: { width: 100, height: 100, backgroundColor: '#3b82f6', borderRadius: 12 },
|
|
606
|
+
row: { flexDirection: 'row', marginTop: 10 },
|
|
607
|
+
verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
|
|
608
|
+
ok: { color: '#4ade80' },
|
|
609
|
+
bad: { color: '#f87171' },
|
|
610
|
+
pending: { color: '#8a8aa0' },
|
|
611
|
+
rowBody: { flex: 1 },
|
|
612
|
+
rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
|
|
613
|
+
rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
|
|
614
|
+
})
|