From 88f572cfce310680affceeef4d8bfd075f2e4c66 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 16:19:00 -0400 Subject: [PATCH] refactor(tui): migrate selection views to V2 theme (#38001) --- .../tui/src/component/dialog-session-list.tsx | 13 ++-- packages/tui/src/component/dialog-stash.tsx | 5 +- .../tui/src/component/prompt/autocomplete.tsx | 24 ++++-- packages/tui/src/ui/dialog-select.tsx | 78 ++++++++++++------- 4 files changed, 77 insertions(+), 43 deletions(-) diff --git a/packages/tui/src/component/dialog-session-list.tsx b/packages/tui/src/component/dialog-session-list.tsx index 04783790e46..b1067fa8598 100644 --- a/packages/tui/src/component/dialog-session-list.tsx +++ b/packages/tui/src/component/dialog-session-list.tsx @@ -20,7 +20,7 @@ export function DialogSessionList() { const dialog = useDialog() const route = useRoute() const data = useData() - const { theme } = useTheme() + const { themeV2, mode } = useTheme().contextual("elevated") const client = useClient() const local = useLocal() const toast = useToast() @@ -109,12 +109,13 @@ export function DialogSessionList() { value: session.id, category, footer, - bg: deleting ? theme.error : undefined, + bg: deleting ? themeV2.background.action.destructive.focused : undefined, + fg: deleting ? themeV2.text.action.destructive.focused : undefined, gutter: data.session.family(session.id).some((id) => data.session.status(id) === "running") ? () => : slot === undefined ? undefined - : () => {slot}, + : () => {slot}, } } @@ -142,12 +143,14 @@ export function DialogSessionList() { }} emptyView={ - No sessions available + No sessions available } noMatchView={ - {searchState().message} + + {searchState().message} + } onMove={() => setToDelete(undefined)} diff --git a/packages/tui/src/component/dialog-stash.tsx b/packages/tui/src/component/dialog-stash.tsx index cefe315ee3a..80aa75250cb 100644 --- a/packages/tui/src/component/dialog-stash.tsx +++ b/packages/tui/src/component/dialog-stash.tsx @@ -29,7 +29,7 @@ function getStashPreview(input: string, maxLength: number = 50): string { export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { const dialog = useDialog() const stash = usePromptStash() - const { theme } = useTheme() + const { themeV2 } = useTheme().contextual("elevated") const shortcuts = Keymap.useShortcuts() const [toDelete, setToDelete] = createSignal() @@ -45,7 +45,8 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { title: isDeleting ? `Press ${shortcuts.get("stash.delete")} again to confirm` : getStashPreview(entry.prompt.text), - bg: isDeleting ? theme.error : undefined, + bg: isDeleting ? themeV2.background.action.destructive.focused : undefined, + fg: isDeleting ? themeV2.text.action.destructive.focused : undefined, value: index, description: getRelativeTime(entry.timestamp), footer: lineCount > 1 ? `~${lineCount} lines` : undefined, diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 5dd80ee3967..921d096819c 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -12,7 +12,7 @@ import { getScrollAcceleration } from "../../util/scroll" import { useTuiPaths } from "../../context/runtime" import { useConfig } from "../../config" import { useLocation } from "../../context/location" -import { useTheme, selectedForeground } from "../../context/theme" +import { useTheme } from "../../context/theme" import { SplitBorder } from "../../ui/border" import { useTerminalDimensions } from "@opentui/solid" import { Locale } from "../../util/locale" @@ -57,7 +57,7 @@ export function Autocomplete(props: { const data = useData() const keymap = Keymap.use() const keymapCommands = Keymap.useCommands() - const { theme } = useTheme() + const { themeV2 } = useTheme().contextual("overlay") const dimensions = useTerminalDimensions() const frecency = useFrecency() const config = useConfig().data @@ -698,11 +698,11 @@ export function Autocomplete(props: { width={position().width} zIndex={100} {...SplitBorder} - borderColor={theme.border} + borderColor={themeV2.border.default} > (scroll = r)} - backgroundColor={theme.backgroundMenu} + backgroundColor={themeV2.background.default} height={height()} scrollbarOptions={{ visible: false }} scrollAcceleration={scrollAcceleration()} @@ -711,7 +711,9 @@ export function Autocomplete(props: { each={options()} fallback={ - {emptyMessage()} + + {emptyMessage()} + } > @@ -719,7 +721,7 @@ export function Autocomplete(props: { { setStore("input", "mouse") @@ -734,11 +736,17 @@ export function Autocomplete(props: { }} onMouseUp={() => select()} > - + {option().display} - + {" " + option().description?.trimStart()} diff --git a/packages/tui/src/ui/dialog-select.tsx b/packages/tui/src/ui/dialog-select.tsx index 257de75b179..88248c92d34 100644 --- a/packages/tui/src/ui/dialog-select.tsx +++ b/packages/tui/src/ui/dialog-select.tsx @@ -1,6 +1,6 @@ import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core" import { Keymap, type KeymapCommand } from "../context/keymap" -import { useTheme, selectedForeground } from "../context/theme" +import { useTheme } from "../context/theme" import { entries, filter, flatMap, groupBy, pipe } from "remeda" import { batch, createEffect, createMemo, createSignal, For, Show, type JSX, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" @@ -74,6 +74,7 @@ export interface DialogSelectOption { categoryView?: JSX.Element disabled?: boolean bg?: RGBA + fg?: RGBA gutter?: () => JSX.Element margin?: JSX.Element onSelect?: (ctx: DialogContext) => void @@ -91,7 +92,7 @@ export function DialogSelect(props: DialogSelectProps) { type VisibleAction = (Action & { label: string }) | FooterHint const dialog = useDialog() - const { theme } = useTheme() + const { themeV2, mode } = useTheme().contextual("elevated") const config = useConfig().data const scrollAcceleration = createMemo(() => getScrollAcceleration(config)) @@ -517,29 +518,44 @@ export function DialogSelect(props: DialogSelectProps) { if (!isActionItem(action.item)) return ( - + {action.item.title}{" "} - {action.item.label} + {action.item.label} ) const item = action.item const active = createMemo(() => isActionFocused(item)) const disabled = createMemo(() => isActionDisabled(item)) - const fg = selectedForeground(theme) return ( trigger(item)} > {item.title} - {item.label} + + {" " + item.label} + ) } @@ -549,11 +565,11 @@ export function DialogSelect(props: DialogSelectProps) { {props.titleView ?? ( - + {props.title} )} - dialog.clear()}> + dialog.clear()}> esc @@ -567,9 +583,9 @@ export function DialogSelect(props: DialogSelectProps) { props.onFilter?.(e) }) }} - focusedBackgroundColor={theme.backgroundPanel} - cursorColor={theme.primary} - focusedTextColor={theme.textMuted} + focusedBackgroundColor={themeV2.background.formfield.focused} + cursorColor={themeV2.text.formfield.focused} + focusedTextColor={themeV2.text.formfield.focused} ref={(r) => { input = r input.traits = { status: "FILTER" } @@ -580,7 +596,7 @@ export function DialogSelect(props: DialogSelectProps) { }, 1) }} placeholder={props.placeholder ?? "Search"} - placeholderColor={theme.textMuted} + placeholderColor={themeV2.text.subdued} /> @@ -594,14 +610,14 @@ export function DialogSelect(props: DialogSelectProps) { fallback={ props.emptyView ?? ( - No items available + No items available ) } > {props.noMatchView ?? ( - No results found + No results found )} @@ -623,7 +639,10 @@ export function DialogSelect(props: DialogSelectProps) { + {category} } @@ -672,8 +691,8 @@ export function DialogSelect(props: DialogSelectProps) { backgroundColor={ active() ? actionFocused() - ? theme.backgroundElement - : (option.bg ?? theme.primary) + ? themeV2.background.surface.overlay + : (option.bg ?? themeV2.background.action.primary.focused) : RGBA.fromInts(0, 0, 0, 0) } > @@ -692,6 +711,7 @@ export function DialogSelect(props: DialogSelectProps) { active={active()} current={current()} muted={actionFocused()} + activeColor={option.fg} gutter={option.gutter} /> @@ -699,7 +719,7 @@ export function DialogSelect(props: DialogSelectProps) { {(detail) => ( {option.detailsWrap @@ -745,15 +765,15 @@ function Option(props: { titleWidth?: number truncateTitle?: boolean | "left" gutter?: () => JSX.Element + activeColor?: RGBA onMouseOver?: () => void }) { - const { theme } = useTheme() - const fg = selectedForeground(theme) + const { themeV2 } = useTheme().contextual("elevated") const text = createMemo(() => { - if (props.active && !props.muted) return fg - if (props.muted && (props.active || props.current)) return theme.textMuted - if (props.current) return theme.primary - return theme.text + if (props.active && !props.muted) return props.activeColor ?? themeV2.text.action.primary.focused + if (props.muted && (props.active || props.current)) return themeV2.text.subdued + if (props.current) return themeV2.text.formfield.selected + return themeV2.text.default }) return ( @@ -783,12 +803,14 @@ function Option(props: { ? Locale.truncateLeft(props.title, props.titleWidth ?? 61) : Locale.truncate(props.title, props.titleWidth ?? 61))} - {props.description} + + {" " + props.description} + - {props.footer} + {props.footer}