cbd-web: give the panes a tab control

Below 700px the panes stack and only the focused one is open, but switching
was bound to `Tab` and to a tap on the collapsed strip — and a phone has no
`Tab` key. The top bar now carries `library` / `queue` buttons that set the
focus, shown at every width: on desktop they double as the readout of which
pane the keys go to, which the inset border says only faintly.

The collapsed strip is the pane's toolbar with all but the title clipped, so
its buttons were the only thing a tap on it could land on — tapping to switch
panes could hit the library's "play" and replace the queue. Hidden there now.

The connection line truncates rather than pushing the tabs off a narrow
screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test User 2026-07-27 09:23:30 +02:00
parent 1db88e0f5a
commit 8e3c210864
4 changed files with 71 additions and 2 deletions

View File

@ -807,9 +807,30 @@ fn TopBar(store: Store) -> impl IntoView {
save_pref("theme", next); save_pref("theme", next);
apply_theme(next); apply_theme(next);
}; };
// What `Tab` does, as a tap target. On a phone the panes stack and only
// the focused one is open, so switching panes has to be reachable without
// a keyboard. It stays on screen at every width, where it doubles as the
// readout of which pane the keys are going to.
let tab = move |target: Focus, label: &'static str| {
view! {
<button
class="ghost"
class:active=move || store.focus.get() == target
aria-pressed=move || (store.focus.get() == target).to_string()
title="switch pane (Tab)"
on:click=move |_| store.focus.set(target)
>
{label}
</button>
}
};
view! { view! {
<header class="topbar"> <header class="topbar">
<span class="brand">"crabidy"</span> <span class="brand">"crabidy"</span>
<span class="tabs">
{tab(Focus::Library, "library")}
{tab(Focus::Queue, "queue")}
</span>
<span <span
class="conn" class="conn"
class:offline=move || !store.connected.get() class:offline=move || !store.connected.get()

View File

@ -126,12 +126,30 @@ input {
& .conn { & .conn {
font-size: 0.85rem; font-size: 0.85rem;
color: var(--fg-dim); color: var(--fg-dim);
/* Truncate rather than push the pane tabs off a narrow screen. */
min-inline-size: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&.offline { &.offline {
color: var(--danger); color: var(--danger);
} }
} }
& .tabs {
display: flex;
gap: 0.1rem;
padding: 0.1rem;
border: 1px solid var(--border);
border-radius: 8px;
/* Comfortable for a thumb — this is the phone's only pane switch. */
& button {
padding: 0.3rem 0.7rem;
}
}
& .topbar-actions { & .topbar-actions {
margin-inline-start: auto; margin-inline-start: auto;
display: flex; display: flex;
@ -462,8 +480,8 @@ input {
/* ---- phone ------------------------------------------------------------- */ /* ---- phone ------------------------------------------------------------- */
@media (max-width: 700px) { @media (max-width: 700px) {
/* One pane at a time; Tab (or tapping a pane edge) switches the /* One pane at a time; the topbar tabs switch, as does Tab or a tap on
unfocused pane collapses to a slim strip acting as its tab. */ the slim strip the unfocused pane collapses to. */
.panes { .panes {
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: 1fr auto; grid-template-rows: 1fr auto;
@ -475,6 +493,13 @@ input {
overflow: hidden; overflow: hidden;
border-block-start: 1px solid var(--border); border-block-start: 1px solid var(--border);
opacity: 0.75; opacity: 0.75;
/* Only the toolbar survives the collapse, so its buttons are all a
tap on the strip can land on and hitting "play" there replaces
the queue when the user meant to switch panes. Leave the title. */
& .toolbar button {
display: none;
}
} }
.transport { .transport {

View File

@ -21,6 +21,12 @@ buttons in
the transport bar, and the progress bar is clickable — a click seeks to the transport bar, and the progress bar is clickable — a click seeks to
that point in the track. The `/` live filter is TUI-only for now. that point in the track. The `/` live filter is TUI-only for now.
A pair of `library` / `queue` tabs in the top bar switches panes and shows
which one the keys go to — the same thing `Tab` does, reachable without a
keyboard. It matters on a phone: below 700px the two panes cannot sit side
by side, so the unfocused one collapses to a title strip (tapping it also
switches) and the tabs are how you get back.
The volume slider spans the server's whole accepted range, ending at 110% The volume slider spans the server's whole accepted range, ending at 110%
— the server clamps there, so a slider that went further would have a — the server clamps there, so a slider that went further would have a
dead stretch at its right end. Its tooltip reports the current level as a dead stretch at its right end. Its tooltip reports the current level as a

View File

@ -1607,3 +1607,20 @@ headroom, and more gain risks clipping — so the slider was the side to move.
The tooltip also reports the level as a percentage now, which is the web The tooltip also reports the level as a percentage now, which is the web
counterpart of the TUI's `Volume: 85%`. counterpart of the TUI's `Volume: 85%`.
## Web pane tabs (2026-07-27)
Below 700px the panes cannot sit side by side, so the layout already stacked
them and collapsed the unfocused one to a title strip. Switching, though, was
only bound to `Tab` and to a tap on that strip — awkward on a phone, where
there is no `Tab` key.
The top bar now carries a `library` / `queue` pair of buttons that set the
focus. They are shown at every width rather than behind a media query: on
desktop they double as the readout of which pane the keys go to, which the
inset border says only faintly.
The collapsed strip is the pane's toolbar with everything but the title
clipped away, so its buttons were the only thing a tap on it could land on —
tapping to switch panes could hit the library's "play" and replace the queue.
Those buttons are hidden on the collapsed pane now.