Externalize sexp component templates and delete redundant HTML fragments
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m13s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m13s
Move 24 defcomp definitions from Python string constants in components.py to 7 grouped .sexp files under shared/sexp/templates/. Add load_sexp_dir() to jinja_bridge.py for file-based loading. Migrate events and market link-card fragment handlers from render_template to sexp. Delete 9 superseded Jinja HTML fragment templates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
44
shared/sexp/templates/cards.sexp
Normal file
44
shared/sexp/templates/cards.sexp
Normal file
@@ -0,0 +1,44 @@
|
||||
(defcomp ~post-card (&key title slug href feature-image excerpt
|
||||
status published-at updated-at publish-requested
|
||||
hx-select like-html widgets-html at-bar-html)
|
||||
(article :class "border-b pb-6 last:border-b-0 relative"
|
||||
(when like-html (raw! like-html))
|
||||
(a :href href
|
||||
:hx-get href
|
||||
:hx-target "#main-panel"
|
||||
:hx-select hx-select
|
||||
:hx-swap "outerHTML"
|
||||
:hx-push-url "true"
|
||||
:class "block rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden"
|
||||
(header :class "mb-2 text-center"
|
||||
(h2 :class "text-4xl font-bold text-stone-900" title)
|
||||
(cond
|
||||
(= status "draft")
|
||||
(begin
|
||||
(div :class "flex justify-center gap-2 mt-1"
|
||||
(span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-800" "Draft")
|
||||
(when publish-requested
|
||||
(span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800" "Publish requested")))
|
||||
(when updated-at
|
||||
(p :class "text-sm text-stone-500" (str "Updated: " updated-at))))
|
||||
published-at
|
||||
(p :class "text-sm text-stone-500" (str "Published: " published-at))))
|
||||
(when feature-image
|
||||
(div :class "mb-4"
|
||||
(img :src feature-image :alt "" :class "rounded-lg w-full object-cover")))
|
||||
(when excerpt
|
||||
(p :class "text-stone-700 text-lg leading-relaxed text-center" excerpt)))
|
||||
(when widgets-html (raw! widgets-html))
|
||||
(when at-bar-html (raw! at-bar-html))))
|
||||
|
||||
(defcomp ~order-summary-card (&key order-id created-at description status currency total-amount)
|
||||
(div :class "rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6 space-y-2 text-xs sm:text-sm text-stone-800"
|
||||
(p (span :class "font-medium" "Order ID:") " " (span :class "font-mono" (str "#" order-id)))
|
||||
(p (span :class "font-medium" "Created:") " " (or created-at "\u2014"))
|
||||
(p (span :class "font-medium" "Description:") " " (or description "\u2013"))
|
||||
(p (span :class "font-medium" "Status:") " " (~status-pill :status (or status "pending") :size "[11px]"))
|
||||
(p (span :class "font-medium" "Currency:") " " (or currency "GBP"))
|
||||
(p (span :class "font-medium" "Total:") " "
|
||||
(if total-amount
|
||||
(str (or currency "GBP") " " total-amount)
|
||||
"\u2013"))))
|
||||
126
shared/sexp/templates/controls.sexp
Normal file
126
shared/sexp/templates/controls.sexp
Normal file
@@ -0,0 +1,126 @@
|
||||
(defcomp ~search-mobile (&key current-local-href search search-count hx-select search-headers-mobile)
|
||||
(div :id "search-mobile-wrapper"
|
||||
:class "flex flex-row gap-2 items-center flex-1 min-w-0 pr-2"
|
||||
(input :id "search-mobile"
|
||||
:type "text" :name "search" :aria-label "search"
|
||||
:class "text-base md:text-sm col-span-5 rounded-md px-3 py-2 mb-2 w-full min-w-0 max-w-full border-2 border-stone-200 placeholder-shown:border-stone-200 [&:not(:placeholder-shown)]:border-yellow-200"
|
||||
:hx-preserve true
|
||||
:value (or search "")
|
||||
:placeholder "search"
|
||||
:hx-trigger "input changed delay:300ms"
|
||||
:hx-target "#main-panel"
|
||||
:hx-select (str (or hx-select "#main-panel") ", #search-mobile-wrapper, #search-desktop-wrapper")
|
||||
:hx-get current-local-href
|
||||
:hx-swap "outerHTML"
|
||||
:hx-push-url "true"
|
||||
:hx-headers search-headers-mobile
|
||||
:hx-sync "this:replace"
|
||||
:autocomplete "off")
|
||||
(div :id "search-count-mobile" :aria-label "search count"
|
||||
:class (if (not search-count) "text-xl text-red-500" "")
|
||||
(when search (raw! (str search-count))))))
|
||||
|
||||
(defcomp ~search-desktop (&key current-local-href search search-count hx-select search-headers-desktop)
|
||||
(div :id "search-desktop-wrapper"
|
||||
:class "flex flex-row gap-2 items-center"
|
||||
(input :id "search-desktop"
|
||||
:type "text" :name "search" :aria-label "search"
|
||||
:class "w-full mx-1 my-3 px-3 py-2 text-md rounded-xl border-2 shadow-sm border-white placeholder-shown:border-white [&:not(:placeholder-shown)]:border-yellow-200"
|
||||
:hx-preserve true
|
||||
:value (or search "")
|
||||
:placeholder "search"
|
||||
:hx-trigger "input changed delay:300ms"
|
||||
:hx-target "#main-panel"
|
||||
:hx-select (str (or hx-select "#main-panel") ", #search-mobile-wrapper, #search-desktop-wrapper")
|
||||
:hx-get current-local-href
|
||||
:hx-swap "outerHTML"
|
||||
:hx-push-url "true"
|
||||
:hx-headers search-headers-desktop
|
||||
:hx-sync "this:replace"
|
||||
:autocomplete "off")
|
||||
(div :id "search-count-desktop" :aria-label "search count"
|
||||
:class (if (not search-count) "text-xl text-red-500" "")
|
||||
(when search (raw! (str search-count))))))
|
||||
|
||||
(defcomp ~mobile-filter (&key filter-summary-html action-buttons-html filter-details-html)
|
||||
(details :class "group/filter p-2 md:hidden" :data-toggle-group "mobile-panels"
|
||||
(summary :class "bg-white/90"
|
||||
(div :class "flex flex-row items-start"
|
||||
(div
|
||||
(div :class "md:hidden mx-2 bg-stone-200 rounded"
|
||||
(span :class "flex items-center justify-center text-stone-600 text-lg h-12 w-12 transition-transform group-open/filter:hidden self-start"
|
||||
(i :class "fa-solid fa-filter"))
|
||||
(span
|
||||
(svg :aria-hidden "true" :viewBox "0 0 24 24"
|
||||
:class "w-12 h-12 rotate-180 transition-transform group-open/filter:block hidden self-start"
|
||||
(path :d "M6 9l6 6 6-6" :fill "currentColor")))))
|
||||
(div :id "filter-summary-mobile"
|
||||
:class "flex-1 md:hidden grid grid-cols-12 items-center gap-3"
|
||||
(div :class "flex flex-col items-start gap-2"
|
||||
(raw! filter-summary-html)))))
|
||||
(raw! (or action-buttons-html ""))
|
||||
(div :id "filter-details-mobile" :style "display:contents"
|
||||
(raw! (or filter-details-html "")))))
|
||||
|
||||
(defcomp ~infinite-scroll (&key url page total-pages id-prefix colspan)
|
||||
(if (< page total-pages)
|
||||
(raw! (str
|
||||
"<tr id=\"" id-prefix "-sentinel-" page "\""
|
||||
" hx-get=\"" url "\""
|
||||
" hx-trigger=\"intersect once delay:250ms, sentinel:retry\""
|
||||
" hx-swap=\"outerHTML\""
|
||||
" _=\""
|
||||
"init "
|
||||
"if not me.dataset.retryMs then set me.dataset.retryMs to 1000 end "
|
||||
"on sentinel:retry "
|
||||
"remove .hidden from .js-loading in me "
|
||||
"add .hidden to .js-neterr in me "
|
||||
"set me.style.pointerEvents to 'none' "
|
||||
"set me.style.opacity to '0' "
|
||||
"trigger htmx:consume on me "
|
||||
"call htmx.trigger(me, 'intersect') "
|
||||
"end "
|
||||
"def backoff() "
|
||||
"add .hidden to .js-loading in me "
|
||||
"remove .hidden from .js-neterr in me "
|
||||
"set myMs to Number(me.dataset.retryMs) "
|
||||
"if myMs < 10000 then set me.dataset.retryMs to myMs * 2 end "
|
||||
"js setTimeout(() => htmx.trigger(me, 'sentinel:retry'), myMs) "
|
||||
"end "
|
||||
"on htmx:beforeRequest "
|
||||
"set me.style.pointerEvents to 'none' "
|
||||
"set me.style.opacity to '0' "
|
||||
"end "
|
||||
"on htmx:afterSwap set me.dataset.retryMs to 1000 end "
|
||||
"on htmx:sendError call backoff() "
|
||||
"on htmx:responseError call backoff() "
|
||||
"on htmx:timeout call backoff()"
|
||||
"\""
|
||||
" role=\"status\" aria-live=\"polite\" aria-hidden=\"true\">"
|
||||
"<td colspan=\"" colspan "\" class=\"px-3 py-4\">"
|
||||
"<div class=\"block md:hidden h-[60vh] js-mobile-sentinel\">"
|
||||
"<div class=\"js-loading text-center text-xs text-stone-400\">loading\u2026 " page " / " total-pages "</div>"
|
||||
"<div class=\"js-neterr hidden flex h-full items-center justify-center\"></div>"
|
||||
"</div>"
|
||||
"<div class=\"hidden md:block h-[30vh] js-desktop-sentinel\">"
|
||||
"<div class=\"js-loading text-center text-xs text-stone-400\">loading\u2026 " page " / " total-pages "</div>"
|
||||
"<div class=\"js-neterr hidden inset-0 grid place-items-center p-4\"></div>"
|
||||
"</div>"
|
||||
"</td></tr>"))
|
||||
(raw! (str
|
||||
"<tr><td colspan=\"" colspan "\" class=\"px-3 py-4 text-center text-xs text-stone-400\">End of results</td></tr>"))))
|
||||
|
||||
(defcomp ~status-pill (&key status size)
|
||||
(let* ((s (or status "pending"))
|
||||
(lower (lower s))
|
||||
(sz (or size "xs"))
|
||||
(colours (cond
|
||||
(= lower "paid") "border-emerald-300 bg-emerald-50 text-emerald-700"
|
||||
(= lower "confirmed") "border-emerald-300 bg-emerald-50 text-emerald-700"
|
||||
(= lower "checked_in") "border-blue-300 bg-blue-50 text-blue-700"
|
||||
(or (= lower "failed") (= lower "cancelled")) "border-rose-300 bg-rose-50 text-rose-700"
|
||||
(= lower "provisional") "border-amber-300 bg-amber-50 text-amber-700"
|
||||
(= lower "ordered") "border-blue-300 bg-blue-50 text-blue-700"
|
||||
true "border-stone-300 bg-stone-50 text-stone-700")))
|
||||
(span :class (str "inline-flex items-center rounded-full border px-2 py-0.5 text-" sz " font-medium " colours)
|
||||
s)))
|
||||
62
shared/sexp/templates/fragments.sexp
Normal file
62
shared/sexp/templates/fragments.sexp
Normal file
@@ -0,0 +1,62 @@
|
||||
(defcomp ~link-card (&key link title image icon subtitle detail data-app)
|
||||
(a :href link
|
||||
:class "block rounded border border-stone-200 bg-white hover:bg-stone-50 transition-colors no-underline"
|
||||
:data-fragment "link-card"
|
||||
:data-app data-app
|
||||
:data-hx-disable true
|
||||
(div :class "flex flex-row items-start gap-3 p-3"
|
||||
(if image
|
||||
(img :src image :alt "" :class "flex-shrink-0 w-16 h-16 rounded object-cover")
|
||||
(div :class "flex-shrink-0 w-16 h-16 rounded bg-stone-100 flex items-center justify-center text-stone-400"
|
||||
(i :class icon)))
|
||||
(div :class "flex-1 min-w-0"
|
||||
(div :class "font-medium text-stone-900 text-sm clamp-2" title)
|
||||
(when subtitle
|
||||
(div :class "text-xs text-stone-500 mt-0.5" subtitle))
|
||||
(when detail
|
||||
(div :class "text-xs text-stone-400 mt-1" detail))))))
|
||||
|
||||
(defcomp ~cart-mini (&key cart-count blog-url cart-url oob)
|
||||
(div :id "cart-mini"
|
||||
:hx-swap-oob oob
|
||||
(if (= cart-count 0)
|
||||
(div :class "h-12 w-12 rounded-full overflow-hidden border border-stone-300 flex-shrink-0"
|
||||
(a :href blog-url
|
||||
:class "h-full w-full font-bold text-5xl flex-shrink-0 flex flex-row items-center gap-1"
|
||||
(img :src (str blog-url "static/img/logo.jpg")
|
||||
:class "h-full w-full rounded-full object-cover border border-stone-300 flex-shrink-0")))
|
||||
(a :href cart-url
|
||||
:class "relative inline-flex items-center justify-center text-stone-700 hover:text-emerald-700"
|
||||
(i :class "fa fa-shopping-cart text-5xl" :aria-hidden "true")
|
||||
(span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full bg-emerald-600 text-white text-sm w-5 h-5"
|
||||
cart-count)))))
|
||||
|
||||
(defcomp ~auth-menu (&key user-email account-url)
|
||||
(<>
|
||||
(span :id "auth-menu-desktop" :class "hidden md:inline-flex"
|
||||
(if user-email
|
||||
(a :href account-url
|
||||
:class "justify-center cursor-pointer flex flex-row items-center p-3 gap-2 rounded bg-stone-200 text-black"
|
||||
:data-close-details true
|
||||
(i :class "fa-solid fa-user")
|
||||
(span user-email))
|
||||
(a :href account-url
|
||||
:class "justify-center cursor-pointer flex flex-row items-center p-3 gap-2 rounded bg-stone-200 text-black"
|
||||
:data-close-details true
|
||||
(i :class "fa-solid fa-key")
|
||||
(span "sign in or register"))))
|
||||
(span :id "auth-menu-mobile" :class "block md:hidden text-md font-bold"
|
||||
(if user-email
|
||||
(a :href account-url :data-close-details true
|
||||
(i :class "fa-solid fa-user")
|
||||
(span user-email))
|
||||
(a :href account-url
|
||||
(i :class "fa-solid fa-key")
|
||||
(span "sign in or register"))))))
|
||||
|
||||
(defcomp ~account-nav-item (&key href label)
|
||||
(div :class "relative nav-group"
|
||||
(a :href href
|
||||
:class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3"
|
||||
:data-hx-disable true
|
||||
label)))
|
||||
164
shared/sexp/templates/layout.sexp
Normal file
164
shared/sexp/templates/layout.sexp
Normal file
@@ -0,0 +1,164 @@
|
||||
(defcomp ~app-shell (&key title asset-url meta-html body-html body-end-html)
|
||||
(<>
|
||||
(raw! "<!doctype html>")
|
||||
(html :lang "en"
|
||||
(head
|
||||
(meta :charset "utf-8")
|
||||
(meta :name "viewport" :content "width=device-width, initial-scale=1")
|
||||
(meta :name "robots" :content "index,follow")
|
||||
(meta :name "theme-color" :content "#ffffff")
|
||||
(title title)
|
||||
(when meta-html (raw! meta-html))
|
||||
(style "@media (min-width: 768px) { .js-mobile-sentinel { display:none !important; } }")
|
||||
(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")
|
||||
(script :src "https://unpkg.com/hyperscript.org@0.9.12")
|
||||
(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"))
|
||||
(link :href "https://unpkg.com/prismjs/themes/prism.css" :rel "stylesheet")
|
||||
(script :src "https://unpkg.com/prismjs/prism.js")
|
||||
(script :src "https://unpkg.com/prismjs/components/prism-javascript.min.js")
|
||||
(script :src "https://unpkg.com/prismjs/components/prism-python.min.js")
|
||||
(script :src "https://unpkg.com/prismjs/components/prism-bash.min.js")
|
||||
(script :src "https://cdn.jsdelivr.net/npm/sweetalert2@11")
|
||||
(script "if(matchMedia('(hover:hover) and (pointer:fine)').matches){document.documentElement.classList.add('hover-capable')}")
|
||||
(script "document.addEventListener('click',function(e){var t=e.target.closest('[data-close-details]');if(!t)return;var d=t.closest('details');if(d)d.removeAttribute('open')})")
|
||||
(style
|
||||
"details[data-toggle-group=\"mobile-panels\"]>summary{list-style:none}"
|
||||
"details[data-toggle-group=\"mobile-panels\"]>summary::-webkit-details-marker{display:none}"
|
||||
"@media(min-width:768px){.nav-group:focus-within .submenu,.nav-group:hover .submenu{display:block}}"
|
||||
"img{max-width:100%;height:auto}"
|
||||
".clamp-2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}"
|
||||
".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}"
|
||||
".js-wrap.open .js-pop{display:block}.js-wrap.open .js-backdrop{display:block}"))
|
||||
(body :class "bg-stone-50 text-stone-900"
|
||||
(raw! body-html)
|
||||
(when body-end-html (raw! body-end-html))
|
||||
(script :src (str asset-url "/scripts/body.js"))))))
|
||||
|
||||
(defcomp ~app-layout (&key title asset-url meta-html menu-colour
|
||||
header-rows-html menu-html
|
||||
filter-html aside-html content-html
|
||||
body-end-html)
|
||||
(let* ((colour (or menu-colour "sky")))
|
||||
(~app-shell :title (or title "Rose Ash") :asset-url asset-url
|
||||
:meta-html meta-html :body-end-html body-end-html
|
||||
:body-html (str
|
||||
"<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-" colour "-500\">"
|
||||
"<div class=\"flex flex-col w-full items-center\">"
|
||||
header-rows-html
|
||||
"</div>"
|
||||
"</div>"
|
||||
"</header>"
|
||||
"</summary>"
|
||||
"<div id=\"root-menu\" hx-swap-oob=\"outerHTML\" class=\"md:hidden\">"
|
||||
(or menu-html "")
|
||||
"</div>"
|
||||
"</details>"
|
||||
"</div>"
|
||||
"<div id=\"filter\">"
|
||||
(or filter-html "")
|
||||
"</div>"
|
||||
"<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\">"
|
||||
(or aside-html "")
|
||||
"</aside>"
|
||||
"<section id=\"main-panel\" class=\"flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport\">"
|
||||
(or content-html "")
|
||||
"<div class=\"pb-8\"></div>"
|
||||
"</section>"
|
||||
"</div>"
|
||||
"</div>"
|
||||
"</main>"
|
||||
"</div>"))))
|
||||
|
||||
(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"
|
||||
(when filter-html (raw! filter-html)))
|
||||
(aside :id "aside" :hx-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"
|
||||
(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)))))
|
||||
|
||||
(defcomp ~header-row (&key cart-mini-html blog-url site-title
|
||||
nav-tree-html auth-menu-html nav-panel-html
|
||||
settings-url is-admin oob hamburger-html)
|
||||
(<>
|
||||
(div :id "root-row"
|
||||
:hx-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))
|
||||
(div :class "font-bold text-5xl flex-1"
|
||||
(a :href (or blog-url "/") :class "flex justify-center md:justify-start"
|
||||
(h1 (or site-title ""))))
|
||||
(nav :class "hidden md:flex gap-4 text-sm ml-2 justify-end items-center flex-0"
|
||||
(when nav-tree-html (raw! nav-tree-html))
|
||||
(when auth-menu-html (raw! auth-menu-html))
|
||||
(when nav-panel-html (raw! nav-panel-html))
|
||||
(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"))))
|
||||
(when hamburger-html (raw! hamburger-html))))
|
||||
(div :class "block md:hidden text-md font-bold"
|
||||
(when auth-menu-html (raw! auth-menu-html)))))
|
||||
|
||||
(defcomp ~menu-row (&key id level colour link-href link-label link-label-html icon
|
||||
hx-select nav-html child-id child-html oob)
|
||||
(let* ((c (or colour "sky"))
|
||||
(lv (or level 1))
|
||||
(shade (str (- 500 (* lv 100)))))
|
||||
(<>
|
||||
(div :id id
|
||||
:hx-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 link-href
|
||||
:hx-target "#main-panel"
|
||||
:hx-select (or hx-select "#main-panel")
|
||||
:hx-swap "outerHTML"
|
||||
:hx-push-url "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)
|
||||
(when link-label (div link-label)))))
|
||||
(when nav-html
|
||||
(nav :class "hidden md:flex gap-4 text-sm ml-2 justify-end items-center flex-0"
|
||||
(raw! nav-html))))
|
||||
(when child-id
|
||||
(div :id child-id :class "flex flex-col w-full items-center"
|
||||
(when child-html (raw! child-html)))))))
|
||||
|
||||
(defcomp ~nav-link (&key href hx-select label icon aclass select-colours)
|
||||
(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"
|
||||
:class (or aclass
|
||||
(str "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 "
|
||||
(or select-colours "")))
|
||||
(when icon (i :class icon :aria-hidden "true"))
|
||||
(when label (span label)))))
|
||||
24
shared/sexp/templates/navigation.sexp
Normal file
24
shared/sexp/templates/navigation.sexp
Normal file
@@ -0,0 +1,24 @@
|
||||
(defcomp ~calendar-entry-nav (&key href name date-str nav-class)
|
||||
(a :href href :class nav-class
|
||||
(div :class "w-8 h-8 rounded bg-stone-200 flex-shrink-0")
|
||||
(div :class "flex-1 min-w-0"
|
||||
(div :class "font-medium truncate" name)
|
||||
(div :class "text-xs text-stone-600 truncate" date-str))))
|
||||
|
||||
(defcomp ~calendar-link-nav (&key href name nav-class)
|
||||
(a :href href :class nav-class
|
||||
(i :class "fa fa-calendar" :aria-hidden "true")
|
||||
(div name)))
|
||||
|
||||
(defcomp ~market-link-nav (&key href name nav-class)
|
||||
(a :href href :class nav-class
|
||||
(i :class "fa fa-shopping-bag" :aria-hidden "true")
|
||||
(div name)))
|
||||
|
||||
(defcomp ~relation-nav (&key href name icon nav-class relation-type)
|
||||
(a :href href :class (or nav-class "flex items-center gap-3 rounded-lg py-2 px-3 text-sm text-stone-700 hover:bg-stone-100 transition-colors")
|
||||
(when icon
|
||||
(div :class "w-8 h-8 rounded bg-stone-200 flex items-center justify-center flex-shrink-0"
|
||||
(i :class icon :aria-hidden "true")))
|
||||
(div :class "flex-1 min-w-0"
|
||||
(div :class "font-medium truncate" name))))
|
||||
25
shared/sexp/templates/pages.sexp
Normal file
25
shared/sexp/templates/pages.sexp
Normal file
@@ -0,0 +1,25 @@
|
||||
(defcomp ~base-shell (&key title asset-url &rest children)
|
||||
(<>
|
||||
(raw! "<!doctype html>")
|
||||
(html :lang "en"
|
||||
(head
|
||||
(meta :charset "utf-8")
|
||||
(meta :name "viewport" :content "width=device-width, initial-scale=1")
|
||||
(title title)
|
||||
(style
|
||||
"body{margin:0;min-height:100vh;display:flex;align-items:center;"
|
||||
"justify-content:center;font-family:system-ui,sans-serif;"
|
||||
"background:#fafaf9;color:#1c1917}")
|
||||
(script :src "https://cdn.tailwindcss.com")
|
||||
(link :rel "stylesheet" :href (str asset-url "/fontawesome/css/all.min.css")))
|
||||
(body :class "bg-stone-50 text-stone-900"
|
||||
children))))
|
||||
|
||||
(defcomp ~error-page (&key title message image asset-url)
|
||||
(~base-shell :title title :asset-url asset-url
|
||||
(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"
|
||||
(div message))
|
||||
(when image
|
||||
(div :class "flex justify-center"
|
||||
(img :src image :width "300" :height "300"))))))
|
||||
15
shared/sexp/templates/relations.sexp
Normal file
15
shared/sexp/templates/relations.sexp
Normal file
@@ -0,0 +1,15 @@
|
||||
(defcomp ~relation-attach (&key create-url label icon)
|
||||
(a :href create-url
|
||||
:hx-get create-url
|
||||
:hx-target "#main-panel"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-push-url "true"
|
||||
:class "flex items-center gap-2 text-sm p-2 rounded hover:bg-stone-100 text-stone-500 hover:text-stone-700 transition-colors"
|
||||
(when icon (i :class icon))
|
||||
(span (or label "Add"))))
|
||||
|
||||
(defcomp ~relation-detach (&key detach-url name)
|
||||
(button :hx-delete detach-url
|
||||
:hx-confirm (str "Remove " (or name "this item") "?")
|
||||
:class "text-red-500 hover:text-red-700 text-sm p-1 rounded hover:bg-red-50 transition-colors"
|
||||
(i :class "fa fa-times" :aria-hidden "true")))
|
||||
Reference in New Issue
Block a user