@onlynative/inertia 0.0.1-alpha.7 → 0.0.1-alpha.8
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/README.md +1 -1
- package/dist/gestureLayer/index.d.mts +119 -0
- package/dist/gestureLayer/index.d.ts +119 -0
- package/dist/gestureLayer/index.js +1745 -0
- package/dist/gestureLayer/index.js.map +1 -0
- package/dist/gestureLayer/index.mjs +1743 -0
- package/dist/gestureLayer/index.mjs.map +1 -0
- package/dist/index.d.mts +114 -74
- package/dist/index.d.ts +114 -74
- package/dist/index.js +279 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +279 -44
- package/dist/index.mjs.map +1 -1
- package/dist/motion/Image.d.mts +1 -1
- package/dist/motion/Image.d.ts +1 -1
- package/dist/motion/Image.js +178 -4
- package/dist/motion/Image.js.map +1 -1
- package/dist/motion/Image.mjs +180 -6
- package/dist/motion/Image.mjs.map +1 -1
- package/dist/motion/Pressable.d.mts +1 -1
- package/dist/motion/Pressable.d.ts +1 -1
- package/dist/motion/Pressable.js +178 -4
- package/dist/motion/Pressable.js.map +1 -1
- package/dist/motion/Pressable.mjs +180 -6
- package/dist/motion/Pressable.mjs.map +1 -1
- package/dist/motion/ScrollView.d.mts +1 -1
- package/dist/motion/ScrollView.d.ts +1 -1
- package/dist/motion/ScrollView.js +178 -4
- package/dist/motion/ScrollView.js.map +1 -1
- package/dist/motion/ScrollView.mjs +180 -6
- package/dist/motion/ScrollView.mjs.map +1 -1
- package/dist/motion/Text.d.mts +1 -1
- package/dist/motion/Text.d.ts +1 -1
- package/dist/motion/Text.js +178 -4
- package/dist/motion/Text.js.map +1 -1
- package/dist/motion/Text.mjs +180 -6
- package/dist/motion/Text.mjs.map +1 -1
- package/dist/motion/View.d.mts +1 -1
- package/dist/motion/View.d.ts +1 -1
- package/dist/motion/View.js +178 -4
- package/dist/motion/View.js.map +1 -1
- package/dist/motion/View.mjs +180 -6
- package/dist/motion/View.mjs.map +1 -1
- 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-NmNeJjo1.d.mts → types-BwyvoH2V.d.mts} +24 -4
- package/dist/{types-NmNeJjo1.d.ts → types-BwyvoH2V.d.ts} +24 -4
- package/dist/useGesture-BPPp9LhV.d.ts +84 -0
- package/dist/useGesture-BnBF4OtT.d.mts +84 -0
- package/llms.txt +12 -3
- package/package.json +15 -1
- package/src/gestureLayer/index.ts +21 -0
- package/src/gestureLayer/useGestureLayer.ts +285 -0
- package/src/index.ts +7 -0
- package/src/layout/index.ts +15 -0
- package/src/layout/sharedRegistry.ts +108 -0
- package/src/layout/useSharedLayout.ts +289 -0
- package/src/motion/createMotionComponent.tsx +60 -4
- package/src/touch/index.ts +18 -0
- package/src/touch/useTouchDrag.ts +289 -0
- package/src/types.ts +23 -3
- package/src/values/index.ts +11 -0
- package/src/values/useBooleanSpring.ts +33 -0
- package/src/values/useColorTransition.ts +72 -0
- package/src/values/useShadow.ts +116 -0
|
@@ -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 '@onlynative/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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -30,18 +30,25 @@ export {
|
|
|
30
30
|
} from './transitions'
|
|
31
31
|
export {
|
|
32
32
|
useAnimation,
|
|
33
|
+
useBooleanSpring,
|
|
34
|
+
useColorTransition,
|
|
33
35
|
useGesture,
|
|
34
36
|
useMotionValue,
|
|
35
37
|
useScroll,
|
|
38
|
+
useShadow,
|
|
36
39
|
useSpring,
|
|
37
40
|
useTransform,
|
|
38
41
|
useVariants,
|
|
39
42
|
} from './values'
|
|
40
43
|
export type {
|
|
44
|
+
ColorStyleKey,
|
|
41
45
|
ExtrapolationMode,
|
|
46
|
+
ShadowConfig,
|
|
47
|
+
UseColorTransitionOptions,
|
|
42
48
|
UseGestureHandlers,
|
|
43
49
|
UseGestureResult,
|
|
44
50
|
UseScrollResult,
|
|
51
|
+
UseShadowOptions,
|
|
45
52
|
UseTransformOptions,
|
|
46
53
|
} from './values'
|
|
47
54
|
export type {
|
package/src/layout/index.ts
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
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,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level registry of last-known on-screen rects for shared-layout
|
|
3
|
+
* elements, indexed by `layoutId`. Backs `<Motion.* layoutId="..." />` —
|
|
4
|
+
* Reanimated 4 dropped the `sharedTransitionTag` API the previous design
|
|
5
|
+
* relied on, so the cross-screen shared-element transition lives in
|
|
6
|
+
* userland now.
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle, per id:
|
|
9
|
+
* 1. While a Motion primitive with `layoutId={id}` is mounted, every
|
|
10
|
+
* `onLayout` updates the rect via `registerLayout`.
|
|
11
|
+
* 2. When that primitive unmounts, the same rect is left behind under a
|
|
12
|
+
* TTL via `releaseLayout` so a subsequent mount can consume it.
|
|
13
|
+
* 3. The next mount calls `consumeLayout(id)` — if a fresh entry exists
|
|
14
|
+
* it becomes the FLIP source rect; the entry is removed so a third
|
|
15
|
+
* mount with the same id doesn't re-animate from a stale snapshot.
|
|
16
|
+
*
|
|
17
|
+
* Rects are stored in **window coordinates** (what `measureInWindow`
|
|
18
|
+
* returns). Cross-screen transitions need this — parent-relative
|
|
19
|
+
* coordinates are different on each screen and wouldn't compose.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Window-relative rect of a measured element. */
|
|
23
|
+
export interface SharedRect {
|
|
24
|
+
x: number
|
|
25
|
+
y: number
|
|
26
|
+
width: number
|
|
27
|
+
height: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Entry {
|
|
31
|
+
rect: SharedRect
|
|
32
|
+
expiresAt: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const REGISTRY = new Map<string, Entry>()
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How long (ms) a released rect remains consumable. Sized to comfortably
|
|
39
|
+
* cover a typical screen transition (slide animation, gesture-driven
|
|
40
|
+
* dismiss) without leaving stale entries lying around if no incoming
|
|
41
|
+
* mount picks them up.
|
|
42
|
+
*/
|
|
43
|
+
export const SHARED_LAYOUT_TTL_MS = 1000
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Provide the current monotonic-ish timestamp. Indirected so tests can
|
|
47
|
+
* stub it via `__setSharedLayoutClock` without touching `Date.now` globally.
|
|
48
|
+
*/
|
|
49
|
+
let now = (): number => Date.now()
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Update the latest known rect for `id`. Called on every `onLayout` of a
|
|
53
|
+
* Motion primitive with `layoutId` set so the registry always holds a
|
|
54
|
+
* current measurement if that primitive becomes the source of a future
|
|
55
|
+
* transition. Resets the TTL each call.
|
|
56
|
+
*/
|
|
57
|
+
export function registerLayout(id: string, rect: SharedRect): void {
|
|
58
|
+
REGISTRY.set(id, { rect, expiresAt: now() + SHARED_LAYOUT_TTL_MS })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Record the rect for `id` on unmount so the next mount can consume it as
|
|
63
|
+
* the FLIP source. Functionally identical to `registerLayout` — the split
|
|
64
|
+
* is purely intent-documenting at the call site.
|
|
65
|
+
*/
|
|
66
|
+
export function releaseLayout(id: string, rect: SharedRect): void {
|
|
67
|
+
REGISTRY.set(id, { rect, expiresAt: now() + SHARED_LAYOUT_TTL_MS })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Take the recorded rect for `id` if it exists and hasn't expired. The
|
|
72
|
+
* entry is removed in either case — at most one incoming mount consumes
|
|
73
|
+
* a given release, and an expired entry is dropped so it can't poison a
|
|
74
|
+
* later transition. Returns `undefined` when no fresh source is available,
|
|
75
|
+
* in which case the caller should mount without a layout animation.
|
|
76
|
+
*/
|
|
77
|
+
export function consumeLayout(id: string): SharedRect | undefined {
|
|
78
|
+
const entry = REGISTRY.get(id)
|
|
79
|
+
if (!entry) return undefined
|
|
80
|
+
REGISTRY.delete(id)
|
|
81
|
+
if (entry.expiresAt < now()) return undefined
|
|
82
|
+
return entry.rect
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Drop all entries. Tests use this to isolate between cases. */
|
|
86
|
+
export function clearSharedRegistry(): void {
|
|
87
|
+
REGISTRY.clear()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Inspect a registry entry without consuming it. Intended for tests and
|
|
92
|
+
* dev tooling; production code should go through `consumeLayout`.
|
|
93
|
+
*/
|
|
94
|
+
export function peekSharedLayout(id: string): SharedRect | undefined {
|
|
95
|
+
const entry = REGISTRY.get(id)
|
|
96
|
+
if (!entry) return undefined
|
|
97
|
+
if (entry.expiresAt < now()) return undefined
|
|
98
|
+
return entry.rect
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Test hook: swap the clock used for TTL calculations. Pass `undefined` to
|
|
103
|
+
* restore `Date.now`. Not exported from the package root — reachable only
|
|
104
|
+
* from inside the workspace.
|
|
105
|
+
*/
|
|
106
|
+
export function __setSharedLayoutClock(fn: (() => number) | undefined): void {
|
|
107
|
+
now = fn ?? Date.now
|
|
108
|
+
}
|