feat(desktop): ship the GitHub themes, with Nous blue on top

The bundled skins were an ad-hoc set that had drifted from anything
recognisable. They are now forks of the VS Code themes people already know,
produced by the repo's own marketplace converter rather than transcribed by
hand, so each palette is byte-identical to what installing the extension
would give you.

`nous` keeps GitHub's chrome and carries the brand blue as its accent. Two
seeds, one colour: `#0053fd` reads at 5.4:1 on the light sidebar but only
3.6:1 on the near-black dark one, so dark carries `#4a84fe` — the same hue
at 263°, lifted to clear AA at 5.9:1. Everything else in both palettes is
upstream's, and a test holds that line.

`github` ships alongside it, unmodified, so the original stays available on
its own terms instead of only existing as the thing nous diverged from.
Catppuccin, Everforest and Solarized join them; the skins nobody could name
are retired, with `midnight` folded into the retired list so anyone sitting
on it lands on nous rather than a dead name.
This commit is contained in:
Brooklyn Nicholson
2026-08-20 01:15:30 -05:00
committed by brooklyn!
parent c2f5d2da21
commit f298612429
4 changed files with 596 additions and 114 deletions
+8 -8
View File
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { __resetBackendSkinSync, ingestBackendSkin } from './backend-sync'
import { skinPref, ThemeProvider, useTheme } from './context'
import { midnightTheme } from './presets'
import { everforestTheme } from './presets'
// The live-authoring loop: Hermes writes/edits one skin file and every surface
// repaints. An in-place edit keeps the NAME — only the palette moves.
@@ -99,9 +99,9 @@ describe('ThemeProvider highlight preview', () => {
const committed = ctx.themeName
act(() => ctx.previewTheme('midnight', 'dark'))
act(() => ctx.previewTheme('everforest', 'dark'))
expect(cssVar('--theme-foreground')).toBe(midnightTheme.colors.foreground)
expect(cssVar('--theme-foreground')).toBe(everforestTheme.darkColors!.foreground)
// The commit surface does not change. The context name and the stored
// preference keep their values.
expect(ctx.themeName).toBe(committed)
@@ -111,22 +111,22 @@ describe('ThemeProvider highlight preview', () => {
it('clearThemePreview repaints the committed appearance', () => {
renderProbe()
act(() => ctx.previewTheme('midnight', 'dark'))
expect(cssVar('--theme-foreground')).toBe(midnightTheme.colors.foreground)
act(() => ctx.previewTheme('everforest', 'dark'))
expect(cssVar('--theme-foreground')).toBe(everforestTheme.darkColors!.foreground)
act(() => ctx.clearThemePreview())
expect(cssVar('--theme-foreground')).not.toBe(midnightTheme.colors.foreground)
expect(cssVar('--theme-foreground')).not.toBe(everforestTheme.darkColors!.foreground)
})
it('a commit replaces the preview and persists', () => {
renderProbe()
act(() => ctx.previewTheme('midnight', 'dark'))
act(() => ctx.previewTheme('everforest', 'dark'))
act(() => ctx.setTheme('mono'))
expect(ctx.themeName).toBe('mono')
expect(skinPref.resolve('default')).toBe('mono')
expect(cssVar('--theme-foreground')).not.toBe(midnightTheme.colors.foreground)
expect(cssVar('--theme-foreground')).not.toBe(everforestTheme.darkColors!.foreground)
})
it('ignores a preview of an unknown theme', () => {
+33 -7
View File
@@ -16,10 +16,13 @@ import { $registryVersion } from '@/contrib/registry'
import { matchesQuery, useMediaQuery } from '@/hooks/use-media-query'
import { persistString, persistStringRecord, storedString, storedStringRecord } from '@/lib/storage'
import { $activeGatewayProfile, normalizeProfileKey } from '@/store/profile'
import { setAppearance } from '@/store/translucency'
import { $backendThemes, $pendingSkinApply } from './backend-sync'
import { hexToRgb, mix, readableOn } from './color'
import { $accentOverride } from './accent-override'
import { harmonize, hexToRgb, mix, readableOn } from './color'
import { BUILTIN_THEME_LIST, DEFAULT_SKIN_NAME, DEFAULT_TYPOGRAPHY, nousTheme } from './presets'
import { retintTheme } from './retint'
import type { DesktopTheme, DesktopThemeColors } from './types'
import { $userThemes, listAllThemes, resolveTheme } from './user-themes'
@@ -35,7 +38,9 @@ const PROFILE_MODES_KEY = 'hermes-desktop-profile-modes-v1'
// Last active profile, recorded so the boot-time paint can pick that profile's
// theme before the gateway reports which profile actually launched.
const LAST_PROFILE_KEY = 'hermes-desktop-active-profile-v1'
const RETIRED_SKINS = new Set(['nous-light', 'default', 'gold'])
// Skins that no longer exist. A profile still pointing at one falls back to
// DEFAULT_SKIN_NAME rather than painting a name nothing resolves.
const RETIRED_SKINS = new Set(['nous-light', 'default', 'gold', 'midnight'])
export type ThemeMode = 'light' | 'dark' | 'system'
@@ -194,6 +199,12 @@ function applyTheme(theme: DesktopTheme, mode: 'light' | 'dark') {
root.dataset.hermesMode = rendered
root.classList.toggle('dark', isDark)
// Translucency is tuned per appearance, and "appearance" means the palette
// actually painted — a skin that keeps a bright surface in "dark" wants
// light's tint. Publishing from here covers the boot paint too, so the very
// first resolved state main is told about is already the right one.
setAppearance(rendered)
// Brand seeds feed every glass + shadcn token via `color-mix()` in styles.css.
const seeds: Record<string, string> = {
'--theme-foreground': c.foreground,
@@ -224,6 +235,11 @@ function applyTheme(theme: DesktopTheme, mode: 'light' | 'dark') {
'--dt-destructive-foreground': c.destructiveForeground,
'--dt-sidebar-border': c.sidebarBorder ?? c.border,
'--dt-user-bubble-border': c.userBubbleBorder ?? c.border,
// Semantic success, bent toward the accent so it settles into the palette
// instead of clashing with it. A green accent barely moves it (see
// `harmonize`); a blue one turns the sidebar's finished dots teal rather
// than leaving eight emerald spots fighting the theme.
'--ui-success': harmonize('#10b981', midground, 0.25),
'--dt-font-sans': typo.fontSans,
'--dt-font-mono': typo.fontMono,
'--noise-opacity-mul': isDark ? 'calc(0.04 / 0.21)' : 'calc(0.34 / 0.21)'
@@ -404,10 +420,20 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
[paintedName, paintedMode, userThemes, backendThemes, registryVersion]
)
// What actually gets painted (matches the `.dark` class applyTheme toggles).
const renderedMode = useMemo(() => renderedModeFor(activeTheme.colors, paintedMode), [activeTheme, paintedMode])
// Dev-only accent retint. `null` (always, in production) returns the theme
// untouched, and retintTheme is an identity when the seed already matches —
// so the picker costs nothing until it's actually moved off the default.
const accentOverride = useStore($accentOverride)
useEffect(() => applyTheme(activeTheme, paintedMode), [activeTheme, paintedMode])
const paintedTheme = useMemo(
() => (accentOverride === null ? activeTheme : retintTheme(activeTheme, accentOverride)),
[activeTheme, accentOverride]
)
// What actually gets painted (matches the `.dark` class applyTheme toggles).
const renderedMode = useMemo(() => renderedModeFor(paintedTheme.colors, paintedMode), [paintedTheme, paintedMode])
useEffect(() => applyTheme(paintedTheme, paintedMode), [paintedTheme, paintedMode])
// Keep the native window appearance pinned to the app theme (vibrancy
// material, titlebar, new-window pre-paint background).
@@ -453,7 +479,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
const value = useMemo<ThemeContextValue>(
() => ({
theme: activeTheme,
theme: paintedTheme,
themeName,
mode,
resolvedMode,
@@ -465,7 +491,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
clearThemePreview
}),
[
activeTheme,
paintedTheme,
themeName,
mode,
resolvedMode,
+554 -98
View File
@@ -1,6 +1,20 @@
/**
* Built-in desktop themes. Names match the CLI skins / dashboard presets.
* Add new themes here — no code changes needed elsewhere.
*
* The palette-bearing skins (nous, catppuccin, everforest, solarized) are forks
* of their VS Code originals, converted by `buildThemeFromMarketplace` (see
* ./install.ts) from the extensions below — the same path a Marketplace import
* takes, so each is identical to installing the extension by hand and costs the
* user neither the download nor the install step.
*
* nous ← github.github-vscode-theme (Light Default / Dark Default)
* catppuccin ← Catppuccin.catppuccin-vsc (Latte / Mocha)
* everforest ← sainnhe.everforest
* solarized ← ryanolsonx.solarized
*
* Re-convert from the upstream extension rather than hand-editing hexes; hand
* edits drift from upstream silently and can't be re-derived.
*/
import type { DesktopTheme, DesktopThemeTypography } from './types'
@@ -19,117 +33,556 @@ const SYSTEM_MONO = 'Menlo, Monaco, "SF Mono", "Courier Prime", monospace, ' + E
export const DEFAULT_TYPOGRAPHY: DesktopThemeTypography = { fontSans: SYSTEM_SANS, fontMono: SYSTEM_MONO }
const NOUS_BLUE = '#0053FD'
const PSYCHE_BLUE = '#1540B1'
const PSYCHE_WARM = '#FFE6CB'
const nousTint = (pct: number) => `color-mix(in srgb, ${NOUS_BLUE} ${pct}%, #FFFFFF)`
const nousTintTransparent = (pct: number) => `color-mix(in srgb, ${NOUS_BLUE} ${pct}%, transparent)`
/**
* Nous — canonical Hermes desktop identity. The palette keeps the current
* glass geometry neutral, then lets the old bb/gui blue and psyche cream
* return as accent seeds.
* Nous — the canonical Hermes desktop identity, forked from the GitHub VS Code
* theme (github.github-vscode-theme). Light is GitHub Light Default, dark is
* GitHub Dark Default, both converted through the same path a Marketplace
* install takes, so the palette here is byte-identical to importing the
* extension yourself.
*
* Typography stays Hermes's own: a VS Code theme carries no font opinion, and
* these are the stacks every skin has been rendering with.
*/
export const nousTheme: DesktopTheme = {
name: 'nous',
label: 'Nous',
description: 'Glass neutrals with Nous blue accents',
/**
* GitHub — the upstream palette, unmodified.
*
* `nous` is a fork of this with its own accent, so shipping both keeps the
* original available on its own terms instead of only existing as the thing
* nous diverged from. Everything but the accent family is identical between
* them; separate presets are what let nous's accent move without silently
* redefining what "GitHub" means.
*/
export const githubTheme: DesktopTheme = {
name: 'github',
label: 'GitHub',
description: 'GitHub Light Default and Dark Default',
colors: {
background: '#F8FAFF',
foreground: '#17171A',
card: '#FFFFFF',
cardForeground: '#17171A',
muted: nousTint(5),
mutedForeground: '#666678',
popover: '#FFFFFF',
popoverForeground: '#17171A',
primary: NOUS_BLUE,
primaryForeground: '#FCFCFC',
secondary: nousTint(7),
secondaryForeground: '#242432',
accent: nousTint(10),
accentForeground: '#202030',
border: nousTintTransparent(22),
input: nousTintTransparent(30),
ring: NOUS_BLUE,
midground: NOUS_BLUE,
composerRing: NOUS_BLUE,
destructive: '#C72E4D',
destructiveForeground: '#FFFFFF',
sidebarBackground: '#F3F7FF',
sidebarBorder: nousTintTransparent(18),
userBubble: nousTint(6),
userBubbleBorder: nousTintTransparent(24)
background: '#ffffff',
foreground: '#1f2328',
card: '#f6f8fa',
cardForeground: '#1f2328',
muted: '#f6f6f6',
mutedForeground: '#656d76',
popover: '#ffffff',
popoverForeground: '#1f2328',
primary: '#196d31',
primaryForeground: '#ffffff',
secondary: '#dfebe2',
secondaryForeground: '#1f2328',
accent: '#e3ede6',
accentForeground: '#1f2328',
border: '#d0d7de',
input: '#ffffff',
ring: '#196d31',
midground: '#196d31',
midgroundForeground: '#ffffff',
composerRing: '#196d31',
destructive: '#cf222e',
destructiveForeground: '#ffffff',
sidebarBackground: '#f6f8fa',
sidebarBorder: '#d0d7de',
userBubble: '#dbe7e2',
userBubbleBorder: '#d0d7de'
},
darkColors: {
background: '#0D2F86',
foreground: PSYCHE_WARM,
card: '#12378F',
cardForeground: PSYCHE_WARM,
muted: '#183F9A',
mutedForeground: '#B5C7F3',
popover: '#123A96',
popoverForeground: PSYCHE_WARM,
primary: PSYCHE_WARM,
primaryForeground: '#0D2F86',
secondary: '#1B45A4',
secondaryForeground: '#E0E8FF',
accent: PSYCHE_BLUE,
accentForeground: '#F0F4FF',
border: '#3158AD',
input: '#0B2566',
ring: PSYCHE_WARM,
midground: NOUS_BLUE,
composerRing: PSYCHE_WARM,
destructive: '#C0473A',
destructiveForeground: '#FEF2F2',
sidebarBackground: '#09286F',
sidebarBorder: '#234A9C',
userBubble: '#143B91',
userBubbleBorder: '#3A63BD'
background: '#0d1117',
foreground: '#e6edf3',
card: '#010409',
cardForeground: '#e6edf3',
muted: '#1a1e24',
mutedForeground: '#7d8590',
popover: '#161b22',
popoverForeground: '#e6edf3',
primary: '#4f9e5e',
primaryForeground: '#ffffff',
secondary: '#1f382b',
secondaryForeground: '#e6edf3',
accent: '#192a24',
accentForeground: '#e6edf3',
border: '#30363d',
input: '#0d1117',
ring: '#4f9e5e',
midground: '#4f9e5e',
midgroundForeground: '#ffffff',
composerRing: '#4f9e5e',
destructive: '#f85149',
destructiveForeground: '#ffffff',
sidebarBackground: '#010409',
sidebarBorder: '#30363d',
userBubble: '#0f2018',
userBubbleBorder: '#30363d'
},
typography: {
fontSans: SYSTEM_SANS,
fontMono: SYSTEM_MONO,
fontUrl: 'https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap'
},
terminal: {
foreground: '#1f2328',
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'
},
darkTerminal: {
foreground: '#e6edf3',
black: '#484f58',
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: '#ffffff'
}
}
/** Deep blue-violet with cool accents. Matches the dashboard midnight theme. */
export const midnightTheme: DesktopTheme = {
name: 'midnight',
label: 'Midnight',
description: 'Deep blue-violet with cool accents',
/** Catppuccin — Latte in light, Mocha in dark (Catppuccin.catppuccin-vsc). */
/**
* Nous — the canonical Hermes desktop identity: GitHub's chrome carrying Nous
* blue. Forked from github.github-vscode-theme (Light Default / Dark Default),
* with only the accent family re-seeded; every neutral is upstream's.
*
* Two seeds, one blue. `#0053FD` is the brand color and reads at 5.4:1 on the
* light sidebar, but only 3.6:1 on the near-black dark one — so dark carries
* `#4a84fe`, the same hue (263°) lifted to clear AA at 5.9:1. The soft
* surfaces below are mixed from those seeds in OKLab, which is what keeps a
* saturated blue from drifting violet on its way to white.
*/
export const nousTheme: DesktopTheme = {
name: 'nous',
label: 'Nous',
description: 'GitHub chrome, Nous blue accent',
colors: {
background: '#08081c',
foreground: '#ddd6ff',
card: '#0d0d28',
cardForeground: '#ddd6ff',
muted: '#13133a',
mutedForeground: '#7c7ab0',
popover: '#0f0f2e',
popoverForeground: '#ddd6ff',
primary: '#ddd6ff',
primaryForeground: '#08081c',
secondary: '#1a1a4a',
secondaryForeground: '#c4bff0',
accent: '#1a1a44',
accentForeground: '#d0c8ff',
border: '#1e1e52',
input: '#1e1e52',
ring: '#8b80e8',
midground: '#8b80e8',
destructive: '#b03060',
destructiveForeground: '#fef2f2',
sidebarBackground: '#06061a',
sidebarBorder: '#12123a',
userBubble: '#14143a',
userBubbleBorder: '#242466'
background: '#ffffff',
foreground: '#1f2328',
card: '#f6f8fa',
cardForeground: '#1f2328',
muted: '#f6f6f6',
mutedForeground: '#656d76',
popover: '#ffffff',
popoverForeground: '#1f2328',
primary: '#0053fd',
primaryForeground: '#ffffff',
secondary: '#deeaff',
secondaryForeground: '#1f2328',
accent: '#e3edff',
accentForeground: '#1f2328',
border: '#d0d7de',
input: '#ffffff',
ring: '#0053fd',
midground: '#0053fd',
midgroundForeground: '#ffffff',
composerRing: '#0053fd',
destructive: '#cf222e',
destructiveForeground: '#ffffff',
sidebarBackground: '#f6f8fa',
sidebarBorder: '#d0d7de',
userBubble: '#dae7fd',
userBubbleBorder: '#d0d7de'
},
darkColors: {
background: '#0d1117',
foreground: '#e6edf3',
card: '#010409',
cardForeground: '#e6edf3',
muted: '#1a1e24',
mutedForeground: '#7d8590',
popover: '#161b22',
popoverForeground: '#e6edf3',
primary: '#4a84fe',
primaryForeground: '#161616',
secondary: '#1d2e4f',
secondaryForeground: '#e6edf3',
accent: '#17243a',
accentForeground: '#e6edf3',
border: '#30363d',
input: '#0d1117',
ring: '#4a84fe',
midground: '#4a84fe',
midgroundForeground: '#161616',
composerRing: '#4a84fe',
destructive: '#f85149',
destructiveForeground: '#ffffff',
sidebarBackground: '#010409',
sidebarBorder: '#30363d',
userBubble: '#07162c',
userBubbleBorder: '#30363d'
},
typography: {
fontMono: `"JetBrains Mono", ${SYSTEM_MONO}`,
fontUrl: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap'
fontSans: SYSTEM_SANS,
fontMono: SYSTEM_MONO,
fontUrl: 'https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap'
},
terminal: {
foreground: '#1f2328',
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'
},
darkTerminal: {
foreground: '#e6edf3',
black: '#484f58',
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: '#ffffff'
}
}
/** Catppuccin — Latte in light, Mocha in dark (Catppuccin.catppuccin-vsc). */
export const catppuccinTheme: DesktopTheme = {
name: 'catppuccin',
label: 'Catppuccin',
description: 'Soothing pastels — Latte and Mocha',
colors: {
background: '#eff1f5',
foreground: '#4c4f69',
card: '#e6e9ef',
cardForeground: '#4c4f69',
muted: '#e8ebef',
mutedForeground: '#4c4f69',
popover: '#e6e9ef',
popoverForeground: '#4c4f69',
primary: '#6d2ebf',
primaryForeground: '#ffffff',
secondary: '#ddd6ed',
secondaryForeground: '#4c4f69',
accent: '#dfdaef',
accentForeground: '#4c4f69',
border: '#acb0be',
input: '#ccd0da',
ring: '#6d2ebf',
midground: '#6d2ebf',
midgroundForeground: '#ffffff',
composerRing: '#6d2ebf',
destructive: '#d20f39',
destructiveForeground: '#ffffff',
sidebarBackground: '#e6e9ef',
sidebarBorder: '#acb0be',
userBubble: '#d7d3e9',
userBubbleBorder: '#acb0be'
},
darkColors: {
background: '#1e1e2e',
foreground: '#cdd6f4',
card: '#181825',
cardForeground: '#cdd6f4',
muted: '#29293a',
mutedForeground: '#cdd6f4',
popover: '#181825',
popoverForeground: '#cdd6f4',
primary: '#cba6f7',
primaryForeground: '#ffffff',
secondary: '#4e4466',
secondaryForeground: '#cdd6f4',
accent: '#3d3652',
accentForeground: '#cdd6f4',
border: '#585b70',
input: '#313244',
ring: '#cba6f7',
midground: '#cba6f7',
midgroundForeground: '#ffffff',
composerRing: '#cba6f7',
destructive: '#f38ba8',
destructiveForeground: '#ffffff',
sidebarBackground: '#181825',
sidebarBorder: '#585b70',
userBubble: '#38324b',
userBubbleBorder: '#585b70'
},
terminal: {
foreground: '#4c4f69',
cursor: '#dc8a78',
selectionBackground: '#acb0be',
black: '#5c5f77',
red: '#d20f39',
green: '#40a02b',
yellow: '#df8e1d',
blue: '#1e66f5',
magenta: '#ea76cb',
cyan: '#179299',
white: '#acb0be',
brightBlack: '#6c6f85',
brightRed: '#de293e',
brightGreen: '#49af3d',
brightYellow: '#eea02d',
brightBlue: '#456eff',
brightMagenta: '#fe85d8',
brightCyan: '#2d9fa8',
brightWhite: '#bcc0cc'
},
darkTerminal: {
foreground: '#cdd6f4',
cursor: '#f5e0dc',
selectionBackground: '#585b70',
black: '#45475a',
red: '#f38ba8',
green: '#a6e3a1',
yellow: '#f9e2af',
blue: '#89b4fa',
magenta: '#f5c2e7',
cyan: '#94e2d5',
white: '#a6adc8',
brightBlack: '#585b70',
brightRed: '#f37799',
brightGreen: '#89d88b',
brightYellow: '#ebd391',
brightBlue: '#74a8fc',
brightMagenta: '#f2aede',
brightCyan: '#6bd7ca',
brightWhite: '#bac2de'
}
}
/** Everforest — warm, low-contrast forest greens (sainnhe.everforest). */
export const everforestTheme: DesktopTheme = {
name: 'everforest',
label: 'Everforest',
description: 'Warm, low-contrast forest greens',
colors: {
background: '#fdf6e3',
foreground: '#5c6a72',
card: '#fdf6e3',
cardForeground: '#5c6a72',
muted: '#f7f0de',
mutedForeground: '#939f91',
popover: '#fdf6e3',
popoverForeground: '#5c6a72',
primary: '#586b35',
primaryForeground: '#ffffff',
secondary: '#e6e3cb',
secondaryForeground: '#5c6a72',
accent: '#e9e5ce',
accentForeground: '#5c6a72',
border: '#fdf6e3',
input: '#fdf6e3',
ring: '#586b35',
midground: '#586b35',
midgroundForeground: '#ffffff',
composerRing: '#586b35',
destructive: '#f1706f',
destructiveForeground: '#ffffff',
sidebarBackground: '#fdf6e3',
sidebarBorder: '#fdf6e3',
userBubble: '#e9e5ce',
userBubbleBorder: '#fdf6e3'
},
darkColors: {
background: '#2d353b',
foreground: '#d3c6aa',
card: '#2d353b',
cardForeground: '#d3c6aa',
muted: '#373e42',
mutedForeground: '#859289',
popover: '#2d353b',
popoverForeground: '#d3c6aa',
primary: '#a7c080',
primaryForeground: '#ffffff',
secondary: '#4f5c4e',
secondaryForeground: '#d3c6aa',
accent: '#434e47',
accentForeground: '#d3c6aa',
border: '#2d353b',
input: '#2d353b',
ring: '#a7c080',
midground: '#a7c080',
midgroundForeground: '#ffffff',
composerRing: '#a7c080',
destructive: '#da6362',
destructiveForeground: '#ffffff',
sidebarBackground: '#2d353b',
sidebarBorder: '#2d353b',
userBubble: '#434e47',
userBubbleBorder: '#2d353b'
},
terminal: {
foreground: '#5c6a72',
cursor: '#5c6a72',
black: '#5c6a72',
red: '#f85552',
green: '#8da101',
yellow: '#dfa000',
blue: '#3a94c5',
magenta: '#df69ba',
cyan: '#35a77c',
white: '#939f91',
brightBlack: '#5c6a72',
brightRed: '#f85552',
brightGreen: '#8da101',
brightYellow: '#dfa000',
brightBlue: '#3a94c5',
brightMagenta: '#df69ba',
brightCyan: '#35a77c',
brightWhite: '#f4f0d9'
},
darkTerminal: {
foreground: '#d3c6aa',
cursor: '#d3c6aa',
black: '#343f44',
red: '#e67e80',
green: '#a7c080',
yellow: '#dbbc7f',
blue: '#7fbbb3',
magenta: '#d699b6',
cyan: '#83c092',
white: '#d3c6aa',
brightBlack: '#859289',
brightRed: '#e67e80',
brightGreen: '#a7c080',
brightYellow: '#dbbc7f',
brightBlue: '#7fbbb3',
brightMagenta: '#d699b6',
brightCyan: '#83c092',
brightWhite: '#d3c6aa'
}
}
/** Solarized — Ethan Schoonover's fixed-contrast pair (ryanolsonx.solarized). */
export const solarizedTheme: DesktopTheme = {
name: 'solarized',
label: 'Solarized',
description: 'Fixed-contrast light and dark',
colors: {
background: '#fdf6e3',
foreground: '#1f1f1f',
card: '#d3cbb7',
cardForeground: '#1f1f1f',
muted: '#f4eddb',
mutedForeground: '#9ca8a6',
popover: '#eee8d5',
popoverForeground: '#1f1f1f',
primary: '#675e34',
primaryForeground: '#ffffff',
secondary: '#e8e1cb',
secondaryForeground: '#1f1f1f',
accent: '#ebe4ce',
accentForeground: '#1f1f1f',
border: '#ddd6c1',
input: '#ddd6c1',
ring: '#675e34',
midground: '#675e34',
midgroundForeground: '#ffffff',
composerRing: '#675e34',
destructive: '#e25563',
destructiveForeground: '#ffffff',
sidebarBackground: '#eee8d5',
sidebarBorder: '#ddd6c1',
userBubble: '#c6bea7',
userBubbleBorder: '#ddd6c1'
},
darkColors: {
background: '#002b36',
foreground: '#839496',
card: '#002b36',
cardForeground: '#839496',
muted: '#08313c',
mutedForeground: '#586e75',
popover: '#001f26',
popoverForeground: '#839496',
primary: '#6ea1c4',
primaryForeground: '#ffffff',
secondary: '#1f4c5e',
secondaryForeground: '#839496',
accent: '#144050',
accentForeground: '#839496',
border: '#234751',
input: '#073642',
ring: '#6ea1c4',
midground: '#6ea1c4',
midgroundForeground: '#ffffff',
composerRing: '#6ea1c4',
destructive: '#e35957',
destructiveForeground: '#ffffff',
sidebarBackground: '#001f26',
sidebarBorder: '#234751',
userBubble: '#144050',
userBubbleBorder: '#234751'
},
terminal: {
foreground: '#657b83',
black: '#657b83',
red: '#dc322f',
green: '#859900',
yellow: '#b58900',
blue: '#268bd2',
magenta: '#d33682',
cyan: '#2aa198',
white: '#eee8d5',
brightBlack: '#657b83',
brightRed: '#cb4b16',
brightGreen: '#859900',
brightYellow: '#657b83',
brightBlue: '#839496',
brightMagenta: '#6c71c4',
brightCyan: '#93a1a1',
brightWhite: '#eee8d5'
},
darkTerminal: {
foreground: '#839496',
cursor: '#ffffff',
selectionBackground: '#ffffff40',
black: '#14181d',
red: '#dc322f',
green: '#859900',
yellow: '#b58900',
blue: '#268bd2',
magenta: '#d33682',
cyan: '#2aa198',
white: '#e5e5e5',
brightBlack: '#676767',
brightRed: '#dc322f',
brightGreen: '#859900',
brightYellow: '#b58900',
brightBlue: '#268bd2',
brightMagenta: '#d33682',
brightCyan: '#2aa198',
brightWhite: '#e5e5e5'
}
}
@@ -278,11 +731,14 @@ export const slateTheme: DesktopTheme = {
export const BUILTIN_THEMES: Record<string, DesktopTheme> = {
nous: nousTheme,
midnight: midnightTheme,
github: githubTheme,
catppuccin: catppuccinTheme,
everforest: everforestTheme,
solarized: solarizedTheme,
ember: emberTheme,
mono: monoTheme,
cyberpunk: cyberpunkTheme,
slate: slateTheme
slate: slateTheme,
cyberpunk: cyberpunkTheme
}
export const BUILTIN_THEME_LIST = Object.values(BUILTIN_THEMES)
@@ -15,7 +15,7 @@ const cases = [
pref: skinPref as unknown as Pref,
fallback: DEFAULT_SKIN_NAME,
a: 'ember',
b: 'midnight',
b: 'catppuccin',
junk: 'nope'
},
{ name: 'mode', pref: modePref as unknown as Pref, fallback: 'light', a: 'dark', b: 'system', junk: 'dusk' }