Преглед изворни кода

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

James Long пре 3 недеља
родитељ
комит
88f572cfce

+ 8 - 5
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")
           ? () => <Spinner />
           : slot === 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={
         <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>
       }
       noMatchView={
         <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>
       }
       onMove={() => setToDelete(undefined)}

+ 3 - 2
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<number>()
@@ -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,

+ 16 - 8
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}
     >
       <scrollbox
         ref={(r: ScrollBoxRenderable) => (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={
             <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>
           }
         >
@@ -719,7 +721,7 @@ export function Autocomplete(props: {
             <box
               paddingLeft={1}
               paddingRight={1}
-              backgroundColor={index === store.selected ? theme.primary : undefined}
+              backgroundColor={index === store.selected ? themeV2.background.action.primary.focused : undefined}
               flexDirection="row"
               onMouseMove={() => {
                 setStore("input", "mouse")
@@ -734,11 +736,17 @@ export function Autocomplete(props: {
               }}
               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}
               </text>
               <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()}
                 </text>
               </Show>

+ 50 - 28
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<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>
     </>