API reference

The Python surface of spaday. Peer-package component classes are not listed here; see Author a component tree and Generate typed classes.

Authoring

class spaday.Component(*children: Component | dict | str, key: str | None = None, props: dict[str, Any] | None = None, **attrs: Any)[source]

Bases: object

Base for a node in the spaday component tree.

Author it two equivalent ways: nest children positionally in the constructor and set generic props as keywords — App(Nav("title"), Body(...), id="root") — or build it up fluently with .child() / .prop(). A string child becomes a text node. Subclasses set the class attribute tag and forward their typed props via props= (only the ones the author set — None means “leave the element’s own default”). CEM-generated subclasses also set the class-level schema used by component catalogs; hand-authored catalog components may set it explicitly.

strict_props: ClassVar[bool] = False

Reject unknown keywords at construction (see set_strict_props()). Off by default.

classmethod set_strict_props(strict: bool = True) → None[source]

Make unknown keyword arguments raise at construction, for cls and its subclasses.

Off by default: a manifest describes an element’s inputs approximately (see ComponentSchema.fields), so an unrecognized keyword is reported by spaday.validate() over a built tree rather than assumed to be a mistake. Turn it on where the earlier error is worth that risk, at whichever scope fits — the whole catalog (Component.set_strict_props()), one package’s components (WaButton.set_strict_props(), which works on a generated catalog you don’t own), or off again for a single class the manifest describes badly (WaSelect.set_strict_props(False)). A class that sets it explicitly keeps that setting when a base class is toggled later, so a per-class opt-out survives turning the catalog strict. Classes without a schema have nothing to check and are unaffected.

Only constructor keywords are checked; prop() stays the explicit escape hatch for an attribute the manifest does not describe.

classmethod retag(tag: str) → type[Component][source]

A subclass of this component bound to a different custom element name.

Use it when an application ships its own element implementing the same contract and wants this class’s authoring surface — props, bindings, actions, catalog schema — pointed at that element instead:

MyGrid = PerspectivePanel.retag("my-data-grid")

The tag is carried into the class’s schema as well, which ComponentPackage requires to agree with tag, so the result can go straight into a package descriptor. The new tag registers for dict-tree validation like any other schema-carrying class.

key(key: str) → Component[source]

Set the reconciliation key (for keyed child diffing).

child(*nodes: Component | dict | str) → Component[source]

Append one or more children to the default slot (a string child becomes a text node).

child_in(slot: str, node: Component | dict | str) → Component[source]

Append a child to a named slot (a string becomes a <span> text node).

text(value: str | Expr) → Component[source]

Set the element’s literal or reactive text content (e.g. a button or option label).

Text is set as the textContent DOM property by the runtime, so this is for leaf elements whose label is their text (don’t combine it with child nodes). An expression such as item("name") becomes a computed binding.

prop(name: str, value: Any) → Component[source]

Set an arbitrary prop (escape hatch for attributes a typed class doesn’t expose).

style(**decls: Any) → Component[source]

Set inline CSS declarations, e.g. .style(padding="1rem", font_size="2rem").

Keys are kebab-cased (font_size → font-size; a trailing _ is dropped so reserved words work, float_ → float). Composes with css() and any literal style prop.

css(**variables: Any) → Component[source]

Set CSS custom properties — the theming knob, e.g. .css(background_color="navy") → --background-color: navy. This is how a web component’s documented –* theme tokens are set from Python (per component), and how the spa-* shell is re-themed at the app level (App().css(spa_surface="#111", spa_border="#333") cascades to the whole shell). WebAwesome’s own custom-property tokens are set the same way. See spaday.theme.

classes(*names: str) → Component[source]

Add CSS classes (component variants / theme states), e.g. .classes("wa-dark").

on(event: str, action: Action) → Component[source]

Bind a declarative Action to a DOM event (e.g. "click").

The action is serialized as data and interpreted in the browser when the event fires — no round-trip to Python.

on_wire(event: str, action: object) → Component[source]

Bind already-serialized action data after validating it with the shared core model.

Editors and source adapters use this when behavior starts as structured data rather than a Python Action instance.

bind_wire(prop: str, binding: object) → Component[source]

Attach already-serialized binding data after shared-core validation.

bind(prop: str, field: str, *, mode: str = 'one-way', event: str | None = None, methods: tuple[str, str] | None = None, state: str | None = None, defer: bool = False, codec: str | None = None, encode: str | None = None) → Component[source]

Reactively bind a prop to a state field in the runtime’s signal store.

mode="one-way" keeps the prop in sync with the field; "two-way" also writes the field back when the control changes (for value-like controls). The binding is data interpreted in the browser — the field’s value flows to the prop with no round-trip to Python.

A two-way binding writes back on change / input; event names the event instead, for a control that reports its changes otherwise (a Lion control’s model-value-changed). methods — ("open", "close") — drives the prop by calling those methods on the element as the field turns truthy / falsy instead of setting it, for an overlay that opens by method (bind("open", "confirm", mode="two-way", event="close", methods=("showModal", "close")) on a <dialog>); the prop then names the element’s own state, read back on event. state names a different property, including a dotted path, when that readable state lives below the element (for example "dialog.open" on a wrapper around <dialog>). defer=True coalesces property writes until the next animation frame, after the element is connected and its children have been assigned. codec="number" converts an empty value to None and other values to numbers on write-back; "json" JSON-encodes values sent to the element and decodes them on return. encode="string" stringifies values sent to the element without changing how its readable state is decoded.

compute(prop: str, expr: Expr) → Component[source]

Reactively set prop to a value computed from state fields (one-way).

expr is a field expression (field() / eq / not_ / all_ / any_ / lit / item / scope) evaluated in the browser and recomputed whenever any global field or repeater scope it reads changes, e.g. compute("disabled", not_(field("enabled"))).

bind_root_class(name: str, field: str) → Component[source]

Toggle a CSS class on the document root (<html>) from a boolean reactive state field.

The escape hatch for page-level theming that lives outside the component tree — most notably WebAwesome’s wa-dark: App(...).bind_root_class("wa-dark", "dark") makes a switch bound to a dark field re-theme the whole page (the rest follows via CSS tokens; canvas widgets that can’t read a class take a .compute("theme", cond(field("dark"), "dark", "light")) instead). One-way (the field drives the class); active only when mounted with a signal Store.

bind_root_attr(name: str, field: str) → Component[source]

Set an attribute on the document root (<html>) from a reactive state field.

The attribute counterpart to bind_root_class(), for the page-level state a class cannot carry: a theme selector whose value is matched (:root[data-density='comfortable']) needs an attribute, since a class carries no value and enumerated values would need mutually exclusive naming plus removal logic. The field’s value is written as the runtime writes any attribute — None/False remove it, True and "" give the bare form (data-vivid), anything else is stringified — so one binding covers both enumerated and boolean root state. One-way (the field drives the attribute); active only when mounted with a signal Store.

to_node() → dict[source]

The node as the core’s JSON-ready dict (empty fields omitted, like the Rust core).

to_json() → str[source]

The node serialized for the core’s diff/apply.

spaday.element(tag: str, *children: Component | dict | str, key: str | None = None, **props: Any) → Component[source]

Build a plain element (e.g. a div container) for structure a typed component doesn’t cover.

Children nest positionally; a prop name with a trailing underscore is de-escaped so reserved words work (class_ → class). e.g. element("div", Strong("hi"), id="root", class_="card").

class spaday.components.shell.Each(template: Component, *, field: str | None = None, items: Any | None = None, key: str, scope: str | None = None, direct: bool = False, **props: Any)[source]

Bases: Component

Render one live component subtree per item in a reactive collection, reusing instances by key.

field reads a global store collection. items accepts an expression, including item() for a nested collection. Inside template, item() reads the current item and scope("name.path") reads a named current or ancestor repeater scope:

Each(Row(Strong().compute("textContent", item("name"))), field="rows", key="id", scope="row")

The first release supports one component template root and read-only item scopes. Item keys must be unique strings or finite numbers. Reordering preserves each live root element and its local state. Set direct=True when the parent custom element requires repeated roots to be direct light-DOM children; the spa-each element remains as an empty reconciliation anchor.

Generic controls

Controls any design renders; see Use generic controls.

Generic controls that any design system renders.

An application authors these once — Button(label="Save", intent="primary") — and the page’s Design decides which element that becomes: a wa-button, a ui5-button, a vaadin-button, or the native baseline’s <button>. The controls share one vocabulary, sized to what every design can express:

  • label (a button’s text, a field’s caption, a dialog’s title), help (a hint under a field) and error (a validation message, which also marks the field invalid);

  • disabled, required, readonly, name;

  • intent — neutral / primary / info / success / warning / danger — the same tones the shell palette carries; appearance — filled / outline / plain; size — sm / md / lg;

  • value, the prop to bind: a string for text and dates, a number for numeric controls, a boolean for a checkbox or switch, or a scalar option value for select and radio controls;

  • open for a dialog.

A control serializes to a ui-* node carrying that vocabulary; spaday.ui.resolve() turns it into the design’s elements before the tree leaves Python, so the browser only ever sees concrete tags. A design that lacks a control falls back to the native baseline. What a design cannot express is dropped rather than half-rendered; Control.for_design() sets a design’s own props on one control where that matters.

class spaday.ui.controls.Alert(*children: Component | dict | str, label: str | None = None, intent: Literal['neutral', 'primary', 'info', 'success', 'warning', 'danger'] | None = None, key: str | None = None, **props: Any)[source]

Bases: Control

A message that needs the user’s attention. Its children are the message body.

class spaday.ui.controls.Button(*children: Component | dict | str, label: str | None = None, intent: Literal['neutral', 'primary', 'info', 'success', 'warning', 'danger'] | None = None, appearance: Literal['filled', 'outline', 'plain'] | None = None, size: Literal['sm', 'md', 'lg'] | None = None, disabled: bool | None = None, name: str | None = None, key: str | None = None, **props: Any)[source]

Bases: Control

A button. Its label is its text; attach behavior with .on("click", …).

Button(label="Save", intent="primary").on("click", SetField("saved", True))

class spaday.ui.controls.Checkbox(*, label: str | None = None, help: str | None = None, error: str | None = None, value: bool | None = None, disabled: bool | None = None, required: bool | None = None, name: str | None = None, size: Literal['sm', 'md', 'lg'] | None = None, key: str | None = None, **props: Any)[source]

Bases: _Toggle

A checkbox. Its value is a boolean; bind it two-way.

Checkbox(label="Agree").bind("value", "agree", mode="two-way")

class spaday.ui.controls.Control(*children: Component | dict | str, key: str | None = None, props: dict[str, Any] | None = None, **attrs: Any)[source]

Bases: Component

Base of the generic controls: a ui-<kind> node any design renders.

for_design(design: str, **props: Any) → Control[source]

Props set on the element only when design renders this control — the design’s own spelling for what the generic vocabulary leaves out (for_design("webawesome", pill=True)). Applied after the generic props, so they can also override one.

class spaday.ui.controls.DateInput(*, label: str | None = None, help: str | None = None, error: str | None = None, value: str | None = None, min: str | None = None, max: str | None = None, disabled: bool | None = None, required: bool | None = None, readonly: bool | None = None, name: str | None = None, size: Literal['sm', 'md', 'lg'] | None = None, key: str | None = None, **props: Any)[source]

Bases: Control

A calendar-date input whose value, minimum and maximum are ISO YYYY-MM-DD strings.

class spaday.ui.controls.Dialog(*children: Component | dict | str, label: str | None = None, open: bool | None = None, key: str | None = None, **props: Any)[source]

Bases: Control

A modal dialog whose children are its content and whose label is its title. Bind open two-way: the field opens and closes it, and a close the dialog does itself (Escape) writes the field back.

Dialog(Paragraph("Saved."), Button(label="OK").on("click", SetField("open", False)), label="Done").bind("open", "open", mode="two-way")

class spaday.ui.controls.NumberInput(*, label: str | None = None, help: str | None = None, error: str | None = None, value: int | float | None = None, min: int | float | None = None, max: int | float | None = None, step: int | float | None = None, placeholder: str | None = None, disabled: bool | None = None, required: bool | None = None, readonly: bool | None = None, name: str | None = None, size: Literal['sm', 'md', 'lg'] | None = None, key: str | None = None, **props: Any)[source]

Bases: Control

A numeric input. Its value is a number, or None while an optional field is empty.

class spaday.ui.controls.Progress(*, label: str | None = None, value: int | float | None = None, max: int | float | None = None, key: str | None = None, **props: Any)[source]

Bases: Control

Progress toward max. Omit value for an indeterminate indicator.

class spaday.ui.controls.RadioGroup(*, label: str | None = None, help: str | None = None, error: str | None = None, options: list[Any] | None = None, value: str | int | float | bool | None = None, disabled: bool | None = None, required: bool | None = None, name: str | None = None, size: Literal['sm', 'md', 'lg'] | None = None, key: str | None = None, **props: Any)[source]

Bases: Control

A single-choice group over options. Values may be strings, numbers or booleans.

class spaday.ui.controls.Select(*, label: str | None = None, help: str | None = None, error: str | None = None, options: list[Any] | None = None, value: str | int | float | bool | None = None, placeholder: str | None = None, disabled: bool | None = None, required: bool | None = None, name: str | None = None, size: Literal['sm', 'md', 'lg'] | None = None, key: str | None = None, **props: Any)[source]

Bases: Control

A single-choice select over scalar values or {value, label, disabled} objects. Bind value two-way.

Select(label="Plan", options=["basic", "plus"]).bind("value", "plan", mode="two-way")

A design renders the options as child elements or as a property; binding options to a store field is only possible with a design that takes them as a property.

class spaday.ui.controls.Slider(*, label: str | None = None, help: str | None = None, error: str | None = None, value: int | float | None = None, min: int | float | None = None, max: int | float | None = None, step: int | float | None = None, disabled: bool | None = None, required: bool | None = None, name: str | None = None, size: Literal['sm', 'md', 'lg'] | None = None, key: str | None = None, **props: Any)[source]

Bases: Control

A single-thumb numeric slider.

class spaday.ui.controls.Switch(*, label: str | None = None, help: str | None = None, error: str | None = None, value: bool | None = None, disabled: bool | None = None, required: bool | None = None, name: str | None = None, size: Literal['sm', 'md', 'lg'] | None = None, key: str | None = None, **props: Any)[source]

Bases: _Toggle

An on/off switch. Its value is a boolean; bind it two-way. (Exported at the top level as ToggleSwitch, beside the shell’s Switch router.)

Switch(label="Dark theme").bind("value", "dark", mode="two-way")

class spaday.ui.controls.TextArea(*, label: str | None = None, help: str | None = None, error: str | None = None, value: str | None = None, placeholder: str | None = None, rows: int | None = None, minlength: int | None = None, maxlength: int | None = None, disabled: bool | None = None, required: bool | None = None, readonly: bool | None = None, name: str | None = None, size: Literal['sm', 'md', 'lg'] | None = None, key: str | None = None, **props: Any)[source]

Bases: Control

A multiline text input. Bind value (a string) two-way to a store field.

class spaday.ui.controls.TextInput(*, label: str | None = None, help: str | None = None, error: str | None = None, value: str | None = None, placeholder: str | None = None, type: Literal['text', 'password', 'email', 'search', 'tel', 'url'] | None = None, disabled: bool | None = None, required: bool | None = None, readonly: bool | None = None, name: str | None = None, size: Literal['sm', 'md', 'lg'] | None = None, key: str | None = None, **props: Any)[source]

Bases: Control

A single-line text input. Bind value (a string) two-way to a store field.

TextInput(label="Name", placeholder="Ada").bind("value", "name", mode="two-way")

Designs: how a design system renders the generic controls, as data.

A Design maps each generic control (see spaday.ui.controls) to one ControlSpec — the element a design renders it as and how the control’s generic surface lands on it: which attribute takes the label (or which slot, or a wrapper element around the control), what an intent of "primary" is called there, which property carries the value and which event reports a change, whether a dialog opens by property or by method. Everything in it is plain data, so a design round-trips through JSON: a design-system package publishes one on its ComponentPackage, an application can ship its own, and a runtime could apply the same mapping without Python.

resolve() applies a design to a serialized tree, replacing every generic ui-* node with the concrete elements the design describes, before the tree leaves Python. Controls a design does not describe fall back to the native baseline (spaday.ui.native.NATIVE) and are marked data-ui-fallback.

pydantic model spaday.ui.design.ControlSpec[source]

Bases: _Data

One generic control as one design renders it. A tuple of Part objects sends the same label, help text, or error to each destination. children_slot routes authored content to a named slot instead of the default slot. accepts restricts generic prop values this realization can preserve; other values and bound variants use the fallback design.

field tag: str [Required]

the element rendered

field fixed: dict[str, Any] [Optional]

props always set on it (type="button")

field props: dict[str, str | None] [Optional]

generic prop → the attribute it becomes; None drops it as unsupported

field values: dict[str, dict[str, str]] [Optional]

generic prop → {generic value: the design’s value}; an unlisted value passes through

field accepts: dict[str, tuple[Any, ...]] [Optional]

generic prop → values this realization can preserve; other or bound values use the fallback

field label: Part | _Parts [Optional]
field help: Part | _Parts [Optional]
field error: Part | _Parts [Optional]
field invalid: dict[str, Any] [Optional]

props set while error is non-empty (invalid, value-state="Negative"); bound errors update these props reactively

field wrap: Wrap | None = None
field value: Value [Optional]
field options: Options | None = None
field open: Open | None = None
field children_slot: str = ''

route the generic control’s children to this named slot instead of the default slot

field events: dict[str, str] [Optional]

generic event → the design’s event name (change → model-value-changed)

pydantic model spaday.ui.design.Design[source]

Bases: _Data

A design system’s realizations, keyed by generic control kind.

field name: str [Required]
field controls: dict[str, ControlSpec] [Optional]
pydantic model spaday.ui.design.Open[source]

Bases: _Data

How an overlay opens: property prop holds its state; methods — (open, close) — are called instead of setting it when the element opens by method; event reports a close the element did itself (Escape, a backdrop click), so a two-way binding follows. state names a different readable property, including a dotted path, for a method-driven wrapper element.

field prop: str = 'open'
field event: str | None = None
field methods: tuple[str, str] | None = None
field state: str | None = None
pydantic model spaday.ui.design.Options[source]

Bases: _Data

How a select’s options land: as child elements (tag each, the value on attribute value, the label as text, on attribute label, or through one or more Part destinations, disabled state on disabled, and the chosen state on selected). fixed sets props on every child, label_attr repeats its label in an attribute, and item_wrap can wrap each child option; wrap can wrap the complete child list. Property options use the field names on value, label, and disabled. defer assigns a property option list after connection. selection makes a value binding drive the selected state of child options.

field kind: Literal['children', 'prop'] = 'children'
field tag: str = 'option'
field fixed: dict[str, Any] [Optional]
field value: str = 'value'
field label: str | Part | _Parts = 'text'
field label_attr: str | None = None
field disabled: str | None = 'disabled'
field selected: str | None = 'selected'
field name: str = 'items'
field wrap: str = ''
field item_wrap: Wrap | None = None
field defer: bool = False
field selection: bool = False
pydantic model spaday.ui.design.Part[source]

Bases: _Data

Where a control’s text part (its label, help text or error message) lands.

attr sets an attribute named name; slot adds an element (tag, with props) to the control’s slot name; text sets the control’s own text content; child adds the element inside the control, before its other children (after puts it last); sibling adds it next to the control inside the design’s Wrap; none drops the part.

field kind: Literal['attr', 'slot', 'text', 'child', 'sibling', 'none'] = 'attr'
field name: str = ''
field tag: str = 'span'
field props: dict[str, Any] [Optional]
field after: bool = False
pydantic model spaday.ui.design.Value[source]

Bases: _Data

The property carrying a control’s value (checked for a toggle) and, for a two-way binding, the event it changes on when that is not the runtime’s default change/input. codec handles controls whose DOM property exposes a number or typed choice as a string; encode can change only the outbound representation, and state names a different readable property. defer waits until the next animation frame before writing, for controls whose setter requires connected children. scale_by normalizes against another generic prop to scale_to, using scale_default when that prop is omitted.

field prop: str = 'value'
field event: str | None = None
field codec: Literal['number', 'json'] | None = None
field encode: Literal['string'] | None = None
field state: str | None = None
field defer: bool = False
field scale_by: str | None = None
field scale_to: float = 1
field scale_default: float | None = None
pydantic model spaday.ui.design.Wrap[source]

Bases: _Data

An element wrapped around the control, for designs that label controls from outside (a bp-field, a fluent-field, a plain <label>). control props are set on the control inside it (a slot="input").

field tag: str [Required]
field props: dict[str, Any] [Optional]
field control: dict[str, Any] [Optional]
spaday.ui.design.resolve(node: dict, design: Design, *, fallback: Design | None = None) → dict[source]

The serialized tree node with every generic control replaced by what design renders it as. A control the design does not describe is rendered by fallback (the native baseline when not given) and marked data-ui-fallback.

spaday.ui.design.select_design(design: Design | str | None, packages: Any = ()) → Design[source]

The design a page renders its generic controls with.

A Design is used as given. None takes the one design the selected packages publish, or the native baseline when none does; several is an error naming them, since one page renders with one design. A name selects a package’s design by the package’s name, or "native" for the baseline.

spaday.ui.native.NATIVE = Design(name='native', controls={'button': ControlSpec(tag='button', fixed={'type': 'button', 'data-ui': 'button'}, props={'intent': 'data-intent', 'appearance': 'data-appearance', 'size': 'data-size', 'disabled': 'disabled', 'name': 'name'}, values={}, accepts={}, label=Part(kind='text', name='', tag='span', props={}, after=False), help=Part(kind='none', name='', tag='span', props={}, after=False), error=Part(kind='none', name='', tag='span', props={}, after=False), invalid={}, wrap=None, value=Value(prop='value', event=None, codec=None, encode=None, state=None, defer=False, scale_by=None, scale_to=1, scale_default=None), options=None, open=None, children_slot='', events={}), 'input': ControlSpec(tag='input', fixed={'data-ui': 'input'}, props={'disabled': 'disabled', 'required': 'required', 'readonly': 'readonly', 'name': 'name', 'size': 'data-size', 'placeholder': 'placeholder', 'type': 'type'}, values={}, accepts={}, label=Part(kind='sibling', name='', tag='span', props={'data-ui': 'label'}, after=False), help=Part(kind='sibling', name='', tag='span', props={'data-ui': 'help'}, after=True), error=Part(kind='sibling', name='', tag='span', props={'data-ui': 'error'}, after=True), invalid={'data-invalid': True}, wrap=Wrap(tag='label', props={'data-ui': 'field'}, control={}), value=Value(prop='value', event=None, codec=None, encode=None, state=None, defer=False, scale_by=None, scale_to=1, scale_default=None), options=None, open=None, children_slot='', events={}), 'textarea': ControlSpec(tag='textarea', fixed={'data-ui': 'textarea'}, props={'disabled': 'disabled', 'required': 'required', 'readonly': 'readonly', 'name': 'name', 'size': 'data-size', 'placeholder': 'placeholder', 'rows': 'rows', 'minlength': 'minlength', 'maxlength': 'maxlength'}, values={}, accepts={}, label=Part(kind='sibling', name='', tag='span', props={'data-ui': 'label'}, after=False), help=Part(kind='sibling', name='', tag='span', props={'data-ui': 'help'}, after=True), error=Part(kind='sibling', name='', tag='span', props={'data-ui': 'error'}, after=True), invalid={'data-invalid': True}, wrap=Wrap(tag='label', props={'data-ui': 'field'}, control={}), value=Value(prop='value', event=None, codec=None, encode=None, state=None, defer=False, scale_by=None, scale_to=1, scale_default=None), options=None, open=None, children_slot='', events={}), 'number-input': ControlSpec(tag='input', fixed={'type': 'number', 'step': 'any', 'data-ui': 'number-input'}, props={'disabled': 'disabled', 'required': 'required', 'readonly': 'readonly', 'name': 'name', 'size': 'data-size', 'placeholder': 'placeholder', 'min': 'min', 'max': 'max', 'step': 'step'}, values={}, accepts={}, label=Part(kind='sibling', name='', tag='span', props={'data-ui': 'label'}, after=False), help=Part(kind='sibling', name='', tag='span', props={'data-ui': 'help'}, after=True), error=Part(kind='sibling', name='', tag='span', props={'data-ui': 'error'}, after=True), invalid={'data-invalid': True}, wrap=Wrap(tag='label', props={'data-ui': 'field'}, control={}), value=Value(prop='value', event=None, codec='number', encode=None, state=None, defer=False, scale_by=None, scale_to=1, scale_default=None), options=None, open=None, children_slot='', events={}), 'date-input': ControlSpec(tag='input', fixed={'type': 'date', 'data-ui': 'date-input'}, props={'disabled': 'disabled', 'required': 'required', 'readonly': 'readonly', 'name': 'name', 'size': 'data-size', 'min': 'min', 'max': 'max'}, values={}, accepts={}, label=Part(kind='sibling', name='', tag='span', props={'data-ui': 'label'}, after=False), help=Part(kind='sibling', name='', tag='span', props={'data-ui': 'help'}, after=True), error=Part(kind='sibling', name='', tag='span', props={'data-ui': 'error'}, after=True), invalid={'data-invalid': True}, wrap=Wrap(tag='label', props={'data-ui': 'field'}, control={}), value=Value(prop='value', event=None, codec=None, encode=None, state=None, defer=False, scale_by=None, scale_to=1, scale_default=None), options=None, open=None, children_slot='', events={}), 'checkbox': ControlSpec(tag='input', fixed={'type': 'checkbox', 'data-ui': 'checkbox'}, props={'disabled': 'disabled', 'required': 'required', 'readonly': 'readonly', 'name': 'name', 'size': 'data-size'}, values={}, accepts={}, label=Part(kind='sibling', name='', tag='span', props={'data-ui': 'label'}, after=True), help=Part(kind='sibling', name='', tag='span', props={'data-ui': 'help'}, after=True), error=Part(kind='sibling', name='', tag='span', props={'data-ui': 'error'}, after=True), invalid={'data-invalid': True}, wrap=Wrap(tag='label', props={'data-ui': 'field', 'data-inline': True}, control={}), value=Value(prop='checked', event=None, codec=None, encode=None, state=None, defer=False, scale_by=None, scale_to=1, scale_default=None), options=None, open=None, children_slot='', events={}), 'switch': ControlSpec(tag='input', fixed={'type': 'checkbox', 'role': 'switch', 'data-ui': 'switch'}, props={'disabled': 'disabled', 'required': 'required', 'readonly': 'readonly', 'name': 'name', 'size': 'data-size'}, values={}, accepts={}, label=Part(kind='sibling', name='', tag='span', props={'data-ui': 'label'}, after=True), help=Part(kind='sibling', name='', tag='span', props={'data-ui': 'help'}, after=True), error=Part(kind='sibling', name='', tag='span', props={'data-ui': 'error'}, after=True), invalid={'data-invalid': True}, wrap=Wrap(tag='label', props={'data-ui': 'field', 'data-inline': True}, control={}), value=Value(prop='checked', event=None, codec=None, encode=None, state=None, defer=False, scale_by=None, scale_to=1, scale_default=None), options=None, open=None, children_slot='', events={}), 'radio-group': ControlSpec(tag='spa-radio-group', fixed={'data-ui': 'radio-group'}, props={'disabled': 'disabled', 'required': 'required', 'name': 'name', 'size': 'data-size'}, values={}, accepts={}, label=Part(kind='sibling', name='', tag='span', props={'data-ui': 'label'}, after=False), help=Part(kind='sibling', name='', tag='span', props={'data-ui': 'help'}, after=True), error=Part(kind='sibling', name='', tag='span', props={'data-ui': 'error'}, after=True), invalid={'data-invalid': True}, wrap=Wrap(tag='div', props={'data-ui': 'field'}, control={}), value=Value(prop='value', event=None, codec=None, encode=None, state=None, defer=False, scale_by=None, scale_to=1, scale_default=None), options=Options(kind='prop', tag='option', fixed={}, value='value', label='label', label_attr=None, disabled='disabled', selected='selected', name='options', wrap='', item_wrap=None, defer=False, selection=False), open=None, children_slot='', events={}), 'slider': ControlSpec(tag='input', fixed={'type': 'range', 'data-ui': 'slider'}, props={'disabled': 'disabled', 'required': 'required', 'name': 'name', 'size': 'data-size', 'min': 'min', 'max': 'max', 'step': 'step'}, values={}, accepts={}, label=Part(kind='sibling', name='', tag='span', props={'data-ui': 'label'}, after=False), help=Part(kind='sibling', name='', tag='span', props={'data-ui': 'help'}, after=True), error=Part(kind='sibling', name='', tag='span', props={'data-ui': 'error'}, after=True), invalid={'data-invalid': True}, wrap=Wrap(tag='label', props={'data-ui': 'field'}, control={}), value=Value(prop='value', event=None, codec='number', encode=None, state=None, defer=False, scale_by=None, scale_to=1, scale_default=None), options=None, open=None, children_slot='', events={}), 'select': ControlSpec(tag='spa-select', fixed={'data-ui': 'select'}, props={'disabled': 'disabled', 'required': 'required', 'readonly': 'readonly', 'name': 'name', 'size': 'data-size', 'placeholder': 'placeholder'}, values={}, accepts={}, label=Part(kind='sibling', name='', tag='span', props={'data-ui': 'label'}, after=False), help=Part(kind='sibling', name='', tag='span', props={'data-ui': 'help'}, after=True), error=Part(kind='sibling', name='', tag='span', props={'data-ui': 'error'}, after=True), invalid={'data-invalid': True}, wrap=Wrap(tag='label', props={'data-ui': 'field'}, control={}), value=Value(prop='value', event=None, codec=None, encode=None, state=None, defer=False, scale_by=None, scale_to=1, scale_default=None), options=Options(kind='prop', tag='option', fixed={}, value='value', label='label', label_attr=None, disabled='disabled', selected='selected', name='options', wrap='', item_wrap=None, defer=False, selection=False), open=None, children_slot='', events={}), 'dialog': ControlSpec(tag='dialog', fixed={'data-ui': 'dialog'}, props={}, values={}, accepts={}, label=Part(kind='child', name='', tag='h2', props={'data-ui': 'title'}, after=False), help=Part(kind='none', name='', tag='span', props={}, after=False), error=Part(kind='none', name='', tag='span', props={}, after=False), invalid={}, wrap=None, value=Value(prop='value', event=None, codec=None, encode=None, state=None, defer=False, scale_by=None, scale_to=1, scale_default=None), options=None, open=Open(prop='open', event='close', methods=('showModal', 'close'), state=None), children_slot='', events={}), 'alert': ControlSpec(tag='div', fixed={'role': 'alert', 'data-ui': 'alert'}, props={'intent': 'data-intent'}, values={}, accepts={}, label=Part(kind='child', name='', tag='strong', props={'data-ui': 'alert-title'}, after=False), help=Part(kind='none', name='', tag='span', props={}, after=False), error=Part(kind='none', name='', tag='span', props={}, after=False), invalid={}, wrap=None, value=Value(prop='value', event=None, codec=None, encode=None, state=None, defer=False, scale_by=None, scale_to=1, scale_default=None), options=None, open=None, children_slot='', events={}), 'progress': ControlSpec(tag='spa-progress', fixed={'data-ui': 'progress'}, props={'max': 'max'}, values={}, accepts={}, label=Part(kind='sibling', name='', tag='span', props={'data-ui': 'label'}, after=False), help=Part(kind='none', name='', tag='span', props={}, after=False), error=Part(kind='none', name='', tag='span', props={}, after=False), invalid={}, wrap=Wrap(tag='label', props={'data-ui': 'field'}, control={}), value=Value(prop='value', event=None, codec=None, encode=None, state=None, defer=False, scale_by=None, scale_to=1, scale_default=None), options=None, open=None, children_slot='', events={})})

The native design.

Action DSL

Behavior attached to a component with Component.on; see Add behavior and reactivity.

Actions

class spaday.SetProp(target: Ref, prop: str, value: Any)[source]

Bases: Action

Set prop on target to value (an Expr or a plain literal).

class spaday.Toggle(target: Ref, prop: str)[source]

Bases: Action

Flip a boolean prop on target (e.g. hidden, checked, open).

class spaday.Sequence(*actions: Action)[source]

Bases: Action

Run several actions in order.

spaday.ActionSequence

alias of Sequence

class spaday.Emit(event: str, detail: Any = None)[source]

Bases: Action

Dispatch a (bubbling) custom DOM event named event with an optional detail expression.

class spaday.SendPatch(model: str, field: str, value: Any)[source]

Bases: Action

Set field to value on a host-routed model (e.g. a transports model).

The runtime surfaces this as a patch intent (a bubbling spaday:patch DOM event carrying {model, field, value}); the app routes it to the actual wire. This is how a control edit is authored declaratively instead of with a hand-written transports listener.

class spaday.If(cond: Any, then: Action, els: Action | None = None)[source]

Bases: Action

Run then if cond is truthy, else els (if given) — branch on live state, e.g. If(prop(by_id("sw"), "checked"), SetProp(...), SetProp(...)).

class spaday.CallEndpoint(method: str, url: str | Expr, body: Any = None, result: str | None = None)[source]

Bases: Action

A REST round-trip: method url with an optional JSON body. url may be a static string or an Expr; body may be an expression or a plain value. The runtime performs the call with fetch.

Pass result (a signal-store field name) to capture the outcome: on completion the runtime writes {"status": <int>, "ok": <bool>, "body": <parsed JSON or text>} to that field, so success/error feedback stays declarative (bind or Show on it):

CallEndpoint("POST", "/api/order", obj({"symbol": field("symbol")}), result="order_result")

Without result the call is fire-and-forget.

class spaday.NamedJs(handler: str)[source]

Bases: Action

The escape hatch: invoke a pre-registered named JS handler (no arbitrary eval). Register it on the JS side with registerHandler(name, fn); use only for the rare irreducible case.

Expressions and references

spaday.lit(value: Any) → Expr[source]

A literal value.

spaday.event_value(path: str = '') → Expr[source]

The triggering event’s value — a control’s checked (booleans) else value else detail. A dot path walks into the value: event_value("label") reads detail.label from a rich CustomEvent, so one field of a structured detail can land in the store or an endpoint body.

spaday.not_(of: Any) → Expr[source]

Boolean negation of an expression (or a literal).

spaday.prop(target: Ref, name: str) → Expr[source]

The current value of a name prop on target — reads live element state, e.g. prop(by_id("sw"), "checked") for use as a condition.

spaday.field(name: str) → Expr[source]

The current value of a reactive state field — for a computed binding (Component.compute), evaluated against the signal store in the browser, e.g. not_(field("enabled")).

spaday.item(path: str = '') → Expr[source]

Read path from the innermost Each item.

An empty path returns the complete item. Missing paths evaluate to undefined in the browser.

spaday.scope(reference: str) → Expr[source]

Read a named current or ancestor item scope.

scope("staging.channel") reads channel from the nearest scope named staging; scope("staging") returns that scope’s complete item.

spaday.eq(a: Any, b: Any) → Expr[source]

True when two expressions are equal, e.g. eq(field("mode"), "advanced").

spaday.all_(*exprs: Any) → Expr[source]

True when every expression is truthy (logical AND).

spaday.any_(*exprs: Any) → Expr[source]

True when any expression is truthy (logical OR).

spaday.cond(test: Any, then: Any, otherwise: Any) → Expr[source]

A ternary for a computed binding (compute()): then when test is truthy, else otherwise (each a plain value or an Expr). Evaluated against the signal store in the browser — e.g. a boolean dark field driving a string theme prop:

chart.compute("theme", cond(field("dark"), "dark", "light"))
spaday.obj(fields: dict[str, Any]) → Expr[source]

Compose a JSON object from named sub-expressions (each value a plain value or an Expr). Lets a whole model be POSTed declaratively as a CallEndpoint body — composing live control values without a hand-written handler:

CallEndpoint("POST", "/api/order", obj({
    "symbol": prop(by_id("symbol"), "value"),
    "qty": prop(by_id("qty"), "value"),
}))
spaday.this() → Ref[source]

The element the event fired on (the listener’s element).

spaday.by_id(id: str) → Ref[source]

The element with this id within the mounted tree.

Binding helper

spaday.bind is a one-way event-driven convenience (control change → set a target prop). For reactive state bindings prefer Component.bind / Component.compute (above).

spaday.bind(source: Any, target: Ref, target_prop: str, *, transform: Any = None) → Any[source]

One-way reactive binding: when source (a control component) changes, set target_prop on target (a Ref, e.g. by_id("panel")) to the source’s value — optionally passed through transform (e.g. not_()). Returns source so it composes in a tree:

bind(WaSwitch().text("Show"), by_id("panel"), "hidden", transform=not_)

Event-driven (sugar over SetProp on the source’s change); the signal-graph reactive engine and two-way binding are future work.

Validation

spaday.validate(tree: Component | dict) → None[source]

Raise ValidationError if the tree has unresolved by_id(...) references or unknown props.

Pass a Component (or its serialized node dict). Returns None on success.

Two checks run. Every by_id reference (in an action or a prop(...) expression) must resolve to a node’s id in the same tree. And on each node that carries a catalog schema (CEM-generated components retain one), every prop and binding name must be a schema prop or a generic global (id, class, style, slot, …, plus data-*/aria-*); an unknown name is reported with the tag and — when it is the snake_case spelling of a real prop — a “did you mean” hint. On a serialized dict tree, schemas resolve by tag instead, covering every schema-carrying class that has been imported — so validate(component.to_node()) checks the same props as validate(component). Nodes with no schema either way (element(...) tags) stay unvalidated, as do two-way binding names, which target live form-control properties a manifest routinely omits.

exception spaday.ValidationError[source]

Bases: ValueError

Raised by validate() when a component tree has unresolved references.

CEM binding generator

spaday.parse_cem(manifest)

Parse a custom-elements.json manifest into the JSON-encoded list of component schemas.

spaday.generate(manifest_path: str, out_path: str | None = None, *, source: str | None = None) → str[source]

Render a manifest’s components into a Python module; write it to out_path if given.

spaday.classes(manifest_path: str) → dict[str, type[Component]][source]
spaday.classes(manifest_path: str, name: str) → type[Component]

Build Component subclasses from a manifest at runtime.

The dynamic counterpart to generate(): build classes without emitting a file. With name, returns just that one class (MyClass = spaday.classes(manifest, "MyClass")); otherwise returns {class_name: class} for the whole manifest. Handy for binding an arbitrary or one-off manifest on the fly. Unlike a committed, generated peer-package catalog, these classes are not statically typed — the type checker can’t see their per-attribute signatures. They do carry the same schema, so validate() checks their keyword names on the built tree exactly as it does a generated catalog’s. Reach for generate() (committed codegen) when you want typing.

Component catalog schemas

class spaday.ComponentSchema(*, tag: str, class_name: str, summary: str | None = None, props: tuple[PropertySchema, ...] = (), fields: tuple[PropertySchema, ...] = (), events: tuple[str, ...] = (), slots: tuple[str, ...] = ())[source]

Bases: BaseModel

Catalog metadata for one component class and custom-element tag.

field fields: tuple[PropertySchema, ...] = ()

Property-only inputs — public fields the element declares with no attribute of their own, so a manifest leaves them out of props. A data component’s payload (an object no attribute can express) lives here; authors set one exactly like a prop and the runtime writes the property.

classmethod from_cem(schema: Mapping[str, Any]) → ComponentSchema[source]

Build catalog metadata from spaday’s normalized CEM schema.

model_config: ClassVar[ConfigDict] = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

to_dict() → dict[str, Any][source]

Return JSON-serializable catalog data.

class spaday.PropertySchema(*, name: str, kind: Literal['string', 'boolean', 'number', 'enum', 'json'], choices: tuple[str, ...] = (), type_text: str | None = None, default: str | None = None, description: str | None = None)[source]

Bases: BaseModel

One editable DOM property exposed by a component.

model_config: ClassVar[ConfigDict] = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

to_dict() → dict[str, Any][source]

Return JSON-serializable catalog data.

field type_text: str | None = None

The manifest’s declared type, carried only for json props — the kind that says nothing about shape. An author cannot tell {rows, cols, values} from a matrix without it, and the browser reports the difference as a setter throwing far from the mistake.

Serving

Generate a page and deliver it on any backend; see Serve and embed and Sync over transports. The generator is framework-agnostic (spaday.bootstrap); a backend (spaday.backends.<name> — starlette, aiohttp, flask, tornado) wires it into routes.

spaday.backends.starlette.serve(page: Page, *, background: Sequence[Awaitable] = (), lifespan: Callable | None = None, **opts) → Starlette[source]

Create a Starlette app and mount() page onto it. background coroutines run for the app’s lifetime (or pass a custom lifespan for ordered startup, e.g. a clustering relay); all other keyword options are mount()’s (prefix/routes/html/js/title/packages/ wire/ws/tree/reconnect/scripts/stylesheets/styles/head/store/ nonce/persist/url).

spaday.backends.starlette.mount(app: Starlette, page: Page, *, prefix: str = '', routes: Sequence = (), html: str | Path | None = None, js: str | Path | None = None, layout: AssetLayout | None = None, title: str = 'spaday', packages: PackageRef | Sequence[PackageRef] = (), wire: str | dict | Wire | Sequence[dict | Wire] | None = None, ws: str = '/ws', tree: TreeMode = 'json', reconnect: bool = False, scripts: Sequence[str] = (), stylesheets: Sequence[str] = (), styles: Sequence[str] = (), head: str = '', store: dict | None = None, nonce: str | None = None, persist: dict[str, str] | None = None, url: dict[str, str] | None = None, design: Design | str | None = None) → Starlette[source]

Add build_routes() to an existing Starlette app and return the app for chaining.

The host owns the app’s lifespan, so run any transports.autosync in your own lifespan (see examples/embed.py).

spaday.backends.starlette.build_routes(page: Page, *, prefix: str = '', routes: Sequence = (), html: str | Path | None = None, js: str | Path | None = None, layout: AssetLayout | None = None, title: str = 'spaday', packages: PackageRef | Sequence[PackageRef] = (), wire: str | dict | Wire | Sequence[dict | Wire] | None = None, ws: str = '/ws', tree: TreeMode = 'json', reconnect: bool = False, scripts: Sequence[str] = (), stylesheets: Sequence[str] = (), styles: Sequence[str] = (), head: str = '', store: dict | None = None, nonce: str | None = None, persist: dict[str, str] | None = None, url: dict[str, str] | None = None, design: Design | str | None = None) → list[BaseRoute][source]

Build spaday’s Starlette routes (page, tree, /js, plus routes) under prefix. The supplied routes are prefixed too (a Route/WebSocketRoute at /ws becomes {prefix}/ws), so a wired panel’s generated ws URL and its endpoint line up — pass the unprefixed path (WebSocketRoute("/ws", …)) and let build_routes add the prefix. Generation options pass to spaday.bootstrap.bootstrap() (incl. store and nonce, a CSP nonce for the generated scripts); html serves a hand-authored bootstrap instead; js overrides the bundle dir. The caller may register the returned routes itself, including translating page and websocket routes onto a FastAPI router with dependencies, or pass the same options to mount().

spaday.backends.starlette.mount_site(app: Starlette, pages: Mapping[str, Page | PageSpec], *, prefix: str = '', routes: Sequence = (), js: str | Path | None = None, layout: AssetLayout | None = None) → Starlette[source]

Add build_site() routes to an existing Starlette app and return the app.

spaday.backends.starlette.build_site(pages: Mapping[str, Component | object | PageSpec], *, prefix: str = '', routes: Sequence = (), js: str | Path | None = None, layout: Literal['source', 'installed'] | None = None) → SiteRoutes[source]

Build a multi-page Starlette site with one shared asset surface.

pages maps URL paths to a PageSpec; a bare page is shorthand for PageSpec(page). "/" remains the root route (including its trailing slash), while "/login" is served without one. Each JSON or frame page gets its own tree URL beneath its page path. Package and core assets are mounted once under prefix.

Page paths are static; put parameterized endpoints in routes. Paths are normalized by collapsing repeated and trailing slashes. Duplicate normalized paths, generated tree collisions, overlapping supplied GET/HEAD routes, and pages under the reserved /js or /components asset paths are rejected. Supplied POST routes and websockets may share a page URL.

class spaday.backends.starlette.PageSpec(page: Component | object, html: str | Path | None = None, title: str = 'spaday', packages: ComponentPackage | str | Sequence[ComponentPackage | str] = (), wire: str | dict | Wire | Sequence[dict | Wire] | None = None, ws: str = '/ws', tree: Literal['json', 'frame', 'inline'] = 'json', reconnect: bool = False, scripts: Sequence[str] = (), stylesheets: Sequence[str] = (), styles: Sequence[str] = (), head: str = '', store: dict | None = None, nonce: str | None = None, persist: dict[str, str] | None = None, url: dict[str, str] | None = None, design: Design | str | None = None)[source]

Bases: object

One page in build_site().

Fields match build_routes()’ page-generation options. Routes, the core asset directory, and the mount prefix belong to the site and are passed to build_site() instead.

class spaday.backends.starlette.SiteRoutes(pages: tuple[Route, ...], supplied: tuple[BaseRoute, ...], assets: tuple[Mount, ...])[source]

Bases: object

Structured routes returned by build_site().

pages contains generated page and tree endpoints, supplied contains caller-provided routes, and assets contains public component and core static mounts. The split lets a FastAPI host apply dependencies to application routes without putting authentication in front of static assets.

all() → list[BaseRoute][source]

Return all routes in safe mounting order.

spaday.bootstrap.bootstrap(*, base: str = '', packages: ComponentPackage | str | Sequence[ComponentPackage | str] = (), wire: str | dict | Wire | Sequence[dict | Wire] | None = None, ws: str = '/ws', tree: Literal['json', 'frame', 'inline'] = 'json', page: Component | object | None = None, tree_url: str | None = None, reconnect: bool = False, scripts: Sequence[str] = (), stylesheets: Sequence[str] = (), styles: Sequence[str] = (), head: str = '', title: str = 'spaday', store: dict | None = None, fragment: bool = False, target: str | None = None, nonce: str | None = None, layout: Literal['source', 'installed'] | None = None, persist: dict[str, str] | None = None, url: dict[str, str] | None = None, design: Design | str | None = None) → str[source]

The bootstrap markup (init the wasm core, fetch the tree, mount it). base prefixes the tree / /js / ws URLs so the page can be mounted under a sub-path. store seeds a local signal Store (reactive UI state for two-way bindings + field actions) even without a wire. persist maps store fields to localStorage keys: a persisted value overrides the field’s seed at boot (before the tree mounts) and every later write to the field is stored, so per-browser preferences (a theme toggle, a chosen view) survive reloads. url maps store fields to query parameters the same way, against the page URL: a parameter seeds its field at boot (after persist — a deep link beats a remembered preference), every later change pushes a history entry, and back/forward write the field back — so what the user is looking at is linkable, bookmarkable, and survives a reload, and a Switch on a bound field is a router. Strings ride the URL verbatim; a field seeded with another type JSON-encodes and reads back as JSON; None/"" clears the parameter.

wire="transports" mirrors one model with the default connection settings. A Wire configures one connection; a list mirrors several models into one store, each namespaced so their fields don’t collide (a chart on global.* next to one on session.*).

By default returns a whole HTML document. With fragment=True it returns just the package tags + the module <script> — a snippet to drop into a host page’s template (Jinja/Django/…), so spaday is one component among many rather than the whole page. Pass target (a CSS selector) to mount into a specific element (e.g. "#widget") instead of document.body; the host provides that element. nonce stamps the generated <script>/<link>/<style> tags with a CSP nonce, so a host with a strict script-src/style-src policy can allow the snippet. stylesheets adds <link rel="stylesheet"> URLs and styles inline <style> blocks to <head> — both nonce-stamped, unlike raw head markup, which is concatenated verbatim. See the module docstring for the rest of the options and the route contract. packages selects external ComponentPackage descriptors directly, by module:attribute path, or by installed entry-point name. tree="inline" requires page and embeds its current tree directly in the module script; callable pages are evaluated once when this markup is built. design selects how that inline page resolves generic controls; JSON and frame modes apply their design when the separate tree route serializes the page. tree_url overrides the JSON or frame fetch URL without changing the asset and websocket base; inline trees reject it because they perform no initial tree fetch. layout selects source-checkout or installed-wheel asset URLs; by default it follows bundles_dir().

class spaday.Wire(url: str, namespace: str | None = None, session: bool = False, flatten: bool = True, codec: str = 'json', batch: bool = False, reconnect: bool = False, retry: int = 1000, authority: Literal['server', 'client'] = 'server', connected: str | None = None)[source]

Bases: object

One transports model wire for a multi-model page — a typed, discoverable alternative to a raw dict in serve/bootstrap wire=[…] (both forms are accepted, mix freely):

  • url — the websocket endpoint the model is mirrored over (matches a backend routes= entry).

  • namespace — mirror the model’s fields under <namespace>. so several models share one signal store without colliding (two Chart models on global.* / session.*); omit for bare fields.

  • session — append ?session=<uuid> so the model is a fresh per-page-load tenant (a Hub).

  • flatten — recurse nested sub-models to dotted parent.child fields (the default, what a form binds); set False for an opaque map/dict field (a chart’s time-keyed data, a Perspective layout) so it’s mirrored whole.

  • codec — the transports connection codec ("json", "msgpack", "cbor", or a registered custom codec).

  • batch — ask the server to batch outbound messages for this connection.

  • reconnect — keep reconnecting with transports’ managed Client.run. This is opt-in for each wire; retry is the delay in milliseconds and authority selects server- or client-authoritative recovery.

  • connected — publish a connection-ready boolean to this exact store field. Namespaced wires default to <namespace>.connected; bare wires publish no status unless this is set. The field becomes true when the managed WebSocket opens, including a reconnect that replays no model frame.

Wire("/ws", namespace="global", flatten=False) reads better than {"url": "/ws", …} and gives editor help; it serializes to exactly that dict.

spaday.bootstrap.tree_json(page: Component | object, design: Design | str | None = None) → str[source]

The authored tree as a JSON string (serve at GET {base}/tree.json).

spaday.bootstrap.tree_frame(page: Component | object, *, id: str = 'spa-tree', design: Design | str | None = None) → bytes[source]

The authored tree as a transports Snapshot frame (serve at GET {base}/tree for tree="frame") — the same length-prefixed, codec-tagged envelope transports uses for model state, so the UI tree and the model data ride one wire.

spaday.bootstrap.bundles_dir(layout: Literal['source', 'installed'] | None = None) → Path[source]

Directory a backend serves at {base}/js.

Uses the source checkout’s js/ directory when present and otherwise the wheel’s packaged spaday/extension assets. layout can force either form, mainly when serving a custom asset directory with a backend’s js= option.

External component packages

class spaday.ComponentPackage(name: str, assets_dir: Path, assets: Sequence[tuple[str, str]], components: Sequence[type[Component]] = (), imports: Sequence[tuple[str, str]] = (), provides: Mapping[str, str] | Sequence[tuple[str, str]] = (), requires: Mapping[str, str] | Sequence[tuple[str, str]] = (), design: Design | None = None)[source]

Bases: object

Assets and catalog metadata for one external component library.

assets contains ("css" | "js", relative_path) pairs under assets_dir. Backends serve that directory at {prefix}/components/{name}; spaday.bootstrap.bootstrap() emits the matching tags. components contains the package’s public Component subclasses. Generated CEM classes already carry schemas; hand-authored classes set Component.schema. catalog returns those schemas without constructing components.

imports publishes vendored modules under their bare specifiers as (specifier, relative_path) pairs, which bootstrap() emits as an import map. It exists for engines that register global custom element names – Perspective, a design system – where a second copy on the page throws from customElements.define and the two cannot coexist. A package that publishes its copy this way lets every other library on the page resolve the same bare specifier to it, so there is one copy rather than a collision. A specifier ending in / maps a whole subtree, per the import-map spec. Two packages publishing the same specifier at different paths is an error: it is the ambiguity the feature exists to remove.

provides records the JS libraries the package puts on the page, by npm name, with the exact version it serves ({"@awesome.me/webawesome": "3.1.0"}); a package’s build writes them, so the Python side knows what the browser gets. design publishes the package’s Design — how it renders the generic controls of spaday.ui — which a page selecting the package renders with.

requires records libraries the package’s own bundle imports without shipping, with the npm version range it was built against ({"@awesome.me/webawesome": "^3.1.0"}). resolve_component_packages() reconciles them across the packages selected for a page: a page holds one copy of a library, so two packages serving it at different versions, or a requirement no selected package satisfies, is an error naming the packages and versions, where the second copy would otherwise half-work. Packages serving the same version of a library may both publish it; the page imports the first.

spaday.resolve_component_packages(packages: ComponentPackage | str | Sequence[ComponentPackage | str] = ()) → tuple[ComponentPackage, ...][source]

Resolve descriptors, module:attribute paths, or installed entry-point names.

Entry points are loaded only when explicitly named; installing an integration never injects assets into unrelated applications.

spaday.discover_component_packages() → tuple[ComponentPackage, ...][source]

Load every installed component-package entry point, sorted by entry-point name.

spaday.discover_component_package_names() → tuple[str, ...][source]

Return installed component-package entry-point names without loading them.

Server-side rendering

spaday.render_html(tree: Component | dict, design: Design | str | None = None) → str[source]

Render a component (or an already-built node dict) to a light-DOM HTML string for hydration.

Notebook host

Web Worker host

class spaday.WorkerApp(render: Callable[[], Component | dict], on_intent: Callable[[dict], None], *, design: Design | str | None = None)[source]

Bases: object

Adapt a render function and intent handler to spaday’s worker message protocol.

dispatch(intent: dict) → dict[source]

Handle one browser intent and return its incremental tree patch message.

dispatch_json(intent: str) → str[source]

Decode a JSON intent and return dispatch() as JSON.

start() → dict[source]

Render and return the initial tree snapshot message.

start_json() → str[source]

Return start() as JSON for a JavaScript worker boundary.

Core diff / apply

The low-level component-tree engine (JSON wire form), shared byte-for-byte with the browser runtime. encode_frame / decode_frame wrap a tree (or patch) in a transports Frame so the UI rides the same envelope as model state (used by tree="frame").

spaday.diff(old, new)

Diff two JSON-encoded component trees, returning the JSON-encoded patch.

Thin wrapper over the shared core (spaday::diff_json); the same code runs in the wasm binding.

spaday.apply(root, patch)

Apply a JSON-encoded patch to a JSON-encoded tree, returning the JSON-encoded result.

spaday.encode_frame(payload, model_type, kind, rev, codec)

Frame a JSON-encoded tree/patch into transports’ length-prefixed envelope bytes.

kind is “snapshot” or “patch”; codec is “application/json” or “application/msgpack”.

spaday.decode_frame(frame)

Decode one frame back to a {“model_type”,”kind”,”rev”,”payload”} JSON string.

Theming

The spa-* shell components are re-themed by setting their --spa-* CSS custom properties via Component.css (e.g. App().css(spa_surface="#111", spa_border="#333"), which cascades to the whole shell). spaday.SHELL_TOKENS maps each css() keyword to the CSS custom property it drives and what it controls — spa_surface, spa_surface_2, spa_border, spa_text, spa_muted, spa_gap, spa_align, spa_justify, spa_gutter_width. The shell ships neutral light and dark defaults — the dark palette is keyed off WebAwesome’s wa-dark class (wa-light flips a nested island back), so App(...).bind_root_class("wa-dark", "dark") alone re-themes the whole page. Both palettes are emitted at zero specificity, so an application or component package overrides them by mapping its own theme tokens onto those variables.

wa-dark/wa-light on the root is spaday’s page-mode convention, and component packages join it through whichever channel themes them:

  • a package themed by CSS tokens ships mode-keyed rules in its own package stylesheet (its css assets land in <head> via packages=, and custom properties cascade into its shadow roots) — key them off the same classes, at low specificity (:where(.wa-dark) { --my-token: … }), exactly as the shell does;

  • a package themed by a prop or constructor options (Perspective’s theme, Lightweight Charts) binds that prop to the same field that drives the root class: component.compute("theme", cond(field("dark"), "dark", "light")).

Component packages expose their theme metadata through TOKENS. A spaday.Token contains the CSS property, description, and optional shell fallback, so tooling can check theme coverage without parsing prose. It remains a two-item tuple for existing consumers.

class spaday.Token(property: str, description: str, *, fallback: str | None = None)[source]

Bases: tuple[str, str]

Metadata for one theme token.

Token is a two-item (property, description) tuple. Existing unpacking, indexing, equality checks, and tuple type checks therefore keep working; new consumers can use named accessors and read fallback directly.

A class states a boolean. For page-level state that carries a value — a design system whose tokens hang off :root[data-density='comfortable'], or a family of root flags in one control group — Component.bind_root_attr(name, field) writes an attribute on <html> instead:

App(...).bind_root_attr("data-density", "density")  # "comfortable" | None
App(...).bind_root_attr("data-vivid", "vivid")      # True | False

The field’s value is written as the runtime writes any attribute: None/False remove it, True and "" give the bare form (data-vivid), anything else is stringified — so an enumerated field replaces the attribute’s value rather than accumulating names, and clearing the field removes it. Both root bindings are one-way and seeded from the store at mount, so a field restored by persist= or url= themes the page before the tree renders.