Design Spec · Companion

Canonical Examples for violetear

draft 2026-05-20 Companion to issues/6-canonical-examples-design.md

Goal

Replace examples/ (currently 9 ad-hoc files with overlapping concerns) with a minimal canonical set of 5 progressively-richer, independent showcases that together stress-test the entire shipped violetear surface, while each remains simple enough to read end to end.

Each example is independent — readable in isolation, no shared helper module, no assumption that the previous example has been read. The progression is in capability tier, not in domain or code reuse.

Legend for flow diagrams

user action client-side (Pyodide) server-side (FastAPI) state mutation I/O artifact

Overview

#FileTierDomain
0101_static.pyPure markup + CSS, no serverDesign-tokens reference page
0202_ssr.pyServer-only, no Pyodide bundleGuestbook (GET list + POST add)
0303_interactive.pySSR + client-side Python, single userUnit converter (m / ft / in)
0404_pwa.pyInstallable + offlinePomodoro timer
0505_realtime.pyMulti-user via WebSocketChat room with presence

01 · Design tokens reference page

Mockup
01_static.html
Design Tokens
Aa Aa Aa

What it stresses

  • StyleSheet with many .select(...) rules.
  • Style fluent builder (.font, .color, .padding, .background, .border, .rounded, .flexbox).
  • A wide slice of Colors.* named registry.
  • Multiple Unit types (px, rem, %, em).
  • Document with head styles, body content tree, HTML.div(...).style(...) chaining, ElementSet.spawn for the swatch grid.

Flow

Document + StyleSheet .render() 01_static.html + 01_static.css

Verification

Test imports the module, invokes its build function, asserts <!DOCTYPE html> in rendered HTML and a known color hex (e.g. #D97757) in rendered CSS.

02 · Guestbook (SSR + form POST)

Mockup
Guestbook · localhost:8000
Alex · 14:02 · Hello, world!
Sam · 14:05 · Nice site.
Iris · 14:11 · 👋
Add entry

What it stresses

  • @app.view for GET routes.
  • Native FastAPI @app.api.post for form-driven mutation — deliberate signal that violetear doesn't yet have a first-class form-POST helper and that's fine for SSR.
  • doc.style(href=..., sheet=...) registering an auto-served stylesheet route.
  • <form> / <input> / <button> via the markup builder.

Flow

browser GET / @app.view("/") read entries[] Document.render() HTML response
browser POST /entries app.api.post("/entries") entries.append({name, msg, ts}) 303 redirect → /

Verification

TestClient: GET / returns 200 with the empty-state markup; POST /entries with form data returns 303; subsequent GET shows the new entry.

03 · Unit converter (reactive + RPC, single user)

Mockup
Unit Converter
1.0
3.281
39.370
quick precise

What it stresses

  • @app.local with multiple primitive fields (m, ft, in, mode).
  • data-bind-value on the inputs — SSR-rendered, hydrated.
  • Multiple @app.client.callback on different events (one per input).
  • @app.client.on("ready") lifecycle.
  • @app.server.rpc with float args + dict return.
  • violetear.storage.store round-trip.
  • violetear.dom.DOM.find.

Flow

user types in m field on_meters_change(event) UiState.meters = v
ReactiveRegistry.notify("UiState.meters") other inputs update via data-bind-value
if mode == "precise":
await precise_convert(meters=v) POST /_violetear/rpc/precise_convert { feet, inches }
UiState.{feet, inches} = ...

Verification

Test asserts SSR markup contains data-bind-value="UiState.meters" etc., the bundle compiles, and POST /_violetear/rpc/precise_convert returns the expected dict.

04 · Pomodoro timer (PWA + offline + storage)

Mockup
🍅 Pomodoro
work break long
24:13
▶ ⏸ ↻
sessions today: 3

What it stresses

  • @app.view with a custom Manifest object.
  • Service Worker asset caching (bundle.py + stylesheet).
  • violetear.storage.store written every tick — validates round-trip-on-mutation.
  • A long-running asyncio loop on the client (validates Pyodide concurrency).
  • @app.local mutation from a non-callback client function.

Flow — first load & SW install

GET / @app.view(pwa=Manifest) HTML + <link manifest> + SW.register
SW.install cache bundle.py, style.css, favicon

Flow — tick loop

asyncio loop: while running await asyncio.sleep(1) PomodoroState.seconds_left -= 1
ReactiveRegistry.notify DOM updates (data-bind-text)
store.pomodoro = snapshot() localStorage

Verification

Test asserts the manifest endpoint serves the expected JSON (name, theme_color, scope = /), the SW endpoint serves a script that lists the bundle URL in its assets, the bundle compiles.

05 · Chat room with presence (multi-user via WebSocket)

Mockup
Chat Room
— Alex joined —
Alex: hi
Sam: hey
— Iris joined —
Iris: 👋
say something…
Alex
Sam
Iris

What it stresses

  • Full WS lifecycle — connect, disconnect, messages both directions.
  • .broadcast(...) (message fan-out) AND .invoke(client_id, ...) (targeted reverse-RPC for history).
  • @app.client.on("connect") — the feature we wired in e32a24b.
  • @app.server.realtime AND @app.client.realtime in the same file.
  • Manual server-side shared state — what @app.shared will replace once it ships.

Flow — message broadcast

Client A clicks send post_message realtime ping @app.server.realtime post_message
messages.append(msg) receive_message.broadcast(msg)
fan-out to all connected clients
A · receive_message(msg) · B · receive_message(msg) · C · receive_message(msg)
each client appends to chat DOM

Flow — connect & history sync

Client B opens page WS onopen @app.client.on("connect")
request_history realtime ping @app.server.realtime request_history
receive_history.invoke(client_id, ...) B only · history rendered
@app.server.on("connect") · users[client_id] = name set_user_list.broadcast(users) all sidebars refresh

Verification

Test connects two TestClient.websocket_connect sessions concurrently; the second client receives the first's join broadcast; sending a message from client A causes client B to receive a receive_message envelope with the right shape.

File conventions

Test strategy

Add tests/test_examples_canonical.py with one thin smoke test per example. Goal: catch regressions when the framework changes — not to validate the examples' behavior in depth (that's what the example itself demonstrates by running).

Each test:

Total cost: roughly 5 small tests, ~150 lines combined.

Disposition of existing examples

Delete all 9 in a final commit after the last new example lands:

basic_pwa.py    broadcast.py    full_pwa.py
hello_world.py  quickstart.py   reactivity.py
rpc_call.py     server_realtime.py  simple_client.py

Rationale: a transitional _legacy/ directory adds clutter without value — the git history is the archive. The new set replaces the old completely. Bonus: deleting reactivity.py removes any lingering reference to the class_name= pattern that was the source of the bug surfaced in slice 1 and fixed in 540a354.

Build order

Build one example at a time, in numerical order. After each:

  1. The example file lands.
  2. The smoke test lands.
  3. make (= make test-unit) is green locally.
  4. Commit: feat(examples): canonical 0N_<slug> — <one-line description>.
  5. Push so CI runs on each commit.

After the last example lands, in a final commit:

Locked decisions

  1. Count. 5 examples — no separate @app.shared example until the feature ships.
  2. Shape. Independent showcases, not cumulative.
  3. Domains. Design tokens / guestbook / unit converter / pomodoro / chat (locked above).
  4. Disposition. Wholesale removal of the 9 legacy files; no _legacy/ directory.
  5. Spec home. issues/6-...md per repo convention (not the global docs/superpowers/specs/ default).
  6. Tier 5 initial-state push. Client realtime ping + server-side .invoke(client_id, ...) — exercises both directions in one flow.
  7. Tier 4 reload behavior. On reload, pause the timer (don't compute elapsed wall-time). Simpler + familiar UX.
  8. Test strategy. Thin smoke test per example, not in-depth behavioral tests.
Open considerations & deferred items
  • Tier-1 output location. Currently writes alongside the script (examples/01_static.html). Alternative: examples/_out/. Lean: alongside, for "open it and see" simplicity.
  • Deep pedagogical docs. Deferred per Alex 2026-05-20 — once the canonical set lands, we'll write a tour.
  • @app.shared example. When the feature ships, revise example 5 to use it (replacing the manual broadcast pattern) OR add a 6th example. Decision deferred until the feature lands.
  • Component subclass showcase. Not in the canonical set. The pattern exists in markup.py:Component but no example exercises it. Add later if magpie/superbot stresses it.
  • Static-site-generator mode. Tier 1 is one file; a "render many pages to disk" scenario could be a 1b variant later if needed.