From 040b182b1c215da2e1ee57a36f2625a7c6f856fd Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Thu, 3 Sep 2026 16:46:04 +0800 Subject: [PATCH] =?UTF-8?q?refactor(theme):=20One=20Dark/Light=20=E8=AF=AD?= =?UTF-8?q?=E6=B3=95=E8=89=B2=E6=9D=BF=E5=8D=95=E6=BA=90=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 src/client/one-dark-palette.ts,作为 one-dark / one-light 两套语法 色系的唯一来源:TerminalView 的 ANSI 16 与 cm-themes 的 CodeMirror 高亮 规则改为从色板常量组装各自的映射(ANSI 槽位 / lezer tag 到色相的映射 仍留在各消费方——两边的角色映射本就不同)。 行为不变的纯重构:每个 hex/rgba 迁移前后逐字节一致(旧新片段在桩 tags 下求值深度比对相等;产物 hex 清单无删除、非色板 hex 计数零变化)。 TerminalView 的 token 兜底色与 cm-themes 的 selection/activeLine rgba 与语法色系无关,保留原处未合并。 chunk 边界:色板为纯常量,被 terminal / editor 两个懒 chunk 各自内联 (每 chunk 约 200B 字面量),不引入跨 chunk import;核心 bundle (client.js / client-registry.js)对该模块零引用(grep 产物验证)。 tests/one-dark-palette.spec.ts 钉住色板字面量防意外漂移。 --- src/client/TerminalView.tsx | 27 ++++++----- src/client/cm-themes.ts | 86 +++++++++++++++++----------------- src/client/one-dark-palette.ts | 62 ++++++++++++++++++++++++ tests/one-dark-palette.spec.ts | 47 +++++++++++++++++++ 4 files changed, 167 insertions(+), 55 deletions(-) create mode 100644 src/client/one-dark-palette.ts create mode 100644 tests/one-dark-palette.spec.ts diff --git a/src/client/TerminalView.tsx b/src/client/TerminalView.tsx index ffaee8a..5c47604 100644 --- a/src/client/TerminalView.tsx +++ b/src/client/TerminalView.tsx @@ -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 * 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 = { - 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 = { - 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). */ diff --git a/src/client/cm-themes.ts b/src/client/cm-themes.ts index 673c464..add96da 100644 --- a/src/client/cm-themes.ts +++ b/src/client/cm-themes.ts @@ -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). */ diff --git a/src/client/one-dark-palette.ts b/src/client/one-dark-palette.ts new file mode 100644 index 0000000..ac984b7 --- /dev/null +++ b/src/client/one-dark-palette.ts @@ -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 diff --git a/tests/one-dark-palette.spec.ts b/tests/one-dark-palette.spec.ts new file mode 100644 index 0000000..0c0b0b9 --- /dev/null +++ b/tests/one-dark-palette.spec.ts @@ -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', + }) + }) +})