summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-28 10:21:32 -0600
committerAdam <[email protected]>2025-12-28 10:21:32 -0600
commit040939fb720f8666eaee3d3b1866fe84e40151ca (patch)
tree744ff5e64dd848eb7f5b5a6ec8d868ff72be93fc /packages/ui/src
parentf89b83a6d797568b1240c0c141db526e170c0299 (diff)
downloadopencode-040939fb720f8666eaee3d3b1866fe84e40151ca.tar.gz
opencode-040939fb720f8666eaee3d3b1866fe84e40151ca.zip
chore: cleanup theme stuff
Diffstat (limited to 'packages/ui/src')
-rw-r--r--packages/ui/src/theme/color.ts73
-rw-r--r--packages/ui/src/theme/context.tsx102
-rw-r--r--packages/ui/src/theme/index.ts24
-rw-r--r--packages/ui/src/theme/loader.ts110
-rw-r--r--packages/ui/src/theme/resolve.ts40
-rw-r--r--packages/ui/src/theme/types.ts41
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>