SXC content: docs/examples/home/reference pages + SX testing runner
New sxc/ content tree with 120 page files across docs, examples, home, and reference demos. sx/sx/testing/ adds page-runner.sx (317L) and index-runner.sx (394L) — SX-native test runner pages for browser-based evaluation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
394
sx/sx/testing/index-runner.sx
Normal file
394
sx/sx/testing/index-runner.sx
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
;; Test index — lists a page + its sub-components with their params and tests.
|
||||||
|
;; One "Run All Tests" button drives a shared iframe through every test of
|
||||||
|
;; the page and of every child component in sequence, reloading the iframe
|
||||||
|
;; to each target URL.
|
||||||
|
|
||||||
|
(define
|
||||||
|
_test-index-list-files
|
||||||
|
(fn
|
||||||
|
(dir)
|
||||||
|
(let
|
||||||
|
((entries (list-dir dir)))
|
||||||
|
(if
|
||||||
|
(nil? entries)
|
||||||
|
(list)
|
||||||
|
(filter
|
||||||
|
(fn
|
||||||
|
(f)
|
||||||
|
(and
|
||||||
|
(string? f)
|
||||||
|
(ends-with? f ".sx")
|
||||||
|
(not (= f "index.sx"))
|
||||||
|
(not (starts-with? f "_"))
|
||||||
|
(not (starts-with? f "."))))
|
||||||
|
entries)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
_test-index-list-dirs
|
||||||
|
(fn
|
||||||
|
(dir)
|
||||||
|
(let
|
||||||
|
((entries (list-dir dir)))
|
||||||
|
(if
|
||||||
|
(nil? entries)
|
||||||
|
(list)
|
||||||
|
(filter
|
||||||
|
(fn
|
||||||
|
(e)
|
||||||
|
(and
|
||||||
|
(string? e)
|
||||||
|
(not (ends-with? e ".sx"))
|
||||||
|
(not (starts-with? e "_"))
|
||||||
|
(not (starts-with? e "."))
|
||||||
|
(not (nil? (read-file (str dir "/" e "/index.sx"))))))
|
||||||
|
entries)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
_test-index-lookup
|
||||||
|
(fn
|
||||||
|
(full-name)
|
||||||
|
(cek-try (fn () (eval-expr (make-symbol full-name))) (fn (err) nil))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
_test-index-entry
|
||||||
|
(fn
|
||||||
|
(prefix base)
|
||||||
|
(let
|
||||||
|
((comp-name (str prefix "/" base)))
|
||||||
|
(let
|
||||||
|
((full-name (str "~" comp-name))
|
||||||
|
(url (component-name->url comp-name))
|
||||||
|
(tests (load-tests-for-name comp-name)))
|
||||||
|
(let
|
||||||
|
((comp (_test-index-lookup full-name)))
|
||||||
|
(let
|
||||||
|
((is-comp (and comp (or (component? comp) (island? comp)))))
|
||||||
|
{:short base :url url :kind (cond (nil? comp) "missing" (island? comp) "island" :else "component") :full-name full-name :params (if is-comp (component-params comp) (list)) :tests tests :name comp-name}))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
load-index-entries
|
||||||
|
(fn
|
||||||
|
(dir-rel)
|
||||||
|
(let
|
||||||
|
((abs-dir (str _sx-components-dir "/" dir-rel))
|
||||||
|
(page-source
|
||||||
|
(or
|
||||||
|
(read-file (str _sx-components-dir "/" dir-rel "/index.sx"))
|
||||||
|
"")))
|
||||||
|
(let
|
||||||
|
((file-bases (map (fn (f) (slice f 0 (- (len f) 3))) (_test-index-list-files abs-dir)))
|
||||||
|
(dir-bases (_test-index-list-dirs abs-dir))
|
||||||
|
(island-bases
|
||||||
|
(map
|
||||||
|
(fn (f) (slice f 0 (- (len f) 3)))
|
||||||
|
(_test-index-list-files (str abs-dir "/_islands")))))
|
||||||
|
(let
|
||||||
|
((all-bases (concat file-bases (concat dir-bases island-bases))))
|
||||||
|
(let
|
||||||
|
((used-bases (filter (fn (b) (string-contains? page-source (str "~" dir-rel "/" b))) all-bases)))
|
||||||
|
(map (fn (b) (_test-index-entry dir-rel b)) used-bases)))))))
|
||||||
|
|
||||||
|
;; Control island — one button runs every test across every component section.
|
||||||
|
(defisland
|
||||||
|
~testing/test-index-controls
|
||||||
|
()
|
||||||
|
(let
|
||||||
|
((current (signal "Ready")))
|
||||||
|
(letrec
|
||||||
|
((get-doc (fn () (host-get (dom-query "#test-iframe") "contentDocument")))
|
||||||
|
(wait-boot
|
||||||
|
(fn
|
||||||
|
(tries)
|
||||||
|
(let
|
||||||
|
((doc (get-doc)))
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
doc
|
||||||
|
(=
|
||||||
|
(dom-get-attr
|
||||||
|
(host-get doc "documentElement")
|
||||||
|
"data-sx-ready")
|
||||||
|
"true"))
|
||||||
|
true
|
||||||
|
(if
|
||||||
|
(<= tries 0)
|
||||||
|
false
|
||||||
|
(do (hs-wait 200) (wait-boot (- tries 1))))))))
|
||||||
|
(wait-for-el
|
||||||
|
(fn
|
||||||
|
(sel tries)
|
||||||
|
(let
|
||||||
|
((doc (get-doc))
|
||||||
|
(el (when doc (host-call doc "querySelector" sel))))
|
||||||
|
(if
|
||||||
|
el
|
||||||
|
el
|
||||||
|
(if
|
||||||
|
(<= tries 0)
|
||||||
|
nil
|
||||||
|
(do (hs-wait 200) (wait-for-el sel (- tries 1))))))))
|
||||||
|
(load-url
|
||||||
|
(fn
|
||||||
|
(url)
|
||||||
|
(let
|
||||||
|
((iframe (dom-query "#test-iframe")))
|
||||||
|
(when
|
||||||
|
iframe
|
||||||
|
(dom-set-prop iframe "src" url)
|
||||||
|
(hs-wait 800)
|
||||||
|
(wait-boot 30)
|
||||||
|
(hs-wait 800)))))
|
||||||
|
(run-action
|
||||||
|
(fn
|
||||||
|
(action)
|
||||||
|
(let
|
||||||
|
((doc (get-doc)) (ty (first action)))
|
||||||
|
(cond
|
||||||
|
(= ty :click)
|
||||||
|
(let
|
||||||
|
((el (wait-for-el (nth action 1) 25)))
|
||||||
|
(if
|
||||||
|
(nil? el)
|
||||||
|
(str "Not found: " (nth action 1))
|
||||||
|
(do (host-call el "click") nil)))
|
||||||
|
(= ty :fill)
|
||||||
|
(let
|
||||||
|
((el (wait-for-el (nth action 1) 25)))
|
||||||
|
(if
|
||||||
|
(nil? el)
|
||||||
|
(str "Not found: " (nth action 1))
|
||||||
|
(do
|
||||||
|
(host-call el "focus")
|
||||||
|
(dom-set-prop el "value" (nth action 2))
|
||||||
|
(dom-dispatch el "input" nil)
|
||||||
|
(dom-dispatch el "change" nil)
|
||||||
|
nil)))
|
||||||
|
(= ty :wait)
|
||||||
|
(do (hs-wait (nth action 1)) nil)
|
||||||
|
(= ty :assert-text)
|
||||||
|
(let
|
||||||
|
((el (wait-for-el (nth action 1) 25)))
|
||||||
|
(if
|
||||||
|
(nil? el)
|
||||||
|
(str "Not found: " (nth action 1))
|
||||||
|
(let
|
||||||
|
((txt (host-get el "textContent"))
|
||||||
|
(kw (nth action 2))
|
||||||
|
(expected (nth action 3)))
|
||||||
|
(cond
|
||||||
|
(and
|
||||||
|
(= kw :contains)
|
||||||
|
(not (contains? txt expected)))
|
||||||
|
(str
|
||||||
|
"Expected '"
|
||||||
|
expected
|
||||||
|
"' in '"
|
||||||
|
(slice txt 0 60)
|
||||||
|
"'")
|
||||||
|
(and (= kw :not-contains) (contains? txt expected))
|
||||||
|
(str "Unexpected '" expected "'")
|
||||||
|
true
|
||||||
|
nil))))
|
||||||
|
(= ty :assert-count)
|
||||||
|
(let
|
||||||
|
((els (host-call doc "querySelectorAll" (nth action 1))))
|
||||||
|
(let
|
||||||
|
((count (host-get els "length"))
|
||||||
|
(kw (nth action 2))
|
||||||
|
(expected (nth action 3)))
|
||||||
|
(if
|
||||||
|
(and (= kw :gte) (< count expected))
|
||||||
|
(str "Expected >=" expected " got " count)
|
||||||
|
nil)))
|
||||||
|
true
|
||||||
|
nil))))
|
||||||
|
(set-status
|
||||||
|
(fn
|
||||||
|
(detail-el glyph)
|
||||||
|
(let
|
||||||
|
((span (host-call detail-el "querySelector" "[data-status-icon]")))
|
||||||
|
(when span (dom-set-prop span "textContent" glyph)))))
|
||||||
|
(run-test
|
||||||
|
(fn
|
||||||
|
(detail-el)
|
||||||
|
(let
|
||||||
|
((name (dom-get-attr detail-el "data-test-name"))
|
||||||
|
(actions-src (dom-get-attr detail-el "data-actions")))
|
||||||
|
(set-status detail-el "⟳")
|
||||||
|
(let
|
||||||
|
((actions (parse actions-src)) (fail-msg nil))
|
||||||
|
(when
|
||||||
|
(list? actions)
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(action)
|
||||||
|
(when
|
||||||
|
(nil? fail-msg)
|
||||||
|
(let
|
||||||
|
((err (run-action action)))
|
||||||
|
(when (string? err) (set! fail-msg err)))))
|
||||||
|
actions))
|
||||||
|
(if
|
||||||
|
(nil? fail-msg)
|
||||||
|
(set-status detail-el "✓")
|
||||||
|
(do
|
||||||
|
(set-status detail-el "✗")
|
||||||
|
(console-log
|
||||||
|
(str "[test-index] FAIL " name ": " fail-msg))))))))
|
||||||
|
(run-section
|
||||||
|
(fn
|
||||||
|
(section-el)
|
||||||
|
(let
|
||||||
|
((url (dom-get-attr section-el "data-component-url"))
|
||||||
|
(tests (dom-query-all section-el "[data-test-name]")))
|
||||||
|
(when
|
||||||
|
(and (string? url) (not (empty? tests)))
|
||||||
|
(reset! current (str "Loading: " url))
|
||||||
|
(load-url url)
|
||||||
|
(for-each run-test tests)))))
|
||||||
|
(run-all
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(reset! current "Running…")
|
||||||
|
(for-each
|
||||||
|
run-section
|
||||||
|
(dom-query-all (dom-body) "[data-component-url]"))
|
||||||
|
(reset! current "Done"))))
|
||||||
|
(div
|
||||||
|
(~tw
|
||||||
|
:tokens "flex items-center gap-4 mb-4 sticky top-0 bg-white py-2 z-10 border-b border-stone-200")
|
||||||
|
(button
|
||||||
|
:id "run-all-index-tests"
|
||||||
|
(~tw
|
||||||
|
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||||
|
:on-click (fn (e) (run-all))
|
||||||
|
"Run All Tests")
|
||||||
|
(span (~tw :tokens "text-sm text-stone-500") (deref current))))))
|
||||||
|
|
||||||
|
;; Render a single test <details> element. Used by both the page-level tests
|
||||||
|
;; and the per-component tests.
|
||||||
|
(defcomp
|
||||||
|
~testing/test-index-test-row
|
||||||
|
(&key test)
|
||||||
|
(details
|
||||||
|
:data-test-name (get test :name)
|
||||||
|
:data-actions (pretty-print (get test :actions))
|
||||||
|
(summary
|
||||||
|
(~tw
|
||||||
|
:tokens "px-4 py-2 cursor-pointer hover:bg-stone-50 flex items-center gap-3")
|
||||||
|
(span
|
||||||
|
:data-status-icon "1"
|
||||||
|
(~tw :tokens "font-bold text-stone-400")
|
||||||
|
"○")
|
||||||
|
(span (~tw :tokens "font-medium text-sm") (get test :name))
|
||||||
|
(span (~tw :tokens "text-xs text-stone-400 ml-auto") (get test :desc)))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "px-4 py-2 bg-stone-50 border-t border-stone-100")
|
||||||
|
(pre
|
||||||
|
(~tw :tokens "text-xs font-mono whitespace-pre-wrap")
|
||||||
|
(get test :source)))))
|
||||||
|
|
||||||
|
(defcomp
|
||||||
|
~testing/test-index
|
||||||
|
(&key dir-name target-url own-tests entries)
|
||||||
|
(let
|
||||||
|
((initial-url (cond (string? target-url) target-url (and entries (not (empty? entries))) (get (first entries) :url) :else "about:blank")))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(h2 (~tw :tokens "text-xl font-semibold") "Test index: " dir-name)
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-stone-500 text-sm")
|
||||||
|
"Tests for page "
|
||||||
|
(a
|
||||||
|
:href target-url
|
||||||
|
(~tw :tokens "text-violet-600 underline font-mono")
|
||||||
|
target-url)
|
||||||
|
" and every child component.")
|
||||||
|
(~testing/test-index-controls)
|
||||||
|
(section
|
||||||
|
:data-component-url target-url
|
||||||
|
(~tw :tokens "border border-stone-200 rounded-lg overflow-hidden")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "px-4 py-3 bg-stone-50 border-b border-stone-200")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex items-center gap-3")
|
||||||
|
(span
|
||||||
|
(~tw
|
||||||
|
:tokens "text-xs font-mono text-stone-400 uppercase tracking-wide")
|
||||||
|
"page")
|
||||||
|
(a
|
||||||
|
:href target-url
|
||||||
|
:target "_blank"
|
||||||
|
(~tw
|
||||||
|
:tokens "font-mono font-semibold text-violet-700 hover:underline")
|
||||||
|
dir-name)))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "divide-y divide-stone-100")
|
||||||
|
(if
|
||||||
|
(or (nil? own-tests) (empty? own-tests))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "px-4 py-2 text-xs text-stone-400 italic")
|
||||||
|
"No page-level tests in "
|
||||||
|
(code (~tw :tokens "font-mono") (str dir-name "/_test/"))
|
||||||
|
".")
|
||||||
|
(map
|
||||||
|
(fn (test) (~testing/test-index-test-row :test test))
|
||||||
|
own-tests))))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(if
|
||||||
|
(or (nil? entries) (empty? entries))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "text-sm text-stone-400 italic")
|
||||||
|
"No child components under "
|
||||||
|
(code (~tw :tokens "font-mono") dir-name)
|
||||||
|
".")
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(entry)
|
||||||
|
(let
|
||||||
|
((short (get entry :short))
|
||||||
|
(url (get entry :url))
|
||||||
|
(params (get entry :params))
|
||||||
|
(kind (get entry :kind))
|
||||||
|
(tests (get entry :tests)))
|
||||||
|
(section
|
||||||
|
:data-component-url url
|
||||||
|
(~tw
|
||||||
|
:tokens "border border-stone-200 rounded-lg overflow-hidden")
|
||||||
|
(div
|
||||||
|
(~tw
|
||||||
|
:tokens "px-4 py-3 bg-stone-50 border-b border-stone-200")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex items-center gap-3")
|
||||||
|
(span
|
||||||
|
(~tw
|
||||||
|
:tokens "text-xs font-mono text-stone-400 uppercase tracking-wide")
|
||||||
|
kind)
|
||||||
|
(a
|
||||||
|
:href url
|
||||||
|
:target "_blank"
|
||||||
|
(~tw
|
||||||
|
:tokens "font-mono font-semibold text-violet-700 hover:underline")
|
||||||
|
short))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "font-mono text-xs text-stone-500 mt-1")
|
||||||
|
"("
|
||||||
|
(if (empty? params) "no params" (join " " params))
|
||||||
|
")"))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "divide-y divide-stone-100")
|
||||||
|
(if
|
||||||
|
(or (nil? tests) (empty? tests))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "px-4 py-2 text-xs text-stone-400 italic")
|
||||||
|
"No tests in _test/.")
|
||||||
|
(map
|
||||||
|
(fn (test) (~testing/test-index-test-row :test test))
|
||||||
|
tests))))))
|
||||||
|
entries)))
|
||||||
|
(iframe
|
||||||
|
:id "test-iframe"
|
||||||
|
:src initial-url
|
||||||
|
(~tw :tokens "w-full border border-stone-200 rounded")
|
||||||
|
:style "height:500px"))))
|
||||||
317
sx/sx/testing/page-runner.sx
Normal file
317
sx/sx/testing/page-runner.sx
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
;; Generic page test runner.
|
||||||
|
;; Tests loaded from <comp>/_test/*.sx, rendered with status icons.
|
||||||
|
;; Run All island drives the iframe via DOM — actions stored on each
|
||||||
|
;; <details> as data-actions (SX source).
|
||||||
|
|
||||||
|
(define
|
||||||
|
segments->url
|
||||||
|
(fn
|
||||||
|
(segs)
|
||||||
|
(if
|
||||||
|
(= (len segs) 1)
|
||||||
|
(str "(" (first segs) ")")
|
||||||
|
(str "(" (first segs) "." (segments->url (rest segs)) ")"))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
component-name->url
|
||||||
|
(fn (name) (str "/sx/" (segments->url (split name "/")))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
action-form->list
|
||||||
|
(fn
|
||||||
|
(form)
|
||||||
|
(if
|
||||||
|
(and (list? form) (not (empty? form)) (symbol? (first form)))
|
||||||
|
(cons (make-keyword (symbol-name (first form))) (rest form))
|
||||||
|
form)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
split-deftest-body
|
||||||
|
(fn
|
||||||
|
(args)
|
||||||
|
(letrec
|
||||||
|
((go (fn (xs keyargs actions) (cond (empty? xs) {:keyargs keyargs :actions (reverse actions)} (and (>= (len xs) 2) (keyword? (first xs))) (go (rest (rest xs)) (assoc keyargs (keyword-name (first xs)) (nth xs 1)) actions) :else (go (rest xs) keyargs (cons (action-form->list (first xs)) actions))))))
|
||||||
|
(go args {} (list)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
deftest->spec
|
||||||
|
(fn
|
||||||
|
(form)
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? form)
|
||||||
|
(>= (len form) 2)
|
||||||
|
(symbol? (first form))
|
||||||
|
(= (symbol-name (first form)) "deftest"))
|
||||||
|
(let
|
||||||
|
((name-sym (nth form 1)))
|
||||||
|
(let
|
||||||
|
((rest-form (slice form 2 (len form))))
|
||||||
|
(let
|
||||||
|
((name (if (symbol? name-sym) (symbol-name name-sym) (str name-sym))))
|
||||||
|
(let
|
||||||
|
((split (split-deftest-body rest-form)))
|
||||||
|
(let
|
||||||
|
((kw (get split :keyargs)) (actions (get split :actions)))
|
||||||
|
{:desc (or (get kw "desc") name) :url (get kw "url") :actions actions :source (pretty-print form) :name name})))))
|
||||||
|
nil)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
extract-deftests
|
||||||
|
(fn
|
||||||
|
(parsed)
|
||||||
|
(let
|
||||||
|
((forms (if (and (list? parsed) (not (empty? parsed)) (list? (first parsed))) parsed (list parsed))))
|
||||||
|
(filter (fn (x) (not (nil? x))) (map deftest->spec forms)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
load-tests-for-name
|
||||||
|
(fn
|
||||||
|
(name)
|
||||||
|
(let
|
||||||
|
((test-dir (str _sx-components-dir "/" name "/_test")))
|
||||||
|
(let
|
||||||
|
((files (list-dir test-dir)))
|
||||||
|
(if
|
||||||
|
(nil? files)
|
||||||
|
(list)
|
||||||
|
(reduce
|
||||||
|
(fn
|
||||||
|
(acc f)
|
||||||
|
(if
|
||||||
|
(and (string? f) (ends-with? f ".sx"))
|
||||||
|
(let
|
||||||
|
((src (read-file (str test-dir "/" f))))
|
||||||
|
(if src (concat acc (extract-deftests (parse src))) acc))
|
||||||
|
acc))
|
||||||
|
(list)
|
||||||
|
files))))))
|
||||||
|
|
||||||
|
;; Control island — Run All + status, reads tests from DOM.
|
||||||
|
(defisland
|
||||||
|
~testing/page-runner-controls
|
||||||
|
(&key target-url)
|
||||||
|
(let
|
||||||
|
((current (signal "Ready")))
|
||||||
|
(letrec
|
||||||
|
((get-doc (fn () (host-get (dom-query "#test-iframe") "contentDocument")))
|
||||||
|
(wait-boot
|
||||||
|
(fn
|
||||||
|
(tries)
|
||||||
|
(let
|
||||||
|
((doc (get-doc)))
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
doc
|
||||||
|
(=
|
||||||
|
(dom-get-attr
|
||||||
|
(host-get doc "documentElement")
|
||||||
|
"data-sx-ready")
|
||||||
|
"true"))
|
||||||
|
true
|
||||||
|
(if
|
||||||
|
(<= tries 0)
|
||||||
|
false
|
||||||
|
(do (hs-wait 200) (wait-boot (- tries 1))))))))
|
||||||
|
(reload-frame
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(let
|
||||||
|
((w (host-get (dom-query "#test-iframe") "contentWindow")))
|
||||||
|
(host-call (host-get w "location") "reload")
|
||||||
|
(hs-wait 800)
|
||||||
|
(wait-boot 30)
|
||||||
|
(hs-wait 1200))))
|
||||||
|
(wait-for-el
|
||||||
|
(fn
|
||||||
|
(sel tries)
|
||||||
|
(let
|
||||||
|
((doc (get-doc))
|
||||||
|
(el (when doc (host-call doc "querySelector" sel))))
|
||||||
|
(if
|
||||||
|
el
|
||||||
|
el
|
||||||
|
(if
|
||||||
|
(<= tries 0)
|
||||||
|
nil
|
||||||
|
(do (hs-wait 200) (wait-for-el sel (- tries 1))))))))
|
||||||
|
(run-action
|
||||||
|
(fn
|
||||||
|
(action)
|
||||||
|
(let
|
||||||
|
((doc (get-doc)) (ty (first action)))
|
||||||
|
(cond
|
||||||
|
(= ty :click)
|
||||||
|
(let
|
||||||
|
((el (host-call doc "querySelector" (nth action 1))))
|
||||||
|
(if
|
||||||
|
(nil? el)
|
||||||
|
(str "Not found: " (nth action 1))
|
||||||
|
(do (host-call el "click") nil)))
|
||||||
|
(= ty :fill)
|
||||||
|
(let
|
||||||
|
((el (host-call doc "querySelector" (nth action 1))))
|
||||||
|
(if
|
||||||
|
(nil? el)
|
||||||
|
(str "Not found: " (nth action 1))
|
||||||
|
(do
|
||||||
|
(host-call el "focus")
|
||||||
|
(dom-set-prop el "value" (nth action 2))
|
||||||
|
(dom-dispatch el "input" nil)
|
||||||
|
(dom-dispatch el "change" nil)
|
||||||
|
nil)))
|
||||||
|
(= ty :wait)
|
||||||
|
(do (hs-wait (nth action 1)) nil)
|
||||||
|
(= ty :assert-text)
|
||||||
|
(let
|
||||||
|
((el (host-call doc "querySelector" (nth action 1))))
|
||||||
|
(if
|
||||||
|
(nil? el)
|
||||||
|
(str "Not found: " (nth action 1))
|
||||||
|
(let
|
||||||
|
((txt (dom-text-content el))
|
||||||
|
(kw (nth action 2))
|
||||||
|
(expected (nth action 3)))
|
||||||
|
(cond
|
||||||
|
(and
|
||||||
|
(= kw :contains)
|
||||||
|
(not (contains? txt expected)))
|
||||||
|
(str
|
||||||
|
"Expected '"
|
||||||
|
expected
|
||||||
|
"' in '"
|
||||||
|
(slice txt 0 60)
|
||||||
|
"'")
|
||||||
|
(and (= kw :not-contains) (contains? txt expected))
|
||||||
|
(str "Unexpected '" expected "'")
|
||||||
|
true
|
||||||
|
nil))))
|
||||||
|
(= ty :assert-count)
|
||||||
|
(let
|
||||||
|
((els (host-call doc "querySelectorAll" (nth action 1))))
|
||||||
|
(let
|
||||||
|
((count (host-get els "length"))
|
||||||
|
(kw (nth action 2))
|
||||||
|
(expected (nth action 3)))
|
||||||
|
(if
|
||||||
|
(and (= kw :gte) (< count expected))
|
||||||
|
(str "Expected >=" expected " got " count)
|
||||||
|
nil)))
|
||||||
|
true
|
||||||
|
nil))))
|
||||||
|
(set-status
|
||||||
|
(fn
|
||||||
|
(detail-el glyph)
|
||||||
|
(let
|
||||||
|
((span (host-call detail-el "querySelector" "[data-status-icon]")))
|
||||||
|
(when span (dom-set-prop span "textContent" glyph)))))
|
||||||
|
(run-one
|
||||||
|
(fn
|
||||||
|
(detail-el)
|
||||||
|
(let
|
||||||
|
((name (dom-get-attr detail-el "data-test-name"))
|
||||||
|
(actions-src (dom-get-attr detail-el "data-actions")))
|
||||||
|
(set-status detail-el "⟳")
|
||||||
|
(reset! current (str "Running: " name))
|
||||||
|
(reload-frame)
|
||||||
|
(let
|
||||||
|
((actions (if actions-src (parse actions-src) (list)))
|
||||||
|
(fail-msg nil))
|
||||||
|
(when
|
||||||
|
(and (list? actions) (not (empty? actions)))
|
||||||
|
(let
|
||||||
|
((first-sel (nth (first actions) 1)))
|
||||||
|
(when
|
||||||
|
(string? first-sel)
|
||||||
|
(let
|
||||||
|
((found (wait-for-el first-sel 25)))
|
||||||
|
(when
|
||||||
|
(nil? found)
|
||||||
|
(set! fail-msg (str "Timeout: " first-sel)))))))
|
||||||
|
(when
|
||||||
|
(and (nil? fail-msg) (list? actions))
|
||||||
|
(for-each
|
||||||
|
(fn
|
||||||
|
(action)
|
||||||
|
(when
|
||||||
|
(nil? fail-msg)
|
||||||
|
(let
|
||||||
|
((err (run-action action)))
|
||||||
|
(when (string? err) (set! fail-msg err)))))
|
||||||
|
actions))
|
||||||
|
(if
|
||||||
|
(nil? fail-msg)
|
||||||
|
(set-status detail-el "✓")
|
||||||
|
(do
|
||||||
|
(set-status detail-el "✗")
|
||||||
|
(console-log (str "[test] FAIL " name ": " fail-msg))))))))
|
||||||
|
(run-all
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(reset! current "Running...")
|
||||||
|
(for-each
|
||||||
|
(fn (el) (run-one el))
|
||||||
|
(dom-query-all (dom-body) "[data-test-name]"))
|
||||||
|
(reset! current "Done"))))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex items-center gap-4 mb-2")
|
||||||
|
(button
|
||||||
|
:id "run-all-tests"
|
||||||
|
(~tw
|
||||||
|
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||||
|
:on-click (fn (e) (run-all))
|
||||||
|
"Run All Tests")
|
||||||
|
(span (~tw :tokens "text-sm text-stone-500") (deref current))))))
|
||||||
|
|
||||||
|
(defcomp
|
||||||
|
~testing/page-runner
|
||||||
|
(&key tests target-url title)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-2")
|
||||||
|
(h2 (~tw :tokens "text-xl font-semibold") (or title "Test runner"))
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-stone-500 text-sm")
|
||||||
|
"Running tests against "
|
||||||
|
(a
|
||||||
|
:href target-url
|
||||||
|
(~tw :tokens "text-violet-600 underline")
|
||||||
|
target-url))
|
||||||
|
(~testing/page-runner-controls :target-url target-url)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-2")
|
||||||
|
(if
|
||||||
|
(or (nil? tests) (empty? tests))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "text-sm text-stone-400 italic")
|
||||||
|
"No deftest specs found in _test/ for this page.")
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(test)
|
||||||
|
(details
|
||||||
|
:data-test-name (get test :name)
|
||||||
|
:data-actions (pretty-print (get test :actions))
|
||||||
|
(~tw
|
||||||
|
:tokens "border border-stone-200 rounded-lg overflow-hidden")
|
||||||
|
(summary
|
||||||
|
(~tw
|
||||||
|
:tokens "px-4 py-2 cursor-pointer hover:bg-stone-50 flex items-center gap-2")
|
||||||
|
(span
|
||||||
|
:data-status-icon "1"
|
||||||
|
(~tw :tokens "font-bold text-stone-400")
|
||||||
|
"○")
|
||||||
|
(span (~tw :tokens "font-medium") (get test :name))
|
||||||
|
(span
|
||||||
|
(~tw :tokens "text-sm text-stone-400 ml-auto")
|
||||||
|
(get test :desc)))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "px-4 py-3 bg-stone-50 border-t border-stone-200")
|
||||||
|
(pre
|
||||||
|
(~tw
|
||||||
|
:tokens "text-xs font-mono whitespace-pre-wrap text-stone-600")
|
||||||
|
(get test :source)))))
|
||||||
|
tests)))
|
||||||
|
(iframe
|
||||||
|
:id "test-iframe"
|
||||||
|
:src target-url
|
||||||
|
(~tw :tokens "w-full border border-stone-200 rounded-lg mt-4")
|
||||||
|
:style "height:600px")))
|
||||||
25
sx/sxc/docs/attr-row.sx
Normal file
25
sx/sxc/docs/attr-row.sx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key attr description exists href)
|
||||||
|
(tr
|
||||||
|
(~tw :tokens "border-b border-stone-100")
|
||||||
|
(td
|
||||||
|
(~tw :tokens "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"
|
||||||
|
(~tw :tokens "text-violet-700 hover:text-violet-900 underline")
|
||||||
|
attr)
|
||||||
|
(span (~tw :tokens "text-violet-700") attr)))
|
||||||
|
(td (~tw :tokens "px-3 py-2 text-stone-700 text-sm") description)
|
||||||
|
(td
|
||||||
|
(~tw :tokens "px-3 py-2 text-center")
|
||||||
|
(if
|
||||||
|
exists
|
||||||
|
(span (~tw :tokens "text-emerald-600 text-sm") "yes")
|
||||||
|
(span (~tw :tokens "text-stone-400 text-sm italic") "not yet")))))
|
||||||
7
sx/sxc/docs/code.sx
Normal file
7
sx/sxc/docs/code.sx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key src)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "not-prose bg-stone-100 rounded-lg p-5 overflow-x-auto my-6")
|
||||||
|
(pre
|
||||||
|
(~tw :tokens "text-sm leading-relaxed whitespace-pre-wrap break-words font-mono")
|
||||||
|
src)))
|
||||||
22
sx/sxc/docs/nav.sx
Normal file
22
sx/sxc/docs/nav.sx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key items current)
|
||||||
|
(nav
|
||||||
|
(~tw :tokens "flex flex-wrap gap-2 mb-8")
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(item)
|
||||||
|
(a
|
||||||
|
:href (nth item 1)
|
||||||
|
:sx-get (nth item 1)
|
||||||
|
:sx-target "#sx-content"
|
||||||
|
:sx-select "#sx-content"
|
||||||
|
:sx-swap "outerHTML"
|
||||||
|
:sx-push-url "true"
|
||||||
|
:class (str
|
||||||
|
"px-3 py-1.5 rounded text-sm font-medium no-underline "
|
||||||
|
(if
|
||||||
|
(= (nth item 0) current)
|
||||||
|
"bg-violet-100 text-violet-800"
|
||||||
|
"bg-stone-100 text-stone-600 hover:bg-stone-200"))
|
||||||
|
(nth item 0)))
|
||||||
|
items)))
|
||||||
5
sx/sxc/docs/note.sx
Normal file
5
sx/sxc/docs/note.sx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key &rest children)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "border-l-4 border-violet-400 bg-violet-50 p-4 text-stone-700 text-sm")
|
||||||
|
children))
|
||||||
5
sx/sxc/docs/page.sx
Normal file
5
sx/sxc/docs/page.sx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key title &rest children)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "max-w-4xl mx-auto px-6 pb-8 pt-4")
|
||||||
|
(div (~tw :tokens "prose prose-stone max-w-none space-y-6") children)))
|
||||||
14
sx/sxc/docs/primitives-table.sx
Normal file
14
sx/sxc/docs/primitives-table.sx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key category primitives)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-2")
|
||||||
|
(h4 (~tw :tokens "text-lg font-semibold text-stone-700") category)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex flex-wrap gap-2")
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(p)
|
||||||
|
(span
|
||||||
|
(~tw :tokens "inline-block px-2 py-1 rounded bg-stone-100 font-mono text-sm text-stone-700")
|
||||||
|
p))
|
||||||
|
primitives))))
|
||||||
7
sx/sxc/docs/section.sx
Normal file
7
sx/sxc/docs/section.sx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key title id &rest children)
|
||||||
|
(section
|
||||||
|
:id id
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(h2 (~tw :tokens "text-2xl font-semibold text-stone-800") title)
|
||||||
|
children))
|
||||||
6
sx/sxc/docs/subsection.sx
Normal file
6
sx/sxc/docs/subsection.sx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key title &rest children)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(h3 (~tw :tokens "text-xl font-semibold text-stone-700") title)
|
||||||
|
children))
|
||||||
22
sx/sxc/docs/table.sx
Normal file
22
sx/sxc/docs/table.sx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key headers rows)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "overflow-x-auto rounded border border-stone-200")
|
||||||
|
(table
|
||||||
|
(~tw :tokens "w-full text-left text-sm")
|
||||||
|
(thead
|
||||||
|
(tr
|
||||||
|
(~tw :tokens "border-b border-stone-200 bg-stone-100")
|
||||||
|
(map
|
||||||
|
(fn (h) (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") h))
|
||||||
|
headers)))
|
||||||
|
(tbody
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(row)
|
||||||
|
(tr
|
||||||
|
(~tw :tokens "border-b border-stone-100")
|
||||||
|
(map
|
||||||
|
(fn (cell) (td (~tw :tokens "px-3 py-2 text-stone-700") cell))
|
||||||
|
row)))
|
||||||
|
rows)))))
|
||||||
17
sx/sxc/examples/active-search-demo.sx
Normal file
17
sx/sxc/examples/active-search-demo.sx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:name "q"
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.search))))"
|
||||||
|
:sx-trigger "keyup delay:300ms changed"
|
||||||
|
:sx-target "#search-results"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:placeholder "Search programming languages..."
|
||||||
|
(~tw :tokens "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"))
|
||||||
|
(div
|
||||||
|
:id "search-results"
|
||||||
|
(~tw :tokens "border border-stone-200 rounded divide-y divide-stone-100")
|
||||||
|
(p (~tw :tokens "p-3 text-sm text-stone-400") "Type to search..."))))
|
||||||
8
sx/sxc/examples/anim-result.sx
Normal file
8
sx/sxc/examples/anim-result.sx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key color time)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "sx-fade-in space-y-2")
|
||||||
|
(div
|
||||||
|
:class (str "p-4 rounded transition-colors duration-700 " color)
|
||||||
|
(p (~tw :tokens "font-medium") "Faded in!")
|
||||||
|
(p (~tw :tokens "text-sm mt-1") (str "Loaded at " time)))))
|
||||||
14
sx/sxc/examples/animations-demo.sx
Normal file
14
sx/sxc/examples/animations-demo.sx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.animate))))"
|
||||||
|
:sx-target "#anim-target"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Load with animation")
|
||||||
|
(div
|
||||||
|
:id "anim-target"
|
||||||
|
(~tw :tokens "p-4 rounded border border-stone-200 bg-stone-100 text-center")
|
||||||
|
(p (~tw :tokens "text-stone-400") "Content will fade in here."))))
|
||||||
17
sx/sxc/examples/bulk-row.sx
Normal file
17
sx/sxc/examples/bulk-row.sx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key id name email status)
|
||||||
|
(tr
|
||||||
|
(~tw :tokens "border-b border-stone-100")
|
||||||
|
(td (~tw :tokens "px-3 py-2") (input :type "checkbox" :name "ids" :value id))
|
||||||
|
(td (~tw :tokens "px-3 py-2 text-stone-700") name)
|
||||||
|
(td (~tw :tokens "px-3 py-2 text-stone-700") email)
|
||||||
|
(td
|
||||||
|
(~tw :tokens "px-3 py-2")
|
||||||
|
(span
|
||||||
|
:class (str
|
||||||
|
"px-2 py-0.5 rounded text-xs font-medium "
|
||||||
|
(if
|
||||||
|
(= status "active")
|
||||||
|
"bg-emerald-100 text-emerald-700"
|
||||||
|
"bg-stone-100 text-stone-500"))
|
||||||
|
status))))
|
||||||
44
sx/sxc/examples/bulk-update-demo.sx
Normal file
44
sx/sxc/examples/bulk-update-demo.sx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key users)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(form
|
||||||
|
:id "bulk-form"
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex gap-2 mb-3")
|
||||||
|
(button
|
||||||
|
:type "button"
|
||||||
|
:sx-post "/sx/(geography.(hypermedia.(example.(api.bulk))))?action=activate"
|
||||||
|
:sx-target "#bulk-table"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-include "#bulk-form"
|
||||||
|
(~tw :tokens "px-3 py-1.5 bg-emerald-600 text-white rounded text-sm hover:bg-emerald-700")
|
||||||
|
"Activate")
|
||||||
|
(button
|
||||||
|
:type "button"
|
||||||
|
:sx-post "/sx/(geography.(hypermedia.(example.(api.bulk))))?action=deactivate"
|
||||||
|
:sx-target "#bulk-table"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-include "#bulk-form"
|
||||||
|
(~tw :tokens "px-3 py-1.5 bg-stone-600 text-white rounded text-sm hover:bg-stone-700")
|
||||||
|
"Deactivate"))
|
||||||
|
(table
|
||||||
|
(~tw :tokens "w-full text-left text-sm")
|
||||||
|
(thead
|
||||||
|
(tr
|
||||||
|
(~tw :tokens "border-b border-stone-200")
|
||||||
|
(th (~tw :tokens "px-3 py-2 w-8") "")
|
||||||
|
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Name")
|
||||||
|
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Email")
|
||||||
|
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Status")))
|
||||||
|
(tbody
|
||||||
|
:id "bulk-table"
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(u)
|
||||||
|
(~examples/bulk-row
|
||||||
|
:id (nth u 0)
|
||||||
|
:name (nth u 1)
|
||||||
|
:email (nth u 2)
|
||||||
|
:status (nth u 3)))
|
||||||
|
users))))))
|
||||||
9
sx/sxc/examples/card.sx
Normal file
9
sx/sxc/examples/card.sx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key title description &rest children)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "border border-stone-200 rounded-lg overflow-hidden")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "bg-stone-100 px-4 py-3 border-b border-stone-200")
|
||||||
|
(h3 (~tw :tokens "font-semibold text-stone-800") title)
|
||||||
|
(when description (p (~tw :tokens "text-sm text-stone-500 mt-1") description)))
|
||||||
|
(div (~tw :tokens "p-4") children)))
|
||||||
8
sx/sxc/examples/click-result.sx
Normal file
8
sx/sxc/examples/click-result.sx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key time)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-2")
|
||||||
|
(p (~tw :tokens "text-stone-800 font-medium") "Content loaded!")
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-stone-500 text-sm")
|
||||||
|
(str "Fetched from the server via sx-get at " time))))
|
||||||
14
sx/sxc/examples/click-to-load-demo.sx
Normal file
14
sx/sxc/examples/click-to-load-demo.sx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(div
|
||||||
|
:id "click-result"
|
||||||
|
(~tw :tokens "p-4 rounded bg-stone-100 text-stone-500 text-center")
|
||||||
|
"Click the button to load content.")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.click))))"
|
||||||
|
:sx-target "#click-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors")
|
||||||
|
"Load content")))
|
||||||
17
sx/sxc/examples/delete-demo.sx
Normal file
17
sx/sxc/examples/delete-demo.sx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key items)
|
||||||
|
(div
|
||||||
|
(table
|
||||||
|
(~tw :tokens "w-full text-left text-sm")
|
||||||
|
(thead
|
||||||
|
(tr
|
||||||
|
(~tw :tokens "border-b border-stone-200")
|
||||||
|
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Item")
|
||||||
|
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600 w-20") "")))
|
||||||
|
(tbody
|
||||||
|
:id "delete-rows"
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(item)
|
||||||
|
(~examples/delete-row :id (nth item 0) :name (nth item 1)))
|
||||||
|
items)))))
|
||||||
15
sx/sxc/examples/delete-row.sx
Normal file
15
sx/sxc/examples/delete-row.sx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key id name)
|
||||||
|
(tr
|
||||||
|
:id (str "row-" id)
|
||||||
|
(~tw :tokens "border-b border-stone-100 transition-all")
|
||||||
|
(td (~tw :tokens "px-3 py-2 text-stone-700") name)
|
||||||
|
(td
|
||||||
|
(~tw :tokens "px-3 py-2")
|
||||||
|
(button
|
||||||
|
:sx-delete (str "/sx/(geography.(hypermedia.(example.(api.(delete." id ")))))")
|
||||||
|
:sx-target (str "#row-" id)
|
||||||
|
:sx-swap "outerHTML"
|
||||||
|
:sx-confirm "Delete this item?"
|
||||||
|
(~tw :tokens "text-rose-500 hover:text-rose-700 text-sm")
|
||||||
|
"delete"))))
|
||||||
5
sx/sxc/examples/demo.sx
Normal file
5
sx/sxc/examples/demo.sx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key &rest children)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "border border-dashed border-stone-300 rounded p-4 bg-stone-100")
|
||||||
|
children))
|
||||||
27
sx/sxc/examples/dialog-modal.sx
Normal file
27
sx/sxc/examples/dialog-modal.sx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key title message)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "fixed inset-0 z-50 flex items-center justify-center")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "absolute inset-0 bg-black/50")
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.dialog-close))))"
|
||||||
|
:sx-target "#dialog-container"
|
||||||
|
:sx-swap "innerHTML")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "relative bg-stone-100 rounded-lg shadow-xl p-6 max-w-md w-full mx-4 space-y-4")
|
||||||
|
(h3 (~tw :tokens "text-lg font-semibold text-stone-800") title)
|
||||||
|
(p (~tw :tokens "text-stone-600") message)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex justify-end gap-2")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.dialog-close))))"
|
||||||
|
:sx-target "#dialog-container"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-stone-200 text-stone-700 rounded text-sm hover:bg-stone-300")
|
||||||
|
"Cancel")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.dialog-close))))"
|
||||||
|
:sx-target "#dialog-container"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700")
|
||||||
|
"Confirm")))))
|
||||||
10
sx/sxc/examples/dialogs-demo.sx
Normal file
10
sx/sxc/examples/dialogs-demo.sx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.dialog))))"
|
||||||
|
:sx-target "#dialog-container"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Open Dialog")
|
||||||
|
(div :id "dialog-container")))
|
||||||
8
sx/sxc/examples/echo-result.sx
Normal file
8
sx/sxc/examples/echo-result.sx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key label items)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-1")
|
||||||
|
(p (~tw :tokens "text-stone-800 font-medium") (str "Server received " label ":"))
|
||||||
|
(map
|
||||||
|
(fn (item) (div (~tw :tokens "text-sm text-stone-600 font-mono") item))
|
||||||
|
items)))
|
||||||
23
sx/sxc/examples/edit-row-demo.sx
Normal file
23
sx/sxc/examples/edit-row-demo.sx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key rows)
|
||||||
|
(div
|
||||||
|
(table
|
||||||
|
(~tw :tokens "w-full text-left text-sm")
|
||||||
|
(thead
|
||||||
|
(tr
|
||||||
|
(~tw :tokens "border-b border-stone-200")
|
||||||
|
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Name")
|
||||||
|
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Price")
|
||||||
|
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Stock")
|
||||||
|
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600 w-24") "")))
|
||||||
|
(tbody
|
||||||
|
:id "edit-rows"
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(row)
|
||||||
|
(~examples/edit-row-view
|
||||||
|
:id (nth row 0)
|
||||||
|
:name (nth row 1)
|
||||||
|
:price (nth row 2)
|
||||||
|
:stock (nth row 3)))
|
||||||
|
rows)))))
|
||||||
44
sx/sxc/examples/edit-row-form.sx
Normal file
44
sx/sxc/examples/edit-row-form.sx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key id name price stock)
|
||||||
|
(tr
|
||||||
|
:id (str "erow-" id)
|
||||||
|
(~tw :tokens "border-b border-stone-100 bg-violet-50")
|
||||||
|
(td
|
||||||
|
(~tw :tokens "px-3 py-2")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:name "name"
|
||||||
|
:value name
|
||||||
|
(~tw :tokens "w-full px-2 py-1 border border-stone-300 rounded text-sm")))
|
||||||
|
(td
|
||||||
|
(~tw :tokens "px-3 py-2")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:name "price"
|
||||||
|
:value price
|
||||||
|
(~tw :tokens "w-20 px-2 py-1 border border-stone-300 rounded text-sm")))
|
||||||
|
(td
|
||||||
|
(~tw :tokens "px-3 py-2")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:name "stock"
|
||||||
|
:value stock
|
||||||
|
(~tw :tokens "w-20 px-2 py-1 border border-stone-300 rounded text-sm")))
|
||||||
|
(td
|
||||||
|
(~tw :tokens "px-3 py-2 space-x-1")
|
||||||
|
(button
|
||||||
|
:sx-post (str "/sx/(geography.(hypermedia.(example.(api.(editrow." id ")))))")
|
||||||
|
:sx-target (str "#erow-" id)
|
||||||
|
:sx-swap "outerHTML"
|
||||||
|
:sx-include (str "#erow-" id)
|
||||||
|
(~tw :tokens "text-sm text-emerald-600 hover:text-emerald-800")
|
||||||
|
"save")
|
||||||
|
(button
|
||||||
|
:sx-get (str
|
||||||
|
"/sx/(geography.(hypermedia.(example.(api.(editrow-cancel."
|
||||||
|
id
|
||||||
|
")))))")
|
||||||
|
:sx-target (str "#erow-" id)
|
||||||
|
:sx-swap "outerHTML"
|
||||||
|
(~tw :tokens "text-sm text-stone-500 hover:text-stone-700")
|
||||||
|
"cancel"))))
|
||||||
16
sx/sxc/examples/edit-row-view.sx
Normal file
16
sx/sxc/examples/edit-row-view.sx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key id name price stock)
|
||||||
|
(tr
|
||||||
|
:id (str "erow-" id)
|
||||||
|
(~tw :tokens "border-b border-stone-100")
|
||||||
|
(td (~tw :tokens "px-3 py-2 text-stone-700") name)
|
||||||
|
(td (~tw :tokens "px-3 py-2 text-stone-700") (str "$" price))
|
||||||
|
(td (~tw :tokens "px-3 py-2 text-stone-700") stock)
|
||||||
|
(td
|
||||||
|
(~tw :tokens "px-3 py-2")
|
||||||
|
(button
|
||||||
|
:sx-get (str "/sx/(geography.(hypermedia.(example.(api.(editrow." id ")))))")
|
||||||
|
:sx-target (str "#erow-" id)
|
||||||
|
:sx-swap "outerHTML"
|
||||||
|
(~tw :tokens "text-sm text-violet-600 hover:text-violet-800")
|
||||||
|
"edit"))))
|
||||||
24
sx/sxc/examples/form-demo.sx
Normal file
24
sx/sxc/examples/form-demo.sx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(form
|
||||||
|
:sx-post "/sx/(geography.(hypermedia.(example.(api.form))))"
|
||||||
|
:sx-target "#form-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(label (~tw :tokens "block text-sm font-medium text-stone-700 mb-1") "Name")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:name "name"
|
||||||
|
:placeholder "Enter a name"
|
||||||
|
(~tw :tokens "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")))
|
||||||
|
(button
|
||||||
|
:type "submit"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Submit"))
|
||||||
|
(div
|
||||||
|
:id "form-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-500 text-sm text-center")
|
||||||
|
"Submit the form to see the result.")))
|
||||||
8
sx/sxc/examples/form-result.sx
Normal file
8
sx/sxc/examples/form-result.sx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key name)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "text-stone-800")
|
||||||
|
(p (str "Hello, " (if (empty? name) "stranger" name) "!"))
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-sm text-stone-500 mt-1")
|
||||||
|
"Submitted via sx-post. The form data was sent as a POST request.")))
|
||||||
22
sx/sxc/examples/infinite-scroll-demo.sx
Normal file
22
sx/sxc/examples/infinite-scroll-demo.sx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "h-64 overflow-y-auto border border-stone-200 rounded")
|
||||||
|
:id "scroll-container"
|
||||||
|
(div
|
||||||
|
:id "scroll-items"
|
||||||
|
(map-indexed
|
||||||
|
(fn
|
||||||
|
(i item)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "px-4 py-3 border-b border-stone-100 text-sm text-stone-700")
|
||||||
|
(str "Item " (+ i 1) " — loaded with the page")))
|
||||||
|
(list 1 2 3 4 5))
|
||||||
|
(div
|
||||||
|
:id "scroll-sentinel"
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.scroll))))?page=2"
|
||||||
|
:sx-trigger "intersect once"
|
||||||
|
:sx-target "#scroll-items"
|
||||||
|
:sx-swap "beforeend"
|
||||||
|
(~tw :tokens "p-3 text-center text-stone-400 text-sm")
|
||||||
|
"Loading more..."))))
|
||||||
6
sx/sxc/examples/inline-edit-demo.sx
Normal file
6
sx/sxc/examples/inline-edit-demo.sx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
:id "edit-target"
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(~examples/inline-view :value "Click edit to change this text")))
|
||||||
25
sx/sxc/examples/inline-edit-form.sx
Normal file
25
sx/sxc/examples/inline-edit-form.sx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key value)
|
||||||
|
(form
|
||||||
|
:sx-post "/sx/(geography.(hypermedia.(example.(api.edit))))"
|
||||||
|
:sx-target "#edit-target"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "flex items-center gap-2 p-3 rounded border border-violet-300 bg-violet-50")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:name "value"
|
||||||
|
:value value
|
||||||
|
(~tw :tokens "flex-1 px-3 py-1.5 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"))
|
||||||
|
(button
|
||||||
|
:type "submit"
|
||||||
|
(~tw :tokens "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700")
|
||||||
|
"save")
|
||||||
|
(button
|
||||||
|
:type "button"
|
||||||
|
:sx-get (str
|
||||||
|
"/sx/(geography.(hypermedia.(example.(api.edit-cancel))))?value="
|
||||||
|
value)
|
||||||
|
:sx-target "#edit-target"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-3 py-1.5 bg-stone-200 text-stone-700 rounded text-sm hover:bg-stone-300")
|
||||||
|
"cancel")))
|
||||||
24
sx/sxc/examples/inline-validation-demo.sx
Normal file
24
sx/sxc/examples/inline-validation-demo.sx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(form
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
:sx-post "/sx/(geography.(hypermedia.(example.(api.validate-submit))))"
|
||||||
|
:sx-target "#validation-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(div
|
||||||
|
(label (~tw :tokens "block text-sm font-medium text-stone-700 mb-1") "Email")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:name "email"
|
||||||
|
:placeholder "user@example.com"
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.validate))))"
|
||||||
|
:sx-trigger "blur"
|
||||||
|
:sx-target "#email-feedback"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"))
|
||||||
|
(div :id "email-feedback" (~tw :tokens "mt-1")))
|
||||||
|
(button
|
||||||
|
:type "submit"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Submit")
|
||||||
|
(div :id "validation-result")))
|
||||||
11
sx/sxc/examples/inline-view.sx
Normal file
11
sx/sxc/examples/inline-view.sx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key value)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex items-center justify-between p-3 rounded border border-stone-200")
|
||||||
|
(span (~tw :tokens "text-stone-800") value)
|
||||||
|
(button
|
||||||
|
:sx-get (str "/sx/(geography.(hypermedia.(example.(api.edit))))?value=" value)
|
||||||
|
:sx-target "#edit-target"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "text-sm text-violet-600 hover:text-violet-800")
|
||||||
|
"edit")))
|
||||||
32
sx/sxc/examples/json-encoding-demo.sx
Normal file
32
sx/sxc/examples/json-encoding-demo.sx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(form
|
||||||
|
:sx-post "/sx/(geography.(hypermedia.(example.(api.json-echo))))"
|
||||||
|
:sx-target "#json-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-encoding "json"
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(label (~tw :tokens "block text-sm font-medium text-stone-700 mb-1") "Name")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:name "name"
|
||||||
|
:value "Ada Lovelace"
|
||||||
|
(~tw :tokens "w-full px-3 py-2 border border-stone-300 rounded text-sm")))
|
||||||
|
(div
|
||||||
|
(label (~tw :tokens "block text-sm font-medium text-stone-700 mb-1") "Age")
|
||||||
|
(input
|
||||||
|
:type "number"
|
||||||
|
:name "age"
|
||||||
|
:value "36"
|
||||||
|
(~tw :tokens "w-full px-3 py-2 border border-stone-300 rounded text-sm")))
|
||||||
|
(button
|
||||||
|
:type "submit"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Submit as JSON"))
|
||||||
|
(div
|
||||||
|
:id "json-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-500 text-sm")
|
||||||
|
"Submit the form to see the server echo the parsed JSON.")))
|
||||||
9
sx/sxc/examples/json-result.sx
Normal file
9
sx/sxc/examples/json-result.sx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key body content-type)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-2")
|
||||||
|
(p (~tw :tokens "text-stone-800 font-medium") "Server received:")
|
||||||
|
(pre
|
||||||
|
(~tw :tokens "text-sm bg-stone-100 p-3 rounded overflow-x-auto")
|
||||||
|
(code body))
|
||||||
|
(p (~tw :tokens "text-sm text-stone-500") (str "Content-Type: " content-type))))
|
||||||
8
sx/sxc/examples/kbd-result.sx
Normal file
8
sx/sxc/examples/kbd-result.sx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key key action)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-1")
|
||||||
|
(p (~tw :tokens "text-stone-800 font-medium") action)
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-sm text-stone-500")
|
||||||
|
(str "Triggered by pressing '" key "'"))))
|
||||||
46
sx/sxc/examples/keyboard-shortcuts-demo.sx
Normal file
46
sx/sxc/examples/keyboard-shortcuts-demo.sx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "p-4 rounded border border-stone-200 bg-stone-100")
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-sm text-stone-600 font-medium mb-2")
|
||||||
|
"Keyboard shortcuts:")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex gap-4")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex items-center gap-1")
|
||||||
|
(kbd
|
||||||
|
(~tw :tokens "px-2 py-0.5 bg-stone-100 border border-stone-300 rounded text-xs font-mono")
|
||||||
|
"s")
|
||||||
|
(span (~tw :tokens "text-sm text-stone-500") "Search"))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex items-center gap-1")
|
||||||
|
(kbd
|
||||||
|
(~tw :tokens "px-2 py-0.5 bg-stone-100 border border-stone-300 rounded text-xs font-mono")
|
||||||
|
"n")
|
||||||
|
(span (~tw :tokens "text-sm text-stone-500") "New item"))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex items-center gap-1")
|
||||||
|
(kbd
|
||||||
|
(~tw :tokens "px-2 py-0.5 bg-stone-100 border border-stone-300 rounded text-xs font-mono")
|
||||||
|
"h")
|
||||||
|
(span (~tw :tokens "text-sm text-stone-500") "Help"))))
|
||||||
|
(div
|
||||||
|
:id "kbd-target"
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.keyboard))))?key=s"
|
||||||
|
:sx-trigger "keyup[key=='s'&&!event.target.matches('input,textarea')] from:body"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "p-4 rounded border border-stone-200 bg-stone-100 text-center")
|
||||||
|
(p (~tw :tokens "text-stone-400 text-sm") "Press a shortcut key..."))
|
||||||
|
(div
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.keyboard))))?key=n"
|
||||||
|
:sx-trigger "keyup[key=='n'&&!event.target.matches('input,textarea')] from:body"
|
||||||
|
:sx-target "#kbd-target"
|
||||||
|
:sx-swap "innerHTML")
|
||||||
|
(div
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.keyboard))))?key=h"
|
||||||
|
:sx-trigger "keyup[key=='h'&&!event.target.matches('input,textarea')] from:body"
|
||||||
|
:sx-target "#kbd-target"
|
||||||
|
:sx-swap "innerHTML")))
|
||||||
17
sx/sxc/examples/lazy-loading-demo.sx
Normal file
17
sx/sxc/examples/lazy-loading-demo.sx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-sm text-stone-500")
|
||||||
|
"The content below loads automatically when the page renders.")
|
||||||
|
(div
|
||||||
|
:id "lazy-target"
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.lazy))))"
|
||||||
|
:sx-trigger "load"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "p-6 rounded border border-stone-200 bg-stone-100 text-center")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "animate-pulse space-y-2")
|
||||||
|
(div (~tw :tokens "h-4 bg-stone-200 rounded w-3/4 mx-auto"))
|
||||||
|
(div (~tw :tokens "h-4 bg-stone-200 rounded w-1/2 mx-auto"))))))
|
||||||
8
sx/sxc/examples/lazy-result.sx
Normal file
8
sx/sxc/examples/lazy-result.sx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key time)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-2")
|
||||||
|
(p (~tw :tokens "text-stone-800 font-medium") "Content loaded on page render!")
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-stone-500 text-sm")
|
||||||
|
(str "Loaded via sx-trigger=\"load\" at " time))))
|
||||||
5
sx/sxc/examples/loading-result.sx
Normal file
5
sx/sxc/examples/loading-result.sx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key time)
|
||||||
|
(div
|
||||||
|
(p (~tw :tokens "text-stone-800 font-medium") "Loaded!")
|
||||||
|
(p (~tw :tokens "text-sm text-stone-500") (str "Response arrived at " time))))
|
||||||
18
sx/sxc/examples/loading-states-demo.sx
Normal file
18
sx/sxc/examples/loading-states-demo.sx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.slow))))"
|
||||||
|
:sx-target "#loading-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "sx-loading-btn px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm flex items-center gap-2")
|
||||||
|
(span
|
||||||
|
(~tw :tokens "sx-spinner w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"))
|
||||||
|
(span "Load slow endpoint"))
|
||||||
|
(div
|
||||||
|
:id "loading-result"
|
||||||
|
(~tw :tokens "p-4 rounded border border-stone-200 bg-stone-100 text-center")
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-stone-400 text-sm")
|
||||||
|
"Click the button — it takes 2 seconds."))))
|
||||||
22
sx/sxc/examples/oob-demo.sx
Normal file
22
sx/sxc/examples/oob-demo.sx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "grid grid-cols-2 gap-4")
|
||||||
|
(div
|
||||||
|
:id "oob-box-a"
|
||||||
|
(~tw :tokens "p-4 rounded border border-stone-200 bg-stone-100 text-center")
|
||||||
|
(p (~tw :tokens "text-stone-500") "Box A")
|
||||||
|
(p (~tw :tokens "text-sm text-stone-400") "Waiting..."))
|
||||||
|
(div
|
||||||
|
:id "oob-box-b"
|
||||||
|
(~tw :tokens "p-4 rounded border border-stone-200 bg-stone-100 text-center")
|
||||||
|
(p (~tw :tokens "text-stone-500") "Box B")
|
||||||
|
(p (~tw :tokens "text-sm text-stone-400") "Waiting...")))
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.oob))))"
|
||||||
|
:sx-target "#oob-box-a"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Update both boxes")))
|
||||||
17
sx/sxc/examples/poll-result.sx
Normal file
17
sx/sxc/examples/poll-result.sx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key time count)
|
||||||
|
(div
|
||||||
|
(p (~tw :tokens "text-stone-800 font-medium") (str "Server time: " time))
|
||||||
|
(p (~tw :tokens "text-stone-500 text-sm mt-1") (str "Poll count: " count))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "mt-2 flex justify-center")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex gap-1")
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(div
|
||||||
|
:class (str
|
||||||
|
"w-2 h-2 rounded-full "
|
||||||
|
(if (<= i count) "bg-violet-500" "bg-stone-200"))))
|
||||||
|
(list 1 2 3 4 5 6 7 8 9 10))))))
|
||||||
11
sx/sxc/examples/polling-demo.sx
Normal file
11
sx/sxc/examples/polling-demo.sx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(div
|
||||||
|
:id "poll-target"
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.poll))))"
|
||||||
|
:sx-trigger "load, every 2s"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "p-4 rounded border border-stone-200 bg-stone-100 text-center font-mono")
|
||||||
|
"Loading...")))
|
||||||
41
sx/sxc/examples/pp-form-full.sx
Normal file
41
sx/sxc/examples/pp-form-full.sx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key name email role)
|
||||||
|
(form
|
||||||
|
:sx-put "/sx/(geography.(hypermedia.(example.(api.putpatch))))"
|
||||||
|
:sx-target "#pp-target"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(label (~tw :tokens "block text-sm font-medium text-stone-700 mb-1") "Name")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:name "name"
|
||||||
|
:value name
|
||||||
|
(~tw :tokens "w-full px-3 py-2 border border-stone-300 rounded text-sm")))
|
||||||
|
(div
|
||||||
|
(label (~tw :tokens "block text-sm font-medium text-stone-700 mb-1") "Email")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:name "email"
|
||||||
|
:value email
|
||||||
|
(~tw :tokens "w-full px-3 py-2 border border-stone-300 rounded text-sm")))
|
||||||
|
(div
|
||||||
|
(label (~tw :tokens "block text-sm font-medium text-stone-700 mb-1") "Role")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:name "role"
|
||||||
|
:value role
|
||||||
|
(~tw :tokens "w-full px-3 py-2 border border-stone-300 rounded text-sm")))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex gap-2")
|
||||||
|
(button
|
||||||
|
:type "submit"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700")
|
||||||
|
"Save All (PUT)")
|
||||||
|
(button
|
||||||
|
:type "button"
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.putpatch-cancel))))"
|
||||||
|
:sx-target "#pp-target"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-stone-200 text-stone-700 rounded text-sm hover:bg-stone-300")
|
||||||
|
"Cancel"))))
|
||||||
16
sx/sxc/examples/pp-view.sx
Normal file
16
sx/sxc/examples/pp-view.sx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key name email role)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex justify-between items-start")
|
||||||
|
(div
|
||||||
|
(p (~tw :tokens "text-stone-800 font-medium") name)
|
||||||
|
(p (~tw :tokens "text-sm text-stone-500") email)
|
||||||
|
(p (~tw :tokens "text-sm text-stone-500") role))
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.putpatch-edit-all))))"
|
||||||
|
:sx-target "#pp-target"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "text-sm text-violet-600 hover:text-violet-800")
|
||||||
|
"Edit All (PUT)"))))
|
||||||
19
sx/sxc/examples/progress-bar-demo.sx
Normal file
19
sx/sxc/examples/progress-bar-demo.sx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(div
|
||||||
|
:id "progress-target"
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "w-full bg-stone-200 rounded-full h-4")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "bg-violet-600 h-4 rounded-full transition-all")
|
||||||
|
:style "width: 0%"))
|
||||||
|
(p (~tw :tokens "text-sm text-stone-500 text-center") "Click start to begin."))
|
||||||
|
(button
|
||||||
|
:sx-post "/sx/(geography.(hypermedia.(example.(api.progress-start))))"
|
||||||
|
:sx-target "#progress-target"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Start job")))
|
||||||
24
sx/sxc/examples/progress-status.sx
Normal file
24
sx/sxc/examples/progress-status.sx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key percent job-id)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "w-full bg-stone-200 rounded-full h-4")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "bg-violet-600 h-4 rounded-full transition-all")
|
||||||
|
:style (str "width: " percent "%")))
|
||||||
|
(p (~tw :tokens "text-sm text-stone-500 text-center") (str percent "% complete"))
|
||||||
|
(when
|
||||||
|
(< percent 100)
|
||||||
|
(div
|
||||||
|
:sx-get (str
|
||||||
|
"/sx/(geography.(hypermedia.(example.(api.progress-status))))?job="
|
||||||
|
job-id)
|
||||||
|
:sx-trigger "load delay:500ms"
|
||||||
|
:sx-target "#progress-target"
|
||||||
|
:sx-swap "innerHTML"))
|
||||||
|
(when
|
||||||
|
(= percent 100)
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-sm text-emerald-600 font-medium text-center")
|
||||||
|
"Job complete!"))))
|
||||||
6
sx/sxc/examples/put-patch-demo.sx
Normal file
6
sx/sxc/examples/put-patch-demo.sx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key name email role)
|
||||||
|
(div
|
||||||
|
:id "pp-target"
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(~examples/pp-view :name name :email email :role role)))
|
||||||
5
sx/sxc/examples/reset-message.sx
Normal file
5
sx/sxc/examples/reset-message.sx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key message time)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "px-3 py-2 bg-stone-100 rounded text-sm text-stone-700")
|
||||||
|
(str "[" time "] " message)))
|
||||||
24
sx/sxc/examples/reset-on-submit-demo.sx
Normal file
24
sx/sxc/examples/reset-on-submit-demo.sx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(form
|
||||||
|
:id "reset-form"
|
||||||
|
:sx-post "/sx/(geography.(hypermedia.(example.(api.reset-submit))))"
|
||||||
|
:sx-target "#reset-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-on:afterSwap "this.reset()"
|
||||||
|
(~tw :tokens "flex gap-2")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:name "message"
|
||||||
|
:placeholder "Type a message..."
|
||||||
|
(~tw :tokens "flex-1 px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"))
|
||||||
|
(button
|
||||||
|
:type "submit"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Send"))
|
||||||
|
(div
|
||||||
|
:id "reset-result"
|
||||||
|
(~tw :tokens "space-y-2")
|
||||||
|
(p (~tw :tokens "text-sm text-stone-400") "Messages will appear here."))))
|
||||||
17
sx/sxc/examples/retry-demo.sx
Normal file
17
sx/sxc/examples/retry-demo.sx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-4")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.flaky))))"
|
||||||
|
:sx-target "#retry-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-retry "exponential:1000:8000"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Call flaky endpoint")
|
||||||
|
(div
|
||||||
|
:id "retry-result"
|
||||||
|
(~tw :tokens "p-4 rounded border border-stone-200 bg-stone-100 text-center")
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-stone-400 text-sm")
|
||||||
|
"Endpoint fails twice, succeeds on 3rd attempt."))))
|
||||||
6
sx/sxc/examples/retry-result.sx
Normal file
6
sx/sxc/examples/retry-result.sx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key attempt message)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-1")
|
||||||
|
(p (~tw :tokens "text-stone-800 font-medium") message)
|
||||||
|
(p (~tw :tokens "text-sm text-stone-500") (str "Succeeded on attempt #" attempt))))
|
||||||
20
sx/sxc/examples/scroll-items.sx
Normal file
20
sx/sxc/examples/scroll-items.sx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key items page)
|
||||||
|
(<>
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(item)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "px-4 py-3 border-b border-stone-100 text-sm text-stone-700")
|
||||||
|
item))
|
||||||
|
items)
|
||||||
|
(when
|
||||||
|
(<= page 5)
|
||||||
|
(div
|
||||||
|
:id "scroll-sentinel"
|
||||||
|
:sx-get (str "/sx/(geography.(hypermedia.(example.(api.scroll))))?page=" page)
|
||||||
|
:sx-trigger "intersect once"
|
||||||
|
:sx-target "#scroll-items"
|
||||||
|
:sx-swap "beforeend"
|
||||||
|
(~tw :tokens "p-3 text-center text-stone-400 text-sm")
|
||||||
|
"Loading more..."))))
|
||||||
11
sx/sxc/examples/search-results.sx
Normal file
11
sx/sxc/examples/search-results.sx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key items query)
|
||||||
|
(<>
|
||||||
|
(if
|
||||||
|
(empty? items)
|
||||||
|
(p
|
||||||
|
(~tw :tokens "p-3 text-sm text-stone-400")
|
||||||
|
(str "No results for \"" query "\""))
|
||||||
|
(map
|
||||||
|
(fn (item) (div (~tw :tokens "px-3 py-2 text-sm text-stone-700") item))
|
||||||
|
items))))
|
||||||
30
sx/sxc/examples/select-filter-demo.sx
Normal file
30
sx/sxc/examples/select-filter-demo.sx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex gap-2")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.dashboard))))"
|
||||||
|
:sx-target "#filter-target"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-select "#dash-stats"
|
||||||
|
(~tw :tokens "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700")
|
||||||
|
"Stats Only")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.dashboard))))"
|
||||||
|
:sx-target "#filter-target"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-select "#dash-header"
|
||||||
|
(~tw :tokens "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700")
|
||||||
|
"Header Only")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.dashboard))))"
|
||||||
|
:sx-target "#filter-target"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-3 py-1.5 bg-stone-600 text-white rounded text-sm hover:bg-stone-700")
|
||||||
|
"Full Dashboard"))
|
||||||
|
(div
|
||||||
|
:id "filter-target"
|
||||||
|
(~tw :tokens "border border-stone-200 rounded p-4 bg-stone-100")
|
||||||
|
(p (~tw :tokens "text-sm text-stone-400") "Click a button to load content."))))
|
||||||
7
sx/sxc/examples/source.sx
Normal file
7
sx/sxc/examples/source.sx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key src-code)
|
||||||
|
(div
|
||||||
|
(~tw :tokens "not-prose bg-stone-100 rounded p-5 mt-3 mx-auto max-w-3xl")
|
||||||
|
(pre
|
||||||
|
(~tw :tokens "text-sm leading-relaxed whitespace-pre-wrap break-words")
|
||||||
|
(code src-code))))
|
||||||
3
sx/sxc/examples/swap-entry.sx
Normal file
3
sx/sxc/examples/swap-entry.sx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key time mode)
|
||||||
|
(div (~tw :tokens "px-3 py-2 text-sm text-stone-700") (str "[" time "] " mode)))
|
||||||
32
sx/sxc/examples/swap-positions-demo.sx
Normal file
32
sx/sxc/examples/swap-positions-demo.sx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex gap-2")
|
||||||
|
(button
|
||||||
|
:sx-post "/sx/(geography.(hypermedia.(example.(api.swap-log))))?mode=beforeend"
|
||||||
|
:sx-target "#swap-log"
|
||||||
|
:sx-swap "beforeend"
|
||||||
|
(~tw :tokens "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700")
|
||||||
|
"Add to End")
|
||||||
|
(button
|
||||||
|
:sx-post "/sx/(geography.(hypermedia.(example.(api.swap-log))))?mode=afterbegin"
|
||||||
|
:sx-target "#swap-log"
|
||||||
|
:sx-swap "afterbegin"
|
||||||
|
(~tw :tokens "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700")
|
||||||
|
"Add to Start")
|
||||||
|
(button
|
||||||
|
:sx-post "/sx/(geography.(hypermedia.(example.(api.swap-log))))?mode=none"
|
||||||
|
:sx-target "#swap-log"
|
||||||
|
:sx-swap "none"
|
||||||
|
(~tw :tokens "px-3 py-1.5 bg-stone-600 text-white rounded text-sm hover:bg-stone-700")
|
||||||
|
"Silent Ping")
|
||||||
|
(span
|
||||||
|
:id "swap-counter"
|
||||||
|
(~tw :tokens "self-center text-sm text-stone-500")
|
||||||
|
"Count: 0"))
|
||||||
|
(div
|
||||||
|
:id "swap-log"
|
||||||
|
(~tw :tokens "border border-stone-200 rounded h-48 overflow-y-auto divide-y divide-stone-100")
|
||||||
|
(p (~tw :tokens "p-3 text-sm text-stone-400") "Log entries will appear here."))))
|
||||||
20
sx/sxc/examples/sync-replace-demo.sx
Normal file
20
sx/sxc/examples/sync-replace-demo.sx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:name "q"
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.slow-search))))"
|
||||||
|
:sx-trigger "keyup delay:200ms changed"
|
||||||
|
:sx-target "#sync-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-sync "replace"
|
||||||
|
:placeholder "Type to search (random delay 0.5-2s)..."
|
||||||
|
(~tw :tokens "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"))
|
||||||
|
(div
|
||||||
|
:id "sync-result"
|
||||||
|
(~tw :tokens "p-4 rounded border border-stone-200 bg-stone-100")
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-sm text-stone-400")
|
||||||
|
"Type to trigger requests — stale ones get aborted."))))
|
||||||
7
sx/sxc/examples/sync-result.sx
Normal file
7
sx/sxc/examples/sync-result.sx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key query delay)
|
||||||
|
(div
|
||||||
|
(p (~tw :tokens "text-stone-800 font-medium") (str "Result for: \"" query "\""))
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-sm text-stone-500")
|
||||||
|
(str "Server took " delay "ms to respond"))))
|
||||||
14
sx/sxc/examples/tab-btn.sx
Normal file
14
sx/sxc/examples/tab-btn.sx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key tab label active)
|
||||||
|
(button
|
||||||
|
:sx-get (str "/sx/(geography.(hypermedia.(example.(api.(tabs." tab ")))))")
|
||||||
|
:sx-target "#tab-content"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-push-url (str "/sx/(geography.(hypermedia.(example.tabs)))?tab=" tab)
|
||||||
|
:class (str
|
||||||
|
"px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors "
|
||||||
|
(if
|
||||||
|
(= active "true")
|
||||||
|
"border-violet-600 text-violet-600"
|
||||||
|
"border-transparent text-stone-500 hover:text-stone-700"))
|
||||||
|
label))
|
||||||
19
sx/sxc/examples/tabs-demo.sx
Normal file
19
sx/sxc/examples/tabs-demo.sx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-0")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex border-b border-stone-200")
|
||||||
|
:id "tab-buttons"
|
||||||
|
(~examples/tab-btn :tab "tab1" :label "Overview" :active "true")
|
||||||
|
(~examples/tab-btn :tab "tab2" :label "Details" :active "false")
|
||||||
|
(~examples/tab-btn :tab "tab3" :label "History" :active "false"))
|
||||||
|
(div
|
||||||
|
:id "tab-content"
|
||||||
|
(~tw :tokens "p-4 border border-t-0 border-stone-200 rounded-b")
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-stone-700")
|
||||||
|
"Welcome to the Overview tab. This content is loaded by default.")
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-stone-500 text-sm mt-2")
|
||||||
|
"Click the tabs above to navigate. Watch the browser URL update."))))
|
||||||
3
sx/sxc/examples/validation-error.sx
Normal file
3
sx/sxc/examples/validation-error.sx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key message)
|
||||||
|
(p (~tw :tokens "text-sm text-rose-600") message))
|
||||||
3
sx/sxc/examples/validation-ok.sx
Normal file
3
sx/sxc/examples/validation-ok.sx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key email)
|
||||||
|
(p (~tw :tokens "text-sm text-emerald-600") (str email " is available")))
|
||||||
36
sx/sxc/examples/vals-headers-demo.sx
Normal file
36
sx/sxc/examples/vals-headers-demo.sx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-6")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-2")
|
||||||
|
(h4
|
||||||
|
(~tw :tokens "text-sm font-semibold text-stone-700")
|
||||||
|
"sx-vals — send extra values")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.echo-vals))))"
|
||||||
|
:sx-target "#vals-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-vals "{\"source\": \"button\", \"version\": \"2.0\"}"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Send with vals")
|
||||||
|
(div
|
||||||
|
:id "vals-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-sm text-stone-400")
|
||||||
|
"Click to see server-received values."))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-2")
|
||||||
|
(h4
|
||||||
|
(~tw :tokens "text-sm font-semibold text-stone-700")
|
||||||
|
"sx-headers — send custom headers")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.echo-headers))))"
|
||||||
|
:sx-target "#headers-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-headers {:X-Request-Source "demo" :X-Custom-Token "abc123"}
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Send with headers")
|
||||||
|
(div
|
||||||
|
:id "headers-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-sm text-stone-400")
|
||||||
|
"Click to see server-received headers."))))
|
||||||
3
sx/sxc/examples/value-options.sx
Normal file
3
sx/sxc/examples/value-options.sx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
(defcomp
|
||||||
|
(&key items)
|
||||||
|
(<> (map (fn (item) (option :value item item)) items)))
|
||||||
23
sx/sxc/examples/value-select-demo.sx
Normal file
23
sx/sxc/examples/value-select-demo.sx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(label (~tw :tokens "block text-sm font-medium text-stone-700 mb-1") "Category")
|
||||||
|
(select
|
||||||
|
:name "category"
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(example.(api.values))))"
|
||||||
|
:sx-trigger "change"
|
||||||
|
:sx-target "#value-items"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
||||||
|
(option :value "" "Pick a category...")
|
||||||
|
(option :value "Languages" "Languages")
|
||||||
|
(option :value "Frameworks" "Frameworks")
|
||||||
|
(option :value "Databases" "Databases")))
|
||||||
|
(div
|
||||||
|
(label (~tw :tokens "block text-sm font-medium text-stone-700 mb-1") "Item")
|
||||||
|
(select
|
||||||
|
:id "value-items"
|
||||||
|
(~tw :tokens "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
||||||
|
(option :value "" "Select a category first...")))))
|
||||||
9
sx/sxc/home/credits.sx
Normal file
9
sx/sxc/home/credits.sx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
(defcomp ()
|
||||||
|
(div (~tw :tokens "max-w-4xl mx-auto px-6 py-12 border-t border-stone-200")
|
||||||
|
(p (~tw :tokens "text-stone-500 text-sm")
|
||||||
|
"sx is heavily inspired by "
|
||||||
|
(a :href "https://htmx.org" (~tw :tokens "text-violet-600 hover:underline") "htmx")
|
||||||
|
" by Carson Gross. This documentation site is modelled on "
|
||||||
|
(a :href "https://four.htmx.org" (~tw :tokens "text-violet-600 hover:underline") "four.htmx.org")
|
||||||
|
". htmx showed that hypermedia belongs on the server. "
|
||||||
|
"sx takes that idea and wraps it in parentheses.")))
|
||||||
13
sx/sxc/home/hero.sx
Normal file
13
sx/sxc/home/hero.sx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
;; SX docs — home page components
|
||||||
|
(defcomp (&key &rest children)
|
||||||
|
(div (~tw :tokens "max-w-4xl mx-auto px-6 py-16 text-center")
|
||||||
|
(h1 (~tw :tokens "text-5xl font-bold text-stone-900 mb-4")
|
||||||
|
(span (~tw :tokens "text-violet-600 font-mono") "(<sx>)"))
|
||||||
|
(p (~tw :tokens "text-2xl text-stone-600 mb-4")
|
||||||
|
"The framework-free reactive hypermedium")
|
||||||
|
(p (~tw :tokens "text-sm text-stone-400")
|
||||||
|
"© Giles Bradshaw 2026")
|
||||||
|
(p (~tw :tokens "text-lg text-stone-500 max-w-2xl mx-auto mb-12")
|
||||||
|
"(sx === code === data === protocol === content === behaviour === layout === style === spec === sx)")
|
||||||
|
(div (~tw :tokens "bg-stone-100 rounded-lg p-6 text-left font-mono text-sm mx-auto max-w-2xl")
|
||||||
|
(pre (~tw :tokens "leading-relaxed whitespace-pre-wrap") children))))
|
||||||
19
sx/sxc/home/how-it-works.sx
Normal file
19
sx/sxc/home/how-it-works.sx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
(defcomp ()
|
||||||
|
(div (~tw :tokens "max-w-4xl mx-auto px-6 py-12")
|
||||||
|
(h2 (~tw :tokens "text-3xl font-bold text-stone-900 mb-8") "How it works")
|
||||||
|
(div (~tw :tokens "space-y-6")
|
||||||
|
(div (~tw :tokens "flex items-start gap-4")
|
||||||
|
(div (~tw :tokens "flex-shrink-0 w-8 h-8 rounded-full bg-violet-100 text-violet-700 flex items-center justify-center font-bold") "1")
|
||||||
|
(div
|
||||||
|
(h3 (~tw :tokens "font-semibold text-stone-900") "Server renders sx")
|
||||||
|
(p (~tw :tokens "text-stone-600") "Python builds s-expression trees. Components, elements, data — all in one format.")))
|
||||||
|
(div (~tw :tokens "flex items-start gap-4")
|
||||||
|
(div (~tw :tokens "flex-shrink-0 w-8 h-8 rounded-full bg-violet-100 text-violet-700 flex items-center justify-center font-bold") "2")
|
||||||
|
(div
|
||||||
|
(h3 (~tw :tokens "font-semibold text-stone-900") "Wire sends text/sx")
|
||||||
|
(p (~tw :tokens "text-stone-600") "Responses are s-expression source code with content type text/sx. Component definitions cached client-side.")))
|
||||||
|
(div (~tw :tokens "flex items-start gap-4")
|
||||||
|
(div (~tw :tokens "flex-shrink-0 w-8 h-8 rounded-full bg-violet-100 text-violet-700 flex items-center justify-center font-bold") "3")
|
||||||
|
(div
|
||||||
|
(h3 (~tw :tokens "font-semibold text-stone-900") "Client evaluates + renders")
|
||||||
|
(p (~tw :tokens "text-stone-600") "sx.js parses, evaluates, and renders to DOM. Same evaluator runs server-side (Python) and client-side (JS)."))))))
|
||||||
20
sx/sxc/home/philosophy.sx
Normal file
20
sx/sxc/home/philosophy.sx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
(defcomp ()
|
||||||
|
(div (~tw :tokens "max-w-4xl mx-auto px-6 py-12")
|
||||||
|
(h2 (~tw :tokens "text-3xl font-bold text-stone-900 mb-8") "Design philosophy")
|
||||||
|
(div (~tw :tokens "grid md:grid-cols-2 gap-8")
|
||||||
|
(div (~tw :tokens "space-y-4")
|
||||||
|
(h3 (~tw :tokens "text-xl font-semibold text-violet-700") "From htmx")
|
||||||
|
(ul (~tw :tokens "space-y-2 text-stone-600")
|
||||||
|
(li "Server-rendered DOM over the wire (no HTML)")
|
||||||
|
(li "Hypermedia attributes on any element (sx-get, sx-post, ...)")
|
||||||
|
(li "Target/swap model for partial page updates")
|
||||||
|
(li "No client-side routing, no virtual DOM")
|
||||||
|
(li "Progressive enhancement — works without JS (mostly)")))
|
||||||
|
(div (~tw :tokens "space-y-4")
|
||||||
|
(h3 (~tw :tokens "text-xl font-semibold text-violet-700") "From React")
|
||||||
|
(ul (~tw :tokens "space-y-2 text-stone-600")
|
||||||
|
(li "Composable components with defcomp")
|
||||||
|
(li "Client-side rendering from s-expression source")
|
||||||
|
(li "Component caching via localStorage + hash invalidation")
|
||||||
|
(li "On-demand CSS — only ship what's used")
|
||||||
|
(li "DOM morphing for smooth history navigation"))))))
|
||||||
23
sx/sxc/reference/ref-boost-demo.sx
Normal file
23
sx/sxc/reference/ref-boost-demo.sx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(nav
|
||||||
|
:sx-boost "true"
|
||||||
|
(~tw :tokens "flex gap-3")
|
||||||
|
(a
|
||||||
|
:href "/sx/(geography.(hypermedia.(reference-detail.attributes.sx-get)))"
|
||||||
|
(~tw :tokens "text-violet-600 hover:text-violet-800 underline text-sm")
|
||||||
|
"sx-get")
|
||||||
|
(a
|
||||||
|
:href "/sx/(geography.(hypermedia.(reference-detail.attributes.sx-post)))"
|
||||||
|
(~tw :tokens "text-violet-600 hover:text-violet-800 underline text-sm")
|
||||||
|
"sx-post")
|
||||||
|
(a
|
||||||
|
:href "/sx/(geography.(hypermedia.(reference-detail.attributes.sx-target)))"
|
||||||
|
(~tw :tokens "text-violet-600 hover:text-violet-800 underline text-sm")
|
||||||
|
"sx-target"))
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-xs text-stone-400")
|
||||||
|
"These links use AJAX navigation via sx-boost — no sx-get needed on each link. "
|
||||||
|
"#sx-content")))
|
||||||
15
sx/sxc/reference/ref-confirm-demo.sx
Normal file
15
sx/sxc/reference/ref-confirm-demo.sx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-2")
|
||||||
|
(div
|
||||||
|
:id "ref-confirm-item"
|
||||||
|
(~tw :tokens "flex items-center justify-between p-3 border border-stone-200 rounded")
|
||||||
|
(span (~tw :tokens "text-sm text-stone-700") "Important file.txt")
|
||||||
|
(button
|
||||||
|
:sx-delete "/sx/(geography.(hypermedia.(reference.(api.(item.confirm)))))"
|
||||||
|
:sx-target "#ref-confirm-item"
|
||||||
|
:sx-swap "delete"
|
||||||
|
:sx-confirm "Are you sure you want to delete this file?"
|
||||||
|
(~tw :tokens "px-3 py-1 text-red-500 text-sm border border-red-200 rounded hover:bg-red-50")
|
||||||
|
"Delete"))))
|
||||||
9
sx/sxc/reference/ref-data-sx-demo.sx
Normal file
9
sx/sxc/reference/ref-data-sx-demo.sx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
:data-sx "(div :class \"p-3 bg-violet-50 rounded\" (h3 :class \"font-semibold text-violet-800\" \"Client-rendered\") (p :class \"text-sm text-stone-600\" \"This was evaluated in the browser — no server request.\"))")
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-xs text-stone-400")
|
||||||
|
"The content above is rendered client-side from the data-sx attribute.")))
|
||||||
10
sx/sxc/reference/ref-data-sx-env-demo.sx
Normal file
10
sx/sxc/reference/ref-data-sx-env-demo.sx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
:data-sx "(div :class \"p-3 bg-emerald-50 rounded\" (h3 :class \"font-semibold text-emerald-800\" title) (p :class \"text-sm text-stone-600\" message))"
|
||||||
|
:data-sx-env "{\"title\": \"Dynamic content\", \"message\": \"Variables passed via data-sx-env are available in the expression.\"}")
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-xs text-stone-400")
|
||||||
|
"The title and message above come from the data-sx-env JSON.")))
|
||||||
34
sx/sxc/reference/ref-delete-demo.sx
Normal file
34
sx/sxc/reference/ref-delete-demo.sx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-2")
|
||||||
|
(div
|
||||||
|
:id "ref-del-1"
|
||||||
|
(~tw :tokens "flex items-center justify-between p-2 border border-stone-200 rounded")
|
||||||
|
(span (~tw :tokens "text-sm text-stone-700") "Item A")
|
||||||
|
(button
|
||||||
|
:sx-delete "/sx/(geography.(hypermedia.(reference.(api.(item.1)))))"
|
||||||
|
:sx-target "#ref-del-1"
|
||||||
|
:sx-swap "delete"
|
||||||
|
(~tw :tokens "text-red-500 text-sm hover:text-red-700")
|
||||||
|
"Remove"))
|
||||||
|
(div
|
||||||
|
:id "ref-del-2"
|
||||||
|
(~tw :tokens "flex items-center justify-between p-2 border border-stone-200 rounded")
|
||||||
|
(span (~tw :tokens "text-sm text-stone-700") "Item B")
|
||||||
|
(button
|
||||||
|
:sx-delete "/sx/(geography.(hypermedia.(reference.(api.(item.2)))))"
|
||||||
|
:sx-target "#ref-del-2"
|
||||||
|
:sx-swap "delete"
|
||||||
|
(~tw :tokens "text-red-500 text-sm hover:text-red-700")
|
||||||
|
"Remove"))
|
||||||
|
(div
|
||||||
|
:id "ref-del-3"
|
||||||
|
(~tw :tokens "flex items-center justify-between p-2 border border-stone-200 rounded")
|
||||||
|
(span (~tw :tokens "text-sm text-stone-700") "Item C")
|
||||||
|
(button
|
||||||
|
:sx-delete "/sx/(geography.(hypermedia.(reference.(api.(item.3)))))"
|
||||||
|
:sx-target "#ref-del-3"
|
||||||
|
:sx-swap "delete"
|
||||||
|
(~tw :tokens "text-red-500 text-sm hover:text-red-700")
|
||||||
|
"Remove"))))
|
||||||
30
sx/sxc/reference/ref-disable-demo.sx
Normal file
30
sx/sxc/reference/ref-disable-demo.sx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "grid grid-cols-2 gap-3")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "p-3 border border-stone-200 rounded")
|
||||||
|
(p (~tw :tokens "text-xs text-stone-400 mb-2") "sx enabled")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))"
|
||||||
|
:sx-target "#ref-dis-a"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-3 py-1 bg-violet-600 text-white rounded text-sm")
|
||||||
|
"Load")
|
||||||
|
(div :id "ref-dis-a" (~tw :tokens "mt-2 text-sm text-stone-500") "—"))
|
||||||
|
(div
|
||||||
|
:sx-disable "true"
|
||||||
|
(~tw :tokens "p-3 border border-stone-200 rounded")
|
||||||
|
(p (~tw :tokens "text-xs text-stone-400 mb-2") "sx disabled")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))"
|
||||||
|
:sx-target "#ref-dis-b"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-3 py-1 bg-stone-400 text-white rounded text-sm")
|
||||||
|
"Load")
|
||||||
|
(div
|
||||||
|
:id "ref-dis-b"
|
||||||
|
(~tw :tokens "mt-2 text-sm text-stone-500")
|
||||||
|
"Button won't fire sx request")))))
|
||||||
22
sx/sxc/reference/ref-disabled-elt-demo.sx
Normal file
22
sx/sxc/reference/ref-disabled-elt-demo.sx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex gap-3 items-center")
|
||||||
|
(button
|
||||||
|
:id "ref-diselt-btn"
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.slow-echo))))"
|
||||||
|
:sx-target "#ref-diselt-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-disabled-elt "#ref-diselt-btn"
|
||||||
|
:sx-vals "{\"q\": \"hello\"}"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm disabled:opacity-50")
|
||||||
|
"Click (disables during request)")
|
||||||
|
(span
|
||||||
|
(~tw :tokens "text-xs text-stone-400")
|
||||||
|
"Button is disabled while request is in-flight."))
|
||||||
|
(div
|
||||||
|
:id "ref-diselt-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm")
|
||||||
|
"Click the button to see it disable during the request.")))
|
||||||
22
sx/sxc/reference/ref-encoding-demo.sx
Normal file
22
sx/sxc/reference/ref-encoding-demo.sx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(form
|
||||||
|
:sx-post "/sx/(geography.(hypermedia.(reference.(api.upload-name))))"
|
||||||
|
:sx-encoding "multipart/form-data"
|
||||||
|
:sx-target "#ref-encoding-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "flex gap-2")
|
||||||
|
(input
|
||||||
|
:type "file"
|
||||||
|
:name "file"
|
||||||
|
(~tw :tokens "flex-1 text-sm text-stone-500 file:mr-2 file:px-3 file:py-1 file:rounded file:border-0 file:text-sm file:bg-violet-50 file:text-violet-700"))
|
||||||
|
(button
|
||||||
|
:type "submit"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700")
|
||||||
|
"Upload"))
|
||||||
|
(div
|
||||||
|
:id "ref-encoding-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm")
|
||||||
|
"Select a file and submit.")))
|
||||||
19
sx/sxc/reference/ref-event-after-request-demo.sx
Normal file
19
sx/sxc/reference/ref-event-after-request-demo.sx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))"
|
||||||
|
:sx-target "#ref-evt-ar-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-on:sx:afterRequest "document.getElementById('ref-evt-ar-log').textContent = 'Response status: ' + (event.detail ? event.detail.status : '?')"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Load (logs after response)")
|
||||||
|
(div
|
||||||
|
:id "ref-evt-ar-log"
|
||||||
|
(~tw :tokens "p-2 rounded bg-emerald-50 text-emerald-700 text-sm")
|
||||||
|
"Event log will appear here.")
|
||||||
|
(div
|
||||||
|
:id "ref-evt-ar-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm")
|
||||||
|
"Click to load — afterRequest fires before the swap.")))
|
||||||
18
sx/sxc/reference/ref-event-after-swap-demo.sx
Normal file
18
sx/sxc/reference/ref-event-after-swap-demo.sx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.swap-item))))"
|
||||||
|
:sx-target "#ref-evt-as-list"
|
||||||
|
:sx-swap "beforeend"
|
||||||
|
:sx-on:sx:afterSwap "var items = document.querySelectorAll('#ref-evt-as-list > div'); if (items.length) items[items.length-1].scrollIntoView({behavior:'smooth'}); document.getElementById('ref-evt-as-count').textContent = items.length + ' items'"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Add item (scrolls after swap)")
|
||||||
|
(div :id "ref-evt-as-count" (~tw :tokens "text-sm text-emerald-700") "1 items")
|
||||||
|
(div
|
||||||
|
:id "ref-evt-as-list"
|
||||||
|
(~tw :tokens "p-3 rounded border border-stone-200 space-y-1 max-h-32 overflow-y-auto")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "text-sm text-stone-500")
|
||||||
|
"Items will be appended and scrolled into view."))))
|
||||||
22
sx/sxc/reference/ref-event-before-request-demo.sx
Normal file
22
sx/sxc/reference/ref-event-before-request-demo.sx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex gap-2 items-center")
|
||||||
|
(input
|
||||||
|
:id "ref-evt-br-input"
|
||||||
|
:type "text"
|
||||||
|
:placeholder "Type something first..."
|
||||||
|
(~tw :tokens "flex-1 px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"))
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))"
|
||||||
|
:sx-target "#ref-evt-br-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-on:sx:beforeRequest "if (!document.getElementById('ref-evt-br-input').value) { event.preventDefault(); document.getElementById('ref-evt-br-result').textContent = 'Cancelled — input is empty!'; }"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Load"))
|
||||||
|
(div
|
||||||
|
:id "ref-evt-br-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm")
|
||||||
|
"Request is cancelled via preventDefault() if the input is empty.")))
|
||||||
26
sx/sxc/reference/ref-event-client-route-demo.sx
Normal file
26
sx/sxc/reference/ref-event-client-route-demo.sx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-sm text-stone-600")
|
||||||
|
"Open DevTools console, then navigate to a pure page (no :data expression). "
|
||||||
|
"You'll see \"sx:route client /path\" in the console — no network request is made.")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex gap-2 flex-wrap")
|
||||||
|
(a
|
||||||
|
:href "/sx/(etc.(essay))"
|
||||||
|
(~tw :tokens "px-3 py-1 bg-violet-100 text-violet-700 rounded text-sm no-underline hover:bg-violet-200")
|
||||||
|
"Essays")
|
||||||
|
(a
|
||||||
|
:href "/sx/(etc.(plan))"
|
||||||
|
(~tw :tokens "px-3 py-1 bg-violet-100 text-violet-700 rounded text-sm no-underline hover:bg-violet-200")
|
||||||
|
"Plans")
|
||||||
|
(a
|
||||||
|
:href "/sx/(applications.(protocol))"
|
||||||
|
(~tw :tokens "px-3 py-1 bg-violet-100 text-violet-700 rounded text-sm no-underline hover:bg-violet-200")
|
||||||
|
"Protocols"))
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-xs text-stone-400")
|
||||||
|
"The sx:clientRoute event fires on the swap target and bubbles to document.body. "
|
||||||
|
"Apps use it to update nav selection, analytics, or other post-navigation state.")))
|
||||||
20
sx/sxc/reference/ref-event-request-error-demo.sx
Normal file
20
sx/sxc/reference/ref-event-request-error-demo.sx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(button
|
||||||
|
:sx-get "https://this-domain-does-not-exist.invalid/api"
|
||||||
|
:sx-target "#ref-evt-re-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-on:sx:requestError "document.getElementById('ref-evt-re-status').style.display = 'block'; document.getElementById('ref-evt-re-status').textContent = 'Network error — request never reached a server'"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Request invalid domain")
|
||||||
|
(div
|
||||||
|
:id "ref-evt-re-status"
|
||||||
|
(~tw :tokens "p-2 rounded bg-red-50 text-red-600 text-sm")
|
||||||
|
:style "display: none"
|
||||||
|
"")
|
||||||
|
(div
|
||||||
|
:id "ref-evt-re-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm")
|
||||||
|
"Click to trigger a network error — sx:requestError fires.")))
|
||||||
20
sx/sxc/reference/ref-event-response-error-demo.sx
Normal file
20
sx/sxc/reference/ref-event-response-error-demo.sx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.error-500))))"
|
||||||
|
:sx-target "#ref-evt-err-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-on:sx:responseError "var s=document.getElementById('ref-evt-err-status'); s.style.display='block'; s.textContent='Error ' + (event.detail ? event.detail.status || '?' : '?') + ' received'"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Call failing endpoint")
|
||||||
|
(div
|
||||||
|
:id "ref-evt-err-status"
|
||||||
|
(~tw :tokens "p-2 rounded bg-red-50 text-red-600 text-sm")
|
||||||
|
:style "display: none"
|
||||||
|
"")
|
||||||
|
(div
|
||||||
|
:id "ref-evt-err-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm")
|
||||||
|
"Click to trigger an error — the sx:responseError event fires.")))
|
||||||
20
sx/sxc/reference/ref-event-sse-error-demo.sx
Normal file
20
sx/sxc/reference/ref-event-sse-error-demo.sx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
:sx-sse "/sx/(geography.(hypermedia.(reference.(api.sse-time))))"
|
||||||
|
:sx-sse-swap "time"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-on:sx:sseError "document.getElementById('ref-evt-sseerr-status').textContent = 'Disconnected'; document.getElementById('ref-evt-sseerr-status').className = 'inline-block px-2 py-0.5 rounded text-xs bg-red-100 text-red-700'"
|
||||||
|
:sx-on:sx:sseOpen "document.getElementById('ref-evt-sseerr-status').textContent = 'Connected'; document.getElementById('ref-evt-sseerr-status').className = 'inline-block px-2 py-0.5 rounded text-xs bg-emerald-100 text-emerald-700'"
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex items-center gap-3")
|
||||||
|
(span
|
||||||
|
:id "ref-evt-sseerr-status"
|
||||||
|
(~tw :tokens "inline-block px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-700")
|
||||||
|
"Connecting...")
|
||||||
|
(span (~tw :tokens "text-sm text-stone-500") "SSE stream")))
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-xs text-stone-400")
|
||||||
|
"If the SSE connection drops, the badge turns red via sx:sseError.")))
|
||||||
18
sx/sxc/reference/ref-event-sse-message-demo.sx
Normal file
18
sx/sxc/reference/ref-event-sse-message-demo.sx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
:sx-sse "/sx/(geography.(hypermedia.(reference.(api.sse-time))))"
|
||||||
|
:sx-sse-swap "time"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-on:sx:sseMessage "var c = parseInt(document.getElementById('ref-evt-ssemsg-count').dataset.count || '0') + 1; document.getElementById('ref-evt-ssemsg-count').dataset.count = c; document.getElementById('ref-evt-ssemsg-count').textContent = c + ' messages received'"
|
||||||
|
(div
|
||||||
|
:id "ref-evt-ssemsg-output"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-600 text-sm font-mono")
|
||||||
|
"Waiting for SSE messages..."))
|
||||||
|
(div
|
||||||
|
:id "ref-evt-ssemsg-count"
|
||||||
|
(~tw :tokens "text-sm text-emerald-700")
|
||||||
|
:data-count "0"
|
||||||
|
"0 messages received")))
|
||||||
19
sx/sxc/reference/ref-event-sse-open-demo.sx
Normal file
19
sx/sxc/reference/ref-event-sse-open-demo.sx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
:sx-sse "/sx/(geography.(hypermedia.(reference.(api.sse-time))))"
|
||||||
|
:sx-sse-swap "time"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-on:sx:sseOpen "document.getElementById('ref-evt-sseopen-status').textContent = 'Connected'; document.getElementById('ref-evt-sseopen-status').className = 'inline-block px-2 py-0.5 rounded text-xs bg-emerald-100 text-emerald-700'"
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex items-center gap-3")
|
||||||
|
(span
|
||||||
|
:id "ref-evt-sseopen-status"
|
||||||
|
(~tw :tokens "inline-block px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-700")
|
||||||
|
"Connecting...")
|
||||||
|
(span (~tw :tokens "text-sm text-stone-500") "SSE stream")))
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-xs text-stone-400")
|
||||||
|
"The status badge turns green when the SSE connection opens.")))
|
||||||
30
sx/sxc/reference/ref-event-validation-failed-demo.sx
Normal file
30
sx/sxc/reference/ref-event-validation-failed-demo.sx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(form
|
||||||
|
:sx-post "/sx/(geography.(hypermedia.(reference.(api.greet))))"
|
||||||
|
:sx-target "#ref-evt-vf-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-validate "true"
|
||||||
|
:sx-on:sx:validationFailed "document.getElementById('ref-evt-vf-status').style.display = 'block'"
|
||||||
|
(~tw :tokens "flex gap-2")
|
||||||
|
(input
|
||||||
|
:type "email"
|
||||||
|
:name "email"
|
||||||
|
:required "true"
|
||||||
|
:placeholder "Email (required)"
|
||||||
|
(~tw :tokens "flex-1 px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500 invalid:border-red-400"))
|
||||||
|
(button
|
||||||
|
:type "submit"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Submit"))
|
||||||
|
(div
|
||||||
|
:id "ref-evt-vf-status"
|
||||||
|
(~tw :tokens "p-2 rounded bg-amber-50 text-amber-700 text-sm")
|
||||||
|
:style "display: none"
|
||||||
|
"Validation failed — form was not submitted.")
|
||||||
|
(div
|
||||||
|
:id "ref-evt-vf-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm")
|
||||||
|
"Submit with empty/invalid email to trigger the event.")))
|
||||||
14
sx/sxc/reference/ref-get-demo.sx
Normal file
14
sx/sxc/reference/ref-get-demo.sx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))"
|
||||||
|
:sx-target "#ref-get-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Load server time")
|
||||||
|
(div
|
||||||
|
:id "ref-get-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm")
|
||||||
|
"Click to load.")))
|
||||||
15
sx/sxc/reference/ref-header-prompt-demo.sx
Normal file
15
sx/sxc/reference/ref-header-prompt-demo.sx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.prompt-echo))))"
|
||||||
|
:sx-target "#ref-hdr-prompt-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-prompt "Enter your name:"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Prompt & send")
|
||||||
|
(div
|
||||||
|
:id "ref-hdr-prompt-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm")
|
||||||
|
"Click to enter a name via prompt — the value is sent as the SX-Prompt header.")))
|
||||||
26
sx/sxc/reference/ref-header-retarget-demo.sx
Normal file
26
sx/sxc/reference/ref-header-retarget-demo.sx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.retarget))))"
|
||||||
|
:sx-target "#ref-hdr-retarget-main"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Load (server retargets)")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "grid grid-cols-2 gap-3")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "rounded border border-stone-200 p-3")
|
||||||
|
(div (~tw :tokens "text-xs text-stone-400 mb-1") "Original target")
|
||||||
|
(div
|
||||||
|
:id "ref-hdr-retarget-main"
|
||||||
|
(~tw :tokens "text-sm text-stone-500")
|
||||||
|
"Waiting..."))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "rounded border border-stone-200 p-3")
|
||||||
|
(div (~tw :tokens "text-xs text-stone-400 mb-1") "Retarget destination")
|
||||||
|
(div
|
||||||
|
:id "ref-hdr-retarget-alt"
|
||||||
|
(~tw :tokens "text-sm text-stone-500")
|
||||||
|
"Waiting...")))))
|
||||||
15
sx/sxc/reference/ref-header-trigger-demo.sx
Normal file
15
sx/sxc/reference/ref-header-trigger-demo.sx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.trigger-event))))"
|
||||||
|
:sx-target "#ref-hdr-trigger-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Load with trigger")
|
||||||
|
(div
|
||||||
|
:id "ref-hdr-trigger-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm")
|
||||||
|
:sx-on:showNotice "this.style.borderColor = '#8b5cf6'; this.style.borderWidth = '2px'"
|
||||||
|
"Click — the server response includes SX-Trigger: showNotice, which highlights this box.")))
|
||||||
15
sx/sxc/reference/ref-headers-demo.sx
Normal file
15
sx/sxc/reference/ref-headers-demo.sx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.echo-headers))))"
|
||||||
|
:sx-headers {:X-Request-Source "demo" :X-Custom-Token "abc123"}
|
||||||
|
:sx-target "#ref-headers-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700")
|
||||||
|
"Send with custom headers")
|
||||||
|
(div
|
||||||
|
:id "ref-headers-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm")
|
||||||
|
"Click to see echoed headers.")))
|
||||||
26
sx/sxc/reference/ref-ignore-demo.sx
Normal file
26
sx/sxc/reference/ref-ignore-demo.sx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))"
|
||||||
|
:sx-target "#ref-ignore-container"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Swap container")
|
||||||
|
(div
|
||||||
|
:id "ref-ignore-container"
|
||||||
|
(~tw :tokens "space-y-2")
|
||||||
|
(div
|
||||||
|
:sx-ignore "true"
|
||||||
|
(~tw :tokens "p-2 bg-amber-50 rounded border border-amber-200")
|
||||||
|
(p
|
||||||
|
(~tw :tokens "text-sm text-amber-800")
|
||||||
|
"This subtree has sx-ignore — it won't change.")
|
||||||
|
(input
|
||||||
|
:type "text"
|
||||||
|
:placeholder "Type here — ignored during swap"
|
||||||
|
(~tw :tokens "mt-1 w-full px-2 py-1 border border-amber-300 rounded text-sm")))
|
||||||
|
(div
|
||||||
|
(~tw :tokens "p-2 bg-stone-100 rounded text-sm text-stone-600")
|
||||||
|
"This text WILL be replaced on swap."))))
|
||||||
26
sx/sxc/reference/ref-include-demo.sx
Normal file
26
sx/sxc/reference/ref-include-demo.sx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex gap-2 items-end")
|
||||||
|
(div
|
||||||
|
(label (~tw :tokens "block text-xs text-stone-500 mb-1") "Category")
|
||||||
|
(select
|
||||||
|
:id "ref-inc-cat"
|
||||||
|
:name "category"
|
||||||
|
(~tw :tokens "px-3 py-2 border border-stone-300 rounded text-sm")
|
||||||
|
(option :value "all" "All")
|
||||||
|
(option :value "books" "Books")
|
||||||
|
(option :value "tools" "Tools")))
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.echo-vals))))"
|
||||||
|
:sx-include "#ref-inc-cat"
|
||||||
|
:sx-target "#ref-include-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700")
|
||||||
|
"Filter"))
|
||||||
|
(div
|
||||||
|
:id "ref-include-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm")
|
||||||
|
"Click Filter — the select value is included in the request.")))
|
||||||
23
sx/sxc/reference/ref-indicator-demo.sx
Normal file
23
sx/sxc/reference/ref-indicator-demo.sx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
(defcomp
|
||||||
|
()
|
||||||
|
(div
|
||||||
|
(~tw :tokens "space-y-3")
|
||||||
|
(div
|
||||||
|
(~tw :tokens "flex gap-3 items-center")
|
||||||
|
(button
|
||||||
|
:sx-get "/sx/(geography.(hypermedia.(reference.(api.slow-echo))))"
|
||||||
|
:sx-target "#ref-indicator-result"
|
||||||
|
:sx-swap "innerHTML"
|
||||||
|
:sx-indicator "#ref-spinner"
|
||||||
|
:sx-vals "{\"q\": \"hello\"}"
|
||||||
|
(~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm")
|
||||||
|
"Load (slow)")
|
||||||
|
(span
|
||||||
|
:id "ref-spinner"
|
||||||
|
(~tw :tokens "text-violet-600 text-sm")
|
||||||
|
:style "display: none"
|
||||||
|
"Loading..."))
|
||||||
|
(div
|
||||||
|
:id "ref-indicator-result"
|
||||||
|
(~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm")
|
||||||
|
"Click to load (indicator shows during request).")))
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user