Skip to content

Commit 592c00e

Browse files
Icarus603claude
andcommitted
feat: Customize TUI with brand colors and refined visual design
Comprehensive theme and UI refinements: Theme Colors: - Replace Claude orange brand color with blue (rgb(88,190,255)) - Add BRAND_COLOR, BRAND_COLOR_LIGHT, BRAND_RED, BRAND_GREEN constants - Update all 6 theme variants (light, dark, light-ansi, dark-ansi, light-daltonized, dark-daltonized) with consistent brand palette - Change permission/suggestion/remember colors to cyan-blue (rgb(131,210,238)) - Set dark theme userMessageBackground to #0f0f0f for better contrast - Update diff colors: burgundy red (rgb(162,0,67)) and forest green Syntax Highlighting: - Add new Royal Gold Dark theme with gold/blue palette and lower saturation - Replace Monokai Extended as default dark syntax theme - Fine-tune scope colors for refined code appearance on black backgrounds UI Behavior Changes: - Remove spinner stalled-to-red warning animation for cleaner UX - Remove syntax highlighting toggle (Ctrl+T) from ThemePicker - Remove top padding from FullscreenLayout ScrollBox - Set inline code color to amber gold (#FEC84A) in dark themes Keybindings: - Remove 'theme:toggleSyntaxHighlighting' action from schema - Remove Ctrl+T binding from ThemePicker context Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 918defa commit 592c00e

11 files changed

Lines changed: 142 additions & 206 deletions

File tree

packages/color-diff-napi/src/index.ts

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ type Theme = {
188188

189189
function defaultSyntaxThemeName(themeName: string): string {
190190
if (themeName.includes('ansi')) return 'ansi'
191-
if (themeName.includes('dark')) return 'Monokai Extended'
191+
if (themeName.includes('dark')) return 'Royal Gold Dark'
192192
return 'GitHub'
193193
}
194194

@@ -221,6 +221,35 @@ const MONOKAI_SCOPES: Record<string, Color> = {
221221
subst: rgb(248, 248, 242),
222222
}
223223

224+
// Custom dark theme for the TUI: lower saturation, richer gold accents, and
225+
// cooler blue-green contrast so code feels more refined on black backgrounds.
226+
const ROYAL_GOLD_DARK_SCOPES: Record<string, Color> = {
227+
keyword: rgb(254, 200, 74),
228+
_storage: rgb(135, 195, 255),
229+
built_in: rgb(135, 195, 255),
230+
type: rgb(135, 195, 255),
231+
literal: rgb(224, 164, 88),
232+
number: rgb(224, 164, 88),
233+
string: rgb(246, 224, 176),
234+
title: rgb(235, 200, 141),
235+
'title.function': rgb(235, 200, 141),
236+
'title.class': rgb(235, 200, 141),
237+
'title.class.inherited': rgb(235, 200, 141),
238+
params: rgb(243, 240, 232),
239+
comment: rgb(139, 125, 107),
240+
meta: rgb(139, 125, 107),
241+
attr: rgb(135, 195, 255),
242+
attribute: rgb(135, 195, 255),
243+
variable: rgb(243, 240, 232),
244+
'variable.language': rgb(243, 240, 232),
245+
property: rgb(243, 240, 232),
246+
operator: rgb(231, 185, 76),
247+
punctuation: rgb(229, 223, 211),
248+
symbol: rgb(224, 164, 88),
249+
regexp: rgb(246, 224, 176),
250+
subst: rgb(229, 223, 211),
251+
}
252+
224253
// highlight.js scope → syntect GitHub-light foreground (measured from Rust)
225254
const GITHUB_SCOPES: Record<string, Color> = {
226255
keyword: rgb(167, 29, 93),
@@ -286,6 +315,18 @@ const ANSI_SCOPES: Record<string, Color> = {
286315
meta: ansiIdx(8),
287316
}
288317

318+
// Brand colors for diff highlighting
319+
const BRAND_DIFF_RED = rgb(162, 0, 67)
320+
const BRAND_DIFF_GREEN = rgb(34, 139, 34)
321+
const BRAND_DIFF_RED_DARK_LINE = rgb(92, 0, 38)
322+
const BRAND_DIFF_RED_DARK_WORD = rgb(132, 0, 54)
323+
const BRAND_DIFF_GREEN_DARK_LINE = rgb(10, 74, 41)
324+
const BRAND_DIFF_GREEN_DARK_WORD = rgb(16, 110, 60)
325+
const BRAND_DIFF_RED_LIGHT_LINE = rgb(242, 220, 230)
326+
const BRAND_DIFF_RED_LIGHT_WORD = rgb(228, 170, 196)
327+
const BRAND_DIFF_GREEN_LIGHT_LINE = rgb(220, 238, 220)
328+
const BRAND_DIFF_GREEN_LIGHT_WORD = rgb(170, 214, 170)
329+
289330
function buildTheme(themeName: string, mode: ColorMode): Theme {
290331
const isDark = themeName.includes('dark')
291332
const isAnsi = themeName.includes('ansi')
@@ -308,57 +349,57 @@ function buildTheme(themeName: string, mode: ColorMode): Theme {
308349

309350
if (isDark) {
310351
const fg = rgb(248, 248, 242)
311-
const deleteLine = rgb(61, 1, 0)
312-
const deleteWord = rgb(92, 2, 0)
313-
const deleteDecoration = rgb(220, 90, 90)
352+
const deleteLine = BRAND_DIFF_RED_DARK_LINE
353+
const deleteWord = BRAND_DIFF_RED_DARK_WORD
354+
const deleteDecoration = BRAND_DIFF_RED
314355
if (isDaltonized) {
315356
return {
316357
addLine: tc ? rgb(0, 27, 41) : ansiIdx(17),
317358
addWord: tc ? rgb(0, 48, 71) : ansiIdx(24),
318359
addDecoration: rgb(81, 160, 200),
319-
deleteLine,
320-
deleteWord,
321-
deleteDecoration,
360+
deleteLine: rgb(61, 1, 0),
361+
deleteWord: rgb(92, 2, 0),
362+
deleteDecoration: rgb(220, 90, 90),
322363
foreground: fg,
323364
background: DEFAULT_BG,
324-
scopes: MONOKAI_SCOPES,
365+
scopes: ROYAL_GOLD_DARK_SCOPES,
325366
}
326367
}
327368
return {
328-
addLine: tc ? rgb(2, 40, 0) : ansiIdx(22),
329-
addWord: tc ? rgb(4, 71, 0) : ansiIdx(28),
330-
addDecoration: rgb(80, 200, 80),
369+
addLine: tc ? BRAND_DIFF_GREEN_DARK_LINE : BRAND_DIFF_GREEN_DARK_LINE,
370+
addWord: tc ? BRAND_DIFF_GREEN_DARK_WORD : BRAND_DIFF_GREEN_DARK_WORD,
371+
addDecoration: BRAND_DIFF_GREEN,
331372
deleteLine,
332373
deleteWord,
333374
deleteDecoration,
334375
foreground: fg,
335376
background: DEFAULT_BG,
336-
scopes: MONOKAI_SCOPES,
377+
scopes: ROYAL_GOLD_DARK_SCOPES,
337378
}
338379
}
339380

340381
// light
341382
const fg = rgb(51, 51, 51)
342-
const deleteLine = rgb(255, 220, 220)
343-
const deleteWord = rgb(255, 199, 199)
344-
const deleteDecoration = rgb(207, 34, 46)
383+
const deleteLine = BRAND_DIFF_RED_LIGHT_LINE
384+
const deleteWord = BRAND_DIFF_RED_LIGHT_WORD
385+
const deleteDecoration = BRAND_DIFF_RED
345386
if (isDaltonized) {
346387
return {
347-
addLine: rgb(219, 237, 255),
348-
addWord: rgb(179, 217, 255),
349-
addDecoration: rgb(36, 87, 138),
350-
deleteLine,
351-
deleteWord,
352-
deleteDecoration,
388+
addLine: BRAND_DIFF_GREEN_LIGHT_LINE,
389+
addWord: BRAND_DIFF_GREEN_LIGHT_WORD,
390+
addDecoration: BRAND_DIFF_GREEN,
391+
deleteLine: rgb(255, 220, 220),
392+
deleteWord: rgb(255, 199, 199),
393+
deleteDecoration: rgb(207, 34, 46),
353394
foreground: fg,
354395
background: DEFAULT_BG,
355396
scopes: GITHUB_SCOPES,
356397
}
357398
}
358399
return {
359-
addLine: rgb(220, 255, 220),
360-
addWord: rgb(178, 255, 178),
361-
addDecoration: rgb(36, 138, 61),
400+
addLine: BRAND_DIFF_GREEN_LIGHT_LINE,
401+
addWord: BRAND_DIFF_GREEN_LIGHT_WORD,
402+
addDecoration: BRAND_DIFF_GREEN,
362403
deleteLine,
363404
deleteWord,
364405
deleteDecoration,

src/components/FullscreenLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ export function FullscreenLayout({
398398
ref={scrollRef}
399399
flexGrow={1}
400400
flexDirection="column"
401-
paddingTop={padCollapsed ? 0 : 1}
401+
paddingTop={0}
402402
stickyScroll
403403
>
404404
<ScrollChromeContext value={chromeCtx}>

src/components/HighlightedCode.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as React from 'react'
22
import { memo, useEffect, useMemo, useRef, useState } from 'react'
3-
import { useSettings } from '../hooks/useSettings.js'
43
import {
54
Ansi,
65
Box,
@@ -34,20 +33,14 @@ export const HighlightedCode = memo(function HighlightedCode({
3433
const ref = useRef<DOMElement>(null)
3534
const [measuredWidth, setMeasuredWidth] = useState(width || DEFAULT_WIDTH)
3635
const [theme] = useTheme()
37-
const settings = useSettings()
38-
const syntaxHighlightingDisabled =
39-
settings.syntaxHighlightingDisabled ?? false
4036

4137
const colorFile = useMemo(() => {
42-
if (syntaxHighlightingDisabled) {
43-
return null
44-
}
4538
const ColorFile = expectColorFile()
4639
if (!ColorFile) {
4740
return null
4841
}
4942
return new ColorFile(code, filePath)
50-
}, [code, filePath, syntaxHighlightingDisabled])
43+
}, [code, filePath])
5144

5245
useEffect(() => {
5346
if (!width && ref.current) {
@@ -69,7 +62,7 @@ export const HighlightedCode = memo(function HighlightedCode({
6962
// line number (max_digits = lineCount.toString().length) + space. No marker
7063
// column like the diff path. Wrap in <NoSelect> so fullscreen selection
7164
// yields clean code without line numbers. Only split in fullscreen mode
72-
// (~ DOM nodes + sliceAnsi cost); non-fullscreen uses terminal-native
65+
// (~4x DOM nodes + sliceAnsi cost); non-fullscreen uses terminal-native
7366
// selection where noSelect is meaningless.
7467
const gutterWidth = useMemo(() => {
7568
if (!isFullscreenEnvEnabled()) return 0
@@ -96,7 +89,7 @@ export const HighlightedCode = memo(function HighlightedCode({
9689
code={code}
9790
filePath={filePath}
9891
dim={dim}
99-
skipColoring={syntaxHighlightingDisabled}
92+
skipColoring={false}
10093
/>
10194
)}
10295
</Box>

src/components/Spinner/GlimmerMessage.tsx

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,13 @@ type Props = {
1616
stalledIntensity?: number
1717
}
1818

19-
const ERROR_RED = { r: 171, g: 43, b: 63 }
20-
2119
export function GlimmerMessage({
2220
message,
2321
mode,
2422
messageColor,
2523
glimmerIndex,
2624
flashOpacity,
2725
shimmerColor,
28-
stalledIntensity = 0,
2926
}: Props): React.ReactNode {
3027
const [themeName] = useTheme()
3128
const theme = getTheme(themeName)
@@ -43,36 +40,6 @@ export function GlimmerMessage({
4340

4441
if (!message) return null
4542

46-
// When stalled, show text that smoothly transitions to red
47-
if (stalledIntensity > 0) {
48-
const baseColorStr = theme[messageColor]
49-
const baseRGB = baseColorStr ? parseRGB(baseColorStr) : null
50-
51-
if (baseRGB) {
52-
const interpolated = interpolateColor(
53-
baseRGB,
54-
ERROR_RED,
55-
stalledIntensity,
56-
)
57-
const color = toRGBColor(interpolated)
58-
return (
59-
<>
60-
<Text color={color}>{message}</Text>
61-
<Text color={color}> </Text>
62-
</>
63-
)
64-
}
65-
66-
// Fallback for ANSI themes: use messageColor until fully stalled, then error
67-
const color = stalledIntensity > 0.5 ? 'error' : messageColor
68-
return (
69-
<>
70-
<Text color={color}>{message}</Text>
71-
<Text color={color}> </Text>
72-
</>
73-
)
74-
}
75-
7643
// tool-use mode: all chars flash with the same opacity, so render as a
7744
// single <Text> instead of N individual FlashingChar components.
7845
if (mode === 'tool-use') {

src/components/Spinner/SpinnerAnimationRow.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,6 @@ export function SpinnerAnimationRow({
334334
<SpinnerGlyph
335335
frame={frame}
336336
messageColor={messageColor}
337-
stalledIntensity={overrideColor ? 0 : stalledIntensity}
338337
reducedMotion={reducedMotion}
339338
time={time}
340339
/>
@@ -345,7 +344,6 @@ export function SpinnerAnimationRow({
345344
glimmerIndex={glimmerIndex}
346345
flashOpacity={flashOpacity}
347346
shimmerColor={shimmerColor}
348-
stalledIntensity={overrideColor ? 0 : stalledIntensity}
349347
/>
350348
{status}
351349
</Box>

src/components/Spinner/SpinnerGlyph.tsx

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import * as React from 'react'
2-
import { Box, Text, useTheme } from '../../ink.js'
3-
import { getTheme, type Theme } from '../../utils/theme.js'
4-
import {
5-
getDefaultCharacters,
6-
interpolateColor,
7-
parseRGB,
8-
toRGBColor,
9-
} from './utils.js'
2+
import { Box, Text } from '../../ink.js'
3+
import type { Theme } from '../../utils/theme.js'
4+
import { getDefaultCharacters } from './utils.js'
105

116
const DEFAULT_CHARACTERS = getDefaultCharacters()
127

@@ -17,7 +12,6 @@ const SPINNER_FRAMES = [
1712

1813
const REDUCED_MOTION_DOT = '●'
1914
const REDUCED_MOTION_CYCLE_MS = 2000 // 2-second cycle: 1s visible, 1s dim
20-
const ERROR_RED = { r: 171, g: 43, b: 63 }
2115

2216
type Props = {
2317
frame: number
@@ -30,13 +24,9 @@ type Props = {
3024
export function SpinnerGlyph({
3125
frame,
3226
messageColor,
33-
stalledIntensity = 0,
3427
reducedMotion = false,
3528
time = 0,
3629
}: Props): React.ReactNode {
37-
const [themeName] = useTheme()
38-
const theme = getTheme(themeName)
39-
4030
// Reduced motion: slowly flashing orange dot
4131
if (reducedMotion) {
4232
const isDim = Math.floor(time / (REDUCED_MOTION_CYCLE_MS / 2)) % 2 === 1
@@ -51,33 +41,6 @@ export function SpinnerGlyph({
5141

5242
const spinnerChar = SPINNER_FRAMES[frame % SPINNER_FRAMES.length]
5343

54-
// Smoothly interpolate from current color to red when stalled
55-
if (stalledIntensity > 0) {
56-
const baseColorStr = theme[messageColor]
57-
const baseRGB = baseColorStr ? parseRGB(baseColorStr) : null
58-
59-
if (baseRGB) {
60-
const interpolated = interpolateColor(
61-
baseRGB,
62-
ERROR_RED,
63-
stalledIntensity,
64-
)
65-
return (
66-
<Box flexWrap="wrap" height={1} width={2}>
67-
<Text color={toRGBColor(interpolated)}>{spinnerChar}</Text>
68-
</Box>
69-
)
70-
}
71-
72-
// Fallback for ANSI themes
73-
const color = stalledIntensity > 0.5 ? 'error' : messageColor
74-
return (
75-
<Box flexWrap="wrap" height={1} width={2}>
76-
<Text color={color}>{spinnerChar}</Text>
77-
</Box>
78-
)
79-
}
80-
8144
return (
8245
<Box flexWrap="wrap" height={1} width={2}>
8346
<Text color={messageColor}>{spinnerChar}</Text>

0 commit comments

Comments
 (0)