@oxyhq/bloom 0.1.21 → 0.1.23

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 (50) hide show
  1. package/lib/commonjs/button/Button.js +6 -4
  2. package/lib/commonjs/button/Button.js.map +1 -1
  3. package/lib/commonjs/loading/Loading.js +6 -4
  4. package/lib/commonjs/loading/Loading.js.map +1 -1
  5. package/lib/commonjs/loading/SpinnerIcon.js +6 -2
  6. package/lib/commonjs/loading/SpinnerIcon.js.map +1 -1
  7. package/lib/commonjs/theme/BloomThemeProvider.js +90 -54
  8. package/lib/commonjs/theme/BloomThemeProvider.js.map +1 -1
  9. package/lib/commonjs/theme/index.js +12 -0
  10. package/lib/commonjs/theme/index.js.map +1 -1
  11. package/lib/module/button/Button.js +6 -4
  12. package/lib/module/button/Button.js.map +1 -1
  13. package/lib/module/loading/Loading.js +6 -4
  14. package/lib/module/loading/Loading.js.map +1 -1
  15. package/lib/module/loading/SpinnerIcon.js +6 -2
  16. package/lib/module/loading/SpinnerIcon.js.map +1 -1
  17. package/lib/module/theme/BloomThemeProvider.js +89 -55
  18. package/lib/module/theme/BloomThemeProvider.js.map +1 -1
  19. package/lib/module/theme/index.js +1 -1
  20. package/lib/module/theme/index.js.map +1 -1
  21. package/lib/typescript/commonjs/button/types.d.ts +1 -0
  22. package/lib/typescript/commonjs/button/types.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/loading/Loading.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/loading/SpinnerIcon.d.ts +1 -0
  25. package/lib/typescript/commonjs/loading/SpinnerIcon.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/loading/types.d.ts +2 -0
  27. package/lib/typescript/commonjs/loading/types.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts +13 -0
  29. package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/theme/index.d.ts +2 -2
  31. package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
  32. package/lib/typescript/module/button/types.d.ts +1 -0
  33. package/lib/typescript/module/button/types.d.ts.map +1 -1
  34. package/lib/typescript/module/loading/Loading.d.ts.map +1 -1
  35. package/lib/typescript/module/loading/SpinnerIcon.d.ts +1 -0
  36. package/lib/typescript/module/loading/SpinnerIcon.d.ts.map +1 -1
  37. package/lib/typescript/module/loading/types.d.ts +2 -0
  38. package/lib/typescript/module/loading/types.d.ts.map +1 -1
  39. package/lib/typescript/module/theme/BloomThemeProvider.d.ts +13 -0
  40. package/lib/typescript/module/theme/BloomThemeProvider.d.ts.map +1 -1
  41. package/lib/typescript/module/theme/index.d.ts +2 -2
  42. package/lib/typescript/module/theme/index.d.ts.map +1 -1
  43. package/package.json +12 -1
  44. package/src/button/Button.tsx +3 -3
  45. package/src/button/types.ts +1 -0
  46. package/src/loading/Loading.tsx +4 -3
  47. package/src/loading/SpinnerIcon.tsx +10 -4
  48. package/src/loading/types.ts +2 -0
  49. package/src/theme/BloomThemeProvider.tsx +104 -67
  50. package/src/theme/index.ts +2 -2
@@ -22,4 +22,5 @@ export interface ButtonProps {
22
22
  hitSlop?: { top: number; bottom: number; left: number; right: number };
23
23
  activeOpacity?: number;
24
24
  testID?: string;
25
+ className?: string;
25
26
  }
@@ -38,6 +38,7 @@ function getReanimated(): ReanimatedType | null {
38
38
  const SpinnerLoading: React.FC<SpinnerLoadingProps> = ({
39
39
  size = 'medium',
40
40
  color,
41
+ className,
41
42
  text,
42
43
  textStyle,
43
44
  style,
@@ -49,12 +50,12 @@ const SpinnerLoading: React.FC<SpinnerLoadingProps> = ({
49
50
  const theme = useTheme();
50
51
  const sizeConfig = SIZE_CONFIG[size];
51
52
  const effectiveIconSize = iconSize ?? sizeConfig.spinner;
52
- const spinnerColor = color ?? theme.colors.primary;
53
+ const spinnerColor = className ? 'currentColor' : (color ?? theme.colors.primary);
53
54
  const textColor = color ?? theme.colors.textSecondary;
54
55
 
55
56
  return (
56
57
  <View style={[styles.container, style]} testID={testID}>
57
- {spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} />}
58
+ {spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} className={className} />}
58
59
  {showText && text && (
59
60
  <Text
60
61
  style={[
@@ -262,9 +263,9 @@ Loading.displayName = 'Loading';
262
263
 
263
264
  const styles = StyleSheet.create({
264
265
  container: {
265
- flex: 1,
266
266
  alignItems: 'center',
267
267
  justifyContent: 'center',
268
+ padding: 16,
268
269
  },
269
270
  text: {
270
271
  textAlign: 'center',
@@ -38,10 +38,12 @@ function getReanimated(): ReanimatedType | null {
38
38
  interface SpinnerIconProps {
39
39
  size?: number;
40
40
  color?: string;
41
+ className?: string;
41
42
  style?: ViewStyle;
42
43
  }
43
44
 
44
- type AnimatedSpinnerProps = SpinnerIconProps & {
45
+ type AnimatedSpinnerProps = Omit<SpinnerIconProps, 'className'> & {
46
+ className?: string;
45
47
  svg: NonNullable<SvgModuleType>;
46
48
  reanimated: NonNullable<ReanimatedType>;
47
49
  };
@@ -51,8 +53,9 @@ type AnimatedSpinnerProps = SpinnerIconProps & {
51
53
  * Only rendered when both react-native-svg and react-native-reanimated are available.
52
54
  */
53
55
  const AnimatedSpinner: React.FC<AnimatedSpinnerProps> = ({
54
- color = '#005c67',
56
+ color = 'currentColor',
55
57
  size = 26,
58
+ className,
56
59
  style,
57
60
  svg,
58
61
  reanimated,
@@ -97,7 +100,8 @@ const AnimatedSpinner: React.FC<AnimatedSpinnerProps> = ({
97
100
  style,
98
101
  ]}
99
102
  >
100
- <Svg viewBox="0 0 100 100" width={size} height={size}>
103
+ {/* @ts-expect-error className is added by NativeWind cssInterop at runtime */}
104
+ <Svg viewBox="0 0 100 100" width={size} height={size} className={className}>
101
105
  <Rect fill={color} height="10" opacity="0" rx="5" ry="5" transform="rotate(-90 50 50)" width="28" x="67" y="45" />
102
106
  <Rect fill={color} height="10" opacity="0.125" rx="5" ry="5" transform="rotate(-45 50 50)" width="28" x="67" y="45" />
103
107
  <Rect fill={color} height="10" opacity="0.25" rx="5" ry="5" transform="rotate(0 50 50)" width="28" x="67" y="45" />
@@ -117,8 +121,9 @@ const AnimatedSpinner: React.FC<AnimatedSpinnerProps> = ({
117
121
  * Falls back to ActivityIndicator if either is missing.
118
122
  */
119
123
  export const SpinnerIcon: React.FC<SpinnerIconProps> = ({
120
- color = '#005c67',
124
+ color = 'currentColor',
121
125
  size = 26,
126
+ className,
122
127
  style,
123
128
  }) => {
124
129
  const svg = getSvgModule();
@@ -132,6 +137,7 @@ export const SpinnerIcon: React.FC<SpinnerIconProps> = ({
132
137
  <AnimatedSpinner
133
138
  color={color}
134
139
  size={size}
140
+ className={className}
135
141
  style={style}
136
142
  svg={svg}
137
143
  reanimated={reanimated}
@@ -11,6 +11,8 @@ interface BaseLoadingProps {
11
11
  size?: LoadingSize;
12
12
  /** Custom color (defaults to theme primary) */
13
13
  color?: string;
14
+ /** NativeWind className for spinner color (e.g. "text-primary"). Overrides color prop. */
15
+ className?: string;
14
16
  /** Custom container style */
15
17
  style?: ViewStyle;
16
18
  /** Whether loading is active (for animated variants) */
@@ -1,4 +1,4 @@
1
- import React, { createContext, useCallback, useEffect, useMemo } from 'react';
1
+ import React, { createContext, useCallback, useContext, useEffect, useMemo } from 'react';
2
2
  import { useColorScheme as useRNColorScheme, Platform } from 'react-native';
3
3
  import { APP_COLOR_PRESETS, type AppColorName } from './color-presets';
4
4
  import { getAdaptiveColors } from './adaptive-colors';
@@ -23,6 +23,78 @@ function hsl(h: number, s: number, l: number): string {
23
23
  return `hsl(${h}, ${s}%, ${l}%)`;
24
24
  }
25
25
 
26
+ /** Build a Theme object from a color preset name and resolved light/dark mode. */
27
+ export function buildTheme(appColor: AppColorName, resolved: 'light' | 'dark', isAdaptive: boolean = false): Theme {
28
+ const isDark = resolved === 'dark';
29
+
30
+ let themeColors: ThemeColors | undefined;
31
+
32
+ if (isAdaptive && Platform.OS !== 'web') {
33
+ const adaptive = getAdaptiveColors();
34
+ if (adaptive) {
35
+ themeColors = adaptive;
36
+ }
37
+ }
38
+
39
+ if (!themeColors) {
40
+ const preset = APP_COLOR_PRESETS[appColor];
41
+ const vars = resolved === 'light' ? preset.light : preset.dark;
42
+ const primaryHue = extractHue(vars['--primary']!);
43
+ const destructiveHue = extractHue(vars['--destructive']!);
44
+
45
+ const surface = hslVarToCSS(vars['--surface']!);
46
+ const background = hslVarToCSS(vars['--background']!);
47
+ const mutedForeground = hslVarToCSS(vars['--muted-foreground']!);
48
+
49
+ themeColors = {
50
+ background,
51
+ backgroundSecondary: surface,
52
+ backgroundTertiary: hslVarToCSS(vars['--muted']!),
53
+
54
+ text: hslVarToCSS(vars['--foreground']!),
55
+ textSecondary: mutedForeground,
56
+ textTertiary: mutedForeground,
57
+
58
+ border: hslVarToCSS(vars['--border']!),
59
+ borderLight: hslVarToCSS(vars['--input']!),
60
+
61
+ primary: preset.hex,
62
+ primaryLight: surface,
63
+ primaryDark: background,
64
+
65
+ secondary: preset.hex,
66
+
67
+ tint: preset.hex,
68
+ icon: mutedForeground,
69
+ iconActive: preset.hex,
70
+
71
+ success: '#10B981',
72
+ error: '#EF4444',
73
+ warning: '#F59E0B',
74
+ info: '#3B82F6',
75
+
76
+ primarySubtle: isDark ? hsl(primaryHue, 50, 10) : hsl(primaryHue, 70, 93),
77
+ primarySubtleForeground: isDark ? hsl(primaryHue, 70, 65) : hsl(primaryHue, 90, 25),
78
+ negative: hsl(destructiveHue, 84, 45),
79
+ negativeForeground: '#FFFFFF',
80
+ negativeSubtle: isDark ? hsl(destructiveHue, 50, 10) : hsl(destructiveHue, 90, 95),
81
+ negativeSubtleForeground: isDark ? hsl(destructiveHue, 70, 65) : hsl(destructiveHue, 80, 40),
82
+ contrast50: isDark ? hsl(primaryHue, 15, 12) : hsl(primaryHue, 10, 93),
83
+
84
+ card: surface,
85
+ shadow: isDark ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.1)',
86
+ overlay: 'rgba(0, 0, 0, 0.5)',
87
+ };
88
+ }
89
+
90
+ return {
91
+ mode: resolved,
92
+ colors: themeColors,
93
+ isDark,
94
+ isLight: !isDark,
95
+ };
96
+ }
97
+
26
98
  export interface BloomThemeContextValue {
27
99
  theme: Theme;
28
100
  mode: ThemeMode;
@@ -80,77 +152,42 @@ export function BloomThemeProvider({
80
152
  );
81
153
 
82
154
  const contextValue = useMemo<BloomThemeContextValue>(() => {
83
- const isDark = resolved === 'dark';
155
+ const theme = buildTheme(appColor, resolved, isAdaptive);
156
+ return { theme, mode, colorPreset: appColor, setMode, setColorPreset };
157
+ }, [resolved, appColor, isAdaptive, mode, setMode, setColorPreset]);
84
158
 
85
- let themeColors: ThemeColors;
159
+ return (
160
+ <BloomThemeContext.Provider value={contextValue}>
161
+ {children}
162
+ </BloomThemeContext.Provider>
163
+ );
164
+ }
86
165
 
87
- if (isAdaptive && Platform.OS !== 'web') {
88
- const adaptive = getAdaptiveColors();
89
- if (adaptive) {
90
- themeColors = adaptive;
91
- }
92
- }
166
+ /**
167
+ * Scoped color override for a subtree.
168
+ * Inherits mode/dark from the parent BloomThemeProvider but overrides the color preset.
169
+ * Use this to tint a section of the UI (e.g. a visited user's profile) without
170
+ * affecting the rest of the app.
171
+ */
172
+ export interface BloomColorScopeProps {
173
+ colorPreset: AppColorName;
174
+ children: React.ReactNode;
175
+ }
93
176
 
94
- if (!themeColors!) {
95
- const preset = APP_COLOR_PRESETS[appColor];
96
- const vars = resolved === 'light' ? preset.light : preset.dark;
97
- const primaryHue = extractHue(vars['--primary']!);
98
- const destructiveHue = extractHue(vars['--destructive']!);
99
-
100
- const surface = hslVarToCSS(vars['--surface']!);
101
- const background = hslVarToCSS(vars['--background']!);
102
- const mutedForeground = hslVarToCSS(vars['--muted-foreground']!);
103
-
104
- themeColors = {
105
- background,
106
- backgroundSecondary: surface,
107
- backgroundTertiary: hslVarToCSS(vars['--muted']!),
108
-
109
- text: hslVarToCSS(vars['--foreground']!),
110
- textSecondary: mutedForeground,
111
- textTertiary: mutedForeground,
112
-
113
- border: hslVarToCSS(vars['--border']!),
114
- borderLight: hslVarToCSS(vars['--input']!),
115
-
116
- primary: preset.hex,
117
- primaryLight: surface,
118
- primaryDark: background,
119
-
120
- secondary: preset.hex,
121
-
122
- tint: preset.hex,
123
- icon: mutedForeground,
124
- iconActive: preset.hex,
125
-
126
- success: '#10B981',
127
- error: '#EF4444',
128
- warning: '#F59E0B',
129
- info: '#3B82F6',
130
-
131
- primarySubtle: isDark ? hsl(primaryHue, 50, 10) : hsl(primaryHue, 70, 93),
132
- primarySubtleForeground: isDark ? hsl(primaryHue, 70, 65) : hsl(primaryHue, 90, 25),
133
- negative: hsl(destructiveHue, 84, 45),
134
- negativeForeground: '#FFFFFF',
135
- negativeSubtle: isDark ? hsl(destructiveHue, 50, 10) : hsl(destructiveHue, 90, 95),
136
- negativeSubtleForeground: isDark ? hsl(destructiveHue, 70, 65) : hsl(destructiveHue, 80, 40),
137
- contrast50: isDark ? hsl(primaryHue, 15, 12) : hsl(primaryHue, 10, 93),
138
-
139
- card: surface,
140
- shadow: isDark ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.1)',
141
- overlay: 'rgba(0, 0, 0, 0.5)',
142
- };
143
- }
177
+ export function BloomColorScope({ colorPreset, children }: BloomColorScopeProps) {
178
+ const parent = useContext(BloomThemeContext);
179
+ if (!parent) {
180
+ throw new Error('BloomColorScope must be used within a <BloomThemeProvider>');
181
+ }
144
182
 
145
- const theme: Theme = {
146
- mode: resolved,
147
- colors: themeColors,
148
- isDark,
149
- isLight: !isDark,
183
+ const contextValue = useMemo<BloomThemeContextValue>(() => {
184
+ const theme = buildTheme(colorPreset, parent.theme.mode as 'light' | 'dark');
185
+ return {
186
+ ...parent,
187
+ theme,
188
+ colorPreset,
150
189
  };
151
-
152
- return { theme, mode, colorPreset: appColor, setMode, setColorPreset };
153
- }, [resolved, appColor, isAdaptive, mode, setMode, setColorPreset]);
190
+ }, [colorPreset, parent]);
154
191
 
155
192
  return (
156
193
  <BloomThemeContext.Provider value={contextValue}>
@@ -1,5 +1,5 @@
1
- export { BloomThemeProvider } from './BloomThemeProvider';
2
- export type { BloomThemeProviderProps, BloomThemeContextValue } from './BloomThemeProvider';
1
+ export { BloomThemeProvider, BloomColorScope, buildTheme } from './BloomThemeProvider';
2
+ export type { BloomThemeProviderProps, BloomThemeContextValue, BloomColorScopeProps } from './BloomThemeProvider';
3
3
  export { useTheme, useThemeColor, useBloomTheme } from './use-theme';
4
4
  export type { Theme, ThemeColors, ThemeMode } from './types';
5
5
  export type { AppColorName, AppColorPreset } from './color-presets';