@versini/ui-styles 8.0.0 → 8.1.0
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/README.md +92 -0
- package/dist/cli/ui-doctor.js +633 -0
- package/dist/index.js +25 -8
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ This package provides the core TailwindCSS configuration, custom styles, and des
|
|
|
11
11
|
- [Features](#features)
|
|
12
12
|
- [Installation](#installation)
|
|
13
13
|
- [Usage](#usage)
|
|
14
|
+
- [CLI Tools](#cli-tools)
|
|
14
15
|
|
|
15
16
|
## Features
|
|
16
17
|
|
|
@@ -19,6 +20,7 @@ This package provides the core TailwindCSS configuration, custom styles, and des
|
|
|
19
20
|
- **🎭 Theme Support**: Built-in light, dark, and system theme support
|
|
20
21
|
- **📐 Typography**: Optimized typography scales and spacing
|
|
21
22
|
- **🌈 Color Tokens**: Semantic color tokens for consistent theming
|
|
23
|
+
- **🩺 ui-doctor CLI**: Detect raw Tailwind color classes and suggest design tokens
|
|
22
24
|
|
|
23
25
|
## Installation
|
|
24
26
|
|
|
@@ -72,3 +74,93 @@ module.exports = {
|
|
|
72
74
|
|
|
73
75
|
@import "@versini/ui-styles/dist/styles.css";
|
|
74
76
|
```
|
|
77
|
+
|
|
78
|
+
## CLI Tools
|
|
79
|
+
|
|
80
|
+
### ui-doctor
|
|
81
|
+
|
|
82
|
+
A CLI tool to detect raw Tailwind CSS color classes (like `bg-slate-500`, `text-red-400`) and suggest semantic design tokens from the `@versini/ui-styles` package.
|
|
83
|
+
|
|
84
|
+
#### Usage
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Scan current directory
|
|
88
|
+
npx ui-doctor
|
|
89
|
+
|
|
90
|
+
# Scan a specific directory
|
|
91
|
+
npx ui-doctor ./src
|
|
92
|
+
|
|
93
|
+
# Scan with specific file extensions
|
|
94
|
+
npx ui-doctor -e tsx,ts
|
|
95
|
+
|
|
96
|
+
# Output in JSON format (for CI/CD integration)
|
|
97
|
+
npx ui-doctor --json
|
|
98
|
+
|
|
99
|
+
# Combine options
|
|
100
|
+
npx ui-doctor ./src --json -e tsx,ts
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Options
|
|
104
|
+
|
|
105
|
+
| Option | Alias | Description | Default |
|
|
106
|
+
| --------------------- | ----- | ---------------------------------- | ------------------------------ |
|
|
107
|
+
| `--path <path>` | `-p` | Path to scan | `.` (current directory) |
|
|
108
|
+
| `--extensions <ext>` | `-e` | Comma-separated file extensions | `js,jsx,ts,tsx` |
|
|
109
|
+
| `--ignore <patterns>` | `-i` | Comma-separated patterns to ignore | `node_modules,dist,build,.git` |
|
|
110
|
+
| `--json` | | Output in JSON format | `false` |
|
|
111
|
+
| `--help` | `-h` | Show help message | |
|
|
112
|
+
| `--version` | `-v` | Show version number | |
|
|
113
|
+
|
|
114
|
+
#### Example Output
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
ui-doctor - Scanning for raw Tailwind color classes...
|
|
118
|
+
|
|
119
|
+
✗ src/components/Button.tsx:15
|
|
120
|
+
<div className="bg-slate-500 text-white">
|
|
121
|
+
Found: bg-slate-500
|
|
122
|
+
Suggestion: Use 'bg-surface-medium'
|
|
123
|
+
|
|
124
|
+
✗ src/components/Card.tsx:23
|
|
125
|
+
<p className="text-red-400">Error</p>
|
|
126
|
+
Found: text-red-400
|
|
127
|
+
Suggestion: Use 'text-copy-error-light'
|
|
128
|
+
Other options: text-border-danger-medium
|
|
129
|
+
|
|
130
|
+
──────────────────────────────────────────────────
|
|
131
|
+
Summary: 2 violations found in 2 files
|
|
132
|
+
Scanned 50 files in 25ms
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### JSON Output
|
|
136
|
+
|
|
137
|
+
When using `--json`, the output is formatted for CI/CD integration:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"filesScanned": 50,
|
|
142
|
+
"totalViolations": 2,
|
|
143
|
+
"duration": 25,
|
|
144
|
+
"violations": [
|
|
145
|
+
{
|
|
146
|
+
"file": "src/components/Button.tsx",
|
|
147
|
+
"issues": [
|
|
148
|
+
{
|
|
149
|
+
"line": 15,
|
|
150
|
+
"column": 12,
|
|
151
|
+
"class": "bg-slate-500",
|
|
152
|
+
"prefix": "bg",
|
|
153
|
+
"color": "slate",
|
|
154
|
+
"shade": "500",
|
|
155
|
+
"suggestions": ["bg-surface-medium"]
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### Exit Codes
|
|
164
|
+
|
|
165
|
+
- `0` - No violations found
|
|
166
|
+
- `1` - Violations found or error occurred
|
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs, { readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import path, { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const TAILWIND_COLOR_PALETTES = [
|
|
6
|
+
"red",
|
|
7
|
+
"orange",
|
|
8
|
+
"amber",
|
|
9
|
+
"yellow",
|
|
10
|
+
"lime",
|
|
11
|
+
"green",
|
|
12
|
+
"emerald",
|
|
13
|
+
"teal",
|
|
14
|
+
"cyan",
|
|
15
|
+
"sky",
|
|
16
|
+
"blue",
|
|
17
|
+
"indigo",
|
|
18
|
+
"violet",
|
|
19
|
+
"purple",
|
|
20
|
+
"fuchsia",
|
|
21
|
+
"pink",
|
|
22
|
+
"rose",
|
|
23
|
+
"slate",
|
|
24
|
+
"gray",
|
|
25
|
+
"zinc",
|
|
26
|
+
"neutral",
|
|
27
|
+
"stone"
|
|
28
|
+
];
|
|
29
|
+
const TAILWIND_SHADES = [
|
|
30
|
+
"50",
|
|
31
|
+
"100",
|
|
32
|
+
"200",
|
|
33
|
+
"300",
|
|
34
|
+
"400",
|
|
35
|
+
"500",
|
|
36
|
+
"600",
|
|
37
|
+
"700",
|
|
38
|
+
"800",
|
|
39
|
+
"900",
|
|
40
|
+
"950"
|
|
41
|
+
];
|
|
42
|
+
const COLOR_UTILITY_PREFIXES = [
|
|
43
|
+
"bg",
|
|
44
|
+
"text",
|
|
45
|
+
"border",
|
|
46
|
+
"border-t",
|
|
47
|
+
"border-b",
|
|
48
|
+
"border-l",
|
|
49
|
+
"border-r",
|
|
50
|
+
"border-x",
|
|
51
|
+
"border-y",
|
|
52
|
+
"outline",
|
|
53
|
+
"ring",
|
|
54
|
+
"shadow",
|
|
55
|
+
"accent",
|
|
56
|
+
"caret",
|
|
57
|
+
"fill",
|
|
58
|
+
"stroke",
|
|
59
|
+
"decoration",
|
|
60
|
+
"divide",
|
|
61
|
+
"from",
|
|
62
|
+
"via",
|
|
63
|
+
"to",
|
|
64
|
+
"placeholder"
|
|
65
|
+
];
|
|
66
|
+
function buildColorPattern() {
|
|
67
|
+
const prefixes = COLOR_UTILITY_PREFIXES.join("|");
|
|
68
|
+
const colors2 = TAILWIND_COLOR_PALETTES.join("|");
|
|
69
|
+
const shades = TAILWIND_SHADES.join("|");
|
|
70
|
+
const pattern = `(?:^|[\\s"'\`{])(((?:[a-zA-Z0-9_-]+:)*)(${prefixes})-(${colors2})-(${shades}))(?:/(\\d+(?:\\.\\d+)?%?))?(?=$|[\\s"'\`}])`;
|
|
71
|
+
return new RegExp(pattern, "g");
|
|
72
|
+
}
|
|
73
|
+
function findColorClassesInLine(line, lineNumber) {
|
|
74
|
+
const matches = [];
|
|
75
|
+
const pattern = buildColorPattern();
|
|
76
|
+
let match = pattern.exec(line);
|
|
77
|
+
while (match !== null) {
|
|
78
|
+
const fullMatch = match[1];
|
|
79
|
+
const variantString = match[2] || "";
|
|
80
|
+
const prefix = match[3];
|
|
81
|
+
const color = match[4];
|
|
82
|
+
const shade = match[5];
|
|
83
|
+
const opacity = match[6];
|
|
84
|
+
const variants = variantString ? variantString.split(":").filter((v) => v.length > 0) : [];
|
|
85
|
+
matches.push({
|
|
86
|
+
fullMatch: opacity ? `${fullMatch}/${opacity}` : fullMatch,
|
|
87
|
+
variants,
|
|
88
|
+
prefix,
|
|
89
|
+
color,
|
|
90
|
+
shade,
|
|
91
|
+
opacity,
|
|
92
|
+
line: lineNumber,
|
|
93
|
+
column: match.index + 1,
|
|
94
|
+
lineContent: line
|
|
95
|
+
});
|
|
96
|
+
match = pattern.exec(line);
|
|
97
|
+
}
|
|
98
|
+
return matches;
|
|
99
|
+
}
|
|
100
|
+
function findColorClasses(content) {
|
|
101
|
+
const lines = content.split("\n");
|
|
102
|
+
const allMatches = [];
|
|
103
|
+
for (let i = 0; i < lines.length; i++) {
|
|
104
|
+
const lineMatches = findColorClassesInLine(lines[i], i + 1);
|
|
105
|
+
allMatches.push(...lineMatches);
|
|
106
|
+
}
|
|
107
|
+
return allMatches;
|
|
108
|
+
}
|
|
109
|
+
const DEFAULT_EXTENSIONS = ["js", "jsx", "ts", "tsx"];
|
|
110
|
+
const DEFAULT_IGNORE = [
|
|
111
|
+
"node_modules",
|
|
112
|
+
"dist",
|
|
113
|
+
"build",
|
|
114
|
+
".git",
|
|
115
|
+
"coverage",
|
|
116
|
+
".next",
|
|
117
|
+
".rslib",
|
|
118
|
+
"tmp"
|
|
119
|
+
];
|
|
120
|
+
function shouldIgnore(filePath, ignorePatterns) {
|
|
121
|
+
const parts = filePath.split(path.sep);
|
|
122
|
+
return parts.some((part) => ignorePatterns.includes(part));
|
|
123
|
+
}
|
|
124
|
+
function hasAllowedExtension(filePath, extensions) {
|
|
125
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
126
|
+
return extensions.includes(ext);
|
|
127
|
+
}
|
|
128
|
+
function scanDirectory(dirPath, rootPath, options, results) {
|
|
129
|
+
let entries;
|
|
130
|
+
try {
|
|
131
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
132
|
+
} catch {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
137
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
138
|
+
if (shouldIgnore(relativePath, options.ignore)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (entry.isDirectory()) {
|
|
142
|
+
scanDirectory(fullPath, rootPath, options, results);
|
|
143
|
+
} else if (entry.isFile() && hasAllowedExtension(entry.name, options.extensions)) {
|
|
144
|
+
try {
|
|
145
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
146
|
+
results.push({
|
|
147
|
+
absolutePath: fullPath,
|
|
148
|
+
relativePath,
|
|
149
|
+
content
|
|
150
|
+
});
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function scanFiles(targetPath, options) {
|
|
157
|
+
const resolvedPath = path.resolve(targetPath);
|
|
158
|
+
const fullOptions = {
|
|
159
|
+
extensions: options?.extensions || DEFAULT_EXTENSIONS,
|
|
160
|
+
ignore: options?.ignore || DEFAULT_IGNORE
|
|
161
|
+
};
|
|
162
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
163
|
+
throw new Error(`Path does not exist: ${resolvedPath}`);
|
|
164
|
+
}
|
|
165
|
+
const stats = fs.statSync(resolvedPath);
|
|
166
|
+
const results = [];
|
|
167
|
+
if (stats.isFile()) {
|
|
168
|
+
if (hasAllowedExtension(resolvedPath, fullOptions.extensions)) {
|
|
169
|
+
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
170
|
+
results.push({
|
|
171
|
+
absolutePath: resolvedPath,
|
|
172
|
+
relativePath: path.basename(resolvedPath),
|
|
173
|
+
content
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
} else if (stats.isDirectory()) {
|
|
177
|
+
scanDirectory(resolvedPath, resolvedPath, fullOptions, results);
|
|
178
|
+
}
|
|
179
|
+
return results;
|
|
180
|
+
}
|
|
181
|
+
function getDefaultScanOptions() {
|
|
182
|
+
return {
|
|
183
|
+
extensions: [...DEFAULT_EXTENSIONS],
|
|
184
|
+
ignore: [...DEFAULT_IGNORE]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const colors = {
|
|
188
|
+
reset: "\x1B[0m",
|
|
189
|
+
bright: "\x1B[1m",
|
|
190
|
+
dim: "\x1B[2m",
|
|
191
|
+
red: "\x1B[31m",
|
|
192
|
+
green: "\x1B[32m",
|
|
193
|
+
yellow: "\x1B[33m",
|
|
194
|
+
cyan: "\x1B[36m"
|
|
195
|
+
};
|
|
196
|
+
function truncateLine(line, maxLength) {
|
|
197
|
+
if (line.length <= maxLength) {
|
|
198
|
+
return line;
|
|
199
|
+
}
|
|
200
|
+
return `${line.slice(0, maxLength - 3)}...`;
|
|
201
|
+
}
|
|
202
|
+
function formatConsoleReport(report) {
|
|
203
|
+
const lines = [];
|
|
204
|
+
lines.push("");
|
|
205
|
+
lines.push(
|
|
206
|
+
`${colors.cyan}${colors.bright}ui-doctor${colors.reset} - Scanning for raw Tailwind color classes...`
|
|
207
|
+
);
|
|
208
|
+
lines.push("");
|
|
209
|
+
if (report.filesWithViolations.length === 0) {
|
|
210
|
+
lines.push(
|
|
211
|
+
`${colors.green}✓${colors.reset} No raw Tailwind color classes found!`
|
|
212
|
+
);
|
|
213
|
+
lines.push(
|
|
214
|
+
`${colors.dim} Scanned ${report.filesScanned} files in ${report.duration}ms${colors.reset}`
|
|
215
|
+
);
|
|
216
|
+
lines.push("");
|
|
217
|
+
return lines.join("\n");
|
|
218
|
+
}
|
|
219
|
+
for (const fileViolation of report.filesWithViolations) {
|
|
220
|
+
for (const violation of fileViolation.violations) {
|
|
221
|
+
const { match, suggestions } = violation;
|
|
222
|
+
lines.push(
|
|
223
|
+
`${colors.red}✗${colors.reset} ${colors.cyan}${fileViolation.filePath}${colors.reset}:${colors.yellow}${match.line}${colors.reset}`
|
|
224
|
+
);
|
|
225
|
+
const linePreview = truncateLine(match.lineContent.trim(), 80);
|
|
226
|
+
lines.push(` ${colors.dim}${linePreview}${colors.reset}`);
|
|
227
|
+
lines.push(
|
|
228
|
+
` ${colors.red}Found:${colors.reset} ${colors.bright}${match.fullMatch}${colors.reset}`
|
|
229
|
+
);
|
|
230
|
+
if (suggestions.length > 0) {
|
|
231
|
+
const topSuggestion = suggestions[0];
|
|
232
|
+
lines.push(
|
|
233
|
+
` ${colors.green}Suggestion:${colors.reset} Use '${colors.green}${topSuggestion.suggestedClass}${colors.reset}'`
|
|
234
|
+
);
|
|
235
|
+
if (suggestions.length > 1) {
|
|
236
|
+
const otherSuggestions = suggestions.slice(1, 4).map((s) => s.suggestedClass).join(", ");
|
|
237
|
+
lines.push(
|
|
238
|
+
` ${colors.dim}Other options: ${otherSuggestions}${colors.reset}`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
lines.push(
|
|
243
|
+
` ${colors.yellow}No token available${colors.reset} for this color combination`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
lines.push("");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
lines.push(`${colors.dim}${"─".repeat(50)}${colors.reset}`);
|
|
250
|
+
const fileCount = report.filesWithViolations.length;
|
|
251
|
+
const violationWord = report.totalViolations === 1 ? "violation" : "violations";
|
|
252
|
+
const fileWord = fileCount === 1 ? "file" : "files";
|
|
253
|
+
lines.push(
|
|
254
|
+
`${colors.red}${colors.bright}Summary:${colors.reset} ${report.totalViolations} ${violationWord} found in ${fileCount} ${fileWord}`
|
|
255
|
+
);
|
|
256
|
+
lines.push(
|
|
257
|
+
`${colors.dim}Scanned ${report.filesScanned} files in ${report.duration}ms${colors.reset}`
|
|
258
|
+
);
|
|
259
|
+
lines.push("");
|
|
260
|
+
return lines.join("\n");
|
|
261
|
+
}
|
|
262
|
+
function formatJsonReport(report) {
|
|
263
|
+
const jsonReport = {
|
|
264
|
+
filesScanned: report.filesScanned,
|
|
265
|
+
totalViolations: report.totalViolations,
|
|
266
|
+
duration: report.duration,
|
|
267
|
+
violations: report.filesWithViolations.map((fv) => ({
|
|
268
|
+
file: fv.filePath,
|
|
269
|
+
issues: fv.violations.map((v) => ({
|
|
270
|
+
line: v.match.line,
|
|
271
|
+
column: v.match.column,
|
|
272
|
+
class: v.match.fullMatch,
|
|
273
|
+
prefix: v.match.prefix,
|
|
274
|
+
color: v.match.color,
|
|
275
|
+
shade: v.match.shade,
|
|
276
|
+
suggestions: v.suggestions.map((s) => s.suggestedClass)
|
|
277
|
+
}))
|
|
278
|
+
}))
|
|
279
|
+
};
|
|
280
|
+
return JSON.stringify(jsonReport, null, 2);
|
|
281
|
+
}
|
|
282
|
+
function printReport(report, json = false) {
|
|
283
|
+
if (json) {
|
|
284
|
+
console.log(formatJsonReport(report));
|
|
285
|
+
} else {
|
|
286
|
+
console.log(formatConsoleReport(report));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function getTokensFilePath() {
|
|
290
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
291
|
+
const __dirname = dirname(__filename);
|
|
292
|
+
const possiblePaths = [
|
|
293
|
+
// From src/cli/ - development mode
|
|
294
|
+
resolve(__dirname, "../plugins/tailwindcss/tokens.ts"),
|
|
295
|
+
// From dist/cli/ - look in src/ folder
|
|
296
|
+
resolve(__dirname, "../../src/plugins/tailwindcss/tokens.ts")
|
|
297
|
+
];
|
|
298
|
+
for (const path2 of possiblePaths) {
|
|
299
|
+
if (existsSync(path2)) {
|
|
300
|
+
return path2;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return possiblePaths[0];
|
|
304
|
+
}
|
|
305
|
+
function parseVariables(content) {
|
|
306
|
+
const variables = /* @__PURE__ */ new Map();
|
|
307
|
+
const constPattern = /const\s+(\w+)\s*=\s*([^;]+);/g;
|
|
308
|
+
const matches = content.matchAll(constPattern);
|
|
309
|
+
for (const match of matches) {
|
|
310
|
+
const varName = match[1];
|
|
311
|
+
const varValue = match[2].trim();
|
|
312
|
+
const colorMatch = varValue.match(/colors\.(\w+)\[(\d+)\]/);
|
|
313
|
+
if (colorMatch) {
|
|
314
|
+
variables.set(varName, `colors.${colorMatch[1]}[${colorMatch[2]}]`);
|
|
315
|
+
} else if (varValue.startsWith('"') || varValue.startsWith("'")) {
|
|
316
|
+
variables.set(varName, varValue.replace(/["']/g, ""));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return variables;
|
|
320
|
+
}
|
|
321
|
+
function resolveTokenValue(value, variables) {
|
|
322
|
+
if (value.match(/colors\.\w+\[\d+\]/)) {
|
|
323
|
+
return value;
|
|
324
|
+
}
|
|
325
|
+
const resolved = variables.get(value);
|
|
326
|
+
if (resolved) {
|
|
327
|
+
return resolved;
|
|
328
|
+
}
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
function parseColorsObject(content, variables) {
|
|
332
|
+
const colors2 = {};
|
|
333
|
+
const colorsMatch = content.match(/colors:\s*\{([\s\S]*?)\n\t\}/);
|
|
334
|
+
if (!colorsMatch) {
|
|
335
|
+
return colors2;
|
|
336
|
+
}
|
|
337
|
+
const colorsContent = colorsMatch[1];
|
|
338
|
+
const tokenPattern = /"([^"]+)":\s*([^,\n]+)/g;
|
|
339
|
+
const matches = colorsContent.matchAll(tokenPattern);
|
|
340
|
+
for (const match of matches) {
|
|
341
|
+
const tokenName = match[1];
|
|
342
|
+
let tokenValue = match[2].trim();
|
|
343
|
+
tokenValue = tokenValue.replace(/,$/, "").trim();
|
|
344
|
+
const resolved = resolveTokenValue(tokenValue, variables);
|
|
345
|
+
colors2[tokenName] = resolved;
|
|
346
|
+
}
|
|
347
|
+
return colors2;
|
|
348
|
+
}
|
|
349
|
+
function parseTokensFile(filePath) {
|
|
350
|
+
const errors = [];
|
|
351
|
+
const tokens = { colors: {} };
|
|
352
|
+
const tokensPath = getTokensFilePath();
|
|
353
|
+
let content;
|
|
354
|
+
try {
|
|
355
|
+
content = readFileSync(tokensPath, "utf-8");
|
|
356
|
+
} catch (_error) {
|
|
357
|
+
errors.push(`Failed to read tokens file: ${tokensPath}`);
|
|
358
|
+
return { tokens, errors };
|
|
359
|
+
}
|
|
360
|
+
const variables = parseVariables(content);
|
|
361
|
+
tokens.colors = parseColorsObject(content, variables);
|
|
362
|
+
if (Object.keys(tokens.colors).length === 0) {
|
|
363
|
+
errors.push("No tokens found in the colors object");
|
|
364
|
+
}
|
|
365
|
+
return { tokens, errors };
|
|
366
|
+
}
|
|
367
|
+
function loadTokenDefinitions() {
|
|
368
|
+
const result = parseTokensFile();
|
|
369
|
+
if (result.errors.length > 0) {
|
|
370
|
+
throw new Error(`Token parsing errors: ${result.errors.join(", ")}`);
|
|
371
|
+
}
|
|
372
|
+
return result.tokens;
|
|
373
|
+
}
|
|
374
|
+
const PREFIX_TO_TOKEN_CATEGORY = {
|
|
375
|
+
bg: ["surface", "action", "table"],
|
|
376
|
+
text: ["copy"],
|
|
377
|
+
border: ["border"],
|
|
378
|
+
"border-t": ["border"],
|
|
379
|
+
"border-b": ["border"],
|
|
380
|
+
"border-l": ["border"],
|
|
381
|
+
"border-r": ["border"],
|
|
382
|
+
"border-x": ["border"],
|
|
383
|
+
"border-y": ["border"],
|
|
384
|
+
outline: ["border", "focus"],
|
|
385
|
+
ring: ["focus", "border"],
|
|
386
|
+
shadow: ["surface"],
|
|
387
|
+
accent: ["action", "surface"],
|
|
388
|
+
caret: ["copy"],
|
|
389
|
+
fill: ["copy", "surface"],
|
|
390
|
+
stroke: ["copy", "border"],
|
|
391
|
+
decoration: ["copy"],
|
|
392
|
+
divide: ["border"],
|
|
393
|
+
from: ["surface", "action"],
|
|
394
|
+
via: ["surface", "action"],
|
|
395
|
+
to: ["surface", "action"],
|
|
396
|
+
placeholder: ["copy"]
|
|
397
|
+
};
|
|
398
|
+
function buildColorKey(color, shade) {
|
|
399
|
+
return `${color}-${shade}`;
|
|
400
|
+
}
|
|
401
|
+
function parseTokenValue(value) {
|
|
402
|
+
const match = value.match(/colors\.(\w+)\[(\d+)\]/);
|
|
403
|
+
if (match) {
|
|
404
|
+
const color = match[1];
|
|
405
|
+
const shade = match[2];
|
|
406
|
+
return { color, shade };
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
class TokenMapper {
|
|
411
|
+
colorToTokens;
|
|
412
|
+
tokenDefinitions;
|
|
413
|
+
constructor(tokenDefinitions) {
|
|
414
|
+
this.tokenDefinitions = tokenDefinitions;
|
|
415
|
+
this.colorToTokens = /* @__PURE__ */ new Map();
|
|
416
|
+
this.buildLookupTable();
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Build the reverse lookup table from colors to tokens.
|
|
420
|
+
*/
|
|
421
|
+
buildLookupTable() {
|
|
422
|
+
for (const [tokenName, colorRef] of Object.entries(
|
|
423
|
+
this.tokenDefinitions.colors
|
|
424
|
+
)) {
|
|
425
|
+
const parsed = parseTokenValue(colorRef);
|
|
426
|
+
if (parsed) {
|
|
427
|
+
const key = buildColorKey(parsed.color, parsed.shade);
|
|
428
|
+
const existing = this.colorToTokens.get(key) || [];
|
|
429
|
+
existing.push({
|
|
430
|
+
tokenName,
|
|
431
|
+
colorReference: colorRef,
|
|
432
|
+
suggestedClass: "",
|
|
433
|
+
// Will be filled when suggesting
|
|
434
|
+
isExactMatch: true
|
|
435
|
+
});
|
|
436
|
+
this.colorToTokens.set(key, existing);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Get token suggestions for a raw color class match.
|
|
442
|
+
* Only returns tokens from semantically appropriate categories.
|
|
443
|
+
*/
|
|
444
|
+
getSuggestions(match) {
|
|
445
|
+
const key = buildColorKey(match.color, match.shade);
|
|
446
|
+
const tokens = this.colorToTokens.get(key);
|
|
447
|
+
if (!tokens || tokens.length === 0) {
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
const preferredCategories = PREFIX_TO_TOKEN_CATEGORY[match.prefix] || [];
|
|
451
|
+
const variantPrefix = match.variants.length > 0 ? `${match.variants.join(":")}:` : "";
|
|
452
|
+
const suggestions = tokens.filter((token) => {
|
|
453
|
+
if (preferredCategories.length === 0) {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
const category = this.getTokenCategory(token.tokenName);
|
|
457
|
+
return preferredCategories.includes(category);
|
|
458
|
+
}).map((token) => ({
|
|
459
|
+
...token,
|
|
460
|
+
suggestedClass: `${variantPrefix}${match.prefix}-${token.tokenName}`
|
|
461
|
+
})).sort((a, b) => {
|
|
462
|
+
const aCategory = this.getTokenCategory(a.tokenName);
|
|
463
|
+
const bCategory = this.getTokenCategory(b.tokenName);
|
|
464
|
+
const aIndex = preferredCategories.indexOf(aCategory);
|
|
465
|
+
const bIndex = preferredCategories.indexOf(bCategory);
|
|
466
|
+
if (aIndex !== bIndex) {
|
|
467
|
+
return aIndex - bIndex;
|
|
468
|
+
}
|
|
469
|
+
return a.tokenName.localeCompare(b.tokenName);
|
|
470
|
+
});
|
|
471
|
+
return suggestions;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Get the category of a token (first part of the name).
|
|
475
|
+
*/
|
|
476
|
+
getTokenCategory(tokenName) {
|
|
477
|
+
const parts = tokenName.split("-");
|
|
478
|
+
return parts[0];
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Get all available tokens.
|
|
482
|
+
*/
|
|
483
|
+
getAllTokens() {
|
|
484
|
+
return Object.keys(this.tokenDefinitions.colors);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function createTokenMapper() {
|
|
488
|
+
const tokenDefinitions = loadTokenDefinitions();
|
|
489
|
+
return new TokenMapper(tokenDefinitions);
|
|
490
|
+
}
|
|
491
|
+
const VERSION = "1.0.0";
|
|
492
|
+
function parseArgs(args) {
|
|
493
|
+
const defaults = getDefaultScanOptions();
|
|
494
|
+
const options = {
|
|
495
|
+
path: ".",
|
|
496
|
+
extensions: defaults.extensions,
|
|
497
|
+
ignore: defaults.ignore,
|
|
498
|
+
json: false,
|
|
499
|
+
help: false,
|
|
500
|
+
version: false
|
|
501
|
+
};
|
|
502
|
+
let i = 0;
|
|
503
|
+
while (i < args.length) {
|
|
504
|
+
const arg = args[i];
|
|
505
|
+
switch (arg) {
|
|
506
|
+
case "-h":
|
|
507
|
+
case "--help":
|
|
508
|
+
options.help = true;
|
|
509
|
+
break;
|
|
510
|
+
case "-v":
|
|
511
|
+
case "--version":
|
|
512
|
+
options.version = true;
|
|
513
|
+
break;
|
|
514
|
+
case "--json":
|
|
515
|
+
options.json = true;
|
|
516
|
+
break;
|
|
517
|
+
case "-p":
|
|
518
|
+
case "--path":
|
|
519
|
+
i++;
|
|
520
|
+
if (i < args.length) {
|
|
521
|
+
options.path = args[i];
|
|
522
|
+
}
|
|
523
|
+
break;
|
|
524
|
+
case "-e":
|
|
525
|
+
case "--extensions":
|
|
526
|
+
i++;
|
|
527
|
+
if (i < args.length) {
|
|
528
|
+
options.extensions = args[i].split(",").map((e) => e.trim());
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
case "-i":
|
|
532
|
+
case "--ignore":
|
|
533
|
+
i++;
|
|
534
|
+
if (i < args.length) {
|
|
535
|
+
options.ignore = args[i].split(",").map((p) => p.trim());
|
|
536
|
+
}
|
|
537
|
+
break;
|
|
538
|
+
default:
|
|
539
|
+
if (!arg.startsWith("-")) {
|
|
540
|
+
options.path = arg;
|
|
541
|
+
}
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
i++;
|
|
545
|
+
}
|
|
546
|
+
return options;
|
|
547
|
+
}
|
|
548
|
+
function printHelp() {
|
|
549
|
+
console.log(`
|
|
550
|
+
ui-doctor v${VERSION}
|
|
551
|
+
|
|
552
|
+
A CLI tool to detect raw Tailwind CSS color classes and suggest
|
|
553
|
+
semantic design tokens from @versini/ui-styles.
|
|
554
|
+
|
|
555
|
+
Usage:
|
|
556
|
+
ui-doctor [path] [options]
|
|
557
|
+
|
|
558
|
+
Arguments:
|
|
559
|
+
path Path to scan (default: current directory)
|
|
560
|
+
|
|
561
|
+
Options:
|
|
562
|
+
-p, --path <path> Path to scan (alternative to positional argument)
|
|
563
|
+
-e, --extensions <ext> Comma-separated file extensions (default: js,jsx,ts,tsx)
|
|
564
|
+
-i, --ignore <patterns> Comma-separated patterns to ignore
|
|
565
|
+
(default: node_modules,dist,build,.git)
|
|
566
|
+
--json Output in JSON format for CI/CD integration
|
|
567
|
+
-h, --help Show this help message
|
|
568
|
+
-v, --version Show version number
|
|
569
|
+
|
|
570
|
+
Examples:
|
|
571
|
+
ui-doctor Scan current directory
|
|
572
|
+
ui-doctor ./src Scan the src directory
|
|
573
|
+
ui-doctor -e tsx,ts Only scan TypeScript files
|
|
574
|
+
ui-doctor --json Output results as JSON
|
|
575
|
+
ui-doctor ./src --json Scan src and output as JSON
|
|
576
|
+
`);
|
|
577
|
+
}
|
|
578
|
+
function printVersion() {
|
|
579
|
+
console.log(`ui-doctor v${VERSION}`);
|
|
580
|
+
}
|
|
581
|
+
function main() {
|
|
582
|
+
const args = process.argv.slice(2);
|
|
583
|
+
const options = parseArgs(args);
|
|
584
|
+
if (options.help) {
|
|
585
|
+
printHelp();
|
|
586
|
+
process.exit(0);
|
|
587
|
+
}
|
|
588
|
+
if (options.version) {
|
|
589
|
+
printVersion();
|
|
590
|
+
process.exit(0);
|
|
591
|
+
}
|
|
592
|
+
const startTime = Date.now();
|
|
593
|
+
let files;
|
|
594
|
+
try {
|
|
595
|
+
files = scanFiles(options.path, {
|
|
596
|
+
extensions: options.extensions,
|
|
597
|
+
ignore: options.ignore
|
|
598
|
+
});
|
|
599
|
+
} catch (error) {
|
|
600
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
601
|
+
console.error(`Error: ${message}`);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
const tokenMapper = createTokenMapper();
|
|
605
|
+
const filesWithViolations = [];
|
|
606
|
+
let totalViolations = 0;
|
|
607
|
+
for (const file of files) {
|
|
608
|
+
const matches = findColorClasses(file.content);
|
|
609
|
+
if (matches.length > 0) {
|
|
610
|
+
const violations = matches.map((match) => ({
|
|
611
|
+
match,
|
|
612
|
+
suggestions: tokenMapper.getSuggestions(match)
|
|
613
|
+
}));
|
|
614
|
+
filesWithViolations.push({
|
|
615
|
+
filePath: file.relativePath,
|
|
616
|
+
violations
|
|
617
|
+
});
|
|
618
|
+
totalViolations += violations.length;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const duration = Date.now() - startTime;
|
|
622
|
+
const report = {
|
|
623
|
+
filesScanned: files.length,
|
|
624
|
+
totalViolations,
|
|
625
|
+
filesWithViolations,
|
|
626
|
+
duration
|
|
627
|
+
};
|
|
628
|
+
printReport(report, options.json);
|
|
629
|
+
if (totalViolations > 0) {
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
main();
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-styles v8.
|
|
2
|
+
@versini/ui-styles v8.1.0
|
|
3
3
|
© 2025 gizmette.com
|
|
4
4
|
*/
|
|
5
5
|
try {
|
|
6
6
|
if (!window.__VERSINI_UI_STYLES__) {
|
|
7
7
|
window.__VERSINI_UI_STYLES__ = {
|
|
8
|
-
version: "8.
|
|
9
|
-
buildTime: "12/
|
|
8
|
+
version: "8.1.0",
|
|
9
|
+
buildTime: "12/21/2025 03:50 PM EST",
|
|
10
10
|
homepage: "https://www.npmjs.com/package/@versini/ui-styles",
|
|
11
11
|
license: "MIT",
|
|
12
12
|
};
|
|
@@ -109,6 +109,7 @@ import colors from "tailwindcss/colors";
|
|
|
109
109
|
"@versini/ui-button",
|
|
110
110
|
"@versini/ui-card",
|
|
111
111
|
"@versini/ui-debug-overlay",
|
|
112
|
+
"@versini/ui-dialog",
|
|
112
113
|
"@versini/ui-dropdown",
|
|
113
114
|
"@versini/ui-fingerprint",
|
|
114
115
|
"@versini/ui-footer",
|
|
@@ -117,6 +118,7 @@ import colors from "tailwindcss/colors";
|
|
|
117
118
|
"@versini/ui-icons",
|
|
118
119
|
"@versini/ui-liveregion",
|
|
119
120
|
"@versini/ui-main",
|
|
121
|
+
"@versini/ui-modal",
|
|
120
122
|
"@versini/ui-panel",
|
|
121
123
|
"@versini/ui-pill",
|
|
122
124
|
"@versini/ui-spinner",
|
|
@@ -129,7 +131,8 @@ import colors from "tailwindcss/colors";
|
|
|
129
131
|
"@versini/ui-togglegroup",
|
|
130
132
|
"@versini/ui-tooltip",
|
|
131
133
|
"@versini/ui-truncate",
|
|
132
|
-
"@versini/ui-types"
|
|
134
|
+
"@versini/ui-types",
|
|
135
|
+
"@versini/ui-utilities"
|
|
133
136
|
];
|
|
134
137
|
|
|
135
138
|
;// CONCATENATED MODULE: external "culori"
|
|
@@ -144,6 +147,9 @@ const accentColor = colors.violet["300"];
|
|
|
144
147
|
colors: {
|
|
145
148
|
/**
|
|
146
149
|
* Action tokens.
|
|
150
|
+
* To be used for background color for interactive elements
|
|
151
|
+
* like buttons, links, etc.
|
|
152
|
+
* @example bg-action-dark, hover:bg-action-dark-hover
|
|
147
153
|
*/ "action-dark": colors.slate["600"],
|
|
148
154
|
"action-dark-hover": colors.slate["700"],
|
|
149
155
|
"action-dark-active": colors.slate["800"],
|
|
@@ -153,14 +159,16 @@ const accentColor = colors.violet["300"];
|
|
|
153
159
|
"action-danger-dark": colors.red["900"],
|
|
154
160
|
"action-danger-dark-hover": colors.red["700"],
|
|
155
161
|
"action-danger-dark-active": colors.red["600"],
|
|
156
|
-
"action-danger-light": colors.red["
|
|
162
|
+
"action-danger-light": colors.red["900"],
|
|
157
163
|
"action-danger-light-hover": colors.red["700"],
|
|
158
|
-
"action-danger-light-active": colors.red["
|
|
164
|
+
"action-danger-light-active": colors.red["600"],
|
|
159
165
|
"action-selected-dark": colors.green["700"],
|
|
160
166
|
"action-selected-dark-hover": colors.green["600"],
|
|
161
167
|
"action-selected-dark-active": colors.green["500"],
|
|
162
168
|
/**
|
|
163
169
|
* Surface tokens.
|
|
170
|
+
* To be used for background colors of containers, cards, modals, etc.
|
|
171
|
+
* @example bg-surface-medium
|
|
164
172
|
*/ "surface-darker": colors.slate["900"],
|
|
165
173
|
"surface-dark": colors.slate["700"],
|
|
166
174
|
"surface-medium": colors.slate["500"],
|
|
@@ -174,6 +182,8 @@ const accentColor = colors.violet["300"];
|
|
|
174
182
|
"surface-error": colors.red["200"],
|
|
175
183
|
/**
|
|
176
184
|
* Typography tokens.
|
|
185
|
+
* To be used for text colors.
|
|
186
|
+
* @example text-copy-light, hover:text-copy-light-hover
|
|
177
187
|
*/ "copy-dark": colors.slate["900"],
|
|
178
188
|
"copy-dark-hover": colors.slate["900"],
|
|
179
189
|
"copy-dark-active": colors.slate["900"],
|
|
@@ -186,16 +196,19 @@ const accentColor = colors.violet["300"];
|
|
|
186
196
|
"copy-lighter": colors.white,
|
|
187
197
|
"copy-lighter-hover": colors.white,
|
|
188
198
|
"copy-lighter-active": colors.white,
|
|
199
|
+
"copy-error": colors.red["800"],
|
|
189
200
|
"copy-error-dark": colors.red["700"],
|
|
190
201
|
"copy-error-light": colors.red["500"],
|
|
191
|
-
"copy-information": colors.violet["800"],
|
|
192
202
|
"copy-success": colors.green["800"],
|
|
203
|
+
"copy-success-light": colors.green["500"],
|
|
204
|
+
"copy-information": colors.violet["800"],
|
|
193
205
|
"copy-warning": colors.orange["800"],
|
|
194
|
-
"copy-error": colors.red["800"],
|
|
195
206
|
"copy-accent": "#a9b9ad",
|
|
196
207
|
"copy-accent-dark": "#cde8d4",
|
|
197
208
|
/**
|
|
198
209
|
* Border tokens.
|
|
210
|
+
* To be used for border colors.
|
|
211
|
+
* @example border-border-medium
|
|
199
212
|
*/ "border-dark": colors.slate["900"],
|
|
200
213
|
"border-medium": colors.slate["400"],
|
|
201
214
|
"border-light": colors.slate["300"],
|
|
@@ -215,12 +228,16 @@ const accentColor = colors.violet["300"];
|
|
|
215
228
|
"border-error": colors.red["400"],
|
|
216
229
|
/**
|
|
217
230
|
* Focus tokens.
|
|
231
|
+
* To be used for focus outlines and rings.
|
|
232
|
+
* @example outline-focus-dark, focus:ring-focus-dark
|
|
218
233
|
*/ "focus-dark": colors.slate["400"],
|
|
219
234
|
"focus-light": colors.slate["400"],
|
|
220
235
|
"focus-error-dark": colors.red["700"],
|
|
221
236
|
"focus-error-light": errorColorLight,
|
|
222
237
|
/**
|
|
223
238
|
* Table tokens
|
|
239
|
+
* To be used for table row and header backgrounds.
|
|
240
|
+
* @example bg-table-dark-odd
|
|
224
241
|
*/ "table-head-dark": colors.gray["950"],
|
|
225
242
|
"table-dark": colors.gray["700"],
|
|
226
243
|
"table-dark-odd": colors.gray["800"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versini/ui-styles",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Arno Versini",
|
|
6
6
|
"publishConfig": {
|
|
@@ -14,16 +14,20 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "dist/index.js",
|
|
16
16
|
"types": "dist/index.d.ts",
|
|
17
|
+
"bin": {
|
|
18
|
+
"ui-doctor": "dist/cli/ui-doctor.js"
|
|
19
|
+
},
|
|
17
20
|
"files": [
|
|
18
21
|
"dist",
|
|
19
22
|
"README.md"
|
|
20
23
|
],
|
|
21
24
|
"scripts": {
|
|
22
25
|
"build:check": "tsc",
|
|
26
|
+
"build:cli": "vite build --config vite.cli.config.ts",
|
|
23
27
|
"build:js": "rslib build",
|
|
24
28
|
"build:types": "echo 'Types now built with rslib'",
|
|
25
29
|
"build:list": "node lib/buildComponentsList.js",
|
|
26
|
-
"build": "npm-run-all --serial clean build:list build:check build:js",
|
|
30
|
+
"build": "npm-run-all --serial clean build:list build:check build:js build:cli",
|
|
27
31
|
"clean": "rimraf dist tmp",
|
|
28
32
|
"dev:js": "rslib build --watch",
|
|
29
33
|
"dev:types": "echo 'Types now watched with rslib'",
|
|
@@ -42,5 +46,5 @@
|
|
|
42
46
|
"fs-extra": "11.3.2",
|
|
43
47
|
"tailwindcss": "4.1.18"
|
|
44
48
|
},
|
|
45
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "8824f7b91393041d0d23badb7a667c34c90fbc87"
|
|
46
50
|
}
|