"use client"; import type { Mode } from "@/lib/types"; export type Reasons = { bad: string[]; dirty: string[]; other: number }; export type Choice = { verdict: number; label: string; hint: string; key: string; tone: "good" | "dirty" | "bad" | "plain"; /** Which reason list level 2 should show, or null to commit immediately. */ reasons: "bad" | "dirty" | null; }; // What a verdict MEANS is the only thing that differs between modes. In sort, // "unclean" parks a clip for salvage; in salvage the same key accepts it as // deliberately dirty. One card, one prop. // // ORDER MATTERS, and it is the ARROW KEYS' order, not the verdict numbers': // left to right the row reads <- down (up) ->, so the button under your finger // is the button in that direction. The row used to run good / unclean / not an // um -- best first -- which put "good" on the LEFT while its key was the RIGHT // arrow, so the picture and the keyboard disagreed on every card. export function choicesFor(mode: Mode): Choice[] { if (mode === "keeps") { return [ { verdict: 0, label: "not a keep", hint: "drop it", key: "←", tone: "bad", reasons: null }, { verdict: 2, label: "trailing", hint: "“um and” — word at the END", key: "↓", tone: "dirty", reasons: null }, { verdict: 3, label: "both sides", hint: "a word each side", key: "↑", tone: "plain", reasons: null }, { verdict: 1, label: "leading", hint: "“and um” — word at the START", key: "→", tone: "good", reasons: null }, ]; } if (mode === "salvage") { return [ { verdict: 0, label: "unsalvageable", hint: "nothing to rescue", key: "←", tone: "bad", reasons: "bad" }, { verdict: 2, label: "deliberately dirty", hint: "keep, placed against a rest", key: "↓", tone: "dirty", reasons: "dirty" }, { verdict: 1, label: "cleaned, accept", hint: "the window fixes it", key: "→", tone: "good", reasons: null }, ]; } if (mode === "verify") { return [ { verdict: 0, label: "drop", hint: "out of the palette", key: "←", tone: "bad", reasons: "bad" }, { verdict: 2, label: "dirty", hint: "keep, but penalised", key: "↓", tone: "dirty", reasons: "dirty" }, { verdict: 1, label: "good, keep", hint: "the flag was wrong", key: "→", tone: "good", reasons: null }, ]; } return [ { verdict: 0, label: "not an um", hint: "reject", key: "←", tone: "bad", reasons: "bad" }, { verdict: 2, label: "unclean", hint: "park it for salvage", key: "↓", tone: "dirty", reasons: "dirty" }, { verdict: 1, label: "good", hint: "into the palette", key: "→", tone: "good", reasons: null }, ]; } const TONE: Record = { good: "border-[var(--color-good)] text-[var(--color-good)] hover:bg-[color-mix(in_srgb,var(--color-good)_14%,transparent)]", dirty: "border-[var(--color-dirty)] text-[var(--color-dirty)] hover:bg-[color-mix(in_srgb,var(--color-dirty)_14%,transparent)]", bad: "border-[var(--color-bad)] text-[var(--color-bad)] hover:bg-[color-mix(in_srgb,var(--color-bad)_14%,transparent)]", plain: "border-[var(--color-line)] text-[var(--color-text)] hover:bg-[var(--color-panel-2)]", }; type Props = { mode: Mode; reasons: Reasons; /** null = level 1 (the verdicts). Non-null = level 2 (that verdict's reasons). */ pending: Choice | null; onChoose: (c: Choice) => void; onReason: (code: number, text?: string) => void; onBack: () => void; freeText: string; onFreeText: (t: string) => void; /** * The clip's detail (and so its window) has arrived. Until it has, a verdict * has nothing to attach a window to and the write would be dropped SILENTLY — * the same failure mode as a click that lands before hydration. Show it as * disabled rather than letting a press do nothing. */ ready: boolean; }; // Two levels, never both at once. // // The reason buttons used to STACK under the verdict buttons, growing the card // until it overflowed. Replacing the row instead of adding to it is what stops // the card growing -- and the same number keys work at both levels, so the // keyboard path is unchanged. export default function VerdictRow({ mode, reasons, pending, onChoose, onReason, onBack, freeText, onFreeText, ready, }: Props) { if (!pending) { return (
{choicesFor(mode).map((c) => ( ))}
); } const list = pending.reasons === "dirty" ? reasons.dirty : reasons.bad; return (
why {pending.label}?   Esc backs out
{list.map((why, i) => ( ))}
onFreeText(e.target.value)} onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Enter" && freeText.trim()) onReason(reasons.other, freeText.trim()); if (e.key === "Escape") onBack(); }} placeholder="…or a note (Enter). Prefer the word selector — notes are not filterable." className="flex-1 rounded-md border border-[var(--color-line)] bg-[var(--color-panel-2)] px-3 py-2 text-sm outline-none focus:border-[var(--color-sel)]" />
); }