ui cleanup

This commit is contained in:
2026-08-07 13:38:32 -04:00
parent c8d29ed59d
commit 85cb9fba26
24 changed files with 1839 additions and 1375 deletions

View File

@@ -19,7 +19,6 @@ import { useNavigation, NavMode } from "@/context/NavigationContext";
import { useAudio } from "@/hooks/useAudio";
import { useAudioNavStore, AudioSource } from "@/stores/audio-nav";
import { useFeedStore } from "@/stores/feed";
import type { Episode } from "@/types/episode";
import { useToast } from "@/ui/toast";
import { emit } from "@/utils/event-bus";
import { LayerGraph } from "@/utils/layer-graph";
@@ -200,8 +199,16 @@ export function Shell() {
useKeyboard(
(evt: any) => {
// Input fields (search boxes, dialogs) own their keys.
if (nav.inputFocused() && nav.mode() !== NavMode.COMMAND) return;
// Input fields (search boxes, dialogs) own their keys — except Escape,
// which defocuses the input so j/k/h navigation resumes (search: h back
// to the tab root, j/k to move the recent-searches list).
if (nav.inputFocused() && nav.mode() !== NavMode.COMMAND) {
if (evt.name === "escape") {
evt.preventDefault();
nav.setInputFocused(false);
}
return;
}
if (nav.mode() === NavMode.COMMAND) {
handleCommandKey(evt);
return;
@@ -460,8 +467,9 @@ export function playEpisodeAndSwitch(
) {
audio.play(episode);
nav.setActiveTab(TABS.PLAYER);
nav.enterTabContent(); // PLAYER is a depth-tab — drop into its content pane.
useAudioNavStore().setSource(AudioSource.FEED);
}
// Re-export Episode type for callers building pane trees.
export type { Episode };
export type { Episode } from "@/types/episode";

View File

@@ -2,12 +2,17 @@
* 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.
* Renders one row per tab (digit + label) using the same selection UI every
* other yazi pane uses: the CURSOR row (the one j/k hovers) gets a `` marker
* and the focus background (`theme.primary` when this pane is the CURRENT
* column, `theme.border` when it is the muted UP/parent column). The ACTIVE
* tab (the one whose content is open) always carries a `●` marker in accent so
* it stays readable in both positions.
*
* `muted` marks the parent-column rendering: the highlight is dimmed (border
* bg, text fg) rather than suppressed, so the Up pane still shows the cursor
* and active tab — matching how every other pane's parent column renders its
* focused row.
*/
import { For } from "solid-js";
@@ -34,18 +39,32 @@ export function TabListPane(props: { muted?: boolean }) {
const nav = useNavigation();
const cursor = () => nav.tabCursor();
const active = () => nav.activeTab();
const muted = () => props.muted ?? false;
const activeTab = () => nav.activeTab();
/** `active=true` when this pane is the CURRENT column (Shell root);
* `false` when it is the muted UP/parent column (pages' parent pane). */
const active = () => !props.muted;
// Same focus-bg / focus-fg contract every other pane uses.
const focusBg = (t: TABS) =>
t === cursor() && active()
? theme.primary
: t === cursor()
? theme.border
: undefined;
const focusFg = (t: TABS) =>
t === cursor() && active() ? theme.surface : theme.text;
return (
<For each={TAB_ORDER}>
{(tab) => {
const isCursor = () => cursor() === tab && !muted();
const isActive = () => active() === tab;
const fg = () =>
const isCursor = () => cursor() === tab;
const isActive = () => activeTab() === tab;
// The active tab is only accented in the Up/parent position — when this
// pane is CURRENT, the cursor highlight is the only highlight.
const labelFg = () =>
isCursor()
? theme.textSelectedPrimary
: isActive()
? focusFg(tab)
: isActive() && !active()
? theme.accent
: theme.text;
return (
@@ -53,21 +72,13 @@ export function TabListPane(props: { muted?: boolean }) {
width="100%"
height={1}
flexDirection="row"
backgroundColor={isCursor() ? theme.primary : "transparent"}
paddingRight={1}
backgroundColor={focusBg(tab)}
>
<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}>
{/* ── selection marker (j/k cursor) ─────────────────────────── */}
<text fg={focusFg(tab)}>{isCursor() ? "" : " "}</text>
<text fg={isCursor() ? focusFg(tab) : theme.textMuted}>{tab}</text>
<text fg={labelFg()} paddingLeft={1}>
{TAB_LABEL[tab]}
</text>
</box>

View File

@@ -11,7 +11,7 @@
* parent — the previous-depth list. Renders a muted `—` placeholder and
* KEEPS its 1/7 slot when blank (never collapses to width 0).
* current — the current-depth list. The only focusable content column; it
* carries the accent focus ring when `focused` is truthy.
* carries the active-border focus ring when `focused` is truthy.
* preview — detail of the hovered item in `current`; always muted border.
*
* The primitive is purely structural: callers pass their own JSX per column
@@ -31,7 +31,7 @@
* />
*/
import { createMemo } from "solid-js";
import { createMemo, Show } from "solid-js";
import type { JSX } from "solid-js";
import type { RGBA } from "@opentui/core";
import { useTheme } from "@/context/ThemeContext";
@@ -47,15 +47,19 @@ export type YaziPaneRowProps = {
parent?: PaneContent;
/** Current column content (the focused list). */
current?: PaneContent;
/** Preview column content (detail of the hovered item). */
/** Preview column content (detail of the hovered item). Omit/undefined
* together with `panes={2}` to render a 2-pane parent|current row. */
preview?: PaneContent;
parentLabel?: PaneLabel;
currentLabel?: PaneLabel;
previewLabel?: PaneLabel;
/** Whether the current column carries the accent focus ring. Defaults to
/** Whether the current column carries the active-border focus ring. Defaults to
* true; pass `false` (or a signal) when the row is inactive. Parent and
* preview columns always render muted borders. */
focused?: boolean | (() => boolean);
/** Number of visible columns. `3` (default) = parent|current|preview;
* `2` = parent|current (preview omitted, current grows to fill). */
panes?: 2 | 3;
};
// ── Helpers ─────────────────────────────────────────────────────────────────
@@ -107,7 +111,12 @@ function YaziPane(props: {
const scrollFocused = createMemo(() => props.scrollFocused());
return (
<box flexDirection="column" flexGrow={props.grow} flexBasis={0} height="100%">
<box
flexDirection="column"
flexGrow={props.grow}
flexBasis={0}
height="100%"
>
{/* ── slim header label row ─────────────────────────────────────────── */}
<box height={1} paddingLeft={1} backgroundColor={theme.background}>
<text fg={theme.textSecondary}>{props.label()}</text>
@@ -143,10 +152,10 @@ function YaziPane(props: {
export function YaziPaneRow(props: YaziPaneRowProps) {
const { theme } = useTheme();
/** true → the current column gets the accent focus ring. */
/** true → the current column gets the active-border focus ring. */
const focused = createMemo(() => {
const f = props.focused;
return typeof f === "function" ? f() : f ?? true;
return typeof f === "function" ? f() : (f ?? true);
});
// Normalize static JSX and accessor children into reactive accessors
@@ -159,6 +168,15 @@ export function YaziPaneRow(props: YaziPaneRowProps) {
const currentLabel = createMemo(() => resolveLabel(props.currentLabel));
const previewLabel = createMemo(() => resolveLabel(props.previewLabel));
// 2-pane mode (parent|current) grows the current column to fill the
// preview slot. Defaults to 3 (parent|current|preview).
const panes = createMemo(() => props.panes ?? 3);
const currentGrow = createMemo(() =>
panes() === 2
? PANE_RATIO.current + PANE_RATIO.preview
: PANE_RATIO.current,
);
return (
<box flexDirection="row" flexGrow={1} width="100%" height="100%">
{/* ── parent (1/7) — previous-depth list; always muted ─────────────── */}
@@ -169,22 +187,24 @@ export function YaziPaneRow(props: YaziPaneRowProps) {
borderColor={() => theme.border}
scrollFocused={() => false}
/>
{/* ── current (3/7) — the focused list; accent ring when focused ───── */}
{/* ── current — the focused list; active-border ring when focused ──────────── */}
<YaziPane
grow={PANE_RATIO.current}
grow={currentGrow()}
label={currentLabel}
content={currentContent}
borderColor={() => (focused() ? theme.accent : theme.border)}
borderColor={() => (focused() ? theme.borderActive : theme.border)}
scrollFocused={() => focused()}
/>
{/* ── preview (3/7) — hovered-item detail; always muted ────────────── */}
<YaziPane
grow={PANE_RATIO.preview}
label={previewLabel}
content={previewContent}
borderColor={() => theme.border}
scrollFocused={() => false}
/>
<Show when={panes() === 3}>
<YaziPane
grow={PANE_RATIO.preview}
label={previewLabel}
content={previewContent}
borderColor={() => theme.border}
scrollFocused={() => false}
/>
</Show>
</box>
);
}