Files
mumh5/scripts/gen-themes.mjs
kibiandClaude Opus 5.5 2883c61965 100 color schemes, a plainer README
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>
2026-10-02 20:57:47 +02:00

171 lines
10 KiB
JavaScript

// Writes the generated color schemes: src/themes-generated.css and src/lib/themes-generated.ts.
// Each scheme is a few numbers (surface hue and saturation, accent hue and saturation); the
// shades are derived from them and nudged until text, links and accent labels are readable.
// npm run themes
import { writeFileSync } from 'node:fs';
import path from 'node:path';
const root = path.resolve(import.meta.dirname, '..');
function hex(h, s, l) {
h = ((h % 360) + 360) % 360; s = Math.max(0, Math.min(100, s)) / 100; l = Math.max(0, Math.min(100, l)) / 100;
const k = n => (n + h / 30) % 12;
const a = s * Math.min(l, 1 - l);
const f = n => Math.round(255 * (l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1))));
return '#' + [f(0), f(8), f(4)].map(v => v.toString(16).padStart(2, '0')).join('');
}
function luminance(c) {
const n = parseInt(c.slice(1), 16);
const [r, g, b] = [n >> 16, (n >> 8) & 255, n & 255].map(v => { const x = v / 255; return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; });
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
const contrast = (a, b) => { const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); return (hi + 0.05) / (lo + 0.05); };
// Moves the lightness away from the backgrounds until the color is readable on all of them
function readable(h, s, l, backgrounds, min, dir) {
let c = hex(h, s, l);
while (backgrounds.some(bg => contrast(c, bg) < min) && l > 0 && l < 100) { l += dir; c = hex(h, s, l); }
return c;
}
// [id, name, note, surface hue, surface saturation, accent hue, accent saturation, options]
const DARK = [
['slate', 'Slate', 'Blue-grey, calm', 215, 14, 215, 70],
['graphite', 'Graphite', 'Neutral grey, no color', 0, 0, 0, 0],
['charcoal', 'Charcoal', 'Warm grey, amber accents', 30, 5, 36, 85],
['obsidian', 'Obsidian', 'Near black, violet accents', 260, 10, 265, 85, { base: 3 }],
['navy', 'Navy', 'Dark blue', 222, 45, 215, 90],
['cobalt', 'Cobalt', 'Blue, yellow accents', 218, 60, 46, 95, { base: 8, link: 190 }],
['indigo', 'Indigo', 'Blue-violet', 240, 35, 245, 90],
['violet', 'Violet', 'Purple', 270, 35, 275, 85],
['orchid', 'Orchid', 'Soft purple-pink', 295, 28, 300, 70],
['magenta', 'Magenta', 'Dark pink', 320, 30, 325, 85],
['crimson', 'Crimson', 'Dark red', 350, 30, 352, 80],
['ruby', 'Ruby', 'Deep red, orange accents', 340, 40, 12, 85, { base: 5 }],
['rust', 'Rust', 'Red-brown', 15, 35, 18, 80],
['copper', 'Copper', 'Brown, muted orange', 25, 30, 28, 65],
['amber', 'Amber', 'Dark yellow-brown', 38, 30, 42, 95],
['olive', 'Olive', 'Yellow-green', 70, 22, 75, 60],
['moss', 'Moss', 'Muted green', 95, 22, 100, 50],
['emerald', 'Emerald', 'Rich green', 150, 35, 152, 70],
['jade', 'Jade', 'Blue-green', 165, 30, 168, 65],
['teal', 'Teal', 'Green-blue', 180, 35, 180, 70],
['lagoon', 'Lagoon', 'Bright turquoise accents', 192, 45, 190, 85],
['arctic', 'Arctic', 'Lighter cool grey-blue', 200, 25, 195, 75, { base: 9 }],
['storm', 'Storm', 'Lighter blue-grey', 225, 18, 200, 60, { base: 10 }],
['dusk', 'Dusk', 'Purple evening, orange accents', 255, 22, 20, 85, { base: 9 }],
['wine', 'Wine', 'Dark red-purple', 335, 35, 340, 60, { base: 5 }],
['cocoa', 'Cocoa', 'Brown', 22, 25, 30, 55, { base: 7 }],
['espresso', 'Espresso', 'Very dark brown', 28, 30, 35, 70, { base: 4 }],
['terminal', 'Terminal', 'Green on black', 120, 30, 120, 90, { base: 2, textHue: 120, textSat: 85, link: 150 }],
['phosphor', 'Phosphor', 'Amber on black', 40, 30, 40, 95, { base: 2, textHue: 40, textSat: 90, link: 25 }],
['neon', 'Neon', 'Black, pink and cyan', 280, 20, 320, 100, { base: 3, link: 185 }],
['synthwave', 'Synthwave', 'Purple, pink and cyan', 265, 45, 320, 90, { base: 8, link: 185 }],
['aurora', 'Aurora', 'Dark blue, green accents', 210, 30, 160, 75],
['abyss', 'Abyss', 'Blue-black, turquoise accents', 230, 50, 195, 90, { base: 3 }],
['carbon', 'Carbon', 'Black, red accents', 0, 0, 0, 85, { base: 3 }],
['ash', 'Ash', 'Lighter grey, blue accents', 0, 0, 210, 60, { base: 12 }],
['steel', 'Steel', 'Grey-blue, orange accents', 210, 10, 30, 80, { base: 11 }],
['ink', 'Ink', 'Blue-black, yellow accents', 235, 30, 50, 90, { base: 3 }],
['sapphire', 'Sapphire', 'Strong blue', 228, 55, 228, 95],
['azure', 'Azure', 'Bright blue accents', 208, 50, 205, 95],
['peacock', 'Peacock', 'Turquoise, purple accents', 188, 50, 300, 70],
['pine', 'Pine', 'Dark green, orange accents', 160, 40, 35, 85, { base: 4 }],
['fern', 'Fern', 'Soft green', 125, 25, 130, 55],
['lime', 'Lime', 'Yellow-green accents', 85, 30, 82, 85],
['citrus', 'Citrus', 'Dark yellow, orange accents', 60, 25, 28, 95],
['sunset', 'Sunset', 'Purple, orange accents', 280, 30, 25, 95],
['flamingo', 'Flamingo', 'Dark rose, bright pink', 330, 25, 345, 90],
['cherry', 'Cherry', 'Deep red', 355, 40, 355, 90, { base: 5 }],
['brick', 'Brick', 'Red-brown, gold accents', 8, 35, 40, 80],
['terracotta', 'Terracotta', 'Clay, teal accents', 18, 30, 175, 55, { base: 8 }],
['sandstone', 'Sandstone', 'Lighter warm grey', 35, 18, 20, 70, { base: 10 }],
['mocha', 'Mocha', 'Brown, lilac accents', 20, 18, 280, 50, { base: 9 }],
['walnut', 'Walnut', 'Dark brown, gold accents', 25, 35, 45, 80, { base: 5 }],
['bronze', 'Bronze', 'Yellow-brown', 40, 35, 38, 70],
['brass', 'Brass', 'Dark gold', 48, 30, 50, 85],
['mulberry', 'Mulberry', 'Dark purple-pink', 310, 35, 310, 65, { base: 5 }],
['grape', 'Grape', 'Strong purple', 280, 40, 280, 90],
['iris', 'Iris', 'Lighter blue-violet', 250, 30, 255, 90, { base: 9 }],
['twilight', 'Twilight', 'Night blue, pink accents', 245, 35, 330, 75, { base: 5 }],
['nebula', 'Nebula', 'Purple, turquoise accents', 270, 45, 190, 85, { base: 4 }],
['cathode', 'Cathode', 'Cyan on black', 185, 30, 185, 90, { base: 2, textHue: 185, textSat: 80, link: 160 }],
['bubblegum', 'Bubblegum', 'Purple-pink, cyan links', 300, 25, 330, 95, { base: 8, link: 185 }],
['volcano', 'Volcano', 'Black, lava orange', 10, 30, 20, 100, { base: 3 }],
['glacier', 'Glacier', 'The lightest dark, cool', 205, 30, 190, 70, { base: 12 }],
['seaweed', 'Seaweed', 'Dark teal, green accents', 170, 35, 90, 60, { base: 4 }]
];
const LIGHT = [
['snow', 'Snow', 'Cool white', 220, 10, 225, 75],
['linen', 'Linen', 'Warm white', 40, 30, 25, 60],
['sky', 'Sky', 'Light blue', 205, 60, 210, 85],
['mint', 'Mint', 'Light green', 150, 40, 160, 70],
['lavender', 'Lavender', 'Light purple', 265, 45, 265, 60],
['blush', 'Blush', 'Light pink', 345, 50, 345, 65],
['peach', 'Peach', 'Light orange', 25, 65, 18, 75],
['lemon', 'Lemon', 'Light yellow', 55, 60, 45, 90],
['sage', 'Sage', 'Grey-green', 100, 20, 110, 35],
['latte', 'Latte', 'Milky brown', 30, 35, 25, 45],
['ice', 'Ice', 'Pale turquoise', 190, 45, 195, 80],
['cloud', 'Cloud', 'Soft blue-grey', 210, 25, 210, 60],
['pearl', 'Pearl', 'Near white, rose accents', 300, 8, 320, 45],
['cream', 'Cream', 'Soft yellow', 48, 55, 30, 70],
['sand', 'Sand', 'Beige', 38, 40, 20, 55],
['rose', 'Rose', 'Pink-red', 355, 55, 350, 70],
['coral', 'Coral', 'Light red-orange', 12, 65, 8, 75],
['apricot', 'Apricot', 'Warm orange', 32, 70, 28, 85],
['pistachio', 'Pistachio', 'Yellow-green', 95, 35, 110, 50],
['seafoam', 'Seafoam', 'Green-turquoise', 165, 40, 172, 75],
['aqua', 'Aqua', 'Turquoise', 182, 50, 186, 85],
['periwinkle', 'Periwinkle', 'Blue-violet', 232, 55, 235, 70],
['lilac', 'Lilac', 'Soft purple', 285, 40, 285, 55],
['petal', 'Petal', 'Purple-pink', 320, 45, 320, 60],
['fog', 'Fog', 'Grey with a little blue', 200, 8, 200, 25]
];
function dark([id, name, note, h, s, a, as, o = {}]) {
const b = o.base ?? 6;
const bg = [b, b + 3, b + 5.5, b + 10].map(l => hex(h, s, l));
const th = o.textHue ?? h, ts = o.textSat ?? Math.min(s, 20);
const accent = hex(a, as, as ? 62 : 80);
const v = {
'--bg-0': bg[0], '--bg-1': bg[1], '--bg-2': bg[2], '--bg-3': bg[3],
'--bg-hover': hex(h, s, b + 13), '--bg-active': hex(h, s, b + 18), '--line': hex(h, s, b + 11),
'--text': readable(th, ts, o.textSat ? 70 : 91, bg, 7.5, 1),
'--text-dim': readable(th, o.textSat ? ts * 0.7 : Math.min(s, 15), o.textSat ? 50 : 66, bg, 4.6, 1),
'--text-faint': hex(th, o.textSat ? ts * 0.5 : Math.min(s, 12), 45),
'--accent': accent, '--accent-hover': hex(a, as, as ? 69 : 88),
'--accent-text': contrast('#ffffff', accent) >= contrast('#0b0b0b', accent) ? '#ffffff' : '#0b0b0b',
'--speaking': a > 90 && a < 170 ? '#f2c14e' : '#3bd16f',
'--danger': readable(4, 85, 62, bg, 4.6, 1), '--warn': '#e8b23c',
'--link': readable(o.link ?? (a > 190 && a < 250 ? 185 : 210), 85, 70, bg, 4.6, 1)
};
return { id, name, note, kind: 'dark', v };
}
function light([id, name, note, h, s, a, as]) {
const bg = [hex(h, s, 89), hex(h, s, 94.5), hex(h, s, 98)];
const accent = readable(a, as, 42, ['#ffffff'], 4.6, -1);
const v = {
'--bg-0': bg[0], '--bg-1': bg[1], '--bg-2': bg[2], '--bg-3': hex(h, s, 91),
'--bg-hover': hex(h, s, 87), '--bg-active': hex(h, s, 82), '--line': hex(h, Math.min(s, 30), 82),
'--text': readable(h, 20, 13, bg, 7.5, -1),
'--text-dim': readable(h, 12, 34, bg, 4.6, -1),
'--text-faint': hex(h, 10, 52),
'--accent': accent, '--accent-hover': readable(a, as, 34, ['#ffffff'], 6, -1), '--accent-text': '#ffffff',
'--speaking': readable(140, 65, 34, bg, 3.2, -1), '--danger': readable(4, 70, 44, bg, 4.6, -1), '--warn': readable(38, 85, 36, bg, 3.2, -1),
'--link': readable(215, 75, 40, bg, 4.6, -1)
};
return { id, name, note, kind: 'light', v };
}
const themes = [...DARK.map(dark), ...LIGHT.map(light)];
const css = '/* Generated by scripts/gen-themes.mjs (npm run themes). Do not edit by hand. */\n\n' + themes.map(t =>
`:root[data-theme="${t.id}"], [data-theme-preview="${t.id}"] {\n --rail: var(--bg-0);\n${Object.entries(t.v).map(([k, c]) => ` ${k}: ${c};`).join('\n')}\n color-scheme: ${t.kind};\n}\n`).join('\n');
writeFileSync(path.join(root, 'src/themes-generated.css'), css);
const ts = '// Generated by scripts/gen-themes.mjs (npm run themes). Do not edit by hand.\n' +
'export const GENERATED_THEMES = [\n' + themes.map(t => ` { id: '${t.id}', name: '${t.name}', note: '${t.note}', light: ${t.kind === 'light'} }`).join(',\n') + '\n] as const;\n';
writeFileSync(path.join(root, 'src/lib/themes-generated.ts'), ts);
console.log(`${themes.length} schemes written`);