Extends ticket 048's light_green pattern (light-inverted surface/border/text scale + light's neutral danger colours, same base and accent hues as the dark-surfaced counterpart) to the remaining five colour schemes across all six files that must stay in sync: index.css, themes.ts, api.ts, settings.rs, useSettingsPanel.ts, and NavMenu.tsx. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
413 lines
15 KiB
TypeScript
413 lines
15 KiB
TypeScript
// A user-supplied style is any CSS file that defines every variable in `THEME_CSS_VARS` somewhere in
|
|
// a `:root { --c-*: ...; }` block (ticket 020/047: styles are selectable on equal footing with the
|
|
// built-ins, and there's no requirement that this block be the *only* thing the file contains — a
|
|
// style is free to also target any other selector, e.g. to restyle layout, spacing, or fonts, not
|
|
// just the palette). `useSettingsPanel` applies the selected one as an unscoped stylesheet only
|
|
// while no built-in `html.<scheme>` class is present, so it wins the cascade without rewriting the
|
|
// selector.
|
|
|
|
/** Canonical list of every `--c-*` variable every theme in `index.css` defines. */
|
|
export const THEME_CSS_VARS = [
|
|
'--c-surface-0',
|
|
'--c-surface-1',
|
|
'--c-surface-2',
|
|
'--c-border',
|
|
'--c-border-soft',
|
|
'--c-border-faint',
|
|
'--c-border-strong',
|
|
'--c-text',
|
|
'--c-text-secondary',
|
|
'--c-text-muted',
|
|
'--c-text-subtle',
|
|
'--c-text-faint',
|
|
'--c-text-faintest',
|
|
'--c-danger-bg',
|
|
'--c-danger-border',
|
|
'--c-danger-text',
|
|
'--c-danger-text-strong',
|
|
'--c-accent',
|
|
'--c-accent-soft',
|
|
'--c-accent-text',
|
|
'--c-accent-2',
|
|
'--c-accent-3',
|
|
] as const
|
|
|
|
export type ThemeVars = Record<(typeof THEME_CSS_VARS)[number], string>
|
|
|
|
// Fully-resolved variable values for each built-in theme, including inherited fallbacks, so a downloaded file is always complete; keep in sync with `index.css` by hand, there's no build step linking the two.
|
|
export const BUILTIN_THEMES: Record<string, ThemeVars> = {
|
|
dark: {
|
|
'--c-surface-0': '#0a0a0a',
|
|
'--c-surface-1': '#171717',
|
|
'--c-surface-2': '#262626',
|
|
'--c-border': '#262626',
|
|
'--c-border-soft': '#404040',
|
|
'--c-border-faint': '#171717',
|
|
'--c-border-strong': '#a3a3a3',
|
|
'--c-text': '#f5f5f5',
|
|
'--c-text-secondary': '#d4d4d4',
|
|
'--c-text-muted': '#a3a3a3',
|
|
'--c-text-subtle': '#737373',
|
|
'--c-text-faint': '#525252',
|
|
'--c-text-faintest': '#404040',
|
|
'--c-danger-bg': '#450a0a',
|
|
'--c-danger-border': '#7f1d1d',
|
|
'--c-danger-text': '#f87171',
|
|
'--c-danger-text-strong': '#fca5a5',
|
|
'--c-accent': '#0ea5e9',
|
|
'--c-accent-soft': '#0369a1',
|
|
'--c-accent-text': '#7dd3fc',
|
|
'--c-accent-2': '#a78bfa',
|
|
'--c-accent-3': '#4ade80',
|
|
},
|
|
light: {
|
|
'--c-surface-0': '#fafafa',
|
|
'--c-surface-1': '#f5f5f5',
|
|
'--c-surface-2': '#e5e5e5',
|
|
'--c-border': '#e5e5e5',
|
|
'--c-border-soft': '#d4d4d4',
|
|
'--c-border-faint': '#f5f5f5',
|
|
'--c-border-strong': '#525252',
|
|
'--c-text': '#171717',
|
|
'--c-text-secondary': '#404040',
|
|
'--c-text-muted': '#525252',
|
|
'--c-text-subtle': '#737373',
|
|
'--c-text-faint': '#a3a3a3',
|
|
'--c-text-faintest': '#d4d4d4',
|
|
'--c-danger-bg': '#fee2e2',
|
|
'--c-danger-border': '#fca5a5',
|
|
'--c-danger-text': '#b91c1c',
|
|
'--c-danger-text-strong': '#991b1b',
|
|
'--c-accent': '#0284c7',
|
|
'--c-accent-soft': '#7dd3fc',
|
|
'--c-accent-text': '#0369a1',
|
|
'--c-accent-2': '#7c3aed',
|
|
'--c-accent-3': '#16a34a',
|
|
},
|
|
red: {
|
|
'--c-surface-0': 'hsl(0 32% 10%)',
|
|
'--c-surface-1': 'hsl(0 28% 16%)',
|
|
'--c-surface-2': 'hsl(0 26% 22%)',
|
|
'--c-border': 'hsl(0 24% 24%)',
|
|
'--c-border-soft': 'hsl(0 22% 33%)',
|
|
'--c-border-faint': 'hsl(0 28% 16%)',
|
|
'--c-border-strong': 'hsl(0 15% 62%)',
|
|
'--c-text': 'hsl(0 15% 94%)',
|
|
'--c-text-secondary': 'hsl(0 12% 82%)',
|
|
'--c-text-muted': 'hsl(0 10% 65%)',
|
|
'--c-text-subtle': 'hsl(0 8% 50%)',
|
|
'--c-text-faint': 'hsl(0 8% 38%)',
|
|
'--c-text-faintest': 'hsl(0 10% 28%)',
|
|
'--c-danger-bg': '#450a0a',
|
|
'--c-danger-border': '#7f1d1d',
|
|
'--c-danger-text': '#f87171',
|
|
'--c-danger-text-strong': '#fca5a5',
|
|
'--c-accent': 'hsl(180 70% 62%)',
|
|
'--c-accent-soft': 'hsl(150 55% 34%)',
|
|
'--c-accent-text': 'hsl(180 75% 80%)',
|
|
'--c-accent-2': 'hsl(90 65% 58%)',
|
|
'--c-accent-3': 'hsl(270 65% 68%)',
|
|
},
|
|
green: {
|
|
'--c-surface-0': 'hsl(140 32% 10%)',
|
|
'--c-surface-1': 'hsl(140 28% 16%)',
|
|
'--c-surface-2': 'hsl(140 26% 22%)',
|
|
'--c-border': 'hsl(140 24% 24%)',
|
|
'--c-border-soft': 'hsl(140 22% 33%)',
|
|
'--c-border-faint': 'hsl(140 28% 16%)',
|
|
'--c-border-strong': 'hsl(140 15% 62%)',
|
|
'--c-text': 'hsl(140 15% 94%)',
|
|
'--c-text-secondary': 'hsl(140 12% 82%)',
|
|
'--c-text-muted': 'hsl(140 10% 65%)',
|
|
'--c-text-subtle': 'hsl(140 8% 50%)',
|
|
'--c-text-faint': 'hsl(140 8% 38%)',
|
|
'--c-text-faintest': 'hsl(140 10% 28%)',
|
|
'--c-danger-bg': '#450a0a',
|
|
'--c-danger-border': '#7f1d1d',
|
|
'--c-danger-text': '#f87171',
|
|
'--c-danger-text-strong': '#fca5a5',
|
|
'--c-accent': 'hsl(320 70% 62%)',
|
|
'--c-accent-soft': 'hsl(290 55% 34%)',
|
|
'--c-accent-text': 'hsl(320 75% 80%)',
|
|
'--c-accent-2': 'hsl(230 65% 62%)',
|
|
'--c-accent-3': 'hsl(50 65% 58%)',
|
|
},
|
|
blue: {
|
|
'--c-surface-0': 'hsl(210 32% 10%)',
|
|
'--c-surface-1': 'hsl(210 28% 16%)',
|
|
'--c-surface-2': 'hsl(210 26% 22%)',
|
|
'--c-border': 'hsl(210 24% 24%)',
|
|
'--c-border-soft': 'hsl(210 22% 33%)',
|
|
'--c-border-faint': 'hsl(210 28% 16%)',
|
|
'--c-border-strong': 'hsl(210 15% 62%)',
|
|
'--c-text': 'hsl(210 15% 94%)',
|
|
'--c-text-secondary': 'hsl(210 12% 82%)',
|
|
'--c-text-muted': 'hsl(210 10% 65%)',
|
|
'--c-text-subtle': 'hsl(210 8% 50%)',
|
|
'--c-text-faint': 'hsl(210 8% 38%)',
|
|
'--c-text-faintest': 'hsl(210 10% 28%)',
|
|
'--c-danger-bg': '#450a0a',
|
|
'--c-danger-border': '#7f1d1d',
|
|
'--c-danger-text': '#f87171',
|
|
'--c-danger-text-strong': '#fca5a5',
|
|
'--c-accent': 'hsl(30 70% 62%)',
|
|
'--c-accent-soft': 'hsl(0 55% 34%)',
|
|
'--c-accent-text': 'hsl(30 75% 80%)',
|
|
'--c-accent-2': 'hsl(300 65% 68%)',
|
|
'--c-accent-3': 'hsl(120 65% 55%)',
|
|
},
|
|
yellow: {
|
|
'--c-surface-0': 'hsl(50 32% 10%)',
|
|
'--c-surface-1': 'hsl(50 28% 16%)',
|
|
'--c-surface-2': 'hsl(50 26% 22%)',
|
|
'--c-border': 'hsl(50 24% 24%)',
|
|
'--c-border-soft': 'hsl(50 22% 33%)',
|
|
'--c-border-faint': 'hsl(50 28% 16%)',
|
|
'--c-border-strong': 'hsl(50 15% 62%)',
|
|
'--c-text': 'hsl(50 15% 94%)',
|
|
'--c-text-secondary': 'hsl(50 12% 82%)',
|
|
'--c-text-muted': 'hsl(50 10% 65%)',
|
|
'--c-text-subtle': 'hsl(50 8% 50%)',
|
|
'--c-text-faint': 'hsl(50 8% 38%)',
|
|
'--c-text-faintest': 'hsl(50 10% 28%)',
|
|
'--c-danger-bg': '#450a0a',
|
|
'--c-danger-border': '#7f1d1d',
|
|
'--c-danger-text': '#f87171',
|
|
'--c-danger-text-strong': '#fca5a5',
|
|
'--c-accent': 'hsl(230 70% 62%)',
|
|
'--c-accent-soft': 'hsl(200 55% 34%)',
|
|
'--c-accent-text': 'hsl(230 75% 80%)',
|
|
'--c-accent-2': 'hsl(140 65% 55%)',
|
|
'--c-accent-3': 'hsl(320 65% 68%)',
|
|
},
|
|
pink: {
|
|
'--c-surface-0': 'hsl(330 32% 10%)',
|
|
'--c-surface-1': 'hsl(330 28% 16%)',
|
|
'--c-surface-2': 'hsl(330 26% 22%)',
|
|
'--c-border': 'hsl(330 24% 24%)',
|
|
'--c-border-soft': 'hsl(330 22% 33%)',
|
|
'--c-border-faint': 'hsl(330 28% 16%)',
|
|
'--c-border-strong': 'hsl(330 15% 62%)',
|
|
'--c-text': 'hsl(330 15% 94%)',
|
|
'--c-text-secondary': 'hsl(330 12% 82%)',
|
|
'--c-text-muted': 'hsl(330 10% 65%)',
|
|
'--c-text-subtle': 'hsl(330 8% 50%)',
|
|
'--c-text-faint': 'hsl(330 8% 38%)',
|
|
'--c-text-faintest': 'hsl(330 10% 28%)',
|
|
'--c-danger-bg': '#450a0a',
|
|
'--c-danger-border': '#7f1d1d',
|
|
'--c-danger-text': '#f87171',
|
|
'--c-danger-text-strong': '#fca5a5',
|
|
'--c-accent': 'hsl(150 70% 62%)',
|
|
'--c-accent-soft': 'hsl(120 55% 34%)',
|
|
'--c-accent-text': 'hsl(150 75% 80%)',
|
|
'--c-accent-2': 'hsl(60 65% 58%)',
|
|
'--c-accent-3': 'hsl(240 65% 68%)',
|
|
},
|
|
orange: {
|
|
'--c-surface-0': 'hsl(30 32% 10%)',
|
|
'--c-surface-1': 'hsl(30 28% 16%)',
|
|
'--c-surface-2': 'hsl(30 26% 22%)',
|
|
'--c-border': 'hsl(30 24% 24%)',
|
|
'--c-border-soft': 'hsl(30 22% 33%)',
|
|
'--c-border-faint': 'hsl(30 28% 16%)',
|
|
'--c-border-strong': 'hsl(30 15% 62%)',
|
|
'--c-text': 'hsl(30 15% 94%)',
|
|
'--c-text-secondary': 'hsl(30 12% 82%)',
|
|
'--c-text-muted': 'hsl(30 10% 65%)',
|
|
'--c-text-subtle': 'hsl(30 8% 50%)',
|
|
'--c-text-faint': 'hsl(30 8% 38%)',
|
|
'--c-text-faintest': 'hsl(30 10% 28%)',
|
|
'--c-danger-bg': '#450a0a',
|
|
'--c-danger-border': '#7f1d1d',
|
|
'--c-danger-text': '#f87171',
|
|
'--c-danger-text-strong': '#fca5a5',
|
|
'--c-accent': 'hsl(210 70% 62%)',
|
|
'--c-accent-soft': 'hsl(180 55% 34%)',
|
|
'--c-accent-text': 'hsl(210 75% 80%)',
|
|
'--c-accent-2': 'hsl(120 65% 55%)',
|
|
'--c-accent-3': 'hsl(300 65% 68%)',
|
|
},
|
|
light_green: {
|
|
'--c-surface-0': 'hsl(140 40% 96%)',
|
|
'--c-surface-1': 'hsl(140 35% 92%)',
|
|
'--c-surface-2': 'hsl(140 30% 85%)',
|
|
'--c-border': 'hsl(140 25% 80%)',
|
|
'--c-border-soft': 'hsl(140 22% 70%)',
|
|
'--c-border-faint': 'hsl(140 35% 92%)',
|
|
'--c-border-strong': 'hsl(140 20% 40%)',
|
|
'--c-text': 'hsl(140 30% 12%)',
|
|
'--c-text-secondary': 'hsl(140 20% 25%)',
|
|
'--c-text-muted': 'hsl(140 15% 40%)',
|
|
'--c-text-subtle': 'hsl(140 10% 50%)',
|
|
'--c-text-faint': 'hsl(140 10% 65%)',
|
|
'--c-text-faintest': 'hsl(140 12% 80%)',
|
|
'--c-danger-bg': '#fee2e2',
|
|
'--c-danger-border': '#fca5a5',
|
|
'--c-danger-text': '#b91c1c',
|
|
'--c-danger-text-strong': '#991b1b',
|
|
'--c-accent': 'hsl(320 70% 40%)',
|
|
'--c-accent-soft': 'hsl(290 60% 85%)',
|
|
'--c-accent-text': 'hsl(320 75% 28%)',
|
|
'--c-accent-2': 'hsl(230 70% 45%)',
|
|
'--c-accent-3': 'hsl(50 70% 38%)',
|
|
},
|
|
light_red: {
|
|
'--c-surface-0': 'hsl(0 40% 96%)',
|
|
'--c-surface-1': 'hsl(0 35% 92%)',
|
|
'--c-surface-2': 'hsl(0 30% 85%)',
|
|
'--c-border': 'hsl(0 25% 80%)',
|
|
'--c-border-soft': 'hsl(0 22% 70%)',
|
|
'--c-border-faint': 'hsl(0 35% 92%)',
|
|
'--c-border-strong': 'hsl(0 20% 40%)',
|
|
'--c-text': 'hsl(0 30% 12%)',
|
|
'--c-text-secondary': 'hsl(0 20% 25%)',
|
|
'--c-text-muted': 'hsl(0 15% 40%)',
|
|
'--c-text-subtle': 'hsl(0 10% 50%)',
|
|
'--c-text-faint': 'hsl(0 10% 65%)',
|
|
'--c-text-faintest': 'hsl(0 12% 80%)',
|
|
'--c-danger-bg': '#fee2e2',
|
|
'--c-danger-border': '#fca5a5',
|
|
'--c-danger-text': '#b91c1c',
|
|
'--c-danger-text-strong': '#991b1b',
|
|
'--c-accent': 'hsl(180 70% 40%)',
|
|
'--c-accent-soft': 'hsl(150 60% 85%)',
|
|
'--c-accent-text': 'hsl(180 75% 28%)',
|
|
'--c-accent-2': 'hsl(90 70% 45%)',
|
|
'--c-accent-3': 'hsl(270 70% 38%)',
|
|
},
|
|
light_blue: {
|
|
'--c-surface-0': 'hsl(210 40% 96%)',
|
|
'--c-surface-1': 'hsl(210 35% 92%)',
|
|
'--c-surface-2': 'hsl(210 30% 85%)',
|
|
'--c-border': 'hsl(210 25% 80%)',
|
|
'--c-border-soft': 'hsl(210 22% 70%)',
|
|
'--c-border-faint': 'hsl(210 35% 92%)',
|
|
'--c-border-strong': 'hsl(210 20% 40%)',
|
|
'--c-text': 'hsl(210 30% 12%)',
|
|
'--c-text-secondary': 'hsl(210 20% 25%)',
|
|
'--c-text-muted': 'hsl(210 15% 40%)',
|
|
'--c-text-subtle': 'hsl(210 10% 50%)',
|
|
'--c-text-faint': 'hsl(210 10% 65%)',
|
|
'--c-text-faintest': 'hsl(210 12% 80%)',
|
|
'--c-danger-bg': '#fee2e2',
|
|
'--c-danger-border': '#fca5a5',
|
|
'--c-danger-text': '#b91c1c',
|
|
'--c-danger-text-strong': '#991b1b',
|
|
'--c-accent': 'hsl(30 70% 40%)',
|
|
'--c-accent-soft': 'hsl(0 60% 85%)',
|
|
'--c-accent-text': 'hsl(30 75% 28%)',
|
|
'--c-accent-2': 'hsl(300 70% 45%)',
|
|
'--c-accent-3': 'hsl(120 70% 38%)',
|
|
},
|
|
light_yellow: {
|
|
'--c-surface-0': 'hsl(50 40% 96%)',
|
|
'--c-surface-1': 'hsl(50 35% 92%)',
|
|
'--c-surface-2': 'hsl(50 30% 85%)',
|
|
'--c-border': 'hsl(50 25% 80%)',
|
|
'--c-border-soft': 'hsl(50 22% 70%)',
|
|
'--c-border-faint': 'hsl(50 35% 92%)',
|
|
'--c-border-strong': 'hsl(50 20% 40%)',
|
|
'--c-text': 'hsl(50 30% 12%)',
|
|
'--c-text-secondary': 'hsl(50 20% 25%)',
|
|
'--c-text-muted': 'hsl(50 15% 40%)',
|
|
'--c-text-subtle': 'hsl(50 10% 50%)',
|
|
'--c-text-faint': 'hsl(50 10% 65%)',
|
|
'--c-text-faintest': 'hsl(50 12% 80%)',
|
|
'--c-danger-bg': '#fee2e2',
|
|
'--c-danger-border': '#fca5a5',
|
|
'--c-danger-text': '#b91c1c',
|
|
'--c-danger-text-strong': '#991b1b',
|
|
'--c-accent': 'hsl(230 70% 40%)',
|
|
'--c-accent-soft': 'hsl(200 60% 85%)',
|
|
'--c-accent-text': 'hsl(230 75% 28%)',
|
|
'--c-accent-2': 'hsl(140 70% 45%)',
|
|
'--c-accent-3': 'hsl(320 70% 38%)',
|
|
},
|
|
light_pink: {
|
|
'--c-surface-0': 'hsl(330 40% 96%)',
|
|
'--c-surface-1': 'hsl(330 35% 92%)',
|
|
'--c-surface-2': 'hsl(330 30% 85%)',
|
|
'--c-border': 'hsl(330 25% 80%)',
|
|
'--c-border-soft': 'hsl(330 22% 70%)',
|
|
'--c-border-faint': 'hsl(330 35% 92%)',
|
|
'--c-border-strong': 'hsl(330 20% 40%)',
|
|
'--c-text': 'hsl(330 30% 12%)',
|
|
'--c-text-secondary': 'hsl(330 20% 25%)',
|
|
'--c-text-muted': 'hsl(330 15% 40%)',
|
|
'--c-text-subtle': 'hsl(330 10% 50%)',
|
|
'--c-text-faint': 'hsl(330 10% 65%)',
|
|
'--c-text-faintest': 'hsl(330 12% 80%)',
|
|
'--c-danger-bg': '#fee2e2',
|
|
'--c-danger-border': '#fca5a5',
|
|
'--c-danger-text': '#b91c1c',
|
|
'--c-danger-text-strong': '#991b1b',
|
|
'--c-accent': 'hsl(150 70% 40%)',
|
|
'--c-accent-soft': 'hsl(120 60% 85%)',
|
|
'--c-accent-text': 'hsl(150 75% 28%)',
|
|
'--c-accent-2': 'hsl(60 70% 45%)',
|
|
'--c-accent-3': 'hsl(240 70% 38%)',
|
|
},
|
|
light_orange: {
|
|
'--c-surface-0': 'hsl(30 40% 96%)',
|
|
'--c-surface-1': 'hsl(30 35% 92%)',
|
|
'--c-surface-2': 'hsl(30 30% 85%)',
|
|
'--c-border': 'hsl(30 25% 80%)',
|
|
'--c-border-soft': 'hsl(30 22% 70%)',
|
|
'--c-border-faint': 'hsl(30 35% 92%)',
|
|
'--c-border-strong': 'hsl(30 20% 40%)',
|
|
'--c-text': 'hsl(30 30% 12%)',
|
|
'--c-text-secondary': 'hsl(30 20% 25%)',
|
|
'--c-text-muted': 'hsl(30 15% 40%)',
|
|
'--c-text-subtle': 'hsl(30 10% 50%)',
|
|
'--c-text-faint': 'hsl(30 10% 65%)',
|
|
'--c-text-faintest': 'hsl(30 12% 80%)',
|
|
'--c-danger-bg': '#fee2e2',
|
|
'--c-danger-border': '#fca5a5',
|
|
'--c-danger-text': '#b91c1c',
|
|
'--c-danger-text-strong': '#991b1b',
|
|
'--c-accent': 'hsl(210 70% 40%)',
|
|
'--c-accent-soft': 'hsl(180 60% 85%)',
|
|
'--c-accent-text': 'hsl(210 75% 28%)',
|
|
'--c-accent-2': 'hsl(120 70% 45%)',
|
|
'--c-accent-3': 'hsl(300 70% 38%)',
|
|
},
|
|
}
|
|
|
|
export function generateThemeCss(vars: ThemeVars): string {
|
|
const lines = THEME_CSS_VARS.map((name) => ` ${name}: ${vars[name]};`)
|
|
return `:root {\n${lines.join('\n')}\n}\n`
|
|
}
|
|
|
|
export interface ParsedTheme {
|
|
vars: ThemeVars
|
|
missing: string[]
|
|
}
|
|
|
|
// Parses `--c-*: value;` declarations from the first `:root { ... }` block (or the whole file, tolerant of a dropped selector); reports missing canonical variables so callers can reject an incomplete upload.
|
|
export function parseThemeCss(content: string): ParsedTheme {
|
|
const rootMatch = content.match(/:root\s*\{([^}]*)\}/)
|
|
const body = rootMatch ? rootMatch[1] : content
|
|
const found: Record<string, string> = {}
|
|
const declRegex = /(--c-[a-z0-9-]+)\s*:\s*([^;]+);/gi
|
|
let match: RegExpExecArray | null
|
|
while ((match = declRegex.exec(body))) {
|
|
found[match[1]] = match[2].trim()
|
|
}
|
|
const missing = THEME_CSS_VARS.filter((name) => !(name in found))
|
|
const vars = Object.fromEntries(THEME_CSS_VARS.map((name) => [name, found[name] ?? ''])) as ThemeVars
|
|
return { vars, missing }
|
|
}
|
|
|
|
export function downloadThemeFile(filename: string, content: string) {
|
|
const blob = new Blob([content], { type: 'text/css' })
|
|
const url = URL.createObjectURL(blob)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = filename
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
link.remove()
|
|
URL.revokeObjectURL(url)
|
|
}
|