From 9e1ffa0c852b7e44cc5fc049acb7a03f12964999 Mon Sep 17 00:00:00 2001 From: ncode Date: Thu, 17 Sep 2026 10:43:19 +0300 Subject: [PATCH] Phase 2: the Drill screen App now owns the panel once and each tab supplies what is lit, what the screen says, and what fills the strip and the 84px card box. Drill adds its strip (deck plus the five Leitner bars) and its card, which has three states: press this combo, name this combo, or the verdict. Two defects fixed that only rendering could show. Dealing the first card happened during render. React tolerated it and the capture looked fine, but dealing picks at random and StrictMode renders twice, so it was fragile rather than wrong today. It is an effect now. The card box overflowed by 8.5px and clipped CLEAR and CHECK. The box is 84px, which leaves 68px once borders and padding are removed, and a 10px button with 5px padding and a 2px border is 26px tall - two rows of those plus the title do not fit. Buttons are 21px now, and the stylesheet records the budget for all three states so the next change has to do the arithmetic rather than guess. Verified by rendering the asking state and the answered state, the latter by temporarily forcing a graded card and restoring the source afterwards. Typecheck clean, 132 tests. Co-Authored-By: Claude Opus 5 --- src/App.tsx | 191 ++++++++++++++++++++++++----------- src/ui/DrillCard.module.css | 168 ++++++++++++++++++++++++++++++ src/ui/DrillCard.tsx | 123 ++++++++++++++++++++++ src/ui/DrillStrip.module.css | 65 ++++++++++++ src/ui/DrillStrip.tsx | 53 ++++++++++ 5 files changed, 542 insertions(+), 58 deletions(-) create mode 100644 src/ui/DrillCard.module.css create mode 100644 src/ui/DrillCard.tsx create mode 100644 src/ui/DrillStrip.module.css create mode 100644 src/ui/DrillStrip.tsx diff --git a/src/App.tsx b/src/App.tsx index fe2e634..8c01409 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,12 @@ /** - * Explore: pick a mode and a layer, then read what the panel lights up. + * The shell. It owns the panel — there is only ever one — and each tab supplies + * what is lit, what the screen says, and what fills the strip and the card box. * - * Angular: useState is roughly a component field plus change detection. Setting - * it re-runs this function, so derived values are recomputed each render; - * useMemo is the explicit opt-out when that stops being cheap. + * Angular: useReducer is the closest thing to a service holding state with + * methods, except dispatch returns a new state rather than mutating one. */ -import { useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useReducer, useState } from "react"; +import { DECKS, boxDistribution, deckPool, knownCount, pickCard } from "./domain/drill"; import { ENTRIES } from "./domain/entries"; import { controlName } from "./domain/format"; import { CTX, CTXHINT } from "./domain/labels"; @@ -16,13 +17,16 @@ import { orderLayerKeys, } from "./domain/layers"; import type { ContextCode, ControlId, Entry } from "./domain/types"; +import { DrillCard } from "./ui/DrillCard"; +import { DrillStrip } from "./ui/DrillStrip"; import { InfoCard } from "./ui/InfoCard"; import { Panel } from "./ui/Panel"; +import { drillReducer, initialDrillState } from "./ui/drillReducer"; import styles from "./App.module.css"; const TABS = ["Explore", "Drill", "Find", "Rules"] as const; +type Tab = (typeof TABS)[number]; -/** Drawn, not typed: caret glyphs sit outside every Latin font subset. */ function Caret() { return (
-
- - - -
+ {isDrill ? ( + undefined} + /> + ) : ( +
+ + +
+ )} e.short), + lit={isDrill ? drillLit : exploreLit} + held={isDrill ? drill.chord : activeLayer === "" ? [] : activeLayer.split("+")} + selected={isDrill ? null : (selected?.tset[0] ?? null)} + onPress={(id) => { + if (isDrill) { + if (drill.answered) deal(); + else dispatch({ type: "press", id }); + return; + } + const here = exploreLit.get(id); + if (!here || here.length === 0) { + setSelected(null); + return; + } + const at = selected ? here.indexOf(selected) : -1; + setSelected(here[(at + 1) % here.length]!); }} + screen={ + isDrill + ? { + top: DECKS[drill.deck].name.toUpperCase(), + right: drill.answered ? (drill.lastCorrect ? "CORRECT" : "MISS") : "", + main: drill.card ? drill.card.name : "—", + chips: [], + } + : { + top: CTX[ctx].toUpperCase(), + right: layerLabel(activeLayer), + main: selected ? selected.short : `${onLayer.length} functions`, + chips: selected ? [] : onLayer.slice(0, 3).map((e) => e.short), + } + } /> - + {isDrill ? ( + dispatch({ type: "check" })} + onClear={() => dispatch({ type: "clear" })} + onReveal={() => dispatch({ type: "reveal" })} + onNext={deal} + onPickOption={(name) => dispatch({ type: "pickOption", name })} + /> + ) : ( + + )}