From e239b3304292a3a66eb3aad016e0f6f697e0a869 Mon Sep 17 00:00:00 2001 From: Mike Freno Date: Thu, 5 Feb 2026 13:46:47 -0500 Subject: [PATCH] getting terminal colors working --- src/App.tsx | 17 +- src/components/Layout.tsx | 55 ++-- src/context/KeybindContext.tsx | 129 ++++++++ src/context/ThemeContext.tsx | 228 ++++++++----- src/context/helper.tsx | 53 +++ src/index.tsx | 20 +- src/types/settings.ts | 2 +- src/ui/command.tsx | 307 ++++++++++++++++++ src/ui/dialog.tsx | 224 +++++++++++++ src/ui/toast.tsx | 153 +++++++++ src/utils/clipboard.ts | 221 +++++++++++++ src/utils/custom-themes.ts | 7 + src/utils/event-bus.ts | 136 ++++++++ src/utils/keybind.ts | 187 +++++++++++ src/utils/theme-observer.ts | 104 ++++++ .../01-analyze-navigation-system.md | 50 --- .../02-fix-discover-tab-crash.md | 51 --- .../03-fix-feeds-tab-crash.md | 50 --- .../04-fix-settings-sources-crash.md | 54 --- .../05-design-layered-navigation-ui.md | 51 --- .../06-implement-layer-navigation-controls.md | 54 --- .../07-implement-enter-escape-controls.md | 55 ---- .../08-design-active-layer-colors.md | 56 ---- .../09-create-theme-context-provider.md | 59 ---- .../10-implement-desktop-theme-types.md | 57 ---- .../11-implement-theme-resolution.md | 59 ---- .../12-create-css-token-system.md | 57 ---- .../13-implement-system-theme-detection.md | 55 ---- .../14-integrate-theme-provider.md | 59 ---- .../15-update-components-to-use-themes.md | 60 ---- .../16-test-navigation-flows.md | 65 ---- .../17-test-tab-crash-fixes.md | 64 ---- .../18-test-theming-system.md | 72 ---- .../README.md | 50 --- .../theme-refactoring-01-create-schema.md | 63 ---- .../theme-refactoring-02-convert-themes.md | 77 ----- .../theme-refactoring-03-update-types.md | 70 ---- .../theme-refactoring-04-theme-resolution.md | 83 ----- .../theme-refactoring-05-theme-context.md | 80 ----- .../theme-refactoring-06-theme-loader.md | 79 ----- .../theme-refactoring-07-system-theme.md | 83 ----- ...heme-refactoring-08-syntax-highlighting.md | 83 ----- .../theme-refactoring-09-theme-utils.md | 77 ----- .../theme-refactoring-10-theme-switching.md | 80 ----- .../theme-refactoring-11-custom-themes.md | 77 ----- 45 files changed, 1718 insertions(+), 2055 deletions(-) create mode 100644 src/context/KeybindContext.tsx create mode 100644 src/context/helper.tsx create mode 100644 src/ui/command.tsx create mode 100644 src/ui/dialog.tsx create mode 100644 src/ui/toast.tsx create mode 100644 src/utils/clipboard.ts create mode 100644 src/utils/event-bus.ts create mode 100644 src/utils/keybind.ts create mode 100644 src/utils/theme-observer.ts delete mode 100644 tasks/podtui-navigation-theming-improvements/01-analyze-navigation-system.md delete mode 100644 tasks/podtui-navigation-theming-improvements/02-fix-discover-tab-crash.md delete mode 100644 tasks/podtui-navigation-theming-improvements/03-fix-feeds-tab-crash.md delete mode 100644 tasks/podtui-navigation-theming-improvements/04-fix-settings-sources-crash.md delete mode 100644 tasks/podtui-navigation-theming-improvements/05-design-layered-navigation-ui.md delete mode 100644 tasks/podtui-navigation-theming-improvements/06-implement-layer-navigation-controls.md delete mode 100644 tasks/podtui-navigation-theming-improvements/07-implement-enter-escape-controls.md delete mode 100644 tasks/podtui-navigation-theming-improvements/08-design-active-layer-colors.md delete mode 100644 tasks/podtui-navigation-theming-improvements/09-create-theme-context-provider.md delete mode 100644 tasks/podtui-navigation-theming-improvements/10-implement-desktop-theme-types.md delete mode 100644 tasks/podtui-navigation-theming-improvements/11-implement-theme-resolution.md delete mode 100644 tasks/podtui-navigation-theming-improvements/12-create-css-token-system.md delete mode 100644 tasks/podtui-navigation-theming-improvements/13-implement-system-theme-detection.md delete mode 100644 tasks/podtui-navigation-theming-improvements/14-integrate-theme-provider.md delete mode 100644 tasks/podtui-navigation-theming-improvements/15-update-components-to-use-themes.md delete mode 100644 tasks/podtui-navigation-theming-improvements/16-test-navigation-flows.md delete mode 100644 tasks/podtui-navigation-theming-improvements/17-test-tab-crash-fixes.md delete mode 100644 tasks/podtui-navigation-theming-improvements/18-test-theming-system.md delete mode 100644 tasks/podtui-navigation-theming-improvements/README.md delete mode 100644 tasks/subtasks/theme-refactoring-01-create-schema.md delete mode 100644 tasks/subtasks/theme-refactoring-02-convert-themes.md delete mode 100644 tasks/subtasks/theme-refactoring-03-update-types.md delete mode 100644 tasks/subtasks/theme-refactoring-04-theme-resolution.md delete mode 100644 tasks/subtasks/theme-refactoring-05-theme-context.md delete mode 100644 tasks/subtasks/theme-refactoring-06-theme-loader.md delete mode 100644 tasks/subtasks/theme-refactoring-07-system-theme.md delete mode 100644 tasks/subtasks/theme-refactoring-08-syntax-highlighting.md delete mode 100644 tasks/subtasks/theme-refactoring-09-theme-utils.md delete mode 100644 tasks/subtasks/theme-refactoring-10-theme-switching.md delete mode 100644 tasks/subtasks/theme-refactoring-11-custom-themes.md diff --git a/src/App.tsx b/src/App.tsx index b70b6fe..788b6da 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { createSignal } from "solid-js"; +import { useRenderer } from "@opentui/solid"; import { Layout } from "./components/Layout"; import { Navigation } from "./components/Navigation"; import { TabNavigation } from "./components/TabNavigation"; @@ -32,31 +33,31 @@ export function App() { // Centralized keyboard handler for all tab navigation and shortcuts useAppKeyboard({ get activeTab() { - return activeTab() + return activeTab(); }, onTabChange: setActiveTab, inputFocused: inputFocused(), navigationEnabled: layerDepth() === 0, layerDepth, onLayerChange: (newDepth) => { - setLayerDepth(newDepth) + setLayerDepth(newDepth); }, onAction: (action) => { if (action === "escape") { if (layerDepth() > 0) { - setLayerDepth(0) - setInputFocused(false) + setLayerDepth(0); + setInputFocused(false); } else { - setShowAuthPanel(false) - setInputFocused(false) + setShowAuthPanel(false); + setInputFocused(false); } } if (action === "enter" && layerDepth() === 0) { - setLayerDepth(1) + setLayerDepth(1); } }, - }) + }); const renderContent = () => { const tab = activeTab(); diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 3265702..d5677c6 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,5 +1,6 @@ import type { JSX } from "solid-js" import type { RGBA } from "@opentui/core" +import { Show, createMemo } from "solid-js" import { useTheme } from "../context/ThemeContext" import { LayerIndicator } from "./LayerIndicator" @@ -16,52 +17,50 @@ type LayoutProps = { } export function Layout(props: LayoutProps) { - const { theme } = useTheme() + const context = useTheme() - // Get layer configuration based on depth - const getLayerConfig = (depth: number): LayerConfig => { - const backgrounds = theme.layerBackgrounds + // Get layer configuration based on depth - wrapped in createMemo for reactivity + const currentLayer = createMemo((): LayerConfig => { + const depth = props.layerDepth || 0 + const backgrounds = context.theme.layerBackgrounds const depthMap: Record = { - 0: { depth: 0, background: backgrounds?.layer0 ?? theme.background }, - 1: { depth: 1, background: backgrounds?.layer1 ?? theme.backgroundPanel }, - 2: { depth: 2, background: backgrounds?.layer2 ?? theme.backgroundElement }, - 3: { depth: 3, background: backgrounds?.layer3 ?? theme.backgroundMenu }, + 0: { depth: 0, background: backgrounds?.layer0 ?? context.theme.background }, + 1: { depth: 1, background: backgrounds?.layer1 ?? context.theme.backgroundPanel }, + 2: { depth: 2, background: backgrounds?.layer2 ?? context.theme.backgroundElement }, + 3: { depth: 3, background: backgrounds?.layer3 ?? context.theme.backgroundMenu }, } - return depthMap[depth] || { depth: 0, background: theme.background } - } - - // Get current layer background - const currentLayer = getLayerConfig(props.layerDepth || 0) + return depthMap[depth] || { depth: 0, background: context.theme.background } + }) + // Note: No need for a ready check here - the ThemeProvider uses + // createSimpleContext which gates children rendering until ready return ( {/* Header */} - {props.header ? ( + }> {props.header} - ) : ( - - )} + {/* Main content area with layer background */} {/* Footer */} - {props.footer ? ( + }> {props.footer} - ) : ( - - )} + {/* Layer indicator */} - {props.layerDepth !== undefined && ( + - + - )} + ) } diff --git a/src/context/KeybindContext.tsx b/src/context/KeybindContext.tsx new file mode 100644 index 0000000..8461674 --- /dev/null +++ b/src/context/KeybindContext.tsx @@ -0,0 +1,129 @@ +import { createMemo } from "solid-js" +import type { ParsedKey, Renderable } from "@opentui/core" +import { createStore } from "solid-js/store" +import { useKeyboard, useRenderer } from "@opentui/solid" +import { createSimpleContext } from "./helper" +import { Keybind, DEFAULT_KEYBINDS, type KeybindsConfig } from "../utils/keybind" + +/** + * Keybind context provider for managing keyboard shortcuts. + * + * Features: + * - Leader key support (like vim's leader key) + * - Configurable keybindings + * - Key parsing and matching + * - Display-friendly key representations + */ +export const { use: useKeybind, provider: KeybindProvider } = createSimpleContext({ + name: "Keybind", + init: (props: { keybinds?: Partial }) => { + // Merge default keybinds with custom keybinds + const customKeybinds = props.keybinds ?? {} + const mergedKeybinds = { ...DEFAULT_KEYBINDS, ...customKeybinds } + + const keybinds = createMemo(() => { + const result: Record = {} + for (const [key, value] of Object.entries(mergedKeybinds)) { + result[key] = Keybind.parse(value) + } + return result + }) + + const [store, setStore] = createStore({ + leader: false, + }) + + const renderer = useRenderer() + + let focus: Renderable | null = null + let timeout: NodeJS.Timeout | undefined + + function leader(active: boolean) { + if (active) { + setStore("leader", true) + focus = renderer.currentFocusedRenderable + focus?.blur() + if (timeout) clearTimeout(timeout) + timeout = setTimeout(() => { + if (!store.leader) return + leader(false) + if (!focus || focus.isDestroyed) return + focus.focus() + }, 2000) // Leader key timeout + return + } + + if (!active) { + if (focus && !renderer.currentFocusedRenderable) { + focus.focus() + } + setStore("leader", false) + } + } + + // Handle leader key + useKeyboard(async (evt) => { + if (!store.leader && result.match("leader", evt)) { + leader(true) + return + } + + if (store.leader && evt.name) { + setImmediate(() => { + if (focus && renderer.currentFocusedRenderable === focus) { + focus.focus() + } + leader(false) + }) + } + }) + + const result = { + get all() { + return keybinds() + }, + get leader() { + return store.leader + }, + /** + * Parse a keyboard event into a Keybind.Info. + */ + parse(evt: ParsedKey): Keybind.Info { + // Handle special case for Ctrl+Underscore (represented as \x1F) + if (evt.name === "\x1F") { + return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader) + } + return Keybind.fromParsedKey(evt, store.leader) + }, + /** + * Check if a keyboard event matches a registered keybind. + */ + match(key: keyof KeybindsConfig, evt: ParsedKey): boolean { + const keybind = keybinds()[key] + if (!keybind) return false + const parsed: Keybind.Info = result.parse(evt) + for (const kb of keybind) { + if (Keybind.match(kb, parsed)) { + return true + } + } + return false + }, + /** + * Get a display string for a registered keybind. + */ + print(key: keyof KeybindsConfig): string { + const first = keybinds()[key]?.at(0) + if (!first) return "" + const display = Keybind.toString(first) + // Replace leader placeholder with actual leader key + const leaderKey = keybinds().leader?.[0] + if (leaderKey) { + return display.replace("", Keybind.toString(leaderKey)) + } + return display + }, + } + return result + }, +}) diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx index 5da095e..1fdac56 100644 --- a/src/context/ThemeContext.tsx +++ b/src/context/ThemeContext.tsx @@ -1,13 +1,14 @@ -import { createContext, createEffect, createMemo, createSignal, Show, useContext } from "solid-js" +import { createEffect, createMemo, onMount, onCleanup } from "solid-js" import { createStore, produce } from "solid-js/store" import { useRenderer } from "@opentui/solid" import type { ThemeName } from "../types/settings" import type { ThemeJson } from "../types/theme-schema" import { useAppStore } from "../stores/app" import { THEME_JSON } from "../constants/themes" -import { resolveTheme } from "../utils/theme-resolver" import { generateSyntax, generateSubtleSyntax } from "../utils/syntax-highlighter" import { resolveTerminalTheme, loadThemes } from "../utils/theme" +import { createSimpleContext } from "./helper" +import { setupThemeSignalHandler, emitThemeChanged, emitThemeModeChanged } from "../utils/theme-observer" import type { RGBA, TerminalColors } from "@opentui/core" type ThemeResolved = { @@ -75,93 +76,154 @@ type ThemeResolved = { thinkingOpacity?: number } -type ThemeContextValue = { - theme: ThemeResolved - selected: () => string - all: () => Record - syntax: () => unknown - subtleSyntax: () => unknown - mode: () => "dark" | "light" - setMode: (mode: "dark" | "light") => void - set: (theme: string) => void - ready: () => boolean -} +/** + * Theme context using the createSimpleContext pattern. + * + * This ensures children are NOT rendered until the theme is ready, + * preventing "useTheme must be used within a ThemeProvider" errors. + * + * The key insight from opencode's implementation is that the provider + * uses `` to gate rendering, so components can + * safely call useTheme() without checking ready state. + */ +export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ + name: "Theme", + init: (props: { mode: "dark" | "light" }) => { + const appStore = useAppStore() + const renderer = useRenderer() + const [store, setStore] = createStore({ + themes: THEME_JSON as Record, + mode: props.mode, + active: appStore.state().settings.theme as string, + system: undefined as undefined | TerminalColors, + ready: false, + }) -const ThemeContext = createContext() + function init() { + resolveSystemTheme() + loadThemes() + .then((custom) => { + setStore( + produce((draft) => { + Object.assign(draft.themes, custom) + }) + ) + }) + .catch(() => { + // If custom themes fail to load, fall back to opencode theme + setStore("active", "opencode") + }) + .finally(() => { + // Only set ready if not waiting for system theme + if (store.active !== "system") { + setStore("ready", true) + } + }) + } -export function ThemeProvider({ children }: { children: any }) { - const appStore = useAppStore() - const renderer = useRenderer() - const [ready, setReady] = createSignal(false) - const [store, setStore] = createStore({ - themes: {} as Record, - mode: "dark" as "dark" | "light", - active: appStore.state().settings.theme as ThemeName, - system: undefined as undefined | TerminalColors, - }) + function resolveSystemTheme() { + renderer + .getPalette({ size: 16 }) + .then((colors) => { + if (!colors.palette[0]) { + // No system colors available, fall back to default + // This happens when the terminal doesn't support OSC palette queries + // (e.g., running inside tmux, or on unsupported terminals) + if (store.active === "system") { + setStore( + produce((draft) => { + draft.active = "opencode" + draft.ready = true + }) + ) + } + return + } + setStore( + produce((draft) => { + draft.system = colors + if (store.active === "system") { + draft.ready = true + } + }) + ) + }) + .catch(() => { + // On error, fall back to default theme if using system + if (store.active === "system") { + setStore( + produce((draft) => { + draft.active = "opencode" + draft.ready = true + }) + ) + } + }) + } - const init = () => { - loadThemes() - .then((custom) => { - setStore( - produce((draft) => { - Object.assign(draft.themes, custom) - }) - ) - }) - .finally(() => setReady(true)) - } + onMount(init) - init() + // Setup SIGUSR2 signal handler for dynamic theme reload + // This allows external tools to trigger a theme refresh by sending: + // `kill -USR2 ` + const cleanupSignalHandler = setupThemeSignalHandler(() => { + renderer.clearPaletteCache() + init() + }) + onCleanup(cleanupSignalHandler) - createEffect(() => { - setStore("active", appStore.state().settings.theme) - }) + // Sync active theme with app store settings + createEffect(() => { + const theme = appStore.state().settings.theme + if (theme) setStore("active", theme) + }) - createEffect(() => { - renderer - .getPalette({ size: 16 }) - .then((colors) => setStore("system", colors)) - .catch(() => {}) - }) + // Emit theme change events for observers + createEffect(() => { + const theme = store.active + const mode = store.mode + if (store.ready) { + emitThemeChanged(theme, mode) + } + }) - const values = createMemo(() => { - const themes = Object.keys(store.themes).length ? store.themes : THEME_JSON - return resolveTerminalTheme(themes, store.active, store.mode, store.system) - }) + const values = createMemo(() => { + return resolveTerminalTheme(store.themes, store.active, store.mode, store.system) + }) - const syntax = createMemo(() => generateSyntax(values() as unknown as Record)) - const subtleSyntax = createMemo(() => - generateSubtleSyntax(values() as unknown as Record & { thinkingOpacity?: number }) - ) + const syntax = createMemo(() => generateSyntax(values() as unknown as Record)) + const subtleSyntax = createMemo(() => + generateSubtleSyntax(values() as unknown as Record & { thinkingOpacity?: number }) + ) - const context: ThemeContextValue = { - theme: new Proxy(values(), { - get(_target, prop) { - return values()[prop as keyof typeof values] - }, - }) as ThemeResolved, - selected: () => store.active, - all: () => store.themes, - syntax, - subtleSyntax, - mode: () => store.mode, - setMode: (mode) => setStore("mode", mode), - set: (theme) => appStore.setTheme(theme as ThemeName), - ready, - } - - return ( - - {children} - - ) -} - -export function useTheme() { - const context = useContext(ThemeContext) - if (!context) { - throw new Error("useTheme must be used within a ThemeProvider") - } - return context -} + return { + theme: new Proxy(values(), { + get(_target, prop) { + // @ts-expect-error - dynamic property access + return values()[prop] + }, + }) as ThemeResolved, + get selected() { + return store.active + }, + all() { + return store.themes + }, + syntax, + subtleSyntax, + mode() { + return store.mode + }, + setMode(mode: "dark" | "light") { + setStore("mode", mode) + emitThemeModeChanged(mode) + }, + set(theme: string) { + appStore.setTheme(theme as ThemeName) + }, + get ready() { + return store.ready + }, + } + }, +}) diff --git a/src/context/helper.tsx b/src/context/helper.tsx new file mode 100644 index 0000000..829e671 --- /dev/null +++ b/src/context/helper.tsx @@ -0,0 +1,53 @@ +import { createContext, Show, useContext, type ParentProps } from "solid-js" + +/** + * Creates a simple context with automatic ready-state handling. + * + * This pattern ensures that child components are NOT rendered until the + * context's `ready` property is true (or undefined, meaning no ready check needed). + * + * This prevents the "useX must be used within a XProvider" errors that occur + * when child components try to use context values before the provider has + * finished async initialization. + * + * Usage: + * ```tsx + * export const { use: useMyContext, provider: MyProvider } = createSimpleContext({ + * name: "MyContext", + * init: (props: { someProp: string }) => { + * const [ready, setReady] = createSignal(false) + * // ... async initialization ... + * return { + * get ready() { return ready() }, + * // ... other values + * } + * }, + * }) + * ``` + */ +export function createSimpleContext>(input: { + name: string + init: ((input: Props) => T) | (() => T) +}) { + const ctx = createContext() + + return { + provider: (props: ParentProps) => { + const init = input.init(props) + // Use an arrow function accessor for the ready check to maintain reactivity. + // The getter `init.ready` reads from a store, so wrapping it in an + // accessor allows Solid to track changes reactively. + return ( + // @ts-expect-error - ready may not exist on all context types + + {props.children} + + ) + }, + use() { + const value = useContext(ctx) + if (!value) throw new Error(`${input.name} context must be used within a context provider`) + return value + }, + } +} diff --git a/src/index.tsx b/src/index.tsx index 696c364..db0fc8a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,10 +1,22 @@ import { render } from "@opentui/solid" import { App } from "./App" import { ThemeProvider } from "./context/ThemeContext" -import "./styles/theme.css" +import { ToastProvider, Toast } from "./ui/toast" +import { KeybindProvider } from "./context/KeybindContext" +import { DialogProvider } from "./ui/dialog" +import { CommandProvider } from "./ui/command" render(() => ( - - - + + + + + + + + + + + + )) diff --git a/src/types/settings.ts b/src/types/settings.ts index b546686..edb45bd 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -1,7 +1,7 @@ import type { RGBA } from "@opentui/core" import type { ColorValue, ThemeJson, Variant } from "./theme-schema" -export type ThemeName = "system" | "catppuccin" | "gruvbox" | "tokyo" | "nord" | "custom" +export type ThemeName = "system" | "opencode" | "catppuccin" | "gruvbox" | "tokyo" | "nord" | "custom" export type LayerBackgrounds = { layer0: ColorValue diff --git a/src/ui/command.tsx b/src/ui/command.tsx new file mode 100644 index 0000000..d05c8db --- /dev/null +++ b/src/ui/command.tsx @@ -0,0 +1,307 @@ +import { + createContext, + createMemo, + createSignal, + onCleanup, + useContext, + type Accessor, + type ParentProps, + For, + Show, +} from "solid-js" +import { useKeyboard, useTerminalDimensions } from "@opentui/solid" +import { useKeybind } from "../context/KeybindContext" +import { useDialog } from "./dialog" +import { useTheme } from "../context/ThemeContext" +import type { KeybindsConfig } from "../utils/keybind" +import { TextAttributes } from "@opentui/core" +import { emit } from "../utils/event-bus" + +/** + * Command option for the command palette. + */ +export type CommandOption = { + /** Display title */ + title: string + /** Unique identifier */ + value: string + /** Description shown below title */ + description?: string + /** Category for grouping */ + category?: string + /** Keybind reference */ + keybind?: keyof KeybindsConfig + /** Whether this command is suggested */ + suggested?: boolean + /** Slash command configuration */ + slash?: { + name: string + aliases?: string[] + } + /** Whether to hide from command list */ + hidden?: boolean + /** Whether command is enabled */ + enabled?: boolean + /** Footer text (usually keybind display) */ + footer?: string + /** Handler when command is selected */ + onSelect?: (dialog: ReturnType) => void +} + +type CommandContext = ReturnType +const ctx = createContext() + +function init() { + const [registrations, setRegistrations] = createSignal[]>([]) + const [suspendCount, setSuspendCount] = createSignal(0) + const dialog = useDialog() + const keybind = useKeybind() + + const entries = createMemo(() => { + const all = registrations().flatMap((x) => x()) + return all.map((x) => ({ + ...x, + footer: x.keybind ? keybind.print(x.keybind) : undefined, + })) + }) + + const isEnabled = (option: CommandOption) => option.enabled !== false + const isVisible = (option: CommandOption) => isEnabled(option) && !option.hidden + + const visibleOptions = createMemo(() => entries().filter((option) => isVisible(option))) + const suggestedOptions = createMemo(() => + visibleOptions() + .filter((option) => option.suggested) + .map((option) => ({ + ...option, + value: `suggested:${option.value}`, + category: "Suggested", + })), + ) + const suspended = () => suspendCount() > 0 + + // Handle keybind shortcuts + useKeyboard((evt) => { + if (suspended()) return + if (dialog.isOpen) return + for (const option of entries()) { + if (!isEnabled(option)) continue + if (option.keybind && keybind.match(option.keybind, evt)) { + evt.preventDefault() + option.onSelect?.(dialog) + emit("command.execute", { command: option.value }) + return + } + } + }) + + const result = { + /** + * Trigger a command by its value. + */ + trigger(name: string) { + for (const option of entries()) { + if (option.value === name) { + if (!isEnabled(option)) return + option.onSelect?.(dialog) + emit("command.execute", { command: name }) + return + } + } + }, + /** + * Get all slash commands. + */ + slashes() { + return visibleOptions().flatMap((option) => { + const slash = option.slash + if (!slash) return [] + return { + display: "/" + slash.name, + description: option.description ?? option.title, + aliases: slash.aliases?.map((alias) => "/" + alias), + onSelect: () => result.trigger(option.value), + } + }) + }, + /** + * Enable/disable keybinds temporarily. + */ + keybinds(enabled: boolean) { + setSuspendCount((count) => count + (enabled ? -1 : 1)) + }, + suspended, + /** + * Show the command palette dialog. + */ + show() { + dialog.replace(() => ) + }, + /** + * Register commands. Returns cleanup function. + */ + register(cb: () => CommandOption[]) { + const results = createMemo(cb) + setRegistrations((arr) => [results, ...arr]) + onCleanup(() => { + setRegistrations((arr) => arr.filter((x) => x !== results)) + }) + }, + /** + * Get all visible options. + */ + get options() { + return visibleOptions() + }, + } + return result +} + +export function useCommandDialog() { + const value = useContext(ctx) + if (!value) { + throw new Error("useCommandDialog must be used within a CommandProvider") + } + return value +} + +export function CommandProvider(props: ParentProps) { + const value = init() + const dialog = useDialog() + const keybind = useKeybind() + + // Open command palette on ctrl+p or command_list keybind + useKeyboard((evt) => { + if (value.suspended()) return + if (dialog.isOpen) return + if (evt.defaultPrevented) return + if (keybind.match("command_list", evt)) { + evt.preventDefault() + value.show() + return + } + }) + + return {props.children} +} + +/** + * Command palette dialog component. + */ +function CommandDialog(props: { options: CommandOption[]; suggestedOptions: CommandOption[] }) { + const { theme } = useTheme() + const dialog = useDialog() + const dimensions = useTerminalDimensions() + const [filter, setFilter] = createSignal("") + const [selectedIndex, setSelectedIndex] = createSignal(0) + + const filteredOptions = createMemo(() => { + const query = filter().toLowerCase() + if (!query) { + return [...props.suggestedOptions, ...props.options] + } + return props.options.filter( + (option) => + option.title.toLowerCase().includes(query) || + option.description?.toLowerCase().includes(query) || + option.category?.toLowerCase().includes(query) + ) + }) + + // Reset selection when filter changes + createMemo(() => { + filter() + setSelectedIndex(0) + }) + + useKeyboard((evt) => { + if (evt.name === "escape") { + dialog.clear() + evt.preventDefault() + return + } + + if (evt.name === "return" || evt.name === "enter") { + const option = filteredOptions()[selectedIndex()] + if (option) { + option.onSelect?.(dialog) + dialog.clear() + } + evt.preventDefault() + return + } + + if (evt.name === "up" || (evt.ctrl && evt.name === "p")) { + setSelectedIndex((i) => Math.max(0, i - 1)) + evt.preventDefault() + return + } + + if (evt.name === "down" || (evt.ctrl && evt.name === "n")) { + setSelectedIndex((i) => Math.min(filteredOptions().length - 1, i + 1)) + evt.preventDefault() + return + } + + // Handle text input + if (evt.name && evt.name.length === 1 && !evt.ctrl && !evt.meta) { + setFilter((f) => f + evt.name) + return + } + + if (evt.name === "backspace") { + setFilter((f) => f.slice(0, -1)) + return + } + }) + + const maxHeight = Math.floor(dimensions().height * 0.6) + + return ( + + {/* Search input */} + + + {"> "} + + + {filter() || "Type to search commands..."} + + + + {/* Command list */} + + + {(option, index) => ( + + + + + {option.title} + + + {option.footer} + + + + {option.description} + + + + )} + + + + No commands found + + + + + ) +} diff --git a/src/ui/dialog.tsx b/src/ui/dialog.tsx new file mode 100644 index 0000000..85d15f4 --- /dev/null +++ b/src/ui/dialog.tsx @@ -0,0 +1,224 @@ +import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" +import { batch, createContext, Show, useContext, type JSX, type ParentProps } from "solid-js" +import { useTheme } from "../context/ThemeContext" +import { RGBA, Renderable } from "@opentui/core" +import { createStore } from "solid-js/store" +import { Clipboard } from "../utils/clipboard" +import { useToast } from "./toast" +import { emit } from "../utils/event-bus" + +export type DialogSize = "medium" | "large" + +/** + * Dialog component that renders a modal overlay with content. + */ +export function Dialog( + props: ParentProps<{ + size?: DialogSize + onClose: () => void + }>, +) { + const dimensions = useTerminalDimensions() + const { theme } = useTheme() + const renderer = useRenderer() + + return ( + { + if (renderer.getSelection()) return + props.onClose?.() + }} + width={dimensions().width} + height={dimensions().height} + alignItems="center" + position="absolute" + paddingTop={Math.floor(dimensions().height / 4)} + left={0} + top={0} + backgroundColor={RGBA.fromInts(0, 0, 0, 150)} + > + { + if (renderer.getSelection()) return + e.stopPropagation() + }} + width={props.size === "large" ? 80 : 60} + maxWidth={dimensions().width - 2} + backgroundColor={theme.backgroundPanel} + paddingTop={1} + > + {props.children} + + + ) +} + +type DialogStackItem = { + element: JSX.Element + onClose?: () => void +} + +function init() { + const [store, setStore] = createStore({ + stack: [] as DialogStackItem[], + size: "medium" as DialogSize, + }) + + const renderer = useRenderer() + let focus: Renderable | null = null + + function refocus() { + setTimeout(() => { + if (!focus) return + if (focus.isDestroyed) return + function find(item: Renderable): boolean { + for (const child of item.getChildren()) { + if (child === focus) return true + if (find(child)) return true + } + return false + } + const found = find(renderer.root) + if (!found) return + focus.focus() + }, 1) + } + + useKeyboard((evt) => { + if (evt.name === "escape" && store.stack.length > 0) { + const current = store.stack.at(-1)! + current.onClose?.() + setStore("stack", store.stack.slice(0, -1)) + evt.preventDefault() + evt.stopPropagation() + refocus() + emit("dialog.close", {}) + } + }) + + return { + /** + * Clear all dialogs from the stack. + */ + clear() { + for (const item of store.stack) { + if (item.onClose) item.onClose() + } + batch(() => { + setStore("size", "medium") + setStore("stack", []) + }) + refocus() + emit("dialog.close", {}) + }, + + /** + * Replace all dialogs with a new one. + */ + replace(input: JSX.Element | (() => JSX.Element), onClose?: () => void) { + if (store.stack.length === 0) { + focus = renderer.currentFocusedRenderable + focus?.blur() + } + for (const item of store.stack) { + if (item.onClose) item.onClose() + } + const element = typeof input === "function" ? input() : input + setStore("size", "medium") + setStore("stack", [{ element, onClose }]) + emit("dialog.open", { dialogId: "dialog" }) + }, + + /** + * Push a new dialog onto the stack. + */ + push(input: JSX.Element | (() => JSX.Element), onClose?: () => void) { + if (store.stack.length === 0) { + focus = renderer.currentFocusedRenderable + focus?.blur() + } + const element = typeof input === "function" ? input() : input + setStore("stack", [...store.stack, { element, onClose }]) + emit("dialog.open", { dialogId: "dialog" }) + }, + + /** + * Pop the top dialog from the stack. + */ + pop() { + if (store.stack.length === 0) return + const current = store.stack.at(-1)! + current.onClose?.() + setStore("stack", store.stack.slice(0, -1)) + if (store.stack.length === 0) { + refocus() + } + emit("dialog.close", {}) + }, + + get stack() { + return store.stack + }, + + get size() { + return store.size + }, + + setSize(size: DialogSize) { + setStore("size", size) + }, + + get isOpen() { + return store.stack.length > 0 + }, + } +} + +export type DialogContext = ReturnType + +const ctx = createContext() + +/** + * DialogProvider wraps the application and provides dialog functionality. + * Also handles clipboard copy on text selection within dialogs. + */ +export function DialogProvider(props: ParentProps) { + const value = init() + const renderer = useRenderer() + const toast = useToast() + + return ( + + {props.children} + { + const text = renderer.getSelection()?.getSelectedText() + if (text && text.length > 0) { + await Clipboard.copy(text) + .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) + .catch(toast.error) + renderer.clearSelection() + } + }} + > + 0}> + value.clear()} size={value.size}> + {value.stack.at(-1)!.element} + + + + + ) +} + +/** + * Hook to access the dialog context. + */ +export function useDialog() { + const value = useContext(ctx) + if (!value) { + throw new Error("useDialog must be used within a DialogProvider") + } + return value +} diff --git a/src/ui/toast.tsx b/src/ui/toast.tsx new file mode 100644 index 0000000..03955a4 --- /dev/null +++ b/src/ui/toast.tsx @@ -0,0 +1,153 @@ +import { createContext, useContext, type ParentProps, Show } from "solid-js" +import { createStore } from "solid-js/store" +import { useTheme } from "../context/ThemeContext" +import { useTerminalDimensions } from "@opentui/solid" +import { TextAttributes } from "@opentui/core" +import { emit } from "../utils/event-bus" + +export type ToastVariant = "info" | "success" | "warning" | "error" + +export type ToastOptions = { + title?: string + message: string + variant: ToastVariant + duration?: number +} + +const DEFAULT_DURATION = 5000 + +/** + * Toast component that displays at the top-right of the screen. + * NOTE: This component must be rendered INSIDE ThemeProvider since it uses useTheme(). + * The ToastProvider itself can be placed outside ThemeProvider if needed. + */ +export function Toast() { + const toast = useToast() + const { theme } = useTheme() + const dimensions = useTerminalDimensions() + + const getVariantColor = (variant: ToastVariant) => { + switch (variant) { + case "success": + return theme.success + case "warning": + return theme.warning + case "error": + return theme.error + case "info": + default: + return theme.info + } + } + + return ( + + {(current) => ( + + + + + {current().title} + + + + {current().message} + + + + )} + + ) +} + +function init() { + const [store, setStore] = createStore({ + currentToast: null as ToastOptions | null, + }) + + let timeoutHandle: NodeJS.Timeout | null = null + + const toast = { + show(options: ToastOptions) { + const duration = options.duration ?? DEFAULT_DURATION + setStore("currentToast", { + title: options.title, + message: options.message, + variant: options.variant, + }) + + // Emit event for other listeners + emit("toast.show", options) + + if (timeoutHandle) clearTimeout(timeoutHandle) + timeoutHandle = setTimeout(() => { + setStore("currentToast", null) + }, duration) + }, + error: (err: unknown) => { + if (err instanceof Error) { + return toast.show({ + variant: "error", + message: err.message, + }) + } + toast.show({ + variant: "error", + message: "An unknown error has occurred", + }) + }, + info: (message: string, title?: string) => { + toast.show({ variant: "info", message, title }) + }, + success: (message: string, title?: string) => { + toast.show({ variant: "success", message, title }) + }, + warning: (message: string, title?: string) => { + toast.show({ variant: "warning", message, title }) + }, + clear: () => { + if (timeoutHandle) clearTimeout(timeoutHandle) + setStore("currentToast", null) + }, + get currentToast(): ToastOptions | null { + return store.currentToast + }, + } + return toast +} + +export type ToastContext = ReturnType + +const ctx = createContext() + +/** + * ToastProvider provides toast functionality. + * NOTE: The Toast UI component is NOT rendered here - you must render + * separately inside your component tree, after ThemeProvider. + */ +export function ToastProvider(props: ParentProps) { + const value = init() + return {props.children} +} + +export function useToast() { + const value = useContext(ctx) + if (!value) { + throw new Error("useToast must be used within a ToastProvider") + } + return value +} diff --git a/src/utils/clipboard.ts b/src/utils/clipboard.ts new file mode 100644 index 0000000..346df68 --- /dev/null +++ b/src/utils/clipboard.ts @@ -0,0 +1,221 @@ +import { $ } from "bun" +import { platform, release } from "os" +import { tmpdir } from "os" +import path from "path" + +/** + * Writes text to clipboard via OSC 52 escape sequence. + * This allows clipboard operations to work over SSH by having + * the terminal emulator handle the clipboard locally. + */ +function writeOsc52(text: string): void { + if (!process.stdout.isTTY) return + const base64 = Buffer.from(text).toString("base64") + const osc52 = `\x1b]52;c;${base64}\x07` + const passthrough = process.env["TMUX"] || process.env["STY"] + const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 + process.stdout.write(sequence) +} + +/** + * Lazy initialization for clipboard copy method. + * Detects the best clipboard method for the current platform. + */ +function createLazy(factory: () => T): () => T { + let value: T | undefined + return () => { + if (value === undefined) { + value = factory() + } + return value + } +} + +export namespace Clipboard { + export interface Content { + data: string + mime: string + } + + /** + * Read content from the clipboard. + * Supports text and image (PNG) content on macOS, Windows, and Linux. + */ + export async function read(): Promise { + const os = platform() + + // macOS: Try to read PNG image first + if (os === "darwin") { + const tmpfile = path.join(tmpdir(), "podtui-clipboard.png") + try { + await $`osascript -e 'set imageData to the clipboard as "PNGf"' -e 'set fileRef to open for access POSIX file "${tmpfile}" with write permission' -e 'set eof fileRef to 0' -e 'write imageData to fileRef' -e 'close access fileRef'` + .nothrow() + .quiet() + const file = Bun.file(tmpfile) + const buffer = await file.arrayBuffer() + if (buffer.byteLength > 0) { + return { data: Buffer.from(buffer).toString("base64"), mime: "image/png" } + } + } catch { + // Ignore errors, fall through to text + } finally { + await $`rm -f "${tmpfile}"`.nothrow().quiet() + } + } + + // Windows/WSL: Try to read PNG image + if (os === "win32" || release().includes("WSL")) { + const script = + "Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }" + const base64 = await $`powershell.exe -NonInteractive -NoProfile -command "${script}"`.nothrow().text() + if (base64) { + const imageBuffer = Buffer.from(base64.trim(), "base64") + if (imageBuffer.length > 0) { + return { data: imageBuffer.toString("base64"), mime: "image/png" } + } + } + } + + // Linux: Try Wayland or X11 + if (os === "linux") { + // Try Wayland first + const wayland = await $`wl-paste -t image/png`.nothrow().arrayBuffer() + if (wayland && wayland.byteLength > 0) { + return { data: Buffer.from(wayland).toString("base64"), mime: "image/png" } + } + // Try X11 + const x11 = await $`xclip -selection clipboard -t image/png -o`.nothrow().arrayBuffer() + if (x11 && x11.byteLength > 0) { + return { data: Buffer.from(x11).toString("base64"), mime: "image/png" } + } + } + + // Fall back to reading text + try { + const text = await readText() + if (text) { + return { data: text, mime: "text/plain" } + } + } catch { + // Ignore errors + } + + return undefined + } + + /** + * Read text from the clipboard. + */ + export async function readText(): Promise { + const os = platform() + + if (os === "darwin") { + const result = await $`pbpaste`.nothrow().text() + return result || undefined + } + + if (os === "linux") { + // Try Wayland first + if (process.env["WAYLAND_DISPLAY"]) { + const result = await $`wl-paste`.nothrow().text() + if (result) return result + } + // Try X11 + const result = await $`xclip -selection clipboard -o`.nothrow().text() + return result || undefined + } + + if (os === "win32" || release().includes("WSL")) { + const result = await $`powershell.exe -NonInteractive -NoProfile -command "Get-Clipboard"`.nothrow().text() + return result?.trim() || undefined + } + + return undefined + } + + const getCopyMethod = createLazy(() => { + const os = platform() + + if (os === "darwin" && Bun.which("osascript")) { + return async (text: string) => { + const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + await $`osascript -e 'set the clipboard to "${escaped}"'`.nothrow().quiet() + } + } + + if (os === "linux") { + if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) { + return async (text: string) => { + const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" }) + proc.stdin.write(text) + proc.stdin.end() + await proc.exited.catch(() => {}) + } + } + if (Bun.which("xclip")) { + return async (text: string) => { + const proc = Bun.spawn(["xclip", "-selection", "clipboard"], { + stdin: "pipe", + stdout: "ignore", + stderr: "ignore", + }) + proc.stdin.write(text) + proc.stdin.end() + await proc.exited.catch(() => {}) + } + } + if (Bun.which("xsel")) { + return async (text: string) => { + const proc = Bun.spawn(["xsel", "--clipboard", "--input"], { + stdin: "pipe", + stdout: "ignore", + stderr: "ignore", + }) + proc.stdin.write(text) + proc.stdin.end() + await proc.exited.catch(() => {}) + } + } + } + + if (os === "win32") { + return async (text: string) => { + // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.) + const proc = Bun.spawn( + [ + "powershell.exe", + "-NonInteractive", + "-NoProfile", + "-Command", + "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())", + ], + { + stdin: "pipe", + stdout: "ignore", + stderr: "ignore", + }, + ) + + proc.stdin.write(text) + proc.stdin.end() + await proc.exited.catch(() => {}) + } + } + + // Fallback: No native clipboard support + return async (_text: string) => { + console.warn("No clipboard support available on this platform") + } + }) + + /** + * Copy text to the clipboard. + * Uses OSC 52 for SSH/tmux support and native clipboard for local. + */ + export async function copy(text: string): Promise { + // Always try OSC 52 first for SSH/tmux support + writeOsc52(text) + // Then use native clipboard + await getCopyMethod()(text) + } +} diff --git a/src/utils/custom-themes.ts b/src/utils/custom-themes.ts index e5b62a5..12fe771 100644 --- a/src/utils/custom-themes.ts +++ b/src/utils/custom-themes.ts @@ -4,6 +4,9 @@ import type { ThemeJson } from "../types/theme-schema" import { THEME_JSON } from "../constants/themes" import { validateTheme } from "./theme-loader" +// Files to exclude from theme loading (not actual themes) +const EXCLUDED_FILES = new Set(["schema", "schema.json"]) + export async function getCustomThemes() { const home = process.env.HOME ?? "" if (!home) return {} @@ -22,6 +25,10 @@ export async function getCustomThemes() { const glob = new Bun.Glob("*.json") for await (const item of glob.scan({ absolute: true, followSymlinks: true, cwd: dir })) { const name = path.basename(item, ".json") + // Skip non-theme files + if (EXCLUDED_FILES.has(name) || EXCLUDED_FILES.has(path.basename(item))) { + continue + } const json = (await Bun.file(item).json()) as ThemeJson validateTheme(json, item) result[name] = json diff --git a/src/utils/event-bus.ts b/src/utils/event-bus.ts new file mode 100644 index 0000000..e52a230 --- /dev/null +++ b/src/utils/event-bus.ts @@ -0,0 +1,136 @@ +/** + * Simple event bus for inter-component communication. + * + * This provides a decoupled way for components to communicate without + * direct dependencies. Components can publish events and subscribe to + * events they're interested in. + * + * Usage: + * ```tsx + * // Subscribe to events + * const unsub = EventBus.on("theme.changed", (data) => { + * console.log("Theme changed to:", data.theme) + * }) + * + * // Publish events + * EventBus.emit("theme.changed", { theme: "dark" }) + * + * // Cleanup + * unsub() + * ``` + */ + +type EventHandler = (data: T) => void + +// Export EventHandler type for external use +export type { EventHandler } + +interface EventBusInstance { + on(event: string, handler: EventHandler): () => void + once(event: string, handler: EventHandler): () => void + off(event: string, handler: EventHandler): void + emit(event: string, data: T): void + clear(): void +} + +function createEventBus(): EventBusInstance { + const handlers = new Map>() + + return { + on(event: string, handler: EventHandler): () => void { + if (!handlers.has(event)) { + handlers.set(event, new Set()) + } + handlers.get(event)!.add(handler as EventHandler) + + // Return unsubscribe function + return () => { + this.off(event, handler) + } + }, + + once(event: string, handler: EventHandler): () => void { + const wrappedHandler: EventHandler = (data) => { + this.off(event, wrappedHandler) + handler(data) + } + return this.on(event, wrappedHandler) + }, + + off(event: string, handler: EventHandler): void { + const eventHandlers = handlers.get(event) + if (eventHandlers) { + eventHandlers.delete(handler as EventHandler) + if (eventHandlers.size === 0) { + handlers.delete(event) + } + } + }, + + emit(event: string, data: T): void { + const eventHandlers = handlers.get(event) + if (eventHandlers) { + for (const handler of eventHandlers) { + try { + handler(data) + } catch (error) { + console.error(`Error in event handler for "${event}":`, error) + } + } + } + }, + + clear(): void { + handlers.clear() + }, + } +} + +// Singleton event bus instance +export const EventBus = createEventBus() + +// Common event types for the application +export type AppEvents = { + "theme.changed": { theme: string; mode: "dark" | "light" } + "theme.mode.changed": { mode: "dark" | "light" } + "theme.reload": {} + "navigation.tab.changed": { tab: string; previousTab?: string } + "navigation.layer.changed": { depth: number; previousDepth: number } + "feed.subscribed": { feedId: string; feedUrl: string } + "feed.unsubscribed": { feedId: string } + "player.play": { episodeId: string } + "player.pause": { episodeId: string } + "player.stop": {} + "auth.login": { userId: string } + "auth.logout": {} + "toast.show": { message: string; variant: "info" | "success" | "warning" | "error"; title?: string; duration?: number } + "dialog.open": { dialogId: string } + "dialog.close": { dialogId?: string } + "command.execute": { command: string; args?: unknown } +} + +// Type-safe emit and on functions +export function emit(event: K, data: AppEvents[K]): void { + EventBus.emit(event, data) +} + +export function on( + event: K, + handler: EventHandler +): () => void { + return EventBus.on(event, handler) +} + +export function once( + event: K, + handler: EventHandler +): () => void { + return EventBus.once(event, handler) +} + +export function off( + event: K, + handler: EventHandler +): void { + EventBus.off(event, handler) +} diff --git a/src/utils/keybind.ts b/src/utils/keybind.ts new file mode 100644 index 0000000..2370444 --- /dev/null +++ b/src/utils/keybind.ts @@ -0,0 +1,187 @@ +import type { ParsedKey } from "@opentui/core" + +/** + * Keyboard shortcut parsing and matching utilities. + * + * Supports key combinations like: + * - "ctrl+c" - Control + c + * - "alt+x" - Alt + x + * - "shift+enter" - Shift + Enter + * - "n" - Leader key followed by n + * - "ctrl+shift+p" - Control + Shift + p + */ + +export namespace Keybind { + export interface Info { + key: string + ctrl: boolean + alt: boolean + shift: boolean + meta: boolean + leader: boolean + } + + /** + * Parse a keybind string into a structured Info object. + * + * Examples: + * - "ctrl+c" -> { key: "c", ctrl: true, ... } + * - "n" -> { key: "n", leader: true, ... } + * - "alt+shift+x" -> { key: "x", alt: true, shift: true, ... } + */ + export function parse(input: string): Info[] { + if (!input) return [] + + // Handle multiple keybinds separated by comma or space + const parts = input.split(/[,\s]+/).filter(Boolean) + + return parts.map((part) => { + const info: Info = { + key: "", + ctrl: false, + alt: false, + shift: false, + meta: false, + leader: false, + } + + // Check for leader key prefix + if (part.startsWith("")) { + info.leader = true + part = part.substring(8) // Remove "" + } + + // Split by + for modifiers + const tokens = part.toLowerCase().split("+") + + for (const token of tokens) { + switch (token) { + case "ctrl": + case "control": + info.ctrl = true + break + case "alt": + case "option": + info.alt = true + break + case "shift": + info.shift = true + break + case "meta": + case "cmd": + case "command": + case "win": + case "super": + info.meta = true + break + default: + // The last non-modifier token is the key + info.key = token + } + } + + return info + }) + } + + /** + * Convert a ParsedKey event to a Keybind.Info. + */ + export function fromParsedKey(evt: ParsedKey, leader: boolean = false): Info { + // ParsedKey has ctrl, shift, meta but may not have alt directly + // We need to check what properties are available + const evtAny = evt as unknown as Record + return { + key: evt.name?.toLowerCase() ?? "", + ctrl: evt.ctrl ?? false, + alt: (evtAny.alt as boolean) ?? false, + shift: evt.shift ?? false, + meta: evt.meta ?? false, + leader, + } + } + + /** + * Check if a keybind matches a parsed key event. + */ + export function match(keybind: Info, evt: Info): boolean { + return ( + keybind.key === evt.key && + keybind.ctrl === evt.ctrl && + keybind.alt === evt.alt && + keybind.shift === evt.shift && + keybind.meta === evt.meta && + keybind.leader === evt.leader + ) + } + + /** + * Convert a keybind Info to a display string. + */ + export function toString(info: Info): string { + const parts: string[] = [] + + if (info.leader) parts.push("") + if (info.ctrl) parts.push("Ctrl") + if (info.alt) parts.push("Alt") + if (info.shift) parts.push("Shift") + if (info.meta) parts.push("Cmd") + + if (info.key) { + // Capitalize special keys + const displayKey = info.key.length === 1 ? info.key.toUpperCase() : capitalize(info.key) + parts.push(displayKey) + } + + return parts.join("+") + } + + function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1) + } +} + +/** + * Default keybindings configuration. + */ +export const DEFAULT_KEYBINDS = { + // Leader key (space by default) + leader: "space", + + // Navigation + tab_next: "tab", + tab_prev: "shift+tab", + + // App commands + command_list: "ctrl+p", + help: "?", + quit: "ctrl+c", + + // Session/content + session_new: "n", + session_list: "s", + + // Theme + theme_list: "t", + + // Player + player_play: "space", + player_pause: "space", + player_next: "n", + player_prev: "p", + player_seek_forward: "l", + player_seek_backward: "h", + + // List navigation + list_up: "k", + list_down: "j", + list_top: "g", + list_bottom: "G", + list_select: "enter", + + // Search + search_focus: "/", + search_clear: "escape", +} + +export type KeybindsConfig = typeof DEFAULT_KEYBINDS diff --git a/src/utils/theme-observer.ts b/src/utils/theme-observer.ts new file mode 100644 index 0000000..cce06b2 --- /dev/null +++ b/src/utils/theme-observer.ts @@ -0,0 +1,104 @@ +/** + * Theme observer utility for detecting and responding to theme changes. + * + * This module provides utilities for: + * - Listening to SIGUSR2 signals for theme reload + * - Emitting theme change events via the event bus + * - Tracking theme change state + */ + +import { emit, on, off, type EventHandler } from "./event-bus" + +/** + * Subscribe to theme reload events. + * These are triggered by SIGUSR2 signals. + */ +export function onThemeReload(handler: EventHandler<{}>): () => void { + return on("theme.reload", handler) +} + +/** + * Subscribe to theme changed events. + * These are triggered when the theme selection changes. + */ +export function onThemeChanged( + handler: EventHandler<{ theme: string; mode: "dark" | "light" }> +): () => void { + return on("theme.changed", handler) +} + +/** + * Subscribe to theme mode changed events. + * These are triggered when switching between dark/light mode. + */ +export function onThemeModeChanged( + handler: EventHandler<{ mode: "dark" | "light" }> +): () => void { + return on("theme.mode.changed", handler) +} + +/** + * Emit a theme reload event. + */ +export function emitThemeReload(): void { + emit("theme.reload", {}) +} + +/** + * Emit a theme changed event. + */ +export function emitThemeChanged(theme: string, mode: "dark" | "light"): void { + emit("theme.changed", { theme, mode }) +} + +/** + * Emit a theme mode changed event. + */ +export function emitThemeModeChanged(mode: "dark" | "light"): void { + emit("theme.mode.changed", { mode }) +} + +/** + * Setup SIGUSR2 signal handler for theme reload. + * This allows external tools to trigger a theme refresh by sending SIGUSR2 to the process. + * + * Usage: `kill -USR2 ` to trigger a theme reload + * + * @param onReload - Callback to execute when SIGUSR2 is received + * @returns Cleanup function to remove the handler + */ +export function setupThemeSignalHandler(onReload: () => void): () => void { + const handler = () => { + emitThemeReload() + onReload() + } + + process.on("SIGUSR2", handler) + + return () => { + process.off("SIGUSR2", handler) + } +} + +/** + * Create a debounced theme change handler to prevent rapid consecutive updates. + * + * @param handler - The handler to debounce + * @param delay - Delay in milliseconds (default: 100ms) + */ +export function createDebouncedThemeHandler( + handler: (event: T) => void, + delay: number = 100 +): (event: T) => void { + let timeout: NodeJS.Timeout | null = null + + return (event: T) => { + if (timeout) { + clearTimeout(timeout) + } + timeout = setTimeout(() => { + handler(event) + timeout = null + }, delay) + } +} diff --git a/tasks/podtui-navigation-theming-improvements/01-analyze-navigation-system.md b/tasks/podtui-navigation-theming-improvements/01-analyze-navigation-system.md deleted file mode 100644 index 570d7f0..0000000 --- a/tasks/podtui-navigation-theming-improvements/01-analyze-navigation-system.md +++ /dev/null @@ -1,50 +0,0 @@ -# 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 diff --git a/tasks/podtui-navigation-theming-improvements/02-fix-discover-tab-crash.md b/tasks/podtui-navigation-theming-improvements/02-fix-discover-tab-crash.md deleted file mode 100644 index 19d3ed9..0000000 --- a/tasks/podtui-navigation-theming-improvements/02-fix-discover-tab-crash.md +++ /dev/null @@ -1,51 +0,0 @@ -# 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 diff --git a/tasks/podtui-navigation-theming-improvements/03-fix-feeds-tab-crash.md b/tasks/podtui-navigation-theming-improvements/03-fix-feeds-tab-crash.md deleted file mode 100644 index b111d86..0000000 --- a/tasks/podtui-navigation-theming-improvements/03-fix-feeds-tab-crash.md +++ /dev/null @@ -1,50 +0,0 @@ -# 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 diff --git a/tasks/podtui-navigation-theming-improvements/04-fix-settings-sources-crash.md b/tasks/podtui-navigation-theming-improvements/04-fix-settings-sources-crash.md deleted file mode 100644 index 4aef492..0000000 --- a/tasks/podtui-navigation-theming-improvements/04-fix-settings-sources-crash.md +++ /dev/null @@ -1,54 +0,0 @@ -# 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 diff --git a/tasks/podtui-navigation-theming-improvements/05-design-layered-navigation-ui.md b/tasks/podtui-navigation-theming-improvements/05-design-layered-navigation-ui.md deleted file mode 100644 index d061507..0000000 --- a/tasks/podtui-navigation-theming-improvements/05-design-layered-navigation-ui.md +++ /dev/null @@ -1,51 +0,0 @@ -# 05. Design Layered Navigation UI System - -meta: - id: podtui-navigation-theming-improvements-05 - 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: [navigation, ui-design, layer-system] - -objective: -- Design a visual layered navigation system that clearly shows depth -- Implement active layer indicators and highlighting -- Create smooth layer transition animations -- Establish visual hierarchy for nested content - -deliverables: -- Enhanced Layout component with layer background system -- Layer indicator component -- Layer transition animations -- Visual hierarchy documentation - -steps: -- Create layer background color system in constants/themes.ts -- Enhance Layout.tsx to support layer backgrounds -- Create LayerIndicator component -- Implement layer depth visual cues -- Add smooth transitions between layers -- Test layer visibility and transitions - -tests: -- Unit: Test LayerIndicator component -- Integration: Test layer navigation visual feedback - -acceptance_criteria: -- Layer backgrounds are visible and distinct -- Active layer is clearly highlighted -- Layer depth is visually indicated -- Transitions are smooth and intuitive -- Visual hierarchy is clear - -validation: -- Run `bun run start` and test layer navigation -- Verify layer backgrounds appear at different depths -- Check that active layer is clearly visible -- Test smooth transitions between layers - -notes: -- Use subtle color variations for layer backgrounds -- Ensure high contrast for readability -- Consider animation duration (200-300ms) -- Layer depth should be limited to 3-4 levels max diff --git a/tasks/podtui-navigation-theming-improvements/06-implement-layer-navigation-controls.md b/tasks/podtui-navigation-theming-improvements/06-implement-layer-navigation-controls.md deleted file mode 100644 index ce83c6f..0000000 --- a/tasks/podtui-navigation-theming-improvements/06-implement-layer-navigation-controls.md +++ /dev/null @@ -1,54 +0,0 @@ -# 06. Implement Left/Right Layer Navigation Controls - -meta: - id: podtui-navigation-theming-improvements-06 - feature: podtui-navigation-theming-improvements - priority: P1 - depends_on: [podtui-navigation-theming-improvements-05] - tags: [implementation, navigation, keyboard] - -objective: -- Enhance left/right arrow key navigation between layers -- Add visual feedback when navigating layers -- Prevent invalid layer transitions (can't go left from layer 0) -- Add navigation hints in Navigation component - -deliverables: -- Enhanced keyboard handler with layer navigation -- Updated Navigation component with layer hints -- Visual feedback for layer navigation -- Layer boundary prevention logic - -steps: -- Update useAppKeyboard hook to handle left/right for layer navigation -- Add layer navigation visual feedback -- Prevent invalid layer transitions (can't go left from layer 0, can't go right beyond max) -- Update Navigation component to show layer navigation hints -- Add visual indicators for current layer position -- 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: -- key navigates to previous layer (prevents going below layer 0) -- key navigates to next layer (prevents exceeding max depth) -- 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 existing useAppKeyboard hook as base -- Consider max layer depth (3-4 levels) -- Ensure smooth visual transitions -- Consider adding sound effects for navigation diff --git a/tasks/podtui-navigation-theming-improvements/07-implement-enter-escape-controls.md b/tasks/podtui-navigation-theming-improvements/07-implement-enter-escape-controls.md deleted file mode 100644 index e5e3447..0000000 --- a/tasks/podtui-navigation-theming-improvements/07-implement-enter-escape-controls.md +++ /dev/null @@ -1,55 +0,0 @@ -# 07. Implement Enter/Escape Layer Navigation Controls - -meta: - id: podtui-navigation-theming-improvements-07 - feature: podtui-navigation-theming-improvements - priority: P1 - depends_on: [podtui-navigation-theming-improvements-05] - tags: [implementation, navigation, keyboard] - -objective: -- Enhance enter key to go down into a layer -- Enhance escape key to go up multiple layers at once -- Add visual feedback when entering/exiting layers -- Prevent invalid layer transitions - -deliverables: -- Enhanced keyboard handler for enter/escape layer navigation -- Updated Navigation component with layer hints -- Visual feedback for layer navigation -- Layer boundary prevention logic - -steps: -- Update useAppKeyboard hook to handle enter for going down -- Update useAppKeyboard hook to handle escape for going up multiple layers -- Add visual feedback when entering/exiting layers -- Prevent invalid layer transitions (can't go down from max depth) -- Update Navigation component to show layer navigation hints -- Add visual indicators for current layer position -- Test enter to go down, escape to go up -- Ensure proper layer nesting behavior - -tests: -- Unit: Test keyboard handler with mocked key events -- Integration: Test enter/escape navigation between layers - -acceptance_criteria: -- key goes down into a layer (prevents going below max depth) -- key goes up multiple layers at once -- 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 enter/escape 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 existing useAppKeyboard hook as base -- Consider max layer depth (3-4 levels) -- Ensure smooth visual transitions -- Consider adding sound effects for navigation diff --git a/tasks/podtui-navigation-theming-improvements/08-design-active-layer-colors.md b/tasks/podtui-navigation-theming-improvements/08-design-active-layer-colors.md deleted file mode 100644 index 72ea284..0000000 --- a/tasks/podtui-navigation-theming-improvements/08-design-active-layer-colors.md +++ /dev/null @@ -1,56 +0,0 @@ -# 08. Design Active Layer Background Color System - -meta: - id: podtui-navigation-theming-improvements-08 - feature: podtui-navigation-theming-improvements - priority: P1 - depends_on: [podtui-navigation-theming-improvements-05] - tags: [implementation, theming, navigation] - -objective: -- Design active layer background colors for each depth level -- Define color palette for layer backgrounds -- Create theme-aware layer styling -- Implement visual hierarchy for layers - -deliverables: -- Enhanced theme system with layer backgrounds -- Layer background colors for all themes -- Visual hierarchy implementation -- Theme tokens for layer backgrounds - -steps: -- Review existing theme system in src/constants/themes.ts -- Design layer background colors for layer 0-3 -- Define color palette that works with existing themes -- Add layerBackgrounds property to theme colors -- Implement layer background rendering in Layout component -- Add visual indicators for active/inactive layers -- Ensure colors work with system/light/dark modes -- Test layer color transitions - -tests: -- Unit: Test theme layer backgrounds -- Integration: Test layer color rendering - -acceptance_criteria: -- Layer background colors are defined for all themes -- Active layer is clearly visible with distinct background -- Inactive layers have subtle background variations -- Visual hierarchy is clear between layers -- Colors work with all theme modes -- Layer backgrounds are accessible and readable - -validation: -- Run `bun run start` and test layer colors -- Verify layer backgrounds appear at different depths -- Check that active layer is clearly visible -- Test with different themes (catppuccin, gruvbox, etc.) -- Verify colors work in both light and dark modes - -notes: -- Use existing theme colors for layer backgrounds -- Ensure high contrast for readability -- Colors should be subtle but clearly visible -- Consider terminal color limitations -- Design should be consistent with existing UI elements diff --git a/tasks/podtui-navigation-theming-improvements/09-create-theme-context-provider.md b/tasks/podtui-navigation-theming-improvements/09-create-theme-context-provider.md deleted file mode 100644 index d5a3749..0000000 --- a/tasks/podtui-navigation-theming-improvements/09-create-theme-context-provider.md +++ /dev/null @@ -1,59 +0,0 @@ -# 09. Create Theme Context Provider - -meta: - id: podtui-navigation-theming-improvements-09 - feature: podtui-navigation-theming-improvements - priority: P1 - depends_on: [] - tags: [theming, implementation, solid-js] - -objective: -- Create theme context provider for global theme management -- Implement theme state management with signals -- Provide theme tokens to all components -- Add system theme detection and preference observer - -deliverables: -- Theme context provider component -- Theme state management hooks -- Theme provider integration -- System theme detection logic - -steps: -- Review existing theme system in src/stores/app.ts -- 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 existing appStore as base for theme management -- Follow SolidJS context patterns -- Use createSignal for reactive theme state -- Ensure proper cleanup in onCleanup -- Test with different theme configurations diff --git a/tasks/podtui-navigation-theming-improvements/10-implement-desktop-theme-types.md b/tasks/podtui-navigation-theming-improvements/10-implement-desktop-theme-types.md deleted file mode 100644 index 9146e89..0000000 --- a/tasks/podtui-navigation-theming-improvements/10-implement-desktop-theme-types.md +++ /dev/null @@ -1,57 +0,0 @@ -# 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 diff --git a/tasks/podtui-navigation-theming-improvements/11-implement-theme-resolution.md b/tasks/podtui-navigation-theming-improvements/11-implement-theme-resolution.md deleted file mode 100644 index b078b72..0000000 --- a/tasks/podtui-navigation-theming-improvements/11-implement-theme-resolution.md +++ /dev/null @@ -1,59 +0,0 @@ -# 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 diff --git a/tasks/podtui-navigation-theming-improvements/12-create-css-token-system.md b/tasks/podtui-navigation-theming-improvements/12-create-css-token-system.md deleted file mode 100644 index 74919ef..0000000 --- a/tasks/podtui-navigation-theming-improvements/12-create-css-token-system.md +++ /dev/null @@ -1,57 +0,0 @@ -# 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 diff --git a/tasks/podtui-navigation-theming-improvements/13-implement-system-theme-detection.md b/tasks/podtui-navigation-theming-improvements/13-implement-system-theme-detection.md deleted file mode 100644 index d432b0e..0000000 --- a/tasks/podtui-navigation-theming-improvements/13-implement-system-theme-detection.md +++ /dev/null @@ -1,55 +0,0 @@ -# 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 diff --git a/tasks/podtui-navigation-theming-improvements/14-integrate-theme-provider.md b/tasks/podtui-navigation-theming-improvements/14-integrate-theme-provider.md deleted file mode 100644 index 210ceec..0000000 --- a/tasks/podtui-navigation-theming-improvements/14-integrate-theme-provider.md +++ /dev/null @@ -1,59 +0,0 @@ -# 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 diff --git a/tasks/podtui-navigation-theming-improvements/15-update-components-to-use-themes.md b/tasks/podtui-navigation-theming-improvements/15-update-components-to-use-themes.md deleted file mode 100644 index 1d85d5d..0000000 --- a/tasks/podtui-navigation-theming-improvements/15-update-components-to-use-themes.md +++ /dev/null @@ -1,60 +0,0 @@ -# 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 diff --git a/tasks/podtui-navigation-theming-improvements/16-test-navigation-flows.md b/tasks/podtui-navigation-theming-improvements/16-test-navigation-flows.md deleted file mode 100644 index b212341..0000000 --- a/tasks/podtui-navigation-theming-improvements/16-test-navigation-flows.md +++ /dev/null @@ -1,65 +0,0 @@ -# 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 diff --git a/tasks/podtui-navigation-theming-improvements/17-test-tab-crash-fixes.md b/tasks/podtui-navigation-theming-improvements/17-test-tab-crash-fixes.md deleted file mode 100644 index 080c7eb..0000000 --- a/tasks/podtui-navigation-theming-improvements/17-test-tab-crash-fixes.md +++ /dev/null @@ -1,64 +0,0 @@ -# 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 diff --git a/tasks/podtui-navigation-theming-improvements/18-test-theming-system.md b/tasks/podtui-navigation-theming-improvements/18-test-theming-system.md deleted file mode 100644 index 421f404..0000000 --- a/tasks/podtui-navigation-theming-improvements/18-test-theming-system.md +++ /dev/null @@ -1,72 +0,0 @@ -# 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 diff --git a/tasks/podtui-navigation-theming-improvements/README.md b/tasks/podtui-navigation-theming-improvements/README.md deleted file mode 100644 index 51b365e..0000000 --- a/tasks/podtui-navigation-theming-improvements/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# 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 -- [x] 01 — Analyze current navigation and layer system → `01-analyze-navigation-system.md` -- [x] 02 — Fix Discover tab crash → `02-fix-discover-tab-crash.md` -- [x] 03 — Fix My Feeds tab crash → `03-fix-feeds-tab-crash.md` -- [x] 04 — Fix Settings/Sources sub-tab crash → `04-fix-settings-sources-crash.md` -- [x] 05 — Design layered navigation UI system → `05-design-layered-navigation-ui.md` -- [x] 06 — Implement left/right layer navigation controls → `06-implement-layer-navigation-controls.md` -- [x] 07 — Implement enter/escape layer navigation controls → `07-implement-enter-escape-controls.md` -- [x] 08 — Design active layer background color system → `08-design-active-layer-colors.md` -- [x] 09 — Create theme context provider → `09-create-theme-context-provider.md` -- [x] 10 — Implement DesktopTheme type and structure → `10-implement-desktop-theme-types.md` -- [x] 11 — Implement theme resolution system → `11-implement-theme-resolution.md` -- [x] 12 — Create CSS variable token system → `12-create-css-token-system.md` -- [x] 13 — Implement system theme detection → `13-implement-system-theme-detection.md` -- [x] 14 — Integrate theme provider into App component → `14-integrate-theme-provider.md` -- [x] 15 — Update components to use theme tokens → `15-update-components-to-use-themes.md` -- [ ] 16 — Test navigation flows and layer transitions → `16-test-navigation-flows.md` -- [ ] 17 — Test tab crash fixes and edge cases → `17-test-tab-crash-fixes.md` -- [ ] 18 — Test theming system with all modes → `18-test-theming-system.md` - -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 diff --git a/tasks/subtasks/theme-refactoring-01-create-schema.md b/tasks/subtasks/theme-refactoring-01-create-schema.md deleted file mode 100644 index 1415d98..0000000 --- a/tasks/subtasks/theme-refactoring-01-create-schema.md +++ /dev/null @@ -1,63 +0,0 @@ -# 01. Create JSON Theme Schema and File Structure - -meta: - id: theme-refactoring-01 - feature: theme-refactoring-json-format - priority: P0 - depends_on: [] - tags: [implementation, infrastructure] - -objective: -- Create the JSON theme schema and establish the file structure for theme definitions -- Define the theme.json schema that opencode uses -- Create the directory structure for JSON theme files - -deliverables: -- `src/types/theme-schema.ts` - JSON theme schema definition -- `src/themes/*.json` - Directory for theme JSON files -- `src/themes/schema.json` - Theme schema reference - -steps: -- Step 1.1: Create `src/types/theme-schema.ts` with TypeScript interfaces matching the opencode theme JSON structure - - Define `ThemeJson` interface with `$schema`, `defs`, and `theme` properties - - Define `ColorValue` type supporting hex colors, color references, variants, and RGBA - - Define `Variant` type for light/dark mode color definitions - - Export interfaces for type checking - -- Step 1.2: Create `src/themes/schema.json` with the opencode theme schema reference - - Add `$schema: "https://opencode.ai/theme.json"` - - Document the theme structure in comments - -- Step 1.3: Create `src/themes/` directory - - Ensure directory exists for JSON theme files - -- Step 1.4: Create a sample theme file in `src/themes/opencode.json` - - Use the opencode theme as reference - - Include proper `$schema` reference - - Define `defs` with all color references - - Define `theme` with semantic color mappings - -tests: -- Unit: - - Test `ThemeJson` type definition matches opencode structure - - Test `ColorValue` type accepts hex colors, references, variants, and RGBA - - Test `Variant` type structure is correct - -- Integration/e2e: - - Verify JSON file can be parsed without errors - - Validate schema reference is correct - -acceptance_criteria: -- `src/types/theme-schema.ts` file exists with all required interfaces -- `src/themes/schema.json` contains valid schema reference -- `src/themes/` directory is created -- `src/themes/opencode.json` can be imported and parsed successfully - -validation: -- Run: `bun run typecheck` - Should pass with no type errors -- Run: `cat src/themes/opencode.json | jq .` - Should be valid JSON - -notes: -- Follow opencode's theme.json structure exactly -- Use TypeScript interfaces to ensure type safety -- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme/` diff --git a/tasks/subtasks/theme-refactoring-02-convert-themes.md b/tasks/subtasks/theme-refactoring-02-convert-themes.md deleted file mode 100644 index 2091324..0000000 --- a/tasks/subtasks/theme-refactoring-02-convert-themes.md +++ /dev/null @@ -1,77 +0,0 @@ -# 02. Convert Existing Themes to JSON Format - -meta: - id: theme-refactoring-02 - feature: theme-refactoring-json-format - priority: P0 - depends_on: [theme-refactoring-01] - tags: [implementation, migration] - -objective: -- Convert all existing PodTui themes (catppuccin, gruvbox, tokyo, nord) from TypeScript constants to JSON format -- Ensure each theme matches the opencode JSON structure -- Maintain color fidelity with original theme definitions - -deliverables: -- `src/themes/catppuccin.json` - Catppuccin theme in JSON format -- `src/themes/gruvbox.json` - Gruvbox theme in JSON format -- `src/themes/tokyo.json` - Tokyo Night theme in JSON format -- `src/themes/nord.json` - Nord theme in JSON format - -steps: -- Step 2.1: Convert `catppuccin` theme from `src/types/desktop-theme.ts` to JSON - - Extract all color definitions from `THEMES_DESKTOP.variants.find((v) => v.name === "catppuccin")!.colors` - - Create `defs` object with all color references (dark and light variants) - - Create `theme` object with semantic mappings - - Add `$schema` reference - - Verify color values match original definitions - -- Step 2.2: Convert `gruvbox` theme from `src/types/desktop-theme.ts` to JSON - - Extract all color definitions from `THEMES_DESKTOP.variants.find((v) => v.name === "gruvbox")!.colors` - - Create `defs` object with all color references - - Create `theme` object with semantic mappings - - Add `$schema` reference - -- Step 2.3: Convert `tokyo` theme from `src/types/desktop-theme.ts` to JSON - - Extract all color definitions from `THEMES_DESKTOP.variants.find((v) => v.name === "tokyo")!.colors` - - Create `defs` object with all color references - - Create `theme` object with semantic mappings - - Add `$schema` reference - -- Step 2.4: Convert `nord` theme from `src/types/desktop-theme.ts` to JSON - - Extract all color definitions from `THEMES_DESKTOP.variants.find((v) => v.name === "nord")!.colors` - - Create `defs` object with all color references - - Create `theme` object with semantic mappings - - Add `$schema` reference - -- Step 2.5: Verify all JSON files are valid - - Check syntax with `bun run typecheck` - - Ensure all imports work correctly - -tests: -- Unit: - - Test each JSON file can be imported without errors - - Verify color values match original TypeScript definitions - -- Integration/e2e: - - Load each theme and verify all colors are present - - Check that theme structure matches expected schema - -acceptance_criteria: -- All four theme JSON files exist in `src/themes/` -- Each JSON file follows the theme schema -- Color values match original theme definitions exactly -- All JSON files are valid and can be parsed - -validation: -- Run: `bun run typecheck` - Should pass -- Run: `cat src/themes/catppuccin.json | jq .` - Should be valid JSON -- Run: `cat src/themes/gruvbox.json | jq .` - Should be valid JSON -- Run: `cat src/themes/tokyo.json | jq .` - Should be valid JSON -- Run: `cat src/themes/nord.json | jq .` - Should be valid JSON - -notes: -- Use opencode's theme structure as reference -- Maintain backward compatibility with existing color definitions -- Ensure both dark and light variants are included if available -- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme/` diff --git a/tasks/subtasks/theme-refactoring-03-update-types.md b/tasks/subtasks/theme-refactoring-03-update-types.md deleted file mode 100644 index 1a8b944..0000000 --- a/tasks/subtasks/theme-refactoring-03-update-types.md +++ /dev/null @@ -1,70 +0,0 @@ -# 03. Update Type Definitions for Color References and Variants - -meta: - id: theme-refactoring-03 - feature: theme-refactoring-json-format - priority: P0 - depends_on: [theme-refactoring-01] - tags: [implementation, types] - -objective: -- Update type definitions to support the new JSON theme structure -- Add support for color references, variants, and light/dark mode -- Maintain backward compatibility with existing code - -deliverables: -- `src/types/theme-schema.ts` - Updated with new types -- `src/types/settings.ts` - Updated with color reference types -- `src/types/desktop-theme.ts` - Updated to support JSON themes - -steps: -- Step 3.1: Update `src/types/theme-schema.ts` - - Export `ThemeJson` interface - - Export `ColorValue` type - - Export `Variant` type - - Add `ThemeColors` type for resolved theme colors - -- Step 3.2: Update `src/types/settings.ts` - - Add `ThemeJson` import - - Add `ColorValue` type definition - - Add `Variant` type definition - - Update `ThemeColors` to support color references - - Add `ThemeJson` type for JSON theme files - -- Step 3.3: Update `src/types/desktop-theme.ts` - - Add imports for `ThemeJson`, `ColorValue`, `Variant` - - Add `ThemeJson` type for JSON theme files - - Update existing types to support color references - - Add helper functions for JSON theme loading - -- Step 3.4: Ensure backward compatibility - - Keep existing `ThemeColors` structure for resolved themes - - Ensure existing code can still use theme colors as strings - - Add type guards for color references - -tests: -- Unit: - - Test `ThemeJson` type accepts valid JSON theme structure - - Test `ColorValue` type accepts hex colors, references, variants, and RGBA - - Test `Variant` type structure is correct - - Test existing `ThemeColors` type remains compatible - -- Integration/e2e: - - Verify type imports work correctly - - Test type inference with JSON theme files - -acceptance_criteria: -- All type definitions are updated and exported -- Backward compatibility maintained with existing code -- New types support color references and variants -- Type checking passes without errors - -validation: -- Run: `bun run typecheck` - Should pass with no errors -- Verify existing components can still use theme colors -- Test type inference with new theme JSON files - -notes: -- Use TypeScript's `with { type: "json" }` for JSON imports -- Ensure all types are properly exported for use across the codebase -- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx` diff --git a/tasks/subtasks/theme-refactoring-04-theme-resolution.md b/tasks/subtasks/theme-refactoring-04-theme-resolution.md deleted file mode 100644 index e0afc8a..0000000 --- a/tasks/subtasks/theme-refactoring-04-theme-resolution.md +++ /dev/null @@ -1,83 +0,0 @@ -# 04. Create Theme Resolution Logic with Color Reference Lookup - -meta: - id: theme-refactoring-04 - feature: theme-refactoring-json-format - priority: P0 - depends_on: [theme-refactoring-02, theme-refactoring-03] - tags: [implementation, logic] - -objective: -- Implement theme resolution logic that handles color references and variants -- Create function to resolve colors from theme JSON files -- Support hex colors, color references, ANSI codes, and RGBA values -- Handle light/dark mode selection based on current mode - -deliverables: -- `src/utils/theme-resolver.ts` - Theme resolution utility -- `src/utils/ansi-to-rgba.ts` - ANSI color conversion utility - -steps: -- Step 4.1: Create `src/utils/ansi-to-rgba.ts` - - Implement `ansiToRgba(code: number): RGBA` function - - Handle standard ANSI colors (0-15) - - Handle 6x6x6 color cube (16-231) - - Handle grayscale ramp (232-255) - - Add type definitions for RGBA - -- Step 4.2: Create `src/utils/theme-resolver.ts` - - Implement `resolveTheme(theme: ThemeJson, mode: "dark" | "light"): ThemeColors` - - Create `resolveColor(c: ColorValue): RGBA` helper function - - Handle RGBA objects directly - - Handle hex color strings - - Handle color references from `defs` - - Handle variants (dark/light mode) - - Handle ANSI codes - - Add error handling for invalid color references - -- Step 4.3: Implement color reference resolution - - Create lookup logic for `defs` object - - Add fallback to theme colors if reference not in defs - - Throw descriptive errors for invalid references - -- Step 4.4: Handle optional theme properties - - Support `selectedListItemText` property - - Support `backgroundMenu` property - - Support `thinkingOpacity` property - - Add default values for missing properties - -tests: -- Unit: - - Test `ansiToRgba` with ANSI codes 0-15 - - Test `ansiToRgba` with color cube codes 16-231 - - Test `ansiToRgba` with grayscale codes 232-255 - - Test `resolveColor` with hex colors - - Test `resolveColor` with color references - - Test `resolveColor` with light/dark variants - - Test `resolveColor` with RGBA objects - - Test `resolveTheme` with complete theme JSON - - Test `resolveTheme` with missing optional properties - - Test error handling for invalid color references - -- Integration/e2e: - - Test resolving colors from actual theme JSON files - - Verify light/dark mode selection works correctly - -acceptance_criteria: -- `src/utils/ansi-to-rgba.ts` file exists with conversion functions -- `src/utils/theme-resolver.ts` file exists with resolution logic -- All color value types are handled correctly -- Error messages are descriptive and helpful -- Theme resolution works with both light and dark modes - -validation: -- Run: `bun run typecheck` - Should pass -- Run unit tests with `bun test src/utils/theme-resolver.test.ts` -- Test with sample theme JSON files -- Verify error messages are clear - -notes: -- Use opencode's implementation as reference for color resolution -- Ensure RGBA values are normalized to 0-1 range -- Add comprehensive error handling for invalid inputs -- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx` (lines 176-277) diff --git a/tasks/subtasks/theme-refactoring-05-theme-context.md b/tasks/subtasks/theme-refactoring-05-theme-context.md deleted file mode 100644 index 50199ad..0000000 --- a/tasks/subtasks/theme-refactoring-05-theme-context.md +++ /dev/null @@ -1,80 +0,0 @@ -# 05. Migrate ThemeContext to SolidJS Pattern - -meta: - id: theme-refactoring-05 - feature: theme-refactoring-json-format - priority: P0 - depends_on: [theme-refactoring-04] - tags: [implementation, context] - -objective: -- Migrate ThemeContext from basic React context to SolidJS context pattern -- Implement reactive state management for theme switching -- Add persistence for theme selection and mode -- Support system theme detection - -deliverables: -- `src/context/ThemeContext.tsx` - Updated with SolidJS pattern -- `src/utils/theme-context.ts` - Theme context utilities - -steps: -- Step 5.1: Update `src/context/ThemeContext.tsx` - - Import SolidJS hooks: `createSignal`, `createEffect`, `createMemo`, `createStore` - - Replace React context with SolidJS `createSimpleContext` pattern (from opencode) - - Add theme loading logic - - Implement reactive theme state - - Add system theme detection - - Support theme persistence via localStorage - -- Step 5.2: Implement theme state management - - Create store with `themes`, `mode`, `active`, `ready` - - Initialize with default theme (opencode) - - Load custom themes from JSON files - - Handle system theme detection - -- Step 5.3: Add reactive theme resolution - - Create `createMemo` for resolved theme colors - - Create `createMemo` for syntax highlighting - - Create `createMemo` for subtle syntax - -- Step 5.4: Implement theme switching - - Add `setTheme(theme: string)` method - - Add `setMode(mode: "dark" | "light")` method - - Persist theme and mode to localStorage - -- Step 5.5: Add system theme detection - - Detect terminal background and foreground colors - - Generate system theme based on terminal palette - - Handle system theme preference changes - -tests: -- Unit: - - Test theme state initialization - - Test theme switching logic - - Test mode switching logic - - Test system theme detection - - Test localStorage persistence - -- Integration/e2e: - - Test theme context in component tree - - Test reactive theme updates - - Test system theme changes - -acceptance_criteria: -- `src/context/ThemeContext.tsx` uses SolidJS pattern -- Theme context provides reactive theme state -- Theme switching works correctly -- System theme detection is functional -- Theme and mode are persisted to localStorage - -validation: -- Run: `bun run typecheck` - Should pass -- Run: `bun test src/context/ThemeContext.test.ts` -- Test theme switching in application -- Test system theme detection - -notes: -- Use opencode's ThemeProvider pattern as reference -- Follow SolidJS best practices for reactive state -- Ensure proper cleanup of effects and listeners -- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx` (lines 279-392) diff --git a/tasks/subtasks/theme-refactoring-06-theme-loader.md b/tasks/subtasks/theme-refactoring-06-theme-loader.md deleted file mode 100644 index 94185fa..0000000 --- a/tasks/subtasks/theme-refactoring-06-theme-loader.md +++ /dev/null @@ -1,79 +0,0 @@ -# 06. Add Theme Loader for JSON Files and Custom Themes - -meta: - id: theme-refactoring-06 - feature: theme-refactoring-json-format - priority: P1 - depends_on: [theme-refactoring-02, theme-refactoring-04] - tags: [implementation, loading] - -objective: -- Create theme loader to load JSON theme files -- Support loading custom themes from multiple directories -- Provide API for theme discovery and loading -- Handle theme file validation - -deliverables: -- `src/utils/theme-loader.ts` - Theme loader utilities -- `src/utils/custom-themes.ts` - Custom theme loading logic - -steps: -- Step 6.1: Create `src/utils/theme-loader.ts` - - Implement `loadTheme(name: string): Promise` - - Implement `loadThemeFromPath(path: string): Promise` - - Implement `getAllThemes(): Promise>` - - Add error handling for missing or invalid theme files - -- Step 6.2: Create `src/utils/custom-themes.ts` - - Implement `getCustomThemes()` function - - Scan for theme files in multiple directories: - - `~/.config/podtui/themes/` - - `./.podtui/themes/` - - Project root `./themes/` - - Support custom theme files with `.json` extension - - Return merged theme registry - -- Step 6.3: Add theme file validation - - Validate theme JSON structure - - Check required properties (`defs`, `theme`) - - Validate color references in `defs` - - Add warning for optional properties - -- Step 6.4: Implement theme discovery - - List all available theme files - - Provide theme metadata (name, description) - - Support theme aliases (e.g., "catppuccin" -> "catppuccin.json") - -tests: -- Unit: - - Test `loadTheme` with existing theme files - - Test `loadTheme` with missing theme files - - Test `loadThemeFromPath` with custom paths - - Test `getAllThemes` returns all available themes - - Test `getCustomThemes` scans multiple directories - - Test theme file validation - -- Integration/e2e: - - Test loading all available themes - - Test custom theme loading from directories - - Verify theme discovery works correctly - -acceptance_criteria: -- `src/utils/theme-loader.ts` file exists with loading functions -- `src/utils/custom-themes.ts` file exists with custom theme logic -- Custom themes can be loaded from multiple directories -- Theme validation prevents invalid files -- Theme discovery API is functional - -validation: -- Run: `bun run typecheck` - Should pass -- Run: `bun test src/utils/theme-loader.test.ts` -- Run: `bun test src/utils/custom-themes.test.ts` -- Test loading all available themes manually -- Test custom theme loading from directories - -notes: -- Use opencode's `getCustomThemes` pattern as reference -- Support both local and global theme directories -- Add comprehensive error messages for invalid theme files -- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx` (lines 394-419) diff --git a/tasks/subtasks/theme-refactoring-07-system-theme.md b/tasks/subtasks/theme-refactoring-07-system-theme.md deleted file mode 100644 index 25a7912..0000000 --- a/tasks/subtasks/theme-refactoring-07-system-theme.md +++ /dev/null @@ -1,83 +0,0 @@ -# 07. Implement System Theme Detection with Terminal Palette - -meta: - id: theme-refactoring-07 - feature: theme-refactoring-json-format - priority: P1 - depends_on: [theme-refactoring-04, theme-refactoring-06] - tags: [implementation, detection] - -objective: -- Implement system theme detection based on terminal palette -- Generate system theme from terminal colors -- Detect light/dark mode based on terminal background -- Handle terminal color palette limitations - -deliverables: -- `src/utils/system-theme.ts` - System theme detection utilities -- `src/utils/color-generation.ts` - Color generation helpers - -steps: -- Step 7.1: Create `src/utils/system-theme.ts` - - Implement `detectSystemTheme()` function - - Detect terminal background and foreground colors - - Determine light/dark mode based on luminance - - Return detected theme information - -- Step 7.2: Implement terminal palette detection - - Detect terminal colors using `@opentui/core` renderer - - Get default background and foreground colors - - Extract color palette (16 standard colors) - - Handle missing or invalid palette data - -- Step 7.3: Create `src/utils/color-generation.ts` - - Implement `generateGrayScale(bg: RGBA, isDark: boolean): Record` - - Implement `generateMutedTextColor(bg: RGBA, isDark: boolean): RGBA` - - Implement `tint(base: RGBA, overlay: RGBA, alpha: number): RGBA` - - Generate gray scale based on background luminance - - Generate muted text colors for readability - -- Step 7.4: Create system theme JSON generator - - Implement `generateSystemTheme(colors: TerminalColors, mode: "dark" | "light"): ThemeJson` - - Use ANSI color references for primary colors - - Generate appropriate background colors - - Generate diff colors with alpha blending - - Generate markdown and syntax colors - -- Step 7.5: Add system theme caching - - Cache terminal palette detection results - - Handle palette cache invalidation - - Support manual cache clear on SIGUSR2 - -tests: -- Unit: - - Test `detectSystemTheme` returns correct mode - - Test `generateGrayScale` produces correct grays - - Test `generateMutedTextColor` produces readable colors - - Test `tint` produces correct blended colors - - Test `generateSystemTheme` produces valid theme JSON - -- Integration/e2e: - - Test system theme detection in terminal - - Test theme generation with actual terminal palette - - Verify light/dark mode detection is accurate - -acceptance_criteria: -- `src/utils/system-theme.ts` file exists with detection functions -- `src/utils/color-generation.ts` file exists with generation helpers -- System theme detection works correctly -- Light/dark mode detection is accurate -- Theme generation produces valid JSON - -validation: -- Run: `bun run typecheck` - Should pass -- Run: `bun test src/utils/system-theme.test.ts` -- Run: `bun test src/utils/color-generation.test.ts` -- Test system theme detection manually in terminal -- Verify theme colors are readable - -notes: -- Use opencode's system theme detection as reference -- Handle terminal transparency gracefully -- Add fallback for terminals without palette support -- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx` (lines 428-535) diff --git a/tasks/subtasks/theme-refactoring-08-syntax-highlighting.md b/tasks/subtasks/theme-refactoring-08-syntax-highlighting.md deleted file mode 100644 index 01ec116..0000000 --- a/tasks/subtasks/theme-refactoring-08-syntax-highlighting.md +++ /dev/null @@ -1,83 +0,0 @@ -# 08. Add Syntax Highlighting Integration - -meta: - id: theme-refactoring-08 - feature: theme-refactoring-json-format - priority: P1 - depends_on: [theme-refactoring-04, theme-refactoring-05] - tags: [implementation, syntax] - -objective: -- Add syntax highlighting support using theme colors -- Generate syntax rules from theme definitions -- Support markdown syntax highlighting -- Integrate with OpenTUI syntax highlighting - -deliverables: -- `src/utils/syntax-highlighter.ts` - Syntax highlighting utilities -- `src/utils/syntax-rules.ts` - Syntax rule generation - -steps: -- Step 8.1: Create `src/utils/syntax-rules.ts` - - Implement `getSyntaxRules(theme: ThemeColors)` function - - Define syntax scopes and their mappings - - Map theme colors to syntax scopes: - - Default text - - Keywords (return, conditional, repeat, coroutine) - - Types (function, class, module) - - Variables (parameter, member, builtin) - - Strings, numbers, booleans - - Comments - - Operators and punctuation - - Markdown-specific scopes (headings, bold, italic, links) - - Diff scopes (added, removed, context) - - Add style properties (foreground, bold, italic, underline) - -- Step 8.2: Create `src/utils/syntax-highlighter.ts` - - Implement `generateSyntax(theme: ThemeColors)` function - - Implement `generateSubtleSyntax(theme: ThemeColors)` function - - Apply opacity to syntax colors for subtle highlighting - - Use theme's `thinkingOpacity` property - -- Step 8.3: Integrate with OpenTUI syntax highlighting - - Import `SyntaxStyle` from `@opentui/core` - - Use `SyntaxStyle.fromTheme()` to create syntax styles - - Apply syntax styles to code components - -- Step 8.4: Add syntax scope mappings - - Map common programming language scopes - - Map markdown and markup scopes - - Map diff and git scopes - - Add scope aliases for common patterns - -tests: -- Unit: - - Test `getSyntaxRules` generates correct rules - - Test `generateSyntax` creates valid syntax styles - - Test `generateSubtleSyntax` applies opacity correctly - - Test syntax rules cover all expected scopes - -- Integration/e2e: - - Test syntax highlighting with different themes - - Verify syntax colors match theme definitions - - Test markdown highlighting - -acceptance_criteria: -- `src/utils/syntax-rules.ts` file exists with rule generation -- `src/utils/syntax-highlighter.ts` file exists with style generation -- Syntax highlighting works with theme colors -- Markdown highlighting is supported -- Syntax rules cover common programming patterns - -validation: -- Run: `bun run typecheck` - Should pass -- Run: `bun test src/utils/syntax-rules.test.ts` -- Run: `bun test src/utils/syntax-highlighter.test.ts` -- Test syntax highlighting in application -- Verify syntax colors are readable - -notes: -- Use opencode's syntax rule generation as reference -- Include comprehensive scope mappings -- Support common programming languages -- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx` (lines 622-1152) diff --git a/tasks/subtasks/theme-refactoring-09-theme-utils.md b/tasks/subtasks/theme-refactoring-09-theme-utils.md deleted file mode 100644 index affde61..0000000 --- a/tasks/subtasks/theme-refactoring-09-theme-utils.md +++ /dev/null @@ -1,77 +0,0 @@ -# 09. Update Theme Utilities and CSS Variable Application - -meta: - id: theme-refactoring-09 - feature: theme-refactoring-json-format - priority: P1 - depends_on: [theme-refactoring-04] - tags: [implementation, utilities] - -objective: -- Update existing theme utilities to work with JSON theme structure -- Refactor CSS variable application logic -- Add support for theme color references -- Ensure backward compatibility with existing components - -deliverables: -- `src/utils/theme.ts` - Updated theme utilities -- `src/utils/theme-css.ts` - CSS variable application utilities - -steps: -- Step 9.1: Update `src/utils/theme.ts` - - Refactor `applyTheme()` to accept `ThemeColors` type - - Keep existing function signature for backward compatibility - - Update to work with resolved theme colors - -- Step 9.2: Create `src/utils/theme-css.ts` - - Implement `applyThemeToCSS(theme: ThemeColors)` function - - Apply theme colors as CSS custom properties - - Support all theme color properties - - Handle layer backgrounds if present - -- Step 9.3: Update theme attribute handling - - Implement `setThemeAttribute(themeName: string)` function - - Update to use data-theme attribute - - Support system theme attribute - -- Step 9.4: Add color reference support - - Implement `resolveColorReference(color: string): string` function - - Convert color references to CSS values - - Handle hex colors, color references, and RGBA - -- Step 9.5: Add theme utility functions - - Implement `getThemeByName(name: string): ThemeJson | undefined` - - Implement `getDefaultTheme(): ThemeJson` - - Implement `getAllThemes(): ThemeJson[]` - -tests: -- Unit: - - Test `applyThemeToCSS` applies all colors correctly - - Test `setThemeAttribute` sets attribute correctly - - Test `resolveColorReference` converts references correctly - - Test theme utility functions return correct results - -- Integration/e2e: - - Test theme application in browser - - Verify CSS variables are updated correctly - - Test theme attribute changes - -acceptance_criteria: -- `src/utils/theme.ts` is updated and backward compatible -- `src/utils/theme-css.ts` file exists with CSS utilities -- CSS variables are applied correctly -- Color references are resolved properly -- Theme utilities are functional - -validation: -- Run: `bun run typecheck` - Should pass -- Run: `bun test src/utils/theme.test.ts` -- Run: `bun test src/utils/theme-css.test.ts` -- Test theme application in browser -- Verify CSS variables are applied correctly - -notes: -- Maintain backward compatibility with existing code -- Use CSS custom properties for theming -- Ensure all theme colors are applied -- Reference: `/home/mike/code/PodTui/src/utils/theme.ts` (original file) diff --git a/tasks/subtasks/theme-refactoring-10-theme-switching.md b/tasks/subtasks/theme-refactoring-10-theme-switching.md deleted file mode 100644 index 3135e38..0000000 --- a/tasks/subtasks/theme-refactoring-10-theme-switching.md +++ /dev/null @@ -1,80 +0,0 @@ -# 10. Test Theme Switching and Light/Dark Mode - -meta: - id: theme-refactoring-10 - feature: theme-refactoring-json-format - priority: P0 - depends_on: [theme-refactoring-05, theme-refactoring-09] - tags: [testing, verification] - -objective: -- Comprehensive testing of theme switching functionality -- Verify light/dark mode switching works correctly -- Test all theme JSON files load and apply correctly -- Ensure theme persistence works across sessions - -deliverables: -- `src/utils/theme.test.ts` - Theme utility tests -- `src/context/ThemeContext.test.ts` - Context tests -- Test results showing all themes work correctly - -steps: -- Step 10.1: Create `src/utils/theme.test.ts` - - Test theme loading functions - - Test theme resolution logic - - Test color reference resolution - - Test ANSI color conversion - -- Step 10.2: Create `src/context/ThemeContext.test.ts` - - Test theme context initialization - - Test theme switching - - Test mode switching - - Test system theme detection - - Test localStorage persistence - - Test reactive theme updates - -- Step 10.3: Manual testing - - Test switching between all themes (catppuccin, gruvbox, tokyo, nord) - - Test light/dark mode switching - - Test system theme detection - - Test theme persistence (close and reopen app) - - Test custom theme loading - -- Step 10.4: Visual verification - - Verify all theme colors are correct - - Check readability of text colors - - Verify background colors are appropriate - - Check that all UI elements use theme colors - -tests: -- Unit: - - Run all theme utility tests - - Run all theme context tests - - Verify all tests pass - -- Integration/e2e: - - Test theme switching in application - - Test light/dark mode switching - - Test theme persistence - - Test system theme detection - -acceptance_criteria: -- All unit tests pass -- All integration tests pass -- Theme switching works correctly -- Light/dark mode switching works correctly -- All themes load and apply correctly -- Theme persistence works across sessions - -validation: -- Run: `bun test src/utils/theme.test.ts` -- Run: `bun test src/context/ThemeContext.test.ts` -- Run: `bun test` - Run all tests -- Manual testing of all themes -- Visual verification of theme appearance - -notes: -- Test with actual terminal to verify system theme detection -- Verify all theme colors are visually appealing -- Check for any color contrast issues -- Test edge cases (missing themes, invalid colors) diff --git a/tasks/subtasks/theme-refactoring-11-custom-themes.md b/tasks/subtasks/theme-refactoring-11-custom-themes.md deleted file mode 100644 index f7e6825..0000000 --- a/tasks/subtasks/theme-refactoring-11-custom-themes.md +++ /dev/null @@ -1,77 +0,0 @@ -# 11. Verify Custom Theme Loading and Persistence - -meta: - id: theme-refactoring-11 - feature: theme-refactoring-json-format - priority: P1 - depends_on: [theme-refactoring-06, theme-refactoring-10] - tags: [testing, verification] - -objective: -- Test custom theme loading from directories -- Verify theme persistence works correctly -- Test custom theme switching -- Ensure custom themes are loaded on app start - -deliverables: -- Test results for custom theme loading -- Documentation for custom theme format -- Verification that custom themes work correctly - -steps: -- Step 11.1: Create test theme files - - Create test theme in `~/.config/podtui/themes/` - - Create test theme in `./.podtui/themes/` - - Create test theme in `./themes/` - -- Step 11.2: Test custom theme loading - - Start application and verify custom themes are loaded - - Switch to custom theme - - Verify custom theme applies correctly - -- Step 11.3: Test theme persistence - - Set custom theme - - Close application - - Reopen application - - Verify custom theme is still selected - -- Step 11.4: Test theme discovery - - List all available themes - - Verify custom themes appear in list - - Test switching to custom themes - -- Step 11.5: Test invalid theme handling - - Create invalid theme JSON - - Verify error is handled gracefully - - Verify app doesn't crash - -tests: -- Unit: - - Test custom theme loading functions - - Test theme discovery - - Test invalid theme handling - -- Integration/e2e: - - Test custom theme loading from directories - - Test theme persistence - - Test theme discovery - - Test invalid theme handling - -acceptance_criteria: -- Custom themes can be loaded from directories -- Custom themes persist across sessions -- Custom themes appear in theme list -- Invalid themes are handled gracefully -- Theme discovery works correctly - -validation: -- Run: `bun test src/utils/custom-themes.test.ts` -- Run: `bun test` - Run all tests -- Manual testing of custom themes -- Verify themes persist after restart - -notes: -- Create documentation for custom theme format -- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx` (lines 394-419) -- Test with multiple custom themes -- Verify all custom themes work correctly