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" const STORAGE_KEYS = { THEME_ID: "opencode-theme-id", COLOR_SCHEME: "opencode-color-scheme", THEME_CSS_LIGHT: "opencode-theme-css-light", THEME_CSS_DARK: "opencode-theme-css-dark", } as const const THEME_STYLE_ID = "oc-theme" function ensureThemeStyleElement(): HTMLStyleElement { const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null if (existing) return existing const element = document.createElement("style") element.id = THEME_STYLE_ID document.head.appendChild(element) return element } function getSystemMode(): "light" | "dark" { return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" } 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") { try { localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css) } catch {} } const fullCss = `:root { color-scheme: ${mode}; --text-mix-blend-mode: ${isDark ? "plus-lighter" : "multiply"}; ${css} }` 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) { 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) try { localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css) } catch {} } } export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ name: "Theme", init: (props: { defaultTheme?: string }) => { const [store, setStore] = createStore({ themes: DEFAULT_THEMES as Record, 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)) 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) } if (savedScheme) { setStore("colorScheme", savedScheme) if (savedScheme !== "system") { setStore("mode", savedScheme) } } const currentTheme = store.themes[store.themeId] if (currentTheme) { cacheThemeVariants(currentTheme, store.themeId) } }) createEffect(() => { const theme = store.themes[store.themeId] if (theme) { applyThemeCss(theme, store.themeId, store.mode) } }) 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 setColorScheme = (scheme: ColorScheme) => { setStore("colorScheme", scheme) localStorage.setItem(STORAGE_KEYS.COLOR_SCHEME, scheme) setStore("mode", scheme === "system" ? getSystemMode() : scheme) } 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) } }, } }, })