/** * The PIG palette. * * Users pick an accent and the whole interface re-tints from it. The accent is * stored as a KEY rather than a hex string, for two reasons: the palette can be * retuned centrally without migrating anyone's saved preference, and nobody can * choose a colour that is illegible against the surfaces. * * Every accent below is specified as HSL triples for light and dark surfaces * independently. A colour that reads well on white is usually too dark on * near-black, so the dark variants are lifted in lightness and slightly * desaturated — the same hue, tuned twice, rather than one value used in both * places and looking wrong in one of them. * * Contrast: every `fg` is chosen to clear WCAG AA (4.5:1) against its own * surface, and `on` is the text colour that clears AA against the accent itself * when used as a solid fill. */ export interface AccentDefinition { key: string; label: string; /** Light-mode: accent as HSL channels, `H S% L%`, for CSS `hsl(var(--x))`. */ light: { accent: string; fg: string; on: string; subtle: string }; /** Dark-mode equivalents. */ dark: { accent: string; fg: string; on: string; subtle: string }; } /** * `pig` is the default and the brand: near-black in light mode, near-white in * dark. The mascot is a black-and-white pig, so the product's own accent is * monochrome and every other choice is the user's personality rather than ours. */ export const ACCENTS: AccentDefinition[] = [ { key: 'pig', label: 'Pig', light: { accent: '240 6% 10%', fg: '240 6% 10%', on: '0 0% 100%', subtle: '240 5% 96%', }, dark: { accent: '0 0% 98%', fg: '0 0% 98%', on: '240 6% 10%', subtle: '240 4% 16%', }, }, { key: 'rose', label: 'Rose', light: { accent: '346 77% 50%', fg: '346 77% 42%', on: '0 0% 100%', subtle: '346 77% 97%' }, dark: { accent: '346 84% 62%', fg: '346 90% 72%', on: '346 90% 12%', subtle: '346 40% 18%' }, }, { key: 'amber', label: 'Amber', light: { accent: '32 95% 44%', fg: '28 80% 36%', on: '0 0% 100%', subtle: '38 92% 95%' }, dark: { accent: '38 92% 58%', fg: '43 96% 68%', on: '26 83% 12%', subtle: '30 40% 18%' }, }, { key: 'emerald', label: 'Emerald', light: { accent: '160 84% 32%', fg: '161 88% 26%', on: '0 0% 100%', subtle: '152 76% 96%' }, dark: { accent: '158 64% 48%', fg: '156 72% 62%', on: '160 90% 10%', subtle: '158 35% 16%' }, }, { key: 'sky', label: 'Sky', light: { accent: '201 90% 40%', fg: '202 90% 33%', on: '0 0% 100%', subtle: '204 94% 96%' }, dark: { accent: '199 89% 58%', fg: '198 93% 68%', on: '202 90% 10%', subtle: '200 40% 17%' }, }, { key: 'violet', label: 'Violet', light: { accent: '262 83% 55%', fg: '263 70% 46%', on: '0 0% 100%', subtle: '270 100% 97%' }, dark: { accent: '258 90% 70%', fg: '255 92% 78%', on: '264 80% 12%', subtle: '260 35% 20%' }, }, { key: 'slate', label: 'Slate', light: { accent: '215 25% 35%', fg: '215 25% 28%', on: '0 0% 100%', subtle: '210 40% 96%' }, dark: { accent: '213 27% 74%', fg: '214 32% 82%', on: '215 28% 12%', subtle: '215 20% 18%' }, }, ]; export const ACCENT_KEYS = ACCENTS.map((a) => a.key); export const DEFAULT_ACCENT = 'pig'; export const THEME_MODES = ['light', 'dark', 'system'] as const; export type ThemeMode = (typeof THEME_MODES)[number]; export const DEFAULT_THEME_MODE: ThemeMode = 'system'; export function getAccent(key: string | null | undefined): AccentDefinition { return ACCENTS.find((a) => a.key === key) ?? ACCENTS[0]!; } export function isValidAccent(key: string): boolean { return ACCENT_KEYS.includes(key); } export function isValidThemeMode(mode: string): mode is ThemeMode { return (THEME_MODES as readonly string[]).includes(mode); } /** * Semantic colours for pipeline stages and health states. * * Deliberately independent of the user's accent: if "at risk" re-tinted to * whatever someone picked, a violet enthusiast would see warnings in violet and * the signal would be gone. Status colour must mean the same thing for * everyone. */ export const STATUS_COLORS = { positive: { light: '160 84% 32%', dark: '158 64% 52%' }, warning: { light: '32 95% 44%', dark: '38 92% 60%' }, danger: { light: '0 72% 45%', dark: '0 84% 65%' }, info: { light: '201 90% 40%', dark: '199 89% 60%' }, neutral: { light: '240 4% 46%', dark: '240 5% 65%' }, } as const; export type StatusColor = keyof typeof STATUS_COLORS;