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
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:
@@ -10,9 +10,7 @@
|
||||
(link :rel "stylesheet" :type "text/css" :href (str asset-url "/styles/basics.css"))
|
||||
(link :rel "stylesheet" :type "text/css" :href (str asset-url "/styles/cards.css"))
|
||||
(link :rel "stylesheet" :type "text/css" :href (str asset-url "/styles/blog-content.css"))
|
||||
(script :src "https://unpkg.com/htmx.org@2.0.8")
|
||||
(meta :name "htmx-config" :content "{\"selfRequestsOnly\":false}")
|
||||
(script :src "https://unpkg.com/hyperscript.org@0.9.12")
|
||||
(meta :name "csrf-token" :content "")
|
||||
(script :src "https://cdn.tailwindcss.com")
|
||||
(link :rel "stylesheet" :href (str asset-url "/fontawesome/css/all.min.css"))
|
||||
(link :rel "stylesheet" :href (str asset-url "/fontawesome/css/v4-shims.min.css"))
|
||||
@@ -33,7 +31,8 @@
|
||||
".clamp-3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}"
|
||||
".no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}"
|
||||
"details.group{overflow:hidden}details.group>summary{list-style:none}details.group>summary::-webkit-details-marker{display:none}"
|
||||
".htmx-indicator{display:none}.htmx-request .htmx-indicator{display:inline-flex}"
|
||||
".sx-indicator{display:none}.sx-request .sx-indicator{display:inline-flex}"
|
||||
".sx-error .sx-indicator{display:none}.sx-loading .sx-indicator{display:inline-flex}"
|
||||
".js-wrap.open .js-pop{display:block}.js-wrap.open .js-backdrop{display:block}")))
|
||||
|
||||
(defcomp ~app-layout (&key title asset-url meta-html menu-colour
|
||||
@@ -55,7 +54,7 @@
|
||||
:class (str "flex items-start gap-2 p-1 bg-" colour "-500")
|
||||
(div :class "flex flex-col w-full items-center"
|
||||
(when header-rows-html (raw! header-rows-html))))))
|
||||
(div :id "root-menu" :hx-swap-oob "outerHTML" :class "md:hidden"
|
||||
(div :id "root-menu" :sx-swap-oob "outerHTML" :class "md:hidden"
|
||||
(when menu-html (raw! menu-html)))))
|
||||
(div :id "filter"
|
||||
(when filter-html (raw! filter-html)))
|
||||
@@ -73,20 +72,60 @@
|
||||
(script :src (str asset-url "/scripts/sexp.js"))
|
||||
(script :src (str asset-url "/scripts/body.js")))))))
|
||||
|
||||
(defcomp ~app-body (&key header-rows filter aside menu content)
|
||||
(div :class "max-w-screen-2xl mx-auto py-1 px-1"
|
||||
(div :class "w-full"
|
||||
(details :class "group/root p-2" :data-toggle-group "mobile-panels"
|
||||
(summary
|
||||
(header :class "z-50"
|
||||
(div :id "root-header-summary"
|
||||
:class "flex items-start gap-2 p-1 bg-sky-500"
|
||||
(div :class "flex flex-col w-full items-center"
|
||||
(when header-rows header-rows)))))
|
||||
(div :id "root-menu" :sx-swap-oob "outerHTML" :class "md:hidden"
|
||||
(when menu menu))))
|
||||
(div :id "filter"
|
||||
(when filter filter))
|
||||
(main :id "root-panel" :class "max-w-full"
|
||||
(div :class "md:min-h-0"
|
||||
(div :class "flex flex-row md:h-full md:min-h-0"
|
||||
(aside :id "aside"
|
||||
:class "hidden md:flex md:flex-col max-w-xs md:h-full md:min-h-0 mr-3"
|
||||
(when aside aside))
|
||||
(section :id "main-panel"
|
||||
:class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"
|
||||
(when content content)
|
||||
(div :class "pb-8")))))))
|
||||
|
||||
(defcomp ~oob-response (&key oobs-html filter-html aside-html menu-html content-html)
|
||||
(<>
|
||||
(when oobs-html (raw! oobs-html))
|
||||
(div :id "filter" :hx-swap-oob "outerHTML"
|
||||
(div :id "filter" :sx-swap-oob "outerHTML"
|
||||
(when filter-html (raw! filter-html)))
|
||||
(aside :id "aside" :hx-swap-oob "outerHTML"
|
||||
(aside :id "aside" :sx-swap-oob "outerHTML"
|
||||
:class "hidden md:flex md:flex-col max-w-xs md:h-full md:min-h-0 mr-3"
|
||||
(when aside-html (raw! aside-html)))
|
||||
(div :id "root-menu" :hx-swap-oob "outerHTML" :class "md:hidden"
|
||||
(div :id "root-menu" :sx-swap-oob "outerHTML" :class "md:hidden"
|
||||
(when menu-html (raw! menu-html)))
|
||||
(section :id "main-panel"
|
||||
:class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"
|
||||
(when content-html (raw! content-html)))))
|
||||
|
||||
;; Sexp-native OOB response — accepts nested sexp expressions, no raw!
|
||||
(defcomp ~oob-sexp (&key oobs filter aside menu content)
|
||||
(<>
|
||||
(when oobs oobs)
|
||||
(div :id "filter" :sx-swap-oob "outerHTML"
|
||||
(when filter filter))
|
||||
(aside :id "aside" :sx-swap-oob "outerHTML"
|
||||
:class "hidden md:flex md:flex-col max-w-xs md:h-full md:min-h-0 mr-3"
|
||||
(when aside aside))
|
||||
(div :id "root-menu" :sx-swap-oob "outerHTML" :class "md:hidden"
|
||||
(when menu menu))
|
||||
(section :id "main-panel"
|
||||
:class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"
|
||||
(when content content))))
|
||||
|
||||
(defcomp ~hamburger ()
|
||||
(div :class "md:hidden bg-stone-200 rounded"
|
||||
(svg :class "h-12 w-12 transition-transform group-open/root:hidden block self-start"
|
||||
@@ -102,7 +141,7 @@
|
||||
settings-url is-admin oob)
|
||||
(<>
|
||||
(div :id "root-row"
|
||||
:hx-swap-oob (if oob "outerHTML" nil)
|
||||
:sx-swap-oob (if oob "outerHTML" nil)
|
||||
:class "flex flex-col items-center md:flex-row justify-center md:justify-between w-full p-1 bg-sky-500"
|
||||
(div :class "w-full flex flex-row items-top"
|
||||
(when cart-mini-html (raw! cart-mini-html))
|
||||
@@ -127,15 +166,15 @@
|
||||
(shade (str (- 500 (* lv 100)))))
|
||||
(<>
|
||||
(div :id id
|
||||
:hx-swap-oob (if oob "outerHTML" nil)
|
||||
:sx-swap-oob (if oob "outerHTML" nil)
|
||||
:class (str "flex flex-col items-center md:flex-row justify-center md:justify-between w-full p-1 bg-" c "-" shade)
|
||||
(div :class "relative nav-group"
|
||||
(a :href link-href
|
||||
:hx-get (if external nil link-href)
|
||||
:hx-target (if external nil "#main-panel")
|
||||
:hx-select (if external nil (or hx-select "#main-panel"))
|
||||
:hx-swap (if external nil "outerHTML")
|
||||
:hx-push-url (if external nil "true")
|
||||
:sx-get (if external nil link-href)
|
||||
:sx-target (if external nil "#main-panel")
|
||||
:sx-select (if external nil (or hx-select "#main-panel"))
|
||||
:sx-swap (if external nil "outerHTML")
|
||||
:sx-push-url (if external nil "true")
|
||||
:class "w-full whitespace-normal flex items-center gap-2 font-bold text-2xl px-3 py-2"
|
||||
(when icon (i :class icon :aria-hidden "true"))
|
||||
(if link-label-html (raw! link-label-html)
|
||||
@@ -158,13 +197,75 @@
|
||||
(span count)))
|
||||
|
||||
(defcomp ~oob-header (&key parent-id child-id row-html)
|
||||
(div :id parent-id :hx-swap-oob "outerHTML" :class "w-full"
|
||||
(div :id parent-id :sx-swap-oob "outerHTML" :class "w-full"
|
||||
(div :class "w-full" (raw! row-html)
|
||||
(div :id child-id))))
|
||||
|
||||
(defcomp ~header-child (&key id inner-html)
|
||||
(div :id (or id "root-header-child") :class "w-full" (raw! inner-html)))
|
||||
|
||||
;; Sexp-native header-row — accepts nested sexp expressions, no raw!
|
||||
(defcomp ~header-row-sx (&key cart-mini blog-url site-title app-label
|
||||
nav-tree auth-menu nav-panel
|
||||
settings-url is-admin oob)
|
||||
(<>
|
||||
(div :id "root-row"
|
||||
:sx-swap-oob (if oob "outerHTML" nil)
|
||||
:class "flex flex-col items-center md:flex-row justify-center md:justify-between w-full p-1 bg-sky-500"
|
||||
(div :class "w-full flex flex-row items-top"
|
||||
(when cart-mini cart-mini)
|
||||
(div :class "font-bold text-5xl flex-1"
|
||||
(a :href (or blog-url "/") :class "flex justify-center md:justify-start items-baseline gap-2"
|
||||
(h1 (or site-title ""))))
|
||||
(nav :class "hidden md:flex gap-4 text-sm ml-2 justify-end items-center flex-0"
|
||||
(when nav-tree nav-tree)
|
||||
(when auth-menu auth-menu)
|
||||
(when nav-panel nav-panel)
|
||||
(when (and is-admin settings-url)
|
||||
(a :href settings-url :class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3"
|
||||
(i :class "fa fa-cog" :aria-hidden "true"))))
|
||||
(~hamburger)))
|
||||
(div :class "block md:hidden text-md font-bold"
|
||||
(when auth-menu auth-menu))))
|
||||
|
||||
;; Sexp-native menu-row — accepts nested sexp expressions, no raw!
|
||||
(defcomp ~menu-row-sx (&key id level colour link-href link-label link-label-content icon
|
||||
hx-select nav child-id child oob external)
|
||||
(let* ((c (or colour "sky"))
|
||||
(lv (or level 1))
|
||||
(shade (str (- 500 (* lv 100)))))
|
||||
(<>
|
||||
(div :id id
|
||||
:sx-swap-oob (if oob "outerHTML" nil)
|
||||
:class (str "flex flex-col items-center md:flex-row justify-center md:justify-between w-full p-1 bg-" c "-" shade)
|
||||
(div :class "relative nav-group"
|
||||
(a :href link-href
|
||||
:sx-get (if external nil link-href)
|
||||
:sx-target (if external nil "#main-panel")
|
||||
:sx-select (if external nil (or hx-select "#main-panel"))
|
||||
:sx-swap (if external nil "outerHTML")
|
||||
:sx-push-url (if external nil "true")
|
||||
:class "w-full whitespace-normal flex items-center gap-2 font-bold text-2xl px-3 py-2"
|
||||
(when icon (i :class icon :aria-hidden "true"))
|
||||
(if link-label-content link-label-content
|
||||
(when link-label (div link-label)))))
|
||||
(when nav
|
||||
(nav :class "hidden md:flex gap-4 text-sm ml-2 justify-end items-center flex-0"
|
||||
nav)))
|
||||
(when (and child-id (not oob))
|
||||
(div :id child-id :class "flex flex-col w-full items-center"
|
||||
(when child child))))))
|
||||
|
||||
;; Sexp-native oob-header — accepts nested sexp expression, no raw!
|
||||
(defcomp ~oob-header-sx (&key parent-id child-id row)
|
||||
(div :id parent-id :sx-swap-oob "outerHTML" :class "w-full"
|
||||
(div :class "w-full" row
|
||||
(div :id child-id))))
|
||||
|
||||
;; Sexp-native header-child — accepts nested sexp expression, no raw!
|
||||
(defcomp ~header-child-sx (&key id inner)
|
||||
(div :id (or id "root-header-child") :class "w-full" inner))
|
||||
|
||||
(defcomp ~error-content (&key errnum message image)
|
||||
(div :class "text-center p-8 max-w-lg mx-auto"
|
||||
(div :class "font-bold text-2xl md:text-4xl text-red-500 mb-4" errnum)
|
||||
@@ -176,11 +277,11 @@
|
||||
(defcomp ~nav-link (&key href hx-select label icon aclass select-colours is-selected)
|
||||
(div :class "relative nav-group"
|
||||
(a :href href
|
||||
:hx-get href
|
||||
:hx-target "#main-panel"
|
||||
:hx-select (or hx-select "#main-panel")
|
||||
:hx-swap "outerHTML"
|
||||
:hx-push-url "true"
|
||||
:sx-get href
|
||||
:sx-target "#main-panel"
|
||||
:sx-select (or hx-select "#main-panel")
|
||||
:sx-swap "outerHTML"
|
||||
:sx-push-url "true"
|
||||
:aria-selected (when is-selected "true")
|
||||
:class (or aclass
|
||||
(str "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 "
|
||||
|
||||
Reference in New Issue
Block a user