Files
musicmouse/web/src/components/TabRail.tsx
Martin Bauer 2a629faf50 Web frontend: switchable dolphin/dinosaur skins
Two kids, two looks: ?theme=dolphin or ?theme=dinosaur picks one, and the
choice is remembered in the browser so the installed home-screen app keeps it.
Everything that differs per child lives in one Skin object per file under
web/src/lib/skins/; layout and feature toggles in lib/theme.ts stay shared.
The dolphin renders pixel-identical to before.

The hardcoded sea hues (210 music, 175 typing game, 340 accent) are now CSS
variables written once at startup, and the Gecko-friendly sRGB gradient stops
come from the skin. The dinosaur is jungle green with a lava-orange accent,
its own mascot, icons, manifest and browser colour.

Nothing underwater is left in the dinosaur skin: leaves instead of bubbles
(play-view canvas, room page, typing game), a walking mascot, lava blobs in
the bubble game, a hatching egg instead of the treasure chest, dino pets that
walk or fly, a prehistoric speed ladder with the T-Rex as the hero rung, and
renamed games and light colours. Pets and ladder rungs are keyed by the
backend's existing ids, used as slots, so the backend and save format are
unchanged. Pets show as emoji until drawings exist.

python-backend/.gitignore: ignore tippen-curriculum-dino.yml, the dino
device's personal curriculum, like the original.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:05:41 +02:00

81 lines
2.9 KiB
TypeScript

/** The tab rail: icon-only, vertically centered on the right edge, all three sharing
* one frosted glass bar rather than each button floating separately - audio player and
* typing always shown, the smarthome tab only when Home Assistant is configured, same
* gate the old single toggle button used. Replaces that button now that there are
* three destinations instead of two.
*
* Its footprint (this file's own `right`/padding/button-size numbers) is mirrored in
* `lib/theme.ts`'s `TAB_RAIL_CLEARANCE`, which the scrollable views reserve as padding
* on their own right edge so a full-width row or grid never renders underneath it. */
import { PAGE_ICON, PAGE_LABEL, SHOW_GLASS_BLUR } from "../lib/theme";
import type { UiState } from "../lib/keyboard";
interface Props {
page: UiState["page"];
onSelect: (page: UiState["page"]) => void;
showRoom: boolean;
}
const PAGES: readonly UiState["page"][] = ["music", "typing", "room"];
export function TabRail({ page, onSelect, showRoom }: Props) {
const pages = PAGES.filter((candidate) => candidate !== "room" || showRoom);
return (
<div
style={{
position: "absolute",
top: "50%",
right: 16,
transform: "translateY(-50%)",
zIndex: 3,
display: "flex",
flexDirection: "column",
gap: 8,
padding: 8,
borderRadius: 999,
background: "oklch(97% 0.01 var(--hue) / .22)",
backdropFilter: SHOW_GLASS_BLUR ? "blur(14px)" : undefined,
WebkitBackdropFilter: SHOW_GLASS_BLUR ? "blur(14px)" : undefined,
border: "1px solid oklch(97% 0.01 var(--hue) / .3)",
boxShadow: "0 8px 24px oklch(15% 0.05 var(--hue) / .35)",
}}
>
{pages.map((candidate) => {
const active = candidate === page;
return (
<button
key={candidate}
onClick={() => onSelect(candidate)}
aria-label={PAGE_LABEL[candidate]}
aria-current={active}
title={PAGE_LABEL[candidate]}
style={{
width: 44,
height: 44,
display: "flex",
alignItems: "center",
justifyContent: "center",
border: "none",
borderRadius: 999,
background: active ? "var(--accent)" : "transparent",
// Explicit, not inherited: at least one of these icons (the music note)
// renders as a plain glyph rather than full-color on some platforms, and
// without this it fell back to the browser's default text colour - too
// close to the glass bar's own near-white background to read.
color: active ? "#fff" : "var(--ink)",
fontSize: 20,
fontWeight: 900,
cursor: "pointer",
transition: "background 0.15s ease, color 0.15s ease",
}}
>
{PAGE_ICON[candidate]}
</button>
);
})}
</div>
);
}