summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src/theme/preload.ts
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-28 05:12:32 -0600
committerAdam <[email protected]>2025-12-28 05:12:36 -0600
commit4a9ff9412e8daedc36319bd2ee8ca62d5aa52be7 (patch)
treead035366cdebc87d4eea75475201af3d11bd64ed /packages/ui/src/theme/preload.ts
parentd6db6ff198c4513ca5511ae49b03d4277bc21718 (diff)
downloadopencode-4a9ff9412e8daedc36319bd2ee8ca62d5aa52be7.tar.gz
opencode-4a9ff9412e8daedc36319bd2ee8ca62d5aa52be7.zip
feat(desktop): themes
Diffstat (limited to 'packages/ui/src/theme/preload.ts')
-rw-r--r--packages/ui/src/theme/preload.ts115
1 files changed, 115 insertions, 0 deletions
diff --git a/packages/ui/src/theme/preload.ts b/packages/ui/src/theme/preload.ts
new file mode 100644
index 000000000..a5eadb323
--- /dev/null
+++ b/packages/ui/src/theme/preload.ts
@@ -0,0 +1,115 @@
+/**
+ * Theme preload script generator.
+ *
+ * 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 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",
+ COLOR_SCHEME: "opencode-color-scheme",
+ THEME_CSS_PREFIX: "opencode-theme-css",
+} as const
+
+/**
+ * Get the localStorage key for cached theme CSS
+ */
+export function getThemeCacheKey(themeId: string, mode: "light" | "dark"): string {
+ 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),
+ }
+}
+
+/**
+ * Static tokens that don't change between themes.
+ * These are embedded in the preload CSS.
+ */
+const STATIC_TOKENS = `--font-family-sans:"Inter","Inter Fallback";--font-family-sans--font-feature-settings:"ss03" 1;--font-family-mono:"IBM Plex Mono","IBM Plex Mono Fallback";--font-family-mono--font-feature-settings:"ss01" 1;--font-size-small:13px;--font-size-base:14px;--font-size-large:16px;--font-size-x-large:20px;--font-weight-regular:400;--font-weight-medium:500;--line-height-large:150%;--line-height-x-large:180%;--line-height-2x-large:200%;--letter-spacing-normal:0;--letter-spacing-tight:-0.16;--letter-spacing-tightest:-0.32;--paragraph-spacing-base:0;--spacing:0.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--breakpoint-2xl:96rem;--container-3xs:16rem;--container-2xs:18rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--radius-xs:0.125rem;--radius-sm:0.25rem;--radius-md:0.375rem;--radius-lg:0.5rem;--radius-xl:0.625rem;--shadow-xs:0 1px 2px -1px rgba(19,16,16,0.04),0 1px 2px 0 rgba(19,16,16,0.06),0 1px 3px 0 rgba(19,16,16,0.08);--shadow-md:0 6px 8px -4px rgba(19,16,16,0.12),0 4px 3px -2px rgba(19,16,16,0.12),0 1px 2px -1px rgba(19,16,16,0.12)`
+
+/**
+ * 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.
+ */
+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{${STATIC_TOKENS};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})();`
+}
+
+/**
+ * Generate a formatted (readable) version of the preload script.
+ * 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";
+ var scheme = localStorage.getItem(SCHEME_KEY) || "system";
+
+ // Determine if dark mode
+ var isDark = scheme === "dark" ||
+ (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches);
+ var mode = isDark ? "dark" : "light";
+
+ // 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");
+ style.id = "oc-theme-preload";
+ style.textContent = ":root{" +
+ "${STATIC_TOKENS};" +
+ "color-scheme:" + mode + ";" +
+ "--text-mix-blend-mode:" + (isDark ? "plus-lighter" : "multiply") + ";" +
+ css +
+ "}";
+ document.head.appendChild(style);
+ }
+
+ // Set data attributes for CSS/JS reference
+ document.documentElement.dataset.theme = themeId;
+ document.documentElement.dataset.colorScheme = mode;
+})();`
+}