Pass over src/*.rs, webapp/src/**, and the in-app "?" docs prose: condensed multi-paragraph doc comments to single lines that state the non-obvious why, deleted comments that only restated adjacent code, and tightened the Docs.tsx help-panel wording. No logic, structure, or code formatting changed — verified via cargo test (143 passed) and tsc -b && vite build. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
267 lines
8.5 KiB
TypeScript
267 lines
8.5 KiB
TypeScript
// A theme file is a CSS file with one `:root { --c-*: ...; }` block defining every variable in `THEME_CSS_VARS`; `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',
|
|
] 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',
|
|
},
|
|
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',
|
|
},
|
|
grey: {
|
|
'--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': '#38bdf8',
|
|
'--c-accent-soft': '#0369a1',
|
|
'--c-accent-text': '#7dd3fc',
|
|
},
|
|
red: {
|
|
'--c-surface-0': '#282828',
|
|
'--c-surface-1': '#4e4e4e',
|
|
'--c-surface-2': '#5a5a5a',
|
|
'--c-border': '#5a5a5a',
|
|
'--c-border-soft': '#6a6a6a',
|
|
'--c-border-faint': '#4e4e4e',
|
|
'--c-border-strong': '#b4b4b4',
|
|
'--c-text': '#f0f0f0',
|
|
'--c-text-secondary': '#d0d0d0',
|
|
'--c-text-muted': '#b4b4b4',
|
|
'--c-text-subtle': '#9a9a9a',
|
|
'--c-text-faint': '#818181',
|
|
'--c-text-faintest': '#6a6a6a',
|
|
'--c-danger-bg': '#450a0a',
|
|
'--c-danger-border': '#7f1d1d',
|
|
'--c-danger-text': '#f87171',
|
|
'--c-danger-text-strong': '#fca5a5',
|
|
'--c-accent': '#f87171',
|
|
'--c-accent-soft': '#b91c1c',
|
|
'--c-accent-text': '#fca5a5',
|
|
},
|
|
green: {
|
|
'--c-surface-0': '#1a1a1a',
|
|
'--c-surface-1': '#343434',
|
|
'--c-surface-2': '#3d3d3d',
|
|
'--c-border': '#3d3d3d',
|
|
'--c-border-soft': '#4a4a4a',
|
|
'--c-border-faint': '#343434',
|
|
'--c-border-strong': '#949494',
|
|
'--c-text': '#ececec',
|
|
'--c-text-secondary': '#bbbbbb',
|
|
'--c-text-muted': '#949494',
|
|
'--c-text-subtle': '#747474',
|
|
'--c-text-faint': '#5c5c5c',
|
|
'--c-text-faintest': '#4a4a4a',
|
|
'--c-danger-bg': '#450a0a',
|
|
'--c-danger-border': '#7f1d1d',
|
|
'--c-danger-text': '#f87171',
|
|
'--c-danger-text-strong': '#fca5a5',
|
|
'--c-accent': '#4ade80',
|
|
'--c-accent-soft': '#15803d',
|
|
'--c-accent-text': '#86efac',
|
|
},
|
|
blue: {
|
|
'--c-surface-0': '#363636',
|
|
'--c-surface-1': '#545454',
|
|
'--c-surface-2': '#666666',
|
|
'--c-border': '#666666',
|
|
'--c-border-soft': '#7a7a7a',
|
|
'--c-border-faint': '#545454',
|
|
'--c-border-strong': '#adadad',
|
|
'--c-text': '#ececec',
|
|
'--c-text-secondary': '#c8c8c8',
|
|
'--c-text-muted': '#adadad',
|
|
'--c-text-subtle': '#989898',
|
|
'--c-text-faint': '#888888',
|
|
'--c-text-faintest': '#7a7a7a',
|
|
'--c-danger-bg': '#450a0a',
|
|
'--c-danger-border': '#7f1d1d',
|
|
'--c-danger-text': '#f87171',
|
|
'--c-danger-text-strong': '#fca5a5',
|
|
'--c-accent': '#60a5fa',
|
|
'--c-accent-soft': '#1d4ed8',
|
|
'--c-accent-text': '#93c5fd',
|
|
},
|
|
yellow: {
|
|
'--c-surface-0': '#242424',
|
|
'--c-surface-1': '#424242',
|
|
'--c-surface-2': '#4a4a4a',
|
|
'--c-border': '#4a4a4a',
|
|
'--c-border-soft': '#545454',
|
|
'--c-border-faint': '#424242',
|
|
'--c-border-strong': '#888888',
|
|
'--c-text': '#e0e0e0',
|
|
'--c-text-secondary': '#a2a2a2',
|
|
'--c-text-muted': '#888888',
|
|
'--c-text-subtle': '#797979',
|
|
'--c-text-faint': '#676767',
|
|
'--c-text-faintest': '#545454',
|
|
'--c-danger-bg': '#450a0a',
|
|
'--c-danger-border': '#7f1d1d',
|
|
'--c-danger-text': '#f87171',
|
|
'--c-danger-text-strong': '#fca5a5',
|
|
'--c-accent': '#facc15',
|
|
'--c-accent-soft': '#a16207',
|
|
'--c-accent-text': '#fde047',
|
|
},
|
|
pink: {
|
|
'--c-surface-0': '#2c2c2c',
|
|
'--c-surface-1': '#4e4e4e',
|
|
'--c-surface-2': '#5a5a5a',
|
|
'--c-border': '#5a5a5a',
|
|
'--c-border-soft': '#6b6b6b',
|
|
'--c-border-faint': '#4e4e4e',
|
|
'--c-border-strong': '#b3b3b3',
|
|
'--c-text': '#f2f2f2',
|
|
'--c-text-secondary': '#d0d0d0',
|
|
'--c-text-muted': '#b3b3b3',
|
|
'--c-text-subtle': '#9a9a9a',
|
|
'--c-text-faint': '#818181',
|
|
'--c-text-faintest': '#6b6b6b',
|
|
'--c-danger-bg': '#450a0a',
|
|
'--c-danger-border': '#7f1d1d',
|
|
'--c-danger-text': '#f87171',
|
|
'--c-danger-text-strong': '#fca5a5',
|
|
'--c-accent': '#f472b6',
|
|
'--c-accent-soft': '#be185d',
|
|
'--c-accent-text': '#f9a8d4',
|
|
},
|
|
orange: {
|
|
'--c-surface-0': '#252525',
|
|
'--c-surface-1': '#474747',
|
|
'--c-surface-2': '#565656',
|
|
'--c-border': '#565656',
|
|
'--c-border-soft': '#676767',
|
|
'--c-border-faint': '#474747',
|
|
'--c-border-strong': '#9b9b9b',
|
|
'--c-text': '#eaeaea',
|
|
'--c-text-secondary': '#b8b8b8',
|
|
'--c-text-muted': '#9b9b9b',
|
|
'--c-text-subtle': '#888888',
|
|
'--c-text-faint': '#7b7b7b',
|
|
'--c-text-faintest': '#676767',
|
|
'--c-danger-bg': '#450a0a',
|
|
'--c-danger-border': '#7f1d1d',
|
|
'--c-danger-text': '#f87171',
|
|
'--c-danger-text-strong': '#fca5a5',
|
|
'--c-accent': '#fb923c',
|
|
'--c-accent-soft': '#c2410c',
|
|
'--c-accent-text': '#fdba74',
|
|
},
|
|
}
|
|
|
|
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)
|
|
}
|