feat(theme): rebuild dark note colours as a consistent family

This commit is contained in:
Frederic Cabassut
2026-07-31 12:03:39 +02:00
committed by Fred Cabassut
parent 365f941240
commit 9600c62288
4 changed files with 57 additions and 17 deletions
+43 -3
View File
@@ -8,13 +8,13 @@ describe('getNoteColor', () => {
}); });
it('returns dark color when isDark is true', () => { it('returns dark color when isDark is true', () => {
expect(getNoteColor('default', true)).toBe('#2a2520'); expect(getNoteColor('default', true)).toBe('#21201f');
expect(getNoteColor('coral', true)).toBe('#4a2522'); expect(getNoteColor('coral', true)).toBe('#3f1f1c');
}); });
it('returns default color for unknown color name', () => { it('returns default color for unknown color name', () => {
expect(getNoteColor('unknown' as any, false)).toBe('#faf5eb'); 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', () => { it('has matching keys in light and dark color maps', () => {
@@ -23,3 +23,43 @@ describe('getNoteColor', () => {
expect(lightKeys).toEqual(darkKeys); 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);
});
});
+12 -12
View File
@@ -16,18 +16,18 @@ export const NOTE_COLORS: Record<NoteColor, { bg: string; label: string }> = {
}; };
export const NOTE_COLORS_DARK: Record<NoteColor, { bg: string; label: string }> = { export const NOTE_COLORS_DARK: Record<NoteColor, { bg: string; label: string }> = {
default: { bg: '#2a2520', label: 'Default' }, default: { bg: '#21201f', label: 'Default' },
coral: { bg: '#4a2522', label: 'Coral' }, coral: { bg: '#3f1f1c', label: 'Coral' },
peach: { bg: '#4a3020', label: 'Peach' }, peach: { bg: '#3f2b1c', label: 'Peach' },
sand: { bg: '#3a3520', label: 'Sand' }, sand: { bg: '#3c371b', label: 'Sand' },
mint: { bg: '#2a3a22', label: 'Mint' }, mint: { bg: '#2a3d1d', label: 'Mint' },
sage: { bg: '#223a32', label: 'Sage' }, sage: { bg: '#1f3d36', label: 'Sage' },
fog: { bg: '#222e3a', label: 'Fog' }, fog: { bg: '#253946', label: 'Fog' },
storm: { bg: '#1e2a35', label: 'Storm' }, storm: { bg: '#1d283d', label: 'Storm' },
dusk: { bg: '#352540', label: 'Dusk' }, dusk: { bg: '#3d2843', label: 'Dusk' },
blossom: { bg: '#3a2830', label: 'Blossom' }, blossom: { bg: '#3d2129', label: 'Blossom' },
clay: { bg: '#302e28', label: 'Clay' }, clay: { bg: '#363126', label: 'Clay' },
chalk: { bg: '#2e2e30', label: 'Chalk' } chalk: { bg: '#303036', label: 'Chalk' }
}; };
export function getNoteColor(color: NoteColor, isDark: boolean): string { export function getNoteColor(color: NoteColor, isDark: boolean): string {
+1 -1
View File
@@ -81,7 +81,7 @@ test.describe.serial('Dark mode', () => {
const noteCard = page.getByTestId('note-card').first(); const noteCard = page.getByTestId('note-card').first();
await expect(noteCard).toBeVisible(); await expect(noteCard).toBeVisible();
const bg = await noteCard.evaluate((el) => getComputedStyle(el).backgroundColor); 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)'); expect(bg).not.toBe('rgb(250, 245, 235)');
}); });
}); });
+1 -1
View File
@@ -139,7 +139,7 @@ test.describe('Public Note Sharing', () => {
await expect(publicPage.locator('html')).toHaveAttribute('data-theme', 'dark'); await expect(publicPage.locator('html')).toHaveAttribute('data-theme', 'dark');
await expect(publicPage.getByTestId('shared-note')).toHaveCSS( await expect(publicPage.getByTestId('shared-note')).toHaveCSS(
'background-color', 'background-color',
'rgb(34, 46, 58)' 'rgb(37, 57, 70)'
); );
await context.close(); await context.close();