understanding
This commit is contained in:
@@ -50,7 +50,7 @@ export function Layout(props: LayoutProps) {
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
height="100%"
|
||||
backgroundColor={theme.background}
|
||||
backgroundColor={theme.surface}
|
||||
>
|
||||
{/* Header - tab bar */}
|
||||
<Show when={props.header}>
|
||||
@@ -119,18 +119,6 @@ export function Layout(props: LayoutProps) {
|
||||
)}
|
||||
</For>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import type { ThemeColors, ThemeDefinition, ThemeName } from "../types/settings"
|
||||
import { 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" }
|
||||
import opencode from "../themes/opencode.json" with { type: "json" }
|
||||
import type {
|
||||
ThemeColors,
|
||||
ThemeDefinition,
|
||||
ThemeName,
|
||||
} from "../types/settings";
|
||||
import {
|
||||
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 = {
|
||||
...BASE_THEME_COLORS,
|
||||
layerBackgrounds: BASE_LAYER_BACKGROUND,
|
||||
}
|
||||
};
|
||||
|
||||
export const THEME_JSON: Record<string, ThemeDefinition> = {
|
||||
opencode: opencode as ThemeDefinition,
|
||||
catppuccin: catppuccin as ThemeDefinition,
|
||||
gruvbox: gruvbox as ThemeDefinition,
|
||||
tokyo: tokyo as ThemeDefinition,
|
||||
nord: nord as ThemeDefinition,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,9 +93,6 @@ type ThemeResolved = {
|
||||
* This ensures children are NOT rendered until the theme is ready,
|
||||
* 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({
|
||||
name: "Theme",
|
||||
@@ -121,8 +118,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
// If custom themes fail to load, fall back to opencode theme
|
||||
setStore("active", "opencode");
|
||||
setStore("active", "catppuccin");
|
||||
})
|
||||
.finally(() => {
|
||||
// Only set ready if not waiting for system theme
|
||||
@@ -206,7 +202,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
if (store.active === "system") {
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.active = "opencode";
|
||||
draft.active = "catppuccin";
|
||||
draft.ready = true;
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import type { Feed, FeedVisibility } from "@/types/feed";
|
||||
import { format } from "date-fns";
|
||||
import { htmlToText } from "@/utils/html-to-text";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
|
||||
interface FeedItemProps {
|
||||
feed: Feed;
|
||||
@@ -36,6 +38,7 @@ export function FeedItem(props: FeedItemProps) {
|
||||
const pinnedIndicator = () => {
|
||||
return props.feed.isPinned ? "*" : " ";
|
||||
};
|
||||
const { theme } = useTheme();
|
||||
|
||||
if (props.compact) {
|
||||
// Compact single-line view
|
||||
@@ -51,8 +54,8 @@ export function FeedItem(props: FeedItemProps) {
|
||||
{props.isSelected ? ">" : " "}
|
||||
</text>
|
||||
<text fg={visibilityColor()}>{visibilityIcon()}</text>
|
||||
<text fg={props.isSelected ? "white" : undefined}>
|
||||
{props.feed.customName || props.feed.podcast.title}
|
||||
<text fg={props.isSelected ? "white" : theme.accent}>
|
||||
{htmlToText(props.feed.customName || props.feed.podcast.title)}
|
||||
</text>
|
||||
{props.showEpisodeCount && <text fg="gray">({episodeCount()})</text>}
|
||||
</box>
|
||||
@@ -76,12 +79,13 @@ export function FeedItem(props: FeedItemProps) {
|
||||
</text>
|
||||
<text fg={visibilityColor()}>{visibilityIcon()}</text>
|
||||
<text fg="yellow">{pinnedIndicator()}</text>
|
||||
<text fg={props.isSelected ? "white" : undefined}>
|
||||
<strong>{props.feed.customName || props.feed.podcast.title}</strong>
|
||||
<text fg={props.isSelected ? "white" : theme.text}>
|
||||
<strong>
|
||||
{htmlToText(props.feed.customName || props.feed.podcast.title)}
|
||||
</strong>
|
||||
</text>
|
||||
</box>
|
||||
|
||||
{/* Details row */}
|
||||
<box flexDirection="row" gap={2} paddingLeft={4}>
|
||||
{props.showEpisodeCount && (
|
||||
<text fg="gray">
|
||||
@@ -93,7 +97,6 @@ export function FeedItem(props: FeedItemProps) {
|
||||
)}
|
||||
</box>
|
||||
|
||||
{/* Description (truncated) */}
|
||||
{props.feed.podcast.description && (
|
||||
<box paddingLeft={4} paddingTop={0}>
|
||||
<text fg="gray">
|
||||
|
||||
@@ -9,6 +9,8 @@ import { useFeedStore } from "@/stores/feed";
|
||||
import { format } from "date-fns";
|
||||
import type { Episode } from "@/types/episode";
|
||||
import type { Feed } from "@/types/feed";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { htmlToText } from "@/utils/html-to-text";
|
||||
|
||||
type FeedPageProps = {
|
||||
focused: boolean;
|
||||
@@ -67,8 +69,13 @@ export function FeedPage(props: FeedPageProps) {
|
||||
}
|
||||
});
|
||||
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<box flexDirection="column" height="100%">
|
||||
<box
|
||||
backgroundColor={theme.background}
|
||||
flexDirection="column"
|
||||
height="100%"
|
||||
>
|
||||
{/* Status line */}
|
||||
<Show when={isRefreshing()}>
|
||||
<text fg="yellow">Refreshing feeds...</text>
|
||||
@@ -104,7 +111,7 @@ export function FeedPage(props: FeedPageProps) {
|
||||
<text fg={index() === selectedIndex() ? "cyan" : "gray"}>
|
||||
{index() === selectedIndex() ? ">" : " "}
|
||||
</text>
|
||||
<text fg={index() === selectedIndex() ? "white" : undefined}>
|
||||
<text fg={index() === selectedIndex() ? "white" : theme.text}>
|
||||
{item.episode.title}
|
||||
</text>
|
||||
</box>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "transparent",
|
||||
"background": "#181825",
|
||||
"surface": "#1e1e2e",
|
||||
"primary": "#89b4fa",
|
||||
"secondary": "#cba6f7",
|
||||
@@ -11,7 +10,7 @@
|
||||
"warning": "#fab387",
|
||||
"error": "#f38ba8",
|
||||
"success": "#a6e3a1",
|
||||
"layer0": "transparent",
|
||||
"layer0": "#181825",
|
||||
"layer1": "#181825",
|
||||
"layer2": "#11111b",
|
||||
"layer3": "#0a0a0f"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "transparent",
|
||||
"background": "#282828",
|
||||
"surface": "#282828",
|
||||
"primary": "#fabd2f",
|
||||
"secondary": "#83a598",
|
||||
@@ -11,7 +10,7 @@
|
||||
"warning": "#fabd2f",
|
||||
"error": "#fb4934",
|
||||
"success": "#b8bb26",
|
||||
"layer0": "transparent",
|
||||
"layer0": "#282828",
|
||||
"layer1": "#32302a",
|
||||
"layer2": "#1d2021",
|
||||
"layer3": "#0d0c0c"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "transparent",
|
||||
"background": "#2e3440",
|
||||
"surface": "#2e3440",
|
||||
"primary": "#88c0d0",
|
||||
"secondary": "#81a1c1",
|
||||
@@ -11,7 +10,7 @@
|
||||
"warning": "#ebcb8b",
|
||||
"error": "#bf616a",
|
||||
"success": "#a3be8c",
|
||||
"layer0": "transparent",
|
||||
"layer0": "#2e3440",
|
||||
"layer1": "#3b4252",
|
||||
"layer2": "#242933",
|
||||
"layer3": "#1a1c23"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {},
|
||||
"theme": {
|
||||
"primary": "#000000",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "transparent",
|
||||
"background": "#0f0f15",
|
||||
"surface": "#1a1b26",
|
||||
"primary": "#7aa2f7",
|
||||
"secondary": "#bb9af7",
|
||||
@@ -11,7 +10,7 @@
|
||||
"warning": "#e0af68",
|
||||
"error": "#f7768e",
|
||||
"success": "#9ece6a",
|
||||
"layer0": "transparent",
|
||||
"layer0": "#0f0f15",
|
||||
"layer1": "#16161e",
|
||||
"layer2": "#0f0f15",
|
||||
"layer3": "#08080b"
|
||||
|
||||
@@ -1,83 +1,89 @@
|
||||
import type { RGBA } from "@opentui/core"
|
||||
import type { ColorValue, ThemeJson, Variant } from "./theme-schema"
|
||||
import type { RGBA } from "@opentui/core";
|
||||
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 = {
|
||||
layer0: ColorValue
|
||||
layer1: ColorValue
|
||||
layer2: ColorValue
|
||||
layer3: ColorValue
|
||||
}
|
||||
layer0: ColorValue;
|
||||
layer1: ColorValue;
|
||||
layer2: ColorValue;
|
||||
layer3: ColorValue;
|
||||
};
|
||||
|
||||
export type ThemeColors = {
|
||||
background: ColorValue
|
||||
surface: ColorValue
|
||||
primary: ColorValue
|
||||
secondary: ColorValue
|
||||
accent: ColorValue
|
||||
text: ColorValue
|
||||
muted: ColorValue
|
||||
warning: ColorValue
|
||||
error: ColorValue
|
||||
success: ColorValue
|
||||
layerBackgrounds?: LayerBackgrounds
|
||||
}
|
||||
background: ColorValue;
|
||||
surface: ColorValue;
|
||||
primary: ColorValue;
|
||||
secondary: ColorValue;
|
||||
accent: ColorValue;
|
||||
text: ColorValue;
|
||||
muted: ColorValue;
|
||||
warning: ColorValue;
|
||||
error: ColorValue;
|
||||
success: ColorValue;
|
||||
layerBackgrounds?: LayerBackgrounds;
|
||||
};
|
||||
|
||||
export type ThemeVariant = {
|
||||
name: string
|
||||
colors: ThemeColors
|
||||
}
|
||||
name: string;
|
||||
colors: ThemeColors;
|
||||
};
|
||||
|
||||
export type ThemeToken = {
|
||||
[key: string]: string
|
||||
}
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export type ResolvedTheme = Record<string, RGBA> & {
|
||||
layerBackgrounds: Record<string, RGBA>
|
||||
_hasSelectedListItemText: boolean
|
||||
thinkingOpacity: number
|
||||
}
|
||||
layerBackgrounds: Record<string, RGBA>;
|
||||
_hasSelectedListItemText: boolean;
|
||||
thinkingOpacity: number;
|
||||
};
|
||||
|
||||
export type DesktopTheme = {
|
||||
name: string
|
||||
variants: ThemeVariant[]
|
||||
defaultVariant: string
|
||||
tokens: ThemeToken
|
||||
}
|
||||
name: string;
|
||||
variants: ThemeVariant[];
|
||||
defaultVariant: string;
|
||||
tokens: ThemeToken;
|
||||
};
|
||||
|
||||
export type VisualizerSettings = {
|
||||
/** Number of frequency bars (8–128, default: 32) */
|
||||
bars: number
|
||||
bars: number;
|
||||
/** Automatic sensitivity: 1 = enabled, 0 = disabled (default: 1) */
|
||||
sensitivity: number
|
||||
sensitivity: number;
|
||||
/** Noise reduction factor 0.0–1.0 (default: 0.77) */
|
||||
noiseReduction: number
|
||||
noiseReduction: number;
|
||||
/** Low frequency cutoff in Hz (default: 50) */
|
||||
lowCutOff: number
|
||||
lowCutOff: number;
|
||||
/** High frequency cutoff in Hz (default: 10000) */
|
||||
highCutOff: number
|
||||
}
|
||||
highCutOff: number;
|
||||
};
|
||||
|
||||
export type AppSettings = {
|
||||
theme: ThemeName
|
||||
fontSize: number
|
||||
playbackSpeed: number
|
||||
downloadPath: string
|
||||
visualizer: VisualizerSettings
|
||||
}
|
||||
theme: ThemeName;
|
||||
fontSize: number;
|
||||
playbackSpeed: number;
|
||||
downloadPath: string;
|
||||
visualizer: VisualizerSettings;
|
||||
};
|
||||
|
||||
export type UserPreferences = {
|
||||
showExplicit: boolean
|
||||
autoDownload: boolean
|
||||
}
|
||||
showExplicit: boolean;
|
||||
autoDownload: boolean;
|
||||
};
|
||||
|
||||
export type AppState = {
|
||||
settings: AppSettings
|
||||
preferences: UserPreferences
|
||||
customTheme: ThemeColors
|
||||
}
|
||||
settings: AppSettings;
|
||||
preferences: UserPreferences;
|
||||
customTheme: ThemeColors;
|
||||
};
|
||||
|
||||
export type ThemeMode = "dark" | "light"
|
||||
export type ThemeVariantValue = Variant
|
||||
export type ThemeDefinition = ThemeJson
|
||||
export type ThemeMode = "dark" | "light";
|
||||
export type ThemeVariantValue = Variant;
|
||||
export type ThemeDefinition = ThemeJson;
|
||||
|
||||
@@ -3,46 +3,54 @@
|
||||
* Handles dynamic theme switching by updating CSS custom properties
|
||||
*/
|
||||
|
||||
import { RGBA, type TerminalColors } from "@opentui/core"
|
||||
import type { ThemeColors } from "../types/settings"
|
||||
import type { ColorValue, ThemeJson } from "../types/theme-schema"
|
||||
import { THEME_JSON } from "../constants/themes"
|
||||
import { getCustomThemes } from "./custom-themes"
|
||||
import { resolveTheme as resolveThemeJson } from "./theme-resolver"
|
||||
import { generateSystemTheme } from "./system-theme"
|
||||
import { RGBA, type TerminalColors } from "@opentui/core";
|
||||
import type { ThemeColors } from "../types/settings";
|
||||
import type { ColorValue, ThemeJson } from "../types/theme-schema";
|
||||
import { THEME_JSON } from "../constants/themes";
|
||||
import { getCustomThemes } from "./custom-themes";
|
||||
import { resolveTheme as resolveThemeJson } from "./theme-resolver";
|
||||
import { generateSystemTheme } from "./system-theme";
|
||||
|
||||
const toCss = (value: ColorValue | RGBA) => {
|
||||
if (value instanceof RGBA) {
|
||||
const r = Math.round(value.r * 255)
|
||||
const g = Math.round(value.g * 255)
|
||||
const b = Math.round(value.b * 255)
|
||||
return `rgba(${r}, ${g}, ${b}, ${value.a})`
|
||||
const r = Math.round(value.r * 255);
|
||||
const g = Math.round(value.g * 255);
|
||||
const b = Math.round(value.b * 255);
|
||||
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>) {
|
||||
if (typeof document === "undefined") return
|
||||
const root = document.documentElement
|
||||
root.style.setProperty("--color-background", toCss(theme.background as ColorValue))
|
||||
root.style.setProperty("--color-surface", toCss(theme.surface as ColorValue))
|
||||
root.style.setProperty("--color-primary", toCss(theme.primary 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-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))
|
||||
if (typeof document === "undefined") return;
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty(
|
||||
"--color-background",
|
||||
toCss(theme.background as ColorValue),
|
||||
);
|
||||
root.style.setProperty("--color-surface", toCss(theme.surface as ColorValue));
|
||||
root.style.setProperty("--color-primary", toCss(theme.primary 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-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) {
|
||||
root.style.setProperty("--color-layer0", toCss(layers.layer0))
|
||||
root.style.setProperty("--color-layer1", toCss(layers.layer1))
|
||||
root.style.setProperty("--color-layer2", toCss(layers.layer2))
|
||||
root.style.setProperty("--color-layer3", toCss(layers.layer3))
|
||||
root.style.setProperty("--color-layer0", toCss(layers.layer0));
|
||||
root.style.setProperty("--color-layer1", toCss(layers.layer1));
|
||||
root.style.setProperty("--color-layer2", toCss(layers.layer2));
|
||||
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
|
||||
*/
|
||||
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
|
||||
return prefersDark ? "dark" : "light"
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
return prefersDark ? "dark" : "light";
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply CSS variable data-theme attribute
|
||||
*/
|
||||
export function setThemeAttribute(themeName: string) {
|
||||
if (typeof document === "undefined") return
|
||||
const root = document.documentElement
|
||||
root.setAttribute("data-theme", themeName)
|
||||
if (typeof document === "undefined") return;
|
||||
const root = document.documentElement;
|
||||
root.setAttribute("data-theme", themeName);
|
||||
}
|
||||
|
||||
export async function loadThemes() {
|
||||
return await getCustomThemes()
|
||||
return await getCustomThemes();
|
||||
}
|
||||
|
||||
export async function loadTheme(name: string) {
|
||||
const themes = await loadThemes()
|
||||
return themes[name]
|
||||
const themes = await loadThemes();
|
||||
return themes[name];
|
||||
}
|
||||
|
||||
export function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
||||
return resolveThemeJson(theme, mode)
|
||||
return resolveThemeJson(theme, mode);
|
||||
}
|
||||
|
||||
export function resolveTerminalTheme(
|
||||
themes: Record<string, ThemeJson>,
|
||||
name: string,
|
||||
mode: "dark" | "light",
|
||||
system?: TerminalColors
|
||||
system?: TerminalColors,
|
||||
) {
|
||||
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) {
|
||||
return resolveThemeJson(THEME_JSON.opencode, mode)
|
||||
return resolveThemeJson(THEME_JSON.catppuccin, mode);
|
||||
}
|
||||
return resolveThemeJson(theme, mode)
|
||||
return resolveThemeJson(theme, mode);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user