@trishchuk/coolors-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/.claude/settings.local.json +39 -0
  2. package/.env +2 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +73 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +71 -0
  5. package/.github/pull_request_template.md +97 -0
  6. package/.github/workflows/ci.yml +127 -0
  7. package/.github/workflows/deploy-docs.yml +56 -0
  8. package/.github/workflows/release.yml +99 -0
  9. package/.mcp.json +12 -0
  10. package/.prettierignore +1 -0
  11. package/CLAUDE.md +201 -0
  12. package/DOCUMENTATION.md +274 -0
  13. package/GEMINI.md +54 -0
  14. package/LICENSE +21 -0
  15. package/README.md +401 -0
  16. package/demo/content_based_color.png +0 -0
  17. package/demo/music-player.html +621 -0
  18. package/demo/podcast-player.html +903 -0
  19. package/dist/bin/coolors-mcp.d.ts +1 -0
  20. package/dist/bin/coolors-mcp.js +154 -0
  21. package/dist/bin/coolors-mcp.js.map +1 -0
  22. package/dist/bin/server.d.ts +1 -0
  23. package/dist/bin/server.js +3292 -0
  24. package/dist/bin/server.js.map +1 -0
  25. package/dist/chunk-IQ7NN26V.js +114 -0
  26. package/dist/chunk-IQ7NN26V.js.map +1 -0
  27. package/dist/chunk-P3ARRKLS.js +1214 -0
  28. package/dist/chunk-P3ARRKLS.js.map +1 -0
  29. package/dist/color/index.d.ts +716 -0
  30. package/dist/color/index.js +153 -0
  31. package/dist/color/index.js.map +1 -0
  32. package/dist/coolors-mcp.d.ts +136 -0
  33. package/dist/coolors-mcp.js +7 -0
  34. package/dist/coolors-mcp.js.map +1 -0
  35. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
  36. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
  37. package/docs/.vitepress/cache/deps/_metadata.json +127 -0
  38. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
  39. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
  40. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +12683 -0
  41. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +7 -0
  42. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +9719 -0
  43. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +7 -0
  44. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
  45. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
  46. package/docs/.vitepress/cache/deps/cytoscape.js +30278 -0
  47. package/docs/.vitepress/cache/deps/cytoscape.js.map +7 -0
  48. package/docs/.vitepress/cache/deps/dayjs.js +285 -0
  49. package/docs/.vitepress/cache/deps/dayjs.js.map +7 -0
  50. package/docs/.vitepress/cache/deps/debug.js +468 -0
  51. package/docs/.vitepress/cache/deps/debug.js.map +7 -0
  52. package/docs/.vitepress/cache/deps/package.json +3 -0
  53. package/docs/.vitepress/cache/deps/prismjs.js +1466 -0
  54. package/docs/.vitepress/cache/deps/prismjs.js.map +7 -0
  55. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +228 -0
  56. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +7 -0
  57. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +142 -0
  58. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +7 -0
  59. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +27 -0
  60. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +7 -0
  61. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +65 -0
  62. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +7 -0
  63. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +53 -0
  64. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +7 -0
  65. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +73 -0
  66. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +7 -0
  67. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
  68. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
  69. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
  70. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
  71. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1146 -0
  72. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
  73. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
  74. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
  75. package/docs/.vitepress/cache/deps/vitepress___minisearch.js +1814 -0
  76. package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
  77. package/docs/.vitepress/cache/deps/vue.js +344 -0
  78. package/docs/.vitepress/cache/deps/vue.js.map +7 -0
  79. package/docs/.vitepress/components/ClientGrid.vue +125 -0
  80. package/docs/.vitepress/components/CodeBlock.vue +231 -0
  81. package/docs/.vitepress/components/ConfigModal.vue +477 -0
  82. package/docs/.vitepress/components/DiagramModal.vue +528 -0
  83. package/docs/.vitepress/components/TroubleshootingModal.vue +472 -0
  84. package/docs/.vitepress/config.js +162 -0
  85. package/docs/.vitepress/theme/FundingLayout.vue +251 -0
  86. package/docs/.vitepress/theme/Layout.vue +134 -0
  87. package/docs/.vitepress/theme/components/AdBanner.vue +317 -0
  88. package/docs/.vitepress/theme/components/AdPlaceholder.vue +78 -0
  89. package/docs/.vitepress/theme/components/FundingEffects.vue +322 -0
  90. package/docs/.vitepress/theme/components/FundingHero.vue +345 -0
  91. package/docs/.vitepress/theme/components/SupportSection.vue +511 -0
  92. package/docs/.vitepress/theme/custom-app.css +339 -0
  93. package/docs/.vitepress/theme/custom.css +699 -0
  94. package/docs/.vitepress/theme/index.js +25 -0
  95. package/docs/README.md +198 -0
  96. package/docs/concepts/accessibility.md +473 -0
  97. package/docs/concepts/color-spaces.md +222 -0
  98. package/docs/concepts/distance-metrics.md +384 -0
  99. package/docs/concepts/hct.md +261 -0
  100. package/docs/concepts/image-analysis.md +396 -0
  101. package/docs/concepts/material-design.md +306 -0
  102. package/docs/concepts/theme-matching.md +399 -0
  103. package/docs/examples/basic-colors.md +490 -0
  104. package/docs/examples/creating-themes.md +898 -0
  105. package/docs/examples/css-refactoring.md +824 -0
  106. package/docs/examples/image-extraction.md +882 -0
  107. package/docs/getting-started.md +366 -0
  108. package/docs/index.md +190 -0
  109. package/docs/installation.md +157 -0
  110. package/docs/tools/README.md +234 -0
  111. package/docs/tools/accessibility.md +614 -0
  112. package/docs/tools/color-operations.md +374 -0
  113. package/docs/tools/image-extraction.md +624 -0
  114. package/docs/tools/material-design.md +347 -0
  115. package/docs/tools/theme-matching.md +552 -0
  116. package/eslint.config.ts +14 -0
  117. package/examples/theme-matching.md +113 -0
  118. package/jsr.json +7 -0
  119. package/mcp-config.json +8 -0
  120. package/note.md +35 -0
  121. package/package.json +122 -0
  122. package/research_results.md +53 -0
  123. package/src/bin/coolors-mcp.ts +194 -0
  124. package/src/bin/server.ts +61 -0
  125. package/src/color/__tests__/conversions-argb.test.ts +198 -0
  126. package/src/color/__tests__/extract-colors.test.ts +360 -0
  127. package/src/color/__tests__/image-utils.test.ts +242 -0
  128. package/src/color/__tests__/reference-colors.test.ts +278 -0
  129. package/src/color/__tests__/round-trip.test.ts +197 -0
  130. package/src/color/conversions.test.ts +402 -0
  131. package/src/color/conversions.ts +393 -0
  132. package/src/color/dislike/__tests__/dislike-analyzer.test.ts +248 -0
  133. package/src/color/dislike/dislike-analyzer.ts +114 -0
  134. package/src/color/extract-colors.ts +228 -0
  135. package/src/color/hct/__tests__/hct-class.test.ts +232 -0
  136. package/src/color/hct/harmonization.ts +204 -0
  137. package/src/color/hct/hct-class.ts +109 -0
  138. package/src/color/hct/hct-solver.ts +168 -0
  139. package/src/color/hct/index.ts +39 -0
  140. package/src/color/hct/tonal-palette.ts +211 -0
  141. package/src/color/hct/types.ts +88 -0
  142. package/src/color/image-utils.ts +79 -0
  143. package/src/color/index.ts +87 -0
  144. package/src/color/material-theme.ts +157 -0
  145. package/src/color/metrics.test.ts +276 -0
  146. package/src/color/metrics.ts +281 -0
  147. package/src/color/quantize/__tests__/quantizer_celebi.test.ts +195 -0
  148. package/src/color/quantize/lab_point_provider.ts +55 -0
  149. package/src/color/quantize/point_provider.ts +27 -0
  150. package/src/color/quantize/quantizer_celebi.ts +51 -0
  151. package/src/color/quantize/quantizer_celebi_test.ts +71 -0
  152. package/src/color/quantize/quantizer_map.ts +47 -0
  153. package/src/color/quantize/quantizer_wsmeans.ts +232 -0
  154. package/src/color/quantize/quantizer_wu.ts +472 -0
  155. package/src/color/score/__tests__/score.test.ts +224 -0
  156. package/src/color/score/score.ts +175 -0
  157. package/src/color/types.ts +151 -0
  158. package/src/color/utils/color_utils.ts +292 -0
  159. package/src/color/utils/math_utils.ts +145 -0
  160. package/src/color/utils.test.ts +403 -0
  161. package/src/color/utils.ts +315 -0
  162. package/src/constants.ts +5 -0
  163. package/src/coolors-mcp.ts +37 -0
  164. package/src/examples/addition.ts +333 -0
  165. package/src/examples/color-demo.ts +125 -0
  166. package/src/examples/custom-logger.ts +201 -0
  167. package/src/examples/oauth-server.ts +113 -0
  168. package/src/examples/session-context.ts +269 -0
  169. package/src/session.ts +116 -0
  170. package/src/theme/__tests__/matcher.test.ts +180 -0
  171. package/src/theme/__tests__/parser.test.ts +148 -0
  172. package/src/theme/__tests__/refactor.test.ts +224 -0
  173. package/src/theme/index.ts +34 -0
  174. package/src/theme/matcher.ts +395 -0
  175. package/src/theme/parser.ts +392 -0
  176. package/src/theme/refactor.ts +360 -0
  177. package/src/theme/types.ts +152 -0
  178. package/src/tools/__tests__/gradient-generator.test.ts +206 -0
  179. package/src/tools/__tests__/palette-with-locks.test.ts +109 -0
  180. package/src/tools/color-conversion.tool.ts +54 -0
  181. package/src/tools/color-distance.tool.ts +41 -0
  182. package/src/tools/colors.ts +31 -0
  183. package/src/tools/contrast-checker.tool.ts +37 -0
  184. package/src/tools/dislike-analyzer.tool.ts +247 -0
  185. package/src/tools/gradient-generator.tool.ts +250 -0
  186. package/src/tools/image-extraction.tools.ts +289 -0
  187. package/src/tools/index.ts +39 -0
  188. package/src/tools/material-theme.tools.ts +250 -0
  189. package/src/tools/palette-generator.tool.ts +135 -0
  190. package/src/tools/palette-with-locks.tool.ts +221 -0
  191. package/src/tools/registry.ts +142 -0
  192. package/src/tools/simple-tools.ts +37 -0
  193. package/src/tools/theme-matching.tools.ts +334 -0
  194. package/src/types.ts +182 -0
  195. package/src/utils.ts +22 -0
  196. package/tsconfig.json +8 -0
  197. package/vitest.config.js +15 -0
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Theme Color Matcher
3
+ * Finds closest theme colors using HCT perceptual distance
4
+ */
5
+
6
+ import type { HCT } from "../color/hct/types.js";
7
+ import type {
8
+ ColorContext,
9
+ MatchingOptions,
10
+ SemanticRole,
11
+ ThemeMatch,
12
+ ThemeVariable,
13
+ ThemeVariables,
14
+ } from "./types.js";
15
+
16
+ import { hexToRgb } from "../color/conversions.js";
17
+ import { rgbToHct } from "../color/hct/hct-solver.js";
18
+ import { getContrastRatio } from "../color/utils.js";
19
+
20
+ /**
21
+ * Default matching weights
22
+ */
23
+ const DEFAULT_WEIGHTS = {
24
+ accessibility: 0.2,
25
+ perceptual: 0.6,
26
+ semantic: 0.2,
27
+ };
28
+
29
+ /**
30
+ * Maximum acceptable distances for different contexts
31
+ */
32
+ const CONTEXT_MAX_DISTANCES: Record<ColorContext, number> = {
33
+ accent: 15, // Moderate for brand consistency
34
+ background: 15, // Moderate flexibility
35
+ border: 20, // More flexibility
36
+ decorative: 30, // Most flexible
37
+ shadow: 25, // Even more flexibility
38
+ text: 10, // Strict for readability
39
+ };
40
+
41
+ /**
42
+ * Find matches for multiple colors
43
+ */
44
+ export function findBatchMatches(
45
+ colors: string[],
46
+ themeVariables: ThemeVariables,
47
+ options: MatchingOptions = {},
48
+ ): Map<string, null | ThemeMatch> {
49
+ const results = new Map<string, null | ThemeMatch>();
50
+
51
+ for (const color of colors) {
52
+ results.set(color, findClosestThemeColor(color, themeVariables, options));
53
+ }
54
+
55
+ return results;
56
+ }
57
+
58
+ /**
59
+ * Find the closest theme color for a given input color
60
+ */
61
+ export function findClosestThemeColor(
62
+ inputColor: string,
63
+ themeVariables: ThemeVariables,
64
+ options: MatchingOptions = {},
65
+ ): null | ThemeMatch {
66
+ // Parse input color to HCT
67
+ let inputHct: HCT;
68
+ try {
69
+ const rgb = parseColorToRgb(inputColor);
70
+ inputHct = rgbToHct(rgb);
71
+ } catch {
72
+ console.error(`Failed to parse input color: ${inputColor}`);
73
+ return null;
74
+ }
75
+
76
+ // Flatten theme variables
77
+ const allVariables = flattenThemeVariables(themeVariables);
78
+ if (allVariables.length === 0) {
79
+ return null;
80
+ }
81
+
82
+ // Calculate distances and scores
83
+ const candidates = allVariables.map((variable) => ({
84
+ distance: calculateHctDistance(inputHct, variable.hct),
85
+ score: 0, // Will be calculated next
86
+ variable,
87
+ }));
88
+
89
+ // Apply context-based filtering
90
+ const maxDistance =
91
+ options.maxDistance ??
92
+ (options.contextType ? CONTEXT_MAX_DISTANCES[options.contextType] : 30);
93
+
94
+ const validCandidates = candidates.filter((c) => c.distance <= maxDistance);
95
+ if (validCandidates.length === 0) {
96
+ return null;
97
+ }
98
+
99
+ // Calculate multi-factor scores
100
+ const weights = options.weights ?? DEFAULT_WEIGHTS;
101
+ for (const candidate of validCandidates) {
102
+ candidate.score = calculateMatchScore(
103
+ inputHct,
104
+ candidate.variable,
105
+ candidate.distance,
106
+ weights,
107
+ options,
108
+ );
109
+ }
110
+
111
+ // Sort by score (higher is better)
112
+ validCandidates.sort((a, b) => b.score - a.score);
113
+
114
+ // Get the best match and alternatives
115
+ const best = validCandidates[0];
116
+ const alternatives = validCandidates.slice(1, 4).map((c) => ({
117
+ alternatives: [],
118
+ confidence: calculateConfidence(c.distance, c.score),
119
+ distance: c.distance,
120
+ semanticRole: c.variable.role,
121
+ value: c.variable.value,
122
+ variable: c.variable.name,
123
+ }));
124
+
125
+ // Calculate accessibility info if needed
126
+ const accessibilityInfo =
127
+ options.contextType === "text" || options.contextType === "background"
128
+ ? calculateAccessibilityInfo(best.variable.value, themeVariables)
129
+ : undefined;
130
+
131
+ return {
132
+ accessibilityInfo,
133
+ alternatives,
134
+ confidence: calculateConfidence(best.distance, best.score),
135
+ distance: best.distance,
136
+ semanticRole: best.variable.role,
137
+ value: best.variable.value,
138
+ variable: best.variable.name,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Calculate accessibility information
144
+ */
145
+ function calculateAccessibilityInfo(
146
+ color: string,
147
+ themeVariables: ThemeVariables,
148
+ ): ThemeMatch["accessibilityInfo"] {
149
+ const rgb = parseColorToRgb(color);
150
+
151
+ // Find typical background and foreground colors
152
+ const backgrounds = findTypicalBackgrounds(themeVariables);
153
+ const foregrounds = findTypicalForegrounds(themeVariables);
154
+
155
+ let maxContrastBg = 0;
156
+ let maxContrastFg = 0;
157
+
158
+ for (const bg of backgrounds) {
159
+ const bgRgb = parseColorToRgb(bg.value);
160
+ const contrast = getContrastRatio(rgb, bgRgb);
161
+ maxContrastBg = Math.max(maxContrastBg, contrast);
162
+ }
163
+
164
+ for (const fg of foregrounds) {
165
+ const fgRgb = parseColorToRgb(fg.value);
166
+ const contrast = getContrastRatio(rgb, fgRgb);
167
+ maxContrastFg = Math.max(maxContrastFg, contrast);
168
+ }
169
+
170
+ return {
171
+ contrastWithBackground: maxContrastBg,
172
+ contrastWithForeground: maxContrastFg,
173
+ meetsAA: maxContrastBg >= 4.5 || maxContrastFg >= 4.5,
174
+ meetsAAA: maxContrastBg >= 7.0 || maxContrastFg >= 7.0,
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Calculate confidence score (0-100)
180
+ */
181
+ function calculateConfidence(distance: number, score: number): number {
182
+ // For very small distances (exact or near-exact matches), return high confidence
183
+ if (distance < 0.5) return 100;
184
+ if (distance < 1) return 98;
185
+ if (distance < 2) return 95;
186
+
187
+ // Combine distance and score for confidence
188
+ const distanceConfidence = Math.max(0, 100 - distance * 2);
189
+ const scoreConfidence = score * 100;
190
+ return Math.round((distanceConfidence + scoreConfidence) / 2);
191
+ }
192
+
193
+ /**
194
+ * Calculate semantic score based on context
195
+ */
196
+ function calculateContextSemanticScore(
197
+ context: ColorContext,
198
+ role: SemanticRole | undefined,
199
+ ): number {
200
+ if (!role) return 0.5;
201
+
202
+ switch (context) {
203
+ case "accent":
204
+ return role === "primary" || role === "secondary" ? 1.0 : 0.5;
205
+ case "background":
206
+ return role === "surface" || role === "background" ? 1.0 : 0.3;
207
+ case "border":
208
+ // Strongly prefer outline role for border context
209
+ return role === "outline" ? 1.0 : role === "neutral" ? 0.4 : 0.2;
210
+ case "decorative":
211
+ return 0.7; // Any role is acceptable
212
+ case "shadow":
213
+ return role === "shadow" ? 1.0 : 0.3;
214
+ case "text":
215
+ return role === "neutral" || role === "surface" ? 0.8 : 0.5;
216
+ default:
217
+ return 0.5;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Calculate HCT perceptual distance
223
+ */
224
+ function calculateHctDistance(color1: HCT, color2: HCT): number {
225
+ // Weight factors for HCT components
226
+ const hueWeight = 1.0;
227
+ const chromaWeight = 1.0;
228
+ const toneWeight = 2.0; // Tone is most important for perception
229
+
230
+ // Calculate hue difference (circular)
231
+ let hueDiff = Math.abs(color1.h - color2.h);
232
+ if (hueDiff > 180) {
233
+ hueDiff = 360 - hueDiff;
234
+ }
235
+
236
+ // Normalize hue difference (0-1)
237
+ hueDiff = hueDiff / 180;
238
+
239
+ // Calculate chroma difference (normalize by max chroma ~120)
240
+ const chromaDiff = Math.abs(color1.c - color2.c) / 120;
241
+
242
+ // Calculate tone difference (0-100)
243
+ const toneDiff = Math.abs(color1.t - color2.t) / 100;
244
+
245
+ // Weighted Euclidean distance
246
+ return (
247
+ Math.sqrt(
248
+ Math.pow(hueDiff * hueWeight, 2) +
249
+ Math.pow(chromaDiff * chromaWeight, 2) +
250
+ Math.pow(toneDiff * toneWeight, 2),
251
+ ) * 100
252
+ ); // Scale to 0-100 range
253
+ }
254
+
255
+ /**
256
+ * Calculate multi-factor match score
257
+ */
258
+ function calculateMatchScore(
259
+ inputHct: HCT,
260
+ candidate: ThemeVariable,
261
+ distance: number,
262
+ weights: { accessibility: number; perceptual: number; semantic: number },
263
+ options: MatchingOptions,
264
+ ): number {
265
+ // Perceptual score (inverse of distance)
266
+ const perceptualScore = Math.max(0, 100 - distance) / 100;
267
+
268
+ // Semantic score
269
+ let semanticScore = 0.5; // Neutral if no preference
270
+ if (options.preferredRole && candidate.role) {
271
+ semanticScore = candidate.role === options.preferredRole ? 1.0 : 0.3;
272
+ } else if (options.contextType) {
273
+ semanticScore = calculateContextSemanticScore(
274
+ options.contextType,
275
+ candidate.role,
276
+ );
277
+ }
278
+
279
+ // Accessibility score (based on tone appropriateness)
280
+ let accessibilityScore = 0.5; // Neutral default
281
+ if (options.contextType === "text") {
282
+ // Prefer high contrast (very light or very dark)
283
+ accessibilityScore = candidate.tone < 30 || candidate.tone > 70 ? 1.0 : 0.3;
284
+ } else if (options.contextType === "background") {
285
+ // Prefer mid-tones for backgrounds
286
+ accessibilityScore =
287
+ candidate.tone >= 90 || candidate.tone <= 10 ? 1.0 : 0.5;
288
+ }
289
+
290
+ // Calculate weighted score
291
+ return (
292
+ perceptualScore * weights.perceptual +
293
+ semanticScore * weights.semantic +
294
+ accessibilityScore * weights.accessibility
295
+ );
296
+ }
297
+
298
+ /**
299
+ * Find typical background colors from theme
300
+ */
301
+ function findTypicalBackgrounds(theme: ThemeVariables): ThemeVariable[] {
302
+ const backgrounds: ThemeVariable[] = [];
303
+
304
+ if (theme.surface) {
305
+ backgrounds.push(
306
+ ...theme.surface.filter((v) => v.tone >= 90 || v.tone <= 10),
307
+ );
308
+ }
309
+ if (theme.background) {
310
+ backgrounds.push(...theme.background);
311
+ }
312
+ if (theme.neutral) {
313
+ backgrounds.push(
314
+ ...theme.neutral.filter((v) => v.tone >= 95 || v.tone <= 5),
315
+ );
316
+ }
317
+
318
+ return backgrounds;
319
+ }
320
+
321
+ /**
322
+ * Find typical foreground colors from theme
323
+ */
324
+ function findTypicalForegrounds(theme: ThemeVariables): ThemeVariable[] {
325
+ const foregrounds: ThemeVariable[] = [];
326
+
327
+ if (theme.neutral) {
328
+ foregrounds.push(
329
+ ...theme.neutral.filter(
330
+ (v) => (v.tone >= 20 && v.tone <= 40) || (v.tone >= 60 && v.tone <= 80),
331
+ ),
332
+ );
333
+ }
334
+ if (theme.surface) {
335
+ foregrounds.push(
336
+ ...theme.surface.filter((v) => v.tone >= 10 && v.tone <= 30),
337
+ );
338
+ }
339
+
340
+ return foregrounds;
341
+ }
342
+
343
+ /**
344
+ * Flatten theme variables into a single array
345
+ */
346
+ function flattenThemeVariables(theme: ThemeVariables): ThemeVariable[] {
347
+ const variables: ThemeVariable[] = [];
348
+
349
+ // Add standard roles
350
+ if (theme.primary) variables.push(...theme.primary);
351
+ if (theme.secondary) variables.push(...theme.secondary);
352
+ if (theme.tertiary) variables.push(...theme.tertiary);
353
+ if (theme.error) variables.push(...theme.error);
354
+ if (theme.neutral) variables.push(...theme.neutral);
355
+ if (theme.surface) variables.push(...theme.surface);
356
+ if (theme.background) variables.push(...theme.background);
357
+ if (theme.outline) variables.push(...theme.outline);
358
+ if (theme.shadow) variables.push(...theme.shadow);
359
+
360
+ // Add custom roles
361
+ if (theme.custom) {
362
+ for (const customVars of Object.values(theme.custom)) {
363
+ variables.push(...customVars);
364
+ }
365
+ }
366
+
367
+ return variables;
368
+ }
369
+
370
+ /**
371
+ * Parse color string to RGB
372
+ */
373
+ function parseColorToRgb(color: string): { b: number; g: number; r: number } {
374
+ // Remove whitespace
375
+ color = color.trim();
376
+
377
+ // Hex color
378
+ if (color.startsWith("#")) {
379
+ return hexToRgb(color);
380
+ }
381
+
382
+ // RGB/RGBA
383
+ if (color.startsWith("rgb")) {
384
+ const match = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
385
+ if (match) {
386
+ return {
387
+ b: parseInt(match[3], 10) / 255,
388
+ g: parseInt(match[2], 10) / 255,
389
+ r: parseInt(match[1], 10) / 255,
390
+ };
391
+ }
392
+ }
393
+
394
+ throw new Error(`Unable to parse color: ${color}`);
395
+ }