import type { SurfaceState } from "./layoutTypes"; /** Design tokens — mirror of DOCS/space-sh.pen variables. Single source for the UI. */ export const COLORS = { accent: "var(--c-accent)", bgApp: "var(--c-bg-app)", bgElevated: "var(--c-bg-elevated)", bgHover: "var(--c-bg-hover)", bgPanel: "var(--c-bg-panel)", panelGlass: "var(--c-panel-glass, var(--c-bg-panel))", panelBlur: "var(--c-panel-blur, none)", appBg: "var(--app-bg, var(--c-bg-app))", elevatedGlass: "var(--c-elevated-glass, var(--c-bg-elevated))", sidebarGlass: "var(--c-sidebar-glass, var(--c-bg-sidebar))", bgSidebar: "var(--c-bg-sidebar)", borderStrong: "var(--c-border-strong)", borderSubtle: "var(--c-border-subtle)", textPrimary: "var(--c-text-primary)", textSecondary: "var(--c-text-secondary)", textMuted: "var(--c-text-muted)", stWork: "var(--c-st-work)", stWait: "var(--c-st-wait)", stDone: "var(--c-st-done)", stError: "var(--c-st-error)", stIdle: "var(--c-st-idle)", searchMatch: "var(--c-search-match)", } as const; export const FONT = { ui: "Inter, system-ui, sans-serif", mono: "'JetBrains Mono Variable', 'JetBrains Mono', monospace", } as const; /** Status color by surface state, plus the stopped pseudo-state. */ export const STATE_COLOR: Record = { work: COLORS.stWork, wait: COLORS.stWait, done: COLORS.stDone, error: COLORS.stError, idle: COLORS.stIdle, stopped: COLORS.stIdle, }; // --------------------------------------------------------------------------- // Palettes // --------------------------------------------------------------------------- type Palette = Record; /** Dark palette — hex values identical to what COLORS contained before. */ const DARK: Palette = { "bg-app": "#0E1116", "bg-elevated": "#1A2029", "bg-hover": "#222A35", "bg-panel": "#0A0D12", "bg-sidebar": "#13171F", "border-strong": "#323C49", "border-subtle": "#232A33", "text-primary": "#E6EDF3", "text-secondary": "#8B97A6", "text-muted": "#5A6573", "st-work": "#4C8DFF", "st-wait": "#F2B84B", "st-done": "#3FB950", "st-error": "#F4544E", "st-idle": "#5A6573", "search-match": "#5A4A1F", }; /** Light palette — a readable counterpart for every token. */ const LIGHT: Palette = { "bg-app": "#F5F7FA", "bg-elevated": "#FFFFFF", "bg-hover": "#E8EDF3", "bg-panel": "#EBEEF3", "bg-sidebar": "#DDE3EC", "border-strong": "#B0BAC7", "border-subtle": "#CDD4DE", "text-primary": "#0D1117", "text-secondary": "#4A5568", "text-muted": "#8898AA", "st-work": "#2266CC", "st-wait": "#C07800", "st-done": "#1A7A30", "st-error": "#C4231F", "st-idle": "#8898AA", "search-match": "#FDE68A", }; export const ACCENTS: Record = { blue: "#4C8DFF", teal: "#34D3C2", purple: "#9B7BFF", green: "#3FB950", orange: "#F2934B", }; export type ThemeName = "dark" | "light"; // --------------------------------------------------------------------------- // Background themes (Warp-style full-window fills) // --------------------------------------------------------------------------- /** * A background theme paints the WHOLE window behind every panel, instead of each * terminal owning an opaque tile. Panels become semi-transparent glass (rgba over * a backdrop blur) so the shared fill shows through uniformly across the grid. * * `css` is the CSS `background` value for the app root: a gradient, or `""` for * the classic solid look ("none"), or the sentinel "custom" handled at apply time * via a user-supplied image. `panelAlpha`/`blur` tune the glass over the fill. */ export interface BackgroundTheme { label: string; css: string; // app-root `background` value ("" = solid bg-app) swatch: string; // gallery preview (gradient/color) panelAlpha: number; // 0..1 — panel glass opacity over the fill blur: number; // px — panel backdrop blur } export const CUSTOM_BACKGROUND = "custom"; export const BACKGROUNDS: Record = { none: { label: "None", css: "", swatch: DARK["bg-panel"], panelAlpha: 1, blur: 0 }, cyberwave: { label: "Cyber Wave", css: "linear-gradient(135deg,#06121f 0%,#0a2a3f 45%,#10183a 100%)", swatch: "linear-gradient(135deg,#06121f,#0a2a3f,#10183a)", panelAlpha: 0.46, blur: 9 }, phenomenon: { label: "Phenomenon", css: "radial-gradient(120% 120% at 80% 0%,#241a2e 0%,#15121d 45%,#0a0910 100%)", swatch: "radial-gradient(120% 120% at 80% 0%,#241a2e,#15121d,#0a0910)", panelAlpha: 0.5, blur: 8 }, dracula: { label: "Dracula", css: "linear-gradient(160deg,#282a36 0%,#21222c 60%,#1a1b23 100%)", swatch: "linear-gradient(160deg,#282a36,#21222c,#1a1b23)", panelAlpha: 0.58, blur: 6 }, aurora: { label: "Aurora", css: "linear-gradient(135deg,#0b3d2e 0%,#0a2c3a 40%,#241147 100%)", swatch: "linear-gradient(135deg,#0b3d2e,#0a2c3a,#241147)", panelAlpha: 0.44, blur: 10 }, ember: { label: "Ember", css: "linear-gradient(135deg,#2a0f12 0%,#3a1410 45%,#160a14 100%)", swatch: "linear-gradient(135deg,#2a0f12,#3a1410,#160a14)", panelAlpha: 0.5, blur: 8 }, referred: { label: "Referred", css: "linear-gradient(120deg,#b9c6ff 0%,#cdb6ff 40%,#ffd6e7 100%)", swatch: "linear-gradient(120deg,#b9c6ff,#cdb6ff,#ffd6e7)", panelAlpha: 0.34, blur: 12 }, }; /** Resolve a background name to its theme, falling back to "none". */ export function backgroundFor(name: string): BackgroundTheme { return BACKGROUNDS[name] ?? BACKGROUNDS.none; } /** Hex (#rrggbb) → rgba() string at the given alpha. */ function hexToRgba(hex: string, alpha: number): string { const h = hex.replace("#", ""); const r = parseInt(h.slice(0, 2), 16); const g = parseInt(h.slice(2, 4), 16); const b = parseInt(h.slice(4, 6), 16); return `rgba(${r},${g},${b},${alpha})`; } /** * Real color values for consumers that can't use var() (xterm). Keys are the * kebab tokens plus "accent" and "term-bg". When a background theme is active the * terminal renders on transparent glass, so "term-bg" is fully transparent and * the panel container supplies the visible tint. */ export function resolvePalette(theme: ThemeName, accent: string, background: string = "none"): Record { const base = theme === "light" ? LIGHT : DARK; const active = background !== "none"; return { ...base, accent: ACCENTS[accent] ?? ACCENTS.blue, "term-bg": active ? "rgba(0,0,0,0)" : base["bg-panel"], }; } /** * Write the active palette + background fill to :root as --c-* custom properties. * `imageDataUrl` is only consulted when `background === "custom"`. */ export function applyTheme(theme: ThemeName, accent: string, background: string = "none", imageDataUrl: string | null = null): void { const p = resolvePalette(theme, accent, background); const root = document.documentElement.style; for (const [k, v] of Object.entries(p)) root.setProperty(`--c-${k}`, v); const bg = backgroundFor(background); const base = theme === "light" ? LIGHT : DARK; const active = background !== "none"; // App-root fill: custom image (cover) > gradient > solid bg-app. const appBg = background === CUSTOM_BACKGROUND && imageDataUrl ? `center / cover no-repeat url("${imageDataUrl}")` : bg.css || base["bg-app"]; root.setProperty("--app-bg", appBg); // Panel glass: rgba(bg-panel, alpha) over the fill, plus optional backdrop blur. // When inactive this is the solid bg-panel so the classic look is byte-identical. const alpha = active ? (background === CUSTOM_BACKGROUND ? 0.5 : bg.panelAlpha) : 1; const blur = active ? (background === CUSTOM_BACKGROUND ? 8 : bg.blur) : 0; root.setProperty("--c-panel-glass", alpha < 1 ? hexToRgba(base["bg-panel"], alpha) : base["bg-panel"]); root.setProperty("--c-panel-blur", blur > 0 ? `blur(${blur}px)` : "none"); // Chrome glass (TopBar / toolbar / sidebar / panel headers) — a touch more // opaque than the panels so labels and controls stay legible over the fill. const chromeAlpha = active ? Math.min(alpha + 0.22, 0.92) : 1; root.setProperty("--c-elevated-glass", chromeAlpha < 1 ? hexToRgba(base["bg-elevated"], chromeAlpha) : base["bg-elevated"]); root.setProperty("--c-sidebar-glass", chromeAlpha < 1 ? hexToRgba(base["bg-sidebar"], chromeAlpha) : base["bg-sidebar"]); }