Add header and event detail pages, fix copyright, rename essay
- Detail pages for all 18 HTTP headers with descriptions, example usage, direction badges (request/response/both), and live demos for SX-Prompt, SX-Trigger, SX-Retarget - Detail pages for all 10 DOM events with descriptions, example usage, and live demos for beforeRequest, afterSettle, responseError, validationFailed - Header and event table rows now link to their detail pages - Fix copyright symbol on home page (was literal \u00a9, now actual ©) - Rename "Godel, Escher, Bach" essay to "Strange Loops" with updated summary - Remove duplicate script injection from bootstrapper page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -901,4 +901,30 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
return Response(generate(), content_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
||||
|
||||
# --- Header demos ---
|
||||
|
||||
@bp.get("/reference/api/trigger-event")
|
||||
async def ref_trigger_event():
|
||||
from shared.sx.helpers import sx_response
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(span :class "text-stone-800 text-sm" "Loaded at " (strong "{now}") " — check the border!")'
|
||||
resp = sx_response(sx_src)
|
||||
resp.headers["SX-Trigger"] = "showNotice"
|
||||
return resp
|
||||
|
||||
@bp.get("/reference/api/retarget")
|
||||
async def ref_retarget():
|
||||
from shared.sx.helpers import sx_response
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(span :class "text-violet-700 text-sm" "Retargeted at " (strong "{now}"))'
|
||||
resp = sx_response(sx_src)
|
||||
resp.headers["SX-Retarget"] = "#ref-hdr-retarget-alt"
|
||||
return resp
|
||||
|
||||
# --- Event demos ---
|
||||
|
||||
@bp.get("/reference/api/error-500")
|
||||
async def ref_error_500():
|
||||
return Response("Server error", status=500, content_type="text/plain")
|
||||
|
||||
return bp
|
||||
|
||||
@@ -261,6 +261,463 @@ EDIT_ROW_DATA = [
|
||||
{"id": "4", "name": "Widget D", "price": "45.00", "stock": "67"},
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reference: Header detail pages
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
HEADER_DETAILS: dict[str, dict] = {
|
||||
# --- Request Headers ---
|
||||
"SX-Request": {
|
||||
"direction": "request",
|
||||
"description": (
|
||||
"Sent on every sx-initiated request. Allows the server to distinguish "
|
||||
"AJAX partial requests from full page loads, and return the appropriate "
|
||||
"response format (fragment vs full page)."
|
||||
),
|
||||
"example": (
|
||||
';; Server-side: check for sx request\n'
|
||||
'(if (header "SX-Request")\n'
|
||||
' ;; Return a fragment\n'
|
||||
' (div :class "result" "Partial content")\n'
|
||||
' ;; Return full page\n'
|
||||
' (~full-page-layout ...))'
|
||||
),
|
||||
},
|
||||
"SX-Current-URL": {
|
||||
"direction": "request",
|
||||
"description": (
|
||||
"Sends the browser's current URL so the server knows where the user is. "
|
||||
"Useful for server-side logic that depends on context — e.g. highlighting "
|
||||
"the current nav item, or returning context-appropriate content."
|
||||
),
|
||||
"example": (
|
||||
';; Server reads the current URL to decide context\n'
|
||||
'(let ((url (header "SX-Current-URL")))\n'
|
||||
' (nav\n'
|
||||
' (a :href "/docs" :class (if (starts-with? url "/docs") "active" "") "Docs")\n'
|
||||
' (a :href "/api" :class (if (starts-with? url "/api") "active" "") "API")))'
|
||||
),
|
||||
},
|
||||
"SX-Target": {
|
||||
"direction": "request",
|
||||
"description": (
|
||||
"Tells the server which element will receive the response. "
|
||||
"The server can use this to tailor the response — for example, "
|
||||
"returning different content depending on whether the target is "
|
||||
"a sidebar, modal, or main panel."
|
||||
),
|
||||
"example": (
|
||||
';; Server checks target to decide response format\n'
|
||||
'(let ((target (header "SX-Target")))\n'
|
||||
' (if (= target "#sidebar")\n'
|
||||
' (~compact-summary :data data)\n'
|
||||
' (~full-detail :data data)))'
|
||||
),
|
||||
},
|
||||
"SX-Components": {
|
||||
"direction": "request",
|
||||
"description": (
|
||||
"Comma-separated list of component names the client already has cached. "
|
||||
"The server can skip sending defcomp definitions the client already knows, "
|
||||
"reducing response size. This is the component caching protocol."
|
||||
),
|
||||
"example": (
|
||||
';; Client sends: SX-Components: ~card,~nav-link,~footer\n'
|
||||
';; Server omits those defcomps from the response.\n'
|
||||
';; Only new/changed components are sent.\n'
|
||||
'(response\n'
|
||||
' :components (filter-new known-components)\n'
|
||||
' :content (~page-content))'
|
||||
),
|
||||
},
|
||||
"SX-Css": {
|
||||
"direction": "request",
|
||||
"description": (
|
||||
"Sends the CSS classes or hash the client already has. "
|
||||
"The server uses this to send only new CSS rules the client needs, "
|
||||
"avoiding duplicate rule injection. Part of the on-demand CSS protocol."
|
||||
),
|
||||
"example": (
|
||||
';; Client sends hash of known CSS classes\n'
|
||||
';; Server compares and only returns new classes\n'
|
||||
'(let ((client-css (header "SX-Css")))\n'
|
||||
' (set-header "SX-Css-Add"\n'
|
||||
' (join "," (diff new-classes client-css))))'
|
||||
),
|
||||
},
|
||||
"SX-History-Restore": {
|
||||
"direction": "request",
|
||||
"description": (
|
||||
"Set to \"true\" when the browser restores a page from history (back/forward). "
|
||||
"The server can use this to return cached content or skip side effects "
|
||||
"that should only happen on initial navigation."
|
||||
),
|
||||
"example": (
|
||||
';; Skip analytics on history restore\n'
|
||||
'(when (not (header "SX-History-Restore"))\n'
|
||||
' (track-page-view url))\n'
|
||||
'(~page-content :data data)'
|
||||
),
|
||||
},
|
||||
"SX-Css-Hash": {
|
||||
"direction": "both",
|
||||
"description": (
|
||||
"Request: 8-character hash of the client's known CSS class set. "
|
||||
"Response: hash of the cumulative CSS set after this response. "
|
||||
"Client stores the response hash and sends it on the next request, "
|
||||
"enabling efficient CSS delta tracking."
|
||||
),
|
||||
"example": (
|
||||
';; Request header: SX-Css-Hash: a1b2c3d4\n'
|
||||
';; Server compares hash to decide if CSS diff needed\n'
|
||||
';;\n'
|
||||
';; Response header: SX-Css-Hash: e5f6g7h8\n'
|
||||
';; Client stores new hash for next request'
|
||||
),
|
||||
},
|
||||
"SX-Prompt": {
|
||||
"direction": "request",
|
||||
"description": (
|
||||
"Contains the value entered by the user in a window.prompt() dialog, "
|
||||
"triggered by the sx-prompt attribute. Allows collecting a single text "
|
||||
"input without a form."
|
||||
),
|
||||
"example": (
|
||||
';; Button triggers a prompt dialog\n'
|
||||
'(button :sx-get "/api/rename"\n'
|
||||
' :sx-prompt "Enter new name:"\n'
|
||||
' "Rename")\n'
|
||||
'\n'
|
||||
';; Server reads the prompted value\n'
|
||||
'(let ((name (header "SX-Prompt")))\n'
|
||||
' (span "Renamed to: " (strong name)))'
|
||||
),
|
||||
"demo": "ref-header-prompt-demo",
|
||||
},
|
||||
# --- Response Headers ---
|
||||
"SX-Css-Add": {
|
||||
"direction": "response",
|
||||
"description": (
|
||||
"Comma-separated list of new CSS class names added by this response. "
|
||||
"The client injects the corresponding CSS rules into the document. "
|
||||
"Only classes the client doesn't already have are included."
|
||||
),
|
||||
"example": (
|
||||
';; Server response includes new CSS classes\n'
|
||||
';; SX-Css-Add: bg-emerald-500,text-white,rounded-xl\n'
|
||||
';;\n'
|
||||
';; Client automatically injects rules for these\n'
|
||||
';; classes from the style dictionary.'
|
||||
),
|
||||
},
|
||||
"SX-Trigger": {
|
||||
"direction": "response",
|
||||
"description": (
|
||||
"Dispatch custom DOM event(s) on the target element after the response "
|
||||
"is received. Can be a simple event name or JSON for multiple events "
|
||||
"with detail data. Useful for coordinating UI updates across components."
|
||||
),
|
||||
"example": (
|
||||
';; Simple event\n'
|
||||
';; SX-Trigger: itemAdded\n'
|
||||
';;\n'
|
||||
';; Multiple events with data\n'
|
||||
';; SX-Trigger: {"itemAdded": {"id": 42}, "showNotification": {"message": "Saved!"}}\n'
|
||||
';;\n'
|
||||
';; Listen in SX:\n'
|
||||
'(div :sx-on:itemAdded "this.querySelector(\'.count\').textContent = event.detail.id")'
|
||||
),
|
||||
"demo": "ref-header-trigger-demo",
|
||||
},
|
||||
"SX-Trigger-After-Swap": {
|
||||
"direction": "response",
|
||||
"description": (
|
||||
"Like SX-Trigger, but fires after the DOM swap completes. "
|
||||
"Use this when your event handler needs to reference the new DOM content "
|
||||
"that was just swapped in."
|
||||
),
|
||||
"example": (
|
||||
';; Server signals that new content needs initialization\n'
|
||||
';; SX-Trigger-After-Swap: contentReady\n'
|
||||
';;\n'
|
||||
';; Client initializes after swap\n'
|
||||
'(div :sx-on:contentReady "initCharts(this)")'
|
||||
),
|
||||
},
|
||||
"SX-Trigger-After-Settle": {
|
||||
"direction": "response",
|
||||
"description": (
|
||||
"Like SX-Trigger, but fires after the DOM has fully settled — "
|
||||
"scripts executed, transitions complete. The latest point to react "
|
||||
"to a response."
|
||||
),
|
||||
"example": (
|
||||
';; SX-Trigger-After-Settle: animationReady\n'
|
||||
';;\n'
|
||||
';; Trigger animations after everything has settled\n'
|
||||
'(div :sx-on:animationReady "this.classList.add(\'fade-in\')")'
|
||||
),
|
||||
},
|
||||
"SX-Retarget": {
|
||||
"direction": "response",
|
||||
"description": (
|
||||
"Override the target element for this response. The server can redirect "
|
||||
"content to a different element than what the client specified in sx-target. "
|
||||
"Useful for error messages or redirecting content dynamically."
|
||||
),
|
||||
"example": (
|
||||
';; Client targets a form result area\n'
|
||||
'(form :sx-post "/api/save"\n'
|
||||
' :sx-target "#result" ...)\n'
|
||||
'\n'
|
||||
';; Server redirects errors to a different element\n'
|
||||
';; SX-Retarget: #error-banner\n'
|
||||
'(div :class "error" "Validation failed")'
|
||||
),
|
||||
"demo": "ref-header-retarget-demo",
|
||||
},
|
||||
"SX-Reswap": {
|
||||
"direction": "response",
|
||||
"description": (
|
||||
"Override the swap strategy for this response. The server can change "
|
||||
"how content is inserted regardless of what the client specified in sx-swap. "
|
||||
"Useful when the server decides the swap mode based on the result."
|
||||
),
|
||||
"example": (
|
||||
';; Client expects innerHTML swap\n'
|
||||
'(button :sx-get "/api/check"\n'
|
||||
' :sx-target "#panel" :sx-swap "innerHTML" ...)\n'
|
||||
'\n'
|
||||
';; Server overrides to append instead\n'
|
||||
';; SX-Reswap: beforeend\n'
|
||||
'(div :class "notification" "New item added")'
|
||||
),
|
||||
},
|
||||
"SX-Redirect": {
|
||||
"direction": "response",
|
||||
"description": (
|
||||
"Redirect the browser to a new URL using full page navigation. "
|
||||
"Unlike sx-push-url which does client-side history, this triggers "
|
||||
"a real browser navigation — useful after form submissions like login or checkout."
|
||||
),
|
||||
"example": (
|
||||
';; After successful login, redirect to dashboard\n'
|
||||
';; SX-Redirect: /dashboard\n'
|
||||
';;\n'
|
||||
';; Server handler:\n'
|
||||
'(when (valid-credentials? user pass)\n'
|
||||
' (set-header "SX-Redirect" "/dashboard")\n'
|
||||
' (span "Redirecting..."))'
|
||||
),
|
||||
},
|
||||
"SX-Refresh": {
|
||||
"direction": "response",
|
||||
"description": (
|
||||
"Set to \"true\" to reload the current page. "
|
||||
"A blunt tool — useful when server-side state has changed significantly "
|
||||
"and a partial update won't suffice."
|
||||
),
|
||||
"example": (
|
||||
';; After a major state change, force refresh\n'
|
||||
';; SX-Refresh: true\n'
|
||||
';;\n'
|
||||
';; Server handler:\n'
|
||||
'(when (deploy-complete?)\n'
|
||||
' (set-header "SX-Refresh" "true")\n'
|
||||
' (span "Deployed — refreshing..."))'
|
||||
),
|
||||
},
|
||||
"SX-Location": {
|
||||
"direction": "response",
|
||||
"description": (
|
||||
"Trigger client-side navigation: fetch the given URL, swap it into "
|
||||
"#main-panel, and push to browser history. Like clicking an sx-boosted link, "
|
||||
"but triggered from the server. Can be a URL string or JSON with options."
|
||||
),
|
||||
"example": (
|
||||
';; Simple: navigate to a page\n'
|
||||
';; SX-Location: /docs/introduction\n'
|
||||
';;\n'
|
||||
';; With options:\n'
|
||||
';; SX-Location: {"path": "/docs/intro", "target": "#sidebar", "swap": "innerHTML"}'
|
||||
),
|
||||
},
|
||||
"SX-Replace-Url": {
|
||||
"direction": "response",
|
||||
"description": (
|
||||
"Replace the current URL using history.replaceState without creating "
|
||||
"a new history entry. Useful for normalizing URLs after redirects, "
|
||||
"or updating the URL to reflect server-resolved state."
|
||||
),
|
||||
"example": (
|
||||
';; Normalize URL after slug resolution\n'
|
||||
';; SX-Replace-Url: /docs/introduction\n'
|
||||
';;\n'
|
||||
';; Server handler:\n'
|
||||
'(let ((canonical (resolve-slug slug)))\n'
|
||||
' (set-header "SX-Replace-Url" canonical)\n'
|
||||
' (~doc-content :slug canonical))'
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reference: Event detail pages
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
EVENT_DETAILS: dict[str, dict] = {
|
||||
"sx:beforeRequest": {
|
||||
"description": (
|
||||
"Fired on the triggering element before an sx request is issued. "
|
||||
"Call event.preventDefault() to cancel the request entirely. "
|
||||
"Useful for validation, confirmation, or conditional request logic."
|
||||
),
|
||||
"example": (
|
||||
';; Cancel request if form is empty\n'
|
||||
'(form :sx-post "/api/save"\n'
|
||||
' :sx-target "#result"\n'
|
||||
' :sx-on:sx:beforeRequest "if (!this.querySelector(\'input\').value) event.preventDefault()"\n'
|
||||
' (input :name "data" :placeholder "Required")\n'
|
||||
' (button :type "submit" "Save"))'
|
||||
),
|
||||
"demo": "ref-event-before-request-demo",
|
||||
},
|
||||
"sx:afterRequest": {
|
||||
"description": (
|
||||
"Fired on the triggering element after a successful sx response is received, "
|
||||
"before the swap happens. The response data is available on event.detail. "
|
||||
"Use this for logging, analytics, or pre-swap side effects."
|
||||
),
|
||||
"example": (
|
||||
';; Log successful requests\n'
|
||||
'(button :sx-get "/api/data"\n'
|
||||
' :sx-target "#result"\n'
|
||||
' :sx-on:sx:afterRequest "console.log(\'Response received\', event.detail)"\n'
|
||||
' "Load data")'
|
||||
),
|
||||
},
|
||||
"sx:afterSwap": {
|
||||
"description": (
|
||||
"Fired after the response content has been swapped into the DOM. "
|
||||
"The new content is in place but scripts may not have executed yet. "
|
||||
"Use this to initialize UI on newly inserted content."
|
||||
),
|
||||
"example": (
|
||||
';; Initialize tooltips on new content\n'
|
||||
'(div :sx-on:sx:afterSwap "initTooltips(this)"\n'
|
||||
' (button :sx-get "/api/items"\n'
|
||||
' :sx-target "#item-list"\n'
|
||||
' "Load items")\n'
|
||||
' (div :id "item-list"))'
|
||||
),
|
||||
},
|
||||
"sx:afterSettle": {
|
||||
"description": (
|
||||
"Fired after the DOM has fully settled — all scripts executed, transitions "
|
||||
"complete. This is the safest point to run code that depends on the final "
|
||||
"state of the DOM after a swap."
|
||||
),
|
||||
"example": (
|
||||
';; Scroll to new content after settle\n'
|
||||
'(div :sx-on:sx:afterSettle "document.getElementById(\'new-item\').scrollIntoView()"\n'
|
||||
' (button :sx-get "/api/append"\n'
|
||||
' :sx-target "#list" :sx-swap "beforeend"\n'
|
||||
' "Add item")\n'
|
||||
' (div :id "list"))'
|
||||
),
|
||||
"demo": "ref-event-after-settle-demo",
|
||||
},
|
||||
"sx:responseError": {
|
||||
"description": (
|
||||
"Fired when the server responds with an HTTP error (4xx or 5xx). "
|
||||
"event.detail contains the status code and response. "
|
||||
"Use this for error handling, showing notifications, or retry logic."
|
||||
),
|
||||
"example": (
|
||||
';; Show error notification\n'
|
||||
'(div :sx-on:sx:responseError "alert(\'Error: \' + event.detail.status)"\n'
|
||||
' (button :sx-get "/api/risky"\n'
|
||||
' :sx-target "#result"\n'
|
||||
' "Try it")\n'
|
||||
' (div :id "result"))'
|
||||
),
|
||||
"demo": "ref-event-response-error-demo",
|
||||
},
|
||||
"sx:sendError": {
|
||||
"description": (
|
||||
"Fired when the request fails to send — typically a network error, "
|
||||
"DNS failure, or CORS issue. Unlike sx:responseError, no HTTP response "
|
||||
"was received at all."
|
||||
),
|
||||
"example": (
|
||||
';; Handle network failures\n'
|
||||
'(div :sx-on:sx:sendError "this.querySelector(\'.status\').textContent = \'Offline\'"\n'
|
||||
' (button :sx-get "/api/data"\n'
|
||||
' :sx-target "#result"\n'
|
||||
' "Load")\n'
|
||||
' (span :class "status")\n'
|
||||
' (div :id "result"))'
|
||||
),
|
||||
},
|
||||
"sx:validationFailed": {
|
||||
"description": (
|
||||
"Fired when sx-validate is set and the form fails HTML5 validation. "
|
||||
"The request is not sent. Use this to show custom validation UI "
|
||||
"or highlight invalid fields."
|
||||
),
|
||||
"example": (
|
||||
';; Highlight invalid fields\n'
|
||||
'(form :sx-post "/api/save"\n'
|
||||
' :sx-validate "true"\n'
|
||||
' :sx-on:sx:validationFailed "this.classList.add(\'shake\')"\n'
|
||||
' (input :type "email" :required "true" :name "email"\n'
|
||||
' :placeholder "Email (required)")\n'
|
||||
' (button :type "submit" "Save"))'
|
||||
),
|
||||
"demo": "ref-event-validation-failed-demo",
|
||||
},
|
||||
"sx:sseOpen": {
|
||||
"description": (
|
||||
"Fired when a Server-Sent Events connection is successfully established. "
|
||||
"Use this to update connection status indicators."
|
||||
),
|
||||
"example": (
|
||||
';; Show connected status\n'
|
||||
'(div :sx-sse "/api/stream"\n'
|
||||
' :sx-on:sx:sseOpen "this.querySelector(\'.status\').textContent = \'Connected\'"\n'
|
||||
' (span :class "status" "Connecting...")\n'
|
||||
' (div :id "messages"))'
|
||||
),
|
||||
},
|
||||
"sx:sseMessage": {
|
||||
"description": (
|
||||
"Fired when an SSE message is received and swapped into the DOM. "
|
||||
"event.detail contains the message data. Fires for each individual message."
|
||||
),
|
||||
"example": (
|
||||
';; Count received messages\n'
|
||||
'(div :sx-sse "/api/stream"\n'
|
||||
' :sx-sse-swap "update"\n'
|
||||
' :sx-on:sx:sseMessage "this.dataset.count = (parseInt(this.dataset.count||0)+1); this.querySelector(\'.count\').textContent = this.dataset.count"\n'
|
||||
' (span :class "count" "0") " messages received"\n'
|
||||
' (div :id "stream-content"))'
|
||||
),
|
||||
},
|
||||
"sx:sseError": {
|
||||
"description": (
|
||||
"Fired when an SSE connection encounters an error or is closed unexpectedly. "
|
||||
"Use this to show reconnection status or fall back to polling."
|
||||
),
|
||||
"example": (
|
||||
';; Show disconnected status\n'
|
||||
'(div :sx-sse "/api/stream"\n'
|
||||
' :sx-on:sx:sseError "this.querySelector(\'.status\').textContent = \'Disconnected\'"\n'
|
||||
' (span :class "status" "Connecting...")\n'
|
||||
' (div :id "messages"))'
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reference: Attribute detail pages
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -34,15 +34,27 @@
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
|
||||
(tbody rows)))))
|
||||
|
||||
(defcomp ~doc-headers-row (&key name value description)
|
||||
(defcomp ~doc-headers-row (&key name value description href)
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" name)
|
||||
(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" name)
|
||||
(span :class "text-violet-700" name)))
|
||||
(td :class "px-3 py-2 font-mono text-sm text-stone-500" value)
|
||||
(td :class "px-3 py-2 text-stone-700 text-sm" description)))
|
||||
|
||||
(defcomp ~doc-two-col-row (&key name description)
|
||||
(defcomp ~doc-two-col-row (&key name description href)
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" name)
|
||||
(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" name)
|
||||
(span :class "text-violet-700" name)))
|
||||
(td :class "px-3 py-2 text-stone-700 text-sm" description)))
|
||||
|
||||
(defcomp ~doc-two-col-table (&key title intro col1 col2 rows)
|
||||
@@ -88,7 +100,8 @@
|
||||
(~doc-headers-row
|
||||
:name (get h "name")
|
||||
:value (get h "value")
|
||||
:description (get h "desc")))
|
||||
:description (get h "desc")
|
||||
:href (get h "href")))
|
||||
headers))))
|
||||
|
||||
;; Build two-col table from a list of {name, desc} dicts.
|
||||
@@ -98,7 +111,8 @@
|
||||
:rows (<> (map (fn (item)
|
||||
(~doc-two-col-row
|
||||
:name (get item "name")
|
||||
:description (get item "desc")))
|
||||
:description (get item "desc")
|
||||
:href (get item "href")))
|
||||
items))))
|
||||
|
||||
;; Build all primitives category tables from a {category: [prim, ...]} dict.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -71,8 +71,8 @@
|
||||
:summary "How SX implements proper tail calls via trampolining in a language that doesn't have them.")
|
||||
(dict :label "Continuations" :href "/essays/continuations"
|
||||
:summary "First-class continuations in a tree-walking evaluator — theory and implementation.")
|
||||
(dict :label "Godel, Escher, Bach" :href "/essays/godel-escher-bach"
|
||||
:summary "Self-reference, strange loops, and what a self-hosting language has to do with GEB.")
|
||||
(dict :label "Strange Loops" :href "/essays/godel-escher-bach"
|
||||
:summary "Self-reference, and the tangled hierarchy of a language that defines itself.")
|
||||
(dict :label "The Reflexive Web" :href "/essays/reflexive-web"
|
||||
:summary "A web where pages can inspect, modify, and extend their own rendering pipeline.")
|
||||
(dict :label "sx sucks" :href "/essays/sx-sucks"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;; Reference page layouts — receive data from Python primitives
|
||||
;; @css bg-blue-100 text-blue-700 bg-emerald-100 text-emerald-700 bg-amber-100 text-amber-700
|
||||
|
||||
(defcomp ~reference-attrs-content (&key req-table beh-table uniq-table)
|
||||
(~doc-page :title "Attribute Reference"
|
||||
@@ -45,6 +46,40 @@
|
||||
"Trigger the demo to see the raw response the server sends.")
|
||||
(~doc-placeholder :id wire-placeholder-id)))))
|
||||
|
||||
(defcomp ~reference-header-detail-content (&key title direction description
|
||||
example-code demo)
|
||||
(~doc-page :title title
|
||||
(let ((badge-class (if (= direction "request")
|
||||
"bg-blue-100 text-blue-700"
|
||||
(if (= direction "response")
|
||||
"bg-emerald-100 text-emerald-700"
|
||||
"bg-amber-100 text-amber-700")))
|
||||
(badge-label (if (= direction "request") "Request Header"
|
||||
(if (= direction "response") "Response Header"
|
||||
"Request & Response"))))
|
||||
(div :class "flex items-center gap-3 mb-4"
|
||||
(span :class (str "text-xs font-medium px-2 py-1 rounded " badge-class)
|
||||
badge-label)))
|
||||
(p :class "text-stone-600 mb-6" description)
|
||||
(when demo
|
||||
(~example-card :title "Demo"
|
||||
(~example-demo demo)))
|
||||
(when example-code
|
||||
(<>
|
||||
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Example usage")
|
||||
(~example-source :code (highlight example-code "lisp"))))))
|
||||
|
||||
(defcomp ~reference-event-detail-content (&key title description example-code demo)
|
||||
(~doc-page :title title
|
||||
(p :class "text-stone-600 mb-6" description)
|
||||
(when demo
|
||||
(~example-card :title "Demo"
|
||||
(~example-demo demo)))
|
||||
(when example-code
|
||||
(<>
|
||||
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Example usage")
|
||||
(~example-source :code (highlight example-code "lisp"))))))
|
||||
|
||||
(defcomp ~reference-attr-not-found (&key slug)
|
||||
(~doc-page :title "Not Found"
|
||||
(p :class "text-stone-600"
|
||||
|
||||
@@ -293,8 +293,8 @@ boot.sx depends on: cssx, orchestration, adapter-dom, render")))
|
||||
" spec files, runs the Python bootstrapper, and displays both the compiler source and its generated JavaScript output. "
|
||||
"The generated code below is live — it was produced by the bootstrapper at page load time, not served from a static file.")
|
||||
(p :class "text-xs text-stone-400 italic"
|
||||
"The bootstrapped JavaScript is also injected as a script tag on this page. "
|
||||
"Open the browser console to verify it loaded."))
|
||||
"The sx-browser.js powering this page IS the bootstrapped output. "
|
||||
"This page re-runs the bootstrapper to display the source and result."))
|
||||
|
||||
(div :class "space-y-3"
|
||||
(div :class "flex items-baseline gap-3"
|
||||
@@ -318,8 +318,7 @@ boot.sx depends on: cssx, orchestration, adapter-dom, render")))
|
||||
"It is a complete, self-contained SX runtime — parser, evaluator, DOM adapter, engine, and CSS system.")
|
||||
(div :class "bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-violet-300"
|
||||
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
|
||||
(code (highlight bootstrapped-output "javascript"))))
|
||||
(script :type "text/javascript" bootstrapped-output)))))
|
||||
(code (highlight bootstrapped-output "javascript"))))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Not found
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(p :class "text-2xl text-stone-600 mb-4"
|
||||
"s-expressions for the web")
|
||||
(p :class "text-sm text-stone-400"
|
||||
"\u00a9 Giles Bradshaw 2026")
|
||||
"© Giles Bradshaw 2026")
|
||||
(p :class "text-lg text-stone-500 max-w-2xl mx-auto mb-12"
|
||||
"A hypermedia-driven UI engine that combines htmx's server-first philosophy "
|
||||
"with React's component model. S-expressions over the wire — no HTML, no JavaScript frameworks.")
|
||||
|
||||
@@ -115,6 +115,43 @@
|
||||
:handler-code attr-handler
|
||||
:wire-placeholder-id attr-wire-id)))
|
||||
|
||||
(defpage reference-header-detail
|
||||
:path "/reference/headers/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Reference"
|
||||
:sub-label "Reference"
|
||||
:sub-href "/reference/"
|
||||
:sub-nav (~section-nav :items reference-nav-items :current "Headers")
|
||||
:selected "Headers")
|
||||
:data (header-detail-data slug)
|
||||
:content (if header-not-found
|
||||
(~reference-attr-not-found :slug slug)
|
||||
(~reference-header-detail-content
|
||||
:title header-title
|
||||
:direction header-direction
|
||||
:description header-description
|
||||
:example-code header-example
|
||||
:demo header-demo)))
|
||||
|
||||
(defpage reference-event-detail
|
||||
:path "/reference/events/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Reference"
|
||||
:sub-label "Reference"
|
||||
:sub-href "/reference/"
|
||||
:sub-nav (~section-nav :items reference-nav-items :current "Events")
|
||||
:selected "Events")
|
||||
:data (event-detail-data slug)
|
||||
:content (if event-not-found
|
||||
(~reference-attr-not-found :slug slug)
|
||||
(~reference-event-detail-content
|
||||
:title event-title
|
||||
:description event-description
|
||||
:example-code event-example
|
||||
:demo event-demo)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Protocols section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -16,6 +16,8 @@ def _register_sx_helpers() -> None:
|
||||
"primitives-data": _primitives_data,
|
||||
"reference-data": _reference_data,
|
||||
"attr-detail-data": _attr_detail_data,
|
||||
"header-detail-data": _header_detail_data,
|
||||
"event-detail-data": _event_detail_data,
|
||||
"read-spec-file": _read_spec_file,
|
||||
"bootstrapper-data": _bootstrapper_data,
|
||||
})
|
||||
@@ -39,7 +41,7 @@ def _reference_data(slug: str) -> dict:
|
||||
from content.pages import (
|
||||
REQUEST_ATTRS, BEHAVIOR_ATTRS, SX_UNIQUE_ATTRS,
|
||||
REQUEST_HEADERS, RESPONSE_HEADERS,
|
||||
EVENTS, JS_API, ATTR_DETAILS,
|
||||
EVENTS, JS_API, ATTR_DETAILS, HEADER_DETAILS,
|
||||
)
|
||||
|
||||
if slug == "attributes":
|
||||
@@ -63,18 +65,22 @@ def _reference_data(slug: str) -> dict:
|
||||
elif slug == "headers":
|
||||
return {
|
||||
"req-headers": [
|
||||
{"name": n, "value": v, "desc": d}
|
||||
{"name": n, "value": v, "desc": d,
|
||||
"href": f"/reference/headers/{n}" if n in HEADER_DETAILS else None}
|
||||
for n, v, d in REQUEST_HEADERS
|
||||
],
|
||||
"resp-headers": [
|
||||
{"name": n, "value": v, "desc": d}
|
||||
{"name": n, "value": v, "desc": d,
|
||||
"href": f"/reference/headers/{n}" if n in HEADER_DETAILS else None}
|
||||
for n, v, d in RESPONSE_HEADERS
|
||||
],
|
||||
}
|
||||
elif slug == "events":
|
||||
from content.pages import EVENT_DETAILS
|
||||
return {
|
||||
"events-list": [
|
||||
{"name": n, "desc": d}
|
||||
{"name": n, "desc": d,
|
||||
"href": f"/reference/events/{n}" if n in EVENT_DETAILS else None}
|
||||
for n, d in EVENTS
|
||||
],
|
||||
}
|
||||
@@ -190,3 +196,42 @@ def _attr_detail_data(slug: str) -> dict:
|
||||
"attr-demo": SxExpr(f"(~{demo_name})") if demo_name else None,
|
||||
"attr-wire-id": wire_id,
|
||||
}
|
||||
|
||||
|
||||
def _header_detail_data(slug: str) -> dict:
|
||||
"""Return header detail data for a specific header slug."""
|
||||
from content.pages import HEADER_DETAILS
|
||||
from shared.sx.helpers import SxExpr
|
||||
|
||||
detail = HEADER_DETAILS.get(slug)
|
||||
if not detail:
|
||||
return {"header-not-found": True}
|
||||
|
||||
demo_name = detail.get("demo")
|
||||
return {
|
||||
"header-not-found": None,
|
||||
"header-title": slug,
|
||||
"header-direction": detail["direction"],
|
||||
"header-description": detail["description"],
|
||||
"header-example": detail.get("example"),
|
||||
"header-demo": SxExpr(f"(~{demo_name})") if demo_name else None,
|
||||
}
|
||||
|
||||
|
||||
def _event_detail_data(slug: str) -> dict:
|
||||
"""Return event detail data for a specific event slug."""
|
||||
from content.pages import EVENT_DETAILS
|
||||
from shared.sx.helpers import SxExpr
|
||||
|
||||
detail = EVENT_DETAILS.get(slug)
|
||||
if not detail:
|
||||
return {"event-not-found": True}
|
||||
|
||||
demo_name = detail.get("demo")
|
||||
return {
|
||||
"event-not-found": None,
|
||||
"event-title": slug,
|
||||
"event-description": detail["description"],
|
||||
"event-example": detail.get("example"),
|
||||
"event-demo": SxExpr(f"(~{demo_name})") if demo_name else None,
|
||||
}
|
||||
|
||||
@@ -643,3 +643,151 @@
|
||||
"Connecting to SSE stream..."))
|
||||
(p :class "text-xs text-stone-400"
|
||||
"Server pushes time updates every 2 seconds via Server-Sent Events.")))
|
||||
|
||||
;; ===========================================================================
|
||||
;; Header detail demos
|
||||
;; ===========================================================================
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; SX-Prompt header demo
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-header-prompt-demo ()
|
||||
(div :class "space-y-3"
|
||||
(button
|
||||
:sx-get "/reference/api/prompt-echo"
|
||||
:sx-target "#ref-hdr-prompt-result"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-prompt "Enter your name:"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Prompt & send")
|
||||
(div :id "ref-hdr-prompt-result"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Click to enter a name via prompt — the value is sent as the SX-Prompt header.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; SX-Trigger response header demo
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-header-trigger-demo ()
|
||||
(div :class "space-y-3"
|
||||
(button
|
||||
:sx-get "/reference/api/trigger-event"
|
||||
:sx-target "#ref-hdr-trigger-result"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Load with trigger")
|
||||
(div :id "ref-hdr-trigger-result"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
:sx-on:showNotice "this.style.borderColor = '#8b5cf6'; this.style.borderWidth = '2px'"
|
||||
"Click — the server response includes SX-Trigger: showNotice, which highlights this box.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; SX-Retarget response header demo
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-header-retarget-demo ()
|
||||
(div :class "space-y-3"
|
||||
(button
|
||||
:sx-get "/reference/api/retarget"
|
||||
:sx-target "#ref-hdr-retarget-main"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Load (server retargets)")
|
||||
(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" "Original target")
|
||||
(div :id "ref-hdr-retarget-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" "Retarget destination")
|
||||
(div :id "ref-hdr-retarget-alt" :class "text-sm text-stone-500" "Waiting...")))))
|
||||
|
||||
;; ===========================================================================
|
||||
;; Event detail demos
|
||||
;; ===========================================================================
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx:beforeRequest event demo
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-event-before-request-demo ()
|
||||
(div :class "space-y-3"
|
||||
(div :class "flex gap-2 items-center"
|
||||
(input :id "ref-evt-br-input" :type "text" :placeholder "Type something first..."
|
||||
: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
|
||||
:sx-get "/reference/api/time"
|
||||
:sx-target "#ref-evt-br-result"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-on:sx:beforeRequest "if (!document.getElementById('ref-evt-br-input').value) { event.preventDefault(); document.getElementById('ref-evt-br-result').textContent = 'Cancelled — input is empty!'; }"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Load"))
|
||||
(div :id "ref-evt-br-result"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Request is cancelled via preventDefault() if the input is empty.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx:afterSettle event demo
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-event-after-settle-demo ()
|
||||
(div :class "space-y-3"
|
||||
(button
|
||||
:sx-get "/reference/api/swap-item"
|
||||
:sx-target "#ref-evt-settle-list"
|
||||
:sx-swap "beforeend"
|
||||
:sx-on:sx:afterSettle "var items = document.querySelectorAll('#ref-evt-settle-list > div'); if (items.length) items[items.length-1].scrollIntoView({behavior:'smooth'})"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Add item (scrolls after settle)")
|
||||
(div :id "ref-evt-settle-list"
|
||||
:class "p-3 rounded border border-stone-200 space-y-1 max-h-32 overflow-y-auto"
|
||||
(div :class "text-sm text-stone-500" "Items will be appended and scrolled into view."))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx:responseError event demo
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~ref-event-response-error-demo ()
|
||||
(div :class "space-y-3"
|
||||
(button
|
||||
:sx-get "/reference/api/error-500"
|
||||
:sx-target "#ref-evt-err-result"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-on:sx:responseError "var s=document.getElementById('ref-evt-err-status'); s.style.display='block'; s.textContent='Error ' + (event.detail ? event.detail.status || '?' : '?') + ' received'"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Call failing endpoint")
|
||||
(div :id "ref-evt-err-status"
|
||||
:class "p-2 rounded bg-red-50 text-red-600 text-sm"
|
||||
:style "display: none"
|
||||
"")
|
||||
(div :id "ref-evt-err-result"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Click to trigger an error — the sx:responseError event fires.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx:validationFailed event demo
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; @css invalid:border-red-400
|
||||
|
||||
(defcomp ~ref-event-validation-failed-demo ()
|
||||
(div :class "space-y-3"
|
||||
(form
|
||||
:sx-post "/reference/api/greet"
|
||||
:sx-target "#ref-evt-vf-result"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-validate "true"
|
||||
:sx-on:sx:validationFailed "document.getElementById('ref-evt-vf-status').style.display = 'block'"
|
||||
:class "flex gap-2"
|
||||
(input :type "email" :name "email" :required "true"
|
||||
:placeholder "Email (required)"
|
||||
:class "flex-1 px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500 invalid:border-red-400")
|
||||
(button :type "submit"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Submit"))
|
||||
(div :id "ref-evt-vf-status"
|
||||
:class "p-2 rounded bg-amber-50 text-amber-700 text-sm"
|
||||
:style "display: none"
|
||||
"Validation failed — form was not submitted.")
|
||||
(div :id "ref-evt-vf-result"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Submit with empty/invalid email to trigger the event.")))
|
||||
|
||||
Reference in New Issue
Block a user