diff options
| author | Adam <[email protected]> | 2025-12-28 10:21:32 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-28 10:21:32 -0600 |
| commit | 040939fb720f8666eaee3d3b1866fe84e40151ca (patch) | |
| tree | 744ff5e64dd848eb7f5b5a6ec8d868ff72be93fc /packages/ui/src | |
| parent | f89b83a6d797568b1240c0c141db526e170c0299 (diff) | |
| download | opencode-040939fb720f8666eaee3d3b1866fe84e40151ca.tar.gz opencode-040939fb720f8666eaee3d3b1866fe84e40151ca.zip | |
chore: cleanup theme stuff
Diffstat (limited to 'packages/ui/src')
| -rw-r--r-- | packages/ui/src/theme/color.ts | 73 | ||||
| -rw-r--r-- | packages/ui/src/theme/context.tsx | 102 | ||||
| -rw-r--r-- | packages/ui/src/theme/index.ts | 24 | ||||
| -rw-r--r-- | packages/ui/src/theme/loader.ts | 110 | ||||
| -rw-r--r-- | packages/ui/src/theme/resolve.ts | 40 | ||||
| -rw-r--r-- | packages/ui/src/theme/types.ts | 41 |
6 files changed, 2 insertions, 388 deletions
diff --git a/packages/ui/src/theme/color.ts b/packages/ui/src/theme/color.ts index 3a0526ca6..f0e15211e 100644 --- a/packages/ui/src/theme/color.ts +++ b/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})` diff --git a/packages/ui/src/theme/context.tsx b/packages/ui/src/theme/context.tsx index e2c31477c..d2fbed3a1 100644 --- a/packages/ui/src/theme/context.tsx +++ b/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 { diff --git a/packages/ui/src/theme/index.ts b/packages/ui/src/theme/index.ts index 49ab76b57..d8e0461be 100644 --- a/packages/ui/src/theme/index.ts +++ b/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, diff --git a/packages/ui/src/theme/loader.ts b/packages/ui/src/theme/loader.ts index b25c833dd..0f61076a0 100644 --- a/packages/ui/src/theme/loader.ts +++ b/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") diff --git a/packages/ui/src/theme/resolve.ts b/packages/ui/src/theme/resolve.ts index e8b6d9c3e..7339088c4 100644 --- a/packages/ui/src/theme/resolve.ts +++ b/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};`) diff --git a/packages/ui/src/theme/types.ts b/packages/ui/src/theme/types.ts index 7a584ef0e..73bd372b4 100644 --- a/packages/ui/src/theme/types.ts +++ b/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> |
