@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.
- package/atomix.config.ts +33 -33
- package/dist/config.d.ts +187 -112
- package/dist/config.js +7 -49
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1958 -900
- package/dist/index.esm.js +2275 -383
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2327 -417
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/theme.d.ts +1390 -276
- package/dist/theme.js +2129 -621
- package/dist/theme.js.map +1 -1
- package/package.json +1 -1
- package/scripts/cli/internal/config-loader.js +30 -20
- package/src/lib/config/index.ts +38 -362
- package/src/lib/config/loader.ts +419 -0
- package/src/lib/config/public-api.ts +43 -0
- package/src/lib/config/types.ts +389 -0
- package/src/lib/config/validator.ts +305 -0
- package/src/lib/theme/adapters/index.ts +1 -1
- package/src/lib/theme/adapters/themeAdapter.ts +358 -229
- package/src/lib/theme/components/ThemeToggle.tsx +276 -0
- package/src/lib/theme/config/configLoader.ts +351 -0
- package/src/lib/theme/config/loader.ts +221 -0
- package/src/lib/theme/core/createTheme.ts +126 -50
- package/src/lib/theme/core/createThemeObject.ts +7 -4
- package/src/lib/theme/hooks/useThemeSwitcher.ts +164 -0
- package/src/lib/theme/index.ts +322 -38
- package/src/lib/theme/runtime/ThemeProvider.tsx +44 -10
- package/src/lib/theme/runtime/__tests__/ThemeProvider.test.tsx +44 -393
- package/src/lib/theme/runtime/useTheme.ts +1 -0
- package/src/lib/theme/tokens/tokens.ts +101 -1
- package/src/lib/theme/types.ts +91 -0
- package/src/lib/theme/utils/performanceMonitor.ts +315 -0
- package/src/lib/theme/utils/responsive.ts +280 -0
- package/src/lib/theme/utils/themeUtils.ts +531 -117
- 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
|
-
*
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
//
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
}
|