@unrulysystems/native-motion-web 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 +14 -0
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/package.json +40 -0
- package/src/AnimatePresence.tsx +284 -0
- package/src/LayoutGroup.tsx +94 -0
- package/src/LayoutScrollOffsetProvider.tsx +30 -0
- package/src/MotionConfig.tsx +136 -0
- package/src/MotionLengthLayoutProvider.tsx +18 -0
- package/src/MotionRoot.tsx +13 -0
- package/src/View.tsx +1356 -0
- package/src/animate.ts +39 -0
- package/src/disposition.ts +167 -0
- package/src/dragControls.ts +3 -0
- package/src/errors.ts +30 -0
- package/src/index.ts +157 -0
- package/src/mode.ts +12 -0
- package/src/normalize.ts +324 -0
- package/src/snap.ts +82 -0
- package/src/useAnimate.ts +18 -0
- package/src/useCycle.ts +12 -0
- package/src/useScroll.ts +19 -0
- package/src/webTransition.ts +69 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
// REQ-API-035 / REQ-WEB-022: motion/react owns web configuration inheritance and transition
|
|
3
|
+
// behavior. This adapter retains Native Motion's named aliases and validates only its own wrapper
|
|
4
|
+
// props; it never substitutes native transition semantics for the pin.
|
|
5
|
+
|
|
6
|
+
import type { Transition as CoreTransition } from '@unrulysystems/native-motion-core'
|
|
7
|
+
import {
|
|
8
|
+
MotionConfig as PinMotionConfig,
|
|
9
|
+
type MotionConfigProps as PinMotionConfigProps,
|
|
10
|
+
} from 'motion/react'
|
|
11
|
+
import { createContext, createElement, useContext, type ReactNode } from 'react'
|
|
12
|
+
import { MotionWebRejectionError } from './errors'
|
|
13
|
+
import { ambientMode } from './mode'
|
|
14
|
+
import type { NormalizeOptions } from './normalize'
|
|
15
|
+
import { mapWebTransitionAliases, type WebTransition } from './webTransition'
|
|
16
|
+
|
|
17
|
+
/** Retained compatibility seam; web hosts no longer consume a parallel validation context. */
|
|
18
|
+
export interface MotionConfigValidationValue {
|
|
19
|
+
readonly transition: undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_VALIDATION: MotionConfigValidationValue = Object.freeze({ transition: undefined })
|
|
23
|
+
export const MotionConfigValidationContext = createContext(DEFAULT_VALIDATION)
|
|
24
|
+
export function useMotionConfigValidation(): MotionConfigValidationValue {
|
|
25
|
+
return useContext(MotionConfigValidationContext)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ReducedMotionConfig = 'always' | 'never' | 'user'
|
|
29
|
+
export type MotionConfigTransition =
|
|
30
|
+
| NonNullable<PinMotionConfigProps['transition']>
|
|
31
|
+
| (CoreTransition & { readonly inherit?: boolean })
|
|
32
|
+
|
|
33
|
+
export interface MotionConfigProps {
|
|
34
|
+
readonly children?: ReactNode
|
|
35
|
+
readonly transition?: MotionConfigTransition | undefined
|
|
36
|
+
readonly reducedMotion?: ReducedMotionConfig | undefined
|
|
37
|
+
readonly nonce?: string | undefined
|
|
38
|
+
readonly isStatic?: boolean | undefined
|
|
39
|
+
readonly skipAnimations?: boolean | undefined
|
|
40
|
+
readonly transformPagePoint?: PinMotionConfigProps['transformPagePoint']
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const MOTION_CONFIG_OWNER = '<MotionConfig>'
|
|
44
|
+
const REDUCED_MOTION_VALUES = new Set(['always', 'never', 'user'])
|
|
45
|
+
|
|
46
|
+
function refuse(options: NormalizeOptions, message: string): void {
|
|
47
|
+
const error = new MotionWebRejectionError(message)
|
|
48
|
+
if (options.mode === 'development') throw error
|
|
49
|
+
options.report(error)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function mapTransition(
|
|
53
|
+
transition: MotionConfigTransition | undefined,
|
|
54
|
+
options: NormalizeOptions,
|
|
55
|
+
): WebTransition | undefined {
|
|
56
|
+
if (transition === undefined) return undefined
|
|
57
|
+
try {
|
|
58
|
+
return mapWebTransitionAliases(transition as WebTransition, MOTION_CONFIG_OWNER)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!(error instanceof MotionWebRejectionError)) throw error
|
|
61
|
+
if (options.mode === 'development') throw error
|
|
62
|
+
options.report(error)
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function gateEnum<T extends string>(
|
|
68
|
+
name: string,
|
|
69
|
+
value: unknown,
|
|
70
|
+
accepted: ReadonlySet<string>,
|
|
71
|
+
options: NormalizeOptions,
|
|
72
|
+
): T | undefined {
|
|
73
|
+
if (value === undefined) return undefined
|
|
74
|
+
if (typeof value === 'string' && accepted.has(value)) return value as T
|
|
75
|
+
refuse(options, `${MOTION_CONFIG_OWNER}: invalid ${name}; the wrapper requires a pinned value`)
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function gateBoolean(name: string, value: unknown, options: NormalizeOptions): boolean | undefined {
|
|
80
|
+
if (value === undefined) return undefined
|
|
81
|
+
if (typeof value === 'boolean') return value
|
|
82
|
+
refuse(options, `${MOTION_CONFIG_OWNER}: invalid ${name}; expected a boolean`)
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createMotionConfig(options: NormalizeOptions) {
|
|
87
|
+
return function MotionConfig({
|
|
88
|
+
children,
|
|
89
|
+
transition,
|
|
90
|
+
reducedMotion,
|
|
91
|
+
nonce,
|
|
92
|
+
isStatic,
|
|
93
|
+
skipAnimations,
|
|
94
|
+
transformPagePoint,
|
|
95
|
+
...rest
|
|
96
|
+
}: MotionConfigProps) {
|
|
97
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
98
|
+
if (value !== undefined)
|
|
99
|
+
refuse(options, `${MOTION_CONFIG_OWNER}: unsupported wrapper prop '${key}'`)
|
|
100
|
+
}
|
|
101
|
+
const mappedTransition = mapTransition(transition, options)
|
|
102
|
+
const mappedReduced = gateEnum<ReducedMotionConfig>(
|
|
103
|
+
'reducedMotion',
|
|
104
|
+
reducedMotion,
|
|
105
|
+
REDUCED_MOTION_VALUES,
|
|
106
|
+
options,
|
|
107
|
+
)
|
|
108
|
+
const mappedStatic = gateBoolean('isStatic', isStatic, options)
|
|
109
|
+
const mappedSkip = gateBoolean('skipAnimations', skipAnimations, options)
|
|
110
|
+
if (nonce !== undefined && typeof nonce !== 'string')
|
|
111
|
+
refuse(options, `${MOTION_CONFIG_OWNER}: invalid nonce; expected a string`)
|
|
112
|
+
if (transformPagePoint !== undefined && typeof transformPagePoint !== 'function')
|
|
113
|
+
refuse(options, `${MOTION_CONFIG_OWNER}: invalid transformPagePoint; expected a function`)
|
|
114
|
+
|
|
115
|
+
const forwarded: PinMotionConfigProps = {
|
|
116
|
+
...(mappedTransition === undefined
|
|
117
|
+
? {}
|
|
118
|
+
: { transition: mappedTransition as NonNullable<PinMotionConfigProps['transition']> }),
|
|
119
|
+
...(mappedReduced === undefined ? {} : { reducedMotion: mappedReduced }),
|
|
120
|
+
...(mappedStatic === undefined ? {} : { isStatic: mappedStatic }),
|
|
121
|
+
...(mappedSkip === undefined ? {} : { skipAnimations: mappedSkip }),
|
|
122
|
+
...(typeof nonce === 'string' ? { nonce } : {}),
|
|
123
|
+
...(typeof transformPagePoint === 'function' ? { transformPagePoint } : {}),
|
|
124
|
+
}
|
|
125
|
+
return createElement(PinMotionConfig, forwarded, children)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function consoleReporter(error: Error): void {
|
|
130
|
+
console.error(error)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const MotionConfig = createMotionConfig({
|
|
134
|
+
mode: ambientMode(),
|
|
135
|
+
report: consoleReporter,
|
|
136
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
// Web entry counterpart of native MotionLengthLayoutProvider (REQ-VALUETYPE-013 namespace parity).
|
|
3
|
+
// Pin motion/react measure-resolves %/auto/calc itself — this provider is a no-op passthrough so
|
|
4
|
+
// dual-entry product code can share the same composition without branching.
|
|
5
|
+
|
|
6
|
+
import type { ReactNode } from 'react'
|
|
7
|
+
import type { LengthLayoutContext } from '@unrulysystems/native-motion-core'
|
|
8
|
+
|
|
9
|
+
export interface MotionLengthLayoutProviderProps {
|
|
10
|
+
readonly value: LengthLayoutContext
|
|
11
|
+
readonly children: ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function MotionLengthLayoutProvider({
|
|
15
|
+
children,
|
|
16
|
+
}: MotionLengthLayoutProviderProps): ReactNode {
|
|
17
|
+
return children
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// REQ-API-022 web counterpart (FLAG 5a, specs/SPEC-GESTURE.md) — MotionRoot on
|
|
2
|
+
// the web engine: the root primitive exists on BOTH entries (one-import universal surface, entry
|
|
3
|
+
// value-key parity), but the web engine needs no gesture root — motion/react's recognizers bind
|
|
4
|
+
// per element. Children render unchanged; no wrapper element, no style knob (a web-ignored style
|
|
5
|
+
// would be an accepted-but-ignored prop, REQ-API-016 — the native entry matches).
|
|
6
|
+
|
|
7
|
+
export interface MotionRootProps {
|
|
8
|
+
readonly children?: React.ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function MotionRoot({ children }: MotionRootProps) {
|
|
12
|
+
return <>{children}</>
|
|
13
|
+
}
|