@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +40 -1
  3. package/dist/atomix.css +96 -39
  4. package/dist/atomix.min.css +2 -2
  5. package/dist/index.d.ts +627 -2
  6. package/dist/index.esm.js +1292 -89
  7. package/dist/index.esm.js.map +1 -1
  8. package/dist/index.js +1316 -88
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.min.js +1 -1
  11. package/dist/index.min.js.map +1 -1
  12. package/dist/themes/applemix.css +96 -39
  13. package/dist/themes/applemix.min.css +2 -2
  14. package/dist/themes/boomdevs.css +96 -39
  15. package/dist/themes/boomdevs.min.css +2 -2
  16. package/dist/themes/esrar.css +96 -39
  17. package/dist/themes/esrar.min.css +2 -2
  18. package/dist/themes/flashtrade.css +97 -40
  19. package/dist/themes/flashtrade.min.css +2 -2
  20. package/dist/themes/mashroom.css +96 -39
  21. package/dist/themes/mashroom.min.css +3 -3
  22. package/dist/themes/shaj-default.css +96 -39
  23. package/dist/themes/shaj-default.min.css +2 -2
  24. package/package.json +13 -2
  25. package/src/components/Card/Card.tsx +9 -4
  26. package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +301 -13
  27. package/src/components/Navigation/SideMenu/SideMenu.tsx +236 -9
  28. package/src/lib/composables/useSideMenu.ts +89 -30
  29. package/src/lib/index.ts +5 -0
  30. package/src/lib/theme/ThemeContext.tsx +17 -0
  31. package/src/lib/theme/ThemeManager.stories.tsx +472 -0
  32. package/src/lib/theme/ThemeManager.test.ts +186 -0
  33. package/src/lib/theme/ThemeManager.ts +501 -0
  34. package/src/lib/theme/ThemeProvider.tsx +227 -0
  35. package/src/lib/theme/index.ts +56 -0
  36. package/src/lib/theme/types.ts +247 -0
  37. package/src/lib/theme/useTheme.test.tsx +66 -0
  38. package/src/lib/theme/useTheme.ts +80 -0
  39. package/src/lib/theme/utils.test.ts +140 -0
  40. package/src/lib/theme/utils.ts +398 -0
  41. package/src/lib/types/components.ts +26 -0
  42. package/src/styles/06-components/_components.card.scss +39 -24
  43. 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;