sonance-brand-mcp 1.3.53 → 1.3.55
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.
|
@@ -4,11 +4,53 @@
|
|
|
4
4
|
* Dynamically discovers and parses theme/design system files from target codebases.
|
|
5
5
|
* Extracts CSS variables, Tailwind colors, and available semantic tokens to provide
|
|
6
6
|
* context to the LLM for intelligent styling decisions.
|
|
7
|
+
*
|
|
8
|
+
* NEW: Includes WCAG contrast ratio calculation to warn about bad color combinations.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import * as fs from "fs";
|
|
10
12
|
import * as path from "path";
|
|
11
13
|
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// TYPE DEFINITIONS
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* RGB color representation
|
|
20
|
+
*/
|
|
21
|
+
interface RGB {
|
|
22
|
+
r: number;
|
|
23
|
+
g: number;
|
|
24
|
+
b: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Contrast warning for a failing color combination
|
|
29
|
+
*/
|
|
30
|
+
interface ContrastWarning {
|
|
31
|
+
background: string;
|
|
32
|
+
text: string;
|
|
33
|
+
ratio: string;
|
|
34
|
+
suggestion: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Safe color pattern that passes WCAG
|
|
39
|
+
*/
|
|
40
|
+
interface SafePattern {
|
|
41
|
+
background: string;
|
|
42
|
+
text: string;
|
|
43
|
+
ratio: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Result of contrast analysis
|
|
48
|
+
*/
|
|
49
|
+
interface ContrastAnalysis {
|
|
50
|
+
warnings: ContrastWarning[];
|
|
51
|
+
safePatterns: SafePattern[];
|
|
52
|
+
}
|
|
53
|
+
|
|
12
54
|
/**
|
|
13
55
|
* Discovered theme information from a codebase
|
|
14
56
|
*/
|
|
@@ -27,8 +69,208 @@ export interface DiscoveredTheme {
|
|
|
27
69
|
|
|
28
70
|
// Files that were discovered and parsed
|
|
29
71
|
discoveredFiles: string[];
|
|
72
|
+
|
|
73
|
+
// Contrast analysis results
|
|
74
|
+
contrastAnalysis?: ContrastAnalysis;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// COLOR PARSING FUNCTIONS
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Convert HSL values to RGB
|
|
83
|
+
* Handles formats: "195 100% 41%" or "hsl(195, 100%, 41%)" or "195deg 100% 41%"
|
|
84
|
+
*/
|
|
85
|
+
function hslToRgb(h: number, s: number, l: number): RGB {
|
|
86
|
+
// Normalize h to 0-360, s and l to 0-1
|
|
87
|
+
h = ((h % 360) + 360) % 360;
|
|
88
|
+
s = Math.max(0, Math.min(1, s / 100));
|
|
89
|
+
l = Math.max(0, Math.min(1, l / 100));
|
|
90
|
+
|
|
91
|
+
const k = (n: number) => (n + h / 30) % 12;
|
|
92
|
+
const a = s * Math.min(l, 1 - l);
|
|
93
|
+
const f = (n: number) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
r: Math.round(f(0) * 255),
|
|
97
|
+
g: Math.round(f(8) * 255),
|
|
98
|
+
b: Math.round(f(4) * 255),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parse hex color to RGB
|
|
104
|
+
* Handles: #fff, #ffffff, fff, ffffff
|
|
105
|
+
*/
|
|
106
|
+
function hexToRgb(hex: string): RGB | null {
|
|
107
|
+
// Remove # if present
|
|
108
|
+
hex = hex.replace(/^#/, "");
|
|
109
|
+
|
|
110
|
+
// Expand shorthand (e.g., "fff" -> "ffffff")
|
|
111
|
+
if (hex.length === 3) {
|
|
112
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (hex.length !== 6) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
120
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
121
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
122
|
+
|
|
123
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { r, g, b };
|
|
30
128
|
}
|
|
31
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Parse CSS color value to RGB
|
|
132
|
+
* Handles multiple formats:
|
|
133
|
+
* - Hex: #fff, #ffffff
|
|
134
|
+
* - RGB: rgb(255, 255, 255), rgb(255 255 255)
|
|
135
|
+
* - HSL: hsl(195, 100%, 41%), hsl(195 100% 41%)
|
|
136
|
+
* - Space-separated HSL: 195 100% 41% (common in Tailwind/shadcn)
|
|
137
|
+
* - oklch: oklch(0.7 0.15 195) - approximated
|
|
138
|
+
*/
|
|
139
|
+
function parseColorToRgb(value: string): RGB | null {
|
|
140
|
+
if (!value || typeof value !== "string") {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const trimmed = value.trim().toLowerCase();
|
|
145
|
+
|
|
146
|
+
// Skip CSS variable references - we can't resolve these without runtime
|
|
147
|
+
if (trimmed.includes("var(")) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Handle hex colors
|
|
152
|
+
if (trimmed.startsWith("#") || /^[0-9a-f]{3,6}$/i.test(trimmed)) {
|
|
153
|
+
return hexToRgb(trimmed);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handle rgb() and rgba()
|
|
157
|
+
const rgbMatch = trimmed.match(/rgba?\s*\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
|
|
158
|
+
if (rgbMatch) {
|
|
159
|
+
return {
|
|
160
|
+
r: parseInt(rgbMatch[1], 10),
|
|
161
|
+
g: parseInt(rgbMatch[2], 10),
|
|
162
|
+
b: parseInt(rgbMatch[3], 10),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Handle hsl() and hsla()
|
|
167
|
+
const hslFuncMatch = trimmed.match(/hsla?\s*\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
|
|
168
|
+
if (hslFuncMatch) {
|
|
169
|
+
const h = parseFloat(hslFuncMatch[1]);
|
|
170
|
+
const s = parseFloat(hslFuncMatch[2]);
|
|
171
|
+
const l = parseFloat(hslFuncMatch[3]);
|
|
172
|
+
return hslToRgb(h, s, l);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Handle space-separated HSL (common in Tailwind/shadcn): "195 100% 41%"
|
|
176
|
+
const hslSpaceMatch = trimmed.match(/^([\d.]+)(?:deg)?\s+([\d.]+)%\s+([\d.]+)%$/);
|
|
177
|
+
if (hslSpaceMatch) {
|
|
178
|
+
const h = parseFloat(hslSpaceMatch[1]);
|
|
179
|
+
const s = parseFloat(hslSpaceMatch[2]);
|
|
180
|
+
const l = parseFloat(hslSpaceMatch[3]);
|
|
181
|
+
return hslToRgb(h, s, l);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Handle oklch() - approximate conversion (not perfect but gives reasonable results)
|
|
185
|
+
const oklchMatch = trimmed.match(/oklch\s*\(\s*([\d.]+)(?:%?)\s+([\d.]+)\s+([\d.]+)/);
|
|
186
|
+
if (oklchMatch) {
|
|
187
|
+
// Simplified oklch to RGB approximation
|
|
188
|
+
// L is lightness (0-1 or 0-100%), C is chroma, H is hue
|
|
189
|
+
let l = parseFloat(oklchMatch[1]);
|
|
190
|
+
if (l > 1) l = l / 100; // Handle percentage format
|
|
191
|
+
const c = parseFloat(oklchMatch[2]);
|
|
192
|
+
const h = parseFloat(oklchMatch[3]);
|
|
193
|
+
|
|
194
|
+
// Very rough approximation: treat as HSL with adjusted values
|
|
195
|
+
// This won't be perfect but gives a reasonable estimate for contrast
|
|
196
|
+
const approxS = Math.min(100, c * 300); // Chroma to saturation approximation
|
|
197
|
+
const approxL = l * 100;
|
|
198
|
+
return hslToRgb(h, approxS, approxL);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Handle named colors (common ones)
|
|
202
|
+
const namedColors: Record<string, RGB> = {
|
|
203
|
+
white: { r: 255, g: 255, b: 255 },
|
|
204
|
+
black: { r: 0, g: 0, b: 0 },
|
|
205
|
+
transparent: { r: 0, g: 0, b: 0 }, // Treat as black for contrast purposes
|
|
206
|
+
red: { r: 255, g: 0, b: 0 },
|
|
207
|
+
green: { r: 0, g: 128, b: 0 },
|
|
208
|
+
blue: { r: 0, g: 0, b: 255 },
|
|
209
|
+
yellow: { r: 255, g: 255, b: 0 },
|
|
210
|
+
cyan: { r: 0, g: 255, b: 255 },
|
|
211
|
+
magenta: { r: 255, g: 0, b: 255 },
|
|
212
|
+
gray: { r: 128, g: 128, b: 128 },
|
|
213
|
+
grey: { r: 128, g: 128, b: 128 },
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if (namedColors[trimmed]) {
|
|
217
|
+
return namedColors[trimmed];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ============================================================================
|
|
224
|
+
// WCAG CONTRAST CALCULATION
|
|
225
|
+
// ============================================================================
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Calculate relative luminance using WCAG formula
|
|
229
|
+
* https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
|
|
230
|
+
*/
|
|
231
|
+
function getLuminance(r: number, g: number, b: number): number {
|
|
232
|
+
const [rs, gs, bs] = [r, g, b].map((c) => {
|
|
233
|
+
c /= 255;
|
|
234
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
235
|
+
});
|
|
236
|
+
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Calculate WCAG contrast ratio between two colors
|
|
241
|
+
* Returns ratio like 4.5, 7.0, 21.0 (max), etc.
|
|
242
|
+
* WCAG AA requires 4.5:1 for normal text, 3:1 for large text
|
|
243
|
+
* WCAG AAA requires 7:1 for normal text, 4.5:1 for large text
|
|
244
|
+
*/
|
|
245
|
+
function getContrastRatio(rgb1: RGB, rgb2: RGB): number {
|
|
246
|
+
const l1 = getLuminance(rgb1.r, rgb1.g, rgb1.b);
|
|
247
|
+
const l2 = getLuminance(rgb2.r, rgb2.g, rgb2.b);
|
|
248
|
+
const lighter = Math.max(l1, l2);
|
|
249
|
+
const darker = Math.min(l1, l2);
|
|
250
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Determine which alternative (white or black) provides better contrast
|
|
255
|
+
*/
|
|
256
|
+
function getBestAlternative(bgRgb: RGB): { color: string; ratio: number } {
|
|
257
|
+
const whiteRgb: RGB = { r: 255, g: 255, b: 255 };
|
|
258
|
+
const blackRgb: RGB = { r: 0, g: 0, b: 0 };
|
|
259
|
+
|
|
260
|
+
const whiteRatio = getContrastRatio(bgRgb, whiteRgb);
|
|
261
|
+
const blackRatio = getContrastRatio(bgRgb, blackRgb);
|
|
262
|
+
|
|
263
|
+
if (whiteRatio >= blackRatio) {
|
|
264
|
+
return { color: "text-white", ratio: whiteRatio };
|
|
265
|
+
} else {
|
|
266
|
+
return { color: "text-black", ratio: blackRatio };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// THEME FILE DISCOVERY
|
|
272
|
+
// ============================================================================
|
|
273
|
+
|
|
32
274
|
/**
|
|
33
275
|
* Patterns for finding theme-related files
|
|
34
276
|
*/
|
|
@@ -221,49 +463,120 @@ export function extractAvailableTokens(cssVariables: Record<string, string>): st
|
|
|
221
463
|
return [...new Set([...tokens, ...commonTokens])];
|
|
222
464
|
}
|
|
223
465
|
|
|
466
|
+
// ============================================================================
|
|
467
|
+
// CONTRAST ANALYSIS
|
|
468
|
+
// ============================================================================
|
|
469
|
+
|
|
224
470
|
/**
|
|
225
471
|
* Analyze CSS variables to identify contrast relationships
|
|
226
|
-
*
|
|
472
|
+
* Calculates actual WCAG contrast ratios and returns warnings for failing combinations
|
|
227
473
|
*/
|
|
228
474
|
export function analyzeContrastRelationships(
|
|
229
475
|
cssVariables: Record<string, string>
|
|
230
|
-
):
|
|
231
|
-
const warnings:
|
|
232
|
-
const
|
|
476
|
+
): ContrastAnalysis {
|
|
477
|
+
const warnings: ContrastWarning[] = [];
|
|
478
|
+
const safePatterns: SafePattern[] = [];
|
|
233
479
|
|
|
234
|
-
//
|
|
235
|
-
|
|
480
|
+
// Track which backgrounds we've analyzed to add white/black alternatives
|
|
481
|
+
const analyzedBackgrounds: { name: string; rgb: RGB }[] = [];
|
|
236
482
|
|
|
483
|
+
// Find all bg/foreground pairs automatically
|
|
237
484
|
for (const [varName, value] of Object.entries(cssVariables)) {
|
|
238
485
|
// Skip if this is a foreground variable
|
|
239
486
|
if (varName.includes("foreground")) continue;
|
|
240
487
|
|
|
488
|
+
// Skip non-color variables
|
|
489
|
+
if (
|
|
490
|
+
varName.includes("radius") ||
|
|
491
|
+
varName.includes("shadow") ||
|
|
492
|
+
varName.includes("ring") ||
|
|
493
|
+
varName.includes("border") && !varName.match(/border$/)
|
|
494
|
+
) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
|
|
241
498
|
const baseName = varName.replace(/^--/, "");
|
|
242
499
|
const foregroundVar = `--${baseName}-foreground`;
|
|
243
500
|
|
|
501
|
+
// Parse background color
|
|
502
|
+
const bgRgb = parseColorToRgb(value);
|
|
503
|
+
if (!bgRgb) continue;
|
|
504
|
+
|
|
505
|
+
// Track this background for alternative suggestions
|
|
506
|
+
analyzedBackgrounds.push({ name: baseName, rgb: bgRgb });
|
|
507
|
+
|
|
508
|
+
// Check if there's a corresponding foreground variable
|
|
244
509
|
if (cssVariables[foregroundVar]) {
|
|
245
|
-
const
|
|
246
|
-
const fgValue = cssVariables[foregroundVar];
|
|
510
|
+
const fgRgb = parseColorToRgb(cssVariables[foregroundVar]);
|
|
247
511
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
512
|
+
if (fgRgb) {
|
|
513
|
+
const ratio = getContrastRatio(bgRgb, fgRgb);
|
|
514
|
+
const passes = ratio >= 4.5; // WCAG AA for normal text
|
|
515
|
+
|
|
516
|
+
if (!passes) {
|
|
517
|
+
// Find the best alternative
|
|
518
|
+
const alternative = getBestAlternative(bgRgb);
|
|
519
|
+
|
|
520
|
+
warnings.push({
|
|
521
|
+
background: `bg-${baseName}`,
|
|
522
|
+
text: `text-${baseName}-foreground`,
|
|
523
|
+
ratio: ratio.toFixed(1),
|
|
524
|
+
suggestion: `Use ${alternative.color} instead (${alternative.ratio.toFixed(1)}:1 contrast)`,
|
|
525
|
+
});
|
|
526
|
+
} else {
|
|
527
|
+
safePatterns.push({
|
|
528
|
+
background: `bg-${baseName}`,
|
|
529
|
+
text: `text-${baseName}-foreground`,
|
|
530
|
+
ratio: ratio.toFixed(1),
|
|
531
|
+
});
|
|
532
|
+
}
|
|
254
533
|
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Add white/black alternatives for all analyzed backgrounds
|
|
538
|
+
const whiteRgb: RGB = { r: 255, g: 255, b: 255 };
|
|
539
|
+
const blackRgb: RGB = { r: 0, g: 0, b: 0 };
|
|
540
|
+
|
|
541
|
+
for (const { name, rgb } of analyzedBackgrounds) {
|
|
542
|
+
// Only add alternatives for semantic color names (primary, accent, etc.)
|
|
543
|
+
if (
|
|
544
|
+
name.includes("primary") ||
|
|
545
|
+
name.includes("secondary") ||
|
|
546
|
+
name.includes("accent") ||
|
|
547
|
+
name.includes("destructive") ||
|
|
548
|
+
name.includes("success") ||
|
|
549
|
+
name.includes("warning") ||
|
|
550
|
+
name.includes("muted")
|
|
551
|
+
) {
|
|
552
|
+
const whiteRatio = getContrastRatio(rgb, whiteRgb);
|
|
553
|
+
const blackRatio = getContrastRatio(rgb, blackRgb);
|
|
255
554
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
555
|
+
if (whiteRatio >= 4.5) {
|
|
556
|
+
safePatterns.push({
|
|
557
|
+
background: `bg-${name}`,
|
|
558
|
+
text: "text-white",
|
|
559
|
+
ratio: whiteRatio.toFixed(1),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (blackRatio >= 4.5) {
|
|
564
|
+
safePatterns.push({
|
|
565
|
+
background: `bg-${name}`,
|
|
566
|
+
text: "text-black",
|
|
567
|
+
ratio: blackRatio.toFixed(1),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
261
570
|
}
|
|
262
571
|
}
|
|
263
572
|
|
|
264
|
-
return { warnings,
|
|
573
|
+
return { warnings, safePatterns };
|
|
265
574
|
}
|
|
266
575
|
|
|
576
|
+
// ============================================================================
|
|
577
|
+
// MAIN ENTRY POINTS
|
|
578
|
+
// ============================================================================
|
|
579
|
+
|
|
267
580
|
/**
|
|
268
581
|
* Main entry point: Discover theme from a project
|
|
269
582
|
*/
|
|
@@ -308,11 +621,15 @@ export async function discoverTheme(projectRoot: string): Promise<DiscoveredThem
|
|
|
308
621
|
// Extract available tokens
|
|
309
622
|
result.availableTokens = extractAvailableTokens(result.cssVariables);
|
|
310
623
|
|
|
624
|
+
// Run contrast analysis
|
|
625
|
+
result.contrastAnalysis = analyzeContrastRelationships(result.cssVariables);
|
|
626
|
+
|
|
311
627
|
return result;
|
|
312
628
|
}
|
|
313
629
|
|
|
314
630
|
/**
|
|
315
631
|
* Format discovered theme as context for the LLM prompt
|
|
632
|
+
* Includes contrast warnings to prevent bad color combinations
|
|
316
633
|
*/
|
|
317
634
|
export function formatThemeForPrompt(theme: DiscoveredTheme): string {
|
|
318
635
|
const lines: string[] = [];
|
|
@@ -369,6 +686,57 @@ export function formatThemeForPrompt(theme: DiscoveredTheme): string {
|
|
|
369
686
|
lines.push("");
|
|
370
687
|
}
|
|
371
688
|
|
|
689
|
+
// ========== CONTRAST ANALYSIS (NEW) ==========
|
|
690
|
+
// Run contrast analysis if not already done
|
|
691
|
+
const contrastAnalysis = theme.contrastAnalysis || analyzeContrastRelationships(theme.cssVariables);
|
|
692
|
+
|
|
693
|
+
// Show warnings first (most important for LLM)
|
|
694
|
+
if (contrastAnalysis.warnings.length > 0) {
|
|
695
|
+
lines.push("");
|
|
696
|
+
lines.push("⚠️ CONTRAST WARNINGS (these combinations FAIL WCAG in this codebase - DO NOT USE):");
|
|
697
|
+
for (const w of contrastAnalysis.warnings) {
|
|
698
|
+
lines.push(` ❌ ${w.background} + ${w.text}: ${w.ratio}:1 → ${w.suggestion}`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Show safe patterns
|
|
703
|
+
if (contrastAnalysis.safePatterns.length > 0) {
|
|
704
|
+
lines.push("");
|
|
705
|
+
lines.push("✓ SAFE COLOR COMBINATIONS (use these instead):");
|
|
706
|
+
|
|
707
|
+
// Deduplicate and sort by background, prioritize semantic foregrounds
|
|
708
|
+
const seen = new Set<string>();
|
|
709
|
+
const sorted = contrastAnalysis.safePatterns
|
|
710
|
+
.filter((p) => {
|
|
711
|
+
const key = `${p.background}+${p.text}`;
|
|
712
|
+
if (seen.has(key)) return false;
|
|
713
|
+
seen.add(key);
|
|
714
|
+
return true;
|
|
715
|
+
})
|
|
716
|
+
.sort((a, b) => {
|
|
717
|
+
// Sort by background name, then prioritize -foreground over white/black
|
|
718
|
+
if (a.background !== b.background) {
|
|
719
|
+
return a.background.localeCompare(b.background);
|
|
720
|
+
}
|
|
721
|
+
// Prioritize -foreground variants
|
|
722
|
+
const aIsForeground = a.text.includes("-foreground");
|
|
723
|
+
const bIsForeground = b.text.includes("-foreground");
|
|
724
|
+
if (aIsForeground && !bIsForeground) return -1;
|
|
725
|
+
if (!aIsForeground && bIsForeground) return 1;
|
|
726
|
+
return 0;
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
for (const s of sorted.slice(0, 15)) { // Limit to avoid prompt bloat
|
|
730
|
+
lines.push(` ✓ ${s.background} + ${s.text}: ${s.ratio}:1`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (sorted.length > 15) {
|
|
734
|
+
lines.push(` ... and ${sorted.length - 15} more safe combinations`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
lines.push("");
|
|
739
|
+
|
|
372
740
|
// Available Tokens
|
|
373
741
|
if (theme.availableTokens.length > 0) {
|
|
374
742
|
lines.push(`Available Semantic Tokens: ${theme.availableTokens.slice(0, 20).join(", ")}`);
|
|
@@ -385,4 +753,3 @@ export function formatThemeForPrompt(theme: DiscoveredTheme): string {
|
|
|
385
753
|
|
|
386
754
|
return lines.join("\n");
|
|
387
755
|
}
|
|
388
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.55",
|
|
4
4
|
"description": "MCP Server for Sonance Brand Guidelines and Component Library - gives Claude instant access to brand colors, typography, and UI components.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|