Merge pull request #534 from omdsh-dev/feat/palette-single-source

refactor(theme): One Dark/Light 语法色板单源化
This commit is contained in:
Atlas Zeng
2026-09-03 20:18:01 +08:00
committed by GitHub
4 changed files with 167 additions and 55 deletions
+14 -13
View File
@@ -37,6 +37,7 @@ import { FitAddon } from '@xterm/addon-fit'
import { writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives'
import '@xterm/xterm/css/xterm.css'
import { t } from './locales.ts'
import { ONE_DARK, ONE_LIGHT } from './one-dark-palette.ts'
import { openWhenSized } from './open-when-sized.ts'
import { api, type SessionScope, type TerminalDepsStatus } from './api.ts'
import { agentUuidOf, isAgentTabId, type SidebarStore } from './state.ts'
@@ -67,24 +68,24 @@ type TerminalDepsInfo = Extract<TerminalDepsStatus, { ok: false }>
* Curated ANSI palettes for the terminal. The surface colors (background,
* foreground, cursor, selection) ride the theme tokens so the terminal
* blends with the panel in both schemes; the 16 ANSI colors are the same
* designed palettes the app's code surfaces use (one-dark family for dark,
* one-light family for light), read live so a scheme flip re-themes in
* place.
* designed syntax families the app's code surfaces use — one-dark for
* dark, one-light for light (one-dark-palette.ts, shared with the
* CodeMirror themes) — read live so a scheme flip re-themes in place.
*/
const ANSI_DARK: Record<string, string> = {
black: '#282c34', red: '#e06c75', green: '#98c379', yellow: '#e5c07b',
blue: '#61afef', magenta: '#c678dd', cyan: '#56b6c2', white: '#abb2bf',
brightBlack: '#5c6370', brightRed: '#e06c75', brightGreen: '#98c379',
brightYellow: '#e5c07b', brightBlue: '#61afef', brightMagenta: '#c678dd',
brightCyan: '#56b6c2', brightWhite: '#ffffff',
black: ONE_DARK.black, red: ONE_DARK.red, green: ONE_DARK.green, yellow: ONE_DARK.yellow,
blue: ONE_DARK.blue, magenta: ONE_DARK.magenta, cyan: ONE_DARK.cyan, white: ONE_DARK.gray,
brightBlack: ONE_DARK.faintGray, brightRed: ONE_DARK.red, brightGreen: ONE_DARK.green,
brightYellow: ONE_DARK.yellow, brightBlue: ONE_DARK.blue, brightMagenta: ONE_DARK.magenta,
brightCyan: ONE_DARK.cyan, brightWhite: ONE_DARK.white,
}
const ANSI_LIGHT: Record<string, string> = {
black: '#383a42', red: '#e45649', green: '#50a14f', yellow: '#c18401',
blue: '#0184bc', magenta: '#a626a4', cyan: '#0997b3', white: '#a0a1a7',
brightBlack: '#4f525e', brightRed: '#e45649', brightGreen: '#50a14f',
brightYellow: '#c18401', brightBlue: '#0184bc', brightMagenta: '#a626a4',
brightCyan: '#0997b3', brightWhite: '#fafafa',
black: ONE_LIGHT.black, red: ONE_LIGHT.red, green: ONE_LIGHT.green, yellow: ONE_LIGHT.yellow,
blue: ONE_LIGHT.blue, magenta: ONE_LIGHT.magenta, cyan: ONE_LIGHT.cyan, white: ONE_LIGHT.gray,
brightBlack: ONE_LIGHT.faintGray, brightRed: ONE_LIGHT.red, brightGreen: ONE_LIGHT.green,
brightYellow: ONE_LIGHT.yellow, brightBlue: ONE_LIGHT.blue, brightMagenta: ONE_LIGHT.magenta,
brightCyan: ONE_LIGHT.cyan, brightWhite: ONE_LIGHT.offWhite,
}
/** The xterm theme for the current scheme (surface from tokens, ANSI curated). */
+44 -42
View File
@@ -2,15 +2,17 @@
* CodeMirror 6 theme pieces for the sidebar editor. The editor surface
* (background, caret, gutter) rides the DSH theme tokens so it blends with
* the panel in both schemes; only the syntax token colors need concrete
* values, and those come from the same designed palettes the app's code
* surfaces use — the one-dark family for dark, the one-light family for
* light. The scheme flip reconfigures these via a compartment (see
* TextEditor), so the document, undo history and scroll survive re-theming.
* values, and those come from the same designed syntax families the app's
* code surfaces use — one-dark for dark, one-light for light
* (one-dark-palette.ts, shared with the terminal's ANSI palette). The
* scheme flip reconfigures these via a compartment (see TextEditor), so
* the document, undo history and scroll survive re-theming.
*/
import { Compartment } from '@codemirror/state'
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
import { tags, type Tag } from '@lezer/highlight'
import { EditorView } from '@codemirror/view'
import { ONE_DARK, ONE_LIGHT } from './one-dark-palette.ts'
/** Token-driven surface shared by both schemes (pure CSS values). */
export const cmSurfaceTheme = EditorView.theme({
@@ -53,52 +55,52 @@ interface HighlightRule {
fontStyle?: string
}
/** one-dark syntax palette (mirrors @codemirror/theme-one-dark). */
/** one-dark syntax rules (mirrors @codemirror/theme-one-dark; hues from one-dark-palette.ts). */
const HIGHLIGHTS_DARK: HighlightRule[] = [
{ tag: tags.comment, color: '#5c6370', fontStyle: 'italic' },
{ tag: tags.keyword, color: '#c678dd' },
{ tag: tags.string, color: '#98c379' },
{ tag: tags.number, color: '#d19a66' },
{ tag: tags.bool, color: '#d19a66' },
{ tag: tags.atom, color: '#d19a66' },
{ tag: tags.typeName, color: '#e5c07b' },
{ tag: tags.className, color: '#e5c07b' },
{ tag: tags.propertyName, color: '#e06c75' },
{ tag: tags.function(tags.variableName), color: '#61afef' },
{ tag: tags.variableName, color: '#e06c75' },
{ tag: tags.operator, color: '#56b6c2' },
{ tag: tags.tagName, color: '#e06c75' },
{ tag: tags.attributeName, color: '#d19a66' },
{ tag: tags.heading, color: '#e06c75', fontStyle: 'bold' },
{ tag: tags.comment, color: ONE_DARK.faintGray, fontStyle: 'italic' },
{ tag: tags.keyword, color: ONE_DARK.magenta },
{ tag: tags.string, color: ONE_DARK.green },
{ tag: tags.number, color: ONE_DARK.orange },
{ tag: tags.bool, color: ONE_DARK.orange },
{ tag: tags.atom, color: ONE_DARK.orange },
{ tag: tags.typeName, color: ONE_DARK.yellow },
{ tag: tags.className, color: ONE_DARK.yellow },
{ tag: tags.propertyName, color: ONE_DARK.red },
{ tag: tags.function(tags.variableName), color: ONE_DARK.blue },
{ tag: tags.variableName, color: ONE_DARK.red },
{ tag: tags.operator, color: ONE_DARK.cyan },
{ tag: tags.tagName, color: ONE_DARK.red },
{ tag: tags.attributeName, color: ONE_DARK.orange },
{ tag: tags.heading, color: ONE_DARK.red, fontStyle: 'bold' },
{ tag: tags.emphasis, fontStyle: 'italic' },
{ tag: tags.strong, fontStyle: 'bold' },
{ tag: tags.link, color: '#61afef', fontStyle: 'underline' },
{ tag: tags.meta, color: '#e5c07b' },
{ tag: tags.invalid, color: '#ffffff', fontStyle: 'bold' },
{ tag: tags.link, color: ONE_DARK.blue, fontStyle: 'underline' },
{ tag: tags.meta, color: ONE_DARK.yellow },
{ tag: tags.invalid, color: ONE_DARK.white, fontStyle: 'bold' },
]
/** one-light syntax palette (the light counterpart of one-dark). */
/** one-light syntax rules (the light counterpart; hues from one-dark-palette.ts). */
const HIGHLIGHTS_LIGHT: HighlightRule[] = [
{ tag: tags.comment, color: '#a0a1a7', fontStyle: 'italic' },
{ tag: tags.keyword, color: '#a626a4' },
{ tag: tags.string, color: '#50a14f' },
{ tag: tags.number, color: '#986801' },
{ tag: tags.bool, color: '#0184bc' },
{ tag: tags.atom, color: '#0184bc' },
{ tag: tags.typeName, color: '#c18401' },
{ tag: tags.className, color: '#c18401' },
{ tag: tags.propertyName, color: '#e45649' },
{ tag: tags.function(tags.variableName), color: '#c18401' },
{ tag: tags.variableName, color: '#e45649' },
{ tag: tags.operator, color: '#383a42' },
{ tag: tags.tagName, color: '#e45649' },
{ tag: tags.attributeName, color: '#986801' },
{ tag: tags.heading, color: '#e45649', fontStyle: 'bold' },
{ tag: tags.comment, color: ONE_LIGHT.gray, fontStyle: 'italic' },
{ tag: tags.keyword, color: ONE_LIGHT.magenta },
{ tag: tags.string, color: ONE_LIGHT.green },
{ tag: tags.number, color: ONE_LIGHT.orange },
{ tag: tags.bool, color: ONE_LIGHT.blue },
{ tag: tags.atom, color: ONE_LIGHT.blue },
{ tag: tags.typeName, color: ONE_LIGHT.yellow },
{ tag: tags.className, color: ONE_LIGHT.yellow },
{ tag: tags.propertyName, color: ONE_LIGHT.red },
{ tag: tags.function(tags.variableName), color: ONE_LIGHT.yellow },
{ tag: tags.variableName, color: ONE_LIGHT.red },
{ tag: tags.operator, color: ONE_LIGHT.black },
{ tag: tags.tagName, color: ONE_LIGHT.red },
{ tag: tags.attributeName, color: ONE_LIGHT.orange },
{ tag: tags.heading, color: ONE_LIGHT.red, fontStyle: 'bold' },
{ tag: tags.emphasis, fontStyle: 'italic' },
{ tag: tags.strong, fontStyle: 'bold' },
{ tag: tags.link, color: '#4078f2', fontStyle: 'underline' },
{ tag: tags.meta, color: '#c18401' },
{ tag: tags.invalid, color: '#ffffff', fontStyle: 'bold' },
{ tag: tags.link, color: ONE_LIGHT.link, fontStyle: 'underline' },
{ tag: tags.meta, color: ONE_LIGHT.yellow },
{ tag: tags.invalid, color: ONE_LIGHT.white, fontStyle: 'bold' },
]
/** The scheme-dependent extension pair (surface tint + syntax highlight). */
+62
View File
@@ -0,0 +1,62 @@
/**
* The single source for the syntax color families the app's code surfaces
* use: one-dark for the dark scheme, one-light for the light scheme. Two
* lazy-chunk consumers assemble their theme objects from these constants —
* the terminal's curated ANSI 16 (TerminalView.tsx) and CodeMirror's
* highlight rules (cm-themes.ts) — so a family color is defined exactly
* once and the two views can never drift apart.
*
* The palette holds only the syntax hues; surface colors (background,
* foreground, caret, gutter) are theme-token driven and selection tints /
* token-fallback colors stay with their consumers (they are not part of
* the designed syntax families).
*
* Chunk boundary: this module is inlined into BOTH lazy chunks that import
* it (terminal and editor) — the constants are plain strings, the per-chunk
* copy is a few hundred bytes, and no cross-chunk import is introduced. The
* core bundle does not (and must not) import this module.
*/
/** one-dark family (dark scheme) syntax hues. */
export const ONE_DARK = {
/** Editor-background tone; ANSI black. */
black: '#282c34',
/** Default foreground gray; ANSI white. */
gray: '#abb2bf',
/** Comment gray; ANSI bright black. */
faintGray: '#5c6370',
/** Pure white; ANSI bright white and the invalid-token color. */
white: '#ffffff',
red: '#e06c75',
green: '#98c379',
yellow: '#e5c07b',
blue: '#61afef',
magenta: '#c678dd',
cyan: '#56b6c2',
/** Numbers, bools, atoms, attribute names. */
orange: '#d19a66',
} as const
/** one-light family (light scheme) syntax hues. */
export const ONE_LIGHT = {
/** Foreground dark; ANSI black and the operator color. */
black: '#383a42',
/** Comment gray; ANSI white. */
gray: '#a0a1a7',
/** ANSI bright black. */
faintGray: '#4f525e',
/** Pure white; the invalid-token color (stays white in light for contrast). */
white: '#ffffff',
/** Off-white; ANSI bright white. */
offWhite: '#fafafa',
red: '#e45649',
green: '#50a14f',
yellow: '#c18401',
blue: '#0184bc',
magenta: '#a626a4',
cyan: '#0997b3',
/** Numbers and attribute names. */
orange: '#986801',
/** Markdown links — the light family's dedicated link blue (dark rides blue). */
link: '#4078f2',
} as const
+47
View File
@@ -0,0 +1,47 @@
/**
* Byte-level guard for the shared syntax color families
* (src/client/one-dark-palette.ts). The terminal's ANSI 16
* (TerminalView.tsx) and CodeMirror's highlight rules (cm-themes.ts) both
* assemble from these constants, so an accidental value edit here silently
* re-themes every code surface in both views. The expected hexes are the
* exact pre-single-sourcing literals from the two consumers — changing one
* must be a deliberate, reviewable act.
*/
import { describe, expect, it } from 'vitest'
import { ONE_DARK, ONE_LIGHT } from '../src/client/one-dark-palette.ts'
describe('one-dark-palette', () => {
it('pins the one-dark family (dark scheme) hues', () => {
expect(ONE_DARK).toEqual({
black: '#282c34',
gray: '#abb2bf',
faintGray: '#5c6370',
white: '#ffffff',
red: '#e06c75',
green: '#98c379',
yellow: '#e5c07b',
blue: '#61afef',
magenta: '#c678dd',
cyan: '#56b6c2',
orange: '#d19a66',
})
})
it('pins the one-light family (light scheme) hues', () => {
expect(ONE_LIGHT).toEqual({
black: '#383a42',
gray: '#a0a1a7',
faintGray: '#4f525e',
white: '#ffffff',
offWhite: '#fafafa',
red: '#e45649',
green: '#50a14f',
yellow: '#c18401',
blue: '#0184bc',
magenta: '#a626a4',
cyan: '#0997b3',
orange: '#986801',
link: '#4078f2',
})
})
})