@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.
- package/lib/commonjs/button/Button.js +6 -4
- package/lib/commonjs/button/Button.js.map +1 -1
- package/lib/commonjs/loading/Loading.js +6 -4
- package/lib/commonjs/loading/Loading.js.map +1 -1
- package/lib/commonjs/loading/SpinnerIcon.js +6 -2
- package/lib/commonjs/loading/SpinnerIcon.js.map +1 -1
- package/lib/commonjs/theme/BloomThemeProvider.js +90 -54
- package/lib/commonjs/theme/BloomThemeProvider.js.map +1 -1
- package/lib/commonjs/theme/index.js +12 -0
- package/lib/commonjs/theme/index.js.map +1 -1
- package/lib/module/button/Button.js +6 -4
- package/lib/module/button/Button.js.map +1 -1
- package/lib/module/loading/Loading.js +6 -4
- package/lib/module/loading/Loading.js.map +1 -1
- package/lib/module/loading/SpinnerIcon.js +6 -2
- package/lib/module/loading/SpinnerIcon.js.map +1 -1
- package/lib/module/theme/BloomThemeProvider.js +89 -55
- package/lib/module/theme/BloomThemeProvider.js.map +1 -1
- package/lib/module/theme/index.js +1 -1
- package/lib/module/theme/index.js.map +1 -1
- package/lib/typescript/commonjs/button/types.d.ts +1 -0
- package/lib/typescript/commonjs/button/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/Loading.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/SpinnerIcon.d.ts +1 -0
- package/lib/typescript/commonjs/loading/SpinnerIcon.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/types.d.ts +2 -0
- package/lib/typescript/commonjs/loading/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts +13 -0
- package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/index.d.ts +2 -2
- package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
- package/lib/typescript/module/button/types.d.ts +1 -0
- package/lib/typescript/module/button/types.d.ts.map +1 -1
- package/lib/typescript/module/loading/Loading.d.ts.map +1 -1
- package/lib/typescript/module/loading/SpinnerIcon.d.ts +1 -0
- package/lib/typescript/module/loading/SpinnerIcon.d.ts.map +1 -1
- package/lib/typescript/module/loading/types.d.ts +2 -0
- package/lib/typescript/module/loading/types.d.ts.map +1 -1
- package/lib/typescript/module/theme/BloomThemeProvider.d.ts +13 -0
- package/lib/typescript/module/theme/BloomThemeProvider.d.ts.map +1 -1
- package/lib/typescript/module/theme/index.d.ts +2 -2
- package/lib/typescript/module/theme/index.d.ts.map +1 -1
- package/package.json +12 -1
- package/src/button/Button.tsx +3 -3
- package/src/button/types.ts +1 -0
- package/src/loading/Loading.tsx +4 -3
- package/src/loading/SpinnerIcon.tsx +10 -4
- package/src/loading/types.ts +2 -0
- package/src/theme/BloomThemeProvider.tsx +104 -67
- package/src/theme/index.ts +2 -2
package/src/button/types.ts
CHANGED
package/src/loading/Loading.tsx
CHANGED
|
@@ -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 = '
|
|
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
|
-
|
|
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 = '
|
|
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}
|
package/src/loading/types.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
159
|
+
return (
|
|
160
|
+
<BloomThemeContext.Provider value={contextValue}>
|
|
161
|
+
{children}
|
|
162
|
+
</BloomThemeContext.Provider>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
86
165
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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}>
|
package/src/theme/index.ts
CHANGED
|
@@ -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';
|