@rootnative/inertia-gradients 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/README.md +1 -1
- package/dist/index.js +8 -6
- package/dist/index.mjs +9 -7
- package/package.json +3 -3
- package/src/MotionLinearGradient.tsx +24 -6
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ This package ships in lockstep with `@rootnative/inertia` — version numbers tr
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.0.10] - 2026-09-06
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **The `@rootnative/inertia` peer range has an upper bound: `>=0.0.9 <0.1.0`.** Pre-`1.0.0`, a minor bump of core is where a breaking change lands, so this adapter must not accept it. Enforced by `check:versions`. Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **The `animatedProps` worklet no longer reads a ref on the UI thread.** `hasLocationsRef.current` was read inside the worklet; the worklet runtime clones a captured ref object once, so the read was a frozen snapshot that happened to be correct only because `locations` presence is locked at mount. The worklet now captures a plain boolean. No behaviour change; a test pins the presence-change throw. Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
|
|
18
|
+
|
|
19
|
+
- **A spring config without a `type` key is now honoured as the top-level transition.** `SpringTransition.type` is optional, so `transition={{ tension: 300, friction: 20 }}` is valid. The adapter's `'type' in transition` check read it as a per-property map, looked up the prop name, found nothing, and ran the library default spring with no warning. The check is now `isTopLevelTransition`, exported from `@rootnative/inertia`, which is the same structural discriminator the core factory uses. Found by the `1.0.0` readiness audit (2026-09-05).
|
|
20
|
+
|
|
21
|
+
- **An inline `transition` literal no longer restarts the animation on every parent re-render.** The value-driving effects keyed on the `transition` object's identity. The documented call shape passes `transition={{ type: 'timing', duration: 600 }}` inline, so each parent render made a new object, re-fired every effect, and re-assigned the animation from its current value with the full duration; a frequently re-rendering parent never let the animation finish. The effects now key on `stableSig(transition)`, exported from `@rootnative/inertia`, so only a change to the config's content re-resolves. Found by the `1.0.0` readiness audit (2026-09-05).
|
|
22
|
+
|
|
9
23
|
## [0.0.9] - 2026-08-22
|
|
10
24
|
|
|
11
25
|
**Lockstep version bump** alongside `@rootnative/inertia@0.0.9` (`useInterpolatedStyle` types its return against the map it was given, so a `style` array no longer needs a cast). No runtime changes in this adapter; the `@rootnative/inertia` peer range moves to `>=0.0.9`.
|
|
@@ -60,7 +74,8 @@ _No git tag was cut for this release; the published artifact is on npm as [`@roo
|
|
|
60
74
|
|
|
61
75
|
- `MotionLinearGradient` over `expo-linear-gradient`. Accepts the same `initial` / `animate` / `transition` shape as the core `Motion.*` primitives, with animatable keys for `colors`, `start`, `end`, and `locations`.
|
|
62
76
|
|
|
63
|
-
[unreleased]: https://github.com/rootnative/inertia/compare/core+gestures+gradients+svg@0.0.
|
|
77
|
+
[unreleased]: https://github.com/rootnative/inertia/compare/core+gestures+gradients+svg@0.0.10...HEAD
|
|
78
|
+
[0.0.10]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.10
|
|
64
79
|
[0.0.9]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.9
|
|
65
80
|
[0.0.8]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.8
|
|
66
81
|
[0.0.7]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.7
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Animatable linear gradient primitive for [`@rootnative/inertia`](../core), built
|
|
|
16
16
|
pnpm add @rootnative/inertia-gradients expo-linear-gradient
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
`expo-linear-gradient` works in bare React Native projects as well as Expo
|
|
19
|
+
`expo-linear-gradient` works in bare React Native projects as well as Expo, but it is an Expo Module. Bare projects need the Expo modules runtime installed. See [Installing Expo modules](https://docs.expo.dev/bare/installing-expo-modules/).
|
|
20
20
|
|
|
21
21
|
**Peer dependencies:** `@rootnative/inertia` (workspace or installed), `react >=19.0.0`, `react-native >=0.81.0`, `react-native-reanimated >=4.0.0`, `expo-linear-gradient >=14.0.0`.
|
|
22
22
|
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ var DEFAULT_START = { x: 0, y: 0 };
|
|
|
17
17
|
var DEFAULT_END = { x: 1, y: 0 };
|
|
18
18
|
function pickTransition(per, key) {
|
|
19
19
|
if (!per) return void 0;
|
|
20
|
-
if (
|
|
20
|
+
if (inertia.isTopLevelTransition(per)) return per;
|
|
21
21
|
return per[key];
|
|
22
22
|
}
|
|
23
23
|
function MotionLinearGradient(props) {
|
|
@@ -33,6 +33,7 @@ function MotionLinearGradient(props) {
|
|
|
33
33
|
} = props;
|
|
34
34
|
const slotCountRef = react.useRef(colors.length);
|
|
35
35
|
const hasLocationsRef = react.useRef(locations !== void 0);
|
|
36
|
+
const hasLocations = hasLocationsRef.current;
|
|
36
37
|
if (__DEV__) {
|
|
37
38
|
if (slotCountRef.current !== colors.length) {
|
|
38
39
|
throw new Error(
|
|
@@ -73,6 +74,7 @@ function MotionLinearGradient(props) {
|
|
|
73
74
|
const startKey = animate?.start ? `${animate.start.x},${animate.start.y}` : "";
|
|
74
75
|
const endKey = animate?.end ? `${animate.end.x},${animate.end.y}` : "";
|
|
75
76
|
const locationsKey = animate?.locations ? animate.locations.join("|") : "";
|
|
77
|
+
const transitionSig = inertia.stableSig(transition);
|
|
76
78
|
const animateColors = animate?.colors;
|
|
77
79
|
const animateStart = animate?.start;
|
|
78
80
|
const animateEnd = animate?.end;
|
|
@@ -84,19 +86,19 @@ function MotionLinearGradient(props) {
|
|
|
84
86
|
const target = animateColors[i] ?? colors[i] ?? "";
|
|
85
87
|
colorSvs[i].value = inertia.resolveTransition(cfg, target);
|
|
86
88
|
}
|
|
87
|
-
}, [colorsKey, reduce,
|
|
89
|
+
}, [colorsKey, reduce, transitionSig]);
|
|
88
90
|
react.useEffect(() => {
|
|
89
91
|
if (!animateStart) return;
|
|
90
92
|
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "start");
|
|
91
93
|
startX.value = inertia.resolveTransition(cfg, animateStart.x);
|
|
92
94
|
startY.value = inertia.resolveTransition(cfg, animateStart.y);
|
|
93
|
-
}, [startKey, reduce,
|
|
95
|
+
}, [startKey, reduce, transitionSig]);
|
|
94
96
|
react.useEffect(() => {
|
|
95
97
|
if (!animateEnd) return;
|
|
96
98
|
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "end");
|
|
97
99
|
endX.value = inertia.resolveTransition(cfg, animateEnd.x);
|
|
98
100
|
endY.value = inertia.resolveTransition(cfg, animateEnd.y);
|
|
99
|
-
}, [endKey, reduce,
|
|
101
|
+
}, [endKey, reduce, transitionSig]);
|
|
100
102
|
react.useEffect(() => {
|
|
101
103
|
if (!animateLocations) return;
|
|
102
104
|
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "locations");
|
|
@@ -106,7 +108,7 @@ function MotionLinearGradient(props) {
|
|
|
106
108
|
locationSvs[i].value = inertia.resolveTransition(cfg, target);
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
|
-
}, [locationsKey, reduce,
|
|
111
|
+
}, [locationsKey, reduce, transitionSig]);
|
|
110
112
|
const animatedProps = Animated.useAnimatedProps(() => {
|
|
111
113
|
"worklet";
|
|
112
114
|
const colorsOut = new Array(colorSvs.length);
|
|
@@ -116,7 +118,7 @@ function MotionLinearGradient(props) {
|
|
|
116
118
|
start: { x: startX.value, y: startY.value },
|
|
117
119
|
end: { x: endX.value, y: endY.value }
|
|
118
120
|
};
|
|
119
|
-
if (
|
|
121
|
+
if (hasLocations) {
|
|
120
122
|
const locsOut = new Array(locationSvs.length);
|
|
121
123
|
for (let i = 0; i < locationSvs.length; i++)
|
|
122
124
|
locsOut[i] = locationSvs[i].value;
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useRef, useEffect } from 'react';
|
|
2
2
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
3
3
|
import Animated, { useSharedValue, useAnimatedProps } from 'react-native-reanimated';
|
|
4
|
-
import { useShouldReduceMotion, resolveTransition } from '@rootnative/inertia';
|
|
4
|
+
import { useShouldReduceMotion, stableSig, resolveTransition, isTopLevelTransition } from '@rootnative/inertia';
|
|
5
5
|
import { jsx } from 'react/jsx-runtime';
|
|
6
6
|
|
|
7
7
|
// src/MotionLinearGradient.tsx
|
|
@@ -11,7 +11,7 @@ var DEFAULT_START = { x: 0, y: 0 };
|
|
|
11
11
|
var DEFAULT_END = { x: 1, y: 0 };
|
|
12
12
|
function pickTransition(per, key) {
|
|
13
13
|
if (!per) return void 0;
|
|
14
|
-
if (
|
|
14
|
+
if (isTopLevelTransition(per)) return per;
|
|
15
15
|
return per[key];
|
|
16
16
|
}
|
|
17
17
|
function MotionLinearGradient(props) {
|
|
@@ -27,6 +27,7 @@ function MotionLinearGradient(props) {
|
|
|
27
27
|
} = props;
|
|
28
28
|
const slotCountRef = useRef(colors.length);
|
|
29
29
|
const hasLocationsRef = useRef(locations !== void 0);
|
|
30
|
+
const hasLocations = hasLocationsRef.current;
|
|
30
31
|
if (__DEV__) {
|
|
31
32
|
if (slotCountRef.current !== colors.length) {
|
|
32
33
|
throw new Error(
|
|
@@ -67,6 +68,7 @@ function MotionLinearGradient(props) {
|
|
|
67
68
|
const startKey = animate?.start ? `${animate.start.x},${animate.start.y}` : "";
|
|
68
69
|
const endKey = animate?.end ? `${animate.end.x},${animate.end.y}` : "";
|
|
69
70
|
const locationsKey = animate?.locations ? animate.locations.join("|") : "";
|
|
71
|
+
const transitionSig = stableSig(transition);
|
|
70
72
|
const animateColors = animate?.colors;
|
|
71
73
|
const animateStart = animate?.start;
|
|
72
74
|
const animateEnd = animate?.end;
|
|
@@ -78,19 +80,19 @@ function MotionLinearGradient(props) {
|
|
|
78
80
|
const target = animateColors[i] ?? colors[i] ?? "";
|
|
79
81
|
colorSvs[i].value = resolveTransition(cfg, target);
|
|
80
82
|
}
|
|
81
|
-
}, [colorsKey, reduce,
|
|
83
|
+
}, [colorsKey, reduce, transitionSig]);
|
|
82
84
|
useEffect(() => {
|
|
83
85
|
if (!animateStart) return;
|
|
84
86
|
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "start");
|
|
85
87
|
startX.value = resolveTransition(cfg, animateStart.x);
|
|
86
88
|
startY.value = resolveTransition(cfg, animateStart.y);
|
|
87
|
-
}, [startKey, reduce,
|
|
89
|
+
}, [startKey, reduce, transitionSig]);
|
|
88
90
|
useEffect(() => {
|
|
89
91
|
if (!animateEnd) return;
|
|
90
92
|
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "end");
|
|
91
93
|
endX.value = resolveTransition(cfg, animateEnd.x);
|
|
92
94
|
endY.value = resolveTransition(cfg, animateEnd.y);
|
|
93
|
-
}, [endKey, reduce,
|
|
95
|
+
}, [endKey, reduce, transitionSig]);
|
|
94
96
|
useEffect(() => {
|
|
95
97
|
if (!animateLocations) return;
|
|
96
98
|
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "locations");
|
|
@@ -100,7 +102,7 @@ function MotionLinearGradient(props) {
|
|
|
100
102
|
locationSvs[i].value = resolveTransition(cfg, target);
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
|
-
}, [locationsKey, reduce,
|
|
105
|
+
}, [locationsKey, reduce, transitionSig]);
|
|
104
106
|
const animatedProps = useAnimatedProps(() => {
|
|
105
107
|
"worklet";
|
|
106
108
|
const colorsOut = new Array(colorSvs.length);
|
|
@@ -110,7 +112,7 @@ function MotionLinearGradient(props) {
|
|
|
110
112
|
start: { x: startX.value, y: startY.value },
|
|
111
113
|
end: { x: endX.value, y: endY.value }
|
|
112
114
|
};
|
|
113
|
-
if (
|
|
115
|
+
if (hasLocations) {
|
|
114
116
|
const locsOut = new Array(locationSvs.length);
|
|
115
117
|
for (let i = 0; i < locationSvs.length; i++)
|
|
116
118
|
locsOut[i] = locationSvs[i].value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rootnative/inertia-gradients",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"description": "Animated linear gradient primitive for @rootnative/inertia, built on expo-linear-gradient.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "RootNative",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"!**/*.test.*"
|
|
51
51
|
],
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@rootnative/inertia": ">=0.0.
|
|
53
|
+
"@rootnative/inertia": ">=0.0.10 <0.1.0",
|
|
54
54
|
"expo-linear-gradient": ">=14.0.0",
|
|
55
55
|
"react": ">=19.0.0",
|
|
56
56
|
"react-native": ">=0.81.0",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"size-limit": "^11.1.0",
|
|
72
72
|
"tsup": "^8.3.5",
|
|
73
73
|
"typescript": "^5.7.3",
|
|
74
|
-
"@rootnative/inertia": "0.0.
|
|
74
|
+
"@rootnative/inertia": "0.0.10"
|
|
75
75
|
},
|
|
76
76
|
"publishConfig": {
|
|
77
77
|
"access": "public"
|
|
@@ -6,7 +6,9 @@ import Animated, {
|
|
|
6
6
|
type SharedValue,
|
|
7
7
|
} from 'react-native-reanimated'
|
|
8
8
|
import {
|
|
9
|
+
isTopLevelTransition,
|
|
9
10
|
resolveTransition,
|
|
11
|
+
stableSig,
|
|
10
12
|
useShouldReduceMotion,
|
|
11
13
|
type TransitionConfig,
|
|
12
14
|
} from '@rootnative/inertia'
|
|
@@ -34,7 +36,11 @@ function pickTransition(
|
|
|
34
36
|
key: keyof LinearGradientPerPropertyTransition,
|
|
35
37
|
): TransitionConfig | undefined {
|
|
36
38
|
if (!per) return undefined
|
|
37
|
-
|
|
39
|
+
// Structural check, not `'type' in per`: `SpringTransition.type` is
|
|
40
|
+
// optional, so `{ tension: 300 }` is a valid top-level config with no
|
|
41
|
+
// `type` key. The old check treated it as a per-property map, looked up
|
|
42
|
+
// `map[key]`, got `undefined`, and ran the default spring instead.
|
|
43
|
+
if (isTopLevelTransition(per)) return per
|
|
38
44
|
return (per as LinearGradientPerPropertyTransition)[key]
|
|
39
45
|
}
|
|
40
46
|
|
|
@@ -114,6 +120,11 @@ export function MotionLinearGradient(props: MotionLinearGradientProps) {
|
|
|
114
120
|
// allocate its shared-value table on the same path.
|
|
115
121
|
const slotCountRef = useRef(colors.length)
|
|
116
122
|
const hasLocationsRef = useRef(locations !== undefined)
|
|
123
|
+
// Plain boolean for the worklet below. A ref's `.current` must not be read
|
|
124
|
+
// on the UI thread — the worklet runtime clones the ref object once, so a
|
|
125
|
+
// later write would be invisible there. Presence is locked at mount, so the
|
|
126
|
+
// value cannot change after the first render.
|
|
127
|
+
const hasLocations = hasLocationsRef.current
|
|
117
128
|
|
|
118
129
|
if (__DEV__) {
|
|
119
130
|
if (slotCountRef.current !== colors.length) {
|
|
@@ -174,6 +185,13 @@ export function MotionLinearGradient(props: MotionLinearGradientProps) {
|
|
|
174
185
|
const endKey = animate?.end ? `${animate.end.x},${animate.end.y}` : ''
|
|
175
186
|
const locationsKey = animate?.locations ? animate.locations.join('|') : ''
|
|
176
187
|
|
|
188
|
+
// Effects below key on this signature, not on the `transition` object. A
|
|
189
|
+
// parent that re-renders with an inline `transition={{ ... }}` literal makes
|
|
190
|
+
// a new object each time; keying on identity re-fired every effect and
|
|
191
|
+
// restarted the in-flight animation from its current value with the full
|
|
192
|
+
// duration. The signature only changes when the config itself changes.
|
|
193
|
+
const transitionSig = stableSig(transition)
|
|
194
|
+
|
|
177
195
|
const animateColors = animate?.colors
|
|
178
196
|
const animateStart = animate?.start
|
|
179
197
|
const animateEnd = animate?.end
|
|
@@ -188,7 +206,7 @@ export function MotionLinearGradient(props: MotionLinearGradientProps) {
|
|
|
188
206
|
}
|
|
189
207
|
// colorSvs / colors are stable across renders by the length-lock above.
|
|
190
208
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
191
|
-
}, [colorsKey, reduce,
|
|
209
|
+
}, [colorsKey, reduce, transitionSig])
|
|
192
210
|
|
|
193
211
|
useEffect(() => {
|
|
194
212
|
if (!animateStart) return
|
|
@@ -196,7 +214,7 @@ export function MotionLinearGradient(props: MotionLinearGradientProps) {
|
|
|
196
214
|
startX.value = resolveTransition(cfg, animateStart.x) as number
|
|
197
215
|
startY.value = resolveTransition(cfg, animateStart.y) as number
|
|
198
216
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
199
|
-
}, [startKey, reduce,
|
|
217
|
+
}, [startKey, reduce, transitionSig])
|
|
200
218
|
|
|
201
219
|
useEffect(() => {
|
|
202
220
|
if (!animateEnd) return
|
|
@@ -204,7 +222,7 @@ export function MotionLinearGradient(props: MotionLinearGradientProps) {
|
|
|
204
222
|
endX.value = resolveTransition(cfg, animateEnd.x) as number
|
|
205
223
|
endY.value = resolveTransition(cfg, animateEnd.y) as number
|
|
206
224
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
207
|
-
}, [endKey, reduce,
|
|
225
|
+
}, [endKey, reduce, transitionSig])
|
|
208
226
|
|
|
209
227
|
useEffect(() => {
|
|
210
228
|
if (!animateLocations) return
|
|
@@ -216,7 +234,7 @@ export function MotionLinearGradient(props: MotionLinearGradientProps) {
|
|
|
216
234
|
}
|
|
217
235
|
}
|
|
218
236
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
219
|
-
}, [locationsKey, reduce,
|
|
237
|
+
}, [locationsKey, reduce, transitionSig])
|
|
220
238
|
|
|
221
239
|
const animatedProps = useAnimatedProps(() => {
|
|
222
240
|
'worklet'
|
|
@@ -232,7 +250,7 @@ export function MotionLinearGradient(props: MotionLinearGradientProps) {
|
|
|
232
250
|
start: { x: startX.value, y: startY.value },
|
|
233
251
|
end: { x: endX.value, y: endY.value },
|
|
234
252
|
}
|
|
235
|
-
if (
|
|
253
|
+
if (hasLocations) {
|
|
236
254
|
const locsOut = new Array<number>(locationSvs.length)
|
|
237
255
|
for (let i = 0; i < locationSvs.length; i++)
|
|
238
256
|
locsOut[i] = locationSvs[i]!.value
|