getting terminal colors working
This commit is contained in:
17
src/App.tsx
17
src/App.tsx
@@ -1,4 +1,5 @@
|
|||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
|
import { useRenderer } from "@opentui/solid";
|
||||||
import { Layout } from "./components/Layout";
|
import { Layout } from "./components/Layout";
|
||||||
import { Navigation } from "./components/Navigation";
|
import { Navigation } from "./components/Navigation";
|
||||||
import { TabNavigation } from "./components/TabNavigation";
|
import { TabNavigation } from "./components/TabNavigation";
|
||||||
@@ -32,31 +33,31 @@ export function App() {
|
|||||||
// Centralized keyboard handler for all tab navigation and shortcuts
|
// Centralized keyboard handler for all tab navigation and shortcuts
|
||||||
useAppKeyboard({
|
useAppKeyboard({
|
||||||
get activeTab() {
|
get activeTab() {
|
||||||
return activeTab()
|
return activeTab();
|
||||||
},
|
},
|
||||||
onTabChange: setActiveTab,
|
onTabChange: setActiveTab,
|
||||||
inputFocused: inputFocused(),
|
inputFocused: inputFocused(),
|
||||||
navigationEnabled: layerDepth() === 0,
|
navigationEnabled: layerDepth() === 0,
|
||||||
layerDepth,
|
layerDepth,
|
||||||
onLayerChange: (newDepth) => {
|
onLayerChange: (newDepth) => {
|
||||||
setLayerDepth(newDepth)
|
setLayerDepth(newDepth);
|
||||||
},
|
},
|
||||||
onAction: (action) => {
|
onAction: (action) => {
|
||||||
if (action === "escape") {
|
if (action === "escape") {
|
||||||
if (layerDepth() > 0) {
|
if (layerDepth() > 0) {
|
||||||
setLayerDepth(0)
|
setLayerDepth(0);
|
||||||
setInputFocused(false)
|
setInputFocused(false);
|
||||||
} else {
|
} else {
|
||||||
setShowAuthPanel(false)
|
setShowAuthPanel(false);
|
||||||
setInputFocused(false)
|
setInputFocused(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "enter" && layerDepth() === 0) {
|
if (action === "enter" && layerDepth() === 0) {
|
||||||
setLayerDepth(1)
|
setLayerDepth(1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
const tab = activeTab();
|
const tab = activeTab();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { JSX } from "solid-js"
|
import type { JSX } from "solid-js"
|
||||||
import type { RGBA } from "@opentui/core"
|
import type { RGBA } from "@opentui/core"
|
||||||
|
import { Show, createMemo } from "solid-js"
|
||||||
import { useTheme } from "../context/ThemeContext"
|
import { useTheme } from "../context/ThemeContext"
|
||||||
import { LayerIndicator } from "./LayerIndicator"
|
import { LayerIndicator } from "./LayerIndicator"
|
||||||
|
|
||||||
@@ -16,52 +17,50 @@ type LayoutProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Layout(props: LayoutProps) {
|
export function Layout(props: LayoutProps) {
|
||||||
const { theme } = useTheme()
|
const context = useTheme()
|
||||||
|
|
||||||
// Get layer configuration based on depth
|
// Get layer configuration based on depth - wrapped in createMemo for reactivity
|
||||||
const getLayerConfig = (depth: number): LayerConfig => {
|
const currentLayer = createMemo((): LayerConfig => {
|
||||||
const backgrounds = theme.layerBackgrounds
|
const depth = props.layerDepth || 0
|
||||||
|
const backgrounds = context.theme.layerBackgrounds
|
||||||
const depthMap: Record<number, LayerConfig> = {
|
const depthMap: Record<number, LayerConfig> = {
|
||||||
0: { depth: 0, background: backgrounds?.layer0 ?? theme.background },
|
0: { depth: 0, background: backgrounds?.layer0 ?? context.theme.background },
|
||||||
1: { depth: 1, background: backgrounds?.layer1 ?? theme.backgroundPanel },
|
1: { depth: 1, background: backgrounds?.layer1 ?? context.theme.backgroundPanel },
|
||||||
2: { depth: 2, background: backgrounds?.layer2 ?? theme.backgroundElement },
|
2: { depth: 2, background: backgrounds?.layer2 ?? context.theme.backgroundElement },
|
||||||
3: { depth: 3, background: backgrounds?.layer3 ?? theme.backgroundMenu },
|
3: { depth: 3, background: backgrounds?.layer3 ?? context.theme.backgroundMenu },
|
||||||
}
|
}
|
||||||
|
|
||||||
return depthMap[depth] || { depth: 0, background: theme.background }
|
return depthMap[depth] || { depth: 0, background: context.theme.background }
|
||||||
}
|
})
|
||||||
|
|
||||||
// Get current layer background
|
|
||||||
const currentLayer = getLayerConfig(props.layerDepth || 0)
|
|
||||||
|
|
||||||
|
// Note: No need for a ready check here - the ThemeProvider uses
|
||||||
|
// createSimpleContext which gates children rendering until ready
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
backgroundColor={theme.background}
|
backgroundColor={context.theme.background}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
{props.header ? (
|
<Show when={props.header} fallback={<box style={{ height: 4 }} />}>
|
||||||
<box
|
<box
|
||||||
style={{
|
style={{
|
||||||
height: 4,
|
height: 4,
|
||||||
backgroundColor: theme.surface ?? theme.backgroundPanel,
|
backgroundColor: context.theme.surface ?? context.theme.backgroundPanel,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<box style={{ padding: 1 }}>
|
<box style={{ padding: 1 }}>
|
||||||
{props.header}
|
{props.header}
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
) : (
|
</Show>
|
||||||
<box style={{ height: 4 }} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Main content area with layer background */}
|
{/* Main content area with layer background */}
|
||||||
<box
|
<box
|
||||||
style={{
|
style={{
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
backgroundColor: currentLayer.background,
|
backgroundColor: currentLayer().background,
|
||||||
paddingLeft: 2,
|
paddingLeft: 2,
|
||||||
paddingRight: 2,
|
paddingRight: 2,
|
||||||
}}
|
}}
|
||||||
@@ -72,34 +71,32 @@ export function Layout(props: LayoutProps) {
|
|||||||
</box>
|
</box>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
{props.footer ? (
|
<Show when={props.footer} fallback={<box style={{ height: 2 }} />}>
|
||||||
<box
|
<box
|
||||||
style={{
|
style={{
|
||||||
height: 2,
|
height: 2,
|
||||||
backgroundColor: theme.surface ?? theme.backgroundPanel,
|
backgroundColor: context.theme.surface ?? context.theme.backgroundPanel,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<box style={{ padding: 1 }}>
|
<box style={{ padding: 1 }}>
|
||||||
{props.footer}
|
{props.footer}
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
) : (
|
</Show>
|
||||||
<box style={{ height: 2 }} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Layer indicator */}
|
{/* Layer indicator */}
|
||||||
{props.layerDepth !== undefined && (
|
<Show when={props.layerDepth !== undefined}>
|
||||||
<box
|
<box
|
||||||
style={{
|
style={{
|
||||||
height: 1,
|
height: 1,
|
||||||
backgroundColor: theme.surface ?? theme.backgroundPanel,
|
backgroundColor: context.theme.surface ?? context.theme.backgroundPanel,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<box style={{ padding: 1 }}>
|
<box style={{ padding: 1 }}>
|
||||||
<LayerIndicator layerDepth={props.layerDepth} />
|
<LayerIndicator layerDepth={props.layerDepth as number} />
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
)}
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
129
src/context/KeybindContext.tsx
Normal file
129
src/context/KeybindContext.tsx
Normal file
@@ -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<KeybindsConfig> }) => {
|
||||||
|
// Merge default keybinds with custom keybinds
|
||||||
|
const customKeybinds = props.keybinds ?? {}
|
||||||
|
const mergedKeybinds = { ...DEFAULT_KEYBINDS, ...customKeybinds }
|
||||||
|
|
||||||
|
const keybinds = createMemo(() => {
|
||||||
|
const result: Record<string, Keybind.Info[]> = {}
|
||||||
|
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("<leader>", Keybind.toString(leaderKey))
|
||||||
|
}
|
||||||
|
return display
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -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 { createStore, produce } from "solid-js/store"
|
||||||
import { useRenderer } from "@opentui/solid"
|
import { useRenderer } from "@opentui/solid"
|
||||||
import type { ThemeName } from "../types/settings"
|
import type { ThemeName } from "../types/settings"
|
||||||
import type { ThemeJson } from "../types/theme-schema"
|
import type { ThemeJson } from "../types/theme-schema"
|
||||||
import { useAppStore } from "../stores/app"
|
import { useAppStore } from "../stores/app"
|
||||||
import { THEME_JSON } from "../constants/themes"
|
import { THEME_JSON } from "../constants/themes"
|
||||||
import { resolveTheme } from "../utils/theme-resolver"
|
|
||||||
import { generateSyntax, generateSubtleSyntax } from "../utils/syntax-highlighter"
|
import { generateSyntax, generateSubtleSyntax } from "../utils/syntax-highlighter"
|
||||||
import { resolveTerminalTheme, loadThemes } from "../utils/theme"
|
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"
|
import type { RGBA, TerminalColors } from "@opentui/core"
|
||||||
|
|
||||||
type ThemeResolved = {
|
type ThemeResolved = {
|
||||||
@@ -75,32 +76,31 @@ type ThemeResolved = {
|
|||||||
thinkingOpacity?: number
|
thinkingOpacity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThemeContextValue = {
|
/**
|
||||||
theme: ThemeResolved
|
* Theme context using the createSimpleContext pattern.
|
||||||
selected: () => string
|
*
|
||||||
all: () => Record<string, ThemeJson>
|
* This ensures children are NOT rendered until the theme is ready,
|
||||||
syntax: () => unknown
|
* preventing "useTheme must be used within a ThemeProvider" errors.
|
||||||
subtleSyntax: () => unknown
|
*
|
||||||
mode: () => "dark" | "light"
|
* The key insight from opencode's implementation is that the provider
|
||||||
setMode: (mode: "dark" | "light") => void
|
* uses `<Show when={ready}>` to gate rendering, so components can
|
||||||
set: (theme: string) => void
|
* safely call useTheme() without checking ready state.
|
||||||
ready: () => boolean
|
*/
|
||||||
}
|
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||||
|
name: "Theme",
|
||||||
const ThemeContext = createContext<ThemeContextValue>()
|
init: (props: { mode: "dark" | "light" }) => {
|
||||||
|
|
||||||
export function ThemeProvider({ children }: { children: any }) {
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
const [ready, setReady] = createSignal(false)
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
themes: {} as Record<string, ThemeJson>,
|
themes: THEME_JSON as Record<string, ThemeJson>,
|
||||||
mode: "dark" as "dark" | "light",
|
mode: props.mode,
|
||||||
active: appStore.state().settings.theme as ThemeName,
|
active: appStore.state().settings.theme as string,
|
||||||
system: undefined as undefined | TerminalColors,
|
system: undefined as undefined | TerminalColors,
|
||||||
|
ready: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const init = () => {
|
function init() {
|
||||||
|
resolveSystemTheme()
|
||||||
loadThemes()
|
loadThemes()
|
||||||
.then((custom) => {
|
.then((custom) => {
|
||||||
setStore(
|
setStore(
|
||||||
@@ -109,25 +109,86 @@ export function ThemeProvider({ children }: { children: any }) {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.finally(() => setReady(true))
|
.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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
init()
|
function resolveSystemTheme() {
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
setStore("active", appStore.state().settings.theme)
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
renderer
|
renderer
|
||||||
.getPalette({ size: 16 })
|
.getPalette({ size: 16 })
|
||||||
.then((colors) => setStore("system", colors))
|
.then((colors) => {
|
||||||
.catch(() => {})
|
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
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(init)
|
||||||
|
|
||||||
|
// Setup SIGUSR2 signal handler for dynamic theme reload
|
||||||
|
// This allows external tools to trigger a theme refresh by sending:
|
||||||
|
// `kill -USR2 <pid>`
|
||||||
|
const cleanupSignalHandler = setupThemeSignalHandler(() => {
|
||||||
|
renderer.clearPaletteCache()
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
onCleanup(cleanupSignalHandler)
|
||||||
|
|
||||||
|
// Sync active theme with app store settings
|
||||||
|
createEffect(() => {
|
||||||
|
const theme = appStore.state().settings.theme
|
||||||
|
if (theme) setStore("active", theme)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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 values = createMemo(() => {
|
||||||
const themes = Object.keys(store.themes).length ? store.themes : THEME_JSON
|
return resolveTerminalTheme(store.themes, store.active, store.mode, store.system)
|
||||||
return resolveTerminalTheme(themes, store.active, store.mode, store.system)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const syntax = createMemo(() => generateSyntax(values() as unknown as Record<string, RGBA>))
|
const syntax = createMemo(() => generateSyntax(values() as unknown as Record<string, RGBA>))
|
||||||
@@ -135,33 +196,34 @@ export function ThemeProvider({ children }: { children: any }) {
|
|||||||
generateSubtleSyntax(values() as unknown as Record<string, RGBA> & { thinkingOpacity?: number })
|
generateSubtleSyntax(values() as unknown as Record<string, RGBA> & { thinkingOpacity?: number })
|
||||||
)
|
)
|
||||||
|
|
||||||
const context: ThemeContextValue = {
|
return {
|
||||||
theme: new Proxy(values(), {
|
theme: new Proxy(values(), {
|
||||||
get(_target, prop) {
|
get(_target, prop) {
|
||||||
return values()[prop as keyof typeof values]
|
// @ts-expect-error - dynamic property access
|
||||||
|
return values()[prop]
|
||||||
},
|
},
|
||||||
}) as ThemeResolved,
|
}) as ThemeResolved,
|
||||||
selected: () => store.active,
|
get selected() {
|
||||||
all: () => store.themes,
|
return store.active
|
||||||
|
},
|
||||||
|
all() {
|
||||||
|
return store.themes
|
||||||
|
},
|
||||||
syntax,
|
syntax,
|
||||||
subtleSyntax,
|
subtleSyntax,
|
||||||
mode: () => store.mode,
|
mode() {
|
||||||
setMode: (mode) => setStore("mode", mode),
|
return store.mode
|
||||||
set: (theme) => appStore.setTheme(theme as ThemeName),
|
},
|
||||||
ready,
|
setMode(mode: "dark" | "light") {
|
||||||
|
setStore("mode", mode)
|
||||||
|
emitThemeModeChanged(mode)
|
||||||
|
},
|
||||||
|
set(theme: string) {
|
||||||
|
appStore.setTheme(theme as ThemeName)
|
||||||
|
},
|
||||||
|
get ready() {
|
||||||
|
return store.ready
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
},
|
||||||
return (
|
})
|
||||||
<Show when={ready()}>
|
|
||||||
<ThemeContext.Provider value={context}>{children}</ThemeContext.Provider>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTheme() {
|
|
||||||
const context = useContext(ThemeContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useTheme must be used within a ThemeProvider")
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|||||||
53
src/context/helper.tsx
Normal file
53
src/context/helper.tsx
Normal file
@@ -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<T, Props extends Record<string, any>>(input: {
|
||||||
|
name: string
|
||||||
|
init: ((input: Props) => T) | (() => T)
|
||||||
|
}) {
|
||||||
|
const ctx = createContext<T>()
|
||||||
|
|
||||||
|
return {
|
||||||
|
provider: (props: ParentProps<Props>) => {
|
||||||
|
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
|
||||||
|
<Show when={init.ready === undefined || init.ready}>
|
||||||
|
<ctx.Provider value={init}>{props.children}</ctx.Provider>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
use() {
|
||||||
|
const value = useContext(ctx)
|
||||||
|
if (!value) throw new Error(`${input.name} context must be used within a context provider`)
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,22 @@
|
|||||||
import { render } from "@opentui/solid"
|
import { render } from "@opentui/solid"
|
||||||
import { App } from "./App"
|
import { App } from "./App"
|
||||||
import { ThemeProvider } from "./context/ThemeContext"
|
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(() => (
|
render(() => (
|
||||||
<ThemeProvider>
|
<ToastProvider>
|
||||||
|
<ThemeProvider mode="dark">
|
||||||
|
<KeybindProvider>
|
||||||
|
<DialogProvider>
|
||||||
|
<CommandProvider>
|
||||||
<App />
|
<App />
|
||||||
|
<Toast />
|
||||||
|
</CommandProvider>
|
||||||
|
</DialogProvider>
|
||||||
|
</KeybindProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</ToastProvider>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { RGBA } from "@opentui/core"
|
import type { RGBA } from "@opentui/core"
|
||||||
import type { ColorValue, ThemeJson, Variant } from "./theme-schema"
|
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 = {
|
export type LayerBackgrounds = {
|
||||||
layer0: ColorValue
|
layer0: ColorValue
|
||||||
|
|||||||
307
src/ui/command.tsx
Normal file
307
src/ui/command.tsx
Normal file
@@ -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<typeof useDialog>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandContext = ReturnType<typeof init>
|
||||||
|
const ctx = createContext<CommandContext>()
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
|
||||||
|
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(() => <CommandDialog options={visibleOptions()} suggestedOptions={suggestedOptions()} />)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 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 <ctx.Provider value={value}>{props.children}</ctx.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<box flexDirection="column" padding={1}>
|
||||||
|
{/* Search input */}
|
||||||
|
<box marginBottom={1}>
|
||||||
|
<text fg={theme.textMuted}>
|
||||||
|
{"> "}
|
||||||
|
</text>
|
||||||
|
<text fg={theme.text}>
|
||||||
|
{filter() || "Type to search commands..."}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
|
||||||
|
{/* Command list */}
|
||||||
|
<box flexDirection="column" maxHeight={maxHeight}>
|
||||||
|
<For each={filteredOptions().slice(0, 10)}>
|
||||||
|
{(option, index) => (
|
||||||
|
<box
|
||||||
|
backgroundColor={index() === selectedIndex() ? theme.primary : undefined}
|
||||||
|
padding={1}
|
||||||
|
>
|
||||||
|
<box flexDirection="column" flexGrow={1}>
|
||||||
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
|
<text
|
||||||
|
fg={index() === selectedIndex() ? theme.selectedListItemText : theme.text}
|
||||||
|
attributes={index() === selectedIndex() ? TextAttributes.BOLD : undefined}
|
||||||
|
>
|
||||||
|
{option.title}
|
||||||
|
</text>
|
||||||
|
<Show when={option.footer}>
|
||||||
|
<text fg={theme.textMuted}>{option.footer}</text>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
<Show when={option.description}>
|
||||||
|
<text fg={theme.textMuted}>{option.description}</text>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<Show when={filteredOptions().length === 0}>
|
||||||
|
<text fg={theme.textMuted} style={{ padding: 1 }}>
|
||||||
|
No commands found
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
224
src/ui/dialog.tsx
Normal file
224
src/ui/dialog.tsx
Normal file
@@ -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 (
|
||||||
|
<box
|
||||||
|
onMouseUp={async () => {
|
||||||
|
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)}
|
||||||
|
>
|
||||||
|
<box
|
||||||
|
onMouseUp={async (e) => {
|
||||||
|
if (renderer.getSelection()) return
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
width={props.size === "large" ? 80 : 60}
|
||||||
|
maxWidth={dimensions().width - 2}
|
||||||
|
backgroundColor={theme.backgroundPanel}
|
||||||
|
paddingTop={1}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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<typeof init>
|
||||||
|
|
||||||
|
const ctx = createContext<DialogContext>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<ctx.Provider value={value}>
|
||||||
|
{props.children}
|
||||||
|
<box
|
||||||
|
position="absolute"
|
||||||
|
onMouseUp={async () => {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={value.stack.length > 0}>
|
||||||
|
<Dialog onClose={() => value.clear()} size={value.size}>
|
||||||
|
{value.stack.at(-1)!.element}
|
||||||
|
</Dialog>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
</ctx.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
153
src/ui/toast.tsx
Normal file
153
src/ui/toast.tsx
Normal file
@@ -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 (
|
||||||
|
<Show when={toast.currentToast}>
|
||||||
|
{(current) => (
|
||||||
|
<box
|
||||||
|
position="absolute"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="flex-start"
|
||||||
|
top={2}
|
||||||
|
right={2}
|
||||||
|
maxWidth={Math.min(60, dimensions().width - 6)}
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
backgroundColor={theme.backgroundPanel}
|
||||||
|
borderColor={getVariantColor(current().variant)}
|
||||||
|
border={["left", "right"]}
|
||||||
|
>
|
||||||
|
<box flexDirection="column">
|
||||||
|
<Show when={current().title}>
|
||||||
|
<text attributes={TextAttributes.BOLD} style={{ marginBottom: 1 }} fg={theme.text}>
|
||||||
|
{current().title}
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
<text fg={theme.text} wrapMode="word" width="100%">
|
||||||
|
{current().message}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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<typeof init>
|
||||||
|
|
||||||
|
const ctx = createContext<ToastContext>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ToastProvider provides toast functionality.
|
||||||
|
* NOTE: The Toast UI component is NOT rendered here - you must render <Toast />
|
||||||
|
* separately inside your component tree, after ThemeProvider.
|
||||||
|
*/
|
||||||
|
export function ToastProvider(props: ParentProps) {
|
||||||
|
const value = init()
|
||||||
|
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useToast() {
|
||||||
|
const value = useContext(ctx)
|
||||||
|
if (!value) {
|
||||||
|
throw new Error("useToast must be used within a ToastProvider")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
221
src/utils/clipboard.ts
Normal file
221
src/utils/clipboard.ts
Normal file
@@ -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<T>(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<Content | undefined> {
|
||||||
|
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<string | undefined> {
|
||||||
|
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<void> {
|
||||||
|
// Always try OSC 52 first for SSH/tmux support
|
||||||
|
writeOsc52(text)
|
||||||
|
// Then use native clipboard
|
||||||
|
await getCopyMethod()(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,9 @@ import type { ThemeJson } from "../types/theme-schema"
|
|||||||
import { THEME_JSON } from "../constants/themes"
|
import { THEME_JSON } from "../constants/themes"
|
||||||
import { validateTheme } from "./theme-loader"
|
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() {
|
export async function getCustomThemes() {
|
||||||
const home = process.env.HOME ?? ""
|
const home = process.env.HOME ?? ""
|
||||||
if (!home) return {}
|
if (!home) return {}
|
||||||
@@ -22,6 +25,10 @@ export async function getCustomThemes() {
|
|||||||
const glob = new Bun.Glob("*.json")
|
const glob = new Bun.Glob("*.json")
|
||||||
for await (const item of glob.scan({ absolute: true, followSymlinks: true, cwd: dir })) {
|
for await (const item of glob.scan({ absolute: true, followSymlinks: true, cwd: dir })) {
|
||||||
const name = path.basename(item, ".json")
|
const name = path.basename(item, ".json")
|
||||||
|
// 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
|
const json = (await Bun.file(item).json()) as ThemeJson
|
||||||
validateTheme(json, item)
|
validateTheme(json, item)
|
||||||
result[name] = json
|
result[name] = json
|
||||||
|
|||||||
136
src/utils/event-bus.ts
Normal file
136
src/utils/event-bus.ts
Normal file
@@ -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<T = unknown> = (data: T) => void
|
||||||
|
|
||||||
|
// Export EventHandler type for external use
|
||||||
|
export type { EventHandler }
|
||||||
|
|
||||||
|
interface EventBusInstance {
|
||||||
|
on<T = unknown>(event: string, handler: EventHandler<T>): () => void
|
||||||
|
once<T = unknown>(event: string, handler: EventHandler<T>): () => void
|
||||||
|
off<T = unknown>(event: string, handler: EventHandler<T>): void
|
||||||
|
emit<T = unknown>(event: string, data: T): void
|
||||||
|
clear(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEventBus(): EventBusInstance {
|
||||||
|
const handlers = new Map<string, Set<EventHandler>>()
|
||||||
|
|
||||||
|
return {
|
||||||
|
on<T = unknown>(event: string, handler: EventHandler<T>): () => 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<T = unknown>(event: string, handler: EventHandler<T>): () => void {
|
||||||
|
const wrappedHandler: EventHandler<T> = (data) => {
|
||||||
|
this.off(event, wrappedHandler)
|
||||||
|
handler(data)
|
||||||
|
}
|
||||||
|
return this.on(event, wrappedHandler)
|
||||||
|
},
|
||||||
|
|
||||||
|
off<T = unknown>(event: string, handler: EventHandler<T>): void {
|
||||||
|
const eventHandlers = handlers.get(event)
|
||||||
|
if (eventHandlers) {
|
||||||
|
eventHandlers.delete(handler as EventHandler)
|
||||||
|
if (eventHandlers.size === 0) {
|
||||||
|
handlers.delete(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
emit<T = unknown>(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<K extends keyof AppEvents>(event: K, data: AppEvents[K]): void {
|
||||||
|
EventBus.emit(event, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function on<K extends keyof AppEvents>(
|
||||||
|
event: K,
|
||||||
|
handler: EventHandler<AppEvents[K]>
|
||||||
|
): () => void {
|
||||||
|
return EventBus.on(event, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function once<K extends keyof AppEvents>(
|
||||||
|
event: K,
|
||||||
|
handler: EventHandler<AppEvents[K]>
|
||||||
|
): () => void {
|
||||||
|
return EventBus.once(event, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function off<K extends keyof AppEvents>(
|
||||||
|
event: K,
|
||||||
|
handler: EventHandler<AppEvents[K]>
|
||||||
|
): void {
|
||||||
|
EventBus.off(event, handler)
|
||||||
|
}
|
||||||
187
src/utils/keybind.ts
Normal file
187
src/utils/keybind.ts
Normal file
@@ -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
|
||||||
|
* - "<leader>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, ... }
|
||||||
|
* - "<leader>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("<leader>")) {
|
||||||
|
info.leader = true
|
||||||
|
part = part.substring(8) // Remove "<leader>"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<string, unknown>
|
||||||
|
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("<leader>")
|
||||||
|
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: "<leader>n",
|
||||||
|
session_list: "<leader>s",
|
||||||
|
|
||||||
|
// Theme
|
||||||
|
theme_list: "<leader>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
|
||||||
104
src/utils/theme-observer.ts
Normal file
104
src/utils/theme-observer.ts
Normal file
@@ -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 <pid>` 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<T>(
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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:
|
|
||||||
- <left> key navigates to previous layer (prevents going below layer 0)
|
|
||||||
- <right> 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
|
|
||||||
@@ -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:
|
|
||||||
- <enter> key goes down into a layer (prevents going below max depth)
|
|
||||||
- <escape> 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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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/`
|
|
||||||
@@ -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/`
|
|
||||||
@@ -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`
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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<ThemeJson>`
|
|
||||||
- Implement `loadThemeFromPath(path: string): Promise<ThemeJson>`
|
|
||||||
- Implement `getAllThemes(): Promise<Record<string, ThemeJson>>`
|
|
||||||
- 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)
|
|
||||||
@@ -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<number, RGBA>`
|
|
||||||
- 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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user