understanding
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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": {},
|
"defs": {},
|
||||||
"theme": {
|
"theme": {
|
||||||
"primary": "#000000",
|
"primary": "#000000",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 (8–128, default: 32) */
|
/** Number of frequency bars (8–128, 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.0–1.0 (default: 0.77) */
|
/** Noise reduction factor 0.0–1.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;
|
||||||
|
|||||||
@@ -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 === "number") return `var(--ansi-${value})`;
|
||||||
if (typeof value === "string") return value
|
if (typeof value === "string") return value;
|
||||||
return value.dark
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user