API reference¶
The Python surface of spaday. Peer-package component classes are not listed here; see Author a component tree and Generate typed classes.
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) anderror(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;openfor 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:
ControlA 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:
ControlA button. Its
labelis 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:
_ToggleA checkbox. Its
valueis 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:
ComponentBase of the generic controls: a
ui-<kind>node any design renders.
- 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:
ControlA calendar-date input whose value, minimum and maximum are ISO
YYYY-MM-DDstrings.
- 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:
ControlA modal dialog whose children are its content and whose
labelis its title. Bindopentwo-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:
ControlA numeric input. Its value is a number, or
Nonewhile 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:
ControlProgress toward
max. Omitvaluefor 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:
ControlA 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:
ControlA single-choice select over scalar values or
{value, label, disabled}objects. Bindvaluetwo-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
optionsto 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:
ControlA 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:
_ToggleAn on/off switch. Its
valueis a boolean; bind it two-way. (Exported at the top level asToggleSwitch, beside the shell’sSwitchrouter.)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:
ControlA 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:
ControlA 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:
_DataOne generic control as one design renders it. A tuple of
Partobjects sends the same label, help text, or error to each destination.children_slotroutes authored content to a named slot instead of the default slot.acceptsrestricts 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;
Nonedrops 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 invalid: dict[str, Any] [Optional]¶
props set while
erroris non-empty (invalid,value-state="Negative"); bound errors update these props reactively
- 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:
_DataA 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:
_DataHow an overlay opens: property
propholds its state;methods—(open, close)— are called instead of setting it when the element opens by method;eventreports a close the element did itself (Escape, a backdrop click), so a two-way binding follows.statenames 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:
_DataHow a select’s
optionsland: as child elements (tageach, the value on attributevalue, the label as text, on attributelabel, or through one or morePartdestinations, disabled state ondisabled, and the chosen state onselected).fixedsets props on every child,label_attrrepeats its label in an attribute, anditem_wrapcan wrap each child option;wrapcan wrap the complete child list. Property options use the field names onvalue,label, anddisabled.deferassigns a property option list after connection.selectionmakes a value binding drive theselectedstate 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_attr: str | None = None¶
- field disabled: str | None = 'disabled'¶
- field selected: str | None = 'selected'¶
- field name: str = 'items'¶
- field wrap: str = ''¶
- field defer: bool = False¶
- field selection: bool = False¶
- pydantic model spaday.ui.design.Part[source]¶
Bases:
_DataWhere a control’s text part (its label, help text or error message) lands.
attrsets an attribute namedname;slotadds an element (tag, withprops) to the control’s slotname;textsets the control’s own text content;childadds the element inside the control, before its other children (afterputs it last);siblingadds it next to the control inside the design’sWrap;nonedrops 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:
_DataThe property carrying a control’s value (
checkedfor a toggle) and, for a two-way binding, the event it changes on when that is not the runtime’s defaultchange/input.codechandles controls whose DOM property exposes a number or typed choice as a string;encodecan change only the outbound representation, andstatenames a different readable property.deferwaits until the next animation frame before writing, for controls whose setter requires connected children.scale_bynormalizes against another generic prop toscale_to, usingscale_defaultwhen 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:
_DataAn element wrapped around the control, for designs that label controls from outside (a
bp-field, afluent-field, a plain<label>).controlprops are set on the control inside it (aslot="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
nodewith every generic control replaced by whatdesignrenders it as. A control the design does not describe is rendered byfallback(the native baseline when not given) and markeddata-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
Designis used as given.Nonetakes the one design the selectedpackagespublish, 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:
ActionSet
propontargettovalue(anExpror a plain literal).
- class spaday.Toggle(target: Ref, prop: str)[source]¶
Bases:
ActionFlip a boolean
propontarget(e.g.hidden,checked,open).
- class spaday.Emit(event: str, detail: Any = None)[source]¶
Bases:
ActionDispatch a (bubbling) custom DOM event named
eventwith an optionaldetailexpression.
- class spaday.SendPatch(model: str, field: str, value: Any)[source]¶
Bases:
ActionSet
fieldtovalueon a host-routedmodel(e.g. a transports model).The runtime surfaces this as a patch intent (a bubbling
spaday:patchDOM 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:
ActionRun
thenifcondis truthy, elseels(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:
ActionA REST round-trip:
methodurlwith an optional JSONbody.urlmay be a static string or anExpr;bodymay be an expression or a plain value. The runtime performs the call withfetch.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 orShowon it):CallEndpoint("POST", "/api/order", obj({"symbol": field("symbol")}), result="order_result")
Without
resultthe call is fire-and-forget.
Expressions and references¶
- spaday.event_value(path: str = '') Expr[source]¶
The triggering event’s value — a control’s
checked(booleans) elsevalueelsedetail. A dotpathwalks into the value:event_value("label")readsdetail.labelfrom a rich CustomEvent, so one field of a structured detail can land in the store or an endpoint body.
- spaday.prop(target: Ref, name: str) Expr[source]¶
The current value of a
nameprop ontarget— 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
pathfrom the innermostEachitem.An empty path returns the complete item. Missing paths evaluate to
undefinedin the browser.
- spaday.scope(reference: str) Expr[source]¶
Read a named current or ancestor item scope.
scope("staging.channel")readschannelfrom the nearest scope namedstaging;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.cond(test: Any, then: Any, otherwise: Any) Expr[source]¶
A ternary for a computed binding (
compute()):thenwhentestis truthy, elseotherwise(each a plain value or anExpr). Evaluated against the signal store in the browser — e.g. a booleandarkfield 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 aCallEndpointbody — 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"), }))
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, settarget_propontarget(aRef, e.g.by_id("panel")) to the source’s value — optionally passed throughtransform(e.g.not_()). Returnssourceso it composes in a tree:bind(WaSwitch().text("Show"), by_id("panel"), "hidden", transform=not_)
Event-driven (sugar over
SetPropon the source’schange); the signal-graph reactive engine and two-way binding are future work.
Validation¶
- spaday.validate(tree: Component | dict) None[source]¶
Raise
ValidationErrorif the tree has unresolvedby_id(...)references or unknown props.Pass a
Component(or its serialized node dict). ReturnsNoneon success.Two checks run. Every
by_idreference (in an action or aprop(...)expression) must resolve to a node’s id in the same tree. And on each node that carries a catalogschema(CEM-generated components retain one), every prop and binding name must be a schema prop or a generic global (id,class,style,slot, …, plusdata-*/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 — sovalidate(component.to_node())checks the same props asvalidate(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:
ValueErrorRaised 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_pathif given.
- spaday.classes(manifest_path: str) dict[str, type[Component]][source]¶
- spaday.classes(manifest_path: str, name: str) type[Component]
Build
Componentsubclasses from a manifest at runtime.The dynamic counterpart to
generate(): build classes without emitting a file. Withname, 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 sameschema, sovalidate()checks their keyword names on the built tree exactly as it does a generated catalog’s. Reach forgenerate()(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:
BaseModelCatalog 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].
- 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:
BaseModelOne 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].
- field type_text: str | None = None¶
The manifest’s declared type, carried only for
jsonprops — 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()pageonto it.backgroundcoroutines run for the app’s lifetime (or pass a customlifespanfor ordered startup, e.g. a clustering relay); all other keyword options aremount()’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 Starletteappand return the app for chaining.The host owns the app’s lifespan, so run any
transports.autosyncin your own lifespan (seeexamples/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, plusroutes) underprefix. The suppliedroutesare prefixed too (aRoute/WebSocketRouteat/wsbecomes{prefix}/ws), so a wired panel’s generated ws URL and its endpoint line up — pass the unprefixed path (WebSocketRoute("/ws", …)) and letbuild_routesadd the prefix. Generation options pass tospaday.bootstrap.bootstrap()(incl.storeandnonce, a CSP nonce for the generated scripts);htmlserves a hand-authored bootstrap instead;jsoverrides 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 tomount().
- 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.
pagesmaps URL paths to aPageSpec; a bare page is shorthand forPageSpec(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 underprefix.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/jsor/componentsasset 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:
objectOne 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 tobuild_site()instead.
- class spaday.backends.starlette.SiteRoutes(pages: tuple[Route, ...], supplied: tuple[BaseRoute, ...], assets: tuple[Mount, ...])[source]¶
Bases:
objectStructured routes returned by
build_site().pagescontains generated page and tree endpoints,suppliedcontains caller-provided routes, andassetscontains 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.
- 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).
baseprefixes the tree //js/ ws URLs so the page can be mounted under a sub-path.storeseeds a local signalStore(reactive UI state for two-way bindings +fieldactions) even without awire.persistmaps 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.urlmaps store fields to query parameters the same way, against the page URL: a parameter seeds its field at boot (afterpersist— 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 aSwitchon 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. AWireconfigures one connection; a list mirrors several models into one store, each namespaced so their fields don’t collide (a chart onglobal.*next to one onsession.*).By default returns a whole HTML document. With
fragment=Trueit 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. Passtarget(a CSS selector) to mount into a specific element (e.g."#widget") instead ofdocument.body; the host provides that element.noncestamps the generated<script>/<link>/<style>tags with a CSP nonce, so a host with a strictscript-src/style-srcpolicy can allow the snippet.stylesheetsadds<link rel="stylesheet">URLs andstylesinline<style>blocks to<head>— both nonce-stamped, unlike rawheadmarkup, which is concatenated verbatim. See the module docstring for the rest of the options and the route contract.packagesselects externalComponentPackagedescriptors directly, bymodule:attributepath, or by installed entry-point name.tree="inline"requirespageand embeds its current tree directly in the module script; callable pages are evaluated once when this markup is built.designselects how that inline page resolves generic controls; JSON and frame modes apply their design when the separate tree route serializes the page.tree_urloverrides the JSON or frame fetch URL without changing the asset and websocketbase; inline trees reject it because they perform no initial tree fetch.layoutselects source-checkout or installed-wheel asset URLs; by default it followsbundles_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:
objectOne transports model wire for a multi-model page — a typed, discoverable alternative to a raw dict in
serve/bootstrapwire=[…](both forms are accepted, mix freely):url— the websocket endpoint the model is mirrored over (matches a backendroutes=entry).namespace— mirror the model’s fields under<namespace>.so several models share one signal store without colliding (twoChartmodels onglobal.*/session.*); omit for bare fields.session— append?session=<uuid>so the model is a fresh per-page-load tenant (aHub).flatten— recurse nested sub-models to dottedparent.childfields (the default, what a form binds); setFalsefor an opaque map/dict field (a chart’s time-keyeddata, a Perspectivelayout) 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’ managedClient.run. This is opt-in for each wire;retryis the delay in milliseconds andauthorityselects 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}/treefortree="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 packagedspaday/extensionassets.layoutcan force either form, mainly when serving a custom asset directory with a backend’sjs=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:
objectAssets and catalog metadata for one external component library.
assetscontains("css" | "js", relative_path)pairs underassets_dir. Backends serve that directory at{prefix}/components/{name};spaday.bootstrap.bootstrap()emits the matching tags.componentscontains the package’s publicComponentsubclasses. Generated CEM classes already carry schemas; hand-authored classes setComponent.schema.catalogreturns those schemas without constructing components.importspublishes vendored modules under their bare specifiers as(specifier, relative_path)pairs, whichbootstrap()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 fromcustomElements.defineand 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.providesrecords 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.designpublishes the package’sDesign— how it renders the generic controls ofspaday.ui— which a page selecting the package renders with.requiresrecords 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:attributepaths, 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.
Server-side rendering¶
Notebook host¶
Web Worker host¶
- class spaday.WorkerApp(render: Callable[[], Component | dict], on_intent: Callable[[dict], None], *, design: Design | str | None = None)[source]¶
Bases:
objectAdapt 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.
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
cssassets land in<head>viapackages=, 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.
Tokenis 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 readfallbackdirectly.
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.