Configurable Surfaces
An item type declares which of its config fields are runtime-configurable —
editable by a viewer or an external override — through a schema-level
configurable keyword. Together those entries form the item type’s
configurable surface: the honest, machine-readable list of knobs a
configurator may expose and the override system
may set, without a human hand-maintaining a parallel list.
The surface is declarative data on the item-type schema, not per-instance config. Every instance of a type shares the same surface. The resolver validates each declaration fail-fast and attaches the validated surface to the resolved instance, so downstream consumers read it without re-parsing the schema.
The configurable keyword
configurable is a top-level keyword on an item-type schema — a sibling of
type and properties, not a config property. It maps each
runtime-configurable config field to a descriptor:
{
"type": "object",
"additionalProperties": false,
"configurable": {
"label": {
"type": "string",
"label": "Label",
"rendering": "text-input",
"constraints": {
"description": "The human label rendered beside the control."
}
},
"disabled": {
"type": "boolean",
"label": "Disabled",
"rendering": "toggle"
}
},
"properties": {
"label": { "type": "string" },
"disabled": { "type": "boolean" }
}
}
Each descriptor carries:
type(required). The field’s value type, drawn from the variable type set:string,number,integer,boolean,enum, orarray. This is the type an editor or override deals in — not necessarily the raw JSON Schema type of the underlying property (see Object-shaped fields).label(optional). A human label for the field, shown by a configurator.constraints(optional). An opaque object the resolver passes through verbatim. Use it to carry editing hints — anenumof allowed values, adescription, the shape of nested sub-fields— for downstream tools. The resolver does not interpret it.rendering(optional). A preferred widget item-type to edit this field with, e.g.text-inputortoggle. It must name a registered widget, so a configurator can auto-pick a control.
Validation rules
On every resolve the resolver validates the configurable declaration of each
node’s item type, fail-fast, reporting CONFIGURABLE_SURFACE_INVALID (with the
offending path, type, and field in the error details) when:
- a declared field name is not a real config property of the item type
(it is absent from the schema’s
properties), or - a field’s
typeis not one of the variable type set, or - a
renderinghint names a widget the catalog does not know.
A type that declares no configurable keyword simply has an empty surface — it
is not an error.
Document-scope surfaces
The same descriptor shape is reused beyond item types. The reserved
document scopes ($manifest, $theme, $variables, $connections,
$root) declare their own configurable surfaces on the document schema, under
a top-level documentScopes keyword that maps each $-keyword to the identical
field → descriptor shape. A configurator
pointed at a reserved scope generates its editor from that surface through the
same machinery — there is no parallel system. Field names are validated against
the scope’s real schema (the manifest’s properties for $manifest, the
theme vocabulary’s tokens for $theme).
Constraints: top-level fields only
The mechanism validates field names against the item type’s top-level
schema properties only, and field types against the variable type set
(string / number / integer / boolean / enum / array — no nested
object). So a surface is declared only on top-level config fields carrying
those types. A nested object property cannot be surfaced as an object; instead
it is surfaced under the closest variable type and its inner shape is described
in constraints.
Object-shaped fields
When a meaningful editable field is itself an object — the
container grid, or the
form layout — it is declared with the closest variable type
(array) and its sub-fields are spelled out in constraints.fields for the
configurator to walk:
"configurable": {
"layout": {
"type": "array",
"label": "Form layout",
"constraints": {
"description": "The form's layout: mode plus columns/rows/gap.",
"fields": {
"mode": { "type": "string", "enum": ["flow", "grid"] },
"columns": { "description": "Flow: column count. Grid: weight array." }
}
}
}
}
What declares a surface
Every shipped item type that has runtime-tunable presentation declares one:
- All 13 widgets surface their shared
labelanddisabled, plus family-specific fields —placeholder(string family andtag-input),min/max/step(number family), andsort(enum and option-array families). - The
formcontainer surfaces itslayout. - The
containersurfaces itsgrid, thevariable-boxsurfaces itsarrangement, and thetablesurfaces itstitle,columns, andquery. - The
blockwrapper surfaces itstitleandvisibility, so those per-block concerns are tunable at runtime through the same mechanism (itsid,content, andthemeare not surfaced).
What reads a surface
The resolver attaches the validated surface to each resolved instance, in sorted field order, so downstream layers read it directly:
- A configurator auto-generates an editor from the
surface — one control per field, picked by the
renderinghint. - The override system knows exactly which fields it may set at runtime.
- A JSON Patch changeset uses the surface as its guardrail — it enumerates the only paths a patch may legally touch.
Because the surface is derived from the schema and validated on every resolve, a declared surface can never drift out of sync with the properties the item type actually accepts.