fix lint: make bun tsc --noEmit actually pass (was a TS crash + latent errors)

tsconfig used jsx:"preserve"+jsxImportSource, which trips a TS 5.9
internal crash ("Expected sourceFile.imports[0] to be the synthesized JSX
runtime import") so tsc could never run clean. Switch to jsx:"react-jsx"
with the package's real jsx-runtime types, and fix the real errors that
surfaced:
- delete dead src/components/Navigation.tsx (imported ./Tab that doesn't
  exist; the component has no importers)
- PlaybackControls: fix relative path to @/utils/audio-player
- SourceBadge: drop dead module-level typeColor (bare 'theme')
- command palette: bind to the real 'command' keybind (:) instead of the
  never-defined 'command_list' action (palette was unreachable)
- yazi-pane-row test: destroy() must return Promise<void> as typed

Also fold in the package.json lint fix (bun tsc --noEmit) and doc polish.
This commit is contained in:
2026-08-07 18:18:25 -04:00
parent 1d3abd53d4
commit 0cc15c8d90
8 changed files with 403 additions and 410 deletions

View File

@@ -23,8 +23,8 @@ make native # build libcavacore.dylib from the vendored C source
bun run dev # launch with hot reload (alias: make dev) bun run dev # launch with hot reload (alias: make dev)
``` ```
The app is a TUI — it expects a real terminal (kitty, iTerm2, WezTerm, tmux, The app is a TUI — it expects a real terminal (Ghostty, kitty, iTerm2,
…). It will not render in a plain captured `bash` session. WezTerm, tmux, …). It will not render in a plain captured `bash` session.
## What each command does ## What each command does
@@ -35,14 +35,11 @@ The app is a TUI — it expects a real terminal (kitty, iTerm2, WezTerm, tmux,
| `bun run dev` | Run with hot reload | | `bun run dev` | Run with hot reload |
| `bun run start` | Run once (no watch) | | `bun run start` | Run once (no watch) |
| `bun test` | Run the test suite (see [Testing](#testing)) | | `bun test` | Run the test suite (see [Testing](#testing)) |
| `make lint` | Type-check with `bun tsc --noEmit` | | `bun run lint` | Type-check |
| `bun run build` | Bundle JS into `dist/` + copy native libs (the `podtui` npm script path) | | `bun run build` | Bundle JS into `dist/` + copy native libs (the `podtui` npm script path) |
| `make dist` | Compile the standalone binary + make the current platform's tarball | | `make dist` | Compile the standalone binary + make the current platform's tarball |
| `make clean` | Remove `dist/` | | `make clean` | Remove `dist/` |
> Note: `package.json` also has a `lint` script that points at a
> `lint.ts` file that doesn't exist. Use `make lint` (real type-checking).
## Repository layout ## Repository layout
``` ```
@@ -112,10 +109,6 @@ Cavacore smoke test: `bun tests/cavacore-smoke.ts`
`-headerpad`” for a prebuilt dylib. The app dlopens the libs by path, so `-headerpad`” for a prebuilt dylib. The app dlopens the libs by path, so
the warning is cosmetic; installs complete and the app boots. the warning is cosmetic; installs complete and the app boots.
4. **`make lint` is the truth, not the `package.json` scripts.**
The repo's ESLint wiring is stale; `make lint` runs the real
type-check and is what CI treats as the clean bar.
## Testing ## Testing
```bash ```bash
@@ -135,7 +128,7 @@ Audio is a no-op during those snapshots. The last frame lands in
## Releasing ## Releasing
Releases are built and published from **tags**; CI does the heavy lifting. Releases are built and published from **tags**
### Steps ### Steps

View File

@@ -14,7 +14,7 @@
"build": "bun run build.ts", "build": "bun run build.ts",
"dist": "bun dist/index.js", "dist": "bun dist/index.js",
"test": "bun test", "test": "bun test",
"lint": "bun run lint.ts" "lint": "bun tsc --noEmit"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",

View File

@@ -1,28 +0,0 @@
import type { TabId } from "./Tab"
import { useTheme } from "@/context/ThemeContext"
type NavigationProps = {
activeTab: TabId
onTabSelect: (tab: TabId) => void
}
export function Navigation(props: NavigationProps) {
const { theme } = useTheme();
return (
<box style={{ flexDirection: "row", width: "100%", height: 1 }}>
<text fg={theme.text}>
{props.activeTab === "feed" ? "[" : " "}Feed{props.activeTab === "feed" ? "]" : " "}
<span> </span>
{props.activeTab === "shows" ? "[" : " "}My Shows{props.activeTab === "shows" ? "]" : " "}
<span> </span>
{props.activeTab === "discover" ? "[" : " "}Discover{props.activeTab === "discover" ? "]" : " "}
<span> </span>
{props.activeTab === "search" ? "[" : " "}Search{props.activeTab === "search" ? "]" : " "}
<span> </span>
{props.activeTab === "player" ? "[" : " "}Player{props.activeTab === "player" ? "]" : " "}
<span> </span>
{props.activeTab === "settings" ? "[" : " "}Settings{props.activeTab === "settings" ? "]" : " "}
</text>
</box>
)
}

View File

@@ -1,64 +1,86 @@
import type { BackendName } from "../utils/audio-player" import type { BackendName } from "@/utils/audio-player";
import { useTheme } from "@/context/ThemeContext" import { useTheme } from "@/context/ThemeContext";
type PlaybackControlsProps = { type PlaybackControlsProps = {
isPlaying: boolean isPlaying: boolean;
volume: number volume: number;
speed: number speed: number;
backendName?: BackendName backendName?: BackendName;
hasAudioUrl?: boolean hasAudioUrl?: boolean;
onToggle: () => void onToggle: () => void;
onPrev: () => void onPrev: () => void;
onNext: () => void onNext: () => void;
onVolumeChange: (value: number) => void onVolumeChange: (value: number) => void;
onSpeedChange: (value: number) => void onSpeedChange: (value: number) => void;
} };
const BACKEND_LABELS: Record<BackendName, string> = { const BACKEND_LABELS: Record<BackendName, string> = {
mpv: "mpv", mpv: "mpv",
ffplay: "ffplay", ffplay: "ffplay",
afplay: "afplay", afplay: "afplay",
system: "system", system: "system",
none: "none", none: "none",
} };
export function PlaybackControls(props: PlaybackControlsProps) { export function PlaybackControls(props: PlaybackControlsProps) {
const { theme } = useTheme(); const { theme } = useTheme();
return ( return (
<box flexDirection="row" gap={1} alignItems="center" border padding={1} borderColor={theme.border}> <box
<box border padding={0} onMouseDown={props.onPrev} borderColor={theme.border}> flexDirection="row"
<text fg={theme.primary}>[Prev]</text> gap={1}
</box> alignItems="center"
<box border padding={0} onMouseDown={props.onToggle} borderColor={theme.border}> border
<text fg={theme.primary}>{props.isPlaying ? "[Pause]" : "[Play]"}</text> padding={1}
</box> borderColor={theme.border}
<box border padding={0} onMouseDown={props.onNext} borderColor={theme.border}> >
<text fg={theme.primary}>[Next]</text> <box
</box> border
<box flexDirection="row" gap={1} marginLeft={2}> padding={0}
<text fg={theme.textMuted}>Vol</text> onMouseDown={props.onPrev}
<text fg={theme.text}>{Math.round(props.volume * 100)}%</text> borderColor={theme.border}
</box> >
<box flexDirection="row" gap={1} marginLeft={2}> <text fg={theme.primary}>[Prev]</text>
<text fg={theme.textMuted}>Speed</text> </box>
<text fg={theme.text}>{props.speed}x</text> <box
</box> border
{props.backendName && props.backendName !== "none" && ( padding={0}
<box flexDirection="row" gap={1} marginLeft={2}> onMouseDown={props.onToggle}
<text fg={theme.textMuted}>via</text> borderColor={theme.border}
<text fg={theme.primary}>{BACKEND_LABELS[props.backendName]}</text> >
</box> <text fg={theme.primary}>{props.isPlaying ? "[Pause]" : "[Play]"}</text>
)} </box>
{props.backendName === "none" && ( <box
<box marginLeft={2}> border
<text fg={theme.warning}>No audio player found</text> padding={0}
</box> onMouseDown={props.onNext}
)} borderColor={theme.border}
{props.hasAudioUrl === false && ( >
<box marginLeft={2}> <text fg={theme.primary}>[Next]</text>
<text fg={theme.warning}>No audio URL</text> </box>
</box> <box flexDirection="row" gap={1} marginLeft={2}>
)} <text fg={theme.textMuted}>Vol</text>
</box> <text fg={theme.text}>{Math.round(props.volume * 100)}%</text>
) </box>
<box flexDirection="row" gap={1} marginLeft={2}>
<text fg={theme.textMuted}>Speed</text>
<text fg={theme.text}>{props.speed}x</text>
</box>
{props.backendName && props.backendName !== "none" && (
<box flexDirection="row" gap={1} marginLeft={2}>
<text fg={theme.textMuted}>via</text>
<text fg={theme.primary}>{BACKEND_LABELS[props.backendName]}</text>
</box>
)}
{props.backendName === "none" && (
<box marginLeft={2}>
<text fg={theme.warning}>No audio player found</text>
</box>
)}
{props.hasAudioUrl === false && (
<box marginLeft={2}>
<text fg={theme.warning}>No audio URL</text>
</box>
)}
</box>
);
} }

View File

@@ -2,42 +2,37 @@ import { SourceType } from "@/types/source";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
type SourceBadgeProps = { type SourceBadgeProps = {
sourceId: string; sourceId: string;
sourceName?: string; sourceName?: string;
sourceType?: SourceType; sourceType?: SourceType;
}; };
const typeLabel = (sourceType?: SourceType) => { const typeLabel = (sourceType?: SourceType) => {
if (sourceType === SourceType.API) return "API"; if (sourceType === SourceType.API) return "API";
if (sourceType === SourceType.RSS) return "RSS"; if (sourceType === SourceType.RSS) return "RSS";
if (sourceType === SourceType.CUSTOM) return "Custom"; if (sourceType === SourceType.CUSTOM) return "Custom";
return "Source"; return "Source";
};
const typeColor = (sourceType?: SourceType) => {
if (sourceType === SourceType.API) return theme.primary;
if (sourceType === SourceType.RSS) return theme.success;
if (sourceType === SourceType.CUSTOM) return theme.warning;
return theme.textMuted;
}; };
// No module-level typeColor here — it needs the theme from the component.
// The correct definition lives inside SourceBadge below.
export function SourceBadge(props: SourceBadgeProps) { export function SourceBadge(props: SourceBadgeProps) {
const { theme } = useTheme(); const { theme } = useTheme();
const label = () => props.sourceName || props.sourceId; const label = () => props.sourceName || props.sourceId;
const typeColor = (sourceType?: SourceType) => { const typeColor = (sourceType?: SourceType) => {
if (sourceType === SourceType.API) return theme.primary; if (sourceType === SourceType.API) return theme.primary;
if (sourceType === SourceType.RSS) return theme.success; if (sourceType === SourceType.RSS) return theme.success;
if (sourceType === SourceType.CUSTOM) return theme.warning; if (sourceType === SourceType.CUSTOM) return theme.warning;
return theme.textMuted; return theme.textMuted;
}; };
return ( return (
<box flexDirection="row" gap={1} padding={0}> <box flexDirection="row" gap={1} padding={0}>
<text fg={typeColor(props.sourceType)}> <text fg={typeColor(props.sourceType)}>
[{typeLabel(props.sourceType)}] [{typeLabel(props.sourceType)}]
</text> </text>
<text fg={theme.textMuted}>{label()}</text> <text fg={theme.textMuted}>{label()}</text>
</box> </box>
); );
} }

View File

@@ -1,13 +1,13 @@
import { import {
createContext, createContext,
createMemo, createMemo,
createSignal, createSignal,
onCleanup, onCleanup,
useContext, useContext,
type Accessor, type Accessor,
type ParentProps, type ParentProps,
For, For,
Show, Show,
} from "solid-js"; } from "solid-js";
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"; import { useKeyboard, useTerminalDimensions } from "@opentui/solid";
import { KeybindsResolved, useKeybinds } from "../context/KeybindContext"; import { KeybindsResolved, useKeybinds } from "../context/KeybindContext";
@@ -21,313 +21,319 @@ import { SelectableBox, SelectableText } from "@/components/Selectable";
* Command option for the command palette. * Command option for the command palette.
*/ */
export type CommandOption = { export type CommandOption = {
/** Display title */ /** Display title */
title: string; title: string;
/** Unique identifier */ /** Unique identifier */
value: string; value: string;
/** Description shown below title */ /** Description shown below title */
description?: string; description?: string;
/** Category for grouping */ /** Category for grouping */
category?: string; category?: string;
/** Keybind reference */ /** Keybind reference */
keybind?: keyof KeybindsResolved; keybind?: keyof KeybindsResolved;
/** Whether this command is suggested */ /** Whether this command is suggested */
suggested?: boolean; suggested?: boolean;
/** Slash command configuration */ /** Slash command configuration */
slash?: { slash?: {
name: string; name: string;
aliases?: string[]; aliases?: string[];
}; };
/** Whether to hide from command list */ /** Whether to hide from command list */
hidden?: boolean; hidden?: boolean;
/** Whether command is enabled */ /** Whether command is enabled */
enabled?: boolean; enabled?: boolean;
/** Footer text (usually keybind display) */ /** Footer text (usually keybind display) */
footer?: string; footer?: string;
/** Handler when command is selected */ /** Handler when command is selected */
onSelect?: (dialog: ReturnType<typeof useDialog>) => void; onSelect?: (dialog: ReturnType<typeof useDialog>) => void;
}; };
type CommandContext = ReturnType<typeof init>; type CommandContext = ReturnType<typeof init>;
const ctx = createContext<CommandContext>(); const ctx = createContext<CommandContext>();
function init() { function init() {
const [registrations, setRegistrations] = createSignal< const [registrations, setRegistrations] = createSignal<
Accessor<CommandOption[]>[] Accessor<CommandOption[]>[]
>([]); >([]);
const [suspendCount, setSuspendCount] = createSignal(0); const [suspendCount, setSuspendCount] = createSignal(0);
const dialog = useDialog(); const dialog = useDialog();
const keybind = useKeybinds(); const keybind = useKeybinds();
const entries = createMemo(() => { const entries = createMemo(() => {
const all = registrations().flatMap((x) => x()); const all = registrations().flatMap((x) => x());
return all.map((x) => ({ return all.map((x) => ({
...x, ...x,
footer: x.keybind ? keybind.print(x.keybind) : undefined, footer: x.keybind ? keybind.print(x.keybind) : undefined,
})); }));
}); });
const isEnabled = (option: CommandOption) => option.enabled !== false; const isEnabled = (option: CommandOption) => option.enabled !== false;
const isVisible = (option: CommandOption) => const isVisible = (option: CommandOption) =>
isEnabled(option) && !option.hidden; isEnabled(option) && !option.hidden;
const visibleOptions = createMemo(() => const visibleOptions = createMemo(() =>
entries().filter((option) => isVisible(option)), entries().filter((option) => isVisible(option)),
); );
const suggestedOptions = createMemo(() => const suggestedOptions = createMemo(() =>
visibleOptions() visibleOptions()
.filter((option) => option.suggested) .filter((option) => option.suggested)
.map((option) => ({ .map((option) => ({
...option, ...option,
value: `suggested:${option.value}`, value: `suggested:${option.value}`,
category: "Suggested", category: "Suggested",
})), })),
); );
const suspended = () => suspendCount() > 0; const suspended = () => suspendCount() > 0;
// Handle keybind shortcuts // Handle keybind shortcuts
useKeyboard((evt) => { useKeyboard((evt) => {
if (suspended()) return; if (suspended()) return;
if (dialog.isOpen) return; if (dialog.isOpen) return;
for (const option of entries()) { for (const option of entries()) {
if (!isEnabled(option)) continue; if (!isEnabled(option)) continue;
if (option.keybind && keybind.match(option.keybind, evt)) { if (option.keybind && keybind.match(option.keybind, evt)) {
evt.preventDefault(); evt.preventDefault();
option.onSelect?.(dialog); option.onSelect?.(dialog);
emit("command.execute", { command: option.value }); emit("command.execute", { command: option.value });
return; return;
} }
} }
}); });
const result = { const result = {
/** /**
* Trigger a command by its value. * Trigger a command by its value.
*/ */
trigger(name: string) { trigger(name: string) {
for (const option of entries()) { for (const option of entries()) {
if (option.value === name) { if (option.value === name) {
if (!isEnabled(option)) return; if (!isEnabled(option)) return;
option.onSelect?.(dialog); option.onSelect?.(dialog);
emit("command.execute", { command: name }); emit("command.execute", { command: name });
return; return;
} }
} }
}, },
/** /**
* Get all slash commands. * Get all slash commands.
*/ */
slashes() { slashes() {
return visibleOptions().flatMap((option) => { return visibleOptions().flatMap((option) => {
const slash = option.slash; const slash = option.slash;
if (!slash) return []; if (!slash) return [];
return { return {
display: "/" + slash.name, display: "/" + slash.name,
description: option.description ?? option.title, description: option.description ?? option.title,
aliases: slash.aliases?.map((alias) => "/" + alias), aliases: slash.aliases?.map((alias) => "/" + alias),
onSelect: () => result.trigger(option.value), onSelect: () => result.trigger(option.value),
}; };
}); });
}, },
/** /**
* Enable/disable keybinds temporarily. * Enable/disable keybinds temporarily.
*/ */
keybinds(enabled: boolean) { keybinds(enabled: boolean) {
setSuspendCount((count) => count + (enabled ? -1 : 1)); setSuspendCount((count) => count + (enabled ? -1 : 1));
}, },
suspended, suspended,
/** /**
* Show the command palette dialog. * Show the command palette dialog.
*/ */
show() { show() {
dialog.replace(() => ( dialog.replace(() => (
<CommandDialog <CommandDialog
options={visibleOptions()} options={visibleOptions()}
suggestedOptions={suggestedOptions()} suggestedOptions={suggestedOptions()}
/> />
)); ));
}, },
/** /**
* Register commands. Returns cleanup function. * Register commands. Returns cleanup function.
*/ */
register(cb: () => CommandOption[]) { register(cb: () => CommandOption[]) {
const results = createMemo(cb); const results = createMemo(cb);
setRegistrations((arr) => [results, ...arr]); setRegistrations((arr) => [results, ...arr]);
onCleanup(() => { onCleanup(() => {
setRegistrations((arr) => arr.filter((x) => x !== results)); setRegistrations((arr) => arr.filter((x) => x !== results));
}); });
}, },
/** /**
* Get all visible options. * Get all visible options.
*/ */
get options() { get options() {
return visibleOptions(); return visibleOptions();
}, },
}; };
return result; return result;
} }
export function useCommandDialog() { export function useCommandDialog() {
const value = useContext(ctx); const value = useContext(ctx);
if (!value) { if (!value) {
throw new Error("useCommandDialog must be used within a CommandProvider"); throw new Error("useCommandDialog must be used within a CommandProvider");
} }
return value; return value;
} }
export function CommandProvider(props: ParentProps) { export function CommandProvider(props: ParentProps) {
const value = init(); const value = init();
const dialog = useDialog(); const dialog = useDialog();
const keybind = useKeybinds(); const keybind = useKeybinds();
// Open command palette on ctrl+p or command_list keybind // Open the command palette via the `command` keybind (bound to `:` in
useKeyboard((evt) => { // keybinds.jsonc). The old hardcoded "command_list" name was never a
if (value.suspended()) return; // canonical action, so the palette was unreachable dead code.
if (dialog.isOpen) return; useKeyboard((evt) => {
if (evt.defaultPrevented) return; if (value.suspended()) return;
if (keybind.match("command_list", evt)) { if (dialog.isOpen) return;
evt.preventDefault(); if (evt.defaultPrevented) return;
value.show(); if (keybind.match("command", evt)) {
return; evt.preventDefault();
} value.show();
}); return;
}
});
return <ctx.Provider value={value}>{props.children}</ctx.Provider>; return <ctx.Provider value={value}>{props.children}</ctx.Provider>;
} }
/** /**
* Command palette dialog component. * Command palette dialog component.
*/ */
function CommandDialog(props: { function CommandDialog(props: {
options: CommandOption[]; options: CommandOption[];
suggestedOptions: CommandOption[]; suggestedOptions: CommandOption[];
}) { }) {
const { theme } = useTheme(); const { theme } = useTheme();
const dialog = useDialog(); const dialog = useDialog();
const dimensions = useTerminalDimensions(); const dimensions = useTerminalDimensions();
const [filter, setFilter] = createSignal(""); const [filter, setFilter] = createSignal("");
const [selectedIndex, setSelectedIndex] = createSignal(0); const [selectedIndex, setSelectedIndex] = createSignal(0);
const filteredOptions = createMemo(() => { const filteredOptions = createMemo(() => {
const query = filter().toLowerCase(); const query = filter().toLowerCase();
if (!query) { if (!query) {
return [...props.suggestedOptions, ...props.options]; return [...props.suggestedOptions, ...props.options];
} }
return props.options.filter( return props.options.filter(
(option) => (option) =>
option.title.toLowerCase().includes(query) || option.title.toLowerCase().includes(query) ||
option.description?.toLowerCase().includes(query) || option.description?.toLowerCase().includes(query) ||
option.category?.toLowerCase().includes(query), option.category?.toLowerCase().includes(query),
); );
}); });
// Reset selection when filter changes // Reset selection when filter changes
createMemo(() => { createMemo(() => {
filter(); filter();
setSelectedIndex(0); setSelectedIndex(0);
}); });
useKeyboard((evt) => { useKeyboard((evt) => {
if (evt.name === "escape") { if (evt.name === "escape") {
dialog.clear(); dialog.clear();
evt.preventDefault(); evt.preventDefault();
return; return;
} }
if (evt.name === "return" || evt.name === "enter") { if (evt.name === "return" || evt.name === "enter") {
const option = filteredOptions()[selectedIndex()]; const option = filteredOptions()[selectedIndex()];
if (option) { if (option) {
option.onSelect?.(dialog); option.onSelect?.(dialog);
dialog.clear(); dialog.clear();
} }
evt.preventDefault(); evt.preventDefault();
return; return;
} }
if (evt.name === "up" || (evt.ctrl && evt.name === "p")) { if (evt.name === "up" || (evt.ctrl && evt.name === "p")) {
setSelectedIndex((i) => Math.max(0, i - 1)); setSelectedIndex((i) => Math.max(0, i - 1));
evt.preventDefault(); evt.preventDefault();
return; return;
} }
if (evt.name === "down" || (evt.ctrl && evt.name === "n")) { if (evt.name === "down" || (evt.ctrl && evt.name === "n")) {
setSelectedIndex((i) => Math.min(filteredOptions().length - 1, i + 1)); setSelectedIndex((i) => Math.min(filteredOptions().length - 1, i + 1));
evt.preventDefault(); evt.preventDefault();
return; return;
} }
// Handle text input // Handle text input
if (evt.name && evt.name.length === 1 && !evt.ctrl && !evt.meta) { if (evt.name && evt.name.length === 1 && !evt.ctrl && !evt.meta) {
setFilter((f) => f + evt.name); setFilter((f) => f + evt.name);
return; return;
} }
if (evt.name === "backspace") { if (evt.name === "backspace") {
setFilter((f) => f.slice(0, -1)); setFilter((f) => f.slice(0, -1));
return; return;
} }
}); });
const maxHeight = Math.floor(dimensions().height * 0.6); const maxHeight = Math.floor(dimensions().height * 0.6);
return ( return (
<box flexDirection="column" padding={1} borderColor={theme.border}> <box flexDirection="column" padding={1} borderColor={theme.border}>
{/* Search input */} {/* Search input */}
<box marginBottom={1}> <box marginBottom={1}>
<text fg={theme.textMuted}>{"> "}</text> <text fg={theme.textMuted}>{"> "}</text>
<text fg={theme.text}>{filter() || "Type to search commands..."}</text> <text fg={theme.text}>{filter() || "Type to search commands..."}</text>
</box> </box>
{/* Command list */} {/* Command list */}
<box flexDirection="column" maxHeight={maxHeight} borderColor={theme.border}> <box
<For each={filteredOptions().slice(0, 10)}> flexDirection="column"
{(option, index) => ( maxHeight={maxHeight}
<SelectableBox borderColor={theme.border}
selected={() => index() === selectedIndex()} >
flexDirection="column" <For each={filteredOptions().slice(0, 10)}>
padding={1} {(option, index) => (
onMouseDown={() => { <SelectableBox
setSelectedIndex(index()); selected={() => index() === selectedIndex()}
const selectedOption = filteredOptions()[index()]; flexDirection="column"
if (selectedOption) { padding={1}
selectedOption.onSelect?.(dialog); onMouseDown={() => {
dialog.clear(); setSelectedIndex(index());
} const selectedOption = filteredOptions()[index()];
}} if (selectedOption) {
> selectedOption.onSelect?.(dialog);
<box flexDirection="column" flexGrow={1}> dialog.clear();
<SelectableText }
selected={() => index() === selectedIndex()} }}
primary >
> <box flexDirection="column" flexGrow={1}>
{option.title} <SelectableText
</SelectableText> selected={() => index() === selectedIndex()}
<Show when={option.footer}> primary
<SelectableText >
selected={() => index() === selectedIndex()} {option.title}
tertiary </SelectableText>
> <Show when={option.footer}>
{option.footer} <SelectableText
</SelectableText> selected={() => index() === selectedIndex()}
</Show> tertiary
<Show when={option.description}> >
<SelectableText {option.footer}
selected={() => index() === selectedIndex()} </SelectableText>
tertiary </Show>
> <Show when={option.description}>
{option.description} <SelectableText
</SelectableText> selected={() => index() === selectedIndex()}
</Show> tertiary
</box> >
</SelectableBox> {option.description}
)} </SelectableText>
</For> </Show>
<Show when={filteredOptions().length === 0}> </box>
<text fg={theme.textMuted} style={{ padding: 1 }}> </SelectableBox>
No commands found )}
</text> </For>
</Show> <Show when={filteredOptions().length === 0}>
</box> <text fg={theme.textMuted} style={{ padding: 1 }}>
</box> No commands found
); </text>
</Show>
</box>
</box>
);
} }

View File

@@ -106,7 +106,12 @@ async function renderPaneRow(props: TestPaneProps): Promise<{
await new Promise((r) => setTimeout(r, 40)); await new Promise((r) => setTimeout(r, 40));
} }
const spans = setup.captureSpans() as unknown as Frame; const spans = setup.captureSpans() as unknown as Frame;
return { spans, destroy: () => setup.renderer.destroy() }; return {
spans,
destroy: async () => {
setup.renderer.destroy();
},
};
} }
const cleanups: (() => void | Promise<void>)[] = []; const cleanups: (() => void | Promise<void>)[] = [];

View File

@@ -4,7 +4,7 @@
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"jsx": "preserve", "jsx": "react-jsx",
"jsxImportSource": "@opentui/solid", "jsxImportSource": "@opentui/solid",
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,