Element
A node in a page or component tree — Primitive or Component kind, with props, styles, and optional repeat.
An Element is one node in a page or component tree. Kind + props + styles, optional repeat and renderIf. It belongs to exactly one container — a page or a component, never both, never the app directly.
packages/modules/element/core/src/domain/element.model.types.ts
packages/config/db/src/element.schema.ts
The canvas is this tree. There is no parallel source of truth.
What it is
Kind is a tagged union:
{ _tag: "Primitive", value: "section" } # registry: HTML, coss, codelab.image, …
{ _tag: "Component", value: "<componentId>" } # user component; list_components firstKnown primitive names must use _tag: "Primitive". You cannot pass "Button" as a Component.
packages/modules/element/kernel/src/element-kind.enum.ts
packages/modules/element/core/src/application/add-element.schema.ts
| Field | Meaning |
|---|---|
props | Kind-specific and custom values: children, href, className, instance fields, event prop slots. A slot is a literal or a {{…}} expression. Event slots store a FunctionModel (functionId + instanceParams). |
styles | Responsive style state. MCP writes these with style_*, not add_element. On insert, bulk Tailwind goes in className. |
repeatable / repeatSource | When true, the node repeats once per item of repeatSource — a complete {{…}} that resolves to an array. Inside the subtree: {{repeat.item}}, {{repeat.index}}, {{repeat.key}}. Render expands Repeat copies; you author one node. |
renderIf | Tagged {{…}} boolean. Falsy → this node and its subtree are omitted. null = always render. |
parentId / siblingPosition | Tree edges. You almost never set these raw — parentElementId + targetPosition on add/move. |
packages/modules/element/core/src/domain/tree/tree.model.ts
packages/modules/action/kernel/src/function.model.ts
packages/modules/expression/core/src/domain/expression.namespaces.ts
The display name in outlines and search is aria-label → component name → kind value.
packages/modules/element/core/src/domain/element.model.ts
Tree mutations take:
container: { _tag: "Page", id: pageId }
# or { _tag: "Component", id: componentId }packages/modules/container/core/src/domain/container-kind.model.ts
How it relates
- Page / component — the two containers.
add_elementwith noparentElementIdappends under that container's root. - Primitive vs Component kind — a primitive is a registry widget (
div,Button,Dialog). A Component kind is an instance of a user component; its props are that instance's values. - Props / Function — settable keys are prop slots. Event slots store one FunctionModel; sequences live on a store Action.
- App — theme tokens and kind styles paint every element of a kind. Per-element
style_*is the last layer.
MCP tools
packages/modules/mcp/server/src/element.mcp.ts
packages/modules/mcp/server/src/registry.mcp.ts
Read first
| Tool | When |
|---|---|
list_element_types | Index by category (layout, form, overlay, …). Call once per group; pass the returned kind to add_element. Do not pass category to add. |
inspect_element_type | Required before any composite primitive. Canonical composition + pitfalls. |
list_examples | Copy-paste patterns by kind / intent when the inspect doc is not enough. |
load_page_outline / load_component_outline | Whole tree. Heavy — prefer find when you can. |
find_element_in_page / find_element_in_component | Label / kind / text → ids. |
inspect_element | One node's props, styles, renderIf, repeat. |
Write
| Tool | When |
|---|---|
add_root_element | First node of an empty component tree. Pages already have body. Root kind = Primitive only. |
add_element | One child. parentElementId omitted → under the root. |
add_elements | A subtree in one persist. Parent command before child; mint elementId on parents so later commands can point at them. Prefer this over N add_element calls. |
update_props | Prop slots: children, href, src, instance fields, expressions, FunctionModel bindings. Set/unset commands — not a full replace. |
update_element | Node fields: kind, repeatable, repeatSource, renderIf. |
style_* | Incremental, breakpoint-aware style (style_layout, style_typography, style_spacing_padding, …). Discover keys with style_schema. |
move_element | Reparent / reorder. Same targetPosition rules. Cannot move the root or into itself. |
remove_element | Delete nodes. mode: "withReparent" unwraps a wrapper and keeps children. This is not delete_component. |
duplicate_element_tree | Deep copy as the next sibling. New ids. Cannot duplicate the root. |
bind_prop | Inside a component: {{props.<key>}} onto a target prop. |
add_element.props is an array of { key, value } with string / number / boolean only. Objects, expressions, and function bindings go on update_props after create. Use className, never class.
packages/modules/props/core/src/domain/props.schema.ts
add_element {
container: { _tag: "Page", id: pageId },
kind: { _tag: "Primitive", value: "section" },
parentElementId: "…", # omit → under body
# targetPosition omitted → append
props: [
{ key: "aria-label", value: "Hero" },
{ key: "className", value: "flex flex-col gap-4 p-6" }
]
}Repeat:
update_element {
elementId: "…",
commands: [
{ op: "set", key: "repeatable", value: true },
{ op: "set", key: "repeatSource",
value: { _tag: "Expression", source: "{{props.events}}" } }
]
}packages/modules/element/adapter/src/ai/update-element.tool.ts
Event prop slot (one FunctionModel per slot — sequences live on store actions, not on the element):
update_props {
elementId: "…",
commands: [{
op: "set",
key: "onClick",
value: {
functionId: "set-property",
instanceParams: { store: "rsvp", property: "modalOpen", value: true }
}
}]
}packages/modules/action/kernel/src/function.model.ts
Gotchas
aria-labelis required on add, and it is a real DOM attribute. 1–3 words of purpose:"Hero","Pricing","Close". Not"div","section wrapper", or a visual description. The tree andfind_element_*use it as the label. A gratuitous label on a node that already has visible text overrides that text in the accessibility tree — still set a purpose name; do not use it as a fake editor tag. (packages/modules/element/adapter/src/ai/add-element.tool.ts)- Omit
targetPositionto append.targetPosition: Ninserts at index N (the current N and everything after shift down).0= first. Passingchildren.lengthis the off-by-one: with 6 children (0..5),5lands before the last. To make it last, omit the field. Same rule onmove_element. (packages/modules/element/core/src/application/add-element.schema.ts) inspect_element_typebefore composites. Blocking for Radio / RadioGroup, SelectItem, MenuItem, Dialog (Trigger / Popup / Header / Title / Description / Panel / Footer / Close), Field + FieldLabel, InputGroup, AccordionTrigger, … Skip only unambiguous leaves (div,span,p,h1–h6,a,img,hr,br). Wrong wrappers fail silently (stacked radios, popups outside their trigger). (packages/modules/mcp/server/src/registry.mcp.ts)- Three write tools, three jobs.
update_props= prop slots (includingchildrenand function bindings).update_element=kind/ repeat /renderIf.style_*= style. Commands are{ op: "set" \| "unset", key, value? }— last write wins; never send the whole object. (packages/modules/mcp/server/src/element.mcp.ts) - Place it once.
parentElementId+targetPositionon insert. Do not add thenmove_element. For a whole subtree,add_elements(parent before child). (packages/modules/element/adapter/src/ai/add-element.tool.ts) repeatSourceis one whole binding.{{props.events}}or{{stores.x.items}}, not a fragment. (packages/modules/element/core/src/application/update-element.application.ts)