Web frontend: hide app title and row titles, regroup help sheet, drop Ctrl+d/u

- New SHOW_APP_TITLE switch (off): the mascot + title header goes, the status pill
  stays on a slim row of its own.
- SHOW_ROW_TITLES off; with only icons, the shelf icon moves into a column left of
  the tiles so an icon-only shelf is no taller than one without.
- Help sheet regrouped into sections in a compact two-column grid, and reachable
  from a small keyboard button (not on phones or the typing page).
- Ctrl+d/Ctrl+u half-page jumps removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 21:53:06 +02:00
parent 5af5c54c62
commit 6981890be0
7 changed files with 180 additions and 106 deletions

View File

@@ -1,5 +1,7 @@
import type { ReactNode } from "react";
import { SHOW_APP_TITLE } from "../lib/theme";
interface Props {
title?: string;
mascot?: string;
@@ -15,6 +17,25 @@ export function AppHeader({
status,
compact = false,
}: Props) {
if (!SHOW_APP_TITLE) {
// Still clear the translucent standalone-mode status bar, and keep the status
// pill - it is the only place an offline connection shows on these screens.
return (
<div
style={{
display: "flex",
justifyContent: "center",
padding: status
? `calc(${compact ? 8 : 10}px + env(safe-area-inset-top)) 16px 0`
: "env(safe-area-inset-top) 0 0",
flex: "none",
}}
>
{status}
</div>
);
}
const mascotSize = compact ? 44 : 64;
return (
<div

View File

@@ -225,15 +225,20 @@ function BrowseViewImpl({
? "4px solid var(--paper)"
: undefined,
outlineOffset: 2,
// Without a title the icon has no header to sit in, so it takes a column
// of its own beside the tiles rather than a row above them.
...(SHOW_ROW_ICONS && !SHOW_ROW_TITLES
? { display: "flex", alignItems: "center", gap: 12 }
: {}),
...glassTint(GROUP_HUE[shelfGroup]),
}}
>
{(SHOW_ROW_TITLES || SHOW_ROW_ICONS) && (
{SHOW_ROW_TITLES && (
<div
style={{
display: "flex",
alignItems: "center",
gap: SHOW_ROW_TITLES && SHOW_ROW_ICONS ? 10 : 6,
gap: 10,
marginBottom: 12,
fontSize: 20,
fontWeight: 900,
@@ -241,39 +246,44 @@ function BrowseViewImpl({
}}
>
{SHOW_ROW_ICONS && <span className="group-icon">{GROUP_ICON[shelfGroup]}</span>}
{SHOW_ROW_TITLES && GROUP_LABEL[shelfGroup]}
{SHOW_ROW_TITLES && <span style={{ opacity: 0.6, fontSize: 17 }}>›</span>}
{GROUP_LABEL[shelfGroup]}
<span style={{ opacity: 0.6, fontSize: 17 }}>›</span>
</div>
)}
{shelfCategories.length === 0 ? (
<div
style={{
textAlign: "center",
fontSize: 14,
fontWeight: 700,
color: "oklch(88% 0.02 210 / .6)",
}}
>
Noch nichts hier
</div>
) : (
<ShelfRow focusedIndex={focusedRow === index ? focusedCol : null}>
{shelfCategories.map((entry, tileIndex) => (
<CategoryTile
key={entry.key}
entry={entry}
navIndex={tileIndex}
selected={focusedRow === index && focusedCol === tileIndex}
onClick={(event) => {
// Otherwise this bubbles to the panel's own onClick, which would
// open the group root instead of the category just clicked.
event.stopPropagation();
onEnterGroup(shelfGroup, entry.key);
}}
/>
))}
</ShelfRow>
{SHOW_ROW_ICONS && !SHOW_ROW_TITLES && (
<span className="group-icon">{GROUP_ICON[shelfGroup]}</span>
)}
<div style={{ flex: 1, minWidth: 0 }}>
{shelfCategories.length === 0 ? (
<div
style={{
textAlign: "center",
fontSize: 14,
fontWeight: 700,
color: "oklch(88% 0.02 210 / .6)",
}}
>
Noch nichts hier
</div>
) : (
<ShelfRow focusedIndex={focusedRow === index ? focusedCol : null}>
{shelfCategories.map((entry, tileIndex) => (
<CategoryTile
key={entry.key}
entry={entry}
navIndex={tileIndex}
selected={focusedRow === index && focusedCol === tileIndex}
onClick={(event) => {
// Otherwise this bubbles to the panel's own onClick, which would
// open the group root instead of the category just clicked.
event.stopPropagation();
onEnterGroup(shelfGroup, entry.key);
}}
/>
))}
</ShelfRow>
)}
</div>
</div>
))}
</div>

View File

@@ -1,66 +1,104 @@
/** The Zaubertasten sheet (F1). Mirrors lib/keyboard.ts - if a binding changes there,
* it changes here. */
const KEYS: Array<[caps: string[], label: string]> = [
[["LEER"], "Play / Pause"],
[["SHIFT+H", "SHIFT+L"], "Song zurück / vor"],
[["SHIFT+J", "SHIFT+K"], "Leiser / lauter"],
[["SHIFT+M"], "Stumm"],
[["⇧←", "⇧→"], "15 Sekunden zurück / vor"],
[["← ↑ ↓ →"], "Auswahl bewegen"],
[["CTRL+H/J/K/L"], "Auswahl bewegen (Raster oder Titelliste)"],
[["CTRL+D", "CTRL+U"], "Eine halbe Seite weiter / zurück"],
[["A-Z"], "Album oder Hörbuch suchen"],
[["?"], "Einzelne Titel suchen"],
[["TAB"], "Musik / Hörbücher / Podcasts"],
[["ENTER"], "Auswahl abspielen"],
[["⇧ENTER"], "Titelliste öffnen"],
[["A", "0-9"], "Aktuellen Titel einer Fernbedienungs-Taste zuweisen"],
[["F1"], "Diese Hilfe"],
[["ESC"], "Schließen / Suche löschen"],
import { Fragment } from "react";
const SECTIONS: Array<[title: string, rows: Array<[caps: string[], label: string]>]> = [
[
"Abspielen",
[
[["LEER"], "Play / Pause"],
[["⇧H", "⇧L"], "Song zurück / vor"],
[["⇧J", "⇧K"], "Leiser / lauter"],
[["⇧M"], "Stumm"],
[["⇧←", "⇧→"], "15 Sekunden zurück / vor"],
],
],
[
"Auswählen",
[
[["← ↑ ↓ →"], "Auswahl bewegen"],
[["STRG+HJKL"], "Auswahl bewegen"],
[["ENTER"], "Auswahl abspielen"],
[["⇧ENTER"], "Titelliste öffnen"],
[["TAB"], "Musik / Hörbücher / Podcasts"],
],
],
[
"Suchen",
[
[["A-Z"], "Album oder Hörbuch suchen"],
[["?"], "Einzelne Titel suchen"],
[["/"], "Suche öffnen / Alben ↔ Titel"],
],
],
[
"Sonstiges",
[
[["A", "0-9"], "Titel auf Fernbedienungs-Taste legen"],
[["F1"], "Diese Hilfe"],
[["ESC"], "Schließen / Suche löschen"],
],
],
];
const CAP_STYLE = { minWidth: 0, padding: "3px 7px", fontSize: 11 } as const;
export function HelpOverlay({ onClose }: { onClose: () => void }) {
return (
<div className="overlay" style={{ zIndex: 5 }} onClick={onClose}>
<div className="sheet" style={{ padding: "26px 30px", width: 330 }}>
<div className="sheet" style={{ padding: "18px 22px" }}>
<div
style={{
fontSize: 17,
fontSize: 15,
fontWeight: 800,
color: "var(--ink)",
marginBottom: 14,
marginBottom: 4,
letterSpacing: 0.3,
}}
>
⌨️ Zaubertasten
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{KEYS.map(([caps, label]) => (
<div key={label} style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={{ display: "flex", gap: 4 }}>
{caps.map((cap) => (
<div key={cap} className="key-cap" style={{ minWidth: caps.length > 1 ? 0 : 56 }}>
{cap}
</div>
))}
</div>
<div style={{ fontSize: 14, fontWeight: 700, color: "oklch(35% 0.03 210)" }}>
{label}
</div>
</div>
))}
</div>
<div
style={{
marginTop: 16,
fontSize: 12,
fontWeight: 600,
color: "oklch(50% 0.03 210 / .8)",
textAlign: "center",
display: "grid",
gridTemplateColumns: "max-content max-content",
columnGap: 14,
rowGap: 5,
alignItems: "center",
}}
>
Tippe irgendwohin, um zu schließen
{SECTIONS.map(([title, rows]) => (
<Fragment key={title}>
<div
style={{
gridColumn: "1 / -1",
marginTop: 10,
fontSize: 10,
fontWeight: 800,
textTransform: "uppercase",
letterSpacing: 1,
color: "oklch(50% 0.03 210 / .8)",
}}
>
{title}
</div>
{rows.map(([caps, label]) => (
<Fragment key={label + caps.join()}>
<div style={{ display: "flex", gap: 3, justifyContent: "flex-end" }}>
{caps.map((cap) => (
<span key={cap} className="key-cap" style={CAP_STYLE}>
{cap}
</span>
))}
</div>
<div style={{ fontSize: 13, fontWeight: 600, color: "oklch(35% 0.03 210)" }}>
{label}
</div>
</Fragment>
))}
</Fragment>
))}
</div>
</div>
</div>