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,218 @@
|
|
|
1
|
+
import React, { useCallback, useContext, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
I18nManager,
|
|
4
|
+
View,
|
|
5
|
+
type LayoutChangeEvent,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
import Animated from "react-native-reanimated";
|
|
10
|
+
|
|
11
|
+
import { SkeletonContext } from "../providers/SkeletonProvider";
|
|
12
|
+
import { useSkeletonTheme } from "../hooks/useSkeletonTheme";
|
|
13
|
+
import { useReducedMotion } from "../hooks/useReducedMotion";
|
|
14
|
+
import { useShimmerTransform } from "../animations/shimmer";
|
|
15
|
+
import { usePulseStyle } from "../animations/pulse";
|
|
16
|
+
import { useWaveTransform } from "../animations/wave";
|
|
17
|
+
import { useBreathingStyle } from "../animations/breathing";
|
|
18
|
+
import { buildSweepGradient } from "../utils/colors";
|
|
19
|
+
import type { SkeletonAnimation, SkeletonProps } from "../types";
|
|
20
|
+
|
|
21
|
+
const LinearGradientComponent = (() => {
|
|
22
|
+
try {
|
|
23
|
+
return require("expo-linear-gradient").LinearGradient as React.ComponentType<any>;
|
|
24
|
+
} catch {
|
|
25
|
+
try {
|
|
26
|
+
return require("react-native-linear-gradient").default as React.ComponentType<any>;
|
|
27
|
+
} catch {
|
|
28
|
+
return View as unknown as React.ComponentType<any>;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
|
|
33
|
+
const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradientComponent);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The base building block of react-native-smart-skeleton. Renders a single
|
|
37
|
+
* rectangular (optionally rounded) placeholder shape with the requested
|
|
38
|
+
* animation, fully theme-aware and accessible.
|
|
39
|
+
*
|
|
40
|
+
* Most consumers will reach for `Skeleton.Rect`, `Skeleton.Circle`,
|
|
41
|
+
* `Skeleton.Square`, or `Skeleton.Pill` instead of this directly, but those
|
|
42
|
+
* are all thin wrappers around this component.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* <Skeleton width={200} height={20} animation="shimmer" />
|
|
46
|
+
*/
|
|
47
|
+
export default function Skeleton({
|
|
48
|
+
width = "100%",
|
|
49
|
+
height = 16,
|
|
50
|
+
borderRadius = 4,
|
|
51
|
+
backgroundColor,
|
|
52
|
+
highlightColor,
|
|
53
|
+
gradientColors,
|
|
54
|
+
animation,
|
|
55
|
+
duration,
|
|
56
|
+
delay = 0,
|
|
57
|
+
rtl,
|
|
58
|
+
colorVariant,
|
|
59
|
+
style,
|
|
60
|
+
accessibilityLabel = "Loading content",
|
|
61
|
+
accessible = true,
|
|
62
|
+
testID,
|
|
63
|
+
}: SkeletonProps) {
|
|
64
|
+
const ctx = useContext(SkeletonContext);
|
|
65
|
+
const themeColors = useSkeletonTheme(colorVariant);
|
|
66
|
+
const osReducedMotion = useReducedMotion();
|
|
67
|
+
|
|
68
|
+
const [measuredWidth, setMeasuredWidth] = useState(0);
|
|
69
|
+
|
|
70
|
+
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
|
71
|
+
setMeasuredWidth(e.nativeEvent.layout.width);
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
// Resolve effective settings, falling back through: prop -> provider -> library default.
|
|
75
|
+
const resolvedAnimation: SkeletonAnimation =
|
|
76
|
+
animation ?? ctx?.animation ?? "shimmer";
|
|
77
|
+
const resolvedDuration = duration ?? ctx?.duration ?? 1200;
|
|
78
|
+
const resolvedPerformanceMode = ctx?.performanceMode ?? "balanced";
|
|
79
|
+
const reducedMotion = ctx?.reducedMotion ?? osReducedMotion;
|
|
80
|
+
const resolvedRtl = rtl ?? ctx?.rtl ?? I18nManager.isRTL;
|
|
81
|
+
|
|
82
|
+
const bg = backgroundColor ?? themeColors.background;
|
|
83
|
+
const highlight = highlightColor ?? themeColors.highlight;
|
|
84
|
+
const colors = gradientColors ?? buildSweepGradient(bg, highlight);
|
|
85
|
+
|
|
86
|
+
// Reduced motion: never run shimmer/wave/pulse at full intensity. Fall back to a
|
|
87
|
+
// slow "breathing" effect unless the consumer explicitly asked for "none".
|
|
88
|
+
const effectiveAnimation: SkeletonAnimation =
|
|
89
|
+
reducedMotion && resolvedAnimation !== "none" ? "breathing" : resolvedAnimation;
|
|
90
|
+
|
|
91
|
+
const disabled = effectiveAnimation === "none";
|
|
92
|
+
|
|
93
|
+
const shimmer = useShimmerTransform(measuredWidth, {
|
|
94
|
+
duration: resolvedDuration,
|
|
95
|
+
delay,
|
|
96
|
+
disabled: disabled || effectiveAnimation !== "shimmer",
|
|
97
|
+
performanceMode: resolvedPerformanceMode,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const wave = useWaveTransform(measuredWidth, {
|
|
101
|
+
duration: resolvedDuration,
|
|
102
|
+
delay,
|
|
103
|
+
disabled: disabled || effectiveAnimation !== "wave",
|
|
104
|
+
performanceMode: resolvedPerformanceMode,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const pulseStyle = usePulseStyle({
|
|
108
|
+
duration: resolvedDuration,
|
|
109
|
+
delay,
|
|
110
|
+
disabled: disabled || effectiveAnimation !== "pulse",
|
|
111
|
+
performanceMode: resolvedPerformanceMode,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const breathingStyle = useBreathingStyle({
|
|
115
|
+
duration: reducedMotion ? resolvedDuration * 1.6 : resolvedDuration,
|
|
116
|
+
delay,
|
|
117
|
+
disabled: disabled || effectiveAnimation !== "breathing",
|
|
118
|
+
performanceMode: resolvedPerformanceMode,
|
|
119
|
+
// Reduced-motion breathing should be extra subtle.
|
|
120
|
+
minScale: reducedMotion ? 0.99 : 0.97,
|
|
121
|
+
minOpacity: reducedMotion ? 0.92 : 0.85,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const containerStyle: StyleProp<ViewStyle> = [
|
|
125
|
+
{
|
|
126
|
+
width,
|
|
127
|
+
height,
|
|
128
|
+
borderRadius,
|
|
129
|
+
overflow: "hidden" as const,
|
|
130
|
+
backgroundColor: bg,
|
|
131
|
+
} as ViewStyle,
|
|
132
|
+
style,
|
|
133
|
+
];
|
|
134
|
+
const renderOverlay = () => {
|
|
135
|
+
if (disabled) return null;
|
|
136
|
+
|
|
137
|
+
if (effectiveAnimation === "pulse") {
|
|
138
|
+
return (
|
|
139
|
+
<Animated.View
|
|
140
|
+
pointerEvents="none"
|
|
141
|
+
style={[
|
|
142
|
+
{
|
|
143
|
+
position: "absolute",
|
|
144
|
+
top: 0,
|
|
145
|
+
right: 0,
|
|
146
|
+
bottom: 0,
|
|
147
|
+
left: 0,
|
|
148
|
+
backgroundColor: highlight,
|
|
149
|
+
},
|
|
150
|
+
pulseStyle,
|
|
151
|
+
]}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (effectiveAnimation === "breathing") {
|
|
157
|
+
return (
|
|
158
|
+
<Animated.View
|
|
159
|
+
pointerEvents="none"
|
|
160
|
+
style={[
|
|
161
|
+
{
|
|
162
|
+
position: "absolute",
|
|
163
|
+
top: 0,
|
|
164
|
+
right: 0,
|
|
165
|
+
bottom: 0,
|
|
166
|
+
left: 0,
|
|
167
|
+
backgroundColor: highlight,
|
|
168
|
+
},
|
|
169
|
+
breathingStyle,
|
|
170
|
+
]}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (effectiveAnimation === "shimmer" || effectiveAnimation === "wave") {
|
|
176
|
+
const transform = effectiveAnimation === "shimmer" ? shimmer : wave;
|
|
177
|
+
const flip = resolvedRtl ? { transform: [{ scaleX: -1 }] } : null;
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<Animated.View
|
|
181
|
+
pointerEvents="none"
|
|
182
|
+
style={[
|
|
183
|
+
{ position: "absolute", top: 0, bottom: 0, left: 0 },
|
|
184
|
+
{ width: transform.sweepWidth },
|
|
185
|
+
transform.style,
|
|
186
|
+
flip,
|
|
187
|
+
]}
|
|
188
|
+
>
|
|
189
|
+
<AnimatedLinearGradient
|
|
190
|
+
colors={colors as any}
|
|
191
|
+
start={{ x: 0, y: 0 }}
|
|
192
|
+
end={{ x: 1, y: 0 }}
|
|
193
|
+
style={{ flex: 1 }}
|
|
194
|
+
/>
|
|
195
|
+
</Animated.View>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<View
|
|
204
|
+
onLayout={onLayout}
|
|
205
|
+
style={containerStyle}
|
|
206
|
+
accessible={accessible}
|
|
207
|
+
accessibilityRole={accessible ? "image" : undefined}
|
|
208
|
+
accessibilityLabel={accessible ? accessibilityLabel : undefined}
|
|
209
|
+
accessibilityState={accessible ? { busy: true } : undefined}
|
|
210
|
+
// importantForAccessibility keeps these decorative-but-informative
|
|
211
|
+
// nodes from over-announcing on Android when nested in lists.
|
|
212
|
+
importantForAccessibility={accessible ? "yes" : "no-hide-descendants"}
|
|
213
|
+
testID={testID}
|
|
214
|
+
>
|
|
215
|
+
{renderOverlay()}
|
|
216
|
+
</View>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Skeleton from "./Skeleton";
|
|
3
|
+
import type { SkeletonSquareProps } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A square skeleton placeholder with equal width and height.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <Skeleton.Square size={64} borderRadius={8} />
|
|
10
|
+
*/
|
|
11
|
+
export default function Square({ size = 50, borderRadius = 4, ...props }: SkeletonSquareProps) {
|
|
12
|
+
return <Skeleton width={size} height={size} borderRadius={borderRadius} {...props} />;
|
|
13
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { AccessibilityInfo } from "react-native";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tracks the OS-level "reduce motion" accessibility setting
|
|
6
|
+
* (Reduce Motion on iOS, Remove Animations on Android) and keeps it
|
|
7
|
+
* up to date if the user changes the setting while the app is running.
|
|
8
|
+
*
|
|
9
|
+
* Skeleton animations should fall back to a static or near-static
|
|
10
|
+
* appearance (or the "breathing" animation at a slow, low-amplitude
|
|
11
|
+
* rate) when this returns true.
|
|
12
|
+
*/
|
|
13
|
+
export function useReducedMotion(): boolean {
|
|
14
|
+
const [reducedMotion, setReducedMotion] = useState(false);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let isMounted = true;
|
|
18
|
+
|
|
19
|
+
AccessibilityInfo.isReduceMotionEnabled?.()
|
|
20
|
+
.then((enabled) => {
|
|
21
|
+
if (isMounted) setReducedMotion(!!enabled);
|
|
22
|
+
})
|
|
23
|
+
.catch(() => {
|
|
24
|
+
// Some platforms/environments (e.g. older RN, web shims) may not
|
|
25
|
+
// implement this — default to false (animations enabled).
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const subscription = AccessibilityInfo.addEventListener(
|
|
29
|
+
"reduceMotionChanged",
|
|
30
|
+
(enabled: boolean) => {
|
|
31
|
+
setReducedMotion(!!enabled);
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return () => {
|
|
36
|
+
isMounted = false;
|
|
37
|
+
// RN >= 0.65 returns an EmitterSubscription with `.remove()`.
|
|
38
|
+
// Older versions returned void and relied on removeEventListener.
|
|
39
|
+
if (subscription && typeof (subscription as any).remove === "function") {
|
|
40
|
+
(subscription as any).remove();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
return reducedMotion;
|
|
46
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useContext, useMemo } from "react";
|
|
2
|
+
import { useColorScheme } from "react-native";
|
|
3
|
+
import { SkeletonContext } from "../providers/SkeletonProvider";
|
|
4
|
+
import { getDefaultThemeColors } from "../utils/theme";
|
|
5
|
+
import { resolveThemeColors } from "../utils/colors";
|
|
6
|
+
import type { ResolvedSkeletonTheme } from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the active light/dark colors for a skeleton.
|
|
10
|
+
*
|
|
11
|
+
* If used inside a <SkeletonProvider>, honors its `theme` prop ("light" | "dark" | "system")
|
|
12
|
+
* and any registered brand `colors`. If used standalone (no provider), falls back to the
|
|
13
|
+
* device's OS color scheme and the library's default palette.
|
|
14
|
+
*
|
|
15
|
+
* @param colorVariant optional brand color variant name registered on the provider
|
|
16
|
+
*/
|
|
17
|
+
export function useSkeletonTheme(colorVariant?: string): ResolvedSkeletonTheme {
|
|
18
|
+
const ctx = useContext(SkeletonContext);
|
|
19
|
+
const systemScheme = useColorScheme();
|
|
20
|
+
|
|
21
|
+
return useMemo(() => {
|
|
22
|
+
if (ctx) {
|
|
23
|
+
const colors = ctx.getVariantColors(colorVariant);
|
|
24
|
+
return {
|
|
25
|
+
isDark: ctx.isDark,
|
|
26
|
+
background: colors.background,
|
|
27
|
+
highlight: colors.highlight,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isDark = systemScheme === "dark";
|
|
32
|
+
const fallback = getDefaultThemeColors(isDark);
|
|
33
|
+
|
|
34
|
+
const colors = resolveThemeColors({
|
|
35
|
+
isDark,
|
|
36
|
+
ambient: fallback,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
isDark,
|
|
41
|
+
background: colors.background,
|
|
42
|
+
highlight: colors.highlight,
|
|
43
|
+
};
|
|
44
|
+
}, [ctx, systemScheme, colorVariant]);
|
|
45
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import SkeletonBase from "./components/Skeleton";
|
|
2
|
+
import Rect from "./components/Rect";
|
|
3
|
+
import Circle from "./components/Circle";
|
|
4
|
+
import Square from "./components/Square";
|
|
5
|
+
import Pill from "./components/Pill";
|
|
6
|
+
import Group from "./components/Group";
|
|
7
|
+
|
|
8
|
+
import Profile from "./presets/Profile";
|
|
9
|
+
import Card from "./presets/Card";
|
|
10
|
+
import Chat from "./presets/Chat";
|
|
11
|
+
import Product from "./presets/Product";
|
|
12
|
+
import Video from "./presets/Video";
|
|
13
|
+
|
|
14
|
+
import SkeletonClone from "./clone/SkeletonClone";
|
|
15
|
+
|
|
16
|
+
export { SkeletonProvider, SkeletonContext } from "./providers/SkeletonProvider";
|
|
17
|
+
export { useSkeletonTheme } from "./hooks/useSkeletonTheme";
|
|
18
|
+
export { useReducedMotion } from "./hooks/useReducedMotion";
|
|
19
|
+
|
|
20
|
+
export type {
|
|
21
|
+
SkeletonAnimation,
|
|
22
|
+
SkeletonPerformanceMode,
|
|
23
|
+
SkeletonThemeMode,
|
|
24
|
+
SkeletonThemeColors,
|
|
25
|
+
SkeletonBrandColors,
|
|
26
|
+
ResolvedSkeletonTheme,
|
|
27
|
+
SkeletonProps,
|
|
28
|
+
SkeletonCircleProps,
|
|
29
|
+
SkeletonSquareProps,
|
|
30
|
+
SkeletonPillProps,
|
|
31
|
+
SkeletonGroupProps,
|
|
32
|
+
SkeletonContextValue,
|
|
33
|
+
SkeletonProviderProps,
|
|
34
|
+
SkeletonCloneShape,
|
|
35
|
+
SkeletonCloneNode,
|
|
36
|
+
SkeletonCloneProps,
|
|
37
|
+
SkeletonPresetProps,
|
|
38
|
+
SkeletonProfilePresetProps,
|
|
39
|
+
SkeletonCardPresetProps,
|
|
40
|
+
SkeletonChatPresetProps,
|
|
41
|
+
SkeletonProductPresetProps,
|
|
42
|
+
SkeletonVideoPresetProps,
|
|
43
|
+
ScreenSizeClass,
|
|
44
|
+
} from "./types";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `Skeleton` is the library's primary export: a callable base component
|
|
48
|
+
* (`<Skeleton />`) that also carries every shape, the layout-grouping
|
|
49
|
+
* helper, and every smart preset as static properties — so you only ever
|
|
50
|
+
* need a single import.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* import { Skeleton, SkeletonProvider } from "react-native-smart-skeleton";
|
|
54
|
+
*
|
|
55
|
+
* <SkeletonProvider>
|
|
56
|
+
* <Skeleton.Rect width={200} height={20} />
|
|
57
|
+
* <Skeleton.Circle size={48} />
|
|
58
|
+
* <Skeleton.Profile lines={2} />
|
|
59
|
+
* </SkeletonProvider>
|
|
60
|
+
*/
|
|
61
|
+
type SkeletonComponent = typeof SkeletonBase & {
|
|
62
|
+
Rect: typeof Rect;
|
|
63
|
+
Circle: typeof Circle;
|
|
64
|
+
Square: typeof Square;
|
|
65
|
+
Pill: typeof Pill;
|
|
66
|
+
Group: typeof Group;
|
|
67
|
+
Clone: typeof SkeletonClone;
|
|
68
|
+
Profile: typeof Profile;
|
|
69
|
+
Card: typeof Card;
|
|
70
|
+
Chat: typeof Chat;
|
|
71
|
+
Product: typeof Product;
|
|
72
|
+
Video: typeof Video;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const Skeleton = SkeletonBase as SkeletonComponent;
|
|
76
|
+
|
|
77
|
+
Skeleton.Rect = Rect;
|
|
78
|
+
Skeleton.Circle = Circle;
|
|
79
|
+
Skeleton.Square = Square;
|
|
80
|
+
Skeleton.Pill = Pill;
|
|
81
|
+
Skeleton.Group = Group;
|
|
82
|
+
Skeleton.Clone = SkeletonClone;
|
|
83
|
+
Skeleton.Profile = Profile;
|
|
84
|
+
Skeleton.Card = Card;
|
|
85
|
+
Skeleton.Chat = Chat;
|
|
86
|
+
Skeleton.Product = Product;
|
|
87
|
+
Skeleton.Video = Video;
|
|
88
|
+
|
|
89
|
+
export { Skeleton };
|
|
90
|
+
export { SkeletonBase };
|
|
91
|
+
export { Rect, Circle, Square, Pill, Group, SkeletonClone };
|
|
92
|
+
export { Profile, Card, Chat, Product, Video };
|
|
93
|
+
|
|
94
|
+
export default Skeleton;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import Skeleton from "../components/Skeleton";
|
|
4
|
+
import Circle from "../components/Circle";
|
|
5
|
+
import Group from "../components/Group";
|
|
6
|
+
import type { SkeletonCardPresetProps, SkeletonProps } from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A ready-made skeleton block mimicking a typical content card: an image
|
|
10
|
+
* area on top, optionally a small avatar + name row, then body text lines.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <Skeleton.Card imageHeight={160} lines={3} />
|
|
14
|
+
*/
|
|
15
|
+
export default function Card({
|
|
16
|
+
imageHeight = 160,
|
|
17
|
+
lines = 3,
|
|
18
|
+
showAvatar = false,
|
|
19
|
+
count = 1,
|
|
20
|
+
gap = 20,
|
|
21
|
+
animation,
|
|
22
|
+
style,
|
|
23
|
+
testID,
|
|
24
|
+
}: SkeletonCardPresetProps) {
|
|
25
|
+
return (
|
|
26
|
+
<View style={style} testID={testID}>
|
|
27
|
+
{Array.from({ length: count }).map((_, blockIndex) => (
|
|
28
|
+
<View key={blockIndex} style={{ marginTop: blockIndex === 0 ? 0 : gap }}>
|
|
29
|
+
<Skeleton width="100%" height={imageHeight} borderRadius={12} animation={animation} />
|
|
30
|
+
|
|
31
|
+
{showAvatar && (
|
|
32
|
+
<View style={{ flexDirection: "row", alignItems: "center", marginTop: 12 }}>
|
|
33
|
+
<Circle size={32} animation={animation} />
|
|
34
|
+
<Skeleton width="40%" height={12} style={{ marginLeft: 8 }} animation={animation} />
|
|
35
|
+
</View>
|
|
36
|
+
)}
|
|
37
|
+
|
|
38
|
+
<Group
|
|
39
|
+
staggerDelay={60}
|
|
40
|
+
animation={animation}
|
|
41
|
+
style={{ marginTop: showAvatar ? 12 : 16 }}
|
|
42
|
+
>
|
|
43
|
+
{Array.from({ length: lines }, (__, lineIndex) => {
|
|
44
|
+
const lineProps: SkeletonProps = {
|
|
45
|
+
width: lineIndex === lines - 1 ? "50%" : "100%",
|
|
46
|
+
height: 12,
|
|
47
|
+
style: { marginTop: lineIndex === 0 ? 0 : 8 },
|
|
48
|
+
};
|
|
49
|
+
return <Skeleton key={lineIndex} {...lineProps} />;
|
|
50
|
+
})}
|
|
51
|
+
</Group>
|
|
52
|
+
</View>
|
|
53
|
+
))}
|
|
54
|
+
</View>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import Skeleton from "../components/Skeleton";
|
|
4
|
+
import type { SkeletonChatPresetProps } from "../types";
|
|
5
|
+
|
|
6
|
+
const BUBBLE_WIDTHS = ["55%", "70%", "45%", "62%"];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A ready-made skeleton block mimicking a chat conversation: a series of
|
|
10
|
+
* message bubbles, alternating left/right alignment by default to read as
|
|
11
|
+
* a two-party conversation while loading.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <Skeleton.Chat messages={5} />
|
|
15
|
+
*/
|
|
16
|
+
export default function Chat({
|
|
17
|
+
messages = 4,
|
|
18
|
+
alternate = true,
|
|
19
|
+
gap = 12,
|
|
20
|
+
animation,
|
|
21
|
+
style,
|
|
22
|
+
testID,
|
|
23
|
+
}: SkeletonChatPresetProps) {
|
|
24
|
+
return (
|
|
25
|
+
<View style={style} testID={testID}>
|
|
26
|
+
{Array.from({ length: messages }).map((_, index) => {
|
|
27
|
+
const isRight = alternate && index % 2 === 1;
|
|
28
|
+
const width = BUBBLE_WIDTHS[index % BUBBLE_WIDTHS.length];
|
|
29
|
+
const height = 18 + ((index % 3) * 6);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<View
|
|
33
|
+
key={index}
|
|
34
|
+
style={{
|
|
35
|
+
flexDirection: "row",
|
|
36
|
+
justifyContent: isRight ? "flex-end" : "flex-start",
|
|
37
|
+
marginTop: index === 0 ? 0 : gap,
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<Skeleton
|
|
41
|
+
width={width}
|
|
42
|
+
height={height}
|
|
43
|
+
borderRadius={14}
|
|
44
|
+
animation={animation}
|
|
45
|
+
delay={index * 70}
|
|
46
|
+
/>
|
|
47
|
+
</View>
|
|
48
|
+
);
|
|
49
|
+
})}
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import Skeleton from "../components/Skeleton";
|
|
4
|
+
import { getRecommendedColumns } from "../utils/responsive";
|
|
5
|
+
import type { SkeletonProductPresetProps } from "../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A ready-made skeleton grid mimicking a product listing (e.g. e-commerce
|
|
9
|
+
* catalog or search results): square-ish image, title line, and price line,
|
|
10
|
+
* tiled into a responsive grid.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <Skeleton.Product count={6} columns={2} />
|
|
14
|
+
*/
|
|
15
|
+
export default function Product({
|
|
16
|
+
imageHeight = 140,
|
|
17
|
+
columns,
|
|
18
|
+
count = 4,
|
|
19
|
+
gap = 16,
|
|
20
|
+
animation,
|
|
21
|
+
style,
|
|
22
|
+
testID,
|
|
23
|
+
}: SkeletonProductPresetProps) {
|
|
24
|
+
const resolvedColumns = columns ?? getRecommendedColumns();
|
|
25
|
+
const itemWidth = `${100 / resolvedColumns}%`;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<View style={[{ flexDirection: "row", flexWrap: "wrap" }, style]} testID={testID}>
|
|
29
|
+
{Array.from({ length: count }).map((_, index) => (
|
|
30
|
+
<View
|
|
31
|
+
key={index}
|
|
32
|
+
style={{
|
|
33
|
+
width: itemWidth as any,
|
|
34
|
+
paddingHorizontal: gap / 2,
|
|
35
|
+
marginBottom: gap,
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<Skeleton width="100%" height={imageHeight} borderRadius={10} animation={animation} delay={index * 40} />
|
|
39
|
+
<Skeleton width="80%" height={12} style={{ marginTop: 8 }} animation={animation} delay={index * 40} />
|
|
40
|
+
<Skeleton width="40%" height={12} style={{ marginTop: 6 }} animation={animation} delay={index * 40} />
|
|
41
|
+
</View>
|
|
42
|
+
))}
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import Skeleton from "../components/Skeleton";
|
|
4
|
+
import Circle from "../components/Circle";
|
|
5
|
+
import Group from "../components/Group";
|
|
6
|
+
import type { SkeletonProfilePresetProps, SkeletonProps } from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A ready-made skeleton block mimicking a profile header: a circular avatar
|
|
10
|
+
* beside a name line and one or more secondary text lines (e.g. handle/bio).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <Skeleton.Profile avatarSize={56} lines={2} />
|
|
14
|
+
*/
|
|
15
|
+
export default function Profile({
|
|
16
|
+
avatarSize = 56,
|
|
17
|
+
lines = 2,
|
|
18
|
+
count = 1,
|
|
19
|
+
gap = 16,
|
|
20
|
+
animation,
|
|
21
|
+
style,
|
|
22
|
+
testID,
|
|
23
|
+
}: SkeletonProfilePresetProps) {
|
|
24
|
+
return (
|
|
25
|
+
<View style={style} testID={testID}>
|
|
26
|
+
{Array.from({ length: count }).map((_, blockIndex) => (
|
|
27
|
+
<View
|
|
28
|
+
key={blockIndex}
|
|
29
|
+
style={{
|
|
30
|
+
flexDirection: "row",
|
|
31
|
+
alignItems: "center",
|
|
32
|
+
marginTop: blockIndex === 0 ? 0 : gap,
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<Circle size={avatarSize} animation={animation} />
|
|
36
|
+
<Group
|
|
37
|
+
staggerDelay={60}
|
|
38
|
+
animation={animation}
|
|
39
|
+
style={{ marginLeft: 12, flex: 1 }}
|
|
40
|
+
>
|
|
41
|
+
{Array.from({ length: lines }, (__, lineIndex) => {
|
|
42
|
+
const lineProps: SkeletonProps = {
|
|
43
|
+
width: lineIndex === 0 ? "60%" : `${85 - lineIndex * 15}%`,
|
|
44
|
+
height: lineIndex === 0 ? 16 : 12,
|
|
45
|
+
style: { marginTop: lineIndex === 0 ? 0 : 8 },
|
|
46
|
+
};
|
|
47
|
+
return <Skeleton key={lineIndex} {...lineProps} />;
|
|
48
|
+
})}
|
|
49
|
+
</Group>
|
|
50
|
+
</View>
|
|
51
|
+
))}
|
|
52
|
+
</View>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import Skeleton from "../components/Skeleton";
|
|
4
|
+
import Circle from "../components/Circle";
|
|
5
|
+
import type { SkeletonVideoPresetProps } from "../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A ready-made skeleton block mimicking a video list item: a video
|
|
9
|
+
* thumbnail at a given aspect ratio, optionally followed by a channel
|
|
10
|
+
* avatar + title row and a meta line (views / date).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <Skeleton.Video aspectRatio={16 / 9} showTitle showMeta />
|
|
14
|
+
*/
|
|
15
|
+
export default function Video({
|
|
16
|
+
aspectRatio = 16 / 9,
|
|
17
|
+
showTitle = true,
|
|
18
|
+
showMeta = true,
|
|
19
|
+
count = 1,
|
|
20
|
+
gap = 20,
|
|
21
|
+
animation,
|
|
22
|
+
style,
|
|
23
|
+
testID,
|
|
24
|
+
}: SkeletonVideoPresetProps) {
|
|
25
|
+
return (
|
|
26
|
+
<View style={style} testID={testID}>
|
|
27
|
+
{Array.from({ length: count }).map((_, index) => (
|
|
28
|
+
<View key={index} style={{ marginTop: index === 0 ? 0 : gap }}>
|
|
29
|
+
<View style={{ width: "100%", aspectRatio }}>
|
|
30
|
+
<Skeleton width="100%" height="100%" borderRadius={10} animation={animation} />
|
|
31
|
+
</View>
|
|
32
|
+
|
|
33
|
+
{(showTitle || showMeta) && (
|
|
34
|
+
<View style={{ flexDirection: "row", marginTop: 10 }}>
|
|
35
|
+
<Circle size={36} animation={animation} />
|
|
36
|
+
<View style={{ marginLeft: 10, flex: 1 }}>
|
|
37
|
+
{showTitle && <Skeleton width="90%" height={14} animation={animation} />}
|
|
38
|
+
{showMeta && (
|
|
39
|
+
<Skeleton
|
|
40
|
+
width="50%"
|
|
41
|
+
height={11}
|
|
42
|
+
style={{ marginTop: 6 }}
|
|
43
|
+
animation={animation}
|
|
44
|
+
/>
|
|
45
|
+
)}
|
|
46
|
+
</View>
|
|
47
|
+
</View>
|
|
48
|
+
)}
|
|
49
|
+
</View>
|
|
50
|
+
))}
|
|
51
|
+
</View>
|
|
52
|
+
);
|
|
53
|
+
}
|