@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
@@ -0,0 +1,360 @@
1
+ /**
2
+ * CSS Refactoring Utilities
3
+ * Refactor CSS to use theme variables
4
+ */
5
+
6
+ import type {
7
+ ColorContext,
8
+ ColorReplacement,
9
+ MatchingOptions,
10
+ RefactoringResult,
11
+ RefactoringWarning,
12
+ ThemeVariables,
13
+ } from "./types.js";
14
+
15
+ import { findClosestThemeColor } from "./matcher.js";
16
+
17
+ /**
18
+ * CSS property to context mapping
19
+ */
20
+ const PROPERTY_CONTEXT_MAP: Record<string, ColorContext> = {
21
+ background: "background",
22
+ "background-color": "background",
23
+ border: "border",
24
+ "border-bottom-color": "border",
25
+ "border-color": "border",
26
+ "border-left-color": "border",
27
+ "border-right-color": "border",
28
+ "border-top-color": "border",
29
+ "box-shadow": "shadow",
30
+ color: "text",
31
+ fill: "decorative",
32
+ outline: "border",
33
+ "outline-color": "border",
34
+ "stop-color": "decorative",
35
+ stroke: "border",
36
+ "text-shadow": "shadow",
37
+ };
38
+
39
+ /**
40
+ * Generate a refactoring report
41
+ */
42
+ export function generateRefactoringReport(result: RefactoringResult): string {
43
+ const lines: string[] = [
44
+ "# CSS Refactoring Report",
45
+ "",
46
+ "## Summary",
47
+ `- Total colors found: ${result.statistics.totalColors}`,
48
+ `- Colors replaced: ${result.statistics.replacedColors}`,
49
+ `- Replacement rate: ${Math.round((result.statistics.replacedColors / result.statistics.totalColors) * 100)}%`,
50
+ `- Average confidence: ${result.statistics.averageConfidence}%`,
51
+ `- Accessibility issues: ${result.statistics.accessibilityIssues}`,
52
+ "",
53
+ "## Replacements",
54
+ ];
55
+
56
+ // Group replacements by confidence
57
+ const highConfidence = result.replacements.filter((r) => r.confidence >= 90);
58
+ const mediumConfidence = result.replacements.filter(
59
+ (r) => r.confidence >= 70 && r.confidence < 90,
60
+ );
61
+ const lowConfidence = result.replacements.filter((r) => r.confidence < 70);
62
+
63
+ if (highConfidence.length > 0) {
64
+ lines.push("", "### High Confidence (≥90%)");
65
+ for (const replacement of highConfidence) {
66
+ lines.push(
67
+ `- Line ${replacement.line}: ${replacement.originalColor} → ${replacement.cssVariable} (${replacement.confidence}%)`,
68
+ );
69
+ }
70
+ }
71
+
72
+ if (mediumConfidence.length > 0) {
73
+ lines.push("", "### Medium Confidence (70-89%)");
74
+ for (const replacement of mediumConfidence) {
75
+ lines.push(
76
+ `- Line ${replacement.line}: ${replacement.originalColor} → ${replacement.cssVariable} (${replacement.confidence}%)`,
77
+ );
78
+ }
79
+ }
80
+
81
+ if (lowConfidence.length > 0) {
82
+ lines.push("", "### Low Confidence (<70%)");
83
+ for (const replacement of lowConfidence) {
84
+ lines.push(
85
+ `- Line ${replacement.line}: ${replacement.originalColor} → ${replacement.cssVariable} (${replacement.confidence}%)`,
86
+ );
87
+ }
88
+ }
89
+
90
+ // Add warnings
91
+ if (result.warnings.length > 0) {
92
+ lines.push("", "## Warnings");
93
+
94
+ const warningsByType = result.warnings.reduce(
95
+ (acc, warning) => {
96
+ if (!acc[warning.type]) acc[warning.type] = [];
97
+ acc[warning.type].push(warning);
98
+ return acc;
99
+ },
100
+ {} as Record<string, typeof result.warnings>,
101
+ );
102
+
103
+ for (const [type, warnings] of Object.entries(warningsByType)) {
104
+ lines.push("", `### ${formatWarningType(type)}`);
105
+ for (const warning of warnings) {
106
+ lines.push(`- ${warning.message}`);
107
+ if (warning.suggestion) {
108
+ lines.push(` Suggestion: ${warning.suggestion}`);
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ return lines.join("\n");
115
+ }
116
+
117
+ /**
118
+ * Refactor a single color value
119
+ */
120
+ export function refactorColor(
121
+ color: string,
122
+ themeVariables: ThemeVariables,
123
+ context?: ColorContext,
124
+ ): {
125
+ alternatives: string[];
126
+ confidence: number;
127
+ variable: null | string;
128
+ } {
129
+ const match = findClosestThemeColor(color, themeVariables, {
130
+ contextType: context,
131
+ });
132
+
133
+ if (!match) {
134
+ return {
135
+ alternatives: [],
136
+ confidence: 0,
137
+ variable: null,
138
+ };
139
+ }
140
+
141
+ return {
142
+ alternatives: match.alternatives.map((alt) => alt.variable),
143
+ confidence: match.confidence,
144
+ variable: match.variable,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Refactor CSS content to use theme variables
150
+ */
151
+ export function refactorCss(
152
+ css: string,
153
+ themeVariables: ThemeVariables,
154
+ options: {
155
+ addComments?: boolean;
156
+ minConfidence?: number;
157
+ preserveOriginal?: boolean;
158
+ } = {},
159
+ ): RefactoringResult {
160
+ const {
161
+ addComments = true,
162
+ minConfidence = 70,
163
+ preserveOriginal = true,
164
+ } = options;
165
+
166
+ const replacements: ColorReplacement[] = [];
167
+ const warnings: RefactoringWarning[] = [];
168
+ let refactoredCss = css;
169
+ let totalColors = 0;
170
+ let replacedColors = 0;
171
+ let totalConfidence = 0;
172
+ let accessibilityIssues = 0;
173
+
174
+ // Process CSS line by line for better tracking
175
+ const lines = css.split("\n");
176
+ const refactoredLines: string[] = [];
177
+
178
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
179
+ const line = lines[lineIndex];
180
+ let refactoredLine = line;
181
+
182
+ // Detect CSS property
183
+ const propertyMatch = line.match(/^\s*([a-z-]+):\s*/i);
184
+ const property = propertyMatch ? propertyMatch[1].toLowerCase() : null;
185
+ const context = property ? PROPERTY_CONTEXT_MAP[property] : undefined;
186
+
187
+ // Find all color values in the line
188
+ const colorMatches = findColorsInLine(line);
189
+
190
+ for (const colorMatch of colorMatches) {
191
+ totalColors++;
192
+ const { color, end, start } = colorMatch;
193
+
194
+ // Find matching theme variable
195
+ const matchOptions: MatchingOptions = {
196
+ contextType: context,
197
+ weights: {
198
+ accessibility:
199
+ context === "text" || context === "background" ? 0.3 : 0.1,
200
+ perceptual: 0.6,
201
+ semantic: context ? 0.3 : 0.2,
202
+ },
203
+ };
204
+
205
+ const match = findClosestThemeColor(color, themeVariables, matchOptions);
206
+
207
+ if (match && match.confidence >= minConfidence) {
208
+ replacedColors++;
209
+ totalConfidence += match.confidence;
210
+
211
+ // Create replacement
212
+ const replacement: ColorReplacement = {
213
+ column: start,
214
+ confidence: match.confidence,
215
+ context,
216
+ cssVariable: match.variable,
217
+ line: lineIndex + 1,
218
+ originalColor: color,
219
+ property: property || undefined,
220
+ };
221
+ replacements.push(replacement);
222
+
223
+ // Check accessibility
224
+ if (match.accessibilityInfo && !match.accessibilityInfo.meetsAA) {
225
+ accessibilityIssues++;
226
+ warnings.push({
227
+ location: { column: start, line: lineIndex + 1 },
228
+ message: `Color ${color} replaced with ${match.variable} may have accessibility issues`,
229
+ type: "accessibility",
230
+ });
231
+ }
232
+
233
+ // Apply replacement to line
234
+ let replacementText = `var(${match.variable})`;
235
+
236
+ if (preserveOriginal && addComments) {
237
+ replacementText = `/* ${color} */ ${replacementText}`;
238
+ }
239
+
240
+ // Replace in line
241
+ refactoredLine =
242
+ refactoredLine.substring(0, start) +
243
+ replacementText +
244
+ refactoredLine.substring(end);
245
+ } else if (match && match.confidence < minConfidence) {
246
+ warnings.push({
247
+ location: { column: start, line: lineIndex + 1 },
248
+ message: `Low confidence match for ${color}: ${match.variable} (${match.confidence}%)`,
249
+ suggestion: `Consider using ${match.variable} or defining a new theme variable`,
250
+ type: "low-confidence",
251
+ });
252
+ } else {
253
+ warnings.push({
254
+ location: { column: start, line: lineIndex + 1 },
255
+ message: `No suitable theme variable found for ${color}`,
256
+ suggestion: "Consider adding this color to your theme",
257
+ type: "no-match",
258
+ });
259
+ }
260
+ }
261
+
262
+ refactoredLines.push(refactoredLine);
263
+ }
264
+
265
+ // Add summary comment at the top if requested
266
+ if (addComments && replacedColors > 0) {
267
+ const summaryComment = `/* CSS Refactored with Theme Variables
268
+ * Total colors found: ${totalColors}
269
+ * Colors replaced: ${replacedColors}
270
+ * Average confidence: ${Math.round(totalConfidence / replacedColors)}%
271
+ * Warnings: ${warnings.length}
272
+ */\n\n`;
273
+ refactoredCss = summaryComment + refactoredLines.join("\n");
274
+ } else {
275
+ refactoredCss = refactoredLines.join("\n");
276
+ }
277
+
278
+ return {
279
+ original: css,
280
+ refactored: refactoredCss,
281
+ replacements,
282
+ statistics: {
283
+ accessibilityIssues,
284
+ averageConfidence:
285
+ replacedColors > 0 ? Math.round(totalConfidence / replacedColors) : 0,
286
+ replacedColors,
287
+ totalColors,
288
+ },
289
+ warnings,
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Find all color values in a line
295
+ */
296
+ function findColorsInLine(
297
+ line: string,
298
+ ): Array<{ color: string; end: number; start: number }> {
299
+ const colors: Array<{ color: string; end: number; start: number }> = [];
300
+
301
+ // Check for hex colors
302
+ let match;
303
+ const hexRegex = /#[0-9a-fA-F]{3,8}/g;
304
+ while ((match = hexRegex.exec(line)) !== null) {
305
+ colors.push({
306
+ color: match[0],
307
+ end: match.index + match[0].length,
308
+ start: match.index,
309
+ });
310
+ }
311
+
312
+ // Check for rgb/rgba - be more precise with the regex
313
+ const rgbRegex = /rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+(?:\s*,\s*[\d.]+)?\s*\)/g;
314
+ while ((match = rgbRegex.exec(line)) !== null) {
315
+ colors.push({
316
+ color: match[0],
317
+ end: match.index + match[0].length,
318
+ start: match.index,
319
+ });
320
+ }
321
+
322
+ // Check for hsl/hsla
323
+ const hslRegex = /hsla?\([^)]+\)/g;
324
+ while ((match = hslRegex.exec(line)) !== null) {
325
+ colors.push({
326
+ color: match[0],
327
+ end: match.index + match[0].length,
328
+ start: match.index,
329
+ });
330
+ }
331
+
332
+ // Sort by position to handle overlaps
333
+ colors.sort((a, b) => a.start - b.start);
334
+
335
+ // Remove duplicates and overlaps
336
+ const uniqueColors: typeof colors = [];
337
+ let lastEnd = -1;
338
+
339
+ for (const color of colors) {
340
+ if (color.start >= lastEnd) {
341
+ uniqueColors.push(color);
342
+ lastEnd = color.end;
343
+ }
344
+ }
345
+
346
+ return uniqueColors;
347
+ }
348
+
349
+ /**
350
+ * Format warning type for display
351
+ */
352
+ function formatWarningType(type: string): string {
353
+ const formats: Record<string, string> = {
354
+ accessibility: "Accessibility Concerns",
355
+ "low-confidence": "Low Confidence Matches",
356
+ "no-match": "No Matches Found",
357
+ "semantic-mismatch": "Semantic Mismatches",
358
+ };
359
+ return formats[type] || type;
360
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Theme Matching Types
3
+ * Types for theme color matching and CSS refactoring
4
+ */
5
+
6
+ import type { HCT } from "../color/hct/types.js";
7
+
8
+ /**
9
+ * Context where color is used
10
+ */
11
+ export type ColorContext =
12
+ | "accent"
13
+ | "background"
14
+ | "border"
15
+ | "decorative"
16
+ | "shadow"
17
+ | "text";
18
+
19
+ /**
20
+ * Single color replacement
21
+ */
22
+ export interface ColorReplacement {
23
+ column?: number;
24
+ confidence: number;
25
+ context?: ColorContext;
26
+ cssVariable: string;
27
+ line?: number;
28
+ originalColor: string;
29
+ property?: string;
30
+ selector?: string;
31
+ }
32
+
33
+ /**
34
+ * Options for color matching
35
+ */
36
+ export interface MatchingOptions {
37
+ contextType?: ColorContext; // Usage context
38
+ maxDistance?: number; // Maximum acceptable distance
39
+ preferredRole?: SemanticRole; // Preferred semantic role
40
+ weights?: {
41
+ accessibility: number; // Weight for accessibility (0-1)
42
+ perceptual: number; // Weight for perceptual distance (0-1)
43
+ semantic: number; // Weight for semantic appropriateness (0-1)
44
+ };
45
+ }
46
+
47
+ /**
48
+ * CSS refactoring result
49
+ */
50
+ export interface RefactoringResult {
51
+ original: string; // Original CSS
52
+ refactored: string; // Refactored CSS with variables
53
+ replacements: ColorReplacement[];
54
+ statistics: {
55
+ accessibilityIssues: number;
56
+ averageConfidence: number;
57
+ replacedColors: number;
58
+ totalColors: number;
59
+ };
60
+ warnings: RefactoringWarning[];
61
+ }
62
+
63
+ /**
64
+ * Warning generated during refactoring
65
+ */
66
+ export interface RefactoringWarning {
67
+ location?: {
68
+ column: number;
69
+ line: number;
70
+ };
71
+ message: string;
72
+ suggestion?: string;
73
+ type: "accessibility" | "low-confidence" | "no-match" | "semantic-mismatch";
74
+ }
75
+
76
+ /**
77
+ * Semantic roles for colors
78
+ */
79
+ export type SemanticRole =
80
+ | "background"
81
+ | "error"
82
+ | "neutral"
83
+ | "outline"
84
+ | "primary"
85
+ | "secondary"
86
+ | "shadow"
87
+ | "surface"
88
+ | "tertiary";
89
+
90
+ /**
91
+ * Result of matching a color to theme variables
92
+ */
93
+ export interface ThemeMatch {
94
+ accessibilityInfo?: {
95
+ contrastWithBackground: number;
96
+ contrastWithForeground: number;
97
+ meetsAA: boolean;
98
+ meetsAAA: boolean;
99
+ };
100
+ alternatives: ThemeMatch[]; // Alternative matches
101
+ confidence: number; // Match confidence (0-100)
102
+ distance: number; // HCT perceptual distance
103
+ semanticRole?: SemanticRole; // Suggested semantic role
104
+ value: string; // Color value
105
+ variable: string; // CSS variable name
106
+ }
107
+
108
+ /**
109
+ * Theme quality assessment
110
+ */
111
+ export interface ThemeQualityReport {
112
+ accessibility: {
113
+ aa: number; // Percentage meeting AA
114
+ aaa: number; // Percentage meeting AAA
115
+ issues: string[];
116
+ };
117
+ consistency: {
118
+ semanticRoles: boolean; // Are semantic roles properly assigned?
119
+ tonalSteps: boolean; // Are tonal steps consistent?
120
+ };
121
+ coverage: {
122
+ chromaRange: number[]; // Min and max chroma
123
+ hueVariety: number; // Hue diversity score
124
+ toneRange: number[]; // Min and max tones used
125
+ };
126
+ overallScore: number; // 0-100
127
+ recommendations: string[];
128
+ }
129
+
130
+ /**
131
+ * Represents a theme color variable
132
+ */
133
+ export interface ThemeVariable {
134
+ hct: HCT; // HCT representation for matching
135
+ name: string; // e.g., "--color-primary-60"
136
+ role?: SemanticRole; // Semantic role if assigned
137
+ tone: number; // Tone value (0-100)
138
+ value: string; // e.g., "#1976d2"
139
+ }
140
+
141
+ /**
142
+ * Collection of theme variables organized by role
143
+ */
144
+ export interface ThemeVariables {
145
+ custom?: Record<string, ThemeVariable[]>;
146
+ error?: ThemeVariable[];
147
+ neutral?: ThemeVariable[];
148
+ primary?: ThemeVariable[];
149
+ secondary?: ThemeVariable[];
150
+ surface?: ThemeVariable[];
151
+ tertiary?: ThemeVariable[];
152
+ }
@@ -0,0 +1,206 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { gradientGeneratorTool } from "../gradient-generator.tool.js";
4
+
5
+ describe("gradientGeneratorTool", () => {
6
+ it("should generate gradient with linear easing", async () => {
7
+ const result = await gradientGeneratorTool.execute({
8
+ colors: ["#000000", "#ffffff"],
9
+ easing: "linear",
10
+ format: "array",
11
+ interpolation: "rgb",
12
+ steps: 5,
13
+ });
14
+
15
+ expect(result).toContain("Generated gradient with 5 steps:");
16
+ expect(result).toContain("Interpolation: rgb");
17
+ expect(result).toContain("Easing: linear");
18
+ expect(result).toContain("Input colors: #000000 → #ffffff");
19
+ });
20
+
21
+ it("should generate gradient with HSL interpolation", async () => {
22
+ const result = await gradientGeneratorTool.execute({
23
+ colors: ["#ff0000", "#00ff00"],
24
+ easing: "linear",
25
+ format: "array",
26
+ interpolation: "hsl",
27
+ steps: 7,
28
+ });
29
+
30
+ expect(result).toContain("Generated gradient with 7 steps:");
31
+ expect(result).toContain("Interpolation: hsl");
32
+ const lines = result.split("\n");
33
+ const colorLines = lines.filter((line) => /^\d+\./.test(line));
34
+ expect(colorLines).toHaveLength(8); // 7 steps + final color
35
+ });
36
+
37
+ it("should generate gradient with LAB interpolation", async () => {
38
+ const result = await gradientGeneratorTool.execute({
39
+ colors: ["#0066cc", "#ff6600"],
40
+ easing: "linear",
41
+ format: "array",
42
+ interpolation: "lab",
43
+ steps: 10,
44
+ });
45
+
46
+ expect(result).toContain("Generated gradient with 10 steps:");
47
+ expect(result).toContain("Interpolation: lab");
48
+ });
49
+
50
+ it("should generate gradient with LCH interpolation", async () => {
51
+ const result = await gradientGeneratorTool.execute({
52
+ colors: ["#ff00ff", "#00ffff"],
53
+ easing: "linear",
54
+ format: "array",
55
+ interpolation: "lch",
56
+ steps: 8,
57
+ });
58
+
59
+ expect(result).toContain("Generated gradient with 8 steps:");
60
+ expect(result).toContain("Interpolation: lch");
61
+ });
62
+
63
+ it("should generate gradient with HCT interpolation", async () => {
64
+ const result = await gradientGeneratorTool.execute({
65
+ colors: ["#4285f4", "#ea4335"],
66
+ easing: "linear",
67
+ format: "array",
68
+ interpolation: "hct",
69
+ steps: 6,
70
+ });
71
+
72
+ expect(result).toContain("Generated gradient with 6 steps:");
73
+ expect(result).toContain("Interpolation: hct");
74
+ });
75
+
76
+ it("should apply ease-in easing", async () => {
77
+ const result = await gradientGeneratorTool.execute({
78
+ colors: ["#000000", "#ffffff"],
79
+ easing: "ease-in",
80
+ format: "array",
81
+ interpolation: "rgb",
82
+ steps: 5,
83
+ });
84
+
85
+ expect(result).toContain("Easing: ease-in");
86
+ });
87
+
88
+ it("should apply ease-out easing", async () => {
89
+ const result = await gradientGeneratorTool.execute({
90
+ colors: ["#000000", "#ffffff"],
91
+ easing: "ease-out",
92
+ format: "array",
93
+ interpolation: "rgb",
94
+ steps: 5,
95
+ });
96
+
97
+ expect(result).toContain("Easing: ease-out");
98
+ });
99
+
100
+ it("should apply ease-in-out easing", async () => {
101
+ const result = await gradientGeneratorTool.execute({
102
+ colors: ["#000000", "#ffffff"],
103
+ easing: "ease-in-out",
104
+ format: "array",
105
+ interpolation: "rgb",
106
+ steps: 5,
107
+ });
108
+
109
+ expect(result).toContain("Easing: ease-in-out");
110
+ });
111
+
112
+ it("should format as CSS linear gradient", async () => {
113
+ const result = await gradientGeneratorTool.execute({
114
+ colors: ["#ff0000", "#0000ff"],
115
+ easing: "linear",
116
+ format: "css-linear",
117
+ interpolation: "lab",
118
+ steps: 5,
119
+ });
120
+
121
+ expect(result).toMatch(/^linear-gradient\(90deg,/);
122
+ expect(result).toContain("0.0%");
123
+ expect(result).toContain("100.0%");
124
+ });
125
+
126
+ it("should format as CSS radial gradient", async () => {
127
+ const result = await gradientGeneratorTool.execute({
128
+ colors: ["#ffffff", "#000000"],
129
+ easing: "linear",
130
+ format: "css-radial",
131
+ interpolation: "lab",
132
+ steps: 4,
133
+ });
134
+
135
+ expect(result).toMatch(/^radial-gradient\(circle,/);
136
+ expect(result).toContain("0.0%");
137
+ expect(result).toContain("100.0%");
138
+ });
139
+
140
+ it("should format as hex string", async () => {
141
+ const result = await gradientGeneratorTool.execute({
142
+ colors: ["#ff0000", "#00ff00"],
143
+ easing: "linear",
144
+ format: "hex",
145
+ interpolation: "rgb",
146
+ steps: 3,
147
+ });
148
+
149
+ expect(result).toMatch(/^#[0-9a-f]{6}(, #[0-9a-f]{6})+$/i);
150
+ });
151
+
152
+ it("should handle multiple color stops", async () => {
153
+ const result = await gradientGeneratorTool.execute({
154
+ colors: ["#ff0000", "#00ff00", "#0000ff"],
155
+ easing: "linear",
156
+ format: "array",
157
+ interpolation: "lab",
158
+ steps: 9,
159
+ });
160
+
161
+ expect(result).toContain("Generated gradient with 9 steps:");
162
+ expect(result).toContain("Input colors: #ff0000 → #00ff00 → #0000ff");
163
+ const lines = result.split("\n");
164
+ const colorLines = lines.filter((line) => /^\d+\./.test(line));
165
+ expect(colorLines).toHaveLength(10); // 9 steps + final color
166
+ });
167
+
168
+ it("should error with less than 2 colors", async () => {
169
+ const result = await gradientGeneratorTool.execute({
170
+ colors: ["#ff0000"],
171
+ easing: "linear",
172
+ format: "array",
173
+ interpolation: "rgb",
174
+ steps: 5,
175
+ });
176
+
177
+ expect(result).toContain("Error: At least 2 colors are required");
178
+ });
179
+
180
+ it("should error on invalid color format", async () => {
181
+ const result = await gradientGeneratorTool.execute({
182
+ colors: ["#ff0000", "invalid"],
183
+ easing: "linear",
184
+ format: "array",
185
+ interpolation: "rgb",
186
+ steps: 5,
187
+ });
188
+
189
+ expect(result).toContain("Invalid color format: invalid");
190
+ });
191
+
192
+ it("should handle gradients with many steps", async () => {
193
+ const result = await gradientGeneratorTool.execute({
194
+ colors: ["#000000", "#ffffff"],
195
+ easing: "linear",
196
+ format: "array",
197
+ interpolation: "lab",
198
+ steps: 50,
199
+ });
200
+
201
+ expect(result).toContain("Generated gradient with 50 steps:");
202
+ const lines = result.split("\n");
203
+ const colorLines = lines.filter((line) => /^\d+\./.test(line));
204
+ expect(colorLines).toHaveLength(51); // 50 steps + final color
205
+ });
206
+ });