diff --git a/apps/desktop/src/themes/context.test.tsx b/apps/desktop/src/themes/context.test.tsx index 2bc2011833..144eca0f6d 100644 --- a/apps/desktop/src/themes/context.test.tsx +++ b/apps/desktop/src/themes/context.test.tsx @@ -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', () => { diff --git a/apps/desktop/src/themes/context.tsx b/apps/desktop/src/themes/context.tsx index 2a575f8d48..831a17d9dd 100644 --- a/apps/desktop/src/themes/context.tsx +++ b/apps/desktop/src/themes/context.tsx @@ -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 = { '--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( () => ({ - theme: activeTheme, + theme: paintedTheme, themeName, mode, resolvedMode, @@ -465,7 +491,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { clearThemePreview }), [ - activeTheme, + paintedTheme, themeName, mode, resolvedMode, diff --git a/apps/desktop/src/themes/presets.ts b/apps/desktop/src/themes/presets.ts index 464381819f..6b46d5a596 100644 --- a/apps/desktop/src/themes/presets.ts +++ b/apps/desktop/src/themes/presets.ts @@ -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 = { 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) diff --git a/apps/desktop/src/themes/profile-theme.test.ts b/apps/desktop/src/themes/profile-theme.test.ts index ce4a46dc44..34ec517aa9 100644 --- a/apps/desktop/src/themes/profile-theme.test.ts +++ b/apps/desktop/src/themes/profile-theme.test.ts @@ -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' }