context.tsx 7.1 KB

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