Add attribute detail pages with live demos for SX reference
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m45s

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:
2026-03-03 17:12:57 +00:00
parent a4377668be
commit 0c9dbd6657
8 changed files with 1331 additions and 8 deletions

View File

@@ -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

View 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 "!"))))

View File

@@ -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,

View File

@@ -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
View 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.")))

View File

@@ -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 (