Files
rose-ash/shared/sx/templates/layout.sx
giles a30e7228d8 Pass all values as &key args to ~root-header/~root-mobile
Nested component calls in _aser are serialized without body expansion,
so free variables inside ~root-header would be sent unresolved to the
client. Fix by making ~root-header/~root-mobile take all values as
&key params, and having parent layout defcomps pass them explicitly.
The parent layout bodies ARE expanded (via async_eval_slot_to_sx),
so their free variables resolve correctly during that expansion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 14:50:14 +00:00

206 lines
9.8 KiB
Plaintext

(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 :id "root-header-child" :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-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 ~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 ~post-label (&key feature-image title)
(<> (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 ~page-cart-badge (&key href count)
(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 ~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 ""))
(when app-label
(span :class "text-lg text-white/80 font-normal" app-label))))
(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))))
; @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 ~menu-row-sx (&key id level colour link-href link-label link-label-content icon
selected 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 selected
(span :class "text-lg text-white/80 font-normal" selected))))))
(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))))))
(defcomp ~oob-header-sx (&key parent-id row)
(div :id parent-id :sx-swap-oob "outerHTML" :class "flex flex-col w-full items-center"
row))
(defcomp ~header-child-sx (&key id 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 ~mobile-menu-section (&key label href colour level 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 ~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 ~root-header (&key cart-mini blog-url site-title app-label
nav-tree auth-menu nav-panel settings-url is-admin oob)
(~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 ~root-mobile (&key nav-tree auth-menu)
(~mobile-root-nav :nav-tree nav-tree :auth-menu auth-menu))
;; ---------------------------------------------------------------------------
;; Built-in layout defcomps — used by register_sx_layout("root", ...)
;; Free variables (cart-mini, blog-url, etc.) come from _ctx_to_env().
;; ---------------------------------------------------------------------------
(defcomp ~layout-root-full ()
(~root-header :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))
(defcomp ~layout-root-oob ()
(~oob-header-sx :parent-id "root-header-child"
:row (~root-header :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)))
(defcomp ~layout-root-mobile ()
(~root-mobile :nav-tree nav-tree :auth-menu auth-menu))
(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)
(div :class "text-stone-600 mb-4" message)
(when image
(div :class "flex justify-center"
(img :src image :width "300" :height "300")))))
(defcomp ~nav-link (&key href hx-select label icon aclass select-colours is-selected)
(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)))))