New hierarchy: Geography (Reactive Islands, Hypermedia Lakes, Marshes, Isomorphism), Language (Docs, Specs, Bootstrappers, Testing), Applications (CSSX, Protocols), Etc (Essays, Philosophy, Plans). All routes updated to match: /reactive/* → /geography/reactive/*, /docs/* → /language/docs/*, /essays/* → /etc/essays/*, etc. Updates nav-data.sx, all defpage routes, API endpoints, internal links across 43 files. Enhanced find-nav-match for nested group resolution. Also includes: page-helpers-demo sf-total fix (reduce instead of set!), rebootstrapped sx-browser.js and sx_ref.py, defensive slice/rest guards. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
778 lines
32 KiB
Plaintext
778 lines
32 KiB
Plaintext
;; SX docs — example and demo components
|
|
|
|
(defcomp ~example-card (&key title description &rest children)
|
|
(div :class "border border-stone-200 rounded-lg overflow-hidden"
|
|
(div :class "bg-stone-100 px-4 py-3 border-b border-stone-200"
|
|
(h3 :class "font-semibold text-stone-800" title)
|
|
(when description
|
|
(p :class "text-sm text-stone-500 mt-1" description)))
|
|
(div :class "p-4" children)))
|
|
|
|
(defcomp ~example-demo (&key &rest children)
|
|
(div :class "border border-dashed border-stone-300 rounded p-4 bg-stone-100" children))
|
|
|
|
(defcomp ~example-source (&key code)
|
|
(div :class "not-prose bg-stone-100 rounded p-5 mt-3 mx-auto max-w-3xl"
|
|
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code))))
|
|
|
|
;; --- Click to load demo ---
|
|
|
|
(defcomp ~click-to-load-demo ()
|
|
(div :class "space-y-4"
|
|
(div :id "click-result" :class "p-4 rounded bg-stone-100 text-stone-500 text-center"
|
|
"Click the button to load content.")
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/click"
|
|
:sx-target "#click-result"
|
|
:sx-swap "innerHTML"
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors"
|
|
"Load content")))
|
|
|
|
(defcomp ~click-result (&key time)
|
|
(div :class "space-y-2"
|
|
(p :class "text-stone-800 font-medium" "Content loaded!")
|
|
(p :class "text-stone-500 text-sm"
|
|
(str "Fetched from the server via sx-get at " time))))
|
|
|
|
;; --- Form submission demo ---
|
|
|
|
(defcomp ~form-demo ()
|
|
(div :class "space-y-4"
|
|
(form
|
|
:sx-post "/geography/hypermedia/examples/api/form"
|
|
:sx-target "#form-result"
|
|
:sx-swap "innerHTML"
|
|
:class "space-y-3"
|
|
(div
|
|
(label :class "block text-sm font-medium text-stone-700 mb-1" "Name")
|
|
(input :type "text" :name "name" :placeholder "Enter a name"
|
|
:class "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"
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
|
"Submit"))
|
|
(div :id "form-result" :class "p-3 rounded bg-stone-100 text-stone-500 text-sm text-center"
|
|
"Submit the form to see the result.")))
|
|
|
|
(defcomp ~form-result (&key name)
|
|
(div :class "text-stone-800"
|
|
(p (str "Hello, " (if (empty? name) "stranger" name) "!"))
|
|
(p :class "text-sm text-stone-500 mt-1" "Submitted via sx-post. The form data was sent as a POST request.")))
|
|
|
|
;; --- Polling demo ---
|
|
|
|
(defcomp ~polling-demo ()
|
|
(div :class "space-y-4"
|
|
(div :id "poll-target"
|
|
:sx-get "/geography/hypermedia/examples/api/poll"
|
|
:sx-trigger "load, every 2s"
|
|
:sx-swap "innerHTML"
|
|
:class "p-4 rounded border border-stone-200 bg-stone-100 text-center font-mono"
|
|
"Loading...")))
|
|
|
|
(defcomp ~poll-result (&key time count)
|
|
(div
|
|
(p :class "text-stone-800 font-medium" (str "Server time: " time))
|
|
(p :class "text-stone-500 text-sm mt-1" (str "Poll count: " count))
|
|
(div :class "mt-2 flex justify-center"
|
|
(div :class "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))))))
|
|
|
|
;; --- Delete row demo ---
|
|
|
|
(defcomp ~delete-demo (&key items)
|
|
(div
|
|
(table :class "w-full text-left text-sm"
|
|
(thead
|
|
(tr :class "border-b border-stone-200"
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Item")
|
|
(th :class "px-3 py-2 font-medium text-stone-600 w-20" "")))
|
|
(tbody :id "delete-rows"
|
|
(map (fn (item)
|
|
(~delete-row :id (nth item 0) :name (nth item 1)))
|
|
items)))))
|
|
|
|
(defcomp ~delete-row (&key id name)
|
|
(tr :id (str "row-" id) :class "border-b border-stone-100 transition-all"
|
|
(td :class "px-3 py-2 text-stone-700" name)
|
|
(td :class "px-3 py-2"
|
|
(button
|
|
:sx-delete (str "/geography/hypermedia/examples/api/delete/" id)
|
|
:sx-target (str "#row-" id)
|
|
:sx-swap "outerHTML"
|
|
:sx-confirm "Delete this item?"
|
|
:class "text-rose-500 hover:text-rose-700 text-sm"
|
|
"delete"))))
|
|
|
|
;; --- Inline edit demo ---
|
|
|
|
(defcomp ~inline-edit-demo ()
|
|
(div :id "edit-target" :class "space-y-3"
|
|
(~inline-view :value "Click edit to change this text")))
|
|
|
|
(defcomp ~inline-view (&key value)
|
|
(div :class "flex items-center justify-between p-3 rounded border border-stone-200"
|
|
(span :class "text-stone-800" value)
|
|
(button
|
|
:sx-get (str "/geography/hypermedia/examples/api/edit?value=" value)
|
|
:sx-target "#edit-target"
|
|
:sx-swap "innerHTML"
|
|
:class "text-sm text-violet-600 hover:text-violet-800"
|
|
"edit")))
|
|
|
|
(defcomp ~inline-edit-form (&key value)
|
|
(form
|
|
:sx-post "/geography/hypermedia/examples/api/edit"
|
|
:sx-target "#edit-target"
|
|
:sx-swap "innerHTML"
|
|
:class "flex items-center gap-2 p-3 rounded border border-violet-300 bg-violet-50"
|
|
(input :type "text" :name "value" :value value
|
|
:class "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"
|
|
:class "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
|
"save")
|
|
(button :type "button"
|
|
:sx-get (str "/geography/hypermedia/examples/api/edit/cancel?value=" value)
|
|
:sx-target "#edit-target"
|
|
:sx-swap "innerHTML"
|
|
:class "px-3 py-1.5 bg-stone-200 text-stone-700 rounded text-sm hover:bg-stone-300"
|
|
"cancel")))
|
|
|
|
;; --- OOB swap demo ---
|
|
|
|
(defcomp ~oob-demo ()
|
|
(div :class "space-y-4"
|
|
(div :class "grid grid-cols-2 gap-4"
|
|
(div :id "oob-box-a" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
|
|
(p :class "text-stone-500" "Box A")
|
|
(p :class "text-sm text-stone-400" "Waiting..."))
|
|
(div :id "oob-box-b" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
|
|
(p :class "text-stone-500" "Box B")
|
|
(p :class "text-sm text-stone-400" "Waiting...")))
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/oob"
|
|
:sx-target "#oob-box-a"
|
|
:sx-swap "innerHTML"
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
|
"Update both boxes")))
|
|
|
|
;; --- Lazy loading demo ---
|
|
|
|
(defcomp ~lazy-loading-demo ()
|
|
(div :class "space-y-4"
|
|
(p :class "text-sm text-stone-500" "The content below loads automatically when the page renders.")
|
|
(div :id "lazy-target"
|
|
:sx-get "/geography/hypermedia/examples/api/lazy"
|
|
:sx-trigger "load"
|
|
:sx-swap "innerHTML"
|
|
:class "p-6 rounded border border-stone-200 bg-stone-100 text-center"
|
|
(div :class "animate-pulse space-y-2"
|
|
(div :class "h-4 bg-stone-200 rounded w-3/4 mx-auto")
|
|
(div :class "h-4 bg-stone-200 rounded w-1/2 mx-auto")))))
|
|
|
|
(defcomp ~lazy-result (&key time)
|
|
(div :class "space-y-2"
|
|
(p :class "text-stone-800 font-medium" "Content loaded on page render!")
|
|
(p :class "text-stone-500 text-sm" (str "Loaded via sx-trigger=\"load\" at " time))))
|
|
|
|
;; --- Infinite scroll demo ---
|
|
|
|
(defcomp ~infinite-scroll-demo ()
|
|
(div :class "h-64 overflow-y-auto border border-stone-200 rounded" :id "scroll-container"
|
|
(div :id "scroll-items"
|
|
(map-indexed (fn (i item)
|
|
(div :class "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 "/geography/hypermedia/examples/api/scroll?page=2"
|
|
:sx-trigger "intersect once"
|
|
:sx-target "#scroll-items"
|
|
:sx-swap "beforeend"
|
|
:class "p-3 text-center text-stone-400 text-sm"
|
|
"Loading more..."))))
|
|
|
|
(defcomp ~scroll-items (&key items page)
|
|
(<>
|
|
(map (fn (item)
|
|
(div :class "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 "/geography/hypermedia/examples/api/scroll?page=" page)
|
|
:sx-trigger "intersect once"
|
|
:sx-target "#scroll-items"
|
|
:sx-swap "beforeend"
|
|
:class "p-3 text-center text-stone-400 text-sm"
|
|
"Loading more..."))))
|
|
|
|
;; --- Progress bar demo ---
|
|
|
|
(defcomp ~progress-bar-demo ()
|
|
(div :class "space-y-4"
|
|
(div :id "progress-target" :class "space-y-3"
|
|
(div :class "w-full bg-stone-200 rounded-full h-4"
|
|
(div :class "bg-violet-600 h-4 rounded-full transition-all" :style "width: 0%"))
|
|
(p :class "text-sm text-stone-500 text-center" "Click start to begin."))
|
|
(button
|
|
:sx-post "/geography/hypermedia/examples/api/progress/start"
|
|
:sx-target "#progress-target"
|
|
:sx-swap "innerHTML"
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
|
"Start job")))
|
|
|
|
(defcomp ~progress-status (&key percent job-id)
|
|
(div :class "space-y-3"
|
|
(div :class "w-full bg-stone-200 rounded-full h-4"
|
|
(div :class "bg-violet-600 h-4 rounded-full transition-all"
|
|
:style (str "width: " percent "%")))
|
|
(p :class "text-sm text-stone-500 text-center" (str percent "% complete"))
|
|
(when (< percent 100)
|
|
(div :sx-get (str "/geography/hypermedia/examples/api/progress/status?job=" job-id)
|
|
:sx-trigger "load delay:500ms"
|
|
:sx-target "#progress-target"
|
|
:sx-swap "innerHTML"))
|
|
(when (= percent 100)
|
|
(p :class "text-sm text-emerald-600 font-medium text-center" "Job complete!"))))
|
|
|
|
;; --- Active search demo ---
|
|
|
|
(defcomp ~active-search-demo ()
|
|
(div :class "space-y-3"
|
|
(input :type "text" :name "q"
|
|
:sx-get "/geography/hypermedia/examples/api/search"
|
|
:sx-trigger "keyup delay:300ms changed"
|
|
:sx-target "#search-results"
|
|
:sx-swap "innerHTML"
|
|
:placeholder "Search programming languages..."
|
|
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
|
(div :id "search-results" :class "border border-stone-200 rounded divide-y divide-stone-100"
|
|
(p :class "p-3 text-sm text-stone-400" "Type to search..."))))
|
|
|
|
(defcomp ~search-results (&key items query)
|
|
(<>
|
|
(if (empty? items)
|
|
(p :class "p-3 text-sm text-stone-400" (str "No results for \"" query "\""))
|
|
(map (fn (item)
|
|
(div :class "px-3 py-2 text-sm text-stone-700" item))
|
|
items))))
|
|
|
|
;; --- Inline validation demo ---
|
|
|
|
(defcomp ~inline-validation-demo ()
|
|
(form :class "space-y-4" :sx-post "/geography/hypermedia/examples/api/validate/submit" :sx-target "#validation-result" :sx-swap "innerHTML"
|
|
(div
|
|
(label :class "block text-sm font-medium text-stone-700 mb-1" "Email")
|
|
(input :type "text" :name "email" :placeholder "user@example.com"
|
|
:sx-get "/geography/hypermedia/examples/api/validate"
|
|
:sx-trigger "blur"
|
|
:sx-target "#email-feedback"
|
|
:sx-swap "innerHTML"
|
|
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
|
(div :id "email-feedback" :class "mt-1"))
|
|
(button :type "submit"
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
|
"Submit")
|
|
(div :id "validation-result")))
|
|
|
|
(defcomp ~validation-ok (&key email)
|
|
(p :class "text-sm text-emerald-600" (str email " is available")))
|
|
|
|
(defcomp ~validation-error (&key message)
|
|
(p :class "text-sm text-rose-600" message))
|
|
|
|
;; --- Value select demo ---
|
|
|
|
(defcomp ~value-select-demo ()
|
|
(div :class "space-y-3"
|
|
(div
|
|
(label :class "block text-sm font-medium text-stone-700 mb-1" "Category")
|
|
(select :name "category"
|
|
:sx-get "/geography/hypermedia/examples/api/values"
|
|
:sx-trigger "change"
|
|
:sx-target "#value-items"
|
|
:sx-swap "innerHTML"
|
|
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"
|
|
(option :value "" "Pick a category...")
|
|
(option :value "Languages" "Languages")
|
|
(option :value "Frameworks" "Frameworks")
|
|
(option :value "Databases" "Databases")))
|
|
(div
|
|
(label :class "block text-sm font-medium text-stone-700 mb-1" "Item")
|
|
(select :id "value-items"
|
|
:class "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...")))))
|
|
|
|
(defcomp ~value-options (&key items)
|
|
(<>
|
|
(map (fn (item) (option :value item item)) items)))
|
|
|
|
;; --- Reset on submit demo ---
|
|
|
|
(defcomp ~reset-on-submit-demo ()
|
|
(div :class "space-y-3"
|
|
(form :id "reset-form"
|
|
:sx-post "/geography/hypermedia/examples/api/reset-submit"
|
|
:sx-target "#reset-result"
|
|
:sx-swap "innerHTML"
|
|
:sx-on:afterSwap "this.reset()"
|
|
:class "flex gap-2"
|
|
(input :type "text" :name "message" :placeholder "Type a message..."
|
|
:class "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"
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
|
"Send"))
|
|
(div :id "reset-result" :class "space-y-2"
|
|
(p :class "text-sm text-stone-400" "Messages will appear here."))))
|
|
|
|
(defcomp ~reset-message (&key message time)
|
|
(div :class "px-3 py-2 bg-stone-100 rounded text-sm text-stone-700"
|
|
(str "[" time "] " message)))
|
|
|
|
;; --- Edit row demo ---
|
|
|
|
(defcomp ~edit-row-demo (&key rows)
|
|
(div
|
|
(table :class "w-full text-left text-sm"
|
|
(thead
|
|
(tr :class "border-b border-stone-200"
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Name")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Price")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Stock")
|
|
(th :class "px-3 py-2 font-medium text-stone-600 w-24" "")))
|
|
(tbody :id "edit-rows"
|
|
(map (fn (row)
|
|
(~edit-row-view :id (nth row 0) :name (nth row 1) :price (nth row 2) :stock (nth row 3)))
|
|
rows)))))
|
|
|
|
(defcomp ~edit-row-view (&key id name price stock)
|
|
(tr :id (str "erow-" id) :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" name)
|
|
(td :class "px-3 py-2 text-stone-700" (str "$" price))
|
|
(td :class "px-3 py-2 text-stone-700" stock)
|
|
(td :class "px-3 py-2"
|
|
(button
|
|
:sx-get (str "/geography/hypermedia/examples/api/editrow/" id)
|
|
:sx-target (str "#erow-" id)
|
|
:sx-swap "outerHTML"
|
|
:class "text-sm text-violet-600 hover:text-violet-800"
|
|
"edit"))))
|
|
|
|
(defcomp ~edit-row-form (&key id name price stock)
|
|
(tr :id (str "erow-" id) :class "border-b border-stone-100 bg-violet-50"
|
|
(td :class "px-3 py-2"
|
|
(input :type "text" :name "name" :value name
|
|
:class "w-full px-2 py-1 border border-stone-300 rounded text-sm"))
|
|
(td :class "px-3 py-2"
|
|
(input :type "text" :name "price" :value price
|
|
:class "w-20 px-2 py-1 border border-stone-300 rounded text-sm"))
|
|
(td :class "px-3 py-2"
|
|
(input :type "text" :name "stock" :value stock
|
|
:class "w-20 px-2 py-1 border border-stone-300 rounded text-sm"))
|
|
(td :class "px-3 py-2 space-x-1"
|
|
(button
|
|
:sx-post (str "/geography/hypermedia/examples/api/editrow/" id)
|
|
:sx-target (str "#erow-" id)
|
|
:sx-swap "outerHTML"
|
|
:sx-include (str "#erow-" id)
|
|
:class "text-sm text-emerald-600 hover:text-emerald-800"
|
|
"save")
|
|
(button
|
|
:sx-get (str "/geography/hypermedia/examples/api/editrow/" id "/cancel")
|
|
:sx-target (str "#erow-" id)
|
|
:sx-swap "outerHTML"
|
|
:class "text-sm text-stone-500 hover:text-stone-700"
|
|
"cancel"))))
|
|
|
|
;; --- Bulk update demo ---
|
|
|
|
(defcomp ~bulk-update-demo (&key users)
|
|
(div :class "space-y-3"
|
|
(form :id "bulk-form"
|
|
(div :class "flex gap-2 mb-3"
|
|
(button :type "button"
|
|
:sx-post "/geography/hypermedia/examples/api/bulk?action=activate"
|
|
:sx-target "#bulk-table"
|
|
:sx-swap "innerHTML"
|
|
:sx-include "#bulk-form"
|
|
:class "px-3 py-1.5 bg-emerald-600 text-white rounded text-sm hover:bg-emerald-700"
|
|
"Activate")
|
|
(button :type "button"
|
|
:sx-post "/geography/hypermedia/examples/api/bulk?action=deactivate"
|
|
:sx-target "#bulk-table"
|
|
:sx-swap "innerHTML"
|
|
:sx-include "#bulk-form"
|
|
:class "px-3 py-1.5 bg-stone-600 text-white rounded text-sm hover:bg-stone-700"
|
|
"Deactivate"))
|
|
(table :class "w-full text-left text-sm"
|
|
(thead
|
|
(tr :class "border-b border-stone-200"
|
|
(th :class "px-3 py-2 w-8" "")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Name")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Email")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Status")))
|
|
(tbody :id "bulk-table"
|
|
(map (fn (u)
|
|
(~bulk-row :id (nth u 0) :name (nth u 1) :email (nth u 2) :status (nth u 3)))
|
|
users))))))
|
|
|
|
(defcomp ~bulk-row (&key id name email status)
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2"
|
|
(input :type "checkbox" :name "ids" :value id))
|
|
(td :class "px-3 py-2 text-stone-700" name)
|
|
(td :class "px-3 py-2 text-stone-700" email)
|
|
(td :class "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))))
|
|
|
|
;; --- Swap positions demo ---
|
|
|
|
(defcomp ~swap-positions-demo ()
|
|
(div :class "space-y-3"
|
|
(div :class "flex gap-2"
|
|
(button
|
|
:sx-post "/geography/hypermedia/examples/api/swap-log?mode=beforeend"
|
|
:sx-target "#swap-log"
|
|
:sx-swap "beforeend"
|
|
:class "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
|
"Add to End")
|
|
(button
|
|
:sx-post "/geography/hypermedia/examples/api/swap-log?mode=afterbegin"
|
|
:sx-target "#swap-log"
|
|
:sx-swap "afterbegin"
|
|
:class "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
|
"Add to Start")
|
|
(button
|
|
:sx-post "/geography/hypermedia/examples/api/swap-log?mode=none"
|
|
:sx-target "#swap-log"
|
|
:sx-swap "none"
|
|
:class "px-3 py-1.5 bg-stone-600 text-white rounded text-sm hover:bg-stone-700"
|
|
"Silent Ping")
|
|
(span :id "swap-counter" :class "self-center text-sm text-stone-500" "Count: 0"))
|
|
(div :id "swap-log"
|
|
:class "border border-stone-200 rounded h-48 overflow-y-auto divide-y divide-stone-100"
|
|
(p :class "p-3 text-sm text-stone-400" "Log entries will appear here."))))
|
|
|
|
(defcomp ~swap-entry (&key time mode)
|
|
(div :class "px-3 py-2 text-sm text-stone-700"
|
|
(str "[" time "] " mode)))
|
|
|
|
;; --- Select filter demo ---
|
|
|
|
(defcomp ~select-filter-demo ()
|
|
(div :class "space-y-3"
|
|
(div :class "flex gap-2"
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/dashboard"
|
|
:sx-target "#filter-target"
|
|
:sx-swap "innerHTML"
|
|
:sx-select "#dash-stats"
|
|
:class "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
|
"Stats Only")
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/dashboard"
|
|
:sx-target "#filter-target"
|
|
:sx-swap "innerHTML"
|
|
:sx-select "#dash-header"
|
|
:class "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
|
"Header Only")
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/dashboard"
|
|
:sx-target "#filter-target"
|
|
:sx-swap "innerHTML"
|
|
:class "px-3 py-1.5 bg-stone-600 text-white rounded text-sm hover:bg-stone-700"
|
|
"Full Dashboard"))
|
|
(div :id "filter-target" :class "border border-stone-200 rounded p-4 bg-stone-100"
|
|
(p :class "text-sm text-stone-400" "Click a button to load content."))))
|
|
|
|
;; --- Tabs demo ---
|
|
|
|
(defcomp ~tabs-demo ()
|
|
(div :class "space-y-0"
|
|
(div :class "flex border-b border-stone-200" :id "tab-buttons"
|
|
(~tab-btn :tab "tab1" :label "Overview" :active "true")
|
|
(~tab-btn :tab "tab2" :label "Details" :active "false")
|
|
(~tab-btn :tab "tab3" :label "History" :active "false"))
|
|
(div :id "tab-content" :class "p-4 border border-t-0 border-stone-200 rounded-b"
|
|
(p :class "text-stone-700" "Welcome to the Overview tab. This content is loaded by default.")
|
|
(p :class "text-stone-500 text-sm mt-2" "Click the tabs above to navigate. Watch the browser URL update."))))
|
|
|
|
(defcomp ~tab-btn (&key tab label active)
|
|
(button
|
|
:sx-get (str "/geography/hypermedia/examples/api/tabs/" tab)
|
|
:sx-target "#tab-content"
|
|
:sx-swap "innerHTML"
|
|
:sx-push-url (str "/geography/hypermedia/examples/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))
|
|
|
|
;; --- Animations demo ---
|
|
|
|
(defcomp ~animations-demo ()
|
|
(div :class "space-y-4"
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/animate"
|
|
:sx-target "#anim-target"
|
|
:sx-swap "innerHTML"
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
|
"Load with animation")
|
|
(div :id "anim-target" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
|
|
(p :class "text-stone-400" "Content will fade in here."))))
|
|
|
|
(defcomp ~anim-result (&key color time)
|
|
(div :class "sx-fade-in space-y-2"
|
|
(div :class (str "p-4 rounded transition-colors duration-700 " color)
|
|
(p :class "font-medium" "Faded in!")
|
|
(p :class "text-sm mt-1" (str "Loaded at " time)))))
|
|
|
|
;; --- Dialogs demo ---
|
|
|
|
(defcomp ~dialogs-demo ()
|
|
(div
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/dialog"
|
|
:sx-target "#dialog-container"
|
|
:sx-swap "innerHTML"
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
|
"Open Dialog")
|
|
(div :id "dialog-container")))
|
|
|
|
(defcomp ~dialog-modal (&key title message)
|
|
(div :class "fixed inset-0 z-50 flex items-center justify-center"
|
|
(div :class "absolute inset-0 bg-black/50"
|
|
:sx-get "/geography/hypermedia/examples/api/dialog/close"
|
|
:sx-target "#dialog-container"
|
|
:sx-swap "innerHTML")
|
|
(div :class "relative bg-stone-100 rounded-lg shadow-xl p-6 max-w-md w-full mx-4 space-y-4"
|
|
(h3 :class "text-lg font-semibold text-stone-800" title)
|
|
(p :class "text-stone-600" message)
|
|
(div :class "flex justify-end gap-2"
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/dialog/close"
|
|
:sx-target "#dialog-container"
|
|
:sx-swap "innerHTML"
|
|
:class "px-4 py-2 bg-stone-200 text-stone-700 rounded text-sm hover:bg-stone-300"
|
|
"Cancel")
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/dialog/close"
|
|
:sx-target "#dialog-container"
|
|
:sx-swap "innerHTML"
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
|
"Confirm")))))
|
|
|
|
;; --- Keyboard shortcuts demo ---
|
|
|
|
(defcomp ~keyboard-shortcuts-demo ()
|
|
(div :class "space-y-4"
|
|
(div :class "p-4 rounded border border-stone-200 bg-stone-100"
|
|
(p :class "text-sm text-stone-600 font-medium mb-2" "Keyboard shortcuts:")
|
|
(div :class "flex gap-4"
|
|
(div :class "flex items-center gap-1"
|
|
(kbd :class "px-2 py-0.5 bg-stone-100 border border-stone-300 rounded text-xs font-mono" "s")
|
|
(span :class "text-sm text-stone-500" "Search"))
|
|
(div :class "flex items-center gap-1"
|
|
(kbd :class "px-2 py-0.5 bg-stone-100 border border-stone-300 rounded text-xs font-mono" "n")
|
|
(span :class "text-sm text-stone-500" "New item"))
|
|
(div :class "flex items-center gap-1"
|
|
(kbd :class "px-2 py-0.5 bg-stone-100 border border-stone-300 rounded text-xs font-mono" "h")
|
|
(span :class "text-sm text-stone-500" "Help"))))
|
|
(div :id "kbd-target"
|
|
:sx-get "/geography/hypermedia/examples/api/keyboard?key=s"
|
|
:sx-trigger "keyup[key=='s'&&!event.target.matches('input,textarea')] from:body"
|
|
:sx-swap "innerHTML"
|
|
:class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
|
|
(p :class "text-stone-400 text-sm" "Press a shortcut key..."))
|
|
(div :sx-get "/geography/hypermedia/examples/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 "/geography/hypermedia/examples/api/keyboard?key=h"
|
|
:sx-trigger "keyup[key=='h'&&!event.target.matches('input,textarea')] from:body"
|
|
:sx-target "#kbd-target"
|
|
:sx-swap "innerHTML")))
|
|
|
|
(defcomp ~kbd-result (&key key action)
|
|
(div :class "space-y-1"
|
|
(p :class "text-stone-800 font-medium" action)
|
|
(p :class "text-sm text-stone-500" (str "Triggered by pressing '" key "'"))))
|
|
|
|
;; --- PUT / PATCH demo ---
|
|
|
|
(defcomp ~put-patch-demo (&key name email role)
|
|
(div :id "pp-target" :class "space-y-4"
|
|
(~pp-view :name name :email email :role role)))
|
|
|
|
(defcomp ~pp-view (&key name email role)
|
|
(div :class "space-y-3"
|
|
(div :class "flex justify-between items-start"
|
|
(div
|
|
(p :class "text-stone-800 font-medium" name)
|
|
(p :class "text-sm text-stone-500" email)
|
|
(p :class "text-sm text-stone-500" role))
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/putpatch/edit-all"
|
|
:sx-target "#pp-target"
|
|
:sx-swap "innerHTML"
|
|
:class "text-sm text-violet-600 hover:text-violet-800"
|
|
"Edit All (PUT)"))))
|
|
|
|
(defcomp ~pp-form-full (&key name email role)
|
|
(form
|
|
:sx-put "/geography/hypermedia/examples/api/putpatch"
|
|
:sx-target "#pp-target"
|
|
:sx-swap "innerHTML"
|
|
:class "space-y-3"
|
|
(div
|
|
(label :class "block text-sm font-medium text-stone-700 mb-1" "Name")
|
|
(input :type "text" :name "name" :value name
|
|
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm"))
|
|
(div
|
|
(label :class "block text-sm font-medium text-stone-700 mb-1" "Email")
|
|
(input :type "text" :name "email" :value email
|
|
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm"))
|
|
(div
|
|
(label :class "block text-sm font-medium text-stone-700 mb-1" "Role")
|
|
(input :type "text" :name "role" :value role
|
|
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm"))
|
|
(div :class "flex gap-2"
|
|
(button :type "submit"
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
|
"Save All (PUT)")
|
|
(button :type "button"
|
|
:sx-get "/geography/hypermedia/examples/api/putpatch/cancel"
|
|
:sx-target "#pp-target"
|
|
:sx-swap "innerHTML"
|
|
:class "px-4 py-2 bg-stone-200 text-stone-700 rounded text-sm hover:bg-stone-300"
|
|
"Cancel"))))
|
|
|
|
;; --- JSON encoding demo ---
|
|
|
|
(defcomp ~json-encoding-demo ()
|
|
(div :class "space-y-4"
|
|
(form
|
|
:sx-post "/geography/hypermedia/examples/api/json-echo"
|
|
:sx-target "#json-result"
|
|
:sx-swap "innerHTML"
|
|
:sx-encoding "json"
|
|
:class "space-y-3"
|
|
(div
|
|
(label :class "block text-sm font-medium text-stone-700 mb-1" "Name")
|
|
(input :type "text" :name "name" :value "Ada Lovelace"
|
|
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm"))
|
|
(div
|
|
(label :class "block text-sm font-medium text-stone-700 mb-1" "Age")
|
|
(input :type "number" :name "age" :value "36"
|
|
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm"))
|
|
(button :type "submit"
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
|
"Submit as JSON"))
|
|
(div :id "json-result" :class "p-3 rounded bg-stone-100 text-stone-500 text-sm"
|
|
"Submit the form to see the server echo the parsed JSON.")))
|
|
|
|
(defcomp ~json-result (&key body content-type)
|
|
(div :class "space-y-2"
|
|
(p :class "text-stone-800 font-medium" "Server received:")
|
|
(pre :class "text-sm bg-stone-100 p-3 rounded overflow-x-auto" (code body))
|
|
(p :class "text-sm text-stone-500" (str "Content-Type: " content-type))))
|
|
|
|
;; --- Vals & Headers demo ---
|
|
|
|
(defcomp ~vals-headers-demo ()
|
|
(div :class "space-y-6"
|
|
(div :class "space-y-2"
|
|
(h4 :class "text-sm font-semibold text-stone-700" "sx-vals — send extra values")
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/echo-vals"
|
|
:sx-target "#vals-result"
|
|
:sx-swap "innerHTML"
|
|
:sx-vals "{\"source\": \"button\", \"version\": \"2.0\"}"
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
|
"Send with vals")
|
|
(div :id "vals-result" :class "p-3 rounded bg-stone-100 text-sm text-stone-400"
|
|
"Click to see server-received values."))
|
|
(div :class "space-y-2"
|
|
(h4 :class "text-sm font-semibold text-stone-700" "sx-headers — send custom headers")
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/echo-headers"
|
|
:sx-target "#headers-result"
|
|
:sx-swap "innerHTML"
|
|
:sx-headers {:X-Custom-Token "abc123" :X-Request-Source "demo"}
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
|
"Send with headers")
|
|
(div :id "headers-result" :class "p-3 rounded bg-stone-100 text-sm text-stone-400"
|
|
"Click to see server-received headers."))))
|
|
|
|
(defcomp ~echo-result (&key label items)
|
|
(div :class "space-y-1"
|
|
(p :class "text-stone-800 font-medium" (str "Server received " label ":"))
|
|
(map (fn (item)
|
|
(div :class "text-sm text-stone-600 font-mono" item))
|
|
items)))
|
|
|
|
;; --- Loading states demo ---
|
|
|
|
(defcomp ~loading-states-demo ()
|
|
(div :class "space-y-4"
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/slow"
|
|
:sx-target "#loading-result"
|
|
:sx-swap "innerHTML"
|
|
:class "sx-loading-btn px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm flex items-center gap-2"
|
|
(span :class "sx-spinner w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin")
|
|
(span "Load slow endpoint"))
|
|
(div :id "loading-result" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
|
|
(p :class "text-stone-400 text-sm" "Click the button — it takes 2 seconds."))))
|
|
|
|
(defcomp ~loading-result (&key time)
|
|
(div
|
|
(p :class "text-stone-800 font-medium" "Loaded!")
|
|
(p :class "text-sm text-stone-500" (str "Response arrived at " time))))
|
|
|
|
;; --- Sync replace demo (request abort) ---
|
|
|
|
(defcomp ~sync-replace-demo ()
|
|
(div :class "space-y-3"
|
|
(input :type "text" :name "q"
|
|
:sx-get "/geography/hypermedia/examples/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)..."
|
|
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
|
(div :id "sync-result" :class "p-4 rounded border border-stone-200 bg-stone-100"
|
|
(p :class "text-sm text-stone-400" "Type to trigger requests — stale ones get aborted."))))
|
|
|
|
(defcomp ~sync-result (&key query delay)
|
|
(div
|
|
(p :class "text-stone-800 font-medium" (str "Result for: \"" query "\""))
|
|
(p :class "text-sm text-stone-500" (str "Server took " delay "ms to respond"))))
|
|
|
|
;; --- Retry demo ---
|
|
|
|
(defcomp ~retry-demo ()
|
|
(div :class "space-y-4"
|
|
(button
|
|
:sx-get "/geography/hypermedia/examples/api/flaky"
|
|
:sx-target "#retry-result"
|
|
:sx-swap "innerHTML"
|
|
:sx-retry "exponential:1000:8000"
|
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
|
"Call flaky endpoint")
|
|
(div :id "retry-result" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center"
|
|
(p :class "text-stone-400 text-sm" "Endpoint fails twice, succeeds on 3rd attempt."))))
|
|
|
|
(defcomp ~retry-result (&key attempt message)
|
|
(div :class "space-y-1"
|
|
(p :class "text-stone-800 font-medium" message)
|
|
(p :class "text-sm text-stone-500" (str "Succeeded on attempt #" attempt))))
|