react-native-skelo 0.0.3 → 0.0.5

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.
Files changed (38) hide show
  1. package/README.md +86 -92
  2. package/lib/commonjs/animations/PulseAnimation.js +17 -18
  3. package/lib/commonjs/animations/PulseAnimation.js.map +1 -1
  4. package/lib/commonjs/animations/ShimmerAnimation.js +47 -28
  5. package/lib/commonjs/animations/ShimmerAnimation.js.map +1 -1
  6. package/lib/commonjs/components/Skeleton.js +22 -16
  7. package/lib/commonjs/components/Skeleton.js.map +1 -1
  8. package/lib/commonjs/core/parser/expandLists.js +67 -0
  9. package/lib/commonjs/core/parser/expandLists.js.map +1 -0
  10. package/lib/commonjs/index.js +1 -0
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/module/animations/PulseAnimation.js +18 -19
  13. package/lib/module/animations/PulseAnimation.js.map +1 -1
  14. package/lib/module/animations/ShimmerAnimation.js +48 -29
  15. package/lib/module/animations/ShimmerAnimation.js.map +1 -1
  16. package/lib/module/components/Skeleton.js +22 -16
  17. package/lib/module/components/Skeleton.js.map +1 -1
  18. package/lib/module/core/parser/expandLists.js +61 -0
  19. package/lib/module/core/parser/expandLists.js.map +1 -0
  20. package/lib/module/index.js +4 -0
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/typescript/animations/PulseAnimation.d.ts.map +1 -1
  23. package/lib/typescript/animations/ShimmerAnimation.d.ts.map +1 -1
  24. package/lib/typescript/components/Skeleton.d.ts +1 -1
  25. package/lib/typescript/components/Skeleton.d.ts.map +1 -1
  26. package/lib/typescript/core/parser/expandLists.d.ts +3 -0
  27. package/lib/typescript/core/parser/expandLists.d.ts.map +1 -0
  28. package/lib/typescript/index.d.ts +1 -0
  29. package/lib/typescript/index.d.ts.map +1 -1
  30. package/lib/typescript/types/skeleton.d.ts +8 -0
  31. package/lib/typescript/types/skeleton.d.ts.map +1 -1
  32. package/package.json +5 -10
  33. package/src/animations/PulseAnimation.tsx +17 -27
  34. package/src/animations/ShimmerAnimation.tsx +36 -47
  35. package/src/components/Skeleton.tsx +24 -18
  36. package/src/core/parser/expandLists.tsx +77 -0
  37. package/src/index.ts +4 -0
  38. package/src/types/skeleton.ts +9 -0
@@ -1,14 +1,6 @@
1
- import React, { useEffect } from 'react';
1
+ import React, { useEffect, useRef } from 'react';
2
2
  import type { DimensionValue, StyleProp, ViewStyle } from 'react-native';
3
- import { StyleSheet } from 'react-native';
4
- import Animated, {
5
- useSharedValue,
6
- useAnimatedStyle,
7
- withRepeat,
8
- withSequence,
9
- withTiming,
10
- Easing,
11
- } from 'react-native-reanimated';
3
+ import { Animated, Easing, StyleSheet } from 'react-native';
12
4
 
13
5
  interface PulseProps {
14
6
  width?: DimensionValue;
@@ -21,7 +13,7 @@ interface PulseProps {
21
13
  children?: React.ReactNode;
22
14
  }
23
15
 
24
- // Breathing opacity loop on the UI thread with Reanimated.
16
+ // Breathing opacity loop using React Native's built-in Animated (native driver).
25
17
  export function PulseAnimation({
26
18
  width,
27
19
  height,
@@ -31,36 +23,34 @@ export function PulseAnimation({
31
23
  style,
32
24
  children,
33
25
  }: PulseProps) {
34
- const opacity = useSharedValue(1);
26
+ const opacity = useRef(new Animated.Value(1)).current;
35
27
 
36
28
  useEffect(() => {
37
- opacity.value = withRepeat(
38
- withSequence(
39
- withTiming(0.5, {
29
+ const loop = Animated.loop(
30
+ Animated.sequence([
31
+ Animated.timing(opacity, {
32
+ toValue: 0.5,
40
33
  duration: duration / 2,
41
34
  easing: Easing.inOut(Easing.ease),
35
+ useNativeDriver: true,
42
36
  }),
43
- withTiming(1, {
37
+ Animated.timing(opacity, {
38
+ toValue: 1,
44
39
  duration: duration / 2,
45
40
  easing: Easing.inOut(Easing.ease),
46
- })
47
- ),
48
- -1,
49
- false
41
+ useNativeDriver: true,
42
+ }),
43
+ ])
50
44
  );
45
+ loop.start();
46
+ return () => loop.stop();
51
47
  }, [duration, opacity]);
52
48
 
53
- const animatedStyle = useAnimatedStyle(() => {
54
- 'worklet';
55
- return { opacity: opacity.value };
56
- }, []);
57
-
58
49
  return (
59
50
  <Animated.View
60
51
  style={[
61
52
  styles.container,
62
- { width, height, borderRadius, backgroundColor: baseColor },
63
- animatedStyle,
53
+ { width, height, borderRadius, backgroundColor: baseColor, opacity },
64
54
  style,
65
55
  ]}
66
56
  >
@@ -1,19 +1,6 @@
1
- import React, { useEffect, useState } from 'react';
2
- import type {
3
- DimensionValue,
4
- LayoutChangeEvent,
5
- StyleProp,
6
- ViewStyle,
7
- } from 'react-native';
8
- import { StyleSheet, View } from 'react-native';
9
- import Animated, {
10
- useSharedValue,
11
- useAnimatedStyle,
12
- withRepeat,
13
- withTiming,
14
- interpolate,
15
- Easing,
16
- } from 'react-native-reanimated';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import type { DimensionValue, LayoutChangeEvent, StyleProp, ViewStyle } from 'react-native';
3
+ import { Animated, Easing, StyleSheet, View } from 'react-native';
17
4
 
18
5
  interface ShimmerProps {
19
6
  width?: DimensionValue;
@@ -26,9 +13,9 @@ interface ShimmerProps {
26
13
  children?: React.ReactNode;
27
14
  }
28
15
 
29
- // Shimmer with no gradient library: the sweeping overlay uses React Native's
30
- // built-in `experimental_backgroundImage` CSS gradient (New Architecture),
31
- // animated on the UI thread with Reanimated.
16
+ // Zero-dependency shimmer: a highlight band (dim/bright/dim segments to fake a
17
+ // soft gradient) sweeps across the base, driven by React Native's built-in
18
+ // Animated native driver. Plain Views only — guaranteed to render and animate.
32
19
  export function ShimmerAnimation({
33
20
  width,
34
21
  height,
@@ -39,15 +26,20 @@ export function ShimmerAnimation({
39
26
  style,
40
27
  children,
41
28
  }: ShimmerProps) {
42
- const progress = useSharedValue(0);
29
+ const progress = useRef(new Animated.Value(0)).current;
43
30
  const [measuredWidth, setMeasuredWidth] = useState(0);
44
31
 
45
32
  useEffect(() => {
46
- progress.value = withRepeat(
47
- withTiming(1, { duration, easing: Easing.linear }),
48
- -1,
49
- false
33
+ const loop = Animated.loop(
34
+ Animated.timing(progress, {
35
+ toValue: 1,
36
+ duration,
37
+ easing: Easing.linear,
38
+ useNativeDriver: true,
39
+ })
50
40
  );
41
+ loop.start();
42
+ return () => loop.stop();
51
43
  }, [duration, progress]);
52
44
 
53
45
  const onLayout = (event: LayoutChangeEvent) => {
@@ -55,38 +47,26 @@ export function ShimmerAnimation({
55
47
  setMeasuredWidth(prev => (prev === w ? prev : w));
56
48
  };
57
49
 
58
- const containerWidth = typeof width === 'number' ? width : measuredWidth;
50
+ const containerWidth = typeof width === 'number' ? width : measuredWidth || 200;
51
+ const bandWidth = Math.max(containerWidth * 0.6, 80);
59
52
 
60
- const animatedStyle = useAnimatedStyle(() => {
61
- 'worklet';
62
- const translateX = interpolate(
63
- progress.value,
64
- [0, 1],
65
- [-containerWidth, containerWidth]
66
- );
67
- return { transform: [{ translateX }] };
68
- }, [containerWidth]);
69
-
70
- // `experimental_backgroundImage` isn't in the ViewStyle types yet.
71
- const gradientStyle = {
72
- ...StyleSheet.absoluteFillObject,
73
- experimental_backgroundImage: `linear-gradient(to right, ${baseColor}, ${highlightColor}, ${baseColor})`,
74
- } as unknown as ViewStyle;
53
+ const translateX = progress.interpolate({
54
+ inputRange: [0, 1],
55
+ outputRange: [-bandWidth, containerWidth],
56
+ });
75
57
 
76
58
  return (
77
59
  <View
78
60
  onLayout={onLayout}
79
- style={[
80
- styles.container,
81
- { width, height, borderRadius, backgroundColor: baseColor },
82
- style,
83
- ]}
61
+ style={[styles.container, { width, height, borderRadius, backgroundColor: baseColor }, style]}
84
62
  >
85
63
  <Animated.View
86
64
  pointerEvents="none"
87
- style={[StyleSheet.absoluteFill, animatedStyle]}
65
+ style={[styles.band, { width: bandWidth, transform: [{ translateX }] }]}
88
66
  >
89
- <View style={gradientStyle} />
67
+ <View style={[styles.segment, { backgroundColor: highlightColor, opacity: 0.25 }]} />
68
+ <View style={[styles.segment, { backgroundColor: highlightColor, opacity: 0.9 }]} />
69
+ <View style={[styles.segment, { backgroundColor: highlightColor, opacity: 0.25 }]} />
90
70
  </Animated.View>
91
71
 
92
72
  {children}
@@ -98,4 +78,13 @@ const styles = StyleSheet.create({
98
78
  container: {
99
79
  overflow: 'hidden',
100
80
  },
81
+ band: {
82
+ position: 'absolute',
83
+ top: 0,
84
+ bottom: 0,
85
+ flexDirection: 'row',
86
+ },
87
+ segment: {
88
+ flex: 1,
89
+ },
101
90
  });
@@ -5,6 +5,7 @@ import { SkeletonRenderer } from './SkeletonRenderer';
5
5
  import { SkeletonIgnore } from './SkeletonIgnore';
6
6
  import { StyleSkeleton } from '../core/generator/StyleSkeleton';
7
7
  import { getDeepExpander } from '../core/deepRegistry';
8
+ import { expandListPlaceholders } from '../core/parser/expandLists';
8
9
  import { DEFAULT_CONFIG } from '../constants/defaults';
9
10
 
10
11
  /**
@@ -40,7 +41,8 @@ export function Skeleton({
40
41
  children,
41
42
  styles,
42
43
  excludeStyles,
43
- deep = false,
44
+ deep,
45
+ count = 6,
44
46
  animation = DEFAULT_CONFIG.animation,
45
47
  duration = DEFAULT_CONFIG.duration,
46
48
  baseColor = DEFAULT_CONFIG.baseColor,
@@ -63,27 +65,31 @@ export function Skeleton({
63
65
  }
64
66
 
65
67
  try {
68
+ // Turn FlatList/SectionList into placeholder rows so lists show a
69
+ // skeleton even though their data (and rows) don't exist yet.
70
+ const prepared = expandListPlaceholders(children, count);
71
+
66
72
  // Deep mode: expand opaque components into their real host tree via the
67
73
  // opt-in expander (registered by importing 'react-native-skelo/deep').
68
- // Falls back to static parsing if unavailable or it returns nothing.
69
- if (deep) {
70
- const expand = getDeepExpander();
71
- if (expand) {
72
- const expanded = expand(<>{children}</>);
73
- if (expanded && expanded.length > 0) {
74
- return expanded;
75
- }
76
- if (__DEV__) {
77
- console.warn('[Skelo] deep expansion returned nothing; falling back to static parsing.');
78
- }
79
- } else if (__DEV__) {
80
- console.warn(
81
- "[Skelo] `deep` requires react-reconciler. Install it and add `import 'react-native-skelo/deep';` in your app entry."
82
- );
74
+ // When `deep` is not set explicitly, it's used automatically whenever the
75
+ // expander is available. Falls back to static parsing otherwise.
76
+ const expand = getDeepExpander();
77
+ const wantDeep = deep === undefined ? expand !== null : deep;
78
+ if (wantDeep && expand) {
79
+ const expanded = expand(<>{prepared}</>);
80
+ if (expanded && expanded.length > 0) {
81
+ return expanded;
82
+ }
83
+ if (__DEV__) {
84
+ console.warn('[Skelo] deep expansion returned nothing; falling back to static parsing.');
83
85
  }
86
+ } else if (deep === true && !expand && __DEV__) {
87
+ console.warn(
88
+ "[Skelo] `deep` requires react-reconciler. Install it and add `import 'react-native-skelo/deep';` in your app entry."
89
+ );
84
90
  }
85
91
 
86
- const parsed = Parser.parse(children);
92
+ const parsed = Parser.parse(prepared);
87
93
 
88
94
  if (__DEV__ && debug) {
89
95
  // Log a lightweight summary: the raw nodes hold React elements
@@ -104,7 +110,7 @@ export function Skeleton({
104
110
  }
105
111
  return [];
106
112
  }
107
- }, [loading, styles, deep, children, debug]);
113
+ }, [loading, styles, deep, count, children, debug]);
108
114
 
109
115
  // Create skeleton config
110
116
  const config = useMemo(
@@ -0,0 +1,77 @@
1
+ import React, { isValidElement } from 'react';
2
+ import type { ReactElement, ReactNode } from 'react';
3
+ import { View } from 'react-native';
4
+ import { getComponentName } from '../../utils/componentUtils';
5
+
6
+ // List components whose rows come from `renderItem` (not JSX children).
7
+ const LIST_TYPES = new Set(['FlatList', 'SectionList', 'VirtualizedList', 'FlashList']);
8
+
9
+ const NOOP_SEPARATORS = {
10
+ highlight: () => {},
11
+ unhighlight: () => {},
12
+ updateProps: () => {},
13
+ };
14
+
15
+ // Renders `count` sample rows from a list's `renderItem` so there's something
16
+ // to skeletonize while the real data is still loading (the rows don't exist in
17
+ // the tree yet). Rows that throw on placeholder data are skipped.
18
+ function renderSampleRows(list: ReactElement, count: number): ReactNode {
19
+ const props = (list.props || {}) as {
20
+ renderItem?: (info: {
21
+ item: unknown;
22
+ index: number;
23
+ section?: unknown;
24
+ separators: typeof NOOP_SEPARATORS;
25
+ }) => ReactNode;
26
+ data?: unknown[];
27
+ contentContainerStyle?: unknown;
28
+ };
29
+
30
+ const data = Array.isArray(props.data) ? props.data : [];
31
+ const sample = data.length > 0 ? data[0] : {};
32
+
33
+ const rows: ReactNode[] = [];
34
+ for (let i = 0; i < count; i++) {
35
+ try {
36
+ const row = props.renderItem?.({
37
+ item: sample,
38
+ index: i,
39
+ section: {},
40
+ separators: NOOP_SEPARATORS,
41
+ });
42
+ if (row) {
43
+ rows.push(<React.Fragment key={i}>{row}</React.Fragment>);
44
+ }
45
+ } catch {
46
+ // Row couldn't render with placeholder data — skip it.
47
+ }
48
+ }
49
+
50
+ return <View style={props.contentContainerStyle as never}>{rows}</View>;
51
+ }
52
+
53
+ // Walks a child tree and replaces FlatList/SectionList with sample rows so a
54
+ // plain `<Skeleton><FlatList/></Skeleton>` produces a skeleton while loading.
55
+ export function expandListPlaceholders(children: ReactNode, count: number): ReactNode {
56
+ return React.Children.map(children, child => {
57
+ if (!isValidElement(child)) {
58
+ return child;
59
+ }
60
+
61
+ const name = getComponentName(child);
62
+ const props = (child.props || {}) as { children?: ReactNode };
63
+
64
+ if (
65
+ LIST_TYPES.has(name) &&
66
+ typeof (props as { renderItem?: unknown }).renderItem === 'function'
67
+ ) {
68
+ return renderSampleRows(child, count);
69
+ }
70
+
71
+ if (props.children) {
72
+ return React.cloneElement(child, undefined, expandListPlaceholders(props.children, count));
73
+ }
74
+
75
+ return child;
76
+ });
77
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,10 @@
6
6
  * @packageDocumentation
7
7
  */
8
8
 
9
+ // Auto-register deep expansion (react-reconciler ships as a dependency), so
10
+ // `<Skeleton deep>` / opaque components work with zero setup.
11
+ import './deep';
12
+
9
13
  // Main component
10
14
  export { Skeleton } from './components/Skeleton';
11
15
  export { SkeletonIgnore } from './components/SkeletonIgnore';
@@ -79,6 +79,15 @@ export interface SkeletonProps {
79
79
  */
80
80
  excludeStyles?: string[];
81
81
 
82
+ /**
83
+ * Number of placeholder rows to render for a `FlatList` / `SectionList`
84
+ * while its data is still loading (its rows don't exist yet, so Skelo calls
85
+ * `renderItem` with a placeholder and repeats it `count` times).
86
+ *
87
+ * @default 6
88
+ */
89
+ count?: number;
90
+
82
91
  /**
83
92
  * Deep mode: expand opaque (custom) child components into their real
84
93
  * host-element tree via offline rendering, producing a structured skeleton