Add a variant
Render one section two ways, with the case seeded by facts or AI and switchable by the user unless locked.
Render one section two ways, with the case seeded before fill ever runs — from the property’s own facts, or by an AI decision — and switchable by the user in the editor unless you lock it.
Prerequisites
Section titled “Prerequisites”- A section to add the variant to — Install scaffolds one.
- The two things the cases render — Add a repeating list the user edits per
item adds a
unitscollection, and Have AI write a slot’s content adds a prose slot. - Its
fixtures.tsbuilt from a scenario, so both cases have facts to resolve against — Pick a fixture scenario.
A variant is a presentation choice the user may overturn — something decides its
starting case, and the editor then offers the switch unless the section says
otherwise. Declare it under display:, a sibling of slots, as a select over
its cases. Add select and compute to the section’s existing kit import
first — neither is one of the names the scaffolded file takes.
// section.ts — a `display` key, beside `slots`display: { variant: select(["single", "multi"], { source: compute((f) => (f.property.units.length > 1 ? "multi" : "single")), }),},Three rules do all the work:
- The first case is the default — and the posture on failure. There is no
defaultparameter; order the cases so the first one is the one you want when nothing decides otherwise. - Exactly one origin.
source: compute(fn)seeds the case deterministically from the derive-time facts view — the same view, and the same purity rules, as a slot’scompute(): no clock, no randomness, no fetch.fill: chooseFrom({ pick })hands the choice to the AI fill pass instead, steered by thepickprose; the model picks only among the cases you declared. Apickhas to name the evidence it judges — a bucket the sectionreads:, or an interpolated${facts.*}— because the call is handed no other decision’s output (Display). Declare one or the other, never both. - Users switch variants by default. The seeded case is a starting point;
the editor renders a selector and the user’s pick sticks. Add
locked: truewhen the split is not an opinion — this section’s single-vs-multi follows the building’s unit count, so it locks:
// section.ts — a deterministic, non-negotiable splitdisplay: { variant: select(["single", "multi"], { source: compute((f) => (f.property.units.length > 1 ? "multi" : "single")), locked: true, }),},The renderer receives the resolved case, already decided, as its variant
prop — narrowed to the union of the declared cases. Destructure it beside
slots in the component’s signature, then branch on it:
// Renderer.tsx — one section, two shapes<div> {variant === "multi" ? ( <Slot.List slot={slot.units} as="ul" className="divide-y"> {bindItems(slot.units).map((unit) => ( <SlotGroup slot={unit} as="li" key={unit.index}> <Slot.Text slot={unit.fields.unit_label} as="span" /> </SlotGroup> ))} </Slot.List> ) : ( <Slot.Text slot={slot.summary} as="p" /> )}</div>Add bindItems and SlotGroup to Renderer.tsx’s kit import for the collection
branch.
The conditional is an addition, not a replacement. It goes beside the markup the
section already renders — the standalone Slot.List
the collection guide adds stays exactly where it is. Branch over
what the section shows extra. Move an ungated collection inside a case instead and
check fails on markers no fixture state repairs: a record list’s per-row markers
are proven fixture by fixture, so every fixture that resolves the other case leaves its
own rows unmarked.
A slot only one case shows gates on that case with when: — a bare case string,
or an array for a slot two cases share (when: ["single", "multi"]). The build
gate checks every when: against the declared cases, so a mistyped case fails
check rather than shipping a slot that quietly never fills.
That gate is also the trap worth planning for. Every fixture resolves to the
FIRST case unless it says otherwise, so the multi branch’s markup renders in
neither preview nor check until some fixture reaches it — and a slot the
renderer shows only under multi reaches no fixture’s marker coverage at all.
Add a state that selects the other case with .at("multi") — Preview a
section’s structural edge cases is where .at() lands — and
both branches get rendered, checked, and previewable.
Complete diff
Section titled “Complete diff”Two additions across section.ts and Renderer.tsx, plus select and
compute on the section’s existing kit import and variant on the renderer’s
own props destructure. The second addition goes beside the markup already
there, not swapped in for it — and the fixture state that renders the other
case is one more addition, in Preview a section’s structural edge
cases:
// section.ts — beside `slots`, at the top level of `defineSchema`display: { variant: select(["single", "multi"], { source: compute((f) => (f.property.units.length > 1 ? "multi" : "single")), }),},// Renderer.tsx — added beside the markup already there, wherever the branch belongs<div> {variant === "multi" ? ( <Slot.List slot={slot.units} as="ul" className="divide-y"> {bindItems(slot.units).map((unit) => ( <SlotGroup slot={unit} as="li" key={unit.index}> <Slot.Text slot={unit.fields.unit_label} as="span" /> </SlotGroup> ))} </Slot.List> ) : ( <Slot.Text slot={slot.summary} as="p" /> )}</div>Verify
Section titled “Verify”Verify with the author loop.
Rules that can fire
Section titled “Rules that can fire”no-nondeterminism— acomputeorigin reads the clock or a random source instead of the facts it is handed.missing-slot-marker— a slot the renderer shows only under one case, with no fixture that resolves to that case, never reaches the DOM with its marker in any fixture.
See also
Section titled “See also”- Display — the full contract: visibility, the pinned vocabulary, gating a slot on a case.
- Preview edge cases — the fixture state that renders the other case.
- Add a repeating list the user edits per item — the
collection the
multicase renders.