mirror of
https://github.com/bretzel-app/crumbs.git
synced 2026-10-12 20:06:57 +02:00
test(theme): replace lightness-band test with pairwise distinguishability
The lightness-band guard was looser than the palette it was meant to catch
(old band 0.0175 vs new 0.0246) and the muted-AA test never failed against
the old palette (5.08:1), so both docs' stated expectations and one spec
claim ("chroma roughly doubles") were measured falsehoods. Replace the band
test with an RGB-distance pairwise check, which does discriminate (old
closest pair fog/storm = 7.5, new closest pair coral/peach = 12), fix the
misleading test comment, and correct the spec/plan docs to match reality.
This commit is contained in:
committed by
Fred Cabassut
parent
5a81d845d1
commit
14dba4f4d2
@@ -82,7 +82,10 @@ describe('dark note colours', () => {
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `pnpm test:unit colors`
|
||||
Expected: FAIL. The muted-text test fails on several colours (the old `#4a2522` family sits too close to `#b3a695`), and the lightness-band test fails because the old values wander.
|
||||
Expected: FAIL. Only the two hex-pinning assertions fail (they still pin the old values). The AA
|
||||
tests are forward guards, not regressions of the old palette — the old palette actually clears
|
||||
both AA thresholds. The distinguishability test is the one that would fail on the old palette
|
||||
(its closest pair, `fog`/`storm`, sits at RGB distance 7.5).
|
||||
|
||||
- [ ] **Step 3: Replace the dark palette**
|
||||
|
||||
|
||||
@@ -91,10 +91,13 @@ html, body { accent-color: var(--accent-check); }
|
||||
|
||||
## 2. Note card colours (`src/lib/utils/colors.ts`)
|
||||
|
||||
Replace `NOTE_COLORS_DARK`. Every entry keeps its light counterpart's hue, chroma roughly doubles
|
||||
against today's values, and lightness is held near-constant (~18%) so the twelve read as siblings —
|
||||
with blues and purples lifted ~3.5 points because they read darker than warm hues at equal HSL
|
||||
lightness.
|
||||
Replace `NOTE_COLORS_DARK`. Every entry keeps its light counterpart's hue, holds lightness
|
||||
near-constant against today's values (measured average change: +4.1 saturation points, −0.1
|
||||
lightness points), and nudges two crowded hues apart (`blossom` and `storm` shift toward pink
|
||||
and true blue respectively) so the twelve read as distinguishable siblings rather than a smudge.
|
||||
The visible improvement in dark mode comes from the base surfaces, the picker chips, the
|
||||
softened border, and the lifted gold — not from the card surfaces, which are close in chroma to
|
||||
the previous dark values.
|
||||
|
||||
```ts
|
||||
export const NOTE_COLORS_DARK: Record<NoteColor, { bg: string; label: string }> = {
|
||||
|
||||
@@ -24,10 +24,14 @@ describe('getNoteColor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function toRgb(hex: string): [number, number, number] {
|
||||
const n = parseInt(hex.slice(1), 16);
|
||||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
||||
}
|
||||
|
||||
/** 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 channels = toRgb(hex).map((v) => {
|
||||
const s = v / 255;
|
||||
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
@@ -39,6 +43,12 @@ function contrastRatio(a: string, b: string): number {
|
||||
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
||||
}
|
||||
|
||||
function rgbDistance(a: string, b: string): number {
|
||||
const [ra, ga, ba] = toRgb(a);
|
||||
const [rb, gb, bb] = toRgb(b);
|
||||
return Math.hypot(ra - rb, ga - gb, ba - bb);
|
||||
}
|
||||
|
||||
describe('dark note colours', () => {
|
||||
// Must track [data-theme="dark"] in src/app.css.
|
||||
const DARK_TEXT = '#ece3d3';
|
||||
@@ -51,15 +61,25 @@ describe('dark note colours', () => {
|
||||
});
|
||||
|
||||
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.
|
||||
// The shipped dark theme failed this at 3.77:1 with its previous muted
|
||||
// colour (#9a8e7e). This guard exists so a future palette edit cannot
|
||||
// reintroduce it — it does not discriminate the old surfaces, which were
|
||||
// darker and so scored higher against the lifted muted colour.
|
||||
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);
|
||||
it('keeps every pair of dark surfaces distinguishable', () => {
|
||||
// The old palette failed this: fog and storm sat 7.5 apart, so the twelve
|
||||
// colours read as one dark smudge. Current closest pair is coral/peach at 12.
|
||||
const entries = Object.entries(NOTE_COLORS_DARK);
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
for (let j = i + 1; j < entries.length; j++) {
|
||||
const [[nameA, a], [nameB, b]] = [entries[i], entries[j]];
|
||||
const distance = rgbDistance(a.bg, b.bg);
|
||||
expect(distance, `${nameA} vs ${nameB}`).toBeGreaterThan(10);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user