@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,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Parser
|
|
3
|
+
* Parses CSS variables and extracts theme colors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SemanticRole, ThemeVariable, ThemeVariables } from "./types.js";
|
|
7
|
+
|
|
8
|
+
import { hexToRgb } from "../color/conversions.js";
|
|
9
|
+
import { rgbToHct } from "../color/hct/hct-solver.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse CSS content and extract theme variables
|
|
13
|
+
*/
|
|
14
|
+
export function parseThemeVariables(css: string): ThemeVariables {
|
|
15
|
+
const variables: ThemeVariable[] = [];
|
|
16
|
+
|
|
17
|
+
// Match CSS custom properties
|
|
18
|
+
const varRegex = /--([\w-]+):\s*([^;]+);/g;
|
|
19
|
+
let match;
|
|
20
|
+
|
|
21
|
+
while ((match = varRegex.exec(css)) !== null) {
|
|
22
|
+
const [, name, value] = match;
|
|
23
|
+
const fullName = `--${name}`;
|
|
24
|
+
const trimmedValue = value.trim();
|
|
25
|
+
|
|
26
|
+
// Check if it's a color value
|
|
27
|
+
if (isColorValue(trimmedValue)) {
|
|
28
|
+
const colorValue = normalizeColorValue(trimmedValue);
|
|
29
|
+
if (colorValue) {
|
|
30
|
+
try {
|
|
31
|
+
const rgb = parseColorToRgb(colorValue);
|
|
32
|
+
const hct = rgbToHct(rgb);
|
|
33
|
+
|
|
34
|
+
variables.push({
|
|
35
|
+
hct,
|
|
36
|
+
name: fullName,
|
|
37
|
+
role: detectSemanticRole(name),
|
|
38
|
+
tone: hct.t,
|
|
39
|
+
value: colorValue,
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
// Skip invalid color values
|
|
43
|
+
console.warn(
|
|
44
|
+
`Failed to parse color variable ${fullName}: ${trimmedValue}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Organize variables by role
|
|
52
|
+
return organizeByRole(variables);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse CSS variables from an object (e.g., from getComputedStyle)
|
|
57
|
+
*/
|
|
58
|
+
export function parseThemeVariablesFromObject(
|
|
59
|
+
variables: Record<string, string>,
|
|
60
|
+
): ThemeVariables {
|
|
61
|
+
const themeVars: ThemeVariable[] = [];
|
|
62
|
+
|
|
63
|
+
for (const [name, value] of Object.entries(variables)) {
|
|
64
|
+
if (name.startsWith("--") && isColorValue(value)) {
|
|
65
|
+
const colorValue = normalizeColorValue(value);
|
|
66
|
+
if (colorValue) {
|
|
67
|
+
try {
|
|
68
|
+
const rgb = parseColorToRgb(colorValue);
|
|
69
|
+
const hct = rgbToHct(rgb);
|
|
70
|
+
|
|
71
|
+
themeVars.push({
|
|
72
|
+
hct,
|
|
73
|
+
name,
|
|
74
|
+
role: detectSemanticRole(name.slice(2)), // Remove -- prefix
|
|
75
|
+
tone: hct.t,
|
|
76
|
+
value: colorValue,
|
|
77
|
+
});
|
|
78
|
+
} catch {
|
|
79
|
+
console.warn(`Failed to parse color variable ${name}: ${value}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return organizeByRole(themeVars);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Detect semantic role from variable name
|
|
90
|
+
*/
|
|
91
|
+
function detectSemanticRole(name: string): SemanticRole | undefined {
|
|
92
|
+
const lower = name.toLowerCase();
|
|
93
|
+
|
|
94
|
+
if (lower.includes("primary")) return "primary";
|
|
95
|
+
if (lower.includes("secondary")) return "secondary";
|
|
96
|
+
if (lower.includes("tertiary")) return "tertiary";
|
|
97
|
+
if (lower.includes("error") || lower.includes("danger")) return "error";
|
|
98
|
+
if (lower.includes("neutral") || lower.includes("gray")) return "neutral";
|
|
99
|
+
if (lower.includes("surface")) return "surface";
|
|
100
|
+
if (lower.includes("background") || lower.includes("bg")) return "background";
|
|
101
|
+
if (lower.includes("outline") || lower.includes("border")) return "outline";
|
|
102
|
+
if (lower.includes("shadow")) return "shadow";
|
|
103
|
+
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* HSL to RGB helper
|
|
109
|
+
*/
|
|
110
|
+
function hslToRgb(
|
|
111
|
+
h: number,
|
|
112
|
+
s: number,
|
|
113
|
+
l: number,
|
|
114
|
+
): { b: number; g: number; r: number } {
|
|
115
|
+
let b, g, r;
|
|
116
|
+
|
|
117
|
+
if (s === 0) {
|
|
118
|
+
r = g = b = l;
|
|
119
|
+
} else {
|
|
120
|
+
const hue2rgb = (p: number, q: number, t: number) => {
|
|
121
|
+
if (t < 0) t += 1;
|
|
122
|
+
if (t > 1) t -= 1;
|
|
123
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
124
|
+
if (t < 1 / 2) return q;
|
|
125
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
126
|
+
return p;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
130
|
+
const p = 2 * l - q;
|
|
131
|
+
r = hue2rgb(p, q, h + 1 / 3);
|
|
132
|
+
g = hue2rgb(p, q, h);
|
|
133
|
+
b = hue2rgb(p, q, h - 1 / 3);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { b, g, r };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a value is a color
|
|
141
|
+
*/
|
|
142
|
+
function isColorValue(value: string): boolean {
|
|
143
|
+
const trimmed = value.trim();
|
|
144
|
+
|
|
145
|
+
// Hex colors
|
|
146
|
+
if (/^#[0-9a-fA-F]{3,8}$/.test(trimmed)) return true;
|
|
147
|
+
|
|
148
|
+
// RGB/RGBA
|
|
149
|
+
if (/^rgba?\(/.test(trimmed)) return true;
|
|
150
|
+
|
|
151
|
+
// HSL/HSLA
|
|
152
|
+
if (/^hsla?\(/.test(trimmed)) return true;
|
|
153
|
+
|
|
154
|
+
// Named colors (basic check)
|
|
155
|
+
if (/^[a-z]+$/i.test(trimmed)) {
|
|
156
|
+
return isNamedColor(trimmed);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if string is a named color
|
|
164
|
+
*/
|
|
165
|
+
function isNamedColor(name: string): boolean {
|
|
166
|
+
const namedColors = [
|
|
167
|
+
"black",
|
|
168
|
+
"white",
|
|
169
|
+
"red",
|
|
170
|
+
"green",
|
|
171
|
+
"blue",
|
|
172
|
+
"yellow",
|
|
173
|
+
"cyan",
|
|
174
|
+
"magenta",
|
|
175
|
+
"gray",
|
|
176
|
+
"grey",
|
|
177
|
+
"orange",
|
|
178
|
+
"purple",
|
|
179
|
+
"brown",
|
|
180
|
+
"pink",
|
|
181
|
+
"lime",
|
|
182
|
+
"navy",
|
|
183
|
+
"teal",
|
|
184
|
+
"silver",
|
|
185
|
+
"gold",
|
|
186
|
+
"indigo",
|
|
187
|
+
"violet",
|
|
188
|
+
"turquoise",
|
|
189
|
+
"coral",
|
|
190
|
+
];
|
|
191
|
+
return namedColors.includes(name.toLowerCase());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Convert named color to hex (simplified)
|
|
196
|
+
*/
|
|
197
|
+
function namedColorToHex(name: string): string {
|
|
198
|
+
const colors: Record<string, string> = {
|
|
199
|
+
black: "#000000",
|
|
200
|
+
blue: "#0000ff",
|
|
201
|
+
brown: "#a52a2a",
|
|
202
|
+
coral: "#ff7f50",
|
|
203
|
+
cyan: "#00ffff",
|
|
204
|
+
gold: "#ffd700",
|
|
205
|
+
gray: "#808080",
|
|
206
|
+
green: "#008000",
|
|
207
|
+
grey: "#808080",
|
|
208
|
+
indigo: "#4b0082",
|
|
209
|
+
lime: "#00ff00",
|
|
210
|
+
magenta: "#ff00ff",
|
|
211
|
+
navy: "#000080",
|
|
212
|
+
orange: "#ffa500",
|
|
213
|
+
pink: "#ffc0cb",
|
|
214
|
+
purple: "#800080",
|
|
215
|
+
red: "#ff0000",
|
|
216
|
+
silver: "#c0c0c0",
|
|
217
|
+
teal: "#008080",
|
|
218
|
+
turquoise: "#40e0d0",
|
|
219
|
+
violet: "#ee82ee",
|
|
220
|
+
white: "#ffffff",
|
|
221
|
+
yellow: "#ffff00",
|
|
222
|
+
};
|
|
223
|
+
return colors[name.toLowerCase()] || "#000000";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Normalize color value to a standard format
|
|
228
|
+
*/
|
|
229
|
+
function normalizeColorValue(value: string): null | string {
|
|
230
|
+
const trimmed = value.trim();
|
|
231
|
+
|
|
232
|
+
// Already in a good format
|
|
233
|
+
if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) {
|
|
234
|
+
return trimmed;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Short hex
|
|
238
|
+
if (/^#[0-9a-fA-F]{3}$/.test(trimmed)) {
|
|
239
|
+
const [, r, g, b] = trimmed.match(/#(.)(.)(.)/)!;
|
|
240
|
+
return `#${r}${r}${g}${g}${b}${b}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// RGB/RGBA
|
|
244
|
+
if (/^rgba?\(/.test(trimmed)) {
|
|
245
|
+
return parseRgbString(trimmed);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// HSL/HSLA
|
|
249
|
+
if (/^hsla?\(/.test(trimmed)) {
|
|
250
|
+
return parseHslString(trimmed);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Named colors
|
|
254
|
+
if (isNamedColor(trimmed)) {
|
|
255
|
+
return namedColorToHex(trimmed);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Organize variables by semantic role
|
|
263
|
+
*/
|
|
264
|
+
function organizeByRole(variables: ThemeVariable[]): ThemeVariables {
|
|
265
|
+
const organized: ThemeVariables = {
|
|
266
|
+
custom: {},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
for (const variable of variables) {
|
|
270
|
+
if (variable.role) {
|
|
271
|
+
const role = variable.role;
|
|
272
|
+
if (!organized[role]) {
|
|
273
|
+
organized[role] = [];
|
|
274
|
+
}
|
|
275
|
+
organized[role].push(variable);
|
|
276
|
+
} else {
|
|
277
|
+
// Extract custom role from name pattern (e.g., --color-brand-50, --accent-warm)
|
|
278
|
+
const colorPattern = variable.name.match(/--(?:color-)?([a-z]+)-\d+/i);
|
|
279
|
+
const namedPattern = variable.name.match(/--([a-z]+)-[a-z]+/i);
|
|
280
|
+
|
|
281
|
+
if (colorPattern) {
|
|
282
|
+
const customRole = colorPattern[1].toLowerCase();
|
|
283
|
+
if (!organized.custom![customRole]) {
|
|
284
|
+
organized.custom![customRole] = [];
|
|
285
|
+
}
|
|
286
|
+
organized.custom![customRole].push(variable);
|
|
287
|
+
} else if (namedPattern) {
|
|
288
|
+
const customRole = namedPattern[1].toLowerCase();
|
|
289
|
+
// Only use this pattern if it's a known semantic pattern
|
|
290
|
+
const knownPatterns = [
|
|
291
|
+
"accent",
|
|
292
|
+
"neutral",
|
|
293
|
+
"success",
|
|
294
|
+
"warning",
|
|
295
|
+
"info",
|
|
296
|
+
];
|
|
297
|
+
if (knownPatterns.some((p) => customRole.includes(p))) {
|
|
298
|
+
if (!organized.custom![customRole]) {
|
|
299
|
+
organized.custom![customRole] = [];
|
|
300
|
+
}
|
|
301
|
+
organized.custom![customRole].push(variable);
|
|
302
|
+
} else {
|
|
303
|
+
// Unknown pattern, put in uncategorized
|
|
304
|
+
if (!organized.custom!["uncategorized"]) {
|
|
305
|
+
organized.custom!["uncategorized"] = [];
|
|
306
|
+
}
|
|
307
|
+
organized.custom!["uncategorized"].push(variable);
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
// Uncategorized
|
|
311
|
+
if (!organized.custom!["uncategorized"]) {
|
|
312
|
+
organized.custom!["uncategorized"] = [];
|
|
313
|
+
}
|
|
314
|
+
organized.custom!["uncategorized"].push(variable);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Sort each role by tone
|
|
320
|
+
for (const role of Object.keys(organized)) {
|
|
321
|
+
if (Array.isArray(organized[role as keyof ThemeVariables])) {
|
|
322
|
+
(organized[role as keyof ThemeVariables] as ThemeVariable[]).sort(
|
|
323
|
+
(a, b) => a.tone - b.tone,
|
|
324
|
+
);
|
|
325
|
+
} else if (role === "custom" && organized.custom) {
|
|
326
|
+
for (const customRole of Object.keys(organized.custom)) {
|
|
327
|
+
organized.custom[customRole].sort((a, b) => a.tone - b.tone);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return organized;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Parse color to RGB
|
|
337
|
+
*/
|
|
338
|
+
function parseColorToRgb(color: string): { b: number; g: number; r: number } {
|
|
339
|
+
// Hex color
|
|
340
|
+
if (color.startsWith("#")) {
|
|
341
|
+
return hexToRgb(color);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// RGB string
|
|
345
|
+
if (color.startsWith("rgb")) {
|
|
346
|
+
const match = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
347
|
+
if (match) {
|
|
348
|
+
return {
|
|
349
|
+
b: parseInt(match[3], 10) / 255,
|
|
350
|
+
g: parseInt(match[2], 10) / 255,
|
|
351
|
+
r: parseInt(match[1], 10) / 255,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
throw new Error(`Unable to parse color: ${color}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Parse HSL/HSLA string to hex
|
|
361
|
+
*/
|
|
362
|
+
function parseHslString(hsl: string): null | string {
|
|
363
|
+
const match = hsl.match(/hsla?\((\d+),\s*(\d+)%,\s*(\d+)%/);
|
|
364
|
+
if (match) {
|
|
365
|
+
const h = parseInt(match[1], 10) / 360;
|
|
366
|
+
const s = parseInt(match[2], 10) / 100;
|
|
367
|
+
const l = parseInt(match[3], 10) / 100;
|
|
368
|
+
|
|
369
|
+
// HSL to RGB conversion
|
|
370
|
+
const rgb = hslToRgb(h, s, l);
|
|
371
|
+
const r = Math.round(rgb.r * 255);
|
|
372
|
+
const g = Math.round(rgb.g * 255);
|
|
373
|
+
const b = Math.round(rgb.b * 255);
|
|
374
|
+
|
|
375
|
+
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Parse RGB/RGBA string to hex
|
|
382
|
+
*/
|
|
383
|
+
function parseRgbString(rgb: string): null | string {
|
|
384
|
+
const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
385
|
+
if (match) {
|
|
386
|
+
const r = parseInt(match[1], 10);
|
|
387
|
+
const g = parseInt(match[2], 10);
|
|
388
|
+
const b = parseInt(match[3], 10);
|
|
389
|
+
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|