import { useState } from "react"; import { useGameMeta } from "../state/gameMeta.tsx"; import type { LaneId } from "../../shared/custom.ts"; import type { View } from "../state/usePrefs.ts"; /** * First run: pick your games, and how you want to read them. * * Asked once, before any events are shown, because a calendar full of games you * don't play is worse than an empty one — it buries the thing you came for. * * Nothing is preselected among the games. An empty state with a disabled button * is clearer than guessing on the reader's behalf and hoping they notice. */ export function Welcome({ available, onConfirm, }: { available: LaneId[]; onConfirm: (chosen: LaneId[], view: View) => void; }) { const gameMeta = useGameMeta(); const [chosen, setChosen] = useState([]); /** * The view is the one thing here that arrives already answered. * * A reader cannot be asked to choose between two layouts they have not seen, * so the question ships with the answer this app is built around — the next * deadline, in one look — and the alternative sitting next to it with a * picture of what it is. Games stay unanswered because only the reader knows * which ones they play; this one has a defensible default. */ const [view, setView] = useState("soon"); const toggle = (id: LaneId) => setChosen((prev) => prev.includes(id) ? prev.filter((g) => g !== id) : [...prev, id], ); return (

EVENTCLOCK

Which games do you play?

You'll only see events for these. Change it any time — nothing is saved anywhere but this browser.

{/* Two columns once there is room for them: a dozen games in one column pushes the view question and the way in off the bottom of the screen, on the one screen where both need to be seen. */}
{available.map((id) => { const game = gameMeta(id); const on = chosen.includes(id); return ( ); })}
{/* The lanes in the timeline sketch are drawn in the hues of the first games on this very screen, because that is what a lane is: a game. Borrowing the urgency ramp for them would teach the wrong colour rule before the reader has seen a single event. */} gameMeta(id).hue)} />

More games are coming as sources are added. Anything you switch on later shows up automatically.

); } /** * Which of the two views to open on. * * The views answer different questions — "what expires next" and "when is * everything" — and which one a reader wants is not derivable from anything we * know about them. It was previously decided for them and then forgotten on * every reload; now it is asked once and remembered (`prefs.view`). * * Each option carries a drawing of itself rather than a description alone: the * words "list" and "timeline" mean nothing until you have seen this app's * version of them, and the miniature is honest about which one is denser. */ function ViewChoice({ value, onChange, hues, }: { value: View; onChange: (view: View) => void; /** Lane colours for the timeline sketch — real games, in their own hues. */ hues: string[]; }) { const options: Array<{ id: View; label: string; hint: string }> = [ { id: "soon", label: "Ending soon", hint: "A list, closest deadline first." }, { id: "timeline", label: "Timeline", hint: "Bars per game, today pinned." }, ]; return (

How do you want to see them?

{options.map((option) => { const on = value === option.id; return ( ); })}
{/* Where the control is, in the reader's own terms — the tabs are 12px text in a corner, which is exactly the thing a first-time reader does not find on their own. */}

You can switch between them any time, from the tabs in the top right.

); } /** Three rows and their meters, at a twelfth of the size. */ function ListSketch() { const rows = [ { width: "72%", ticks: 3, color: "var(--color-critical)" }, { width: "58%", ticks: 6, color: "var(--color-soon)" }, { width: "66%", ticks: 9, color: "var(--color-near)" }, ]; return ( {rows.map((row) => ( {Array.from({ length: 12 }, (_, i) => ( ))} ))} ); } /** Three lanes of bars against the now-rule, at the same size. */ function TimelineSketch({ hues }: { hues: string[] }) { const geometry = [ { left: "4%", width: "44%" }, { left: "28%", width: "52%" }, { left: "12%", width: "70%" }, ]; const bars = geometry.map((bar, i) => ({ ...bar, // Falls back to the neutral tone rather than to a heat colour: a lane is an // identity, and this sketch must never look like it is showing urgency. hue: hues[i] ?? "var(--color-calm)", })); return ( {bars.map((bar) => ( ))} ); }