| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359 |
- import { createEffect, onMount } from "solid-js"
- import { createStore } from "solid-js/store"
- import { makeEventListener } from "@solid-primitives/event-listener"
- import { createSimpleContext } from "../context/helper"
- import oc2ThemeJson from "./themes/oc-2.json"
- import { resolveThemeVariant, themeToCss } from "./resolve"
- import type { DesktopTheme } from "./types"
- 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"
- let files: Record<string, () => Promise<{ default: DesktopTheme }>> | undefined
- let ids: string[] | undefined
- let known: Set<string> | undefined
- function getFiles() {
- if (files) return files
- files = import.meta.glob<{ default: DesktopTheme }>("./themes/*.json")
- return files
- }
- function themeIDs() {
- if (ids) return ids
- ids = Object.keys(getFiles())
- .map((path) => path.slice("./themes/".length, -".json".length))
- .sort()
- return ids
- }
- function knownThemes() {
- if (known) return known
- known = new Set(themeIDs())
- return known
- }
- const names: Record<string, string> = {
- "oc-2": "OC-2",
- amoled: "AMOLED",
- aura: "Aura",
- ayu: "Ayu",
- carbonfox: "Carbonfox",
- catppuccin: "Catppuccin",
- "catppuccin-frappe": "Catppuccin Frappe",
- "catppuccin-macchiato": "Catppuccin Macchiato",
- cobalt2: "Cobalt2",
- cursor: "Cursor",
- dracula: "Dracula",
- everforest: "Everforest",
- flexoki: "Flexoki",
- github: "GitHub",
- gruvbox: "Gruvbox",
- kanagawa: "Kanagawa",
- "lucent-orng": "Lucent Orng",
- material: "Material",
- matrix: "Matrix",
- mercury: "Mercury",
- monokai: "Monokai",
- nightowl: "Night Owl",
- nord: "Nord",
- "one-dark": "One Dark",
- onedarkpro: "One Dark Pro",
- opencode: "OpenCode",
- orng: "Orng",
- "osaka-jade": "Osaka Jade",
- palenight: "Palenight",
- rosepine: "Rose Pine",
- shadesofpurple: "Shades of Purple",
- solarized: "Solarized",
- synthwave84: "Synthwave '84",
- tokyonight: "Tokyonight",
- vercel: "Vercel",
- vesper: "Vesper",
- zenburn: "Zenburn",
- }
- const oc2Theme = oc2ThemeJson as DesktopTheme
- function normalize(id: string | null | undefined) {
- return id === "oc-1" ? "oc-2" : id
- }
- function read(key: string) {
- if (typeof localStorage !== "object") return null
- try {
- return localStorage.getItem(key)
- } catch {
- return null
- }
- }
- function write(key: string, value: string) {
- if (typeof localStorage !== "object") return
- try {
- localStorage.setItem(key, value)
- } catch {}
- }
- function drop(key: string) {
- if (typeof localStorage !== "object") return
- try {
- localStorage.removeItem(key)
- } catch {}
- }
- function clear() {
- drop(STORAGE_KEYS.THEME_CSS_LIGHT)
- drop(STORAGE_KEYS.THEME_CSS_DARK)
- }
- 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" {
- if (typeof window !== "object") return "light"
- 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-2") {
- write(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
- }
- 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-2") 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)
- write(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
- }
- }
- export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
- name: "Theme",
- init: (props: { defaultTheme?: string; onThemeApplied?: (theme: DesktopTheme, mode: "light" | "dark") => void }) => {
- const themeId = normalize(read(STORAGE_KEYS.THEME_ID) ?? props.defaultTheme) ?? "oc-2"
- const colorScheme = (read(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null) ?? "system"
- const mode = colorScheme === "system" ? getSystemMode() : colorScheme
- const [store, setStore] = createStore({
- themes: {
- "oc-2": oc2Theme,
- } as Record<string, DesktopTheme>,
- themeId,
- colorScheme,
- mode,
- previewThemeId: null as string | null,
- previewScheme: null as ColorScheme | null,
- })
- const loads = new Map<string, Promise<DesktopTheme | undefined>>()
- const load = (id: string) => {
- const next = normalize(id)
- if (!next) return Promise.resolve(undefined)
- const hit = store.themes[next]
- if (hit) return Promise.resolve(hit)
- const pending = loads.get(next)
- if (pending) return pending
- const file = getFiles()[`./themes/${next}.json`]
- if (!file) return Promise.resolve(undefined)
- const task = file()
- .then((mod) => {
- const theme = mod.default
- setStore("themes", next, theme)
- return theme
- })
- .finally(() => {
- loads.delete(next)
- })
- loads.set(next, task)
- return task
- }
- const applyTheme = (theme: DesktopTheme, themeId: string, mode: "light" | "dark") => {
- applyThemeCss(theme, themeId, mode)
- props.onThemeApplied?.(theme, mode)
- }
- const ids = () => {
- const extra = Object.keys(store.themes)
- .filter((id) => !knownThemes().has(id))
- .sort()
- const all = themeIDs()
- if (extra.length === 0) return all
- return [...all, ...extra]
- }
- const loadThemes = () => Promise.all(themeIDs().map(load)).then(() => store.themes)
- const onStorage = (e: StorageEvent) => {
- if (e.key === STORAGE_KEYS.THEME_ID && e.newValue) {
- const next = normalize(e.newValue)
- if (!next) return
- if (next !== "oc-2" && !knownThemes().has(next) && !store.themes[next]) return
- setStore("themeId", next)
- if (next === "oc-2") {
- clear()
- return
- }
- void load(next).then((theme) => {
- if (!theme || store.themeId !== next) return
- cacheThemeVariants(theme, next)
- })
- }
- if (e.key === STORAGE_KEYS.COLOR_SCHEME && e.newValue) {
- setStore("colorScheme", e.newValue as ColorScheme)
- setStore("mode", e.newValue === "system" ? getSystemMode() : (e.newValue as "light" | "dark"))
- }
- }
- onMount(() => {
- makeEventListener(window, "storage", onStorage)
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
- const onMedia = () => {
- if (store.colorScheme !== "system") return
- setStore("mode", getSystemMode())
- }
- makeEventListener(mediaQuery, "change", onMedia)
- const rawTheme = read(STORAGE_KEYS.THEME_ID)
- const savedTheme = normalize(rawTheme ?? props.defaultTheme) ?? "oc-2"
- const savedScheme = (read(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null) ?? "system"
- if (rawTheme && rawTheme !== savedTheme) {
- write(STORAGE_KEYS.THEME_ID, savedTheme)
- clear()
- }
- if (savedTheme !== store.themeId) setStore("themeId", savedTheme)
- if (savedScheme !== store.colorScheme) setStore("colorScheme", savedScheme)
- setStore("mode", savedScheme === "system" ? getSystemMode() : savedScheme)
- void load(savedTheme).then((theme) => {
- if (!theme || store.themeId !== savedTheme) return
- cacheThemeVariants(theme, savedTheme)
- })
- })
- createEffect(() => {
- const theme = store.themes[store.themeId]
- if (!theme) return
- applyTheme(theme, store.themeId, store.mode)
- })
- const setTheme = (id: string) => {
- const next = normalize(id)
- if (!next) {
- console.warn(`Theme "${id}" not found`)
- return
- }
- if (next !== "oc-2" && !knownThemes().has(next) && !store.themes[next]) {
- console.warn(`Theme "${id}" not found`)
- return
- }
- setStore("themeId", next)
- if (next === "oc-2") {
- write(STORAGE_KEYS.THEME_ID, next)
- clear()
- return
- }
- void load(next).then((theme) => {
- if (!theme || store.themeId !== next) return
- cacheThemeVariants(theme, next)
- write(STORAGE_KEYS.THEME_ID, next)
- })
- }
- const setColorScheme = (scheme: ColorScheme) => {
- setStore("colorScheme", scheme)
- write(STORAGE_KEYS.COLOR_SCHEME, scheme)
- setStore("mode", scheme === "system" ? getSystemMode() : scheme)
- }
- return {
- themeId: () => store.themeId,
- colorScheme: () => store.colorScheme,
- mode: () => store.mode,
- ids,
- name: (id: string) => store.themes[id]?.name ?? names[id] ?? id,
- loadThemes,
- themes: () => store.themes,
- setTheme,
- setColorScheme,
- registerTheme: (theme: DesktopTheme) => setStore("themes", theme.id, theme),
- previewTheme: (id: string) => {
- const next = normalize(id)
- if (!next) return
- if (next !== "oc-2" && !knownThemes().has(next) && !store.themes[next]) return
- setStore("previewThemeId", next)
- void load(next).then((theme) => {
- if (!theme || store.previewThemeId !== next) return
- const mode = store.previewScheme
- ? store.previewScheme === "system"
- ? getSystemMode()
- : store.previewScheme
- : store.mode
- applyTheme(theme, next, mode)
- })
- },
- previewColorScheme: (scheme: ColorScheme) => {
- setStore("previewScheme", scheme)
- const mode = scheme === "system" ? getSystemMode() : scheme
- const id = store.previewThemeId ?? store.themeId
- void load(id).then((theme) => {
- if (!theme) return
- if ((store.previewThemeId ?? store.themeId) !== id) return
- if (store.previewScheme !== scheme) return
- applyTheme(theme, id, mode)
- })
- },
- 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)
- void load(store.themeId).then((theme) => {
- if (!theme) return
- applyTheme(theme, store.themeId, store.mode)
- })
- },
- }
- },
- })
|