Add Specs section, Reflexive Web essay, fix highlight and dev caching
- Fix highlight() returning SxExpr so syntax-highlighted code renders as DOM elements instead of leaking SX source text into the page - Add Specs section that reads and displays canonical SX spec files from shared/sx/ref/ with syntax highlighting - Add "The Reflexive Web" essay on SX becoming a complete LISP with AI as native participant - Change logo from (<x>) to (<sx>) everywhere - Unify all backgrounds to bg-stone-100, center code blocks - Skip component/style cookie cache in dev mode so .sx edits are visible immediately on refresh without clearing localStorage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -627,8 +627,9 @@ def sx_page(ctx: dict, page_sx: str, *,
|
||||
component_hash = get_component_hash()
|
||||
|
||||
# Check if client already has this version cached (via cookie)
|
||||
# In dev mode, always send full source so edits are visible immediately
|
||||
client_hash = _get_sx_comp_cookie()
|
||||
if client_hash and client_hash == component_hash:
|
||||
if not _is_dev_mode() and client_hash and client_hash == component_hash:
|
||||
# Client has current components cached — send empty source
|
||||
component_defs = ""
|
||||
else:
|
||||
@@ -675,7 +676,7 @@ def sx_page(ctx: dict, page_sx: str, *,
|
||||
# Style dictionary for client-side css primitive
|
||||
styles_hash = _get_style_dict_hash()
|
||||
client_styles_hash = _get_sx_styles_cookie()
|
||||
if client_styles_hash and client_styles_hash == styles_hash:
|
||||
if not _is_dev_mode() and client_styles_hash and client_styles_hash == styles_hash:
|
||||
styles_json = "" # Client has cached version
|
||||
else:
|
||||
styles_json = _build_style_dict_json()
|
||||
|
||||
@@ -236,14 +236,15 @@ def _tokenize_bash(code: str) -> list[tuple[str, str]]:
|
||||
return tokens
|
||||
|
||||
|
||||
def highlight(code: str, language: str = "lisp") -> str:
|
||||
"""Highlight code in the given language. Returns sx source."""
|
||||
def highlight(code: str, language: str = "lisp"):
|
||||
"""Highlight code in the given language. Returns SxExpr for wire format."""
|
||||
from shared.sx.parser import SxExpr
|
||||
if language in ("lisp", "sx", "sexp"):
|
||||
return highlight_sx(code)
|
||||
return SxExpr(highlight_sx(code))
|
||||
elif language in ("python", "py"):
|
||||
return highlight_python(code)
|
||||
return SxExpr(highlight_python(code))
|
||||
elif language in ("bash", "sh", "shell"):
|
||||
return highlight_bash(code)
|
||||
return SxExpr(highlight_bash(code))
|
||||
# Fallback: no highlighting, just escaped text
|
||||
escaped = code.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'(span "{escaped}")'
|
||||
return SxExpr(f'(span "{escaped}")')
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
(defcomp ~doc-placeholder (&key id)
|
||||
(div :id id
|
||||
(div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3"
|
||||
(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 ~doc-oob-code (&key target-id text)
|
||||
(div :id target-id :sx-swap-oob "innerHTML"
|
||||
(div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3 overflow-x-auto"
|
||||
(pre :class "text-sm whitespace-pre-wrap"
|
||||
(div :class "bg-stone-100 rounded p-4 mt-3"
|
||||
(pre :class "text-sm whitespace-pre-wrap break-words"
|
||||
(code text)))))
|
||||
|
||||
(defcomp ~doc-attr-table (&key title rows)
|
||||
@@ -17,7 +17,7 @@
|
||||
(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-50"
|
||||
(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?")))
|
||||
@@ -28,7 +28,7 @@
|
||||
(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-50"
|
||||
(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")))
|
||||
@@ -51,13 +51,13 @@
|
||||
(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-50"
|
||||
(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 ~sx-docs-label ()
|
||||
(span :class "font-mono" "(<x>)"))
|
||||
(span :class "font-mono" "(<sx>)"))
|
||||
|
||||
(defcomp ~doc-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)"
|
||||
|
||||
103
sx/sx/essays.sx
103
sx/sx/essays.sx
File diff suppressed because one or more lines are too long
@@ -11,7 +11,8 @@
|
||||
(dict :label "Reference" :href "/reference/")
|
||||
(dict :label "Protocols" :href "/protocols/wire-format")
|
||||
(dict :label "Examples" :href "/examples/click-to-load")
|
||||
(dict :label "Essays" :href "/essays/sx-sucks"))))
|
||||
(dict :label "Essays" :href "/essays/sx-sucks")
|
||||
(dict :label "Specs" :href "/specs/core"))))
|
||||
(<> (map (lambda (item)
|
||||
(~nav-link
|
||||
:href (get item "href")
|
||||
|
||||
@@ -64,7 +64,15 @@
|
||||
(dict :label "The SX Manifesto" :href "/essays/sx-manifesto")
|
||||
(dict :label "Tail-Call Optimization" :href "/essays/tail-call-optimization")
|
||||
(dict :label "Continuations" :href "/essays/continuations")
|
||||
(dict :label "Godel, Escher, Bach" :href "/essays/godel-escher-bach")))
|
||||
(dict :label "Godel, Escher, Bach" :href "/essays/godel-escher-bach")
|
||||
(dict :label "The Reflexive Web" :href "/essays/reflexive-web")))
|
||||
|
||||
(define specs-nav-items (list
|
||||
(dict :label "Core" :href "/specs/core")
|
||||
(dict :label "Parser" :href "/specs/parser")
|
||||
(dict :label "Evaluator" :href "/specs/evaluator")
|
||||
(dict :label "Primitives" :href "/specs/primitives")
|
||||
(dict :label "Renderer" :href "/specs/renderer")))
|
||||
|
||||
;; Find the current nav label for a slug by matching href suffix.
|
||||
;; Returns the label string or nil if no match.
|
||||
|
||||
36
sx/sx/specs.sx
Normal file
36
sx/sx/specs.sx
Normal file
@@ -0,0 +1,36 @@
|
||||
;; Spec viewer components — display canonical SX specification source
|
||||
|
||||
(defcomp ~spec-core-content (&key spec-files)
|
||||
(~doc-page :title "SX Core Specification"
|
||||
(p :class "text-stone-600 mb-6"
|
||||
"SX is defined in SX. These four files constitute the canonical, self-hosting specification of the language. Each file is both documentation and executable definition — bootstrap compilers read them to generate native implementations.")
|
||||
(div :class "space-y-8"
|
||||
(map (fn (spec)
|
||||
(div :class "space-y-3"
|
||||
(div :class "flex items-baseline gap-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800"
|
||||
(a :href (get spec "href")
|
||||
:sx-get (get spec "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"
|
||||
(get spec "title")))
|
||||
(span :class "text-sm text-stone-400 font-mono" (get spec "filename")))
|
||||
(p :class "text-stone-600" (get spec "desc"))
|
||||
(div :class "bg-stone-100 rounded-lg p-5 max-h-72 overflow-y-auto"
|
||||
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
|
||||
(code (highlight (get spec "source") "sx"))))))
|
||||
spec-files))))
|
||||
|
||||
(defcomp ~spec-detail-content (&key spec-title spec-desc spec-filename spec-source)
|
||||
(~doc-page :title spec-title
|
||||
(div :class "flex items-baseline gap-3 mb-4"
|
||||
(span :class "text-sm text-stone-400 font-mono" spec-filename)
|
||||
(span :class "text-sm text-stone-500" spec-desc))
|
||||
(div :class "bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl"
|
||||
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words"
|
||||
(code (highlight spec-source "sx"))))))
|
||||
|
||||
(defcomp ~spec-not-found (&key slug)
|
||||
(~doc-page :title "Spec Not Found"
|
||||
(p :class "text-stone-600"
|
||||
"No specification found for \"" slug "\". This spec may not exist yet.")))
|
||||
@@ -16,8 +16,8 @@
|
||||
children))
|
||||
|
||||
(defcomp ~doc-code (&key code)
|
||||
(div :class "bg-stone-50 border border-stone-200 rounded-lg p-4 overflow-x-auto"
|
||||
(pre :class "text-sm" (code code))))
|
||||
(div :class "bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl"
|
||||
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code))))
|
||||
|
||||
(defcomp ~doc-note (&key &rest children)
|
||||
(div :class "border-l-4 border-violet-400 bg-violet-50 p-4 text-stone-700 text-sm"
|
||||
@@ -27,7 +27,7 @@
|
||||
(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"
|
||||
(tr :class "border-b border-stone-200 bg-stone-100"
|
||||
(map (fn (h) (th :class "px-3 py-2 font-medium text-stone-600" h)) headers)))
|
||||
(tbody
|
||||
(map (fn (row)
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
|
||||
(defcomp ~example-card (&key title description &rest children)
|
||||
(div :class "border border-stone-200 rounded-lg overflow-hidden"
|
||||
(div :class "bg-stone-50 px-4 py-3 border-b border-stone-200"
|
||||
(div :class "bg-stone-100 px-4 py-3 border-b border-stone-200"
|
||||
(h3 :class "font-semibold text-stone-800" title)
|
||||
(when description
|
||||
(p :class "text-sm text-stone-500 mt-1" description)))
|
||||
(div :class "p-4" children)))
|
||||
|
||||
(defcomp ~example-demo (&key &rest children)
|
||||
(div :class "border border-dashed border-stone-300 rounded p-4 bg-white" children))
|
||||
(div :class "border border-dashed border-stone-300 rounded p-4 bg-stone-100" children))
|
||||
|
||||
(defcomp ~example-source (&key code)
|
||||
(div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3 overflow-x-auto"
|
||||
(pre :class "text-sm" (code code))))
|
||||
(div :class "bg-stone-100 rounded p-5 mt-3 mx-auto max-w-3xl"
|
||||
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code))))
|
||||
|
||||
;; --- Click to load demo ---
|
||||
|
||||
(defcomp ~click-to-load-demo ()
|
||||
(div :class "space-y-4"
|
||||
(div :id "click-result" :class "p-4 rounded bg-stone-50 text-stone-500 text-center"
|
||||
(div :id "click-result" :class "p-4 rounded bg-stone-100 text-stone-500 text-center"
|
||||
"Click the button to load content.")
|
||||
(button
|
||||
:sx-get "/examples/api/click"
|
||||
@@ -50,7 +50,7 @@
|
||||
(button :type "submit"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Submit"))
|
||||
(div :id "form-result" :class "p-3 rounded bg-stone-50 text-stone-500 text-sm text-center"
|
||||
(div :id "form-result" :class "p-3 rounded bg-stone-100 text-stone-500 text-sm text-center"
|
||||
"Submit the form to see the result.")))
|
||||
|
||||
(defcomp ~form-result (&key name)
|
||||
@@ -66,7 +66,7 @@
|
||||
:sx-get "/examples/api/poll"
|
||||
:sx-trigger "load, every 2s"
|
||||
:sx-swap "innerHTML"
|
||||
:class "p-4 rounded border border-stone-200 bg-white text-center font-mono"
|
||||
:class "p-4 rounded border border-stone-200 bg-stone-100 text-center font-mono"
|
||||
"Loading...")))
|
||||
|
||||
(defcomp ~poll-result (&key time count)
|
||||
@@ -145,10 +145,10 @@
|
||||
(defcomp ~oob-demo ()
|
||||
(div :class "space-y-4"
|
||||
(div :class "grid grid-cols-2 gap-4"
|
||||
(div :id "oob-box-a" :class "p-4 rounded border border-stone-200 bg-white text-center"
|
||||
(div :id "oob-box-a" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
|
||||
(p :class "text-stone-500" "Box A")
|
||||
(p :class "text-sm text-stone-400" "Waiting..."))
|
||||
(div :id "oob-box-b" :class "p-4 rounded border border-stone-200 bg-white text-center"
|
||||
(div :id "oob-box-b" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
|
||||
(p :class "text-stone-500" "Box B")
|
||||
(p :class "text-sm text-stone-400" "Waiting...")))
|
||||
(button
|
||||
@@ -167,7 +167,7 @@
|
||||
:sx-get "/examples/api/lazy"
|
||||
:sx-trigger "load"
|
||||
:sx-swap "innerHTML"
|
||||
:class "p-6 rounded border border-stone-200 bg-stone-50 text-center"
|
||||
:class "p-6 rounded border border-stone-200 bg-stone-100 text-center"
|
||||
(div :class "animate-pulse space-y-2"
|
||||
(div :class "h-4 bg-stone-200 rounded w-3/4 mx-auto")
|
||||
(div :class "h-4 bg-stone-200 rounded w-1/2 mx-auto")))))
|
||||
@@ -328,7 +328,7 @@
|
||||
(p :class "text-sm text-stone-400" "Messages will appear here."))))
|
||||
|
||||
(defcomp ~reset-message (&key message time)
|
||||
(div :class "px-3 py-2 bg-stone-50 rounded text-sm text-stone-700"
|
||||
(div :class "px-3 py-2 bg-stone-100 rounded text-sm text-stone-700"
|
||||
(str "[" time "] " message)))
|
||||
|
||||
;; --- Edit row demo ---
|
||||
@@ -488,7 +488,7 @@
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-3 py-1.5 bg-stone-600 text-white rounded text-sm hover:bg-stone-700"
|
||||
"Full Dashboard"))
|
||||
(div :id "filter-target" :class "border border-stone-200 rounded p-4 bg-white"
|
||||
(div :id "filter-target" :class "border border-stone-200 rounded p-4 bg-stone-100"
|
||||
(p :class "text-sm text-stone-400" "Click a button to load content."))))
|
||||
|
||||
;; --- Tabs demo ---
|
||||
@@ -525,7 +525,7 @@
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Load with animation")
|
||||
(div :id "anim-target" :class "p-4 rounded border border-stone-200 bg-white text-center"
|
||||
(div :id "anim-target" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
|
||||
(p :class "text-stone-400" "Content will fade in here."))))
|
||||
|
||||
(defcomp ~anim-result (&key color time)
|
||||
@@ -552,7 +552,7 @@
|
||||
:sx-get "/examples/api/dialog/close"
|
||||
:sx-target "#dialog-container"
|
||||
:sx-swap "innerHTML")
|
||||
(div :class "relative bg-white rounded-lg shadow-xl p-6 max-w-md w-full mx-4 space-y-4"
|
||||
(div :class "relative bg-stone-100 rounded-lg shadow-xl p-6 max-w-md w-full mx-4 space-y-4"
|
||||
(h3 :class "text-lg font-semibold text-stone-800" title)
|
||||
(p :class "text-stone-600" message)
|
||||
(div :class "flex justify-end gap-2"
|
||||
@@ -573,23 +573,23 @@
|
||||
|
||||
(defcomp ~keyboard-shortcuts-demo ()
|
||||
(div :class "space-y-4"
|
||||
(div :class "p-4 rounded border border-stone-200 bg-stone-50"
|
||||
(div :class "p-4 rounded border border-stone-200 bg-stone-100"
|
||||
(p :class "text-sm text-stone-600 font-medium mb-2" "Keyboard shortcuts:")
|
||||
(div :class "flex gap-4"
|
||||
(div :class "flex items-center gap-1"
|
||||
(kbd :class "px-2 py-0.5 bg-white border border-stone-300 rounded text-xs font-mono" "s")
|
||||
(kbd :class "px-2 py-0.5 bg-stone-100 border border-stone-300 rounded text-xs font-mono" "s")
|
||||
(span :class "text-sm text-stone-500" "Search"))
|
||||
(div :class "flex items-center gap-1"
|
||||
(kbd :class "px-2 py-0.5 bg-white border border-stone-300 rounded text-xs font-mono" "n")
|
||||
(kbd :class "px-2 py-0.5 bg-stone-100 border border-stone-300 rounded text-xs font-mono" "n")
|
||||
(span :class "text-sm text-stone-500" "New item"))
|
||||
(div :class "flex items-center gap-1"
|
||||
(kbd :class "px-2 py-0.5 bg-white border border-stone-300 rounded text-xs font-mono" "h")
|
||||
(kbd :class "px-2 py-0.5 bg-stone-100 border border-stone-300 rounded text-xs font-mono" "h")
|
||||
(span :class "text-sm text-stone-500" "Help"))))
|
||||
(div :id "kbd-target"
|
||||
:sx-get "/examples/api/keyboard?key=s"
|
||||
:sx-trigger "keyup[key=='s'&&!event.target.matches('input,textarea')] from:body"
|
||||
:sx-swap "innerHTML"
|
||||
:class "p-4 rounded border border-stone-200 bg-white text-center"
|
||||
:class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
|
||||
(p :class "text-stone-400 text-sm" "Press a shortcut key..."))
|
||||
(div :sx-get "/examples/api/keyboard?key=n"
|
||||
:sx-trigger "keyup[key=='n'&&!event.target.matches('input,textarea')] from:body"
|
||||
@@ -675,7 +675,7 @@
|
||||
(button :type "submit"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Submit as JSON"))
|
||||
(div :id "json-result" :class "p-3 rounded bg-stone-50 text-stone-500 text-sm"
|
||||
(div :id "json-result" :class "p-3 rounded bg-stone-100 text-stone-500 text-sm"
|
||||
"Submit the form to see the server echo the parsed JSON.")))
|
||||
|
||||
(defcomp ~json-result (&key body content-type)
|
||||
@@ -697,7 +697,7 @@
|
||||
:sx-vals "{\"source\": \"button\", \"version\": \"2.0\"}"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Send with vals")
|
||||
(div :id "vals-result" :class "p-3 rounded bg-stone-50 text-sm text-stone-400"
|
||||
(div :id "vals-result" :class "p-3 rounded bg-stone-100 text-sm text-stone-400"
|
||||
"Click to see server-received values."))
|
||||
(div :class "space-y-2"
|
||||
(h4 :class "text-sm font-semibold text-stone-700" "sx-headers — send custom headers")
|
||||
@@ -708,7 +708,7 @@
|
||||
:sx-headers {:X-Custom-Token "abc123" :X-Request-Source "demo"}
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Send with headers")
|
||||
(div :id "headers-result" :class "p-3 rounded bg-stone-50 text-sm text-stone-400"
|
||||
(div :id "headers-result" :class "p-3 rounded bg-stone-100 text-sm text-stone-400"
|
||||
"Click to see server-received headers."))))
|
||||
|
||||
(defcomp ~echo-result (&key label items)
|
||||
@@ -729,7 +729,7 @@
|
||||
:class "sx-loading-btn px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm flex items-center gap-2"
|
||||
(span :class "sx-spinner w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin")
|
||||
(span "Load slow endpoint"))
|
||||
(div :id "loading-result" :class "p-4 rounded border border-stone-200 bg-white text-center"
|
||||
(div :id "loading-result" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
|
||||
(p :class "text-stone-400 text-sm" "Click the button — it takes 2 seconds."))))
|
||||
|
||||
(defcomp ~loading-result (&key time)
|
||||
@@ -749,7 +749,7 @@
|
||||
:sx-sync "replace"
|
||||
:placeholder "Type to search (random delay 0.5-2s)..."
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
||||
(div :id "sync-result" :class "p-4 rounded border border-stone-200 bg-white"
|
||||
(div :id "sync-result" :class "p-4 rounded border border-stone-200 bg-stone-100"
|
||||
(p :class "text-sm text-stone-400" "Type to trigger requests — stale ones get aborted."))))
|
||||
|
||||
(defcomp ~sync-result (&key query delay)
|
||||
@@ -768,7 +768,7 @@
|
||||
:sx-retry "exponential:1000:8000"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Call flaky endpoint")
|
||||
(div :id "retry-result" :class "p-4 rounded border border-stone-200 bg-white text-center"
|
||||
(div :id "retry-result" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
|
||||
(p :class "text-stone-400 text-sm" "Endpoint fails twice, succeeds on 3rd attempt."))))
|
||||
|
||||
(defcomp ~retry-result (&key attempt message)
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
(div :class "p-3 bg-amber-50 rounded text-center"
|
||||
(p :class "text-2xl font-bold text-amber-700" "$4.2k")
|
||||
(p :class "text-xs text-amber-600" "Revenue")))
|
||||
(div :id "dash-footer" :class "p-3 bg-stone-50 rounded"
|
||||
(div :id "dash-footer" :class "p-3 bg-stone-100 rounded"
|
||||
(p :class "text-sm text-stone-500" "Last updated: " now)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
(defcomp ~sx-hero (&key &rest children)
|
||||
(div :class "max-w-4xl mx-auto px-6 py-16 text-center"
|
||||
(h1 :class "text-5xl font-bold text-stone-900 mb-4"
|
||||
(span :class "text-violet-600 font-mono" "(<x>)"))
|
||||
(span :class "text-violet-600 font-mono" "(<sx>)"))
|
||||
(p :class "text-2xl text-stone-600 mb-8"
|
||||
"s-expressions for the web")
|
||||
(p :class "text-lg text-stone-500 max-w-2xl mx-auto mb-12"
|
||||
"A hypermedia-driven UI engine that combines htmx's server-first philosophy "
|
||||
"with React's component model. S-expressions over the wire — no HTML, no JavaScript frameworks.")
|
||||
(div :class "bg-stone-50 border border-stone-200 rounded-lg p-6 text-left font-mono text-sm overflow-x-auto"
|
||||
(pre :class "leading-relaxed" children))))
|
||||
(div :class "bg-stone-100 rounded-lg p-6 text-left font-mono text-sm mx-auto max-w-2xl"
|
||||
(pre :class "leading-relaxed whitespace-pre-wrap" children))))
|
||||
|
||||
(defcomp ~sx-philosophy ()
|
||||
(div :class "max-w-4xl mx-auto px-6 py-12"
|
||||
|
||||
@@ -240,4 +240,42 @@
|
||||
"tail-call-optimization" (~essay-tail-call-optimization)
|
||||
"continuations" (~essay-continuations)
|
||||
"godel-escher-bach" (~essay-godel-escher-bach)
|
||||
"reflexive-web" (~essay-reflexive-web)
|
||||
:else (~essay-sx-sucks)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Specs section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage specs-index
|
||||
:path "/specs/"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Specs"
|
||||
:sub-label "Specs"
|
||||
:sub-href "/specs/core"
|
||||
:sub-nav (~section-nav :items specs-nav-items :current "Core")
|
||||
:selected "Core")
|
||||
:data (spec-data "core")
|
||||
:content (~spec-core-content :spec-files spec-files))
|
||||
|
||||
(defpage specs-page
|
||||
:path "/specs/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Specs"
|
||||
:sub-label "Specs"
|
||||
:sub-href "/specs/core"
|
||||
:sub-nav (~section-nav :items specs-nav-items
|
||||
:current (find-current specs-nav-items slug))
|
||||
:selected (or (find-current specs-nav-items slug) ""))
|
||||
:data (spec-data slug)
|
||||
:content (if spec-not-found
|
||||
(~spec-not-found :slug slug)
|
||||
(case slug
|
||||
"core" (~spec-core-content :spec-files spec-files)
|
||||
:else (~spec-detail-content
|
||||
:spec-title spec-title
|
||||
:spec-desc spec-desc
|
||||
:spec-filename spec-filename
|
||||
:spec-source spec-source))))
|
||||
|
||||
@@ -16,6 +16,7 @@ def _register_sx_helpers() -> None:
|
||||
"primitives-data": _primitives_data,
|
||||
"reference-data": _reference_data,
|
||||
"attr-detail-data": _attr_detail_data,
|
||||
"spec-data": _spec_data,
|
||||
})
|
||||
|
||||
|
||||
@@ -103,6 +104,63 @@ def _reference_data(slug: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
_SPEC_FILES = {
|
||||
"parser": ("parser.sx", "Parser", "Tokenization and parsing of SX source text into AST."),
|
||||
"evaluator": ("eval.sx", "Evaluator", "Tree-walking evaluation of SX expressions."),
|
||||
"primitives": ("primitives.sx", "Primitives", "All built-in pure functions and their signatures."),
|
||||
"renderer": ("render.sx", "Renderer", "Rendering evaluated expressions to DOM, HTML, or SX wire format."),
|
||||
}
|
||||
|
||||
|
||||
def _spec_data(slug: str) -> dict:
|
||||
"""Return spec file source and highlighted version for display."""
|
||||
import os
|
||||
from content.highlight import highlight as _highlight
|
||||
|
||||
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref")
|
||||
# Normalise — inside container shared is at /app/shared
|
||||
if not os.path.isdir(ref_dir):
|
||||
ref_dir = "/app/shared/sx/ref"
|
||||
|
||||
base = {"spec-not-found": None, "spec-title": None, "spec-desc": None,
|
||||
"spec-filename": None, "spec-source": None, "spec-files": None}
|
||||
|
||||
if slug == "core":
|
||||
specs = []
|
||||
for key in ("parser", "evaluator", "primitives", "renderer"):
|
||||
filename, title, desc = _SPEC_FILES[key]
|
||||
filepath = os.path.join(ref_dir, filename)
|
||||
source = _read_spec(filepath)
|
||||
specs.append({
|
||||
"title": title,
|
||||
"desc": desc,
|
||||
"filename": filename,
|
||||
"source": source,
|
||||
"href": f"/specs/{key}",
|
||||
})
|
||||
return {**base, "spec-title": "SX Core Specification", "spec-files": specs}
|
||||
|
||||
info = _SPEC_FILES.get(slug)
|
||||
if not info:
|
||||
return {**base, "spec-not-found": True}
|
||||
|
||||
filename, title, desc = info
|
||||
filepath = os.path.join(ref_dir, filename)
|
||||
source = _read_spec(filepath)
|
||||
return {**base,
|
||||
"spec-title": title, "spec-desc": desc,
|
||||
"spec-filename": filename, "spec-source": source}
|
||||
|
||||
|
||||
def _read_spec(filepath: str) -> str:
|
||||
"""Read a spec file, returning empty string if missing."""
|
||||
try:
|
||||
with open(filepath, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
return ";; spec file not found"
|
||||
|
||||
|
||||
def _attr_detail_data(slug: str) -> dict:
|
||||
"""Return attribute detail data for a specific attribute slug.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Load server time")
|
||||
(div :id "ref-get-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Click to load.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -36,7 +36,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Greet"))
|
||||
(div :id "ref-post-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Submit to see greeting.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
(defcomp ~ref-put-demo ()
|
||||
(div :id "ref-put-view"
|
||||
(div :class "flex items-center justify-between p-3 bg-stone-50 rounded"
|
||||
(div :class "flex items-center justify-between p-3 bg-stone-100 rounded"
|
||||
(span :class "text-stone-700 text-sm" "Status: " (strong "draft"))
|
||||
(button
|
||||
:sx-put "/reference/api/status"
|
||||
@@ -83,7 +83,7 @@
|
||||
|
||||
(defcomp ~ref-patch-demo ()
|
||||
(div :id "ref-patch-view" :class "space-y-2"
|
||||
(div :class "p-3 bg-stone-50 rounded"
|
||||
(div :class "p-3 bg-stone-100 rounded"
|
||||
(span :class "text-stone-700 text-sm" "Theme: " (strong :id "ref-patch-val" "light")))
|
||||
(div :class "flex gap-2"
|
||||
(button :sx-patch "/reference/api/theme"
|
||||
@@ -93,7 +93,7 @@
|
||||
(button :sx-patch "/reference/api/theme"
|
||||
:sx-vals "{\"theme\": \"light\"}"
|
||||
:sx-target "#ref-patch-val" :sx-swap "innerHTML"
|
||||
:class "px-3 py-1 bg-white border border-stone-300 text-stone-700 rounded text-sm" "Light"))))
|
||||
:class "px-3 py-1 bg-stone-100 border border-stone-300 text-stone-700 rounded text-sm" "Light"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; sx-trigger
|
||||
@@ -108,7 +108,7 @@
|
||||
:sx-swap "innerHTML"
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
||||
(div :id "ref-trigger-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Start typing to trigger a search.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -186,7 +186,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Load (selecting #the-content)")
|
||||
(div :id "ref-select-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Only the selected fragment will appear here.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -242,7 +242,7 @@
|
||||
(p :class "text-xs text-stone-400"
|
||||
"With sync:replace, each new keystroke aborts the in-flight request.")
|
||||
(div :id "ref-sync-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Type to see only the latest result.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -262,7 +262,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Upload"))
|
||||
(div :id "ref-encoding-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Select a file and submit.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -278,7 +278,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Send with custom headers")
|
||||
(div :id "ref-headers-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Click to see echoed headers.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -302,7 +302,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Filter"))
|
||||
(div :id "ref-include-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Click Filter — the select value is included in the request.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -318,7 +318,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Send with extra values")
|
||||
(div :id "ref-vals-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Click to see echoed values.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -369,7 +369,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Click me")
|
||||
(div :id "ref-on-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Click the button — runs JavaScript, no server request.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -385,7 +385,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Call flaky endpoint")
|
||||
(div :id "ref-retry-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"This endpoint fails 2 out of 3 times. sx-retry retries automatically.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -440,7 +440,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Hover then click (preloaded)")
|
||||
(div :id "ref-preload-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Hover over the button first, then click — the response is instant.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -461,7 +461,7 @@
|
||||
(input :id "ref-preserved-input" :sx-preserve "true"
|
||||
:type "text" :placeholder "Type here — preserved across swaps"
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm")
|
||||
(div :class "p-2 bg-stone-50 rounded text-sm text-stone-600"
|
||||
(div :class "p-2 bg-stone-100 rounded text-sm text-stone-600"
|
||||
"This text will be replaced on swap."))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -484,7 +484,7 @@
|
||||
:style "display: none"
|
||||
"Loading..."))
|
||||
(div :id "ref-indicator-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Click to load (indicator shows during request).")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -506,7 +506,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Submit"))
|
||||
(div :id "ref-validate-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Submit with invalid/empty email to see validation.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -526,7 +526,7 @@
|
||||
(p :class "text-sm text-amber-800" "This subtree has sx-ignore — it won't change.")
|
||||
(input :type "text" :placeholder "Type here — ignored during swap"
|
||||
:class "mt-1 w-full px-2 py-1 border border-amber-300 rounded text-sm"))
|
||||
(div :class "p-2 bg-stone-50 rounded text-sm text-stone-600"
|
||||
(div :class "p-2 bg-stone-100 rounded text-sm text-stone-600"
|
||||
"This text WILL be replaced on swap."))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -566,7 +566,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Load (replaces URL)")
|
||||
(div :id "ref-replurl-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Click to load — URL changes but no new history entry.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -586,7 +586,7 @@
|
||||
"Click (disables during request)")
|
||||
(span :class "text-xs text-stone-400" "Button is disabled while request is in-flight."))
|
||||
(div :id "ref-diselt-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Click the button to see it disable during the request.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -603,7 +603,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Prompt & send")
|
||||
(div :id "ref-prompt-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Click to enter a name via prompt — it is sent as the SX-Prompt header.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -626,7 +626,7 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Submit"))
|
||||
(div :id "ref-params-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-400 text-sm"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-400 text-sm"
|
||||
"Only 'name' will be sent — 'secret' is filtered by sx-params.")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -639,7 +639,7 @@
|
||||
:sx-sse-swap "time"
|
||||
:sx-swap "innerHTML"
|
||||
(div :id "ref-sse-result"
|
||||
:class "p-3 rounded bg-stone-50 text-stone-600 text-sm font-mono"
|
||||
:class "p-3 rounded bg-stone-100 text-stone-600 text-sm font-mono"
|
||||
"Connecting to SSE stream..."))
|
||||
(p :class "text-xs text-stone-400"
|
||||
"Server pushes time updates every 2 seconds via Server-Sent Events.")))
|
||||
|
||||
Reference in New Issue
Block a user