diff --git a/packages/cli/src/ui/components/ThemeDialog.test.tsx b/packages/cli/src/ui/components/ThemeDialog.test.tsx index aadf8d27d0..46b514c05b 100644 --- a/packages/cli/src/ui/components/ThemeDialog.test.tsx +++ b/packages/cli/src/ui/components/ThemeDialog.test.tsx @@ -4,13 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { render } from '../../test-utils/render.js'; +import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { ThemeDialog } from './ThemeDialog.js'; import { LoadedSettings } from '../../config/settings.js'; -import { KeypressProvider } from '../contexts/KeypressContext.js'; -import { SettingsContext } from '../contexts/SettingsContext.js'; import { DEFAULT_THEME, themeManager } from '../themes/theme-manager.js'; import { act } from 'react'; @@ -76,12 +74,9 @@ describe('ThemeDialog Snapshots', () => { it('should render correctly in theme selection mode', () => { const settings = createMockSettings(); - const { lastFrame } = render( - - - - - , + const { lastFrame } = renderWithProviders( + , + { settings }, ); expect(lastFrame()).toMatchSnapshot(); @@ -89,12 +84,9 @@ describe('ThemeDialog Snapshots', () => { it('should render correctly in scope selector mode', async () => { const settings = createMockSettings(); - const { lastFrame, stdin } = render( - - - - - , + const { lastFrame, stdin } = renderWithProviders( + , + { settings }, ); // Press Tab to switch to scope selector mode @@ -111,16 +103,13 @@ describe('ThemeDialog Snapshots', () => { it('should call onCancel when ESC is pressed', async () => { const mockOnCancel = vi.fn(); const settings = createMockSettings(); - const { stdin } = render( - - - - - , + const { stdin } = renderWithProviders( + , + { settings }, ); act(() => { @@ -131,4 +120,26 @@ describe('ThemeDialog Snapshots', () => { expect(mockOnCancel).toHaveBeenCalled(); }); }); + + it('should call refreshStatic when a theme is selected', async () => { + const mockRefreshStatic = vi.fn(); + const settings = createMockSettings(); + const { stdin } = renderWithProviders( + , + { + settings, + uiActions: { refreshStatic: mockRefreshStatic }, + }, + ); + + // Press Enter to select the theme + act(() => { + stdin.write('\r'); + }); + + await waitFor(() => { + expect(mockRefreshStatic).toHaveBeenCalled(); + expect(baseProps.onSelect).toHaveBeenCalled(); + }); + }); }); diff --git a/packages/cli/src/ui/components/ThemeDialog.tsx b/packages/cli/src/ui/components/ThemeDialog.tsx index f36d55a652..514873f64a 100644 --- a/packages/cli/src/ui/components/ThemeDialog.tsx +++ b/packages/cli/src/ui/components/ThemeDialog.tsx @@ -21,6 +21,7 @@ import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js'; import { ScopeSelector } from './shared/ScopeSelector.js'; +import { useUIActions } from '../contexts/UIActionsContext.js'; interface ThemeDialogProps { /** Callback function when a theme is selected */ @@ -46,6 +47,7 @@ export function ThemeDialog({ terminalWidth, }: ThemeDialogProps): React.JSX.Element { const isAlternateBuffer = useAlternateBuffer(); + const { refreshStatic } = useUIActions(); const [selectedScope, setSelectedScope] = useState( SettingScope.User, ); @@ -93,8 +95,9 @@ export function ThemeDialog({ const handleThemeSelect = useCallback( (themeName: string) => { onSelect(themeName, selectedScope); + refreshStatic(); }, - [onSelect, selectedScope], + [onSelect, selectedScope, refreshStatic], ); const handleThemeHighlight = (themeName: string) => { @@ -109,8 +112,9 @@ export function ThemeDialog({ const handleScopeSelect = useCallback( (scope: LoadableSettingScope) => { onSelect(highlightedThemeName, scope); + refreshStatic(); }, - [onSelect, highlightedThemeName], + [onSelect, highlightedThemeName, refreshStatic], ); const [mode, setMode] = useState<'theme' | 'scope'>('theme'); diff --git a/packages/cli/src/ui/components/__snapshots__/ThemeDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ThemeDialog.test.tsx.snap index 5bf4fbb931..08fd207cb1 100644 --- a/packages/cli/src/ui/components/__snapshots__/ThemeDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ThemeDialog.test.tsx.snap @@ -1,38 +1,38 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`ThemeDialog Snapshots > should render correctly in scope selector mode 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ > Apply To │ -│ ● 1. User Settings │ -│ 2. Workspace Settings │ -│ 3. System Settings │ -│ │ -│ (Use Enter to apply scope, Tab to select theme, Esc to close) │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ > Apply To │ +│ ● 1. User Settings │ +│ 2. Workspace Settings │ +│ 3. System Settings │ +│ │ +│ (Use Enter to apply scope, Tab to select theme, Esc to close) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`ThemeDialog Snapshots > should render correctly in theme selection mode 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ > Select Theme Preview │ -│ ▲ ┌─────────────────────────────────────────────────┐ │ -│ 1. ANSI Dark │ │ │ -│ 2. Atom One Dark │ 1 # function │ │ -│ 3. Ayu Dark │ 2 def fibonacci(n): │ │ -│ ● 4. Default Dark │ 3 a, b = 0, 1 │ │ -│ 5. Dracula Dark │ 4 for _ in range(n): │ │ -│ 6. GitHub Dark │ 5 a, b = b, a + b │ │ -│ 7. Shades Of Purple Dark │ 6 return a │ │ -│ 8. ANSI Light Light │ │ │ -│ 9. Ayu Light Light │ 1 - print("Hello, " + name) │ │ -│ 10. Default Light Light │ 1 + print(f"Hello, {name}!") │ │ -│ 11. GitHub Light Light │ │ │ -│ 12. Google Code Light └─────────────────────────────────────────────────┘ │ -│ ▼ │ -│ │ -│ (Use Enter to select, Tab to configure scope, Esc to close) │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ > Select Theme Preview │ +│ ▲ ┌────────────────────────────────────────────────────────────┐ │ +│ 1. ANSI Dark │ │ │ +│ 2. Atom One Dark │ 1 # function │ │ +│ 3. Ayu Dark │ 2 def fibonacci(n): │ │ +│ ● 4. Default Dark │ 3 a, b = 0, 1 │ │ +│ 5. Dracula Dark │ 4 for _ in range(n): │ │ +│ 6. GitHub Dark │ 5 a, b = b, a + b │ │ +│ 7. Holiday Dark │ 6 return a │ │ +│ 8. Shades Of Purple Dark │ │ │ +│ 9. ANSI Light Light │ 1 - print("Hello, " + name) │ │ +│ 10. Ayu Light Light │ 1 + print(f"Hello, {name}!") │ │ +│ 11. Default Light Light │ │ │ +│ 12. GitHub Light Light └────────────────────────────────────────────────────────────┘ │ +│ ▼ │ +│ │ +│ (Use Enter to select, Tab to configure scope, Esc to close) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" `; diff --git a/packages/cli/src/ui/themes/holiday.ts b/packages/cli/src/ui/themes/holiday.ts new file mode 100644 index 0000000000..b3e72b1cc1 --- /dev/null +++ b/packages/cli/src/ui/themes/holiday.ts @@ -0,0 +1,169 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { type ColorsTheme, Theme } from './theme.js'; +import { interpolateColor } from './color-utils.js'; + +const holidayColors: ColorsTheme = { + type: 'dark', + Background: '#00210e', + Foreground: '#F0F8FF', + LightBlue: '#B0E0E6', + AccentBlue: '#3CB371', + AccentPurple: '#FF9999', + AccentCyan: '#33F9FF', + AccentGreen: '#3CB371', + AccentYellow: '#FFEE8C', + AccentRed: '#FF6347', + DiffAdded: '#2E8B57', + DiffRemoved: '#CD5C5C', + Comment: '#8FBC8F', + Gray: '#D7F5D3', + DarkGray: interpolateColor('#D7F5D3', '#151B18', 0.5), + GradientColors: ['#FF0000', '#FFFFFF', '#008000'], +}; + +export const Holiday: Theme = new Theme( + 'Holiday', + 'dark', + { + hljs: { + display: 'block', + overflowX: 'auto', + padding: '0.5em', + background: holidayColors.Background, + color: holidayColors.Foreground, + }, + 'hljs-keyword': { + color: holidayColors.AccentBlue, + }, + 'hljs-literal': { + color: holidayColors.AccentBlue, + }, + 'hljs-symbol': { + color: holidayColors.AccentBlue, + }, + 'hljs-name': { + color: holidayColors.AccentBlue, + }, + 'hljs-link': { + color: holidayColors.AccentBlue, + textDecoration: 'underline', + }, + 'hljs-built_in': { + color: holidayColors.AccentCyan, + }, + 'hljs-type': { + color: holidayColors.AccentCyan, + }, + 'hljs-number': { + color: holidayColors.AccentGreen, + }, + 'hljs-class': { + color: holidayColors.AccentGreen, + }, + 'hljs-string': { + color: holidayColors.AccentYellow, + }, + 'hljs-meta-string': { + color: holidayColors.AccentYellow, + }, + 'hljs-regexp': { + color: holidayColors.AccentRed, + }, + 'hljs-template-tag': { + color: holidayColors.AccentRed, + }, + 'hljs-subst': { + color: holidayColors.Foreground, + }, + 'hljs-function': { + color: holidayColors.Foreground, + }, + 'hljs-title': { + color: holidayColors.Foreground, + }, + 'hljs-params': { + color: holidayColors.Foreground, + }, + 'hljs-formula': { + color: holidayColors.Foreground, + }, + 'hljs-comment': { + color: holidayColors.Comment, + fontStyle: 'italic', + }, + 'hljs-quote': { + color: holidayColors.Comment, + fontStyle: 'italic', + }, + 'hljs-doctag': { + color: holidayColors.Comment, + }, + 'hljs-meta': { + color: holidayColors.Gray, + }, + 'hljs-meta-keyword': { + color: holidayColors.Gray, + }, + 'hljs-tag': { + color: holidayColors.Gray, + }, + 'hljs-variable': { + color: holidayColors.AccentPurple, + }, + 'hljs-template-variable': { + color: holidayColors.AccentPurple, + }, + 'hljs-attr': { + color: holidayColors.LightBlue, + }, + 'hljs-attribute': { + color: holidayColors.LightBlue, + }, + 'hljs-builtin-name': { + color: holidayColors.LightBlue, + }, + 'hljs-section': { + color: holidayColors.AccentYellow, + }, + 'hljs-emphasis': { + fontStyle: 'italic', + }, + 'hljs-strong': { + fontWeight: 'bold', + }, + 'hljs-bullet': { + color: holidayColors.AccentYellow, + }, + 'hljs-selector-tag': { + color: holidayColors.AccentYellow, + }, + 'hljs-selector-id': { + color: holidayColors.AccentYellow, + }, + 'hljs-selector-class': { + color: holidayColors.AccentYellow, + }, + 'hljs-selector-attr': { + color: holidayColors.AccentYellow, + }, + 'hljs-selector-pseudo': { + color: holidayColors.AccentYellow, + }, + 'hljs-addition': { + backgroundColor: holidayColors.DiffAdded, + display: 'inline-block', + width: '100%', + }, + 'hljs-deletion': { + backgroundColor: holidayColors.DiffRemoved, + display: 'inline-block', + width: '100%', + }, + }, + holidayColors, +); diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts index 081224d9eb..68db10827f 100644 --- a/packages/cli/src/ui/themes/theme-manager.ts +++ b/packages/cli/src/ui/themes/theme-manager.ts @@ -11,6 +11,7 @@ import { Dracula } from './dracula.js'; import { GitHubDark } from './github-dark.js'; import { GitHubLight } from './github-light.js'; import { GoogleCode } from './googlecode.js'; +import { Holiday } from './holiday.js'; import { DefaultLight } from './default-light.js'; import { DefaultDark } from './default.js'; import { ShadesOfPurple } from './shades-of-purple.js'; @@ -51,6 +52,7 @@ class ThemeManager { GitHubDark, GitHubLight, GoogleCode, + Holiday, ShadesOfPurple, XCode, ANSI,