@shohojdhara/atomix 0.3.13 → 0.3.14

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 (151) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +2 -0
  3. package/dist/atomix.css +95 -77
  4. package/dist/atomix.css.map +1 -1
  5. package/dist/atomix.min.css +2 -2
  6. package/dist/atomix.min.css.map +1 -1
  7. package/dist/charts.d.ts +1 -1
  8. package/dist/charts.js +1 -1
  9. package/dist/core.d.ts +41 -11
  10. package/dist/core.js +39 -23
  11. package/dist/core.js.map +1 -1
  12. package/dist/forms.d.ts +28 -11
  13. package/dist/forms.js +8 -5
  14. package/dist/forms.js.map +1 -1
  15. package/dist/heavy.d.ts +1 -1
  16. package/dist/heavy.js +15 -6
  17. package/dist/heavy.js.map +1 -1
  18. package/dist/index.d.ts +122 -46
  19. package/dist/index.esm.js +849 -182
  20. package/dist/index.esm.js.map +1 -1
  21. package/dist/index.js +854 -186
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.min.js +1 -1
  24. package/dist/index.min.js.map +1 -1
  25. package/dist/theme.d.ts +27 -2
  26. package/dist/theme.js +721 -108
  27. package/dist/theme.js.map +1 -1
  28. package/package.json +1 -1
  29. package/scripts/atomix-cli.js +610 -1111
  30. package/scripts/cli/component-generator.js +610 -0
  31. package/scripts/cli/documentation-sync.js +542 -0
  32. package/scripts/cli/interactive-init.js +84 -288
  33. package/scripts/cli/mappings.js +211 -0
  34. package/scripts/cli/migration-tools.js +95 -288
  35. package/scripts/cli/template-manager.js +107 -0
  36. package/scripts/cli/templates/README.md +123 -0
  37. package/scripts/cli/templates/composable-templates.js +149 -0
  38. package/scripts/cli/templates/config-templates.js +126 -0
  39. package/scripts/cli/templates/index.js +95 -0
  40. package/scripts/cli/templates/project-templates.js +214 -0
  41. package/scripts/cli/templates/react-templates.js +261 -0
  42. package/scripts/cli/templates/scss-templates.js +156 -0
  43. package/scripts/cli/templates/storybook-templates.js +236 -0
  44. package/scripts/cli/templates/testing-templates.js +45 -0
  45. package/scripts/cli/templates/token-templates.js +447 -0
  46. package/scripts/cli/templates/types-templates.js +133 -0
  47. package/scripts/cli/templates-original-backup.js +1655 -0
  48. package/scripts/cli/templates.js +35 -0
  49. package/scripts/cli/templates_backup.js +684 -0
  50. package/scripts/cli/theme-bridge.js +20 -14
  51. package/scripts/cli/token-manager.js +150 -77
  52. package/scripts/cli/utils.js +37 -25
  53. package/src/components/Accordion/Accordion.stories.tsx +5 -5
  54. package/src/components/Accordion/Accordion.test.tsx +57 -0
  55. package/src/components/Accordion/Accordion.tsx +4 -0
  56. package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +1 -1
  57. package/src/components/AtomixGlass/stories/Examples.stories.tsx +37 -37
  58. package/src/components/AtomixGlass/stories/Playground.stories.tsx +50 -51
  59. package/src/components/Avatar/Avatar.stories.tsx +26 -26
  60. package/src/components/Badge/Badge.stories.tsx +31 -31
  61. package/src/components/Badge/Badge.test.tsx +51 -0
  62. package/src/components/Badge/Badge.tsx +20 -1
  63. package/src/components/Block/Block.stories.tsx +5 -5
  64. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +1 -1
  65. package/src/components/Breadcrumb/Breadcrumb.tsx +2 -2
  66. package/src/components/Button/Button.stories.tsx +13 -13
  67. package/src/components/Button/Button.tsx +4 -4
  68. package/src/components/Button/ButtonGroup.stories.tsx +2 -2
  69. package/src/components/Button/README.md +5 -0
  70. package/src/components/Callout/Callout.stories.tsx +11 -11
  71. package/src/components/Callout/Callout.test.tsx +10 -10
  72. package/src/components/Callout/Callout.tsx +7 -7
  73. package/src/components/Callout/README.md +9 -8
  74. package/src/components/Card/Card.tsx +2 -2
  75. package/src/components/Chart/Chart.stories.tsx +6 -6
  76. package/src/components/Chart/Chart.tsx +1 -1
  77. package/src/components/ColorModeToggle/ColorModeToggle.stories.tsx +1 -1
  78. package/src/components/DataTable/DataTable.tsx +14 -12
  79. package/src/components/DatePicker/DatePicker.stories.tsx +6 -6
  80. package/src/components/Dropdown/Dropdown.stories.tsx +4 -4
  81. package/src/components/Form/Checkbox.stories.tsx +3 -3
  82. package/src/components/Form/Checkbox.tsx +4 -2
  83. package/src/components/Form/Form.stories.tsx +3 -3
  84. package/src/components/Form/FormGroup.stories.tsx +1 -1
  85. package/src/components/Form/Input.stories.tsx +28 -16
  86. package/src/components/Form/Input.test.tsx +59 -0
  87. package/src/components/Form/Input.tsx +97 -95
  88. package/src/components/Form/Radio.stories.tsx +94 -94
  89. package/src/components/Form/Radio.tsx +2 -2
  90. package/src/components/Form/Select.stories.tsx +4 -4
  91. package/src/components/Form/Select.tsx +2 -2
  92. package/src/components/Form/Textarea.stories.tsx +22 -7
  93. package/src/components/Form/Textarea.test.tsx +45 -0
  94. package/src/components/Form/Textarea.tsx +88 -86
  95. package/src/components/List/List.stories.tsx +2 -2
  96. package/src/components/Modal/Modal.stories.tsx +4 -4
  97. package/src/components/Navigation/Navbar/Navbar.stories.tsx +5 -5
  98. package/src/components/Navigation/Navbar/Navbar.tsx +1 -1
  99. package/src/components/Navigation/README.md +1 -1
  100. package/src/components/Pagination/Pagination.stories.tsx +5 -2
  101. package/src/components/Pagination/Pagination.tsx +1 -1
  102. package/src/components/PhotoViewer/PhotoViewer.stories.tsx +10 -10
  103. package/src/components/Popover/Popover.stories.tsx +1 -1
  104. package/src/components/ProductReview/ProductReview.tsx +1 -1
  105. package/src/components/Progress/Progress.tsx +46 -46
  106. package/src/components/Rating/Rating.stories.tsx +4 -4
  107. package/src/components/Rating/Rating.tsx +8 -8
  108. package/src/components/Slider/Slider.stories.tsx +63 -63
  109. package/src/components/Spinner/Spinner.stories.tsx +2 -2
  110. package/src/components/Spinner/Spinner.test.tsx +35 -0
  111. package/src/components/Spinner/Spinner.tsx +9 -2
  112. package/src/components/Testimonial/Testimonial.stories.tsx +1 -1
  113. package/src/components/Toggle/Toggle.stories.tsx +32 -9
  114. package/src/components/Toggle/Toggle.test.tsx +91 -0
  115. package/src/components/Toggle/Toggle.tsx +44 -27
  116. package/src/components/Tooltip/Tooltip.tsx +1 -1
  117. package/src/layouts/Grid/Grid.stories.tsx +49 -49
  118. package/src/layouts/MasonryGrid/MasonryGrid.stories.tsx +2 -2
  119. package/src/lib/composables/useAccordion.ts +12 -3
  120. package/src/lib/composables/useBreadcrumb.ts +2 -2
  121. package/src/lib/composables/useCallout.ts +7 -7
  122. package/src/lib/composables/useNavbar.ts +1 -1
  123. package/src/lib/constants/components.ts +1 -1
  124. package/src/lib/storybook/InteractiveDemo.tsx +113 -0
  125. package/src/lib/storybook/PreviewContainer.tsx +36 -0
  126. package/src/lib/storybook/VariantsGrid.tsx +21 -0
  127. package/src/lib/storybook/index.ts +3 -0
  128. package/src/lib/theme/core/createThemeObject.ts +9 -5
  129. package/src/lib/theme/devtools/CLI.ts +155 -0
  130. package/src/lib/theme/devtools/DesignTokensCustomizer.stories.tsx +213 -0
  131. package/src/lib/theme/devtools/DesignTokensCustomizer.tsx +566 -0
  132. package/src/lib/theme/devtools/LiveEditor.tsx +2 -1
  133. package/src/lib/theme/devtools/index.ts +3 -0
  134. package/src/lib/theme/errors/errors.ts +8 -0
  135. package/src/lib/theme/runtime/ThemeProvider.tsx +117 -57
  136. package/src/lib/theme/runtime/__tests__/ThemeProvider.integration.test.tsx +305 -0
  137. package/src/lib/theme/runtime/__tests__/ThemeProvider.test.tsx +588 -0
  138. package/src/lib/theme/utils/__tests__/themeValidation.test.ts +264 -0
  139. package/src/lib/theme/utils/index.ts +1 -0
  140. package/src/lib/theme/utils/themeValidation.ts +501 -0
  141. package/src/lib/theme-tools.ts +32 -3
  142. package/src/lib/types/components.ts +81 -26
  143. package/src/lib/utils/themeNaming.ts +1 -1
  144. package/src/styles/06-components/_components.callout.scss +29 -33
  145. package/src/styles/06-components/_index.scss +1 -1
  146. package/src/styles/99-utilities/_utilities.display.scss +14 -3
  147. package/src/styles/99-utilities/_utilities.flex.scss +10 -10
  148. package/src/styles/99-utilities/_utilities.text.scss +28 -8
  149. package/scripts/cli/__tests__/cli-commands.test.js +0 -204
  150. package/scripts/cli/__tests__/utils.test.js +0 -201
  151. package/scripts/cli/__tests__/vitest.config.js +0 -26
@@ -0,0 +1,501 @@
1
+ /**
2
+ * Theme Validation Utilities
3
+ *
4
+ * Comprehensive validation utilities for DesignTokens objects.
5
+ * Includes color format validation, accessibility checks, and required properties verification.
6
+ */
7
+
8
+ import type { ThemeValidationResult } from '../types';
9
+ import type { DesignTokens } from '../tokens';
10
+ import { getLogger, ThemeError, ThemeErrorCode } from '../errors';
11
+ import { defaultTokens } from '../tokens/tokens';
12
+
13
+ const logger = getLogger();
14
+
15
+ /**
16
+ * Color format validation patterns
17
+ */
18
+ const COLOR_PATTERNS = {
19
+ /** Hex color: #RGB, #RRGGBB, #RRGGBBAA */
20
+ hex: /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/,
21
+ /** RGB color: rgb(r, g, b) or rgb(r, g, b, a) */
22
+ rgb: /^rgb\(\s*(\d{1,3}(?:\.\d+)?)\s*,\s*(\d{1,3}(?:\.\d+)?)\s*,\s*(\d{1,3}(?:\.\d+)?)\s*(?:,\s*([01]?\.?\d+))?\s*\)$/,
23
+ /** RGBA color: rgba(r, g, b, a) */
24
+ rgba: /^rgba\(\s*(\d{1,3}(?:\.\d+)?)\s*,\s*(\d{1,3}(?:\.\d+)?)\s*,\s*(\d{1,3}(?:\.\d+)?)\s*,\s*([01]?\.?\d+)\s*\)$/,
25
+ /** HSL color: hsl(h, s%, l%) */
26
+ hsl: /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/,
27
+ /** HSLA color: hsla(h, s%, l%, a) */
28
+ hsla: /^hsla\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*,\s*([01]?\.?\d+)\s*\)$/,
29
+ };
30
+
31
+ /**
32
+ * Required color tokens that must be present in DesignTokens
33
+ */
34
+ const REQUIRED_COLOR_TOKENS = [
35
+ 'primary',
36
+ 'secondary',
37
+ 'success',
38
+ 'info',
39
+ 'warning',
40
+ 'error',
41
+ 'light',
42
+ 'dark',
43
+ ];
44
+
45
+ /**
46
+ * Required text emphasis tokens
47
+ */
48
+ const REQUIRED_TEXT_EMPHASIS_TOKENS = [
49
+ 'primary-text-emphasis',
50
+ 'secondary-text-emphasis',
51
+ 'tertiary-text-emphasis',
52
+ 'disabled-text-emphasis',
53
+ ];
54
+
55
+ /**
56
+ * Accessibility-critical text/background combinations to validate
57
+ */
58
+ const ACCESSIBILITY_CHECKS = [
59
+ {
60
+ text: 'primary-text-emphasis',
61
+ background: 'primary-bg-subtle',
62
+ name: 'Primary text on subtle background',
63
+ },
64
+ {
65
+ text: 'secondary-text-emphasis',
66
+ background: 'secondary-bg-subtle',
67
+ name: 'Secondary text on subtle background',
68
+ },
69
+ {
70
+ text: 'error-text-emphasis',
71
+ background: 'error-bg-subtle',
72
+ name: 'Error text on subtle background',
73
+ },
74
+ {
75
+ text: 'success-text-emphasis',
76
+ background: 'success-bg-subtle',
77
+ name: 'Success text on subtle background',
78
+ },
79
+ ];
80
+
81
+ /**
82
+ * Convert hex color to RGB values
83
+ */
84
+ function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
85
+ const result = COLOR_PATTERNS.hex.exec(hex);
86
+ if (!result || !result[1]) return null;
87
+
88
+ const digits = result[1];
89
+ let r: number, g: number, b: number;
90
+
91
+ switch (digits.length) {
92
+ case 3: // #RGB
93
+ r = parseInt((digits[0] ?? '0') + (digits[0] ?? '0'), 16);
94
+ g = parseInt((digits[1] ?? '0') + (digits[1] ?? '0'), 16);
95
+ b = parseInt((digits[2] ?? '0') + (digits[2] ?? '0'), 16);
96
+ break;
97
+ case 4: // #RGBA (ignore alpha)
98
+ r = parseInt((digits[0] ?? '0') + (digits[0] ?? '0'), 16);
99
+ g = parseInt((digits[1] ?? '0') + (digits[1] ?? '0'), 16);
100
+ b = parseInt((digits[2] ?? '0') + (digits[2] ?? '0'), 16);
101
+ break;
102
+ case 6: // #RRGGBB
103
+ r = parseInt(digits.slice(0, 2), 16);
104
+ g = parseInt(digits.slice(2, 4), 16);
105
+ b = parseInt(digits.slice(4, 6), 16);
106
+ break;
107
+ case 8: // #RRGGBBAA (ignore alpha)
108
+ r = parseInt(digits.slice(0, 2), 16);
109
+ g = parseInt(digits.slice(2, 4), 16);
110
+ b = parseInt(digits.slice(4, 6), 16);
111
+ break;
112
+ default:
113
+ return null;
114
+ }
115
+
116
+ return { r, g, b };
117
+ }
118
+
119
+ /**
120
+ * Parse RGB/RGBA color string to RGB values
121
+ */
122
+ function rgbToRgb(rgb: string): { r: number; g: number; b: number } | null {
123
+ const match = rgb.match(COLOR_PATTERNS.rgb) || rgb.match(COLOR_PATTERNS.rgba);
124
+ if (!match) return null;
125
+
126
+ const r = parseInt(match[1] ?? '0', 10);
127
+ const g = parseInt(match[2] ?? '0', 10);
128
+ const b = parseInt(match[3] ?? '0', 10);
129
+
130
+ // Validate RGB ranges
131
+ if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {
132
+ return null;
133
+ }
134
+
135
+ return { r, g, b };
136
+ }
137
+
138
+ /**
139
+ * Parse HSL/HSLA color string to RGB values
140
+ */
141
+ function hslToRgb(hsl: string): { r: number; g: number; b: number } | null {
142
+ const match = hsl.match(COLOR_PATTERNS.hsl) || hsl.match(COLOR_PATTERNS.hsla);
143
+ if (!match) return null;
144
+
145
+ let h = parseInt(match[1] ?? '0', 10) / 360;
146
+ let s = parseInt(match[2] ?? '0', 10) / 100;
147
+ let l = parseInt(match[3] ?? '0', 10) / 100;
148
+
149
+ // Validate HSL ranges
150
+ if (h < 0 || h > 1 || s < 0 || s > 1 || l < 0 || l > 1) {
151
+ return null;
152
+ }
153
+
154
+ const hue2rgb = (p: number, q: number, t: number): number => {
155
+ if (t < 0) t += 1;
156
+ if (t > 1) t -= 1;
157
+ if (t < 1/6) return p + (q - p) * 6 * t;
158
+ if (t < 1/2) return q;
159
+ if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
160
+ return p;
161
+ };
162
+
163
+ let r: number, g: number, b: number;
164
+
165
+ if (s === 0) {
166
+ r = g = b = l; // achromatic
167
+ } else {
168
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
169
+ const p = 2 * l - q;
170
+ r = hue2rgb(p, q, h + 1/3);
171
+ g = hue2rgb(p, q, h);
172
+ b = hue2rgb(p, q, h - 1/3);
173
+ }
174
+
175
+ return {
176
+ r: Math.round(r * 255),
177
+ g: Math.round(g * 255),
178
+ b: Math.round(b * 255)
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Convert any valid color format to RGB values
184
+ */
185
+ export function colorToRgb(color: string): { r: number; g: number; b: number } | null {
186
+ // Try hex first
187
+ if (COLOR_PATTERNS.hex.test(color)) {
188
+ return hexToRgb(color);
189
+ }
190
+
191
+ // Try RGB/RGBA
192
+ if (color.startsWith('rgb')) {
193
+ return rgbToRgb(color);
194
+ }
195
+
196
+ // Try HSL/HSLA
197
+ if (color.startsWith('hsl')) {
198
+ return hslToRgb(color);
199
+ }
200
+
201
+ return null;
202
+ }
203
+
204
+ /**
205
+ * Validate that a color string is in a valid format (hex, rgb, hsl, etc.)
206
+ * Includes validation of value ranges for RGB and HSL
207
+ */
208
+ export function validateColorFormat(color: string): boolean {
209
+ // Check hex first (regex is sufficient)
210
+ if (COLOR_PATTERNS.hex.test(color)) {
211
+ return true;
212
+ }
213
+
214
+ // Check RGB/RGBA with value validation
215
+ if (COLOR_PATTERNS.rgb.test(color) || COLOR_PATTERNS.rgba.test(color)) {
216
+ const rgb = rgbToRgb(color);
217
+ return rgb !== null;
218
+ }
219
+
220
+ // Check HSL/HSLA with value validation
221
+ if (COLOR_PATTERNS.hsl.test(color) || COLOR_PATTERNS.hsla.test(color)) {
222
+ const hsl = hslToRgb(color);
223
+ return hsl !== null;
224
+ }
225
+
226
+ return false;
227
+ }
228
+
229
+ /**
230
+ * Calculate relative luminance of a color
231
+ * Based on WCAG guidelines: https://www.w3.org/TR/WCAG20-TECHS/G17.html
232
+ */
233
+ function getRelativeLuminance(r: number, g: number, b: number): number {
234
+ const normalize = (val: number) => {
235
+ val = val / 255;
236
+ return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
237
+ };
238
+
239
+ return 0.2126 * normalize(r) + 0.7152 * normalize(g) + 0.0722 * normalize(b);
240
+ }
241
+
242
+ /**
243
+ * Calculate contrast ratio between two colors (supports multiple formats)
244
+ */
245
+ export function getContrastRatioBetweenColors(color1: string, color2: string): number | null {
246
+ const rgb1 = colorToRgb(color1);
247
+ const rgb2 = colorToRgb(color2);
248
+
249
+ if (!rgb1 || !rgb2) {
250
+ return null;
251
+ }
252
+
253
+ const lum1 = getRelativeLuminance(rgb1.r, rgb1.g, rgb1.b);
254
+ const lum2 = getRelativeLuminance(rgb2.r, rgb2.g, rgb2.b);
255
+
256
+ const lighter = Math.max(lum1, lum2);
257
+ const darker = Math.min(lum1, lum2);
258
+
259
+ return (lighter + 0.05) / (darker + 0.05);
260
+ }
261
+
262
+ /**
263
+ * Check if contrast ratio meets WCAG AA standards
264
+ * AA requires 4.5:1 for normal text, 3:1 for large text
265
+ */
266
+ export function validateContrastRatio(
267
+ foreground: string,
268
+ background: string,
269
+ isLargeText = false
270
+ ): { valid: boolean; ratio: number; requiredRatio: number } {
271
+ const ratio = getContrastRatioBetweenColors(foreground, background);
272
+
273
+ if (ratio === null) {
274
+ return { valid: false, ratio: 0, requiredRatio: isLargeText ? 3 : 4.5 };
275
+ }
276
+
277
+ const requiredRatio = isLargeText ? 3 : 4.5;
278
+
279
+ return {
280
+ valid: ratio >= requiredRatio,
281
+ ratio,
282
+ requiredRatio,
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Validate all color formats in DesignTokens
288
+ */
289
+ export function validateColorFormats(tokens: Partial<DesignTokens>): {
290
+ valid: boolean;
291
+ errors: string[];
292
+ warnings: string[];
293
+ } {
294
+ const errors: string[] = [];
295
+ const warnings: string[] = [];
296
+
297
+ // Define tokens that should be validated as colors
298
+ // Only validate tokens that are intended to be actual colors, not gradients, font weights, etc.
299
+ const colorTokenKeys = new Set([
300
+ // Base colors
301
+ 'primary', 'secondary', 'success', 'info', 'warning', 'error', 'light', 'dark',
302
+ // Text emphasis
303
+ 'primary-text-emphasis', 'secondary-text-emphasis', 'tertiary-text-emphasis',
304
+ 'disabled-text-emphasis', 'invert-text-emphasis', 'brand-text-emphasis',
305
+ 'error-text-emphasis', 'success-text-emphasis', 'warning-text-emphasis',
306
+ 'info-text-emphasis', 'light-text-emphasis', 'dark-text-emphasis',
307
+ // Background subtle
308
+ 'primary-bg-subtle', 'secondary-bg-subtle', 'tertiary-bg-subtle', 'invert-bg-subtle',
309
+ 'brand-bg-subtle', 'error-bg-subtle', 'success-bg-subtle', 'warning-bg-subtle',
310
+ 'info-bg-subtle', 'light-bg-subtle', 'dark-bg-subtle',
311
+ // Border subtle
312
+ 'primary-border-subtle', 'secondary-border-subtle', 'success-border-subtle',
313
+ 'error-border-subtle', 'warning-border-subtle', 'info-border-subtle',
314
+ 'brand-border-subtle', 'light-border-subtle', 'dark-border-subtle',
315
+ // Hover states
316
+ 'primary-hover', 'secondary-hover', 'light-hover', 'dark-hover',
317
+ 'error-hover', 'success-hover', 'warning-hover', 'info-hover',
318
+ // Colors from scales (primary, red, green, blue, yellow)
319
+ ...Array.from({ length: 10 }, (_, i) => [
320
+ `primary-${i + 1}`, `red-${i + 1}`, `green-${i + 1}`, `blue-${i + 1}`, `yellow-${i + 1}`
321
+ ]).flat(),
322
+ // Gray scale
323
+ ...Array.from({ length: 10 }, (_, i) => `gray-${i + 1}`),
324
+ // Body colors
325
+ 'body-color', 'heading-color',
326
+ // Link colors
327
+ 'link-color', 'link-hover-color',
328
+ // Highlight & code
329
+ 'highlight-bg', 'code-color',
330
+ // Border colors
331
+ 'border-color', 'border-color-translucent',
332
+ // Focus ring
333
+ 'focus-border-color',
334
+ // Form validation
335
+ 'form-valid-color', 'form-valid-border-color',
336
+ 'form-invalid-color', 'form-invalid-border-color',
337
+ ]);
338
+
339
+ for (const key of colorTokenKeys) {
340
+ if (!(key in tokens)) continue; // Skip if token not present
341
+
342
+ const value = (tokens as any)[key];
343
+ if (typeof value !== 'string') {
344
+ errors.push(`Token '${key}' must be a string, got ${typeof value}`);
345
+ continue;
346
+ }
347
+
348
+ if (!validateColorFormat(value)) {
349
+ errors.push(`Token '${key}' has invalid color format: '${value}'`);
350
+ }
351
+ }
352
+
353
+ return { valid: errors.length === 0, errors, warnings };
354
+ }
355
+
356
+ /**
357
+ * Validate that all required tokens are present
358
+ */
359
+ export function validateRequiredTokens(tokens: Partial<DesignTokens>): {
360
+ valid: boolean;
361
+ errors: string[];
362
+ warnings: string[];
363
+ } {
364
+ const errors: string[] = [];
365
+ const warnings: string[] = [];
366
+
367
+ // Check required color tokens
368
+ for (const token of REQUIRED_COLOR_TOKENS) {
369
+ if (!(token in tokens)) {
370
+ errors.push(`Required color token '${token}' is missing`);
371
+ }
372
+ }
373
+
374
+ // Check required text emphasis tokens
375
+ for (const token of REQUIRED_TEXT_EMPHASIS_TOKENS) {
376
+ if (!(token in tokens)) {
377
+ errors.push(`Required text emphasis token '${token}' is missing`);
378
+ }
379
+ }
380
+
381
+ // Check for RGB versions of base colors
382
+ for (const token of REQUIRED_COLOR_TOKENS) {
383
+ const rgbToken = `${token}-rgb`;
384
+ if (!(rgbToken in tokens)) {
385
+ warnings.push(`RGB version of '${token}' token '${rgbToken}' is missing`);
386
+ }
387
+ }
388
+
389
+ return { valid: errors.length === 0, errors, warnings };
390
+ }
391
+
392
+ /**
393
+ * Validate accessibility contrast ratios
394
+ */
395
+ export function validateAccessibility(tokens: Partial<DesignTokens>): {
396
+ valid: boolean;
397
+ errors: string[];
398
+ warnings: string[];
399
+ } {
400
+ const errors: string[] = [];
401
+ const warnings: string[] = [];
402
+
403
+ for (const check of ACCESSIBILITY_CHECKS) {
404
+ const textColor = (tokens as any)[check.text];
405
+ const bgColor = (tokens as any)[check.background];
406
+
407
+ if (!textColor || !bgColor) {
408
+ warnings.push(`Cannot validate contrast for ${check.name}: missing tokens`);
409
+ continue;
410
+ }
411
+
412
+ const contrast = validateContrastRatio(textColor, bgColor);
413
+
414
+ if (!contrast.valid) {
415
+ const level = contrast.requiredRatio === 3 ? 'large text (AA)' : 'normal text (AA)';
416
+ errors.push(
417
+ `${check.name}: contrast ratio ${contrast.ratio.toFixed(2)}:1 is below required ${contrast.requiredRatio}:1 for ${level}`
418
+ );
419
+ }
420
+ }
421
+
422
+ return { valid: errors.length === 0, errors, warnings };
423
+ }
424
+
425
+ /**
426
+ * Validation options
427
+ */
428
+ export interface ValidationOptions {
429
+ /** Skip accessibility contrast validation */
430
+ skipAccessibility?: boolean;
431
+ /** Skip color format validation */
432
+ skipColorValidation?: boolean;
433
+ /** Skip required token validation */
434
+ skipRequiredTokens?: boolean;
435
+ }
436
+
437
+ /**
438
+ * Comprehensive validation of DesignTokens
439
+ */
440
+ export function validateDesignTokens(tokens: Partial<DesignTokens>, options: ValidationOptions = {}): ThemeValidationResult {
441
+ const results = [];
442
+
443
+ if (!options.skipRequiredTokens) {
444
+ results.push(validateRequiredTokens(tokens));
445
+ }
446
+
447
+ if (!options.skipColorValidation) {
448
+ results.push(validateColorFormats(tokens));
449
+ }
450
+
451
+ if (!options.skipAccessibility) {
452
+ results.push(validateAccessibility(tokens));
453
+ }
454
+
455
+ const allErrors = results.flatMap(r => r.errors);
456
+ const allWarnings = results.flatMap(r => r.warnings);
457
+
458
+ const valid = allErrors.length === 0;
459
+
460
+ // Log validation results
461
+ if (!valid) {
462
+ logger.error(
463
+ 'DesignTokens validation failed',
464
+ new Error(`Validation failed with ${allErrors.length} errors and ${allWarnings.length} warnings`),
465
+ {
466
+ errors: allErrors,
467
+ warnings: allWarnings,
468
+ tokenCount: Object.keys(tokens).length,
469
+ }
470
+ );
471
+ } else if (allWarnings.length > 0) {
472
+ logger.warn(
473
+ `DesignTokens validation passed with ${allWarnings.length} warnings`,
474
+ { warnings: allWarnings }
475
+ );
476
+ } else {
477
+ logger.debug('DesignTokens validation passed');
478
+ }
479
+
480
+ return {
481
+ valid,
482
+ errors: allErrors,
483
+ warnings: allWarnings,
484
+ };
485
+ }
486
+
487
+ /**
488
+ * Safely validate and merge partial tokens with defaults
489
+ */
490
+ export function validateAndMergeTokens(partialTokens?: Partial<DesignTokens>): {
491
+ tokens: DesignTokens;
492
+ validation: ThemeValidationResult;
493
+ } {
494
+ const merged = { ...defaultTokens, ...partialTokens };
495
+ const validation = validateDesignTokens(merged);
496
+
497
+ return {
498
+ tokens: merged,
499
+ validation,
500
+ };
501
+ }
@@ -26,20 +26,49 @@ export function quickTheme(name: string, primaryColor: string, secondaryColor?:
26
26
  * Create a dark theme variant from a light theme
27
27
  */
28
28
  export function createDarkVariant(lightTheme: Theme): Theme {
29
- return extendTheme(lightTheme, {
29
+ // We'll extend the theme by merging the new properties with the existing theme
30
+ const darkVariant: Partial<Theme> = {
30
31
  name: `${lightTheme.name} Dark`,
31
32
  palette: {
32
33
  mode: 'dark',
34
+ primary: lightTheme.palette?.primary, // Preserve original primary
35
+ secondary: lightTheme.palette?.secondary, // Preserve original secondary
36
+ error: lightTheme.palette?.error, // Preserve original error
37
+ warning: lightTheme.palette?.warning, // Preserve original warning
38
+ info: lightTheme.palette?.info, // Preserve original info
39
+ success: lightTheme.palette?.success, // Preserve original success
33
40
  background: {
34
41
  default: '#121212',
42
+ paper: '#1e1e1e', // Added missing paper property
35
43
  subtle: '#1e1e1e',
36
44
  },
37
45
  text: {
38
46
  primary: '#ffffff',
39
47
  secondary: 'rgba(255, 255, 255, 0.7)',
48
+ disabled: 'rgba(255, 255, 255, 0.38)', // Added missing disabled property
40
49
  },
41
50
  },
42
- });
51
+ };
52
+
53
+ // Create a new theme by extending the light theme with the dark variant
54
+ const extendedTheme: Theme = {
55
+ ...lightTheme,
56
+ ...darkVariant,
57
+ palette: {
58
+ ...lightTheme.palette,
59
+ ...darkVariant.palette,
60
+ background: {
61
+ ...lightTheme.palette?.background,
62
+ ...darkVariant.palette?.background,
63
+ },
64
+ text: {
65
+ ...lightTheme.palette?.text,
66
+ ...darkVariant.palette?.text,
67
+ },
68
+ },
69
+ };
70
+
71
+ return extendedTheme;
43
72
  }
44
73
 
45
74
  /**
@@ -121,4 +150,4 @@ export function importTheme(json: string): Theme {
121
150
 
122
151
  // Note: createTheme, extendTheme, mergeTheme, and generateCSSVariables
123
152
  // are already exported from './theme' module. Import them directly from there.
124
- // This file only exports theme-tools specific utilities.
153
+ // This file only exports theme-tools specific utilities.