(defcomp ~shared:layout/app-body (&key header-rows filter aside menu content) (div :class "max-w-screen-2xl mx-auto py-1 px-1" (when header-rows (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 :id "root-header-child" :class "flex flex-col w-full items-center" 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 ~shared:layout/oob-sx (&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 ~shared:layout/hamburger () (div :class "md:hidden bg-stone-200 rounded" (svg :class "h-12 w-12 transition-transform group-open/root:hidden block self-start" :viewBox "0 0 24 24" :fill "none" :stroke "currentColor" (path :stroke-linecap "round" :stroke-linejoin "round" :stroke-width "2" :d "M4 6h16M4 12h16M4 18h16")) (svg :aria-hidden "true" :viewBox "0 0 24 24" :class "w-12 h-12 rotate-180 transition-transform group-open/root:block hidden self-start" (path :d "M6 9l6 6 6-6" :fill "currentColor")))) (defcomp ~shared:layout/post-label (&key (feature-image :as string?) (title :as string)) (<> (when feature-image (img :src feature-image :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0")) (span title))) (defcomp ~shared:layout/page-cart-badge (&key (href :as string) (count :as string)) (a :href href :class "relative inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 transition" (i :class "fa fa-shopping-cart" :aria-hidden "true") (span count))) (defcomp ~shared:layout/header-row-sx (&key cart-mini (blog-url :as string?) (site-title :as string?) (app-label :as string?) nav-tree auth-menu nav-panel (settings-url :as string?) (is-admin :as boolean?) (oob :as boolean?)) (<> (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 "")) (when app-label (span :class "text-lg text-white/80 font-normal" app-label)))) (nav :class "hidden md:flex flex-wrap 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")))) (~shared:layout/hamburger))) (div :class "block md:hidden text-md font-bold" (when auth-menu auth-menu)))) ; @css bg-sky-400 bg-sky-300 bg-sky-200 bg-sky-100 bg-violet-400 bg-violet-300 bg-violet-200 bg-violet-100 ; @css aria-selected:bg-violet-200 aria-selected:text-violet-900 aria-selected:bg-stone-500 aria-selected:text-white (defcomp ~shared:layout/menu-row-sx (&key (id :as string) (level :as number?) (colour :as string?) (link-href :as string) (link-label :as string?) link-label-content (icon :as string?) (selected :as string?) (hx-select :as string?) nav (child-id :as string?) child (oob :as boolean?) (external :as boolean?)) (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 md:items-baseline 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 selected (span :class "text-lg text-white/80 font-normal" selected)))))) (when nav (nav :class "hidden md:flex flex-wrap gap-4 text-sm ml-2 justify-end items-baseline flex-0" nav))) (when (and child-id (not oob)) (div :id child-id :class "flex flex-col w-full items-center" (when child child)))))) (defcomp ~shared:layout/oob-header-sx (&key (parent-id :as string) row) (div :id parent-id :sx-swap-oob "outerHTML" :class "flex flex-col w-full items-center" row)) (defcomp ~shared:layout/header-child-sx (&key (id :as string?) inner) (div :id (or id "root-header-child") :class "flex flex-col w-full items-center" inner)) ;; --------------------------------------------------------------------------- ;; Mobile menu — vertical nav sections for hamburger panel ;; --------------------------------------------------------------------------- ;; Labelled section: colour bar header + vertical nav items (defcomp ~shared:layout/mobile-menu-section (&key (label :as string) (href :as string?) (colour :as string?) (level :as number?) items) (let* ((c (or colour "sky")) (lv (or level 1)) (shade (str (- 500 (* lv 100))))) (div (div :class (str "flex items-center gap-2 px-3 py-1.5 text-sm font-bold bg-" c "-" shade) (if href (a :href href :class "hover:underline" label) (span label))) (div :class "flex flex-col gap-1 p-2 text-sm" items)))) ;; Root-level mobile nav: site nav items + auth links (defcomp ~shared:layout/mobile-root-nav (&key nav-tree auth-menu) (<> (when nav-tree (div :class "flex flex-col gap-2 p-3 text-sm" nav-tree)) (when auth-menu (div :class "p-3 border-t border-stone-200" auth-menu)))) ;; --------------------------------------------------------------------------- ;; Root header/mobile shorthand — pass-through to shared defcomps. ;; All values must be supplied as &key args (not free variables) because ;; nested component calls in _aser are serialized without expansion. ;; --------------------------------------------------------------------------- (defcomp ~shared:layout/root-header (&key cart-mini (blog-url :as string?) (site-title :as string?) (app-label :as string?) nav-tree auth-menu nav-panel (settings-url :as string?) (is-admin :as boolean?) (oob :as boolean?)) (~shared:layout/header-row-sx :cart-mini cart-mini :blog-url blog-url :site-title site-title :app-label app-label :nav-tree nav-tree :auth-menu auth-menu :nav-panel nav-panel :settings-url settings-url :is-admin is-admin :oob oob)) (defcomp ~shared:layout/root-mobile (&key nav-tree auth-menu) (~shared:layout/mobile-root-nav :nav-tree nav-tree :auth-menu auth-menu)) ;; --------------------------------------------------------------------------- ;; Auto-fetching header/mobile macros — use IO primitives to self-populate. ;; These expand inline so IO calls resolve in _aser mode within layout bodies. ;; Replaces the 10-parameter ~shared:layout/root-header boilerplate in layout defcomps. ;; --------------------------------------------------------------------------- (defmacro ~root-header-auto (oob) (quasiquote (let ((__rhctx (root-header-ctx))) (~shared:layout/header-row-sx :cart-mini (get __rhctx "cart-mini") :blog-url (get __rhctx "blog-url") :site-title (get __rhctx "site-title") :app-label (get __rhctx "app-label") :nav-tree (get __rhctx "nav-tree") :auth-menu (get __rhctx "auth-menu") :nav-panel (get __rhctx "nav-panel") :settings-url (get __rhctx "settings-url") :is-admin (get __rhctx "is-admin") :oob (unquote oob))))) (defmacro ~root-mobile-auto () (quasiquote (let ((__rhctx (root-header-ctx))) (~shared:layout/mobile-root-nav :nav-tree (get __rhctx "nav-tree") :auth-menu (get __rhctx "auth-menu"))))) ;; --------------------------------------------------------------------------- ;; Built-in layout defcomps — used by register_sx_layout("root", ...) ;; These use ~root-header-auto / ~root-mobile-auto macros (IO primitives). ;; --------------------------------------------------------------------------- (defcomp ~shared:layout/root-full () (~root-header-auto)) (defcomp ~shared:layout/root-oob () (~shared:layout/oob-header-sx :parent-id "root-header-child" :row (~root-header-auto true))) (defcomp ~shared:layout/root-mobile () (~root-mobile-auto)) ;; Post layout — root + post header (defcomp ~shared:layout/post-full () (<> (~root-header-auto) (~shared:layout/header-child-sx :inner (~post-header-auto)))) (defcomp ~shared:layout/post-oob () (<> (~post-header-auto true) (~shared:layout/oob-header-sx :parent-id "post-header-child" :row ""))) (defcomp ~shared:layout/post-mobile () (let ((__phctx (post-header-ctx)) (__rhctx (root-header-ctx))) (<> (when (get __phctx "slug") (~shared:layout/mobile-menu-section :label (slice (get __phctx "title") 0 40) :href (get __phctx "link-href") :level 1 :items (~post-nav-auto))) (~root-mobile-auto)))) ;; Post-admin layout — root + post header with nested admin row (defcomp ~shared:layout/post-admin-full (&key (selected :as string?)) (let ((__admin-hdr (~post-admin-header-auto nil selected))) (<> (~root-header-auto) (~shared:layout/header-child-sx :inner (~post-header-auto nil))))) (defcomp ~shared:layout/post-admin-oob (&key (selected :as string?)) (<> (~post-header-auto true) (~shared:layout/oob-header-sx :parent-id "post-header-child" :row (~post-admin-header-auto nil selected)))) (defcomp ~shared:layout/post-admin-mobile (&key (selected :as string?)) (let ((__phctx (post-header-ctx))) (<> (when (get __phctx "slug") (~shared:layout/mobile-menu-section :label "admin" :href (get __phctx "admin-href") :level 2 :items (~post-admin-nav-auto selected))) (when (get __phctx "slug") (~shared:layout/mobile-menu-section :label (slice (get __phctx "title") 0 40) :href (get __phctx "link-href") :level 1 :items (~post-nav-auto))) (~root-mobile-auto)))) (defcomp ~shared:layout/error-content (&key (errnum :as string) (message :as string) (image :as string?)) (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) (div :class "text-stone-600 mb-4" message) (when image (div :class "flex justify-center" (img :src image :width "300" :height "300"))))) (defcomp ~shared:layout/clear-oob-div (&key (id :as string)) (div :id id :sx-swap-oob "outerHTML")) ;; --------------------------------------------------------------------------- ;; Post-level auto-fetching macros — use (post-header-ctx) IO primitive ;; --------------------------------------------------------------------------- (defmacro ~post-nav-auto () "Post-level nav items: page cart badge + container nav + admin cog." (quasiquote (let ((__phctx (post-header-ctx))) (when (get __phctx "slug") (<> (when (> (get __phctx "page-cart-count") 0) (~shared:layout/page-cart-badge :href (get __phctx "cart-href") :count (str (get __phctx "page-cart-count")))) (when (get __phctx "container-nav") (~shared:layout/container-nav-wrapper :content (get __phctx "container-nav"))) (when (get __phctx "is-admin") (~shared:layout/admin-cog-button :href (get __phctx "admin-href") :is-admin-page (get __phctx "is-admin-page")))))))) (defmacro ~post-header-auto (oob) "Post-level header row. Reads post data via (post-header-ctx)." (quasiquote (let ((__phctx (post-header-ctx))) (when (get __phctx "slug") (~shared:layout/menu-row-sx :id "post-row" :level 1 :link-href (get __phctx "link-href") :link-label-content (~shared:layout/post-label :feature-image (get __phctx "feature-image") :title (get __phctx "title")) :nav (~post-nav-auto) :child-id "post-header-child" :oob (unquote oob) :external true))))) (defmacro ~post-admin-nav-auto (selected) "Post-admin nav items: calendars, markets, etc." (quasiquote (let ((__phctx (post-header-ctx))) (when (get __phctx "slug") (let ((__slug (get __phctx "slug")) (__sc (get __phctx "select-colours"))) (<> (~shared:layout/nav-link :href (app-url "events" (str "/" __slug "/admin/")) :label "calendars" :select-colours __sc :is-selected (when (= (unquote selected) "calendars") "true")) (~shared:layout/nav-link :href (app-url "market" (str "/" __slug "/admin/")) :label "markets" :select-colours __sc :is-selected (when (= (unquote selected) "markets") "true")) (~shared:layout/nav-link :href (app-url "cart" (str "/" __slug "/admin/payments/")) :label "payments" :select-colours __sc :is-selected (when (= (unquote selected) "payments") "true")) (~shared:layout/nav-link :href (app-url "blog" (str "/" __slug "/admin/entries/")) :label "entries" :select-colours __sc :is-selected (when (= (unquote selected) "entries") "true")) (~shared:layout/nav-link :href (app-url "blog" (str "/" __slug "/admin/data/")) :label "data" :select-colours __sc :is-selected (when (= (unquote selected) "data") "true")) (~shared:layout/nav-link :href (app-url "blog" (str "/" __slug "/admin/preview/")) :label "preview" :select-colours __sc :is-selected (when (= (unquote selected) "preview") "true")) (~shared:layout/nav-link :href (app-url "blog" (str "/" __slug "/admin/edit/")) :label "edit" :select-colours __sc :is-selected (when (= (unquote selected) "edit") "true")) (~shared:layout/nav-link :href (app-url "blog" (str "/" __slug "/admin/settings/")) :label "settings" :select-colours __sc :is-selected (when (= (unquote selected) "settings") "true")))))))) (defmacro ~post-admin-header-auto (oob selected) "Post-admin header row. Uses (post-header-ctx) for slug + URLs." (quasiquote (let ((__phctx (post-header-ctx))) (when (get __phctx "slug") (~shared:layout/menu-row-sx :id "post-admin-row" :level 2 :link-href (get __phctx "admin-href") :link-label-content (~shared:layout/post-admin-label :selected (unquote selected)) :nav (~post-admin-nav-auto (unquote selected)) :child-id "post-admin-header-child" :oob (unquote oob)))))) ;; --------------------------------------------------------------------------- ;; Shared nav helpers — used by post_header_sx / post_admin_header_sx ;; --------------------------------------------------------------------------- (defcomp ~shared:layout/container-nav-wrapper (&key content) (div :id "entries-calendars-nav-wrapper" :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl" content)) ; @css justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 !bg-stone-500 !text-white (defcomp ~shared:layout/admin-cog-button (&key (href :as string) (is-admin-page :as boolean?)) (div :class "relative nav-group" (a :href href :class (str "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 " (if is-admin-page "!bg-stone-500 !text-white" "")) (i :class "fa fa-cog" :aria-hidden "true")))) (defcomp ~shared:layout/post-admin-label (&key (selected :as string?)) (<> (i :class "fa fa-shield-halved" :aria-hidden "true") " admin" (when selected (span :class "text-white" selected)))) (defcomp ~shared:layout/nav-link (&key (href :as string) (hx-select :as string?) (label :as string?) (icon :as string?) (aclass :as string?) (select-colours :as string?) (is-selected :as string?)) (div :class "relative nav-group" (a :href href :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 " (or select-colours ""))) (when icon (i :class icon :aria-hidden "true")) (when label (span label)))))