Buckets:
| // @refresh reload | |
| 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 { resolveThemeVariantV2, themeV2ToCss } from "./v2/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) | |
| const v2 = themeV2ToCss(resolveThemeVariantV2(variant, isDark)) | |
| if (themeId !== "oc-2") { | |
| write(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, `${css}\n ${v2}`) | |
| } | |
| const fullCss = `:root { | |
| color-scheme: ${mode}; | |
| --text-mix-blend-mode: ${isDark ? "plus-lighter" : "multiply"}; | |
| ${css} | |
| ${v2} | |
| }` | |
| document.getElementById("oc-theme-preload")?.remove() | |
| ensureThemeStyleElement().textContent = fullCss | |
| document.documentElement.dataset.theme = themeId | |
| document.documentElement.dataset.colorScheme = mode | |
| // Update theme-color meta tag to match light/dark mode | |
| const meta = document.querySelector('meta[name="theme-color"]') | |
| if (meta) meta.setAttribute("content", isDark ? "#131010" : "#F8F7F7") | |
| } | |
| 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) | |
| const v2 = themeV2ToCss(resolveThemeVariantV2(variant, isDark)) | |
| write(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, `${css}\n ${v2}`) | |
| } | |
| } | |
| 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) | |
| }) | |
| }, | |
| } | |
| }, | |
| }) | |
Xet Storage Details
- Size:
- 11.8 kB
- Xet hash:
- 623fa690555c519e731d6a8be0a266583195909dbbbf87af75ce24bb9e7e5a5c
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.