diff --git a/src/lib/utils/colors.test.ts b/src/lib/utils/colors.test.ts index 6dc1c0c..7977c30 100644 --- a/src/lib/utils/colors.test.ts +++ b/src/lib/utils/colors.test.ts @@ -8,13 +8,13 @@ describe('getNoteColor', () => { }); it('returns dark color when isDark is true', () => { - expect(getNoteColor('default', true)).toBe('#2a2520'); - expect(getNoteColor('coral', true)).toBe('#4a2522'); + expect(getNoteColor('default', true)).toBe('#21201f'); + expect(getNoteColor('coral', true)).toBe('#3f1f1c'); }); it('returns default color for unknown color name', () => { expect(getNoteColor('unknown' as any, false)).toBe('#faf5eb'); - expect(getNoteColor('unknown' as any, true)).toBe('#2a2520'); + expect(getNoteColor('unknown' as any, true)).toBe('#21201f'); }); it('has matching keys in light and dark color maps', () => { @@ -23,3 +23,43 @@ describe('getNoteColor', () => { expect(lightKeys).toEqual(darkKeys); }); }); + +/** WCAG relative luminance. */ +function relativeLuminance(hex: string): number { + const n = parseInt(hex.slice(1), 16); + const channels = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((v) => { + const s = v / 255; + return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); + }); + return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2]; +} + +function contrastRatio(a: string, b: string): number { + const [l1, l2] = [relativeLuminance(a), relativeLuminance(b)]; + return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); +} + +describe('dark note colours', () => { + // Must track [data-theme="dark"] in src/app.css. + const DARK_TEXT = '#ece3d3'; + const DARK_TEXT_MUTED = '#b3a695'; + + it('keeps body text above the AA threshold on every dark card', () => { + for (const [name, { bg }] of Object.entries(NOTE_COLORS_DARK)) { + expect(contrastRatio(bg, DARK_TEXT), `${name} (${bg})`).toBeGreaterThanOrEqual(4.5); + } + }); + + it('keeps muted text above the AA threshold on every dark card', () => { + // This is the invariant the old palette broke: muted/struck-through text + // became unreadable on the lighter surfaces. + for (const [name, { bg }] of Object.entries(NOTE_COLORS_DARK)) { + expect(contrastRatio(bg, DARK_TEXT_MUTED), `${name} (${bg})`).toBeGreaterThanOrEqual(4.5); + } + }); + + it('holds the twelve surfaces within a narrow lightness band so they read as a family', () => { + const luminances = Object.values(NOTE_COLORS_DARK).map((c) => relativeLuminance(c.bg)); + expect(Math.max(...luminances) - Math.min(...luminances)).toBeLessThan(0.03); + }); +}); diff --git a/src/lib/utils/colors.ts b/src/lib/utils/colors.ts index 09f7190..f3aa947 100644 --- a/src/lib/utils/colors.ts +++ b/src/lib/utils/colors.ts @@ -16,18 +16,18 @@ export const NOTE_COLORS: Record = { }; export const NOTE_COLORS_DARK: Record = { - default: { bg: '#2a2520', label: 'Default' }, - coral: { bg: '#4a2522', label: 'Coral' }, - peach: { bg: '#4a3020', label: 'Peach' }, - sand: { bg: '#3a3520', label: 'Sand' }, - mint: { bg: '#2a3a22', label: 'Mint' }, - sage: { bg: '#223a32', label: 'Sage' }, - fog: { bg: '#222e3a', label: 'Fog' }, - storm: { bg: '#1e2a35', label: 'Storm' }, - dusk: { bg: '#352540', label: 'Dusk' }, - blossom: { bg: '#3a2830', label: 'Blossom' }, - clay: { bg: '#302e28', label: 'Clay' }, - chalk: { bg: '#2e2e30', label: 'Chalk' } + default: { bg: '#21201f', label: 'Default' }, + coral: { bg: '#3f1f1c', label: 'Coral' }, + peach: { bg: '#3f2b1c', label: 'Peach' }, + sand: { bg: '#3c371b', label: 'Sand' }, + mint: { bg: '#2a3d1d', label: 'Mint' }, + sage: { bg: '#1f3d36', label: 'Sage' }, + fog: { bg: '#253946', label: 'Fog' }, + storm: { bg: '#1d283d', label: 'Storm' }, + dusk: { bg: '#3d2843', label: 'Dusk' }, + blossom: { bg: '#3d2129', label: 'Blossom' }, + clay: { bg: '#363126', label: 'Clay' }, + chalk: { bg: '#303036', label: 'Chalk' } }; export function getNoteColor(color: NoteColor, isDark: boolean): string { diff --git a/tests/e2e/dark-mode.spec.ts b/tests/e2e/dark-mode.spec.ts index f409dd7..254d537 100644 --- a/tests/e2e/dark-mode.spec.ts +++ b/tests/e2e/dark-mode.spec.ts @@ -81,7 +81,7 @@ test.describe.serial('Dark mode', () => { const noteCard = page.getByTestId('note-card').first(); await expect(noteCard).toBeVisible(); const bg = await noteCard.evaluate((el) => getComputedStyle(el).backgroundColor); - // Dark default color is #2a2520 = rgb(42, 37, 32); NOT light default rgb(250, 245, 235) + // Dark default color is #21201f = rgb(33, 32, 31); NOT light default rgb(250, 245, 235) expect(bg).not.toBe('rgb(250, 245, 235)'); }); }); diff --git a/tests/e2e/public-sharing.spec.ts b/tests/e2e/public-sharing.spec.ts index 2768984..5ba69c1 100644 --- a/tests/e2e/public-sharing.spec.ts +++ b/tests/e2e/public-sharing.spec.ts @@ -139,7 +139,7 @@ test.describe('Public Note Sharing', () => { await expect(publicPage.locator('html')).toHaveAttribute('data-theme', 'dark'); await expect(publicPage.getByTestId('shared-note')).toHaveCSS( 'background-color', - 'rgb(34, 46, 58)' + 'rgb(37, 57, 70)' ); await context.close();