Send all responses as sexp wire format with client-side rendering
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m35s

- Server sends sexp source text, client (sexp.js) renders everything
- SexpExpr marker class for nested sexp composition in serialize()
- sexp_page() HTML shell with data-mount="body" for full page loads
- sexp_response() returns text/sexp for OOB/partial responses
- ~app-body layout component replaces ~app-layout (no raw!)
- ~rich-text is the only component using raw! (for CMS HTML content)
- Fragment endpoints return text/sexp, auto-wrapped in SexpExpr
- All _*_html() helpers converted to _*_sexp() returning sexp source
- Head auto-hoist: sexp.js moves meta/title/link/script[ld+json]
  from rendered body to document.head automatically
- Unknown components render warning box instead of crashing page
- Component kwargs preserve AST for lazy rendering (fixes <> in kwargs)
- Fix unterminated paren in events/sexp/tickets.sexpr

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 09:45:07 +00:00
parent 0d48fd22ee
commit 22802bd36b
270 changed files with 7153 additions and 5382 deletions

View File

@@ -1,16 +1,16 @@
;; Events admin components
(defcomp ~events-calendar-admin-panel (&key description-html csrf description)
(defcomp ~events-calendar-admin-panel (&key description-content csrf description)
(section :class "max-w-3xl mx-auto p-4 space-y-10"
(div
(h2 :class "text-xl font-semibold" "Calendar configuration")
(div :id "cal-put-errors" :class "mt-2 text-sm text-red-600")
(div (label :class "block text-sm font-medium text-stone-700" "Description")
(raw! description-html))
(form :id "calendar-form" :method "post" :hx-target "#main-panel" :hx-select "#main-panel"
:hx-on::before-request "document.querySelector('#cal-put-errors').textContent='';"
:hx-on::response-error "document.querySelector('#cal-put-errors').innerHTML = event.detail.xhr.responseText;"
:hx-on::after-request "if (event.detail.successful) this.reset()"
(when description-content description-content))
(form :id "calendar-form" :method "post" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-on:beforeRequest "document.querySelector('#cal-put-errors').textContent='';"
:sx-on:responseError "document.querySelector('#cal-put-errors').textContent='Error'; if(event.detail.response){event.detail.response.clone().text().then(function(t){event.target.closest('form').querySelector('[id$=errors]').innerHTML=t})}"
:sx-on:afterRequest "if (event.detail.successful) this.reset()"
:class "hidden space-y-4 mt-4" :autocomplete "off"
(input :type "hidden" :name "csrf_token" :value csrf)
(div (label :class "block text-sm font-medium text-stone-700" "Description")
@@ -23,10 +23,10 @@
(a :href href :class "inline-flex items-center gap-1 px-2 py-1 text-xs text-stone-500 hover:text-stone-700 hover:bg-stone-100 rounded"
(i :class "fa fa-cog" :aria-hidden "true") " Admin"))
(defcomp ~events-entry-field (&key label content-html)
(defcomp ~events-entry-field (&key label content)
(div :class "flex flex-col mb-4"
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" label)
(raw! content-html)))
content))
(defcomp ~events-entry-name-field (&key name)
(div :class "mt-1 text-lg font-medium" name))
@@ -42,50 +42,50 @@
(defcomp ~events-entry-time-field (&key time-str)
(div :class "mt-1" time-str))
(defcomp ~events-entry-state-field (&key entry-id badge-html)
(div :class "mt-1" (div :id (str "entry-state-" entry-id) (raw! badge-html))))
(defcomp ~events-entry-state-field (&key entry-id badge)
(div :class "mt-1" (div :id (str "entry-state-" entry-id) badge)))
(defcomp ~events-entry-cost-field (&key cost-html)
(div :class "mt-1" (span :class "font-medium text-green-600" (raw! cost-html))))
(defcomp ~events-entry-cost-field (&key cost)
(div :class "mt-1" (span :class "font-medium text-green-600" cost)))
(defcomp ~events-entry-tickets-field (&key entry-id tickets-config-html)
(div :class "mt-1" :id (str "entry-tickets-" entry-id) (raw! tickets-config-html)))
(defcomp ~events-entry-tickets-field (&key entry-id tickets-config)
(div :class "mt-1" :id (str "entry-tickets-" entry-id) tickets-config))
(defcomp ~events-entry-date-field (&key date-str)
(div :class "mt-1" date-str))
(defcomp ~events-entry-posts-field (&key entry-id posts-panel-html)
(div :class "mt-1" :id (str "entry-posts-" entry-id) (raw! posts-panel-html)))
(defcomp ~events-entry-posts-field (&key entry-id posts-panel)
(div :class "mt-1" :id (str "entry-posts-" entry-id) posts-panel))
(defcomp ~events-entry-panel (&key entry-id list-container name-html slot-html time-html state-html cost-html
tickets-html buy-html date-html posts-html options-html pre-action edit-url)
(defcomp ~events-entry-panel (&key entry-id list-container name slot time state cost
tickets buy date posts options pre-action edit-url)
(section :id (str "entry-" entry-id) :class list-container
(raw! name-html) (raw! slot-html) (raw! time-html) (raw! state-html) (raw! cost-html)
(raw! tickets-html) (raw! buy-html) (raw! date-html) (raw! posts-html)
name slot time state cost
tickets buy date posts
(div :class "flex gap-2 mt-6"
(raw! options-html)
options
(button :type "button" :class pre-action
:hx-get edit-url :hx-target (str "#entry-" entry-id) :hx-swap "outerHTML"
:sx-get edit-url :sx-target (str "#entry-" entry-id) :sx-swap "outerHTML"
"Edit"))))
(defcomp ~events-entry-title (&key name badge-html)
(<> (i :class "fa fa-clock") " " name " " (raw! badge-html)))
(defcomp ~events-entry-title (&key name badge)
(<> (i :class "fa fa-clock") " " name " " badge))
(defcomp ~events-entry-times (&key time-str)
(div :class "text-sm text-gray-600" time-str))
(defcomp ~events-entry-optioned-oob (&key entry-id title-html state-html)
(<> (div :id (str "entry-title-" entry-id) :hx-swap-oob "innerHTML" (raw! title-html))
(div :id (str "entry-state-" entry-id) :hx-swap-oob "innerHTML" (raw! state-html))))
(defcomp ~events-entry-optioned-oob (&key entry-id title state)
(<> (div :id (str "entry-title-" entry-id) :sx-swap-oob "innerHTML" title)
(div :id (str "entry-state-" entry-id) :sx-swap-oob "innerHTML" state)))
(defcomp ~events-entry-options (&key entry-id buttons-html)
(defcomp ~events-entry-options (&key entry-id buttons)
(div :id (str "calendar_entry_options_" entry-id) :class "flex flex-col md:flex-row gap-1"
(raw! buttons-html)))
buttons))
(defcomp ~events-entry-option-button (&key url target csrf btn-type action-btn confirm-title confirm-text
label is-btn)
(form :hx-post url :hx-select target :hx-target target :hx-swap "outerHTML"
:hx-trigger (if is-btn "confirmed" nil)
(form :sx-post url :sx-select target :sx-target target :sx-swap "outerHTML"
:sx-trigger (if is-btn "confirmed" nil)
(input :type "hidden" :name "csrf_token" :value csrf)
(button :type btn-type :class action-btn
:data-confirm "true" :data-confirm-title confirm-title