@rootnative/inertia 0.0.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 +33 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/gestureLayer/index.d.mts +119 -0
- package/dist/gestureLayer/index.d.ts +119 -0
- package/dist/gestureLayer/index.js +346 -0
- package/dist/gestureLayer/index.js.map +1 -0
- package/dist/gestureLayer/index.mjs +344 -0
- package/dist/gestureLayer/index.mjs.map +1 -0
- package/dist/index.d.mts +507 -0
- package/dist/index.d.ts +507 -0
- package/dist/index.js +1546 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1515 -0
- package/dist/index.mjs.map +1 -0
- package/dist/motion/Image.d.mts +12 -0
- package/dist/motion/Image.d.ts +12 -0
- package/dist/motion/Image.js +1138 -0
- package/dist/motion/Image.js.map +1 -0
- package/dist/motion/Image.mjs +1132 -0
- package/dist/motion/Image.mjs.map +1 -0
- package/dist/motion/Pressable.d.mts +15 -0
- package/dist/motion/Pressable.d.ts +15 -0
- package/dist/motion/Pressable.js +1138 -0
- package/dist/motion/Pressable.js.map +1 -0
- package/dist/motion/Pressable.mjs +1132 -0
- package/dist/motion/Pressable.mjs.map +1 -0
- package/dist/motion/ScrollView.d.mts +12 -0
- package/dist/motion/ScrollView.d.ts +12 -0
- package/dist/motion/ScrollView.js +1138 -0
- package/dist/motion/ScrollView.js.map +1 -0
- package/dist/motion/ScrollView.mjs +1132 -0
- package/dist/motion/ScrollView.mjs.map +1 -0
- package/dist/motion/Text.d.mts +11 -0
- package/dist/motion/Text.d.ts +11 -0
- package/dist/motion/Text.js +1138 -0
- package/dist/motion/Text.js.map +1 -0
- package/dist/motion/Text.mjs +1132 -0
- package/dist/motion/Text.mjs.map +1 -0
- package/dist/motion/View.d.mts +11 -0
- package/dist/motion/View.d.ts +11 -0
- package/dist/motion/View.js +1138 -0
- package/dist/motion/View.js.map +1 -0
- package/dist/motion/View.mjs +1132 -0
- package/dist/motion/View.mjs.map +1 -0
- package/dist/testing/index.d.mts +57 -0
- package/dist/testing/index.d.ts +57 -0
- package/dist/testing/index.js +19 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/index.mjs +16 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/touch/index.d.mts +146 -0
- package/dist/touch/index.d.ts +146 -0
- package/dist/touch/index.js +166 -0
- package/dist/touch/index.js.map +1 -0
- package/dist/touch/index.mjs +164 -0
- package/dist/touch/index.mjs.map +1 -0
- package/dist/types-cU43dEmH.d.mts +339 -0
- package/dist/types-cU43dEmH.d.ts +339 -0
- package/dist/useGesture-BnY65PlQ.d.ts +84 -0
- package/dist/useGesture-DxtXdz-K.d.mts +84 -0
- package/jest-preset.js +33 -0
- package/jest-setup.js +213 -0
- package/llms.txt +142 -0
- package/package.json +151 -0
- package/src/__type-tests__/animate.test-d.tsx +88 -0
- package/src/__type-tests__/variants.test-d.tsx +67 -0
- package/src/config/MotionConfig.tsx +30 -0
- package/src/config/MotionConfigContext.ts +53 -0
- package/src/config/index.ts +9 -0
- package/src/gestureLayer/index.ts +21 -0
- package/src/gestureLayer/useGestureLayer.ts +285 -0
- package/src/gestures/focusVisibility.ts +61 -0
- package/src/gestures/index.ts +1 -0
- package/src/index.ts +72 -0
- package/src/layout/index.ts +16 -0
- package/src/layout/resolveLayout.ts +54 -0
- package/src/layout/sharedRegistry.ts +111 -0
- package/src/layout/useSharedLayout.ts +289 -0
- package/src/motion/Image.tsx +9 -0
- package/src/motion/Pressable.tsx +12 -0
- package/src/motion/ScrollView.tsx +9 -0
- package/src/motion/Text.tsx +8 -0
- package/src/motion/View.tsx +8 -0
- package/src/motion/createMotionComponent.tsx +1306 -0
- package/src/motion/index.ts +26 -0
- package/src/motion/installCheck.ts +65 -0
- package/src/presence/Presence.tsx +165 -0
- package/src/presence/PresenceContext.ts +28 -0
- package/src/presence/index.ts +6 -0
- package/src/testing/index.ts +78 -0
- package/src/touch/index.ts +18 -0
- package/src/touch/useTouchDrag.ts +289 -0
- package/src/transitions/easing.ts +53 -0
- package/src/transitions/index.ts +7 -0
- package/src/transitions/keys.ts +32 -0
- package/src/transitions/resolve.ts +169 -0
- package/src/transitions/resolveSequence.ts +87 -0
- package/src/transitions/runtime.ts +63 -0
- package/src/transitions/sig.ts +40 -0
- package/src/transitions/spring.ts +42 -0
- package/src/types.ts +383 -0
- package/src/values/index.ts +26 -0
- package/src/values/useAnimation.ts +69 -0
- package/src/values/useBooleanSpring.ts +33 -0
- package/src/values/useColorTransition.ts +72 -0
- package/src/values/useGesture.ts +144 -0
- package/src/values/useMotionValue.ts +33 -0
- package/src/values/useScroll.ts +72 -0
- package/src/values/useShadow.ts +116 -0
- package/src/values/useSpring.ts +98 -0
- package/src/values/useTransform.ts +132 -0
- package/src/values/useVariants.ts +60 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ReactNode, useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
MotionConfigContext,
|
|
4
|
+
type MotionConfigValue,
|
|
5
|
+
type ReducedMotion,
|
|
6
|
+
} from './MotionConfigContext'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Provider that controls how descendant Motion primitives respond to
|
|
10
|
+
* reduced-motion preferences. Wrap the root of your app once with the
|
|
11
|
+
* default (`reducedMotion="user"`) to respect the OS accessibility setting,
|
|
12
|
+
* or scope a subtree with `'always'` / `'never'` for specific use cases.
|
|
13
|
+
*/
|
|
14
|
+
export function MotionConfig({
|
|
15
|
+
reducedMotion = 'user',
|
|
16
|
+
children,
|
|
17
|
+
}: {
|
|
18
|
+
reducedMotion?: ReducedMotion
|
|
19
|
+
children: ReactNode
|
|
20
|
+
}) {
|
|
21
|
+
const value = useMemo<MotionConfigValue>(
|
|
22
|
+
() => ({ reducedMotion }),
|
|
23
|
+
[reducedMotion],
|
|
24
|
+
)
|
|
25
|
+
return (
|
|
26
|
+
<MotionConfigContext.Provider value={value}>
|
|
27
|
+
{children}
|
|
28
|
+
</MotionConfigContext.Provider>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
import { useReducedMotion } from 'react-native-reanimated'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How descendant Motion primitives should treat reduced-motion preferences.
|
|
6
|
+
*
|
|
7
|
+
* - `'user'` (default): defer to the OS accessibility setting via
|
|
8
|
+
* Reanimated's `useReducedMotion()`. This is the only value that respects
|
|
9
|
+
* user choice and is the right default for app-level wrappers.
|
|
10
|
+
* - `'never'`: animate regardless of OS setting. Use sparingly — e.g. for
|
|
11
|
+
* onboarding transitions you've decided are essential.
|
|
12
|
+
* - `'always'`: never animate, regardless of OS setting. Useful for tests
|
|
13
|
+
* and snapshots.
|
|
14
|
+
*/
|
|
15
|
+
export type ReducedMotion = 'user' | 'never' | 'always'
|
|
16
|
+
|
|
17
|
+
export interface MotionConfigValue {
|
|
18
|
+
reducedMotion: ReducedMotion
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default config used when a Motion primitive is rendered without a
|
|
23
|
+
* `<MotionConfig>` ancestor. `'user'` means respect the OS setting.
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_MOTION_CONFIG: MotionConfigValue = {
|
|
26
|
+
reducedMotion: 'user',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const MotionConfigContext = createContext<MotionConfigValue>(
|
|
30
|
+
DEFAULT_MOTION_CONFIG,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Read the active `<MotionConfig>` from a descendant. Returns the default
|
|
35
|
+
* (`'user'`) when no provider is present.
|
|
36
|
+
*/
|
|
37
|
+
export function useMotionConfig(): MotionConfigValue {
|
|
38
|
+
return useContext(MotionConfigContext)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the active reduced-motion mode to a boolean. `'user'` consults
|
|
43
|
+
* Reanimated's OS-backed hook; `'always'` / `'never'` shortcut. Motion
|
|
44
|
+
* primitives call this to decide whether to swap transitions for
|
|
45
|
+
* `no-animation`.
|
|
46
|
+
*/
|
|
47
|
+
export function useShouldReduceMotion(): boolean {
|
|
48
|
+
const { reducedMotion } = useMotionConfig()
|
|
49
|
+
const osReduced = useReducedMotion()
|
|
50
|
+
if (reducedMotion === 'never') return false
|
|
51
|
+
if (reducedMotion === 'always') return true
|
|
52
|
+
return osReduced
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in subpath: `@rootnative/inertia/gesture-layer`.
|
|
3
|
+
*
|
|
4
|
+
* `useGestureLayer` is a generic interactive-feedback primitive that sits one
|
|
5
|
+
* step above `useGesture()` — consumer supplies per-state target values; the
|
|
6
|
+
* hook handles "strongest active layer wins" composition (clamped-max for
|
|
7
|
+
* numerics, priority cascade for colors), the worklet, and the transition.
|
|
8
|
+
*
|
|
9
|
+
* Reach for it when MD3 state-layer haloes, iOS-translucent overlays, or
|
|
10
|
+
* any "highest engaged state controls the overlay" pattern shows up. For
|
|
11
|
+
* composition models this hook doesn't express (additive, multiply, custom
|
|
12
|
+
* per-key blends), drop to `useGesture()` and write a `useAnimatedStyle`
|
|
13
|
+
* block by hand.
|
|
14
|
+
*/
|
|
15
|
+
export { useGestureLayer } from './useGestureLayer'
|
|
16
|
+
export type {
|
|
17
|
+
GestureLayerStates,
|
|
18
|
+
GestureLayerStyle,
|
|
19
|
+
UseGestureLayerOptions,
|
|
20
|
+
UseGestureLayerResult,
|
|
21
|
+
} from './useGestureLayer'
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { useEffect, useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
interpolateColor,
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
type AnimatedStyle,
|
|
7
|
+
} from 'react-native-reanimated'
|
|
8
|
+
import { useShouldReduceMotion } from '../config'
|
|
9
|
+
import { isTopLevelTransition, resolveTransition } from '../transitions'
|
|
10
|
+
import { useGesture, type UseGestureHandlers } from '../values/useGesture'
|
|
11
|
+
import { type GestureLayerTransitions, type TransitionConfig } from '../types'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A single gesture-layer style — a flat map of style keys to a value. Numeric
|
|
15
|
+
* values participate in clamped-max composition (the "strongest active layer
|
|
16
|
+
* wins" model used by MD3 state-layer haloes); string values are treated as
|
|
17
|
+
* colors and composed via priority cascade with `interpolateColor`.
|
|
18
|
+
*
|
|
19
|
+
* The hook does not validate that string values are valid colors — passing
|
|
20
|
+
* something like `borderStyle: 'solid'` will crash inside the worklet. Keep
|
|
21
|
+
* string values to color strings.
|
|
22
|
+
*/
|
|
23
|
+
export type GestureLayerStyle = {
|
|
24
|
+
[key: string]: number | string | undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Per-state style maps. Every key is optional; missing layers default to
|
|
29
|
+
* `rest` (or `0` / `'transparent'` if `rest` is also absent for that key).
|
|
30
|
+
*
|
|
31
|
+
* - `rest` — base values, applied when no other layer is active.
|
|
32
|
+
* - `hovered` / `focused` / `focusVisible` / `pressed` — gesture-driven
|
|
33
|
+
* states tracked via the underlying `useGesture` hook. Each owns an
|
|
34
|
+
* independent 0↔1 progress that fades the layer in/out per the configured
|
|
35
|
+
* transition.
|
|
36
|
+
* - `disabled` — gated by the JS-side `options.disabled` flag rather than a
|
|
37
|
+
* gesture. Sits at the top of the priority cascade and overrides every
|
|
38
|
+
* gesture layer when active.
|
|
39
|
+
*/
|
|
40
|
+
export interface GestureLayerStates {
|
|
41
|
+
rest?: GestureLayerStyle
|
|
42
|
+
hovered?: GestureLayerStyle
|
|
43
|
+
focused?: GestureLayerStyle
|
|
44
|
+
focusVisible?: GestureLayerStyle
|
|
45
|
+
pressed?: GestureLayerStyle
|
|
46
|
+
disabled?: GestureLayerStyle
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UseGestureLayerOptions {
|
|
50
|
+
/**
|
|
51
|
+
* When `true`, the `disabled` layer becomes active (or `rest` if `disabled`
|
|
52
|
+
* is undefined). Animates via the top-level transition or the library
|
|
53
|
+
* default spring; per-layer transitions (`GestureLayerTransitions`) do not
|
|
54
|
+
* apply to `disabled`.
|
|
55
|
+
*/
|
|
56
|
+
disabled?: boolean
|
|
57
|
+
/**
|
|
58
|
+
* Transition forwarded to the underlying `useGesture` hook. Either a single
|
|
59
|
+
* `TransitionConfig` for every gesture layer, or a `GestureLayerTransitions`
|
|
60
|
+
* map for per-layer fades. Reduced motion collapses every transition to
|
|
61
|
+
* `no-animation`.
|
|
62
|
+
*/
|
|
63
|
+
transition?: TransitionConfig | GestureLayerTransitions
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface UseGestureLayerResult {
|
|
67
|
+
/**
|
|
68
|
+
* Animated style produced by `useAnimatedStyle` — spread on an
|
|
69
|
+
* `Animated.View` or pass through `<Motion.View style={...} />`.
|
|
70
|
+
*/
|
|
71
|
+
style: AnimatedStyle<Record<string, unknown>>
|
|
72
|
+
/** Handlers to spread on the receiving `Pressable`. */
|
|
73
|
+
handlers: UseGestureHandlers
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A "strongest active layer wins" interactive-feedback primitive. Sits one
|
|
78
|
+
* step above `useGesture()` — the consumer supplies the per-state target
|
|
79
|
+
* values, the hook handles the four gesture progress shared values, the
|
|
80
|
+
* disabled override, the worklet, and the transition.
|
|
81
|
+
*
|
|
82
|
+
* Composition model:
|
|
83
|
+
*
|
|
84
|
+
* - **Numeric keys** (opacity, scale, borderWidth, etc.) compose via
|
|
85
|
+
* clamped-max with `rest` as the floor:
|
|
86
|
+
* `out = max(rest, ...for each active gesture layer: lerp(rest, layer, progress))`.
|
|
87
|
+
* This matches the MD3 state-layer halo pattern — multiple states active
|
|
88
|
+
* simultaneously raise the value to the strongest, not the sum.
|
|
89
|
+
* - **Color keys** (any string value) compose via priority cascade with
|
|
90
|
+
* `interpolateColor`, lowest priority first: `hovered → focused →
|
|
91
|
+
* focusVisible → pressed`. Clamped-max doesn't apply to colors; this
|
|
92
|
+
* matches the cascade used by the declarative `gesture` prop.
|
|
93
|
+
* - **Disabled** sits at the top of the cascade for both numeric and color
|
|
94
|
+
* keys — when active, it lerps the composed value toward the `disabled`
|
|
95
|
+
* target.
|
|
96
|
+
*
|
|
97
|
+
* Reach for this when you want MD3 / iOS-translucent state-layer overlays
|
|
98
|
+
* without rewriting the worklet by hand for every consumer; reach for plain
|
|
99
|
+
* `useGesture()` when you need a composition model this hook doesn't
|
|
100
|
+
* express (additive, multiply, per-key custom blends).
|
|
101
|
+
*
|
|
102
|
+
* @example MD3 state-layer halo
|
|
103
|
+
* ```tsx
|
|
104
|
+
* import { useGestureLayer } from '@rootnative/inertia/gesture-layer'
|
|
105
|
+
* import Animated from 'react-native-reanimated'
|
|
106
|
+
* import { Pressable } from 'react-native'
|
|
107
|
+
*
|
|
108
|
+
* function SwitchHalo({ disabled }: { disabled?: boolean }) {
|
|
109
|
+
* const { style, handlers } = useGestureLayer(
|
|
110
|
+
* {
|
|
111
|
+
* rest: { opacity: 0, backgroundColor: 'transparent' },
|
|
112
|
+
* hovered: { opacity: 0.08, backgroundColor: '#000' },
|
|
113
|
+
* focused: { opacity: 0.10, backgroundColor: '#000' },
|
|
114
|
+
* pressed: { opacity: 0.12, backgroundColor: '#000' },
|
|
115
|
+
* },
|
|
116
|
+
* { disabled, transition: { type: 'timing', duration: 150 } },
|
|
117
|
+
* )
|
|
118
|
+
*
|
|
119
|
+
* return (
|
|
120
|
+
* <Pressable {...handlers}>
|
|
121
|
+
* <Animated.View style={style} />
|
|
122
|
+
* </Pressable>
|
|
123
|
+
* )
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export function useGestureLayer(
|
|
128
|
+
states: GestureLayerStates,
|
|
129
|
+
options: UseGestureLayerOptions = {},
|
|
130
|
+
): UseGestureLayerResult {
|
|
131
|
+
const { disabled: isDisabled = false, transition } = options
|
|
132
|
+
const shouldReduceMotion = useShouldReduceMotion()
|
|
133
|
+
const gesture = useGesture(transition)
|
|
134
|
+
const disabledProgress = useSharedValue(0)
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
const target = isDisabled ? 1 : 0
|
|
138
|
+
const cfg = shouldReduceMotion
|
|
139
|
+
? ({ type: 'no-animation' } as const)
|
|
140
|
+
: (disabledTransition(transition) ?? ({ type: 'spring' } as const))
|
|
141
|
+
disabledProgress.value = resolveTransition(cfg, target) as never
|
|
142
|
+
}, [isDisabled, shouldReduceMotion, transition, disabledProgress])
|
|
143
|
+
|
|
144
|
+
// JS-thread precompute: union of keys across all layers, per-key type
|
|
145
|
+
// (number vs color), and a rest-fallback table. The worklet body reads
|
|
146
|
+
// from `meta` instead of probing each layer per frame — the type check
|
|
147
|
+
// only runs when layer identities change.
|
|
148
|
+
const meta = useMemo(() => {
|
|
149
|
+
const layers = {
|
|
150
|
+
rest: states.rest,
|
|
151
|
+
hovered: states.hovered,
|
|
152
|
+
focused: states.focused,
|
|
153
|
+
focusVisible: states.focusVisible,
|
|
154
|
+
pressed: states.pressed,
|
|
155
|
+
disabled: states.disabled,
|
|
156
|
+
}
|
|
157
|
+
const sources = [
|
|
158
|
+
layers.rest,
|
|
159
|
+
layers.hovered,
|
|
160
|
+
layers.focused,
|
|
161
|
+
layers.focusVisible,
|
|
162
|
+
layers.pressed,
|
|
163
|
+
layers.disabled,
|
|
164
|
+
]
|
|
165
|
+
const keySet = new Set<string>()
|
|
166
|
+
for (const src of sources) {
|
|
167
|
+
if (!src) continue
|
|
168
|
+
for (const k in src) if (src[k] !== undefined) keySet.add(k)
|
|
169
|
+
}
|
|
170
|
+
const keys = Array.from(keySet)
|
|
171
|
+
const types: Record<string, 'number' | 'color'> = {}
|
|
172
|
+
const restValues: Record<string, number | string> = {}
|
|
173
|
+
for (const k of keys) {
|
|
174
|
+
let firstDefined: number | string | undefined
|
|
175
|
+
for (const src of sources) {
|
|
176
|
+
if (src && src[k] !== undefined) {
|
|
177
|
+
firstDefined = src[k]
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const isColor = typeof firstDefined === 'string'
|
|
182
|
+
types[k] = isColor ? 'color' : 'number'
|
|
183
|
+
const restRaw = layers.rest ? layers.rest[k] : undefined
|
|
184
|
+
restValues[k] =
|
|
185
|
+
restRaw !== undefined ? restRaw : isColor ? 'transparent' : 0
|
|
186
|
+
}
|
|
187
|
+
return { layers, keys, types, restValues }
|
|
188
|
+
}, [
|
|
189
|
+
states.rest,
|
|
190
|
+
states.hovered,
|
|
191
|
+
states.focused,
|
|
192
|
+
states.focusVisible,
|
|
193
|
+
states.pressed,
|
|
194
|
+
states.disabled,
|
|
195
|
+
])
|
|
196
|
+
|
|
197
|
+
const style = useAnimatedStyle(() => {
|
|
198
|
+
const { layers, keys, types, restValues } = meta
|
|
199
|
+
const ph = gesture.hovered.value
|
|
200
|
+
const pf = gesture.focused.value
|
|
201
|
+
const pfv = gesture.focusVisible.value
|
|
202
|
+
const pp = gesture.pressed.value
|
|
203
|
+
const pd = disabledProgress.value
|
|
204
|
+
|
|
205
|
+
const hoveredLayer = layers.hovered
|
|
206
|
+
const focusedLayer = layers.focused
|
|
207
|
+
const focusVisibleLayer = layers.focusVisible
|
|
208
|
+
const pressedLayer = layers.pressed
|
|
209
|
+
const disabledLayer = layers.disabled
|
|
210
|
+
|
|
211
|
+
const out: Record<string, unknown> = {}
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < keys.length; i++) {
|
|
214
|
+
const k = keys[i]!
|
|
215
|
+
const isColor = types[k] === 'color'
|
|
216
|
+
const rest = restValues[k]!
|
|
217
|
+
|
|
218
|
+
if (isColor) {
|
|
219
|
+
let v = rest as string
|
|
220
|
+
if (hoveredLayer && ph > 0 && hoveredLayer[k] !== undefined) {
|
|
221
|
+
v = interpolateColor(ph, [0, 1], [v, hoveredLayer[k] as string])
|
|
222
|
+
}
|
|
223
|
+
if (focusedLayer && pf > 0 && focusedLayer[k] !== undefined) {
|
|
224
|
+
v = interpolateColor(pf, [0, 1], [v, focusedLayer[k] as string])
|
|
225
|
+
}
|
|
226
|
+
if (
|
|
227
|
+
focusVisibleLayer &&
|
|
228
|
+
pfv > 0 &&
|
|
229
|
+
focusVisibleLayer[k] !== undefined
|
|
230
|
+
) {
|
|
231
|
+
v = interpolateColor(pfv, [0, 1], [v, focusVisibleLayer[k] as string])
|
|
232
|
+
}
|
|
233
|
+
if (pressedLayer && pp > 0 && pressedLayer[k] !== undefined) {
|
|
234
|
+
v = interpolateColor(pp, [0, 1], [v, pressedLayer[k] as string])
|
|
235
|
+
}
|
|
236
|
+
if (disabledLayer && pd > 0 && disabledLayer[k] !== undefined) {
|
|
237
|
+
v = interpolateColor(pd, [0, 1], [v, disabledLayer[k] as string])
|
|
238
|
+
}
|
|
239
|
+
out[k] = v
|
|
240
|
+
} else {
|
|
241
|
+
const base = rest as number
|
|
242
|
+
let m = base
|
|
243
|
+
if (hoveredLayer && ph > 0 && hoveredLayer[k] !== undefined) {
|
|
244
|
+
const c = base + ((hoveredLayer[k] as number) - base) * ph
|
|
245
|
+
if (c > m) m = c
|
|
246
|
+
}
|
|
247
|
+
if (focusedLayer && pf > 0 && focusedLayer[k] !== undefined) {
|
|
248
|
+
const c = base + ((focusedLayer[k] as number) - base) * pf
|
|
249
|
+
if (c > m) m = c
|
|
250
|
+
}
|
|
251
|
+
if (
|
|
252
|
+
focusVisibleLayer &&
|
|
253
|
+
pfv > 0 &&
|
|
254
|
+
focusVisibleLayer[k] !== undefined
|
|
255
|
+
) {
|
|
256
|
+
const c = base + ((focusVisibleLayer[k] as number) - base) * pfv
|
|
257
|
+
if (c > m) m = c
|
|
258
|
+
}
|
|
259
|
+
if (pressedLayer && pp > 0 && pressedLayer[k] !== undefined) {
|
|
260
|
+
const c = base + ((pressedLayer[k] as number) - base) * pp
|
|
261
|
+
if (c > m) m = c
|
|
262
|
+
}
|
|
263
|
+
if (disabledLayer && pd > 0 && disabledLayer[k] !== undefined) {
|
|
264
|
+
m = m + ((disabledLayer[k] as number) - m) * pd
|
|
265
|
+
}
|
|
266
|
+
out[k] = m
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return out
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
style: style as AnimatedStyle<Record<string, unknown>>,
|
|
275
|
+
handlers: gesture.handlers,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function disabledTransition(
|
|
280
|
+
transition: TransitionConfig | GestureLayerTransitions | undefined,
|
|
281
|
+
): TransitionConfig | undefined {
|
|
282
|
+
if (!transition) return undefined
|
|
283
|
+
if (isTopLevelTransition(transition)) return transition
|
|
284
|
+
return undefined
|
|
285
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Platform } from 'react-native'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Input-modality tracker for the `focusVisible` gesture sub-state.
|
|
5
|
+
*
|
|
6
|
+
* Implements the W3C `:focus-visible` heuristic: a focus event counts as
|
|
7
|
+
* "visible" only when the most recent user input was keyboard-driven. Mouse,
|
|
8
|
+
* pointer, and touch events flip the modality to `'pointer'`; keyboard events
|
|
9
|
+
* flip it back to `'keyboard'`.
|
|
10
|
+
*
|
|
11
|
+
* On native platforms there is no pointer-vs-keyboard distinction — focus
|
|
12
|
+
* arrives via D-pad, screen reader, or hardware keyboard, all of which are
|
|
13
|
+
* keyboard-equivalent — so `isFocusVisible()` is unconditionally `true`.
|
|
14
|
+
*
|
|
15
|
+
* The web listeners attach lazily on first call (capture phase, so they run
|
|
16
|
+
* before the focus event reaches the focused element) and stay installed for
|
|
17
|
+
* the lifetime of the document. They are passive and idle-cheap; the cost is
|
|
18
|
+
* one boolean read per `onFocus` dispatch.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
type InputModality = 'keyboard' | 'pointer'
|
|
22
|
+
|
|
23
|
+
// Default to `'keyboard'` so a programmatic / autofocus that happens before
|
|
24
|
+
// any user input still draws a focus ring — matches the W3C polyfill default.
|
|
25
|
+
let modality: InputModality = 'keyboard'
|
|
26
|
+
let installed = false
|
|
27
|
+
|
|
28
|
+
function setKeyboard() {
|
|
29
|
+
modality = 'keyboard'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function setPointer() {
|
|
33
|
+
modality = 'pointer'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureInstalled(): void {
|
|
37
|
+
if (installed) return
|
|
38
|
+
if (Platform.OS !== 'web') return
|
|
39
|
+
if (typeof document === 'undefined') return
|
|
40
|
+
document.addEventListener('keydown', setKeyboard, true)
|
|
41
|
+
document.addEventListener('mousedown', setPointer, true)
|
|
42
|
+
document.addEventListener('pointerdown', setPointer, true)
|
|
43
|
+
document.addEventListener('touchstart', setPointer, true)
|
|
44
|
+
installed = true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `true` if the next `onFocus` should be treated as "focus-visible" (keyboard
|
|
49
|
+
* focus). On native, always `true`. On web, reflects the most recent user
|
|
50
|
+
* input modality.
|
|
51
|
+
*/
|
|
52
|
+
export function isFocusVisible(): boolean {
|
|
53
|
+
if (Platform.OS !== 'web') return true
|
|
54
|
+
ensureInstalled()
|
|
55
|
+
return modality === 'keyboard'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @internal — test-only hook to reset module state between cases. */
|
|
59
|
+
export function __resetFocusVisibilityForTests(next: InputModality): void {
|
|
60
|
+
modality = next
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isFocusVisible } from './focusVisibility'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rootnative/inertia` — declarative animation primitives for React Native.
|
|
3
|
+
*
|
|
4
|
+
* v0.1 surface: `Motion.View` / `Motion.Text` / `Motion.Image` /
|
|
5
|
+
* `Motion.Pressable` / `Motion.ScrollView`, with `initial` / `animate` /
|
|
6
|
+
* `exit` / `transition` / `variants` / `gesture` / `controller` /
|
|
7
|
+
* `onAnimationEnd` props. Sequences, repeats (single-value and
|
|
8
|
+
* sequence-level), spring / timing / decay / no-animation transitions,
|
|
9
|
+
* and `<Presence>` for mount / unmount transitions all in scope.
|
|
10
|
+
* `<MotionConfig reducedMotion>` gates motion against the OS setting.
|
|
11
|
+
*/
|
|
12
|
+
export {
|
|
13
|
+
Motion,
|
|
14
|
+
MotionView,
|
|
15
|
+
MotionText,
|
|
16
|
+
MotionImage,
|
|
17
|
+
MotionPressable,
|
|
18
|
+
MotionScrollView,
|
|
19
|
+
createMotionComponent,
|
|
20
|
+
} from './motion'
|
|
21
|
+
export { MotionConfig, useMotionConfig, useShouldReduceMotion } from './config'
|
|
22
|
+
export type { MotionConfigValue, ReducedMotion } from './config'
|
|
23
|
+
export { Presence, usePresence } from './presence'
|
|
24
|
+
export type { PresenceContextValue } from './presence'
|
|
25
|
+
export {
|
|
26
|
+
buildReleaseAnimation,
|
|
27
|
+
resolveTransition,
|
|
28
|
+
resolveAnimatableValue,
|
|
29
|
+
ensureWorkletEasing,
|
|
30
|
+
} from './transitions'
|
|
31
|
+
export {
|
|
32
|
+
useAnimation,
|
|
33
|
+
useBooleanSpring,
|
|
34
|
+
useColorTransition,
|
|
35
|
+
useGesture,
|
|
36
|
+
useMotionValue,
|
|
37
|
+
useScroll,
|
|
38
|
+
useShadow,
|
|
39
|
+
useSpring,
|
|
40
|
+
useTransform,
|
|
41
|
+
useVariants,
|
|
42
|
+
} from './values'
|
|
43
|
+
export type {
|
|
44
|
+
ColorStyleKey,
|
|
45
|
+
ExtrapolationMode,
|
|
46
|
+
ShadowConfig,
|
|
47
|
+
UseColorTransitionOptions,
|
|
48
|
+
UseGestureHandlers,
|
|
49
|
+
UseGestureResult,
|
|
50
|
+
UseScrollResult,
|
|
51
|
+
UseShadowOptions,
|
|
52
|
+
UseTransformOptions,
|
|
53
|
+
} from './values'
|
|
54
|
+
export type {
|
|
55
|
+
AnimatableValue,
|
|
56
|
+
AnimateStyle,
|
|
57
|
+
AnimationCallbackInfo,
|
|
58
|
+
DecayTransition,
|
|
59
|
+
GestureSubStates,
|
|
60
|
+
MotionComponent,
|
|
61
|
+
MotionProps,
|
|
62
|
+
NoAnimationTransition,
|
|
63
|
+
PerPropertyTransition,
|
|
64
|
+
RepeatConfig,
|
|
65
|
+
SequenceStep,
|
|
66
|
+
SpringTransition,
|
|
67
|
+
TimingTransition,
|
|
68
|
+
Transition,
|
|
69
|
+
TransitionConfig,
|
|
70
|
+
VariantController,
|
|
71
|
+
VariantsMap,
|
|
72
|
+
} from './types'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { resolveLayoutTransition, type LayoutProp } from './resolveLayout'
|
|
2
|
+
export {
|
|
3
|
+
clearSharedRegistry,
|
|
4
|
+
consumeLayout,
|
|
5
|
+
peekSharedLayout,
|
|
6
|
+
registerLayout,
|
|
7
|
+
releaseLayout,
|
|
8
|
+
SHARED_LAYOUT_TTL_MS,
|
|
9
|
+
__setSharedLayoutClock,
|
|
10
|
+
type SharedRect,
|
|
11
|
+
} from './sharedRegistry'
|
|
12
|
+
export {
|
|
13
|
+
useSharedLayout,
|
|
14
|
+
type SharedLayoutBindings,
|
|
15
|
+
type SharedLayoutValues,
|
|
16
|
+
} from './useSharedLayout'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { LinearTransition } from 'react-native-reanimated'
|
|
2
|
+
import { ensureWorkletEasing } from '../transitions/easing'
|
|
3
|
+
import { DEFAULT_SPRING, springToReanimated } from '../transitions/spring'
|
|
4
|
+
import { type TransitionConfig } from '../types'
|
|
5
|
+
|
|
6
|
+
export type LayoutProp = boolean | TransitionConfig | undefined
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the public `layout` prop into a Reanimated `LinearTransition` builder.
|
|
10
|
+
*
|
|
11
|
+
* - `undefined` / `false` / `{ type: 'no-animation' }` → `undefined` (no layout
|
|
12
|
+
* animation; layout changes snap).
|
|
13
|
+
* - `true` → default spring with the library's tuned tension / friction / mass.
|
|
14
|
+
* - `{ type: 'spring', ... }` → spring with react-spring vocabulary, bridged
|
|
15
|
+
* into `springify().damping().stiffness().mass()` via `springToReanimated`.
|
|
16
|
+
* - `{ type: 'timing', ... }` → `.duration().easing()`. User easing fns are
|
|
17
|
+
* auto-wrapped as worklets (Reanimated 3.9+ validates this).
|
|
18
|
+
* - `{ type: 'decay', ... }` → silently downgrades to spring; decay doesn't
|
|
19
|
+
* have a clear target for a layout transition.
|
|
20
|
+
*
|
|
21
|
+
* When `reducedMotion` is true the caller should pass `undefined` so the
|
|
22
|
+
* underlying component renders without a layout animation at all. We choose
|
|
23
|
+
* this over `LinearTransition.duration(0)` because Reanimated still runs the
|
|
24
|
+
* commit-tracking machinery in that case; the snap path is genuinely cheaper.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveLayoutTransition(
|
|
27
|
+
layout: LayoutProp,
|
|
28
|
+
): LinearTransition | undefined {
|
|
29
|
+
if (!layout) return undefined
|
|
30
|
+
|
|
31
|
+
const cfg: TransitionConfig = layout === true ? { type: 'spring' } : layout
|
|
32
|
+
|
|
33
|
+
if (cfg.type === 'no-animation') return undefined
|
|
34
|
+
|
|
35
|
+
if (cfg.type === 'timing') {
|
|
36
|
+
let builder = LinearTransition.duration(cfg.duration ?? 300)
|
|
37
|
+
const easing = ensureWorkletEasing(cfg.easing)
|
|
38
|
+
if (easing) builder = builder.easing(easing)
|
|
39
|
+
if (cfg.delay) builder = builder.delay(cfg.delay)
|
|
40
|
+
return builder
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const spring = cfg.type === 'decay' ? ({ type: 'spring' } as const) : cfg
|
|
44
|
+
const { stiffness, damping, mass } = springToReanimated({
|
|
45
|
+
...DEFAULT_SPRING,
|
|
46
|
+
...spring,
|
|
47
|
+
})
|
|
48
|
+
let builder = LinearTransition.springify()
|
|
49
|
+
.stiffness(stiffness)
|
|
50
|
+
.damping(damping)
|
|
51
|
+
.mass(mass)
|
|
52
|
+
if ('delay' in spring && spring.delay) builder = builder.delay(spring.delay)
|
|
53
|
+
return builder
|
|
54
|
+
}
|