Fix examples.sx: paren balance + dict eval crash at startup

1. Extra closing paren in ex-tabs handler
2. tab-content dict values contained (div ...) HTML tags which crash
   during register_components since HTML primitives aren't in env

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 01:30:21 +00:00
parent c23d0888ea
commit 17c58a2b5b
10 changed files with 110 additions and 1103 deletions

View File

@@ -561,3 +561,15 @@ def prim_into(target: Any, coll: Any) -> Any:
return result
raise ValueError(f"into: unsupported target type {type(target).__name__}")
@register_primitive("random-int")
def prim_random_int(low: int, high: int) -> int:
import random
return random.randint(int(low), int(high))
@register_primitive("json-encode")
def prim_json_encode(value) -> str:
import json
return json.dumps(value, indent=2)

View File

@@ -408,6 +408,18 @@ async def _io_request_form_all(
return dict(form)
@register_io_handler("request-form-list")
async def _io_request_form_list(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
) -> list:
"""``(request-form-list "field")`` → all values for a multi-value form field."""
if not args:
raise ValueError("request-form-list requires a field name")
from quart import request
form = await request.form
return form.getlist(str(args[0]))
@register_io_handler("request-headers-all")
async def _io_request_headers_all(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext

View File

@@ -202,6 +202,13 @@
:doc "All form fields as a dict."
:context :request)
(define-io-primitive "request-form-list"
:params (field-name)
:returns "list"
:async true
:doc "All values for a multi-value form field as a list."
:context :request)
(define-io-primitive "request-headers-all"
:params ()
:returns "dict"

View File

@@ -70,6 +70,17 @@
:doc "Modulo a % b."
:body (native-mod a b))
(define-primitive "random-int"
:params ((low :as number) (high :as number))
:returns "number"
:doc "Random integer in [low, high] inclusive."
:body (native-random-int low high))
(define-primitive "json-encode"
:params (value)
:returns "string"
:doc "Encode value as JSON string with indentation.")
(define-primitive "sqrt"
:params ((x :as number))
:returns "number"

View File

@@ -1,714 +1,21 @@
"""SX docs page routes.
Page GET routes are defined declaratively in sxc/pages/docs.sx via defpage.
This file contains only redirect routes and example API endpoints.
Example API endpoints are now defined in sx/handlers/examples.sx via defhandler.
This file contains only SSE and marsh demo endpoints that need Python.
"""
from __future__ import annotations
import asyncio
import json
import random
from datetime import datetime
from uuid import uuid4
from quart import Blueprint, Response, make_response, request
from shared.browser.app.csrf import csrf_exempt
from quart import Blueprint, Response, request
def register(url_prefix: str = "/") -> Blueprint:
bp = Blueprint("pages", __name__, url_prefix=url_prefix)
# ------------------------------------------------------------------
# Example API endpoints (for live demos)
# ------------------------------------------------------------------
@bp.get("/geography/hypermedia/examples/api/click")
async def api_click():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
sx_src = f'(~click-result :time "{now}")'
comp_text = _component_source_text("click-result")
wire_text = _full_wire_text(sx_src, "click-result")
oob_wire = _oob_code("click-wire", wire_text)
oob_comp = _oob_code("click-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
@csrf_exempt
@bp.post("/geography/hypermedia/examples/api/form")
async def api_form():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
form = await request.form
name = form.get("name", "")
escaped = name.replace('"', '\\"')
sx_src = f'(~form-result :name "{escaped}")'
comp_text = _component_source_text("form-result")
wire_text = _full_wire_text(sx_src, "form-result")
oob_wire = _oob_code("form-wire", wire_text)
oob_comp = _oob_code("form-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
_poll_count = {"n": 0}
@bp.get("/geography/hypermedia/examples/api/poll")
async def api_poll():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
_poll_count["n"] += 1
now = datetime.now().strftime("%H:%M:%S")
count = min(_poll_count["n"], 10)
sx_src = f'(~poll-result :time "{now}" :count {count})'
comp_text = _component_source_text("poll-result")
wire_text = _full_wire_text(sx_src, "poll-result")
oob_wire = _oob_code("poll-wire", wire_text)
oob_comp = _oob_code("poll-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
@csrf_exempt
@bp.delete("/geography/hypermedia/examples/api/delete/<item_id>")
async def api_delete(item_id: str):
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
# Empty primary response — outerHTML swap removes the row
# But send OOB swaps to show what happened
wire_text = _full_wire_text(f'(empty — row #{item_id} removed by outerHTML swap)')
comp_text = _component_source_text("delete-row")
oob_wire = _oob_code("delete-wire", wire_text)
oob_comp = _oob_code("delete-comp", comp_text)
return sx_response(f'(<> {oob_wire} {oob_comp})')
@bp.get("/geography/hypermedia/examples/api/edit")
async def api_edit_form():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
value = request.args.get("value", "")
escaped = value.replace('"', '\\"')
sx_src = f'(~inline-edit-form :value "{escaped}")'
comp_text = _component_source_text("inline-edit-form")
wire_text = _full_wire_text(sx_src, "inline-edit-form")
oob_wire = _oob_code("edit-wire", wire_text)
oob_comp = _oob_code("edit-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
@csrf_exempt
@bp.post("/geography/hypermedia/examples/api/edit")
async def api_edit_save():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
form = await request.form
value = form.get("value", "")
escaped = value.replace('"', '\\"')
sx_src = f'(~inline-view :value "{escaped}")'
comp_text = _component_source_text("inline-view")
wire_text = _full_wire_text(sx_src, "inline-view")
oob_wire = _oob_code("edit-wire", wire_text)
oob_comp = _oob_code("edit-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
@bp.get("/geography/hypermedia/examples/api/edit/cancel")
async def api_edit_cancel():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
value = request.args.get("value", "")
escaped = value.replace('"', '\\"')
sx_src = f'(~inline-view :value "{escaped}")'
comp_text = _component_source_text("inline-view")
wire_text = _full_wire_text(sx_src, "inline-view")
oob_wire = _oob_code("edit-wire", wire_text)
oob_comp = _oob_code("edit-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
@bp.get("/geography/hypermedia/examples/api/oob")
async def api_oob():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _full_wire_text
now = datetime.now().strftime("%H:%M:%S")
sx_src = (
f'(<>'
f' (p :class "text-emerald-600 font-medium" "Box A updated!")'
f' (p :class "text-sm text-stone-500" "at {now}")'
f' (div :id "oob-box-b" :sx-swap-oob "innerHTML"'
f' (p :class "text-violet-600 font-medium" "Box B updated via OOB!")'
f' (p :class "text-sm text-stone-500" "at {now}")))'
)
wire_text = _full_wire_text(sx_src)
oob_wire = _oob_code("oob-wire", wire_text)
return sx_response(f'(<> {sx_src} {oob_wire})')
# --- Lazy Loading ---
@bp.get("/geography/hypermedia/examples/api/lazy")
async def api_lazy():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
now = datetime.now().strftime("%H:%M:%S")
sx_src = f'(~lazy-result :time "{now}")'
comp_text = _component_source_text("lazy-result")
wire_text = _full_wire_text(sx_src, "lazy-result")
oob_wire = _oob_code("lazy-wire", wire_text)
oob_comp = _oob_code("lazy-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# --- Infinite Scroll ---
@bp.get("/geography/hypermedia/examples/api/scroll")
async def api_scroll():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _full_wire_text
page = int(request.args.get("page", 2))
start = (page - 1) * 5 + 1
next_page = page + 1
items_html = " ".join(
f'(div :class "px-4 py-3 border-b border-stone-100 text-sm text-stone-700" "Item {i} — loaded from page {page}")'
for i in range(start, start + 5)
)
if next_page <= 6:
sentinel = (
f'(div :id "scroll-sentinel"'
f' :sx-get "/geography/hypermedia/examples/api/scroll?page={next_page}"'
f' :sx-trigger "intersect once"'
f' :sx-target "#scroll-items"'
f' :sx-swap "beforeend"'
f' :class "p-3 text-center text-stone-400 text-sm"'
f' "Loading more...")'
)
else:
sentinel = (
'(div :class "p-3 text-center text-stone-500 text-sm font-medium"'
' "All items loaded.")'
)
sx_src = f'(<> {items_html} {sentinel})'
wire_text = _full_wire_text(sx_src)
oob_wire = _oob_code("scroll-wire", wire_text)
return sx_response(f'(<> {sx_src} {oob_wire})')
# --- Progress Bar ---
_jobs: dict[str, int] = {}
@csrf_exempt
@bp.post("/geography/hypermedia/examples/api/progress/start")
async def api_progress_start():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
job_id = str(uuid4())[:8]
_jobs[job_id] = 0
sx_src = f'(~progress-status :percent 0 :job-id "{job_id}")'
comp_text = _component_source_text("progress-status")
wire_text = _full_wire_text(sx_src, "progress-status")
oob_wire = _oob_code("progress-wire", wire_text)
oob_comp = _oob_code("progress-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
@bp.get("/geography/hypermedia/examples/api/progress/status")
async def api_progress_status():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
job_id = request.args.get("job", "")
current = _jobs.get(job_id, 0)
current = min(current + random.randint(15, 30), 100)
_jobs[job_id] = current
sx_src = f'(~progress-status :percent {current} :job-id "{job_id}")'
comp_text = _component_source_text("progress-status")
wire_text = _full_wire_text(sx_src, "progress-status")
oob_wire = _oob_code("progress-wire", wire_text)
oob_comp = _oob_code("progress-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# --- Active Search ---
@bp.get("/geography/hypermedia/examples/api/search")
async def api_search():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
from content.pages import SEARCH_LANGUAGES
q = request.args.get("q", "").strip().lower()
if not q:
results = SEARCH_LANGUAGES
else:
results = [lang for lang in SEARCH_LANGUAGES if q in lang.lower()]
items_sx = " ".join(f'"{r}"' for r in results)
escaped_q = q.replace('"', '\\"')
sx_src = f'(~search-results :items (list {items_sx}) :query "{escaped_q}")'
comp_text = _component_source_text("search-results")
wire_text = _full_wire_text(sx_src, "search-results")
oob_wire = _oob_code("search-wire", wire_text)
oob_comp = _oob_code("search-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# --- Inline Validation ---
_TAKEN_EMAILS = {"admin@example.com", "test@example.com", "user@example.com"}
@bp.get("/geography/hypermedia/examples/api/validate")
async def api_validate():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
email = request.args.get("email", "").strip()
if not email:
sx_src = '(~validation-error :message "Email is required")'
comp_name = "validation-error"
elif "@" not in email or "." not in email.split("@")[-1]:
sx_src = '(~validation-error :message "Invalid email format")'
comp_name = "validation-error"
elif email.lower() in _TAKEN_EMAILS:
escaped = email.replace('"', '\\"')
sx_src = f'(~validation-error :message "{escaped} is already taken")'
comp_name = "validation-error"
else:
escaped = email.replace('"', '\\"')
sx_src = f'(~validation-ok :email "{escaped}")'
comp_name = "validation-ok"
comp_text = _component_source_text(comp_name)
wire_text = _full_wire_text(sx_src, comp_name)
oob_wire = _oob_code("validate-wire", wire_text)
oob_comp = _oob_code("validate-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
@csrf_exempt
@bp.post("/geography/hypermedia/examples/api/validate/submit")
async def api_validate_submit():
from shared.sx.helpers import sx_response
form = await request.form
email = form.get("email", "").strip()
if not email or "@" not in email:
return sx_response('(p :class "text-sm text-rose-600 mt-2" "Please enter a valid email.")')
escaped = email.replace('"', '\\"')
return sx_response(f'(p :class "text-sm text-emerald-600 mt-2" "Form submitted with: {escaped}")')
# --- Value Select ---
@bp.get("/geography/hypermedia/examples/api/values")
async def api_values():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _full_wire_text
from content.pages import VALUE_SELECT_DATA
cat = request.args.get("category", "")
items = VALUE_SELECT_DATA.get(cat, [])
options_sx = " ".join(f'(option :value "{i}" "{i}")' for i in items)
if not options_sx:
options_sx = '(option :value "" "No items")'
sx_src = f'(<> {options_sx})'
wire_text = _full_wire_text(sx_src)
oob_wire = _oob_code("values-wire", wire_text)
return sx_response(f'(<> {options_sx} {oob_wire})')
# --- Reset on Submit ---
@csrf_exempt
@bp.post("/geography/hypermedia/examples/api/reset-submit")
async def api_reset_submit():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
form = await request.form
msg = form.get("message", "").strip() or "(empty)"
escaped = msg.replace('"', '\\"')
now = datetime.now().strftime("%H:%M:%S")
sx_src = f'(~reset-message :message "{escaped}" :time "{now}")'
comp_text = _component_source_text("reset-message")
wire_text = _full_wire_text(sx_src, "reset-message")
oob_wire = _oob_code("reset-wire", wire_text)
oob_comp = _oob_code("reset-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# --- Edit Row ---
_edit_rows: dict[str, dict] = {}
def _get_edit_rows() -> dict[str, dict]:
if not _edit_rows:
from content.pages import EDIT_ROW_DATA
for r in EDIT_ROW_DATA:
_edit_rows[r["id"]] = dict(r)
return _edit_rows
@bp.get("/geography/hypermedia/examples/api/editrow/<row_id>")
async def api_editrow_form(row_id: str):
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
rows = _get_edit_rows()
row = rows.get(row_id, {"id": row_id, "name": "", "price": "0", "stock": "0"})
sx_src = (f'(~edit-row-form :id "{row["id"]}" :name "{row["name"]}"'
f' :price "{row["price"]}" :stock "{row["stock"]}")')
comp_text = _component_source_text("edit-row-form")
wire_text = _full_wire_text(sx_src, "edit-row-form")
oob_wire = _oob_code("editrow-wire", wire_text)
oob_comp = _oob_code("editrow-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
@csrf_exempt
@bp.post("/geography/hypermedia/examples/api/editrow/<row_id>")
async def api_editrow_save(row_id: str):
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
form = await request.form
rows = _get_edit_rows()
rows[row_id] = {
"id": row_id,
"name": form.get("name", ""),
"price": form.get("price", "0"),
"stock": form.get("stock", "0"),
}
row = rows[row_id]
sx_src = (f'(~edit-row-view :id "{row["id"]}" :name "{row["name"]}"'
f' :price "{row["price"]}" :stock "{row["stock"]}")')
comp_text = _component_source_text("edit-row-view")
wire_text = _full_wire_text(sx_src, "edit-row-view")
oob_wire = _oob_code("editrow-wire", wire_text)
oob_comp = _oob_code("editrow-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
@bp.get("/geography/hypermedia/examples/api/editrow/<row_id>/cancel")
async def api_editrow_cancel(row_id: str):
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
rows = _get_edit_rows()
row = rows.get(row_id, {"id": row_id, "name": "", "price": "0", "stock": "0"})
sx_src = (f'(~edit-row-view :id "{row["id"]}" :name "{row["name"]}"'
f' :price "{row["price"]}" :stock "{row["stock"]}")')
comp_text = _component_source_text("edit-row-view")
wire_text = _full_wire_text(sx_src, "edit-row-view")
oob_wire = _oob_code("editrow-wire", wire_text)
oob_comp = _oob_code("editrow-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# --- Bulk Update ---
_bulk_users: dict[str, dict] = {}
def _get_bulk_users() -> dict[str, dict]:
if not _bulk_users:
from content.pages import BULK_USERS
for u in BULK_USERS:
_bulk_users[u["id"]] = dict(u)
return _bulk_users
@csrf_exempt
@bp.post("/geography/hypermedia/examples/api/bulk")
async def api_bulk():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
action = request.args.get("action", "activate")
form = await request.form
ids = form.getlist("ids")
users = _get_bulk_users()
new_status = "active" if action == "activate" else "inactive"
for uid in ids:
if uid in users:
users[uid]["status"] = new_status
rows = []
for u in users.values():
rows.append(
f'(~bulk-row :id "{u["id"]}" :name "{u["name"]}"'
f' :email "{u["email"]}" :status "{u["status"]}")'
)
sx_src = f'(<> {" ".join(rows)})'
comp_text = _component_source_text("bulk-row")
wire_text = _full_wire_text(sx_src, "bulk-row")
oob_wire = _oob_code("bulk-wire", wire_text)
oob_comp = _oob_code("bulk-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# --- Swap Positions ---
_swap_count = {"n": 0}
@csrf_exempt
@bp.post("/geography/hypermedia/examples/api/swap-log")
async def api_swap_log():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _full_wire_text
mode = request.args.get("mode", "beforeend")
_swap_count["n"] += 1
now = datetime.now().strftime("%H:%M:%S")
n = _swap_count["n"]
entry = f'(div :class "px-3 py-2 text-sm text-stone-700" "[{now}] {mode} (#{n})")'
oob_counter = (
f'(span :id "swap-counter" :sx-swap-oob "innerHTML"'
f' :class "self-center text-sm text-stone-500" "Count: {n}")'
)
sx_src = f'(<> {entry} {oob_counter})'
wire_text = _full_wire_text(sx_src)
oob_wire = _oob_code("swap-wire", wire_text)
return sx_response(f'(<> {sx_src} {oob_wire})')
# --- Select Filter (dashboard) ---
@bp.get("/geography/hypermedia/examples/api/dashboard")
async def api_dashboard():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _full_wire_text
now = datetime.now().strftime("%H:%M:%S")
sx_src = (
f'(<>'
f' (div :id "dash-header" :class "p-3 bg-violet-50 rounded mb-3"'
f' (h4 :class "font-semibold text-violet-800" "Dashboard Header")'
f' (p :class "text-sm text-violet-600" "Generated at {now}"))'
f' (div :id "dash-stats" :class "grid grid-cols-3 gap-3 mb-3"'
f' (div :class "p-3 bg-emerald-50 rounded text-center"'
f' (p :class "text-2xl font-bold text-emerald-700" "142")'
f' (p :class "text-xs text-emerald-600" "Users"))'
f' (div :class "p-3 bg-blue-50 rounded text-center"'
f' (p :class "text-2xl font-bold text-blue-700" "89")'
f' (p :class "text-xs text-blue-600" "Orders"))'
f' (div :class "p-3 bg-amber-50 rounded text-center"'
f' (p :class "text-2xl font-bold text-amber-700" "$4.2k")'
f' (p :class "text-xs text-amber-600" "Revenue")))'
f' (div :id "dash-footer" :class "p-3 bg-stone-50 rounded"'
f' (p :class "text-sm text-stone-500" "Last updated: {now}")))'
)
wire_text = _full_wire_text(sx_src)
oob_wire = _oob_code("filter-wire", wire_text)
return sx_response(f'(<> {sx_src} {oob_wire})')
# --- Tabs ---
_TAB_CONTENT = {
"tab1": ('(div (p :class "text-stone-700" "Welcome to the Overview tab.")'
' (p :class "text-stone-500 text-sm mt-2"'
' "This is the default tab content loaded via sx-get."))'),
"tab2": ('(div (p :class "text-stone-700" "Here are the details.")'
' (ul :class "mt-2 space-y-1 text-sm text-stone-600"'
' (li "Version: 1.0.0")'
' (li "Build: 2024-01-15")'
' (li "Engine: sx")))'),
"tab3": ('(div (p :class "text-stone-700" "Recent history:")'
' (ol :class "mt-2 space-y-1 text-sm text-stone-600 list-decimal list-inside"'
' (li "Initial release")'
' (li "Added component caching")'
' (li "Wire format v2")))'),
}
@bp.get("/geography/hypermedia/examples/api/tabs/<tab>")
async def api_tabs(tab: str):
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _full_wire_text
sx_src = _TAB_CONTENT.get(tab, _TAB_CONTENT["tab1"])
buttons = []
for t, label in [("tab1", "Overview"), ("tab2", "Details"), ("tab3", "History")]:
active = "true" if t == tab else "false"
buttons.append(f'(~tab-btn :tab "{t}" :label "{label}" :active "{active}")')
oob_tabs = (
f'(div :id "tab-buttons" :sx-swap-oob "innerHTML"'
f' :class "flex border-b border-stone-200"'
f' {" ".join(buttons)})'
)
wire_text = _full_wire_text(f'(<> {sx_src} {oob_tabs})', "tab-btn")
oob_wire = _oob_code("tabs-wire", wire_text)
return sx_response(f'(<> {sx_src} {oob_tabs} {oob_wire})')
# --- Animations ---
@bp.get("/geography/hypermedia/examples/api/animate")
async def api_animate():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
colors = ["bg-violet-100", "bg-emerald-100", "bg-blue-100", "bg-amber-100", "bg-rose-100"]
color = random.choice(colors)
now = datetime.now().strftime("%H:%M:%S")
sx_src = f'(~anim-result :color "{color}" :time "{now}")'
comp_text = _component_source_text("anim-result")
wire_text = _full_wire_text(sx_src, "anim-result")
oob_wire = _oob_code("anim-wire", wire_text)
oob_comp = _oob_code("anim-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# --- Dialogs ---
@bp.get("/geography/hypermedia/examples/api/dialog")
async def api_dialog():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
sx_src = '(~dialog-modal :title "Confirm Action" :message "Are you sure you want to proceed? This is a demo dialog rendered entirely with sx components.")'
comp_text = _component_source_text("dialog-modal")
wire_text = _full_wire_text(sx_src, "dialog-modal")
oob_wire = _oob_code("dialog-wire", wire_text)
oob_comp = _oob_code("dialog-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
@bp.get("/geography/hypermedia/examples/api/dialog/close")
async def api_dialog_close():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _full_wire_text
wire_text = _full_wire_text("(empty — dialog closed)")
oob_wire = _oob_code("dialog-wire", wire_text)
return sx_response(f'(<> {oob_wire})')
# --- Keyboard Shortcuts ---
_KBD_ACTIONS = {
"s": "Search panel activated",
"n": "New item created",
"h": "Help panel opened",
}
@bp.get("/geography/hypermedia/examples/api/keyboard")
async def api_keyboard():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
key = request.args.get("key", "")
action = _KBD_ACTIONS.get(key, f"Unknown key: {key}")
escaped_action = action.replace('"', '\\"')
escaped_key = key.replace('"', '\\"')
sx_src = f'(~kbd-result :key "{escaped_key}" :action "{escaped_action}")'
comp_text = _component_source_text("kbd-result")
wire_text = _full_wire_text(sx_src, "kbd-result")
oob_wire = _oob_code("kbd-wire", wire_text)
oob_comp = _oob_code("kbd-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# --- PUT / PATCH ---
_profile = {}
def _get_profile() -> dict:
if not _profile:
from content.pages import PROFILE_DEFAULT
_profile.update(PROFILE_DEFAULT)
return _profile
@bp.get("/geography/hypermedia/examples/api/putpatch/edit-all")
async def api_pp_edit_all():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
p = _get_profile()
sx_src = f'(~pp-form-full :name "{p["name"]}" :email "{p["email"]}" :role "{p["role"]}")'
comp_text = _component_source_text("pp-form-full")
wire_text = _full_wire_text(sx_src, "pp-form-full")
oob_wire = _oob_code("pp-wire", wire_text)
oob_comp = _oob_code("pp-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
@csrf_exempt
@bp.put("/geography/hypermedia/examples/api/putpatch")
async def api_pp_put():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
form = await request.form
p = _get_profile()
p["name"] = form.get("name", p["name"])
p["email"] = form.get("email", p["email"])
p["role"] = form.get("role", p["role"])
sx_src = f'(~pp-view :name "{p["name"]}" :email "{p["email"]}" :role "{p["role"]}")'
comp_text = _component_source_text("pp-view")
wire_text = _full_wire_text(sx_src, "pp-view")
oob_wire = _oob_code("pp-wire", wire_text)
oob_comp = _oob_code("pp-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
@bp.get("/geography/hypermedia/examples/api/putpatch/cancel")
async def api_pp_cancel():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
p = _get_profile()
sx_src = f'(~pp-view :name "{p["name"]}" :email "{p["email"]}" :role "{p["role"]}")'
comp_text = _component_source_text("pp-view")
wire_text = _full_wire_text(sx_src, "pp-view")
oob_wire = _oob_code("pp-wire", wire_text)
oob_comp = _oob_code("pp-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# --- JSON Encoding ---
@csrf_exempt
@bp.post("/geography/hypermedia/examples/api/json-echo")
async def api_json_echo():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
data = await request.get_json(silent=True) or {}
body = json.dumps(data, indent=2)
ct = request.content_type or "unknown"
escaped_body = body.replace('\\', '\\\\').replace('"', '\\"')
escaped_ct = ct.replace('"', '\\"')
sx_src = f'(~json-result :body "{escaped_body}" :content-type "{escaped_ct}")'
comp_text = _component_source_text("json-result")
wire_text = _full_wire_text(sx_src, "json-result")
oob_wire = _oob_code("json-wire", wire_text)
oob_comp = _oob_code("json-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# --- Vals & Headers ---
@bp.get("/geography/hypermedia/examples/api/echo-vals")
async def api_echo_vals():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
vals = {k: v for k, v in request.args.items()
if k not in ("_", "sx-request")}
items_sx = " ".join(f'"{k}: {v}"' for k, v in vals.items())
sx_src = f'(~echo-result :label "values" :items (list {items_sx}))'
comp_text = _component_source_text("echo-result")
wire_text = _full_wire_text(sx_src, "echo-result")
oob_wire = _oob_code("vals-wire", wire_text)
oob_comp = _oob_code("vals-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
@bp.get("/geography/hypermedia/examples/api/echo-headers")
async def api_echo_headers():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
custom = {k: v for k, v in request.headers if k.lower().startswith("x-")}
items_sx = " ".join(f'"{k}: {v}"' for k, v in custom.items())
sx_src = f'(~echo-result :label "headers" :items (list {items_sx}))'
comp_text = _component_source_text("echo-result")
wire_text = _full_wire_text(sx_src, "echo-result")
oob_wire = _oob_code("vals-wire", wire_text)
oob_comp = _oob_code("vals-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# --- Loading States ---
@bp.get("/geography/hypermedia/examples/api/slow")
async def api_slow():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
await asyncio.sleep(2)
now = datetime.now().strftime("%H:%M:%S")
sx_src = f'(~loading-result :time "{now}")'
comp_text = _component_source_text("loading-result")
wire_text = _full_wire_text(sx_src, "loading-result")
oob_wire = _oob_code("loading-wire", wire_text)
oob_comp = _oob_code("loading-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# --- Request Abort (sync replace) ---
@bp.get("/geography/hypermedia/examples/api/slow-search")
async def api_slow_search():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
delay = random.uniform(0.5, 2.0)
await asyncio.sleep(delay)
q = request.args.get("q", "").strip()
delay_ms = int(delay * 1000)
escaped = q.replace('"', '\\"')
sx_src = f'(~sync-result :query "{escaped}" :delay "{delay_ms}")'
comp_text = _component_source_text("sync-result")
wire_text = _full_wire_text(sx_src, "sync-result")
oob_wire = _oob_code("sync-wire", wire_text)
oob_comp = _oob_code("sync-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# --- Retry ---
_flaky = {"n": 0}
@bp.get("/geography/hypermedia/examples/api/flaky")
async def api_flaky():
from shared.sx.helpers import sx_response
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
_flaky["n"] += 1
n = _flaky["n"]
if n % 3 != 0:
return Response("", status=503, content_type="text/plain")
sx_src = f'(~retry-result :attempt "{n}" :message "Success! The endpoint finally responded.")'
comp_text = _component_source_text("retry-result")
wire_text = _full_wire_text(sx_src, "retry-result")
oob_wire = _oob_code("retry-wire", wire_text)
oob_comp = _oob_code("retry-comp", comp_text)
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
# ------------------------------------------------------------------
# Reference API endpoints — remaining Python-only
#

View File

@@ -114,3 +114,8 @@
:params (filename title desc)
:returns "dict"
:service "sx")
(define-page-helper "handler-source"
:params (name)
:returns "string"
:service "sx")

View File

@@ -9,7 +9,7 @@
:demo-description "Click the button to load server-rendered content."
:demo (~click-to-load-demo)
:sx-code "(button\n :sx-get \"/geography/hypermedia/examples/api/click\"\n :sx-target \"#click-result\"\n :sx-swap \"innerHTML\"\n \"Load content\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/click\")\nasync def api_click():\n now = datetime.now().strftime(...)\n return sx_response(\n f'(~click-result :time \"{now}\")')"
:handler-code (handler-source "ex-click")
:comp-placeholder-id "click-comp"
:wire-placeholder-id "click-wire"
:wire-note "The server responds with content-type text/sx. New CSS rules are prepended as a style tag. Clear the component cache to see component definitions included in the wire response."))
@@ -21,7 +21,7 @@
:demo-description "Enter a name and submit."
:demo (~form-demo)
:sx-code "(form\n :sx-post \"/geography/hypermedia/examples/api/form\"\n :sx-target \"#form-result\"\n :sx-swap \"innerHTML\"\n (input :type \"text\" :name \"name\")\n (button :type \"submit\" \"Submit\"))"
:handler-code "@bp.post(\"/geography/hypermedia/examples/api/form\")\nasync def api_form():\n form = await request.form\n name = form.get(\"name\", \"\")\n return sx_response(\n f'(~form-result :name \"{name}\")')"
:handler-code (handler-source "ex-form")
:comp-placeholder-id "form-comp"
:wire-placeholder-id "form-wire"))
@@ -32,7 +32,7 @@
:demo-description "This div polls the server every 2 seconds."
:demo (~polling-demo)
:sx-code "(div\n :sx-get \"/geography/hypermedia/examples/api/poll\"\n :sx-trigger \"load, every 2s\"\n :sx-swap \"innerHTML\"\n \"Loading...\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/poll\")\nasync def api_poll():\n poll_count[\"n\"] += 1\n now = datetime.now().strftime(\"%H:%M:%S\")\n count = min(poll_count[\"n\"], 10)\n return sx_response(\n f'(~poll-result :time \"{now}\" :count {count})')"
:handler-code (handler-source "ex-poll")
:comp-placeholder-id "poll-comp"
:wire-placeholder-id "poll-wire"
:wire-note "Updates every 2 seconds — watch the time and count change."))
@@ -49,7 +49,7 @@
(list "4" "Deploy to production")
(list "5" "Add unit tests")))
:sx-code "(button\n :sx-delete \"/api/delete/1\"\n :sx-target \"#row-1\"\n :sx-swap \"outerHTML\"\n :sx-confirm \"Delete this item?\"\n \"delete\")"
:handler-code "@bp.delete(\"/geography/hypermedia/examples/api/delete/<item_id>\")\nasync def api_delete(item_id: str):\n # Empty response — outerHTML swap removes the row\n return Response(\"\", status=200,\n content_type=\"text/sx\")"
:handler-code (handler-source "ex-delete")
:comp-placeholder-id "delete-comp"
:wire-placeholder-id "delete-wire"
:wire-note "Empty body — outerHTML swap replaces the target element with nothing."))
@@ -61,7 +61,7 @@
:demo-description "Click edit, modify the text, save or cancel."
:demo (~inline-edit-demo)
:sx-code ";; View mode — shows text + edit button\n(~inline-view :value \"some text\")\n\n;; Edit mode — returned by server on click\n(~inline-edit-form :value \"some text\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/edit\")\nasync def api_edit_form():\n value = request.args.get(\"value\", \"\")\n return sx_response(\n f'(~inline-edit-form :value \"{value}\")')\n\n@bp.post(\"/geography/hypermedia/examples/api/edit\")\nasync def api_edit_save():\n form = await request.form\n value = form.get(\"value\", \"\")\n return sx_response(\n f'(~inline-view :value \"{value}\")')"
:handler-code (str (handler-source "ex-edit-form") "\n\n" (handler-source "ex-edit-save"))
:comp-placeholder-id "edit-comp"
:comp-heading "Components"
:handler-heading "Server handlers"
@@ -74,7 +74,7 @@
:demo-description "One request updates both Box A (via sx-target) and Box B (via sx-swap-oob)."
:demo (~oob-demo)
:sx-code ";; Button targets Box A\n(button\n :sx-get \"/geography/hypermedia/examples/api/oob\"\n :sx-target \"#oob-box-a\"\n :sx-swap \"innerHTML\"\n \"Update both boxes\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/oob\")\nasync def api_oob():\n now = datetime.now().strftime(\"%H:%M:%S\")\n return sx_response(\n f'(<>'\n f' (p \"Box A updated at {now}\")'\n f' (div :id \"oob-box-b\"'\n f' :sx-swap-oob \"innerHTML\"'\n f' (p \"Box B updated at {now}\")))')"
:handler-code (handler-source "ex-oob")
:wire-placeholder-id "oob-wire"
:wire-note "The fragment contains both the main content and an OOB element. sx.js splits them: main content goes to sx-target, OOB elements find their targets by ID."))
@@ -85,7 +85,7 @@
:demo-description "Content loads automatically when the page renders."
:demo (~lazy-loading-demo)
:sx-code "(div\n :sx-get \"/geography/hypermedia/examples/api/lazy\"\n :sx-trigger \"load\"\n :sx-swap \"innerHTML\"\n (div :class \"animate-pulse\" \"Loading...\"))"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/lazy\")\nasync def api_lazy():\n now = datetime.now().strftime(...)\n return sx_response(\n f'(~lazy-result :time \"{now}\")')"
:handler-code (handler-source "ex-lazy")
:comp-placeholder-id "lazy-comp"
:wire-placeholder-id "lazy-wire"))
@@ -96,7 +96,7 @@
:demo-description "Scroll down in the container to load more items (5 pages total)."
:demo (~infinite-scroll-demo)
:sx-code "(div :id \"scroll-sentinel\"\n :sx-get \"/geography/hypermedia/examples/api/scroll?page=2\"\n :sx-trigger \"intersect once\"\n :sx-target \"#scroll-items\"\n :sx-swap \"beforeend\"\n \"Loading more...\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/scroll\")\nasync def api_scroll():\n page = int(request.args.get(\"page\", 2))\n items = [f\"Item {i}\" for i in range(...)]\n # Include next sentinel if more pages\n return sx_response(items_sx + sentinel_sx)"
:handler-code (handler-source "ex-scroll")
:comp-placeholder-id "scroll-comp"
:wire-placeholder-id "scroll-wire"))
@@ -107,7 +107,7 @@
:demo-description "Click start to begin a simulated job."
:demo (~progress-bar-demo)
:sx-code ";; Start the job\n(button\n :sx-post \"/geography/hypermedia/examples/api/progress/start\"\n :sx-target \"#progress-target\"\n :sx-swap \"innerHTML\")\n\n;; Each response re-polls via sx-trigger=\"load\"\n(div :sx-get \"/api/progress/status?job=ID\"\n :sx-trigger \"load delay:500ms\"\n :sx-target \"#progress-target\"\n :sx-swap \"innerHTML\")"
:handler-code "@bp.post(\"/geography/hypermedia/examples/api/progress/start\")\nasync def api_progress_start():\n job_id = str(uuid4())[:8]\n _jobs[job_id] = 0\n return sx_response(\n f'(~progress-status :percent 0 :job-id \"{job_id}\")')"
:handler-code (str (handler-source "ex-progress-start") "\n\n" (handler-source "ex-progress-status"))
:comp-placeholder-id "progress-comp"
:wire-placeholder-id "progress-wire"))
@@ -118,7 +118,7 @@
:demo-description "Type to search through 20 programming languages."
:demo (~active-search-demo)
:sx-code "(input :type \"text\" :name \"q\"\n :sx-get \"/geography/hypermedia/examples/api/search\"\n :sx-trigger \"keyup delay:300ms changed\"\n :sx-target \"#search-results\"\n :sx-swap \"innerHTML\"\n :placeholder \"Search...\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/search\")\nasync def api_search():\n q = request.args.get(\"q\", \"\").lower()\n results = [l for l in LANGUAGES if q in l.lower()]\n return sx_response(\n f'(~search-results :items (...) :query \"{q}\")')"
:handler-code (handler-source "ex-search")
:comp-placeholder-id "search-comp"
:wire-placeholder-id "search-wire"))
@@ -129,7 +129,7 @@
:demo-description "Enter an email and click away (blur) to validate."
:demo (~inline-validation-demo)
:sx-code "(input :type \"text\" :name \"email\"\n :sx-get \"/geography/hypermedia/examples/api/validate\"\n :sx-trigger \"blur\"\n :sx-target \"#email-feedback\"\n :sx-swap \"innerHTML\"\n :placeholder \"user@example.com\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/validate\")\nasync def api_validate():\n email = request.args.get(\"email\", \"\")\n if \"@\" not in email:\n return sx_response('(~validation-error ...)')\n return sx_response('(~validation-ok ...)')"
:handler-code (handler-source "ex-validate")
:comp-placeholder-id "validate-comp"
:wire-placeholder-id "validate-wire"))
@@ -140,7 +140,7 @@
:demo-description "Select a category to populate the item dropdown."
:demo (~value-select-demo)
:sx-code "(select :name \"category\"\n :sx-get \"/geography/hypermedia/examples/api/values\"\n :sx-trigger \"change\"\n :sx-target \"#value-items\"\n :sx-swap \"innerHTML\"\n (option \"Languages\")\n (option \"Frameworks\")\n (option \"Databases\"))"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/values\")\nasync def api_values():\n cat = request.args.get(\"category\", \"\")\n items = VALUE_SELECT_DATA.get(cat, [])\n return sx_response(\n f'(~value-options :items (list ...))')"
:handler-code (handler-source "ex-values")
:comp-placeholder-id "values-comp"
:wire-placeholder-id "values-wire"))
@@ -151,7 +151,7 @@
:demo-description "Submit a message — the input resets after each send."
:demo (~reset-on-submit-demo)
:sx-code "(form :id \"reset-form\"\n :sx-post \"/geography/hypermedia/examples/api/reset-submit\"\n :sx-target \"#reset-result\"\n :sx-swap \"innerHTML\"\n :sx-on:afterSwap \"this.reset()\"\n (input :type \"text\" :name \"message\")\n (button :type \"submit\" \"Send\"))"
:handler-code "@bp.post(\"/geography/hypermedia/examples/api/reset-submit\")\nasync def api_reset_submit():\n form = await request.form\n msg = form.get(\"message\", \"\")\n return sx_response(\n f'(~reset-message :message \"{msg}\" :time \"...\")')"
:handler-code (handler-source "ex-reset-submit")
:comp-placeholder-id "reset-comp"
:wire-placeholder-id "reset-wire"))
@@ -166,7 +166,7 @@
(list "3" "Widget C" "12.00" "305")
(list "4" "Widget D" "45.00" "67")))
:sx-code "(button\n :sx-get \"/geography/hypermedia/examples/api/editrow/1\"\n :sx-target \"#erow-1\"\n :sx-swap \"outerHTML\"\n \"edit\")\n\n;; Save sends form data via POST\n(button\n :sx-post \"/geography/hypermedia/examples/api/editrow/1\"\n :sx-target \"#erow-1\"\n :sx-swap \"outerHTML\"\n :sx-include \"#erow-1\"\n \"save\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/editrow/<id>\")\nasync def api_editrow_form(id):\n row = EDIT_ROW_DATA[id]\n return sx_response(\n f'(~edit-row-form :id ... :name ...)')\n\n@bp.post(\"/geography/hypermedia/examples/api/editrow/<id>\")\nasync def api_editrow_save(id):\n form = await request.form\n return sx_response(\n f'(~edit-row-view :id ... :name ...)')"
:handler-code (str (handler-source "ex-editrow-form") "\n\n" (handler-source "ex-editrow-save"))
:comp-placeholder-id "editrow-comp"
:wire-placeholder-id "editrow-wire"))
@@ -182,7 +182,7 @@
(list "4" "Dan Okafor" "dan@example.com" "inactive")
(list "5" "Eve Larsson" "eve@example.com" "active")))
:sx-code "(button\n :sx-post \"/geography/hypermedia/examples/api/bulk?action=activate\"\n :sx-target \"#bulk-table\"\n :sx-swap \"innerHTML\"\n :sx-include \"#bulk-form\"\n \"Activate\")"
:handler-code "@bp.post(\"/geography/hypermedia/examples/api/bulk\")\nasync def api_bulk():\n action = request.args.get(\"action\")\n form = await request.form\n ids = form.getlist(\"ids\")\n # Update matching users\n return sx_response(updated_rows)"
:handler-code (handler-source "ex-bulk")
:comp-placeholder-id "bulk-comp"
:wire-placeholder-id "bulk-wire"))
@@ -193,7 +193,7 @@
:demo-description "Try each button to see different swap behaviours."
:demo (~swap-positions-demo)
:sx-code ";; Append to end\n(button :sx-post \"/api/swap-log?mode=beforeend\"\n :sx-target \"#swap-log\" :sx-swap \"beforeend\"\n \"Add to End\")\n\n;; Prepend to start\n(button :sx-post \"/api/swap-log?mode=afterbegin\"\n :sx-target \"#swap-log\" :sx-swap \"afterbegin\"\n \"Add to Start\")\n\n;; No swap — OOB counter update only\n(button :sx-post \"/api/swap-log?mode=none\"\n :sx-target \"#swap-log\" :sx-swap \"none\"\n \"Silent Ping\")"
:handler-code "@bp.post(\"/geography/hypermedia/examples/api/swap-log\")\nasync def api_swap_log():\n mode = request.args.get(\"mode\")\n # OOB counter updates on every request\n oob = f'(span :id \"swap-counter\" :sx-swap-oob \"innerHTML\" \"Count: {n}\")'\n return sx_response(entry + oob)"
:handler-code (handler-source "ex-swap-log")
:wire-placeholder-id "swap-wire"))
(defcomp ~example-select-filter ()
@@ -203,7 +203,7 @@
:demo-description "Different buttons select different parts of the same server response."
:demo (~select-filter-demo)
:sx-code ";; Pick just the stats section from the response\n(button\n :sx-get \"/geography/hypermedia/examples/api/dashboard\"\n :sx-target \"#filter-target\"\n :sx-swap \"innerHTML\"\n :sx-select \"#dash-stats\"\n \"Stats Only\")\n\n;; No sx-select — get the full response\n(button\n :sx-get \"/geography/hypermedia/examples/api/dashboard\"\n :sx-target \"#filter-target\"\n :sx-swap \"innerHTML\"\n \"Full Dashboard\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/dashboard\")\nasync def api_dashboard():\n # Returns header + stats + footer\n # Client uses sx-select to pick sections\n return sx_response(\n '(<> (div :id \"dash-header\" ...) '\n ' (div :id \"dash-stats\" ...) '\n ' (div :id \"dash-footer\" ...))')"
:handler-code (handler-source "ex-dashboard")
:wire-placeholder-id "filter-wire"))
(defcomp ~example-tabs ()
@@ -213,7 +213,7 @@
:demo-description "Click tabs to switch content. Watch the browser URL change."
:demo (~tabs-demo)
:sx-code "(button\n :sx-get \"/geography/hypermedia/examples/api/tabs/tab1\"\n :sx-target \"#tab-content\"\n :sx-swap \"innerHTML\"\n :sx-push-url \"/geography/hypermedia/examples/tabs?tab=tab1\"\n \"Overview\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/tabs/<tab>\")\nasync def api_tabs(tab: str):\n content = TAB_CONTENT[tab]\n return sx_response(content)"
:handler-code (handler-source "ex-tabs")
:wire-placeholder-id "tabs-wire"))
(defcomp ~example-animations ()
@@ -223,7 +223,7 @@
:demo-description "Click to swap in content with a fade-in animation."
:demo (~animations-demo)
:sx-code "(button\n :sx-get \"/geography/hypermedia/examples/api/animate\"\n :sx-target \"#anim-target\"\n :sx-swap \"innerHTML\"\n \"Load with animation\")\n\n;; Component uses CSS animation class\n(defcomp ~anim-result (&key color time)\n (div :class \"sx-fade-in ...\"\n (style \".sx-fade-in { animation: sxFadeIn 0.5s }\")\n (p \"Faded in!\")))"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/animate\")\nasync def api_animate():\n colors = [\"bg-violet-100\", \"bg-emerald-100\", ...]\n color = random.choice(colors)\n return sx_response(\n f'(~anim-result :color \"{color}\" :time \"{now}\")')"
:handler-code (handler-source "ex-animate")
:comp-placeholder-id "anim-comp"
:wire-placeholder-id "anim-wire"))
@@ -234,7 +234,7 @@
:demo-description "Click to open a modal dialog."
:demo (~dialogs-demo)
:sx-code "(button\n :sx-get \"/geography/hypermedia/examples/api/dialog\"\n :sx-target \"#dialog-container\"\n :sx-swap \"innerHTML\"\n \"Open Dialog\")\n\n;; Dialog closes by swapping empty content\n(button\n :sx-get \"/geography/hypermedia/examples/api/dialog/close\"\n :sx-target \"#dialog-container\"\n :sx-swap \"innerHTML\"\n \"Close\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/dialog\")\nasync def api_dialog():\n return sx_response(\n '(~dialog-modal :title \"Confirm\"'\n ' :message \"Are you sure?\")')\n\n@bp.get(\"/geography/hypermedia/examples/api/dialog/close\")\nasync def api_dialog_close():\n return sx_response(\"\")"
:handler-code (str (handler-source "ex-dialog") "\n\n" (handler-source "ex-dialog-close"))
:comp-placeholder-id "dialog-comp"
:wire-placeholder-id "dialog-wire"))
@@ -245,7 +245,7 @@
:demo-description "Press s, n, or h on your keyboard."
:demo (~keyboard-shortcuts-demo)
:sx-code "(div :id \"kbd-target\"\n :sx-get \"/geography/hypermedia/examples/api/keyboard?key=s\"\n :sx-trigger \"keyup[key=='s'&&!event.target.matches('input,textarea')] from:body\"\n :sx-swap \"innerHTML\"\n \"Press a shortcut key...\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/keyboard\")\nasync def api_keyboard():\n key = request.args.get(\"key\", \"\")\n actions = {\"s\": \"Search\", \"n\": \"New item\", \"h\": \"Help\"}\n return sx_response(\n f'(~kbd-result :key \"{key}\" :action \"{actions[key]}\")')"
:handler-code (handler-source "ex-keyboard")
:comp-placeholder-id "kbd-comp"
:wire-placeholder-id "kbd-wire"))
@@ -256,7 +256,7 @@
:demo-description "Click Edit All to replace the full profile via PUT."
:demo (~put-patch-demo :name "Ada Lovelace" :email "ada@example.com" :role "Engineer")
:sx-code ";; Replace entire resource\n(form :sx-put \"/geography/hypermedia/examples/api/putpatch\"\n :sx-target \"#pp-target\" :sx-swap \"innerHTML\"\n (input :name \"name\") (input :name \"email\")\n (button \"Save All (PUT)\"))"
:handler-code "@bp.put(\"/geography/hypermedia/examples/api/putpatch\")\nasync def api_put():\n form = await request.form\n # Full replacement\n return sx_response('(~pp-view ...)')"
:handler-code (str (handler-source "ex-pp-edit-all") "\n\n" (handler-source "ex-pp-put"))
:comp-placeholder-id "pp-comp"
:wire-placeholder-id "pp-wire"))
@@ -267,7 +267,7 @@
:demo-description "Submit the form and see the JSON body the server received."
:demo (~json-encoding-demo)
:sx-code "(form\n :sx-post \"/geography/hypermedia/examples/api/json-echo\"\n :sx-target \"#json-result\"\n :sx-swap \"innerHTML\"\n :sx-encoding \"json\"\n (input :name \"name\" :value \"Ada\")\n (input :type \"number\" :name \"age\" :value \"36\")\n (button \"Submit as JSON\"))"
:handler-code "@bp.post(\"/geography/hypermedia/examples/api/json-echo\")\nasync def api_json_echo():\n data = await request.get_json()\n body = json.dumps(data, indent=2)\n ct = request.content_type\n return sx_response(\n f'(~json-result :body \"{body}\" :content-type \"{ct}\")')"
:handler-code (handler-source "ex-json-echo")
:comp-placeholder-id "json-comp"
:wire-placeholder-id "json-wire"))
@@ -278,7 +278,7 @@
:demo-description "Click each button to see what the server receives."
:demo (~vals-headers-demo)
:sx-code ";; Send extra values with the request\n(button\n :sx-get \"/geography/hypermedia/examples/api/echo-vals\"\n :sx-vals \"{\\\"source\\\": \\\"button\\\"}\"\n \"Send with vals\")\n\n;; Send custom headers\n(button\n :sx-get \"/geography/hypermedia/examples/api/echo-headers\"\n :sx-headers {:X-Custom-Token \"abc123\"}\n \"Send with headers\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/echo-vals\")\nasync def api_echo_vals():\n vals = dict(request.args)\n return sx_response(\n f'(~echo-result :label \"values\" :items (...))')\n\n@bp.get(\"/geography/hypermedia/examples/api/echo-headers\")\nasync def api_echo_headers():\n custom = {k: v for k, v in request.headers\n if k.startswith(\"X-\")}\n return sx_response(\n f'(~echo-result :label \"headers\" :items (...))')"
:handler-code (str (handler-source "ex-echo-vals") "\n\n" (handler-source "ex-echo-headers"))
:comp-placeholder-id "vals-comp"
:wire-placeholder-id "vals-wire"))
@@ -289,7 +289,7 @@
:demo-description "Click the button — it shows a spinner during the 2-second request."
:demo (~loading-states-demo)
:sx-code ";; .sx-request class added during request\n(style \".sx-loading-btn.sx-request {\n opacity: 0.7; pointer-events: none; }\n.sx-loading-btn.sx-request .sx-spinner {\n display: inline-block; }\n.sx-loading-btn .sx-spinner {\n display: none; }\")\n\n(button :class \"sx-loading-btn\"\n :sx-get \"/geography/hypermedia/examples/api/slow\"\n :sx-target \"#loading-result\"\n (span :class \"sx-spinner animate-spin\" \"...\")\n \"Load slow endpoint\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/slow\")\nasync def api_slow():\n await asyncio.sleep(2)\n return sx_response(\n f'(~loading-result :time \"{now}\")')"
:handler-code (handler-source "ex-slow")
:comp-placeholder-id "loading-comp"
:wire-placeholder-id "loading-wire"))
@@ -300,7 +300,7 @@
:demo-description "Type quickly — only the latest result appears despite random 0.5-2s server delays."
:demo (~sync-replace-demo)
:sx-code "(input :type \"text\" :name \"q\"\n :sx-get \"/geography/hypermedia/examples/api/slow-search\"\n :sx-trigger \"keyup delay:200ms changed\"\n :sx-target \"#sync-result\"\n :sx-swap \"innerHTML\"\n :sx-sync \"replace\"\n \"Type to search...\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/slow-search\")\nasync def api_slow_search():\n delay = random.uniform(0.5, 2.0)\n await asyncio.sleep(delay)\n q = request.args.get(\"q\", \"\")\n return sx_response(\n f'(~sync-result :query \"{q}\" :delay \"{delay_ms}\")')"
:handler-code (handler-source "ex-slow-search")
:comp-placeholder-id "sync-comp"
:wire-placeholder-id "sync-wire"))
@@ -311,6 +311,6 @@
:demo-description "Click the button — watch it retry automatically after failures."
:demo (~retry-demo)
:sx-code "(button\n :sx-get \"/geography/hypermedia/examples/api/flaky\"\n :sx-target \"#retry-result\"\n :sx-swap \"innerHTML\"\n :sx-retry \"exponential:1000:8000\"\n \"Call flaky endpoint\")"
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/flaky\")\nasync def api_flaky():\n _flaky[\"n\"] += 1\n if _flaky[\"n\"] % 3 != 0:\n return Response(\"\", status=503)\n return sx_response(\n f'(~retry-result :attempt {n} ...)')"
:handler-code (handler-source "ex-flaky")
:comp-placeholder-id "retry-comp"
:wire-placeholder-id "retry-wire"))

View File

@@ -24,19 +24,9 @@
(list "admin@example.com" "test@example.com" "user@example.com"))
(define tab-content
{"tab1" (div (p :class "text-stone-700" "Welcome to the Overview tab.")
(p :class "text-stone-500 text-sm mt-2"
"This is the default tab content loaded via sx-get."))
"tab2" (div (p :class "text-stone-700" "Here are the details.")
(ul :class "mt-2 space-y-1 text-sm text-stone-600"
(li "Version: 1.0.0")
(li "Build: 2024-01-15")
(li "Engine: sx")))
"tab3" (div (p :class "text-stone-700" "Recent history:")
(ol :class "mt-2 space-y-1 text-sm text-stone-600 list-decimal list-inside"
(li "Initial release")
(li "Added component caching")
(li "Wire format v2")))})
{"tab1" "Welcome to the Overview tab. This is the default tab content loaded via sx-get."
"tab2" "Here are the details. Version: 1.0.0, Build: 2024-01-15, Engine: sx"
"tab3" "Recent history: Initial release, Added component caching, Wire format v2"})
(define kbd-actions
{"s" "Search panel activated"

View File

@@ -1,367 +0,0 @@
;; SX example API handlers — defhandler definitions
;;
;; These serve the live demos on the Examples docs pages.
;; Each handler's source is displayed in the "Server handler" code block
;; on its corresponding example page (self-referencing via handler-source).
;; ---------------------------------------------------------------------------
;; Click to Load
;; ---------------------------------------------------------------------------
(defhandler click (&key)
(let ((now (format-time (now) "%H:%M:%S")))
(~click-result :time now)))
;; ---------------------------------------------------------------------------
;; Form Submission
;; ---------------------------------------------------------------------------
(defhandler form (&key)
(let ((name (form-data "name")))
(~form-result :name name)))
;; ---------------------------------------------------------------------------
;; Polling
;; ---------------------------------------------------------------------------
(defhandler poll (&key)
(let ((now (format-time (now) "%H:%M:%S"))
(count (inc-counter "poll" :max 10)))
(~poll-result :time now :count count)))
;; ---------------------------------------------------------------------------
;; Delete Row
;; ---------------------------------------------------------------------------
(defhandler delete (&key item-id)
;; Empty response — outerHTML swap removes the row
"")
;; ---------------------------------------------------------------------------
;; Inline Edit
;; ---------------------------------------------------------------------------
(defhandler edit-form (&key)
(let ((value (request-arg "value")))
(~inline-edit-form :value value)))
(defhandler edit-save (&key)
(let ((value (form-data "value")))
(~inline-view :value value)))
(defhandler edit-cancel (&key)
(let ((value (request-arg "value")))
(~inline-view :value value)))
;; ---------------------------------------------------------------------------
;; Out-of-Band Swaps
;; ---------------------------------------------------------------------------
(defhandler oob (&key)
(let ((now (format-time (now) "%H:%M:%S")))
(<>
(p :class "text-emerald-600 font-medium" "Box A updated!")
(p :class "text-sm text-stone-500" "at " now)
(div :id "oob-box-b" :sx-swap-oob "innerHTML"
(p :class "text-violet-600 font-medium" "Box B updated via OOB!")
(p :class "text-sm text-stone-500" "at " now)))))
;; ---------------------------------------------------------------------------
;; Lazy Loading
;; ---------------------------------------------------------------------------
(defhandler lazy (&key)
(let ((now (format-time (now) "%H:%M:%S")))
(~lazy-result :time now)))
;; ---------------------------------------------------------------------------
;; Infinite Scroll
;; ---------------------------------------------------------------------------
(defhandler scroll (&key)
(let ((page (or (parse-int (request-arg "page")) 2))
(start (+ (* (- page 1) 5) 1))
(next (+ page 1)))
(<>
(map (fn (i)
(div :class "px-4 py-3 border-b border-stone-100 text-sm text-stone-700"
"Item " i " — loaded from page " page))
(range start (+ start 5)))
(if (<= next 6)
(div :id "scroll-sentinel"
:sx-get (str "/geography/hypermedia/examples/api/scroll?page=" next)
:sx-trigger "intersect once"
:sx-target "#scroll-items"
:sx-swap "beforeend"
:class "p-3 text-center text-stone-400 text-sm"
"Loading more...")
(div :class "p-3 text-center text-stone-500 text-sm font-medium"
"All items loaded.")))))
;; ---------------------------------------------------------------------------
;; Progress Bar
;; ---------------------------------------------------------------------------
(defhandler progress-start (&key)
(let ((job-id (new-job)))
(~progress-status :percent 0 :job-id job-id)))
(defhandler progress-status (&key)
(let ((job-id (request-arg "job"))
(percent (advance-job job-id)))
(~progress-status :percent percent :job-id job-id)))
;; ---------------------------------------------------------------------------
;; Active Search
;; ---------------------------------------------------------------------------
(defhandler search (&key)
(let ((q (request-arg "q"))
(results (filter-list LANGUAGES q)))
(~search-results :items results :query q)))
;; ---------------------------------------------------------------------------
;; Inline Validation
;; ---------------------------------------------------------------------------
(defhandler validate (&key)
(let ((email (request-arg "email")))
(cond
((not email)
(~validation-error :message "Email is required"))
((not (contains? email "@"))
(~validation-error :message "Invalid email format"))
((contains? TAKEN_EMAILS (lower email))
(~validation-error
:message (str email " is already taken")))
(t (~validation-ok :email email)))))
;; ---------------------------------------------------------------------------
;; Value Select
;; ---------------------------------------------------------------------------
(defhandler values (&key)
(let ((cat (request-arg "category"))
(items (get VALUE_SELECT_DATA cat)))
(if (empty? items)
(option :value "" "No items")
(map (fn (i) (option :value i i)) items))))
;; ---------------------------------------------------------------------------
;; Reset on Submit
;; ---------------------------------------------------------------------------
(defhandler reset-submit (&key)
(let ((msg (or (form-data "message") "(empty)"))
(now (format-time (now) "%H:%M:%S")))
(~reset-message :message msg :time now)))
;; ---------------------------------------------------------------------------
;; Edit Row
;; ---------------------------------------------------------------------------
(defhandler editrow-form (&key row-id)
(let ((row (get ROWS row-id)))
(~edit-row-form :id row-id
:name (get row "name")
:price (get row "price")
:stock (get row "stock"))))
(defhandler editrow-save (&key row-id)
(let ((name (form-data "name"))
(price (form-data "price"))
(stock (form-data "stock")))
(~edit-row-view :id row-id
:name name :price price :stock stock)))
(defhandler editrow-cancel (&key row-id)
(let ((row (get ROWS row-id)))
(~edit-row-view :id row-id
:name (get row "name")
:price (get row "price")
:stock (get row "stock"))))
;; ---------------------------------------------------------------------------
;; Bulk Update
;; ---------------------------------------------------------------------------
(defhandler bulk (&key)
(let ((action (request-arg "action"))
(ids (form-list "ids"))
(status (if (= action "activate")
"active" "inactive")))
(update-users ids :status status)
(map (fn (u)
(~bulk-row
:id (get u "id")
:name (get u "name")
:email (get u "email")
:status (get u "status")))
USERS)))
;; ---------------------------------------------------------------------------
;; Swap Positions
;; ---------------------------------------------------------------------------
(defhandler swap-log (&key)
(let ((mode (request-arg "mode"))
(n (inc-counter "swap"))
(now (format-time (now) "%H:%M:%S")))
(<>
(div :class "px-3 py-2 text-sm text-stone-700"
"[" now "] " mode " (#" n ")")
(span :id "swap-counter"
:sx-swap-oob "innerHTML"
:class "self-center text-sm text-stone-500"
"Count: " n))))
;; ---------------------------------------------------------------------------
;; Select Filter (Dashboard)
;; ---------------------------------------------------------------------------
(defhandler dashboard (&key)
(let ((now (format-time (now) "%H:%M:%S")))
(<>
(div :id "dash-header" :class "p-3 bg-violet-50 rounded mb-3"
(h4 :class "font-semibold text-violet-800" "Dashboard Header")
(p :class "text-sm text-violet-600" "Generated at " now))
(div :id "dash-stats" :class "grid grid-cols-3 gap-3 mb-3"
(div :class "p-3 bg-emerald-50 rounded text-center"
(p :class "text-2xl font-bold text-emerald-700" "142")
(p :class "text-xs text-emerald-600" "Users"))
(div :class "p-3 bg-blue-50 rounded text-center"
(p :class "text-2xl font-bold text-blue-700" "89")
(p :class "text-xs text-blue-600" "Orders"))
(div :class "p-3 bg-amber-50 rounded text-center"
(p :class "text-2xl font-bold text-amber-700" "$4.2k")
(p :class "text-xs text-amber-600" "Revenue")))
(div :id "dash-footer" :class "p-3 bg-stone-100 rounded"
(p :class "text-sm text-stone-500" "Last updated: " now)))))
;; ---------------------------------------------------------------------------
;; Tabs
;; ---------------------------------------------------------------------------
(defhandler tabs (&key tab)
(let ((content (get TAB_CONTENT tab)))
(<> content
(div :id "tab-buttons"
:sx-swap-oob "innerHTML"
:class "flex border-b border-stone-200"
(map (fn (t)
(~tab-btn
:tab (first t)
:label (last t)
:active (if (= (first t) tab) "true" "false")))
TAB_LIST)))))
;; ---------------------------------------------------------------------------
;; Animations
;; ---------------------------------------------------------------------------
(defhandler animate (&key)
(let ((color (random-choice
"bg-violet-100" "bg-emerald-100"
"bg-blue-100" "bg-amber-100"))
(now (format-time (now) "%H:%M:%S")))
(~anim-result :color color :time now)))
;; ---------------------------------------------------------------------------
;; Dialogs
;; ---------------------------------------------------------------------------
(defhandler dialog (&key)
(~dialog-modal
:title "Confirm Action"
:message "Are you sure you want to proceed?"))
(defhandler dialog-close (&key)
"")
;; ---------------------------------------------------------------------------
;; Keyboard Shortcuts
;; ---------------------------------------------------------------------------
(defhandler keyboard (&key)
(let ((key (request-arg "key"))
(actions {:s "Search panel activated"
:n "New item created"
:h "Help panel opened"})
(action (get actions key)))
(~kbd-result :key key :action action)))
;; ---------------------------------------------------------------------------
;; PUT / PATCH
;; ---------------------------------------------------------------------------
(defhandler pp-edit-all (&key)
(let ((p (get-profile)))
(~pp-form-full
:name (get p "name")
:email (get p "email")
:role (get p "role"))))
(defhandler put-profile (&key)
(let ((name (form-data "name"))
(email (form-data "email"))
(role (form-data "role")))
(~pp-view :name name :email email :role role)))
(defhandler pp-cancel (&key)
(let ((p (get-profile)))
(~pp-view
:name (get p "name")
:email (get p "email")
:role (get p "role"))))
;; ---------------------------------------------------------------------------
;; JSON Encoding
;; ---------------------------------------------------------------------------
(defhandler json-echo (&key)
(let ((data (request-json))
(body (json-pretty data))
(ct (request-header "content-type")))
(~json-result :body body :content-type ct)))
;; ---------------------------------------------------------------------------
;; Vals & Headers
;; ---------------------------------------------------------------------------
(defhandler echo-vals (&key)
(let ((vals (request-args)))
(~echo-result :label "values" :items vals)))
(defhandler echo-headers (&key)
(let ((headers (request-headers :prefix "X-")))
(~echo-result :label "headers" :items headers)))
;; ---------------------------------------------------------------------------
;; Loading States
;; ---------------------------------------------------------------------------
(defhandler slow (&key)
(sleep 2000)
(let ((now (format-time (now) "%H:%M:%S")))
(~loading-result :time now)))
;; ---------------------------------------------------------------------------
;; Request Abort (sync replace)
;; ---------------------------------------------------------------------------
(defhandler slow-search (&key)
(let ((delay (random-int 500 2000)))
(sleep delay)
(let ((q (request-arg "q")))
(~sync-result :query q :delay delay))))
;; ---------------------------------------------------------------------------
;; Retry
;; ---------------------------------------------------------------------------
(defhandler flaky (&key)
(let ((n (inc-counter "flaky")))
(if (!= (mod n 3) 0)
(error 503)
(~retry-result :attempt n
:message "Success! The endpoint finally responded."))))

View File

@@ -35,6 +35,7 @@ def _register_sx_helpers() -> None:
"prove-data": _prove_data,
"page-helpers-demo-data": _page_helpers_demo_data,
"spec-explorer-data": _spec_explorer_data,
"handler-source": _handler_source,
})
@@ -68,6 +69,35 @@ def _component_source(name: str) -> str:
})
def _handler_source(name: str) -> str:
"""Return the pretty-printed defhandler source for a named handler."""
from shared.sx.handlers import get_handler
from shared.sx.parser import serialize
hdef = get_handler("sx", name)
if not hdef:
return f";;; Handler not found: {name}"
parts = [f"(defhandler {hdef.name}"]
if hdef.path:
parts.append(f' :path "{hdef.path}"')
if hdef.method != "get":
parts.append(f" :method :{hdef.method}")
if not hdef.csrf:
parts.append(" :csrf false")
if hdef.returns != "element":
parts.append(f' :returns "{hdef.returns}"')
param_strs = ["&key"] + list(hdef.params) if hdef.params else []
parts.append(f" ({' '.join(param_strs)})" if param_strs else " ()")
body_sx = serialize(hdef.body, pretty=True)
# Indent body by 2 spaces
body_lines = body_sx.split("\n")
parts.append(" " + body_lines[0])
for line in body_lines[1:]:
parts.append(" " + line)
return "\n".join(parts) + ")"
def _primitives_data() -> dict:
"""Return the PRIMITIVES dict for the primitives docs page."""
from content.pages import PRIMITIVES