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 return result
raise ValueError(f"into: unsupported target type {type(target).__name__}") 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) 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") @register_io_handler("request-headers-all")
async def _io_request_headers_all( async def _io_request_headers_all(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext args: list[Any], kwargs: dict[str, Any], ctx: RequestContext

View File

@@ -202,6 +202,13 @@
:doc "All form fields as a dict." :doc "All form fields as a dict."
:context :request) :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" (define-io-primitive "request-headers-all"
:params () :params ()
:returns "dict" :returns "dict"

View File

@@ -70,6 +70,17 @@
:doc "Modulo a % b." :doc "Modulo a % b."
:body (native-mod 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" (define-primitive "sqrt"
:params ((x :as number)) :params ((x :as number))
:returns "number" :returns "number"

View File

@@ -1,714 +1,21 @@
"""SX docs page routes. """SX docs page routes.
Page GET routes are defined declaratively in sxc/pages/docs.sx via defpage. 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 from __future__ import annotations
import asyncio import asyncio
import json
import random import random
from datetime import datetime from datetime import datetime
from uuid import uuid4
from quart import Blueprint, Response, make_response, request from quart import Blueprint, Response, request
from shared.browser.app.csrf import csrf_exempt
def register(url_prefix: str = "/") -> Blueprint: def register(url_prefix: str = "/") -> Blueprint:
bp = Blueprint("pages", __name__, url_prefix=url_prefix) 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 # Reference API endpoints — remaining Python-only
# #

View File

@@ -114,3 +114,8 @@
:params (filename title desc) :params (filename title desc)
:returns "dict" :returns "dict"
:service "sx") :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-description "Click the button to load server-rendered content."
:demo (~click-to-load-demo) :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\")" :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" :comp-placeholder-id "click-comp"
:wire-placeholder-id "click-wire" :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.")) :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-description "Enter a name and submit."
:demo (~form-demo) :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\"))" :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" :comp-placeholder-id "form-comp"
:wire-placeholder-id "form-wire")) :wire-placeholder-id "form-wire"))
@@ -32,7 +32,7 @@
:demo-description "This div polls the server every 2 seconds." :demo-description "This div polls the server every 2 seconds."
:demo (~polling-demo) :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...\")" :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" :comp-placeholder-id "poll-comp"
:wire-placeholder-id "poll-wire" :wire-placeholder-id "poll-wire"
:wire-note "Updates every 2 seconds — watch the time and count change.")) :wire-note "Updates every 2 seconds — watch the time and count change."))
@@ -49,7 +49,7 @@
(list "4" "Deploy to production") (list "4" "Deploy to production")
(list "5" "Add unit tests"))) (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\")" :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" :comp-placeholder-id "delete-comp"
:wire-placeholder-id "delete-wire" :wire-placeholder-id "delete-wire"
:wire-note "Empty body — outerHTML swap replaces the target element with nothing.")) :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-description "Click edit, modify the text, save or cancel."
:demo (~inline-edit-demo) :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\")" :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-placeholder-id "edit-comp"
:comp-heading "Components" :comp-heading "Components"
:handler-heading "Server handlers" :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-description "One request updates both Box A (via sx-target) and Box B (via sx-swap-oob)."
:demo (~oob-demo) :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\")" :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-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.")) :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-description "Content loads automatically when the page renders."
:demo (~lazy-loading-demo) :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...\"))" :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" :comp-placeholder-id "lazy-comp"
:wire-placeholder-id "lazy-wire")) :wire-placeholder-id "lazy-wire"))
@@ -96,7 +96,7 @@
:demo-description "Scroll down in the container to load more items (5 pages total)." :demo-description "Scroll down in the container to load more items (5 pages total)."
:demo (~infinite-scroll-demo) :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...\")" :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" :comp-placeholder-id "scroll-comp"
:wire-placeholder-id "scroll-wire")) :wire-placeholder-id "scroll-wire"))
@@ -107,7 +107,7 @@
:demo-description "Click start to begin a simulated job." :demo-description "Click start to begin a simulated job."
:demo (~progress-bar-demo) :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\")" :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" :comp-placeholder-id "progress-comp"
:wire-placeholder-id "progress-wire")) :wire-placeholder-id "progress-wire"))
@@ -118,7 +118,7 @@
:demo-description "Type to search through 20 programming languages." :demo-description "Type to search through 20 programming languages."
:demo (~active-search-demo) :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...\")" :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" :comp-placeholder-id "search-comp"
:wire-placeholder-id "search-wire")) :wire-placeholder-id "search-wire"))
@@ -129,7 +129,7 @@
:demo-description "Enter an email and click away (blur) to validate." :demo-description "Enter an email and click away (blur) to validate."
:demo (~inline-validation-demo) :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\")" :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" :comp-placeholder-id "validate-comp"
:wire-placeholder-id "validate-wire")) :wire-placeholder-id "validate-wire"))
@@ -140,7 +140,7 @@
:demo-description "Select a category to populate the item dropdown." :demo-description "Select a category to populate the item dropdown."
:demo (~value-select-demo) :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\"))" :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" :comp-placeholder-id "values-comp"
:wire-placeholder-id "values-wire")) :wire-placeholder-id "values-wire"))
@@ -151,7 +151,7 @@
:demo-description "Submit a message — the input resets after each send." :demo-description "Submit a message — the input resets after each send."
:demo (~reset-on-submit-demo) :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\"))" :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" :comp-placeholder-id "reset-comp"
:wire-placeholder-id "reset-wire")) :wire-placeholder-id "reset-wire"))
@@ -166,7 +166,7 @@
(list "3" "Widget C" "12.00" "305") (list "3" "Widget C" "12.00" "305")
(list "4" "Widget D" "45.00" "67"))) (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\")" :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" :comp-placeholder-id "editrow-comp"
:wire-placeholder-id "editrow-wire")) :wire-placeholder-id "editrow-wire"))
@@ -182,7 +182,7 @@
(list "4" "Dan Okafor" "dan@example.com" "inactive") (list "4" "Dan Okafor" "dan@example.com" "inactive")
(list "5" "Eve Larsson" "eve@example.com" "active"))) (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\")" :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" :comp-placeholder-id "bulk-comp"
:wire-placeholder-id "bulk-wire")) :wire-placeholder-id "bulk-wire"))
@@ -193,7 +193,7 @@
:demo-description "Try each button to see different swap behaviours." :demo-description "Try each button to see different swap behaviours."
:demo (~swap-positions-demo) :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\")" :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")) :wire-placeholder-id "swap-wire"))
(defcomp ~example-select-filter () (defcomp ~example-select-filter ()
@@ -203,7 +203,7 @@
:demo-description "Different buttons select different parts of the same server response." :demo-description "Different buttons select different parts of the same server response."
:demo (~select-filter-demo) :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\")" :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")) :wire-placeholder-id "filter-wire"))
(defcomp ~example-tabs () (defcomp ~example-tabs ()
@@ -213,7 +213,7 @@
:demo-description "Click tabs to switch content. Watch the browser URL change." :demo-description "Click tabs to switch content. Watch the browser URL change."
:demo (~tabs-demo) :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\")" :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")) :wire-placeholder-id "tabs-wire"))
(defcomp ~example-animations () (defcomp ~example-animations ()
@@ -223,7 +223,7 @@
:demo-description "Click to swap in content with a fade-in animation." :demo-description "Click to swap in content with a fade-in animation."
:demo (~animations-demo) :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!\")))" :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" :comp-placeholder-id "anim-comp"
:wire-placeholder-id "anim-wire")) :wire-placeholder-id "anim-wire"))
@@ -234,7 +234,7 @@
:demo-description "Click to open a modal dialog." :demo-description "Click to open a modal dialog."
:demo (~dialogs-demo) :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\")" :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" :comp-placeholder-id "dialog-comp"
:wire-placeholder-id "dialog-wire")) :wire-placeholder-id "dialog-wire"))
@@ -245,7 +245,7 @@
:demo-description "Press s, n, or h on your keyboard." :demo-description "Press s, n, or h on your keyboard."
:demo (~keyboard-shortcuts-demo) :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...\")" :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" :comp-placeholder-id "kbd-comp"
:wire-placeholder-id "kbd-wire")) :wire-placeholder-id "kbd-wire"))
@@ -256,7 +256,7 @@
:demo-description "Click Edit All to replace the full profile via PUT." :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") :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)\"))" :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" :comp-placeholder-id "pp-comp"
:wire-placeholder-id "pp-wire")) :wire-placeholder-id "pp-wire"))
@@ -267,7 +267,7 @@
:demo-description "Submit the form and see the JSON body the server received." :demo-description "Submit the form and see the JSON body the server received."
:demo (~json-encoding-demo) :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\"))" :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" :comp-placeholder-id "json-comp"
:wire-placeholder-id "json-wire")) :wire-placeholder-id "json-wire"))
@@ -278,7 +278,7 @@
:demo-description "Click each button to see what the server receives." :demo-description "Click each button to see what the server receives."
:demo (~vals-headers-demo) :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\")" :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" :comp-placeholder-id "vals-comp"
:wire-placeholder-id "vals-wire")) :wire-placeholder-id "vals-wire"))
@@ -289,7 +289,7 @@
:demo-description "Click the button — it shows a spinner during the 2-second request." :demo-description "Click the button — it shows a spinner during the 2-second request."
:demo (~loading-states-demo) :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\")" :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" :comp-placeholder-id "loading-comp"
:wire-placeholder-id "loading-wire")) :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-description "Type quickly — only the latest result appears despite random 0.5-2s server delays."
:demo (~sync-replace-demo) :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...\")" :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" :comp-placeholder-id "sync-comp"
:wire-placeholder-id "sync-wire")) :wire-placeholder-id "sync-wire"))
@@ -311,6 +311,6 @@
:demo-description "Click the button — watch it retry automatically after failures." :demo-description "Click the button — watch it retry automatically after failures."
:demo (~retry-demo) :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\")" :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" :comp-placeholder-id "retry-comp"
:wire-placeholder-id "retry-wire")) :wire-placeholder-id "retry-wire"))

View File

@@ -24,19 +24,9 @@
(list "admin@example.com" "test@example.com" "user@example.com")) (list "admin@example.com" "test@example.com" "user@example.com"))
(define tab-content (define tab-content
{"tab1" (div (p :class "text-stone-700" "Welcome to the Overview tab.") {"tab1" "Welcome to the Overview tab. This is the default tab content loaded via sx-get."
(p :class "text-stone-500 text-sm mt-2" "tab2" "Here are the details. Version: 1.0.0, Build: 2024-01-15, Engine: sx"
"This is the default tab content loaded via sx-get.")) "tab3" "Recent history: Initial release, Added component caching, Wire format v2"})
"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")))})
(define kbd-actions (define kbd-actions
{"s" "Search panel activated" {"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, "prove-data": _prove_data,
"page-helpers-demo-data": _page_helpers_demo_data, "page-helpers-demo-data": _page_helpers_demo_data,
"spec-explorer-data": _spec_explorer_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: def _primitives_data() -> dict:
"""Return the PRIMITIVES dict for the primitives docs page.""" """Return the PRIMITIVES dict for the primitives docs page."""
from content.pages import PRIMITIVES from content.pages import PRIMITIVES