Menu

Components (prototype)

On this page

Every block type is a component: a props schema, a template and a stylesheet. The native catalogue (heading, text, callout, quiz…) is a set of built-in components shipped with Elabora. A course adds its own in components/<id>/, at two levels:

  • block: a block type placed on screens, in layers and in the slots of other components (a ratio card, a case-study card with a quiz inside, an image quiz);
  • screen: a screen layout ("layout": "<id>" on a screen), with named slots for the blocks of the screen.

Authors edit instances through the props form, move blocks between slots, duplicate instances and pick styles. Agents and developers edit definitions. Nothing in a component runs JavaScript: what must be reliable (scoring, saved state, SCORM records, focus, keyboard, announcements) belongs to the player’s behaviours, and templates only provide the markup.

components/ratio-card/
  component.json     manifest: props schema, slots, fallback, styles, examples, version, parent, origin
  template.html      HTML with bindings and behaviour hooks
  component.css      stylesheet (cascade layer elabora.components, over the theme's defaults)
  migrations.mjs     optional: steps that migrate instance props when the props shape changes

1. Manifest

{
  "id": "ratio-card", "title": "Ratio card", "version": "1.0.0", "category": "content",
  "description": "A key ratio as a large figure…", "instructions": "Keep the label to one or two words…",
  "props": { "type": "object", "additionalProperties": false, "required": ["label", "value", "body"], "properties": {
    "label":  { "type": "string", "maxLength": 40, "x-translatable": true },
    "value":  { "type": "string", "maxLength": 16, "x-translatable": true },
    "body":   { "type": "string", "x-kind": "markdown", "x-translatable": true },
    "accent": { "type": "string", "x-kind": "token", "x-token-prefix": "color.", "x-advanced": true, "default": "color.copper" }
  } },
  "fallback": [ { "type": "text", "props": { "markdown": "**{{label}}: {{value}}**\n\n{{body}}" } } ],
  "examples": [ { "title": "Reduction", "props": { "label": "Reduce", "value": "÷ 3.6", "body": "Ring held, the sun drives the carrier." } } ]
}

props is a JSON Schema of type object. Strings take an x-kind:

x-kind Value In a template
text (default) plain text escaped text
markdown Elabora Markdown, maths included HTML in content (`
tex a LaTeX equation MathML (display; `
asset a file of assets/ assets/<path> (shipped in the package, checked like image assets)
token a theme token name (color.copper) var(--t-color-copper), only in style="--x: {{prop}}"

Mark text x-translatable. The component’s title, description, slot labels and settings’ title and description are what authors read in the editor: the course’s translation covers them too (units component.<id>.…, written in its component.json), so a course made in another language shows them in it; instructions, for agents, stays as written. The inspector lays the settings out from the schema. x-group names a setting’s section, in the course’s language ("x-group": "Figure"): one section per distinct meaning, of two settings or more, never two for the same thing; most components need none, their settings going under « Content ». x-advanced: true marks a setting authors rarely change (a ratio, a colour that follows the charter): it waits under « More settings » while it has its default. Sections are translated with the course (units component.<id>.prop.<setting>.group). An enum holds values for the template (stack, split); x-enum-labels says how each reads in the inspector, in the course’s language: "arrangement": { "enum": ["stack", "split"], "x-enum-labels": { "stack": "Stacked", "split": "Page on the right" }, … }, on a setting or on a field of a list’s items. Its keys are values of the enum (E_COMPONENT_SCHEMA); a value without words shows capitalised, so word them all. They are translated with the course (units component.<id>.prop.<setting>.value.<value>). The editor picks each setting’s control from its schema; x-control asks for one as in free-object manifests: textarea (a few lines of text), slider (a number with minimum and maximum), range (a low and a high number: an array of two numbers whose items have minimum and maximum), vec2 and vec3 (an array of two or three numbers, X, Y and Z on one row), color (a string of format: "color"). In instances, a text or Markdown prop may be { "textRef": "TXT-2" }: the locked text of the plan, used word for word and counted as used.

2. Template

<div class="pg-ratio" style="--ratio-accent: {{accent}}">
  <p class="pg-ratio-label">{{label}}</p>
  <p class="pg-ratio-value">{{value}}</p>
  <div class="pg-ratio-body t-text">{{body}}</div>
</div>
Syntax Meaning
{{path}} a prop (label, item.field); {{.}} the current item
{{@index}}, {{@number}}, {{@count}}, {{@first}}, {{@last}} inside data-each
{{@id}} id of the block (or screen): unique SVG ids, id="g-{{@id}}", fill="url(#g-{{@id}})"
data-if="expr" keep the element when prop, !prop, prop == value or prop != value holds
data-each="list" repeat the element for each item (item fields in scope; data-if on the same element filters)
data-slot="id" empty element receiving the blocks of a slot (not repeated, not conditional)
data-title screen components only: where the screen title (h1) goes
data-t-… behaviour hooks (§3)

Templates are rendered at build time; the player inserts the HTML, fills the slots and binds the behaviours. The validator refuses (E_COMPONENT_TEMPLATE): script, style, iframe, object, form, select, textarea, link, a (links live in Markdown props), media (place a video block in a slot), event handler attributes, external or script URLs, href/src other than SVG fragments, <img> whose src is not a single asset binding or without alt (bind it to a text prop, or alt="" when decorative), buttons and inputs without a hook, h1, style other than custom properties bound to props, bindings to undeclared props. It warns about words in the template (W_COMPONENT_TEXT: make them props or player labels), SVG without aria-hidden (W_COMPONENT_DECORATION), fixed ids (W_COMPONENT_ID_ATTR) and props bound nowhere (W_COMPONENT_PROP_UNUSED).

Write templates in reading order; heading order is checked through templates and slots (E_HEADING_ORDER).

3. Behaviours (headless) and primitives

A fork of quiz, numeric, fill-in, sort, reflection, decision, dialogue, results or speech keeps its behaviour: the player scores it, saves its state under the same key (quiz:<id>, fillin:<id>…), records SCORM interactions, announces feedback and manages focus. The template only has to provide the hooks.

Behaviour Required hooks Optional hooks
quiz data-t-stem, data-t-option="{{id}}" on a <label> repeated with data-each="options", data-t-submit (button), data-t-feedback data-t-group (default: the template root), data-t-input (an <input> in the option; created if absent), data-t-option-feedback and data-t-flag (inside the option, or next to it as ="{{id}}" in the same repetition)
numeric data-t-stem, data-t-input (input), data-t-submit, data-t-feedback data-t-input-label (label), data-t-unit, data-t-expected, data-t-group
decision data-t-prompt, data-t-choice="{{id}}" (button, repeated over options), data-t-feedback data-t-group
fill-in data-t-stem, data-t-text (where its markdown goes: the build leaves a <span data-t-blank="n"> there for each blank), data-t-submit (button), data-t-feedback data-t-group, data-t-bank (where the words to place go; made after the text when absent)
sort data-t-stem, data-t-sort-item="{{id}}" on a <button> repeated with data-each="items", data-t-submit, data-t-feedback data-t-group, data-t-pool (where items wait; default: the first item’s parent), data-t-sort-target="{{id}}" repeated with data-each="targets", holding a <button data-t-place> (the target’s words) and an optional data-t-zone (where its items go; made when absent)
reflection data-t-prompt, data-t-response (where the player builds the field or the scale), data-t-submit (Save), data-t-feedback data-t-group, data-t-model (the expert’s answer, shown once saved; made after the feedback when absent), data-t-download (button: « Mes réponses »)
results at least one of data-t-score, data-t-status, data-t-message data-t-retry (button)
dialogue data-t-log (the thread the player fills), data-t-options (the options’ room) data-t-group, data-t-stage (style scene: the backdrop and the cast), data-t-debrief (the debrief card), data-t-start (button: the start card’s, begins: "click")
speech data-t-line="{{id}}" repeated with data-each="lines" (a bubble) data-t-speaker (where the player writes the speaker’s name), data-t-portrait (where it mounts the portrait)

Speech (« Personnage », D63). The player fills the hooks from the block’s speaker in course.json: the name in data-t-speaker (hidden from screen readers), « Name : » before the first words of each data-t-line (so each bubble reads « Marc : … »), the portrait in data-t-portrait (decorative): the character’s SVG framed on its data-portrait box at rest (its default expression, the mouth closed, data-reduced-motion: nothing breathes nor blinks), its portrait picture, or its initial; the speaker’s colour as --t-speech-color on the block. The built-in styles .t-speech (--speech, --thought, --start, --end), .t-speech-portrait, .t-speech-name, .t-speech-bubble with the dialogues’ dialog.* tokens.

Dialogue. The player owns what goes in the hooks: it gives data-t-log role="log" and fills it with rows (.t-dialogue-row[data-role="character|learner|narration|note|retry|end"][data-side][data-speaker][data-line][data-decision], each with .t-dialogue-bubble, .t-dialogue-name, .t-dialogue-ink and its words .t-w.is-said), gives data-t-options role="group", « Votre réponse » and tabindex="-1" and fills it with the prompt, the options (button.t-dialogue-choice[data-option]), the turn’s controls and the end’s (.t-dialogue-turn, .t-dialogue-endbar), mounts the cast in data-t-stage (created when a scene template has none), the debrief card in data-t-debrief (in the thread when there is none) and its title and label on data-t-start. The group (data-t-group, else the template’s root) gets .t-dialogue-root, data-t-look="messaging|scene", data-t-reveal, and while it plays data-t-paused, data-t-history, data-t-debrief-open. A fork changes everything around the hooks (a title, a frame, the order of thread and options, its own classes) and restyles the rows through these classes in its component.css; it keeps the thread and the options’ room as two elements of their own, the room after the thread.

Empty hooks receive the player’s labels in the course language (Submit, Your answer, Correct answer…): templates stay without words. For styling, options get is-selected, is-correct, is-wrong; the group gets data-t-result="correct|incorrect" (decision: the option id) and data-t-final once the answer is final.

The activities of D63 never hold input, select or textarea in their templates: the player builds them in the hooks, named from its labels (« Trou 1 », « Placer dans « … » », « Monter « … » »). A fill-in’s blanks get .t-blank with is-filled, is-correct, is-wrong and, after the last try, .t-blank-answer; its words .t-word (aria-pressed when chosen, is-used once placed: it keeps its room). A sort’s items get aria-pressed when chosen, is-correct, is-wrong and, after the last try, .t-sort-belongs; the room an item leaves keeps a hidden copy (.t-sort-ghost); order wraps each item in a .t-sort-row (a list item) with its move buttons. Placing is tap first: an item, then where it goes; Escape lets go; a mouse may drag. A reflection’s group gets data-t-saved once saved.

Room for answers (slides). The player renders every possible outcome of an activity (each option’s feedback, misconceptions, retry messages, results statuses) on hidden copies at the block’s width and reserves the tallest as the block’s min-height: answering never moves the slide. Keep the room small: short option feedback, and on slides feedbackStyle: "compact" on a quiz (its feedback without repeating the chosen option’s text, which stays marked where it is and is read to screen readers: about half the room of the feedback; elabora fit names the quizzes it would help when a slide scrolls). A quiz fork’s template may set data-t-feedback-style="compact" on its group to be compact by design. A template whose feedback floats over its own content (an overlay, a popover) opts out with data-t-reserve="off" on any element. elabora fit and elabora screenshot --state answered show the tallest outcome.

Primitives, for any template:

Hook Behaviour
data-t-click on a button fires the screen event click with the block as target (like the built-in button; W_DEAD_BUTTON without event). data-t-click="name" names it, and events can listen to that name only; the name may be bound to a prop (data-each="markers" with data-t-click="{{id}}": one marker per item)
data-t-toggle="k" on a button + data-t-part="k" disclosure: aria-expanded, aria-controls, part hidden until opened (data-t-open shows it first); fires click with the name k
data-t-tabs containing data-t-tab="k" buttons and data-t-panel="k" tabs: roles, selection, arrow keys, Home/End (data-t-selected picks the first); selecting a tab fires click with the name k
data-t-label="submit" the player label of that id (see packages/player/src/labels.ts)
data-t-bleed on a root element of the template the block goes edge to edge, as width: "bleed" does (D63): the window’s width in scroll presentation, the slide’s in slides, its slot’s in a layout’s slot; the player adds t-w-bleed to the block. Keep text inside at the reading width: padding-inline: var(--t-bleed-gutter)
data-t-compare on a container of two elements (before, then after), with optional data-t-compare-label inside each before and after (D63): the player adds a native range over the container (.t-compare-range, 0 to 100, arrows, Home, End, Page Up and Down) that writes where the line is to --t-compare on the container (50% at first); the stylesheet clips the second element there. It is named by the two labels (else the player’s Before and After) and says how much of each shows
data-t-heading on an <h2>–<h6> a heading that follows the page: rendered one level under the heading before the instance in reading order (the screen title is level 1), the other headings of the template marked so keeping their distance to the first. A card with an <h3 data-t-heading> reads as an h2 at the top of a screen and as an h3 under an h2; E_HEADING_ORDER follows the same levels
data-t-sequence on a container; data-t-item="k" on each item; data-t-prev, data-t-next (buttons); optional data-t-goto="k" (buttons), data-t-position, data-t-selected (on the item shown first) one item at a time (D63), the APG carousel with buttons: the other items are hidden, it never moves by itself. Arrow keys work inside it (Home and End too, mirrored right to left) and a horizontal swipe of 40 px on touch. It writes data-t-current="k" on the container and aria-current on the goto button of the item shown; Previous and Next get aria-disabled at the ends and stay in reach; data-t-position receives “2 / 5”; empty Previous and Next buttons get their words from the labels (an aria-label when they hold only an icon), and a goto button without words (a thumbnail whose picture is decorative, a dot) is named “2 of 5”. The items’ container reserves the room of the tallest item, in both presentations, and is read when an item shows (aria-live); the item that comes fades in with a short slide. Each item is a control, visited once shown; showing it fires click with its name
data-t-card="k" holding data-t-face="front" and data-t-face="back"; data-t-flip="k" (button: on a face, in the card or next to it) a flip card (D63, TPGi’s pattern): data-t-flipped on the card, the hidden face inert and aria-hidden, an empty flip button worded by the labels (“Turn the card over”, “Show the front”) and described by the front. The faces share one grid cell (player.css), so the card is as tall as its taller face whichever shows; it turns on its vertical axis, and under reduced motion it cross-fades. The card is a control, visited once turned over; each turn fires click with its name
data-t-zoom on a button holding an <img>; optional data-t-caption inside it or next to it opens the image at full size in the player’s lightbox, a modal dialog named by the image’s alt, with the caption, Close, and previous and next between the zoom buttons of the block (arrow keys too). Tab stays in it, Escape closes it and focus returns to the button of the image shown
data-t-exclusive on a container of data-t-toggle buttons opening one part closes the others. Escape closes the part open and returns focus to its toggle. data-t-exclusive="popover" also closes it on a pointer press outside the container and its parts (the template’s CSS floats the part, so nothing moves)
<details data-t-details="k"> a native disclosure that is a control: visited once opened (or when it comes into view open), each opening or closing fires click with its name
data-t-reveal="k" on an element visited once it has been in view (half of it, or half of the window for a taller one; not in a pending step nor concealed): a timeline’s event, a scrolling step. It fires click with its name the first time
data-t-follow on a container of data-t-reveal elements the page’s scroll followed (D63, a steps block’s scroll): the reveal crossing the middle of the window is current (the last one above it between two, the first before them all), and the player writes data-t-following on the container, data-t-current="k" and --t-current (its rank, from 1) as the page scrolls. Native scroll: nothing holds the page and no scroll event is listened to (an observer tells when a reveal crosses the middle). The stylesheet decides what follows: a sticky figure whose layers read --t-current (opacity: clamp(0, calc(var(--t-current) - var(--layer) + 1), 1) stacks layers 1 to n), shown only with [data-t-following], so that without the player each step keeps its own picture
Controls. The buttons, tabs and disclosures of a block are its controls, named by their hook value (an unnamed data-t-click has no name), and so are the items of a sequence (data-t-item), flip cards (data-t-flip), <details data-t-details> and data-t-reveal elements, which always carry a name. Each is visited once the learner has used it (a tab or disclosure: once its content is shown, so the tab open at first counts when its block comes into view; an item once shown, the first one when its block comes into view; a card once turned over; a reveal once in view): the player sets data-t-visited on it, saves it with the learner’s progress (a key per control in elabora.lock.json, a few characters of suspend data each) and conditions read it ({ "visited": "<block id>" } for all its controls, "name" for one). Names are lowercase letters, digits, -, _, .; the validator checks literal names in the template and bound ones on each instance.

Screen layouts may also use what the player writes about the screen, in the course’s words (D63): data-t-duration (« 6 min », the page’s reading time that the build computes: the plan’s estimate, D50, plus the media played there; named « Temps de lecture » for screen readers). It fills an empty element; a template leaves it out with data-if when it is not wanted. The built-in layout opening uses it. The player writes nothing about where the screen is (no section, no « Leçon 2 sur 6 » above a title, D63 amended): that is the module’s UI, or a number the course’s style sets itself (a part opener’s number).

<details>/<summary> are allowed for native disclosure (no visited state without data-t-details).

Hooks the validator checks (E_COMPONENT_HOOK): a data-t-sequence holds data-t-item elements, a data-t-prev and a data-t-next (no sequence inside another); data-t-item, data-t-prev, data-t-next, data-t-goto and data-t-position sit inside a sequence, and a literal data-t-goto names an item; a data-t-card holds both faces and has a data-t-flip of the same name; data-t-face is front or back, inside a card; data-t-zoom is a button holding an <img>, and data-t-caption needs a zoom; data-t-exclusive is empty or popover and holds data-t-toggle buttons; data-t-details sits on <details>; data-t-prev, data-t-next, data-t-goto, data-t-flip and data-t-zoom sit on <button>; items, cards, details and reveals need a name; a data-t-follow holds data-t-reveal elements, and no other data-t-follow.

Steps that wait for exploration. A screen’s steps.<n>.opens: "explored" shows the Continue of step n (Next in slides) locked until every activity of the steps above is answered and every control of their blocks is visited; a block with optional: true (and what it holds) does not count, nor does it for completeWhen: "interactions-done". Locked, the Continue says what is left under it (“1 question left to answer. 2 cards left to turn over.”, from the player’s labels), and in its tooltip when pressed. Things of one kind are named: cards to turn over (leftCards), panels to open (details, leftPanels), tabs to open (leftTabs), images to see (a gallery’s sequence items, leftImages), cards to see (sequence items that are flip cards, a stack: leftCardsToSee), the steps of a steps block (its timeline’s: events, leftSteps, leftEvents) and the markers of an interactive picture (leftMarkers), forks included, each kind in its own sentence; when a control left does not say what it is, the whole is items to explore (leftToExplore). A tab and a panel of the same name (tabs shown as an accordion in a narrow container) count as what is rendered, and a tab that is not rendered is not visited by being in view. A name is one control whatever holds it: an interactive picture’s marker (data-t-toggle) and its entry in the narrow list (data-t-reveal, the same name) are one, visited by either. Components are not scored by themselves: to score inside a component, fork an activity or place activity blocks in its slots.

Built-in screen layouts. A screen names a built-in layout in layout as it names a course layout: opening (« Ouverture », D63), whose props are in layouts/opening.schema.json and whose slots are question and body. Its template is packages/core/src/components/builtin/opening.html, its stylesheet part of the player’s (elabora.base, under the theme: .t-opening, .t-layout--opening). A course component cannot take its name; it may fork it (extends: { "component": "opening" }, level: "screen") to change its markup, and keeps its stylesheet.

4. Slots, styles and the DOM

"slots": [ { "id": "figure", "label": "Figure and legend", "accepts": ["image", "text"], "maxBlocks": 4 },
           { "id": "body", "label": "Explanation", "default": true } ]

An instance carries the blocks of its slots in blocks, each with slot (default slot otherwise):

{ "id": "case-1", "type": "case-card", "props": { "client": "Kestrel" },
  "blocks": [ { "id": "case-1-q", "type": "quiz", "slot": "check", "props": { "questionRef": "Q-4" } } ] }

accepts lists types; a fork is accepted where its parent is. Blocks in slots are ordinary blocks: events target them, activities count for completion and scoring.

Named styles: styles in component.json (inherited by forks) and theme.styles.<id> in course.json, each { "label": "Dark", "description": "…" } (the name authors pick it by and what it is for) or the description alone. A style declared for a type applies to its forks.

DOM: every block is div.t-block.t-block--<id> plus t-block--<parent> for each parent (a fork keeps the parent’s CSS), [data-type="<id>"], [data-component="<id> <parent>…"] for forks, [data-behavior], [data-style]. A screen layout adds .t-layout.t-layout--<id> and [data-layout] on article.t-screen. Scope component CSS with .t-block--<id> (or .t-layout--<id>). Stylesheets are cascade layer elabora.components, after the player (elabora.base) and the course theme’s rules (elabora.theme), before the theme’s @layer overrides blocks (elabora.overrides); parents’ stylesheets load before their forks’. A component’s rule wins over the theme’s defaults whatever their specificity, so its stylesheet is complete: where the template uses a player hook the theme styles (.t-screen-title, .t-text, .t-figure img), give the component’s own value in component.css. A value the theme should be able to tune is a custom property read with its default (var(--<id>-gap, 1.5rem)), never set by the component (docs/design.md, Cascade layers; D32). In a course without theme.cascade (written before 13 October 2026) the theme still comes after the components.

5. Fallback

Every component can be expressed with native blocks. Exports (LESSON.md), the client review (elabora project --client), text views, translation tools, editors that do not know the component and detach use it.

"fallback": [
  { "type": "heading", "props": { "text": "{{title}}", "level": 3 } },
  { "$if": "body", "type": "text", "props": { "markdown": "{{body}}{{#steps}}\n- {{label}}{{/steps}}" } },
  { "$each": "faq", "type": "callout", "props": { "title": "{{q}}", "markdown": "{{a}}" } },
  { "type": "accordion", "props": { "items": { "$each": "panels", "as": { "id": "p{{@number}}", "title": "{{title}}", "markdown": "{{body}}" } } } },
  { "$slot": "check" }
]

Strings take {{path}}, {{#list}}…{{/list}} (repeat; “if” for a value) and {{^path}}…{{/path}}. A string that is a single {{path}} keeps the raw value: a { "textRef" } stays a plan link. Slot contents go where $slot says, otherwise after. Without fallback, Elabora generates one from the props (text and Markdown in a text block, lists as bullet lists, assets as decorative images, TeX as equations). Forks of a built-in fall back to the built-in with the props it knows (a fork of quiz becomes a quiz; with questionRef, the plan question).

Checks: each fallback block must be valid (E_COMPONENT_FALLBACK); text of an instance that the fallback drops is reported (W_COMPONENT_FALLBACK_LOSS).

6. Forks

{ "id": "config-quiz", "title": "Configuration question", "version": "1.0.0",
  "extends": { "component": "quiz", "version": "0.1.0" },
  "propsExtend": { "properties": {
    "legend":  { "type": "array", "items": { … } },
    "options": { "items": { "properties": { "figure": { "type": "string", "x-kind": "asset" } } } } } } }

elabora component fork quiz config-quiz (MCP elabora_fork_component) copies the template so it can change and records the parent and its version. Until the fork declares them, props, slots, styles, fallback and template come from the parent; propsOmit drops the parent’s props the fork does not use ("propsOmit": ["body", "steps"]: no W_COMPONENT_PROP_UNUSED for them, and instances may no longer give them; a name the parent lacks is refused, so is a prop the parent’s behaviour needs), then propsExtend merges into the parent’s props (a fork of an activity cannot replace them). The parent’s stylesheet still applies. I_COMPONENT_PARENT_CHANGED tells when the parent moved on.

With a plan question (questionRef), a quiz fork may still add presentation fields per option, keyed by option id: the plan’s text, correctness and feedback always win, and options that try to change them are refused (E_QUIZ_SOURCE).

{ "id": "assessment-q3", "type": "config-quiz", "props": { "questionRef": "Q-3",
  "options": [ { "id": "a", "figure": "images/config-sun-carrier.svg" }, … ] } }

Video, audio and free objects are rendered by code: place them in slots, do not fork them.

7. Duplicate, detach, move

  • elabora block duplicate <block> (MCP elabora_duplicate_block): a copy of the instance and of the blocks of its slots, with new ids (<id>-copy), after the original or at a given place. Learner state never carries over: copies are new activities.
  • elabora block detach <block> (elabora_detach_block): replaces the instance with its fallback, then the blocks of its slots. The first block keeps the id, so events and learner state still point at it. Undoable like every edit.
  • elabora_move_block with parent and slot moves blocks into and out of slots; elabora_add_block with parent adds into a slot.

8. Versions and migrations

elabora.lock.json records, per component, the version its instances were written for. When the shape of the props changes, raise version and add a step:

// components/key-card/migrations.mjs
export default {
  '2.0.0': ({ title, ...rest }) => ({ ...rest, heading: title })
};

elabora validate reports E_COMPONENT_MIGRATION_PENDING until elabora component migrate (MCP elabora_migrate_components) runs the steps on every instance and layout, checks every result against the new schema and writes them in one undoable edit; nothing is written if one instance fails. Versions without migration steps (compatible changes) are recorded by the next build.

9. Libraries

A library is a folder shared by several courses (a client’s design system): library.json ({ "id", "title" }) and components/<id>/.

  • elabora component add <id> --library <dir> [--as <id>]: copies the component into the course with origin (library, version, hash). The course is pinned: it never changes when the library does.
  • elabora component add <id> --library <dir> --upgrade: replaces the copy with the library’s version (then elabora component migrate if the props changed). Refused when the copy was edited locally (W_COMPONENT_DIVERGED) unless --force.
  • elabora component publish <id> --library <dir>: copies a course component into the library (refused if the library has the same version with other content).

10. Review

A component is reviewed like a design, not like a contract: screenshots and checks. elabora component preview <id> --width 1280|375 (MCP elabora_preview_component) renders its examples on a specimen screen; examples are checked like instances (E_COMPONENT_EXAMPLE). The build report lists components with version, parent, library origin and instance count. I_COMPONENT_SINGLE_USE: a component used once is often better as a native block with a named style; W_COMPONENT_UNUSED: never used.

11. Who changes what

Authors (editor, no code) Agents and developers
Instances props through the form, design props (tokens, enums), style, slot contents, duplicate, detach, move everything
Definitions — manifest, template, stylesheet, fallback, migrations, forks, library operations