Send all responses as sexp wire format with client-side rendering
- 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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user