@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.
Files changed (57) hide show
  1. package/README.md +3 -1
  2. package/lib/commonjs/index.js +36 -36
  3. package/lib/commonjs/index.js.map +1 -1
  4. package/lib/commonjs/index.web.js +38 -38
  5. package/lib/commonjs/index.web.js.map +1 -1
  6. package/lib/commonjs/loading/Loading.js +1 -1
  7. package/lib/commonjs/loading/Loading.web.js +237 -0
  8. package/lib/commonjs/loading/Loading.web.js.map +1 -0
  9. package/lib/commonjs/loading/SpinnerIcon.web.js +144 -0
  10. package/lib/commonjs/loading/SpinnerIcon.web.js.map +1 -0
  11. package/lib/commonjs/loading/index.js +2 -2
  12. package/lib/commonjs/loading/index.web.js +20 -0
  13. package/lib/commonjs/loading/index.web.js.map +1 -0
  14. package/lib/commonjs/theme/apply-dark-class.js +6 -1
  15. package/lib/commonjs/theme/apply-dark-class.js.map +1 -1
  16. package/lib/module/index.js +1 -1
  17. package/lib/module/index.js.map +1 -1
  18. package/lib/module/index.web.js +1 -1
  19. package/lib/module/index.web.js.map +1 -1
  20. package/lib/module/loading/Loading.js +1 -1
  21. package/lib/module/loading/Loading.js.map +1 -1
  22. package/lib/module/loading/Loading.web.js +232 -0
  23. package/lib/module/loading/Loading.web.js.map +1 -0
  24. package/lib/module/loading/SpinnerIcon.web.js +138 -0
  25. package/lib/module/loading/SpinnerIcon.web.js.map +1 -0
  26. package/lib/module/loading/index.js +2 -2
  27. package/lib/module/loading/index.js.map +1 -1
  28. package/lib/module/loading/index.web.js +18 -0
  29. package/lib/module/loading/index.web.js.map +1 -0
  30. package/lib/module/theme/apply-dark-class.js +6 -1
  31. package/lib/module/theme/apply-dark-class.js.map +1 -1
  32. package/lib/typescript/commonjs/index.web.d.ts +1 -1
  33. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/loading/Loading.web.d.ts +4 -0
  35. package/lib/typescript/commonjs/loading/Loading.web.d.ts.map +1 -0
  36. package/lib/typescript/commonjs/loading/SpinnerIcon.web.d.ts +17 -0
  37. package/lib/typescript/commonjs/loading/SpinnerIcon.web.d.ts.map +1 -0
  38. package/lib/typescript/commonjs/loading/index.web.d.ts +4 -0
  39. package/lib/typescript/commonjs/loading/index.web.d.ts.map +1 -0
  40. package/lib/typescript/commonjs/theme/apply-dark-class.d.ts +5 -0
  41. package/lib/typescript/commonjs/theme/apply-dark-class.d.ts.map +1 -1
  42. package/lib/typescript/module/index.web.d.ts +1 -1
  43. package/lib/typescript/module/index.web.d.ts.map +1 -1
  44. package/lib/typescript/module/loading/Loading.web.d.ts +4 -0
  45. package/lib/typescript/module/loading/Loading.web.d.ts.map +1 -0
  46. package/lib/typescript/module/loading/SpinnerIcon.web.d.ts +17 -0
  47. package/lib/typescript/module/loading/SpinnerIcon.web.d.ts.map +1 -0
  48. package/lib/typescript/module/loading/index.web.d.ts +4 -0
  49. package/lib/typescript/module/loading/index.web.d.ts.map +1 -0
  50. package/lib/typescript/module/theme/apply-dark-class.d.ts +5 -0
  51. package/lib/typescript/module/theme/apply-dark-class.d.ts.map +1 -1
  52. package/package.json +6 -1
  53. package/src/index.web.ts +1 -1
  54. package/src/loading/Loading.web.tsx +244 -0
  55. package/src/loading/SpinnerIcon.web.tsx +134 -0
  56. package/src/loading/index.web.ts +24 -0
  57. 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.11",
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, `hsl(${value})`);
29
+ root.setProperty(key, value);
25
30
  }
26
31
  }