fix: a NUL byte was hiding two source files from grep, git and diff

TaskHost.tsx used a literal NUL as the delimiter in its drag-and-drop
payload, and tools/dict/build.mjs used one to join headword and part of
speech. Both work at runtime. Both also make the file *binary* to every
text tool: git shows "Bin 12259 bytes" instead of a diff, and grep prints
nothing at all for a match.

That is not hypothetical. Searching TaskHost.tsx for "<input" came back
empty three times while reviewing it, which is how its four exercise
inputs came to be reported as absent -- and why the accessibility defect
in them went unseen. Written as the escape \u0000 the value is identical
and the file stays text.

test/source-hygiene.test.ts fails on any control byte in a source file, so
this cannot come back quietly.

With the files readable again, the sweep the NUL had been hiding: eleven
form controls had no accessible name. The exercise blanks announced only
an ellipsis, and the part-of-speech select announced nothing. A
placeholder is not a label -- it disappears the moment you type. All
eleven now carry one, named after the thing they answer.

`npm run lint` gains --max-warnings 0. exhaustive-deps is configured as a
warning, so a hooks-dependency bug would have passed CI silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-09-08 21:06:39 +02:00
parent c93ea657f9
commit 0b04931699
7 changed files with 58 additions and 2 deletions

View File

@@ -152,6 +152,7 @@ function ConjugationTrainer() {
className="ko"
value={value}
placeholder="…"
aria-label={`Conjugate ${question.dict}${mode === "past" ? "past 반말" : "반말"}`}
onChange={(e) => {
composer.onExternalInput();
setValue(e.target.value);

View File

@@ -108,6 +108,7 @@ export function VocabTab() {
type="search"
value={query}
placeholder="Search 한글 or English…"
aria-label="Search your words in 한글 or English"
onChange={(e) => setQuery(e.target.value)}
/>
<button className="btn" onClick={() => setAdding((a) => !a)} aria-expanded={adding}>
@@ -142,16 +143,19 @@ export function VocabTab() {
className="ko"
value={draft.headword}
placeholder="한글"
aria-label="The word, in 한글"
required
onChange={(e) => setDraft({ ...draft, headword: e.target.value })}
/>
<input
value={draft.gloss}
placeholder="What it means"
aria-label="What it means"
required
onChange={(e) => setDraft({ ...draft, gloss: e.target.value })}
/>
<select
aria-label="Part of speech"
value={draft.pos}
onChange={(e) => setDraft({ ...draft, pos: e.target.value })}
>

Binary file not shown.

View File

@@ -146,6 +146,7 @@ export function WordRail({ words, revealed, onReveal }: WordRailProps) {
type="search"
value={query}
placeholder="Look a word up…"
aria-label="Look a word up"
onChange={(e) => setQuery(e.target.value)}
/>
</div>