This page documents component taxonomy and projection semantics. Layout components live in
packages/core/src/schema/layout/, display components in
packages/core/src/schema/display/, and the projection pass in
packages/core/src/serialization/descriptor_compiler.ts. Everything below is
re-exported from @adonia/core; you reach it through the s builder handed to
BaseResource.schema(s).
One tree, three modes
A resource declares one schema. The compiler projects it into create,
edit, and detail descriptors — layout is preserved, fields become display
entries in detail, and anything that does not belong in a mode is absent
from it. There is no visible: false on the wire (protocol §2 rule 3).
export default class PostResource extends BaseResource<typeof Post> {
static override model = Post
static override slug = 'posts'
schema(s: SchemaBuilder): SchemaComponent[] {
return [
s.grid().columns(3).components([
s.section('Content').columns(2).columnSpan(2).components([
F.text('title').label('Title').required(),
F.text('body').label('Body').columnSpan('full'),
]),
s.aside().columnSpan(1).components([
F.text('slug').label('Slug').readonlyOn('edit'),
s.textEntry('publishedAt').label('Published at').format('datetime'),
]),
]),
]
}
}Layout components (§7.1)
| Factory | Type key | Notes |
|---|---|---|
s.section(label?) |
section |
Titled group with its own column grid |
s.grid() |
grid |
Column grid; also the implicit tree root |
s.tabs() / s.tab(label?) |
tabs / tab |
Tab strip and one panel |
s.wizard() / s.step(label?) |
wizard / step |
Stepper and one step |
s.aside() |
aside |
Secondary column of a record page |
s.fieldset(label?) |
fieldset |
Bordered group of related controls |
s.card(label?) |
card |
Elevated surface |
s.divider(label?) |
divider |
Horizontal rule; childless |
s.spacer() |
spacer |
Empty space / empty grid cell; childless |
Every container carries the same DSL, so there is nothing to memorize per
component: columns(n), columnSpan(n | 'full'), collapsible(),
collapsed(), icon(name), description(text), label(text),
visible(cb) (the §7.1 spelling of canSee), and components([...]).
divider/spacer have no children, so they carry only the leaf half of that
DSL — columnSpan and visible/canSee — plus label and size
respectively.
Two rules make layout safe to nest arbitrarily:
- State keys stay flat.
tabs > tab > tabs > tab > section > fieldset > F.text('subtitle')contributes exactlysubtitle. Layout never namespaces a key (protocol §2 rule 2), sovisibleWhenvarpaths andreactive.sets[].targetalways resolve. - Emptied containers disappear. When every child of a container is
projected away, the container goes with it. The grid root is the sole
exception —
descriptor.schemais always a singlegrid(protocol §2 rule 4), possibly with no children.
Display components (§7.1)
Read-only projections of one record attribute, declared directly in the schema
tree and rendered in detail only — they carry no form state, so they are
absent from create/edit trees, from the validator, and from the fill set.
| Factory | Type key | Typed props |
|---|---|---|
s.textEntry(attr) |
text-entry |
format(name), copyable() |
s.badgeEntry(attr) |
badge-entry |
colors(map), icons(map) |
s.imageEntry(attr) |
image-entry |
disk(name), visibility(v), height(px) |
s.codeEntry(attr) |
code-entry |
language(name) |
s.keyValueEntry(attr) |
key-value-entry |
keyLabel(text), valueLabel(text) |
s.relationEntry(attr) |
relation-entry |
resource(slug), optionLabel(attr), link(target), multiple() |
s.html(attr) |
html |
— (markup is sanitized on write, not here) |
All seven share label, helper, columnSpan, canSee/visible, and
withProps. Each carries its attribute as the node key, so a hand-written
s.textEntry('title') is indistinguishable on the wire from the node
F.text('title') projects to.
Projection semantics (§7.4)
compile(resource, mode, ctx, record?) resolves four independent gates, in
this order, for every node:
canSee(ctx, record)— request-dependent removal.visibleOn/hiddenOn— static, per rendering context.create/editcompile against'form';detailcompiles against'detail'. ('index'is table derivation’s own context, §7.5.)- Category — display entries exist in
detailonly. readonlyOn(mode)— the node stays, and gainsreadonly: trueon its props for the matching form sub-mode.
Gates 1–3 are omission, and omission is total: the node is missing from the
tree, from descriptor.stateKeys, from the compiled validator, and from the
fill set, all in one pass. That is what makes mass assignment impossible by
construction (§19) — the fill set is derived from the compiled schema for the
exact mode of the submission, never from the payload. readonlyOn is the one
gate that keeps the node: a read-only field still renders and still validates,
but it is dropped from that sub-mode’s fill set, so a tampered value cannot
reach the model.
In detail, each field is replaced by its displayType
(text-input → text-entry, image → image-entry, rich-text → html, …).
That is an instance getter, not just a class static: a choice field
(select, radio, checkbox-list) projects to badge-entry when its own
options carry colours and to text-entry when they do not, and the same
split drives its columnType (badge vs text, §7.5).
Only chrome props survive the projection — label, helper, columnSpan,
icon, description, prefix, suffix, plus format, which protocol §8
mandates for the date family (datetime → text-entry with
format: 'datetime'). Validation props (required, maxLength,
nullable, …) and input-only props (placeholder, default, disabled,
readonly) are dropped, and the reactive block is dropped entirely:
visibility was already resolved server-side.
collectFields(resource, ctx?, record?, context?) is the shared walk behind
all of this. Passing a context applies gate 2; validator and fill derivation
pass 'form' (the default of collectFieldMetas), while table derivation
passes nothing because it projects into 'index' itself.
Tests
packages/core/tests/layout_components.spec.ts (component × mode descriptor
snapshots under tests/fixtures/snapshots/, shared DSL, flat state keys,
empty-layout omission) and packages/core/tests/projection.spec.ts (the
four gates against tree + stateKeys + validator + fill set).