still self referencing
This commit is contained in:
21
src/App.tsx
21
src/App.tsx
@@ -11,7 +11,6 @@ import { SearchPage } from "./components/SearchPage";
|
|||||||
import { DiscoverPage } from "./components/DiscoverPage";
|
import { DiscoverPage } from "./components/DiscoverPage";
|
||||||
import { Player } from "./components/Player";
|
import { Player } from "./components/Player";
|
||||||
import { SettingsScreen } from "./components/SettingsScreen";
|
import { SettingsScreen } from "./components/SettingsScreen";
|
||||||
import { ThemeProvider } from "./context/ThemeContext";
|
|
||||||
import { useAuthStore } from "./stores/auth";
|
import { useAuthStore } from "./stores/auth";
|
||||||
import { useFeedStore } from "./stores/feed";
|
import { useFeedStore } from "./stores/feed";
|
||||||
import { useAppStore } from "./stores/app";
|
import { useAppStore } from "./stores/app";
|
||||||
@@ -185,16 +184,14 @@ export function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<Layout
|
||||||
<Layout
|
layerDepth={layerDepth()}
|
||||||
layerDepth={layerDepth()}
|
header={
|
||||||
header={
|
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} />
|
||||||
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} />
|
}
|
||||||
}
|
footer={<Navigation activeTab={activeTab()} onTabSelect={setActiveTab} />}
|
||||||
footer={<Navigation activeTab={activeTab()} onTabSelect={setActiveTab} />}
|
>
|
||||||
>
|
<box style={{ padding: 1 }}>{renderContent()}</box>
|
||||||
<box style={{ padding: 1 }}>{renderContent()}</box>
|
</Layout>
|
||||||
</Layout>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useRenderer } from "@opentui/solid"
|
import { useTheme } from "../context/ThemeContext"
|
||||||
|
|
||||||
export function LayerIndicator({ layerDepth }: { layerDepth: number }) {
|
export function LayerIndicator({ layerDepth }: { layerDepth: number }) {
|
||||||
const renderer = useRenderer()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const getLayerIndicator = () => {
|
const getLayerIndicator = () => {
|
||||||
const indicators = []
|
const indicators = []
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
const isActive = i <= layerDepth
|
const isActive = i <= layerDepth
|
||||||
const color = isActive ? "var(--color-accent)" : "var(--color-muted)"
|
const color = isActive ? theme.accent : theme.textMuted
|
||||||
const size = isActive ? "●" : "○"
|
const size = isActive ? "●" : "○"
|
||||||
indicators.push(
|
indicators.push(
|
||||||
<text fg={color} marginRight={1}>
|
<text fg={color} marginRight={1}>
|
||||||
@@ -20,9 +20,9 @@ export function LayerIndicator({ layerDepth }: { layerDepth: number }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<box flexDirection="row" alignItems="center">
|
<box flexDirection="row" alignItems="center">
|
||||||
<text fg="var(--color-muted)" marginRight={1}>Depth:</text>
|
<text fg={theme.textMuted} marginRight={1}>Depth:</text>
|
||||||
{getLayerIndicator()}
|
{getLayerIndicator()}
|
||||||
<text fg="var(--color-muted)" marginLeft={1}>
|
<text fg={theme.textMuted} marginLeft={1}>
|
||||||
{layerDepth}
|
{layerDepth}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
@@ -1,41 +1,34 @@
|
|||||||
import type { JSX } from "solid-js"
|
import type { JSX } from "solid-js"
|
||||||
import type { ThemeColors } from "../types/settings"
|
import type { RGBA } from "@opentui/core"
|
||||||
import type { ColorValue } from "../types/theme-schema"
|
import { useTheme } from "../context/ThemeContext"
|
||||||
import { resolveColorReference } from "../utils/theme-css"
|
|
||||||
import { LayerIndicator } from "./LayerIndicator"
|
import { LayerIndicator } from "./LayerIndicator"
|
||||||
|
|
||||||
type LayerConfig = {
|
type LayerConfig = {
|
||||||
depth: number
|
depth: number
|
||||||
background: string
|
background: RGBA
|
||||||
}
|
}
|
||||||
|
|
||||||
type LayoutProps = {
|
type LayoutProps = {
|
||||||
header?: JSX.Element
|
header?: JSX.Element
|
||||||
footer?: JSX.Element
|
footer?: JSX.Element
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
theme?: ThemeColors
|
|
||||||
layerDepth?: number
|
layerDepth?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Layout(props: LayoutProps) {
|
export function Layout(props: LayoutProps) {
|
||||||
const theme = props.theme
|
const { theme } = useTheme()
|
||||||
const toColor = (value?: ColorValue) => (value ? resolveColorReference(value) : undefined)
|
|
||||||
|
|
||||||
// Get layer configuration based on depth
|
// Get layer configuration based on depth
|
||||||
const getLayerConfig = (depth: number): LayerConfig => {
|
const getLayerConfig = (depth: number): LayerConfig => {
|
||||||
if (!theme?.layerBackgrounds) {
|
|
||||||
return { depth: 0, background: "transparent" }
|
|
||||||
}
|
|
||||||
|
|
||||||
const backgrounds = theme.layerBackgrounds
|
const backgrounds = theme.layerBackgrounds
|
||||||
const depthMap: Record<number, LayerConfig> = {
|
const depthMap: Record<number, LayerConfig> = {
|
||||||
0: { depth: 0, background: resolveColorReference(backgrounds.layer0) },
|
0: { depth: 0, background: backgrounds?.layer0 ?? theme.background },
|
||||||
1: { depth: 1, background: resolveColorReference(backgrounds.layer1) },
|
1: { depth: 1, background: backgrounds?.layer1 ?? theme.backgroundPanel },
|
||||||
2: { depth: 2, background: resolveColorReference(backgrounds.layer2) },
|
2: { depth: 2, background: backgrounds?.layer2 ?? theme.backgroundElement },
|
||||||
3: { depth: 3, background: resolveColorReference(backgrounds.layer3) },
|
3: { depth: 3, background: backgrounds?.layer3 ?? theme.backgroundMenu },
|
||||||
}
|
}
|
||||||
|
|
||||||
return depthMap[depth] || { depth: 0, background: "transparent" }
|
return depthMap[depth] || { depth: 0, background: theme.background }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current layer background
|
// Get current layer background
|
||||||
@@ -46,14 +39,14 @@ export function Layout(props: LayoutProps) {
|
|||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
backgroundColor={toColor(theme?.background)}
|
backgroundColor={theme.background}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
{props.header ? (
|
{props.header ? (
|
||||||
<box
|
<box
|
||||||
style={{
|
style={{
|
||||||
height: 4,
|
height: 4,
|
||||||
backgroundColor: toColor(theme?.surface),
|
backgroundColor: theme.surface ?? theme.backgroundPanel,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<box style={{ padding: 1 }}>
|
<box style={{ padding: 1 }}>
|
||||||
@@ -83,7 +76,7 @@ export function Layout(props: LayoutProps) {
|
|||||||
<box
|
<box
|
||||||
style={{
|
style={{
|
||||||
height: 2,
|
height: 2,
|
||||||
backgroundColor: toColor(theme?.surface),
|
backgroundColor: theme.surface ?? theme.backgroundPanel,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<box style={{ padding: 1 }}>
|
<box style={{ padding: 1 }}>
|
||||||
@@ -99,7 +92,7 @@ export function Layout(props: LayoutProps) {
|
|||||||
<box
|
<box
|
||||||
style={{
|
style={{
|
||||||
height: 1,
|
height: 1,
|
||||||
backgroundColor: toColor(theme?.surface),
|
backgroundColor: theme.surface ?? theme.backgroundPanel,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<box style={{ padding: 1 }}>
|
<box style={{ padding: 1 }}>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { createSignal } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
import { useAuthStore } from "../stores/auth"
|
import { useAuthStore } from "../stores/auth"
|
||||||
|
import { useTheme } from "../context/ThemeContext"
|
||||||
import { AUTH_CONFIG } from "../config/auth"
|
import { AUTH_CONFIG } from "../config/auth"
|
||||||
|
|
||||||
interface LoginScreenProps {
|
interface LoginScreenProps {
|
||||||
@@ -17,6 +18,7 @@ type FocusField = "email" | "password" | "submit" | "code" | "oauth"
|
|||||||
|
|
||||||
export function LoginScreen(props: LoginScreenProps) {
|
export function LoginScreen(props: LoginScreenProps) {
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const { theme } = useTheme()
|
||||||
const [email, setEmail] = createSignal("")
|
const [email, setEmail] = createSignal("")
|
||||||
const [password, setPassword] = createSignal("")
|
const [password, setPassword] = createSignal("")
|
||||||
const [focusField, setFocusField] = createSignal<FocusField>("email")
|
const [focusField, setFocusField] = createSignal<FocusField>("email")
|
||||||
@@ -90,7 +92,7 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
|
|
||||||
{/* Email field */}
|
{/* Email field */}
|
||||||
<box flexDirection="column" gap={0}>
|
<box flexDirection="column" gap={0}>
|
||||||
<text fg={focusField() === "email" ? "var(--color-primary)" : undefined}>Email:</text>
|
<text fg={focusField() === "email" ? theme.primary : undefined}>Email:</text>
|
||||||
<input
|
<input
|
||||||
value={email()}
|
value={email()}
|
||||||
onInput={setEmail}
|
onInput={setEmail}
|
||||||
@@ -99,13 +101,13 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
width={30}
|
width={30}
|
||||||
/>
|
/>
|
||||||
{emailError() && (
|
{emailError() && (
|
||||||
<text fg="var(--color-error)">{emailError()}</text>
|
<text fg={theme.error}>{emailError()}</text>
|
||||||
)}
|
)}
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
{/* Password field */}
|
{/* Password field */}
|
||||||
<box flexDirection="column" gap={0}>
|
<box flexDirection="column" gap={0}>
|
||||||
<text fg={focusField() === "password" ? "var(--color-primary)" : undefined}>
|
<text fg={focusField() === "password" ? theme.primary : undefined}>
|
||||||
Password:
|
Password:
|
||||||
</text>
|
</text>
|
||||||
<input
|
<input
|
||||||
@@ -116,7 +118,7 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
width={30}
|
width={30}
|
||||||
/>
|
/>
|
||||||
{passwordError() && (
|
{passwordError() && (
|
||||||
<text fg="var(--color-error)">{passwordError()}</text>
|
<text fg={theme.error}>{passwordError()}</text>
|
||||||
)}
|
)}
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
@@ -127,9 +129,9 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
padding={1}
|
padding={1}
|
||||||
backgroundColor={focusField() === "submit" ? "var(--color-primary)" : undefined}
|
backgroundColor={focusField() === "submit" ? theme.primary : undefined}
|
||||||
>
|
>
|
||||||
<text fg={focusField() === "submit" ? "var(--color-text)" : undefined}>
|
<text fg={focusField() === "submit" ? theme.text : undefined}>
|
||||||
{auth.isLoading ? "Signing in..." : "[Enter] Sign In"}
|
{auth.isLoading ? "Signing in..." : "[Enter] Sign In"}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
@@ -137,21 +139,21 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
|
|
||||||
{/* Auth error message */}
|
{/* Auth error message */}
|
||||||
{auth.error && (
|
{auth.error && (
|
||||||
<text fg="var(--color-error)">{auth.error.message}</text>
|
<text fg={theme.error}>{auth.error.message}</text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<box height={1} />
|
<box height={1} />
|
||||||
|
|
||||||
{/* Alternative auth options */}
|
{/* Alternative auth options */}
|
||||||
<text fg="var(--color-muted)">Or authenticate with:</text>
|
<text fg={theme.textMuted}>Or authenticate with:</text>
|
||||||
|
|
||||||
<box flexDirection="row" gap={2}>
|
<box flexDirection="row" gap={2}>
|
||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
padding={1}
|
padding={1}
|
||||||
backgroundColor={focusField() === "code" ? "var(--color-primary)" : undefined}
|
backgroundColor={focusField() === "code" ? theme.primary : undefined}
|
||||||
>
|
>
|
||||||
<text fg={focusField() === "code" ? "var(--color-accent)" : "var(--color-muted)"}>
|
<text fg={focusField() === "code" ? theme.accent : theme.textMuted}>
|
||||||
[C] Sync Code
|
[C] Sync Code
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
@@ -159,9 +161,9 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
padding={1}
|
padding={1}
|
||||||
backgroundColor={focusField() === "oauth" ? "var(--color-primary)" : undefined}
|
backgroundColor={focusField() === "oauth" ? theme.primary : undefined}
|
||||||
>
|
>
|
||||||
<text fg={focusField() === "oauth" ? "var(--color-accent)" : "var(--color-muted)"}>
|
<text fg={focusField() === "oauth" ? theme.accent : theme.textMuted}>
|
||||||
[O] OAuth Info
|
[O] OAuth Info
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
@@ -169,7 +171,7 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
|
|
||||||
<box height={1} />
|
<box height={1} />
|
||||||
|
|
||||||
<text fg="var(--color-muted)">Tab to navigate, Enter to select</text>
|
<text fg={theme.textMuted}>Tab to navigate, Enter to select</text>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createSignal } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
import { useKeyboard } from "@opentui/solid"
|
import { useKeyboard } from "@opentui/solid"
|
||||||
import { useAppStore } from "../stores/app"
|
import { useAppStore } from "../stores/app"
|
||||||
|
import { useTheme } from "../context/ThemeContext"
|
||||||
import type { ThemeName } from "../types/settings"
|
import type { ThemeName } from "../types/settings"
|
||||||
|
|
||||||
type FocusField = "theme" | "font" | "speed" | "explicit" | "auto"
|
type FocusField = "theme" | "font" | "speed" | "explicit" | "auto"
|
||||||
@@ -16,6 +17,7 @@ const THEME_LABELS: Array<{ value: ThemeName; label: string }> = [
|
|||||||
|
|
||||||
export function PreferencesPanel() {
|
export function PreferencesPanel() {
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { theme } = useTheme()
|
||||||
const [focusField, setFocusField] = createSignal<FocusField>("theme")
|
const [focusField, setFocusField] = createSignal<FocusField>("theme")
|
||||||
|
|
||||||
const settings = () => appStore.state().settings
|
const settings = () => appStore.state().settings
|
||||||
@@ -76,55 +78,55 @@ export function PreferencesPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<box flexDirection="column" gap={1}>
|
<box flexDirection="column" gap={1}>
|
||||||
<text fg="var(--color-muted)">Preferences</text>
|
<text fg={theme.textMuted}>Preferences</text>
|
||||||
|
|
||||||
<box flexDirection="column" gap={1}>
|
<box flexDirection="column" gap={1}>
|
||||||
<box flexDirection="row" gap={1} alignItems="center">
|
<box flexDirection="row" gap={1} alignItems="center">
|
||||||
<text fg={focusField() === "theme" ? "var(--color-primary)" : "var(--color-muted)"}>Theme:</text>
|
<text fg={focusField() === "theme" ? theme.primary : theme.textMuted}>Theme:</text>
|
||||||
<box border padding={0}>
|
<box border padding={0}>
|
||||||
<text fg="var(--color-text)">{THEME_LABELS.find((t) => t.value === settings().theme)?.label}</text>
|
<text fg={theme.text}>{THEME_LABELS.find((t) => t.value === settings().theme)?.label}</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg="var(--color-muted)">[Left/Right]</text>
|
<text fg={theme.textMuted}>[Left/Right]</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1} alignItems="center">
|
<box flexDirection="row" gap={1} alignItems="center">
|
||||||
<text fg={focusField() === "font" ? "var(--color-primary)" : "var(--color-muted)"}>Font Size:</text>
|
<text fg={focusField() === "font" ? theme.primary : theme.textMuted}>Font Size:</text>
|
||||||
<box border padding={0}>
|
<box border padding={0}>
|
||||||
<text fg="var(--color-text)">{settings().fontSize}px</text>
|
<text fg={theme.text}>{settings().fontSize}px</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg="var(--color-muted)">[Left/Right]</text>
|
<text fg={theme.textMuted}>[Left/Right]</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1} alignItems="center">
|
<box flexDirection="row" gap={1} alignItems="center">
|
||||||
<text fg={focusField() === "speed" ? "var(--color-primary)" : "var(--color-muted)"}>Playback:</text>
|
<text fg={focusField() === "speed" ? theme.primary : theme.textMuted}>Playback:</text>
|
||||||
<box border padding={0}>
|
<box border padding={0}>
|
||||||
<text fg="var(--color-text)">{settings().playbackSpeed}x</text>
|
<text fg={theme.text}>{settings().playbackSpeed}x</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg="var(--color-muted)">[Left/Right]</text>
|
<text fg={theme.textMuted}>[Left/Right]</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1} alignItems="center">
|
<box flexDirection="row" gap={1} alignItems="center">
|
||||||
<text fg={focusField() === "explicit" ? "var(--color-primary)" : "var(--color-muted)"}>Show Explicit:</text>
|
<text fg={focusField() === "explicit" ? theme.primary : theme.textMuted}>Show Explicit:</text>
|
||||||
<box border padding={0}>
|
<box border padding={0}>
|
||||||
<text fg={preferences().showExplicit ? "var(--color-success)" : "var(--color-muted)"}>
|
<text fg={preferences().showExplicit ? theme.success : theme.textMuted}>
|
||||||
{preferences().showExplicit ? "On" : "Off"}
|
{preferences().showExplicit ? "On" : "Off"}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg="var(--color-muted)">[Space]</text>
|
<text fg={theme.textMuted}>[Space]</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1} alignItems="center">
|
<box flexDirection="row" gap={1} alignItems="center">
|
||||||
<text fg={focusField() === "auto" ? "var(--color-primary)" : "var(--color-muted)"}>Auto Download:</text>
|
<text fg={focusField() === "auto" ? theme.primary : theme.textMuted}>Auto Download:</text>
|
||||||
<box border padding={0}>
|
<box border padding={0}>
|
||||||
<text fg={preferences().autoDownload ? "var(--color-success)" : "var(--color-muted)"}>
|
<text fg={preferences().autoDownload ? theme.success : theme.textMuted}>
|
||||||
{preferences().autoDownload ? "On" : "Off"}
|
{preferences().autoDownload ? "On" : "Off"}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg="var(--color-muted)">[Space]</text>
|
<text fg={theme.textMuted}>[Space]</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<text fg="var(--color-muted)">Tab to move focus, Left/Right to adjust</text>
|
<text fg={theme.textMuted}>Tab to move focus, Left/Right to adjust</text>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createSignal } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
import { useKeyboard } from "@opentui/solid"
|
import { useKeyboard } from "@opentui/solid"
|
||||||
import { SourceManager } from "./SourceManager"
|
import { SourceManager } from "./SourceManager"
|
||||||
|
import { useTheme } from "../context/ThemeContext"
|
||||||
import { PreferencesPanel } from "./PreferencesPanel"
|
import { PreferencesPanel } from "./PreferencesPanel"
|
||||||
import { SyncPanel } from "./SyncPanel"
|
import { SyncPanel } from "./SyncPanel"
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ const SECTIONS: Array<{ id: SectionId; label: string }> = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export function SettingsScreen(props: SettingsScreenProps) {
|
export function SettingsScreen(props: SettingsScreenProps) {
|
||||||
|
const { theme } = useTheme()
|
||||||
const [activeSection, setActiveSection] = createSignal<SectionId>("sync")
|
const [activeSection, setActiveSection] = createSignal<SectionId>("sync")
|
||||||
|
|
||||||
useKeyboard((key) => {
|
useKeyboard((key) => {
|
||||||
@@ -50,7 +52,7 @@ export function SettingsScreen(props: SettingsScreenProps) {
|
|||||||
<text>
|
<text>
|
||||||
<strong>Settings</strong>
|
<strong>Settings</strong>
|
||||||
</text>
|
</text>
|
||||||
<text fg="var(--color-muted)">[Tab] Switch section | 1-4 jump | Esc up</text>
|
<text fg={theme.textMuted}>[Tab] Switch section | 1-4 jump | Esc up</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
@@ -58,10 +60,10 @@ export function SettingsScreen(props: SettingsScreenProps) {
|
|||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
padding={0}
|
padding={0}
|
||||||
backgroundColor={activeSection() === section.id ? "var(--color-primary)" : undefined}
|
backgroundColor={activeSection() === section.id ? theme.primary : undefined}
|
||||||
onMouseDown={() => setActiveSection(section.id)}
|
onMouseDown={() => setActiveSection(section.id)}
|
||||||
>
|
>
|
||||||
<text fg={activeSection() === section.id ? "var(--color-text)" : "var(--color-muted)"}>
|
<text fg={activeSection() === section.id ? theme.text : theme.textMuted}>
|
||||||
[{index + 1}] {section.label}
|
[{index + 1}] {section.label}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
@@ -74,21 +76,21 @@ export function SettingsScreen(props: SettingsScreenProps) {
|
|||||||
{activeSection() === "preferences" && <PreferencesPanel />}
|
{activeSection() === "preferences" && <PreferencesPanel />}
|
||||||
{activeSection() === "account" && (
|
{activeSection() === "account" && (
|
||||||
<box flexDirection="column" gap={1}>
|
<box flexDirection="column" gap={1}>
|
||||||
<text fg="var(--color-muted)">Account</text>
|
<text fg={theme.textMuted}>Account</text>
|
||||||
<box flexDirection="row" gap={2} alignItems="center">
|
<box flexDirection="row" gap={2} alignItems="center">
|
||||||
<text fg="var(--color-muted)">Status:</text>
|
<text fg={theme.textMuted}>Status:</text>
|
||||||
<text fg={props.accountStatus === "signed-in" ? "var(--color-success)" : "var(--color-warning)"}>
|
<text fg={props.accountStatus === "signed-in" ? theme.success : theme.warning}>
|
||||||
{props.accountLabel}
|
{props.accountLabel}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<box border padding={0} onMouseDown={() => props.onOpenAccount?.()}>
|
<box border padding={0} onMouseDown={() => props.onOpenAccount?.()}>
|
||||||
<text fg="var(--color-primary)">[A] Manage Account</text>
|
<text fg={theme.primary}>[A] Manage Account</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<text fg="var(--color-muted)">Enter to dive | Esc up</text>
|
<text fg={theme.textMuted}>Enter to dive | Esc up</text>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { createSignal, For } from "solid-js"
|
import { createSignal, For } from "solid-js"
|
||||||
import { useFeedStore } from "../stores/feed"
|
import { useFeedStore } from "../stores/feed"
|
||||||
|
import { useTheme } from "../context/ThemeContext"
|
||||||
import { SourceType } from "../types/source"
|
import { SourceType } from "../types/source"
|
||||||
import type { PodcastSource } from "../types/source"
|
import type { PodcastSource } from "../types/source"
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ type FocusArea = "list" | "add" | "url" | "country" | "explicit" | "language"
|
|||||||
|
|
||||||
export function SourceManager(props: SourceManagerProps) {
|
export function SourceManager(props: SourceManagerProps) {
|
||||||
const feedStore = useFeedStore()
|
const feedStore = useFeedStore()
|
||||||
|
const { theme } = useTheme()
|
||||||
const [selectedIndex, setSelectedIndex] = createSignal(0)
|
const [selectedIndex, setSelectedIndex] = createSignal(0)
|
||||||
const [focusArea, setFocusArea] = createSignal<FocusArea>("list")
|
const [focusArea, setFocusArea] = createSignal<FocusArea>("list")
|
||||||
const [newSourceUrl, setNewSourceUrl] = createSignal("")
|
const [newSourceUrl, setNewSourceUrl] = createSignal("")
|
||||||
@@ -155,15 +157,15 @@ export function SourceManager(props: SourceManagerProps) {
|
|||||||
<strong>Podcast Sources</strong>
|
<strong>Podcast Sources</strong>
|
||||||
</text>
|
</text>
|
||||||
<box border padding={0} onMouseDown={props.onClose}>
|
<box border padding={0} onMouseDown={props.onClose}>
|
||||||
<text fg="var(--color-primary)">[Esc] Close</text>
|
<text fg={theme.primary}>[Esc] Close</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<text fg="var(--color-muted)">Manage where to search for podcasts</text>
|
<text fg={theme.textMuted}>Manage where to search for podcasts</text>
|
||||||
|
|
||||||
{/* Source list */}
|
{/* Source list */}
|
||||||
<box border padding={1} flexDirection="column" gap={1}>
|
<box border padding={1} flexDirection="column" gap={1}>
|
||||||
<text fg={focusArea() === "list" ? "var(--color-primary)" : "var(--color-muted)"}>Sources:</text>
|
<text fg={focusArea() === "list" ? theme.primary : theme.textMuted}>Sources:</text>
|
||||||
<scrollbox height={6}>
|
<scrollbox height={6}>
|
||||||
<For each={sources()}>
|
<For each={sources()}>
|
||||||
{(source, index) => (
|
{(source, index) => (
|
||||||
@@ -173,7 +175,7 @@ export function SourceManager(props: SourceManagerProps) {
|
|||||||
padding={0}
|
padding={0}
|
||||||
backgroundColor={
|
backgroundColor={
|
||||||
focusArea() === "list" && index() === selectedIndex()
|
focusArea() === "list" && index() === selectedIndex()
|
||||||
? "var(--color-primary)"
|
? theme.primary
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onMouseDown={() => {
|
onMouseDown={() => {
|
||||||
@@ -184,21 +186,21 @@ export function SourceManager(props: SourceManagerProps) {
|
|||||||
>
|
>
|
||||||
<text fg={
|
<text fg={
|
||||||
focusArea() === "list" && index() === selectedIndex()
|
focusArea() === "list" && index() === selectedIndex()
|
||||||
? "var(--color-primary)"
|
? theme.primary
|
||||||
: "var(--color-muted)"
|
: theme.textMuted
|
||||||
}>
|
}>
|
||||||
{focusArea() === "list" && index() === selectedIndex()
|
{focusArea() === "list" && index() === selectedIndex()
|
||||||
? ">"
|
? ">"
|
||||||
: " "}
|
: " "}
|
||||||
</text>
|
</text>
|
||||||
<text fg={source.enabled ? "var(--color-success)" : "var(--color-error)"}>
|
<text fg={source.enabled ? theme.success : theme.error}>
|
||||||
{source.enabled ? "[x]" : "[ ]"}
|
{source.enabled ? "[x]" : "[ ]"}
|
||||||
</text>
|
</text>
|
||||||
<text fg="var(--color-accent)">{getSourceIcon(source)}</text>
|
<text fg={theme.accent}>{getSourceIcon(source)}</text>
|
||||||
<text
|
<text
|
||||||
fg={
|
fg={
|
||||||
focusArea() === "list" && index() === selectedIndex()
|
focusArea() === "list" && index() === selectedIndex()
|
||||||
? "var(--color-text)"
|
? theme.text
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -208,54 +210,54 @@ export function SourceManager(props: SourceManagerProps) {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</scrollbox>
|
</scrollbox>
|
||||||
<text fg="var(--color-muted)">Space/Enter to toggle, d to delete, a to add</text>
|
<text fg={theme.textMuted}>Space/Enter to toggle, d to delete, a to add</text>
|
||||||
|
|
||||||
{/* API settings */}
|
{/* API settings */}
|
||||||
<box flexDirection="column" gap={1}>
|
<box flexDirection="column" gap={1}>
|
||||||
<text fg={isApiSource() ? "var(--color-muted)" : "var(--color-accent)"}>
|
<text fg={isApiSource() ? theme.textMuted : theme.accent}>
|
||||||
{isApiSource() ? "API Settings" : "API Settings (select an API source)"}
|
{isApiSource() ? "API Settings" : "API Settings (select an API source)"}
|
||||||
</text>
|
</text>
|
||||||
<box flexDirection="row" gap={2}>
|
<box flexDirection="row" gap={2}>
|
||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
padding={0}
|
padding={0}
|
||||||
backgroundColor={focusArea() === "country" ? "var(--color-primary)" : undefined}
|
backgroundColor={focusArea() === "country" ? theme.primary : undefined}
|
||||||
>
|
>
|
||||||
<text fg={focusArea() === "country" ? "var(--color-primary)" : "var(--color-muted)"}>
|
<text fg={focusArea() === "country" ? theme.primary : theme.textMuted}>
|
||||||
Country: {sourceCountry()}
|
Country: {sourceCountry()}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
padding={0}
|
padding={0}
|
||||||
backgroundColor={focusArea() === "language" ? "var(--color-primary)" : undefined}
|
backgroundColor={focusArea() === "language" ? theme.primary : undefined}
|
||||||
>
|
>
|
||||||
<text fg={focusArea() === "language" ? "var(--color-primary)" : "var(--color-muted)"}>
|
<text fg={focusArea() === "language" ? theme.primary : theme.textMuted}>
|
||||||
Language: {sourceLanguage() === "ja_jp" ? "Japanese" : "English"}
|
Language: {sourceLanguage() === "ja_jp" ? "Japanese" : "English"}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
padding={0}
|
padding={0}
|
||||||
backgroundColor={focusArea() === "explicit" ? "var(--color-primary)" : undefined}
|
backgroundColor={focusArea() === "explicit" ? theme.primary : undefined}
|
||||||
>
|
>
|
||||||
<text fg={focusArea() === "explicit" ? "var(--color-primary)" : "var(--color-muted)"}>
|
<text fg={focusArea() === "explicit" ? theme.primary : theme.textMuted}>
|
||||||
Explicit: {sourceExplicit() ? "Yes" : "No"}
|
Explicit: {sourceExplicit() ? "Yes" : "No"}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
<text fg="var(--color-muted)">Enter/Space to toggle focused setting</text>
|
<text fg={theme.textMuted}>Enter/Space to toggle focused setting</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
{/* Add new source form */}
|
{/* Add new source form */}
|
||||||
<box border padding={1} flexDirection="column" gap={1}>
|
<box border padding={1} flexDirection="column" gap={1}>
|
||||||
<text fg={focusArea() === "add" || focusArea() === "url" ? "var(--color-primary)" : "var(--color-muted)"}>
|
<text fg={focusArea() === "add" || focusArea() === "url" ? theme.primary : theme.textMuted}>
|
||||||
Add New Source:
|
Add New Source:
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<text fg="var(--color-muted)">Name:</text>
|
<text fg={theme.textMuted}>Name:</text>
|
||||||
<input
|
<input
|
||||||
value={newSourceName()}
|
value={newSourceName()}
|
||||||
onInput={setNewSourceName}
|
onInput={setNewSourceName}
|
||||||
@@ -266,7 +268,7 @@ export function SourceManager(props: SourceManagerProps) {
|
|||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<text fg="var(--color-muted)">URL:</text>
|
<text fg={theme.textMuted}>URL:</text>
|
||||||
<input
|
<input
|
||||||
value={newSourceUrl()}
|
value={newSourceUrl()}
|
||||||
onInput={(v) => {
|
onInput={(v) => {
|
||||||
@@ -285,16 +287,16 @@ export function SourceManager(props: SourceManagerProps) {
|
|||||||
width={15}
|
width={15}
|
||||||
onMouseDown={handleAddSource}
|
onMouseDown={handleAddSource}
|
||||||
>
|
>
|
||||||
<text fg="var(--color-success)">[+] Add Source</text>
|
<text fg={theme.success}>[+] Add Source</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
{/* Error message */}
|
{/* Error message */}
|
||||||
{error() && (
|
{error() && (
|
||||||
<text fg="var(--color-error)">{error()}</text>
|
<text fg={theme.error}>{error()}</text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<text fg="var(--color-muted)">Tab to switch sections, Esc to close</text>
|
<text fg={theme.textMuted}>Tab to switch sections, Esc to close</text>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useTheme } from "../context/ThemeContext"
|
||||||
|
|
||||||
export type TabId = "discover" | "feeds" | "search" | "player" | "settings"
|
export type TabId = "discover" | "feeds" | "search" | "player" | "settings"
|
||||||
|
|
||||||
export type TabDefinition = {
|
export type TabDefinition = {
|
||||||
@@ -20,11 +22,12 @@ type TabProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Tab(props: TabProps) {
|
export function Tab(props: TabProps) {
|
||||||
|
const { theme } = useTheme()
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
onMouseDown={() => props.onSelect(props.tab.id)}
|
onMouseDown={() => props.onSelect(props.tab.id)}
|
||||||
style={{ padding: 1, backgroundColor: props.active ? "var(--color-primary)" : "transparent" }}
|
style={{ padding: 1, backgroundColor: props.active ? theme.primary : "transparent" }}
|
||||||
>
|
>
|
||||||
<text>
|
<text>
|
||||||
{props.active ? "[" : " "}
|
{props.active ? "[" : " "}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createContext, createEffect, createMemo, createSignal, useContext } from "solid-js"
|
import { createContext, createEffect, createMemo, createSignal, Show, useContext } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { useRenderer } from "@opentui/solid"
|
import { useRenderer } from "@opentui/solid"
|
||||||
import type { ThemeName } from "../types/settings"
|
import type { ThemeName } from "../types/settings"
|
||||||
@@ -7,13 +7,76 @@ import { useAppStore } from "../stores/app"
|
|||||||
import { THEME_JSON } from "../constants/themes"
|
import { THEME_JSON } from "../constants/themes"
|
||||||
import { resolveTheme } from "../utils/theme-resolver"
|
import { resolveTheme } from "../utils/theme-resolver"
|
||||||
import { generateSyntax, generateSubtleSyntax } from "../utils/syntax-highlighter"
|
import { generateSyntax, generateSubtleSyntax } from "../utils/syntax-highlighter"
|
||||||
import { generateSystemTheme } from "../utils/system-theme"
|
import { resolveTerminalTheme, loadThemes } from "../utils/theme"
|
||||||
import { getCustomThemes } from "../utils/custom-themes"
|
import type { RGBA, TerminalColors } from "@opentui/core"
|
||||||
import { setThemeAttribute } from "../utils/theme"
|
|
||||||
import type { RGBA } from "@opentui/core"
|
type ThemeResolved = {
|
||||||
|
primary: RGBA
|
||||||
|
secondary: RGBA
|
||||||
|
accent: RGBA
|
||||||
|
error: RGBA
|
||||||
|
warning: RGBA
|
||||||
|
success: RGBA
|
||||||
|
info: RGBA
|
||||||
|
text: RGBA
|
||||||
|
textMuted: RGBA
|
||||||
|
selectedListItemText: RGBA
|
||||||
|
background: RGBA
|
||||||
|
backgroundPanel: RGBA
|
||||||
|
backgroundElement: RGBA
|
||||||
|
backgroundMenu: RGBA
|
||||||
|
border: RGBA
|
||||||
|
borderActive: RGBA
|
||||||
|
borderSubtle: RGBA
|
||||||
|
diffAdded: RGBA
|
||||||
|
diffRemoved: RGBA
|
||||||
|
diffContext: RGBA
|
||||||
|
diffHunkHeader: RGBA
|
||||||
|
diffHighlightAdded: RGBA
|
||||||
|
diffHighlightRemoved: RGBA
|
||||||
|
diffAddedBg: RGBA
|
||||||
|
diffRemovedBg: RGBA
|
||||||
|
diffContextBg: RGBA
|
||||||
|
diffLineNumber: RGBA
|
||||||
|
diffAddedLineNumberBg: RGBA
|
||||||
|
diffRemovedLineNumberBg: RGBA
|
||||||
|
markdownText: RGBA
|
||||||
|
markdownHeading: RGBA
|
||||||
|
markdownLink: RGBA
|
||||||
|
markdownLinkText: RGBA
|
||||||
|
markdownCode: RGBA
|
||||||
|
markdownBlockQuote: RGBA
|
||||||
|
markdownEmph: RGBA
|
||||||
|
markdownStrong: RGBA
|
||||||
|
markdownHorizontalRule: RGBA
|
||||||
|
markdownListItem: RGBA
|
||||||
|
markdownListEnumeration: RGBA
|
||||||
|
markdownImage: RGBA
|
||||||
|
markdownImageText: RGBA
|
||||||
|
markdownCodeBlock: RGBA
|
||||||
|
syntaxComment: RGBA
|
||||||
|
syntaxKeyword: RGBA
|
||||||
|
syntaxFunction: RGBA
|
||||||
|
syntaxVariable: RGBA
|
||||||
|
syntaxString: RGBA
|
||||||
|
syntaxNumber: RGBA
|
||||||
|
syntaxType: RGBA
|
||||||
|
syntaxOperator: RGBA
|
||||||
|
syntaxPunctuation: RGBA
|
||||||
|
muted?: RGBA
|
||||||
|
surface?: RGBA
|
||||||
|
layerBackgrounds?: {
|
||||||
|
layer0: RGBA
|
||||||
|
layer1: RGBA
|
||||||
|
layer2: RGBA
|
||||||
|
layer3: RGBA
|
||||||
|
}
|
||||||
|
_hasSelectedListItemText?: boolean
|
||||||
|
thinkingOpacity?: number
|
||||||
|
}
|
||||||
|
|
||||||
type ThemeContextValue = {
|
type ThemeContextValue = {
|
||||||
theme: Record<string, unknown>
|
theme: ThemeResolved
|
||||||
selected: () => string
|
selected: () => string
|
||||||
all: () => Record<string, ThemeJson>
|
all: () => Record<string, ThemeJson>
|
||||||
syntax: () => unknown
|
syntax: () => unknown
|
||||||
@@ -31,13 +94,14 @@ export function ThemeProvider({ children }: { children: any }) {
|
|||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
const [ready, setReady] = createSignal(false)
|
const [ready, setReady] = createSignal(false)
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
themes: { ...THEME_JSON } as Record<string, ThemeJson>,
|
themes: {} as Record<string, ThemeJson>,
|
||||||
mode: "dark" as "dark" | "light",
|
mode: "dark" as "dark" | "light",
|
||||||
active: appStore.state().settings.theme as ThemeName,
|
active: appStore.state().settings.theme as ThemeName,
|
||||||
|
system: undefined as undefined | TerminalColors,
|
||||||
})
|
})
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
getCustomThemes()
|
loadThemes()
|
||||||
.then((custom) => {
|
.then((custom) => {
|
||||||
setStore(
|
setStore(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
@@ -52,26 +116,18 @@ export function ThemeProvider({ children }: { children: any }) {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setStore("active", appStore.state().settings.theme)
|
setStore("active", appStore.state().settings.theme)
|
||||||
setThemeAttribute(appStore.state().settings.theme)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (store.active !== "system") return
|
|
||||||
renderer
|
renderer
|
||||||
.getPalette({ size: 16 })
|
.getPalette({ size: 16 })
|
||||||
.then((colors) => {
|
.then((colors) => setStore("system", colors))
|
||||||
setStore(
|
|
||||||
produce((draft) => {
|
|
||||||
draft.themes.system = generateSystemTheme(colors, store.mode)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
const values = createMemo(() => {
|
const values = createMemo(() => {
|
||||||
const theme = store.themes[store.active] ?? store.themes.opencode
|
const themes = Object.keys(store.themes).length ? store.themes : THEME_JSON
|
||||||
return resolveTheme(theme, store.mode)
|
return resolveTerminalTheme(themes, store.active, store.mode, store.system)
|
||||||
})
|
})
|
||||||
|
|
||||||
const syntax = createMemo(() => generateSyntax(values() as unknown as Record<string, RGBA>))
|
const syntax = createMemo(() => generateSyntax(values() as unknown as Record<string, RGBA>))
|
||||||
@@ -80,11 +136,11 @@ export function ThemeProvider({ children }: { children: any }) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const context: ThemeContextValue = {
|
const context: ThemeContextValue = {
|
||||||
theme: new Proxy(values(), {
|
theme: new Proxy(values(), {
|
||||||
get(_target, prop) {
|
get(_target, prop) {
|
||||||
return values()[prop as keyof typeof values]
|
return values()[prop as keyof typeof values]
|
||||||
},
|
},
|
||||||
}),
|
}) as ThemeResolved,
|
||||||
selected: () => store.active,
|
selected: () => store.active,
|
||||||
all: () => store.themes,
|
all: () => store.themes,
|
||||||
syntax,
|
syntax,
|
||||||
@@ -95,7 +151,11 @@ export function ThemeProvider({ children }: { children: any }) {
|
|||||||
ready,
|
ready,
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ThemeContext.Provider value={context}>{children}</ThemeContext.Provider>
|
return (
|
||||||
|
<Show when={ready()}>
|
||||||
|
<ThemeContext.Provider value={context}>{children}</ThemeContext.Provider>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTheme() {
|
export function useTheme() {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { render } from "@opentui/solid"
|
import { render } from "@opentui/solid"
|
||||||
import { App } from "./App"
|
import { App } from "./App"
|
||||||
|
import { ThemeProvider } from "./context/ThemeContext"
|
||||||
import "./styles/theme.css"
|
import "./styles/theme.css"
|
||||||
|
|
||||||
render(() => <App />)
|
render(() => (
|
||||||
|
<ThemeProvider>
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
|
))
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { mkdir } from "fs/promises"
|
||||||
import type { ThemeJson } from "../types/theme-schema"
|
import type { ThemeJson } from "../types/theme-schema"
|
||||||
|
import { THEME_JSON } from "../constants/themes"
|
||||||
import { validateTheme } from "./theme-loader"
|
import { validateTheme } from "./theme-loader"
|
||||||
|
|
||||||
export async function getCustomThemes() {
|
export async function getCustomThemes() {
|
||||||
const dirs = [
|
const home = process.env.HOME ?? ""
|
||||||
path.join(process.env.HOME ?? "", ".config/podtui/themes"),
|
if (!home) return {}
|
||||||
path.resolve(process.cwd(), ".podtui/themes"),
|
|
||||||
path.resolve(process.cwd(), "themes"),
|
const dir = path.join(home, ".config/podtui/themes")
|
||||||
]
|
await mkdir(dir, { recursive: true })
|
||||||
|
|
||||||
|
for (const [name, theme] of Object.entries(THEME_JSON)) {
|
||||||
|
const file = path.join(dir, `${name}.json`)
|
||||||
|
const exists = await Bun.file(file).exists()
|
||||||
|
if (exists) continue
|
||||||
|
await Bun.write(file, JSON.stringify(theme, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
const result: Record<string, ThemeJson> = {}
|
const result: Record<string, ThemeJson> = {}
|
||||||
for (const dir of dirs) {
|
const glob = new Bun.Glob("*.json")
|
||||||
const glob = new Bun.Glob("*.json")
|
for await (const item of glob.scan({ absolute: true, followSymlinks: true, cwd: dir })) {
|
||||||
for await (const item of glob.scan({ absolute: true, followSymlinks: true, cwd: dir })) {
|
const name = path.basename(item, ".json")
|
||||||
const name = path.basename(item, ".json")
|
const json = (await Bun.file(item).json()) as ThemeJson
|
||||||
const json = (await Bun.file(item).json()) as ThemeJson
|
validateTheme(json, item)
|
||||||
validateTheme(json, item)
|
result[name] = json
|
||||||
result[name] = json
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { ansiToRgba } from "./ansi-to-rgba"
|
|||||||
export type ThemeMode = "dark" | "light"
|
export type ThemeMode = "dark" | "light"
|
||||||
|
|
||||||
export function resolveTheme(theme: ThemeJson, mode: ThemeMode) {
|
export function resolveTheme(theme: ThemeJson, mode: ThemeMode) {
|
||||||
|
if (!theme || !theme.theme) {
|
||||||
|
throw new Error("Invalid theme: missing theme object")
|
||||||
|
}
|
||||||
const defs = theme.defs ?? {}
|
const defs = theme.defs ?? {}
|
||||||
|
|
||||||
function resolveColor(value: ColorValue): RGBA {
|
function resolveColor(value: ColorValue): RGBA {
|
||||||
@@ -38,8 +41,21 @@ export function resolveTheme(theme: ThemeJson, mode: ThemeMode) {
|
|||||||
|
|
||||||
const thinkingOpacity = theme.theme.thinkingOpacity ?? 0.6
|
const thinkingOpacity = theme.theme.thinkingOpacity ?? 0.6
|
||||||
|
|
||||||
|
const background = resolved.background
|
||||||
|
const backgroundPanel = resolved.backgroundPanel ?? background
|
||||||
|
const backgroundElement = resolved.backgroundElement ?? backgroundPanel
|
||||||
|
const backgroundMenu = resolved.backgroundMenu ?? backgroundElement
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...resolved,
|
...resolved,
|
||||||
|
muted: resolved.textMuted ?? resolved.muted,
|
||||||
|
surface: resolved.backgroundPanel ?? resolved.surface,
|
||||||
|
layerBackgrounds: {
|
||||||
|
layer0: background,
|
||||||
|
layer1: backgroundPanel,
|
||||||
|
layer2: backgroundElement,
|
||||||
|
layer3: backgroundMenu,
|
||||||
|
},
|
||||||
_hasSelectedListItemText: hasSelected,
|
_hasSelectedListItemText: hasSelected,
|
||||||
thinkingOpacity,
|
thinkingOpacity,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,13 @@
|
|||||||
* Handles dynamic theme switching by updating CSS custom properties
|
* Handles dynamic theme switching by updating CSS custom properties
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { RGBA } 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 } from "../types/theme-schema"
|
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) => {
|
const toCss = (value: ColorValue | RGBA) => {
|
||||||
if (value instanceof RGBA) {
|
if (value instanceof RGBA) {
|
||||||
@@ -60,3 +64,32 @@ export function setThemeAttribute(themeName: string) {
|
|||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
root.setAttribute("data-theme", themeName)
|
root.setAttribute("data-theme", themeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadThemes() {
|
||||||
|
return await getCustomThemes()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadTheme(name: string) {
|
||||||
|
const themes = await loadThemes()
|
||||||
|
return themes[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
||||||
|
return resolveThemeJson(theme, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTerminalTheme(
|
||||||
|
themes: Record<string, ThemeJson>,
|
||||||
|
name: string,
|
||||||
|
mode: "dark" | "light",
|
||||||
|
system?: TerminalColors
|
||||||
|
) {
|
||||||
|
if (name === "system" && system) {
|
||||||
|
return resolveThemeJson(generateSystemTheme(system, mode), mode)
|
||||||
|
}
|
||||||
|
const theme = themes[name] ?? themes.opencode
|
||||||
|
if (!theme) {
|
||||||
|
return resolveThemeJson(THEME_JSON.opencode, mode)
|
||||||
|
}
|
||||||
|
return resolveThemeJson(theme, mode)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user