understanding

This commit is contained in:
2026-02-06 16:29:09 -05:00
parent bfea6816ef
commit 1cee931913
13 changed files with 161 additions and 398 deletions

View File

@@ -50,7 +50,7 @@ export function Layout(props: LayoutProps) {
flexDirection="column" flexDirection="column"
width="100%" width="100%"
height="100%" height="100%"
backgroundColor={theme.background} backgroundColor={theme.surface}
> >
{/* Header - tab bar */} {/* Header - tab bar */}
<Show when={props.header}> <Show when={props.header}>
@@ -119,18 +119,6 @@ export function Layout(props: LayoutProps) {
)} )}
</For> </For>
</box> </box>
{/* Footer - status/nav bar */}
<Show when={props.footer}>
<box
style={{
height: 2,
backgroundColor: theme.surface ?? theme.backgroundPanel,
}}
>
<box style={{ padding: 1 }}>{props.footer}</box>
</box>
</Show>
</box> </box>
); );
} }

View File

@@ -1,20 +1,25 @@
import type { ThemeColors, ThemeDefinition, ThemeName } from "../types/settings" import type {
import { BASE_THEME_COLORS, BASE_LAYER_BACKGROUND } from "../types/desktop-theme" ThemeColors,
import catppuccin from "../themes/catppuccin.json" with { type: "json" } ThemeDefinition,
import gruvbox from "../themes/gruvbox.json" with { type: "json" } ThemeName,
import tokyo from "../themes/tokyo.json" with { type: "json" } } from "../types/settings";
import nord from "../themes/nord.json" with { type: "json" } import {
import opencode from "../themes/opencode.json" with { type: "json" } BASE_THEME_COLORS,
BASE_LAYER_BACKGROUND,
} from "../types/desktop-theme";
import catppuccin from "../themes/catppuccin.json" with { type: "json" };
import gruvbox from "../themes/gruvbox.json" with { type: "json" };
import tokyo from "../themes/tokyo.json" with { type: "json" };
import nord from "../themes/nord.json" with { type: "json" };
export const DEFAULT_THEME: ThemeColors = { export const DEFAULT_THEME: ThemeColors = {
...BASE_THEME_COLORS, ...BASE_THEME_COLORS,
layerBackgrounds: BASE_LAYER_BACKGROUND, layerBackgrounds: BASE_LAYER_BACKGROUND,
} };
export const THEME_JSON: Record<string, ThemeDefinition> = { export const THEME_JSON: Record<string, ThemeDefinition> = {
opencode: opencode as ThemeDefinition,
catppuccin: catppuccin as ThemeDefinition, catppuccin: catppuccin as ThemeDefinition,
gruvbox: gruvbox as ThemeDefinition, gruvbox: gruvbox as ThemeDefinition,
tokyo: tokyo as ThemeDefinition, tokyo: tokyo as ThemeDefinition,
nord: nord as ThemeDefinition, nord: nord as ThemeDefinition,
} };

View File

@@ -93,9 +93,6 @@ type ThemeResolved = {
* This ensures children are NOT rendered until the theme is ready, * This ensures children are NOT rendered until the theme is ready,
* preventing "useTheme must be used within a ThemeProvider" errors. * preventing "useTheme must be used within a ThemeProvider" errors.
* *
* The key insight from opencode's implementation is that the provider
* uses `<Show when={ready}>` to gate rendering, so components can
* safely call useTheme() without checking ready state.
*/ */
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
name: "Theme", name: "Theme",
@@ -121,8 +118,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
); );
}) })
.catch(() => { .catch(() => {
// If custom themes fail to load, fall back to opencode theme setStore("active", "catppuccin");
setStore("active", "opencode");
}) })
.finally(() => { .finally(() => {
// Only set ready if not waiting for system theme // Only set ready if not waiting for system theme
@@ -206,7 +202,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
if (store.active === "system") { if (store.active === "system") {
setStore( setStore(
produce((draft) => { produce((draft) => {
draft.active = "opencode"; draft.active = "catppuccin";
draft.ready = true; draft.ready = true;
}), }),
); );

View File

@@ -5,6 +5,8 @@
import type { Feed, FeedVisibility } from "@/types/feed"; import type { Feed, FeedVisibility } from "@/types/feed";
import { format } from "date-fns"; import { format } from "date-fns";
import { htmlToText } from "@/utils/html-to-text";
import { useTheme } from "@/context/ThemeContext";
interface FeedItemProps { interface FeedItemProps {
feed: Feed; feed: Feed;
@@ -36,6 +38,7 @@ export function FeedItem(props: FeedItemProps) {
const pinnedIndicator = () => { const pinnedIndicator = () => {
return props.feed.isPinned ? "*" : " "; return props.feed.isPinned ? "*" : " ";
}; };
const { theme } = useTheme();
if (props.compact) { if (props.compact) {
// Compact single-line view // Compact single-line view
@@ -51,8 +54,8 @@ export function FeedItem(props: FeedItemProps) {
{props.isSelected ? ">" : " "} {props.isSelected ? ">" : " "}
</text> </text>
<text fg={visibilityColor()}>{visibilityIcon()}</text> <text fg={visibilityColor()}>{visibilityIcon()}</text>
<text fg={props.isSelected ? "white" : undefined}> <text fg={props.isSelected ? "white" : theme.accent}>
{props.feed.customName || props.feed.podcast.title} {htmlToText(props.feed.customName || props.feed.podcast.title)}
</text> </text>
{props.showEpisodeCount && <text fg="gray">({episodeCount()})</text>} {props.showEpisodeCount && <text fg="gray">({episodeCount()})</text>}
</box> </box>
@@ -76,12 +79,13 @@ export function FeedItem(props: FeedItemProps) {
</text> </text>
<text fg={visibilityColor()}>{visibilityIcon()}</text> <text fg={visibilityColor()}>{visibilityIcon()}</text>
<text fg="yellow">{pinnedIndicator()}</text> <text fg="yellow">{pinnedIndicator()}</text>
<text fg={props.isSelected ? "white" : undefined}> <text fg={props.isSelected ? "white" : theme.text}>
<strong>{props.feed.customName || props.feed.podcast.title}</strong> <strong>
{htmlToText(props.feed.customName || props.feed.podcast.title)}
</strong>
</text> </text>
</box> </box>
{/* Details row */}
<box flexDirection="row" gap={2} paddingLeft={4}> <box flexDirection="row" gap={2} paddingLeft={4}>
{props.showEpisodeCount && ( {props.showEpisodeCount && (
<text fg="gray"> <text fg="gray">
@@ -93,7 +97,6 @@ export function FeedItem(props: FeedItemProps) {
)} )}
</box> </box>
{/* Description (truncated) */}
{props.feed.podcast.description && ( {props.feed.podcast.description && (
<box paddingLeft={4} paddingTop={0}> <box paddingLeft={4} paddingTop={0}>
<text fg="gray"> <text fg="gray">

View File

@@ -9,6 +9,8 @@ import { useFeedStore } from "@/stores/feed";
import { format } from "date-fns"; import { format } from "date-fns";
import type { Episode } from "@/types/episode"; import type { Episode } from "@/types/episode";
import type { Feed } from "@/types/feed"; import type { Feed } from "@/types/feed";
import { useTheme } from "@/context/ThemeContext";
import { htmlToText } from "@/utils/html-to-text";
type FeedPageProps = { type FeedPageProps = {
focused: boolean; focused: boolean;
@@ -67,8 +69,13 @@ export function FeedPage(props: FeedPageProps) {
} }
}); });
const { theme } = useTheme();
return ( return (
<box flexDirection="column" height="100%"> <box
backgroundColor={theme.background}
flexDirection="column"
height="100%"
>
{/* Status line */} {/* Status line */}
<Show when={isRefreshing()}> <Show when={isRefreshing()}>
<text fg="yellow">Refreshing feeds...</text> <text fg="yellow">Refreshing feeds...</text>
@@ -104,7 +111,7 @@ export function FeedPage(props: FeedPageProps) {
<text fg={index() === selectedIndex() ? "cyan" : "gray"}> <text fg={index() === selectedIndex() ? "cyan" : "gray"}>
{index() === selectedIndex() ? ">" : " "} {index() === selectedIndex() ? ">" : " "}
</text> </text>
<text fg={index() === selectedIndex() ? "white" : undefined}> <text fg={index() === selectedIndex() ? "white" : theme.text}>
{item.episode.title} {item.episode.title}
</text> </text>
</box> </box>

View File

@@ -1,7 +1,6 @@
{ {
"$schema": "https://opencode.ai/theme.json",
"defs": { "defs": {
"background": "transparent", "background": "#181825",
"surface": "#1e1e2e", "surface": "#1e1e2e",
"primary": "#89b4fa", "primary": "#89b4fa",
"secondary": "#cba6f7", "secondary": "#cba6f7",
@@ -11,7 +10,7 @@
"warning": "#fab387", "warning": "#fab387",
"error": "#f38ba8", "error": "#f38ba8",
"success": "#a6e3a1", "success": "#a6e3a1",
"layer0": "transparent", "layer0": "#181825",
"layer1": "#181825", "layer1": "#181825",
"layer2": "#11111b", "layer2": "#11111b",
"layer3": "#0a0a0f" "layer3": "#0a0a0f"

View File

@@ -1,7 +1,6 @@
{ {
"$schema": "https://opencode.ai/theme.json",
"defs": { "defs": {
"background": "transparent", "background": "#282828",
"surface": "#282828", "surface": "#282828",
"primary": "#fabd2f", "primary": "#fabd2f",
"secondary": "#83a598", "secondary": "#83a598",
@@ -11,7 +10,7 @@
"warning": "#fabd2f", "warning": "#fabd2f",
"error": "#fb4934", "error": "#fb4934",
"success": "#b8bb26", "success": "#b8bb26",
"layer0": "transparent", "layer0": "#282828",
"layer1": "#32302a", "layer1": "#32302a",
"layer2": "#1d2021", "layer2": "#1d2021",
"layer3": "#0d0c0c" "layer3": "#0d0c0c"

View File

@@ -1,7 +1,6 @@
{ {
"$schema": "https://opencode.ai/theme.json",
"defs": { "defs": {
"background": "transparent", "background": "#2e3440",
"surface": "#2e3440", "surface": "#2e3440",
"primary": "#88c0d0", "primary": "#88c0d0",
"secondary": "#81a1c1", "secondary": "#81a1c1",
@@ -11,7 +10,7 @@
"warning": "#ebcb8b", "warning": "#ebcb8b",
"error": "#bf616a", "error": "#bf616a",
"success": "#a3be8c", "success": "#a3be8c",
"layer0": "transparent", "layer0": "#2e3440",
"layer1": "#3b4252", "layer1": "#3b4252",
"layer2": "#242933", "layer2": "#242933",
"layer3": "#1a1c23" "layer3": "#1a1c23"

View File

@@ -1,245 +0,0 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkStep1": "#0a0a0a",
"darkStep2": "#141414",
"darkStep3": "#1e1e1e",
"darkStep4": "#282828",
"darkStep5": "#323232",
"darkStep6": "#3c3c3c",
"darkStep7": "#484848",
"darkStep8": "#606060",
"darkStep9": "#fab283",
"darkStep10": "#ffc09f",
"darkStep11": "#808080",
"darkStep12": "#eeeeee",
"darkSecondary": "#5c9cf5",
"darkAccent": "#9d7cd8",
"darkRed": "#e06c75",
"darkOrange": "#f5a742",
"darkGreen": "#7fd88f",
"darkCyan": "#56b6c2",
"darkYellow": "#e5c07b",
"lightStep1": "#ffffff",
"lightStep2": "#fafafa",
"lightStep3": "#f5f5f5",
"lightStep4": "#ebebeb",
"lightStep5": "#e1e1e1",
"lightStep6": "#d4d4d4",
"lightStep7": "#b8b8b8",
"lightStep8": "#a0a0a0",
"lightStep9": "#3b7dd8",
"lightStep10": "#2968c3",
"lightStep11": "#8a8a8a",
"lightStep12": "#1a1a1a",
"lightSecondary": "#7b5bb6",
"lightAccent": "#d68c27",
"lightRed": "#d1383d",
"lightOrange": "#d68c27",
"lightGreen": "#3d9a57",
"lightCyan": "#318795",
"lightYellow": "#b0851f"
},
"theme": {
"primary": {
"dark": "darkStep9",
"light": "lightStep9"
},
"secondary": {
"dark": "darkSecondary",
"light": "lightSecondary"
},
"accent": {
"dark": "darkAccent",
"light": "lightAccent"
},
"error": {
"dark": "darkRed",
"light": "lightRed"
},
"warning": {
"dark": "darkOrange",
"light": "lightOrange"
},
"success": {
"dark": "darkGreen",
"light": "lightGreen"
},
"info": {
"dark": "darkCyan",
"light": "lightCyan"
},
"text": {
"dark": "darkStep12",
"light": "lightStep12"
},
"textMuted": {
"dark": "darkStep11",
"light": "lightStep11"
},
"background": {
"dark": "darkStep1",
"light": "lightStep1"
},
"backgroundPanel": {
"dark": "darkStep2",
"light": "lightStep2"
},
"backgroundElement": {
"dark": "darkStep3",
"light": "lightStep3"
},
"border": {
"dark": "darkStep7",
"light": "lightStep7"
},
"borderActive": {
"dark": "darkStep8",
"light": "lightStep8"
},
"borderSubtle": {
"dark": "darkStep6",
"light": "lightStep6"
},
"diffAdded": {
"dark": "#4fd6be",
"light": "#1e725c"
},
"diffRemoved": {
"dark": "#c53b53",
"light": "#c53b53"
},
"diffContext": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHunkHeader": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHighlightAdded": {
"dark": "#b8db87",
"light": "#4db380"
},
"diffHighlightRemoved": {
"dark": "#e26a75",
"light": "#f52a65"
},
"diffAddedBg": {
"dark": "#20303b",
"light": "#d5e5d5"
},
"diffRemovedBg": {
"dark": "#37222c",
"light": "#f7d8db"
},
"diffContextBg": {
"dark": "darkStep2",
"light": "lightStep2"
},
"diffLineNumber": {
"dark": "darkStep3",
"light": "lightStep3"
},
"diffAddedLineNumberBg": {
"dark": "#1b2b34",
"light": "#c5d5c5"
},
"diffRemovedLineNumberBg": {
"dark": "#2d1f26",
"light": "#e7c8cb"
},
"markdownText": {
"dark": "darkStep12",
"light": "lightStep12"
},
"markdownHeading": {
"dark": "darkAccent",
"light": "lightAccent"
},
"markdownLink": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownLinkText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCode": {
"dark": "darkGreen",
"light": "lightGreen"
},
"markdownBlockQuote": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownEmph": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownStrong": {
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownHorizontalRule": {
"dark": "darkStep11",
"light": "lightStep11"
},
"markdownListItem": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownListEnumeration": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownImage": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownImageText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCodeBlock": {
"dark": "darkStep12",
"light": "lightStep12"
},
"syntaxComment": {
"dark": "darkStep11",
"light": "lightStep11"
},
"syntaxKeyword": {
"dark": "darkAccent",
"light": "lightAccent"
},
"syntaxFunction": {
"dark": "darkStep9",
"light": "lightStep9"
},
"syntaxVariable": {
"dark": "darkRed",
"light": "lightRed"
},
"syntaxString": {
"dark": "darkGreen",
"light": "lightGreen"
},
"syntaxNumber": {
"dark": "darkOrange",
"light": "lightOrange"
},
"syntaxType": {
"dark": "darkYellow",
"light": "lightYellow"
},
"syntaxOperator": {
"dark": "darkCyan",
"light": "lightCyan"
},
"syntaxPunctuation": {
"dark": "darkStep12",
"light": "lightStep12"
}
}
}

View File

@@ -1,5 +1,4 @@
{ {
"$schema": "https://opencode.ai/theme.json",
"defs": {}, "defs": {},
"theme": { "theme": {
"primary": "#000000", "primary": "#000000",

View File

@@ -1,7 +1,6 @@
{ {
"$schema": "https://opencode.ai/theme.json",
"defs": { "defs": {
"background": "transparent", "background": "#0f0f15",
"surface": "#1a1b26", "surface": "#1a1b26",
"primary": "#7aa2f7", "primary": "#7aa2f7",
"secondary": "#bb9af7", "secondary": "#bb9af7",
@@ -11,7 +10,7 @@
"warning": "#e0af68", "warning": "#e0af68",
"error": "#f7768e", "error": "#f7768e",
"success": "#9ece6a", "success": "#9ece6a",
"layer0": "transparent", "layer0": "#0f0f15",
"layer1": "#16161e", "layer1": "#16161e",
"layer2": "#0f0f15", "layer2": "#0f0f15",
"layer3": "#08080b" "layer3": "#08080b"

View File

@@ -1,83 +1,89 @@
import type { RGBA } from "@opentui/core" import type { RGBA } from "@opentui/core";
import type { ColorValue, ThemeJson, Variant } from "./theme-schema" import type { ColorValue, ThemeJson, Variant } from "./theme-schema";
export type ThemeName = "system" | "opencode" | "catppuccin" | "gruvbox" | "tokyo" | "nord" | "custom" export type ThemeName =
| "system"
| "catppuccin"
| "gruvbox"
| "tokyo"
| "nord"
| "custom";
export type LayerBackgrounds = { export type LayerBackgrounds = {
layer0: ColorValue layer0: ColorValue;
layer1: ColorValue layer1: ColorValue;
layer2: ColorValue layer2: ColorValue;
layer3: ColorValue layer3: ColorValue;
} };
export type ThemeColors = { export type ThemeColors = {
background: ColorValue background: ColorValue;
surface: ColorValue surface: ColorValue;
primary: ColorValue primary: ColorValue;
secondary: ColorValue secondary: ColorValue;
accent: ColorValue accent: ColorValue;
text: ColorValue text: ColorValue;
muted: ColorValue muted: ColorValue;
warning: ColorValue warning: ColorValue;
error: ColorValue error: ColorValue;
success: ColorValue success: ColorValue;
layerBackgrounds?: LayerBackgrounds layerBackgrounds?: LayerBackgrounds;
} };
export type ThemeVariant = { export type ThemeVariant = {
name: string name: string;
colors: ThemeColors colors: ThemeColors;
} };
export type ThemeToken = { export type ThemeToken = {
[key: string]: string [key: string]: string;
} };
export type ResolvedTheme = Record<string, RGBA> & { export type ResolvedTheme = Record<string, RGBA> & {
layerBackgrounds: Record<string, RGBA> layerBackgrounds: Record<string, RGBA>;
_hasSelectedListItemText: boolean _hasSelectedListItemText: boolean;
thinkingOpacity: number thinkingOpacity: number;
} };
export type DesktopTheme = { export type DesktopTheme = {
name: string name: string;
variants: ThemeVariant[] variants: ThemeVariant[];
defaultVariant: string defaultVariant: string;
tokens: ThemeToken tokens: ThemeToken;
} };
export type VisualizerSettings = { export type VisualizerSettings = {
/** Number of frequency bars (8128, default: 32) */ /** Number of frequency bars (8128, default: 32) */
bars: number bars: number;
/** Automatic sensitivity: 1 = enabled, 0 = disabled (default: 1) */ /** Automatic sensitivity: 1 = enabled, 0 = disabled (default: 1) */
sensitivity: number sensitivity: number;
/** Noise reduction factor 0.01.0 (default: 0.77) */ /** Noise reduction factor 0.01.0 (default: 0.77) */
noiseReduction: number noiseReduction: number;
/** Low frequency cutoff in Hz (default: 50) */ /** Low frequency cutoff in Hz (default: 50) */
lowCutOff: number lowCutOff: number;
/** High frequency cutoff in Hz (default: 10000) */ /** High frequency cutoff in Hz (default: 10000) */
highCutOff: number highCutOff: number;
} };
export type AppSettings = { export type AppSettings = {
theme: ThemeName theme: ThemeName;
fontSize: number fontSize: number;
playbackSpeed: number playbackSpeed: number;
downloadPath: string downloadPath: string;
visualizer: VisualizerSettings visualizer: VisualizerSettings;
} };
export type UserPreferences = { export type UserPreferences = {
showExplicit: boolean showExplicit: boolean;
autoDownload: boolean autoDownload: boolean;
} };
export type AppState = { export type AppState = {
settings: AppSettings settings: AppSettings;
preferences: UserPreferences preferences: UserPreferences;
customTheme: ThemeColors customTheme: ThemeColors;
} };
export type ThemeMode = "dark" | "light" export type ThemeMode = "dark" | "light";
export type ThemeVariantValue = Variant export type ThemeVariantValue = Variant;
export type ThemeDefinition = ThemeJson export type ThemeDefinition = ThemeJson;

View File

@@ -3,46 +3,54 @@
* Handles dynamic theme switching by updating CSS custom properties * Handles dynamic theme switching by updating CSS custom properties
*/ */
import { RGBA, type TerminalColors } from "@opentui/core" import { RGBA, type TerminalColors } from "@opentui/core";
import type { ThemeColors } from "../types/settings" import type { ThemeColors } from "../types/settings";
import type { ColorValue, ThemeJson } from "../types/theme-schema" import type { ColorValue, ThemeJson } from "../types/theme-schema";
import { THEME_JSON } from "../constants/themes" import { THEME_JSON } from "../constants/themes";
import { getCustomThemes } from "./custom-themes" import { getCustomThemes } from "./custom-themes";
import { resolveTheme as resolveThemeJson } from "./theme-resolver" import { resolveTheme as resolveThemeJson } from "./theme-resolver";
import { generateSystemTheme } from "./system-theme" import { generateSystemTheme } from "./system-theme";
const toCss = (value: ColorValue | RGBA) => { const toCss = (value: ColorValue | RGBA) => {
if (value instanceof RGBA) { if (value instanceof RGBA) {
const r = Math.round(value.r * 255) const r = Math.round(value.r * 255);
const g = Math.round(value.g * 255) const g = Math.round(value.g * 255);
const b = Math.round(value.b * 255) const b = Math.round(value.b * 255);
return `rgba(${r}, ${g}, ${b}, ${value.a})` return `rgba(${r}, ${g}, ${b}, ${value.a})`;
}
if (typeof value === "number") return `var(--ansi-${value})`
if (typeof value === "string") return value
return value.dark
} }
if (typeof value === "number") return `var(--ansi-${value})`;
if (typeof value === "string") return value;
return value.dark;
};
export function applyTheme(theme: ThemeColors | Record<string, RGBA>) { export function applyTheme(theme: ThemeColors | Record<string, RGBA>) {
if (typeof document === "undefined") return if (typeof document === "undefined") return;
const root = document.documentElement const root = document.documentElement;
root.style.setProperty("--color-background", toCss(theme.background as ColorValue)) root.style.setProperty(
root.style.setProperty("--color-surface", toCss(theme.surface as ColorValue)) "--color-background",
root.style.setProperty("--color-primary", toCss(theme.primary as ColorValue)) toCss(theme.background as ColorValue),
root.style.setProperty("--color-secondary", toCss(theme.secondary as ColorValue)) );
root.style.setProperty("--color-accent", toCss(theme.accent as ColorValue)) root.style.setProperty("--color-surface", toCss(theme.surface as ColorValue));
root.style.setProperty("--color-text", toCss(theme.text as ColorValue)) root.style.setProperty("--color-primary", toCss(theme.primary as ColorValue));
root.style.setProperty("--color-muted", toCss(theme.muted as ColorValue)) root.style.setProperty(
root.style.setProperty("--color-warning", toCss(theme.warning as ColorValue)) "--color-secondary",
root.style.setProperty("--color-error", toCss(theme.error as ColorValue)) toCss(theme.secondary as ColorValue),
root.style.setProperty("--color-success", toCss(theme.success as ColorValue)) );
root.style.setProperty("--color-accent", toCss(theme.accent as ColorValue));
root.style.setProperty("--color-text", toCss(theme.text as ColorValue));
root.style.setProperty("--color-muted", toCss(theme.muted as ColorValue));
root.style.setProperty("--color-warning", toCss(theme.warning as ColorValue));
root.style.setProperty("--color-error", toCss(theme.error as ColorValue));
root.style.setProperty("--color-success", toCss(theme.success as ColorValue));
const layers = theme.layerBackgrounds as Record<string, ColorValue> | undefined const layers = theme.layerBackgrounds as
| Record<string, ColorValue>
| undefined;
if (layers) { if (layers) {
root.style.setProperty("--color-layer0", toCss(layers.layer0)) root.style.setProperty("--color-layer0", toCss(layers.layer0));
root.style.setProperty("--color-layer1", toCss(layers.layer1)) root.style.setProperty("--color-layer1", toCss(layers.layer1));
root.style.setProperty("--color-layer2", toCss(layers.layer2)) root.style.setProperty("--color-layer2", toCss(layers.layer2));
root.style.setProperty("--color-layer3", toCss(layers.layer3)) root.style.setProperty("--color-layer3", toCss(layers.layer3));
} }
} }
@@ -50,46 +58,46 @@ export function applyTheme(theme: ThemeColors | Record<string, RGBA>) {
* Get theme mode from system preference * Get theme mode from system preference
*/ */
export function getSystemThemeMode(): "dark" | "light" { export function getSystemThemeMode(): "dark" | "light" {
if (typeof window === "undefined") return "dark" if (typeof window === "undefined") return "dark";
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
return prefersDark ? "dark" : "light" return prefersDark ? "dark" : "light";
} }
/** /**
* Apply CSS variable data-theme attribute * Apply CSS variable data-theme attribute
*/ */
export function setThemeAttribute(themeName: string) { export function setThemeAttribute(themeName: string) {
if (typeof document === "undefined") return if (typeof document === "undefined") return;
const root = document.documentElement const root = document.documentElement;
root.setAttribute("data-theme", themeName) root.setAttribute("data-theme", themeName);
} }
export async function loadThemes() { export async function loadThemes() {
return await getCustomThemes() return await getCustomThemes();
} }
export async function loadTheme(name: string) { export async function loadTheme(name: string) {
const themes = await loadThemes() const themes = await loadThemes();
return themes[name] return themes[name];
} }
export function resolveTheme(theme: ThemeJson, mode: "dark" | "light") { export function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
return resolveThemeJson(theme, mode) return resolveThemeJson(theme, mode);
} }
export function resolveTerminalTheme( export function resolveTerminalTheme(
themes: Record<string, ThemeJson>, themes: Record<string, ThemeJson>,
name: string, name: string,
mode: "dark" | "light", mode: "dark" | "light",
system?: TerminalColors system?: TerminalColors,
) { ) {
if (name === "system" && system) { if (name === "system" && system) {
return resolveThemeJson(generateSystemTheme(system, mode), mode) return resolveThemeJson(generateSystemTheme(system, mode), mode);
} }
const theme = themes[name] ?? themes.opencode const theme = themes[name] ?? themes.catppuccin;
if (!theme) { if (!theme) {
return resolveThemeJson(THEME_JSON.opencode, mode) return resolveThemeJson(THEME_JSON.catppuccin, mode);
} }
return resolveThemeJson(theme, mode) return resolveThemeJson(theme, mode);
} }