Problem Synthesis Phase
A synthesis card. The caller supplies a prompt and an ordered list of components — each one a slot the student must articulate (the invariant, the recurrence, the base case). The student types into each slot, hits Synthesise, and the canonical phrasing reveals beneath each input. The card emits onSynthesize(values, blankIds) so the parent can score, advance phases, or persist the synthesis text.
Preview
Synthesise insertion sort. Articulate the three components a correctness argument needs.
What never changes across iterations?
Customize
Behaviour
Installation
npx shadcn@latest add https://craftbits.dev/r/problem-synthesis-phase.jsonUsage
import { ProblemSynthesisPhase, type ProblemSynthesisComponent } from "@craft-bits/core";
const COMPONENTS: ReadonlyArray<ProblemSynthesisComponent> = [
{
id: "invariant",
label: "Loop invariant",
placeholder: "Describe the property that stays true on every iteration…",
answer: "Every element in the prefix arr[0..i] is in its final sorted position.",
},
{
id: "recurrence",
label: "Recurrence",
answer: "sorted(i) = insert(arr[i]) into sorted(i − 1).",
multiline: true,
},
{
id: "base",
label: "Base case",
answer: "A one-element prefix is trivially sorted.",
},
];
<ProblemSynthesisPhase
prompt="Synthesise insertion sort. Articulate the three components a correctness argument needs."
components={COMPONENTS}
onSynthesize={(values, blankIds) => {
console.log("captured", values, "blanks", blankIds);
}}
/>Controlled — lift both the value map and the reveal flag so you can score, persist, or advance phases:
const [values, setValues] = useState<Record<string, string>>({});
const [revealed, setRevealed] = useState(false);
<ProblemSynthesisPhase
prompt="Articulate the three components."
components={COMPONENTS}
values={values}
onValuesChange={setValues}
revealed={revealed}
onSynthesize={(captured) => {
setRevealed(true);
persistSynthesis(captured);
}}
requireAll
/>Read-only review — render the canonical answers without letting the student re-edit:
<ProblemSynthesisPhase
prompt="Synthesis (review)"
components={COMPONENTS}
values={savedValues}
revealed
tone="success"
/>Understanding the component
- Edit, then reveal. Each component renders as an editable slot. Hitting Synthesise locks the inputs and fades in the canonical
answerbeneath each one — coloured with the chosen tone ramp. - Controlled + uncontrolled, twice. Both the value map (
values+onValuesChange+defaultValues) and the reveal flag (revealed) follow the Radix pattern. Either may be omitted; the component keeps internal state for whichever you skip. - Submit gating.
requireAllkeeps the submit button disabled until every slot has a non-empty value. With it off, the student can submit a partial synthesis and the secondonSynthesizeargument lists the ids they left blank. - Multiline slots. Set
multiline: trueon any component to render atextareainstead of aninput. Useful for recurrences, derivations, or anything longer than a phrase. - Summary banner. Provide a
summaryReactNode to land the lesson — restate the takeaway, link forward, or surface a code bridge. It fades in alongside the reveals on a success-tone banner. - Reduced motion. Reveal banners, submit press feedback, and summary entrance collapse to instant under
prefers-reduced-motion: reduce. The locking + reveal logic still runs.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
prompt | ReactNode | required | Synthesis prompt rendered at the top of the card. |
components | ReadonlyArray<ProblemSynthesisComponent> | required | Ordered list of slots the student must articulate. |
values | Record<string, string> | — | Controlled value map keyed by component id. |
defaultValues | Record<string, string> | {} | Uncontrolled initial values. |
onValuesChange | (next) => void | — | Fires on every keystroke with the next value map. |
onSynthesize | (values, blankIds) => void | — | Fires on submission with the captured values + ids left blank. |
revealed | boolean | — | Controlled reveal flag. When true, slots lock and answers show. |
submitLabel | ReactNode | "Synthesise" | Label for the primary submit button. |
requireAll | boolean | false | Keep submit disabled until every slot has a value. |
tone | "default" | "accent" | "success" | "warning" | "error" | "accent" | Tone for the reveal banner accent. |
header | ReactNode | — | Content rendered above the prompt. |
summary | ReactNode | — | Reveal-time summary rendered below the slots. |
transition | Transition | SPRINGS.smooth | Override reveal transitions. Reduced-motion users snap regardless. |
className | string | — | Merged onto the root via cn(). |
ProblemSynthesisComponent
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier — keyed in the values map. |
label | ReactNode | Slot label rendered above the input. |
answer | ReactNode | Canonical phrasing revealed after submission. |
placeholder | string | Placeholder text for the empty input. |
helperText | ReactNode | Optional muted helper line under the input. |
multiline | boolean | Render as textarea instead of input. Default false. |
Accessibility
- The slot list is wrapped in
role="group"and labelled by the prompt id so screen readers navigate the slots as a single synthesis unit. - Every slot input is connected to its label via
htmlFor+id, exposes a focus-visible accent ring, and enforces a 44 px minimum hit area regardless of label length. - Reveal banners use
role="status"+aria-live="polite"so each canonical phrasing announces without stealing focus, and the same applies to the summary banner. - Tone is never the only signal — the canonical answer renders on a tinted background with a coloured left border so colour-blind users still perceive the reveal.
- The submit button exposes
aria-disabledwhenever therequireAllgate is unmet, so assistive tech surfaces the unmet precondition. - Motion respects
prefers-reduced-motion: reduce— reveal banner enter/exit and submit tap scale collapse to instant.
Credits
- Extracted from:
AlgoFlashcards(src/lessons/primitives/interaction/ProblemSynthesisPhase.tsx). The source bound the phase touseMultiQuiz, aLessonContextusePhaseGate,playSound, atrackHexconfidence meter, projectRichText, and a multi-question MCQ loop. The library extract recasts the phase as a construction interaction (editable slot per synthesis component, with amultilinetoggle) instead of a recognition quiz, lifts both the value map and the reveal flag into Radix-style controlled / uncontrolled APIs, routes the per-track accent through the--cb-*token ramp, swaps the bespoke spring + confidence meter forSPRINGS.smooth+ a single success-tone summary banner, and surfacesonSynthesize(values, blankIds)so consumers can score, persist, or advance phases on top of it.