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.
- package/README.md +111 -0
- package/dist/analyzer/color-parser.d.ts +18 -0
- package/dist/analyzer/color-parser.d.ts.map +1 -0
- package/dist/analyzer/color-parser.js +68 -0
- package/dist/analyzer/color-parser.js.map +1 -0
- package/dist/analyzer/consolidator.d.ts +13 -0
- package/dist/analyzer/consolidator.d.ts.map +1 -0
- package/dist/analyzer/consolidator.js +72 -0
- package/dist/analyzer/consolidator.js.map +1 -0
- package/dist/analyzer/similarity.d.ts +10 -0
- package/dist/analyzer/similarity.d.ts.map +1 -0
- package/dist/analyzer/similarity.js +72 -0
- package/dist/analyzer/similarity.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/scanner/color-extractor.d.ts +10 -0
- package/dist/scanner/color-extractor.d.ts.map +1 -0
- package/dist/scanner/color-extractor.js +98 -0
- package/dist/scanner/color-extractor.js.map +1 -0
- package/dist/scanner/file-walker.d.ts +2 -0
- package/dist/scanner/file-walker.d.ts.map +1 -0
- package/dist/scanner/file-walker.js +45 -0
- package/dist/scanner/file-walker.js.map +1 -0
- package/dist/server/api.d.ts +33 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +72 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/server.d.ts +4 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +41 -0
- package/dist/server/server.js.map +1 -0
- package/package.json +51 -0
- package/src/ui/app.jsx +277 -0
- 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'));
|