Merge the typing game into the music player as a tab, with lock/unlock UI

Moves tippen from a standalone app into web/ as a third tab (audio player /
smarthome / typing), replacing the old single room-toggle corner button with a
vertical icon tab rail. Curriculum and progress now come from the backend
(musicmouse/tippen/*) instead of a build-time YAML import and localStorage.

Adds reward-driven lock rendering: Cover/BrowseView/AlbumModal show a question
mark for locked albums/tracks with a hint on what unlocks them, and
ResultSheet gets a new unlock-animation block alongside the existing
lesson-unlock and aquarium-creature celebrations.

CSS from the two apps is merged carefully: identical rules (bubble/card/
key-cap/view-enter/backdrop-enter and their keyframes) are shared as-is,
while rules that bake in each app's own hue are kept separate under a
`tp-` prefix and scoped to the typing tab's own .tp-stage wrapper, so
neither app's look bleeds into the other's.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 21:25:42 +02:00
parent f7a5d24d8d
commit 7f5e2733c2
84 changed files with 1577 additions and 4642 deletions

View File

@@ -12,6 +12,7 @@
import type { CSSProperties } from "react";
import type { Group } from "./search";
import type { UiState } from "./keyboard";
// ---------------------------------------------------------------- toggles --
@@ -74,6 +75,21 @@ export const GROUP_LABEL: Record<Group, string> = {
podcasts: "Podcasts",
};
/** Icon/label for the vertical tab rail - one entry per `UiState["page"]`. The
* smarthome tab only renders when Home Assistant is configured (see `App.tsx`); the
* other two always show. */
export const PAGE_ICON: Record<UiState["page"], string> = {
music: "🎵",
room: "💡",
typing: "⌨️",
};
export const PAGE_LABEL: Record<UiState["page"], string> = {
music: "Musik",
room: "Mein Zimmer",
typing: "Tippen",
};
/** A shelf/row's frosted background and border, tinted with its group's hue - or,
* with `SHOW_ROW_TINT` off, `{}` so `.glass-panel`'s own neutral CSS shows through. */
export function glassTint(hue: number): CSSProperties {