diff --git a/sx/sx/testing/index-runner.sx b/sx/sx/testing/index-runner.sx new file mode 100644 index 00000000..92777082 --- /dev/null +++ b/sx/sx/testing/index-runner.sx @@ -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
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")))) diff --git a/sx/sx/testing/page-runner.sx b/sx/sx/testing/page-runner.sx new file mode 100644 index 00000000..609381f3 --- /dev/null +++ b/sx/sx/testing/page-runner.sx @@ -0,0 +1,317 @@ +;; Generic page test runner. +;; Tests loaded from /_test/*.sx, rendered with status icons. +;; Run All island drives the iframe via DOM — actions stored on each +;;
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"))) diff --git a/sx/sxc/docs/attr-row.sx b/sx/sxc/docs/attr-row.sx new file mode 100644 index 00000000..c9e06aff --- /dev/null +++ b/sx/sxc/docs/attr-row.sx @@ -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"))))) diff --git a/sx/sxc/docs/code.sx b/sx/sxc/docs/code.sx new file mode 100644 index 00000000..5bdedca8 --- /dev/null +++ b/sx/sxc/docs/code.sx @@ -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))) diff --git a/sx/sxc/docs/nav.sx b/sx/sxc/docs/nav.sx new file mode 100644 index 00000000..f50257fa --- /dev/null +++ b/sx/sxc/docs/nav.sx @@ -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))) diff --git a/sx/sxc/docs/note.sx b/sx/sxc/docs/note.sx new file mode 100644 index 00000000..24f2f604 --- /dev/null +++ b/sx/sxc/docs/note.sx @@ -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)) diff --git a/sx/sxc/docs/page.sx b/sx/sxc/docs/page.sx new file mode 100644 index 00000000..bcb116fd --- /dev/null +++ b/sx/sxc/docs/page.sx @@ -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))) diff --git a/sx/sxc/docs/primitives-table.sx b/sx/sxc/docs/primitives-table.sx new file mode 100644 index 00000000..eb4bb2f0 --- /dev/null +++ b/sx/sxc/docs/primitives-table.sx @@ -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)))) diff --git a/sx/sxc/docs/section.sx b/sx/sxc/docs/section.sx new file mode 100644 index 00000000..45d587b7 --- /dev/null +++ b/sx/sxc/docs/section.sx @@ -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)) diff --git a/sx/sxc/docs/subsection.sx b/sx/sxc/docs/subsection.sx new file mode 100644 index 00000000..94052bdc --- /dev/null +++ b/sx/sxc/docs/subsection.sx @@ -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)) diff --git a/sx/sxc/docs/table.sx b/sx/sxc/docs/table.sx new file mode 100644 index 00000000..a68d46d7 --- /dev/null +++ b/sx/sxc/docs/table.sx @@ -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))))) diff --git a/sx/sxc/examples/active-search-demo.sx b/sx/sxc/examples/active-search-demo.sx new file mode 100644 index 00000000..abf5f062 --- /dev/null +++ b/sx/sxc/examples/active-search-demo.sx @@ -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...")))) diff --git a/sx/sxc/examples/anim-result.sx b/sx/sxc/examples/anim-result.sx new file mode 100644 index 00000000..728f02a0 --- /dev/null +++ b/sx/sxc/examples/anim-result.sx @@ -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))))) diff --git a/sx/sxc/examples/animations-demo.sx b/sx/sxc/examples/animations-demo.sx new file mode 100644 index 00000000..9027e9c9 --- /dev/null +++ b/sx/sxc/examples/animations-demo.sx @@ -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.")))) diff --git a/sx/sxc/examples/bulk-row.sx b/sx/sxc/examples/bulk-row.sx new file mode 100644 index 00000000..e2e0db1a --- /dev/null +++ b/sx/sxc/examples/bulk-row.sx @@ -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)))) diff --git a/sx/sxc/examples/bulk-update-demo.sx b/sx/sxc/examples/bulk-update-demo.sx new file mode 100644 index 00000000..7752aac6 --- /dev/null +++ b/sx/sxc/examples/bulk-update-demo.sx @@ -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)))))) diff --git a/sx/sxc/examples/card.sx b/sx/sxc/examples/card.sx new file mode 100644 index 00000000..d85f5935 --- /dev/null +++ b/sx/sxc/examples/card.sx @@ -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))) diff --git a/sx/sxc/examples/click-result.sx b/sx/sxc/examples/click-result.sx new file mode 100644 index 00000000..bfa2533a --- /dev/null +++ b/sx/sxc/examples/click-result.sx @@ -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)))) diff --git a/sx/sxc/examples/click-to-load-demo.sx b/sx/sxc/examples/click-to-load-demo.sx new file mode 100644 index 00000000..469dca6f --- /dev/null +++ b/sx/sxc/examples/click-to-load-demo.sx @@ -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"))) diff --git a/sx/sxc/examples/delete-demo.sx b/sx/sxc/examples/delete-demo.sx new file mode 100644 index 00000000..209db829 --- /dev/null +++ b/sx/sxc/examples/delete-demo.sx @@ -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))))) diff --git a/sx/sxc/examples/delete-row.sx b/sx/sxc/examples/delete-row.sx new file mode 100644 index 00000000..7d59f06c --- /dev/null +++ b/sx/sxc/examples/delete-row.sx @@ -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")))) diff --git a/sx/sxc/examples/demo.sx b/sx/sxc/examples/demo.sx new file mode 100644 index 00000000..8dea6bea --- /dev/null +++ b/sx/sxc/examples/demo.sx @@ -0,0 +1,5 @@ +(defcomp + (&key &rest children) + (div + (~tw :tokens "border border-dashed border-stone-300 rounded p-4 bg-stone-100") + children)) diff --git a/sx/sxc/examples/dialog-modal.sx b/sx/sxc/examples/dialog-modal.sx new file mode 100644 index 00000000..c1c635d3 --- /dev/null +++ b/sx/sxc/examples/dialog-modal.sx @@ -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"))))) diff --git a/sx/sxc/examples/dialogs-demo.sx b/sx/sxc/examples/dialogs-demo.sx new file mode 100644 index 00000000..f734a7c3 --- /dev/null +++ b/sx/sxc/examples/dialogs-demo.sx @@ -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"))) diff --git a/sx/sxc/examples/echo-result.sx b/sx/sxc/examples/echo-result.sx new file mode 100644 index 00000000..4d07e244 --- /dev/null +++ b/sx/sxc/examples/echo-result.sx @@ -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))) diff --git a/sx/sxc/examples/edit-row-demo.sx b/sx/sxc/examples/edit-row-demo.sx new file mode 100644 index 00000000..2ed41f18 --- /dev/null +++ b/sx/sxc/examples/edit-row-demo.sx @@ -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))))) diff --git a/sx/sxc/examples/edit-row-form.sx b/sx/sxc/examples/edit-row-form.sx new file mode 100644 index 00000000..0d35ac7b --- /dev/null +++ b/sx/sxc/examples/edit-row-form.sx @@ -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")))) diff --git a/sx/sxc/examples/edit-row-view.sx b/sx/sxc/examples/edit-row-view.sx new file mode 100644 index 00000000..9658fdb8 --- /dev/null +++ b/sx/sxc/examples/edit-row-view.sx @@ -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")))) diff --git a/sx/sxc/examples/form-demo.sx b/sx/sxc/examples/form-demo.sx new file mode 100644 index 00000000..d921444e --- /dev/null +++ b/sx/sxc/examples/form-demo.sx @@ -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."))) diff --git a/sx/sxc/examples/form-result.sx b/sx/sxc/examples/form-result.sx new file mode 100644 index 00000000..387c45d3 --- /dev/null +++ b/sx/sxc/examples/form-result.sx @@ -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."))) diff --git a/sx/sxc/examples/infinite-scroll-demo.sx b/sx/sxc/examples/infinite-scroll-demo.sx new file mode 100644 index 00000000..fcc6c4b2 --- /dev/null +++ b/sx/sxc/examples/infinite-scroll-demo.sx @@ -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...")))) diff --git a/sx/sxc/examples/inline-edit-demo.sx b/sx/sxc/examples/inline-edit-demo.sx new file mode 100644 index 00000000..6ba9d4ee --- /dev/null +++ b/sx/sxc/examples/inline-edit-demo.sx @@ -0,0 +1,6 @@ +(defcomp + () + (div + :id "edit-target" + (~tw :tokens "space-y-3") + (~examples/inline-view :value "Click edit to change this text"))) diff --git a/sx/sxc/examples/inline-edit-form.sx b/sx/sxc/examples/inline-edit-form.sx new file mode 100644 index 00000000..f2483809 --- /dev/null +++ b/sx/sxc/examples/inline-edit-form.sx @@ -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"))) diff --git a/sx/sxc/examples/inline-validation-demo.sx b/sx/sxc/examples/inline-validation-demo.sx new file mode 100644 index 00000000..2ea762c9 --- /dev/null +++ b/sx/sxc/examples/inline-validation-demo.sx @@ -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"))) diff --git a/sx/sxc/examples/inline-view.sx b/sx/sxc/examples/inline-view.sx new file mode 100644 index 00000000..fb3a0399 --- /dev/null +++ b/sx/sxc/examples/inline-view.sx @@ -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"))) diff --git a/sx/sxc/examples/json-encoding-demo.sx b/sx/sxc/examples/json-encoding-demo.sx new file mode 100644 index 00000000..cfbfcc3a --- /dev/null +++ b/sx/sxc/examples/json-encoding-demo.sx @@ -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."))) diff --git a/sx/sxc/examples/json-result.sx b/sx/sxc/examples/json-result.sx new file mode 100644 index 00000000..1980bcfa --- /dev/null +++ b/sx/sxc/examples/json-result.sx @@ -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)))) diff --git a/sx/sxc/examples/kbd-result.sx b/sx/sxc/examples/kbd-result.sx new file mode 100644 index 00000000..91510b87 --- /dev/null +++ b/sx/sxc/examples/kbd-result.sx @@ -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 "'")))) diff --git a/sx/sxc/examples/keyboard-shortcuts-demo.sx b/sx/sxc/examples/keyboard-shortcuts-demo.sx new file mode 100644 index 00000000..582ca76f --- /dev/null +++ b/sx/sxc/examples/keyboard-shortcuts-demo.sx @@ -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"))) diff --git a/sx/sxc/examples/lazy-loading-demo.sx b/sx/sxc/examples/lazy-loading-demo.sx new file mode 100644 index 00000000..65cd0456 --- /dev/null +++ b/sx/sxc/examples/lazy-loading-demo.sx @@ -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")))))) diff --git a/sx/sxc/examples/lazy-result.sx b/sx/sxc/examples/lazy-result.sx new file mode 100644 index 00000000..2f4ea2c2 --- /dev/null +++ b/sx/sxc/examples/lazy-result.sx @@ -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)))) diff --git a/sx/sxc/examples/loading-result.sx b/sx/sxc/examples/loading-result.sx new file mode 100644 index 00000000..e49d1d05 --- /dev/null +++ b/sx/sxc/examples/loading-result.sx @@ -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)))) diff --git a/sx/sxc/examples/loading-states-demo.sx b/sx/sxc/examples/loading-states-demo.sx new file mode 100644 index 00000000..ed71ee04 --- /dev/null +++ b/sx/sxc/examples/loading-states-demo.sx @@ -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.")))) diff --git a/sx/sxc/examples/oob-demo.sx b/sx/sxc/examples/oob-demo.sx new file mode 100644 index 00000000..8643ebac --- /dev/null +++ b/sx/sxc/examples/oob-demo.sx @@ -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"))) diff --git a/sx/sxc/examples/poll-result.sx b/sx/sxc/examples/poll-result.sx new file mode 100644 index 00000000..6fbdcabf --- /dev/null +++ b/sx/sxc/examples/poll-result.sx @@ -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)))))) diff --git a/sx/sxc/examples/polling-demo.sx b/sx/sxc/examples/polling-demo.sx new file mode 100644 index 00000000..dc4a4590 --- /dev/null +++ b/sx/sxc/examples/polling-demo.sx @@ -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..."))) diff --git a/sx/sxc/examples/pp-form-full.sx b/sx/sxc/examples/pp-form-full.sx new file mode 100644 index 00000000..b7b7785a --- /dev/null +++ b/sx/sxc/examples/pp-form-full.sx @@ -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")))) diff --git a/sx/sxc/examples/pp-view.sx b/sx/sxc/examples/pp-view.sx new file mode 100644 index 00000000..2cee638f --- /dev/null +++ b/sx/sxc/examples/pp-view.sx @@ -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)")))) diff --git a/sx/sxc/examples/progress-bar-demo.sx b/sx/sxc/examples/progress-bar-demo.sx new file mode 100644 index 00000000..a30b5bde --- /dev/null +++ b/sx/sxc/examples/progress-bar-demo.sx @@ -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"))) diff --git a/sx/sxc/examples/progress-status.sx b/sx/sxc/examples/progress-status.sx new file mode 100644 index 00000000..fa35676f --- /dev/null +++ b/sx/sxc/examples/progress-status.sx @@ -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!")))) diff --git a/sx/sxc/examples/put-patch-demo.sx b/sx/sxc/examples/put-patch-demo.sx new file mode 100644 index 00000000..e0e96046 --- /dev/null +++ b/sx/sxc/examples/put-patch-demo.sx @@ -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))) diff --git a/sx/sxc/examples/reset-message.sx b/sx/sxc/examples/reset-message.sx new file mode 100644 index 00000000..092ae0c9 --- /dev/null +++ b/sx/sxc/examples/reset-message.sx @@ -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))) diff --git a/sx/sxc/examples/reset-on-submit-demo.sx b/sx/sxc/examples/reset-on-submit-demo.sx new file mode 100644 index 00000000..41316322 --- /dev/null +++ b/sx/sxc/examples/reset-on-submit-demo.sx @@ -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.")))) diff --git a/sx/sxc/examples/retry-demo.sx b/sx/sxc/examples/retry-demo.sx new file mode 100644 index 00000000..304262a1 --- /dev/null +++ b/sx/sxc/examples/retry-demo.sx @@ -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.")))) diff --git a/sx/sxc/examples/retry-result.sx b/sx/sxc/examples/retry-result.sx new file mode 100644 index 00000000..0f82643b --- /dev/null +++ b/sx/sxc/examples/retry-result.sx @@ -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)))) diff --git a/sx/sxc/examples/scroll-items.sx b/sx/sxc/examples/scroll-items.sx new file mode 100644 index 00000000..676d8352 --- /dev/null +++ b/sx/sxc/examples/scroll-items.sx @@ -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...")))) diff --git a/sx/sxc/examples/search-results.sx b/sx/sxc/examples/search-results.sx new file mode 100644 index 00000000..5ada34b2 --- /dev/null +++ b/sx/sxc/examples/search-results.sx @@ -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)))) diff --git a/sx/sxc/examples/select-filter-demo.sx b/sx/sxc/examples/select-filter-demo.sx new file mode 100644 index 00000000..933911ca --- /dev/null +++ b/sx/sxc/examples/select-filter-demo.sx @@ -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.")))) diff --git a/sx/sxc/examples/source.sx b/sx/sxc/examples/source.sx new file mode 100644 index 00000000..9e60467b --- /dev/null +++ b/sx/sxc/examples/source.sx @@ -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)))) diff --git a/sx/sxc/examples/swap-entry.sx b/sx/sxc/examples/swap-entry.sx new file mode 100644 index 00000000..02d8443e --- /dev/null +++ b/sx/sxc/examples/swap-entry.sx @@ -0,0 +1,3 @@ +(defcomp + (&key time mode) + (div (~tw :tokens "px-3 py-2 text-sm text-stone-700") (str "[" time "] " mode))) diff --git a/sx/sxc/examples/swap-positions-demo.sx b/sx/sxc/examples/swap-positions-demo.sx new file mode 100644 index 00000000..b67bb606 --- /dev/null +++ b/sx/sxc/examples/swap-positions-demo.sx @@ -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.")))) diff --git a/sx/sxc/examples/sync-replace-demo.sx b/sx/sxc/examples/sync-replace-demo.sx new file mode 100644 index 00000000..c4e5fd41 --- /dev/null +++ b/sx/sxc/examples/sync-replace-demo.sx @@ -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.")))) diff --git a/sx/sxc/examples/sync-result.sx b/sx/sxc/examples/sync-result.sx new file mode 100644 index 00000000..aae38988 --- /dev/null +++ b/sx/sxc/examples/sync-result.sx @@ -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")))) diff --git a/sx/sxc/examples/tab-btn.sx b/sx/sxc/examples/tab-btn.sx new file mode 100644 index 00000000..d21a19e4 --- /dev/null +++ b/sx/sxc/examples/tab-btn.sx @@ -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)) diff --git a/sx/sxc/examples/tabs-demo.sx b/sx/sxc/examples/tabs-demo.sx new file mode 100644 index 00000000..16c394d0 --- /dev/null +++ b/sx/sxc/examples/tabs-demo.sx @@ -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.")))) diff --git a/sx/sxc/examples/validation-error.sx b/sx/sxc/examples/validation-error.sx new file mode 100644 index 00000000..43200914 --- /dev/null +++ b/sx/sxc/examples/validation-error.sx @@ -0,0 +1,3 @@ +(defcomp + (&key message) + (p (~tw :tokens "text-sm text-rose-600") message)) diff --git a/sx/sxc/examples/validation-ok.sx b/sx/sxc/examples/validation-ok.sx new file mode 100644 index 00000000..fb398147 --- /dev/null +++ b/sx/sxc/examples/validation-ok.sx @@ -0,0 +1,3 @@ +(defcomp + (&key email) + (p (~tw :tokens "text-sm text-emerald-600") (str email " is available"))) diff --git a/sx/sxc/examples/vals-headers-demo.sx b/sx/sxc/examples/vals-headers-demo.sx new file mode 100644 index 00000000..a6e6b09c --- /dev/null +++ b/sx/sxc/examples/vals-headers-demo.sx @@ -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.")))) diff --git a/sx/sxc/examples/value-options.sx b/sx/sxc/examples/value-options.sx new file mode 100644 index 00000000..0d2b0652 --- /dev/null +++ b/sx/sxc/examples/value-options.sx @@ -0,0 +1,3 @@ +(defcomp + (&key items) + (<> (map (fn (item) (option :value item item)) items))) diff --git a/sx/sxc/examples/value-select-demo.sx b/sx/sxc/examples/value-select-demo.sx new file mode 100644 index 00000000..9a076b15 --- /dev/null +++ b/sx/sxc/examples/value-select-demo.sx @@ -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..."))))) diff --git a/sx/sxc/home/credits.sx b/sx/sxc/home/credits.sx new file mode 100644 index 00000000..7780fe95 --- /dev/null +++ b/sx/sxc/home/credits.sx @@ -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."))) diff --git a/sx/sxc/home/hero.sx b/sx/sxc/home/hero.sx new file mode 100644 index 00000000..e0defcd5 --- /dev/null +++ b/sx/sxc/home/hero.sx @@ -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") "()")) + (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)))) diff --git a/sx/sxc/home/how-it-works.sx b/sx/sxc/home/how-it-works.sx new file mode 100644 index 00000000..e49d926a --- /dev/null +++ b/sx/sxc/home/how-it-works.sx @@ -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).")))))) diff --git a/sx/sxc/home/philosophy.sx b/sx/sxc/home/philosophy.sx new file mode 100644 index 00000000..f61be76e --- /dev/null +++ b/sx/sxc/home/philosophy.sx @@ -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")))))) diff --git a/sx/sxc/reference/ref-boost-demo.sx b/sx/sxc/reference/ref-boost-demo.sx new file mode 100644 index 00000000..821c3482 --- /dev/null +++ b/sx/sxc/reference/ref-boost-demo.sx @@ -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"))) diff --git a/sx/sxc/reference/ref-confirm-demo.sx b/sx/sxc/reference/ref-confirm-demo.sx new file mode 100644 index 00000000..25780e6c --- /dev/null +++ b/sx/sxc/reference/ref-confirm-demo.sx @@ -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")))) diff --git a/sx/sxc/reference/ref-data-sx-demo.sx b/sx/sxc/reference/ref-data-sx-demo.sx new file mode 100644 index 00000000..3e6c9983 --- /dev/null +++ b/sx/sxc/reference/ref-data-sx-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-data-sx-env-demo.sx b/sx/sxc/reference/ref-data-sx-env-demo.sx new file mode 100644 index 00000000..237617fc --- /dev/null +++ b/sx/sxc/reference/ref-data-sx-env-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-delete-demo.sx b/sx/sxc/reference/ref-delete-demo.sx new file mode 100644 index 00000000..291e230c --- /dev/null +++ b/sx/sxc/reference/ref-delete-demo.sx @@ -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")))) diff --git a/sx/sxc/reference/ref-disable-demo.sx b/sx/sxc/reference/ref-disable-demo.sx new file mode 100644 index 00000000..31006091 --- /dev/null +++ b/sx/sxc/reference/ref-disable-demo.sx @@ -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"))))) diff --git a/sx/sxc/reference/ref-disabled-elt-demo.sx b/sx/sxc/reference/ref-disabled-elt-demo.sx new file mode 100644 index 00000000..3a4bca43 --- /dev/null +++ b/sx/sxc/reference/ref-disabled-elt-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-encoding-demo.sx b/sx/sxc/reference/ref-encoding-demo.sx new file mode 100644 index 00000000..e4e0e8b2 --- /dev/null +++ b/sx/sxc/reference/ref-encoding-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-event-after-request-demo.sx b/sx/sxc/reference/ref-event-after-request-demo.sx new file mode 100644 index 00000000..f8941829 --- /dev/null +++ b/sx/sxc/reference/ref-event-after-request-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-event-after-swap-demo.sx b/sx/sxc/reference/ref-event-after-swap-demo.sx new file mode 100644 index 00000000..0da19c7d --- /dev/null +++ b/sx/sxc/reference/ref-event-after-swap-demo.sx @@ -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.")))) diff --git a/sx/sxc/reference/ref-event-before-request-demo.sx b/sx/sxc/reference/ref-event-before-request-demo.sx new file mode 100644 index 00000000..db383d02 --- /dev/null +++ b/sx/sxc/reference/ref-event-before-request-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-event-client-route-demo.sx b/sx/sxc/reference/ref-event-client-route-demo.sx new file mode 100644 index 00000000..ddb07dc8 --- /dev/null +++ b/sx/sxc/reference/ref-event-client-route-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-event-request-error-demo.sx b/sx/sxc/reference/ref-event-request-error-demo.sx new file mode 100644 index 00000000..1a7a9c3f --- /dev/null +++ b/sx/sxc/reference/ref-event-request-error-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-event-response-error-demo.sx b/sx/sxc/reference/ref-event-response-error-demo.sx new file mode 100644 index 00000000..4bc0149a --- /dev/null +++ b/sx/sxc/reference/ref-event-response-error-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-event-sse-error-demo.sx b/sx/sxc/reference/ref-event-sse-error-demo.sx new file mode 100644 index 00000000..382517ae --- /dev/null +++ b/sx/sxc/reference/ref-event-sse-error-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-event-sse-message-demo.sx b/sx/sxc/reference/ref-event-sse-message-demo.sx new file mode 100644 index 00000000..77b51edc --- /dev/null +++ b/sx/sxc/reference/ref-event-sse-message-demo.sx @@ -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"))) diff --git a/sx/sxc/reference/ref-event-sse-open-demo.sx b/sx/sxc/reference/ref-event-sse-open-demo.sx new file mode 100644 index 00000000..0ed548e1 --- /dev/null +++ b/sx/sxc/reference/ref-event-sse-open-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-event-validation-failed-demo.sx b/sx/sxc/reference/ref-event-validation-failed-demo.sx new file mode 100644 index 00000000..f7ce5eaa --- /dev/null +++ b/sx/sxc/reference/ref-event-validation-failed-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-get-demo.sx b/sx/sxc/reference/ref-get-demo.sx new file mode 100644 index 00000000..9510b72a --- /dev/null +++ b/sx/sxc/reference/ref-get-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-header-prompt-demo.sx b/sx/sxc/reference/ref-header-prompt-demo.sx new file mode 100644 index 00000000..5848beac --- /dev/null +++ b/sx/sxc/reference/ref-header-prompt-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-header-retarget-demo.sx b/sx/sxc/reference/ref-header-retarget-demo.sx new file mode 100644 index 00000000..c3b8f552 --- /dev/null +++ b/sx/sxc/reference/ref-header-retarget-demo.sx @@ -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..."))))) diff --git a/sx/sxc/reference/ref-header-trigger-demo.sx b/sx/sxc/reference/ref-header-trigger-demo.sx new file mode 100644 index 00000000..3ba97c7c --- /dev/null +++ b/sx/sxc/reference/ref-header-trigger-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-headers-demo.sx b/sx/sxc/reference/ref-headers-demo.sx new file mode 100644 index 00000000..4bc94783 --- /dev/null +++ b/sx/sxc/reference/ref-headers-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-ignore-demo.sx b/sx/sxc/reference/ref-ignore-demo.sx new file mode 100644 index 00000000..a5c4dc6f --- /dev/null +++ b/sx/sxc/reference/ref-ignore-demo.sx @@ -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.")))) diff --git a/sx/sxc/reference/ref-include-demo.sx b/sx/sxc/reference/ref-include-demo.sx new file mode 100644 index 00000000..435fd409 --- /dev/null +++ b/sx/sxc/reference/ref-include-demo.sx @@ -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."))) diff --git a/sx/sxc/reference/ref-indicator-demo.sx b/sx/sxc/reference/ref-indicator-demo.sx new file mode 100644 index 00000000..2bf23959 --- /dev/null +++ b/sx/sxc/reference/ref-indicator-demo.sx @@ -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)."))) diff --git a/sx/sxc/reference/ref-media-demo.sx b/sx/sxc/reference/ref-media-demo.sx new file mode 100644 index 00000000..087450b1 --- /dev/null +++ b/sx/sxc/reference/ref-media-demo.sx @@ -0,0 +1,17 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (a + :href "/sx/(geography.(hypermedia.(reference-detail.attributes.sx-get)))" + :sx-get "/sx/(geography.(hypermedia.(reference-detail.attributes.sx-get)))" + :sx-target "#sx-content" + :sx-select "#sx-content" + :sx-swap "outerHTML" + :sx-push-url "true" + :sx-media "(min-width: 768px)" + (~tw :tokens "inline-block px-4 py-2 bg-violet-600 text-white rounded text-sm no-underline hover:bg-violet-700") + "sx navigation (desktop only)") + (p + (~tw :tokens "text-sm text-stone-500") + "On screens narrower than 768px this link uses normal navigation. On wider screens it uses sx."))) diff --git a/sx/sxc/reference/ref-on-demo.sx b/sx/sxc/reference/ref-on-demo.sx new file mode 100644 index 00000000..5658a1b1 --- /dev/null +++ b/sx/sxc/reference/ref-on-demo.sx @@ -0,0 +1,12 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (button + :sx-on:click "document.getElementById('ref-on-result').textContent = 'Clicked at ' + new Date().toLocaleTimeString()" + (~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700") + "Click me") + (div + :id "ref-on-result" + (~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm") + "Click the button — runs JavaScript, no server request."))) diff --git a/sx/sxc/reference/ref-oob-demo.sx b/sx/sxc/reference/ref-oob-demo.sx new file mode 100644 index 00000000..7c742960 --- /dev/null +++ b/sx/sxc/reference/ref-oob-demo.sx @@ -0,0 +1,20 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (button + :sx-get "/sx/(geography.(hypermedia.(reference.(api.oob))))" + :sx-target "#ref-oob-main" + :sx-swap "innerHTML" + (~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700") + "Update both boxes") + (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") "Main target") + (div :id "ref-oob-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") "OOB target") + (div :id "ref-oob-side" (~tw :tokens "text-sm text-stone-500") "Waiting..."))))) diff --git a/sx/sxc/reference/ref-optimistic-demo.sx b/sx/sxc/reference/ref-optimistic-demo.sx new file mode 100644 index 00000000..d74b0ffb --- /dev/null +++ b/sx/sxc/reference/ref-optimistic-demo.sx @@ -0,0 +1,29 @@ +(defcomp + () + (div + (~tw :tokens "space-y-2") + (div + :id "ref-opt-item-1" + (~tw :tokens "flex items-center justify-between p-2 border border-stone-200 rounded") + (span (~tw :tokens "text-sm text-stone-700") "Optimistic item A") + (button + :sx-delete "/sx/(geography.(hypermedia.(reference.(api.(item.opt1)))))" + :sx-target "#ref-opt-item-1" + :sx-swap "delete" + :sx-optimistic "remove" + (~tw :tokens "text-red-500 text-sm hover:text-red-700") + "Remove")) + (div + :id "ref-opt-item-2" + (~tw :tokens "flex items-center justify-between p-2 border border-stone-200 rounded") + (span (~tw :tokens "text-sm text-stone-700") "Optimistic item B") + (button + :sx-delete "/sx/(geography.(hypermedia.(reference.(api.(item.opt2)))))" + :sx-target "#ref-opt-item-2" + :sx-swap "delete" + :sx-optimistic "remove" + (~tw :tokens "text-red-500 text-sm hover:text-red-700") + "Remove")) + (p + (~tw :tokens "text-xs text-stone-400") + "Items fade out immediately on click (optimistic), then are removed when the server responds."))) diff --git a/sx/sxc/reference/ref-params-demo.sx b/sx/sxc/reference/ref-params-demo.sx new file mode 100644 index 00000000..3fa7db94 --- /dev/null +++ b/sx/sxc/reference/ref-params-demo.sx @@ -0,0 +1,28 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (form + :sx-post "/sx/(geography.(hypermedia.(reference.(api.echo-vals))))" + :sx-target "#ref-params-result" + :sx-swap "innerHTML" + :sx-params "name" + (~tw :tokens "flex gap-2") + (input + :type "text" + :name "name" + :placeholder "Name (sent)" + (~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")) + (input + :type "text" + :name "secret" + :placeholder "Secret (filtered)" + (~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") + "Submit")) + (div + :id "ref-params-result" + (~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm") + "Only 'name' will be sent — 'secret' is filtered by sx-params."))) diff --git a/sx/sxc/reference/ref-patch-demo.sx b/sx/sxc/reference/ref-patch-demo.sx new file mode 100644 index 00000000..7115f57f --- /dev/null +++ b/sx/sxc/reference/ref-patch-demo.sx @@ -0,0 +1,27 @@ +(defcomp + () + (div + :id "ref-patch-view" + (~tw :tokens "space-y-2") + (div + (~tw :tokens "p-3 bg-stone-100 rounded") + (span + (~tw :tokens "text-stone-700 text-sm") + "Theme: " + (strong :id "ref-patch-val" "light"))) + (div + (~tw :tokens "flex gap-2") + (button + :sx-patch "/sx/(geography.(hypermedia.(reference.(api.theme))))" + :sx-vals "{\"theme\": \"dark\"}" + :sx-target "#ref-patch-val" + :sx-swap "innerHTML" + (~tw :tokens "px-3 py-1 bg-stone-800 text-white rounded text-sm") + "Dark") + (button + :sx-patch "/sx/(geography.(hypermedia.(reference.(api.theme))))" + :sx-vals "{\"theme\": \"light\"}" + :sx-target "#ref-patch-val" + :sx-swap "innerHTML" + (~tw :tokens "px-3 py-1 bg-stone-100 border border-stone-300 text-stone-700 rounded text-sm") + "Light")))) diff --git a/sx/sxc/reference/ref-post-demo.sx b/sx/sxc/reference/ref-post-demo.sx new file mode 100644 index 00000000..2809a3e3 --- /dev/null +++ b/sx/sxc/reference/ref-post-demo.sx @@ -0,0 +1,22 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (form + :sx-post "/sx/(geography.(hypermedia.(reference.(api.greet))))" + :sx-target "#ref-post-result" + :sx-swap "innerHTML" + (~tw :tokens "flex gap-2") + (input + :type "text" + :name "name" + :placeholder "Your name" + (~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") + "Greet")) + (div + :id "ref-post-result" + (~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm") + "Submit to see greeting."))) diff --git a/sx/sxc/reference/ref-preload-demo.sx b/sx/sxc/reference/ref-preload-demo.sx new file mode 100644 index 00000000..e5aa5320 --- /dev/null +++ b/sx/sxc/reference/ref-preload-demo.sx @@ -0,0 +1,15 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (button + :sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))" + :sx-target "#ref-preload-result" + :sx-swap "innerHTML" + :sx-preload "mouseover" + (~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm") + "Hover then click (preloaded)") + (div + :id "ref-preload-result" + (~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm") + "Hover over the button first, then click — the response is instant."))) diff --git a/sx/sxc/reference/ref-preserve-demo.sx b/sx/sxc/reference/ref-preserve-demo.sx new file mode 100644 index 00000000..6c2942cd --- /dev/null +++ b/sx/sxc/reference/ref-preserve-demo.sx @@ -0,0 +1,27 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (div + (~tw :tokens "flex gap-2 items-center") + (button + :sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))" + :sx-target "#ref-preserve-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") + (span + (~tw :tokens "text-xs text-stone-400") + "The input below keeps its value across swaps.")) + (div + :id "ref-preserve-container" + (~tw :tokens "space-y-2") + (input + :id "ref-preserved-input" + :sx-preserve "true" + :type "text" + :placeholder "Type here — preserved across swaps" + (~tw :tokens "w-full px-3 py-2 border border-stone-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.")))) diff --git a/sx/sxc/reference/ref-prompt-demo.sx b/sx/sxc/reference/ref-prompt-demo.sx new file mode 100644 index 00000000..0098cdbb --- /dev/null +++ b/sx/sxc/reference/ref-prompt-demo.sx @@ -0,0 +1,15 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (button + :sx-get "/sx/(geography.(hypermedia.(reference.(api.prompt-echo))))" + :sx-target "#ref-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-prompt-result" + (~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm") + "Click to enter a name via prompt — it is sent as the SX-Prompt header."))) diff --git a/sx/sxc/reference/ref-pushurl-demo.sx b/sx/sxc/reference/ref-pushurl-demo.sx new file mode 100644 index 00000000..405366f1 --- /dev/null +++ b/sx/sxc/reference/ref-pushurl-demo.sx @@ -0,0 +1,27 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (div + (~tw :tokens "flex gap-2") + (a + :href "/sx/(geography.(hypermedia.(reference-detail.attributes.sx-get)))" + :sx-get "/sx/(geography.(hypermedia.(reference-detail.attributes.sx-get)))" + :sx-target "#sx-content" + :sx-select "#sx-content" + :sx-swap "outerHTML" + :sx-push-url "true" + (~tw :tokens "px-3 py-1 bg-violet-100 text-violet-700 rounded text-sm no-underline hover:bg-violet-200") + "sx-get page") + (a + :href "/sx/(geography.(hypermedia.(reference-detail.attributes.sx-post)))" + :sx-get "/sx/(geography.(hypermedia.(reference-detail.attributes.sx-post)))" + :sx-target "#sx-content" + :sx-select "#sx-content" + :sx-swap "outerHTML" + :sx-push-url "true" + (~tw :tokens "px-3 py-1 bg-violet-100 text-violet-700 rounded text-sm no-underline hover:bg-violet-200") + "sx-post page")) + (p + (~tw :tokens "text-sm text-stone-500") + "Click a link — the URL bar updates without a full page reload. Use browser back to return."))) diff --git a/sx/sxc/reference/ref-put-demo.sx b/sx/sxc/reference/ref-put-demo.sx new file mode 100644 index 00000000..676e5863 --- /dev/null +++ b/sx/sxc/reference/ref-put-demo.sx @@ -0,0 +1,14 @@ +(defcomp + () + (div + :id "ref-put-view" + (div + (~tw :tokens "flex items-center justify-between p-3 bg-stone-100 rounded") + (span (~tw :tokens "text-stone-700 text-sm") "Status: " (strong "draft")) + (button + :sx-put "/sx/(geography.(hypermedia.(reference.(api.status))))" + :sx-target "#ref-put-view" + :sx-swap "innerHTML" + :sx-vals "{\"status\": \"published\"}" + (~tw :tokens "px-3 py-1 bg-violet-600 text-white rounded text-sm hover:bg-violet-700") + "Publish")))) diff --git a/sx/sxc/reference/ref-replace-url-demo.sx b/sx/sxc/reference/ref-replace-url-demo.sx new file mode 100644 index 00000000..add7ede4 --- /dev/null +++ b/sx/sxc/reference/ref-replace-url-demo.sx @@ -0,0 +1,15 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (button + :sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))" + :sx-target "#ref-replurl-result" + :sx-swap "innerHTML" + :sx-replace-url "true" + (~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm") + "Load (replaces URL)") + (div + :id "ref-replurl-result" + (~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm") + "Click to load — URL changes but no new history entry."))) diff --git a/sx/sxc/reference/ref-retry-demo.sx b/sx/sxc/reference/ref-retry-demo.sx new file mode 100644 index 00000000..59074571 --- /dev/null +++ b/sx/sxc/reference/ref-retry-demo.sx @@ -0,0 +1,15 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (button + :sx-get "/sx/(geography.(hypermedia.(reference.(api.flaky))))" + :sx-target "#ref-retry-result" + :sx-swap "innerHTML" + :sx-retry "true" + (~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700") + "Call flaky endpoint") + (div + :id "ref-retry-result" + (~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm") + "This endpoint fails 2 out of 3 times. sx-retry retries automatically."))) diff --git a/sx/sxc/reference/ref-select-demo.sx b/sx/sxc/reference/ref-select-demo.sx new file mode 100644 index 00000000..b4343657 --- /dev/null +++ b/sx/sxc/reference/ref-select-demo.sx @@ -0,0 +1,15 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (button + :sx-get "/sx/(geography.(hypermedia.(reference.(api.select-page))))" + :sx-target "#ref-select-result" + :sx-select "#the-content" + :sx-swap "innerHTML" + (~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700") + "Load (selecting #the-content)") + (div + :id "ref-select-result" + (~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm") + "Only the selected fragment will appear here."))) diff --git a/sx/sxc/reference/ref-sse-demo.sx b/sx/sxc/reference/ref-sse-demo.sx new file mode 100644 index 00000000..e0b89171 --- /dev/null +++ b/sx/sxc/reference/ref-sse-demo.sx @@ -0,0 +1,15 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (div + :sx-sse "/sx/(geography.(hypermedia.(reference.(api.sse-time))))" + :sx-sse-swap "time" + :sx-swap "innerHTML" + (div + :id "ref-sse-result" + (~tw :tokens "p-3 rounded bg-stone-100 text-stone-600 text-sm font-mono") + "Connecting to SSE stream...")) + (p + (~tw :tokens "text-xs text-stone-400") + "Server pushes time updates every 2 seconds via Server-Sent Events."))) diff --git a/sx/sxc/reference/ref-swap-demo.sx b/sx/sxc/reference/ref-swap-demo.sx new file mode 100644 index 00000000..fdac174e --- /dev/null +++ b/sx/sxc/reference/ref-swap-demo.sx @@ -0,0 +1,28 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (div + (~tw :tokens "flex gap-2 flex-wrap") + (button + :sx-get "/sx/(geography.(hypermedia.(reference.(api.swap-item))))" + :sx-target "#ref-swap-list" + :sx-swap "beforeend" + (~tw :tokens "px-3 py-1 bg-violet-600 text-white rounded text-sm") + "beforeend") + (button + :sx-get "/sx/(geography.(hypermedia.(reference.(api.swap-item))))" + :sx-target "#ref-swap-list" + :sx-swap "afterbegin" + (~tw :tokens "px-3 py-1 bg-emerald-600 text-white rounded text-sm") + "afterbegin") + (button + :sx-get "/sx/(geography.(hypermedia.(reference.(api.swap-item))))" + :sx-target "#ref-swap-list" + :sx-swap "innerHTML" + (~tw :tokens "px-3 py-1 bg-blue-600 text-white rounded text-sm") + "innerHTML")) + (div + :id "ref-swap-list" + (~tw :tokens "p-3 rounded border border-stone-200 space-y-1 min-h-[3rem]") + (div (~tw :tokens "text-sm text-stone-500") "Original item")))) diff --git a/sx/sxc/reference/ref-sync-demo.sx b/sx/sxc/reference/ref-sync-demo.sx new file mode 100644 index 00000000..1a1380fb --- /dev/null +++ b/sx/sxc/reference/ref-sync-demo.sx @@ -0,0 +1,21 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (input + :type "text" + :name "q" + :placeholder "Type quickly..." + :sx-get "/sx/(geography.(hypermedia.(reference.(api.slow-echo))))" + :sx-trigger "input changed delay:100ms" + :sx-sync "replace" + :sx-target "#ref-sync-result" + :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")) + (p + (~tw :tokens "text-xs text-stone-400") + "With sync:replace, each new keystroke aborts the in-flight request.") + (div + :id "ref-sync-result" + (~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm") + "Type to see only the latest result."))) diff --git a/sx/sxc/reference/ref-target-demo.sx b/sx/sxc/reference/ref-target-demo.sx new file mode 100644 index 00000000..3619fdc0 --- /dev/null +++ b/sx/sxc/reference/ref-target-demo.sx @@ -0,0 +1,28 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (div + (~tw :tokens "flex gap-2") + (button + :sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))" + :sx-target "#ref-target-a" + :sx-swap "innerHTML" + (~tw :tokens "px-3 py-1 bg-violet-600 text-white rounded text-sm hover:bg-violet-700") + "Update Box A") + (button + :sx-get "/sx/(geography.(hypermedia.(reference.(api.time))))" + :sx-target "#ref-target-b" + :sx-swap "innerHTML" + (~tw :tokens "px-3 py-1 bg-emerald-600 text-white rounded text-sm hover:bg-emerald-700") + "Update Box B")) + (div + (~tw :tokens "grid grid-cols-2 gap-3") + (div + :id "ref-target-a" + (~tw :tokens "p-3 rounded border border-violet-200 bg-violet-50 text-sm text-stone-500") + "Box A") + (div + :id "ref-target-b" + (~tw :tokens "p-3 rounded border border-emerald-200 bg-emerald-50 text-sm text-stone-500") + "Box B")))) diff --git a/sx/sxc/reference/ref-trigger-demo.sx b/sx/sxc/reference/ref-trigger-demo.sx new file mode 100644 index 00000000..a8c9d9d5 --- /dev/null +++ b/sx/sxc/reference/ref-trigger-demo.sx @@ -0,0 +1,17 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (input + :type "text" + :name "q" + :placeholder "Type to search..." + :sx-get "/sx/(geography.(hypermedia.(reference.(api.trigger-search))))" + :sx-trigger "input changed delay:300ms" + :sx-target "#ref-trigger-result" + :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 "ref-trigger-result" + (~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm") + "Start typing to trigger a search."))) diff --git a/sx/sxc/reference/ref-validate-demo.sx b/sx/sxc/reference/ref-validate-demo.sx new file mode 100644 index 00000000..afda94ff --- /dev/null +++ b/sx/sxc/reference/ref-validate-demo.sx @@ -0,0 +1,24 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (form + :sx-post "/sx/(geography.(hypermedia.(reference.(api.greet))))" + :sx-target "#ref-validate-result" + :sx-swap "innerHTML" + :sx-validate "true" + (~tw :tokens "flex gap-2") + (input + :type "email" + :name "name" + :required "true" + :placeholder "Enter 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")) + (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-validate-result" + (~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm") + "Submit with invalid/empty email to see validation."))) diff --git a/sx/sxc/reference/ref-vals-demo.sx b/sx/sxc/reference/ref-vals-demo.sx new file mode 100644 index 00000000..8666c323 --- /dev/null +++ b/sx/sxc/reference/ref-vals-demo.sx @@ -0,0 +1,15 @@ +(defcomp + () + (div + (~tw :tokens "space-y-3") + (button + :sx-post "/sx/(geography.(hypermedia.(reference.(api.echo-vals))))" + :sx-vals "{\"source\": \"demo\", \"page\": \"3\"}" + :sx-target "#ref-vals-result" + :sx-swap "innerHTML" + (~tw :tokens "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700") + "Send with extra values") + (div + :id "ref-vals-result" + (~tw :tokens "p-3 rounded bg-stone-100 text-stone-400 text-sm") + "Click to see echoed values.")))