loader.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. import type { DesktopTheme, ResolvedTheme } from "./types"
  2. import { resolveThemeVariant, themeToCss } from "./resolve"
  3. let activeTheme: DesktopTheme | null = null
  4. const THEME_STYLE_ID = "opencode-theme"
  5. function ensureLoaderStyleElement(): HTMLStyleElement {
  6. const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null
  7. if (existing) {
  8. return existing
  9. }
  10. const element = document.createElement("style")
  11. element.id = THEME_STYLE_ID
  12. document.head.appendChild(element)
  13. return element
  14. }
  15. export function applyTheme(theme: DesktopTheme, themeId?: string): void {
  16. activeTheme = theme
  17. const lightTokens = resolveThemeVariant(theme.light, false)
  18. const darkTokens = resolveThemeVariant(theme.dark, true)
  19. const targetThemeId = themeId ?? theme.id
  20. const css = buildThemeCss(lightTokens, darkTokens, targetThemeId)
  21. const themeStyleElement = ensureLoaderStyleElement()
  22. themeStyleElement.textContent = css
  23. document.documentElement.setAttribute("data-theme", targetThemeId)
  24. }
  25. function buildThemeCss(light: ResolvedTheme, dark: ResolvedTheme, themeId: string): string {
  26. const isDefaultTheme = themeId === "oc-1"
  27. const lightCss = themeToCss(light)
  28. const darkCss = themeToCss(dark)
  29. if (isDefaultTheme) {
  30. return `
  31. :root {
  32. color-scheme: light;
  33. --text-mix-blend-mode: multiply;
  34. ${lightCss}
  35. @media (prefers-color-scheme: dark) {
  36. color-scheme: dark;
  37. --text-mix-blend-mode: plus-lighter;
  38. ${darkCss}
  39. }
  40. }
  41. `
  42. }
  43. return `
  44. html[data-theme="${themeId}"] {
  45. color-scheme: light;
  46. --text-mix-blend-mode: multiply;
  47. ${lightCss}
  48. @media (prefers-color-scheme: dark) {
  49. color-scheme: dark;
  50. --text-mix-blend-mode: plus-lighter;
  51. ${darkCss}
  52. }
  53. }
  54. `
  55. }
  56. export async function loadThemeFromUrl(url: string): Promise<DesktopTheme> {
  57. const response = await fetch(url)
  58. if (!response.ok) {
  59. throw new Error(`Failed to load theme from ${url}: ${response.statusText}`)
  60. }
  61. return response.json()
  62. }
  63. export function getActiveTheme(): DesktopTheme | null {
  64. const activeId = document.documentElement.getAttribute("data-theme")
  65. if (!activeId) {
  66. return null
  67. }
  68. if (activeTheme?.id === activeId) {
  69. return activeTheme
  70. }
  71. return null
  72. }
  73. export function removeTheme(): void {
  74. activeTheme = null
  75. const existingElement = document.getElementById(THEME_STYLE_ID)
  76. if (existingElement) {
  77. existingElement.remove()
  78. }
  79. document.documentElement.removeAttribute("data-theme")
  80. }
  81. export function setColorScheme(scheme: "light" | "dark" | "auto"): void {
  82. if (scheme === "auto") {
  83. document.documentElement.style.removeProperty("color-scheme")
  84. } else {
  85. document.documentElement.style.setProperty("color-scheme", scheme)
  86. }
  87. }