Date: Wed, 24 Dec 2025 06:17:13 -0500
Subject: [PATCH 017/219] fix: remove SVG favicon to improve SEO (#5755)
---
packages/ui/src/components/favicon.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/packages/ui/src/components/favicon.tsx b/packages/ui/src/components/favicon.tsx
index dec18f1adfc..3462384d458 100644
--- a/packages/ui/src/components/favicon.tsx
+++ b/packages/ui/src/components/favicon.tsx
@@ -4,7 +4,6 @@ export const Favicon = () => {
return (
<>
-
From 3aca9e5fa5909ba3fef3e6908eeb01a898316744 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Wed, 24 Dec 2025 05:22:21 -0600
Subject: [PATCH 018/219] fix(desktop): conditionally show review pane toggle
---
packages/app/src/components/header.tsx | 60 +++++++++++++-------------
packages/app/src/pages/session.tsx | 3 +-
2 files changed, 32 insertions(+), 31 deletions(-)
diff --git a/packages/app/src/components/header.tsx b/packages/app/src/components/header.tsx
index ec7cdfa25ba..3eae0e05d41 100644
--- a/packages/app/src/components/header.tsx
+++ b/packages/app/src/components/header.tsx
@@ -109,35 +109,37 @@ export function Header(props: {
-
- Toggle review
- {command.keybind("review.toggle")}
-
- }
- >
-
-
+
+
+ Toggle review
+ {command.keybind("review.toggle")}
+
+ }
+ >
+
+
+
layout.review.toggle(),
},
{
From aa1c560e5ee3cbc2c996b2af0105cdf4f55df818 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Wed, 24 Dec 2025 05:49:35 -0600
Subject: [PATCH 019/219] fix(desktop): hang on backtracing-prone regex
---
packages/ui/src/components/markdown.tsx | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx
index 071132e80bb..380a3c8a46d 100644
--- a/packages/ui/src/components/markdown.tsx
+++ b/packages/ui/src/components/markdown.tsx
@@ -2,9 +2,18 @@ import { useMarked } from "../context/marked"
import { ComponentProps, createResource, splitProps } from "solid-js"
function strip(text: string): string {
- const wrappedRe = /^\s*<([A-Za-z]\w*)>\s*([\s\S]*?)\s*<\/\1>\s*$/
- const match = text.match(wrappedRe)
- return match ? match[2] : text
+ const trimmed = text.trim()
+ const match = trimmed.match(/^<([A-Za-z]\w*)>/)
+ if (!match) return text
+
+ const tagName = match[1]
+ const closingTag = `${tagName}>`
+ if (trimmed.endsWith(closingTag)) {
+ const content = trimmed.slice(match[0].length, -closingTag.length)
+ return content.trim()
+ }
+
+ return text
}
export function Markdown(
From ede4e467db2bf949cfa2b25c70ff873aeb97be9a Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Wed, 24 Dec 2025 05:55:28 -0600
Subject: [PATCH 020/219] deps: update marked and marked-shiki
---
bun.lock | 18 +++++++++---------
package.json | 2 ++
packages/app/package.json | 4 ++--
packages/ui/package.json | 4 ++--
packages/web/package.json | 4 ++--
5 files changed, 17 insertions(+), 15 deletions(-)
diff --git a/bun.lock b/bun.lock
index c8a98fca6e3..fb45259f88b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -51,8 +51,8 @@
"fuzzysort": "catalog:",
"ghostty-web": "0.3.0",
"luxon": "catalog:",
- "marked": "16.2.0",
- "marked-shiki": "1.2.1",
+ "marked": "catalog:",
+ "marked-shiki": "catalog:",
"remeda": "catalog:",
"shiki": "catalog:",
"solid-js": "catalog:",
@@ -403,8 +403,8 @@
"@typescript/native-preview": "catalog:",
"fuzzysort": "catalog:",
"luxon": "catalog:",
- "marked": "16.2.0",
- "marked-shiki": "1.2.1",
+ "marked": "catalog:",
+ "marked-shiki": "catalog:",
"remeda": "catalog:",
"shiki": "catalog:",
"solid-js": "catalog:",
@@ -451,8 +451,8 @@
"js-base64": "3.7.7",
"lang-map": "0.4.0",
"luxon": "catalog:",
- "marked": "15.0.12",
- "marked-shiki": "1.2.1",
+ "marked": "catalog:",
+ "marked-shiki": "catalog:",
"rehype-autolink-headings": "7.1.0",
"remeda": "catalog:",
"shiki": "catalog:",
@@ -502,6 +502,8 @@
"hono": "4.10.7",
"hono-openapi": "1.1.2",
"luxon": "3.6.1",
+ "marked": "17.0.1",
+ "marked-shiki": "1.2.1",
"remeda": "2.26.0",
"shiki": "3.20.0",
"solid-js": "1.9.10",
@@ -2865,7 +2867,7 @@
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
- "marked": ["marked@16.2.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg=="],
+ "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
"marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="],
@@ -4113,8 +4115,6 @@
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="],
- "@opencode-ai/web/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
-
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
"@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="],
diff --git a/package.json b/package.json
index 23ef2253a31..7346a4ca853 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,8 @@
"hono-openapi": "1.1.2",
"fuzzysort": "3.1.0",
"luxon": "3.6.1",
+ "marked": "17.0.1",
+ "marked-shiki": "1.2.1",
"typescript": "5.8.2",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
"zod": "4.1.8",
diff --git a/packages/app/package.json b/packages/app/package.json
index 38d51cbe873..cf1246b3b63 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -49,8 +49,8 @@
"fuzzysort": "catalog:",
"ghostty-web": "0.3.0",
"luxon": "catalog:",
- "marked": "16.2.0",
- "marked-shiki": "1.2.1",
+ "marked": "catalog:",
+ "marked-shiki": "catalog:",
"remeda": "catalog:",
"shiki": "catalog:",
"solid-js": "catalog:",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 0bc87953a4d..940b3252a03 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -44,8 +44,8 @@
"@typescript/native-preview": "catalog:",
"fuzzysort": "catalog:",
"luxon": "catalog:",
- "marked": "16.2.0",
- "marked-shiki": "1.2.1",
+ "marked": "catalog:",
+ "marked-shiki": "catalog:",
"remeda": "catalog:",
"shiki": "catalog:",
"solid-js": "catalog:",
diff --git a/packages/web/package.json b/packages/web/package.json
index 0c996a62ee3..68cb311843b 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -24,8 +24,8 @@
"js-base64": "3.7.7",
"lang-map": "0.4.0",
"luxon": "catalog:",
- "marked": "15.0.12",
- "marked-shiki": "1.2.1",
+ "marked": "catalog:",
+ "marked-shiki": "catalog:",
"rehype-autolink-headings": "7.1.0",
"remeda": "catalog:",
"shiki": "catalog:",
From db57e7023a3fc1e60f89d527606c2bd99075259d Mon Sep 17 00:00:00 2001
From: Github Action
Date: Wed, 24 Dec 2025 11:56:43 +0000
Subject: [PATCH 021/219] Update Nix flake.lock and hashes
---
flake.lock | 6 +++---
nix/hashes.json | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/flake.lock b/flake.lock
index f464145b09f..4ff2c1d0e11 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1766410818,
- "narHash": "sha256-ruVneSx6wFy5PMw1ow3BE+znl653TJ6+eeNUj4B/9y8=",
+ "lastModified": 1766532406,
+ "narHash": "sha256-acLU/ag9VEoKkzOD202QASX25nG1eArXg5A0mHjKgxM=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "3a7affa77a5a539afa1c7859e2c31abdb1aeadf3",
+ "rev": "8142186f001295e5a3239f485c8a49bf2de2695a",
"type": "github"
},
"original": {
diff --git a/nix/hashes.json b/nix/hashes.json
index 4184ed1d580..66c0baaf791 100644
--- a/nix/hashes.json
+++ b/nix/hashes.json
@@ -1,3 +1,3 @@
{
- "nodeModules": "sha256-CDOAY2h2AAcSuVqV1uyxDmfzSa/vV8lnXOKDgAC4mgg="
+ "nodeModules": "sha256-hotsyeWJA6/dP6DvZTN1Ak2RSKcsyvXlXPI/jexBHME="
}
From 8eab6770940295c95412f04e39b7392152848ccc Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Wed, 24 Dec 2025 05:57:27 -0600
Subject: [PATCH 022/219] fix: don't disable text selection
---
packages/app/index.html | 2 +-
packages/enterprise/src/entry-server.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/app/index.html b/packages/app/index.html
index 9803517a07e..2c3a0eabd40 100644
--- a/packages/app/index.html
+++ b/packages/app/index.html
@@ -14,7 +14,7 @@
-
+
-
diff --git a/packages/app/script/inject-theme-preload.ts b/packages/app/script/inject-theme-preload.ts
new file mode 100644
index 00000000000..511ab7a3b5b
--- /dev/null
+++ b/packages/app/script/inject-theme-preload.ts
@@ -0,0 +1,18 @@
+/**
+ * Injects the theme preload script into index.html.
+ * Run this as part of the build process.
+ */
+
+import { generatePreloadScript } from "@opencode-ai/ui/theme"
+
+const htmlPath = new URL("../index.html", import.meta.url).pathname
+const html = await Bun.file(htmlPath).text()
+
+const script = generatePreloadScript()
+const injectedHtml = html.replace(
+ /`,
+)
+
+await Bun.write(htmlPath, injectedHtml)
+console.log("Injected theme preload script into index.html")
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index de8fcf7d124..bf5ba956622 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -8,6 +8,7 @@ import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
import { Diff } from "@opencode-ai/ui/diff"
import { Code } from "@opencode-ai/ui/code"
+import { ThemeProvider } from "@opencode-ai/ui/theme"
import { GlobalSyncProvider } from "@/context/global-sync"
import { LayoutProvider } from "@/context/layout"
import { GlobalSDKProvider } from "@/context/global-sdk"
@@ -45,48 +46,50 @@ export function App() {
return (
- }>
-
-
-
-
-
-
-
-
- (
-
- {props.children}
-
- )}
- >
-
-
- } />
- (
-
-
-
-
-
-
-
- )}
- />
-
-
-
-
-
-
-
-
-
-
-
+
+ }>
+
+
+
+
+
+
+
+
+ (
+
+ {props.children}
+
+ )}
+ >
+
+
+ } />
+ (
+
+
+
+
+
+
+
+ )}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx
index 1d6bab2a44a..03251fe5f5e 100644
--- a/packages/app/src/components/terminal.tsx
+++ b/packages/app/src/components/terminal.tsx
@@ -1,9 +1,9 @@
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
-import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
+import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
import { useSDK } from "@/context/sdk"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/terminal"
-import { usePrefersDark } from "@solid-primitives/media"
+import { resolveThemeVariant, useTheme } from "@opencode-ai/ui/theme"
export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY
@@ -12,8 +12,28 @@ export interface TerminalProps extends ComponentProps<"div"> {
onConnectError?: (error: unknown) => void
}
+type TerminalColors = {
+ background: string
+ foreground: string
+ cursor: string
+}
+
+const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
+ light: {
+ background: "#fcfcfc",
+ foreground: "#211e1e",
+ cursor: "#211e1e",
+ },
+ dark: {
+ background: "#191515",
+ foreground: "#d4d4d4",
+ cursor: "#d4d4d4",
+ },
+}
+
export const Terminal = (props: TerminalProps) => {
const sdk = useSDK()
+ const theme = useTheme()
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
let ws: WebSocket
@@ -22,7 +42,35 @@ export const Terminal = (props: TerminalProps) => {
let serializeAddon: SerializeAddon
let fitAddon: FitAddon
let handleResize: () => void
- const prefersDark = usePrefersDark()
+
+ const getTerminalColors = (): TerminalColors => {
+ const mode = theme.mode()
+ const fallback = DEFAULT_TERMINAL_COLORS[mode]
+ const currentTheme = theme.themes()[theme.themeId()]
+ if (!currentTheme) return fallback
+ const variant = mode === "dark" ? currentTheme.dark : currentTheme.light
+ if (!variant?.seeds) return fallback
+ const resolved = resolveThemeVariant(variant, mode === "dark")
+ const text = resolved["text-base"] ?? fallback.foreground
+ const background = resolved["background-stronger"] ?? fallback.background
+ return {
+ background,
+ foreground: text,
+ cursor: text,
+ }
+ }
+
+ const [terminalColors, setTerminalColors] = createSignal(getTerminalColors())
+
+ createEffect(() => {
+ const colors = getTerminalColors()
+ setTerminalColors(colors)
+ if (!term) return
+ const setOption = (term as unknown as { setOption?: (key: string, value: TerminalColors) => void }).setOption
+ if (!setOption) return
+ setOption("theme", colors)
+ })
+
const focusTerminal = () => term?.focus()
const copySelection = () => {
if (!term || !term.hasSelection()) return false
@@ -62,17 +110,7 @@ export const Terminal = (props: TerminalProps) => {
fontSize: 14,
fontFamily: "IBM Plex Mono, monospace",
allowTransparency: true,
- theme: prefersDark()
- ? {
- background: "#191515",
- foreground: "#d4d4d4",
- cursor: "#d4d4d4",
- }
- : {
- background: "#fcfcfc",
- foreground: "#211e1e",
- cursor: "#211e1e",
- },
+ theme: terminalColors(),
scrollback: 10_000,
ghostty,
})
@@ -192,6 +230,7 @@ export const Terminal = (props: TerminalProps) => {
ref={container}
data-component="terminal"
data-prevent-autofocus
+ style={{ "background-color": terminalColors().background }}
classList={{
...(local.classList ?? {}),
"size-full px-6 py-3 font-mono": true,
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 0b4e040b749..08b3401871f 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -47,6 +47,7 @@ import { useNotification } from "@/context/notification"
import { Binary } from "@opencode-ai/util/binary"
import { Header } from "@/components/header"
import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { useCommand } from "@/context/command"
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
@@ -89,6 +90,41 @@ export default function Layout(props: ParentProps) {
const providers = useProviders()
const dialog = useDialog()
const command = useCommand()
+ const theme = useTheme()
+ const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
+ const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
+ const colorSchemeLabel: Record = {
+ system: "System",
+ light: "Light",
+ dark: "Dark",
+ }
+
+ function cycleTheme(direction = 1) {
+ const ids = availableThemeEntries().map(([id]) => id)
+ if (ids.length === 0) return
+ const currentIndex = ids.indexOf(theme.themeId())
+ const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length
+ const nextThemeId = ids[nextIndex]
+ theme.setTheme(nextThemeId)
+ const nextTheme = theme.themes()[nextThemeId]
+ showToast({
+ title: "Theme switched",
+ description: nextTheme?.name ?? nextThemeId,
+ })
+ }
+
+ function cycleColorScheme(direction = 1) {
+ const current = theme.colorScheme()
+ const currentIndex = colorSchemeOrder.indexOf(current)
+ const nextIndex =
+ currentIndex === -1 ? 0 : (currentIndex + direction + colorSchemeOrder.length) % colorSchemeOrder.length
+ const next = colorSchemeOrder[nextIndex]
+ theme.setColorScheme(next)
+ showToast({
+ title: "Color scheme",
+ description: colorSchemeLabel[next],
+ })
+ }
onMount(async () => {
if (platform.checkUpdate && platform.update && platform.restart) {
@@ -286,57 +322,94 @@ export default function Layout(props: ParentProps) {
}
}
- command.register(() => [
- {
- id: "sidebar.toggle",
- title: "Toggle sidebar",
- category: "View",
- keybind: "mod+b",
- onSelect: () => layout.sidebar.toggle(),
- },
- ...(platform.openDirectoryPickerDialog
- ? [
- {
- id: "project.open",
- title: "Open project",
- category: "Project",
- keybind: "mod+o",
- onSelect: () => chooseProject(),
- },
- ]
- : []),
- {
- id: "provider.connect",
- title: "Connect provider",
- category: "Provider",
- onSelect: () => connectProvider(),
- },
- {
- id: "session.previous",
- title: "Previous session",
- category: "Session",
- keybind: "alt+arrowup",
- onSelect: () => navigateSessionByOffset(-1),
- },
- {
- id: "session.next",
- title: "Next session",
- category: "Session",
- keybind: "alt+arrowdown",
- onSelect: () => navigateSessionByOffset(1),
- },
- {
- id: "session.archive",
- title: "Archive session",
- category: "Session",
- keybind: "mod+shift+backspace",
- disabled: !params.dir || !params.id,
- onSelect: () => {
- const session = currentSessions().find((s) => s.id === params.id)
- if (session) archiveSession(session)
+ command.register(() => {
+ const commands = [
+ {
+ id: "sidebar.toggle",
+ title: "Toggle sidebar",
+ category: "View",
+ keybind: "mod+b",
+ onSelect: () => layout.sidebar.toggle(),
+ },
+ ...(platform.openDirectoryPickerDialog
+ ? [
+ {
+ id: "project.open",
+ title: "Open project",
+ category: "Project",
+ keybind: "mod+o",
+ onSelect: () => chooseProject(),
+ },
+ ]
+ : []),
+ {
+ id: "provider.connect",
+ title: "Connect provider",
+ category: "Provider",
+ onSelect: () => connectProvider(),
+ },
+ {
+ id: "session.previous",
+ title: "Previous session",
+ category: "Session",
+ keybind: "alt+arrowup",
+ onSelect: () => navigateSessionByOffset(-1),
},
- },
- ])
+ {
+ id: "session.next",
+ title: "Next session",
+ category: "Session",
+ keybind: "alt+arrowdown",
+ onSelect: () => navigateSessionByOffset(1),
+ },
+ {
+ id: "session.archive",
+ title: "Archive session",
+ category: "Session",
+ keybind: "mod+shift+backspace",
+ disabled: !params.dir || !params.id,
+ onSelect: () => {
+ const session = currentSessions().find((s) => s.id === params.id)
+ if (session) archiveSession(session)
+ },
+ },
+ {
+ id: "theme.cycle",
+ title: "Cycle theme",
+ category: "Theme",
+ keybind: "mod+shift+t",
+ onSelect: () => cycleTheme(1),
+ },
+ ]
+
+ for (const [id, definition] of availableThemeEntries()) {
+ commands.push({
+ id: `theme.set.${id}`,
+ title: `Use theme: ${definition.name ?? id}`,
+ category: "Theme",
+ onSelect: () => theme.setTheme(id),
+ })
+ }
+
+ commands.push({
+ id: "theme.scheme.cycle",
+ title: "Cycle color scheme",
+ category: "Theme",
+ keybind: "mod+shift+s",
+ onSelect: () => cycleColorScheme(1),
+ })
+
+ for (const scheme of colorSchemeOrder) {
+ commands.push({
+ id: `theme.scheme.${scheme}`,
+ title: `Use color scheme: ${colorSchemeLabel[scheme]}`,
+ category: "Theme",
+ onSelect: () => theme.setColorScheme(scheme),
+ })
+ }
+
+ return commands
+ })
function connectProvider() {
dialog.show(() => )
diff --git a/packages/app/vite.js b/packages/app/vite.js
index 6b8fd61376c..5ab36883653 100644
--- a/packages/app/vite.js
+++ b/packages/app/vite.js
@@ -1,6 +1,23 @@
import solidPlugin from "vite-plugin-solid"
import tailwindcss from "@tailwindcss/vite"
import { fileURLToPath } from "url"
+import { generatePreloadScript } from "@opencode-ai/ui/theme"
+
+/**
+ * Vite plugin that injects the theme preload script into index.html.
+ * This ensures the theme is applied before the page renders, avoiding FOUC.
+ * @type {import("vite").Plugin}
+ */
+const themePreloadPlugin = {
+ name: "opencode-desktop:theme-preload",
+ transformIndexHtml(html) {
+ const script = generatePreloadScript()
+ return html.replace(
+ /`,
+ )
+ },
+}
/**
* @type {import("vite").PluginOption}
@@ -21,6 +38,7 @@ export default [
}
},
},
+ themePreloadPlugin,
tailwindcss(),
solidPlugin(),
]
diff --git a/packages/desktop/index.html b/packages/desktop/index.html
index faeb1a1fde0..ea656068ca9 100644
--- a/packages/desktop/index.html
+++ b/packages/desktop/index.html
@@ -1,5 +1,5 @@
-
+
@@ -13,14 +13,12 @@
+
+
-
diff --git a/packages/ui/package.json b/packages/ui/package.json
index bb6adb0fb76..0aa059cd974 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -11,6 +11,9 @@
"./context/*": "./src/context/*.tsx",
"./styles": "./src/styles/index.css",
"./styles/tailwind": "./src/styles/tailwind/index.css",
+ "./theme": "./src/theme/index.ts",
+ "./theme/*": "./src/theme/*.ts",
+ "./theme/context": "./src/theme/context.tsx",
"./icons/provider": "./src/components/provider-icons/types.ts",
"./icons/file-type": "./src/components/file-icons/types.ts",
"./fonts/*": "./src/assets/fonts/*",
diff --git a/packages/ui/script/generate-preload.ts b/packages/ui/script/generate-preload.ts
new file mode 100644
index 00000000000..180b30564ef
--- /dev/null
+++ b/packages/ui/script/generate-preload.ts
@@ -0,0 +1,16 @@
+/**
+ * Generates the theme preload script content.
+ * Run this to get the script that should be embedded in index.html.
+ */
+
+import { generatePreloadScript, generatePreloadScriptFormatted } from "../src/theme/preload"
+
+const formatted = process.argv.includes("--formatted")
+
+if (formatted) {
+ console.log("")
+} else {
+ console.log("")
+}
diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css
index a8a9e6a31ed..92323876ca2 100644
--- a/packages/ui/src/components/message-part.css
+++ b/packages/ui/src/components/message-part.css
@@ -137,6 +137,9 @@
[data-slot="message-part-tool-error-message"] {
color: var(--text-on-critical-weak);
+ max-height: 240px;
+ overflow-y: auto;
+ word-break: break-word;
}
}
diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css
index 1748feab963..599116d28a8 100644
--- a/packages/ui/src/components/session-turn.css
+++ b/packages/ui/src/components/session-turn.css
@@ -343,6 +343,8 @@
.error-card {
color: var(--text-on-critical-base);
+ max-height: 240px;
+ overflow-y: auto;
}
[data-slot="session-turn-collapsible-content-inner"] {
diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css
index 6584a5e6487..e8de4a06cb5 100644
--- a/packages/ui/src/styles/theme.css
+++ b/packages/ui/src/styles/theme.css
@@ -68,987 +68,8 @@
color-scheme: light;
--text-mix-blend-mode: multiply;
- /* OC-1-light */
- --background-base: #f8f7f7;
- --background-weak: var(--smoke-light-3);
- --background-strong: var(--smoke-light-1);
- --background-stronger: #fcfcfc;
- --surface-base: var(--smoke-light-alpha-2);
- --base: var(--smoke-light-alpha-2);
- --surface-base-hover: #0500000f;
- --surface-base-active: var(--smoke-light-alpha-3);
- --surface-base-interactive-active: var(--cobalt-light-alpha-3);
- --base2: var(--smoke-light-alpha-2);
- --base3: var(--smoke-light-alpha-2);
- --surface-inset-base: var(--smoke-light-alpha-2);
- --surface-inset-base-hover: var(--smoke-light-alpha-3);
- --surface-inset-strong: #1f000017;
- --surface-inset-strong-hover: #1f000017;
- --surface-raised-base: var(--smoke-light-alpha-1);
- --surface-float-base: var(--smoke-dark-1);
- --surface-float-base-hover: var(--smoke-dark-2);
- --surface-raised-base-hover: var(--smoke-light-alpha-2);
- --surface-raised-base-active: var(--smoke-light-alpha-3);
- --surface-raised-strong: var(--smoke-light-1);
- --surface-raised-strong-hover: var(--white);
- --surface-raised-stronger: var(--white);
- --surface-raised-stronger-hover: var(--white);
- --surface-weak: var(--smoke-light-alpha-3);
- --surface-weaker: var(--smoke-light-alpha-4);
- --surface-strong: #ffffff;
- --surface-raised-stronger-non-alpha: var(--white);
- --surface-brand-base: var(--yuzu-light-9);
- --surface-brand-hover: var(--yuzu-light-10);
- --surface-interactive-base: var(--cobalt-light-3);
- --surface-interactive-hover: var(--cobalt-light-4);
- --surface-interactive-weak: var(--cobalt-light-2);
- --surface-interactive-weak-hover: var(--cobalt-light-3);
- --surface-success-base: var(--apple-light-3);
- --surface-success-weak: var(--apple-light-2);
- --surface-success-strong: var(--apple-light-9);
- --surface-warning-base: var(--solaris-light-3);
- --surface-warning-weak: var(--solaris-light-2);
- --surface-warning-strong: var(--solaris-light-9);
- --surface-critical-base: var(--ember-light-3);
- --surface-critical-weak: var(--ember-light-2);
- --surface-critical-strong: var(--ember-light-9);
- --surface-info-base: var(--lilac-light-3);
- --surface-info-weak: var(--lilac-light-2);
- --surface-info-strong: var(--lilac-light-9);
- --surface-diff-unchanged-base: #ffffff00;
- --surface-diff-skip-base: var(--smoke-light-2);
- --surface-diff-hidden-base: var(--blue-light-3);
- --surface-diff-hidden-weak: var(--blue-light-2);
- --surface-diff-hidden-weaker: var(--blue-light-1);
- --surface-diff-hidden-strong: var(--blue-light-5);
- --surface-diff-hidden-stronger: var(--blue-light-9);
- --surface-diff-add-base: #dafbe0;
- --surface-diff-add-weak: var(--mint-light-2);
- --surface-diff-add-weaker: var(--mint-light-1);
- --surface-diff-add-strong: var(--mint-light-5);
- --surface-diff-add-stronger: var(--mint-light-9);
- --surface-diff-delete-base: var(--ember-light-3);
- --surface-diff-delete-weak: var(--ember-light-2);
- --surface-diff-delete-weaker: var(--ember-light-1);
- --surface-diff-delete-strong: var(--ember-light-6);
- --surface-diff-delete-stronger: var(--ember-light-9);
- --input-base: var(--smoke-light-1);
- --input-hover: var(--smoke-light-2);
- --input-active: var(--cobalt-light-1);
- --input-selected: var(--cobalt-light-4);
- --input-focus: var(--cobalt-light-1);
- --input-disabled: var(--smoke-light-4);
- --text-base: var(--smoke-light-11);
- --text-weak: var(--smoke-light-9);
- --text-weaker: var(--smoke-light-8);
- --text-strong: var(--smoke-light-12);
- --text-invert-base: var(--smoke-dark-alpha-11);
- --text-invert-weak: var(--smoke-dark-alpha-9);
- --text-invert-weaker: var(--smoke-dark-alpha-8);
- --text-invert-strong: var(--smoke-dark-alpha-12);
- --text-interactive-base: var(--cobalt-light-9);
- --text-on-brand-base: var(--smoke-light-alpha-11);
- --text-on-interactive-base: var(--smoke-light-1);
- --text-on-interactive-weak: var(--smoke-dark-alpha-11);
- --text-on-success-base: var(--apple-light-10);
- --text-on-critical-base: var(--ember-light-10);
- --text-on-critical-weak: var(--ember-light-8);
- --text-on-critical-strong: var(--ember-light-12);
- --text-on-warning-base: var(--smoke-dark-alpha-11);
- --text-on-info-base: var(--smoke-dark-alpha-11);
- --text-diff-add-base: var(--mint-light-11);
- --text-diff-delete-base: var(--ember-light-10);
- --text-diff-delete-strong: var(--ember-light-12);
- --text-diff-add-strong: var(--mint-light-12);
- --text-on-info-weak: var(--smoke-dark-alpha-9);
- --text-on-info-strong: var(--smoke-dark-alpha-12);
- --text-on-warning-weak: var(--smoke-dark-alpha-9);
- --text-on-warning-strong: var(--smoke-dark-alpha-12);
- --text-on-success-weak: var(--apple-light-6);
- --text-on-success-strong: var(--apple-light-12);
- --text-on-brand-weak: var(--smoke-light-alpha-9);
- --text-on-brand-weaker: var(--smoke-light-alpha-8);
- --text-on-brand-strong: var(--smoke-light-alpha-12);
- --button-secondary-base: #fdfcfc;
- --button-secondary-hover: #faf9f9;
- --border-base: var(--smoke-light-alpha-7);
- --border-hover: var(--smoke-light-alpha-8);
- --border-active: var(--smoke-light-alpha-9);
- --border-selected: var(--cobalt-light-alpha-9);
- --border-disabled: var(--smoke-light-alpha-8);
- --border-focus: var(--smoke-light-alpha-9);
- --border-weak-base: var(--smoke-light-alpha-5);
- --border-strong-base: var(--smoke-light-alpha-7);
- --border-strong-hover: var(--smoke-light-alpha-8);
- --border-strong-active: var(--smoke-light-alpha-7);
- --border-strong-selected: var(--cobalt-light-alpha-6);
- --border-strong-disabled: var(--smoke-light-alpha-6);
- --border-strong-focus: var(--smoke-light-alpha-7);
- --border-weak-hover: var(--smoke-light-alpha-6);
- --border-weak-active: var(--smoke-light-alpha-7);
- --border-weak-selected: var(--cobalt-light-alpha-5);
- --border-weak-disabled: var(--smoke-light-alpha-6);
- --border-weak-focus: var(--smoke-light-alpha-7);
- --border-interactive-base: var(--cobalt-light-7);
- --border-interactive-hover: var(--cobalt-light-8);
- --border-interactive-active: var(--cobalt-light-9);
- --border-interactive-selected: var(--cobalt-light-9);
- --border-interactive-disabled: var(--smoke-light-8);
- --border-interactive-focus: var(--cobalt-light-9);
- --border-success-base: var(--apple-light-6);
- --border-success-hover: var(--apple-light-7);
- --border-success-selected: var(--apple-light-9);
- --border-warning-base: var(--solaris-light-6);
- --border-warning-hover: var(--solaris-light-7);
- --border-warning-selected: var(--solaris-light-9);
- --border-critical-base: var(--ember-light-6);
- --border-critical-hover: var(--ember-light-7);
- --border-critical-selected: var(--ember-light-9);
- --border-info-base: var(--lilac-light-6);
- --border-info-hover: var(--lilac-light-7);
- --border-info-selected: var(--lilac-light-9);
- --icon-base: var(--smoke-light-9);
- --icon-hover: var(--smoke-light-11);
- --icon-active: var(--smoke-light-12);
- --icon-selected: var(--smoke-light-12);
- --icon-disabled: var(--smoke-light-8);
- --icon-focus: var(--smoke-light-12);
- --icon-invert-base: #ffffff;
- --icon-weak-base: var(--smoke-light-7);
- --icon-weak-hover: var(--smoke-light-8);
- --icon-weak-active: var(--smoke-light-9);
- --icon-weak-selected: var(--smoke-light-10);
- --icon-weak-disabled: var(--smoke-light-6);
- --icon-weak-focus: var(--smoke-light-9);
- --icon-strong-base: var(--smoke-light-12);
- --icon-strong-hover: #151313;
- --icon-strong-active: #020202;
- --icon-strong-selected: #020202;
- --icon-strong-disabled: var(--smoke-light-8);
- --icon-strong-focus: #020202;
- --icon-brand-base: var(--smoke-light-12);
- --icon-interactive-base: var(--cobalt-light-9);
- --icon-success-base: var(--apple-light-7);
- --icon-success-hover: var(--apple-light-8);
- --icon-success-active: var(--apple-light-11);
- --icon-warning-base: var(--amber-light-7);
- --icon-warning-hover: var(--amber-light-8);
- --icon-warning-active: var(--amber-light-11);
- --icon-critical-base: var(--ember-light-10);
- --icon-critical-hover: var(--ember-light-11);
- --icon-critical-active: var(--ember-light-12);
- --icon-info-base: var(--lilac-light-7);
- --icon-info-hover: var(--lilac-light-8);
- --icon-info-active: var(--lilac-light-11);
- --icon-on-brand-base: var(--smoke-light-alpha-11);
- --icon-on-brand-hover: var(--smoke-light-alpha-12);
- --icon-on-brand-selected: var(--smoke-light-alpha-12);
- --icon-on-interactive-base: var(--smoke-light-1);
- --icon-agent-plan-base: var(--purple-light-9);
- --icon-agent-docs-base: var(--amber-light-9);
- --icon-agent-ask-base: var(--cyan-light-9);
- --icon-agent-build-base: var(--cobalt-light-9);
- --icon-on-success-base: var(--apple-light-alpha-9);
- --icon-on-success-hover: var(--apple-light-alpha-10);
- --icon-on-success-selected: var(--apple-light-alpha-11);
- --icon-on-warning-base: var(--amber-lightalpha-9);
- --icon-on-warning-hover: var(--amber-lightalpha-10);
- --icon-on-warning-selected: var(--amber-lightalpha-11);
- --icon-on-critical-base: var(--ember-light-alpha-9);
- --icon-on-critical-hover: var(--ember-light-alpha-10);
- --icon-on-critical-selected: var(--ember-light-alpha-11);
- --icon-on-info-base: var(--lilac-light-9);
- --icon-on-info-hover: var(--lilac-light-alpha-10);
- --icon-on-info-selected: var(--lilac-light-alpha-11);
- --icon-diff-add-base: var(--mint-light-11);
- --icon-diff-add-hover: var(--mint-light-12);
- --icon-diff-add-active: var(--mint-light-12);
- --icon-diff-delete-base: var(--ember-light-10);
- --icon-diff-delete-hover: var(--ember-light-11);
- --syntax-comment: var(--text-weak);
- --syntax-regexp: var(--text-base);
- --syntax-string: #006656;
- --syntax-keyword: var(--text-weak);
- --syntax-primitive: #fb4804;
- --syntax-operator: var(--text-base);
- --syntax-variable: var(--text-strong);
- --syntax-property: #ed6dc8;
- --syntax-type: #596600;
- --syntax-constant: #007b80;
- --syntax-punctuation: var(--text-base);
- --syntax-object: var(--text-strong);
- --syntax-success: var(--apple-light-10);
- --syntax-warning: var(--amber-light-10);
- --syntax-critical: var(--ember-light-10);
- --syntax-info: #0092a8;
- --syntax-diff-add: var(--mint-light-11);
- --syntax-diff-delete: var(--ember-light-11);
- --syntax-diff-unknown: #ff0000;
- --markdown-heading: #d68c27;
- --markdown-text: #1a1a1a;
- --markdown-link: #3b7dd8;
- --markdown-link-text: #318795;
- --markdown-code: #3d9a57;
- --markdown-block-quote: #b0851f;
- --markdown-emph: #b0851f;
- --markdown-strong: #d68c27;
- --markdown-horizontal-rule: #8a8a8a;
- --markdown-list-item: #3b7dd8;
- --markdown-list-enumeration: #318795;
- --markdown-image: #3b7dd8;
- --markdown-image-text: #318795;
- --markdown-code-block: #1a1a1a;
- --border-color: #ffffff;
- --border-weaker-base: var(--smoke-light-alpha-3);
- --border-weaker-hover: var(--smoke-light-alpha-4);
- --border-weaker-active: var(--smoke-light-alpha-6);
- --border-weaker-selected: var(--cobalt-light-alpha-4);
- --border-weaker-disabled: var(--smoke-light-alpha-2);
- --border-weaker-focus: var(--smoke-light-alpha-6);
- --button-ghost-hover: var(--smoke-light-alpha-2);
- --button-ghost-hover2: var(--smoke-light-alpha-3);
- --avatar-background-pink: #feeef8;
- --avatar-background-mint: #e1fbf4;
- --avatar-background-orange: #fff1e7;
- --avatar-background-purple: #f9f1fe;
- --avatar-background-cyan: #e7f9fb;
- --avatar-background-lime: #eefadc;
- --avatar-text-pink: #cd1d8d;
- --avatar-text-mint: #147d6f;
- --avatar-text-orange: #ed5f00;
- --avatar-text-purple: #8445bc;
- --avatar-text-cyan: #0894b3;
- --avatar-text-lime: #5d770d;
-
@media (prefers-color-scheme: dark) {
color-scheme: dark;
--text-mix-blend-mode: plus-lighter;
-
- /* OC-1-dark */
- --background-base: var(--smoke-dark-1);
- --background-weak: #1c1717;
- --background-strong: #151313;
- --background-stronger: #191515;
- --surface-base: var(--smoke-dark-alpha-2);
- --base: var(--smoke-dark-alpha-2);
- --surface-base-hover: #e0b7b716;
- --surface-base-active: var(--smoke-dark-alpha-3);
- --surface-base-interactive-active: var(--cobalt-dark-alpha-2);
- --base2: var(--smoke-dark-alpha-2);
- --base3: var(--smoke-dark-alpha-2);
- --surface-inset-base: #0e0b0b7f;
- --surface-inset-base-hover: #0e0b0b7f;
- --surface-inset-strong: #060505cc;
- --surface-inset-strong-hover: #060505cc;
- --surface-raised-base: var(--smoke-dark-alpha-3);
- --surface-float-base: var(--smoke-dark-1);
- --surface-float-base-hover: var(--smoke-dark-2);
- --surface-raised-base-hover: var(--smoke-dark-alpha-4);
- --surface-raised-base-active: var(--smoke-dark-alpha-5);
- --surface-raised-strong: var(--smoke-dark-alpha-4);
- --surface-raised-strong-hover: var(--smoke-dark-alpha-6);
- --surface-raised-stronger: var(--smoke-dark-alpha-6);
- --surface-raised-stronger-hover: var(--smoke-dark-alpha-7);
- --surface-weak: var(--smoke-dark-alpha-4);
- --surface-weaker: var(--smoke-dark-alpha-5);
- --surface-strong: var(--smoke-dark-alpha-7);
- --surface-raised-stronger-non-alpha: var(--smoke-dark-3);
- --surface-brand-base: var(--yuzu-light-9);
- --surface-brand-hover: var(--yuzu-light-10);
- --surface-interactive-base: var(--cobalt-light-3);
- --surface-interactive-hover: var(--cobalt-light-4);
- --surface-interactive-weak: var(--cobalt-light-2);
- --surface-interactive-weak-hover: var(--cobalt-light-3);
- --surface-success-base: var(--apple-light-3);
- --surface-success-weak: var(--apple-light-2);
- --surface-success-strong: var(--apple-light-9);
- --surface-warning-base: var(--solaris-light-3);
- --surface-warning-weak: var(--solaris-light-2);
- --surface-warning-strong: var(--solaris-light-9);
- --surface-critical-base: var(--ember-dark-3);
- --surface-critical-weak: var(--ember-dark-2);
- --surface-critical-strong: var(--ember-dark-9);
- --surface-info-base: var(--lilac-light-3);
- --surface-info-weak: var(--lilac-light-2);
- --surface-info-strong: var(--lilac-light-9);
- --surface-diff-unchanged-base: var(--smoke-dark-1);
- --surface-diff-skip-base: var(--smoke-dark-alpha-1);
- --surface-diff-hidden-base: var(--blue-dark-2);
- --surface-diff-hidden-weak: var(--blue-dark-1);
- --surface-diff-hidden-weaker: var(--blue-dark-3);
- --surface-diff-hidden-strong: var(--blue-dark-5);
- --surface-diff-hidden-stronger: var(--blue-dark-11);
- --surface-diff-add-base: var(--mint-dark-3);
- --surface-diff-add-weak: var(--mint-dark-4);
- --surface-diff-add-weaker: var(--mint-dark-3);
- --surface-diff-add-strong: var(--mint-dark-5);
- --surface-diff-add-stronger: var(--mint-dark-11);
- --surface-diff-delete-base: var(--ember-dark-3);
- --surface-diff-delete-weak: var(--ember-dark-4);
- --surface-diff-delete-weaker: var(--ember-dark-3);
- --surface-diff-delete-strong: var(--ember-dark-5);
- --surface-diff-delete-stronger: var(--ember-dark-11);
- --input-base: var(--smoke-dark-2);
- --input-hover: var(--smoke-dark-2);
- --input-active: var(--cobalt-dark-1);
- --input-selected: var(--cobalt-dark-2);
- --input-focus: var(--cobalt-dark-1);
- --input-disabled: var(--smoke-dark-4);
- --text-base: var(--smoke-dark-alpha-11);
- --text-weak: var(--smoke-dark-alpha-9);
- --text-weaker: var(--smoke-dark-alpha-8);
- --text-strong: var(--smoke-dark-alpha-12);
- --text-invert-base: var(--smoke-dark-alpha-11);
- --text-invert-weak: var(--smoke-dark-alpha-9);
- --text-invert-weaker: var(--smoke-dark-alpha-8);
- --text-invert-strong: var(--smoke-dark-alpha-12);
- --text-interactive-base: var(--cobalt-dark-11);
- --text-on-brand-base: var(--smoke-dark-alpha-11);
- --text-on-interactive-base: var(--smoke-dark-12);
- --text-on-interactive-weak: var(--smoke-dark-alpha-11);
- --text-on-success-base: var(--apple-dark-9);
- --text-on-critical-base: var(--ember-dark-9);
- --text-on-critical-weak: var(--ember-dark-8);
- --text-on-critical-strong: var(--ember-dark-12);
- --text-on-warning-base: var(--smoke-dark-alpha-11);
- --text-on-info-base: var(--smoke-dark-alpha-11);
- --text-diff-add-base: var(--mint-dark-11);
- --text-diff-delete-base: var(--ember-dark-9);
- --text-diff-delete-strong: var(--ember-dark-12);
- --text-diff-add-strong: var(--mint-dark-8);
- --text-on-info-weak: var(--smoke-dark-alpha-9);
- --text-on-info-strong: var(--smoke-dark-alpha-12);
- --text-on-warning-weak: var(--smoke-dark-alpha-9);
- --text-on-warning-strong: var(--smoke-dark-alpha-12);
- --text-on-success-weak: var(--apple-dark-8);
- --text-on-success-strong: var(--apple-dark-12);
- --text-on-brand-weak: var(--smoke-dark-alpha-9);
- --text-on-brand-weaker: var(--smoke-dark-alpha-8);
- --text-on-brand-strong: var(--smoke-dark-alpha-12);
- --button-secondary-base: #231f1f;
- --button-secondary-hover: #2a2727;
- --border-base: var(--smoke-dark-alpha-7);
- --border-hover: var(--smoke-dark-alpha-8);
- --border-active: var(--smoke-dark-alpha-9);
- --border-selected: var(--cobalt-dark-alpha-11);
- --border-disabled: var(--smoke-dark-alpha-8);
- --border-focus: var(--smoke-dark-alpha-9);
- --border-weak-base: var(--smoke-dark-alpha-6);
- --border-strong-base: var(--smoke-dark-alpha-8);
- --border-strong-hover: var(--smoke-dark-alpha-7);
- --border-strong-active: var(--smoke-dark-alpha-8);
- --border-strong-selected: var(--cobalt-dark-alpha-6);
- --border-strong-disabled: var(--smoke-dark-alpha-6);
- --border-strong-focus: var(--smoke-dark-alpha-8);
- --border-weak-hover: var(--smoke-dark-alpha-7);
- --border-weak-active: var(--smoke-dark-alpha-8);
- --border-weak-selected: var(--cobalt-dark-alpha-6);
- --border-weak-disabled: var(--smoke-dark-alpha-6);
- --border-weak-focus: var(--smoke-dark-alpha-8);
- --border-interactive-base: var(--cobalt-light-7);
- --border-interactive-hover: var(--cobalt-light-8);
- --border-interactive-active: var(--cobalt-light-9);
- --border-interactive-selected: var(--cobalt-light-9);
- --border-interactive-disabled: var(--smoke-light-8);
- --border-interactive-focus: var(--cobalt-light-9);
- --border-success-base: var(--apple-light-6);
- --border-success-hover: var(--apple-light-7);
- --border-success-selected: var(--apple-light-9);
- --border-warning-base: var(--solaris-light-6);
- --border-warning-hover: var(--solaris-light-7);
- --border-warning-selected: var(--solaris-light-9);
- --border-critical-base: var(--ember-dark-5);
- --border-critical-hover: var(--ember-dark-7);
- --border-critical-selected: var(--ember-dark-9);
- --border-info-base: var(--lilac-light-6);
- --border-info-hover: var(--lilac-light-7);
- --border-info-selected: var(--lilac-light-9);
- --icon-base: var(--smoke-dark-9);
- --icon-hover: var(--smoke-dark-10);
- --icon-active: var(--smoke-dark-11);
- --icon-selected: var(--smoke-dark-12);
- --icon-disabled: var(--smoke-dark-7);
- --icon-focus: var(--smoke-dark-12);
- --icon-invert-base: var(--smoke-dark-1);
- --icon-weak-base: var(--smoke-dark-6);
- --icon-weak-hover: var(--smoke-light-7);
- --icon-weak-active: var(--smoke-light-8);
- --icon-weak-selected: var(--smoke-light-9);
- --icon-weak-disabled: var(--smoke-light-4);
- --icon-weak-focus: var(--smoke-light-9);
- --icon-strong-base: var(--smoke-dark-12);
- --icon-strong-hover: #f6f3f3;
- --icon-strong-active: #fcfcfc;
- --icon-strong-selected: #fdfcfc;
- --icon-strong-disabled: var(--smoke-dark-8);
- --icon-strong-focus: #fdfcfc;
- --icon-brand-base: var(--white);
- --icon-interactive-base: var(--cobalt-dark-9);
- --icon-success-base: var(--apple-dark-7);
- --icon-success-hover: var(--apple-dark-8);
- --icon-success-active: var(--apple-dark-11);
- --icon-warning-base: var(--amber-dark-7);
- --icon-warning-hover: var(--amber-dark-8);
- --icon-warning-active: var(--amber-dark-11);
- --icon-critical-base: var(--ember-dark-9);
- --icon-critical-hover: var(--ember-dark-11);
- --icon-critical-active: var(--ember-dark-12);
- --icon-info-base: var(--lilac-dark-7);
- --icon-info-hover: var(--lilac-dark-8);
- --icon-info-active: var(--lilac-dark-11);
- --icon-on-brand-base: var(--smoke-light-alpha-11);
- --icon-on-brand-hover: var(--smoke-light-alpha-12);
- --icon-on-brand-selected: var(--smoke-light-alpha-12);
- --icon-on-interactive-base: var(--smoke-dark-12);
- --icon-agent-plan-base: var(--purple-dark-9);
- --icon-agent-docs-base: var(--amber-dark-9);
- --icon-agent-ask-base: var(--cyan-dark-9);
- --icon-agent-build-base: var(--cobalt-dark-11);
- --icon-on-success-base: var(--apple-dark-alpha-9);
- --icon-on-success-hover: var(--apple-dark-alpha-10);
- --icon-on-success-selected: var(--apple-dark-alpha-11);
- --icon-on-warning-base: var(--amber-darkalpha-9);
- --icon-on-warning-hover: var(--amber-darkalpha-10);
- --icon-on-warning-selected: var(--amber-darkalpha-11);
- --icon-on-critical-base: var(--ember-dark-alpha-9);
- --icon-on-critical-hover: var(--ember-dark-alpha-10);
- --icon-on-critical-selected: var(--ember-dark-alpha-11);
- --icon-on-info-base: var(--lilac-dark-9);
- --icon-on-info-hover: var(--lilac-dark-alpha-10);
- --icon-on-info-selected: var(--lilac-dark-alpha-11);
- --icon-diff-add-base: var(--mint-dark-11);
- --icon-diff-add-hover: var(--mint-dark-10);
- --icon-diff-add-active: var(--mint-dark-11);
- --icon-diff-delete-base: var(--ember-dark-9);
- --icon-diff-delete-hover: var(--ember-dark-10);
- --syntax-comment: var(--text-weak);
- --syntax-regexp: var(--text-base);
- --syntax-string: #00ceb9;
- --syntax-keyword: var(--text-weak);
- --syntax-primitive: #ffba92;
- --syntax-operator: var(--text-weak);
- --syntax-variable: var(--text-strong);
- --syntax-property: #ff9ae2;
- --syntax-type: #ecf58c;
- --syntax-constant: #93e9f6;
- --syntax-punctuation: var(--text-weak);
- --syntax-object: var(--text-strong);
- --syntax-success: var(--apple-dark-10);
- --syntax-warning: var(--amber-dark-10);
- --syntax-critical: var(--ember-dark-10);
- --syntax-info: #93e9f6;
- --syntax-diff-add: var(--mint-dark-11);
- --syntax-diff-delete: var(--ember-dark-11);
- --syntax-diff-unknown: #ff0000;
- --markdown-heading: #9d7cd8;
- --markdown-text: #eeeeee;
- --markdown-link: #fab283;
- --markdown-link-text: #56b6c2;
- --markdown-code: #7fd88f;
- --markdown-block-quote: #e5c07b;
- --markdown-emph: #e5c07b;
- --markdown-strong: #f5a742;
- --markdown-horizontal-rule: #808080;
- --markdown-list-item: #fab283;
- --markdown-list-enumeration: #56b6c2;
- --markdown-image: #fab283;
- --markdown-image-text: #56b6c2;
- --markdown-code-block: #eeeeee;
- --border-color: #ffffff;
- --border-weaker-base: var(--smoke-dark-alpha-3);
- --border-weaker-hover: var(--smoke-dark-alpha-4);
- --border-weaker-active: var(--smoke-dark-alpha-6);
- --border-weaker-selected: var(--cobalt-dark-alpha-3);
- --border-weaker-disabled: var(--smoke-dark-alpha-2);
- --border-weaker-focus: var(--smoke-dark-alpha-6);
- --button-ghost-hover: var(--smoke-dark-alpha-2);
- --button-ghost-hover2: var(--smoke-dark-alpha-3);
- --avatar-background-pink: #501b3f;
- --avatar-background-mint: #033a34;
- --avatar-background-orange: #5f2a06;
- --avatar-background-purple: #432155;
- --avatar-background-cyan: #0f3058;
- --avatar-background-lime: #2b3711;
- --avatar-text-pink: #e34ba9;
- --avatar-text-mint: #95f3d9;
- --avatar-text-orange: #ff802b;
- --avatar-text-purple: #9d5bd2;
- --avatar-text-cyan: #369eff;
- --avatar-text-lime: #c4f042;
- }
-}
-
-html[data-theme="oc-2-paper"] {
- /* OC-2-paper */
- --background-base: #f7f8f8;
- --background-weak: var(--ink-light-3);
- --background-strong: var(--ink-light-1);
- --background-stronger: #fcfcfc;
- --surface-base: var(--ink-light-alpha-2);
- --base: var(--ink-light-alpha-2);
- --surface-base-hover: #0005050f;
- --surface-base-active: var(--ink-light-alpha-3);
- --surface-base-interactive-active: var(--cobalt-light-alpha-3);
- --base2: var(--ink-light-alpha-2);
- --base3: var(--ink-light-alpha-2);
- --surface-inset-base: var(--ink-light-alpha-2);
- --surface-inset-base-hover: var(--ink-light-alpha-3);
- --surface-inset-strong: #001f1f17;
- --surface-inset-strong-hover: #001f1f17;
- --surface-raised-base: var(--ink-light-alpha-1);
- --surface-float-base: var(--ink-dark-1);
- --surface-float-base-hover: var(--ink-dark-2);
- --surface-raised-base-hover: var(--ink-light-alpha-2);
- --surface-raised-base-active: var(--ink-light-alpha-3);
- --surface-raised-strong: var(--ink-light-1);
- --surface-raised-strong-hover: var(--white);
- --surface-raised-stronger: var(--white);
- --surface-raised-stronger-hover: var(--white);
- --surface-weak: var(--ink-light-alpha-3);
- --surface-weaker: var(--ink-light-alpha-4);
- --surface-strong: #ffffff;
- --surface-raised-stronger-non-alpha: var(--white);
- --surface-brand-base: var(--yuzu-light-9);
- --surface-brand-hover: var(--yuzu-light-10);
- --surface-interactive-base: var(--cobalt-light-3);
- --surface-interactive-hover: var(--cobalt-light-4);
- --surface-interactive-weak: var(--cobalt-light-2);
- --surface-interactive-weak-hover: var(--cobalt-light-3);
- --surface-success-base: var(--apple-light-3);
- --surface-success-weak: var(--apple-light-2);
- --surface-success-strong: var(--apple-light-9);
- --surface-warning-base: var(--solaris-light-3);
- --surface-warning-weak: var(--solaris-light-2);
- --surface-warning-strong: var(--solaris-light-9);
- --surface-critical-base: var(--ember-light-3);
- --surface-critical-weak: var(--ember-light-2);
- --surface-critical-strong: var(--ember-light-9);
- --surface-info-base: var(--lilac-light-3);
- --surface-info-weak: var(--lilac-light-2);
- --surface-info-strong: var(--lilac-light-9);
- --surface-diff-unchanged-base: #ffffff00;
- --surface-diff-skip-base: var(--ink-light-2);
- --surface-diff-hidden-base: var(--blue-light-3);
- --surface-diff-hidden-weak: var(--blue-light-2);
- --surface-diff-hidden-weaker: var(--blue-light-1);
- --surface-diff-hidden-strong: var(--blue-light-5);
- --surface-diff-hidden-stronger: var(--blue-light-9);
- --surface-diff-add-base: var(--mint-light-3);
- --surface-diff-add-weak: var(--mint-light-2);
- --surface-diff-add-weaker: var(--mint-light-1);
- --surface-diff-add-strong: var(--mint-light-5);
- --surface-diff-add-stronger: var(--mint-light-9);
- --surface-diff-delete-base: var(--ember-light-3);
- --surface-diff-delete-weak: var(--ember-light-2);
- --surface-diff-delete-weaker: var(--ember-light-1);
- --surface-diff-delete-strong: var(--ember-light-6);
- --surface-diff-delete-stronger: var(--ember-light-9);
- --text-base: var(--ink-light-11);
- --input-base: var(--ink-light-1);
- --input-hover: var(--ink-light-2);
- --input-active: var(--cobalt-light-1);
- --input-selected: var(--cobalt-light-4);
- --input-focus: var(--cobalt-light-1);
- --input-disabled: var(--ink-light-4);
- --text-weak: var(--ink-light-9);
- --text-weaker: var(--ink-light-8);
- --text-strong: var(--ink-light-12);
- --text-interactive-base: var(--cobalt-light-9);
- --text-on-brand-base: var(--ink-light-alpha-11);
- --text-on-interactive-base: var(--ink-light-1);
- --text-on-interactive-weak: var(--ink-light-alpha-11);
- --text-on-success-base: var(--apple-light-10);
- --text-on-critical-base: var(--ember-light-10);
- --text-on-critical-weak: var(--ember-light-8);
- --text-on-critical-strong: var(--ember-light-12);
- --text-on-warning-base: var(--ink-dark-alpha-11);
- --text-on-info-base: var(--ink-dark-alpha-11);
- --text-diff-add-base: var(--mint-light-11);
- --text-diff-delete-base: var(--ember-light-10);
- --text-diff-delete-strong: var(--ember-light-12);
- --text-diff-add-strong: var(--mint-light-12);
- --text-on-info-weak: var(--ink-dark-alpha-9);
- --text-on-info-strong: var(--ink-dark-alpha-12);
- --text-on-warning-weak: var(--ink-dark-alpha-9);
- --text-on-warning-strong: var(--ink-dark-alpha-12);
- --text-on-success-weak: var(--apple-light-6);
- --text-on-success-strong: var(--apple-light-12);
- --text-on-brand-weak: var(--ink-light-alpha-9);
- --text-on-brand-weaker: var(--ink-light-alpha-8);
- --text-on-brand-strong: var(--ink-light-alpha-12);
- --button-secondary-base: #fcfdfd;
- --button-secondary-hover: #f9fafa;
- --border-base: var(--ink-light-alpha-7);
- --border-hover: var(--ink-light-alpha-8);
- --border-active: var(--ink-light-alpha-9);
- --border-selected: var(--cobalt-light-alpha-9);
- --border-disabled: var(--ink-light-alpha-8);
- --border-focus: var(--ink-light-alpha-9);
- --border-weak-base: var(--ink-light-alpha-5);
- --border-strong-base: var(--ink-light-alpha-7);
- --border-strong-hover: var(--ink-light-alpha-8);
- --border-strong-active: var(--ink-light-alpha-7);
- --border-strong-selected: var(--cobalt-light-alpha-6);
- --border-strong-disabled: var(--ink-light-alpha-6);
- --border-strong-focus: var(--ink-light-alpha-7);
- --border-weak-hover: var(--ink-light-alpha-6);
- --border-weak-active: var(--ink-light-alpha-7);
- --border-weak-selected: var(--cobalt-light-alpha-5);
- --border-weak-disabled: var(--ink-light-alpha-6);
- --border-weak-focus: var(--ink-light-alpha-7);
- --border-interactive-base: var(--cobalt-light-7);
- --border-interactive-hover: var(--cobalt-light-8);
- --border-interactive-active: var(--cobalt-light-9);
- --border-interactive-selected: var(--cobalt-light-9);
- --border-interactive-disabled: var(--ink-light-8);
- --border-interactive-focus: var(--cobalt-light-9);
- --border-success-base: var(--apple-light-6);
- --border-success-hover: var(--apple-light-7);
- --border-success-selected: var(--apple-light-9);
- --border-warning-base: var(--solaris-light-6);
- --border-warning-hover: var(--solaris-light-7);
- --border-warning-selected: var(--solaris-light-9);
- --border-critical-base: var(--ember-light-6);
- --border-critical-hover: var(--ember-light-7);
- --border-critical-selected: var(--ember-light-9);
- --border-info-base: var(--lilac-light-6);
- --border-info-hover: var(--lilac-light-7);
- --border-info-selected: var(--lilac-light-9);
- --icon-base: var(--ink-light-9);
- --icon-hover: var(--ink-light-11);
- --icon-active: var(--ink-light-12);
- --icon-selected: var(--ink-light-12);
- --icon-disabled: var(--ink-light-8);
- --icon-focus: var(--ink-light-12);
- --icon-invert-base: #ffffff;
- --icon-weak-base: var(--ink-light-7);
- --icon-weak-hover: var(--ink-light-8);
- --icon-weak-active: var(--ink-light-9);
- --icon-weak-selected: var(--ink-light-10);
- --icon-weak-disabled: var(--ink-light-6);
- --icon-weak-focus: var(--ink-light-9);
- --icon-strong-base: var(--ink-light-12);
- --icon-strong-hover: #131515;
- --icon-strong-active: #020202;
- --icon-strong-selected: #020202;
- --icon-strong-disabled: var(--ink-light-8);
- --icon-strong-focus: #020202;
- --icon-brand-base: var(--ink-light-12);
- --icon-interactive-base: var(--cobalt-light-9);
- --icon-success-base: var(--apple-light-7);
- --icon-success-hover: var(--apple-light-8);
- --icon-success-active: var(--apple-light-11);
- --icon-warning-base: var(--amber-light-7);
- --icon-warning-hover: var(--amber-light-8);
- --icon-warning-active: var(--amber-light-11);
- --icon-critical-base: var(--ember-light-10);
- --icon-critical-hover: var(--ember-light-11);
- --icon-critical-active: var(--ember-light-12);
- --icon-info-base: var(--lilac-light-7);
- --icon-info-hover: var(--lilac-light-8);
- --icon-info-active: var(--lilac-light-11);
- --icon-on-brand-base: var(--ink-light-alpha-11);
- --icon-on-brand-hover: var(--ink-light-alpha-12);
- --icon-on-brand-selected: var(--ink-light-alpha-12);
- --icon-on-interactive-base: var(--ink-light-1);
- --icon-agent-plan-base: var(--purple-light-9);
- --icon-agent-docs-base: var(--amber-light-9);
- --icon-agent-ask-base: var(--cyan-light-9);
- --icon-agent-build-base: var(--cobalt-light-9);
- --icon-on-success-base: var(--apple-light-alpha-9);
- --icon-on-success-hover: var(--apple-light-alpha-10);
- --icon-on-success-selected: var(--apple-light-alpha-11);
- --icon-on-warning-base: var(--amber-lightalpha-9);
- --icon-on-warning-hover: var(--amber-lightalpha-10);
- --icon-on-warning-selected: var(--amber-lightalpha-11);
- --icon-on-critical-base: var(--ember-light-alpha-9);
- --icon-on-critical-hover: var(--ember-light-alpha-10);
- --icon-on-critical-selected: var(--ember-light-alpha-11);
- --icon-on-info-base: var(--lilac-light-9);
- --icon-on-info-hover: var(--lilac-light-alpha-10);
- --icon-on-info-selected: var(--lilac-light-alpha-11);
- --icon-diff-add-base: var(--mint-light-11);
- --icon-diff-add-hover: var(--mint-light-12);
- --icon-diff-add-active: var(--mint-light-12);
- --icon-diff-delete-base: var(--ember-light-10);
- --icon-diff-delete-hover: var(--ember-light-11);
- --syntax-comment: var(--text-weak);
- --syntax-regexp: var(--text-base);
- --syntax-string: #007663;
- --syntax-keyword: var(--text-weak);
- --syntax-primitive: #fb7f51;
- --syntax-operator: var(--text-weak);
- --syntax-variable: var(--text-strong);
- --syntax-property: #ec6cc8;
- --syntax-type: #738400;
- --syntax-constant: #00b2b9;
- --syntax-punctuation: var(--text-weak);
- --syntax-object: var(--text-strong);
- --syntax-success: var(--apple-light-10);
- --syntax-warning: var(--amber-light-10);
- --syntax-critical: var(--ember-light-9);
- --syntax-info: #0091a7;
- --syntax-diff-add: var(--mint-light-11);
- --syntax-diff-delete: var(--ember-light-11);
- --syntax-diff-unknown: #ff0000;
- --markdown-heading: #d68c27;
- --markdown-text: #1a1a1a;
- --markdown-link: #3b7dd8;
- --markdown-link-text: #318795;
- --markdown-code: #3d9a57;
- --markdown-block-quote: #b0851f;
- --markdown-emph: #b0851f;
- --markdown-strong: #d68c27;
- --markdown-horizontal-rule: #8a8a8a;
- --markdown-list-item: #3b7dd8;
- --markdown-list-enumeration: #318795;
- --markdown-image: #3b7dd8;
- --markdown-image-text: #318795;
- --markdown-code-block: #1a1a1a;
- --border-color: #ffffff;
- --border-weaker-base: var(--ink-light-alpha-3);
- --border-weaker-hover: var(--ink-light-alpha-4);
- --border-weaker-active: var(--ink-light-alpha-6);
- --border-weaker-selected: var(--cobalt-light-alpha-4);
- --border-weaker-disabled: var(--ink-light-alpha-2);
- --border-weaker-focus: var(--ink-light-alpha-6);
- --button-ghost-hover: var(--ink-light-alpha-2);
- --button-ghost-hover2: var(--ink-light-alpha-3);
-
- @media (prefers-color-scheme: dark) {
- --background-base: var(--ink-dark-1);
- --background-weak: #171c1c;
- --background-strong: #131515;
- --background-stronger: #151919;
- --surface-base: var(--ink-dark-alpha-2);
- --base: var(--ink-dark-alpha-2);
- --surface-base-hover: #b8e0e00f;
- --surface-base-active: var(--ink-dark-alpha-3);
- --surface-base-interactive-active: var(--cobalt-light-alpha-3);
- --base2: var(--ink-dark-alpha-2);
- --base3: var(--ink-dark-alpha-2);
- --surface-inset-base: #0b0e0e7f;
- --surface-inset-base-hover: #0b0e0e7f;
- --surface-inset-strong: #050606cc;
- --surface-inset-strong-hover: #050606cc;
- --surface-raised-base: var(--ink-light-alpha-1);
- --surface-float-base: var(--ink-dark-1);
- --surface-float-base-hover: var(--ink-dark-2);
- --surface-raised-base-hover: var(--ink-light-alpha-2);
- --surface-raised-base-active: var(--ink-light-alpha-3);
- --surface-raised-strong: var(--ink-light-1);
- --surface-raised-strong-hover: var(--white);
- --surface-raised-stronger: var(--white);
- --surface-raised-stronger-hover: var(--white);
- --surface-weak: var(--ink-dark-alpha-4);
- --surface-weaker: var(--ink-dark-alpha-5);
- --surface-strong: var(--ink-dark-alpha-7);
- --surface-raised-stronger-non-alpha: var(--white);
- --surface-brand-base: var(--yuzu-light-9);
- --surface-brand-hover: var(--yuzu-light-10);
- --surface-interactive-base: var(--cobalt-light-3);
- --surface-interactive-hover: var(--cobalt-light-4);
- --surface-interactive-weak: var(--cobalt-light-2);
- --surface-interactive-weak-hover: var(--cobalt-light-3);
- --surface-success-base: var(--apple-light-3);
- --surface-success-weak: var(--apple-light-2);
- --surface-success-strong: var(--apple-light-9);
- --surface-warning-base: var(--solaris-light-3);
- --surface-warning-weak: var(--solaris-light-2);
- --surface-warning-strong: var(--solaris-light-9);
- --surface-critical-base: var(--ember-light-3);
- --surface-critical-weak: var(--ember-light-2);
- --surface-critical-strong: var(--ember-light-9);
- --surface-info-base: var(--lilac-light-3);
- --surface-info-weak: var(--lilac-light-2);
- --surface-info-strong: var(--lilac-light-9);
- --surface-diff-unchanged-base: #ffffff00;
- --surface-diff-skip-base: var(--ink-light-2);
- --surface-diff-hidden-base: var(--blue-light-3);
- --surface-diff-hidden-weak: var(--blue-light-2);
- --surface-diff-hidden-weaker: var(--blue-light-1);
- --surface-diff-hidden-strong: var(--blue-light-5);
- --surface-diff-hidden-stronger: var(--blue-light-9);
- --surface-diff-add-base: var(--mint-light-3);
- --surface-diff-add-weak: var(--mint-light-2);
- --surface-diff-add-weaker: var(--mint-light-1);
- --surface-diff-add-strong: var(--mint-light-5);
- --surface-diff-add-stronger: var(--mint-light-9);
- --surface-diff-delete-base: var(--ember-light-3);
- --surface-diff-delete-weak: var(--ember-light-2);
- --surface-diff-delete-weaker: var(--ember-light-1);
- --surface-diff-delete-strong: var(--ember-light-6);
- --surface-diff-delete-stronger: var(--ember-light-9);
- --text-base: var(--ink-light-11);
- --input-base: var(--ink-light-1);
- --input-hover: var(--ink-light-2);
- --input-active: var(--cobalt-light-1);
- --input-selected: var(--cobalt-light-4);
- --input-focus: var(--cobalt-light-1);
- --input-disabled: var(--ink-light-4);
- --text-weak: var(--ink-light-9);
- --text-weaker: var(--ink-light-8);
- --text-strong: var(--ink-light-12);
- --text-interactive-base: var(--cobalt-light-9);
- --text-on-brand-base: var(--ink-light-alpha-11);
- --text-on-interactive-base: var(--ink-light-1);
- --text-on-interactive-weak: var(--ink-light-alpha-11);
- --text-on-success-base: var(--apple-light-10);
- --text-on-critical-base: var(--ember-light-10);
- --text-on-critical-weak: var(--ember-light-8);
- --text-on-critical-strong: var(--ember-light-12);
- --text-on-warning-base: var(--ink-dark-alpha-11);
- --text-on-info-base: var(--ink-dark-alpha-11);
- --text-diff-add-base: var(--mint-light-11);
- --text-diff-delete-base: var(--ember-light-10);
- --text-diff-delete-strong: var(--ember-light-12);
- --text-diff-add-strong: var(--mint-light-12);
- --text-on-info-weak: var(--ink-dark-alpha-9);
- --text-on-info-strong: var(--ink-dark-alpha-12);
- --text-on-warning-weak: var(--ink-dark-alpha-9);
- --text-on-warning-strong: var(--ink-dark-alpha-12);
- --text-on-success-weak: var(--apple-light-6);
- --text-on-success-strong: var(--apple-light-12);
- --text-on-brand-weak: var(--ink-light-alpha-9);
- --text-on-brand-weaker: var(--ink-light-alpha-8);
- --text-on-brand-strong: var(--ink-light-alpha-12);
- --button-secondary-base: #fcfdfd;
- --button-secondary-hover: #f9fafa;
- --border-base: var(--ink-light-alpha-7);
- --border-hover: var(--ink-light-alpha-8);
- --border-active: var(--ink-light-alpha-9);
- --border-selected: var(--cobalt-light-alpha-9);
- --border-disabled: var(--ink-light-alpha-8);
- --border-focus: var(--ink-light-alpha-9);
- --border-weak-base: var(--ink-light-alpha-5);
- --border-strong-base: var(--ink-light-alpha-7);
- --border-strong-hover: var(--ink-light-alpha-8);
- --border-strong-active: var(--ink-light-alpha-7);
- --border-strong-selected: var(--cobalt-light-alpha-6);
- --border-strong-disabled: var(--ink-light-alpha-6);
- --border-strong-focus: var(--ink-light-alpha-7);
- --border-weak-hover: var(--ink-light-alpha-6);
- --border-weak-active: var(--ink-light-alpha-7);
- --border-weak-selected: var(--cobalt-light-alpha-5);
- --border-weak-disabled: var(--ink-light-alpha-6);
- --border-weak-focus: var(--ink-light-alpha-7);
- --border-interactive-base: var(--cobalt-light-7);
- --border-interactive-hover: var(--cobalt-light-8);
- --border-interactive-active: var(--cobalt-light-9);
- --border-interactive-selected: var(--cobalt-light-9);
- --border-interactive-disabled: var(--ink-light-8);
- --border-interactive-focus: var(--cobalt-light-9);
- --border-success-base: var(--apple-light-6);
- --border-success-hover: var(--apple-light-7);
- --border-success-selected: var(--apple-light-9);
- --border-warning-base: var(--solaris-light-6);
- --border-warning-hover: var(--solaris-light-7);
- --border-warning-selected: var(--solaris-light-9);
- --border-critical-base: var(--ember-light-6);
- --border-critical-hover: var(--ember-light-7);
- --border-critical-selected: var(--ember-light-9);
- --border-info-base: var(--lilac-light-6);
- --border-info-hover: var(--lilac-light-7);
- --border-info-selected: var(--lilac-light-9);
- --icon-base: var(--ink-light-9);
- --icon-hover: var(--ink-light-11);
- --icon-active: var(--ink-light-12);
- --icon-selected: var(--ink-light-12);
- --icon-disabled: var(--ink-light-8);
- --icon-focus: var(--ink-light-12);
- --icon-invert-base: #ffffff;
- --icon-weak-base: var(--ink-light-7);
- --icon-weak-hover: var(--ink-light-8);
- --icon-weak-active: var(--ink-light-9);
- --icon-weak-selected: var(--ink-light-10);
- --icon-weak-disabled: var(--ink-light-6);
- --icon-weak-focus: var(--ink-light-9);
- --icon-strong-base: var(--ink-light-12);
- --icon-strong-hover: #131515;
- --icon-strong-active: #020202;
- --icon-strong-selected: #020202;
- --icon-strong-disabled: var(--ink-light-8);
- --icon-strong-focus: #020202;
- --icon-brand-base: var(--ink-light-12);
- --icon-interactive-base: var(--cobalt-light-9);
- --icon-success-base: var(--apple-light-7);
- --icon-success-hover: var(--apple-light-8);
- --icon-success-active: var(--apple-light-11);
- --icon-warning-base: var(--amber-light-7);
- --icon-warning-hover: var(--amber-light-8);
- --icon-warning-active: var(--amber-light-11);
- --icon-critical-base: var(--ember-light-10);
- --icon-critical-hover: var(--ember-light-11);
- --icon-critical-active: var(--ember-light-12);
- --icon-info-base: var(--lilac-light-7);
- --icon-info-hover: var(--lilac-light-8);
- --icon-info-active: var(--lilac-light-11);
- --icon-on-brand-base: var(--ink-light-alpha-11);
- --icon-on-brand-hover: var(--ink-light-alpha-12);
- --icon-on-brand-selected: var(--ink-light-alpha-12);
- --icon-on-interactive-base: var(--ink-light-1);
- --icon-agent-plan-base: var(--purple-light-9);
- --icon-agent-docs-base: var(--amber-light-9);
- --icon-agent-ask-base: var(--cyan-light-9);
- --icon-agent-build-base: var(--cobalt-light-9);
- --icon-on-success-base: var(--apple-light-alpha-9);
- --icon-on-success-hover: var(--apple-light-alpha-10);
- --icon-on-success-selected: var(--apple-light-alpha-11);
- --icon-on-warning-base: var(--amber-lightalpha-9);
- --icon-on-warning-hover: var(--amber-lightalpha-10);
- --icon-on-warning-selected: var(--amber-lightalpha-11);
- --icon-on-critical-base: var(--ember-light-alpha-9);
- --icon-on-critical-hover: var(--ember-light-alpha-10);
- --icon-on-critical-selected: var(--ember-light-alpha-11);
- --icon-on-info-base: var(--lilac-light-9);
- --icon-on-info-hover: var(--lilac-light-alpha-10);
- --icon-on-info-selected: var(--lilac-light-alpha-11);
- --icon-diff-add-base: var(--mint-light-11);
- --icon-diff-add-hover: var(--mint-light-12);
- --icon-diff-add-active: var(--mint-light-12);
- --icon-diff-delete-base: var(--ember-light-10);
- --icon-diff-delete-hover: var(--ember-light-11);
- --syntax-comment: var(--text-weak);
- --syntax-regexp: var(--text-base);
- --syntax-string: #007663;
- --syntax-keyword: var(--text-weak);
- --syntax-primitive: #fb7f51;
- --syntax-operator: var(--text-weak);
- --syntax-variable: var(--text-strong);
- --syntax-property: #ec6cc8;
- --syntax-type: #738400;
- --syntax-constant: #00b2b9;
- --syntax-punctuation: var(--text-weak);
- --syntax-object: var(--text-strong);
- --syntax-success: var(--apple-light-10);
- --syntax-warning: var(--amber-light-10);
- --syntax-critical: var(--ember-light-9);
- --syntax-info: #0091a7;
- --syntax-diff-add: var(--mint-light-11);
- --syntax-diff-delete: var(--ember-light-11);
- --syntax-diff-unknown: #ff0000;
- --markdown-heading: #d68c27;
- --markdown-text: #1a1a1a;
- --markdown-link: #3b7dd8;
- --markdown-link-text: #318795;
- --markdown-code: #3d9a57;
- --markdown-block-quote: #b0851f;
- --markdown-emph: #b0851f;
- --markdown-strong: #d68c27;
- --markdown-horizontal-rule: #8a8a8a;
- --markdown-list-item: #3b7dd8;
- --markdown-list-enumeration: #318795;
- --markdown-image: #3b7dd8;
- --markdown-image-text: #318795;
- --markdown-code-block: #1a1a1a;
- --border-color: #ffffff;
- --border-weaker-base: var(--ink-light-alpha-3);
- --border-weaker-hover: var(--ink-light-alpha-4);
- --border-weaker-active: var(--ink-light-alpha-6);
- --border-weaker-selected: var(--cobalt-light-alpha-4);
- --border-weaker-disabled: var(--ink-light-alpha-2);
- --border-weaker-focus: var(--ink-light-alpha-6);
- --button-ghost-hover: var(--ink-light-alpha-2);
- --button-ghost-hover2: var(--ink-light-alpha-3);
}
}
diff --git a/packages/ui/src/theme/color.ts b/packages/ui/src/theme/color.ts
new file mode 100644
index 00000000000..3a0526ca6c3
--- /dev/null
+++ b/packages/ui/src/theme/color.ts
@@ -0,0 +1,267 @@
+/**
+ * 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 =
+ h.length === 3
+ ? h
+ .split("")
+ .map((c) => c + c)
+ .join("")
+ : h
+
+ const num = parseInt(full, 16)
+ return {
+ r: ((num >> 16) & 255) / 255,
+ g: ((num >> 8) & 255) / 255,
+ b: (num & 255) / 255,
+ }
+}
+
+/**
+ * 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))
+ const int = Math.round(clamped * 255)
+ return int.toString(16).padStart(2, "0")
+ }
+ 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
+
+ const l = Math.cbrt(l_)
+ const m = Math.cbrt(m_)
+ const s = Math.cbrt(s_)
+
+ const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s
+ const a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s
+ const bOk = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
+
+ const C = Math.sqrt(a * a + bOk * bOk)
+ let H = Math.atan2(bOk, a) * (180 / Math.PI)
+ if (H < 0) H += 360
+
+ 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
+
+ const a = C * Math.cos((H * Math.PI) / 180)
+ const b = C * Math.sin((H * Math.PI) / 180)
+
+ const l = L + 0.3963377774 * a + 0.2158037573 * b
+ const m = L - 0.1055613458 * a - 0.0638541728 * b
+ const s = L - 0.0894841775 * a - 1.291485548 * b
+
+ const l3 = l * l * l
+ const m3 = m * m * m
+ const s3 = s * s * s
+
+ const lr = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3
+ const lg = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3
+ const lb = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.707614701 * s3
+
+ return {
+ r: linearToSrgb(lr),
+ g: linearToSrgb(lg),
+ b: linearToSrgb(lb),
+ }
+}
+
+/**
+ * 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]
+
+ for (let i = 0; i < 12; i++) {
+ scale.push(
+ oklchToHex({
+ l: lightSteps[i],
+ c: base.c * chromaMultipliers[i],
+ h: base.h,
+ }),
+ )
+ }
+
+ 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
+ ? [0.13, 0.16, 0.2, 0.24, 0.28, 0.33, 0.4, 0.52, 0.58, 0.66, 0.82, 0.96]
+ : [0.995, 0.98, 0.96, 0.94, 0.91, 0.88, 0.84, 0.78, 0.62, 0.56, 0.46, 0.2]
+
+ for (let i = 0; i < 12; i++) {
+ scale.push(
+ oklchToHex({
+ l: lightSteps[i],
+ c: neutralChroma,
+ h: base.h,
+ }),
+ )
+ }
+
+ 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]
+
+ return scale.map((hex, i) => {
+ 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)
+
+ return oklchToHex({
+ l: c1.l + (c2.l - c1.l) * amount,
+ c: c1.c + (c2.c - c1.c) * amount,
+ h: c1.h + (c2.h - c1.h) * amount,
+ })
+}
+
+/**
+ * Lighten a color by a given amount (0-1)
+ */
+export function lighten(color: HexColor, amount: number): HexColor {
+ const oklch = hexToOklch(color)
+ return oklchToHex({
+ ...oklch,
+ l: Math.min(1, oklch.l + amount),
+ })
+}
+
+/**
+ * Darken a color by a given amount (0-1)
+ */
+export function darken(color: HexColor, amount: number): HexColor {
+ const oklch = hexToOklch(color)
+ return oklchToHex({
+ ...oklch,
+ l: Math.max(0, oklch.l - amount),
+ })
+}
+
+/**
+ * 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
new file mode 100644
index 00000000000..d8ca6d50777
--- /dev/null
+++ b/packages/ui/src/theme/context.tsx
@@ -0,0 +1,280 @@
+/**
+ * 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,
+ createSignal,
+ onMount,
+ onCleanup,
+ createEffect,
+ type JSX,
+ type Accessor,
+} from "solid-js"
+import type { DesktopTheme } from "./types"
+import { resolveThemeVariant, themeToCss } from "./resolve"
+import { STORAGE_KEYS, getThemeCacheKey } from "./preload"
+import { DEFAULT_THEMES } from "./default-themes"
+
+export type ColorScheme = "light" | "dark" | "system"
+
+interface ThemeContextValue {
+ /** Currently active theme ID */
+ themeId: Accessor
+ /** Current color scheme preference */
+ colorScheme: Accessor
+ /** Resolved current mode (light or dark) */
+ mode: Accessor<"light" | "dark">
+ /** All available themes */
+ themes: Accessor>
+ /** 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
+}
+
+const ThemeContext = createContext()
+
+/**
+ * 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 {
+ const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null
+ if (existing) {
+ return existing
+ }
+ const element = document.createElement("style")
+ element.id = THEME_STYLE_ID
+ document.head.appendChild(element)
+ 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
+ const cacheKey = getThemeCacheKey(themeId, mode)
+ try {
+ localStorage.setItem(cacheKey, css)
+ } catch {
+ // localStorage might be full or disabled
+ }
+
+ // 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()
+ }
+
+ 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 {
+ for (const mode of ["light", "dark"] as const) {
+ const isDark = mode === "dark"
+ const variant = isDark ? theme.dark : theme.light
+ const tokens = resolveThemeVariant(variant, isDark)
+ const css = themeToCss(tokens)
+ const cacheKey = getThemeCacheKey(themeId, mode)
+ try {
+ localStorage.setItem(cacheKey, css)
+ } catch {
+ // localStorage might be full or disabled
+ }
+ }
+}
+
+export function ThemeProvider(props: { children: JSX.Element; defaultTheme?: string }) {
+ const [themes, setThemes] = createSignal>(DEFAULT_THEMES)
+ const [themeId, setThemeIdSignal] = createSignal(props.defaultTheme ?? "oc-1")
+ const [colorScheme, setColorSchemeSignal] = createSignal("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 = () => {
+ if (colorScheme() === "system") {
+ setMode(getSystemMode())
+ }
+ }
+ 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()
+ const theme = themes()[id]
+ if (theme) {
+ applyThemeCss(theme, id, m)
+ }
+ })
+
+ const setTheme = (id: string) => {
+ const theme = themes()[id]
+ if (!theme) {
+ 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 {
+ setMode(scheme)
+ }
+ }
+
+ const registerTheme = (theme: DesktopTheme) => {
+ setThemes((prev) => ({
+ ...prev,
+ [theme.id]: theme,
+ }))
+ }
+
+ return (
+
+ {props.children}
+
+ )
+}
+
+export function useTheme(): ThemeContextValue {
+ const ctx = useContext(ThemeContext)
+ if (!ctx) {
+ throw new Error("useTheme must be used within a ThemeProvider")
+ }
+ return ctx
+}
diff --git a/packages/ui/src/theme/default-themes.ts b/packages/ui/src/theme/default-themes.ts
new file mode 100644
index 00000000000..749d5e97ccb
--- /dev/null
+++ b/packages/ui/src/theme/default-themes.ts
@@ -0,0 +1,35 @@
+import type { DesktopTheme } from "./types"
+import oc1ThemeJson from "./themes/oc-1.json"
+import tokyoThemeJson from "./themes/tokyonight.json"
+import draculaThemeJson from "./themes/dracula.json"
+import monokaiThemeJson from "./themes/monokai.json"
+import solarizedThemeJson from "./themes/solarized.json"
+import nordThemeJson from "./themes/nord.json"
+import catppuccinThemeJson from "./themes/catppuccin.json"
+import ayuThemeJson from "./themes/ayu.json"
+import oneDarkProThemeJson from "./themes/onedarkpro.json"
+import shadesOfPurpleThemeJson from "./themes/shadesofpurple.json"
+
+export const oc1Theme = oc1ThemeJson as DesktopTheme
+export const tokyonightTheme = tokyoThemeJson as DesktopTheme
+export const draculaTheme = draculaThemeJson as DesktopTheme
+export const monokaiTheme = monokaiThemeJson as DesktopTheme
+export const solarizedTheme = solarizedThemeJson as DesktopTheme
+export const nordTheme = nordThemeJson as DesktopTheme
+export const catppuccinTheme = catppuccinThemeJson as DesktopTheme
+export const ayuTheme = ayuThemeJson as DesktopTheme
+export const oneDarkProTheme = oneDarkProThemeJson as DesktopTheme
+export const shadesOfPurpleTheme = shadesOfPurpleThemeJson as DesktopTheme
+
+export const DEFAULT_THEMES: Record = {
+ "oc-1": oc1Theme,
+ tokyonight: tokyonightTheme,
+ dracula: draculaTheme,
+ monokai: monokaiTheme,
+ solarized: solarizedTheme,
+ nord: nordTheme,
+ catppuccin: catppuccinTheme,
+ ayu: ayuTheme,
+ onedarkpro: oneDarkProTheme,
+ shadesofpurple: shadesOfPurpleTheme,
+}
diff --git a/packages/ui/src/theme/desktop-theme.schema.json b/packages/ui/src/theme/desktop-theme.schema.json
new file mode 100644
index 00000000000..b60a8f37cad
--- /dev/null
+++ b/packages/ui/src/theme/desktop-theme.schema.json
@@ -0,0 +1,104 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://opencode.ai/desktop-theme.json",
+ "title": "OpenCode Desktop Theme",
+ "description": "A theme definition for the OpenCode desktop application",
+ "type": "object",
+ "required": ["name", "id", "light", "dark"],
+ "properties": {
+ "$schema": {
+ "type": "string",
+ "description": "JSON Schema reference"
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable theme name"
+ },
+ "id": {
+ "type": "string",
+ "description": "Unique theme identifier (slug)",
+ "pattern": "^[a-z0-9-]+$"
+ },
+ "light": {
+ "$ref": "#/definitions/ThemeVariant",
+ "description": "Light mode color variant"
+ },
+ "dark": {
+ "$ref": "#/definitions/ThemeVariant",
+ "description": "Dark mode color variant"
+ }
+ },
+ "definitions": {
+ "HexColor": {
+ "type": "string",
+ "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$",
+ "description": "A hex color value like #fff, #ffff, #ffffff, or #ffffffff"
+ },
+ "ColorValue": {
+ "type": "string",
+ "pattern": "^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|var\(--[a-z0-9-]+\))$",
+ "description": "Either a hex color value (#rgb/#rgba/#rrggbb/#rrggbbaa) or a CSS variable reference"
+ },
+ "ThemeSeedColors": {
+ "type": "object",
+ "description": "The minimum set of colors needed to generate a theme",
+ "required": ["neutral", "primary", "success", "warning", "error", "info", "interactive", "diffAdd", "diffDelete"],
+ "properties": {
+ "neutral": {
+ "$ref": "#/definitions/HexColor",
+ "description": "Base neutral color for generating the gray scale"
+ },
+ "primary": {
+ "$ref": "#/definitions/HexColor",
+ "description": "Primary brand/accent color"
+ },
+ "success": {
+ "$ref": "#/definitions/HexColor",
+ "description": "Success state color (typically green)"
+ },
+ "warning": {
+ "$ref": "#/definitions/HexColor",
+ "description": "Warning state color (typically yellow/orange)"
+ },
+ "error": {
+ "$ref": "#/definitions/HexColor",
+ "description": "Error/critical state color (typically red)"
+ },
+ "info": {
+ "$ref": "#/definitions/HexColor",
+ "description": "Informational state color (typically purple/blue)"
+ },
+ "interactive": {
+ "$ref": "#/definitions/HexColor",
+ "description": "Interactive element color (links, buttons)"
+ },
+ "diffAdd": {
+ "$ref": "#/definitions/HexColor",
+ "description": "Color for diff additions"
+ },
+ "diffDelete": {
+ "$ref": "#/definitions/HexColor",
+ "description": "Color for diff deletions"
+ }
+ }
+ },
+ "ThemeVariant": {
+ "type": "object",
+ "description": "A theme variant (light or dark) with seed colors and optional overrides",
+ "required": ["seeds"],
+ "properties": {
+ "seeds": {
+ "$ref": "#/definitions/ThemeSeedColors",
+ "description": "Seed colors used to generate the full palette"
+ },
+ "overrides": {
+ "type": "object",
+ "description": "Optional direct overrides for any CSS variable (without -- prefix)",
+ "additionalProperties": {
+ "$ref": "#/definitions/ColorValue"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/ui/src/theme/index.ts b/packages/ui/src/theme/index.ts
new file mode 100644
index 00000000000..8f3da4ca130
--- /dev/null
+++ b/packages/ui/src/theme/index.ts
@@ -0,0 +1,71 @@
+/**
+ * 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,
+ ThemeVariant,
+ HexColor,
+ OklchColor,
+ ResolvedTheme,
+ ColorValue,
+ CssVarRef,
+} from "./types"
+
+// Color utilities
+export {
+ hexToRgb,
+ rgbToHex,
+ hexToOklch,
+ oklchToHex,
+ rgbToOklch,
+ oklchToRgb,
+ generateScale,
+ generateNeutralScale,
+ generateAlphaScale,
+ mixColors,
+ lighten,
+ darken,
+ 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"
+
+// Preload script utilities
+export { generatePreloadScript, generatePreloadScriptFormatted, STORAGE_KEYS, getThemeCacheKey } from "./preload"
+
+// Default themes
+export {
+ DEFAULT_THEMES,
+ oc1Theme,
+ tokyonightTheme,
+ draculaTheme,
+ monokaiTheme,
+ solarizedTheme,
+ nordTheme,
+ catppuccinTheme,
+ ayuTheme,
+ oneDarkProTheme,
+ shadesOfPurpleTheme,
+} from "./default-themes"
diff --git a/packages/ui/src/theme/loader.ts b/packages/ui/src/theme/loader.ts
new file mode 100644
index 00000000000..b25c833dd3d
--- /dev/null
+++ b/packages/ui/src/theme/loader.ts
@@ -0,0 +1,213 @@
+/**
+ * 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 {
+ const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null
+ if (existing) {
+ return existing
+ }
+ const element = document.createElement("style")
+ element.id = THEME_STYLE_ID
+ document.head.appendChild(element)
+ return element
+}
+
+/**
+ * Load and apply a theme to the document.
+ * Creates or updates a