Template Forge

A fill-in-the-blanks template builder. The caller passes a single template string containing {{slot}} placeholders, plus an ordered list of slots — each slot carries a label and its own bank of option strings. The student taps an option to fill the active blank; tapping a filled blank clears it. Controlled (values + onValuesChange) and uncontrolled (defaultValues) on the Radix pattern.

Generic enough to cover any "build the template by picking values for named blanks" interaction — exact-match vs boundary-search comparison, loop-condition / hi-update / return-value triples, recurrence skeletons, DP base cases — without baking in scoring, phases, or audio. The caller compares the emitted values record against its accepted answers.

while () { const mid = lo + Math.floor((hi - lo) / 2); if (arr[mid] < target) lo = mid + 1; else ; } return ;
Loop condition
Customize
Highlight
1
Behaviour

Installation

npx shadcn@latest add https://craftbits.dev/r/template-forge.json

Usage

import {
  TemplateForge,
  type TemplateSlot,
} from "@craft-bits/core";
 
const template = `while (BLANK1) {
  const mid = lo + Math.floor((hi - lo) / 2);
  if (arr[mid] < target) lo = mid + 1;
  else BLANK2;
}
return BLANK3;`;
 
const slots: TemplateSlot[] = [
  {
    id: "condition",
    label: "Loop condition",
    options: ["lo <= hi", "lo < hi"],
  },
  {
    id: "hiUpdate",
    label: "hi update",
    options: ["hi = mid - 1", "hi = mid"],
  },
  {
    id: "returnVal",
    label: "Return value",
    options: ["-1", "lo"],
  },
];
 
<TemplateForge template={template} slots={slots} />

(In the real template, replace BLANK1 / BLANK2 / BLANK3 with {{condition}} / {{hiUpdate}} / {{returnVal}} — placeholder syntax is omitted here so this code block stays valid MDX.)

Controlled — parent owns the values record and validates against accepted answers:

const [values, setValues] = useState({});
 
<TemplateForge
  template={template}
  slots={slots}
  values={values}
  onValuesChange={setValues}
/>

Read-only summary — show the accepted template without letting the user edit:

<TemplateForge
  template={template}
  slots={slots}
  values={acceptedAnswer}
  editable={false}
  tone="success"
/>

Understanding the component

  1. Active slot, then fill. The first empty slot in template order is "active" — its option bank renders below the template. Tap an option to fill the active blank; the next empty slot becomes active automatically.
  2. Templates are strings, not trees. The template is a plain string; the component parses {{slot}} placeholders into drop targets and renders the surrounding text verbatim in a monospace block. Whitespace and newlines are preserved, so multi-line code reads as expected.
  3. Each slot brings its own bank. Slots can offer completely different option sets — the bank refreshes when the active slot changes. This is what lets one component teach exact-match vs boundary-search by reusing the same shape with different option sets.
  4. Controlled + uncontrolled. values + onValuesChange is the Radix controlled pattern. defaultValues lets the component own values internally. If neither is provided, all slots start empty.
  5. Values shape. values is a record keyed by slot id — null for empty, otherwise the chosen option string. The component initialises missing keys to null so caller bugs never crash the layout.
  6. Unknown placeholders are visible. A {{foo}} placeholder without a matching slot renders in the error tone instead of disappearing silently, so caller mismatches surface during development.
  7. Hit target. Every blank and option enforces a 44 × 44px minimum hit area (per WCAG 2.5.8) — short fills still tap on mobile.
  8. Reduced motion. Slot enter, fill bounce, and the placeholder breathing collapse to instant under prefers-reduced-motion: reduce. The values still update; only the motion drops.

Props

PropTypeDefaultDescription
templatestringrequiredTemplate string with {{slot}} placeholders. Each placeholder name must match a slots[].id.
slotsTemplateSlot[]requiredOrdered list of slots — each carries an id, label, options, and optional placeholder.
valuesRecord<string, string | null>Controlled values keyed by slot id. Pair with onValuesChange.
defaultValuesRecord<string, string | null>Uncontrolled initial values.
onValuesChange(next: Record<string, string | null>) => voidFires with the next values record on every fill or clear.
editablebooleantrueWhen false, template and bank are non-interactive.
tone"default" | "accent" | "success" | "warning" | "error""accent"Highlight palette for the active slot, filled fills, and focus ring.
headerReactNodeContent rendered above the template.
footerReactNodeContent rendered below the option bank.
transitionTransitionSPRINGS.smoothOverride slot / option transitions. Reduced-motion users snap regardless.
classNamestringMerged onto the outer <div> via cn().

Accessibility

  • The template wrapper is role="group" with an explicit aria-label, so screen readers announce "code template with blanks to fill" before the line-by-line content.
  • Each blank is a <button> with an explicit aria-label that includes the slot label and either its current value or "active, choose a value below." / "not yet reached."
  • The option bank is role="listbox" with an aria-label keyed to the active slot's label; each option is a role="option" button keyboard-activated via Space or Enter.
  • Empty, not-yet-reached blanks are disabled — focus moves to the active blank or to the bank, so keyboard users never land on a dead target.
  • All interactive targets enforce a 44 × 44px minimum hit area (per WCAG 2.5.8 AAA) regardless of label width.
  • Blanks expose data-state (empty / active / filled) and data-slot carrying the slot id, so consumer apps can hook custom styles or per-slot validation feedback.
  • Tone is never the only signal — active and filled states layer fill, stroke, and ring changes so colour-blind users see the distinction.
  • Unknown {{placeholder}} names render in the error tone so missing-slot bugs are visible to sighted reviewers; screen readers announce them as plain text.
  • Motion respects prefers-reduced-motion: reduce — slot enter, fill bounce, and the placeholder breathing collapse to instant.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/construction/TemplateForge.tsx). The source was a 1500-line four-act lesson component bundling a linear-search pain phase, a tile-drag template assembly with distractors, a per-iteration trace with prediction gates, an exact-match vs boundary-search comparison, sound cues, and a weighted act-by-act scoring rollup. The library extract keeps only the construction primitive — a template string with {{slot}} placeholders, per-slot option banks, controlled / uncontrolled values, five tones — and lets the caller compose any acts, validation, traces, or sound on top via the header / footer slots.