@object-ui/core 3.1.5 → 3.3.1

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 (110) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +20 -1
  3. package/dist/actions/ActionRunner.d.ts +9 -0
  4. package/dist/actions/ActionRunner.js +41 -4
  5. package/dist/adapters/ValueDataSource.d.ts +5 -1
  6. package/dist/adapters/ValueDataSource.js +30 -1
  7. package/dist/errors/index.js +2 -3
  8. package/dist/evaluator/ExpressionCache.d.ts +9 -10
  9. package/dist/evaluator/ExpressionCache.js +29 -8
  10. package/dist/evaluator/SafeExpressionParser.d.ts +131 -0
  11. package/dist/evaluator/SafeExpressionParser.js +851 -0
  12. package/dist/evaluator/index.d.ts +1 -0
  13. package/dist/evaluator/index.js +1 -0
  14. package/dist/protocols/DndProtocol.js +2 -14
  15. package/dist/protocols/KeyboardProtocol.js +1 -4
  16. package/dist/protocols/NotificationProtocol.js +3 -13
  17. package/dist/utils/debug.js +2 -1
  18. package/dist/utils/filter-converter.js +25 -5
  19. package/package.json +33 -9
  20. package/.turbo/turbo-build.log +0 -4
  21. package/src/__benchmarks__/core.bench.ts +0 -64
  22. package/src/__tests__/protocols/DndProtocol.test.ts +0 -186
  23. package/src/__tests__/protocols/KeyboardProtocol.test.ts +0 -177
  24. package/src/__tests__/protocols/NotificationProtocol.test.ts +0 -142
  25. package/src/__tests__/protocols/ResponsiveProtocol.test.ts +0 -176
  26. package/src/__tests__/protocols/SharingProtocol.test.ts +0 -188
  27. package/src/actions/ActionEngine.ts +0 -268
  28. package/src/actions/ActionRunner.ts +0 -717
  29. package/src/actions/TransactionManager.ts +0 -521
  30. package/src/actions/UndoManager.ts +0 -215
  31. package/src/actions/__tests__/ActionEngine.test.ts +0 -206
  32. package/src/actions/__tests__/ActionRunner.params.test.ts +0 -134
  33. package/src/actions/__tests__/ActionRunner.test.ts +0 -711
  34. package/src/actions/__tests__/TransactionManager.test.ts +0 -447
  35. package/src/actions/__tests__/UndoManager.test.ts +0 -320
  36. package/src/actions/index.ts +0 -12
  37. package/src/adapters/ApiDataSource.ts +0 -376
  38. package/src/adapters/README.md +0 -180
  39. package/src/adapters/ValueDataSource.ts +0 -438
  40. package/src/adapters/__tests__/ApiDataSource.test.ts +0 -418
  41. package/src/adapters/__tests__/ValueDataSource.test.ts +0 -472
  42. package/src/adapters/__tests__/resolveDataSource.test.ts +0 -144
  43. package/src/adapters/index.ts +0 -15
  44. package/src/adapters/resolveDataSource.ts +0 -79
  45. package/src/builder/__tests__/schema-builder.test.ts +0 -235
  46. package/src/builder/schema-builder.ts +0 -584
  47. package/src/data-scope/DataScopeManager.ts +0 -269
  48. package/src/data-scope/ViewDataProvider.ts +0 -282
  49. package/src/data-scope/__tests__/DataScopeManager.test.ts +0 -211
  50. package/src/data-scope/__tests__/ViewDataProvider.test.ts +0 -270
  51. package/src/data-scope/index.ts +0 -24
  52. package/src/errors/__tests__/errors.test.ts +0 -292
  53. package/src/errors/index.ts +0 -270
  54. package/src/evaluator/ExpressionCache.ts +0 -192
  55. package/src/evaluator/ExpressionContext.ts +0 -118
  56. package/src/evaluator/ExpressionEvaluator.ts +0 -315
  57. package/src/evaluator/FormulaFunctions.ts +0 -398
  58. package/src/evaluator/__tests__/ExpressionCache.test.ts +0 -135
  59. package/src/evaluator/__tests__/ExpressionContext.test.ts +0 -110
  60. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +0 -131
  61. package/src/evaluator/__tests__/FormulaFunctions.test.ts +0 -447
  62. package/src/evaluator/index.ts +0 -12
  63. package/src/index.ts +0 -38
  64. package/src/protocols/DndProtocol.ts +0 -184
  65. package/src/protocols/KeyboardProtocol.ts +0 -185
  66. package/src/protocols/NotificationProtocol.ts +0 -159
  67. package/src/protocols/ResponsiveProtocol.ts +0 -210
  68. package/src/protocols/SharingProtocol.ts +0 -185
  69. package/src/protocols/index.ts +0 -13
  70. package/src/query/__tests__/query-ast.test.ts +0 -211
  71. package/src/query/__tests__/window-functions.test.ts +0 -275
  72. package/src/query/index.ts +0 -7
  73. package/src/query/query-ast.ts +0 -341
  74. package/src/registry/PluginScopeImpl.ts +0 -259
  75. package/src/registry/PluginSystem.ts +0 -206
  76. package/src/registry/Registry.ts +0 -219
  77. package/src/registry/WidgetRegistry.ts +0 -316
  78. package/src/registry/__tests__/PluginSystem.test.ts +0 -309
  79. package/src/registry/__tests__/Registry.test.ts +0 -293
  80. package/src/registry/__tests__/WidgetRegistry.test.ts +0 -321
  81. package/src/registry/__tests__/plugin-scope-integration.test.ts +0 -283
  82. package/src/theme/ThemeEngine.ts +0 -530
  83. package/src/theme/__tests__/ThemeEngine.test.ts +0 -668
  84. package/src/theme/index.ts +0 -24
  85. package/src/types/index.ts +0 -21
  86. package/src/utils/__tests__/debug-collector.test.ts +0 -102
  87. package/src/utils/__tests__/debug.test.ts +0 -134
  88. package/src/utils/__tests__/expand-fields.test.ts +0 -120
  89. package/src/utils/__tests__/extract-records.test.ts +0 -50
  90. package/src/utils/__tests__/filter-converter.test.ts +0 -118
  91. package/src/utils/__tests__/merge-views-into-objects.test.ts +0 -110
  92. package/src/utils/__tests__/normalize-quick-filter.test.ts +0 -123
  93. package/src/utils/debug-collector.ts +0 -100
  94. package/src/utils/debug.ts +0 -147
  95. package/src/utils/expand-fields.ts +0 -76
  96. package/src/utils/extract-records.ts +0 -33
  97. package/src/utils/filter-converter.ts +0 -133
  98. package/src/utils/merge-views-into-objects.ts +0 -36
  99. package/src/utils/normalize-quick-filter.ts +0 -78
  100. package/src/validation/__tests__/object-validation-engine.test.ts +0 -567
  101. package/src/validation/__tests__/schema-validator.test.ts +0 -118
  102. package/src/validation/__tests__/validation-engine.test.ts +0 -102
  103. package/src/validation/index.ts +0 -10
  104. package/src/validation/schema-validator.ts +0 -344
  105. package/src/validation/validation-engine.ts +0 -528
  106. package/src/validation/validators/index.ts +0 -25
  107. package/src/validation/validators/object-validation-engine.ts +0 -722
  108. package/tsconfig.json +0 -15
  109. package/tsconfig.tsbuildinfo +0 -1
  110. package/vitest.config.ts +0 -2
@@ -1,530 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * @object-ui/core - Theme Engine
11
- *
12
- * Converts a spec-aligned Theme JSON into CSS custom properties
13
- * that can be injected into the DOM. Also handles theme inheritance
14
- * (extends), media-query-aware mode resolution, and token merging.
15
- *
16
- * @module theme
17
- * @packageDocumentation
18
- */
19
-
20
- import type { Theme, ColorPalette, ThemeMode } from '@object-ui/types';
21
-
22
- // ============================================================================
23
- // Color Utilities
24
- // ============================================================================
25
-
26
- /**
27
- * Convert a hex color (#RRGGBB or #RGB) to an HSL string "H S% L%".
28
- * Returns null if the input is not a valid hex color.
29
- */
30
- export function hexToHSL(hex: string): string | null {
31
- // Expand shorthand (#RGB → #RRGGBB)
32
- let clean = hex.replace(/^#/, '');
33
- if (clean.length === 3) {
34
- clean = clean[0] + clean[0] + clean[1] + clean[1] + clean[2] + clean[2];
35
- }
36
- const match = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(clean);
37
- if (!match) return null;
38
-
39
- const r = parseInt(match[1], 16) / 255;
40
- const g = parseInt(match[2], 16) / 255;
41
- const b = parseInt(match[3], 16) / 255;
42
-
43
- const max = Math.max(r, g, b);
44
- const min = Math.min(r, g, b);
45
- let h = 0;
46
- let s = 0;
47
- const l = (max + min) / 2;
48
-
49
- if (max !== min) {
50
- const d = max - min;
51
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
52
- switch (max) {
53
- case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
54
- case g: h = ((b - r) / d + 2) / 6; break;
55
- case b: h = ((r - g) / d + 4) / 6; break;
56
- }
57
- }
58
-
59
- return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
60
- }
61
-
62
- /**
63
- * Detect if a color string is a hex value.
64
- */
65
- function isHex(color: string): boolean {
66
- return /^#([a-f\d]{3}|[a-f\d]{6})$/i.test(color);
67
- }
68
-
69
- /**
70
- * Convert a color to a CSS-ready value.
71
- * - Hex colors → HSL format for Shadcn CSS variable compatibility
72
- * - Non-hex colors → passed through as-is (rgb, hsl, oklch, etc.)
73
- */
74
- export function toCSSColor(color: string): string {
75
- if (isHex(color)) {
76
- return hexToHSL(color) ?? color;
77
- }
78
- return color;
79
- }
80
-
81
- // ============================================================================
82
- // Theme → CSS Variable Mapping
83
- // ============================================================================
84
-
85
- /**
86
- * Mapping from spec ColorPalette keys → Shadcn CSS variable names.
87
- *
88
- * The spec uses semantic names (primary, secondary, error, text, surface, etc.)
89
- * while Shadcn uses its own naming (--primary, --secondary, --destructive, etc.)
90
- * This maps the spec keys to the closest Shadcn equivalent.
91
- */
92
- const COLOR_TO_CSS_MAP: Record<keyof ColorPalette, string | string[]> = {
93
- primary: '--primary',
94
- secondary: '--secondary',
95
- accent: '--accent',
96
- success: '--success',
97
- warning: '--warning',
98
- error: '--destructive',
99
- info: '--info',
100
- background: '--background',
101
- surface: '--card',
102
- text: '--foreground',
103
- textSecondary: '--muted-foreground',
104
- border: '--border',
105
- disabled: '--muted',
106
- primaryLight: '--primary-light',
107
- primaryDark: '--primary-dark',
108
- secondaryLight: '--secondary-light',
109
- secondaryDark: '--secondary-dark',
110
- };
111
-
112
- /**
113
- * Generate CSS custom properties from a Theme's color palette.
114
- */
115
- export function generateColorVars(colors: ColorPalette): Record<string, string> {
116
- const vars: Record<string, string> = {};
117
-
118
- for (const [key, cssVar] of Object.entries(COLOR_TO_CSS_MAP)) {
119
- const value = colors[key as keyof ColorPalette];
120
- if (value) {
121
- const cssValue = toCSSColor(value);
122
- if (Array.isArray(cssVar)) {
123
- for (const v of cssVar) {
124
- vars[v] = cssValue;
125
- }
126
- } else {
127
- vars[cssVar] = cssValue;
128
- }
129
- }
130
- }
131
-
132
- return vars;
133
- }
134
-
135
- /**
136
- * Generate CSS custom properties from a Theme's typography config.
137
- */
138
- export function generateTypographyVars(typography: NonNullable<Theme['typography']>): Record<string, string> {
139
- const vars: Record<string, string> = {};
140
-
141
- if (typography.fontFamily?.base) {
142
- vars['--font-sans'] = typography.fontFamily.base;
143
- }
144
- if (typography.fontFamily?.heading) {
145
- vars['--font-heading'] = typography.fontFamily.heading;
146
- }
147
- if (typography.fontFamily?.mono) {
148
- vars['--font-mono'] = typography.fontFamily.mono;
149
- }
150
-
151
- if (typography.fontSize) {
152
- for (const [key, value] of Object.entries(typography.fontSize)) {
153
- if (value) vars[`--font-size-${key}`] = value;
154
- }
155
- }
156
-
157
- if (typography.fontWeight) {
158
- for (const [key, value] of Object.entries(typography.fontWeight)) {
159
- if (value != null) vars[`--font-weight-${key}`] = String(value);
160
- }
161
- }
162
-
163
- if (typography.lineHeight) {
164
- for (const [key, value] of Object.entries(typography.lineHeight)) {
165
- if (value) vars[`--line-height-${key}`] = value;
166
- }
167
- }
168
-
169
- if (typography.letterSpacing) {
170
- for (const [key, value] of Object.entries(typography.letterSpacing)) {
171
- if (value) vars[`--letter-spacing-${key}`] = value;
172
- }
173
- }
174
-
175
- return vars;
176
- }
177
-
178
- /**
179
- * Generate CSS custom properties from a Theme's border radius config.
180
- */
181
- export function generateBorderRadiusVars(borderRadius: NonNullable<Theme['borderRadius']>): Record<string, string> {
182
- const vars: Record<string, string> = {};
183
- const map: Record<string, string> = {
184
- none: '--radius-none',
185
- sm: '--radius-sm',
186
- base: '--radius',
187
- md: '--radius-md',
188
- lg: '--radius-lg',
189
- xl: '--radius-xl',
190
- '2xl': '--radius-2xl',
191
- full: '--radius-full',
192
- };
193
-
194
- for (const [key, cssVar] of Object.entries(map)) {
195
- const value = borderRadius[key as keyof typeof borderRadius];
196
- if (value) vars[cssVar] = value;
197
- }
198
-
199
- return vars;
200
- }
201
-
202
- /**
203
- * Generate CSS custom properties from a Theme's shadow config.
204
- */
205
- export function generateShadowVars(shadows: NonNullable<Theme['shadows']>): Record<string, string> {
206
- const vars: Record<string, string> = {};
207
- const map: Record<string, string> = {
208
- none: '--shadow-none',
209
- sm: '--shadow-sm',
210
- base: '--shadow',
211
- md: '--shadow-md',
212
- lg: '--shadow-lg',
213
- xl: '--shadow-xl',
214
- '2xl': '--shadow-2xl',
215
- inner: '--shadow-inner',
216
- };
217
-
218
- for (const [key, cssVar] of Object.entries(map)) {
219
- const value = shadows[key as keyof typeof shadows];
220
- if (value) vars[cssVar] = value;
221
- }
222
-
223
- return vars;
224
- }
225
-
226
- /**
227
- * Generate CSS custom properties from a Theme's animation config.
228
- */
229
- export function generateAnimationVars(animation: NonNullable<Theme['animation']>): Record<string, string> {
230
- const vars: Record<string, string> = {};
231
-
232
- if (animation.duration) {
233
- for (const [key, value] of Object.entries(animation.duration)) {
234
- if (value) vars[`--duration-${key}`] = value;
235
- }
236
- }
237
-
238
- if (animation.timing) {
239
- for (const [key, value] of Object.entries(animation.timing)) {
240
- if (value) vars[`--timing-${key}`] = value;
241
- }
242
- }
243
-
244
- return vars;
245
- }
246
-
247
- /**
248
- * Generate CSS custom properties from a Theme's z-index config.
249
- */
250
- export function generateZIndexVars(zIndex: NonNullable<Theme['zIndex']>): Record<string, string> {
251
- const vars: Record<string, string> = {};
252
-
253
- for (const [key, value] of Object.entries(zIndex)) {
254
- if (value != null) vars[`--z-${key}`] = String(value);
255
- }
256
-
257
- return vars;
258
- }
259
-
260
- /**
261
- * Generate ALL CSS custom properties from a complete Theme.
262
- * This is the main entry point for theme → CSS conversion.
263
- */
264
- export function generateThemeVars(theme: Theme): Record<string, string> {
265
- const vars: Record<string, string> = {};
266
-
267
- // Colors (always present — colors.primary is required)
268
- Object.assign(vars, generateColorVars(theme.colors));
269
-
270
- // Typography
271
- if (theme.typography) {
272
- Object.assign(vars, generateTypographyVars(theme.typography));
273
- }
274
-
275
- // Border Radius
276
- if (theme.borderRadius) {
277
- Object.assign(vars, generateBorderRadiusVars(theme.borderRadius));
278
- }
279
-
280
- // Shadows
281
- if (theme.shadows) {
282
- Object.assign(vars, generateShadowVars(theme.shadows));
283
- }
284
-
285
- // Animation
286
- if (theme.animation) {
287
- Object.assign(vars, generateAnimationVars(theme.animation));
288
- }
289
-
290
- // Z-Index
291
- if (theme.zIndex) {
292
- Object.assign(vars, generateZIndexVars(theme.zIndex));
293
- }
294
-
295
- // Custom CSS variables (passthrough)
296
- if (theme.customVars) {
297
- for (const [key, value] of Object.entries(theme.customVars)) {
298
- // Ensure CSS variable prefix
299
- const varName = key.startsWith('--') ? key : `--${key}`;
300
- vars[varName] = value;
301
- }
302
- }
303
-
304
- return vars;
305
- }
306
-
307
- // ============================================================================
308
- // Theme Inheritance
309
- // ============================================================================
310
-
311
- /**
312
- * Deep-merge two Theme objects. The `child` overrides the `parent`.
313
- * Only defined properties in child override; undefined falls back to parent.
314
- */
315
- export function mergeThemes(parent: Theme, child: Partial<Theme>): Theme {
316
- return {
317
- ...parent,
318
- ...child,
319
- // Deep-merge colors
320
- colors: {
321
- ...parent.colors,
322
- ...(child.colors ?? {}),
323
- },
324
- // Deep-merge typography
325
- typography: child.typography || parent.typography
326
- ? {
327
- ...parent.typography,
328
- ...child.typography,
329
- fontFamily: {
330
- ...parent.typography?.fontFamily,
331
- ...child.typography?.fontFamily,
332
- },
333
- fontSize: {
334
- ...parent.typography?.fontSize,
335
- ...child.typography?.fontSize,
336
- },
337
- fontWeight: {
338
- ...parent.typography?.fontWeight,
339
- ...child.typography?.fontWeight,
340
- },
341
- lineHeight: {
342
- ...parent.typography?.lineHeight,
343
- ...child.typography?.lineHeight,
344
- },
345
- letterSpacing: {
346
- ...parent.typography?.letterSpacing,
347
- ...child.typography?.letterSpacing,
348
- },
349
- }
350
- : undefined,
351
- // Deep-merge border radius
352
- borderRadius: child.borderRadius || parent.borderRadius
353
- ? { ...parent.borderRadius, ...child.borderRadius }
354
- : undefined,
355
- // Deep-merge shadows
356
- shadows: child.shadows || parent.shadows
357
- ? { ...parent.shadows, ...child.shadows }
358
- : undefined,
359
- // Deep-merge breakpoints
360
- breakpoints: child.breakpoints || parent.breakpoints
361
- ? { ...parent.breakpoints, ...child.breakpoints }
362
- : undefined,
363
- // Deep-merge animation
364
- animation: child.animation || parent.animation
365
- ? {
366
- ...parent.animation,
367
- ...child.animation,
368
- duration: {
369
- ...parent.animation?.duration,
370
- ...child.animation?.duration,
371
- },
372
- timing: {
373
- ...parent.animation?.timing,
374
- ...child.animation?.timing,
375
- },
376
- }
377
- : undefined,
378
- // Deep-merge zIndex
379
- zIndex: child.zIndex || parent.zIndex
380
- ? { ...parent.zIndex, ...child.zIndex }
381
- : undefined,
382
- // Deep-merge spacing
383
- spacing: child.spacing || parent.spacing
384
- ? { ...parent.spacing, ...child.spacing }
385
- : undefined,
386
- // Deep-merge customVars
387
- customVars: child.customVars || parent.customVars
388
- ? { ...parent.customVars, ...child.customVars }
389
- : undefined,
390
- // Deep-merge logo
391
- logo: child.logo || parent.logo
392
- ? { ...parent.logo, ...child.logo }
393
- : undefined,
394
- };
395
- }
396
-
397
- /**
398
- * Resolve theme inheritance from a registry of themes.
399
- * If a theme has `extends`, the parent is looked up and merged recursively.
400
- *
401
- * @param theme - The theme to resolve
402
- * @param registry - Map of theme name → Theme
403
- * @param visited - Set of already-visited names (cycle detection)
404
- * @returns The fully resolved theme
405
- */
406
- export function resolveThemeInheritance(
407
- theme: Theme,
408
- registry: Map<string, Theme>,
409
- visited: Set<string> = new Set(),
410
- ): Theme {
411
- if (!theme.extends) return theme;
412
-
413
- // Cycle detection
414
- if (visited.has(theme.name)) return theme;
415
- visited.add(theme.name);
416
-
417
- const parent = registry.get(theme.extends);
418
- if (!parent) return theme;
419
-
420
- // Recursively resolve parent first
421
- const resolvedParent = resolveThemeInheritance(parent, registry, visited);
422
-
423
- return mergeThemes(resolvedParent, theme);
424
- }
425
-
426
- // ============================================================================
427
- // Mode Resolution
428
- // ============================================================================
429
-
430
- /**
431
- * Resolve the effective mode from a ThemeMode value.
432
- * 'auto' checks the system preference (prefers-color-scheme).
433
- *
434
- * @param mode - The declared mode
435
- * @param systemDark - Whether the system prefers dark mode (for SSR or testing)
436
- * @returns 'light' or 'dark'
437
- */
438
- export function resolveMode(
439
- mode: ThemeMode = 'auto',
440
- systemDark?: boolean,
441
- ): 'light' | 'dark' {
442
- if (mode === 'light' || mode === 'dark') return mode;
443
-
444
- // 'auto' — check system preference
445
- if (systemDark !== undefined) return systemDark ? 'dark' : 'light';
446
-
447
- if (typeof window !== 'undefined' && window.matchMedia) {
448
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
449
- }
450
-
451
- return 'light'; // fallback
452
- }
453
-
454
- // ============================================================================
455
- // WCAG Contrast Checking (v2.0.7)
456
- // ============================================================================
457
-
458
- /**
459
- * Parse a hex color string to RGB values [0-255].
460
- */
461
- function hexToRGB(hex: string): [number, number, number] | null {
462
- let clean = hex.replace(/^#/, '');
463
- if (clean.length === 3) {
464
- clean = clean[0] + clean[0] + clean[1] + clean[1] + clean[2] + clean[2];
465
- }
466
- const match = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(clean);
467
- if (!match) return null;
468
- return [parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16)];
469
- }
470
-
471
- /**
472
- * Calculate relative luminance per WCAG 2.1 spec.
473
- * @see https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
474
- */
475
- function relativeLuminance(r: number, g: number, b: number): number {
476
- const [rs, gs, bs] = [r, g, b].map(c => {
477
- const s = c / 255;
478
- return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
479
- });
480
- return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
481
- }
482
-
483
- /**
484
- * Calculate the WCAG 2.1 contrast ratio between two hex colors.
485
- * Returns a value between 1 and 21.
486
- *
487
- * @param hex1 - First color in hex format (#RGB or #RRGGBB)
488
- * @param hex2 - Second color in hex format (#RGB or #RRGGBB)
489
- * @returns Contrast ratio (1-21), or null if colors are invalid
490
- */
491
- export function contrastRatio(hex1: string, hex2: string): number | null {
492
- const rgb1 = hexToRGB(hex1);
493
- const rgb2 = hexToRGB(hex2);
494
- if (!rgb1 || !rgb2) return null;
495
-
496
- const l1 = relativeLuminance(...rgb1);
497
- const l2 = relativeLuminance(...rgb2);
498
- const lighter = Math.max(l1, l2);
499
- const darker = Math.min(l1, l2);
500
- return (lighter + 0.05) / (darker + 0.05);
501
- }
502
-
503
- /**
504
- * Check if two colors meet the specified WCAG contrast level.
505
- *
506
- * WCAG levels:
507
- * - AA: 4.5:1 for normal text, 3:1 for large text
508
- * - AAA: 7:1 for normal text, 4.5:1 for large text
509
- *
510
- * @param hex1 - First color in hex format
511
- * @param hex2 - Second color in hex format
512
- * @param level - WCAG level: 'AA' or 'AAA'
513
- * @param isLargeText - Whether the text is large (18pt+ or 14pt+ bold)
514
- * @returns true if the color pair meets the required contrast level
515
- */
516
- export function meetsContrastLevel(
517
- hex1: string,
518
- hex2: string,
519
- level: 'AA' | 'AAA' = 'AA',
520
- isLargeText = false,
521
- ): boolean {
522
- const ratio = contrastRatio(hex1, hex2);
523
- if (ratio === null) return false;
524
-
525
- if (level === 'AAA') {
526
- return isLargeText ? ratio >= 4.5 : ratio >= 7;
527
- }
528
- // AA
529
- return isLargeText ? ratio >= 3 : ratio >= 4.5;
530
- }