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/
|
sx-rust/
|
||||||
shared/static/scripts/sx-full-test.js
|
shared/static/scripts/sx-full-test.js
|
||||||
hosts/ocaml/_build/
|
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)
|
match args with [String msg] -> raise (Eval_error msg)
|
||||||
| [a] -> raise (Eval_error (to_string a))
|
| [a] -> raise (Eval_error (to_string a))
|
||||||
| _ -> raise (Eval_error "error: 1 arg"));
|
| _ -> 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 *)
|
(* client? — false by default (server); sx_browser.ml sets _is_client := true *)
|
||||||
register "client?" (fun _args -> Bool !_is_client);
|
register "client?" (fun _args -> Bool !_is_client);
|
||||||
(* Named stores — global mutable registry, bypasses env scoping issues *)
|
(* 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
|
boost-el
|
||||||
(let
|
(let
|
||||||
((attr (dom-get-attr boost-el "sx-boost")))
|
((attr (dom-get-attr boost-el "sx-boost")))
|
||||||
(if (and attr (not (= attr "true"))) attr "#main-panel"))
|
(if (and attr (not (= attr "true"))) attr "#sx-content"))
|
||||||
"#main-panel")))
|
"#sx-content")))
|
||||||
(if
|
(if
|
||||||
(try-client-route (url-pathname href) target-sel)
|
(try-client-route (url-pathname href) target-sel)
|
||||||
(do (browser-push-state nil "" href) (browser-scroll-to 0 0))
|
(do (browser-push-state nil "" href) (browser-scroll-to 0 0))
|
||||||
(do
|
(do
|
||||||
(when
|
(log-info (str "sx:route server fetch " href))
|
||||||
(not (dom-has-attr? link "sx-get"))
|
(dom-set-attr link "sx-get" href)
|
||||||
(dom-set-attr link "sx-get" href))
|
(dom-set-attr link "sx-target" target-sel)
|
||||||
(when
|
(dom-set-attr link "sx-select" target-sel)
|
||||||
(not (dom-has-attr? link "sx-push-url"))
|
(dom-set-attr link "sx-push-url" "true")
|
||||||
(dom-set-attr link "sx-push-url" "true"))
|
|
||||||
(execute-request link nil nil)))))))))
|
(execute-request link nil nil)))))))))
|
||||||
|
|
||||||
(define sw-post-message (fn (msg) nil))
|
(define sw-post-message (fn (msg) nil))
|
||||||
@@ -604,7 +603,7 @@
|
|||||||
(fn
|
(fn
|
||||||
(expr)
|
(expr)
|
||||||
(let
|
(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))))
|
(when result (dom-append frag result))))
|
||||||
exprs)
|
exprs)
|
||||||
(scope-pop! "sx-render-markers")
|
(scope-pop! "sx-render-markers")
|
||||||
@@ -630,7 +629,15 @@
|
|||||||
(and text (> (len text) 0))
|
(and text (> (len text) 0))
|
||||||
(let
|
(let
|
||||||
((exprs (sx-parse text)))
|
((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))))
|
scripts))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -641,7 +648,10 @@
|
|||||||
selector
|
selector
|
||||||
(let
|
(let
|
||||||
((selected (dom-query container selector)))
|
((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))))
|
(children-to-fragment container))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -282,9 +282,11 @@
|
|||||||
(fn
|
(fn
|
||||||
(t oob (s :as string))
|
(t oob (s :as string))
|
||||||
(dispose-islands-in t)
|
(dispose-islands-in t)
|
||||||
(swap-dom-nodes t oob s)
|
(swap-dom-nodes
|
||||||
(sx-hydrate t)
|
t
|
||||||
(process-elements t)))
|
(if (= s "innerHTML") (children-to-fragment oob) oob)
|
||||||
|
s)
|
||||||
|
(post-swap t)))
|
||||||
(let
|
(let
|
||||||
((select-sel (dom-get-attr el "sx-select"))
|
((select-sel (dom-get-attr el "sx-select"))
|
||||||
(content
|
(content
|
||||||
@@ -324,20 +326,31 @@
|
|||||||
(if
|
(if
|
||||||
select-sel
|
select-sel
|
||||||
(let
|
(let
|
||||||
((html (select-html-from-doc doc select-sel)))
|
((container (dom-create-element "div" nil)))
|
||||||
(with-transition
|
(dom-set-inner-html container (dom-body-inner-html doc))
|
||||||
use-transition
|
(process-oob-swaps
|
||||||
|
container
|
||||||
(fn
|
(fn
|
||||||
()
|
(t oob (s :as string))
|
||||||
(let
|
(dispose-islands-in t)
|
||||||
((swap-root (swap-html-string target html swap-style)))
|
(swap-dom-nodes t oob s)
|
||||||
(log-info
|
(post-swap t)))
|
||||||
(str
|
(hoist-head-elements container)
|
||||||
"swap-root: "
|
(let
|
||||||
(if swap-root (dom-tag-name swap-root) "nil")
|
((html (select-from-container container select-sel)))
|
||||||
" target: "
|
(with-transition
|
||||||
(dom-tag-name target)))
|
use-transition
|
||||||
(post-swap (or swap-root target))))))
|
(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
|
(let
|
||||||
((container (dom-create-element "div" nil)))
|
((container (dom-create-element "div" nil)))
|
||||||
(dom-set-inner-html container (dom-body-inner-html doc))
|
(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};
|
blake2_js_for_wasm_create: blake2_js_for_wasm_create};
|
||||||
}
|
}
|
||||||
(globalThis))
|
(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
|
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_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
|
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
|
||||||
(defcomp ~docs/placeholder (&key (id :as string))
|
(&key (id :as string))
|
||||||
(div :id id
|
(div
|
||||||
(div :class "bg-stone-100 rounded p-4 mt-3"
|
:id id
|
||||||
(p :class "text-stone-400 italic text-sm"
|
(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."))))
|
"Trigger the demo to see the actual content."))))
|
||||||
|
|
||||||
(defcomp ~docs/oob-code (&key (target-id :as string) (text :as string))
|
(defcomp
|
||||||
(div :id target-id :sx-swap-oob "innerHTML"
|
~docs/oob-code
|
||||||
(div :class "not-prose bg-stone-100 rounded p-4 mt-3"
|
(&key (target-id :as string) (text :as string))
|
||||||
(pre :class "text-sm whitespace-pre-wrap break-words"
|
(div
|
||||||
(code text)))))
|
: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)
|
(defcomp
|
||||||
(div :class "space-y-3"
|
~docs/attr-table
|
||||||
|
(&key (title :as string) rows)
|
||||||
|
(div
|
||||||
|
:class "space-y-3"
|
||||||
(h3 :class "text-xl font-semibold text-stone-700" title)
|
(h3 :class "text-xl font-semibold text-stone-700" title)
|
||||||
(div :class "overflow-x-auto rounded border border-stone-200"
|
(div
|
||||||
(table :class "w-full text-left text-sm"
|
:class "overflow-x-auto rounded border border-stone-200"
|
||||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
(table
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Attribute")
|
:class "w-full text-left text-sm"
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Description")
|
(thead
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600 text-center w-20" "In sx?")))
|
(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)))))
|
(tbody rows)))))
|
||||||
|
|
||||||
(defcomp ~docs/headers-table (&key (title :as string) rows)
|
(defcomp
|
||||||
(div :class "space-y-3"
|
~docs/headers-table
|
||||||
|
(&key (title :as string) rows)
|
||||||
|
(div
|
||||||
|
:class "space-y-3"
|
||||||
(h3 :class "text-xl font-semibold text-stone-700" title)
|
(h3 :class "text-xl font-semibold text-stone-700" title)
|
||||||
(div :class "overflow-x-auto rounded border border-stone-200"
|
(div
|
||||||
(table :class "w-full text-left text-sm"
|
:class "overflow-x-auto rounded border border-stone-200"
|
||||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
(table
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Header")
|
:class "w-full text-left text-sm"
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Value")
|
(thead
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
|
(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)))))
|
(tbody rows)))))
|
||||||
|
|
||||||
(defcomp ~docs/headers-row (&key (name :as string) (value :as string) (description :as string) (href :as string?))
|
(defcomp
|
||||||
(tr :class "border-b border-stone-100"
|
~docs/headers-row
|
||||||
(td :class "px-3 py-2 font-mono text-sm whitespace-nowrap"
|
(&key
|
||||||
(if href
|
(name :as string)
|
||||||
(a :href href
|
(value :as string)
|
||||||
:sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
|
(description :as string)
|
||||||
:sx-swap "outerHTML" :sx-push-url "true"
|
(href :as string?))
|
||||||
:class "text-violet-700 hover:text-violet-900 underline" name)
|
(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)))
|
(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 font-mono text-sm text-stone-500" value)
|
||||||
(td :class "px-3 py-2 text-stone-700 text-sm" description)))
|
(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?))
|
(defcomp
|
||||||
(tr :class "border-b border-stone-100"
|
~docs/two-col-row
|
||||||
(td :class "px-3 py-2 font-mono text-sm whitespace-nowrap"
|
(&key (name :as string) (description :as string) (href :as string?))
|
||||||
(if href
|
(tr
|
||||||
(a :href href
|
:class "border-b border-stone-100"
|
||||||
:sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
|
(td
|
||||||
:sx-swap "outerHTML" :sx-push-url "true"
|
:class "px-3 py-2 font-mono text-sm whitespace-nowrap"
|
||||||
:class "text-violet-700 hover:text-violet-900 underline" name)
|
(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)))
|
(span :class "text-violet-700" name)))
|
||||||
(td :class "px-3 py-2 text-stone-700 text-sm" description)))
|
(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)
|
(defcomp
|
||||||
(div :class "space-y-3"
|
~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 title (h3 :class "text-xl font-semibold text-stone-700" title))
|
||||||
(when intro (p :class "text-stone-600 mb-6" intro))
|
(when intro (p :class "text-stone-600 mb-6" intro))
|
||||||
(div :class "overflow-x-auto rounded border border-stone-200"
|
(div
|
||||||
(table :class "w-full text-left text-sm"
|
:class "overflow-x-auto rounded border border-stone-200"
|
||||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
(table
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" (or col1 "Name"))
|
:class "w-full text-left text-sm"
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" (or col2 "Description"))))
|
(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)))))
|
(tbody rows)))))
|
||||||
|
|
||||||
(defcomp ~docs/label ()
|
(defcomp ~docs/label () (span :class "font-mono" "(<sx>)"))
|
||||||
(span :class "font-mono" "(<sx>)"))
|
|
||||||
|
|
||||||
(defcomp ~docs/clear-cache-btn ()
|
(defcomp
|
||||||
(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)"
|
~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"
|
:class "text-xs text-stone-400 hover:text-stone-600 border border-stone-200 rounded px-2 py-1 transition-colors"
|
||||||
"Clear component cache"))
|
"Clear component cache"))
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
(defcomp
|
||||||
;; Data-driven table builders — replace Python sx_call() composition
|
~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.
|
(defcomp
|
||||||
;; Replaces _attr_table_sx() in utils.py.
|
~docs/headers-table-from-data
|
||||||
(defcomp ~docs/attr-table-from-data (&key (title :as string) (attrs :as list))
|
(&key (title :as string) (headers :as list))
|
||||||
(~docs/attr-table :title title
|
(~docs/headers-table
|
||||||
:rows (<> (map (fn (a)
|
:title title
|
||||||
(~docs/attr-row
|
:rows (<>
|
||||||
:attr (get a "name")
|
(map
|
||||||
:description (get a "desc")
|
(fn
|
||||||
:exists (get a "exists")
|
(h)
|
||||||
:href (get a "href")))
|
(~docs/headers-row
|
||||||
attrs))))
|
: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.
|
(defcomp
|
||||||
;; Replaces _headers_table_sx() in utils.py.
|
~docs/two-col-table-from-data
|
||||||
(defcomp ~docs/headers-table-from-data (&key (title :as string) (headers :as list))
|
(&key
|
||||||
(~docs/headers-table :title title
|
(title :as string?)
|
||||||
:rows (<> (map (fn (h)
|
(intro :as string?)
|
||||||
(~docs/headers-row
|
(col1 :as string?)
|
||||||
:name (get h "name")
|
(col2 :as string?)
|
||||||
:value (get h "value")
|
(items :as list))
|
||||||
:description (get h "desc")
|
(~docs/two-col-table
|
||||||
:href (get h "href")))
|
:title title
|
||||||
headers))))
|
: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.
|
(defcomp
|
||||||
;; Replaces the _reference_events_sx / _reference_js_api_sx builders.
|
~docs/primitives-tables
|
||||||
(defcomp ~docs/two-col-table-from-data (&key (title :as string?) (intro :as string?) (col1 :as string?) (col2 :as string?) (items :as list))
|
(&key (primitives :as dict))
|
||||||
(~docs/two-col-table :title title :intro intro :col1 col1 :col2 col2
|
(<>
|
||||||
:rows (<> (map (fn (item)
|
(map
|
||||||
(~docs/two-col-row
|
(fn
|
||||||
:name (get item "name")
|
(cat)
|
||||||
:description (get item "desc")
|
(~docs/primitives-table
|
||||||
:href (get item "href")))
|
:category cat
|
||||||
items))))
|
:primitives (get primitives cat)))
|
||||||
|
(keys primitives))))
|
||||||
|
|
||||||
;; Build all primitives category tables from a {category: [prim, ...]} dict.
|
(defcomp
|
||||||
;; Replaces _primitives_section_sx() in utils.py.
|
~docs/special-forms-tables
|
||||||
(defcomp ~docs/primitives-tables (&key (primitives :as dict))
|
(&key (forms :as dict))
|
||||||
(<> (map (fn (cat)
|
(<>
|
||||||
(~docs/primitives-table
|
(map
|
||||||
:category cat
|
(fn
|
||||||
:primitives (get primitives cat)))
|
(cat)
|
||||||
(keys primitives))))
|
(~docs/special-forms-category :category cat :forms (get forms cat)))
|
||||||
|
(keys forms))))
|
||||||
|
|
||||||
;; Build all special form category sections from a {category: [form, ...]} dict.
|
(defcomp
|
||||||
(defcomp ~docs/special-forms-tables (&key (forms :as dict))
|
~docs/special-forms-category
|
||||||
(<> (map (fn (cat)
|
(&key (category :as string) (forms :as list))
|
||||||
(~docs/special-forms-category
|
(div
|
||||||
:category cat
|
:class "space-y-4"
|
||||||
:forms (get forms cat)))
|
(h3
|
||||||
(keys forms))))
|
:class "text-xl font-semibold text-stone-800 border-b border-stone-200 pb-2"
|
||||||
|
category)
|
||||||
(defcomp ~docs/special-forms-category (&key (category :as string) (forms :as list))
|
(div
|
||||||
(div :class "space-y-4"
|
:class "space-y-4"
|
||||||
(h3 :class "text-xl font-semibold text-stone-800 border-b border-stone-200 pb-2" category)
|
(map
|
||||||
(div :class "space-y-4"
|
(fn
|
||||||
(map (fn (f)
|
(f)
|
||||||
(~docs/special-form-card
|
(~docs/special-form-card
|
||||||
:name (get f "name")
|
:name (get f "name")
|
||||||
:syntax (get f "syntax")
|
:syntax (get f "syntax")
|
||||||
:doc (get f "doc")
|
:doc (get f "doc")
|
||||||
:tail-position (get f "tail-position")
|
:tail-position (get f "tail-position")
|
||||||
:example (get f "example")))
|
:example (get f "example")))
|
||||||
forms))))
|
forms))))
|
||||||
|
|
||||||
(defcomp ~docs/special-form-card (&key (name :as string) (syntax :as string) (doc :as string) (tail-position :as string) (example :as string))
|
(defcomp
|
||||||
(div :class "not-prose border border-stone-200 rounded-lg p-4 space-y-3"
|
~docs/special-form-card
|
||||||
(div :class "flex items-baseline gap-3"
|
(&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)
|
(code :class "text-lg font-bold text-violet-700" name)
|
||||||
(when (not (= tail-position "none"))
|
(when
|
||||||
(span :class "text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-700" "TCO")))
|
(not (= tail-position "none"))
|
||||||
(when (not (= syntax ""))
|
(span
|
||||||
(pre :class "bg-stone-100 rounded px-3 py-2 text-sm font-mono text-stone-700 overflow-x-auto"
|
: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))
|
syntax))
|
||||||
(p :class "text-stone-600 text-sm whitespace-pre-line" doc)
|
(p :class "text-stone-600 text-sm whitespace-pre-line" doc)
|
||||||
(when (not (= tail-position ""))
|
(when
|
||||||
(p :class "text-xs text-stone-500"
|
(not (= tail-position ""))
|
||||||
(span :class "font-semibold" "Tail position: ") tail-position))
|
(p
|
||||||
(when (not (= example ""))
|
:class "text-xs text-stone-500"
|
||||||
|
(span :class "font-semibold" "Tail position: ")
|
||||||
|
tail-position))
|
||||||
|
(when
|
||||||
|
(not (= example ""))
|
||||||
(~docs/code :src (highlight example "lisp")))))
|
(~docs/code :src (highlight example "lisp")))))
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
(defcomp ~essays/index/essays-index-content ()
|
(defcomp
|
||||||
(~docs/page :title "Essays"
|
~essays/index/essays-index-content
|
||||||
(div :class "space-y-4"
|
()
|
||||||
(p :class "text-lg text-stone-600 mb-4"
|
(~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.")
|
"Opinions, rationales, and explorations around SX and the ideas behind it.")
|
||||||
(div :class "space-y-3"
|
(div
|
||||||
(map (fn (item)
|
:class "space-y-3"
|
||||||
(a :href (get item "href")
|
(map
|
||||||
:sx-get (get item "href") :sx-target "#main-panel" :sx-select "#main-panel"
|
(fn
|
||||||
:sx-swap "outerHTML" :sx-push-url "true"
|
(item)
|
||||||
:class "block rounded border border-stone-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"
|
(a
|
||||||
(div :class "font-semibold text-stone-800" (get item "label"))
|
:href (get item "href")
|
||||||
(when (get item "summary")
|
:sx-get (get item "href")
|
||||||
(p :class "text-sm text-stone-500 mt-1" (get item "summary")))))
|
: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)))))
|
essays-nav-items)))))
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
;; Essay content — static content extracted from essays.py
|
(defcomp
|
||||||
|
~essays/philosophy-index/content
|
||||||
;; ---------------------------------------------------------------------------
|
()
|
||||||
;; Philosophy section content
|
(~docs/page
|
||||||
;; ---------------------------------------------------------------------------
|
:title "Philosophy"
|
||||||
|
(div
|
||||||
(defcomp ~essays/philosophy-index/content ()
|
:class "space-y-4"
|
||||||
(~docs/page :title "Philosophy"
|
(p
|
||||||
(div :class "space-y-4"
|
:class "text-lg text-stone-600 mb-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.")
|
"The deeper ideas behind SX — manifestos, self-reference, and the philosophical traditions that shaped the language.")
|
||||||
(div :class "space-y-3"
|
(div
|
||||||
(map (fn (item)
|
:class "space-y-3"
|
||||||
(a :href (get item "href")
|
(map
|
||||||
:sx-get (get item "href") :sx-target "#main-panel" :sx-select "#main-panel"
|
(fn
|
||||||
:sx-swap "outerHTML" :sx-push-url "true"
|
(item)
|
||||||
:class "block rounded border border-stone-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"
|
(a
|
||||||
(div :class "font-semibold text-stone-800" (get item "label"))
|
:href (get item "href")
|
||||||
(when (get item "summary")
|
:sx-get (get item "href")
|
||||||
(p :class "text-sm text-stone-500 mt-1" (get item "summary")))))
|
: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)))))
|
philosophy-nav-items)))))
|
||||||
|
|||||||
@@ -1,58 +1,103 @@
|
|||||||
;; Example page template and reference index
|
(defcomp
|
||||||
;; Template receives data values (code strings, titles), calls highlight internally.
|
~examples/page-content
|
||||||
|
(&key
|
||||||
(defcomp ~examples/page-content (&key (title :as string) (description :as string) (demo-description :as string?) demo
|
(title :as string)
|
||||||
(sx-code :as string) (sx-lang :as string?) (handler-code :as string) (handler-lang :as string?)
|
(description :as string)
|
||||||
(comp-placeholder-id :as string?) (wire-placeholder-id :as string?) (wire-note :as string?)
|
(demo-description :as string?)
|
||||||
(comp-heading :as string?) (handler-heading :as string?))
|
demo
|
||||||
(~docs/page :title title
|
(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)
|
(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))
|
(~examples/demo demo))
|
||||||
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")
|
(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")))
|
(~examples/source
|
||||||
(when comp-placeholder-id
|
: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"))
|
(if comp-heading comp-heading "Component"))
|
||||||
(~docs/placeholder :id comp-placeholder-id)))
|
(~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"))
|
(if handler-heading handler-heading "Server handler"))
|
||||||
(~examples/source :code (highlight handler-code (if handler-lang handler-lang "python")))
|
(~examples/source
|
||||||
(div :class "flex items-center justify-between mt-6"
|
: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")
|
(h3 :class "text-lg font-semibold text-stone-700" "Wire response")
|
||||||
(~docs/clear-cache-btn))
|
(~docs/clear-cache-btn))
|
||||||
(when wire-note
|
(when wire-note (p :class "text-stone-500 text-sm mb-2" 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-placeholder-id
|
|
||||||
(~docs/placeholder :id wire-placeholder-id))))
|
|
||||||
|
|
||||||
(defcomp ~examples/reference-index-content ()
|
(defcomp
|
||||||
(~docs/page :title "Reference"
|
~examples/reference-index-content
|
||||||
(p :class "text-stone-600 mb-6"
|
()
|
||||||
|
(~docs/page
|
||||||
|
:title "Reference"
|
||||||
|
(p
|
||||||
|
:class "text-stone-600 mb-6"
|
||||||
"Complete reference for the sx client library.")
|
"Complete reference for the sx client library.")
|
||||||
(div :class "grid gap-4 sm:grid-cols-2"
|
(div
|
||||||
(a :href "/sx/(geography.(hypermedia.(reference.attributes)))"
|
:class "grid gap-4 sm:grid-cols-2"
|
||||||
:sx-get "/sx/(geography.(hypermedia.(reference.attributes)))" :sx-target "#main-panel" :sx-select "#main-panel"
|
(a
|
||||||
:sx-swap "outerHTML" :sx-push-url "true"
|
: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"
|
: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")
|
(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."))
|
(p
|
||||||
(a :href "/sx/(geography.(hypermedia.(reference.headers)))"
|
:class "text-stone-600 text-sm"
|
||||||
:sx-get "/sx/(geography.(hypermedia.(reference.headers)))" :sx-target "#main-panel" :sx-select "#main-panel"
|
"All sx attributes — request verbs, behavior modifiers, and sx-unique features."))
|
||||||
:sx-swap "outerHTML" :sx-push-url "true"
|
(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"
|
: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")
|
(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."))
|
(p
|
||||||
(a :href "/sx/(geography.(hypermedia.(reference.events)))"
|
:class "text-stone-600 text-sm"
|
||||||
:sx-get "/sx/(geography.(hypermedia.(reference.events)))" :sx-target "#main-panel" :sx-select "#main-panel"
|
"Custom HTTP headers used to coordinate between the sx client and server."))
|
||||||
:sx-swap "outerHTML" :sx-push-url "true"
|
(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"
|
: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")
|
(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."))
|
(p
|
||||||
(a :href "/sx/(geography.(hypermedia.(reference.js-api)))"
|
:class "text-stone-600 text-sm"
|
||||||
:sx-get "/sx/(geography.(hypermedia.(reference.js-api)))" :sx-target "#main-panel" :sx-select "#main-panel"
|
"DOM events fired during the sx request lifecycle."))
|
||||||
:sx-swap "outerHTML" :sx-push-url "true"
|
(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"
|
: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")
|
(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
|
(a
|
||||||
:href "/sx/"
|
:href "/sx/"
|
||||||
:sx-get "/sx/"
|
:sx-get "/sx/"
|
||||||
:sx-target "#main-panel"
|
:sx-target "#sx-content"
|
||||||
:sx-select "#main-panel"
|
:sx-select "#sx-content"
|
||||||
:sx-swap "innerHTML"
|
:sx-swap "innerHTML"
|
||||||
:sx-push-url "true"
|
:sx-push-url "true"
|
||||||
(~cssx/tw :tokens "block no-underline")
|
(~cssx/tw :tokens "block no-underline")
|
||||||
@@ -82,8 +82,8 @@
|
|||||||
(a
|
(a
|
||||||
:href (get prev-node "href")
|
:href (get prev-node "href")
|
||||||
:sx-get (get prev-node "href")
|
:sx-get (get prev-node "href")
|
||||||
:sx-target "#main-panel"
|
:sx-target "#sx-content"
|
||||||
:sx-select "#main-panel"
|
:sx-select "#sx-content"
|
||||||
:sx-swap "innerHTML"
|
:sx-swap "innerHTML"
|
||||||
:sx-push-url "true"
|
:sx-push-url "true"
|
||||||
:class "text-right min-w-0 truncate"
|
:class "text-right min-w-0 truncate"
|
||||||
@@ -92,8 +92,8 @@
|
|||||||
(a
|
(a
|
||||||
:href (get node "href")
|
:href (get node "href")
|
||||||
:sx-get (get node "href")
|
:sx-get (get node "href")
|
||||||
:sx-target "#main-panel"
|
:sx-target "#sx-content"
|
||||||
:sx-select "#main-panel"
|
:sx-select "#sx-content"
|
||||||
:sx-swap "innerHTML"
|
:sx-swap "innerHTML"
|
||||||
:sx-push-url "true"
|
:sx-push-url "true"
|
||||||
:class "text-center min-w-0 truncate px-1"
|
:class "text-center min-w-0 truncate px-1"
|
||||||
@@ -105,8 +105,8 @@
|
|||||||
(a
|
(a
|
||||||
:href (get next-node "href")
|
:href (get next-node "href")
|
||||||
:sx-get (get next-node "href")
|
:sx-get (get next-node "href")
|
||||||
:sx-target "#main-panel"
|
:sx-target "#sx-content"
|
||||||
:sx-select "#main-panel"
|
:sx-select "#sx-content"
|
||||||
:sx-swap "innerHTML"
|
:sx-swap "innerHTML"
|
||||||
:sx-push-url "true"
|
:sx-push-url "true"
|
||||||
:class "text-left min-w-0 truncate"
|
:class "text-left min-w-0 truncate"
|
||||||
@@ -127,8 +127,8 @@
|
|||||||
(a
|
(a
|
||||||
:href (get item "href")
|
:href (get item "href")
|
||||||
:sx-get (get item "href")
|
:sx-get (get item "href")
|
||||||
:sx-target "#main-panel"
|
:sx-target "#sx-content"
|
||||||
:sx-select "#main-panel"
|
:sx-select "#sx-content"
|
||||||
:sx-swap "innerHTML"
|
:sx-swap "innerHTML"
|
||||||
:sx-push-url "true"
|
:sx-push-url "true"
|
||||||
:class "px-3 py-1.5 rounded border transition-colors"
|
:class "px-3 py-1.5 rounded border transition-colors"
|
||||||
@@ -149,6 +149,7 @@
|
|||||||
(div
|
(div
|
||||||
:id "sx-nav"
|
:id "sx-nav"
|
||||||
:class "mb-6"
|
:class "mb-6"
|
||||||
|
:sx-swap-oob "innerHTML"
|
||||||
(div
|
(div
|
||||||
:id "logo-opacity"
|
:id "logo-opacity"
|
||||||
:style (str
|
:style (str
|
||||||
@@ -170,7 +171,7 @@
|
|||||||
(when
|
(when
|
||||||
(get nav-state "children")
|
(get nav-state "children")
|
||||||
(~layouts/nav-children :items (get nav-state "children"))))
|
(~layouts/nav-children :items (get nav-state "children"))))
|
||||||
children
|
(div :id "sx-content" (error-boundary children))
|
||||||
(~cssx/flush))))
|
(~cssx/flush))))
|
||||||
|
|
||||||
(defcomp ~layouts/docs-layout-full () nil)
|
(defcomp ~layouts/docs-layout-full () nil)
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
;; 404 Not Found page content
|
(defcomp
|
||||||
|
~not-found/content
|
||||||
(defcomp ~not-found/content (&key (path :as string?))
|
(&key (path :as string?))
|
||||||
(div :class "max-w-3xl mx-auto px-4 py-12 text-center"
|
(div
|
||||||
(h1 :style (tw "text-stone-800 text-3xl font-bold")
|
:class "max-w-3xl mx-auto px-4 py-12 text-center"
|
||||||
"404")
|
(h1 :style (tw "text-stone-800 text-3xl font-bold") "404")
|
||||||
(p :class "mt-4"
|
(p :class "mt-4" :style (tw "text-stone-500 text-lg") "Page not found")
|
||||||
:style (tw "text-stone-500 text-lg")
|
(when
|
||||||
"Page not found")
|
path
|
||||||
(when path
|
(p :class "mt-2" :style (tw "text-stone-400 text-sm font-mono") path))
|
||||||
(p :class "mt-2"
|
(a
|
||||||
:style (tw "text-stone-400 text-sm font-mono")
|
:href "/sx/"
|
||||||
path))
|
:sx-get "/sx/"
|
||||||
(a :href "/sx/"
|
:sx-target "#sx-content"
|
||||||
:sx-get "/sx/" :sx-target "#main-panel" :sx-select "#main-panel"
|
:sx-select "#sx-content"
|
||||||
:sx-swap "outerHTML" :sx-push-url "true"
|
:sx-swap "outerHTML"
|
||||||
:class "inline-block mt-6 px-4 py-2 rounded border transition-colors"
|
:sx-push-url "true"
|
||||||
:style (tw "text-violet-700 text-sm border-violet-200")
|
: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")))
|
"Back to home")))
|
||||||
|
|||||||
@@ -1,81 +1,104 @@
|
|||||||
;; Optimistic update demo — exercises Phase 7c client-side predicted mutations.
|
(defcomp
|
||||||
;;
|
~optimistic-demo/content
|
||||||
;; This page shows a todo list with optimistic add/remove.
|
(&key items server-time)
|
||||||
;; Mutations are predicted client-side, sent to server, and confirmed/reverted.
|
(div
|
||||||
;;
|
:class "space-y-8"
|
||||||
;; Open browser console and look for:
|
(div
|
||||||
;; "sx:optimistic confirmed" — server accepted the mutation
|
:class "border-b border-stone-200 pb-6"
|
||||||
;; "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"
|
|
||||||
(h1 :class "text-2xl font-bold text-stone-900" "Optimistic Updates")
|
(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 "
|
"This page tests Phase 7c optimistic data mutations. Items are updated "
|
||||||
"instantly on the client, then confirmed or reverted when the server responds."))
|
"instantly on the client, then confirmed or reverted when the server responds."))
|
||||||
|
(div
|
||||||
;; Server metadata
|
: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")
|
(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")
|
(dt :class "font-medium text-stone-600" "Server time")
|
||||||
(dd :class "font-mono text-stone-900" server-time)
|
(dd :class "font-mono text-stone-900" server-time)
|
||||||
(dt :class "font-medium text-stone-600" "Item count")
|
(dt :class "font-medium text-stone-600" "Item count")
|
||||||
(dd :class "text-stone-900" (str (len items)))))
|
(dd :class "text-stone-900" (str (len items)))))
|
||||||
|
(div
|
||||||
;; Item list
|
:class "space-y-3"
|
||||||
(div :class "space-y-3"
|
|
||||||
(h2 :class "text-lg font-semibold text-stone-800" "Items")
|
(h2 :class "text-lg font-semibold text-stone-800" "Items")
|
||||||
(div :id "optimistic-items" :class "space-y-2"
|
(div
|
||||||
(map (fn (item)
|
:id "optimistic-items"
|
||||||
(div :class "flex items-center justify-between rounded border border-stone-100 bg-white p-3"
|
:class "space-y-2"
|
||||||
(div :class "flex items-center gap-3"
|
(map
|
||||||
(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"
|
(fn
|
||||||
(str (get item "id")))
|
(item)
|
||||||
(span :class "text-stone-900" (get item "label")))
|
(div
|
||||||
(span :class "text-xs px-2 py-0.5 rounded-full"
|
:class "flex items-center justify-between rounded border border-stone-100 bg-white p-3"
|
||||||
:class (case (get item "status")
|
(div
|
||||||
"confirmed" "bg-green-100 text-green-700"
|
:class "flex items-center gap-3"
|
||||||
"pending" "bg-amber-100 text-amber-700"
|
(span
|
||||||
"reverted" "bg-red-100 text-red-700"
|
:class "flex-none rounded-full bg-violet-100 text-violet-700 w-6 h-6 flex items-center justify-center text-xs font-bold"
|
||||||
:else "bg-stone-100 text-stone-500")
|
(str (get item "id")))
|
||||||
(get item "status"))))
|
(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))
|
items))
|
||||||
|
(div
|
||||||
;; Add button — triggers optimistic mutation
|
:class "pt-2"
|
||||||
(div :class "pt-2"
|
(button
|
||||||
(button :class "px-4 py-2 bg-violet-500 text-white rounded hover:bg-violet-600 text-sm"
|
: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-post "/sx/action/add-demo-item"
|
||||||
:sx-target "#main-panel"
|
:sx-target "#sx-content"
|
||||||
:sx-vals "{\"label\": \"New item\"}"
|
:sx-vals "{\"label\": \"New item\"}"
|
||||||
"Add item (optimistic)")))
|
"Add item (optimistic)")))
|
||||||
|
(div
|
||||||
;; How it works
|
:class "space-y-4"
|
||||||
(div :class "space-y-4"
|
|
||||||
(h2 :class "text-lg font-semibold text-stone-800" "How it works")
|
(h2 :class "text-lg font-semibold text-stone-800" "How it works")
|
||||||
(div :class "space-y-2"
|
(div
|
||||||
|
:class "space-y-2"
|
||||||
(map-indexed
|
(map-indexed
|
||||||
(fn (i step)
|
(fn
|
||||||
(div :class "flex items-start gap-3 rounded border border-stone-100 bg-white p-3"
|
(i step)
|
||||||
(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"
|
(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)))
|
(str (+ i 1)))
|
||||||
(div
|
(div
|
||||||
(div :class "font-medium text-stone-900" (get step "label"))
|
(div :class "font-medium text-stone-900" (get step "label"))
|
||||||
(div :class "text-sm text-stone-500" (get step "detail")))))
|
(div :class "text-sm text-stone-500" (get step "detail")))))
|
||||||
(list
|
(list
|
||||||
(dict :label "Predict" :detail "Client applies mutator function to cached data immediately")
|
(dict
|
||||||
(dict :label "Snapshot" :detail "Pre-mutation data saved in _optimistic-snapshots for rollback")
|
:label "Predict"
|
||||||
(dict :label "Re-render" :detail "Page content re-evaluated and swapped with predicted data")
|
:detail "Client applies mutator function to cached data immediately")
|
||||||
(dict :label "Submit" :detail "Mutation sent to server via POST /sx/action/<name>")
|
(dict
|
||||||
(dict :label "Confirm or revert" :detail "Server responds — cache updated with truth, or reverted to snapshot")))))
|
:label "Snapshot"
|
||||||
|
:detail "Pre-mutation data saved in _optimistic-snapshots for rollback")
|
||||||
;; How to verify
|
(dict
|
||||||
(div :class "rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm space-y-2"
|
: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")
|
(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 "Open the browser console (F12)")
|
||||||
(li "Navigate to this page from another isomorphism page")
|
(li "Navigate to this page from another isomorphism page")
|
||||||
(li "Click \"Add item\" — item appears instantly with \"pending\" status")
|
(li
|
||||||
(li "Watch console for " (code :class "bg-amber-100 px-1 rounded" "sx:optimistic confirmed"))
|
"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")))))
|
(li "Item status changes to \"confirmed\" when server responds")))))
|
||||||
|
|||||||
@@ -1,25 +1,28 @@
|
|||||||
;; Plans section — architecture roadmaps and implementation plans
|
(defcomp
|
||||||
|
~plans/index/plans-index-content
|
||||||
;; ---------------------------------------------------------------------------
|
()
|
||||||
;; Plans index page
|
(~docs/page
|
||||||
;; ---------------------------------------------------------------------------
|
:title "Plans"
|
||||||
|
(div
|
||||||
(defcomp ~plans/index/plans-index-content ()
|
:class "space-y-4"
|
||||||
(~docs/page :title "Plans"
|
(p
|
||||||
(div :class "space-y-4"
|
:class "text-lg text-stone-600 mb-4"
|
||||||
(p :class "text-lg text-stone-600 mb-4"
|
|
||||||
"Architecture roadmaps and implementation plans for SX.")
|
"Architecture roadmaps and implementation plans for SX.")
|
||||||
(div :class "space-y-3"
|
(div
|
||||||
(map (fn (item)
|
:class "space-y-3"
|
||||||
(a :href (get item "href")
|
(map
|
||||||
:sx-get (get item "href") :sx-target "#main-panel" :sx-select "#main-panel"
|
(fn
|
||||||
:sx-swap "outerHTML" :sx-push-url "true"
|
(item)
|
||||||
:class "block rounded border border-stone-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"
|
(a
|
||||||
(div :class "font-semibold text-stone-800" (get item "label"))
|
:href (get item "href")
|
||||||
(when (get item "summary")
|
:sx-get (get item "href")
|
||||||
(p :class "text-sm text-stone-500 mt-1" (get item "summary")))))
|
: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)))))
|
plans-nav-items)))))
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
|
||||||
;; Reader Macros
|
|
||||||
;; ---------------------------------------------------------------------------
|
|
||||||
|
|||||||
@@ -41,8 +41,8 @@
|
|||||||
(a
|
(a
|
||||||
:href (get item "href")
|
:href (get item "href")
|
||||||
:sx-get (get item "href")
|
:sx-get (get item "href")
|
||||||
:sx-target "#main-panel"
|
:sx-target "#sx-content"
|
||||||
:sx-select "#main-panel"
|
:sx-select "#sx-content"
|
||||||
:sx-swap "outerHTML"
|
:sx-swap "outerHTML"
|
||||||
:sx-push-url "true"
|
:sx-push-url "true"
|
||||||
:class "text-violet-600 hover:underline"
|
:class "text-violet-600 hover:underline"
|
||||||
|
|||||||
@@ -1,58 +1,128 @@
|
|||||||
;; ---------------------------------------------------------------------------
|
(defcomp
|
||||||
;; Event Bridge — DOM events for lake→island communication
|
~reactive-islands/event-bridge/reactive-islands-event-bridge-content
|
||||||
;; ---------------------------------------------------------------------------
|
()
|
||||||
|
(~docs/page
|
||||||
(defcomp ~reactive-islands/event-bridge/reactive-islands-event-bridge-content ()
|
:title "Event Bridge"
|
||||||
(~docs/page :title "Event Bridge"
|
(~docs/section
|
||||||
|
:title "The Problem"
|
||||||
(~docs/section :title "The Problem" :id "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
|
||||||
(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.")
|
"A reactive island can contain server-rendered content — an htmx \"lake\" that swaps via "
|
||||||
(p "The event bridge solves this: DOM custom events bubble from the lake up to the island, where an effect listens and updates signals."))
|
(code "sx-get")
|
||||||
|
"/"
|
||||||
(~docs/section :title "How it works" :id "how"
|
(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:")
|
(p "Three components:")
|
||||||
(ol :class "space-y-2 text-stone-600 list-decimal list-inside"
|
(ol
|
||||||
(li (strong "Server emits: ") "Server-rendered elements carry " (code "data-sx-emit") " attributes. When the user interacts, the client dispatches a CustomEvent.")
|
:class "space-y-2 text-stone-600 list-decimal list-inside"
|
||||||
(li (strong "Event bubbles: ") "The event bubbles up through the DOM tree until it reaches the island container.")
|
(li
|
||||||
(li (strong "Effect catches: ") "An effect inside the island listens for the event name and updates a signal."))
|
(strong "Server emits: ")
|
||||||
|
"Server-rendered elements carry "
|
||||||
(~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"))
|
(code "data-sx-emit")
|
||||||
|
" attributes. When the user interacts, the client dispatches a CustomEvent.")
|
||||||
(p "The server handler for " (code "/products/:id/details") " returns HTML with emit attributes:")
|
(li
|
||||||
(~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"))
|
(strong "Event bubbles: ")
|
||||||
(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."))
|
"The event bubbles up through the DOM tree until it reaches the island container.")
|
||||||
|
(li
|
||||||
(~docs/section :title "Why signals survive swaps" :id "survival"
|
(strong "Effect catches: ")
|
||||||
(p "Signals live in JavaScript memory (closures), not in the DOM. When htmx swaps content inside an island:")
|
"An effect inside the island listens for the event name and updates a signal."))
|
||||||
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
|
(~docs/code
|
||||||
(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.")
|
:src (highlight
|
||||||
(li (strong "Swap outside island: ") "Signals survive. The island is not affected by swaps to other parts of the page.")
|
";; 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\"))))"
|
||||||
(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.")))
|
"lisp"))
|
||||||
|
(p
|
||||||
(~docs/section :title "Spec" :id "spec"
|
"The server handler for "
|
||||||
(p "The event bridge is spec'd in " (code "signals.sx") " (sections 12-13). Three functions:")
|
(code "/products/:id/details")
|
||||||
|
" returns HTML with emit attributes:")
|
||||||
(~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"))
|
(~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:")
|
(p "Platform interface required:")
|
||||||
(div :class "overflow-x-auto rounded border border-stone-200 mt-2"
|
(div
|
||||||
(table :class "w-full text-left text-sm"
|
:class "overflow-x-auto rounded border border-stone-200 mt-2"
|
||||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
(table
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Function")
|
:class "w-full text-left text-sm"
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
|
(thead
|
||||||
(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
|
(tr
|
||||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "(event-detail e)")
|
:class "border-b border-stone-200 bg-stone-100"
|
||||||
(td :class "px-3 py-2 text-stone-700" "Extract .detail from CustomEvent"))))))))
|
(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
|
||||||
;; Named Stores — page-level signal containers
|
: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 @@
|
|||||||
;; ---------------------------------------------------------------------------
|
(defcomp
|
||||||
;; Plan — the full design document (moved from plans section)
|
~reactive-islands/plan/reactive-islands-plan-content
|
||||||
;; ---------------------------------------------------------------------------
|
()
|
||||||
|
(~docs/page
|
||||||
(defcomp ~reactive-islands/plan/reactive-islands-plan-content ()
|
:title "Reactive Islands Plan"
|
||||||
(~docs/page :title "Reactive Islands Plan"
|
(~docs/section
|
||||||
|
:title "Context"
|
||||||
(~docs/section :title "Context" :id "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
|
||||||
(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.")
|
"SX already has a sliding bar for "
|
||||||
(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:")
|
(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.")
|
||||||
(div :class "overflow-x-auto mt-4 mb-4"
|
(p
|
||||||
(table :class "w-full text-sm text-left"
|
"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
|
(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" "")
|
||||||
(th :class "py-2 px-3 font-semibold text-stone-700" "Server State")
|
(th
|
||||||
(th :class "py-2 px-3 font-semibold text-stone-700" "Client State")))
|
:class "py-2 px-3 font-semibold text-stone-700"
|
||||||
(tbody :class "text-stone-600"
|
"Server State")
|
||||||
(tr :class "border-b border-stone-100"
|
(th
|
||||||
(td :class "py-2 px-3 font-semibold text-stone-700" "Server Rendering")
|
: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" "Pure hypermedia (htmx)")
|
||||||
(td :class "py-2 px-3" "SSR + hydrated islands (Next.js)"))
|
(td :class "py-2 px-3" "SSR + hydrated islands (Next.js)"))
|
||||||
(tr :class "border-b border-stone-100"
|
(tr
|
||||||
(td :class "py-2 px-3 font-semibold text-stone-700" "Client Rendering")
|
: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" "SX wire format (current)")
|
||||||
(td :class "py-2 px-3 font-semibold text-violet-700" "Reactive islands (this plan)")))))
|
(td
|
||||||
|
:class "py-2 px-3 font-semibold text-violet-700"
|
||||||
(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."))
|
"Reactive islands (this plan)")))))
|
||||||
|
(p
|
||||||
(~docs/section :title "The Spectrum" :id "spectrum"
|
"Today SX occupies the bottom-left quadrant — client-rendered components with server state. This plan adds the bottom-right: "
|
||||||
(p "Four levels of client interactivity. Each is independently valuable. Each is opt-in per component.")
|
(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/subsection :title "Level 0: Pure Hypermedia"
|
(~docs/section
|
||||||
(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."))
|
:title "The Spectrum"
|
||||||
|
:id "spectrum"
|
||||||
(~docs/subsection :title "Level 1: Local DOM Operations"
|
(p
|
||||||
(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."))
|
"Four levels of client interactivity. Each is independently valuable. Each is opt-in per component.")
|
||||||
|
(~docs/subsection
|
||||||
(~docs/subsection :title "Level 2: Reactive Islands"
|
:title "Level 0: Pure Hypermedia"
|
||||||
(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."))
|
(p
|
||||||
|
"The default. "
|
||||||
(~docs/subsection :title "Level 3: Connected Islands"
|
(code "sx-get")
|
||||||
(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.")))
|
", "
|
||||||
|
(code "sx-post")
|
||||||
(~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.")
|
(code "sx-swap")
|
||||||
(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") ".")
|
". 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
|
||||||
(~docs/subsection :title "Navigation scenarios"
|
:title "Level 1: Local DOM Operations"
|
||||||
(div :class "space-y-3"
|
(p
|
||||||
(div :class "rounded border border-green-200 bg-green-50 p-3"
|
"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")
|
(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."))
|
(p
|
||||||
(div :class "rounded border border-green-200 bg-green-50 p-3"
|
: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")
|
(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."))
|
(p
|
||||||
(div :class "rounded border border-amber-200 bg-amber-50 p-3"
|
: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")
|
(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."))
|
(p
|
||||||
(div :class "rounded border border-stone-200 p-3"
|
: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")
|
(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.")))))
|
(p
|
||||||
|
:class "text-sm text-stone-600 mt-1"
|
||||||
(~docs/section :title "Reactive DOM Rendering" :id "reactive-rendering"
|
"Everything cleared. clean slate. clear-stores wipes the registry.")))))
|
||||||
(p "The existing " (code "renderDOM") " function walks the AST and creates DOM nodes. Inside an island, it becomes signal-aware:")
|
(~docs/section
|
||||||
|
:title "Reactive DOM Rendering"
|
||||||
(~docs/subsection :title "Text bindings"
|
:id "reactive-rendering"
|
||||||
(~docs/code :src (highlight ";; (span (deref count)) creates:\n;; const text = document.createTextNode(sig.value)\n;; effect(() => text.nodeValue = sig.value)" "lisp"))
|
(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."))
|
(p "Only the text node updates. The span is untouched."))
|
||||||
|
(~docs/subsection
|
||||||
(~docs/subsection :title "Attribute bindings"
|
:title "Attribute bindings"
|
||||||
(~docs/code :src (highlight ";; (div :class (str \"panel \" (if (deref open?) \"visible\" \"hidden\")))\n;; effect(() => div.className = ...)" "lisp")))
|
(~docs/code
|
||||||
|
:src (highlight
|
||||||
(~docs/subsection :title "Conditional fragments"
|
";; (div :class (str \"panel \" (if (deref open?) \"visible\" \"hidden\")))\n;; effect(() => div.className = ...)"
|
||||||
(~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"))
|
"lisp")))
|
||||||
(p "Equivalent to SolidJS's " (code "Show") " — but falls out naturally from the evaluator."))
|
(~docs/subsection
|
||||||
|
:title "Conditional fragments"
|
||||||
(~docs/subsection :title "List rendering"
|
(~docs/code
|
||||||
(~docs/code :src (highlight "(map (fn (item) (li :key (get item \"id\") (get item \"name\")))\n (deref items))" "lisp"))
|
:src (highlight
|
||||||
(p "Keyed elements are reused and reordered. Unkeyed elements are morphed.")))
|
";; (when (deref show?) (~details)) creates:\n;; A marker comment node, then:\n;; effect(() => show ? insert-after(marker, render(~details)) : remove)"
|
||||||
|
"lisp"))
|
||||||
(~docs/section :title "Status" :id "status"
|
(p
|
||||||
(div :class "overflow-x-auto rounded border border-stone-200"
|
"Equivalent to SolidJS's "
|
||||||
(table :class "w-full text-left text-sm"
|
(code "Show")
|
||||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
" — but falls out naturally from the evaluator."))
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Task")
|
(~docs/subsection
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Status")
|
:title "List rendering"
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
|
(~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
|
(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-stone-700" "Signal runtime")
|
||||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
(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"))
|
(td
|
||||||
(tr :class "border-b border-stone-100"
|
: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-stone-700" "Named stores (L3)")
|
||||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
(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"))
|
(td
|
||||||
(tr :class "border-b border-stone-100"
|
: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-stone-700" "Event bridge")
|
||||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
(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"))
|
(td
|
||||||
(tr :class "border-b border-stone-100"
|
: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-stone-700" "Event bindings")
|
||||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
(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"))
|
(td
|
||||||
(tr :class "border-b border-stone-100"
|
: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-stone-700" "data-sx-emit")
|
||||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
(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"))
|
(td
|
||||||
(tr :class "border-b border-stone-100"
|
: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-stone-700" "Client hydration")
|
||||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
(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"))
|
(td
|
||||||
(tr :class "border-b border-stone-100"
|
: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-stone-700" "Bootstrapping")
|
||||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
(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"))
|
(td
|
||||||
(tr :class "border-b border-stone-100"
|
: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-stone-700" "Island disposal")
|
||||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
(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"))
|
(td
|
||||||
(tr :class "border-b border-stone-100"
|
: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-stone-700" "Reactive list")
|
||||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
(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"))
|
(td
|
||||||
(tr :class "border-b border-stone-100"
|
:class "px-3 py-2 text-stone-700"
|
||||||
(td :class "px-3 py-2 text-stone-700" "Input binding + keyed lists")
|
"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-green-700 font-medium" "Done")
|
||||||
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: :bind signal, :key attr"))
|
(td
|
||||||
(tr :class "border-b border-stone-100"
|
: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-stone-700" "Portals")
|
||||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
(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"))
|
(td
|
||||||
(tr :class "border-b border-stone-100"
|
: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-stone-700" "Error boundaries")
|
||||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
(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"))
|
(td
|
||||||
(tr :class "border-b border-stone-100"
|
: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-700" "Suspense")
|
||||||
(td :class "px-3 py-2 text-stone-500 font-medium" "N/A")
|
(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
|
(tr
|
||||||
(td :class "px-3 py-2 text-stone-700" "Transitions")
|
(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-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"
|
||||||
(~docs/section :title "Design Principles" :id "principles"
|
"covered by existing primitives"))))))
|
||||||
(ol :class "space-y-3 text-stone-600 list-decimal list-inside"
|
(~docs/section
|
||||||
(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.")
|
:title "Design Principles"
|
||||||
(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.")
|
:id "principles"
|
||||||
(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.")
|
(ol
|
||||||
(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") ".")
|
:class "space-y-3 text-stone-600 list-decimal list-inside"
|
||||||
(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
|
||||||
(li (strong "No build step.") " Reactive bindings are created at runtime during DOM rendering. No JSX compilation, no Babel transforms, no Vite plugins."))
|
(strong "Islands are opt-in.")
|
||||||
|
" "
|
||||||
(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."))))
|
(code "defcomp")
|
||||||
|
" remains the default. Components are inert unless you choose "
|
||||||
|
(code "defisland")
|
||||||
;; ---------------------------------------------------------------------------
|
". No reactive overhead for static content.")
|
||||||
;; Phase 2 Plan — remaining reactive features
|
(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
|
(defcomp
|
||||||
;; @css bg-blue-100 text-blue-700 bg-emerald-100 text-emerald-700 bg-amber-100 text-amber-700
|
~reference/attrs-content
|
||||||
|
(&key req-table beh-table uniq-table)
|
||||||
(defcomp ~reference/attrs-content (&key req-table beh-table uniq-table)
|
(~docs/page
|
||||||
(~docs/page :title "Attribute Reference"
|
:title "Attribute Reference"
|
||||||
(p :class "text-stone-600 mb-6"
|
(p
|
||||||
|
:class "text-stone-600 mb-6"
|
||||||
"sx attributes mirror htmx where possible. This table shows all available attributes and their status.")
|
"sx attributes mirror htmx where possible. This table shows all available attributes and their status.")
|
||||||
(div :class "space-y-8"
|
(div :class "space-y-8" req-table beh-table uniq-table)))
|
||||||
req-table
|
|
||||||
beh-table
|
|
||||||
uniq-table)))
|
|
||||||
|
|
||||||
(defcomp ~reference/headers-content (&key req-table resp-table)
|
(defcomp
|
||||||
(~docs/page :title "Headers"
|
~reference/headers-content
|
||||||
(p :class "text-stone-600 mb-6"
|
(&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.")
|
"sx uses custom HTTP headers to coordinate between client and server.")
|
||||||
(div :class "space-y-8"
|
(div :class "space-y-8" req-table resp-table)))
|
||||||
req-table
|
|
||||||
resp-table)))
|
|
||||||
|
|
||||||
(defcomp ~reference/events-content (&key table)
|
(defcomp
|
||||||
(~docs/page :title "Events"
|
~reference/events-content
|
||||||
(p :class "text-stone-600 mb-6"
|
(&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. "
|
"sx fires custom DOM events at various points in the request lifecycle. "
|
||||||
"Listen for them with sx-on:* attributes or addEventListener. "
|
"Listen for them with sx-on:* attributes or addEventListener. "
|
||||||
"Client-side routing fires sx:clientRoute instead of request lifecycle events.")
|
"Client-side routing fires sx:clientRoute instead of request lifecycle events.")
|
||||||
table))
|
table))
|
||||||
|
|
||||||
(defcomp ~reference/js-api-content (&key table)
|
(defcomp
|
||||||
(~docs/page :title "JavaScript API"
|
~reference/js-api-content
|
||||||
table))
|
(&key table)
|
||||||
|
(~docs/page :title "JavaScript API" table))
|
||||||
|
|
||||||
(defcomp ~reference/attr-detail-content (&key (title :as string) (description :as string) demo
|
(defcomp
|
||||||
(example-code :as string) (handler-code :as string?) (wire-placeholder-id :as string?))
|
~reference/attr-detail-content
|
||||||
(~docs/page :title title
|
(&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)
|
(p :class "text-stone-600 mb-6" description)
|
||||||
(when demo
|
(when demo (~examples/card :title "Demo" (~examples/demo demo)))
|
||||||
(~examples/card :title "Demo"
|
|
||||||
(~examples/demo demo)))
|
|
||||||
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")
|
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")
|
||||||
(~examples/source :code (highlight example-code "lisp"))
|
(~examples/source :src-code (highlight example-code "lisp"))
|
||||||
(when handler-code
|
(when
|
||||||
|
handler-code
|
||||||
(<>
|
(<>
|
||||||
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")
|
(h3
|
||||||
(~examples/source :code (highlight handler-code "lisp"))))
|
:class "text-lg font-semibold text-stone-700 mt-6"
|
||||||
(when wire-placeholder-id
|
"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")
|
(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.")
|
"Trigger the demo to see the raw response the server sends.")
|
||||||
(~docs/placeholder :id wire-placeholder-id)))))
|
(~docs/placeholder :id wire-placeholder-id)))))
|
||||||
|
|
||||||
(defcomp ~reference/header-detail-content (&key (title :as string) (direction :as string) (description :as string)
|
(defcomp
|
||||||
(example-code :as string?) demo)
|
~reference/header-detail-content
|
||||||
(~docs/page :title title
|
(&key
|
||||||
(let ((badge-class (if (= direction "request")
|
(title :as string)
|
||||||
"bg-blue-100 text-blue-700"
|
(direction :as string)
|
||||||
(if (= direction "response")
|
(description :as string)
|
||||||
"bg-emerald-100 text-emerald-700"
|
(example-code :as string?)
|
||||||
"bg-amber-100 text-amber-700")))
|
demo)
|
||||||
(badge-label (if (= direction "request") "Request Header"
|
(~docs/page
|
||||||
(if (= direction "response") "Response Header"
|
: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"))))
|
"Request & Response"))))
|
||||||
(div :class "flex items-center gap-3 mb-4"
|
(div
|
||||||
(span :class (str "text-xs font-medium px-2 py-1 rounded " badge-class)
|
:class "flex items-center gap-3 mb-4"
|
||||||
|
(span
|
||||||
|
:class (str "text-xs font-medium px-2 py-1 rounded " badge-class)
|
||||||
badge-label)))
|
badge-label)))
|
||||||
(p :class "text-stone-600 mb-6" description)
|
(p :class "text-stone-600 mb-6" description)
|
||||||
(when demo
|
(when demo (~examples/card :title "Demo" (~examples/demo demo)))
|
||||||
(~examples/card :title "Demo"
|
(when
|
||||||
(~examples/demo demo)))
|
example-code
|
||||||
(when example-code
|
|
||||||
(<>
|
(<>
|
||||||
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Example usage")
|
(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)
|
(defcomp
|
||||||
(~docs/page :title title
|
~reference/event-detail-content
|
||||||
|
(&key title description example-code demo)
|
||||||
|
(~docs/page
|
||||||
|
:title title
|
||||||
(p :class "text-stone-600 mb-6" description)
|
(p :class "text-stone-600 mb-6" description)
|
||||||
(when demo
|
(when demo (~examples/card :title "Demo" (~examples/demo demo)))
|
||||||
(~examples/card :title "Demo"
|
(when
|
||||||
(~examples/demo demo)))
|
example-code
|
||||||
(when example-code
|
|
||||||
(<>
|
(<>
|
||||||
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Example usage")
|
(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))
|
(defcomp
|
||||||
(~docs/page :title "Not Found"
|
~reference/attr-not-found
|
||||||
(p :class "text-stone-600"
|
(&key (slug :as string))
|
||||||
(str "No documentation found for \"" slug "\"."))))
|
(~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.
|
(defcomp
|
||||||
;; Shows which pages route client-side (pure, instant) vs server-side (IO/data).
|
~routing-analyzer/content
|
||||||
;; @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
|
(&key pages total-pages client-count server-count registry-sample)
|
||||||
|
(~docs/page
|
||||||
(defcomp ~routing-analyzer/content (&key pages total-pages client-count
|
:title "Routing Analyzer"
|
||||||
server-count registry-sample)
|
(p
|
||||||
(~docs/page :title "Routing Analyzer"
|
:class "text-stone-600 mb-6"
|
||||||
|
"Live classification of all "
|
||||||
(p :class "text-stone-600 mb-6"
|
(strong (str total-pages))
|
||||||
"Live classification of all " (strong (str total-pages)) " pages by routing mode. "
|
" pages by routing mode. "
|
||||||
"Pages without " (code ":data") " dependencies are "
|
"Pages without "
|
||||||
|
(code ":data")
|
||||||
|
" dependencies are "
|
||||||
(span :class "text-green-700 font-medium" "client-routable")
|
(span :class "text-green-700 font-medium" "client-routable")
|
||||||
" — after initial load they render instantly from the page registry without a server roundtrip. "
|
" — after initial load they render instantly from the page registry without a server roundtrip. "
|
||||||
"Pages with data dependencies fall back to "
|
"Pages with data dependencies fall back to "
|
||||||
(span :class "text-amber-700 font-medium" "server fetch")
|
(span :class "text-amber-700 font-medium" "server fetch")
|
||||||
" transparently. Powered by "
|
" 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 "
|
" 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.")
|
" IO detection.")
|
||||||
|
(div
|
||||||
(div :class "mb-8 grid grid-cols-4 gap-4"
|
:class "mb-8 grid grid-cols-4 gap-4"
|
||||||
(~analyzer/stat :label "Total Pages" :value (str total-pages)
|
(~analyzer/stat
|
||||||
:cls "text-violet-600")
|
:label "Total Pages"
|
||||||
(~analyzer/stat :label "Client-Routable" :value (str client-count)
|
:value (str total-pages)
|
||||||
:cls "text-green-600")
|
:cls "text-violet-600")
|
||||||
(~analyzer/stat :label "Server-Only" :value (str server-count)
|
(~analyzer/stat
|
||||||
:cls "text-amber-600")
|
:label "Client-Routable"
|
||||||
(~analyzer/stat :label "Client Ratio" :value (str (round (* (/ client-count total-pages) 100)) "%")
|
:value (str client-count)
|
||||||
:cls "text-blue-600"))
|
:cls "text-green-600")
|
||||||
|
(~analyzer/stat
|
||||||
;; Route classification bar
|
:label "Server-Only"
|
||||||
(div :class "mb-8"
|
:value (str server-count)
|
||||||
(div :class "flex items-center gap-2 mb-2"
|
: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")
|
(span :class "text-sm font-medium text-stone-600" "Client")
|
||||||
(div :class "flex-1")
|
(div :class "flex-1")
|
||||||
(span :class "text-sm font-medium text-stone-600" "Server"))
|
(span :class "text-sm font-medium text-stone-600" "Server"))
|
||||||
(div :class "w-full bg-amber-200 rounded-full h-4 overflow-hidden"
|
(div
|
||||||
(div :class "bg-green-500 h-4 rounded-l-full transition-all"
|
:class "w-full bg-amber-200 rounded-full h-4 overflow-hidden"
|
||||||
:style (str "width: " (round (* (/ client-count total-pages) 100)) "%"))))
|
(div
|
||||||
|
:class "bg-green-500 h-4 rounded-l-full transition-all"
|
||||||
(~docs/section :title "Route Table" :id "routes"
|
:style (str "width: " (round (* (/ client-count total-pages) 100)) "%"))))
|
||||||
(div :class "space-y-2"
|
(~docs/section
|
||||||
(map (fn (page)
|
:title "Route Table"
|
||||||
(~routing-analyzer/routing-row
|
:id "routes"
|
||||||
:name (get page "name")
|
(div
|
||||||
:path (get page "path")
|
:class "space-y-2"
|
||||||
:mode (get page "mode")
|
(map
|
||||||
:has-data (get page "has-data")
|
(fn
|
||||||
:content-expr (get page "content-expr")
|
(page)
|
||||||
:reason (get page "reason")))
|
(~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)))
|
pages)))
|
||||||
|
(~docs/section
|
||||||
(~docs/section :title "Page Registry Format" :id "registry"
|
:title "Page Registry Format"
|
||||||
(p :class "text-stone-600 mb-4"
|
:id "registry"
|
||||||
|
(p
|
||||||
|
:class "text-stone-600 mb-4"
|
||||||
"The server serializes page metadata as SX dict literals inside "
|
"The server serializes page metadata as SX dict literals inside "
|
||||||
(code "<script type=\"text/sx-pages\">")
|
(code "<script type=\"text/sx-pages\">")
|
||||||
". The client's parser reads these at boot, building a route table with parsed URL patterns. "
|
". 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.")
|
"No JSON involved — the same SX parser handles everything.")
|
||||||
(when (not (empty? registry-sample))
|
(when
|
||||||
(div :class "not-prose"
|
(not (empty? registry-sample))
|
||||||
(pre :class "text-xs leading-relaxed whitespace-pre-wrap overflow-x-auto bg-stone-100 rounded border border-stone-200 p-4"
|
(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"))))))
|
(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"
|
(defcomp
|
||||||
(ol :class "list-decimal pl-5 space-y-2 text-stone-700"
|
~routing-analyzer/routing-row
|
||||||
(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") ".")
|
(&key
|
||||||
(li (strong "Click: ") "orchestration.sx intercepts boost link clicks via " (code "bind-client-route-link") ". Extracts the pathname from the href.")
|
(name :as string)
|
||||||
(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.")
|
(path :as string)
|
||||||
(li (strong "Check: ") "If the matched page has " (code ":has-data true") ", skip to server fetch. Otherwise proceed to client eval.")
|
(mode :as string)
|
||||||
(li (strong "Eval: ") (code "try-eval-content") " merges the component env + URL params + closure, then parses and renders the content expression to DOM.")
|
(has-data :as boolean)
|
||||||
(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") ".")
|
(content-expr :as string?)
|
||||||
(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.")))))
|
(reason :as string?))
|
||||||
|
(div
|
||||||
(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?))
|
:class (str
|
||||||
(div :class (str "rounded border p-3 flex items-center gap-3 "
|
"rounded border p-3 flex items-center gap-3 "
|
||||||
(if (= mode "client")
|
(if
|
||||||
"border-green-200 bg-green-50"
|
(= mode "client")
|
||||||
"border-amber-200 bg-amber-50"))
|
"border-green-200 bg-green-50"
|
||||||
;; Mode badge
|
"border-amber-200 bg-amber-50"))
|
||||||
(span :class (str "inline-block px-2 py-0.5 rounded text-xs font-bold uppercase "
|
(span
|
||||||
(if (= mode "client")
|
:class (str
|
||||||
"bg-green-600 text-white"
|
"inline-block px-2 py-0.5 rounded text-xs font-bold uppercase "
|
||||||
"bg-amber-500 text-white"))
|
(if
|
||||||
|
(= mode "client")
|
||||||
|
"bg-green-600 text-white"
|
||||||
|
"bg-amber-500 text-white"))
|
||||||
mode)
|
mode)
|
||||||
;; Page info
|
(div
|
||||||
(div :class "flex-1 min-w-0"
|
:class "flex-1 min-w-0"
|
||||||
(div :class "flex items-center gap-2"
|
(div
|
||||||
|
:class "flex items-center gap-2"
|
||||||
(span :class "font-mono font-semibold text-stone-800 text-sm" name)
|
(span :class "font-mono font-semibold text-stone-800 text-sm" name)
|
||||||
(span :class "text-stone-400 text-xs font-mono" path))
|
(span :class "text-stone-400 text-xs font-mono" path))
|
||||||
(when reason
|
(when reason (div :class "text-xs text-stone-500 mt-0.5" reason)))
|
||||||
(div :class "text-xs text-stone-500 mt-0.5" reason)))
|
(when
|
||||||
;; Content expression
|
content-expr
|
||||||
(when content-expr
|
(div
|
||||||
(div :class "hidden md:block max-w-xs truncate"
|
:class "hidden md:block max-w-xs truncate"
|
||||||
(code :class "text-xs text-stone-500" content-expr)))))
|
(code :class "text-xs text-stone-500" content-expr)))))
|
||||||
|
|||||||
@@ -1,256 +1,293 @@
|
|||||||
;; ---------------------------------------------------------------------------
|
(defcomp
|
||||||
;; Spec Explorer — structured interactive view of SX spec files
|
~specs-explorer/spec-explorer-content
|
||||||
;; ---------------------------------------------------------------------------
|
(&key data)
|
||||||
|
:affinity :server
|
||||||
(defcomp ~specs-explorer/spec-explorer-content (&key data) :affinity :server
|
(~docs/page
|
||||||
(~docs/page :title (str (get data "title") " — Explorer")
|
:title (str (get data "title") " — Explorer")
|
||||||
|
|
||||||
;; Header with filename and source link
|
|
||||||
(~specs-explorer/spec-explorer-header
|
(~specs-explorer/spec-explorer-header
|
||||||
:filename (get data "filename")
|
:filename (get data "filename")
|
||||||
:title (get data "title")
|
:title (get data "title")
|
||||||
:desc (get data "desc")
|
:desc (get data "desc")
|
||||||
:slug (replace (get data "filename") ".sx" ""))
|
:slug (replace (get data "filename") ".sx" ""))
|
||||||
|
|
||||||
;; Stats bar
|
|
||||||
(~specs-explorer/spec-explorer-stats :stats (get data "stats"))
|
(~specs-explorer/spec-explorer-stats :stats (get data "stats"))
|
||||||
|
(map
|
||||||
;; Sections
|
(fn (section) (~specs-explorer/spec-explorer-section :section section))
|
||||||
(map (fn (section)
|
|
||||||
(~specs-explorer/spec-explorer-section :section section))
|
|
||||||
(get data "sections"))
|
(get data "sections"))
|
||||||
|
(when
|
||||||
|
(not (empty? (get data "platform-interface")))
|
||||||
|
(~specs-explorer/spec-platform-interface
|
||||||
|
:items (get data "platform-interface")))))
|
||||||
|
|
||||||
;; Platform interface
|
(defcomp
|
||||||
(when (not (empty? (get data "platform-interface")))
|
~specs-explorer/spec-explorer-header
|
||||||
(~specs-explorer/spec-platform-interface :items (get data "platform-interface")))))
|
(&key filename title desc slug)
|
||||||
|
(div
|
||||||
|
:class "mb-6"
|
||||||
;; ---------------------------------------------------------------------------
|
(div
|
||||||
;; Header
|
: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
|
(div
|
||||||
(h1 :class "text-2xl font-bold text-stone-800" title)
|
(h1 :class "text-2xl font-bold text-stone-800" title)
|
||||||
(p :class "text-sm text-stone-500 mt-1" desc))
|
(p :class "text-sm text-stone-500 mt-1" desc))
|
||||||
(a :href (str "/sx/(language.(spec." slug "))")
|
(a
|
||||||
:sx-get (str "/sx/(language.(spec." slug "))")
|
:href (str "/sx/(language.(spec." slug "))")
|
||||||
:sx-target "#main-panel" :sx-select "#main-panel"
|
:sx-get (str "/sx/(language.(spec." slug "))")
|
||||||
:sx-swap "outerHTML" :sx-push-url "true"
|
:sx-target "#sx-content"
|
||||||
:class "text-sm text-violet-600 hover:text-violet-800 font-medium"
|
: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"))
|
"View Source"))
|
||||||
(p :class "text-xs text-stone-400 font-mono mt-2" filename)))
|
(p :class "text-xs text-stone-400 font-mono mt-2" filename)))
|
||||||
|
|
||||||
|
(defcomp
|
||||||
;; ---------------------------------------------------------------------------
|
~specs-explorer/spec-explorer-stats
|
||||||
;; Stats bar
|
(&key stats)
|
||||||
;; ---------------------------------------------------------------------------
|
(div
|
||||||
|
:class "flex flex-wrap gap-2 mb-6 text-xs"
|
||||||
(defcomp ~specs-explorer/spec-explorer-stats (&key stats)
|
(span
|
||||||
(div :class "flex flex-wrap gap-2 mb-6 text-xs"
|
:class "bg-stone-100 text-stone-600 px-2 py-0.5 rounded font-medium"
|
||||||
(span :class "bg-stone-100 text-stone-600 px-2 py-0.5 rounded font-medium"
|
|
||||||
(str (get stats "total-defines") " defines"))
|
(str (get stats "total-defines") " defines"))
|
||||||
(when (> (get stats "pure-count") 0)
|
(when
|
||||||
(span :class "bg-green-100 text-green-700 px-2 py-0.5 rounded"
|
(> (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")))
|
(str (get stats "pure-count") " pure")))
|
||||||
(when (> (get stats "mutation-count") 0)
|
(when
|
||||||
(span :class "bg-amber-100 text-amber-700 px-2 py-0.5 rounded"
|
(> (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")))
|
(str (get stats "mutation-count") " mutation")))
|
||||||
(when (> (get stats "io-count") 0)
|
(when
|
||||||
(span :class "bg-orange-100 text-orange-700 px-2 py-0.5 rounded"
|
(> (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")))
|
(str (get stats "io-count") " io")))
|
||||||
(when (> (get stats "render-count") 0)
|
(when
|
||||||
(span :class "bg-sky-100 text-sky-700 px-2 py-0.5 rounded"
|
(> (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")))
|
(str (get stats "render-count") " render")))
|
||||||
(when (> (get stats "test-total") 0)
|
(when
|
||||||
(span :class "bg-violet-100 text-violet-700 px-2 py-0.5 rounded"
|
(> (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")))
|
(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"))))
|
(str (get stats "lines") " lines"))))
|
||||||
|
|
||||||
|
(defcomp
|
||||||
;; ---------------------------------------------------------------------------
|
~specs-explorer/spec-explorer-section
|
||||||
;; Section
|
(&key section)
|
||||||
;; ---------------------------------------------------------------------------
|
(div
|
||||||
|
:class "mb-8"
|
||||||
(defcomp ~specs-explorer/spec-explorer-section (&key section)
|
(h2
|
||||||
(div :class "mb-8"
|
:class "text-lg font-semibold text-stone-700 border-b border-stone-200 pb-1 mb-3"
|
||||||
(h2 :class "text-lg font-semibold text-stone-700 border-b border-stone-200 pb-1 mb-3"
|
|
||||||
:id (replace (lower (get section "title")) " " "-")
|
:id (replace (lower (get section "title")) " " "-")
|
||||||
(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")))
|
(p :class "text-sm text-stone-500 mb-3" (get section "comment")))
|
||||||
(div :class "space-y-4"
|
(div
|
||||||
(map (fn (d) (~specs-explorer/spec-explorer-define :d d))
|
:class "space-y-4"
|
||||||
|
(map
|
||||||
|
(fn (d) (~specs-explorer/spec-explorer-define :d d))
|
||||||
(get section "defines")))))
|
(get section "defines")))))
|
||||||
|
|
||||||
|
(defcomp
|
||||||
;; ---------------------------------------------------------------------------
|
~specs-explorer/spec-explorer-define
|
||||||
;; Define card — one function/constant with all five rings
|
(&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"))
|
:id (str "fn-" (get d "name"))
|
||||||
|
(div
|
||||||
;; Name + effect badges
|
: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 "font-mono font-semibold text-stone-800" (get d "name"))
|
||||||
(span :class "text-xs text-stone-400" (get d "kind"))
|
(span :class "text-xs text-stone-400" (get d "kind"))
|
||||||
(if (empty? (get d "effects"))
|
(if
|
||||||
(span :class "text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700" "pure")
|
(empty? (get d "effects"))
|
||||||
(map (fn (eff) (~specs-explorer/spec-effect-badge :effect eff))
|
(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"))))
|
(get d "effects"))))
|
||||||
|
(when
|
||||||
;; Params
|
(not (empty? (get d "params")))
|
||||||
(when (not (empty? (get d "params")))
|
|
||||||
(~specs-explorer/spec-param-list :params (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
|
(~specs-explorer/spec-ring-translations
|
||||||
:source (get d "source")
|
:source (get d "source")
|
||||||
:python (get d "python")
|
:python (get d "python")
|
||||||
:javascript (get d "javascript")
|
:javascript (get d "javascript")
|
||||||
:z3 (get d "z3"))
|
:z3 (get d "z3"))
|
||||||
|
(when
|
||||||
;; Ring 3: Cross-references
|
(not (empty? (get d "refs")))
|
||||||
(when (not (empty? (get d "refs")))
|
|
||||||
(~specs-explorer/spec-ring-bridge :refs (get d "refs")))
|
(~specs-explorer/spec-ring-bridge :refs (get d "refs")))
|
||||||
|
(when
|
||||||
;; Ring 4: Tests
|
(> (get d "test-count") 0)
|
||||||
(when (> (get d "test-count") 0)
|
|
||||||
(~specs-explorer/spec-ring-runtime
|
(~specs-explorer/spec-ring-runtime
|
||||||
:tests (get d "tests")
|
:tests (get d "tests")
|
||||||
:test-count (get d "test-count")))))
|
:test-count (get d "test-count")))))
|
||||||
|
|
||||||
|
(defcomp
|
||||||
;; ---------------------------------------------------------------------------
|
~specs-explorer/spec-effect-badge
|
||||||
;; Effect badge
|
(&key effect)
|
||||||
;; ---------------------------------------------------------------------------
|
(span
|
||||||
|
:class (str
|
||||||
(defcomp ~specs-explorer/spec-effect-badge (&key effect)
|
"text-xs px-1.5 py-0.5 rounded "
|
||||||
(span :class (str "text-xs px-1.5 py-0.5 rounded "
|
(case
|
||||||
(case effect
|
effect
|
||||||
"mutation" "bg-amber-100 text-amber-700"
|
"mutation"
|
||||||
"io" "bg-orange-100 text-orange-700"
|
"bg-amber-100 text-amber-700"
|
||||||
"render" "bg-sky-100 text-sky-700"
|
"io"
|
||||||
:else "bg-stone-100 text-stone-500"))
|
"bg-orange-100 text-orange-700"
|
||||||
|
"render"
|
||||||
|
"bg-sky-100 text-sky-700"
|
||||||
|
:else "bg-stone-100 text-stone-500"))
|
||||||
effect))
|
effect))
|
||||||
|
|
||||||
|
(defcomp
|
||||||
;; ---------------------------------------------------------------------------
|
~specs-explorer/spec-param-list
|
||||||
;; Param list
|
(&key params)
|
||||||
;; ---------------------------------------------------------------------------
|
(div
|
||||||
|
:class "mt-1 flex flex-wrap gap-1"
|
||||||
(defcomp ~specs-explorer/spec-param-list (&key params)
|
(map
|
||||||
(div :class "mt-1 flex flex-wrap gap-1"
|
(fn
|
||||||
(map (fn (p)
|
(p)
|
||||||
(let ((name (get p "name"))
|
(let
|
||||||
(typ (get p "type")))
|
((name (get p "name")) (typ (get p "type")))
|
||||||
(if (or (= name "&rest") (= name "&key"))
|
(if
|
||||||
(span :class "text-xs font-mono text-violet-500" name)
|
(or (= name "&rest") (= name "&key"))
|
||||||
(span :class "text-xs font-mono px-1 py-0.5 rounded bg-stone-50 border border-stone-200"
|
(span :class "text-xs font-mono text-violet-500" name)
|
||||||
(if typ
|
(span
|
||||||
(<> (span :class "text-stone-700" name)
|
: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-stone-400" " : ")
|
||||||
(span :class "text-violet-600" typ))
|
(span :class "text-violet-600" typ))
|
||||||
(span :class "text-stone-700" name))))))
|
(span :class "text-stone-700" name))))))
|
||||||
params)))
|
params)))
|
||||||
|
|
||||||
|
(defcomp
|
||||||
;; ---------------------------------------------------------------------------
|
~specs-explorer/spec-ring-translations
|
||||||
;; Ring 2: Translation panels (nucleus + bootstrapper)
|
(&key source python javascript z3)
|
||||||
;; ---------------------------------------------------------------------------
|
(when
|
||||||
|
(not (= source ""))
|
||||||
(defcomp ~specs-explorer/spec-ring-translations (&key source python javascript z3)
|
(div
|
||||||
(when (not (= source ""))
|
:class "mt-3 border border-stone-200 rounded-lg overflow-hidden"
|
||||||
(div :class "mt-3 border border-stone-200 rounded-lg overflow-hidden"
|
(details
|
||||||
;; SX source — Ring 1: the nucleus (always open)
|
:open "true"
|
||||||
(details :open "true"
|
(summary
|
||||||
(summary :class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer"
|
:class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer"
|
||||||
"SX")
|
"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"))))
|
(code (highlight source "sx"))))
|
||||||
;; Python — Ring 2: bootstrapper
|
(when
|
||||||
(when python
|
python
|
||||||
(details
|
(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")
|
"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")))))
|
(code (highlight python "python")))))
|
||||||
;; JavaScript — Ring 2: bootstrapper
|
(when
|
||||||
(when javascript
|
javascript
|
||||||
(details
|
(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")
|
"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")))))
|
(code (highlight javascript "javascript")))))
|
||||||
;; Z3 / SMT-LIB — Ring 2: formal translation
|
(when
|
||||||
(when z3
|
z3
|
||||||
(details
|
(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")
|
"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"))))))))
|
(code (highlight z3 "lisp"))))))))
|
||||||
|
|
||||||
|
(defcomp
|
||||||
;; ---------------------------------------------------------------------------
|
~specs-explorer/spec-ring-bridge
|
||||||
;; Ring 3: Cross-references (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")
|
(span :class "text-xs font-medium text-stone-500" "References")
|
||||||
(div :class "flex flex-wrap gap-1 mt-1"
|
(div
|
||||||
(map (fn (ref)
|
:class "flex flex-wrap gap-1 mt-1"
|
||||||
(a :href (str "#fn-" ref)
|
(map
|
||||||
:class "text-xs px-1.5 py-0.5 rounded bg-stone-100 text-stone-600 font-mono hover:bg-stone-200"
|
(fn
|
||||||
ref))
|
(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))))
|
refs))))
|
||||||
|
|
||||||
|
(defcomp
|
||||||
;; ---------------------------------------------------------------------------
|
~specs-explorer/spec-ring-runtime
|
||||||
;; Ring 4: Tests (runtime)
|
(&key tests test-count)
|
||||||
;; ---------------------------------------------------------------------------
|
(div
|
||||||
|
:class "mt-2"
|
||||||
(defcomp ~specs-explorer/spec-ring-runtime (&key tests test-count)
|
(div
|
||||||
(div :class "mt-2"
|
:class "flex items-center gap-1"
|
||||||
(div :class "flex items-center gap-1"
|
|
||||||
(span :class "text-xs font-medium text-stone-500" "Tests")
|
(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)))
|
(str test-count)))
|
||||||
(ul :class "mt-1 text-xs text-stone-500 list-none"
|
(ul
|
||||||
(map (fn (t)
|
:class "mt-1 text-xs text-stone-500 list-none"
|
||||||
(li :class "flex items-center gap-1"
|
(map
|
||||||
(span :class "text-green-500 text-xs" "●")
|
(fn
|
||||||
(get t "name")))
|
(t)
|
||||||
|
(li
|
||||||
|
:class "flex items-center gap-1"
|
||||||
|
(span :class "text-green-500 text-xs" "●")
|
||||||
|
(get t "name")))
|
||||||
tests))))
|
tests))))
|
||||||
|
|
||||||
|
(defcomp
|
||||||
;; ---------------------------------------------------------------------------
|
~specs-explorer/spec-platform-interface
|
||||||
;; Platform interface table (Ring 3 overview)
|
(&key items)
|
||||||
;; ---------------------------------------------------------------------------
|
(div
|
||||||
|
:class "mt-8"
|
||||||
(defcomp ~specs-explorer/spec-platform-interface (&key items)
|
(h2
|
||||||
(div :class "mt-8"
|
:class "text-lg font-semibold text-stone-700 border-b border-stone-200 pb-1 mb-3"
|
||||||
(h2 :class "text-lg font-semibold text-stone-700 border-b border-stone-200 pb-1 mb-3"
|
|
||||||
"Platform Interface")
|
"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.")
|
"Functions the host platform must provide.")
|
||||||
(div :class "overflow-x-auto rounded border border-stone-200"
|
(div
|
||||||
(table :class "w-full text-left text-sm"
|
:class "overflow-x-auto rounded border border-stone-200"
|
||||||
(thead (tr :class "border-b border-stone-200 bg-stone-50"
|
(table
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Name")
|
:class "w-full text-left text-sm"
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Params")
|
(thead
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Returns")
|
(tr
|
||||||
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
|
: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
|
(tbody
|
||||||
(map (fn (item)
|
(map
|
||||||
(tr :class "border-b border-stone-100"
|
(fn
|
||||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" (get item "name"))
|
(item)
|
||||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" (get item "params"))
|
(tr
|
||||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" (get item "returns"))
|
:class "border-b border-stone-100"
|
||||||
(td :class "px-3 py-2 text-stone-600" (get item "doc"))))
|
(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))))))
|
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
|
(a
|
||||||
:href href
|
:href href
|
||||||
:sx-get href
|
:sx-get href
|
||||||
:sx-target "#main-panel"
|
:sx-target "#sx-content"
|
||||||
:sx-select "#main-panel"
|
:sx-select "#sx-content"
|
||||||
:sx-swap "outerHTML"
|
:sx-swap "outerHTML"
|
||||||
:sx-push-url "true"
|
:sx-push-url "true"
|
||||||
:class "text-violet-700 hover:text-violet-900 underline"
|
:class "text-violet-700 hover:text-violet-900 underline"
|
||||||
@@ -116,8 +116,8 @@
|
|||||||
(a
|
(a
|
||||||
:href (nth item 1)
|
:href (nth item 1)
|
||||||
:sx-get (nth item 1)
|
:sx-get (nth item 1)
|
||||||
:sx-target "#main-panel"
|
:sx-target "#sx-content"
|
||||||
:sx-select "#main-panel"
|
:sx-select "#sx-content"
|
||||||
:sx-swap "outerHTML"
|
:sx-swap "outerHTML"
|
||||||
:sx-push-url "true"
|
:sx-push-url "true"
|
||||||
:class (str
|
: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
|
boost-el
|
||||||
(let
|
(let
|
||||||
((attr (dom-get-attr boost-el "sx-boost")))
|
((attr (dom-get-attr boost-el "sx-boost")))
|
||||||
(if (and attr (not (= attr "true"))) attr "#main-panel"))
|
(if (and attr (not (= attr "true"))) attr "#sx-content"))
|
||||||
"#main-panel")))
|
"#sx-content")))
|
||||||
(if
|
(if
|
||||||
(try-client-route (url-pathname href) target-sel)
|
(try-client-route (url-pathname href) target-sel)
|
||||||
(do (browser-push-state nil "" href) (browser-scroll-to 0 0))
|
(do (browser-push-state nil "" href) (browser-scroll-to 0 0))
|
||||||
(do
|
(do
|
||||||
(when
|
(log-info (str "sx:route server fetch " href))
|
||||||
(not (dom-has-attr? link "sx-get"))
|
(dom-set-attr link "sx-get" href)
|
||||||
(dom-set-attr link "sx-get" href))
|
(dom-set-attr link "sx-target" target-sel)
|
||||||
(when
|
(dom-set-attr link "sx-select" target-sel)
|
||||||
(not (dom-has-attr? link "sx-push-url"))
|
(dom-set-attr link "sx-push-url" "true")
|
||||||
(dom-set-attr link "sx-push-url" "true"))
|
|
||||||
(execute-request link nil nil)))))))))
|
(execute-request link nil nil)))))))))
|
||||||
|
|
||||||
(define sw-post-message (fn (msg) nil))
|
(define sw-post-message (fn (msg) nil))
|
||||||
@@ -604,7 +603,7 @@
|
|||||||
(fn
|
(fn
|
||||||
(expr)
|
(expr)
|
||||||
(let
|
(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))))
|
(when result (dom-append frag result))))
|
||||||
exprs)
|
exprs)
|
||||||
(scope-pop! "sx-render-markers")
|
(scope-pop! "sx-render-markers")
|
||||||
@@ -630,7 +629,15 @@
|
|||||||
(and text (> (len text) 0))
|
(and text (> (len text) 0))
|
||||||
(let
|
(let
|
||||||
((exprs (sx-parse text)))
|
((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))))
|
scripts))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -641,7 +648,10 @@
|
|||||||
selector
|
selector
|
||||||
(let
|
(let
|
||||||
((selected (dom-query container selector)))
|
((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))))
|
(children-to-fragment container))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
|
|||||||
@@ -282,9 +282,11 @@
|
|||||||
(fn
|
(fn
|
||||||
(t oob (s :as string))
|
(t oob (s :as string))
|
||||||
(dispose-islands-in t)
|
(dispose-islands-in t)
|
||||||
(swap-dom-nodes t oob s)
|
(swap-dom-nodes
|
||||||
(sx-hydrate t)
|
t
|
||||||
(process-elements t)))
|
(if (= s "innerHTML") (children-to-fragment oob) oob)
|
||||||
|
s)
|
||||||
|
(post-swap t)))
|
||||||
(let
|
(let
|
||||||
((select-sel (dom-get-attr el "sx-select"))
|
((select-sel (dom-get-attr el "sx-select"))
|
||||||
(content
|
(content
|
||||||
@@ -324,20 +326,31 @@
|
|||||||
(if
|
(if
|
||||||
select-sel
|
select-sel
|
||||||
(let
|
(let
|
||||||
((html (select-html-from-doc doc select-sel)))
|
((container (dom-create-element "div" nil)))
|
||||||
(with-transition
|
(dom-set-inner-html container (dom-body-inner-html doc))
|
||||||
use-transition
|
(process-oob-swaps
|
||||||
|
container
|
||||||
(fn
|
(fn
|
||||||
()
|
(t oob (s :as string))
|
||||||
(let
|
(dispose-islands-in t)
|
||||||
((swap-root (swap-html-string target html swap-style)))
|
(swap-dom-nodes t oob s)
|
||||||
(log-info
|
(post-swap t)))
|
||||||
(str
|
(hoist-head-elements container)
|
||||||
"swap-root: "
|
(let
|
||||||
(if swap-root (dom-tag-name swap-root) "nil")
|
((html (select-from-container container select-sel)))
|
||||||
" target: "
|
(with-transition
|
||||||
(dom-tag-name target)))
|
use-transition
|
||||||
(post-swap (or swap-root target))))))
|
(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
|
(let
|
||||||
((container (dom-create-element "div" nil)))
|
((container (dom-create-element "div" nil)))
|
||||||
(dom-set-inner-html container (dom-body-inner-html doc))
|
(dom-set-inner-html container (dom-body-inner-html doc))
|
||||||
|
|||||||
Reference in New Issue
Block a user