fix navigation flow

This commit is contained in:
2026-08-07 00:28:24 -04:00
parent 9e7a44309a
commit c8d29ed59d
14 changed files with 924 additions and 484 deletions

View File

@@ -15,10 +15,7 @@ import { createSignal, Show, For } from "solid-js";
import { useKeyboard } from "@opentui/solid";
import { useTheme } from "@/context/ThemeContext";
import { useKeybinds, type KeybindActionName } from "@/context/KeybindContext";
import {
useNavigation,
NavMode,
} from "@/context/NavigationContext";
import { useNavigation, NavMode } from "@/context/NavigationContext";
import { useAudio } from "@/hooks/useAudio";
import { useAudioNavStore, AudioSource } from "@/stores/audio-nav";
import { useFeedStore } from "@/stores/feed";
@@ -28,6 +25,8 @@ import { emit } from "@/utils/event-bus";
import { LayerGraph } from "@/utils/layer-graph";
import { TABS, TabPaneCount } from "@/utils/navigation";
import { createDispatcher } from "@/utils/dispatch";
import { TabListPane } from "@/components/TabPanel";
import { YaziPaneRow } from "@/components/YaziPaneRow";
const TAB_LABEL: Record<TABS, string> = {
[TABS.FEED]: "Feed",
@@ -94,7 +93,7 @@ export function Shell() {
case "q":
case "quit":
case "exit":
process.exit(0);
return process.exit(0);
case "refresh":
case "r":
emit("nav.action", {
@@ -190,7 +189,10 @@ export function Shell() {
// ── Unified router (normal + visual) ───────────────────────────────────────
const { dispatch } = createDispatcher({
nav,
audio: { togglePlayback: audio.togglePlayback, seekRelative: audio.seekRelative },
audio: {
togglePlayback: audio.togglePlayback,
seekRelative: audio.seekRelative,
},
k,
setShowHelp,
advanceEpisode,
@@ -232,11 +234,36 @@ export function Shell() {
height="100%"
backgroundColor={t.surface}
>
{/* ── Middle row: full-width active page ──────────────────────────────── */}
<box flexGrow={1} width="100%">
{LayerGraph[nav.activeTab()]()}
{/* ── Middle row: tab list (pane 0) + active tab content (panes 1..n) ─ */}
<box flexDirection="row" flexGrow={1} width="100%">
<Show
when={nav.atRootTab()}
fallback={
<box flexGrow={1} width="100%">
{LayerGraph[nav.activeTab()]()}
</box>
}
>
{/* app root: the tab list is the CURRENT pane, nothing in UP */}
<YaziPaneRow
parent={
<box padding={1}>
<text fg={t.textMuted}></text>
</box>
}
current={<TabListPane />}
preview={
<box padding={1}>
<text fg={t.textMuted}>j/k move · l/Enter open a tab</text>
</box>
}
parentLabel="Up"
currentLabel="Tabs"
previewLabel=""
focused
/>
</Show>
</box>
{/* ── Bottom status / command bar ─────────────────────────────────────── */}
<box
flexDirection="row"
@@ -252,10 +279,13 @@ export function Shell() {
{modeLabel()}
</text>
<text fg={t.textMuted} paddingLeft={1}>
{TAB_LABEL[nav.activeTab()]} ·{" "}
{nav.isDepthTab()
? `depth ${nav.currentDepth()}`
: `pane ${nav.activePane() + 1}/${TabPaneCount[nav.activeTab()]}`}
{nav.atRootTab()
? "Tabs · root"
: `${TAB_LABEL[nav.activeTab()]} · ${
nav.isDepthTab()
? `depth ${nav.currentDepth()}`
: `pane ${nav.activePane()}/${TabPaneCount[nav.activeTab()]}`
}`}
</text>
<Show when={nav.selectedIds().length > 0}>
<text fg={t.warning} paddingLeft={1}>
@@ -271,27 +301,6 @@ export function Shell() {
<text fg={t.textMuted} paddingRight={1}>
{pendingLabel()}
</text>
{/* ── Tab strip ─────────────────────────────────────────────────── */}
<For
each={Object.values(TABS).filter(
(v): v is TABS => typeof v === "number",
)}
>
{(tab) => {
const active = () => nav.activeTab() === tab;
return (
<text
fg={active() ? t.surface : t.textMuted}
backgroundColor={
active() ? t.primary : undefined
}
>
{active() ? "≡" : " "}[{tab}]{" "}
{TAB_LABEL[tab]}{" "}
</text>
);
}}
</For>
<text fg={t.textMuted} paddingRight={1}>
~
</text>
@@ -310,7 +319,6 @@ export function Shell() {
</Show>
</Show>
</box>
{/* ── Help overlay ─────────────────────────────────────────────────────── */}
<Show when={showHelp()}>
<HelpOverlay
@@ -338,7 +346,9 @@ function helpSections(k: ReturnType<typeof useKeybinds>) {
{
group: "Panes",
items: [
["h/l", "swipe pane"],
["j/k", "switch tab (tab panel)"],
["l/enter", "enter tab content"],
["h", "back to tab panel"],
["1-6 / [ ]", "switch tabs"],
[":", "command"],
["~", "help"],

View File

@@ -0,0 +1,78 @@
/**
* TabListPane — the tab list as a pane you can drop into the UP | CURRENT |
* PREVIEW flow (replaces the old fixed chrome tab column).
*
* Renders one row per tab (digit + label): the ACTIVE tab gets a ● marker and
* accent fg; the CURSOR row (the one j/k hovers) gets the primary highlight.
* `focused` only matters to the surrounding frame (the CURRENT column draws
* its own accent ring in YaziPaneRow); when rendered as the muted UP/parent
* column (`muted`), the cursor highlight is suppressed and only the active ●
* shows, so it reads as the read-only parent listing.
*/
import { For } from "solid-js";
import { useTheme } from "@/context/ThemeContext";
import { useNavigation } from "@/context/NavigationContext";
import { TABS } from "@/utils/navigation";
const TAB_LABEL: Record<TABS, string> = {
[TABS.FEED]: "Feed",
[TABS.MYSHOWS]: "My Shows",
[TABS.DISCOVER]: "Discover",
[TABS.SEARCH]: "Search",
[TABS.PLAYER]: "Player",
[TABS.SETTINGS]: "Settings",
};
/** Numeric TABS values, in declaration order (1..TabsCount). */
const TAB_ORDER = Object.values(TABS).filter(
(v): v is TABS => typeof v === "number",
) as TABS[];
export function TabListPane(props: { muted?: boolean }) {
const { theme } = useTheme();
const nav = useNavigation();
const cursor = () => nav.tabCursor();
const active = () => nav.activeTab();
const muted = () => props.muted ?? false;
return (
<For each={TAB_ORDER}>
{(tab) => {
const isCursor = () => cursor() === tab && !muted();
const isActive = () => active() === tab;
const fg = () =>
isCursor()
? theme.textSelectedPrimary
: isActive()
? theme.accent
: theme.text;
return (
<box
width="100%"
height={1}
flexDirection="row"
backgroundColor={isCursor() ? theme.primary : "transparent"}
>
<text
width={2}
fg={isCursor() ? theme.textSelectedPrimary : "transparent"}
>
{isActive() ? "●" : " "}
</text>
<text
width={2}
fg={isCursor() ? theme.textSelectedPrimary : theme.textMuted}
>
{tab}
</text>
<text fg={fg()} paddingLeft={1}>
{TAB_LABEL[tab]}
</text>
</box>
);
}}
</For>
);
}