@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,685 @@
|
|
|
1
|
+
// M2.4 gesture-binding checks (specs/M2.4-BUILD-PACKET.md §pinned semantics). All seven rows drive
|
|
2
|
+
// the binding with SYNTHETIC Pan-event shapes + a ManualClock/ManualScheduler graph — fully
|
|
3
|
+
// deterministic, no real touches (the live legs are the smoke: adb swipes on Android, a replayed
|
|
4
|
+
// stream on iOS). A screenshot is the assertion; try/catch is evidence capture, never suppression.
|
|
5
|
+
|
|
6
|
+
import { ScrollView, StyleSheet, Text, View } from 'react-native'
|
|
7
|
+
import {
|
|
8
|
+
ManualClock,
|
|
9
|
+
ManualScheduler,
|
|
10
|
+
createMotionGraph,
|
|
11
|
+
type MotionGraph,
|
|
12
|
+
} from '@unrulysystems/native-motion-core'
|
|
13
|
+
import {
|
|
14
|
+
createGestureBinding,
|
|
15
|
+
routePanEnd,
|
|
16
|
+
routePanFinalize,
|
|
17
|
+
type GestureBinding,
|
|
18
|
+
} from '../../native/motion/gestureBinding'
|
|
19
|
+
import {
|
|
20
|
+
createTapGestureWiring,
|
|
21
|
+
createGlobalTapObserver,
|
|
22
|
+
gatePostMountTapArrival,
|
|
23
|
+
gateTapCallbackProps,
|
|
24
|
+
} from '../../native/motion/tapGestureWiring'
|
|
25
|
+
import { createDragHubStream } from '../../native/motion/dragHubStream'
|
|
26
|
+
import { createGestureStateResolver } from '../../native/motion/gestureStateResolver'
|
|
27
|
+
import { gateGestureStateProps } from '../../native/motion/gestureStateGate'
|
|
28
|
+
|
|
29
|
+
const DT = 16.67
|
|
30
|
+
|
|
31
|
+
interface Row {
|
|
32
|
+
readonly id: string
|
|
33
|
+
readonly pass: boolean
|
|
34
|
+
readonly detail: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Rig {
|
|
38
|
+
readonly graph: MotionGraph
|
|
39
|
+
readonly clock: ManualClock
|
|
40
|
+
readonly scheduler: ManualScheduler
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function rig(): Rig {
|
|
44
|
+
const clock = new ManualClock()
|
|
45
|
+
const scheduler = new ManualScheduler(clock)
|
|
46
|
+
return { graph: createMotionGraph({ clock, scheduler }), clock, scheduler }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const SNAPPED = {
|
|
50
|
+
constraints: { min: 0, max: 240, elastic: 0.4 },
|
|
51
|
+
snap: { points: [0, 240] as const },
|
|
52
|
+
} as const
|
|
53
|
+
|
|
54
|
+
function bindingWith(
|
|
55
|
+
r: Rig,
|
|
56
|
+
extra: Partial<Parameters<typeof createGestureBinding>[0]> = {},
|
|
57
|
+
): GestureBinding {
|
|
58
|
+
return createGestureBinding({
|
|
59
|
+
graph: r.graph,
|
|
60
|
+
clock: r.clock,
|
|
61
|
+
initial: 0,
|
|
62
|
+
constraints: SNAPPED.constraints,
|
|
63
|
+
snap: { points: [...SNAPPED.snap.points] },
|
|
64
|
+
...extra,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function checkDeadZone(): Row {
|
|
69
|
+
const id = 'activation dead-zone: no jump at grab'
|
|
70
|
+
try {
|
|
71
|
+
const r = rig()
|
|
72
|
+
const b = bindingWith(r)
|
|
73
|
+
b.onStart({ translationX: 12, velocityX: 0 }) // recognizer already moved 12px to activate
|
|
74
|
+
if (b.session.value() !== 0) {
|
|
75
|
+
return { id, pass: false, detail: `value jumped to ${b.session.value()} at begin, want 0` }
|
|
76
|
+
}
|
|
77
|
+
b.onUpdate({ translationX: 13, velocityX: 60 })
|
|
78
|
+
const first = b.session.value()
|
|
79
|
+
if (Math.abs(first - 1) > 1e-9) {
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
pass: false,
|
|
83
|
+
detail: `first active frame value=${first}, want 1 (13−12 from grab)`,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
id,
|
|
88
|
+
pass: true,
|
|
89
|
+
detail: 'begin at translation 12 → value 0; first frame 13 → value 1 (C0)',
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return { id, pass: false, detail: e instanceof Error ? e.message : String(e) }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function checkDragFollowAndHandoff(): Row {
|
|
97
|
+
const id = 'drag-follow; §G platform velocity passes UNSCALED'
|
|
98
|
+
try {
|
|
99
|
+
const r = rig()
|
|
100
|
+
const b = bindingWith(r)
|
|
101
|
+
b.onStart({ translationX: 0, velocityX: 0 })
|
|
102
|
+
for (let translation = 20; translation <= 200; translation += 20) {
|
|
103
|
+
b.onUpdate({ translationX: translation, velocityX: 500 })
|
|
104
|
+
r.scheduler.frame(DT)
|
|
105
|
+
}
|
|
106
|
+
if (Math.abs(b.session.value() - 200) > 1e-9) {
|
|
107
|
+
return { id, pass: false, detail: `value ${b.session.value()} after dragging to 200` }
|
|
108
|
+
}
|
|
109
|
+
b.onEnd({ translationX: 200, velocityX: 833.25 })
|
|
110
|
+
const handoff = b.session.handoffVelocity()
|
|
111
|
+
if (handoff !== 833.25) {
|
|
112
|
+
return {
|
|
113
|
+
id,
|
|
114
|
+
pass: false,
|
|
115
|
+
detail: `handoff ${String(handoff)}, want the platform's exact 833.25 (§G)`,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { id, pass: true, detail: 'followed to 200; handoff === 833.25 exactly (unscaled)' }
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return { id, pass: false, detail: e instanceof Error ? e.message : String(e) }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function checkReleaseSnap(): Row {
|
|
125
|
+
const id = 'release settles at the projected snap point'
|
|
126
|
+
try {
|
|
127
|
+
const r = rig()
|
|
128
|
+
const b = bindingWith(r)
|
|
129
|
+
b.onStart({ translationX: 0, velocityX: 0 })
|
|
130
|
+
b.onUpdate({ translationX: 150, velocityX: 900 })
|
|
131
|
+
b.onEnd({ translationX: 150, velocityX: 900 }) // fast rightward release from 150
|
|
132
|
+
r.scheduler.frames(240, DT)
|
|
133
|
+
const settled = b.session.value()
|
|
134
|
+
if (Math.abs(settled - 240) > 0.5 || !b.session.isSettled()) {
|
|
135
|
+
return {
|
|
136
|
+
id,
|
|
137
|
+
pass: false,
|
|
138
|
+
detail: `settled at ${settled} (settled=${String(b.session.isSettled())}), want snap 240`,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (b.session.state() !== 'idle') {
|
|
142
|
+
return { id, pass: false, detail: `state ${b.session.state()} after settle, want idle` }
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
id,
|
|
146
|
+
pass: true,
|
|
147
|
+
detail: 'released at 150 w/ +900 px/s → snapped to 240, settled, idle',
|
|
148
|
+
}
|
|
149
|
+
} catch (e) {
|
|
150
|
+
return { id, pass: false, detail: e instanceof Error ? e.message : String(e) }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function checkConstraints(): Row {
|
|
155
|
+
const id = 'constraints: hard clamp at 0, rubber-band above 0'
|
|
156
|
+
try {
|
|
157
|
+
const rHard = rig()
|
|
158
|
+
const hard = bindingWith(rHard, { constraints: { min: 0, max: 240, elastic: 0 } })
|
|
159
|
+
hard.onStart({ translationX: 0, velocityX: 0 })
|
|
160
|
+
hard.onUpdate({ translationX: 300, velocityX: 0 })
|
|
161
|
+
const clamped = hard.session.value()
|
|
162
|
+
if (clamped !== 240)
|
|
163
|
+
return { id, pass: false, detail: `elastic 0: value ${clamped}, want hard 240` }
|
|
164
|
+
const rSoft = rig()
|
|
165
|
+
const soft = bindingWith(rSoft, { constraints: { min: 0, max: 240, elastic: 0.5 } })
|
|
166
|
+
soft.onStart({ translationX: 0, velocityX: 0 })
|
|
167
|
+
soft.onUpdate({ translationX: 300, velocityX: 0 })
|
|
168
|
+
const banded = soft.session.value()
|
|
169
|
+
if (!(banded > 240 && banded < 300)) {
|
|
170
|
+
return {
|
|
171
|
+
id,
|
|
172
|
+
pass: false,
|
|
173
|
+
detail: `elastic 0.5: value ${banded}, want strictly between 240 and 300`,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { id, pass: true, detail: `clamped 300→240; rubber-banded 300→${banded.toFixed(1)}` }
|
|
177
|
+
} catch (e) {
|
|
178
|
+
return { id, pass: false, detail: e instanceof Error ? e.message : String(e) }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function checkRegrabContinuity(): Row {
|
|
183
|
+
const id = 'mid-settle re-grab seeds from the live value'
|
|
184
|
+
try {
|
|
185
|
+
const r = rig()
|
|
186
|
+
const b = bindingWith(r)
|
|
187
|
+
b.onStart({ translationX: 0, velocityX: 0 })
|
|
188
|
+
b.onUpdate({ translationX: 150, velocityX: 900 })
|
|
189
|
+
b.onEnd({ translationX: 150, velocityX: 900 })
|
|
190
|
+
r.scheduler.frames(6, DT) // spring in flight toward 240
|
|
191
|
+
const live = b.session.value()
|
|
192
|
+
if (!(live > 150 && live < 240)) {
|
|
193
|
+
return { id, pass: false, detail: `live value ${live} not mid-flight` }
|
|
194
|
+
}
|
|
195
|
+
b.onStart({ translationX: 30, velocityX: 0 }) // re-grab mid-settle (nonzero activation offset)
|
|
196
|
+
if (b.session.state() !== 'active') {
|
|
197
|
+
return { id, pass: false, detail: `state ${b.session.state()} after re-grab, want active` }
|
|
198
|
+
}
|
|
199
|
+
const atGrab = b.session.value()
|
|
200
|
+
if (Math.abs(atGrab - live) > 1e-6) {
|
|
201
|
+
return {
|
|
202
|
+
id,
|
|
203
|
+
pass: false,
|
|
204
|
+
detail: `re-grab snapped ${live.toFixed(2)} → ${atGrab.toFixed(2)}`,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { id, pass: true, detail: `re-grabbed at live ${live.toFixed(1)}, no snap-back, active` }
|
|
208
|
+
} catch (e) {
|
|
209
|
+
return { id, pass: false, detail: e instanceof Error ? e.message : String(e) }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function checkCancelFailAndFreeDrag(): Row {
|
|
214
|
+
const id = 'cancel/fail → pre-grab; free-drag release projects (REQ-GESTURE-023)'
|
|
215
|
+
try {
|
|
216
|
+
const r = rig()
|
|
217
|
+
const b = bindingWith(r)
|
|
218
|
+
b.onStart({ translationX: 0, velocityX: 0 })
|
|
219
|
+
b.onUpdate({ translationX: 120, velocityX: 0 })
|
|
220
|
+
b.onCancel()
|
|
221
|
+
r.scheduler.frames(240, DT)
|
|
222
|
+
if (Math.abs(b.session.value()) > 0.5 || !b.session.isSettled()) {
|
|
223
|
+
return { id, pass: false, detail: `cancel settled at ${b.session.value()}, want pre-grab 0` }
|
|
224
|
+
}
|
|
225
|
+
const r2 = rig()
|
|
226
|
+
const b2 = bindingWith(r2)
|
|
227
|
+
b2.onStart({ translationX: 0, velocityX: 0 })
|
|
228
|
+
b2.onUpdate({ translationX: 80, velocityX: 0 })
|
|
229
|
+
b2.onFail()
|
|
230
|
+
r2.scheduler.frames(240, DT)
|
|
231
|
+
if (Math.abs(b2.session.value()) > 0.5) {
|
|
232
|
+
return { id, pass: false, detail: `fail settled at ${b2.session.value()}, want pre-grab 0` }
|
|
233
|
+
}
|
|
234
|
+
// Free-drag (no snap, no constraints): release settles at project(50, 100) = 130.
|
|
235
|
+
const r3 = rig()
|
|
236
|
+
const free = createGestureBinding({ graph: r3.graph, clock: r3.clock, initial: 0 })
|
|
237
|
+
free.onStart({ translationX: 0, velocityX: 0 })
|
|
238
|
+
free.onUpdate({ translationX: 50, velocityX: 100 })
|
|
239
|
+
free.onEnd({ translationX: 50, velocityX: 100 })
|
|
240
|
+
r3.scheduler.frames(400, DT)
|
|
241
|
+
if (Math.abs(free.session.value() - 130) > 1 || !free.session.isSettled()) {
|
|
242
|
+
return {
|
|
243
|
+
id,
|
|
244
|
+
pass: false,
|
|
245
|
+
detail: `free-drag settled at ${free.session.value()}, want ~130`,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
id,
|
|
250
|
+
pass: true,
|
|
251
|
+
detail: 'cancel + fail settle at pre-grab; free-drag release projects to ideal',
|
|
252
|
+
}
|
|
253
|
+
} catch (e) {
|
|
254
|
+
return { id, pass: false, detail: e instanceof Error ? e.message : String(e) }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function checkMalformedEventGuard(): Row {
|
|
259
|
+
const id = 'malformed Pan events fail loud at the binding'
|
|
260
|
+
try {
|
|
261
|
+
const r = rig()
|
|
262
|
+
const b = bindingWith(r)
|
|
263
|
+
b.onStart({ translationX: 0, velocityX: 0 })
|
|
264
|
+
let missingError: unknown = null
|
|
265
|
+
try {
|
|
266
|
+
b.onUpdate({ velocityX: 5 })
|
|
267
|
+
} catch (e) {
|
|
268
|
+
missingError = e
|
|
269
|
+
}
|
|
270
|
+
const missingOk = missingError instanceof Error && missingError.message.includes('translationX')
|
|
271
|
+
let badTypeError: unknown = null
|
|
272
|
+
try {
|
|
273
|
+
b.onUpdate({ translationX: 5, velocityX: 'fast' })
|
|
274
|
+
} catch (e) {
|
|
275
|
+
badTypeError = e
|
|
276
|
+
}
|
|
277
|
+
const badTypeOk = badTypeError instanceof Error && badTypeError.message.includes('velocityX')
|
|
278
|
+
if (!missingOk || !badTypeOk) {
|
|
279
|
+
return {
|
|
280
|
+
id,
|
|
281
|
+
pass: false,
|
|
282
|
+
detail: `missing→${missingError instanceof Error ? missingError.message.slice(0, 50) : 'no throw'}; badType→${badTypeError instanceof Error ? badTypeError.message.slice(0, 50) : 'no throw'}`,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
id,
|
|
287
|
+
pass: true,
|
|
288
|
+
detail: 'missing translationX and non-numeric velocityX both throw naming the field',
|
|
289
|
+
}
|
|
290
|
+
} catch (e) {
|
|
291
|
+
return { id, pass: false, detail: e instanceof Error ? e.message : String(e) }
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// R5 (REQ-API-030): the tap protocol rows — the recognizer wiring exercised with scripted
|
|
296
|
+
// substrate event sequences (real-touch evidence rides the Android proof leg; the iOS leg
|
|
297
|
+
// self-describes through these rows, R5-F2).
|
|
298
|
+
function checkTapProtocol(): Row {
|
|
299
|
+
const id = 'tap protocol: up-target law (leave-and-return taps; release-outside cancels)'
|
|
300
|
+
try {
|
|
301
|
+
const events: string[] = []
|
|
302
|
+
const wiring = createTapGestureWiring({
|
|
303
|
+
callbacks: {
|
|
304
|
+
onTapStart: () => events.push('start'),
|
|
305
|
+
onTap: (_event, info) => events.push(`tap@${info.point.x},${info.point.y}`),
|
|
306
|
+
onTapCancel: () => events.push('cancel'),
|
|
307
|
+
},
|
|
308
|
+
getElementSize: () => ({ width: 100, height: 100 }),
|
|
309
|
+
})
|
|
310
|
+
const at = (x: number, y: number, absoluteX = x, absoluteY = y) => ({
|
|
311
|
+
x,
|
|
312
|
+
y,
|
|
313
|
+
absoluteX,
|
|
314
|
+
absoluteY,
|
|
315
|
+
})
|
|
316
|
+
wiring.handleBegin(at(12, 34))
|
|
317
|
+
wiring.handleEnd(at(12, 34), true) // release inside — leave-and-return still lands here
|
|
318
|
+
wiring.handleFinalize(at(12, 34), true)
|
|
319
|
+
wiring.handleBegin(at(5, 5))
|
|
320
|
+
wiring.handleEnd(at(200, 5), true) // recognizer ended; the UP target is OFF the element
|
|
321
|
+
wiring.handleFinalize(at(200, 5), true)
|
|
322
|
+
wiring.handleFinalize(at(0, 0), false) // never began — nothing
|
|
323
|
+
const want = ['start', 'tap@12,34', 'start', 'cancel']
|
|
324
|
+
if (events.join('|') !== want.join('|')) {
|
|
325
|
+
return { id, pass: false, detail: `got ${events.join('|')}, want ${want.join('|')}` }
|
|
326
|
+
}
|
|
327
|
+
return { id, pass: true, detail: 'up-target verdict: inside taps, outside cancels, once each' }
|
|
328
|
+
} catch (e) {
|
|
329
|
+
return { id, pass: false, detail: e instanceof Error ? e.message : String(e) }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// U7d (REQ-API-030 global form): the global press observer, exercised through the REAL
|
|
334
|
+
// stack — root hub stream → tap-observation channel → global observer → wiring
|
|
335
|
+
// (globalTarget: true) — with scripted root touches. The pinned law (motion-dom
|
|
336
|
+
// press/index.ts: useGlobalTarget window listener + the up-verdict short-circuit): a down
|
|
337
|
+
// ANYWHERE under the root begins the press (containment never read), EVERY up resolves it,
|
|
338
|
+
// TapInfo.point carries the absolute up point; start-gates (non-primary, active drag) refuse;
|
|
339
|
+
// cancel arrives only from the touch-cancel path; observation is unconditional — no
|
|
340
|
+
// recognizer claim suppresses it (packet F1/F2).
|
|
341
|
+
function checkGlobalTapObserver(): Row {
|
|
342
|
+
const id = 'global tap: root-outside down begins; up-anywhere resolves; cancel only via stream'
|
|
343
|
+
try {
|
|
344
|
+
const events: string[] = []
|
|
345
|
+
let dragActive = false
|
|
346
|
+
const wiring = createTapGestureWiring({
|
|
347
|
+
callbacks: {
|
|
348
|
+
onTapStart: () => events.push('start'),
|
|
349
|
+
onTap: (_event, info) => events.push(`tap@${info.point.x},${info.point.y}`),
|
|
350
|
+
onTapCancel: () => events.push('cancel'),
|
|
351
|
+
},
|
|
352
|
+
// A 100x100 element at the origin — every touch below lands OUTSIDE it; the local
|
|
353
|
+
// form would cancel each one at the containment verdict (checkTapProtocol above).
|
|
354
|
+
getElementSize: () => ({ width: 100, height: 100 }),
|
|
355
|
+
globalTarget: true,
|
|
356
|
+
})
|
|
357
|
+
const observer = createGlobalTapObserver({ wiring, isDragActive: () => dragActive })
|
|
358
|
+
const stream = createDragHubStream()
|
|
359
|
+
stream.subscribeTapObserver(observer)
|
|
360
|
+
const touch = (id: number, x: number, y: number) => ({ id, absoluteX: x, absoluteY: y })
|
|
361
|
+
|
|
362
|
+
// Root-outside down BEGINS the press; the up lands far off-element and still resolves,
|
|
363
|
+
// with TapInfo.point carrying the absolute up point.
|
|
364
|
+
stream.notifyTapDown(touch(1, 400, 500))
|
|
365
|
+
stream.notifyTapUp(touch(1, 900, 50))
|
|
366
|
+
// The multi-touch law: a second (non-primary) touch neither starts nor resolves the
|
|
367
|
+
// press — only the touch that began it does.
|
|
368
|
+
stream.notifyTapDown(touch(1, 10, 10))
|
|
369
|
+
stream.notifyTapDown(touch(2, 20, 20))
|
|
370
|
+
stream.notifyTapUp(touch(2, 20, 20))
|
|
371
|
+
stream.notifyTapUp(touch(1, 10, 10))
|
|
372
|
+
// The start-gate: no press begins while a drag is active.
|
|
373
|
+
dragActive = true
|
|
374
|
+
stream.notifyTapDown(touch(1, 30, 30))
|
|
375
|
+
stream.notifyTapUp(touch(1, 30, 30))
|
|
376
|
+
dragActive = false
|
|
377
|
+
// Cancel arrives only from the touch-cancel path; the id is forgotten, so its later up
|
|
378
|
+
// (and an id the stream never saw) notify nothing.
|
|
379
|
+
stream.notifyTapDown(touch(1, 40, 40))
|
|
380
|
+
stream.notifyTapCancel(touch(1, 40, 40))
|
|
381
|
+
stream.notifyTapUp(touch(1, 40, 40))
|
|
382
|
+
stream.notifyTapUp(touch(99, 0, 0))
|
|
383
|
+
|
|
384
|
+
const want = ['start', 'tap@900,50', 'start', 'tap@10,10', 'start', 'cancel']
|
|
385
|
+
if (events.join('|') !== want.join('|')) {
|
|
386
|
+
return { id, pass: false, detail: `got ${events.join('|')}, want ${want.join('|')}` }
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
id,
|
|
390
|
+
pass: true,
|
|
391
|
+
detail:
|
|
392
|
+
'outside down + far outside up tapped (@900,50); 2nd finger inert; drag-gated; cancel-path only',
|
|
393
|
+
}
|
|
394
|
+
} catch (e) {
|
|
395
|
+
return { id, pass: false, detail: e instanceof Error ? e.message : String(e) }
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function checkTapBoundaryGates(): Row {
|
|
400
|
+
const id = 'tap boundary: malformed callback + post-mount arrival fail loud'
|
|
401
|
+
try {
|
|
402
|
+
let malformed: unknown = null
|
|
403
|
+
try {
|
|
404
|
+
gateTapCallbackProps({ onTap: 42 as never })
|
|
405
|
+
} catch (e) {
|
|
406
|
+
malformed = e
|
|
407
|
+
}
|
|
408
|
+
let postMount: unknown = null
|
|
409
|
+
try {
|
|
410
|
+
gatePostMountTapArrival(false, true, undefined, '<Motion.View>')
|
|
411
|
+
} catch (e) {
|
|
412
|
+
postMount = e
|
|
413
|
+
}
|
|
414
|
+
const malformedOk = malformed instanceof Error && malformed.message.includes('onTap')
|
|
415
|
+
const postMountOk = postMount instanceof Error && postMount.message.includes('fixed at mount')
|
|
416
|
+
if (!malformedOk || !postMountOk) {
|
|
417
|
+
return {
|
|
418
|
+
id,
|
|
419
|
+
pass: false,
|
|
420
|
+
detail: `malformed→${malformed instanceof Error ? 'typed' : 'no throw'}; postMount→${postMount instanceof Error ? 'typed' : 'no throw'}`,
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return { id, pass: true, detail: 'both boundary violations throw typed, named errors' }
|
|
424
|
+
} catch (e) {
|
|
425
|
+
return { id, pass: false, detail: e instanceof Error ? e.message : String(e) }
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function checkPanEndRouting(): Row {
|
|
430
|
+
// Review-cycle-1 major 1: RNGH fires onEnd(event, success=false) for FAILED/CANCELLED gestures
|
|
431
|
+
// BEFORE onFinalize. An unsuccessful end must NOT release — the session stays active so the
|
|
432
|
+
// finalize path cancels to the pre-grab value; a successful end releases normally.
|
|
433
|
+
const id = 'RNGH end routing: unsuccessful end cancels, never releases'
|
|
434
|
+
try {
|
|
435
|
+
const r = rig()
|
|
436
|
+
const b = bindingWith(r)
|
|
437
|
+
b.onStart({ translationX: 0, velocityX: 0 })
|
|
438
|
+
b.onUpdate({ translationX: 120, velocityX: 400 })
|
|
439
|
+
routePanEnd(b, { translationX: 120, velocityX: 400 }, false) // FAILED/CANCELLED end
|
|
440
|
+
if (b.session.state() !== 'active') {
|
|
441
|
+
return {
|
|
442
|
+
id,
|
|
443
|
+
pass: false,
|
|
444
|
+
detail: `unsuccessful end moved state to ${b.session.state()}, want active`,
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
routePanFinalize(b, false)
|
|
448
|
+
r.scheduler.frames(240, DT)
|
|
449
|
+
if (Math.abs(b.session.value()) > 0.5 || !b.session.isSettled()) {
|
|
450
|
+
return {
|
|
451
|
+
id,
|
|
452
|
+
pass: false,
|
|
453
|
+
detail: `cancel path settled at ${b.session.value()}, want pre-grab 0`,
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const r2 = rig()
|
|
457
|
+
const b2 = bindingWith(r2)
|
|
458
|
+
b2.onStart({ translationX: 0, velocityX: 0 })
|
|
459
|
+
b2.onUpdate({ translationX: 150, velocityX: 900 })
|
|
460
|
+
routePanEnd(b2, { translationX: 150, velocityX: 900 }, true) // successful release
|
|
461
|
+
routePanFinalize(b2, true)
|
|
462
|
+
r2.scheduler.frames(240, DT)
|
|
463
|
+
if (Math.abs(b2.session.value() - 240) > 0.5) {
|
|
464
|
+
return {
|
|
465
|
+
id,
|
|
466
|
+
pass: false,
|
|
467
|
+
detail: `successful end settled at ${b2.session.value()}, want snap 240`,
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
id,
|
|
472
|
+
pass: true,
|
|
473
|
+
detail: 'end(false) stayed active → finalize cancelled to 0; end(true) released to 240',
|
|
474
|
+
}
|
|
475
|
+
} catch (e) {
|
|
476
|
+
return { id, pass: false, detail: e instanceof Error ? e.message : String(e) }
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// R6 (REQ-API-031): the gesture-STATE rows — the resolver's overlay/revert law and the
|
|
481
|
+
// boundary gates exercised with scripted flips (real-touch evidence rides the Android proof
|
|
482
|
+
// leg; the iOS leg self-describes through these rows, the R5-F2 posture).
|
|
483
|
+
function checkGestureStateResolver(): Row {
|
|
484
|
+
const id = 'gesture state: overlay + revert law (pinned priority; destination transitions)'
|
|
485
|
+
try {
|
|
486
|
+
const SOFT = { type: 'spring', stiffness: 120, damping: 20 } as const
|
|
487
|
+
const ELEMENT = { type: 'spring', stiffness: 300, damping: 30 } as const
|
|
488
|
+
let animateTarget: Record<string, number> = { scale: 1, opacity: 1 }
|
|
489
|
+
const resolver = createGestureStateResolver({
|
|
490
|
+
layers: () => ({
|
|
491
|
+
animate: { target: animateTarget },
|
|
492
|
+
whileTap: { target: { scale: 0.9 }, transition: SOFT },
|
|
493
|
+
whileDrag: { target: { scale: 1.1 } },
|
|
494
|
+
}),
|
|
495
|
+
baseValueFor: () => undefined,
|
|
496
|
+
elementTransition: () => ELEMENT,
|
|
497
|
+
})
|
|
498
|
+
// Every retarget asserts key AND transition (r27 major f69e1db7a2c4: a wrong-property
|
|
499
|
+
// or wrong-transition retarget must FAIL the row — R6-F1's per-key destination law).
|
|
500
|
+
const expectRetarget = (
|
|
501
|
+
label: string,
|
|
502
|
+
got: readonly { key: string; value: unknown; transition: unknown }[],
|
|
503
|
+
want: { key: string; value: number; transition: unknown },
|
|
504
|
+
): string | null => {
|
|
505
|
+
const one = got.length === 1 ? got[0]! : undefined
|
|
506
|
+
if (
|
|
507
|
+
one === undefined ||
|
|
508
|
+
one.key !== want.key ||
|
|
509
|
+
one.value !== want.value ||
|
|
510
|
+
one.transition !== want.transition
|
|
511
|
+
) {
|
|
512
|
+
return `${label}: got ${JSON.stringify(got)}, want ${JSON.stringify(want)}`
|
|
513
|
+
}
|
|
514
|
+
return null
|
|
515
|
+
}
|
|
516
|
+
const pressBad = expectRetarget('press', resolver.setActive('whileTap', true), {
|
|
517
|
+
key: 'scale',
|
|
518
|
+
value: 0.9,
|
|
519
|
+
transition: SOFT, // the layer's OWN transition
|
|
520
|
+
})
|
|
521
|
+
if (pressBad !== null) return { id, pass: false, detail: pressBad }
|
|
522
|
+
const dragOverBad = expectRetarget('drag-over', resolver.setActive('whileDrag', true), {
|
|
523
|
+
key: 'scale',
|
|
524
|
+
value: 1.1,
|
|
525
|
+
transition: ELEMENT, // whileDrag has no layer transition — element fallback
|
|
526
|
+
})
|
|
527
|
+
if (dragOverBad !== null) return { id, pass: false, detail: dragOverBad }
|
|
528
|
+
const dragOffBad = expectRetarget('drag-off', resolver.setActive('whileDrag', false), {
|
|
529
|
+
key: 'scale',
|
|
530
|
+
value: 0.9,
|
|
531
|
+
transition: SOFT, // the DESTINATION layer's (whileTap's) transition, R6-F1
|
|
532
|
+
})
|
|
533
|
+
if (dragOffBad !== null) return { id, pass: false, detail: dragOffBad }
|
|
534
|
+
animateTarget = { scale: 0.5, opacity: 1 } // a live rerender while pressed (r4 major 19)
|
|
535
|
+
const releaseBad = expectRetarget('release', resolver.setActive('whileTap', false), {
|
|
536
|
+
key: 'scale',
|
|
537
|
+
value: 0.5,
|
|
538
|
+
transition: ELEMENT, // the animate destination rides the element transition
|
|
539
|
+
})
|
|
540
|
+
if (releaseBad !== null) return { id, pass: false, detail: releaseBad }
|
|
541
|
+
return { id, pass: true, detail: 'overlay 0.9 → drag 1.1 → back to 0.9 → live revert 0.5' }
|
|
542
|
+
} catch (e) {
|
|
543
|
+
return { id, pass: false, detail: e instanceof Error ? e.message : String(e) }
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function checkGestureStateBoundary(): Row {
|
|
548
|
+
const id = 'gesture-state boundary: label resolution + inert options + bound-key XOR'
|
|
549
|
+
try {
|
|
550
|
+
// R7 (REQ-API-032): the label form RESOLVES against the dictionary (the R6-era refusal
|
|
551
|
+
// is superseded); a local miss is the pinned NO-OP — an empty layer, never an error.
|
|
552
|
+
const softTap = { type: 'spring', stiffness: 120, damping: 20 } as const
|
|
553
|
+
const resolved = gateGestureStateProps(
|
|
554
|
+
{ whileTap: 'pressed' as never },
|
|
555
|
+
{
|
|
556
|
+
dragAxes: [],
|
|
557
|
+
variantsDictionary: { pressed: { scale: 0.9, transition: softTap } },
|
|
558
|
+
},
|
|
559
|
+
)
|
|
560
|
+
// Structural, never identity: the gate stores a RESOLVED frozen snapshot of the authored
|
|
561
|
+
// transition (major 0cb829 — the accepted layer never holds the caller's object), and the
|
|
562
|
+
// host test (gestureStateGate.test.ts "R7: a label form RESOLVES") asserts toEqual. The
|
|
563
|
+
// earlier `=== softTap` form read the snapshot law as a failure — broken since T19's
|
|
564
|
+
// per-property resolution (daf4fc8f) put resolveTransitionForKey on this path.
|
|
565
|
+
const attribution = resolved.whileTap?.transitions?.['scale']
|
|
566
|
+
const resolvedRight =
|
|
567
|
+
resolved.whileTap !== undefined &&
|
|
568
|
+
(resolved.whileTap.target as { scale?: number }).scale === 0.9 &&
|
|
569
|
+
attribution?.type === 'spring' &&
|
|
570
|
+
(attribution as { stiffness?: number }).stiffness === 120 &&
|
|
571
|
+
(attribution as { damping?: number }).damping === 20
|
|
572
|
+
if (!resolvedRight) {
|
|
573
|
+
return { id, pass: false, detail: 'label must resolve with per-key attribution (law b)' }
|
|
574
|
+
}
|
|
575
|
+
const miss = gateGestureStateProps({ whileTap: 'missing' as never }, { dragAxes: [] })
|
|
576
|
+
const missRight = miss.whileTap !== undefined && Object.keys(miss.whileTap.target).length === 0
|
|
577
|
+
if (!missRight) {
|
|
578
|
+
return { id, pass: false, detail: 'a local miss must be the empty-layer NO-OP (law a)' }
|
|
579
|
+
}
|
|
580
|
+
const delayAccepted = gateGestureStateProps(
|
|
581
|
+
{ whileTap: { opacity: 0.5, transition: { delay: 0.2 } } },
|
|
582
|
+
{ dragAxes: [] },
|
|
583
|
+
)
|
|
584
|
+
const delayRight =
|
|
585
|
+
delayAccepted.whileTap?.transition?.delay === 0.2 &&
|
|
586
|
+
(delayAccepted.whileTap.target as { opacity?: number }).opacity === 0.5
|
|
587
|
+
if (!delayRight) {
|
|
588
|
+
return { id, pass: false, detail: 'property-lane delay must be accepted and preserved' }
|
|
589
|
+
}
|
|
590
|
+
const reported: Error[] = []
|
|
591
|
+
const accepted = gateGestureStateProps(
|
|
592
|
+
{ whileTap: { scale: 0.9, opacity: 0.5 } },
|
|
593
|
+
{ dragAxes: [], boundKeys: ['scale'] },
|
|
594
|
+
{
|
|
595
|
+
severity: 'production',
|
|
596
|
+
report: (error) => reported.push(error),
|
|
597
|
+
componentId: '<Motion.View>' as const,
|
|
598
|
+
},
|
|
599
|
+
)
|
|
600
|
+
const strippedRight =
|
|
601
|
+
reported.length === 1 &&
|
|
602
|
+
accepted.whileTap !== undefined &&
|
|
603
|
+
!('scale' in accepted.whileTap.target) &&
|
|
604
|
+
(accepted.whileTap.target as { opacity?: number }).opacity === 0.5
|
|
605
|
+
if (!strippedRight) {
|
|
606
|
+
return { id, pass: false, detail: 'bound key must STRIP with one report; survivor kept' }
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
id,
|
|
610
|
+
pass: true,
|
|
611
|
+
detail: 'label resolved (law a/b), delay accepted, bound key stripped + survivor kept',
|
|
612
|
+
}
|
|
613
|
+
} catch (e) {
|
|
614
|
+
return { id, pass: false, detail: e instanceof Error ? e.message : String(e) }
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function GestureChecksScreen() {
|
|
619
|
+
const rows: Row[] = [
|
|
620
|
+
checkDeadZone(),
|
|
621
|
+
checkDragFollowAndHandoff(),
|
|
622
|
+
checkReleaseSnap(),
|
|
623
|
+
checkConstraints(),
|
|
624
|
+
checkRegrabContinuity(),
|
|
625
|
+
checkCancelFailAndFreeDrag(),
|
|
626
|
+
checkMalformedEventGuard(),
|
|
627
|
+
checkPanEndRouting(),
|
|
628
|
+
checkTapProtocol(),
|
|
629
|
+
checkGlobalTapObserver(),
|
|
630
|
+
checkTapBoundaryGates(),
|
|
631
|
+
checkGestureStateResolver(),
|
|
632
|
+
checkGestureStateBoundary(),
|
|
633
|
+
]
|
|
634
|
+
const allPass = rows.every((row) => row.pass)
|
|
635
|
+
|
|
636
|
+
return (
|
|
637
|
+
<ScrollView style={styles.scroll} contentContainerStyle={styles.container}>
|
|
638
|
+
<Text style={styles.title}>M2.4 gesture checks</Text>
|
|
639
|
+
<Text style={styles.subtitle}>
|
|
640
|
+
binding semantics vs synthetic Pan events + a manual graph
|
|
641
|
+
</Text>
|
|
642
|
+
<View style={[styles.banner, allPass ? styles.pass : styles.fail]}>
|
|
643
|
+
<Text style={styles.bannerText}>{allPass ? 'PASS' : 'FAIL'}</Text>
|
|
644
|
+
</View>
|
|
645
|
+
{rows.map((row) => (
|
|
646
|
+
<View key={row.id} style={styles.row}>
|
|
647
|
+
<Text style={[styles.verdict, row.pass ? styles.ok : styles.bad]}>
|
|
648
|
+
{row.pass ? 'PASS' : 'FAIL'}
|
|
649
|
+
</Text>
|
|
650
|
+
<View style={styles.rowBody}>
|
|
651
|
+
<Text style={styles.rowId}>{row.id}</Text>
|
|
652
|
+
<Text style={styles.rowDetail} numberOfLines={3}>
|
|
653
|
+
{row.detail}
|
|
654
|
+
</Text>
|
|
655
|
+
</View>
|
|
656
|
+
</View>
|
|
657
|
+
))}
|
|
658
|
+
</ScrollView>
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const styles = StyleSheet.create({
|
|
663
|
+
scroll: { flex: 1, backgroundColor: '#0b0b12' },
|
|
664
|
+
container: { paddingVertical: 72, paddingHorizontal: 20 },
|
|
665
|
+
title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
|
|
666
|
+
subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
|
|
667
|
+
banner: {
|
|
668
|
+
alignSelf: 'center',
|
|
669
|
+
paddingHorizontal: 36,
|
|
670
|
+
paddingVertical: 10,
|
|
671
|
+
borderRadius: 10,
|
|
672
|
+
marginTop: 18,
|
|
673
|
+
marginBottom: 10,
|
|
674
|
+
},
|
|
675
|
+
pass: { backgroundColor: '#1d7a3e' },
|
|
676
|
+
fail: { backgroundColor: '#a32633' },
|
|
677
|
+
bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
|
|
678
|
+
row: { flexDirection: 'row', marginTop: 10 },
|
|
679
|
+
verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
|
|
680
|
+
ok: { color: '#4ade80' },
|
|
681
|
+
bad: { color: '#f87171' },
|
|
682
|
+
rowBody: { flex: 1 },
|
|
683
|
+
rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
|
|
684
|
+
rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
|
|
685
|
+
})
|