feat(themes): dedicated code-syntax palette keys

Code highlighting reused brand tokens (accent/text/border/muted), so it couldn't
be themed independently. Add syntax_string/number/keyword/comment skin keys →
syntax* theme tokens (defaulting to those brand tokens, so defaults are
unchanged) and point the highlighter at them. Documented in the element→key map.
This commit is contained in:
Brooklyn Nicholson 2026-07-21 20:01:32 -05:00
parent 4f4f938aef
commit 3ba6eebc7c
6 changed files with 49 additions and 5 deletions

View file

@ -44,6 +44,10 @@ export const SKIN_COLOR_TOKENS = [
'diff_removed',
'diff_added_word',
'diff_removed_word',
'syntax_string',
'syntax_number',
'syntax_keyword',
'syntax_comment',
// CLI / TUI chrome.
'prompt',
'input_rule',

View file

@ -42,6 +42,10 @@ All fields are optional. Missing values inherit from the ``default`` skin.
diff_removed: "#ffdcdc" # Diff removed-line background
diff_added_word: "#248a3d" # Diff added word-level foreground
diff_removed_word: "#cf222e" # Diff removed word-level foreground
syntax_string: "#FFBF00" # Code strings; falls back to ui_accent
syntax_number: "#FFF8DC" # Code numbers; falls back to ui_text
syntax_keyword: "#CD7F32" # Code keywords; falls back to ui_border
syntax_comment: "#CC9B1F" # Code comments; falls back to banner_dim
prompt: "#FFF8DC" # Prompt text color
input_rule: "#CD7F32" # Input area horizontal rule
response_border: "#FFD700" # Response box border (ANSI)

View file

@ -61,6 +61,7 @@ key in its row (element-specific keys fall back to the shared one when unset).
| Success / warn / error | `ui_ok` / `ui_warn` / `ui_error` | — |
| Status bar text + usage | `status_bar_text`, `status_bar_good/warn/bad/critical` | — |
| Diff add/remove (line + word) | `diff_added` / `diff_removed` / `diff_added_word` / `diff_removed_word` | built-in |
| Code syntax (string/number/keyword/comment) | `syntax_string` / `syntax_number` / `syntax_keyword` / `syntax_comment` | accent/text/border/muted |
| Completion menu | `completion_menu_bg` / `completion_menu_current_bg` / `…_meta_bg` | — |
Note the sharing: `ui_accent` colors tool markers **and** headings/links/chevrons,

View file

@ -548,6 +548,23 @@ describe('background-aware adaptation (OSC-11 light terminals)', () => {
expect(fallback.color.thinking).toBe('#123456')
})
it('gives code syntax its own keys, defaulting to accent/text/border/muted', async () => {
const { fromSkin } = await importThemeWithCleanEnv()
const themed = fromSkin(
{ syntax_string: '#aa0000', syntax_number: '#00aa00', syntax_keyword: '#0000aa', syntax_comment: '#888888' },
{}
)
expect(themed.color.syntaxString).toBe('#aa0000')
expect(themed.color.syntaxNumber).toBe('#00aa00')
expect(themed.color.syntaxKeyword).toBe('#0000aa')
expect(themed.color.syntaxComment).toBe('#888888')
const fallback = fromSkin({ ui_accent: '#abcdef' }, {})
expect(fallback.color.syntaxString).toBe('#abcdef') // string follows accent
})
it('lets skins override diff colors', async () => {
const { fromSkin } = await importThemeWithCleanEnv()

View file

@ -80,7 +80,7 @@ export function highlightLine(line: string, lang: string, t: Theme): Token[] {
}
if (spec.comment && line.trimStart().startsWith(spec.comment)) {
return [[t.color.muted, line]]
return [[t.color.syntaxComment, line]]
}
const tokens: Token[] = []
@ -97,11 +97,11 @@ export function highlightLine(line: string, lang: string, t: Theme): Token[] {
const ch = tok[0]!
if (ch === '"' || ch === "'" || ch === '`') {
tokens.push([t.color.accent, tok])
tokens.push([t.color.syntaxString, tok])
} else if (ch >= '0' && ch <= '9') {
tokens.push([t.color.text, tok])
tokens.push([t.color.syntaxNumber, tok])
} else if (spec.keywords.has(tok)) {
tokens.push([t.color.border, tok])
tokens.push([t.color.syntaxKeyword, tok])
} else {
tokens.push(['', tok])
}

View file

@ -23,6 +23,12 @@ export interface ThemeColors {
/** Reasoning/thinking body text. Defaults to `muted`. */
thinking: string
/** Code-block syntax highlight. Default to accent/text/border/muted. */
syntaxString: string
syntaxNumber: string
syntaxKeyword: string
syntaxComment: string
prompt: string
sessionLabel: string
sessionBorder: string
@ -310,6 +316,13 @@ export function buildPalette(seeds: ThemeSeeds, isLight: boolean): ThemeColors {
tool: seeds.accent,
thinking: muted,
// Code-syntax tokens default to brand tokens (unchanged highlighting)
// but are independently skinnable.
syntaxString: seeds.accent,
syntaxNumber: seeds.text,
syntaxKeyword: seeds.border ?? tones.border,
syntaxComment: muted,
prompt: seeds.prompt ?? seeds.text,
// sessionLabel/sessionBorder track the muted tone — "same role, same
// colour" by design (#11300).
@ -855,7 +868,12 @@ export function fromSkin(
diffAdded: c('diff_added') ?? derived.diffAdded,
diffRemoved: c('diff_removed') ?? derived.diffRemoved,
diffAddedWord: c('diff_added_word') ?? derived.diffAddedWord,
diffRemovedWord: c('diff_removed_word') ?? derived.diffRemovedWord
diffRemovedWord: c('diff_removed_word') ?? derived.diffRemovedWord,
// Code-syntax tokens: overridable, else the derived brand-token defaults.
syntaxString: c('syntax_string') ?? derived.syntaxString,
syntaxNumber: c('syntax_number') ?? derived.syntaxNumber,
syntaxKeyword: c('syntax_keyword') ?? derived.syntaxKeyword,
syntaxComment: c('syntax_comment') ?? derived.syntaxComment
}
// 4. Guard: contrast floors against the real background + fill polarity.