@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,563 @@
|
|
|
1
|
+
// The gesture-state boundary gate (R6, REQ-API-031; R7 REQ-API-032 adds the label form): an
|
|
2
|
+
// orphan whileDrag (no drag axis) refuses under the R6-F2 native law (the pinned web engine
|
|
3
|
+
// leaves it silently inert — a recorded divergence; this package never ships a silently dead
|
|
4
|
+
// gesture surface); a whileDrag key colliding with the drag axis is a dual-authority refusal
|
|
5
|
+
// (REQ-DRIVER-029); embedded transitions split from the target; the remaining target
|
|
6
|
+
// validates through the SAME core boundary the animate lane rides (REQ-API-013). A LABEL
|
|
7
|
+
// definition resolves against the element's gated dictionary (entries already carry core
|
|
8
|
+
// validity + native drivability from the dictionary gate) into a per-key-attributed layer
|
|
9
|
+
// (law b); the member conflicts (axis, bound XOR) apply identically to both forms.
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
describeValue,
|
|
13
|
+
InvalidTargetError,
|
|
14
|
+
InvalidTransitionError,
|
|
15
|
+
captureTransition,
|
|
16
|
+
flattenVariantApplications,
|
|
17
|
+
hostCapabilities,
|
|
18
|
+
TARGET_PROPERTY_KEYS,
|
|
19
|
+
validateTargetRefusal,
|
|
20
|
+
validateTransitionRefusal,
|
|
21
|
+
type DragAxis,
|
|
22
|
+
type Target,
|
|
23
|
+
type Transition,
|
|
24
|
+
type VariantResolver,
|
|
25
|
+
type VariantsDictionary,
|
|
26
|
+
} from '@unrulysystems/native-motion-core'
|
|
27
|
+
import {
|
|
28
|
+
executableLengthValueRefusal,
|
|
29
|
+
resolveTransitionForKey,
|
|
30
|
+
} from '@unrulysystems/native-motion-core/internal-driver'
|
|
31
|
+
import type { GestureStateLayer } from './gestureStateResolver'
|
|
32
|
+
import {
|
|
33
|
+
isNativeExecutableLengthValue,
|
|
34
|
+
NATIVE_PROPERTY_TRANSITION_OPTIONS,
|
|
35
|
+
nativeMountTargetValueKind,
|
|
36
|
+
} from './shippedSurface'
|
|
37
|
+
import { keyframeTransitionRefusal } from '@unrulysystems/native-motion-core/internal-driver'
|
|
38
|
+
import {
|
|
39
|
+
captureBoundedArray,
|
|
40
|
+
capturedArrayDescription,
|
|
41
|
+
} from '@unrulysystems/native-motion-core/internal-driver'
|
|
42
|
+
import type { StyleValueGate } from './styleValueBinding'
|
|
43
|
+
import type { MotionComponentId } from './motionViewController'
|
|
44
|
+
import { gateLabelDefinition, resolveVariantApplicationsGated } from './variantProps'
|
|
45
|
+
|
|
46
|
+
const THROW_GATE: StyleValueGate = {
|
|
47
|
+
severity: 'development',
|
|
48
|
+
report: () => {},
|
|
49
|
+
componentId: '<Motion.View>',
|
|
50
|
+
}
|
|
51
|
+
const NATIVE_HOST = hostCapabilities('native', ['universal', 'native-extension'])
|
|
52
|
+
const TRANSITION_MAP_KEYS = new Set<string>(['default', 'layout', ...TARGET_PROPERTY_KEYS])
|
|
53
|
+
|
|
54
|
+
function hasTransitionMap(transition: Transition): boolean {
|
|
55
|
+
return Reflect.ownKeys(transition).some(
|
|
56
|
+
(key) => typeof key === 'string' && TRANSITION_MAP_KEYS.has(key),
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function nativePropertyTransitionRefusal(
|
|
61
|
+
componentId: MotionComponentId,
|
|
62
|
+
path: string,
|
|
63
|
+
transition: Transition,
|
|
64
|
+
propertyKeys: readonly string[] = ['__root__'],
|
|
65
|
+
): Error | null {
|
|
66
|
+
const refusalForOption = (
|
|
67
|
+
option: string,
|
|
68
|
+
optionValue: unknown,
|
|
69
|
+
optionPath: string,
|
|
70
|
+
): Error | null =>
|
|
71
|
+
NATIVE_PROPERTY_TRANSITION_OPTIONS.has(option)
|
|
72
|
+
? null
|
|
73
|
+
: new InvalidTransitionError(
|
|
74
|
+
componentId,
|
|
75
|
+
option,
|
|
76
|
+
optionValue,
|
|
77
|
+
optionPath +
|
|
78
|
+
' is not executable by the native property lane (it drives: ' +
|
|
79
|
+
NATIVE_PROPERTY_TRANSITION_OPTIONS.options.join(', ') +
|
|
80
|
+
') — a silently inert option is refused (G-INV-8, shippedSurface.ts)',
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const refusalForFlatTransition = (selected: Transition, selectedPath: string): Error | null => {
|
|
84
|
+
for (const option of Reflect.ownKeys(selected)) {
|
|
85
|
+
if (typeof option !== 'string' || TRANSITION_MAP_KEYS.has(option) || option === 'inherit')
|
|
86
|
+
continue
|
|
87
|
+
const optionValue = (selected as Record<string, unknown>)[option]
|
|
88
|
+
if (optionValue === undefined) continue
|
|
89
|
+
const refusal = refusalForOption(option, optionValue, selectedPath + '.' + option)
|
|
90
|
+
if (refusal !== null) return refusal
|
|
91
|
+
}
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!hasTransitionMap(transition)) return refusalForFlatTransition(transition, path)
|
|
96
|
+
|
|
97
|
+
const source = transition as Record<PropertyKey, unknown>
|
|
98
|
+
const selectors = propertyKeys.length === 0 ? ['__root__'] : propertyKeys
|
|
99
|
+
const seen = new Set<string>()
|
|
100
|
+
for (const propertyKey of selectors) {
|
|
101
|
+
const mapKey =
|
|
102
|
+
Object.hasOwn(source, propertyKey) &&
|
|
103
|
+
source[propertyKey] !== undefined &&
|
|
104
|
+
source[propertyKey] !== null
|
|
105
|
+
? propertyKey
|
|
106
|
+
: Object.hasOwn(source, 'default') && source['default'] !== undefined
|
|
107
|
+
? 'default'
|
|
108
|
+
: 'root'
|
|
109
|
+
if (seen.has(mapKey)) continue
|
|
110
|
+
seen.add(mapKey)
|
|
111
|
+
const selected = resolveTransitionForKey(transition, propertyKey)
|
|
112
|
+
const refusal = refusalForFlatTransition(
|
|
113
|
+
selected,
|
|
114
|
+
mapKey === 'root' ? path : path + '.' + mapKey,
|
|
115
|
+
)
|
|
116
|
+
if (refusal !== null) return refusal
|
|
117
|
+
}
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Capture a caller-owned embedded transition ONCE into a frozen snapshot (review major 0cb829). A malformed
|
|
122
|
+
// SHAPE (primitive, array, prototype-carrying) stays raw so validateTransitionRefusal throws its typed
|
|
123
|
+
// refusal rather than a capture laundering it into an empty `{}`.
|
|
124
|
+
function captureEmbeddedTransition(transition: Transition | undefined): Transition | undefined {
|
|
125
|
+
if (transition === undefined) return undefined
|
|
126
|
+
if (typeof transition !== 'object' || transition === null || Array.isArray(transition)) {
|
|
127
|
+
return transition
|
|
128
|
+
}
|
|
129
|
+
const proto = Object.getPrototypeOf(transition) as unknown
|
|
130
|
+
if (proto !== Object.prototype && proto !== null) return transition
|
|
131
|
+
return captureTransition(transition)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface GestureStatePropsInput {
|
|
135
|
+
readonly whileTap?: unknown
|
|
136
|
+
readonly whileDrag?: unknown
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface AcceptedGestureState {
|
|
140
|
+
whileTap?: GestureStateLayer
|
|
141
|
+
whileDrag?: GestureStateLayer
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface GestureStateGateResult {
|
|
145
|
+
readonly state: AcceptedGestureState
|
|
146
|
+
/** A property selected the element transition before production refused that property. */
|
|
147
|
+
readonly fallbackAttempted: boolean
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function gateGestureStateProps(
|
|
151
|
+
props: GestureStatePropsInput,
|
|
152
|
+
context: {
|
|
153
|
+
readonly dragAxes: readonly DragAxis[]
|
|
154
|
+
/** Style-bound keys (REQ-DRIVER-029 XOR): a bound key can never be gesture-driven. */
|
|
155
|
+
readonly boundKeys?: readonly string[]
|
|
156
|
+
/** Exact direct-public numeric bindings whose identity channel owns declarative collisions. */
|
|
157
|
+
readonly identityEligibleBoundKeys?: readonly string[]
|
|
158
|
+
/** R7: the element's GATED dictionary — label definitions resolve against it (law a). */
|
|
159
|
+
readonly variantsDictionary?: VariantsDictionary | undefined
|
|
160
|
+
/**
|
|
161
|
+
* The element transition's caller-independent snapshot. A gesture member without an attributed or
|
|
162
|
+
* embedded transition executes through this fallback, so its target/timing pair is gated here too.
|
|
163
|
+
*/
|
|
164
|
+
readonly elementTransition?: Transition | undefined
|
|
165
|
+
// T24 B1: the component custom payload for RESOLVER labels in gesture-state slots.
|
|
166
|
+
readonly custom?: unknown
|
|
167
|
+
readonly readValueState?:
|
|
168
|
+
| (() => {
|
|
169
|
+
readonly current: Readonly<Record<string, unknown>>
|
|
170
|
+
readonly velocity: Readonly<Record<string, number>>
|
|
171
|
+
})
|
|
172
|
+
| undefined
|
|
173
|
+
readonly componentId?: MotionComponentId
|
|
174
|
+
},
|
|
175
|
+
gate: StyleValueGate = THROW_GATE,
|
|
176
|
+
): AcceptedGestureState {
|
|
177
|
+
return gateGestureStatePropsWithEvidence(props, context, gate).state
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Keep fallback-attempt evidence beside the accepted layers. The public-shaped layer object remains
|
|
182
|
+
* free of bookkeeping; MotionView alone needs this to avoid a second inert-transition report.
|
|
183
|
+
*/
|
|
184
|
+
export function gateGestureStatePropsWithEvidence(
|
|
185
|
+
props: GestureStatePropsInput,
|
|
186
|
+
context: {
|
|
187
|
+
readonly dragAxes: readonly DragAxis[]
|
|
188
|
+
readonly boundKeys?: readonly string[]
|
|
189
|
+
readonly identityEligibleBoundKeys?: readonly string[]
|
|
190
|
+
readonly variantsDictionary?: VariantsDictionary | undefined
|
|
191
|
+
readonly elementTransition?: Transition | undefined
|
|
192
|
+
// T24 B1: the component custom payload for RESOLVER labels in gesture-state slots.
|
|
193
|
+
readonly custom?: unknown
|
|
194
|
+
readonly readValueState?:
|
|
195
|
+
| (() => {
|
|
196
|
+
readonly current: Readonly<Record<string, unknown>>
|
|
197
|
+
readonly velocity: Readonly<Record<string, number>>
|
|
198
|
+
})
|
|
199
|
+
| undefined
|
|
200
|
+
readonly componentId?: MotionComponentId
|
|
201
|
+
},
|
|
202
|
+
gate: StyleValueGate = THROW_GATE,
|
|
203
|
+
): GestureStateGateResult {
|
|
204
|
+
const componentId = context.componentId ?? '<Motion.View>'
|
|
205
|
+
const accepted: AcceptedGestureState = {}
|
|
206
|
+
let fallbackAttempted = false
|
|
207
|
+
const refuse = (error: Error): void => {
|
|
208
|
+
if (gate.severity === 'development') throw error
|
|
209
|
+
gate.report(error)
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The member conflicts BOTH forms ride (per-KEY granularity, r4 major 20): the drag-axis
|
|
213
|
+
* collision and the bound-key XOR (REQ-DRIVER-029) — the error, or null when the member
|
|
214
|
+
* is conflict-free.
|
|
215
|
+
*/
|
|
216
|
+
const memberConflict = (key: 'whileTap' | 'whileDrag', memberKey: string): Error | null => {
|
|
217
|
+
if (key === 'whileDrag' && context.dragAxes.includes(memberKey as DragAxis)) {
|
|
218
|
+
return new Error(
|
|
219
|
+
`${componentId}: R6 scope: whileDrag names the drag axis key '${memberKey}' — a key has ONE ` +
|
|
220
|
+
'reconciliation authority (REQ-DRIVER-029); the recognizer owns the axis while ' +
|
|
221
|
+
'dragging.',
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
if (
|
|
225
|
+
context.boundKeys?.includes(memberKey) === true &&
|
|
226
|
+
context.identityEligibleBoundKeys?.includes(memberKey) !== true
|
|
227
|
+
) {
|
|
228
|
+
return new Error(
|
|
229
|
+
`${componentId}: R6 scope: '${memberKey}' is style-bound AND a ${key} target key — a key has ONE ` +
|
|
230
|
+
'reconciliation authority (REQ-DRIVER-029); drive it through the binding or the ' +
|
|
231
|
+
'gesture state, never both.',
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
return null
|
|
235
|
+
}
|
|
236
|
+
for (const key of ['whileTap', 'whileDrag'] as const) {
|
|
237
|
+
const value = props[key]
|
|
238
|
+
if (value === undefined) continue
|
|
239
|
+
if (typeof value === 'function' || typeof value === 'string' || Array.isArray(value)) {
|
|
240
|
+
// The LABEL form (R7, law b): normalize the definition, resolve against the gated
|
|
241
|
+
// dictionary (a local miss contributes nothing — law a's propagation-source shape),
|
|
242
|
+
// flatten to per-key attribution, then run the SAME member conflicts the object form
|
|
243
|
+
// runs. Core validity, drivability, and the property-lane option law already ran at
|
|
244
|
+
// the dictionary gate — an entry that failed them never reached this dictionary.
|
|
245
|
+
const labels =
|
|
246
|
+
typeof value === 'function'
|
|
247
|
+
? null
|
|
248
|
+
: gateLabelDefinition(key, value as string | readonly unknown[], gate, componentId)
|
|
249
|
+
if (labels === undefined) continue // production refused the label prop — no layer
|
|
250
|
+
if (key === 'whileDrag' && context.dragAxes.length === 0) {
|
|
251
|
+
refuse(
|
|
252
|
+
new Error(
|
|
253
|
+
`${componentId}: "whileDrag" requires an enabled drag axis — without one the state could never ` +
|
|
254
|
+
'activate (a silently dead gesture surface; R6-F2 native law, recorded ' +
|
|
255
|
+
'divergence from the pinned inertness).',
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
const definition: string | readonly string[] | VariantResolver =
|
|
261
|
+
typeof value === 'function' ? (value as VariantResolver) : labels!
|
|
262
|
+
const hasResolver =
|
|
263
|
+
typeof definition === 'function' ||
|
|
264
|
+
definition.some(
|
|
265
|
+
(label) =>
|
|
266
|
+
context.variantsDictionary !== undefined &&
|
|
267
|
+
Object.hasOwn(context.variantsDictionary, label) &&
|
|
268
|
+
typeof context.variantsDictionary[label] === 'function',
|
|
269
|
+
)
|
|
270
|
+
if (hasResolver) {
|
|
271
|
+
accepted[key] = {
|
|
272
|
+
target: {},
|
|
273
|
+
resolveAtActivation: () => {
|
|
274
|
+
const applications = resolveVariantApplicationsGated(
|
|
275
|
+
definition,
|
|
276
|
+
context.variantsDictionary,
|
|
277
|
+
gate,
|
|
278
|
+
componentId,
|
|
279
|
+
context.custom,
|
|
280
|
+
context.readValueState,
|
|
281
|
+
)
|
|
282
|
+
const synthetic: Record<string, Target & { transition?: Transition }> = {}
|
|
283
|
+
const syntheticLabels: string[] = []
|
|
284
|
+
applications.forEach((application, index) => {
|
|
285
|
+
const label = `__activation_${index}`
|
|
286
|
+
syntheticLabels.push(label)
|
|
287
|
+
synthetic[label] = {
|
|
288
|
+
...application.target,
|
|
289
|
+
...(application.transition === undefined
|
|
290
|
+
? {}
|
|
291
|
+
: { transition: application.transition }),
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
const resolved = gateGestureStatePropsWithEvidence(
|
|
295
|
+
{ [key]: syntheticLabels },
|
|
296
|
+
{ ...context, variantsDictionary: synthetic },
|
|
297
|
+
gate,
|
|
298
|
+
).state[key]
|
|
299
|
+
return resolved ?? { target: {} }
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
const flattened = flattenVariantApplications(
|
|
305
|
+
resolveVariantApplicationsGated(
|
|
306
|
+
definition,
|
|
307
|
+
context.variantsDictionary,
|
|
308
|
+
gate,
|
|
309
|
+
componentId,
|
|
310
|
+
context.custom,
|
|
311
|
+
context.readValueState,
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
const labelTarget: Record<string, unknown> = {}
|
|
315
|
+
const labelTransitions: Record<string, Transition> = {}
|
|
316
|
+
for (const [memberKey, memberValue] of Object.entries(flattened.target)) {
|
|
317
|
+
const conflict = memberConflict(key, memberKey)
|
|
318
|
+
if (conflict !== null) {
|
|
319
|
+
refuse(conflict)
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
// The label lane runs the SAME native-depth executable matrix as the object lane
|
|
323
|
+
// (review 91d41675d511): a label-resolved scalar or array element outside the matrix
|
|
324
|
+
// refuses at the supplying boundary, never latently at activation.
|
|
325
|
+
if (nativeMountTargetValueKind(memberKey) !== 'color') {
|
|
326
|
+
const labelLengthRefusal = executableLengthValueRefusal(memberKey, memberValue)
|
|
327
|
+
if (labelLengthRefusal !== null) {
|
|
328
|
+
refuse(
|
|
329
|
+
new Error(
|
|
330
|
+
`${componentId}: R6 scope: ${key}.${memberKey} received ${describeValue(memberValue)} — ` +
|
|
331
|
+
`${labelLengthRefusal} (REQ-API-031).`,
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// A resolved label's per-key attribution wins; otherwise it inherits the element transition
|
|
338
|
+
// just like the object form. Validate the EFFECTIVE transition before constructing the resolver
|
|
339
|
+
// so a timing mismatch cannot wait until gesture activation.
|
|
340
|
+
let attributed: Transition | undefined = flattened.transitions[memberKey]
|
|
341
|
+
// The SAME property-lane option law the object form runs on its embedded transition
|
|
342
|
+
// (review r2 major 1): the dictionary gate ADMITS the tree-lane orchestration options
|
|
343
|
+
// (they are legal on the animate lane, where the channel executes them), so a
|
|
344
|
+
// gesture-pulled label's attribution must refuse them HERE — gesture-lane wave
|
|
345
|
+
// orchestration is an unshipped REQ-API-053 deferred sibling, and an admitted key
|
|
346
|
+
// would ride a retarget into the property lane. Production drops the ATTRIBUTION only
|
|
347
|
+
// (the member survives on the element fallback, which the resolver supplies
|
|
348
|
+
// driver-clean) — mirroring the object form's transition-drop granularity.
|
|
349
|
+
if (attributed !== undefined) {
|
|
350
|
+
const attributionRefusal = nativePropertyTransitionRefusal(
|
|
351
|
+
componentId,
|
|
352
|
+
key + '.' + memberKey,
|
|
353
|
+
attributed,
|
|
354
|
+
[memberKey],
|
|
355
|
+
)
|
|
356
|
+
if (attributionRefusal !== null) {
|
|
357
|
+
refuse(attributionRefusal)
|
|
358
|
+
attributed = undefined
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const effectiveTransition =
|
|
362
|
+
attributed !== undefined
|
|
363
|
+
? resolveTransitionForKey(attributed, memberKey)
|
|
364
|
+
: context.elementTransition === undefined
|
|
365
|
+
? undefined
|
|
366
|
+
: resolveTransitionForKey(context.elementTransition, memberKey)
|
|
367
|
+
// Evidence reads the LOCAL attribution: a production-dropped orchestration-bearing
|
|
368
|
+
// attribution really does consume the element fallback (the object form's drop path
|
|
369
|
+
// reaches the same conclusion through its own local `transition`).
|
|
370
|
+
if (attributed === undefined && context.elementTransition !== undefined) {
|
|
371
|
+
fallbackAttempted = true
|
|
372
|
+
}
|
|
373
|
+
const timingRefusal = keyframeTransitionRefusal(
|
|
374
|
+
componentId,
|
|
375
|
+
memberKey,
|
|
376
|
+
memberValue,
|
|
377
|
+
effectiveTransition,
|
|
378
|
+
)
|
|
379
|
+
if (timingRefusal !== null) {
|
|
380
|
+
refuse(timingRefusal)
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
labelTarget[memberKey] = memberValue
|
|
384
|
+
if (attributed !== undefined) labelTransitions[memberKey] = effectiveTransition!
|
|
385
|
+
}
|
|
386
|
+
accepted[key] = {
|
|
387
|
+
target: labelTarget as Target,
|
|
388
|
+
...(Object.keys(labelTransitions).length === 0 ? {} : { transitions: labelTransitions }),
|
|
389
|
+
}
|
|
390
|
+
continue
|
|
391
|
+
}
|
|
392
|
+
// PLAIN targets only (r3 major 16): a prototype-carrying object (new Date(0)) destructures
|
|
393
|
+
// to an empty target and would mount a silently dead gesture layer.
|
|
394
|
+
const proto =
|
|
395
|
+
typeof value === 'object' && value !== null ? Object.getPrototypeOf(value) : undefined
|
|
396
|
+
if (
|
|
397
|
+
typeof value !== 'object' ||
|
|
398
|
+
value === null ||
|
|
399
|
+
(proto !== Object.prototype && proto !== null)
|
|
400
|
+
) {
|
|
401
|
+
refuse(
|
|
402
|
+
new Error(
|
|
403
|
+
`${componentId}: "${key}" must be a plain target object, received ${value === null ? 'null' : typeof value} (REQ-API-031).`,
|
|
404
|
+
),
|
|
405
|
+
)
|
|
406
|
+
continue
|
|
407
|
+
}
|
|
408
|
+
if (key === 'whileDrag' && context.dragAxes.length === 0) {
|
|
409
|
+
refuse(
|
|
410
|
+
new Error(
|
|
411
|
+
`${componentId}: "whileDrag" requires an enabled drag axis — without one the state could never ` +
|
|
412
|
+
'activate (a silently dead gesture surface; R6-F2 native law, recorded divergence ' +
|
|
413
|
+
'from the pinned inertness).',
|
|
414
|
+
),
|
|
415
|
+
)
|
|
416
|
+
continue
|
|
417
|
+
}
|
|
418
|
+
// Read `value.transition` EXACTLY ONCE via the destructure (review major a13e69f) — re-reading it for
|
|
419
|
+
// the capture would observe an outer-property accessor twice. Capture the embedded transition ONCE
|
|
420
|
+
// (review major 0cb829): it is validated here AND stored in the accepted layer that gesture activation
|
|
421
|
+
// later executes — a caller-owned accessor validated as `0.2` must not mutate to `-1` after this render
|
|
422
|
+
// and change what the gesture forwards. A malformed SHAPE stays raw so validateTransitionRefusal throws
|
|
423
|
+
// its typed refusal (a capture would launder it).
|
|
424
|
+
const { transition: rawTransition, ...target } = value as Target & { transition?: Transition }
|
|
425
|
+
let transition = captureEmbeddedTransition(rawTransition)
|
|
426
|
+
// The embedded transition is runtime-validated like any other member (r4 major 21,
|
|
427
|
+
// G-INV-8): `transition: null` must never ride a nullish fallback into "absent".
|
|
428
|
+
// Production drops the malformed member with a named report; the target survives.
|
|
429
|
+
// Beyond core's vocabulary/range law, the NATIVE property lane executes only its own
|
|
430
|
+
// option set (r5 major d6c3850af147): `delay` is rebased by the property driver and
|
|
431
|
+
// `velocity` seeds the spring since T21 — the surviving refusal exemplar is a tree-lane
|
|
432
|
+
// orchestration key, which this lane never reads and must never ride as a silent no-op.
|
|
433
|
+
// RETURN-shaped, no catch (M3 r8 major b9eb82c0d429): only a returned/constructed
|
|
434
|
+
// refusal is refused; a dependency fault thrown through core validation propagates
|
|
435
|
+
// with its exact identity.
|
|
436
|
+
const transitionRefusal = validateTransitionRefusal(transition, {
|
|
437
|
+
componentId,
|
|
438
|
+
})
|
|
439
|
+
if (transitionRefusal !== null) {
|
|
440
|
+
refuse(transitionRefusal)
|
|
441
|
+
transition = undefined
|
|
442
|
+
} else if (transition !== undefined) {
|
|
443
|
+
const nativeTransitionRefusal = nativePropertyTransitionRefusal(
|
|
444
|
+
componentId,
|
|
445
|
+
key + '.transition',
|
|
446
|
+
transition,
|
|
447
|
+
Object.keys(target),
|
|
448
|
+
)
|
|
449
|
+
if (nativeTransitionRefusal !== null) {
|
|
450
|
+
refuse(nativeTransitionRefusal)
|
|
451
|
+
transition = undefined
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// PER-KEY validation (r3 major 16) mirroring the shared severity law's granularity: core
|
|
455
|
+
// validateTarget AND the native drivability law per member — development throws on the
|
|
456
|
+
// first offender, production reports it and KEEPS the valid members. The drivability check
|
|
457
|
+
// exists because a unit string (x: '10px') is core-valid but this rung's native lanes
|
|
458
|
+
// drive numbers (rgba keys ride the color lane) — accepting it would mount a dead value.
|
|
459
|
+
const acceptedTarget: Record<string, unknown> = {}
|
|
460
|
+
for (const [memberKey, rawMember] of Object.entries(target)) {
|
|
461
|
+
// Per-KEY refusal (r4 major 20; the bound XOR strips HERE, r3 major 17): the conflict
|
|
462
|
+
// drops this member only — valid siblings survive, the severity law's granularity.
|
|
463
|
+
const conflict = memberConflict(key, memberKey)
|
|
464
|
+
if (conflict !== null) {
|
|
465
|
+
refuse(conflict)
|
|
466
|
+
continue
|
|
467
|
+
}
|
|
468
|
+
// Materialize a keyframe ARRAY member into a frozen snapshot ONCE, before validation (review
|
|
469
|
+
// major 40, the r3-major-16 one-read law): an accessor-backed array could return a valid member
|
|
470
|
+
// to the validator and a different value (null) when the controller later reads it. The single
|
|
471
|
+
// frozen snapshot is the ONE truth validated AND forwarded — a scalar is already immutable.
|
|
472
|
+
let memberValue: unknown = rawMember
|
|
473
|
+
if (Array.isArray(rawMember)) {
|
|
474
|
+
const capture = captureBoundedArray(rawMember)
|
|
475
|
+
if (capture.kind !== 'captured') {
|
|
476
|
+
refuse(
|
|
477
|
+
new InvalidTargetError(
|
|
478
|
+
componentId,
|
|
479
|
+
memberKey,
|
|
480
|
+
capturedArrayDescription(capture),
|
|
481
|
+
'keyframe arrays must have a safe bounded length (REQ-API-033)',
|
|
482
|
+
),
|
|
483
|
+
)
|
|
484
|
+
continue
|
|
485
|
+
}
|
|
486
|
+
memberValue = capture.values
|
|
487
|
+
}
|
|
488
|
+
const memberRefusal = validateTargetRefusal(
|
|
489
|
+
{ [memberKey]: memberValue } as Target,
|
|
490
|
+
NATIVE_HOST,
|
|
491
|
+
{
|
|
492
|
+
componentId,
|
|
493
|
+
},
|
|
494
|
+
)
|
|
495
|
+
if (memberRefusal !== null) {
|
|
496
|
+
refuse(memberRefusal)
|
|
497
|
+
continue
|
|
498
|
+
}
|
|
499
|
+
const valueKind = nativeMountTargetValueKind(memberKey)
|
|
500
|
+
// Both lanes drive a scalar OR a keyframe ARRAY (REQ-API-033 on the gesture-state surface, majors
|
|
501
|
+
// 31 + 35): core validation already proved a well-formed same-value-type array, and the
|
|
502
|
+
// value-agnostic resolver + array-aware controller drive it — the color lane through the progress
|
|
503
|
+
// projection (projectColorSequence). Length strings ride the ONE key-aware executable matrix
|
|
504
|
+
// element-wise (review 91d41675d511): positional keys, auto for width/height only.
|
|
505
|
+
const lengthRefusal =
|
|
506
|
+
valueKind === 'color' ? null : executableLengthValueRefusal(memberKey, memberValue)
|
|
507
|
+
const drivable =
|
|
508
|
+
valueKind === 'color'
|
|
509
|
+
? typeof memberValue === 'string' || Array.isArray(memberValue)
|
|
510
|
+
: isNativeExecutableLengthValue(memberKey, memberValue)
|
|
511
|
+
if (!drivable) {
|
|
512
|
+
refuse(
|
|
513
|
+
new Error(
|
|
514
|
+
`${componentId}: R6 scope: ${key}.${memberKey} received ${describeValue(memberValue)} — ${
|
|
515
|
+
lengthRefusal ??
|
|
516
|
+
`the native ${valueKind} lane drives ${
|
|
517
|
+
valueKind === 'color'
|
|
518
|
+
? 'color strings or a color keyframe array'
|
|
519
|
+
: 'numbers, numeric keyframe arrays, or measure-resolved positional lengths'
|
|
520
|
+
}; the supplied value is not executable (REQ-API-031).`
|
|
521
|
+
}`,
|
|
522
|
+
),
|
|
523
|
+
)
|
|
524
|
+
continue
|
|
525
|
+
}
|
|
526
|
+
// R8-F1 at the SUPPLYING boundary (review major 42): a keyframe array member under an EMBEDDED
|
|
527
|
+
// spring interpolates exactly two keyframes — a longer array is refused EAGERLY here (production
|
|
528
|
+
// reports + drops the member, siblings survive) instead of an uncaught controller throw on
|
|
529
|
+
// activation. An array with NO embedded spring rides the element-transition fallback, refused by
|
|
530
|
+
// the controller's own severity-aware compat filter.
|
|
531
|
+
const effectiveTransition =
|
|
532
|
+
transition !== undefined
|
|
533
|
+
? resolveTransitionForKey(transition, memberKey)
|
|
534
|
+
: context.elementTransition === undefined
|
|
535
|
+
? undefined
|
|
536
|
+
: resolveTransitionForKey(context.elementTransition, memberKey)
|
|
537
|
+
// A supplied embedded transition owns this member even when it is later refused. Only a member
|
|
538
|
+
// authored without one attempts the element fallback.
|
|
539
|
+
// A malformed embedded transition is refused before this member is evaluated. If the
|
|
540
|
+
// surviving target then reaches the element fallback, it is a real fallback attempt just
|
|
541
|
+
// like an omitted embedded transition; raw authorship is not execution ownership.
|
|
542
|
+
if (transition === undefined && context.elementTransition !== undefined) {
|
|
543
|
+
fallbackAttempted = true
|
|
544
|
+
}
|
|
545
|
+
// R8 M2-B (REQ-API-033 law d): validate this layer's EMBEDDED keyframe timing. Shape-valid
|
|
546
|
+
// count mismatches use the pin's offsets/easing fallback; keyframe-only timing on a scalar
|
|
547
|
+
// remains inert and refuses. Report + drop per member under severity; siblings survive.
|
|
548
|
+
const timingRefusal = keyframeTransitionRefusal(
|
|
549
|
+
componentId,
|
|
550
|
+
memberKey,
|
|
551
|
+
memberValue,
|
|
552
|
+
effectiveTransition,
|
|
553
|
+
)
|
|
554
|
+
if (timingRefusal !== null) {
|
|
555
|
+
refuse(timingRefusal)
|
|
556
|
+
continue
|
|
557
|
+
}
|
|
558
|
+
acceptedTarget[memberKey] = memberValue
|
|
559
|
+
}
|
|
560
|
+
accepted[key] = { target: acceptedTarget as Target, transition }
|
|
561
|
+
}
|
|
562
|
+
return { state: accepted, fallbackAttempted }
|
|
563
|
+
}
|