@ngcorex/css 0.1.6 → 0.1.7
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/dist/constraints/constraint-error.d.ts.map +1 -1
- package/dist/constraints/constraint-error.js +2 -2
- package/dist/engine/build-css.js +2 -2
- package/dist/errors/ngcorex-error.d.ts +68 -1
- package/dist/errors/ngcorex-error.d.ts.map +1 -1
- package/dist/errors/ngcorex-error.js +135 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/tokens/normalize-colors.js +2 -2
- package/dist/tokens/normalize.d.ts.map +1 -1
- package/dist/tokens/normalize.js +2 -2
- package/dist/validation/color-format.d.ts +22 -0
- package/dist/validation/color-format.d.ts.map +1 -0
- package/dist/validation/color-format.js +249 -0
- package/dist/validation/index.d.ts +42 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +206 -0
- package/dist/validation/scale-consistency.d.ts +14 -0
- package/dist/validation/scale-consistency.d.ts.map +1 -0
- package/dist/validation/scale-consistency.js +214 -0
- package/dist/validation/shadow-format.d.ts +14 -0
- package/dist/validation/shadow-format.d.ts.map +1 -0
- package/dist/validation/shadow-format.js +217 -0
- package/dist/validation/spacing-format.d.ts +14 -0
- package/dist/validation/spacing-format.d.ts.map +1 -0
- package/dist/validation/spacing-format.js +173 -0
- package/dist/validation/token-duplicates.d.ts +22 -0
- package/dist/validation/token-duplicates.d.ts.map +1 -0
- package/dist/validation/token-duplicates.js +126 -0
- package/dist/validation/types.d.ts +160 -0
- package/dist/validation/types.d.ts.map +1 -0
- package/dist/validation/types.js +4 -0
- package/dist/validation/z-index-logic.d.ts +14 -0
- package/dist/validation/z-index-logic.d.ts.map +1 -0
- package/dist/validation/z-index-logic.js +230 -0
- package/package.json +2 -2
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main validation entry point
|
|
3
|
+
* Coordinates all non-blocking token validations
|
|
4
|
+
*/
|
|
5
|
+
import { validateDuplicateTokens } from './token-duplicates.js';
|
|
6
|
+
import { validateScaleConsistency } from './scale-consistency.js';
|
|
7
|
+
import { validateColorFormat } from './color-format.js';
|
|
8
|
+
import { validateSpacingFormat } from './spacing-format.js';
|
|
9
|
+
import { validateShadowFormat } from './shadow-format.js';
|
|
10
|
+
import { validateZIndexLogic } from './z-index-logic.js';
|
|
11
|
+
/**
|
|
12
|
+
* Default validation configuration
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_VALIDATION_CONFIG = {
|
|
15
|
+
enabled: {
|
|
16
|
+
duplicateTokens: true,
|
|
17
|
+
scaleConsistency: true,
|
|
18
|
+
colorFormat: true,
|
|
19
|
+
spacingFormat: true,
|
|
20
|
+
shadowFormat: true,
|
|
21
|
+
zIndexLogic: true
|
|
22
|
+
},
|
|
23
|
+
severity: 'warning'
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Run all enabled validations on tokens
|
|
27
|
+
*/
|
|
28
|
+
export function runValidations(tokens, config = DEFAULT_VALIDATION_CONFIG) {
|
|
29
|
+
const results = [];
|
|
30
|
+
// Run duplicate token validation
|
|
31
|
+
if (config.enabled.duplicateTokens) {
|
|
32
|
+
const duplicateResults = validateDuplicateTokens(tokens, 'tokens.json', config.severity);
|
|
33
|
+
results.push(...duplicateResults);
|
|
34
|
+
}
|
|
35
|
+
// Run scale consistency validation
|
|
36
|
+
if (config.enabled.scaleConsistency) {
|
|
37
|
+
const scaleResults = validateScaleConsistency(tokens, config.severity);
|
|
38
|
+
results.push(...scaleResults);
|
|
39
|
+
}
|
|
40
|
+
// Run color format validation
|
|
41
|
+
if (config.enabled.colorFormat) {
|
|
42
|
+
const colorResults = validateColorFormat(tokens, config.severity);
|
|
43
|
+
results.push(...colorResults);
|
|
44
|
+
}
|
|
45
|
+
// Run spacing format validation
|
|
46
|
+
if (config.enabled.spacingFormat) {
|
|
47
|
+
const spacingResults = validateSpacingFormat(tokens, config.severity);
|
|
48
|
+
results.push(...spacingResults);
|
|
49
|
+
}
|
|
50
|
+
// Run shadow format validation
|
|
51
|
+
if (config.enabled.shadowFormat) {
|
|
52
|
+
const shadowResults = validateShadowFormat(tokens, config.severity);
|
|
53
|
+
results.push(...shadowResults);
|
|
54
|
+
}
|
|
55
|
+
// Run z-index logic validation
|
|
56
|
+
if (config.enabled.zIndexLogic) {
|
|
57
|
+
const zIndexResults = validateZIndexLogic(tokens, config.severity);
|
|
58
|
+
results.push(...zIndexResults);
|
|
59
|
+
}
|
|
60
|
+
// Run custom validators
|
|
61
|
+
if (config.customValidators) {
|
|
62
|
+
for (const validator of config.customValidators) {
|
|
63
|
+
const customResults = validator.validate(tokens);
|
|
64
|
+
results.push(...customResults);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Count results by severity
|
|
68
|
+
const summary = {
|
|
69
|
+
info: results.filter(r => r.severity === 'info').length,
|
|
70
|
+
warning: results.filter(r => r.severity === 'warning').length,
|
|
71
|
+
error: results.filter(r => r.severity === 'error').length
|
|
72
|
+
};
|
|
73
|
+
// Report is valid if there are no errors (warnings and info are non-blocking)
|
|
74
|
+
const valid = summary.error === 0;
|
|
75
|
+
return {
|
|
76
|
+
valid,
|
|
77
|
+
results,
|
|
78
|
+
summary
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Run validations and return only warnings and errors (filter out info)
|
|
83
|
+
*/
|
|
84
|
+
export function runValidationsWithoutInfo(tokens, config = DEFAULT_VALIDATION_CONFIG) {
|
|
85
|
+
const report = runValidations(tokens, config);
|
|
86
|
+
const filteredResults = report.results.filter(r => r.severity !== 'info');
|
|
87
|
+
return {
|
|
88
|
+
valid: report.valid,
|
|
89
|
+
results: filteredResults,
|
|
90
|
+
summary: {
|
|
91
|
+
info: 0,
|
|
92
|
+
warning: filteredResults.filter(r => r.severity === 'warning').length,
|
|
93
|
+
error: filteredResults.filter(r => r.severity === 'error').length
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get a human-readable summary of the validation report
|
|
99
|
+
*/
|
|
100
|
+
export function getValidationSummary(report) {
|
|
101
|
+
const lines = [];
|
|
102
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
103
|
+
lines.push(' 🔍 Token Validation Report');
|
|
104
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
105
|
+
lines.push('');
|
|
106
|
+
if (report.valid) {
|
|
107
|
+
lines.push(' ✅ No blocking errors found');
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
lines.push(` ❌ ${report.summary.error} error${report.summary.error > 1 ? 's' : ''} found`);
|
|
111
|
+
}
|
|
112
|
+
if (report.summary.warning > 0) {
|
|
113
|
+
lines.push(` ⚠️ ${report.summary.warning} warning${report.summary.warning > 1 ? 's' : ''}`);
|
|
114
|
+
}
|
|
115
|
+
if (report.summary.info > 0) {
|
|
116
|
+
lines.push(` ℹ️ ${report.summary.info} info message${report.summary.info > 1 ? 's' : ''}`);
|
|
117
|
+
}
|
|
118
|
+
lines.push('');
|
|
119
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
120
|
+
lines.push('');
|
|
121
|
+
return lines.join('\n');
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Print validation results grouped by severity
|
|
125
|
+
*/
|
|
126
|
+
export function printValidationResults(report) {
|
|
127
|
+
if (report.results.length === 0) {
|
|
128
|
+
console.log('✅ No validation issues found');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Print errors first
|
|
132
|
+
const errors = report.results.filter(r => r.severity === 'error');
|
|
133
|
+
if (errors.length > 0) {
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
136
|
+
console.log(` ❌ ${errors.length} Error${errors.length > 1 ? 's' : ''}`);
|
|
137
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
138
|
+
console.log('');
|
|
139
|
+
for (const result of errors) {
|
|
140
|
+
console.log(` ❌ ${result.message}`);
|
|
141
|
+
console.log(` Path: ${result.path}`);
|
|
142
|
+
if (result.suggestion) {
|
|
143
|
+
console.log(` 💡 ${result.suggestion}`);
|
|
144
|
+
}
|
|
145
|
+
console.log('');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Print warnings
|
|
149
|
+
const warnings = report.results.filter(r => r.severity === 'warning');
|
|
150
|
+
if (warnings.length > 0) {
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
153
|
+
console.log(` ⚠️ ${warnings.length} Warning${warnings.length > 1 ? 's' : ''}`);
|
|
154
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
155
|
+
console.log('');
|
|
156
|
+
for (const result of warnings) {
|
|
157
|
+
console.log(` ⚠️ ${result.message}`);
|
|
158
|
+
console.log(` Path: ${result.path}`);
|
|
159
|
+
if (result.suggestion) {
|
|
160
|
+
console.log(` 💡 ${result.suggestion}`);
|
|
161
|
+
}
|
|
162
|
+
console.log('');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Print info messages
|
|
166
|
+
const info = report.results.filter(r => r.severity === 'info');
|
|
167
|
+
if (info.length > 0) {
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
170
|
+
console.log(` ℹ️ ${info.length} Info Message${info.length > 1 ? 's' : ''}`);
|
|
171
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
172
|
+
console.log('');
|
|
173
|
+
for (const result of info) {
|
|
174
|
+
console.log(` ℹ️ ${result.message}`);
|
|
175
|
+
console.log(` Path: ${result.path}`);
|
|
176
|
+
if (result.suggestion) {
|
|
177
|
+
console.log(` 💡 ${result.suggestion}`);
|
|
178
|
+
}
|
|
179
|
+
console.log('');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Check if validation report has any results
|
|
185
|
+
*/
|
|
186
|
+
export function hasValidationResults(report) {
|
|
187
|
+
return report.results.length > 0;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Check if validation report has errors
|
|
191
|
+
*/
|
|
192
|
+
export function hasValidationErrors(report) {
|
|
193
|
+
return report.summary.error > 0;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check if validation report has warnings
|
|
197
|
+
*/
|
|
198
|
+
export function hasValidationWarnings(report) {
|
|
199
|
+
return report.summary.warning > 0;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Check if validation report has info messages
|
|
203
|
+
*/
|
|
204
|
+
export function hasValidationInfo(report) {
|
|
205
|
+
return report.summary.info > 0;
|
|
206
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scale consistency checks
|
|
3
|
+
* Checks for logical progressions in token scales (e.g., spacing, radius, typography)
|
|
4
|
+
*/
|
|
5
|
+
import type { ValidationResult, TokensForValidation, ScaleConsistencyIssue } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Validate all scales for consistency
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateScaleConsistency(tokens: TokensForValidation, severity?: 'info' | 'warning' | 'error'): ValidationResult[];
|
|
10
|
+
/**
|
|
11
|
+
* Get a summary of scale consistency issues
|
|
12
|
+
*/
|
|
13
|
+
export declare function getScaleConsistencySummary(issues: ScaleConsistencyIssue[]): string;
|
|
14
|
+
//# sourceMappingURL=scale-consistency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scale-consistency.d.ts","sourceRoot":"","sources":["../../src/validation/scale-consistency.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EAEtB,MAAM,YAAY,CAAC;AAmMpB;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,mBAAmB,EAC3B,QAAQ,GAAE,MAAM,GAAG,SAAS,GAAG,OAAmB,GACjD,gBAAgB,EAAE,CAgCpB;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,MAAM,CAelF"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scale consistency checks
|
|
3
|
+
* Checks for logical progressions in token scales (e.g., spacing, radius, typography)
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validation code for scale consistency
|
|
7
|
+
*/
|
|
8
|
+
const SCALE_CONSISTENCY_CODE = 'SCALE_CONSISTENCY';
|
|
9
|
+
/**
|
|
10
|
+
* Common scale orders for different categories
|
|
11
|
+
*/
|
|
12
|
+
const COMMON_SCALE_ORDERS = {
|
|
13
|
+
spacing: {
|
|
14
|
+
category: 'spacing',
|
|
15
|
+
expectedOrder: ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl'],
|
|
16
|
+
isNumeric: true
|
|
17
|
+
},
|
|
18
|
+
radius: {
|
|
19
|
+
category: 'radius',
|
|
20
|
+
expectedOrder: ['none', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', 'full'],
|
|
21
|
+
isNumeric: true
|
|
22
|
+
},
|
|
23
|
+
fontSize: {
|
|
24
|
+
category: 'fontSize',
|
|
25
|
+
expectedOrder: ['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl'],
|
|
26
|
+
isNumeric: true
|
|
27
|
+
},
|
|
28
|
+
fontWeight: {
|
|
29
|
+
category: 'fontWeight',
|
|
30
|
+
expectedOrder: ['thin', 'light', 'normal', 'medium', 'semibold', 'bold', 'extrabold', 'black'],
|
|
31
|
+
isNumeric: true
|
|
32
|
+
},
|
|
33
|
+
lineHeight: {
|
|
34
|
+
category: 'lineHeight',
|
|
35
|
+
expectedOrder: ['none', 'tight', 'snug', 'normal', 'relaxed', 'loose'],
|
|
36
|
+
isNumeric: true
|
|
37
|
+
},
|
|
38
|
+
zIndex: {
|
|
39
|
+
category: 'zIndex',
|
|
40
|
+
expectedOrder: ['base', 'dropdown', 'sticky', 'fixed', 'modal', 'popover', 'tooltip'],
|
|
41
|
+
isNumeric: true
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Parse a CSS value to extract numeric value and unit
|
|
46
|
+
*/
|
|
47
|
+
function parseCssValue(value) {
|
|
48
|
+
const match = value.match(/^(-?\d*\.?\d+)(px|rem|em|%|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|s|ms)?$/);
|
|
49
|
+
if (!match)
|
|
50
|
+
return null;
|
|
51
|
+
return {
|
|
52
|
+
value: parseFloat(match[1]),
|
|
53
|
+
unit: match[2] || ''
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if values are in a monotonic progression
|
|
58
|
+
*/
|
|
59
|
+
function checkMonotonicProgression(values, expectedIncreasing = true) {
|
|
60
|
+
const issues = [];
|
|
61
|
+
for (let i = 1; i < values.length; i++) {
|
|
62
|
+
const diff = values[i] - values[i - 1];
|
|
63
|
+
if (expectedIncreasing && diff <= 0) {
|
|
64
|
+
issues.push(i);
|
|
65
|
+
}
|
|
66
|
+
else if (!expectedIncreasing && diff >= 0) {
|
|
67
|
+
issues.push(i);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
isMonotonic: issues.length === 0,
|
|
72
|
+
issues
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check for gaps in a numeric progression
|
|
77
|
+
*/
|
|
78
|
+
function checkForGaps(values, keys, expectedOrder) {
|
|
79
|
+
const gapIndices = [];
|
|
80
|
+
// Check if any expected keys are missing
|
|
81
|
+
for (let i = 0; i < expectedOrder.length; i++) {
|
|
82
|
+
if (!keys.includes(expectedOrder[i])) {
|
|
83
|
+
// Find the position where this key should be
|
|
84
|
+
const position = expectedOrder.indexOf(expectedOrder[i]);
|
|
85
|
+
if (position < keys.length) {
|
|
86
|
+
gapIndices.push(i);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
hasGaps: gapIndices.length > 0,
|
|
92
|
+
gapIndices
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Validate scale consistency for a token category
|
|
97
|
+
*/
|
|
98
|
+
function validateScale(category, tokens, severity = 'warning') {
|
|
99
|
+
const results = [];
|
|
100
|
+
const scaleOrder = COMMON_SCALE_ORDERS[category];
|
|
101
|
+
if (!scaleOrder) {
|
|
102
|
+
// No predefined order for this category, skip
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
105
|
+
// Get keys in expected order (if they exist)
|
|
106
|
+
const keys = [];
|
|
107
|
+
const values = [];
|
|
108
|
+
const keyToValueMap = new Map();
|
|
109
|
+
for (const key of scaleOrder.expectedOrder) {
|
|
110
|
+
if (key in tokens) {
|
|
111
|
+
keys.push(key);
|
|
112
|
+
const rawValue = tokens[key];
|
|
113
|
+
let numericValue;
|
|
114
|
+
if (typeof rawValue === 'number') {
|
|
115
|
+
numericValue = rawValue;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const parsed = parseCssValue(rawValue);
|
|
119
|
+
if (parsed) {
|
|
120
|
+
numericValue = parsed.value;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Can't parse as numeric, skip this key
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
values.push(numericValue);
|
|
128
|
+
keyToValueMap.set(key, { value: numericValue, raw: rawValue });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (values.length < 2) {
|
|
132
|
+
// Not enough values to check consistency
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
135
|
+
// Check for monotonic progression
|
|
136
|
+
const { isMonotonic, issues } = checkMonotonicProgression(values, scaleOrder.isNumeric);
|
|
137
|
+
if (!isMonotonic) {
|
|
138
|
+
for (const issueIndex of issues) {
|
|
139
|
+
const key = keys[issueIndex];
|
|
140
|
+
const prevKey = keys[issueIndex - 1];
|
|
141
|
+
const currentValue = keyToValueMap.get(key).raw;
|
|
142
|
+
const prevValue = keyToValueMap.get(prevKey).raw;
|
|
143
|
+
results.push({
|
|
144
|
+
severity,
|
|
145
|
+
code: SCALE_CONSISTENCY_CODE,
|
|
146
|
+
message: `Non-monotonic progression in ${category} scale: "${key}" (${currentValue}) is not greater than "${prevKey}" (${prevValue})`,
|
|
147
|
+
path: `${category}.${key}`,
|
|
148
|
+
suggestion: `Consider adjusting the value of "${key}" to be greater than "${prevKey}" for a consistent scale.`,
|
|
149
|
+
example: `// Example:\n// ${category}.${prevKey}: ${prevValue}\n// ${category}.${key}: ${currentValue} // Should be > ${prevValue}`
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Check for gaps in the scale
|
|
154
|
+
const { hasGaps, gapIndices } = checkForGaps(values, keys, scaleOrder.expectedOrder);
|
|
155
|
+
if (hasGaps) {
|
|
156
|
+
const missingKeys = gapIndices.map(i => scaleOrder.expectedOrder[i]);
|
|
157
|
+
results.push({
|
|
158
|
+
severity: 'info', // Gaps are less severe than non-monotonic values
|
|
159
|
+
code: SCALE_CONSISTENCY_CODE,
|
|
160
|
+
message: `Potential gaps in ${category} scale: missing keys ${missingKeys.map(k => `"${k}"`).join(', ')}`,
|
|
161
|
+
path: category,
|
|
162
|
+
suggestion: `Consider adding the missing scale keys for a more complete scale.`,
|
|
163
|
+
example: `// Example:\n// ${category}.${missingKeys[0]}: "value"`
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Validate all scales for consistency
|
|
170
|
+
*/
|
|
171
|
+
export function validateScaleConsistency(tokens, severity = 'warning') {
|
|
172
|
+
const results = [];
|
|
173
|
+
// Validate spacing scale
|
|
174
|
+
if (tokens.spacing) {
|
|
175
|
+
results.push(...validateScale('spacing', tokens.spacing, severity));
|
|
176
|
+
}
|
|
177
|
+
// Validate radius scale
|
|
178
|
+
if (tokens.radius) {
|
|
179
|
+
results.push(...validateScale('radius', tokens.radius, severity));
|
|
180
|
+
}
|
|
181
|
+
// Validate typography scales
|
|
182
|
+
if (tokens.typography) {
|
|
183
|
+
if (tokens.typography.fontSize) {
|
|
184
|
+
results.push(...validateScale('fontSize', tokens.typography.fontSize, severity));
|
|
185
|
+
}
|
|
186
|
+
if (tokens.typography.fontWeight) {
|
|
187
|
+
results.push(...validateScale('fontWeight', tokens.typography.fontWeight, severity));
|
|
188
|
+
}
|
|
189
|
+
if (tokens.typography.lineHeight) {
|
|
190
|
+
results.push(...validateScale('lineHeight', tokens.typography.lineHeight, severity));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Validate z-index scale
|
|
194
|
+
if (tokens.zIndex) {
|
|
195
|
+
results.push(...validateScale('zIndex', tokens.zIndex, severity));
|
|
196
|
+
}
|
|
197
|
+
return results;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get a summary of scale consistency issues
|
|
201
|
+
*/
|
|
202
|
+
export function getScaleConsistencySummary(issues) {
|
|
203
|
+
if (issues.length === 0) {
|
|
204
|
+
return 'No scale consistency issues found.';
|
|
205
|
+
}
|
|
206
|
+
const summary = [];
|
|
207
|
+
summary.push(`Found ${issues.length} scale consistency issue${issues.length > 1 ? 's' : ''}:`);
|
|
208
|
+
for (const issue of issues) {
|
|
209
|
+
summary.push(` • ${issue.category}: ${issue.issue}`);
|
|
210
|
+
summary.push(` ${issue.details}`);
|
|
211
|
+
summary.push(` 💡 ${issue.suggestion}`);
|
|
212
|
+
}
|
|
213
|
+
return summary.join('\n');
|
|
214
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow format validation
|
|
3
|
+
* Checks for valid CSS box-shadow syntax
|
|
4
|
+
*/
|
|
5
|
+
import type { ValidationResult, TokensForValidation, ShadowFormatIssue } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Validate all shadow formats
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateShadowFormat(tokens: TokensForValidation, severity?: 'info' | 'warning' | 'error'): ValidationResult[];
|
|
10
|
+
/**
|
|
11
|
+
* Get a summary of shadow format issues
|
|
12
|
+
*/
|
|
13
|
+
export declare function getShadowFormatSummary(issues: ShadowFormatIssue[]): string;
|
|
14
|
+
//# sourceMappingURL=shadow-format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadow-format.d.ts","sourceRoot":"","sources":["../../src/validation/shadow-format.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AA6NpB;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,mBAAmB,EAC3B,QAAQ,GAAE,MAAM,GAAG,SAAS,GAAG,OAAmB,GACjD,gBAAgB,EAAE,CAqBpB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAe1E"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow format validation
|
|
3
|
+
* Checks for valid CSS box-shadow syntax
|
|
4
|
+
*/
|
|
5
|
+
import { isValidColor, getColorFormat } from './color-format.js';
|
|
6
|
+
/**
|
|
7
|
+
* Validation code for shadow format
|
|
8
|
+
*/
|
|
9
|
+
const SHADOW_FORMAT_CODE = 'SHADOW_FORMAT';
|
|
10
|
+
/**
|
|
11
|
+
* Parse a box-shadow value
|
|
12
|
+
* Format: offset-x | offset-y | blur-radius | spread-radius | color | inset
|
|
13
|
+
*/
|
|
14
|
+
function parseBoxShadow(value) {
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
const parts = trimmed.split(/\s+/);
|
|
17
|
+
const result = {
|
|
18
|
+
valid: false,
|
|
19
|
+
offsetX: undefined,
|
|
20
|
+
offsetY: undefined,
|
|
21
|
+
blur: undefined,
|
|
22
|
+
spread: undefined,
|
|
23
|
+
color: undefined,
|
|
24
|
+
inset: false,
|
|
25
|
+
error: undefined
|
|
26
|
+
};
|
|
27
|
+
// Check for inset keyword
|
|
28
|
+
let startIndex = 0;
|
|
29
|
+
if (parts[0] === 'inset') {
|
|
30
|
+
result.inset = true;
|
|
31
|
+
startIndex = 1;
|
|
32
|
+
}
|
|
33
|
+
// Need at least 2 values (offset-x and offset-y)
|
|
34
|
+
if (parts.length - startIndex < 2) {
|
|
35
|
+
result.error = 'Box-shadow requires at least offset-x and offset-y values';
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
// Parse offset-x
|
|
39
|
+
if (!isValidLength(parts[startIndex])) {
|
|
40
|
+
result.error = `Invalid offset-x value: "${parts[startIndex]}"`;
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
result.offsetX = parts[startIndex];
|
|
44
|
+
// Parse offset-y
|
|
45
|
+
if (!isValidLength(parts[startIndex + 1])) {
|
|
46
|
+
result.error = `Invalid offset-y value: "${parts[startIndex + 1]}"`;
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
result.offsetY = parts[startIndex + 1];
|
|
50
|
+
// Parse blur-radius (optional)
|
|
51
|
+
if (parts.length > startIndex + 2) {
|
|
52
|
+
if (isValidLength(parts[startIndex + 2])) {
|
|
53
|
+
result.blur = parts[startIndex + 2];
|
|
54
|
+
startIndex += 3;
|
|
55
|
+
}
|
|
56
|
+
else if (isValidColor(parts[startIndex + 2])) {
|
|
57
|
+
result.color = parts[startIndex + 2];
|
|
58
|
+
startIndex += 2;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
result.error = `Invalid blur-radius or color value: "${parts[startIndex + 2]}"`;
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
startIndex += 2;
|
|
67
|
+
}
|
|
68
|
+
// Parse spread-radius (optional)
|
|
69
|
+
if (parts.length > startIndex && isValidLength(parts[startIndex])) {
|
|
70
|
+
result.spread = parts[startIndex];
|
|
71
|
+
startIndex += 1;
|
|
72
|
+
}
|
|
73
|
+
// Parse color (optional)
|
|
74
|
+
if (parts.length > startIndex && isValidColor(parts[startIndex])) {
|
|
75
|
+
result.color = parts[startIndex];
|
|
76
|
+
}
|
|
77
|
+
result.valid = true;
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check if a value is a valid CSS length
|
|
82
|
+
*/
|
|
83
|
+
function isValidLength(value) {
|
|
84
|
+
const match = value.match(/^(-?\d*\.?\d+)(px|rem|em|%|vh|vw|vmin|vmax|ch|ex)?$/);
|
|
85
|
+
if (!match)
|
|
86
|
+
return false;
|
|
87
|
+
const numericValue = parseFloat(match[1]);
|
|
88
|
+
return !isNaN(numericValue);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if a length value is negative
|
|
92
|
+
*/
|
|
93
|
+
function isNegativeLength(value) {
|
|
94
|
+
return value.startsWith('-');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Validate a single shadow value
|
|
98
|
+
*/
|
|
99
|
+
function validateShadowValue(path, value, severity = 'warning') {
|
|
100
|
+
const parsed = parseBoxShadow(value);
|
|
101
|
+
if (!parsed.valid) {
|
|
102
|
+
return {
|
|
103
|
+
severity: 'error',
|
|
104
|
+
code: SHADOW_FORMAT_CODE,
|
|
105
|
+
message: `Invalid shadow format: "${value}"`,
|
|
106
|
+
path,
|
|
107
|
+
suggestion: parsed.error || 'Use valid box-shadow syntax: offset-x offset-y [blur-radius] [spread-radius] [color] [inset]',
|
|
108
|
+
example: `// Examples:\n// "${path}": "0 1px 2px 0 rgba(0,0,0,0.05)"\n// "${path}": "0 4px 6px -1px rgba(0,0,0,0.1)"`
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const issues = [];
|
|
112
|
+
// Check for negative blur-radius
|
|
113
|
+
if (parsed.blur && isNegativeLength(parsed.blur)) {
|
|
114
|
+
issues.push('Blur-radius should not be negative');
|
|
115
|
+
}
|
|
116
|
+
// Check for color format
|
|
117
|
+
if (parsed.color) {
|
|
118
|
+
const format = getColorFormat(parsed.color);
|
|
119
|
+
if (format === 'hex3' || format === 'hex4') {
|
|
120
|
+
issues.push('Consider using full hex color format (hex6) for better precision');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (issues.length > 0) {
|
|
124
|
+
return {
|
|
125
|
+
severity,
|
|
126
|
+
code: SHADOW_FORMAT_CODE,
|
|
127
|
+
message: `Shadow format issues: ${issues.join(', ')}`,
|
|
128
|
+
path,
|
|
129
|
+
suggestion: 'Review the shadow value for best practices.',
|
|
130
|
+
example: `// Example:\n// "${path}": "0 1px 2px 0 rgba(0,0,0,0.05)"`
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check for shadow scale consistency
|
|
137
|
+
*/
|
|
138
|
+
function checkShadowScaleConsistency(shadows, severity = 'warning') {
|
|
139
|
+
const results = [];
|
|
140
|
+
const keys = Object.keys(shadows);
|
|
141
|
+
if (keys.length < 2)
|
|
142
|
+
return results;
|
|
143
|
+
// Expected shadow scale order
|
|
144
|
+
const expectedOrder = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
|
|
145
|
+
const orderedKeys = keys.filter(k => expectedOrder.includes(k)).sort((a, b) => {
|
|
146
|
+
return expectedOrder.indexOf(a) - expectedOrder.indexOf(b);
|
|
147
|
+
});
|
|
148
|
+
if (orderedKeys.length < 2)
|
|
149
|
+
return results;
|
|
150
|
+
// Parse shadows to compare blur values
|
|
151
|
+
const blurValues = [];
|
|
152
|
+
for (const key of orderedKeys) {
|
|
153
|
+
const parsed = parseBoxShadow(shadows[key]);
|
|
154
|
+
let blur = null;
|
|
155
|
+
if (parsed.blur) {
|
|
156
|
+
const match = parsed.blur.match(/^(-?\d*\.?\d+)(px|rem|em)?$/);
|
|
157
|
+
if (match) {
|
|
158
|
+
blur = parseFloat(match[1]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
blurValues.push({ key, blur });
|
|
162
|
+
}
|
|
163
|
+
// Check for monotonic progression
|
|
164
|
+
for (let i = 1; i < blurValues.length; i++) {
|
|
165
|
+
const prev = blurValues[i - 1];
|
|
166
|
+
const curr = blurValues[i];
|
|
167
|
+
if (prev.blur !== null && curr.blur !== null) {
|
|
168
|
+
if (curr.blur <= prev.blur) {
|
|
169
|
+
results.push({
|
|
170
|
+
severity,
|
|
171
|
+
code: SHADOW_FORMAT_CODE,
|
|
172
|
+
message: `Non-monotonic shadow scale: "${curr.key}" blur (${curr.blur}px) is not greater than "${prev.key}" (${prev.blur}px)`,
|
|
173
|
+
path: `shadows.${curr.key}`,
|
|
174
|
+
suggestion: 'Shadow blur values should increase with scale size.',
|
|
175
|
+
example: `// Example:\n// "shadows.${prev.key}": "${shadows[prev.key]}"\n// "shadows.${curr.key}": "0 ${curr.blur + 2}px ..."`
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return results;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Validate all shadow formats
|
|
184
|
+
*/
|
|
185
|
+
export function validateShadowFormat(tokens, severity = 'warning') {
|
|
186
|
+
const results = [];
|
|
187
|
+
if (!tokens.shadows) {
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
190
|
+
const shadows = tokens.shadows;
|
|
191
|
+
// Validate each shadow value
|
|
192
|
+
for (const [key, value] of Object.entries(shadows)) {
|
|
193
|
+
const result = validateShadowValue(`shadows.${key}`, value, severity);
|
|
194
|
+
if (result) {
|
|
195
|
+
results.push(result);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Check for scale consistency
|
|
199
|
+
results.push(...checkShadowScaleConsistency(shadows, severity));
|
|
200
|
+
return results;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get a summary of shadow format issues
|
|
204
|
+
*/
|
|
205
|
+
export function getShadowFormatSummary(issues) {
|
|
206
|
+
if (issues.length === 0) {
|
|
207
|
+
return 'No shadow format issues found.';
|
|
208
|
+
}
|
|
209
|
+
const summary = [];
|
|
210
|
+
summary.push(`Found ${issues.length} shadow format issue${issues.length > 1 ? 's' : ''}:`);
|
|
211
|
+
for (const issue of issues) {
|
|
212
|
+
summary.push(` • ${issue.path}: ${issue.issue}`);
|
|
213
|
+
summary.push(` ${issue.details}`);
|
|
214
|
+
summary.push(` 💡 ${issue.suggestion}`);
|
|
215
|
+
}
|
|
216
|
+
return summary.join('\n');
|
|
217
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spacing format validation
|
|
3
|
+
* Checks for valid CSS spacing units and consistency
|
|
4
|
+
*/
|
|
5
|
+
import type { ValidationResult, TokensForValidation, SpacingFormatIssue } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Validate all spacing formats
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateSpacingFormat(tokens: TokensForValidation, severity?: 'info' | 'warning' | 'error'): ValidationResult[];
|
|
10
|
+
/**
|
|
11
|
+
* Get a summary of spacing format issues
|
|
12
|
+
*/
|
|
13
|
+
export declare function getSpacingFormatSummary(issues: SpacingFormatIssue[]): string;
|
|
14
|
+
//# sourceMappingURL=spacing-format.d.ts.map
|