Move market composition from Python to .sx defcomps (Phase 3)

Python sxc/pages/ functions no longer build nested sx_call chains or
reference leaf component names. Instead they extract data (URLs, prices,
CSRF, cart state) and call a single top-level composition defcomp with
pure data values. The .sx defcomps handle all component-to-component
wiring, iteration (map), and conditional rendering.

New .sx composition defcomps:
- headers.sx: ~market-header-from-data, ~market-desktop-nav-from-data,
  ~market-product-header-from-data, ~market-product-admin-header-from-data
- prices.sx: ~market-prices-header-from-data, ~market-card-price-from-data
- navigation.sx: ~market-mobile-nav-from-data
- cards.sx: ~market-product-cards-content, ~market-card-from-data,
  ~market-cards-content, ~market-landing-from-data
- detail.sx: ~market-product-detail-from-data, ~market-detail-gallery-from-data,
  ~market-detail-info-from-data
- meta.sx: ~market-product-meta-from-data
- filters.sx: ~market-desktop-filter-from-data, ~market-mobile-chips-from-data,
  ~market-mobile-filter-content-from-data, plus 6 sub-composition defcomps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 01:11:57 +00:00
parent 36a0bd8577
commit e81d77437e
12 changed files with 961 additions and 781 deletions

View File

@@ -95,3 +95,63 @@
(if desc-content desc-content (when desc desc)))
(if badge-content badge-content (when badge badge))))
;; ---------------------------------------------------------------------------
;; Composition defcomps — receive data lists, compose component trees
;; ---------------------------------------------------------------------------
;; Product cards grid with infinite scroll sentinels
(defcomp ~market-product-cards-content (&key products page total-pages next-url
mobile-sentinel-hs desktop-sentinel-hs)
(<>
(map (lambda (p)
(~market-product-card
:href (get p "href") :hx-select (get p "hx-select") :slug (get p "slug")
:image (get p "image") :brand (get p "brand") :brand-highlight (get p "brand-highlight")
:special-price (get p "special-price") :regular-price (get p "regular-price")
:cart-action (get p "cart-action") :quantity (get p "quantity")
:cart-href (get p "cart-href") :csrf (get p "csrf")
:title (get p "title") :has-like (get p "has-like")
:liked (get p "liked") :like-action (get p "like-action")
:labels (get p "labels") :stickers (get p "stickers")
:has-highlight (get p "has-highlight")
:search-pre (get p "search-pre") :search-mid (get p "search-mid")
:search-post (get p "search-post")))
products)
(if (< page total-pages)
(<> (~sentinel-mobile :id (str "sentinel-" page "-m") :next-url next-url
:hyperscript mobile-sentinel-hs)
(~sentinel-desktop :id (str "sentinel-" page "-d") :next-url next-url
:hyperscript desktop-sentinel-hs))
(~end-of-results))))
;; Single market card from data (handles conditional title/desc/badge)
(defcomp ~market-card-from-data (&key name description href show-badge badge-href badge-title)
(~market-market-card
:title-content (if href
(~market-market-card-title-link :href href :name name)
(~market-market-card-title :name name))
:desc-content (when description
(~market-market-card-desc :description description))
:badge-content (when (and show-badge badge-title)
(~market-market-card-badge :href badge-href :title badge-title))))
;; Market cards list with infinite scroll sentinel
(defcomp ~market-cards-content (&key markets page has-more next-url)
(<>
(map (lambda (m)
(~market-card-from-data
:name (get m "name") :description (get m "description")
:href (get m "href") :show-badge (get m "show-badge")
:badge-href (get m "badge-href") :badge-title (get m "badge-title")))
markets)
(when has-more
(~sentinel-simple :id (str "sentinel-" page) :next-url next-url))))
;; Market landing page content from data
(defcomp ~market-landing-from-data (&key excerpt feature-image html)
(~market-landing-content :inner
(<> (when excerpt (~market-landing-excerpt :text excerpt))
(when feature-image (~market-landing-image :src feature-image))
(when html (~market-landing-html :html html)))))

View File

@@ -92,3 +92,71 @@
(defcomp ~market-landing-content (&key inner)
(<> (article :class "relative w-full" inner) (div :class "pb-8")))
;; ---------------------------------------------------------------------------
;; Composition: product detail page from data
;; ---------------------------------------------------------------------------
;; Gallery section from pre-computed data
(defcomp ~market-detail-gallery-from-data (&key images labels brand like-data has-nav-buttons thumbs)
(let ((like-sx (when like-data
(~market-like-button
:form-id (get like-data "form-id") :action (get like-data "action")
:slug (get like-data "slug") :csrf (get like-data "csrf")
:icon-cls (get like-data "icon-cls")))))
(if images
(<>
(~market-detail-gallery
:inner (~market-detail-gallery-inner
:like like-sx
:image (get (first images) "src") :alt (get (first images) "alt")
:labels (when labels
(<> (map (lambda (src) (~market-label-overlay :src src)) labels)))
:brand brand)
:nav (when has-nav-buttons (~market-detail-nav-buttons)))
(when thumbs
(~market-detail-thumbs :thumbs
(<> (map (lambda (t)
(~market-detail-thumb
:title (get t "title") :src (get t "src") :alt (get t "alt")))
thumbs)))))
(~market-detail-no-image :like like-sx))))
;; Right column details from data
(defcomp ~market-detail-info-from-data (&key extras desc-short desc-html sections)
(~market-detail-right-col :inner
(<>
(when extras
(~market-detail-extras :inner
(<> (map (lambda (e)
(if (= (get e "type") "unit-price")
(~market-detail-unit-price :price (get e "value"))
(~market-detail-case-size :size (get e "value"))))
extras))))
(when (or desc-short desc-html)
(~market-detail-desc-wrapper :inner
(<> (when desc-short (~market-detail-desc-short :text desc-short))
(when desc-html (~market-detail-desc-html :html desc-html)))))
(when sections
(~market-detail-sections :items
(<> (map (lambda (s)
(~market-detail-section :title (get s "title") :html (get s "html")))
sections)))))))
;; Full product detail layout from data
(defcomp ~market-product-detail-from-data (&key images labels brand like-data
has-nav-buttons thumbs sticker-items
extras desc-short desc-html sections)
(~market-detail-layout
:gallery (~market-detail-gallery-from-data
:images images :labels labels :brand brand :like-data like-data
:has-nav-buttons has-nav-buttons :thumbs thumbs)
:stickers (when sticker-items
(~market-detail-stickers :items
(<> (map (lambda (s)
(~market-detail-sticker :src (get s "src") :name (get s "name")))
sticker-items))))
:details (~market-detail-info-from-data
:extras extras :desc-short desc-short :desc-html desc-html
:sections sections)))

View File

@@ -122,3 +122,152 @@
(defcomp ~market-mobile-chip-brand-list (&key items)
(ul items))
;; ---------------------------------------------------------------------------
;; Composition defcomps — receive data, compose filter component trees
;; ---------------------------------------------------------------------------
;; Sort option stickers from data
(defcomp ~market-filter-sort-from-data (&key items)
(~market-filter-sort-row :items
(<> (map (lambda (s)
(~market-filter-sort-item
:href (get s "href") :hx-select (get s "hx-select")
:ring-cls (get s "ring-cls") :src (get s "src") :label (get s "label")))
items))))
;; Like filter from data
(defcomp ~market-filter-like-from-data (&key href hx-select liked mobile)
(~market-filter-like
:href href :hx-select hx-select
:icon-cls (if liked "fa-solid fa-heart text-red-500" "fa-regular fa-heart text-stone-400")
:size-cls (if mobile "text-[40px]" "text-2xl")))
;; Label filter items from data
(defcomp ~market-filter-labels-from-data (&key items hx-select)
(<> (map (lambda (lb)
(~market-filter-label-item
:href (get lb "href") :hx-select hx-select
:ring-cls (get lb "ring-cls") :src (get lb "src") :name (get lb "name")))
items)))
;; Sticker filter items from data
(defcomp ~market-filter-stickers-from-data (&key items hx-select)
(~market-filter-stickers-row :items
(<> (map (lambda (st)
(~market-filter-sticker-item
:href (get st "href") :hx-select hx-select
:ring-cls (get st "ring-cls") :src (get st "src") :name (get st "name")
:count-cls (get st "count-cls") :count (get st "count")))
items))))
;; Brand filter items from data
(defcomp ~market-filter-brands-from-data (&key items hx-select)
(~market-filter-brands-panel :items
(<> (map (lambda (br)
(~market-filter-brand-item
:href (get br "href") :hx-select hx-select
:bg-cls (get br "bg-cls") :name-cls (get br "name-cls")
:name (get br "name") :count (get br "count")))
items))))
;; Subcategory selector from data
(defcomp ~market-filter-subcategories-from-data (&key items hx-select all-href current-sub)
(~market-filter-subcategory-panel :items
(<>
(~market-filter-subcategory-item
:href all-href :hx-select hx-select
:active-cls (if (not current-sub) " bg-stone-200 font-medium" "")
:name "All")
(map (lambda (sub)
(~market-filter-subcategory-item
:href (get sub "href") :hx-select hx-select
:active-cls (get sub "active-cls") :name (get sub "name")))
items))))
;; Desktop filter panel from data
(defcomp ~market-desktop-filter-from-data (&key search-sx category-label
sort-data like-data label-data
sticker-data brand-data sub-data hx-select)
(<>
search-sx
(~market-desktop-category-summary :inner
(<>
(~market-filter-category-label :label category-label)
(when sort-data (~market-filter-sort-from-data :items sort-data))
(~market-filter-like-labels-nav :inner
(<>
(~market-filter-like-from-data
:href (get like-data "href") :hx-select hx-select
:liked (get like-data "liked") :mobile false)
(when label-data
(~market-filter-labels-from-data :items label-data :hx-select hx-select))))
(when sticker-data
(~market-filter-stickers-from-data :items sticker-data :hx-select hx-select))
(when sub-data
(~market-filter-subcategories-from-data
:items (get sub-data "items") :hx-select hx-select
:all-href (get sub-data "all-href")
:current-sub (get sub-data "current-sub")))))
(~market-desktop-brand-summary
:inner (when brand-data
(~market-filter-brands-from-data :items brand-data :hx-select hx-select)))))
;; Mobile filter chips from active filter data
(defcomp ~market-mobile-chips-from-data (&key sort-chip liked-chip label-chips sticker-chips brand-chips)
(~market-mobile-chips-row :inner
(<>
(when sort-chip
(~market-mobile-chip-sort :src (get sort-chip "src") :label (get sort-chip "label")))
(when liked-chip
(~market-mobile-chip-liked :inner
(<>
(~market-mobile-chip-liked-icon)
(when (get liked-chip "count")
(~market-mobile-chip-count
:cls (get liked-chip "count-cls") :count (get liked-chip "count"))))))
(when label-chips
(~market-mobile-chip-list :items
(<> (map (lambda (lc)
(~market-mobile-chip-item :inner
(<>
(~market-mobile-chip-image :src (get lc "src") :name (get lc "name"))
(when (get lc "count")
(~market-mobile-chip-count :cls (get lc "count-cls") :count (get lc "count"))))))
label-chips))))
(when sticker-chips
(~market-mobile-chip-list :items
(<> (map (lambda (sc)
(~market-mobile-chip-item :inner
(<>
(~market-mobile-chip-image :src (get sc "src") :name (get sc "name"))
(when (get sc "count")
(~market-mobile-chip-count :cls (get sc "count-cls") :count (get sc "count"))))))
sticker-chips))))
(when brand-chips
(~market-mobile-chip-brand-list :items
(<> (map (lambda (bc)
(if (get bc "has-count")
(~market-mobile-chip-brand :name (get bc "name") :count (get bc "count"))
(~market-mobile-chip-brand-zero :name (get bc "name"))))
brand-chips)))))))
;; Mobile filter content (expanded panel) from data
(defcomp ~market-mobile-filter-content-from-data (&key sort-data like-data label-data
sticker-data brand-data clear-href hx-select)
(<>
(when sort-data (~market-filter-sort-from-data :items sort-data))
(when clear-href
(~market-mobile-clear-filters :href clear-href :hx-select hx-select))
(~market-mobile-like-labels-row :inner
(<>
(~market-filter-like-from-data
:href (get like-data "href") :hx-select hx-select
:liked (get like-data "liked") :mobile true)
(when label-data
(~market-filter-labels-from-data :items label-data :hx-select hx-select))))
(when sticker-data
(~market-filter-stickers-from-data :items sticker-data :hx-select hx-select))
(when brand-data
(~market-filter-brands-from-data :items brand-data :hx-select hx-select))))

View File

@@ -15,3 +15,64 @@
:class "px-2 py-1 text-stone-500 hover:text-stone-700"
(i :class "fa fa-cog" :aria-hidden "true")))
;; ---------------------------------------------------------------------------
;; Composition defcomps — receive data, compose component trees
;; ---------------------------------------------------------------------------
;; Desktop category nav from pre-computed category data
(defcomp ~market-desktop-nav-from-data (&key categories hx-select select-colours
all-href all-active admin-href)
(~market-desktop-category-nav
:links (<>
(~market-category-link :href all-href :hx-select hx-select
:active all-active :select-colours select-colours :label "All")
(map (lambda (cat)
(~market-category-link
:href (get cat "href") :hx-select hx-select
:active (get cat "active") :select-colours select-colours
:label (get cat "label"))) categories))
:admin (when admin-href
(~market-admin-link :href admin-href :hx-select hx-select))))
;; Market-level header row from data
(defcomp ~market-header-from-data (&key market-title top-slug sub-slug link-href
categories hx-select select-colours
all-href all-active admin-href oob)
(~menu-row-sx :id "market-row" :level 2
:link-href link-href
:link-label-content (~market-shop-label
:title market-title :top-slug (or top-slug "") :sub-div sub-slug)
:nav (~market-desktop-nav-from-data
:categories categories :hx-select hx-select :select-colours select-colours
:all-href all-href :all-active all-active :admin-href admin-href)
:child-id "market-header-child"
:oob oob))
;; Product-level header row from data
(defcomp ~market-product-header-from-data (&key title link-href hx-select
price-data admin-href oob)
(~menu-row-sx :id "product-row" :level 3
:link-href link-href
:link-label-content (~market-product-label :title title)
:nav (<>
(~market-prices-header-from-data
:cart-id (get price-data "cart-id")
:cart-action (get price-data "cart-action")
:csrf (get price-data "csrf")
:quantity (get price-data "quantity")
:cart-href (get price-data "cart-href")
:sp-val (get price-data "sp-val") :sp-str (get price-data "sp-str")
:rp-val (get price-data "rp-val") :rp-str (get price-data "rp-str")
:rrp-str (get price-data "rrp-str"))
(when admin-href
(~market-admin-link :href admin-href :hx-select hx-select)))
:child-id "product-header-child"
:oob oob))
;; Product admin header row from data
(defcomp ~market-product-admin-header-from-data (&key link-href oob)
(~menu-row-sx :id "product-admin-row" :level 4
:link-href link-href :link-label "admin!!" :icon "fa fa-cog"
:child-id "product-admin-header-child" :oob oob))

View File

@@ -17,3 +17,35 @@
(defcomp ~market-meta-jsonld (&key json)
(script :type "application/ld+json" (~rich-text :html json)))
;; ---------------------------------------------------------------------------
;; Composition: all product meta tags from data
;; ---------------------------------------------------------------------------
(defcomp ~market-product-meta-from-data (&key title description canonical image-url
site-title brand price price-currency
jsonld-json)
(<>
(~market-meta-title :title title)
(~market-meta-description :description description)
(when canonical (~market-meta-canonical :href canonical))
;; OpenGraph
(~market-meta-og :property "og:site_name" :content site-title)
(~market-meta-og :property "og:type" :content "product")
(~market-meta-og :property "og:title" :content title)
(~market-meta-og :property "og:description" :content description)
(when canonical (~market-meta-og :property "og:url" :content canonical))
(when image-url (~market-meta-og :property "og:image" :content image-url))
(when (and price price-currency)
(<> (~market-meta-og :property "product:price:amount" :content price)
(~market-meta-og :property "product:price:currency" :content price-currency)))
(when brand (~market-meta-og :property "product:brand" :content brand))
;; Twitter
(~market-meta-twitter :name "twitter:card"
:content (if image-url "summary_large_image" "summary"))
(~market-meta-twitter :name "twitter:title" :content title)
(~market-meta-twitter :name "twitter:description" :content description)
(when image-url (~market-meta-twitter :name "twitter:image" :content image-url))
;; JSON-LD
(~market-meta-jsonld :json jsonld-json)))

View File

@@ -61,3 +61,37 @@
(defcomp ~market-mobile-cat-details (&key open summary subs)
(details :class "group/cat py-1" :open open
summary subs))
;; ---------------------------------------------------------------------------
;; Composition: mobile nav panel from pre-computed category data
;; ---------------------------------------------------------------------------
(defcomp ~market-mobile-nav-from-data (&key categories all-href all-active hx-select select-colours)
(~market-mobile-nav-wrapper :items
(<>
(~market-mobile-all-link :href all-href :hx-select hx-select
:active all-active :select-colours select-colours)
(map (lambda (cat)
(~market-mobile-cat-details
:open (get cat "active")
:summary (~market-mobile-cat-summary
:bg-cls (if (get cat "active") " bg-stone-900 text-white hover:bg-stone-900" "")
:href (get cat "href") :hx-select hx-select
:select-colours select-colours :cat-name (get cat "name")
:count-label (str (get cat "count") " products")
:count-str (str (get cat "count"))
:chevron (~market-mobile-chevron))
:subs (if (get cat "subs")
(~market-mobile-subs-panel :links
(<> (map (lambda (sub)
(~market-mobile-sub-link
:select-colours select-colours
:active (get sub "active")
:href (get sub "href") :hx-select hx-select
:label (get sub "label")
:count-label (str (get sub "count") " products")
:count-str (str (get sub "count"))))
(get cat "subs"))))
(~market-mobile-view-all :href (get cat "href") :hx-select hx-select))))
categories))))

View File

@@ -32,3 +32,36 @@
(defcomp ~market-prices-row (&key inner)
(div :class "flex flex-row items-center justify-between md:gap-2 md:px-2" inner))
;; ---------------------------------------------------------------------------
;; Composition: prices header + cart button from data
;; ---------------------------------------------------------------------------
(defcomp ~market-prices-header-from-data (&key cart-id cart-action csrf quantity cart-href
sp-val sp-str rp-val rp-str rrp-str)
(~market-prices-row :inner
(<>
(if quantity
(~market-cart-add-quantity :cart-id cart-id :action cart-action :csrf csrf
:minus-val (str (- quantity 1)) :plus-val (str (+ quantity 1))
:quantity (str quantity) :cart-href cart-href)
(~market-cart-add-empty :cart-id cart-id :action cart-action :csrf csrf))
(when sp-val
(<> (~market-header-price-special-label)
(~market-header-price-special :price sp-str)
(when rp-val (~market-header-price-strike :price rp-str))))
(when (and (not sp-val) rp-val)
(<> (~market-header-price-regular-label)
(~market-header-price-regular :price rp-str)))
(when rrp-str (~market-header-rrp :rrp rrp-str)))))
;; Card price line from data (used in product cards)
(defcomp ~market-card-price-from-data (&key sp-val sp-str rp-val rp-str)
(~market-price-line :inner
(<>
(when sp-val
(<> (~market-price-special :price sp-str)
(when rp-val (~market-price-regular-strike :price rp-str))))
(when (and (not sp-val) rp-val)
(~market-price-regular :price rp-str)))))