refactor(tui): migrate selection views to V2 theme (#38001)
This commit is contained in:
parent
2271f9b222
commit
88f572cfce
4 changed files with 77 additions and 43 deletions
|
|
@ -20,7 +20,7 @@ export function DialogSessionList() {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const { theme } = useTheme()
|
const { themeV2, mode } = useTheme().contextual("elevated")
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
@ -109,12 +109,13 @@ export function DialogSessionList() {
|
||||||
value: session.id,
|
value: session.id,
|
||||||
category,
|
category,
|
||||||
footer,
|
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")
|
gutter: data.session.family(session.id).some((id) => data.session.status(id) === "running")
|
||||||
? () => <Spinner />
|
? () => <Spinner />
|
||||||
: slot === undefined
|
: slot === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: () => <text fg={theme.accent}>{slot}</text>,
|
: () => <text fg={themeV2.hue.accent[mode() === "light" ? 800 : 200]}>{slot}</text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,12 +143,14 @@ export function DialogSessionList() {
|
||||||
}}
|
}}
|
||||||
emptyView={
|
emptyView={
|
||||||
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
||||||
<text fg={theme.textMuted}>No sessions available</text>
|
<text fg={themeV2.text.subdued}>No sessions available</text>
|
||||||
</box>
|
</box>
|
||||||
}
|
}
|
||||||
noMatchView={
|
noMatchView={
|
||||||
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
||||||
<text fg={searchState().error ? theme.error : theme.textMuted}>{searchState().message}</text>
|
<text fg={searchState().error ? themeV2.text.feedback.error.default : themeV2.text.subdued}>
|
||||||
|
{searchState().message}
|
||||||
|
</text>
|
||||||
</box>
|
</box>
|
||||||
}
|
}
|
||||||
onMove={() => setToDelete(undefined)}
|
onMove={() => setToDelete(undefined)}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ function getStashPreview(input: string, maxLength: number = 50): string {
|
||||||
export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
|
export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const stash = usePromptStash()
|
const stash = usePromptStash()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
const shortcuts = Keymap.useShortcuts()
|
const shortcuts = Keymap.useShortcuts()
|
||||||
|
|
||||||
const [toDelete, setToDelete] = createSignal<number>()
|
const [toDelete, setToDelete] = createSignal<number>()
|
||||||
|
|
@ -45,7 +45,8 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
|
||||||
title: isDeleting
|
title: isDeleting
|
||||||
? `Press ${shortcuts.get("stash.delete")} again to confirm`
|
? `Press ${shortcuts.get("stash.delete")} again to confirm`
|
||||||
: getStashPreview(entry.prompt.text),
|
: 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,
|
value: index,
|
||||||
description: getRelativeTime(entry.timestamp),
|
description: getRelativeTime(entry.timestamp),
|
||||||
footer: lineCount > 1 ? `~${lineCount} lines` : undefined,
|
footer: lineCount > 1 ? `~${lineCount} lines` : undefined,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { getScrollAcceleration } from "../../util/scroll"
|
||||||
import { useTuiPaths } from "../../context/runtime"
|
import { useTuiPaths } from "../../context/runtime"
|
||||||
import { useConfig } from "../../config"
|
import { useConfig } from "../../config"
|
||||||
import { useLocation } from "../../context/location"
|
import { useLocation } from "../../context/location"
|
||||||
import { useTheme, selectedForeground } from "../../context/theme"
|
import { useTheme } from "../../context/theme"
|
||||||
import { SplitBorder } from "../../ui/border"
|
import { SplitBorder } from "../../ui/border"
|
||||||
import { useTerminalDimensions } from "@opentui/solid"
|
import { useTerminalDimensions } from "@opentui/solid"
|
||||||
import { Locale } from "../../util/locale"
|
import { Locale } from "../../util/locale"
|
||||||
|
|
@ -57,7 +57,7 @@ export function Autocomplete(props: {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const keymap = Keymap.use()
|
const keymap = Keymap.use()
|
||||||
const keymapCommands = Keymap.useCommands()
|
const keymapCommands = Keymap.useCommands()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("overlay")
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const frecency = useFrecency()
|
const frecency = useFrecency()
|
||||||
const config = useConfig().data
|
const config = useConfig().data
|
||||||
|
|
@ -698,11 +698,11 @@ export function Autocomplete(props: {
|
||||||
width={position().width}
|
width={position().width}
|
||||||
zIndex={100}
|
zIndex={100}
|
||||||
{...SplitBorder}
|
{...SplitBorder}
|
||||||
borderColor={theme.border}
|
borderColor={themeV2.border.default}
|
||||||
>
|
>
|
||||||
<scrollbox
|
<scrollbox
|
||||||
ref={(r: ScrollBoxRenderable) => (scroll = r)}
|
ref={(r: ScrollBoxRenderable) => (scroll = r)}
|
||||||
backgroundColor={theme.backgroundMenu}
|
backgroundColor={themeV2.background.default}
|
||||||
height={height()}
|
height={height()}
|
||||||
scrollbarOptions={{ visible: false }}
|
scrollbarOptions={{ visible: false }}
|
||||||
scrollAcceleration={scrollAcceleration()}
|
scrollAcceleration={scrollAcceleration()}
|
||||||
|
|
@ -711,7 +711,9 @@ export function Autocomplete(props: {
|
||||||
each={options()}
|
each={options()}
|
||||||
fallback={
|
fallback={
|
||||||
<box paddingLeft={1} paddingRight={1}>
|
<box paddingLeft={1} paddingRight={1}>
|
||||||
<text fg={emptyError() ? theme.error : theme.textMuted}>{emptyMessage()}</text>
|
<text fg={emptyError() ? themeV2.text.feedback.error.default : themeV2.text.subdued}>
|
||||||
|
{emptyMessage()}
|
||||||
|
</text>
|
||||||
</box>
|
</box>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -719,7 +721,7 @@ export function Autocomplete(props: {
|
||||||
<box
|
<box
|
||||||
paddingLeft={1}
|
paddingLeft={1}
|
||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
backgroundColor={index === store.selected ? theme.primary : undefined}
|
backgroundColor={index === store.selected ? themeV2.background.action.primary.focused : undefined}
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
onMouseMove={() => {
|
onMouseMove={() => {
|
||||||
setStore("input", "mouse")
|
setStore("input", "mouse")
|
||||||
|
|
@ -734,11 +736,17 @@ export function Autocomplete(props: {
|
||||||
}}
|
}}
|
||||||
onMouseUp={() => select()}
|
onMouseUp={() => select()}
|
||||||
>
|
>
|
||||||
<text fg={index === store.selected ? selectedForeground(theme) : theme.text} flexShrink={0}>
|
<text
|
||||||
|
fg={index === store.selected ? themeV2.text.action.primary.focused : themeV2.text.default}
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
{option().display}
|
{option().display}
|
||||||
</text>
|
</text>
|
||||||
<Show when={option().description}>
|
<Show when={option().description}>
|
||||||
<text fg={index === store.selected ? selectedForeground(theme) : theme.textMuted} wrapMode="none">
|
<text
|
||||||
|
fg={index === store.selected ? themeV2.text.action.primary.focused : themeV2.text.subdued}
|
||||||
|
wrapMode="none"
|
||||||
|
>
|
||||||
{" " + option().description?.trimStart()}
|
{" " + option().description?.trimStart()}
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
|
import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
|
||||||
import { Keymap, type KeymapCommand } from "../context/keymap"
|
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 { entries, filter, flatMap, groupBy, pipe } from "remeda"
|
||||||
import { batch, createEffect, createMemo, createSignal, For, Show, type JSX, on, onCleanup } from "solid-js"
|
import { batch, createEffect, createMemo, createSignal, For, Show, type JSX, on, onCleanup } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
|
@ -74,6 +74,7 @@ export interface DialogSelectOption<T = any> {
|
||||||
categoryView?: JSX.Element
|
categoryView?: JSX.Element
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
bg?: RGBA
|
bg?: RGBA
|
||||||
|
fg?: RGBA
|
||||||
gutter?: () => JSX.Element
|
gutter?: () => JSX.Element
|
||||||
margin?: JSX.Element
|
margin?: JSX.Element
|
||||||
onSelect?: (ctx: DialogContext) => void
|
onSelect?: (ctx: DialogContext) => void
|
||||||
|
|
@ -91,7 +92,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||||
type VisibleAction = (Action & { label: string }) | FooterHint
|
type VisibleAction = (Action & { label: string }) | FooterHint
|
||||||
|
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const { theme } = useTheme()
|
const { themeV2, mode } = useTheme().contextual("elevated")
|
||||||
const config = useConfig().data
|
const config = useConfig().data
|
||||||
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
|
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
|
||||||
|
|
||||||
|
|
@ -517,29 +518,44 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||||
if (!isActionItem(action.item))
|
if (!isActionItem(action.item))
|
||||||
return (
|
return (
|
||||||
<text>
|
<text>
|
||||||
<span style={{ fg: theme.text }}>
|
<span style={{ fg: themeV2.text.default }}>
|
||||||
<b>{action.item.title}</b>{" "}
|
<b>{action.item.title}</b>{" "}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fg: theme.textMuted }}>{action.item.label}</span>
|
<span style={{ fg: themeV2.text.subdued }}>{action.item.label}</span>
|
||||||
</text>
|
</text>
|
||||||
)
|
)
|
||||||
const item = action.item
|
const item = action.item
|
||||||
const active = createMemo(() => isActionFocused(item))
|
const active = createMemo(() => isActionFocused(item))
|
||||||
const disabled = createMemo(() => isActionDisabled(item))
|
const disabled = createMemo(() => isActionDisabled(item))
|
||||||
const fg = selectedForeground(theme)
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
flexDirection="row"
|
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)}
|
onMouseUp={() => trigger(item)}
|
||||||
>
|
>
|
||||||
<text
|
<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}
|
attributes={active() ? TextAttributes.BOLD : undefined}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</text>
|
</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>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -549,11 +565,11 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||||
<box paddingLeft={4} paddingRight={4}>
|
<box paddingLeft={4} paddingRight={4}>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
{props.titleView ?? (
|
{props.titleView ?? (
|
||||||
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
<text fg={themeV2.text.default} attributes={TextAttributes.BOLD}>
|
||||||
{props.title}
|
{props.title}
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
<text fg={themeV2.text.subdued} onMouseUp={() => dialog.clear()}>
|
||||||
esc
|
esc
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
@ -567,9 +583,9 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||||
props.onFilter?.(e)
|
props.onFilter?.(e)
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
focusedBackgroundColor={theme.backgroundPanel}
|
focusedBackgroundColor={themeV2.background.formfield.focused}
|
||||||
cursorColor={theme.primary}
|
cursorColor={themeV2.text.formfield.focused}
|
||||||
focusedTextColor={theme.textMuted}
|
focusedTextColor={themeV2.text.formfield.focused}
|
||||||
ref={(r) => {
|
ref={(r) => {
|
||||||
input = r
|
input = r
|
||||||
input.traits = { status: "FILTER" }
|
input.traits = { status: "FILTER" }
|
||||||
|
|
@ -580,7 +596,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||||
}, 1)
|
}, 1)
|
||||||
}}
|
}}
|
||||||
placeholder={props.placeholder ?? "Search"}
|
placeholder={props.placeholder ?? "Search"}
|
||||||
placeholderColor={theme.textMuted}
|
placeholderColor={themeV2.text.subdued}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -594,14 +610,14 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||||
fallback={
|
fallback={
|
||||||
props.emptyView ?? (
|
props.emptyView ?? (
|
||||||
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
<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>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{props.noMatchView ?? (
|
{props.noMatchView ?? (
|
||||||
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
<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>
|
</box>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -623,7 +639,10 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||||
<Show
|
<Show
|
||||||
when={options[0]?.categoryView}
|
when={options[0]?.categoryView}
|
||||||
fallback={
|
fallback={
|
||||||
<text fg={theme.accent} attributes={TextAttributes.BOLD}>
|
<text
|
||||||
|
fg={themeV2.hue.accent[mode() === "light" ? 800 : 200]}
|
||||||
|
attributes={TextAttributes.BOLD}
|
||||||
|
>
|
||||||
{category}
|
{category}
|
||||||
</text>
|
</text>
|
||||||
}
|
}
|
||||||
|
|
@ -672,8 +691,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||||
backgroundColor={
|
backgroundColor={
|
||||||
active()
|
active()
|
||||||
? actionFocused()
|
? actionFocused()
|
||||||
? theme.backgroundElement
|
? themeV2.background.surface.overlay
|
||||||
: (option.bg ?? theme.primary)
|
: (option.bg ?? themeV2.background.action.primary.focused)
|
||||||
: RGBA.fromInts(0, 0, 0, 0)
|
: RGBA.fromInts(0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -692,6 +711,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||||
active={active()}
|
active={active()}
|
||||||
current={current()}
|
current={current()}
|
||||||
muted={actionFocused()}
|
muted={actionFocused()}
|
||||||
|
activeColor={option.fg}
|
||||||
gutter={option.gutter}
|
gutter={option.gutter}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
|
|
@ -699,7 +719,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||||
{(detail) => (
|
{(detail) => (
|
||||||
<box paddingLeft={3} paddingRight={3}>
|
<box paddingLeft={3} paddingRight={3}>
|
||||||
<text
|
<text
|
||||||
fg={option.detailsColor ?? theme.textMuted}
|
fg={option.detailsColor ?? themeV2.text.subdued}
|
||||||
wrapMode={option.detailsWrap ? "word" : "none"}
|
wrapMode={option.detailsWrap ? "word" : "none"}
|
||||||
>
|
>
|
||||||
{option.detailsWrap
|
{option.detailsWrap
|
||||||
|
|
@ -745,15 +765,15 @@ function Option(props: {
|
||||||
titleWidth?: number
|
titleWidth?: number
|
||||||
truncateTitle?: boolean | "left"
|
truncateTitle?: boolean | "left"
|
||||||
gutter?: () => JSX.Element
|
gutter?: () => JSX.Element
|
||||||
|
activeColor?: RGBA
|
||||||
onMouseOver?: () => void
|
onMouseOver?: () => void
|
||||||
}) {
|
}) {
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
const fg = selectedForeground(theme)
|
|
||||||
const text = createMemo(() => {
|
const text = createMemo(() => {
|
||||||
if (props.active && !props.muted) return fg
|
if (props.active && !props.muted) return props.activeColor ?? themeV2.text.action.primary.focused
|
||||||
if (props.muted && (props.active || props.current)) return theme.textMuted
|
if (props.muted && (props.active || props.current)) return themeV2.text.subdued
|
||||||
if (props.current) return theme.primary
|
if (props.current) return themeV2.text.formfield.selected
|
||||||
return theme.text
|
return themeV2.text.default
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -783,12 +803,14 @@ function Option(props: {
|
||||||
? Locale.truncateLeft(props.title, props.titleWidth ?? 61)
|
? Locale.truncateLeft(props.title, props.titleWidth ?? 61)
|
||||||
: Locale.truncate(props.title, props.titleWidth ?? 61))}
|
: Locale.truncate(props.title, props.titleWidth ?? 61))}
|
||||||
<Show when={props.description}>
|
<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>
|
</Show>
|
||||||
</text>
|
</text>
|
||||||
<Show when={props.footer}>
|
<Show when={props.footer}>
|
||||||
<box flexShrink={0}>
|
<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>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue