@versini/ui-styles 8.0.1 → 8.1.1

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 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,137 @@
1
+ import colors from "tailwindcss/colors";
2
+
3
+ const errorColorLight = "#ff3f3f";
4
+ const accentColor = colors.violet[300];
5
+
6
+ export default {
7
+ colors: {
8
+ /**
9
+ * Action tokens.
10
+ * To be used for background color for interactive elements
11
+ * like buttons, links, etc.
12
+ * @example bg-action-dark, hover:bg-action-dark-hover
13
+ */
14
+ "action-dark": colors.slate[600],
15
+ "action-dark-hover": colors.slate[700],
16
+ "action-dark-active": colors.slate[800],
17
+
18
+ "action-light": accentColor,
19
+ "action-light-hover": colors.violet[400],
20
+ "action-light-active": colors.violet[500],
21
+
22
+ "action-danger-dark": colors.red[900],
23
+ "action-danger-dark-hover": colors.red[700],
24
+ "action-danger-dark-active": colors.red[600],
25
+
26
+ "action-danger-light": colors.red[900],
27
+ "action-danger-light-hover": colors.red[700],
28
+ "action-danger-light-active": colors.red[600],
29
+
30
+ "action-selected-dark": colors.green[700],
31
+ "action-selected-dark-hover": colors.green[600],
32
+ "action-selected-dark-active": colors.green[500],
33
+
34
+ /**
35
+ * Surface tokens.
36
+ * To be used for background colors of containers, cards, modals, etc.
37
+ * @example bg-surface-medium
38
+ */
39
+ "surface-darker": colors.slate[900],
40
+ "surface-dark": colors.slate[700],
41
+ "surface-medium": colors.slate[500],
42
+ "surface-light": colors.slate[300],
43
+ "surface-lighter": colors.slate[200],
44
+ "surface-lightest": colors.slate[100],
45
+ "surface-accent": "#0071EB",
46
+ "surface-information": colors.violet[200],
47
+ "surface-success": colors.green[200],
48
+ "surface-warning": colors.orange[200],
49
+ "surface-error": colors.red[200],
50
+
51
+ /**
52
+ * Typography tokens.
53
+ * To be used for text colors.
54
+ * @example text-copy-light, hover:text-copy-light-hover
55
+ */
56
+ "copy-dark": colors.slate[900],
57
+ "copy-dark-hover": colors.slate[900],
58
+ "copy-dark-active": colors.slate[900],
59
+
60
+ "copy-medium": colors.slate[400],
61
+ "copy-medium-hover": colors.slate[400],
62
+ "copy-medium-active": colors.slate[400],
63
+
64
+ "copy-light": colors.slate[200],
65
+ "copy-light-hover": colors.slate[200],
66
+ "copy-light-active": colors.slate[200],
67
+
68
+ "copy-lighter": colors.white,
69
+ "copy-lighter-hover": colors.white,
70
+ "copy-lighter-active": colors.white,
71
+
72
+ "copy-error": colors.red[800],
73
+ "copy-error-dark": colors.red[700],
74
+ "copy-error-light": colors.red[500],
75
+
76
+ "copy-success": colors.green[800],
77
+ "copy-success-light": colors.green[500],
78
+
79
+ "copy-information": colors.violet[800],
80
+ "copy-warning": colors.orange[800],
81
+
82
+ "copy-accent": "#a9b9ad",
83
+ "copy-accent-dark": "#cde8d4",
84
+
85
+ /**
86
+ * Border tokens.
87
+ * To be used for border colors.
88
+ * @example border-border-medium
89
+ */
90
+ "border-dark": colors.slate[900],
91
+ "border-medium": colors.slate[400],
92
+ "border-light": colors.slate[300],
93
+ "border-accent": accentColor,
94
+
95
+ "border-danger-dark": colors.red[900],
96
+ "border-danger-medium": colors.red[400],
97
+ "border-danger-light": colors.red[300],
98
+
99
+ "border-selected-dark": colors.green[800],
100
+ "border-selected-medium": colors.green[400],
101
+ "border-selected-light": colors.green[300],
102
+
103
+ "border-white": colors.white,
104
+ "border-error-dark": colors.red[700],
105
+ "border-error-light": errorColorLight,
106
+
107
+ "border-information": colors.violet[400],
108
+ "border-success": colors.green[400],
109
+ "border-warning": colors.orange[400],
110
+ "border-error": colors.red[400],
111
+
112
+ /**
113
+ * Focus tokens.
114
+ * To be used for focus outlines and rings.
115
+ * @example outline-focus-dark, focus:ring-focus-dark
116
+ */
117
+ "focus-dark": colors.slate[400],
118
+ "focus-light": colors.slate[400],
119
+ "focus-error-dark": colors.red[700],
120
+ "focus-error-light": errorColorLight,
121
+
122
+ /**
123
+ * Table tokens
124
+ * To be used for table row and header backgrounds.
125
+ * @example bg-table-dark-odd
126
+ */
127
+ "table-head-dark": colors.gray[950],
128
+ "table-dark": colors.gray[700],
129
+ "table-dark-odd": colors.gray[800],
130
+ "table-dark-even": colors.gray[900],
131
+
132
+ "table-head-light": colors.gray[100],
133
+ "table-light": colors.gray[100],
134
+ "table-light-odd": colors.gray[200],
135
+ "table-light-even": colors.gray[300],
136
+ },
137
+ };
@@ -0,0 +1,630 @@
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 prodPath = resolve(__dirname, "tokens.ts");
293
+ if (existsSync(prodPath)) {
294
+ return prodPath;
295
+ }
296
+ const devPath = resolve(__dirname, "../plugins/tailwindcss/tokens.ts");
297
+ if (existsSync(devPath)) {
298
+ return devPath;
299
+ }
300
+ return devPath;
301
+ }
302
+ function parseVariables(content) {
303
+ const variables = /* @__PURE__ */ new Map();
304
+ const constPattern = /const\s+(\w+)\s*=\s*([^;]+);/g;
305
+ const matches = content.matchAll(constPattern);
306
+ for (const match of matches) {
307
+ const varName = match[1];
308
+ const varValue = match[2].trim();
309
+ const colorMatch = varValue.match(/colors\.(\w+)\[(\d+)\]/);
310
+ if (colorMatch) {
311
+ variables.set(varName, `colors.${colorMatch[1]}[${colorMatch[2]}]`);
312
+ } else if (varValue.startsWith('"') || varValue.startsWith("'")) {
313
+ variables.set(varName, varValue.replace(/["']/g, ""));
314
+ }
315
+ }
316
+ return variables;
317
+ }
318
+ function resolveTokenValue(value, variables) {
319
+ if (value.match(/colors\.\w+\[\d+\]/)) {
320
+ return value;
321
+ }
322
+ const resolved = variables.get(value);
323
+ if (resolved) {
324
+ return resolved;
325
+ }
326
+ return value;
327
+ }
328
+ function parseColorsObject(content, variables) {
329
+ const colors2 = {};
330
+ const colorsMatch = content.match(/colors:\s*\{([\s\S]*?)\n\t\}/);
331
+ if (!colorsMatch) {
332
+ return colors2;
333
+ }
334
+ const colorsContent = colorsMatch[1];
335
+ const tokenPattern = /"([^"]+)":\s*([^,\n]+)/g;
336
+ const matches = colorsContent.matchAll(tokenPattern);
337
+ for (const match of matches) {
338
+ const tokenName = match[1];
339
+ let tokenValue = match[2].trim();
340
+ tokenValue = tokenValue.replace(/,$/, "").trim();
341
+ const resolved = resolveTokenValue(tokenValue, variables);
342
+ colors2[tokenName] = resolved;
343
+ }
344
+ return colors2;
345
+ }
346
+ function parseTokensFile(filePath) {
347
+ const errors = [];
348
+ const tokens = { colors: {} };
349
+ const tokensPath = getTokensFilePath();
350
+ let content;
351
+ try {
352
+ content = readFileSync(tokensPath, "utf-8");
353
+ } catch (_error) {
354
+ errors.push(`Failed to read tokens file: ${tokensPath}`);
355
+ return { tokens, errors };
356
+ }
357
+ const variables = parseVariables(content);
358
+ tokens.colors = parseColorsObject(content, variables);
359
+ if (Object.keys(tokens.colors).length === 0) {
360
+ errors.push("No tokens found in the colors object");
361
+ }
362
+ return { tokens, errors };
363
+ }
364
+ function loadTokenDefinitions() {
365
+ const result = parseTokensFile();
366
+ if (result.errors.length > 0) {
367
+ throw new Error(`Token parsing errors: ${result.errors.join(", ")}`);
368
+ }
369
+ return result.tokens;
370
+ }
371
+ const PREFIX_TO_TOKEN_CATEGORY = {
372
+ bg: ["surface", "action", "table"],
373
+ text: ["copy"],
374
+ border: ["border"],
375
+ "border-t": ["border"],
376
+ "border-b": ["border"],
377
+ "border-l": ["border"],
378
+ "border-r": ["border"],
379
+ "border-x": ["border"],
380
+ "border-y": ["border"],
381
+ outline: ["border", "focus"],
382
+ ring: ["focus", "border"],
383
+ shadow: ["surface"],
384
+ accent: ["action", "surface"],
385
+ caret: ["copy"],
386
+ fill: ["copy", "surface"],
387
+ stroke: ["copy", "border"],
388
+ decoration: ["copy"],
389
+ divide: ["border"],
390
+ from: ["surface", "action"],
391
+ via: ["surface", "action"],
392
+ to: ["surface", "action"],
393
+ placeholder: ["copy"]
394
+ };
395
+ function buildColorKey(color, shade) {
396
+ return `${color}-${shade}`;
397
+ }
398
+ function parseTokenValue(value) {
399
+ const match = value.match(/colors\.(\w+)\[(\d+)\]/);
400
+ if (match) {
401
+ const color = match[1];
402
+ const shade = match[2];
403
+ return { color, shade };
404
+ }
405
+ return null;
406
+ }
407
+ class TokenMapper {
408
+ colorToTokens;
409
+ tokenDefinitions;
410
+ constructor(tokenDefinitions) {
411
+ this.tokenDefinitions = tokenDefinitions;
412
+ this.colorToTokens = /* @__PURE__ */ new Map();
413
+ this.buildLookupTable();
414
+ }
415
+ /**
416
+ * Build the reverse lookup table from colors to tokens.
417
+ */
418
+ buildLookupTable() {
419
+ for (const [tokenName, colorRef] of Object.entries(
420
+ this.tokenDefinitions.colors
421
+ )) {
422
+ const parsed = parseTokenValue(colorRef);
423
+ if (parsed) {
424
+ const key = buildColorKey(parsed.color, parsed.shade);
425
+ const existing = this.colorToTokens.get(key) || [];
426
+ existing.push({
427
+ tokenName,
428
+ colorReference: colorRef,
429
+ suggestedClass: "",
430
+ // Will be filled when suggesting
431
+ isExactMatch: true
432
+ });
433
+ this.colorToTokens.set(key, existing);
434
+ }
435
+ }
436
+ }
437
+ /**
438
+ * Get token suggestions for a raw color class match.
439
+ * Only returns tokens from semantically appropriate categories.
440
+ */
441
+ getSuggestions(match) {
442
+ const key = buildColorKey(match.color, match.shade);
443
+ const tokens = this.colorToTokens.get(key);
444
+ if (!tokens || tokens.length === 0) {
445
+ return [];
446
+ }
447
+ const preferredCategories = PREFIX_TO_TOKEN_CATEGORY[match.prefix] || [];
448
+ const variantPrefix = match.variants.length > 0 ? `${match.variants.join(":")}:` : "";
449
+ const suggestions = tokens.filter((token) => {
450
+ if (preferredCategories.length === 0) {
451
+ return true;
452
+ }
453
+ const category = this.getTokenCategory(token.tokenName);
454
+ return preferredCategories.includes(category);
455
+ }).map((token) => ({
456
+ ...token,
457
+ suggestedClass: `${variantPrefix}${match.prefix}-${token.tokenName}`
458
+ })).sort((a, b) => {
459
+ const aCategory = this.getTokenCategory(a.tokenName);
460
+ const bCategory = this.getTokenCategory(b.tokenName);
461
+ const aIndex = preferredCategories.indexOf(aCategory);
462
+ const bIndex = preferredCategories.indexOf(bCategory);
463
+ if (aIndex !== bIndex) {
464
+ return aIndex - bIndex;
465
+ }
466
+ return a.tokenName.localeCompare(b.tokenName);
467
+ });
468
+ return suggestions;
469
+ }
470
+ /**
471
+ * Get the category of a token (first part of the name).
472
+ */
473
+ getTokenCategory(tokenName) {
474
+ const parts = tokenName.split("-");
475
+ return parts[0];
476
+ }
477
+ /**
478
+ * Get all available tokens.
479
+ */
480
+ getAllTokens() {
481
+ return Object.keys(this.tokenDefinitions.colors);
482
+ }
483
+ }
484
+ function createTokenMapper() {
485
+ const tokenDefinitions = loadTokenDefinitions();
486
+ return new TokenMapper(tokenDefinitions);
487
+ }
488
+ const VERSION = "1.0.0";
489
+ function parseArgs(args) {
490
+ const defaults = getDefaultScanOptions();
491
+ const options = {
492
+ path: ".",
493
+ extensions: defaults.extensions,
494
+ ignore: defaults.ignore,
495
+ json: false,
496
+ help: false,
497
+ version: false
498
+ };
499
+ let i = 0;
500
+ while (i < args.length) {
501
+ const arg = args[i];
502
+ switch (arg) {
503
+ case "-h":
504
+ case "--help":
505
+ options.help = true;
506
+ break;
507
+ case "-v":
508
+ case "--version":
509
+ options.version = true;
510
+ break;
511
+ case "--json":
512
+ options.json = true;
513
+ break;
514
+ case "-p":
515
+ case "--path":
516
+ i++;
517
+ if (i < args.length) {
518
+ options.path = args[i];
519
+ }
520
+ break;
521
+ case "-e":
522
+ case "--extensions":
523
+ i++;
524
+ if (i < args.length) {
525
+ options.extensions = args[i].split(",").map((e) => e.trim());
526
+ }
527
+ break;
528
+ case "-i":
529
+ case "--ignore":
530
+ i++;
531
+ if (i < args.length) {
532
+ options.ignore = args[i].split(",").map((p) => p.trim());
533
+ }
534
+ break;
535
+ default:
536
+ if (!arg.startsWith("-")) {
537
+ options.path = arg;
538
+ }
539
+ break;
540
+ }
541
+ i++;
542
+ }
543
+ return options;
544
+ }
545
+ function printHelp() {
546
+ console.log(`
547
+ ui-doctor v${VERSION}
548
+
549
+ A CLI tool to detect raw Tailwind CSS color classes and suggest
550
+ semantic design tokens from @versini/ui-styles.
551
+
552
+ Usage:
553
+ ui-doctor [path] [options]
554
+
555
+ Arguments:
556
+ path Path to scan (default: current directory)
557
+
558
+ Options:
559
+ -p, --path <path> Path to scan (alternative to positional argument)
560
+ -e, --extensions <ext> Comma-separated file extensions (default: js,jsx,ts,tsx)
561
+ -i, --ignore <patterns> Comma-separated patterns to ignore
562
+ (default: node_modules,dist,build,.git)
563
+ --json Output in JSON format for CI/CD integration
564
+ -h, --help Show this help message
565
+ -v, --version Show version number
566
+
567
+ Examples:
568
+ ui-doctor Scan current directory
569
+ ui-doctor ./src Scan the src directory
570
+ ui-doctor -e tsx,ts Only scan TypeScript files
571
+ ui-doctor --json Output results as JSON
572
+ ui-doctor ./src --json Scan src and output as JSON
573
+ `);
574
+ }
575
+ function printVersion() {
576
+ console.log(`ui-doctor v${VERSION}`);
577
+ }
578
+ function main() {
579
+ const args = process.argv.slice(2);
580
+ const options = parseArgs(args);
581
+ if (options.help) {
582
+ printHelp();
583
+ process.exit(0);
584
+ }
585
+ if (options.version) {
586
+ printVersion();
587
+ process.exit(0);
588
+ }
589
+ const startTime = Date.now();
590
+ let files;
591
+ try {
592
+ files = scanFiles(options.path, {
593
+ extensions: options.extensions,
594
+ ignore: options.ignore
595
+ });
596
+ } catch (error) {
597
+ const message = error instanceof Error ? error.message : String(error);
598
+ console.error(`Error: ${message}`);
599
+ process.exit(1);
600
+ }
601
+ const tokenMapper = createTokenMapper();
602
+ const filesWithViolations = [];
603
+ let totalViolations = 0;
604
+ for (const file of files) {
605
+ const matches = findColorClasses(file.content);
606
+ if (matches.length > 0) {
607
+ const violations = matches.map((match) => ({
608
+ match,
609
+ suggestions: tokenMapper.getSuggestions(match)
610
+ }));
611
+ filesWithViolations.push({
612
+ filePath: file.relativePath,
613
+ violations
614
+ });
615
+ totalViolations += violations.length;
616
+ }
617
+ }
618
+ const duration = Date.now() - startTime;
619
+ const report = {
620
+ filesScanned: files.length,
621
+ totalViolations,
622
+ filesWithViolations,
623
+ duration
624
+ };
625
+ printReport(report, options.json);
626
+ if (totalViolations > 0) {
627
+ process.exit(1);
628
+ }
629
+ }
630
+ main();
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /*!
2
- @versini/ui-styles v8.0.1
2
+ @versini/ui-styles v8.1.1
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.0.1",
9
- buildTime: "12/15/2025 07:43 PM EST",
8
+ version: "8.1.1",
9
+ buildTime: "12/21/2025 04:51 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"],
@@ -161,6 +167,8 @@ const accentColor = colors.violet["300"];
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.0.1",
3
+ "version": "8.1.1",
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'",
@@ -39,8 +43,11 @@
39
43
  "@tailwindcss/container-queries": "0.1.1",
40
44
  "@tailwindcss/typography": "0.5.19",
41
45
  "culori": "4.0.2",
42
- "fs-extra": "11.3.2",
46
+ "fs-extra": "11.3.3",
43
47
  "tailwindcss": "4.1.18"
44
48
  },
45
- "gitHead": "699b13029f1d14089c1875bc50f049c1c7b9859a"
49
+ "devDependencies": {
50
+ "rollup-plugin-copy": "3.5.0"
51
+ },
52
+ "gitHead": "72dae70f6fb65a797e1abbca1afd471af34b804e"
46
53
  }