From 4a9ff9412e8daedc36319bd2ee8ca62d5aa52be7 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sun, 28 Dec 2025 05:12:32 -0600 Subject: feat(desktop): themes --- packages/ui/src/theme/preload.ts | 115 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 packages/ui/src/theme/preload.ts (limited to 'packages/ui/src/theme/preload.ts') 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 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 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; +})();` +} -- cgit v1.2.3