SPA navigation, page component refactors, WASM rebuild

Refactor page components (docs, examples, specs, reference, layouts)
and adapters (adapter-sx, boot-helpers, orchestration) across sx/ and
web/ directories. Add Playwright SPA navigation tests. Rebuild WASM
kernel with updated bytecode. Add OCaml primitives for request handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 11:00:51 +00:00
parent 9f097026f8
commit 584445a843
38 changed files with 5854 additions and 3695 deletions

View File

@@ -1,162 +1,274 @@
;; SX docs utility components
(defcomp ~docs/placeholder (&key (id :as string))
(div :id id
(div :class "bg-stone-100 rounded p-4 mt-3"
(p :class "text-stone-400 italic text-sm"
(defcomp
~docs/placeholder
(&key (id :as string))
(div
:id id
(div
:class "bg-stone-100 rounded p-4 mt-3"
(p
:class "text-stone-400 italic text-sm"
"Trigger the demo to see the actual content."))))
(defcomp ~docs/oob-code (&key (target-id :as string) (text :as string))
(div :id target-id :sx-swap-oob "innerHTML"
(div :class "not-prose bg-stone-100 rounded p-4 mt-3"
(pre :class "text-sm whitespace-pre-wrap break-words"
(code text)))))
(defcomp
~docs/oob-code
(&key (target-id :as string) (text :as string))
(div
:id target-id
:sx-swap-oob "innerHTML"
(div
:class "not-prose bg-stone-100 rounded p-4 mt-3"
(pre :class "text-sm whitespace-pre-wrap break-words" (code text)))))
(defcomp ~docs/attr-table (&key (title :as string) rows)
(div :class "space-y-3"
(defcomp
~docs/attr-table
(&key (title :as string) rows)
(div
:class "space-y-3"
(h3 :class "text-xl font-semibold text-stone-700" title)
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Attribute")
(th :class "px-3 py-2 font-medium text-stone-600" "Description")
(th :class "px-3 py-2 font-medium text-stone-600 text-center w-20" "In sx?")))
(div
:class "overflow-x-auto rounded border border-stone-200"
(table
:class "w-full text-left text-sm"
(thead
(tr
:class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Attribute")
(th :class "px-3 py-2 font-medium text-stone-600" "Description")
(th
:class "px-3 py-2 font-medium text-stone-600 text-center w-20"
"In sx?")))
(tbody rows)))))
(defcomp ~docs/headers-table (&key (title :as string) rows)
(div :class "space-y-3"
(defcomp
~docs/headers-table
(&key (title :as string) rows)
(div
:class "space-y-3"
(h3 :class "text-xl font-semibold text-stone-700" title)
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Header")
(th :class "px-3 py-2 font-medium text-stone-600" "Value")
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
(div
:class "overflow-x-auto rounded border border-stone-200"
(table
:class "w-full text-left text-sm"
(thead
(tr
:class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Header")
(th :class "px-3 py-2 font-medium text-stone-600" "Value")
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
(tbody rows)))))
(defcomp ~docs/headers-row (&key (name :as string) (value :as string) (description :as string) (href :as string?))
(tr :class "border-b border-stone-100"
(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)
(defcomp
~docs/headers-row
(&key
(name :as string)
(value :as string)
(description :as string)
(href :as string?))
(tr
:class "border-b border-stone-100"
(td
:class "px-3 py-2 font-mono text-sm whitespace-nowrap"
(if
href
(a
:href href
:sx-get href
:sx-target "#sx-content"
:sx-select "#sx-content"
: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 ~docs/two-col-row (&key (name :as string) (description :as string) (href :as string?))
(tr :class "border-b border-stone-100"
(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)
(defcomp
~docs/two-col-row
(&key (name :as string) (description :as string) (href :as string?))
(tr
:class "border-b border-stone-100"
(td
:class "px-3 py-2 font-mono text-sm whitespace-nowrap"
(if
href
(a
:href href
:sx-get href
:sx-target "#sx-content"
:sx-select "#sx-content"
: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 ~docs/two-col-table (&key (title :as string?) (intro :as string?) (col1 :as string?) (col2 :as string?) rows)
(div :class "space-y-3"
(defcomp
~docs/two-col-table
(&key
(title :as string?)
(intro :as string?)
(col1 :as string?)
(col2 :as string?)
rows)
(div
:class "space-y-3"
(when title (h3 :class "text-xl font-semibold text-stone-700" title))
(when intro (p :class "text-stone-600 mb-6" intro))
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" (or col1 "Name"))
(th :class "px-3 py-2 font-medium text-stone-600" (or col2 "Description"))))
(div
:class "overflow-x-auto rounded border border-stone-200"
(table
:class "w-full text-left text-sm"
(thead
(tr
:class "border-b border-stone-200 bg-stone-100"
(th
:class "px-3 py-2 font-medium text-stone-600"
(or col1 "Name"))
(th
:class "px-3 py-2 font-medium text-stone-600"
(or col2 "Description"))))
(tbody rows)))))
(defcomp ~docs/label ()
(span :class "font-mono" "(<sx>)"))
(defcomp ~docs/label () (span :class "font-mono" "(<sx>)"))
(defcomp ~docs/clear-cache-btn ()
(button :onclick "localStorage.removeItem('sx-components-hash');localStorage.removeItem('sx-components-src');var e=Sx.getEnv();Object.keys(e).forEach(function(k){if(k.charAt(0)==='~')delete e[k]});var b=this;b.textContent='Cleared!';setTimeout(function(){b.textContent='Clear component cache'},2000)"
(defcomp
~docs/clear-cache-btn
()
(button
:onclick "localStorage.removeItem('sx-components-hash');localStorage.removeItem('sx-components-src');var e=Sx.getEnv();Object.keys(e).forEach(function(k){if(k.charAt(0)==='~')delete e[k]});var b=this;b.textContent='Cleared!';setTimeout(function(){b.textContent='Clear component cache'},2000)"
:class "text-xs text-stone-400 hover:text-stone-600 border border-stone-200 rounded px-2 py-1 transition-colors"
"Clear component cache"))
;; ---------------------------------------------------------------------------
;; Data-driven table builders — replace Python sx_call() composition
;; ---------------------------------------------------------------------------
(defcomp
~docs/attr-table-from-data
(&key (title :as string) (attrs :as list))
(~docs/attr-table
:title title
:rows (<>
(map
(fn
(a)
(~docs/attr-row
:attr (get a "name")
:description (get a "desc")
:exists (get a "exists")
:href (get a "href")))
attrs))))
;; Build attr table from a list of {name, desc, exists, href} dicts.
;; Replaces _attr_table_sx() in utils.py.
(defcomp ~docs/attr-table-from-data (&key (title :as string) (attrs :as list))
(~docs/attr-table :title title
:rows (<> (map (fn (a)
(~docs/attr-row
:attr (get a "name")
:description (get a "desc")
:exists (get a "exists")
:href (get a "href")))
attrs))))
(defcomp
~docs/headers-table-from-data
(&key (title :as string) (headers :as list))
(~docs/headers-table
:title title
:rows (<>
(map
(fn
(h)
(~docs/headers-row
:name (get h "name")
:value (get h "value")
:description (get h "desc")
:href (get h "href")))
headers))))
;; Build headers table from a list of {name, value, desc} dicts.
;; Replaces _headers_table_sx() in utils.py.
(defcomp ~docs/headers-table-from-data (&key (title :as string) (headers :as list))
(~docs/headers-table :title title
:rows (<> (map (fn (h)
(~docs/headers-row
:name (get h "name")
:value (get h "value")
:description (get h "desc")
:href (get h "href")))
headers))))
(defcomp
~docs/two-col-table-from-data
(&key
(title :as string?)
(intro :as string?)
(col1 :as string?)
(col2 :as string?)
(items :as list))
(~docs/two-col-table
:title title
:intro intro
:col1 col1
:col2 col2
:rows (<>
(map
(fn
(item)
(~docs/two-col-row
:name (get item "name")
:description (get item "desc")
:href (get item "href")))
items))))
;; Build two-col table from a list of {name, desc} dicts.
;; Replaces the _reference_events_sx / _reference_js_api_sx builders.
(defcomp ~docs/two-col-table-from-data (&key (title :as string?) (intro :as string?) (col1 :as string?) (col2 :as string?) (items :as list))
(~docs/two-col-table :title title :intro intro :col1 col1 :col2 col2
:rows (<> (map (fn (item)
(~docs/two-col-row
:name (get item "name")
:description (get item "desc")
:href (get item "href")))
items))))
(defcomp
~docs/primitives-tables
(&key (primitives :as dict))
(<>
(map
(fn
(cat)
(~docs/primitives-table
:category cat
:primitives (get primitives cat)))
(keys primitives))))
;; Build all primitives category tables from a {category: [prim, ...]} dict.
;; Replaces _primitives_section_sx() in utils.py.
(defcomp ~docs/primitives-tables (&key (primitives :as dict))
(<> (map (fn (cat)
(~docs/primitives-table
:category cat
:primitives (get primitives cat)))
(keys primitives))))
(defcomp
~docs/special-forms-tables
(&key (forms :as dict))
(<>
(map
(fn
(cat)
(~docs/special-forms-category :category cat :forms (get forms cat)))
(keys forms))))
;; Build all special form category sections from a {category: [form, ...]} dict.
(defcomp ~docs/special-forms-tables (&key (forms :as dict))
(<> (map (fn (cat)
(~docs/special-forms-category
:category cat
:forms (get forms cat)))
(keys forms))))
(defcomp ~docs/special-forms-category (&key (category :as string) (forms :as list))
(div :class "space-y-4"
(h3 :class "text-xl font-semibold text-stone-800 border-b border-stone-200 pb-2" category)
(div :class "space-y-4"
(map (fn (f)
(~docs/special-form-card
:name (get f "name")
:syntax (get f "syntax")
:doc (get f "doc")
:tail-position (get f "tail-position")
:example (get f "example")))
(defcomp
~docs/special-forms-category
(&key (category :as string) (forms :as list))
(div
:class "space-y-4"
(h3
:class "text-xl font-semibold text-stone-800 border-b border-stone-200 pb-2"
category)
(div
:class "space-y-4"
(map
(fn
(f)
(~docs/special-form-card
:name (get f "name")
:syntax (get f "syntax")
:doc (get f "doc")
:tail-position (get f "tail-position")
:example (get f "example")))
forms))))
(defcomp ~docs/special-form-card (&key (name :as string) (syntax :as string) (doc :as string) (tail-position :as string) (example :as string))
(div :class "not-prose border border-stone-200 rounded-lg p-4 space-y-3"
(div :class "flex items-baseline gap-3"
(defcomp
~docs/special-form-card
(&key
(name :as string)
(syntax :as string)
(doc :as string)
(tail-position :as string)
(example :as string))
(div
:class "not-prose border border-stone-200 rounded-lg p-4 space-y-3"
(div
:class "flex items-baseline gap-3"
(code :class "text-lg font-bold text-violet-700" name)
(when (not (= tail-position "none"))
(span :class "text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-700" "TCO")))
(when (not (= syntax ""))
(pre :class "bg-stone-100 rounded px-3 py-2 text-sm font-mono text-stone-700 overflow-x-auto"
(when
(not (= tail-position "none"))
(span
:class "text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-700"
"TCO")))
(when
(not (= syntax ""))
(pre
:class "bg-stone-100 rounded px-3 py-2 text-sm font-mono text-stone-700 overflow-x-auto"
syntax))
(p :class "text-stone-600 text-sm whitespace-pre-line" doc)
(when (not (= tail-position ""))
(p :class "text-xs text-stone-500"
(span :class "font-semibold" "Tail position: ") tail-position))
(when (not (= example ""))
(when
(not (= tail-position ""))
(p
:class "text-xs text-stone-500"
(span :class "font-semibold" "Tail position: ")
tail-position))
(when
(not (= example ""))
(~docs/code :src (highlight example "lisp")))))

View File

@@ -1,15 +1,28 @@
(defcomp ~essays/index/essays-index-content ()
(~docs/page :title "Essays"
(div :class "space-y-4"
(p :class "text-lg text-stone-600 mb-4"
(defcomp
~essays/index/essays-index-content
()
(~docs/page
:title "Essays"
(div
:class "space-y-4"
(p
:class "text-lg text-stone-600 mb-4"
"Opinions, rationales, and explorations around SX and the ideas behind it.")
(div :class "space-y-3"
(map (fn (item)
(a :href (get item "href")
:sx-get (get item "href") :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
:class "block rounded border border-stone-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(div :class "font-semibold text-stone-800" (get item "label"))
(when (get item "summary")
(p :class "text-sm text-stone-500 mt-1" (get item "summary")))))
(div
:class "space-y-3"
(map
(fn
(item)
(a
:href (get item "href")
:sx-get (get item "href")
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "block rounded border border-stone-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(div :class "font-semibold text-stone-800" (get item "label"))
(when
(get item "summary")
(p :class "text-sm text-stone-500 mt-1" (get item "summary")))))
essays-nav-items)))))

View File

@@ -1,21 +1,28 @@
;; Essay content — static content extracted from essays.py
;; ---------------------------------------------------------------------------
;; Philosophy section content
;; ---------------------------------------------------------------------------
(defcomp ~essays/philosophy-index/content ()
(~docs/page :title "Philosophy"
(div :class "space-y-4"
(p :class "text-lg text-stone-600 mb-4"
(defcomp
~essays/philosophy-index/content
()
(~docs/page
:title "Philosophy"
(div
:class "space-y-4"
(p
:class "text-lg text-stone-600 mb-4"
"The deeper ideas behind SX — manifestos, self-reference, and the philosophical traditions that shaped the language.")
(div :class "space-y-3"
(map (fn (item)
(a :href (get item "href")
:sx-get (get item "href") :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
:class "block rounded border border-stone-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(div :class "font-semibold text-stone-800" (get item "label"))
(when (get item "summary")
(p :class "text-sm text-stone-500 mt-1" (get item "summary")))))
(div
:class "space-y-3"
(map
(fn
(item)
(a
:href (get item "href")
:sx-get (get item "href")
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "block rounded border border-stone-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(div :class "font-semibold text-stone-800" (get item "label"))
(when
(get item "summary")
(p :class "text-sm text-stone-500 mt-1" (get item "summary")))))
philosophy-nav-items)))))

View File

@@ -1,58 +1,103 @@
;; Example page template and reference index
;; Template receives data values (code strings, titles), calls highlight internally.
(defcomp ~examples/page-content (&key (title :as string) (description :as string) (demo-description :as string?) demo
(sx-code :as string) (sx-lang :as string?) (handler-code :as string) (handler-lang :as string?)
(comp-placeholder-id :as string?) (wire-placeholder-id :as string?) (wire-note :as string?)
(comp-heading :as string?) (handler-heading :as string?))
(~docs/page :title title
(defcomp
~examples/page-content
(&key
(title :as string)
(description :as string)
(demo-description :as string?)
demo
(sx-code :as string)
(sx-lang :as string?)
(handler-code :as string)
(handler-lang :as string?)
(comp-placeholder-id :as string?)
(wire-placeholder-id :as string?)
(wire-note :as string?)
(comp-heading :as string?)
(handler-heading :as string?))
(~docs/page
:title title
(p :class "text-stone-600 mb-6" description)
(~examples/card :title "Demo" :description demo-description
(~examples/card
:title "Demo"
:description demo-description
(~examples/demo demo))
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")
(~examples/source :code (highlight sx-code (if sx-lang sx-lang "lisp")))
(when comp-placeholder-id
(~examples/source
:src-code (highlight sx-code (if sx-lang sx-lang "lisp")))
(when
comp-placeholder-id
(<>
(h3 :class "text-lg font-semibold text-stone-700 mt-6"
(h3
:class "text-lg font-semibold text-stone-700 mt-6"
(if comp-heading comp-heading "Component"))
(~docs/placeholder :id comp-placeholder-id)))
(h3 :class "text-lg font-semibold text-stone-700 mt-6"
(h3
:class "text-lg font-semibold text-stone-700 mt-6"
(if handler-heading handler-heading "Server handler"))
(~examples/source :code (highlight handler-code (if handler-lang handler-lang "python")))
(div :class "flex items-center justify-between mt-6"
(~examples/source
:src-code (highlight handler-code (if handler-lang handler-lang "python")))
(div
:class "flex items-center justify-between mt-6"
(h3 :class "text-lg font-semibold text-stone-700" "Wire response")
(~docs/clear-cache-btn))
(when wire-note
(p :class "text-stone-500 text-sm mb-2" wire-note))
(when wire-placeholder-id
(~docs/placeholder :id wire-placeholder-id))))
(when wire-note (p :class "text-stone-500 text-sm mb-2" wire-note))
(when wire-placeholder-id (~docs/placeholder :id wire-placeholder-id))))
(defcomp ~examples/reference-index-content ()
(~docs/page :title "Reference"
(p :class "text-stone-600 mb-6"
(defcomp
~examples/reference-index-content
()
(~docs/page
:title "Reference"
(p
:class "text-stone-600 mb-6"
"Complete reference for the sx client library.")
(div :class "grid gap-4 sm:grid-cols-2"
(a :href "/sx/(geography.(hypermedia.(reference.attributes)))"
:sx-get "/sx/(geography.(hypermedia.(reference.attributes)))" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
(div
:class "grid gap-4 sm:grid-cols-2"
(a
:href "/sx/(geography.(hypermedia.(reference.attributes)))"
:sx-get "/sx/(geography.(hypermedia.(reference.attributes)))"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "block p-5 rounded-lg border border-stone-200 hover:border-violet-300 hover:shadow-sm transition-all no-underline"
(h3 :class "text-lg font-semibold text-violet-700 mb-1" "Attributes")
(p :class "text-stone-600 text-sm" "All sx attributes — request verbs, behavior modifiers, and sx-unique features."))
(a :href "/sx/(geography.(hypermedia.(reference.headers)))"
:sx-get "/sx/(geography.(hypermedia.(reference.headers)))" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
(p
:class "text-stone-600 text-sm"
"All sx attributes — request verbs, behavior modifiers, and sx-unique features."))
(a
:href "/sx/(geography.(hypermedia.(reference.headers)))"
:sx-get "/sx/(geography.(hypermedia.(reference.headers)))"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "block p-5 rounded-lg border border-stone-200 hover:border-violet-300 hover:shadow-sm transition-all no-underline"
(h3 :class "text-lg font-semibold text-violet-700 mb-1" "Headers")
(p :class "text-stone-600 text-sm" "Custom HTTP headers used to coordinate between the sx client and server."))
(a :href "/sx/(geography.(hypermedia.(reference.events)))"
:sx-get "/sx/(geography.(hypermedia.(reference.events)))" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
(p
:class "text-stone-600 text-sm"
"Custom HTTP headers used to coordinate between the sx client and server."))
(a
:href "/sx/(geography.(hypermedia.(reference.events)))"
:sx-get "/sx/(geography.(hypermedia.(reference.events)))"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "block p-5 rounded-lg border border-stone-200 hover:border-violet-300 hover:shadow-sm transition-all no-underline"
(h3 :class "text-lg font-semibold text-violet-700 mb-1" "Events")
(p :class "text-stone-600 text-sm" "DOM events fired during the sx request lifecycle."))
(a :href "/sx/(geography.(hypermedia.(reference.js-api)))"
:sx-get "/sx/(geography.(hypermedia.(reference.js-api)))" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
(p
:class "text-stone-600 text-sm"
"DOM events fired during the sx request lifecycle."))
(a
:href "/sx/(geography.(hypermedia.(reference.js-api)))"
:sx-get "/sx/(geography.(hypermedia.(reference.js-api)))"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "block p-5 rounded-lg border border-stone-200 hover:border-violet-300 hover:shadow-sm transition-all no-underline"
(h3 :class "text-lg font-semibold text-violet-700 mb-1" "JS API")
(p :class "text-stone-600 text-sm" "JavaScript functions for parsing, evaluating, and rendering s-expressions.")))))
(p
:class "text-stone-600 text-sm"
"JavaScript functions for parsing, evaluating, and rendering s-expressions.")))))

View File

@@ -14,8 +14,8 @@
(a
:href "/sx/"
:sx-get "/sx/"
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "innerHTML"
:sx-push-url "true"
(~cssx/tw :tokens "block no-underline")
@@ -82,8 +82,8 @@
(a
:href (get prev-node "href")
:sx-get (get prev-node "href")
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "innerHTML"
:sx-push-url "true"
:class "text-right min-w-0 truncate"
@@ -92,8 +92,8 @@
(a
:href (get node "href")
:sx-get (get node "href")
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "innerHTML"
:sx-push-url "true"
:class "text-center min-w-0 truncate px-1"
@@ -105,8 +105,8 @@
(a
:href (get next-node "href")
:sx-get (get next-node "href")
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "innerHTML"
:sx-push-url "true"
:class "text-left min-w-0 truncate"
@@ -127,8 +127,8 @@
(a
:href (get item "href")
:sx-get (get item "href")
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "innerHTML"
:sx-push-url "true"
:class "px-3 py-1.5 rounded border transition-colors"
@@ -149,6 +149,7 @@
(div
:id "sx-nav"
:class "mb-6"
:sx-swap-oob "innerHTML"
(div
:id "logo-opacity"
:style (str
@@ -170,7 +171,7 @@
(when
(get nav-state "children")
(~layouts/nav-children :items (get nav-state "children"))))
children
(div :id "sx-content" (error-boundary children))
(~cssx/flush))))
(defcomp ~layouts/docs-layout-full () nil)

View File

@@ -1,19 +1,20 @@
;; 404 Not Found page content
(defcomp ~not-found/content (&key (path :as string?))
(div :class "max-w-3xl mx-auto px-4 py-12 text-center"
(h1 :style (tw "text-stone-800 text-3xl font-bold")
"404")
(p :class "mt-4"
:style (tw "text-stone-500 text-lg")
"Page not found")
(when path
(p :class "mt-2"
:style (tw "text-stone-400 text-sm font-mono")
path))
(a :href "/sx/"
:sx-get "/sx/" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
:class "inline-block mt-6 px-4 py-2 rounded border transition-colors"
:style (tw "text-violet-700 text-sm border-violet-200")
(defcomp
~not-found/content
(&key (path :as string?))
(div
:class "max-w-3xl mx-auto px-4 py-12 text-center"
(h1 :style (tw "text-stone-800 text-3xl font-bold") "404")
(p :class "mt-4" :style (tw "text-stone-500 text-lg") "Page not found")
(when
path
(p :class "mt-2" :style (tw "text-stone-400 text-sm font-mono") path))
(a
:href "/sx/"
:sx-get "/sx/"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "inline-block mt-6 px-4 py-2 rounded border transition-colors"
:style (tw "text-violet-700 text-sm border-violet-200")
"Back to home")))

View File

@@ -1,81 +1,104 @@
;; Optimistic update demo — exercises Phase 7c client-side predicted mutations.
;;
;; This page shows a todo list with optimistic add/remove.
;; Mutations are predicted client-side, sent to server, and confirmed/reverted.
;;
;; Open browser console and look for:
;; "sx:optimistic confirmed" — server accepted the mutation
;; "sx:optimistic reverted" — server rejected, data rolled back
(defcomp ~optimistic-demo/content (&key items server-time)
(div :class "space-y-8"
(div :class "border-b border-stone-200 pb-6"
(defcomp
~optimistic-demo/content
(&key items server-time)
(div
:class "space-y-8"
(div
:class "border-b border-stone-200 pb-6"
(h1 :class "text-2xl font-bold text-stone-900" "Optimistic Updates")
(p :class "mt-2 text-stone-600"
(p
:class "mt-2 text-stone-600"
"This page tests Phase 7c optimistic data mutations. Items are updated "
"instantly on the client, then confirmed or reverted when the server responds."))
;; Server metadata
(div :class "rounded-lg border border-stone-200 bg-white p-6 space-y-3"
(div
:class "rounded-lg border border-stone-200 bg-white p-6 space-y-3"
(h2 :class "text-lg font-semibold text-stone-800" "Current state")
(dl :class "grid grid-cols-2 gap-2 text-sm"
(dl
:class "grid grid-cols-2 gap-2 text-sm"
(dt :class "font-medium text-stone-600" "Server time")
(dd :class "font-mono text-stone-900" server-time)
(dt :class "font-medium text-stone-600" "Item count")
(dd :class "text-stone-900" (str (len items)))))
;; Item list
(div :class "space-y-3"
(div
:class "space-y-3"
(h2 :class "text-lg font-semibold text-stone-800" "Items")
(div :id "optimistic-items" :class "space-y-2"
(map (fn (item)
(div :class "flex items-center justify-between rounded border border-stone-100 bg-white p-3"
(div :class "flex items-center gap-3"
(span :class "flex-none rounded-full bg-violet-100 text-violet-700 w-6 h-6 flex items-center justify-center text-xs font-bold"
(str (get item "id")))
(span :class "text-stone-900" (get item "label")))
(span :class "text-xs px-2 py-0.5 rounded-full"
:class (case (get item "status")
"confirmed" "bg-green-100 text-green-700"
"pending" "bg-amber-100 text-amber-700"
"reverted" "bg-red-100 text-red-700"
:else "bg-stone-100 text-stone-500")
(get item "status"))))
(div
:id "optimistic-items"
:class "space-y-2"
(map
(fn
(item)
(div
:class "flex items-center justify-between rounded border border-stone-100 bg-white p-3"
(div
:class "flex items-center gap-3"
(span
:class "flex-none rounded-full bg-violet-100 text-violet-700 w-6 h-6 flex items-center justify-center text-xs font-bold"
(str (get item "id")))
(span :class "text-stone-900" (get item "label")))
(span
:class "text-xs px-2 py-0.5 rounded-full"
:class (case
(get item "status")
"confirmed"
"bg-green-100 text-green-700"
"pending"
"bg-amber-100 text-amber-700"
"reverted"
"bg-red-100 text-red-700"
:else "bg-stone-100 text-stone-500")
(get item "status"))))
items))
;; Add button — triggers optimistic mutation
(div :class "pt-2"
(button :class "px-4 py-2 bg-violet-500 text-white rounded hover:bg-violet-600 text-sm"
:sx-post "/sx/action/add-demo-item"
:sx-target "#main-panel"
:sx-vals "{\"label\": \"New item\"}"
(div
:class "pt-2"
(button
:class "px-4 py-2 bg-violet-500 text-white rounded hover:bg-violet-600 text-sm"
:sx-post "/sx/action/add-demo-item"
:sx-target "#sx-content"
:sx-vals "{\"label\": \"New item\"}"
"Add item (optimistic)")))
;; How it works
(div :class "space-y-4"
(div
:class "space-y-4"
(h2 :class "text-lg font-semibold text-stone-800" "How it works")
(div :class "space-y-2"
(div
:class "space-y-2"
(map-indexed
(fn (i step)
(div :class "flex items-start gap-3 rounded border border-stone-100 bg-white p-3"
(span :class "flex-none rounded-full bg-stone-100 text-stone-700 w-6 h-6 flex items-center justify-center text-xs font-bold"
(fn
(i step)
(div
:class "flex items-start gap-3 rounded border border-stone-100 bg-white p-3"
(span
:class "flex-none rounded-full bg-stone-100 text-stone-700 w-6 h-6 flex items-center justify-center text-xs font-bold"
(str (+ i 1)))
(div
(div :class "font-medium text-stone-900" (get step "label"))
(div :class "text-sm text-stone-500" (get step "detail")))))
(list
(dict :label "Predict" :detail "Client applies mutator function to cached data immediately")
(dict :label "Snapshot" :detail "Pre-mutation data saved in _optimistic-snapshots for rollback")
(dict :label "Re-render" :detail "Page content re-evaluated and swapped with predicted data")
(dict :label "Submit" :detail "Mutation sent to server via POST /sx/action/<name>")
(dict :label "Confirm or revert" :detail "Server responds — cache updated with truth, or reverted to snapshot")))))
;; How to verify
(div :class "rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm space-y-2"
(dict
:label "Predict"
:detail "Client applies mutator function to cached data immediately")
(dict
:label "Snapshot"
:detail "Pre-mutation data saved in _optimistic-snapshots for rollback")
(dict
:label "Re-render"
:detail "Page content re-evaluated and swapped with predicted data")
(dict
:label "Submit"
:detail "Mutation sent to server via POST /sx/action/<name>")
(dict
:label "Confirm or revert"
:detail "Server responds — cache updated with truth, or reverted to snapshot")))))
(div
:class "rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm space-y-2"
(p :class "font-semibold text-amber-800" "How to verify")
(ol :class "list-decimal list-inside text-amber-700 space-y-1"
(ol
:class "list-decimal list-inside text-amber-700 space-y-1"
(li "Open the browser console (F12)")
(li "Navigate to this page from another isomorphism page")
(li "Click \"Add item\" — item appears instantly with \"pending\" status")
(li "Watch console for " (code :class "bg-amber-100 px-1 rounded" "sx:optimistic confirmed"))
(li
"Click \"Add item\" — item appears instantly with \"pending\" status")
(li
"Watch console for "
(code :class "bg-amber-100 px-1 rounded" "sx:optimistic confirmed"))
(li "Item status changes to \"confirmed\" when server responds")))))

View File

@@ -1,25 +1,28 @@
;; Plans section — architecture roadmaps and implementation plans
;; ---------------------------------------------------------------------------
;; Plans index page
;; ---------------------------------------------------------------------------
(defcomp ~plans/index/plans-index-content ()
(~docs/page :title "Plans"
(div :class "space-y-4"
(p :class "text-lg text-stone-600 mb-4"
(defcomp
~plans/index/plans-index-content
()
(~docs/page
:title "Plans"
(div
:class "space-y-4"
(p
:class "text-lg text-stone-600 mb-4"
"Architecture roadmaps and implementation plans for SX.")
(div :class "space-y-3"
(map (fn (item)
(a :href (get item "href")
:sx-get (get item "href") :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
:class "block rounded border border-stone-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(div :class "font-semibold text-stone-800" (get item "label"))
(when (get item "summary")
(p :class "text-sm text-stone-500 mt-1" (get item "summary")))))
(div
:class "space-y-3"
(map
(fn
(item)
(a
:href (get item "href")
:sx-get (get item "href")
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "block rounded border border-stone-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(div :class "font-semibold text-stone-800" (get item "label"))
(when
(get item "summary")
(p :class "text-sm text-stone-500 mt-1" (get item "summary")))))
plans-nav-items)))))
;; ---------------------------------------------------------------------------
;; Reader Macros
;; ---------------------------------------------------------------------------

View File

@@ -41,8 +41,8 @@
(a
:href (get item "href")
:sx-get (get item "href")
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "text-violet-600 hover:underline"

View File

@@ -1,58 +1,128 @@
;; ---------------------------------------------------------------------------
;; Event Bridge — DOM events for lake→island communication
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands/event-bridge/reactive-islands-event-bridge-content ()
(~docs/page :title "Event Bridge"
(~docs/section :title "The Problem" :id "problem"
(p "A reactive island can contain server-rendered content — an htmx \"lake\" that swaps via " (code "sx-get") "/" (code "sx-post") ". The lake content is pure HTML from the server. It has no access to island signals.")
(p "But sometimes the lake needs to " (em "tell") " the island something happened. A server-rendered \"Add to Cart\" button needs to update the island's cart signal. A server-rendered search form needs to feed results into the island's result signal.")
(p "The event bridge solves this: DOM custom events bubble from the lake up to the island, where an effect listens and updates signals."))
(~docs/section :title "How it works" :id "how"
(defcomp
~reactive-islands/event-bridge/reactive-islands-event-bridge-content
()
(~docs/page
:title "Event Bridge"
(~docs/section
:title "The Problem"
:id "problem"
(p
"A reactive island can contain server-rendered content — an htmx \"lake\" that swaps via "
(code "sx-get")
"/"
(code "sx-post")
". The lake content is pure HTML from the server. It has no access to island signals.")
(p
"But sometimes the lake needs to "
(em "tell")
" the island something happened. A server-rendered \"Add to Cart\" button needs to update the island's cart signal. A server-rendered search form needs to feed results into the island's result signal.")
(p
"The event bridge solves this: DOM custom events bubble from the lake up to the island, where an effect listens and updates signals."))
(~docs/section
:title "How it works"
:id "how"
(p "Three components:")
(ol :class "space-y-2 text-stone-600 list-decimal list-inside"
(li (strong "Server emits: ") "Server-rendered elements carry " (code "data-sx-emit") " attributes. When the user interacts, the client dispatches a CustomEvent.")
(li (strong "Event bubbles: ") "The event bubbles up through the DOM tree until it reaches the island container.")
(li (strong "Effect catches: ") "An effect inside the island listens for the event name and updates a signal."))
(~docs/code :src (highlight ";; Island with an event bridge\n(defisland ~reactive-islands/event-bridge/product-page (&key product)\n (let ((cart-items (signal (list))))\n\n ;; Bridge: listen for \"cart:add\" events from server content\n (bridge-event container \"cart:add\" cart-items\n (fn (detail)\n (append (deref cart-items)\n (dict :id (get detail \"id\")\n :name (get detail \"name\")\n :price (get detail \"price\")))))\n\n (div\n ;; Island header with reactive cart count\n (div :class \"flex justify-between\"\n (h1 (get product \"name\"))\n (span :class \"badge\" (length (deref cart-items)) \" items\"))\n\n ;; htmx lake — server-rendered product details\n ;; This content is swapped by sx-get, not rendered by the island\n (div :id \"product-details\"\n :sx-get (str \"/products/\" (get product \"id\") \"/details\")\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\"))))" "lisp"))
(p "The server handler for " (code "/products/:id/details") " returns HTML with emit attributes:")
(~docs/code :src (highlight ";; Server-rendered response (pure HTML, no signals)\n(div\n (p (get product \"description\"))\n (div :class \"flex gap-2 mt-4\"\n (button\n :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize\n (dict :id (get product \"id\")\n :name (get product \"name\")\n :price (get product \"price\")))\n :class \"bg-violet-600 text-white px-4 py-2 rounded\"\n \"Add to Cart\")))" "lisp"))
(p "The button is plain server HTML. When clicked, the client's event bridge dispatches " (code "cart:add") " with the JSON detail. The island effect catches it and appends to " (code "cart-items") ". The badge updates reactively."))
(~docs/section :title "Why signals survive swaps" :id "survival"
(p "Signals live in JavaScript memory (closures), not in the DOM. When htmx swaps content inside an island:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Swap inside island: ") "Signals survive. The lake content is replaced but the island's signal closures are untouched. Effects re-bind to new DOM nodes if needed.")
(li (strong "Swap outside island: ") "Signals survive. The island is not affected by swaps to other parts of the page.")
(li (strong "Swap replaces island: ") "Signals are " (em "lost") ". The island is disposed. This is where " (a :href "/sx/(geography.(reactive.(named-stores)))" :sx-get "/sx/(geography.(reactive.(named-stores)))" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "named stores") " come in — they persist at page level, surviving island destruction.")))
(~docs/section :title "Spec" :id "spec"
(p "The event bridge is spec'd in " (code "signals.sx") " (sections 12-13). Three functions:")
(~docs/code :src (highlight ";; Low-level: dispatch a custom event\n(emit-event el \"cart:add\" {:id 42 :name \"Widget\"})\n\n;; Low-level: listen for a custom event\n(on-event container \"cart:add\" (fn (e)\n (swap! items (fn (old) (append old (event-detail e))))))\n\n;; High-level: bridge an event directly to a signal\n;; Creates an effect with automatic cleanup on dispose\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))" "lisp"))
(ol
:class "space-y-2 text-stone-600 list-decimal list-inside"
(li
(strong "Server emits: ")
"Server-rendered elements carry "
(code "data-sx-emit")
" attributes. When the user interacts, the client dispatches a CustomEvent.")
(li
(strong "Event bubbles: ")
"The event bubbles up through the DOM tree until it reaches the island container.")
(li
(strong "Effect catches: ")
"An effect inside the island listens for the event name and updates a signal."))
(~docs/code
:src (highlight
";; Island with an event bridge\n(defisland ~reactive-islands/event-bridge/product-page (&key product)\n (let ((cart-items (signal (list))))\n\n ;; Bridge: listen for \"cart:add\" events from server content\n (bridge-event container \"cart:add\" cart-items\n (fn (detail)\n (append (deref cart-items)\n (dict :id (get detail \"id\")\n :name (get detail \"name\")\n :price (get detail \"price\")))))\n\n (div\n ;; Island header with reactive cart count\n (div :class \"flex justify-between\"\n (h1 (get product \"name\"))\n (span :class \"badge\" (length (deref cart-items)) \" items\"))\n\n ;; htmx lake — server-rendered product details\n ;; This content is swapped by sx-get, not rendered by the island\n (div :id \"product-details\"\n :sx-get (str \"/products/\" (get product \"id\") \"/details\")\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\"))))"
"lisp"))
(p
"The server handler for "
(code "/products/:id/details")
" returns HTML with emit attributes:")
(~docs/code
:src (highlight
";; Server-rendered response (pure HTML, no signals)\n(div\n (p (get product \"description\"))\n (div :class \"flex gap-2 mt-4\"\n (button\n :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize\n (dict :id (get product \"id\")\n :name (get product \"name\")\n :price (get product \"price\")))\n :class \"bg-violet-600 text-white px-4 py-2 rounded\"\n \"Add to Cart\")))"
"lisp"))
(p
"The button is plain server HTML. When clicked, the client's event bridge dispatches "
(code "cart:add")
" with the JSON detail. The island effect catches it and appends to "
(code "cart-items")
". The badge updates reactively."))
(~docs/section
:title "Why signals survive swaps"
:id "survival"
(p
"Signals live in JavaScript memory (closures), not in the DOM. When htmx swaps content inside an island:")
(ul
:class "space-y-2 text-stone-600 list-disc pl-5"
(li
(strong "Swap inside island: ")
"Signals survive. The lake content is replaced but the island's signal closures are untouched. Effects re-bind to new DOM nodes if needed.")
(li
(strong "Swap outside island: ")
"Signals survive. The island is not affected by swaps to other parts of the page.")
(li
(strong "Swap replaces island: ")
"Signals are "
(em "lost")
". The island is disposed. This is where "
(a
:href "/sx/(geography.(reactive.(named-stores)))"
:sx-get "/sx/(geography.(reactive.(named-stores)))"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "text-violet-700 underline"
"named stores")
" come in — they persist at page level, surviving island destruction.")))
(~docs/section
:title "Spec"
:id "spec"
(p
"The event bridge is spec'd in "
(code "signals.sx")
" (sections 12-13). Three functions:")
(~docs/code
:src (highlight
";; Low-level: dispatch a custom event\n(emit-event el \"cart:add\" {:id 42 :name \"Widget\"})\n\n;; Low-level: listen for a custom event\n(on-event container \"cart:add\" (fn (e)\n (swap! items (fn (old) (append old (event-detail e))))))\n\n;; High-level: bridge an event directly to a signal\n;; Creates an effect with automatic cleanup on dispose\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))"
"lisp"))
(p "Platform interface required:")
(div :class "overflow-x-auto rounded border border-stone-200 mt-2"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Function")
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "(dom-listen el name handler)")
(td :class "px-3 py-2 text-stone-700" "Attach event listener, return remove function"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "(dom-dispatch el name detail)")
(td :class "px-3 py-2 text-stone-700" "Dispatch CustomEvent with detail, bubbles: true"))
(div
:class "overflow-x-auto rounded border border-stone-200 mt-2"
(table
:class "w-full text-left text-sm"
(thead
(tr
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "(event-detail e)")
(td :class "px-3 py-2 text-stone-700" "Extract .detail from CustomEvent"))))))))
;; ---------------------------------------------------------------------------
;; Named Stores — page-level signal containers
;; ---------------------------------------------------------------------------
:class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Function")
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
(tbody
(tr
:class "border-b border-stone-100"
(td
:class "px-3 py-2 font-mono text-sm text-violet-700"
"(dom-listen el name handler)")
(td
:class "px-3 py-2 text-stone-700"
"Attach event listener, return remove function"))
(tr
:class "border-b border-stone-100"
(td
:class "px-3 py-2 font-mono text-sm text-violet-700"
"(dom-dispatch el name detail)")
(td
:class "px-3 py-2 text-stone-700"
"Dispatch CustomEvent with detail, bubbles: true"))
(tr
(td
:class "px-3 py-2 font-mono text-sm text-violet-700"
"(event-detail e)")
(td
:class "px-3 py-2 text-stone-700"
"Extract .detail from CustomEvent"))))))))

File diff suppressed because it is too large Load Diff

View File

@@ -1,163 +1,333 @@
;; ---------------------------------------------------------------------------
;; Plan — the full design document (moved from plans section)
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands/plan/reactive-islands-plan-content ()
(~docs/page :title "Reactive Islands Plan"
(~docs/section :title "Context" :id "context"
(p "SX already has a sliding bar for " (em "where") " rendering happens — server-side HTML, SX wire format for client rendering, or any point between. This is the isomorphism bar. It controls the render boundary.")
(p "There is a second bar, orthogonal to the first: " (em "how state flows.") " On one end, all state lives on the server — every user action is a round-trip, every UI update is a fresh render. This is the htmx model. On the other end, state lives on the client — signals, subscriptions, fine-grained DOM updates without server involvement. This is the React model.")
(p "These two bars are independent. You can have server-rendered HTML with client state (SSR + hydrated React). You can have client-rendered components with server state (current SX). The combination creates four quadrants:")
(div :class "overflow-x-auto mt-4 mb-4"
(table :class "w-full text-sm text-left"
(defcomp
~reactive-islands/plan/reactive-islands-plan-content
()
(~docs/page
:title "Reactive Islands Plan"
(~docs/section
:title "Context"
:id "context"
(p
"SX already has a sliding bar for "
(em "where")
" rendering happens — server-side HTML, SX wire format for client rendering, or any point between. This is the isomorphism bar. It controls the render boundary.")
(p
"There is a second bar, orthogonal to the first: "
(em "how state flows.")
" On one end, all state lives on the server — every user action is a round-trip, every UI update is a fresh render. This is the htmx model. On the other end, state lives on the client — signals, subscriptions, fine-grained DOM updates without server involvement. This is the React model.")
(p
"These two bars are independent. You can have server-rendered HTML with client state (SSR + hydrated React). You can have client-rendered components with server state (current SX). The combination creates four quadrants:")
(div
:class "overflow-x-auto mt-4 mb-4"
(table
:class "w-full text-sm text-left"
(thead
(tr :class "border-b border-stone-200"
(tr
:class "border-b border-stone-200"
(th :class "py-2 px-3 font-semibold text-stone-700" "")
(th :class "py-2 px-3 font-semibold text-stone-700" "Server State")
(th :class "py-2 px-3 font-semibold text-stone-700" "Client State")))
(tbody :class "text-stone-600"
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold text-stone-700" "Server Rendering")
(th
:class "py-2 px-3 font-semibold text-stone-700"
"Server State")
(th
:class "py-2 px-3 font-semibold text-stone-700"
"Client State")))
(tbody
:class "text-stone-600"
(tr
:class "border-b border-stone-100"
(td
:class "py-2 px-3 font-semibold text-stone-700"
"Server Rendering")
(td :class "py-2 px-3" "Pure hypermedia (htmx)")
(td :class "py-2 px-3" "SSR + hydrated islands (Next.js)"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold text-stone-700" "Client Rendering")
(tr
:class "border-b border-stone-100"
(td
:class "py-2 px-3 font-semibold text-stone-700"
"Client Rendering")
(td :class "py-2 px-3" "SX wire format (current)")
(td :class "py-2 px-3 font-semibold text-violet-700" "Reactive islands (this plan)")))))
(p "Today SX occupies the bottom-left quadrant — client-rendered components with server state. This plan adds the bottom-right: " (strong "reactive islands") " with client-local signals. A page can mix all four quadrants. Most content stays hypermedia. Interactive regions opt into reactivity. The author controls where each component sits on both bars."))
(~docs/section :title "The Spectrum" :id "spectrum"
(p "Four levels of client interactivity. Each is independently valuable. Each is opt-in per component.")
(~docs/subsection :title "Level 0: Pure Hypermedia"
(p "The default. " (code "sx-get") ", " (code "sx-post") ", " (code "sx-swap") ". Server renders everything. Client swaps fragments. No client state. No JavaScript state management. This is where 90% of a typical application should live."))
(~docs/subsection :title "Level 1: Local DOM Operations"
(p "Imperative escape hatches for micro-interactions too small for a server round-trip: toggling a menu, switching a tab, showing a tooltip. " (code "toggle!") ", " (code "set-attr!") ", " (code "on-event") ". No reactive graph. Just do the thing directly."))
(~docs/subsection :title "Level 2: Reactive Islands"
(p (code "defisland") " components with local signals. Fine-grained DOM updates — no virtual DOM, no diffing, no component re-renders. A signal change updates only the DOM nodes that read it. Islands are isolated by default. The server can render their initial state."))
(~docs/subsection :title "Level 3: Connected Islands"
(p "Islands that share state via signal props or named stores (" (code "def-store") " / " (code "use-store") "). Plus event bridges for htmx lake-to-island communication. This is where SX starts to feel like React — but only in the regions that need it. The surrounding page remains hypermedia.")))
(~docs/section :title "htmx Lakes" :id "lakes"
(p "An htmx lake is server-driven content " (em "inside") " a reactive island. The island provides the reactive boundary; the lake content is swapped via " (code "sx-get") "/" (code "sx-post") " like normal hypermedia.")
(p "This works because signals live in JavaScript closures, not in the DOM. When a swap replaces lake content, the island's signals are unaffected. The lake can communicate back to the island via the " (a :href "/sx/(geography.(reactive.(event-bridge)))" :sx-get "/sx/(geography.(reactive.(event-bridge)))" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "event bridge") ".")
(~docs/subsection :title "Navigation scenarios"
(div :class "space-y-3"
(div :class "rounded border border-green-200 bg-green-50 p-3"
(td
:class "py-2 px-3 font-semibold text-violet-700"
"Reactive islands (this plan)")))))
(p
"Today SX occupies the bottom-left quadrant — client-rendered components with server state. This plan adds the bottom-right: "
(strong "reactive islands")
" with client-local signals. A page can mix all four quadrants. Most content stays hypermedia. Interactive regions opt into reactivity. The author controls where each component sits on both bars."))
(~docs/section
:title "The Spectrum"
:id "spectrum"
(p
"Four levels of client interactivity. Each is independently valuable. Each is opt-in per component.")
(~docs/subsection
:title "Level 0: Pure Hypermedia"
(p
"The default. "
(code "sx-get")
", "
(code "sx-post")
", "
(code "sx-swap")
". Server renders everything. Client swaps fragments. No client state. No JavaScript state management. This is where 90% of a typical application should live."))
(~docs/subsection
:title "Level 1: Local DOM Operations"
(p
"Imperative escape hatches for micro-interactions too small for a server round-trip: toggling a menu, switching a tab, showing a tooltip. "
(code "toggle!")
", "
(code "set-attr!")
", "
(code "on-event")
". No reactive graph. Just do the thing directly."))
(~docs/subsection
:title "Level 2: Reactive Islands"
(p
(code "defisland")
" components with local signals. Fine-grained DOM updates — no virtual DOM, no diffing, no component re-renders. A signal change updates only the DOM nodes that read it. Islands are isolated by default. The server can render their initial state."))
(~docs/subsection
:title "Level 3: Connected Islands"
(p
"Islands that share state via signal props or named stores ("
(code "def-store")
" / "
(code "use-store")
"). Plus event bridges for htmx lake-to-island communication. This is where SX starts to feel like React — but only in the regions that need it. The surrounding page remains hypermedia.")))
(~docs/section
:title "htmx Lakes"
:id "lakes"
(p
"An htmx lake is server-driven content "
(em "inside")
" a reactive island. The island provides the reactive boundary; the lake content is swapped via "
(code "sx-get")
"/"
(code "sx-post")
" like normal hypermedia.")
(p
"This works because signals live in JavaScript closures, not in the DOM. When a swap replaces lake content, the island's signals are unaffected. The lake can communicate back to the island via the "
(a
:href "/sx/(geography.(reactive.(event-bridge)))"
:sx-get "/sx/(geography.(reactive.(event-bridge)))"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "text-violet-700 underline"
"event bridge")
".")
(~docs/subsection
:title "Navigation scenarios"
(div
:class "space-y-3"
(div
:class "rounded border border-green-200 bg-green-50 p-3"
(div :class "font-semibold text-green-800" "Swap inside island")
(p :class "text-sm text-stone-600 mt-1" "Lake content replaced. Signals survive. Effects can rebind to new DOM. User state intact."))
(div :class "rounded border border-green-200 bg-green-50 p-3"
(p
:class "text-sm text-stone-600 mt-1"
"Lake content replaced. Signals survive. Effects can rebind to new DOM. User state intact."))
(div
:class "rounded border border-green-200 bg-green-50 p-3"
(div :class "font-semibold text-green-800" "Swap outside island")
(p :class "text-sm text-stone-600 mt-1" "Different part of page updated. Island completely unaffected. User state intact."))
(div :class "rounded border border-amber-200 bg-amber-50 p-3"
(p
:class "text-sm text-stone-600 mt-1"
"Different part of page updated. Island completely unaffected. User state intact."))
(div
:class "rounded border border-amber-200 bg-amber-50 p-3"
(div :class "font-semibold text-amber-800" "Swap replaces island")
(p :class "text-sm text-stone-600 mt-1" "Island disposed. Local signals lost. Named stores persist — new island reconnects via use-store."))
(div :class "rounded border border-stone-200 p-3"
(p
:class "text-sm text-stone-600 mt-1"
"Island disposed. Local signals lost. Named stores persist — new island reconnects via use-store."))
(div
:class "rounded border border-stone-200 p-3"
(div :class "font-semibold text-stone-800" "Full page navigation")
(p :class "text-sm text-stone-600 mt-1" "Everything cleared. clean slate. clear-stores wipes the registry.")))))
(~docs/section :title "Reactive DOM Rendering" :id "reactive-rendering"
(p "The existing " (code "renderDOM") " function walks the AST and creates DOM nodes. Inside an island, it becomes signal-aware:")
(~docs/subsection :title "Text bindings"
(~docs/code :src (highlight ";; (span (deref count)) creates:\n;; const text = document.createTextNode(sig.value)\n;; effect(() => text.nodeValue = sig.value)" "lisp"))
(p
:class "text-sm text-stone-600 mt-1"
"Everything cleared. clean slate. clear-stores wipes the registry.")))))
(~docs/section
:title "Reactive DOM Rendering"
:id "reactive-rendering"
(p
"The existing "
(code "renderDOM")
" function walks the AST and creates DOM nodes. Inside an island, it becomes signal-aware:")
(~docs/subsection
:title "Text bindings"
(~docs/code
:src (highlight
";; (span (deref count)) creates:\n;; const text = document.createTextNode(sig.value)\n;; effect(() => text.nodeValue = sig.value)"
"lisp"))
(p "Only the text node updates. The span is untouched."))
(~docs/subsection :title "Attribute bindings"
(~docs/code :src (highlight ";; (div :class (str \"panel \" (if (deref open?) \"visible\" \"hidden\")))\n;; effect(() => div.className = ...)" "lisp")))
(~docs/subsection :title "Conditional fragments"
(~docs/code :src (highlight ";; (when (deref show?) (~details)) creates:\n;; A marker comment node, then:\n;; effect(() => show ? insert-after(marker, render(~details)) : remove)" "lisp"))
(p "Equivalent to SolidJS's " (code "Show") " — but falls out naturally from the evaluator."))
(~docs/subsection :title "List rendering"
(~docs/code :src (highlight "(map (fn (item) (li :key (get item \"id\") (get item \"name\")))\n (deref items))" "lisp"))
(p "Keyed elements are reused and reordered. Unkeyed elements are morphed.")))
(~docs/section :title "Status" :id "status"
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Task")
(th :class "px-3 py-2 font-medium text-stone-600" "Status")
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
(~docs/subsection
:title "Attribute bindings"
(~docs/code
:src (highlight
";; (div :class (str \"panel \" (if (deref open?) \"visible\" \"hidden\")))\n;; effect(() => div.className = ...)"
"lisp")))
(~docs/subsection
:title "Conditional fragments"
(~docs/code
:src (highlight
";; (when (deref show?) (~details)) creates:\n;; A marker comment node, then:\n;; effect(() => show ? insert-after(marker, render(~details)) : remove)"
"lisp"))
(p
"Equivalent to SolidJS's "
(code "Show")
" — but falls out naturally from the evaluator."))
(~docs/subsection
:title "List rendering"
(~docs/code
:src (highlight
"(map (fn (item) (li :key (get item \"id\") (get item \"name\")))\n (deref items))"
"lisp"))
(p
"Keyed elements are reused and reordered. Unkeyed elements are morphed.")))
(~docs/section
:title "Status"
:id "status"
(div
:class "overflow-x-auto rounded border border-stone-200"
(table
:class "w-full text-left text-sm"
(thead
(tr
:class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Task")
(th :class "px-3 py-2 font-medium text-stone-600" "Status")
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
(tbody
(tr :class "border-b border-stone-100"
(tr
:class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Signal runtime")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "signals.sx: signal, deref, reset!, swap!, computed, effect, batch"))
(tr :class "border-b border-stone-100"
(td
:class "px-3 py-2 text-stone-700"
"signals.sx: signal, deref, reset!, swap!, computed, effect, batch"))
(tr
:class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Named stores (L3)")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "signals.sx: def-store, use-store, clear-stores"))
(tr :class "border-b border-stone-100"
(td
:class "px-3 py-2 text-stone-700"
"signals.sx: def-store, use-store, clear-stores"))
(tr
:class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Event bridge")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "signals.sx: emit-event, on-event, bridge-event"))
(tr :class "border-b border-stone-100"
(td
:class "px-3 py-2 text-stone-700"
"signals.sx: emit-event, on-event, bridge-event"))
(tr
:class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Event bindings")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: :on-click (fn ...) → domListen"))
(tr :class "border-b border-stone-100"
(td
:class "px-3 py-2 text-stone-700"
"adapter-dom.sx: :on-click (fn ...) → domListen"))
(tr
:class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "data-sx-emit")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "orchestration.sx: auto-dispatch custom events from server content"))
(tr :class "border-b border-stone-100"
(td
:class "px-3 py-2 text-stone-700"
"orchestration.sx: auto-dispatch custom events from server content"))
(tr
:class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Client hydration")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "boot.sx: hydrate-island, dispose-island, post-swap wiring"))
(tr :class "border-b border-stone-100"
(td
:class "px-3 py-2 text-stone-700"
"boot.sx: hydrate-island, dispose-island, post-swap wiring"))
(tr
:class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Bootstrapping")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "All functions transpiled to JS and Python, platform primitives implemented"))
(tr :class "border-b border-stone-100"
(td
:class "px-3 py-2 text-stone-700"
"All functions transpiled to JS and Python, platform primitives implemented"))
(tr
:class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Island disposal")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "boot.sx, orchestration.sx: effects/computeds auto-register disposers, pre-swap cleanup"))
(tr :class "border-b border-stone-100"
(td
:class "px-3 py-2 text-stone-700"
"boot.sx, orchestration.sx: effects/computeds auto-register disposers, pre-swap cleanup"))
(tr
:class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Reactive list")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: map + deref auto-upgrades to reactive-list"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Input binding + keyed lists")
(td
:class "px-3 py-2 text-stone-700"
"adapter-dom.sx: map + deref auto-upgrades to reactive-list"))
(tr
:class "border-b border-stone-100"
(td
:class "px-3 py-2 text-stone-700"
"Input binding + keyed lists")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: :bind signal, :key attr"))
(tr :class "border-b border-stone-100"
(td
:class "px-3 py-2 text-stone-700"
"adapter-dom.sx: :bind signal, :key attr"))
(tr
:class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Portals")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: portal render-dom form"))
(tr :class "border-b border-stone-100"
(td
:class "px-3 py-2 text-stone-700"
"adapter-dom.sx: portal render-dom form"))
(tr
:class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Error boundaries")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: error-boundary render-dom form"))
(tr :class "border-b border-stone-100"
(td
:class "px-3 py-2 text-stone-700"
"adapter-dom.sx: error-boundary render-dom form"))
(tr
:class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Suspense")
(td :class "px-3 py-2 text-stone-500 font-medium" "N/A")
(td :class "px-3 py-2 text-stone-700" "covered by existing primitives"))
(td
:class "px-3 py-2 text-stone-700"
"covered by existing primitives"))
(tr
(td :class "px-3 py-2 text-stone-700" "Transitions")
(td :class "px-3 py-2 text-stone-500 font-medium" "N/A")
(td :class "px-3 py-2 text-stone-700" "covered by existing primitives"))))))
(~docs/section :title "Design Principles" :id "principles"
(ol :class "space-y-3 text-stone-600 list-decimal list-inside"
(li (strong "Islands are opt-in.") " " (code "defcomp") " remains the default. Components are inert unless you choose " (code "defisland") ". No reactive overhead for static content.")
(li (strong "Signals are values, not hooks.") " Create them anywhere. Pass them as arguments. Store them in dicts. No rules about calling order or conditional creation.")
(li (strong "Fine-grained, not component-grained.") " A signal change updates the specific DOM node that reads it. The island does not re-render. There is no virtual DOM and no diffing beyond the morph algorithm already in SxEngine.")
(li (strong "The server is still the authority.") " Islands handle client interactions. The server handles auth, data, routing. The server can push state into islands via OOB swaps. Islands can submit data to the server via " (code "sx-post") ".")
(li (strong "Spec-first.") " Signal semantics live in " (code "signals.sx") ". Bootstrapped to JS and Python. The same primitives will work in future hosts — Go, Rust, native.")
(li (strong "No build step.") " Reactive bindings are created at runtime during DOM rendering. No JSX compilation, no Babel transforms, no Vite plugins."))
(p :class "mt-4" "The recommendation from the " (a :href "/sx/(etc.(essay.client-reactivity))" :class "text-violet-700 underline" "Client Reactivity") " essay was: \"Tier 4 probably never.\" This plan is what happens when the answer changes. The design avoids every footgun that essay warns about — no useState cascading to useEffect cascading to Context cascading to a state management library. Signals are one primitive. Islands are one boundary. The rest is composition."))))
;; ---------------------------------------------------------------------------
;; Phase 2 Plan — remaining reactive features
;; ---------------------------------------------------------------------------
(td
:class "px-3 py-2 text-stone-700"
"covered by existing primitives"))))))
(~docs/section
:title "Design Principles"
:id "principles"
(ol
:class "space-y-3 text-stone-600 list-decimal list-inside"
(li
(strong "Islands are opt-in.")
" "
(code "defcomp")
" remains the default. Components are inert unless you choose "
(code "defisland")
". No reactive overhead for static content.")
(li
(strong "Signals are values, not hooks.")
" Create them anywhere. Pass them as arguments. Store them in dicts. No rules about calling order or conditional creation.")
(li
(strong "Fine-grained, not component-grained.")
" A signal change updates the specific DOM node that reads it. The island does not re-render. There is no virtual DOM and no diffing beyond the morph algorithm already in SxEngine.")
(li
(strong "The server is still the authority.")
" Islands handle client interactions. The server handles auth, data, routing. The server can push state into islands via OOB swaps. Islands can submit data to the server via "
(code "sx-post")
".")
(li
(strong "Spec-first.")
" Signal semantics live in "
(code "signals.sx")
". Bootstrapped to JS and Python. The same primitives will work in future hosts — Go, Rust, native.")
(li
(strong "No build step.")
" Reactive bindings are created at runtime during DOM rendering. No JSX compilation, no Babel transforms, no Vite plugins."))
(p
:class "mt-4"
"The recommendation from the "
(a
:href "/sx/(etc.(essay.client-reactivity))"
:class "text-violet-700 underline"
"Client Reactivity")
" essay was: \"Tier 4 probably never.\" This plan is what happens when the answer changes. The design avoids every footgun that essay warns about — no useState cascading to useEffect cascading to Context cascading to a state management library. Signals are one primitive. Islands are one boundary. The rest is composition."))))

View File

@@ -1,90 +1,120 @@
;; 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)
(~docs/page :title "Attribute Reference"
(p :class "text-stone-600 mb-6"
(defcomp
~reference/attrs-content
(&key req-table beh-table uniq-table)
(~docs/page
:title "Attribute Reference"
(p
:class "text-stone-600 mb-6"
"sx attributes mirror htmx where possible. This table shows all available attributes and their status.")
(div :class "space-y-8"
req-table
beh-table
uniq-table)))
(div :class "space-y-8" req-table beh-table uniq-table)))
(defcomp ~reference/headers-content (&key req-table resp-table)
(~docs/page :title "Headers"
(p :class "text-stone-600 mb-6"
(defcomp
~reference/headers-content
(&key req-table resp-table)
(~docs/page
:title "Headers"
(p
:class "text-stone-600 mb-6"
"sx uses custom HTTP headers to coordinate between client and server.")
(div :class "space-y-8"
req-table
resp-table)))
(div :class "space-y-8" req-table resp-table)))
(defcomp ~reference/events-content (&key table)
(~docs/page :title "Events"
(p :class "text-stone-600 mb-6"
(defcomp
~reference/events-content
(&key table)
(~docs/page
:title "Events"
(p
:class "text-stone-600 mb-6"
"sx fires custom DOM events at various points in the request lifecycle. "
"Listen for them with sx-on:* attributes or addEventListener. "
"Client-side routing fires sx:clientRoute instead of request lifecycle events.")
table))
(defcomp ~reference/js-api-content (&key table)
(~docs/page :title "JavaScript API"
table))
(defcomp
~reference/js-api-content
(&key table)
(~docs/page :title "JavaScript API" table))
(defcomp ~reference/attr-detail-content (&key (title :as string) (description :as string) demo
(example-code :as string) (handler-code :as string?) (wire-placeholder-id :as string?))
(~docs/page :title title
(defcomp
~reference/attr-detail-content
(&key
(title :as string)
(description :as string)
demo
(example-code :as string)
(handler-code :as string?)
(wire-placeholder-id :as string?))
(~docs/page
:title title
(p :class "text-stone-600 mb-6" description)
(when demo
(~examples/card :title "Demo"
(~examples/demo demo)))
(when demo (~examples/card :title "Demo" (~examples/demo demo)))
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")
(~examples/source :code (highlight example-code "lisp"))
(when handler-code
(~examples/source :src-code (highlight example-code "lisp"))
(when
handler-code
(<>
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")
(~examples/source :code (highlight handler-code "lisp"))))
(when wire-placeholder-id
(h3
:class "text-lg font-semibold text-stone-700 mt-6"
"Server handler")
(~examples/source :src-code (highlight handler-code "lisp"))))
(when
wire-placeholder-id
(<>
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Wire response")
(p :class "text-stone-500 text-sm mb-2"
(p
:class "text-stone-500 text-sm mb-2"
"Trigger the demo to see the raw response the server sends.")
(~docs/placeholder :id wire-placeholder-id)))))
(defcomp ~reference/header-detail-content (&key (title :as string) (direction :as string) (description :as string)
(example-code :as string?) demo)
(~docs/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"
(defcomp
~reference/header-detail-content
(&key
(title :as string)
(direction :as string)
(description :as string)
(example-code :as string?)
demo)
(~docs/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)
(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
(~examples/card :title "Demo"
(~examples/demo demo)))
(when example-code
(when demo (~examples/card :title "Demo" (~examples/demo demo)))
(when
example-code
(<>
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Example usage")
(~examples/source :code (highlight example-code "lisp"))))))
(~examples/source :src-code (highlight example-code "lisp"))))))
(defcomp ~reference/event-detail-content (&key title description example-code demo)
(~docs/page :title title
(defcomp
~reference/event-detail-content
(&key title description example-code demo)
(~docs/page
:title title
(p :class "text-stone-600 mb-6" description)
(when demo
(~examples/card :title "Demo"
(~examples/demo demo)))
(when example-code
(when demo (~examples/card :title "Demo" (~examples/demo demo)))
(when
example-code
(<>
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Example usage")
(~examples/source :code (highlight example-code "lisp"))))))
(~examples/source :src-code (highlight example-code "lisp"))))))
(defcomp ~reference/attr-not-found (&key (slug :as string))
(~docs/page :title "Not Found"
(p :class "text-stone-600"
(str "No documentation found for \"" slug "\"."))))
(defcomp
~reference/attr-not-found
(&key (slug :as string))
(~docs/page
:title "Not Found"
(p :class "text-stone-600" (str "No documentation found for \"" slug "\"."))))

View File

@@ -1,96 +1,175 @@
;; Routing analyzer — live demonstration of client-side routing classification.
;; Shows which pages route client-side (pure, instant) vs server-side (IO/data).
;; @css bg-green-100 text-green-800 bg-violet-600 bg-stone-200 text-violet-600 text-stone-600 text-green-600 rounded-full h-2.5 grid-cols-2 bg-blue-100 text-blue-800 bg-amber-100 text-amber-800 grid-cols-4 marker:text-stone-400 bg-blue-50 bg-amber-50 text-blue-700 text-amber-700 border-blue-200 border-amber-200 bg-blue-500 bg-amber-500 grid-cols-3 border-green-200 bg-green-50 text-green-700
(defcomp ~routing-analyzer/content (&key pages total-pages client-count
server-count registry-sample)
(~docs/page :title "Routing Analyzer"
(p :class "text-stone-600 mb-6"
"Live classification of all " (strong (str total-pages)) " pages by routing mode. "
"Pages without " (code ":data") " dependencies are "
(defcomp
~routing-analyzer/content
(&key pages total-pages client-count server-count registry-sample)
(~docs/page
:title "Routing Analyzer"
(p
:class "text-stone-600 mb-6"
"Live classification of all "
(strong (str total-pages))
" pages by routing mode. "
"Pages without "
(code ":data")
" dependencies are "
(span :class "text-green-700 font-medium" "client-routable")
" — after initial load they render instantly from the page registry without a server roundtrip. "
"Pages with data dependencies fall back to "
(span :class "text-amber-700 font-medium" "server fetch")
" transparently. Powered by "
(a :href "/sx/(language.(spec.router))" :class "text-violet-700 underline" "router.sx")
(a
:href "/sx/(language.(spec.router))"
:class "text-violet-700 underline"
"router.sx")
" route matching and "
(a :href "/sx/(language.(spec.deps))" :class "text-violet-700 underline" "deps.sx")
(a
:href "/sx/(language.(spec.deps))"
:class "text-violet-700 underline"
"deps.sx")
" IO detection.")
(div :class "mb-8 grid grid-cols-4 gap-4"
(~analyzer/stat :label "Total Pages" :value (str total-pages)
:cls "text-violet-600")
(~analyzer/stat :label "Client-Routable" :value (str client-count)
:cls "text-green-600")
(~analyzer/stat :label "Server-Only" :value (str server-count)
:cls "text-amber-600")
(~analyzer/stat :label "Client Ratio" :value (str (round (* (/ client-count total-pages) 100)) "%")
:cls "text-blue-600"))
;; Route classification bar
(div :class "mb-8"
(div :class "flex items-center gap-2 mb-2"
(div
:class "mb-8 grid grid-cols-4 gap-4"
(~analyzer/stat
:label "Total Pages"
:value (str total-pages)
:cls "text-violet-600")
(~analyzer/stat
:label "Client-Routable"
:value (str client-count)
:cls "text-green-600")
(~analyzer/stat
:label "Server-Only"
:value (str server-count)
:cls "text-amber-600")
(~analyzer/stat
:label "Client Ratio"
:value (str (round (* (/ client-count total-pages) 100)) "%")
:cls "text-blue-600"))
(div
:class "mb-8"
(div
:class "flex items-center gap-2 mb-2"
(span :class "text-sm font-medium text-stone-600" "Client")
(div :class "flex-1")
(span :class "text-sm font-medium text-stone-600" "Server"))
(div :class "w-full bg-amber-200 rounded-full h-4 overflow-hidden"
(div :class "bg-green-500 h-4 rounded-l-full transition-all"
:style (str "width: " (round (* (/ client-count total-pages) 100)) "%"))))
(~docs/section :title "Route Table" :id "routes"
(div :class "space-y-2"
(map (fn (page)
(~routing-analyzer/routing-row
:name (get page "name")
:path (get page "path")
:mode (get page "mode")
:has-data (get page "has-data")
:content-expr (get page "content-expr")
:reason (get page "reason")))
(div
:class "w-full bg-amber-200 rounded-full h-4 overflow-hidden"
(div
:class "bg-green-500 h-4 rounded-l-full transition-all"
:style (str "width: " (round (* (/ client-count total-pages) 100)) "%"))))
(~docs/section
:title "Route Table"
:id "routes"
(div
:class "space-y-2"
(map
(fn
(page)
(~routing-analyzer/routing-row
:name (get page "name")
:path (get page "path")
:mode (get page "mode")
:has-data (get page "has-data")
:content-expr (get page "content-expr")
:reason (get page "reason")))
pages)))
(~docs/section :title "Page Registry Format" :id "registry"
(p :class "text-stone-600 mb-4"
(~docs/section
:title "Page Registry Format"
:id "registry"
(p
:class "text-stone-600 mb-4"
"The server serializes page metadata as SX dict literals inside "
(code "<script type=\"text/sx-pages\">")
". The client's parser reads these at boot, building a route table with parsed URL patterns. "
"No JSON involved — the same SX parser handles everything.")
(when (not (empty? registry-sample))
(div :class "not-prose"
(pre :class "text-xs leading-relaxed whitespace-pre-wrap overflow-x-auto bg-stone-100 rounded border border-stone-200 p-4"
(when
(not (empty? registry-sample))
(div
:class "not-prose"
(pre
:class "text-xs leading-relaxed whitespace-pre-wrap overflow-x-auto bg-stone-100 rounded border border-stone-200 p-4"
(code (highlight registry-sample "lisp"))))))
(~docs/section
:title "How Client Routing Works"
:id "how"
(ol
:class "list-decimal pl-5 space-y-2 text-stone-700"
(li
(strong "Boot: ")
"boot.sx finds "
(code "<script type=\"text/sx-pages\">")
", calls "
(code "parse")
" on the SX content, then "
(code "parse-route-pattern")
" on each page's path to build "
(code "_page-routes")
".")
(li
(strong "Click: ")
"orchestration.sx intercepts boost link clicks via "
(code "bind-client-route-link")
". Extracts the pathname from the href.")
(li
(strong "Match: ")
(code "find-matching-route")
" from router.sx tests the pathname against all parsed patterns. Returns the first match with extracted URL params.")
(li
(strong "Check: ")
"If the matched page has "
(code ":has-data true")
", skip to server fetch. Otherwise proceed to client eval.")
(li
(strong "Eval: ")
(code "try-eval-content")
" merges the component env + URL params + closure, then parses and renders the content expression to DOM.")
(li
(strong "Swap: ")
"On success, the rendered DOM replaces "
(code "#sx-content")
" contents, "
(code "pushState")
" updates the URL, and the console logs "
(code "sx:route client /path")
".")
(li
(strong "Fallback: ")
"If anything fails (no match, eval error, missing component), the click falls through to a standard server fetch. Console logs "
(code "sx:route server /path")
". The user sees no difference.")))))
(~docs/section :title "How Client Routing Works" :id "how"
(ol :class "list-decimal pl-5 space-y-2 text-stone-700"
(li (strong "Boot: ") "boot.sx finds " (code "<script type=\"text/sx-pages\">") ", calls " (code "parse") " on the SX content, then " (code "parse-route-pattern") " on each page's path to build " (code "_page-routes") ".")
(li (strong "Click: ") "orchestration.sx intercepts boost link clicks via " (code "bind-client-route-link") ". Extracts the pathname from the href.")
(li (strong "Match: ") (code "find-matching-route") " from router.sx tests the pathname against all parsed patterns. Returns the first match with extracted URL params.")
(li (strong "Check: ") "If the matched page has " (code ":has-data true") ", skip to server fetch. Otherwise proceed to client eval.")
(li (strong "Eval: ") (code "try-eval-content") " merges the component env + URL params + closure, then parses and renders the content expression to DOM.")
(li (strong "Swap: ") "On success, the rendered DOM replaces " (code "#main-panel") " contents, " (code "pushState") " updates the URL, and the console logs " (code "sx:route client /path") ".")
(li (strong "Fallback: ") "If anything fails (no match, eval error, missing component), the click falls through to a standard server fetch. Console logs " (code "sx:route server /path") ". The user sees no difference.")))))
(defcomp ~routing-analyzer/routing-row (&key (name :as string) (path :as string) (mode :as string) (has-data :as boolean) (content-expr :as string?) (reason :as string?))
(div :class (str "rounded border p-3 flex items-center gap-3 "
(if (= mode "client")
"border-green-200 bg-green-50"
"border-amber-200 bg-amber-50"))
;; Mode badge
(span :class (str "inline-block px-2 py-0.5 rounded text-xs font-bold uppercase "
(if (= mode "client")
"bg-green-600 text-white"
"bg-amber-500 text-white"))
(defcomp
~routing-analyzer/routing-row
(&key
(name :as string)
(path :as string)
(mode :as string)
(has-data :as boolean)
(content-expr :as string?)
(reason :as string?))
(div
:class (str
"rounded border p-3 flex items-center gap-3 "
(if
(= mode "client")
"border-green-200 bg-green-50"
"border-amber-200 bg-amber-50"))
(span
:class (str
"inline-block px-2 py-0.5 rounded text-xs font-bold uppercase "
(if
(= mode "client")
"bg-green-600 text-white"
"bg-amber-500 text-white"))
mode)
;; Page info
(div :class "flex-1 min-w-0"
(div :class "flex items-center gap-2"
(div
:class "flex-1 min-w-0"
(div
:class "flex items-center gap-2"
(span :class "font-mono font-semibold text-stone-800 text-sm" name)
(span :class "text-stone-400 text-xs font-mono" path))
(when reason
(div :class "text-xs text-stone-500 mt-0.5" reason)))
;; Content expression
(when content-expr
(div :class "hidden md:block max-w-xs truncate"
(when reason (div :class "text-xs text-stone-500 mt-0.5" reason)))
(when
content-expr
(div
:class "hidden md:block max-w-xs truncate"
(code :class "text-xs text-stone-500" content-expr)))))

View File

@@ -1,256 +1,293 @@
;; ---------------------------------------------------------------------------
;; Spec Explorer — structured interactive view of SX spec files
;; ---------------------------------------------------------------------------
(defcomp ~specs-explorer/spec-explorer-content (&key data) :affinity :server
(~docs/page :title (str (get data "title") " — Explorer")
;; Header with filename and source link
(defcomp
~specs-explorer/spec-explorer-content
(&key data)
:affinity :server
(~docs/page
:title (str (get data "title") " — Explorer")
(~specs-explorer/spec-explorer-header
:filename (get data "filename")
:title (get data "title")
:desc (get data "desc")
:slug (replace (get data "filename") ".sx" ""))
;; Stats bar
(~specs-explorer/spec-explorer-stats :stats (get data "stats"))
;; Sections
(map (fn (section)
(~specs-explorer/spec-explorer-section :section section))
(map
(fn (section) (~specs-explorer/spec-explorer-section :section section))
(get data "sections"))
(when
(not (empty? (get data "platform-interface")))
(~specs-explorer/spec-platform-interface
:items (get data "platform-interface")))))
;; Platform interface
(when (not (empty? (get data "platform-interface")))
(~specs-explorer/spec-platform-interface :items (get data "platform-interface")))))
;; ---------------------------------------------------------------------------
;; Header
;; ---------------------------------------------------------------------------
(defcomp ~specs-explorer/spec-explorer-header (&key filename title desc slug)
(div :class "mb-6"
(div :class "flex items-center justify-between"
(defcomp
~specs-explorer/spec-explorer-header
(&key filename title desc slug)
(div
:class "mb-6"
(div
:class "flex items-center justify-between"
(div
(h1 :class "text-2xl font-bold text-stone-800" title)
(p :class "text-sm text-stone-500 mt-1" desc))
(a :href (str "/sx/(language.(spec." slug "))")
:sx-get (str "/sx/(language.(spec." slug "))")
:sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
:class "text-sm text-violet-600 hover:text-violet-800 font-medium"
(a
:href (str "/sx/(language.(spec." slug "))")
:sx-get (str "/sx/(language.(spec." slug "))")
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "text-sm text-violet-600 hover:text-violet-800 font-medium"
"View Source"))
(p :class "text-xs text-stone-400 font-mono mt-2" filename)))
;; ---------------------------------------------------------------------------
;; Stats bar
;; ---------------------------------------------------------------------------
(defcomp ~specs-explorer/spec-explorer-stats (&key stats)
(div :class "flex flex-wrap gap-2 mb-6 text-xs"
(span :class "bg-stone-100 text-stone-600 px-2 py-0.5 rounded font-medium"
(defcomp
~specs-explorer/spec-explorer-stats
(&key stats)
(div
:class "flex flex-wrap gap-2 mb-6 text-xs"
(span
:class "bg-stone-100 text-stone-600 px-2 py-0.5 rounded font-medium"
(str (get stats "total-defines") " defines"))
(when (> (get stats "pure-count") 0)
(span :class "bg-green-100 text-green-700 px-2 py-0.5 rounded"
(when
(> (get stats "pure-count") 0)
(span
:class "bg-green-100 text-green-700 px-2 py-0.5 rounded"
(str (get stats "pure-count") " pure")))
(when (> (get stats "mutation-count") 0)
(span :class "bg-amber-100 text-amber-700 px-2 py-0.5 rounded"
(when
(> (get stats "mutation-count") 0)
(span
:class "bg-amber-100 text-amber-700 px-2 py-0.5 rounded"
(str (get stats "mutation-count") " mutation")))
(when (> (get stats "io-count") 0)
(span :class "bg-orange-100 text-orange-700 px-2 py-0.5 rounded"
(when
(> (get stats "io-count") 0)
(span
:class "bg-orange-100 text-orange-700 px-2 py-0.5 rounded"
(str (get stats "io-count") " io")))
(when (> (get stats "render-count") 0)
(span :class "bg-sky-100 text-sky-700 px-2 py-0.5 rounded"
(when
(> (get stats "render-count") 0)
(span
:class "bg-sky-100 text-sky-700 px-2 py-0.5 rounded"
(str (get stats "render-count") " render")))
(when (> (get stats "test-total") 0)
(span :class "bg-violet-100 text-violet-700 px-2 py-0.5 rounded"
(when
(> (get stats "test-total") 0)
(span
:class "bg-violet-100 text-violet-700 px-2 py-0.5 rounded"
(str (get stats "test-total") " tests")))
(span :class "bg-stone-100 text-stone-500 px-2 py-0.5 rounded"
(span
:class "bg-stone-100 text-stone-500 px-2 py-0.5 rounded"
(str (get stats "lines") " lines"))))
;; ---------------------------------------------------------------------------
;; Section
;; ---------------------------------------------------------------------------
(defcomp ~specs-explorer/spec-explorer-section (&key section)
(div :class "mb-8"
(h2 :class "text-lg font-semibold text-stone-700 border-b border-stone-200 pb-1 mb-3"
(defcomp
~specs-explorer/spec-explorer-section
(&key section)
(div
:class "mb-8"
(h2
:class "text-lg font-semibold text-stone-700 border-b border-stone-200 pb-1 mb-3"
:id (replace (lower (get section "title")) " " "-")
(get section "title"))
(when (get section "comment")
(when
(get section "comment")
(p :class "text-sm text-stone-500 mb-3" (get section "comment")))
(div :class "space-y-4"
(map (fn (d) (~specs-explorer/spec-explorer-define :d d))
(div
:class "space-y-4"
(map
(fn (d) (~specs-explorer/spec-explorer-define :d d))
(get section "defines")))))
;; ---------------------------------------------------------------------------
;; Define card — one function/constant with all five rings
;; ---------------------------------------------------------------------------
(defcomp ~specs-explorer/spec-explorer-define (&key d)
(div :class "rounded border border-stone-200 p-4"
(defcomp
~specs-explorer/spec-explorer-define
(&key d)
(div
:class "rounded border border-stone-200 p-4"
:id (str "fn-" (get d "name"))
;; Name + effect badges
(div :class "flex items-center gap-2 flex-wrap"
(div
:class "flex items-center gap-2 flex-wrap"
(span :class "font-mono font-semibold text-stone-800" (get d "name"))
(span :class "text-xs text-stone-400" (get d "kind"))
(if (empty? (get d "effects"))
(span :class "text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700" "pure")
(map (fn (eff) (~specs-explorer/spec-effect-badge :effect eff))
(if
(empty? (get d "effects"))
(span
:class "text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700"
"pure")
(map
(fn (eff) (~specs-explorer/spec-effect-badge :effect eff))
(get d "effects"))))
;; Params
(when (not (empty? (get d "params")))
(when
(not (empty? (get d "params")))
(~specs-explorer/spec-param-list :params (get d "params")))
;; Ring 2: Translation panels (SX + Python + JavaScript + Z3)
(~specs-explorer/spec-ring-translations
:source (get d "source")
:python (get d "python")
:javascript (get d "javascript")
:z3 (get d "z3"))
;; Ring 3: Cross-references
(when (not (empty? (get d "refs")))
(when
(not (empty? (get d "refs")))
(~specs-explorer/spec-ring-bridge :refs (get d "refs")))
;; Ring 4: Tests
(when (> (get d "test-count") 0)
(when
(> (get d "test-count") 0)
(~specs-explorer/spec-ring-runtime
:tests (get d "tests")
:test-count (get d "test-count")))))
;; ---------------------------------------------------------------------------
;; Effect badge
;; ---------------------------------------------------------------------------
(defcomp ~specs-explorer/spec-effect-badge (&key effect)
(span :class (str "text-xs px-1.5 py-0.5 rounded "
(case effect
"mutation" "bg-amber-100 text-amber-700"
"io" "bg-orange-100 text-orange-700"
"render" "bg-sky-100 text-sky-700"
:else "bg-stone-100 text-stone-500"))
(defcomp
~specs-explorer/spec-effect-badge
(&key effect)
(span
:class (str
"text-xs px-1.5 py-0.5 rounded "
(case
effect
"mutation"
"bg-amber-100 text-amber-700"
"io"
"bg-orange-100 text-orange-700"
"render"
"bg-sky-100 text-sky-700"
:else "bg-stone-100 text-stone-500"))
effect))
;; ---------------------------------------------------------------------------
;; Param list
;; ---------------------------------------------------------------------------
(defcomp ~specs-explorer/spec-param-list (&key params)
(div :class "mt-1 flex flex-wrap gap-1"
(map (fn (p)
(let ((name (get p "name"))
(typ (get p "type")))
(if (or (= name "&rest") (= name "&key"))
(span :class "text-xs font-mono text-violet-500" name)
(span :class "text-xs font-mono px-1 py-0.5 rounded bg-stone-50 border border-stone-200"
(if typ
(<> (span :class "text-stone-700" name)
(defcomp
~specs-explorer/spec-param-list
(&key params)
(div
:class "mt-1 flex flex-wrap gap-1"
(map
(fn
(p)
(let
((name (get p "name")) (typ (get p "type")))
(if
(or (= name "&rest") (= name "&key"))
(span :class "text-xs font-mono text-violet-500" name)
(span
:class "text-xs font-mono px-1 py-0.5 rounded bg-stone-50 border border-stone-200"
(if
typ
(<>
(span :class "text-stone-700" name)
(span :class "text-stone-400" " : ")
(span :class "text-violet-600" typ))
(span :class "text-stone-700" name))))))
(span :class "text-stone-700" name))))))
params)))
;; ---------------------------------------------------------------------------
;; Ring 2: Translation panels (nucleus + bootstrapper)
;; ---------------------------------------------------------------------------
(defcomp ~specs-explorer/spec-ring-translations (&key source python javascript z3)
(when (not (= source ""))
(div :class "mt-3 border border-stone-200 rounded-lg overflow-hidden"
;; SX source — Ring 1: the nucleus (always open)
(details :open "true"
(summary :class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer"
(defcomp
~specs-explorer/spec-ring-translations
(&key source python javascript z3)
(when
(not (= source ""))
(div
:class "mt-3 border border-stone-200 rounded-lg overflow-hidden"
(details
:open "true"
(summary
:class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer"
"SX")
(pre :class "text-xs p-3 overflow-x-auto bg-white"
(pre
:class "text-xs p-3 overflow-x-auto bg-white"
(code (highlight source "sx"))))
;; Python — Ring 2: bootstrapper
(when python
(when
python
(details
(summary :class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer border-t border-stone-200"
(summary
:class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer border-t border-stone-200"
"Python")
(pre :class "text-xs p-3 overflow-x-auto bg-white"
(pre
:class "text-xs p-3 overflow-x-auto bg-white"
(code (highlight python "python")))))
;; JavaScript — Ring 2: bootstrapper
(when javascript
(when
javascript
(details
(summary :class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer border-t border-stone-200"
(summary
:class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer border-t border-stone-200"
"JavaScript")
(pre :class "text-xs p-3 overflow-x-auto bg-white"
(pre
:class "text-xs p-3 overflow-x-auto bg-white"
(code (highlight javascript "javascript")))))
;; Z3 / SMT-LIB — Ring 2: formal translation
(when z3
(when
z3
(details
(summary :class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer border-t border-stone-200"
(summary
:class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer border-t border-stone-200"
"Z3 / SMT-LIB")
(pre :class "text-xs p-3 overflow-x-auto bg-white"
(pre
:class "text-xs p-3 overflow-x-auto bg-white"
(code (highlight z3 "lisp"))))))))
;; ---------------------------------------------------------------------------
;; Ring 3: Cross-references (bridge)
;; ---------------------------------------------------------------------------
(defcomp ~specs-explorer/spec-ring-bridge (&key refs)
(div :class "mt-2"
(defcomp
~specs-explorer/spec-ring-bridge
(&key refs)
(div
:class "mt-2"
(span :class "text-xs font-medium text-stone-500" "References")
(div :class "flex flex-wrap gap-1 mt-1"
(map (fn (ref)
(a :href (str "#fn-" ref)
:class "text-xs px-1.5 py-0.5 rounded bg-stone-100 text-stone-600 font-mono hover:bg-stone-200"
ref))
(div
:class "flex flex-wrap gap-1 mt-1"
(map
(fn
(ref)
(a
:href (str "#fn-" ref)
:class "text-xs px-1.5 py-0.5 rounded bg-stone-100 text-stone-600 font-mono hover:bg-stone-200"
ref))
refs))))
;; ---------------------------------------------------------------------------
;; Ring 4: Tests (runtime)
;; ---------------------------------------------------------------------------
(defcomp ~specs-explorer/spec-ring-runtime (&key tests test-count)
(div :class "mt-2"
(div :class "flex items-center gap-1"
(defcomp
~specs-explorer/spec-ring-runtime
(&key tests test-count)
(div
:class "mt-2"
(div
:class "flex items-center gap-1"
(span :class "text-xs font-medium text-stone-500" "Tests")
(span :class "text-xs px-1.5 py-0.5 rounded bg-violet-100 text-violet-700"
(span
:class "text-xs px-1.5 py-0.5 rounded bg-violet-100 text-violet-700"
(str test-count)))
(ul :class "mt-1 text-xs text-stone-500 list-none"
(map (fn (t)
(li :class "flex items-center gap-1"
(span :class "text-green-500 text-xs" "●")
(get t "name")))
(ul
:class "mt-1 text-xs text-stone-500 list-none"
(map
(fn
(t)
(li
:class "flex items-center gap-1"
(span :class "text-green-500 text-xs" "●")
(get t "name")))
tests))))
;; ---------------------------------------------------------------------------
;; Platform interface table (Ring 3 overview)
;; ---------------------------------------------------------------------------
(defcomp ~specs-explorer/spec-platform-interface (&key items)
(div :class "mt-8"
(h2 :class "text-lg font-semibold text-stone-700 border-b border-stone-200 pb-1 mb-3"
(defcomp
~specs-explorer/spec-platform-interface
(&key items)
(div
:class "mt-8"
(h2
:class "text-lg font-semibold text-stone-700 border-b border-stone-200 pb-1 mb-3"
"Platform Interface")
(p :class "text-sm text-stone-500 mb-3"
(p
:class "text-sm text-stone-500 mb-3"
"Functions the host platform must provide.")
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-50"
(th :class "px-3 py-2 font-medium text-stone-600" "Name")
(th :class "px-3 py-2 font-medium text-stone-600" "Params")
(th :class "px-3 py-2 font-medium text-stone-600" "Returns")
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
(div
:class "overflow-x-auto rounded border border-stone-200"
(table
:class "w-full text-left text-sm"
(thead
(tr
:class "border-b border-stone-200 bg-stone-50"
(th :class "px-3 py-2 font-medium text-stone-600" "Name")
(th :class "px-3 py-2 font-medium text-stone-600" "Params")
(th :class "px-3 py-2 font-medium text-stone-600" "Returns")
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
(tbody
(map (fn (item)
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" (get item "name"))
(td :class "px-3 py-2 font-mono text-xs text-stone-500" (get item "params"))
(td :class "px-3 py-2 font-mono text-xs text-stone-500" (get item "returns"))
(td :class "px-3 py-2 text-stone-600" (get item "doc"))))
(map
(fn
(item)
(tr
:class "border-b border-stone-100"
(td
:class "px-3 py-2 font-mono text-sm text-violet-700"
(get item "name"))
(td
:class "px-3 py-2 font-mono text-xs text-stone-500"
(get item "params"))
(td
:class "px-3 py-2 font-mono text-xs text-stone-500"
(get item "returns"))
(td :class "px-3 py-2 text-stone-600" (get item "doc"))))
items))))))

File diff suppressed because it is too large Load Diff

View File

@@ -74,8 +74,8 @@
(a
:href href
:sx-get href
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "text-violet-700 hover:text-violet-900 underline"
@@ -116,8 +116,8 @@
(a
:href (nth item 1)
:sx-get (nth item 1)
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
:class (str

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff