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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,3 +15,4 @@ sx-haskell/
|
||||
sx-rust/
|
||||
shared/static/scripts/sx-full-test.js
|
||||
hosts/ocaml/_build/
|
||||
hosts/ocaml/browser/sx_browser.bc.wasm.assets/
|
||||
|
||||
@@ -665,6 +665,13 @@ let () =
|
||||
match args with [String msg] -> raise (Eval_error msg)
|
||||
| [a] -> raise (Eval_error (to_string a))
|
||||
| _ -> raise (Eval_error "error: 1 arg"));
|
||||
register "try-catch" (fun args ->
|
||||
match args with
|
||||
| [try_fn; catch_fn] ->
|
||||
(try !_sx_trampoline_fn (!_sx_call_fn try_fn [])
|
||||
with Eval_error msg ->
|
||||
!_sx_trampoline_fn (!_sx_call_fn catch_fn [String msg]))
|
||||
| _ -> raise (Eval_error "try-catch: expected (try-fn catch-fn)"));
|
||||
(* client? — false by default (server); sx_browser.ml sets _is_client := true *)
|
||||
register "client?" (fun _args -> Bool !_is_client);
|
||||
(* Named stores — global mutable registry, bypasses env scoping issues *)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -508,18 +508,17 @@
|
||||
boost-el
|
||||
(let
|
||||
((attr (dom-get-attr boost-el "sx-boost")))
|
||||
(if (and attr (not (= attr "true"))) attr "#main-panel"))
|
||||
"#main-panel")))
|
||||
(if (and attr (not (= attr "true"))) attr "#sx-content"))
|
||||
"#sx-content")))
|
||||
(if
|
||||
(try-client-route (url-pathname href) target-sel)
|
||||
(do (browser-push-state nil "" href) (browser-scroll-to 0 0))
|
||||
(do
|
||||
(when
|
||||
(not (dom-has-attr? link "sx-get"))
|
||||
(dom-set-attr link "sx-get" href))
|
||||
(when
|
||||
(not (dom-has-attr? link "sx-push-url"))
|
||||
(dom-set-attr link "sx-push-url" "true"))
|
||||
(log-info (str "sx:route server fetch " href))
|
||||
(dom-set-attr link "sx-get" href)
|
||||
(dom-set-attr link "sx-target" target-sel)
|
||||
(dom-set-attr link "sx-select" target-sel)
|
||||
(dom-set-attr link "sx-push-url" "true")
|
||||
(execute-request link nil nil)))))))))
|
||||
|
||||
(define sw-post-message (fn (msg) nil))
|
||||
@@ -604,7 +603,7 @@
|
||||
(fn
|
||||
(expr)
|
||||
(let
|
||||
((result (render-to-dom expr (get-render-env nil) nil)))
|
||||
((result (try-catch (fn () (render-to-dom expr (get-render-env nil) nil)) (fn (err) (log-error (str "sx-render: " err)) (let ((el (dom-create-element "div" nil))) (dom-set-attr el "class" "sx-render-error") (dom-set-attr el "style" "color:red;font-size:0.875rem;padding:0.5rem;border:1px solid red;border-radius:4px;margin:0.25rem 0;") (dom-set-text-content el (str "Render error: " err)) el)))))
|
||||
(when result (dom-append frag result))))
|
||||
exprs)
|
||||
(scope-pop! "sx-render-markers")
|
||||
@@ -630,7 +629,15 @@
|
||||
(and text (> (len text) 0))
|
||||
(let
|
||||
((exprs (sx-parse text)))
|
||||
(for-each (fn (expr) (cek-eval expr)) exprs))))))
|
||||
(for-each
|
||||
(fn
|
||||
(expr)
|
||||
(try-catch
|
||||
(fn () (cek-eval expr))
|
||||
(fn
|
||||
(err)
|
||||
(log-error (str "sx-process-scripts: " err)))))
|
||||
exprs))))))
|
||||
scripts))))
|
||||
|
||||
(define
|
||||
@@ -641,7 +648,10 @@
|
||||
selector
|
||||
(let
|
||||
((selected (dom-query container selector)))
|
||||
(if selected selected (children-to-fragment container)))
|
||||
(if
|
||||
selected
|
||||
(children-to-fragment selected)
|
||||
(children-to-fragment container)))
|
||||
(children-to-fragment container))))
|
||||
|
||||
(define
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -282,9 +282,11 @@
|
||||
(fn
|
||||
(t oob (s :as string))
|
||||
(dispose-islands-in t)
|
||||
(swap-dom-nodes t oob s)
|
||||
(sx-hydrate t)
|
||||
(process-elements t)))
|
||||
(swap-dom-nodes
|
||||
t
|
||||
(if (= s "innerHTML") (children-to-fragment oob) oob)
|
||||
s)
|
||||
(post-swap t)))
|
||||
(let
|
||||
((select-sel (dom-get-attr el "sx-select"))
|
||||
(content
|
||||
@@ -324,20 +326,31 @@
|
||||
(if
|
||||
select-sel
|
||||
(let
|
||||
((html (select-html-from-doc doc select-sel)))
|
||||
(with-transition
|
||||
use-transition
|
||||
((container (dom-create-element "div" nil)))
|
||||
(dom-set-inner-html container (dom-body-inner-html doc))
|
||||
(process-oob-swaps
|
||||
container
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((swap-root (swap-html-string target html swap-style)))
|
||||
(log-info
|
||||
(str
|
||||
"swap-root: "
|
||||
(if swap-root (dom-tag-name swap-root) "nil")
|
||||
" target: "
|
||||
(dom-tag-name target)))
|
||||
(post-swap (or swap-root target))))))
|
||||
(t oob (s :as string))
|
||||
(dispose-islands-in t)
|
||||
(swap-dom-nodes t oob s)
|
||||
(post-swap t)))
|
||||
(hoist-head-elements container)
|
||||
(let
|
||||
((html (select-from-container container select-sel)))
|
||||
(with-transition
|
||||
use-transition
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((swap-root (swap-dom-nodes target html swap-style)))
|
||||
(log-info
|
||||
(str
|
||||
"swap-root: "
|
||||
(if swap-root (dom-tag-name swap-root) "nil")
|
||||
" target: "
|
||||
(dom-tag-name target)))
|
||||
(post-swap (or swap-root target)))))))
|
||||
(let
|
||||
((container (dom-create-element "div" nil)))
|
||||
(dom-set-inner-html container (dom-body-inner-html doc))
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
shared/static/wasm/sx_browser.bc.wasm.assets/sx-afdcb8e8.wasm
Normal file
BIN
shared/static/wasm/sx_browser.bc.wasm.assets/sx-afdcb8e8.wasm
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1792,7 +1792,7 @@
|
||||
blake2_js_for_wasm_create: blake2_js_for_wasm_create};
|
||||
}
|
||||
(globalThis))
|
||||
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-80621fb4",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-8ae21d0a",[2,3,5]],["std_exit-10fb8830",[2]],["start-80fdb768",0]],"generated":(b=>{var
|
||||
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-afdcb8e8",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-8ae21d0a",[2,3,5]],["std_exit-10fb8830",[2]],["start-80fdb768",0]],"generated":(b=>{var
|
||||
c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new
|
||||
Error("caml_ba_kind_of_typed_array not implemented")},"caml_exn_with_js_backtrace":()=>{throw new
|
||||
Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new
|
||||
|
||||
366
sx/sx/docs.sx
366
sx/sx/docs.sx
@@ -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")))))
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
@@ -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.")))))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")))
|
||||
|
||||
@@ -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")))))
|
||||
|
||||
@@ -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
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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."))))
|
||||
|
||||
@@ -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 "\"."))))
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
@@ -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))))))
|
||||
|
||||
1493
sx/sx/specs.sx
1493
sx/sx/specs.sx
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
1042
sx/sxc/examples.sx
1042
sx/sxc/examples.sx
File diff suppressed because it is too large
Load Diff
1142
sx/sxc/reference.sx
1142
sx/sxc/reference.sx
File diff suppressed because it is too large
Load Diff
90
tests/playwright/spa-navigation.spec.js
Normal file
90
tests/playwright/spa-navigation.spec.js
Normal file
@@ -0,0 +1,90 @@
|
||||
// SPA navigation tests — verify header, nav, and content survive navigation
|
||||
// Tests that OOB swaps update nav while preserving the header island,
|
||||
// and that rendering errors are scoped to #sx-content.
|
||||
|
||||
const { test, expect } = require('playwright/test');
|
||||
const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013';
|
||||
|
||||
test.describe('SPA navigation', () => {
|
||||
|
||||
test('header island survives SPA nav to sibling page', async ({ page }) => {
|
||||
await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example)))', { waitUntil: 'networkidle' });
|
||||
|
||||
// Header should be present and hydrated
|
||||
const headerIsland = page.locator('[data-sx-island="layouts/header"]');
|
||||
await expect(headerIsland).toHaveCount(1);
|
||||
await expect(headerIsland).toContainText('sx');
|
||||
|
||||
// Navigate via SPA
|
||||
await page.click('a[href*="click-to-load"]');
|
||||
await page.waitForURL('**/click-to-load**');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Header island should still exist
|
||||
await expect(page.locator('[data-sx-island="layouts/header"]')).toHaveCount(1);
|
||||
await expect(page.locator('[data-sx-island="layouts/header"]')).toContainText('sx');
|
||||
});
|
||||
|
||||
test('nav updates via OOB on SPA navigation', async ({ page }) => {
|
||||
await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example)))', { waitUntil: 'networkidle' });
|
||||
|
||||
// Nav should show current breadcrumbs
|
||||
const nav = page.locator('#sx-nav');
|
||||
await expect(nav).toHaveCount(1);
|
||||
const navTextBefore = await nav.textContent();
|
||||
expect(navTextBefore).toContain('Examples');
|
||||
|
||||
// Navigate to a child page
|
||||
await page.click('a[href*="click-to-load"]');
|
||||
await page.waitForURL('**/click-to-load**');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Nav should update to show the new page in breadcrumbs
|
||||
const navTextAfter = await page.locator('#sx-nav').first().textContent();
|
||||
expect(navTextAfter).toContain('Click to Load');
|
||||
});
|
||||
|
||||
test('#sx-content exists after SPA navigation', async ({ page }) => {
|
||||
await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example)))', { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('#sx-content')).toHaveCount(1);
|
||||
|
||||
await page.click('a[href*="click-to-load"]');
|
||||
await page.waitForURL('**/click-to-load**');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// sx-content should still exist (may contain error boundary, but not be missing)
|
||||
await expect(page.locator('#sx-content').first()).toBeAttached();
|
||||
});
|
||||
|
||||
test('rendering error scoped to #sx-content, not full page', async ({ page }) => {
|
||||
await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example)))', { waitUntil: 'networkidle' });
|
||||
|
||||
// Verify page structure before nav
|
||||
await expect(page.locator('#sx-nav')).toHaveCount(1);
|
||||
await expect(page.locator('#sx-content')).toHaveCount(1);
|
||||
|
||||
await page.click('a[href*="click-to-load"]');
|
||||
await page.waitForURL('**/click-to-load**');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// If there's a render error, it should be inside #sx-content
|
||||
const errors = page.locator('.sx-render-error');
|
||||
const errorCount = await errors.count();
|
||||
if (errorCount > 0) {
|
||||
// Error should be a descendant of #sx-content, not replacing the whole page
|
||||
const errorParent = await errors.first().evaluate(el => {
|
||||
let p = el;
|
||||
while (p) {
|
||||
if (p.id === 'sx-content') return 'sx-content';
|
||||
if (p.id === 'main-panel') return 'main-panel';
|
||||
p = p.parentElement;
|
||||
}
|
||||
return 'unknown';
|
||||
});
|
||||
expect(errorParent).toBe('sx-content');
|
||||
|
||||
// Nav should still be present even with an error
|
||||
await expect(page.locator('#sx-nav').first()).toContainText('Click to Load');
|
||||
}
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -508,18 +508,17 @@
|
||||
boost-el
|
||||
(let
|
||||
((attr (dom-get-attr boost-el "sx-boost")))
|
||||
(if (and attr (not (= attr "true"))) attr "#main-panel"))
|
||||
"#main-panel")))
|
||||
(if (and attr (not (= attr "true"))) attr "#sx-content"))
|
||||
"#sx-content")))
|
||||
(if
|
||||
(try-client-route (url-pathname href) target-sel)
|
||||
(do (browser-push-state nil "" href) (browser-scroll-to 0 0))
|
||||
(do
|
||||
(when
|
||||
(not (dom-has-attr? link "sx-get"))
|
||||
(dom-set-attr link "sx-get" href))
|
||||
(when
|
||||
(not (dom-has-attr? link "sx-push-url"))
|
||||
(dom-set-attr link "sx-push-url" "true"))
|
||||
(log-info (str "sx:route server fetch " href))
|
||||
(dom-set-attr link "sx-get" href)
|
||||
(dom-set-attr link "sx-target" target-sel)
|
||||
(dom-set-attr link "sx-select" target-sel)
|
||||
(dom-set-attr link "sx-push-url" "true")
|
||||
(execute-request link nil nil)))))))))
|
||||
|
||||
(define sw-post-message (fn (msg) nil))
|
||||
@@ -604,7 +603,7 @@
|
||||
(fn
|
||||
(expr)
|
||||
(let
|
||||
((result (render-to-dom expr (get-render-env nil) nil)))
|
||||
((result (try-catch (fn () (render-to-dom expr (get-render-env nil) nil)) (fn (err) (log-error (str "sx-render: " err)) (let ((el (dom-create-element "div" nil))) (dom-set-attr el "class" "sx-render-error") (dom-set-attr el "style" "color:red;font-size:0.875rem;padding:0.5rem;border:1px solid red;border-radius:4px;margin:0.25rem 0;") (dom-set-text-content el (str "Render error: " err)) el)))))
|
||||
(when result (dom-append frag result))))
|
||||
exprs)
|
||||
(scope-pop! "sx-render-markers")
|
||||
@@ -630,7 +629,15 @@
|
||||
(and text (> (len text) 0))
|
||||
(let
|
||||
((exprs (sx-parse text)))
|
||||
(for-each (fn (expr) (cek-eval expr)) exprs))))))
|
||||
(for-each
|
||||
(fn
|
||||
(expr)
|
||||
(try-catch
|
||||
(fn () (cek-eval expr))
|
||||
(fn
|
||||
(err)
|
||||
(log-error (str "sx-process-scripts: " err)))))
|
||||
exprs))))))
|
||||
scripts))))
|
||||
|
||||
(define
|
||||
@@ -641,7 +648,10 @@
|
||||
selector
|
||||
(let
|
||||
((selected (dom-query container selector)))
|
||||
(if selected selected (children-to-fragment container)))
|
||||
(if
|
||||
selected
|
||||
(children-to-fragment selected)
|
||||
(children-to-fragment container)))
|
||||
(children-to-fragment container))))
|
||||
|
||||
(define
|
||||
|
||||
@@ -282,9 +282,11 @@
|
||||
(fn
|
||||
(t oob (s :as string))
|
||||
(dispose-islands-in t)
|
||||
(swap-dom-nodes t oob s)
|
||||
(sx-hydrate t)
|
||||
(process-elements t)))
|
||||
(swap-dom-nodes
|
||||
t
|
||||
(if (= s "innerHTML") (children-to-fragment oob) oob)
|
||||
s)
|
||||
(post-swap t)))
|
||||
(let
|
||||
((select-sel (dom-get-attr el "sx-select"))
|
||||
(content
|
||||
@@ -324,20 +326,31 @@
|
||||
(if
|
||||
select-sel
|
||||
(let
|
||||
((html (select-html-from-doc doc select-sel)))
|
||||
(with-transition
|
||||
use-transition
|
||||
((container (dom-create-element "div" nil)))
|
||||
(dom-set-inner-html container (dom-body-inner-html doc))
|
||||
(process-oob-swaps
|
||||
container
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((swap-root (swap-html-string target html swap-style)))
|
||||
(log-info
|
||||
(str
|
||||
"swap-root: "
|
||||
(if swap-root (dom-tag-name swap-root) "nil")
|
||||
" target: "
|
||||
(dom-tag-name target)))
|
||||
(post-swap (or swap-root target))))))
|
||||
(t oob (s :as string))
|
||||
(dispose-islands-in t)
|
||||
(swap-dom-nodes t oob s)
|
||||
(post-swap t)))
|
||||
(hoist-head-elements container)
|
||||
(let
|
||||
((html (select-from-container container select-sel)))
|
||||
(with-transition
|
||||
use-transition
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((swap-root (swap-dom-nodes target html swap-style)))
|
||||
(log-info
|
||||
(str
|
||||
"swap-root: "
|
||||
(if swap-root (dom-tag-name swap-root) "nil")
|
||||
" target: "
|
||||
(dom-tag-name target)))
|
||||
(post-swap (or swap-root target)))))))
|
||||
(let
|
||||
((container (dom-create-element "div" nil)))
|
||||
(dom-set-inner-html container (dom-body-inner-html doc))
|
||||
|
||||
Reference in New Issue
Block a user