Files
rose-ash/sx/sx/applications/htmx/index.sx
giles 4f02f82f4e HS parser: fix number+comparison keyword collision, eval-hs uses hs-compile
Parser: skip unit suffix when next ident is a comparison keyword
(starts, ends, contains, matches, is, does, in, precedes, follows).
Fixes "123 starts with '12'" returning "123starts" instead of true.

eval-hs: use hs-compile directly instead of hs-to-sx-from-source with
"return " prefix, which was causing the parser to consume the comparison
as a string suffix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:29:01 +00:00

314 lines
12 KiB
Plaintext

;; htmx demo — live interactive examples using hx-* attributes
(defcomp
()
(~docs/page
:title "htmx"
(p
(~tw :tokens "text-lg text-gray-600 mb-2")
"Every "
(code "hx-*")
" attribute is syntactic sugar for a "
(a
:href "/sx/(applications.(hyperscript))"
(~tw :tokens "text-violet-600 underline")
"_hyperscript")
" event handler. Same runtime, same bytecode, zero duplication.")
(p
(~tw :tokens "text-gray-500 mb-8")
"These demos use real "
(code "hx-*")
" attributes processed by "
(code "htmx-activate!")
" — not "
(code "_=\"...\"")
" hyperscript syntax. "
"Click buttons, type in inputs, and watch the network tab.")
(~docs/section
:title "Click to Load"
:id "click-to-load"
(p
(~tw :tokens "text-gray-600 mb-3")
"The simplest htmx pattern: a button with "
(code "hx-get")
" that loads server content into a target element.")
(~docs/code
:src "<button hx-get=\"/sx/(applications.(htmx.(api.click)))\"\n hx-target=\"#click-result\"\n hx-swap=\"innerHTML\">\n Load Content\n</button>")
(div
(~tw :tokens "mt-4 flex flex-col gap-3")
(button
(~tw
:tokens "self-start px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors")
:hx-get "/sx/(applications.(htmx.(api.click)))"
:hx-target "#click-result"
:hx-swap "innerHTML"
"Load Content")
(div
:id "click-result"
(~tw
:tokens "min-h-16 border border-dashed border-gray-200 rounded-lg flex items-center justify-center")
(span
(~tw :tokens "text-gray-400 text-sm")
"Content will appear here"))))
(~docs/section
:title "Active Search"
:id "active-search"
(p
(~tw :tokens "text-gray-600 mb-3")
"Search-as-you-type with "
(code "hx-trigger=\"input changed delay:300ms\"")
" — debounces input and only fires when the value actually changes.")
(~docs/code
:src "<input hx-get=\"/sx/(applications.(htmx.(api.search)))\"\n hx-trigger=\"input changed delay:300ms\"\n hx-target=\"#search-results\"\n name=\"q\"\n placeholder=\"Search patterns...\">")
(div
(~tw :tokens "mt-4 space-y-3")
(input
(~tw
:tokens "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-300 focus:border-violet-400 outline-none")
:type "text"
:name "q"
:placeholder "Search patterns… (try 'tab', 'modal', 'scroll')"
:hx-get "/sx/(applications.(htmx.(api.search)))"
:hx-trigger "input changed delay:300ms"
:hx-target "#search-results")
(div
:id "search-results"
(~tw :tokens "border border-gray-200 rounded-lg overflow-hidden")
(p (~tw :tokens "text-gray-400 text-sm p-3") "Type to search…"))))
(~docs/section
:title "Tabs"
:id "tabs"
(p
(~tw :tokens "text-gray-600 mb-3")
"Tab buttons with "
(code "hx-get")
" loading content from the server. "
"Each click replaces the tab panel with server-rendered HTML.")
(~docs/code
:src "<button hx-get=\"/api/tab?tab=overview\"\n hx-target=\"#tab-content\"\n hx-swap=\"innerHTML\">\n Overview\n</button>")
(div
(~tw :tokens "mt-4 border border-gray-200 rounded-lg overflow-hidden")
(div
(~tw :tokens "flex border-b border-gray-200 bg-gray-50")
(button
(~tw
:tokens "px-4 py-2 text-sm font-medium text-violet-700 border-b-2 border-violet-600 bg-white")
:hx-get "/sx/(applications.(htmx.(api.tab)))?tab=overview"
:hx-target "#htmx-tab-content"
:hx-swap "innerHTML"
"Overview")
(button
(~tw
:tokens "px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700")
:hx-get "/sx/(applications.(htmx.(api.tab)))?tab=features"
:hx-target "#htmx-tab-content"
:hx-swap "innerHTML"
"Features")
(button
(~tw
:tokens "px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700")
:hx-get "/sx/(applications.(htmx.(api.tab)))?tab=code"
:hx-target "#htmx-tab-content"
:hx-swap "innerHTML"
"Code"))
(div
:id "htmx-tab-content"
(~tw :tokens "p-4")
(p
(~tw :tokens "text-gray-400 text-sm")
"Click a tab to load content"))))
(~docs/section
:title "Append Items"
:id "append"
(p
(~tw :tokens "text-gray-600 mb-3")
"Using "
(code "hx-swap=\"beforeend\"")
" to append new items instead of replacing content. "
"The htmx v4 alias "
(code "append")
" also works.")
(~docs/code
:src "<button hx-post=\"/api/append\"\n hx-target=\"#item-list\"\n hx-swap=\"beforeend\">\n Add Item\n</button>")
(div
(~tw :tokens "mt-4 space-y-3")
(button
(~tw
:tokens "px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors")
:hx-post "/sx/(applications.(htmx.(api.append)))"
:hx-target "#item-list"
:hx-swap "beforeend"
"Add Item")
(div
:id "item-list"
(~tw
:tokens "min-h-16 border border-dashed border-gray-200 rounded-lg p-2")
(span
(~tw :tokens "text-gray-400 text-sm")
"Items will append here"))))
(~docs/section
:title "Delete with Confirm"
:id "delete"
(p
(~tw :tokens "text-gray-600 mb-3")
(code "hx-confirm")
" shows a native dialog before firing the request. "
(code "hx-delete")
" sends a DELETE method. The empty response removes the target.")
(~docs/code
:src "<button hx-delete=\"/api/delete\"\n hx-target=\"closest .item\"\n hx-swap=\"outerHTML\"\n hx-confirm=\"Are you sure?\">\n Delete\n</button>")
(div
:id "delete-demo"
(~tw :tokens "mt-4 space-y-2")
(div
(~tw
:tokens "flex items-center justify-between p-3 bg-gray-50 border border-gray-200 rounded-lg item")
(span "First item")
(button
(~tw
:tokens "px-3 py-1 text-sm text-red-600 border border-red-200 rounded hover:bg-red-50")
:hx-delete "/sx/(applications.(htmx.(api.delete)))"
:hx-target "closest .item"
:hx-swap "outerHTML"
:hx-confirm "Delete this item?"
"Delete"))
(div
(~tw
:tokens "flex items-center justify-between p-3 bg-gray-50 border border-gray-200 rounded-lg item")
(span "Second item")
(button
(~tw
:tokens "px-3 py-1 text-sm text-red-600 border border-red-200 rounded hover:bg-red-50")
:hx-delete "/sx/(applications.(htmx.(api.delete)))"
:hx-target "closest .item"
:hx-swap "outerHTML"
:hx-confirm "Delete this item?"
"Delete"))
(div
(~tw
:tokens "flex items-center justify-between p-3 bg-gray-50 border border-gray-200 rounded-lg item")
(span "Third item")
(button
(~tw
:tokens "px-3 py-1 text-sm text-red-600 border border-red-200 rounded hover:bg-red-50")
:hx-delete "/sx/(applications.(htmx.(api.delete)))"
:hx-target "closest .item"
:hx-swap "outerHTML"
:hx-confirm "Delete this item?"
"Delete"))))
(~docs/section
:title "Form Submission"
:id "form"
(p
(~tw :tokens "text-gray-600 mb-3")
"A form with "
(code "hx-post")
" sends form data via AJAX. "
"The response replaces the target — no full page reload.")
(~docs/code
:src "<form hx-post=\"/api/form\"\n hx-target=\"#form-result\"\n hx-swap=\"innerHTML\">\n <input name=\"name\" />\n <input name=\"email\" type=\"email\" />\n <button type=\"submit\">Submit</button>\n</form>")
(div
(~tw :tokens "mt-4 space-y-3")
(form
(~tw :tokens "space-y-3")
:hx-post "/sx/(applications.(htmx.(api.form)))"
:hx-target "#form-result"
:hx-swap "innerHTML"
(div
(~tw :tokens "flex gap-3")
(input
(~tw
:tokens "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-300 outline-none")
:type "text"
:name "name"
:placeholder "Name")
(input
(~tw
:tokens "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-300 outline-none")
:type "email"
:name "email"
:placeholder "Email"))
(button
(~tw
:tokens "px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors")
:type "submit"
"Submit"))
(div
:id "form-result"
(~tw :tokens "min-h-12")
(span
(~tw :tokens "text-gray-400 text-sm")
"Submit the form to see the response"))))
(~docs/section
:title "How It Works"
:id "how-it-works"
(~docs/note
(p
(strong "Zero separate runtime. ")
"htmx attributes are translated to the same runtime calls as "
(a
:href "/sx/(applications.(hyperscript))"
(~tw :tokens "text-violet-600 underline")
"_hyperscript")
" event handlers. "
(code "hx-get")
" becomes "
(code "perform io-fetch")
". "
(code "hx-swap=\"innerHTML\"")
" becomes "
(code "dom-set-inner-html")
". "
"Same bytecode path."))
(p
(~tw :tokens "text-gray-600 mt-3")
"When the page loads, "
(code "htmx-boot!")
" scans the DOM for elements with "
(code "hx-get")
", "
(code "hx-post")
", etc. For each element, "
(code "htmx-activate!")
" reads the attributes, builds a handler function "
"from the same primitives the hyperscript compiler emits, and registers "
"it via "
(code "hs-on")
".")
(ol
(~tw :tokens "mt-3 space-y-1 text-gray-600 list-decimal list-inside")
(li
(code "htmx-boot!")
" finds all "
(code "[hx-get],[hx-post],…")
" elements")
(li
(code "htmx-activate!")
" reads "
(code "hx-get")
", "
(code "hx-target")
", "
(code "hx-swap")
", "
(code "hx-trigger")
" attributes")
(li
"Builds a handler: "
(code
"(fn (evt) (perform (io-fetch url method)) (hx-swap! target ...))"))
(li
"Registers via "
(code "hs-on")
" — same event system as hyperscript")
(li "On trigger, handler fires through the same bytecode VM"))
(p
(~tw :tokens "text-gray-600 mt-3")
"The full implementation is ~400 lines in "
(code "lib/hyperscript/htmx.sx")
" with "
(a
:href "/sx/(applications.(hyperscript.htmx))"
(~tw :tokens "text-violet-600 underline")
"57 tests")
" covering parsing, swap modes, triggers, and v4 features."))))