@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,250 @@
|
|
|
1
|
+
// REQ-API-022 (FLAG 5a, specs/SPEC-GESTURE.md) — MotionRoot, the public root
|
|
2
|
+
// primitive. On native it OWNS the platform gesture root (GestureHandlerRootView — consumers
|
|
3
|
+
// never import the substrate, the dogfood ban law); the web entry's counterpart renders children
|
|
4
|
+
// unchanged, keeping the one-import universal surface. A gesture-carrying View mounted outside
|
|
5
|
+
// this root would silently never receive touches — the context lets the component fail loud
|
|
6
|
+
// instead (gateMotionRoot), per the severity law.
|
|
7
|
+
//
|
|
8
|
+
// R15 D3 (REQ-API-043 / packet Finding 8): also hosts the hub touch stream — a manual-activation
|
|
9
|
+
// pan spanning the root that records touches and drives hub-originated drag sessions so sibling
|
|
10
|
+
// handles work (the pin's window-level PanSession analog).
|
|
11
|
+
|
|
12
|
+
import { createContext, useContext, useMemo, useRef } from 'react'
|
|
13
|
+
import type { ViewStyle } from 'react-native'
|
|
14
|
+
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
|
|
15
|
+
import { runNativeEventCallback, useWorkletDriver } from '../driver/workletDriver'
|
|
16
|
+
import { createDragHubStream, type DragHubStream } from './dragHubStream'
|
|
17
|
+
import { createLayoutIdentityBinding, type LayoutIdentityBinding } from './layoutIdBinding'
|
|
18
|
+
import type { MotionComponentId, NativeSeverity } from './motionViewController'
|
|
19
|
+
|
|
20
|
+
// Null outside a MotionRoot: gesture props use the presence bit, while layoutId Views consume the
|
|
21
|
+
// binding itself. One provider value therefore owns both root-scoped capabilities without parallel
|
|
22
|
+
// root contexts that could skew under nested roots.
|
|
23
|
+
interface MotionRootContextValue {
|
|
24
|
+
readonly layoutIdentity: LayoutIdentityBinding
|
|
25
|
+
readonly subscribeLayoutIdentitySettle: (id: string, listener: () => void) => () => void
|
|
26
|
+
/** R15 D3: the hub touch stream for imperative dragControls.start (Finding 8). */
|
|
27
|
+
readonly dragHubStream: DragHubStream
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const MotionRootContext = createContext<MotionRootContextValue | null>(null)
|
|
31
|
+
|
|
32
|
+
export function useInsideMotionRoot(): boolean {
|
|
33
|
+
return useContext(MotionRootContext) !== null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useLayoutIdentityRootBinding(): LayoutIdentityBinding | null {
|
|
37
|
+
return useContext(MotionRootContext)?.layoutIdentity ?? null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Projection geometry must live on ONE forest under MotionRoot so layoutId's identity driver
|
|
41
|
+
// (writeHolder → setDirectProjection) and host-frame feeds share the same tree. Without this,
|
|
42
|
+
// the first non-layoutId Motion ancestor opens a private useWorkletDriver() forest and a nested
|
|
43
|
+
// layoutId child's projectionComponentId is never registered where the root frame publishes it.
|
|
44
|
+
export function useLayoutIdentityProjectionBinding():
|
|
45
|
+
| LayoutIdentityBinding['projectionBinding']
|
|
46
|
+
| null {
|
|
47
|
+
return useContext(MotionRootContext)?.layoutIdentity.projectionBinding ?? null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// layoutId flights run on MotionRoot's shared driver, while each host owns its ordinary property
|
|
51
|
+
// driver. This subscription delivers the shared driver's PER-IDENTITY settle edge — keyed by the
|
|
52
|
+
// identity id, independent of unrelated driver work — to the mounted host that initiated an
|
|
53
|
+
// identity flight; it never polls or crosses the UI runtime per frame.
|
|
54
|
+
export function useLayoutIdentitySettleSubscription():
|
|
55
|
+
| ((id: string, listener: () => void) => () => void)
|
|
56
|
+
| null {
|
|
57
|
+
return useContext(MotionRootContext)?.subscribeLayoutIdentitySettle ?? null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** R15 D3: the hub stream for DragControls.start correlation (null outside MotionRoot). */
|
|
61
|
+
export function useDragHubStream(): DragHubStream | null {
|
|
62
|
+
return useContext(MotionRootContext)?.dragHubStream ?? null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The REQ-API-022 gate, pure so the suite pins it: true = proceed; development throws naming
|
|
66
|
+
// MotionRoot; production reports and refuses (the caller drops the gesture wiring, fail-closed).
|
|
67
|
+
export function gateMotionRoot(
|
|
68
|
+
insideRoot: boolean,
|
|
69
|
+
severity: NativeSeverity,
|
|
70
|
+
report: (error: Error) => void,
|
|
71
|
+
componentId: MotionComponentId = '<Motion.View>',
|
|
72
|
+
): boolean {
|
|
73
|
+
if (insideRoot) return true
|
|
74
|
+
const error = new Error(
|
|
75
|
+
`${componentId}: gesture props need a <MotionRoot> above them: MotionRoot owns the platform gesture root ` +
|
|
76
|
+
"(REQ-API-022). Wrap your app (or the gesture subtree) in MotionRoot from '@unrulysystems/native-motion'.",
|
|
77
|
+
)
|
|
78
|
+
if (severity === 'development') throw error
|
|
79
|
+
report(error)
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Children only — deliberately NO style knob on either entry: the web counterpart renders no
|
|
84
|
+
// element, so a style prop would be web-ignored (an accepted-but-ignored prop, REQ-API-016), and
|
|
85
|
+
// no example needs root styling. A layout knob can land ADDITIVELY when an example demands it.
|
|
86
|
+
export interface MotionRootProps {
|
|
87
|
+
readonly children?: React.ReactNode
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// GestureHandlerRootView must span the gesture area; flex: 1 is the substrate's own documented
|
|
91
|
+
// expectation for an app-root mount.
|
|
92
|
+
const ROOT_FILL: ViewStyle = { flex: 1 }
|
|
93
|
+
|
|
94
|
+
export function MotionRoot({ children }: MotionRootProps) {
|
|
95
|
+
// The registry lives in the driver's UI-runtime layout slot. Creating this driver here makes the
|
|
96
|
+
// registry identity root-scoped: every descendant layoutId View exchanges through this ONE
|
|
97
|
+
// binding, while each View keeps its existing private driver for ordinary animated properties.
|
|
98
|
+
const driver = useWorkletDriver()
|
|
99
|
+
const valueRef = useRef<MotionRootContextValue | null>(null)
|
|
100
|
+
if (valueRef.current === null) {
|
|
101
|
+
valueRef.current = {
|
|
102
|
+
layoutIdentity: createLayoutIdentityBinding(driver),
|
|
103
|
+
subscribeLayoutIdentitySettle: (id, listener) => driver.layout.subscribeSettled(id, listener),
|
|
104
|
+
dragHubStream: createDragHubStream(),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const stream = valueRef.current.dragHubStream
|
|
108
|
+
|
|
109
|
+
// R15 D3 hub pan: manual-activation, simultaneous with everything (never blocks scroll/child
|
|
110
|
+
// gestures), records touches, activates only when a hub start has armed the stream.
|
|
111
|
+
const hubPan = useMemo(
|
|
112
|
+
() =>
|
|
113
|
+
Gesture.Pan()
|
|
114
|
+
.runOnJS(true)
|
|
115
|
+
.manualActivation(true)
|
|
116
|
+
// Unarmed touches are never activated — child/scroll recognizers keep the stream.
|
|
117
|
+
.onTouchesDown((event, manager) => {
|
|
118
|
+
runNativeEventCallback(() => {
|
|
119
|
+
for (const touch of event.allTouches) {
|
|
120
|
+
stream.recordTouch({
|
|
121
|
+
id: touch.id,
|
|
122
|
+
absoluteX: touch.absoluteX,
|
|
123
|
+
absoluteY: touch.absoluteY,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
// U7d tap observation rides CHANGED touches — one notification per physical down
|
|
127
|
+
// (allTouches re-reports already-active touches on every subsequent down).
|
|
128
|
+
for (const touch of event.changedTouches) {
|
|
129
|
+
stream.notifyTapDown({
|
|
130
|
+
id: touch.id,
|
|
131
|
+
absoluteX: touch.absoluteX,
|
|
132
|
+
absoluteY: touch.absoluteY,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
// Stay inert unless already armed by a hub start between down and move.
|
|
136
|
+
if (stream.armed === null) {
|
|
137
|
+
// Do not activate — leave the touch for child/scroll recognizers.
|
|
138
|
+
void manager
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
.onTouchesMove((event, manager) => {
|
|
143
|
+
runNativeEventCallback(() => {
|
|
144
|
+
const armed = stream.armed
|
|
145
|
+
if (armed === null) return
|
|
146
|
+
// Route ONLY the correlated touchId (seal review r3 — multi-touch ownership).
|
|
147
|
+
// Synthetic arm id -1 also accepts any touch within correlation (rebound on
|
|
148
|
+
// recordTouch; seal review r4 major).
|
|
149
|
+
const touch =
|
|
150
|
+
armed.touchId === -1
|
|
151
|
+
? (event.allTouches[0] ?? event.changedTouches[0])
|
|
152
|
+
: (event.allTouches.find((t) => t.id === armed.touchId) ??
|
|
153
|
+
event.changedTouches.find((t) => t.id === armed.touchId))
|
|
154
|
+
if (touch === undefined) return
|
|
155
|
+
// Activate the recognizer so onUpdate/onEnd own velocity (REQ-GESTURE-014).
|
|
156
|
+
// Once activated, ONLY the recognizer .onUpdate feed drives the session — never
|
|
157
|
+
// dual-feed onTouchesMove + onUpdate (seal review r4 major / Finding 8).
|
|
158
|
+
if (!armed.activated) {
|
|
159
|
+
manager.activate()
|
|
160
|
+
// Pre-activation bridge: one absolute sample so distanceThreshold can clear
|
|
161
|
+
// before the first .onUpdate (same-frame Pressable arm race).
|
|
162
|
+
stream.onPanUpdate({
|
|
163
|
+
absoluteX: touch.absoluteX,
|
|
164
|
+
absoluteY: touch.absoluteY,
|
|
165
|
+
translationX: touch.absoluteX - armed.originAbsoluteX,
|
|
166
|
+
translationY: touch.absoluteY - armed.originAbsoluteY,
|
|
167
|
+
velocityX: 0,
|
|
168
|
+
velocityY: 0,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
.onTouchesUp((event) => {
|
|
174
|
+
runNativeEventCallback(() => {
|
|
175
|
+
// RNGH Android delivers onTouchesUp BEFORE the handler processes the up event
|
|
176
|
+
// (GestureHandlerOrchestrator), so onEnd still follows with the real velocity
|
|
177
|
+
// (REQ-GESTURE-014). Never end/finalize here when activated — that would zero
|
|
178
|
+
// velocity and disarm before onEnd (seal review r2 major).
|
|
179
|
+
// Pre-activation: cancel when the CORRELATED touch lifts (not a second finger).
|
|
180
|
+
// Synthetic arm id -1 (Pressable before recordTouch rebind): any lift correlates —
|
|
181
|
+
// real changedTouches never carry id -1 (adversarial seal Issue 3).
|
|
182
|
+
const armed = stream.armed
|
|
183
|
+
const correlatedUp =
|
|
184
|
+
armed !== null &&
|
|
185
|
+
event.changedTouches.length > 0 &&
|
|
186
|
+
(armed.touchId === -1 || event.changedTouches.some((t) => t.id === armed.touchId))
|
|
187
|
+
if (armed !== null && !armed.activated && correlatedUp) {
|
|
188
|
+
stream.terminate(false)
|
|
189
|
+
}
|
|
190
|
+
for (const touch of event.changedTouches) {
|
|
191
|
+
stream.notifyTapUp({
|
|
192
|
+
id: touch.id,
|
|
193
|
+
absoluteX: touch.absoluteX,
|
|
194
|
+
absoluteY: touch.absoluteY,
|
|
195
|
+
})
|
|
196
|
+
stream.forgetTouch(touch.id)
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
.onTouchesCancelled((event) => {
|
|
201
|
+
runNativeEventCallback(() => {
|
|
202
|
+
// Cancel when the correlated touch is cancelled. Synthetic -1: any cancel
|
|
203
|
+
// correlates (same as up; Issue 3).
|
|
204
|
+
const armed = stream.armed
|
|
205
|
+
const correlated =
|
|
206
|
+
armed !== null &&
|
|
207
|
+
event.changedTouches.length > 0 &&
|
|
208
|
+
(armed.touchId === -1 || event.changedTouches.some((t) => t.id === armed.touchId))
|
|
209
|
+
if (armed !== null && correlated) {
|
|
210
|
+
stream.terminate(false)
|
|
211
|
+
}
|
|
212
|
+
for (const touch of event.changedTouches) {
|
|
213
|
+
// U7d: the touch-cancel path is the global press's ONLY cancel source (the R11
|
|
214
|
+
// touch-steal mapping, reproduced through the stream's own cancel event — F1).
|
|
215
|
+
stream.notifyTapCancel({
|
|
216
|
+
id: touch.id,
|
|
217
|
+
absoluteX: touch.absoluteX,
|
|
218
|
+
absoluteY: touch.absoluteY,
|
|
219
|
+
})
|
|
220
|
+
stream.forgetTouch(touch.id)
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
// Recognizer lifecycle owns velocity when ACTIVE (REQ-GESTURE-014 preferred path).
|
|
225
|
+
.onStart((event) => runNativeEventCallback(() => stream.onPanStart(event)))
|
|
226
|
+
.onUpdate((event) => runNativeEventCallback(() => stream.onPanUpdate(event)))
|
|
227
|
+
.onEnd((event, success) => runNativeEventCallback(() => stream.onPanEnd(event, success)))
|
|
228
|
+
.onFinalize((event, success) =>
|
|
229
|
+
runNativeEventCallback(() => stream.onPanFinalize(event, success)),
|
|
230
|
+
),
|
|
231
|
+
[stream],
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
// RNGH GestureDetector requires exactly one child element (Wrap.tsx Children.only).
|
|
235
|
+
// MotionRootProps still accepts arbitrary ReactNode — wrap so empty/multi-child trees
|
|
236
|
+
// match the web root (no public API restriction) without crashing (review seal major).
|
|
237
|
+
// Nested GestureHandlerRootView is a single native host for the detector; mocks already
|
|
238
|
+
// provide it as a passthrough (no react-native View import required in this module).
|
|
239
|
+
return (
|
|
240
|
+
<MotionRootContext.Provider value={valueRef.current}>
|
|
241
|
+
<GestureHandlerRootView style={ROOT_FILL}>
|
|
242
|
+
<GestureDetector gesture={hubPan}>
|
|
243
|
+
<GestureHandlerRootView style={ROOT_FILL} collapsable={false}>
|
|
244
|
+
{children}
|
|
245
|
+
</GestureHandlerRootView>
|
|
246
|
+
</GestureDetector>
|
|
247
|
+
</GestureHandlerRootView>
|
|
248
|
+
</MotionRootContext.Provider>
|
|
249
|
+
)
|
|
250
|
+
}
|