Compare commits

...

4 Commits

Author SHA1 Message Date
624a6ba022 pause to fix nav/theme 2026-02-04 12:38:35 -05:00
cdabf2c3e0 checkpoint 2026-02-04 12:10:30 -05:00
b8549777ba missing md 2026-02-04 11:36:47 -05:00
9b1a3585e6 fix 2026-02-04 11:24:19 -05:00
54 changed files with 2931 additions and 94 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.opencode
opencode
# dependencies (bun install)
node_modules

View File

@@ -1,58 +1,75 @@
import { createSignal } from "solid-js"
import { Layout } from "./components/Layout"
import { Navigation } from "./components/Navigation"
import { TabNavigation } from "./components/TabNavigation"
import { SyncPanel } from "./components/SyncPanel"
import { FeedList } from "./components/FeedList"
import { LoginScreen } from "./components/LoginScreen"
import { CodeValidation } from "./components/CodeValidation"
import { OAuthPlaceholder } from "./components/OAuthPlaceholder"
import { SyncProfile } from "./components/SyncProfile"
import { SearchPage } from "./components/SearchPage"
import { DiscoverPage } from "./components/DiscoverPage"
import { useAuthStore } from "./stores/auth"
import { useFeedStore } from "./stores/feed"
import { FeedVisibility } from "./types/feed"
import { useAppKeyboard } from "./hooks/useAppKeyboard"
import type { TabId } from "./components/Tab"
import type { AuthScreen } from "./types/auth"
import { createSignal } from "solid-js";
import { Layout } from "./components/Layout";
import { Navigation } from "./components/Navigation";
import { TabNavigation } from "./components/TabNavigation";
import { FeedList } from "./components/FeedList";
import { LoginScreen } from "./components/LoginScreen";
import { CodeValidation } from "./components/CodeValidation";
import { OAuthPlaceholder } from "./components/OAuthPlaceholder";
import { SyncProfile } from "./components/SyncProfile";
import { SearchPage } from "./components/SearchPage";
import { DiscoverPage } from "./components/DiscoverPage";
import { Player } from "./components/Player";
import { SettingsScreen } from "./components/SettingsScreen";
import { useAuthStore } from "./stores/auth";
import { useFeedStore } from "./stores/feed";
import { useAppStore } from "./stores/app";
import { FeedVisibility } from "./types/feed";
import { useAppKeyboard } from "./hooks/useAppKeyboard";
import type { TabId } from "./components/Tab";
import type { AuthScreen } from "./types/auth";
export function App() {
const [activeTab, setActiveTab] = createSignal<TabId>("discover")
const [authScreen, setAuthScreen] = createSignal<AuthScreen>("login")
const [showAuthPanel, setShowAuthPanel] = createSignal(false)
const [inputFocused, setInputFocused] = createSignal(false)
const auth = useAuthStore()
const feedStore = useFeedStore()
const [activeTab, setActiveTab] = createSignal<TabId>("settings");
const [authScreen, setAuthScreen] = createSignal<AuthScreen>("login");
const [showAuthPanel, setShowAuthPanel] = createSignal(false);
const [inputFocused, setInputFocused] = createSignal(false);
const [layerDepth, setLayerDepth] = createSignal(0);
const auth = useAuthStore();
const feedStore = useFeedStore();
const appStore = useAppStore();
// Centralized keyboard handler for all tab navigation and shortcuts
useAppKeyboard({
get activeTab() { return activeTab() },
get activeTab() {
return activeTab();
},
onTabChange: setActiveTab,
inputFocused: inputFocused(),
navigationEnabled: layerDepth() === 0,
onAction: (action) => {
if (action === "escape") {
setShowAuthPanel(false)
setInputFocused(false)
if (layerDepth() > 0) {
setLayerDepth(0);
setInputFocused(false);
} else {
setShowAuthPanel(false);
setInputFocused(false);
}
}
if (action === "enter" && layerDepth() === 0) {
setLayerDepth(1);
}
},
})
});
const renderContent = () => {
const tab = activeTab()
const tab = activeTab();
switch (tab) {
case "feeds":
return (
<FeedList
focused={true}
focused={layerDepth() > 0}
showEpisodeCount={true}
showLastUpdated={true}
onFocusChange={() => setLayerDepth(0)}
onOpenFeed={(feed) => {
// Would open feed detail view
}}
/>
)
);
case "settings":
// Show auth panel or sync panel based on state
@@ -60,123 +77,117 @@ export function App() {
if (auth.isAuthenticated) {
return (
<SyncProfile
focused={true}
focused={layerDepth() > 0}
onLogout={() => {
auth.logout()
setShowAuthPanel(false)
auth.logout();
setShowAuthPanel(false);
}}
onManageSync={() => setShowAuthPanel(false)}
/>
)
);
}
switch (authScreen()) {
case "code":
return (
<CodeValidation
focused={true}
focused={layerDepth() > 0}
onBack={() => setAuthScreen("login")}
/>
)
);
case "oauth":
return (
<OAuthPlaceholder
focused={true}
focused={layerDepth() > 0}
onBack={() => setAuthScreen("login")}
onNavigateToCode={() => setAuthScreen("code")}
/>
)
);
case "login":
default:
return (
<LoginScreen
focused={true}
focused={layerDepth() > 0}
onNavigateToCode={() => setAuthScreen("code")}
onNavigateToOAuth={() => setAuthScreen("oauth")}
/>
)
);
}
}
return (
<box flexDirection="column" gap={1}>
<SyncPanel />
<box height={1} />
<box border padding={1}>
<box flexDirection="row" gap={2}>
<text fg="gray">Account:</text>
{auth.isAuthenticated ? (
<text fg="green">Signed in as {auth.user?.email}</text>
) : (
<text fg="yellow">Not signed in</text>
)}
<box
border
padding={0}
onMouseDown={() => setShowAuthPanel(true)}
>
<text fg="cyan">
{auth.isAuthenticated ? "[A] Account" : "[A] Sign In"}
</text>
</box>
</box>
</box>
</box>
)
<SettingsScreen
onOpenAccount={() => setShowAuthPanel(true)}
accountLabel={
auth.isAuthenticated
? `Signed in as ${auth.user?.email}`
: "Not signed in"
}
accountStatus={auth.isAuthenticated ? "signed-in" : "signed-out"}
onExit={() => setLayerDepth(0)}
/>
);
case "discover":
return (
<DiscoverPage focused={!inputFocused()} />
)
<DiscoverPage
focused={layerDepth() > 0}
onExit={() => setLayerDepth(0)}
/>
);
case "search":
return (
<SearchPage
focused={!inputFocused()}
focused={layerDepth() > 0}
onInputFocusChange={setInputFocused}
onExit={() => setLayerDepth(0)}
onSubscribe={(result) => {
const feeds = feedStore.feeds()
const feeds = feedStore.feeds();
const alreadySubscribed = feeds.some(
(feed) =>
feed.podcast.id === result.podcast.id ||
feed.podcast.feedUrl === result.podcast.feedUrl
)
feed.podcast.feedUrl === result.podcast.feedUrl,
);
if (!alreadySubscribed) {
feedStore.addFeed(
{ ...result.podcast, isSubscribed: true },
result.sourceId,
FeedVisibility.PUBLIC
)
FeedVisibility.PUBLIC,
);
}
}}
/>
)
);
case "player":
return (
<Player focused={layerDepth() > 0} onExit={() => setLayerDepth(0)} />
);
default:
return (
<box border style={{ padding: 2 }}>
<text>
<strong>{tab}</strong>
<br />
Player - coming in later phases
Coming soon
</text>
</box>
)
);
}
}
};
return (
<Layout
theme={appStore.resolveTheme()}
header={
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} />
}
footer={
<Navigation activeTab={activeTab()} onTabSelect={setActiveTab} />
}
footer={<Navigation activeTab={activeTab()} onTabSelect={setActiveTab} />}
>
<box style={{ padding: 1 }}>{renderContent()}</box>
</Layout>
)
);
}

73
src/api/client.ts Normal file
View File

@@ -0,0 +1,73 @@
import type { Feed } from "../types/feed"
import type { Episode } from "../types/episode"
import type { Podcast } from "../types/podcast"
import type { PodcastSource } from "../types/source"
import { parseRSSFeed } from "@/api/rss-parser"
import { handleAPISource, handleCustomSource, handleRSSSource } from "@/api/source-handler"
export const fetchEpisodes = async (feedUrl: string): Promise<Episode[]> => {
try {
const response = await fetch(feedUrl)
if (!response.ok) return []
const xml = await response.text()
return parseRSSFeed(xml, feedUrl).episodes
} catch {
return []
}
}
export const fetchFeeds = async (
sourceIds: string[],
sources: PodcastSource[]
): Promise<Feed[]> => {
const active = sources.filter((source) => sourceIds.includes(source.id))
const feeds: Feed[] = []
await Promise.all(
active.map(async (source) => {
try {
if (source.type === "rss") {
const rssFeeds = await handleRSSSource(source)
feeds.push(...rssFeeds)
} else if (source.type === "api") {
const apiFeeds = await handleAPISource(source, "")
feeds.push(...apiFeeds)
} else {
const customFeeds = await handleCustomSource(source, "")
feeds.push(...customFeeds)
}
} catch {
// ignore individual source errors
}
})
)
return feeds
}
export const searchPodcasts = async (
query: string,
sources: PodcastSource[]
): Promise<Podcast[]> => {
const results: Podcast[] = []
await Promise.all(
sources.map(async (source) => {
try {
if (source.type === "rss") {
const feeds = await handleRSSSource(source)
results.push(...feeds.map((feed: Feed) => feed.podcast))
} else if (source.type === "api") {
const feeds = await handleAPISource(source, query)
results.push(...feeds.map((feed: Feed) => feed.podcast))
} else {
const feeds = await handleCustomSource(source, query)
results.push(...feeds.map((feed: Feed) => feed.podcast))
}
} catch {
// ignore errors
}
})
)
return results
}

53
src/api/rss-parser.ts Normal file
View File

@@ -0,0 +1,53 @@
import type { Podcast } from "../types/podcast"
import type { Episode } from "../types/episode"
const getTagValue = (xml: string, tag: string): string => {
const match = xml.match(new RegExp(`<${tag}[^>]*>([\s\S]*?)</${tag}>`, "i"))
return match?.[1]?.trim() ?? ""
}
const decodeEntities = (value: string) =>
value
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&amp;/g, "&")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
export const parseRSSFeed = (xml: string, feedUrl: string): Podcast & { episodes: Episode[] } => {
const channel = xml.match(/<channel[\s\S]*?<\/channel>/i)?.[0] ?? xml
const title = decodeEntities(getTagValue(channel, "title")) || "Untitled Podcast"
const description = decodeEntities(getTagValue(channel, "description"))
const author = decodeEntities(getTagValue(channel, "itunes:author"))
const lastUpdated = new Date()
const items = channel.match(/<item[\s\S]*?<\/item>/gi) ?? []
const episodes = items.map((item, index) => {
const epTitle = decodeEntities(getTagValue(item, "title")) || `Episode ${index + 1}`
const epDescription = decodeEntities(getTagValue(item, "description"))
const pubDate = new Date(getTagValue(item, "pubDate") || Date.now())
const enclosure = item.match(/<enclosure[^>]*url=["']([^"']+)["'][^>]*>/i)
const audioUrl = enclosure?.[1] ?? ""
return {
id: `${feedUrl}#${index}`,
podcastId: feedUrl,
title: epTitle,
description: epDescription,
audioUrl,
duration: 0,
pubDate,
}
})
return {
id: feedUrl,
title,
description,
author,
feedUrl,
lastUpdated,
isSubscribed: true,
episodes,
}
}

94
src/api/source-handler.ts Normal file
View File

@@ -0,0 +1,94 @@
import { FeedVisibility } from "../types/feed"
import type { Feed } from "../types/feed"
import type { PodcastSource } from "../types/source"
import type { Podcast } from "../types/podcast"
import { parseRSSFeed } from "./rss-parser"
const buildFeedFromPodcast = (podcast: Podcast, sourceId: string): Feed => {
return {
id: `${sourceId}-${podcast.id}`,
podcast,
episodes: [],
visibility: FeedVisibility.PUBLIC,
sourceId,
lastUpdated: new Date(),
isPinned: false,
}
}
export const handleRSSSource = async (source: PodcastSource): Promise<Feed[]> => {
if (!source.baseUrl) return []
const response = await fetch(source.baseUrl)
if (!response.ok) return []
const xml = await response.text()
const parsed = parseRSSFeed(xml, source.baseUrl)
return [
{
id: `${source.id}-${parsed.feedUrl}`,
podcast: {
id: parsed.id,
title: parsed.title,
description: parsed.description,
feedUrl: parsed.feedUrl,
author: parsed.author,
categories: parsed.categories,
lastUpdated: parsed.lastUpdated,
isSubscribed: true,
},
episodes: parsed.episodes,
visibility: FeedVisibility.PUBLIC,
sourceId: source.id,
lastUpdated: parsed.lastUpdated,
isPinned: false,
},
]
}
export const handleAPISource = async (
source: PodcastSource,
query: string
): Promise<Feed[]> => {
const url = new URL(source.baseUrl || "https://itunes.apple.com/search")
url.searchParams.set("term", query || "podcast")
url.searchParams.set("media", "podcast")
url.searchParams.set("entity", "podcast")
url.searchParams.set("country", source.country || "US")
url.searchParams.set("lang", source.language || "en_us")
const response = await fetch(url.toString())
if (!response.ok) return []
const data = (await response.json()) as { results?: Array<{ collectionId?: number; collectionName?: string; feedUrl?: string; artistName?: string }> }
const results = data.results ?? []
return results
.filter((item) => item.collectionName && item.feedUrl)
.map((item) => {
const podcast: Podcast = {
id: item.collectionId ? `itunes-${item.collectionId}` : `${source.id}-${item.collectionName}`,
title: item.collectionName || "Untitled Podcast",
description: item.collectionName || "",
feedUrl: item.feedUrl || "",
author: item.artistName,
lastUpdated: new Date(),
isSubscribed: false,
}
return buildFeedFromPodcast(podcast, source.id)
})
}
export const handleCustomSource = async (
source: PodcastSource,
query: string
): Promise<Feed[]> => {
if (!query) return []
const podcast: Podcast = {
id: `${source.id}-${query.toLowerCase().replace(/\s+/g, "-")}`,
title: `${query} Highlights`,
description: `Curated results for ${query}`,
feedUrl: source.baseUrl || "",
author: source.name,
lastUpdated: new Date(),
isSubscribed: false,
}
return [buildFeedFromPodcast(podcast, source.id)]
}

View File

@@ -10,6 +10,7 @@ import { TrendingShows } from "./TrendingShows"
type DiscoverPageProps = {
focused: boolean
onExit?: () => void
}
type FocusArea = "categories" | "shows"
@@ -36,18 +37,25 @@ export function DiscoverPage(props: DiscoverPageProps) {
return
}
if (key.name === "enter" && area === "categories") {
setFocusArea("shows")
return
}
// Category navigation
if (area === "categories") {
if (key.name === "left" || key.name === "h") {
setCategoryIndex((i) => Math.max(0, i - 1))
const cat = DISCOVER_CATEGORIES[categoryIndex()]
const nextIndex = Math.max(0, categoryIndex() - 1)
setCategoryIndex(nextIndex)
const cat = DISCOVER_CATEGORIES[nextIndex]
if (cat) discoverStore.setSelectedCategory(cat.id)
setShowIndex(0) // Reset show selection when changing category
setShowIndex(0)
return
}
if (key.name === "right" || key.name === "l") {
setCategoryIndex((i) => Math.min(DISCOVER_CATEGORIES.length - 1, i + 1))
const cat = DISCOVER_CATEGORIES[categoryIndex()]
const nextIndex = Math.min(DISCOVER_CATEGORIES.length - 1, categoryIndex() + 1)
setCategoryIndex(nextIndex)
const cat = DISCOVER_CATEGORIES[nextIndex]
if (cat) discoverStore.setSelectedCategory(cat.id)
setShowIndex(0)
return
@@ -67,10 +75,15 @@ export function DiscoverPage(props: DiscoverPageProps) {
if (area === "shows") {
const shows = discoverStore.filteredPodcasts()
if (key.name === "down" || key.name === "j") {
if (shows.length === 0) return
setShowIndex((i) => Math.min(i + 1, shows.length - 1))
return
}
if (key.name === "up" || key.name === "k") {
if (shows.length === 0) {
setFocusArea("categories")
return
}
const newIndex = showIndex() - 1
if (newIndex < 0) {
setFocusArea("categories")
@@ -89,6 +102,15 @@ export function DiscoverPage(props: DiscoverPageProps) {
}
}
if (key.name === "escape") {
if (area === "shows") {
setFocusArea("categories")
} else {
props.onExit?.()
}
return
}
// Refresh with 'r'
if (key.name === "r") {
discoverStore.refresh()
@@ -170,6 +192,7 @@ export function DiscoverPage(props: DiscoverPageProps) {
<text fg="gray">[Tab] Switch focus</text>
<text fg="gray">[j/k] Navigate</text>
<text fg="gray">[Enter] Subscribe</text>
<text fg="gray">[Esc] Up</text>
<text fg="gray">[R] Refresh</text>
</box>
</box>

View File

@@ -4,6 +4,7 @@
*/
import { createSignal, For, Show } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import type { Feed } from "../types/feed"
import type { Episode } from "../types/episode"
import { format } from "date-fns"
@@ -72,6 +73,11 @@ export function FeedDetail(props: FeedDetailProps) {
}
}
useKeyboard((key) => {
if (!props.focused) return
handleKeyPress(key)
})
return (
<box flexDirection="column" gap={1}>
{/* Header with back button */}

View File

@@ -4,6 +4,7 @@
*/
import { createSignal, For, Show } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { FeedItem } from "./FeedItem"
import { useFeedStore } from "../stores/feed"
import { FeedVisibility, FeedSortField } from "../types/feed"
@@ -16,6 +17,7 @@ interface FeedListProps {
showLastUpdated?: boolean
onSelectFeed?: (feed: Feed) => void
onOpenFeed?: (feed: Feed) => void
onFocusChange?: (focused: boolean) => void
}
export function FeedList(props: FeedListProps) {
@@ -25,6 +27,10 @@ export function FeedList(props: FeedListProps) {
const filteredFeeds = () => feedStore.getFilteredFeeds()
const handleKeyPress = (key: { name: string }) => {
if (key.name === "escape") {
props.onFocusChange?.(false)
return
}
const feeds = filteredFeeds()
if (key.name === "up" || key.name === "k") {
@@ -65,6 +71,11 @@ export function FeedList(props: FeedListProps) {
}
}
useKeyboard((key) => {
if (!props.focused) return
handleKeyPress(key)
})
const cycleVisibilityFilter = () => {
const current = feedStore.filter().visibility
let next: FeedVisibility | "all"
@@ -174,7 +185,7 @@ export function FeedList(props: FeedListProps) {
{/* Navigation help */}
<box paddingTop={0}>
<text fg="gray">
j/k navigate | Enter open | p pin | f filter | s sort | Click to select
Enter open | Esc up | j/k navigate | p pin | f filter | s sort
</text>
</box>
</box>

View File

@@ -1,14 +1,21 @@
import type { JSX } from "solid-js"
import type { ThemeColors } from "../types/settings"
type LayoutProps = {
header?: JSX.Element
footer?: JSX.Element
children?: JSX.Element
theme?: ThemeColors
}
export function Layout(props: LayoutProps) {
return (
<box style={{ flexDirection: "column", width: "100%", height: "100%" }}>
<box
flexDirection="column"
width="100%"
height="100%"
backgroundColor={props.theme?.background}
>
{props.header ? <box style={{ height: 3 }}>{props.header}</box> : <text></text>}
<box style={{ flexGrow: 1 }}>{props.children}</box>
{props.footer ? <box style={{ height: 1 }}>{props.footer}</box> : <text></text>}

View File

@@ -0,0 +1,34 @@
type PlaybackControlsProps = {
isPlaying: boolean
volume: number
speed: number
onToggle: () => void
onPrev: () => void
onNext: () => void
onVolumeChange: (value: number) => void
onSpeedChange: (value: number) => void
}
export function PlaybackControls(props: PlaybackControlsProps) {
return (
<box flexDirection="row" gap={1} alignItems="center" border padding={1}>
<box border padding={0} onMouseDown={props.onPrev}>
<text fg="cyan">[Prev]</text>
</box>
<box border padding={0} onMouseDown={props.onToggle}>
<text fg="cyan">{props.isPlaying ? "[Pause]" : "[Play]"}</text>
</box>
<box border padding={0} onMouseDown={props.onNext}>
<text fg="cyan">[Next]</text>
</box>
<box flexDirection="row" gap={1} marginLeft={2}>
<text fg="gray">Vol</text>
<text fg="white">{Math.round(props.volume * 100)}%</text>
</box>
<box flexDirection="row" gap={1} marginLeft={2}>
<text fg="gray">Speed</text>
<text fg="white">{props.speed}x</text>
</box>
</box>
)
}

114
src/components/Player.tsx Normal file
View File

@@ -0,0 +1,114 @@
import { createSignal } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { PlaybackControls } from "./PlaybackControls"
import { Waveform } from "./Waveform"
import { createWaveform } from "../utils/waveform"
import type { Episode } from "../types/episode"
type PlayerProps = {
focused: boolean
onExit?: () => void
}
const SAMPLE_EPISODE: Episode = {
id: "sample-ep",
podcastId: "sample-podcast",
title: "A Tour of the Productive Mind",
description: "A short guided session on building creative focus.",
audioUrl: "",
duration: 2780,
pubDate: new Date(),
}
export function Player(props: PlayerProps) {
const [isPlaying, setIsPlaying] = createSignal(false)
const [position, setPosition] = createSignal(0)
const [volume, setVolume] = createSignal(0.7)
const [speed, setSpeed] = createSignal(1)
const waveform = () => createWaveform(64)
useKeyboard((key: { name: string }) => {
if (!props.focused) return
if (key.name === "space") {
setIsPlaying((value: boolean) => !value)
return
}
if (key.name === "escape") {
props.onExit?.()
return
}
if (key.name === "left") {
setPosition((value: number) => Math.max(0, value - 10))
}
if (key.name === "right") {
setPosition((value: number) => Math.min(SAMPLE_EPISODE.duration, value + 10))
}
if (key.name === "up") {
setVolume((value: number) => Math.min(1, Number((value + 0.05).toFixed(2))))
}
if (key.name === "down") {
setVolume((value: number) => Math.max(0, Number((value - 0.05).toFixed(2))))
}
if (key.name === "s") {
setSpeed((value: number) => (value >= 2 ? 0.5 : Number((value + 0.25).toFixed(2))))
}
})
const progressPercent = () => Math.round((position() / SAMPLE_EPISODE.duration) * 100)
return (
<box flexDirection="column" gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text>
<strong>Now Playing</strong>
</text>
<text fg="gray">
Episode {Math.floor(position() / 60)}:{String(Math.floor(position() % 60)).padStart(2, "0")}
</text>
</box>
<box border padding={1} flexDirection="column" gap={1}>
<text fg="white">
<strong>{SAMPLE_EPISODE.title}</strong>
</text>
<text fg="gray">{SAMPLE_EPISODE.description}</text>
<box flexDirection="column" gap={1}>
<box flexDirection="row" gap={1} alignItems="center">
<text fg="gray">Progress:</text>
<box flexGrow={1} height={1} backgroundColor="#2a2f3a">
<box
width={`${progressPercent()}%`}
height={1}
backgroundColor={isPlaying() ? "#6fa8ff" : "#7d8590"}
/>
</box>
<text fg="gray">{progressPercent()}%</text>
</box>
<Waveform
data={waveform()}
position={position()}
duration={SAMPLE_EPISODE.duration}
isPlaying={isPlaying()}
onSeek={(next: number) => setPosition(next)}
/>
</box>
</box>
<PlaybackControls
isPlaying={isPlaying()}
volume={volume()}
speed={speed()}
onToggle={() => setIsPlaying((value: boolean) => !value)}
onPrev={() => setPosition(0)}
onNext={() => setPosition(SAMPLE_EPISODE.duration)}
onSpeedChange={setSpeed}
onVolumeChange={setVolume}
/>
<text fg="gray">Enter dive | Esc up | Space play/pause | Left/Right seek</text>
</box>
)
}

View File

@@ -0,0 +1,130 @@
import { createSignal } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { useAppStore } from "../stores/app"
import type { ThemeName } from "../types/settings"
type FocusField = "theme" | "font" | "speed" | "explicit" | "auto"
const THEME_LABELS: Array<{ value: ThemeName; label: string }> = [
{ value: "system", label: "System" },
{ value: "catppuccin", label: "Catppuccin" },
{ value: "gruvbox", label: "Gruvbox" },
{ value: "tokyo", label: "Tokyo" },
{ value: "nord", label: "Nord" },
{ value: "custom", label: "Custom" },
]
export function PreferencesPanel() {
const appStore = useAppStore()
const [focusField, setFocusField] = createSignal<FocusField>("theme")
const settings = () => appStore.state().settings
const preferences = () => appStore.state().preferences
const handleKey = (key: { name: string; shift?: boolean }) => {
if (key.name === "tab") {
const fields: FocusField[] = ["theme", "font", "speed", "explicit", "auto"]
const idx = fields.indexOf(focusField())
const next = key.shift
? (idx - 1 + fields.length) % fields.length
: (idx + 1) % fields.length
setFocusField(fields[next])
return
}
if (key.name === "left" || key.name === "h") {
stepValue(-1)
}
if (key.name === "right" || key.name === "l") {
stepValue(1)
}
if (key.name === "space" || key.name === "enter") {
toggleValue()
}
}
const stepValue = (delta: number) => {
const field = focusField()
if (field === "theme") {
const idx = THEME_LABELS.findIndex((t) => t.value === settings().theme)
const next = (idx + delta + THEME_LABELS.length) % THEME_LABELS.length
appStore.setTheme(THEME_LABELS[next].value)
return
}
if (field === "font") {
const next = Math.min(20, Math.max(10, settings().fontSize + delta))
appStore.updateSettings({ fontSize: next })
return
}
if (field === "speed") {
const next = Math.min(2, Math.max(0.5, settings().playbackSpeed + delta * 0.1))
appStore.updateSettings({ playbackSpeed: Number(next.toFixed(1)) })
}
}
const toggleValue = () => {
const field = focusField()
if (field === "explicit") {
appStore.updatePreferences({ showExplicit: !preferences().showExplicit })
}
if (field === "auto") {
appStore.updatePreferences({ autoDownload: !preferences().autoDownload })
}
}
useKeyboard(handleKey)
return (
<box flexDirection="column" gap={1}>
<text fg="gray">Preferences</text>
<box flexDirection="column" gap={1}>
<box flexDirection="row" gap={1} alignItems="center">
<text fg={focusField() === "theme" ? "cyan" : "gray"}>Theme:</text>
<box border padding={0}>
<text fg="white">{THEME_LABELS.find((t) => t.value === settings().theme)?.label}</text>
</box>
<text fg="gray">[Left/Right]</text>
</box>
<box flexDirection="row" gap={1} alignItems="center">
<text fg={focusField() === "font" ? "cyan" : "gray"}>Font Size:</text>
<box border padding={0}>
<text fg="white">{settings().fontSize}px</text>
</box>
<text fg="gray">[Left/Right]</text>
</box>
<box flexDirection="row" gap={1} alignItems="center">
<text fg={focusField() === "speed" ? "cyan" : "gray"}>Playback:</text>
<box border padding={0}>
<text fg="white">{settings().playbackSpeed}x</text>
</box>
<text fg="gray">[Left/Right]</text>
</box>
<box flexDirection="row" gap={1} alignItems="center">
<text fg={focusField() === "explicit" ? "cyan" : "gray"}>Show Explicit:</text>
<box border padding={0}>
<text fg={preferences().showExplicit ? "green" : "gray"}>
{preferences().showExplicit ? "On" : "Off"}
</text>
</box>
<text fg="gray">[Space]</text>
</box>
<box flexDirection="row" gap={1} alignItems="center">
<text fg={focusField() === "auto" ? "cyan" : "gray"}>Auto Download:</text>
<box border padding={0}>
<text fg={preferences().autoDownload ? "green" : "gray"}>
{preferences().autoDownload ? "On" : "Off"}
</text>
</box>
<text fg="gray">[Space]</text>
</box>
</box>
<text fg="gray">Tab to move focus, Left/Right to adjust</text>
</box>
)
}

View File

@@ -13,6 +13,7 @@ type SearchPageProps = {
focused: boolean
onSubscribe?: (result: SearchResult) => void
onInputFocusChange?: (focused: boolean) => void
onExit?: () => void
}
type FocusArea = "input" | "results" | "history"
@@ -34,6 +35,9 @@ export function SearchPage(props: SearchPageProps) {
props.onInputFocusChange?.(false)
}
}
if (props.focused && focusArea() === "input") {
props.onInputFocusChange?.(true)
}
}
const handleHistorySelect = async (query: string) => {
@@ -144,10 +148,14 @@ export function SearchPage(props: SearchPageProps) {
}
}
// Escape goes back to input
// Escape goes back to input or up one level
if (key.name === "escape") {
setFocusArea("input")
props.onInputFocusChange?.(true)
if (area === "input") {
props.onExit?.()
} else {
setFocusArea("input")
props.onInputFocusChange?.(true)
}
return
}
@@ -261,7 +269,7 @@ export function SearchPage(props: SearchPageProps) {
<text fg="gray">[Tab] Switch focus</text>
<text fg="gray">[/] Focus search</text>
<text fg="gray">[Enter] Select</text>
<text fg="gray">[Esc] Back to search</text>
<text fg="gray">[Esc] Up</text>
</box>
</box>
)

View File

@@ -0,0 +1,94 @@
import { createSignal } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { SourceManager } from "./SourceManager"
import { PreferencesPanel } from "./PreferencesPanel"
import { SyncPanel } from "./SyncPanel"
type SettingsScreenProps = {
accountLabel: string
accountStatus: "signed-in" | "signed-out"
onOpenAccount?: () => void
onExit?: () => void
}
type SectionId = "sync" | "sources" | "preferences" | "account"
const SECTIONS: Array<{ id: SectionId; label: string }> = [
{ id: "sync", label: "Sync" },
{ id: "sources", label: "Sources" },
{ id: "preferences", label: "Preferences" },
{ id: "account", label: "Account" },
]
export function SettingsScreen(props: SettingsScreenProps) {
const [activeSection, setActiveSection] = createSignal<SectionId>("sync")
useKeyboard((key) => {
if (key.name === "escape") {
props.onExit?.()
return
}
if (key.name === "tab") {
const idx = SECTIONS.findIndex((s) => s.id === activeSection())
const next = key.shift
? (idx - 1 + SECTIONS.length) % SECTIONS.length
: (idx + 1) % SECTIONS.length
setActiveSection(SECTIONS[next].id)
return
}
if (key.name === "1") setActiveSection("sync")
if (key.name === "2") setActiveSection("sources")
if (key.name === "3") setActiveSection("preferences")
if (key.name === "4") setActiveSection("account")
})
return (
<box flexDirection="column" gap={1} height="100%">
<box flexDirection="row" justifyContent="space-between" alignItems="center">
<text>
<strong>Settings</strong>
</text>
<text fg="gray">[Tab] Switch section | 1-4 jump | Esc up</text>
</box>
<box flexDirection="row" gap={1}>
{SECTIONS.map((section, index) => (
<box
border
padding={0}
backgroundColor={activeSection() === section.id ? "#2b303b" : undefined}
onMouseDown={() => setActiveSection(section.id)}
>
<text fg={activeSection() === section.id ? "cyan" : "gray"}>
[{index + 1}] {section.label}
</text>
</box>
))}
</box>
<box border flexGrow={1} padding={1} flexDirection="column" gap={1}>
{activeSection() === "sync" && <SyncPanel />}
{activeSection() === "sources" && <SourceManager focused />}
{activeSection() === "preferences" && <PreferencesPanel />}
{activeSection() === "account" && (
<box flexDirection="column" gap={1}>
<text fg="gray">Account</text>
<box flexDirection="row" gap={2} alignItems="center">
<text fg="gray">Status:</text>
<text fg={props.accountStatus === "signed-in" ? "green" : "yellow"}>
{props.accountLabel}
</text>
</box>
<box border padding={0} onMouseDown={() => props.onOpenAccount?.()}>
<text fg="cyan">[A] Manage Account</text>
</box>
</box>
)}
</box>
<text fg="gray">Enter to dive | Esc up</text>
</box>
)
}

View File

@@ -0,0 +1,52 @@
type WaveformProps = {
data: number[]
position: number
duration: number
isPlaying: boolean
onSeek?: (next: number) => void
}
const bars = [".", "-", "~", "=", "#"]
export function Waveform(props: WaveformProps) {
const playedRatio = () => (props.duration === 0 ? 0 : props.position / props.duration)
const renderLine = () => {
const playedCount = Math.floor(props.data.length * playedRatio())
const playedColor = props.isPlaying ? "#6fa8ff" : "#7d8590"
const futureColor = "#3b4252"
const played = props.data
.map((value, index) =>
index <= playedCount
? bars[Math.min(bars.length - 1, Math.floor(value * bars.length))]
: ""
)
.join("")
const upcoming = props.data
.map((value, index) =>
index > playedCount
? bars[Math.min(bars.length - 1, Math.floor(value * bars.length))]
: ""
)
.join("")
return (
<box flexDirection="row" gap={0}>
<text fg={playedColor}>{played || " "}</text>
<text fg={futureColor}>{upcoming || " "}</text>
</box>
)
}
const handleClick = (event: { x: number }) => {
const ratio = props.data.length === 0 ? 0 : event.x / props.data.length
const next = Math.max(0, Math.min(props.duration, Math.round(props.duration * ratio)))
props.onSeek?.(next)
}
return (
<box border padding={1} onMouseDown={handleClick}>
{renderLine()}
</box>
)
}

67
src/constants/themes.ts Normal file
View File

@@ -0,0 +1,67 @@
import type { ThemeColors, ThemeName } from "../types/settings"
export const DEFAULT_THEME: ThemeColors = {
background: "transparent",
surface: "#1b1f27",
primary: "#6fa8ff",
secondary: "#a9b1d6",
accent: "#f6c177",
text: "#e6edf3",
muted: "#7d8590",
warning: "#f0b429",
error: "#f47067",
success: "#3fb950",
}
export const THEMES: Record<ThemeName, ThemeColors> = {
system: DEFAULT_THEME,
catppuccin: {
background: "transparent",
surface: "#1e1e2e",
primary: "#89b4fa",
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,
}

View File

@@ -13,6 +13,7 @@ type ShortcutOptions = {
onTabChange: (tab: TabId) => void
onAction?: (action: string) => void
inputFocused?: boolean
navigationEnabled?: boolean
}
export function useAppKeyboard(options: ShortcutOptions) {
@@ -35,11 +36,25 @@ export function useAppKeyboard(options: ShortcutOptions) {
return
}
if (key.name === "escape") {
options.onAction?.("escape")
return
}
// Skip global shortcuts if input is focused (let input handle keys)
if (options.inputFocused) {
return
}
if (options.navigationEnabled === false) {
return
}
if (key.name === "enter") {
options.onAction?.("enter")
return
}
// Tab navigation with left/right arrows OR [ and ]
if (key.name === "right" || key.name === "]") {
options.onTabChange(getNextTab(options.activeTab))
@@ -89,8 +104,6 @@ export function useAppKeyboard(options: ShortcutOptions) {
options.onAction("save")
} else if (key.ctrl && key.name === "f") {
options.onAction("find")
} else if (key.name === "escape") {
options.onAction("escape")
} else if (key.name === "?" || (key.shift && key.name === "/")) {
options.onAction("help")
}

View File

@@ -0,0 +1,34 @@
import { createSignal, onCleanup } from "solid-js"
type CacheOptions<T> = {
fetcher: () => Promise<T>
intervalMs?: number
}
export const useCachedData = <T,>(options: CacheOptions<T>) => {
const [data, setData] = createSignal<T | null>(null)
const [loading, setLoading] = createSignal(false)
const [error, setError] = createSignal<string | null>(null)
const refresh = async () => {
setLoading(true)
setError(null)
try {
const value = await options.fetcher()
setData(() => value)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load data")
} finally {
setLoading(false)
}
}
refresh()
if (options.intervalMs) {
const interval = setInterval(refresh, options.intervalMs)
onCleanup(() => clearInterval(interval))
}
return { data, loading, error, refresh }
}

109
src/stores/app.ts Normal file
View File

@@ -0,0 +1,109 @@
import { createSignal } from "solid-js"
import { DEFAULT_THEME, THEMES } from "../constants/themes"
import type { AppSettings, AppState, ThemeColors, ThemeName, UserPreferences } from "../types/settings"
const STORAGE_KEY = "podtui_app_state"
const defaultSettings: AppSettings = {
theme: "system",
fontSize: 14,
playbackSpeed: 1,
downloadPath: "",
}
const defaultPreferences: UserPreferences = {
showExplicit: false,
autoDownload: false,
}
const defaultState: AppState = {
settings: defaultSettings,
preferences: defaultPreferences,
customTheme: DEFAULT_THEME,
}
const loadState = (): AppState => {
if (typeof localStorage === "undefined") return defaultState
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return defaultState
const parsed = JSON.parse(raw) as Partial<AppState>
return {
settings: { ...defaultSettings, ...parsed.settings },
preferences: { ...defaultPreferences, ...parsed.preferences },
customTheme: { ...DEFAULT_THEME, ...parsed.customTheme },
}
} catch {
return defaultState
}
}
const saveState = (state: AppState) => {
if (typeof localStorage === "undefined") return
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
} catch {
// ignore storage errors
}
}
export function createAppStore() {
const [state, setState] = createSignal<AppState>(loadState())
const updateState = (next: AppState) => {
setState(next)
saveState(next)
}
const updateSettings = (updates: Partial<AppSettings>) => {
const next = {
...state(),
settings: { ...state().settings, ...updates },
}
updateState(next)
}
const updatePreferences = (updates: Partial<UserPreferences>) => {
const next = {
...state(),
preferences: { ...state().preferences, ...updates },
}
updateState(next)
}
const updateCustomTheme = (updates: Partial<ThemeColors>) => {
const next = {
...state(),
customTheme: { ...state().customTheme, ...updates },
}
updateState(next)
}
const setTheme = (theme: ThemeName) => {
updateSettings({ theme })
}
const resolveTheme = (): ThemeColors => {
const theme = state().settings.theme
if (theme === "custom") return state().customTheme
return THEMES[theme] ?? DEFAULT_THEME
}
return {
state,
updateSettings,
updatePreferences,
updateCustomTheme,
setTheme,
resolveTheme,
}
}
let appStoreInstance: ReturnType<typeof createAppStore> | null = null
export function useAppStore() {
if (!appStoreInstance) {
appStoreInstance = createAppStore()
}
return appStoreInstance
}

32
src/types/settings.ts Normal file
View File

@@ -0,0 +1,32 @@
export type ThemeName = "system" | "catppuccin" | "gruvbox" | "tokyo" | "nord" | "custom"
export type ThemeColors = {
background: string
surface: string
primary: string
secondary: string
accent: string
text: string
muted: string
warning: string
error: string
success: string
}
export type AppSettings = {
theme: ThemeName
fontSize: number
playbackSpeed: number
downloadPath: string
}
export type UserPreferences = {
showExplicit: boolean
autoDownload: boolean
}
export type AppState = {
settings: AppSettings
preferences: UserPreferences
customTheme: ThemeColors
}

57
src/utils/cache.ts Normal file
View File

@@ -0,0 +1,57 @@
type CacheEntry<T> = {
value: T
timestamp: number
}
const CACHE_KEY = "podtui_cache"
const DEFAULT_TTL = 1000 * 60 * 60
const loadCache = (): Record<string, CacheEntry<unknown>> => {
if (typeof localStorage === "undefined") return {}
try {
const raw = localStorage.getItem(CACHE_KEY)
return raw ? (JSON.parse(raw) as Record<string, CacheEntry<unknown>>) : {}
} catch {
return {}
}
}
const saveCache = (cache: Record<string, CacheEntry<unknown>>) => {
if (typeof localStorage === "undefined") return
try {
localStorage.setItem(CACHE_KEY, JSON.stringify(cache))
} catch {
// ignore
}
}
const cache = loadCache()
export const cacheValue = <T,>(key: string, value: T) => {
cache[key] = { value, timestamp: Date.now() }
saveCache(cache)
}
export const getCachedValue = <T,>(key: string, ttl = DEFAULT_TTL): T | null => {
const entry = cache[key] as CacheEntry<T> | undefined
if (!entry) return null
if (Date.now() - entry.timestamp > ttl) {
delete cache[key]
saveCache(cache)
return null
}
return entry.value
}
export const invalidateCache = (prefix?: string) => {
if (!prefix) {
Object.keys(cache).forEach((key) => delete cache[key])
saveCache(cache)
return
}
Object.keys(cache)
.filter((key) => key.startsWith(prefix))
.forEach((key) => delete cache[key])
saveCache(cache)
}

57
src/utils/data-fetcher.ts Normal file
View File

@@ -0,0 +1,57 @@
import { FeedVisibility } from "../types/feed"
import type { Feed } from "../types/feed"
import type { Episode } from "../types/episode"
import type { Podcast } from "../types/podcast"
import { cacheValue, getCachedValue } from "./cache"
import { fetchEpisodes } from "@/api/client"
const feedKey = (feedUrl: string) => `feed:${feedUrl}`
const episodesKey = (feedUrl: string) => `episodes:${feedUrl}`
const searchKey = (query: string) => `search:${query.toLowerCase()}`
export const fetchFeedWithCache = async (feedUrl: string): Promise<Feed | null> => {
const cached = getCachedValue<Feed>(feedKey(feedUrl))
if (cached) return cached
try {
const episodes = await fetchEpisodes(feedUrl)
const feed: Feed = {
id: feedUrl,
podcast: {
id: feedUrl,
title: feedUrl,
description: "",
feedUrl,
lastUpdated: new Date(),
isSubscribed: true,
},
episodes,
visibility: FeedVisibility.PUBLIC,
sourceId: "rss",
lastUpdated: new Date(),
isPinned: false,
}
cacheValue(feedKey(feedUrl), feed)
return feed
} catch {
return null
}
}
export const fetchEpisodesWithCache = async (feedUrl: string): Promise<Episode[]> => {
const cached = getCachedValue<Episode[]>(episodesKey(feedUrl))
if (cached) return cached
const episodes = await fetchEpisodes(feedUrl)
cacheValue(episodesKey(feedUrl), episodes)
return episodes
}
export const searchWithCache = async (
query: string,
fetcher: () => Promise<Podcast[]>
): Promise<Podcast[]> => {
const cached = getCachedValue<Podcast[]>(searchKey(query))
if (cached) return cached
const results = await fetcher()
cacheValue(searchKey(query), results)
return results
}

77
src/utils/persistence.ts Normal file
View File

@@ -0,0 +1,77 @@
import type { AppSettings, UserPreferences } from "../types/settings"
import type { Feed } from "../types/feed"
const STORAGE_KEYS = {
settings: "podtui_settings",
preferences: "podtui_preferences",
feeds: "podtui_feeds",
}
export const savePreference = (key: keyof UserPreferences, value: boolean) => {
const current = loadPreferences()
const next = { ...current, [key]: value }
savePreferences(next)
}
export const loadPreference = (key: keyof UserPreferences) => {
return loadPreferences()[key]
}
export const saveSettings = (settings: AppSettings) => {
if (typeof localStorage === "undefined") return
try {
localStorage.setItem(STORAGE_KEYS.settings, JSON.stringify(settings))
} catch {
// ignore
}
}
export const loadSettings = (): AppSettings | null => {
if (typeof localStorage === "undefined") return null
try {
const raw = localStorage.getItem(STORAGE_KEYS.settings)
return raw ? (JSON.parse(raw) as AppSettings) : null
} catch {
return null
}
}
export const savePreferences = (preferences: UserPreferences) => {
if (typeof localStorage === "undefined") return
try {
localStorage.setItem(STORAGE_KEYS.preferences, JSON.stringify(preferences))
} catch {
// ignore
}
}
export const loadPreferences = (): UserPreferences => {
if (typeof localStorage === "undefined") {
return { showExplicit: false, autoDownload: false }
}
try {
const raw = localStorage.getItem(STORAGE_KEYS.preferences)
return raw ? (JSON.parse(raw) as UserPreferences) : { showExplicit: false, autoDownload: false }
} catch {
return { showExplicit: false, autoDownload: false }
}
}
export const saveFeeds = (feeds: Feed[]) => {
if (typeof localStorage === "undefined") return
try {
localStorage.setItem(STORAGE_KEYS.feeds, JSON.stringify(feeds))
} catch {
// ignore
}
}
export const loadFeeds = (): Feed[] => {
if (typeof localStorage === "undefined") return []
try {
const raw = localStorage.getItem(STORAGE_KEYS.feeds)
return raw ? (JSON.parse(raw) as Feed[]) : []
} catch {
return []
}
}

8
src/utils/waveform.ts Normal file
View File

@@ -0,0 +1,8 @@
export const createWaveform = (width: number): number[] => {
const data: number[] = []
for (let i = 0; i < width; i += 1) {
const value = 0.2 + Math.abs(Math.sin(i / 3)) * 0.8
data.push(Number(value.toFixed(2)))
}
return data
}

View File

@@ -0,0 +1,52 @@
# 50. Create Global State Store (Signals)
meta:
id: podcast-tui-app-50
feature: podcast-tui-app
priority: P1
depends_on: [66]
tags: [state-management, signals, solidjs, global-store]
objective:
- Create a global state store using SolidJS Signals for reactive state management
- Implement a store that manages application-wide state (feeds, settings, user, etc.)
- Provide reactive subscriptions for state changes
- Ensure thread-safe state updates
deliverables:
- `/src/store/index.ts` - Global state store with Signals
- `/src/store/types.ts` - State type definitions
- `/src/store/hooks.ts` - Custom hooks for state access
- Updated `src/index.tsx` to initialize the store
steps:
- Define state interface with all application state properties (feeds, settings, user, etc.)
- Create Signal-based store using `createSignal` from SolidJS
- Implement computed signals for derived state (filtered feeds, search results, etc.)
- Create state update functions that trigger reactivity
- Add subscription mechanism for reactive UI updates
- Export store and hooks for use across components
tests:
- Unit: Test that signals update correctly when state changes
- Unit: Test computed signals produce correct derived values
- Integration: Verify store updates trigger UI re-renders
- Integration: Test multiple components can subscribe to same state
acceptance_criteria:
- Store can be initialized with default state
- State changes trigger reactive updates in components
- Computed signals work correctly for derived state
- Multiple components can access and subscribe to store
- State updates are thread-safe
validation:
- Run `bun run build` to verify TypeScript compilation
- Run application and verify state changes are reactive
- Check console for any errors during state updates
notes:
- Use `createSignal`, `createComputed`, `createEffect` from SolidJS
- Store should follow single source of truth pattern
- Consider using `batch` for multiple state updates
- State should be serializable for persistence

View File

@@ -0,0 +1,56 @@
# 59. Create Theme System Architecture
meta:
id: podcast-tui-app-59
feature: podcast-tui-app
priority: P1
depends_on: [09]
tags: [theming, architecture, solidjs, design-system]
objective:
- Design and implement a flexible theme system architecture
- Support multiple color themes (Catppuccin, Gruvbox, Tokyo, Nord, custom)
- Provide theme switching capabilities
- Ensure theme persistence across sessions
- Define theme properties (colors, fonts, spacing, borders)
deliverables:
- `/src/themes/types.ts` - Theme type definitions
- `/src/themes/theme.ts` - Theme system core implementation
- `/src/themes/themes/` - Directory with theme files
- `/src/themes/default.ts` - Default system theme
- `/src/themes/hooks.ts` - Theme hooks (useTheme, useThemeColor)
steps:
- Define `Theme` interface with properties: name, colors, fonts, spacing, borders
- Create theme color palettes (background, foreground, primary, secondary, accent, etc.)
- Implement `ThemeManager` class to handle theme loading, switching, and persistence
- Create `useTheme` hook for React/Solid components to access current theme
- Implement `useThemeColor` hook for accessing specific theme colors
- Add theme persistence using localStorage or file-based storage
- Export theme utilities for programmatic theme access
tests:
- Unit: Test theme loading from JSON file
- Unit: Test theme switching updates current theme
- Unit: Test theme persistence saves/loads correctly
- Integration: Verify components update when theme changes
acceptance_criteria:
- Theme system can load multiple theme definitions
- Theme switching works instantly across all components
- Theme preferences persist across application restarts
- Theme colors are accessible via hooks
- Theme manager handles errors gracefully
validation:
- Run `bun run build` to verify TypeScript compilation
- Test theme switching manually in application
- Verify localStorage/file storage works
- Check all theme colors render correctly
notes:
- Theme files should be JSON or TypeScript modules
- Use CSS variables or terminal color codes
- Consider dark/light mode compatibility
- Themes should be easily extensible

View File

@@ -0,0 +1,52 @@
# 60. Implement Default Theme (System Terminal)
meta:
id: podcast-tui-app-60
feature: podcast-tui-app
priority: P1
depends_on: [59]
tags: [theming, default, solidjs, terminal]
objective:
- Implement the default system terminal theme
- Define colors matching common terminal environments
- Ensure good readability and contrast
- Support both light and dark terminal modes
deliverables:
- `/src/themes/themes/default.ts` - Default theme definition
- `/src/themes/themes/default-light.ts` - Light mode theme
- `/src/themes/themes/default-dark.ts` - Dark mode theme
- Updated `/src/themes/theme.ts` to load default theme
steps:
- Define default color palette for system terminals
- Create light mode theme with standard terminal colors
- Create dark mode theme with standard terminal colors
- Ensure proper contrast ratios for readability
- Test theme in both light and dark terminal environments
- Export default theme as fallback
tests:
- Unit: Verify default theme colors are defined
- Unit: Test theme renders correctly in light mode
- Unit: Test theme renders correctly in dark mode
- Visual: Verify text contrast meets accessibility standards
acceptance_criteria:
- Default theme works in light terminal mode
- Default theme works in dark terminal mode
- Colors have good readability and contrast
- Theme is used as fallback when no theme selected
validation:
- Run `bun run build` to verify TypeScript compilation
- Test in light terminal (e.g., iTerm2, Terminal.app)
- Test in dark terminal (e.g., Kitty, Alacritty)
- Check color contrast visually
notes:
- Use standard terminal color codes (ANSI escape codes)
- Consider common terminal themes (Solarized, Dracula, etc.)
- Test on multiple terminal emulators
- Document terminal requirements

View File

@@ -0,0 +1,52 @@
# 61. Add Catppuccin Theme
meta:
id: podcast-tui-app-61
feature: podcast-tui-app
priority: P1
depends_on: [59]
tags: [theming, catppuccin, solidjs, popular]
objective:
- Implement Catppuccin Mocha theme for the podcast TUI
- Provide beautiful, modern color scheme
- Ensure high contrast and readability
- Support both dark and light variants
deliverables:
- `/src/themes/themes/catppuccin.ts` - Catppuccin theme definition
- `/src/themes/themes/catppuccin-mocha.ts` - Dark mode Catppuccin
- `/src/themes/themes/catppuccin-latte.ts` - Light mode Catppuccin
- Updated `/src/themes/themes/index.ts` to export Catppuccin themes
steps:
- Research and implement Catppuccin Mocha color palette
- Define all color tokens (background, foreground, surface, primary, secondary, etc.)
- Create Catppuccin Mocha (dark) theme
- Create Catppuccin Latte (light) theme
- Ensure proper color contrast for accessibility
- Add Catppuccin to theme registry
tests:
- Unit: Verify Catppuccin theme colors are defined
- Unit: Test Catppuccin Mocha renders correctly
- Unit: Test Catppuccin Latte renders correctly
- Visual: Verify Catppuccin colors are visually appealing
acceptance_criteria:
- Catppuccin Mocha theme works in dark terminals
- Catppuccin Latte theme works in light terminals
- Colors match official Catppuccin design
- Theme is selectable from theme list
validation:
- Run `bun run build` to verify TypeScript compilation
- Test Catppuccin theme manually in application
- Compare with official Catppuccin color palette
- Check color harmony and contrast
notes:
- Catppuccin Mocha is the recommended dark theme
- Include all standard Catppuccin colors (latte, frappe, macchiato, mocha)
- Use official color values from Catppuccin repository
- Consider adding custom color variations

View File

@@ -0,0 +1,52 @@
# 62. Add Gruvbox Theme
meta:
id: podcast-tui-app-62
feature: podcast-tui-app
priority: P1
depends_on: [59]
tags: [theming, gruvbox, solidjs, retro]
objective:
- Implement Gruvbox Dark theme for the podcast TUI
- Provide warm, nostalgic color scheme
- Ensure good contrast and readability
- Support both dark and light variants
deliverables:
- `/src/themes/themes/gruvbox.ts` - Gruvbox theme definition
- `/src/themes/themes/gruvbox-dark.ts` - Dark mode Gruvbox
- `/src/themes/themes/gruvbox-light.ts` - Light mode Gruvbox
- Updated `/src/themes/themes/index.ts` to export Gruvbox themes
steps:
- Research and implement Gruvbox Dark theme color palette
- Define all color tokens (background, foreground, primary, secondary, etc.)
- Create Gruvbox Dark theme
- Create Gruvbox Light theme
- Ensure proper color contrast for accessibility
- Add Gruvbox to theme registry
tests:
- Unit: Verify Gruvbox theme colors are defined
- Unit: Test Gruvbox Dark renders correctly
- Unit: Test Gruvbox Light renders correctly
- Visual: Verify Gruvbox colors are visually appealing
acceptance_criteria:
- Gruvbox Dark theme works in dark terminals
- Gruvbox Light theme works in light terminals
- Colors match official Gruvbox design
- Theme is selectable from theme list
validation:
- Run `bun run build` to verify TypeScript compilation
- Test Gruvbox theme manually in application
- Compare with official Gruvbox color palette
- Check color harmony and contrast
notes:
- Gruvbox Dark is the recommended variant
- Include all standard Gruvbox colors (hard, soft, light)
- Use official color values from Gruvbox repository
- Gruvbox is popular among developers for its warmth

View File

@@ -0,0 +1,52 @@
# 63. Add Tokyo Night Theme
meta:
id: podcast-tui-app-63
feature: podcast-tui-app
priority: P1
depends_on: [59]
tags: [theming, tokyo-night, solidjs, modern]
objective:
- Implement Tokyo Night theme for the podcast TUI
- Provide modern, vibrant color scheme
- Ensure good contrast and readability
- Support both dark and light variants
deliverables:
- `/src/themes/themes/tokyo-night.ts` - Tokyo Night theme definition
- `/src/themes/themes/tokyo-night-day.ts` - Light mode Tokyo Night
- `/src/themes/themes/tokyo-night-night.ts` - Dark mode Tokyo Night
- Updated `/src/themes/themes/index.ts` to export Tokyo Night themes
steps:
- Research and implement Tokyo Night theme color palette
- Define all color tokens (background, foreground, primary, secondary, etc.)
- Create Tokyo Night Night (dark) theme
- Create Tokyo Night Day (light) theme
- Ensure proper color contrast for accessibility
- Add Tokyo Night to theme registry
tests:
- Unit: Verify Tokyo Night theme colors are defined
- Unit: Test Tokyo Night Night renders correctly
- Unit: Test Tokyo Night Day renders correctly
- Visual: Verify Tokyo Night colors are visually appealing
acceptance_criteria:
- Tokyo Night Night theme works in dark terminals
- Tokyo Night Day theme works in light terminals
- Colors match official Tokyo Night design
- Theme is selectable from theme list
validation:
- Run `bun run build` to verify TypeScript compilation
- Test Tokyo Night theme manually in application
- Compare with official Tokyo Night color palette
- Check color harmony and contrast
notes:
- Tokyo Night Night is the recommended variant
- Include all standard Tokyo Night colors
- Use official color values from Tokyo Night repository
- Tokyo Night is popular for its modern, clean look

View File

@@ -0,0 +1,52 @@
# 64. Add Nord Theme
meta:
id: podcast-tui-app-64
feature: podcast-tui-app
priority: P1
depends_on: [59]
tags: [theming, nord, solidjs, minimal]
objective:
- Implement Nord theme for the podcast TUI
- Provide clean, minimal color scheme
- Ensure good contrast and readability
- Support both dark and light variants
deliverables:
- `/src/themes/themes/nord.ts` - Nord theme definition
- `/src/themes/themes/nord-dark.ts` - Dark mode Nord
- `/src/themes/themes/nord-light.ts` - Light mode Nord
- Updated `/src/themes/themes/index.ts` to export Nord themes
steps:
- Research and implement Nord theme color palette
- Define all color tokens (background, foreground, primary, secondary, etc.)
- Create Nord Dark theme
- Create Nord Light theme
- Ensure proper color contrast for accessibility
- Add Nord to theme registry
tests:
- Unit: Verify Nord theme colors are defined
- Unit: Test Nord Dark renders correctly
- Unit: Test Nord Light renders correctly
- Visual: Verify Nord colors are visually appealing
acceptance_criteria:
- Nord Dark theme works in dark terminals
- Nord Light theme works in light terminals
- Colors match official Nord design
- Theme is selectable from theme list
validation:
- Run `bun run build` to verify TypeScript compilation
- Test Nord theme manually in application
- Compare with official Nord color palette
- Check color harmony and contrast
notes:
- Nord Dark is the recommended variant
- Include all standard Nord colors
- Use official color values from Nord repository
- Nord is popular for its minimalist, clean aesthetic

View File

@@ -0,0 +1,56 @@
# 65. Implement Custom Theme Editor
meta:
id: podcast-tui-app-65
feature: podcast-tui-app
priority: P2
depends_on: [59]
tags: [theming, editor, custom, solidjs]
objective:
- Build a UI for creating and editing custom themes
- Allow users to modify color palettes
- Provide live preview of theme changes
- Save custom themes to storage
deliverables:
- `/src/components/ThemeEditor.tsx` - Theme editor component
- `/src/components/ThemePreview.tsx` - Live theme preview component
- `/src/components/ColorPicker.tsx` - Custom color picker component
- `/src/store/theme-editor.ts` - Theme editor state management
- Updated settings screen to include theme editor
steps:
- Create ThemeEditor component with color palette editor
- Implement ColorPicker component for selecting theme colors
- Create ThemePreview component showing live theme changes
- Build form controls for editing all theme properties
- Add save functionality for custom themes
- Implement delete functionality for custom themes
- Add validation for theme color values
- Update settings screen to show theme editor
tests:
- Unit: Test theme editor saves custom themes correctly
- Unit: Test color picker updates theme colors
- Unit: Test theme preview updates in real-time
- Integration: Test custom theme can be loaded
acceptance_criteria:
- Theme editor allows editing all theme colors
- Custom theme can be saved and loaded
- Live preview shows theme changes in real-time
- Custom themes persist across sessions
- Invalid color values are rejected
validation:
- Run `bun run build` to verify TypeScript compilation
- Test theme editor manually in application
- Verify custom themes save/load correctly
- Check live preview works smoothly
notes:
- Use existing theme type definitions
- Provide preset color palettes for quick selection
- Consider adding theme templates
- Ensure editor works on small terminal sizes

View File

@@ -0,0 +1,56 @@
# 66. Add Theme Selector in Settings
meta:
id: podcast-tui-app-66
feature: podcast-tui-app
priority: P1
depends_on: [59, 60, 61, 62, 63, 64, 65]
tags: [theming, settings, selector, solidjs]
objective:
- Add theme selection UI in settings screen
- Display all available themes (default, Catppuccin, Gruvbox, Tokyo, Nord, custom)
- Allow users to select and switch themes
- Show currently selected theme
- Persist theme preference
deliverables:
- `/src/components/ThemeSelector.tsx` - Theme selector component
- Updated `/src/components/SettingsScreen.tsx` to include theme selector
- Updated `/src/store/theme.ts` to handle theme preference persistence
- Theme selector in settings navigation
steps:
- Create ThemeSelector component with theme list
- Implement theme selection logic
- Display current theme with visual indicator
- Add theme descriptions or icons
- Integrate theme selector into Settings screen
- Save theme preference to storage
- Handle theme switching with instant updates
- Add keyboard navigation for theme list
tests:
- Unit: Test theme selector displays all themes
- Unit: Test theme selection updates current theme
- Unit: Test theme preference saves to storage
- Integration: Test theme switching works across all components
acceptance_criteria:
- Theme selector shows all available themes
- Users can select and switch themes
- Current theme is clearly indicated
- Theme preference persists across sessions
- Theme changes apply immediately to all components
validation:
- Run `bun run build` to verify TypeScript compilation
- Test theme selector manually in settings
- Verify theme preference saves/loads correctly
- Check theme applies to all UI components
notes:
- Use existing theme manager for theme switching
- Consider adding theme search/filter
- Show theme preview in selector if possible
- Ensure accessibility for keyboard navigation

View File

@@ -0,0 +1,57 @@
# 67. Implement Browser Redirect Flow for OAuth
meta:
id: podcast-tui-app-67
feature: podcast-tui-app
priority: P2
depends_on: [04]
tags: [oauth, authentication, browser, solidjs]
objective:
- Implement browser redirect flow for OAuth authentication
- Handle OAuth callback in terminal application
- Exchange authorization code for access token
- Store authentication tokens securely
deliverables:
- `/src/auth/oauth-redirect.ts` - OAuth redirect handler
- `/src/auth/oauth-callback.ts` - OAuth callback handler
- `/src/auth/token-handler.ts` - Token exchange and storage
- Updated `/src/auth/login-screen.tsx` with OAuth option
- Updated `/src/auth/authentication-state.ts` for OAuth state
steps:
- Implement OAuth authorization URL generation with client ID/secret
- Create OAuth redirect handler to capture callback
- Handle authorization code exchange with token endpoint
- Store access token and refresh token securely
- Implement error handling for OAuth failures
- Add OAuth state parameter for security
- Update authentication state to track OAuth login status
- Add OAuth logout functionality
tests:
- Unit: Test OAuth authorization URL generation
- Unit: Test token exchange with valid/invalid code
- Unit: Test token storage and retrieval
- Integration: Test OAuth flow from start to finish
acceptance_criteria:
- OAuth authorization URL is generated correctly
- OAuth callback is handled without errors
- Access token is stored securely
- OAuth flow works with valid credentials
- OAuth errors are handled gracefully
validation:
- Run `bun run build` to verify TypeScript compilation
- Test OAuth flow manually with test credentials
- Verify token storage in localStorage
- Check error handling for invalid tokens
notes:
- Use standard OAuth 2.0 flow (authorization code grant)
- Document OAuth requirements (client ID, redirect URI)
- Consider using PKCE for enhanced security
- Test with real OAuth provider if possible
- Document limitations and requirements

View File

@@ -0,0 +1,58 @@
# 68. Build QR Code Display for Mobile Verification
meta:
id: podcast-tui-app-68
feature: podcast-tui-app
priority: P2
depends_on: [04]
tags: [authentication, qr-code, mobile, verification, solidjs]
objective:
- Display QR code for mobile device verification
- Generate QR code from verification URL
- Handle manual code entry as fallback
- Show verification status and expiration
deliverables:
- `/src/components/QrCodeDisplay.tsx` - QR code display component
- `/src/utils/qr-code-generator.ts` - QR code generation utility
- `/src/auth/verification-handler.ts` - Verification flow handler
- Updated `/src/auth/login-screen.tsx` with QR code option
- Updated `/src/auth/code-validation.tsx` for manual entry
steps:
- Integrate QR code generation library (e.g., `qrcode` package)
- Create QRCodeDisplay component with generated QR image
- Implement verification URL generation
- Add manual code entry fallback UI
- Show verification expiration timer
- Display verification success/failure status
- Handle QR code scan timeout
- Update authentication state on successful verification
tests:
- Unit: Test QR code generation
- Unit: Test verification URL generation
- Unit: Test verification code validation
- Integration: Test complete verification flow
acceptance_criteria:
- QR code is generated correctly from verification URL
- QR code is displayed in terminal
- Manual code entry works as fallback
- Verification status is shown clearly
- Verification expires after timeout
- Successful verification updates auth state
validation:
- Run `bun run build` to verify TypeScript compilation
- Test QR code display manually
- Test manual code entry fallback
- Verify verification flow works end-to-end
notes:
- Use `qrcode` or similar library for QR generation
- Display QR code in ASCII or image format
- Consider using external scanner app
- Add verification expiration (e.g., 5 minutes)
- Document mobile app requirements for scanning

View File

@@ -0,0 +1,50 @@
# 01. Analyze Current Navigation and Layer System
meta:
id: podtui-navigation-theming-improvements-01
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: []
tags: [analysis, debugging, navigation]
objective:
- Analyze current navigation implementation and layer system
- Identify issues with the existing layerDepth signal
- Document current navigation behavior and identify gaps
- Understand how layers should work per user requirements
deliverables:
- Analysis document with current navigation state
- List of identified issues and gaps
- Recommendations for navigation improvements
steps:
- Read src/App.tsx to understand current layerDepth implementation
- Read src/components/Layout.tsx to understand layout structure
- Read src/hooks/useAppKeyboard.ts to understand keyboard handling
- Read src/components/TabNavigation.tsx and src/components/Navigation.tsx
- Review how layerDepth is used across components
- Identify issues with current navigation UX
- Document requirements: clear layer separation, active layer bg colors, left/right navigation, enter/escape controls
- Create analysis summary
tests:
- Unit: None (analysis task)
- Integration: None (analysis task)
acceptance_criteria:
- Analysis document is created and saved
- All current navigation patterns are documented
- All identified issues and gaps are listed
- Clear recommendations are provided for navigation improvements
validation:
- Review analysis document for completeness
- Verify all relevant files were analyzed
- Check that requirements are clearly documented
notes:
- Focus on understanding the gap between current implementation and user requirements
- Pay special attention to how layerDepth signal is managed
- Note any issues with keyboard event handling
- Consider how to make navigation more intuitive

View File

@@ -0,0 +1,51 @@
# 02. Fix Discover Tab Crash
meta:
id: podtui-navigation-theming-improvements-02
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: [podtui-navigation-theming-improvements-01]
tags: [bug-fix, discover, crash]
objective:
- Identify and fix crash when Discover tab is selected
- Ensure DiscoverPage component loads without errors
- Test all functionality in Discover tab
deliverables:
- Fixed DiscoverPage.tsx component
- Debugged crash identified and resolved
- Test results showing no crashes
steps:
- Read src/components/DiscoverPage.tsx thoroughly
- Check for null/undefined references in DiscoverPage
- Verify useDiscoverStore() is properly initialized
- Check DISCOVER_CATEGORIES constant
- Verify TrendingShows component works correctly
- Check CategoryFilter component for issues
- Add null checks and error boundaries if needed
- Test tab selection in App.tsx
- Verify no console errors
- Test all keyboard shortcuts in Discover tab
tests:
- Unit: Test DiscoverPage component with mocked store
- Integration: Test Discover tab selection and navigation
acceptance_criteria:
- Discover tab can be selected without crashes
- No console errors when Discover tab is active
- All DiscoverPage functionality works (keyboard shortcuts, navigation)
- TrendingShows and CategoryFilter components render correctly
validation:
- Run `bun run start` and select Discover tab
- Check console for errors
- Test all keyboard interactions (j/k, tab, enter, escape, r)
- Verify content renders correctly
notes:
- Common crash causes: null store, undefined categories, missing component imports
- Check for unhandled promises or async operations
- Verify all props are properly passed from App.tsx

View File

@@ -0,0 +1,50 @@
# 03. Fix My Feeds Tab Crash
meta:
id: podtui-navigation-theming-improvements-03
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: [podtui-navigation-theming-improvements-01]
tags: [bug-fix, feeds, crash]
objective:
- Identify and fix crash when My Feeds tab is selected
- Ensure FeedList component loads without errors
- Test all functionality in My Feeds tab
deliverables:
- Fixed FeedList.tsx component
- Debugged crash identified and resolved
- Test results showing no crashes
steps:
- Read src/components/FeedList.tsx thoroughly
- Check for null/undefined references in FeedList
- Verify useFeedStore() is properly initialized
- Check FeedItem component for issues
- Verify filteredFeeds() returns valid array
- Add null checks and error boundaries if needed
- Test tab selection in App.tsx
- Verify no console errors
- Test all keyboard shortcuts in FeedList
tests:
- Unit: Test FeedList component with mocked store
- Integration: Test Feeds tab selection and navigation
acceptance_criteria:
- My Feeds tab can be selected without crashes
- No console errors when My Feeds tab is active
- All FeedList functionality works (keyboard shortcuts, navigation)
- FeedItem components render correctly
validation:
- Run `bun run start` and select My Feeds tab
- Check console for errors
- Test all keyboard interactions (j/k, enter, f, s, esc)
- Verify feed list renders correctly
notes:
- Common crash causes: null store, undefined feeds, missing component imports
- Check for unhandled promises or async operations
- Verify all props are properly passed from App.tsx

View File

@@ -0,0 +1,54 @@
# 04. Fix Settings/Sources Sub-tab Crash
meta:
id: podtui-navigation-theming-improvements-04
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: [podtui-navigation-theming-improvements-01]
tags: [bug-fix, settings, crash]
objective:
- Identify and fix crash when Settings/Sources sub-tab is selected
- Ensure SourceManager component loads without errors
- Test all functionality in Settings/Sources sub-tab
deliverables:
- Fixed SourceManager.tsx component
- Debugged crash identified and resolved
- Test results showing no crashes
steps:
- Read src/components/SourceManager.tsx thoroughly
- Check for null/undefined references in SourceManager
- Verify useFeedStore() is properly initialized
- Check all focus areas (list, add, url, country, explicit, language)
- Verify input component is properly imported and used
- Add null checks and error boundaries if needed
- Test Settings tab selection in App.tsx
- Test Sources sub-tab selection in SettingsScreen.tsx
- Verify no console errors
- Test all keyboard shortcuts and interactions
tests:
- Unit: Test SourceManager component with mocked store
- Integration: Test Settings tab → Sources sub-tab navigation
acceptance_criteria:
- Settings tab can be selected without crashes
- Sources sub-tab can be selected without crashes
- No console errors when Settings/Sources sub-tab is active
- All SourceManager functionality works (keyboard shortcuts, navigation)
- All form inputs and buttons work correctly
validation:
- Run `bun run start` and select Settings tab
- Select Sources sub-tab and verify it loads
- Check console for errors
- Test all keyboard interactions (tab, esc, enter, space, a, d)
- Verify form inputs work correctly
notes:
- Common crash causes: null store, undefined sources, missing component imports
- Check for unhandled promises or async operations
- Verify all props are properly passed from SettingsScreen.tsx
- Ensure useKeyboard hook doesn't conflict with parent keyboard handlers

View File

@@ -0,0 +1,53 @@
# 05. Design Layered Navigation UI System
meta:
id: podtui-navigation-theming-improvements-05
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-02, podtui-navigation-theming-improvements-03, podtui-navigation-theming-improvements-04]
tags: [design, navigation, ui]
objective:
- Design the layered navigation UI based on user requirements
- Create visual design for layer separation and active states
- Define how layers should be displayed and navigated
deliverables:
- Navigation design document
- Layer visualization mockups
- Clear specifications for layer colors and borders
- Implementation plan for layered navigation
steps:
- Review user requirements for navigation (clear layer separation, bg colors, left/right navigation, enter/escape controls)
- Analyze current layerDepth signal implementation
- Design layer separation mechanism (borders, backgrounds, spacing)
- Define active layer visual state (bg color, borders, indicators)
- Design navigation controls (left/right arrows, enter arrow down, escape arrow up)
- Create layer visualization showing how multiple layers should work
- Document layer structure and hierarchy
- Create implementation plan for Navigation component
- Define theme colors for layer backgrounds
tests:
- Unit: None (design task)
- Integration: None (design task)
acceptance_criteria:
- Navigation design document is created
- Layer separation mechanism is clearly specified
- Active layer visual state is defined
- Navigation controls are documented
- Implementation plan is provided
validation:
- Review design document for clarity
- Verify it addresses all user requirements
- Check that design is feasible to implement
notes:
- Layers should be clearly delineated with visual separation
- Active layer should have distinct background color
- Navigation should be intuitive with clear visual feedback
- Consider terminal width limitations
- Design should work with existing theme system

View File

@@ -0,0 +1,54 @@
# 06. Implement Left/Right Layer Navigation Controls
meta:
id: podtui-navigation-theming-improvements-06
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-05]
tags: [implementation, navigation, keyboard]
objective:
- Implement left/right arrow key navigation between layers
- Add keyboard handlers for <left> and <right> keys
- Update navigation state to track current layer index
deliverables:
- Updated Navigation component with left/right navigation
- Keyboard handler implementation in App.tsx
- Updated layer management logic
steps:
- Read references/keyboard/REFERENCE.md for keyboard handling patterns
- Design layer index management (currentLayer, maxLayers)
- 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
- Update layer rendering to show active layer with left/right arrows
- Test navigation between layers
- Ensure keyboard shortcuts don't conflict with page-specific shortcuts
tests:
- Unit: Test keyboard handler with mocked key events
- Integration: Test left/right navigation between layers
acceptance_criteria:
- <left> key navigates to previous layer
- <right> key navigates to next layer
- Current layer is visually indicated
- Navigation hints are shown in Navigation component
- No keyboard conflicts with page-specific shortcuts
- Navigation works correctly at layer boundaries
validation:
- Run `bun run start` and test left/right navigation
- Verify current layer is highlighted
- Check that navigation hints are visible
- Test at layer boundaries (first/last layer)
- Verify no conflicts with page shortcuts
notes:
- Use references/keyboard/REFERENCE.md for proper keyboard handling patterns
- Consider accessibility and screen reader support
- Ensure consistent behavior across all pages
- Test with different terminal sizes

View File

@@ -0,0 +1,57 @@
# 07. Implement Enter/Escape Layer Navigation Controls
meta:
id: podtui-navigation-theming-improvements-07
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-05]
tags: [implementation, navigation, keyboard]
objective:
- Implement <enter> key to go down into a layer
- Implement <escape> key to go up one layer
- Update layer navigation to support entering/exiting layers
deliverables:
- Updated layer navigation logic for enter/escape
- Updated Navigation component to show enter/escape hints
- Updated App.tsx keyboard handlers
steps:
- Update Navigation component to show enter/escape navigation hints
- Add <enter> key handler in App.tsx useAppKeyboard hook
- 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
- Update all page components to handle layerDepth prop
- Test enter to go down, escape to go up
- Ensure proper layer nesting behavior
tests:
- Unit: Test keyboard handler with mocked key events
- Integration: Test enter/escape navigation between layers
acceptance_criteria:
- <enter> key goes down into a layer
- <escape> key goes up one layer
- Navigation hints show enter/escape directions
- Layer depth is properly tracked and managed
- Visual feedback shows current layer depth
- No keyboard conflicts with page-specific shortcuts
- Proper layer nesting behavior
validation:
- Run `bun run start` and test enter/escape navigation
- Verify layer depth is visually indicated
- Check that navigation hints are visible
- Test proper layer nesting behavior
- Verify no conflicts with page shortcuts
notes:
- Use references/keyboard/REFERENCE.md for proper keyboard handling patterns
- Consider terminal width limitations for layer hints
- Ensure consistent behavior across all pages
- Test with different layer depths
- Verify escape works at all layer depths

View File

@@ -0,0 +1,53 @@
# 08. Design Active Layer Background Color System
meta:
id: podtui-navigation-theming-improvements-08
feature: podtui-navigation-theming-improvements
priority: P3
depends_on: [podtui-navigation-theming-improvements-05]
tags: [design, theming, navigation]
objective:
- Design active layer background colors
- Define color palette for layer backgrounds
- Create theme-aware layer styling
deliverables:
- Layer color design document
- Theme tokens for layer backgrounds
- Implementation plan for layer styling
steps:
- Review existing theme system in src/constants/themes.ts
- Design layer background colors (active layer, inactive layers)
- Define color palette that works with existing themes
- Create theme tokens for layer backgrounds (e.g., layer-active-bg, layer-inactive-bg)
- Design border colors for layer separation
- Design indicator colors for current layer position
- Create implementation plan for applying layer colors
- Ensure colors work with system/light/dark modes
tests:
- Unit: None (design task)
- Integration: None (design task)
acceptance_criteria:
- Layer color design document is created
- Active layer background color is defined
- Inactive layer background color is defined
- Theme tokens are designed for layer backgrounds
- Colors work with existing theme system
- Implementation plan is provided
validation:
- Review design document for clarity
- Verify colors work with existing themes
- Check that colors are accessible and readable
- Ensure colors work in both light and dark modes
notes:
- Use existing theme tokens where possible
- Ensure contrast ratios meet accessibility standards
- Colors should be subtle but clearly visible
- Consider terminal color limitations
- Design should be consistent with existing UI elements

View File

@@ -0,0 +1,57 @@
# 09. Create Theme Context Provider
meta:
id: podtui-navigation-theming-improvements-09
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: []
tags: [theming, implementation, solid-js]
objective:
- Create theme context provider based on opencode implementation
- Implement theme state management
- Provide theme tokens to all components
deliverables:
- Theme context provider component
- Theme state management hooks
- Theme provider integration
steps:
- Read opencode/packages/ui/src/theme/context.tsx for reference
- Create theme context using SolidJS createContext
- Design theme state structure (themeId, colorScheme, mode, etc.)
- Implement theme state management with signals
- Add theme selection and color scheme management functions
- Create ThemeProvider component
- Add theme persistence to localStorage
- Implement system theme detection
- Add theme change listeners
- Test theme context provider
tests:
- Unit: Test theme context provider with mocked state
- Integration: Test theme provider integration
acceptance_criteria:
- Theme context provider is created
- Theme state management works correctly
- Theme selection and color scheme management functions work
- Theme persistence to localStorage works
- System theme detection works
- Theme change listeners work
- Theme provider can be used to wrap App component
validation:
- Run `bun run start` and verify theme context provider works
- Test theme selection functionality
- Test color scheme switching
- Verify localStorage persistence
- Check system theme detection
notes:
- Use references/solid/REFERENCE.md for SolidJS patterns
- Follow opencode theming implementation patterns
- Use createSignal for reactive theme state
- Ensure proper cleanup in onCleanup
- Test with different theme configurations

View File

@@ -0,0 +1,57 @@
# 10. Implement DesktopTheme Type and Structure
meta:
id: podtui-navigation-theming-improvements-10
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-09]
tags: [theming, implementation, types]
objective:
- Implement DesktopTheme type and structure based on opencode
- Define theme data structure for light and dark variants
- Create theme token types
deliverables:
- DesktopTheme type definition
- Theme variant structure
- Token type definitions
- Example theme data
steps:
- Read opencode/packages/ui/src/theme/types.ts for reference
- Create DesktopTheme type interface
- Define ThemeVariant structure with seeds and overrides
- Define ThemeColor type
- Define ColorScheme type
- Define ResolvedTheme type
- Define ColorValue type
- Create theme constants file
- Add example theme data (system, catppuccin, gruvbox, tokyo, nord)
- Test type definitions
tests:
- Unit: Test type definitions with TypeScript compiler
- Integration: None (type definition task)
acceptance_criteria:
- DesktopTheme type is defined
- ThemeVariant structure is defined
- ThemeColor type is defined
- ColorScheme type is defined
- ResolvedTheme type is defined
- ColorValue type is defined
- Example theme data is provided
- All types are exported correctly
validation:
- Run `tsc --noEmit` to verify no TypeScript errors
- Test theme type usage in components
- Verify theme data structure is correct
notes:
- Use references/solid/REFERENCE.md for SolidJS patterns
- Follow opencode theming implementation patterns
- Ensure types are comprehensive and well-documented
- Add JSDoc comments for complex types
- Consider TypeScript strict mode compliance

View File

@@ -0,0 +1,59 @@
# 11. Implement Theme Resolution System
meta:
id: podtui-navigation-theming-improvements-11
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-10]
tags: [theming, implementation, theme-resolution]
objective:
- Implement theme resolution system for light and dark variants
- Create theme token generation logic
- Define color scale generation functions
deliverables:
- Theme resolution functions
- Color scale generation functions
- Theme token generation logic
- Theme CSS variable generation
steps:
- Read opencode/packages/ui/src/theme/resolve.ts for reference
- Implement resolveThemeVariant function
- Implement generateNeutralScale function
- Implement generateScale function
- Implement hexToOklch and oklchToHex functions
- Implement withAlpha function
- Implement themeToCss function
- Create theme resolution constants
- Define color scale configurations
- Implement theme resolution for each theme
- Test theme resolution functions
tests:
- Unit: Test theme resolution with mocked themes
- Integration: Test theme resolution in context provider
acceptance_criteria:
- resolveThemeVariant function works correctly
- generateNeutralScale function works correctly
- generateScale function works correctly
- Color conversion functions work correctly
- themeToCss function generates valid CSS
- Theme resolution works for all themes
- Light and dark variants are resolved correctly
validation:
- Run `bun run start` and verify theme resolution works
- Test theme resolution for all themes
- Verify light/dark variants are correct
- Check CSS variable generation
- Test with different color schemes
notes:
- Use references/solid/REFERENCE.md for SolidJS patterns
- Follow opencode theming implementation patterns
- Ensure color scales are generated correctly
- Test color conversion functions
- Verify theme tokens match expected values

View File

@@ -0,0 +1,57 @@
# 12. Create CSS Variable Token System
meta:
id: podtui-navigation-theming-improvements-12
feature: podtui-navigation-theming-improvements
priority: P3
depends_on: [podtui-navigation-theming-improvements-11]
tags: [theming, implementation, css-variables]
objective:
- Create comprehensive CSS variable token system
- Define all theme tokens for OpenTUI components
- Generate CSS variables from theme tokens
deliverables:
- CSS variable token definitions
- Theme CSS generation functions
- CSS variable application utilities
steps:
- Read opencode/packages/ui/src/theme/resolve.ts for reference
- Review opencode theme token definitions
- Define theme tokens for OpenTUI components:
- Background tokens (background-base, background-weak, etc.)
- Surface tokens (surface-base, surface-raised, etc.)
- Text tokens (text-base, text-weak, etc.)
- Border tokens (border-base, border-hover, etc.)
- Interactive tokens (interactive-base, interactive-hover, etc.)
- Color tokens for specific states (success, warning, error, etc.)
- Layer navigation tokens (layer-active-bg, layer-inactive-bg, etc.)
- Implement themeToCss function to generate CSS variables
- Create utility to apply theme tokens to components
- Test CSS variable generation
tests:
- Unit: Test CSS variable generation
- Integration: Test CSS variable application in components
acceptance_criteria:
- All OpenTUI theme tokens are defined
- CSS variable generation works correctly
- Theme tokens can be applied to components
- Generated CSS is valid
- Tokens cover all component styling needs
validation:
- Run `bun run start` and verify CSS variables are applied
- Test theme token application in components
- Check CSS variable values
- Verify tokens work with existing components
notes:
- Use references/solid/REFERENCE.md for SolidJS patterns
- Follow opencode theming implementation patterns
- Ensure tokens are comprehensive and well-organized
- Test token application in various components
- Consider backward compatibility with existing hardcoded colors

View File

@@ -0,0 +1,55 @@
# 13. Implement System Theme Detection
meta:
id: podtui-navigation-theming-improvements-13
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-12]
tags: [theming, implementation, system-theme]
objective:
- Implement system theme detection (prefers-color-scheme)
- Add theme mode management (light/dark/auto)
- Implement automatic theme switching based on system preferences
deliverables:
- System theme detection implementation
- Theme mode management functions
- Automatic theme switching logic
steps:
- Read opencode/packages/ui/src/theme/context.tsx for reference
- Implement getSystemMode function
- Add theme mode state to theme context
- Implement colorScheme state management
- Add window.matchMedia listener for system theme changes
- Implement automatic mode switching when colorScheme is "system"
- Add theme mode persistence to localStorage
- Test system theme detection
- Test automatic theme switching
tests:
- Unit: Test system theme detection with mocked media queries
- Integration: Test theme mode switching
acceptance_criteria:
- getSystemMode function works correctly
- Theme mode state is managed correctly
- Window.matchMedia listener works correctly
- Automatic theme switching works when colorScheme is "system"
- Theme mode persistence to localStorage works
- System theme changes are detected and applied
validation:
- Run `bun run start` and test system theme detection
- Test switching system between light/dark modes
- Verify automatic theme switching works
- Check localStorage persistence
- Test theme mode selection (system/light/dark)
notes:
- Use references/solid/REFERENCE.md for SolidJS patterns
- Follow opencode theming implementation patterns
- Ensure proper cleanup in onCleanup
- Test with different system theme settings
- Verify theme updates are reactive

View File

@@ -0,0 +1,59 @@
# 14. Integrate Theme Provider into App Component
meta:
id: podtui-navigation-theming-improvements-14
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-09, podtui-navigation-theming-improvements-13]
tags: [integration, theming, app]
objective:
- Integrate theme provider into App component
- Apply theme tokens to all components
- Ensure theme changes are reactive
deliverables:
- Updated App.tsx with theme provider
- Theme application logic
- Theme integration tests
steps:
- Read references/solid/REFERENCE.md for SolidJS patterns
- Wrap App component with ThemeProvider
- Implement theme application in Layout component
- Apply theme tokens to all components:
- TabNavigation (active tab background)
- Navigation (footer navigation)
- DiscoverPage (background, borders, text colors)
- FeedList (background, borders, text colors)
- SettingsScreen (background, borders, text colors)
- SourceManager (background, borders, text colors)
- All other components
- Update theme tokens usage in src/constants/themes.ts
- Test theme application in all components
- Test theme changes are reactive
tests:
- Unit: Test theme provider integration
- Integration: Test theme changes in all components
acceptance_criteria:
- ThemeProvider is integrated into App component
- Theme tokens are applied to all components
- Theme changes are reactive
- All components render correctly with theme tokens
- No console errors related to theming
validation:
- Run `bun run start` and verify theme is applied
- Test theme selection and color scheme switching
- Test system theme detection
- Verify all components use theme tokens
- Check console for errors
notes:
- Use references/solid/REFERENCE.md for SolidJS patterns
- Follow opencode theming implementation patterns
- Ensure theme tokens are used consistently
- Test theme changes in all components
- Verify no hardcoded colors remain

View File

@@ -0,0 +1,60 @@
# 15. Update Components to Use Theme Tokens
meta:
id: podtui-navigation-theming-improvements-15
feature: podtui-navigation-theming-improvements
priority: P3
depends_on: [podtui-navigation-theming-improvements-14]
tags: [theming, implementation, component-updates]
objective:
- Replace all hardcoded colors with theme tokens
- Update all components to use theme context
- Ensure consistent theme application across all components
deliverables:
- Updated components using theme tokens
- Theme token usage guide
- List of components updated
steps:
- Review all components in src/components/
- Identify all hardcoded color values
- Replace hardcoded colors with theme tokens:
- Background colors (background-base, surface-base, etc.)
- Text colors (text-base, text-weak, etc.)
- Border colors (border-base, border-hover, etc.)
- Interactive colors (interactive-base, interactive-hover, etc.)
- Color tokens (success, warning, error, etc.)
- Layer navigation colors (layer-active-bg, layer-inactive-bg, etc.)
- Update src/constants/themes.ts to export theme tokens
- Update all components to use theme context
- Test theme tokens in all components
- Verify no hardcoded colors remain
- Create theme token usage guide
tests:
- Unit: Test theme tokens in individual components
- Integration: Test theme tokens in all components
acceptance_criteria:
- All hardcoded colors are replaced with theme tokens
- All components use theme tokens
- Theme tokens are exported from themes.ts
- Theme token usage guide is created
- No console errors related to theming
- All components render correctly with theme tokens
validation:
- Run `bun run start` and verify all components use theme tokens
- Test theme selection and color scheme switching
- Verify all components render correctly
- Check console for errors
- Verify no hardcoded colors remain
notes:
- Use references/solid/REFERENCE.md for SolidJS patterns
- Follow opencode theming implementation patterns
- Ensure consistent theme application across all components
- Test theme tokens in all components
- Verify backward compatibility if needed

View File

@@ -0,0 +1,65 @@
# 16. Test Navigation Flows and Layer Transitions
meta:
id: podtui-navigation-theming-improvements-16
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-06, podtui-navigation-theming-improvements-07, podtui-navigation-theming-improvements-08]
tags: [testing, navigation, integration]
objective:
- Test all navigation flows and layer transitions
- Verify left/right navigation works correctly
- Verify enter/escape navigation works correctly
- Test layer transitions between different pages
deliverables:
- Navigation test results
- Test cases for navigation flows
- Bug reports for any issues
steps:
- Create test cases for navigation flows:
- Test left/right navigation between layers
- Test enter to go down into layers
- Test escape to go up from layers
- Test layer boundaries (first/last layer)
- Test layer nesting behavior
- Test navigation with different terminal sizes
- Run `bun run start` and perform all test cases
- Document any issues or bugs
- Test navigation in all pages:
- Discover tab
- My Feeds tab
- Search tab
- Player tab
- Settings tab
- Test keyboard shortcut conflicts
- Test visual feedback for navigation
- Test layer color visibility
tests:
- Unit: Test navigation logic with mocked state
- Integration: Test navigation flows in actual application
acceptance_criteria:
- All navigation flows work correctly
- Left/right navigation works between layers
- Enter/escape navigation works correctly
- Layer boundaries are handled correctly
- No keyboard shortcut conflicts
- Visual feedback is clear and accurate
- All pages work correctly with navigation
validation:
- Run `bun run start` and perform all test cases
- Document all test results
- Report any issues found
- Verify all navigation flows work
notes:
- Test with different terminal sizes
- Test with different layer depths
- Test keyboard shortcuts in all pages
- Verify visual feedback is clear
- Test edge cases and error conditions

View File

@@ -0,0 +1,64 @@
# 17. Test Tab Crash Fixes and Edge Cases
meta:
id: podtui-navigation-theming-improvements-17
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: [podtui-navigation-theming-improvements-02, podtui-navigation-theming-improvements-03, podtui-navigation-theming-improvements-04]
tags: [testing, crash-fix, integration]
objective:
- Test all tab crash fixes
- Verify Discover, My Feeds, and Settings tabs load without errors
- Test edge cases and error conditions
deliverables:
- Crash fix test results
- Test cases for tab crashes
- Bug reports for any remaining issues
steps:
- Create test cases for tab crashes:
- Test Discover tab selection
- Test My Feeds tab selection
- Test Settings tab selection
- Test Settings/Sources sub-tab selection
- Test navigation between tabs
- Run `bun run start` and perform all test cases
- Test all keyboard shortcuts in each tab
- Test edge cases:
- Empty feed lists
- Missing data
- Network errors
- Invalid inputs
- Rapid tab switching
- Test error boundaries
- Document any remaining issues
- Verify no console errors
tests:
- Unit: Test components with mocked data
- Integration: Test all tabs and sub-tabs in actual application
acceptance_criteria:
- All tabs load without crashes
- No console errors when selecting tabs
- All keyboard shortcuts work correctly
- Edge cases are handled gracefully
- Error boundaries work correctly
- All test cases pass
validation:
- Run `bun run start` and perform all test cases
- Check console for errors
- Test all keyboard shortcuts
- Test edge cases
- Document all test results
- Report any remaining issues
notes:
- Test with different data states (empty, full, partial)
- Test network error scenarios
- Test rapid user interactions
- Verify error messages are clear
- Test with different terminal sizes

View File

@@ -0,0 +1,72 @@
# 18. Test Theming System with All Modes
meta:
id: podtui-navigation-theming-improvements-18
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-15, podtui-navigation-theming-improvements-16, podtui-navigation-theming-improvements-17]
tags: [testing, theming, integration]
objective:
- Test theming system with all modes (system/light/dark)
- Verify theme tokens work correctly
- Ensure theming is consistent across all components
- Test theme persistence and system theme detection
deliverables:
- Theming system test results
- Theme token test cases
- Theme consistency report
steps:
- Create test cases for theming system:
- Test system theme mode
- Test light theme mode
- Test dark theme mode
- Test theme persistence
- Test system theme detection
- Test theme selection
- Test color scheme switching
- Run `bun run start` and perform all test cases
- Test theme tokens in all components:
- Background colors
- Text colors
- Border colors
- Interactive colors
- Color tokens (success, warning, error, etc.)
- Layer navigation colors
- Test theme changes are reactive
- Test theme tokens work in all terminals
- Verify theme consistency across all components
- Document any issues
tests:
- Unit: Test theme tokens with mocked themes
- Integration: Test theming in all components and modes
acceptance_criteria:
- Theming system works correctly in all modes
- Theme tokens work correctly in all components
- Theme changes are reactive
- Theme persistence works correctly
- System theme detection works correctly
- Theme consistency is maintained across all components
- All test cases pass
validation:
- Run `bun run start` and perform all test cases
- Test all theme modes (system/light/dark)
- Test theme selection and color scheme switching
- Verify theme persistence
- Test system theme detection
- Check console for errors
- Verify theme consistency across all components
- Document all test results
notes:
- Test with different terminal sizes
- Test with different color schemes
- Test rapid theme changes
- Verify theme updates are reactive
- Test all components with all themes
- Ensure accessibility standards are met

View File

@@ -0,0 +1,50 @@
# PodTUI Navigation and Theming Improvements
Objective: Implement layered navigation system, fix tab crashes, and integrate sophisticated theming based on opencode
Status legend: [ ] todo, [~] in-progress, [x] done
Tasks
- [ ] 01 — Analyze current navigation and layer system → `01-analyze-navigation-system.md`
- [ ] 02 — Fix Discover tab crash → `02-fix-discover-tab-crash.md`
- [ ] 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`
- [ ] 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`
- [ ] 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`
- [ ] 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`
- [ ] 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`
Dependencies
- 01 depends on
- 02, 03, 04 depends on 01
- 05 depends on 02, 03, 04
- 06, 07, 08 depends on 05
- 16 depends on 06, 07, 08
- 09 depends on
- 10 depends on 09
- 11 depends on 10
- 12 depends on 11
- 13 depends on 12
- 14 depends on 13
- 15 depends on 14
- 18 depends on 15, 16, 17
Exit criteria
- Navigation is clearly visualized with layered backgrounds and active states
- Left/right keys navigate between layers, enter goes down, escape goes up
- All tabs (Discover, My Feeds, Settings) load without crashes
- Settings/Sources sub-tab loads without crashes
- Theming system works correctly with system/light/dark/auto modes
- All components use theme tokens consistently
- No hardcoded colors remain in components
- All tests pass and crashes are resolved