@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,425 @@
1
+ // U7g addScaleCorrector device checks: catalog Layout-Projection-custom-values ("Framer
2
+ // border") on the public MotionView path. iOS has no tap injection, so the screen self-drives.
3
+ // Discriminators are the channel (Decision 82 analog of 77): worklet registration accepted,
4
+ // CSS-variable / boxShadow / non-worklet refuse loud, catalog layout flight starts then
5
+ // completes. Visual constancy (computed × scale ≈ authored) is the web e2e witness; the math
6
+ // is unit-proven (applyInstalledScaleCorrector). Native cannot read
7
+ // host paint.
8
+
9
+ import { useCallback, useEffect, useRef, useState } from 'react'
10
+ import { ScrollView, StyleSheet, Text, View } from 'react-native'
11
+ import { addScaleCorrector } from '../../native/motion/addScaleCorrector'
12
+ import { MotionView } from '../../native/motion/MotionView'
13
+ import { ambientNativeSeverity } from '../../native/motion/severity'
14
+
15
+ const ROW_REGISTER = 'worklet catalog registration accepted (four border-width correctors)'
16
+ const ROW_CSS_VAR = 'CSS-variable key refuses loud'
17
+ const ROW_BOX_SHADOW = 'boxShadow refuses loud (F3: no silent native built-in)'
18
+ const ROW_WORKLET = 'non-worklet corrector refuses loud (F1)'
19
+ const ROW_FLIGHT = 'catalog layout flight: start then complete'
20
+ const ROW_CLEAN = 'clean run: only the three expected refusal reports'
21
+ const ROWS = [
22
+ ROW_REGISTER,
23
+ ROW_CSS_VAR,
24
+ ROW_BOX_SHADOW,
25
+ ROW_WORKLET,
26
+ ROW_FLIGHT,
27
+ ROW_CLEAN,
28
+ ] as const
29
+
30
+ const TRANSITION = { duration: 0.8, ease: 'circIn' } as const
31
+ const T_REGISTER = 250
32
+ const T_REFUSALS = 500
33
+ const T_FLIGHT = 800
34
+ const START_DEADLINE_MS = 4000
35
+ const SETTLE_DEADLINE_MS = 6000
36
+ const STABILITY_MS = 400
37
+
38
+ interface RowResult {
39
+ readonly pass: boolean
40
+ readonly detail: string
41
+ }
42
+
43
+ function correctBorderX(
44
+ latest: number | string,
45
+ context: {
46
+ targetDelta: { x: { scale: number }; y: { scale: number } }
47
+ treeScale: { x: number; y: number }
48
+ },
49
+ ): number {
50
+ 'worklet'
51
+ const value = typeof latest === 'string' ? Number.parseFloat(String(latest)) : latest
52
+ return value / context.targetDelta.x.scale / context.treeScale.x
53
+ }
54
+
55
+ function correctBorderY(
56
+ latest: number | string,
57
+ context: {
58
+ targetDelta: { x: { scale: number }; y: { scale: number } }
59
+ treeScale: { x: number; y: number }
60
+ },
61
+ ): number {
62
+ 'worklet'
63
+ const value = typeof latest === 'string' ? Number.parseFloat(String(latest)) : latest
64
+ return value / context.targetDelta.y.scale / context.treeScale.y
65
+ }
66
+
67
+ const catalogCorrectors = {
68
+ borderTopWidth: { correct: correctBorderY },
69
+ borderLeftWidth: { correct: correctBorderX },
70
+ borderRightWidth: { correct: correctBorderX },
71
+ borderBottomWidth: { correct: correctBorderY },
72
+ }
73
+
74
+ export function ScaleCorrectorChecksScreen() {
75
+ const severity = ambientNativeSeverity()
76
+ const [isOn, setOn] = useState(false)
77
+ const [stage, setStage] = useState('boot')
78
+ const [rows, setRows] = useState<Partial<Record<(typeof ROWS)[number], RowResult>>>({})
79
+ const [starts, setStarts] = useState(0)
80
+ const [completes, setCompletes] = useState(0)
81
+
82
+ const reportsRef = useRef<string[]>([])
83
+ const startsRef = useRef(0)
84
+ const completesRef = useRef(0)
85
+ const rafsRef = useRef<ReturnType<typeof requestAnimationFrame>[]>([])
86
+ const mountStampRef = useRef(Date.now())
87
+
88
+ const now = (): number => Date.now() - mountStampRef.current
89
+
90
+ const finalize = (id: (typeof ROWS)[number], pass: boolean, detail: string): void => {
91
+ setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
92
+ }
93
+
94
+ const onStart = useCallback((): void => {
95
+ startsRef.current += 1
96
+ setStarts(startsRef.current)
97
+ }, [])
98
+
99
+ const onComplete = useCallback((): void => {
100
+ completesRef.current += 1
101
+ setCompletes(completesRef.current)
102
+ }, [])
103
+
104
+ useEffect(() => {
105
+ const originalError = console.error
106
+ console.error = (...args: unknown[]) => {
107
+ for (const arg of args) {
108
+ const message = arg instanceof Error ? arg.message : typeof arg === 'string' ? arg : null
109
+ if (
110
+ message !== null &&
111
+ /motion|addScaleCorrector|worklet|boxShadow|CSS variable/i.test(message)
112
+ ) {
113
+ reportsRef.current.push(message)
114
+ }
115
+ }
116
+ originalError(...args)
117
+ }
118
+
119
+ const timers: ReturnType<typeof setTimeout>[] = []
120
+ const at = (ms: number, run: () => void): void => {
121
+ timers.push(setTimeout(run, ms))
122
+ }
123
+ const trackRaf = (id: ReturnType<typeof requestAnimationFrame>): void => {
124
+ rafsRef.current.push(id)
125
+ }
126
+ const pollUntil = (
127
+ condition: () => boolean,
128
+ deadlineAt: number,
129
+ onResolve: (ok: boolean) => void,
130
+ ): void => {
131
+ const step = (): void => {
132
+ if (condition()) {
133
+ onResolve(true)
134
+ return
135
+ }
136
+ if (now() >= deadlineAt) {
137
+ onResolve(false)
138
+ return
139
+ }
140
+ trackRaf(requestAnimationFrame(step))
141
+ }
142
+ trackRaf(requestAnimationFrame(step))
143
+ }
144
+
145
+ const captureRefusal = (run: () => void, pattern: RegExp): { hit: boolean; detail: string } => {
146
+ const before = reportsRef.current.length
147
+ let thrown: string | null = null
148
+ try {
149
+ run()
150
+ } catch (error) {
151
+ thrown = error instanceof Error ? error.message : String(error)
152
+ }
153
+ const reported = reportsRef.current.slice(before)
154
+ const text = thrown ?? reported.join(' | ')
155
+ const hit =
156
+ (thrown !== null && pattern.test(thrown)) ||
157
+ reported.some((message) => pattern.test(message))
158
+ return {
159
+ hit,
160
+ detail: hit
161
+ ? `${thrown !== null ? 'threw' : 'reported'}: ${text.slice(0, 120)}`
162
+ : `no matching refusal (thrown=${thrown ?? 'none'}; new reports=${reported.length})`,
163
+ }
164
+ }
165
+
166
+ at(T_REGISTER, () => {
167
+ let thrown: string | null = null
168
+ try {
169
+ addScaleCorrector(catalogCorrectors)
170
+ } catch (error) {
171
+ thrown = error instanceof Error ? error.message : String(error)
172
+ }
173
+ finalize(
174
+ ROW_REGISTER,
175
+ thrown === null,
176
+ thrown === null
177
+ ? 'four catalog border-width worklet correctors registered'
178
+ : `registration threw: ${thrown.slice(0, 120)}`,
179
+ )
180
+ setStage('registered')
181
+ })
182
+
183
+ at(T_REFUSALS, () => {
184
+ const cssVar = captureRefusal(
185
+ () => addScaleCorrector({ '--frame-border': { correct: correctBorderX } }),
186
+ /CSS variable|--frame-border/,
187
+ )
188
+ finalize(ROW_CSS_VAR, cssVar.hit, cssVar.detail)
189
+ const boxShadow = captureRefusal(
190
+ () => addScaleCorrector({ boxShadow: { correct: correctBorderX } }),
191
+ /boxShadow/,
192
+ )
193
+ finalize(ROW_BOX_SHADOW, boxShadow.hit, boxShadow.detail)
194
+ const plain = captureRefusal(
195
+ () =>
196
+ addScaleCorrector({
197
+ paddingTop: {
198
+ correct: (latest) => latest,
199
+ },
200
+ }),
201
+ /worklet/,
202
+ )
203
+ finalize(ROW_WORKLET, plain.hit, plain.detail)
204
+ setStage('refusals sampled')
205
+ })
206
+
207
+ at(T_FLIGHT, () => {
208
+ setOn(true)
209
+ setStage('layout flight pressed (100×100 → 280×160)')
210
+ pollUntil(
211
+ () => startsRef.current >= 1,
212
+ now() + START_DEADLINE_MS,
213
+ (okStart) => {
214
+ if (!okStart) {
215
+ finalize(
216
+ ROW_FLIGHT,
217
+ false,
218
+ `layout start never fired within ${START_DEADLINE_MS}ms (starts ${startsRef.current})`,
219
+ )
220
+ setStage('flight row finalized (FAIL)')
221
+ at(STABILITY_MS, () => finishClean())
222
+ return
223
+ }
224
+ pollUntil(
225
+ () => completesRef.current >= 1,
226
+ now() + SETTLE_DEADLINE_MS,
227
+ (okSettle) => {
228
+ finalize(
229
+ ROW_FLIGHT,
230
+ okSettle && startsRef.current >= 1,
231
+ okSettle
232
+ ? `start ${startsRef.current} then complete ${completesRef.current} on the catalog outer`
233
+ : `layout complete never fired within ${SETTLE_DEADLINE_MS}ms (starts ${startsRef.current}, completes ${completesRef.current})`,
234
+ )
235
+ setStage(okSettle ? 'flight row finalized' : 'flight row finalized (FAIL)')
236
+ at(STABILITY_MS, () => finishClean())
237
+ },
238
+ )
239
+ },
240
+ )
241
+ })
242
+
243
+ const finishClean = (): void => {
244
+ const expected = reportsRef.current.filter(
245
+ (message) =>
246
+ /CSS variable|--frame-border/.test(message) ||
247
+ /boxShadow/.test(message) ||
248
+ /worklet/.test(message),
249
+ )
250
+ const unexpected = reportsRef.current.filter(
251
+ (message) =>
252
+ !/CSS variable|--frame-border/.test(message) &&
253
+ !/boxShadow/.test(message) &&
254
+ !/worklet/.test(message),
255
+ )
256
+ finalize(
257
+ ROW_CLEAN,
258
+ unexpected.length === 0,
259
+ unexpected.length === 0
260
+ ? `0 unexpected reports; ${expected.length} expected refusal(s) (css-var/boxShadow/worklet)`
261
+ : `${unexpected.length} unexpected: ${unexpected.map((message) => message.slice(0, 80)).join(' | ')}`,
262
+ )
263
+ setStage('done')
264
+ }
265
+
266
+ return () => {
267
+ for (const timer of timers) clearTimeout(timer)
268
+ for (const raf of rafsRef.current) cancelAnimationFrame(raf)
269
+ console.error = originalError
270
+ }
271
+ }, [])
272
+
273
+ const done = ROWS.every((id) => rows[id] !== undefined)
274
+ const failing = ROWS.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
275
+ const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
276
+ const passed = Object.keys(rows).length - failing.length
277
+
278
+ return (
279
+ <ScrollView style={styles.scroll} contentContainerStyle={styles.container}>
280
+ <Text style={styles.title}>U7g scale-corrector checks</Text>
281
+ <Text style={styles.subtitle}>
282
+ catalog Framer-border channel + F1/F3 refusals ({severity} severity)
283
+ </Text>
284
+ <View
285
+ style={[
286
+ styles.banner,
287
+ verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
288
+ ]}
289
+ >
290
+ <Text style={styles.bannerText}>{verdict}</Text>
291
+ </View>
292
+ <Text style={styles.tally}>
293
+ {done
294
+ ? `${passed} PASS / ${failing.length} FAIL of ${ROWS.length} rows`
295
+ : `${Object.keys(rows).length}/${ROWS.length} rows complete — RUNNING`}
296
+ </Text>
297
+ <Text style={styles.tallyDetail}>
298
+ {done
299
+ ? failing.length === 0
300
+ ? 'no failing rows'
301
+ : `FAILING: ${failing.join(' | ')}`
302
+ : `stage: ${stage}`}
303
+ </Text>
304
+ <Text style={styles.status}>
305
+ on={isOn ? 1 : 0} · starts={starts} · completes={completes} · {severity}
306
+ </Text>
307
+
308
+ <View style={styles.stage} accessibilityLabel="u7g-scale-corrector-stage">
309
+ <MotionView
310
+ layout
311
+ transition={TRANSITION}
312
+ onLayoutAnimationStart={onStart}
313
+ onLayoutAnimationComplete={onComplete}
314
+ style={[styles.outer, isOn ? styles.outerOn : styles.outerOff]}
315
+ accessibilityLabel="u7g-scale-corrector-outer"
316
+ >
317
+ <MotionView
318
+ layout
319
+ initial={false}
320
+ animate={
321
+ isOn
322
+ ? {
323
+ borderTopWidth: 5,
324
+ borderRightWidth: 5,
325
+ borderLeftWidth: 5,
326
+ borderBottomWidth: 30,
327
+ borderColor: '#000000',
328
+ }
329
+ : {
330
+ borderTopWidth: 50,
331
+ borderRightWidth: 50,
332
+ borderLeftWidth: 50,
333
+ borderBottomWidth: 50,
334
+ borderColor: '#9900ff',
335
+ }
336
+ }
337
+ transition={TRANSITION}
338
+ style={styles.inner}
339
+ accessibilityLabel="u7g-scale-corrector-inner"
340
+ />
341
+ </MotionView>
342
+ </View>
343
+
344
+ {ROWS.map((id) => {
345
+ const row = rows[id]
346
+ return (
347
+ <View key={id} style={styles.row}>
348
+ <Text
349
+ style={[
350
+ styles.verdict,
351
+ row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
352
+ ]}
353
+ >
354
+ {row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
355
+ </Text>
356
+ <View style={styles.rowBody}>
357
+ <Text style={styles.rowId}>{id}</Text>
358
+ <Text style={styles.rowDetail} numberOfLines={3}>
359
+ {row === undefined ? 'pending' : row.detail}
360
+ </Text>
361
+ </View>
362
+ </View>
363
+ )
364
+ })}
365
+ </ScrollView>
366
+ )
367
+ }
368
+
369
+ const styles = StyleSheet.create({
370
+ scroll: { flex: 1, backgroundColor: '#0b0b12' },
371
+ container: { paddingVertical: 72, paddingHorizontal: 8 },
372
+ title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
373
+ subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
374
+ banner: {
375
+ alignSelf: 'center',
376
+ paddingHorizontal: 36,
377
+ paddingVertical: 10,
378
+ borderRadius: 10,
379
+ marginTop: 18,
380
+ marginBottom: 10,
381
+ },
382
+ pass: { backgroundColor: '#1d7a3e' },
383
+ fail: { backgroundColor: '#a32633' },
384
+ running: { backgroundColor: '#7a641d' },
385
+ bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
386
+ tally: { color: '#e6e6f0', fontSize: 14, fontWeight: '700', textAlign: 'center' },
387
+ tallyDetail: { color: '#f87171', fontSize: 11, marginTop: 3, textAlign: 'center' },
388
+ status: {
389
+ color: '#c9c9d8',
390
+ fontSize: 11,
391
+ marginTop: 8,
392
+ textAlign: 'center',
393
+ fontFamily: 'Menlo',
394
+ },
395
+ stage: { height: 180, marginTop: 14 },
396
+ outer: {
397
+ position: 'absolute',
398
+ top: 0,
399
+ left: 8,
400
+ backgroundColor: '#e6e6f0',
401
+ },
402
+ outerOff: { width: 100, height: 100 },
403
+ outerOn: { width: 280, height: 160 },
404
+ inner: {
405
+ position: 'absolute',
406
+ top: 0,
407
+ left: 0,
408
+ right: 0,
409
+ bottom: 0,
410
+ borderStyle: 'solid',
411
+ borderColor: '#9900ff',
412
+ borderTopWidth: 50,
413
+ borderRightWidth: 50,
414
+ borderLeftWidth: 50,
415
+ borderBottomWidth: 50,
416
+ },
417
+ row: { flexDirection: 'row', marginTop: 10 },
418
+ verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
419
+ ok: { color: '#4ade80' },
420
+ bad: { color: '#f87171' },
421
+ pending: { color: '#8a8aa0' },
422
+ rowBody: { flex: 1 },
423
+ rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
424
+ rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
425
+ })