@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,598 @@
|
|
|
1
|
+
// The NATIVE component-boundary gate for the R7 variants surface (REQ-API-032 law f):
|
|
2
|
+
// outer shapes refuse AS A UNIT at the supplying render — `variants` a plain object,
|
|
3
|
+
// `inherit` a boolean, a label ARRAY string-only (a non-string member names its index) —
|
|
4
|
+
// and every dictionary entry validates eagerly through core's entry law (shape, member
|
|
5
|
+
// validity, the embedded-transition law, the dynamic-form refusal) PLUS the native depth
|
|
6
|
+
// the R6 gesture gate pins: member drivability (the numeric/color lanes drive numbers and
|
|
7
|
+
// color strings — a core-valid unit string is native-undrivable) and the property-lane
|
|
8
|
+
// option set (an option the lane never reads — e.g. a tree-lane orchestration key — must
|
|
9
|
+
// refuse loud, never ride as a silent no-op; G-INV-8; `velocity` left this class at T21,
|
|
10
|
+
// the lane seeds springs from it). Development throws; production reports once and refuses the
|
|
11
|
+
// offending UNIT — the whole prop for outer shapes, the single ENTRY within a valid
|
|
12
|
+
// dictionary — while the valid remainder survives (the severity law's granularity).
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
describeValue,
|
|
16
|
+
hostCapabilities,
|
|
17
|
+
isDiscreteKeyword,
|
|
18
|
+
ORCHESTRATION_OPTION_KEYS,
|
|
19
|
+
parseValue,
|
|
20
|
+
TARGET_PROPERTY_KEYS,
|
|
21
|
+
InvalidTransitionError,
|
|
22
|
+
InvalidVariantError,
|
|
23
|
+
resolveInitialOverlay,
|
|
24
|
+
resolveVariantDefinition,
|
|
25
|
+
validateVariantEntrySnapshot,
|
|
26
|
+
variantsShapeRefusal,
|
|
27
|
+
type Target,
|
|
28
|
+
type VariantApplication,
|
|
29
|
+
type VariantEntry,
|
|
30
|
+
type VariantResolver,
|
|
31
|
+
type Transition,
|
|
32
|
+
type VariantsDictionary,
|
|
33
|
+
} from '@unrulysystems/native-motion-core'
|
|
34
|
+
import {
|
|
35
|
+
executableLengthValueRefusal,
|
|
36
|
+
resolveVariantDefinitionResult,
|
|
37
|
+
} from '@unrulysystems/native-motion-core/internal-driver'
|
|
38
|
+
import {
|
|
39
|
+
isNativeExecutableLengthValue,
|
|
40
|
+
NATIVE_PROPERTY_TRANSITION_OPTIONS,
|
|
41
|
+
nativeMountTargetValueKind,
|
|
42
|
+
} from './shippedSurface'
|
|
43
|
+
import {
|
|
44
|
+
captureBoundedArray,
|
|
45
|
+
capturedArrayDescription,
|
|
46
|
+
keyframeTransitionRefusal,
|
|
47
|
+
resolveTransitionForKey,
|
|
48
|
+
} from '@unrulysystems/native-motion-core/internal-driver'
|
|
49
|
+
import type { StyleValueGate } from './styleValueBinding'
|
|
50
|
+
import type { MotionComponentId } from './motionViewController'
|
|
51
|
+
|
|
52
|
+
const NATIVE_HOST = hostCapabilities('native', ['universal', 'native-extension'])
|
|
53
|
+
const TRANSITION_MAP_KEYS = new Set<string>(['default', 'layout', ...TARGET_PROPERTY_KEYS])
|
|
54
|
+
|
|
55
|
+
/** The label (variant) form of a declarative prop: a string, or an array of strings. */
|
|
56
|
+
export function isVariantLabelForm(value: unknown): value is string | readonly string[] {
|
|
57
|
+
return typeof value === 'string' || Array.isArray(value)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The pin's runtime-only direct function form plus its public label form. */
|
|
61
|
+
export function isRuntimeVariantDefinitionForm(
|
|
62
|
+
value: unknown,
|
|
63
|
+
): value is string | readonly string[] | VariantResolver {
|
|
64
|
+
return typeof value === 'function' || isVariantLabelForm(value)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Law (g): the target keys a dictionary names — the mount-known data that joins the mounted
|
|
69
|
+
* key set regardless of which label (if any) is active at mount.
|
|
70
|
+
*/
|
|
71
|
+
export function dictionaryTargetKeys(dictionary: VariantsDictionary | undefined): string[] {
|
|
72
|
+
if (dictionary === undefined) return []
|
|
73
|
+
return [
|
|
74
|
+
...new Set(
|
|
75
|
+
Object.values(dictionary).flatMap((entry) =>
|
|
76
|
+
Object.entries(entry).flatMap(([key, value]) => {
|
|
77
|
+
if (key === 'transition') return []
|
|
78
|
+
// T23 B3b: the transitionEnd CARRIER is structural, never an animated key — but its
|
|
79
|
+
// SUB-keys are mount-registered animated keys (a value must be mounted to jump).
|
|
80
|
+
if (key === 'transitionEnd') {
|
|
81
|
+
return typeof value === 'object' && value !== null ? Object.keys(value) : []
|
|
82
|
+
}
|
|
83
|
+
return [key]
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* T23 B3b: the EXIT lane is not chartered for settle jumps — T23 B3 charters the
|
|
92
|
+
* animate-lane jump; an exit jump precedes removal. A carrier on an exit target refuses
|
|
93
|
+
* loud naming the deferred lane (development throws; production reports and DROPS the
|
|
94
|
+
* member — sibling exit keys survive). Non-object shapes pass through untouched: the exit
|
|
95
|
+
* shape gates downstream own those refusals.
|
|
96
|
+
*/
|
|
97
|
+
export function gateExitTransitionEnd(target: Target, gate: StyleValueGate): Target {
|
|
98
|
+
if (typeof target !== 'object' || target === null || !Object.hasOwn(target, 'transitionEnd')) {
|
|
99
|
+
return target
|
|
100
|
+
}
|
|
101
|
+
const error = new Error(
|
|
102
|
+
`${gate.componentId}: "exit" carries transitionEnd — the presence exit jump is the exit-jump ` +
|
|
103
|
+
'successor lane (T23 B3 charters the animate-lane settle jump; an exit jump precedes ' +
|
|
104
|
+
'removal); the member is refused and sibling exit keys survive.',
|
|
105
|
+
)
|
|
106
|
+
if (gate.severity === 'development') throw error
|
|
107
|
+
gate.report(error)
|
|
108
|
+
const { transitionEnd: _dropped, ...rest } = target as Record<string, unknown>
|
|
109
|
+
return rest as Target
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Law (f): the native depth ON TOP of core's entry law — every member must be drivable by
|
|
114
|
+
* the native lanes, and an embedded transition may carry only property-lane options.
|
|
115
|
+
* RETURN-shaped over the already-validated plain SNAPSHOT (M3 r3 major 5cb2381e272c): no
|
|
116
|
+
* foreign object is ever re-enumerated here, so no catch or class discriminant exists —
|
|
117
|
+
* the returned refusal is this boundary's by construction; the caller owns severity.
|
|
118
|
+
*/
|
|
119
|
+
const ORCHESTRATION_KEY_SET: ReadonlySet<string> = new Set(ORCHESTRATION_OPTION_KEYS)
|
|
120
|
+
|
|
121
|
+
function entryNativeDepthRefusal(
|
|
122
|
+
label: string,
|
|
123
|
+
entry: VariantEntry,
|
|
124
|
+
componentId: MotionComponentId,
|
|
125
|
+
): Error | null {
|
|
126
|
+
const { transition, ...target } = entry
|
|
127
|
+
if (transition !== undefined) {
|
|
128
|
+
const source = transition as Record<PropertyKey, unknown>
|
|
129
|
+
const hasMap = Reflect.ownKeys(transition).some(
|
|
130
|
+
(key) => typeof key === 'string' && TRANSITION_MAP_KEYS.has(key),
|
|
131
|
+
)
|
|
132
|
+
const refusalForSelected = (selected: Transition, path: string): Error | null => {
|
|
133
|
+
for (const option of Reflect.ownKeys(selected)) {
|
|
134
|
+
if (typeof option !== 'string' || TRANSITION_MAP_KEYS.has(option)) continue
|
|
135
|
+
// T21 (REQ-API-053): root-level orchestration options are TREE-lane vocabulary — the
|
|
136
|
+
// variant episode consumes them and MotionView strips them before any property-lane
|
|
137
|
+
// command (L1), so they are consumed, not inert. Core's map-entry law upstream still
|
|
138
|
+
// refuses them INSIDE per-value entries; this depth check governs the property lane.
|
|
139
|
+
if (ORCHESTRATION_KEY_SET.has(option)) continue
|
|
140
|
+
const optionValue = (selected as Record<string, unknown>)[option]
|
|
141
|
+
if (optionValue === undefined) continue // explicitly-undefined reads as absent (R6 law)
|
|
142
|
+
if (!NATIVE_PROPERTY_TRANSITION_OPTIONS.has(option)) {
|
|
143
|
+
return new InvalidTransitionError(
|
|
144
|
+
componentId,
|
|
145
|
+
option,
|
|
146
|
+
optionValue,
|
|
147
|
+
`${path}.${option} is not executable by the native property lane (it drives: ` +
|
|
148
|
+
`${NATIVE_PROPERTY_TRANSITION_OPTIONS.options.join(', ')}) — a silently inert ` +
|
|
149
|
+
'option is refused (G-INV-8, shippedSurface.ts)',
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
if (!hasMap) {
|
|
156
|
+
const refusal = refusalForSelected(transition, `variants.${label}.transition`)
|
|
157
|
+
if (refusal !== null) return refusal
|
|
158
|
+
} else {
|
|
159
|
+
const selectedKeys = Object.keys(target)
|
|
160
|
+
const selectors = selectedKeys.length === 0 ? ['__root__'] : selectedKeys
|
|
161
|
+
const seen = new Set<string>()
|
|
162
|
+
for (const key of selectors) {
|
|
163
|
+
const mapKey =
|
|
164
|
+
TRANSITION_MAP_KEYS.has(key) &&
|
|
165
|
+
Object.hasOwn(source, key) &&
|
|
166
|
+
source[key] !== undefined &&
|
|
167
|
+
source[key] !== null
|
|
168
|
+
? key
|
|
169
|
+
: Object.hasOwn(source, 'default') && source['default'] !== undefined
|
|
170
|
+
? 'default'
|
|
171
|
+
: 'root'
|
|
172
|
+
if (seen.has(mapKey)) continue
|
|
173
|
+
seen.add(mapKey)
|
|
174
|
+
const selected = resolveTransitionForKey(transition, key)
|
|
175
|
+
const refusal = refusalForSelected(selected, `variants.${label}.transition.${mapKey}`)
|
|
176
|
+
if (refusal !== null) return refusal
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const [memberKey, memberValue] of Object.entries(target)) {
|
|
181
|
+
// T23 B3b: the transitionEnd carrier passes whole — core deep-validated each sub-value
|
|
182
|
+
// (family + path-named diagnostics, B3a), and the controller's jump lane re-gates native
|
|
183
|
+
// executability at jump time (applyTransitionEnd). It is structural, not a drivable member.
|
|
184
|
+
if (memberKey === 'transitionEnd') continue
|
|
185
|
+
const valueKind = nativeMountTargetValueKind(memberKey)
|
|
186
|
+
// Both lanes drive a scalar OR a keyframe ARRAY (REQ-API-033 on variants, majors 31 + 35): core
|
|
187
|
+
// validation already proved a well-formed same-value-type array, and the array-aware controller
|
|
188
|
+
// drives it — the color lane through the progress projection (projectColorSequence), the numeric
|
|
189
|
+
// lane directly. Positional length strings share the controller's measure/defer lane — checked
|
|
190
|
+
// ELEMENT-WISE through the ONE key-aware executable matrix (review 91d41675d511): a scalar or
|
|
191
|
+
// array element outside it refuses eagerly here, never latently at selection.
|
|
192
|
+
const lengthRefusal =
|
|
193
|
+
valueKind === 'color' || valueKind === 'discrete'
|
|
194
|
+
? null
|
|
195
|
+
: executableLengthValueRefusal(memberKey, memberValue)
|
|
196
|
+
// The discrete lane (T23 B2b) drives a SINGLE keyword of the discrete FAMILY — the
|
|
197
|
+
// controller's own triple guard (isDiscreteKeyword pre-guards the parse; the family verdict
|
|
198
|
+
// refuses 'auto'/named colors); keyframe arrays are not chartered for discrete keys.
|
|
199
|
+
const drivable =
|
|
200
|
+
valueKind === 'color'
|
|
201
|
+
? typeof memberValue === 'string' || Array.isArray(memberValue)
|
|
202
|
+
: valueKind === 'discrete'
|
|
203
|
+
? typeof memberValue === 'string' &&
|
|
204
|
+
isDiscreteKeyword(memberValue) &&
|
|
205
|
+
parseValue(memberValue).kind === 'discrete'
|
|
206
|
+
: isNativeExecutableLengthValue(memberKey, memberValue)
|
|
207
|
+
if (!drivable) {
|
|
208
|
+
return new InvalidVariantError(
|
|
209
|
+
label,
|
|
210
|
+
entry,
|
|
211
|
+
`${componentId}: variants.${label}.${memberKey} received ${describeValue(memberValue)} — ${
|
|
212
|
+
lengthRefusal ??
|
|
213
|
+
`the native ${valueKind} lane drives ${
|
|
214
|
+
valueKind === 'color'
|
|
215
|
+
? 'color strings or a color keyframe array'
|
|
216
|
+
: valueKind === 'discrete'
|
|
217
|
+
? 'a single CSS keyword string (keyframe arrays are not chartered for discrete keys)'
|
|
218
|
+
: 'numbers, numeric keyframe arrays, or measure-resolved positional lengths'
|
|
219
|
+
}; the supplied value is not executable by the native property lane (REQ-API-032 law f)`
|
|
220
|
+
}`,
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
// R8-F1 at the SUPPLYING boundary (review major 42): a keyframe array member under an EMBEDDED
|
|
224
|
+
// spring interpolates exactly two keyframes — a longer array refuses the ENTRY EAGERLY here (an
|
|
225
|
+
// invalid entry can never sit latent until selection, the R7 law) instead of an uncaught controller
|
|
226
|
+
// throw. An array with NO embedded spring rides the controller's own severity-aware compat filter.
|
|
227
|
+
// R8 M2-B (REQ-API-033 law d): validate this variant's EMBEDDED keyframe timing eagerly (the
|
|
228
|
+
// R7 no-latent law). Exact-count `times` override even offsets; other shape-valid counts fall
|
|
229
|
+
// back as a whole. Per-segment easing lists allow missing/extra entries; scalar consumers still
|
|
230
|
+
// refuse keyframe-only timing as inert.
|
|
231
|
+
const timingRefusal = keyframeTransitionRefusal(
|
|
232
|
+
`variants.${label}`,
|
|
233
|
+
memberKey,
|
|
234
|
+
memberValue,
|
|
235
|
+
transition === undefined ? undefined : resolveTransitionForKey(transition, memberKey),
|
|
236
|
+
)
|
|
237
|
+
if (timingRefusal !== null) return new InvalidVariantError(label, entry, timingRefusal.message)
|
|
238
|
+
}
|
|
239
|
+
return null
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// A dictionary entry with no embedded transition inherits the element transition when a label selects
|
|
243
|
+
// it. That fallback is an effective per-property consumer, not an optional later detail: timing or a
|
|
244
|
+
// spring keyframe cardinality it cannot execute must be refused before the resolver/controller sees it.
|
|
245
|
+
// Core and the existing native-depth gate retain their entry-unit law for embedded transitions. The
|
|
246
|
+
// inherited case is per-property under production severity, matching every other fallback supplier:
|
|
247
|
+
// an incompatible key is absent while a valid sibling remains executable.
|
|
248
|
+
export function gateVariantApplicationFallback(
|
|
249
|
+
application: VariantApplication,
|
|
250
|
+
elementTransition: Transition | undefined,
|
|
251
|
+
gate: StyleValueGate,
|
|
252
|
+
componentId: MotionComponentId = '<Motion.View>',
|
|
253
|
+
): VariantApplication {
|
|
254
|
+
if (application.transition !== undefined || elementTransition === undefined) return application
|
|
255
|
+
const accepted = Object.create(null) as Record<string, unknown>
|
|
256
|
+
for (const [key, value] of Object.entries(application.target)) {
|
|
257
|
+
const timingRefusal = keyframeTransitionRefusal(
|
|
258
|
+
componentId,
|
|
259
|
+
key,
|
|
260
|
+
value,
|
|
261
|
+
resolveTransitionForKey(elementTransition, key),
|
|
262
|
+
)
|
|
263
|
+
if (timingRefusal !== null) {
|
|
264
|
+
if (gate.severity === 'development') throw timingRefusal
|
|
265
|
+
gate.report(timingRefusal)
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
accepted[key] = value
|
|
269
|
+
}
|
|
270
|
+
return { target: accepted as VariantEntry, transition: undefined }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Only concrete timing consumers (animate/exit) inherit the element transition. */
|
|
274
|
+
export function gateVariantApplicationFallbacks(
|
|
275
|
+
applications: readonly VariantApplication[],
|
|
276
|
+
elementTransition: Transition | undefined,
|
|
277
|
+
gate: StyleValueGate,
|
|
278
|
+
componentId: MotionComponentId = '<Motion.View>',
|
|
279
|
+
): readonly VariantApplication[] {
|
|
280
|
+
return gateVariantApplicationFallbacksWithEvidence(
|
|
281
|
+
applications,
|
|
282
|
+
elementTransition,
|
|
283
|
+
gate,
|
|
284
|
+
componentId,
|
|
285
|
+
).applications
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The accepted projection alone cannot say whether a fallback property was attempted: production may
|
|
290
|
+
* have removed every such key. Keep that evidence beside the same resolved applications so the
|
|
291
|
+
* element-transition owner decision distinguishes a genuinely inert transition from one already refused.
|
|
292
|
+
*/
|
|
293
|
+
export function gateVariantApplicationFallbacksWithEvidence(
|
|
294
|
+
applications: readonly VariantApplication[],
|
|
295
|
+
elementTransition: Transition | undefined,
|
|
296
|
+
gate: StyleValueGate,
|
|
297
|
+
componentId: MotionComponentId = '<Motion.View>',
|
|
298
|
+
): {
|
|
299
|
+
readonly applications: readonly VariantApplication[]
|
|
300
|
+
readonly fallbackAttempted: boolean
|
|
301
|
+
} {
|
|
302
|
+
const fallbackAttempted =
|
|
303
|
+
elementTransition !== undefined &&
|
|
304
|
+
applications.some(
|
|
305
|
+
(application) =>
|
|
306
|
+
application.transition === undefined && Object.keys(application.target).length > 0,
|
|
307
|
+
)
|
|
308
|
+
return {
|
|
309
|
+
applications: applications.map((application) =>
|
|
310
|
+
gateVariantApplicationFallback(application, elementTransition, gate, componentId),
|
|
311
|
+
),
|
|
312
|
+
fallbackAttempted,
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The dictionary gate: outer shape as a unit, then EVERY entry eagerly (an invalid entry
|
|
318
|
+
* can never sit latent until selection). Returns the SANITIZED dictionary — in production
|
|
319
|
+
* an offending entry is DROPPED (reported once) and the valid remainder survives; a
|
|
320
|
+
* malformed outer shape refuses the whole prop to `undefined`.
|
|
321
|
+
*/
|
|
322
|
+
export function gateVariantsDictionary(
|
|
323
|
+
variants: unknown,
|
|
324
|
+
gate: StyleValueGate,
|
|
325
|
+
componentId: MotionComponentId = '<Motion.View>',
|
|
326
|
+
): VariantsDictionary | undefined {
|
|
327
|
+
if (variants === undefined) return undefined
|
|
328
|
+
// RETURN-shaped refusals, no catch (M3 r3 majors 1b53e9337aec/13af48a64530/5cb2381e272c):
|
|
329
|
+
// the shape probe and the entry READ phase run foreign traps UNGUARDED — their faults
|
|
330
|
+
// (including authentic same-class sentinels) propagate untouched by construction. A
|
|
331
|
+
// returned refusal is boundary-made; severity applies only to those.
|
|
332
|
+
const shapeRefusal = variantsShapeRefusal(variants)
|
|
333
|
+
if (shapeRefusal !== null) {
|
|
334
|
+
const ownedRefusal = new InvalidVariantError(
|
|
335
|
+
'(variants)',
|
|
336
|
+
variants,
|
|
337
|
+
`${componentId}: ${shapeRefusal.message}`,
|
|
338
|
+
shapeRefusal,
|
|
339
|
+
)
|
|
340
|
+
if (gate.severity === 'development') throw ownedRefusal
|
|
341
|
+
gate.report(ownedRefusal)
|
|
342
|
+
return undefined
|
|
343
|
+
}
|
|
344
|
+
// Null-prototype accumulator (M2 r1 major 11): a valid own '__proto__' label must land as
|
|
345
|
+
// an OWN property — plain-object assignment would invoke the legacy prototype setter and
|
|
346
|
+
// silently turn the label into a local miss (and pollute the accumulator's prototype).
|
|
347
|
+
const accepted: Record<string, VariantEntry | VariantResolver> = Object.create(null) as Record<
|
|
348
|
+
string,
|
|
349
|
+
VariantEntry | VariantResolver
|
|
350
|
+
>
|
|
351
|
+
for (const [label, entry] of Object.entries(variants as Record<string, unknown>)) {
|
|
352
|
+
const { refusal, entry: snapshot } = validateVariantEntrySnapshot(label, entry, NATIVE_HOST, {
|
|
353
|
+
componentId,
|
|
354
|
+
})
|
|
355
|
+
// T24 B2: a RESOLVER entry mounts by identity, uninvoked — its keys do not exist until
|
|
356
|
+
// resolution, so BOTH the core entry law and the native depth law apply to its RESULT
|
|
357
|
+
// inside resolveVariantApplicationsGated, never here.
|
|
358
|
+
if (typeof snapshot === 'function') {
|
|
359
|
+
accepted[label] = snapshot
|
|
360
|
+
continue
|
|
361
|
+
}
|
|
362
|
+
// Production: the ENTRY is the refusal unit within a valid dictionary (law f).
|
|
363
|
+
const nativeRefusal = refusal ?? entryNativeDepthRefusal(label, snapshot!, componentId)
|
|
364
|
+
if (nativeRefusal !== null) {
|
|
365
|
+
if (gate.severity === 'development') throw nativeRefusal
|
|
366
|
+
gate.report(nativeRefusal)
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
// The SNAPSHOT is what mounts (one read, one truth): a stateful proxy cannot show the
|
|
370
|
+
// validators one value and the engine another.
|
|
371
|
+
accepted[label] = snapshot!
|
|
372
|
+
}
|
|
373
|
+
return accepted
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// The fallback state for a value-less/pre-mount native site. Mounted hosts supply their
|
|
377
|
+
// controller's fresh live-state reader at each resolver arm.
|
|
378
|
+
const EMPTY_CURRENT: Readonly<Record<string, unknown>> = Object.freeze({})
|
|
379
|
+
const EMPTY_VELOCITY: Readonly<Record<string, number>> = Object.freeze({})
|
|
380
|
+
const EMPTY_VALUE_STATE = Object.freeze({ current: EMPTY_CURRENT, velocity: EMPTY_VELOCITY })
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* T24 B2: THE native resolution seam — every native site resolves labels through this
|
|
384
|
+
* gate, never bare `resolveVariantDefinition`. Static labels are the pinned static law
|
|
385
|
+
* unchanged. A RESOLVER label resolves through the core seam (unguarded invocation — user
|
|
386
|
+
* faults propagate untouched; typed refusals for the two-step bound and the core entry
|
|
387
|
+
* law) and its RESULT then rides the SAME native depth law a static entry rides at the
|
|
388
|
+
* boundary. Severity granularity is the LABEL: development throws; production reports once
|
|
389
|
+
* and skips the offending label while the remainder survives.
|
|
390
|
+
*/
|
|
391
|
+
export function resolveVariantApplicationsGated(
|
|
392
|
+
definition: string | readonly string[] | VariantResolver,
|
|
393
|
+
dictionary: VariantsDictionary | undefined,
|
|
394
|
+
gate: StyleValueGate,
|
|
395
|
+
componentId: MotionComponentId,
|
|
396
|
+
custom?: unknown,
|
|
397
|
+
readValueState: () => {
|
|
398
|
+
readonly current: Readonly<Record<string, unknown>>
|
|
399
|
+
readonly velocity: Readonly<Record<string, number>>
|
|
400
|
+
} = () => EMPTY_VALUE_STATE,
|
|
401
|
+
): readonly VariantApplication[] {
|
|
402
|
+
if (typeof definition === 'function') {
|
|
403
|
+
const resolution = resolveVariantDefinitionResult(definition, dictionary, {
|
|
404
|
+
custom,
|
|
405
|
+
readValueState,
|
|
406
|
+
host: NATIVE_HOST,
|
|
407
|
+
componentId,
|
|
408
|
+
})
|
|
409
|
+
if (resolution.refusal !== null) {
|
|
410
|
+
if (gate.severity === 'development') throw resolution.refusal
|
|
411
|
+
gate.report(resolution.refusal)
|
|
412
|
+
return []
|
|
413
|
+
}
|
|
414
|
+
return resolution.applications.filter((application) => {
|
|
415
|
+
const producedEntry = {
|
|
416
|
+
...application.target,
|
|
417
|
+
...(application.transition === undefined ? {} : { transition: application.transition }),
|
|
418
|
+
} as VariantEntry
|
|
419
|
+
const refusal = entryNativeDepthRefusal('(definition)', producedEntry, componentId)
|
|
420
|
+
if (refusal === null) return true
|
|
421
|
+
if (gate.severity === 'development') throw refusal
|
|
422
|
+
gate.report(refusal)
|
|
423
|
+
return false
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
const labels = typeof definition === 'string' ? [definition] : definition
|
|
427
|
+
const applications: VariantApplication[] = []
|
|
428
|
+
for (const label of labels) {
|
|
429
|
+
if (dictionary === undefined || !Object.hasOwn(dictionary, label)) continue
|
|
430
|
+
if (typeof dictionary[label] !== 'function') {
|
|
431
|
+
// Static entries cannot refuse here (validated at the boundary) — the bare core
|
|
432
|
+
// resolution is the single static law.
|
|
433
|
+
for (const application of resolveVariantDefinition(label, dictionary)) {
|
|
434
|
+
applications.push(application)
|
|
435
|
+
}
|
|
436
|
+
continue
|
|
437
|
+
}
|
|
438
|
+
const resolution = resolveVariantDefinitionResult(label, dictionary, {
|
|
439
|
+
custom,
|
|
440
|
+
readValueState,
|
|
441
|
+
host: NATIVE_HOST,
|
|
442
|
+
componentId,
|
|
443
|
+
})
|
|
444
|
+
if (resolution.refusal !== null) {
|
|
445
|
+
if (gate.severity === 'development') throw resolution.refusal
|
|
446
|
+
gate.report(resolution.refusal)
|
|
447
|
+
continue
|
|
448
|
+
}
|
|
449
|
+
for (const application of resolution.applications) {
|
|
450
|
+
// The produced entry rides the SAME native depth law a static entry rides.
|
|
451
|
+
const producedEntry = {
|
|
452
|
+
...application.target,
|
|
453
|
+
...(application.transition === undefined ? {} : { transition: application.transition }),
|
|
454
|
+
} as VariantEntry
|
|
455
|
+
const depthRefusal = entryNativeDepthRefusal(label, producedEntry, componentId)
|
|
456
|
+
if (depthRefusal !== null) {
|
|
457
|
+
if (gate.severity === 'development') throw depthRefusal
|
|
458
|
+
gate.report(depthRefusal)
|
|
459
|
+
continue
|
|
460
|
+
}
|
|
461
|
+
applications.push(application)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return applications
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* T24 B1: law (c) through the gated seam — the synchronous initial overlay with RESOLVER
|
|
469
|
+
* labels admitted. Composes core `resolveInitialOverlay` PER LABEL (the overlay law is an
|
|
470
|
+
* ordered per-application assign, so per-label calls compose identically) so severity keeps
|
|
471
|
+
* the LABEL granularity: development throws, production reports and skips the offending
|
|
472
|
+
* label while the remainder overlays. Static-only definitions behave byte-identically to
|
|
473
|
+
* the bare core overlay.
|
|
474
|
+
*/
|
|
475
|
+
export function resolveInitialOverlayGated(
|
|
476
|
+
definition: string | readonly string[] | VariantResolver,
|
|
477
|
+
dictionary: VariantsDictionary | undefined,
|
|
478
|
+
gate: StyleValueGate,
|
|
479
|
+
componentId: MotionComponentId,
|
|
480
|
+
custom?: unknown,
|
|
481
|
+
readValueState?: () => {
|
|
482
|
+
readonly current: Readonly<Record<string, unknown>>
|
|
483
|
+
readonly velocity: Readonly<Record<string, number>>
|
|
484
|
+
},
|
|
485
|
+
): Target {
|
|
486
|
+
if (typeof definition === 'function') {
|
|
487
|
+
const overlay: Record<string, unknown> = {}
|
|
488
|
+
for (const application of resolveVariantApplicationsGated(
|
|
489
|
+
definition,
|
|
490
|
+
dictionary,
|
|
491
|
+
gate,
|
|
492
|
+
componentId,
|
|
493
|
+
custom,
|
|
494
|
+
readValueState,
|
|
495
|
+
)) {
|
|
496
|
+
const { transitionEnd: carrier, ...plain } = application.target as Record<string, unknown>
|
|
497
|
+
Object.assign(overlay, plain)
|
|
498
|
+
if (typeof carrier === 'object' && carrier !== null && !Array.isArray(carrier)) {
|
|
499
|
+
Object.assign(overlay, carrier)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return overlay as Target
|
|
503
|
+
}
|
|
504
|
+
const labels = typeof definition === 'string' ? [definition] : definition
|
|
505
|
+
const overlay: Record<string, unknown> = {}
|
|
506
|
+
for (const label of labels) {
|
|
507
|
+
if (dictionary === undefined || !Object.hasOwn(dictionary, label)) continue
|
|
508
|
+
if (typeof dictionary[label] !== 'function') {
|
|
509
|
+
// Static labels: the bare core overlay is the single law (boundary-validated entries).
|
|
510
|
+
Object.assign(overlay, resolveInitialOverlay(label, dictionary))
|
|
511
|
+
continue
|
|
512
|
+
}
|
|
513
|
+
// Resolver labels resolve through the ONE depth-checked seam, then law (c) applies on
|
|
514
|
+
// top: targets assign in order, transitions discarded, the transitionEnd carrier folds
|
|
515
|
+
// instantly over its own target keys (T23 B3c) and never rides the overlay itself.
|
|
516
|
+
for (const application of resolveVariantApplicationsGated(
|
|
517
|
+
label,
|
|
518
|
+
dictionary,
|
|
519
|
+
gate,
|
|
520
|
+
componentId,
|
|
521
|
+
custom,
|
|
522
|
+
readValueState,
|
|
523
|
+
)) {
|
|
524
|
+
const { transitionEnd: carrier, ...plain } = application.target as Record<string, unknown>
|
|
525
|
+
Object.assign(overlay, plain)
|
|
526
|
+
if (typeof carrier === 'object' && carrier !== null && !Array.isArray(carrier)) {
|
|
527
|
+
Object.assign(overlay, carrier)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return overlay as Target
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Law (f): `inherit` is a boolean; anything else refuses naming the contract. */
|
|
535
|
+
export function gateInheritProp(
|
|
536
|
+
inherit: unknown,
|
|
537
|
+
gate: StyleValueGate,
|
|
538
|
+
componentId: MotionComponentId = '<Motion.View>',
|
|
539
|
+
): boolean | undefined {
|
|
540
|
+
if (inherit === undefined || typeof inherit === 'boolean') return inherit
|
|
541
|
+
// describeValue, never JSON.stringify (M3 r2 major 0a96290934e2): the same formatter law
|
|
542
|
+
// the label gate learned — a BigInt crashes the serializer with zero reports.
|
|
543
|
+
const error = new Error(
|
|
544
|
+
`${componentId}: "inherit" must be a boolean — received ${describeValue(inherit)} (REQ-API-032 law f; ` +
|
|
545
|
+
'the pinned visual-state switch takes true/false only)',
|
|
546
|
+
)
|
|
547
|
+
if (gate.severity === 'development') throw error
|
|
548
|
+
gate.report(error)
|
|
549
|
+
return undefined
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Law (f): normalize a label definition to the ordered label list. A string is one label;
|
|
554
|
+
* an ARRAY must be string-only — a non-string member refuses naming the prop and its index,
|
|
555
|
+
* and production refuses the whole PROP (the outer-shape unit).
|
|
556
|
+
*/
|
|
557
|
+
export function gateLabelDefinition(
|
|
558
|
+
prop: string,
|
|
559
|
+
definition: string | readonly unknown[],
|
|
560
|
+
gate: StyleValueGate,
|
|
561
|
+
componentId: MotionComponentId = '<Motion.View>',
|
|
562
|
+
): readonly string[] | undefined {
|
|
563
|
+
if (typeof definition === 'string') return [definition]
|
|
564
|
+
const capturedDefinition = captureBoundedArray(definition)
|
|
565
|
+
if (capturedDefinition.kind !== 'captured') {
|
|
566
|
+
const error = new Error(
|
|
567
|
+
`${componentId}: "${prop}" label array must have a safe length — received ${capturedArrayDescription(capturedDefinition)} ` +
|
|
568
|
+
'(REQ-API-032 law f)',
|
|
569
|
+
)
|
|
570
|
+
if (gate.severity === 'development') throw error
|
|
571
|
+
gate.report(error)
|
|
572
|
+
return undefined
|
|
573
|
+
}
|
|
574
|
+
const length = capturedDefinition.values.length
|
|
575
|
+
const captured: string[] = []
|
|
576
|
+
captured.length = length
|
|
577
|
+
for (let index = 0; index < length; index++) {
|
|
578
|
+
const member = Object.hasOwn(capturedDefinition.values, index)
|
|
579
|
+
? capturedDefinition.values[index]
|
|
580
|
+
: undefined
|
|
581
|
+
if (typeof member === 'string') {
|
|
582
|
+
captured[index] = member
|
|
583
|
+
continue
|
|
584
|
+
}
|
|
585
|
+
// describeValue, never JSON.stringify (M3 r1 major cdc42f01c118): a BigInt member
|
|
586
|
+
// would crash the FORMATTER itself — a raw serialize TypeError replacing the typed
|
|
587
|
+
// law-(f) refusal in development and escaping unreported in production.
|
|
588
|
+
const error = new Error(
|
|
589
|
+
`${componentId}: "${prop}" label array member at index ${index} must be a string — received ` +
|
|
590
|
+
`${describeValue(member)} (REQ-API-032 law f; the label array is ` +
|
|
591
|
+
'string-only)',
|
|
592
|
+
)
|
|
593
|
+
if (gate.severity === 'development') throw error
|
|
594
|
+
gate.report(error)
|
|
595
|
+
return undefined
|
|
596
|
+
}
|
|
597
|
+
return Object.freeze(captured)
|
|
598
|
+
}
|