Improves how reviewer gates render their content, and adds two small
hooks that make the frontend cooperate with the standalone replay server
(also useful as generic primitives).
Gate body rendering:
- IntroBody: gate intros now render paragraphs plus real bullet lists
with a hanging indent and emphasised bullet lead term, filling the
card width (no narrow measure cap). Backed by introFormat (parser) +
IntroBody (renderer), with tests.
- GateArtifactBody: artifact-review gates whose artifact is a bare file
reference (e.g. {kind:"phase", data:{file:"question.md"}}) now fetch
the referenced session document and render its full content, instead
of showing the file reference as text. Falls back to ArtifactView for
inline-content artifacts and unmapped files. Backed by gateArtifact
(resolver) with tests. ArtifactGateWidget passes sessionId through.
- SelectWidget renders intros via IntroBody.
Replay-friendly session exit:
- AppShell: a system_event {event:"session_exit"} now triggers
stopSession(), returning to the landing view. Generic primitive (the
real backend can emit it too); used by the replay server's "Esci".
- SessionMenu: the ⋮ trigger is now always visible (was opacity-0 /
group-hover only), so Resume is reachable without hovering.
Verified offline against the replay server (no VPN needed); tsc clean.
48 lines
1.6 KiB
TypeScript
48 lines
1.6 KiB
TypeScript
import { parseIntroBlocks, splitLead } from "./introFormat";
|
|
|
|
test("splits paragraphs and bullet lists on blank lines", () => {
|
|
const blocks = parseIntroBlocks(
|
|
"Domanda: «X».\n\nEcco le famiglie:\n• uno → A\n• due → B\n\nConclusione."
|
|
);
|
|
expect(blocks).toEqual([
|
|
{ kind: "para", text: "Domanda: «X»." },
|
|
{ kind: "para", text: "Ecco le famiglie:" },
|
|
{ kind: "list", items: ["uno → A", "due → B"] },
|
|
{ kind: "para", text: "Conclusione." },
|
|
]);
|
|
});
|
|
|
|
test("joins soft-wrapped paragraph lines into one flowing paragraph", () => {
|
|
const blocks = parseIntroBlocks("prima riga\nseconda riga");
|
|
expect(blocks).toEqual([{ kind: "para", text: "prima riga seconda riga" }]);
|
|
});
|
|
|
|
test("accepts -, * and • as bullet markers", () => {
|
|
const blocks = parseIntroBlocks("- a\n* b\n• c");
|
|
expect(blocks).toEqual([{ kind: "list", items: ["a", "b", "c"] }]);
|
|
});
|
|
|
|
test("bolds the lead term up to an arrow separator", () => {
|
|
expect(splitLead("fact_impianto → IMPIANTO INIZIALE (dettaglio)")).toEqual({
|
|
lead: "fact_impianto",
|
|
rest: " → IMPIANTO INIZIALE (dettaglio)",
|
|
});
|
|
});
|
|
|
|
test("bolds the lead term up to a colon separator", () => {
|
|
expect(splitLead("Nota: i controlli non sono impianto")).toEqual({
|
|
lead: "Nota",
|
|
rest: ": i controlli non sono impianto",
|
|
});
|
|
});
|
|
|
|
test("does not bold when there is no separator", () => {
|
|
expect(splitLead("un punto elenco semplice")).toBeNull();
|
|
});
|
|
|
|
test("does not bold a long leading clause", () => {
|
|
expect(
|
|
splitLead("questa è una frase lunga con molte parole prima del: due punti")
|
|
).toBeNull();
|
|
});
|