@trishchuk/coolors-mcp 1.0.1 → 1.1.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/.github/workflows/ci.yml +23 -20
- package/.github/workflows/deploy-docs.yml +6 -3
- package/.github/workflows/release.yml +11 -9
- package/README.md +123 -14
- package/dist/bin/server.js +997 -256
- package/dist/bin/server.js.map +1 -1
- package/dist/{chunk-P3ARRKLS.js → chunk-HOMDMKUY.js} +3 -1
- package/dist/{chunk-P3ARRKLS.js.map → chunk-HOMDMKUY.js.map} +1 -1
- package/dist/{chunk-IQ7NN26V.js → chunk-LHW2ZTOU.js} +14 -2
- package/dist/chunk-LHW2ZTOU.js.map +1 -0
- package/dist/color/index.js +1 -1
- package/dist/coolors-mcp.d.ts +4 -4
- package/dist/coolors-mcp.js +1 -1
- package/eslint.config.ts +13 -0
- package/jsr.json +1 -1
- package/package.json +16 -12
- package/src/bin/server.ts +13 -1
- package/src/color/__tests__/extract-colors.test.ts +20 -30
- package/src/color/apca.ts +105 -0
- package/src/color/color-blindness.ts +109 -0
- package/src/coolors-mcp.ts +1 -1
- package/src/session.ts +10 -2
- package/src/theme/matcher.ts +1 -1
- package/src/theme/refactor.ts +1 -1
- package/src/theme/types.ts +3 -0
- package/src/tools/__tests__/cohesion.test.ts +97 -0
- package/src/tools/__tests__/color-blindness.test.ts +45 -0
- package/src/tools/__tests__/color-conversion.test.ts +38 -0
- package/src/tools/__tests__/contrast-checker.test.ts +56 -0
- package/src/tools/__tests__/palette-export.test.ts +54 -0
- package/src/tools/adjust-color.tool.ts +80 -0
- package/src/tools/cohesion.tools.ts +380 -0
- package/src/tools/color-blindness.tool.ts +168 -0
- package/src/tools/color-conversion.tool.ts +1 -1
- package/src/tools/contrast-checker.tool.ts +53 -14
- package/src/tools/dislike-analyzer.tool.ts +41 -54
- package/src/tools/image-extraction.tools.ts +62 -115
- package/src/tools/index.ts +15 -2
- package/src/tools/palette-export.tool.ts +174 -0
- package/src/tools/palette-with-locks.tool.ts +8 -6
- package/src/types.ts +2 -3
- package/tsconfig.json +12 -2
- package/vitest.config.js +1 -3
- package/.claude/settings.local.json +0 -35
- package/.env +0 -2
- package/.mcp.json +0 -12
- package/CLAUDE.md +0 -201
- package/DOCUMENTATION.md +0 -274
- package/GEMINI.md +0 -54
- package/TOOLS_UK.md +0 -233
- package/demo/content_based_color.png +0 -0
- package/demo/music-player.html +0 -621
- package/demo/podcast-player.html +0 -903
- package/dist/chunk-IQ7NN26V.js.map +0 -1
- package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +0 -111
- package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +0 -7
- package/docs/.vitepress/cache/deps/_metadata.json +0 -127
- package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +0 -12
- package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +0 -7
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +0 -13614
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +0 -7
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +0 -10698
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +0 -7
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +0 -5609
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +0 -7
- package/docs/.vitepress/cache/deps/cytoscape.js +0 -36234
- package/docs/.vitepress/cache/deps/cytoscape.js.map +0 -7
- package/docs/.vitepress/cache/deps/dayjs.js +0 -507
- package/docs/.vitepress/cache/deps/dayjs.js.map +0 -7
- package/docs/.vitepress/cache/deps/debug.js +0 -512
- package/docs/.vitepress/cache/deps/debug.js.map +0 -7
- package/docs/.vitepress/cache/deps/package.json +0 -3
- package/docs/.vitepress/cache/deps/prismjs.js +0 -1638
- package/docs/.vitepress/cache/deps/prismjs.js.map +0 -7
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +0 -235
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +0 -7
- package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +0 -173
- package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +0 -7
- package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +0 -27
- package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +0 -7
- package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +0 -72
- package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +0 -7
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +0 -56
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +0 -7
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +0 -107
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +0 -7
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -5074
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -584
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +0 -1483
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +0 -7
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +0 -1779
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +0 -7
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js +0 -2023
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +0 -7
- package/docs/.vitepress/cache/deps/vue.js +0 -344
- package/docs/.vitepress/cache/deps/vue.js.map +0 -7
- package/examples/theme-matching.md +0 -113
- package/mcp-config.json +0 -8
- package/note.md +0 -34
- package/research_results.md +0 -53
- package/src/tools/colors.ts +0 -31
- package/src/tools/registry.ts +0 -142
- package/src/tools/simple-tools.ts +0 -37
package/src/bin/server.ts
CHANGED
|
@@ -24,16 +24,28 @@ const server = new CoolorsMcp({
|
|
|
24
24
|
instructions:
|
|
25
25
|
"Advanced color operations server with Material Design 3 support, CSS theme matching, image extraction, and accessibility compliance. Uses HCT color space for perceptually accurate operations.",
|
|
26
26
|
name: "coolors-mcp",
|
|
27
|
-
version: "1.
|
|
27
|
+
version: "1.1.0",
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
// Core color operations: conversion, distance metrics, accessibility
|
|
31
31
|
server.addTool(tools.colorConversionTool);
|
|
32
32
|
server.addTool(tools.colorDistanceTool);
|
|
33
33
|
server.addTool(tools.contrastCheckerTool);
|
|
34
|
+
server.addTool(tools.adjustColorTool);
|
|
34
35
|
server.addTool(tools.paletteGeneratorTool);
|
|
35
36
|
server.addTool(tools.paletteWithLocksTool);
|
|
36
37
|
server.addTool(tools.gradientGeneratorTool);
|
|
38
|
+
server.addTool(tools.exportPaletteTool);
|
|
39
|
+
|
|
40
|
+
// Color-blindness simulation & accessibility audit
|
|
41
|
+
server.addTool(tools.simulateColorBlindnessTool);
|
|
42
|
+
server.addTool(tools.checkPaletteAccessibilityTool);
|
|
43
|
+
|
|
44
|
+
// Visual cohesion: tonal scales, state colors, palette consistency, semantics
|
|
45
|
+
server.addTool(tools.generateTonalScaleTool);
|
|
46
|
+
server.addTool(tools.generateStateColorsTool);
|
|
47
|
+
server.addTool(tools.analyzePaletteConsistencyTool);
|
|
48
|
+
server.addTool(tools.generateSemanticPaletteTool);
|
|
37
49
|
|
|
38
50
|
// Material Design 3: theme generation, harmonization, tonal palettes
|
|
39
51
|
server.addTool(tools.generateMaterialThemeTool);
|
|
@@ -27,9 +27,8 @@ describe("Color Extraction", () => {
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
it("should extract colors with default options", async () => {
|
|
30
|
-
const { QuantizerCelebi } =
|
|
31
|
-
"../quantize/quantizer_celebi.js"
|
|
32
|
-
);
|
|
30
|
+
const { QuantizerCelebi } =
|
|
31
|
+
await import("../quantize/quantizer_celebi.js");
|
|
33
32
|
const { Score } = await import("../score/score.js");
|
|
34
33
|
|
|
35
34
|
// Mock quantizer to return test colors
|
|
@@ -106,9 +105,8 @@ describe("Color Extraction", () => {
|
|
|
106
105
|
});
|
|
107
106
|
|
|
108
107
|
it("should extract colors without scoring", async () => {
|
|
109
|
-
const { QuantizerCelebi } =
|
|
110
|
-
"../quantize/quantizer_celebi.js"
|
|
111
|
-
);
|
|
108
|
+
const { QuantizerCelebi } =
|
|
109
|
+
await import("../quantize/quantizer_celebi.js");
|
|
112
110
|
const { Score } = await import("../score/score.js");
|
|
113
111
|
|
|
114
112
|
const mockQuantized = new Map([
|
|
@@ -140,9 +138,8 @@ describe("Color Extraction", () => {
|
|
|
140
138
|
});
|
|
141
139
|
|
|
142
140
|
it("should handle empty image data", async () => {
|
|
143
|
-
const { QuantizerCelebi } =
|
|
144
|
-
"../quantize/quantizer_celebi.js"
|
|
145
|
-
);
|
|
141
|
+
const { QuantizerCelebi } =
|
|
142
|
+
await import("../quantize/quantizer_celebi.js");
|
|
146
143
|
const { Score } = await import("../score/score.js");
|
|
147
144
|
|
|
148
145
|
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(new Map());
|
|
@@ -159,9 +156,8 @@ describe("Color Extraction", () => {
|
|
|
159
156
|
});
|
|
160
157
|
|
|
161
158
|
it("should calculate correct percentages", async () => {
|
|
162
|
-
const { QuantizerCelebi } =
|
|
163
|
-
"../quantize/quantizer_celebi.js"
|
|
164
|
-
);
|
|
159
|
+
const { QuantizerCelebi } =
|
|
160
|
+
await import("../quantize/quantizer_celebi.js");
|
|
165
161
|
const { Score } = await import("../score/score.js");
|
|
166
162
|
|
|
167
163
|
const mockQuantized = new Map([
|
|
@@ -194,9 +190,8 @@ describe("Color Extraction", () => {
|
|
|
194
190
|
});
|
|
195
191
|
|
|
196
192
|
it("should extract theme palette with primary color", async () => {
|
|
197
|
-
const { QuantizerCelebi } =
|
|
198
|
-
"../quantize/quantizer_celebi.js"
|
|
199
|
-
);
|
|
193
|
+
const { QuantizerCelebi } =
|
|
194
|
+
await import("../quantize/quantizer_celebi.js");
|
|
200
195
|
const { Score } = await import("../score/score.js");
|
|
201
196
|
|
|
202
197
|
const mockQuantized = new Map([
|
|
@@ -219,9 +214,8 @@ describe("Color Extraction", () => {
|
|
|
219
214
|
});
|
|
220
215
|
|
|
221
216
|
it("should extract secondary with different hue", async () => {
|
|
222
|
-
const { QuantizerCelebi } =
|
|
223
|
-
"../quantize/quantizer_celebi.js"
|
|
224
|
-
);
|
|
217
|
+
const { QuantizerCelebi } =
|
|
218
|
+
await import("../quantize/quantizer_celebi.js");
|
|
225
219
|
const { Score } = await import("../score/score.js");
|
|
226
220
|
|
|
227
221
|
const mockQuantized = new Map([
|
|
@@ -251,9 +245,8 @@ describe("Color Extraction", () => {
|
|
|
251
245
|
});
|
|
252
246
|
|
|
253
247
|
it("should find neutral color with low chroma", async () => {
|
|
254
|
-
const { QuantizerCelebi } =
|
|
255
|
-
"../quantize/quantizer_celebi.js"
|
|
256
|
-
);
|
|
248
|
+
const { QuantizerCelebi } =
|
|
249
|
+
await import("../quantize/quantizer_celebi.js");
|
|
257
250
|
const { Score } = await import("../score/score.js");
|
|
258
251
|
|
|
259
252
|
const mockQuantized = new Map([
|
|
@@ -280,9 +273,8 @@ describe("Color Extraction", () => {
|
|
|
280
273
|
});
|
|
281
274
|
|
|
282
275
|
it("should find error color in red hue range", async () => {
|
|
283
|
-
const { QuantizerCelebi } =
|
|
284
|
-
"../quantize/quantizer_celebi.js"
|
|
285
|
-
);
|
|
276
|
+
const { QuantizerCelebi } =
|
|
277
|
+
await import("../quantize/quantizer_celebi.js");
|
|
286
278
|
const { Score } = await import("../score/score.js");
|
|
287
279
|
|
|
288
280
|
const primaryArgb = utils.argbFromRgb(103, 80, 164);
|
|
@@ -311,9 +303,8 @@ describe("Color Extraction", () => {
|
|
|
311
303
|
});
|
|
312
304
|
|
|
313
305
|
it("should throw error for empty image", async () => {
|
|
314
|
-
const { QuantizerCelebi } =
|
|
315
|
-
"../quantize/quantizer_celebi.js"
|
|
316
|
-
);
|
|
306
|
+
const { QuantizerCelebi } =
|
|
307
|
+
await import("../quantize/quantizer_celebi.js");
|
|
317
308
|
const { Score } = await import("../score/score.js");
|
|
318
309
|
|
|
319
310
|
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(new Map());
|
|
@@ -331,9 +322,8 @@ describe("Color Extraction", () => {
|
|
|
331
322
|
});
|
|
332
323
|
|
|
333
324
|
it("should handle single color image", async () => {
|
|
334
|
-
const { QuantizerCelebi } =
|
|
335
|
-
"../quantize/quantizer_celebi.js"
|
|
336
|
-
);
|
|
325
|
+
const { QuantizerCelebi } =
|
|
326
|
+
await import("../quantize/quantizer_celebi.js");
|
|
337
327
|
const { Score } = await import("../score/score.js");
|
|
338
328
|
|
|
339
329
|
const mockQuantized = new Map([
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APCA — Accessible Perceptual Contrast Algorithm (WCAG 3 draft)
|
|
3
|
+
*
|
|
4
|
+
* Implementation of APCA W3 0.1.9 / SAPC by Andrew Somers (Myndex).
|
|
5
|
+
* Returns Lc, an absolute contrast value typically in the range [-108, +106].
|
|
6
|
+
* Sign indicates polarity: positive when text is darker than background
|
|
7
|
+
* ("dark on light"), negative for light text on dark background. The magnitude
|
|
8
|
+
* (|Lc|) maps to readability thresholds via APCA's "Bronze Simple Tables":
|
|
9
|
+
*
|
|
10
|
+
* |Lc| >= 75 body text
|
|
11
|
+
* |Lc| >= 60 content text
|
|
12
|
+
* |Lc| >= 45 large fluent text
|
|
13
|
+
* |Lc| >= 30 non-content / spot text
|
|
14
|
+
* |Lc| < 15 invisible — fails for any text use
|
|
15
|
+
*
|
|
16
|
+
* Reference: https://github.com/Myndex/SAPC-APCA
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { RGB } from "./types.js";
|
|
20
|
+
|
|
21
|
+
// APCA constants (W3-0.1.9 4g)
|
|
22
|
+
const SA98G = {
|
|
23
|
+
blkClmp: 1.414,
|
|
24
|
+
blkThrs: 0.022,
|
|
25
|
+
deltaYmin: 0.0005,
|
|
26
|
+
loBoTclip: -0.6,
|
|
27
|
+
loBoTexp: 0.74,
|
|
28
|
+
loClip: 0.1,
|
|
29
|
+
// Polarity exponents/factors
|
|
30
|
+
normBG: 0.56,
|
|
31
|
+
normTXT: 0.57,
|
|
32
|
+
revBG: 0.62,
|
|
33
|
+
revTXT: 0.65,
|
|
34
|
+
// Soft-clip and scale
|
|
35
|
+
scaleBoW: 1.14,
|
|
36
|
+
scaleWoB: 1.14,
|
|
37
|
+
trailingW: 0.027,
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
const sRGBtrc = 2.4;
|
|
41
|
+
const Rco = 0.2126729;
|
|
42
|
+
const Gco = 0.7151522;
|
|
43
|
+
const Bco = 0.072175;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compute APCA Lc for foreground text on background.
|
|
47
|
+
* `text` is the text/foreground color, `bg` is the background color.
|
|
48
|
+
*/
|
|
49
|
+
export function apcaContrast(text: RGB, bg: RGB): number {
|
|
50
|
+
let txtY = apcaY(text);
|
|
51
|
+
let bgY = apcaY(bg);
|
|
52
|
+
|
|
53
|
+
// Soft black clamp
|
|
54
|
+
if (txtY <= SA98G.blkThrs) {
|
|
55
|
+
txtY += Math.pow(SA98G.blkThrs - txtY, SA98G.blkClmp);
|
|
56
|
+
}
|
|
57
|
+
if (bgY <= SA98G.blkThrs) {
|
|
58
|
+
bgY += Math.pow(SA98G.blkThrs - bgY, SA98G.blkClmp);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (Math.abs(bgY - txtY) < SA98G.deltaYmin) return 0;
|
|
62
|
+
|
|
63
|
+
let outputContrast: number;
|
|
64
|
+
|
|
65
|
+
if (bgY > txtY) {
|
|
66
|
+
// Normal polarity (dark text on light background) — positive Lc
|
|
67
|
+
const SAPC =
|
|
68
|
+
(Math.pow(bgY, SA98G.normBG) - Math.pow(txtY, SA98G.normTXT)) *
|
|
69
|
+
SA98G.scaleBoW;
|
|
70
|
+
outputContrast = SAPC < SA98G.loClip ? 0 : SAPC - SA98G.trailingW;
|
|
71
|
+
} else {
|
|
72
|
+
// Reverse polarity (light text on dark background) — negative Lc
|
|
73
|
+
const SAPC =
|
|
74
|
+
(Math.pow(bgY, SA98G.revBG) - Math.pow(txtY, SA98G.revTXT)) *
|
|
75
|
+
SA98G.scaleWoB;
|
|
76
|
+
outputContrast = SAPC > -SA98G.loClip ? 0 : SAPC + SA98G.trailingW;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return outputContrast * 100;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Classify an APCA Lc value against the Bronze Simple level thresholds.
|
|
84
|
+
*/
|
|
85
|
+
export function apcaLevel(lc: number): {
|
|
86
|
+
body: boolean;
|
|
87
|
+
content: boolean;
|
|
88
|
+
large: boolean;
|
|
89
|
+
spot: boolean;
|
|
90
|
+
} {
|
|
91
|
+
const abs = Math.abs(lc);
|
|
92
|
+
return {
|
|
93
|
+
body: abs >= 75,
|
|
94
|
+
content: abs >= 60,
|
|
95
|
+
large: abs >= 45,
|
|
96
|
+
spot: abs >= 30,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function apcaY(rgb: RGB): number {
|
|
101
|
+
const r = Math.pow(rgb.r / 255, sRGBtrc);
|
|
102
|
+
const g = Math.pow(rgb.g / 255, sRGBtrc);
|
|
103
|
+
const b = Math.pow(rgb.b / 255, sRGBtrc);
|
|
104
|
+
return Rco * r + Gco * g + Bco * b;
|
|
105
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color Vision Deficiency (CVD) simulation.
|
|
3
|
+
*
|
|
4
|
+
* Linear-sRGB transformation matrices from Machado, Oliveira, and Fernandes
|
|
5
|
+
* (2009), "A Physiologically-based Model for Simulation of Color Vision
|
|
6
|
+
* Deficiency". Severity 1.0 matrices are used for the dichromatic forms
|
|
7
|
+
* (protanopia/deuteranopia/tritanopia); severity 0.6 matrices are used for the
|
|
8
|
+
* milder anomaly forms. Achromatopsia uses ITU-R BT.709 luminance.
|
|
9
|
+
*
|
|
10
|
+
* Pipeline: sRGB → linear sRGB → matrix → sRGB.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { RGB } from "./types.js";
|
|
14
|
+
|
|
15
|
+
export type CvdType =
|
|
16
|
+
| "achromatopsia"
|
|
17
|
+
| "deuteranomaly"
|
|
18
|
+
| "deuteranopia"
|
|
19
|
+
| "protanomaly"
|
|
20
|
+
| "protanopia"
|
|
21
|
+
| "tritanomaly"
|
|
22
|
+
| "tritanopia";
|
|
23
|
+
|
|
24
|
+
// Confusion-line matrices applied in linear sRGB space (Machado et al. 2009
|
|
25
|
+
// linear approximation, common in libraries like color-blind / colorjs.io).
|
|
26
|
+
// Each matrix maps linear-sRGB to the visible linear-sRGB for the given CVD.
|
|
27
|
+
const CVD_MATRICES: Record<
|
|
28
|
+
Exclude<CvdType, "achromatopsia">,
|
|
29
|
+
[number, number, number, number, number, number, number, number, number]
|
|
30
|
+
> = {
|
|
31
|
+
deuteranomaly: [
|
|
32
|
+
0.547494, 0.607765, -0.155259, 0.181692, 0.781742, 0.036566, -0.01041,
|
|
33
|
+
0.027275, 0.983136,
|
|
34
|
+
],
|
|
35
|
+
deuteranopia: [
|
|
36
|
+
0.367322, 0.860646, -0.227968, 0.280085, 0.672501, 0.047413, -0.01182,
|
|
37
|
+
0.04294, 0.968881,
|
|
38
|
+
],
|
|
39
|
+
// Mild forms (-omaly: severity ~0.6, Machado severity 0.6 table)
|
|
40
|
+
protanomaly: [
|
|
41
|
+
0.458064, 0.679578, -0.137642, 0.092785, 0.846313, 0.060902, -0.007494,
|
|
42
|
+
-0.016807, 1.024301,
|
|
43
|
+
],
|
|
44
|
+
// Strong forms (-opia: full dichromacy, severity 1.0)
|
|
45
|
+
protanopia: [
|
|
46
|
+
0.152286, 1.052583, -0.204868, 0.114503, 0.786281, 0.099216, -0.003882,
|
|
47
|
+
-0.048116, 1.051998,
|
|
48
|
+
],
|
|
49
|
+
tritanomaly: [
|
|
50
|
+
1.193214, -0.109812, -0.083402, 0.058694, 0.901185, 0.040121, -0.005978,
|
|
51
|
+
0.401901, 0.604077,
|
|
52
|
+
],
|
|
53
|
+
tritanopia: [
|
|
54
|
+
1.255528, -0.076749, -0.178779, -0.078411, 0.930809, 0.147602, 0.004733,
|
|
55
|
+
0.691367, 0.3039,
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Simulate how a color appears to a viewer with the given color vision
|
|
61
|
+
* deficiency.
|
|
62
|
+
*/
|
|
63
|
+
export function simulateCvd(rgb: RGB, type: CvdType): RGB {
|
|
64
|
+
if (type === "achromatopsia") {
|
|
65
|
+
// ITU-R BT.709 luminance (Rec. 709) — perceived brightness.
|
|
66
|
+
const y = 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b;
|
|
67
|
+
const g = Math.max(0, Math.min(255, Math.round(y)));
|
|
68
|
+
return { b: g, g, r: g };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const m = CVD_MATRICES[type];
|
|
72
|
+
const r = srgbToLinear(rgb.r);
|
|
73
|
+
const g = srgbToLinear(rgb.g);
|
|
74
|
+
const b = srgbToLinear(rgb.b);
|
|
75
|
+
|
|
76
|
+
const rOut = m[0] * r + m[1] * g + m[2] * b;
|
|
77
|
+
const gOut = m[3] * r + m[4] * g + m[5] * b;
|
|
78
|
+
const bOut = m[6] * r + m[7] * g + m[8] * b;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
b: linearToSrgb(bOut),
|
|
82
|
+
g: linearToSrgb(gOut),
|
|
83
|
+
r: linearToSrgb(rOut),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function linearToSrgb(c: number): number {
|
|
88
|
+
const v = c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
89
|
+
return Math.max(0, Math.min(255, Math.round(v * 255)));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function srgbToLinear(c: number): number {
|
|
93
|
+
const n = c / 255;
|
|
94
|
+
return n <= 0.04045 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Approximate share of the population affected by each CVD type (worldwide,
|
|
99
|
+
* combining male+female prevalence as reported by Colour Blind Awareness).
|
|
100
|
+
*/
|
|
101
|
+
export const CVD_PREVALENCE: Record<CvdType, number> = {
|
|
102
|
+
achromatopsia: 0.003,
|
|
103
|
+
deuteranomaly: 5.0,
|
|
104
|
+
deuteranopia: 1.0,
|
|
105
|
+
protanomaly: 1.0,
|
|
106
|
+
protanopia: 1.0,
|
|
107
|
+
tritanomaly: 0.01,
|
|
108
|
+
tritanopia: 0.003,
|
|
109
|
+
};
|
package/src/coolors-mcp.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
-
import { EventEmitter } from "events";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
3
|
import { StrictEventEmitter } from "strict-event-emitter-types";
|
|
4
4
|
|
|
5
5
|
import { CoolorsMCPSession } from "./session.js";
|
package/src/session.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
ListToolsRequestSchema,
|
|
7
7
|
McpError,
|
|
8
8
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
import { EventEmitter } from "events";
|
|
9
|
+
import { EventEmitter } from "node:events";
|
|
10
10
|
import { StrictEventEmitter } from "strict-event-emitter-types";
|
|
11
11
|
import { toJsonSchema } from "xsschema";
|
|
12
12
|
|
|
@@ -99,9 +99,17 @@ export class CoolorsMCPSession extends (EventEmitter as {
|
|
|
99
99
|
args = parsed.value;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
const noop = () => {};
|
|
103
|
+
const context = {
|
|
104
|
+
client: { version: this.#server.getClientVersion() },
|
|
105
|
+
log: { debug: noop, error: noop, info: noop, warn: noop },
|
|
106
|
+
reportProgress: async () => {},
|
|
107
|
+
session: undefined,
|
|
108
|
+
streamContent: async () => {},
|
|
109
|
+
};
|
|
102
110
|
const result = await tool.execute(
|
|
103
111
|
args as Parameters<typeof tool.execute>[0],
|
|
104
|
-
|
|
112
|
+
context as Parameters<typeof tool.execute>[1],
|
|
105
113
|
);
|
|
106
114
|
|
|
107
115
|
if (typeof result === "string") {
|
package/src/theme/matcher.ts
CHANGED
|
@@ -256,7 +256,7 @@ function calculateHctDistance(color1: HCT, color2: HCT): number {
|
|
|
256
256
|
* Calculate multi-factor match score
|
|
257
257
|
*/
|
|
258
258
|
function calculateMatchScore(
|
|
259
|
-
|
|
259
|
+
_inputHct: HCT,
|
|
260
260
|
candidate: ThemeVariable,
|
|
261
261
|
distance: number,
|
|
262
262
|
weights: { accessibility: number; perceptual: number; semantic: number },
|
package/src/theme/refactor.ts
CHANGED
|
@@ -165,7 +165,7 @@ export function refactorCss(
|
|
|
165
165
|
|
|
166
166
|
const replacements: ColorReplacement[] = [];
|
|
167
167
|
const warnings: RefactoringWarning[] = [];
|
|
168
|
-
let refactoredCss
|
|
168
|
+
let refactoredCss: string;
|
|
169
169
|
let totalColors = 0;
|
|
170
170
|
let replacedColors = 0;
|
|
171
171
|
let totalConfidence = 0;
|
package/src/theme/types.ts
CHANGED
|
@@ -142,11 +142,14 @@ export interface ThemeVariable {
|
|
|
142
142
|
* Collection of theme variables organized by role
|
|
143
143
|
*/
|
|
144
144
|
export interface ThemeVariables {
|
|
145
|
+
background?: ThemeVariable[];
|
|
145
146
|
custom?: Record<string, ThemeVariable[]>;
|
|
146
147
|
error?: ThemeVariable[];
|
|
147
148
|
neutral?: ThemeVariable[];
|
|
149
|
+
outline?: ThemeVariable[];
|
|
148
150
|
primary?: ThemeVariable[];
|
|
149
151
|
secondary?: ThemeVariable[];
|
|
152
|
+
shadow?: ThemeVariable[];
|
|
150
153
|
surface?: ThemeVariable[];
|
|
151
154
|
tertiary?: ThemeVariable[];
|
|
152
155
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
analyzePaletteConsistencyTool,
|
|
5
|
+
generateSemanticPaletteTool,
|
|
6
|
+
generateStateColorsTool,
|
|
7
|
+
generateTonalScaleTool,
|
|
8
|
+
} from "../cohesion.tools.js";
|
|
9
|
+
|
|
10
|
+
describe("generateTonalScaleTool", () => {
|
|
11
|
+
it("generates 11 stops by default", async () => {
|
|
12
|
+
const result = (await generateTonalScaleTool.execute({
|
|
13
|
+
seed: "#6750a4",
|
|
14
|
+
})) as string;
|
|
15
|
+
expect(result).toContain("| 50 |");
|
|
16
|
+
expect(result).toContain("| 500 |");
|
|
17
|
+
expect(result).toContain("| 950 |");
|
|
18
|
+
expect(result.match(/^\| \d+ \|/gm)?.length ?? 0).toBeGreaterThanOrEqual(
|
|
19
|
+
11,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("emits CSS custom properties with the given prefix", async () => {
|
|
24
|
+
const result = (await generateTonalScaleTool.execute({
|
|
25
|
+
name: "brand",
|
|
26
|
+
seed: "#6750a4",
|
|
27
|
+
})) as string;
|
|
28
|
+
expect(result).toContain("--brand-50:");
|
|
29
|
+
expect(result).toContain("--brand-950:");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("rejects invalid colors", async () => {
|
|
33
|
+
const result = await generateTonalScaleTool.execute({ seed: "nope" });
|
|
34
|
+
expect(result).toMatch(/Invalid color format/);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("generateStateColorsTool", () => {
|
|
39
|
+
it("produces all expected interaction states", async () => {
|
|
40
|
+
const result = (await generateStateColorsTool.execute({
|
|
41
|
+
base: "#6750a4",
|
|
42
|
+
})) as string;
|
|
43
|
+
for (const state of [
|
|
44
|
+
"base",
|
|
45
|
+
"hover",
|
|
46
|
+
"active",
|
|
47
|
+
"pressed",
|
|
48
|
+
"focus",
|
|
49
|
+
"disabled",
|
|
50
|
+
"selected",
|
|
51
|
+
]) {
|
|
52
|
+
expect(result).toContain(`| ${state} |`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("analyzePaletteConsistencyTool", () => {
|
|
58
|
+
it("returns a cohesion score for a small palette", async () => {
|
|
59
|
+
const result = (await analyzePaletteConsistencyTool.execute({
|
|
60
|
+
colors: ["#6750a4", "#7f67be", "#a48dc8"],
|
|
61
|
+
})) as string;
|
|
62
|
+
expect(result).toContain("Overall cohesion");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("flags outliers", async () => {
|
|
66
|
+
const result = (await analyzePaletteConsistencyTool.execute({
|
|
67
|
+
colors: ["#6750a4", "#7f67be", "#a48dc8", "#ff0000"],
|
|
68
|
+
})) as string;
|
|
69
|
+
expect(result).toMatch(/outlier/i);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("requires at least 2 colors", async () => {
|
|
73
|
+
const result = await analyzePaletteConsistencyTool.execute({
|
|
74
|
+
colors: ["#6750a4"],
|
|
75
|
+
});
|
|
76
|
+
expect(result).toMatch(/at least 2/i);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("generateSemanticPaletteTool", () => {
|
|
81
|
+
it("returns all 7 semantic roles", async () => {
|
|
82
|
+
const result = (await generateSemanticPaletteTool.execute({
|
|
83
|
+
brand: "#6750a4",
|
|
84
|
+
})) as string;
|
|
85
|
+
for (const role of [
|
|
86
|
+
"primary",
|
|
87
|
+
"secondary",
|
|
88
|
+
"tertiary",
|
|
89
|
+
"success",
|
|
90
|
+
"warning",
|
|
91
|
+
"error",
|
|
92
|
+
"info",
|
|
93
|
+
]) {
|
|
94
|
+
expect(result).toContain(`--color-${role}:`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { simulateCvd } from "../../color/color-blindness.js";
|
|
4
|
+
import {
|
|
5
|
+
checkPaletteAccessibilityTool,
|
|
6
|
+
simulateColorBlindnessTool,
|
|
7
|
+
} from "../color-blindness.tool.js";
|
|
8
|
+
|
|
9
|
+
describe("simulateCvd", () => {
|
|
10
|
+
it("achromatopsia collapses to grayscale", () => {
|
|
11
|
+
const r = simulateCvd({ b: 70, g: 80, r: 230 }, "achromatopsia");
|
|
12
|
+
expect(r.r).toBe(r.g);
|
|
13
|
+
expect(r.g).toBe(r.b);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("protanopia shifts pure red toward yellow/olive", () => {
|
|
17
|
+
const r = simulateCvd({ b: 0, g: 0, r: 255 }, "protanopia");
|
|
18
|
+
// Protanopes lose long-wavelength sensitivity → red becomes dark/yellow-ish.
|
|
19
|
+
expect(r.r).toBeLessThan(255);
|
|
20
|
+
expect(r.g).toBeGreaterThan(0);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("simulateColorBlindnessTool", () => {
|
|
25
|
+
it("returns a row per color", async () => {
|
|
26
|
+
const result = (await simulateColorBlindnessTool.execute({
|
|
27
|
+
colors: ["#e63946", "#2a9d8f"],
|
|
28
|
+
types: ["protanopia"],
|
|
29
|
+
})) as string;
|
|
30
|
+
expect(result).toContain("#e63946");
|
|
31
|
+
expect(result).toContain("#2a9d8f");
|
|
32
|
+
expect(result).toContain("protanopia");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("checkPaletteAccessibilityTool", () => {
|
|
37
|
+
it("audits a 3-color palette across CVD types", async () => {
|
|
38
|
+
const result = (await checkPaletteAccessibilityTool.execute({
|
|
39
|
+
colors: ["#000000", "#888888", "#ffffff"],
|
|
40
|
+
})) as string;
|
|
41
|
+
expect(result).toContain("Palette Accessibility Audit");
|
|
42
|
+
expect(result).toContain("protanopia");
|
|
43
|
+
expect(result).toContain("Summary");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { colorConversionTool } from "../color-conversion.tool.js";
|
|
4
|
+
|
|
5
|
+
describe("colorConversionTool", () => {
|
|
6
|
+
// Regression: previously multiplied by 255 again, producing rgb(26265, ...).
|
|
7
|
+
it("converts hex to rgb without re-scaling", async () => {
|
|
8
|
+
const result = await colorConversionTool.execute({
|
|
9
|
+
color: "#6750a4",
|
|
10
|
+
to: "rgb",
|
|
11
|
+
});
|
|
12
|
+
expect(result).toBe("rgb(103, 80, 164)");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("converts hex to hex (round-trip)", async () => {
|
|
16
|
+
const result = await colorConversionTool.execute({
|
|
17
|
+
color: "#6750a4",
|
|
18
|
+
to: "hex",
|
|
19
|
+
});
|
|
20
|
+
expect(result).toBe("#6750a4");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("converts hex to hsl", async () => {
|
|
24
|
+
const result = await colorConversionTool.execute({
|
|
25
|
+
color: "#6750a4",
|
|
26
|
+
to: "hsl",
|
|
27
|
+
});
|
|
28
|
+
expect(result).toMatch(/^hsl\(\d+, \d+%, \d+%\)$/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("rejects invalid colors", async () => {
|
|
32
|
+
const result = await colorConversionTool.execute({
|
|
33
|
+
color: "not-a-color",
|
|
34
|
+
to: "rgb",
|
|
35
|
+
});
|
|
36
|
+
expect(result).toMatch(/Invalid color format/);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { apcaContrast } from "../../color/apca.js";
|
|
4
|
+
import { contrastCheckerTool } from "../contrast-checker.tool.js";
|
|
5
|
+
|
|
6
|
+
describe("apcaContrast", () => {
|
|
7
|
+
// Reference values from Myndex/SAPC-APCA test vectors
|
|
8
|
+
it("matches reference: #888 on #fff ≈ 63", () => {
|
|
9
|
+
const lc = apcaContrast(
|
|
10
|
+
{ b: 0x88, g: 0x88, r: 0x88 },
|
|
11
|
+
{ b: 255, g: 255, r: 255 },
|
|
12
|
+
);
|
|
13
|
+
expect(lc).toBeCloseTo(63.06, 1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("matches reference: #000 on #fff ≈ 106", () => {
|
|
17
|
+
const lc = apcaContrast({ b: 0, g: 0, r: 0 }, { b: 255, g: 255, r: 255 });
|
|
18
|
+
expect(lc).toBeCloseTo(106.04, 1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("matches reference: #fff on #000 ≈ -107", () => {
|
|
22
|
+
const lc = apcaContrast({ b: 255, g: 255, r: 255 }, { b: 0, g: 0, r: 0 });
|
|
23
|
+
expect(lc).toBeCloseTo(-107.28, 1);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("contrastCheckerTool", () => {
|
|
28
|
+
it("returns WCAG block by default", async () => {
|
|
29
|
+
const result = await contrastCheckerTool.execute({
|
|
30
|
+
background: "#ffffff",
|
|
31
|
+
foreground: "#000000",
|
|
32
|
+
});
|
|
33
|
+
expect(result).toContain("WCAG 2.x");
|
|
34
|
+
expect(result).toContain("21.00:1");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns APCA when requested", async () => {
|
|
38
|
+
const result = await contrastCheckerTool.execute({
|
|
39
|
+
algorithm: "apca",
|
|
40
|
+
background: "#ffffff",
|
|
41
|
+
foreground: "#000000",
|
|
42
|
+
});
|
|
43
|
+
expect(result).toContain("APCA");
|
|
44
|
+
expect(result).toContain("Lc:");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns both when algorithm=both", async () => {
|
|
48
|
+
const result = await contrastCheckerTool.execute({
|
|
49
|
+
algorithm: "both",
|
|
50
|
+
background: "#ffffff",
|
|
51
|
+
foreground: "#000000",
|
|
52
|
+
});
|
|
53
|
+
expect(result).toContain("WCAG 2.x");
|
|
54
|
+
expect(result).toContain("APCA");
|
|
55
|
+
});
|
|
56
|
+
});
|