""" Shared s-expression component definitions. Loaded at app startup via ``load_shared_components()``. Each component replaces a per-service Jinja fragment template with a single reusable s-expression definition. """ from __future__ import annotations from .jinja_bridge import register_components def load_shared_components() -> None: """Register all shared s-expression components.""" register_components(_LINK_CARD) register_components(_CART_MINI) register_components(_AUTH_MENU) register_components(_ACCOUNT_NAV_ITEM) register_components(_CALENDAR_ENTRY_NAV) register_components(_CALENDAR_LINK_NAV) register_components(_MARKET_LINK_NAV) register_components(_POST_CARD) register_components(_BASE_SHELL) register_components(_ERROR_PAGE) # Phase 6: layout infrastructure register_components(_APP_SHELL) register_components(_APP_LAYOUT) register_components(_OOB_RESPONSE) register_components(_HEADER_ROW) register_components(_MENU_ROW) register_components(_NAV_LINK) register_components(_INFINITE_SCROLL) register_components(_STATUS_PILL) register_components(_SEARCH_MOBILE) register_components(_SEARCH_DESKTOP) register_components(_ORDER_SUMMARY_CARD) # --------------------------------------------------------------------------- # ~link-card # --------------------------------------------------------------------------- # Replaces: blog/templates/fragments/link_card.html # market/templates/fragments/link_card.html # events/templates/fragments/link_card.html # federation/templates/fragments/link_card.html # artdag/l1/app/templates/fragments/link_card.html # # Usage: # sexp('(~link-card :link "/post/apple/" :title "Apple" :image "/img/a.jpg")') # sexp('(~link-card :link url :title title :icon "fas fa-file-alt")', **ctx) # --------------------------------------------------------------------------- _LINK_CARD = ''' (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)))))) ''' # --------------------------------------------------------------------------- # ~cart-mini # --------------------------------------------------------------------------- # Replaces: cart/templates/fragments/cart_mini.html # # Usage: # sexp('(~cart-mini :cart-count count :blog-url burl :cart-url curl)', # count=0, burl="https://blog.rose-ash.com", curl="https://cart.rose-ash.com") # --------------------------------------------------------------------------- _CART_MINI = ''' (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 (str 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 (str 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))))) ''' # --------------------------------------------------------------------------- # ~auth-menu # --------------------------------------------------------------------------- # Replaces: account/templates/fragments/auth_menu.html # # Usage: # sexp('(~auth-menu :user-email email :account-url aurl)', # email="user@example.com", aurl="https://account.rose-ash.com") # --------------------------------------------------------------------------- _AUTH_MENU = ''' (defcomp ~auth-menu (&key user-email account-url) (<> (span :id "auth-menu-desktop" :class "hidden md:inline-flex" (if user-email (a :href (str 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 (str 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 (str account-url "/") :data-close-details true (i :class "fa-solid fa-user") (span user-email)) (a :href (str account-url "/") (i :class "fa-solid fa-key") (span "sign in or register")))))) ''' # --------------------------------------------------------------------------- # ~account-nav-item # --------------------------------------------------------------------------- # Replaces: hardcoded HTML in cart/bp/fragments/routes.py # and orders/bp/fragments/routes.py # # Usage: # sexp('(~account-nav-item :href url :label "orders")', url=cart_url("/orders/")) # --------------------------------------------------------------------------- _ACCOUNT_NAV_ITEM = ''' (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))) ''' # --------------------------------------------------------------------------- # ~calendar-entry-nav # --------------------------------------------------------------------------- # Replaces: events/templates/fragments/container_nav_entries.html (per-entry) # # Usage: # sexp('(~calendar-entry-nav :href url :name name :date-str "Jan 15, 2026 at 14:00")', # url="/events/...", name="Workshop") # --------------------------------------------------------------------------- _CALENDAR_ENTRY_NAV = ''' (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)))) ''' # --------------------------------------------------------------------------- # ~calendar-link-nav # --------------------------------------------------------------------------- # Replaces: events/templates/fragments/container_nav_calendars.html (per-calendar) # # Usage: # sexp('(~calendar-link-nav :href url :name "My Calendar")', url="/events/...") # --------------------------------------------------------------------------- _CALENDAR_LINK_NAV = ''' (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))) ''' # --------------------------------------------------------------------------- # ~market-link-nav # --------------------------------------------------------------------------- # Replaces: market/templates/fragments/container_nav_markets.html (per-market) # # Usage: # sexp('(~market-link-nav :href url :name "Farm Shop")', url="/market/...") # --------------------------------------------------------------------------- _MARKET_LINK_NAV = ''' (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))) ''' # --------------------------------------------------------------------------- # ~post-card # --------------------------------------------------------------------------- # Replaces: blog/templates/_types/blog/_card.html # # A simplified s-expression version of the blog listing card. # The full card is complex (like buttons, card widgets, at_bar with tag/author # filtering). This component covers the core card structure; the at_bar and # card_widgets are passed as pre-rendered HTML via :at-bar-html and # :widgets-html kwargs for incremental migration. # # Usage: # sexp('(~post-card :title t :slug s :href h ...)', **ctx) # --------------------------------------------------------------------------- _POST_CARD = ''' (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)))) ''' # --------------------------------------------------------------------------- # ~base-shell — full HTML document wrapper # --------------------------------------------------------------------------- # Replaces: shared/browser/templates/_types/root/index.html (the shell) # # Usage: For full-page s-expression rendering (Step 4 proof of concept) # --------------------------------------------------------------------------- _BASE_SHELL = ''' (defcomp ~base-shell (&key title asset-url &rest children) (<> (raw! "") (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)))) ''' # --------------------------------------------------------------------------- # ~error-page — styled error page # --------------------------------------------------------------------------- # Replaces: shared/browser/templates/_types/root/exceptions/_.html # + base.html + 404/message.html + 404/img.html # # Usage: # sexp('(~error-page :title "Not Found" :message "NOT FOUND" :image img-url :asset-url aurl)', # img_url="/static/errors/404.gif", aurl="/static") # --------------------------------------------------------------------------- _ERROR_PAGE = ''' (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")))))) ''' # =================================================================== # Phase 6: Layout infrastructure components # =================================================================== # --------------------------------------------------------------------------- # ~app-shell — full HTML document with all required CSS/JS assets # --------------------------------------------------------------------------- # Replaces: _types/root/index.html ... shell # # This includes htmx, hyperscript, tailwind, fontawesome, prism, and # all shared CSS/JS. ``~base-shell`` remains the lightweight error-page # shell; ``~app-shell`` is for real app pages. # # Usage: # sexp('(~app-shell :title t :asset-url a :meta-html m :body-html b)', **ctx) # --------------------------------------------------------------------------- _APP_SHELL = r''' (defcomp ~app-shell (&key title asset-url meta-html body-html body-end-html) (<> (raw! "") (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")))))) ''' # --------------------------------------------------------------------------- # ~app-layout — page body layout (header + filter + aside + main-panel) # --------------------------------------------------------------------------- # Replaces: _types/root/index.html body structure # # The header uses a
/ pattern for mobile menu toggle. # All content sections are passed as pre-rendered HTML strings. # # Usage: # sexp('(~app-layout :title t :asset-url a :header-rows-html h # :menu-html m :filter-html f :aside-html a :content-html c)', **ctx) # --------------------------------------------------------------------------- _APP_LAYOUT = r''' (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 "
" "
" "
" "" "
" "
" "
" header-rows-html "
" "
" "
" "
" "
" (or menu-html "") "
" "
" "
" "
" (or filter-html "") "
" "
" "
" "
" "" "
" (or content-html "") "
" "
" "
" "
" "
" "
")))) ''' # --------------------------------------------------------------------------- # ~oob-response — HTMX OOB multi-target swap wrapper # --------------------------------------------------------------------------- # Replaces: oob_elements.html base template # # Each named region gets hx-swap-oob="outerHTML" on its wrapper div. # The oobs-html param contains any extra OOB elements (header row swaps). # # Usage: # sexp('(~oob-response :oobs-html oh :filter-html fh :aside-html ah # :menu-html mh :content-html ch)', **ctx) # --------------------------------------------------------------------------- _OOB_RESPONSE = ''' (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))))) ''' # --------------------------------------------------------------------------- # ~header-row — root header bar (cart-mini, title, nav-tree, auth-menu) # --------------------------------------------------------------------------- # Replaces: _types/root/header/_header.html header_row macro # # Usage: # sexp('(~header-row :cart-mini-html cm :blog-url bu :site-title st # :nav-tree-html nh :auth-menu-html ah :nav-panel-html np # :settings-url su :is-admin ia)', **ctx) # --------------------------------------------------------------------------- _HEADER_ROW = ''' (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 (str (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))))) ''' # --------------------------------------------------------------------------- # ~menu-row — section header row (wraps in colored bar) # --------------------------------------------------------------------------- # Replaces: macros/links.html menu_row macro # # Each nested header row gets a progressively lighter background. # The route handler passes the level (0-based depth after root). # # Usage: # sexp('(~menu-row :id "auth-row" :level 1 :colour "sky" # :link-href url :link-label "account" :icon "fa-solid fa-user" # :nav-html nh :child-id "auth-header-child" :child-html ch)', **ctx) # --------------------------------------------------------------------------- _MENU_ROW = ''' (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))))))) ''' # --------------------------------------------------------------------------- # ~nav-link — HTMX navigation link (replaces macros/links.html link macro) # --------------------------------------------------------------------------- _NAV_LINK = ''' (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))))) ''' # --------------------------------------------------------------------------- # ~infinite-scroll — pagination sentinel for table-based lists # --------------------------------------------------------------------------- # Replaces: sentinel pattern in _rows.html templates # # For table rows (orders, etc.): renders with intersection observer. # Uses hyperscript for retry with exponential backoff. # # Usage: # sexp('(~infinite-scroll :url next-url :page p :total-pages tp # :id-prefix "orders" :colspan 5)', **ctx) # --------------------------------------------------------------------------- _INFINITE_SCROLL = r''' (defcomp ~infinite-scroll (&key url page total-pages id-prefix colspan) (if (< page total-pages) (raw! (str " 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\">" "" "
" "
loading… " page " / " total-pages "
" "" "
" "
" "
loading… " page " / " total-pages "
" "" "
" "")) (raw! (str "End of results")))) ''' # --------------------------------------------------------------------------- # ~status-pill — colored status indicator # --------------------------------------------------------------------------- # Replaces: inline Jinja status pill patterns across templates # # Usage: # sexp('(~status-pill :status s :size "sm")', status="paid") # --------------------------------------------------------------------------- _STATUS_PILL = ''' (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))) ''' # --------------------------------------------------------------------------- # ~search-mobile — mobile search input with htmx # --------------------------------------------------------------------------- _SEARCH_MOBILE = ''' (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)))))) ''' # --------------------------------------------------------------------------- # ~search-desktop — desktop search input with htmx # --------------------------------------------------------------------------- _SEARCH_DESKTOP = ''' (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)))))) ''' # --------------------------------------------------------------------------- # ~order-summary-card — reusable order summary card # --------------------------------------------------------------------------- _ORDER_SUMMARY_CARD = r''' (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")))) '''