@unrulysystems/native-motion 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +55 -0
  4. package/android/build.gradle +24 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/com/unrulysystems/nativemotion/GestureExclusionModule.kt +55 -0
  7. package/android/src/main/java/com/unrulysystems/nativemotion/NativeMotionPackage.kt +27 -0
  8. package/package.json +57 -0
  9. package/react-native.config.js +14 -0
  10. package/src/native/driver/instantWindow.ts +31 -0
  11. package/src/native/driver/strictModeReplay.ts +20 -0
  12. package/src/native/driver/uiLayoutEngine.ts +845 -0
  13. package/src/native/driver/uiLayoutGraph.ts +338 -0
  14. package/src/native/driver/uiValueChannel.ts +4640 -0
  15. package/src/native/driver/workletDriver.ts +3860 -0
  16. package/src/native/motion/AnimatePresence.tsx +1605 -0
  17. package/src/native/motion/LayoutGroup.tsx +163 -0
  18. package/src/native/motion/MotionConfig.tsx +400 -0
  19. package/src/native/motion/MotionRoot.tsx +250 -0
  20. package/src/native/motion/MotionView.tsx +6761 -0
  21. package/src/native/motion/addScaleCorrector.ts +97 -0
  22. package/src/native/motion/colorEndpointUiFeed.ts +32 -0
  23. package/src/native/motion/colorProjection.ts +39 -0
  24. package/src/native/motion/composeTransform.ts +271 -0
  25. package/src/native/motion/constraintMeasure.ts +157 -0
  26. package/src/native/motion/deferredPendingSnapshots.ts +25 -0
  27. package/src/native/motion/discreteProjection.ts +36 -0
  28. package/src/native/motion/dragAncestorPanContext.ts +28 -0
  29. package/src/native/motion/dragControls.ts +152 -0
  30. package/src/native/motion/dragGestureWiring.ts +1168 -0
  31. package/src/native/motion/dragHandoffBinding.ts +179 -0
  32. package/src/native/motion/dragHubStream.ts +756 -0
  33. package/src/native/motion/dragPropagationLock.ts +57 -0
  34. package/src/native/motion/driverValueChannel.ts +3288 -0
  35. package/src/native/motion/externalDragDriver.ts +214 -0
  36. package/src/native/motion/frameData.ts +31 -0
  37. package/src/native/motion/gestureBinding.ts +107 -0
  38. package/src/native/motion/gestureStateGate.ts +563 -0
  39. package/src/native/motion/gestureStateResolver.ts +286 -0
  40. package/src/native/motion/identityValueChannelControllerAdapter.ts +2043 -0
  41. package/src/native/motion/identityValueChannelControllerReconciliation.ts +176 -0
  42. package/src/native/motion/identityValueChannelLaneMarker.ts +14 -0
  43. package/src/native/motion/imperativeAnimate.ts +816 -0
  44. package/src/native/motion/keyframeTiming.ts +7 -0
  45. package/src/native/motion/layoutIdBinding.ts +261 -0
  46. package/src/native/motion/layoutIdFlightConfig.ts +51 -0
  47. package/src/native/motion/layoutProjection.ts +89 -0
  48. package/src/native/motion/layoutScroll.ts +204 -0
  49. package/src/native/motion/layoutTransition.ts +1302 -0
  50. package/src/native/motion/lengthLayoutContext.tsx +100 -0
  51. package/src/native/motion/lengthLayoutHost.ts +44 -0
  52. package/src/native/motion/mappedKeys.ts +184 -0
  53. package/src/native/motion/motionViewController.ts +3180 -0
  54. package/src/native/motion/nativeHostMarker.ts +22 -0
  55. package/src/native/motion/panSessionWiring.ts +127 -0
  56. package/src/native/motion/pathTransition.ts +101 -0
  57. package/src/native/motion/popLayout.ts +176 -0
  58. package/src/native/motion/presenceBinding.ts +382 -0
  59. package/src/native/motion/scaleCorrectorRegistry.ts +123 -0
  60. package/src/native/motion/scrollValues.ts +260 -0
  61. package/src/native/motion/serializablePayload.ts +151 -0
  62. package/src/native/motion/severity.ts +16 -0
  63. package/src/native/motion/shippedSurface.ts +843 -0
  64. package/src/native/motion/staticLengthGate.ts +86 -0
  65. package/src/native/motion/styleBaseGate.ts +72 -0
  66. package/src/native/motion/styleValueBinding.ts +657 -0
  67. package/src/native/motion/systemGestureExclusion.ts +61 -0
  68. package/src/native/motion/tapGestureWiring.ts +215 -0
  69. package/src/native/motion/transformOrder.ts +38 -0
  70. package/src/native/motion/transformStringBinding.ts +200 -0
  71. package/src/native/motion/transformTemplateGate.ts +45 -0
  72. package/src/native/motion/transitionGate.ts +435 -0
  73. package/src/native/motion/useAnimate.ts +52 -0
  74. package/src/native/motion/useCycle.ts +44 -0
  75. package/src/native/motion/useInstantTransition.ts +68 -0
  76. package/src/native/motion/useReducedMotion.ts +58 -0
  77. package/src/native/motion/useScroll.ts +162 -0
  78. package/src/native/motion/useViewportScroll.ts +29 -0
  79. package/src/native/motion/valueChannel.ts +7121 -0
  80. package/src/native/motion/valueHooks.ts +901 -0
  81. package/src/native/motion/variantChildRegistry.ts +67 -0
  82. package/src/native/motion/variantContext.tsx +172 -0
  83. package/src/native/motion/variantProps.ts +598 -0
  84. package/src/native.ts +159 -0
  85. package/src/verification/harnessMetrics.ts +78 -0
  86. package/src/verification/probe/LayoutIdentityWorkletProbe.tsx +251 -0
  87. package/src/verification/probe/WorkletParityProbe.tsx +162 -0
  88. package/src/verification/probe/layoutIdentityProbeEngine.ts +429 -0
  89. package/src/verification/screens/ArcPathChecksScreen.tsx +273 -0
  90. package/src/verification/screens/BooleanAnimateChecksScreen.tsx +358 -0
  91. package/src/verification/screens/ChoreographyGalleryScreen.tsx +1010 -0
  92. package/src/verification/screens/ColorBindingChecksScreen.tsx +568 -0
  93. package/src/verification/screens/CompletionChecksScreen.tsx +294 -0
  94. package/src/verification/screens/ConformanceScreen.tsx +573 -0
  95. package/src/verification/screens/ContentionProbeScreen.tsx +224 -0
  96. package/src/verification/screens/DriverSmokeScreen.tsx +99 -0
  97. package/src/verification/screens/DurationOnlyTweenChecksScreen.tsx +294 -0
  98. package/src/verification/screens/DynamicDragConfigChecksScreen.tsx +237 -0
  99. package/src/verification/screens/FrameDataChecksScreen.tsx +252 -0
  100. package/src/verification/screens/GestureChecksScreen.tsx +685 -0
  101. package/src/verification/screens/InstantTransitionChecksScreen.tsx +839 -0
  102. package/src/verification/screens/LayoutAnimationStartChecksScreen.tsx +648 -0
  103. package/src/verification/screens/LayoutChecksScreen.tsx +822 -0
  104. package/src/verification/screens/LayoutCommitSpikeScreen.tsx +142 -0
  105. package/src/verification/screens/MotionViewChecksScreen.tsx +930 -0
  106. package/src/verification/screens/PresenceChecksScreen.tsx +614 -0
  107. package/src/verification/screens/ReducedMotionChecksScreen.tsx +614 -0
  108. package/src/verification/screens/RestThresholdChecksScreen.tsx +634 -0
  109. package/src/verification/screens/ScaleCorrectorChecksScreen.tsx +425 -0
  110. package/src/verification/screens/SharedLayoutContinuityChecksScreen.tsx +338 -0
  111. package/src/verification/screens/SharedLayoutCrossfadeChecksScreen.tsx +425 -0
  112. package/src/verification/screens/TransitionDefaultSelectionChecksScreen.tsx +514 -0
  113. package/src/verification/screens/ViewportScrollAliasChecksScreen.tsx +303 -0
  114. package/src/verification/screens/conformanceBanner.ts +21 -0
  115. package/src/verification/screens/proofConsoleTap.ts +18 -0
  116. package/src/verification.ts +47 -0
  117. package/src/web.ts +98 -0
@@ -0,0 +1,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=&quot;u6-box&quot; 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
+ })