@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.
Files changed (197) hide show
  1. package/.claude/settings.local.json +39 -0
  2. package/.env +2 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +73 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +71 -0
  5. package/.github/pull_request_template.md +97 -0
  6. package/.github/workflows/ci.yml +127 -0
  7. package/.github/workflows/deploy-docs.yml +56 -0
  8. package/.github/workflows/release.yml +99 -0
  9. package/.mcp.json +12 -0
  10. package/.prettierignore +1 -0
  11. package/CLAUDE.md +201 -0
  12. package/DOCUMENTATION.md +274 -0
  13. package/GEMINI.md +54 -0
  14. package/LICENSE +21 -0
  15. package/README.md +401 -0
  16. package/demo/content_based_color.png +0 -0
  17. package/demo/music-player.html +621 -0
  18. package/demo/podcast-player.html +903 -0
  19. package/dist/bin/coolors-mcp.d.ts +1 -0
  20. package/dist/bin/coolors-mcp.js +154 -0
  21. package/dist/bin/coolors-mcp.js.map +1 -0
  22. package/dist/bin/server.d.ts +1 -0
  23. package/dist/bin/server.js +3292 -0
  24. package/dist/bin/server.js.map +1 -0
  25. package/dist/chunk-IQ7NN26V.js +114 -0
  26. package/dist/chunk-IQ7NN26V.js.map +1 -0
  27. package/dist/chunk-P3ARRKLS.js +1214 -0
  28. package/dist/chunk-P3ARRKLS.js.map +1 -0
  29. package/dist/color/index.d.ts +716 -0
  30. package/dist/color/index.js +153 -0
  31. package/dist/color/index.js.map +1 -0
  32. package/dist/coolors-mcp.d.ts +136 -0
  33. package/dist/coolors-mcp.js +7 -0
  34. package/dist/coolors-mcp.js.map +1 -0
  35. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
  36. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
  37. package/docs/.vitepress/cache/deps/_metadata.json +127 -0
  38. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
  39. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
  40. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +12683 -0
  41. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +7 -0
  42. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +9719 -0
  43. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +7 -0
  44. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
  45. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
  46. package/docs/.vitepress/cache/deps/cytoscape.js +30278 -0
  47. package/docs/.vitepress/cache/deps/cytoscape.js.map +7 -0
  48. package/docs/.vitepress/cache/deps/dayjs.js +285 -0
  49. package/docs/.vitepress/cache/deps/dayjs.js.map +7 -0
  50. package/docs/.vitepress/cache/deps/debug.js +468 -0
  51. package/docs/.vitepress/cache/deps/debug.js.map +7 -0
  52. package/docs/.vitepress/cache/deps/package.json +3 -0
  53. package/docs/.vitepress/cache/deps/prismjs.js +1466 -0
  54. package/docs/.vitepress/cache/deps/prismjs.js.map +7 -0
  55. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +228 -0
  56. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +7 -0
  57. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +142 -0
  58. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +7 -0
  59. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +27 -0
  60. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +7 -0
  61. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +65 -0
  62. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +7 -0
  63. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +53 -0
  64. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +7 -0
  65. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +73 -0
  66. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +7 -0
  67. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
  68. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
  69. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
  70. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
  71. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1146 -0
  72. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
  73. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
  74. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
  75. package/docs/.vitepress/cache/deps/vitepress___minisearch.js +1814 -0
  76. package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
  77. package/docs/.vitepress/cache/deps/vue.js +344 -0
  78. package/docs/.vitepress/cache/deps/vue.js.map +7 -0
  79. package/docs/.vitepress/components/ClientGrid.vue +125 -0
  80. package/docs/.vitepress/components/CodeBlock.vue +231 -0
  81. package/docs/.vitepress/components/ConfigModal.vue +477 -0
  82. package/docs/.vitepress/components/DiagramModal.vue +528 -0
  83. package/docs/.vitepress/components/TroubleshootingModal.vue +472 -0
  84. package/docs/.vitepress/config.js +162 -0
  85. package/docs/.vitepress/theme/FundingLayout.vue +251 -0
  86. package/docs/.vitepress/theme/Layout.vue +134 -0
  87. package/docs/.vitepress/theme/components/AdBanner.vue +317 -0
  88. package/docs/.vitepress/theme/components/AdPlaceholder.vue +78 -0
  89. package/docs/.vitepress/theme/components/FundingEffects.vue +322 -0
  90. package/docs/.vitepress/theme/components/FundingHero.vue +345 -0
  91. package/docs/.vitepress/theme/components/SupportSection.vue +511 -0
  92. package/docs/.vitepress/theme/custom-app.css +339 -0
  93. package/docs/.vitepress/theme/custom.css +699 -0
  94. package/docs/.vitepress/theme/index.js +25 -0
  95. package/docs/README.md +198 -0
  96. package/docs/concepts/accessibility.md +473 -0
  97. package/docs/concepts/color-spaces.md +222 -0
  98. package/docs/concepts/distance-metrics.md +384 -0
  99. package/docs/concepts/hct.md +261 -0
  100. package/docs/concepts/image-analysis.md +396 -0
  101. package/docs/concepts/material-design.md +306 -0
  102. package/docs/concepts/theme-matching.md +399 -0
  103. package/docs/examples/basic-colors.md +490 -0
  104. package/docs/examples/creating-themes.md +898 -0
  105. package/docs/examples/css-refactoring.md +824 -0
  106. package/docs/examples/image-extraction.md +882 -0
  107. package/docs/getting-started.md +366 -0
  108. package/docs/index.md +190 -0
  109. package/docs/installation.md +157 -0
  110. package/docs/tools/README.md +234 -0
  111. package/docs/tools/accessibility.md +614 -0
  112. package/docs/tools/color-operations.md +374 -0
  113. package/docs/tools/image-extraction.md +624 -0
  114. package/docs/tools/material-design.md +347 -0
  115. package/docs/tools/theme-matching.md +552 -0
  116. package/eslint.config.ts +14 -0
  117. package/examples/theme-matching.md +113 -0
  118. package/jsr.json +7 -0
  119. package/mcp-config.json +8 -0
  120. package/note.md +35 -0
  121. package/package.json +122 -0
  122. package/research_results.md +53 -0
  123. package/src/bin/coolors-mcp.ts +194 -0
  124. package/src/bin/server.ts +61 -0
  125. package/src/color/__tests__/conversions-argb.test.ts +198 -0
  126. package/src/color/__tests__/extract-colors.test.ts +360 -0
  127. package/src/color/__tests__/image-utils.test.ts +242 -0
  128. package/src/color/__tests__/reference-colors.test.ts +278 -0
  129. package/src/color/__tests__/round-trip.test.ts +197 -0
  130. package/src/color/conversions.test.ts +402 -0
  131. package/src/color/conversions.ts +393 -0
  132. package/src/color/dislike/__tests__/dislike-analyzer.test.ts +248 -0
  133. package/src/color/dislike/dislike-analyzer.ts +114 -0
  134. package/src/color/extract-colors.ts +228 -0
  135. package/src/color/hct/__tests__/hct-class.test.ts +232 -0
  136. package/src/color/hct/harmonization.ts +204 -0
  137. package/src/color/hct/hct-class.ts +109 -0
  138. package/src/color/hct/hct-solver.ts +168 -0
  139. package/src/color/hct/index.ts +39 -0
  140. package/src/color/hct/tonal-palette.ts +211 -0
  141. package/src/color/hct/types.ts +88 -0
  142. package/src/color/image-utils.ts +79 -0
  143. package/src/color/index.ts +87 -0
  144. package/src/color/material-theme.ts +157 -0
  145. package/src/color/metrics.test.ts +276 -0
  146. package/src/color/metrics.ts +281 -0
  147. package/src/color/quantize/__tests__/quantizer_celebi.test.ts +195 -0
  148. package/src/color/quantize/lab_point_provider.ts +55 -0
  149. package/src/color/quantize/point_provider.ts +27 -0
  150. package/src/color/quantize/quantizer_celebi.ts +51 -0
  151. package/src/color/quantize/quantizer_celebi_test.ts +71 -0
  152. package/src/color/quantize/quantizer_map.ts +47 -0
  153. package/src/color/quantize/quantizer_wsmeans.ts +232 -0
  154. package/src/color/quantize/quantizer_wu.ts +472 -0
  155. package/src/color/score/__tests__/score.test.ts +224 -0
  156. package/src/color/score/score.ts +175 -0
  157. package/src/color/types.ts +151 -0
  158. package/src/color/utils/color_utils.ts +292 -0
  159. package/src/color/utils/math_utils.ts +145 -0
  160. package/src/color/utils.test.ts +403 -0
  161. package/src/color/utils.ts +315 -0
  162. package/src/constants.ts +5 -0
  163. package/src/coolors-mcp.ts +37 -0
  164. package/src/examples/addition.ts +333 -0
  165. package/src/examples/color-demo.ts +125 -0
  166. package/src/examples/custom-logger.ts +201 -0
  167. package/src/examples/oauth-server.ts +113 -0
  168. package/src/examples/session-context.ts +269 -0
  169. package/src/session.ts +116 -0
  170. package/src/theme/__tests__/matcher.test.ts +180 -0
  171. package/src/theme/__tests__/parser.test.ts +148 -0
  172. package/src/theme/__tests__/refactor.test.ts +224 -0
  173. package/src/theme/index.ts +34 -0
  174. package/src/theme/matcher.ts +395 -0
  175. package/src/theme/parser.ts +392 -0
  176. package/src/theme/refactor.ts +360 -0
  177. package/src/theme/types.ts +152 -0
  178. package/src/tools/__tests__/gradient-generator.test.ts +206 -0
  179. package/src/tools/__tests__/palette-with-locks.test.ts +109 -0
  180. package/src/tools/color-conversion.tool.ts +54 -0
  181. package/src/tools/color-distance.tool.ts +41 -0
  182. package/src/tools/colors.ts +31 -0
  183. package/src/tools/contrast-checker.tool.ts +37 -0
  184. package/src/tools/dislike-analyzer.tool.ts +247 -0
  185. package/src/tools/gradient-generator.tool.ts +250 -0
  186. package/src/tools/image-extraction.tools.ts +289 -0
  187. package/src/tools/index.ts +39 -0
  188. package/src/tools/material-theme.tools.ts +250 -0
  189. package/src/tools/palette-generator.tool.ts +135 -0
  190. package/src/tools/palette-with-locks.tool.ts +221 -0
  191. package/src/tools/registry.ts +142 -0
  192. package/src/tools/simple-tools.ts +37 -0
  193. package/src/tools/theme-matching.tools.ts +334 -0
  194. package/src/types.ts +182 -0
  195. package/src/utils.ts +22 -0
  196. package/tsconfig.json +8 -0
  197. 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";