Files
rose-ash/market/sx/cards.sx
giles b0920a1121 Rename all 1,169 components to path-based names with namespace support
Component names now reflect filesystem location using / as path separator
and : as namespace separator for shared components:
  ~sx-header → ~layouts/header
  ~layout-app-body → ~shared:layout/app-body
  ~blog-admin-dashboard → ~admin/dashboard

209 files, 4,941 replacements across all services.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:00:12 +00:00

158 lines
9.1 KiB
Plaintext

;; Market card components — pure data, no raw! HTML injection
(defcomp ~cards/label-overlay (&key (src :as string))
(img :src src :alt ""
:class "pointer-events-none absolute inset-0 w-full h-full object-contain object-top"))
(defcomp ~cards/image (&key (image :as string) (labels :as list?) (brand :as string) (brand-highlight :as string?))
(div :class "w-full aspect-square bg-stone-100 relative"
(figure :class "inline-block w-full h-full"
(div :class "relative w-full h-full"
(img :src image :alt "no image" :class "absolute inset-0 w-full h-full object-contain object-top" :loading "lazy" :decoding "async" :fetchpriority "low")
(when labels (map (lambda (src) (~cards/label-overlay :src src)) labels)))
(figcaption :class (str "mt-2 text-sm text-center" brand-highlight " text-stone-600") brand))))
(defcomp ~cards/no-image (&key (labels :as list?) (brand :as string))
(div :class "w-full aspect-square bg-stone-100 relative"
(div :class "p-2 flex flex-col items-center justify-center gap-2 text-red-500 h-full relative"
(div :class "text-stone-400 text-xs" "No image")
(when labels (ul :class "flex flex-row gap-1" (map (lambda (l) (li l)) labels)))
(div :class "text-stone-900 text-center line-clamp-3 break-words [overflow-wrap:anywhere]" brand))))
(defcomp ~cards/sticker (&key (src :as string) (name :as string) (ring-cls :as string?))
(img :src src :alt name :class (str "w-6 h-6" ring-cls)))
(defcomp ~cards/stickers (&key (stickers :as list))
(div :class "flex flex-row justify-center gap-2 p-2"
(map (lambda (s) (~cards/sticker :src (get s "src") :name (get s "name") :ring-cls (get s "ring-cls"))) stickers)))
(defcomp ~cards/highlight (&key (pre :as string) (mid :as string) (post :as string))
(<> pre (mark mid) post))
;; Price — delegates to shared ~shared:misc/price
(defcomp ~cards/price (&key (special-price :as string?) (regular-price :as string?))
(~shared:misc/price :special-price special-price :regular-price regular-price))
;; Main product card — accepts pure data, composes sub-components
(defcomp ~cards/product-card (&key (href :as string) (hx-select :as string)
(has-like :as boolean) (liked :as boolean?) (slug :as string) (csrf :as string) (like-action :as string?)
(image :as string?) (labels :as list?) (brand :as string) (brand-highlight :as string?)
(special-price :as string?) (regular-price :as string?)
(cart-action :as string) (quantity :as number?) (cart-href :as string)
(stickers :as list?)
(title :as string) (has-highlight :as boolean) (search-pre :as string?) (search-mid :as string?) (search-post :as string?))
(div :class "flex flex-col rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden relative"
(when has-like
(~cards/like-button :form-id (str "like-" slug) :action like-action :slug slug :csrf csrf
:icon-cls (if liked "fa-solid fa-heart text-red-500" "fa-regular fa-heart text-stone-400")))
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
(if image
(~cards/image :image image :labels labels :brand brand :brand-highlight brand-highlight)
(~cards/no-image :labels labels :brand brand))
(~cards/price :special-price special-price :regular-price regular-price))
(div :class "flex justify-center"
(if quantity
(~cart/add-quantity :cart-id (str "cart-" slug) :action cart-action :csrf csrf
:minus-val (str (- quantity 1)) :plus-val (str (+ quantity 1))
:quantity (str quantity) :cart-href cart-href)
(~cart/add-empty :cart-id (str "cart-" slug) :action cart-action :csrf csrf)))
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
(when stickers (~cards/stickers :stickers stickers))
(div :class "text-sm font-medium text-stone-800 text-center line-clamp-3 break-words [overflow-wrap:anywhere]"
(if has-highlight
(~cards/highlight :pre search-pre :mid search-mid :post search-post)
title)))))
(defcomp ~cards/like-button (&key (form-id :as string) (action :as string) (slug :as string) (csrf :as string) (icon-cls :as string))
(div :class "absolute top-2 right-2 z-10 text-6xl md:text-xl"
(form :id form-id :action action :method "post"
:sx-post action :sx-target (str "#like-" slug) :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class "cursor-pointer"
(i :class icon-cls :aria-hidden "true")))))
(defcomp ~cards/market-card-title-link (&key (href :as string) (name :as string))
(a :href href :class "hover:text-emerald-700"
(h2 :class "text-lg font-semibold text-stone-900" name)))
(defcomp ~cards/market-card-title (&key (name :as string))
(h2 :class "text-lg font-semibold text-stone-900" name))
(defcomp ~cards/market-card-desc (&key (description :as string))
(p :class "text-sm text-stone-600 mt-1 line-clamp-2" description))
(defcomp ~cards/market-card-badge (&key (href :as string) (title :as string))
(div :class "flex flex-wrap items-center gap-1.5 mt-3"
(a :href href :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200"
title)))
(defcomp ~cards/market-card (&key (title-content :as list?) (desc-content :as list?) (badge-content :as list?) (title :as string?) (desc :as string?) (badge :as string?))
(article :class "rounded-xl bg-white shadow-sm border border-stone-200 p-5 flex flex-col justify-between hover:border-stone-400 transition-colors"
(div
(if title-content title-content (when title title))
(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 ~cards/product-cards-content (&key (products :as list) (page :as number) (total-pages :as number) (next-url :as string)
(mobile-sentinel-hs :as string?) (desktop-sentinel-hs :as string?))
(<>
(map (lambda (p)
(~cards/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)
(<> (~shared:misc/sentinel-mobile :id (str "sentinel-" page "-m") :next-url next-url
:hyperscript mobile-sentinel-hs)
(~shared:misc/sentinel-desktop :id (str "sentinel-" page "-d") :next-url next-url
:hyperscript desktop-sentinel-hs))
(~shared:misc/end-of-results))))
;; Single market card from data (handles conditional title/desc/badge)
(defcomp ~cards/from-data (&key (name :as string) (description :as string?) (href :as string?) (show-badge :as boolean) (badge-href :as string?) (badge-title :as string?))
(~cards/market-card
:title-content (if href
(~cards/market-card-title-link :href href :name name)
(~cards/market-card-title :name name))
:desc-content (when description
(~cards/market-card-desc :description description))
:badge-content (when (and show-badge badge-title)
(~cards/market-card-badge :href badge-href :title badge-title))))
;; Market cards list with infinite scroll sentinel
(defcomp ~cards/content (&key (markets :as list) (page :as number) (has-more :as boolean) (next-url :as string))
(<>
(map (lambda (m)
(~cards/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
(~shared:misc/sentinel-simple :id (str "sentinel-" page) :next-url next-url))))
;; Market landing page content from data
(defcomp ~cards/landing-from-data (&key (excerpt :as string?) (feature-image :as string?) (html :as string?))
(~detail/landing-content :inner
(<> (when excerpt (~detail/landing-excerpt :text excerpt))
(when feature-image (~detail/landing-image :src feature-image))
(when html (~detail/landing-html :html html)))))