@tcbs/react-native-mazic-ui 0.1.1 → 0.1.2
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/package.json +3 -3
- package/src/components/TcbsButton.tsx +36 -15
- package/src/components/TcbsButton.types.ts +3 -0
- package/src/components/ThemeModal.tsx +123 -0
- package/src/index.ts +1 -0
- package/src/store/themeStore.ts +291 -34
package/package.json
CHANGED
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
"react-native-vector-icons"
|
|
14
14
|
],
|
|
15
15
|
"author": "TechCraft By Subrata <subraatakumar@gmail.com>",
|
|
16
|
-
"version": "0.1.
|
|
17
|
-
|
|
16
|
+
"version": "0.1.2",
|
|
17
|
+
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
20
|
"main": "lib/commonjs/index.js",
|
|
21
21
|
"module": "lib/module/index.js",
|
|
22
|
-
"types": "lib/
|
|
22
|
+
"types": "lib/index.d.ts",
|
|
23
23
|
"react-native": "src/index.ts",
|
|
24
24
|
"files": [
|
|
25
25
|
"lib/",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
+
import { Appearance } from 'react-native';
|
|
2
3
|
import { TouchableOpacity, Text, View , StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
3
4
|
import AntDesign from 'react-native-vector-icons/AntDesign';
|
|
4
5
|
import Feather from 'react-native-vector-icons/Feather';
|
|
@@ -33,7 +34,8 @@ const FONT_SIZES: Record<ButtonSize, number> = {
|
|
|
33
34
|
[BUTTON_SIZE.SMALL]: 14,
|
|
34
35
|
};
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
// Support for BORDER_RADIUS.NONE and BORDER_RADIUS.FULL (50%)
|
|
38
|
+
const BORDER_RADIUSES: Record<ButtonSize, number | string> = {
|
|
37
39
|
[BUTTON_SIZE.LARGE]: BORDER_RADIUS.MEDIUM,
|
|
38
40
|
[BUTTON_SIZE.MEDIUM]: BORDER_RADIUS.SMALL,
|
|
39
41
|
[BUTTON_SIZE.SMALL]: BORDER_RADIUS.SMALL,
|
|
@@ -72,25 +74,35 @@ export const TcbsButton: React.FC<TcbsButtonProps> = ({
|
|
|
72
74
|
accessibilityHint,
|
|
73
75
|
accessibilityRole = 'button',
|
|
74
76
|
accessibilityState,
|
|
75
|
-
themeColor,
|
|
76
|
-
screenBgColor,
|
|
77
77
|
}) => {
|
|
78
|
-
// Use
|
|
79
|
-
const {
|
|
80
|
-
const effectiveThemeColor =
|
|
78
|
+
// Use themeColors from store if not provided as prop
|
|
79
|
+
const { themeColors, tcbsTheme } = useTcbsColorStore();
|
|
80
|
+
const effectiveThemeColor = themeColors;
|
|
81
81
|
// Normalize colors: if only one color is set, use it for all
|
|
82
82
|
const normalizedColors = {
|
|
83
83
|
btnColor: effectiveThemeColor?.btnColor ?? effectiveThemeColor?.themeColor ?? '#007AFF',
|
|
84
84
|
btnBorderColor: effectiveThemeColor?.btnBorderColor ?? effectiveThemeColor?.btnColor ?? '#007AFF',
|
|
85
85
|
btnIconColor: effectiveThemeColor?.btnIconColor,
|
|
86
|
-
btnTextColor: effectiveThemeColor?.btnTextColor ?? effectiveThemeColor?.btnTxtColor
|
|
86
|
+
btnTextColor: effectiveThemeColor?.btnTextColor ?? effectiveThemeColor?.btnTxtColor,
|
|
87
87
|
themeColor: effectiveThemeColor?.themeColor ?? effectiveThemeColor?.btnColor ?? '#007AFF',
|
|
88
88
|
};
|
|
89
89
|
|
|
90
90
|
const buttonStyle = useMemo<StyleProp<ViewStyle>>(() => {
|
|
91
|
+
const height = HEIGHTS[size];
|
|
92
|
+
let computedBorderRadius: number | string;
|
|
93
|
+
if (borderRadius === BORDER_RADIUS.NONE) {
|
|
94
|
+
computedBorderRadius = 0;
|
|
95
|
+
} else if (borderRadius === BORDER_RADIUS.FULL) {
|
|
96
|
+
computedBorderRadius = height / 2;
|
|
97
|
+
} else if (borderRadius !== undefined) {
|
|
98
|
+
computedBorderRadius = borderRadius;
|
|
99
|
+
} else {
|
|
100
|
+
computedBorderRadius = BORDER_RADIUSES[size];
|
|
101
|
+
}
|
|
102
|
+
|
|
91
103
|
const baseStyle: ViewStyle = {
|
|
92
|
-
height
|
|
93
|
-
borderRadius:
|
|
104
|
+
height,
|
|
105
|
+
borderRadius: computedBorderRadius,
|
|
94
106
|
alignItems: 'center',
|
|
95
107
|
justifyContent: 'center',
|
|
96
108
|
opacity: disabled ? 0.6 : 1,
|
|
@@ -129,10 +141,20 @@ export const TcbsButton: React.FC<TcbsButtonProps> = ({
|
|
|
129
141
|
}, [size, variant, normalizedColors, style, disabled, borderRadius]);
|
|
130
142
|
|
|
131
143
|
const themedTextStyle = useMemo<TextStyle>(() => {
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
let baseTextColor;
|
|
145
|
+
if (variant === BUTTON_VARIANT.PRIMARY) {
|
|
146
|
+
baseTextColor = normalizedColors.btnTextColor || '#FFFFFF';
|
|
147
|
+
} else if (variant === BUTTON_VARIANT.NO_BORDER) {
|
|
148
|
+
let colorScheme = tcbsTheme;
|
|
149
|
+
if (tcbsTheme === 'system') {
|
|
150
|
+
colorScheme = Appearance.getColorScheme() || 'light';
|
|
151
|
+
}
|
|
152
|
+
baseTextColor = colorScheme === 'dark'
|
|
134
153
|
? normalizedColors.btnTextColor || '#FFFFFF'
|
|
135
|
-
:
|
|
154
|
+
: normalizedColors?.btnColor || '#007AFF';
|
|
155
|
+
} else {
|
|
156
|
+
baseTextColor = normalizedColors?.btnColor || '#FFFFFF';
|
|
157
|
+
}
|
|
136
158
|
|
|
137
159
|
return {
|
|
138
160
|
color: baseTextColor,
|
|
@@ -140,13 +162,13 @@ export const TcbsButton: React.FC<TcbsButtonProps> = ({
|
|
|
140
162
|
fontWeight: '700',
|
|
141
163
|
...(textStyle as TextStyle),
|
|
142
164
|
};
|
|
143
|
-
}, [size, variant, normalizedColors, textStyle]);
|
|
165
|
+
}, [size, variant, normalizedColors, textStyle, tcbsTheme]);
|
|
144
166
|
|
|
145
167
|
const renderIcon = (IconComponent: IconComponentType) => (
|
|
146
168
|
<IconComponent
|
|
147
169
|
name={iconName!}
|
|
148
170
|
size={iconSize || FONT_SIZES[size] * 2}
|
|
149
|
-
color={iconColor ||
|
|
171
|
+
color={iconColor || themedTextStyle.color}
|
|
150
172
|
style={
|
|
151
173
|
iconPosition === 'top'
|
|
152
174
|
? { marginBottom: 2 }
|
|
@@ -230,6 +252,5 @@ export const TcbsButton: React.FC<TcbsButtonProps> = ({
|
|
|
230
252
|
);
|
|
231
253
|
};
|
|
232
254
|
|
|
233
|
-
// Export constants for use in consuming applications
|
|
234
255
|
export { BUTTON_SIZE, BUTTON_VARIANT, BORDER_RADIUS };
|
|
235
256
|
export type { ButtonSize, ButtonVariant, IconGroupType, IconPosition };
|
|
@@ -17,12 +17,15 @@ export const BUTTON_VARIANT = {
|
|
|
17
17
|
PRIMARY: 'primary',
|
|
18
18
|
SECONDARY: 'secondary',
|
|
19
19
|
NO_BORDER: 'no_border',
|
|
20
|
+
|
|
20
21
|
} as const;
|
|
21
22
|
|
|
22
23
|
export const BORDER_RADIUS = {
|
|
23
24
|
SMALL: 8,
|
|
24
25
|
MEDIUM: 12,
|
|
25
26
|
LARGE: 16,
|
|
27
|
+
NONE: 0,
|
|
28
|
+
FULL: '50%',
|
|
26
29
|
} as const;
|
|
27
30
|
|
|
28
31
|
export type ButtonSize = (typeof BUTTON_SIZE)[keyof typeof BUTTON_SIZE];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Modal, Pressable, TouchableOpacity, View, Text, StyleSheet } from 'react-native';
|
|
3
|
+
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
|
4
|
+
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
5
|
+
import Entypo from 'react-native-vector-icons/Entypo';
|
|
6
|
+
import { useTcbsColorStore } from '../store/themeStore';
|
|
7
|
+
import { BUTTON_VARIANT, TcbsButton } from './TcbsButton';
|
|
8
|
+
|
|
9
|
+
export interface ThemeModalProps {
|
|
10
|
+
visible: boolean;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ThemeModal: React.FC<ThemeModalProps> = ({ visible, onClose }) => {
|
|
15
|
+
const { tcbsTheme, setTcbsTheme, themeColors } = useTcbsColorStore();
|
|
16
|
+
// You can customize these colors or get them from your theme
|
|
17
|
+
const colors = {
|
|
18
|
+
menuCardBkgColor: themeColors.screenBgColor || '#fff',
|
|
19
|
+
textDark: themeColors.btnTextColor || '#222',
|
|
20
|
+
textGray: '#888',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Modal
|
|
25
|
+
transparent={true}
|
|
26
|
+
animationType="fade"
|
|
27
|
+
visible={visible}
|
|
28
|
+
onRequestClose={onClose}
|
|
29
|
+
>
|
|
30
|
+
<Pressable style={styles.modalOverlay} onPress={onClose}>
|
|
31
|
+
<Pressable style={[styles.modalCard, { backgroundColor: colors.menuCardBkgColor }]}
|
|
32
|
+
onPress={() => {}} // Prevent closing when pressing inside card
|
|
33
|
+
>
|
|
34
|
+
<View style={styles.modalClose}>
|
|
35
|
+
<TcbsButton
|
|
36
|
+
onPress={onClose}
|
|
37
|
+
iconName="close"
|
|
38
|
+
iconPosition="left"
|
|
39
|
+
variant={BUTTON_VARIANT.NO_BORDER}
|
|
40
|
+
iconSize={22}
|
|
41
|
+
accessibilityLabel="Close"
|
|
42
|
+
style={{ padding: 8, marginRight: 0, minWidth: 0, minHeight: 0, alignSelf: 'flex-end' }}
|
|
43
|
+
/>
|
|
44
|
+
</View>
|
|
45
|
+
<Text style={[styles.modalTitle, { color: colors.textDark }]}>Theme</Text>
|
|
46
|
+
<Text style={[styles.modalSubtitle, { color: colors.textGray }]}>Choose how the app looks on this device.</Text>
|
|
47
|
+
<View style={{ marginTop: 18 }}>
|
|
48
|
+
<TcbsButton
|
|
49
|
+
title="Light"
|
|
50
|
+
onPress={() => setTcbsTheme('light')}
|
|
51
|
+
style={{ marginBottom: 8 }}
|
|
52
|
+
variant={tcbsTheme === 'light' ? 'primary' : 'secondary'}
|
|
53
|
+
iconGroup="Ionicons"
|
|
54
|
+
iconName="sunny"
|
|
55
|
+
iconPosition="left"
|
|
56
|
+
textStyle={{ flex: 1, textAlign: 'center' }}
|
|
57
|
+
/>
|
|
58
|
+
<TcbsButton
|
|
59
|
+
title="Dark"
|
|
60
|
+
onPress={() => setTcbsTheme('dark')}
|
|
61
|
+
style={{ marginBottom: 8 }}
|
|
62
|
+
variant={tcbsTheme === 'dark' ? 'primary' : 'secondary'}
|
|
63
|
+
iconGroup="Ionicons"
|
|
64
|
+
iconName="moon"
|
|
65
|
+
iconPosition="left"
|
|
66
|
+
textStyle={{ flex: 1, textAlign: 'center' }}
|
|
67
|
+
/>
|
|
68
|
+
<TcbsButton
|
|
69
|
+
title="System (default)"
|
|
70
|
+
onPress={() => setTcbsTheme('system')}
|
|
71
|
+
variant={tcbsTheme === 'system' ? 'primary' : 'secondary'}
|
|
72
|
+
iconGroup="Ionicons"
|
|
73
|
+
iconName="settings"
|
|
74
|
+
iconPosition="left"
|
|
75
|
+
textStyle={{ flex: 1, textAlign: 'center' }}
|
|
76
|
+
/>
|
|
77
|
+
</View>
|
|
78
|
+
</Pressable>
|
|
79
|
+
</Pressable>
|
|
80
|
+
</Modal>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const styles = StyleSheet.create({
|
|
85
|
+
modalOverlay: {
|
|
86
|
+
flex: 1,
|
|
87
|
+
backgroundColor: 'rgba(0,0,0,0.3)',
|
|
88
|
+
justifyContent: 'center',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
},
|
|
91
|
+
modalCard: {
|
|
92
|
+
minWidth: 300,
|
|
93
|
+
borderRadius: 16,
|
|
94
|
+
padding: 24,
|
|
95
|
+
alignItems: 'stretch',
|
|
96
|
+
shadowColor: '#000',
|
|
97
|
+
shadowOpacity: 0.15,
|
|
98
|
+
shadowRadius: 12,
|
|
99
|
+
shadowOffset: { width: 0, height: 4 },
|
|
100
|
+
elevation: 4,
|
|
101
|
+
},
|
|
102
|
+
modalClose: {
|
|
103
|
+
position: 'absolute',
|
|
104
|
+
top: 8,
|
|
105
|
+
right: 8,
|
|
106
|
+
zIndex: 2,
|
|
107
|
+
},
|
|
108
|
+
modalTitle: {
|
|
109
|
+
fontSize: 20,
|
|
110
|
+
fontWeight: '700',
|
|
111
|
+
marginTop: 8,
|
|
112
|
+
marginBottom: 2,
|
|
113
|
+
textAlign: 'center',
|
|
114
|
+
},
|
|
115
|
+
modalSubtitle: {
|
|
116
|
+
fontSize: 14,
|
|
117
|
+
fontWeight: '400',
|
|
118
|
+
marginBottom: 8,
|
|
119
|
+
textAlign: 'center',
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export default ThemeModal;
|
package/src/index.ts
CHANGED
package/src/store/themeStore.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
|
+
import { Appearance } from 'react-native';
|
|
3
|
+
import { MMKV } from 'react-native-mmkv';
|
|
4
|
+
|
|
5
|
+
// MMKV instance for theme persistence
|
|
6
|
+
const storage = new MMKV();
|
|
7
|
+
const THEME_KEY = 'tcbsTheme';
|
|
8
|
+
|
|
9
|
+
// Store the listener subscription so we can remove it
|
|
10
|
+
let appearanceListener: { remove: () => void } | null = null;
|
|
2
11
|
|
|
3
12
|
export type ThemeColor = {
|
|
4
13
|
btnColor: string;
|
|
@@ -6,10 +15,30 @@ export type ThemeColor = {
|
|
|
6
15
|
btnIconColor?: string;
|
|
7
16
|
themeColor: string;
|
|
8
17
|
btnTextColor: string;
|
|
18
|
+
tabBarIconActiveColor?: string;
|
|
19
|
+
tabBarIconInactiveColor?: string;
|
|
20
|
+
primaryColor?: string;
|
|
21
|
+
secondaryColor?: string;
|
|
22
|
+
tertiaryColor?: string;
|
|
9
23
|
screenBgColor?: string;
|
|
24
|
+
modalBgColor?: string;
|
|
25
|
+
modalHeaderBgColor?: string;
|
|
26
|
+
modalCardBgColor?: string;
|
|
27
|
+
textPrimary?: string;
|
|
28
|
+
textSecondary?: string;
|
|
29
|
+
borderColor?: string;
|
|
30
|
+
dividerColor?: string;
|
|
31
|
+
inputBgColor?: string;
|
|
32
|
+
inputBorderColor?: string;
|
|
33
|
+
cardBgColor?: string;
|
|
34
|
+
cardBorderColor?: string;
|
|
35
|
+
accentColor?: string;
|
|
36
|
+
errorColor?: string;
|
|
37
|
+
successColor?: string;
|
|
38
|
+
warningColor?: string;
|
|
10
39
|
};
|
|
11
40
|
|
|
12
|
-
export type ThemeMode = 'light' | 'dark';
|
|
41
|
+
export type ThemeMode = 'light' | 'dark' | 'system';
|
|
13
42
|
|
|
14
43
|
export type ThemeColors = {
|
|
15
44
|
light: ThemeColor;
|
|
@@ -18,49 +47,277 @@ export type ThemeColors = {
|
|
|
18
47
|
|
|
19
48
|
export interface ThemeStore {
|
|
20
49
|
colors: ThemeColors;
|
|
21
|
-
|
|
50
|
+
tcbsTheme: ThemeMode;
|
|
51
|
+
themeColors: ThemeColor;
|
|
52
|
+
/**
|
|
53
|
+
* Returns the current theme as 'light' or 'dark' (never 'system').
|
|
54
|
+
* If tcbsTheme is 'system', resolves to the current system color scheme.
|
|
55
|
+
*/
|
|
56
|
+
currentThemeMode: 'light' | 'dark';
|
|
22
57
|
setTcbsColor: (colors: Partial<ThemeColor> & { light?: Partial<ThemeColor>; dark?: Partial<ThemeColor> }) => void;
|
|
23
58
|
setTcbsTheme: (mode: ThemeMode) => void;
|
|
59
|
+
toggleTcbsTheme: () => void;
|
|
60
|
+
setMazicColor: (baseColor: string) => void;
|
|
24
61
|
}
|
|
25
62
|
|
|
26
63
|
const defaultColors: ThemeColors = {
|
|
27
64
|
light: {
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
65
|
+
// You can set initial defaults here if needed, or leave them empty
|
|
66
|
+
btnColor: '#007AFF',
|
|
67
|
+
btnBorderColor: '#007AFF',
|
|
68
|
+
btnIconColor: '#FFFFFF',
|
|
69
|
+
themeColor: '#007AFF',
|
|
70
|
+
btnTextColor: '#FFFFFF',
|
|
33
71
|
},
|
|
34
72
|
dark: {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
73
|
+
btnColor: '#222222',
|
|
74
|
+
btnBorderColor: '#222222',
|
|
75
|
+
btnIconColor: '#FFFFFF',
|
|
76
|
+
themeColor: '#222222',
|
|
77
|
+
btnTextColor: '#FFFFFF',
|
|
40
78
|
},
|
|
41
79
|
};
|
|
42
80
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
81
|
+
// Try to load persisted theme, fallback to 'light'. If 'system', use current system color scheme.
|
|
82
|
+
const defaultTheme = storage.getString(THEME_KEY) as ThemeMode | undefined || 'light';
|
|
83
|
+
|
|
84
|
+
// Helper functions for color manipulation
|
|
85
|
+
const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => {
|
|
86
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
87
|
+
return result ? {
|
|
88
|
+
r: parseInt(result[1], 16),
|
|
89
|
+
g: parseInt(result[2], 16),
|
|
90
|
+
b: parseInt(result[3], 16)
|
|
91
|
+
} : null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const rgbToHex = (r: number, g: number, b: number): string => {
|
|
95
|
+
return '#' + [r, g, b].map(x => {
|
|
96
|
+
const hex = Math.round(x).toString(16);
|
|
97
|
+
return hex.length === 1 ? '0' + hex : hex;
|
|
98
|
+
}).join('');
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const adjustBrightness = (hex: string, percent: number): string => {
|
|
102
|
+
const rgb = hexToRgb(hex);
|
|
103
|
+
if (!rgb) return hex;
|
|
104
|
+
|
|
105
|
+
const adjust = (value: number) => Math.max(0, Math.min(255, value + (255 * percent / 100)));
|
|
106
|
+
return rgbToHex(adjust(rgb.r), adjust(rgb.g), adjust(rgb.b));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// NEW HELPER: Determines if a color is dark enough to require white text
|
|
110
|
+
const isColorDark = (hex: string): boolean => {
|
|
111
|
+
const rgb = hexToRgb(hex);
|
|
112
|
+
if (!rgb) return true; // Default to white text if color is invalid
|
|
113
|
+
// Calculate Luma (YIQ method is good for perceived brightness)
|
|
114
|
+
// Formula: (R * 299 + G * 587 + B * 114) / 1000
|
|
115
|
+
const luma = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
|
|
116
|
+
// Threshold 0.45 is slightly lower than standard 0.5, favoring white text
|
|
117
|
+
return luma < 0.45;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const addAlpha = (hex: string, alpha: number): string => {
|
|
121
|
+
const alphaHex = Math.round(alpha * 255).toString(16).padStart(2, '0');
|
|
122
|
+
return hex + alphaHex;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
export const useTcbsColorStore = create<ThemeStore>((set: (fn: (state: ThemeStore) => Partial<ThemeStore>) => void, get) => {
|
|
127
|
+
// Helper to get the current theme color
|
|
128
|
+
const getThemeColors = (theme: ThemeMode, colors: ThemeColors): ThemeColor => {
|
|
129
|
+
if (theme === 'light' || theme === 'dark') {
|
|
130
|
+
return colors[theme];
|
|
131
|
+
} else {
|
|
132
|
+
// system: use Appearance API
|
|
133
|
+
const colorScheme = Appearance.getColorScheme?.() || 'light';
|
|
134
|
+
return colors[colorScheme as 'light' | 'dark'] || colors.light;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Initial state
|
|
139
|
+
const initialColors = defaultColors;
|
|
140
|
+
const initialTheme = defaultTheme;
|
|
141
|
+
const initialThemeColors = getThemeColors(initialTheme, initialColors);
|
|
142
|
+
|
|
143
|
+
// Listen to system theme changes if needed
|
|
144
|
+
if (initialTheme === 'system' && !appearanceListener) {
|
|
145
|
+
appearanceListener = Appearance.addChangeListener?.(({ colorScheme }) => {
|
|
146
|
+
set((state: ThemeStore) => ({
|
|
147
|
+
themeColors: state.colors[(colorScheme as 'light' | 'dark') || 'light']
|
|
148
|
+
}));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
colors: initialColors,
|
|
154
|
+
tcbsTheme: initialTheme,
|
|
155
|
+
themeColors: initialThemeColors,
|
|
156
|
+
get currentThemeMode() {
|
|
157
|
+
const state = get();
|
|
158
|
+
if (state.tcbsTheme === 'light' || state.tcbsTheme === 'dark') {
|
|
159
|
+
return state.tcbsTheme;
|
|
160
|
+
}
|
|
161
|
+
// system: use Appearance API
|
|
162
|
+
const colorScheme = Appearance.getColorScheme?.() || 'light';
|
|
163
|
+
return (colorScheme === 'dark' ? 'dark' : 'light');
|
|
164
|
+
},
|
|
165
|
+
setTcbsColor: (colors: Partial<ThemeColor> & { light?: Partial<ThemeColor>; dark?: Partial<ThemeColor> }) => {
|
|
166
|
+
set((state: ThemeStore) => {
|
|
167
|
+
let newColors = { ...state.colors };
|
|
168
|
+
// If colors for both themes are provided
|
|
169
|
+
if (colors.light || colors.dark) {
|
|
170
|
+
if (colors.light) {
|
|
171
|
+
newColors.light = { ...newColors.light, ...colors.light };
|
|
172
|
+
}
|
|
173
|
+
if (colors.dark) {
|
|
174
|
+
newColors.dark = { ...newColors.dark, ...colors.dark };
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
// If only one set, update both themes
|
|
178
|
+
newColors.light = { ...newColors.light, ...colors };
|
|
179
|
+
newColors.dark = { ...newColors.dark, ...colors };
|
|
56
180
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
181
|
+
// Update themeColors as well
|
|
182
|
+
const themeColors = getThemeColors(state.tcbsTheme, newColors);
|
|
183
|
+
return { colors: newColors, themeColors };
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
setTcbsTheme: (newTheme: ThemeMode) => {
|
|
187
|
+
// Persist user selection
|
|
188
|
+
storage.set(THEME_KEY, newTheme);
|
|
189
|
+
// Remove previous listener if exists
|
|
190
|
+
if (appearanceListener) {
|
|
191
|
+
appearanceListener.remove();
|
|
192
|
+
appearanceListener = null;
|
|
61
193
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
194
|
+
// If new theme is system, add listener
|
|
195
|
+
if (newTheme === 'system') {
|
|
196
|
+
appearanceListener = Appearance.addChangeListener?.(({ colorScheme }) => {
|
|
197
|
+
set((state: ThemeStore) => ({
|
|
198
|
+
themeColors: state.colors[(colorScheme as 'light' | 'dark') || 'light']
|
|
199
|
+
}));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// Update themeColors as well
|
|
203
|
+
set((state: ThemeStore) => ({
|
|
204
|
+
tcbsTheme: newTheme,
|
|
205
|
+
themeColors: getThemeColors(newTheme, state.colors)
|
|
206
|
+
}));
|
|
207
|
+
},
|
|
208
|
+
toggleTcbsTheme: () => {
|
|
209
|
+
set((state: ThemeStore) => {
|
|
210
|
+
const themes: ThemeMode[] = ['light', 'dark', 'system'];
|
|
211
|
+
const currentIdx = themes.indexOf(state.tcbsTheme);
|
|
212
|
+
const nextTheme = themes[(currentIdx + 1) % themes.length];
|
|
213
|
+
// Note: setTcbsTheme is called outside the state update, but here it's called internally
|
|
214
|
+
// which is a common pattern in zustand actions.
|
|
215
|
+
// @ts-ignore
|
|
216
|
+
state.setTcbsTheme(nextTheme);
|
|
217
|
+
return {};
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
// REWRITTEN FUNCTION using hardcoded neutrals for better UI contrast
|
|
222
|
+
setMazicColor: (baseColor: string) => {
|
|
223
|
+
// Determine the best text color for the button based on the base color's brightness
|
|
224
|
+
// const buttonTextColor = isColorDark(baseColor) ? '#FFFFFF' : '#000000';
|
|
225
|
+
const buttonTextColor = '#FFFFFF' ;
|
|
226
|
+
const secondaryBaseColor = adjustBrightness(baseColor, -10); // A slightly darker shade for accents
|
|
227
|
+
|
|
228
|
+
// --- Light Theme Palette ---
|
|
229
|
+
const lightColors: ThemeColor = {
|
|
230
|
+
// Primary & Button Colors (baseColor is the accent)
|
|
231
|
+
btnColor: addAlpha(baseColor,1),
|
|
232
|
+
btnBorderColor: baseColor,
|
|
233
|
+
btnIconColor: buttonTextColor,
|
|
234
|
+
themeColor: baseColor,
|
|
235
|
+
btnTextColor: buttonTextColor,
|
|
236
|
+
|
|
237
|
+
tabBarIconActiveColor: buttonTextColor,
|
|
238
|
+
tabBarIconInactiveColor: addAlpha("#000000",0.4),
|
|
239
|
+
|
|
240
|
+
primaryColor: addAlpha(baseColor,1),
|
|
241
|
+
secondaryColor: addAlpha(baseColor,0.7),
|
|
242
|
+
tertiaryColor: addAlpha(baseColor,0.1),
|
|
243
|
+
|
|
244
|
+
// Backgrounds (Clean white/near-white neutrals)
|
|
245
|
+
screenBgColor: addAlpha(baseColor,0.1), // Pure white
|
|
246
|
+
modalBgColor: addAlpha('#000000', 0.5), // Standard dark overlay
|
|
247
|
+
modalHeaderBgColor: '#F0F0F0', // Light gray
|
|
248
|
+
modalCardBgColor: '#FAFAFA', // Off-white for cards/modals
|
|
249
|
+
|
|
250
|
+
// Text Colors (High contrast black/dark gray)
|
|
251
|
+
textPrimary: '#1F1F1F', // Very dark gray
|
|
252
|
+
textSecondary: '#6B7280', // Medium gray
|
|
253
|
+
|
|
254
|
+
// Borders & Dividers (Very subtle grays)
|
|
255
|
+
borderColor: '#E5E7EB',
|
|
256
|
+
dividerColor: '#F3F4F6',
|
|
257
|
+
|
|
258
|
+
// Inputs & Cards
|
|
259
|
+
inputBgColor: '#FFFFFF',
|
|
260
|
+
inputBorderColor: '#D1D5DB',
|
|
261
|
+
cardBgColor: '#FFFFFF',
|
|
262
|
+
cardBorderColor: '#E5E7EB',
|
|
263
|
+
|
|
264
|
+
// Status Colors (Standard, high-contrast semantic colors)
|
|
265
|
+
accentColor: secondaryBaseColor,
|
|
266
|
+
errorColor: '#DC2626',
|
|
267
|
+
successColor: '#16A34A',
|
|
268
|
+
warningColor: '#F59E0B',
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// --- Dark Theme Palette ---
|
|
272
|
+
const darkColors: ThemeColor = {
|
|
273
|
+
// Primary & Button Colors
|
|
274
|
+
btnColor: addAlpha(baseColor,1),
|
|
275
|
+
btnBorderColor: baseColor,
|
|
276
|
+
btnIconColor: buttonTextColor,
|
|
277
|
+
themeColor: baseColor,
|
|
278
|
+
btnTextColor: buttonTextColor,
|
|
279
|
+
|
|
280
|
+
tabBarIconActiveColor: buttonTextColor,
|
|
281
|
+
tabBarIconInactiveColor: addAlpha("#000000",0.4),
|
|
282
|
+
primaryColor: addAlpha(baseColor,1),
|
|
283
|
+
secondaryColor: addAlpha(baseColor,0.7),
|
|
284
|
+
tertiaryColor: addAlpha(baseColor,0.2),
|
|
285
|
+
|
|
286
|
+
// Backgrounds (Clean dark/near-black neutrals)
|
|
287
|
+
screenBgColor: addAlpha(baseColor,0.8), // Very dark gray
|
|
288
|
+
modalBgColor: addAlpha('#000000', 0.8), // Darker overlay
|
|
289
|
+
modalHeaderBgColor: '#1F1F1F', // Slightly lighter dark gray
|
|
290
|
+
modalCardBgColor: '#2C2C2C', // Medium dark gray for cards/modals
|
|
291
|
+
|
|
292
|
+
// Text Colors (High contrast white/light gray)
|
|
293
|
+
textPrimary: '#FFFFFF', // Pure white
|
|
294
|
+
textSecondary: '#A0A0A0', // Light gray
|
|
295
|
+
|
|
296
|
+
// Borders & Dividers (Subtle dark grays)
|
|
297
|
+
borderColor: '#374151',
|
|
298
|
+
dividerColor: '#2C2C2C',
|
|
299
|
+
|
|
300
|
+
// Inputs & Cards
|
|
301
|
+
inputBgColor: '#1F1F1F',
|
|
302
|
+
inputBorderColor: '#374151',
|
|
303
|
+
cardBgColor: '#1F1F1F',
|
|
304
|
+
cardBorderColor: '#374151',
|
|
305
|
+
|
|
306
|
+
// Status Colors (Brighter semantic colors for dark background)
|
|
307
|
+
accentColor: secondaryBaseColor,
|
|
308
|
+
errorColor: '#EF4444',
|
|
309
|
+
successColor: '#22C55E',
|
|
310
|
+
warningColor: '#FBBF24',
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
set((state: ThemeStore) => {
|
|
314
|
+
const newColors = {
|
|
315
|
+
light: lightColors,
|
|
316
|
+
dark: darkColors,
|
|
317
|
+
};
|
|
318
|
+
const themeColors = getThemeColors(state.tcbsTheme, newColors);
|
|
319
|
+
return { colors: newColors, themeColors };
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
});
|