""" 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) # --------------------------------------------------------------------------- # ~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")))))) '''