loader.ts 3.0 KB

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