@opencode-ai/ui 0.0.0-beta-202606251302

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (346) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +110 -0
  3. package/src/assets/audio/alert-01.aac +0 -0
  4. package/src/assets/audio/alert-01.mp3 +0 -0
  5. package/src/assets/audio/alert-02.aac +0 -0
  6. package/src/assets/audio/alert-02.mp3 +0 -0
  7. package/src/assets/audio/alert-03.aac +0 -0
  8. package/src/assets/audio/alert-03.mp3 +0 -0
  9. package/src/assets/audio/alert-04.aac +0 -0
  10. package/src/assets/audio/alert-04.mp3 +0 -0
  11. package/src/assets/audio/alert-05.aac +0 -0
  12. package/src/assets/audio/alert-05.mp3 +0 -0
  13. package/src/assets/audio/alert-06.aac +0 -0
  14. package/src/assets/audio/alert-06.mp3 +0 -0
  15. package/src/assets/audio/alert-07.aac +0 -0
  16. package/src/assets/audio/alert-07.mp3 +0 -0
  17. package/src/assets/audio/alert-08.aac +0 -0
  18. package/src/assets/audio/alert-08.mp3 +0 -0
  19. package/src/assets/audio/alert-09.aac +0 -0
  20. package/src/assets/audio/alert-09.mp3 +0 -0
  21. package/src/assets/audio/alert-10.aac +0 -0
  22. package/src/assets/audio/alert-10.mp3 +0 -0
  23. package/src/assets/audio/bip-bop-01.aac +0 -0
  24. package/src/assets/audio/bip-bop-01.mp3 +0 -0
  25. package/src/assets/audio/bip-bop-02.aac +0 -0
  26. package/src/assets/audio/bip-bop-02.mp3 +0 -0
  27. package/src/assets/audio/bip-bop-03.aac +0 -0
  28. package/src/assets/audio/bip-bop-03.mp3 +0 -0
  29. package/src/assets/audio/bip-bop-04.aac +0 -0
  30. package/src/assets/audio/bip-bop-04.mp3 +0 -0
  31. package/src/assets/audio/bip-bop-05.aac +0 -0
  32. package/src/assets/audio/bip-bop-05.mp3 +0 -0
  33. package/src/assets/audio/bip-bop-06.aac +0 -0
  34. package/src/assets/audio/bip-bop-06.mp3 +0 -0
  35. package/src/assets/audio/bip-bop-07.aac +0 -0
  36. package/src/assets/audio/bip-bop-07.mp3 +0 -0
  37. package/src/assets/audio/bip-bop-08.aac +0 -0
  38. package/src/assets/audio/bip-bop-08.mp3 +0 -0
  39. package/src/assets/audio/bip-bop-09.aac +0 -0
  40. package/src/assets/audio/bip-bop-09.mp3 +0 -0
  41. package/src/assets/audio/bip-bop-10.aac +0 -0
  42. package/src/assets/audio/bip-bop-10.mp3 +0 -0
  43. package/src/assets/audio/nope-01.aac +0 -0
  44. package/src/assets/audio/nope-01.mp3 +0 -0
  45. package/src/assets/audio/nope-02.aac +0 -0
  46. package/src/assets/audio/nope-02.mp3 +0 -0
  47. package/src/assets/audio/nope-03.aac +0 -0
  48. package/src/assets/audio/nope-03.mp3 +0 -0
  49. package/src/assets/audio/nope-04.aac +0 -0
  50. package/src/assets/audio/nope-04.mp3 +0 -0
  51. package/src/assets/audio/nope-05.aac +0 -0
  52. package/src/assets/audio/nope-05.mp3 +0 -0
  53. package/src/assets/audio/nope-06.aac +0 -0
  54. package/src/assets/audio/nope-06.mp3 +0 -0
  55. package/src/assets/audio/nope-07.aac +0 -0
  56. package/src/assets/audio/nope-07.mp3 +0 -0
  57. package/src/assets/audio/nope-08.aac +0 -0
  58. package/src/assets/audio/nope-08.mp3 +0 -0
  59. package/src/assets/audio/nope-09.aac +0 -0
  60. package/src/assets/audio/nope-09.mp3 +0 -0
  61. package/src/assets/audio/nope-10.aac +0 -0
  62. package/src/assets/audio/nope-10.mp3 +0 -0
  63. package/src/assets/audio/nope-11.aac +0 -0
  64. package/src/assets/audio/nope-11.mp3 +0 -0
  65. package/src/assets/audio/nope-12.aac +0 -0
  66. package/src/assets/audio/nope-12.mp3 +0 -0
  67. package/src/assets/audio/staplebops-01.aac +0 -0
  68. package/src/assets/audio/staplebops-01.mp3 +0 -0
  69. package/src/assets/audio/staplebops-02.aac +0 -0
  70. package/src/assets/audio/staplebops-02.mp3 +0 -0
  71. package/src/assets/audio/staplebops-03.aac +0 -0
  72. package/src/assets/audio/staplebops-03.mp3 +0 -0
  73. package/src/assets/audio/staplebops-04.aac +0 -0
  74. package/src/assets/audio/staplebops-04.mp3 +0 -0
  75. package/src/assets/audio/staplebops-05.aac +0 -0
  76. package/src/assets/audio/staplebops-05.mp3 +0 -0
  77. package/src/assets/audio/staplebops-06.aac +0 -0
  78. package/src/assets/audio/staplebops-06.mp3 +0 -0
  79. package/src/assets/audio/staplebops-07.aac +0 -0
  80. package/src/assets/audio/staplebops-07.mp3 +0 -0
  81. package/src/assets/audio/yup-01.aac +0 -0
  82. package/src/assets/audio/yup-01.mp3 +0 -0
  83. package/src/assets/audio/yup-02.aac +0 -0
  84. package/src/assets/audio/yup-02.mp3 +0 -0
  85. package/src/assets/audio/yup-03.aac +0 -0
  86. package/src/assets/audio/yup-03.mp3 +0 -0
  87. package/src/assets/audio/yup-04.aac +0 -0
  88. package/src/assets/audio/yup-04.mp3 +0 -0
  89. package/src/assets/audio/yup-05.aac +0 -0
  90. package/src/assets/audio/yup-05.mp3 +0 -0
  91. package/src/assets/audio/yup-06.aac +0 -0
  92. package/src/assets/audio/yup-06.mp3 +0 -0
  93. package/src/assets/fonts/Inter.ttf +0 -0
  94. package/src/assets/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  95. package/src/assets/icons/app/android-studio.svg +369 -0
  96. package/src/assets/icons/app/antigravity.svg +97 -0
  97. package/src/assets/icons/app/cursor.svg +16 -0
  98. package/src/assets/icons/app/file-explorer.svg +20 -0
  99. package/src/assets/icons/app/finder.png +0 -0
  100. package/src/assets/icons/app/ghostty.svg +13 -0
  101. package/src/assets/icons/app/iterm2.svg +13 -0
  102. package/src/assets/icons/app/powershell.svg +14 -0
  103. package/src/assets/icons/app/sublimetext.svg +17 -0
  104. package/src/assets/icons/app/terminal.png +0 -0
  105. package/src/assets/icons/app/textmate.png +0 -0
  106. package/src/assets/icons/app/vscode.svg +39 -0
  107. package/src/assets/icons/app/warp.png +0 -0
  108. package/src/assets/icons/app/xcode.png +0 -0
  109. package/src/assets/icons/app/zed-dark.svg +15 -0
  110. package/src/assets/icons/app/zed.svg +15 -0
  111. package/src/components/accordion.css +123 -0
  112. package/src/components/accordion.tsx +92 -0
  113. package/src/components/animated-number.css +75 -0
  114. package/src/components/animated-number.tsx +109 -0
  115. package/src/components/app-icon.css +5 -0
  116. package/src/components/app-icon.tsx +85 -0
  117. package/src/components/app-icons/sprite.svg +114 -0
  118. package/src/components/app-icons/types.ts +21 -0
  119. package/src/components/avatar.css +49 -0
  120. package/src/components/avatar.tsx +55 -0
  121. package/src/components/button.css +194 -0
  122. package/src/components/button.tsx +33 -0
  123. package/src/components/card.css +94 -0
  124. package/src/components/card.tsx +123 -0
  125. package/src/components/checkbox.css +131 -0
  126. package/src/components/checkbox.tsx +43 -0
  127. package/src/components/collapsible.css +148 -0
  128. package/src/components/collapsible.tsx +48 -0
  129. package/src/components/context-menu.css +134 -0
  130. package/src/components/context-menu.tsx +308 -0
  131. package/src/components/dialog.css +181 -0
  132. package/src/components/dialog.tsx +72 -0
  133. package/src/components/diff-changes.css +42 -0
  134. package/src/components/diff-changes.tsx +115 -0
  135. package/src/components/dock-surface.css +23 -0
  136. package/src/components/dock-surface.tsx +54 -0
  137. package/src/components/dropdown-menu.css +135 -0
  138. package/src/components/dropdown-menu.tsx +308 -0
  139. package/src/components/favicon.tsx +13 -0
  140. package/src/components/file-icon.css +26 -0
  141. package/src/components/file-icon.tsx +588 -0
  142. package/src/components/file-icons/sprite.svg +11707 -0
  143. package/src/components/file-icons/types.ts +1095 -0
  144. package/src/components/font.tsx +1 -0
  145. package/src/components/hover-card.css +61 -0
  146. package/src/components/hover-card.tsx +32 -0
  147. package/src/components/icon-button.css +181 -0
  148. package/src/components/icon-button.tsx +29 -0
  149. package/src/components/icon.css +34 -0
  150. package/src/components/icon.tsx +169 -0
  151. package/src/components/image-preview.css +63 -0
  152. package/src/components/image-preview.tsx +32 -0
  153. package/src/components/inline-input.css +17 -0
  154. package/src/components/inline-input.tsx +22 -0
  155. package/src/components/keybind.css +18 -0
  156. package/src/components/keybind.tsx +20 -0
  157. package/src/components/list.css +331 -0
  158. package/src/components/list.tsx +394 -0
  159. package/src/components/logo.css +4 -0
  160. package/src/components/logo.tsx +62 -0
  161. package/src/components/motion-spring.tsx +58 -0
  162. package/src/components/popover.css +98 -0
  163. package/src/components/popover.tsx +153 -0
  164. package/src/components/progress-circle.css +12 -0
  165. package/src/components/progress-circle.tsx +57 -0
  166. package/src/components/progress.css +63 -0
  167. package/src/components/progress.tsx +39 -0
  168. package/src/components/provider-icon.css +5 -0
  169. package/src/components/provider-icon.tsx +25 -0
  170. package/src/components/provider-icons/sprite.svg +1135 -0
  171. package/src/components/provider-icons/types.ts +105 -0
  172. package/src/components/radio-group.css +187 -0
  173. package/src/components/radio-group.tsx +83 -0
  174. package/src/components/resize-handle.css +58 -0
  175. package/src/components/resize-handle.tsx +82 -0
  176. package/src/components/scroll-view.css +66 -0
  177. package/src/components/scroll-view.tsx +250 -0
  178. package/src/components/select.css +202 -0
  179. package/src/components/select.tsx +174 -0
  180. package/src/components/spinner.css +6 -0
  181. package/src/components/spinner.tsx +52 -0
  182. package/src/components/sticky-accordion-header.css +6 -0
  183. package/src/components/sticky-accordion-header.tsx +18 -0
  184. package/src/components/switch.css +132 -0
  185. package/src/components/switch.tsx +29 -0
  186. package/src/components/tabs.css +635 -0
  187. package/src/components/tabs.tsx +125 -0
  188. package/src/components/tag.css +37 -0
  189. package/src/components/tag.tsx +22 -0
  190. package/src/components/text-field.css +134 -0
  191. package/src/components/text-field.tsx +128 -0
  192. package/src/components/text-reveal.css +150 -0
  193. package/src/components/text-reveal.tsx +143 -0
  194. package/src/components/text-shimmer.css +119 -0
  195. package/src/components/text-shimmer.tsx +62 -0
  196. package/src/components/text-strikethrough.css +27 -0
  197. package/src/components/text-strikethrough.tsx +84 -0
  198. package/src/components/toast.css +236 -0
  199. package/src/components/toast.tsx +185 -0
  200. package/src/components/tooltip.css +74 -0
  201. package/src/components/tooltip.tsx +161 -0
  202. package/src/components/typewriter.css +14 -0
  203. package/src/components/typewriter.tsx +55 -0
  204. package/src/context/dialog.tsx +197 -0
  205. package/src/context/file.tsx +10 -0
  206. package/src/context/helper.tsx +38 -0
  207. package/src/context/i18n.tsx +38 -0
  208. package/src/context/index.ts +4 -0
  209. package/src/context/marked.tsx +522 -0
  210. package/src/context/worker-pool.tsx +20 -0
  211. package/src/custom-elements.d.ts +17 -0
  212. package/src/hooks/create-auto-scroll.tsx +237 -0
  213. package/src/hooks/index.ts +2 -0
  214. package/src/hooks/use-filtered-list.tsx +134 -0
  215. package/src/i18n/ar.ts +168 -0
  216. package/src/i18n/br.ts +168 -0
  217. package/src/i18n/bs.ts +172 -0
  218. package/src/i18n/da.ts +167 -0
  219. package/src/i18n/de.ts +173 -0
  220. package/src/i18n/en.ts +176 -0
  221. package/src/i18n/es.ts +168 -0
  222. package/src/i18n/fr.ts +168 -0
  223. package/src/i18n/ja.ts +167 -0
  224. package/src/i18n/ko.ts +168 -0
  225. package/src/i18n/no.ts +171 -0
  226. package/src/i18n/pl.ts +167 -0
  227. package/src/i18n/ru.ts +167 -0
  228. package/src/i18n/th.ts +169 -0
  229. package/src/i18n/tr.ts +174 -0
  230. package/src/i18n/uk.ts +167 -0
  231. package/src/i18n/zh.ts +171 -0
  232. package/src/i18n/zht.ts +171 -0
  233. package/src/storybook/fixtures.ts +51 -0
  234. package/src/storybook/scaffold.tsx +62 -0
  235. package/src/styles/animations.css +141 -0
  236. package/src/styles/base.css +404 -0
  237. package/src/styles/colors.css +772 -0
  238. package/src/styles/index.css +53 -0
  239. package/src/styles/tailwind/colors.css +285 -0
  240. package/src/styles/tailwind/index.css +78 -0
  241. package/src/styles/tailwind/utilities.css +131 -0
  242. package/src/styles/theme.css +609 -0
  243. package/src/styles/utilities.css +118 -0
  244. package/src/theme/color.ts +299 -0
  245. package/src/theme/context.tsx +370 -0
  246. package/src/theme/default-themes.ts +116 -0
  247. package/src/theme/index.ts +78 -0
  248. package/src/theme/loader.ts +112 -0
  249. package/src/theme/resolve.ts +540 -0
  250. package/src/theme/themes/amoled.json +49 -0
  251. package/src/theme/themes/aura.json +51 -0
  252. package/src/theme/themes/ayu.json +51 -0
  253. package/src/theme/themes/carbonfox.json +53 -0
  254. package/src/theme/themes/catppuccin-frappe.json +85 -0
  255. package/src/theme/themes/catppuccin-macchiato.json +85 -0
  256. package/src/theme/themes/catppuccin.json +45 -0
  257. package/src/theme/themes/cobalt2.json +87 -0
  258. package/src/theme/themes/cursor.json +91 -0
  259. package/src/theme/themes/dracula.json +49 -0
  260. package/src/theme/themes/everforest.json +89 -0
  261. package/src/theme/themes/flexoki.json +86 -0
  262. package/src/theme/themes/github.json +85 -0
  263. package/src/theme/themes/gruvbox.json +45 -0
  264. package/src/theme/themes/kanagawa.json +89 -0
  265. package/src/theme/themes/lucent-orng.json +87 -0
  266. package/src/theme/themes/material.json +87 -0
  267. package/src/theme/themes/matrix.json +113 -0
  268. package/src/theme/themes/mercury.json +86 -0
  269. package/src/theme/themes/monokai.json +49 -0
  270. package/src/theme/themes/nightowl.json +46 -0
  271. package/src/theme/themes/nord.json +46 -0
  272. package/src/theme/themes/oc-2.json +468 -0
  273. package/src/theme/themes/one-dark.json +89 -0
  274. package/src/theme/themes/onedarkpro.json +45 -0
  275. package/src/theme/themes/opencode.json +89 -0
  276. package/src/theme/themes/orng.json +87 -0
  277. package/src/theme/themes/osaka-jade.json +88 -0
  278. package/src/theme/themes/palenight.json +85 -0
  279. package/src/theme/themes/rosepine.json +85 -0
  280. package/src/theme/themes/shadesofpurple.json +51 -0
  281. package/src/theme/themes/solarized.json +49 -0
  282. package/src/theme/themes/synthwave84.json +87 -0
  283. package/src/theme/themes/tokyonight.json +47 -0
  284. package/src/theme/themes/vercel.json +90 -0
  285. package/src/theme/themes/vesper.json +51 -0
  286. package/src/theme/themes/zenburn.json +87 -0
  287. package/src/theme/types.ts +75 -0
  288. package/src/theme/v2/avatar.ts +48 -0
  289. package/src/theme/v2/default-primitives.ts +114 -0
  290. package/src/theme/v2/foreground.ts +60 -0
  291. package/src/theme/v2/mapping.ts +138 -0
  292. package/src/theme/v2/resolve.ts +153 -0
  293. package/src/v2/components/accordion-v2.css +139 -0
  294. package/src/v2/components/accordion-v2.tsx +86 -0
  295. package/src/v2/components/avatar-v2.css +70 -0
  296. package/src/v2/components/avatar-v2.tsx +59 -0
  297. package/src/v2/components/badge-v2.css +27 -0
  298. package/src/v2/components/badge-v2.tsx +20 -0
  299. package/src/v2/components/button-v2.css +186 -0
  300. package/src/v2/components/button-v2.tsx +35 -0
  301. package/src/v2/components/checkbox-v2.css +184 -0
  302. package/src/v2/components/checkbox-v2.tsx +65 -0
  303. package/src/v2/components/dialog-v2.css +150 -0
  304. package/src/v2/components/dialog-v2.tsx +93 -0
  305. package/src/v2/components/diff-changes-v2.css +24 -0
  306. package/src/v2/components/diff-changes-v2.tsx +28 -0
  307. package/src/v2/components/field-v2.css +94 -0
  308. package/src/v2/components/field-v2.tsx +265 -0
  309. package/src/v2/components/icon-button-v2.css +155 -0
  310. package/src/v2/components/icon-button-v2.tsx +37 -0
  311. package/src/v2/components/icon.tsx +129 -0
  312. package/src/v2/components/inline-input-v2.css +218 -0
  313. package/src/v2/components/inline-input-v2.tsx +90 -0
  314. package/src/v2/components/keybind-v2.css +76 -0
  315. package/src/v2/components/keybind-v2.tsx +30 -0
  316. package/src/v2/components/line-comment-v2.css +204 -0
  317. package/src/v2/components/line-comment-v2.tsx +155 -0
  318. package/src/v2/components/menu-v2.css +190 -0
  319. package/src/v2/components/menu-v2.tsx +225 -0
  320. package/src/v2/components/project-avatar-v2.css +126 -0
  321. package/src/v2/components/project-avatar-v2.tsx +64 -0
  322. package/src/v2/components/radio-v2.css +202 -0
  323. package/src/v2/components/radio-v2.tsx +72 -0
  324. package/src/v2/components/segmented-control-v2.css +80 -0
  325. package/src/v2/components/segmented-control-v2.tsx +208 -0
  326. package/src/v2/components/select-v2.css +285 -0
  327. package/src/v2/components/select-v2.tsx +208 -0
  328. package/src/v2/components/switch-v2.css +154 -0
  329. package/src/v2/components/switch-v2.tsx +28 -0
  330. package/src/v2/components/tab-state-indicator.tsx +37 -0
  331. package/src/v2/components/tabs-v2.css +225 -0
  332. package/src/v2/components/tabs-v2.tsx +147 -0
  333. package/src/v2/components/text-input-v2.css +145 -0
  334. package/src/v2/components/text-input-v2.tsx +67 -0
  335. package/src/v2/components/text-shimmer-v2.css +125 -0
  336. package/src/v2/components/text-shimmer-v2.tsx +63 -0
  337. package/src/v2/components/textarea-v2.css +78 -0
  338. package/src/v2/components/textarea-v2.tsx +31 -0
  339. package/src/v2/components/toast-v2.css +215 -0
  340. package/src/v2/components/toast-v2.tsx +144 -0
  341. package/src/v2/components/tooltip-v2.css +53 -0
  342. package/src/v2/components/tooltip-v2.tsx +146 -0
  343. package/src/v2/components/wordmark-v2.tsx +92 -0
  344. package/src/v2/styles/colors.css +172 -0
  345. package/src/v2/styles/tailwind.css +2 -0
  346. package/src/v2/styles/theme.css +441 -0
@@ -0,0 +1,370 @@
1
+ // @refresh reload
2
+
3
+ import { createEffect, onMount } from "solid-js"
4
+ import { createStore } from "solid-js/store"
5
+ import { makeEventListener } from "@solid-primitives/event-listener"
6
+ import { createSimpleContext } from "../context/helper"
7
+ import oc2ThemeJson from "./themes/oc-2.json"
8
+ import { resolveThemeVariant, themeToCss } from "./resolve"
9
+ import { resolveThemeVariantV2, themeV2ToCss } from "./v2/resolve"
10
+ import type { DesktopTheme } from "./types"
11
+
12
+ export type ColorScheme = "light" | "dark" | "system"
13
+
14
+ const STORAGE_KEYS = {
15
+ THEME_ID: "opencode-theme-id",
16
+ COLOR_SCHEME: "opencode-color-scheme",
17
+ THEME_CSS_LIGHT: "opencode-theme-css-light",
18
+ THEME_CSS_DARK: "opencode-theme-css-dark",
19
+ } as const
20
+
21
+ const THEME_STYLE_ID = "oc-theme"
22
+ let files: Record<string, () => Promise<{ default: DesktopTheme }>> | undefined
23
+ let ids: string[] | undefined
24
+ let known: Set<string> | undefined
25
+
26
+ function getFiles() {
27
+ if (files) return files
28
+ files = import.meta.glob<{ default: DesktopTheme }>("./themes/*.json")
29
+ return files
30
+ }
31
+
32
+ function themeIDs() {
33
+ if (ids) return ids
34
+ ids = Object.keys(getFiles())
35
+ .map((path) => path.slice("./themes/".length, -".json".length))
36
+ .sort()
37
+ return ids
38
+ }
39
+
40
+ function knownThemes() {
41
+ if (known) return known
42
+ known = new Set(themeIDs())
43
+ return known
44
+ }
45
+
46
+ const names: Record<string, string> = {
47
+ "oc-2": "OC-2",
48
+ amoled: "AMOLED",
49
+ aura: "Aura",
50
+ ayu: "Ayu",
51
+ carbonfox: "Carbonfox",
52
+ catppuccin: "Catppuccin",
53
+ "catppuccin-frappe": "Catppuccin Frappe",
54
+ "catppuccin-macchiato": "Catppuccin Macchiato",
55
+ cobalt2: "Cobalt2",
56
+ cursor: "Cursor",
57
+ dracula: "Dracula",
58
+ everforest: "Everforest",
59
+ flexoki: "Flexoki",
60
+ github: "GitHub",
61
+ gruvbox: "Gruvbox",
62
+ kanagawa: "Kanagawa",
63
+ "lucent-orng": "Lucent Orng",
64
+ material: "Material",
65
+ matrix: "Matrix",
66
+ mercury: "Mercury",
67
+ monokai: "Monokai",
68
+ nightowl: "Night Owl",
69
+ nord: "Nord",
70
+ "one-dark": "One Dark",
71
+ onedarkpro: "One Dark Pro",
72
+ opencode: "OpenCode",
73
+ orng: "Orng",
74
+ "osaka-jade": "Osaka Jade",
75
+ palenight: "Palenight",
76
+ rosepine: "Rose Pine",
77
+ shadesofpurple: "Shades of Purple",
78
+ solarized: "Solarized",
79
+ synthwave84: "Synthwave '84",
80
+ tokyonight: "Tokyonight",
81
+ vercel: "Vercel",
82
+ vesper: "Vesper",
83
+ zenburn: "Zenburn",
84
+ }
85
+ const oc2Theme = oc2ThemeJson as DesktopTheme
86
+
87
+ function normalize(id: string | null | undefined) {
88
+ return id === "oc-1" ? "oc-2" : id
89
+ }
90
+
91
+ function read(key: string) {
92
+ if (typeof localStorage !== "object") return null
93
+ try {
94
+ return localStorage.getItem(key)
95
+ } catch {
96
+ return null
97
+ }
98
+ }
99
+
100
+ function write(key: string, value: string) {
101
+ if (typeof localStorage !== "object") return
102
+ try {
103
+ localStorage.setItem(key, value)
104
+ } catch {}
105
+ }
106
+
107
+ function drop(key: string) {
108
+ if (typeof localStorage !== "object") return
109
+ try {
110
+ localStorage.removeItem(key)
111
+ } catch {}
112
+ }
113
+
114
+ function clear() {
115
+ drop(STORAGE_KEYS.THEME_CSS_LIGHT)
116
+ drop(STORAGE_KEYS.THEME_CSS_DARK)
117
+ }
118
+
119
+ function ensureThemeStyleElement(): HTMLStyleElement {
120
+ const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null
121
+ if (existing) return existing
122
+ const element = document.createElement("style")
123
+ element.id = THEME_STYLE_ID
124
+ document.head.appendChild(element)
125
+ return element
126
+ }
127
+
128
+ function getSystemMode(): "light" | "dark" {
129
+ if (typeof window !== "object") return "light"
130
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
131
+ }
132
+
133
+ function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "dark") {
134
+ const isDark = mode === "dark"
135
+ const variant = isDark ? theme.dark : theme.light
136
+ const tokens = resolveThemeVariant(variant, isDark)
137
+ const css = themeToCss(tokens)
138
+ const v2 = themeV2ToCss(resolveThemeVariantV2(variant, isDark))
139
+
140
+ if (themeId !== "oc-2") {
141
+ write(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, `${css}\n ${v2}`)
142
+ }
143
+
144
+ const fullCss = `:root {
145
+ color-scheme: ${mode};
146
+ --text-mix-blend-mode: ${isDark ? "plus-lighter" : "multiply"};
147
+ ${css}
148
+ ${v2}
149
+ }`
150
+
151
+ document.getElementById("oc-theme-preload")?.remove()
152
+ ensureThemeStyleElement().textContent = fullCss
153
+ document.documentElement.dataset.theme = themeId
154
+ document.documentElement.dataset.colorScheme = mode
155
+ document.documentElement.style.backgroundColor = isDark ? "#080808" : "#fafafa"
156
+
157
+ // Update theme-color meta tag to match light/dark mode
158
+ const meta = document.querySelector('meta[name="theme-color"]')
159
+ if (meta) meta.setAttribute("content", isDark ? "#080808" : "#fafafa")
160
+ }
161
+
162
+ function cacheThemeVariants(theme: DesktopTheme, themeId: string) {
163
+ if (themeId === "oc-2") return
164
+ for (const mode of ["light", "dark"] as const) {
165
+ const isDark = mode === "dark"
166
+ const variant = isDark ? theme.dark : theme.light
167
+ const tokens = resolveThemeVariant(variant, isDark)
168
+ const css = themeToCss(tokens)
169
+ const v2 = themeV2ToCss(resolveThemeVariantV2(variant, isDark))
170
+ write(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, `${css}\n ${v2}`)
171
+ }
172
+ }
173
+
174
+ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
175
+ name: "Theme",
176
+ init: (props: { defaultTheme?: string; onThemeApplied?: (theme: DesktopTheme, mode: "light" | "dark") => void }) => {
177
+ const themeId = normalize(read(STORAGE_KEYS.THEME_ID) ?? props.defaultTheme) ?? "oc-2"
178
+ const colorScheme = (read(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null) ?? "system"
179
+ const mode = colorScheme === "system" ? getSystemMode() : colorScheme
180
+ const [store, setStore] = createStore({
181
+ themes: {
182
+ "oc-2": oc2Theme,
183
+ } as Record<string, DesktopTheme>,
184
+ themeId,
185
+ colorScheme,
186
+ mode,
187
+ previewThemeId: null as string | null,
188
+ previewScheme: null as ColorScheme | null,
189
+ })
190
+
191
+ const loads = new Map<string, Promise<DesktopTheme | undefined>>()
192
+
193
+ const load = (id: string) => {
194
+ const next = normalize(id)
195
+ if (!next) return Promise.resolve(undefined)
196
+ const hit = store.themes[next]
197
+ if (hit) return Promise.resolve(hit)
198
+ const pending = loads.get(next)
199
+ if (pending) return pending
200
+ const file = getFiles()[`./themes/${next}.json`]
201
+ if (!file) return Promise.resolve(undefined)
202
+ const task = file()
203
+ .then((mod) => {
204
+ const theme = mod.default
205
+ setStore("themes", next, theme)
206
+ return theme
207
+ })
208
+ .finally(() => {
209
+ loads.delete(next)
210
+ })
211
+ loads.set(next, task)
212
+ return task
213
+ }
214
+
215
+ const applyTheme = (theme: DesktopTheme, themeId: string, mode: "light" | "dark") => {
216
+ applyThemeCss(theme, themeId, mode)
217
+ props.onThemeApplied?.(theme, mode)
218
+ }
219
+
220
+ const ids = () => {
221
+ const extra = Object.keys(store.themes)
222
+ .filter((id) => !knownThemes().has(id))
223
+ .sort()
224
+ const all = themeIDs()
225
+ if (extra.length === 0) return all
226
+ return [...all, ...extra]
227
+ }
228
+
229
+ const loadThemes = () => Promise.all(themeIDs().map(load)).then(() => store.themes)
230
+
231
+ const onStorage = (e: StorageEvent) => {
232
+ if (e.key === STORAGE_KEYS.THEME_ID && e.newValue) {
233
+ const next = normalize(e.newValue)
234
+ if (!next) return
235
+ if (next !== "oc-2" && !knownThemes().has(next) && !store.themes[next]) return
236
+ setStore("themeId", next)
237
+ if (next === "oc-2") {
238
+ clear()
239
+ return
240
+ }
241
+ void load(next).then((theme) => {
242
+ if (!theme || store.themeId !== next) return
243
+ cacheThemeVariants(theme, next)
244
+ })
245
+ }
246
+ if (e.key === STORAGE_KEYS.COLOR_SCHEME && e.newValue) {
247
+ setStore("colorScheme", e.newValue as ColorScheme)
248
+ setStore("mode", e.newValue === "system" ? getSystemMode() : (e.newValue as "light" | "dark"))
249
+ }
250
+ }
251
+
252
+ onMount(() => {
253
+ makeEventListener(window, "storage", onStorage)
254
+
255
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
256
+ const onMedia = () => {
257
+ if (store.colorScheme !== "system") return
258
+ setStore("mode", getSystemMode())
259
+ }
260
+ makeEventListener(mediaQuery, "change", onMedia)
261
+
262
+ const rawTheme = read(STORAGE_KEYS.THEME_ID)
263
+ const savedTheme = normalize(rawTheme ?? props.defaultTheme) ?? "oc-2"
264
+ const savedScheme = (read(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null) ?? "system"
265
+ if (rawTheme && rawTheme !== savedTheme) {
266
+ write(STORAGE_KEYS.THEME_ID, savedTheme)
267
+ clear()
268
+ }
269
+ if (savedTheme !== store.themeId) setStore("themeId", savedTheme)
270
+ if (savedScheme !== store.colorScheme) setStore("colorScheme", savedScheme)
271
+ setStore("mode", savedScheme === "system" ? getSystemMode() : savedScheme)
272
+ void load(savedTheme).then((theme) => {
273
+ if (!theme || store.themeId !== savedTheme) return
274
+ cacheThemeVariants(theme, savedTheme)
275
+ })
276
+ })
277
+
278
+ createEffect(() => {
279
+ const theme = store.themes[store.themeId]
280
+ if (!theme) return
281
+ applyTheme(theme, store.themeId, store.mode)
282
+ })
283
+
284
+ const setTheme = (id: string) => {
285
+ const next = normalize(id)
286
+ if (!next) {
287
+ console.warn(`Theme "${id}" not found`)
288
+ return
289
+ }
290
+ if (next !== "oc-2" && !knownThemes().has(next) && !store.themes[next]) {
291
+ console.warn(`Theme "${id}" not found`)
292
+ return
293
+ }
294
+ setStore("themeId", next)
295
+ if (next === "oc-2") {
296
+ write(STORAGE_KEYS.THEME_ID, next)
297
+ clear()
298
+ return
299
+ }
300
+ void load(next).then((theme) => {
301
+ if (!theme || store.themeId !== next) return
302
+ cacheThemeVariants(theme, next)
303
+ write(STORAGE_KEYS.THEME_ID, next)
304
+ })
305
+ }
306
+
307
+ const setColorScheme = (scheme: ColorScheme) => {
308
+ setStore("colorScheme", scheme)
309
+ write(STORAGE_KEYS.COLOR_SCHEME, scheme)
310
+ setStore("mode", scheme === "system" ? getSystemMode() : scheme)
311
+ }
312
+
313
+ return {
314
+ themeId: () => store.themeId,
315
+ colorScheme: () => store.colorScheme,
316
+ mode: () => store.mode,
317
+ ids,
318
+ name: (id: string) => store.themes[id]?.name ?? names[id] ?? id,
319
+ loadThemes,
320
+ themes: () => store.themes,
321
+ setTheme,
322
+ setColorScheme,
323
+ registerTheme: (theme: DesktopTheme) => setStore("themes", theme.id, theme),
324
+ previewTheme: (id: string) => {
325
+ const next = normalize(id)
326
+ if (!next) return
327
+ if (next !== "oc-2" && !knownThemes().has(next) && !store.themes[next]) return
328
+ setStore("previewThemeId", next)
329
+ void load(next).then((theme) => {
330
+ if (!theme || store.previewThemeId !== next) return
331
+ const mode = store.previewScheme
332
+ ? store.previewScheme === "system"
333
+ ? getSystemMode()
334
+ : store.previewScheme
335
+ : store.mode
336
+ applyTheme(theme, next, mode)
337
+ })
338
+ },
339
+ previewColorScheme: (scheme: ColorScheme) => {
340
+ setStore("previewScheme", scheme)
341
+ const mode = scheme === "system" ? getSystemMode() : scheme
342
+ const id = store.previewThemeId ?? store.themeId
343
+ void load(id).then((theme) => {
344
+ if (!theme) return
345
+ if ((store.previewThemeId ?? store.themeId) !== id) return
346
+ if (store.previewScheme !== scheme) return
347
+ applyTheme(theme, id, mode)
348
+ })
349
+ },
350
+ commitPreview: () => {
351
+ if (store.previewThemeId) {
352
+ setTheme(store.previewThemeId)
353
+ }
354
+ if (store.previewScheme) {
355
+ setColorScheme(store.previewScheme)
356
+ }
357
+ setStore("previewThemeId", null)
358
+ setStore("previewScheme", null)
359
+ },
360
+ cancelPreview: () => {
361
+ setStore("previewThemeId", null)
362
+ setStore("previewScheme", null)
363
+ void load(store.themeId).then((theme) => {
364
+ if (!theme) return
365
+ applyTheme(theme, store.themeId, store.mode)
366
+ })
367
+ },
368
+ }
369
+ },
370
+ })
@@ -0,0 +1,116 @@
1
+ import type { DesktopTheme } from "./types"
2
+ import oc2ThemeJson from "./themes/oc-2.json"
3
+ import amoledThemeJson from "./themes/amoled.json"
4
+ import auraThemeJson from "./themes/aura.json"
5
+ import ayuThemeJson from "./themes/ayu.json"
6
+ import carbonfoxThemeJson from "./themes/carbonfox.json"
7
+ import catppuccinThemeJson from "./themes/catppuccin.json"
8
+ import catppuccinFrappeThemeJson from "./themes/catppuccin-frappe.json"
9
+ import catppuccinMacchiatoThemeJson from "./themes/catppuccin-macchiato.json"
10
+ import cobalt2ThemeJson from "./themes/cobalt2.json"
11
+ import cursorThemeJson from "./themes/cursor.json"
12
+ import draculaThemeJson from "./themes/dracula.json"
13
+ import everforestThemeJson from "./themes/everforest.json"
14
+ import flexokiThemeJson from "./themes/flexoki.json"
15
+ import githubThemeJson from "./themes/github.json"
16
+ import gruvboxThemeJson from "./themes/gruvbox.json"
17
+ import kanagawaThemeJson from "./themes/kanagawa.json"
18
+ import lucentOrngThemeJson from "./themes/lucent-orng.json"
19
+ import materialThemeJson from "./themes/material.json"
20
+ import matrixThemeJson from "./themes/matrix.json"
21
+ import mercuryThemeJson from "./themes/mercury.json"
22
+ import monokaiThemeJson from "./themes/monokai.json"
23
+ import nightowlThemeJson from "./themes/nightowl.json"
24
+ import nordThemeJson from "./themes/nord.json"
25
+ import oneDarkThemeJson from "./themes/one-dark.json"
26
+ import oneDarkProThemeJson from "./themes/onedarkpro.json"
27
+ import opencodeThemeJson from "./themes/opencode.json"
28
+ import orngThemeJson from "./themes/orng.json"
29
+ import osakaJadeThemeJson from "./themes/osaka-jade.json"
30
+ import palenightThemeJson from "./themes/palenight.json"
31
+ import rosepineThemeJson from "./themes/rosepine.json"
32
+ import shadesOfPurpleThemeJson from "./themes/shadesofpurple.json"
33
+ import solarizedThemeJson from "./themes/solarized.json"
34
+ import synthwave84ThemeJson from "./themes/synthwave84.json"
35
+ import tokyonightThemeJson from "./themes/tokyonight.json"
36
+ import vercelThemeJson from "./themes/vercel.json"
37
+ import vesperThemeJson from "./themes/vesper.json"
38
+ import zenburnThemeJson from "./themes/zenburn.json"
39
+
40
+ export const oc2Theme = oc2ThemeJson as DesktopTheme
41
+ export const amoledTheme = amoledThemeJson as DesktopTheme
42
+ export const auraTheme = auraThemeJson as DesktopTheme
43
+ export const ayuTheme = ayuThemeJson as DesktopTheme
44
+ export const carbonfoxTheme = carbonfoxThemeJson as DesktopTheme
45
+ export const catppuccinTheme = catppuccinThemeJson as DesktopTheme
46
+ export const catppuccinFrappeTheme = catppuccinFrappeThemeJson as DesktopTheme
47
+ export const catppuccinMacchiatoTheme = catppuccinMacchiatoThemeJson as DesktopTheme
48
+ export const cobalt2Theme = cobalt2ThemeJson as DesktopTheme
49
+ export const cursorTheme = cursorThemeJson as DesktopTheme
50
+ export const draculaTheme = draculaThemeJson as DesktopTheme
51
+ export const everforestTheme = everforestThemeJson as DesktopTheme
52
+ export const flexokiTheme = flexokiThemeJson as DesktopTheme
53
+ export const githubTheme = githubThemeJson as DesktopTheme
54
+ export const gruvboxTheme = gruvboxThemeJson as DesktopTheme
55
+ export const kanagawaTheme = kanagawaThemeJson as DesktopTheme
56
+ export const lucentOrngTheme = lucentOrngThemeJson as DesktopTheme
57
+ export const materialTheme = materialThemeJson as DesktopTheme
58
+ export const matrixTheme = matrixThemeJson as DesktopTheme
59
+ export const mercuryTheme = mercuryThemeJson as DesktopTheme
60
+ export const monokaiTheme = monokaiThemeJson as DesktopTheme
61
+ export const nightowlTheme = nightowlThemeJson as DesktopTheme
62
+ export const nordTheme = nordThemeJson as DesktopTheme
63
+ export const oneDarkTheme = oneDarkThemeJson as DesktopTheme
64
+ export const oneDarkProTheme = oneDarkProThemeJson as DesktopTheme
65
+ export const opencodeTheme = opencodeThemeJson as DesktopTheme
66
+ export const orngTheme = orngThemeJson as DesktopTheme
67
+ export const osakaJadeTheme = osakaJadeThemeJson as DesktopTheme
68
+ export const palenightTheme = palenightThemeJson as DesktopTheme
69
+ export const rosepineTheme = rosepineThemeJson as DesktopTheme
70
+ export const shadesOfPurpleTheme = shadesOfPurpleThemeJson as DesktopTheme
71
+ export const solarizedTheme = solarizedThemeJson as DesktopTheme
72
+ export const synthwave84Theme = synthwave84ThemeJson as DesktopTheme
73
+ export const tokyonightTheme = tokyonightThemeJson as DesktopTheme
74
+ export const vercelTheme = vercelThemeJson as DesktopTheme
75
+ export const vesperTheme = vesperThemeJson as DesktopTheme
76
+ export const zenburnTheme = zenburnThemeJson as DesktopTheme
77
+
78
+ export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
79
+ "oc-2": oc2Theme,
80
+ amoled: amoledTheme,
81
+ aura: auraTheme,
82
+ ayu: ayuTheme,
83
+ carbonfox: carbonfoxTheme,
84
+ catppuccin: catppuccinTheme,
85
+ "catppuccin-frappe": catppuccinFrappeTheme,
86
+ "catppuccin-macchiato": catppuccinMacchiatoTheme,
87
+ cobalt2: cobalt2Theme,
88
+ cursor: cursorTheme,
89
+ dracula: draculaTheme,
90
+ everforest: everforestTheme,
91
+ flexoki: flexokiTheme,
92
+ github: githubTheme,
93
+ gruvbox: gruvboxTheme,
94
+ kanagawa: kanagawaTheme,
95
+ "lucent-orng": lucentOrngTheme,
96
+ material: materialTheme,
97
+ matrix: matrixTheme,
98
+ mercury: mercuryTheme,
99
+ monokai: monokaiTheme,
100
+ nightowl: nightowlTheme,
101
+ nord: nordTheme,
102
+ "one-dark": oneDarkTheme,
103
+ onedarkpro: oneDarkProTheme,
104
+ opencode: opencodeTheme,
105
+ orng: orngTheme,
106
+ "osaka-jade": osakaJadeTheme,
107
+ palenight: palenightTheme,
108
+ rosepine: rosepineTheme,
109
+ shadesofpurple: shadesOfPurpleTheme,
110
+ solarized: solarizedTheme,
111
+ synthwave84: synthwave84Theme,
112
+ tokyonight: tokyonightTheme,
113
+ vercel: vercelTheme,
114
+ vesper: vesperTheme,
115
+ zenburn: zenburnTheme,
116
+ }
@@ -0,0 +1,78 @@
1
+ export type {
2
+ DesktopTheme,
3
+ ThemePaletteColors,
4
+ ThemeSeedColors,
5
+ ThemeVariant,
6
+ HexColor,
7
+ OklchColor,
8
+ ResolvedTheme,
9
+ ColorValue,
10
+ CssVarRef,
11
+ V2ColorValue,
12
+ ResolvedV2Theme,
13
+ } from "./types"
14
+
15
+ export {
16
+ hexToRgb,
17
+ rgbToHex,
18
+ hexToOklch,
19
+ oklchToHex,
20
+ rgbToOklch,
21
+ oklchToRgb,
22
+ generateScale,
23
+ generateNeutralScale,
24
+ generateAlphaScale,
25
+ fitOklch,
26
+ blend,
27
+ mixColors,
28
+ shift,
29
+ lighten,
30
+ darken,
31
+ withAlpha,
32
+ } from "./color"
33
+
34
+ export { resolveThemeVariant, resolveTheme, themeToCss } from "./resolve"
35
+ export { resolveThemeVariantV2, resolveThemeV2, themeV2ToCss, generateV2Primitives } from "./v2/resolve"
36
+ export { applyTheme, loadThemeFromUrl, getActiveTheme, removeTheme, setColorScheme } from "./loader"
37
+ export { ThemeProvider, useTheme, type ColorScheme } from "./context"
38
+
39
+ export {
40
+ DEFAULT_THEMES,
41
+ oc2Theme,
42
+ amoledTheme,
43
+ auraTheme,
44
+ ayuTheme,
45
+ carbonfoxTheme,
46
+ catppuccinTheme,
47
+ catppuccinFrappeTheme,
48
+ catppuccinMacchiatoTheme,
49
+ cobalt2Theme,
50
+ cursorTheme,
51
+ draculaTheme,
52
+ everforestTheme,
53
+ flexokiTheme,
54
+ githubTheme,
55
+ gruvboxTheme,
56
+ kanagawaTheme,
57
+ lucentOrngTheme,
58
+ materialTheme,
59
+ matrixTheme,
60
+ mercuryTheme,
61
+ monokaiTheme,
62
+ nightowlTheme,
63
+ nordTheme,
64
+ oneDarkTheme,
65
+ oneDarkProTheme,
66
+ opencodeTheme,
67
+ orngTheme,
68
+ osakaJadeTheme,
69
+ palenightTheme,
70
+ rosepineTheme,
71
+ shadesOfPurpleTheme,
72
+ solarizedTheme,
73
+ synthwave84Theme,
74
+ tokyonightTheme,
75
+ vercelTheme,
76
+ vesperTheme,
77
+ zenburnTheme,
78
+ } from "./default-themes"
@@ -0,0 +1,112 @@
1
+ import type { DesktopTheme, ResolvedTheme, ResolvedV2Theme } from "./types"
2
+ import { resolveThemeVariant, themeToCss } from "./resolve"
3
+ import { resolveThemeVariantV2, themeV2ToCss } from "./v2/resolve"
4
+
5
+ let activeTheme: DesktopTheme | null = null
6
+ const THEME_STYLE_ID = "opencode-theme"
7
+
8
+ function ensureLoaderStyleElement(): HTMLStyleElement {
9
+ const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null
10
+ if (existing) {
11
+ return existing
12
+ }
13
+ const element = document.createElement("style")
14
+ element.id = THEME_STYLE_ID
15
+ document.head.appendChild(element)
16
+ return element
17
+ }
18
+
19
+ export function applyTheme(theme: DesktopTheme, themeId?: string): void {
20
+ activeTheme = theme
21
+ const lightTokens = resolveThemeVariant(theme.light, false)
22
+ const darkTokens = resolveThemeVariant(theme.dark, true)
23
+ const lightV2Tokens = resolveThemeVariantV2(theme.light, false)
24
+ const darkV2Tokens = resolveThemeVariantV2(theme.dark, true)
25
+ const targetThemeId = themeId ?? theme.id
26
+ const css = buildThemeCss(lightTokens, darkTokens, lightV2Tokens, darkV2Tokens, targetThemeId)
27
+ const themeStyleElement = ensureLoaderStyleElement()
28
+ themeStyleElement.textContent = css
29
+ document.documentElement.setAttribute("data-theme", targetThemeId)
30
+ }
31
+
32
+ function buildThemeCss(
33
+ light: ResolvedTheme,
34
+ dark: ResolvedTheme,
35
+ lightV2: ResolvedV2Theme,
36
+ darkV2: ResolvedV2Theme,
37
+ themeId: string,
38
+ ): string {
39
+ const isDefaultTheme = themeId === "oc-2"
40
+ const lightCss = `${themeToCss(light)}\n ${themeV2ToCss(lightV2)}`
41
+ const darkCss = `${themeToCss(dark)}\n ${themeV2ToCss(darkV2)}`
42
+
43
+ if (isDefaultTheme) {
44
+ return `
45
+ :root {
46
+ color-scheme: light;
47
+ --text-mix-blend-mode: multiply;
48
+
49
+ ${lightCss}
50
+
51
+ @media (prefers-color-scheme: dark) {
52
+ color-scheme: dark;
53
+ --text-mix-blend-mode: plus-lighter;
54
+
55
+ ${darkCss}
56
+ }
57
+ }
58
+ `
59
+ }
60
+
61
+ return `
62
+ html[data-theme="${themeId}"] {
63
+ color-scheme: light;
64
+ --text-mix-blend-mode: multiply;
65
+
66
+ ${lightCss}
67
+
68
+ @media (prefers-color-scheme: dark) {
69
+ color-scheme: dark;
70
+ --text-mix-blend-mode: plus-lighter;
71
+
72
+ ${darkCss}
73
+ }
74
+ }
75
+ `
76
+ }
77
+
78
+ export async function loadThemeFromUrl(url: string): Promise<DesktopTheme> {
79
+ const response = await fetch(url)
80
+ if (!response.ok) {
81
+ throw new Error(`Failed to load theme from ${url}: ${response.statusText}`)
82
+ }
83
+ return response.json()
84
+ }
85
+
86
+ export function getActiveTheme(): DesktopTheme | null {
87
+ const activeId = document.documentElement.getAttribute("data-theme")
88
+ if (!activeId) {
89
+ return null
90
+ }
91
+ if (activeTheme?.id === activeId) {
92
+ return activeTheme
93
+ }
94
+ return null
95
+ }
96
+
97
+ export function removeTheme(): void {
98
+ activeTheme = null
99
+ const existingElement = document.getElementById(THEME_STYLE_ID)
100
+ if (existingElement) {
101
+ existingElement.remove()
102
+ }
103
+ document.documentElement.removeAttribute("data-theme")
104
+ }
105
+
106
+ export function setColorScheme(scheme: "light" | "dark" | "auto"): void {
107
+ if (scheme === "auto") {
108
+ document.documentElement.style.removeProperty("color-scheme")
109
+ } else {
110
+ document.documentElement.style.setProperty("color-scheme", scheme)
111
+ }
112
+ }