/** * dsh-themes skin data: five classic colorways, each with a light and a dark * variant. Every variant overrides the same semantic surface (see * ROLE_TOKENS) with one value per role, so a skin never goes illegible — any * token a variant does not set falls back to the built-in base palette. * * This module is the single source of truth for both the client plugin * (registration) and the scripts/check-skins.ts validation gate. */ /** RGB hex → rgba() string helper for derived border/hover/ghost tokens. */ function rgba(hex: string, alpha: number): string { const r = parseInt(hex.slice(1, 3), 16) const g = parseInt(hex.slice(3, 5), 16) const b = parseInt(hex.slice(5, 7), 16) return `rgba(${r}, ${g}, ${b}, ${alpha})` } /** Every alias token the skin surface overrides, keyed by semantic role. */ export const ROLE_TOKENS = { base: '--dsw-alias-bg-base', layer1: '--dsw-alias-bg-layer-1', layer2: '--dsw-alias-bg-layer-2', layer3: '--dsw-alias-bg-layer-3', overlay: '--dsw-alias-bg-overlay', borderL1: '--dsw-alias-border-l1', borderL2: '--dsw-alias-border-l2', borderL3: '--dsw-alias-border-l3', brand: '--dsw-alias-brand-primary', brandText: '--dsw-alias-brand-text', brandInvert: '--dsw-alias-brand-primary-invert', labelPrimary: '--dsw-alias-label-primary', labelSecondary: '--dsw-alias-label-secondary', labelTertiary: '--dsw-alias-label-tertiary', labelDimmed: '--dsw-alias-label-dimmed', labelCaption: '--dsw-alias-label-caption', buttonPrimaryFill: '--dsw-alias-button-primary-fill', buttonPrimaryHover: '--dsw-alias-button-primary-hover', buttonPrimaryDimmed: '--dsw-alias-button-primary-dimmed', buttonInfoFill: '--dsw-alias-button-info-fill', buttonInfoHover: '--dsw-alias-button-info-hover', buttonContrastFill: '--dsw-alias-button-contrast-fill', buttonElevatedFill: '--dsw-alias-button-elevated-fill', buttonFloatingFill: '--dsw-alias-button-floating-fill', hover: '--dsw-alias-interactive-bg-hover', active: '--dsw-alias-interactive-bg-active', hoverSolid: '--dsw-alias-interactive-bg-hover-solid', hoverAccent: '--dsw-alias-interactive-bg-hover-accent', error: '--dsw-alias-state-error-primary', errorSecondary: '--dsw-alias-state-error-secondary', success: '--dsw-alias-state-success-primary', successSecondary: '--dsw-alias-state-success-secondary', warn: '--dsw-alias-state-warn-primary', warnSecondary: '--dsw-alias-state-warn-secondary', business: '--dsw-alias-state-business-primary', codeBlock: '--dsw-alias-markdown-code-block', codeInline: '--dsw-alias-markdown-inline-code', placeholder: '--dsw-alias-markdown-placeholder', scrollbarBg: '--dsw-alias-scrollbar-bg-l1', scrollbarHover: '--dsw-alias-scrollbar-hover-l1', sidebar: '--dsw-specific-sidebar-fill', menu: '--dsw-specific-menu', selector: '--dsw-specific-selector', bubble: '--dsw-specific-bubble', bubbleHighlight: '--dsw-specific-bubble-highlight', inputMajor: '--dsw-specific-input-major', loginInput: '--dsw-specific-login-input', toast: '--dsw-alias-toast-bg', tooltip: '--dsw-alias-tooltip-bg', } as const export type SkinRole = keyof typeof ROLE_TOKENS /** One value per semantic role — required by construction for every variant. */ export type RoleValues = Record export interface SkinVariant { /** Registered theme id: `-` (e.g. "nord-dark"). */ id: string /** Which base palette this variant builds on. */ colorScheme: 'light' | 'dark' /** Skin key (e.g. "nord"). */ skin: string /** Preview colors for the settings-row swatch. */ preview: { base: string; brand: string } /** Role values applied as inline alias-token overrides. */ values: RoleValues } export interface Skin { /** Stable key; also the theme-id prefix. */ key: string /** Display name (proper noun, same in zh/en). */ name: string /** [light, dark] variant pair. */ variants: [SkinVariant, SkinVariant] } /** Compose the ThemeDefinition token dictionary for one variant. */ export function variantTokens(variant: SkinVariant): Record { const tokens: Record = {} for (const [role, token] of Object.entries(ROLE_TOKENS)) { tokens[token] = variant.values[role as SkinRole] } return tokens } /** * Per-scheme palette fields. Border and hover hues are given as one hex and * derived with alphas so every variant keeps the same relationship between * borders, hovers, and their base hue. */ interface PaletteScheme { base: string layer1: string layer2: string layer3: string overlay: string border: string label1: string label2: string label3: string dimmed: string caption: string brand: string brandHover: string info: string infoHover: string contrast: string elevated: string floating: string error: string error2: string success: string success2: string warn: string warn2: string business: string code: string codeInline: string placeholder: string scrollbarBg: string scrollbarHover: string sidebar: string menu: string selector: string bubble: string bubbleHi: string input: string login: string toast: string tooltip: string } interface SkinPalette { key: string name: string light: PaletteScheme dark: PaletteScheme } function buildVariant(skin: SkinPalette, scheme: 'light' | 'dark', pal: PaletteScheme): SkinVariant { const base = scheme === 'dark' ? pal.label1 : pal.base return { id: `${skin.key}-${scheme}`, colorScheme: scheme, skin: skin.key, preview: { base: pal.base, brand: pal.brand }, values: { base: pal.base, layer1: pal.layer1, layer2: pal.layer2, layer3: pal.layer3, overlay: pal.overlay, borderL1: rgba(pal.border, 0.08), borderL2: rgba(pal.border, 0.14), borderL3: rgba(pal.border, 0.2), brand: pal.brand, brandText: scheme === 'dark' ? pal.label1 : pal.base, brandInvert: scheme === 'dark' ? pal.label1 : pal.base, labelPrimary: pal.label1, labelSecondary: pal.label2, labelTertiary: pal.label3, labelDimmed: pal.dimmed, labelCaption: pal.caption, buttonPrimaryFill: pal.brand, buttonPrimaryHover: pal.brandHover, buttonPrimaryDimmed: rgba(pal.brand, 0.22), buttonInfoFill: pal.info, buttonInfoHover: pal.infoHover, buttonContrastFill: pal.contrast, buttonElevatedFill: pal.elevated, buttonFloatingFill: pal.floating, hover: rgba(base, scheme === 'dark' ? 0.08 : 0.06), active: rgba(base, scheme === 'dark' ? 0.14 : 0.1), hoverSolid: pal.layer2, hoverAccent: rgba(pal.brand, scheme === 'dark' ? 0.18 : 0.14), error: pal.error, errorSecondary: pal.error2, success: pal.success, successSecondary: pal.success2, warn: pal.warn, warnSecondary: pal.warn2, business: pal.business, codeBlock: pal.code, codeInline: pal.codeInline, placeholder: pal.placeholder, scrollbarBg: pal.scrollbarBg, scrollbarHover: pal.scrollbarHover, sidebar: pal.sidebar, menu: pal.menu, selector: pal.selector, bubble: pal.bubble, bubbleHighlight: pal.bubbleHi, inputMajor: pal.input, loginInput: pal.login, toast: pal.toast, tooltip: pal.tooltip, }, } } function skin(palette: SkinPalette): Skin { return { key: palette.key, name: palette.name, variants: [buildVariant(palette, 'light', palette.light), buildVariant(palette, 'dark', palette.dark)], } } /** The five skins, in display order. */ export const SKINS: readonly Skin[] = [ skin({ key: 'nord', name: 'Nord', // Arctic blue-gray: polar night + snow storm + frost accents. light: { base: '#ECEFF4', layer1: '#E5E9F0', layer2: '#D8DEE9', layer3: '#C9D2DE', overlay: '#FFFFFF', border: '#2E3440', label1: '#2E3440', label2: '#4C566A', label3: '#6B7688', dimmed: '#94A0B4', caption: '#7E8899', brand: '#5E81AC', brandHover: '#6B93BC', info: '#5E81AC', infoHover: '#6B93BC', contrast: '#3B4252', elevated: '#FFFFFF', floating: '#FFFFFF', error: '#BF616A', error2: '#CE7A82', success: '#7FA36B', success2: '#97B385', warn: '#C08A3E', warn2: '#D2A45F', business: '#9D7E96', code: '#E5E9F0', codeInline: '#D8DEE9', placeholder: '#AEB9CB', scrollbarBg: '#D8DEE9', scrollbarHover: '#C2CDDC', sidebar: '#E5E9F0', menu: '#D8DEE9', selector: '#E0E5EE', bubble: '#E5E9F0', bubbleHi: '#D8DEE9', input: '#FFFFFF', login: '#E5E9F0', toast: '#3B4252', tooltip: '#434C5E', }, dark: { base: '#2E3440', layer1: '#3B4252', layer2: '#434C5E', layer3: '#4C566A', overlay: '#3B4252', border: '#D8DEE9', label1: '#ECEFF4', label2: '#D8DEE9', label3: '#A9B5C8', dimmed: '#7E8CA1', caption: '#93A1B5', brand: '#81A1C1', brandHover: '#8FB1D1', info: '#5E81AC', infoHover: '#6E91BB', contrast: '#D8DEE9', elevated: '#3B4252', floating: '#3B4252', error: '#BF616A', error2: '#D07B83', success: '#A3BE8C', success2: '#B5CBA1', warn: '#EBCB8B', warn2: '#F0D8A0', business: '#B48EAD', code: '#252B36', codeInline: '#3B4252', placeholder: '#4C566A', scrollbarBg: '#3B4252', scrollbarHover: '#4C566A', sidebar: '#2E3440', menu: '#3B4252', selector: '#3B4252', bubble: '#3B4252', bubbleHi: '#434C5E', input: '#3B4252', login: '#2E3440', toast: '#3B4252', tooltip: '#4C566A', }, }), skin({ key: 'gruvbox', name: 'Gruvbox', // Retro warm: sepia canvas, muted earth accents. light: { base: '#FBF1C7', layer1: '#F4EBCE', layer2: '#EDE0B8', layer3: '#E5D6AA', overlay: '#FFFFFF', border: '#504945', label1: '#282828', label2: '#3C3836', label3: '#504945', dimmed: '#7C6F64', caption: '#665C54', brand: '#076678', brandHover: '#0E7E93', info: '#076678', infoHover: '#0E7E93', contrast: '#282828', elevated: '#FBF1C7', floating: '#FBF1C7', error: '#CC241D', error2: '#D6453E', success: '#79740E', success2: '#8F8A2C', warn: '#B57614', warn2: '#C48B2F', business: '#8F3F71', code: '#EFE5C9', codeInline: '#EBDBB2', placeholder: '#BDAE93', scrollbarBg: '#EBDBB2', scrollbarHover: '#D5C4A1', sidebar: '#F1E7C4', menu: '#EBDBB2', selector: '#F1E7C4', bubble: '#F1E7C4', bubbleHi: '#EBDBB2', input: '#FFFFFF', login: '#F1E7C4', toast: '#3C3836', tooltip: '#504945', }, dark: { base: '#1D2021', layer1: '#282828', layer2: '#3C3836', layer3: '#504945', overlay: '#3C3836', border: '#EBDBB2', label1: '#FBF1C7', label2: '#EBDBB2', label3: '#D5C4A1', dimmed: '#A89984', caption: '#BDAE93', brand: '#83A598', brandHover: '#93B3C4', info: '#83A598', infoHover: '#93B3C4', contrast: '#FBF1C7', elevated: '#3C3836', floating: '#3C3836', error: '#FB4934', error2: '#FC6B5A', success: '#B8BB26', success2: '#C6C94E', warn: '#FABD2F', warn2: '#FBCA58', business: '#D3869B', code: '#20221D', codeInline: '#3C3836', placeholder: '#665C54', scrollbarBg: '#3C3836', scrollbarHover: '#504945', sidebar: '#1D2021', menu: '#3C3836', selector: '#3C3836', bubble: '#3C3836', bubbleHi: '#504945', input: '#282828', login: '#1D2021', toast: '#3C3836', tooltip: '#504945', }, }), skin({ key: 'solarized', name: 'Solarized', // Low-contrast precision: the exact base0x ramp, identical accents in both schemes. light: { base: '#FDF6E3', layer1: '#F7F0DD', layer2: '#EEE8D5', layer3: '#E4DECC', overlay: '#FFFFFF', border: '#586E75', label1: '#073642', label2: '#586E75', label3: '#657B83', dimmed: '#839496', caption: '#586E75', brand: '#268BD2', brandHover: '#4D9EDB', info: '#268BD2', infoHover: '#4D9EDB', contrast: '#073642', elevated: '#FDF6E3', floating: '#FDF6E3', error: '#DC322F', error2: '#E25A58', success: '#859900', success2: '#9AA32E', warn: '#B58900', warn2: '#C19B2E', business: '#6C71C4', code: '#EEE8D5', codeInline: '#EEE8D5', placeholder: '#93A1A1', scrollbarBg: '#EEE8D5', scrollbarHover: '#D5CFBE', sidebar: '#EEE8D5', menu: '#E4DECC', selector: '#F1EBD8', bubble: '#EEE8D5', bubbleHi: '#E4DECC', input: '#FDF6E3', login: '#EEE8D5', toast: '#073642', tooltip: '#073642', }, dark: { base: '#002B36', layer1: '#073642', layer2: '#0B3A45', layer3: '#123F4A', overlay: '#073642', border: '#93A1A1', label1: '#EEE8D5', label2: '#93A1A1', label3: '#839496', dimmed: '#586E75', caption: '#657B83', brand: '#268BD2', brandHover: '#3E9BD8', info: '#268BD2', infoHover: '#3E9BD8', contrast: '#EEE8D5', elevated: '#073642', floating: '#073642', error: '#DC322F', error2: '#E25A58', success: '#859900', success2: '#9AA32E', warn: '#B58900', warn2: '#C19B2E', business: '#6C71C4', code: '#00212B', codeInline: '#073642', placeholder: '#586E75', scrollbarBg: '#073642', scrollbarHover: '#586E75', sidebar: '#002B36', menu: '#073642', selector: '#073642', bubble: '#073642', bubbleHi: '#0B3A45', input: '#073642', login: '#002B36', toast: '#073642', tooltip: '#073642', }, }), skin({ key: 'dracula', name: 'Dracula', // Purple-tinted dark with neon accents; the light variant is a soft paper take. light: { base: '#F8F8F2', layer1: '#EFEFE9', layer2: '#E6E6DF', layer3: '#DCDCD4', overlay: '#FFFFFF', border: '#44475A', label1: '#282A36', label2: '#44475A', label3: '#5F6171', dimmed: '#858795', caption: '#6E7080', brand: '#6D4FAF', brandHover: '#7C5FBE', info: '#6272A4', infoHover: '#7181B4', contrast: '#282A36', elevated: '#F8F8F2', floating: '#F8F8F2', error: '#D8434E', error2: '#E15D66', success: '#3BA55D', success2: '#5DB87A', warn: '#A88A1F', warn2: '#BCA040', business: '#C4569F', code: '#EFEFE9', codeInline: '#E6E6DF', placeholder: '#A3A5B2', scrollbarBg: '#E6E6DF', scrollbarHover: '#D2D2CA', sidebar: '#EFEFE9', menu: '#E6E6DF', selector: '#EFEFE9', bubble: '#EFEFE9', bubbleHi: '#E6E6DF', input: '#FFFFFF', login: '#EFEFE9', toast: '#282A36', tooltip: '#343746', }, dark: { base: '#282A36', layer1: '#2F3241', layer2: '#383B4D', layer3: '#414457', overlay: '#343746', border: '#F8F8F2', label1: '#F8F8F2', label2: '#C9CAD4', label3: '#A9AAB8', dimmed: '#8B8C9E', caption: '#9A9BA9', brand: '#BD93F9', brandHover: '#CBA4FB', info: '#6272A4', infoHover: '#7181B4', contrast: '#F8F8F2', elevated: '#343746', floating: '#343746', error: '#FF5555', error2: '#FF7373', success: '#50FA7B', success2: '#79FC9C', warn: '#F1FA8C', warn2: '#F4FBA6', business: '#FF79C6', code: '#21222C', codeInline: '#343746', placeholder: '#6272A4', scrollbarBg: '#343746', scrollbarHover: '#44475A', sidebar: '#21222C', menu: '#2F3241', selector: '#343746', bubble: '#343746', bubbleHi: '#44475A', input: '#21222C', login: '#21222C', toast: '#343746', tooltip: '#44475A', }, }), skin({ key: 'tokyo', name: 'Tokyo Night', // Neon cyber: deep indigo night; the light variant is the official Day palette. light: { base: '#E1E2E7', layer1: '#D5D6DB', layer2: '#C4C8DA', layer3: '#B9BDD0', overlay: '#FFFFFF', border: '#848CB5', label1: '#3760BF', label2: '#5F6FA8', label3: '#7B8AB8', dimmed: '#9AA5C9', caption: '#6E7FB2', brand: '#2E7DE9', brandHover: '#4B90EC', info: '#2E7DE9', infoHover: '#4B90EC', contrast: '#1A1B26', elevated: '#E1E2E7', floating: '#E1E2E7', error: '#F52A65', error2: '#F6537F', success: '#587539', success2: '#6F8F4A', warn: '#8C6C3E', warn2: '#A28453', business: '#9854F1', code: '#D5D6DB', codeInline: '#C4C8DA', placeholder: '#848CB5', scrollbarBg: '#C4C8DA', scrollbarHover: '#B0B6CF', sidebar: '#D5D6DB', menu: '#C4C8DA', selector: '#D5D6DB', bubble: '#D5D6DB', bubbleHi: '#C4C8DA', input: '#E1E2E7', login: '#D5D6DB', toast: '#1A1B26', tooltip: '#24283B', }, dark: { base: '#1A1B26', layer1: '#1F2130', layer2: '#24283B', layer3: '#292E42', overlay: '#292E42', border: '#565F89', label1: '#C0CAF5', label2: '#A9B2DF', label3: '#8B93C4', dimmed: '#565F89', caption: '#737BAA', brand: '#7AA2F7', brandHover: '#8FB0F9', info: '#7AA2F7', infoHover: '#8FB0F9', contrast: '#C0CAF5', elevated: '#24283B', floating: '#24283B', error: '#F7768E', error2: '#F98EA2', success: '#9ECE6A', success2: '#B1D885', warn: '#E0AF68', warn2: '#E6BE80', business: '#BB9AF7', code: '#16161E', codeInline: '#24283B', placeholder: '#565F89', scrollbarBg: '#24283B', scrollbarHover: '#565F89', sidebar: '#16161E', menu: '#1F2130', selector: '#24283B', bubble: '#24283B', bubbleHi: '#292E42', input: '#1F2130', login: '#16161E', toast: '#24283B', tooltip: '#292E42', }, }), ] /** All registered variants, in registration order. */ export const VARIANTS: readonly SkinVariant[] = SKINS.flatMap(skin => skin.variants) /** One selectable ThemeDefinition per variant (client registration input). */ export function variantDefinitions(): ReadonlyArray<{ id: string; colorScheme: 'light' | 'dark'; tokens: Record }> { return VARIANTS.map(variant => ({ id: variant.id, colorScheme: variant.colorScheme, tokens: variantTokens(variant), })) }