Six more schemes by hand and 89 generated from a hue and an accent (npm run themes), with a readability test over all of them and a search box in the picker. The README describes the software plainly and covers text channels, the Rooms interface and the new settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
45 lines
2.1 KiB
TypeScript
45 lines
2.1 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
// Every color scheme must stay readable: text on the surfaces it sits on, and the label on an accent button
|
|
const css = ['themes.css', 'themes-generated.css'].map(f => readFileSync(path.resolve(import.meta.dirname, '../src', f), 'utf8')).join('\n');
|
|
const settings = ['settings.svelte.ts', 'themes-generated.ts'].map(f => readFileSync(path.resolve(import.meta.dirname, '../src/lib', f), 'utf8')).join('\n');
|
|
|
|
function luminance(hex: string): number {
|
|
const n = parseInt(hex.slice(1), 16);
|
|
const [r, g, b] = [n >> 16, (n >> 8) & 255, n & 255].map(v => {
|
|
const c = v / 255;
|
|
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
});
|
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
}
|
|
const contrast = (a: string, b: string) => {
|
|
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
return (hi + 0.05) / (lo + 0.05);
|
|
};
|
|
|
|
const themes = [...css.matchAll(/\[data-theme-preview="([\w-]+)"\] \{([^}]*)\}/g)].map(m => ({
|
|
id: m[1],
|
|
vars: Object.fromEntries([...m[2].matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6});/gi)].map(v => [v[1], v[2]]))
|
|
}));
|
|
|
|
test('every scheme in the settings list has its colors, and the other way round', () => {
|
|
const listed = [...settings.matchAll(/\{ id: '([\w-]+)', name:/g)].map(m => m[1]).sort();
|
|
assert.deepEqual(themes.map(t => t.id).sort(), listed);
|
|
assert.equal(listed.length, 100);
|
|
});
|
|
|
|
for (const t of themes) {
|
|
test(`${t.id}: text is readable`, () => {
|
|
const v = t.vars;
|
|
for (const bg of ['--bg-0', '--bg-1', '--bg-2']) {
|
|
assert.ok(contrast(v['--text'], v[bg]) >= 7, `text on ${bg}: ${contrast(v['--text'], v[bg]).toFixed(1)}`);
|
|
assert.ok(contrast(v['--text-dim'], v[bg]) >= 4.5, `dim text on ${bg}: ${contrast(v['--text-dim'], v[bg]).toFixed(1)}`);
|
|
}
|
|
assert.ok(contrast(v['--accent-text'], v['--accent']) >= 3.5, `accent label: ${contrast(v['--accent-text'], v['--accent']).toFixed(1)}`);
|
|
assert.ok(contrast(v['--link'], v['--bg-2']) >= 4, `link: ${contrast(v['--link'], v['--bg-2']).toFixed(1)}`);
|
|
});
|
|
}
|