Kaynağa Gözat

chore: cleanup theme stuff

Adam 1 ay önce
ebeveyn
işleme
0156f03e0e

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

@@ -3,7 +3,6 @@ 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)
 
@@ -27,6 +26,7 @@ export interface CommandOption {
   suggested?: boolean
   disabled?: boolean
   onSelect?: (source?: "palette" | "keybind" | "slash") => void
+  onHighlight?: () => (() => void) | void
 }
 
 export function parseKeybind(config: string): Keybind[] {
@@ -116,24 +116,18 @@ export function formatKeybind(config: string): string {
 
 function DialogCommand(props: { options: CommandOption[] }) {
   const dialog = useDialog()
-  const theme = useTheme()
+  let cleanup: (() => void) | void
   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)
-    }
+    cleanup?.()
+    cleanup = option?.onHighlight?.()
   }
 
   const handleSelect = (option: CommandOption | undefined) => {
     if (option) {
-      theme.commitPreview()
       committed = true
+      cleanup = undefined
       dialog.close()
       option.onSelect?.("palette")
     }
@@ -141,7 +135,7 @@ function DialogCommand(props: { options: CommandOption[] }) {
 
   onCleanup(() => {
     if (!committed) {
-      theme.cancelPreview()
+      cleanup?.()
     }
   })
 

+ 12 - 4
packages/app/src/pages/layout.tsx

@@ -49,7 +49,7 @@ import { Header } from "@/components/header"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
 import { DialogSelectProvider } from "@/components/dialog-select-provider"
-import { useCommand } from "@/context/command"
+import { useCommand, type CommandOption } from "@/context/command"
 import { ConstrainDragXAxis } from "@/utils/solid-dnd"
 
 export default function Layout(props: ParentProps) {
@@ -323,7 +323,7 @@ export default function Layout(props: ParentProps) {
   }
 
   command.register(() => {
-    const commands = [
+    const commands: CommandOption[] = [
       {
         id: "sidebar.toggle",
         title: "Toggle sidebar",
@@ -387,7 +387,11 @@ export default function Layout(props: ParentProps) {
         id: `theme.set.${id}`,
         title: `Use theme: ${definition.name ?? id}`,
         category: "Theme",
-        onSelect: () => theme.setTheme(id),
+        onSelect: () => theme.commitPreview(),
+        onHighlight: () => {
+          theme.previewTheme(id)
+          return () => theme.cancelPreview()
+        },
       })
     }
 
@@ -404,7 +408,11 @@ export default function Layout(props: ParentProps) {
         id: `theme.scheme.${scheme}`,
         title: `Use color scheme: ${colorSchemeLabel[scheme]}`,
         category: "Theme",
-        onSelect: () => theme.setColorScheme(scheme),
+        onSelect: () => theme.commitPreview(),
+        onHighlight: () => {
+          theme.previewColorScheme(scheme)
+          return () => theme.cancelPreview()
+        },
       })
     }
 

+ 114 - 179
packages/ui/src/theme/context.tsx

@@ -1,52 +1,24 @@
-import {
-  createContext,
-  useContext,
-  createSignal,
-  onMount,
-  onCleanup,
-  createEffect,
-  type JSX,
-  type Accessor,
-} from "solid-js"
+import { onMount, onCleanup, createEffect } from "solid-js"
+import { createStore } from "solid-js/store"
 import type { DesktopTheme } from "./types"
 import { resolveThemeVariant, themeToCss } from "./resolve"
 import { DEFAULT_THEMES } from "./default-themes"
+import { createSimpleContext } from "../context/helper"
 
 export type ColorScheme = "light" | "dark" | "system"
 
-interface ThemeContextValue {
-  themeId: Accessor<string>
-  colorScheme: Accessor<ColorScheme>
-  mode: Accessor<"light" | "dark">
-  themes: Accessor<Record<string, DesktopTheme>>
-  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>()
-
 const STORAGE_KEYS = {
   THEME_ID: "opencode-theme-id",
   COLOR_SCHEME: "opencode-color-scheme",
-  THEME_CSS_PREFIX: "opencode-theme-css",
+  THEME_CSS_LIGHT: "opencode-theme-css-light",
+  THEME_CSS_DARK: "opencode-theme-css-dark",
 } as const
 
-function getThemeCacheKey(themeId: string, mode: "light" | "dark"): string {
-  return `${STORAGE_KEYS.THEME_CSS_PREFIX}-${themeId}-${mode}`
-}
-
 const THEME_STYLE_ID = "oc-theme"
 
 function ensureThemeStyleElement(): HTMLStyleElement {
   const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null
-  if (existing) {
-    return existing
-  }
+  if (existing) return existing
   const element = document.createElement("style")
   element.id = THEME_STYLE_ID
   document.head.appendChild(element)
@@ -57,16 +29,15 @@ function getSystemMode(): "light" | "dark" {
   return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
 }
 
-function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "dark"): void {
+function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "dark") {
   const isDark = mode === "dark"
   const variant = isDark ? theme.dark : theme.light
   const tokens = resolveThemeVariant(variant, isDark)
   const css = themeToCss(tokens)
 
   if (themeId !== "oc-1") {
-    const cacheKey = getThemeCacheKey(themeId, mode)
     try {
-      localStorage.setItem(cacheKey, css)
+      localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
     } catch {}
   }
 
@@ -76,170 +47,134 @@ function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "da
   ${css}
 }`
 
-  const preloadStyle = document.getElementById("oc-theme-preload")
-  if (preloadStyle) {
-    preloadStyle.remove()
-  }
-
-  const themeStyleElement = ensureThemeStyleElement()
-  themeStyleElement.textContent = fullCss
-
+  document.getElementById("oc-theme-preload")?.remove()
+  ensureThemeStyleElement().textContent = fullCss
   document.documentElement.dataset.theme = themeId
   document.documentElement.dataset.colorScheme = mode
 }
 
-function cacheThemeVariants(theme: DesktopTheme, themeId: string): void {
+function cacheThemeVariants(theme: DesktopTheme, themeId: string) {
   if (themeId === "oc-1") return
-
   for (const mode of ["light", "dark"] as const) {
     const isDark = mode === "dark"
     const variant = isDark ? theme.dark : theme.light
     const tokens = resolveThemeVariant(variant, isDark)
     const css = themeToCss(tokens)
-    const cacheKey = getThemeCacheKey(themeId, mode)
     try {
-      localStorage.setItem(cacheKey, css)
+      localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
     } catch {}
   }
 }
 
-export function ThemeProvider(props: { children: JSX.Element; defaultTheme?: string }) {
-  const [themes, setThemes] = createSignal<Record<string, DesktopTheme>>(DEFAULT_THEMES)
-  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)")
-    const handler = () => {
-      if (colorScheme() === "system") {
-        setMode(getSystemMode())
+export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
+  name: "Theme",
+  init: (props: { defaultTheme?: string }) => {
+    const [store, setStore] = createStore({
+      themes: DEFAULT_THEMES as Record<string, DesktopTheme>,
+      themeId: props.defaultTheme ?? "oc-1",
+      colorScheme: "system" as ColorScheme,
+      mode: getSystemMode(),
+      previewThemeId: null as string | null,
+      previewScheme: null as ColorScheme | null,
+    })
+
+    onMount(() => {
+      const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
+      const handler = () => {
+        if (store.colorScheme === "system") {
+          setStore("mode", getSystemMode())
+        }
       }
-    }
-    mediaQuery.addEventListener("change", handler)
-    onCleanup(() => mediaQuery.removeEventListener("change", handler))
+      mediaQuery.addEventListener("change", handler)
+      onCleanup(() => mediaQuery.removeEventListener("change", handler))
 
-    const savedTheme = localStorage.getItem(STORAGE_KEYS.THEME_ID)
-    const savedScheme = localStorage.getItem(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null
-    if (savedTheme && themes()[savedTheme]) {
-      setThemeIdSignal(savedTheme)
-    }
-    if (savedScheme) {
-      setColorSchemeSignal(savedScheme)
-      if (savedScheme !== "system") {
-        setMode(savedScheme)
+      const savedTheme = localStorage.getItem(STORAGE_KEYS.THEME_ID)
+      const savedScheme = localStorage.getItem(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null
+      if (savedTheme && store.themes[savedTheme]) {
+        setStore("themeId", savedTheme)
       }
-    }
-    const currentTheme = themes()[themeId()]
-    if (currentTheme) {
-      cacheThemeVariants(currentTheme, themeId())
-    }
-  })
-
-  createEffect(() => {
-    const id = themeId()
-    const m = mode()
-    const theme = themes()[id]
-    if (theme) {
-      applyThemeCss(theme, id, m)
-    }
-  })
-
-  const setTheme = (id: string) => {
-    const theme = themes()[id]
-    if (!theme) {
-      console.warn(`Theme "${id}" not found`)
-      return
-    }
-    setThemeIdSignal(id)
-    localStorage.setItem(STORAGE_KEYS.THEME_ID, id)
-    cacheThemeVariants(theme, id)
-  }
-
-  const setColorSchemePref = (scheme: ColorScheme) => {
-    setColorSchemeSignal(scheme)
-    localStorage.setItem(STORAGE_KEYS.COLOR_SCHEME, scheme)
-    if (scheme === "system") {
-      setMode(getSystemMode())
-    } else {
-      setMode(scheme)
-    }
-  }
-
-  const registerTheme = (theme: DesktopTheme) => {
-    setThemes((prev) => ({
-      ...prev,
-      [theme.id]: theme,
-    }))
-  }
+      if (savedScheme) {
+        setStore("colorScheme", savedScheme)
+        if (savedScheme !== "system") {
+          setStore("mode", savedScheme)
+        }
+      }
+      const currentTheme = store.themes[store.themeId]
+      if (currentTheme) {
+        cacheThemeVariants(currentTheme, store.themeId)
+      }
+    })
 
-  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")
-  }
+    createEffect(() => {
+      const theme = store.themes[store.themeId]
+      if (theme) {
+        applyThemeCss(theme, store.themeId, store.mode)
+      }
+    })
 
-  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 setTheme = (id: string) => {
+      const theme = store.themes[id]
+      if (!theme) {
+        console.warn(`Theme "${id}" not found`)
+        return
+      }
+      setStore("themeId", id)
+      localStorage.setItem(STORAGE_KEYS.THEME_ID, id)
+      cacheThemeVariants(theme, id)
     }
-  }
 
-  const commitPreview = () => {
-    const id = previewThemeId()
-    const scheme = previewScheme()
-    if (id) {
-      setTheme(id)
-    }
-    if (scheme) {
-      setColorSchemePref(scheme)
+    const setColorScheme = (scheme: ColorScheme) => {
+      setStore("colorScheme", scheme)
+      localStorage.setItem(STORAGE_KEYS.COLOR_SCHEME, scheme)
+      setStore("mode", scheme === "system" ? getSystemMode() : scheme)
     }
-    setPreviewThemeId(null)
-    setPreviewScheme(null)
-  }
 
-  const cancelPreview = () => {
-    setPreviewThemeId(null)
-    setPreviewScheme(null)
-    const theme = themes()[themeId()]
-    if (theme) {
-      applyThemeCss(theme, themeId(), mode())
+    return {
+      themeId: () => store.themeId,
+      colorScheme: () => store.colorScheme,
+      mode: () => store.mode,
+      themes: () => store.themes,
+      setTheme,
+      setColorScheme,
+      registerTheme: (theme: DesktopTheme) => setStore("themes", theme.id, theme),
+      previewTheme: (id: string) => {
+        const theme = store.themes[id]
+        if (!theme) return
+        setStore("previewThemeId", id)
+        const previewMode = store.previewScheme
+          ? store.previewScheme === "system"
+            ? getSystemMode()
+            : store.previewScheme
+          : store.mode
+        applyThemeCss(theme, id, previewMode)
+      },
+      previewColorScheme: (scheme: ColorScheme) => {
+        setStore("previewScheme", scheme)
+        const previewMode = scheme === "system" ? getSystemMode() : scheme
+        const id = store.previewThemeId ?? store.themeId
+        const theme = store.themes[id]
+        if (theme) {
+          applyThemeCss(theme, id, previewMode)
+        }
+      },
+      commitPreview: () => {
+        if (store.previewThemeId) {
+          setTheme(store.previewThemeId)
+        }
+        if (store.previewScheme) {
+          setColorScheme(store.previewScheme)
+        }
+        setStore("previewThemeId", null)
+        setStore("previewScheme", null)
+      },
+      cancelPreview: () => {
+        setStore("previewThemeId", null)
+        setStore("previewScheme", null)
+        const theme = store.themes[store.themeId]
+        if (theme) {
+          applyThemeCss(theme, store.themeId, store.mode)
+        }
+      },
     }
-  }
-
-  return (
-    <ThemeContext.Provider
-      value={{
-        themeId,
-        colorScheme,
-        mode,
-        themes,
-        setTheme,
-        setColorScheme: setColorSchemePref,
-        registerTheme,
-        previewTheme,
-        previewColorScheme,
-        commitPreview,
-        cancelPreview,
-      }}
-    >
-      {props.children}
-    </ThemeContext.Provider>
-  )
-}
-
-export function useTheme(): ThemeContextValue {
-  const ctx = useContext(ThemeContext)
-  if (!ctx) {
-    throw new Error("useTheme must be used within a ThemeProvider")
-  }
-  return ctx
-}
+  },
+})