@trishchuk/coolors-mcp 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/.claude/settings.local.json +39 -0
- package/.env +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +73 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +71 -0
- package/.github/pull_request_template.md +97 -0
- package/.github/workflows/ci.yml +127 -0
- package/.github/workflows/deploy-docs.yml +56 -0
- package/.github/workflows/release.yml +99 -0
- package/.mcp.json +12 -0
- package/.prettierignore +1 -0
- package/CLAUDE.md +201 -0
- package/DOCUMENTATION.md +274 -0
- package/GEMINI.md +54 -0
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/demo/content_based_color.png +0 -0
- package/demo/music-player.html +621 -0
- package/demo/podcast-player.html +903 -0
- package/dist/bin/coolors-mcp.d.ts +1 -0
- package/dist/bin/coolors-mcp.js +154 -0
- package/dist/bin/coolors-mcp.js.map +1 -0
- package/dist/bin/server.d.ts +1 -0
- package/dist/bin/server.js +3292 -0
- package/dist/bin/server.js.map +1 -0
- package/dist/chunk-IQ7NN26V.js +114 -0
- package/dist/chunk-IQ7NN26V.js.map +1 -0
- package/dist/chunk-P3ARRKLS.js +1214 -0
- package/dist/chunk-P3ARRKLS.js.map +1 -0
- package/dist/color/index.d.ts +716 -0
- package/dist/color/index.js +153 -0
- package/dist/color/index.js.map +1 -0
- package/dist/coolors-mcp.d.ts +136 -0
- package/dist/coolors-mcp.js +7 -0
- package/dist/coolors-mcp.js.map +1 -0
- package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
- package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
- package/docs/.vitepress/cache/deps/_metadata.json +127 -0
- package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
- package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +12683 -0
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +7 -0
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +9719 -0
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +7 -0
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
- package/docs/.vitepress/cache/deps/cytoscape.js +30278 -0
- package/docs/.vitepress/cache/deps/cytoscape.js.map +7 -0
- package/docs/.vitepress/cache/deps/dayjs.js +285 -0
- package/docs/.vitepress/cache/deps/dayjs.js.map +7 -0
- package/docs/.vitepress/cache/deps/debug.js +468 -0
- package/docs/.vitepress/cache/deps/debug.js.map +7 -0
- package/docs/.vitepress/cache/deps/package.json +3 -0
- package/docs/.vitepress/cache/deps/prismjs.js +1466 -0
- package/docs/.vitepress/cache/deps/prismjs.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +228 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +142 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +27 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +65 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +53 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +73 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1146 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js +1814 -0
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
- package/docs/.vitepress/cache/deps/vue.js +344 -0
- package/docs/.vitepress/cache/deps/vue.js.map +7 -0
- package/docs/.vitepress/components/ClientGrid.vue +125 -0
- package/docs/.vitepress/components/CodeBlock.vue +231 -0
- package/docs/.vitepress/components/ConfigModal.vue +477 -0
- package/docs/.vitepress/components/DiagramModal.vue +528 -0
- package/docs/.vitepress/components/TroubleshootingModal.vue +472 -0
- package/docs/.vitepress/config.js +162 -0
- package/docs/.vitepress/theme/FundingLayout.vue +251 -0
- package/docs/.vitepress/theme/Layout.vue +134 -0
- package/docs/.vitepress/theme/components/AdBanner.vue +317 -0
- package/docs/.vitepress/theme/components/AdPlaceholder.vue +78 -0
- package/docs/.vitepress/theme/components/FundingEffects.vue +322 -0
- package/docs/.vitepress/theme/components/FundingHero.vue +345 -0
- package/docs/.vitepress/theme/components/SupportSection.vue +511 -0
- package/docs/.vitepress/theme/custom-app.css +339 -0
- package/docs/.vitepress/theme/custom.css +699 -0
- package/docs/.vitepress/theme/index.js +25 -0
- package/docs/README.md +198 -0
- package/docs/concepts/accessibility.md +473 -0
- package/docs/concepts/color-spaces.md +222 -0
- package/docs/concepts/distance-metrics.md +384 -0
- package/docs/concepts/hct.md +261 -0
- package/docs/concepts/image-analysis.md +396 -0
- package/docs/concepts/material-design.md +306 -0
- package/docs/concepts/theme-matching.md +399 -0
- package/docs/examples/basic-colors.md +490 -0
- package/docs/examples/creating-themes.md +898 -0
- package/docs/examples/css-refactoring.md +824 -0
- package/docs/examples/image-extraction.md +882 -0
- package/docs/getting-started.md +366 -0
- package/docs/index.md +190 -0
- package/docs/installation.md +157 -0
- package/docs/tools/README.md +234 -0
- package/docs/tools/accessibility.md +614 -0
- package/docs/tools/color-operations.md +374 -0
- package/docs/tools/image-extraction.md +624 -0
- package/docs/tools/material-design.md +347 -0
- package/docs/tools/theme-matching.md +552 -0
- package/eslint.config.ts +14 -0
- package/examples/theme-matching.md +113 -0
- package/jsr.json +7 -0
- package/mcp-config.json +8 -0
- package/note.md +35 -0
- package/package.json +122 -0
- package/research_results.md +53 -0
- package/src/bin/coolors-mcp.ts +194 -0
- package/src/bin/server.ts +61 -0
- package/src/color/__tests__/conversions-argb.test.ts +198 -0
- package/src/color/__tests__/extract-colors.test.ts +360 -0
- package/src/color/__tests__/image-utils.test.ts +242 -0
- package/src/color/__tests__/reference-colors.test.ts +278 -0
- package/src/color/__tests__/round-trip.test.ts +197 -0
- package/src/color/conversions.test.ts +402 -0
- package/src/color/conversions.ts +393 -0
- package/src/color/dislike/__tests__/dislike-analyzer.test.ts +248 -0
- package/src/color/dislike/dislike-analyzer.ts +114 -0
- package/src/color/extract-colors.ts +228 -0
- package/src/color/hct/__tests__/hct-class.test.ts +232 -0
- package/src/color/hct/harmonization.ts +204 -0
- package/src/color/hct/hct-class.ts +109 -0
- package/src/color/hct/hct-solver.ts +168 -0
- package/src/color/hct/index.ts +39 -0
- package/src/color/hct/tonal-palette.ts +211 -0
- package/src/color/hct/types.ts +88 -0
- package/src/color/image-utils.ts +79 -0
- package/src/color/index.ts +87 -0
- package/src/color/material-theme.ts +157 -0
- package/src/color/metrics.test.ts +276 -0
- package/src/color/metrics.ts +281 -0
- package/src/color/quantize/__tests__/quantizer_celebi.test.ts +195 -0
- package/src/color/quantize/lab_point_provider.ts +55 -0
- package/src/color/quantize/point_provider.ts +27 -0
- package/src/color/quantize/quantizer_celebi.ts +51 -0
- package/src/color/quantize/quantizer_celebi_test.ts +71 -0
- package/src/color/quantize/quantizer_map.ts +47 -0
- package/src/color/quantize/quantizer_wsmeans.ts +232 -0
- package/src/color/quantize/quantizer_wu.ts +472 -0
- package/src/color/score/__tests__/score.test.ts +224 -0
- package/src/color/score/score.ts +175 -0
- package/src/color/types.ts +151 -0
- package/src/color/utils/color_utils.ts +292 -0
- package/src/color/utils/math_utils.ts +145 -0
- package/src/color/utils.test.ts +403 -0
- package/src/color/utils.ts +315 -0
- package/src/constants.ts +5 -0
- package/src/coolors-mcp.ts +37 -0
- package/src/examples/addition.ts +333 -0
- package/src/examples/color-demo.ts +125 -0
- package/src/examples/custom-logger.ts +201 -0
- package/src/examples/oauth-server.ts +113 -0
- package/src/examples/session-context.ts +269 -0
- package/src/session.ts +116 -0
- package/src/theme/__tests__/matcher.test.ts +180 -0
- package/src/theme/__tests__/parser.test.ts +148 -0
- package/src/theme/__tests__/refactor.test.ts +224 -0
- package/src/theme/index.ts +34 -0
- package/src/theme/matcher.ts +395 -0
- package/src/theme/parser.ts +392 -0
- package/src/theme/refactor.ts +360 -0
- package/src/theme/types.ts +152 -0
- package/src/tools/__tests__/gradient-generator.test.ts +206 -0
- package/src/tools/__tests__/palette-with-locks.test.ts +109 -0
- package/src/tools/color-conversion.tool.ts +54 -0
- package/src/tools/color-distance.tool.ts +41 -0
- package/src/tools/colors.ts +31 -0
- package/src/tools/contrast-checker.tool.ts +37 -0
- package/src/tools/dislike-analyzer.tool.ts +247 -0
- package/src/tools/gradient-generator.tool.ts +250 -0
- package/src/tools/image-extraction.tools.ts +289 -0
- package/src/tools/index.ts +39 -0
- package/src/tools/material-theme.tools.ts +250 -0
- package/src/tools/palette-generator.tool.ts +135 -0
- package/src/tools/palette-with-locks.tool.ts +221 -0
- package/src/tools/registry.ts +142 -0
- package/src/tools/simple-tools.ts +37 -0
- package/src/tools/theme-matching.tools.ts +334 -0
- package/src/types.ts +182 -0
- package/src/utils.ts +22 -0
- package/tsconfig.json +8 -0
- package/vitest.config.js +15 -0
package/src/session.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ErrorCode,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
McpError,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { EventEmitter } from "events";
|
|
10
|
+
import { StrictEventEmitter } from "strict-event-emitter-types";
|
|
11
|
+
import { toJsonSchema } from "xsschema";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
CoolorsMCPSessionAuth,
|
|
15
|
+
CoolorsMCPSessionEvents,
|
|
16
|
+
ServerOptions,
|
|
17
|
+
Tool,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
|
|
20
|
+
export class CoolorsMCPSession extends (EventEmitter as {
|
|
21
|
+
new (): StrictEventEmitter<EventEmitter, CoolorsMCPSessionEvents>;
|
|
22
|
+
}) {
|
|
23
|
+
#server: Server;
|
|
24
|
+
#tools: Tool<CoolorsMCPSessionAuth>[];
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
options: ServerOptions<CoolorsMCPSessionAuth>,
|
|
28
|
+
tools: Tool<CoolorsMCPSessionAuth>[],
|
|
29
|
+
) {
|
|
30
|
+
super();
|
|
31
|
+
this.#server = new Server(
|
|
32
|
+
{ name: options.name, version: options.version },
|
|
33
|
+
{ capabilities: { tools: {} } },
|
|
34
|
+
);
|
|
35
|
+
this.#tools = tools;
|
|
36
|
+
this.setupToolHandlers();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async connect(transport: Transport) {
|
|
40
|
+
await this.#server.connect(transport);
|
|
41
|
+
this.emit("ready");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private setupToolHandlers() {
|
|
45
|
+
this.#server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
46
|
+
return {
|
|
47
|
+
tools: await Promise.all(
|
|
48
|
+
this.#tools.map(async (tool) => {
|
|
49
|
+
return {
|
|
50
|
+
annotations: tool.annotations,
|
|
51
|
+
description: tool.description,
|
|
52
|
+
inputSchema: tool.parameters
|
|
53
|
+
? await toJsonSchema(tool.parameters)
|
|
54
|
+
: {
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
properties: {},
|
|
57
|
+
type: "object",
|
|
58
|
+
},
|
|
59
|
+
name: tool.name,
|
|
60
|
+
};
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.#server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
67
|
+
const tool = this.#tools.find(
|
|
68
|
+
(tool) => tool.name === request.params.name,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (!tool) {
|
|
72
|
+
throw new McpError(
|
|
73
|
+
ErrorCode.MethodNotFound,
|
|
74
|
+
`Unknown tool: ${request.params.name}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let args: unknown = undefined;
|
|
79
|
+
|
|
80
|
+
if (tool.parameters) {
|
|
81
|
+
const parsed = await tool.parameters["~standard"].validate(
|
|
82
|
+
request.params.arguments,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (parsed.issues) {
|
|
86
|
+
const friendlyErrors = parsed.issues
|
|
87
|
+
.map((issue) => {
|
|
88
|
+
const path = issue.path?.join(".") || "root";
|
|
89
|
+
return `${path}: ${issue.message}`;
|
|
90
|
+
})
|
|
91
|
+
.join(", ");
|
|
92
|
+
|
|
93
|
+
throw new McpError(
|
|
94
|
+
ErrorCode.InvalidParams,
|
|
95
|
+
`Tool '${request.params.name}' parameter validation failed: ${friendlyErrors}. Please check the parameter types and values according to the tool's schema.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
args = parsed.value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = await tool.execute(
|
|
103
|
+
args as Parameters<typeof tool.execute>[0],
|
|
104
|
+
{} as CoolorsMCPSessionAuth,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (typeof result === "string") {
|
|
108
|
+
return {
|
|
109
|
+
content: [{ text: result, type: "text" }],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Theme Color Matcher
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import type { ThemeVariables } from "../types.js";
|
|
8
|
+
|
|
9
|
+
import { findBatchMatches, findClosestThemeColor } from "../matcher.js";
|
|
10
|
+
import { parseThemeVariables } from "../parser.js";
|
|
11
|
+
|
|
12
|
+
describe("Theme Color Matcher", () => {
|
|
13
|
+
// Sample theme for testing
|
|
14
|
+
const sampleThemeCSS = `
|
|
15
|
+
:root {
|
|
16
|
+
--color-primary-0: #000000;
|
|
17
|
+
--color-primary-10: #21005d;
|
|
18
|
+
--color-primary-20: #381e72;
|
|
19
|
+
--color-primary-30: #4f378b;
|
|
20
|
+
--color-primary-40: #6750a4;
|
|
21
|
+
--color-primary-50: #7f67be;
|
|
22
|
+
--color-primary-60: #9a82db;
|
|
23
|
+
--color-primary-70: #b69df8;
|
|
24
|
+
--color-primary-80: #d0bcff;
|
|
25
|
+
--color-primary-90: #eaddff;
|
|
26
|
+
--color-primary-95: #f6edff;
|
|
27
|
+
--color-primary-99: #fffbfe;
|
|
28
|
+
--color-primary-100: #ffffff;
|
|
29
|
+
|
|
30
|
+
--color-surface-0: #000000;
|
|
31
|
+
--color-surface-10: #1c1b1f;
|
|
32
|
+
--color-surface-90: #e6e1e5;
|
|
33
|
+
--color-surface-95: #f4eff4;
|
|
34
|
+
--color-surface-99: #fffbfe;
|
|
35
|
+
--color-surface-100: #ffffff;
|
|
36
|
+
|
|
37
|
+
--color-error-40: #ba1a1a;
|
|
38
|
+
--color-error-90: #ffdad6;
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const themeVariables: ThemeVariables = parseThemeVariables(sampleThemeCSS);
|
|
43
|
+
|
|
44
|
+
describe("findClosestThemeColor", () => {
|
|
45
|
+
it("should find exact match", () => {
|
|
46
|
+
const match = findClosestThemeColor("#6750a4", themeVariables);
|
|
47
|
+
|
|
48
|
+
expect(match).toBeDefined();
|
|
49
|
+
expect(match?.variable).toBe("--color-primary-40");
|
|
50
|
+
expect(match?.confidence).toBeGreaterThan(95);
|
|
51
|
+
expect(match?.distance).toBeLessThan(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should find close match", () => {
|
|
55
|
+
const match = findClosestThemeColor("#6850a0", themeVariables);
|
|
56
|
+
|
|
57
|
+
expect(match).toBeDefined();
|
|
58
|
+
expect(match?.variable).toBe("--color-primary-40");
|
|
59
|
+
expect(match?.confidence).toBeGreaterThan(70);
|
|
60
|
+
expect(match?.distance).toBeLessThan(10);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should return null for colors too far from theme", () => {
|
|
64
|
+
const match = findClosestThemeColor("#00ff00", themeVariables, {
|
|
65
|
+
maxDistance: 20,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(match).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should use context for better matching", () => {
|
|
72
|
+
const textMatch = findClosestThemeColor("#1a1a1a", themeVariables, {
|
|
73
|
+
contextType: "text",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const bgMatch = findClosestThemeColor("#f5f5f5", themeVariables, {
|
|
77
|
+
contextType: "background",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(textMatch).toBeDefined();
|
|
81
|
+
expect(bgMatch).toBeDefined();
|
|
82
|
+
|
|
83
|
+
// Text should prefer darker colors
|
|
84
|
+
if (textMatch?.variable.includes("surface")) {
|
|
85
|
+
expect(textMatch.variable).toMatch(/-(0|10)/);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Background should prefer lighter colors
|
|
89
|
+
if (bgMatch?.variable.includes("surface")) {
|
|
90
|
+
expect(bgMatch.variable).toMatch(/-(90|95|99|100)/);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should provide alternatives", () => {
|
|
95
|
+
const match = findClosestThemeColor("#5040a0", themeVariables);
|
|
96
|
+
|
|
97
|
+
expect(match).toBeDefined();
|
|
98
|
+
expect(match?.alternatives).toBeDefined();
|
|
99
|
+
expect(match?.alternatives.length).toBeGreaterThan(0);
|
|
100
|
+
expect(match?.alternatives.length).toBeLessThanOrEqual(3);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should detect semantic role", () => {
|
|
104
|
+
const match = findClosestThemeColor("#ba1a1a", themeVariables);
|
|
105
|
+
|
|
106
|
+
expect(match).toBeDefined();
|
|
107
|
+
expect(match?.semanticRole).toBe("error");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should calculate accessibility info for text context", () => {
|
|
111
|
+
const match = findClosestThemeColor("#000000", themeVariables, {
|
|
112
|
+
contextType: "text",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(match).toBeDefined();
|
|
116
|
+
expect(match?.accessibilityInfo).toBeDefined();
|
|
117
|
+
expect(match?.accessibilityInfo?.contrastWithBackground).toBeGreaterThan(
|
|
118
|
+
0,
|
|
119
|
+
);
|
|
120
|
+
expect(match?.accessibilityInfo?.meetsAA).toBeDefined();
|
|
121
|
+
expect(match?.accessibilityInfo?.meetsAAA).toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should respect preferred role", () => {
|
|
125
|
+
const match = findClosestThemeColor("#d0bcff", themeVariables, {
|
|
126
|
+
preferredRole: "primary",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(match).toBeDefined();
|
|
130
|
+
expect(match?.variable).toBe("--color-primary-80");
|
|
131
|
+
expect(match?.semanticRole).toBe("primary");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("findBatchMatches", () => {
|
|
136
|
+
it("should match multiple colors", () => {
|
|
137
|
+
const colors = ["#6750a4", "#ba1a1a", "#ffffff"];
|
|
138
|
+
const matches = findBatchMatches(colors, themeVariables);
|
|
139
|
+
|
|
140
|
+
expect(matches.size).toBe(3);
|
|
141
|
+
|
|
142
|
+
const primaryMatch = matches.get("#6750a4");
|
|
143
|
+
expect(primaryMatch).toBeDefined();
|
|
144
|
+
expect(primaryMatch?.variable).toBe("--color-primary-40");
|
|
145
|
+
|
|
146
|
+
const errorMatch = matches.get("#ba1a1a");
|
|
147
|
+
expect(errorMatch).toBeDefined();
|
|
148
|
+
expect(errorMatch?.variable).toBe("--color-error-40");
|
|
149
|
+
|
|
150
|
+
const whiteMatch = matches.get("#ffffff");
|
|
151
|
+
expect(whiteMatch).toBeDefined();
|
|
152
|
+
expect(whiteMatch?.variable).toMatch(/-(100|99)$/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should handle invalid colors", () => {
|
|
156
|
+
const colors = ["#6750a4", "invalid", "#ffffff"];
|
|
157
|
+
const matches = findBatchMatches(colors, themeVariables);
|
|
158
|
+
|
|
159
|
+
expect(matches.size).toBe(3);
|
|
160
|
+
expect(matches.get("invalid")).toBeNull();
|
|
161
|
+
expect(matches.get("#6750a4")).toBeDefined();
|
|
162
|
+
expect(matches.get("#ffffff")).toBeDefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should apply context to all colors", () => {
|
|
166
|
+
const colors = ["#1c1b1f", "#e6e1e5"];
|
|
167
|
+
const matches = findBatchMatches(colors, themeVariables, {
|
|
168
|
+
contextType: "text",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(matches.size).toBe(2);
|
|
172
|
+
|
|
173
|
+
const darkMatch = matches.get("#1c1b1f");
|
|
174
|
+
const lightMatch = matches.get("#e6e1e5");
|
|
175
|
+
|
|
176
|
+
expect(darkMatch).toBeDefined();
|
|
177
|
+
expect(lightMatch).toBeDefined();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Theme Parser
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
parseThemeVariables,
|
|
9
|
+
parseThemeVariablesFromObject,
|
|
10
|
+
} from "../parser.js";
|
|
11
|
+
|
|
12
|
+
describe("Theme Parser", () => {
|
|
13
|
+
describe("parseThemeVariables", () => {
|
|
14
|
+
it("should parse CSS custom properties", () => {
|
|
15
|
+
const css = `
|
|
16
|
+
:root {
|
|
17
|
+
--color-primary-0: #000000;
|
|
18
|
+
--color-primary-50: #1976d2;
|
|
19
|
+
--color-primary-100: #ffffff;
|
|
20
|
+
--color-secondary-50: #dc004e;
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const result = parseThemeVariables(css);
|
|
25
|
+
|
|
26
|
+
expect(result.primary).toBeDefined();
|
|
27
|
+
expect(result.primary).toHaveLength(3);
|
|
28
|
+
expect(result.primary![0].name).toBe("--color-primary-0");
|
|
29
|
+
expect(result.primary![0].value).toBe("#000000");
|
|
30
|
+
expect(result.primary![0].tone).toBeCloseTo(0, 1);
|
|
31
|
+
|
|
32
|
+
expect(result.secondary).toBeDefined();
|
|
33
|
+
expect(result.secondary).toHaveLength(1);
|
|
34
|
+
expect(result.secondary![0].name).toBe("--color-secondary-50");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should handle different color formats", () => {
|
|
38
|
+
const css = `
|
|
39
|
+
:root {
|
|
40
|
+
--primary: #ff0000;
|
|
41
|
+
--secondary: rgb(0, 255, 0);
|
|
42
|
+
--tertiary: hsl(240, 100%, 50%);
|
|
43
|
+
--neutral: gray;
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const result = parseThemeVariables(css);
|
|
48
|
+
|
|
49
|
+
expect(result.primary).toBeDefined();
|
|
50
|
+
expect(result.primary![0].value).toBe("#ff0000");
|
|
51
|
+
|
|
52
|
+
expect(result.secondary).toBeDefined();
|
|
53
|
+
expect(result.secondary![0].value).toBe("#00ff00");
|
|
54
|
+
|
|
55
|
+
expect(result.tertiary).toBeDefined();
|
|
56
|
+
expect(result.tertiary![0].value).toBe("#0000ff");
|
|
57
|
+
|
|
58
|
+
expect(result.neutral).toBeDefined();
|
|
59
|
+
expect(result.neutral![0].value).toBe("#808080");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should detect semantic roles from variable names", () => {
|
|
63
|
+
const css = `
|
|
64
|
+
:root {
|
|
65
|
+
--primary-color: #1976d2;
|
|
66
|
+
--secondary-accent: #dc004e;
|
|
67
|
+
--error-red: #d32f2f;
|
|
68
|
+
--surface-light: #f5f5f5;
|
|
69
|
+
--background-dark: #121212;
|
|
70
|
+
--border-gray: #e0e0e0;
|
|
71
|
+
--shadow-color: rgba(0,0,0,0.2);
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
const result = parseThemeVariables(css);
|
|
76
|
+
|
|
77
|
+
expect(result.primary).toBeDefined();
|
|
78
|
+
expect(result.primary![0].role).toBe("primary");
|
|
79
|
+
|
|
80
|
+
expect(result.secondary).toBeDefined();
|
|
81
|
+
expect(result.secondary![0].role).toBe("secondary");
|
|
82
|
+
|
|
83
|
+
expect(result.error).toBeDefined();
|
|
84
|
+
expect(result.error![0].role).toBe("error");
|
|
85
|
+
|
|
86
|
+
expect(result.surface).toBeDefined();
|
|
87
|
+
expect(result.surface![0].role).toBe("surface");
|
|
88
|
+
|
|
89
|
+
expect(result.background).toBeDefined();
|
|
90
|
+
expect(result.background![0].role).toBe("background");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should organize custom roles", () => {
|
|
94
|
+
const css = `
|
|
95
|
+
:root {
|
|
96
|
+
--color-brand-0: #000000;
|
|
97
|
+
--color-brand-50: #1976d2;
|
|
98
|
+
--color-brand-100: #ffffff;
|
|
99
|
+
--accent-warm: #ff6b6b;
|
|
100
|
+
--random-color: #123456;
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
const result = parseThemeVariables(css);
|
|
105
|
+
|
|
106
|
+
expect(result.custom).toBeDefined();
|
|
107
|
+
expect(result.custom!["brand"]).toBeDefined();
|
|
108
|
+
expect(result.custom!["brand"]).toHaveLength(3);
|
|
109
|
+
// accent-warm will be parsed as 'accent'
|
|
110
|
+
expect(result.custom!["accent"]).toBeDefined();
|
|
111
|
+
expect(result.custom!["uncategorized"]).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should sort variables by tone", () => {
|
|
115
|
+
const css = `
|
|
116
|
+
:root {
|
|
117
|
+
--color-primary-90: #e1f5fe;
|
|
118
|
+
--color-primary-10: #01579b;
|
|
119
|
+
--color-primary-50: #1976d2;
|
|
120
|
+
}
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
const result = parseThemeVariables(css);
|
|
124
|
+
|
|
125
|
+
expect(result.primary).toBeDefined();
|
|
126
|
+
expect(result.primary![0].tone).toBeLessThan(result.primary![1].tone);
|
|
127
|
+
expect(result.primary![1].tone).toBeLessThan(result.primary![2].tone);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("parseThemeVariablesFromObject", () => {
|
|
132
|
+
it("should parse variables from object", () => {
|
|
133
|
+
const variables = {
|
|
134
|
+
"--color-primary-50": "#1976d2",
|
|
135
|
+
"--color-secondary-50": "#dc004e",
|
|
136
|
+
"--not-a-color": "16px",
|
|
137
|
+
"not-a-variable": "#000000",
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const result = parseThemeVariablesFromObject(variables);
|
|
141
|
+
|
|
142
|
+
expect(result.primary).toBeDefined();
|
|
143
|
+
expect(result.primary).toHaveLength(1);
|
|
144
|
+
expect(result.secondary).toBeDefined();
|
|
145
|
+
expect(result.secondary).toHaveLength(1);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CSS Refactoring Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { parseThemeVariables } from "../parser.js";
|
|
8
|
+
import {
|
|
9
|
+
generateRefactoringReport,
|
|
10
|
+
refactorColor,
|
|
11
|
+
refactorCss,
|
|
12
|
+
} from "../refactor.js";
|
|
13
|
+
|
|
14
|
+
describe("CSS Refactoring", () => {
|
|
15
|
+
const themeCSS = `
|
|
16
|
+
:root {
|
|
17
|
+
--color-primary-40: #6750a4;
|
|
18
|
+
--color-primary-60: #9a82db;
|
|
19
|
+
--color-primary-80: #d0bcff;
|
|
20
|
+
--color-surface-10: #1c1b1f;
|
|
21
|
+
--color-surface-90: #e6e1e5;
|
|
22
|
+
--color-surface-99: #fffbfe;
|
|
23
|
+
--color-error-40: #ba1a1a;
|
|
24
|
+
--color-neutral-50: #79747e;
|
|
25
|
+
--color-outline-50: #79747e;
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const themeVariables = parseThemeVariables(themeCSS);
|
|
30
|
+
|
|
31
|
+
describe("refactorCss", () => {
|
|
32
|
+
it("should refactor hex colors", () => {
|
|
33
|
+
const css = `.button {
|
|
34
|
+
background-color: #6750a4;
|
|
35
|
+
color: #fffbfe;
|
|
36
|
+
border: 1px solid #79747e;
|
|
37
|
+
}`;
|
|
38
|
+
|
|
39
|
+
const result = refactorCss(css, themeVariables);
|
|
40
|
+
|
|
41
|
+
expect(result.statistics.totalColors).toBe(3);
|
|
42
|
+
expect(result.statistics.replacedColors).toBe(3);
|
|
43
|
+
expect(result.refactored).toContain("var(--color-primary-40)");
|
|
44
|
+
expect(result.refactored).toContain("var(--color-surface-99)");
|
|
45
|
+
expect(result.refactored).toContain("var(--color-");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should preserve original colors as comments when requested", () => {
|
|
49
|
+
const css = `.button { background: #6750a4; }`;
|
|
50
|
+
|
|
51
|
+
const result = refactorCss(css, themeVariables, {
|
|
52
|
+
addComments: true,
|
|
53
|
+
preserveOriginal: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result.refactored).toContain("/* #6750a4 */");
|
|
57
|
+
expect(result.refactored).toContain("var(--color-primary-40)");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should not preserve original when disabled", () => {
|
|
61
|
+
const css = `.button { background: #6750a4; }`;
|
|
62
|
+
|
|
63
|
+
const result = refactorCss(css, themeVariables, {
|
|
64
|
+
addComments: false,
|
|
65
|
+
preserveOriginal: false,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(result.refactored).not.toContain("/* #6750a4 */");
|
|
69
|
+
expect(result.refactored).toContain("var(--color-primary-40)");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should handle rgb colors", () => {
|
|
73
|
+
const css = `.text { color: rgb(28, 27, 31); }`;
|
|
74
|
+
|
|
75
|
+
// Lower confidence threshold since HCT conversion may cause slight differences
|
|
76
|
+
const result = refactorCss(css, themeVariables, { minConfidence: 60 });
|
|
77
|
+
|
|
78
|
+
expect(result.statistics.totalColors).toBe(1);
|
|
79
|
+
expect(result.statistics.replacedColors).toBe(1);
|
|
80
|
+
expect(result.refactored).toContain("var(--color-surface-10)");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should respect minimum confidence", () => {
|
|
84
|
+
const css = `.button { background: #5040a0; }`; // Similar but not exact
|
|
85
|
+
|
|
86
|
+
const result = refactorCss(css, themeVariables, {
|
|
87
|
+
minConfidence: 95,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(result.statistics.replacedColors).toBe(0);
|
|
91
|
+
expect(result.warnings.length).toBeGreaterThan(0);
|
|
92
|
+
expect(result.warnings[0].type).toBe("low-confidence");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should generate warnings for no matches", () => {
|
|
96
|
+
const css = `.button { background: #00ff00; }`; // Green - not in theme
|
|
97
|
+
|
|
98
|
+
const result = refactorCss(css, themeVariables);
|
|
99
|
+
|
|
100
|
+
expect(result.statistics.replacedColors).toBe(0);
|
|
101
|
+
expect(result.warnings.length).toBe(1);
|
|
102
|
+
expect(result.warnings[0].type).toBe("no-match");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should use context from CSS properties", () => {
|
|
106
|
+
const css = `
|
|
107
|
+
.element {
|
|
108
|
+
color: #1c1b1f;
|
|
109
|
+
background-color: #fffbfe;
|
|
110
|
+
border-color: #79747e;
|
|
111
|
+
box-shadow: 0 2px 4px #1c1b1f;
|
|
112
|
+
}
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
const result = refactorCss(css, themeVariables);
|
|
116
|
+
|
|
117
|
+
expect(result.statistics.replacedColors).toBe(4);
|
|
118
|
+
expect(result.replacements[0].context).toBe("text");
|
|
119
|
+
expect(result.replacements[1].context).toBe("background");
|
|
120
|
+
expect(result.replacements[2].context).toBe("border");
|
|
121
|
+
expect(result.replacements[3].context).toBe("shadow");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should add summary comment when requested", () => {
|
|
125
|
+
const css = `.button { background: #6750a4; }`;
|
|
126
|
+
|
|
127
|
+
const result = refactorCss(css, themeVariables, {
|
|
128
|
+
addComments: true,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(result.refactored).toContain(
|
|
132
|
+
"/* CSS Refactored with Theme Variables",
|
|
133
|
+
);
|
|
134
|
+
expect(result.refactored).toContain("Total colors found:");
|
|
135
|
+
expect(result.refactored).toContain("Colors replaced:");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should track line and column positions", () => {
|
|
139
|
+
const css = `.button {\n background: #6750a4;\n color: #fffbfe;\n}`;
|
|
140
|
+
|
|
141
|
+
const result = refactorCss(css, themeVariables);
|
|
142
|
+
|
|
143
|
+
expect(result.replacements[0].line).toBe(2);
|
|
144
|
+
expect(result.replacements[1].line).toBe(3);
|
|
145
|
+
expect(result.replacements[0].column).toBeGreaterThan(0);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("refactorColor", () => {
|
|
150
|
+
it("should refactor single color", () => {
|
|
151
|
+
const result = refactorColor("#6750a4", themeVariables);
|
|
152
|
+
|
|
153
|
+
expect(result.variable).toBe("--color-primary-40");
|
|
154
|
+
expect(result.confidence).toBeGreaterThan(95);
|
|
155
|
+
expect(result.alternatives).toBeDefined();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should return null for no match", () => {
|
|
159
|
+
const result = refactorColor("#00ff00", themeVariables);
|
|
160
|
+
|
|
161
|
+
expect(result.variable).toBeNull();
|
|
162
|
+
expect(result.confidence).toBe(0);
|
|
163
|
+
expect(result.alternatives).toHaveLength(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should use context when provided", () => {
|
|
167
|
+
const result = refactorColor("#79747e", themeVariables, "border");
|
|
168
|
+
|
|
169
|
+
expect(result.variable).toBe("--color-outline-50");
|
|
170
|
+
expect(result.confidence).toBeGreaterThan(0);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("generateRefactoringReport", () => {
|
|
175
|
+
it("should generate markdown report", () => {
|
|
176
|
+
const css = `
|
|
177
|
+
.button {
|
|
178
|
+
background: #6750a4;
|
|
179
|
+
color: #fffbfe;
|
|
180
|
+
border: 1px solid #00ff00;
|
|
181
|
+
}
|
|
182
|
+
`;
|
|
183
|
+
|
|
184
|
+
const result = refactorCss(css, themeVariables);
|
|
185
|
+
const report = generateRefactoringReport(result);
|
|
186
|
+
|
|
187
|
+
expect(report).toContain("# CSS Refactoring Report");
|
|
188
|
+
expect(report).toContain("## Summary");
|
|
189
|
+
expect(report).toContain("Total colors found:");
|
|
190
|
+
expect(report).toContain("## Replacements");
|
|
191
|
+
expect(report).toContain("## Warnings");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should group replacements by confidence", () => {
|
|
195
|
+
const css = `
|
|
196
|
+
.high { color: #6750a4; }
|
|
197
|
+
.medium { color: #6751a5; }
|
|
198
|
+
.low { color: #5040a0; }
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
const result = refactorCss(css, themeVariables, {
|
|
202
|
+
minConfidence: 30,
|
|
203
|
+
});
|
|
204
|
+
const report = generateRefactoringReport(result);
|
|
205
|
+
|
|
206
|
+
expect(report).toMatch(
|
|
207
|
+
/High Confidence|Medium Confidence|Low Confidence/,
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should group warnings by type", () => {
|
|
212
|
+
const css = `
|
|
213
|
+
.no-match { color: #00ff00; }
|
|
214
|
+
.low-conf { color: #123456; }
|
|
215
|
+
`;
|
|
216
|
+
|
|
217
|
+
const result = refactorCss(css, themeVariables);
|
|
218
|
+
const report = generateRefactoringReport(result);
|
|
219
|
+
|
|
220
|
+
expect(report).toContain("No Matches Found");
|
|
221
|
+
expect(report).toMatch(/Low Confidence|No Matches/);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Module
|
|
3
|
+
* Export all theme-related functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Matcher
|
|
7
|
+
export { findBatchMatches, findClosestThemeColor } from "./matcher.js";
|
|
8
|
+
|
|
9
|
+
// Parser
|
|
10
|
+
export {
|
|
11
|
+
parseThemeVariables,
|
|
12
|
+
parseThemeVariablesFromObject,
|
|
13
|
+
} from "./parser.js";
|
|
14
|
+
|
|
15
|
+
// Refactor
|
|
16
|
+
export {
|
|
17
|
+
generateRefactoringReport,
|
|
18
|
+
refactorColor,
|
|
19
|
+
refactorCss,
|
|
20
|
+
} from "./refactor.js";
|
|
21
|
+
|
|
22
|
+
// Types
|
|
23
|
+
export type {
|
|
24
|
+
ColorContext,
|
|
25
|
+
ColorReplacement,
|
|
26
|
+
MatchingOptions,
|
|
27
|
+
RefactoringResult,
|
|
28
|
+
RefactoringWarning,
|
|
29
|
+
SemanticRole,
|
|
30
|
+
ThemeMatch,
|
|
31
|
+
ThemeQualityReport,
|
|
32
|
+
ThemeVariable,
|
|
33
|
+
ThemeVariables,
|
|
34
|
+
} from "./types.js";
|