react-native-smart-skeleton 0.0.1

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 (173) hide show
  1. package/README.md +255 -0
  2. package/lib/commonjs/animations/breathing.js +53 -0
  3. package/lib/commonjs/animations/breathing.js.map +1 -0
  4. package/lib/commonjs/animations/pulse.js +50 -0
  5. package/lib/commonjs/animations/pulse.js.map +1 -0
  6. package/lib/commonjs/animations/shimmer.js +68 -0
  7. package/lib/commonjs/animations/shimmer.js.map +1 -0
  8. package/lib/commonjs/animations/wave.js +54 -0
  9. package/lib/commonjs/animations/wave.js.map +1 -0
  10. package/lib/commonjs/clone/SkeletonClone.js +91 -0
  11. package/lib/commonjs/clone/SkeletonClone.js.map +1 -0
  12. package/lib/commonjs/components/Circle.js +27 -0
  13. package/lib/commonjs/components/Circle.js.map +1 -0
  14. package/lib/commonjs/components/Group.js +57 -0
  15. package/lib/commonjs/components/Group.js.map +1 -0
  16. package/lib/commonjs/components/Pill.js +32 -0
  17. package/lib/commonjs/components/Pill.js.map +1 -0
  18. package/lib/commonjs/components/Rect.js +21 -0
  19. package/lib/commonjs/components/Rect.js.map +1 -0
  20. package/lib/commonjs/components/Skeleton.js +196 -0
  21. package/lib/commonjs/components/Skeleton.js.map +1 -0
  22. package/lib/commonjs/components/Square.js +28 -0
  23. package/lib/commonjs/components/Square.js.map +1 -0
  24. package/lib/commonjs/demo/demo.gif +0 -0
  25. package/lib/commonjs/hooks/useReducedMotion.js +42 -0
  26. package/lib/commonjs/hooks/useReducedMotion.js.map +1 -0
  27. package/lib/commonjs/hooks/useSkeletonTheme.js +46 -0
  28. package/lib/commonjs/hooks/useSkeletonTheme.js.map +1 -0
  29. package/lib/commonjs/index.js +149 -0
  30. package/lib/commonjs/index.js.map +1 -0
  31. package/lib/commonjs/presets/Card.js +83 -0
  32. package/lib/commonjs/presets/Card.js.map +1 -0
  33. package/lib/commonjs/presets/Chat.js +54 -0
  34. package/lib/commonjs/presets/Chat.js.map +1 -0
  35. package/lib/commonjs/presets/Product.js +70 -0
  36. package/lib/commonjs/presets/Product.js.map +1 -0
  37. package/lib/commonjs/presets/Profile.js +67 -0
  38. package/lib/commonjs/presets/Profile.js.map +1 -0
  39. package/lib/commonjs/presets/Video.js +76 -0
  40. package/lib/commonjs/presets/Video.js.map +1 -0
  41. package/lib/commonjs/providers/SkeletonProvider.js +70 -0
  42. package/lib/commonjs/providers/SkeletonProvider.js.map +1 -0
  43. package/lib/commonjs/types/index.js +6 -0
  44. package/lib/commonjs/types/index.js.map +1 -0
  45. package/lib/commonjs/utils/colors.js +144 -0
  46. package/lib/commonjs/utils/colors.js.map +1 -0
  47. package/lib/commonjs/utils/responsive.js +84 -0
  48. package/lib/commonjs/utils/responsive.js.map +1 -0
  49. package/lib/commonjs/utils/theme.js +39 -0
  50. package/lib/commonjs/utils/theme.js.map +1 -0
  51. package/lib/module/animations/breathing.js +47 -0
  52. package/lib/module/animations/breathing.js.map +1 -0
  53. package/lib/module/animations/pulse.js +44 -0
  54. package/lib/module/animations/pulse.js.map +1 -0
  55. package/lib/module/animations/shimmer.js +61 -0
  56. package/lib/module/animations/shimmer.js.map +1 -0
  57. package/lib/module/animations/wave.js +48 -0
  58. package/lib/module/animations/wave.js.map +1 -0
  59. package/lib/module/clone/SkeletonClone.js +84 -0
  60. package/lib/module/clone/SkeletonClone.js.map +1 -0
  61. package/lib/module/components/Circle.js +20 -0
  62. package/lib/module/components/Circle.js.map +1 -0
  63. package/lib/module/components/Group.js +50 -0
  64. package/lib/module/components/Group.js.map +1 -0
  65. package/lib/module/components/Pill.js +25 -0
  66. package/lib/module/components/Pill.js.map +1 -0
  67. package/lib/module/components/Rect.js +14 -0
  68. package/lib/module/components/Rect.js.map +1 -0
  69. package/lib/module/components/Skeleton.js +188 -0
  70. package/lib/module/components/Skeleton.js.map +1 -0
  71. package/lib/module/components/Square.js +21 -0
  72. package/lib/module/components/Square.js.map +1 -0
  73. package/lib/module/demo/demo.gif +0 -0
  74. package/lib/module/hooks/useReducedMotion.js +37 -0
  75. package/lib/module/hooks/useReducedMotion.js.map +1 -0
  76. package/lib/module/hooks/useSkeletonTheme.js +40 -0
  77. package/lib/module/hooks/useSkeletonTheme.js.map +1 -0
  78. package/lib/module/index.js +50 -0
  79. package/lib/module/index.js.map +1 -0
  80. package/lib/module/presets/Card.js +76 -0
  81. package/lib/module/presets/Card.js.map +1 -0
  82. package/lib/module/presets/Chat.js +47 -0
  83. package/lib/module/presets/Chat.js.map +1 -0
  84. package/lib/module/presets/Product.js +63 -0
  85. package/lib/module/presets/Product.js.map +1 -0
  86. package/lib/module/presets/Profile.js +60 -0
  87. package/lib/module/presets/Profile.js.map +1 -0
  88. package/lib/module/presets/Video.js +69 -0
  89. package/lib/module/presets/Video.js.map +1 -0
  90. package/lib/module/providers/SkeletonProvider.js +61 -0
  91. package/lib/module/providers/SkeletonProvider.js.map +1 -0
  92. package/lib/module/types/index.js +2 -0
  93. package/lib/module/types/index.js.map +1 -0
  94. package/lib/module/utils/colors.js +132 -0
  95. package/lib/module/utils/colors.js.map +1 -0
  96. package/lib/module/utils/responsive.js +74 -0
  97. package/lib/module/utils/responsive.js.map +1 -0
  98. package/lib/module/utils/theme.js +32 -0
  99. package/lib/module/utils/theme.js.map +1 -0
  100. package/lib/typescript/animations/breathing.d.ts +26 -0
  101. package/lib/typescript/animations/breathing.d.ts.map +1 -0
  102. package/lib/typescript/animations/pulse.d.ts +22 -0
  103. package/lib/typescript/animations/pulse.d.ts.map +1 -0
  104. package/lib/typescript/animations/shimmer.d.ts +34 -0
  105. package/lib/typescript/animations/shimmer.d.ts.map +1 -0
  106. package/lib/typescript/animations/wave.d.ts +23 -0
  107. package/lib/typescript/animations/wave.d.ts.map +1 -0
  108. package/lib/typescript/clone/SkeletonClone.d.ts +30 -0
  109. package/lib/typescript/clone/SkeletonClone.d.ts.map +1 -0
  110. package/lib/typescript/components/Circle.d.ts +10 -0
  111. package/lib/typescript/components/Circle.d.ts.map +1 -0
  112. package/lib/typescript/components/Group.d.ts +21 -0
  113. package/lib/typescript/components/Group.d.ts.map +1 -0
  114. package/lib/typescript/components/Pill.d.ts +12 -0
  115. package/lib/typescript/components/Pill.d.ts.map +1 -0
  116. package/lib/typescript/components/Rect.d.ts +12 -0
  117. package/lib/typescript/components/Rect.d.ts.map +1 -0
  118. package/lib/typescript/components/Skeleton.d.ts +16 -0
  119. package/lib/typescript/components/Skeleton.d.ts.map +1 -0
  120. package/lib/typescript/components/Square.d.ts +10 -0
  121. package/lib/typescript/components/Square.d.ts.map +1 -0
  122. package/lib/typescript/hooks/useReducedMotion.d.ts +11 -0
  123. package/lib/typescript/hooks/useReducedMotion.d.ts.map +1 -0
  124. package/lib/typescript/hooks/useSkeletonTheme.d.ts +12 -0
  125. package/lib/typescript/hooks/useSkeletonTheme.d.ts.map +1 -0
  126. package/lib/typescript/index.d.ts +51 -0
  127. package/lib/typescript/index.d.ts.map +1 -0
  128. package/lib/typescript/presets/Card.d.ts +11 -0
  129. package/lib/typescript/presets/Card.d.ts.map +1 -0
  130. package/lib/typescript/presets/Chat.d.ts +12 -0
  131. package/lib/typescript/presets/Chat.d.ts.map +1 -0
  132. package/lib/typescript/presets/Product.d.ts +12 -0
  133. package/lib/typescript/presets/Product.d.ts.map +1 -0
  134. package/lib/typescript/presets/Profile.d.ts +11 -0
  135. package/lib/typescript/presets/Profile.d.ts.map +1 -0
  136. package/lib/typescript/presets/Video.d.ts +12 -0
  137. package/lib/typescript/presets/Video.d.ts.map +1 -0
  138. package/lib/typescript/providers/SkeletonProvider.d.ts +18 -0
  139. package/lib/typescript/providers/SkeletonProvider.d.ts.map +1 -0
  140. package/lib/typescript/types/index.d.ts +149 -0
  141. package/lib/typescript/types/index.d.ts.map +1 -0
  142. package/lib/typescript/utils/colors.d.ts +51 -0
  143. package/lib/typescript/utils/colors.d.ts.map +1 -0
  144. package/lib/typescript/utils/responsive.d.ts +27 -0
  145. package/lib/typescript/utils/responsive.d.ts.map +1 -0
  146. package/lib/typescript/utils/theme.d.ts +16 -0
  147. package/lib/typescript/utils/theme.d.ts.map +1 -0
  148. package/package.json +74 -0
  149. package/src/animations/breathing.tsx +81 -0
  150. package/src/animations/pulse.tsx +75 -0
  151. package/src/animations/shimmer.tsx +88 -0
  152. package/src/animations/wave.tsx +74 -0
  153. package/src/clone/SkeletonClone.tsx +82 -0
  154. package/src/components/Circle.tsx +13 -0
  155. package/src/components/Group.tsx +55 -0
  156. package/src/components/Pill.tsx +23 -0
  157. package/src/components/Rect.tsx +15 -0
  158. package/src/components/Skeleton.tsx +218 -0
  159. package/src/components/Square.tsx +13 -0
  160. package/src/demo/demo.gif +0 -0
  161. package/src/hooks/useReducedMotion.ts +46 -0
  162. package/src/hooks/useSkeletonTheme.ts +45 -0
  163. package/src/index.ts +94 -0
  164. package/src/presets/Card.tsx +56 -0
  165. package/src/presets/Chat.tsx +52 -0
  166. package/src/presets/Product.tsx +45 -0
  167. package/src/presets/Profile.tsx +54 -0
  168. package/src/presets/Video.tsx +53 -0
  169. package/src/providers/SkeletonProvider.tsx +67 -0
  170. package/src/types/index.ts +246 -0
  171. package/src/utils/colors.ts +132 -0
  172. package/src/utils/responsive.ts +73 -0
  173. package/src/utils/theme.ts +35 -0
@@ -0,0 +1,67 @@
1
+ import React, { createContext, useMemo } from "react";
2
+ import { I18nManager, useColorScheme } from "react-native";
3
+ import { useReducedMotion } from "../hooks/useReducedMotion";
4
+ import { resolveIsDark, getDefaultThemeColors } from "../utils/theme";
5
+ import { resolveThemeColors } from "../utils/colors";
6
+ import type { SkeletonContextValue, SkeletonProviderProps, SkeletonThemeColors } from "../types";
7
+
8
+ export const SkeletonContext = createContext<SkeletonContextValue | null>(null);
9
+
10
+ /**
11
+ * Global configuration provider for react-native-smart-skeleton.
12
+ *
13
+ * Wrap your app (or a subtree) once to set defaults for theme, animation,
14
+ * duration, performance mode, RTL, reduced-motion override, and brand colors.
15
+ * Any individual Skeleton component can still override these via its own props.
16
+ *
17
+ * @example
18
+ * <SkeletonProvider theme="system" animation="shimmer" duration={1200}>
19
+ * <App />
20
+ * </SkeletonProvider>
21
+ */
22
+ export function SkeletonProvider({
23
+ children,
24
+ theme = "system",
25
+ animation = "shimmer",
26
+ duration = 1200,
27
+ performanceMode = "balanced",
28
+ rtl,
29
+ reducedMotion: reducedMotionOverride,
30
+ colors,
31
+ }: SkeletonProviderProps) {
32
+ const systemScheme = useColorScheme();
33
+ const osReducedMotion = useReducedMotion();
34
+
35
+ const isDark = resolveIsDark(theme, systemScheme);
36
+ const reducedMotion = reducedMotionOverride ?? osReducedMotion;
37
+ const resolvedRtl = rtl ?? I18nManager.isRTL;
38
+
39
+ const value = useMemo<SkeletonContextValue>(() => {
40
+ const ambient = getDefaultThemeColors(isDark);
41
+
42
+ const getVariantColors = (variant?: string): SkeletonThemeColors =>
43
+ resolveThemeColors({
44
+ colorVariant: variant,
45
+ isDark,
46
+ brandColors: colors,
47
+ ambient,
48
+ });
49
+
50
+ return {
51
+ theme,
52
+ isDark,
53
+ animation,
54
+ duration,
55
+ performanceMode,
56
+ reducedMotion,
57
+ rtl: resolvedRtl,
58
+ colors,
59
+ resolvedColors: ambient,
60
+ getVariantColors,
61
+ };
62
+ }, [theme, isDark, animation, duration, performanceMode, reducedMotion, resolvedRtl, colors]);
63
+
64
+ return <SkeletonContext.Provider value={value}>{children}</SkeletonContext.Provider>;
65
+ }
66
+
67
+ export default SkeletonProvider;
@@ -0,0 +1,246 @@
1
+ import type React from "react";
2
+ import type {
3
+ ViewStyle,
4
+ StyleProp,
5
+ DimensionValue,
6
+ } from "react-native";
7
+
8
+ export type SkeletonDimensionValue = number | string | "auto" | undefined;
9
+
10
+ /* -------------------------------------------------------------------------- */
11
+ /* Animations */
12
+ /* -------------------------------------------------------------------------- */
13
+
14
+ export type SkeletonAnimation =
15
+ | "shimmer"
16
+ | "pulse"
17
+ | "wave"
18
+ | "breathing"
19
+ | "none";
20
+
21
+ /* -------------------------------------------------------------------------- */
22
+ /* Performance */
23
+ /* -------------------------------------------------------------------------- */
24
+
25
+ /**
26
+ * Controls how much work the animation driver does.
27
+ * - "high": full fidelity (gradients, smooth easing)
28
+ * - "balanced": default, good fidelity with cheaper easing curves
29
+ * - "battery-saver": reduces frame work, simpler interpolations, longer durations
30
+ */
31
+ export type SkeletonPerformanceMode = "high" | "balanced" | "battery-saver";
32
+
33
+ /* -------------------------------------------------------------------------- */
34
+ /* Theme */
35
+ /* -------------------------------------------------------------------------- */
36
+
37
+ export type SkeletonThemeMode = "light" | "dark" | "system";
38
+
39
+ export interface SkeletonThemeColors {
40
+ background: string;
41
+ highlight: string;
42
+ }
43
+
44
+ export interface SkeletonBrandColors {
45
+ light: SkeletonThemeColors;
46
+ dark: SkeletonThemeColors;
47
+ }
48
+
49
+ export interface ResolvedSkeletonTheme extends SkeletonThemeColors {
50
+ isDark: boolean;
51
+ }
52
+
53
+ /* -------------------------------------------------------------------------- */
54
+ /* Shared shape props */
55
+ /* -------------------------------------------------------------------------- */
56
+
57
+ export interface SkeletonProps {
58
+ /** Width of the skeleton. */
59
+ width?: SkeletonDimensionValue;
60
+
61
+ /** Height of the skeleton. */
62
+ height?: SkeletonDimensionValue;
63
+
64
+ borderRadius?: number;
65
+
66
+ backgroundColor?: string;
67
+ highlightColor?: string;
68
+ gradientColors?: string[];
69
+
70
+ animation?: SkeletonAnimation;
71
+ duration?: number;
72
+ delay?: number;
73
+ rtl?: boolean;
74
+ colorVariant?: string;
75
+
76
+ /** Style overrides */
77
+ style?: StyleProp<ViewStyle>;
78
+
79
+ accessibilityLabel?: string;
80
+
81
+ accessible?: boolean;
82
+ testID?: string;
83
+ }
84
+
85
+ /* -------------------------------------------------------------------------- */
86
+ /* Shape-specific props */
87
+ /* -------------------------------------------------------------------------- */
88
+
89
+ export interface SkeletonCircleProps extends Omit<SkeletonProps, "width" | "height" | "borderRadius"> {
90
+ /** Diameter of the circle. Defaults to 50. */
91
+ size?: number;
92
+ }
93
+
94
+ export interface SkeletonSquareProps extends Omit<SkeletonProps, "width" | "height"> {
95
+ /** Side length of the square. Defaults to 50. */
96
+ size?: number;
97
+ }
98
+
99
+ export interface SkeletonPillProps extends SkeletonProps {
100
+ /** Width of the pill. Defaults to 80. */
101
+ width?: SkeletonDimensionValue;
102
+
103
+ /** Height of the pill. Defaults to 24. */
104
+ height?: SkeletonDimensionValue;
105
+ }
106
+ /* -------------------------------------------------------------------------- */
107
+ /* Group / stagger */
108
+ /* -------------------------------------------------------------------------- */
109
+
110
+ export interface SkeletonGroupProps {
111
+ children: React.ReactNode;
112
+ /** Delay added between each direct child's animation start, in ms. Defaults to 80. */
113
+ staggerDelay?: number;
114
+ /** Animation applied to all children unless a child overrides it. */
115
+ animation?: SkeletonAnimation;
116
+ /** Layout direction for stagger ordering. Defaults to "column". */
117
+ direction?: "row" | "column";
118
+ style?: ViewStyle;
119
+ testID?: string;
120
+ }
121
+
122
+ /* -------------------------------------------------------------------------- */
123
+ /* Provider / context */
124
+ /* -------------------------------------------------------------------------- */
125
+
126
+ export interface SkeletonContextValue {
127
+ theme: SkeletonThemeMode;
128
+ isDark: boolean;
129
+ animation: SkeletonAnimation;
130
+ duration: number;
131
+ performanceMode: SkeletonPerformanceMode;
132
+ reducedMotion: boolean;
133
+ rtl: boolean;
134
+ colors?: Record<string, SkeletonBrandColors>;
135
+ resolvedColors: SkeletonThemeColors;
136
+ getVariantColors: (variant?: string) => SkeletonThemeColors;
137
+ }
138
+
139
+ export interface SkeletonProviderProps {
140
+ children: React.ReactNode;
141
+ /** "light" | "dark" | "system" (default). */
142
+ theme?: SkeletonThemeMode;
143
+ /** Default animation for all descendant skeletons. Defaults to "shimmer". */
144
+ animation?: SkeletonAnimation;
145
+ /** Default animation loop duration in ms. Defaults to 1200. */
146
+ duration?: number;
147
+ /** Default performance mode. Defaults to "balanced". */
148
+ performanceMode?: SkeletonPerformanceMode;
149
+ /** Force-enable/disable RTL mirroring. Defaults to following I18nManager. */
150
+ rtl?: boolean;
151
+ /** Force reduced-motion behavior regardless of OS setting. */
152
+ reducedMotion?: boolean;
153
+ /** Named brand color palettes, keyed by variant name, each with light/dark colors. */
154
+ colors?: Record<string, SkeletonBrandColors>;
155
+ }
156
+
157
+ /* -------------------------------------------------------------------------- */
158
+ /* Layout cloning */
159
+ /* -------------------------------------------------------------------------- */
160
+
161
+ export type SkeletonCloneShape = "rect" | "circle" | "pill";
162
+
163
+ // export interface SkeletonCloneNode {
164
+ // /** Shape to render for this node. Defaults to "rect". */
165
+ // shape?: SkeletonCloneShape;
166
+ // width?: number | string;
167
+ // height?: number | string;
168
+ // borderRadius?: number;
169
+ // style?: ViewStyle;
170
+ // /** Nested children, laid out according to `direction`. */
171
+ // children?: SkeletonCloneNode[];
172
+ // /** Layout direction for this node's children. Defaults to "column". */
173
+ // direction?: "row" | "column";
174
+ // /** Gap between this node's children, in dp. Defaults to 8. */
175
+ // gap?: number;
176
+ // }
177
+
178
+
179
+ export interface SkeletonCloneNode {
180
+ shape?: "rect" | "circle" | "pill";
181
+ width?: SkeletonDimensionValue;
182
+ height?: SkeletonDimensionValue;
183
+
184
+ borderRadius?: number;
185
+ style?: StyleProp<ViewStyle>;
186
+
187
+ children?: SkeletonCloneNode[];
188
+ direction?: "row" | "column";
189
+ gap?: number;
190
+ }
191
+
192
+ export interface SkeletonCloneProps {
193
+ /** Declarative layout tree describing the real content's shape. */
194
+ layout: SkeletonCloneNode;
195
+ animation?: SkeletonAnimation;
196
+ style?: ViewStyle;
197
+ testID?: string;
198
+ }
199
+
200
+ /* -------------------------------------------------------------------------- */
201
+ /* Presets */
202
+ /* -------------------------------------------------------------------------- */
203
+
204
+ export interface SkeletonPresetProps {
205
+ /** Number of times to repeat this preset block. Defaults to 1. */
206
+ count?: number;
207
+ /** Gap between repeated blocks, in dp. Defaults to 16. */
208
+ gap?: number;
209
+ animation?: SkeletonAnimation;
210
+ style?: ViewStyle;
211
+ testID?: string;
212
+ }
213
+
214
+ export interface SkeletonProfilePresetProps extends SkeletonPresetProps {
215
+ avatarSize?: number;
216
+ lines?: number;
217
+ }
218
+
219
+ export interface SkeletonCardPresetProps extends SkeletonPresetProps {
220
+ imageHeight?: number;
221
+ lines?: number;
222
+ showAvatar?: boolean;
223
+ }
224
+
225
+ export interface SkeletonChatPresetProps extends SkeletonPresetProps {
226
+ messages?: number;
227
+ /** Alternate bubble alignment left/right to mimic a real conversation. Defaults to true. */
228
+ alternate?: boolean;
229
+ }
230
+
231
+ export interface SkeletonProductPresetProps extends SkeletonPresetProps {
232
+ imageHeight?: number;
233
+ columns?: number;
234
+ }
235
+
236
+ export interface SkeletonVideoPresetProps extends SkeletonPresetProps {
237
+ aspectRatio?: number;
238
+ showTitle?: boolean;
239
+ showMeta?: boolean;
240
+ }
241
+
242
+ /* -------------------------------------------------------------------------- */
243
+ /* Responsive utils */
244
+ /* -------------------------------------------------------------------------- */
245
+
246
+ export type ScreenSizeClass = "compact" | "regular" | "wide";
@@ -0,0 +1,132 @@
1
+ import type { SkeletonBrandColors, SkeletonThemeColors } from "../types";
2
+
3
+ /**
4
+ * Default light/dark palettes used when no brand colors or explicit
5
+ * backgroundColor/highlightColor are supplied.
6
+ */
7
+ export const DEFAULT_LIGHT_COLORS: SkeletonThemeColors = {
8
+ background: "#E5E7EB",
9
+ highlight: "#F3F4F6",
10
+ };
11
+
12
+ export const DEFAULT_DARK_COLORS: SkeletonThemeColors = {
13
+ background: "#2D3748",
14
+ highlight: "#4A5568",
15
+ };
16
+
17
+ /**
18
+ * Clamp a number between 0 and 255.
19
+ */
20
+ function clampChannel(value: number): number {
21
+ return Math.max(0, Math.min(255, Math.round(value)));
22
+ }
23
+
24
+ /**
25
+ * Parse a hex color (#RGB, #RRGGBB, #RRGGBBAA) into r/g/b/a channels.
26
+ * Returns null if the string isn't a parseable hex color.
27
+ */
28
+ export function parseHexColor(
29
+ hex: string
30
+ ): { r: number; g: number; b: number; a: number } | null {
31
+ const normalized = hex.trim().replace("#", "");
32
+
33
+ if (normalized.length === 3) {
34
+ const r = parseInt(normalized[0] + normalized[0], 16);
35
+ const g = parseInt(normalized[1] + normalized[1], 16);
36
+ const b = parseInt(normalized[2] + normalized[2], 16);
37
+ return { r, g, b, a: 1 };
38
+ }
39
+
40
+ if (normalized.length === 6) {
41
+ const r = parseInt(normalized.slice(0, 2), 16);
42
+ const g = parseInt(normalized.slice(2, 4), 16);
43
+ const b = parseInt(normalized.slice(4, 6), 16);
44
+ return { r, g, b, a: 1 };
45
+ }
46
+
47
+ if (normalized.length === 8) {
48
+ const r = parseInt(normalized.slice(0, 2), 16);
49
+ const g = parseInt(normalized.slice(2, 4), 16);
50
+ const b = parseInt(normalized.slice(4, 6), 16);
51
+ const a = parseInt(normalized.slice(6, 8), 16) / 255;
52
+ return { r, g, b, a };
53
+ }
54
+
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * Convert an rgb/rgba channel set back into a CSS rgba() string.
60
+ */
61
+ export function toRgbaString(r: number, g: number, b: number, a: number): string {
62
+ return `rgba(${clampChannel(r)}, ${clampChannel(g)}, ${clampChannel(b)}, ${Math.max(
63
+ 0,
64
+ Math.min(1, a)
65
+ )})`;
66
+ }
67
+
68
+ /**
69
+ * Lighten or darken a hex color by a percentage (-1 to 1).
70
+ * Positive amount lightens, negative darkens. Non-hex input is returned unchanged.
71
+ */
72
+ export function shadeColor(hex: string, amount: number): string {
73
+ const parsed = parseHexColor(hex);
74
+ if (!parsed) return hex;
75
+
76
+ const { r, g, b, a } = parsed;
77
+ const factor = amount;
78
+
79
+ const adjust = (channel: number) =>
80
+ factor >= 0
81
+ ? channel + (255 - channel) * factor
82
+ : channel + channel * factor;
83
+
84
+ return toRgbaString(adjust(r), adjust(g), adjust(b), a);
85
+ }
86
+
87
+ /**
88
+ * Given a hex/rgba color, return a translucent rgba() version at the given opacity.
89
+ * Falls back to the original string if it isn't parseable (e.g. already rgba(), or a named color).
90
+ */
91
+ export function withOpacity(color: string, opacity: number): string {
92
+ const parsed = parseHexColor(color);
93
+ if (!parsed) return color;
94
+ return toRgbaString(parsed.r, parsed.g, parsed.b, opacity);
95
+ }
96
+
97
+ /**
98
+ * Resolve the {background, highlight} pair to use for a skeleton, given (in priority order):
99
+ * 1. explicit backgroundColor/highlightColor props
100
+ * 2. a brand color variant (colorVariant + colors map from the provider)
101
+ * 3. the ambient theme colors resolved from useSkeletonTheme
102
+ */
103
+ export function resolveThemeColors(params: {
104
+ backgroundColor?: string;
105
+ highlightColor?: string;
106
+ colorVariant?: string;
107
+ isDark: boolean;
108
+ brandColors?: Record<string, SkeletonBrandColors>;
109
+ ambient: SkeletonThemeColors;
110
+ }): SkeletonThemeColors {
111
+ const { backgroundColor, highlightColor, colorVariant, isDark, brandColors, ambient } = params;
112
+
113
+ let base: SkeletonThemeColors = ambient;
114
+
115
+ if (colorVariant && brandColors && brandColors[colorVariant]) {
116
+ const variant = brandColors[colorVariant];
117
+ base = isDark ? variant.dark : variant.light;
118
+ }
119
+
120
+ return {
121
+ background: backgroundColor ?? base.background,
122
+ highlight: highlightColor ?? base.highlight,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Build a 3-stop gradient color array from a background/highlight pair,
128
+ * matching the classic skeleton "sweep" look: base -> highlight -> base.
129
+ */
130
+ export function buildSweepGradient(background: string, highlight: string): string[] {
131
+ return [background, highlight, background];
132
+ }
@@ -0,0 +1,73 @@
1
+ import { Dimensions, PixelRatio } from "react-native";
2
+ import type { ScreenSizeClass } from "../types";
3
+
4
+ /**
5
+ * Breakpoints (in dp) used to classify the current screen width.
6
+ * Roughly: phones -> "compact", small tablets / large phones landscape -> "regular",
7
+ * tablets / desktop-like windows -> "wide".
8
+ */
9
+ const BREAKPOINTS = {
10
+ regular: 480,
11
+ wide: 768,
12
+ };
13
+
14
+ /**
15
+ * Returns the current window width, with a safe fallback for environments
16
+ * where Dimensions may not be ready (e.g. very early app boot, tests).
17
+ */
18
+ export function getScreenWidth(): number {
19
+ try {
20
+ const { width } = Dimensions.get("window");
21
+ return width || 0;
22
+ } catch {
23
+ return 0;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Classify the current screen width into a coarse size bucket, useful for
29
+ * preset components that want to adapt column counts or avatar sizes.
30
+ */
31
+ export function getScreenSizeClass(width: number = getScreenWidth()): ScreenSizeClass {
32
+ if (width >= BREAKPOINTS.wide) return "wide";
33
+ if (width >= BREAKPOINTS.regular) return "regular";
34
+ return "compact";
35
+ }
36
+
37
+ /**
38
+ * Scale a base dp value by the size class, useful for nudging preset
39
+ * dimensions (avatar size, line height) up slightly on larger screens.
40
+ */
41
+ export function scaleBySize(base: number, sizeClass: ScreenSizeClass): number {
42
+ switch (sizeClass) {
43
+ case "wide":
44
+ return Math.round(base * 1.25);
45
+ case "regular":
46
+ return Math.round(base * 1.1);
47
+ default:
48
+ return base;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Resolve a recommended column count for grid-style presets (e.g. Product),
54
+ * based on the current screen size class.
55
+ */
56
+ export function getRecommendedColumns(sizeClass: ScreenSizeClass = getScreenSizeClass()): number {
57
+ switch (sizeClass) {
58
+ case "wide":
59
+ return 4;
60
+ case "regular":
61
+ return 3;
62
+ default:
63
+ return 2;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Snap a dp value to the nearest physical pixel, reducing anti-aliased/blurry
69
+ * edges on skeleton borders, similar to React Native's own hairline handling.
70
+ */
71
+ export function pixelSnap(value: number): number {
72
+ return PixelRatio.roundToNearestPixel(value);
73
+ }
@@ -0,0 +1,35 @@
1
+ import { Appearance } from "react-native";
2
+ import type { SkeletonThemeMode } from "../types";
3
+ import { DEFAULT_DARK_COLORS, DEFAULT_LIGHT_COLORS } from "./colors";
4
+
5
+ /**
6
+ * Resolve a SkeletonThemeMode ("light" | "dark" | "system") plus the current
7
+ * OS color scheme into a concrete isDark boolean.
8
+ */
9
+ export function resolveIsDark(mode: SkeletonThemeMode, systemScheme: "light" | "dark" | null | undefined): boolean {
10
+ if (mode === "light") return false;
11
+ if (mode === "dark") return true;
12
+ return systemScheme === "dark";
13
+ }
14
+
15
+ /**
16
+ * Synchronous read of the current system color scheme, for use outside
17
+ * of component render (e.g. computing an initial value).
18
+ */
19
+ export function getSystemColorScheme():
20
+ | "light"
21
+ | "dark"
22
+ | null {
23
+ try {
24
+ return Appearance.getColorScheme() ?? null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Default theme colors for a resolved dark/light state.
32
+ */
33
+ export function getDefaultThemeColors(isDark: boolean) {
34
+ return isDark ? DEFAULT_DARK_COLORS : DEFAULT_LIGHT_COLORS;
35
+ }