react-native-smart-skeleton 0.0.1
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 +255 -0
- package/lib/commonjs/animations/breathing.js +53 -0
- package/lib/commonjs/animations/breathing.js.map +1 -0
- package/lib/commonjs/animations/pulse.js +50 -0
- package/lib/commonjs/animations/pulse.js.map +1 -0
- package/lib/commonjs/animations/shimmer.js +68 -0
- package/lib/commonjs/animations/shimmer.js.map +1 -0
- package/lib/commonjs/animations/wave.js +54 -0
- package/lib/commonjs/animations/wave.js.map +1 -0
- package/lib/commonjs/clone/SkeletonClone.js +91 -0
- package/lib/commonjs/clone/SkeletonClone.js.map +1 -0
- package/lib/commonjs/components/Circle.js +27 -0
- package/lib/commonjs/components/Circle.js.map +1 -0
- package/lib/commonjs/components/Group.js +57 -0
- package/lib/commonjs/components/Group.js.map +1 -0
- package/lib/commonjs/components/Pill.js +32 -0
- package/lib/commonjs/components/Pill.js.map +1 -0
- package/lib/commonjs/components/Rect.js +21 -0
- package/lib/commonjs/components/Rect.js.map +1 -0
- package/lib/commonjs/components/Skeleton.js +196 -0
- package/lib/commonjs/components/Skeleton.js.map +1 -0
- package/lib/commonjs/components/Square.js +28 -0
- package/lib/commonjs/components/Square.js.map +1 -0
- package/lib/commonjs/demo/demo.gif +0 -0
- package/lib/commonjs/hooks/useReducedMotion.js +42 -0
- package/lib/commonjs/hooks/useReducedMotion.js.map +1 -0
- package/lib/commonjs/hooks/useSkeletonTheme.js +46 -0
- package/lib/commonjs/hooks/useSkeletonTheme.js.map +1 -0
- package/lib/commonjs/index.js +149 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/presets/Card.js +83 -0
- package/lib/commonjs/presets/Card.js.map +1 -0
- package/lib/commonjs/presets/Chat.js +54 -0
- package/lib/commonjs/presets/Chat.js.map +1 -0
- package/lib/commonjs/presets/Product.js +70 -0
- package/lib/commonjs/presets/Product.js.map +1 -0
- package/lib/commonjs/presets/Profile.js +67 -0
- package/lib/commonjs/presets/Profile.js.map +1 -0
- package/lib/commonjs/presets/Video.js +76 -0
- package/lib/commonjs/presets/Video.js.map +1 -0
- package/lib/commonjs/providers/SkeletonProvider.js +70 -0
- package/lib/commonjs/providers/SkeletonProvider.js.map +1 -0
- package/lib/commonjs/types/index.js +6 -0
- package/lib/commonjs/types/index.js.map +1 -0
- package/lib/commonjs/utils/colors.js +144 -0
- package/lib/commonjs/utils/colors.js.map +1 -0
- package/lib/commonjs/utils/responsive.js +84 -0
- package/lib/commonjs/utils/responsive.js.map +1 -0
- package/lib/commonjs/utils/theme.js +39 -0
- package/lib/commonjs/utils/theme.js.map +1 -0
- package/lib/module/animations/breathing.js +47 -0
- package/lib/module/animations/breathing.js.map +1 -0
- package/lib/module/animations/pulse.js +44 -0
- package/lib/module/animations/pulse.js.map +1 -0
- package/lib/module/animations/shimmer.js +61 -0
- package/lib/module/animations/shimmer.js.map +1 -0
- package/lib/module/animations/wave.js +48 -0
- package/lib/module/animations/wave.js.map +1 -0
- package/lib/module/clone/SkeletonClone.js +84 -0
- package/lib/module/clone/SkeletonClone.js.map +1 -0
- package/lib/module/components/Circle.js +20 -0
- package/lib/module/components/Circle.js.map +1 -0
- package/lib/module/components/Group.js +50 -0
- package/lib/module/components/Group.js.map +1 -0
- package/lib/module/components/Pill.js +25 -0
- package/lib/module/components/Pill.js.map +1 -0
- package/lib/module/components/Rect.js +14 -0
- package/lib/module/components/Rect.js.map +1 -0
- package/lib/module/components/Skeleton.js +188 -0
- package/lib/module/components/Skeleton.js.map +1 -0
- package/lib/module/components/Square.js +21 -0
- package/lib/module/components/Square.js.map +1 -0
- package/lib/module/demo/demo.gif +0 -0
- package/lib/module/hooks/useReducedMotion.js +37 -0
- package/lib/module/hooks/useReducedMotion.js.map +1 -0
- package/lib/module/hooks/useSkeletonTheme.js +40 -0
- package/lib/module/hooks/useSkeletonTheme.js.map +1 -0
- package/lib/module/index.js +50 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/presets/Card.js +76 -0
- package/lib/module/presets/Card.js.map +1 -0
- package/lib/module/presets/Chat.js +47 -0
- package/lib/module/presets/Chat.js.map +1 -0
- package/lib/module/presets/Product.js +63 -0
- package/lib/module/presets/Product.js.map +1 -0
- package/lib/module/presets/Profile.js +60 -0
- package/lib/module/presets/Profile.js.map +1 -0
- package/lib/module/presets/Video.js +69 -0
- package/lib/module/presets/Video.js.map +1 -0
- package/lib/module/providers/SkeletonProvider.js +61 -0
- package/lib/module/providers/SkeletonProvider.js.map +1 -0
- package/lib/module/types/index.js +2 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/utils/colors.js +132 -0
- package/lib/module/utils/colors.js.map +1 -0
- package/lib/module/utils/responsive.js +74 -0
- package/lib/module/utils/responsive.js.map +1 -0
- package/lib/module/utils/theme.js +32 -0
- package/lib/module/utils/theme.js.map +1 -0
- package/lib/typescript/animations/breathing.d.ts +26 -0
- package/lib/typescript/animations/breathing.d.ts.map +1 -0
- package/lib/typescript/animations/pulse.d.ts +22 -0
- package/lib/typescript/animations/pulse.d.ts.map +1 -0
- package/lib/typescript/animations/shimmer.d.ts +34 -0
- package/lib/typescript/animations/shimmer.d.ts.map +1 -0
- package/lib/typescript/animations/wave.d.ts +23 -0
- package/lib/typescript/animations/wave.d.ts.map +1 -0
- package/lib/typescript/clone/SkeletonClone.d.ts +30 -0
- package/lib/typescript/clone/SkeletonClone.d.ts.map +1 -0
- package/lib/typescript/components/Circle.d.ts +10 -0
- package/lib/typescript/components/Circle.d.ts.map +1 -0
- package/lib/typescript/components/Group.d.ts +21 -0
- package/lib/typescript/components/Group.d.ts.map +1 -0
- package/lib/typescript/components/Pill.d.ts +12 -0
- package/lib/typescript/components/Pill.d.ts.map +1 -0
- package/lib/typescript/components/Rect.d.ts +12 -0
- package/lib/typescript/components/Rect.d.ts.map +1 -0
- package/lib/typescript/components/Skeleton.d.ts +16 -0
- package/lib/typescript/components/Skeleton.d.ts.map +1 -0
- package/lib/typescript/components/Square.d.ts +10 -0
- package/lib/typescript/components/Square.d.ts.map +1 -0
- package/lib/typescript/hooks/useReducedMotion.d.ts +11 -0
- package/lib/typescript/hooks/useReducedMotion.d.ts.map +1 -0
- package/lib/typescript/hooks/useSkeletonTheme.d.ts +12 -0
- package/lib/typescript/hooks/useSkeletonTheme.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +51 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/presets/Card.d.ts +11 -0
- package/lib/typescript/presets/Card.d.ts.map +1 -0
- package/lib/typescript/presets/Chat.d.ts +12 -0
- package/lib/typescript/presets/Chat.d.ts.map +1 -0
- package/lib/typescript/presets/Product.d.ts +12 -0
- package/lib/typescript/presets/Product.d.ts.map +1 -0
- package/lib/typescript/presets/Profile.d.ts +11 -0
- package/lib/typescript/presets/Profile.d.ts.map +1 -0
- package/lib/typescript/presets/Video.d.ts +12 -0
- package/lib/typescript/presets/Video.d.ts.map +1 -0
- package/lib/typescript/providers/SkeletonProvider.d.ts +18 -0
- package/lib/typescript/providers/SkeletonProvider.d.ts.map +1 -0
- package/lib/typescript/types/index.d.ts +149 -0
- package/lib/typescript/types/index.d.ts.map +1 -0
- package/lib/typescript/utils/colors.d.ts +51 -0
- package/lib/typescript/utils/colors.d.ts.map +1 -0
- package/lib/typescript/utils/responsive.d.ts +27 -0
- package/lib/typescript/utils/responsive.d.ts.map +1 -0
- package/lib/typescript/utils/theme.d.ts +16 -0
- package/lib/typescript/utils/theme.d.ts.map +1 -0
- package/package.json +74 -0
- package/src/animations/breathing.tsx +81 -0
- package/src/animations/pulse.tsx +75 -0
- package/src/animations/shimmer.tsx +88 -0
- package/src/animations/wave.tsx +74 -0
- package/src/clone/SkeletonClone.tsx +82 -0
- package/src/components/Circle.tsx +13 -0
- package/src/components/Group.tsx +55 -0
- package/src/components/Pill.tsx +23 -0
- package/src/components/Rect.tsx +15 -0
- package/src/components/Skeleton.tsx +218 -0
- package/src/components/Square.tsx +13 -0
- package/src/demo/demo.gif +0 -0
- package/src/hooks/useReducedMotion.ts +46 -0
- package/src/hooks/useSkeletonTheme.ts +45 -0
- package/src/index.ts +94 -0
- package/src/presets/Card.tsx +56 -0
- package/src/presets/Chat.tsx +52 -0
- package/src/presets/Product.tsx +45 -0
- package/src/presets/Profile.tsx +54 -0
- package/src/presets/Video.tsx +53 -0
- package/src/providers/SkeletonProvider.tsx +67 -0
- package/src/types/index.ts +246 -0
- package/src/utils/colors.ts +132 -0
- package/src/utils/responsive.ts +73 -0
- package/src/utils/theme.ts +35 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Easing,
|
|
4
|
+
cancelAnimation,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withDelay,
|
|
8
|
+
withRepeat,
|
|
9
|
+
withTiming,
|
|
10
|
+
} from "react-native-reanimated";
|
|
11
|
+
import type { SkeletonPerformanceMode } from "../types";
|
|
12
|
+
|
|
13
|
+
export interface BreathingOptions {
|
|
14
|
+
duration?: number;
|
|
15
|
+
delay?: number;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
performanceMode?: SkeletonPerformanceMode;
|
|
18
|
+
/** Smallest scale factor during the "exhale" phase. Defaults to 0.97. */
|
|
19
|
+
minScale?: number;
|
|
20
|
+
/** Largest scale factor during the "inhale" phase. Defaults to 1. */
|
|
21
|
+
maxScale?: number;
|
|
22
|
+
/** Paired opacity dip alongside the scale-down. Defaults to 0.85. */
|
|
23
|
+
minOpacity?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A slow, very gentle scale + opacity oscillation — softer and slower than
|
|
28
|
+
* "pulse" — intended as the most reduced-motion-friendly animation. This is
|
|
29
|
+
* the animation `Skeleton` falls back to (at a slowed rate) when the OS
|
|
30
|
+
* reduce-motion setting is on and the consumer hasn't explicitly chosen "none".
|
|
31
|
+
*/
|
|
32
|
+
export function useBreathingStyle({
|
|
33
|
+
duration = 2200,
|
|
34
|
+
delay = 0,
|
|
35
|
+
disabled = false,
|
|
36
|
+
performanceMode = "balanced",
|
|
37
|
+
minScale = 0.97,
|
|
38
|
+
maxScale = 1,
|
|
39
|
+
minOpacity = 0.85,
|
|
40
|
+
}: BreathingOptions) {
|
|
41
|
+
const progress = useSharedValue(0);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (disabled) {
|
|
45
|
+
progress.value = 0;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const effectiveDuration =
|
|
50
|
+
performanceMode === "battery-saver" ? duration * 1.2 : duration;
|
|
51
|
+
|
|
52
|
+
progress.value = 0;
|
|
53
|
+
progress.value = withDelay(
|
|
54
|
+
delay,
|
|
55
|
+
withRepeat(
|
|
56
|
+
withTiming(1, {
|
|
57
|
+
duration: effectiveDuration / 2,
|
|
58
|
+
easing: Easing.inOut(Easing.ease),
|
|
59
|
+
}),
|
|
60
|
+
-1,
|
|
61
|
+
true
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
cancelAnimation(progress);
|
|
67
|
+
};
|
|
68
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
69
|
+
}, [duration, delay, disabled, performanceMode]);
|
|
70
|
+
|
|
71
|
+
const style = useAnimatedStyle(() => {
|
|
72
|
+
const scale = minScale + progress.value * (maxScale - minScale);
|
|
73
|
+
const opacity = minOpacity + progress.value * (1 - minOpacity);
|
|
74
|
+
return {
|
|
75
|
+
opacity,
|
|
76
|
+
transform: [{ scale }],
|
|
77
|
+
};
|
|
78
|
+
}, [minScale, maxScale, minOpacity]);
|
|
79
|
+
|
|
80
|
+
return style;
|
|
81
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Easing,
|
|
4
|
+
cancelAnimation,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withDelay,
|
|
8
|
+
withRepeat,
|
|
9
|
+
withTiming,
|
|
10
|
+
} from "react-native-reanimated";
|
|
11
|
+
import type { SkeletonPerformanceMode } from "../types";
|
|
12
|
+
|
|
13
|
+
export interface PulseOptions {
|
|
14
|
+
duration?: number;
|
|
15
|
+
delay?: number;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
performanceMode?: SkeletonPerformanceMode;
|
|
18
|
+
/** Minimum opacity of the "dim" phase. Defaults to 0.4. */
|
|
19
|
+
minOpacity?: number;
|
|
20
|
+
/** Maximum opacity of the "bright" phase. Defaults to 1. */
|
|
21
|
+
maxOpacity?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Classic fade in/out pulse: opacity oscillates between minOpacity and
|
|
26
|
+
* maxOpacity using an ease-in-out curve, looping indefinitely.
|
|
27
|
+
*
|
|
28
|
+
* Returns an animated style with just `opacity`, ready to spread onto the
|
|
29
|
+
* skeleton's base View.
|
|
30
|
+
*/
|
|
31
|
+
export function usePulseStyle({
|
|
32
|
+
duration = 1000,
|
|
33
|
+
delay = 0,
|
|
34
|
+
disabled = false,
|
|
35
|
+
performanceMode = "balanced",
|
|
36
|
+
minOpacity = 0.4,
|
|
37
|
+
maxOpacity = 1,
|
|
38
|
+
}: PulseOptions) {
|
|
39
|
+
const progress = useSharedValue(0);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (disabled) {
|
|
43
|
+
progress.value = 0;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const effectiveDuration =
|
|
48
|
+
performanceMode === "battery-saver" ? duration * 1.3 : duration;
|
|
49
|
+
|
|
50
|
+
progress.value = 0;
|
|
51
|
+
progress.value = withDelay(
|
|
52
|
+
delay,
|
|
53
|
+
withRepeat(
|
|
54
|
+
withTiming(1, {
|
|
55
|
+
duration: effectiveDuration / 2,
|
|
56
|
+
easing: Easing.inOut(Easing.ease),
|
|
57
|
+
}),
|
|
58
|
+
-1,
|
|
59
|
+
true // reverse, giving the fade-in/fade-out loop
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
cancelAnimation(progress);
|
|
65
|
+
};
|
|
66
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
67
|
+
}, [duration, delay, disabled, performanceMode]);
|
|
68
|
+
|
|
69
|
+
const style = useAnimatedStyle(() => {
|
|
70
|
+
const opacity = minOpacity + progress.value * (maxOpacity - minOpacity);
|
|
71
|
+
return { opacity };
|
|
72
|
+
}, [minOpacity, maxOpacity]);
|
|
73
|
+
|
|
74
|
+
return style;
|
|
75
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Easing,
|
|
4
|
+
cancelAnimation,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withDelay,
|
|
8
|
+
withRepeat,
|
|
9
|
+
withTiming,
|
|
10
|
+
} from "react-native-reanimated";
|
|
11
|
+
import type { SkeletonPerformanceMode } from "../types";
|
|
12
|
+
|
|
13
|
+
export interface ShimmerOptions {
|
|
14
|
+
duration?: number;
|
|
15
|
+
delay?: number;
|
|
16
|
+
/** When true, freezes the animation at progress 0 (used for reduced motion / "none"). */
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
performanceMode?: SkeletonPerformanceMode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Drives a 0 -> 1 -> 0 progress value looping indefinitely, intended to be
|
|
23
|
+
* mapped (by the consuming component) onto a translateX of a gradient
|
|
24
|
+
* overlay to create the classic "shimmer sweep" effect.
|
|
25
|
+
*
|
|
26
|
+
* Returns both the raw shared value-derived animated style (opacity passthrough,
|
|
27
|
+
* so it can be spread directly) and the progress itself isn't exposed —
|
|
28
|
+
* components should use `useShimmerTransform` below to get a transform style
|
|
29
|
+
* sized to their own width.
|
|
30
|
+
*/
|
|
31
|
+
export function useShimmerProgress({
|
|
32
|
+
duration = 1200,
|
|
33
|
+
delay = 0,
|
|
34
|
+
disabled = false,
|
|
35
|
+
performanceMode = "balanced",
|
|
36
|
+
}: ShimmerOptions) {
|
|
37
|
+
const progress = useSharedValue(0);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (disabled) {
|
|
41
|
+
progress.value = 0;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const effectiveDuration =
|
|
46
|
+
performanceMode === "battery-saver" ? duration * 1.4 : duration;
|
|
47
|
+
|
|
48
|
+
progress.value = 0;
|
|
49
|
+
progress.value = withDelay(
|
|
50
|
+
delay,
|
|
51
|
+
withRepeat(
|
|
52
|
+
withTiming(1, {
|
|
53
|
+
duration: effectiveDuration,
|
|
54
|
+
easing: Easing.linear,
|
|
55
|
+
}),
|
|
56
|
+
-1,
|
|
57
|
+
false
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
cancelAnimation(progress);
|
|
63
|
+
};
|
|
64
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
65
|
+
}, [duration, delay, disabled, performanceMode]);
|
|
66
|
+
|
|
67
|
+
return progress;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Convenience hook: given a measured width, returns an animated style that
|
|
72
|
+
* translates a highlight overlay (assumed ~60% of `width` wide) from off-screen
|
|
73
|
+
* left to off-screen right, looping. Use on an absolutely-positioned overlay
|
|
74
|
+
* View/LinearGradient layered on top of the base skeleton rect.
|
|
75
|
+
*/
|
|
76
|
+
export function useShimmerTransform(width: number, options: ShimmerOptions) {
|
|
77
|
+
const progress = useShimmerProgress(options);
|
|
78
|
+
const sweepWidth = Math.max(width, 1) * 0.6;
|
|
79
|
+
|
|
80
|
+
const style = useAnimatedStyle(() => {
|
|
81
|
+
const translateX = -sweepWidth + progress.value * (width + sweepWidth);
|
|
82
|
+
return {
|
|
83
|
+
transform: [{ translateX }],
|
|
84
|
+
};
|
|
85
|
+
}, [width, sweepWidth]);
|
|
86
|
+
|
|
87
|
+
return { style, sweepWidth };
|
|
88
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Easing,
|
|
4
|
+
cancelAnimation,
|
|
5
|
+
interpolate,
|
|
6
|
+
useAnimatedStyle,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
withDelay,
|
|
9
|
+
withRepeat,
|
|
10
|
+
withTiming,
|
|
11
|
+
} from "react-native-reanimated";
|
|
12
|
+
import type { SkeletonPerformanceMode } from "../types";
|
|
13
|
+
|
|
14
|
+
export interface WaveOptions {
|
|
15
|
+
duration?: number;
|
|
16
|
+
delay?: number;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
performanceMode?: SkeletonPerformanceMode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Similar to shimmer, but drives a smoother sine-like sweep (ease-in-out
|
|
23
|
+
* instead of linear) intended for a softer "ripple" look, typically paired
|
|
24
|
+
* with a wider, lower-contrast gradient than shimmer uses.
|
|
25
|
+
*
|
|
26
|
+
* Returns a translateX-based animated style for a given measured width.
|
|
27
|
+
*/
|
|
28
|
+
export function useWaveTransform(width: number, options: WaveOptions) {
|
|
29
|
+
const { duration = 1500, delay = 0, disabled = false, performanceMode = "balanced" } = options;
|
|
30
|
+
const progress = useSharedValue(0);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (disabled) {
|
|
34
|
+
progress.value = 0;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const effectiveDuration =
|
|
39
|
+
performanceMode === "battery-saver" ? duration * 1.4 : duration;
|
|
40
|
+
|
|
41
|
+
progress.value = 0;
|
|
42
|
+
progress.value = withDelay(
|
|
43
|
+
delay,
|
|
44
|
+
withRepeat(
|
|
45
|
+
withTiming(1, {
|
|
46
|
+
duration: effectiveDuration,
|
|
47
|
+
easing: Easing.inOut(Easing.sin),
|
|
48
|
+
}),
|
|
49
|
+
-1,
|
|
50
|
+
false
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
cancelAnimation(progress);
|
|
56
|
+
};
|
|
57
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
58
|
+
}, [duration, delay, disabled, performanceMode]);
|
|
59
|
+
|
|
60
|
+
const sweepWidth = Math.max(width, 1) * 0.8;
|
|
61
|
+
|
|
62
|
+
const style = useAnimatedStyle(() => {
|
|
63
|
+
const translateX = interpolate(
|
|
64
|
+
progress.value,
|
|
65
|
+
[0, 1],
|
|
66
|
+
[-sweepWidth, width + sweepWidth]
|
|
67
|
+
);
|
|
68
|
+
return {
|
|
69
|
+
transform: [{ translateX }],
|
|
70
|
+
};
|
|
71
|
+
}, [width, sweepWidth]);
|
|
72
|
+
|
|
73
|
+
return { style, sweepWidth };
|
|
74
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import Skeleton from "../components/Skeleton";
|
|
4
|
+
import type { SkeletonCloneNode, SkeletonCloneProps } from "../types";
|
|
5
|
+
|
|
6
|
+
function renderNode(node: SkeletonCloneNode, animation: SkeletonCloneProps["animation"], keyPrefix: string) {
|
|
7
|
+
const { shape = "rect", width, height, borderRadius, style, children, direction = "column", gap = 8 } = node;
|
|
8
|
+
|
|
9
|
+
let resolvedBorderRadius = borderRadius;
|
|
10
|
+
if (shape === "circle") {
|
|
11
|
+
const numericSize = typeof width === "number" ? width : typeof height === "number" ? height : 50;
|
|
12
|
+
resolvedBorderRadius = numericSize / 2;
|
|
13
|
+
} else if (shape === "pill") {
|
|
14
|
+
const numericHeight = typeof height === "number" ? height : 24;
|
|
15
|
+
resolvedBorderRadius = resolvedBorderRadius ?? numericHeight / 2;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (children && children.length > 0) {
|
|
19
|
+
return (
|
|
20
|
+
<View
|
|
21
|
+
key={keyPrefix}
|
|
22
|
+
style={[
|
|
23
|
+
{ flexDirection: direction === "row" ? "row" : "column" },
|
|
24
|
+
style,
|
|
25
|
+
]}
|
|
26
|
+
>
|
|
27
|
+
{children.map((child, i) => (
|
|
28
|
+
<View
|
|
29
|
+
key={`${keyPrefix}-${i}`}
|
|
30
|
+
style={
|
|
31
|
+
direction === "row"
|
|
32
|
+
? { marginLeft: i === 0 ? 0 : gap }
|
|
33
|
+
: { marginTop: i === 0 ? 0 : gap }
|
|
34
|
+
}
|
|
35
|
+
>
|
|
36
|
+
{renderNode(child, animation, `${keyPrefix}-${i}`)}
|
|
37
|
+
</View>
|
|
38
|
+
))}
|
|
39
|
+
</View>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Skeleton
|
|
45
|
+
width={width ?? "100%"}
|
|
46
|
+
height={height ?? 16}
|
|
47
|
+
borderRadius={resolvedBorderRadius ?? 4}
|
|
48
|
+
animation={animation}
|
|
49
|
+
style={style}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Clones an arbitrary layout tree as skeleton placeholders. Useful when you
|
|
56
|
+
* have a complex, bespoke screen layout and don't want to hand-place a
|
|
57
|
+
* Skeleton for every element — instead describe the rough shape once as a
|
|
58
|
+
* `SkeletonCloneNode` tree (mirroring your real layout's structure) and let
|
|
59
|
+
* SkeletonClone render matching placeholders.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* <SkeletonClone
|
|
63
|
+
* layout={{
|
|
64
|
+
* direction: "row",
|
|
65
|
+
* gap: 12,
|
|
66
|
+
* children: [
|
|
67
|
+
* { shape: "circle", width: 48, height: 48 },
|
|
68
|
+
* {
|
|
69
|
+
* direction: "column",
|
|
70
|
+
* gap: 6,
|
|
71
|
+
* children: [
|
|
72
|
+
* { width: 120, height: 14 },
|
|
73
|
+
* { width: 80, height: 12 },
|
|
74
|
+
* ],
|
|
75
|
+
* },
|
|
76
|
+
* ],
|
|
77
|
+
* }}
|
|
78
|
+
* />
|
|
79
|
+
*/
|
|
80
|
+
export default function SkeletonClone({ layout, animation, style, testID }: SkeletonCloneProps) {
|
|
81
|
+
return <View style={style} testID={testID}>{renderNode(layout, animation, "clone-root")}</View>;
|
|
82
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Skeleton from "./Skeleton";
|
|
3
|
+
import type { SkeletonCircleProps } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A circular skeleton placeholder, commonly used for avatars.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <Skeleton.Circle size={48} />
|
|
10
|
+
*/
|
|
11
|
+
export default function Circle({ size = 50, ...props }: SkeletonCircleProps) {
|
|
12
|
+
return <Skeleton width={size} height={size} borderRadius={size / 2} {...props} />;
|
|
13
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { Children, isValidElement, cloneElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import type { SkeletonGroupProps } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Groups multiple skeleton children together and automatically staggers
|
|
7
|
+
* their animation start so they don't all pulse/shimmer perfectly in sync —
|
|
8
|
+
* a subtle touch that reads as more natural for lists of skeleton rows.
|
|
9
|
+
*
|
|
10
|
+
* Any direct child that accepts a `delay` prop (i.e. any Skeleton.* shape,
|
|
11
|
+
* or a custom component that forwards `delay` to one) will have an
|
|
12
|
+
* incremental delay injected automatically. Children that already specify
|
|
13
|
+
* their own `delay` are left untouched.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <Skeleton.Group staggerDelay={80}>
|
|
17
|
+
* <Skeleton.Rect width="100%" height={16} />
|
|
18
|
+
* <Skeleton.Rect width="80%" height={16} />
|
|
19
|
+
* <Skeleton.Rect width="60%" height={16} />
|
|
20
|
+
* </Skeleton.Group>
|
|
21
|
+
*/
|
|
22
|
+
export default function Group({
|
|
23
|
+
children,
|
|
24
|
+
staggerDelay = 80,
|
|
25
|
+
animation,
|
|
26
|
+
direction = "column",
|
|
27
|
+
style,
|
|
28
|
+
testID,
|
|
29
|
+
}: SkeletonGroupProps) {
|
|
30
|
+
const items = Children.toArray(children);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<View
|
|
34
|
+
style={[{ flexDirection: direction === "row" ? "row" : "column" }, style]}
|
|
35
|
+
testID={testID}
|
|
36
|
+
>
|
|
37
|
+
{items.map((child, index) => {
|
|
38
|
+
if (!isValidElement(child)) return child;
|
|
39
|
+
|
|
40
|
+
const existingProps = child.props as Record<string, unknown>;
|
|
41
|
+
const injectedProps: Record<string, unknown> = {};
|
|
42
|
+
|
|
43
|
+
if (existingProps.delay === undefined) {
|
|
44
|
+
injectedProps.delay = index * staggerDelay;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (animation && existingProps.animation === undefined) {
|
|
48
|
+
injectedProps.animation = animation;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return cloneElement(child, { key: child.key ?? index, ...injectedProps });
|
|
52
|
+
})}
|
|
53
|
+
</View>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Skeleton from "./Skeleton";
|
|
3
|
+
import type { SkeletonPillProps } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A fully-rounded "pill" skeleton placeholder, commonly used for buttons,
|
|
7
|
+
* tags/chips, or badges. Border radius is automatically derived from height
|
|
8
|
+
* unless overridden.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <Skeleton.Pill width={80} height={28} />
|
|
12
|
+
*/
|
|
13
|
+
export default function Pill({ width = 80, height = 24, borderRadius, ...props }: SkeletonPillProps) {
|
|
14
|
+
const numericHeight = typeof height === "number" ? height : 24;
|
|
15
|
+
return (
|
|
16
|
+
<Skeleton
|
|
17
|
+
width={width}
|
|
18
|
+
height={height}
|
|
19
|
+
borderRadius={borderRadius ?? numericHeight / 2}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Skeleton from "./Skeleton";
|
|
3
|
+
import type { SkeletonProps } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A rectangular skeleton placeholder. Equivalent to `<Skeleton />` directly,
|
|
7
|
+
* provided for a consistent, explicit naming convention alongside
|
|
8
|
+
* `Skeleton.Circle`, `Skeleton.Square`, and `Skeleton.Pill`.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <Skeleton.Rect width={200} height={20} />
|
|
12
|
+
*/
|
|
13
|
+
export default function Rect(props: SkeletonProps) {
|
|
14
|
+
return <Skeleton {...props} />;
|
|
15
|
+
}
|