diff --git a/interface/opencode-embed-src/embed.tsx b/interface/opencode-embed-src/embed.tsx index 8787d8670..fa6b7d3c7 100644 --- a/interface/opencode-embed-src/embed.tsx +++ b/interface/opencode-embed-src/embed.tsx @@ -20,7 +20,7 @@ import { Font } from "@opencode-ai/ui/font" import { ThemeProvider, useTheme, type DesktopTheme, type ColorScheme } from "@opencode-ai/ui/theme" import { MetaProvider } from "@solidjs/meta" import { MemoryRouter, Route, createMemoryHistory } from "@solidjs/router" -import { ErrorBoundary, lazy, onMount, type ParentProps, Show, Suspense } from "solid-js" +import { createEffect, createSignal, ErrorBoundary, lazy, onMount, type ParentProps, Show, Suspense } from "solid-js" import { render } from "solid-js/web" // Theme overrides use `var(--color-*)` SpaceUI tokens rather than static hex, // so the embed tracks whichever Spacebot theme class is on (dark, @@ -124,8 +124,9 @@ function ServerKey(props: ParentProps) { /** * Registers and activates a custom theme + color scheme inside the - * ThemeProvider. Runs once on mount — theme changes propagate - * reactively through OpenCode's own effect in the ThemeProvider. + * ThemeProvider. Theme registration runs once on mount; colorScheme + * tracks props reactively so the host can flip light/dark without + * remounting (preserves session state). */ function ThemeInjector(props: ParentProps & { theme?: DesktopTheme; colorScheme?: ColorScheme }) { const ctx = useTheme() @@ -135,6 +136,9 @@ function ThemeInjector(props: ParentProps & { theme?: DesktopTheme; colorScheme? ctx.registerTheme(theme) ctx.setTheme(theme.id) } + }) + // Reactive — re-fires whenever the host updates the colorScheme signal + createEffect(() => { if (props.colorScheme) { ctx.setColorScheme(props.colorScheme) } @@ -180,6 +184,13 @@ export type MountOpenCodeHandle = { * e.g. handle.navigate("//session/") */ navigate: (route: string) => void + + /** + * Update the active color scheme ("light" | "dark" | "system") at runtime. + * Lets host apps re-theme the embed when their own theme changes, + * without remounting the embedded SolidJS app. + */ + setColorScheme: (scheme: ColorScheme) => void } /** @@ -195,7 +206,10 @@ export function mountOpenCode( container: HTMLElement, config: MountOpenCodeConfig, ): MountOpenCodeHandle { - const { serverUrl, initialRoute = "/", colorScheme = "dark" } = config + const { serverUrl, initialRoute = "/", colorScheme: initialColorScheme = "dark" } = config + // Reactive signal so the host can flip color scheme post-mount + // (handle.setColorScheme) without remounting the SolidJS tree. + const [colorScheme, setColorScheme] = createSignal(initialColorScheme) // Resolve theme: undefined → default Spacebot theme, null → no injection const theme = config.theme === undefined ? (spacebotTheme as DesktopTheme) @@ -238,7 +252,7 @@ export function mountOpenCode( - + }> @@ -284,5 +298,8 @@ export function mountOpenCode( navigate: (route: string) => { memory.set({ value: route }) }, + setColorScheme: (scheme: ColorScheme) => { + setColorScheme(scheme) + }, } } diff --git a/interface/opencode-embed-src/spacebot-theme.json b/interface/opencode-embed-src/spacebot-theme.json index abffeee9d..fcf256f51 100644 --- a/interface/opencode-embed-src/spacebot-theme.json +++ b/interface/opencode-embed-src/spacebot-theme.json @@ -37,9 +37,9 @@ "input-selected": "var(--color-app-selected)", "text-base": "var(--color-ink)", "text-weak": "var(--color-ink-dull)", - "text-weaker": "var(--color-ink-faint)", + "text-weaker": "#5a5a66", "text-strong": "var(--color-ink)", - "text-interactive-base": "var(--color-accent)", + "text-interactive-base": "#5818b8", "icon-base": "var(--color-ink-dull)", "icon-weak-base": "var(--color-ink-faint)", "icon-strong-base": "var(--color-ink)", @@ -77,23 +77,23 @@ "surface-diff-add-base": "#0e2018", "surface-diff-delete-base": "#200e10", "surface-diff-hidden-base": "#18181f", - "syntax-comment": "var(--color-ink-faint)", - "syntax-string": "#5cf0b0", - "syntax-primitive": "#f06060", - "syntax-property": "#c080f0", - "syntax-type": "#f0c070", - "syntax-constant": "#70d0f0", + "syntax-comment": "#5a5a66", + "syntax-string": "#066b30", + "syntax-primitive": "#a00808", + "syntax-property": "#5818b8", + "syntax-type": "#5d3800", + "syntax-constant": "#0a4d85", "syntax-keyword": "var(--color-ink-dull)", "syntax-operator": "var(--color-ink-dull)", "syntax-variable": "var(--color-ink)", "syntax-object": "var(--color-ink)", "syntax-punctuation": "var(--color-ink-dull)", - "syntax-info": "#70d0f0", + "syntax-info": "#0a4d85", "markdown-heading": "var(--color-accent-faint)", "markdown-text": "var(--color-ink)", - "markdown-link": "var(--color-accent)", - "markdown-link-text": "var(--color-accent)", - "markdown-code": "#5cf0b0", + "markdown-link": "#5818b8", + "markdown-link-text": "#5818b8", + "markdown-code": "#a00808", "markdown-block-quote": "var(--color-ink-dull)", "markdown-emph": "#f0c070", "markdown-strong": "var(--color-accent-faint)", @@ -102,7 +102,8 @@ "markdown-list-enumeration": "var(--color-accent-faint)", "markdown-image": "var(--color-accent-faint)", "markdown-image-text": "var(--color-accent-faint)", - "markdown-code-block": "var(--color-ink)" + "markdown-code-block": "var(--color-ink)", + "text-interactive-strong": "#5818b8" } }, "dark": { diff --git a/interface/src/components/OpenCodeEmbed.tsx b/interface/src/components/OpenCodeEmbed.tsx index 7af73e692..9f5623df2 100644 --- a/interface/src/components/OpenCodeEmbed.tsx +++ b/interface/src/components/OpenCodeEmbed.tsx @@ -1,4 +1,5 @@ import {useState, useEffect, useRef} from "react"; +import {useTheme} from "../hooks/useTheme"; /** RFC 4648 base64url encoding (no padding), matching OpenCode's directory encoding. */ export function base64UrlEncode(value: string): string { @@ -14,10 +15,15 @@ export function base64UrlEncode(value: string): string { let embedAssetsPromise: Promise<{ mountOpenCode: ( el: HTMLElement, - config: {serverUrl: string; initialRoute?: string}, + config: { + serverUrl: string; + initialRoute?: string; + colorScheme?: "light" | "dark" | "system"; + }, ) => { dispose: () => void; navigate: (route: string) => void; + setColorScheme?: (scheme: "light" | "dark" | "system") => void; }; cssText: string; }> | null = null; @@ -84,8 +90,60 @@ function loadEmbedAssets() { * Multiple OpenCodeEmbed instances can coexist (e.g. orchestration view); * the portal CSS is only removed when the last instance unmounts. */ +/** + * SpaceUI theme tokens (CSS custom properties) that need to cross the Shadow + * DOM boundary so the embedded OpenCode SPA can theme its prompt-input, + * buttons, etc. against the active Spacebot theme. Sourced from + * spaceui/packages/tokens/src/css/themes/.css — keep in sync when + * spaceui adds/removes tokens. + */ +const FORWARDED_THEME_TOKENS = [ + "--color-accent", "--color-accent-faint", "--color-accent-deep", + "--color-ink", "--color-ink-dull", "--color-ink-faint", + "--color-sidebar", "--color-sidebar-box", "--color-sidebar-line", + "--color-sidebar-ink", "--color-sidebar-ink-dull", "--color-sidebar-ink-faint", + "--color-sidebar-divider", "--color-sidebar-button", "--color-sidebar-selected", + "--color-sidebar-shade", + "--color-app", "--color-app-box", "--color-app-dark-box", "--color-app-darker-box", + "--color-app-light-box", "--color-app-overlay", "--color-app-input", "--color-app-focus", + "--color-app-line", "--color-app-divider", "--color-app-button", "--color-app-hover", + "--color-app-selected", "--color-app-selected-item", "--color-app-active", + "--color-app-shade", "--color-app-frame", "--color-app-slider", + "--color-app-explorer-scrollbar", + "--color-menu", "--color-menu-line", "--color-menu-ink", "--color-menu-faint", + "--color-menu-hover", "--color-menu-selected", "--color-menu-shade", +]; + +/** + * Read the current values of FORWARDED_THEME_TOKENS from the document root + * and inject them as `:host { --foo: bar; ... }` into the given style + * element. This makes the tokens available inside the Shadow DOM so the + * OpenCode embed's CSS can resolve var(--color-app-box) etc. against the + * active Spacebot theme. + */ +function forwardThemeTokens(styleEl: HTMLStyleElement) { + const styles = getComputedStyle(document.documentElement); + const declarations = FORWARDED_THEME_TOKENS + .map((name) => { + const value = styles.getPropertyValue(name).trim(); + return value ? `${name}: ${value};` : ""; + }) + .filter(Boolean) + .join("\n\t"); + styleEl.textContent = `:host {\n\t${declarations}\n}`; +} + let portalCssRefCount = 0; +/** + * Map a Spacebot theme to OpenCode's color scheme. Vanilla is the only + * dedicated light theme in the Spacebot palette today; everything else is + * a dark variant. If a future theme adds light variants, extend this list. + */ +function colorSchemeForTheme(theme: string): "light" | "dark" { + return theme === "vanilla" ? "light" : "dark"; +} + export function OpenCodeEmbed({ port, sessionId, @@ -101,7 +159,12 @@ export function OpenCodeEmbed({ const handleRef = useRef<{ dispose: () => void; navigate: (route: string) => void; + setColorScheme?: (scheme: "light" | "dark" | "system") => void; } | null>(null); + // Style element inside the shadow root that mirrors :root's SpaceUI + // theme tokens. Refresh on every theme change via the effect below. + const themeForwardRef = useRef(null); + const {theme} = useTheme(); // Route through the Spacebot proxy so it works for hosted/Tailscale // users, not just local dev. The proxy handles forwarding to the @@ -256,6 +319,15 @@ export function OpenCodeEmbed({ // Clear any previous content shadow.innerHTML = ""; + // Forward SpaceUI theme tokens into the shadow as :host CSS + // custom properties so OpenCode's embedded styles + our overrides + // can resolve var(--color-app-box) etc. against the active theme. + const themeForward = document.createElement("style"); + themeForward.id = "spacebot-theme-forward"; + shadow.appendChild(themeForward); + forwardThemeTokens(themeForward); + themeForwardRef.current = themeForward; + // Inject the OpenCode CSS into the shadow root const style = document.createElement("style"); style.textContent = cssText; @@ -319,6 +391,7 @@ export function OpenCodeEmbed({ const handle = mountOpenCode(mountDiv, { serverUrl, initialRoute: initialRouteRef.current, + colorScheme: colorSchemeForTheme(theme), }); handleRef.current = handle; @@ -354,6 +427,17 @@ export function OpenCodeEmbed({ }; }, [serverUrl]); + // Re-forward SpaceUI theme tokens AND update OpenCode's color scheme + // whenever the active Spacebot theme changes — both without remounting, + // preserving session state. + useEffect(() => { + const styleEl = themeForwardRef.current; + if (styleEl) { + forwardThemeTokens(styleEl); + } + handleRef.current?.setColorScheme?.(colorSchemeForTheme(theme)); + }, [theme]); + // Navigate the embedded app when the route changes (directory discovered // via SSE probe, or props changed). This avoids remounting the entire // SolidJS app just to change routes. diff --git a/interface/src/components/settings/AppearanceSection.tsx b/interface/src/components/settings/AppearanceSection.tsx index aef830bf3..a7ba8920b 100644 --- a/interface/src/components/settings/AppearanceSection.tsx +++ b/interface/src/components/settings/AppearanceSection.tsx @@ -1,55 +1,233 @@ -import {useTheme, THEMES, type ThemeId} from "@/hooks/useTheme"; +import { + useTheme, + THEMES, + type ThemeId, + type ThemeMode, + type ThemeOption, +} from "@/hooks/useTheme"; +const MODE_OPTIONS: {id: ThemeMode; label: string; description: string}[] = [ + {id: "light", label: "Light", description: "Always use the chosen light theme"}, + {id: "dark", label: "Dark", description: "Always use the chosen dark theme"}, + { + id: "system", + label: "Auto", + description: "Follow your system's light/dark preference", + }, +]; + +/** Settings panel: pick mode (light/dark/auto) and the theme used in each + * surface. The dark/light pickers stay visible together in auto mode so users + * can preview both without flipping their OS preference. */ export function AppearanceSection() { - const {theme, setTheme} = useTheme(); + const { + mode, + setMode, + lightTheme, + setLightTheme, + darkTheme, + setDarkTheme, + theme, + } = useTheme(); + + const lightThemes = THEMES.filter((t) => t.isLight); + const darkThemes = THEMES.filter((t) => !t.isLight); return (
-

Theme

+

Appearance

- Choose a theme for the dashboard interface. + Choose how the dashboard handles light and dark mode.

-
- {THEMES.map((t) => ( - - ))} + + +
+ {(mode === "light" || mode === "system") && ( + + )} + {(mode === "dark" || mode === "system") && ( + + )}
); } +/** Mode picker (light / dark / auto) backed by native radio inputs so screen + * readers and arrow-key navigation work without custom ARIA. */ +function ModeSelector({ + mode, + setMode, +}: { + mode: ThemeMode; + setMode: (m: ThemeMode) => void; +}) { + return ( +
+ + Mode + +
+ {MODE_OPTIONS.map((opt) => { + const selected = mode === opt.id; + return ( + + ); + })} +
+
+ ); +} + +/** Theme grid for a single surface (light or dark). `groupName` must differ + * between the two pickers so each is its own native radio group. */ +function ThemePickerSection({ + title, + subtitle, + themes, + selected, + onSelect, + active, + groupName, +}: { + title: string; + subtitle: string; + themes: ThemeOption[]; + selected: ThemeId; + onSelect: (id: ThemeId) => void; + active: ThemeId; + groupName: string; +}) { + return ( +
+
+ + {title} + +

{subtitle}

+
+
+ {themes.map((t) => { + const isSelected = selected === t.id; + const isActive = active === t.id; + return ( + + ); + })} +
+
+ ); +} + +/** Inline preview swatches per theme — cheap visual cue without computing + * resolved CSS vars. Pull a representative bg/sidebar/accent from each + * theme's known palette. */ +const PREVIEW_COLORS: Record< + ThemeId, + {bg: string; sidebar: string; accent: string} +> = { + default: {bg: "#1c1d26", sidebar: "#101118", accent: "#2499ff"}, + vanilla: {bg: "#ffffff", sidebar: "#f5f5f6", accent: "#2499ff"}, + midnight: {bg: "#121428", sidebar: "#0a0b14", accent: "#2499ff"}, + noir: {bg: "#080808", sidebar: "#000000", accent: "#2499ff"}, + slate: {bg: "#151619", sidebar: "#0e0f12", accent: "#2499ff"}, + nord: {bg: "#1a1e27", sidebar: "#11141b", accent: "#2499ff"}, + mocha: {bg: "#1a1614", sidebar: "#110f0d", accent: "#2499ff"}, + "catppuccin-latte": {bg: "#eff1f5", sidebar: "#e6e9ef", accent: "#8839ef"}, + "solarized-light": {bg: "#fdf6e3", sidebar: "#eee8d5", accent: "#268bd2"}, + "solarized-dark": {bg: "#002b36", sidebar: "#073642", accent: "#268bd2"}, +}; + +/** Tiny inline swatch (background + sidebar + accent stripes) drawn from a + * static palette so we don't have to compute resolved CSS vars per card. */ function ThemePreview({themeId}: {themeId: ThemeId}) { - const colors: Record = - { - default: {bg: "#1c1d26", sidebar: "#101118", accent: "#2499ff"}, - vanilla: {bg: "#ffffff", sidebar: "#f5f5f6", accent: "#2499ff"}, - midnight: {bg: "#121428", sidebar: "#0a0b14", accent: "#2499ff"}, - noir: {bg: "#080808", sidebar: "#000000", accent: "#2499ff"}, - slate: {bg: "#151619", sidebar: "#0e0f12", accent: "#2499ff"}, - nord: {bg: "#1a1e27", sidebar: "#11141b", accent: "#2499ff"}, - mocha: {bg: "#1a1614", sidebar: "#110f0d", accent: "#2499ff"}, - }; - const c = colors[themeId]; + const c = PREVIEW_COLORS[themeId]; return (
t.id === stored)) { - return stored as ThemeId; +/** Look up a theme option by its id, or undefined if unknown. */ +function getThemeById(id: string): ThemeOption | undefined { + return THEMES.find((t) => t.id === id); +} + +/** Read mode + per-mode theme slots from localStorage with one-time migration + * from the legacy single-key format ("spacebot-theme"). */ +function readPersisted(): { + mode: ThemeMode; + lightTheme: ThemeId; + darkTheme: ThemeId; +} { + if (typeof window === "undefined") { + return { mode: "system", lightTheme: DEFAULT_LIGHT, darkTheme: DEFAULT_DARK }; + } + + let mode = localStorage.getItem(MODE_KEY) as ThemeMode | null; + let lightTheme = localStorage.getItem(LIGHT_KEY) as ThemeId | null; + let darkTheme = localStorage.getItem(DARK_KEY) as ThemeId | null; + + // Migration: legacy `spacebot-theme` is a single ThemeId. Pin mode to the + // surface the user explicitly chose (light/dark) so they don't flip when + // the OS preference disagrees. Cleared after migration. + if (!mode && !lightTheme && !darkTheme) { + const legacy = localStorage.getItem(LEGACY_KEY); + const legacyTheme = legacy ? getThemeById(legacy) : undefined; + if (legacyTheme) { + if (legacyTheme.isLight) { + lightTheme = legacyTheme.id; + mode = "light"; + } else { + darkTheme = legacyTheme.id; + mode = "dark"; + } + localStorage.setItem(MODE_KEY, mode); + if (lightTheme) localStorage.setItem(LIGHT_KEY, lightTheme); + if (darkTheme) localStorage.setItem(DARK_KEY, darkTheme); + localStorage.removeItem(LEGACY_KEY); + console.info( + "[useTheme] migrated legacy spacebot-theme=%s → mode=%s light=%s dark=%s", + legacy, + mode, + lightTheme ?? DEFAULT_LIGHT, + darkTheme ?? DEFAULT_DARK, + ); + } } - return "default"; + + return { + mode: + mode === "light" || mode === "dark" || mode === "system" ? mode : "system", + lightTheme: + lightTheme && getThemeById(lightTheme)?.isLight + ? lightTheme + : DEFAULT_LIGHT, + darkTheme: + darkTheme && getThemeById(darkTheme)?.isLight === false + ? darkTheme + : DEFAULT_DARK, + }; } +/** Read the OS-level color-scheme preference. Defaults to dark on SSR or in + * environments without `matchMedia` so we don't flash a light surface. */ +function getSystemPrefersDark(): boolean { + if (typeof window === "undefined" || !window.matchMedia) return true; + return window.matchMedia("(prefers-color-scheme: dark)").matches; +} + +/** Compute the currently-active theme id from mode + per-mode choices + + * system preference. */ +function effectiveTheme( + mode: ThemeMode, + lightTheme: ThemeId, + darkTheme: ThemeId, + systemPrefersDark: boolean, +): ThemeId { + if (mode === "light") return lightTheme; + if (mode === "dark") return darkTheme; + return systemPrefersDark ? darkTheme : lightTheme; +} + +/** Toggle theme classes on `` so SpaceUI tokens resolve to the selected + * theme. All known theme classes are removed before the new one is added so + * there's never more than one active. */ function applyThemeClass(themeId: ThemeId) { - const theme = THEMES.find((t) => t.id === themeId); + const theme = getThemeById(themeId); const root = document.documentElement; // Remove all theme classes - THEMES.forEach((t) => { - if (t.className) { - root.classList.remove(t.className); - } - }); + for (const t of THEMES) { + if (t.className) root.classList.remove(t.className); + } // Add the selected theme class if (theme?.className) { @@ -89,24 +206,94 @@ function applyThemeClass(themeId: ThemeId) { } } +/** Theme state hook with per-mode persistence, OS-preference subscription, + * and FOUC-safe pre-hydration init. Exposes `mode` (light/dark/system), a + * theme slot per surface, and a back-compat `setTheme` setter. */ export function useTheme() { - const [theme, setThemeState] = useState(getInitialTheme); + const [{ mode, lightTheme, darkTheme }, setPersisted] = useState(readPersisted); + const [systemPrefersDark, setSystemPrefersDark] = useState(getSystemPrefersDark); - // Apply theme on mount and when theme changes + // Subscribe to OS-level color-scheme preference changes + useEffect(() => { + if (typeof window === "undefined" || !window.matchMedia) return; + const mq = window.matchMedia("(prefers-color-scheme: dark)"); + const onChange = (e: MediaQueryListEvent) => + setSystemPrefersDark(e.matches); + mq.addEventListener("change", onChange); + return () => mq.removeEventListener("change", onChange); + }, []); + + const theme = useMemo( + () => effectiveTheme(mode, lightTheme, darkTheme, systemPrefersDark), + [mode, lightTheme, darkTheme, systemPrefersDark], + ); + + // Apply theme class on every change useEffect(() => { applyThemeClass(theme); }, [theme]); - const setTheme = useCallback((newTheme: ThemeId) => { - setThemeState(newTheme); - localStorage.setItem(STORAGE_KEY, newTheme); + const setMode = useCallback((newMode: ThemeMode) => { + setPersisted((prev) => ({ ...prev, mode: newMode })); + localStorage.setItem(MODE_KEY, newMode); + }, []); + + const setLightTheme = useCallback((id: ThemeId) => { + const opt = getThemeById(id); + if (!opt?.isLight) return; + setPersisted((prev) => ({ ...prev, lightTheme: id })); + localStorage.setItem(LIGHT_KEY, id); }, []); - return { theme, setTheme, themes: THEMES }; + const setDarkTheme = useCallback((id: ThemeId) => { + const opt = getThemeById(id); + if (!opt || opt.isLight) return; + setPersisted((prev) => ({ ...prev, darkTheme: id })); + localStorage.setItem(DARK_KEY, id); + }, []); + + /** Backward-compatible setter — routes the chosen theme into the right + * slot based on isLight, and switches mode away from "system" so the + * choice sticks. Keeps existing call sites working unchanged. */ + const setTheme = useCallback( + (id: ThemeId) => { + const opt = getThemeById(id); + if (!opt) return; + if (opt.isLight) { + setLightTheme(id); + setMode("light"); + } else { + setDarkTheme(id); + setMode("dark"); + } + }, + [setLightTheme, setDarkTheme, setMode], + ); + + const isLight = !!getThemeById(theme)?.isLight; + + return { + mode, + setMode, + lightTheme, + setLightTheme, + darkTheme, + setDarkTheme, + theme, + setTheme, + themes: THEMES, + isLight, + }; } -// Initialize theme on page load (before React hydrates) +// Initialize theme on page load (before React hydrates) to avoid FOUC. if (typeof window !== "undefined") { - const initialTheme = getInitialTheme(); - applyThemeClass(initialTheme); + const persisted = readPersisted(); + const initial = effectiveTheme( + persisted.mode, + persisted.lightTheme, + persisted.darkTheme, + getSystemPrefersDark(), + ); + applyThemeClass(initial); } diff --git a/interface/src/styles.css b/interface/src/styles.css index 6e176bff9..70aa75fd3 100644 --- a/interface/src/styles.css +++ b/interface/src/styles.css @@ -21,6 +21,11 @@ @import "@spacedrive/tokens/css/themes/nord"; @import "@spacedrive/tokens/css/themes/mocha"; +/* Local theme additions (Spacebot-specific; not yet upstream in @spacedrive/tokens) */ +@import "./themes/solarized-light.css"; +@import "./themes/solarized-dark.css"; +@import "./themes/catppuccin-latte.css"; + /* Tell Tailwind to scan SpaceUI component output for classes */ @source "../node_modules/@spacedrive/primitives/dist"; @source "../node_modules/@spacedrive/ai/dist"; diff --git a/interface/src/themes/catppuccin-latte.css b/interface/src/themes/catppuccin-latte.css new file mode 100644 index 000000000..d9796b113 --- /dev/null +++ b/interface/src/themes/catppuccin-latte.css @@ -0,0 +1,46 @@ +/* Catppuccin Latte — official light variant pairing with the existing mocha + (Catppuccin Mocha) dark theme. Translated into SpaceUI tokens. */ +.catppuccin-latte-theme { + --color-accent: hsl(266, 85%, 58%); /* mauve */ + --color-accent-faint: hsl(266, 85%, 68%); + --color-accent-deep: hsl(266, 85%, 48%); + --color-ink: hsl(234, 16%, 35%); /* text */ + --color-ink-dull: hsl(233, 13%, 41%); /* subtext1 */ + --color-ink-faint: hsl(233, 10%, 47%); /* subtext0 */ + --color-sidebar: hsl(220, 21%, 89%); /* mantle */ + --color-sidebar-box: hsl(220, 23%, 95%); /* base */ + --color-sidebar-line: hsl(225, 14%, 76%); /* surface1 */ + --color-sidebar-ink: hsl(234, 16%, 35%); + --color-sidebar-ink-dull: hsl(233, 13%, 41%); + --color-sidebar-ink-faint: hsl(233, 10%, 47%); + --color-sidebar-divider: hsl(225, 14%, 84%); + --color-sidebar-button: hsl(220, 23%, 97%); + --color-sidebar-selected: hsl(225, 14%, 80%); + --color-sidebar-shade: hsl(220, 23%, 95%); + --color-app: hsl(220, 23%, 95%); /* base */ + --color-app-box: hsl(220, 22%, 92%); + --color-app-dark-box: hsl(220, 21%, 89%); /* mantle */ + --color-app-darker-box: hsl(220, 17%, 87%); /* crust */ + --color-app-light-box: hsl(220, 23%, 97%); + --color-app-overlay: hsl(220, 22%, 92%); + --color-app-input: hsl(220, 23%, 97%); + --color-app-focus: hsl(220, 23%, 95%); + --color-app-line: hsl(225, 14%, 80%); + --color-app-divider: hsl(225, 14%, 70%); + --color-app-button: hsl(220, 23%, 97%); + --color-app-hover: hsl(220, 22%, 90%); + --color-app-selected: hsl(225, 14%, 80%); + --color-app-selected-item: hsl(220, 22%, 92%); + --color-app-active: hsl(225, 14%, 75%); + --color-app-shade: hsl(225, 14%, 50%); + --color-app-frame: hsl(220, 23%, 95%); + --color-app-slider: hsl(220, 22%, 92%); + --color-app-explorer-scrollbar: hsl(225, 14%, 65%); + --color-menu: hsl(220, 23%, 95%); + --color-menu-line: hsl(220, 22%, 92%); + --color-menu-ink: hsl(234, 16%, 25%); + --color-menu-faint: hsl(225, 14%, 70%); + --color-menu-hover: hsl(220, 22%, 88%); + --color-menu-selected: hsl(225, 14%, 80%); + --color-menu-shade: hsl(234, 16%, 0%); +} diff --git a/interface/src/themes/solarized-dark.css b/interface/src/themes/solarized-dark.css new file mode 100644 index 000000000..d2312eddf --- /dev/null +++ b/interface/src/themes/solarized-dark.css @@ -0,0 +1,46 @@ +/* Solarized Dark — Ethan Schoonover's palette translated into SpaceUI tokens. + Background: base03 (#002b36). Body text: base1 (#93a1a1). Accent: blue. */ +.solarized-dark-theme { + --color-accent: hsl(205, 69%, 49%); /* solarized blue */ + --color-accent-faint: hsl(205, 69%, 60%); + --color-accent-deep: hsl(205, 69%, 40%); + --color-ink: hsl(180, 7%, 60%); /* base1 */ + --color-ink-dull: hsl(186, 8%, 55%); /* base0 */ + --color-ink-faint: hsl(194, 14%, 40%); /* base01 */ + --color-sidebar: hsl(192, 100%, 8%); + --color-sidebar-box: hsl(192, 81%, 14%); /* base02 */ + --color-sidebar-line: hsl(192, 81%, 18%); + --color-sidebar-ink: hsl(180, 7%, 65%); + --color-sidebar-ink-dull: hsl(186, 8%, 55%); + --color-sidebar-ink-faint: hsl(194, 14%, 40%); + --color-sidebar-divider: hsl(192, 100%, 9%); + --color-sidebar-button: hsl(192, 81%, 14%); + --color-sidebar-selected: hsl(192, 81%, 22%); + --color-sidebar-shade: hsl(192, 100%, 4%); + --color-app: hsl(192, 100%, 11%); /* base03 */ + --color-app-box: hsl(192, 81%, 14%); /* base02 */ + --color-app-dark-box: hsl(192, 100%, 9%); + --color-app-darker-box: hsl(192, 100%, 6%); + --color-app-light-box: hsl(192, 81%, 22%); + --color-app-overlay: hsl(192, 81%, 14%); + --color-app-input: hsl(192, 81%, 16%); + --color-app-focus: hsl(192, 100%, 8%); + --color-app-line: hsl(192, 81%, 20%); + --color-app-divider: hsl(192, 100%, 6%); + --color-app-button: hsl(192, 81%, 18%); + --color-app-hover: hsl(192, 81%, 22%); + --color-app-selected: hsl(192, 81%, 26%); + --color-app-selected-item: hsl(192, 81%, 14%); + --color-app-active: hsl(192, 81%, 30%); + --color-app-shade: hsl(192, 100%, 2%); + --color-app-frame: hsl(192, 81%, 22%); + --color-app-slider: hsl(192, 81%, 16%); + --color-app-explorer-scrollbar: hsl(192, 81%, 22%); + --color-menu: hsl(192, 100%, 8%); + --color-menu-line: hsl(192, 81%, 14%); + --color-menu-ink: hsl(180, 7%, 70%); + --color-menu-faint: hsl(186, 8%, 55%); + --color-menu-hover: hsl(192, 81%, 22%); + --color-menu-selected: hsl(192, 81%, 26%); + --color-menu-shade: hsl(192, 100%, 2%); +} diff --git a/interface/src/themes/solarized-light.css b/interface/src/themes/solarized-light.css new file mode 100644 index 000000000..9030a3148 --- /dev/null +++ b/interface/src/themes/solarized-light.css @@ -0,0 +1,46 @@ +/* Solarized Light — Ethan Schoonover's palette translated into SpaceUI tokens. + Background: base3 (#fdf6e3). Body text: base01 (#586e75). Accent: blue. */ +.solarized-light-theme { + --color-accent: hsl(205, 69%, 49%); /* solarized blue */ + --color-accent-faint: hsl(205, 69%, 60%); + --color-accent-deep: hsl(205, 69%, 40%); + --color-ink: hsl(194, 14%, 40%); /* base01 */ + --color-ink-dull: hsl(194, 14%, 50%); /* base00 */ + --color-ink-faint: hsl(180, 7%, 60%); /* base1 */ + --color-sidebar: hsl(46, 42%, 88%); /* base2 */ + --color-sidebar-box: hsl(44, 87%, 94%); /* base3 */ + --color-sidebar-line: hsl(46, 30%, 78%); + --color-sidebar-ink: hsl(194, 14%, 40%); + --color-sidebar-ink-dull: hsl(194, 14%, 50%); + --color-sidebar-ink-faint: hsl(180, 7%, 60%); + --color-sidebar-divider: hsl(46, 42%, 85%); + --color-sidebar-button: hsl(44, 87%, 94%); + --color-sidebar-selected: hsl(46, 42%, 80%); + --color-sidebar-shade: hsl(46, 42%, 92%); + --color-app: hsl(44, 87%, 94%); /* base3 */ + --color-app-box: hsl(44, 50%, 96%); + --color-app-dark-box: hsl(46, 42%, 88%); /* base2 */ + --color-app-darker-box: hsl(46, 42%, 84%); + --color-app-light-box: hsl(44, 87%, 97%); + --color-app-overlay: hsl(44, 50%, 96%); + --color-app-input: hsl(44, 87%, 97%); + --color-app-focus: hsl(44, 87%, 94%); + --color-app-line: hsl(46, 30%, 80%); + --color-app-divider: hsl(46, 42%, 70%); + --color-app-button: hsl(44, 87%, 97%); + --color-app-hover: hsl(46, 42%, 90%); + --color-app-selected: hsl(46, 42%, 82%); + --color-app-selected-item: hsl(46, 42%, 88%); + --color-app-active: hsl(46, 42%, 75%); + --color-app-shade: hsl(46, 42%, 50%); + --color-app-frame: hsl(44, 50%, 96%); + --color-app-slider: hsl(46, 42%, 88%); + --color-app-explorer-scrollbar: hsl(46, 30%, 65%); + --color-menu: hsl(44, 50%, 96%); + --color-menu-line: hsl(46, 42%, 88%); + --color-menu-ink: hsl(194, 14%, 30%); + --color-menu-faint: hsl(46, 42%, 70%); + --color-menu-hover: hsl(46, 42%, 88%); + --color-menu-selected: hsl(46, 42%, 80%); + --color-menu-shade: hsl(194, 14%, 0%); +}