Add attribute detail pages with live demos for SX reference
Per-attribute documentation pages at /reference/attributes/<slug> with: - Live interactive demos (demo components in reference.sx) - S-expression source code display - Server handler code shown as s-expressions (defhandlers in handlers/reference.sx) - Wire response display via OOB swaps on demo interaction - Linked attribute names in the reference table Covers all 20 implemented attributes (sx-get/post/put/delete/patch, sx-trigger/target/swap/swap-oob/select/confirm/push-url/sync/encoding/ headers/include/vals/media/disable/on:*, sx-retry, data-sx, data-sx-env). Also adds sx-on:* to BEHAVIOR_ATTRS, updates REFERENCE_NAV to link /reference/attributes, and makes /reference/ an index page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -709,4 +709,177 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
oob_comp = _oob_code("retry-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Reference attribute detail API endpoints (for live demos)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _ref_wire(wire_id: str, sx_src: str) -> str:
|
||||
"""Build OOB swap showing the wire response text."""
|
||||
from sxc.sx_components import _oob_code
|
||||
return _oob_code(f"ref-wire-{wire_id}", sx_src)
|
||||
|
||||
@bp.get("/reference/api/time")
|
||||
async def ref_time():
|
||||
from shared.sx.helpers import sx_response
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(span :class "text-stone-800 text-sm" "Server time: " (strong "{now}"))'
|
||||
oob = _ref_wire("sx-get", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/reference/api/greet")
|
||||
async def ref_greet():
|
||||
from shared.sx.helpers import sx_response
|
||||
form = await request.form
|
||||
name = form.get("name") or "stranger"
|
||||
sx_src = f'(span :class "text-stone-800 text-sm" "Hello, " (strong "{name}") "!")'
|
||||
oob = _ref_wire("sx-post", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.put("/reference/api/status")
|
||||
async def ref_status():
|
||||
from shared.sx.helpers import sx_response
|
||||
form = await request.form
|
||||
status = form.get("status", "unknown")
|
||||
sx_src = f'(span :class "text-stone-700 text-sm" "Status: " (strong "{status}") " — updated via PUT")'
|
||||
oob = _ref_wire("sx-put", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.patch("/reference/api/theme")
|
||||
async def ref_theme():
|
||||
from shared.sx.helpers import sx_response
|
||||
form = await request.form
|
||||
theme = form.get("theme", "unknown")
|
||||
sx_src = f'"{theme}"'
|
||||
oob = _ref_wire("sx-patch", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.delete("/reference/api/item/<item_id>")
|
||||
async def ref_delete(item_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
oob = _ref_wire("sx-delete", '""')
|
||||
return sx_response(f'(<> {oob})')
|
||||
|
||||
@bp.get("/reference/api/trigger-search")
|
||||
async def ref_trigger_search():
|
||||
from shared.sx.helpers import sx_response
|
||||
q = request.args.get("q", "")
|
||||
if not q:
|
||||
sx_src = '(span :class "text-stone-400 text-sm" "Start typing to trigger a search.")'
|
||||
else:
|
||||
sx_src = f'(span :class "text-stone-800 text-sm" "Results for: " (strong "{q}"))'
|
||||
oob = _ref_wire("sx-trigger", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob})')
|
||||
|
||||
@bp.get("/reference/api/swap-item")
|
||||
async def ref_swap_item():
|
||||
from shared.sx.helpers import sx_response
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(div :class "text-sm text-violet-700" "New item (" "{now}" ")")'
|
||||
oob = _ref_wire("sx-swap", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob})')
|
||||
|
||||
@bp.get("/reference/api/oob")
|
||||
async def ref_oob():
|
||||
from shared.sx.helpers import sx_response
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = (
|
||||
f'(<>'
|
||||
f' (span :class "text-emerald-700 text-sm" "Main updated at " "{now}")'
|
||||
f' (div :id "ref-oob-side" :sx-swap-oob "innerHTML"'
|
||||
f' (span :class "text-violet-700 text-sm" "OOB updated at " "{now}")))')
|
||||
oob = _ref_wire("sx-swap-oob", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob})')
|
||||
|
||||
@bp.get("/reference/api/select-page")
|
||||
async def ref_select_page():
|
||||
from shared.sx.helpers import sx_response
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = (
|
||||
f'(<>'
|
||||
f' (div :id "the-header" (h3 "Page header — not selected"))'
|
||||
f' (div :id "the-content"'
|
||||
f' (span :class "text-emerald-700 text-sm"'
|
||||
f' "This fragment was selected from a larger response. Time: " "{now}"))'
|
||||
f' (div :id "the-footer" (p "Page footer — not selected")))')
|
||||
oob = _ref_wire("sx-select", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob})')
|
||||
|
||||
@bp.get("/reference/api/slow-echo")
|
||||
async def ref_slow_echo():
|
||||
from shared.sx.helpers import sx_response
|
||||
await asyncio.sleep(0.8)
|
||||
q = request.args.get("q", "")
|
||||
sx_src = f'(span :class "text-stone-800 text-sm" "Echo: " (strong "{q}"))'
|
||||
oob = _ref_wire("sx-sync", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/reference/api/upload-name")
|
||||
async def ref_upload_name():
|
||||
from shared.sx.helpers import sx_response
|
||||
files = await request.files
|
||||
f = files.get("file")
|
||||
name = f.filename if f else "(no file)"
|
||||
sx_src = f'(span :class "text-stone-800 text-sm" "Received: " (strong "{name}"))'
|
||||
oob = _ref_wire("sx-encoding", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob})')
|
||||
|
||||
@bp.get("/reference/api/echo-headers")
|
||||
async def ref_echo_headers():
|
||||
from shared.sx.helpers import sx_response
|
||||
custom = [(k, v) for k, v in request.headers if k.lower().startswith("x-")]
|
||||
if not custom:
|
||||
sx_src = '(span :class "text-stone-400 text-sm" "No custom headers received.")'
|
||||
else:
|
||||
items = " ".join(
|
||||
f'(li (strong "{k}") ": " "{v}")' for k, v in custom)
|
||||
sx_src = f'(ul :class "text-sm text-stone-700 space-y-1" {items})'
|
||||
oob = _ref_wire("sx-headers", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob})')
|
||||
|
||||
@bp.get("/reference/api/echo-vals")
|
||||
async def ref_echo_vals_get():
|
||||
from shared.sx.helpers import sx_response
|
||||
vals = list(request.args.items())
|
||||
if not vals:
|
||||
sx_src = '(span :class "text-stone-400 text-sm" "No values received.")'
|
||||
else:
|
||||
items = " ".join(
|
||||
f'(li (strong "{k}") ": " "{v}")' for k, v in vals)
|
||||
sx_src = f'(ul :class "text-sm text-stone-700 space-y-1" {items})'
|
||||
oob_include = _ref_wire("sx-include", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob_include})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/reference/api/echo-vals")
|
||||
async def ref_echo_vals_post():
|
||||
from shared.sx.helpers import sx_response
|
||||
form = await request.form
|
||||
vals = list(form.items())
|
||||
if not vals:
|
||||
sx_src = '(span :class "text-stone-400 text-sm" "No values received.")'
|
||||
else:
|
||||
items = " ".join(
|
||||
f'(li (strong "{k}") ": " "{v}")' for k, v in vals)
|
||||
sx_src = f'(ul :class "text-sm text-stone-700 space-y-1" {items})'
|
||||
oob = _ref_wire("sx-vals", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob})')
|
||||
|
||||
_ref_flaky = {"n": 0}
|
||||
|
||||
@bp.get("/reference/api/flaky")
|
||||
async def ref_flaky():
|
||||
from shared.sx.helpers import sx_response
|
||||
_ref_flaky["n"] += 1
|
||||
n = _ref_flaky["n"]
|
||||
if n % 3 != 0:
|
||||
return Response("", status=503, content_type="text/plain")
|
||||
sx_src = f'(span :class "text-emerald-700 text-sm" "Success on attempt " "{n}" "!")'
|
||||
oob = _ref_wire("sx-retry", sx_src)
|
||||
return sx_response(f'(<> {sx_src} {oob})')
|
||||
|
||||
return bp
|
||||
|
||||
@@ -20,7 +20,7 @@ DOCS_NAV = [
|
||||
]
|
||||
|
||||
REFERENCE_NAV = [
|
||||
("Attributes", "/reference/"),
|
||||
("Attributes", "/reference/attributes"),
|
||||
("Headers", "/reference/headers"),
|
||||
("Events", "/reference/events"),
|
||||
("JS API", "/reference/js-api"),
|
||||
@@ -107,6 +107,7 @@ BEHAVIOR_ATTRS = [
|
||||
("sx-vals", "Add values to the request as a JSON string", True),
|
||||
("sx-media", "Only enable this element when the media query matches", True),
|
||||
("sx-disable", "Disable sx processing on this element and its children", True),
|
||||
("sx-on:*", "Inline event handler — e.g. sx-on:click runs JavaScript on event", True),
|
||||
]
|
||||
|
||||
SX_UNIQUE_ATTRS = [
|
||||
@@ -236,3 +237,481 @@ EDIT_ROW_DATA = [
|
||||
{"id": "3", "name": "Widget C", "price": "12.00", "stock": "305"},
|
||||
{"id": "4", "name": "Widget D", "price": "45.00", "stock": "67"},
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reference: Attribute detail pages
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ATTR_DETAILS: dict[str, dict] = {
|
||||
# --- Request Attributes ---
|
||||
"sx-get": {
|
||||
"description": (
|
||||
"Issues a GET request to the given URL when triggered. "
|
||||
"The response HTML is swapped into the target element. "
|
||||
"This is the most common sx attribute — use it for loading content, "
|
||||
"navigation, and any read operation."
|
||||
),
|
||||
"demo": "ref-get-demo",
|
||||
"example": (
|
||||
'(button :sx-get "/reference/api/time"\n'
|
||||
' :sx-target "#ref-get-result"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "Load server time")'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-time (&key)\n'
|
||||
' (let ((now (format-time (now) "%H:%M:%S")))\n'
|
||||
' (span :class "text-stone-800 text-sm"\n'
|
||||
' "Server time: " (strong now))))'
|
||||
),
|
||||
},
|
||||
"sx-post": {
|
||||
"description": (
|
||||
"Issues a POST request to the given URL. "
|
||||
"Form values from the enclosing form (or sx-include target) are sent as the request body. "
|
||||
"Use for creating resources, submitting forms, and any write operation."
|
||||
),
|
||||
"demo": "ref-post-demo",
|
||||
"example": (
|
||||
'(form :sx-post "/reference/api/greet"\n'
|
||||
' :sx-target "#ref-post-result"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' (input :type "text" :name "name"\n'
|
||||
' :placeholder "Your name")\n'
|
||||
' (button :type "submit" "Greet"))'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-greet (&key)\n'
|
||||
' (let ((name (or (form-data "name") "stranger")))\n'
|
||||
' (span :class "text-stone-800 text-sm"\n'
|
||||
' "Hello, " (strong name) "!")))'
|
||||
),
|
||||
},
|
||||
"sx-put": {
|
||||
"description": (
|
||||
"Issues a PUT request to the given URL. "
|
||||
"Used for full replacement updates of a resource. "
|
||||
"Form values are sent as the request body."
|
||||
),
|
||||
"demo": "ref-put-demo",
|
||||
"example": (
|
||||
'(button :sx-put "/reference/api/status"\n'
|
||||
' :sx-target "#ref-put-view"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' :sx-vals "{\\"status\\": \\"published\\"}"\n'
|
||||
' "Publish")'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-status (&key)\n'
|
||||
' (let ((status (or (form-data "status") "unknown")))\n'
|
||||
' (span :class "text-stone-700 text-sm"\n'
|
||||
' "Status: " (strong status) " — updated via PUT")))'
|
||||
),
|
||||
},
|
||||
"sx-delete": {
|
||||
"description": (
|
||||
"Issues a DELETE request to the given URL. "
|
||||
"Commonly paired with sx-confirm for a confirmation dialog, "
|
||||
'and sx-swap "delete" to remove the element from the DOM.'
|
||||
),
|
||||
"demo": "ref-delete-demo",
|
||||
"example": (
|
||||
'(button :sx-delete "/reference/api/item/1"\n'
|
||||
' :sx-target "#ref-del-1"\n'
|
||||
' :sx-swap "delete"\n'
|
||||
' "Remove")'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-delete (&key item-id)\n'
|
||||
' ;; Empty response — swap "delete" removes the target\n'
|
||||
' "")'
|
||||
),
|
||||
},
|
||||
"sx-patch": {
|
||||
"description": (
|
||||
"Issues a PATCH request to the given URL. "
|
||||
"Used for partial updates — only changed fields are sent. "
|
||||
"Form values are sent as the request body."
|
||||
),
|
||||
"demo": "ref-patch-demo",
|
||||
"example": (
|
||||
'(button :sx-patch "/reference/api/theme"\n'
|
||||
' :sx-vals "{\\"theme\\": \\"dark\\"}"\n'
|
||||
' :sx-target "#ref-patch-val"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "Dark")'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-theme (&key)\n'
|
||||
' (let ((theme (or (form-data "theme") "unknown")))\n'
|
||||
' (str theme)))'
|
||||
),
|
||||
},
|
||||
|
||||
# --- Behavior Attributes ---
|
||||
"sx-trigger": {
|
||||
"description": (
|
||||
"Specifies which DOM event triggers the request. "
|
||||
"Defaults to 'click' for most elements and 'submit' for forms. "
|
||||
"Supports modifiers: once, changed, delay:<time>, from:<selector>, "
|
||||
"intersect, revealed, load, every:<time>. "
|
||||
"Multiple triggers can be comma-separated."
|
||||
),
|
||||
"demo": "ref-trigger-demo",
|
||||
"example": (
|
||||
'(input :type "text" :name "q"\n'
|
||||
' :placeholder "Type to search..."\n'
|
||||
' :sx-get "/reference/api/trigger-search"\n'
|
||||
' :sx-trigger "input changed delay:300ms"\n'
|
||||
' :sx-target "#ref-trigger-result"\n'
|
||||
' :sx-swap "innerHTML")'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-trigger-search (&key)\n'
|
||||
' (let ((q (or (request-arg "q") "")))\n'
|
||||
' (if (empty? q)\n'
|
||||
' (span "Start typing to trigger a search.")\n'
|
||||
' (span "Results for: " (strong q)))))'
|
||||
),
|
||||
},
|
||||
"sx-target": {
|
||||
"description": (
|
||||
"CSS selector identifying which element receives the response content. "
|
||||
'Defaults to the element itself. Use "closest <selector>" to find '
|
||||
"the nearest ancestor matching the selector."
|
||||
),
|
||||
"demo": "ref-target-demo",
|
||||
"example": (
|
||||
';; Two buttons targeting different elements\n'
|
||||
'(button :sx-get "/reference/api/time"\n'
|
||||
' :sx-target "#ref-target-a"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "Update Box A")\n'
|
||||
'\n'
|
||||
'(button :sx-get "/reference/api/time"\n'
|
||||
' :sx-target "#ref-target-b"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "Update Box B")'
|
||||
),
|
||||
},
|
||||
"sx-swap": {
|
||||
"description": (
|
||||
"Controls how the response is swapped into the target element. "
|
||||
"Values: innerHTML (default), outerHTML, afterend, beforeend, "
|
||||
"afterbegin, beforebegin, delete, none."
|
||||
),
|
||||
"demo": "ref-swap-demo",
|
||||
"example": (
|
||||
';; Append to the end of a list\n'
|
||||
'(button :sx-get "/reference/api/swap-item"\n'
|
||||
' :sx-target "#ref-swap-list"\n'
|
||||
' :sx-swap "beforeend"\n'
|
||||
' "beforeend")\n'
|
||||
'\n'
|
||||
';; Prepend to the start\n'
|
||||
'(button :sx-get "/reference/api/swap-item"\n'
|
||||
' :sx-target "#ref-swap-list"\n'
|
||||
' :sx-swap "afterbegin"\n'
|
||||
' "afterbegin")'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-swap-item (&key)\n'
|
||||
' (let ((now (format-time (now) "%H:%M:%S")))\n'
|
||||
' (div :class "text-sm text-violet-700"\n'
|
||||
' "New item (" now ")")))'
|
||||
),
|
||||
},
|
||||
"sx-swap-oob": {
|
||||
"description": (
|
||||
"Out-of-band swap — updates elements elsewhere in the DOM by ID, "
|
||||
"outside the normal target. The server includes extra elements in "
|
||||
"the response with sx-swap-oob attributes, and they are swapped "
|
||||
"into matching elements in the page."
|
||||
),
|
||||
"demo": "ref-oob-demo",
|
||||
"example": (
|
||||
'(button :sx-get "/reference/api/oob"\n'
|
||||
' :sx-target "#ref-oob-main"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "Update both boxes")'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-oob (&key)\n'
|
||||
' (let ((now (format-time (now) "%H:%M:%S")))\n'
|
||||
' (<>\n'
|
||||
' (span "Main updated at " now)\n'
|
||||
' (div :id "ref-oob-side"\n'
|
||||
' :sx-swap-oob "innerHTML"\n'
|
||||
' (span "OOB updated at " now)))))'
|
||||
),
|
||||
},
|
||||
"sx-select": {
|
||||
"description": (
|
||||
"CSS selector to pick a fragment from the response HTML. "
|
||||
"Only the matching element is swapped into the target. "
|
||||
"Useful for extracting part of a full-page response."
|
||||
),
|
||||
"demo": "ref-select-demo",
|
||||
"example": (
|
||||
'(button :sx-get "/reference/api/select-page"\n'
|
||||
' :sx-target "#ref-select-result"\n'
|
||||
' :sx-select "#the-content"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "Load (selecting #the-content)")'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-select-page (&key)\n'
|
||||
' (let ((now (format-time (now) "%H:%M:%S")))\n'
|
||||
' (<>\n'
|
||||
' (div :id "the-header" (h3 "Page header — not selected"))\n'
|
||||
' (div :id "the-content"\n'
|
||||
' (span "Selected fragment. Time: " now))\n'
|
||||
' (div :id "the-footer" (p "Page footer — not selected")))))'
|
||||
),
|
||||
},
|
||||
"sx-confirm": {
|
||||
"description": (
|
||||
"Shows a browser confirmation dialog before issuing the request. "
|
||||
"The request is cancelled if the user clicks Cancel. "
|
||||
"The value is the message shown in the dialog."
|
||||
),
|
||||
"demo": "ref-confirm-demo",
|
||||
"example": (
|
||||
'(button :sx-delete "/reference/api/item/confirm"\n'
|
||||
' :sx-target "#ref-confirm-item"\n'
|
||||
' :sx-swap "delete"\n'
|
||||
' :sx-confirm "Are you sure you want to delete this file?"\n'
|
||||
' "Delete")'
|
||||
),
|
||||
},
|
||||
"sx-push-url": {
|
||||
"description": (
|
||||
'Push the request URL into the browser location bar, enabling '
|
||||
'back/forward navigation. Set to "true" to push the request URL, '
|
||||
'or provide a custom URL string.'
|
||||
),
|
||||
"demo": "ref-pushurl-demo",
|
||||
"example": (
|
||||
'(a :href "/reference/attributes/sx-get"\n'
|
||||
' :sx-get "/reference/attributes/sx-get"\n'
|
||||
' :sx-target "#main-panel"\n'
|
||||
' :sx-select "#main-panel"\n'
|
||||
' :sx-swap "outerHTML"\n'
|
||||
' :sx-push-url "true"\n'
|
||||
' "sx-get page")'
|
||||
),
|
||||
},
|
||||
"sx-sync": {
|
||||
"description": (
|
||||
"Controls synchronization of concurrent requests from the same element. "
|
||||
'Strategies: "drop" (ignore new while in-flight), '
|
||||
'"replace" (abort in-flight, send new), '
|
||||
'"queue" (queue and send after current completes).'
|
||||
),
|
||||
"demo": "ref-sync-demo",
|
||||
"example": (
|
||||
'(input :type "text" :name "q"\n'
|
||||
' :placeholder "Type quickly..."\n'
|
||||
' :sx-get "/reference/api/slow-echo"\n'
|
||||
' :sx-trigger "input changed delay:100ms"\n'
|
||||
' :sx-sync "replace"\n'
|
||||
' :sx-target "#ref-sync-result"\n'
|
||||
' :sx-swap "innerHTML")'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-slow-echo (&key)\n'
|
||||
' (sleep 800)\n'
|
||||
' (let ((q (or (request-arg "q") "")))\n'
|
||||
' (span "Echo: " (strong q))))'
|
||||
),
|
||||
},
|
||||
"sx-encoding": {
|
||||
"description": (
|
||||
"Sets the encoding type for the request body. "
|
||||
'Use "multipart/form-data" for file uploads. '
|
||||
"Defaults to application/x-www-form-urlencoded for forms."
|
||||
),
|
||||
"demo": "ref-encoding-demo",
|
||||
"example": (
|
||||
'(form :sx-post "/reference/api/upload-name"\n'
|
||||
' :sx-encoding "multipart/form-data"\n'
|
||||
' :sx-target "#ref-encoding-result"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' (input :type "file" :name "file")\n'
|
||||
' (button :type "submit" "Upload"))'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-upload-name (&key)\n'
|
||||
' (let ((name (or (file-name "file") "(no file)")))\n'
|
||||
' (span "Received: " (strong name))))'
|
||||
),
|
||||
},
|
||||
"sx-headers": {
|
||||
"description": (
|
||||
"Adds custom headers to the request as a JSON object string. "
|
||||
"Useful for passing metadata like API keys or content types."
|
||||
),
|
||||
"demo": "ref-headers-demo",
|
||||
"example": (
|
||||
'(button :sx-get "/reference/api/echo-headers"\n'
|
||||
' :sx-headers \'{"X-Custom-Token": "abc123", "X-Request-Source": "demo"}\'\n'
|
||||
' :sx-target "#ref-headers-result"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "Send with custom headers")'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-echo-headers (&key)\n'
|
||||
' (let ((headers (request-headers :prefix "X-")))\n'
|
||||
' (if (empty? headers)\n'
|
||||
' (span "No custom headers received.")\n'
|
||||
' (ul (map (fn (h)\n'
|
||||
' (li (strong (first h)) ": " (last h)))\n'
|
||||
' headers)))))'
|
||||
),
|
||||
},
|
||||
"sx-include": {
|
||||
"description": (
|
||||
"Include values from additional elements in the request. "
|
||||
"Takes a CSS selector. The matched element's form values "
|
||||
"(inputs, selects, textareas) are added to the request."
|
||||
),
|
||||
"demo": "ref-include-demo",
|
||||
"example": (
|
||||
'(select :id "ref-inc-cat" :name "category"\n'
|
||||
' (option :value "all" "All")\n'
|
||||
' (option :value "books" "Books")\n'
|
||||
' (option :value "tools" "Tools"))\n'
|
||||
'\n'
|
||||
'(button :sx-get "/reference/api/echo-vals"\n'
|
||||
' :sx-include "#ref-inc-cat"\n'
|
||||
' :sx-target "#ref-include-result"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "Filter")'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-echo-vals (&key)\n'
|
||||
' (let ((vals (request-args)))\n'
|
||||
' (if (empty? vals)\n'
|
||||
' (span "No values received.")\n'
|
||||
' (ul (map (fn (v)\n'
|
||||
' (li (strong (first v)) ": " (last v)))\n'
|
||||
' vals)))))'
|
||||
),
|
||||
},
|
||||
"sx-vals": {
|
||||
"description": (
|
||||
"Adds extra values to the request as a JSON object string. "
|
||||
"These are merged with any form values. "
|
||||
"Useful for passing additional data without hidden inputs."
|
||||
),
|
||||
"demo": "ref-vals-demo",
|
||||
"example": (
|
||||
'(button :sx-post "/reference/api/echo-vals"\n'
|
||||
' :sx-vals \'{"source": "demo", "page": "3"}\'\n'
|
||||
' :sx-target "#ref-vals-result"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "Send with extra values")'
|
||||
),
|
||||
},
|
||||
"sx-media": {
|
||||
"description": (
|
||||
"Only enables the sx attributes on this element when the given "
|
||||
"CSS media query matches. When the media query does not match, "
|
||||
"the element behaves as a normal HTML element."
|
||||
),
|
||||
"demo": "ref-media-demo",
|
||||
"example": (
|
||||
'(a :href "/reference/attributes/sx-get"\n'
|
||||
' :sx-get "/reference/attributes/sx-get"\n'
|
||||
' :sx-target "#main-panel"\n'
|
||||
' :sx-select "#main-panel"\n'
|
||||
' :sx-swap "outerHTML"\n'
|
||||
' :sx-push-url "true"\n'
|
||||
' :sx-media "(min-width: 768px)"\n'
|
||||
' "sx navigation (desktop only)")'
|
||||
),
|
||||
},
|
||||
"sx-disable": {
|
||||
"description": (
|
||||
"Disables sx processing on this element and all its children. "
|
||||
"The element renders as normal HTML without any sx behavior. "
|
||||
"Useful for opting out of sx in specific subtrees."
|
||||
),
|
||||
"demo": "ref-disable-demo",
|
||||
"example": (
|
||||
';; Left box: sx works normally\n'
|
||||
';; Right box: sx-disable prevents any sx behavior\n'
|
||||
'(div :sx-disable "true"\n'
|
||||
' (button :sx-get "/reference/api/time"\n'
|
||||
' :sx-target "#ref-dis-b"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "Load")\n'
|
||||
' ;; This button will NOT fire an sx request\n'
|
||||
' )'
|
||||
),
|
||||
},
|
||||
"sx-on:*": {
|
||||
"description": (
|
||||
"Inline event handler — attaches JavaScript to a DOM event. "
|
||||
'The * is replaced by the event name (e.g. sx-on:click, sx-on:keydown). '
|
||||
"The handler code runs as inline JavaScript with 'this' bound to the element."
|
||||
),
|
||||
"demo": "ref-on-demo",
|
||||
"example": (
|
||||
'(button\n'
|
||||
' :sx-on:click "document.getElementById(\'ref-on-result\')\n'
|
||||
' .textContent = \'Clicked at \' + new Date()\n'
|
||||
' .toLocaleTimeString()"\n'
|
||||
' "Click me")'
|
||||
),
|
||||
},
|
||||
|
||||
# --- Unique to sx ---
|
||||
"sx-retry": {
|
||||
"description": (
|
||||
"Enables exponential backoff retry on request failure. "
|
||||
'Set to "true" for default retry behavior (3 attempts, 1s/2s/4s delays) '
|
||||
"or provide a custom retry count."
|
||||
),
|
||||
"demo": "ref-retry-demo",
|
||||
"example": (
|
||||
'(button :sx-get "/reference/api/flaky"\n'
|
||||
' :sx-target "#ref-retry-result"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' :sx-retry "true"\n'
|
||||
' "Call flaky endpoint")'
|
||||
),
|
||||
"handler": (
|
||||
'(defhandler ref-flaky (&key)\n'
|
||||
' (let ((n (inc-counter "ref-flaky")))\n'
|
||||
' (if (!= (mod n 3) 0)\n'
|
||||
' (error 503)\n'
|
||||
' (span "Success on attempt " n "!"))))'
|
||||
),
|
||||
},
|
||||
"data-sx": {
|
||||
"description": (
|
||||
"Client-side rendering — evaluates the s-expression source in this "
|
||||
"attribute and renders the result into the element. No server request "
|
||||
"is made. Useful for purely client-side UI and interactive components."
|
||||
),
|
||||
"demo": "ref-data-sx-demo",
|
||||
"example": (
|
||||
'(div :data-sx "(div :class \\"p-3 bg-violet-50 rounded\\"\n'
|
||||
' (h3 :class \\"font-semibold\\" \\"Client-rendered\\")\n'
|
||||
' (p \\"Evaluated in the browser.\\")")'
|
||||
),
|
||||
},
|
||||
"data-sx-env": {
|
||||
"description": (
|
||||
"Provides environment variables as a JSON object for data-sx rendering. "
|
||||
"These values are available as variables in the s-expression."
|
||||
),
|
||||
"demo": "ref-data-sx-env-demo",
|
||||
"example": (
|
||||
'(div\n'
|
||||
' :data-sx "(div (h3 title) (p message))"\n'
|
||||
' :data-sx-env \'{"title": "Dynamic", "message": "From env"}\')'
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -35,9 +35,15 @@
|
||||
(map (fn (cell) (td :class "px-3 py-2 text-stone-700" cell)) row)))
|
||||
rows)))))
|
||||
|
||||
(defcomp ~doc-attr-row (&key attr description exists)
|
||||
(defcomp ~doc-attr-row (&key attr description exists href)
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" attr)
|
||||
(td :class "px-3 py-2 font-mono text-sm whitespace-nowrap"
|
||||
(if href
|
||||
(a :href href
|
||||
:sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:class "text-violet-700 hover:text-violet-900 underline" attr)
|
||||
(span :class "text-violet-700" attr)))
|
||||
(td :class "px-3 py-2 text-stone-700 text-sm" description)
|
||||
(td :class "px-3 py-2 text-center"
|
||||
(if exists
|
||||
|
||||
149
sx/sxc/handlers/reference.sx
Normal file
149
sx/sxc/handlers/reference.sx
Normal file
@@ -0,0 +1,149 @@
|
||||
;; SX reference — defhandler definitions for attribute detail demos
|
||||
;;
|
||||
;; These serve the live demos on the Reference > Attributes detail pages.
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Shared: return server time
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-time (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(span :class "text-stone-800 text-sm"
|
||||
"Server time: " (strong now))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-post: greet
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-greet (&key)
|
||||
(let ((name (or (form-data "name") "stranger")))
|
||||
(span :class "text-stone-800 text-sm"
|
||||
"Hello, " (strong name) "!")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-put: update status
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-status (&key)
|
||||
(let ((status (or (form-data "status") "unknown")))
|
||||
(span :class "text-stone-700 text-sm"
|
||||
"Status: " (strong status) " — updated via PUT")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-patch: update theme
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-theme (&key)
|
||||
(let ((theme (or (form-data "theme") "unknown")))
|
||||
(str theme)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-delete: remove item
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-delete (&key item-id)
|
||||
"")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-trigger: search
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-trigger-search (&key)
|
||||
(let ((q (or (request-arg "q") "")))
|
||||
(if (empty? q)
|
||||
(span :class "text-stone-400 text-sm" "Start typing to trigger a search.")
|
||||
(span :class "text-stone-800 text-sm"
|
||||
"Results for: " (strong q)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-swap: new item
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-swap-item (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(div :class "text-sm text-violet-700"
|
||||
"New item (" now ")")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-swap-oob: update two targets
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-oob (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(<>
|
||||
(span :class "text-emerald-700 text-sm"
|
||||
"Main updated at " now)
|
||||
(div :id "ref-oob-side" :sx-swap-oob "innerHTML"
|
||||
(span :class "text-violet-700 text-sm"
|
||||
"OOB updated at " now)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-select: page with multiple sections
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-select-page (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(<>
|
||||
(div :id "the-header"
|
||||
(h3 "Page header — not selected"))
|
||||
(div :id "the-content"
|
||||
(span :class "text-emerald-700 text-sm"
|
||||
"This fragment was selected from a larger response. Time: " now))
|
||||
(div :id "the-footer"
|
||||
(p "Page footer — not selected")))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-sync: slow echo
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-slow-echo (&key)
|
||||
(sleep 800)
|
||||
(let ((q (or (request-arg "q") "")))
|
||||
(span :class "text-stone-800 text-sm"
|
||||
"Echo: " (strong q))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-encoding: file upload name
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-upload-name (&key)
|
||||
(let ((name (or (file-name "file") "(no file)")))
|
||||
(span :class "text-stone-800 text-sm"
|
||||
"Received: " (strong name))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-headers: echo custom headers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-echo-headers (&key)
|
||||
(let ((headers (request-headers :prefix "X-")))
|
||||
(if (empty? headers)
|
||||
(span :class "text-stone-400 text-sm" "No custom headers received.")
|
||||
(ul :class "text-sm text-stone-700 space-y-1"
|
||||
(map (fn (h)
|
||||
(li (strong (first h)) ": " (last h)))
|
||||
headers)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-include / sx-vals: echo all values
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-echo-vals (&key)
|
||||
(let ((vals (request-args)))
|
||||
(if (empty? vals)
|
||||
(span :class "text-stone-400 text-sm" "No values received.")
|
||||
(ul :class "text-sm text-stone-700 space-y-1"
|
||||
(map (fn (v)
|
||||
(li (strong (first v)) ": " (last v)))
|
||||
vals)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-retry: flaky endpoint
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler ref-flaky (&key)
|
||||
(let ((n (inc-counter "ref-flaky")))
|
||||
(if (!= (mod n 3) 0)
|
||||
(error 503)
|
||||
(span :class "text-emerald-700 text-sm"
|
||||
"Success on attempt " n "!"))))
|
||||
@@ -148,6 +148,7 @@ def _register_sx_helpers() -> None:
|
||||
from shared.sx.pages import register_page_helpers
|
||||
from sxc.sx_components import (
|
||||
_docs_content_sx, _reference_content_sx,
|
||||
_reference_index_sx, _reference_attr_detail_sx,
|
||||
_protocol_content_sx, _examples_content_sx,
|
||||
_essay_content_sx,
|
||||
_docs_nav_sx, _reference_nav_sx,
|
||||
@@ -189,6 +190,8 @@ def _register_sx_helpers() -> None:
|
||||
"home-content": _home_content,
|
||||
"docs-content": _docs_content_sx,
|
||||
"reference-content": _reference_content_sx,
|
||||
"reference-index-content": _reference_index_sx,
|
||||
"reference-attr-detail": _reference_attr_detail_sx,
|
||||
"protocol-content": _protocol_content_sx,
|
||||
"examples-content": _examples_content_sx,
|
||||
"essay-content": _essay_content_sx,
|
||||
|
||||
@@ -48,9 +48,9 @@
|
||||
:section "Reference"
|
||||
:sub-label "Reference"
|
||||
:sub-href "/reference/"
|
||||
:sub-nav (reference-nav "Attributes")
|
||||
:selected "Attributes")
|
||||
:content (reference-content ""))
|
||||
:sub-nav (reference-nav "")
|
||||
:selected "")
|
||||
:content (reference-index-content))
|
||||
|
||||
(defpage reference-page
|
||||
:path "/reference/<slug>"
|
||||
@@ -63,6 +63,17 @@
|
||||
:selected (or (find-current REFERENCE_NAV slug) ""))
|
||||
:content (reference-content slug))
|
||||
|
||||
(defpage reference-attr-detail
|
||||
:path "/reference/attributes/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Reference"
|
||||
:sub-label "Reference"
|
||||
:sub-href "/reference/"
|
||||
:sub-nav (reference-nav "Attributes")
|
||||
:selected "Attributes")
|
||||
:content (reference-attr-detail slug))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Protocols section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
408
sx/sxc/reference.sx
Normal file
408
sx/sxc/reference.sx
Normal file
@@ -0,0 +1,408 @@
|
||||
;; SX reference — demo components for attribute detail pages
|
||||
;;
|
||||
;; Each attribute gets a small, focused demo showing exactly
|
||||
;; what that attribute does.
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-get
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-get-demo ()
|
||||
(div :class "space-y-3"
|
||||
(button
|
||||
:sx-get "/reference/api/time"
|
||||
:sx-target "#ref-get-result"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Load server time")
|
||||
(div :id "ref-get-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
"Click to load.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-post
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-post-demo ()
|
||||
(div :class "space-y-3"
|
||||
(form
|
||||
:sx-post "/reference/api/greet"
|
||||
:sx-target "#ref-post-result"
|
||||
:sx-swap "innerHTML"
|
||||
:class "flex gap-2"
|
||||
(input :type "text" :name "name" :placeholder "Your name"
|
||||
:class "flex-1 px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
||||
(button :type "submit"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Greet"))
|
||||
(div :id "ref-post-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
"Submit to see greeting.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-put
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-put-demo ()
|
||||
(div :id "ref-put-view"
|
||||
(div :class "flex items-center justify-between p-3 bg-stone-50 rounded"
|
||||
(span :class "text-stone-700 text-sm" "Status: " (strong "draft"))
|
||||
(button
|
||||
:sx-put "/reference/api/status"
|
||||
:sx-target "#ref-put-view"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-vals "{\"status\": \"published\"}"
|
||||
:class "px-3 py-1 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Publish"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-delete
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-delete-demo ()
|
||||
(div :class "space-y-2"
|
||||
(div :id "ref-del-1" :class "flex items-center justify-between p-2 border border-stone-200 rounded"
|
||||
(span :class "text-sm text-stone-700" "Item A")
|
||||
(button :sx-delete "/reference/api/item/1"
|
||||
:sx-target "#ref-del-1" :sx-swap "delete"
|
||||
:class "text-red-500 text-sm hover:text-red-700" "Remove"))
|
||||
(div :id "ref-del-2" :class "flex items-center justify-between p-2 border border-stone-200 rounded"
|
||||
(span :class "text-sm text-stone-700" "Item B")
|
||||
(button :sx-delete "/reference/api/item/2"
|
||||
:sx-target "#ref-del-2" :sx-swap "delete"
|
||||
:class "text-red-500 text-sm hover:text-red-700" "Remove"))
|
||||
(div :id "ref-del-3" :class "flex items-center justify-between p-2 border border-stone-200 rounded"
|
||||
(span :class "text-sm text-stone-700" "Item C")
|
||||
(button :sx-delete "/reference/api/item/3"
|
||||
:sx-target "#ref-del-3" :sx-swap "delete"
|
||||
:class "text-red-500 text-sm hover:text-red-700" "Remove"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-patch
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-patch-demo ()
|
||||
(div :id "ref-patch-view" :class "space-y-2"
|
||||
(div :class "p-3 bg-stone-50 rounded"
|
||||
(span :class "text-stone-700 text-sm" "Theme: " (strong :id "ref-patch-val" "light")))
|
||||
(div :class "flex gap-2"
|
||||
(button :sx-patch "/reference/api/theme"
|
||||
:sx-vals "{\"theme\": \"dark\"}"
|
||||
:sx-target "#ref-patch-val" :sx-swap "innerHTML"
|
||||
:class "px-3 py-1 bg-stone-800 text-white rounded text-sm" "Dark")
|
||||
(button :sx-patch "/reference/api/theme"
|
||||
:sx-vals "{\"theme\": \"light\"}"
|
||||
:sx-target "#ref-patch-val" :sx-swap "innerHTML"
|
||||
:class "px-3 py-1 bg-white border border-stone-300 text-stone-700 rounded text-sm" "Light"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-trigger
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-trigger-demo ()
|
||||
(div :class "space-y-3"
|
||||
(input :type "text" :name "q" :placeholder "Type to search..."
|
||||
:sx-get "/reference/api/trigger-search"
|
||||
:sx-trigger "input changed delay:300ms"
|
||||
:sx-target "#ref-trigger-result"
|
||||
:sx-swap "innerHTML"
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
||||
(div :id "ref-trigger-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
"Start typing to trigger a search.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-target
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-target-demo ()
|
||||
(div :class "space-y-3"
|
||||
(div :class "flex gap-2"
|
||||
(button :sx-get "/reference/api/time"
|
||||
:sx-target "#ref-target-a"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-3 py-1 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Update Box A")
|
||||
(button :sx-get "/reference/api/time"
|
||||
:sx-target "#ref-target-b"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-3 py-1 bg-emerald-600 text-white rounded text-sm hover:bg-emerald-700"
|
||||
"Update Box B"))
|
||||
(div :class "grid grid-cols-2 gap-3"
|
||||
(div :id "ref-target-a" :class "p-3 rounded border border-violet-200 bg-violet-50 text-sm text-stone-500"
|
||||
"Box A")
|
||||
(div :id "ref-target-b" :class "p-3 rounded border border-emerald-200 bg-emerald-50 text-sm text-stone-500"
|
||||
"Box B"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-swap
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-swap-demo ()
|
||||
(div :class "space-y-3"
|
||||
(div :class "flex gap-2 flex-wrap"
|
||||
(button :sx-get "/reference/api/swap-item"
|
||||
:sx-target "#ref-swap-list" :sx-swap "beforeend"
|
||||
:class "px-3 py-1 bg-violet-600 text-white rounded text-sm" "beforeend")
|
||||
(button :sx-get "/reference/api/swap-item"
|
||||
:sx-target "#ref-swap-list" :sx-swap "afterbegin"
|
||||
:class "px-3 py-1 bg-emerald-600 text-white rounded text-sm" "afterbegin")
|
||||
(button :sx-get "/reference/api/swap-item"
|
||||
:sx-target "#ref-swap-list" :sx-swap "innerHTML"
|
||||
:class "px-3 py-1 bg-blue-600 text-white rounded text-sm" "innerHTML"))
|
||||
(div :id "ref-swap-list"
|
||||
:class "p-3 rounded border border-stone-200 space-y-1 min-h-[3rem]"
|
||||
(div :class "text-sm text-stone-500" "Original item"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-swap-oob
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-oob-demo ()
|
||||
(div :class "space-y-3"
|
||||
(button :sx-get "/reference/api/oob"
|
||||
:sx-target "#ref-oob-main"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Update both boxes")
|
||||
(div :class "grid grid-cols-2 gap-3"
|
||||
(div :class "rounded border border-stone-200 p-3"
|
||||
(div :class "text-xs text-stone-400 mb-1" "Main target")
|
||||
(div :id "ref-oob-main" :class "text-sm text-stone-500" "Waiting..."))
|
||||
(div :class "rounded border border-stone-200 p-3"
|
||||
(div :class "text-xs text-stone-400 mb-1" "OOB target")
|
||||
(div :id "ref-oob-side" :class "text-sm text-stone-500" "Waiting...")))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-select
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-select-demo ()
|
||||
(div :class "space-y-3"
|
||||
(button :sx-get "/reference/api/select-page"
|
||||
:sx-target "#ref-select-result"
|
||||
:sx-select "#the-content"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Load (selecting #the-content)")
|
||||
(div :id "ref-select-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
"Only the selected fragment will appear here.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-confirm
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-confirm-demo ()
|
||||
(div :class "space-y-2"
|
||||
(div :id "ref-confirm-item"
|
||||
:class "flex items-center justify-between p-3 border border-stone-200 rounded"
|
||||
(span :class "text-sm text-stone-700" "Important file.txt")
|
||||
(button :sx-delete "/reference/api/item/confirm"
|
||||
:sx-target "#ref-confirm-item" :sx-swap "delete"
|
||||
:sx-confirm "Are you sure you want to delete this file?"
|
||||
:class "px-3 py-1 text-red-500 text-sm border border-red-200 rounded hover:bg-red-50"
|
||||
"Delete"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-push-url
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-pushurl-demo ()
|
||||
(div :class "space-y-3"
|
||||
(div :class "flex gap-2"
|
||||
(a :href "/reference/attributes/sx-get"
|
||||
:sx-get "/reference/attributes/sx-get"
|
||||
:sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:class "px-3 py-1 bg-violet-100 text-violet-700 rounded text-sm no-underline hover:bg-violet-200"
|
||||
"sx-get page")
|
||||
(a :href "/reference/attributes/sx-post"
|
||||
:sx-get "/reference/attributes/sx-post"
|
||||
:sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:class "px-3 py-1 bg-violet-100 text-violet-700 rounded text-sm no-underline hover:bg-violet-200"
|
||||
"sx-post page"))
|
||||
(p :class "text-sm text-stone-500"
|
||||
"Click a link — the URL bar updates without a full page reload. Use browser back to return.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-sync
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-sync-demo ()
|
||||
(div :class "space-y-3"
|
||||
(input :type "text" :name "q" :placeholder "Type quickly..."
|
||||
:sx-get "/reference/api/slow-echo"
|
||||
:sx-trigger "input changed delay:100ms"
|
||||
:sx-sync "replace"
|
||||
:sx-target "#ref-sync-result"
|
||||
:sx-swap "innerHTML"
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
||||
(p :class "text-xs text-stone-400"
|
||||
"With sync:replace, each new keystroke aborts the in-flight request.")
|
||||
(div :id "ref-sync-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
"Type to see only the latest result.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-encoding
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-encoding-demo ()
|
||||
(div :class "space-y-3"
|
||||
(form :sx-post "/reference/api/upload-name"
|
||||
:sx-encoding "multipart/form-data"
|
||||
:sx-target "#ref-encoding-result"
|
||||
:sx-swap "innerHTML"
|
||||
:class "flex gap-2"
|
||||
(input :type "file" :name "file"
|
||||
:class "flex-1 text-sm text-stone-500 file:mr-2 file:px-3 file:py-1 file:rounded file:border-0 file:text-sm file:bg-violet-50 file:text-violet-700")
|
||||
(button :type "submit"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Upload"))
|
||||
(div :id "ref-encoding-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
"Select a file and submit.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-headers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-headers-demo ()
|
||||
(div :class "space-y-3"
|
||||
(button :sx-get "/reference/api/echo-headers"
|
||||
:sx-headers "{\"X-Custom-Token\": \"abc123\", \"X-Request-Source\": \"demo\"}"
|
||||
:sx-target "#ref-headers-result"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Send with custom headers")
|
||||
(div :id "ref-headers-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
"Click to see echoed headers.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-include
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-include-demo ()
|
||||
(div :class "space-y-3"
|
||||
(div :class "flex gap-2 items-end"
|
||||
(div
|
||||
(label :class "block text-xs text-stone-500 mb-1" "Category")
|
||||
(select :id "ref-inc-cat" :name "category"
|
||||
:class "px-3 py-2 border border-stone-300 rounded text-sm"
|
||||
(option :value "all" "All")
|
||||
(option :value "books" "Books")
|
||||
(option :value "tools" "Tools")))
|
||||
(button :sx-get "/reference/api/echo-vals"
|
||||
:sx-include "#ref-inc-cat"
|
||||
:sx-target "#ref-include-result"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Filter"))
|
||||
(div :id "ref-include-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
"Click Filter — the select value is included in the request.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-vals
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-vals-demo ()
|
||||
(div :class "space-y-3"
|
||||
(button :sx-post "/reference/api/echo-vals"
|
||||
:sx-vals "{\"source\": \"demo\", \"page\": \"3\"}"
|
||||
:sx-target "#ref-vals-result"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Send with extra values")
|
||||
(div :id "ref-vals-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
"Click to see echoed values.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-media
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-media-demo ()
|
||||
(div :class "space-y-3"
|
||||
(a :href "/reference/attributes/sx-get"
|
||||
:sx-get "/reference/attributes/sx-get"
|
||||
:sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:sx-media "(min-width: 768px)"
|
||||
:class "inline-block px-4 py-2 bg-violet-600 text-white rounded text-sm no-underline hover:bg-violet-700"
|
||||
"sx navigation (desktop only)")
|
||||
(p :class "text-sm text-stone-500"
|
||||
"On screens narrower than 768px this link uses normal navigation. On wider screens it uses sx.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-disable
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-disable-demo ()
|
||||
(div :class "space-y-3"
|
||||
(div :class "grid grid-cols-2 gap-3"
|
||||
(div :class "p-3 border border-stone-200 rounded"
|
||||
(p :class "text-xs text-stone-400 mb-2" "sx enabled")
|
||||
(button :sx-get "/reference/api/time"
|
||||
:sx-target "#ref-dis-a" :sx-swap "innerHTML"
|
||||
:class "px-3 py-1 bg-violet-600 text-white rounded text-sm" "Load")
|
||||
(div :id "ref-dis-a" :class "mt-2 text-sm text-stone-500" "—"))
|
||||
(div :sx-disable "true" :class "p-3 border border-stone-200 rounded"
|
||||
(p :class "text-xs text-stone-400 mb-2" "sx disabled")
|
||||
(button :sx-get "/reference/api/time"
|
||||
:sx-target "#ref-dis-b" :sx-swap "innerHTML"
|
||||
:class "px-3 py-1 bg-stone-400 text-white rounded text-sm" "Load")
|
||||
(div :id "ref-dis-b" :class "mt-2 text-sm text-stone-500"
|
||||
"Button won't fire sx request")))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-on:*
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-on-demo ()
|
||||
(div :class "space-y-3"
|
||||
(button
|
||||
:sx-on:click "document.getElementById('ref-on-result').textContent = 'Clicked at ' + new Date().toLocaleTimeString()"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Click me")
|
||||
(div :id "ref-on-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
"Click the button — runs JavaScript, no server request.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-retry
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-retry-demo ()
|
||||
(div :class "space-y-3"
|
||||
(button :sx-get "/reference/api/flaky"
|
||||
:sx-target "#ref-retry-result"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-retry "true"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Call flaky endpoint")
|
||||
(div :id "ref-retry-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
"This endpoint fails 2 out of 3 times. sx-retry retries automatically.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; data-sx
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-data-sx-demo ()
|
||||
(div :class "space-y-3"
|
||||
(div :data-sx "(div :class \"p-3 bg-violet-50 rounded\" (h3 :class \"font-semibold text-violet-800\" \"Client-rendered\") (p :class \"text-sm text-stone-600\" \"This was evaluated in the browser — no server request.\"))")
|
||||
(p :class "text-xs text-stone-400" "The content above is rendered client-side from the data-sx attribute.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; data-sx-env
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-data-sx-env-demo ()
|
||||
(div :class "space-y-3"
|
||||
(div :data-sx "(div :class \"p-3 bg-emerald-50 rounded\" (h3 :class \"font-semibold text-emerald-800\" title) (p :class \"text-sm text-stone-600\" message))"
|
||||
:data-sx-env "{\"title\": \"Dynamic content\", \"message\": \"Variables passed via data-sx-env are available in the expression.\"}")
|
||||
(p :class "text-xs text-stone-400" "The title and message above come from the data-sx-env JSON.")))
|
||||
@@ -189,10 +189,13 @@ def _doc_nav_sx(items: list[tuple[str, str]], current: str) -> str:
|
||||
|
||||
def _attr_table_sx(title: str, attrs: list[tuple[str, str, bool]]) -> str:
|
||||
"""Build an attribute reference table."""
|
||||
from content.pages import ATTR_DETAILS
|
||||
rows = []
|
||||
for attr, desc, exists in attrs:
|
||||
href = f"/reference/attributes/{attr}" if exists and attr in ATTR_DETAILS else None
|
||||
rows.append(sx_call("doc-attr-row", attr=attr, description=desc,
|
||||
exists="true" if exists else None))
|
||||
exists="true" if exists else None,
|
||||
href=href))
|
||||
return (
|
||||
f'(div :class "space-y-3"'
|
||||
f' (h3 :class "text-xl font-semibold text-stone-700" "{title}")'
|
||||
@@ -470,7 +473,6 @@ def _docs_server_rendering_sx() -> str:
|
||||
|
||||
def _reference_content_sx(slug: str) -> str:
|
||||
builders = {
|
||||
"": _reference_attrs_sx,
|
||||
"attributes": _reference_attrs_sx,
|
||||
"headers": _reference_headers_sx,
|
||||
"events": _reference_events_sx,
|
||||
@@ -479,6 +481,98 @@ def _reference_content_sx(slug: str) -> str:
|
||||
return builders.get(slug or "", _reference_attrs_sx)()
|
||||
|
||||
|
||||
def _reference_index_sx() -> str:
|
||||
"""Build the reference index page with links to sub-sections."""
|
||||
sections = [
|
||||
("Attributes", "/reference/attributes",
|
||||
"All sx attributes — request verbs, behavior modifiers, and sx-unique features."),
|
||||
("Headers", "/reference/headers",
|
||||
"Custom HTTP headers used to coordinate between the sx client and server."),
|
||||
("Events", "/reference/events",
|
||||
"DOM events fired during the sx request lifecycle."),
|
||||
("JS API", "/reference/js-api",
|
||||
"JavaScript functions for parsing, evaluating, and rendering s-expressions."),
|
||||
]
|
||||
cards = []
|
||||
for label, href, desc in sections:
|
||||
cards.append(
|
||||
f'(a :href "{href}"'
|
||||
f' :sx-get "{href}" :sx-target "#main-panel" :sx-select "#main-panel"'
|
||||
f' :sx-swap "outerHTML" :sx-push-url "true"'
|
||||
f' :class "block p-5 rounded-lg border border-stone-200 hover:border-violet-300'
|
||||
f' hover:shadow-sm transition-all no-underline"'
|
||||
f' (h3 :class "text-lg font-semibold text-violet-700 mb-1" "{label}")'
|
||||
f' (p :class "text-stone-600 text-sm" "{desc}"))'
|
||||
)
|
||||
return (
|
||||
f'(~doc-page :title "Reference"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "Complete reference for the sx client library.")'
|
||||
f' (div :class "grid gap-4 sm:grid-cols-2"'
|
||||
f' {" ".join(cards)}))'
|
||||
)
|
||||
|
||||
|
||||
def _reference_attr_detail_sx(slug: str) -> str:
|
||||
"""Build a detail page for a single sx attribute."""
|
||||
from content.pages import ATTR_DETAILS
|
||||
detail = ATTR_DETAILS.get(slug)
|
||||
if not detail:
|
||||
return (
|
||||
f'(~doc-page :title "Not Found"'
|
||||
f' (p :class "text-stone-600"'
|
||||
f' "No documentation found for \\"{slug}\\"."))'
|
||||
)
|
||||
|
||||
title = slug
|
||||
desc = detail["description"]
|
||||
escaped_desc = desc.replace('\\', '\\\\').replace('"', '\\"')
|
||||
|
||||
# Live demo
|
||||
demo_name = detail.get("demo")
|
||||
demo_sx = ""
|
||||
if demo_name:
|
||||
demo_sx = (
|
||||
f' (~example-card :title "Demo"'
|
||||
f' (~example-demo (~{demo_name})))'
|
||||
)
|
||||
|
||||
# S-expression source
|
||||
example_sx = _example_code(detail["example"], "lisp")
|
||||
|
||||
# Server handler (s-expression)
|
||||
handler_sx = ""
|
||||
if "handler" in detail:
|
||||
handler_sx = (
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6"'
|
||||
f' "Server handler")'
|
||||
f' {_example_code(detail["handler"], "lisp")}'
|
||||
)
|
||||
|
||||
# Wire response placeholder (only for attrs with server interaction)
|
||||
wire_sx = ""
|
||||
if "handler" in detail:
|
||||
wire_id = slug.replace(":", "-").replace("*", "star")
|
||||
wire_sx = (
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6"'
|
||||
f' "Wire response")'
|
||||
f' (p :class "text-stone-500 text-sm mb-2"'
|
||||
f' "Trigger the demo to see the raw response the server sends.")'
|
||||
f' {_placeholder("ref-wire-" + wire_id)}'
|
||||
)
|
||||
|
||||
return (
|
||||
f'(~doc-page :title "{title}"'
|
||||
f' (p :class "text-stone-600 mb-6" "{escaped_desc}")'
|
||||
f' {demo_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6"'
|
||||
f' "S-expression")'
|
||||
f' {example_sx}'
|
||||
f' {handler_sx}'
|
||||
f' {wire_sx})'
|
||||
)
|
||||
|
||||
|
||||
def _reference_attrs_sx() -> str:
|
||||
from content.pages import REQUEST_ATTRS, BEHAVIOR_ATTRS, SX_UNIQUE_ATTRS, HTMX_MISSING_ATTRS
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user