@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
|
@@ -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
|
+
});
|