Adam 1 месяц назад
Родитель
Сommit
040939fb72

+ 0 - 73
packages/ui/src/theme/color.ts

@@ -1,13 +1,5 @@
-/**
- * Color utilities for theme generation using OKLCH color space.
- * OKLCH provides perceptually uniform color manipulation.
- */
-
 import type { HexColor, OklchColor } from "./types"
 
-/**
- * Convert hex color to RGB values (0-1 range)
- */
 export function hexToRgb(hex: HexColor): { r: number; g: number; b: number } {
   const h = hex.replace("#", "")
   const full =
@@ -26,9 +18,6 @@ export function hexToRgb(hex: HexColor): { r: number; g: number; b: number } {
   }
 }
 
-/**
- * Convert RGB (0-1 range) to hex color
- */
 export function rgbToHex(r: number, g: number, b: number): HexColor {
   const toHex = (v: number) => {
     const clamped = Math.max(0, Math.min(1, v))
@@ -38,32 +27,21 @@ export function rgbToHex(r: number, g: number, b: number): HexColor {
   return `#${toHex(r)}${toHex(g)}${toHex(b)}`
 }
 
-/**
- * Convert linear RGB to sRGB
- */
 function linearToSrgb(c: number): number {
   if (c <= 0.0031308) return c * 12.92
   return 1.055 * Math.pow(c, 1 / 2.4) - 0.055
 }
 
-/**
- * Convert sRGB to linear RGB
- */
 function srgbToLinear(c: number): number {
   if (c <= 0.04045) return c / 12.92
   return Math.pow((c + 0.055) / 1.055, 2.4)
 }
 
-/**
- * Convert RGB to OKLCH
- */
 export function rgbToOklch(r: number, g: number, b: number): OklchColor {
-  // Convert to linear RGB
   const lr = srgbToLinear(r)
   const lg = srgbToLinear(g)
   const lb = srgbToLinear(b)
 
-  // RGB to OKLab matrix multiplication
   const l_ = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb
   const m_ = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb
   const s_ = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb
@@ -83,9 +61,6 @@ export function rgbToOklch(r: number, g: number, b: number): OklchColor {
   return { l: L, c: C, h: H }
 }
 
-/**
- * Convert OKLCH to RGB
- */
 export function oklchToRgb(oklch: OklchColor): { r: number; g: number; b: number } {
   const { l: L, c: C, h: H } = oklch
 
@@ -111,43 +86,24 @@ export function oklchToRgb(oklch: OklchColor): { r: number; g: number; b: number
   }
 }
 
-/**
- * Convert hex to OKLCH
- */
 export function hexToOklch(hex: HexColor): OklchColor {
   const { r, g, b } = hexToRgb(hex)
   return rgbToOklch(r, g, b)
 }
 
-/**
- * Convert OKLCH to hex
- */
 export function oklchToHex(oklch: OklchColor): HexColor {
   const { r, g, b } = oklchToRgb(oklch)
   return rgbToHex(r, g, b)
 }
 
-/**
- * Generate a 12-step color scale from a seed color.
- * Steps 1-4: Very light (backgrounds)
- * Steps 5-7: Mid tones (borders, subtle UI)
- * Steps 8-9: Saturated (primary buttons, text)
- * Steps 10-12: Dark (text, strong accents)
- *
- * @param seed - The seed color (typically step 9 - the main accent)
- * @param isDark - Whether generating for dark mode
- */
 export function generateScale(seed: HexColor, isDark: boolean): HexColor[] {
   const base = hexToOklch(seed)
   const scale: HexColor[] = []
 
-  // Lightness values for each step (0-1)
-  // These are tuned to match Radix-style color scales
   const lightSteps = isDark
     ? [0.15, 0.18, 0.22, 0.26, 0.32, 0.38, 0.46, 0.56, base.l, base.l - 0.05, 0.75, 0.93]
     : [0.99, 0.97, 0.94, 0.9, 0.85, 0.79, 0.72, 0.64, base.l, base.l + 0.05, 0.45, 0.25]
 
-  // Chroma multipliers - less saturation at extremes
   const chromaMultipliers = isDark
     ? [0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.85, 1, 1, 0.9, 0.6]
     : [0.1, 0.15, 0.25, 0.35, 0.45, 0.55, 0.7, 0.85, 1, 1, 0.95, 0.85]
@@ -165,18 +121,9 @@ export function generateScale(seed: HexColor, isDark: boolean): HexColor[] {
   return scale
 }
 
-/**
- * Generate a neutral gray scale from a seed color.
- * The seed color's hue is used to tint the grays slightly.
- *
- * @param seed - A neutral-ish color to derive the gray scale from
- * @param isDark - Whether generating for dark mode
- */
 export function generateNeutralScale(seed: HexColor, isDark: boolean): HexColor[] {
   const base = hexToOklch(seed)
   const scale: HexColor[] = []
-
-  // Very low chroma for neutrals - just a hint of the hue
   const neutralChroma = Math.min(base.c, 0.02)
 
   const lightSteps = isDark
@@ -196,12 +143,7 @@ export function generateNeutralScale(seed: HexColor, isDark: boolean): HexColor[
   return scale
 }
 
-/**
- * Generate alpha variants of a color scale.
- * Returns hex colors with alpha pre-multiplied against white (light) or black (dark).
- */
 export function generateAlphaScale(scale: HexColor[], isDark: boolean): HexColor[] {
-  // Alpha values for each step
   const alphas = isDark
     ? [0.02, 0.04, 0.08, 0.12, 0.16, 0.2, 0.26, 0.36, 0.44, 0.52, 0.76, 0.96]
     : [0.01, 0.03, 0.06, 0.09, 0.12, 0.15, 0.2, 0.28, 0.48, 0.56, 0.64, 0.88]
@@ -210,21 +152,15 @@ export function generateAlphaScale(scale: HexColor[], isDark: boolean): HexColor
     const { r, g, b } = hexToRgb(hex)
     const a = alphas[i]
 
-    // Pre-multiply against white (light) or black (dark)
     const bg = isDark ? 0 : 1
     const blendedR = r * a + bg * (1 - a)
     const blendedG = g * a + bg * (1 - a)
     const blendedB = b * a + bg * (1 - a)
 
-    // Return as hex with alpha encoded in the color itself
-    // For true alpha, we'd need rgba(), but this approximates it
     return rgbToHex(blendedR, blendedG, blendedB)
   })
 }
 
-/**
- * Mix two colors together
- */
 export function mixColors(color1: HexColor, color2: HexColor, amount: number): HexColor {
   const c1 = hexToOklch(color1)
   const c2 = hexToOklch(color2)
@@ -236,9 +172,6 @@ export function mixColors(color1: HexColor, color2: HexColor, amount: number): H
   })
 }
 
-/**
- * Lighten a color by a given amount (0-1)
- */
 export function lighten(color: HexColor, amount: number): HexColor {
   const oklch = hexToOklch(color)
   return oklchToHex({
@@ -247,9 +180,6 @@ export function lighten(color: HexColor, amount: number): HexColor {
   })
 }
 
-/**
- * Darken a color by a given amount (0-1)
- */
 export function darken(color: HexColor, amount: number): HexColor {
   const oklch = hexToOklch(color)
   return oklchToHex({
@@ -258,9 +188,6 @@ export function darken(color: HexColor, amount: number): HexColor {
   })
 }
 
-/**
- * Adjust the alpha/opacity of a hex color (returns rgba string)
- */
 export function withAlpha(color: HexColor, alpha: number): string {
   const { r, g, b } = hexToRgb(color)
   return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${alpha})`

+ 2 - 100
packages/ui/src/theme/context.tsx

@@ -1,12 +1,3 @@
-/**
- * Theme context for SolidJS applications.
- * Provides reactive theme management with localStorage persistence and caching.
- *
- * Works in conjunction with the preload script to provide zero-FOUC theming:
- * 1. Preload script applies cached CSS immediately from localStorage
- * 2. ThemeProvider takes over, resolves theme, and updates cache
- */
-
 import {
   createContext,
   useContext,
@@ -24,19 +15,12 @@ import { DEFAULT_THEMES } from "./default-themes"
 export type ColorScheme = "light" | "dark" | "system"
 
 interface ThemeContextValue {
-  /** Currently active theme ID */
   themeId: Accessor<string>
-  /** Current color scheme preference */
   colorScheme: Accessor<ColorScheme>
-  /** Resolved current mode (light or dark) */
   mode: Accessor<"light" | "dark">
-  /** All available themes */
   themes: Accessor<Record<string, DesktopTheme>>
-  /** Set the active theme by ID */
   setTheme: (id: string) => void
-  /** Set color scheme preference */
   setColorScheme: (scheme: ColorScheme) => void
-  /** Register a custom theme */
   registerTheme: (theme: DesktopTheme) => void
 }
 
@@ -52,59 +36,6 @@ function getThemeCacheKey(themeId: string, mode: "light" | "dark"): string {
   return `${STORAGE_KEYS.THEME_CSS_PREFIX}-${themeId}-${mode}`
 }
 
-/**
- * Static tokens that don't change between themes
- */
-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);
-  --shadow-xs-border: 0 0 0 1px var(--border-base), 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-xs-border-base: 0 0 0 1px var(--border-weak-base), 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-xs-border-select: 0 0 0 3px var(--border-weak-selected), 0 0 0 1px var(--border-selected), 0 1px 2px -1px rgba(19, 16, 16, 0.25), 0 1px 2px 0 rgba(19, 16, 16, 0.08), 0 1px 3px 0 rgba(19, 16, 16, 0.12);
-  --shadow-xs-border-focus: 0 0 0 1px var(--border-base), 0 1px 2px -1px rgba(19, 16, 16, 0.25), 0 1px 2px 0 rgba(19, 16, 16, 0.08), 0 1px 3px 0 rgba(19, 16, 16, 0.12), 0 0 0 2px var(--background-weak), 0 0 0 3px var(--border-selected);
-`
-
 const THEME_STYLE_ID = "oc-theme"
 
 function ensureThemeStyleElement(): HTMLStyleElement {
@@ -118,41 +49,29 @@ function ensureThemeStyleElement(): HTMLStyleElement {
   return element
 }
 
-/**
- * Resolve a mode from system preference
- */
 function getSystemMode(): "light" | "dark" {
   return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
 }
 
-/**
- * Apply theme CSS to the document
- */
 function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "dark"): void {
   const isDark = mode === "dark"
   const variant = isDark ? theme.dark : theme.light
   const tokens = resolveThemeVariant(variant, isDark)
   const css = themeToCss(tokens)
 
-  // Cache to localStorage for preload script
   if (themeId !== "oc-1") {
     const cacheKey = getThemeCacheKey(themeId, mode)
     try {
       localStorage.setItem(cacheKey, css)
-    } catch {
-      // localStorage might be full or disabled
-    }
+    } catch {}
   }
 
-  // Build full CSS
   const fullCss = `:root {
-  ${STATIC_TOKENS}
   color-scheme: ${mode};
   --text-mix-blend-mode: ${isDark ? "plus-lighter" : "multiply"};
   ${css}
 }`
 
-  // Remove preload style if it exists
   const preloadStyle = document.getElementById("oc-theme-preload")
   if (preloadStyle) {
     preloadStyle.remove()
@@ -161,14 +80,10 @@ function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "da
   const themeStyleElement = ensureThemeStyleElement()
   themeStyleElement.textContent = fullCss
 
-  // Update data attributes
   document.documentElement.dataset.theme = themeId
   document.documentElement.dataset.colorScheme = mode
 }
 
-/**
- * Cache both light and dark variants of a theme
- */
 function cacheThemeVariants(theme: DesktopTheme, themeId: string): void {
   if (themeId === "oc-1") return
 
@@ -180,9 +95,7 @@ function cacheThemeVariants(theme: DesktopTheme, themeId: string): void {
     const cacheKey = getThemeCacheKey(themeId, mode)
     try {
       localStorage.setItem(cacheKey, css)
-    } catch {
-      // localStorage might be full or disabled
-    }
+    } catch {}
   }
 }
 
@@ -192,7 +105,6 @@ export function ThemeProvider(props: { children: JSX.Element; defaultTheme?: str
   const [colorScheme, setColorSchemeSignal] = createSignal<ColorScheme>("system")
   const [mode, setMode] = createSignal<"light" | "dark">(getSystemMode())
 
-  // Listen for system color scheme changes
   onMount(() => {
     const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
     const handler = () => {
@@ -203,29 +115,23 @@ export function ThemeProvider(props: { children: JSX.Element; defaultTheme?: str
     mediaQuery.addEventListener("change", handler)
     onCleanup(() => mediaQuery.removeEventListener("change", handler))
 
-    // Load saved preferences
     const savedTheme = localStorage.getItem(STORAGE_KEYS.THEME_ID)
     const savedScheme = localStorage.getItem(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null
-
     if (savedTheme && themes()[savedTheme]) {
       setThemeIdSignal(savedTheme)
     }
-
     if (savedScheme) {
       setColorSchemeSignal(savedScheme)
       if (savedScheme !== "system") {
         setMode(savedScheme)
       }
     }
-
-    // Cache current theme variants for future preloads
     const currentTheme = themes()[themeId()]
     if (currentTheme) {
       cacheThemeVariants(currentTheme, themeId())
     }
   })
 
-  // Apply theme when themeId or mode changes
   createEffect(() => {
     const id = themeId()
     const m = mode()
@@ -241,18 +147,14 @@ export function ThemeProvider(props: { children: JSX.Element; defaultTheme?: str
       console.warn(`Theme "${id}" not found`)
       return
     }
-
     setThemeIdSignal(id)
     localStorage.setItem(STORAGE_KEYS.THEME_ID, id)
-
-    // Cache both variants for future preloads
     cacheThemeVariants(theme, id)
   }
 
   const setColorSchemePref = (scheme: ColorScheme) => {
     setColorSchemeSignal(scheme)
     localStorage.setItem(STORAGE_KEYS.COLOR_SCHEME, scheme)
-
     if (scheme === "system") {
       setMode(getSystemMode())
     } else {

+ 0 - 24
packages/ui/src/theme/index.ts

@@ -1,20 +1,3 @@
-/**
- * Desktop Theme System
- *
- * Provides JSON-based theming for the desktop app. Unlike TUI themes,
- * desktop themes use more design tokens and generate full color scales
- * from seed colors.
- *
- * Usage:
- * ```ts
- * import { applyTheme } from "@opencode/ui/theme"
- * import myTheme from "./themes/my-theme.json"
- *
- * applyTheme(myTheme)
- * ```
- */
-
-// Types
 export type {
   DesktopTheme,
   ThemeSeedColors,
@@ -26,7 +9,6 @@ export type {
   CssVarRef,
 } from "./types"
 
-// Color utilities
 export {
   hexToRgb,
   rgbToHex,
@@ -43,16 +25,10 @@ export {
   withAlpha,
 } from "./color"
 
-// Theme resolution
 export { resolveThemeVariant, resolveTheme, themeToCss } from "./resolve"
-
-// Theme loader
 export { applyTheme, loadThemeFromUrl, getActiveTheme, removeTheme, setColorScheme } from "./loader"
-
-// Theme context (SolidJS)
 export { ThemeProvider, useTheme, type ColorScheme } from "./context"
 
-// Default themes
 export {
   DEFAULT_THEMES,
   oc1Theme,

+ 0 - 110
packages/ui/src/theme/loader.ts

@@ -1,13 +1,7 @@
-/**
- * Theme loader - loads theme JSON files and applies them to the DOM.
- */
-
 import type { DesktopTheme, ResolvedTheme } from "./types"
 import { resolveThemeVariant, themeToCss } from "./resolve"
 
-/** Currently active theme */
 let activeTheme: DesktopTheme | null = null
-
 const THEME_STYLE_ID = "opencode-theme"
 
 function ensureLoaderStyleElement(): HTMLStyleElement {
@@ -21,115 +15,25 @@ function ensureLoaderStyleElement(): HTMLStyleElement {
   return element
 }
 
-/**
- * Load and apply a theme to the document.
- * Creates or updates a <style> element with the theme's CSS custom properties.
- *
- * @param theme - The desktop theme to apply
- * @param themeId - Optional theme ID for the data-theme attribute
- */
 export function applyTheme(theme: DesktopTheme, themeId?: string): void {
   activeTheme = theme
-
-  // Resolve both variants
   const lightTokens = resolveThemeVariant(theme.light, false)
   const darkTokens = resolveThemeVariant(theme.dark, true)
-
   const targetThemeId = themeId ?? theme.id
-
-  // Build the CSS
   const css = buildThemeCss(lightTokens, darkTokens, targetThemeId)
-
   const themeStyleElement = ensureLoaderStyleElement()
   themeStyleElement.textContent = css
-
   document.documentElement.setAttribute("data-theme", targetThemeId)
 }
 
-/**
- * Build CSS string from resolved theme tokens
- */
 function buildThemeCss(light: ResolvedTheme, dark: ResolvedTheme, themeId: string): string {
   const isDefaultTheme = themeId === "oc-1"
-
-  // Static tokens that don't change between themes
-  const staticTokens = `
-  --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.1599999964237213;
-  --letter-spacing-tightest: -0.3199999928474426;
-  --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);
-  --shadow-xs-border:
-    0 0 0 1px var(--border-base, rgba(11, 6, 0, 0.2)), 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-xs-border-base:
-    0 0 0 1px var(--border-weak-base, rgba(17, 0, 0, 0.12)), 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-xs-border-select:
-    0 0 0 3px var(--border-weak-selected, rgba(1, 103, 255, 0.29)),
-    0 0 0 1px var(--border-selected, rgba(0, 74, 255, 0.99)), 0 1px 2px -1px rgba(19, 16, 16, 0.25),
-    0 1px 2px 0 rgba(19, 16, 16, 0.08), 0 1px 3px 0 rgba(19, 16, 16, 0.12);
-  --shadow-xs-border-focus:
-    0 0 0 1px var(--border-base, rgba(11, 6, 0, 0.2)), 0 1px 2px -1px rgba(19, 16, 16, 0.25),
-    0 1px 2px 0 rgba(19, 16, 16, 0.08), 0 1px 3px 0 rgba(19, 16, 16, 0.12), 0 0 0 2px var(--background-weak, #f1f0f0),
-    0 0 0 3px var(--border-selected, rgba(0, 74, 255, 0.99));`
-
   const lightCss = themeToCss(light)
   const darkCss = themeToCss(dark)
 
-  // For the default theme, we use :root directly
-  // For named themes, we use the [data-theme] selector
   if (isDefaultTheme) {
     return `
 :root {
-  ${staticTokens}
-
   color-scheme: light;
   --text-mix-blend-mode: multiply;
 
@@ -147,8 +51,6 @@ function buildThemeCss(light: ResolvedTheme, dark: ResolvedTheme, themeId: strin
 
   return `
 html[data-theme="${themeId}"] {
-  ${staticTokens}
-
   color-scheme: light;
   --text-mix-blend-mode: multiply;
 
@@ -164,9 +66,6 @@ html[data-theme="${themeId}"] {
 `
 }
 
-/**
- * Load a theme from a JSON file URL
- */
 export async function loadThemeFromUrl(url: string): Promise<DesktopTheme> {
   const response = await fetch(url)
   if (!response.ok) {
@@ -175,9 +74,6 @@ export async function loadThemeFromUrl(url: string): Promise<DesktopTheme> {
   return response.json()
 }
 
-/**
- * Get the currently active theme
- */
 export function getActiveTheme(): DesktopTheme | null {
   const activeId = document.documentElement.getAttribute("data-theme")
   if (!activeId) {
@@ -189,9 +85,6 @@ export function getActiveTheme(): DesktopTheme | null {
   return null
 }
 
-/**
- * Remove the current theme and clean up
- */
 export function removeTheme(): void {
   activeTheme = null
   const existingElement = document.getElementById(THEME_STYLE_ID)
@@ -201,9 +94,6 @@ export function removeTheme(): void {
   document.documentElement.removeAttribute("data-theme")
 }
 
-/**
- * Force a specific color scheme (light/dark) regardless of system preference
- */
 export function setColorScheme(scheme: "light" | "dark" | "auto"): void {
   if (scheme === "auto") {
     document.documentElement.style.removeProperty("color-scheme")

+ 0 - 40
packages/ui/src/theme/resolve.ts

@@ -1,17 +1,9 @@
-/**
- * Theme resolver - generates all CSS custom properties from theme seed colors.
- */
-
 import type { ColorValue, DesktopTheme, HexColor, ResolvedTheme, ThemeVariant } from "./types"
 import { generateNeutralScale, generateScale, hexToOklch, oklchToHex, withAlpha } from "./color"
 
-/**
- * Resolve a theme variant to all CSS custom properties
- */
 export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): ResolvedTheme {
   const { seeds, overrides = {} } = variant
 
-  // Generate color scales
   const neutral = generateNeutralScale(seeds.neutral, isDark)
   const primary = generateScale(seeds.primary, isDark)
   const success = generateScale(seeds.success, isDark)
@@ -22,19 +14,15 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   const diffAdd = generateScale(seeds.diffAdd, isDark)
   const diffDelete = generateScale(seeds.diffDelete, isDark)
 
-  // Generate alpha variants for neutral
   const neutralAlpha = generateNeutralAlphaScale(neutral, isDark)
 
-  // Build the full token map
   const tokens: ResolvedTheme = {}
 
-  // === Background tokens ===
   tokens["background-base"] = neutral[0]
   tokens["background-weak"] = neutral[2]
   tokens["background-strong"] = neutral[0]
   tokens["background-stronger"] = isDark ? neutral[1] : "#fcfcfc"
 
-  // === Surface tokens ===
   tokens["surface-base"] = neutralAlpha[1]
   tokens["base"] = neutralAlpha[1]
   tokens["surface-base-hover"] = neutralAlpha[2]
@@ -62,17 +50,14 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["surface-strong"] = isDark ? neutralAlpha[6] : "#ffffff"
   tokens["surface-raised-stronger-non-alpha"] = isDark ? neutral[2] : "#ffffff"
 
-  // Brand surface
   tokens["surface-brand-base"] = primary[8]
   tokens["surface-brand-hover"] = primary[9]
 
-  // Interactive surfaces
   tokens["surface-interactive-base"] = interactive[2]
   tokens["surface-interactive-hover"] = interactive[3]
   tokens["surface-interactive-weak"] = interactive[1]
   tokens["surface-interactive-weak-hover"] = interactive[2]
 
-  // Status surfaces
   tokens["surface-success-base"] = success[2]
   tokens["surface-success-weak"] = success[1]
   tokens["surface-success-strong"] = success[8]
@@ -86,7 +71,6 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["surface-info-weak"] = info[1]
   tokens["surface-info-strong"] = info[8]
 
-  // Diff surfaces
   tokens["surface-diff-unchanged-base"] = isDark ? neutral[0] : "#ffffff00"
   tokens["surface-diff-skip-base"] = isDark ? neutralAlpha[0] : neutral[1]
   tokens["surface-diff-hidden-base"] = interactive[isDark ? 1 : 2]
@@ -105,7 +89,6 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["surface-diff-delete-strong"] = diffDelete[isDark ? 4 : 5]
   tokens["surface-diff-delete-stronger"] = diffDelete[isDark ? 10 : 8]
 
-  // === Input tokens ===
   tokens["input-base"] = isDark ? neutral[1] : neutral[0]
   tokens["input-hover"] = neutral[1]
   tokens["input-active"] = interactive[0]
@@ -113,7 +96,6 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["input-focus"] = interactive[0]
   tokens["input-disabled"] = neutral[3]
 
-  // === Text tokens ===
   tokens["text-base"] = neutral[10]
   tokens["text-weak"] = neutral[8]
   tokens["text-weaker"] = neutral[7]
@@ -146,13 +128,11 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["text-on-brand-weaker"] = neutralAlpha[7]
   tokens["text-on-brand-strong"] = neutralAlpha[11]
 
-  // === Button tokens ===
   tokens["button-secondary-base"] = isDark ? neutral[2] : neutral[0]
   tokens["button-secondary-hover"] = isDark ? neutral[3] : neutral[1]
   tokens["button-ghost-hover"] = neutralAlpha[1]
   tokens["button-ghost-hover2"] = neutralAlpha[2]
 
-  // === Border tokens ===
   tokens["border-base"] = neutralAlpha[6]
   tokens["border-hover"] = neutralAlpha[7]
   tokens["border-active"] = neutralAlpha[8]
@@ -178,7 +158,6 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["border-weaker-disabled"] = neutralAlpha[1]
   tokens["border-weaker-focus"] = neutralAlpha[5]
 
-  // Interactive borders
   tokens["border-interactive-base"] = interactive[6]
   tokens["border-interactive-hover"] = interactive[7]
   tokens["border-interactive-active"] = interactive[8]
@@ -186,7 +165,6 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["border-interactive-disabled"] = neutral[7]
   tokens["border-interactive-focus"] = interactive[8]
 
-  // Status borders
   tokens["border-success-base"] = success[5]
   tokens["border-success-hover"] = success[6]
   tokens["border-success-selected"] = success[8]
@@ -201,7 +179,6 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["border-info-selected"] = info[8]
   tokens["border-color"] = "#ffffff"
 
-  // === Icon tokens ===
   tokens["icon-base"] = neutral[8]
   tokens["icon-hover"] = neutral[isDark ? 9 : 10]
   tokens["icon-active"] = neutral[isDark ? 10 : 11]
@@ -240,13 +217,11 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["icon-on-brand-selected"] = neutralAlpha[11]
   tokens["icon-on-interactive-base"] = isDark ? neutral[11] : neutral[0]
 
-  // Agent icons (using semantic colors)
   tokens["icon-agent-plan-base"] = info[8]
   tokens["icon-agent-docs-base"] = warning[8]
   tokens["icon-agent-ask-base"] = interactive[8]
   tokens["icon-agent-build-base"] = interactive[isDark ? 10 : 8]
 
-  // Status icons
   tokens["icon-on-success-base"] = withAlpha(success[8], 0.9) as ColorValue
   tokens["icon-on-success-hover"] = withAlpha(success[9], 0.9) as ColorValue
   tokens["icon-on-success-selected"] = withAlpha(success[10], 0.9) as ColorValue
@@ -260,14 +235,12 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["icon-on-info-hover"] = withAlpha(info[9], 0.9) as ColorValue
   tokens["icon-on-info-selected"] = withAlpha(info[10], 0.9) as ColorValue
 
-  // Diff icons
   tokens["icon-diff-add-base"] = diffAdd[10]
   tokens["icon-diff-add-hover"] = diffAdd[isDark ? 9 : 11]
   tokens["icon-diff-add-active"] = diffAdd[isDark ? 10 : 11]
   tokens["icon-diff-delete-base"] = diffDelete[isDark ? 8 : 9]
   tokens["icon-diff-delete-hover"] = diffDelete[isDark ? 9 : 10]
 
-  // === Syntax tokens ===
   tokens["syntax-comment"] = "var(--text-weak)"
   tokens["syntax-regexp"] = "var(--text-base)"
   tokens["syntax-string"] = isDark ? "#00ceb9" : "#006656"
@@ -288,7 +261,6 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["syntax-diff-delete"] = diffDelete[10]
   tokens["syntax-diff-unknown"] = "#ff0000"
 
-  // === Markdown tokens ===
   tokens["markdown-heading"] = isDark ? "#9d7cd8" : "#d68c27"
   tokens["markdown-text"] = isDark ? "#eeeeee" : "#1a1a1a"
   tokens["markdown-link"] = isDark ? "#fab283" : "#3b7dd8"
@@ -304,7 +276,6 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["markdown-image-text"] = isDark ? "#56b6c2" : "#318795"
   tokens["markdown-code-block"] = isDark ? "#eeeeee" : "#1a1a1a"
 
-  // === Avatar tokens ===
   tokens["avatar-background-pink"] = isDark ? "#501b3f" : "#feeef8"
   tokens["avatar-background-mint"] = isDark ? "#033a34" : "#e1fbf4"
   tokens["avatar-background-orange"] = isDark ? "#5f2a06" : "#fff1e7"
@@ -318,7 +289,6 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   tokens["avatar-text-cyan"] = isDark ? "#369eff" : "#0894b3"
   tokens["avatar-text-lime"] = isDark ? "#c4f042" : "#5d770d"
 
-  // Apply any overrides
   for (const [key, value] of Object.entries(overrides)) {
     tokens[key] = value
   }
@@ -326,9 +296,6 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
   return tokens
 }
 
-/**
- * Generate neutral alpha scale (approximated as solid colors)
- */
 function generateNeutralAlphaScale(neutralScale: HexColor[], isDark: boolean): HexColor[] {
   const alphas = isDark
     ? [0.02, 0.04, 0.08, 0.12, 0.16, 0.2, 0.26, 0.36, 0.44, 0.52, 0.72, 0.94]
@@ -336,7 +303,6 @@ function generateNeutralAlphaScale(neutralScale: HexColor[], isDark: boolean): H
 
   return neutralScale.map((hex, i) => {
     const baseOklch = hexToOklch(hex)
-    // Adjust lightness based on alpha - closer to background color
     const targetL = isDark ? 0.1 + alphas[i] * 0.8 : 1 - alphas[i] * 0.8
     return oklchToHex({
       ...baseOklch,
@@ -345,9 +311,6 @@ function generateNeutralAlphaScale(neutralScale: HexColor[], isDark: boolean): H
   })
 }
 
-/**
- * Resolve a complete theme to CSS for both light and dark modes
- */
 export function resolveTheme(theme: DesktopTheme): { light: ResolvedTheme; dark: ResolvedTheme } {
   return {
     light: resolveThemeVariant(theme.light, false),
@@ -355,9 +318,6 @@ export function resolveTheme(theme: DesktopTheme): { light: ResolvedTheme; dark:
   }
 }
 
-/**
- * Convert resolved theme tokens to CSS custom properties string
- */
 export function themeToCss(tokens: ResolvedTheme): string {
   return Object.entries(tokens)
     .map(([key, value]) => `--${key}: ${value};`)

+ 0 - 41
packages/ui/src/theme/types.ts

@@ -1,68 +1,36 @@
-/**
- * Desktop Theme System
- *
- * Unlike the TUI themes, desktop themes require more design tokens and use
- * OKLCH color space for generating color scales from seed colors.
- */
-
-/** A hex color string like "#ffffff" or "#fff" */
 export type HexColor = `#${string}`
 
-/** OKLCH color representation for calculations */
 export interface OklchColor {
   l: number // Lightness 0-1
   c: number // Chroma 0-0.4+
   h: number // Hue 0-360
 }
 
-/** The minimum colors needed to define a theme variant */
 export interface ThemeSeedColors {
-  /** Base neutral color - used to generate gray scale (smoke/ink) */
   neutral: HexColor
-  /** Primary brand/accent color */
   primary: HexColor
-  /** Success color (green) */
   success: HexColor
-  /** Warning color (yellow/orange) */
   warning: HexColor
-  /** Error/critical color (red) */
   error: HexColor
-  /** Info color (purple/blue) */
   info: HexColor
-  /** Interactive/link color (blue) */
   interactive: HexColor
-  /** Diff add color */
   diffAdd: HexColor
-  /** Diff delete color */
   diffDelete: HexColor
 }
 
-/** A theme variant (light or dark) with seed colors and optional overrides */
 export interface ThemeVariant {
-  /** Seed colors used to generate the full palette */
   seeds: ThemeSeedColors
-  /** Optional direct overrides for any CSS variable (without -- prefix) */
   overrides?: Record<string, ColorValue>
 }
 
-/** A complete desktop theme definition */
 export interface DesktopTheme {
-  /** Schema version for future compatibility */
   $schema?: string
-  /** Theme display name */
   name: string
-  /** Theme identifier (slug) */
   id: string
-  /** Light mode variant */
   light: ThemeVariant
-  /** Dark mode variant */
   dark: ThemeVariant
 }
 
-/**
- * Categories of CSS variables that get generated from seed colors.
- * Each category maps to specific CSS custom properties.
- */
 export type TokenCategory =
   | "background"
   | "surface"
@@ -76,19 +44,10 @@ export type TokenCategory =
   | "diff"
   | "avatar"
 
-/**
- * All CSS variable names (without -- prefix) that the theme system generates.
- * These match the variables defined in theme.css
- */
 export type ThemeToken = string
 
-/** A CSS variable reference like "var(--text-weak)" */
 export type CssVarRef = `var(--${string})`
 
-/** A color value - either a hex color or a CSS variable reference */
 export type ColorValue = HexColor | CssVarRef
 
-/**
- * Resolved theme - all tokens mapped to their final colors
- */
 export type ResolvedTheme = Record<ThemeToken, ColorValue>