@unrulysystems/native-motion-core 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 +9 -0
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/clock.cjs +71 -0
- package/dist/clock.d.cts +23 -0
- package/dist/clock.d.ts +23 -0
- package/dist/clock.js +66 -0
- package/dist/collect-reads.cjs +68 -0
- package/dist/collect-reads.d.cts +23 -0
- package/dist/collect-reads.d.ts +23 -0
- package/dist/collect-reads.js +62 -0
- package/dist/component/boundedArray.cjs +74 -0
- package/dist/component/boundedArray.d.cts +25 -0
- package/dist/component/boundedArray.d.ts +25 -0
- package/dist/component/boundedArray.js +68 -0
- package/dist/component/index.cjs +57 -0
- package/dist/component/index.d.cts +12 -0
- package/dist/component/index.d.ts +12 -0
- package/dist/component/index.js +22 -0
- package/dist/component/orchestration.cjs +129 -0
- package/dist/component/orchestration.d.cts +66 -0
- package/dist/component/orchestration.d.ts +66 -0
- package/dist/component/orchestration.js +124 -0
- package/dist/component/resolve.cjs +104 -0
- package/dist/component/resolve.d.cts +11 -0
- package/dist/component/resolve.d.ts +11 -0
- package/dist/component/resolve.js +97 -0
- package/dist/component/transition.cjs +352 -0
- package/dist/component/transition.d.cts +33 -0
- package/dist/component/transition.d.ts +33 -0
- package/dist/component/transition.js +339 -0
- package/dist/component/types.cjs +114 -0
- package/dist/component/types.d.cts +60 -0
- package/dist/component/types.d.ts +60 -0
- package/dist/component/types.js +111 -0
- package/dist/component/validate.cjs +1015 -0
- package/dist/component/validate.d.cts +37 -0
- package/dist/component/validate.d.ts +37 -0
- package/dist/component/validate.js +1002 -0
- package/dist/component/variants.cjs +333 -0
- package/dist/component/variants.d.cts +106 -0
- package/dist/component/variants.d.ts +106 -0
- package/dist/component/variants.js +321 -0
- package/dist/config/constants.cjs +41 -0
- package/dist/config/constants.d.cts +29 -0
- package/dist/config/constants.d.ts +29 -0
- package/dist/config/constants.js +38 -0
- package/dist/delay.cjs +72 -0
- package/dist/delay.d.cts +26 -0
- package/dist/delay.d.ts +26 -0
- package/dist/delay.js +70 -0
- package/dist/derived.cjs +143 -0
- package/dist/derived.d.cts +41 -0
- package/dist/derived.d.ts +41 -0
- package/dist/derived.js +139 -0
- package/dist/driver/index.cjs +22 -0
- package/dist/driver/index.d.cts +7 -0
- package/dist/driver/index.d.ts +7 -0
- package/dist/driver/index.js +14 -0
- package/dist/driver/keyframeTiming.cjs +130 -0
- package/dist/driver/keyframeTiming.d.cts +18 -0
- package/dist/driver/keyframeTiming.d.ts +18 -0
- package/dist/driver/keyframeTiming.js +124 -0
- package/dist/driver/keyframeTimingConfig.cjs +24 -0
- package/dist/driver/keyframeTimingConfig.d.cts +5 -0
- package/dist/driver/keyframeTimingConfig.d.ts +5 -0
- package/dist/driver/keyframeTimingConfig.js +22 -0
- package/dist/driver/prepare.cjs +459 -0
- package/dist/driver/prepare.d.cts +4 -0
- package/dist/driver/prepare.d.ts +4 -0
- package/dist/driver/prepare.js +454 -0
- package/dist/driver/reference.cjs +762 -0
- package/dist/driver/reference.d.cts +53 -0
- package/dist/driver/reference.d.ts +53 -0
- package/dist/driver/reference.js +757 -0
- package/dist/driver/step.cjs +55 -0
- package/dist/driver/step.d.cts +9 -0
- package/dist/driver/step.d.ts +9 -0
- package/dist/driver/step.js +52 -0
- package/dist/driver/tiers.cjs +40 -0
- package/dist/driver/tiers.d.cts +2 -0
- package/dist/driver/tiers.d.ts +2 -0
- package/dist/driver/tiers.js +37 -0
- package/dist/driver/types.cjs +9 -0
- package/dist/driver/types.d.cts +68 -0
- package/dist/driver/types.d.ts +68 -0
- package/dist/driver/types.js +8 -0
- package/dist/external-animation-ledger.cjs +1250 -0
- package/dist/external-animation-ledger.d.cts +416 -0
- package/dist/external-animation-ledger.d.ts +416 -0
- package/dist/external-animation-ledger.js +1243 -0
- package/dist/gesture/directionLock.cjs +25 -0
- package/dist/gesture/directionLock.d.cts +8 -0
- package/dist/gesture/directionLock.d.ts +8 -0
- package/dist/gesture/directionLock.js +21 -0
- package/dist/gesture/dragConfig.cjs +634 -0
- package/dist/gesture/dragConfig.d.cts +298 -0
- package/dist/gesture/dragConfig.d.ts +298 -0
- package/dist/gesture/dragConfig.js +624 -0
- package/dist/gesture/elastic.cjs +44 -0
- package/dist/gesture/elastic.d.cts +4 -0
- package/dist/gesture/elastic.d.ts +4 -0
- package/dist/gesture/elastic.js +39 -0
- package/dist/gesture/handoffSession.cjs +161 -0
- package/dist/gesture/handoffSession.d.cts +35 -0
- package/dist/gesture/handoffSession.d.ts +35 -0
- package/dist/gesture/handoffSession.js +158 -0
- package/dist/gesture/index.cjs +37 -0
- package/dist/gesture/index.d.cts +12 -0
- package/dist/gesture/index.d.ts +12 -0
- package/dist/gesture/index.js +11 -0
- package/dist/gesture/projection.cjs +95 -0
- package/dist/gesture/projection.d.cts +7 -0
- package/dist/gesture/projection.d.ts +7 -0
- package/dist/gesture/projection.js +87 -0
- package/dist/gesture/session.cjs +162 -0
- package/dist/gesture/session.d.cts +29 -0
- package/dist/gesture/session.d.ts +29 -0
- package/dist/gesture/session.js +158 -0
- package/dist/gesture/types.cjs +5 -0
- package/dist/gesture/types.d.cts +13 -0
- package/dist/gesture/types.d.ts +13 -0
- package/dist/gesture/types.js +4 -0
- package/dist/gesture/viewportConstraints.cjs +38 -0
- package/dist/gesture/viewportConstraints.d.cts +6 -0
- package/dist/gesture/viewportConstraints.d.ts +6 -0
- package/dist/gesture/viewportConstraints.js +34 -0
- package/dist/graph.cjs +226 -0
- package/dist/graph.d.cts +2 -0
- package/dist/graph.d.ts +2 -0
- package/dist/graph.js +223 -0
- package/dist/index.cjs +140 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +70 -0
- package/dist/inertia.cjs +214 -0
- package/dist/inertia.d.cts +53 -0
- package/dist/inertia.d.ts +53 -0
- package/dist/inertia.js +212 -0
- package/dist/instant.cjs +66 -0
- package/dist/instant.d.cts +16 -0
- package/dist/instant.d.ts +16 -0
- package/dist/instant.js +63 -0
- package/dist/internal-driver.cjs +81 -0
- package/dist/internal-driver.d.cts +19 -0
- package/dist/internal-driver.d.ts +19 -0
- package/dist/internal-driver.js +35 -0
- package/dist/keyframes.cjs +191 -0
- package/dist/keyframes.d.cts +13 -0
- package/dist/keyframes.d.ts +13 -0
- package/dist/keyframes.js +188 -0
- package/dist/layout/commitDetector.cjs +67 -0
- package/dist/layout/commitDetector.d.cts +21 -0
- package/dist/layout/commitDetector.d.ts +21 -0
- package/dist/layout/commitDetector.js +64 -0
- package/dist/layout/compose.cjs +67 -0
- package/dist/layout/compose.d.cts +30 -0
- package/dist/layout/compose.d.ts +30 -0
- package/dist/layout/compose.js +65 -0
- package/dist/layout/constants.cjs +13 -0
- package/dist/layout/constants.d.cts +6 -0
- package/dist/layout/constants.d.ts +6 -0
- package/dist/layout/constants.js +10 -0
- package/dist/layout/identity.cjs +700 -0
- package/dist/layout/identity.d.cts +67 -0
- package/dist/layout/identity.d.ts +67 -0
- package/dist/layout/identity.js +698 -0
- package/dist/layout/index.cjs +36 -0
- package/dist/layout/index.d.cts +18 -0
- package/dist/layout/index.d.ts +18 -0
- package/dist/layout/index.js +13 -0
- package/dist/layout/measure.cjs +81 -0
- package/dist/layout/measure.d.cts +43 -0
- package/dist/layout/measure.d.ts +43 -0
- package/dist/layout/measure.js +78 -0
- package/dist/layout/projection.cjs +99 -0
- package/dist/layout/projection.d.cts +23 -0
- package/dist/layout/projection.d.ts +23 -0
- package/dist/layout/projection.js +97 -0
- package/dist/layout/scroll.cjs +27 -0
- package/dist/layout/scroll.d.cts +13 -0
- package/dist/layout/scroll.d.ts +13 -0
- package/dist/layout/scroll.js +24 -0
- package/dist/layout/session.cjs +207 -0
- package/dist/layout/session.d.cts +73 -0
- package/dist/layout/session.d.ts +73 -0
- package/dist/layout/session.js +205 -0
- package/dist/layout/tree.cjs +826 -0
- package/dist/layout/tree.d.cts +70 -0
- package/dist/layout/tree.d.ts +70 -0
- package/dist/layout/tree.js +823 -0
- package/dist/layout/types.cjs +36 -0
- package/dist/layout/types.d.cts +31 -0
- package/dist/layout/types.d.ts +31 -0
- package/dist/layout/types.js +35 -0
- package/dist/motion-arc.cjs +184 -0
- package/dist/motion-arc.d.cts +78 -0
- package/dist/motion-arc.d.ts +78 -0
- package/dist/motion-arc.js +183 -0
- package/dist/motion-mix.cjs +205 -0
- package/dist/motion-mix.d.cts +3 -0
- package/dist/motion-mix.d.ts +3 -0
- package/dist/motion-mix.js +202 -0
- package/dist/motion-value-driver-port.cjs +636 -0
- package/dist/motion-value-driver-port.d.cts +406 -0
- package/dist/motion-value-driver-port.d.ts +406 -0
- package/dist/motion-value-driver-port.js +627 -0
- package/dist/motion-value.cjs +189 -0
- package/dist/motion-value.d.cts +51 -0
- package/dist/motion-value.d.ts +51 -0
- package/dist/motion-value.js +185 -0
- package/dist/presence/controller.cjs +657 -0
- package/dist/presence/controller.d.cts +6 -0
- package/dist/presence/controller.d.ts +6 -0
- package/dist/presence/controller.js +652 -0
- package/dist/presence/index.cjs +19 -0
- package/dist/presence/index.d.cts +4 -0
- package/dist/presence/index.d.ts +4 -0
- package/dist/presence/index.js +12 -0
- package/dist/presence/machine.cjs +50 -0
- package/dist/presence/machine.d.cts +10 -0
- package/dist/presence/machine.d.ts +10 -0
- package/dist/presence/machine.js +46 -0
- package/dist/presence/types.cjs +6 -0
- package/dist/presence/types.d.cts +32 -0
- package/dist/presence/types.d.ts +32 -0
- package/dist/presence/types.js +5 -0
- package/dist/repeat.cjs +311 -0
- package/dist/repeat.d.cts +174 -0
- package/dist/repeat.d.ts +174 -0
- package/dist/repeat.js +300 -0
- package/dist/spring.cjs +128 -0
- package/dist/spring.d.cts +18 -0
- package/dist/spring.d.ts +18 -0
- package/dist/spring.js +125 -0
- package/dist/subscriptions.cjs +74 -0
- package/dist/subscriptions.d.cts +19 -0
- package/dist/subscriptions.d.ts +19 -0
- package/dist/subscriptions.js +69 -0
- package/dist/subset/index.cjs +23 -0
- package/dist/subset/index.d.cts +6 -0
- package/dist/subset/index.d.ts +6 -0
- package/dist/subset/index.js +13 -0
- package/dist/subset/normalize.cjs +90 -0
- package/dist/subset/normalize.d.cts +10 -0
- package/dist/subset/normalize.d.ts +10 -0
- package/dist/subset/normalize.js +85 -0
- package/dist/subset/registry.cjs +259 -0
- package/dist/subset/registry.d.cts +25 -0
- package/dist/subset/registry.d.ts +25 -0
- package/dist/subset/registry.js +256 -0
- package/dist/subset/resolve.cjs +98 -0
- package/dist/subset/resolve.d.cts +24 -0
- package/dist/subset/resolve.d.ts +24 -0
- package/dist/subset/resolve.js +90 -0
- package/dist/timing.cjs +206 -0
- package/dist/timing.d.cts +17 -0
- package/dist/timing.d.ts +17 -0
- package/dist/timing.js +201 -0
- package/dist/transformTemplate.cjs +219 -0
- package/dist/transformTemplate.d.cts +29 -0
- package/dist/transformTemplate.d.ts +29 -0
- package/dist/transformTemplate.js +215 -0
- package/dist/transition.cjs +231 -0
- package/dist/transition.d.cts +89 -0
- package/dist/transition.d.ts +89 -0
- package/dist/transition.js +223 -0
- package/dist/types.cjs +4 -0
- package/dist/types.d.cts +175 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.js +3 -0
- package/dist/value-types/color.cjs +220 -0
- package/dist/value-types/color.d.cts +19 -0
- package/dist/value-types/color.d.ts +19 -0
- package/dist/value-types/color.js +218 -0
- package/dist/value-types/complex.cjs +160 -0
- package/dist/value-types/complex.d.cts +19 -0
- package/dist/value-types/complex.d.ts +19 -0
- package/dist/value-types/complex.js +155 -0
- package/dist/value-types/constants.cjs +15 -0
- package/dist/value-types/constants.d.cts +6 -0
- package/dist/value-types/constants.d.ts +6 -0
- package/dist/value-types/constants.js +12 -0
- package/dist/value-types/discrete.cjs +62 -0
- package/dist/value-types/discrete.d.cts +4 -0
- package/dist/value-types/discrete.d.ts +4 -0
- package/dist/value-types/discrete.js +56 -0
- package/dist/value-types/index.cjs +59 -0
- package/dist/value-types/index.d.cts +14 -0
- package/dist/value-types/index.d.ts +14 -0
- package/dist/value-types/index.js +24 -0
- package/dist/value-types/measure-resolve.cjs +325 -0
- package/dist/value-types/measure-resolve.d.cts +91 -0
- package/dist/value-types/measure-resolve.d.ts +91 -0
- package/dist/value-types/measure-resolve.js +313 -0
- package/dist/value-types/mix.cjs +90 -0
- package/dist/value-types/mix.d.cts +27 -0
- package/dist/value-types/mix.d.ts +27 -0
- package/dist/value-types/mix.js +85 -0
- package/dist/value-types/named-colors.cjs +61 -0
- package/dist/value-types/named-colors.d.cts +2 -0
- package/dist/value-types/named-colors.d.ts +2 -0
- package/dist/value-types/named-colors.js +58 -0
- package/dist/value-types/numeric.cjs +86 -0
- package/dist/value-types/numeric.d.cts +16 -0
- package/dist/value-types/numeric.d.ts +16 -0
- package/dist/value-types/numeric.js +79 -0
- package/dist/worklet-layout/config/constants.js +39 -0
- package/dist/worklet-layout/layout/constants.js +11 -0
- package/dist/worklet-layout/layout/identity.js +699 -0
- package/dist/worklet-layout/layout/projection.js +98 -0
- package/dist/worklet-layout/layout/session.js +206 -0
- package/dist/worklet-layout/layout/tree.js +824 -0
- package/dist/worklet-layout/layout/types.js +36 -0
- package/dist/worklet-layout/spring.js +125 -0
- package/dist/worklet-layout/timing.js +201 -0
- package/dist/worklet-layout/transition.js +223 -0
- package/dist/worklet-layout.cjs +47 -0
- package/dist/worklet-layout.d.cts +15 -0
- package/dist/worklet-layout.d.ts +15 -0
- package/dist/worklet-layout.js +28 -0
- package/dist/wrap.cjs +7 -0
- package/dist/wrap.d.cts +1 -0
- package/dist/wrap.d.ts +1 -0
- package/dist/wrap.js +4 -0
- package/package.json +45 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
// REQ-API-020/-021/-023 (FLAG 5a, specs/SPEC-GESTURE.md) — the drag prop
|
|
2
|
+
// conversion boundary, SINGLE-SOURCED in core so both engines resolve identical accepted sets
|
|
3
|
+
// (BRIEF "one API, one behavior" / "no forked logic"). Motion's public drag shapes
|
|
4
|
+
// (`dragConstraints {left/right/top/bottom}`, `dragElastic`, `dragSnapPoints {x?/y?}`) convert to
|
|
5
|
+
// core's gesture config here; every incompatible-but-valid shape is a TYPED refusal naming what
|
|
6
|
+
// and why — never a silent ignore. Host-agnostic (REQ-CORE-003): no react/react-native/motion
|
|
7
|
+
// import; the severity model is a plain policy enum + callback, adapted by each engine.
|
|
8
|
+
//
|
|
9
|
+
// Native uses the resolved `ResolvedDragConfig` to build the gesture session; the web engine
|
|
10
|
+
// calls this for the VERDICT (identical accept/refuse) then passes Motion's own shapes through to
|
|
11
|
+
// motion/react and maps `dragSnapPoints` to `dragTransition.modifyTarget` from the same points.
|
|
12
|
+
// Adopted from the PINNED oracle, cited not hand-transcribed (BRIEF numeric-constants law):
|
|
13
|
+
// framer-motion/src/gestures/drag/utils/constraints.ts:195 `export const defaultElastic = 0.35`
|
|
14
|
+
// (motion@12.42.2, source SHA 61833240). Applies when dragConstraints are present and
|
|
15
|
+
// dragElastic is omitted — Motion's own default, so omitted-elastic behaves identically on both
|
|
16
|
+
// engines.
|
|
17
|
+
export const DEFAULT_DRAG_ELASTIC = 0.35;
|
|
18
|
+
// The hook's replacement box is a boundary INPUT, validated identically on both engines
|
|
19
|
+
// (review r7 majors 3): complete — all four sides present numbers; legal — never NaN,
|
|
20
|
+
// infinities only with the correct polarity (the constraint legality vocabulary, G-INV-8);
|
|
21
|
+
// ordered per axis. A malformed replacement is a typed refusal — severity-adapted by the
|
|
22
|
+
// caller (native: the grab/poll report channel; web: the forwarded wrapper, which drops the
|
|
23
|
+
// replacement in production).
|
|
24
|
+
export function validateMeasureReplacementBox(replaced) {
|
|
25
|
+
// TOTAL (review r8 minor 5): a non-object truthy return (number, boolean, string — outside
|
|
26
|
+
// the declared box shape) is the same typed refusal, never a raw property-access TypeError.
|
|
27
|
+
// (Falsy returns — undefined/null/false — are the pin's no-op: `!!userConstraints`.)
|
|
28
|
+
if (typeof replaced !== 'object' || replaced === null) {
|
|
29
|
+
throw new Error(`onMeasureDragConstraints must return a complete { top, left, bottom, right } box of ` +
|
|
30
|
+
`finite numbers — got ${describeValue(replaced)} (REQ-API-042, G-INV-8).`);
|
|
31
|
+
}
|
|
32
|
+
const side = (name, value, polarity) => {
|
|
33
|
+
if (typeof value !== 'number' ||
|
|
34
|
+
Number.isNaN(value) ||
|
|
35
|
+
(!Number.isFinite(value) && value !== polarity * Number.POSITIVE_INFINITY)) {
|
|
36
|
+
throw new Error(`onMeasureDragConstraints must return a complete { top, left, bottom, right } box of ` +
|
|
37
|
+
`finite numbers (an open ${name} side only as ` +
|
|
38
|
+
`${polarity === -1 ? '-Infinity' : '+Infinity'}) — got ${name}: ` +
|
|
39
|
+
`${describeValue(value)} (REQ-API-042).`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
const left = side('left', replaced.left, -1);
|
|
44
|
+
const right = side('right', replaced.right, 1);
|
|
45
|
+
const top = side('top', replaced.top, -1);
|
|
46
|
+
const bottom = side('bottom', replaced.bottom, 1);
|
|
47
|
+
if (left > right || top > bottom) {
|
|
48
|
+
throw new Error(`onMeasureDragConstraints returned inverted bounds (left ${left} > right ${right} or ` +
|
|
49
|
+
`top ${top} > bottom ${bottom}) — an inverted box is a malformed replacement, refused ` +
|
|
50
|
+
'(REQ-API-042, G-INV-8).');
|
|
51
|
+
}
|
|
52
|
+
return { top, left, bottom, right };
|
|
53
|
+
}
|
|
54
|
+
const DRAG_CONTROL_OPTION_KEYS = new Set(['distanceThreshold', 'snapToCursor']);
|
|
55
|
+
/**
|
|
56
|
+
* Runtime-validate DragControls.start options (G-INV-8; seal review r6/r7/r8).
|
|
57
|
+
* Shared by native and web so both engines refuse malformed options identically.
|
|
58
|
+
* Omitted options stay undefined (pin defaults at the session); present keys must match shape.
|
|
59
|
+
* Closed plain-object shape only — unknown keys and non-plain objects refuse loud.
|
|
60
|
+
*/
|
|
61
|
+
export function validateDragControlOptions(options) {
|
|
62
|
+
if (options === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
if (typeof options !== 'object' ||
|
|
65
|
+
options === null ||
|
|
66
|
+
Array.isArray(options) ||
|
|
67
|
+
Object.getPrototypeOf(options) !== Object.prototype) {
|
|
68
|
+
throw new Error(`DragControls.start options must be a plain object, got ${describeValue(options)} ` +
|
|
69
|
+
'(REQ-GESTURE-026; G-INV-8).');
|
|
70
|
+
}
|
|
71
|
+
const record = options;
|
|
72
|
+
// ONE ownKeys snapshot (seal review r10 major): never call getOwnPropertyNames and
|
|
73
|
+
// getOwnPropertySymbols separately — a Proxy can change between the two reads.
|
|
74
|
+
const ownKeys = Reflect.ownKeys(record);
|
|
75
|
+
for (const key of ownKeys) {
|
|
76
|
+
if (typeof key === 'symbol') {
|
|
77
|
+
throw new Error(`DragControls.start options: symbol keys are not accepted (REQ-GESTURE-026; G-INV-8).`);
|
|
78
|
+
}
|
|
79
|
+
if (!DRAG_CONTROL_OPTION_KEYS.has(key)) {
|
|
80
|
+
throw new Error(`DragControls.start options: unknown key '${key}' — only distanceThreshold and ` +
|
|
81
|
+
`snapToCursor are accepted (REQ-GESTURE-026; G-INV-8).`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// ONE snapshot per key (seal review r9 major): never re-read an accessor that can change.
|
|
85
|
+
const out = {};
|
|
86
|
+
if (Object.prototype.hasOwnProperty.call(record, 'distanceThreshold')) {
|
|
87
|
+
const value = record.distanceThreshold;
|
|
88
|
+
if (value !== undefined) {
|
|
89
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
90
|
+
throw new Error(`DragControls.start distanceThreshold must be a finite non-negative number, got ` +
|
|
91
|
+
`${describeValue(value)} (REQ-GESTURE-026; G-INV-8).`);
|
|
92
|
+
}
|
|
93
|
+
out.distanceThreshold = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (Object.prototype.hasOwnProperty.call(record, 'snapToCursor')) {
|
|
97
|
+
const value = record.snapToCursor;
|
|
98
|
+
if (value !== undefined) {
|
|
99
|
+
if (typeof value !== 'boolean') {
|
|
100
|
+
throw new Error(`DragControls.start snapToCursor must be a boolean, got ` +
|
|
101
|
+
`${describeValue(value)} (REQ-GESTURE-026; G-INV-8).`);
|
|
102
|
+
}
|
|
103
|
+
out.snapToCursor = value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
// The dragged axes in recognizer-field order (deterministic for every consumer loop).
|
|
109
|
+
export function resolvedDragAxes(config) {
|
|
110
|
+
return ['x', 'y'].filter((axis) => config[axis] !== undefined);
|
|
111
|
+
}
|
|
112
|
+
// The fixed-at-mount comparison signature (REQ-API-016, shared by BOTH engines so the verdict
|
|
113
|
+
// is identical by construction): the resolved config canonicalized for STABLE equality. The
|
|
114
|
+
// element-ref channel (REQ-API-042) contributes the ref OBJECT'S identity via a process-local
|
|
115
|
+
// token, never its payload — `.current` is a live host element (a DOM node on web: circular
|
|
116
|
+
// under JSON.stringify; a host component ref on device), and the measured box re-resolves per
|
|
117
|
+
// grab regardless (REQ-GESTURE-028). Passing the same ref object every render (the universal
|
|
118
|
+
// useRef pattern) is a NO-OP; swapping the ref object IS a config change → the refusal fires.
|
|
119
|
+
const refIdentityTokens = new WeakMap();
|
|
120
|
+
let nextRefIdentityToken = 0;
|
|
121
|
+
function refIdentityToken(ref) {
|
|
122
|
+
const existing = refIdentityTokens.get(ref);
|
|
123
|
+
if (existing !== undefined)
|
|
124
|
+
return existing;
|
|
125
|
+
const token = (nextRefIdentityToken += 1);
|
|
126
|
+
refIdentityTokens.set(ref, token);
|
|
127
|
+
return token;
|
|
128
|
+
}
|
|
129
|
+
export function dragConfigMountSignature(config) {
|
|
130
|
+
if (config === null)
|
|
131
|
+
return 'null';
|
|
132
|
+
const axis = (axisConfig) => axisConfig === undefined
|
|
133
|
+
? undefined
|
|
134
|
+
: {
|
|
135
|
+
...(axisConfig.constraints === undefined ? {} : { constraints: axisConfig.constraints }),
|
|
136
|
+
...(axisConfig.snap === undefined ? {} : { snap: axisConfig.snap }),
|
|
137
|
+
...(axisConfig.constraintsRef === undefined
|
|
138
|
+
? {}
|
|
139
|
+
: {
|
|
140
|
+
constraintsRef: {
|
|
141
|
+
ref: refIdentityToken(axisConfig.constraintsRef.ref),
|
|
142
|
+
elastic: axisConfig.constraintsRef.elastic,
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
return JSON.stringify({
|
|
147
|
+
...(config.x === undefined ? {} : { x: axis(config.x) }),
|
|
148
|
+
...(config.y === undefined ? {} : { y: axis(config.y) }),
|
|
149
|
+
dragPropagation: config.dragPropagation,
|
|
150
|
+
dragListener: config.dragListener,
|
|
151
|
+
dragControlsToken: config.dragControlsToken,
|
|
152
|
+
dragDirectionLock: config.dragDirectionLock,
|
|
153
|
+
dragMomentum: config.dragMomentum,
|
|
154
|
+
dragTransition: config.dragTransition,
|
|
155
|
+
dragSnapToOrigin: config.dragSnapToOrigin,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function describeShape(value) {
|
|
159
|
+
if (value === null)
|
|
160
|
+
return 'null';
|
|
161
|
+
if (Array.isArray(value))
|
|
162
|
+
return 'an array';
|
|
163
|
+
return typeof value;
|
|
164
|
+
}
|
|
165
|
+
// A SAFE value formatter for typed error messages (review minor): the inputs are `unknown`, so
|
|
166
|
+
// `JSON.stringify` would THROW on a BigInt (and drop `undefined`/functions/symbols) — a raw
|
|
167
|
+
// serialization TypeError instead of the typed drag error naming the property. This never throws:
|
|
168
|
+
// primitives stringify directly, BigInt gets an `n` suffix, everything else falls back to the shape.
|
|
169
|
+
// Exported (review r4 minor 844ee5e) so the native drag wiring + handoff binding format recognizer
|
|
170
|
+
// fields through this ONE safe formatter — no duplicated JSON.stringify that a BigInt field crashes.
|
|
171
|
+
export function describeValue(value) {
|
|
172
|
+
// TOTAL even for hostile objects (M3 r2 major 830433fc74f2): the SHAPE probe itself can
|
|
173
|
+
// throw — including an Error's hostile `message` getter or Array.isArray on a REVOKED PROXY
|
|
174
|
+
// — and a throwing formatter turns every typed refusal built over it into a raw crash with
|
|
175
|
+
// zero reports. The catch is the formatter's totality contract, not error suppression: the
|
|
176
|
+
// refusal that carries this description is the loud path.
|
|
177
|
+
try {
|
|
178
|
+
const t = typeof value;
|
|
179
|
+
if (t === 'bigint')
|
|
180
|
+
return `${String(value)}n`;
|
|
181
|
+
if (t === 'string')
|
|
182
|
+
return `"${String(value)}"`;
|
|
183
|
+
if (t === 'number' || t === 'boolean' || t === 'undefined' || t === 'symbol')
|
|
184
|
+
return String(value);
|
|
185
|
+
if (value instanceof Error)
|
|
186
|
+
return value.message;
|
|
187
|
+
return describeShape(value);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return 'an unreadable object (a revoked proxy or a throwing probe trap)';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Motion's constraint keys per axis: the drag value is a single axis, so exactly that axis's
|
|
194
|
+
// bound pair applies; the other pair is an axis-inconsistent config (REQ-API-021), refused.
|
|
195
|
+
const AXIS_BOUNDS = {
|
|
196
|
+
x: { lower: 'left', upper: 'right' },
|
|
197
|
+
y: { lower: 'top', upper: 'bottom' },
|
|
198
|
+
};
|
|
199
|
+
function requireFiniteBound(value, key) {
|
|
200
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
201
|
+
throw new Error(`dragConstraints.${key} must be a finite number, got ${describeValue(value)} (REQ-API-021).`);
|
|
202
|
+
}
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
// Motion's dragElastic: number in [0, 1]; R15 D5 also ships boolean (true → pin 0.35, false → 0).
|
|
206
|
+
// Per-side object form stays deferred until a demand case. Omitted = pinned oracle default (0.35).
|
|
207
|
+
function resolveElastic(elastic) {
|
|
208
|
+
if (elastic === undefined)
|
|
209
|
+
return DEFAULT_DRAG_ELASTIC;
|
|
210
|
+
// R15 D5 (REQ-API-045): bare true/false are the pin's boolean forms.
|
|
211
|
+
if (elastic === true)
|
|
212
|
+
return DEFAULT_DRAG_ELASTIC;
|
|
213
|
+
if (elastic === false)
|
|
214
|
+
return 0;
|
|
215
|
+
if (typeof elastic !== 'number') {
|
|
216
|
+
throw new Error(`dragElastic ${describeShape(elastic)} forms (per-side object) are deferred — ` +
|
|
217
|
+
'a number in [0, 1] or boolean is the D5 shape (REQ-API-045).');
|
|
218
|
+
}
|
|
219
|
+
if (!Number.isFinite(elastic) || elastic < 0 || elastic > 1) {
|
|
220
|
+
throw new Error(`dragElastic must be in [0, 1], got ${elastic} (REQ-API-021).`);
|
|
221
|
+
}
|
|
222
|
+
return elastic;
|
|
223
|
+
}
|
|
224
|
+
// The constraints record shape checks shared by every drag form (review major: a typo like
|
|
225
|
+
// `typoBound` must fail loud, never be an accepted-but-ignored config — REQ-API-013/-015,
|
|
226
|
+
// G-INV-8). The numeric form accepts only the four bound keys; the ref form is recognized
|
|
227
|
+
// structurally by `current` and may carry unrelated metadata (review r9 minor 2), never a
|
|
228
|
+
// bound key. Single-axis resolution catches the wrong-axis pair below with a more specific
|
|
229
|
+
// message.
|
|
230
|
+
function requireConstraintsRecord(constraints) {
|
|
231
|
+
if (typeof constraints !== 'object' || constraints === null || Array.isArray(constraints)) {
|
|
232
|
+
throw new Error(`dragConstraints must be a { left/right/top/bottom } object or an element ref ({ current }), got ${describeShape(constraints)} (REQ-API-020/-042).`);
|
|
233
|
+
}
|
|
234
|
+
const record = constraints;
|
|
235
|
+
const keys = Object.keys(record);
|
|
236
|
+
if ('current' in record) {
|
|
237
|
+
// REQ-API-042 (R15 D2): the element-ref form ships — recognized STRUCTURALLY by `current`
|
|
238
|
+
// (upstream Motion's isRefObject). The exclusivity law prohibits mixing `current` with
|
|
239
|
+
// NUMERIC BOUNDS (left/right/top/bottom) — never merged into one config (fail-loud,
|
|
240
|
+
// REQ-API-015); unrelated ref metadata rides along ignored (review r9 minor 2 — upstream
|
|
241
|
+
// recognizes a ref by `current` alone, so a metadata-carrying ref is valid input).
|
|
242
|
+
for (const key of keys) {
|
|
243
|
+
if (key === 'left' || key === 'right' || key === 'top' || key === 'bottom') {
|
|
244
|
+
throw new Error("dragConstraints with 'current' is the element-ref form — it never merges with numeric " +
|
|
245
|
+
`bounds (got '${key}' alongside); pass either a ref ({ current }) or { left/right/top/bottom } (REQ-API-042).`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return record;
|
|
249
|
+
}
|
|
250
|
+
for (const key of keys) {
|
|
251
|
+
if (key !== 'left' && key !== 'right' && key !== 'top' && key !== 'bottom') {
|
|
252
|
+
throw new Error(`dragConstraints has an unknown key '${key}' — only { left, right, top, bottom } or an ` +
|
|
253
|
+
'element ref ({ current }) are valid (REQ-API-015/-042); an unrecognized bound is a typo, refused not ignored.');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return record;
|
|
257
|
+
}
|
|
258
|
+
// Narrow a validated constraints record to the element-ref form (REQ-API-042): `requireConstraintsRecord`
|
|
259
|
+
// already refused any numeric bound key alongside `current`, so the presence check IS the
|
|
260
|
+
// discrimination (unrelated metadata is tolerated — review r9 minor 2). The resolved config
|
|
261
|
+
// carries this object BY IDENTITY — the grab-time measure reads `.current` lazily
|
|
262
|
+
// (native-motion's per-grab measure channel); a re-wrap would freeze the pre-commit null.
|
|
263
|
+
function isConstraintsRefRecord(record) {
|
|
264
|
+
return 'current' in record;
|
|
265
|
+
}
|
|
266
|
+
// Resolve one axis's bound pair to core's `DragConstraints {min, max, elastic}`. Presence =
|
|
267
|
+
// defined value, not `key in record` (exactOptionalPropertyTypes + Motion parity):
|
|
268
|
+
// `{ left: 0, right: undefined }` is a legal public shape meaning "right omitted / open upper".
|
|
269
|
+
// One-sided numeric form (REQ-API-024 shipping slice): open side is ±Infinity so
|
|
270
|
+
// applyConstraints skips that side. Authored bounds still require finite numbers.
|
|
271
|
+
function resolveAxisBounds(axis, lowerRaw, upperRaw, elastic) {
|
|
272
|
+
const bounds = AXIS_BOUNDS[axis];
|
|
273
|
+
const hasLower = lowerRaw !== undefined;
|
|
274
|
+
const hasUpper = upperRaw !== undefined;
|
|
275
|
+
const min = hasLower ? requireFiniteBound(lowerRaw, bounds.lower) : Number.NEGATIVE_INFINITY;
|
|
276
|
+
const max = hasUpper ? requireFiniteBound(upperRaw, bounds.upper) : Number.POSITIVE_INFINITY;
|
|
277
|
+
if (hasLower && hasUpper && min > max) {
|
|
278
|
+
throw new Error(`dragConstraints.${bounds.lower} (${min}) must be <= dragConstraints.${bounds.upper} (${max}) (REQ-API-021).`);
|
|
279
|
+
}
|
|
280
|
+
return { min, max, elastic: resolveElastic(elastic) };
|
|
281
|
+
}
|
|
282
|
+
// Convert Motion's `dragConstraints {left/right/top/bottom}` + `dragElastic` to core's
|
|
283
|
+
// `DragConstraints {min, max, elastic}` for the declared axis.
|
|
284
|
+
function resolveConstraints(axis, constraints, elastic) {
|
|
285
|
+
const record = requireConstraintsRecord(constraints);
|
|
286
|
+
const bounds = AXIS_BOUNDS[axis];
|
|
287
|
+
const other = AXIS_BOUNDS[axis === 'x' ? 'y' : 'x'];
|
|
288
|
+
// Same defined-value presence as open-side omission (review major 4ec3a607f2bd): public types
|
|
289
|
+
// mark cross-axis keys as `?: undefined`, so `{ left: 0, top: undefined }` is a legal pass-through
|
|
290
|
+
// shape. `key in record` would refuse it; only a DEFINED off-axis number is inconsistent.
|
|
291
|
+
if (record[other.lower] !== undefined || record[other.upper] !== undefined) {
|
|
292
|
+
throw new Error(`dragConstraints declares ${other.lower}/${other.upper} but drag is '${axis}' — bounds for ` +
|
|
293
|
+
'a non-dragged axis are inconsistent with the declared axis and are refused, never ' +
|
|
294
|
+
'ignored (REQ-API-021).');
|
|
295
|
+
}
|
|
296
|
+
const lowerRaw = record[bounds.lower];
|
|
297
|
+
const upperRaw = record[bounds.upper];
|
|
298
|
+
if (lowerRaw === undefined && upperRaw === undefined) {
|
|
299
|
+
throw new Error(`dragConstraints declares no bound for axis '${axis}' — expected ${bounds.lower} and/or ` +
|
|
300
|
+
`${bounds.upper} (REQ-API-020; one-sided numeric form ships).`);
|
|
301
|
+
}
|
|
302
|
+
return resolveAxisBounds(axis, lowerRaw, upperRaw, elastic);
|
|
303
|
+
}
|
|
304
|
+
// R15 D1 (REQ-API-041 / REQ-GESTURE-024): both-axes constraints — every bound key is legal and
|
|
305
|
+
// each axis resolves independently; an axis with no defined bound drags free (no channel entry).
|
|
306
|
+
function resolveBothAxesConstraints(constraints, elastic) {
|
|
307
|
+
const record = requireConstraintsRecord(constraints);
|
|
308
|
+
// ONE read per bound, captured before presence checks AND resolution (review round-12 major):
|
|
309
|
+
// an accessor-backed bound must be checked and resolved from the SAME snapshot.
|
|
310
|
+
const left = record['left'];
|
|
311
|
+
const right = record['right'];
|
|
312
|
+
const top = record['top'];
|
|
313
|
+
const bottom = record['bottom'];
|
|
314
|
+
const hasX = left !== undefined || right !== undefined;
|
|
315
|
+
const hasY = top !== undefined || bottom !== undefined;
|
|
316
|
+
if (!hasX && !hasY) {
|
|
317
|
+
throw new Error('dragConstraints declares no bound for either axis — expected at least one of ' +
|
|
318
|
+
'left/right/top/bottom (REQ-API-020/-041; one-sided numeric form ships).');
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
...(hasX ? { x: resolveAxisBounds('x', left, right, elastic) } : {}),
|
|
322
|
+
...(hasY ? { y: resolveAxisBounds('y', top, bottom, elastic) } : {}),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
// dragSnapPoints — the ratified Native-Motion extension: ABSOLUTE positions, one array per axis.
|
|
326
|
+
function resolveSnap(axis, snap) {
|
|
327
|
+
if (typeof snap !== 'object' || snap === null || Array.isArray(snap)) {
|
|
328
|
+
throw new Error(`dragSnapPoints must be an object with per-axis arrays ({x?, y?}), got ${describeShape(snap)} (REQ-API-020).`);
|
|
329
|
+
}
|
|
330
|
+
const record = snap;
|
|
331
|
+
// Reject UNKNOWN keys (review major): a typo like `typoAxis` must fail loud, never be an
|
|
332
|
+
// accepted-but-ignored config (REQ-API-013/-015, G-INV-8). Only `x`/`y` are valid axes.
|
|
333
|
+
for (const key of Object.keys(record)) {
|
|
334
|
+
if (key !== 'x' && key !== 'y') {
|
|
335
|
+
throw new Error(`dragSnapPoints has an unknown key '${key}' — only per-axis arrays { x?, y? } are valid ` +
|
|
336
|
+
'(REQ-API-015); an unrecognized axis is a typo, refused not ignored.');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const declaredAxes = ['x', 'y'].filter((key) => record[key] !== undefined);
|
|
340
|
+
if (declaredAxes.length === 0) {
|
|
341
|
+
throw new Error('dragSnapPoints declares no axis — expected absolute snap positions under x and/or y (REQ-API-020).');
|
|
342
|
+
}
|
|
343
|
+
const offAxis = declaredAxes.find((key) => key !== axis);
|
|
344
|
+
if (offAxis !== undefined) {
|
|
345
|
+
throw new Error(`dragSnapPoints.${offAxis} is declared but drag is '${axis}' — snap points for a ` +
|
|
346
|
+
'non-dragged axis are refused, never ignored (REQ-API-021).');
|
|
347
|
+
}
|
|
348
|
+
const points = record[axis];
|
|
349
|
+
if (!Array.isArray(points)) {
|
|
350
|
+
throw new Error(`dragSnapPoints.${axis} must be an array of finite numbers, got ${describeShape(points)} (REQ-API-020).`);
|
|
351
|
+
}
|
|
352
|
+
if (points.length === 0) {
|
|
353
|
+
throw new Error(`dragSnapPoints.${axis} must declare at least one snap point (REQ-API-020).`);
|
|
354
|
+
}
|
|
355
|
+
for (const point of points) {
|
|
356
|
+
if (typeof point !== 'number' || !Number.isFinite(point)) {
|
|
357
|
+
// describeValue (safe) not String() (review r5 major 21): String(Object.create(null)) throws
|
|
358
|
+
// a raw "No default value" TypeError; the typed error must name the field for any bad member.
|
|
359
|
+
throw new Error(`dragSnapPoints.${axis} must contain only finite numbers, got ${describeValue(point)} (REQ-API-020).`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// SNAPSHOT the points (review r8 major 25): return a COPY, never the caller's array by reference.
|
|
363
|
+
// The resolved config is retained as the fixed-at-mount config and compared against re-resolves; an
|
|
364
|
+
// aliased array would let a later in-place mutation change the "mounted" config too (comparison
|
|
365
|
+
// bypassed → dynamic gestures silently enable, post-mount out-of-constraints validation bypassed, and
|
|
366
|
+
// native — which drives the gesture session off this array — diverges from web). One copy at the
|
|
367
|
+
// boundary makes every downstream holder independent of the caller.
|
|
368
|
+
return { points: [...points] };
|
|
369
|
+
}
|
|
370
|
+
// The measure hook requires the element-ref constraints form (R15 D2 review r3 major 1): the
|
|
371
|
+
// pin fires it only inside resolveRefConstraints — with numeric or absent constraints it would
|
|
372
|
+
// be a silently dead callback (G-INV-8). Called after constraints resolution in every drag
|
|
373
|
+
// branch; the function SHAPE stays entry-gated (a malformed callback drops there).
|
|
374
|
+
function requireMeasureHookHasRefConstraints(onMeasureDragConstraints, constraintsRef) {
|
|
375
|
+
if (onMeasureDragConstraints !== undefined && constraintsRef === undefined) {
|
|
376
|
+
throw new Error('onMeasureDragConstraints requires an element-ref dragConstraints ({ current }) — the pin ' +
|
|
377
|
+
'fires the measure hook only inside resolveRefConstraints; with numeric or absent ' +
|
|
378
|
+
'constraints it would be a silently dead callback (REQ-API-042, G-INV-8).');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function isDragControlsHandle(value) {
|
|
382
|
+
if (typeof value !== 'object' || value === null)
|
|
383
|
+
return false;
|
|
384
|
+
const hub = value;
|
|
385
|
+
// subscribe is required: web motion.div and native MotionView both subscribe; a structural
|
|
386
|
+
// start/cancel/stop-only object is not a hub (seal review r8 major — shared parity).
|
|
387
|
+
return (typeof hub['start'] === 'function' &&
|
|
388
|
+
typeof hub['cancel'] === 'function' &&
|
|
389
|
+
typeof hub['stop'] === 'function' &&
|
|
390
|
+
typeof hub['subscribe'] === 'function');
|
|
391
|
+
}
|
|
392
|
+
function resolveOrThrow(input) {
|
|
393
|
+
// ONE read per member, captured at the boundary (reviews round-8 / round-11): a mutating
|
|
394
|
+
// getter must validate and resolve the SAME snapshot — never two reads that can disagree.
|
|
395
|
+
const { drag, dragConstraints, dragElastic, dragSnapPoints, dragPropagation, onMeasureDragConstraints, dragControls, dragListener, dragDirectionLock, dragMomentum, dragTransition, dragSnapToOrigin, } = input;
|
|
396
|
+
if (drag === undefined) {
|
|
397
|
+
// Config without the capability is refused, never ignored.
|
|
398
|
+
const orphan = dragConstraints !== undefined
|
|
399
|
+
? 'dragConstraints'
|
|
400
|
+
: dragElastic !== undefined
|
|
401
|
+
? 'dragElastic'
|
|
402
|
+
: dragSnapPoints !== undefined
|
|
403
|
+
? 'dragSnapPoints'
|
|
404
|
+
: onMeasureDragConstraints !== undefined
|
|
405
|
+
? 'onMeasureDragConstraints'
|
|
406
|
+
: dragControls !== undefined
|
|
407
|
+
? 'dragControls'
|
|
408
|
+
: dragListener !== undefined
|
|
409
|
+
? 'dragListener'
|
|
410
|
+
: dragDirectionLock !== undefined
|
|
411
|
+
? 'dragDirectionLock'
|
|
412
|
+
: dragMomentum !== undefined
|
|
413
|
+
? 'dragMomentum'
|
|
414
|
+
: dragTransition !== undefined
|
|
415
|
+
? 'dragTransition'
|
|
416
|
+
: dragSnapToOrigin !== undefined
|
|
417
|
+
? 'dragSnapToOrigin'
|
|
418
|
+
: 'dragPropagation';
|
|
419
|
+
throw new Error(`${orphan} requires drag to be 'x', 'y', or true (REQ-API-021).`);
|
|
420
|
+
}
|
|
421
|
+
if (dragPropagation !== undefined && typeof dragPropagation !== 'boolean') {
|
|
422
|
+
throw new Error(`dragPropagation must be a boolean, got ${describeValue(dragPropagation)} (REQ-API-021).`);
|
|
423
|
+
}
|
|
424
|
+
// R15 D3 (REQ-API-043 / G-INV-8): shared severity boundary — never accept-but-inert.
|
|
425
|
+
if (dragListener !== undefined && typeof dragListener !== 'boolean') {
|
|
426
|
+
throw new Error(`dragListener must be a boolean, got ${describeValue(dragListener)} (REQ-API-043).`);
|
|
427
|
+
}
|
|
428
|
+
if (dragControls !== undefined && !isDragControlsHandle(dragControls)) {
|
|
429
|
+
throw new Error(`dragControls must be a DragControls hub (start/cancel/stop/subscribe), got ${describeValue(dragControls)} (REQ-API-043).`);
|
|
430
|
+
}
|
|
431
|
+
// R15 D4 (REQ-API-044): direction lock is boolean; omitted === false.
|
|
432
|
+
if (dragDirectionLock !== undefined && typeof dragDirectionLock !== 'boolean') {
|
|
433
|
+
throw new Error(`dragDirectionLock must be a boolean, got ${describeValue(dragDirectionLock)} (REQ-API-044).`);
|
|
434
|
+
}
|
|
435
|
+
// R15 D5 (REQ-API-045): release physics props.
|
|
436
|
+
if (dragMomentum !== undefined && typeof dragMomentum !== 'boolean') {
|
|
437
|
+
throw new Error(`dragMomentum must be a boolean, got ${describeValue(dragMomentum)} (REQ-API-045).`);
|
|
438
|
+
}
|
|
439
|
+
if (dragSnapToOrigin !== undefined &&
|
|
440
|
+
dragSnapToOrigin !== true &&
|
|
441
|
+
dragSnapToOrigin !== false &&
|
|
442
|
+
dragSnapToOrigin !== 'x' &&
|
|
443
|
+
dragSnapToOrigin !== 'y') {
|
|
444
|
+
throw new Error(`dragSnapToOrigin must be boolean | 'x' | 'y', got ${describeValue(dragSnapToOrigin)} (REQ-API-045).`);
|
|
445
|
+
}
|
|
446
|
+
let resolvedDragTransition = null;
|
|
447
|
+
if (dragTransition !== undefined) {
|
|
448
|
+
if (dragTransition === null ||
|
|
449
|
+
typeof dragTransition !== 'object' ||
|
|
450
|
+
Array.isArray(dragTransition)) {
|
|
451
|
+
throw new Error(`dragTransition must be an object with bounceStiffness/bounceDamping numbers, got ${describeValue(dragTransition)} (REQ-API-045).`);
|
|
452
|
+
}
|
|
453
|
+
const t = dragTransition;
|
|
454
|
+
// Allowlist only bounce fields (seal H3): unknown keys refuse, never silent strip (G-INV-8).
|
|
455
|
+
for (const key of Object.keys(t)) {
|
|
456
|
+
if (key !== 'bounceStiffness' && key !== 'bounceDamping') {
|
|
457
|
+
throw new Error(`dragTransition unknown key '${key}' — only bounceStiffness/bounceDamping ship at D5; ` +
|
|
458
|
+
'other InertiaOptions stay deferred (REQ-API-045 / G-INV-8).');
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (t.bounceStiffness !== undefined && typeof t.bounceStiffness !== 'number') {
|
|
462
|
+
throw new Error(`dragTransition.bounceStiffness must be a number, got ${describeValue(t.bounceStiffness)} (REQ-API-045).`);
|
|
463
|
+
}
|
|
464
|
+
if (t.bounceDamping !== undefined && typeof t.bounceDamping !== 'number') {
|
|
465
|
+
throw new Error(`dragTransition.bounceDamping must be a number, got ${describeValue(t.bounceDamping)} (REQ-API-045).`);
|
|
466
|
+
}
|
|
467
|
+
if ((typeof t.bounceStiffness === 'number' && !Number.isFinite(t.bounceStiffness)) ||
|
|
468
|
+
(typeof t.bounceDamping === 'number' && !Number.isFinite(t.bounceDamping))) {
|
|
469
|
+
throw new Error(`dragTransition bounceStiffness/bounceDamping must be finite numbers (REQ-API-045).`);
|
|
470
|
+
}
|
|
471
|
+
// R16 review minor 7: the bounce pair names the inertia generator's boundary spring
|
|
472
|
+
// (REQ-GESTURE-027). The pin accepts 0, but core's solver degenerates on a non-positive pair
|
|
473
|
+
// (a silent NaN trajectory on the external channel, a late release-time throw on the host
|
|
474
|
+
// path) — refuse it HERE, at mount, the fail-loud altitude (a tightening, not a parity break).
|
|
475
|
+
if ((typeof t.bounceStiffness === 'number' && t.bounceStiffness <= 0) ||
|
|
476
|
+
(typeof t.bounceDamping === 'number' && t.bounceDamping <= 0)) {
|
|
477
|
+
throw new Error(`dragTransition bounceStiffness/bounceDamping must be greater than zero, got ` +
|
|
478
|
+
`bounceStiffness=${describeValue(t.bounceStiffness)} bounceDamping=${describeValue(t.bounceDamping)} (REQ-API-045).`);
|
|
479
|
+
}
|
|
480
|
+
resolvedDragTransition = {
|
|
481
|
+
...(typeof t.bounceStiffness === 'number' ? { bounceStiffness: t.bounceStiffness } : {}),
|
|
482
|
+
...(typeof t.bounceDamping === 'number' ? { bounceDamping: t.bounceDamping } : {}),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
// Canonicalize omission vs the explicit default (review round-3 minor): omitted and
|
|
486
|
+
// `dragPropagation={false}` are the SAME configuration — the fixed-at-mount comparison must
|
|
487
|
+
// never split them (the dragElastic default-normalization precedent).
|
|
488
|
+
// R15 D3: dragListener default true (pin); dragControls identity via WeakMap token.
|
|
489
|
+
// R15 D4: dragDirectionLock default false.
|
|
490
|
+
// R15 D5: dragMomentum default true (pin); dragSnapToOrigin default false; dragTransition null.
|
|
491
|
+
const hubFields = {
|
|
492
|
+
dragPropagation: dragPropagation === true,
|
|
493
|
+
dragListener: dragListener !== false,
|
|
494
|
+
dragControlsToken: dragControls !== undefined && typeof dragControls === 'object' && dragControls !== null
|
|
495
|
+
? refIdentityToken(dragControls)
|
|
496
|
+
: null,
|
|
497
|
+
dragDirectionLock: dragDirectionLock === true,
|
|
498
|
+
dragMomentum: dragMomentum !== false,
|
|
499
|
+
dragTransition: resolvedDragTransition,
|
|
500
|
+
dragSnapToOrigin: dragSnapToOrigin === true || dragSnapToOrigin === 'x' || dragSnapToOrigin === 'y'
|
|
501
|
+
? dragSnapToOrigin
|
|
502
|
+
: false,
|
|
503
|
+
};
|
|
504
|
+
if (dragElastic !== undefined) {
|
|
505
|
+
// Validate even when constraints are absent (U7f no-op uses the value only if
|
|
506
|
+
// constraints exist). Out-of-range / per-side object still refuse.
|
|
507
|
+
resolveElastic(dragElastic);
|
|
508
|
+
}
|
|
509
|
+
if (drag === false) {
|
|
510
|
+
return hubFields;
|
|
511
|
+
}
|
|
512
|
+
if (drag === true) {
|
|
513
|
+
// R15 D1 (REQ-API-041): both-axes drag ships. Snap stays refused with it: the web mapping
|
|
514
|
+
// projects release targets through Motion's single axis-blind modifyTarget (REQ-WEB-013),
|
|
515
|
+
// which per-axis snap sets cannot pass faithfully — a recorded fidelity divergence deferred
|
|
516
|
+
// to its own rung, identical on both engines by construction (this single verdict).
|
|
517
|
+
if (dragSnapPoints !== undefined) {
|
|
518
|
+
throw new Error('dragSnapPoints with both-axes drag (drag={true}) is deferred — the release ' +
|
|
519
|
+
"projection routes through Motion's single axis-blind modifyTarget, which cannot " +
|
|
520
|
+
'express per-axis snap sets faithfully (REQ-WEB-013; recorded fidelity divergence, ' +
|
|
521
|
+
'its own rung).');
|
|
522
|
+
}
|
|
523
|
+
const constraintsRecord = dragConstraints !== undefined ? requireConstraintsRecord(dragConstraints) : undefined;
|
|
524
|
+
const constraintsRef = constraintsRecord !== undefined && isConstraintsRefRecord(constraintsRecord)
|
|
525
|
+
? { ref: constraintsRecord, elastic: resolveElastic(dragElastic) }
|
|
526
|
+
: undefined;
|
|
527
|
+
const both = constraintsRecord !== undefined && constraintsRef === undefined
|
|
528
|
+
? resolveBothAxesConstraints(constraintsRecord, dragElastic)
|
|
529
|
+
: undefined;
|
|
530
|
+
requireMeasureHookHasRefConstraints(onMeasureDragConstraints, constraintsRef);
|
|
531
|
+
return {
|
|
532
|
+
x: constraintsRef !== undefined
|
|
533
|
+
? { constraintsRef }
|
|
534
|
+
: both?.x === undefined
|
|
535
|
+
? {}
|
|
536
|
+
: { constraints: both.x },
|
|
537
|
+
y: constraintsRef !== undefined
|
|
538
|
+
? { constraintsRef }
|
|
539
|
+
: both?.y === undefined
|
|
540
|
+
? {}
|
|
541
|
+
: { constraints: both.y },
|
|
542
|
+
...hubFields,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
if (drag !== 'x' && drag !== 'y') {
|
|
546
|
+
throw new Error(`drag must be 'x', 'y', true, or false, got ${describeValue(drag)} (REQ-API-020/-023).`);
|
|
547
|
+
}
|
|
548
|
+
const constraintsRecord = dragConstraints !== undefined ? requireConstraintsRecord(dragConstraints) : undefined;
|
|
549
|
+
const constraintsRef = constraintsRecord !== undefined && isConstraintsRefRecord(constraintsRecord)
|
|
550
|
+
? { ref: constraintsRecord, elastic: resolveElastic(dragElastic) }
|
|
551
|
+
: undefined;
|
|
552
|
+
// Ref + snap is REFUSED identically on both engines (review r7 major 2): the pin's web
|
|
553
|
+
// engine applies its axis-blind modifyTarget with NO measurement-time bounds check, while
|
|
554
|
+
// native's law is the typed grab-time contradiction refusal (r5 major 20) — the two engines
|
|
555
|
+
// cannot validate the combination identically, so both refuse it at resolve (recorded
|
|
556
|
+
// fidelity divergence, its own rung — the both-axes snap precedent). The wiring keeps a
|
|
557
|
+
// grab-time snap-vs-measured check as the deep defense for host-constructed configs.
|
|
558
|
+
if (constraintsRef !== undefined && dragSnapPoints !== undefined) {
|
|
559
|
+
throw new Error("dragSnapPoints with element-ref dragConstraints is deferred — the pin's web engine " +
|
|
560
|
+
'applies its axis-blind modifyTarget with no measurement-time bounds check, so the ' +
|
|
561
|
+
'snap-vs-bounds contradiction law cannot run identically on both engines; the ' +
|
|
562
|
+
'combination is refused on BOTH (recorded fidelity divergence, its own rung).');
|
|
563
|
+
}
|
|
564
|
+
const constraints = constraintsRecord !== undefined && constraintsRef === undefined
|
|
565
|
+
? resolveConstraints(drag, constraintsRecord, dragElastic)
|
|
566
|
+
: undefined;
|
|
567
|
+
requireMeasureHookHasRefConstraints(onMeasureDragConstraints, constraintsRef);
|
|
568
|
+
// Free-drag (REQ-GESTURE-023): snap is optional. When present, resolve axis/shape; when absent,
|
|
569
|
+
// release uses projected landing (optionally constrained). Empty/wrong-axis snap still fails in
|
|
570
|
+
// resolveSnap.
|
|
571
|
+
const snap = dragSnapPoints !== undefined ? resolveSnap(drag, dragSnapPoints) : undefined;
|
|
572
|
+
// A snap point OUTSIDE the declared hard constraints is a CONTRADICTORY config (review r5 major 20,
|
|
573
|
+
// REQ-GESTURE-015): selectReleaseTarget returns the projected-then-snapped point UNCLAMPED when the
|
|
574
|
+
// release is within bounds, so an out-of-bounds snap would settle the box outside the allowed
|
|
575
|
+
// region — and diverge from the web engine's constrained inertia path. Reject the incompatible set
|
|
576
|
+
// at THIS boundary (both engines call resolveDragConfig) so every terminal state stays within the
|
|
577
|
+
// hard bounds by construction. Bounds are inclusive: a snap AT min/max is valid. The ref form's
|
|
578
|
+
// bounds are measured per grab — snap-vs-ref-bounds validates at the grab-time measure instead.
|
|
579
|
+
if (constraints !== undefined && snap !== undefined) {
|
|
580
|
+
for (const point of snap.points) {
|
|
581
|
+
if (point < constraints.min || point > constraints.max) {
|
|
582
|
+
throw new Error(`dragSnapPoints.${drag} contains ${point}, outside the declared dragConstraints ` +
|
|
583
|
+
`[${constraints.min}, ${constraints.max}] — a snap outside the hard bounds would settle ` +
|
|
584
|
+
'out of range; every snap point must lie within the constraints (REQ-GESTURE-015).');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const axisConfig = {
|
|
589
|
+
...(constraints === undefined ? {} : { constraints }),
|
|
590
|
+
...(constraintsRef === undefined ? {} : { constraintsRef }),
|
|
591
|
+
...(snap === undefined ? {} : { snap }),
|
|
592
|
+
};
|
|
593
|
+
return drag === 'x' ? { x: axisConfig, ...hubFields } : { y: axisConfig, ...hubFields };
|
|
594
|
+
}
|
|
595
|
+
// REQ-API-016: an onDrag* callback on an element with no `drag` axis would be an
|
|
596
|
+
// accepted-but-ignored prop — it can never fire. Refused under the severity law, never silent.
|
|
597
|
+
export function gateOrphanDragCallbacks(callbackName, severity, report) {
|
|
598
|
+
const error = new Error(`${callbackName} requires drag to be 'x', 'y', or true — a drag callback without a drag axis can ` +
|
|
599
|
+
'never fire and is refused, never silently ignored (REQ-API-016/-021).');
|
|
600
|
+
if (severity === 'development')
|
|
601
|
+
throw error;
|
|
602
|
+
report(error);
|
|
603
|
+
}
|
|
604
|
+
// The resolver catches values from user-owned proxy traps as well as ordinary Errors. Normalize
|
|
605
|
+
// every catch at this boundary; even an Error instance can have a hostile `message` getter.
|
|
606
|
+
// `describeValue` owns every property read under its total guard, so both severity lanes retain
|
|
607
|
+
// their typed drag refusal instead of leaking a secondary coercion failure.
|
|
608
|
+
function normalizedResolverError(error) {
|
|
609
|
+
return new Error(`drag configuration resolution failed: ${describeValue(error)} (REQ-API-021).`);
|
|
610
|
+
}
|
|
611
|
+
// The severity gate (REQ-API-021): development throws through; production reports the typed error
|
|
612
|
+
// and refuses the whole gesture surface for this element (null) — fail-closed, no partial config.
|
|
613
|
+
export function resolveDragConfig(input, severity, report) {
|
|
614
|
+
try {
|
|
615
|
+
return resolveOrThrow(input);
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
const normalized = normalizedResolverError(error);
|
|
619
|
+
if (severity === 'development')
|
|
620
|
+
throw normalized;
|
|
621
|
+
report(normalized);
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
}
|