Skip to content

Commit 8783f58

Browse files
committed
skeleton added to docs site
1 parent 1bf57ee commit 8783f58

File tree

4 files changed

+340
-1
lines changed

4 files changed

+340
-1
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import React, { useEffect, useRef } from 'react';
2+
import { Animated, View, StyleSheet, Platform } from 'react-native';
3+
import { skeletonColors, animationConfig } from './styles';
4+
5+
// Generate a unique ID for each skeleton instance
6+
let skeletonCounter = 0;
7+
8+
export const Skeleton = ({
9+
className,
10+
mode = 'light',
11+
animated = true,
12+
}: {
13+
className: string;
14+
mode?: 'light' | 'dark';
15+
animated?: boolean;
16+
}) => {
17+
// Create a unique ID for this skeleton instance
18+
const skeletonId = useRef(`skeleton-${skeletonCounter++}`);
19+
20+
// Store the animated value in a ref so it persists between renders
21+
const animatedValueRef = useRef<Animated.Value | null>(null);
22+
23+
// Initialize the animated value only once
24+
if (animatedValueRef.current === null) {
25+
animatedValueRef.current = new Animated.Value(0);
26+
}
27+
28+
useEffect(() => {
29+
// Only set up animation if animated prop is true
30+
if (animated && animatedValueRef.current) {
31+
// Stop any existing animation and reset value
32+
animatedValueRef.current.stopAnimation();
33+
animatedValueRef.current.setValue(0);
34+
35+
// Create and start the animation loop
36+
const animationLoop = Animated.loop(
37+
Animated.sequence([
38+
Animated.timing(animatedValueRef.current, {
39+
toValue: 1,
40+
duration: animationConfig.duration,
41+
useNativeDriver: animationConfig.useNativeDriver,
42+
}),
43+
Animated.timing(animatedValueRef.current, {
44+
toValue: 0,
45+
duration: animationConfig.duration,
46+
useNativeDriver: animationConfig.useNativeDriver,
47+
}),
48+
])
49+
);
50+
51+
animationLoop.start();
52+
53+
// Clean up animation when component unmounts or animation changes
54+
return () => {
55+
if (animatedValueRef.current) {
56+
animatedValueRef.current.stopAnimation();
57+
}
58+
animationLoop.stop();
59+
};
60+
}
61+
}, [animated]); // Only depend on animated prop
62+
63+
// Get the base and highlight colors for the current mode
64+
const baseColor = mode === 'dark' ? skeletonColors.dark.base : skeletonColors.light.base;
65+
const highlightColor =
66+
mode === 'dark' ? skeletonColors.dark.highlight : skeletonColors.light.highlight;
67+
68+
// Create the animated style if animation is enabled
69+
let animatedStyle = {};
70+
let webAnimationClass = '';
71+
72+
if (animated) {
73+
if (Platform.OS === 'web') {
74+
// Use instance-specific class for web animation
75+
webAnimationClass = skeletonId.current;
76+
} else if (animatedValueRef.current) {
77+
// Use React Native's Animated API for native platforms
78+
const animatedBackgroundColor = animatedValueRef.current.interpolate({
79+
inputRange: [0, 1],
80+
outputRange: [baseColor, highlightColor],
81+
});
82+
animatedStyle = { backgroundColor: animatedBackgroundColor };
83+
}
84+
}
85+
86+
// Base style with background color
87+
const baseStyle = {
88+
backgroundColor: baseColor,
89+
// Add these styles to ensure the component is visible
90+
overflow: 'hidden' as const,
91+
opacity: 1,
92+
};
93+
94+
// Combine all styles
95+
const combinedStyle = animated && Platform.OS !== 'web' ? [baseStyle, animatedStyle] : baseStyle;
96+
97+
// Handle web animation - update or create instance-specific style element
98+
useEffect(() => {
99+
if (Platform.OS === 'web' && animated) {
100+
const styleId = `${skeletonId.current}-style`;
101+
const webStyles = `
102+
@keyframes ${skeletonId.current}Pulse {
103+
0%, 100% { background-color: ${baseColor}; }
104+
50% { background-color: ${highlightColor}; }
105+
}
106+
107+
.${skeletonId.current} {
108+
animation: ${skeletonId.current}Pulse ${animationConfig.duration * 2}ms infinite;
109+
background-color: ${baseColor};
110+
}
111+
`;
112+
113+
// Update or create the style element for this instance
114+
if (typeof document !== 'undefined') {
115+
let styleEl = document.getElementById(styleId);
116+
117+
if (!styleEl) {
118+
styleEl = document.createElement('style');
119+
styleEl.id = styleId;
120+
document.head.appendChild(styleEl);
121+
}
122+
123+
styleEl.innerHTML = webStyles;
124+
}
125+
126+
// Clean up on unmount
127+
return () => {
128+
if (typeof document !== 'undefined') {
129+
const styleEl = document.getElementById(styleId);
130+
if (styleEl) {
131+
styleEl.remove();
132+
}
133+
}
134+
};
135+
}
136+
}, [animated, baseColor, highlightColor, mode]);
137+
138+
// Use the appropriate View component
139+
return Platform.OS === 'web' && animated ? (
140+
<View className={`${className} ${skeletonId.current}`} style={baseStyle} />
141+
) : animated ? (
142+
<Animated.View className={className} style={combinedStyle} />
143+
) : (
144+
<View className={className} style={baseStyle} />
145+
);
146+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Color constants for skeleton
2+
export const skeletonColors = {
3+
light: {
4+
base: '#E5E5E5',
5+
highlight: '#F2F2F2',
6+
},
7+
dark: {
8+
base: '#1E1E1E',
9+
highlight: '#2A2A2A',
10+
},
11+
};
12+
13+
// Animation configuration
14+
export const animationConfig = {
15+
duration: 800,
16+
useNativeDriver: false,
17+
};

docs/pages/components/_meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"alert": "Alert",
44
"button": "Button",
55
"alert-dialog": "Alert Dialog",
6-
"avatar": "Avatar"
6+
"avatar": "Avatar",
7+
"skeleton": "Skeleton"
78
}

docs/pages/components/skeleton.mdx

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Skeleton Component
2+
3+
import { Skeleton } from '../../components/ui/skeleton'
4+
import ComponentPreview, { PreviewModeContext } from '../../components/ComponentPreview'
5+
import ComponentCode from '../../components/ComponentCode'
6+
import { useContext } from 'react'
7+
import { View } from 'react-native'
8+
9+
export const SkeletonWithMode = (props) => {
10+
const mode = useContext(PreviewModeContext) || 'light';
11+
// Ensure animation is enabled by default
12+
return <Skeleton mode={mode} animated={props.animated !== false} {...props} />;
13+
}
14+
15+
## Installation
16+
17+
The Skeleton component provides a placeholder loading state for content that is still loading.
18+
19+
<ComponentCode
20+
language="bash"
21+
code="npx @nativecn/cli add skeleton"
22+
title="Installation Command"
23+
/>
24+
25+
## Basic Usage
26+
27+
<ComponentPreview
28+
title="Basic Skeleton"
29+
code={`import { View } from 'react-native';
30+
import { Skeleton } from '../components/ui/skeleton';
31+
32+
export default function BasicSkeleton() {
33+
return (
34+
<View style={{ gap: 12 }}>
35+
<Skeleton mode="light" className="h-4 w-[250px]" />
36+
<Skeleton mode="light" className="h-4 w-[200px]" />
37+
<Skeleton mode="light" className="h-4 w-[150px]" />
38+
</View>
39+
);
40+
}`}
41+
>
42+
<View style={{ gap: 12 }}>
43+
<SkeletonWithMode className="h-4 w-[250px]" />
44+
<SkeletonWithMode className="h-4 w-[200px]" />
45+
<SkeletonWithMode className="h-4 w-[150px]" />
46+
</View>
47+
</ComponentPreview>
48+
49+
## Card Skeleton Example
50+
51+
<ComponentPreview
52+
title="Card Loading State"
53+
code={`import { View } from 'react-native';
54+
import { Skeleton } from '../components/ui/skeleton';
55+
56+
export default function CardSkeleton() {
57+
return (
58+
<View style={{ gap: 16 }}>
59+
<Skeleton mode="light" className="h-12 w-12 rounded-full" />
60+
<View style={{ gap: 8 }}>
61+
<Skeleton mode="light" className="h-4 w-[250px]" />
62+
<Skeleton mode="light" className="h-4 w-[200px]" />
63+
</View>
64+
</View>
65+
);
66+
}`}
67+
>
68+
<View style={{ gap: 16 }}>
69+
<SkeletonWithMode className="h-12 w-12 rounded-full" />
70+
<View style={{ gap: 8 }}>
71+
<SkeletonWithMode className="h-4 w-[250px]" />
72+
<SkeletonWithMode className="h-4 w-[200px]" />
73+
</View>
74+
</View>
75+
</ComponentPreview>
76+
77+
## Static vs Animated
78+
79+
<ComponentPreview
80+
title="Animation Comparison"
81+
code={`import { View } from 'react-native';
82+
import { Skeleton } from '../components/ui/skeleton';
83+
84+
export default function AnimationComparison() {
85+
return (
86+
<View style={{ gap: 12 }}>
87+
<Skeleton mode="light" className="h-4 w-[200px]" animated={true} />
88+
<Skeleton mode="light" className="h-4 w-[200px]" animated={false} />
89+
</View>
90+
);
91+
}`}
92+
>
93+
<View style={{ gap: 12 }}>
94+
<SkeletonWithMode className="h-4 w-[200px]" animated={true} />
95+
<SkeletonWithMode className="h-4 w-[200px]" animated={false} />
96+
</View>
97+
</ComponentPreview>
98+
99+
## Complex Content Example
100+
101+
<ComponentPreview
102+
title="Complex Loading State"
103+
code={`import { View } from 'react-native';
104+
import { Skeleton } from '../components/ui/skeleton';
105+
106+
export default function ComplexSkeleton() {
107+
return (
108+
<View>
109+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
110+
<Skeleton mode="light" className="h-12 w-12 rounded-full" />
111+
<View style={{ gap: 8 }}>
112+
<Skeleton mode="light" className="h-4 w-[250px]" />
113+
<Skeleton mode="light" className="h-4 w-[200px]" />
114+
</View>
115+
</View>
116+
<Skeleton mode="light" className="h-32 w-full mt-4" />
117+
</View>
118+
);
119+
}`}
120+
>
121+
<View>
122+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
123+
<SkeletonWithMode className="h-12 w-12 rounded-full" />
124+
<View style={{ gap: 8 }}>
125+
<SkeletonWithMode className="h-4 w-[250px]" />
126+
<SkeletonWithMode className="h-4 w-[200px]" />
127+
</View>
128+
</View>
129+
<SkeletonWithMode className="h-32 w-full mt-4" />
130+
</View>
131+
</ComponentPreview>
132+
133+
## Usage Example
134+
135+
<ComponentCode
136+
title="Basic Skeleton Usage with Responsive Mode"
137+
code={`import { View } from 'react-native';
138+
import { Skeleton } from '../components/ui/skeleton';
139+
import { useColorScheme } from 'react-native';
140+
141+
export function LoadingCard() {
142+
const colorScheme = useColorScheme();
143+
144+
return (
145+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
146+
<Skeleton
147+
className="h-12 w-12 rounded-full"
148+
mode={colorScheme} // 'light' or 'dark' based on system preference
149+
/>
150+
<View style={{ gap: 8 }}>
151+
<Skeleton
152+
className="h-4 w-[250px]"
153+
mode={colorScheme}
154+
/>
155+
<Skeleton
156+
className="h-4 w-[200px]"
157+
mode={colorScheme}
158+
/>
159+
</View>
160+
</View>
161+
);
162+
}`}
163+
/>
164+
165+
## Reference
166+
167+
<ComponentCode
168+
title="Skeleton Props"
169+
language="typescript"
170+
code={`interface SkeletonProps {
171+
className: string; // Required - Tailwind classes for styling
172+
mode?: 'light' | 'dark'; // Optional - Controls light/dark appearance
173+
animated?: boolean; // Optional - Controls animation, defaults to true
174+
}`}
175+
/>

0 commit comments

Comments
 (0)