refactor(tui): migrate selection views to V2 theme (#38001)

This commit is contained in:
James Long 2026-07-22 16:19:00 -04:00 committed by GitHub
commit 88f572cfce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 77 additions and 43 deletions

View file

@ -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<T = any> {
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<T>(props: DialogSelectProps<T>) {
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<T>(props: DialogSelectProps<T>) {
if (!isActionItem(action.item))
return (
<text>
<span style={{ fg: theme.text }}>
<span style={{ fg: themeV2.text.default }}>
<b>{action.item.title}</b>{" "}
</span>
<span style={{ fg: theme.textMuted }}>{action.item.label}</span>
<span style={{ fg: themeV2.text.subdued }}>{action.item.label}</span>
</text>
)
const item = action.item
const active = createMemo(() => isActionFocused(item))
const disabled = createMemo(() => isActionDisabled(item))
const fg = selectedForeground(theme)
return (
<box
flexDirection="row"
backgroundColor={active() ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
backgroundColor={active() ? themeV2.background.action.primary.focused : RGBA.fromInts(0, 0, 0, 0)}
onMouseUp={() => trigger(item)}
>
<text
fg={disabled() ? theme.textMuted : active() ? fg : theme.text}
fg={
disabled()
? themeV2.text.action.primary.disabled
: active()
? themeV2.text.action.primary.focused
: themeV2.text.default
}
attributes={active() ? TextAttributes.BOLD : undefined}
>
{item.title}
</text>
<text fg={disabled() ? theme.textMuted : active() ? fg : theme.textMuted}> {item.label}</text>
<text
fg={
disabled()
? themeV2.text.action.primary.disabled
: active()
? themeV2.text.action.primary.focused
: themeV2.text.subdued
}
>
{" " + item.label}
</text>
</box>
)
}
@ -549,11 +565,11 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
<box paddingLeft={4} paddingRight={4}>
<box flexDirection="row" justifyContent="space-between">
{props.titleView ?? (
<text fg={theme.text} attributes={TextAttributes.BOLD}>
<text fg={themeV2.text.default} attributes={TextAttributes.BOLD}>
{props.title}
</text>
)}
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@ -567,9 +583,9 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
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<T>(props: DialogSelectProps<T>) {
}, 1)
}}
placeholder={props.placeholder ?? "Search"}
placeholderColor={theme.textMuted}
placeholderColor={themeV2.text.subdued}
/>
</box>
</Show>
@ -594,14 +610,14 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
fallback={
props.emptyView ?? (
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
<text fg={theme.textMuted}>No items available</text>
<text fg={themeV2.text.subdued}>No items available</text>
</box>
)
}
>
{props.noMatchView ?? (
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
<text fg={theme.textMuted}>No results found</text>
<text fg={themeV2.text.subdued}>No results found</text>
</box>
)}
</Show>
@ -623,7 +639,10 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
<Show
when={options[0]?.categoryView}
fallback={
<text fg={theme.accent} attributes={TextAttributes.BOLD}>
<text
fg={themeV2.hue.accent[mode() === "light" ? 800 : 200]}
attributes={TextAttributes.BOLD}
>
{category}
</text>
}
@ -672,8 +691,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
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<T>(props: DialogSelectProps<T>) {
active={active()}
current={current()}
muted={actionFocused()}
activeColor={option.fg}
gutter={option.gutter}
/>
</box>
@ -699,7 +719,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
{(detail) => (
<box paddingLeft={3} paddingRight={3}>
<text
fg={option.detailsColor ?? theme.textMuted}
fg={option.detailsColor ?? themeV2.text.subdued}
wrapMode={option.detailsWrap ? "word" : "none"}
>
{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))}
<Show when={props.description}>
<span style={{ fg: props.active && !props.muted ? fg : theme.textMuted }}> {props.description}</span>
<span style={{ fg: props.active && !props.muted ? text() : themeV2.text.subdued }}>
{" " + props.description}
</span>
</Show>
</text>
<Show when={props.footer}>
<box flexShrink={0}>
<text fg={props.active && !props.muted ? fg : theme.textMuted}>{props.footer}</text>
<text fg={props.active && !props.muted ? text() : themeV2.text.subdued}>{props.footer}</text>
</box>
</Show>
</>