Files
cinny/src/app/utils/accentColor.test.ts
T
jaredandClaude Opus 4.8 c1efa7b94e
CI / Build & Quality Checks (push) Successful in 10m53s
CI / Trigger Desktop Build (push) Successful in 8s
feat(accent): custom accent themes links, text selection, and focus rings
The accent previously only overrode the folds Primary.* family; links kept the
hardcoded --tc-link blue, ::selection was browser-default, and focus rings were
neutral grey (Other.FocusRing). Now all three derive from the chosen base color:
- --tc-link → accent hex (messages, topics, URL previews)
- ::selection via an injected <style id="lotus-accent-style"> (accent bg +
  WCAG-contrasting text)
- Other.FocusRing → rgba(accent, 0.5)

Deliberately NOT recolored: Secondary.* (doubles as the neutral text/button/
badge palette), Success.* + mention pills (semantic mention/notification green),
scrollbar thumbs (folds styles them per-component; a global rule would only
half-apply). removeCustomAccent() clears everything — no residue when switching
off or to the TDS theme. +2 unit tests (561 total).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:44:26 -04:00

90 lines
3.7 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
hexToRgb,
lighten,
darken,
rgba,
relativeLuminance,
contrastingText,
varNameFromToken,
derivePrimaryPalette,
deriveAccentExtras,
buildAccentCss,
} from './accentColor';
test('hexToRgb parses 6-digit hex (with/without #, trimmed)', () => {
assert.deepEqual(hexToRgb('#ff8800'), { r: 255, g: 136, b: 0 });
assert.deepEqual(hexToRgb('ff8800'), { r: 255, g: 136, b: 0 });
assert.deepEqual(hexToRgb(' #FF8800 '), { r: 255, g: 136, b: 0 });
assert.equal(hexToRgb('#fff'), undefined); // 3-digit not supported
assert.equal(hexToRgb('nope'), undefined);
});
test('lighten moves channels toward white', () => {
assert.deepEqual(lighten({ r: 255, g: 0, b: 0 }, 0.5), { r: 255, g: 127.5, b: 127.5 });
assert.deepEqual(lighten({ r: 0, g: 0, b: 0 }, 1), { r: 255, g: 255, b: 255 });
assert.deepEqual(lighten({ r: 10, g: 20, b: 30 }, 0), { r: 10, g: 20, b: 30 });
});
test('darken moves channels toward black', () => {
assert.deepEqual(darken({ r: 200, g: 100, b: 50 }, 0.5), { r: 100, g: 50, b: 25 });
assert.deepEqual(darken({ r: 255, g: 255, b: 255 }, 1), { r: 0, g: 0, b: 0 });
assert.deepEqual(darken({ r: 10, g: 20, b: 30 }, 0), { r: 10, g: 20, b: 30 });
});
test('rgba formats and clamps channels', () => {
assert.equal(rgba({ r: 255, g: 136, b: 0 }, 0.5), 'rgba(255, 136, 0, 0.5)');
assert.equal(rgba({ r: 300, g: -5, b: 128 }, 1), 'rgba(255, 0, 128, 1)');
});
test('relativeLuminance: black is 0, white is 1', () => {
assert.ok(Math.abs(relativeLuminance({ r: 0, g: 0, b: 0 })) < 1e-9);
assert.ok(Math.abs(relativeLuminance({ r: 255, g: 255, b: 255 }) - 1) < 1e-9);
// green contributes more than blue (per WCAG coefficients)
assert.ok(relativeLuminance({ r: 0, g: 255, b: 0 }) > relativeLuminance({ r: 0, g: 0, b: 255 }));
});
test('contrastingText picks black on light, white on dark', () => {
assert.equal(contrastingText({ r: 255, g: 255, b: 255 }), '#000');
assert.equal(contrastingText({ r: 0, g: 0, b: 0 }), '#fff');
});
test('varNameFromToken extracts the CSS var name', () => {
assert.equal(varNameFromToken('var(--oq6d07f)'), '--oq6d07f');
assert.equal(varNameFromToken('--bare'), undefined);
assert.equal(varNameFromToken('not a token'), undefined);
});
test('derivePrimaryPalette produces the full Primary token set', () => {
const palette = derivePrimaryPalette({ r: 255, g: 136, b: 0 });
assert.equal(Object.keys(palette).length, 10);
assert.equal(palette.Main, '#ff8800');
assert.equal(palette.MainLine, '#ff8800');
assert.equal(palette.Container, 'rgba(255, 136, 0, 0.12)');
assert.equal(palette.ContainerLine, 'rgba(255, 136, 0, 0.4)');
assert.equal(palette.OnMain, contrastingText({ r: 255, g: 136, b: 0 }));
// hover/active are valid 6-digit hex strings
assert.match(palette.MainHover, /^#[0-9a-f]{6}$/);
assert.match(palette.MainActive, /^#[0-9a-f]{6}$/);
});
test('deriveAccentExtras derives focus ring, link and selection from one base', () => {
const base = { r: 255, g: 136, b: 0 };
const extras = deriveAccentExtras(base);
// focus ring keeps the translucent character in the accent hue
assert.equal(extras.focusRing, 'rgba(255, 136, 0, 0.5)');
// link + selection background are the solid base hex
assert.equal(extras.link, '#ff8800');
assert.equal(extras.selectionBg, '#ff8800');
// selection text is WCAG-aware contrasting text over the base
assert.equal(extras.selectionText, contrastingText(base));
});
test('buildAccentCss emits selection rules using the derived palette', () => {
const base = { r: 0, g: 0, b: 0 };
const css = buildAccentCss(base);
assert.match(css, /::selection\{background:#000000;color:#fff;\}/);
assert.match(css, /::-moz-selection\{background:#000000;color:#fff;\}/);
});