@shohojdhara/atomix 0.2.8 → 0.2.9
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/CHANGELOG.md +56 -0
- package/README.md +40 -1
- package/dist/atomix.css +96 -39
- package/dist/atomix.min.css +2 -2
- package/dist/index.d.ts +627 -2
- package/dist/index.esm.js +1292 -89
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1316 -88
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/themes/applemix.css +96 -39
- package/dist/themes/applemix.min.css +2 -2
- package/dist/themes/boomdevs.css +96 -39
- package/dist/themes/boomdevs.min.css +2 -2
- package/dist/themes/esrar.css +96 -39
- package/dist/themes/esrar.min.css +2 -2
- package/dist/themes/flashtrade.css +97 -40
- package/dist/themes/flashtrade.min.css +2 -2
- package/dist/themes/mashroom.css +96 -39
- package/dist/themes/mashroom.min.css +3 -3
- package/dist/themes/shaj-default.css +96 -39
- package/dist/themes/shaj-default.min.css +2 -2
- package/package.json +13 -2
- package/src/components/Card/Card.tsx +9 -4
- package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +301 -13
- package/src/components/Navigation/SideMenu/SideMenu.tsx +236 -9
- package/src/lib/composables/useSideMenu.ts +89 -30
- package/src/lib/index.ts +5 -0
- package/src/lib/theme/ThemeContext.tsx +17 -0
- package/src/lib/theme/ThemeManager.stories.tsx +472 -0
- package/src/lib/theme/ThemeManager.test.ts +186 -0
- package/src/lib/theme/ThemeManager.ts +501 -0
- package/src/lib/theme/ThemeProvider.tsx +227 -0
- package/src/lib/theme/index.ts +56 -0
- package/src/lib/theme/types.ts +247 -0
- package/src/lib/theme/useTheme.test.tsx +66 -0
- package/src/lib/theme/useTheme.ts +80 -0
- package/src/lib/theme/utils.test.ts +140 -0
- package/src/lib/theme/utils.ts +398 -0
- package/src/lib/types/components.ts +26 -0
- package/src/styles/06-components/_components.card.scss +39 -24
- package/src/styles/06-components/_components.side-menu.scss +79 -18
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Provider
|
|
3
|
+
*
|
|
4
|
+
* React context provider for theme management
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
8
|
+
import { ThemeManager } from './ThemeManager';
|
|
9
|
+
import { ThemeContext } from './ThemeContext';
|
|
10
|
+
import type { ThemeProviderProps, ThemeMetadata } from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ThemeProvider component
|
|
14
|
+
*
|
|
15
|
+
* Provides theme context to child components and manages theme state.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* import { ThemeProvider } from '@shohojdhara/atomix/theme';
|
|
20
|
+
* import { themesConfig } from '@shohojdhara/atomix/themes/themes.config';
|
|
21
|
+
*
|
|
22
|
+
* function App() {
|
|
23
|
+
* return (
|
|
24
|
+
* <ThemeProvider
|
|
25
|
+
* themes={themesConfig.metadata}
|
|
26
|
+
* defaultTheme="shaj-default"
|
|
27
|
+
* >
|
|
28
|
+
* <YourApp />
|
|
29
|
+
* </ThemeProvider>
|
|
30
|
+
* );
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
35
|
+
children,
|
|
36
|
+
defaultTheme = 'shaj-default',
|
|
37
|
+
themes = {},
|
|
38
|
+
basePath = '/themes',
|
|
39
|
+
cdnPath = null,
|
|
40
|
+
preload = [],
|
|
41
|
+
lazy = true,
|
|
42
|
+
storageKey = 'atomix-theme',
|
|
43
|
+
dataAttribute = 'data-theme',
|
|
44
|
+
enablePersistence = true,
|
|
45
|
+
useMinified = false,
|
|
46
|
+
onThemeChange,
|
|
47
|
+
onError,
|
|
48
|
+
}) => {
|
|
49
|
+
// Initialize theme manager
|
|
50
|
+
const themeManager = useMemo(() => {
|
|
51
|
+
try {
|
|
52
|
+
return new ThemeManager({
|
|
53
|
+
themes,
|
|
54
|
+
defaultTheme,
|
|
55
|
+
basePath,
|
|
56
|
+
cdnPath,
|
|
57
|
+
preload,
|
|
58
|
+
lazy,
|
|
59
|
+
storageKey,
|
|
60
|
+
dataAttribute,
|
|
61
|
+
enablePersistence,
|
|
62
|
+
useMinified,
|
|
63
|
+
onThemeChange,
|
|
64
|
+
onError,
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Failed to initialize ThemeManager:', error);
|
|
68
|
+
// Return a minimal fallback manager
|
|
69
|
+
return new ThemeManager({
|
|
70
|
+
themes: { [defaultTheme]: { name: defaultTheme } },
|
|
71
|
+
defaultTheme,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}, [
|
|
75
|
+
themes,
|
|
76
|
+
defaultTheme,
|
|
77
|
+
basePath,
|
|
78
|
+
cdnPath,
|
|
79
|
+
preload,
|
|
80
|
+
lazy,
|
|
81
|
+
storageKey,
|
|
82
|
+
dataAttribute,
|
|
83
|
+
enablePersistence,
|
|
84
|
+
useMinified,
|
|
85
|
+
onThemeChange,
|
|
86
|
+
onError,
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
// State
|
|
90
|
+
const [currentTheme, setCurrentTheme] = useState<string>(themeManager.getTheme());
|
|
91
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
92
|
+
const [error, setError] = useState<Error | null>(null);
|
|
93
|
+
|
|
94
|
+
// Get available themes
|
|
95
|
+
const availableThemes = useMemo<ThemeMetadata[]>(
|
|
96
|
+
() => themeManager.getAvailableThemes(),
|
|
97
|
+
[themeManager]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Set theme function
|
|
101
|
+
const setTheme = useCallback(
|
|
102
|
+
async (themeName: string, options?: { fallbackOnError?: boolean }): Promise<void> => {
|
|
103
|
+
setIsLoading(true);
|
|
104
|
+
setError(null);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await themeManager.setTheme(themeName, options);
|
|
108
|
+
setCurrentTheme(themeName);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
111
|
+
setError(error);
|
|
112
|
+
|
|
113
|
+
// If fallback is enabled and theme is not default, try to fallback
|
|
114
|
+
if (options?.fallbackOnError && themeName !== defaultTheme) {
|
|
115
|
+
try {
|
|
116
|
+
await themeManager.setTheme(defaultTheme, { fallbackOnError: false });
|
|
117
|
+
setCurrentTheme(defaultTheme);
|
|
118
|
+
setError(null);
|
|
119
|
+
return;
|
|
120
|
+
} catch (fallbackErr) {
|
|
121
|
+
// If fallback also fails, throw original error
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
throw error;
|
|
127
|
+
} finally {
|
|
128
|
+
setIsLoading(false);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
[themeManager, defaultTheme]
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Check if theme is loaded
|
|
135
|
+
const isThemeLoaded = useCallback(
|
|
136
|
+
(themeName: string): boolean => {
|
|
137
|
+
return themeManager.isThemeLoaded(themeName);
|
|
138
|
+
},
|
|
139
|
+
[themeManager]
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Preload theme
|
|
143
|
+
const preloadTheme = useCallback(
|
|
144
|
+
async (themeName: string): Promise<void> => {
|
|
145
|
+
try {
|
|
146
|
+
await themeManager.preloadTheme(themeName);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
149
|
+
setError(error);
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
[themeManager]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Listen for theme changes
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
const handleThemeChange = () => {
|
|
159
|
+
setCurrentTheme(themeManager.getTheme());
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
themeManager.on('themeChange', handleThemeChange);
|
|
163
|
+
|
|
164
|
+
return () => {
|
|
165
|
+
themeManager.off('themeChange', handleThemeChange);
|
|
166
|
+
};
|
|
167
|
+
}, [themeManager]);
|
|
168
|
+
|
|
169
|
+
// Load initial theme
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
const loadInitialTheme = async () => {
|
|
172
|
+
setIsLoading(true);
|
|
173
|
+
try {
|
|
174
|
+
await themeManager.setTheme(themeManager.getTheme());
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
177
|
+
setError(error);
|
|
178
|
+
console.error('Failed to load initial theme:', error);
|
|
179
|
+
} finally {
|
|
180
|
+
setIsLoading(false);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
loadInitialTheme();
|
|
185
|
+
}, [themeManager]);
|
|
186
|
+
|
|
187
|
+
// Cleanup on unmount
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
return () => {
|
|
190
|
+
themeManager.destroy();
|
|
191
|
+
};
|
|
192
|
+
}, [themeManager]);
|
|
193
|
+
|
|
194
|
+
// Context value
|
|
195
|
+
const contextValue = useMemo(
|
|
196
|
+
() => ({
|
|
197
|
+
theme: currentTheme,
|
|
198
|
+
setTheme,
|
|
199
|
+
availableThemes,
|
|
200
|
+
isLoading,
|
|
201
|
+
error,
|
|
202
|
+
isThemeLoaded,
|
|
203
|
+
preloadTheme,
|
|
204
|
+
themeManager,
|
|
205
|
+
}),
|
|
206
|
+
[
|
|
207
|
+
currentTheme,
|
|
208
|
+
setTheme,
|
|
209
|
+
availableThemes,
|
|
210
|
+
isLoading,
|
|
211
|
+
error,
|
|
212
|
+
isThemeLoaded,
|
|
213
|
+
preloadTheme,
|
|
214
|
+
themeManager,
|
|
215
|
+
]
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<ThemeContext.Provider value={contextValue}>
|
|
220
|
+
{children}
|
|
221
|
+
</ThemeContext.Provider>
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
ThemeProvider.displayName = 'ThemeProvider';
|
|
226
|
+
|
|
227
|
+
export default ThemeProvider;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Module Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Exports all theme management utilities for the Atomix Design System
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Core theme manager
|
|
8
|
+
export { ThemeManager } from './ThemeManager';
|
|
9
|
+
export { default as ThemeManagerDefault } from './ThemeManager';
|
|
10
|
+
|
|
11
|
+
// React integration
|
|
12
|
+
export { ThemeProvider } from './ThemeProvider';
|
|
13
|
+
export { default as ThemeProviderDefault } from './ThemeProvider';
|
|
14
|
+
export { useTheme } from './useTheme';
|
|
15
|
+
export { default as useThemeDefault } from './useTheme';
|
|
16
|
+
export { ThemeContext } from './ThemeContext';
|
|
17
|
+
export { default as ThemeContextDefault } from './ThemeContext';
|
|
18
|
+
|
|
19
|
+
// Types
|
|
20
|
+
export type {
|
|
21
|
+
ThemeMetadata,
|
|
22
|
+
ThemeManagerConfig,
|
|
23
|
+
ThemeChangeEvent,
|
|
24
|
+
ThemeLoadOptions,
|
|
25
|
+
ThemeValidationResult,
|
|
26
|
+
ThemeManagerEvent,
|
|
27
|
+
ThemeChangeCallback,
|
|
28
|
+
ThemeLoadCallback,
|
|
29
|
+
ThemeErrorCallback,
|
|
30
|
+
ThemeEventListeners,
|
|
31
|
+
UseThemeOptions,
|
|
32
|
+
UseThemeReturn,
|
|
33
|
+
ThemeProviderProps,
|
|
34
|
+
ThemeContextValue,
|
|
35
|
+
StorageAdapter,
|
|
36
|
+
} from './types';
|
|
37
|
+
|
|
38
|
+
// Utilities
|
|
39
|
+
export {
|
|
40
|
+
isBrowser,
|
|
41
|
+
isServer,
|
|
42
|
+
getThemeLinkId,
|
|
43
|
+
buildThemePath,
|
|
44
|
+
loadThemeCSS,
|
|
45
|
+
removeThemeCSS,
|
|
46
|
+
removeAllThemeCSS,
|
|
47
|
+
applyThemeAttributes,
|
|
48
|
+
removeThemeAttributes,
|
|
49
|
+
getCurrentThemeFromDOM,
|
|
50
|
+
getSystemTheme,
|
|
51
|
+
isThemeLoaded,
|
|
52
|
+
validateThemeMetadata,
|
|
53
|
+
isValidThemeName,
|
|
54
|
+
createLocalStorageAdapter,
|
|
55
|
+
debounce,
|
|
56
|
+
} from './utils';
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Manager Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* TypeScript types and interfaces for the Atomix Design System theme management system.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ThemeManager as ThemeManagerType } from './ThemeManager';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Theme metadata interface matching themes.config.js structure
|
|
11
|
+
*/
|
|
12
|
+
export interface ThemeMetadata {
|
|
13
|
+
/** Display name of the theme */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Unique identifier/class name for the theme */
|
|
16
|
+
class?: string;
|
|
17
|
+
/** Theme description */
|
|
18
|
+
description?: string;
|
|
19
|
+
/** Theme author */
|
|
20
|
+
author?: string;
|
|
21
|
+
/** Theme version (semver) */
|
|
22
|
+
version?: string;
|
|
23
|
+
/** Theme tags for categorization */
|
|
24
|
+
tags?: string[];
|
|
25
|
+
/** Whether the theme supports dark mode */
|
|
26
|
+
supportsDarkMode?: boolean;
|
|
27
|
+
/** Theme status: stable, beta, experimental, deprecated */
|
|
28
|
+
status?: 'stable' | 'beta' | 'experimental' | 'deprecated';
|
|
29
|
+
/** Accessibility information */
|
|
30
|
+
a11y?: {
|
|
31
|
+
/** Target contrast ratio */
|
|
32
|
+
contrastTarget?: number;
|
|
33
|
+
/** Supported color modes */
|
|
34
|
+
modes?: string[];
|
|
35
|
+
};
|
|
36
|
+
/** Primary theme color (for UI display) */
|
|
37
|
+
color?: string;
|
|
38
|
+
/** Theme features list */
|
|
39
|
+
features?: string[];
|
|
40
|
+
/** Theme dependencies (other themes required) */
|
|
41
|
+
dependencies?: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Theme manager configuration options
|
|
46
|
+
*/
|
|
47
|
+
export interface ThemeManagerConfig {
|
|
48
|
+
/** Available themes metadata */
|
|
49
|
+
themes: Record<string, ThemeMetadata>;
|
|
50
|
+
/** Default theme to use */
|
|
51
|
+
defaultTheme?: string;
|
|
52
|
+
/** Base path for theme CSS files */
|
|
53
|
+
basePath?: string;
|
|
54
|
+
/** CDN path for theme CSS files (optional) */
|
|
55
|
+
cdnPath?: string | null;
|
|
56
|
+
/** Themes to preload on initialization */
|
|
57
|
+
preload?: string[];
|
|
58
|
+
/** Enable lazy loading of themes */
|
|
59
|
+
lazy?: boolean;
|
|
60
|
+
/** localStorage key for persistence */
|
|
61
|
+
storageKey?: string;
|
|
62
|
+
/** Data attribute name for theme */
|
|
63
|
+
dataAttribute?: string;
|
|
64
|
+
/** Enable persistence */
|
|
65
|
+
enablePersistence?: boolean;
|
|
66
|
+
/** Custom CSS file extension */
|
|
67
|
+
cssExtension?: string;
|
|
68
|
+
/** Use minified CSS files */
|
|
69
|
+
useMinified?: boolean;
|
|
70
|
+
/** Callback when theme changes */
|
|
71
|
+
onThemeChange?: (theme: string) => void;
|
|
72
|
+
/** Callback when theme load fails */
|
|
73
|
+
onError?: (error: Error, themeName: string) => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Theme change event payload
|
|
78
|
+
*/
|
|
79
|
+
export interface ThemeChangeEvent {
|
|
80
|
+
/** Previous theme name */
|
|
81
|
+
previousTheme: string | null;
|
|
82
|
+
/** New theme name */
|
|
83
|
+
currentTheme: string;
|
|
84
|
+
/** Timestamp of the change */
|
|
85
|
+
timestamp: number;
|
|
86
|
+
/** Whether the change was from user action or system */
|
|
87
|
+
source: 'user' | 'system' | 'storage';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Theme load options
|
|
92
|
+
*/
|
|
93
|
+
export interface ThemeLoadOptions {
|
|
94
|
+
/** Force reload even if already loaded */
|
|
95
|
+
force?: boolean;
|
|
96
|
+
/** Preload without applying */
|
|
97
|
+
preload?: boolean;
|
|
98
|
+
/** Remove previous theme CSS */
|
|
99
|
+
removePrevious?: boolean;
|
|
100
|
+
/** Custom CSS path override */
|
|
101
|
+
customPath?: string;
|
|
102
|
+
/** Fallback to default theme on error */
|
|
103
|
+
fallbackOnError?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Theme validation result
|
|
108
|
+
*/
|
|
109
|
+
export interface ThemeValidationResult {
|
|
110
|
+
/** Whether the theme is valid */
|
|
111
|
+
valid: boolean;
|
|
112
|
+
/** Validation errors */
|
|
113
|
+
errors: string[];
|
|
114
|
+
/** Validation warnings */
|
|
115
|
+
warnings: string[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Theme manager event types
|
|
120
|
+
*/
|
|
121
|
+
export type ThemeManagerEvent = 'themeChange' | 'themeLoad' | 'themeError';
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Theme change callback function
|
|
125
|
+
*/
|
|
126
|
+
export type ThemeChangeCallback = (event: ThemeChangeEvent) => void;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Theme load callback function
|
|
130
|
+
*/
|
|
131
|
+
export type ThemeLoadCallback = (themeName: string) => void;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Theme error callback function
|
|
135
|
+
*/
|
|
136
|
+
export type ThemeErrorCallback = (error: Error, themeName: string) => void;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Event listener map
|
|
140
|
+
*/
|
|
141
|
+
export interface ThemeEventListeners {
|
|
142
|
+
themeChange: ThemeChangeCallback[];
|
|
143
|
+
themeLoad: ThemeLoadCallback[];
|
|
144
|
+
themeError: ThemeErrorCallback[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* React hook options for useTheme
|
|
149
|
+
*/
|
|
150
|
+
export interface UseThemeOptions {
|
|
151
|
+
/** Default theme (overrides ThemeProvider default) */
|
|
152
|
+
defaultTheme?: string;
|
|
153
|
+
/** Enable persistence for this hook instance */
|
|
154
|
+
enablePersistence?: boolean;
|
|
155
|
+
/** Custom storage key */
|
|
156
|
+
storageKey?: string;
|
|
157
|
+
/** Callback when theme changes */
|
|
158
|
+
onChange?: (theme: string) => void;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* React hook return type for useTheme
|
|
163
|
+
*/
|
|
164
|
+
export interface UseThemeReturn {
|
|
165
|
+
/** Current theme name */
|
|
166
|
+
theme: string;
|
|
167
|
+
/** Function to change theme */
|
|
168
|
+
setTheme: (theme: string, options?: ThemeLoadOptions) => Promise<void>;
|
|
169
|
+
/** Available themes */
|
|
170
|
+
availableThemes: ThemeMetadata[];
|
|
171
|
+
/** Whether a theme is currently loading */
|
|
172
|
+
isLoading: boolean;
|
|
173
|
+
/** Current error, if any */
|
|
174
|
+
error: Error | null;
|
|
175
|
+
/** Whether a specific theme is loaded */
|
|
176
|
+
isThemeLoaded: (themeName: string) => boolean;
|
|
177
|
+
/** Preload a theme */
|
|
178
|
+
preloadTheme: (themeName: string) => Promise<void>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Theme provider props
|
|
183
|
+
*/
|
|
184
|
+
export interface ThemeProviderProps {
|
|
185
|
+
/** Child components */
|
|
186
|
+
children: React.ReactNode;
|
|
187
|
+
/** Default theme */
|
|
188
|
+
defaultTheme?: string;
|
|
189
|
+
/** Available themes */
|
|
190
|
+
themes?: Record<string, ThemeMetadata>;
|
|
191
|
+
/** Base path for theme CSS */
|
|
192
|
+
basePath?: string;
|
|
193
|
+
/** CDN path for theme CSS */
|
|
194
|
+
cdnPath?: string | null;
|
|
195
|
+
/** Themes to preload */
|
|
196
|
+
preload?: string[];
|
|
197
|
+
/** Enable lazy loading */
|
|
198
|
+
lazy?: boolean;
|
|
199
|
+
/** localStorage key */
|
|
200
|
+
storageKey?: string;
|
|
201
|
+
/** Data attribute name */
|
|
202
|
+
dataAttribute?: string;
|
|
203
|
+
/** Enable persistence */
|
|
204
|
+
enablePersistence?: boolean;
|
|
205
|
+
/** Use minified CSS */
|
|
206
|
+
useMinified?: boolean;
|
|
207
|
+
/** Callback when theme changes */
|
|
208
|
+
onThemeChange?: (theme: string) => void;
|
|
209
|
+
/** Callback on error */
|
|
210
|
+
onError?: (error: Error, themeName: string) => void;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Theme context value
|
|
215
|
+
*/
|
|
216
|
+
export interface ThemeContextValue {
|
|
217
|
+
/** Current theme */
|
|
218
|
+
theme: string;
|
|
219
|
+
/** Set theme function */
|
|
220
|
+
setTheme: (theme: string, options?: ThemeLoadOptions) => Promise<void>;
|
|
221
|
+
/** Available themes */
|
|
222
|
+
availableThemes: ThemeMetadata[];
|
|
223
|
+
/** Loading state */
|
|
224
|
+
isLoading: boolean;
|
|
225
|
+
/** Error state */
|
|
226
|
+
error: Error | null;
|
|
227
|
+
/** Check if theme is loaded */
|
|
228
|
+
isThemeLoaded: (themeName: string) => boolean;
|
|
229
|
+
/** Preload theme */
|
|
230
|
+
preloadTheme: (themeName: string) => Promise<void>;
|
|
231
|
+
/** Theme manager instance */
|
|
232
|
+
themeManager: ThemeManagerType;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Storage adapter interface for custom storage implementations
|
|
237
|
+
*/
|
|
238
|
+
export interface StorageAdapter {
|
|
239
|
+
/** Get item from storage */
|
|
240
|
+
getItem(key: string): string | null;
|
|
241
|
+
/** Set item in storage */
|
|
242
|
+
setItem(key: string, value: string): void;
|
|
243
|
+
/** Remove item from storage */
|
|
244
|
+
removeItem(key: string): void;
|
|
245
|
+
/** Check if storage is available */
|
|
246
|
+
isAvailable(): boolean;
|
|
247
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { renderHook, act } from '@testing-library/react';
|
|
4
|
+
import { useTheme } from './useTheme';
|
|
5
|
+
import { ThemeContext } from './ThemeContext';
|
|
6
|
+
import type { ThemeContextValue } from './types';
|
|
7
|
+
|
|
8
|
+
describe('useTheme', () => {
|
|
9
|
+
const mockSetTheme = vi.fn(() => Promise.resolve());
|
|
10
|
+
const mockPreloadTheme = vi.fn(() => Promise.resolve());
|
|
11
|
+
const mockIsThemeLoaded = vi.fn(() => true);
|
|
12
|
+
|
|
13
|
+
const mockContextValue: ThemeContextValue = {
|
|
14
|
+
theme: 'default-theme',
|
|
15
|
+
setTheme: mockSetTheme,
|
|
16
|
+
availableThemes: [{ name: 'Default', class: 'default-theme' }],
|
|
17
|
+
isLoading: false,
|
|
18
|
+
error: null,
|
|
19
|
+
isThemeLoaded: mockIsThemeLoaded,
|
|
20
|
+
preloadTheme: mockPreloadTheme,
|
|
21
|
+
themeManager: {} as any, // We don't need the actual manager for this test
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
it('should throw error when used outside ThemeProvider', () => {
|
|
25
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
26
|
+
|
|
27
|
+
expect(() => {
|
|
28
|
+
renderHook(() => useTheme());
|
|
29
|
+
}).toThrow('useTheme must be used within a ThemeProvider');
|
|
30
|
+
|
|
31
|
+
consoleSpy.mockRestore();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return theme context values', () => {
|
|
35
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
36
|
+
<ThemeContext.Provider value={mockContextValue}>
|
|
37
|
+
{children}
|
|
38
|
+
</ThemeContext.Provider>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
42
|
+
|
|
43
|
+
expect(result.current.theme).toBe('default-theme');
|
|
44
|
+
expect(result.current.availableThemes).toHaveLength(1);
|
|
45
|
+
expect(result.current.isLoading).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should call onChange callback when provided', async () => {
|
|
49
|
+
const onChangeSpy = vi.fn();
|
|
50
|
+
|
|
51
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
52
|
+
<ThemeContext.Provider value={mockContextValue}>
|
|
53
|
+
{children}
|
|
54
|
+
</ThemeContext.Provider>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const { result } = renderHook(() => useTheme({ onChange: onChangeSpy }), { wrapper });
|
|
58
|
+
|
|
59
|
+
await act(async () => {
|
|
60
|
+
await result.current.setTheme('new-theme');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(mockSetTheme).toHaveBeenCalledWith('new-theme');
|
|
64
|
+
expect(onChangeSpy).toHaveBeenCalledWith('new-theme');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTheme Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for accessing and managing theme state
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useContext, useCallback } from 'react';
|
|
8
|
+
import { ThemeContext } from './ThemeContext';
|
|
9
|
+
import type { UseThemeReturn, UseThemeOptions, ThemeLoadOptions } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* useTheme hook
|
|
13
|
+
*
|
|
14
|
+
* Access theme context and manage theme state in React components.
|
|
15
|
+
* Must be used within a ThemeProvider.
|
|
16
|
+
*
|
|
17
|
+
* @param options - Hook options
|
|
18
|
+
* @returns Theme state and methods
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* function ThemeSwitcher() {
|
|
23
|
+
* const { theme, setTheme, availableThemes, isLoading } = useTheme();
|
|
24
|
+
*
|
|
25
|
+
* return (
|
|
26
|
+
* <select value={theme} onChange={(e) => setTheme(e.target.value)}>
|
|
27
|
+
* {availableThemes.map(t => (
|
|
28
|
+
* <option key={t.class} value={t.class}>{t.name}</option>
|
|
29
|
+
* ))}
|
|
30
|
+
* </select>
|
|
31
|
+
* );
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const useTheme = (options: UseThemeOptions = {}): UseThemeReturn => {
|
|
36
|
+
const context = useContext(ThemeContext);
|
|
37
|
+
|
|
38
|
+
if (!context) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'useTheme must be used within a ThemeProvider. ' +
|
|
41
|
+
'Wrap your component tree with <ThemeProvider> to use this hook.'
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
theme,
|
|
47
|
+
setTheme: contextSetTheme,
|
|
48
|
+
availableThemes,
|
|
49
|
+
isLoading,
|
|
50
|
+
error,
|
|
51
|
+
isThemeLoaded,
|
|
52
|
+
preloadTheme,
|
|
53
|
+
} = context;
|
|
54
|
+
|
|
55
|
+
// Extract onChange callback to avoid dependency on entire options object
|
|
56
|
+
const onChange = options?.onChange;
|
|
57
|
+
|
|
58
|
+
// Wrap setTheme to call onChange callback if provided
|
|
59
|
+
const setTheme = useCallback(
|
|
60
|
+
async (themeName: string, themeOptions?: ThemeLoadOptions): Promise<void> => {
|
|
61
|
+
await contextSetTheme(themeName, themeOptions);
|
|
62
|
+
if (onChange) {
|
|
63
|
+
onChange(themeName);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
[contextSetTheme, onChange]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
theme,
|
|
71
|
+
setTheme,
|
|
72
|
+
availableThemes,
|
|
73
|
+
isLoading,
|
|
74
|
+
error,
|
|
75
|
+
isThemeLoaded,
|
|
76
|
+
preloadTheme,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default useTheme;
|