skip to the page

@jam-overture/loom/react

Rendering: a tree to React elements, theme mounting, addressing, render diagnostics.

Everything below is exported from that import. The names, the signatures and the sentences are read from the package itself rather than written here, so this page says what the copy of Loom in your node_modules says — and it changes in the same pull request the code does.

111 exports, in 21 modules. Generated from ./dist/render/index.d.ts, which is the declaration file this package publishes for @jam-overture/loom/react.

Install this first. @jam-overture/loom/react loads it the moment the import runs. Without it, the import itself fails — before any of your own code has run.

  • react^19.0.0optional peer dependency
pnpm add react

No import here has everything behind it. @jam-overture/loom/react publishes 111 of the 1,298 names this package publishes. The other 1,187 are behind one of the 16 other imports, and 15 of those 16 publish nothing this one does. The imports do not nest: a name that is not on this page is not a name that does not exist, and the search at the top of the page says which import it comes from.

Some of these names are published elsewhere too: 5 by @jam-overture/loom — the same declarations reached through two doors, so either import gives you the same thing.

Addressing

render/addressing

Which node in the tree a click in the browser can actually land on.

DecorationLookuptype

Whether elements of this type reach the DOM carrying their decoration.

type DecorationLookup = (type: PrimitiveType) => boolean;

UnaddressableReasontype

type UnaddressableReason = 
/** Text and slot nodes render without an element of their own. */
"not-an-element"
/** A registered primitive that ignores `loom.editable`, or one that is not registered at all. */
 | "undecorated-primitive"
/** Not in this tree. */
 | "absent";

Addressingtype

type Addressing = {
    readonly outcome: "addressable";
    readonly nodeId: NodeId;
}
/** The request is answerable, but by an ancestor rather than by the node asked for. */
 | {
    readonly outcome: "delegated";
    readonly nodeId: NodeId;
    readonly requested: NodeId;
    readonly reason: UnaddressableReason;
}
/** Nothing on the path to the root is addressable, so there is nowhere to point. */
 | {
    readonly outcome: "unaddressable";
    readonly requested: NodeId;
    readonly reason: UnaddressableReason;
};

addressNodefunction

The node a click can land on when the user meant nodeId: itself when it is decorated, otherwise its nearest decorated ancestor, otherwise nothing.

const addressNode: (root: LoomNode, nodeId: NodeId, decorates: DecorationLookup) => Addressing

addressedNodeIdfunction

The id a caller should look for in the DOM, or null when there is none.

const addressedNodeId: (addressing: Addressing) => NodeId | null

describeAddressingfunction

const describeAddressing: (addressing: Addressing) => string

Anchor

render/anchor

A band the page's own links can point at.

AnchorAttributestype

What a primitive spreads to become a fragment target.

type AnchorAttributes = {
    readonly id: string;
};

AnchorReadingtype

What the renderer learned about a node's declared anchor.

type AnchorReading = {
    readonly status: "anchored";
    readonly attributes: AnchorAttributes;
} | {
    readonly status: "unusable";
    readonly detail: string;
} | {
    readonly status: "claimed";
    readonly anchor: string;
    readonly holder: NodeId;
};

ANCHOR_MAX_LENGTHvalue

The longest anchor a tree may name.

const ANCHOR_MAX_LENGTH = 64

AnchorLedgertype

Who holds each anchor in one render.

type AnchorLedger = {
    /**
     * Records this node as the holder, or answers with the node that got there
     * first. First claim in document order wins: the walk reaches a node before
     * its descendants and reads siblings in tree order, so the winner is a fact
     * about the tree rather than about the order the renderer happened to visit.
     */
    readonly claim: (anchor: string, nodeId: NodeId) => NodeId | undefined;
};

createAnchorLedgerfunction

const createAnchorLedger: () => AnchorLedger

resolveAnchorfunction

The whole check, as a function of a declared value and what is already taken.

const resolveAnchor: (declared: JsonValue | undefined, nodeId: NodeId, ledger: AnchorLedger) => AnchorReading

Behaviour

render/behaviour

The behaviour seam: the things a primitive *does* that its props cannot carry.

BEHAVIOUR_NAMESvalue

const BEHAVIOUR_NAMES: readonly ["copy", "disclose", "adjust", "present", "dismiss"]

BehaviourNametype

type BehaviourName = (typeof BEHAVIOUR_NAMES)[number];

ADJUST_PROPERTYvalue

The custom property an adjust control publishes its value on: a plain number between {@link ADJUST_MINIMUM} and {@link ADJUST_MAXIMUM}, no unit.

const ADJUST_PROPERTY = "--loom-adjust"

ADJUST_MINIMUMvalue

The bottom of an adjust control's range.

const ADJUST_MINIMUM = 0

ADJUST_MAXIMUMvalue

The top of an adjust control's range.

const ADJUST_MAXIMUM = 100

ADJUST_RESTING_PROPERTYvalue

The property a primitive declares to say **where its control should start**, on the element it places the control in. Read once, at mount, and never again — where the slider goes after that is the reader's.

const ADJUST_RESTING_PROPERTY = "--loom-adjust-resting"

ADJUST_RESTINGvalue

Where the control sits when nothing declared otherwise.

const ADJUST_RESTING = 50

BEHAVIOURSvalue

const BEHAVIOURS: Readonly<Record<BehaviourName, Behaviour>>

isBehaviourNamefunction

const isBehaviourName: (value: string) => value is BehaviourName

PrimitiveBehaviourstype

The controls a primitive receives, by declared behaviour name — already built and ready to place, the way loom.slots hands over rendered regions.

type PrimitiveBehaviours<TName extends BehaviourName = never> = Readonly<Record<TName, ReactNode>>;

NO_BEHAVIOURSvalue

The map handed to a primitive that declared none. Null-prototype for the reason NO_TEXT gives: constructor is not a behaviour name today, and a lookup that answered with a function off Object.prototype if it ever were is not a thing anyone should have to debug from a rendered page.

const NO_BEHAVIOURS: PrimitiveBehaviours<BehaviourName>

BehaviourResolverinterface

The renderer's whole dependency on the seam: which behaviours a type declared.

interface BehaviourResolver {
    readonly behavioursFor: (type: PrimitiveType) => readonly BehaviourName[];
    /**
     * The prop each of this type's controls takes its name from, where its author
     * said so.
     *
     * Optional on the interface, and that is the only concession this seam makes
     * to compatibility: a resolver written before a control could be named from a
     * tree answers nothing here and is read as a library whose primitives all name
     * their controls themselves, which is what it is. A registry built by the SDK
     * always answers.
     */
    readonly controlNamePropsFor?: (type: PrimitiveType) => ControlNameProps;
}

isBehaviourResolverfunction

const isBehaviourResolver: (value: object) => value is BehaviourResolver

UnnamedBehaviourtype

A declared behaviour whose control could not be given a name.

type UnnamedBehaviour = {
    readonly behaviour: BehaviourName;
    readonly key: string;
};

ResolvedBehaviourstype

type ResolvedBehaviours = {
    readonly behaviours: PrimitiveBehaviours<BehaviourName>;
    /**
     * Behaviours left out because a key resolved to nothing. The registry refuses
     * a primitive that declares neither, so the way here is a host dictionary
     * that answers a declared key with a blank — which `overlayText` has no
     * business second-guessing and this has no business rendering.
     */
    readonly unnamed: readonly UnnamedBehaviour[];
};

NO_RESOLVED_BEHAVIOURSvalue

const NO_RESOLVED_BEHAVIOURS: ResolvedBehaviours

resolveBehavioursfunction

Builds the controls for one node.

const resolveBehaviours: (names: readonly BehaviourName[], content: string, text: PrimitiveText<string>, named?: ControlNames) => ResolvedBehaviours

Control

render/control

The handle a primitive has on a control it did not build.

CONTROL_CLASSvalue

The class every control carries, and the stem of the second one naming which behaviour built it: loom-control loom-control-copy.

const CONTROL_CLASS = "loom-control"

controlClassfunction

The classes the runtime stamps on the control one behaviour builds.

const controlClass: (name: BehaviourName) => string

CONTROL_DISPLAY_PROPERTYvalue

The custom property that decides whether a control is displayed — every control at once.

const CONTROL_DISPLAY_PROPERTY = "--loom-control-display"

controlDisplayPropertyfunction

The custom property that decides whether *one* behaviour's control is displayed: --loom-copy-display.

const controlDisplayProperty: (name: BehaviourName) => string

controlDisplayfunction

The value a control writes for display: its own, behind the two properties a primitive may override it with.

const controlDisplay: (name: BehaviourName, resting: string) => string

Control name

render/control-name

Where a control's name comes from when the primitive's own vocabulary is the wrong place to keep it.

ControlNamePropstype

What a primitive declared: the prop each of its controls is named by.

type ControlNameProps = Readonly<Partial<Record<BehaviourName, string>>>;

NO_CONTROL_NAME_PROPSvalue

The answer for a primitive that named none, which is almost all of them.

const NO_CONTROL_NAME_PROPS: ControlNameProps

ControlNamestype

What one node resolved to: the words each control is announced by here.

type ControlNames = Readonly<Partial<Record<BehaviourName, string>>>;

NO_CONTROL_NAMESvalue

The answer for a node whose primitive named none, or that filled none in.

const NO_CONTROL_NAMES: ControlNames

resolveControlNamesfunction

Reads the named props off one node.

const resolveControlNames: (props: JsonObject, declared: ControlNameProps) => ControlNames

Decorative

render/decorative

The same children again, as a copy nothing can resolve to a node.

DecorativeChildrentype

The copy, rendered on demand.

type DecorativeChildren = () => ReactNode;

Diagnostics

render/diagnostics

Rendering is total: it always returns an element. Anything it could not honour comes back beside the element as a diagnostic rather than as a thrown error or an absent page.

UnresolvedResolutiontype

Which way a render came to have no answer for a node that asked for one.

type UnresolvedResolution = "absent" | "unrelated";

RenderDiagnostictype

type RenderDiagnostic = {
    readonly code: "unknown-primitive";
    readonly nodeId: NodeId;
    readonly type: PrimitiveType;
} | {
    readonly code: "invalid-props";
    readonly nodeId: NodeId;
    readonly type: PrimitiveType;
    readonly issues: readonly PropsIssue[];
} | {
    /**
     * The resolver knows this type and the validator does not, which is a
     * composition-root fault rather than anything the tree did. The node
     * still renders — one seam not recognising a type the other resolved is
     * no reason to blank the page — but the props went unchecked, and that is
     * worth saying out loud.
     */
    readonly code: "props-undeclared";
    readonly nodeId: NodeId;
    readonly type: PrimitiveType;
} | {
    /**
     * The root names a theme and the registry refused it — an id nobody
     * registered, or a selection that is not three ids. The page renders
     * unstyled rather than not at all, which is the same bargain every other
     * diagnostic here makes.
     */
    readonly code: "theme-unresolved";
    readonly nodeId: NodeId;
    readonly error: ThemeError;
} | {
    /** The root names a theme and this render was given no registry. */
    readonly code: "theme-unregistered";
    readonly nodeId: NodeId;
} | {
    /**
     * A theme below the root, which is not mounted: the variables are
     * mounted once, at the render root, so a nested selection would be read
     * by nothing. Reported rather than dropped, because someone meant it.
     */
    readonly code: "theme-misplaced";
    readonly nodeId: NodeId;
} | {
    /** A key in the runtime's reserved namespace that the runtime does not read. */
    readonly code: "reserved-prop-unrecognised";
    readonly nodeId: NodeId;
    readonly key: string;
} | {
    /**
     * `loom:data` that is not a map of binding names to registered sources.
     * The node renders — its props are its own and are still valid — with no
     * data at all, which is what a primitive's unavailable path is for.
     */
    readonly code: "data-misdeclared";
    readonly nodeId: NodeId;
    readonly error: BindingError;
} | {
    /**
     * A binding that could not be answered: no such source, params the source
     * refuses, an answer that fails its own schema, or an adapter that could
     * not reach what it wraps. One region of the page is short of data; the
     * page still renders, because the alternative is a whole page lost to one
     * integration being down.
     */
    readonly code: "data-unavailable";
    readonly nodeId: NodeId;
    readonly name: BindingName;
    readonly source: SourceId;
    readonly unavailable: DataUnavailable;
} | {
    /**
     * A node asks for data and this render has no answers for it — neither a
     * value nor a named reason there is none.
     *
     * There are two ways to arrive here and they are fixed in the same place
     * by the same person, which is why they are one code carrying which:
     * `absent`, a render given no resolution at all, and `unrelated`, a render
     * given one built from a different tree's plan. The second is the one that
     * used to say nothing: `EMPTY_DATA_RESOLUTION` answers `NO_DATA` for every
     * node, so a composition root that reached for it as a neutral "no data"
     * value got a page whose every binding vanished in silence.
     */
    readonly code: "data-unresolved";
    readonly nodeId: NodeId;
    readonly resolution: UnresolvedResolution;
} | {
    /**
     * A binding the node asked under a name this primitive says it does not
     * read. The answer arrived and nothing will ever look at it.
     *
     * The quietest of the three ways a binding can be wrong, and until the
     * primitive declared its names it was the only one nothing could see: an
     * unregistered source is refused as `no-such-source`, params the source
     * did not declare are refused by its own schema, and a name nothing reads
     * resolves perfectly and is then dropped on the floor. The node renders —
     * the binding cost it nothing but a round trip — and what is reported is
     * that somebody asked a question whose answer has no reader.
     *
     * Only ever reported for a primitive whose author has declared `reads`.
     * Absence is not emptiness, so a primitive that has said nothing
     * says nothing about the names it is given.
     */
    readonly code: "data-unread";
    readonly nodeId: NodeId;
    readonly type: PrimitiveType;
    readonly name: string;
} | {
    /**
     * An answer arrived, under a name the primitive reads, and the primitive
     * placed some of its rows and not the rest.
     *
     * The one way a binding can be wrong that is invisible from outside the
     * component: each row has a shape only the primitive knows, so only the
     * primitive can say that eleven of twelve stopped reading. A listing skips
     * the row it cannot read and names it to the reader without a count
     *. There was nowhere else for it to say the count.
     *
     * Both counts rather than the difference, because the sentence the author
     * needs is *eleven of twelve*. Never the rows: a diagnostic is logged and
     * a row is the host's data, the same line `data-unavailable` holds.
     *
     * Reported only when `shown < given`. An answer read whole is the ordinary
     * case and would be one line per bound region in every log.
     */
    readonly code: "data-unshown";
    readonly nodeId: NodeId;
    readonly type: PrimitiveType;
    readonly name: string;
    readonly given: number;
    readonly shown: number;
} | {
    /**
     * A primitive declares what it could not show and its declaration could
     * not be believed: it threw, or it returned a reading that cannot describe
     * an answer.
     *
     * The node renders exactly as it would have. A declaration is bookkeeping
     * about an answer, not part of drawing it, and a page lost to bookkeeping
     * is the worst trade avail
…

Cut short here — the whole of it is longer than a page can usefully hold.

describeRenderDiagnosticfunction

const describeRenderDiagnostic: (diagnostic: RenderDiagnostic) => string

Disclosed

render/disclosed

The disclose contract's one name, in a module with nothing else in it.

DISCLOSED_ATTRIBUTEvalue

The attribute a disclosure control stamps on its own button: "true" when the region it names is open, "false" when it is closed.

const DISCLOSED_ATTRIBUTE = "data-loom-disclosed"

Editable

render/editable

Edit mode decorates; it never restructures.

LOOM_NODE_ATTRIBUTEvalue

const LOOM_NODE_ATTRIBUTE = "data-loom-node"

LOOM_TYPE_ATTRIBUTEvalue

const LOOM_TYPE_ATTRIBUTE = "data-loom-type"

LOOM_TREE_ATTRIBUTEvalue

const LOOM_TREE_ATTRIBUTE = "data-loom-tree"

LOOM_REVISION_ATTRIBUTEvalue

const LOOM_REVISION_ATTRIBUTE = "data-loom-revision"

EditableAttributestype

type EditableAttributes = {
    readonly [LOOM_NODE_ATTRIBUTE]: string;
    readonly [LOOM_TYPE_ATTRIBUTE]: string;
    /** Root element only — see `editableAttributes`. */
    readonly [LOOM_TREE_ATTRIBUTE]?: string;
    readonly [LOOM_REVISION_ATTRIBUTE]?: string;
};

editableAttributesfunction

The root element additionally carries the tree it belongs to and the revision it was rendered from, which is exactly the pair an EditIntent needs as its base. A portal can author an intent from the DOM alone, and an intent authored against a stale render names the stale revision rather than silently claiming the current one.

const editableAttributes: (node: ElementNode, tree?: LoomTree) => EditableAttributes

Frame

render/frame

Holding a node's framable props against the origins a deployment permits.

FrameResolverinterface

Which of a primitive's props reach a frame, read off whatever registered it.

interface FrameResolver {
    readonly framePropsFor: (type: PrimitiveType) => readonly string[];
}

isFrameResolverfunction

const isFrameResolver: (value: object) => value is FrameResolver

resolveNodeFramesfunction

Every declared framable prop the node carries, checked, with each outcome reported to the caller as it is decided.

const resolveNodeFrames: (props: JsonObject, declared: readonly string[], registry: FrameOriginRegistry | undefined, report: (prop: string, outcome: FrameOutcome) => void) => NodeFrames

Inline variables

render/inline-variables

A render whose colours are values rather than references.

VariableSubstitutiontype

A CSS value with the theme's variables resolved, and what could not be.

type VariableSubstitution = {
    readonly value: string;
    /**
     * Custom properties the theme does not declare **and** that named no
     * fallback. A reference with a fallback is not reported: the fallback is the
     * value, exactly as it is in a browser, and `--loom-mono-family` is written
     * that way on purpose.
     */
    readonly unresolved: readonly string[];
};

substituteVariablesfunction

const substituteVariables: (value: string, variables: ThemeVariables) => VariableSubstitution

InlinedProjectiontype

A projection with the theme resolved into it, and what the theme did not answer.

type InlinedProjection = {
    readonly element: ReactNode;
    readonly unresolved: readonly string[];
};

inlineThemeVariablesfunction

The projection, with every theme reference in an inline style replaced by what the mounted theme says it is.

const inlineThemeVariables: (element: ReactNode, variables: ThemeVariables) => InlinedProjection

Presented

render/presented

The present contract's one name, in a module with nothing else in it.

PRESENTED_ATTRIBUTEvalue

The attribute a presentation control stamps on **the element the primitive placed it in**: "true" while the region it opens is showing, "false" otherwise.

const PRESENTED_ATTRIBUTE = "data-loom-presented"

DISMISS_EVENTvalue

The event a dismiss control dispatches to close the presentation it sits inside: bubbling, so the nearest presentation above it hears it and no other.

const DISMISS_EVENT = "loom:dismiss"

Primitive

render/primitive

The primitive contract — what a registered component receives, and the only thing the renderer needs from a registry.

LoomRenderContexttype

type LoomRenderContext<TText extends string = never, TBehaviour extends BehaviourName = never> = {
    readonly nodeId: NodeId;
    readonly type: PrimitiveType;
    /**
     * Present only when the tree was rendered in edit mode. Spread onto the
     * primitive's own root element; a primitive that drops it renders correctly
     * but becomes invisible to the portal.
     */
    readonly editable?: EditableAttributes;
    /**
     * The `id` this node answers to, so a link on the same page can point at it.
     *
     * Present only when the tree named an anchor that is usable and that no
     * earlier node claimed; absent otherwise, including inside a decorative copy,
     * which carries no identity. Spread onto the primitive's own root element,
     * beside `editable` — a primitive that drops it renders correctly and cannot
     * be linked to.
     *
     * There is nothing here to distinguish "the tree named none" from "the tree
     * named one and it was refused", which every other seam on this context takes
     * care to keep apart. The difference is real and it is in the diagnostics; it
     * is not here because a primitive would do the same thing with both, and a
     * shape that offers a choice nobody can act on invites one to be invented.
     */
    readonly anchor?: AnchorAttributes;
    /**
     * The tree's theme, flattened into CSS custom properties. Present on the
     * **root node only**, and only when the tree names a theme the render could
     * resolve; apply it as `style` on the primitive's own root element.
     *
     * It arrives here rather than on a wrapper the renderer emits for the reason
     * `editable.ts` gives for the same choice: a wrapper changes what `>`,
     * `:first-child` and `:nth-child` select, and a page that only lays out
     * correctly when it is unthemed is not a page anyone reviewed. The cost is
     * that a root primitive which drops it renders unstyled — which is what the
     * two starter palettes exist to catch.
     */
    readonly theme?: CSSProperties;
    /**
     * The content of this element's own `slot` children, keyed by slot name — the
     * named regions the primitive declared and is responsible for placing.
     *
     * Always present, empty when the node has no slot children, so a primitive
     * reads `loom.slots.aside` without first proving the map exists. A slot the
     * primitive does not place renders nothing: that is what makes a region a
     * region rather than a position in `children`.
     */
    readonly slots: SlotChildren;
    /**
     * What the host answered for this node's bindings, by binding name.
     *
     * Always present, empty when the node binds nothing, so a primitive reads
     * `loom.data.services` without first proving the map exists. Each answer is
     * `ready` or `unavailable` with a reason — never merely absent — because a
     * primitive shows different things for "you have no services yet" and "we
     * could not reach your services", and a shape that cannot tell them apart
     * guarantees it eventually shows the wrong one.
     *
     * It arrives beside `props` rather than merged into them because the two have
     * different authors. Props are in the tree, proposed by a model and weighed by
     * the Gate; data is the host's answer to a question the tree asked, and it
     * belongs to whoever runs the deployment.
     */
    readonly data: NodeData;
    /**
     * Where this node's form posts, if it declared a submission.
     *
     * Absent — not `unavailable` — when the node declared none: the three states
     * are distinct and a form primitive acts differently on each. Absent is a
     * tree that never said where to post, which is an authoring gap; `unavailable`
     * is a deployment that could not answer right now, which is not.
     *
     * Unlike `data`, `text` and `slots` there is at most one per node, because a
     * form posts to one place. Everything on a `ready` target is host-authored:
     * nothing about the address is in the tree, so nothing about it survives into
     * a delta, a revision, or the diff a reviewer reads.
     */
    readonly submit?: SubmissionOutcome;
    /**
     * Whether the URLs this primitive said it frames may be framed, by the
     * prop name that carried each one.
     *
     * Always present, empty for the primitive that declared no framable prop —
     * which is all but one of them — so a primitive reads `loom.frames.src`
     * without first proving the map exists. An entry appears for every declared
     * prop the node actually carries, so a primitive that has a `src` to frame
     * always has an answer about it, and one whose optional `src` was left out
     * has nothing to frame and no entry.
     *
     * Unlike `data` and `submit`, no part of this waits on anything: an allowlist
     * is a static fact about a deployment, so the check happens inside the walk
     * and there is nothing for a host to resolve first. What a host *does* have
     * to wire is the allowlist itself — absent, every frame is refused, because a
     * deployment that has not said whose documents it will run has not agreed to
     * run anybody's.
     */
    readonly frames: NodeFrames;
    /**
     * The strings this primitive declared, resolved for this deployment.
     *
     * Always present and always complete: every key the primitive declared is
     * here, carrying the host's translation where there is one and the declared
     * string where there is not. A primitive reads `loom.text.excluded` and gets a
     * string, never `undefined` — a control whose accessible name went missing
     * because nobody translated it is the failure this seam exists to prevent.
     *
     * Empty for a primitive that declared none, and typed as such: `TText` is the
     * union of declared keys, so reading one that was never declared does not
     * compile.
     */
    readonly text: PrimitiveText<TTex
…

Cut short here — the whole of it is longer than a page can usefully hold.

SlotChildrentype

Rendered slot content, by slot name.

type SlotChildren = Readonly<Record<string, ReactNode>>;

NO_SLOTSvalue

const NO_SLOTS: SlotChildren

LoomPrimitivePropstype

TProps is what a primitive's declared schema accepts. It defaults to the whole JSON object space, so a primitive that declares nothing — or a host that registers without §4's SDK — is still a LoomPrimitive. A narrower TProps is a claim about what the bag contains, and the only thing that makes the claim true is the render seam validating against the same schema the type came from; that pairing is the registry's job (see sdk/registry.ts).

Shown in use on Primitives and the registry — Defining one, Children and slots — Slots: the parent decides where and Where the content comes from — Reading it, in a primitive of your own

type LoomPrimitiveProps<TProps extends JsonObjectView = JsonObject, TText extends string = never, TBehaviour extends BehaviourName = never> = {
    readonly loom: LoomRenderContext<TText, TBehaviour>;
    /** The node's props, exactly as they appear in the tree. */
    readonly props: TProps;
    /** Rendered children in tree order, or null when the node has none. */
    readonly children: ReactNode;
};

LoomPrimitivetype

type LoomPrimitive<TProps extends JsonObjectView = JsonObject, TText extends string = never, TBehaviour extends BehaviourName = never> = ComponentType<LoomPrimitiveProps<TProps, TText, TBehaviour>>;

CallablePrimitivetype

A primitive called as the plain function of its props, rather than mounted.

type CallablePrimitive = (props: LoomPrimitiveProps) => ReactNode;

asCallablePrimitivefunction

A LoomPrimitive is either a function component or a class, and only the first can be called as a plain function. typeof cannot tell them apart — a class is a function too — so the marker React puts on a class component's prototype is what decides it.

const asCallablePrimitive: (primitive: LoomPrimitive) => Result<CallablePrimitive, string>

PrimitiveResolverinterface

The renderer's whole dependency on the registry: one lookup. §4 owns registration — declared prop schemas, packaging, scaffolding — and whatever it grows into has to satisfy no more than this to be renderable.

interface PrimitiveResolver {
    readonly resolve: (type: PrimitiveType) => LoomPrimitive | undefined;
}

staticPrimitiveResolverfunction

A resolver over a fixed map, for hosts that register at module scope.

const staticPrimitiveResolver: (primitives: Readonly<Record<string, LoomPrimitive>>) => PrimitiveResolver

Props

render/props

The prop-validation seam.

PropsIssuetype

type PropsIssue = {
    /** Dotted path within the props object; `props` when the whole bag is wrong. */
    readonly path: string;
    /**
     * Developer-facing text from the declaring schema. Zod's own messages can
     * name a rejected value (an enum mismatch quotes what it received), so a
     * consumer must treat this as content rather than as a safe-to-log constant.
     */
    readonly message: string;
};

PropsVerdicttype

type PropsVerdict = {
    readonly outcome: "valid";
} | {
    readonly outcome: "invalid";
    readonly issues: readonly PropsIssue[];
}
/** The validator has no schema for this type — see `render.ts` for what that means. */
 | {
    readonly outcome: "undeclared";
};

PropsValidatorinterface

interface PropsValidator {
    readonly validateProps: (type: PrimitiveType, props: JsonObject) => PropsVerdict;
}

Reads

render/reads

What a primitive says it reads, and what a tree asked for that nothing will look at.

BindingDeclarationtype

The name a primitive reads one answer under: a fixed one, or the one a prop gives.

type BindingDeclaration<Name extends string = string> = Name | {
    /** The prop whose value is the name. Declared by the primitive's schema. */
    readonly fromProp: string;
    /** The name read when that prop is absent, which is the common case. */
    readonly default: Name;
};

BindingReaderinterface

Which binding names a primitive says it reads, asked per type.

interface BindingReader {
    readonly bindingsReadBy: (type: PrimitiveType) => readonly BindingDeclaration[] | undefined;
}

isBindingReaderfunction

const isBindingReader: (value: object) => value is BindingReader

unreadBindingsfunction

The names this node asked under that its primitive says it does not read.

const unreadBindings: (asked: Iterable<string>, declared: readonly BindingDeclaration[] | undefined, props: JsonObject) => readonly string[]

Rendering

render/render

The tree, projected into React.

SlotContenttype

Content the host projects into named slots. A slot the host does not name falls back to the node's own children; to project nothing into one, name it with null.

type SlotContent = Readonly<Record<string, ReactNode>>;

RenderOptionstype

type RenderOptions = {
    readonly resolver: PrimitiveResolver;
    /**
     * Absent means props are not checked against declared schemas. A registry
     * built by §4's SDK satisfies both this and `resolver`, so wiring the same
     * object into both is the ordinary case.
     */
    readonly validator?: PropsValidator;
    /**
     * Absent means the tree's theme is not resolved and nothing is mounted, and
     * a tree that names one says so in a diagnostic. Supplying a registry is what
     * bounds the palettes, font packs and style presets a proposal may name.
     */
    readonly themes?: ThemeRegistry;
    /**
     * Absent means the tree's bindings are not answered, and a tree that declares
     * one says so in a diagnostic. Resolution is async and rendering is not, so it
     * happens before the walk — `renderRequest` does it, and a caller driving
     * `renderLoomTree` itself resolves with `resolveTreeData` first.
     */
    readonly data?: DataResolution;
    /**
     * Absent means the tree's submissions are not answered, and a tree that names
     * an endpoint says so in a diagnostic. Resolution may do IO — minting a token
     * usually does — so it happens before the walk, the same way data does;
     * `renderRequest` does it, and a caller driving `renderLoomTree` itself
     * resolves with `resolveTreeSubmissions` first.
     */
    readonly submissions?: SubmissionResolution;
    /**
     * The origins this deployment is willing to frame. Absent means every
     * framable prop is refused, and each one says so in a diagnostic — the seam
     * fails closed, because the registry *is* the allowlist and a deployment that
     * has not written one has not agreed to run anybody's script inside its
     * pages.
     *
     * Unlike `data` and `submissions` there is nothing to resolve first: an
     * allowlist is static, so the check happens in the walk.
     */
    readonly origins?: FrameOriginRegistry;
    /**
     * The host's answer for the strings primitives declared — a dictionary, in the
     * language this deployment serves. Absent means the declared strings render as
     * their authors wrote them, which is what an untranslated deployment wants and
     * needs no wiring: the renderer reads declarations off `resolver` when that
     * resolver is a registry. `textResolverFor` builds one of these from a
     * registry and a dictionary.
     *
     * A key this does not answer keeps its declared string, so a partial
     * dictionary is a partial translation rather than a missing accessible name.
     * There is deliberately no wiring that suppresses a declared string: a control
     * with no accessible name is the failure the seam exists to prevent, and it
     * should not be reachable by leaving something out.
     */
    readonly text?: TextResolver;
    /** Off by default: decoration is opt-in per request, never ambient. */
    readonly editMode?: boolean;
    /**
     * Stamp each element's identity on the markup without anything else edit mode
     * means, so a published page can say which node a reader saw or clicked.
     *
     * Off by default, and the markup is byte-identical when it is off. It writes
     * exactly the attributes edit mode writes — node id and type on every
     * decorated element, the tree id and revision on the root — because those are
     * the addresses an event about a reader has to carry.
     */
    readonly addressed?: boolean;
    readonly slots?: SlotContent;
    readonly themeValues?: "variables" | "literals";
};

RenderOutputtype

type RenderOutput = {
    readonly element: ReactNode;
    readonly diagnostics: readonly RenderDiagnostic[];
    /**
     * What the root is wearing, absent when the tree named no theme or the render
     * could not resolve it. The variables are already mounted; this is here so a
     * caller can *say* which palette it served without resolving the ids again.
     */
    readonly theme?: ResolvedTheme;
};

renderLoomTreefunction

Shown in use on Quickstart — What just happened, Rendering a tree, Where the content comes from — Asking, before anything is drawn, Making it look like yours — Registering your own, What a form posts to — When it is resolved, and why not later and What your readers do — One: let the page say which node is which

const renderLoomTree: (tree: LoomTree, options: RenderOptions) => RenderOutput

ExcerptOutputtype

type ExcerptOutput = RenderOutput & {
    /**
     * Whether the tree holds the node asked for. `false` comes with a null
     * element and an `excerpt-absent` diagnostic; it is here because it is the
     * one thing every caller branches on, and scanning a diagnostic array to
     * decide whether to render a fallback is worse than being told.
     */
    readonly found: boolean;
};

renderLoomExcerptfunction

Part of a tree, rendered on its own and wearing the tree's theme.

const renderLoomExcerpt: (tree: LoomTree, nodeId: NodeId, options: RenderOptions) => ExcerptOutput

One render, end to end

render/request

Resolution, once per request.

RenderRequesttype

type RenderRequest = {
    readonly treeId: TreeId;
    /** Decoration is a property of the request, so one process can serve both. */
    readonly editMode: boolean;
    /**
     * Identity on a published page's markup, so reader signals can name the node
     * they are about. Off when absent.
     */
    readonly addressed?: boolean;
    /** Opaque host context — audience, locale, flags — passed through to the source. */
    readonly context?: JsonObject;
};

TreeSourceErrortype

type TreeSourceError = {
    readonly code: "not-found" | "unavailable";
    readonly detail: string;
};

TreeSourceinterface

interface TreeSource {
    readonly load: (request: RenderRequest) => Promise<Result<unknown, TreeSourceError>>;
}

RenderDependenciestype

type RenderDependencies = {
    readonly source: TreeSource;
    readonly resolver: PrimitiveResolver;
    readonly validator?: PropsValidator;
    /** Absent means the tree's theme is not resolved — see `RenderOptions.themes`. */
    readonly themes?: ThemeRegistry;
    /**
     * Absent means the tree's bindings are not answered — see `RenderOptions.data`.
     * Supplying a registry is what bounds the sources a proposal may name, the
     * same way `themes` bounds the palettes.
     */
    readonly sources?: DataRegistry;
    /**
     * Absent means the tree's submissions are not answered — see
     * `RenderOptions.submissions`. Supplying a registry is what bounds the
     * destinations a proposal may name, and it is the only thing that does: an
     * address never appears in a tree, so a deployment that registers nothing has
     * no form that posts anywhere.
     */
    readonly endpoints?: EndpointRegistry;
    /**
     * The origins this deployment will frame — see `RenderOptions.origins`.
     * Absent means every framable prop is refused with a diagnostic.
     *
     * It sits here beside the other three registries and, unlike them, adds no
     * `await`: an allowlist is a static fact, so it is passed straight through to
     * the walk rather than resolved first.
     */
    readonly origins?: FrameOriginRegistry;
    /**
     * Absent means primitives render the strings their authors declared — see
     * `RenderOptions.text`. A host serving one language in the library's own
     * language wires nothing here; a host serving another builds a resolver per
     * dictionary and picks by whatever it reads the request's language from, which
     * is its own business and not the framework's.
     */
    readonly text?: TextResolver;
    readonly slots?: SlotContent;
    /**
     * References or values — see `RenderOptions.themeValues`. It belongs to the
     * request rather than to the deployment: one route serves a page to a browser
     * and the next draws the same tree into an image, off the same dependencies.
     */
    readonly themeValues?: "variables" | "literals";
};

RenderRequestErrortype

type RenderRequestError = {
    readonly code: "source-failed";
    readonly error: TreeSourceError;
} | {
    readonly code: "invalid-tree";
    readonly error: TreeError;
} | {
    readonly code: "tree-id-mismatch";
    readonly requested: TreeId;
    readonly received: TreeId;
};

RenderedRequesttype

The rendered result carries the tree it came from: the caller needs revision to set a cache validator, and needs the parsed tree to be the same object the element was built from rather than one it loads again.

type RenderedRequest = RenderOutput & {
    readonly tree: LoomTree;
};

renderRequestfunction

Shown in use on Rendering a tree — Serving a page, rather than rendering a value, Making it look like yours — Registering your own and What a form posts to — When it is resolved, and why not later

const renderRequest: (request: RenderRequest, dependencies: RenderDependencies) => Promise<Result<RenderedRequest, RenderRequestError>>

Text

render/text

The text seam: the strings a primitive owns, and the deployment's chance to translate them.

PrimitiveTexttype

The strings a primitive receives, by declared key.

type PrimitiveText<TKey extends string = never> = Readonly<Record<TKey, string>>;

NO_TEXTvalue

The map handed to a primitive that declares nothing, and the answer for a type the resolver does not know.

const NO_TEXT: PrimitiveText<string>

TextResolverinterface

The renderer's whole dependency on translation: one lookup, by primitive type.

interface TextResolver {
    readonly textFor: (type: PrimitiveType) => PrimitiveText<string>;
}

isTextResolverfunction

Whether a resolver can also answer for the strings its primitives declared.

const isTextResolver: (value: object) => value is TextResolver

overlayTextfunction

A primitive's declared strings with a host's answers laid over them, for the one case where both are in play and they are not the same object.

const overlayText: (declared: PrimitiveText<string>, supplied: PrimitiveText<string>) => PrimitiveText<string>

Theme

render/theme

The theme read out of the runtime's own props.

ThemeResolutiontype

type ThemeResolution = {
    readonly outcome: "themed";
    readonly theme: ResolvedTheme;
}
/** The node names no theme — the ordinary case for every node but the root. */
 | {
    readonly outcome: "unthemed";
}
/** Named, and the registry refused it: an unknown id, or a malformed selection. */
 | {
    readonly outcome: "unresolved";
    readonly error: ThemeError;
}
/** Named, and this render was given no registry to resolve it against. */
 | {
    readonly outcome: "unregistered";
};

resolveThemefunction

There is no fallback theme, deliberately. A host-supplied default would mount a look the tree does not name, which makes the page a function of deployment config as well as of the tree — the failure that rejecting host-supplied theming outright was meant to prevent. An unthemed tree renders unstyled, and says so.

const resolveTheme: (reserved: JsonObject, registry: ThemeRegistry | undefined) => ThemeResolution

themeStylefunction

A resolved theme as a style object the root primitive applies.

Shown in use on Making it look like yours — Putting it on a page

const themeStyle: (theme: ResolvedTheme) => CSSProperties

ThemeGroundtype

The ground a host paints when it draws a frame standing in for the page.

type ThemeGround = {
    readonly backgroundColor: string;
    readonly color: string;
    /**
     * Absent — rather than guessed — when either end of the palette's body-copy
     * pair is a colour `channelsOf` declines to read, in which case whatever
     * `color-scheme` the host's own stylesheet sets stands. Every registered
     * palette declares both as hex and resolves.
     */
    readonly colorScheme?: PaletteScheme;
};

themeGroundfunction

A resolved theme as the three declarations a host's own frame applies.

Shown in use on Making it look like yours — Putting it on a page

const themeGround: (theme: ResolvedTheme) => ThemeGround | undefined

Unshown

render/unshown

What a primitive could not show of an answer it was given, and how the walk is told.

UnshownReadingtype

What a primitive made of one answer: the rows it was given, and the rows it placed.

type UnshownReading = {
    /** The binding name the answer arrived under. */
    readonly name: string;
    /** Rows in the answer as the primitive found it. */
    readonly given: number;
    /** Rows the primitive placed on the page. */
    readonly shown: number;
};

UnshownDeclarationtype

What a primitive declares: its own reading of this node's answers.

type UnshownDeclaration = (props: JsonObject, data: NodeData) => readonly UnshownReading[];

UnshownReaderinterface

Which primitive declares a reading, asked per type.

interface UnshownReader {
    readonly unshownBy: (type: PrimitiveType) => UnshownDeclaration | undefined;
}

isUnshownReaderfunction

const isUnshownReader: (value: object) => value is UnshownReader

UnshownFaulttype

Why a declaration's readings could not be believed.

type UnshownFault = {
    /**
     * The declaration threw. Rendering is total (see `diagnostics.ts`) and a
     * declaration is not a licence to make it otherwise — a page lost to a
     * primitive's *bookkeeping* would be the worst trade in this package.
     */
    readonly kind: "threw";
    readonly detail: string;
} | {
    /**
     * A reading that cannot describe anything: an unnamed answer, a count that
     * is not a whole number of rows, or more rows shown than given. Refused
     * rather than clamped, because a diagnostic saying *thirteen of twelve*
     * sends the author to look for a defect that is in the primitive telling
     * them about it.
     */
    readonly kind: "impossible";
    readonly detail: string;
};

describeUnshownFaultfunction

const describeUnshownFault: (fault: UnshownFault) => string

readUnshownfunction

The readings one declaration makes of this node, or the fault that means there are none to be had.

const readUnshown: (declaration: UnshownDeclaration, props: JsonObject, data: NodeData) => Result<readonly UnshownReading[], UnshownFault>

unshownRowsfunction

The readings worth reporting, in the order the walk reports them.

const unshownRows: (readings: readonly UnshownReading[]) => readonly UnshownReading[]

The loom: namespace

reserved-props

The runtime's own corner of a node's props.

RESERVED_PROP_PREFIXvalue

Prop keys under this prefix belong to the runtime rather than to a primitive.

const RESERVED_PROP_PREFIX = "loom:"

THEME_PROP_KEYvalue

The palette, font pack and style preset a tree names, honoured on the root node.

const THEME_PROP_KEY = "loom:theme"

isReservedPropKeyfunction

const isReservedPropKey: (key: string) => boolean

PartitionedPropstype

type PartitionedProps = {
    /** What the primitive and the validator see. */
    readonly props: JsonObject;
    /** What the runtime reads, keyed as it appears in the tree. */
    readonly reserved: JsonObject;
};

partitionReservedPropsfunction

Splitting costs one pass over the keys, and allocates nothing at all for the ordinary node that carries no reserved key — which is most of them.

const partitionReservedProps: (props: JsonObject) => PartitionedProps