react-native-skelo 0.0.1 → 0.0.2
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 +147 -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 +76 -0
- package/lib/commonjs/animations/PulseAnimation.js.map +1 -0
- package/lib/commonjs/animations/ShimmerAnimation.js +103 -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 +69 -0
- package/lib/module/animations/PulseAnimation.js.map +1 -0
- package/lib/module/animations/ShimmerAnimation.js +96 -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 +59 -0
- package/lib/typescript/animations/PulseAnimation.d.ts.map +1 -0
- package/lib/typescript/animations/ShimmerAnimation.d.ts +59 -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 +171 -11
- package/src/Skelo.ts +103 -0
- package/src/animations/AnimationWrapper.tsx +139 -0
- package/src/animations/PulseAnimation.tsx +138 -0
- package/src/animations/ShimmerAnimation.tsx +169 -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,341 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import type { ComponentNode, SkeletonConfig } from '../types';
|
|
4
|
+
import { registry } from '../core/registry';
|
|
5
|
+
import { SkeletonBox, SkeletonText, SkeletonImage, SkeletonCircle } from './primitives';
|
|
6
|
+
import { MeasuredBlock } from './MeasuredBlock';
|
|
7
|
+
import { resolveStyle, extractDimensions } from '../utils/styleUtils';
|
|
8
|
+
import { randomWidth } from '../utils/componentUtils';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Host component types the renderer knows how to skeletonize directly.
|
|
12
|
+
* Anything else is treated as an opaque component and measured at runtime.
|
|
13
|
+
*/
|
|
14
|
+
const HOST_CONTAINER_TYPES = new Set([
|
|
15
|
+
'View',
|
|
16
|
+
'ScrollView',
|
|
17
|
+
'SafeAreaView',
|
|
18
|
+
'KeyboardAvoidingView',
|
|
19
|
+
'TouchableOpacity',
|
|
20
|
+
'TouchableHighlight',
|
|
21
|
+
'TouchableWithoutFeedback',
|
|
22
|
+
'TouchableNativeFeedback',
|
|
23
|
+
'Pressable',
|
|
24
|
+
'Fragment',
|
|
25
|
+
]);
|
|
26
|
+
const HOST_LIST_TYPES = new Set(['FlatList', 'SectionList', 'VirtualizedList']);
|
|
27
|
+
|
|
28
|
+
// Track which opaque component names we've already warned about (dev only).
|
|
29
|
+
const warnedOpaqueTypes = new Set<string>();
|
|
30
|
+
|
|
31
|
+
interface SkeletonRendererProps {
|
|
32
|
+
/**
|
|
33
|
+
* Parsed component tree
|
|
34
|
+
*/
|
|
35
|
+
tree: ComponentNode[];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Skeleton configuration
|
|
39
|
+
*/
|
|
40
|
+
config: SkeletonConfig;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Accessibility enabled
|
|
44
|
+
*/
|
|
45
|
+
accessible?: boolean;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Accessibility label
|
|
49
|
+
*/
|
|
50
|
+
accessibilityLabel?: string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Debug mode
|
|
54
|
+
*/
|
|
55
|
+
debug?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Renders skeleton from parsed component tree
|
|
60
|
+
*
|
|
61
|
+
* Uses plugins to transform components into skeleton equivalents:
|
|
62
|
+
* - View -> SkeletonBox
|
|
63
|
+
* - Text -> SkeletonText
|
|
64
|
+
* - Image -> SkeletonImage
|
|
65
|
+
* - Custom components -> Plugin strategies
|
|
66
|
+
*/
|
|
67
|
+
export function SkeletonRenderer({
|
|
68
|
+
tree,
|
|
69
|
+
config,
|
|
70
|
+
accessible,
|
|
71
|
+
accessibilityLabel,
|
|
72
|
+
debug,
|
|
73
|
+
}: SkeletonRendererProps) {
|
|
74
|
+
/**
|
|
75
|
+
* Render a single component node
|
|
76
|
+
*/
|
|
77
|
+
const renderNode = (node: ComponentNode, index: number): React.ReactElement | null => {
|
|
78
|
+
if (__DEV__ && debug) {
|
|
79
|
+
console.log(`[Skelo] Rendering node: ${node.type}`, node);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Skeleton.Ignore escape hatch: render the real children untouched.
|
|
83
|
+
if (node.type === 'Skeleton.Ignore') {
|
|
84
|
+
return <React.Fragment key={index}>{node.props?.children}</React.Fragment>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check for registered plugin
|
|
88
|
+
const plugin = registry.getPluginByName(node.type);
|
|
89
|
+
if (plugin) {
|
|
90
|
+
try {
|
|
91
|
+
return (
|
|
92
|
+
<React.Fragment key={index}>
|
|
93
|
+
{plugin.strategy(node.props, {
|
|
94
|
+
config,
|
|
95
|
+
theme: 'light', // TODO: Add theme detection
|
|
96
|
+
parseChildren: children => {
|
|
97
|
+
const parsed =
|
|
98
|
+
typeof children === 'object' && children !== null
|
|
99
|
+
? [children as unknown as ComponentNode]
|
|
100
|
+
: [];
|
|
101
|
+
return parsed.map((child, i) => renderNode(child, i));
|
|
102
|
+
},
|
|
103
|
+
primitives: {
|
|
104
|
+
Box: SkeletonBox,
|
|
105
|
+
Text: SkeletonText,
|
|
106
|
+
Image: SkeletonImage,
|
|
107
|
+
Circle: SkeletonCircle,
|
|
108
|
+
},
|
|
109
|
+
utils: {
|
|
110
|
+
resolveStyle,
|
|
111
|
+
extractDimensions,
|
|
112
|
+
randomWidth,
|
|
113
|
+
},
|
|
114
|
+
})}
|
|
115
|
+
</React.Fragment>
|
|
116
|
+
);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (__DEV__) {
|
|
119
|
+
console.error(`[Skelo] Plugin "${plugin.name}" failed:`, error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Built-in host component handling
|
|
125
|
+
switch (node.type) {
|
|
126
|
+
case 'Text':
|
|
127
|
+
return renderText(node, index);
|
|
128
|
+
|
|
129
|
+
case 'Image':
|
|
130
|
+
return renderImage(node, index);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (HOST_LIST_TYPES.has(node.type) || node.type === 'ScrollView') {
|
|
134
|
+
return renderScrollableView(node, index);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (HOST_CONTAINER_TYPES.has(node.type)) {
|
|
138
|
+
return renderView(node, index);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Opaque component: the parser can't see inside it (a user-defined
|
|
142
|
+
// function/class component). Render it hidden and measure its real size,
|
|
143
|
+
// then overlay a size-matched shimmer.
|
|
144
|
+
return renderOpaque(node, index);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Render an opaque (non-host) component via runtime measurement.
|
|
149
|
+
*/
|
|
150
|
+
const renderOpaque = (node: ComponentNode, index: number): React.ReactElement => {
|
|
151
|
+
if (__DEV__ && !warnedOpaqueTypes.has(node.type)) {
|
|
152
|
+
warnedOpaqueTypes.add(node.type);
|
|
153
|
+
console.warn(
|
|
154
|
+
`[Skelo] "${node.type}" is a custom component, so Skelo can't inspect ` +
|
|
155
|
+
`its internal View/Text/Image structure. It will be measured and ` +
|
|
156
|
+
`shown as a single size-matched skeleton block.\n` +
|
|
157
|
+
`For a structured skeleton, either compose "${node.type}" from host ` +
|
|
158
|
+
`elements directly inside <Skeleton>, register a plugin via ` +
|
|
159
|
+
`Skelo.register(${node.type}, ...), or provide a manual skeleton.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// If we still have the original element, measure it. Otherwise fall back
|
|
164
|
+
// to a container/box render.
|
|
165
|
+
if (node.element) {
|
|
166
|
+
return (
|
|
167
|
+
<MeasuredBlock key={index} config={config}>
|
|
168
|
+
{node.element}
|
|
169
|
+
</MeasuredBlock>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return renderView(node, index);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Render View component
|
|
178
|
+
*/
|
|
179
|
+
const renderView = (node: ComponentNode, index: number): React.ReactElement => {
|
|
180
|
+
const { width, height } = node.style;
|
|
181
|
+
const hasChildren = node.children && node.children.length > 0;
|
|
182
|
+
|
|
183
|
+
// If View has explicit dimensions, render as box
|
|
184
|
+
if (width && height) {
|
|
185
|
+
return (
|
|
186
|
+
<SkeletonBox
|
|
187
|
+
key={index}
|
|
188
|
+
width={width}
|
|
189
|
+
height={height}
|
|
190
|
+
borderRadius={node.style.borderRadius ?? config.borderRadius}
|
|
191
|
+
animation={config.animation}
|
|
192
|
+
baseColor={config.baseColor}
|
|
193
|
+
highlightColor={config.highlightColor}
|
|
194
|
+
duration={config.duration}
|
|
195
|
+
style={node.style}
|
|
196
|
+
/>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// If View has children, render container with children
|
|
201
|
+
if (hasChildren) {
|
|
202
|
+
return (
|
|
203
|
+
<View key={index} style={node.style}>
|
|
204
|
+
{node.children.map((child, i) => renderNode(child, i))}
|
|
205
|
+
</View>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Empty View with no dimensions - render as small box
|
|
210
|
+
return (
|
|
211
|
+
<SkeletonBox
|
|
212
|
+
key={index}
|
|
213
|
+
width={width ?? 100}
|
|
214
|
+
height={height ?? 20}
|
|
215
|
+
borderRadius={node.style.borderRadius ?? config.borderRadius}
|
|
216
|
+
animation={config.animation}
|
|
217
|
+
baseColor={config.baseColor}
|
|
218
|
+
highlightColor={config.highlightColor}
|
|
219
|
+
duration={config.duration}
|
|
220
|
+
style={node.style}
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Render Text component
|
|
227
|
+
*/
|
|
228
|
+
const renderText = (node: ComponentNode, index: number): React.ReactElement => {
|
|
229
|
+
const fontSize = (node.style.fontSize as number) ?? 14;
|
|
230
|
+
const lineHeight = (node.style.lineHeight as number) ?? fontSize * 1.4;
|
|
231
|
+
|
|
232
|
+
// Estimate number of lines from text content
|
|
233
|
+
const textContent = node.props.children;
|
|
234
|
+
const estimatedLines = estimateTextLines(textContent);
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<SkeletonText
|
|
238
|
+
key={index}
|
|
239
|
+
lines={estimatedLines}
|
|
240
|
+
fontSize={fontSize}
|
|
241
|
+
lineHeight={lineHeight}
|
|
242
|
+
width={node.style.width ?? '100%'}
|
|
243
|
+
animation={config.animation}
|
|
244
|
+
baseColor={config.baseColor}
|
|
245
|
+
highlightColor={config.highlightColor}
|
|
246
|
+
duration={config.duration}
|
|
247
|
+
style={node.style}
|
|
248
|
+
/>
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Render Image component
|
|
254
|
+
*/
|
|
255
|
+
const renderImage = (node: ComponentNode, index: number): React.ReactElement => {
|
|
256
|
+
const width = node.style.width ?? 100;
|
|
257
|
+
const height = node.style.height ?? 100;
|
|
258
|
+
const borderRadius = node.style.borderRadius ?? 0;
|
|
259
|
+
|
|
260
|
+
// Check if it's a circular image (avatar)
|
|
261
|
+
const isCircle =
|
|
262
|
+
typeof width === 'number' &&
|
|
263
|
+
typeof height === 'number' &&
|
|
264
|
+
width === height &&
|
|
265
|
+
borderRadius === width / 2;
|
|
266
|
+
|
|
267
|
+
if (isCircle) {
|
|
268
|
+
return (
|
|
269
|
+
<SkeletonCircle
|
|
270
|
+
key={index}
|
|
271
|
+
size={width as number}
|
|
272
|
+
animation={config.animation}
|
|
273
|
+
baseColor={config.baseColor}
|
|
274
|
+
highlightColor={config.highlightColor}
|
|
275
|
+
duration={config.duration}
|
|
276
|
+
style={node.style}
|
|
277
|
+
/>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<SkeletonImage
|
|
283
|
+
key={index}
|
|
284
|
+
width={width}
|
|
285
|
+
height={height}
|
|
286
|
+
borderRadius={borderRadius as number}
|
|
287
|
+
animation={config.animation}
|
|
288
|
+
baseColor={config.baseColor}
|
|
289
|
+
highlightColor={config.highlightColor}
|
|
290
|
+
duration={config.duration}
|
|
291
|
+
style={node.style}
|
|
292
|
+
/>
|
|
293
|
+
);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Render scrollable views
|
|
298
|
+
*/
|
|
299
|
+
const renderScrollableView = (node: ComponentNode, index: number): React.ReactElement => {
|
|
300
|
+
// For scrollable views, render their children
|
|
301
|
+
return (
|
|
302
|
+
<View key={index} style={node.style}>
|
|
303
|
+
{node.children.map((child, i) => renderNode(child, i))}
|
|
304
|
+
</View>
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Estimate number of lines from text content
|
|
310
|
+
*/
|
|
311
|
+
const estimateTextLines = (children: any): number => {
|
|
312
|
+
if (!children) return 1;
|
|
313
|
+
|
|
314
|
+
if (typeof children === 'string' || typeof children === 'number') {
|
|
315
|
+
const text = String(children);
|
|
316
|
+
const avgCharsPerLine = 40;
|
|
317
|
+
const estimatedLines = Math.ceil(text.length / avgCharsPerLine);
|
|
318
|
+
return Math.max(1, Math.min(estimatedLines, 5)); // Cap at 5 lines
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (Array.isArray(children)) {
|
|
322
|
+
// Multiple text nodes - count as separate lines
|
|
323
|
+
return Math.min(children.length, 5);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return 1;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Render the skeleton tree
|
|
330
|
+
return (
|
|
331
|
+
<View
|
|
332
|
+
accessible={accessible}
|
|
333
|
+
accessibilityLabel={accessibilityLabel}
|
|
334
|
+
accessibilityRole="progressbar"
|
|
335
|
+
>
|
|
336
|
+
{tree.map((node, index) => renderNode(node, index))}
|
|
337
|
+
</View>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
SkeletonRenderer.displayName = 'SkeletonRenderer';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import type { BoxProps } from '../../types';
|
|
4
|
+
import { AnimationWrapper } from '../../animations';
|
|
5
|
+
import type { AnimationType } from '../../animations';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Basic box skeleton primitive
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <SkeletonBox width={100} height={50} borderRadius={8} />
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function SkeletonBox({
|
|
16
|
+
width,
|
|
17
|
+
height,
|
|
18
|
+
borderRadius = 4,
|
|
19
|
+
style,
|
|
20
|
+
children,
|
|
21
|
+
animation = 'shimmer',
|
|
22
|
+
baseColor,
|
|
23
|
+
highlightColor,
|
|
24
|
+
duration,
|
|
25
|
+
}: BoxProps & {
|
|
26
|
+
animation?: AnimationType;
|
|
27
|
+
baseColor?: string;
|
|
28
|
+
highlightColor?: string;
|
|
29
|
+
duration?: number;
|
|
30
|
+
}) {
|
|
31
|
+
return (
|
|
32
|
+
<AnimationWrapper
|
|
33
|
+
animation={animation}
|
|
34
|
+
width={width}
|
|
35
|
+
height={height}
|
|
36
|
+
borderRadius={borderRadius}
|
|
37
|
+
baseColor={baseColor}
|
|
38
|
+
highlightColor={highlightColor}
|
|
39
|
+
duration={duration}
|
|
40
|
+
style={[styles.box, style]}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</AnimationWrapper>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const styles = StyleSheet.create({
|
|
48
|
+
box: {
|
|
49
|
+
overflow: 'hidden',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
SkeletonBox.displayName = 'Skeleton.Box';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import type { CircleProps } from '../../types';
|
|
4
|
+
import { AnimationWrapper } from '../../animations';
|
|
5
|
+
import type { AnimationType } from '../../animations';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Circle skeleton primitive (useful for avatars)
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <SkeletonCircle size={40} />
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function SkeletonCircle({
|
|
16
|
+
size = 40,
|
|
17
|
+
style,
|
|
18
|
+
animation = 'shimmer',
|
|
19
|
+
baseColor,
|
|
20
|
+
highlightColor,
|
|
21
|
+
duration,
|
|
22
|
+
}: CircleProps & {
|
|
23
|
+
animation?: AnimationType;
|
|
24
|
+
baseColor?: string;
|
|
25
|
+
highlightColor?: string;
|
|
26
|
+
duration?: number;
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<AnimationWrapper
|
|
30
|
+
animation={animation}
|
|
31
|
+
width={size}
|
|
32
|
+
height={size}
|
|
33
|
+
borderRadius={size / 2}
|
|
34
|
+
baseColor={baseColor}
|
|
35
|
+
highlightColor={highlightColor}
|
|
36
|
+
duration={duration}
|
|
37
|
+
style={[styles.circle, style]}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const styles = StyleSheet.create({
|
|
43
|
+
circle: {
|
|
44
|
+
overflow: 'hidden',
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
SkeletonCircle.displayName = 'Skeleton.Circle';
|
|
@@ -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
|
+
}
|