@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,429 @@
|
|
|
1
|
+
// RN-free engine for the layout-identity worklet probe: the plain-object probe graph, the fixed
|
|
2
|
+
// two-holder registry trajectory, and the live-leg state machine. Extracted from
|
|
3
|
+
// LayoutIdentityWorkletProbe.tsx so the reporting semantics are unit-testable off-device (vitest
|
|
4
|
+
// runs this on the JS thread, where the 'worklet' directives are inert); the screen keeps the
|
|
5
|
+
// React/Reanimated wiring. Function-level directives on app source are workletized per-function by
|
|
6
|
+
// the worklets babel plugin and importable into worklets cross-module — the same pattern as
|
|
7
|
+
// motion/colorProjection.ts (device-proven by the L1/L4 rungs).
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
createLayoutIdentityRegistry,
|
|
11
|
+
type ActivityHandle,
|
|
12
|
+
type Animation,
|
|
13
|
+
type Generator,
|
|
14
|
+
type GeneratorFactory,
|
|
15
|
+
type LayoutIdentityRegistry,
|
|
16
|
+
type MotionGraph,
|
|
17
|
+
type MotionValue,
|
|
18
|
+
type MotionValueEvent,
|
|
19
|
+
type ValueChangeListener,
|
|
20
|
+
type WindowSpaceRect,
|
|
21
|
+
MAX_VELOCITY_DELTA_MS,
|
|
22
|
+
} from '@unrulysystems/native-motion-core/worklet-layout'
|
|
23
|
+
|
|
24
|
+
const ID = 'probe-card'
|
|
25
|
+
const FROM_TOKEN = 'from-holder'
|
|
26
|
+
const TO_TOKEN = 'to-holder'
|
|
27
|
+
export const STEPS_PER_LEG = 600
|
|
28
|
+
export const LIVE_MAX_FRAMES = STEPS_PER_LEG * 2
|
|
29
|
+
export const DT_MS = 16.67
|
|
30
|
+
const SPRING = { stiffness: 550, damping: 45 } as const
|
|
31
|
+
const FROM_RECT = { x: 24, y: 420, width: 112, height: 72 } as WindowSpaceRect
|
|
32
|
+
const TO_RECT = { x: 176, y: 96, width: 168, height: 132 } as WindowSpaceRect
|
|
33
|
+
|
|
34
|
+
interface ProbeAnimation extends Animation {
|
|
35
|
+
done: boolean
|
|
36
|
+
/**
|
|
37
|
+
* Reentrancy latch for the interruption window (r21): this graph's count SCANS `done`, so
|
|
38
|
+
* the exactly-once guard cannot flip it before the cancel notification — the listener must
|
|
39
|
+
* still observe a live count. Core's markFinished maps to this latch here.
|
|
40
|
+
*/
|
|
41
|
+
__latched?: boolean
|
|
42
|
+
/**
|
|
43
|
+
* Ledger/ownership teardown with or without the end-of-animation velocity cleanup (r19):
|
|
44
|
+
* interruption carries velocity into the successor seed, so it finishes WITHOUT cleanup;
|
|
45
|
+
* a direct stop() cleans up (r17).
|
|
46
|
+
*/
|
|
47
|
+
__finish(runEndCleanup: boolean): void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ProbeMotionValue extends MotionValue<number> {
|
|
51
|
+
__active: ProbeAnimation | null
|
|
52
|
+
__applyAnimationSample(value: number, velocity: number): void
|
|
53
|
+
__endAnimation(): void
|
|
54
|
+
/** Fire a lifecycle event on this value (r15 e18b6f40a2d7 — the union is EMITTED, not just accepted). */
|
|
55
|
+
__emit(event: 'animationStart' | 'animationComplete' | 'animationCancel' | 'destroy'): void
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ProbeActiveAnimation {
|
|
59
|
+
readonly value: ProbeMotionValue
|
|
60
|
+
readonly generator: Generator
|
|
61
|
+
readonly handle: ProbeAnimation
|
|
62
|
+
elapsedMs: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ProbeGraph extends MotionGraph {
|
|
66
|
+
frame(dtMs: number): void
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createProbeMotionValue(initial: number, now: () => number): ProbeMotionValue {
|
|
70
|
+
'worklet'
|
|
71
|
+
let value = initial
|
|
72
|
+
// Core MotionValueImpl's EXACT sample-window model (r20 26d0a09db7f4 — a manual-write
|
|
73
|
+
// timestamp alone missed same-frame compounding, identical-write suppression, and the
|
|
74
|
+
// clock velocity of listener writes): the animated velocity takes precedence while a
|
|
75
|
+
// flight drives the value; manual writes roll the frame window only when time moved.
|
|
76
|
+
let animatedVelocity: number | null = null
|
|
77
|
+
let prev: number | null = null
|
|
78
|
+
let prevUpdatedAtMs: number | null = null
|
|
79
|
+
let updatedAtMs = now()
|
|
80
|
+
const listeners: Array<ValueChangeListener<number>> = []
|
|
81
|
+
// 'change' listeners take the latest value (the pinned payload); lifecycle listeners take
|
|
82
|
+
// none — one storage shape, lifecycle emission casts to the payload-free call.
|
|
83
|
+
const eventListeners: Record<MotionValueEvent, Array<(latest: number) => void>> = {
|
|
84
|
+
change: [],
|
|
85
|
+
animationStart: [],
|
|
86
|
+
animationComplete: [],
|
|
87
|
+
animationCancel: [],
|
|
88
|
+
destroy: [],
|
|
89
|
+
}
|
|
90
|
+
const mv: ProbeMotionValue = {
|
|
91
|
+
__active: null,
|
|
92
|
+
__applyAnimationSample(next, nextVelocity) {
|
|
93
|
+
value = next
|
|
94
|
+
animatedVelocity = nextVelocity
|
|
95
|
+
for (const listener of listeners) listener(value)
|
|
96
|
+
for (const listener of eventListeners.change) listener(value)
|
|
97
|
+
},
|
|
98
|
+
__endAnimation() {
|
|
99
|
+
// Core MotionValueImpl.endAnimation() verbatim (r20): clears the animated velocity AND
|
|
100
|
+
// resets the clock window — the registry's EPSILON_VEL settle floor clears, and no
|
|
101
|
+
// stale manual sample can synthesize a velocity against the flight's final value.
|
|
102
|
+
animatedVelocity = null
|
|
103
|
+
prev = null
|
|
104
|
+
prevUpdatedAtMs = null
|
|
105
|
+
updatedAtMs = now()
|
|
106
|
+
},
|
|
107
|
+
get: () => value,
|
|
108
|
+
set(next) {
|
|
109
|
+
// Core set() verbatim (r20): window rolls only when time moved; identical writes
|
|
110
|
+
// do not notify.
|
|
111
|
+
animatedVelocity = null
|
|
112
|
+
const nowMs = now()
|
|
113
|
+
if (nowMs !== updatedAtMs) {
|
|
114
|
+
prev = value
|
|
115
|
+
prevUpdatedAtMs = updatedAtMs
|
|
116
|
+
updatedAtMs = nowMs
|
|
117
|
+
}
|
|
118
|
+
const previous = value
|
|
119
|
+
value = next
|
|
120
|
+
if (value !== previous) {
|
|
121
|
+
for (const listener of listeners) listener(value)
|
|
122
|
+
for (const listener of eventListeners.change) listener(value)
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
jump(next) {
|
|
126
|
+
// Core jump() VERBATIM (r16/r17/r20): set (window semantics + identical-write
|
|
127
|
+
// suppression + clock velocity for the change listener) -> window reset -> stop whose
|
|
128
|
+
// cancellation reads 0 with the animation still observable.
|
|
129
|
+
mv.set(next)
|
|
130
|
+
prev = null
|
|
131
|
+
prevUpdatedAtMs = null
|
|
132
|
+
updatedAtMs = now()
|
|
133
|
+
mv.stop()
|
|
134
|
+
},
|
|
135
|
+
subscribe(listener) {
|
|
136
|
+
listeners.push(listener)
|
|
137
|
+
return () => {
|
|
138
|
+
const at = listeners.indexOf(listener)
|
|
139
|
+
if (at >= 0) listeners.splice(at, 1)
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
getVelocity: () => {
|
|
143
|
+
// Core getVelocity() verbatim: the animated velocity wins while a flight drives the
|
|
144
|
+
// value; otherwise the clock formula over the manual-write window, stale -> 0.
|
|
145
|
+
if (animatedVelocity !== null) return animatedVelocity
|
|
146
|
+
if (prev === null || prevUpdatedAtMs === null) return 0
|
|
147
|
+
if (now() - updatedAtMs > MAX_VELOCITY_DELTA_MS) return 0
|
|
148
|
+
const dtMs = Math.min(updatedAtMs - prevUpdatedAtMs, MAX_VELOCITY_DELTA_MS)
|
|
149
|
+
if (dtMs <= 0) return 0
|
|
150
|
+
return ((value - prev) * 1000) / dtMs
|
|
151
|
+
},
|
|
152
|
+
on(event, listener) {
|
|
153
|
+
eventListeners[event].push(listener)
|
|
154
|
+
return () => {
|
|
155
|
+
const at = eventListeners[event].indexOf(listener)
|
|
156
|
+
if (at >= 0) eventListeners[event].splice(at, 1)
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
// Liveness IS the __active handle (set at animate, nulled at stop and natural completion).
|
|
160
|
+
isAnimating: () => mv.__active !== null,
|
|
161
|
+
// Lifecycle emission (r15 e18b6f40a2d7): mirrors uiLayoutGraph / core parity.
|
|
162
|
+
__emit(event) {
|
|
163
|
+
for (const listener of eventListeners[event]) (listener as () => void)()
|
|
164
|
+
},
|
|
165
|
+
stop() {
|
|
166
|
+
if (mv.__active !== null) {
|
|
167
|
+
// Pinned ORDER (r17): the cancellation is notified while the animation is still
|
|
168
|
+
// observable; the handle's ledger/liveness/velocity cleanup runs after.
|
|
169
|
+
mv.__emit('animationCancel')
|
|
170
|
+
mv.__active.stop()
|
|
171
|
+
mv.__active = null
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
destroy() {
|
|
175
|
+
// Pinned teardown ORDER (r16 major 1): destroy announces FIRST (the listener observes
|
|
176
|
+
// the live animation), then the SILENT stop — never an animationCancel from teardown.
|
|
177
|
+
mv.__emit('destroy')
|
|
178
|
+
if (mv.__active !== null) {
|
|
179
|
+
mv.__active.stop()
|
|
180
|
+
mv.__active = null
|
|
181
|
+
}
|
|
182
|
+
listeners.length = 0
|
|
183
|
+
eventListeners.change.length = 0
|
|
184
|
+
eventListeners.animationStart.length = 0
|
|
185
|
+
eventListeners.animationComplete.length = 0
|
|
186
|
+
eventListeners.animationCancel.length = 0
|
|
187
|
+
eventListeners.destroy.length = 0
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
return mv
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function createProbeGraph(): ProbeGraph {
|
|
194
|
+
'worklet'
|
|
195
|
+
const animations: ProbeActiveAnimation[] = []
|
|
196
|
+
let openActivities = 0
|
|
197
|
+
// The graph's frame clock (r19): raw frame time, mirroring uiLayoutGraph.
|
|
198
|
+
let nowMs = 0
|
|
199
|
+
|
|
200
|
+
function activeAnimationCount(): number {
|
|
201
|
+
'worklet'
|
|
202
|
+
let count = 0
|
|
203
|
+
for (const animation of animations) if (!animation.handle.done) count += 1
|
|
204
|
+
return count
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const graph: ProbeGraph = {
|
|
208
|
+
motionValue<T = number>(initial: T): MotionValue<T> {
|
|
209
|
+
return createProbeMotionValue(initial as number, () => nowMs) as unknown as MotionValue<T>
|
|
210
|
+
},
|
|
211
|
+
derived() {
|
|
212
|
+
throw new Error(
|
|
213
|
+
'LayoutIdentityWorkletProbe: derived values are not used by the layout session crossing.',
|
|
214
|
+
)
|
|
215
|
+
},
|
|
216
|
+
animate(value: MotionValue<number>, factory: GeneratorFactory): Animation {
|
|
217
|
+
const motionValue = value as ProbeMotionValue
|
|
218
|
+
// Interruption ORDER (r19/r20, core-graph parity): the outgoing handle LATCHES first
|
|
219
|
+
// (core's markFinished-before-notify — a reentrant listener stop() hits the
|
|
220
|
+
// exactly-once latch, never a null dereference), the cancellation is notified while
|
|
221
|
+
// liveness is still observable (a valid listener may write the value), ownership
|
|
222
|
+
// clears, and only THEN does the successor seed read the CURRENT state
|
|
223
|
+
// (REQ-CORE-002/REQ-LAYOUT-018 continuity — including the listener write's clock
|
|
224
|
+
// velocity, which core's set() law now computes here too).
|
|
225
|
+
const outgoing = motionValue.__active
|
|
226
|
+
if (outgoing !== null && !outgoing.done) {
|
|
227
|
+
// Core's markFinished shape (r21): the reentrancy LATCH flips before the
|
|
228
|
+
// notification while the scan-visible `done` stays false — the listener observes a
|
|
229
|
+
// live count/liveness. done + ownership clear AFTER, in a finally (r22): a throwing
|
|
230
|
+
// listener must never leave a latched-but-never-done handle wedging the scan count.
|
|
231
|
+
outgoing.__latched = true
|
|
232
|
+
try {
|
|
233
|
+
motionValue.__emit('animationCancel')
|
|
234
|
+
} finally {
|
|
235
|
+
outgoing.done = true
|
|
236
|
+
if (motionValue.__active === outgoing) motionValue.__active = null
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const generator = factory({ from: motionValue.get(), velocity: motionValue.getVelocity() })
|
|
240
|
+
const handle: ProbeAnimation = {
|
|
241
|
+
done: false,
|
|
242
|
+
__finish(runEndCleanup) {
|
|
243
|
+
if (handle.done || handle.__latched === true) return
|
|
244
|
+
handle.done = true
|
|
245
|
+
if (motionValue.__active === handle) {
|
|
246
|
+
motionValue.__active = null
|
|
247
|
+
if (runEndCleanup) motionValue.__endAnimation()
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
stop() {
|
|
251
|
+
handle.__finish(true)
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
motionValue.__active = handle
|
|
255
|
+
animations.push({ value: motionValue, generator, handle, elapsedMs: 0 })
|
|
256
|
+
// Core parity (graph.animate): the start event fires after the stopper is installed.
|
|
257
|
+
motionValue.__emit('animationStart')
|
|
258
|
+
return handle
|
|
259
|
+
},
|
|
260
|
+
activeAnimationCount,
|
|
261
|
+
registerActivity(): ActivityHandle {
|
|
262
|
+
openActivities += 1
|
|
263
|
+
let released = false
|
|
264
|
+
return {
|
|
265
|
+
release() {
|
|
266
|
+
if (released) return
|
|
267
|
+
released = true
|
|
268
|
+
openActivities -= 1
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
isSettled: () => activeAnimationCount() === 0 && openActivities === 0,
|
|
273
|
+
whenSettled() {
|
|
274
|
+
throw new Error(
|
|
275
|
+
'LayoutIdentityWorkletProbe: whenSettled is not used by the synchronous probe graph.',
|
|
276
|
+
)
|
|
277
|
+
},
|
|
278
|
+
frame(dtMs) {
|
|
279
|
+
nowMs += dtMs
|
|
280
|
+
const completed: ProbeActiveAnimation[] = []
|
|
281
|
+
for (const animation of animations) {
|
|
282
|
+
if (animation.handle.done) continue
|
|
283
|
+
animation.elapsedMs += dtMs
|
|
284
|
+
const sample = animation.generator.sample(animation.elapsedMs)
|
|
285
|
+
animation.value.__applyAnimationSample(sample.value, sample.velocity)
|
|
286
|
+
if (sample.done) completed.push(animation)
|
|
287
|
+
}
|
|
288
|
+
for (const animation of completed) {
|
|
289
|
+
animation.handle.done = true
|
|
290
|
+
if (animation.value.__active === animation.handle) {
|
|
291
|
+
animation.value.__active = null
|
|
292
|
+
animation.value.__endAnimation()
|
|
293
|
+
animation.value.__emit('animationComplete')
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
return graph
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function readTrajectorySample(registry: LayoutIdentityRegistry): readonly number[] {
|
|
302
|
+
'worklet'
|
|
303
|
+
const from = registry.read(ID, FROM_TOKEN)
|
|
304
|
+
const to = registry.read(ID, TO_TOKEN)
|
|
305
|
+
return [
|
|
306
|
+
from.transform.translateX,
|
|
307
|
+
from.transform.translateY,
|
|
308
|
+
from.transform.scaleX,
|
|
309
|
+
from.transform.scaleY,
|
|
310
|
+
from.opacity,
|
|
311
|
+
to.transform.translateX,
|
|
312
|
+
to.transform.translateY,
|
|
313
|
+
to.transform.scaleX,
|
|
314
|
+
to.transform.scaleY,
|
|
315
|
+
to.opacity,
|
|
316
|
+
registry.zLifted(ID, FROM_TOKEN) ? 1 : 0,
|
|
317
|
+
registry.zLifted(ID, TO_TOKEN) ? 1 : 0,
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function beginProbeRegistry(): {
|
|
322
|
+
readonly graph: ProbeGraph
|
|
323
|
+
readonly registry: LayoutIdentityRegistry
|
|
324
|
+
} {
|
|
325
|
+
'worklet'
|
|
326
|
+
const graph = createProbeGraph()
|
|
327
|
+
const registry = createLayoutIdentityRegistry({ graph, spring: SPRING })
|
|
328
|
+
registry.enter(ID, FROM_TOKEN, FROM_RECT)
|
|
329
|
+
registry.enter(ID, TO_TOKEN, TO_RECT)
|
|
330
|
+
return { graph, registry }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function runLayoutIdentityTrajectory(): {
|
|
334
|
+
readonly values: readonly number[]
|
|
335
|
+
readonly enterSettled: boolean
|
|
336
|
+
readonly exitReleased: boolean
|
|
337
|
+
readonly activeAnimations: number
|
|
338
|
+
} {
|
|
339
|
+
'worklet'
|
|
340
|
+
const { graph, registry } = beginProbeRegistry()
|
|
341
|
+
const values: number[] = []
|
|
342
|
+
for (let i = 0; i < STEPS_PER_LEG; i++) {
|
|
343
|
+
graph.frame(DT_MS)
|
|
344
|
+
values.push(...readTrajectorySample(registry))
|
|
345
|
+
}
|
|
346
|
+
const enterSettled = registry.isSettled(ID)
|
|
347
|
+
registry.leave(ID, TO_TOKEN)
|
|
348
|
+
const releasedBeforeExitSettles = registry.releasable(ID, TO_TOKEN)
|
|
349
|
+
for (let i = 0; i < STEPS_PER_LEG; i++) {
|
|
350
|
+
graph.frame(DT_MS)
|
|
351
|
+
values.push(...readTrajectorySample(registry))
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
values,
|
|
355
|
+
enterSettled,
|
|
356
|
+
exitReleased: !releasedBeforeExitSettles && registry.releasable(ID, TO_TOKEN),
|
|
357
|
+
activeAnimations: graph.activeAnimationCount(),
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
interface LiveProbeState {
|
|
362
|
+
readonly graph: ProbeGraph
|
|
363
|
+
readonly registry: LayoutIdentityRegistry
|
|
364
|
+
frames: number
|
|
365
|
+
done: boolean
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
interface LiveProbeGlobal {
|
|
369
|
+
__nativeMotionLayoutIdentityProbe?: LiveProbeState
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function resetLiveProbe(): void {
|
|
373
|
+
'worklet'
|
|
374
|
+
const { graph, registry } = beginProbeRegistry()
|
|
375
|
+
;(globalThis as LiveProbeGlobal).__nativeMotionLayoutIdentityProbe = {
|
|
376
|
+
graph,
|
|
377
|
+
registry,
|
|
378
|
+
frames: 0,
|
|
379
|
+
done: false,
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export interface LiveProbeStepResult {
|
|
384
|
+
// True exactly when this frame carries the run's verdict; the caller schedules the RN-side
|
|
385
|
+
// report only on report=true frames.
|
|
386
|
+
readonly report: boolean
|
|
387
|
+
readonly pass: boolean
|
|
388
|
+
readonly detail: string
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function stepLiveProbe(dtMs: number): LiveProbeStepResult {
|
|
392
|
+
'worklet'
|
|
393
|
+
const state = (globalThis as LiveProbeGlobal).__nativeMotionLayoutIdentityProbe
|
|
394
|
+
if (state === undefined) {
|
|
395
|
+
return { report: true, pass: false, detail: 'live probe was not initialized on the UI runtime' }
|
|
396
|
+
}
|
|
397
|
+
if (state.done) {
|
|
398
|
+
// The verdict frame already reported; the callback stays active until the RN thread
|
|
399
|
+
// deactivates it, so frames keep arriving. Never re-report here — a pass:true "already
|
|
400
|
+
// settled" from this branch used to overwrite a real timeout verdict on the RN side
|
|
401
|
+
// (review-found fail-open, 2026-07-09).
|
|
402
|
+
return {
|
|
403
|
+
report: false,
|
|
404
|
+
pass: false,
|
|
405
|
+
detail: `verdict already reported after ${state.frames} frames`,
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
state.graph.frame(dtMs)
|
|
409
|
+
state.frames += 1
|
|
410
|
+
if (state.registry.isSettled(ID)) {
|
|
411
|
+
state.done = true
|
|
412
|
+
return {
|
|
413
|
+
report: true,
|
|
414
|
+
pass: true,
|
|
415
|
+
detail: `useFrameCallback reached layout-identity settle after ${state.frames} UI frames`,
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (state.frames >= LIVE_MAX_FRAMES) {
|
|
419
|
+
state.done = true
|
|
420
|
+
return {
|
|
421
|
+
report: true,
|
|
422
|
+
pass: false,
|
|
423
|
+
detail: `useFrameCallback did not settle within ${LIVE_MAX_FRAMES} UI frames`,
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// pass is meaningful only on report=true frames; keep it false everywhere else so a caller
|
|
427
|
+
// that mistakenly reads it anyway fails closed.
|
|
428
|
+
return { report: false, pass: false, detail: `stepping frame ${state.frames}` }
|
|
429
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// U8 arc() device checks: catalog transition-arc-playground + spring playground on the
|
|
2
|
+
// public MotionView path. iOS has no tap injection, so the screen self-drives.
|
|
3
|
+
// Discriminators (Decision 94 analog of 77/82/86/90): native is the parametric channel
|
|
4
|
+
// (committed x/y/pathRotation via onUpdate), not host paint. Tween y uses peak-over-flight
|
|
5
|
+
// like the web e2e data-peaky — a wall-clock mid sample races the 800ms linear tween.
|
|
6
|
+
// Host paint is the web e2e. layout.path is not this unit.
|
|
7
|
+
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
9
|
+
import { StyleSheet, Text, View } from 'react-native'
|
|
10
|
+
import { arc } from '@unrulysystems/native-motion-core'
|
|
11
|
+
import { MotionRoot } from '../../native/motion/MotionRoot'
|
|
12
|
+
import { MotionView } from '../../native/motion/MotionView'
|
|
13
|
+
import { ambientNativeSeverity } from '../../native/motion/severity'
|
|
14
|
+
import { armProofConsoleTap } from './proofConsoleTap'
|
|
15
|
+
|
|
16
|
+
const ROW_TWEEN = 'tween arc: y deflects mid-chord then settles'
|
|
17
|
+
const ROW_SPRING = 'spring arc: bounce overshoots the 400px target'
|
|
18
|
+
const ROW_ROTATE = 'rotate: pathRotation is non-zero mid-flight'
|
|
19
|
+
const ROW_CLEAN = 'clean run: zero unexpected reports'
|
|
20
|
+
const ROWS = [ROW_TWEEN, ROW_SPRING, ROW_ROTATE, ROW_CLEAN] as const
|
|
21
|
+
|
|
22
|
+
const T_TWEEN_END = 1100
|
|
23
|
+
const T_SPRING_END = 2200
|
|
24
|
+
const T_ROTATE_MID = 2450
|
|
25
|
+
const T_CLEAN = 3200
|
|
26
|
+
|
|
27
|
+
interface RowResult {
|
|
28
|
+
readonly pass: boolean
|
|
29
|
+
readonly detail: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Sample {
|
|
33
|
+
x: number
|
|
34
|
+
y: number
|
|
35
|
+
pathRotation: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ArcPathChecksScreen() {
|
|
39
|
+
const severity = ambientNativeSeverity()
|
|
40
|
+
const tweenPath = useMemo(() => arc({ strength: 1, peak: 0.5 }), [])
|
|
41
|
+
const springPath = useMemo(() => arc({ strength: 1, peak: 0.5 }), [])
|
|
42
|
+
const rotatePath = useMemo(() => arc({ strength: 1, peak: 0.5, rotate: true }), [])
|
|
43
|
+
const [tweenTarget, setTweenTarget] = useState<'a' | 'b'>('a')
|
|
44
|
+
const [springTarget, setSpringTarget] = useState<'a' | 'b'>('a')
|
|
45
|
+
const [rotateTarget, setRotateTarget] = useState<'a' | 'b'>('a')
|
|
46
|
+
const [stage, setStage] = useState('boot')
|
|
47
|
+
const [rows, setRows] = useState<Partial<Record<(typeof ROWS)[number], RowResult>>>({})
|
|
48
|
+
const tweenRef = useRef<Sample>({ x: 0, y: 0, pathRotation: 0 })
|
|
49
|
+
const springRef = useRef<Sample>({ x: 0, y: 0, pathRotation: 0 })
|
|
50
|
+
const rotateRef = useRef<Sample>({ x: 0, y: 0, pathRotation: 0 })
|
|
51
|
+
const tweenPeakY = useRef(0)
|
|
52
|
+
const springMaxX = useRef(0)
|
|
53
|
+
const rotatePeak = useRef(0)
|
|
54
|
+
const reportsRef = useRef<string[]>([])
|
|
55
|
+
const restoreTapRef = useRef<null | (() => void)>(null)
|
|
56
|
+
if (restoreTapRef.current === null) {
|
|
57
|
+
restoreTapRef.current = armProofConsoleTap(reportsRef.current, (message) =>
|
|
58
|
+
/motion|arc|path|InvalidTransition/i.test(message),
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const finalize = (id: (typeof ROWS)[number], pass: boolean, detail: string): void => {
|
|
63
|
+
setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const onTween = useCallback((latest: Readonly<Record<string, string | number>>): void => {
|
|
67
|
+
const x = typeof latest.x === 'number' ? latest.x : tweenRef.current.x
|
|
68
|
+
const y = typeof latest.y === 'number' ? latest.y : tweenRef.current.y
|
|
69
|
+
tweenRef.current = { x, y, pathRotation: 0 }
|
|
70
|
+
if (Math.abs(y) > Math.abs(tweenPeakY.current)) tweenPeakY.current = y
|
|
71
|
+
}, [])
|
|
72
|
+
const onSpring = useCallback((latest: Readonly<Record<string, string | number>>): void => {
|
|
73
|
+
const x = typeof latest.x === 'number' ? latest.x : springRef.current.x
|
|
74
|
+
springRef.current = { ...springRef.current, x }
|
|
75
|
+
if (x > springMaxX.current) springMaxX.current = x
|
|
76
|
+
}, [])
|
|
77
|
+
const onRotate = useCallback((latest: Readonly<Record<string, string | number>>): void => {
|
|
78
|
+
const rot = typeof latest.pathRotation === 'number' ? latest.pathRotation : 0
|
|
79
|
+
rotateRef.current = { ...rotateRef.current, pathRotation: rot }
|
|
80
|
+
if (Math.abs(rot) > Math.abs(rotatePeak.current)) rotatePeak.current = rot
|
|
81
|
+
}, [])
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const timers: ReturnType<typeof setTimeout>[] = []
|
|
85
|
+
const at = (ms: number, run: () => void): void => {
|
|
86
|
+
timers.push(setTimeout(run, ms))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
at(80, () => {
|
|
90
|
+
setTweenTarget('b')
|
|
91
|
+
setStage('tween started')
|
|
92
|
+
})
|
|
93
|
+
at(T_TWEEN_END, () => {
|
|
94
|
+
// Peak y, not a wall-clock mid sample: Android at f1f6c601 captured t≈1.04
|
|
95
|
+
// (x=417.6 y=-36.8) when the 500ms timer fired after the 800ms linear tween
|
|
96
|
+
// had already passed the chord. Same discriminator as the web e2e data-peaky.
|
|
97
|
+
const end = tweenRef.current
|
|
98
|
+
const peakY = tweenPeakY.current
|
|
99
|
+
const peakPass = Math.abs(peakY) > 80
|
|
100
|
+
const endPass = end.x > 380 && Math.abs(end.y) < 25
|
|
101
|
+
const pass = peakPass && endPass
|
|
102
|
+
finalize(
|
|
103
|
+
ROW_TWEEN,
|
|
104
|
+
pass,
|
|
105
|
+
`peak y=${peakY.toFixed(0)}; end x=${end.x.toFixed(0)} y=${end.y.toFixed(0)}`,
|
|
106
|
+
)
|
|
107
|
+
setSpringTarget('b')
|
|
108
|
+
setStage(pass ? 'tween pass; spring started' : 'tween FAIL; spring started')
|
|
109
|
+
})
|
|
110
|
+
at(T_SPRING_END, () => {
|
|
111
|
+
const pass = springMaxX.current > 410
|
|
112
|
+
finalize(
|
|
113
|
+
ROW_SPRING,
|
|
114
|
+
pass,
|
|
115
|
+
pass
|
|
116
|
+
? `maxX=${springMaxX.current.toFixed(0)}`
|
|
117
|
+
: `maxX=${springMaxX.current.toFixed(0)} — expected >410 overshoot`,
|
|
118
|
+
)
|
|
119
|
+
setRotateTarget('b')
|
|
120
|
+
setStage(pass ? 'spring pass; rotate started' : 'spring FAIL; rotate started')
|
|
121
|
+
})
|
|
122
|
+
at(T_ROTATE_MID, () => {
|
|
123
|
+
const pass = Math.abs(rotatePeak.current) > 5
|
|
124
|
+
finalize(
|
|
125
|
+
ROW_ROTATE,
|
|
126
|
+
pass,
|
|
127
|
+
pass
|
|
128
|
+
? `peak pathRotation=${rotatePeak.current.toFixed(1)}`
|
|
129
|
+
: `peak pathRotation=${rotatePeak.current.toFixed(1)} — expected non-zero`,
|
|
130
|
+
)
|
|
131
|
+
setStage(pass ? 'rotate pass' : 'rotate FAIL')
|
|
132
|
+
})
|
|
133
|
+
at(T_CLEAN, () => {
|
|
134
|
+
const pass = reportsRef.current.length === 0
|
|
135
|
+
finalize(
|
|
136
|
+
ROW_CLEAN,
|
|
137
|
+
pass,
|
|
138
|
+
pass
|
|
139
|
+
? '0 unexpected reports'
|
|
140
|
+
: `${reportsRef.current.length} unexpected: ${reportsRef.current
|
|
141
|
+
.map((message) => message.slice(0, 80))
|
|
142
|
+
.join(' | ')}`,
|
|
143
|
+
)
|
|
144
|
+
setStage('done')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
return () => {
|
|
148
|
+
for (const timer of timers) clearTimeout(timer)
|
|
149
|
+
restoreTapRef.current?.()
|
|
150
|
+
restoreTapRef.current = null
|
|
151
|
+
}
|
|
152
|
+
}, [])
|
|
153
|
+
|
|
154
|
+
const done = ROWS.every((id) => rows[id] !== undefined)
|
|
155
|
+
const failing = ROWS.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
|
|
156
|
+
const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
|
|
157
|
+
const passed = Object.keys(rows).length - failing.length
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<MotionRoot>
|
|
161
|
+
<View style={styles.scroll}>
|
|
162
|
+
<View style={styles.container}>
|
|
163
|
+
<Text style={styles.title}>U8 arc path checks</Text>
|
|
164
|
+
<Text style={styles.subtitle}>
|
|
165
|
+
catalog tween + spring playgrounds ({severity} severity)
|
|
166
|
+
</Text>
|
|
167
|
+
<View
|
|
168
|
+
style={[
|
|
169
|
+
styles.banner,
|
|
170
|
+
verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
|
|
171
|
+
]}
|
|
172
|
+
>
|
|
173
|
+
<Text style={styles.bannerText}>{verdict}</Text>
|
|
174
|
+
</View>
|
|
175
|
+
<Text style={styles.tally}>
|
|
176
|
+
{done
|
|
177
|
+
? `${passed} PASS / ${failing.length} FAIL of ${ROWS.length} rows`
|
|
178
|
+
: `${Object.keys(rows).length}/${ROWS.length} rows complete — RUNNING`}
|
|
179
|
+
</Text>
|
|
180
|
+
<Text style={styles.tallyDetail}>
|
|
181
|
+
{done
|
|
182
|
+
? failing.length === 0
|
|
183
|
+
? 'no failing rows'
|
|
184
|
+
: `FAILING: ${failing.join(' | ')}`
|
|
185
|
+
: `stage: ${stage}`}
|
|
186
|
+
</Text>
|
|
187
|
+
|
|
188
|
+
<MotionView
|
|
189
|
+
animate={tweenTarget === 'a' ? { x: 0, y: 0 } : { x: 400, y: 0 }}
|
|
190
|
+
transition={{ duration: 0.8, ease: 'linear', path: tweenPath }}
|
|
191
|
+
onUpdate={onTween}
|
|
192
|
+
style={styles.box}
|
|
193
|
+
accessibilityLabel="u8-arc-tween-box"
|
|
194
|
+
/>
|
|
195
|
+
<MotionView
|
|
196
|
+
animate={springTarget === 'a' ? { x: 0, y: 0 } : { x: 400, y: 0 }}
|
|
197
|
+
transition={{ type: 'spring', bounce: 0.6, visualDuration: 0.45, path: springPath }}
|
|
198
|
+
onUpdate={onSpring}
|
|
199
|
+
style={[styles.box, styles.boxSpring]}
|
|
200
|
+
accessibilityLabel="u8-arc-spring-box"
|
|
201
|
+
/>
|
|
202
|
+
<MotionView
|
|
203
|
+
animate={rotateTarget === 'a' ? { x: 0, y: 0 } : { x: 400, y: 0 }}
|
|
204
|
+
transition={{ duration: 0.8, ease: 'linear', path: rotatePath }}
|
|
205
|
+
onUpdate={onRotate}
|
|
206
|
+
style={[styles.box, styles.boxRotate]}
|
|
207
|
+
accessibilityLabel="u8-arc-rotate-box"
|
|
208
|
+
/>
|
|
209
|
+
|
|
210
|
+
{ROWS.map((id) => {
|
|
211
|
+
const row = rows[id]
|
|
212
|
+
return (
|
|
213
|
+
<View key={id} style={styles.row}>
|
|
214
|
+
<Text
|
|
215
|
+
style={[
|
|
216
|
+
styles.verdict,
|
|
217
|
+
row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
|
|
218
|
+
]}
|
|
219
|
+
>
|
|
220
|
+
{row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
|
|
221
|
+
</Text>
|
|
222
|
+
<View style={styles.rowBody}>
|
|
223
|
+
<Text style={styles.rowId}>{id}</Text>
|
|
224
|
+
<Text style={styles.rowDetail} numberOfLines={3}>
|
|
225
|
+
{row === undefined ? 'pending' : row.detail}
|
|
226
|
+
</Text>
|
|
227
|
+
</View>
|
|
228
|
+
</View>
|
|
229
|
+
)
|
|
230
|
+
})}
|
|
231
|
+
</View>
|
|
232
|
+
</View>
|
|
233
|
+
</MotionRoot>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const styles = StyleSheet.create({
|
|
238
|
+
scroll: { flex: 1, backgroundColor: '#0b0b12' },
|
|
239
|
+
container: { paddingVertical: 72, paddingHorizontal: 8 },
|
|
240
|
+
title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
|
|
241
|
+
subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
|
|
242
|
+
banner: {
|
|
243
|
+
alignSelf: 'center',
|
|
244
|
+
paddingHorizontal: 36,
|
|
245
|
+
paddingVertical: 10,
|
|
246
|
+
borderRadius: 10,
|
|
247
|
+
marginTop: 18,
|
|
248
|
+
marginBottom: 10,
|
|
249
|
+
},
|
|
250
|
+
pass: { backgroundColor: '#1d7a3e' },
|
|
251
|
+
fail: { backgroundColor: '#a32633' },
|
|
252
|
+
running: { backgroundColor: '#7a641d' },
|
|
253
|
+
bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
|
|
254
|
+
tally: { color: '#e6e6f0', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
255
|
+
tallyDetail: { color: '#f87171', fontSize: 11, marginTop: 3, textAlign: 'center' },
|
|
256
|
+
box: {
|
|
257
|
+
width: 40,
|
|
258
|
+
height: 40,
|
|
259
|
+
marginTop: 12,
|
|
260
|
+
backgroundColor: '#ef4444',
|
|
261
|
+
borderRadius: 8,
|
|
262
|
+
},
|
|
263
|
+
boxSpring: { backgroundColor: '#f97316' },
|
|
264
|
+
boxRotate: { backgroundColor: '#eab308' },
|
|
265
|
+
row: { flexDirection: 'row', marginTop: 10 },
|
|
266
|
+
verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
|
|
267
|
+
ok: { color: '#4ade80' },
|
|
268
|
+
bad: { color: '#f87171' },
|
|
269
|
+
pending: { color: '#8a8aa0' },
|
|
270
|
+
rowBody: { flex: 1 },
|
|
271
|
+
rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
|
|
272
|
+
rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
|
|
273
|
+
})
|