@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.
- package/CHANGELOG.md +19 -0
- package/README.md +2 -0
- package/dist/atomix.css +95 -77
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +2 -2
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +1 -1
- package/dist/charts.js +1 -1
- package/dist/core.d.ts +41 -11
- package/dist/core.js +39 -23
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +28 -11
- package/dist/forms.js +8 -5
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +1 -1
- package/dist/heavy.js +15 -6
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +122 -46
- package/dist/index.esm.js +849 -182
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +854 -186
- 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 +27 -2
- package/dist/theme.js +721 -108
- package/dist/theme.js.map +1 -1
- package/package.json +1 -1
- package/scripts/atomix-cli.js +610 -1111
- package/scripts/cli/component-generator.js +610 -0
- package/scripts/cli/documentation-sync.js +542 -0
- package/scripts/cli/interactive-init.js +84 -288
- package/scripts/cli/mappings.js +211 -0
- package/scripts/cli/migration-tools.js +95 -288
- package/scripts/cli/template-manager.js +107 -0
- package/scripts/cli/templates/README.md +123 -0
- package/scripts/cli/templates/composable-templates.js +149 -0
- package/scripts/cli/templates/config-templates.js +126 -0
- package/scripts/cli/templates/index.js +95 -0
- package/scripts/cli/templates/project-templates.js +214 -0
- package/scripts/cli/templates/react-templates.js +261 -0
- package/scripts/cli/templates/scss-templates.js +156 -0
- package/scripts/cli/templates/storybook-templates.js +236 -0
- package/scripts/cli/templates/testing-templates.js +45 -0
- package/scripts/cli/templates/token-templates.js +447 -0
- package/scripts/cli/templates/types-templates.js +133 -0
- package/scripts/cli/templates-original-backup.js +1655 -0
- package/scripts/cli/templates.js +35 -0
- package/scripts/cli/templates_backup.js +684 -0
- package/scripts/cli/theme-bridge.js +20 -14
- package/scripts/cli/token-manager.js +150 -77
- package/scripts/cli/utils.js +37 -25
- package/src/components/Accordion/Accordion.stories.tsx +5 -5
- package/src/components/Accordion/Accordion.test.tsx +57 -0
- package/src/components/Accordion/Accordion.tsx +4 -0
- package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +1 -1
- package/src/components/AtomixGlass/stories/Examples.stories.tsx +37 -37
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +50 -51
- package/src/components/Avatar/Avatar.stories.tsx +26 -26
- package/src/components/Badge/Badge.stories.tsx +31 -31
- package/src/components/Badge/Badge.test.tsx +51 -0
- package/src/components/Badge/Badge.tsx +20 -1
- package/src/components/Block/Block.stories.tsx +5 -5
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +1 -1
- package/src/components/Breadcrumb/Breadcrumb.tsx +2 -2
- package/src/components/Button/Button.stories.tsx +13 -13
- package/src/components/Button/Button.tsx +4 -4
- package/src/components/Button/ButtonGroup.stories.tsx +2 -2
- package/src/components/Button/README.md +5 -0
- package/src/components/Callout/Callout.stories.tsx +11 -11
- package/src/components/Callout/Callout.test.tsx +10 -10
- package/src/components/Callout/Callout.tsx +7 -7
- package/src/components/Callout/README.md +9 -8
- package/src/components/Card/Card.tsx +2 -2
- package/src/components/Chart/Chart.stories.tsx +6 -6
- package/src/components/Chart/Chart.tsx +1 -1
- package/src/components/ColorModeToggle/ColorModeToggle.stories.tsx +1 -1
- package/src/components/DataTable/DataTable.tsx +14 -12
- package/src/components/DatePicker/DatePicker.stories.tsx +6 -6
- package/src/components/Dropdown/Dropdown.stories.tsx +4 -4
- package/src/components/Form/Checkbox.stories.tsx +3 -3
- package/src/components/Form/Checkbox.tsx +4 -2
- package/src/components/Form/Form.stories.tsx +3 -3
- package/src/components/Form/FormGroup.stories.tsx +1 -1
- package/src/components/Form/Input.stories.tsx +28 -16
- package/src/components/Form/Input.test.tsx +59 -0
- package/src/components/Form/Input.tsx +97 -95
- package/src/components/Form/Radio.stories.tsx +94 -94
- package/src/components/Form/Radio.tsx +2 -2
- package/src/components/Form/Select.stories.tsx +4 -4
- package/src/components/Form/Select.tsx +2 -2
- package/src/components/Form/Textarea.stories.tsx +22 -7
- package/src/components/Form/Textarea.test.tsx +45 -0
- package/src/components/Form/Textarea.tsx +88 -86
- package/src/components/List/List.stories.tsx +2 -2
- package/src/components/Modal/Modal.stories.tsx +4 -4
- package/src/components/Navigation/Navbar/Navbar.stories.tsx +5 -5
- package/src/components/Navigation/Navbar/Navbar.tsx +1 -1
- package/src/components/Navigation/README.md +1 -1
- package/src/components/Pagination/Pagination.stories.tsx +5 -2
- package/src/components/Pagination/Pagination.tsx +1 -1
- package/src/components/PhotoViewer/PhotoViewer.stories.tsx +10 -10
- package/src/components/Popover/Popover.stories.tsx +1 -1
- package/src/components/ProductReview/ProductReview.tsx +1 -1
- package/src/components/Progress/Progress.tsx +46 -46
- package/src/components/Rating/Rating.stories.tsx +4 -4
- package/src/components/Rating/Rating.tsx +8 -8
- package/src/components/Slider/Slider.stories.tsx +63 -63
- package/src/components/Spinner/Spinner.stories.tsx +2 -2
- package/src/components/Spinner/Spinner.test.tsx +35 -0
- package/src/components/Spinner/Spinner.tsx +9 -2
- package/src/components/Testimonial/Testimonial.stories.tsx +1 -1
- package/src/components/Toggle/Toggle.stories.tsx +32 -9
- package/src/components/Toggle/Toggle.test.tsx +91 -0
- package/src/components/Toggle/Toggle.tsx +44 -27
- package/src/components/Tooltip/Tooltip.tsx +1 -1
- package/src/layouts/Grid/Grid.stories.tsx +49 -49
- package/src/layouts/MasonryGrid/MasonryGrid.stories.tsx +2 -2
- package/src/lib/composables/useAccordion.ts +12 -3
- package/src/lib/composables/useBreadcrumb.ts +2 -2
- package/src/lib/composables/useCallout.ts +7 -7
- package/src/lib/composables/useNavbar.ts +1 -1
- package/src/lib/constants/components.ts +1 -1
- package/src/lib/storybook/InteractiveDemo.tsx +113 -0
- package/src/lib/storybook/PreviewContainer.tsx +36 -0
- package/src/lib/storybook/VariantsGrid.tsx +21 -0
- package/src/lib/storybook/index.ts +3 -0
- package/src/lib/theme/core/createThemeObject.ts +9 -5
- package/src/lib/theme/devtools/CLI.ts +155 -0
- package/src/lib/theme/devtools/DesignTokensCustomizer.stories.tsx +213 -0
- package/src/lib/theme/devtools/DesignTokensCustomizer.tsx +566 -0
- package/src/lib/theme/devtools/LiveEditor.tsx +2 -1
- package/src/lib/theme/devtools/index.ts +3 -0
- package/src/lib/theme/errors/errors.ts +8 -0
- package/src/lib/theme/runtime/ThemeProvider.tsx +117 -57
- package/src/lib/theme/runtime/__tests__/ThemeProvider.integration.test.tsx +305 -0
- package/src/lib/theme/runtime/__tests__/ThemeProvider.test.tsx +588 -0
- package/src/lib/theme/utils/__tests__/themeValidation.test.ts +264 -0
- package/src/lib/theme/utils/index.ts +1 -0
- package/src/lib/theme/utils/themeValidation.ts +501 -0
- package/src/lib/theme-tools.ts +32 -3
- package/src/lib/types/components.ts +81 -26
- package/src/lib/utils/themeNaming.ts +1 -1
- package/src/styles/06-components/_components.callout.scss +29 -33
- package/src/styles/06-components/_index.scss +1 -1
- package/src/styles/99-utilities/_utilities.display.scss +14 -3
- package/src/styles/99-utilities/_utilities.flex.scss +10 -10
- package/src/styles/99-utilities/_utilities.text.scss +28 -8
- package/scripts/cli/__tests__/cli-commands.test.js +0 -204
- package/scripts/cli/__tests__/utils.test.js +0 -201
- 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
|
+
}
|
package/src/lib/theme-tools.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|