react-native-skelo 0.0.1 → 0.0.3
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/LICENSE +21 -0
- package/README.md +149 -20
- package/lib/commonjs/Skelo.js +104 -0
- package/lib/commonjs/Skelo.js.map +1 -0
- package/lib/commonjs/animations/AnimationWrapper.js +76 -0
- package/lib/commonjs/animations/AnimationWrapper.js.map +1 -0
- package/lib/commonjs/animations/PulseAnimation.js +52 -0
- package/lib/commonjs/animations/PulseAnimation.js.map +1 -0
- package/lib/commonjs/animations/ShimmerAnimation.js +73 -0
- package/lib/commonjs/animations/ShimmerAnimation.js.map +1 -0
- package/lib/commonjs/animations/index.js +27 -0
- package/lib/commonjs/animations/index.js.map +1 -0
- package/lib/commonjs/components/MeasuredBlock.js +64 -0
- package/lib/commonjs/components/MeasuredBlock.js.map +1 -0
- package/lib/commonjs/components/Skeleton.js +144 -0
- package/lib/commonjs/components/Skeleton.js.map +1 -0
- package/lib/commonjs/components/SkeletonIgnore.js +30 -0
- package/lib/commonjs/components/SkeletonIgnore.js.map +1 -0
- package/lib/commonjs/components/SkeletonRenderer.js +270 -0
- package/lib/commonjs/components/SkeletonRenderer.js.map +1 -0
- package/lib/commonjs/components/primitives/SkeletonBox.js +47 -0
- package/lib/commonjs/components/primitives/SkeletonBox.js.map +1 -0
- package/lib/commonjs/components/primitives/SkeletonCircle.js +44 -0
- package/lib/commonjs/components/primitives/SkeletonCircle.js.map +1 -0
- package/lib/commonjs/components/primitives/SkeletonImage.js +46 -0
- package/lib/commonjs/components/primitives/SkeletonImage.js.map +1 -0
- package/lib/commonjs/components/primitives/SkeletonText.js +60 -0
- package/lib/commonjs/components/primitives/SkeletonText.js.map +1 -0
- package/lib/commonjs/components/primitives/index.js +34 -0
- package/lib/commonjs/components/primitives/index.js.map +1 -0
- package/lib/commonjs/components/withSkeleton.js +45 -0
- package/lib/commonjs/components/withSkeleton.js.map +1 -0
- package/lib/commonjs/constants/defaults.js +43 -0
- package/lib/commonjs/constants/defaults.js.map +1 -0
- package/lib/commonjs/core/deepRegistry.js +18 -0
- package/lib/commonjs/core/deepRegistry.js.map +1 -0
- package/lib/commonjs/core/generator/StyleSkeleton.js +101 -0
- package/lib/commonjs/core/generator/StyleSkeleton.js.map +1 -0
- package/lib/commonjs/core/parser/Parser.js +84 -0
- package/lib/commonjs/core/parser/Parser.js.map +1 -0
- package/lib/commonjs/core/parser/expandOffline.js +83 -0
- package/lib/commonjs/core/parser/expandOffline.js.map +1 -0
- package/lib/commonjs/core/parser/hostTreeReconciler.js +228 -0
- package/lib/commonjs/core/parser/hostTreeReconciler.js.map +1 -0
- package/lib/commonjs/core/parser/index.js +13 -0
- package/lib/commonjs/core/parser/index.js.map +1 -0
- package/lib/commonjs/core/registry/SkeletonRegistry.js +113 -0
- package/lib/commonjs/core/registry/SkeletonRegistry.js.map +1 -0
- package/lib/commonjs/core/registry/index.js +19 -0
- package/lib/commonjs/core/registry/index.js.map +1 -0
- package/lib/commonjs/core/resolver/StyleResolver.js +83 -0
- package/lib/commonjs/core/resolver/StyleResolver.js.map +1 -0
- package/lib/commonjs/core/resolver/index.js +13 -0
- package/lib/commonjs/core/resolver/index.js.map +1 -0
- package/lib/commonjs/deep.js +18 -0
- package/lib/commonjs/deep.js.map +1 -0
- package/lib/commonjs/index.js +66 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/plugins/ImagePlugin.js +60 -0
- package/lib/commonjs/plugins/ImagePlugin.js.map +1 -0
- package/lib/commonjs/plugins/TextPlugin.js +70 -0
- package/lib/commonjs/plugins/TextPlugin.js.map +1 -0
- package/lib/commonjs/plugins/ViewPlugin.js +71 -0
- package/lib/commonjs/plugins/ViewPlugin.js.map +1 -0
- package/lib/commonjs/plugins/index.js +27 -0
- package/lib/commonjs/plugins/index.js.map +1 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/commonjs/types/index.js.map +1 -0
- package/lib/commonjs/types/plugin.js +6 -0
- package/lib/commonjs/types/plugin.js.map +1 -0
- package/lib/commonjs/types/skeleton.js +6 -0
- package/lib/commonjs/types/skeleton.js.map +1 -0
- package/lib/commonjs/utils/componentUtils.js +138 -0
- package/lib/commonjs/utils/componentUtils.js.map +1 -0
- package/lib/commonjs/utils/index.js +62 -0
- package/lib/commonjs/utils/index.js.map +1 -0
- package/lib/commonjs/utils/styleUtils.js +76 -0
- package/lib/commonjs/utils/styleUtils.js.map +1 -0
- package/lib/module/Skelo.js +98 -0
- package/lib/module/Skelo.js.map +1 -0
- package/lib/module/animations/AnimationWrapper.js +69 -0
- package/lib/module/animations/AnimationWrapper.js.map +1 -0
- package/lib/module/animations/PulseAnimation.js +45 -0
- package/lib/module/animations/PulseAnimation.js.map +1 -0
- package/lib/module/animations/ShimmerAnimation.js +66 -0
- package/lib/module/animations/ShimmerAnimation.js.map +1 -0
- package/lib/module/animations/index.js +4 -0
- package/lib/module/animations/index.js.map +1 -0
- package/lib/module/components/MeasuredBlock.js +57 -0
- package/lib/module/components/MeasuredBlock.js.map +1 -0
- package/lib/module/components/Skeleton.js +138 -0
- package/lib/module/components/Skeleton.js.map +1 -0
- package/lib/module/components/SkeletonIgnore.js +23 -0
- package/lib/module/components/SkeletonIgnore.js.map +1 -0
- package/lib/module/components/SkeletonRenderer.js +264 -0
- package/lib/module/components/SkeletonRenderer.js.map +1 -0
- package/lib/module/components/primitives/SkeletonBox.js +40 -0
- package/lib/module/components/primitives/SkeletonBox.js.map +1 -0
- package/lib/module/components/primitives/SkeletonCircle.js +37 -0
- package/lib/module/components/primitives/SkeletonCircle.js.map +1 -0
- package/lib/module/components/primitives/SkeletonImage.js +39 -0
- package/lib/module/components/primitives/SkeletonImage.js.map +1 -0
- package/lib/module/components/primitives/SkeletonText.js +53 -0
- package/lib/module/components/primitives/SkeletonText.js.map +1 -0
- package/lib/module/components/primitives/index.js +5 -0
- package/lib/module/components/primitives/index.js.map +1 -0
- package/lib/module/components/withSkeleton.js +38 -0
- package/lib/module/components/withSkeleton.js.map +1 -0
- package/lib/module/constants/defaults.js +37 -0
- package/lib/module/constants/defaults.js.map +1 -0
- package/lib/module/core/deepRegistry.js +11 -0
- package/lib/module/core/deepRegistry.js.map +1 -0
- package/lib/module/core/generator/StyleSkeleton.js +95 -0
- package/lib/module/core/generator/StyleSkeleton.js.map +1 -0
- package/lib/module/core/parser/Parser.js +77 -0
- package/lib/module/core/parser/Parser.js.map +1 -0
- package/lib/module/core/parser/expandOffline.js +77 -0
- package/lib/module/core/parser/expandOffline.js.map +1 -0
- package/lib/module/core/parser/hostTreeReconciler.js +222 -0
- package/lib/module/core/parser/hostTreeReconciler.js.map +1 -0
- package/lib/module/core/parser/index.js +2 -0
- package/lib/module/core/parser/index.js.map +1 -0
- package/lib/module/core/registry/SkeletonRegistry.js +106 -0
- package/lib/module/core/registry/SkeletonRegistry.js.map +1 -0
- package/lib/module/core/registry/index.js +2 -0
- package/lib/module/core/registry/index.js.map +1 -0
- package/lib/module/core/resolver/StyleResolver.js +77 -0
- package/lib/module/core/resolver/StyleResolver.js.map +1 -0
- package/lib/module/core/resolver/index.js +2 -0
- package/lib/module/core/resolver/index.js.map +1 -0
- package/lib/module/deep.js +11 -0
- package/lib/module/deep.js.map +1 -0
- package/lib/module/index.js +21 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/plugins/ImagePlugin.js +53 -0
- package/lib/module/plugins/ImagePlugin.js.map +1 -0
- package/lib/module/plugins/TextPlugin.js +63 -0
- package/lib/module/plugins/TextPlugin.js.map +1 -0
- package/lib/module/plugins/ViewPlugin.js +64 -0
- package/lib/module/plugins/ViewPlugin.js.map +1 -0
- package/lib/module/plugins/index.js +4 -0
- package/lib/module/plugins/index.js.map +1 -0
- package/lib/module/types/index.js +2 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/types/plugin.js +2 -0
- package/lib/module/types/plugin.js.map +1 -0
- package/lib/module/types/skeleton.js +2 -0
- package/lib/module/types/skeleton.js.map +1 -0
- package/lib/module/utils/componentUtils.js +123 -0
- package/lib/module/utils/componentUtils.js.map +1 -0
- package/lib/module/utils/index.js +3 -0
- package/lib/module/utils/index.js.map +1 -0
- package/lib/module/utils/styleUtils.js +66 -0
- package/lib/module/utils/styleUtils.js.map +1 -0
- package/lib/typescript/Skelo.d.ts +76 -0
- package/lib/typescript/Skelo.d.ts.map +1 -0
- package/lib/typescript/animations/AnimationWrapper.d.ts +66 -0
- package/lib/typescript/animations/AnimationWrapper.d.ts.map +1 -0
- package/lib/typescript/animations/PulseAnimation.d.ts +15 -0
- package/lib/typescript/animations/PulseAnimation.d.ts.map +1 -0
- package/lib/typescript/animations/ShimmerAnimation.d.ts +15 -0
- package/lib/typescript/animations/ShimmerAnimation.d.ts.map +1 -0
- package/lib/typescript/animations/index.d.ts +5 -0
- package/lib/typescript/animations/index.d.ts.map +1 -0
- package/lib/typescript/components/MeasuredBlock.d.ts +32 -0
- package/lib/typescript/components/MeasuredBlock.d.ts.map +1 -0
- package/lib/typescript/components/Skeleton.d.ts +37 -0
- package/lib/typescript/components/Skeleton.d.ts.map +1 -0
- package/lib/typescript/components/SkeletonIgnore.d.ts +26 -0
- package/lib/typescript/components/SkeletonIgnore.d.ts.map +1 -0
- package/lib/typescript/components/SkeletonRenderer.d.ts +39 -0
- package/lib/typescript/components/SkeletonRenderer.d.ts.map +1 -0
- package/lib/typescript/components/primitives/SkeletonBox.d.ts +21 -0
- package/lib/typescript/components/primitives/SkeletonBox.d.ts.map +1 -0
- package/lib/typescript/components/primitives/SkeletonCircle.d.ts +21 -0
- package/lib/typescript/components/primitives/SkeletonCircle.d.ts.map +1 -0
- package/lib/typescript/components/primitives/SkeletonImage.d.ts +21 -0
- package/lib/typescript/components/primitives/SkeletonImage.d.ts.map +1 -0
- package/lib/typescript/components/primitives/SkeletonText.d.ts +21 -0
- package/lib/typescript/components/primitives/SkeletonText.d.ts.map +1 -0
- package/lib/typescript/components/primitives/index.d.ts +5 -0
- package/lib/typescript/components/primitives/index.d.ts.map +1 -0
- package/lib/typescript/components/withSkeleton.d.ts +37 -0
- package/lib/typescript/components/withSkeleton.d.ts.map +1 -0
- package/lib/typescript/constants/defaults.d.ts +29 -0
- package/lib/typescript/constants/defaults.d.ts.map +1 -0
- package/lib/typescript/core/deepRegistry.d.ts +7 -0
- package/lib/typescript/core/deepRegistry.d.ts.map +1 -0
- package/lib/typescript/core/generator/StyleSkeleton.d.ts +36 -0
- package/lib/typescript/core/generator/StyleSkeleton.d.ts.map +1 -0
- package/lib/typescript/core/parser/Parser.d.ts +30 -0
- package/lib/typescript/core/parser/Parser.d.ts.map +1 -0
- package/lib/typescript/core/parser/expandOffline.d.ts +10 -0
- package/lib/typescript/core/parser/expandOffline.d.ts.map +1 -0
- package/lib/typescript/core/parser/hostTreeReconciler.d.ts +28 -0
- package/lib/typescript/core/parser/hostTreeReconciler.d.ts.map +1 -0
- package/lib/typescript/core/parser/index.d.ts +2 -0
- package/lib/typescript/core/parser/index.d.ts.map +1 -0
- package/lib/typescript/core/registry/SkeletonRegistry.d.ts +57 -0
- package/lib/typescript/core/registry/SkeletonRegistry.d.ts.map +1 -0
- package/lib/typescript/core/registry/index.d.ts +2 -0
- package/lib/typescript/core/registry/index.d.ts.map +1 -0
- package/lib/typescript/core/resolver/StyleResolver.d.ts +54 -0
- package/lib/typescript/core/resolver/StyleResolver.d.ts.map +1 -0
- package/lib/typescript/core/resolver/index.d.ts +2 -0
- package/lib/typescript/core/resolver/index.d.ts.map +1 -0
- package/lib/typescript/deep.d.ts +2 -0
- package/lib/typescript/deep.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +17 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/plugins/ImagePlugin.d.ts +11 -0
- package/lib/typescript/plugins/ImagePlugin.d.ts.map +1 -0
- package/lib/typescript/plugins/TextPlugin.d.ts +12 -0
- package/lib/typescript/plugins/TextPlugin.d.ts.map +1 -0
- package/lib/typescript/plugins/ViewPlugin.d.ts +11 -0
- package/lib/typescript/plugins/ViewPlugin.d.ts.map +1 -0
- package/lib/typescript/plugins/index.d.ts +4 -0
- package/lib/typescript/plugins/index.d.ts.map +1 -0
- package/lib/typescript/types/index.d.ts +3 -0
- package/lib/typescript/types/index.d.ts.map +1 -0
- package/lib/typescript/types/plugin.d.ts +144 -0
- package/lib/typescript/types/plugin.d.ts.map +1 -0
- package/lib/typescript/types/skeleton.d.ts +176 -0
- package/lib/typescript/types/skeleton.d.ts.map +1 -0
- package/lib/typescript/utils/componentUtils.d.ts +40 -0
- package/lib/typescript/utils/componentUtils.d.ts.map +1 -0
- package/lib/typescript/utils/index.d.ts +3 -0
- package/lib/typescript/utils/index.d.ts.map +1 -0
- package/lib/typescript/utils/styleUtils.d.ts +41 -0
- package/lib/typescript/utils/styleUtils.d.ts.map +1 -0
- package/package.json +169 -11
- package/src/Skelo.ts +103 -0
- package/src/animations/AnimationWrapper.tsx +139 -0
- package/src/animations/PulseAnimation.tsx +76 -0
- package/src/animations/ShimmerAnimation.tsx +101 -0
- package/src/animations/index.ts +4 -0
- package/src/components/MeasuredBlock.tsx +85 -0
- package/src/components/Skeleton.tsx +149 -0
- package/src/components/SkeletonIgnore.tsx +27 -0
- package/src/components/SkeletonRenderer.tsx +341 -0
- package/src/components/primitives/SkeletonBox.tsx +53 -0
- package/src/components/primitives/SkeletonCircle.tsx +48 -0
- package/src/components/primitives/SkeletonImage.tsx +50 -0
- package/src/components/primitives/SkeletonText.tsx +69 -0
- package/src/components/primitives/index.ts +4 -0
- package/src/components/withSkeleton.tsx +62 -0
- package/src/constants/defaults.ts +38 -0
- package/src/core/deepRegistry.ts +16 -0
- package/src/core/generator/StyleSkeleton.tsx +138 -0
- package/src/core/parser/Parser.ts +86 -0
- package/src/core/parser/expandOffline.tsx +83 -0
- package/src/core/parser/hostTreeReconciler.tsx +271 -0
- package/src/core/parser/index.ts +1 -0
- package/src/core/registry/SkeletonRegistry.ts +111 -0
- package/src/core/registry/index.ts +1 -0
- package/src/core/resolver/StyleResolver.ts +80 -0
- package/src/core/resolver/index.ts +1 -0
- package/src/deep.ts +12 -0
- package/src/index.ts +30 -0
- package/src/plugins/ImagePlugin.tsx +65 -0
- package/src/plugins/TextPlugin.tsx +71 -0
- package/src/plugins/ViewPlugin.tsx +67 -0
- package/src/plugins/index.ts +3 -0
- package/src/types/index.ts +14 -0
- package/src/types/plugin.ts +164 -0
- package/src/types/skeleton.ts +197 -0
- package/src/utils/componentUtils.ts +166 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/styleUtils.ts +72 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import type { ImageProps } from '../../types';
|
|
4
|
+
import { AnimationWrapper } from '../../animations';
|
|
5
|
+
import type { AnimationType } from '../../animations';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Image skeleton primitive
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <SkeletonImage width={80} height={80} borderRadius={40} />
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function SkeletonImage({
|
|
16
|
+
width = 100,
|
|
17
|
+
height = 100,
|
|
18
|
+
borderRadius = 0,
|
|
19
|
+
style,
|
|
20
|
+
animation = 'shimmer',
|
|
21
|
+
baseColor,
|
|
22
|
+
highlightColor,
|
|
23
|
+
duration,
|
|
24
|
+
}: ImageProps & {
|
|
25
|
+
animation?: AnimationType;
|
|
26
|
+
baseColor?: string;
|
|
27
|
+
highlightColor?: string;
|
|
28
|
+
duration?: number;
|
|
29
|
+
}) {
|
|
30
|
+
return (
|
|
31
|
+
<AnimationWrapper
|
|
32
|
+
animation={animation}
|
|
33
|
+
width={width}
|
|
34
|
+
height={height}
|
|
35
|
+
borderRadius={borderRadius}
|
|
36
|
+
baseColor={baseColor}
|
|
37
|
+
highlightColor={highlightColor}
|
|
38
|
+
duration={duration}
|
|
39
|
+
style={[styles.image, style]}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const styles = StyleSheet.create({
|
|
45
|
+
image: {
|
|
46
|
+
overflow: 'hidden',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
SkeletonImage.displayName = 'Skeleton.Image';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import type { TextProps } from '../../types';
|
|
4
|
+
import { AnimationWrapper } from '../../animations';
|
|
5
|
+
import type { AnimationType } from '../../animations';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Text skeleton primitive with multiple lines
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <SkeletonText lines={3} fontSize={16} lastLineWidth="70%" />
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function SkeletonText({
|
|
16
|
+
lines = 1,
|
|
17
|
+
fontSize = 14,
|
|
18
|
+
lineHeight,
|
|
19
|
+
width = '100%',
|
|
20
|
+
lastLineWidth = '60%',
|
|
21
|
+
style,
|
|
22
|
+
animation = 'shimmer',
|
|
23
|
+
baseColor,
|
|
24
|
+
highlightColor,
|
|
25
|
+
duration,
|
|
26
|
+
}: TextProps & {
|
|
27
|
+
animation?: AnimationType;
|
|
28
|
+
baseColor?: string;
|
|
29
|
+
highlightColor?: string;
|
|
30
|
+
duration?: number;
|
|
31
|
+
}) {
|
|
32
|
+
const calculatedLineHeight = lineHeight || fontSize * 1.4;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View style={style}>
|
|
36
|
+
{Array.from({ length: lines }).map((_, index) => {
|
|
37
|
+
const isLastLine = index === lines - 1;
|
|
38
|
+
const lineWidth = isLastLine && lines > 1 ? lastLineWidth : width;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<AnimationWrapper
|
|
42
|
+
key={index}
|
|
43
|
+
animation={animation}
|
|
44
|
+
width={lineWidth}
|
|
45
|
+
height={fontSize}
|
|
46
|
+
borderRadius={4}
|
|
47
|
+
baseColor={baseColor}
|
|
48
|
+
highlightColor={highlightColor}
|
|
49
|
+
duration={duration}
|
|
50
|
+
style={[
|
|
51
|
+
styles.line,
|
|
52
|
+
{
|
|
53
|
+
marginBottom: index < lines - 1 ? calculatedLineHeight - fontSize : 0,
|
|
54
|
+
},
|
|
55
|
+
]}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
})}
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const styles = StyleSheet.create({
|
|
64
|
+
line: {
|
|
65
|
+
overflow: 'hidden',
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
SkeletonText.displayName = 'Skeleton.Text';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ComponentType } from 'react';
|
|
3
|
+
import type { SkeletonConfig } from '../types';
|
|
4
|
+
import type { StyleInput } from '../core/generator/StyleSkeleton';
|
|
5
|
+
import { Skeleton } from './Skeleton';
|
|
6
|
+
|
|
7
|
+
export interface WithSkeletonOptions extends Partial<SkeletonConfig> {
|
|
8
|
+
/**
|
|
9
|
+
* Styles used to generate the skeleton while loading. Required when the
|
|
10
|
+
* wrapped component is opaque (a typical screen/component), since Skelo can't
|
|
11
|
+
* introspect its internals.
|
|
12
|
+
*/
|
|
13
|
+
styles?: StyleInput;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Style keys to skip when generating the skeleton (e.g. layout containers).
|
|
17
|
+
*/
|
|
18
|
+
excludeStyles?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Higher-order component that adds a loading skeleton to any component.
|
|
23
|
+
*
|
|
24
|
+
* The returned component accepts a `loading` prop (plus the wrapped
|
|
25
|
+
* component's own props). While `loading` is true it renders a skeleton
|
|
26
|
+
* derived from `options.styles`; otherwise it renders the real component.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* const Home = withSkeleton(HomeScreen, { styles });
|
|
31
|
+
*
|
|
32
|
+
* <Home loading={isLoading} user={user} />
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function withSkeleton<P extends object>(
|
|
36
|
+
Component: ComponentType<P>,
|
|
37
|
+
options: WithSkeletonOptions = {}
|
|
38
|
+
) {
|
|
39
|
+
const WithSkeleton = (props: P & { loading: boolean }) => {
|
|
40
|
+
const { loading, ...componentProps } = props;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Skeleton
|
|
44
|
+
loading={loading}
|
|
45
|
+
styles={options.styles}
|
|
46
|
+
excludeStyles={options.excludeStyles}
|
|
47
|
+
animation={options.animation}
|
|
48
|
+
duration={options.duration}
|
|
49
|
+
baseColor={options.baseColor}
|
|
50
|
+
highlightColor={options.highlightColor}
|
|
51
|
+
borderRadius={options.borderRadius}
|
|
52
|
+
>
|
|
53
|
+
<Component {...(componentProps as unknown as P)} />
|
|
54
|
+
</Skeleton>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const name = Component.displayName || Component.name || 'Component';
|
|
59
|
+
WithSkeleton.displayName = `withSkeleton(${name})`;
|
|
60
|
+
|
|
61
|
+
return WithSkeleton;
|
|
62
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { SkeletonConfig } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default skeleton configuration
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_CONFIG: Required<SkeletonConfig> = {
|
|
7
|
+
animation: 'shimmer',
|
|
8
|
+
duration: 1200,
|
|
9
|
+
baseColor: '#E1E9EE',
|
|
10
|
+
highlightColor: '#F0F4F8',
|
|
11
|
+
borderRadius: 4,
|
|
12
|
+
debug: false,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Dark mode colors
|
|
17
|
+
*/
|
|
18
|
+
export const DARK_MODE_COLORS = {
|
|
19
|
+
baseColor: '#2A3F4F',
|
|
20
|
+
highlightColor: '#3A5364',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default dimensions for components without explicit sizing
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_DIMENSIONS = {
|
|
27
|
+
image: {
|
|
28
|
+
width: 100,
|
|
29
|
+
height: 100,
|
|
30
|
+
},
|
|
31
|
+
text: {
|
|
32
|
+
fontSize: 14,
|
|
33
|
+
lineHeight: 20,
|
|
34
|
+
},
|
|
35
|
+
box: {
|
|
36
|
+
height: 50,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import type { ComponentNode } from '../types';
|
|
3
|
+
|
|
4
|
+
// Deep expansion is opt-in so the core bundle never depends on react-reconciler.
|
|
5
|
+
// `react-native-skelo/deep` registers the expander; Skeleton reads it here.
|
|
6
|
+
type DeepExpander = (element: ReactElement) => ComponentNode[] | null;
|
|
7
|
+
|
|
8
|
+
let expander: DeepExpander | null = null;
|
|
9
|
+
|
|
10
|
+
export function setDeepExpander(fn: DeepExpander): void {
|
|
11
|
+
expander = fn;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getDeepExpander(): DeepExpander | null {
|
|
15
|
+
return expander;
|
|
16
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import type { DimensionValue } from 'react-native';
|
|
4
|
+
import type { SkeletonConfig } from '../../types';
|
|
5
|
+
import { SkeletonBox, SkeletonText, SkeletonCircle } from '../../components/primitives';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A single named style, or an ordered list of styles, or a whole StyleSheet
|
|
9
|
+
* object as returned by `StyleSheet.create`.
|
|
10
|
+
*/
|
|
11
|
+
export type StyleInput = Record<string, unknown> | unknown[];
|
|
12
|
+
|
|
13
|
+
interface StyleSkeletonProps {
|
|
14
|
+
/**
|
|
15
|
+
* The styles to derive a skeleton from. Accepts a `StyleSheet.create` object
|
|
16
|
+
* (values used in definition order) or an array of styles (used in order).
|
|
17
|
+
*/
|
|
18
|
+
styles: StyleInput;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Skeleton configuration (animation, colors, radius).
|
|
22
|
+
*/
|
|
23
|
+
config: SkeletonConfig;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional list of style keys to skip (e.g. layout containers you don't want
|
|
27
|
+
* a skeleton for). Only applies when `styles` is a keyed object.
|
|
28
|
+
*/
|
|
29
|
+
exclude?: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface FlatStyle {
|
|
33
|
+
width?: number | string;
|
|
34
|
+
height?: number | string;
|
|
35
|
+
borderRadius?: number;
|
|
36
|
+
fontSize?: number;
|
|
37
|
+
lineHeight?: number;
|
|
38
|
+
marginTop?: number;
|
|
39
|
+
marginBottom?: number;
|
|
40
|
+
aspectRatio?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Turn one flattened style into the most appropriate skeleton primitive.
|
|
45
|
+
*
|
|
46
|
+
* Heuristics:
|
|
47
|
+
* - a `fontSize` with no explicit height → text bar
|
|
48
|
+
* - square with radius ≥ half its size → circle (avatar)
|
|
49
|
+
* - anything with width and/or height → box
|
|
50
|
+
* - a pure layout container (no visual dimensions) → skipped (returns null)
|
|
51
|
+
*/
|
|
52
|
+
function styleToSkeleton(
|
|
53
|
+
rawStyle: unknown,
|
|
54
|
+
key: string | number,
|
|
55
|
+
config: SkeletonConfig
|
|
56
|
+
): React.ReactElement | null {
|
|
57
|
+
const flat = (StyleSheet.flatten(rawStyle as never) || {}) as FlatStyle;
|
|
58
|
+
const { width, height, borderRadius, fontSize } = flat;
|
|
59
|
+
|
|
60
|
+
const anim = {
|
|
61
|
+
animation: config.animation,
|
|
62
|
+
baseColor: config.baseColor,
|
|
63
|
+
highlightColor: config.highlightColor,
|
|
64
|
+
duration: config.duration,
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
const spacing = {
|
|
68
|
+
marginTop: flat.marginTop ?? 0,
|
|
69
|
+
marginBottom: flat.marginBottom ?? 12,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Text-like: has a font size but isn't a fixed-height box.
|
|
73
|
+
if (fontSize != null && height == null) {
|
|
74
|
+
return (
|
|
75
|
+
<SkeletonText
|
|
76
|
+
key={key}
|
|
77
|
+
lines={1}
|
|
78
|
+
fontSize={fontSize}
|
|
79
|
+
lineHeight={flat.lineHeight}
|
|
80
|
+
width={(width as DimensionValue) ?? '60%'}
|
|
81
|
+
style={spacing}
|
|
82
|
+
{...anim}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Circle / avatar: square with a radius that rounds it fully.
|
|
88
|
+
if (
|
|
89
|
+
typeof width === 'number' &&
|
|
90
|
+
width === height &&
|
|
91
|
+
typeof borderRadius === 'number' &&
|
|
92
|
+
borderRadius >= width / 2
|
|
93
|
+
) {
|
|
94
|
+
return <SkeletonCircle key={key} size={width} style={spacing} {...anim} />;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Box: anything with an explicit dimension.
|
|
98
|
+
if (width != null || height != null) {
|
|
99
|
+
return (
|
|
100
|
+
<SkeletonBox
|
|
101
|
+
key={key}
|
|
102
|
+
width={(width as DimensionValue) ?? '100%'}
|
|
103
|
+
height={(height as DimensionValue) ?? 20}
|
|
104
|
+
borderRadius={borderRadius ?? config.borderRadius ?? 4}
|
|
105
|
+
style={spacing}
|
|
106
|
+
{...anim}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Pure layout container (no visual footprint) — nothing to draw.
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generates a skeleton directly from a set of styles.
|
|
117
|
+
*
|
|
118
|
+
* Because a StyleSheet is flat (no element hierarchy), the output is a vertical
|
|
119
|
+
* stack of skeleton shapes — one per style that has visual dimensions — sized
|
|
120
|
+
* from each style's own props. Layout-only styles are skipped.
|
|
121
|
+
*/
|
|
122
|
+
export function StyleSkeleton({ styles, config, exclude }: StyleSkeletonProps) {
|
|
123
|
+
const entries: Array<[string | number, unknown]> = Array.isArray(styles)
|
|
124
|
+
? styles.map((s, i) => [i, s])
|
|
125
|
+
: Object.entries(styles || {}).filter(([name]) => !exclude || !exclude.includes(name));
|
|
126
|
+
|
|
127
|
+
const nodes = entries
|
|
128
|
+
.map(([key, style]) => styleToSkeleton(style, key, config))
|
|
129
|
+
.filter((n): n is React.ReactElement => n !== null);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<View accessibilityRole="progressbar" accessible>
|
|
133
|
+
{nodes}
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
StyleSkeleton.displayName = 'Skeleton.FromStyles';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { isValidElement } from 'react';
|
|
2
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
3
|
+
import type { ComponentNode } from '../../types';
|
|
4
|
+
import { StyleResolver } from '../resolver';
|
|
5
|
+
import { getComponentName, flattenChildren } from '../../utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parser class for traversing and analyzing React component trees
|
|
9
|
+
*/
|
|
10
|
+
export class Parser {
|
|
11
|
+
/**
|
|
12
|
+
* Parse a React element tree into a component node tree
|
|
13
|
+
*
|
|
14
|
+
* @param children - React children to parse
|
|
15
|
+
* @returns Array of component nodes
|
|
16
|
+
*/
|
|
17
|
+
static parse(children: ReactNode): ComponentNode[] {
|
|
18
|
+
const flattened = flattenChildren(children);
|
|
19
|
+
const nodes: ComponentNode[] = [];
|
|
20
|
+
|
|
21
|
+
flattened.forEach(child => {
|
|
22
|
+
if (!isValidElement(child)) {
|
|
23
|
+
// Skip non-element children (strings, numbers, null, etc.)
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const node = this.parseElement(child);
|
|
28
|
+
if (node) {
|
|
29
|
+
nodes.push(node);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return nodes;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse a single React element
|
|
38
|
+
*
|
|
39
|
+
* @param element - React element
|
|
40
|
+
* @returns Component node or null
|
|
41
|
+
*/
|
|
42
|
+
static parseElement(element: ReactElement): ComponentNode | null {
|
|
43
|
+
const type = getComponentName(element);
|
|
44
|
+
const props = element.props || {};
|
|
45
|
+
const style = StyleResolver.resolve(element);
|
|
46
|
+
|
|
47
|
+
// Parse children recursively
|
|
48
|
+
const children = props.children ? this.parse(props.children) : [];
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
type,
|
|
52
|
+
props,
|
|
53
|
+
style,
|
|
54
|
+
children,
|
|
55
|
+
element,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Estimate number of text lines from content and style
|
|
61
|
+
*
|
|
62
|
+
* @param children - Text content
|
|
63
|
+
* @param style - Resolved style
|
|
64
|
+
* @returns Estimated number of lines
|
|
65
|
+
*/
|
|
66
|
+
static estimateTextLines(children: ReactNode, _style?: any): number {
|
|
67
|
+
if (!children) {
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Convert children to string
|
|
72
|
+
const text = React.Children.toArray(children)
|
|
73
|
+
.filter(child => typeof child === 'string' || typeof child === 'number')
|
|
74
|
+
.join('');
|
|
75
|
+
|
|
76
|
+
if (!text) {
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Simple heuristic: ~40 characters per line
|
|
81
|
+
const avgCharsPerLine = 40;
|
|
82
|
+
const estimatedLines = Math.ceil(text.length / avgCharsPerLine);
|
|
83
|
+
|
|
84
|
+
return Math.max(1, Math.min(estimatedLines, 5)); // Cap at 5 lines
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import type { ComponentNode, ResolvedStyle } from '../../types';
|
|
3
|
+
import { resolveStyle } from '../../utils/styleUtils';
|
|
4
|
+
import { renderToHostTree } from './hostTreeReconciler';
|
|
5
|
+
import type { HostTreeNode } from './hostTreeReconciler';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Offline structural expansion.
|
|
9
|
+
*
|
|
10
|
+
* Renders an element (including opaque function/class components) through a
|
|
11
|
+
* JS-only `react-reconciler` renderer to recover its underlying host-element
|
|
12
|
+
* tree (View / Text / Image + resolved styles), then converts it to Skelo's
|
|
13
|
+
* `ComponentNode` tree.
|
|
14
|
+
*
|
|
15
|
+
* This is what lets `<Skeleton><AnyComponent/></Skeleton>` produce a *structured*
|
|
16
|
+
* skeleton without the developer inlining host elements — the core DX. No
|
|
17
|
+
* deprecated APIs, no `act()`.
|
|
18
|
+
*
|
|
19
|
+
* Caveats (inherent to rendering the real component):
|
|
20
|
+
* - The component's mount effects run during expansion. Effectful screens
|
|
21
|
+
* should be guarded or given an explicit skeleton via `Skelo.register`.
|
|
22
|
+
* - It renders outside the app's providers, so context-dependent components may
|
|
23
|
+
* throw — caught, returning null so the caller falls back to static parsing.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize a host instance type to a canonical name the renderer understands.
|
|
28
|
+
* On device, RN host components may surface as native names (e.g. RCTView,
|
|
29
|
+
* RCTSinglelineTextInputView); map them to View/Text/Image/etc.
|
|
30
|
+
*/
|
|
31
|
+
function normalizeType(type: string): string {
|
|
32
|
+
if (/image/i.test(type)) return 'Image';
|
|
33
|
+
if (/textinput/i.test(type)) return 'TextInput';
|
|
34
|
+
if (/text/i.test(type)) return 'Text';
|
|
35
|
+
if (/scroll/i.test(type)) return 'ScrollView';
|
|
36
|
+
if (/(flat|section|virtualized)list/i.test(type)) return 'FlatList';
|
|
37
|
+
// Everything else is treated as a plain container view.
|
|
38
|
+
return 'View';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toComponentNode(node: HostTreeNode): ComponentNode | null {
|
|
42
|
+
if (node.type === '#text') {
|
|
43
|
+
return null; // text is folded into its parent's props.children
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const textParts: string[] = [];
|
|
47
|
+
const elementChildren: HostTreeNode[] = [];
|
|
48
|
+
for (const child of node.children) {
|
|
49
|
+
if (child.type === '#text') {
|
|
50
|
+
if (child.text) textParts.push(child.text);
|
|
51
|
+
} else {
|
|
52
|
+
elementChildren.push(child);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const props: Record<string, unknown> = { ...node.props };
|
|
57
|
+
if (textParts.length > 0 && props.children == null) {
|
|
58
|
+
props.children = textParts.join('');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const style: ResolvedStyle = resolveStyle(node.props.style);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
type: normalizeType(node.type),
|
|
65
|
+
props,
|
|
66
|
+
style,
|
|
67
|
+
children: elementChildren.map(toComponentNode).filter((n): n is ComponentNode => n !== null),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Expand an element to a `ComponentNode[]` via offline rendering.
|
|
73
|
+
*
|
|
74
|
+
* @returns the host-tree nodes, or `null` if offline rendering is unavailable
|
|
75
|
+
* (react-reconciler not installed) or the render failed.
|
|
76
|
+
*/
|
|
77
|
+
export function expandToComponentNodes(element: ReactElement): ComponentNode[] | null {
|
|
78
|
+
const tree = renderToHostTree(element);
|
|
79
|
+
if (!tree) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return tree.map(toComponentNode).filter((n): n is ComponentNode => n !== null);
|
|
83
|
+
}
|