Stacks: deploy with every compose file (labels from inspect, not comma-joined ps); refuse a stale overwrite; empty list without Docker. Alerts: firing state survives a restart without re-announcing; vanished alerts are recorded as cleared with the reason; rule values validated; 200 rules. Docker: 404 for a removed container's logs. Skins: four palettes lifted to readable contrast. Phone: bigger toggles and swatches. Installer: deploy republishes; an older published build is never installed silently. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
125 lines
10 KiB
TypeScript
125 lines
10 KiB
TypeScript
// Global themes. Each one is a set of CSS variables applied to <html>, a matching xterm
|
|
// palette, and a data-theme attribute the stylesheet can hang extra flourishes on.
|
|
import type { ITheme } from '@xterm/xterm'
|
|
|
|
export interface Theme {
|
|
id: string
|
|
name: string
|
|
blurb: string
|
|
vars: Record<string, string>
|
|
xterm: ITheme
|
|
}
|
|
|
|
const base = {
|
|
'--ok': '#3fb950', '--bad': '#ff7b72', '--s1': '#3987e5', '--s2': '#c98500', '--track': 'rgba(57, 135, 229, .18)',
|
|
'--btn-fg': '#111', '--term-bg': '#0d1117', '--font': 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
'--mono': 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',
|
|
}
|
|
|
|
// A palette in one line: surfaces, text, the two accents, status colours, and the sixteen
|
|
// terminal colours (black red green yellow blue magenta cyan white, then the bright eight).
|
|
function mk(id: string, name: string, blurb: string, ui: string[], term: string[], extra: Record<string, string> = {}): Theme {
|
|
const [bg, bg2, bg3, border, fg, muted, accent, accent2, ok, bad, s1, s2, termBg] = ui
|
|
const k = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'brightBlack', 'brightRed', 'brightGreen', 'brightYellow', 'brightBlue', 'brightMagenta', 'brightCyan', 'brightWhite']
|
|
const light = parseInt(bg.slice(1, 3), 16) > 128
|
|
return {
|
|
id, name, blurb,
|
|
vars: { ...base, '--bg': bg, '--bg2': bg2, '--bg3': bg3, '--border': border, '--fg': fg, '--muted': muted, '--accent': accent, '--accent2': accent2, '--ok': ok, '--bad': bad, '--s1': s1, '--s2': s2,
|
|
'--track': light ? 'rgba(0, 0, 0, .10)' : 'rgba(255, 255, 255, .09)', '--term-bg': termBg, '--btn-fg': light ? '#fff' : '#111', ...extra },
|
|
xterm: { background: termBg, foreground: fg, cursor: accent, selectionBackground: light ? 'rgba(0,0,0,.18)' : 'rgba(255,255,255,.22)', ...Object.fromEntries(k.map((name, i) => [name, term[i]])) },
|
|
}
|
|
}
|
|
|
|
export const THEMES: Theme[] = [
|
|
{
|
|
id: 'dark', name: 'Dark', blurb: 'The default. Graphite surfaces, amber brand, blue accents.',
|
|
vars: { ...base, '--bg': '#111418', '--bg2': '#171b21', '--bg3': '#1e242c', '--border': '#2a313b', '--fg': '#d0d7de', '--muted': '#7d8590', '--accent': '#e3a33b', '--accent2': '#58a6ff' },
|
|
xterm: {
|
|
background: '#0d1117', foreground: '#d0d7de', cursor: '#d0d7de', selectionBackground: '#264f78',
|
|
black: '#0d1117', red: '#ff7b72', green: '#3fb950', yellow: '#d29922', blue: '#58a6ff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#b1bac4',
|
|
brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364', brightYellow: '#e3b341', brightBlue: '#79c0ff', brightMagenta: '#d2a8ff', brightCyan: '#56d4dd', brightWhite: '#f0f6fc',
|
|
},
|
|
},
|
|
{
|
|
id: 'light', name: 'Light', blurb: 'Paper white with the same layout. For daylight and bright rooms.',
|
|
vars: { ...base, '--bg': '#f6f8fa', '--bg2': '#ffffff', '--bg3': '#eaeef2', '--border': '#d0d7de', '--fg': '#1f2328', '--muted': '#59636e', '--accent': '#b35c00', '--accent2': '#0969da',
|
|
'--ok': '#1a7f37', '--bad': '#cf222e', '--s1': '#2a78d6', '--s2': '#9a6700', '--track': 'rgba(42, 120, 214, .16)', '--btn-fg': '#ffffff', '--term-bg': '#ffffff' },
|
|
xterm: {
|
|
background: '#ffffff', foreground: '#1f2328', cursor: '#1f2328', selectionBackground: '#b6d6ff',
|
|
black: '#24292f', red: '#cf222e', green: '#116329', yellow: '#4d2d00', blue: '#0969da', magenta: '#8250df', cyan: '#1b7c83', white: '#6e7781',
|
|
brightBlack: '#57606a', brightRed: '#a40e26', brightGreen: '#1a7f37', brightYellow: '#633c01', brightBlue: '#218bff', brightMagenta: '#a475f9', brightCyan: '#3192aa', brightWhite: '#8c959f',
|
|
},
|
|
},
|
|
{
|
|
id: 'cyberpunk', name: 'Cyberpunk', blurb: 'Deep violet, magenta brand, cyan accents, a little neon glow.',
|
|
vars: { ...base, '--bg': '#0b0014', '--bg2': '#140026', '--bg3': '#1f0a3a', '--border': '#3b1f6b', '--fg': '#e8e0ff', '--muted': '#9b8bd0', '--accent': '#ff2bd6', '--accent2': '#00e5ff',
|
|
'--ok': '#39ff14', '--bad': '#ff3860', '--s1': '#00e5ff', '--s2': '#ffe600', '--track': 'rgba(0, 229, 255, .16)', '--btn-fg': '#0b0014', '--term-bg': '#07000f' },
|
|
xterm: {
|
|
background: '#07000f', foreground: '#e8e0ff', cursor: '#ff2bd6', selectionBackground: '#3b1f6b',
|
|
black: '#0b0014', red: '#ff3860', green: '#39ff14', yellow: '#ffe600', blue: '#00e5ff', magenta: '#ff2bd6', cyan: '#5cf6ff', white: '#c8bff0',
|
|
brightBlack: '#6a55a3', brightRed: '#ff6b8a', brightGreen: '#7dff5c', brightYellow: '#fff27a', brightBlue: '#66eeff', brightMagenta: '#ff7de8', brightCyan: '#a5fbff', brightWhite: '#ffffff',
|
|
},
|
|
},
|
|
{
|
|
id: 'retro', name: 'Retro', blurb: 'Amber phosphor terminal. Monospace everywhere, faint scanlines.',
|
|
vars: { ...base, '--bg': '#0e0b05', '--bg2': '#171106', '--bg3': '#221909', '--border': '#3a2c10', '--fg': '#ffb000', '--muted': '#a8781c', '--accent': '#ffcc33', '--accent2': '#ffb000',
|
|
'--ok': '#ffd166', '--bad': '#ff6b35', '--s1': '#ffb000', '--s2': '#ff8c00', '--track': 'rgba(255, 176, 0, .16)', '--btn-fg': '#0e0b05', '--term-bg': '#0a0803',
|
|
'--font': 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace' },
|
|
xterm: {
|
|
background: '#0a0803', foreground: '#ffb000', cursor: '#ffcc33', selectionBackground: '#3a2c10',
|
|
black: '#0a0803', red: '#ff6b35', green: '#ffd166', yellow: '#ffcc33', blue: '#e0a020', magenta: '#ff9f43', cyan: '#f5c76b', white: '#ffb000',
|
|
brightBlack: '#6b4e14', brightRed: '#ff8c5a', brightGreen: '#ffe08a', brightYellow: '#ffe066', brightBlue: '#f0b040', brightMagenta: '#ffb870', brightCyan: '#ffdd99', brightWhite: '#fff3cc',
|
|
},
|
|
},
|
|
mk('hud', 'HUD', 'Hairlines of cyan light on near-black. Thin type, no fills. At your service.',
|
|
['#03070c', '#050c14', '#0a1a28', '#123047', '#bfe9ff', '#5b8ba6', '#35d6ff', '#7df9ff', '#4dffb5', '#ff5470', '#35d6ff', '#ffb547', '#02050a'],
|
|
['#02050a', '#ff5470', '#4dffb5', '#ffb547', '#35a8ff', '#b28dff', '#35d6ff', '#bfe9ff', '#3a5a70', '#ff8398', '#8dffd0', '#ffd08a', '#7cc6ff', '#cdb5ff', '#7df9ff', '#ffffff']),
|
|
mk('nord', 'Nord', 'Arctic blue-greys. Calm, low contrast, easy at night.',
|
|
['#2e3440', '#3b4252', '#434c5e', '#4c566a', '#d8dee9', '#8f9bb3', '#88c0d0', '#81a1c1', '#a3be8c', '#d9828a', '#81a1c1', '#ebcb8b', '#2b303b'],
|
|
['#3b4252', '#bf616a', '#a3be8c', '#ebcb8b', '#81a1c1', '#b48ead', '#88c0d0', '#e5e9f0', '#4c566a', '#bf616a', '#a3be8c', '#ebcb8b', '#81a1c1', '#b48ead', '#8fbcbb', '#eceff4']),
|
|
mk('gruvbox', 'Gruvbox', 'Warm retro earth tones. Cream on charcoal.',
|
|
['#282828', '#32302f', '#3c3836', '#504945', '#ebdbb2', '#a89984', '#fabd2f', '#83a598', '#b8bb26', '#fb4934', '#83a598', '#fe8019', '#1d2021'],
|
|
['#282828', '#cc241d', '#98971a', '#d79921', '#458588', '#b16286', '#689d6a', '#a89984', '#928374', '#fb4934', '#b8bb26', '#fabd2f', '#83a598', '#d3869b', '#8ec07c', '#ebdbb2']),
|
|
mk('catppuccin', 'Catppuccin', 'Mocha: soft pastels on deep mauve-grey.',
|
|
['#1e1e2e', '#181825', '#313244', '#45475a', '#cdd6f4', '#9399b2', '#cba6f7', '#89b4fa', '#a6e3a1', '#f38ba8', '#89b4fa', '#fab387', '#11111b'],
|
|
['#45475a', '#f38ba8', '#a6e3a1', '#f9e2af', '#89b4fa', '#f5c2e7', '#94e2d5', '#bac2de', '#585b70', '#f38ba8', '#a6e3a1', '#f9e2af', '#89b4fa', '#f5c2e7', '#94e2d5', '#a6adc8']),
|
|
mk('latte', 'Catppuccin Latte', 'The same pastels, by daylight.',
|
|
['#eff1f5', '#e6e9ef', '#dce0e8', '#bcc0cc', '#4c4f69', '#6c6f85', '#8839ef', '#1e66f5', '#35891f', '#d20f39', '#1e66f5', '#fe640b', '#e6e9ef'],
|
|
['#5c5f77', '#d20f39', '#40a02b', '#df8e1d', '#1e66f5', '#ea76cb', '#179299', '#acb0be', '#6c6f85', '#d20f39', '#40a02b', '#df8e1d', '#1e66f5', '#ea76cb', '#179299', '#bcc0cc']),
|
|
mk('dracula', 'Dracula', 'Purple, pink and green on midnight.',
|
|
['#282a36', '#21222c', '#343746', '#44475a', '#f8f8f2', '#8b93b8', '#bd93f9', '#8be9fd', '#50fa7b', '#ff5555', '#8be9fd', '#ffb86c', '#1e1f29'],
|
|
['#21222c', '#ff5555', '#50fa7b', '#f1fa8c', '#bd93f9', '#ff79c6', '#8be9fd', '#f8f8f2', '#6272a4', '#ff6e6e', '#69ff94', '#ffffa5', '#d6acff', '#ff92df', '#a4ffff', '#ffffff']),
|
|
mk('tokyo', 'Tokyo Night', 'Neon signs over a dark blue city.',
|
|
['#1a1b26', '#16161e', '#24283b', '#343b58', '#c0caf5', '#737aa2', '#7aa2f7', '#bb9af7', '#9ece6a', '#f7768e', '#7aa2f7', '#ff9e64', '#13141c'],
|
|
['#15161e', '#f7768e', '#9ece6a', '#e0af68', '#7aa2f7', '#bb9af7', '#7dcfff', '#a9b1d6', '#414868', '#f7768e', '#9ece6a', '#e0af68', '#7aa2f7', '#bb9af7', '#7dcfff', '#c0caf5']),
|
|
mk('solarized', 'Solarized Dark', 'The precise, low-glare classic.',
|
|
['#002b36', '#073642', '#0a4352', '#1a5666', '#a9b5b5', '#7b9199', '#b58900', '#268bd2', '#859900', '#ea4b48', '#268bd2', '#cb4b16', '#00212b'],
|
|
['#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', '#586e75', '#cb4b16', '#93a1a1', '#b58900', '#839496', '#6c71c4', '#2aa198', '#fdf6e3']),
|
|
mk('solarized-light', 'Solarized Light', 'The same sixteen colours on warm paper.',
|
|
['#fdf6e3', '#eee8d5', '#e4ddc8', '#d3cbb4', '#40555c', '#64777a', '#946f00', '#268bd2', '#6a7a00', '#dc322f', '#268bd2', '#cb4b16', '#eee8d5'],
|
|
['#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', '#002b36', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', '#fdf6e3']),
|
|
]
|
|
|
|
export const DEFAULT_THEME = 'dark'
|
|
const KEY = 'hamlab.theme'
|
|
|
|
export function themeById(id: string | null | undefined): Theme {
|
|
return THEMES.find((t) => t.id === id) ?? THEMES[0]
|
|
}
|
|
|
|
export function currentThemeId(): string {
|
|
return localStorage.getItem(KEY) || DEFAULT_THEME
|
|
}
|
|
|
|
// applyTheme sets the variables on <html>, remembers the choice, and tells open
|
|
// terminals to repaint. Cheap enough to call on every load before first render.
|
|
export function applyTheme(id: string) {
|
|
const t = themeById(id) // unknown ids fall back to the default
|
|
const root = document.documentElement
|
|
for (const [k, v] of Object.entries(t.vars)) root.style.setProperty(k, v)
|
|
root.dataset.theme = t.id
|
|
localStorage.setItem(KEY, t.id)
|
|
document.querySelector('meta[name=theme-color]')?.setAttribute('content', t.vars['--bg2'])
|
|
window.dispatchEvent(new CustomEvent('hamlab:theme', { detail: t }))
|
|
}
|