Component
A reusable element tree with a declared prop schema.
A Component is a reusable element tree you define once and place many times. Identity + fields (the prop schema) + testData for the canvas. The tree is loaded with the component when you outline or edit it.
packages/modules/component/core/src/domain/component.model.types.ts
It is not an element. An element on a page can be a component instance: kind: { _tag: "Component", value: "<componentId>" }. That node's props are the instance values (title, events, …). The definition lives here; the instance is a page (or parent component) node.
What it is
| Field | Meaning |
|---|---|
name / description / thumbnail | Catalog identity. list_components matches search against name and description. |
layer | shell | rail | panel | section | control. |
category | Discovery group: layout, form, navigation, overlay, … |
fields | Declared prop schema — what a call site may pass. Same field shape stores use. |
testData | Sample values for those fields while editing the definition. Preview with nothing bound falls back here. |
isPublished | When true, other apps may reference this component by id (no copy). Owner-only toggle. |
packages/modules/component/kernel/src/ui-layer.enum.ts
packages/modules/component/kernel/src/component-category.enum.ts
create_component starts with no elements. A page scaffolds a body root; a component does not — the root kind is yours. Seed it with add_root_element, then add_element for children. The root kind must be a registered Primitive (header, div, section, …), not a Component reference.
packages/modules/component/server/src/services/create-component.service.ts
packages/modules/element/adapter/src/ai/add-root-element.tool.ts
Optional fields on create are the prop schema — the same array set_props_schema would write. Do not follow create with set_props_schema just to re-declare them.
packages/modules/component/adapter/src/ai/create-component.tool.ts
Inside the tree, {{props.title}} is this component's RuntimeContainer. Entering the component replaces the outer frame — an inner {{props.X}} never reaches the page's props. Pass page or store data in at the instance (update_props on the Component-kind element), then bind_prop (or a {{props.*}} expression) onto inner nodes.
packages/modules/container/core/src/domain/runtime-container.model.ts
packages/modules/expression/core/src/domain/expression.namespaces.ts
bind_prop writes a tagged expression onto a target prop:
{ elementId, propKey: "title", targetProp: "children" }
→ element.props.children = { _tag: "Expression", source: "{{props.title}}" }packages/modules/props/adapter/src/ai/bind-prop.tool.ts
A store can attach to the component (container: { _tag: "Component", id }) so state travels with it and the name state stays free.
packages/modules/store/adapter/src/ai/create-store.tool.ts
How it relates
- App — components are listed and created in the active app (
contextId). - Page — place the component as an element. Set instance values with
update_props(orinstancePropsonadd_elements). - Element — the definition is an element tree (
container: { _tag: "Component", id }). Same Primitive vs Component kind, props, styles, repeat. - Page vs component as containers — both are
RuntimeContainer. Only the component side is what you usually declarefieldsfor; pages also have authorfields, plus platformid/appId/params/search.
MCP tools
packages/modules/mcp/server/src/component.mcp.ts
| Tool | What it does |
|---|---|
list_components | { search? } — id, name, description, layer, category, isPublished. Search immediately before create so you do not duplicate. |
load_component_outline | Same indented outline as load_page_outline, plus fields and testData. |
find_element_in_component | Search by aria-label, text, or kind inside this tree. |
create_component | { name, description?, layer, category, fields? }. Empty tree. |
update_component | Metadata + isPublished. Not schema, not test data, not the tree. |
delete_component | Deletes the definition and the elements it owns (component_id cascade). Page instances that reference it are not removed — remove_element those. |
set_props_schema | Replaces the whole fields array. Remember the keys for bind_prop / set_test_data. |
set_test_data | Sample values keyed by those prop names. |
bind_prop | Wire {{props.<key>}} onto an inner element's prop. |
Tree mutations use the element tools with container: { _tag: "Component", id: componentId }.
Build sequence:
list_components { search: "ProductCard" } # skip create if it exists
create_component { name: "ProductCard", layer: "section", category: "layout",
fields: [{ name: "title", type: { kind: "text" } }] }
add_root_element { container: { _tag: "Component", id }, kind: { _tag: "Primitive", value: "article" } }
add_element … children …
bind_prop { propKey: "title", targetProp: "children" }
set_test_data { title: "Summer festival" }On a page:
add_element {
container: { _tag: "Page", id: pageId },
kind: { _tag: "Component", value: componentId },
parentElementId: "…"
}
update_props { elementId, commands: [{ op: "set", key: "title", value: "Ceremony" }] }Gotchas
- Empty until you seed a root.
add_elementneeds an existing parent and errors on an empty tree.add_root_elementis the first call; pages never need it. (packages/modules/element/adapter/src/ai/add-root-element.tool.ts) set_props_schemais wholesale.list/load_component_outlinefirst, then send the fullfieldsarray. Same pattern asupdate_store. (packages/modules/component/adapter/src/ai/set-schema-fields.tool.ts)- Search, then create.
list_components({ search })is a substring on name and description. A listing from earlier in the session is how duplicates get made. (packages/modules/component/adapter/src/ai/list-components.tool.ts) - Props do not leak inward. Bind
signedIn: "{{session.signedIn}}"(or a store field) on the instance, thenrenderIf: "{{props.signedIn}}"inside. (packages/modules/container/core/src/domain/runtime-container.model.ts) instancePropsonly on Component kinds inadd_elements. Primitive nodes useprops. Both fold into the same record the renderer reads. (packages/modules/element/adapter/src/ai/add-elements.schema.ts)- Same tree rules as pages. Purpose
aria-label, omittargetPositionto append,inspect_element_typebefore composite primitives. See Element.