The reworked artifact made every Korean word on the lesson screen a way in to the dictionary, in three tiers. This ports them. 1. The underline. Every Korean run in a message, a gloss or an exercise is a button whose underline says what the word is to him: amber new, blue learning, jade in review, faint when secure or merely explainable, dotted when nothing knows it. A form carries its dictionary word's state — 갔어 is 가다's — through the one resolver the gate uses (domain/words.ts). The artifact gave "in review" no colour at all. 2. The popover, beside the word: the meaning, "+ Add to deck" (the dictionary entry, so 먹었어 adds 먹다), "Ask 선생님" for a word he was shown without being taught, a search, the word list. English on an exercise's English side opens its Korean, from an index of the course's own material — never the frequency bands, whose thousands of glosses would bury the answer. An answer chip's tap belongs to the exercise, so a 420ms hold glosses it, or a tap with 힌트 on; the hold's click is swallowed before the exercise sees it. Pressing a word never takes focus from the answer being typed, and the popover no longer closes when the window resizes — on a phone that was the keyboard moving. 3. The word list gains + / ✓ on every row, search results included. Every lookup, whichever tier, goes into the answer's "I had to look up" and the peek tally — counted under the Korean looked at; the artifact counted an English lookup under the English word. A dictionary word added this way is studied: a card on an entry outside the curated sources now counts as a deck word, in review and in "my units". Before, it got a card that no review would ever show. The word list glosses through the same resolver as the popover and the gate, rather than the surface table alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
84 lines
2.5 KiB
TypeScript
84 lines
2.5 KiB
TypeScript
/* The colour-coded sentence breakdown.
|
|
|
|
Each chunk is a column: 한글 on top, a micro-gloss underneath, and a role
|
|
carried by the underline. The optional fourth field highlights the
|
|
meaningful piece INSIDE the word — the particle, the tense marker, the
|
|
ending — so the grammatical morpheme reads distinctly from the stem. */
|
|
|
|
import { Fragment } from "react";
|
|
import type { GlossBlock as Block, GlossPart, GlossRole } from "@lib/blocks.js";
|
|
import { ROLE_STYLES, isRole, legendFor } from "./roles.js";
|
|
import { KoText } from "./lookup.js";
|
|
import "./gloss.css";
|
|
|
|
/** Wrap the LAST occurrence of the highlight, which is where a suffix sits. */
|
|
function withHighlight(ko: string, highlight: string) {
|
|
const at = highlight ? ko.lastIndexOf(highlight) : -1;
|
|
if (at < 0) return <KoText text={ko} />;
|
|
return (
|
|
<>
|
|
<KoText text={ko.slice(0, at)} />
|
|
<em>
|
|
<KoText text={highlight} />
|
|
</em>
|
|
<KoText text={ko.slice(at + highlight.length)} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
function Word({ part }: { part: GlossPart }) {
|
|
const role: GlossRole = isRole(part.role) ? part.role : "N";
|
|
const style = ROLE_STYLES[role];
|
|
|
|
return (
|
|
<span
|
|
className="gw"
|
|
data-role={role}
|
|
data-underline={style.underline}
|
|
style={
|
|
{
|
|
"--role-color": `var(${style.color})`,
|
|
"--role-bg": `var(${style.bg})`,
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
<span className="k ko">{withHighlight(part.ko, part.highlight)}</span>
|
|
{part.gloss && <span className="g">{part.gloss}</span>}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function GlossBlocks({ blocks }: { blocks: Block[] }) {
|
|
const used = legendFor(
|
|
blocks.flatMap((b) => b.parts.map((p) => (isRole(p.role) ? p.role : "N"))),
|
|
);
|
|
|
|
return (
|
|
<div className="gloss-set">
|
|
{blocks.map((b, i) => (
|
|
<div className="gloss" key={i}>
|
|
<div className="gloss-line">
|
|
{b.parts.map((p, j) => (
|
|
<Word part={p} key={j} />
|
|
))}
|
|
</div>
|
|
{b.en && <div className="gloss-en">{b.en}</div>}
|
|
</div>
|
|
))}
|
|
|
|
{used.length > 0 && (
|
|
<div className="gloss-key">
|
|
{used.map((r) => (
|
|
<Fragment key={r}>
|
|
<span className="key-item">
|
|
<i style={{ background: `var(${ROLE_STYLES[r].color})` }} />
|
|
<span className="ko">{ROLE_STYLES[r].label}</span>
|
|
</span>
|
|
</Fragment>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|