@trishchuk/coolors-mcp 1.0.0 → 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/ISSUE_TEMPLATE/bug_report.md +20 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +22 -8
- package/.github/pull_request_template.md +33 -8
- package/.github/workflows/ci.yml +107 -104
- package/.github/workflows/deploy-docs.yml +14 -11
- package/.github/workflows/release.yml +25 -23
- package/README.md +149 -15
- 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/docs/.vitepress/components/ClientGrid.vue +9 -3
- package/docs/.vitepress/components/CodeBlock.vue +51 -44
- package/docs/.vitepress/components/ConfigModal.vue +151 -67
- package/docs/.vitepress/components/DiagramModal.vue +186 -154
- package/docs/.vitepress/components/TroubleshootingModal.vue +101 -96
- package/docs/.vitepress/config.js +171 -141
- package/docs/.vitepress/theme/FundingLayout.vue +65 -54
- package/docs/.vitepress/theme/Layout.vue +21 -21
- package/docs/.vitepress/theme/components/AdBanner.vue +73 -52
- package/docs/.vitepress/theme/components/AdPlaceholder.vue +3 -3
- package/docs/.vitepress/theme/components/FundingEffects.vue +77 -53
- package/docs/.vitepress/theme/components/FundingHero.vue +78 -63
- package/docs/.vitepress/theme/components/SupportSection.vue +106 -89
- package/docs/.vitepress/theme/custom-app.css +19 -12
- package/docs/.vitepress/theme/custom.css +33 -25
- package/docs/.vitepress/theme/index.js +19 -16
- package/docs/concepts/accessibility.md +59 -47
- package/docs/concepts/color-spaces.md +28 -6
- package/docs/concepts/distance-metrics.md +45 -30
- package/docs/concepts/hct.md +30 -27
- package/docs/concepts/image-analysis.md +52 -21
- package/docs/concepts/material-design.md +43 -17
- package/docs/concepts/theme-matching.md +64 -40
- package/docs/examples/basic-colors.md +92 -108
- package/docs/examples/creating-themes.md +104 -108
- package/docs/examples/css-refactoring.md +33 -29
- package/docs/examples/image-extraction.md +145 -138
- package/docs/getting-started.md +45 -34
- package/docs/index.md +5 -1
- package/docs/installation.md +15 -1
- package/docs/tools/accessibility.md +74 -68
- package/docs/tools/image-extraction.md +62 -54
- package/docs/tools/theme-matching.md +45 -42
- package/eslint.config.ts +13 -0
- package/jsr.json +1 -1
- package/package.json +17 -13
- 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 -39
- 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/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 -93
- 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 -9
- package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +0 -7
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +0 -12683
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +0 -7
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +0 -9719
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +0 -7
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +0 -4710
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +0 -7
- package/docs/.vitepress/cache/deps/cytoscape.js +0 -30278
- package/docs/.vitepress/cache/deps/cytoscape.js.map +0 -7
- package/docs/.vitepress/cache/deps/dayjs.js +0 -285
- package/docs/.vitepress/cache/deps/dayjs.js.map +0 -7
- package/docs/.vitepress/cache/deps/debug.js +0 -468
- 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 -1466
- package/docs/.vitepress/cache/deps/prismjs.js.map +0 -7
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +0 -228
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +0 -7
- package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +0 -142
- 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 -65
- package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +0 -7
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +0 -53
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +0 -7
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +0 -73
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +0 -7
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -4507
- 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 -1146
- 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 -1667
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +0 -7
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js +0 -1814
- 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 -35
- 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/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
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { exportPaletteTool } from "../palette-export.tool.js";
|
|
4
|
+
|
|
5
|
+
describe("exportPaletteTool", () => {
|
|
6
|
+
it("exports CSS custom properties", async () => {
|
|
7
|
+
const result = (await exportPaletteTool.execute({
|
|
8
|
+
colors: ["#6750a4", "#ff6b6b"],
|
|
9
|
+
format: "css",
|
|
10
|
+
names: ["primary", "danger"],
|
|
11
|
+
})) as string;
|
|
12
|
+
expect(result).toContain("--primary: #6750a4;");
|
|
13
|
+
expect(result).toContain("--danger: #ff6b6b;");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("exports SCSS variables", async () => {
|
|
17
|
+
const result = (await exportPaletteTool.execute({
|
|
18
|
+
colors: ["#6750a4"],
|
|
19
|
+
format: "scss",
|
|
20
|
+
names: ["primary"],
|
|
21
|
+
})) as string;
|
|
22
|
+
expect(result).toContain("$primary: #6750a4;");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("exports Tailwind config snippet", async () => {
|
|
26
|
+
const result = (await exportPaletteTool.execute({
|
|
27
|
+
colors: ["#fef3c7", "#fde68a"],
|
|
28
|
+
format: "tailwind",
|
|
29
|
+
prefix: "amber",
|
|
30
|
+
})) as string;
|
|
31
|
+
expect(result).toContain("tailwind.config.js");
|
|
32
|
+
expect(result).toContain('"amber"');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("exports W3C design tokens", async () => {
|
|
36
|
+
const result = (await exportPaletteTool.execute({
|
|
37
|
+
colors: ["#6750a4"],
|
|
38
|
+
format: "tokens",
|
|
39
|
+
names: ["primary"],
|
|
40
|
+
})) as string;
|
|
41
|
+
const parsed = JSON.parse(result);
|
|
42
|
+
expect(parsed.color.primary.$type).toBe("color");
|
|
43
|
+
expect(parsed.color.primary.$value).toBe("#6750a4");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("rejects mismatched names length", async () => {
|
|
47
|
+
const result = await exportPaletteTool.execute({
|
|
48
|
+
colors: ["#000", "#fff"],
|
|
49
|
+
format: "css",
|
|
50
|
+
names: ["only-one"],
|
|
51
|
+
});
|
|
52
|
+
expect(result).toMatch(/length/);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color Adjust / Mix Tool
|
|
3
|
+
* Exposes the lighten/darken/saturate/desaturate/grayscale/invert/mix utilities
|
|
4
|
+
* via MCP.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
darken,
|
|
11
|
+
desaturate,
|
|
12
|
+
invertColor,
|
|
13
|
+
lighten,
|
|
14
|
+
mixColors,
|
|
15
|
+
parseColor,
|
|
16
|
+
rgbToHex,
|
|
17
|
+
saturate,
|
|
18
|
+
toGrayscale,
|
|
19
|
+
} from "../color/index.js";
|
|
20
|
+
|
|
21
|
+
const OPS = [
|
|
22
|
+
"lighten",
|
|
23
|
+
"darken",
|
|
24
|
+
"saturate",
|
|
25
|
+
"desaturate",
|
|
26
|
+
"grayscale",
|
|
27
|
+
"invert",
|
|
28
|
+
"mix",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export const adjustColorTool = {
|
|
32
|
+
description:
|
|
33
|
+
"Adjust a color: lighten, darken, saturate, desaturate, grayscale, invert, or mix with a second color. Amount is 0-100 (percent) for lighten/darken/saturate/desaturate, 0-1 for mix weight.",
|
|
34
|
+
execute: async (args: {
|
|
35
|
+
amount?: number;
|
|
36
|
+
color: string;
|
|
37
|
+
operation: (typeof OPS)[number];
|
|
38
|
+
with?: string;
|
|
39
|
+
}) => {
|
|
40
|
+
const rgb = parseColor(args.color);
|
|
41
|
+
if (!rgb) return `Invalid color format: ${args.color}`;
|
|
42
|
+
|
|
43
|
+
switch (args.operation) {
|
|
44
|
+
case "darken":
|
|
45
|
+
return rgbToHex(darken(rgb, args.amount ?? 10));
|
|
46
|
+
case "desaturate":
|
|
47
|
+
return rgbToHex(desaturate(rgb, args.amount ?? 10));
|
|
48
|
+
case "grayscale":
|
|
49
|
+
return rgbToHex(toGrayscale(rgb));
|
|
50
|
+
case "invert":
|
|
51
|
+
return rgbToHex(invertColor(rgb));
|
|
52
|
+
case "lighten":
|
|
53
|
+
return rgbToHex(lighten(rgb, args.amount ?? 10));
|
|
54
|
+
case "mix": {
|
|
55
|
+
if (!args.with) return "Error: 'with' is required for mix operation";
|
|
56
|
+
const other = parseColor(args.with);
|
|
57
|
+
if (!other) return `Invalid color format: ${args.with}`;
|
|
58
|
+
const weight = args.amount ?? 0.5;
|
|
59
|
+
if (weight < 0 || weight > 1) {
|
|
60
|
+
return "Error: mix amount must be between 0 and 1";
|
|
61
|
+
}
|
|
62
|
+
return rgbToHex(mixColors(rgb, other, weight));
|
|
63
|
+
}
|
|
64
|
+
case "saturate":
|
|
65
|
+
return rgbToHex(saturate(rgb, args.amount ?? 10));
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
name: "adjust_color",
|
|
69
|
+
parameters: z.object({
|
|
70
|
+
amount: z
|
|
71
|
+
.number()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe(
|
|
74
|
+
"Amount of change. For lighten/darken/saturate/desaturate: 0-100 (percent). For mix: 0-1 (weight of first color). Default 10 / 0.5.",
|
|
75
|
+
),
|
|
76
|
+
color: z.string().describe("Color to adjust (hex, rgb, hsl)"),
|
|
77
|
+
operation: z.enum(OPS).describe("Adjustment operation"),
|
|
78
|
+
with: z.string().optional().describe("Second color (only for mix)"),
|
|
79
|
+
}),
|
|
80
|
+
};
|