@shohojdhara/atomix 0.2.9 → 0.3.1

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 (102) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/atomix.css +309 -105
  3. package/dist/atomix.min.css +3 -5
  4. package/dist/index.d.ts +807 -51
  5. package/dist/index.esm.js +16367 -16405
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/index.js +16277 -16330
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.js +1 -1
  10. package/dist/index.min.js.map +1 -1
  11. package/dist/themes/applemix.css +309 -105
  12. package/dist/themes/applemix.min.css +5 -7
  13. package/dist/themes/boomdevs.css +202 -10
  14. package/dist/themes/boomdevs.min.css +3 -5
  15. package/dist/themes/esrar.css +309 -105
  16. package/dist/themes/esrar.min.css +4 -6
  17. package/dist/themes/flashtrade.css +310 -105
  18. package/dist/themes/flashtrade.min.css +5 -7
  19. package/dist/themes/mashroom.css +300 -96
  20. package/dist/themes/mashroom.min.css +4 -6
  21. package/dist/themes/shaj-default.css +300 -96
  22. package/dist/themes/shaj-default.min.css +4 -6
  23. package/package.json +1 -1
  24. package/src/components/AtomixGlass/AtomixGlass.test.tsx +21 -32
  25. package/src/components/AtomixGlass/AtomixGlass.tsx +55 -42
  26. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +205 -57
  27. package/src/components/AtomixGlass/GlassFilter.tsx +22 -8
  28. package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +221 -0
  29. package/src/components/AtomixGlass/atomixGLass.old.tsx +0 -3
  30. package/src/components/AtomixGlass/shader-utils.ts +8 -0
  31. package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +319 -100
  32. package/src/components/AtomixGlass/stories/Examples.stories.tsx +601 -105
  33. package/src/components/AtomixGlass/stories/Modes.stories.tsx +30 -12
  34. package/src/components/AtomixGlass/stories/Playground.stories.tsx +173 -38
  35. package/src/components/AtomixGlass/stories/ShaderVariants.stories.tsx +18 -18
  36. package/src/components/AtomixGlass/stories/shared-components.tsx +27 -5
  37. package/src/components/Breadcrumb/Breadcrumb.tsx +8 -3
  38. package/src/components/Button/Button.tsx +62 -17
  39. package/src/components/Callout/Callout.test.tsx +8 -14
  40. package/src/components/Card/Card.tsx +103 -1
  41. package/src/components/Card/index.ts +3 -2
  42. package/src/components/Footer/Footer.stories.tsx +1 -2
  43. package/src/components/Footer/Footer.tsx +0 -5
  44. package/src/components/Footer/FooterLink.tsx +3 -2
  45. package/src/components/Footer/FooterSection.tsx +0 -7
  46. package/src/components/Icon/index.ts +1 -1
  47. package/src/components/Modal/Modal.stories.tsx +29 -38
  48. package/src/components/Modal/Modal.tsx +4 -4
  49. package/src/components/Navigation/Nav/NavItem.tsx +8 -3
  50. package/src/components/Navigation/SideMenu/SideMenu.tsx +49 -41
  51. package/src/components/Navigation/SideMenu/SideMenuItem.tsx +63 -19
  52. package/src/components/Popover/Popover.tsx +1 -1
  53. package/src/components/VideoPlayer/VideoPlayer.stories.tsx +977 -400
  54. package/src/components/VideoPlayer/VideoPlayer.tsx +1 -6
  55. package/src/lib/composables/shared-mouse-tracker.ts +133 -0
  56. package/src/lib/composables/useAtomixGlass.ts +303 -115
  57. package/src/lib/theme/ThemeManager.integration.test.ts +124 -0
  58. package/src/lib/theme/ThemeManager.stories.tsx +13 -13
  59. package/src/lib/theme/ThemeManager.test.ts +4 -0
  60. package/src/lib/theme/ThemeManager.ts +203 -59
  61. package/src/lib/theme/ThemeProvider.tsx +183 -33
  62. package/src/lib/theme/composeTheme.ts +375 -0
  63. package/src/lib/theme/createTheme.test.ts +475 -0
  64. package/src/lib/theme/createTheme.ts +510 -0
  65. package/src/lib/theme/generateCSSVariables.ts +713 -0
  66. package/src/lib/theme/index.ts +67 -0
  67. package/src/lib/theme/themeUtils.ts +333 -0
  68. package/src/lib/theme/types.ts +337 -8
  69. package/src/lib/theme/useTheme.test.tsx +2 -1
  70. package/src/lib/theme/useTheme.ts +6 -22
  71. package/src/lib/types/components.ts +152 -57
  72. package/src/styles/01-settings/_index.scss +2 -2
  73. package/src/styles/01-settings/_settings.badge.scss +2 -2
  74. package/src/styles/01-settings/_settings.border-radius.scss +1 -1
  75. package/src/styles/01-settings/{_settings.maps.scss → _settings.design-tokens.scss} +163 -49
  76. package/src/styles/01-settings/_settings.modal.scss +1 -1
  77. package/src/styles/01-settings/_settings.spacing.scss +14 -13
  78. package/src/styles/03-generic/_generic.root.scss +131 -50
  79. package/src/styles/05-objects/_objects.block.scss +1 -1
  80. package/src/styles/06-components/_components.atomix-glass.scss +20 -22
  81. package/src/styles/06-components/_components.badge.scss +2 -2
  82. package/src/styles/06-components/_components.button.scss +1 -1
  83. package/src/styles/06-components/_components.callout.scss +1 -1
  84. package/src/styles/06-components/_components.card.scss +74 -2
  85. package/src/styles/06-components/_components.chart.scss +1 -1
  86. package/src/styles/06-components/_components.dropdown.scss +6 -0
  87. package/src/styles/06-components/_components.footer.scss +1 -1
  88. package/src/styles/06-components/_components.list-group.scss +1 -1
  89. package/src/styles/06-components/_components.list.scss +1 -1
  90. package/src/styles/06-components/_components.menu.scss +1 -1
  91. package/src/styles/06-components/_components.messages.scss +1 -1
  92. package/src/styles/06-components/_components.modal.scss +7 -2
  93. package/src/styles/06-components/_components.navbar.scss +1 -1
  94. package/src/styles/06-components/_components.popover.scss +10 -0
  95. package/src/styles/06-components/_components.product-review.scss +1 -1
  96. package/src/styles/06-components/_components.progress.scss +1 -1
  97. package/src/styles/06-components/_components.rating.scss +1 -1
  98. package/src/styles/06-components/_components.spinner.scss +1 -1
  99. package/src/styles/99-utilities/_utilities.background.scss +1 -1
  100. package/src/styles/99-utilities/_utilities.border.scss +1 -1
  101. package/src/styles/99-utilities/_utilities.link.scss +1 -1
  102. package/src/styles/99-utilities/_utilities.text.scss +1 -1
@@ -0,0 +1,713 @@
1
+ /**
2
+ * CSS Variable Generator
3
+ *
4
+ * Generates CSS custom properties from theme objects and injects them into the DOM.
5
+ * Follows the existing --atomix- prefix convention.
6
+ */
7
+
8
+ import type { Theme } from './types';
9
+ import { isBrowser } from './utils';
10
+ import { hexToRgb, alpha, lighten, darken, emphasize } from './themeUtils';
11
+
12
+ // ============================================================================
13
+ // CSS Variable Generation
14
+ // ============================================================================
15
+
16
+ /**
17
+ * Options for CSS variable generation
18
+ */
19
+ export interface GenerateCSSVariablesOptions {
20
+ /** CSS selector for the variables (default: ':root') */
21
+ selector?: string;
22
+ /** Whether to inject the CSS into the DOM */
23
+ inject?: boolean;
24
+ /** ID for the injected style element */
25
+ styleId?: string;
26
+ /** Prefix for CSS variables (default: 'atomix') */
27
+ prefix?: string;
28
+ }
29
+
30
+ /**
31
+ * Convert a nested object to flat CSS variable declarations
32
+ */
33
+ function flattenObject(
34
+ obj: Record<string, any>,
35
+ prefix: string = '',
36
+ result: Record<string, string> = {}
37
+ ): Record<string, string> {
38
+ for (const key in obj) {
39
+ if (!obj.hasOwnProperty(key)) continue;
40
+
41
+ const value = obj[key];
42
+ const newKey = prefix ? `${prefix}-${key}` : key;
43
+
44
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
45
+ // Skip special objects like functions
46
+ if (typeof value === 'function') continue;
47
+
48
+ // Recursively flatten nested objects
49
+ flattenObject(value, newKey, result);
50
+ } else if (typeof value === 'string' || typeof value === 'number') {
51
+ // Convert camelCase to kebab-case
52
+ const kebabKey = newKey.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
53
+ result[kebabKey] = String(value);
54
+ }
55
+ }
56
+
57
+ return result;
58
+ }
59
+
60
+ /**
61
+ * Generate a color scale from a base color (1-10 steps)
62
+ * Creates lighter to darker variations
63
+ */
64
+ function generateColorScale(baseColor: string, prefix: string, colorName: string): Record<string, string> {
65
+ const vars: Record<string, string> = {};
66
+ const rgb = hexToRgb(baseColor);
67
+ if (!rgb) return vars;
68
+
69
+ // Generate 10-step scale
70
+ // Steps 1-5: lighter variations
71
+ // Step 6: base color
72
+ // Steps 7-10: darker variations
73
+ for (let i = 1; i <= 10; i++) {
74
+ let color: string;
75
+ if (i < 6) {
76
+ // Lighter: mix with white
77
+ const mixRatio = (6 - i) / 5;
78
+ color = lighten(baseColor, mixRatio * 0.8);
79
+ } else if (i === 6) {
80
+ // Base color
81
+ color = baseColor;
82
+ } else {
83
+ // Darker: mix with black
84
+ const mixRatio = (i - 6) / 4;
85
+ color = darken(baseColor, mixRatio * 0.6);
86
+ }
87
+ vars[`${prefix}-${colorName}-${i}`] = color;
88
+ }
89
+
90
+ return vars;
91
+ }
92
+
93
+ /**
94
+ * Generate CSS variables from theme palette
95
+ */
96
+ function generatePaletteVariables(palette: Theme['palette'], prefix: string): Record<string, string> {
97
+ const vars: Record<string, string> = {};
98
+
99
+ // Primary, secondary, error, warning, info, success
100
+ const colorKeys = ['primary', 'secondary', 'error', 'warning', 'info', 'success'] as const;
101
+ colorKeys.forEach((key) => {
102
+ const color = palette[key];
103
+ if (color && typeof color === 'object') {
104
+ // Main color
105
+ vars[`${prefix}-${key}`] = color.main;
106
+
107
+ // Generate RGB for transparency support
108
+ const rgb = hexToRgb(color.main);
109
+ if (rgb) {
110
+ vars[`${prefix}-${key}-rgb`] = `${rgb.r}, ${rgb.g}, ${rgb.b}`;
111
+ }
112
+
113
+ // Map dark variant to hover (closest SCSS equivalent)
114
+ if (color.dark) {
115
+ vars[`${prefix}-${key}-hover`] = color.dark;
116
+ }
117
+
118
+ // Generate semantic color variants
119
+ // Text emphasis: emphasized version of the color for text
120
+ vars[`${prefix}-${key}-text-emphasis`] = emphasize(color.main, 0.15);
121
+
122
+ // Background subtle: very light version for backgrounds
123
+ vars[`${prefix}-${key}-bg-subtle`] = alpha(color.main, 0.1);
124
+
125
+ // Border subtle: light version for borders
126
+ vars[`${prefix}-${key}-border-subtle`] = alpha(color.main, 0.2);
127
+
128
+ // Generate full color scale (1-10)
129
+ const colorScale = generateColorScale(color.main, prefix, key);
130
+ Object.assign(vars, colorScale);
131
+ }
132
+ });
133
+
134
+ // Generate gray scale from text colors
135
+ // Use text.primary as base for gray scale
136
+ if (palette.text?.primary) {
137
+ const grayScale = generateColorScale(palette.text.primary, prefix, 'gray');
138
+ Object.assign(vars, grayScale);
139
+ }
140
+
141
+ // Generate red, green, blue, yellow scales if available
142
+ // These are typically used for semantic colors but can be extended
143
+ if (palette.error && typeof palette.error === 'object' && palette.error.main) {
144
+ const redScale = generateColorScale(palette.error.main, prefix, 'red');
145
+ Object.assign(vars, redScale);
146
+ }
147
+ if (palette.success && typeof palette.success === 'object' && palette.success.main) {
148
+ const greenScale = generateColorScale(palette.success.main, prefix, 'green');
149
+ Object.assign(vars, greenScale);
150
+ }
151
+ if (palette.info && typeof palette.info === 'object' && palette.info.main) {
152
+ const blueScale = generateColorScale(palette.info.main, prefix, 'blue');
153
+ Object.assign(vars, blueScale);
154
+ }
155
+ if (palette.warning && typeof palette.warning === 'object' && palette.warning.main) {
156
+ const yellowScale = generateColorScale(palette.warning.main, prefix, 'yellow');
157
+ Object.assign(vars, yellowScale);
158
+ }
159
+
160
+ // Background mappings to SCSS body variables
161
+ if (palette.background) {
162
+ vars[`${prefix}-body-bg`] = palette.background.default;
163
+ }
164
+
165
+ // Text mappings to SCSS body variables
166
+ if (palette.text) {
167
+ vars[`${prefix}-body-color`] = palette.text.primary;
168
+ }
169
+
170
+ // Heading color (defaults to text primary)
171
+ if (palette.text) {
172
+ vars[`${prefix}-heading-color`] = palette.text.primary;
173
+ }
174
+
175
+ // Link colors (defaults to primary color)
176
+ if (palette.primary) {
177
+ vars[`${prefix}-link-color`] = palette.primary.main;
178
+ const linkRgb = hexToRgb(palette.primary.main);
179
+ if (linkRgb) {
180
+ vars[`${prefix}-link-color-rgb`] = `${linkRgb.r}, ${linkRgb.g}, ${linkRgb.b}`;
181
+ }
182
+ // Link hover color (slightly darker)
183
+ vars[`${prefix}-link-hover-color`] = palette.primary.dark || darken(palette.primary.main, 0.1);
184
+ const linkHoverRgb = hexToRgb(palette.primary.dark || darken(palette.primary.main, 0.1));
185
+ if (linkHoverRgb) {
186
+ vars[`${prefix}-link-hover-color-rgb`] = `${linkHoverRgb.r}, ${linkHoverRgb.g}, ${linkHoverRgb.b}`;
187
+ }
188
+ // Link decoration (default: underline)
189
+ vars[`${prefix}-link-decoration`] = 'underline';
190
+ vars[`${prefix}-link-hover-decoration`] = 'none';
191
+ }
192
+
193
+ // Border color (defaults to subtle gray)
194
+ if (palette.text) {
195
+ vars[`${prefix}-border-color`] = alpha(palette.text.primary, 0.1);
196
+ vars[`${prefix}-border-color-translucent`] = alpha(palette.text.primary, 0.15);
197
+ }
198
+
199
+ // Focus border color (defaults to primary)
200
+ if (palette.primary) {
201
+ vars[`${prefix}-focus-border-color`] = palette.primary.main;
202
+ }
203
+
204
+ // Form validation colors
205
+ if (palette.success) {
206
+ vars[`${prefix}-form-valid-color`] = palette.success.main;
207
+ vars[`${prefix}-form-valid-border-color`] = alpha(palette.success.main, 0.3);
208
+ }
209
+ if (palette.error) {
210
+ vars[`${prefix}-form-invalid-color`] = palette.error.main;
211
+ vars[`${prefix}-form-invalid-border-color`] = alpha(palette.error.main, 0.3);
212
+ }
213
+
214
+ // Code/highlight colors
215
+ // Highlight background (defaults to subtle yellow)
216
+ if (palette.warning) {
217
+ vars[`${prefix}-highlight-bg`] = alpha(palette.warning.main, 0.2);
218
+ } else {
219
+ vars[`${prefix}-highlight-bg`] = 'rgba(255, 235, 59, 0.2)';
220
+ }
221
+
222
+ // Code color (defaults to text secondary)
223
+ if (palette.text) {
224
+ vars[`${prefix}-code-color`] = palette.text.secondary;
225
+ }
226
+
227
+ return vars;
228
+ }
229
+
230
+ /**
231
+ * Generate CSS variables from theme typography
232
+ */
233
+ function generateTypographyVariables(
234
+ typography: Theme['typography'],
235
+ prefix: string
236
+ ): Record<string, string> {
237
+ const vars: Record<string, string> = {};
238
+
239
+ // Font family (SCSS: --atomix-body-font-family)
240
+ vars[`${prefix}-body-font-family`] = typography.fontFamily;
241
+ // Additional font family tokens
242
+ vars[`${prefix}-font-sans-serif`] = typography.fontFamily;
243
+ vars[`${prefix}-font-monospace`] = 'SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
244
+
245
+ // Root font size (SCSS: --atomix-root-font-size)
246
+ // Typically 16px, but can be customized
247
+ const rootFontSize = typography.fontSize || 16;
248
+ vars[`${prefix}-root-font-size`] = `${rootFontSize}px`;
249
+
250
+ // Base font size (SCSS: --atomix-body-font-size)
251
+ const baseFontSize = typography.fontSize;
252
+ vars[`${prefix}-body-font-size`] = `${baseFontSize}px`;
253
+
254
+ // Base font weight (SCSS: --atomix-body-font-weight)
255
+ vars[`${prefix}-body-font-weight`] = String(typography.fontWeightRegular);
256
+
257
+ // Font weight scale
258
+ vars[`${prefix}-font-weight-light`] = String(typography.fontWeightLight ?? 300);
259
+ vars[`${prefix}-font-weight-normal`] = String(typography.fontWeightRegular ?? 400);
260
+ vars[`${prefix}-font-weight-medium`] = String(typography.fontWeightMedium ?? 500);
261
+ vars[`${prefix}-font-weight-semibold`] = String(typography.fontWeightSemiBold ?? 600);
262
+ vars[`${prefix}-font-weight-bold`] = String(typography.fontWeightBold ?? 700);
263
+ // Optional font weights (may not be in theme, but exist in design tokens)
264
+ if ('fontWeightHeavy' in typography) {
265
+ vars[`${prefix}-font-weight-heavy`] = String((typography as any).fontWeightHeavy || 800);
266
+ }
267
+ if ('fontWeightBlack' in typography) {
268
+ vars[`${prefix}-font-weight-black`] = String((typography as any).fontWeightBlack || 900);
269
+ }
270
+
271
+ // Base line height (SCSS: --atomix-body-line-height)
272
+ const baseLineHeight = typeof typography.body1?.lineHeight === 'number'
273
+ ? typography.body1.lineHeight
274
+ : parseFloat(String(typography.body1?.lineHeight || 1.2));
275
+ vars[`${prefix}-body-line-height`] = String(baseLineHeight);
276
+
277
+ // Line height scale (using calculated defaults based on design tokens)
278
+ vars[`${prefix}-line-height-base`] = String(baseLineHeight);
279
+ vars[`${prefix}-line-height-sm`] = String(1.43);
280
+ vars[`${prefix}-line-height-lg`] = String(1.56);
281
+
282
+ // Extended font size scale (matching design system tokens)
283
+ const fontSizeXs = baseFontSize * 0.75; // 12px if base is 16px
284
+ const fontSizeSm = baseFontSize * 0.875; // 14px if base is 16px
285
+ const fontSizeMd = baseFontSize * 1; // 16px if base is 16px (same as base)
286
+ const fontSizeLg = baseFontSize * 1.125; // 18px if base is 16px
287
+ const fontSizeXl = baseFontSize * 1.5; // 24px if base is 16px
288
+ const fontSize2xl = baseFontSize * 2; // 32px if base is 16px
289
+
290
+ vars[`${prefix}-font-size-xs`] = `${fontSizeXs}px`;
291
+ vars[`${prefix}-font-size-sm`] = `${fontSizeSm}px`;
292
+ vars[`${prefix}-font-size-md`] = `${fontSizeMd}px`;
293
+ vars[`${prefix}-font-size-lg`] = `${fontSizeLg}px`;
294
+ vars[`${prefix}-font-size-xl`] = `${fontSizeXl}px`;
295
+ vars[`${prefix}-font-size-2xl`] = `${fontSize2xl}px`;
296
+
297
+ // Display font size (optional, may not be in theme)
298
+ if ('display1' in typography) {
299
+ const display1 = (typography as any).display1;
300
+ vars[`${prefix}-display-1`] = typeof display1 === 'string' ? display1 : `${display1}px`;
301
+ }
302
+
303
+ // Letter spacing for headings (from typography config)
304
+ const headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;
305
+ headings.forEach((heading) => {
306
+ const headingConfig = typography[heading];
307
+ if (headingConfig?.letterSpacing) {
308
+ vars[`${prefix}-letter-spacing-${heading}`] = String(headingConfig.letterSpacing);
309
+ }
310
+ });
311
+
312
+ return vars;
313
+ }
314
+
315
+ /**
316
+ * Generate CSS variables from theme shadows
317
+ */
318
+ function generateShadowVariables(shadows: Theme['shadows'], prefix: string): Record<string, string> {
319
+ const vars: Record<string, string> = {};
320
+
321
+ // Map JS shadow keys to SCSS variables
322
+ // SCSS uses --atomix-box-shadow (base) and --atomix-box-shadow-{size}
323
+ if (shadows.md) vars[`${prefix}-box-shadow`] = shadows.md; // Map md to base
324
+ if (shadows.xs) vars[`${prefix}-box-shadow-xs`] = shadows.xs;
325
+ if (shadows.sm) vars[`${prefix}-box-shadow-sm`] = shadows.sm;
326
+ if (shadows.lg) vars[`${prefix}-box-shadow-lg`] = shadows.lg;
327
+ if (shadows.xl) vars[`${prefix}-box-shadow-xl`] = shadows.xl;
328
+
329
+ // Inset shadow (generate from base shadow if not provided)
330
+ if (shadows.inset) {
331
+ vars[`${prefix}-box-shadow-inset`] = shadows.inset;
332
+ } else if (shadows.sm) {
333
+ // Generate inset shadow from sm shadow
334
+ vars[`${prefix}-box-shadow-inset`] = shadows.sm.replace(/^0\s/, 'inset ');
335
+ }
336
+
337
+ return vars;
338
+ }
339
+
340
+ /**
341
+ * Generate CSS variables from theme transitions
342
+ */
343
+ function generateTransitionVariables(
344
+ transitions: Theme['transitions'],
345
+ prefix: string
346
+ ): Record<string, string> {
347
+ const vars: Record<string, string> = {};
348
+
349
+ // Map JS transition durations to SCSS equivalents (in seconds to match design tokens)
350
+ const durationFast = transitions.duration.shortest || 150;
351
+ const durationBase = transitions.duration.standard || 300;
352
+ const durationSlow = transitions.duration.complex || 500;
353
+ const durationSlower = 700; // Default value for slower duration
354
+ const easingBase = transitions.easing.easeInOut || 'cubic-bezier(0.23, 1, 0.32, 1)';
355
+
356
+ vars[`${prefix}-transition-duration-fast`] = `${durationFast / 1000}s`;
357
+ vars[`${prefix}-transition-duration-base`] = `${durationBase / 1000}s`;
358
+ vars[`${prefix}-transition-duration-slow`] = `${durationSlow / 1000}s`;
359
+ vars[`${prefix}-transition-duration-slower`] = `${durationSlower / 1000}s`;
360
+
361
+ // Map easing functions
362
+ vars[`${prefix}-easing-base`] = easingBase;
363
+ vars[`${prefix}-easing-ease-in-out`] = transitions.easing.easeInOut || 'cubic-bezier(0.4, 0, 0.2, 1)';
364
+ vars[`${prefix}-easing-ease-out`] = transitions.easing.easeOut || 'cubic-bezier(0, 0, 0.2, 1)';
365
+ vars[`${prefix}-easing-ease-in`] = transitions.easing.easeIn || 'cubic-bezier(0.4, 0, 1, 1)';
366
+ vars[`${prefix}-easing-ease-linear`] = 'linear';
367
+
368
+ // Generate full transition strings
369
+ vars[`${prefix}-transition-fast`] = `all ${durationFast / 1000}s ${easingBase}`;
370
+ vars[`${prefix}-transition-base`] = `all ${durationBase / 1000}s ${easingBase}`;
371
+ vars[`${prefix}-transition-slow`] = `all ${durationSlow / 1000}s ${easingBase}`;
372
+
373
+ return vars;
374
+ }
375
+
376
+ /**
377
+ * Generate CSS variables from theme z-index
378
+ */
379
+ function generateZIndexVariables(zIndex: Theme['zIndex'], prefix: string): Record<string, string> {
380
+ const vars: Record<string, string> = {};
381
+
382
+ // Map to SCSS z-layers
383
+ if (zIndex.mobileStepper) vars[`${prefix}-z-dropdown`] = String(zIndex.mobileStepper);
384
+ if (zIndex.appBar) vars[`${prefix}-z-sticky`] = String(zIndex.appBar);
385
+ vars[`${prefix}-z-fixed`] = '1030'; // Default fixed
386
+ if (zIndex.modal) vars[`${prefix}-z-modal`] = String(zIndex.modal);
387
+ if (zIndex.speedDial) vars[`${prefix}-z-popover`] = String(zIndex.speedDial);
388
+ if (zIndex.tooltip) vars[`${prefix}-z-tooltip`] = String(zIndex.tooltip);
389
+ if (zIndex.drawer) vars[`${prefix}-z-drawer`] = String(zIndex.drawer);
390
+
391
+ // Keep original mappings if needed or remove if strictly aligning
392
+ if (zIndex.snackbar) vars[`${prefix}-z-snackbar`] = String(zIndex.snackbar);
393
+
394
+ return vars;
395
+ }
396
+
397
+ /**
398
+ * Generate CSS variables from theme breakpoints
399
+ */
400
+ function generateBreakpointVariables(
401
+ breakpoints: Theme['breakpoints'],
402
+ prefix: string
403
+ ): Record<string, string> {
404
+ const vars: Record<string, string> = {};
405
+
406
+ Object.entries(breakpoints.values).forEach(([key, value]) => {
407
+ vars[`${prefix}-breakpoint-${key}`] = `${value}${breakpoints.unit}`;
408
+ });
409
+
410
+ return vars;
411
+ }
412
+
413
+ /**
414
+ * Generate CSS variables from theme spacing
415
+ */
416
+ function generateSpacingVariables(
417
+ spacing: Theme['spacing'],
418
+ prefix: string
419
+ ): Record<string, string> {
420
+ const vars: Record<string, string> = {};
421
+
422
+ // Generate spacing scale based on design system tokens
423
+ // Note: Some spacing values like px-6, px-10, etc. are generated from the spacing function
424
+ // and should match the actual design system spacing scale
425
+ // Values are multipliers for the spacing base unit (default 4px)
426
+ const spacingScale: Record<string, number> = {
427
+ '0': 0,
428
+ '1': 1, // 4px (1 × 4 = 4px)
429
+ 'px-6': 1.5, // 6px (1.5 × 4 = 6px)
430
+ '2': 2, // 8px (2 × 4 = 8px)
431
+ 'px-10': 2.5, // 10px (2.5 × 4 = 10px)
432
+ '3': 3, // 12px (3 × 4 = 12px)
433
+ 'px-14': 3.5, // 14px (3.5 × 4 = 14px)
434
+ '4': 4, // 16px (4 × 4 = 16px)
435
+ '5': 5, // 20px (5 × 4 = 20px)
436
+ 'px-22': 5.5, // 22px (5.5 × 4 = 22px)
437
+ '6': 6, // 24px (6 × 4 = 24px)
438
+ '7': 7, // 28px (7 × 4 = 28px)
439
+ 'px-30': 7.5, // 30px (7.5 × 4 = 30px)
440
+ '8': 8, // 32px (8 × 4 = 32px)
441
+ '9': 9, // 36px (9 × 4 = 36px)
442
+ '10': 10, // 40px (10 × 4 = 40px)
443
+ '11': 11, // 44px (11 × 4 = 44px)
444
+ '12': 12, // 48px (12 × 4 = 48px)
445
+ '14': 14, // 56px (14 × 4 = 56px)
446
+ '16': 16, // 64px (16 × 4 = 64px)
447
+ '20': 20, // 80px (20 × 4 = 80px)
448
+ '24': 24, // 96px (24 × 4 = 96px)
449
+ '28': 28, // 112px (28 × 4 = 112px)
450
+ '32': 32, // 128px (32 × 4 = 128px)
451
+ '36': 36, // 144px (36 × 4 = 144px)
452
+ '40': 40, // 160px (40 × 4 = 160px)
453
+ '44': 44, // 176px (44 × 4 = 176px)
454
+ '48': 48, // 192px (48 × 4 = 192px)
455
+ '52': 52, // 208px (52 × 4 = 208px)
456
+ '56': 56, // 224px (56 × 4 = 224px)
457
+ '60': 60, // 240px (60 × 4 = 240px)
458
+ '64': 64, // 256px (64 × 4 = 256px)
459
+ '72': 72, // 288px (72 × 4 = 288px)
460
+ '80': 80, // 320px (80 × 4 = 320px)
461
+ '90': 90, // 360px (90 × 4 = 360px)
462
+ '200': 200, // 800px (200 × 4 = 800px)
463
+ };
464
+
465
+ // Generate spacing variables
466
+ // Use the spacing function to calculate values
467
+ Object.entries(spacingScale).forEach(([key, multiplier]) => {
468
+ const spacingValue = spacing(multiplier);
469
+ // Extract numeric value and convert to rem if needed
470
+ const match = spacingValue.match(/([\d.]+)px/);
471
+ if (match && match[1]) {
472
+ const pxValue = parseFloat(match[1]);
473
+ const remValue = pxValue / 16; // Convert px to rem (assuming 16px base)
474
+ vars[`${prefix}-spacing-${key}`] = `${remValue}rem`;
475
+ } else {
476
+ vars[`${prefix}-spacing-${key}`] = spacingValue;
477
+ }
478
+ });
479
+
480
+ return vars;
481
+ }
482
+
483
+ /**
484
+ * Generate border-related CSS variables
485
+ */
486
+ function generateBorderVariables(
487
+ palette: Theme['palette'],
488
+ prefix: string
489
+ ): Record<string, string> {
490
+ const vars: Record<string, string> = {};
491
+
492
+ // Border width
493
+ vars[`${prefix}-border-width`] = '1px';
494
+
495
+ // Border style
496
+ vars[`${prefix}-border-style`] = 'solid';
497
+
498
+ // Border color (already generated in palette, but ensure it exists)
499
+ if (!vars[`${prefix}-border-color`] && palette.text) {
500
+ vars[`${prefix}-border-color`] = alpha(palette.text.primary, 0.1);
501
+ }
502
+
503
+ return vars;
504
+ }
505
+
506
+ /**
507
+ * Generate border radius CSS variables
508
+ */
509
+ function generateBorderRadiusVariables(
510
+ borderRadius: Theme['borderRadius'],
511
+ prefix: string
512
+ ): Record<string, string> {
513
+ const vars: Record<string, string> = {};
514
+
515
+ // Convert values to string with proper units
516
+ const formatValue = (value: string | number | undefined, defaultValue: string): string => {
517
+ if (value === undefined) return defaultValue;
518
+ if (typeof value === 'number') return `${value}px`;
519
+ return String(value);
520
+ };
521
+
522
+ // Base border radius (maps to spacing-2 = 8px)
523
+ vars[`${prefix}-border-radius`] = formatValue(borderRadius.base, '0.5rem');
524
+
525
+ // Small border radius (maps to spacing-1 = 4px)
526
+ vars[`${prefix}-border-radius-sm`] = formatValue(borderRadius.sm, '0.25rem');
527
+
528
+ // Large border radius (maps to spacing-2.5 = 10px)
529
+ vars[`${prefix}-border-radius-lg`] = formatValue(borderRadius.lg, '0.625rem');
530
+
531
+ // Extra large border radius (maps to spacing-3 = 12px)
532
+ vars[`${prefix}-border-radius-xl`] = formatValue(borderRadius.xl, '0.75rem');
533
+
534
+ // 2X large border radius (maps to spacing-4 = 16px)
535
+ vars[`${prefix}-border-radius-xxl`] = formatValue(borderRadius.xxl, '1rem');
536
+ // Also add deprecated 2xl alias for consistency
537
+ vars[`${prefix}-border-radius-2xl`] = formatValue(borderRadius.xxl, '1rem');
538
+
539
+ // 3X large border radius (maps to spacing-6 = 24px)
540
+ vars[`${prefix}-border-radius-3xl`] = formatValue(borderRadius['3xl'], '1.5rem');
541
+
542
+ // 4X large border radius (maps to spacing-8 = 32px)
543
+ vars[`${prefix}-border-radius-4xl`] = formatValue(borderRadius['4xl'], '2rem');
544
+
545
+ // Pill shape (fully rounded, maps to spacing-200 = 800px)
546
+ vars[`${prefix}-border-radius-pill`] = formatValue(borderRadius.pill, '50rem');
547
+
548
+ return vars;
549
+ }
550
+
551
+ /**
552
+ * Generate focus ring CSS variables
553
+ */
554
+ function generateFocusRingVariables(
555
+ palette: Theme['palette'],
556
+ prefix: string
557
+ ): Record<string, string> {
558
+ const vars: Record<string, string> = {};
559
+
560
+ // Focus ring properties
561
+ vars[`${prefix}-focus-ring-width`] = '3px';
562
+ vars[`${prefix}-focus-ring-offset`] = '2px';
563
+ vars[`${prefix}-focus-ring-opacity`] = '0.25';
564
+
565
+ return vars;
566
+ }
567
+
568
+ /**
569
+ * Generate CSS custom properties from a theme object
570
+ *
571
+ * @param theme - Theme object created with createTheme
572
+ * @param options - Generation options
573
+ * @returns CSS string with custom properties
574
+ */
575
+ export function generateCSSVariables(
576
+ theme: Theme,
577
+ options: GenerateCSSVariablesOptions = {}
578
+ ): string {
579
+ const {
580
+ selector = ':root',
581
+ inject = false,
582
+ styleId = 'atomix-theme-variables',
583
+ prefix = 'atomix',
584
+ } = options;
585
+
586
+ const variables: Record<string, string> = {};
587
+
588
+ // Generate variables from each theme section
589
+ Object.assign(variables, generatePaletteVariables(theme.palette, prefix));
590
+ Object.assign(variables, generateTypographyVariables(theme.typography, prefix));
591
+ Object.assign(variables, generateShadowVariables(theme.shadows, prefix));
592
+ Object.assign(variables, generateTransitionVariables(theme.transitions, prefix));
593
+ Object.assign(variables, generateZIndexVariables(theme.zIndex, prefix));
594
+ Object.assign(variables, generateBreakpointVariables(theme.breakpoints, prefix));
595
+ Object.assign(variables, generateSpacingVariables(theme.spacing, prefix));
596
+ Object.assign(variables, generateBorderVariables(theme.palette, prefix));
597
+ Object.assign(variables, generateBorderRadiusVariables(theme.borderRadius, prefix));
598
+ Object.assign(variables, generateFocusRingVariables(theme.palette, prefix));
599
+
600
+ // Add custom properties if present
601
+ if (theme.custom && Object.keys(theme.custom).length > 0) {
602
+ const customVars = flattenObject(theme.custom, `${prefix}-custom`);
603
+ Object.assign(variables, customVars);
604
+ }
605
+
606
+ // Convert to CSS string
607
+ const cssVariables = Object.entries(variables)
608
+ .map(([key, value]) => ` --${key}: ${value};`)
609
+ .join('\n');
610
+
611
+ const css = `${selector} {\n${cssVariables}\n}`;
612
+
613
+ // Inject into DOM if requested
614
+ if (inject && isBrowser()) {
615
+ injectCSS(css, styleId);
616
+ }
617
+
618
+ return css;
619
+ }
620
+
621
+ /**
622
+ * Inject CSS into the DOM
623
+ *
624
+ * @param css - CSS string to inject
625
+ * @param styleId - ID for the style element
626
+ */
627
+ export function injectCSS(css: string, styleId: string = 'atomix-theme-variables'): void {
628
+ if (!isBrowser()) return;
629
+
630
+ let styleElement = document.getElementById(styleId) as HTMLStyleElement | null;
631
+
632
+ if (!styleElement) {
633
+ styleElement = document.createElement('style');
634
+ styleElement.id = styleId;
635
+ styleElement.setAttribute('data-atomix-theme-vars', 'true');
636
+ document.head.appendChild(styleElement);
637
+ }
638
+
639
+ styleElement.textContent = css;
640
+ }
641
+
642
+ /**
643
+ * Remove injected CSS from the DOM
644
+ *
645
+ * @param styleId - ID of the style element to remove
646
+ */
647
+ export function removeInjectedCSS(styleId: string = 'atomix-theme-variables'): void {
648
+ if (!isBrowser()) return;
649
+
650
+ const styleElement = document.getElementById(styleId);
651
+ if (styleElement) {
652
+ styleElement.remove();
653
+ }
654
+ }
655
+
656
+ /**
657
+ * Generate CSS variables for a specific theme section
658
+ *
659
+ * @param theme - Theme object
660
+ * @param section - Theme section to generate variables for
661
+ * @param options - Generation options
662
+ * @returns CSS string with custom properties for the section
663
+ */
664
+ export function generateSectionVariables(
665
+ theme: Theme,
666
+ section: 'palette' | 'typography' | 'shadows' | 'transitions' | 'zIndex' | 'breakpoints' | 'spacing' | 'borders' | 'borderRadius' | 'focusRing',
667
+ options: GenerateCSSVariablesOptions = {}
668
+ ): string {
669
+ const { selector = ':root', prefix = 'atomix' } = options;
670
+
671
+ let variables: Record<string, string> = {};
672
+
673
+ switch (section) {
674
+ case 'palette':
675
+ variables = generatePaletteVariables(theme.palette, prefix);
676
+ break;
677
+ case 'typography':
678
+ variables = generateTypographyVariables(theme.typography, prefix);
679
+ break;
680
+ case 'shadows':
681
+ variables = generateShadowVariables(theme.shadows, prefix);
682
+ break;
683
+ case 'transitions':
684
+ variables = generateTransitionVariables(theme.transitions, prefix);
685
+ break;
686
+ case 'zIndex':
687
+ variables = generateZIndexVariables(theme.zIndex, prefix);
688
+ break;
689
+ case 'breakpoints':
690
+ variables = generateBreakpointVariables(theme.breakpoints, prefix);
691
+ break;
692
+ case 'spacing':
693
+ variables = generateSpacingVariables(theme.spacing, prefix);
694
+ break;
695
+ case 'borders':
696
+ variables = generateBorderVariables(theme.palette, prefix);
697
+ break;
698
+ case 'borderRadius':
699
+ variables = generateBorderRadiusVariables(theme.borderRadius, prefix);
700
+ break;
701
+ case 'focusRing':
702
+ variables = generateFocusRingVariables(theme.palette, prefix);
703
+ break;
704
+ }
705
+
706
+ const cssVariables = Object.entries(variables)
707
+ .map(([key, value]) => ` --${key}: ${value};`)
708
+ .join('\n');
709
+
710
+ return `${selector} {\n${cssVariables}\n}`;
711
+ }
712
+
713
+ export default generateCSSVariables;