Adam 3 месяцев назад
Родитель
Сommit
82a876da4d

+ 29 - 2
packages/app/index.html

@@ -13,9 +13,36 @@
     <meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
     <meta property="og:image" content="/social-share.png" />
     <meta property="twitter:image" content="/social-share.png" />
-    <!-- Theme preload script - injected by Vite plugin to avoid FOUC -->
+    <!-- Theme preload script - applies cached theme to avoid FOUC -->
     <script id="oc-theme-preload-script">
-      /* THEME_PRELOAD_SCRIPT */
+      ;(function () {
+        var themeId = localStorage.getItem("opencode-theme-id")
+        if (!themeId) return
+
+        var scheme = localStorage.getItem("opencode-color-scheme") || "system"
+        var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
+        var mode = isDark ? "dark" : "light"
+
+        document.documentElement.dataset.theme = themeId
+        document.documentElement.dataset.colorScheme = mode
+
+        if (themeId === "oc-1") return
+
+        var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
+        if (css) {
+          var style = document.createElement("style")
+          style.id = "oc-theme-preload"
+          style.textContent =
+            ":root{color-scheme:" +
+            mode +
+            ";--text-mix-blend-mode:" +
+            (isDark ? "plus-lighter" : "multiply") +
+            ";" +
+            css +
+            "}"
+          document.head.appendChild(style)
+        }
+      })()
     </script>
   </head>
   <body class="antialiased overscroll-none text-12-regular overflow-hidden">

+ 0 - 18
packages/app/script/inject-theme-preload.ts

@@ -1,18 +0,0 @@
-/**
- * Injects the theme preload script into index.html.
- * Run this as part of the build process.
- */
-
-import { generatePreloadScript } from "@opencode-ai/ui/theme"
-
-const htmlPath = new URL("../index.html", import.meta.url).pathname
-const html = await Bun.file(htmlPath).text()
-
-const script = generatePreloadScript()
-const injectedHtml = html.replace(
-  /<script id="oc-theme-preload-script">\s*\/\* THEME_PRELOAD_SCRIPT \*\/\s*<\/script>/,
-  `<script id="oc-theme-preload-script">${script}</script>`,
-)
-
-await Bun.write(htmlPath, injectedHtml)
-console.log("Injected theme preload script into index.html")

+ 0 - 18
packages/app/vite.js

@@ -1,23 +1,6 @@
 import solidPlugin from "vite-plugin-solid"
 import tailwindcss from "@tailwindcss/vite"
 import { fileURLToPath } from "url"
-import { generatePreloadScript } from "@opencode-ai/ui/theme"
-
-/**
- * Vite plugin that injects the theme preload script into index.html.
- * This ensures the theme is applied before the page renders, avoiding FOUC.
- * @type {import("vite").Plugin}
- */
-const themePreloadPlugin = {
-  name: "opencode-desktop:theme-preload",
-  transformIndexHtml(html) {
-    const script = generatePreloadScript()
-    return html.replace(
-      /<script id="oc-theme-preload-script">\s*\/\* THEME_PRELOAD_SCRIPT \*\/\s*<\/script>/,
-      `<script id="oc-theme-preload-script">${script}</script>`,
-    )
-  },
-}
 
 /**
  * @type {import("vite").PluginOption}
@@ -38,7 +21,6 @@ export default [
       }
     },
   },
-  themePreloadPlugin,
   tailwindcss(),
   solidPlugin(),
 ]

+ 29 - 2
packages/desktop/index.html

@@ -13,9 +13,36 @@
     <meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
     <meta property="og:image" content="/social-share.png" />
     <meta property="twitter:image" content="/social-share.png" />
-    <!-- Theme preload script - injected by Vite plugin to avoid FOUC -->
+    <!-- Theme preload script - applies cached theme to avoid FOUC -->
     <script id="oc-theme-preload-script">
-      /* THEME_PRELOAD_SCRIPT */
+      ;(function () {
+        var themeId = localStorage.getItem("opencode-theme-id")
+        if (!themeId) return
+
+        var scheme = localStorage.getItem("opencode-color-scheme") || "system"
+        var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
+        var mode = isDark ? "dark" : "light"
+
+        document.documentElement.dataset.theme = themeId
+        document.documentElement.dataset.colorScheme = mode
+
+        if (themeId === "oc-1") return
+
+        var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
+        if (css) {
+          var style = document.createElement("style")
+          style.id = "oc-theme-preload"
+          style.textContent =
+            ":root{color-scheme:" +
+            mode +
+            ";--text-mix-blend-mode:" +
+            (isDark ? "plus-lighter" : "multiply") +
+            ";" +
+            css +
+            "}"
+          document.head.appendChild(style)
+        }
+      })()
     </script>
   </head>
   <body class="antialiased overscroll-none text-12-regular overflow-hidden">

+ 9 - 5
packages/ui/src/theme/context.tsx

@@ -126,11 +126,13 @@ function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "da
   const css = themeToCss(tokens)
 
   // Cache to localStorage for preload script
-  const cacheKey = getThemeCacheKey(themeId, mode)
-  try {
-    localStorage.setItem(cacheKey, css)
-  } catch {
-    // localStorage might be full or disabled
+  if (themeId !== "oc-1") {
+    const cacheKey = getThemeCacheKey(themeId, mode)
+    try {
+      localStorage.setItem(cacheKey, css)
+    } catch {
+      // localStorage might be full or disabled
+    }
   }
 
   // Build full CSS
@@ -159,6 +161,8 @@ function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "da
  * Cache both light and dark variants of a theme
  */
 function cacheThemeVariants(theme: DesktopTheme, themeId: string): void {
+  if (themeId === "oc-1") return
+
   for (const mode of ["light", "dark"] as const) {
     const isDark = mode === "dark"
     const variant = isDark ? theme.dark : theme.light

+ 18 - 40
packages/ui/src/theme/preload.ts

@@ -4,15 +4,13 @@
  * Generates a minimal inline script that:
  * 1. Reads theme preferences from localStorage
  * 2. Applies cached theme CSS immediately (avoiding FOUC)
- * 3. Falls back to embedded default theme CSS on first visit
+ *
+ * The default (oc-1) theme is provided by `@opencode-ai/ui/styles` via `theme.css`,
+ * so the preload script only runs when a non-default theme is selected.
  *
  * The script should be placed in the document <head> before any stylesheets.
  */
 
-import { resolveThemeVariant, themeToCss } from "./resolve"
-import type { DesktopTheme } from "./types"
-import oc1Theme from "./themes/oc-1.json"
-
 // Storage keys used by both the preload script and the ThemeProvider
 export const STORAGE_KEYS = {
   THEME_ID: "opencode-theme-id",
@@ -27,34 +25,17 @@ export function getThemeCacheKey(themeId: string, mode: "light" | "dark"): strin
   return `${STORAGE_KEYS.THEME_CSS_PREFIX}-${themeId}-${mode}`
 }
 
-/**
- * Generate the embedded default theme CSS for the preload script.
- * This is used as a fallback when no cached theme exists.
- */
-function generateEmbeddedDefaults(): { light: string; dark: string } {
-  const theme = oc1Theme as DesktopTheme
-  const lightTokens = resolveThemeVariant(theme.light, false)
-  const darkTokens = resolveThemeVariant(theme.dark, true)
-
-  return {
-    light: themeToCss(lightTokens),
-    dark: themeToCss(darkTokens),
-  }
-}
-
 /**
  * Generate the inline preload script.
  *
  * This script should be placed in the document <head> to avoid FOUC.
- * It reads theme preferences from localStorage and applies the theme CSS
- * immediately, falling back to an embedded default theme.
+ * It reads theme preferences from localStorage and applies cached theme CSS
+ * immediately.
  */
 export function generatePreloadScript(): string {
-  const defaults = generateEmbeddedDefaults()
-
   // Minified version of the preload logic
   // Variables: T=themeId, S=scheme, D=isDark, M=mode, C=css, K=cacheKey
-  return `(function(){var T=localStorage.getItem("${STORAGE_KEYS.THEME_ID}")||"oc-1";var S=localStorage.getItem("${STORAGE_KEYS.COLOR_SCHEME}")||"system";var D=S==="dark"||(S==="system"&&matchMedia("(prefers-color-scheme:dark)").matches);var M=D?"dark":"light";var K="${STORAGE_KEYS.THEME_CSS_PREFIX}-"+T+"-"+M;var C=localStorage.getItem(K);if(!C&&T==="oc-1"){C=D?${JSON.stringify(defaults.dark)}:${JSON.stringify(defaults.light)}}if(C){var s=document.createElement("style");s.id="oc-theme-preload";s.textContent=":root{color-scheme:"+M+";--text-mix-blend-mode:"+(D?"plus-lighter":"multiply")+";"+C+"}";document.head.appendChild(s)}document.documentElement.dataset.theme=T;document.documentElement.dataset.colorScheme=M})();`
+  return `(function(){var T=localStorage.getItem("${STORAGE_KEYS.THEME_ID}");if(!T)return;var S=localStorage.getItem("${STORAGE_KEYS.COLOR_SCHEME}")||"system";var D=S==="dark"||(S==="system"&&matchMedia("(prefers-color-scheme:dark)").matches);var M=D?"dark":"light";document.documentElement.dataset.theme=T;document.documentElement.dataset.colorScheme=M;if(T==="oc-1")return;var K="${STORAGE_KEYS.THEME_CSS_PREFIX}-"+T+"-"+M;var C=localStorage.getItem(K);if(C){var s=document.createElement("style");s.id="oc-theme-preload";s.textContent=":root{color-scheme:"+M+";--text-mix-blend-mode:"+(D?"plus-lighter":"multiply")+";"+C+"}";document.head.appendChild(s)}})();`
 }
 
 /**
@@ -62,15 +43,16 @@ export function generatePreloadScript(): string {
  * Useful for debugging.
  */
 export function generatePreloadScriptFormatted(): string {
-  const defaults = generateEmbeddedDefaults()
-
   return `(function() {
   var THEME_KEY = "${STORAGE_KEYS.THEME_ID}";
   var SCHEME_KEY = "${STORAGE_KEYS.COLOR_SCHEME}";
   var CSS_PREFIX = "${STORAGE_KEYS.THEME_CSS_PREFIX}";
 
-  // Read preferences from localStorage
-  var themeId = localStorage.getItem(THEME_KEY) || "oc-1";
+  // Only preload when a theme is selected
+  var themeId = localStorage.getItem(THEME_KEY);
+  if (!themeId) return;
+
+  // Read color scheme preference
   var scheme = localStorage.getItem(SCHEME_KEY) || "system";
 
   // Determine if dark mode
@@ -78,17 +60,17 @@ export function generatePreloadScriptFormatted(): string {
     (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches);
   var mode = isDark ? "dark" : "light";
 
+  // Set data attributes for CSS/JS reference
+  document.documentElement.dataset.theme = themeId;
+  document.documentElement.dataset.colorScheme = mode;
+
+  // Default theme is handled by theme.css
+  if (themeId === "oc-1") return;
+
   // Try to get cached CSS for this theme + mode
   var cacheKey = CSS_PREFIX + "-" + themeId + "-" + mode;
   var css = localStorage.getItem(cacheKey);
 
-  // Fallback to embedded default for oc-1 theme
-  if (!css && themeId === "oc-1") {
-    css = isDark
-      ? ${JSON.stringify(defaults.dark)}
-      : ${JSON.stringify(defaults.light)};
-  }
-
   // Apply CSS if we have it
   if (css) {
     var style = document.createElement("style");
@@ -100,9 +82,5 @@ export function generatePreloadScriptFormatted(): string {
     "}";
     document.head.appendChild(style);
   }
-
-  // Set data attributes for CSS/JS reference
-  document.documentElement.dataset.theme = themeId;
-  document.documentElement.dataset.colorScheme = mode;
 })();`
 }