context.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import { onMount, onCleanup, createEffect } from "solid-js"
  2. import { createStore } from "solid-js/store"
  3. import type { DesktopTheme } from "./types"
  4. import { resolveThemeVariant, themeToCss } from "./resolve"
  5. import { DEFAULT_THEMES } from "./default-themes"
  6. import { createSimpleContext } from "../context/helper"
  7. export type ColorScheme = "light" | "dark" | "system"
  8. const STORAGE_KEYS = {
  9. THEME_ID: "opencode-theme-id",
  10. COLOR_SCHEME: "opencode-color-scheme",
  11. THEME_CSS_LIGHT: "opencode-theme-css-light",
  12. THEME_CSS_DARK: "opencode-theme-css-dark",
  13. } as const
  14. const THEME_STYLE_ID = "oc-theme"
  15. function ensureThemeStyleElement(): HTMLStyleElement {
  16. const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null
  17. if (existing) return existing
  18. const element = document.createElement("style")
  19. element.id = THEME_STYLE_ID
  20. document.head.appendChild(element)
  21. return element
  22. }
  23. function getSystemMode(): "light" | "dark" {
  24. return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
  25. }
  26. function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "dark") {
  27. const isDark = mode === "dark"
  28. const variant = isDark ? theme.dark : theme.light
  29. const tokens = resolveThemeVariant(variant, isDark)
  30. const css = themeToCss(tokens)
  31. if (themeId !== "oc-1") {
  32. try {
  33. localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
  34. } catch {}
  35. }
  36. const fullCss = `:root {
  37. color-scheme: ${mode};
  38. --text-mix-blend-mode: ${isDark ? "plus-lighter" : "multiply"};
  39. ${css}
  40. }`
  41. document.getElementById("oc-theme-preload")?.remove()
  42. ensureThemeStyleElement().textContent = fullCss
  43. document.documentElement.dataset.theme = themeId
  44. document.documentElement.dataset.colorScheme = mode
  45. }
  46. function cacheThemeVariants(theme: DesktopTheme, themeId: string) {
  47. if (themeId === "oc-1") return
  48. for (const mode of ["light", "dark"] as const) {
  49. const isDark = mode === "dark"
  50. const variant = isDark ? theme.dark : theme.light
  51. const tokens = resolveThemeVariant(variant, isDark)
  52. const css = themeToCss(tokens)
  53. try {
  54. localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
  55. } catch {}
  56. }
  57. }
  58. export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
  59. name: "Theme",
  60. init: (props: { defaultTheme?: string }) => {
  61. const [store, setStore] = createStore({
  62. themes: DEFAULT_THEMES as Record<string, DesktopTheme>,
  63. themeId: props.defaultTheme ?? "oc-1",
  64. colorScheme: "system" as ColorScheme,
  65. mode: getSystemMode(),
  66. previewThemeId: null as string | null,
  67. previewScheme: null as ColorScheme | null,
  68. })
  69. onMount(() => {
  70. const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
  71. const handler = () => {
  72. if (store.colorScheme === "system") {
  73. setStore("mode", getSystemMode())
  74. }
  75. }
  76. mediaQuery.addEventListener("change", handler)
  77. onCleanup(() => mediaQuery.removeEventListener("change", handler))
  78. const savedTheme = localStorage.getItem(STORAGE_KEYS.THEME_ID)
  79. const savedScheme = localStorage.getItem(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null
  80. if (savedTheme && store.themes[savedTheme]) {
  81. setStore("themeId", savedTheme)
  82. }
  83. if (savedScheme) {
  84. setStore("colorScheme", savedScheme)
  85. if (savedScheme !== "system") {
  86. setStore("mode", savedScheme)
  87. }
  88. }
  89. const currentTheme = store.themes[store.themeId]
  90. if (currentTheme) {
  91. cacheThemeVariants(currentTheme, store.themeId)
  92. }
  93. })
  94. createEffect(() => {
  95. const theme = store.themes[store.themeId]
  96. if (theme) {
  97. applyThemeCss(theme, store.themeId, store.mode)
  98. }
  99. })
  100. const setTheme = (id: string) => {
  101. const theme = store.themes[id]
  102. if (!theme) {
  103. console.warn(`Theme "${id}" not found`)
  104. return
  105. }
  106. setStore("themeId", id)
  107. localStorage.setItem(STORAGE_KEYS.THEME_ID, id)
  108. cacheThemeVariants(theme, id)
  109. }
  110. const setColorScheme = (scheme: ColorScheme) => {
  111. setStore("colorScheme", scheme)
  112. localStorage.setItem(STORAGE_KEYS.COLOR_SCHEME, scheme)
  113. setStore("mode", scheme === "system" ? getSystemMode() : scheme)
  114. }
  115. return {
  116. themeId: () => store.themeId,
  117. colorScheme: () => store.colorScheme,
  118. mode: () => store.mode,
  119. themes: () => store.themes,
  120. setTheme,
  121. setColorScheme,
  122. registerTheme: (theme: DesktopTheme) => setStore("themes", theme.id, theme),
  123. previewTheme: (id: string) => {
  124. const theme = store.themes[id]
  125. if (!theme) return
  126. setStore("previewThemeId", id)
  127. const previewMode = store.previewScheme
  128. ? store.previewScheme === "system"
  129. ? getSystemMode()
  130. : store.previewScheme
  131. : store.mode
  132. applyThemeCss(theme, id, previewMode)
  133. },
  134. previewColorScheme: (scheme: ColorScheme) => {
  135. setStore("previewScheme", scheme)
  136. const previewMode = scheme === "system" ? getSystemMode() : scheme
  137. const id = store.previewThemeId ?? store.themeId
  138. const theme = store.themes[id]
  139. if (theme) {
  140. applyThemeCss(theme, id, previewMode)
  141. }
  142. },
  143. commitPreview: () => {
  144. if (store.previewThemeId) {
  145. setTheme(store.previewThemeId)
  146. }
  147. if (store.previewScheme) {
  148. setColorScheme(store.previewScheme)
  149. }
  150. setStore("previewThemeId", null)
  151. setStore("previewScheme", null)
  152. },
  153. cancelPreview: () => {
  154. setStore("previewThemeId", null)
  155. setStore("previewScheme", null)
  156. const theme = store.themes[store.themeId]
  157. if (theme) {
  158. applyThemeCss(theme, store.themeId, store.mode)
  159. }
  160. },
  161. }
  162. },
  163. })