@oxyhq/bloom 0.6.11 → 0.6.13
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/README.md +3 -1
- package/lib/commonjs/index.js +36 -36
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +38 -38
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/loading/Loading.js +1 -1
- package/lib/commonjs/loading/Loading.web.js +237 -0
- package/lib/commonjs/loading/Loading.web.js.map +1 -0
- package/lib/commonjs/loading/SpinnerIcon.web.js +144 -0
- package/lib/commonjs/loading/SpinnerIcon.web.js.map +1 -0
- package/lib/commonjs/loading/index.js +2 -2
- package/lib/commonjs/loading/index.web.js +20 -0
- package/lib/commonjs/loading/index.web.js.map +1 -0
- package/lib/commonjs/theme/apply-dark-class.js +6 -1
- package/lib/commonjs/theme/apply-dark-class.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/loading/Loading.js +1 -1
- package/lib/module/loading/Loading.js.map +1 -1
- package/lib/module/loading/Loading.web.js +232 -0
- package/lib/module/loading/Loading.web.js.map +1 -0
- package/lib/module/loading/SpinnerIcon.web.js +138 -0
- package/lib/module/loading/SpinnerIcon.web.js.map +1 -0
- package/lib/module/loading/index.js +2 -2
- package/lib/module/loading/index.js.map +1 -1
- package/lib/module/loading/index.web.js +18 -0
- package/lib/module/loading/index.web.js.map +1 -0
- package/lib/module/theme/apply-dark-class.js +6 -1
- package/lib/module/theme/apply-dark-class.js.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +1 -1
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/Loading.web.d.ts +4 -0
- package/lib/typescript/commonjs/loading/Loading.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/loading/SpinnerIcon.web.d.ts +17 -0
- package/lib/typescript/commonjs/loading/SpinnerIcon.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/loading/index.web.d.ts +4 -0
- package/lib/typescript/commonjs/loading/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/apply-dark-class.d.ts +5 -0
- package/lib/typescript/commonjs/theme/apply-dark-class.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +1 -1
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/loading/Loading.web.d.ts +4 -0
- package/lib/typescript/module/loading/Loading.web.d.ts.map +1 -0
- package/lib/typescript/module/loading/SpinnerIcon.web.d.ts +17 -0
- package/lib/typescript/module/loading/SpinnerIcon.web.d.ts.map +1 -0
- package/lib/typescript/module/loading/index.web.d.ts +4 -0
- package/lib/typescript/module/loading/index.web.d.ts.map +1 -0
- package/lib/typescript/module/theme/apply-dark-class.d.ts +5 -0
- package/lib/typescript/module/theme/apply-dark-class.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/index.web.ts +1 -1
- package/src/loading/Loading.web.tsx +244 -0
- package/src/loading/SpinnerIcon.web.tsx +134 -0
- package/src/loading/index.web.ts +24 -0
- package/src/theme/apply-dark-class.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/bloom",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.13",
|
|
4
4
|
"description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -172,6 +172,11 @@
|
|
|
172
172
|
},
|
|
173
173
|
"./loading": {
|
|
174
174
|
"react-native": "./src/loading/index.ts",
|
|
175
|
+
"browser": {
|
|
176
|
+
"types": "./lib/typescript/module/loading/index.web.d.ts",
|
|
177
|
+
"import": "./lib/module/loading/index.web.js",
|
|
178
|
+
"require": "./lib/commonjs/loading/index.web.js"
|
|
179
|
+
},
|
|
175
180
|
"import": {
|
|
176
181
|
"types": "./lib/typescript/module/loading/index.d.ts",
|
|
177
182
|
"default": "./lib/module/loading/index.js"
|
package/src/index.web.ts
CHANGED
|
@@ -54,7 +54,7 @@ export type {
|
|
|
54
54
|
ErrorBoundaryFallbackContext,
|
|
55
55
|
} from './error-boundary';
|
|
56
56
|
export * from './avatar';
|
|
57
|
-
export * from './loading';
|
|
57
|
+
export * from './loading/index.web';
|
|
58
58
|
export * as PromptInput from './prompt-input';
|
|
59
59
|
export * from './switch';
|
|
60
60
|
export { toast, type Toast } from './toast/index.web';
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import React, { memo, useMemo } from 'react';
|
|
2
|
+
import { View, Text, StyleSheet, type DimensionValue, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { useTheme } from '../theme/use-theme';
|
|
5
|
+
import { animation } from '../styles/tokens';
|
|
6
|
+
import { SpinnerIcon } from './SpinnerIcon.web';
|
|
7
|
+
import type {
|
|
8
|
+
LoadingProps,
|
|
9
|
+
SpinnerLoadingProps,
|
|
10
|
+
TopLoadingProps,
|
|
11
|
+
SkeletonLoadingProps,
|
|
12
|
+
InlineLoadingProps,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
const SIZE_CONFIG = {
|
|
16
|
+
small: { spinner: 20, text: 13 },
|
|
17
|
+
medium: { spinner: 24, text: 15 },
|
|
18
|
+
large: { spinner: 44, text: 16 },
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
const SpinnerLoading: React.FC<SpinnerLoadingProps> = ({
|
|
22
|
+
size = 'medium',
|
|
23
|
+
color,
|
|
24
|
+
className,
|
|
25
|
+
text,
|
|
26
|
+
textStyle,
|
|
27
|
+
style,
|
|
28
|
+
showText = true,
|
|
29
|
+
iconSize,
|
|
30
|
+
spinnerIcon,
|
|
31
|
+
testID,
|
|
32
|
+
}) => {
|
|
33
|
+
const theme = useTheme();
|
|
34
|
+
const sizeConfig = SIZE_CONFIG[size];
|
|
35
|
+
const effectiveIconSize = iconSize ?? sizeConfig.spinner;
|
|
36
|
+
const spinnerColor = className ? 'currentColor' : (color ?? theme.colors.primary);
|
|
37
|
+
const textColor = color ?? theme.colors.textSecondary;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View style={[styles.container, style]} testID={testID}>
|
|
41
|
+
{spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} className={className} />}
|
|
42
|
+
{showText && text && (
|
|
43
|
+
<Text
|
|
44
|
+
style={[
|
|
45
|
+
styles.text,
|
|
46
|
+
{ color: textColor, fontSize: sizeConfig.text, marginTop: 8 },
|
|
47
|
+
textStyle,
|
|
48
|
+
]}
|
|
49
|
+
>
|
|
50
|
+
{text}
|
|
51
|
+
</Text>
|
|
52
|
+
)}
|
|
53
|
+
</View>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Web fork of the `top` variant.
|
|
59
|
+
*
|
|
60
|
+
* The native variant collapses/expands the container height and slides the
|
|
61
|
+
* spinner in/out with `react-native-reanimated`. Reanimated can't ship to a
|
|
62
|
+
* web bundle (its worklets Babel plugin has no web equivalent and importing it
|
|
63
|
+
* statically breaks the bundler), so this fork drives the same motion with CSS
|
|
64
|
+
* transitions — react-native-web emits `transition-*` style props to the DOM.
|
|
65
|
+
* The element stays mounted across `showLoading` toggles so both directions
|
|
66
|
+
* animate, exactly like the native `withTiming` on `height`, `opacity`, and
|
|
67
|
+
* `translateY`.
|
|
68
|
+
*/
|
|
69
|
+
const TopLoading: React.FC<TopLoadingProps> = ({
|
|
70
|
+
size = 'medium',
|
|
71
|
+
color,
|
|
72
|
+
style,
|
|
73
|
+
showLoading = true,
|
|
74
|
+
iconSize,
|
|
75
|
+
heightOffset = 0,
|
|
76
|
+
spinnerIcon,
|
|
77
|
+
testID,
|
|
78
|
+
}) => {
|
|
79
|
+
const theme = useTheme();
|
|
80
|
+
const sizeConfig = SIZE_CONFIG[size];
|
|
81
|
+
const effectiveIconSize = iconSize ?? sizeConfig.spinner;
|
|
82
|
+
const targetHeight = Math.max(0, effectiveIconSize + sizeConfig.spinner + heightOffset);
|
|
83
|
+
const spinnerColor = color ?? theme.colors.primary;
|
|
84
|
+
|
|
85
|
+
const duration = animation.duration.slow;
|
|
86
|
+
// `cubic-bezier(0.33, 1, 0.68, 1)` is the standard CSS approximation of
|
|
87
|
+
// reanimated's `Easing.out(Easing.cubic)` used by the native variant.
|
|
88
|
+
const easing = 'cubic-bezier(0.33, 1, 0.68, 1)';
|
|
89
|
+
|
|
90
|
+
const containerTransition = {
|
|
91
|
+
transitionProperty: 'height',
|
|
92
|
+
transitionDuration: `${duration}ms`,
|
|
93
|
+
transitionTimingFunction: easing,
|
|
94
|
+
} as ViewStyle;
|
|
95
|
+
|
|
96
|
+
const innerTransition = {
|
|
97
|
+
transitionProperty: 'opacity, transform',
|
|
98
|
+
transitionDuration: `${duration}ms`,
|
|
99
|
+
transitionTimingFunction: easing,
|
|
100
|
+
} as ViewStyle;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<View
|
|
104
|
+
style={[styles.topContainer, { height: showLoading ? targetHeight : 0 }, containerTransition]}
|
|
105
|
+
testID={testID}
|
|
106
|
+
>
|
|
107
|
+
<View
|
|
108
|
+
style={[
|
|
109
|
+
styles.topLoadingView,
|
|
110
|
+
{ height: targetHeight },
|
|
111
|
+
{
|
|
112
|
+
opacity: showLoading ? 1 : 0,
|
|
113
|
+
transform: [{ translateY: showLoading ? 0 : -targetHeight }],
|
|
114
|
+
},
|
|
115
|
+
innerTransition,
|
|
116
|
+
style,
|
|
117
|
+
]}
|
|
118
|
+
>
|
|
119
|
+
{spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} />}
|
|
120
|
+
</View>
|
|
121
|
+
</View>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const SkeletonLoading: React.FC<SkeletonLoadingProps> = ({
|
|
126
|
+
lines = 3,
|
|
127
|
+
width = '100%',
|
|
128
|
+
lineHeight = 16,
|
|
129
|
+
style,
|
|
130
|
+
testID,
|
|
131
|
+
}) => {
|
|
132
|
+
const theme = useTheme();
|
|
133
|
+
const skeletonColor = theme.colors.backgroundSecondary;
|
|
134
|
+
|
|
135
|
+
const skeletonLines = useMemo(
|
|
136
|
+
() =>
|
|
137
|
+
Array.from({ length: lines }, (_, index) => (
|
|
138
|
+
<View
|
|
139
|
+
key={index}
|
|
140
|
+
style={[
|
|
141
|
+
styles.skeletonLine,
|
|
142
|
+
{
|
|
143
|
+
width: (typeof width === 'string' ? width : `${width}%`) as DimensionValue,
|
|
144
|
+
height: lineHeight,
|
|
145
|
+
backgroundColor: skeletonColor,
|
|
146
|
+
marginBottom: index < lines - 1 ? 8 : 0,
|
|
147
|
+
},
|
|
148
|
+
]}
|
|
149
|
+
/>
|
|
150
|
+
)),
|
|
151
|
+
[lines, width, lineHeight, skeletonColor],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<View style={[styles.skeletonContainer, style]} testID={testID}>
|
|
156
|
+
{skeletonLines}
|
|
157
|
+
</View>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const InlineLoading: React.FC<InlineLoadingProps> = ({
|
|
162
|
+
size = 'small',
|
|
163
|
+
color,
|
|
164
|
+
text,
|
|
165
|
+
style,
|
|
166
|
+
textStyle,
|
|
167
|
+
spinnerIcon,
|
|
168
|
+
testID,
|
|
169
|
+
}) => {
|
|
170
|
+
const theme = useTheme();
|
|
171
|
+
const sizeConfig = SIZE_CONFIG[size];
|
|
172
|
+
const spinnerColor = color ?? theme.colors.primary;
|
|
173
|
+
const textColor = theme.colors.textSecondary;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<View style={[styles.inlineContainer, style]} testID={testID}>
|
|
177
|
+
{spinnerIcon ?? <SpinnerIcon size={SIZE_CONFIG.small.spinner} color={spinnerColor} />}
|
|
178
|
+
{text && (
|
|
179
|
+
<Text
|
|
180
|
+
style={[
|
|
181
|
+
{ color: textColor, fontSize: sizeConfig.text, marginLeft: 8 },
|
|
182
|
+
textStyle,
|
|
183
|
+
]}
|
|
184
|
+
>
|
|
185
|
+
{text}
|
|
186
|
+
</Text>
|
|
187
|
+
)}
|
|
188
|
+
</View>
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const LoadingComponent: React.FC<LoadingProps> = (props) => {
|
|
193
|
+
const variant = props.variant ?? 'spinner';
|
|
194
|
+
|
|
195
|
+
switch (variant) {
|
|
196
|
+
case 'top':
|
|
197
|
+
return <TopLoading {...(props as TopLoadingProps)} />;
|
|
198
|
+
case 'skeleton':
|
|
199
|
+
return <SkeletonLoading {...(props as SkeletonLoadingProps)} />;
|
|
200
|
+
case 'inline':
|
|
201
|
+
return <InlineLoading {...(props as InlineLoadingProps)} />;
|
|
202
|
+
case 'spinner':
|
|
203
|
+
default:
|
|
204
|
+
return <SpinnerLoading {...(props as SpinnerLoadingProps)} />;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const Loading = memo(LoadingComponent);
|
|
209
|
+
Loading.displayName = 'Loading';
|
|
210
|
+
|
|
211
|
+
const styles = StyleSheet.create({
|
|
212
|
+
container: {
|
|
213
|
+
alignItems: 'center',
|
|
214
|
+
justifyContent: 'center',
|
|
215
|
+
padding: 16,
|
|
216
|
+
},
|
|
217
|
+
text: {
|
|
218
|
+
textAlign: 'center',
|
|
219
|
+
},
|
|
220
|
+
topContainer: {
|
|
221
|
+
width: '100%',
|
|
222
|
+
position: 'relative',
|
|
223
|
+
overflow: 'hidden',
|
|
224
|
+
},
|
|
225
|
+
topLoadingView: {
|
|
226
|
+
width: '100%',
|
|
227
|
+
alignItems: 'center',
|
|
228
|
+
justifyContent: 'center',
|
|
229
|
+
position: 'absolute',
|
|
230
|
+
top: 0,
|
|
231
|
+
left: 0,
|
|
232
|
+
},
|
|
233
|
+
skeletonContainer: {
|
|
234
|
+
width: '100%',
|
|
235
|
+
},
|
|
236
|
+
skeletonLine: {
|
|
237
|
+
borderRadius: 4,
|
|
238
|
+
},
|
|
239
|
+
inlineContainer: {
|
|
240
|
+
flexDirection: 'row',
|
|
241
|
+
alignItems: 'center',
|
|
242
|
+
justifyContent: 'center',
|
|
243
|
+
},
|
|
244
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { StyleSheet, View, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
interface SpinnerIconProps {
|
|
5
|
+
size?: number;
|
|
6
|
+
color?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: ViewStyle;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* iOS-style spinner — 8 rotating blades with an opacity-gradient trail.
|
|
13
|
+
*
|
|
14
|
+
* Web fork of `./SpinnerIcon`. The native variant draws the blades with
|
|
15
|
+
* `react-native-svg` and spins them with `react-native-reanimated`. Neither
|
|
16
|
+
* dependency exists in a plain web bundle (and importing reanimated statically
|
|
17
|
+
* breaks any web bundler — its Babel worklets plugin has no web equivalent), so
|
|
18
|
+
* this fork renders the blades as plain `View`s and spins the container with a
|
|
19
|
+
* CSS `@keyframes` rotation injected once into `<head>`.
|
|
20
|
+
*
|
|
21
|
+
* The geometry mirrors the native SVG exactly. The SVG draws each blade on a
|
|
22
|
+
* 100×100 canvas as a `28×10` rounded rect with its top-left at `(67, 45)` —
|
|
23
|
+
* i.e. a blade whose centre sits `31` units to the right of the canvas centre
|
|
24
|
+
* `(50, 50)` — then rotates copies of it about the centre at
|
|
25
|
+
* `-90…225°` in `45°` steps with opacities `0…0.875`. Here each blade is a
|
|
26
|
+
* `View` centred on the container, then `rotate(θ) translateX(radius)` places
|
|
27
|
+
* its centre at distance `radius` from the container centre at angle `θ`. All
|
|
28
|
+
* lengths scale by `size / 100` so any `size` is pixel-identical to native.
|
|
29
|
+
*/
|
|
30
|
+
const SVG_CANVAS = 100;
|
|
31
|
+
const BLADE_WIDTH = 28;
|
|
32
|
+
const BLADE_HEIGHT = 10;
|
|
33
|
+
/** Distance from canvas centre to a blade's centre: (67 + 28/2) − 50 = 31. */
|
|
34
|
+
const BLADE_RADIUS = 31;
|
|
35
|
+
const BLADE_RADIUS_CORNER = BLADE_HEIGHT / 2;
|
|
36
|
+
|
|
37
|
+
/** Angle (deg) / opacity for each of the 8 blades, matching the native SVG. */
|
|
38
|
+
const BLADES: ReadonlyArray<{ angle: number; opacity: number }> = [
|
|
39
|
+
{ angle: -90, opacity: 0 },
|
|
40
|
+
{ angle: -45, opacity: 0.125 },
|
|
41
|
+
{ angle: 0, opacity: 0.25 },
|
|
42
|
+
{ angle: 45, opacity: 0.375 },
|
|
43
|
+
{ angle: 90, opacity: 0.5 },
|
|
44
|
+
{ angle: 135, opacity: 0.625 },
|
|
45
|
+
{ angle: 180, opacity: 0.75 },
|
|
46
|
+
{ angle: 225, opacity: 0.875 },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const SPIN_DURATION_MS = 400;
|
|
50
|
+
const KEYFRAMES_ID = 'bloom-spinner-keyframes';
|
|
51
|
+
const SPIN_ANIMATION_NAME = 'bloomSpinnerRotate';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* CSS keyframes powering the spin. Injected once into `<head>` (keyed by id so
|
|
55
|
+
* multiple spinners and re-mounts don't duplicate the rule). Mirrors the
|
|
56
|
+
* native `withRepeat(withTiming(360, { duration: 400, easing: linear }))`.
|
|
57
|
+
*/
|
|
58
|
+
export const BLOOM_SPINNER_CSS = `
|
|
59
|
+
@keyframes ${SPIN_ANIMATION_NAME} { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
function useKeyframes(): void {
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (typeof document === 'undefined') return;
|
|
65
|
+
if (document.getElementById(KEYFRAMES_ID)) return;
|
|
66
|
+
const style = document.createElement('style');
|
|
67
|
+
style.id = KEYFRAMES_ID;
|
|
68
|
+
style.textContent = BLOOM_SPINNER_CSS;
|
|
69
|
+
document.head.appendChild(style);
|
|
70
|
+
}, []);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const SpinnerIcon: React.FC<SpinnerIconProps> = ({
|
|
74
|
+
color = 'currentColor',
|
|
75
|
+
size = 26,
|
|
76
|
+
className,
|
|
77
|
+
style,
|
|
78
|
+
}) => {
|
|
79
|
+
useKeyframes();
|
|
80
|
+
|
|
81
|
+
const scale = size / SVG_CANVAS;
|
|
82
|
+
const bladeWidth = BLADE_WIDTH * scale;
|
|
83
|
+
const bladeHeight = BLADE_HEIGHT * scale;
|
|
84
|
+
const bladeRadius = BLADE_RADIUS * scale;
|
|
85
|
+
const bladeCorner = BLADE_RADIUS_CORNER * scale;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<View
|
|
89
|
+
{...(className ? ({ className } as Record<string, string>) : {})}
|
|
90
|
+
// `animation` is a real CSS prop on web; react-native-web passes it
|
|
91
|
+
// through to the DOM node. Typed as a cast because RN's ViewStyle has no
|
|
92
|
+
// `animation` key.
|
|
93
|
+
style={[
|
|
94
|
+
styles.container,
|
|
95
|
+
{ width: size, height: size },
|
|
96
|
+
{ animation: `${SPIN_ANIMATION_NAME} ${SPIN_DURATION_MS}ms linear infinite` } as ViewStyle,
|
|
97
|
+
style,
|
|
98
|
+
]}
|
|
99
|
+
>
|
|
100
|
+
{BLADES.map(({ angle, opacity }) => (
|
|
101
|
+
<View
|
|
102
|
+
key={angle}
|
|
103
|
+
style={[
|
|
104
|
+
styles.blade,
|
|
105
|
+
{
|
|
106
|
+
width: bladeWidth,
|
|
107
|
+
height: bladeHeight,
|
|
108
|
+
borderRadius: bladeCorner,
|
|
109
|
+
backgroundColor: color,
|
|
110
|
+
opacity,
|
|
111
|
+
marginLeft: -bladeWidth / 2,
|
|
112
|
+
marginTop: -bladeHeight / 2,
|
|
113
|
+
transform: [{ rotate: `${angle}deg` }, { translateX: bladeRadius }],
|
|
114
|
+
},
|
|
115
|
+
]}
|
|
116
|
+
/>
|
|
117
|
+
))}
|
|
118
|
+
</View>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
SpinnerIcon.displayName = 'SpinnerIcon';
|
|
123
|
+
|
|
124
|
+
const styles = StyleSheet.create({
|
|
125
|
+
container: {
|
|
126
|
+
alignItems: 'center',
|
|
127
|
+
justifyContent: 'center',
|
|
128
|
+
},
|
|
129
|
+
blade: {
|
|
130
|
+
position: 'absolute',
|
|
131
|
+
top: '50%',
|
|
132
|
+
left: '50%',
|
|
133
|
+
},
|
|
134
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Web variant of the `./loading` barrel.
|
|
2
|
+
//
|
|
3
|
+
// The default barrel (`./index.ts`) re-exports from `./Loading` and
|
|
4
|
+
// `./SpinnerIcon`, which statically import `react-native-reanimated` (and lazily
|
|
5
|
+
// `react-native-svg`). Reanimated has no web build — its worklets Babel plugin
|
|
6
|
+
// is native-only and importing it statically breaks every web bundler (Vite,
|
|
7
|
+
// webpack, Metro-web). The web forks (`./Loading.web`, `./SpinnerIcon.web`)
|
|
8
|
+
// render the same components with CSS keyframes/transitions and no native deps.
|
|
9
|
+
//
|
|
10
|
+
// Web bundlers select this file via the `"browser"` export condition in
|
|
11
|
+
// `package.json`'s `exports['./loading']`; native bundlers fall through to the
|
|
12
|
+
// React Native build above. Types are platform-agnostic, so they come straight
|
|
13
|
+
// from `./types`.
|
|
14
|
+
export { Loading } from './Loading.web';
|
|
15
|
+
export { SpinnerIcon } from './SpinnerIcon.web';
|
|
16
|
+
export type {
|
|
17
|
+
LoadingProps,
|
|
18
|
+
LoadingVariant,
|
|
19
|
+
LoadingSize,
|
|
20
|
+
SpinnerLoadingProps,
|
|
21
|
+
TopLoadingProps,
|
|
22
|
+
SkeletonLoadingProps,
|
|
23
|
+
InlineLoadingProps,
|
|
24
|
+
} from './types';
|
|
@@ -10,6 +10,11 @@ export function applyDarkClass(resolved: 'light' | 'dark') {
|
|
|
10
10
|
/**
|
|
11
11
|
* Apply a color preset's CSS custom properties to the document root.
|
|
12
12
|
* No-op on native — only affects web.
|
|
13
|
+
*
|
|
14
|
+
* Values are written as raw HSL triples (e.g. `185 100% 20%`), matching the
|
|
15
|
+
* shadcn/Tailwind convention where stylesheets wrap them themselves with
|
|
16
|
+
* `hsl(var(--primary))`. Writing pre-resolved `hsl(...)` values here would
|
|
17
|
+
* produce invalid `hsl(hsl(...))` in consuming stylesheets and break theming.
|
|
13
18
|
*/
|
|
14
19
|
export function applyColorPresetVars(preset: AppColorName, resolved: 'light' | 'dark') {
|
|
15
20
|
if (Platform.OS !== 'web' || typeof document === 'undefined') return;
|
|
@@ -21,6 +26,6 @@ export function applyColorPresetVars(preset: AppColorName, resolved: 'light' | '
|
|
|
21
26
|
const root = document.documentElement.style;
|
|
22
27
|
|
|
23
28
|
for (const [key, value] of Object.entries(vars)) {
|
|
24
|
-
root.setProperty(key,
|
|
29
|
+
root.setProperty(key, value);
|
|
25
30
|
}
|
|
26
31
|
}
|