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