Re-scoped from @lilith/ui-theme to @cocotte/ui-theme. In-set cross-package deps re-pointed to @cocotte; out-of-set @lilith deps preserved (same Verdaccio). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
177 lines
No EOL
5.7 KiB
JavaScript
177 lines
No EOL
5.7 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { contrastRatio, isOpaqueHexColor } from '../utils/contrast';
|
|
import { cyberpunkAdapter } from '../adapters/cyberpunk-adapter';
|
|
import { synthwaveAdapter } from '../adapters/synthwave-adapter';
|
|
import { synthwaveLightAdapter } from '../adapters/synthwave-light-adapter';
|
|
import { luxeAdapter } from '../adapters/luxe-adapter';
|
|
import { lilithAdapter } from '../adapters/lilith-adapter';
|
|
import { neutralThemeAdapter } from '../adapters/neutral-adapter';
|
|
import { corporateThemeAdapter } from '../adapters/corporate-adapter';
|
|
import { creatorPortalThemeAdapter } from '../adapters/creator-portal-adapter';
|
|
import { pitchDeckAdapter, pitchDeckLightAdapter } from '../adapters/pitch-deck-adapter';
|
|
// ---------------------------------------------------------------------------
|
|
// Theme registry — all 10 adapters
|
|
// ---------------------------------------------------------------------------
|
|
const ALL_THEMES = [
|
|
['cyberpunk', cyberpunkAdapter],
|
|
['synthwave', synthwaveAdapter],
|
|
['synthwave-light', synthwaveLightAdapter],
|
|
['luxe', luxeAdapter],
|
|
['lilith', lilithAdapter],
|
|
['neutral', neutralThemeAdapter],
|
|
['corporate', corporateThemeAdapter],
|
|
['creator-portal', creatorPortalThemeAdapter],
|
|
['pitch-deck', pitchDeckAdapter],
|
|
['pitch-deck-light', pitchDeckLightAdapter],
|
|
];
|
|
/**
|
|
* Text on backgrounds — WCAG AA normal text (4.5:1)
|
|
*/
|
|
const TEXT_ON_BACKGROUNDS = [
|
|
{
|
|
name: 'text.primary on background.primary',
|
|
fg: (t) => t.colors.text.primary,
|
|
bg: (t) => t.colors.background.primary,
|
|
min: 4.5,
|
|
},
|
|
{
|
|
name: 'text.primary on background.secondary',
|
|
fg: (t) => t.colors.text.primary,
|
|
bg: (t) => t.colors.background.secondary,
|
|
min: 4.5,
|
|
},
|
|
{
|
|
name: 'text.primary on background.tertiary',
|
|
fg: (t) => t.colors.text.primary,
|
|
bg: (t) => t.colors.background.tertiary,
|
|
min: 4.5,
|
|
},
|
|
{
|
|
name: 'text.secondary on background.primary',
|
|
fg: (t) => t.colors.text.secondary,
|
|
bg: (t) => t.colors.background.primary,
|
|
min: 4.5,
|
|
},
|
|
{
|
|
name: 'text.secondary on background.secondary',
|
|
fg: (t) => t.colors.text.secondary,
|
|
bg: (t) => t.colors.background.secondary,
|
|
min: 4.5,
|
|
},
|
|
{
|
|
name: 'text.tertiary on background.primary',
|
|
fg: (t) => t.colors.text.tertiary,
|
|
bg: (t) => t.colors.background.primary,
|
|
min: 4.5,
|
|
},
|
|
{
|
|
name: 'text.primary on surface',
|
|
fg: (t) => t.colors.text.primary,
|
|
bg: (t) => t.colors.surface,
|
|
min: 4.5,
|
|
},
|
|
];
|
|
/**
|
|
* Semantic status colors — WCAG AA normal text (4.5:1)
|
|
*/
|
|
const SEMANTIC_STATUS = [
|
|
{
|
|
name: 'success.text on success.background',
|
|
fg: (t) => t.colors.success.text,
|
|
bg: (t) => t.colors.success.background,
|
|
min: 4.5,
|
|
},
|
|
{
|
|
name: 'warning.text on warning.background',
|
|
fg: (t) => t.colors.warning.text,
|
|
bg: (t) => t.colors.warning.background,
|
|
min: 4.5,
|
|
},
|
|
{
|
|
name: 'error.text on error.background',
|
|
fg: (t) => t.colors.error.text,
|
|
bg: (t) => t.colors.error.background,
|
|
min: 4.5,
|
|
},
|
|
{
|
|
name: 'info.text on info.background',
|
|
fg: (t) => t.colors.info.text,
|
|
bg: (t) => t.colors.info.background,
|
|
min: 4.5,
|
|
},
|
|
];
|
|
/**
|
|
* Interactive UI elements — WCAG AA-large (3:1)
|
|
*/
|
|
const UI_ELEMENTS = [
|
|
{
|
|
name: 'primary.main on background.primary',
|
|
fg: (t) => t.colors.primary.main,
|
|
bg: (t) => t.colors.background.primary,
|
|
min: 3.0,
|
|
},
|
|
{
|
|
name: 'secondary.main on background.primary',
|
|
fg: (t) => t.colors.secondary.main,
|
|
bg: (t) => t.colors.background.primary,
|
|
min: 3.0,
|
|
},
|
|
{
|
|
name: 'border.default on background.primary',
|
|
fg: (t) => t.colors.border.default,
|
|
bg: (t) => t.colors.background.primary,
|
|
min: 3.0,
|
|
},
|
|
{
|
|
name: 'disabled.text on disabled.background',
|
|
fg: (t) => t.colors.disabled.text,
|
|
bg: (t) => t.colors.disabled.background,
|
|
min: 3.0,
|
|
},
|
|
];
|
|
/**
|
|
* Button labels — white text on accent backgrounds (3:1)
|
|
*/
|
|
const BUTTON_LABELS = [
|
|
{
|
|
name: '#ffffff on primary.main',
|
|
fg: () => '#ffffff',
|
|
bg: (t) => t.colors.primary.main,
|
|
min: 3.0,
|
|
},
|
|
{
|
|
name: '#ffffff on error.main',
|
|
fg: () => '#ffffff',
|
|
bg: (t) => t.colors.error.main,
|
|
min: 3.0,
|
|
},
|
|
{
|
|
name: '#ffffff on success.main',
|
|
fg: () => '#ffffff',
|
|
bg: (t) => t.colors.success.main,
|
|
min: 3.0,
|
|
},
|
|
];
|
|
const ALL_COLOR_PAIRS = [
|
|
...TEXT_ON_BACKGROUNDS,
|
|
...SEMANTIC_STATUS,
|
|
...UI_ELEMENTS,
|
|
...BUTTON_LABELS,
|
|
];
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
describe.each(ALL_THEMES)('Theme: %s', (themeName, theme) => {
|
|
it.each(ALL_COLOR_PAIRS)('$name meets contrast ratio $min:1', ({ fg, bg, min }) => {
|
|
const fgColor = fg(theme);
|
|
const bgColor = bg(theme);
|
|
// Skip non-hex or alpha-bearing hex values (e.g. #ff006e15 semantic backgrounds).
|
|
// Alpha hex requires compositing against a known base — can't compute contrast in isolation.
|
|
if (!isOpaqueHexColor(fgColor) || !isOpaqueHexColor(bgColor)) {
|
|
return;
|
|
}
|
|
const ratio = contrastRatio(fgColor, bgColor);
|
|
expect(ratio, `${themeName}: ratio ${ratio.toFixed(2)}:1 (need ${min}:1) — fg: ${fgColor}, bg: ${bgColor}`).toBeGreaterThanOrEqual(min);
|
|
});
|
|
});
|
|
//# sourceMappingURL=theme-contrast.test.js.map
|