@shohojdhara/atomix 0.4.7 ā 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/atomix.config.ts +58 -1
- package/dist/atomix.css +172 -157
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +4 -4
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +33 -0
- package/dist/charts.js +1274 -164
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +33 -10
- package/dist/core.js +1099 -83
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +33 -0
- package/dist/forms.js +2106 -1050
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +42 -1
- package/dist/heavy.js +1663 -638
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +442 -270
- package/dist/index.esm.js +1947 -680
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1982 -712
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/package.json +6 -3
- package/scripts/atomix-cli.js +136 -1827
- package/scripts/cli/__tests__/basic.test.js +3 -2
- package/scripts/cli/__tests__/clean.test.js +278 -0
- package/scripts/cli/__tests__/component-validator.test.js +433 -0
- package/scripts/cli/__tests__/generator.test.js +613 -0
- package/scripts/cli/__tests__/glass-motion.test.js +256 -0
- package/scripts/cli/__tests__/integration.test.js +719 -108
- package/scripts/cli/__tests__/migrate.test.js +74 -0
- package/scripts/cli/__tests__/security.test.js +206 -0
- package/scripts/cli/__tests__/test-setup.js +3 -1
- package/scripts/cli/__tests__/theme-bridge.test.js +507 -0
- package/scripts/cli/__tests__/token-provider.test.js +361 -0
- package/scripts/cli/__tests__/utils.test.js +5 -5
- package/scripts/cli/commands/benchmark.js +105 -0
- package/scripts/cli/commands/build-theme.js +115 -0
- package/scripts/cli/commands/clean.js +109 -0
- package/scripts/cli/commands/doctor.js +88 -0
- package/scripts/cli/commands/generate.js +218 -0
- package/scripts/cli/commands/init.js +73 -0
- package/scripts/cli/commands/migrate.js +106 -0
- package/scripts/cli/commands/sync-tokens.js +206 -0
- package/scripts/cli/commands/theme-bridge.js +248 -0
- package/scripts/cli/commands/tokens.js +157 -0
- package/scripts/cli/commands/validate.js +194 -0
- package/scripts/cli/internal/ai-engine.js +156 -0
- package/scripts/cli/internal/compiler.js +114 -0
- package/scripts/cli/internal/component-validator.js +443 -0
- package/scripts/cli/internal/config-loader.js +162 -0
- package/scripts/cli/internal/filesystem.js +158 -0
- package/scripts/cli/internal/generator.js +430 -0
- package/scripts/cli/internal/glass-generator.js +398 -0
- package/scripts/cli/internal/hook-generator.js +369 -0
- package/scripts/cli/internal/hooks.js +61 -0
- package/scripts/cli/internal/itcss-generator.js +565 -0
- package/scripts/cli/internal/motion-generator.js +679 -0
- package/scripts/cli/internal/template-engine.js +301 -0
- package/scripts/cli/internal/theme-bridge.js +664 -0
- package/scripts/cli/internal/tokens/engine.js +122 -0
- package/scripts/cli/internal/tokens/provider.js +34 -0
- package/scripts/cli/internal/tokens/providers/figma.js +50 -0
- package/scripts/cli/internal/tokens/providers/style-dictionary.js +48 -0
- package/scripts/cli/internal/tokens/providers/w3c.js +48 -0
- package/scripts/cli/internal/tokens/token-provider.js +443 -0
- package/scripts/cli/internal/tokens/token-validator.js +513 -0
- package/scripts/cli/internal/validator.js +276 -0
- package/scripts/cli/internal/wizard.js +115 -0
- package/scripts/cli/mappings.js +23 -0
- package/scripts/cli/migration-tools.js +164 -94
- package/scripts/cli/plugins/style-dictionary.js +46 -0
- package/scripts/cli/templates/README.md +525 -95
- package/scripts/cli/templates/common-templates.js +40 -14
- package/scripts/cli/templates/components/react-component.ts +282 -0
- package/scripts/cli/templates/config/project-config.ts +112 -0
- package/scripts/cli/templates/hooks/use-component.ts +477 -0
- package/scripts/cli/templates/index.js +19 -4
- package/scripts/cli/templates/index.ts +171 -0
- package/scripts/cli/templates/next-templates.js +72 -0
- package/scripts/cli/templates/react-templates.js +70 -126
- package/scripts/cli/templates/scss-templates.js +35 -35
- package/scripts/cli/templates/stories/storybook-story.ts +241 -0
- package/scripts/cli/templates/styles/scss-component.ts +255 -0
- package/scripts/cli/templates/tests/vitest-test.ts +229 -0
- package/scripts/cli/templates/token-templates.js +337 -1
- package/scripts/cli/templates/tokens/token-generators.ts +1088 -0
- package/scripts/cli/templates/types/component-types.ts +145 -0
- package/scripts/cli/templates/utils/testing-utils.ts +144 -0
- package/scripts/cli/templates/vanilla-templates.js +39 -0
- package/scripts/cli/token-manager.js +8 -2
- package/scripts/cli/utils/cache-manager.js +240 -0
- package/scripts/cli/utils/detector.js +46 -0
- package/scripts/cli/utils/diagnostics.js +289 -0
- package/scripts/cli/utils/error.js +89 -0
- package/scripts/cli/utils/helpers.js +67 -0
- package/scripts/cli/utils/logger.js +75 -0
- package/scripts/cli/utils/security.js +302 -0
- package/scripts/cli/utils/telemetry.js +115 -0
- package/scripts/cli/utils/validation.js +37 -0
- package/scripts/cli/utils.js +28 -341
- package/src/components/Accordion/Accordion.stories.tsx +0 -18
- package/src/components/Accordion/Accordion.test.tsx +0 -17
- package/src/components/Accordion/Accordion.tsx +0 -4
- package/src/components/AtomixGlass/AtomixGlass.test.tsx +37 -3
- package/src/components/AtomixGlass/AtomixGlass.tsx +143 -31
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +129 -31
- package/src/components/AtomixGlass/PerformanceDashboard.tsx +219 -0
- package/src/components/AtomixGlass/README.md +25 -10
- package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +216 -0
- package/src/components/AtomixGlass/animation-system.ts +578 -0
- package/src/components/AtomixGlass/shader-utils.ts +4 -1
- package/src/components/AtomixGlass/stories/Overview.stories.tsx +157 -6
- package/src/components/AtomixGlass/stories/Phase1-Animation.stories.tsx +653 -0
- package/src/components/AtomixGlass/stories/Phase1-Test.stories.tsx +95 -0
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +51 -51
- package/src/components/AtomixGlass/stories/shared-components.tsx +6 -0
- package/src/components/Avatar/Avatar.tsx +1 -1
- package/src/components/Button/Button.stories.disabled-link.tsx +10 -0
- package/src/components/Button/Button.stories.tsx +10 -0
- package/src/components/Button/Button.test.tsx +16 -11
- package/src/components/Button/Button.tsx +4 -4
- package/src/components/Card/Card.tsx +1 -1
- package/src/components/Dropdown/Dropdown.tsx +12 -12
- package/src/components/Form/Select.tsx +62 -3
- package/src/components/Modal/Modal.tsx +14 -3
- package/src/components/Navigation/Navbar/Navbar.tsx +44 -0
- package/src/components/Slider/Slider.stories.tsx +3 -3
- package/src/components/Slider/Slider.tsx +38 -0
- package/src/components/Steps/Steps.tsx +3 -3
- package/src/components/Tabs/Tabs.tsx +77 -8
- package/src/components/Testimonial/Testimonial.tsx +1 -1
- package/src/components/TypedButton/TypedButton.stories.tsx +59 -0
- package/src/components/TypedButton/TypedButton.tsx +39 -0
- package/src/components/TypedButton/index.ts +2 -0
- package/src/components/VideoPlayer/VideoPlayer.tsx +11 -4
- package/src/lib/composables/index.ts +4 -7
- package/src/lib/composables/types.ts +45 -0
- package/src/lib/composables/useAccordion.ts +0 -7
- package/src/lib/composables/useAtomixGlass.ts +148 -6
- package/src/lib/composables/useAtomixGlassStyles.ts +9 -7
- package/src/lib/composables/useChartExport.ts +3 -13
- package/src/lib/composables/useDropdown.ts +66 -0
- package/src/lib/composables/useFocusTrap.ts +80 -0
- package/src/lib/composables/usePerformanceMonitor.ts +448 -0
- package/src/lib/composables/useResponsiveGlass.presets.ts +192 -0
- package/src/lib/composables/useResponsiveGlass.ts +441 -0
- package/src/lib/composables/useTooltip.ts +16 -0
- package/src/lib/composables/useTypedButton.ts +66 -0
- package/src/lib/config/index.ts +62 -5
- package/src/lib/constants/components.ts +62 -7
- package/src/lib/theme/devtools/__tests__/useHistory.test.tsx +150 -0
- package/src/lib/theme/tokens/centralized-tokens.ts +120 -0
- package/src/lib/theme/utils/__tests__/domUtils.test.ts +101 -0
- package/src/lib/types/components.ts +37 -11
- package/src/lib/types/glass.ts +35 -0
- package/src/lib/types/index.ts +1 -0
- package/src/lib/utils/displacement-generator.ts +1 -1
- package/src/styles/01-settings/_settings.testtypecheck.scss +53 -0
- package/src/styles/01-settings/_settings.typedbutton.scss +53 -0
- package/src/styles/06-components/_components.atomix-glass.scss +17 -21
- package/src/styles/06-components/_components.edge-panel.scss +1 -5
- package/src/styles/06-components/_components.modal.scss +1 -4
- package/src/styles/06-components/_components.navbar.scss +1 -1
- package/src/styles/06-components/_components.testbutton.scss +212 -0
- package/src/styles/06-components/_components.testtypecheck.scss +212 -0
- package/src/styles/06-components/_components.tooltip.scss +9 -5
- package/src/styles/06-components/_components.typedbutton.scss +212 -0
- package/src/styles/99-utilities/_index.scss +1 -0
- package/src/styles/99-utilities/_utilities.text.scss +1 -1
- package/src/styles/99-utilities/_utilities.touch-target.scss +36 -0
- package/scripts/cli/component-generator.js +0 -564
- package/scripts/cli/interactive-init.js +0 -357
- package/src/styles/06-components/old.chart.styles.scss +0 -2788
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Validator System
|
|
3
|
+
* Validates design tokens for consistency, accessibility, and best practices
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from '../../utils/logger.js';
|
|
7
|
+
import { AtomixCLIError } from '../../utils/error.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validation rule severity levels
|
|
11
|
+
*/
|
|
12
|
+
export const SEVERITY = {
|
|
13
|
+
ERROR: 'error',
|
|
14
|
+
WARNING: 'warning',
|
|
15
|
+
INFO: 'info'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Built-in validation rules for design tokens
|
|
20
|
+
*/
|
|
21
|
+
export const VALIDATION_RULES = {
|
|
22
|
+
/**
|
|
23
|
+
* Color contrast validation
|
|
24
|
+
* Ensures text colors have sufficient contrast with backgrounds
|
|
25
|
+
*/
|
|
26
|
+
COLOR_CONTRAST: {
|
|
27
|
+
name: 'color-contrast',
|
|
28
|
+
description: 'Ensures color contrast meets WCAG AA standards',
|
|
29
|
+
severity: SEVERITY.ERROR,
|
|
30
|
+
validate: (tokens) => {
|
|
31
|
+
const issues = [];
|
|
32
|
+
const colorTokens = tokens.color || {};
|
|
33
|
+
|
|
34
|
+
// Check common text/background combinations
|
|
35
|
+
const textColors = ['text', 'foreground', 'primary', 'secondary'];
|
|
36
|
+
const backgroundColors = ['background', 'bg', 'surface', 'card'];
|
|
37
|
+
|
|
38
|
+
for (const textKey of textColors) {
|
|
39
|
+
for (const bgKey of backgroundColors) {
|
|
40
|
+
const textColor = findTokenByPartialKey(colorTokens, textKey);
|
|
41
|
+
const bgColor = findTokenByPartialKey(colorTokens, bgKey);
|
|
42
|
+
|
|
43
|
+
if (textColor && bgColor) {
|
|
44
|
+
const contrast = calculateContrast(textColor.value, bgColor.value);
|
|
45
|
+
if (contrast < 4.5) {
|
|
46
|
+
issues.push({
|
|
47
|
+
rule: 'color-contrast',
|
|
48
|
+
message: `Insufficient contrast ratio (${contrast.toFixed(2)}:1) between ${textKey} and ${bgKey}`,
|
|
49
|
+
suggestion: 'Adjust colors to meet WCAG AA standard (4.5:1 minimum)',
|
|
50
|
+
severity: SEVERITY.ERROR
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return issues;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Semantic naming validation
|
|
63
|
+
* Ensures tokens use semantic names instead of literal values
|
|
64
|
+
*/
|
|
65
|
+
SEMANTIC_NAMING: {
|
|
66
|
+
name: 'semantic-naming',
|
|
67
|
+
description: 'Ensures tokens use semantic names',
|
|
68
|
+
severity: SEVERITY.WARNING,
|
|
69
|
+
validate: (tokens) => {
|
|
70
|
+
const issues = [];
|
|
71
|
+
|
|
72
|
+
// Check for non-semantic color names
|
|
73
|
+
const literalColorNames = ['blue', 'red', 'green', 'yellow', 'orange', 'purple', 'pink'];
|
|
74
|
+
const semanticPrefixes = ['brand', 'primary', 'secondary', 'accent', 'success', 'warning', 'error', 'info'];
|
|
75
|
+
const colorTokens = tokens.color || {};
|
|
76
|
+
|
|
77
|
+
for (const [tokenName, tokenData] of Object.entries(colorTokens)) {
|
|
78
|
+
const nameLower = tokenName.toLowerCase();
|
|
79
|
+
|
|
80
|
+
// Skip if token has semantic prefix
|
|
81
|
+
const hasSemanticPrefix = semanticPrefixes.some(prefix => nameLower.startsWith(prefix));
|
|
82
|
+
if (hasSemanticPrefix) continue;
|
|
83
|
+
|
|
84
|
+
for (const literalName of literalColorNames) {
|
|
85
|
+
// Check if literal color name appears but not as part of a semantic name
|
|
86
|
+
if (nameLower.includes(literalName) && !nameLower.includes('brand')) {
|
|
87
|
+
issues.push({
|
|
88
|
+
rule: 'semantic-naming',
|
|
89
|
+
message: `Token "color.${tokenName}" uses literal color name`,
|
|
90
|
+
suggestion: `Consider using semantic names like "primary", "accent", "success" instead of "${literalName}"`,
|
|
91
|
+
severity: SEVERITY.WARNING
|
|
92
|
+
});
|
|
93
|
+
break; // Only report once per token
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return issues;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Spacing scale consistency
|
|
104
|
+
* Ensures spacing tokens follow a consistent scale
|
|
105
|
+
*/
|
|
106
|
+
SPACING_SCALE: {
|
|
107
|
+
name: 'spacing-scale',
|
|
108
|
+
description: 'Ensures spacing tokens follow a consistent scale',
|
|
109
|
+
severity: SEVERITY.INFO,
|
|
110
|
+
validate: (tokens) => {
|
|
111
|
+
const issues = [];
|
|
112
|
+
const spacingTokens = tokens.spacing || {};
|
|
113
|
+
|
|
114
|
+
const values = Object.values(spacingTokens).map(t =>
|
|
115
|
+
typeof t === 'object' ? t.value : t
|
|
116
|
+
).filter(v => typeof v === 'string');
|
|
117
|
+
|
|
118
|
+
// Parse spacing values
|
|
119
|
+
const numericValues = values.map(v => {
|
|
120
|
+
const match = v.match(/^([\d.]+)(px|rem|em)$/);
|
|
121
|
+
return match ? parseFloat(match[1]) : null;
|
|
122
|
+
}).filter(v => v !== null);
|
|
123
|
+
|
|
124
|
+
if (numericValues.length > 0) {
|
|
125
|
+
// Check for consistent increments (e.g., multiples of 4 or 8)
|
|
126
|
+
const hasConsistentScale = numericValues.every(v => v % 4 === 0 || v % 0.25 === 0);
|
|
127
|
+
|
|
128
|
+
if (!hasConsistentScale) {
|
|
129
|
+
issues.push({
|
|
130
|
+
rule: 'spacing-scale',
|
|
131
|
+
message: 'Spacing values do not follow a consistent scale',
|
|
132
|
+
suggestion: 'Use a consistent scale (e.g., 4px or 8px base unit)',
|
|
133
|
+
severity: SEVERITY.INFO
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return issues;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* No hardcoded colors in components
|
|
144
|
+
* Ensures components reference tokens instead of hardcoded values
|
|
145
|
+
*/
|
|
146
|
+
NO_HARDCODED_COLORS: {
|
|
147
|
+
name: 'no-hardcoded-colors',
|
|
148
|
+
description: 'Prevents hardcoded colors in component code',
|
|
149
|
+
severity: SEVERITY.ERROR,
|
|
150
|
+
validate: (tokens, context = {}) => {
|
|
151
|
+
const issues = [];
|
|
152
|
+
const { codeContent = '' } = context;
|
|
153
|
+
|
|
154
|
+
// Check for hex colors
|
|
155
|
+
const hexColorRegex = /#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})\b/g;
|
|
156
|
+
const hexMatches = codeContent.match(hexColorRegex) || [];
|
|
157
|
+
|
|
158
|
+
// Check for RGB/RGBA colors
|
|
159
|
+
const rgbRegex = /rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[\d.]+\s*)?\)/gi;
|
|
160
|
+
const rgbMatches = codeContent.match(rgbRegex) || [];
|
|
161
|
+
|
|
162
|
+
// Check for HSL/HSLA colors
|
|
163
|
+
const hslRegex = /hsla?\(\s*\d+\s*,\s*[\d.]+%?\s*,\s*[\d.]+%?\s*(,\s*[\d.]+\s*)?\)/gi;
|
|
164
|
+
const hslMatches = codeContent.match(hslRegex) || [];
|
|
165
|
+
|
|
166
|
+
const allMatches = [...hexMatches, ...rgbMatches, ...hslMatches];
|
|
167
|
+
|
|
168
|
+
if (allMatches.length > 0) {
|
|
169
|
+
issues.push({
|
|
170
|
+
rule: 'no-hardcoded-colors',
|
|
171
|
+
message: `Found ${allMatches.length} hardcoded color value(s): ${allMatches.slice(0, 5).join(', ')}`,
|
|
172
|
+
suggestion: 'Replace hardcoded colors with design tokens (e.g., var(--color-primary))',
|
|
173
|
+
severity: SEVERITY.ERROR,
|
|
174
|
+
matches: allMatches
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return issues;
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Token completeness validation
|
|
184
|
+
* Ensures required token categories exist
|
|
185
|
+
*/
|
|
186
|
+
TOKEN_COMPLETENESS: {
|
|
187
|
+
name: 'token-completeness',
|
|
188
|
+
description: 'Ensures all required token categories are present',
|
|
189
|
+
severity: SEVERITY.WARNING,
|
|
190
|
+
validate: (tokens) => {
|
|
191
|
+
const issues = [];
|
|
192
|
+
const requiredCategories = ['color', 'spacing', 'typography'];
|
|
193
|
+
const optionalCategories = ['shadow', 'radius', 'animation', 'breakpoint'];
|
|
194
|
+
|
|
195
|
+
for (const category of requiredCategories) {
|
|
196
|
+
if (!tokens[category] || Object.keys(tokens[category]).length === 0) {
|
|
197
|
+
issues.push({
|
|
198
|
+
rule: 'token-completeness',
|
|
199
|
+
message: `Missing required token category: "${category}"`,
|
|
200
|
+
suggestion: `Add ${category} tokens to ensure design consistency`,
|
|
201
|
+
severity: SEVERITY.WARNING
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check for optional but recommended categories
|
|
207
|
+
for (const category of optionalCategories) {
|
|
208
|
+
if (!tokens[category]) {
|
|
209
|
+
issues.push({
|
|
210
|
+
rule: 'token-completeness',
|
|
211
|
+
message: `Missing optional token category: "${category}"`,
|
|
212
|
+
suggestion: `Consider adding ${category} tokens for enhanced theming`,
|
|
213
|
+
severity: SEVERITY.INFO
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return issues;
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Duplicate token detection
|
|
224
|
+
* Identifies tokens with identical values that might be redundant
|
|
225
|
+
*/
|
|
226
|
+
DUPLICATE_DETECTION: {
|
|
227
|
+
name: 'duplicate-detection',
|
|
228
|
+
description: 'Detects potentially redundant duplicate tokens',
|
|
229
|
+
severity: SEVERITY.INFO,
|
|
230
|
+
validate: (tokens) => {
|
|
231
|
+
const issues = [];
|
|
232
|
+
const valueMap = new Map();
|
|
233
|
+
|
|
234
|
+
// Flatten all tokens
|
|
235
|
+
for (const [category, categoryTokens] of Object.entries(tokens)) {
|
|
236
|
+
for (const [tokenName, tokenData] of Object.entries(categoryTokens)) {
|
|
237
|
+
const value = typeof tokenData === 'object' ? tokenData.value : tokenData;
|
|
238
|
+
const key = `${value}`;
|
|
239
|
+
|
|
240
|
+
if (!valueMap.has(key)) {
|
|
241
|
+
valueMap.set(key, []);
|
|
242
|
+
}
|
|
243
|
+
valueMap.get(key).push(`${category}.${tokenName}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Find duplicates
|
|
248
|
+
for (const [value, tokenNames] of valueMap.entries()) {
|
|
249
|
+
if (tokenNames.length > 1) {
|
|
250
|
+
issues.push({
|
|
251
|
+
rule: 'duplicate-detection',
|
|
252
|
+
message: `Multiple tokens share the same value "${value}": ${tokenNames.join(', ')}`,
|
|
253
|
+
suggestion: 'Consider consolidating duplicate tokens or documenting their distinct purposes',
|
|
254
|
+
severity: SEVERITY.INFO
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return issues;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Typography scale validation
|
|
265
|
+
* Ensures typography follows a harmonious scale
|
|
266
|
+
*/
|
|
267
|
+
TYPOGRAPHY_SCALE: {
|
|
268
|
+
name: 'typography-scale',
|
|
269
|
+
description: 'Ensures typography follows a harmonious scale',
|
|
270
|
+
severity: SEVERITY.INFO,
|
|
271
|
+
validate: (tokens) => {
|
|
272
|
+
const issues = [];
|
|
273
|
+
const typeTokens = tokens.typography || {};
|
|
274
|
+
|
|
275
|
+
const fontSizes = Object.entries(typeTokens)
|
|
276
|
+
.filter(([key]) => key.includes('size') || key.includes('heading'))
|
|
277
|
+
.map(([, data]) => {
|
|
278
|
+
const value = typeof data === 'object' ? data.value : data;
|
|
279
|
+
const match = String(value).match(/^([\d.]+)(px|rem|em)$/);
|
|
280
|
+
return match ? parseFloat(match[1]) : null;
|
|
281
|
+
})
|
|
282
|
+
.filter(v => v !== null);
|
|
283
|
+
|
|
284
|
+
if (fontSizes.length >= 2) {
|
|
285
|
+
// Check for reasonable scale ratios (between 1.2 and 1.5 is ideal)
|
|
286
|
+
fontSizes.sort((a, b) => a - b);
|
|
287
|
+
const ratios = [];
|
|
288
|
+
|
|
289
|
+
for (let i = 1; i < fontSizes.length; i++) {
|
|
290
|
+
ratios.push(fontSizes[i] / fontSizes[i - 1]);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const avgRatio = ratios.reduce((a, b) => a + b, 0) / ratios.length;
|
|
294
|
+
|
|
295
|
+
if (avgRatio < 1.1 || avgRatio > 1.6) {
|
|
296
|
+
issues.push({
|
|
297
|
+
rule: 'typography-scale',
|
|
298
|
+
message: `Typography scale ratio (${avgRatio.toFixed(2)}) may create visual inconsistency`,
|
|
299
|
+
suggestion: 'Consider using a modular scale (1.2-1.5 ratio) for harmonious typography',
|
|
300
|
+
severity: SEVERITY.INFO
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return issues;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Helper function to find token by partial key match
|
|
312
|
+
* @private
|
|
313
|
+
*/
|
|
314
|
+
function findTokenByPartialKey(tokens, partialKey) {
|
|
315
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
316
|
+
if (key.toLowerCase().includes(partialKey.toLowerCase())) {
|
|
317
|
+
return typeof value === 'object' ? value : { value };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Calculate contrast ratio between two colors
|
|
325
|
+
* Uses WCAG 2.0 formula
|
|
326
|
+
* @private
|
|
327
|
+
*/
|
|
328
|
+
function calculateContrast(color1, color2) {
|
|
329
|
+
// Simple implementation - in production, use a proper color library
|
|
330
|
+
const lum1 = getLuminance(color1);
|
|
331
|
+
const lum2 = getLuminance(color2);
|
|
332
|
+
|
|
333
|
+
const lighter = Math.max(lum1, lum2);
|
|
334
|
+
const darker = Math.min(lum1, lum2);
|
|
335
|
+
|
|
336
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get relative luminance of a color
|
|
341
|
+
* @private
|
|
342
|
+
*/
|
|
343
|
+
function getLuminance(color) {
|
|
344
|
+
// Simplified - assumes hex color input
|
|
345
|
+
let hex = color.replace('#', '');
|
|
346
|
+
|
|
347
|
+
if (hex.length === 3) {
|
|
348
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const r = parseInt(hex.substr(0, 2), 16) / 255;
|
|
352
|
+
const g = parseInt(hex.substr(2, 2), 16) / 255;
|
|
353
|
+
const b = parseInt(hex.substr(4, 2), 16) / 255;
|
|
354
|
+
|
|
355
|
+
const a = [r, g, b].map(v => {
|
|
356
|
+
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* TokenValidator class
|
|
364
|
+
*/
|
|
365
|
+
export class TokenValidator {
|
|
366
|
+
constructor(options = {}) {
|
|
367
|
+
this.rules = new Map();
|
|
368
|
+
this.enabledRules = options.enabledRules || Object.keys(VALIDATION_RULES);
|
|
369
|
+
|
|
370
|
+
// Register built-in rules
|
|
371
|
+
for (const [key, rule] of Object.entries(VALIDATION_RULES)) {
|
|
372
|
+
this.registerRule(key, rule);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Register a custom validation rule
|
|
378
|
+
* @param {string} name - Rule name
|
|
379
|
+
* @param {Object} rule - Rule definition
|
|
380
|
+
*/
|
|
381
|
+
registerRule(name, rule) {
|
|
382
|
+
if (!rule.name || !rule.validate || !rule.severity) {
|
|
383
|
+
throw new Error(`Invalid rule "${name}": must have name, validate, and severity`);
|
|
384
|
+
}
|
|
385
|
+
this.rules.set(name, rule);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Enable or disable a rule
|
|
390
|
+
* @param {string} ruleName - Rule to toggle
|
|
391
|
+
* @param {boolean} enabled - Whether to enable
|
|
392
|
+
*/
|
|
393
|
+
toggleRule(ruleName, enabled) {
|
|
394
|
+
if (enabled) {
|
|
395
|
+
if (!this.enabledRules.includes(ruleName)) {
|
|
396
|
+
this.enabledRules.push(ruleName);
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
this.enabledRules = this.enabledRules.filter(r => r !== ruleName);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Validate tokens against all enabled rules
|
|
405
|
+
* @param {Object} tokens - Tokens to validate
|
|
406
|
+
* @param {Object} context - Additional context for validation
|
|
407
|
+
* @returns {Object} Validation results
|
|
408
|
+
*/
|
|
409
|
+
validate(tokens, context = {}) {
|
|
410
|
+
const results = {
|
|
411
|
+
valid: true,
|
|
412
|
+
issues: [],
|
|
413
|
+
summary: {
|
|
414
|
+
errors: 0,
|
|
415
|
+
warnings: 0,
|
|
416
|
+
info: 0
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
for (const ruleName of this.enabledRules) {
|
|
421
|
+
const rule = this.rules.get(ruleName);
|
|
422
|
+
|
|
423
|
+
if (!rule) {
|
|
424
|
+
logger.debug(`Rule "${ruleName}" not found, skipping`);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const issues = rule.validate(tokens, context);
|
|
430
|
+
|
|
431
|
+
if (issues.length > 0) {
|
|
432
|
+
results.valid = false;
|
|
433
|
+
results.issues.push(...issues);
|
|
434
|
+
|
|
435
|
+
// Update summary
|
|
436
|
+
for (const issue of issues) {
|
|
437
|
+
if (issue.severity === SEVERITY.ERROR) {
|
|
438
|
+
results.summary.errors++;
|
|
439
|
+
} else if (issue.severity === SEVERITY.WARNING) {
|
|
440
|
+
results.summary.warnings++;
|
|
441
|
+
} else {
|
|
442
|
+
results.summary.info++;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
} catch (error) {
|
|
447
|
+
logger.warn(`Rule "${ruleName}" failed: ${error.message}`);
|
|
448
|
+
results.issues.push({
|
|
449
|
+
rule: ruleName,
|
|
450
|
+
message: `Validation rule error: ${error.message}`,
|
|
451
|
+
severity: SEVERITY.WARNING
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return results;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Validate component code against token rules
|
|
461
|
+
* @param {string} codeContent - Component code
|
|
462
|
+
* @param {Object} tokens - Design tokens
|
|
463
|
+
* @returns {Object} Validation results
|
|
464
|
+
*/
|
|
465
|
+
validateComponent(codeContent, tokens = {}) {
|
|
466
|
+
return this.validate(tokens, { codeContent });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Get detailed report of validation results
|
|
471
|
+
* @param {Object} results - Validation results
|
|
472
|
+
* @returns {string} Formatted report
|
|
473
|
+
*/
|
|
474
|
+
getReport(results) {
|
|
475
|
+
const lines = [
|
|
476
|
+
'\nš Token Validation Report',
|
|
477
|
+
'='.repeat(50),
|
|
478
|
+
`Status: ${results.valid ? 'ā
PASSED' : 'ā FAILED'}`,
|
|
479
|
+
'',
|
|
480
|
+
'Summary:',
|
|
481
|
+
` Errors: ${results.summary.errors}`,
|
|
482
|
+
` Warnings: ${results.summary.warnings}`,
|
|
483
|
+
` Info: ${results.summary.info}`,
|
|
484
|
+
''
|
|
485
|
+
];
|
|
486
|
+
|
|
487
|
+
if (results.issues.length > 0) {
|
|
488
|
+
lines.push('Issues:');
|
|
489
|
+
|
|
490
|
+
for (const issue of results.issues) {
|
|
491
|
+
const icon = issue.severity === SEVERITY.ERROR ? 'ā' :
|
|
492
|
+
issue.severity === SEVERITY.WARNING ? 'ā ļø' : 'ā¹ļø';
|
|
493
|
+
|
|
494
|
+
lines.push(` ${icon} [${issue.rule}] ${issue.message}`);
|
|
495
|
+
|
|
496
|
+
if (issue.suggestion) {
|
|
497
|
+
lines.push(` š” ${issue.suggestion}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
lines.push('='.repeat(50));
|
|
503
|
+
|
|
504
|
+
return lines.join('\n');
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Create a singleton validator instance
|
|
510
|
+
*/
|
|
511
|
+
export const tokenValidator = new TokenValidator();
|
|
512
|
+
|
|
513
|
+
export default tokenValidator;
|