@umituz/react-native-design-system 1.15.0 → 2.0.0
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 +26 -19
- package/src/atoms/AtomicAvatar.tsx +161 -0
- package/src/atoms/AtomicButton.tsx +241 -0
- package/src/atoms/AtomicChip.tsx +226 -0
- package/src/atoms/AtomicDatePicker.tsx +255 -0
- package/src/atoms/AtomicFab.tsx +99 -0
- package/src/atoms/AtomicIcon.tsx +149 -0
- package/src/atoms/AtomicInput.tsx +308 -0
- package/src/atoms/AtomicPicker.tsx +310 -0
- package/src/atoms/AtomicProgress.tsx +149 -0
- package/src/atoms/AtomicText.tsx +55 -0
- package/src/atoms/__tests__/AtomicButton.test.tsx +107 -0
- package/src/atoms/__tests__/AtomicIcon.test.tsx +110 -0
- package/src/atoms/__tests__/AtomicInput.test.tsx +195 -0
- package/src/atoms/datepicker/components/DatePickerButton.tsx +112 -0
- package/src/atoms/datepicker/components/DatePickerModal.tsx +143 -0
- package/src/atoms/fab/styles/fabStyles.ts +98 -0
- package/src/atoms/fab/types/index.ts +88 -0
- package/src/atoms/index.ts +70 -0
- package/src/atoms/input/hooks/useInputState.ts +63 -0
- package/src/atoms/input/styles/inputStylesHelper.ts +120 -0
- package/src/atoms/picker/components/PickerChips.tsx +57 -0
- package/src/atoms/picker/components/PickerModal.tsx +214 -0
- package/src/atoms/picker/styles/pickerStyles.ts +223 -0
- package/src/atoms/picker/types/index.ts +42 -0
- package/src/index.ts +133 -56
- package/src/molecules/ConfirmationModal.tsx +42 -0
- package/src/molecules/ConfirmationModalContent.tsx +87 -0
- package/src/molecules/ConfirmationModalMain.tsx +91 -0
- package/src/molecules/FormField.tsx +155 -0
- package/src/molecules/IconContainer.tsx +79 -0
- package/src/molecules/ListItem.tsx +35 -0
- package/src/molecules/ScreenHeader.tsx +171 -0
- package/src/molecules/SearchBar.tsx +198 -0
- package/src/molecules/confirmation-modal/components.tsx +94 -0
- package/src/molecules/confirmation-modal/index.ts +7 -0
- package/src/molecules/confirmation-modal/styles/confirmationModalStyles.ts +133 -0
- package/src/molecules/confirmation-modal/types/index.ts +41 -0
- package/src/molecules/confirmation-modal/useConfirmationModal.ts +50 -0
- package/src/molecules/index.ts +19 -0
- package/src/molecules/listitem/index.ts +6 -0
- package/src/molecules/listitem/styles/listItemStyles.ts +37 -0
- package/src/molecules/listitem/types/index.ts +21 -0
- package/src/organisms/AppHeader.tsx +136 -0
- package/src/organisms/FormContainer.tsx +169 -0
- package/src/organisms/ScreenLayout.tsx +183 -0
- package/src/organisms/index.ts +31 -0
- package/src/responsive/config.ts +139 -0
- package/src/responsive/deviceDetection.ts +155 -0
- package/src/responsive/gridUtils.ts +79 -0
- package/src/responsive/index.ts +52 -0
- package/src/responsive/platformConstants.ts +98 -0
- package/src/responsive/responsive.ts +61 -0
- package/src/responsive/responsiveLayout.ts +137 -0
- package/src/responsive/responsiveSizing.ts +134 -0
- package/src/responsive/useResponsive.ts +140 -0
- package/src/responsive/validation.ts +158 -0
- package/src/theme/core/BaseTokens.ts +42 -0
- package/src/theme/core/ColorPalette.ts +29 -0
- package/src/theme/core/CustomColors.ts +122 -0
- package/src/theme/core/NavigationTheme.ts +72 -0
- package/src/theme/core/TokenFactory.ts +103 -0
- package/src/theme/core/colors/ColorUtils.ts +53 -0
- package/src/theme/core/colors/DarkColors.ts +146 -0
- package/src/theme/core/colors/LightColors.ts +146 -0
- package/src/theme/core/constants/DesignConstants.ts +31 -0
- package/src/theme/core/themes.ts +118 -0
- package/src/theme/core/tokens/BaseTokens.ts +144 -0
- package/src/theme/core/tokens/Borders.ts +43 -0
- package/src/theme/core/tokens/Sizes.ts +51 -0
- package/src/theme/core/tokens/Spacing.ts +38 -0
- package/src/theme/core/tokens/Typography.ts +143 -0
- package/src/theme/hooks/useAppDesignTokens.ts +45 -0
- package/src/theme/hooks/useCommonStyles.ts +248 -0
- package/src/theme/hooks/useThemedStyles.ts +68 -0
- package/src/theme/index.ts +94 -0
- package/src/theme/infrastructure/globalThemeStore.ts +69 -0
- package/src/theme/infrastructure/storage/ThemeStorage.ts +93 -0
- package/src/theme/infrastructure/stores/themeStore.ts +109 -0
- package/src/typography/__tests__/colorValidationUtils.test.ts +180 -0
- package/src/typography/__tests__/textColorUtils.test.ts +185 -0
- package/src/typography/__tests__/textStyleUtils.test.ts +168 -0
- package/src/typography/domain/entities/TypographyTypes.ts +88 -0
- package/src/typography/index.ts +53 -0
- package/src/typography/presentation/utils/colorValidationUtils.ts +133 -0
- package/src/typography/presentation/utils/textColorUtils.ts +205 -0
- package/src/typography/presentation/utils/textStyleUtils.ts +159 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useThemedStyles Hook
|
|
3
|
+
* Helper hook for creating StyleSheets with theme support
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ```typescript
|
|
7
|
+
* const createStyles = (theme: Theme) => StyleSheet.create({
|
|
8
|
+
* container: {
|
|
9
|
+
* backgroundColor: theme.colors.backgroundPrimary,
|
|
10
|
+
* }
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* const Component = () => {
|
|
14
|
+
* const styles = useThemedStyles(createStyles);
|
|
15
|
+
* return <View style={styles.container} />;
|
|
16
|
+
* };
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useMemo } from 'react';
|
|
21
|
+
import { StyleSheet } from 'react-native';
|
|
22
|
+
import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';
|
|
23
|
+
import { useTheme } from '../infrastructure/stores/themeStore';
|
|
24
|
+
import type { Theme } from '../core/themes';
|
|
25
|
+
|
|
26
|
+
type NamedStyles<T> = {
|
|
27
|
+
[P in keyof T]: ViewStyle | TextStyle | ImageStyle;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hook for creating themed styles
|
|
32
|
+
* Returns memoized styles that update when theme changes
|
|
33
|
+
*/
|
|
34
|
+
export function useThemedStyles<T extends NamedStyles<T>>(
|
|
35
|
+
createStyles: (theme: Theme) => T,
|
|
36
|
+
): T {
|
|
37
|
+
const { theme } = useTheme();
|
|
38
|
+
|
|
39
|
+
return useMemo(() => createStyles(theme), [theme, createStyles]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Alternative: Direct StyleSheet creation with theme
|
|
44
|
+
* Returns memoized styles that update when theme changes
|
|
45
|
+
*/
|
|
46
|
+
export function useThemedStyleSheet<T extends NamedStyles<T>>(
|
|
47
|
+
styleFactory: (theme: Theme) => T,
|
|
48
|
+
): T {
|
|
49
|
+
const { theme } = useTheme();
|
|
50
|
+
|
|
51
|
+
return useMemo(
|
|
52
|
+
() => StyleSheet.create(styleFactory(theme)),
|
|
53
|
+
[theme, styleFactory],
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/react-native-design-system-theme - Public API
|
|
3
|
+
*
|
|
4
|
+
* Theme management system for React Native apps
|
|
5
|
+
* Provides colors, design tokens, and theme state management
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { useAppDesignTokens, useDesignSystemTheme, lightColors, darkColors } from '@umituz/react-native-design-system-theme';
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// COLOR PALETTE
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
lightColors,
|
|
17
|
+
darkColors,
|
|
18
|
+
getColorPalette,
|
|
19
|
+
withAlpha,
|
|
20
|
+
type ColorPalette,
|
|
21
|
+
type ThemeMode,
|
|
22
|
+
} from './core/ColorPalette';
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
applyCustomColors,
|
|
26
|
+
type CustomThemeColors,
|
|
27
|
+
} from './core/CustomColors';
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// BASE TOKENS - Static Design Tokens
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
BASE_TOKENS,
|
|
35
|
+
BASE_TOKENS as STATIC_TOKENS,
|
|
36
|
+
spacing,
|
|
37
|
+
typography,
|
|
38
|
+
borders,
|
|
39
|
+
type Spacing,
|
|
40
|
+
type Typography,
|
|
41
|
+
type Borders,
|
|
42
|
+
type BaseTokens,
|
|
43
|
+
type IconSizes,
|
|
44
|
+
type Opacity,
|
|
45
|
+
type AvatarSizes,
|
|
46
|
+
type ComponentSizes,
|
|
47
|
+
} from './core/BaseTokens';
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// TOKEN FACTORY
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
createDesignTokens,
|
|
55
|
+
type DesignTokens,
|
|
56
|
+
} from './core/TokenFactory';
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// HOOKS
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
export { useAppDesignTokens } from './hooks/useAppDesignTokens';
|
|
63
|
+
export { useDesignSystemTheme } from './infrastructure/globalThemeStore';
|
|
64
|
+
export { useTheme } from './infrastructure/stores/themeStore';
|
|
65
|
+
export { useThemedStyles, useThemedStyleSheet } from './hooks/useThemedStyles';
|
|
66
|
+
export { useCommonStyles } from './hooks/useCommonStyles';
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// THEME OBJECTS
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
lightTheme,
|
|
74
|
+
darkTheme,
|
|
75
|
+
createResponsiveValue,
|
|
76
|
+
type Theme,
|
|
77
|
+
type ExtendedColorPalette,
|
|
78
|
+
} from './core/themes';
|
|
79
|
+
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// STORAGE
|
|
82
|
+
// =============================================================================
|
|
83
|
+
|
|
84
|
+
export { ThemeStorage } from './infrastructure/storage/ThemeStorage';
|
|
85
|
+
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// NAVIGATION THEME
|
|
88
|
+
// =============================================================================
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
createNavigationTheme,
|
|
92
|
+
type NavigationTheme,
|
|
93
|
+
} from './core/NavigationTheme';
|
|
94
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Theme Store for Design System
|
|
3
|
+
*
|
|
4
|
+
* Minimal Zustand store for theme state management.
|
|
5
|
+
* Apps can sync their theme state with this global store.
|
|
6
|
+
*
|
|
7
|
+
* WHY THIS EXISTS:
|
|
8
|
+
* - ScreenLayout needs to know current theme mode
|
|
9
|
+
* - Without prop drilling or Context API
|
|
10
|
+
* - Single source of truth for design system components
|
|
11
|
+
* - Apps control theme, design system reacts
|
|
12
|
+
*
|
|
13
|
+
* USAGE IN APP:
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { useDesignSystemTheme } from '@umituz/react-native-design-system-theme';
|
|
16
|
+
* import { useTheme } from '@domains/theme';
|
|
17
|
+
*
|
|
18
|
+
* // Sync app theme with design system
|
|
19
|
+
* const { themeMode } = useTheme();
|
|
20
|
+
* const { setThemeMode } = useDesignSystemTheme();
|
|
21
|
+
*
|
|
22
|
+
* useEffect(() => {
|
|
23
|
+
* setThemeMode(themeMode);
|
|
24
|
+
* }, [themeMode]);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { create } from 'zustand';
|
|
29
|
+
import type { ThemeMode } from '../core/ColorPalette';
|
|
30
|
+
import type { CustomThemeColors } from '../core/CustomColors';
|
|
31
|
+
|
|
32
|
+
interface GlobalThemeStore {
|
|
33
|
+
/** Current theme mode */
|
|
34
|
+
themeMode: ThemeMode;
|
|
35
|
+
|
|
36
|
+
/** Custom theme colors override */
|
|
37
|
+
customColors?: CustomThemeColors;
|
|
38
|
+
|
|
39
|
+
/** Update theme mode (called by app when theme changes) */
|
|
40
|
+
setThemeMode: (mode: ThemeMode) => void;
|
|
41
|
+
|
|
42
|
+
/** Set custom theme colors (called by app when custom colors change) */
|
|
43
|
+
setCustomColors: (colors?: CustomThemeColors) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Global theme store for design system components
|
|
48
|
+
*
|
|
49
|
+
* This is a MINIMAL store - app has the real theme logic.
|
|
50
|
+
* Design system just mirrors the current theme for its components.
|
|
51
|
+
*/
|
|
52
|
+
export const useDesignSystemTheme = create<GlobalThemeStore>()((set: any, get: any) => ({
|
|
53
|
+
themeMode: 'dark',
|
|
54
|
+
customColors: undefined,
|
|
55
|
+
setThemeMode: (mode: ThemeMode) => {
|
|
56
|
+
// Only update if mode actually changed to prevent unnecessary re-renders
|
|
57
|
+
const currentMode = get().themeMode;
|
|
58
|
+
if (currentMode !== mode) {
|
|
59
|
+
set({ themeMode: mode });
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
setCustomColors: (colors?: CustomThemeColors) => {
|
|
63
|
+
set({ customColors: colors });
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
// Re-export ThemeMode for backward compatibility
|
|
68
|
+
export type { ThemeMode };
|
|
69
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Storage
|
|
3
|
+
* Persists theme preference using AsyncStorage
|
|
4
|
+
*
|
|
5
|
+
* CRITICAL: This is a standalone storage utility for theme package.
|
|
6
|
+
* Apps should use this for theme persistence.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
10
|
+
import type { ThemeMode } from '../../core/ColorPalette';
|
|
11
|
+
import { DESIGN_CONSTANTS } from '../../core/constants/DesignConstants';
|
|
12
|
+
|
|
13
|
+
const STORAGE_KEY = `${DESIGN_CONSTANTS.STORAGE_NAMESPACE}/mode`;
|
|
14
|
+
|
|
15
|
+
export class ThemeStorage {
|
|
16
|
+
/**
|
|
17
|
+
* Get stored theme mode
|
|
18
|
+
*/
|
|
19
|
+
static async getThemeMode(): Promise<ThemeMode | null> {
|
|
20
|
+
try {
|
|
21
|
+
const value = await AsyncStorage.getItem(STORAGE_KEY);
|
|
22
|
+
if (!value) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Validate theme mode value
|
|
27
|
+
if (value === 'light' || value === 'dark') {
|
|
28
|
+
return value as ThemeMode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (__DEV__) {
|
|
32
|
+
console.warn('[ThemeStorage] Invalid theme mode value stored:', value);
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
37
|
+
if (__DEV__) {
|
|
38
|
+
console.error('[ThemeStorage] Error getting theme mode:', errorMessage);
|
|
39
|
+
}
|
|
40
|
+
// Return null instead of throwing to prevent app crashes
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Save theme mode
|
|
47
|
+
*/
|
|
48
|
+
static async setThemeMode(mode: ThemeMode): Promise<void> {
|
|
49
|
+
try {
|
|
50
|
+
// Validate input
|
|
51
|
+
if (!mode || (mode !== 'light' && mode !== 'dark')) {
|
|
52
|
+
throw new Error(`Invalid theme mode: ${mode}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await AsyncStorage.setItem(STORAGE_KEY, mode);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
58
|
+
if (__DEV__) {
|
|
59
|
+
console.error('[ThemeStorage] Error saving theme mode:', errorMessage);
|
|
60
|
+
}
|
|
61
|
+
// Re-throw validation errors but swallow storage errors to prevent app crashes
|
|
62
|
+
if (errorMessage.includes('Invalid theme mode')) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Clear stored theme mode
|
|
70
|
+
*/
|
|
71
|
+
static async clearThemeMode(): Promise<void> {
|
|
72
|
+
try {
|
|
73
|
+
await AsyncStorage.removeItem(STORAGE_KEY);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
76
|
+
if (__DEV__) {
|
|
77
|
+
console.error('[ThemeStorage] Error clearing theme mode:', errorMessage);
|
|
78
|
+
}
|
|
79
|
+
// Don't throw - clearing storage is not critical
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Store - Zustand State Management
|
|
3
|
+
*
|
|
4
|
+
* CRITICAL: NO Context Provider pattern - uses Zustand for global state
|
|
5
|
+
*
|
|
6
|
+
* Architecture:
|
|
7
|
+
* - Zustand for state management (NOT Context API)
|
|
8
|
+
* - AsyncStorage for persistence via ThemeStorage
|
|
9
|
+
* - Automatic initialization on app launch
|
|
10
|
+
* - Syncs with design system global theme store
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { create } from 'zustand';
|
|
14
|
+
import { lightTheme, darkTheme, type Theme } from '../../core/themes';
|
|
15
|
+
import { ThemeStorage } from '../storage/ThemeStorage';
|
|
16
|
+
import { useDesignSystemTheme } from '../globalThemeStore';
|
|
17
|
+
import type { ThemeMode } from '../../core/ColorPalette';
|
|
18
|
+
|
|
19
|
+
interface ThemeState {
|
|
20
|
+
theme: Theme;
|
|
21
|
+
themeMode: ThemeMode;
|
|
22
|
+
isDark: boolean;
|
|
23
|
+
isInitialized: boolean;
|
|
24
|
+
setThemeMode: (mode: ThemeMode) => Promise<void>;
|
|
25
|
+
toggleTheme: () => Promise<void>;
|
|
26
|
+
initialize: () => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Theme Store - Global state management for theme
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import { useTheme } from '@umituz/react-native-design-system-theme';
|
|
35
|
+
*
|
|
36
|
+
* const MyComponent = () => {
|
|
37
|
+
* const { theme, themeMode, setThemeMode, toggleTheme } = useTheme();
|
|
38
|
+
* // ...
|
|
39
|
+
* };
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export const useTheme = create<ThemeState>()((set: any, get: any) => ({
|
|
43
|
+
theme: darkTheme,
|
|
44
|
+
themeMode: 'dark',
|
|
45
|
+
isDark: true,
|
|
46
|
+
isInitialized: false,
|
|
47
|
+
|
|
48
|
+
initialize: async () => {
|
|
49
|
+
try {
|
|
50
|
+
const savedMode = await ThemeStorage.getThemeMode();
|
|
51
|
+
if (savedMode) {
|
|
52
|
+
const theme = savedMode === 'light' ? lightTheme : darkTheme;
|
|
53
|
+
set({
|
|
54
|
+
themeMode: savedMode,
|
|
55
|
+
theme,
|
|
56
|
+
isDark: savedMode === 'dark',
|
|
57
|
+
isInitialized: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Sync with design system global theme
|
|
61
|
+
useDesignSystemTheme.getState().setThemeMode(savedMode);
|
|
62
|
+
} else {
|
|
63
|
+
// No saved mode - use default 'dark' and sync to design system store
|
|
64
|
+
set({ isInitialized: true });
|
|
65
|
+
// Ensure design system store is synced even if no saved mode exists
|
|
66
|
+
useDesignSystemTheme.getState().setThemeMode('dark');
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
/* eslint-disable-next-line no-console */
|
|
70
|
+
if (__DEV__) console.error('[ThemeStore] Initialization error:', error);
|
|
71
|
+
// Silent failure - still mark as initialized to prevent blocking
|
|
72
|
+
set({ isInitialized: true });
|
|
73
|
+
// Ensure design system store is synced even on error
|
|
74
|
+
useDesignSystemTheme.getState().setThemeMode('dark');
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
setThemeMode: async (mode: ThemeMode) => {
|
|
79
|
+
try {
|
|
80
|
+
const theme = mode === 'light' ? lightTheme : darkTheme;
|
|
81
|
+
|
|
82
|
+
set({
|
|
83
|
+
themeMode: mode,
|
|
84
|
+
theme,
|
|
85
|
+
isDark: mode === 'dark',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await ThemeStorage.setThemeMode(mode);
|
|
89
|
+
|
|
90
|
+
// Sync with design system global theme
|
|
91
|
+
useDesignSystemTheme.getState().setThemeMode(mode);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
/* eslint-disable-next-line no-console */
|
|
94
|
+
if (__DEV__) console.error('[ThemeStore] Error setting theme mode:', error);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
toggleTheme: async () => {
|
|
99
|
+
const { themeMode, setThemeMode } = get();
|
|
100
|
+
const newMode: ThemeMode = themeMode === 'light' ? 'dark' : 'light';
|
|
101
|
+
await setThemeMode(newMode);
|
|
102
|
+
},
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for color validation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
isValidHexColor,
|
|
7
|
+
isValidRgbColor,
|
|
8
|
+
isValidHslColor,
|
|
9
|
+
isValidNamedColor,
|
|
10
|
+
isValidColor,
|
|
11
|
+
getColorFormat,
|
|
12
|
+
normalizeColor,
|
|
13
|
+
} from '../presentation/utils/colorValidationUtils';
|
|
14
|
+
|
|
15
|
+
describe('isValidHexColor', () => {
|
|
16
|
+
test('should return true for valid 3-digit hex colors', () => {
|
|
17
|
+
expect(isValidHexColor('#fff')).toBe(true);
|
|
18
|
+
expect(isValidHexColor('#000')).toBe(true);
|
|
19
|
+
expect(isValidHexColor('#abc')).toBe(true);
|
|
20
|
+
expect(isValidHexColor('#123')).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('should return true for valid 6-digit hex colors', () => {
|
|
24
|
+
expect(isValidHexColor('#ffffff')).toBe(true);
|
|
25
|
+
expect(isValidHexColor('#000000')).toBe(true);
|
|
26
|
+
expect(isValidHexColor('#abcdef')).toBe(true);
|
|
27
|
+
expect(isValidHexColor('#123456')).toBe(true);
|
|
28
|
+
expect(isValidHexColor('#FF5733')).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should return false for invalid hex colors', () => {
|
|
32
|
+
expect(isValidHexColor('#ff')).toBe(false);
|
|
33
|
+
expect(isValidHexColor('#ffff')).toBe(false);
|
|
34
|
+
expect(isValidHexColor('#ggg')).toBe(false);
|
|
35
|
+
expect(isValidHexColor('ffffff')).toBe(false);
|
|
36
|
+
expect(isValidHexColor('#12345')).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('isValidRgbColor', () => {
|
|
41
|
+
test('should return true for valid RGB colors', () => {
|
|
42
|
+
expect(isValidRgbColor('rgb(255, 0, 0)')).toBe(true);
|
|
43
|
+
expect(isValidRgbColor('rgb(0, 255, 0)')).toBe(true);
|
|
44
|
+
expect(isValidRgbColor('rgb(0, 0, 255)')).toBe(true);
|
|
45
|
+
expect(isValidRgbColor('rgb(128, 128, 128)')).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('should return true for valid RGBA colors', () => {
|
|
49
|
+
expect(isValidRgbColor('rgba(255, 0, 0, 0.5)')).toBe(true);
|
|
50
|
+
expect(isValidRgbColor('rgba(0, 255, 0, 1)')).toBe(true);
|
|
51
|
+
expect(isValidRgbColor('rgba(0, 0, 255, 0)')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should return true for RGB colors with percentages', () => {
|
|
55
|
+
expect(isValidRgbColor('rgb(100%, 0%, 0%)')).toBe(true);
|
|
56
|
+
expect(isValidRgbColor('rgb(50%, 50%, 50%)')).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should return false for invalid RGB colors', () => {
|
|
60
|
+
expect(isValidRgbColor('rgb(256, 0, 0)')).toBe(false);
|
|
61
|
+
expect(isValidRgbColor('rgb(255, -1, 0)')).toBe(false);
|
|
62
|
+
expect(isValidRgbColor('rgb(255, 0)')).toBe(false);
|
|
63
|
+
expect(isValidRgbColor('255, 0, 0')).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('isValidHslColor', () => {
|
|
68
|
+
test('should return true for valid HSL colors', () => {
|
|
69
|
+
expect(isValidHslColor('hsl(0, 100%, 50%)')).toBe(true);
|
|
70
|
+
expect(isValidHslColor('hsl(120, 100%, 50%)')).toBe(true);
|
|
71
|
+
expect(isValidHslColor('hsl(240, 100%, 50%)')).toBe(true);
|
|
72
|
+
expect(isValidHslColor('hsl(180, 50%, 50%)')).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('should return true for valid HSLA colors', () => {
|
|
76
|
+
expect(isValidHslColor('hsla(0, 100%, 50%, 0.5)')).toBe(true);
|
|
77
|
+
expect(isValidHslColor('hsla(120, 100%, 50%, 1)')).toBe(true);
|
|
78
|
+
expect(isValidHslColor('hsla(240, 100%, 50%, 0)')).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('should return false for invalid HSL colors', () => {
|
|
82
|
+
expect(isValidHslColor('hsl(361, 100%, 50%)')).toBe(false);
|
|
83
|
+
expect(isValidHslColor('hsl(120, 101%, 50%)')).toBe(false);
|
|
84
|
+
expect(isValidHslColor('hsl(120, 100%, 101%)')).toBe(false);
|
|
85
|
+
expect(isValidHslColor('hsl(120, 100%)')).toBe(false);
|
|
86
|
+
expect(isValidHslColor('120, 100%, 50%')).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('isValidNamedColor', () => {
|
|
91
|
+
test('should return true for valid CSS color names', () => {
|
|
92
|
+
expect(isValidNamedColor('red')).toBe(true);
|
|
93
|
+
expect(isValidNamedColor('blue')).toBe(true);
|
|
94
|
+
expect(isValidNamedColor('green')).toBe(true);
|
|
95
|
+
expect(isValidNamedColor('transparent')).toBe(true);
|
|
96
|
+
expect(isValidNamedColor('currentColor')).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('should return false for invalid color names', () => {
|
|
100
|
+
expect(isValidNamedColor('notacolor')).toBe(false);
|
|
101
|
+
expect(isValidNamedColor('color123')).toBe(false);
|
|
102
|
+
expect(isValidNamedColor('')).toBe(false);
|
|
103
|
+
expect(isValidNamedColor('red-blue')).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('isValidColor', () => {
|
|
108
|
+
test('should return true for any valid color format', () => {
|
|
109
|
+
expect(isValidColor('#fff')).toBe(true);
|
|
110
|
+
expect(isValidColor('#ffffff')).toBe(true);
|
|
111
|
+
expect(isValidColor('rgb(255, 0, 0)')).toBe(true);
|
|
112
|
+
expect(isValidColor('rgba(255, 0, 0, 0.5)')).toBe(true);
|
|
113
|
+
expect(isValidColor('hsl(0, 100%, 50%)')).toBe(true);
|
|
114
|
+
expect(isValidColor('hsla(0, 100%, 50%, 0.5)')).toBe(true);
|
|
115
|
+
expect(isValidColor('red')).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('should return false for invalid colors', () => {
|
|
119
|
+
expect(isValidColor('invalid')).toBe(false);
|
|
120
|
+
expect(isValidColor('#ggg')).toBe(false);
|
|
121
|
+
expect(isValidColor('rgb(256, 0, 0)')).toBe(false);
|
|
122
|
+
expect(isValidColor('')).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('getColorFormat', () => {
|
|
127
|
+
test('should return correct format for hex colors', () => {
|
|
128
|
+
expect(getColorFormat('#fff')).toBe('hex');
|
|
129
|
+
expect(getColorFormat('#ffffff')).toBe('hex');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('should return correct format for RGB colors', () => {
|
|
133
|
+
expect(getColorFormat('rgb(255, 0, 0)')).toBe('rgb');
|
|
134
|
+
expect(getColorFormat('rgba(255, 0, 0, 0.5)')).toBe('rgb');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should return correct format for HSL colors', () => {
|
|
138
|
+
expect(getColorFormat('hsl(0, 100%, 50%)')).toBe('hsl');
|
|
139
|
+
expect(getColorFormat('hsla(0, 100%, 50%, 0.5)')).toBe('hsl');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('should return correct format for named colors', () => {
|
|
143
|
+
expect(getColorFormat('red')).toBe('named');
|
|
144
|
+
expect(getColorFormat('blue')).toBe('named');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('should return unknown for invalid colors', () => {
|
|
148
|
+
expect(getColorFormat('invalid')).toBe('unknown');
|
|
149
|
+
expect(getColorFormat('')).toBe('unknown');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('normalizeColor', () => {
|
|
154
|
+
test('should convert 3-digit hex to 6-digit', () => {
|
|
155
|
+
expect(normalizeColor('#fff')).toBe('#ffffff');
|
|
156
|
+
expect(normalizeColor('#abc')).toBe('#aabbcc');
|
|
157
|
+
expect(normalizeColor('#123')).toBe('#112233');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('should keep 6-digit hex unchanged', () => {
|
|
161
|
+
expect(normalizeColor('#ffffff')).toBe('#ffffff');
|
|
162
|
+
expect(normalizeColor('#abcdef')).toBe('#abcdef');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('should convert to lowercase', () => {
|
|
166
|
+
expect(normalizeColor('#FFFFFF')).toBe('#ffffff');
|
|
167
|
+
expect(normalizeColor('#ABCDEF')).toBe('#abcdef');
|
|
168
|
+
expect(normalizeColor('RED')).toBe('red');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should return unchanged for invalid colors', () => {
|
|
172
|
+
expect(normalizeColor('invalid')).toBe('invalid');
|
|
173
|
+
expect(normalizeColor('')).toBe('');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('should return unchanged for non-hex colors', () => {
|
|
177
|
+
expect(normalizeColor('rgb(255, 0, 0)')).toBe('rgb(255, 0, 0)');
|
|
178
|
+
expect(normalizeColor('hsl(0, 100%, 50%)')).toBe('hsl(0, 100%, 50%)');
|
|
179
|
+
});
|
|
180
|
+
});
|