فهرست منبع

feat(desktop): theme preview

Adam 1 ماه پیش
والد
کامیت
a4411c21b6
3فایلهای تغییر یافته به همراه89 افزوده شده و 6 حذف شده
  1. 31 6
      packages/app/src/context/command.tsx
  2. 8 0
      packages/ui/src/components/list.tsx
  3. 50 0
      packages/ui/src/theme/context.tsx

+ 31 - 6
packages/app/src/context/command.tsx

@@ -3,6 +3,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { Dialog } from "@opencode-ai/ui/dialog"
 import { List } from "@opencode-ai/ui/list"
+import { useTheme } from "@opencode-ai/ui/theme"
 
 const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
 
@@ -115,6 +116,34 @@ export function formatKeybind(config: string): string {
 
 function DialogCommand(props: { options: CommandOption[] }) {
   const dialog = useDialog()
+  const theme = useTheme()
+  let committed = false
+
+  const handleMove = (option: CommandOption | undefined) => {
+    if (!option) return
+    if (option.id.startsWith("theme.set.")) {
+      const id = option.id.replace("theme.set.", "")
+      theme.previewTheme(id)
+    } else if (option.id.startsWith("theme.scheme.") && !option.id.includes("cycle")) {
+      const scheme = option.id.replace("theme.scheme.", "") as "light" | "dark" | "system"
+      theme.previewColorScheme(scheme)
+    }
+  }
+
+  const handleSelect = (option: CommandOption | undefined) => {
+    if (option) {
+      theme.commitPreview()
+      committed = true
+      dialog.close()
+      option.onSelect?.("palette")
+    }
+  }
+
+  onCleanup(() => {
+    if (!committed) {
+      theme.cancelPreview()
+    }
+  })
 
   return (
     <Dialog title="Commands">
@@ -125,12 +154,8 @@ function DialogCommand(props: { options: CommandOption[] }) {
         key={(x) => x?.id}
         filterKeys={["title", "description", "category"]}
         groupBy={(x) => x.category ?? ""}
-        onSelect={(option) => {
-          if (option) {
-            dialog.close()
-            option.onSelect?.("palette")
-          }
-        }}
+        onMove={handleMove}
+        onSelect={handleSelect}
       >
         {(option) => (
           <div class="w-full flex items-center justify-between gap-4">

+ 8 - 0
packages/ui/src/components/list.tsx

@@ -15,6 +15,7 @@ export interface ListProps<T> extends FilteredListProps<T> {
   children: (item: T) => JSX.Element
   emptyMessage?: string
   onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
+  onMove?: (item: T | undefined) => void
   activeIcon?: IconProps["name"]
   filter?: string
   search?: ListSearchProps | boolean
@@ -82,6 +83,13 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
     element?.scrollIntoView({ block: "center", behavior: "smooth" })
   })
 
+  createEffect(() => {
+    const all = flat()
+    const current = active()
+    const item = all.find((x) => props.key(x) === current)
+    props.onMove?.(item)
+  })
+
   const handleSelect = (item: T | undefined, index: number) => {
     props.onSelect?.(item, index)
   }

+ 50 - 0
packages/ui/src/theme/context.tsx

@@ -22,6 +22,10 @@ interface ThemeContextValue {
   setTheme: (id: string) => void
   setColorScheme: (scheme: ColorScheme) => void
   registerTheme: (theme: DesktopTheme) => void
+  previewTheme: (id: string) => void
+  previewColorScheme: (scheme: ColorScheme) => void
+  commitPreview: () => void
+  cancelPreview: () => void
 }
 
 const ThemeContext = createContext<ThemeContextValue>()
@@ -104,6 +108,8 @@ export function ThemeProvider(props: { children: JSX.Element; defaultTheme?: str
   const [themeId, setThemeIdSignal] = createSignal(props.defaultTheme ?? "oc-1")
   const [colorScheme, setColorSchemeSignal] = createSignal<ColorScheme>("system")
   const [mode, setMode] = createSignal<"light" | "dark">(getSystemMode())
+  const [previewThemeId, setPreviewThemeId] = createSignal<string | null>(null)
+  const [previewScheme, setPreviewScheme] = createSignal<ColorScheme | null>(null)
 
   onMount(() => {
     const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
@@ -169,6 +175,46 @@ export function ThemeProvider(props: { children: JSX.Element; defaultTheme?: str
     }))
   }
 
+  const previewTheme = (id: string) => {
+    const theme = themes()[id]
+    if (!theme) return
+    setPreviewThemeId(id)
+    const previewMode = previewScheme() ? (previewScheme() === "system" ? getSystemMode() : previewScheme()!) : mode()
+    applyThemeCss(theme, id, previewMode as "light" | "dark")
+  }
+
+  const previewColorScheme = (scheme: ColorScheme) => {
+    setPreviewScheme(scheme)
+    const previewMode = scheme === "system" ? getSystemMode() : scheme
+    const id = previewThemeId() ?? themeId()
+    const theme = themes()[id]
+    if (theme) {
+      applyThemeCss(theme, id, previewMode)
+    }
+  }
+
+  const commitPreview = () => {
+    const id = previewThemeId()
+    const scheme = previewScheme()
+    if (id) {
+      setTheme(id)
+    }
+    if (scheme) {
+      setColorSchemePref(scheme)
+    }
+    setPreviewThemeId(null)
+    setPreviewScheme(null)
+  }
+
+  const cancelPreview = () => {
+    setPreviewThemeId(null)
+    setPreviewScheme(null)
+    const theme = themes()[themeId()]
+    if (theme) {
+      applyThemeCss(theme, themeId(), mode())
+    }
+  }
+
   return (
     <ThemeContext.Provider
       value={{
@@ -179,6 +225,10 @@ export function ThemeProvider(props: { children: JSX.Element; defaultTheme?: str
         setTheme,
         setColorScheme: setColorSchemePref,
         registerTheme,
+        previewTheme,
+        previewColorScheme,
+        commitPreview,
+        cancelPreview,
       }}
     >
       {props.children}