Compare commits
2 Commits
624a6ba022
...
c26150221a
| Author | SHA1 | Date | |
|---|---|---|---|
| c26150221a | |||
| 39a4f88496 |
23
src/App.tsx
23
src/App.tsx
@@ -11,6 +11,7 @@ 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";
|
||||||
@@ -32,27 +33,31 @@ export function App() {
|
|||||||
// Centralized keyboard handler for all tab navigation and shortcuts
|
// Centralized keyboard handler for all tab navigation and shortcuts
|
||||||
useAppKeyboard({
|
useAppKeyboard({
|
||||||
get activeTab() {
|
get activeTab() {
|
||||||
return activeTab();
|
return activeTab()
|
||||||
},
|
},
|
||||||
onTabChange: setActiveTab,
|
onTabChange: setActiveTab,
|
||||||
inputFocused: inputFocused(),
|
inputFocused: inputFocused(),
|
||||||
navigationEnabled: layerDepth() === 0,
|
navigationEnabled: layerDepth() === 0,
|
||||||
|
layerDepth,
|
||||||
|
onLayerChange: (newDepth) => {
|
||||||
|
setLayerDepth(newDepth)
|
||||||
|
},
|
||||||
onAction: (action) => {
|
onAction: (action) => {
|
||||||
if (action === "escape") {
|
if (action === "escape") {
|
||||||
if (layerDepth() > 0) {
|
if (layerDepth() > 0) {
|
||||||
setLayerDepth(0);
|
setLayerDepth(0)
|
||||||
setInputFocused(false);
|
setInputFocused(false)
|
||||||
} else {
|
} else {
|
||||||
setShowAuthPanel(false);
|
setShowAuthPanel(false)
|
||||||
setInputFocused(false);
|
setInputFocused(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "enter" && layerDepth() === 0) {
|
if (action === "enter" && layerDepth() === 0) {
|
||||||
setLayerDepth(1);
|
setLayerDepth(1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
const tab = activeTab();
|
const tab = activeTab();
|
||||||
@@ -180,8 +185,9 @@ export function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
<Layout
|
<Layout
|
||||||
theme={appStore.resolveTheme()}
|
layerDepth={layerDepth()}
|
||||||
header={
|
header={
|
||||||
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} />
|
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} />
|
||||||
}
|
}
|
||||||
@@ -189,5 +195,6 @@ export function App() {
|
|||||||
>
|
>
|
||||||
<box style={{ padding: 1 }}>{renderContent()}</box>
|
<box style={{ padding: 1 }}>{renderContent()}</box>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/components/LayerIndicator.tsx
Normal file
30
src/components/LayerIndicator.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useRenderer } from "@opentui/solid"
|
||||||
|
|
||||||
|
export function LayerIndicator({ layerDepth }: { layerDepth: number }) {
|
||||||
|
const renderer = useRenderer()
|
||||||
|
|
||||||
|
const getLayerIndicator = () => {
|
||||||
|
const indicators = []
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const isActive = i <= layerDepth
|
||||||
|
const color = isActive ? "var(--color-accent)" : "var(--color-muted)"
|
||||||
|
const size = isActive ? "●" : "○"
|
||||||
|
indicators.push(
|
||||||
|
<text fg={color} marginRight={1}>
|
||||||
|
{size}
|
||||||
|
</text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return indicators
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box flexDirection="row" alignItems="center">
|
||||||
|
<text fg="var(--color-muted)" marginRight={1}>Depth:</text>
|
||||||
|
{getLayerIndicator()}
|
||||||
|
<text fg="var(--color-muted)" marginLeft={1}>
|
||||||
|
{layerDepth}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,24 +1,109 @@
|
|||||||
import type { JSX } from "solid-js"
|
import type { JSX } from "solid-js"
|
||||||
import type { ThemeColors } from "../types/settings"
|
import type { ThemeColors, LayerBackgrounds } from "../types/settings"
|
||||||
|
import { LayerIndicator } from "./LayerIndicator"
|
||||||
|
|
||||||
|
type LayerConfig = {
|
||||||
|
depth: number
|
||||||
|
background: string
|
||||||
|
}
|
||||||
|
|
||||||
type LayoutProps = {
|
type LayoutProps = {
|
||||||
header?: JSX.Element
|
header?: JSX.Element
|
||||||
footer?: JSX.Element
|
footer?: JSX.Element
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
theme?: ThemeColors
|
theme?: ThemeColors
|
||||||
|
layerDepth?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Layout(props: LayoutProps) {
|
export function Layout(props: LayoutProps) {
|
||||||
|
const theme = props.theme
|
||||||
|
|
||||||
|
// Get layer configuration based on depth
|
||||||
|
const getLayerConfig = (depth: number): LayerConfig => {
|
||||||
|
if (!theme?.layerBackgrounds) {
|
||||||
|
return { depth: 0, background: "transparent" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const backgrounds = theme.layerBackgrounds
|
||||||
|
const depthMap: Record<number, LayerConfig> = {
|
||||||
|
0: { depth: 0, background: backgrounds.layer0 },
|
||||||
|
1: { depth: 1, background: backgrounds.layer1 },
|
||||||
|
2: { depth: 2, background: backgrounds.layer2 },
|
||||||
|
3: { depth: 3, background: backgrounds.layer3 },
|
||||||
|
}
|
||||||
|
|
||||||
|
return depthMap[depth] || { depth: 0, background: "transparent" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current layer background
|
||||||
|
const currentLayer = getLayerConfig(props.layerDepth || 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
backgroundColor={props.theme?.background}
|
backgroundColor={theme?.background}
|
||||||
>
|
>
|
||||||
{props.header ? <box style={{ height: 3 }}>{props.header}</box> : <text></text>}
|
{/* Header */}
|
||||||
<box style={{ flexGrow: 1 }}>{props.children}</box>
|
{props.header ? (
|
||||||
{props.footer ? <box style={{ height: 1 }}>{props.footer}</box> : <text></text>}
|
<box
|
||||||
|
style={{
|
||||||
|
height: 4,
|
||||||
|
backgroundColor: theme?.surface,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box style={{ padding: 1 }}>
|
||||||
|
{props.header}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
) : (
|
||||||
|
<box style={{ height: 4 }} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main content area with layer background */}
|
||||||
|
<box
|
||||||
|
style={{
|
||||||
|
flexGrow: 1,
|
||||||
|
backgroundColor: currentLayer.background,
|
||||||
|
paddingLeft: 2,
|
||||||
|
paddingRight: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box style={{ flexGrow: 1 }}>
|
||||||
|
{props.children}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
{props.footer ? (
|
||||||
|
<box
|
||||||
|
style={{
|
||||||
|
height: 2,
|
||||||
|
backgroundColor: theme?.surface,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box style={{ padding: 1 }}>
|
||||||
|
{props.footer}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
) : (
|
||||||
|
<box style={{ height: 2 }} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Layer indicator */}
|
||||||
|
{props.layerDepth !== undefined && (
|
||||||
|
<box
|
||||||
|
style={{
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: theme?.surface,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box style={{ padding: 1 }}>
|
||||||
|
<LayerIndicator layerDepth={props.layerDepth} />
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
|
|
||||||
{/* Email field */}
|
{/* Email field */}
|
||||||
<box flexDirection="column" gap={0}>
|
<box flexDirection="column" gap={0}>
|
||||||
<text fg={focusField() === "email" ? "cyan" : undefined}>Email:</text>
|
<text fg={focusField() === "email" ? "var(--color-primary)" : undefined}>Email:</text>
|
||||||
<input
|
<input
|
||||||
value={email()}
|
value={email()}
|
||||||
onInput={setEmail}
|
onInput={setEmail}
|
||||||
@@ -99,13 +99,13 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
width={30}
|
width={30}
|
||||||
/>
|
/>
|
||||||
{emailError() && (
|
{emailError() && (
|
||||||
<text fg="red">{emailError()}</text>
|
<text fg="var(--color-error)">{emailError()}</text>
|
||||||
)}
|
)}
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
{/* Password field */}
|
{/* Password field */}
|
||||||
<box flexDirection="column" gap={0}>
|
<box flexDirection="column" gap={0}>
|
||||||
<text fg={focusField() === "password" ? "cyan" : undefined}>
|
<text fg={focusField() === "password" ? "var(--color-primary)" : undefined}>
|
||||||
Password:
|
Password:
|
||||||
</text>
|
</text>
|
||||||
<input
|
<input
|
||||||
@@ -116,7 +116,7 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
width={30}
|
width={30}
|
||||||
/>
|
/>
|
||||||
{passwordError() && (
|
{passwordError() && (
|
||||||
<text fg="red">{passwordError()}</text>
|
<text fg="var(--color-error)">{passwordError()}</text>
|
||||||
)}
|
)}
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
@@ -127,9 +127,9 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
padding={1}
|
padding={1}
|
||||||
backgroundColor={focusField() === "submit" ? "#333" : undefined}
|
backgroundColor={focusField() === "submit" ? "var(--color-primary)" : undefined}
|
||||||
>
|
>
|
||||||
<text fg={focusField() === "submit" ? "cyan" : undefined}>
|
<text fg={focusField() === "submit" ? "var(--color-text)" : undefined}>
|
||||||
{auth.isLoading ? "Signing in..." : "[Enter] Sign In"}
|
{auth.isLoading ? "Signing in..." : "[Enter] Sign In"}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
@@ -137,21 +137,21 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
|
|
||||||
{/* Auth error message */}
|
{/* Auth error message */}
|
||||||
{auth.error && (
|
{auth.error && (
|
||||||
<text fg="red">{auth.error.message}</text>
|
<text fg="var(--color-error)">{auth.error.message}</text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<box height={1} />
|
<box height={1} />
|
||||||
|
|
||||||
{/* Alternative auth options */}
|
{/* Alternative auth options */}
|
||||||
<text fg="gray">Or authenticate with:</text>
|
<text fg="var(--color-muted)">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" ? "#333" : undefined}
|
backgroundColor={focusField() === "code" ? "var(--color-primary)" : undefined}
|
||||||
>
|
>
|
||||||
<text fg={focusField() === "code" ? "yellow" : "gray"}>
|
<text fg={focusField() === "code" ? "var(--color-accent)" : "var(--color-muted)"}>
|
||||||
[C] Sync Code
|
[C] Sync Code
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
@@ -159,9 +159,9 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
padding={1}
|
padding={1}
|
||||||
backgroundColor={focusField() === "oauth" ? "#333" : undefined}
|
backgroundColor={focusField() === "oauth" ? "var(--color-primary)" : undefined}
|
||||||
>
|
>
|
||||||
<text fg={focusField() === "oauth" ? "yellow" : "gray"}>
|
<text fg={focusField() === "oauth" ? "var(--color-accent)" : "var(--color-muted)"}>
|
||||||
[O] OAuth Info
|
[O] OAuth Info
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
@@ -169,7 +169,7 @@ export function LoginScreen(props: LoginScreenProps) {
|
|||||||
|
|
||||||
<box height={1} />
|
<box height={1} />
|
||||||
|
|
||||||
<text fg="gray">Tab to navigate, Enter to select</text>
|
<text fg="var(--color-muted)">Tab to navigate, Enter to select</text>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,55 +76,55 @@ export function PreferencesPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<box flexDirection="column" gap={1}>
|
<box flexDirection="column" gap={1}>
|
||||||
<text fg="gray">Preferences</text>
|
<text fg="var(--color-muted)">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" ? "cyan" : "gray"}>Theme:</text>
|
<text fg={focusField() === "theme" ? "var(--color-primary)" : "var(--color-muted)"}>Theme:</text>
|
||||||
<box border padding={0}>
|
<box border padding={0}>
|
||||||
<text fg="white">{THEME_LABELS.find((t) => t.value === settings().theme)?.label}</text>
|
<text fg="var(--color-text)">{THEME_LABELS.find((t) => t.value === settings().theme)?.label}</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg="gray">[Left/Right]</text>
|
<text fg="var(--color-muted)">[Left/Right]</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1} alignItems="center">
|
<box flexDirection="row" gap={1} alignItems="center">
|
||||||
<text fg={focusField() === "font" ? "cyan" : "gray"}>Font Size:</text>
|
<text fg={focusField() === "font" ? "var(--color-primary)" : "var(--color-muted)"}>Font Size:</text>
|
||||||
<box border padding={0}>
|
<box border padding={0}>
|
||||||
<text fg="white">{settings().fontSize}px</text>
|
<text fg="var(--color-text)">{settings().fontSize}px</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg="gray">[Left/Right]</text>
|
<text fg="var(--color-muted)">[Left/Right]</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1} alignItems="center">
|
<box flexDirection="row" gap={1} alignItems="center">
|
||||||
<text fg={focusField() === "speed" ? "cyan" : "gray"}>Playback:</text>
|
<text fg={focusField() === "speed" ? "var(--color-primary)" : "var(--color-muted)"}>Playback:</text>
|
||||||
<box border padding={0}>
|
<box border padding={0}>
|
||||||
<text fg="white">{settings().playbackSpeed}x</text>
|
<text fg="var(--color-text)">{settings().playbackSpeed}x</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg="gray">[Left/Right]</text>
|
<text fg="var(--color-muted)">[Left/Right]</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1} alignItems="center">
|
<box flexDirection="row" gap={1} alignItems="center">
|
||||||
<text fg={focusField() === "explicit" ? "cyan" : "gray"}>Show Explicit:</text>
|
<text fg={focusField() === "explicit" ? "var(--color-primary)" : "var(--color-muted)"}>Show Explicit:</text>
|
||||||
<box border padding={0}>
|
<box border padding={0}>
|
||||||
<text fg={preferences().showExplicit ? "green" : "gray"}>
|
<text fg={preferences().showExplicit ? "var(--color-success)" : "var(--color-muted)"}>
|
||||||
{preferences().showExplicit ? "On" : "Off"}
|
{preferences().showExplicit ? "On" : "Off"}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg="gray">[Space]</text>
|
<text fg="var(--color-muted)">[Space]</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1} alignItems="center">
|
<box flexDirection="row" gap={1} alignItems="center">
|
||||||
<text fg={focusField() === "auto" ? "cyan" : "gray"}>Auto Download:</text>
|
<text fg={focusField() === "auto" ? "var(--color-primary)" : "var(--color-muted)"}>Auto Download:</text>
|
||||||
<box border padding={0}>
|
<box border padding={0}>
|
||||||
<text fg={preferences().autoDownload ? "green" : "gray"}>
|
<text fg={preferences().autoDownload ? "var(--color-success)" : "var(--color-muted)"}>
|
||||||
{preferences().autoDownload ? "On" : "Off"}
|
{preferences().autoDownload ? "On" : "Off"}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg="gray">[Space]</text>
|
<text fg="var(--color-muted)">[Space]</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<text fg="gray">Tab to move focus, Left/Right to adjust</text>
|
<text fg="var(--color-muted)">Tab to move focus, Left/Right to adjust</text>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function SettingsScreen(props: SettingsScreenProps) {
|
|||||||
<text>
|
<text>
|
||||||
<strong>Settings</strong>
|
<strong>Settings</strong>
|
||||||
</text>
|
</text>
|
||||||
<text fg="gray">[Tab] Switch section | 1-4 jump | Esc up</text>
|
<text fg="var(--color-muted)">[Tab] Switch section | 1-4 jump | Esc up</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
@@ -58,10 +58,10 @@ export function SettingsScreen(props: SettingsScreenProps) {
|
|||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
padding={0}
|
padding={0}
|
||||||
backgroundColor={activeSection() === section.id ? "#2b303b" : undefined}
|
backgroundColor={activeSection() === section.id ? "var(--color-primary)" : undefined}
|
||||||
onMouseDown={() => setActiveSection(section.id)}
|
onMouseDown={() => setActiveSection(section.id)}
|
||||||
>
|
>
|
||||||
<text fg={activeSection() === section.id ? "cyan" : "gray"}>
|
<text fg={activeSection() === section.id ? "var(--color-text)" : "var(--color-muted)"}>
|
||||||
[{index + 1}] {section.label}
|
[{index + 1}] {section.label}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
@@ -74,21 +74,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="gray">Account</text>
|
<text fg="var(--color-muted)">Account</text>
|
||||||
<box flexDirection="row" gap={2} alignItems="center">
|
<box flexDirection="row" gap={2} alignItems="center">
|
||||||
<text fg="gray">Status:</text>
|
<text fg="var(--color-muted)">Status:</text>
|
||||||
<text fg={props.accountStatus === "signed-in" ? "green" : "yellow"}>
|
<text fg={props.accountStatus === "signed-in" ? "var(--color-success)" : "var(--color-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="cyan">[A] Manage Account</text>
|
<text fg="var(--color-primary)">[A] Manage Account</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<text fg="gray">Enter to dive | Esc up</text>
|
<text fg="var(--color-muted)">Enter to dive | Esc up</text>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,15 +155,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="cyan">[Esc] Close</text>
|
<text fg="var(--color-primary)">[Esc] Close</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
<text fg="gray">Manage where to search for podcasts</text>
|
<text fg="var(--color-muted)">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" ? "cyan" : "gray"}>Sources:</text>
|
<text fg={focusArea() === "list" ? "var(--color-primary)" : "var(--color-muted)"}>Sources:</text>
|
||||||
<scrollbox height={6}>
|
<scrollbox height={6}>
|
||||||
<For each={sources()}>
|
<For each={sources()}>
|
||||||
{(source, index) => (
|
{(source, index) => (
|
||||||
@@ -173,7 +173,7 @@ export function SourceManager(props: SourceManagerProps) {
|
|||||||
padding={0}
|
padding={0}
|
||||||
backgroundColor={
|
backgroundColor={
|
||||||
focusArea() === "list" && index() === selectedIndex()
|
focusArea() === "list" && index() === selectedIndex()
|
||||||
? "#333"
|
? "var(--color-primary)"
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onMouseDown={() => {
|
onMouseDown={() => {
|
||||||
@@ -184,21 +184,21 @@ export function SourceManager(props: SourceManagerProps) {
|
|||||||
>
|
>
|
||||||
<text fg={
|
<text fg={
|
||||||
focusArea() === "list" && index() === selectedIndex()
|
focusArea() === "list" && index() === selectedIndex()
|
||||||
? "cyan"
|
? "var(--color-primary)"
|
||||||
: "gray"
|
: "var(--color-muted)"
|
||||||
}>
|
}>
|
||||||
{focusArea() === "list" && index() === selectedIndex()
|
{focusArea() === "list" && index() === selectedIndex()
|
||||||
? ">"
|
? ">"
|
||||||
: " "}
|
: " "}
|
||||||
</text>
|
</text>
|
||||||
<text fg={source.enabled ? "green" : "red"}>
|
<text fg={source.enabled ? "var(--color-success)" : "var(--color-error)"}>
|
||||||
{source.enabled ? "[x]" : "[ ]"}
|
{source.enabled ? "[x]" : "[ ]"}
|
||||||
</text>
|
</text>
|
||||||
<text fg="yellow">{getSourceIcon(source)}</text>
|
<text fg="var(--color-accent)">{getSourceIcon(source)}</text>
|
||||||
<text
|
<text
|
||||||
fg={
|
fg={
|
||||||
focusArea() === "list" && index() === selectedIndex()
|
focusArea() === "list" && index() === selectedIndex()
|
||||||
? "white"
|
? "var(--color-text)"
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -208,54 +208,54 @@ export function SourceManager(props: SourceManagerProps) {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</scrollbox>
|
</scrollbox>
|
||||||
<text fg="gray">Space/Enter to toggle, d to delete, a to add</text>
|
<text fg="var(--color-muted)">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() ? "gray" : "yellow"}>
|
<text fg={isApiSource() ? "var(--color-muted)" : "var(--color-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" ? "#333" : undefined}
|
backgroundColor={focusArea() === "country" ? "var(--color-primary)" : undefined}
|
||||||
>
|
>
|
||||||
<text fg={focusArea() === "country" ? "cyan" : "gray"}>
|
<text fg={focusArea() === "country" ? "var(--color-primary)" : "var(--color-muted)"}>
|
||||||
Country: {sourceCountry()}
|
Country: {sourceCountry()}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
padding={0}
|
padding={0}
|
||||||
backgroundColor={focusArea() === "language" ? "#333" : undefined}
|
backgroundColor={focusArea() === "language" ? "var(--color-primary)" : undefined}
|
||||||
>
|
>
|
||||||
<text fg={focusArea() === "language" ? "cyan" : "gray"}>
|
<text fg={focusArea() === "language" ? "var(--color-primary)" : "var(--color-muted)"}>
|
||||||
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" ? "#333" : undefined}
|
backgroundColor={focusArea() === "explicit" ? "var(--color-primary)" : undefined}
|
||||||
>
|
>
|
||||||
<text fg={focusArea() === "explicit" ? "cyan" : "gray"}>
|
<text fg={focusArea() === "explicit" ? "var(--color-primary)" : "var(--color-muted)"}>
|
||||||
Explicit: {sourceExplicit() ? "Yes" : "No"}
|
Explicit: {sourceExplicit() ? "Yes" : "No"}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
<text fg="gray">Enter/Space to toggle focused setting</text>
|
<text fg="var(--color-muted)">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" ? "cyan" : "gray"}>
|
<text fg={focusArea() === "add" || focusArea() === "url" ? "var(--color-primary)" : "var(--color-muted)"}>
|
||||||
Add New Source:
|
Add New Source:
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<text fg="gray">Name:</text>
|
<text fg="var(--color-muted)">Name:</text>
|
||||||
<input
|
<input
|
||||||
value={newSourceName()}
|
value={newSourceName()}
|
||||||
onInput={setNewSourceName}
|
onInput={setNewSourceName}
|
||||||
@@ -266,7 +266,7 @@ export function SourceManager(props: SourceManagerProps) {
|
|||||||
</box>
|
</box>
|
||||||
|
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<text fg="gray">URL:</text>
|
<text fg="var(--color-muted)">URL:</text>
|
||||||
<input
|
<input
|
||||||
value={newSourceUrl()}
|
value={newSourceUrl()}
|
||||||
onInput={(v) => {
|
onInput={(v) => {
|
||||||
@@ -285,16 +285,16 @@ export function SourceManager(props: SourceManagerProps) {
|
|||||||
width={15}
|
width={15}
|
||||||
onMouseDown={handleAddSource}
|
onMouseDown={handleAddSource}
|
||||||
>
|
>
|
||||||
<text fg="green">[+] Add Source</text>
|
<text fg="var(--color-success)">[+] Add Source</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
{/* Error message */}
|
{/* Error message */}
|
||||||
{error() && (
|
{error() && (
|
||||||
<text fg="red">{error()}</text>
|
<text fg="var(--color-error)">{error()}</text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<text fg="gray">Tab to switch sections, Esc to close</text>
|
<text fg="var(--color-muted)">Tab to switch sections, Esc to close</text>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function Tab(props: TabProps) {
|
|||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
onMouseDown={() => props.onSelect(props.tab.id)}
|
onMouseDown={() => props.onSelect(props.tab.id)}
|
||||||
style={{ padding: 1, backgroundColor: props.active ? "#333333" : "transparent" }}
|
style={{ padding: 1, backgroundColor: props.active ? "var(--color-primary)" : "transparent" }}
|
||||||
>
|
>
|
||||||
<text>
|
<text>
|
||||||
{props.active ? "[" : " "}
|
{props.active ? "[" : " "}
|
||||||
|
|||||||
@@ -1,67 +1,16 @@
|
|||||||
import type { ThemeColors, ThemeName } from "../types/settings"
|
import type { ThemeColors, ThemeName } from "../types/settings"
|
||||||
|
import { BASE_THEME_COLORS, BASE_LAYER_BACKGROUND, THEMES_DESKTOP } from "../types/desktop-theme"
|
||||||
|
|
||||||
export const DEFAULT_THEME: ThemeColors = {
|
export const DEFAULT_THEME: ThemeColors = {
|
||||||
background: "transparent",
|
...BASE_THEME_COLORS,
|
||||||
surface: "#1b1f27",
|
layerBackgrounds: BASE_LAYER_BACKGROUND,
|
||||||
primary: "#6fa8ff",
|
|
||||||
secondary: "#a9b1d6",
|
|
||||||
accent: "#f6c177",
|
|
||||||
text: "#e6edf3",
|
|
||||||
muted: "#7d8590",
|
|
||||||
warning: "#f0b429",
|
|
||||||
error: "#f47067",
|
|
||||||
success: "#3fb950",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const THEMES: Record<ThemeName, ThemeColors> = {
|
export const THEMES: Record<ThemeName, ThemeColors> = {
|
||||||
system: DEFAULT_THEME,
|
system: DEFAULT_THEME,
|
||||||
catppuccin: {
|
catppuccin: THEMES_DESKTOP.variants.find((v) => v.name === "catppuccin")!.colors,
|
||||||
background: "transparent",
|
gruvbox: THEMES_DESKTOP.variants.find((v) => v.name === "gruvbox")!.colors,
|
||||||
surface: "#1e1e2e",
|
tokyo: THEMES_DESKTOP.variants.find((v) => v.name === "tokyo")!.colors,
|
||||||
primary: "#89b4fa",
|
nord: THEMES_DESKTOP.variants.find((v) => v.name === "nord")!.colors,
|
||||||
secondary: "#cba6f7",
|
|
||||||
accent: "#f9e2af",
|
|
||||||
text: "#cdd6f4",
|
|
||||||
muted: "#7f849c",
|
|
||||||
warning: "#fab387",
|
|
||||||
error: "#f38ba8",
|
|
||||||
success: "#a6e3a1",
|
|
||||||
},
|
|
||||||
gruvbox: {
|
|
||||||
background: "transparent",
|
|
||||||
surface: "#282828",
|
|
||||||
primary: "#fabd2f",
|
|
||||||
secondary: "#83a598",
|
|
||||||
accent: "#fe8019",
|
|
||||||
text: "#ebdbb2",
|
|
||||||
muted: "#928374",
|
|
||||||
warning: "#fabd2f",
|
|
||||||
error: "#fb4934",
|
|
||||||
success: "#b8bb26",
|
|
||||||
},
|
|
||||||
tokyo: {
|
|
||||||
background: "transparent",
|
|
||||||
surface: "#1a1b26",
|
|
||||||
primary: "#7aa2f7",
|
|
||||||
secondary: "#bb9af7",
|
|
||||||
accent: "#e0af68",
|
|
||||||
text: "#c0caf5",
|
|
||||||
muted: "#565f89",
|
|
||||||
warning: "#e0af68",
|
|
||||||
error: "#f7768e",
|
|
||||||
success: "#9ece6a",
|
|
||||||
},
|
|
||||||
nord: {
|
|
||||||
background: "transparent",
|
|
||||||
surface: "#2e3440",
|
|
||||||
primary: "#88c0d0",
|
|
||||||
secondary: "#81a1c1",
|
|
||||||
accent: "#ebcb8b",
|
|
||||||
text: "#eceff4",
|
|
||||||
muted: "#4c566a",
|
|
||||||
warning: "#ebcb8b",
|
|
||||||
error: "#bf616a",
|
|
||||||
success: "#a3be8c",
|
|
||||||
},
|
|
||||||
custom: DEFAULT_THEME,
|
custom: DEFAULT_THEME,
|
||||||
}
|
}
|
||||||
|
|||||||
73
src/context/ThemeContext.tsx
Normal file
73
src/context/ThemeContext.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
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
|
||||||
|
isSystemTheme: () => boolean
|
||||||
|
currentMode: () => "dark" | "light"
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextType>()
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }: { children: any }) {
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const [themeName, setThemeName] = createSignal<ThemeName>(appStore.state().settings.theme)
|
||||||
|
const [resolvedTheme, setResolvedTheme] = createSignal<ThemeColors>(appStore.resolveTheme())
|
||||||
|
const [currentMode, setCurrentMode] = createSignal<"dark" | "light">(getSystemThemeMode())
|
||||||
|
|
||||||
|
const isSystemTheme = () => themeName() === "system"
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<ThemeContext.Provider value={{ themeName, setThemeName, resolvedTheme, isSystemTheme, currentMode }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const context = useContext(ThemeContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useTheme must be used within a ThemeProvider")
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { useKeyboard, useRenderer } from "@opentui/solid"
|
import { useKeyboard, useRenderer } from "@opentui/solid"
|
||||||
import type { TabId } from "../components/Tab"
|
import type { TabId } from "../components/Tab"
|
||||||
|
import type { Accessor } from "solid-js"
|
||||||
|
|
||||||
const TAB_ORDER: TabId[] = ["discover", "feeds", "search", "player", "settings"]
|
const TAB_ORDER: TabId[] = ["discover", "feeds", "search", "player", "settings"]
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ type ShortcutOptions = {
|
|||||||
onAction?: (action: string) => void
|
onAction?: (action: string) => void
|
||||||
inputFocused?: boolean
|
inputFocused?: boolean
|
||||||
navigationEnabled?: boolean
|
navigationEnabled?: boolean
|
||||||
|
layerDepth?: Accessor<number>
|
||||||
|
onLayerChange?: (newDepth: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAppKeyboard(options: ShortcutOptions) {
|
export function useAppKeyboard(options: ShortcutOptions) {
|
||||||
@@ -55,6 +58,26 @@ export function useAppKeyboard(options: ShortcutOptions) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Layer navigation with left/right arrows
|
||||||
|
if (options.layerDepth !== undefined && options.onLayerChange) {
|
||||||
|
const currentDepth = options.layerDepth()
|
||||||
|
const maxLayers = 3
|
||||||
|
|
||||||
|
if (key.name === "right") {
|
||||||
|
if (currentDepth < maxLayers) {
|
||||||
|
options.onLayerChange(currentDepth + 1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.name === "left") {
|
||||||
|
if (currentDepth > 0) {
|
||||||
|
options.onLayerChange(currentDepth - 1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tab navigation with left/right arrows OR [ and ]
|
// Tab navigation with left/right arrows OR [ and ]
|
||||||
if (key.name === "right" || key.name === "]") {
|
if (key.name === "right" || key.name === "]") {
|
||||||
options.onTabChange(getNextTab(options.activeTab))
|
options.onTabChange(getNextTab(options.activeTab))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { render } from "@opentui/solid"
|
import { render } from "@opentui/solid"
|
||||||
import { App } from "./App"
|
import { App } from "./App"
|
||||||
|
import "./styles/theme.css"
|
||||||
|
|
||||||
render(() => <App />)
|
render(() => <App />)
|
||||||
|
|||||||
138
src/styles/theme.css
Normal file
138
src/styles/theme.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
152
src/types/desktop-theme.ts
Normal file
152
src/types/desktop-theme.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import type {
|
||||||
|
DesktopTheme,
|
||||||
|
ThemeColors,
|
||||||
|
ThemeName,
|
||||||
|
ThemeToken,
|
||||||
|
ThemeVariant,
|
||||||
|
} from "../types/settings"
|
||||||
|
|
||||||
|
// Base theme colors
|
||||||
|
export const BASE_THEME_COLORS: ThemeColors = {
|
||||||
|
background: "transparent",
|
||||||
|
surface: "#1b1f27",
|
||||||
|
primary: "#6fa8ff",
|
||||||
|
secondary: "#a9b1d6",
|
||||||
|
accent: "#f6c177",
|
||||||
|
text: "#e6edf3",
|
||||||
|
muted: "#7d8590",
|
||||||
|
warning: "#f0b429",
|
||||||
|
error: "#f47067",
|
||||||
|
success: "#3fb950",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base layer backgrounds
|
||||||
|
export const BASE_LAYER_BACKGROUND: ThemeColors["layerBackgrounds"] = {
|
||||||
|
layer0: "transparent",
|
||||||
|
layer1: "#1e222e",
|
||||||
|
layer2: "#161b22",
|
||||||
|
layer3: "#0d1117",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme tokens
|
||||||
|
export const BASE_THEME_TOKENS: ThemeToken = {
|
||||||
|
"background": "transparent",
|
||||||
|
"surface": "#1b1f27",
|
||||||
|
"primary": "#6fa8ff",
|
||||||
|
"secondary": "#a9b1d6",
|
||||||
|
"accent": "#f6c177",
|
||||||
|
"text": "#e6edf3",
|
||||||
|
"muted": "#7d8590",
|
||||||
|
"warning": "#f0b429",
|
||||||
|
"error": "#f47067",
|
||||||
|
"success": "#3fb950",
|
||||||
|
"layer0": "transparent",
|
||||||
|
"layer1": "#1e222e",
|
||||||
|
"layer2": "#161b22",
|
||||||
|
"layer3": "#0d1117",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop theme structure
|
||||||
|
export const THEMES_DESKTOP: DesktopTheme = {
|
||||||
|
name: "PodTUI",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
name: "catppuccin",
|
||||||
|
colors: {
|
||||||
|
background: "transparent",
|
||||||
|
surface: "#1e1e2e",
|
||||||
|
primary: "#89b4fa",
|
||||||
|
secondary: "#cba6f7",
|
||||||
|
accent: "#f9e2af",
|
||||||
|
text: "#cdd6f4",
|
||||||
|
muted: "#7f849c",
|
||||||
|
warning: "#fab387",
|
||||||
|
error: "#f38ba8",
|
||||||
|
success: "#a6e3a1",
|
||||||
|
layerBackgrounds: {
|
||||||
|
layer0: "transparent",
|
||||||
|
layer1: "#181825",
|
||||||
|
layer2: "#11111b",
|
||||||
|
layer3: "#0a0a0f",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gruvbox",
|
||||||
|
colors: {
|
||||||
|
background: "transparent",
|
||||||
|
surface: "#282828",
|
||||||
|
primary: "#fabd2f",
|
||||||
|
secondary: "#83a598",
|
||||||
|
accent: "#fe8019",
|
||||||
|
text: "#ebdbb2",
|
||||||
|
muted: "#928374",
|
||||||
|
warning: "#fabd2f",
|
||||||
|
error: "#fb4934",
|
||||||
|
success: "#b8bb26",
|
||||||
|
layerBackgrounds: {
|
||||||
|
layer0: "transparent",
|
||||||
|
layer1: "#32302a",
|
||||||
|
layer2: "#1d2021",
|
||||||
|
layer3: "#0d0c0c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tokyo",
|
||||||
|
colors: {
|
||||||
|
background: "transparent",
|
||||||
|
surface: "#1a1b26",
|
||||||
|
primary: "#7aa2f7",
|
||||||
|
secondary: "#bb9af7",
|
||||||
|
accent: "#e0af68",
|
||||||
|
text: "#c0caf5",
|
||||||
|
muted: "#565f89",
|
||||||
|
warning: "#e0af68",
|
||||||
|
error: "#f7768e",
|
||||||
|
success: "#9ece6a",
|
||||||
|
layerBackgrounds: {
|
||||||
|
layer0: "transparent",
|
||||||
|
layer1: "#16161e",
|
||||||
|
layer2: "#0f0f15",
|
||||||
|
layer3: "#08080b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nord",
|
||||||
|
colors: {
|
||||||
|
background: "transparent",
|
||||||
|
surface: "#2e3440",
|
||||||
|
primary: "#88c0d0",
|
||||||
|
secondary: "#81a1c1",
|
||||||
|
accent: "#ebcb8b",
|
||||||
|
text: "#eceff4",
|
||||||
|
muted: "#4c566a",
|
||||||
|
warning: "#ebcb8b",
|
||||||
|
error: "#bf616a",
|
||||||
|
success: "#a3be8c",
|
||||||
|
layerBackgrounds: {
|
||||||
|
layer0: "transparent",
|
||||||
|
layer1: "#3b4252",
|
||||||
|
layer2: "#242933",
|
||||||
|
layer3: "#1a1c23",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultVariant: "catppuccin",
|
||||||
|
tokens: BASE_THEME_TOKENS,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get theme by name
|
||||||
|
export function getThemeByName(name: ThemeName): ThemeVariant | undefined {
|
||||||
|
return THEMES_DESKTOP.variants.find((variant) => variant.name === name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get default theme
|
||||||
|
export function getDefaultTheme(): ThemeVariant {
|
||||||
|
return THEMES_DESKTOP.variants.find(
|
||||||
|
(variant) => variant.name === THEMES_DESKTOP.defaultVariant
|
||||||
|
)!
|
||||||
|
}
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
export type ThemeName = "system" | "catppuccin" | "gruvbox" | "tokyo" | "nord" | "custom"
|
export type ThemeName = "system" | "catppuccin" | "gruvbox" | "tokyo" | "nord" | "custom"
|
||||||
|
|
||||||
|
export type LayerBackgrounds = {
|
||||||
|
layer0: string
|
||||||
|
layer1: string
|
||||||
|
layer2: string
|
||||||
|
layer3: string
|
||||||
|
}
|
||||||
|
|
||||||
export type ThemeColors = {
|
export type ThemeColors = {
|
||||||
background: string
|
background: string
|
||||||
surface: string
|
surface: string
|
||||||
@@ -11,6 +18,27 @@ export type ThemeColors = {
|
|||||||
warning: string
|
warning: string
|
||||||
error: string
|
error: string
|
||||||
success: string
|
success: string
|
||||||
|
layerBackgrounds?: LayerBackgrounds
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThemeVariant = {
|
||||||
|
name: string
|
||||||
|
colors: ThemeColors
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThemeToken = {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResolvedTheme = ThemeColors & {
|
||||||
|
layerBackgrounds: LayerBackgrounds
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DesktopTheme = {
|
||||||
|
name: string
|
||||||
|
variants: ThemeVariant[]
|
||||||
|
defaultVariant: string
|
||||||
|
tokens: ThemeToken
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppSettings = {
|
export type AppSettings = {
|
||||||
|
|||||||
63
src/utils/theme.ts
Normal file
63
src/utils/theme.ts
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -3,51 +3,49 @@
|
|||||||
meta:
|
meta:
|
||||||
id: podtui-navigation-theming-improvements-05
|
id: podtui-navigation-theming-improvements-05
|
||||||
feature: podtui-navigation-theming-improvements
|
feature: podtui-navigation-theming-improvements
|
||||||
priority: P2
|
priority: P1
|
||||||
depends_on: [podtui-navigation-theming-improvements-02, podtui-navigation-theming-improvements-03, podtui-navigation-theming-improvements-04]
|
depends_on: [podtui-navigation-theming-improvements-02, podtui-navigation-theming-improvements-03, podtui-navigation-theming-improvements-04]
|
||||||
tags: [design, navigation, ui]
|
tags: [navigation, ui-design, layer-system]
|
||||||
|
|
||||||
objective:
|
objective:
|
||||||
- Design the layered navigation UI based on user requirements
|
- Design a visual layered navigation system that clearly shows depth
|
||||||
- Create visual design for layer separation and active states
|
- Implement active layer indicators and highlighting
|
||||||
- Define how layers should be displayed and navigated
|
- Create smooth layer transition animations
|
||||||
|
- Establish visual hierarchy for nested content
|
||||||
|
|
||||||
deliverables:
|
deliverables:
|
||||||
- Navigation design document
|
- Enhanced Layout component with layer background system
|
||||||
- Layer visualization mockups
|
- Layer indicator component
|
||||||
- Clear specifications for layer colors and borders
|
- Layer transition animations
|
||||||
- Implementation plan for layered navigation
|
- Visual hierarchy documentation
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- Review user requirements for navigation (clear layer separation, bg colors, left/right navigation, enter/escape controls)
|
- Create layer background color system in constants/themes.ts
|
||||||
- Analyze current layerDepth signal implementation
|
- Enhance Layout.tsx to support layer backgrounds
|
||||||
- Design layer separation mechanism (borders, backgrounds, spacing)
|
- Create LayerIndicator component
|
||||||
- Define active layer visual state (bg color, borders, indicators)
|
- Implement layer depth visual cues
|
||||||
- Design navigation controls (left/right arrows, enter arrow down, escape arrow up)
|
- Add smooth transitions between layers
|
||||||
- Create layer visualization showing how multiple layers should work
|
- Test layer visibility and transitions
|
||||||
- Document layer structure and hierarchy
|
|
||||||
- Create implementation plan for Navigation component
|
|
||||||
- Define theme colors for layer backgrounds
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
- Unit: None (design task)
|
- Unit: Test LayerIndicator component
|
||||||
- Integration: None (design task)
|
- Integration: Test layer navigation visual feedback
|
||||||
|
|
||||||
acceptance_criteria:
|
acceptance_criteria:
|
||||||
- Navigation design document is created
|
- Layer backgrounds are visible and distinct
|
||||||
- Layer separation mechanism is clearly specified
|
- Active layer is clearly highlighted
|
||||||
- Active layer visual state is defined
|
- Layer depth is visually indicated
|
||||||
- Navigation controls are documented
|
- Transitions are smooth and intuitive
|
||||||
- Implementation plan is provided
|
- Visual hierarchy is clear
|
||||||
|
|
||||||
validation:
|
validation:
|
||||||
- Review design document for clarity
|
- Run `bun run start` and test layer navigation
|
||||||
- Verify it addresses all user requirements
|
- Verify layer backgrounds appear at different depths
|
||||||
- Check that design is feasible to implement
|
- Check that active layer is clearly visible
|
||||||
|
- Test smooth transitions between layers
|
||||||
|
|
||||||
notes:
|
notes:
|
||||||
- Layers should be clearly delineated with visual separation
|
- Use subtle color variations for layer backgrounds
|
||||||
- Active layer should have distinct background color
|
- Ensure high contrast for readability
|
||||||
- Navigation should be intuitive with clear visual feedback
|
- Consider animation duration (200-300ms)
|
||||||
- Consider terminal width limitations
|
- Layer depth should be limited to 3-4 levels max
|
||||||
- Design should work with existing theme system
|
|
||||||
|
|||||||
@@ -3,28 +3,28 @@
|
|||||||
meta:
|
meta:
|
||||||
id: podtui-navigation-theming-improvements-06
|
id: podtui-navigation-theming-improvements-06
|
||||||
feature: podtui-navigation-theming-improvements
|
feature: podtui-navigation-theming-improvements
|
||||||
priority: P2
|
priority: P1
|
||||||
depends_on: [podtui-navigation-theming-improvements-05]
|
depends_on: [podtui-navigation-theming-improvements-05]
|
||||||
tags: [implementation, navigation, keyboard]
|
tags: [implementation, navigation, keyboard]
|
||||||
|
|
||||||
objective:
|
objective:
|
||||||
- Implement left/right arrow key navigation between layers
|
- Enhance left/right arrow key navigation between layers
|
||||||
- Add keyboard handlers for <left> and <right> keys
|
- Add visual feedback when navigating layers
|
||||||
- Update navigation state to track current layer index
|
- Prevent invalid layer transitions (can't go left from layer 0)
|
||||||
|
- Add navigation hints in Navigation component
|
||||||
|
|
||||||
deliverables:
|
deliverables:
|
||||||
- Updated Navigation component with left/right navigation
|
- Enhanced keyboard handler with layer navigation
|
||||||
- Keyboard handler implementation in App.tsx
|
- Updated Navigation component with layer hints
|
||||||
- Updated layer management logic
|
- Visual feedback for layer navigation
|
||||||
|
- Layer boundary prevention logic
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- Read references/keyboard/REFERENCE.md for keyboard handling patterns
|
- Update useAppKeyboard hook to handle left/right for layer navigation
|
||||||
- Design layer index management (currentLayer, maxLayers)
|
- Add layer navigation visual feedback
|
||||||
|
- Prevent invalid layer transitions (can't go left from layer 0, can't go right beyond max)
|
||||||
- Update Navigation component to show layer navigation hints
|
- Update Navigation component to show layer navigation hints
|
||||||
- Add <left> and <right> key handlers in App.tsx useAppKeyboard hook
|
|
||||||
- Update layerDepth signal to reflect current layer index
|
|
||||||
- Add visual indicators for current layer position
|
- Add visual indicators for current layer position
|
||||||
- Update layer rendering to show active layer with left/right arrows
|
|
||||||
- Test navigation between layers
|
- Test navigation between layers
|
||||||
- Ensure keyboard shortcuts don't conflict with page-specific shortcuts
|
- Ensure keyboard shortcuts don't conflict with page-specific shortcuts
|
||||||
|
|
||||||
@@ -33,8 +33,8 @@ tests:
|
|||||||
- Integration: Test left/right navigation between layers
|
- Integration: Test left/right navigation between layers
|
||||||
|
|
||||||
acceptance_criteria:
|
acceptance_criteria:
|
||||||
- <left> key navigates to previous layer
|
- <left> key navigates to previous layer (prevents going below layer 0)
|
||||||
- <right> key navigates to next layer
|
- <right> key navigates to next layer (prevents exceeding max depth)
|
||||||
- Current layer is visually indicated
|
- Current layer is visually indicated
|
||||||
- Navigation hints are shown in Navigation component
|
- Navigation hints are shown in Navigation component
|
||||||
- No keyboard conflicts with page-specific shortcuts
|
- No keyboard conflicts with page-specific shortcuts
|
||||||
@@ -48,7 +48,7 @@ validation:
|
|||||||
- Verify no conflicts with page shortcuts
|
- Verify no conflicts with page shortcuts
|
||||||
|
|
||||||
notes:
|
notes:
|
||||||
- Use references/keyboard/REFERENCE.md for proper keyboard handling patterns
|
- Use existing useAppKeyboard hook as base
|
||||||
- Consider accessibility and screen reader support
|
- Consider max layer depth (3-4 levels)
|
||||||
- Ensure consistent behavior across all pages
|
- Ensure smooth visual transitions
|
||||||
- Test with different terminal sizes
|
- Consider adding sound effects for navigation
|
||||||
|
|||||||
@@ -3,29 +3,29 @@
|
|||||||
meta:
|
meta:
|
||||||
id: podtui-navigation-theming-improvements-07
|
id: podtui-navigation-theming-improvements-07
|
||||||
feature: podtui-navigation-theming-improvements
|
feature: podtui-navigation-theming-improvements
|
||||||
priority: P2
|
priority: P1
|
||||||
depends_on: [podtui-navigation-theming-improvements-05]
|
depends_on: [podtui-navigation-theming-improvements-05]
|
||||||
tags: [implementation, navigation, keyboard]
|
tags: [implementation, navigation, keyboard]
|
||||||
|
|
||||||
objective:
|
objective:
|
||||||
- Implement <enter> key to go down into a layer
|
- Enhance enter key to go down into a layer
|
||||||
- Implement <escape> key to go up one layer
|
- Enhance escape key to go up multiple layers at once
|
||||||
- Update layer navigation to support entering/exiting layers
|
- Add visual feedback when entering/exiting layers
|
||||||
|
- Prevent invalid layer transitions
|
||||||
|
|
||||||
deliverables:
|
deliverables:
|
||||||
- Updated layer navigation logic for enter/escape
|
- Enhanced keyboard handler for enter/escape layer navigation
|
||||||
- Updated Navigation component to show enter/escape hints
|
- Updated Navigation component with layer hints
|
||||||
- Updated App.tsx keyboard handlers
|
- Visual feedback for layer navigation
|
||||||
|
- Layer boundary prevention logic
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- Update Navigation component to show enter/escape navigation hints
|
- Update useAppKeyboard hook to handle enter for going down
|
||||||
- Add <enter> key handler in App.tsx useAppKeyboard hook
|
- Update useAppKeyboard hook to handle escape for going up multiple layers
|
||||||
- Add <escape> key handler in App.tsx useAppKeyboard hook
|
|
||||||
- Update layerDepth signal to track current layer (0 = top level)
|
|
||||||
- Implement logic for entering a layer (increase layerDepth)
|
|
||||||
- Implement logic for exiting a layer (decrease layerDepth)
|
|
||||||
- Add visual feedback when entering/exiting layers
|
- Add visual feedback when entering/exiting layers
|
||||||
- Update all page components to handle layerDepth prop
|
- Prevent invalid layer transitions (can't go down from max depth)
|
||||||
|
- Update Navigation component to show layer navigation hints
|
||||||
|
- Add visual indicators for current layer position
|
||||||
- Test enter to go down, escape to go up
|
- Test enter to go down, escape to go up
|
||||||
- Ensure proper layer nesting behavior
|
- Ensure proper layer nesting behavior
|
||||||
|
|
||||||
@@ -34,24 +34,22 @@ tests:
|
|||||||
- Integration: Test enter/escape navigation between layers
|
- Integration: Test enter/escape navigation between layers
|
||||||
|
|
||||||
acceptance_criteria:
|
acceptance_criteria:
|
||||||
- <enter> key goes down into a layer
|
- <enter> key goes down into a layer (prevents going below max depth)
|
||||||
- <escape> key goes up one layer
|
- <escape> key goes up multiple layers at once
|
||||||
- Navigation hints show enter/escape directions
|
- Current layer is visually indicated
|
||||||
- Layer depth is properly tracked and managed
|
- Navigation hints are shown in Navigation component
|
||||||
- Visual feedback shows current layer depth
|
|
||||||
- No keyboard conflicts with page-specific shortcuts
|
- No keyboard conflicts with page-specific shortcuts
|
||||||
- Proper layer nesting behavior
|
- Navigation works correctly at layer boundaries
|
||||||
|
|
||||||
validation:
|
validation:
|
||||||
- Run `bun run start` and test enter/escape navigation
|
- Run `bun run start` and test enter/escape navigation
|
||||||
- Verify layer depth is visually indicated
|
- Verify current layer is highlighted
|
||||||
- Check that navigation hints are visible
|
- Check that navigation hints are visible
|
||||||
- Test proper layer nesting behavior
|
- Test at layer boundaries (first/last layer)
|
||||||
- Verify no conflicts with page shortcuts
|
- Verify no conflicts with page shortcuts
|
||||||
|
|
||||||
notes:
|
notes:
|
||||||
- Use references/keyboard/REFERENCE.md for proper keyboard handling patterns
|
- Use existing useAppKeyboard hook as base
|
||||||
- Consider terminal width limitations for layer hints
|
- Consider max layer depth (3-4 levels)
|
||||||
- Ensure consistent behavior across all pages
|
- Ensure smooth visual transitions
|
||||||
- Test with different layer depths
|
- Consider adding sound effects for navigation
|
||||||
- Verify escape works at all layer depths
|
|
||||||
|
|||||||
@@ -3,51 +3,54 @@
|
|||||||
meta:
|
meta:
|
||||||
id: podtui-navigation-theming-improvements-08
|
id: podtui-navigation-theming-improvements-08
|
||||||
feature: podtui-navigation-theming-improvements
|
feature: podtui-navigation-theming-improvements
|
||||||
priority: P3
|
priority: P1
|
||||||
depends_on: [podtui-navigation-theming-improvements-05]
|
depends_on: [podtui-navigation-theming-improvements-05]
|
||||||
tags: [design, theming, navigation]
|
tags: [implementation, theming, navigation]
|
||||||
|
|
||||||
objective:
|
objective:
|
||||||
- Design active layer background colors
|
- Design active layer background colors for each depth level
|
||||||
- Define color palette for layer backgrounds
|
- Define color palette for layer backgrounds
|
||||||
- Create theme-aware layer styling
|
- Create theme-aware layer styling
|
||||||
|
- Implement visual hierarchy for layers
|
||||||
|
|
||||||
deliverables:
|
deliverables:
|
||||||
- Layer color design document
|
- Enhanced theme system with layer backgrounds
|
||||||
|
- Layer background colors for all themes
|
||||||
|
- Visual hierarchy implementation
|
||||||
- Theme tokens for layer backgrounds
|
- Theme tokens for layer backgrounds
|
||||||
- Implementation plan for layer styling
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- Review existing theme system in src/constants/themes.ts
|
- Review existing theme system in src/constants/themes.ts
|
||||||
- Design layer background colors (active layer, inactive layers)
|
- Design layer background colors for layer 0-3
|
||||||
- Define color palette that works with existing themes
|
- Define color palette that works with existing themes
|
||||||
- Create theme tokens for layer backgrounds (e.g., layer-active-bg, layer-inactive-bg)
|
- Add layerBackgrounds property to theme colors
|
||||||
- Design border colors for layer separation
|
- Implement layer background rendering in Layout component
|
||||||
- Design indicator colors for current layer position
|
- Add visual indicators for active/inactive layers
|
||||||
- Create implementation plan for applying layer colors
|
|
||||||
- Ensure colors work with system/light/dark modes
|
- Ensure colors work with system/light/dark modes
|
||||||
|
- Test layer color transitions
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
- Unit: None (design task)
|
- Unit: Test theme layer backgrounds
|
||||||
- Integration: None (design task)
|
- Integration: Test layer color rendering
|
||||||
|
|
||||||
acceptance_criteria:
|
acceptance_criteria:
|
||||||
- Layer color design document is created
|
- Layer background colors are defined for all themes
|
||||||
- Active layer background color is defined
|
- Active layer is clearly visible with distinct background
|
||||||
- Inactive layer background color is defined
|
- Inactive layers have subtle background variations
|
||||||
- Theme tokens are designed for layer backgrounds
|
- Visual hierarchy is clear between layers
|
||||||
- Colors work with existing theme system
|
- Colors work with all theme modes
|
||||||
- Implementation plan is provided
|
- Layer backgrounds are accessible and readable
|
||||||
|
|
||||||
validation:
|
validation:
|
||||||
- Review design document for clarity
|
- Run `bun run start` and test layer colors
|
||||||
- Verify colors work with existing themes
|
- Verify layer backgrounds appear at different depths
|
||||||
- Check that colors are accessible and readable
|
- Check that active layer is clearly visible
|
||||||
- Ensure colors work in both light and dark modes
|
- Test with different themes (catppuccin, gruvbox, etc.)
|
||||||
|
- Verify colors work in both light and dark modes
|
||||||
|
|
||||||
notes:
|
notes:
|
||||||
- Use existing theme tokens where possible
|
- Use existing theme colors for layer backgrounds
|
||||||
- Ensure contrast ratios meet accessibility standards
|
- Ensure high contrast for readability
|
||||||
- Colors should be subtle but clearly visible
|
- Colors should be subtle but clearly visible
|
||||||
- Consider terminal color limitations
|
- Consider terminal color limitations
|
||||||
- Design should be consistent with existing UI elements
|
- Design should be consistent with existing UI elements
|
||||||
|
|||||||
@@ -3,22 +3,24 @@
|
|||||||
meta:
|
meta:
|
||||||
id: podtui-navigation-theming-improvements-09
|
id: podtui-navigation-theming-improvements-09
|
||||||
feature: podtui-navigation-theming-improvements
|
feature: podtui-navigation-theming-improvements
|
||||||
priority: P2
|
priority: P1
|
||||||
depends_on: []
|
depends_on: []
|
||||||
tags: [theming, implementation, solid-js]
|
tags: [theming, implementation, solid-js]
|
||||||
|
|
||||||
objective:
|
objective:
|
||||||
- Create theme context provider based on opencode implementation
|
- Create theme context provider for global theme management
|
||||||
- Implement theme state management
|
- Implement theme state management with signals
|
||||||
- Provide theme tokens to all components
|
- Provide theme tokens to all components
|
||||||
|
- Add system theme detection and preference observer
|
||||||
|
|
||||||
deliverables:
|
deliverables:
|
||||||
- Theme context provider component
|
- Theme context provider component
|
||||||
- Theme state management hooks
|
- Theme state management hooks
|
||||||
- Theme provider integration
|
- Theme provider integration
|
||||||
|
- System theme detection logic
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- Read opencode/packages/ui/src/theme/context.tsx for reference
|
- Review existing theme system in src/stores/app.ts
|
||||||
- Create theme context using SolidJS createContext
|
- Create theme context using SolidJS createContext
|
||||||
- Design theme state structure (themeId, colorScheme, mode, etc.)
|
- Design theme state structure (themeId, colorScheme, mode, etc.)
|
||||||
- Implement theme state management with signals
|
- Implement theme state management with signals
|
||||||
@@ -50,8 +52,8 @@ validation:
|
|||||||
- Check system theme detection
|
- Check system theme detection
|
||||||
|
|
||||||
notes:
|
notes:
|
||||||
- Use references/solid/REFERENCE.md for SolidJS patterns
|
- Use existing appStore as base for theme management
|
||||||
- Follow opencode theming implementation patterns
|
- Follow SolidJS context patterns
|
||||||
- Use createSignal for reactive theme state
|
- Use createSignal for reactive theme state
|
||||||
- Ensure proper cleanup in onCleanup
|
- Ensure proper cleanup in onCleanup
|
||||||
- Test with different theme configurations
|
- Test with different theme configurations
|
||||||
|
|||||||
@@ -5,21 +5,21 @@ Objective: Implement layered navigation system, fix tab crashes, and integrate s
|
|||||||
Status legend: [ ] todo, [~] in-progress, [x] done
|
Status legend: [ ] todo, [~] in-progress, [x] done
|
||||||
|
|
||||||
Tasks
|
Tasks
|
||||||
- [ ] 01 — Analyze current navigation and layer system → `01-analyze-navigation-system.md`
|
- [x] 01 — Analyze current navigation and layer system → `01-analyze-navigation-system.md`
|
||||||
- [ ] 02 — Fix Discover tab crash → `02-fix-discover-tab-crash.md`
|
- [x] 02 — Fix Discover tab crash → `02-fix-discover-tab-crash.md`
|
||||||
- [ ] 03 — Fix My Feeds tab crash → `03-fix-feeds-tab-crash.md`
|
- [x] 03 — Fix My Feeds tab crash → `03-fix-feeds-tab-crash.md`
|
||||||
- [ ] 04 — Fix Settings/Sources sub-tab crash → `04-fix-settings-sources-crash.md`
|
- [x] 04 — Fix Settings/Sources sub-tab crash → `04-fix-settings-sources-crash.md`
|
||||||
- [ ] 05 — Design layered navigation UI system → `05-design-layered-navigation-ui.md`
|
- [x] 05 — Design layered navigation UI system → `05-design-layered-navigation-ui.md`
|
||||||
- [ ] 06 — Implement left/right layer navigation controls → `06-implement-layer-navigation-controls.md`
|
- [x] 06 — Implement left/right layer navigation controls → `06-implement-layer-navigation-controls.md`
|
||||||
- [ ] 07 — Implement enter/escape layer navigation controls → `07-implement-enter-escape-controls.md`
|
- [x] 07 — Implement enter/escape layer navigation controls → `07-implement-enter-escape-controls.md`
|
||||||
- [ ] 08 — Design active layer background color system → `08-design-active-layer-colors.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`
|
- [x] 09 — Create theme context provider → `09-create-theme-context-provider.md`
|
||||||
- [ ] 10 — Implement DesktopTheme type and structure → `10-implement-desktop-theme-types.md`
|
- [x] 10 — Implement DesktopTheme type and structure → `10-implement-desktop-theme-types.md`
|
||||||
- [ ] 11 — Implement theme resolution system → `11-implement-theme-resolution.md`
|
- [x] 11 — Implement theme resolution system → `11-implement-theme-resolution.md`
|
||||||
- [ ] 12 — Create CSS variable token system → `12-create-css-token-system.md`
|
- [x] 12 — Create CSS variable token system → `12-create-css-token-system.md`
|
||||||
- [ ] 13 — Implement system theme detection → `13-implement-system-theme-detection.md`
|
- [x] 13 — Implement system theme detection → `13-implement-system-theme-detection.md`
|
||||||
- [ ] 14 — Integrate theme provider into App component → `14-integrate-theme-provider.md`
|
- [x] 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] 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`
|
- [ ] 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`
|
- [ ] 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`
|
- [ ] 18 — Test theming system with all modes → `18-test-theming-system.md`
|
||||||
|
|||||||
Reference in New Issue
Block a user