diff --git a/packages/cli/src/ui/components/AboutBox.theme.test.tsx b/packages/cli/src/ui/components/AboutBox.theme.test.tsx new file mode 100644 index 000000000..de045579e --- /dev/null +++ b/packages/cli/src/ui/components/AboutBox.theme.test.tsx @@ -0,0 +1,60 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { render } from 'ink-testing-library'; +import type { BoxProps } from 'ink'; + +const recordedBoxProps: Array<{ backgroundColor?: string }> = []; + +const InstrumentedBox = (props: BoxProps & { children?: React.ReactNode }) => { + recordedBoxProps.push({ + backgroundColor: props.backgroundColor, + }); + return React.createElement('ink-box', props, props.children); +}; + +vi.mock('ink', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + Box: InstrumentedBox, + }; +}); + +const { Colors } = await import('../colors.js'); +const { AboutBox } = await import('./AboutBox.js'); + +describe('AboutBox theming', () => { + beforeEach(() => { + recordedBoxProps.length = 0; + }); + + it('sets the background color from the active theme', () => { + render( + , + ); + + const themedBox = recordedBoxProps.find( + (entry) => entry.backgroundColor === Colors.Background, + ); + + expect(themedBox).toBeDefined(); + }); +}); diff --git a/packages/cli/src/ui/components/AboutBox.tsx b/packages/cli/src/ui/components/AboutBox.tsx index d4d3c36ab..ab4fabcf1 100644 --- a/packages/cli/src/ui/components/AboutBox.tsx +++ b/packages/cli/src/ui/components/AboutBox.tsx @@ -41,6 +41,7 @@ export const AboutBox: React.FC = ({ padding={1} marginY={1} width="100%" + backgroundColor={Colors.Background} > diff --git a/packages/cli/src/ui/components/AuthDialog.theme.test.tsx b/packages/cli/src/ui/components/AuthDialog.theme.test.tsx new file mode 100644 index 000000000..f4beb1422 --- /dev/null +++ b/packages/cli/src/ui/components/AuthDialog.theme.test.tsx @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { renderWithProviders } from '../../test-utils/render.js'; +import { LoadedSettings } from '../../config/settings.js'; +import type { BoxProps } from 'ink'; + +const recordedBoxProps: Array<{ backgroundColor?: string }> = []; + +const InstrumentedBox = (props: BoxProps & { children?: React.ReactNode }) => { + recordedBoxProps.push({ + backgroundColor: props.backgroundColor, + }); + return React.createElement('ink-box', props, props.children); +}; + +vi.mock('ink', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + Box: InstrumentedBox, + }; +}); + +const { Colors } = await import('../colors.js'); +const { AuthDialog } = await import('./AuthDialog.js'); + +describe('AuthDialog theming', () => { + beforeEach(() => { + recordedBoxProps.length = 0; + }); + + it('sets the background color from the active theme', () => { + const settings = new LoadedSettings( + { settings: { ui: { customThemes: {} }, mcpServers: {} }, path: '' }, + { settings: {}, path: '' }, + { settings: {}, path: '' }, + { settings: { ui: { customThemes: {} }, mcpServers: {} }, path: '' }, + true, + ); + + renderWithProviders(); + + const themedBox = recordedBoxProps.find( + (entry) => entry.backgroundColor === Colors.Background, + ); + + expect(themedBox).toBeDefined(); + }); +}); diff --git a/packages/cli/src/ui/components/AuthDialog.tsx b/packages/cli/src/ui/components/AuthDialog.tsx index b0912c5f4..dd3936d71 100644 --- a/packages/cli/src/ui/components/AuthDialog.tsx +++ b/packages/cli/src/ui/components/AuthDialog.tsx @@ -146,6 +146,7 @@ export function AuthDialog({ flexDirection="column" padding={1} width="100%" + backgroundColor={Colors.Background} > OAuth Authentication diff --git a/packages/cli/src/ui/components/shared/ScrollableList.theme.test.tsx b/packages/cli/src/ui/components/shared/ScrollableList.theme.test.tsx new file mode 100644 index 000000000..e34120d32 --- /dev/null +++ b/packages/cli/src/ui/components/shared/ScrollableList.theme.test.tsx @@ -0,0 +1,74 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { render } from 'ink-testing-library'; +import { ScrollProvider } from '../../contexts/ScrollProvider.js'; + +const items = [{ id: 'one' }]; +const renderItem = () => <>; +const estimatedItemHeight = () => 1; +const keyExtractor = (item: { id: string }) => item.id; + +const recordedVirtualizedListProps: Array<{ scrollbarThumbColor?: string }> = + []; + +const mockVirtualizedList = React.forwardRef( + (props: { scrollbarThumbColor?: string }) => { + recordedVirtualizedListProps.push({ + scrollbarThumbColor: props.scrollbarThumbColor, + }); + return null; + }, +); + +mockVirtualizedList.displayName = 'VirtualizedList'; + +vi.mock('./VirtualizedList.js', () => ({ + VirtualizedList: mockVirtualizedList, + SCROLL_TO_ITEM_END: Number.MAX_SAFE_INTEGER, +})); + +vi.mock('ink', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + measureElement: vi.fn(() => ({ width: 10, height: 10 })), + }; +}); + +const { Colors } = await import('../../colors.js'); +const { ScrollableList } = await import('./ScrollableList.js'); + +describe('ScrollableList theming', () => { + it('uses theme colors for the scrollbar thumb based on focus', () => { + recordedVirtualizedListProps.length = 0; + + const renderList = (hasFocus: boolean) => + render( + + + , + ); + + renderList(true); + renderList(false); + + expect(recordedVirtualizedListProps).toContainEqual({ + scrollbarThumbColor: Colors.Foreground, + }); + expect(recordedVirtualizedListProps).toContainEqual({ + scrollbarThumbColor: Colors.Gray, + }); + }); +}); diff --git a/packages/cli/src/ui/components/shared/ScrollableList.tsx b/packages/cli/src/ui/components/shared/ScrollableList.tsx index 0b957c22a..915ebe0e4 100644 --- a/packages/cli/src/ui/components/shared/ScrollableList.tsx +++ b/packages/cli/src/ui/components/shared/ScrollableList.tsx @@ -19,6 +19,7 @@ import { } from './VirtualizedList.js'; import { useScrollable } from '../../contexts/ScrollProvider.js'; import { Box, type DOMElement } from 'ink'; +import { Colors } from '../../colors.js'; import { useKeypress, type Key } from '../../hooks/useKeypress.js'; import { keyMatchers, Command } from '../../keyMatchers.js'; @@ -132,7 +133,7 @@ function ScrollableList( useScrollable(scrollableEntry, hasFocus); - const scrollbarColor = hasFocus ? 'gray' : 'darkgray'; + const scrollbarColor = hasFocus ? Colors.Foreground : Colors.Gray; return ( <>; +const estimatedItemHeight = () => 1; +const keyExtractor = (item: { id: string }) => item.id; + +const recordedBoxProps: Array<{ scrollbarThumbColor?: string }> = []; + +const recordScrollbarThumb = (props: BoxProps) => { + if (props.scrollbarThumbColor) { + recordedBoxProps.push({ + scrollbarThumbColor: props.scrollbarThumbColor, + }); + } +}; + +const InstrumentedBox = (props: BoxProps & { children?: React.ReactNode }) => { + recordScrollbarThumb(props); + return React.createElement('ink-box', props, props.children); +}; + +vi.mock('ink', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + Box: InstrumentedBox, + measureElement: vi.fn(() => ({ width: 10, height: 10 })), + }; +}); + +vi.mock('../../hooks/useBatchedScroll.js', () => ({ + useBatchedScroll: () => ({ + getScrollTop: () => 0, + setPendingScrollTop: vi.fn(), + }), +})); + +const { Colors } = await import('../../colors.js'); +const { VirtualizedList } = await import('./VirtualizedList.js'); + +describe('VirtualizedList theming', () => { + beforeEach(() => { + recordedBoxProps.length = 0; + }); + + it('defaults the scrollbar thumb color to the theme gray', () => { + render( + , + ); + + const thumbEntry = recordedBoxProps.find( + (entry) => entry.scrollbarThumbColor === Colors.Gray, + ); + + expect(thumbEntry).toBeDefined(); + }); +}); diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.tsx index a3d7376e5..cdfb4befa 100644 --- a/packages/cli/src/ui/components/shared/VirtualizedList.tsx +++ b/packages/cli/src/ui/components/shared/VirtualizedList.tsx @@ -17,6 +17,7 @@ import { import type React from 'react'; import { useBatchedScroll } from '../../hooks/useBatchedScroll.js'; import { type DOMElement, measureElement, Box } from 'ink'; +import { Colors } from '../../colors.js'; export const SCROLL_TO_ITEM_END = Number.MAX_SAFE_INTEGER; @@ -469,7 +470,7 @@ function VirtualizedList( overflowY="scroll" overflowX="hidden" scrollTop={scrollTop} - scrollbarThumbColor={props.scrollbarThumbColor ?? 'gray'} + scrollbarThumbColor={props.scrollbarThumbColor ?? Colors.Gray} width="100%" height="100%" flexDirection="column"