summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-28 15:47:05 -0600
committerAdam <[email protected]>2025-12-28 15:47:05 -0600
commita4411c21b6687e7392447da08dbaec2dc530075f (patch)
tree6bbcdc798fb325da57a91ebec460e348a0d96d63 /packages
parent9d61370ac458627c394195ef761669ba70c93237 (diff)
downloadopencode-a4411c21b6687e7392447da08dbaec2dc530075f.tar.gz
opencode-a4411c21b6687e7392447da08dbaec2dc530075f.zip
feat(desktop): theme preview
Diffstat (limited to 'packages')
-rw-r--r--packages/app/src/context/command.tsx37
-rw-r--r--packages/ui/src/components/list.tsx8
-rw-r--r--packages/ui/src/theme/context.tsx50
3 files changed, 89 insertions, 6 deletions
diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx
index f91a1cf05..626bfdbe4 100644
--- a/packages/app/src/context/command.tsx
+++ b/packages/app/src/context/command.tsx
@@ -3,6 +3,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
+import { useTheme } from "@opencode-ai/ui/theme"
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
@@ -115,6 +116,34 @@ export function formatKeybind(config: string): string {
function DialogCommand(props: { options: CommandOption[] }) {
const dialog = useDialog()
+ const theme = useTheme()
+ let committed = false
+
+ const handleMove = (option: CommandOption | undefined) => {
+ if (!option) return
+ if (option.id.startsWith("theme.set.")) {
+ const id = option.id.replace("theme.set.", "")
+ theme.previewTheme(id)
+ } else if (option.id.startsWith("theme.scheme.") && !option.id.includes("cycle")) {
+ const scheme = option.id.replace("theme.scheme.", "") as "light" | "dark" | "system"
+ theme.previewColorScheme(scheme)
+ }
+ }
+
+ const handleSelect = (option: CommandOption | undefined) => {
+ if (option) {
+ theme.commitPreview()
+ committed = true
+ dialog.close()
+ option.onSelect?.("palette")
+ }
+ }
+
+ onCleanup(() => {
+ if (!committed) {
+ theme.cancelPreview()
+ }
+ })
return (
<Dialog title="Commands">
@@ -125,12 +154,8 @@ function DialogCommand(props: { options: CommandOption[] }) {
key={(x) => x?.id}
filterKeys={["title", "description", "category"]}
groupBy={(x) => x.category ?? ""}
- onSelect={(option) => {
- if (option) {
- dialog.close()
- option.onSelect?.("palette")
- }
- }}
+ onMove={handleMove}
+ onSelect={handleSelect}
>
{(option) => (
<div class="w-full flex items-center justify-between gap-4">
diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx
index 4f6df0faf..808c9b032 100644
--- a/packages/ui/src/components/list.tsx
+++ b/packages/ui/src/components/list.tsx
@@ -15,6 +15,7 @@ export interface ListProps<T> extends FilteredListProps<T> {
children: (item: T) => JSX.Element
emptyMessage?: string
onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
+ onMove?: (item: T | undefined) => void
activeIcon?: IconProps["name"]
filter?: string
search?: ListSearchProps | boolean
@@ -82,6 +83,13 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
element?.scrollIntoView({ block: "center", behavior: "smooth" })
})
+ createEffect(() => {
+ const all = flat()
+ const current = active()
+ const item = all.find((x) => props.key(x) === current)
+ props.onMove?.(item)
+ })
+
const handleSelect = (item: T | undefined, index: number) => {
props.onSelect?.(item, index)
}
diff --git a/packages/ui/src/theme/context.tsx b/packages/ui/src/theme/context.tsx
index d2fbed3a1..210773f0c 100644
--- a/packages/ui/src/theme/context.tsx
+++ b/packages/ui/src/theme/context.tsx
@@ -22,6 +22,10 @@ interface ThemeContextValue {
setTheme: (id: string) => void
setColorScheme: (scheme: ColorScheme) => void
registerTheme: (theme: DesktopTheme) => void
+ previewTheme: (id: string) => void
+ previewColorScheme: (scheme: ColorScheme) => void
+ commitPreview: () => void
+ cancelPreview: () => void
}
const ThemeContext = createContext<ThemeContextValue>()
@@ -104,6 +108,8 @@ export function ThemeProvider(props: { children: JSX.Element; defaultTheme?: str
const [themeId, setThemeIdSignal] = createSignal(props.defaultTheme ?? "oc-1")
const [colorScheme, setColorSchemeSignal] = createSignal<ColorScheme>("system")
const [mode, setMode] = createSignal<"light" | "dark">(getSystemMode())
+ const [previewThemeId, setPreviewThemeId] = createSignal<string | null>(null)
+ const [previewScheme, setPreviewScheme] = createSignal<ColorScheme | null>(null)
onMount(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
@@ -169,6 +175,46 @@ export function ThemeProvider(props: { children: JSX.Element; defaultTheme?: str
}))
}
+ const previewTheme = (id: string) => {
+ const theme = themes()[id]
+ if (!theme) return
+ setPreviewThemeId(id)
+ const previewMode = previewScheme() ? (previewScheme() === "system" ? getSystemMode() : previewScheme()!) : mode()
+ applyThemeCss(theme, id, previewMode as "light" | "dark")
+ }
+
+ const previewColorScheme = (scheme: ColorScheme) => {
+ setPreviewScheme(scheme)
+ const previewMode = scheme === "system" ? getSystemMode() : scheme
+ const id = previewThemeId() ?? themeId()
+ const theme = themes()[id]
+ if (theme) {
+ applyThemeCss(theme, id, previewMode)
+ }
+ }
+
+ const commitPreview = () => {
+ const id = previewThemeId()
+ const scheme = previewScheme()
+ if (id) {
+ setTheme(id)
+ }
+ if (scheme) {
+ setColorSchemePref(scheme)
+ }
+ setPreviewThemeId(null)
+ setPreviewScheme(null)
+ }
+
+ const cancelPreview = () => {
+ setPreviewThemeId(null)
+ setPreviewScheme(null)
+ const theme = themes()[themeId()]
+ if (theme) {
+ applyThemeCss(theme, themeId(), mode())
+ }
+ }
+
return (
<ThemeContext.Provider
value={{
@@ -179,6 +225,10 @@ export function ThemeProvider(props: { children: JSX.Element; defaultTheme?: str
setTheme,
setColorScheme: setColorSchemePref,
registerTheme,
+ previewTheme,
+ previewColorScheme,
+ commitPreview,
+ cancelPreview,
}}
>
{props.children}