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:
@@ -26,9 +26,12 @@ interface Props {
|
||||
style?: CSSProperties;
|
||||
/** Print the title over generated art. Off for thumbnails, where it would not fit. */
|
||||
label?: boolean;
|
||||
/** A reward-gated album/track not yet earned - shows a question mark instead of the
|
||||
* real art or title, regardless of `has_cover`. */
|
||||
locked?: boolean;
|
||||
}
|
||||
|
||||
export function Cover({ album, size, fit = "width", radius, className, style, label }: Props) {
|
||||
export function Cover({ album, size, fit = "width", radius, className, style, label, locked }: Props) {
|
||||
const book = isBook(album);
|
||||
const defaultRadius = book ? "6px 18px 18px 6px" : "16px";
|
||||
const box: CSSProperties =
|
||||
@@ -49,7 +52,7 @@ export function Cover({ album, size, fit = "width", radius, className, style, la
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{!album.has_cover && label && (
|
||||
{locked ? (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -57,35 +60,54 @@ export function Cover({ album, size, fit = "width", radius, className, style, la
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: book ? "10% 14% 10% 20%" : "10%",
|
||||
textAlign: "center",
|
||||
fontSize: Math.max(11, Math.round(size / 11)),
|
||||
fontWeight: 900,
|
||||
lineHeight: 1.15,
|
||||
color: "oklch(99% 0 0 / .92)",
|
||||
fontSize: Math.max(22, Math.round(size / 2.2)),
|
||||
color: "oklch(99% 0 0 / .85)",
|
||||
textShadow: "0 2px 8px oklch(15% 0.05 210 / .6)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{album.title}
|
||||
❓
|
||||
</div>
|
||||
)}
|
||||
{album.has_cover && (
|
||||
<img
|
||||
src={coverUrl(album.id)}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
// Books keep a sliver of spine showing on the right, so the shelf metaphor
|
||||
// survives contact with real square artwork.
|
||||
clipPath: book ? "inset(0 5% 0 0)" : undefined,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{!album.has_cover && label && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: book ? "10% 14% 10% 20%" : "10%",
|
||||
textAlign: "center",
|
||||
fontSize: Math.max(11, Math.round(size / 11)),
|
||||
fontWeight: 900,
|
||||
lineHeight: 1.15,
|
||||
color: "oklch(99% 0 0 / .92)",
|
||||
textShadow: "0 2px 8px oklch(15% 0.05 210 / .6)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{album.title}
|
||||
</div>
|
||||
)}
|
||||
{album.has_cover && (
|
||||
<img
|
||||
src={coverUrl(album.id)}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
// Books keep a sliver of spine showing on the right, so the shelf
|
||||
// metaphor survives contact with real square artwork.
|
||||
clipPath: book ? "inset(0 5% 0 0)" : undefined,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user