@optilogic/core 1.0.0-beta.9 → 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 (53) hide show
  1. package/dist/index.cjs +1115 -45
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +326 -1
  4. package/dist/index.d.ts +326 -1
  5. package/dist/index.js +1097 -46
  6. package/dist/index.js.map +1 -1
  7. package/dist/styles.css +22 -0
  8. package/dist/tailwind-preset.cjs +17 -2
  9. package/dist/tailwind-preset.cjs.map +1 -1
  10. package/dist/tailwind-preset.js +17 -2
  11. package/dist/tailwind-preset.js.map +1 -1
  12. package/package.json +15 -1
  13. package/src/components/autocomplete.tsx +2 -1
  14. package/src/components/button.tsx +10 -8
  15. package/src/components/calendar.tsx +7 -7
  16. package/src/components/data-grid/DataGrid.tsx +6 -1
  17. package/src/components/data-grid/components/CellEditor.tsx +3 -3
  18. package/src/components/data-grid/hooks/useDataGridState.ts +18 -3
  19. package/src/components/data-grid/types.ts +4 -0
  20. package/src/components/data-grid/utils/dataProcessing.ts +40 -11
  21. package/src/components/date-picker.tsx +2 -1
  22. package/src/components/dropdown-menu.tsx +1 -1
  23. package/src/components/file-view/FileView.tsx +147 -0
  24. package/src/components/file-view/components/CodeRenderer.tsx +97 -0
  25. package/src/components/file-view/components/CsvRenderer.tsx +127 -0
  26. package/src/components/file-view/components/HtmlRenderer.tsx +24 -0
  27. package/src/components/file-view/components/ImageRenderer.tsx +67 -0
  28. package/src/components/file-view/components/MarkdownRenderer.tsx +304 -0
  29. package/src/components/file-view/components/PlainTextRenderer.tsx +27 -0
  30. package/src/components/file-view/components/index.ts +4 -0
  31. package/src/components/file-view/hooks/index.ts +5 -0
  32. package/src/components/file-view/hooks/useContentType.ts +34 -0
  33. package/src/components/file-view/hooks/useDarkMode.ts +62 -0
  34. package/src/components/file-view/hooks/useHighlightedTokens.ts +83 -0
  35. package/src/components/file-view/hooks/useShikiHighlighter.ts +69 -0
  36. package/src/components/file-view/index.ts +47 -0
  37. package/src/components/file-view/types.ts +180 -0
  38. package/src/components/file-view/utils/contentTypeDetection.ts +157 -0
  39. package/src/components/file-view/utils/index.ts +12 -0
  40. package/src/components/file-view/utils/languageMapping.ts +78 -0
  41. package/src/components/file-view/utils/rendererRegistry.ts +42 -0
  42. package/src/components/input.tsx +1 -1
  43. package/src/components/popover.tsx +1 -1
  44. package/src/components/select.tsx +1 -1
  45. package/src/components/switch.tsx +5 -3
  46. package/src/components/textarea.tsx +1 -1
  47. package/src/index.ts +39 -0
  48. package/src/styles.css +22 -0
  49. package/src/tailwind-preset.ts +17 -1
  50. package/src/theme/index.ts +5 -0
  51. package/src/theme/presets.ts +112 -2
  52. package/src/theme/types.ts +35 -0
  53. package/src/theme/utils.ts +231 -0
@@ -40,11 +40,21 @@ export const GREEN_THEME: Theme = {
40
40
  border: "#243630",
41
41
  input: "#243630",
42
42
  ring: "#6FCF97",
43
+ toggleTrack: "#354840",
44
+ toggleTrackForeground: "#E6F5EC",
45
+ inputHover: "#6FCF97",
43
46
  chart1: "#6FCF97",
44
47
  chart2: "#8FE3B0",
45
48
  chart3: "#9DB8A8",
46
49
  chart4: "#2d4038",
47
50
  chart5: "#354840",
51
+ chart6: "#4CAF50",
52
+ chart7: "#81C784",
53
+ chart8: "#F2C94C",
54
+ chart9: "#EB5757",
55
+ chart10: "#56CCF2",
56
+ chart11: "#BB6BD9",
57
+ chart12: "#2D9CDB",
48
58
  radius: "0.5rem",
49
59
  };
50
60
 
@@ -79,13 +89,23 @@ export const OPTILOGIC_LEGACY_THEME: Theme = {
79
89
  chip: "#B8C5F9",
80
90
  chipForeground: "#0C0A5A",
81
91
  border: "#D0D0D0",
82
- input: "#FFFFFF",
92
+ input: "#D0D0D0",
83
93
  ring: "#5766F2",
94
+ toggleTrack: "#D0D0D0",
95
+ toggleTrackForeground: "#FFFFFF",
96
+ inputHover: "#5766F2",
84
97
  chart1: "#78D237",
85
98
  chart2: "#F5CF47",
86
99
  chart3: "#5766F2",
87
100
  chart4: "#44BD7E",
88
101
  chart5: "#929BEF",
102
+ chart6: "#DB2828",
103
+ chart7: "#E07B39",
104
+ chart8: "#2BBBAD",
105
+ chart9: "#A5673F",
106
+ chart10: "#00B5AD",
107
+ chart11: "#6435C9",
108
+ chart12: "#E03997",
89
109
  radius: "0.5rem",
90
110
  };
91
111
 
@@ -122,11 +142,21 @@ export const FUTURISTIC_THEME: Theme = {
122
142
  border: "#1e293b",
123
143
  input: "#1e293b",
124
144
  ring: "#6366f1",
145
+ toggleTrack: "#334155",
146
+ toggleTrackForeground: "#e0e7ff",
147
+ inputHover: "#6366f1",
125
148
  chart1: "#6366f1",
126
149
  chart2: "#8b5cf6",
127
150
  chart3: "#a855f7",
128
151
  chart4: "#ec4899",
129
152
  chart5: "#f43f5e",
153
+ chart6: "#06b6d4",
154
+ chart7: "#14b8a6",
155
+ chart8: "#f97316",
156
+ chart9: "#eab308",
157
+ chart10: "#22c55e",
158
+ chart11: "#0ea5e9",
159
+ chart12: "#f43f5e",
130
160
  radius: "0.5rem",
131
161
  };
132
162
 
@@ -163,11 +193,21 @@ export const NATURE_THEME: Theme = {
163
193
  border: "#243824",
164
194
  input: "#243824",
165
195
  ring: "#4caf50",
196
+ toggleTrack: "#3d5a3d",
197
+ toggleTrackForeground: "#e8f5e9",
198
+ inputHover: "#4caf50",
166
199
  chart1: "#4caf50",
167
200
  chart2: "#66bb6a",
168
201
  chart3: "#81c784",
169
202
  chart4: "#a5d6a7",
170
203
  chart5: "#c8e6c9",
204
+ chart6: "#8bc34a",
205
+ chart7: "#cddc39",
206
+ chart8: "#ffb74d",
207
+ chart9: "#4db6ac",
208
+ chart10: "#7986cb",
209
+ chart11: "#e57373",
210
+ chart12: "#64b5f6",
171
211
  radius: "0.5rem",
172
212
  };
173
213
 
@@ -204,11 +244,21 @@ export const SCIFI_THEME: Theme = {
204
244
  border: "#30363d",
205
245
  input: "#21262d",
206
246
  ring: "#00d9ff",
247
+ toggleTrack: "#30363d",
248
+ toggleTrackForeground: "#c9d1d9",
249
+ inputHover: "#00d9ff",
207
250
  chart1: "#00d9ff",
208
251
  chart2: "#ff00ff",
209
252
  chart3: "#00ff88",
210
253
  chart4: "#ffd93d",
211
254
  chart5: "#58a6ff",
255
+ chart6: "#ff6b6b",
256
+ chart7: "#4ecdc4",
257
+ chart8: "#a855f7",
258
+ chart9: "#fb923c",
259
+ chart10: "#38bdf8",
260
+ chart11: "#f472b6",
261
+ chart12: "#34d399",
212
262
  radius: "0.5rem",
213
263
  };
214
264
 
@@ -245,11 +295,21 @@ export const OCEAN_THEME: Theme = {
245
295
  border: "#1e3a5f",
246
296
  input: "#1e3a5f",
247
297
  ring: "#2196f3",
298
+ toggleTrack: "#264a6e",
299
+ toggleTrackForeground: "#e3f2fd",
300
+ inputHover: "#2196f3",
248
301
  chart1: "#2196f3",
249
302
  chart2: "#00bcd4",
250
303
  chart3: "#03a9f4",
251
304
  chart4: "#0288d1",
252
305
  chart5: "#0277bd",
306
+ chart6: "#26c6da",
307
+ chart7: "#42a5f5",
308
+ chart8: "#66bb6a",
309
+ chart9: "#ffb74d",
310
+ chart10: "#ef5350",
311
+ chart11: "#ab47bc",
312
+ chart12: "#78909c",
253
313
  radius: "0.5rem",
254
314
  };
255
315
 
@@ -286,11 +346,21 @@ export const SUNSET_THEME: Theme = {
286
346
  border: "#241424",
287
347
  input: "#241424",
288
348
  ring: "#ff6b35",
349
+ toggleTrack: "#3d2a3d",
350
+ toggleTrackForeground: "#ffe0e6",
351
+ inputHover: "#ff6b35",
289
352
  chart1: "#ff6b35",
290
353
  chart2: "#c44569",
291
354
  chart3: "#f7931e",
292
355
  chart4: "#ffb703",
293
356
  chart5: "#ff8c42",
357
+ chart6: "#06d6a0",
358
+ chart7: "#118ab2",
359
+ chart8: "#ef476f",
360
+ chart9: "#ffd166",
361
+ chart10: "#073b4c",
362
+ chart11: "#8338ec",
363
+ chart12: "#3a86ff",
294
364
  radius: "0.5rem",
295
365
  };
296
366
 
@@ -327,11 +397,21 @@ export const FOREST_THEME: Theme = {
327
397
  border: "#1b5e20",
328
398
  input: "#1a2e1a",
329
399
  ring: "#2e7d32",
400
+ toggleTrack: "#2d4a2d",
401
+ toggleTrackForeground: "#e8f5e9",
402
+ inputHover: "#2e7d32",
330
403
  chart1: "#2e7d32",
331
404
  chart2: "#388e3c",
332
405
  chart3: "#43a047",
333
406
  chart4: "#66bb6a",
334
407
  chart5: "#81c784",
408
+ chart6: "#a5d6a7",
409
+ chart7: "#c8e6c9",
410
+ chart8: "#ffb74d",
411
+ chart9: "#4db6ac",
412
+ chart10: "#7986cb",
413
+ chart11: "#e57373",
414
+ chart12: "#64b5f6",
335
415
  radius: "0.5rem",
336
416
  };
337
417
 
@@ -368,11 +448,21 @@ export const CYBERPUNK_THEME: Theme = {
368
448
  border: "#1a1a2e",
369
449
  input: "#16213e",
370
450
  ring: "#ff00ff",
451
+ toggleTrack: "#2a2a4e",
452
+ toggleTrackForeground: "#f0f0f0",
453
+ inputHover: "#ff00ff",
371
454
  chart1: "#ff00ff",
372
455
  chart2: "#00ffff",
373
456
  chart3: "#00ff88",
374
457
  chart4: "#ffd700",
375
458
  chart5: "#ff1744",
459
+ chart6: "#ff6b6b",
460
+ chart7: "#4ecdc4",
461
+ chart8: "#a855f7",
462
+ chart9: "#fb923c",
463
+ chart10: "#38bdf8",
464
+ chart11: "#84cc16",
465
+ chart12: "#f97316",
376
466
  radius: "0.5rem",
377
467
  };
378
468
 
@@ -407,13 +497,23 @@ export const MINIMALIST_LIGHT_THEME: Theme = {
407
497
  chip: "#e9ecef",
408
498
  chipForeground: "#495057",
409
499
  border: "#dee2e6",
410
- input: "#ffffff",
500
+ input: "#dee2e6",
411
501
  ring: "#000000",
502
+ toggleTrack: "#ced4da",
503
+ toggleTrackForeground: "#ffffff",
504
+ inputHover: "#6c757d",
412
505
  chart1: "#000000",
413
506
  chart2: "#6c757d",
414
507
  chart3: "#adb5bd",
415
508
  chart4: "#dee2e6",
416
509
  chart5: "#e9ecef",
510
+ chart6: "#343a40",
511
+ chart7: "#495057",
512
+ chart8: "#868e96",
513
+ chart9: "#5c6bc0",
514
+ chart10: "#26a69a",
515
+ chart11: "#ef5350",
516
+ chart12: "#ffa726",
417
517
  radius: "0.5rem",
418
518
  };
419
519
 
@@ -450,11 +550,21 @@ export const DARK_ELEGANT_THEME: Theme = {
450
550
  border: "#2d2d2d",
451
551
  input: "#1e1e1e",
452
552
  ring: "#bb86fc",
553
+ toggleTrack: "#3d3d3d",
554
+ toggleTrackForeground: "#e0e0e0",
555
+ inputHover: "#bb86fc",
453
556
  chart1: "#bb86fc",
454
557
  chart2: "#03dac6",
455
558
  chart3: "#4caf50",
456
559
  chart4: "#ff9800",
457
560
  chart5: "#cf6679",
561
+ chart6: "#64b5f6",
562
+ chart7: "#81c784",
563
+ chart8: "#ffb74d",
564
+ chart9: "#e57373",
565
+ chart10: "#7986cb",
566
+ chart11: "#4db6ac",
567
+ chart12: "#f06292",
458
568
  radius: "0.5rem",
459
569
  };
460
570
 
@@ -54,12 +54,27 @@ export interface Theme {
54
54
  input: string; // --input
55
55
  ring: string; // --ring (focus ring)
56
56
 
57
+ /** Interactive control colors (optional, with fallbacks) */
58
+ toggleTrack?: string; // --toggle-track (switch track bg when off; fallback: muted)
59
+ toggleTrackForeground?: string; // --toggle-track-foreground (switch thumb when off; fallback: background)
60
+ inputHover?: string; // --input-hover (form control border on hover; fallback: derived)
61
+
57
62
  /** Chart colors */
58
63
  chart1: string; // --chart-1
59
64
  chart2: string; // --chart-2
60
65
  chart3: string; // --chart-3
61
66
  chart4: string; // --chart-4
62
67
  chart5: string; // --chart-5
68
+ chart6: string; // --chart-6
69
+ chart7: string; // --chart-7
70
+ chart8: string; // --chart-8
71
+ chart9: string; // --chart-9
72
+ chart10: string; // --chart-10
73
+ chart11: string; // --chart-11
74
+ chart12: string; // --chart-12
75
+
76
+ /** Disabled state */
77
+ disabledOpacity?: string; // --disabled-opacity (default: 0.5)
63
78
 
64
79
  /** Border radius */
65
80
  radius?: string; // --radius (default: 0.5rem)
@@ -105,11 +120,21 @@ export interface ThemeHSL extends Omit<Theme, keyof ThemeColorFields> {
105
120
  border: string;
106
121
  input: string;
107
122
  ring: string;
123
+ toggleTrack?: string;
124
+ toggleTrackForeground?: string;
125
+ inputHover?: string;
108
126
  chart1: string;
109
127
  chart2: string;
110
128
  chart3: string;
111
129
  chart4: string;
112
130
  chart5: string;
131
+ chart6: string;
132
+ chart7: string;
133
+ chart8: string;
134
+ chart9: string;
135
+ chart10: string;
136
+ chart11: string;
137
+ chart12: string;
113
138
  }
114
139
 
115
140
  type ThemeColorFields = {
@@ -138,11 +163,21 @@ type ThemeColorFields = {
138
163
  border: string;
139
164
  input: string;
140
165
  ring: string;
166
+ toggleTrack?: string;
167
+ toggleTrackForeground?: string;
168
+ inputHover?: string;
141
169
  chart1: string;
142
170
  chart2: string;
143
171
  chart3: string;
144
172
  chart4: string;
145
173
  chart5: string;
174
+ chart6: string;
175
+ chart7: string;
176
+ chart8: string;
177
+ chart9: string;
178
+ chart10: string;
179
+ chart11: string;
180
+ chart12: string;
146
181
  };
147
182
 
148
183
  /**
@@ -85,14 +85,40 @@ export function themeToHsl(theme: Theme): ThemeHSL {
85
85
  border: hexToHsl(theme.border),
86
86
  input: hexToHsl(theme.input),
87
87
  ring: hexToHsl(theme.ring),
88
+ toggleTrack: theme.toggleTrack ? hexToHsl(theme.toggleTrack) : undefined,
89
+ toggleTrackForeground: theme.toggleTrackForeground
90
+ ? hexToHsl(theme.toggleTrackForeground)
91
+ : undefined,
92
+ inputHover: theme.inputHover ? hexToHsl(theme.inputHover) : undefined,
88
93
  chart1: hexToHsl(theme.chart1),
89
94
  chart2: hexToHsl(theme.chart2),
90
95
  chart3: hexToHsl(theme.chart3),
91
96
  chart4: hexToHsl(theme.chart4),
92
97
  chart5: hexToHsl(theme.chart5),
98
+ chart6: hexToHsl(theme.chart6),
99
+ chart7: hexToHsl(theme.chart7),
100
+ chart8: hexToHsl(theme.chart8),
101
+ chart9: hexToHsl(theme.chart9),
102
+ chart10: hexToHsl(theme.chart10),
103
+ chart11: hexToHsl(theme.chart11),
104
+ chart12: hexToHsl(theme.chart12),
93
105
  };
94
106
  }
95
107
 
108
+ /**
109
+ * Derive an input hover border color from the theme's HSL values.
110
+ * Blends toward the foreground for a subtle but visible hover effect.
111
+ */
112
+ function deriveInputHoverHsl(hslTheme: ThemeHSL): string {
113
+ const parts = hslTheme.foreground.split(/\s+/);
114
+ if (parts.length >= 3) {
115
+ const h = parts[0];
116
+ const s = parts[1];
117
+ return `${h} ${s} 50%`;
118
+ }
119
+ return hslTheme.foreground;
120
+ }
121
+
96
122
  /**
97
123
  * Apply a theme to the DOM
98
124
  *
@@ -135,11 +161,37 @@ export function applyTheme(theme: Theme, targetElement?: HTMLElement): void {
135
161
  element.style.setProperty("--border", hslTheme.border);
136
162
  element.style.setProperty("--input", hslTheme.input);
137
163
  element.style.setProperty("--ring", hslTheme.ring);
164
+
165
+ // Interactive control tokens (with fallbacks)
166
+ element.style.setProperty(
167
+ "--toggle-track",
168
+ hslTheme.toggleTrack ?? hslTheme.muted
169
+ );
170
+ element.style.setProperty(
171
+ "--toggle-track-foreground",
172
+ hslTheme.toggleTrackForeground ?? hslTheme.background
173
+ );
174
+ element.style.setProperty(
175
+ "--input-hover",
176
+ hslTheme.inputHover ?? deriveInputHoverHsl(hslTheme)
177
+ );
178
+
138
179
  element.style.setProperty("--chart-1", hslTheme.chart1);
139
180
  element.style.setProperty("--chart-2", hslTheme.chart2);
140
181
  element.style.setProperty("--chart-3", hslTheme.chart3);
141
182
  element.style.setProperty("--chart-4", hslTheme.chart4);
142
183
  element.style.setProperty("--chart-5", hslTheme.chart5);
184
+ element.style.setProperty("--chart-6", hslTheme.chart6);
185
+ element.style.setProperty("--chart-7", hslTheme.chart7);
186
+ element.style.setProperty("--chart-8", hslTheme.chart8);
187
+ element.style.setProperty("--chart-9", hslTheme.chart9);
188
+ element.style.setProperty("--chart-10", hslTheme.chart10);
189
+ element.style.setProperty("--chart-11", hslTheme.chart11);
190
+ element.style.setProperty("--chart-12", hslTheme.chart12);
191
+
192
+ if (theme.disabledOpacity) {
193
+ element.style.setProperty("--disabled-opacity", theme.disabledOpacity);
194
+ }
143
195
 
144
196
  if (theme.radius) {
145
197
  element.style.setProperty("--radius", theme.radius);
@@ -195,6 +247,13 @@ export function validateTheme(theme: unknown): theme is Theme {
195
247
  "chart3",
196
248
  "chart4",
197
249
  "chart5",
250
+ "chart6",
251
+ "chart7",
252
+ "chart8",
253
+ "chart9",
254
+ "chart10",
255
+ "chart11",
256
+ "chart12",
198
257
  ];
199
258
 
200
259
  for (const field of requiredFields) {
@@ -254,11 +313,21 @@ export function areThemesEqual(theme1: Theme, theme2: Theme): boolean {
254
313
  "border",
255
314
  "input",
256
315
  "ring",
316
+ "toggleTrack",
317
+ "toggleTrackForeground",
318
+ "inputHover",
257
319
  "chart1",
258
320
  "chart2",
259
321
  "chart3",
260
322
  "chart4",
261
323
  "chart5",
324
+ "chart6",
325
+ "chart7",
326
+ "chart8",
327
+ "chart9",
328
+ "chart10",
329
+ "chart11",
330
+ "chart12",
262
331
  ];
263
332
 
264
333
  for (const field of colorFields) {
@@ -307,3 +376,165 @@ export function importTheme(jsonString: string): {
307
376
  };
308
377
  }
309
378
  }
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // Contrast validation utilities
382
+ // ---------------------------------------------------------------------------
383
+
384
+ /**
385
+ * Parse a hex color string into linear-light sRGB components (0-1).
386
+ */
387
+ function hexToLinearRgb(hex: string): [number, number, number] {
388
+ hex = hex.replace(/^#/, "");
389
+ const r = parseInt(hex.substring(0, 2), 16) / 255;
390
+ const g = parseInt(hex.substring(2, 4), 16) / 255;
391
+ const b = parseInt(hex.substring(4, 6), 16) / 255;
392
+
393
+ const toLinear = (c: number) =>
394
+ c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
395
+
396
+ return [toLinear(r), toLinear(g), toLinear(b)];
397
+ }
398
+
399
+ /**
400
+ * Calculate WCAG 2.x relative luminance for a hex color.
401
+ * @see https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
402
+ */
403
+ export function getRelativeLuminance(hex: string): number {
404
+ const [r, g, b] = hexToLinearRgb(hex);
405
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
406
+ }
407
+
408
+ /**
409
+ * Calculate the WCAG contrast ratio between two hex colors.
410
+ * Returns a value >= 1, where 1 means no contrast and 21 is maximum.
411
+ * @see https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio
412
+ */
413
+ export function getContrastRatio(hex1: string, hex2: string): number {
414
+ const l1 = getRelativeLuminance(hex1);
415
+ const l2 = getRelativeLuminance(hex2);
416
+ const lighter = Math.max(l1, l2);
417
+ const darker = Math.min(l1, l2);
418
+ return (lighter + 0.05) / (darker + 0.05);
419
+ }
420
+
421
+ export interface ContrastWarning {
422
+ pair: [string, string];
423
+ pairLabels: [string, string];
424
+ ratio: number;
425
+ required: number;
426
+ level: "fail" | "AA-large" | "AA";
427
+ }
428
+
429
+ /**
430
+ * Validate contrast ratios for critical color pairs in a theme.
431
+ *
432
+ * WCAG 2.1 requirements:
433
+ * - **3:1** minimum for UI components and large text (AA)
434
+ * - **4.5:1** minimum for normal text (AA)
435
+ *
436
+ * Returns an array of warnings for pairs that fall below the recommended
437
+ * minimums. An empty array means all checked pairs pass.
438
+ */
439
+ export function validateThemeContrast(theme: Theme): ContrastWarning[] {
440
+ const warnings: ContrastWarning[] = [];
441
+
442
+ const uiPairs: {
443
+ a: string;
444
+ b: string;
445
+ labelA: string;
446
+ labelB: string;
447
+ minRatio: number;
448
+ }[] = [
449
+ // Text on background
450
+ {
451
+ a: theme.foreground,
452
+ b: theme.background,
453
+ labelA: "foreground",
454
+ labelB: "background",
455
+ minRatio: 4.5,
456
+ },
457
+ // Primary button text on primary bg
458
+ {
459
+ a: theme.primaryForeground,
460
+ b: theme.primary,
461
+ labelA: "primaryForeground",
462
+ labelB: "primary",
463
+ minRatio: 4.5,
464
+ },
465
+ // Accent foreground on accent
466
+ {
467
+ a: theme.accentForeground,
468
+ b: theme.accent,
469
+ labelA: "accentForeground",
470
+ labelB: "accent",
471
+ minRatio: 4.5,
472
+ },
473
+ // Input border vs background (UI component boundary — 3:1)
474
+ {
475
+ a: theme.input,
476
+ b: theme.background,
477
+ labelA: "input",
478
+ labelB: "background",
479
+ minRatio: 3,
480
+ },
481
+ // Toggle track vs background (UI component — 3:1)
482
+ {
483
+ a: theme.toggleTrack ?? theme.muted,
484
+ b: theme.background,
485
+ labelA: "toggleTrack",
486
+ labelB: "background",
487
+ minRatio: 3,
488
+ },
489
+ // Toggle thumb vs track (UI component — 3:1)
490
+ {
491
+ a: theme.toggleTrackForeground ?? theme.background,
492
+ b: theme.toggleTrack ?? theme.muted,
493
+ labelA: "toggleTrackForeground",
494
+ labelB: "toggleTrack",
495
+ minRatio: 3,
496
+ },
497
+ // Border vs background (UI component boundary — 3:1)
498
+ {
499
+ a: theme.border,
500
+ b: theme.background,
501
+ labelA: "border",
502
+ labelB: "background",
503
+ minRatio: 3,
504
+ },
505
+ // Muted foreground on muted bg
506
+ {
507
+ a: theme.mutedForeground,
508
+ b: theme.muted,
509
+ labelA: "mutedForeground",
510
+ labelB: "muted",
511
+ minRatio: 4.5,
512
+ },
513
+ // Destructive foreground on destructive bg
514
+ {
515
+ a: theme.destructiveForeground,
516
+ b: theme.destructive,
517
+ labelA: "destructiveForeground",
518
+ labelB: "destructive",
519
+ minRatio: 4.5,
520
+ },
521
+ ];
522
+
523
+ for (const { a, b, labelA, labelB, minRatio } of uiPairs) {
524
+ const ratio = getContrastRatio(a, b);
525
+ if (ratio < minRatio) {
526
+ let level: ContrastWarning["level"] = "fail";
527
+ if (ratio >= 3) level = "AA-large";
528
+
529
+ warnings.push({
530
+ pair: [a, b],
531
+ pairLabels: [labelA, labelB],
532
+ ratio: Math.round(ratio * 100) / 100,
533
+ required: minRatio,
534
+ level,
535
+ });
536
+ }
537
+ }
538
+
539
+ return warnings;
540
+ }