fix: opentui subtree disposal on depth swap; theme inputs; recent-click
This commit is contained in:
@@ -159,10 +159,17 @@ function DiscoverPage() {
|
|||||||
: `${focusedCategory()?.name ?? "Discover"} · ${podcasts().length}`;
|
: `${focusedCategory()?.name ?? "Discover"} · ${podcasts().length}`;
|
||||||
|
|
||||||
// ── parent pane: previous-depth list (muted/blank at depth 0) ─────────────
|
// ── parent pane: previous-depth list (muted/blank at depth 0) ─────────────
|
||||||
// Stable <Show> gate (not a ternary root swap) so the parent list
|
// Sibling <Show> blocks per depth (the known-good opentui disposal
|
||||||
// mounts/unmounts cleanly on depth change.
|
// pattern, mirrors Settings): a STABLE fragment root whose inner <Show>
|
||||||
|
// children toggle on depth change, so the old subtree is disposed instead
|
||||||
|
// of left orphaned next to the new one (single <Show with fallback> and
|
||||||
|
// ternary root swaps both leak the previous root).
|
||||||
const parentContent = () => (
|
const parentContent = () => (
|
||||||
<Show when={depth() >= 1} fallback={<TabListPane muted />}>
|
<>
|
||||||
|
<Show when={depth() === 0}>
|
||||||
|
<TabListPane muted />
|
||||||
|
</Show>
|
||||||
|
<Show when={depth() >= 1}>
|
||||||
<For each={categories()}>
|
<For each={categories()}>
|
||||||
{(cat, index) => {
|
{(cat, index) => {
|
||||||
const lf = () => nav.depthFocus(0);
|
const lf = () => nav.depthFocus(0);
|
||||||
@@ -187,6 +194,7 @@ function DiscoverPage() {
|
|||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── current pane ───────────────────────────────────────────────────────────
|
// ── current pane ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -74,12 +74,16 @@ function SearchPage() {
|
|||||||
// j/k (yielding to a non-existent input) and only the scrollbox's native
|
// j/k (yielding to a non-existent input) and only the scrollbox's native
|
||||||
// scroll responds.
|
// scroll responds.
|
||||||
//
|
//
|
||||||
// The effect only re-runs on a depth transition, so Escape (defocus) and
|
// The depth STACK signal is also written by focus moves (setDepthFocus),
|
||||||
// `s` (refocus) at the same depth are not clobbered.
|
// so gate the sync on the depth VALUE via a memo: the effect must re-run
|
||||||
|
// only on an actual depth transition. Without the memo every j/k at the
|
||||||
|
// query depth re-focuses the input (undoing Escape), which keeps the
|
||||||
|
// recents list unreachable by keyboard.
|
||||||
onMount(() => nav.setInputFocused(depth() === 0));
|
onMount(() => nav.setInputFocused(depth() === 0));
|
||||||
onCleanup(() => nav.setInputFocused(false));
|
onCleanup(() => nav.setInputFocused(false));
|
||||||
|
const isQueryDepth = createMemo(() => depth() === 0);
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
nav.setInputFocused(depth() === 0);
|
nav.setInputFocused(isQueryDepth());
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── results (depth 1) ─────────────────────────────────────────────────────
|
// ── results (depth 1) ─────────────────────────────────────────────────────
|
||||||
@@ -213,8 +217,17 @@ function SearchPage() {
|
|||||||
: theme.text;
|
: theme.text;
|
||||||
|
|
||||||
// ── parent pane: previous-depth content (tab list at depth 0) ──────────────
|
// ── parent pane: previous-depth content (tab list at depth 0) ──────────────
|
||||||
|
// Sibling <Show> blocks per depth (the known-good opentui disposal
|
||||||
|
// pattern, mirrors Settings): a STABLE fragment root whose inner <Show>
|
||||||
|
// children toggle on depth change, so the old subtree is disposed instead
|
||||||
|
// of left orphaned next to the new one (single <Show with fallback> and
|
||||||
|
// ternary root swaps both leak the previous root).
|
||||||
const parentContent = () => (
|
const parentContent = () => (
|
||||||
<Show when={depth() >= 1} fallback={<TabListPane muted />}>
|
<>
|
||||||
|
<Show when={depth() === 0}>
|
||||||
|
<TabListPane muted />
|
||||||
|
</Show>
|
||||||
|
<Show when={depth() >= 1}>
|
||||||
<box flexDirection="column" gap={1} padding={1}>
|
<box flexDirection="column" gap={1} padding={1}>
|
||||||
<text fg={theme.textSecondary}>Query</text>
|
<text fg={theme.textSecondary}>Query</text>
|
||||||
<text fg={muted()}>{submittedQuery() || "(empty)"}</text>
|
<text fg={muted()}>{submittedQuery() || "(empty)"}</text>
|
||||||
@@ -222,6 +235,7 @@ function SearchPage() {
|
|||||||
<text fg={muted()}>h: back to query</text>
|
<text fg={muted()}>h: back to query</text>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── current pane ────────────────────────────────────────────────────────────
|
// ── current pane ────────────────────────────────────────────────────────────
|
||||||
@@ -239,6 +253,9 @@ function SearchPage() {
|
|||||||
placeholder="Enter podcast name..."
|
placeholder="Enter podcast name..."
|
||||||
focused={inputActive()}
|
focused={inputActive()}
|
||||||
width={28}
|
width={28}
|
||||||
|
textColor={theme.text}
|
||||||
|
focusedTextColor={theme.accent}
|
||||||
|
cursorColor={theme.accent}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
<Show when={searchStore.isSearching()}>
|
<Show when={searchStore.isSearching()}>
|
||||||
@@ -274,6 +291,10 @@ function SearchPage() {
|
|||||||
onMouseDown={() => {
|
onMouseDown={() => {
|
||||||
nav.setActivePane(DEPTH_CENTER_PANE);
|
nav.setActivePane(DEPTH_CENTER_PANE);
|
||||||
nav.setDepthFocus(index(), 0);
|
nav.setDepthFocus(index(), 0);
|
||||||
|
// A recent is an action, not an item: clicking
|
||||||
|
// it re-runs that search (focus-only would be
|
||||||
|
// invisible — the input still owns the keys).
|
||||||
|
selectRecent(query);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<text fg={focusFg(index(), lf(), isActive())}>
|
<text fg={focusFg(index(), lf(), isActive())}>
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ export function ExportDialog() {
|
|||||||
value={filename[0]()}
|
value={filename[0]()}
|
||||||
onInput={filename[1]}
|
onInput={filename[1]}
|
||||||
style={{ width: 30 }}
|
style={{ width: 30 }}
|
||||||
|
textColor={theme.text}
|
||||||
|
focusedTextColor={theme.accent}
|
||||||
|
cursorColor={theme.accent}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
<box style={{ flexDirection: "row", gap: 1 }}>
|
<box style={{ flexDirection: "row", gap: 1 }}>
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export function FilePicker(props: FilePickerProps) {
|
|||||||
onInput={props.onChange}
|
onInput={props.onChange}
|
||||||
placeholder="/path/to/sync-file.json"
|
placeholder="/path/to/sync-file.json"
|
||||||
style={{ width: 40 }}
|
style={{ width: 40 }}
|
||||||
|
textColor={theme.text}
|
||||||
|
focusedTextColor={theme.accent}
|
||||||
|
cursorColor={theme.accent}
|
||||||
/>
|
/>
|
||||||
<text fg={theme.text}>Format: {format}</text>
|
<text fg={theme.text}>Format: {format}</text>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ function AddSourceForm() {
|
|||||||
onInput={setName}
|
onInput={setName}
|
||||||
placeholder="My Custom Feed"
|
placeholder="My Custom Feed"
|
||||||
width={25}
|
width={25}
|
||||||
|
textColor={theme.text}
|
||||||
|
focusedTextColor={theme.accent}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
@@ -116,6 +118,8 @@ function AddSourceForm() {
|
|||||||
}}
|
}}
|
||||||
placeholder="https://example.com/feed.rss"
|
placeholder="https://example.com/feed.rss"
|
||||||
width={35}
|
width={35}
|
||||||
|
textColor={theme.text}
|
||||||
|
focusedTextColor={theme.accent}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
<box
|
<box
|
||||||
|
|||||||
Reference in New Issue
Block a user