diff --git a/src/App.tsx b/src/App.tsx index d76ce9e..2721335 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -187,7 +187,6 @@ export function App() { return ( diff --git a/src/components/LayerIndicator.tsx b/src/components/LayerIndicator.tsx index 2ee5680..a286041 100644 --- a/src/components/LayerIndicator.tsx +++ b/src/components/LayerIndicator.tsx @@ -7,7 +7,7 @@ export function LayerIndicator({ layerDepth }: { layerDepth: number }) { const indicators = [] for (let i = 0; i < 4; i++) { const isActive = i <= layerDepth - const color = isActive ? "#f6c177" : "#4c566a" + const color = isActive ? "var(--color-accent)" : "var(--color-muted)" const size = isActive ? "●" : "○" indicators.push( @@ -20,9 +20,9 @@ export function LayerIndicator({ layerDepth }: { layerDepth: number }) { return ( - Depth: + Depth: {getLayerIndicator()} - + {layerDepth} diff --git a/src/components/LoginScreen.tsx b/src/components/LoginScreen.tsx index b256fb9..e43c237 100644 --- a/src/components/LoginScreen.tsx +++ b/src/components/LoginScreen.tsx @@ -90,7 +90,7 @@ export function LoginScreen(props: LoginScreenProps) { {/* Email field */} - Email: + Email: {emailError() && ( - {emailError()} + {emailError()} )} {/* Password field */} - + Password: {passwordError() && ( - {passwordError()} + {passwordError()} )} @@ -127,9 +127,9 @@ export function LoginScreen(props: LoginScreenProps) { - + {auth.isLoading ? "Signing in..." : "[Enter] Sign In"} @@ -137,21 +137,21 @@ export function LoginScreen(props: LoginScreenProps) { {/* Auth error message */} {auth.error && ( - {auth.error.message} + {auth.error.message} )} {/* Alternative auth options */} - Or authenticate with: + Or authenticate with: - + [C] Sync Code @@ -159,9 +159,9 @@ export function LoginScreen(props: LoginScreenProps) { - + [O] OAuth Info @@ -169,7 +169,7 @@ export function LoginScreen(props: LoginScreenProps) { - Tab to navigate, Enter to select + Tab to navigate, Enter to select ) } diff --git a/src/components/PreferencesPanel.tsx b/src/components/PreferencesPanel.tsx index 17860fb..509a2ea 100644 --- a/src/components/PreferencesPanel.tsx +++ b/src/components/PreferencesPanel.tsx @@ -76,55 +76,55 @@ export function PreferencesPanel() { return ( - Preferences + Preferences - Theme: + Theme: - {THEME_LABELS.find((t) => t.value === settings().theme)?.label} + {THEME_LABELS.find((t) => t.value === settings().theme)?.label} - [Left/Right] + [Left/Right] - Font Size: + Font Size: - {settings().fontSize}px + {settings().fontSize}px - [Left/Right] + [Left/Right] - Playback: + Playback: - {settings().playbackSpeed}x + {settings().playbackSpeed}x - [Left/Right] + [Left/Right] - Show Explicit: + Show Explicit: - + {preferences().showExplicit ? "On" : "Off"} - [Space] + [Space] - Auto Download: + Auto Download: - + {preferences().autoDownload ? "On" : "Off"} - [Space] + [Space] - Tab to move focus, Left/Right to adjust + Tab to move focus, Left/Right to adjust ) } diff --git a/src/components/SettingsScreen.tsx b/src/components/SettingsScreen.tsx index 0bd4e83..d340aa1 100644 --- a/src/components/SettingsScreen.tsx +++ b/src/components/SettingsScreen.tsx @@ -50,7 +50,7 @@ export function SettingsScreen(props: SettingsScreenProps) { Settings - [Tab] Switch section | 1-4 jump | Esc up + [Tab] Switch section | 1-4 jump | Esc up @@ -58,10 +58,10 @@ export function SettingsScreen(props: SettingsScreenProps) { setActiveSection(section.id)} > - + [{index + 1}] {section.label} @@ -74,21 +74,21 @@ export function SettingsScreen(props: SettingsScreenProps) { {activeSection() === "preferences" && } {activeSection() === "account" && ( - Account + Account - Status: - + Status: + {props.accountLabel} props.onOpenAccount?.()}> - [A] Manage Account + [A] Manage Account )} - Enter to dive | Esc up + Enter to dive | Esc up ) } diff --git a/src/components/SourceManager.tsx b/src/components/SourceManager.tsx index b344a51..903c75a 100644 --- a/src/components/SourceManager.tsx +++ b/src/components/SourceManager.tsx @@ -155,15 +155,15 @@ export function SourceManager(props: SourceManagerProps) { Podcast Sources - [Esc] Close + [Esc] Close - Manage where to search for podcasts + Manage where to search for podcasts {/* Source list */} - Sources: + Sources: {(source, index) => ( @@ -173,7 +173,7 @@ export function SourceManager(props: SourceManagerProps) { padding={0} backgroundColor={ focusArea() === "list" && index() === selectedIndex() - ? "#333" + ? "var(--color-primary)" : undefined } onMouseDown={() => { @@ -184,21 +184,21 @@ export function SourceManager(props: SourceManagerProps) { > {focusArea() === "list" && index() === selectedIndex() ? ">" : " "} - + {source.enabled ? "[x]" : "[ ]"} - {getSourceIcon(source)} + {getSourceIcon(source)} @@ -208,54 +208,54 @@ export function SourceManager(props: SourceManagerProps) { )} - Space/Enter to toggle, d to delete, a to add + Space/Enter to toggle, d to delete, a to add {/* API settings */} - + {isApiSource() ? "API Settings" : "API Settings (select an API source)"} - + Country: {sourceCountry()} - + Language: {sourceLanguage() === "ja_jp" ? "Japanese" : "English"} - + Explicit: {sourceExplicit() ? "Yes" : "No"} - Enter/Space to toggle focused setting + Enter/Space to toggle focused setting {/* Add new source form */} - + Add New Source: - + - Name: + Name: - URL: + URL: { @@ -285,16 +285,16 @@ export function SourceManager(props: SourceManagerProps) { width={15} onMouseDown={handleAddSource} > - [+] Add Source + [+] Add Source {/* Error message */} {error() && ( - {error()} + {error()} )} - Tab to switch sections, Esc to close + Tab to switch sections, Esc to close ) } diff --git a/src/components/Tab.tsx b/src/components/Tab.tsx index a6b9bed..4053333 100644 --- a/src/components/Tab.tsx +++ b/src/components/Tab.tsx @@ -24,7 +24,7 @@ export function Tab(props: TabProps) { props.onSelect(props.tab.id)} - style={{ padding: 1, backgroundColor: props.active ? "#333333" : "transparent" }} + style={{ padding: 1, backgroundColor: props.active ? "var(--color-primary)" : "transparent" }} > {props.active ? "[" : " "} diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx index 68e17c9..5b2484a 100644 --- a/src/context/ThemeContext.tsx +++ b/src/context/ThemeContext.tsx @@ -1,41 +1,64 @@ -import { createContext, useContext, createSignal } from "solid-js" +import { createContext, useContext, createSignal, createEffect, onCleanup } from "solid-js" import type { ThemeColors, ThemeName } from "../types/settings" +import { useAppStore } from "../stores/app" +import { applyTheme, setThemeAttribute, getSystemThemeMode } from "../utils/theme" type ThemeContextType = { themeName: () => ThemeName setThemeName: (theme: ThemeName) => void - resolvedTheme: ThemeColors + resolvedTheme: () => ThemeColors isSystemTheme: () => boolean + currentMode: () => "dark" | "light" } const ThemeContext = createContext() export function ThemeProvider({ children }: { children: any }) { - const [themeName, setThemeName] = createSignal("system") + const appStore = useAppStore() + const [themeName, setThemeName] = createSignal(appStore.state().settings.theme) + const [resolvedTheme, setResolvedTheme] = createSignal(appStore.resolveTheme()) + const [currentMode, setCurrentMode] = createSignal<"dark" | "light">(getSystemThemeMode()) const isSystemTheme = () => themeName() === "system" - const resolvedTheme = { - background: "transparent", - surface: "#1b1f27", - primary: "#6fa8ff", - secondary: "#a9b1d6", - accent: "#f6c177", - text: "#e6edf3", - muted: "#7d8590", - warning: "#f0b429", - error: "#f47067", - success: "#3fb950", - layerBackgrounds: { - layer0: "transparent", - layer1: "#1e222e", - layer2: "#161b22", - layer3: "#0d1117", - }, - } + // Update theme when appStore theme changes + createEffect(() => { + const currentTheme = appStore.state().settings.theme + setThemeName(currentTheme) + setResolvedTheme(appStore.resolveTheme()) + + // Apply theme to CSS variables + if (currentTheme === "system") { + const mode = getSystemThemeMode() + setCurrentMode(mode) + applyTheme(resolvedTheme()) + } else { + setCurrentMode("dark") // All themes are dark by default + } + + setThemeAttribute(currentTheme === "system" ? "system" : currentTheme) + }) + + // Handle system theme changes + createEffect(() => { + if (isSystemTheme()) { + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") + const handler = () => { + const newMode = getSystemThemeMode() + setCurrentMode(newMode) + setResolvedTheme(appStore.resolveTheme()) + } + + mediaQuery.addEventListener("change", handler) + + onCleanup(() => { + mediaQuery.removeEventListener("change", handler) + }) + } + }) return ( - + {children} ) diff --git a/src/index.tsx b/src/index.tsx index ba39920..9d800cd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,5 @@ import { render } from "@opentui/solid" import { App } from "./App" +import "./styles/theme.css" render(() => ) diff --git a/src/styles/theme.css b/src/styles/theme.css new file mode 100644 index 0000000..65fe017 --- /dev/null +++ b/src/styles/theme.css @@ -0,0 +1,138 @@ +/* Theme CSS Variables */ + +:root { + /* Base Colors */ + --color-background: transparent; + --color-surface: #1b1f27; + --color-primary: #6fa8ff; + --color-secondary: #a9b1d6; + --color-accent: #f6c177; + --color-text: #e6edf3; + --color-muted: #7d8590; + --color-warning: #f0b429; + --color-error: #f47067; + --color-success: #3fb950; + + /* Layer Backgrounds */ + --color-layer0: transparent; + --color-layer1: #1e222e; + --color-layer2: #161b22; + --color-layer3: #0d1117; +} + +/* Dark Theme (Catppuccin default) */ +[data-theme="dark"] { + --color-background: transparent; + --color-surface: #1e1e2e; + --color-primary: #89b4fa; + --color-secondary: #cba6f7; + --color-accent: #f9e2af; + --color-text: #cdd6f4; + --color-muted: #7f849c; + --color-warning: #fab387; + --color-error: #f38ba8; + --color-success: #a6e3a1; + + --color-layer0: transparent; + --color-layer1: #181825; + --color-layer2: #11111b; + --color-layer3: #0a0a0f; +} + +/* Light Theme (Gruvbox) */ +[data-theme="light"] { + --color-background: transparent; + --color-surface: #282828; + --color-primary: #fabd2f; + --color-secondary: #83a598; + --color-accent: #fe8019; + --color-text: #ebdbb2; + --color-muted: #928374; + --color-warning: #fabd2f; + --color-error: #fb4934; + --color-success: #b8bb26; + + --color-layer0: transparent; + --color-layer1: #32302a; + --color-layer2: #1d2021; + --color-layer3: #0d0c0c; +} + +/* Tokyo Theme */ +[data-theme="tokyo"] { + --color-background: transparent; + --color-surface: #1a1b26; + --color-primary: #7aa2f7; + --color-secondary: #bb9af7; + --color-accent: #e0af68; + --color-text: #c0caf5; + --color-muted: #565f89; + --color-warning: #e0af68; + --color-error: #f7768e; + --color-success: #9ece6a; + + --color-layer0: transparent; + --color-layer1: #16161e; + --color-layer2: #0f0f15; + --color-layer3: #08080b; +} + +/* Nord Theme */ +[data-theme="nord"] { + --color-background: transparent; + --color-surface: #2e3440; + --color-primary: #88c0d0; + --color-secondary: #81a1c1; + --color-accent: #ebcb8b; + --color-text: #eceff4; + --color-muted: #4c566a; + --color-warning: #ebcb8b; + --color-error: #bf616a; + --color-success: #a3be8c; + + --color-layer0: transparent; + --color-layer1: #3b4252; + --color-layer2: #242933; + --color-layer3: #1a1c23; +} + +/* System Theme */ +@media (prefers-color-scheme: dark) { + [data-theme="system"] { + --color-background: transparent; + --color-surface: #1e1e2e; + --color-primary: #89b4fa; + --color-secondary: #cba6f7; + --color-accent: #f9e2af; + --color-text: #cdd6f4; + --color-muted: #7f849c; + --color-warning: #fab387; + --color-error: #f38ba8; + --color-success: #a6e3a1; + + --color-layer0: transparent; + --color-layer1: #181825; + --color-layer2: #11111b; + --color-layer3: #0a0a0f; + } +} + +@media (prefers-color-scheme: light) { + [data-theme="system"] { + --color-background: transparent; + --color-surface: #282828; + --color-primary: #fabd2f; + --color-secondary: #83a598; + --color-accent: #fe8019; + --color-text: #ebdbb2; + --color-muted: #928374; + --color-warning: #fabd2f; + --color-error: #fb4934; + --color-success: #b8bb26; + + --color-layer0: transparent; + --color-layer1: #32302a; + --color-layer2: #1d2021; + --color-layer3: #0d0c0c; + } +} diff --git a/src/utils/theme.ts b/src/utils/theme.ts new file mode 100644 index 0000000..2ce9ff8 --- /dev/null +++ b/src/utils/theme.ts @@ -0,0 +1,63 @@ +/** + * Theme CSS Variable Manager + * Handles dynamic theme switching by updating CSS custom properties + */ + +export function applyTheme(theme: { + background: string + surface: string + primary: string + secondary: string + accent: string + text: string + muted: string + warning: string + error: string + success: string + layerBackgrounds?: { + layer0: string + layer1: string + layer2: string + layer3: string + } +}) { + const root = document.documentElement + + // Apply base theme colors + root.style.setProperty("--color-background", theme.background) + root.style.setProperty("--color-surface", theme.surface) + root.style.setProperty("--color-primary", theme.primary) + root.style.setProperty("--color-secondary", theme.secondary) + root.style.setProperty("--color-accent", theme.accent) + root.style.setProperty("--color-text", theme.text) + root.style.setProperty("--color-muted", theme.muted) + root.style.setProperty("--color-warning", theme.warning) + root.style.setProperty("--color-error", theme.error) + root.style.setProperty("--color-success", theme.success) + + // Apply layer backgrounds if available + if (theme.layerBackgrounds) { + root.style.setProperty("--color-layer0", theme.layerBackgrounds.layer0) + root.style.setProperty("--color-layer1", theme.layerBackgrounds.layer1) + root.style.setProperty("--color-layer2", theme.layerBackgrounds.layer2) + root.style.setProperty("--color-layer3", theme.layerBackgrounds.layer3) + } +} + +/** + * Get theme mode from system preference + */ +export function getSystemThemeMode(): "dark" | "light" { + if (typeof window === "undefined") return "dark" + + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches + return prefersDark ? "dark" : "light" +} + +/** + * Apply CSS variable data-theme attribute + */ +export function setThemeAttribute(themeName: string) { + const root = document.documentElement + root.setAttribute("data-theme", themeName) +} diff --git a/tasks/podtui-navigation-theming-improvements/README.md b/tasks/podtui-navigation-theming-improvements/README.md index 920f6fc..51b365e 100644 --- a/tasks/podtui-navigation-theming-improvements/README.md +++ b/tasks/podtui-navigation-theming-improvements/README.md @@ -13,13 +13,13 @@ Tasks - [x] 06 — Implement left/right layer navigation controls → `06-implement-layer-navigation-controls.md` - [x] 07 — Implement enter/escape layer navigation controls → `07-implement-enter-escape-controls.md` - [x] 08 — Design active layer background color system → `08-design-active-layer-colors.md` -- [ ] 09 — Create theme context provider → `09-create-theme-context-provider.md` -- [ ] 10 — Implement DesktopTheme type and structure → `10-implement-desktop-theme-types.md` -- [ ] 11 — Implement theme resolution system → `11-implement-theme-resolution.md` -- [ ] 12 — Create CSS variable token system → `12-create-css-token-system.md` -- [ ] 13 — Implement system theme detection → `13-implement-system-theme-detection.md` -- [ ] 14 — Integrate theme provider into App component → `14-integrate-theme-provider.md` -- [ ] 15 — Update components to use theme tokens → `15-update-components-to-use-themes.md` +- [x] 09 — Create theme context provider → `09-create-theme-context-provider.md` +- [x] 10 — Implement DesktopTheme type and structure → `10-implement-desktop-theme-types.md` +- [x] 11 — Implement theme resolution system → `11-implement-theme-resolution.md` +- [x] 12 — Create CSS variable token system → `12-create-css-token-system.md` +- [x] 13 — Implement system theme detection → `13-implement-system-theme-detection.md` +- [x] 14 — Integrate theme provider into App component → `14-integrate-theme-provider.md` +- [x] 15 — Update components to use theme tokens → `15-update-components-to-use-themes.md` - [ ] 16 — Test navigation flows and layer transitions → `16-test-navigation-flows.md` - [ ] 17 — Test tab crash fixes and edge cases → `17-test-tab-crash-fixes.md` - [ ] 18 — Test theming system with all modes → `18-test-theming-system.md`