@shohojdhara/atomix 0.3.6 → 0.3.8
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/README.md +3 -3
- package/dist/atomix.css +77 -0
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +77 -0
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.js +50 -142
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/core.js +179 -274
- package/dist/core.js.map +1 -1
- package/dist/forms.js +50 -142
- package/dist/forms.js.map +1 -1
- package/dist/heavy.js +179 -274
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +1255 -1226
- package/dist/index.esm.js +2806 -2958
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3113 -3269
- 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 +313 -667
- package/dist/theme.js +1818 -2589
- package/dist/theme.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AtomixGlass/AtomixGlass.tsx +128 -356
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +1 -1
- package/src/components/Button/Button.tsx +85 -167
- package/src/components/DataTable/DataTable.stories.tsx +238 -0
- package/src/components/DataTable/DataTable.test.tsx +450 -0
- package/src/components/DataTable/DataTable.tsx +384 -61
- package/src/components/DatePicker/DatePicker.tsx +29 -38
- package/src/components/Upload/Upload.tsx +539 -40
- package/src/lib/composables/useAtomixGlass.ts +7 -7
- package/src/lib/composables/useDataTable.ts +355 -15
- package/src/lib/composables/useDatePicker.ts +19 -0
- package/src/lib/config/loader.ts +2 -3
- package/src/lib/constants/components.ts +17 -0
- package/src/lib/hooks/usePerformanceMonitor.ts +1 -1
- package/src/lib/theme/adapters/cssVariableMapper.ts +29 -14
- package/src/lib/theme/adapters/index.ts +1 -4
- package/src/lib/theme/config/configLoader.ts +82 -223
- package/src/lib/theme/config/loader.ts +15 -21
- package/src/lib/theme/constants/constants.ts +1 -1
- package/src/lib/theme/core/ThemeRegistry.ts +75 -279
- package/src/lib/theme/core/composeTheme.ts +30 -88
- package/src/lib/theme/core/createTheme.ts +88 -51
- package/src/lib/theme/core/createThemeObject.ts +2 -2
- package/src/lib/theme/core/index.ts +15 -2
- package/src/lib/theme/errors/errors.ts +1 -1
- package/src/lib/theme/generators/generateCSSNested.ts +131 -0
- package/src/lib/theme/generators/generateCSSVariables.ts +24 -16
- package/src/lib/theme/generators/index.ts +6 -0
- package/src/lib/theme/index.ts +45 -27
- package/src/lib/theme/runtime/ThemeApplicator.ts +6 -109
- package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +1 -1
- package/src/lib/theme/runtime/ThemeProvider.tsx +393 -544
- package/src/lib/theme/runtime/index.ts +1 -0
- package/src/lib/theme/runtime/useTheme.ts +1 -1
- package/src/lib/theme/runtime/useThemeTokens.ts +122 -0
- package/src/lib/theme/test/testTheme.ts +2 -1
- package/src/lib/theme/types.ts +14 -14
- package/src/lib/theme/utils/componentTheming.ts +140 -0
- package/src/lib/theme/utils/domUtils.ts +57 -15
- package/src/lib/theme/utils/injectCSS.ts +0 -1
- package/src/lib/theme/utils/naming.ts +100 -0
- package/src/lib/theme/utils/themeHelpers.ts +1 -39
- package/src/lib/theme/utils/themeUtils.ts +1 -170
- package/src/lib/types/components.ts +145 -0
- package/src/lib/utils/componentUtils.ts +1 -1
- package/src/lib/utils/dataTableExport.ts +143 -0
- package/src/lib/utils/memoryMonitor.ts +3 -3
- package/src/lib/utils/themeNaming.ts +135 -0
- package/src/styles/06-components/_components.data-table.scss +95 -0
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
export { ThemeProvider } from './ThemeProvider';
|
|
8
8
|
export { ThemeErrorBoundary } from './ThemeErrorBoundary';
|
|
9
9
|
export { useTheme } from './useTheme';
|
|
10
|
+
export { useThemeTokens } from './useThemeTokens';
|
|
10
11
|
export { ThemeApplicator, getThemeApplicator, applyTheme } from './ThemeApplicator';
|
|
11
12
|
export type { ThemeErrorBoundaryProps } from './ThemeErrorBoundary';
|
|
12
13
|
|
|
@@ -38,7 +38,7 @@ export function useTheme(): UseThemeReturn {
|
|
|
38
38
|
|
|
39
39
|
return {
|
|
40
40
|
theme: context.theme,
|
|
41
|
-
|
|
41
|
+
activeTokens: context.activeTokens,
|
|
42
42
|
setTheme: context.setTheme,
|
|
43
43
|
availableThemes: context.availableThemes,
|
|
44
44
|
isLoading: context.isLoading,
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useTheme } from './useTheme';
|
|
3
|
+
import type { DesignTokens } from '../tokens/tokens';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Standardized hook for accessing theme tokens in components
|
|
7
|
+
*
|
|
8
|
+
* Provides consistent access to theme values using CSS custom properties
|
|
9
|
+
* and DesignTokens.
|
|
10
|
+
*/
|
|
11
|
+
type ThemeTokens = {
|
|
12
|
+
theme: string;
|
|
13
|
+
activeTokens: DesignTokens | null;
|
|
14
|
+
getToken: (tokenName: string, fallback?: string) => string;
|
|
15
|
+
colors: {
|
|
16
|
+
primary: string;
|
|
17
|
+
secondary: string;
|
|
18
|
+
error: string;
|
|
19
|
+
success: string;
|
|
20
|
+
warning: string;
|
|
21
|
+
info: string;
|
|
22
|
+
light: string;
|
|
23
|
+
dark: string;
|
|
24
|
+
};
|
|
25
|
+
spacing: Record<string, string>;
|
|
26
|
+
borderRadius: Record<string, string>;
|
|
27
|
+
typography: {
|
|
28
|
+
fontFamily: Record<string, string>;
|
|
29
|
+
fontSize: Record<string, string>;
|
|
30
|
+
fontWeight: Record<string, string>;
|
|
31
|
+
};
|
|
32
|
+
shadows: Record<string, string>;
|
|
33
|
+
transitions: Record<string, string>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function useThemeTokens(): ThemeTokens {
|
|
37
|
+
const { theme, activeTokens } = useTheme();
|
|
38
|
+
|
|
39
|
+
// Helper function to get CSS variable value
|
|
40
|
+
const getToken = useCallback((tokenName: string, fallback?: string) => {
|
|
41
|
+
if (typeof window === 'undefined') return fallback || '';
|
|
42
|
+
|
|
43
|
+
const cssVarName = `--atomix-${tokenName}`;
|
|
44
|
+
const computedStyle = getComputedStyle(document.documentElement);
|
|
45
|
+
return computedStyle.getPropertyValue(cssVarName).trim() || fallback || '';
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
// Return unified API for accessing theme values
|
|
49
|
+
// Note: For SSR or direct token access, use activeTokens directly
|
|
50
|
+
return <ThemeTokens>{
|
|
51
|
+
theme,
|
|
52
|
+
activeTokens,
|
|
53
|
+
getToken,
|
|
54
|
+
// Commonly used tokens with fallbacks
|
|
55
|
+
colors: {
|
|
56
|
+
primary: getToken('primary', '#3b82f6'),
|
|
57
|
+
secondary: getToken('secondary', '#10b981'),
|
|
58
|
+
error: getToken('error', '#ef4444'),
|
|
59
|
+
success: getToken('success', '#22c55e'),
|
|
60
|
+
warning: getToken('warning', '#eab308'),
|
|
61
|
+
info: getToken('info', '#3b82f6'),
|
|
62
|
+
light: getToken('light', '#f9fafb'),
|
|
63
|
+
dark: getToken('dark', '#111827'),
|
|
64
|
+
},
|
|
65
|
+
spacing: {
|
|
66
|
+
1: getToken('spacing-1', '0.25rem'),
|
|
67
|
+
2: getToken('spacing-2', '0.5rem'),
|
|
68
|
+
3: getToken('spacing-3', '0.75rem'),
|
|
69
|
+
4: getToken('spacing-4', '1rem'),
|
|
70
|
+
5: getToken('spacing-5', '1.25rem'),
|
|
71
|
+
6: getToken('spacing-6', '1.5rem'),
|
|
72
|
+
8: getToken('spacing-8', '2rem'),
|
|
73
|
+
10: getToken('spacing-10', '2.5rem'),
|
|
74
|
+
12: getToken('spacing-12', '3rem'),
|
|
75
|
+
16: getToken('spacing-16', '4rem'),
|
|
76
|
+
20: getToken('spacing-20', '5rem'),
|
|
77
|
+
},
|
|
78
|
+
borderRadius: {
|
|
79
|
+
sm: getToken('border-radius-sm', '0.25rem'),
|
|
80
|
+
md: getToken('border-radius-md', '0.5rem'),
|
|
81
|
+
lg: getToken('border-radius-lg', '0.75rem'),
|
|
82
|
+
xl: getToken('border-radius-xl', '1rem'),
|
|
83
|
+
full: getToken('border-radius-full', '9999px'),
|
|
84
|
+
},
|
|
85
|
+
typography: {
|
|
86
|
+
fontFamily: {
|
|
87
|
+
sans: getToken('font-sans-serif', 'Inter, system-ui, sans-serif'),
|
|
88
|
+
serif: getToken('font-serif', 'Georgia, serif'),
|
|
89
|
+
mono: getToken('font-monospace', 'Fira Code, monospace'),
|
|
90
|
+
},
|
|
91
|
+
fontSize: {
|
|
92
|
+
xs: getToken('font-size-xs', '0.75rem'),
|
|
93
|
+
sm: getToken('font-size-sm', '0.875rem'),
|
|
94
|
+
md: getToken('font-size-md', '1rem'),
|
|
95
|
+
lg: getToken('font-size-lg', '1.125rem'),
|
|
96
|
+
xl: getToken('font-size-xl', '1.25rem'),
|
|
97
|
+
'2xl': getToken('font-size-2xl', '1.5rem'),
|
|
98
|
+
'3xl': getToken('font-size-3xl', '1.875rem'),
|
|
99
|
+
'4xl': getToken('font-size-4xl', '2.25rem'),
|
|
100
|
+
},
|
|
101
|
+
fontWeight: {
|
|
102
|
+
light: getToken('font-weight-light', '300'),
|
|
103
|
+
normal: getToken('font-weight-normal', '400'),
|
|
104
|
+
medium: getToken('font-weight-medium', '500'),
|
|
105
|
+
semibold: getToken('font-weight-semibold', '600'),
|
|
106
|
+
bold: getToken('font-weight-bold', '700'),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
shadows: {
|
|
110
|
+
sm: getToken('box-shadow-sm', '0 1px 2px 0 rgba(0, 0, 0, 0.05)'),
|
|
111
|
+
md: getToken('box-shadow-md', '0 4px 6px -1px rgba(0, 0, 0, 0.1)'),
|
|
112
|
+
lg: getToken('box-shadow-lg', '0 10px 15px -3px rgba(0, 0, 0, 0.1)'),
|
|
113
|
+
xl: getToken('box-shadow-xl', '0 20px 25px -5px rgba(0, 0, 0, 0.1)'),
|
|
114
|
+
inset: getToken('box-shadow-inset', 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)'),
|
|
115
|
+
},
|
|
116
|
+
transitions: {
|
|
117
|
+
fast: getToken('transition-fast', '150ms'),
|
|
118
|
+
base: getToken('transition-base', '200ms'),
|
|
119
|
+
slow: getToken('transition-slow', '300ms'),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -270,7 +270,8 @@ export function createTestThemeObject(): Theme {
|
|
|
270
270
|
*/
|
|
271
271
|
export function generateTestThemeCSSFromObject(): string {
|
|
272
272
|
const theme = createTestThemeObject();
|
|
273
|
-
|
|
273
|
+
const tokens = themeToDesignTokens(theme);
|
|
274
|
+
return createTheme(tokens);
|
|
274
275
|
}
|
|
275
276
|
|
|
276
277
|
// ============================================================================
|
package/src/lib/theme/types.ts
CHANGED
|
@@ -50,8 +50,8 @@ export interface ThemeChangeEvent {
|
|
|
50
50
|
previousTheme: string | null;
|
|
51
51
|
/** New theme name */
|
|
52
52
|
currentTheme: string;
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/** DesignTokens if using tokens-based theme */
|
|
54
|
+
tokens?: import('./tokens').DesignTokens | null;
|
|
55
55
|
/** Timestamp of the change */
|
|
56
56
|
timestamp: number;
|
|
57
57
|
/** Whether the change was from user action or system */
|
|
@@ -126,7 +126,7 @@ export interface UseThemeOptions {
|
|
|
126
126
|
/** Custom storage key */
|
|
127
127
|
storageKey?: string;
|
|
128
128
|
/** Callback when theme changes */
|
|
129
|
-
onChange?: (theme: string |
|
|
129
|
+
onChange?: (theme: string | import('./tokens').DesignTokens) => void;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
@@ -135,10 +135,10 @@ export interface UseThemeOptions {
|
|
|
135
135
|
export interface UseThemeReturn {
|
|
136
136
|
/** Current theme name */
|
|
137
137
|
theme: string;
|
|
138
|
-
/** Current active
|
|
139
|
-
|
|
140
|
-
/** Function to change theme (supports string
|
|
141
|
-
setTheme: (theme: string |
|
|
138
|
+
/** Current active DesignTokens (if using tokens-based theme) */
|
|
139
|
+
activeTokens: import('./tokens').DesignTokens | null;
|
|
140
|
+
/** Function to change theme (supports string or DesignTokens) */
|
|
141
|
+
setTheme: (theme: string | import('./tokens').DesignTokens | Partial<import('./tokens').DesignTokens>, options?: ThemeLoadOptions) => Promise<void>;
|
|
142
142
|
/** Available themes */
|
|
143
143
|
availableThemes: ThemeMetadata[];
|
|
144
144
|
/** Whether a theme is currently loading */
|
|
@@ -213,8 +213,8 @@ export interface ThemeComponentOverrides {
|
|
|
213
213
|
export interface ThemeProviderProps {
|
|
214
214
|
/** Child components */
|
|
215
215
|
children: React.ReactNode;
|
|
216
|
-
/** Default theme */
|
|
217
|
-
defaultTheme?: string |
|
|
216
|
+
/** Default theme (string name or DesignTokens) */
|
|
217
|
+
defaultTheme?: string | import('./tokens').DesignTokens | Partial<import('./tokens').DesignTokens>;
|
|
218
218
|
/** Available themes */
|
|
219
219
|
themes?: Record<string, ThemeMetadata>;
|
|
220
220
|
/** Base path for theme CSS */
|
|
@@ -234,7 +234,7 @@ export interface ThemeProviderProps {
|
|
|
234
234
|
/** Use minified CSS */
|
|
235
235
|
useMinified?: boolean;
|
|
236
236
|
/** Callback when theme changes */
|
|
237
|
-
onThemeChange?: (theme: string |
|
|
237
|
+
onThemeChange?: (theme: string | import('./tokens').DesignTokens) => void;
|
|
238
238
|
/** Callback on error */
|
|
239
239
|
onError?: (error: Error, themeName: string) => void;
|
|
240
240
|
}
|
|
@@ -245,10 +245,10 @@ export interface ThemeProviderProps {
|
|
|
245
245
|
export interface ThemeContextValue {
|
|
246
246
|
/** Current theme name */
|
|
247
247
|
theme: string;
|
|
248
|
-
/** Current active
|
|
249
|
-
|
|
250
|
-
/** Set theme function (supports string
|
|
251
|
-
setTheme: (theme: string |
|
|
248
|
+
/** Current active DesignTokens (if using tokens-based theme) */
|
|
249
|
+
activeTokens: import('./tokens').DesignTokens | null;
|
|
250
|
+
/** Set theme function (supports string or DesignTokens) */
|
|
251
|
+
setTheme: (theme: string | import('./tokens').DesignTokens | Partial<import('./tokens').DesignTokens>, options?: ThemeLoadOptions) => Promise<void>;
|
|
252
252
|
/** Available themes */
|
|
253
253
|
availableThemes: ThemeMetadata[];
|
|
254
254
|
/** Loading state */
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Theming Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent patterns for applying theme values to components
|
|
5
|
+
* using DesignTokens and CSS variables
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DesignTokens } from '../tokens/tokens';
|
|
9
|
+
|
|
10
|
+
export interface ComponentThemeOptions {
|
|
11
|
+
component: string;
|
|
12
|
+
variant?: string;
|
|
13
|
+
size?: string;
|
|
14
|
+
tokens?: Partial<DesignTokens>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get a theme value for a specific component using CSS variables
|
|
19
|
+
* This ensures all components access theme values consistently
|
|
20
|
+
*/
|
|
21
|
+
export function getComponentThemeValue(
|
|
22
|
+
component: string,
|
|
23
|
+
property: string,
|
|
24
|
+
variant?: string,
|
|
25
|
+
size?: string
|
|
26
|
+
): string {
|
|
27
|
+
// Build CSS variable name following consistent pattern
|
|
28
|
+
const parts = ['atomix', component];
|
|
29
|
+
|
|
30
|
+
if (variant) {
|
|
31
|
+
parts.push(variant);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (size) {
|
|
35
|
+
parts.push(size);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
parts.push(property);
|
|
39
|
+
|
|
40
|
+
const cssVarName = `--${parts.join('-')}`;
|
|
41
|
+
const fallbackVarName = `--atomix-${property}`;
|
|
42
|
+
|
|
43
|
+
// Return CSS variable reference with fallback
|
|
44
|
+
return `var(${cssVarName}, var(${fallbackVarName}, initial))`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate component-specific CSS variables from DesignTokens
|
|
49
|
+
*/
|
|
50
|
+
export function generateComponentCSSVars(
|
|
51
|
+
component: string,
|
|
52
|
+
tokens?: Partial<DesignTokens>,
|
|
53
|
+
variant?: string,
|
|
54
|
+
size?: string
|
|
55
|
+
): Record<string, string> {
|
|
56
|
+
const vars: Record<string, string> = {};
|
|
57
|
+
|
|
58
|
+
if (!tokens) return vars;
|
|
59
|
+
|
|
60
|
+
const prefixParts = ['atomix', component];
|
|
61
|
+
|
|
62
|
+
if (variant) {
|
|
63
|
+
prefixParts.push(variant);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (size) {
|
|
67
|
+
prefixParts.push(size);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const prefix = prefixParts.join('-');
|
|
71
|
+
|
|
72
|
+
// Map common DesignTokens to component-specific CSS variables
|
|
73
|
+
if (tokens.primary) {
|
|
74
|
+
vars[`--${prefix}-color`] = tokens.primary;
|
|
75
|
+
}
|
|
76
|
+
if (tokens['primary-9']) {
|
|
77
|
+
vars[`--${prefix}-color-hover`] = tokens['primary-9'];
|
|
78
|
+
}
|
|
79
|
+
if (tokens['body-color']) {
|
|
80
|
+
vars[`--${prefix}-color-disabled`] = tokens['body-color'];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (tokens['body-font-family']) {
|
|
84
|
+
vars[`--${prefix}-font-family`] = tokens['body-font-family'];
|
|
85
|
+
}
|
|
86
|
+
if (tokens['body-font-size']) {
|
|
87
|
+
vars[`--${prefix}-font-size`] = tokens['body-font-size'];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (tokens['spacing-1']) {
|
|
91
|
+
vars[`--${prefix}-spacing-sm`] = tokens['spacing-1'];
|
|
92
|
+
}
|
|
93
|
+
if (tokens['spacing-2']) {
|
|
94
|
+
vars[`--${prefix}-spacing-md`] = tokens['spacing-2'];
|
|
95
|
+
}
|
|
96
|
+
if (tokens['spacing-4']) {
|
|
97
|
+
vars[`--${prefix}-spacing-lg`] = tokens['spacing-4'];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return vars;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Apply consistent theme to component style object using DesignTokens
|
|
105
|
+
*/
|
|
106
|
+
export function applyComponentTheme(
|
|
107
|
+
component: string,
|
|
108
|
+
style: React.CSSProperties = {},
|
|
109
|
+
variant?: string,
|
|
110
|
+
size?: string,
|
|
111
|
+
tokens?: Partial<DesignTokens>
|
|
112
|
+
): React.CSSProperties {
|
|
113
|
+
// If no tokens provided, return original style
|
|
114
|
+
if (!tokens) {
|
|
115
|
+
return style;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Generate component-specific CSS variables
|
|
119
|
+
const componentVars = generateComponentCSSVars(component, tokens, variant, size);
|
|
120
|
+
|
|
121
|
+
// Merge with existing style
|
|
122
|
+
return {
|
|
123
|
+
...componentVars,
|
|
124
|
+
...style,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create a hook for consistent component theming
|
|
130
|
+
*/
|
|
131
|
+
export function useComponentTheme(
|
|
132
|
+
component: string,
|
|
133
|
+
variant?: string,
|
|
134
|
+
size?: string,
|
|
135
|
+
tokens?: Partial<DesignTokens>
|
|
136
|
+
): (property: string) => string {
|
|
137
|
+
return (property: string) => {
|
|
138
|
+
return getComponentThemeValue(component, property, variant, size);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ThemeMetadata, ThemeValidationResult } from '../types';
|
|
9
9
|
import { THEME_LINK_ID_PREFIX } from '../constants/constants';
|
|
10
|
+
import { ThemeError, ThemeErrorCode } from '../errors/errors';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Check if code is running in a browser environment
|
|
@@ -29,6 +30,20 @@ export const getThemeLinkId = (themeName: string): string => {
|
|
|
29
30
|
return `${THEME_LINK_ID_PREFIX}${themeName}`;
|
|
30
31
|
};
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Sanitize path to prevent path injection attacks
|
|
35
|
+
*
|
|
36
|
+
* @param path - Path to sanitize
|
|
37
|
+
* @returns Sanitized path
|
|
38
|
+
*/
|
|
39
|
+
const sanitizePath = (path: string): string => {
|
|
40
|
+
return path
|
|
41
|
+
.replace(/[<>"']/g, '') // Remove dangerous characters
|
|
42
|
+
.replace(/\.\./g, '') // Remove path traversal attempts
|
|
43
|
+
.replace(/\/+/g, '/') // Normalize multiple slashes
|
|
44
|
+
.replace(/^\/+|\/+$/g, ''); // Trim leading/trailing slashes
|
|
45
|
+
};
|
|
46
|
+
|
|
32
47
|
/**
|
|
33
48
|
* Build the CSS file path for a theme
|
|
34
49
|
*
|
|
@@ -37,6 +52,7 @@ export const getThemeLinkId = (themeName: string): string => {
|
|
|
37
52
|
* @param useMinified - Whether to use minified CSS
|
|
38
53
|
* @param cdnPath - Optional CDN path
|
|
39
54
|
* @returns Full path to the theme CSS file
|
|
55
|
+
* @throws Error if theme name is invalid
|
|
40
56
|
*/
|
|
41
57
|
export const buildThemePath = (
|
|
42
58
|
themeName: string,
|
|
@@ -46,21 +62,24 @@ export const buildThemePath = (
|
|
|
46
62
|
): string => {
|
|
47
63
|
// Validate theme name to prevent path injection
|
|
48
64
|
if (!isValidThemeName(themeName)) {
|
|
49
|
-
throw new
|
|
65
|
+
throw new ThemeError(
|
|
66
|
+
`Invalid theme name: "${themeName}". Theme names must be lowercase alphanumeric with hyphens (e.g., "my-theme").`,
|
|
67
|
+
ThemeErrorCode.INVALID_THEME_NAME,
|
|
68
|
+
{ themeName, pattern: /^[a-z0-9]+(-[a-z0-9]+)*$/ }
|
|
69
|
+
);
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
const extension = useMinified ? '.min.css' : '.css';
|
|
53
73
|
const fileName = `${themeName}${extension}`;
|
|
54
74
|
|
|
55
75
|
if (cdnPath) {
|
|
56
|
-
//
|
|
57
|
-
const cleanCdnPath = cdnPath
|
|
76
|
+
// Sanitize CDN path to prevent path injection
|
|
77
|
+
const cleanCdnPath = sanitizePath(cdnPath);
|
|
58
78
|
return `${cleanCdnPath}/${fileName}`;
|
|
59
79
|
}
|
|
60
80
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
const cleanBasePath = basePath.replace(/\/$/, '').replace(/[<>"']/g, '');
|
|
81
|
+
// Sanitize basePath to prevent path injection
|
|
82
|
+
const cleanBasePath = sanitizePath(basePath);
|
|
64
83
|
const cleanFileName = fileName.replace(/^\//, '');
|
|
65
84
|
|
|
66
85
|
return `${cleanBasePath}/${cleanFileName}`;
|
|
@@ -110,7 +129,11 @@ export const loadThemeCSS = (
|
|
|
110
129
|
link.onerror = () => {
|
|
111
130
|
// Remove failed link element
|
|
112
131
|
link.remove();
|
|
113
|
-
reject(new
|
|
132
|
+
reject(new ThemeError(
|
|
133
|
+
`Failed to load theme CSS from: ${fullPath}. Please check that the file exists and is accessible.`,
|
|
134
|
+
ThemeErrorCode.THEME_LOAD_FAILED,
|
|
135
|
+
{ fullPath, linkId }
|
|
136
|
+
));
|
|
114
137
|
};
|
|
115
138
|
|
|
116
139
|
// Append to head
|
|
@@ -166,8 +189,10 @@ export const applyThemeAttributes = (
|
|
|
166
189
|
return;
|
|
167
190
|
}
|
|
168
191
|
|
|
169
|
-
// Set data attribute on body
|
|
170
|
-
document.body
|
|
192
|
+
// Set data attribute on body (with null check)
|
|
193
|
+
if (document.body) {
|
|
194
|
+
document.body.setAttribute(dataAttribute, themeName);
|
|
195
|
+
}
|
|
171
196
|
|
|
172
197
|
// Also set on documentElement for broader compatibility
|
|
173
198
|
document.documentElement.setAttribute(dataAttribute, themeName);
|
|
@@ -185,7 +210,12 @@ export const removeThemeAttributes = (
|
|
|
185
210
|
return;
|
|
186
211
|
}
|
|
187
212
|
|
|
188
|
-
|
|
213
|
+
// Remove from body (with null check)
|
|
214
|
+
if (document.body) {
|
|
215
|
+
document.body.removeAttribute(dataAttribute);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Remove from documentElement
|
|
189
219
|
document.documentElement.removeAttribute(dataAttribute);
|
|
190
220
|
};
|
|
191
221
|
|
|
@@ -202,8 +232,10 @@ export const getCurrentThemeFromDOM = (
|
|
|
202
232
|
return null;
|
|
203
233
|
}
|
|
204
234
|
|
|
205
|
-
|
|
206
|
-
|
|
235
|
+
// Add null checks for SSR safety
|
|
236
|
+
const bodyTheme = document.body?.getAttribute(dataAttribute);
|
|
237
|
+
const htmlTheme = document.documentElement?.getAttribute(dataAttribute);
|
|
238
|
+
return bodyTheme || htmlTheme || null;
|
|
207
239
|
};
|
|
208
240
|
|
|
209
241
|
/**
|
|
@@ -377,15 +409,15 @@ export const createLocalStorageAdapter = () => {
|
|
|
377
409
|
*
|
|
378
410
|
* @param func - Function to debounce
|
|
379
411
|
* @param wait - Wait time in milliseconds
|
|
380
|
-
* @returns Debounced function
|
|
412
|
+
* @returns Debounced function with cancel method
|
|
381
413
|
*/
|
|
382
414
|
export const debounce = <T extends (...args: any[]) => any>(
|
|
383
415
|
func: T,
|
|
384
416
|
wait: number
|
|
385
|
-
): ((...args: Parameters<T>) => void) => {
|
|
417
|
+
): ((...args: Parameters<T>) => void) & { cancel: () => void } => {
|
|
386
418
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
387
419
|
|
|
388
|
-
|
|
420
|
+
const debounced = function executedFunction(...args: Parameters<T>) {
|
|
389
421
|
const later = () => {
|
|
390
422
|
timeout = null;
|
|
391
423
|
func(...args);
|
|
@@ -396,4 +428,14 @@ export const debounce = <T extends (...args: any[]) => any>(
|
|
|
396
428
|
}
|
|
397
429
|
timeout = setTimeout(later, wait);
|
|
398
430
|
};
|
|
431
|
+
|
|
432
|
+
// Add cancel method for cleanup
|
|
433
|
+
debounced.cancel = () => {
|
|
434
|
+
if (timeout !== null) {
|
|
435
|
+
clearTimeout(timeout);
|
|
436
|
+
timeout = null;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
return debounced;
|
|
399
441
|
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Naming Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent naming conventions across the theme system
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface NamingOptions {
|
|
8
|
+
prefix?: string;
|
|
9
|
+
component?: string;
|
|
10
|
+
variant?: string;
|
|
11
|
+
state?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate consistent CSS class names following BEM methodology
|
|
16
|
+
*/
|
|
17
|
+
export function generateClassName(block: string, element?: string, modifiers?: Record<string, boolean | string>): string {
|
|
18
|
+
let className = block;
|
|
19
|
+
|
|
20
|
+
if (element) {
|
|
21
|
+
className += `__${element}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (modifiers) {
|
|
25
|
+
Object.entries(modifiers).forEach(([key, value]) => {
|
|
26
|
+
if (value) {
|
|
27
|
+
className += `--${key}`;
|
|
28
|
+
if (typeof value === 'string' && value !== key) {
|
|
29
|
+
className += `-${value}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return className;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate consistent CSS variable names
|
|
40
|
+
*/
|
|
41
|
+
export function generateCSSVariableName(property: string, options: NamingOptions = {}): string {
|
|
42
|
+
const { prefix = 'atomix', component, variant, state } = options;
|
|
43
|
+
|
|
44
|
+
const parts = [prefix];
|
|
45
|
+
|
|
46
|
+
if (component) {
|
|
47
|
+
parts.push(component);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (variant) {
|
|
51
|
+
parts.push(variant);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (state) {
|
|
55
|
+
parts.push(state);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
parts.push(property);
|
|
59
|
+
|
|
60
|
+
return `--${parts.join('-')}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Normalize theme tokens to consistent naming convention
|
|
65
|
+
*/
|
|
66
|
+
export function normalizeThemeTokens(tokens: Record<string, any>): Record<string, any> {
|
|
67
|
+
const normalized: Record<string, any> = {};
|
|
68
|
+
|
|
69
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
70
|
+
if (typeof value === 'object' && value !== null) {
|
|
71
|
+
// Recursively normalize nested objects
|
|
72
|
+
normalized[key] = normalizeThemeTokens(value);
|
|
73
|
+
} else {
|
|
74
|
+
// Apply consistent naming transformation
|
|
75
|
+
normalized[key] = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return normalized;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Convert camelCase to kebab-case for CSS custom properties
|
|
84
|
+
*/
|
|
85
|
+
export function camelToKebab(str: string): string {
|
|
86
|
+
return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Convert theme property to CSS variable name
|
|
91
|
+
*/
|
|
92
|
+
export function themePropertyToCSSVar(propertyPath: string, prefix: string = 'atomix'): string {
|
|
93
|
+
// Convert nested property paths to kebab-case
|
|
94
|
+
const path = propertyPath
|
|
95
|
+
.split('.')
|
|
96
|
+
.map(part => camelToKebab(part))
|
|
97
|
+
.join('-');
|
|
98
|
+
|
|
99
|
+
return `--${prefix}-${path}`;
|
|
100
|
+
}
|
|
@@ -1,34 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Theme Helper Functions
|
|
3
3
|
*
|
|
4
|
-
* Utility functions for working with
|
|
4
|
+
* Utility functions for working with DesignTokens
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Theme } from '../types';
|
|
8
7
|
import type { DesignTokens } from '../tokens/tokens';
|
|
9
|
-
import { createDesignTokensFromTheme } from '../adapters/themeAdapter';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Get DesignTokens from current theme
|
|
13
|
-
*
|
|
14
|
-
* Converts a Theme object to DesignTokens. Useful when you need to
|
|
15
|
-
* work with DesignTokens but have a Theme object.
|
|
16
|
-
*
|
|
17
|
-
* @param theme - Theme object to convert
|
|
18
|
-
* @returns DesignTokens object
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```typescript
|
|
22
|
-
* // If you have a Theme object, convert it to DesignTokens
|
|
23
|
-
* const tokens = getDesignTokensFromTheme(theme);
|
|
24
|
-
* // Now you can use tokens with unified theme system
|
|
25
|
-
* const css = createTheme(tokens);
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
export function getDesignTokensFromTheme(theme: Theme | null): DesignTokens | null {
|
|
29
|
-
if (!theme) return null;
|
|
30
|
-
return createDesignTokensFromTheme(theme);
|
|
31
|
-
}
|
|
32
8
|
|
|
33
9
|
/**
|
|
34
10
|
* Check if a value is DesignTokens
|
|
@@ -62,17 +38,3 @@ export function isDesignTokens(value: unknown): value is DesignTokens {
|
|
|
62
38
|
return hasDesignTokenKeys;
|
|
63
39
|
}
|
|
64
40
|
|
|
65
|
-
/**
|
|
66
|
-
* Check if a value is a Theme object
|
|
67
|
-
*
|
|
68
|
-
* Type guard to check if an object is a Theme.
|
|
69
|
-
*
|
|
70
|
-
* @param value - Value to check
|
|
71
|
-
* @returns True if value is Theme
|
|
72
|
-
*/
|
|
73
|
-
export function isThemeObject(value: unknown): value is Theme {
|
|
74
|
-
if (!value || typeof value !== 'object') return false;
|
|
75
|
-
const obj = value as Record<string, unknown>;
|
|
76
|
-
return '__isJSTheme' in obj || ('palette' in obj && 'typography' in obj);
|
|
77
|
-
}
|
|
78
|
-
|