Cheat sheet¶
The whole authoring surface on one page. Each section links to the full treatment.
The model, in 30 seconds¶
A component = name.py (sited signals + handlers) beside name.html (real HTML + :/@ binders). One mount call compiles the pair once and derives every route; the browser gets a 5–12 KB generated island over a shared shim. The site decides the transport. await in a handler makes it a server command. Everything outside the dialect is a CompileError.
Sites¶
| Declaration | Lives | Client | Server | Set by |
|---|---|---|---|---|
x: Local[T] = default |
browser | S.get/S.set, compiled |
— | code |
x: Url[str] = "all" |
query string | read-only | context(view) |
<a href="../?x=…"> |
x: Server[Facade] |
server | any access = CompileError |
facade in handlers, rows in template | commands |
x: Synced[T] |
reserved | — | — | — |
The .py side¶
from __future__ import annotations # required
from situ import Local, Url, Server # + Synced, Prop, Emit, Provide, Inject
search: Local[str] = ""
tags: Local[set] = set() # Local[set] is first-class
filter: Url[str] = "all"
issues: Server[IssuesFacade] # Protocol under TYPE_CHECKING
def clear(): # no await → compiled to client JS
global search # bare-name signal write: declare global
search = ""
async def close(id): # await → POST /cmd/close/{id}
await issues.set_status(id, "closed")
reset("search") # injected: back to defaults, rides the response
signal("toast", "Closed") # injected: push a value into a client signal
The .html side — two rules¶
Every reactive element lives in <header> (survives command swaps — search boxes, dialogs, toasts) or in the single <div data-region> (re-rendered by commands). data-region is that div's first attribute.
Events & binders¶
| Token | Does | Notes |
|---|---|---|
@click @dblclick @submit @input @escape |
listeners | @submit preventDefaults; rows allow only @click/@submit |
:text :show :class="c: expr" :attr="a: expr" |
DOM updates | :show sets hidden — keep [hidden]{display:none!important} in your CSS |
:bind="sig" |
two-way input | Local only; type=number coerces |
:each="r in list" |
keyed client reconciler | Local[list]; on a wrapper element; rows keyed by id |
:tree="r.children" |
self-recursive rows | inside a :each |
:virtual |
windowed rows via /window |
server handler + window_template= |
:autohide="sig" |
self-clearing status | |
:tabs :table :combobox :datagrid+sel :menu :palette :sortable :cells |
kit widget runtimes | load widgets.js; data stays in Locals |
Server rows: {% for t in issues %} + data-id="{{ t.id }}" data-title="{{ t.title }}" → binders on/inside the row may read t.id, t.title (dataset). Full table
The dialect (client-compiled Python)¶
Expressions: names · literals · and or not · == != < <= > >= (numeric-normalized, strict) · + - * / % · x if c else y · in/not in (Set.has / .includes) · f-strings (no !r/format specs) · [i]/slices · {...}/[...] literals · len sum(gen) min max abs round today() · .strip() .lower() .upper() .startswith() .endswith()
Statements: x = expr (single target) · += -= *= /= %= · if/elif/else · return · s.add/.discard, l.append/.remove · reset(...) · global
Rejected, always loudly: loops · while with try yield lambda · comprehensions (beyond the sum generator) · imports · nested defs · unknown names/calls — and every client touch of Server state. Dialect · Errors
Actions (@click="…"): ;-separated sig = expr + calls. Leading local assigns before a server call = optimistic apply, auto-rollback on failure.
Mounts¶
# Litestar (the reference adapter; from `situ`) — lazily imported, no Litestar until called
mount_static_component(path=, stem=, template=, meta=) # pure client
mount_component(path=, stem=, template=, meta=, # + the server seam
service_type=Store, context=ctx, # Litestar mount: Dishka container
components=Context.from_dir(DIR), # optional tree
live=True, window_template=..., unified=True) # opt-ins
mount_tree(path=, root=, components=, template=, meta=) # pure-client tree
# Flask (from `situ.mount.flask`, in situ[flask]) — a Blueprint, no Dishka, no Litestar
mount_flask(path=, app_factory=lambda cmd: compile_app(front, cmd), template=, meta=,
resolve=lambda: store, context=ctx, facade=Store)
Serve situ.static_dir() at /static; template dirs: [yours, situ.templates_dir()]. API
The command wire¶
POST {mount}/cmd/{name}[/{id:int}] X-Siting: 1 · form fields + local `with`s + url params
← 200 region HTML + X-Siting-Signals: {"sig": value} (resets/pushes)
In flight: control disabled, region aria-busy. Non-2xx: rollback, no swap. Wire protocol
Composition¶
<Confirm :open="flag" @confirmed="save"/> <!-- Prop / Emit, name-matched -->
<Card><template slot="title">Hi</template>body</Card> <!-- slots: parent scope -->
Provide[T]/Inject[T] = ancestor value channel. A child owning Locals + handlers is uncontrolled — place it N times, get N independent states. Child <style> scopes to the child. Cycles error; data recursion = :each + :tree. Check trees statically: python -m situ.check ROOT --from-dir DIR [--kit]. Composition
declui, in one breath¶
class Issue: # @dataclass / attrs / msgspec / pydantic / SQLAlchemy (info={"declui": Field(...)})
id: Annotated[int, Field(hidden=True)]
title: Annotated[str, Field(label_field=True, search=True)]
status: Status # Enum → <select>
owner: User | None # reference → <select>/Combobox
notes: list[Note] # one-to-many, read-only
bio: Annotated[str, Field(widget="RichText", editable="rating > 50")]
@action(visible="status == 'open'", message="Closed") # async → per-row command
async def close(self): ...
mount_model(path=, screen=Screen(model=Issue, zones={...}), # form (default)
screens=("list","detail"), rows=[...], choices={...}) # client modes
mount_model(path=, screen=, service_type=, facade=, context=) # server tracker
Predicates (visible/editable/valid, object_editable) read value / siblings / editing → client binders; @action(visible=) → a server row gate. valid="cond or 'message'". declui
Gotchas¶
- Components compile once per process — run with
--reloadduring development. [hidden] { display: none !important; }in your CSS, or:showloses to yourdisplay:rules.- Buttons in forms default to
type=submit— give Canceltype="button". - A
@submithandler's params bind from the form'sname=fields. - The Litestar
mount_componentneeds a Dishka container (request.state.dishka_container); the Flaskmount_flasktakes aresolvecallable instead. Local[set]crosses to commands as a JSON array.- A
Urlsignal is set by navigation only; changing it resets in-progressLocalstate.
Machine-readable versions of this reference: /llms.txt (index) and /llms-full.txt (the single-file reference for AI coding tools).