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 <noreply@anthropic.com>
124 lines
3.4 KiB
TypeScript
124 lines
3.4 KiB
TypeScript
/**
|
|
* Drill's card area, in the same 84px box Explore uses for its info card.
|
|
*
|
|
* Three states: asking you to press a combo, asking you to name one, or showing
|
|
* the verdict. Feedback is orange or an outline — the palette has no green or red.
|
|
*/
|
|
import { comboParts } from "../domain/format";
|
|
import type { Entry } from "../domain/types";
|
|
import type { DrillState } from "./drillReducer";
|
|
import { cx } from "./cx";
|
|
import styles from "./DrillCard.module.css";
|
|
|
|
export interface DrillCardProps {
|
|
readonly state: DrillState;
|
|
readonly onCheck: () => void;
|
|
readonly onClear: () => void;
|
|
readonly onReveal: () => void;
|
|
readonly onNext: () => void;
|
|
readonly onPickOption: (name: string) => void;
|
|
}
|
|
|
|
function Chips({ entry }: { entry: Entry }) {
|
|
return (
|
|
<span className={styles.combo}>
|
|
{comboParts(entry).map((part, i) => (
|
|
<span key={part}>
|
|
{i > 0 ? <i className={styles.plus}>+</i> : null}
|
|
<b className={styles.chip}>{part}</b>
|
|
</span>
|
|
))}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function DrillCard({
|
|
state,
|
|
onCheck,
|
|
onClear,
|
|
onReveal,
|
|
onNext,
|
|
onPickOption,
|
|
}: DrillCardProps) {
|
|
const { card, chord, answered, lastCorrect, direction, input } = state;
|
|
|
|
if (!card) {
|
|
return (
|
|
<div className={styles.card}>
|
|
<p className={styles.body}>No cards in this deck.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (answered) {
|
|
return (
|
|
<div className={styles.card}>
|
|
<div className={styles.meta}>
|
|
<span className={cx(styles.verdict, !lastCorrect && styles.verdictMiss)}>
|
|
{lastCorrect ? "CORRECT" : "NOT QUITE"}
|
|
</span>
|
|
<span>P.{card.page}</span>
|
|
</div>
|
|
<div className={styles.row}>
|
|
<Chips entry={card} />
|
|
<button className={styles.primary} onClick={onNext}>
|
|
NEXT
|
|
</button>
|
|
</div>
|
|
<p className={styles.body}>{card.desc || card.name}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (direction === "name") {
|
|
return (
|
|
<div className={styles.card}>
|
|
<div className={styles.meta}>
|
|
<Chips entry={card} />
|
|
<span>WHICH IS IT?</span>
|
|
</div>
|
|
<div className={styles.options}>
|
|
{state.options.map((name) => (
|
|
<button key={name} className={styles.option} onClick={() => onPickOption(name)}>
|
|
{name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={styles.card}>
|
|
<div className={styles.meta}>
|
|
<span className={styles.ellipsis}>
|
|
{input === "hold" ? "HOLD THE COMBO" : "TAP EACH, THEN CHECK"}
|
|
</span>
|
|
<button className={styles.ghost} onClick={onReveal}>
|
|
SHOW ME
|
|
</button>
|
|
</div>
|
|
<h2 className={styles.title}>{card.name}</h2>
|
|
<div className={styles.row}>
|
|
<span className={styles.pressed}>
|
|
{chord.length === 0 ? "—" : chord.join(" + ").toUpperCase()}
|
|
</span>
|
|
{input === "tap" ? (
|
|
<span className={styles.row}>
|
|
<button className={styles.ghost} onClick={onClear} disabled={chord.length === 0}>
|
|
CLEAR
|
|
</button>
|
|
<button
|
|
className={styles.primary}
|
|
onClick={onCheck}
|
|
disabled={chord.length === 0}
|
|
>
|
|
CHECK
|
|
</button>
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|