colx 1.0.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.
Files changed (36) hide show
  1. package/README.md +111 -0
  2. package/dist/analyzer/color-parser.d.ts +18 -0
  3. package/dist/analyzer/color-parser.d.ts.map +1 -0
  4. package/dist/analyzer/color-parser.js +68 -0
  5. package/dist/analyzer/color-parser.js.map +1 -0
  6. package/dist/analyzer/consolidator.d.ts +13 -0
  7. package/dist/analyzer/consolidator.d.ts.map +1 -0
  8. package/dist/analyzer/consolidator.js +72 -0
  9. package/dist/analyzer/consolidator.js.map +1 -0
  10. package/dist/analyzer/similarity.d.ts +10 -0
  11. package/dist/analyzer/similarity.d.ts.map +1 -0
  12. package/dist/analyzer/similarity.js +72 -0
  13. package/dist/analyzer/similarity.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +104 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/scanner/color-extractor.d.ts +10 -0
  19. package/dist/scanner/color-extractor.d.ts.map +1 -0
  20. package/dist/scanner/color-extractor.js +98 -0
  21. package/dist/scanner/color-extractor.js.map +1 -0
  22. package/dist/scanner/file-walker.d.ts +2 -0
  23. package/dist/scanner/file-walker.d.ts.map +1 -0
  24. package/dist/scanner/file-walker.js +45 -0
  25. package/dist/scanner/file-walker.js.map +1 -0
  26. package/dist/server/api.d.ts +33 -0
  27. package/dist/server/api.d.ts.map +1 -0
  28. package/dist/server/api.js +72 -0
  29. package/dist/server/api.js.map +1 -0
  30. package/dist/server/server.d.ts +4 -0
  31. package/dist/server/server.d.ts.map +1 -0
  32. package/dist/server/server.js +41 -0
  33. package/dist/server/server.js.map +1 -0
  34. package/package.json +51 -0
  35. package/src/ui/app.jsx +277 -0
  36. package/src/ui/index.html +341 -0
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findTsxJsxFiles = findTsxJsxFiles;
4
+ const promises_1 = require("fs/promises");
5
+ const path_1 = require("path");
6
+ const EXCLUDED_DIRS = ['node_modules', '.git', 'dist', 'build', '.next', '.turbo'];
7
+ async function findTsxJsxFiles(rootDir) {
8
+ const files = [];
9
+ const visited = new Set();
10
+ async function walk(dir) {
11
+ const normalizedPath = (0, path_1.join)(dir);
12
+ // Avoid infinite loops and excluded directories
13
+ if (visited.has(normalizedPath))
14
+ return;
15
+ visited.add(normalizedPath);
16
+ try {
17
+ const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
18
+ for (const entry of entries) {
19
+ const fullPath = (0, path_1.join)(dir, entry.name);
20
+ const normalizedFullPath = (0, path_1.join)(fullPath);
21
+ if (entry.isDirectory()) {
22
+ // Skip excluded directories
23
+ if (!EXCLUDED_DIRS.includes(entry.name)) {
24
+ await walk(normalizedFullPath);
25
+ }
26
+ }
27
+ else if (entry.isFile()) {
28
+ // Check for .tsx or .jsx extension
29
+ if (entry.name.endsWith('.tsx') || entry.name.endsWith('.jsx')) {
30
+ files.push(normalizedFullPath);
31
+ }
32
+ }
33
+ }
34
+ }
35
+ catch (error) {
36
+ // Skip directories we can't read (permissions, etc.)
37
+ if (error.code !== 'EACCES') {
38
+ console.warn(`Warning: Could not read directory ${dir}: ${error}`);
39
+ }
40
+ }
41
+ }
42
+ await walk(rootDir);
43
+ return files;
44
+ }
45
+ //# sourceMappingURL=file-walker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-walker.js","sourceRoot":"","sources":["../../src/scanner/file-walker.ts"],"names":[],"mappings":";;AAKA,0CAwCC;AA7CD,0CAA4C;AAC5C,+BAA4B;AAE5B,MAAM,aAAa,GAAG,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;AAE5E,KAAK,UAAU,eAAe,CAAC,OAAe;IACnD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAElC,KAAK,UAAU,IAAI,CAAC,GAAW;QAC7B,MAAM,cAAc,GAAG,IAAA,WAAI,EAAC,GAAG,CAAC,CAAC;QAEjC,gDAAgD;QAChD,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;YAAE,OAAO;QACxC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAE5B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,IAAA,kBAAO,EAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;YAE5D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC5B,MAAM,QAAQ,GAAG,IAAA,WAAI,EAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;gBACvC,MAAM,kBAAkB,GAAG,IAAA,WAAI,EAAC,QAAQ,CAAC,CAAC;gBAE1C,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;oBACxB,4BAA4B;oBAC5B,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;wBACxC,MAAM,IAAI,CAAC,kBAAkB,CAAC,CAAC;oBACjC,CAAC;gBACH,CAAC;qBAAM,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;oBAC1B,mCAAmC;oBACnC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;wBAC/D,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;oBACjC,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,qDAAqD;YACrD,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACvD,OAAO,CAAC,IAAI,CAAC,qCAAqC,GAAG,KAAK,KAAK,EAAE,CAAC,CAAC;YACrE,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC;IACpB,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { Express } from 'express';
2
+ import { ColorOccurrence } from '../scanner/color-extractor';
3
+ import { ParsedColor } from '../analyzer/color-parser';
4
+ import { SimilarColorGroup } from '../analyzer/similarity';
5
+ import { CSSVariableSuggestion } from '../analyzer/consolidator';
6
+ export interface ColorData {
7
+ id: string;
8
+ hex: string;
9
+ originalValue: string;
10
+ format: 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla';
11
+ occurrences: Array<{
12
+ file: string;
13
+ line: number;
14
+ className: string;
15
+ }>;
16
+ }
17
+ export interface SuggestionsResponse {
18
+ cssVariables: CSSVariableSuggestion[];
19
+ merges: Array<{
20
+ colors: string[];
21
+ suggestedColor: string;
22
+ similarity: number;
23
+ }>;
24
+ }
25
+ export interface StatsResponse {
26
+ totalOccurrences: number;
27
+ uniqueColors: number;
28
+ filesScanned: number;
29
+ formats: Record<string, number>;
30
+ }
31
+ export declare function setColorData(occurrences: ColorOccurrence[], parsedColors: Map<string, ParsedColor>, similarGroups: SimilarColorGroup[], cssVariables: CSSVariableSuggestion[]): void;
32
+ export declare function setupApiRoutes(app: Express): void;
33
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/server/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAc,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;IAChD,WAAW,EAAE,KAAK,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,qBAAqB,EAAE,CAAC;IACtC,MAAM,EAAE,KAAK,CAAC;QACZ,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,aAAa;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAMD,wBAAgB,YAAY,CAC1B,WAAW,EAAE,eAAe,EAAE,EAC9B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EACtC,aAAa,EAAE,iBAAiB,EAAE,EAClC,YAAY,EAAE,qBAAqB,EAAE,QAwDtC;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,QAiB1C"}
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setColorData = setColorData;
4
+ exports.setupApiRoutes = setupApiRoutes;
5
+ const color_parser_1 = require("../analyzer/color-parser");
6
+ let colorDataCache = [];
7
+ let suggestionsCache = null;
8
+ let statsCache = null;
9
+ function setColorData(occurrences, parsedColors, similarGroups, cssVariables) {
10
+ // Group occurrences by hex color
11
+ const colorMap = new Map();
12
+ for (const occurrence of occurrences) {
13
+ // Parse the color to get its hex value
14
+ const parsed = (0, color_parser_1.parseColor)(occurrence.originalValue, occurrence.format);
15
+ if (parsed) {
16
+ const hex = parsed.hex;
17
+ if (!colorMap.has(hex)) {
18
+ colorMap.set(hex, {
19
+ id: hex,
20
+ hex,
21
+ originalValue: parsed.originalValue,
22
+ format: parsed.format,
23
+ occurrences: []
24
+ });
25
+ }
26
+ colorMap.get(hex).occurrences.push({
27
+ file: occurrence.file,
28
+ line: occurrence.line,
29
+ className: occurrence.className
30
+ });
31
+ }
32
+ }
33
+ colorDataCache = Array.from(colorMap.values());
34
+ // Build suggestions
35
+ suggestionsCache = {
36
+ cssVariables,
37
+ merges: similarGroups.map(group => ({
38
+ colors: group.colors,
39
+ suggestedColor: group.suggestedColor,
40
+ similarity: group.averageSimilarity
41
+ }))
42
+ };
43
+ // Build stats
44
+ const uniqueFiles = new Set(occurrences.map(o => o.file));
45
+ const formatCounts = {};
46
+ for (const occurrence of occurrences) {
47
+ formatCounts[occurrence.format] = (formatCounts[occurrence.format] || 0) + 1;
48
+ }
49
+ statsCache = {
50
+ totalOccurrences: occurrences.length,
51
+ uniqueColors: colorDataCache.length,
52
+ filesScanned: uniqueFiles.size,
53
+ formats: formatCounts
54
+ };
55
+ }
56
+ function setupApiRoutes(app) {
57
+ app.get('/api/colors', (_req, res) => {
58
+ res.json(colorDataCache);
59
+ });
60
+ app.get('/api/suggestions', (_req, res) => {
61
+ res.json(suggestionsCache || { cssVariables: [], merges: [] });
62
+ });
63
+ app.get('/api/stats', (_req, res) => {
64
+ res.json(statsCache || {
65
+ totalOccurrences: 0,
66
+ uniqueColors: 0,
67
+ filesScanned: 0,
68
+ formats: {}
69
+ });
70
+ });
71
+ }
72
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/server/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { Express } from 'express';
2
+ export declare function createServer(port: number | undefined, uiDir: string): Express;
3
+ export declare function startServer(port: number | undefined, uiDir: string, shouldOpen?: boolean): Promise<void>;
4
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAK3C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,YAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAexE;AAED,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,YAAO,EACnB,KAAK,EAAE,MAAM,EACb,UAAU,GAAE,OAAc,GACzB,OAAO,CAAC,IAAI,CAAC,CAmBf"}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createServer = createServer;
7
+ exports.startServer = startServer;
8
+ const express_1 = __importDefault(require("express"));
9
+ const path_1 = require("path");
10
+ const api_1 = require("./api");
11
+ const open_1 = __importDefault(require("open"));
12
+ function createServer(port = 6969, uiDir) {
13
+ const app = (0, express_1.default)();
14
+ // Serve static files from UI directory
15
+ app.use(express_1.default.static(uiDir));
16
+ // Setup API routes
17
+ (0, api_1.setupApiRoutes)(app);
18
+ // Fallback to index.html for SPA routing
19
+ app.get('*', (_req, res) => {
20
+ res.sendFile((0, path_1.join)(uiDir, 'index.html'));
21
+ });
22
+ return app;
23
+ }
24
+ async function startServer(port = 6969, uiDir, shouldOpen = true) {
25
+ const app = createServer(port, uiDir);
26
+ return new Promise((resolve) => {
27
+ app.listen(port, () => {
28
+ const url = `http://localhost:${port}`;
29
+ console.log(`\n🚀 Server running at ${url}`);
30
+ console.log(`📊 Open your browser to view the color visualizer\n`);
31
+ if (shouldOpen) {
32
+ (0, open_1.default)(url).catch((err) => {
33
+ console.warn(`Could not open browser automatically: ${err}`);
34
+ console.log(`Please open ${url} manually`);
35
+ });
36
+ }
37
+ resolve();
38
+ });
39
+ });
40
+ }
41
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":";;;;;AAKA,oCAeC;AAED,kCAuBC;AA7CD,sDAA2C;AAC3C,+BAA4B;AAC5B,+BAAuC;AACvC,gDAAwB;AAExB,SAAgB,YAAY,CAAC,OAAe,IAAI,EAAE,KAAa;IAC7D,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;IAEtB,uCAAuC;IACvC,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/B,mBAAmB;IACnB,IAAA,oBAAc,EAAC,GAAG,CAAC,CAAC;IAEpB,yCAAyC;IACzC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QACzB,GAAG,CAAC,QAAQ,CAAC,IAAA,WAAI,EAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC;AAEM,KAAK,UAAU,WAAW,CAC/B,OAAe,IAAI,EACnB,KAAa,EACb,aAAsB,IAAI;IAE1B,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAEtC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;YACpB,MAAM,GAAG,GAAG,oBAAoB,IAAI,EAAE,CAAC;YACvC,OAAO,CAAC,GAAG,CAAC,0BAA0B,GAAG,EAAE,CAAC,CAAC;YAC7C,OAAO,CAAC,GAAG,CAAC,qDAAqD,CAAC,CAAC;YAEnE,IAAI,UAAU,EAAE,CAAC;gBACf,IAAA,cAAI,EAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;oBACtB,OAAO,CAAC,IAAI,CAAC,yCAAyC,GAAG,EAAE,CAAC,CAAC;oBAC7D,OAAO,CAAC,GAAG,CAAC,eAAe,GAAG,WAAW,CAAC,CAAC;gBAC7C,CAAC,CAAC,CAAC;YACL,CAAC;YAED,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "colx",
3
+ "version": "1.0.0",
4
+ "description": "Scan and visualize Tailwind arbitrary color values with CSS variable consolidation suggestions",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "colx": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "bun run src/index.ts",
12
+ "start": "node dist/index.js"
13
+ },
14
+ "keywords": [
15
+ "tailwind",
16
+ "tailwindcss",
17
+ "color",
18
+ "visualizer",
19
+ "css-variables",
20
+ "arbitrary-values"
21
+ ],
22
+ "author": "hellosatyajit <hellosatyajit@users.noreply.github.com>",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/hellosatyajit/colx.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/hellosatyajit/colx/issues"
30
+ },
31
+ "homepage": "https://github.com/hellosatyajit/colx#readme",
32
+ "engines": {
33
+ "node": ">=16"
34
+ },
35
+ "dependencies": {
36
+ "commander": "^11.1.0",
37
+ "express": "^4.18.2",
38
+ "chroma-js": "^2.4.2",
39
+ "open": "^10.1.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/chroma-js": "^3.1.2",
43
+ "@types/express": "^4.17.21",
44
+ "@types/node": "^20.10.6",
45
+ "typescript": "^5.3.3"
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "src/ui"
50
+ ]
51
+ }
package/src/ui/app.jsx ADDED
@@ -0,0 +1,277 @@
1
+ const { useState, useEffect } = React;
2
+
3
+ function ColorCard({ color, onClick }) {
4
+ return (
5
+ <div className="color-card" onClick={() => onClick(color)}>
6
+ <div
7
+ className="color-swatch"
8
+ style={{ backgroundColor: color.hex }}
9
+ />
10
+ <div className="color-info">
11
+ <div className="color-hex">{color.hex}</div>
12
+ <div className="color-format">{color.format}</div>
13
+ </div>
14
+ </div>
15
+ );
16
+ }
17
+
18
+ function ColorDetails({ color, onClose }) {
19
+ if (!color) return null;
20
+
21
+ return (
22
+ <div className="color-details active">
23
+ <div className="color-details-header">
24
+ <div
25
+ className="color-details-swatch"
26
+ style={{ backgroundColor: color.hex }}
27
+ />
28
+ <div className="color-details-info">
29
+ <h3>{color.hex}</h3>
30
+ <div className="color-format">{color.format.toUpperCase()}</div>
31
+ <div style={{ marginTop: '0.5rem', color: '#666', fontSize: '0.875rem' }}>
32
+ {color.occurrences.length} occurrence{color.occurrences.length !== 1 ? 's' : ''}
33
+ </div>
34
+ </div>
35
+ </div>
36
+ <div>
37
+ <h4 style={{ marginBottom: '0.75rem', fontSize: '0.875rem', color: '#666', textTransform: 'uppercase' }}>
38
+ Used in:
39
+ </h4>
40
+ <ul className="occurrences-list">
41
+ {color.occurrences.map((occ, idx) => (
42
+ <li key={idx} className="occurrence-item">
43
+ <span className="occurrence-file">{occ.file}</span>
44
+ <span className="occurrence-line">:{occ.line}</span>
45
+ <div style={{ marginTop: '0.25rem', color: '#999', fontSize: '0.75rem' }}>
46
+ {occ.className}
47
+ </div>
48
+ </li>
49
+ ))}
50
+ </ul>
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ function Suggestions({ suggestions, stats }) {
57
+ const copyToClipboard = (text) => {
58
+ navigator.clipboard.writeText(text).then(() => {
59
+ alert('Copied to clipboard!');
60
+ });
61
+ };
62
+
63
+ return (
64
+ <div className="suggestions">
65
+ <h2>Suggestions</h2>
66
+
67
+ {suggestions.merges.length > 0 && (
68
+ <div className="suggestion-section">
69
+ <h3>Similar Colors (Consider Merging)</h3>
70
+ {suggestions.merges.map((merge, idx) => (
71
+ <div key={idx} className="merge-group">
72
+ <div style={{ fontSize: '0.875rem', color: '#666', marginBottom: '0.5rem' }}>
73
+ These colors are very similar (Delta E: {merge.similarity.toFixed(2)})
74
+ </div>
75
+ <div className="merge-colors">
76
+ {merge.colors.map((color, colorIdx) => (
77
+ <div
78
+ key={colorIdx}
79
+ className="merge-color-swatch"
80
+ style={{ backgroundColor: color }}
81
+ title={color}
82
+ />
83
+ ))}
84
+ </div>
85
+ <div className="merge-suggested">
86
+ <span style={{ fontSize: '0.875rem', color: '#666' }}>Suggested merge:</span>
87
+ <div
88
+ className="merge-color-swatch"
89
+ style={{ backgroundColor: merge.suggestedColor }}
90
+ title={merge.suggestedColor}
91
+ />
92
+ <span style={{ fontFamily: 'Monaco, Courier New, monospace', fontSize: '0.875rem' }}>
93
+ {merge.suggestedColor}
94
+ </span>
95
+ </div>
96
+ </div>
97
+ ))}
98
+ </div>
99
+ )}
100
+
101
+ {suggestions.merges.length === 0 && (
102
+ <div style={{ color: '#666', textAlign: 'center', padding: '2rem' }}>
103
+ No suggestions at this time. All colors are unique and well-organized!
104
+ </div>
105
+ )}
106
+ </div>
107
+ );
108
+ }
109
+
110
+ function FilterBar({ colors, selectedFilter, onFilterChange }) {
111
+ // Extract unique utility prefixes from colors
112
+ const utilityPrefixes = new Set();
113
+ colors.forEach(color => {
114
+ color.occurrences.forEach(occ => {
115
+ // Extract prefix from className (e.g., "bg-[#ff5733]" -> "bg")
116
+ const match = occ.className.match(/^([a-z]+)-\[/);
117
+ if (match) {
118
+ utilityPrefixes.add(match[1]);
119
+ }
120
+ });
121
+ });
122
+
123
+ const filters = ['all', ...Array.from(utilityPrefixes).sort()];
124
+
125
+ return (
126
+ <div className="filter-container">
127
+ <div className="filter-title">Filter by Utility Class</div>
128
+ <div className="filter-buttons">
129
+ {filters.map(filter => (
130
+ <button
131
+ key={filter}
132
+ className={`filter-button ${selectedFilter === filter ? 'active' : ''}`}
133
+ onClick={() => onFilterChange(filter)}
134
+ >
135
+ {filter === 'all' ? 'All' : filter}
136
+ </button>
137
+ ))}
138
+ </div>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ function App() {
144
+ const [colors, setColors] = useState([]);
145
+ const [allColors, setAllColors] = useState([]);
146
+ const [suggestions, setSuggestions] = useState({ cssVariables: [], merges: [] });
147
+ const [stats, setStats] = useState(null);
148
+ const [selectedColor, setSelectedColor] = useState(null);
149
+ const [selectedFilter, setSelectedFilter] = useState('all');
150
+ const [loading, setLoading] = useState(true);
151
+ const [error, setError] = useState(null);
152
+
153
+ useEffect(() => {
154
+ async function fetchData() {
155
+ try {
156
+ const [colorsRes, suggestionsRes, statsRes] = await Promise.all([
157
+ fetch('/api/colors'),
158
+ fetch('/api/suggestions'),
159
+ fetch('/api/stats')
160
+ ]);
161
+
162
+ if (!colorsRes.ok || !suggestionsRes.ok || !statsRes.ok) {
163
+ throw new Error('Failed to fetch data');
164
+ }
165
+
166
+ const colorsData = await colorsRes.json();
167
+ const suggestionsData = await suggestionsRes.json();
168
+ const statsData = await statsRes.json();
169
+
170
+ setAllColors(colorsData);
171
+ setColors(colorsData);
172
+ setSuggestions(suggestionsData);
173
+ setStats(statsData);
174
+ setLoading(false);
175
+ } catch (err) {
176
+ setError(err.message);
177
+ setLoading(false);
178
+ }
179
+ }
180
+
181
+ fetchData();
182
+ }, []);
183
+
184
+ if (loading) {
185
+ return (
186
+ <div className="container">
187
+ <div className="loading">Loading color data...</div>
188
+ </div>
189
+ );
190
+ }
191
+
192
+ if (error) {
193
+ return (
194
+ <div className="container">
195
+ <div className="error">Error: {error}</div>
196
+ </div>
197
+ );
198
+ }
199
+
200
+ // Filter colors based on selected utility class
201
+ const handleFilterChange = (filter) => {
202
+ setSelectedFilter(filter);
203
+ if (filter === 'all') {
204
+ setColors(allColors);
205
+ } else {
206
+ const filtered = allColors.map(color => {
207
+ const filteredOccurrences = color.occurrences.filter(occ => {
208
+ const match = occ.className.match(/^([a-z]+)-\[/);
209
+ return match && match[1] === filter;
210
+ });
211
+ return {
212
+ ...color,
213
+ occurrences: filteredOccurrences
214
+ };
215
+ }).filter(color => color.occurrences.length > 0);
216
+ setColors(filtered);
217
+ }
218
+ };
219
+
220
+ return (
221
+ <div className="container">
222
+ <div className="header">
223
+ <h1>Tailwind Color Visualizer</h1>
224
+ <p>Visualize and analyze arbitrary color values in your Tailwind CSS project</p>
225
+ {stats && (
226
+ <div className="stats">
227
+ <div className="stat">
228
+ <div className="stat-label">Total Occurrences</div>
229
+ <div className="stat-value">{stats.totalOccurrences}</div>
230
+ </div>
231
+ <div className="stat">
232
+ <div className="stat-label">Unique Colors</div>
233
+ <div className="stat-value">{stats.uniqueColors}</div>
234
+ </div>
235
+ <div className="stat">
236
+ <div className="stat-label">Files Scanned</div>
237
+ <div className="stat-value">{stats.filesScanned}</div>
238
+ </div>
239
+ </div>
240
+ )}
241
+ </div>
242
+
243
+ <FilterBar
244
+ colors={allColors}
245
+ selectedFilter={selectedFilter}
246
+ onFilterChange={handleFilterChange}
247
+ />
248
+
249
+ {selectedColor && (
250
+ <ColorDetails
251
+ color={selectedColor}
252
+ onClose={() => setSelectedColor(null)}
253
+ />
254
+ )}
255
+
256
+ <div className="palette-grid">
257
+ {colors.map((color) => (
258
+ <ColorCard
259
+ key={color.id}
260
+ color={color}
261
+ onClick={setSelectedColor}
262
+ />
263
+ ))}
264
+ </div>
265
+
266
+ {colors.length === 0 && selectedFilter !== 'all' && (
267
+ <div style={{ textAlign: 'center', padding: '3rem', color: '#666' }}>
268
+ No colors found for "{selectedFilter}" utility class
269
+ </div>
270
+ )}
271
+
272
+ <Suggestions suggestions={suggestions} stats={stats} />
273
+ </div>
274
+ );
275
+ }
276
+
277
+ ReactDOM.render(<App />, document.getElementById('root'));