@shohojdhara/atomix 0.5.2 → 0.5.4

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 (39) hide show
  1. package/atomix.config.ts +33 -33
  2. package/dist/config.d.ts +187 -112
  3. package/dist/config.js +7 -49
  4. package/dist/config.js.map +1 -1
  5. package/dist/index.d.ts +1958 -900
  6. package/dist/index.esm.js +2275 -383
  7. package/dist/index.esm.js.map +1 -1
  8. package/dist/index.js +2327 -417
  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/theme.d.ts +1390 -276
  13. package/dist/theme.js +2129 -621
  14. package/dist/theme.js.map +1 -1
  15. package/package.json +1 -1
  16. package/scripts/cli/internal/config-loader.js +30 -20
  17. package/src/lib/config/index.ts +38 -362
  18. package/src/lib/config/loader.ts +419 -0
  19. package/src/lib/config/public-api.ts +43 -0
  20. package/src/lib/config/types.ts +389 -0
  21. package/src/lib/config/validator.ts +305 -0
  22. package/src/lib/theme/adapters/index.ts +1 -1
  23. package/src/lib/theme/adapters/themeAdapter.ts +358 -229
  24. package/src/lib/theme/components/ThemeToggle.tsx +276 -0
  25. package/src/lib/theme/config/configLoader.ts +351 -0
  26. package/src/lib/theme/config/loader.ts +221 -0
  27. package/src/lib/theme/core/createTheme.ts +126 -50
  28. package/src/lib/theme/core/createThemeObject.ts +7 -4
  29. package/src/lib/theme/hooks/useThemeSwitcher.ts +164 -0
  30. package/src/lib/theme/index.ts +322 -38
  31. package/src/lib/theme/runtime/ThemeProvider.tsx +44 -10
  32. package/src/lib/theme/runtime/__tests__/ThemeProvider.test.tsx +44 -393
  33. package/src/lib/theme/runtime/useTheme.ts +1 -0
  34. package/src/lib/theme/tokens/tokens.ts +101 -1
  35. package/src/lib/theme/types.ts +91 -0
  36. package/src/lib/theme/utils/performanceMonitor.ts +315 -0
  37. package/src/lib/theme/utils/responsive.ts +280 -0
  38. package/src/lib/theme/utils/themeUtils.ts +531 -117
  39. package/src/styles/05-objects/_objects.masonry-grid.scss +3 -3
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Theme Configuration Loader
3
+ *
4
+ * Loads and validates the theme configuration from atomix.config.ts (and other formats)
5
+ */
6
+
7
+ import type {
8
+ ConfigLoaderOptions,
9
+ LoadedThemeConfig,
10
+ ConfigValidationResult,
11
+ } from './types';
12
+ import { validateConfig } from './validator';
13
+ import { ThemeError, ThemeErrorCode, getLogger } from '../errors';
14
+ import {
15
+ DEFAULT_ATOMIX_CONFIG_PATH,
16
+ DEFAULT_CONFIG_RELATIVE_PATH,
17
+ DEFAULT_BASE_PATH,
18
+ DEFAULT_STORAGE_KEY,
19
+ DEFAULT_DATA_ATTRIBUTE,
20
+ DEFAULT_INTEGRATION_CLASS_NAMES,
21
+ DEFAULT_INTEGRATION_CSS_VARIABLES,
22
+ DEFAULT_BUILD_OUTPUT_DIR,
23
+ DEFAULT_SASS_CONFIG,
24
+ ENV_DEFAULTS,
25
+ } from '../constants';
26
+
27
+ /**
28
+ * Cache for loaded configuration
29
+ */
30
+ let cachedConfig: LoadedThemeConfig | null = null;
31
+
32
+ /**
33
+ * Logger instance
34
+ */
35
+ const logger = getLogger();
36
+
37
+ /**
38
+ * Load theme configuration from atomix.config.ts (and other formats)
39
+ *
40
+ * @param options - Loader options
41
+ * @returns Loaded and validated theme configuration
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * import { loadThemeConfig } from '@shohojdhara/atomix/theme/config';
46
+ * const config = loadThemeConfig();
47
+ * ```
48
+ */
49
+ export function loadThemeConfig(
50
+ options: ConfigLoaderOptions = {}
51
+ ): LoadedThemeConfig {
52
+ const {
53
+ configPath = DEFAULT_ATOMIX_CONFIG_PATH,
54
+ validate = true,
55
+ env = typeof process !== 'undefined' && process.env ? (process.env.NODE_ENV === 'production' ? 'production' : 'development') : 'development',
56
+ } = options;
57
+
58
+ // Return cached config if available
59
+ if (cachedConfig) {
60
+ return cachedConfig;
61
+ }
62
+
63
+ // Try to load config dynamically
64
+ let config: LoadedThemeConfig;
65
+
66
+ try {
67
+ // In browser/Vite environment, we can't load config dynamically
68
+ if (typeof window !== 'undefined') {
69
+ throw new Error('Theme config loading not supported in browser environment');
70
+ }
71
+
72
+ // In ESM environments, require might be undefined.
73
+ let nodeRequire: any;
74
+ try {
75
+ nodeRequire = require;
76
+ } catch {
77
+ // require is not defined
78
+ }
79
+
80
+ if (!nodeRequire) {
81
+ throw new Error('Theme config loading not supported in this environment (require is undefined)');
82
+ }
83
+
84
+ // Type for config module
85
+ interface ConfigModule {
86
+ default?: any;
87
+ [key: string]: unknown;
88
+ }
89
+
90
+ let configModule: ConfigModule;
91
+
92
+ // First, try to resolve the config file path using our unified approach
93
+ let resolvedConfigPath: string | null = null;
94
+
95
+ // If a specific config path is provided, try to use it
96
+ if (configPath && configPath !== DEFAULT_ATOMIX_CONFIG_PATH) {
97
+ const path = nodeRequire('path') as typeof import('path');
98
+ const fs = nodeRequire('fs') as typeof import('fs');
99
+ const fullPath = path.resolve(process.cwd(), configPath);
100
+
101
+ if (fs.existsSync(fullPath)) {
102
+ resolvedConfigPath = fullPath;
103
+ }
104
+ } else {
105
+ // Otherwise, look for any of the supported config formats
106
+ const possiblePaths = [
107
+ 'atomix.config.ts',
108
+ 'atomix.config.js',
109
+ 'atomix.config.json'
110
+ ];
111
+
112
+ const path = nodeRequire('path') as typeof import('path');
113
+ const fs = nodeRequire('fs') as typeof import('fs');
114
+
115
+ for (const fileName of possiblePaths) {
116
+ const fullPath = path.resolve(process.cwd(), fileName);
117
+ if (fs.existsSync(fullPath)) {
118
+ resolvedConfigPath = fullPath;
119
+ break;
120
+ }
121
+ }
122
+ }
123
+
124
+ if (!resolvedConfigPath) {
125
+ throw new ThemeError(
126
+ `Config file not found: ${configPath}`,
127
+ ThemeErrorCode.CONFIG_LOAD_FAILED,
128
+ { configPath }
129
+ );
130
+ }
131
+
132
+ // Handle JSON files differently
133
+ if (resolvedConfigPath.endsWith('.json')) {
134
+ const fs = nodeRequire('fs') as typeof import('fs');
135
+ configModule = JSON.parse(fs.readFileSync(resolvedConfigPath, 'utf8'));
136
+ } else {
137
+ // Use require (Node.js/CommonJS) for JS/TS files
138
+ try {
139
+ const resolvedPath = nodeRequire.resolve(resolvedConfigPath);
140
+ if (nodeRequire.cache && nodeRequire.cache[resolvedPath]) {
141
+ delete nodeRequire.cache[resolvedPath];
142
+ }
143
+ configModule = nodeRequire(resolvedConfigPath) as ConfigModule;
144
+ } catch (requireError) {
145
+ const errorMessage = requireError instanceof Error
146
+ ? requireError.message
147
+ : String(requireError);
148
+ throw new ThemeError(
149
+ `Cannot load config: ${errorMessage}`,
150
+ ThemeErrorCode.CONFIG_LOAD_FAILED,
151
+ { configPath: resolvedConfigPath, error: errorMessage }
152
+ );
153
+ }
154
+ }
155
+
156
+ const rawConfig = configModule.default || configModule;
157
+
158
+ // Process the AtomixConfig structure
159
+ const processedConfig: LoadedThemeConfig = {
160
+ themes: rawConfig.theme?.themes || {},
161
+ build: rawConfig.build || {},
162
+ runtime: rawConfig.runtime || {},
163
+ integration: rawConfig.integration || {},
164
+ dependencies: rawConfig.dependencies || {},
165
+ validated: false, // Will be set after validation
166
+ // Store tokens for generator
167
+ __tokens: rawConfig.theme?.tokens,
168
+ __extend: rawConfig.theme?.extend,
169
+ };
170
+
171
+ // Store the raw config in the result for later use
172
+ Object.assign(processedConfig, rawConfig);
173
+
174
+ config = processedConfig;
175
+
176
+ // Validate the config if requested
177
+ if (validate) {
178
+ const validationResult = validateConfig(config);
179
+ if (!validationResult.valid) {
180
+ logger.warn(`Configuration validation warnings:\n${validationResult.warnings.join('\n')}`);
181
+ if (validationResult.errors.length > 0) {
182
+ logger.error(`Configuration validation errors:\n${validationResult.errors.join('\n')}`);
183
+ throw new ThemeError(
184
+ 'Configuration validation failed',
185
+ ThemeErrorCode.CONFIG_VALIDATION_FAILED,
186
+ { errors: validationResult.errors }
187
+ );
188
+ }
189
+ }
190
+ config.validated = true;
191
+ } else {
192
+ config.validated = false;
193
+ }
194
+
195
+ // Cache the loaded config
196
+ cachedConfig = config;
197
+
198
+ logger.info(`Successfully loaded theme configuration from ${resolvedConfigPath}`);
199
+ return config;
200
+ } catch (error) {
201
+ if (error instanceof ThemeError) {
202
+ throw error;
203
+ }
204
+
205
+ throw new ThemeError(
206
+ `Failed to load theme configuration: ${(error as Error).message}`,
207
+ ThemeErrorCode.CONFIG_LOAD_FAILED,
208
+ { configPath, error: (error as Error).stack }
209
+ );
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Clear the configuration cache
215
+ *
216
+ * This is useful when the config file has been modified and needs to be reloaded
217
+ */
218
+ export function clearConfigCache(): void {
219
+ cachedConfig = null;
220
+ logger.debug('Configuration cache cleared');
221
+ }
@@ -1,93 +1,169 @@
1
1
  /**
2
2
  * Core Theme Functions
3
3
  *
4
- * Simplified theme system using DesignTokens only.
4
+ * Unified theme system that handles both DesignTokens and Theme objects.
5
5
  * Config-first approach: loads from atomix.config.ts when no input is provided.
6
+ * Config-first approach: loads advanced features from config when available.
6
7
  */
7
8
 
8
9
  import type { DesignTokens } from '../tokens/tokens';
10
+ import type { Theme } from '../types';
9
11
  import type { GenerateCSSVariablesOptions } from '../generators/generateCSS';
10
12
  import { createTokens } from '../tokens/tokens';
11
13
  import { generateCSSVariables } from '../generators/generateCSS';
12
- import { ThemeError, ThemeErrorCode } from '../errors/errors';
14
+ import { themeToDesignTokens } from '../adapters/themeAdapter';
15
+ import { loadThemeFromConfigSync } from '../config/configLoader';
16
+ import { loadAtomixConfig } from '../../config/loader';
13
17
 
14
18
  /**
15
- * Create theme CSS from DesignTokens
16
- *
19
+ * Create theme CSS from tokens or Theme object
20
+ *
17
21
  * **Config-First Approach**: If no input is provided, loads from `atomix.config.ts`.
18
- *
19
- * @param input - DesignTokens (partial) or undefined (loads from config)
22
+ * Config file is required for automatic loading.
23
+ *
24
+ * @param input - DesignTokens (partial), Theme object, or undefined (loads from config)
20
25
  * @param options - CSS generation options (prefix is automatically read from config if not provided)
21
26
  * @returns CSS string with custom properties
22
27
  * @throws Error if config loading fails when no input is provided
23
- *
28
+ *
24
29
  * @example
25
30
  * ```typescript
26
- * // Loads from atomix.config.ts
31
+ * // Loads from atomix.config.ts (config file required)
27
32
  * const css = createTheme();
28
- *
33
+ *
29
34
  * // Using DesignTokens
30
35
  * const css = createTheme({
31
36
  * 'primary': '#7c3aed',
32
37
  * 'spacing-4': '1rem',
33
38
  * });
34
- *
39
+ *
40
+ * // Using Theme object
41
+ * const theme = createThemeObject({ palette: { primary: { main: '#7c3aed' } } });
42
+ * const css = createTheme(theme);
43
+ *
35
44
  * // With custom options
36
45
  * const css = createTheme(undefined, { prefix: 'myapp', selector: ':root' });
37
46
  * ```
38
47
  */
39
48
  export function createTheme(
40
- input?: Partial<DesignTokens>,
49
+ input?: Partial<DesignTokens> | Theme,
41
50
  options?: GenerateCSSVariablesOptions
42
51
  ): string {
43
- // Validate options if provided
44
- if (options?.prefix) {
45
- const prefixPattern = /^[a-z][a-z0-9-]*$/;
46
- if (!prefixPattern.test(options.prefix)) {
47
- throw new ThemeError(
48
- `Invalid CSS variable prefix: "${options.prefix}". Prefix must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens (e.g., "atomix", "my-app").`,
49
- ThemeErrorCode.THEME_VALIDATION_FAILED,
50
- { prefix: options.prefix, pattern: prefixPattern.toString() }
51
- );
52
- }
53
- }
52
+ let tokens: Partial<DesignTokens>;
53
+ let configPrefix: string | undefined;
54
54
 
55
- // Validate selector if provided
56
- if (options?.selector) {
57
- // Basic validation - selector should be a valid CSS selector
58
- if (typeof options.selector !== 'string' || options.selector.trim().length === 0) {
59
- throw new ThemeError(
60
- `Invalid CSS selector: "${options.selector}". Selector must be a non-empty string (e.g., ":root", ".my-theme").`,
61
- ThemeErrorCode.THEME_VALIDATION_FAILED,
62
- { selector: options.selector }
63
- );
55
+ // If no input provided, load from config (required)
56
+ if (!input) {
57
+ const configTokens = loadThemeFromConfigSync();
58
+
59
+ // Get prefix from config
60
+ try {
61
+ // Use the imported function directly instead of require to avoid bundling issues
62
+ const config = loadAtomixConfig({ configPath: 'atomix.config.ts', required: true });
63
+ configPrefix = config?.prefix;
64
+ } catch (error) {
65
+ // Prefix loading failed, but tokens were loaded, so continue
66
+ }
67
+
68
+ tokens = configTokens;
69
+ } else {
70
+ // Check if it's a Theme object
71
+ const isThemeObject = (input as any).__isJSTheme === true ||
72
+ ((input as any).palette && (input as any).typography);
73
+
74
+ if (isThemeObject) {
75
+ // Convert Theme object to DesignTokens
76
+ tokens = themeToDesignTokens(input as Theme);
77
+ } else {
78
+ // Use DesignTokens directly
79
+ tokens = input as Partial<DesignTokens>;
64
80
  }
65
81
  }
66
82
 
83
+ // Merge with defaults and generate CSS
84
+ const allTokens = createTokens(tokens);
85
+
86
+ // Get prefix from options, config, or use default
87
+ const prefix = options?.prefix ?? configPrefix ?? 'atomix';
88
+
89
+ return generateCSSVariables(allTokens, { ...options, prefix });
90
+ }
91
+
92
+ // Helper type guard function
93
+ function isThemeObject(obj: any): obj is Theme {
94
+ return obj && typeof obj === 'object' && (
95
+ obj.palette ||
96
+ obj.typography ||
97
+ obj.spacing ||
98
+ obj.breakpoints ||
99
+ obj.colors
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Create theme CSS from tokens or Theme object (asynchronous version)
105
+ *
106
+ * **Config-First Approach**: If no input is provided, loads from `atomix.config.ts`.
107
+ * Config file is required for automatic loading.
108
+ *
109
+ * @param input - DesignTokens (partial), Theme object, or undefined (loads from config)
110
+ * @param options - CSS generation options (prefix is automatically read from config if not provided)
111
+ * @returns Promise resolving to CSS string with custom properties
112
+ * @throws Error if config loading fails when no input is provided
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * // Loads from atomix.config.ts (config file required)
117
+ * const css = await createThemeAsync();
118
+ *
119
+ * // Using DesignTokens
120
+ * const css = await createThemeAsync({
121
+ * 'primary': '#7c3aed',
122
+ * 'spacing-4': '1rem',
123
+ * });
124
+ *
125
+ * // Using Theme object
126
+ * const theme = createThemeObject({ palette: { primary: { main: '#7c3aed' } } });
127
+ * const css = await createThemeAsync(theme);
128
+ *
129
+ * // With custom options
130
+ * const css = await createThemeAsync(undefined, { prefix: 'myapp', selector: ':root' });
131
+ * ```
132
+ */
133
+ export async function createThemeAsync(
134
+ input?: Partial<DesignTokens> | Theme,
135
+ options?: GenerateCSSVariablesOptions
136
+ ): Promise<string> {
67
137
  // Determine tokens based on input
68
138
  let tokens: Partial<DesignTokens>;
69
-
139
+
70
140
  if (!input) {
71
- // Auto-loading config from file system is removed for browser compatibility.
72
- // If no input is provided, we return an empty theme (using defaults only) or user must provide tokens.
73
- // This allows createTheme to be isomorphic.
74
-
75
- // Warn in development if no input provided
76
- if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') {
77
- console.warn('Atomix: createTheme() called without tokens. Using default tokens only.');
141
+ // Load from config when no input provided
142
+ if (typeof window !== 'undefined') {
143
+ throw new Error('createTheme: No input provided and config loading is not available in browser environment. Please provide tokens explicitly or use Node.js/SSR environment.');
78
144
  }
79
-
80
- tokens = {};
81
- } else {
82
- // Validate input tokens structure
83
- if (typeof input !== 'object' || input === null || Array.isArray(input)) {
84
- throw new ThemeError(
85
- `Invalid tokens input. Expected an object with DesignTokens, but received: ${typeof input}.`,
86
- ThemeErrorCode.THEME_VALIDATION_FAILED,
87
- { inputType: typeof input }
88
- );
145
+
146
+ // Dynamically import config loaders in a way that prevents bundling in browser
147
+ const { loadThemeFromConfigSync } = await import('../config/configLoader');
148
+ const { loadAtomixConfig } = await import('../../config/loader');
149
+
150
+ tokens = loadThemeFromConfigSync();
151
+
152
+ // Get prefix from config if needed
153
+ if (!options?.prefix) {
154
+ try {
155
+ const config = loadAtomixConfig({ configPath: 'atomix.config.ts', required: false });
156
+ options = { ...options, prefix: config?.prefix || 'atomix' };
157
+ } catch (error) {
158
+ // If config loading fails, use default prefix
159
+ options = { ...options, prefix: 'atomix' };
160
+ }
89
161
  }
90
-
162
+ } else if (isThemeObject(input)) {
163
+ // Convert Theme object to DesignTokens
164
+ const { themeToDesignTokens } = await import('../adapters/themeAdapter');
165
+ tokens = themeToDesignTokens(input);
166
+ } else {
91
167
  // Use DesignTokens directly
92
168
  tokens = input;
93
169
  }
@@ -242,16 +242,16 @@ function createPaletteColor(color: Partial<PaletteColor> | string): PaletteColor
242
242
  if (typeof color === 'string') {
243
243
  return {
244
244
  main: color,
245
- light: lighten(color),
246
- dark: darken(color),
245
+ light: lighten(color, 0.15),
246
+ dark: darken(color, 0.15),
247
247
  contrastText: getContrastText(color),
248
248
  };
249
249
  }
250
250
 
251
251
  return {
252
252
  main: color.main || '#000000',
253
- light: color.light || lighten(color.main || '#000000'),
254
- dark: color.dark || darken(color.main || '#000000'),
253
+ light: color.light || lighten(color.main || '#000000', 0.15),
254
+ dark: color.dark || darken(color.main || '#000000', 0.15),
255
255
  contrastText: color.contrastText || getContrastText(color.main || '#000000'),
256
256
  };
257
257
  }
@@ -327,6 +327,7 @@ export function createThemeObject(...options: ThemeOptions[]): Theme {
327
327
  }),
328
328
  background: {
329
329
  default: mergedOptions.palette?.background?.default || DEFAULT_PALETTE.background.default,
330
+ paper: mergedOptions.palette?.background?.paper || DEFAULT_PALETTE.background.paper,
330
331
  subtle: mergedOptions.palette?.background?.subtle || DEFAULT_PALETTE.background.subtle,
331
332
  },
332
333
  text: {
@@ -334,6 +335,8 @@ export function createThemeObject(...options: ThemeOptions[]): Theme {
334
335
  secondary: mergedOptions.palette?.text?.secondary || DEFAULT_PALETTE.text.secondary,
335
336
  disabled: mergedOptions.palette?.text?.disabled || DEFAULT_PALETTE.text.disabled,
336
337
  },
338
+ // Spread other palette properties
339
+ ...mergedOptions.palette,
337
340
  };
338
341
 
339
342
  // Create typography
@@ -0,0 +1,164 @@
1
+ /**
2
+ * useThemeSwitcher Hook
3
+ *
4
+ * React hook for managing theme switching with persistence and system preference detection.
5
+ * Provides an easy-to-use API for dark/light mode toggling.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { useThemeSwitcher } from '@shohojdhara/atomix/theme';
10
+ *
11
+ * function ThemeToggle() {
12
+ * const { mode, toggle, setMode, isDark } = useThemeSwitcher();
13
+ *
14
+ * return (
15
+ * <button onClick={toggle}>
16
+ * {isDark ? '☀️ Light' : '🌙 Dark'}
17
+ * </button>
18
+ * );
19
+ * }
20
+ * ```
21
+ */
22
+
23
+ import { useState, useEffect, useCallback } from 'react';
24
+ import {
25
+ switchTheme,
26
+ toggleTheme,
27
+ getCurrentTheme,
28
+ getSystemTheme,
29
+ initializeTheme,
30
+ listenToSystemTheme,
31
+ type ThemeMode,
32
+ type ThemeSwitcherOptions,
33
+ } from '../utils/themeUtils';
34
+
35
+ export interface UseThemeSwitcherOptions extends ThemeSwitcherOptions {
36
+ /** Initial theme mode (default: 'system') */
37
+ initialMode?: ThemeMode;
38
+ /** Automatically sync with system preference (default: false) */
39
+ syncWithSystem?: boolean;
40
+ }
41
+
42
+ export interface UseThemeSwitcherReturn {
43
+ /** Current theme mode */
44
+ mode: ThemeMode;
45
+ /** Whether current theme is dark */
46
+ isDark: boolean;
47
+ /** Whether current theme is light */
48
+ isLight: boolean;
49
+ /** Toggle between light and dark */
50
+ toggle: () => ThemeMode;
51
+ /** Set specific theme mode */
52
+ setMode: (mode: ThemeMode) => void;
53
+ /** Reset to system preference */
54
+ resetToSystem: () => void;
55
+ /** Clear saved preference */
56
+ clearPreference: () => void;
57
+ }
58
+
59
+ /**
60
+ * Hook for managing theme switching
61
+ *
62
+ * @param options - Configuration options
63
+ * @returns Theme switcher controls
64
+ */
65
+ export function useThemeSwitcher(options: UseThemeSwitcherOptions = {}): UseThemeSwitcherReturn {
66
+ const {
67
+ initialMode = 'system',
68
+ syncWithSystem = false,
69
+ storageKey = 'atomix-theme',
70
+ enableTransition = true,
71
+ transitionDuration = 300,
72
+ } = options;
73
+
74
+ // State for current mode
75
+ const [mode, setModeState] = useState<ThemeMode>(() => {
76
+ if (typeof window === 'undefined') return initialMode;
77
+
78
+ // Check for saved preference first
79
+ const saved = getCurrentTheme(storageKey);
80
+ if (saved && saved !== 'system') return saved;
81
+
82
+ // Fall back to initial mode or system
83
+ return initialMode === 'system' ? getSystemTheme() : initialMode;
84
+ });
85
+
86
+ // Initialize theme on mount
87
+ useEffect(() => {
88
+ if (typeof window === 'undefined') return;
89
+
90
+ // Initialize with proper theme application
91
+ initializeTheme({
92
+ storageKey,
93
+ enableTransition,
94
+ transitionDuration,
95
+ });
96
+
97
+ // Update state to match initialized theme
98
+ setModeState(getCurrentTheme(storageKey));
99
+ }, [storageKey, enableTransition, transitionDuration]);
100
+
101
+ // Listen for system theme changes if enabled
102
+ useEffect(() => {
103
+ if (!syncWithSystem) return;
104
+
105
+ const cleanup = listenToSystemTheme((newMode) => {
106
+ setModeState(newMode);
107
+ switchTheme(newMode, {
108
+ storageKey,
109
+ enableTransition,
110
+ transitionDuration,
111
+ });
112
+ });
113
+
114
+ return cleanup;
115
+ }, [syncWithSystem, storageKey, enableTransition, transitionDuration]);
116
+
117
+ // Toggle theme
118
+ const toggle = useCallback((): ThemeMode => {
119
+ const newMode = toggleTheme({
120
+ storageKey,
121
+ enableTransition,
122
+ transitionDuration,
123
+ });
124
+ setModeState(newMode);
125
+ return newMode;
126
+ }, [storageKey, enableTransition, transitionDuration]);
127
+
128
+ // Set specific mode
129
+ const setMode = useCallback((newMode: ThemeMode) => {
130
+ switchTheme(newMode, {
131
+ storageKey,
132
+ enableTransition,
133
+ transitionDuration,
134
+ });
135
+ setModeState(newMode);
136
+ }, [storageKey, enableTransition, transitionDuration]);
137
+
138
+ // Reset to system preference
139
+ const resetToSystem = useCallback(() => {
140
+ const systemMode = getSystemTheme();
141
+ switchTheme(systemMode, {
142
+ storageKey,
143
+ enableTransition,
144
+ transitionDuration,
145
+ });
146
+ setModeState(systemMode);
147
+ }, [storageKey, enableTransition, transitionDuration]);
148
+
149
+ // Clear saved preference
150
+ const clearPreference = useCallback(() => {
151
+ if (typeof window === 'undefined') return;
152
+ localStorage.removeItem(storageKey);
153
+ }, [storageKey]);
154
+
155
+ return {
156
+ mode,
157
+ isDark: mode === 'dark',
158
+ isLight: mode === 'light',
159
+ toggle,
160
+ setMode,
161
+ resetToSystem,
162
+ clearPreference,
163
+ };
164
+ }