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.
Files changed (269) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -20
  3. package/lib/commonjs/Skelo.js +104 -0
  4. package/lib/commonjs/Skelo.js.map +1 -0
  5. package/lib/commonjs/animations/AnimationWrapper.js +76 -0
  6. package/lib/commonjs/animations/AnimationWrapper.js.map +1 -0
  7. package/lib/commonjs/animations/PulseAnimation.js +52 -0
  8. package/lib/commonjs/animations/PulseAnimation.js.map +1 -0
  9. package/lib/commonjs/animations/ShimmerAnimation.js +73 -0
  10. package/lib/commonjs/animations/ShimmerAnimation.js.map +1 -0
  11. package/lib/commonjs/animations/index.js +27 -0
  12. package/lib/commonjs/animations/index.js.map +1 -0
  13. package/lib/commonjs/components/MeasuredBlock.js +64 -0
  14. package/lib/commonjs/components/MeasuredBlock.js.map +1 -0
  15. package/lib/commonjs/components/Skeleton.js +144 -0
  16. package/lib/commonjs/components/Skeleton.js.map +1 -0
  17. package/lib/commonjs/components/SkeletonIgnore.js +30 -0
  18. package/lib/commonjs/components/SkeletonIgnore.js.map +1 -0
  19. package/lib/commonjs/components/SkeletonRenderer.js +270 -0
  20. package/lib/commonjs/components/SkeletonRenderer.js.map +1 -0
  21. package/lib/commonjs/components/primitives/SkeletonBox.js +47 -0
  22. package/lib/commonjs/components/primitives/SkeletonBox.js.map +1 -0
  23. package/lib/commonjs/components/primitives/SkeletonCircle.js +44 -0
  24. package/lib/commonjs/components/primitives/SkeletonCircle.js.map +1 -0
  25. package/lib/commonjs/components/primitives/SkeletonImage.js +46 -0
  26. package/lib/commonjs/components/primitives/SkeletonImage.js.map +1 -0
  27. package/lib/commonjs/components/primitives/SkeletonText.js +60 -0
  28. package/lib/commonjs/components/primitives/SkeletonText.js.map +1 -0
  29. package/lib/commonjs/components/primitives/index.js +34 -0
  30. package/lib/commonjs/components/primitives/index.js.map +1 -0
  31. package/lib/commonjs/components/withSkeleton.js +45 -0
  32. package/lib/commonjs/components/withSkeleton.js.map +1 -0
  33. package/lib/commonjs/constants/defaults.js +43 -0
  34. package/lib/commonjs/constants/defaults.js.map +1 -0
  35. package/lib/commonjs/core/deepRegistry.js +18 -0
  36. package/lib/commonjs/core/deepRegistry.js.map +1 -0
  37. package/lib/commonjs/core/generator/StyleSkeleton.js +101 -0
  38. package/lib/commonjs/core/generator/StyleSkeleton.js.map +1 -0
  39. package/lib/commonjs/core/parser/Parser.js +84 -0
  40. package/lib/commonjs/core/parser/Parser.js.map +1 -0
  41. package/lib/commonjs/core/parser/expandOffline.js +83 -0
  42. package/lib/commonjs/core/parser/expandOffline.js.map +1 -0
  43. package/lib/commonjs/core/parser/hostTreeReconciler.js +228 -0
  44. package/lib/commonjs/core/parser/hostTreeReconciler.js.map +1 -0
  45. package/lib/commonjs/core/parser/index.js +13 -0
  46. package/lib/commonjs/core/parser/index.js.map +1 -0
  47. package/lib/commonjs/core/registry/SkeletonRegistry.js +113 -0
  48. package/lib/commonjs/core/registry/SkeletonRegistry.js.map +1 -0
  49. package/lib/commonjs/core/registry/index.js +19 -0
  50. package/lib/commonjs/core/registry/index.js.map +1 -0
  51. package/lib/commonjs/core/resolver/StyleResolver.js +83 -0
  52. package/lib/commonjs/core/resolver/StyleResolver.js.map +1 -0
  53. package/lib/commonjs/core/resolver/index.js +13 -0
  54. package/lib/commonjs/core/resolver/index.js.map +1 -0
  55. package/lib/commonjs/deep.js +18 -0
  56. package/lib/commonjs/deep.js.map +1 -0
  57. package/lib/commonjs/index.js +66 -0
  58. package/lib/commonjs/index.js.map +1 -0
  59. package/lib/commonjs/plugins/ImagePlugin.js +60 -0
  60. package/lib/commonjs/plugins/ImagePlugin.js.map +1 -0
  61. package/lib/commonjs/plugins/TextPlugin.js +70 -0
  62. package/lib/commonjs/plugins/TextPlugin.js.map +1 -0
  63. package/lib/commonjs/plugins/ViewPlugin.js +71 -0
  64. package/lib/commonjs/plugins/ViewPlugin.js.map +1 -0
  65. package/lib/commonjs/plugins/index.js +27 -0
  66. package/lib/commonjs/plugins/index.js.map +1 -0
  67. package/lib/commonjs/types/index.js +2 -0
  68. package/lib/commonjs/types/index.js.map +1 -0
  69. package/lib/commonjs/types/plugin.js +6 -0
  70. package/lib/commonjs/types/plugin.js.map +1 -0
  71. package/lib/commonjs/types/skeleton.js +6 -0
  72. package/lib/commonjs/types/skeleton.js.map +1 -0
  73. package/lib/commonjs/utils/componentUtils.js +138 -0
  74. package/lib/commonjs/utils/componentUtils.js.map +1 -0
  75. package/lib/commonjs/utils/index.js +62 -0
  76. package/lib/commonjs/utils/index.js.map +1 -0
  77. package/lib/commonjs/utils/styleUtils.js +76 -0
  78. package/lib/commonjs/utils/styleUtils.js.map +1 -0
  79. package/lib/module/Skelo.js +98 -0
  80. package/lib/module/Skelo.js.map +1 -0
  81. package/lib/module/animations/AnimationWrapper.js +69 -0
  82. package/lib/module/animations/AnimationWrapper.js.map +1 -0
  83. package/lib/module/animations/PulseAnimation.js +45 -0
  84. package/lib/module/animations/PulseAnimation.js.map +1 -0
  85. package/lib/module/animations/ShimmerAnimation.js +66 -0
  86. package/lib/module/animations/ShimmerAnimation.js.map +1 -0
  87. package/lib/module/animations/index.js +4 -0
  88. package/lib/module/animations/index.js.map +1 -0
  89. package/lib/module/components/MeasuredBlock.js +57 -0
  90. package/lib/module/components/MeasuredBlock.js.map +1 -0
  91. package/lib/module/components/Skeleton.js +138 -0
  92. package/lib/module/components/Skeleton.js.map +1 -0
  93. package/lib/module/components/SkeletonIgnore.js +23 -0
  94. package/lib/module/components/SkeletonIgnore.js.map +1 -0
  95. package/lib/module/components/SkeletonRenderer.js +264 -0
  96. package/lib/module/components/SkeletonRenderer.js.map +1 -0
  97. package/lib/module/components/primitives/SkeletonBox.js +40 -0
  98. package/lib/module/components/primitives/SkeletonBox.js.map +1 -0
  99. package/lib/module/components/primitives/SkeletonCircle.js +37 -0
  100. package/lib/module/components/primitives/SkeletonCircle.js.map +1 -0
  101. package/lib/module/components/primitives/SkeletonImage.js +39 -0
  102. package/lib/module/components/primitives/SkeletonImage.js.map +1 -0
  103. package/lib/module/components/primitives/SkeletonText.js +53 -0
  104. package/lib/module/components/primitives/SkeletonText.js.map +1 -0
  105. package/lib/module/components/primitives/index.js +5 -0
  106. package/lib/module/components/primitives/index.js.map +1 -0
  107. package/lib/module/components/withSkeleton.js +38 -0
  108. package/lib/module/components/withSkeleton.js.map +1 -0
  109. package/lib/module/constants/defaults.js +37 -0
  110. package/lib/module/constants/defaults.js.map +1 -0
  111. package/lib/module/core/deepRegistry.js +11 -0
  112. package/lib/module/core/deepRegistry.js.map +1 -0
  113. package/lib/module/core/generator/StyleSkeleton.js +95 -0
  114. package/lib/module/core/generator/StyleSkeleton.js.map +1 -0
  115. package/lib/module/core/parser/Parser.js +77 -0
  116. package/lib/module/core/parser/Parser.js.map +1 -0
  117. package/lib/module/core/parser/expandOffline.js +77 -0
  118. package/lib/module/core/parser/expandOffline.js.map +1 -0
  119. package/lib/module/core/parser/hostTreeReconciler.js +222 -0
  120. package/lib/module/core/parser/hostTreeReconciler.js.map +1 -0
  121. package/lib/module/core/parser/index.js +2 -0
  122. package/lib/module/core/parser/index.js.map +1 -0
  123. package/lib/module/core/registry/SkeletonRegistry.js +106 -0
  124. package/lib/module/core/registry/SkeletonRegistry.js.map +1 -0
  125. package/lib/module/core/registry/index.js +2 -0
  126. package/lib/module/core/registry/index.js.map +1 -0
  127. package/lib/module/core/resolver/StyleResolver.js +77 -0
  128. package/lib/module/core/resolver/StyleResolver.js.map +1 -0
  129. package/lib/module/core/resolver/index.js +2 -0
  130. package/lib/module/core/resolver/index.js.map +1 -0
  131. package/lib/module/deep.js +11 -0
  132. package/lib/module/deep.js.map +1 -0
  133. package/lib/module/index.js +21 -0
  134. package/lib/module/index.js.map +1 -0
  135. package/lib/module/plugins/ImagePlugin.js +53 -0
  136. package/lib/module/plugins/ImagePlugin.js.map +1 -0
  137. package/lib/module/plugins/TextPlugin.js +63 -0
  138. package/lib/module/plugins/TextPlugin.js.map +1 -0
  139. package/lib/module/plugins/ViewPlugin.js +64 -0
  140. package/lib/module/plugins/ViewPlugin.js.map +1 -0
  141. package/lib/module/plugins/index.js +4 -0
  142. package/lib/module/plugins/index.js.map +1 -0
  143. package/lib/module/types/index.js +2 -0
  144. package/lib/module/types/index.js.map +1 -0
  145. package/lib/module/types/plugin.js +2 -0
  146. package/lib/module/types/plugin.js.map +1 -0
  147. package/lib/module/types/skeleton.js +2 -0
  148. package/lib/module/types/skeleton.js.map +1 -0
  149. package/lib/module/utils/componentUtils.js +123 -0
  150. package/lib/module/utils/componentUtils.js.map +1 -0
  151. package/lib/module/utils/index.js +3 -0
  152. package/lib/module/utils/index.js.map +1 -0
  153. package/lib/module/utils/styleUtils.js +66 -0
  154. package/lib/module/utils/styleUtils.js.map +1 -0
  155. package/lib/typescript/Skelo.d.ts +76 -0
  156. package/lib/typescript/Skelo.d.ts.map +1 -0
  157. package/lib/typescript/animations/AnimationWrapper.d.ts +66 -0
  158. package/lib/typescript/animations/AnimationWrapper.d.ts.map +1 -0
  159. package/lib/typescript/animations/PulseAnimation.d.ts +15 -0
  160. package/lib/typescript/animations/PulseAnimation.d.ts.map +1 -0
  161. package/lib/typescript/animations/ShimmerAnimation.d.ts +15 -0
  162. package/lib/typescript/animations/ShimmerAnimation.d.ts.map +1 -0
  163. package/lib/typescript/animations/index.d.ts +5 -0
  164. package/lib/typescript/animations/index.d.ts.map +1 -0
  165. package/lib/typescript/components/MeasuredBlock.d.ts +32 -0
  166. package/lib/typescript/components/MeasuredBlock.d.ts.map +1 -0
  167. package/lib/typescript/components/Skeleton.d.ts +37 -0
  168. package/lib/typescript/components/Skeleton.d.ts.map +1 -0
  169. package/lib/typescript/components/SkeletonIgnore.d.ts +26 -0
  170. package/lib/typescript/components/SkeletonIgnore.d.ts.map +1 -0
  171. package/lib/typescript/components/SkeletonRenderer.d.ts +39 -0
  172. package/lib/typescript/components/SkeletonRenderer.d.ts.map +1 -0
  173. package/lib/typescript/components/primitives/SkeletonBox.d.ts +21 -0
  174. package/lib/typescript/components/primitives/SkeletonBox.d.ts.map +1 -0
  175. package/lib/typescript/components/primitives/SkeletonCircle.d.ts +21 -0
  176. package/lib/typescript/components/primitives/SkeletonCircle.d.ts.map +1 -0
  177. package/lib/typescript/components/primitives/SkeletonImage.d.ts +21 -0
  178. package/lib/typescript/components/primitives/SkeletonImage.d.ts.map +1 -0
  179. package/lib/typescript/components/primitives/SkeletonText.d.ts +21 -0
  180. package/lib/typescript/components/primitives/SkeletonText.d.ts.map +1 -0
  181. package/lib/typescript/components/primitives/index.d.ts +5 -0
  182. package/lib/typescript/components/primitives/index.d.ts.map +1 -0
  183. package/lib/typescript/components/withSkeleton.d.ts +37 -0
  184. package/lib/typescript/components/withSkeleton.d.ts.map +1 -0
  185. package/lib/typescript/constants/defaults.d.ts +29 -0
  186. package/lib/typescript/constants/defaults.d.ts.map +1 -0
  187. package/lib/typescript/core/deepRegistry.d.ts +7 -0
  188. package/lib/typescript/core/deepRegistry.d.ts.map +1 -0
  189. package/lib/typescript/core/generator/StyleSkeleton.d.ts +36 -0
  190. package/lib/typescript/core/generator/StyleSkeleton.d.ts.map +1 -0
  191. package/lib/typescript/core/parser/Parser.d.ts +30 -0
  192. package/lib/typescript/core/parser/Parser.d.ts.map +1 -0
  193. package/lib/typescript/core/parser/expandOffline.d.ts +10 -0
  194. package/lib/typescript/core/parser/expandOffline.d.ts.map +1 -0
  195. package/lib/typescript/core/parser/hostTreeReconciler.d.ts +28 -0
  196. package/lib/typescript/core/parser/hostTreeReconciler.d.ts.map +1 -0
  197. package/lib/typescript/core/parser/index.d.ts +2 -0
  198. package/lib/typescript/core/parser/index.d.ts.map +1 -0
  199. package/lib/typescript/core/registry/SkeletonRegistry.d.ts +57 -0
  200. package/lib/typescript/core/registry/SkeletonRegistry.d.ts.map +1 -0
  201. package/lib/typescript/core/registry/index.d.ts +2 -0
  202. package/lib/typescript/core/registry/index.d.ts.map +1 -0
  203. package/lib/typescript/core/resolver/StyleResolver.d.ts +54 -0
  204. package/lib/typescript/core/resolver/StyleResolver.d.ts.map +1 -0
  205. package/lib/typescript/core/resolver/index.d.ts +2 -0
  206. package/lib/typescript/core/resolver/index.d.ts.map +1 -0
  207. package/lib/typescript/deep.d.ts +2 -0
  208. package/lib/typescript/deep.d.ts.map +1 -0
  209. package/lib/typescript/index.d.ts +17 -0
  210. package/lib/typescript/index.d.ts.map +1 -0
  211. package/lib/typescript/plugins/ImagePlugin.d.ts +11 -0
  212. package/lib/typescript/plugins/ImagePlugin.d.ts.map +1 -0
  213. package/lib/typescript/plugins/TextPlugin.d.ts +12 -0
  214. package/lib/typescript/plugins/TextPlugin.d.ts.map +1 -0
  215. package/lib/typescript/plugins/ViewPlugin.d.ts +11 -0
  216. package/lib/typescript/plugins/ViewPlugin.d.ts.map +1 -0
  217. package/lib/typescript/plugins/index.d.ts +4 -0
  218. package/lib/typescript/plugins/index.d.ts.map +1 -0
  219. package/lib/typescript/types/index.d.ts +3 -0
  220. package/lib/typescript/types/index.d.ts.map +1 -0
  221. package/lib/typescript/types/plugin.d.ts +144 -0
  222. package/lib/typescript/types/plugin.d.ts.map +1 -0
  223. package/lib/typescript/types/skeleton.d.ts +176 -0
  224. package/lib/typescript/types/skeleton.d.ts.map +1 -0
  225. package/lib/typescript/utils/componentUtils.d.ts +40 -0
  226. package/lib/typescript/utils/componentUtils.d.ts.map +1 -0
  227. package/lib/typescript/utils/index.d.ts +3 -0
  228. package/lib/typescript/utils/index.d.ts.map +1 -0
  229. package/lib/typescript/utils/styleUtils.d.ts +41 -0
  230. package/lib/typescript/utils/styleUtils.d.ts.map +1 -0
  231. package/package.json +169 -11
  232. package/src/Skelo.ts +103 -0
  233. package/src/animations/AnimationWrapper.tsx +139 -0
  234. package/src/animations/PulseAnimation.tsx +76 -0
  235. package/src/animations/ShimmerAnimation.tsx +101 -0
  236. package/src/animations/index.ts +4 -0
  237. package/src/components/MeasuredBlock.tsx +85 -0
  238. package/src/components/Skeleton.tsx +149 -0
  239. package/src/components/SkeletonIgnore.tsx +27 -0
  240. package/src/components/SkeletonRenderer.tsx +341 -0
  241. package/src/components/primitives/SkeletonBox.tsx +53 -0
  242. package/src/components/primitives/SkeletonCircle.tsx +48 -0
  243. package/src/components/primitives/SkeletonImage.tsx +50 -0
  244. package/src/components/primitives/SkeletonText.tsx +69 -0
  245. package/src/components/primitives/index.ts +4 -0
  246. package/src/components/withSkeleton.tsx +62 -0
  247. package/src/constants/defaults.ts +38 -0
  248. package/src/core/deepRegistry.ts +16 -0
  249. package/src/core/generator/StyleSkeleton.tsx +138 -0
  250. package/src/core/parser/Parser.ts +86 -0
  251. package/src/core/parser/expandOffline.tsx +83 -0
  252. package/src/core/parser/hostTreeReconciler.tsx +271 -0
  253. package/src/core/parser/index.ts +1 -0
  254. package/src/core/registry/SkeletonRegistry.ts +111 -0
  255. package/src/core/registry/index.ts +1 -0
  256. package/src/core/resolver/StyleResolver.ts +80 -0
  257. package/src/core/resolver/index.ts +1 -0
  258. package/src/deep.ts +12 -0
  259. package/src/index.ts +30 -0
  260. package/src/plugins/ImagePlugin.tsx +65 -0
  261. package/src/plugins/TextPlugin.tsx +71 -0
  262. package/src/plugins/ViewPlugin.tsx +67 -0
  263. package/src/plugins/index.ts +3 -0
  264. package/src/types/index.ts +14 -0
  265. package/src/types/plugin.ts +164 -0
  266. package/src/types/skeleton.ts +197 -0
  267. package/src/utils/componentUtils.ts +166 -0
  268. package/src/utils/index.ts +9 -0
  269. package/src/utils/styleUtils.ts +72 -0
@@ -0,0 +1,85 @@
1
+ import React, { useState } from 'react';
2
+ import type { LayoutChangeEvent } from 'react-native';
3
+ import { StyleSheet, View } from 'react-native';
4
+ import type { ReactNode } from 'react';
5
+ import type { SkeletonConfig } from '../types';
6
+ import { AnimationWrapper } from '../animations';
7
+
8
+ interface MeasuredBlockProps {
9
+ /**
10
+ * The real element to render (hidden) and measure.
11
+ */
12
+ children: ReactNode;
13
+
14
+ /**
15
+ * Skeleton configuration (animation, colors, etc.)
16
+ */
17
+ config: SkeletonConfig;
18
+ }
19
+
20
+ interface Size {
21
+ width: number;
22
+ height: number;
23
+ }
24
+
25
+ /**
26
+ * Renders an opaque (non-host) component hidden so it lays out at its real
27
+ * size, measures that size via `onLayout`, then overlays a shimmer block of
28
+ * exactly those dimensions.
29
+ *
30
+ * This is Skelo's runtime-measurement fallback: when the parser can't
31
+ * introspect a component (e.g. a user-defined function component), we can't
32
+ * know its internal View/Text/Image structure without native view-tree access,
33
+ * but we *can* render it off-screen and reproduce its footprint accurately.
34
+ *
35
+ * The result is a single, correctly-sized skeleton block rather than a
36
+ * misleading fixed-size placeholder.
37
+ */
38
+ export function MeasuredBlock({ children, config }: MeasuredBlockProps) {
39
+ const [size, setSize] = useState<Size | null>(null);
40
+
41
+ const handleLayout = (event: LayoutChangeEvent) => {
42
+ const { width, height } = event.nativeEvent.layout;
43
+ setSize(prev =>
44
+ prev && prev.width === width && prev.height === height ? prev : { width, height }
45
+ );
46
+ };
47
+
48
+ return (
49
+ <View>
50
+ {/* Real content, rendered invisibly purely to occupy space + measure. */}
51
+ <View
52
+ style={styles.hidden}
53
+ pointerEvents="none"
54
+ importantForAccessibility="no-hide-descendants"
55
+ accessibilityElementsHidden
56
+ onLayout={handleLayout}
57
+ >
58
+ {children}
59
+ </View>
60
+
61
+ {/* Size-matched shimmer overlay, shown once measured. */}
62
+ {size && size.width > 0 && size.height > 0 && (
63
+ <View style={StyleSheet.absoluteFill} pointerEvents="none">
64
+ <AnimationWrapper
65
+ animation={config.animation}
66
+ width={size.width}
67
+ height={size.height}
68
+ baseColor={config.baseColor}
69
+ highlightColor={config.highlightColor}
70
+ duration={config.duration}
71
+ borderRadius={config.borderRadius}
72
+ />
73
+ </View>
74
+ )}
75
+ </View>
76
+ );
77
+ }
78
+
79
+ const styles = StyleSheet.create({
80
+ hidden: {
81
+ opacity: 0,
82
+ },
83
+ });
84
+
85
+ MeasuredBlock.displayName = 'Skeleton.MeasuredBlock';
@@ -0,0 +1,149 @@
1
+ import React, { useMemo } from 'react';
2
+ import type { SkeletonProps } from '../types';
3
+ import { Parser } from '../core/parser';
4
+ import { SkeletonRenderer } from './SkeletonRenderer';
5
+ import { SkeletonIgnore } from './SkeletonIgnore';
6
+ import { StyleSkeleton } from '../core/generator/StyleSkeleton';
7
+ import { getDeepExpander } from '../core/deepRegistry';
8
+ import { DEFAULT_CONFIG } from '../constants/defaults';
9
+
10
+ /**
11
+ * Main Skeleton component
12
+ *
13
+ * Automatically generates skeleton loading states for your React Native components.
14
+ * Wraps your UI and shows skeleton placeholders while loading.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * <Skeleton loading={isLoading}>
19
+ * <ProfileScreen />
20
+ * </Skeleton>
21
+ * ```
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * <Skeleton
26
+ * loading={isLoading}
27
+ * animation="pulse"
28
+ * baseColor="#f0f0f0"
29
+ * highlightColor="#ffffff"
30
+ * >
31
+ * <View>
32
+ * <Text>Hello World</Text>
33
+ * <Image source={avatar} />
34
+ * </View>
35
+ * </Skeleton>
36
+ * ```
37
+ */
38
+ export function Skeleton({
39
+ loading,
40
+ children,
41
+ styles,
42
+ excludeStyles,
43
+ deep = false,
44
+ animation = DEFAULT_CONFIG.animation,
45
+ duration = DEFAULT_CONFIG.duration,
46
+ baseColor = DEFAULT_CONFIG.baseColor,
47
+ highlightColor = DEFAULT_CONFIG.highlightColor,
48
+ borderRadius = DEFAULT_CONFIG.borderRadius,
49
+ accessible = true,
50
+ accessibilityLabel = 'Loading content',
51
+ debug = false,
52
+ }: SkeletonProps) {
53
+ // Parse component tree and generate skeleton.
54
+ // Hooks must run unconditionally (rules of hooks), but parsing stays lazy:
55
+ // we skip the work entirely when not loading or in styles-driven mode.
56
+ const skeletonTree = useMemo(() => {
57
+ if (!loading || styles) {
58
+ return [];
59
+ }
60
+
61
+ if (__DEV__ && debug) {
62
+ console.log('[Skelo] Parsing component tree...');
63
+ }
64
+
65
+ try {
66
+ // Deep mode: expand opaque components into their real host tree via the
67
+ // 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
+ );
83
+ }
84
+ }
85
+
86
+ const parsed = Parser.parse(children);
87
+
88
+ if (__DEV__ && debug) {
89
+ // Log a lightweight summary: the raw nodes hold React elements
90
+ // (`element`) and arbitrary props that aren't safely serializable.
91
+ const summarize = (nodes: typeof parsed): unknown =>
92
+ nodes.map(node => ({
93
+ type: node.type,
94
+ style: node.style,
95
+ children: summarize(node.children),
96
+ }));
97
+ console.log('[Skelo] Parsed tree:', JSON.stringify(summarize(parsed), null, 2));
98
+ }
99
+
100
+ return parsed;
101
+ } catch (error) {
102
+ if (__DEV__) {
103
+ console.error('[Skelo] Failed to parse component tree:', error);
104
+ }
105
+ return [];
106
+ }
107
+ }, [loading, styles, deep, children, debug]);
108
+
109
+ // Create skeleton config
110
+ const config = useMemo(
111
+ () => ({
112
+ animation,
113
+ duration,
114
+ baseColor,
115
+ highlightColor,
116
+ borderRadius,
117
+ }),
118
+ [animation, duration, baseColor, highlightColor, borderRadius]
119
+ );
120
+
121
+ // If not loading, render the real children directly.
122
+ if (!loading) {
123
+ return <>{children}</>;
124
+ }
125
+
126
+ // Styles-driven mode: derive the skeleton from the provided styles instead of
127
+ // introspecting the child tree (useful when children are opaque components).
128
+ if (styles) {
129
+ return <StyleSkeleton styles={styles} config={config} exclude={excludeStyles} />;
130
+ }
131
+
132
+ return (
133
+ <SkeletonRenderer
134
+ tree={skeletonTree}
135
+ config={config}
136
+ accessible={accessible}
137
+ accessibilityLabel={accessibilityLabel}
138
+ debug={debug}
139
+ />
140
+ );
141
+ }
142
+
143
+ Skeleton.displayName = 'Skeleton';
144
+
145
+ /**
146
+ * Escape hatch to keep children as real content inside a loading `<Skeleton>`.
147
+ * @see SkeletonIgnore
148
+ */
149
+ Skeleton.Ignore = SkeletonIgnore;
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import type { ReactNode } from 'react';
3
+
4
+ interface SkeletonIgnoreProps {
5
+ children?: ReactNode;
6
+ }
7
+
8
+ /**
9
+ * Escape hatch that renders its children as real content, even inside a
10
+ * loading `<Skeleton>`. Use it for elements that should stay visible during
11
+ * loading — section headings, static labels, action buttons, etc.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <Skeleton loading={loading}>
16
+ * <Skeleton.Ignore>
17
+ * <Text>Profile</Text> // stays as real text
18
+ * </Skeleton.Ignore>
19
+ * <ProfileCard /> // becomes a skeleton
20
+ * </Skeleton>
21
+ * ```
22
+ */
23
+ export function SkeletonIgnore({ children }: SkeletonIgnoreProps) {
24
+ return <>{children}</>;
25
+ }
26
+
27
+ SkeletonIgnore.displayName = 'Skeleton.Ignore';
@@ -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';