Add (param :as type) annotations to defcomp params across all services and templates

Annotates ~500 defcomp params across 62 files: market (5), blog (7), cart (5),
events (3), federation (4), account (3), orders (2), shared templates (11),
sx docs (14), plus remaining spec fn params (z3, test-framework, adapter-dom,
adapter-async, engine, eval). Total annotations in codebase: 1043.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 21:01:02 +00:00
parent 98c1023b81
commit 477ce766ff
62 changed files with 537 additions and 502 deletions

View File

@@ -1,12 +1,12 @@
;; Auth page components (device auth — account-specific) ;; Auth page components (device auth — account-specific)
;; Login and check-email components are shared: see shared/sx/templates/auth.sx ;; Login and check-email components are shared: see shared/sx/templates/auth.sx
(defcomp ~account-device-error (&key error) (defcomp ~account-device-error (&key (error :as string))
(when error (when error
(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4" (div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4"
error))) error)))
(defcomp ~account-device-form (&key error action csrf-token code) (defcomp ~account-device-form (&key error (action :as string) (csrf-token :as string) (code :as string))
(div :class "py-8 max-w-md mx-auto" (div :class "py-8 max-w-md mx-auto"
(h1 :class "text-2xl font-bold mb-6" "Authorize device") (h1 :class "text-2xl font-bold mb-6" "Authorize device")
(p :class "text-stone-600 mb-4" "Enter the code shown in your terminal to sign in.") (p :class "text-stone-600 mb-4" "Enter the code shown in your terminal to sign in.")
@@ -29,21 +29,21 @@
;; Assembled auth page content — replaces Python _login_page_content etc. ;; Assembled auth page content — replaces Python _login_page_content etc.
(defcomp ~account-login-content (&key error email) (defcomp ~account-login-content (&key (error :as string?) (email :as string?))
(~auth-login-form (~auth-login-form
:error (when error (~auth-error-banner :error error)) :error (when error (~auth-error-banner :error error))
:action (url-for "auth.start_login") :action (url-for "auth.start_login")
:csrf-token (csrf-token) :csrf-token (csrf-token)
:email (or email ""))) :email (or email "")))
(defcomp ~account-device-content (&key error code) (defcomp ~account-device-content (&key (error :as string?) (code :as string?))
(~account-device-form (~account-device-form
:error (when error (~account-device-error :error error)) :error (when error (~account-device-error :error error))
:action (url-for "auth.device_submit") :action (url-for "auth.device_submit")
:csrf-token (csrf-token) :csrf-token (csrf-token)
:code (or code ""))) :code (or code "")))
(defcomp ~account-check-email-content (&key email email-error) (defcomp ~account-check-email-content (&key (email :as string?) (email-error :as string?))
(~auth-check-email (~auth-check-email
:email (escape (or email "")) :email (escape (or email ""))
:error (when email-error :error (when email-error

View File

@@ -1,26 +1,26 @@
;; Account dashboard components ;; Account dashboard components
(defcomp ~account-error-banner (&key error) (defcomp ~account-error-banner (&key (error :as string))
(when error (when error
(div :class "rounded-lg border border-red-200 bg-red-50 text-red-800 px-4 py-3 text-sm" (div :class "rounded-lg border border-red-200 bg-red-50 text-red-800 px-4 py-3 text-sm"
error))) error)))
(defcomp ~account-user-email (&key email) (defcomp ~account-user-email (&key (email :as string))
(when email (when email
(p :class "text-sm text-stone-500 mt-1" email))) (p :class "text-sm text-stone-500 mt-1" email)))
(defcomp ~account-user-name (&key name) (defcomp ~account-user-name (&key (name :as string))
(when name (when name
(p :class "text-sm text-stone-600" name))) (p :class "text-sm text-stone-600" name)))
(defcomp ~account-logout-form (&key csrf-token) (defcomp ~account-logout-form (&key (csrf-token :as string))
(form :action "/auth/logout/" :method "post" (form :action "/auth/logout/" :method "post"
(input :type "hidden" :name "csrf_token" :value csrf-token) (input :type "hidden" :name "csrf_token" :value csrf-token)
(button :type "submit" (button :type "submit"
:class "inline-flex items-center gap-2 rounded-full border border-stone-300 px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-50 transition" :class "inline-flex items-center gap-2 rounded-full border border-stone-300 px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-50 transition"
(i :class "fa-solid fa-right-from-bracket text-xs") " Sign out"))) (i :class "fa-solid fa-right-from-bracket text-xs") " Sign out")))
(defcomp ~account-label-item (&key name) (defcomp ~account-label-item (&key (name :as string))
(span :class "inline-flex items-center rounded-full border border-stone-200 px-3 py-1 text-xs font-medium bg-white/60" (span :class "inline-flex items-center rounded-full border border-stone-200 px-3 py-1 text-xs font-medium bg-white/60"
name)) name))
@@ -43,7 +43,7 @@
labels))) labels)))
;; Assembled dashboard content — replaces Python _account_main_panel_sx ;; Assembled dashboard content — replaces Python _account_main_panel_sx
(defcomp ~account-dashboard-content (&key error) (defcomp ~account-dashboard-content (&key (error :as string?))
(let* ((user (current-user)) (let* ((user (current-user))
(csrf (csrf-token))) (csrf (csrf-token)))
(~account-main-panel (~account-main-panel

View File

@@ -1,17 +1,17 @@
;; Newsletter management components ;; Newsletter management components
(defcomp ~account-newsletter-desc (&key description) (defcomp ~account-newsletter-desc (&key (description :as string))
(when description (when description
(p :class "text-xs text-stone-500 mt-0.5 truncate" description))) (p :class "text-xs text-stone-500 mt-0.5 truncate" description)))
(defcomp ~account-newsletter-toggle (&key id url hdrs target cls checked knob-cls) (defcomp ~account-newsletter-toggle (&key (id :as string) (url :as string) (hdrs :as dict) (target :as string) (cls :as string) (checked :as string) (knob-cls :as string))
(div :id id :class "flex items-center" (div :id id :class "flex items-center"
(button :sx-post url :sx-headers hdrs :sx-target target :sx-swap "outerHTML" (button :sx-post url :sx-headers hdrs :sx-target target :sx-swap "outerHTML"
:class cls :role "switch" :aria-checked checked :class cls :role "switch" :aria-checked checked
(span :class knob-cls)))) (span :class knob-cls))))
(defcomp ~account-newsletter-item (&key name desc toggle) (defcomp ~account-newsletter-item (&key (name :as string) desc toggle)
(div :class "flex items-center justify-between py-4 first:pt-0 last:pb-0" (div :class "flex items-center justify-between py-4 first:pt-0 last:pb-0"
(div :class "min-w-0 flex-1" (div :class "min-w-0 flex-1"
(p :class "text-sm font-medium text-stone-800" name) (p :class "text-sm font-medium text-stone-800" name)
@@ -32,7 +32,7 @@
;; Assembled newsletters content — replaces Python _newsletters_panel_sx ;; Assembled newsletters content — replaces Python _newsletters_panel_sx
;; Takes pre-fetched newsletter-list from page helper ;; Takes pre-fetched newsletter-list from page helper
(defcomp ~account-newsletters-content (&key newsletter-list account-url) (defcomp ~account-newsletters-content (&key (newsletter-list :as list) (account-url :as string?))
(let* ((csrf (csrf-token))) (let* ((csrf (csrf-token)))
(if (empty? newsletter-list) (if (empty? newsletter-list)
(~account-newsletter-empty) (~account-newsletter-empty)

View File

@@ -1,6 +1,6 @@
;; Blog admin panel components ;; Blog admin panel components
(defcomp ~blog-cache-panel (&key clear-url csrf) (defcomp ~blog-cache-panel (&key (clear-url :as string) (csrf :as string))
(div :class "max-w-2xl mx-auto px-4 py-6 space-y-6" (div :class "max-w-2xl mx-auto px-4 py-6 space-y-6"
(div :class "flex flex-col md:flex-row gap-3 items-start" (div :class "flex flex-col md:flex-row gap-3 items-start"
(form :sx-post clear-url :sx-trigger "submit" :sx-target "#cache-status" :sx-swap "innerHTML" (form :sx-post clear-url :sx-trigger "submit" :sx-target "#cache-status" :sx-swap "innerHTML"
@@ -19,10 +19,10 @@
:sx-headers hx-headers :class "text-sm border border-stone-300 rounded px-2 py-1" :sx-headers hx-headers :class "text-sm border border-stone-300 rounded px-2 py-1"
options)) options))
(defcomp ~blog-snippet-option (&key value selected label) (defcomp ~blog-snippet-option (&key (value :as string) (selected :as boolean) (label :as string))
(option :value value :selected selected label)) (option :value value :selected selected label))
(defcomp ~blog-snippet-row (&key name owner badge-cls visibility extra) (defcomp ~blog-snippet-row (&key (name :as string) (owner :as string) (badge-cls :as string) (visibility :as string) extra)
(div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition" (div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition"
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "font-medium truncate" name) (div :class "font-medium truncate" name)
@@ -42,7 +42,7 @@
(div :id "menu-item-form" :class "mb-6") (div :id "menu-item-form" :class "mb-6")
(div :id "menu-items-list" list))) (div :id "menu-items-list" list)))
(defcomp ~blog-menu-item-row (&key img label slug sort-order edit-url delete-url confirm-text hx-headers) (defcomp ~blog-menu-item-row (&key img (label :as string) (slug :as string) (sort-order :as string) (edit-url :as string) (delete-url :as string) (confirm-text :as string) hx-headers)
(div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition" (div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition"
(div :class "text-stone-400 cursor-move" (i :class "fa fa-grip-vertical")) (div :class "text-stone-400 cursor-move" (i :class "fa fa-grip-vertical"))
img img
@@ -81,7 +81,7 @@
(div :class "h-8 w-8 rounded-full text-xs font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0" (div :class "h-8 w-8 rounded-full text-xs font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0"
:style style initial)) :style style initial))
(defcomp ~blog-tag-group-li (&key icon edit-href name slug sort-order) (defcomp ~blog-tag-group-li (&key icon (edit-href :as string) (name :as string) (slug :as string) (sort-order :as number))
(li :class "border rounded p-3 bg-white flex items-center gap-3" (li :class "border rounded p-3 bg-white flex items-center gap-3"
icon icon
(div :class "flex-1" (div :class "flex-1"
@@ -106,7 +106,7 @@
;; Tag group edit ;; Tag group edit
(defcomp ~blog-tag-checkbox (&key tag-id checked img name) (defcomp ~blog-tag-checkbox (&key (tag-id :as string) (checked :as boolean) img (name :as string))
(label :class "flex items-center gap-2 px-2 py-1 hover:bg-stone-50 rounded text-sm cursor-pointer" (label :class "flex items-center gap-2 px-2 py-1 hover:bg-stone-50 rounded text-sm cursor-pointer"
(input :type "checkbox" :name "tag_ids" :value tag-id :checked checked :class "rounded border-stone-300") (input :type "checkbox" :name "tag_ids" :value tag-id :checked checked :class "rounded border-stone-300")
img (span name))) img (span name)))
@@ -114,7 +114,7 @@
(defcomp ~blog-tag-checkbox-image (&key src) (defcomp ~blog-tag-checkbox-image (&key src)
(img :src src :alt "" :class "h-4 w-4 rounded-full object-cover")) (img :src src :alt "" :class "h-4 w-4 rounded-full object-cover"))
(defcomp ~blog-tag-group-edit-form (&key save-url csrf name colour sort-order feature-image tags) (defcomp ~blog-tag-group-edit-form (&key (save-url :as string) (csrf :as string) (name :as string) (colour :as string?) (sort-order :as number) (feature-image :as string?) tags)
(form :method "post" :action save-url :class "border rounded p-4 bg-white space-y-4" (form :method "post" :action save-url :class "border rounded p-4 bg-white space-y-4"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(div :class "space-y-3" (div :class "space-y-3"
@@ -133,7 +133,7 @@
(div :class "flex gap-3" (div :class "flex gap-3"
(button :type "submit" :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" "Save")))) (button :type "submit" :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" "Save"))))
(defcomp ~blog-tag-group-delete-form (&key delete-url csrf) (defcomp ~blog-tag-group-delete-form (&key (delete-url :as string) (csrf :as string))
(form :method "post" :action delete-url :class "border-t pt-4" (form :method "post" :action delete-url :class "border-t pt-4"
:onsubmit "return confirm('Delete this tag group? Tags will not be deleted.')" :onsubmit "return confirm('Delete this tag group? Tags will not be deleted.')"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)

View File

@@ -4,17 +4,17 @@
(div :class "absolute top-20 right-2 z-10 text-6xl md:text-4xl" (div :class "absolute top-20 right-2 z-10 text-6xl md:text-4xl"
(~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart))) (~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart)))
(defcomp ~blog-draft-status (&key publish-requested timestamp) (defcomp ~blog-draft-status (&key (publish-requested :as boolean) (timestamp :as string?))
(<> (div :class "flex justify-center gap-2 mt-1" (<> (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") (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 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 timestamp (p :class "text-sm text-stone-500" (str "Updated: " timestamp))))) (when timestamp (p :class "text-sm text-stone-500" (str "Updated: " timestamp)))))
(defcomp ~blog-published-status (&key timestamp) (defcomp ~blog-published-status (&key (timestamp :as string))
(p :class "text-sm text-stone-500" (str "Published: " timestamp))) (p :class "text-sm text-stone-500" (str "Published: " timestamp)))
;; Tag components — accept data, not HTML ;; Tag components — accept data, not HTML
(defcomp ~blog-tag-icon (&key src name initial) (defcomp ~blog-tag-icon (&key (src :as string?) (name :as string) (initial :as string))
(if src (if src
(img :src src :alt name :class "h-4 w-4 rounded-full object-cover border border-stone-300 flex-shrink-0") (img :src src :alt name :class "h-4 w-4 rounded-full object-cover border border-stone-300 flex-shrink-0")
(div :class "h-4 w-4 rounded-full text-[8px] font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0 bg-stone-200 text-stone-600" initial))) (div :class "h-4 w-4 rounded-full text-[8px] font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0 bg-stone-200 text-stone-600" initial)))
@@ -45,12 +45,12 @@
(span :class "text-stone-700" name))) (span :class "text-stone-700" name)))
;; Card — accepts pure data ;; Card — accepts pure data
(defcomp ~blog-card (&key slug href hx-select title (defcomp ~blog-card (&key (slug :as string) (href :as string) (hx-select :as string?) (title :as string)
feature-image excerpt (feature-image :as string?) (excerpt :as string?)
status is-draft publish-requested status-timestamp status (is-draft :as boolean) (publish-requested :as boolean) (status-timestamp :as string?)
liked like-url csrf-token (liked :as boolean) (like-url :as string?) (csrf-token :as string?)
has-like (has-like :as boolean)
tags authors widget) (tags :as list?) (authors :as list?) widget)
(article :class "border-b pb-6 last:border-b-0 relative" (article :class "border-b pb-6 last:border-b-0 relative"
(when has-like (when has-like
(~blog-like-button (~blog-like-button
@@ -80,9 +80,9 @@
(ul :class "flex flex-wrap gap-2 text-sm" (ul :class "flex flex-wrap gap-2 text-sm"
(map (lambda (a) (~blog-author-item :image (get a "image") :name (get a "name"))) authors)))))))) (map (lambda (a) (~blog-author-item :image (get a "image") :name (get a "name"))) authors))))))))
(defcomp ~blog-card-tile (&key href hx-select feature-image title (defcomp ~blog-card-tile (&key (href :as string) (hx-select :as string?) (feature-image :as string?) (title :as string)
is-draft publish-requested status-timestamp (is-draft :as boolean) (publish-requested :as boolean) (status-timestamp :as string?)
excerpt tags authors) (excerpt :as string?) (tags :as list?) (authors :as list?))
(article :class "relative" (article :class "relative"
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true" :sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true"
@@ -107,7 +107,7 @@
(map (lambda (a) (~blog-author-item :image (get a "image") :name (get a "name"))) authors)))))))) (map (lambda (a) (~blog-author-item :image (get a "image") :name (get a "name"))) authors))))))))
;; Data-driven blog cards list (replaces Python _blog_cards_sx loop) ;; Data-driven blog cards list (replaces Python _blog_cards_sx loop)
(defcomp ~blog-cards-from-data (&key posts view sentinel) (defcomp ~blog-cards-from-data (&key (posts :as list?) (view :as string?) sentinel)
(<> (<>
(map (lambda (p) (map (lambda (p)
(if (= view "tile") (if (= view "tile")
@@ -131,7 +131,7 @@
sentinel)) sentinel))
;; Data-driven page cards list (replaces Python _page_cards_sx loop) ;; Data-driven page cards list (replaces Python _page_cards_sx loop)
(defcomp ~page-cards-from-data (&key pages sentinel) (defcomp ~page-cards-from-data (&key (pages :as list?) sentinel)
(<> (<>
(map (lambda (pg) (map (lambda (pg)
(~blog-page-card (~blog-page-card
@@ -150,7 +150,7 @@
(when has-market (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800" (when has-market (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800"
(i :class "fa fa-shopping-bag mr-1") "Market")))) (i :class "fa fa-shopping-bag mr-1") "Market"))))
(defcomp ~blog-page-card (&key href hx-select title has-calendar has-market pub-timestamp feature-image excerpt) (defcomp ~blog-page-card (&key (href :as string) (hx-select :as string?) (title :as string) (has-calendar :as boolean) (has-market :as boolean) (pub-timestamp :as string?) (feature-image :as string?) (excerpt :as string?))
(article :class "border-b pb-6 last:border-b-0 relative" (article :class "border-b pb-6 last:border-b-0 relative"
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true" :sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true"

View File

@@ -1,6 +1,6 @@
;; Blog post detail components ;; Blog post detail components
(defcomp ~blog-detail-edit-link (&key href hx-select) (defcomp ~blog-detail-edit-link (&key (href :as string) (hx-select :as string))
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class "inline-block px-3 py-1 rounded-full text-sm font-semibold bg-stone-700 text-white hover:bg-stone-800 transition-colors" :class "inline-block px-3 py-1 rounded-full text-sm font-semibold bg-stone-700 text-white hover:bg-stone-800 transition-colors"
@@ -20,7 +20,7 @@
(div :class "absolute top-2 right-2 z-10 text-8xl md:text-6xl" (div :class "absolute top-2 right-2 z-10 text-8xl md:text-6xl"
(~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart))) (~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart)))
(defcomp ~blog-detail-excerpt (&key excerpt) (defcomp ~blog-detail-excerpt (&key (excerpt :as string))
(div :class "w-full text-center italic text-3xl p-2" excerpt)) (div :class "w-full text-center italic text-3xl p-2" excerpt))
(defcomp ~blog-detail-chrome (&key like excerpt at-bar) (defcomp ~blog-detail-chrome (&key like excerpt at-bar)
@@ -43,10 +43,10 @@
;; Data-driven composition — replaces _post_main_panel_sx ;; Data-driven composition — replaces _post_main_panel_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-post-detail-content (&key slug is-draft publish-requested can-edit edit-href (defcomp ~blog-post-detail-content (&key (slug :as string) (is-draft :as boolean) (publish-requested :as boolean) (can-edit :as boolean) (edit-href :as string?)
is-page has-user liked like-url csrf (is-page :as boolean) (has-user :as boolean) (liked :as boolean) (like-url :as string?) (csrf :as string?)
custom-excerpt tags authors (custom-excerpt :as string?) (tags :as list?) (authors :as list?)
feature-image html-content sx-content) (feature-image :as string?) (html-content :as string?) (sx-content :as string?))
(let* ((hx-select "#main-panel") (let* ((hx-select "#main-panel")
(draft-sx (when is-draft (draft-sx (when is-draft
(~blog-detail-draft (~blog-detail-draft
@@ -70,7 +70,7 @@
:html-content html-content :html-content html-content
:sx-content sx-content))) :sx-content sx-content)))
(defcomp ~blog-meta (&key robots page-title desc canonical og-type og-title image twitter-card twitter-title) (defcomp ~blog-meta (&key (robots :as string) (page-title :as string) (desc :as string) (canonical :as string?) (og-type :as string) (og-title :as string) (image :as string?) (twitter-card :as string) (twitter-title :as string))
(<> (<>
(meta :name "robots" :content robots) (meta :name "robots" :content robots)
(title page-title) (title page-title)

View File

@@ -4,7 +4,7 @@
(div :class "max-w-[768px] mx-auto mt-[16px] rounded-[8px] border border-red-300 bg-red-50 px-[16px] py-[12px] text-[14px] text-red-700" (div :class "max-w-[768px] mx-auto mt-[16px] rounded-[8px] border border-red-300 bg-red-50 px-[16px] py-[12px] text-[14px] text-red-700"
(strong "Save failed:") " " error)) (strong "Save failed:") " " error))
(defcomp ~blog-editor-form (&key csrf title-placeholder create-label) (defcomp ~blog-editor-form (&key (csrf :as string) (title-placeholder :as string) (create-label :as string))
(form :id "post-new-form" :method "post" :class "max-w-[768px] mx-auto pb-[48px]" (form :id "post-new-form" :method "post" :class "max-w-[768px] mx-auto pb-[48px]"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :id "lexical-json-input" :name "lexical" :value "") (input :type "hidden" :id "lexical-json-input" :name "lexical" :value "")
@@ -56,11 +56,11 @@
:class "px-[20px] py-[6px] bg-stone-700 text-white text-[14px] rounded-[8px] hover:bg-stone-800 transition-colors cursor-pointer" create-label)))) :class "px-[20px] py-[6px] bg-stone-700 text-white text-[14px] rounded-[8px] hover:bg-stone-800 transition-colors cursor-pointer" create-label))))
;; Edit form — pre-populated version for /<slug>/admin/edit/ ;; Edit form — pre-populated version for /<slug>/admin/edit/
(defcomp ~blog-editor-edit-form (&key csrf updated-at title-val excerpt-val (defcomp ~blog-editor-edit-form (&key (csrf :as string) (updated-at :as string) (title-val :as string?) (excerpt-val :as string?)
feature-image feature-image-caption (feature-image :as string?) (feature-image-caption :as string?)
sx-content-val lexical-json (sx-content-val :as string?) (lexical-json :as string?)
has-sx title-placeholder (has-sx :as boolean) (title-placeholder :as string)
status already-emailed (status :as string) (already-emailed :as boolean)
newsletter-options footer-extra) newsletter-options footer-extra)
(let* ((sel-cls "text-[14px] rounded-[4px] border border-stone-200 px-[8px] py-[6px] bg-white text-stone-600") (let* ((sel-cls "text-[14px] rounded-[4px] border border-stone-200 px-[8px] py-[6px] bg-white text-stone-600")
(active "px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent") (active "px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent")
@@ -153,14 +153,14 @@
" sync();" " sync();"
"})();")) "})();"))
(defcomp ~blog-editor-styles (&key css-href) (defcomp ~blog-editor-styles (&key (css-href :as string))
(<> (link :rel "stylesheet" :href css-href) (<> (link :rel "stylesheet" :href css-href)
(style (style
"#lexical-editor { display: flow-root; }" "#lexical-editor { display: flow-root; }"
"#lexical-editor [data-kg-card=\"html\"] * { float: none !important; }" "#lexical-editor [data-kg-card=\"html\"] * { float: none !important; }"
"#lexical-editor [data-kg-card=\"html\"] table { width: 100% !important; }"))) "#lexical-editor [data-kg-card=\"html\"] table { width: 100% !important; }")))
(defcomp ~blog-editor-scripts (&key js-src sx-editor-js-src init-js) (defcomp ~blog-editor-scripts (&key (js-src :as string) (sx-editor-js-src :as string?) (init-js :as string))
(<> (script :src js-src) (<> (script :src js-src)
(when sx-editor-js-src (script :src sx-editor-js-src)) (when sx-editor-js-src (script :src sx-editor-js-src))
(script init-js))) (script init-js)))

View File

@@ -1,11 +1,11 @@
;; Blog filter components ;; Blog filter components
(defcomp ~blog-action-button (&key href hx-select btn-class title icon-class label) (defcomp ~blog-action-button (&key (href :as string) (hx-select :as string) (btn-class :as string) (title :as string) (icon-class :as string) (label :as string))
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class btn-class :title title (i :class icon-class) label)) :class btn-class :title title (i :class icon-class) label))
(defcomp ~blog-drafts-button (&key href hx-select btn-class title label draft-count) (defcomp ~blog-drafts-button (&key (href :as string) (hx-select :as string) (btn-class :as string) (title :as string) (label :as string) (draft-count :as number))
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class btn-class :title title (i :class "fa fa-file-text-o mr-1") " Drafts " :class btn-class :title title (i :class "fa fa-file-text-o mr-1") " Drafts "
@@ -61,7 +61,7 @@
(span :class "flex-1") (span :class "flex-1")
(span :class "inline-block bg-stone-100 text-stone-600 px-2 py-1 text-xs font-medium border border-stone-200" count)))) (span :class "inline-block bg-stone-100 text-stone-600 px-2 py-1 text-xs font-medium border border-stone-200" count))))
(defcomp ~blog-filter-summary (&key text) (defcomp ~blog-filter-summary (&key (text :as string))
(span :class "text-sm text-stone-600" text)) (span :class "text-sm text-stone-600" text))
;; Data-driven tag groups filter (replaces Python _tag_groups_filter_sx loop) ;; Data-driven tag groups filter (replaces Python _tag_groups_filter_sx loop)

View File

@@ -7,7 +7,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Image card ;; Image card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-image (&key src alt caption width href) (defcomp ~kg-image (&key (src :as string) (alt :as string?) (caption :as string?) (width :as string?) (href :as string?))
(figure :class (str "kg-card kg-image-card" (figure :class (str "kg-card kg-image-card"
(if (= width "wide") " kg-width-wide" (if (= width "wide") " kg-width-wide"
(if (= width "full") " kg-width-full" ""))) (if (= width "full") " kg-width-full" "")))
@@ -19,7 +19,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Gallery card ;; Gallery card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-gallery (&key images caption) (defcomp ~kg-gallery (&key (images :as list) (caption :as string?))
(figure :class "kg-card kg-gallery-card kg-width-wide" (figure :class "kg-card kg-gallery-card kg-width-wide"
(div :class "kg-gallery-container" (div :class "kg-gallery-container"
(map (lambda (row) (map (lambda (row)
@@ -48,7 +48,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Embed card ;; Embed card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-embed (&key html caption) (defcomp ~kg-embed (&key (html :as string) (caption :as string?))
(figure :class "kg-card kg-embed-card" (figure :class "kg-card kg-embed-card"
(~rich-text :html html) (~rich-text :html html)
(when caption (figcaption caption)))) (when caption (figcaption caption))))
@@ -56,7 +56,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Bookmark card ;; Bookmark card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-bookmark (&key url title description icon author publisher thumbnail caption) (defcomp ~kg-bookmark (&key (url :as string) (title :as string?) (description :as string?) (icon :as string?) (author :as string?) (publisher :as string?) (thumbnail :as string?) (caption :as string?))
(figure :class "kg-card kg-bookmark-card" (figure :class "kg-card kg-bookmark-card"
(a :class "kg-bookmark-container" :href url (a :class "kg-bookmark-container" :href url
(div :class "kg-bookmark-content" (div :class "kg-bookmark-content"
@@ -75,7 +75,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Callout card ;; Callout card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-callout (&key color emoji content) (defcomp ~kg-callout (&key (color :as string?) (emoji :as string?) (content :as string?))
(div :class (str "kg-card kg-callout-card kg-callout-card-" (or color "grey")) (div :class (str "kg-card kg-callout-card kg-callout-card-" (or color "grey"))
(when emoji (div :class "kg-callout-emoji" emoji)) (when emoji (div :class "kg-callout-emoji" emoji))
(div :class "kg-callout-text" (or content "")))) (div :class "kg-callout-text" (or content ""))))
@@ -83,14 +83,14 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Button card ;; Button card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-button (&key url text alignment) (defcomp ~kg-button (&key (url :as string) (text :as string?) (alignment :as string?))
(div :class (str "kg-card kg-button-card kg-align-" (or alignment "center")) (div :class (str "kg-card kg-button-card kg-align-" (or alignment "center"))
(a :href url :class "kg-btn kg-btn-accent" (or text "")))) (a :href url :class "kg-btn kg-btn-accent" (or text ""))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Toggle card (accordion) ;; Toggle card (accordion)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-toggle (&key heading content) (defcomp ~kg-toggle (&key (heading :as string?) (content :as string?))
(div :class "kg-card kg-toggle-card" :data-kg-toggle-state "close" (div :class "kg-card kg-toggle-card" :data-kg-toggle-state "close"
(div :class "kg-toggle-heading" (div :class "kg-toggle-heading"
(h4 :class "kg-toggle-heading-text" (or heading "")) (h4 :class "kg-toggle-heading-text" (or heading ""))
@@ -101,7 +101,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Audio card ;; Audio card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-audio (&key src title duration thumbnail) (defcomp ~kg-audio (&key (src :as string) (title :as string?) (duration :as string?) (thumbnail :as string?))
(div :class "kg-card kg-audio-card" (div :class "kg-card kg-audio-card"
(if thumbnail (if thumbnail
(img :src thumbnail :alt "audio-thumbnail" :class "kg-audio-thumbnail") (img :src thumbnail :alt "audio-thumbnail" :class "kg-audio-thumbnail")
@@ -124,7 +124,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Video card ;; Video card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-video (&key src caption width thumbnail loop) (defcomp ~kg-video (&key (src :as string) (caption :as string?) (width :as string?) (thumbnail :as string?) (loop :as boolean?))
(figure :class (str "kg-card kg-video-card" (figure :class (str "kg-card kg-video-card"
(if (= width "wide") " kg-width-wide" (if (= width "wide") " kg-width-wide"
(if (= width "full") " kg-width-full" ""))) (if (= width "full") " kg-width-full" "")))
@@ -136,7 +136,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; File card ;; File card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-file (&key src filename title filesize caption) (defcomp ~kg-file (&key (src :as string) (filename :as string?) (title :as string?) (filesize :as string?) (caption :as string?))
(div :class "kg-card kg-file-card" (div :class "kg-card kg-file-card"
(a :class "kg-file-card-container" :href src :download (or filename "") (a :class "kg-file-card-container" :href src :download (or filename "")
(div :class "kg-file-card-contents" (div :class "kg-file-card-contents"

View File

@@ -1,6 +1,6 @@
;; Blog settings panel components (features, markets, associated entries) ;; Blog settings panel components (features, markets, associated entries)
(defcomp ~blog-features-form (&key features-url calendar-checked market-checked hs-trigger) (defcomp ~blog-features-form (&key (features-url :as string) (calendar-checked :as boolean) (market-checked :as boolean) (hs-trigger :as string))
(form :sx-put features-url :sx-target "#features-panel" :sx-swap "outerHTML" (form :sx-put features-url :sx-target "#features-panel" :sx-swap "outerHTML"
:sx-headers {:Content-Type "application/json"} :sx-encoding "json" :class "space-y-3" :sx-headers {:Content-Type "application/json"} :sx-encoding "json" :class "space-y-3"
(label :class "flex items-center gap-3 cursor-pointer" (label :class "flex items-center gap-3 cursor-pointer"
@@ -31,7 +31,7 @@
;; Markets panel ;; Markets panel
(defcomp ~blog-market-item (&key name slug delete-url confirm-text) (defcomp ~blog-market-item (&key (name :as string) (slug :as string) (delete-url :as string) (confirm-text :as string))
(li :class "flex items-center justify-between p-3 bg-stone-50 rounded" (li :class "flex items-center justify-between p-3 bg-stone-50 rounded"
(div (span :class "font-medium" name) (div (span :class "font-medium" name)
(span :class "text-stone-400 text-sm ml-2" (str "/" slug "/"))) (span :class "text-stone-400 text-sm ml-2" (str "/" slug "/")))
@@ -93,11 +93,11 @@
;; Associated entries ;; Associated entries
(defcomp ~blog-entry-image (&key src title) (defcomp ~blog-entry-image (&key (src :as string?) (title :as string))
(if src (img :src src :alt title :class "w-8 h-8 rounded-full object-cover flex-shrink-0") (if src (img :src src :alt title :class "w-8 h-8 rounded-full object-cover flex-shrink-0")
(div :class "w-8 h-8 rounded-full bg-stone-200 flex-shrink-0"))) (div :class "w-8 h-8 rounded-full bg-stone-200 flex-shrink-0")))
(defcomp ~blog-associated-entry (&key confirm-text toggle-url hx-headers img name date-str) (defcomp ~blog-associated-entry (&key (confirm-text :as string) (toggle-url :as string) hx-headers img (name :as string) (date-str :as string))
(button :type "button" (button :type "button"
:class "w-full text-left p-3 rounded border bg-green-50 border-green-300 transition hover:bg-green-100" :class "w-full text-left p-3 rounded border bg-green-50 border-green-300 transition hover:bg-green-100"
:data-confirm "" :data-confirm-title "Remove entry?" :data-confirm "" :data-confirm-title "Remove entry?"
@@ -150,7 +150,7 @@
;; Entries browser composition — replaces _h_post_entries_content ;; Entries browser composition — replaces _h_post_entries_content
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-calendar-browser-item (&key name title image view-url) (defcomp ~blog-calendar-browser-item (&key (name :as string) (title :as string) (image :as string?) (view-url :as string))
(details :class "border rounded-lg bg-white" :data-toggle-group "calendar-browser" (details :class "border rounded-lg bg-white" :data-toggle-group "calendar-browser"
(summary :class "p-4 cursor-pointer hover:bg-stone-50 flex items-center gap-3" (summary :class "p-4 cursor-pointer hover:bg-stone-50 flex items-center gap-3"
(if image (if image
@@ -182,11 +182,11 @@
;; Post settings form composition — replaces _h_post_settings_content ;; Post settings form composition — replaces _h_post_settings_content
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-settings-field-label (&key text field-for) (defcomp ~blog-settings-field-label (&key (text :as string) (field-for :as string))
(label :for field-for (label :for field-for
:class "block text-[13px] font-medium text-stone-500 mb-[4px]" text)) :class "block text-[13px] font-medium text-stone-500 mb-[4px]" text))
(defcomp ~blog-settings-section (&key title content is-open) (defcomp ~blog-settings-section (&key (title :as string) content (is-open :as boolean))
(details :class "border border-stone-200 rounded-[8px] overflow-hidden" :open is-open (details :class "border border-stone-200 rounded-[8px] overflow-hidden" :open is-open
(summary :class "px-[16px] py-[10px] bg-stone-50 text-[14px] font-medium text-stone-600 cursor-pointer select-none hover:bg-stone-100 transition-colors" (summary :class "px-[16px] py-[10px] bg-stone-50 text-[14px] font-medium text-stone-600 cursor-pointer select-none hover:bg-stone-100 transition-colors"
title) title)

View File

@@ -1,6 +1,6 @@
;; Cart calendar entry components ;; Cart calendar entry components
(defcomp ~cart-cal-entry (&key name date-str cost) (defcomp ~cart-cal-entry (&key (name :as string) (date-str :as string) (cost :as string))
(li :class "flex items-start justify-between text-sm" (li :class "flex items-start justify-between text-sm"
(div (div :class "font-medium" name) (div (div :class "font-medium" name)
(div :class "text-xs text-stone-500" date-str)) (div :class "text-xs text-stone-500" date-str))

View File

@@ -1,12 +1,12 @@
;; Cart item components ;; Cart item components
(defcomp ~cart-item-img (&key src alt) (defcomp ~cart-item-img (&key (src :as string) (alt :as string))
(img :src src :alt alt :class "w-24 h-24 sm:w-32 sm:h-28 object-cover rounded-xl border border-stone-100" :loading "lazy")) (img :src src :alt alt :class "w-24 h-24 sm:w-32 sm:h-28 object-cover rounded-xl border border-stone-100" :loading "lazy"))
(defcomp ~cart-item-price (&key text) (defcomp ~cart-item-price (&key (text :as string))
(p :class "text-sm sm:text-base font-semibold text-stone-900" text)) (p :class "text-sm sm:text-base font-semibold text-stone-900" text))
(defcomp ~cart-item-price-was (&key text) (defcomp ~cart-item-price-was (&key (text :as string))
(p :class "text-xs text-stone-400 line-through" text)) (p :class "text-xs text-stone-400 line-through" text))
(defcomp ~cart-item-no-price () (defcomp ~cart-item-no-price ()
@@ -17,13 +17,13 @@
(i :class "fa-solid fa-triangle-exclamation text-[0.6rem]" :aria-hidden "true") (i :class "fa-solid fa-triangle-exclamation text-[0.6rem]" :aria-hidden "true")
" This item is no longer available or price has changed")) " This item is no longer available or price has changed"))
(defcomp ~cart-item-brand (&key brand) (defcomp ~cart-item-brand (&key (brand :as string))
(p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" brand)) (p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" brand))
(defcomp ~cart-item-line-total (&key text) (defcomp ~cart-item-line-total (&key (text :as string))
(p :class "text-sm sm:text-base font-semibold text-stone-900" text)) (p :class "text-sm sm:text-base font-semibold text-stone-900" text))
(defcomp ~cart-item (&key id img prod-url title brand deleted price qty-url csrf minus qty plus line-total) (defcomp ~cart-item (&key (id :as string) img (prod-url :as string) (title :as string) brand deleted price (qty-url :as string) (csrf :as string) (minus :as string) (qty :as string) (plus :as string) line-total)
(article :id id :class "flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4 md:p-5" (article :id id :class "flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4 md:p-5"
(div :class "w-full sm:w-32 shrink-0 flex justify-center sm:block" (when img img)) (div :class "w-full sm:w-32 shrink-0 flex justify-center sm:block" (when img img))
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
@@ -54,7 +54,7 @@
summary)))) summary))))
;; Assembled cart item from serialized data — replaces Python _cart_item_sx ;; Assembled cart item from serialized data — replaces Python _cart_item_sx
(defcomp ~cart-item-from-data (&key item) (defcomp ~cart-item-from-data (&key (item :as dict))
(let* ((slug (or (get item "slug") "")) (let* ((slug (or (get item "slug") ""))
(title (or (get item "title") "")) (title (or (get item "title") ""))
(image (get item "image")) (image (get item "image"))
@@ -96,7 +96,7 @@
(~cart-item-line-total :text (str "Line total: " symbol (format-decimal line-total 2))))))) (~cart-item-line-total :text (str "Line total: " symbol (format-decimal line-total 2)))))))
;; Assembled calendar entries section — replaces Python _calendar_entries_sx ;; Assembled calendar entries section — replaces Python _calendar_entries_sx
(defcomp ~cart-cal-section-from-data (&key entries) (defcomp ~cart-cal-section-from-data (&key (entries :as list))
(when (not (empty? entries)) (when (not (empty? entries))
(~cart-cal-section (~cart-cal-section
:items (map (lambda (e) :items (map (lambda (e)
@@ -108,7 +108,7 @@
entries)))) entries))))
;; Assembled ticket groups section — replaces Python _ticket_groups_sx ;; Assembled ticket groups section — replaces Python _ticket_groups_sx
(defcomp ~cart-tickets-section-from-data (&key ticket-groups) (defcomp ~cart-tickets-section-from-data (&key (ticket-groups :as list))
(when (not (empty? ticket-groups)) (when (not (empty? ticket-groups))
(let* ((csrf (csrf-token)) (let* ((csrf (csrf-token))
(qty-url (url-for "cart_global.update_ticket_quantity"))) (qty-url (url-for "cart_global.update_ticket_quantity")))
@@ -137,7 +137,7 @@
ticket-groups))))) ticket-groups)))))
;; Assembled cart summary — replaces Python _cart_summary_sx ;; Assembled cart summary — replaces Python _cart_summary_sx
(defcomp ~cart-summary-from-data (&key item-count grand-total symbol is-logged-in checkout-action login-href user-email) (defcomp ~cart-summary-from-data (&key (item-count :as number) (grand-total :as number) (symbol :as string) (is-logged-in :as boolean) (checkout-action :as string) (login-href :as string) (user-email :as string?))
(~cart-summary-panel (~cart-summary-panel
:item-count (str item-count) :item-count (str item-count)
:subtotal (str symbol (format-decimal grand-total 2)) :subtotal (str symbol (format-decimal grand-total 2))
@@ -148,7 +148,7 @@
(~cart-checkout-signin :href login-href)))) (~cart-checkout-signin :href login-href))))
;; Assembled page cart content — replaces Python _page_cart_main_panel_sx ;; Assembled page cart content — replaces Python _page_cart_main_panel_sx
(defcomp ~cart-page-cart-content (&key cart-items cal-entries ticket-groups summary) (defcomp ~cart-page-cart-content (&key (cart-items :as list?) (cal-entries :as list?) (ticket-groups :as list?) summary)
(if (and (empty? (or cart-items (list))) (if (and (empty? (or cart-items (list)))
(empty? (or cal-entries (list))) (empty? (or cal-entries (list)))
(empty? (or ticket-groups (list)))) (empty? (or ticket-groups (list))))

View File

@@ -1,6 +1,6 @@
;; Cart overview components ;; Cart overview components
(defcomp ~cart-badge (&key icon text) (defcomp ~cart-badge (&key (icon :as string) (text :as string))
(span :class "inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-stone-100" (span :class "inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-stone-100"
(i :class icon :aria-hidden "true") text)) (i :class icon :aria-hidden "true") text))
@@ -8,13 +8,13 @@
(div :class "mt-1 flex flex-wrap gap-2 text-xs text-stone-600" (div :class "mt-1 flex flex-wrap gap-2 text-xs text-stone-600"
badges)) badges))
(defcomp ~cart-group-card-img (&key src alt) (defcomp ~cart-group-card-img (&key (src :as string) (alt :as string))
(img :src src :alt alt :class "h-16 w-16 rounded-xl object-cover border border-stone-200 flex-shrink-0")) (img :src src :alt alt :class "h-16 w-16 rounded-xl object-cover border border-stone-200 flex-shrink-0"))
(defcomp ~cart-mp-subtitle (&key title) (defcomp ~cart-mp-subtitle (&key (title :as string))
(p :class "text-xs text-stone-500 truncate" title)) (p :class "text-xs text-stone-500 truncate" title))
(defcomp ~cart-group-card (&key href img display-title subtitle badges total) (defcomp ~cart-group-card (&key (href :as string) img (display-title :as string) subtitle badges (total :as string))
(a :href href :class "block rounded-2xl border border-stone-200 bg-white shadow-sm hover:shadow-md hover:border-stone-300 transition p-4 sm:p-5" (a :href href :class "block rounded-2xl border border-stone-200 bg-white shadow-sm hover:shadow-md hover:border-stone-300 transition p-4 sm:p-5"
(div :class "flex items-start gap-4" (div :class "flex items-start gap-4"
img img
@@ -25,7 +25,7 @@
(div :class "text-lg font-bold text-stone-900" total) (div :class "text-lg font-bold text-stone-900" total)
(div :class "mt-1 text-xs text-emerald-700 font-medium" "View cart \u2192"))))) (div :class "mt-1 text-xs text-emerald-700 font-medium" "View cart \u2192")))))
(defcomp ~cart-orphan-card (&key badges total) (defcomp ~cart-orphan-card (&key badges (total :as string))
(div :class "rounded-2xl border border-dashed border-amber-300 bg-amber-50/60 p-4 sm:p-5" (div :class "rounded-2xl border border-dashed border-amber-300 bg-amber-50/60 p-4 sm:p-5"
(div :class "flex items-start gap-4" (div :class "flex items-start gap-4"
(div :class "h-16 w-16 rounded-xl bg-amber-100 flex items-center justify-center flex-shrink-0" (div :class "h-16 w-16 rounded-xl bg-amber-100 flex items-center justify-center flex-shrink-0"
@@ -46,7 +46,7 @@
(~empty-state :icon "fa fa-shopping-cart" :message "Your cart is empty" :cls "text-center")))) (~empty-state :icon "fa fa-shopping-cart" :message "Your cart is empty" :cls "text-center"))))
;; Assembled page group card — replaces Python _page_group_card_sx ;; Assembled page group card — replaces Python _page_group_card_sx
(defcomp ~cart-page-group-card-from-data (&key grp cart-url-base) (defcomp ~cart-page-group-card-from-data (&key (grp :as dict) (cart-url-base :as string))
(let* ((post (get grp "post")) (let* ((post (get grp "post"))
(product-count (or (get grp "product_count") 0)) (product-count (or (get grp "product_count") 0))
(calendar-count (or (get grp "calendar_count") 0)) (calendar-count (or (get grp "calendar_count") 0))
@@ -85,7 +85,7 @@
:total (str "\u00a3" (format-decimal total 2)))))) :total (str "\u00a3" (format-decimal total 2))))))
;; Assembled cart overview content — replaces Python _overview_main_panel_sx ;; Assembled cart overview content — replaces Python _overview_main_panel_sx
(defcomp ~cart-overview-content (&key page-groups cart-url-base) (defcomp ~cart-overview-content (&key (page-groups :as list) (cart-url-base :as string))
(if (empty? page-groups) (if (empty? page-groups)
(~cart-empty) (~cart-empty)
(~cart-overview-panel (~cart-overview-panel

View File

@@ -1,17 +1,17 @@
;; Cart summary / checkout components ;; Cart summary / checkout components
(defcomp ~cart-checkout-form (&key action csrf label) (defcomp ~cart-checkout-form (&key (action :as string) (csrf :as string) (label :as string))
(form :method "post" :action action :class "w-full" (form :method "post" :action action :class "w-full"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class "w-full inline-flex items-center justify-center px-4 py-2 text-xs sm:text-sm rounded-full border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 transition" (button :type "submit" :class "w-full inline-flex items-center justify-center px-4 py-2 text-xs sm:text-sm rounded-full border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 transition"
(i :class "fa-solid fa-credit-card mr-2" :aria-hidden "true") label))) (i :class "fa-solid fa-credit-card mr-2" :aria-hidden "true") label)))
(defcomp ~cart-checkout-signin (&key href) (defcomp ~cart-checkout-signin (&key (href :as string))
(div :class "w-full flex" (div :class "w-full flex"
(a :href href :class "w-full cursor-pointer flex flex-row items-center justify-center p-3 gap-2 rounded bg-stone-200 text-black hover:bg-stone-300 transition" (a :href href :class "w-full cursor-pointer flex flex-row items-center justify-center p-3 gap-2 rounded bg-stone-200 text-black hover:bg-stone-300 transition"
(i :class "fa-solid fa-key") (span "sign in or register to checkout")))) (i :class "fa-solid fa-key") (span "sign in or register to checkout"))))
(defcomp ~cart-summary-panel (&key item-count subtotal checkout) (defcomp ~cart-summary-panel (&key (item-count :as string) (subtotal :as string) checkout)
(aside :id "cart-summary" :class "lg:pl-2" (aside :id "cart-summary" :class "lg:pl-2"
(div :class "rounded-2xl bg-white shadow-sm border border-stone-200 p-4 sm:p-5" (div :class "rounded-2xl bg-white shadow-sm border border-stone-200 p-4 sm:p-5"
(h2 :class "text-sm sm:text-base font-semibold text-stone-900 mb-3 sm:mb-4" "Order summary") (h2 :class "text-sm sm:text-base font-semibold text-stone-900 mb-3 sm:mb-4" "Order summary")

View File

@@ -1,12 +1,12 @@
;; Cart ticket components ;; Cart ticket components
(defcomp ~cart-ticket-type-name (&key name) (defcomp ~cart-ticket-type-name (&key (name :as string))
(p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" name)) (p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" name))
(defcomp ~cart-ticket-type-hidden (&key value) (defcomp ~cart-ticket-type-hidden (&key (value :as string))
(input :type "hidden" :name "ticket_type_id" :value value)) (input :type "hidden" :name "ticket_type_id" :value value))
(defcomp ~cart-ticket-article (&key name type-name date-str price qty-url csrf entry-id type-hidden minus qty plus line-total) (defcomp ~cart-ticket-article (&key (name :as string) type-name (date-str :as string) (price :as string) (qty-url :as string) (csrf :as string) (entry-id :as string) type-hidden (minus :as string) (qty :as string) (plus :as string) (line-total :as string))
(article :class "flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4" (article :class "flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4"
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "flex flex-col sm:flex-row sm:items-start justify-between gap-2 sm:gap-3" (div :class "flex flex-col sm:flex-row sm:items-start justify-between gap-2 sm:gap-3"

View File

@@ -1,28 +1,28 @@
;; Events calendar components ;; Events calendar components
(defcomp ~events-calendar-nav-arrow (&key pill-cls href label) (defcomp ~events-calendar-nav-arrow (&key (pill-cls :as string) (href :as string) (label :as string))
(a :class (str pill-cls " text-xl") :href href (a :class (str pill-cls " text-xl") :href href
:sx-get href :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" label)) :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" label))
(defcomp ~events-calendar-month-label (&key month-name year) (defcomp ~events-calendar-month-label (&key (month-name :as string) (year :as string))
(div :class "px-3 font-medium" (str month-name " " year))) (div :class "px-3 font-medium" (str month-name " " year)))
(defcomp ~events-calendar-weekday (&key name) (defcomp ~events-calendar-weekday (&key (name :as string))
(div :class "py-1" name)) (div :class "py-1" name))
(defcomp ~events-calendar-day-short (&key day-str) (defcomp ~events-calendar-day-short (&key (day-str :as string))
(span :class "sm:hidden text-[16px] text-stone-500" day-str)) (span :class "sm:hidden text-[16px] text-stone-500" day-str))
(defcomp ~events-calendar-day-num (&key pill-cls href num) (defcomp ~events-calendar-day-num (&key (pill-cls :as string) (href :as string) (num :as string))
(a :class pill-cls :href href :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" (a :class pill-cls :href href :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true" num)) :sx-swap "outerHTML" :sx-push-url "true" num))
(defcomp ~events-calendar-entry-badge (&key bg-cls name state-label) (defcomp ~events-calendar-entry-badge (&key (bg-cls :as string) (name :as string) (state-label :as string))
(div :class (str "flex items-center justify-between gap-1 text-[11px] rounded px-1 py-0.5 " bg-cls) (div :class (str "flex items-center justify-between gap-1 text-[11px] rounded px-1 py-0.5 " bg-cls)
(span :class "truncate" name) (span :class "truncate" name)
(span :class "shrink-0 text-[10px] font-semibold uppercase tracking-tight" state-label))) (span :class "shrink-0 text-[10px] font-semibold uppercase tracking-tight" state-label)))
(defcomp ~events-calendar-cell (&key cell-cls day-short day-num badges) (defcomp ~events-calendar-cell (&key (cell-cls :as string) day-short day-num badges)
(div :class cell-cls (div :class cell-cls
(div :class "flex justify-between items-center" (div :class "flex justify-between items-center"
(div :class "flex flex-col" day-short day-num)) (div :class "flex flex-col" day-short day-num))
@@ -37,10 +37,10 @@
(div :class "grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200 rounded-xl overflow-hidden" cells)))) (div :class "grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200 rounded-xl overflow-hidden" cells))))
;; Calendar grid from data — all iteration in sx ;; Calendar grid from data — all iteration in sx
(defcomp ~events-calendar-grid-from-data (&key pill-cls month-name year (defcomp ~events-calendar-grid-from-data (&key (pill-cls :as string) (month-name :as string) (year :as string)
prev-year-href prev-month-href (prev-year-href :as string) (prev-month-href :as string)
next-month-href next-year-href (next-month-href :as string) (next-year-href :as string)
weekday-names cells) (weekday-names :as list) (cells :as list))
(~events-calendar-grid (~events-calendar-grid
:arrows (<> :arrows (<>
(~events-calendar-nav-arrow :pill-cls pill-cls :href prev-year-href :label "\u00ab") (~events-calendar-nav-arrow :pill-cls pill-cls :href prev-year-href :label "\u00ab")
@@ -66,7 +66,7 @@
(get cell "badges")))))) (get cell "badges"))))))
(or cells (list)))))) (or cells (list))))))
(defcomp ~events-calendar-description-display (&key description edit-url) (defcomp ~events-calendar-description-display (&key (description :as string?) (edit-url :as string))
(div :id "calendar-description" (div :id "calendar-description"
(if description (if description
(p :class "text-stone-700 whitespace-pre-line break-all" description) (p :class "text-stone-700 whitespace-pre-line break-all" description)
@@ -75,12 +75,12 @@
:sx-get edit-url :sx-target "#calendar-description" :sx-swap "outerHTML" :sx-get edit-url :sx-target "#calendar-description" :sx-swap "outerHTML"
(i :class "fas fa-edit")))) (i :class "fas fa-edit"))))
(defcomp ~events-calendar-description-title-oob (&key description) (defcomp ~events-calendar-description-title-oob (&key (description :as string))
(div :id "calendar-description-title" :sx-swap-oob "outerHTML" (div :id "calendar-description-title" :sx-swap-oob "outerHTML"
:class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block" :class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
description)) description))
(defcomp ~events-calendar-description-edit-form (&key save-url cancel-url csrf description) (defcomp ~events-calendar-description-edit-form (&key (save-url :as string) (cancel-url :as string) (csrf :as string) (description :as string?))
(div :id "calendar-description" (div :id "calendar-description"
(form :sx-post save-url :sx-target "#calendar-description" :sx-swap "outerHTML" (form :sx-post save-url :sx-target "#calendar-description" :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)

View File

@@ -1,6 +1,6 @@
;; Events day components ;; Events day components
(defcomp ~events-day-entry-link (&key href name time-str) (defcomp ~events-day-entry-link (&key (href :as string) (name :as string) (time-str :as string))
(a :href href :class "flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0" (a :href href :class "flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0"
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "font-medium truncate" name) (div :class "font-medium truncate" name)
@@ -12,7 +12,7 @@
(div :class "flex overflow-x-auto gap-1 scrollbar-thin" (div :class "flex overflow-x-auto gap-1 scrollbar-thin"
inner))) inner)))
(defcomp ~events-day-table (&key list-container rows pre-action add-url) (defcomp ~events-day-table (&key (list-container :as string) rows (pre-action :as string) (add-url :as string))
(section :id "day-entries" :class list-container (section :id "day-entries" :class list-container
(table :class "w-full text-sm border table-fixed" (table :class "w-full text-sm border table-fixed"
(thead :class "bg-stone-100" (thead :class "bg-stone-100"
@@ -32,27 +32,27 @@
(defcomp ~events-day-empty-row () (defcomp ~events-day-empty-row ()
(tr (td :colspan "6" :class "p-3 text-stone-500" "No entries yet."))) (tr (td :colspan "6" :class "p-3 text-stone-500" "No entries yet.")))
(defcomp ~events-day-row-name (&key href pill-cls name) (defcomp ~events-day-row-name (&key (href :as string) (pill-cls :as string) (name :as string))
(td :class "p-2 align-top w-2/6" (div :class "font-medium" (td :class "p-2 align-top w-2/6" (div :class "font-medium"
(a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" (a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true" name)))) :sx-swap "outerHTML" :sx-push-url "true" name))))
(defcomp ~events-day-row-slot (&key href pill-cls slot-name time-str) (defcomp ~events-day-row-slot (&key (href :as string) (pill-cls :as string) (slot-name :as string) (time-str :as string))
(td :class "p-2 align-top w-1/6" (div :class "text-xs font-medium" (td :class "p-2 align-top w-1/6" (div :class "text-xs font-medium"
(a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" (a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true" slot-name) :sx-swap "outerHTML" :sx-push-url "true" slot-name)
(span :class "text-stone-600 font-normal" time-str)))) (span :class "text-stone-600 font-normal" time-str))))
(defcomp ~events-day-row-time (&key start end) (defcomp ~events-day-row-time (&key (start :as string) (end :as string))
(td :class "p-2 align-top w-1/6" (div :class "text-xs text-stone-600" (str start end)))) (td :class "p-2 align-top w-1/6" (div :class "text-xs text-stone-600" (str start end))))
(defcomp ~events-day-row-state (&key state-id badge) (defcomp ~events-day-row-state (&key (state-id :as string) badge)
(td :class "p-2 align-top w-1/6" (div :id state-id badge))) (td :class "p-2 align-top w-1/6" (div :id state-id badge)))
(defcomp ~events-day-row-cost (&key cost-str) (defcomp ~events-day-row-cost (&key (cost-str :as string))
(td :class "p-2 align-top w-1/6" (span :class "font-medium text-green-600" cost-str))) (td :class "p-2 align-top w-1/6" (span :class "font-medium text-green-600" cost-str)))
(defcomp ~events-day-row-tickets (&key price-str count-str) (defcomp ~events-day-row-tickets (&key (price-str :as string) (count-str :as string))
(td :class "p-2 align-top w-1/6" (div :class "text-xs space-y-1" (td :class "p-2 align-top w-1/6" (div :class "text-xs space-y-1"
(div :class "font-medium text-green-600" price-str) (div :class "font-medium text-green-600" price-str)
(div :class "text-stone-600" count-str)))) (div :class "text-stone-600" count-str))))
@@ -63,7 +63,7 @@
(defcomp ~events-day-row-actions () (defcomp ~events-day-row-actions ()
(td :class "p-2 align-top w-1/6")) (td :class "p-2 align-top w-1/6"))
(defcomp ~events-day-row (&key tr-cls name slot state cost tickets actions) (defcomp ~events-day-row (&key (tr-cls :as string) name slot state cost tickets actions)
(tr :class tr-cls name slot state cost tickets actions)) (tr :class tr-cls name slot state cost tickets actions))
(defcomp ~events-day-admin-panel () (defcomp ~events-day-admin-panel ()
@@ -77,14 +77,14 @@
:id "day-entries-nav-wrapper" :sx-swap-oob "true" :id "day-entries-nav-wrapper" :sx-swap-oob "true"
(div :class "flex overflow-x-auto gap-1 scrollbar-thin" items))) (div :class "flex overflow-x-auto gap-1 scrollbar-thin" items)))
(defcomp ~events-day-nav-entry (&key href nav-btn name time-str) (defcomp ~events-day-nav-entry (&key (href :as string) (nav-btn :as string) (name :as string) (time-str :as string))
(a :href href :class nav-btn (a :href href :class nav-btn
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "font-medium truncate" name) (div :class "font-medium truncate" name)
(div :class "text-xs text-stone-600 truncate" time-str)))) (div :class "text-xs text-stone-600 truncate" time-str))))
;; Day table from data — all row iteration in sx ;; Day table from data — all row iteration in sx
(defcomp ~events-day-table-from-data (&key list-container pre-action add-url tr-cls pill-cls rows) (defcomp ~events-day-table-from-data (&key (list-container :as string) (pre-action :as string) (add-url :as string) (tr-cls :as string) (pill-cls :as string) (rows :as list?))
(~events-day-table (~events-day-table
:list-container list-container :list-container list-container
:rows (if (empty? (or rows (list))) :rows (if (empty? (or rows (list)))
@@ -112,7 +112,7 @@
:pre-action pre-action :add-url add-url)) :pre-action pre-action :add-url add-url))
;; Day entries nav OOB from data ;; Day entries nav OOB from data
(defcomp ~events-day-entries-nav-oob-from-data (&key nav-btn entries) (defcomp ~events-day-entries-nav-oob-from-data (&key (nav-btn :as string) (entries :as list?))
(if (empty? (or entries (list))) (if (empty? (or entries (list)))
(~events-day-entries-nav-oob-empty) (~events-day-entries-nav-oob-empty)
(~events-day-entries-nav-oob (~events-day-entries-nav-oob

View File

@@ -1,6 +1,6 @@
;; Events ticket components ;; Events ticket components
(defcomp ~events-ticket-card (&key href entry-name type-name time-str cal-name badge code-prefix) (defcomp ~events-ticket-card (&key (href :as string) (entry-name :as string) (type-name :as string?) (time-str :as string?) (cal-name :as string?) badge (code-prefix :as string))
(a :href href :class "block rounded-xl border border-stone-200 bg-white p-4 hover:shadow-md transition" (a :href href :class "block rounded-xl border border-stone-200 bg-white p-4 hover:shadow-md transition"
(div :class "flex items-start justify-between gap-4" (div :class "flex items-start justify-between gap-4"
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
@@ -12,7 +12,7 @@
badge badge
(span :class "text-xs text-stone-400 font-mono" (str code-prefix "...")))))) (span :class "text-xs text-stone-400 font-mono" (str code-prefix "..."))))))
(defcomp ~events-tickets-panel (&key list-container has-tickets cards) (defcomp ~events-tickets-panel (&key (list-container :as string) (has-tickets :as boolean) cards)
(section :id "tickets-list" :class list-container (section :id "tickets-list" :class list-container
(h1 :class "text-2xl font-bold mb-6" "My Tickets") (h1 :class "text-2xl font-bold mb-6" "My Tickets")
(if has-tickets (if has-tickets
@@ -22,9 +22,9 @@
(p :class "text-lg" "No tickets yet") (p :class "text-lg" "No tickets yet")
(p :class "text-sm mt-1" "Tickets will appear here after you purchase them."))))) (p :class "text-sm mt-1" "Tickets will appear here after you purchase them.")))))
(defcomp ~events-ticket-detail (&key list-container back-href header-bg entry-name badge (defcomp ~events-ticket-detail (&key (list-container :as string) (back-href :as string) (header-bg :as string) (entry-name :as string) badge
type-name code time-date time-range cal-name (type-name :as string?) (code :as string) (time-date :as string?) (time-range :as string?) (cal-name :as string?)
type-desc checkin-str qr-script) (type-desc :as string?) (checkin-str :as string?) (qr-script :as string))
(section :id "ticket-detail" :class (str list-container " max-w-lg mx-auto") (section :id "ticket-detail" :class (str list-container " max-w-lg mx-auto")
(a :href back-href :class "inline-flex items-center gap-1 text-sm text-stone-500 hover:text-stone-700 mb-4" (a :href back-href :class "inline-flex items-center gap-1 text-sm text-stone-500 hover:text-stone-700 mb-4"
(i :class "fa fa-arrow-left" :aria-hidden "true") " Back to my tickets") (i :class "fa fa-arrow-left" :aria-hidden "true") " Back to my tickets")
@@ -54,25 +54,25 @@
(script :src "https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js") (script :src "https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js")
(script qr-script))) (script qr-script)))
(defcomp ~events-ticket-admin-stat (&key border bg text-cls label-cls value label) (defcomp ~events-ticket-admin-stat (&key (border :as string) (bg :as string) (text-cls :as string) (label-cls :as string) (value :as string) (label :as string))
(div :class (str "rounded-xl border " border " " bg " p-4 text-center") (div :class (str "rounded-xl border " border " " bg " p-4 text-center")
(div :class (str "text-2xl font-bold " text-cls) value) (div :class (str "text-2xl font-bold " text-cls) value)
(div :class (str "text-xs " label-cls " uppercase tracking-wide") label))) (div :class (str "text-xs " label-cls " uppercase tracking-wide") label)))
(defcomp ~events-ticket-admin-date (&key date-str) (defcomp ~events-ticket-admin-date (&key (date-str :as string))
(div :class "text-xs text-stone-500" date-str)) (div :class "text-xs text-stone-500" date-str))
(defcomp ~events-ticket-admin-checkin-form (&key checkin-url code csrf) (defcomp ~events-ticket-admin-checkin-form (&key (checkin-url :as string) (code :as string) (csrf :as string))
(form :sx-post checkin-url :sx-target (str "#ticket-row-" code) :sx-swap "outerHTML" (form :sx-post checkin-url :sx-target (str "#ticket-row-" code) :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition" (button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition"
(i :class "fa fa-check mr-1" :aria-hidden "true") "Check in"))) (i :class "fa fa-check mr-1" :aria-hidden "true") "Check in")))
(defcomp ~events-ticket-admin-checked-in (&key time-str) (defcomp ~events-ticket-admin-checked-in (&key (time-str :as string))
(span :class "text-xs text-blue-600" (span :class "text-xs text-blue-600"
(i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str))) (i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str)))
(defcomp ~events-ticket-admin-row (&key code code-short entry-name date type-name badge action) (defcomp ~events-ticket-admin-row (&key (code :as string) (code-short :as string) (entry-name :as string) date (type-name :as string) badge action)
(tr :class "hover:bg-stone-50 transition" :id (str "ticket-row-" code) (tr :class "hover:bg-stone-50 transition" :id (str "ticket-row-" code)
(td :class "px-4 py-3" (span :class "font-mono text-xs" code-short)) (td :class "px-4 py-3" (span :class "font-mono text-xs" code-short))
(td :class "px-4 py-3" (div :class "font-medium" entry-name) date) (td :class "px-4 py-3" (div :class "font-medium" entry-name) date)
@@ -80,7 +80,7 @@
(td :class "px-4 py-3" badge) (td :class "px-4 py-3" badge)
(td :class "px-4 py-3" action))) (td :class "px-4 py-3" action)))
(defcomp ~events-ticket-admin-panel (&key list-container stats lookup-url has-tickets rows) (defcomp ~events-ticket-admin-panel (&key (list-container :as string) stats (lookup-url :as string) (has-tickets :as boolean) rows)
(section :id "ticket-admin" :class list-container (section :id "ticket-admin" :class list-container
(h1 :class "text-2xl font-bold mb-6" "Ticket Admin") (h1 :class "text-2xl font-bold mb-6" "Ticket Admin")
(div :class "grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8" stats) (div :class "grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8" stats)
@@ -113,11 +113,11 @@
(tbody :class "divide-y divide-stone-100" rows)) (tbody :class "divide-y divide-stone-100" rows))
(div :class "px-6 py-8 text-center text-stone-500" "No tickets yet")))))) (div :class "px-6 py-8 text-center text-stone-500" "No tickets yet"))))))
(defcomp ~events-checkin-error (&key message) (defcomp ~events-checkin-error (&key (message :as string))
(div :class "rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800" (div :class "rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800"
(i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message)) (i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message))
(defcomp ~events-checkin-success-row (&key code code-short entry-name date type-name badge time-str) (defcomp ~events-checkin-success-row (&key (code :as string) (code-short :as string) (entry-name :as string) date (type-name :as string) badge (time-str :as string))
(tr :class "bg-blue-50" :id (str "ticket-row-" code) (tr :class "bg-blue-50" :id (str "ticket-row-" code)
(td :class "px-4 py-3" (span :class "font-mono text-xs" code-short)) (td :class "px-4 py-3" (span :class "font-mono text-xs" code-short))
(td :class "px-4 py-3" (div :class "font-medium" entry-name) date) (td :class "px-4 py-3" (div :class "font-medium" entry-name) date)
@@ -127,29 +127,29 @@
(span :class "text-xs text-blue-600" (span :class "text-xs text-blue-600"
(i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str))))) (i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str)))))
(defcomp ~events-lookup-error (&key message) (defcomp ~events-lookup-error (&key (message :as string))
(div :class "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800" (div :class "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800"
(i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message)) (i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message))
(defcomp ~events-lookup-info (&key entry-name) (defcomp ~events-lookup-info (&key (entry-name :as string))
(div :class "font-semibold text-lg" entry-name)) (div :class "font-semibold text-lg" entry-name))
(defcomp ~events-lookup-type (&key type-name) (defcomp ~events-lookup-type (&key (type-name :as string))
(div :class "text-sm text-stone-600" type-name)) (div :class "text-sm text-stone-600" type-name))
(defcomp ~events-lookup-date (&key date-str) (defcomp ~events-lookup-date (&key (date-str :as string))
(div :class "text-sm text-stone-500 mt-1" date-str)) (div :class "text-sm text-stone-500 mt-1" date-str))
(defcomp ~events-lookup-cal (&key cal-name) (defcomp ~events-lookup-cal (&key (cal-name :as string))
(div :class "text-xs text-stone-400 mt-0.5" cal-name)) (div :class "text-xs text-stone-400 mt-0.5" cal-name))
(defcomp ~events-lookup-status (&key badge code) (defcomp ~events-lookup-status (&key badge (code :as string))
(div :class "mt-2" badge (span :class "text-xs text-stone-400 ml-2 font-mono" code))) (div :class "mt-2" badge (span :class "text-xs text-stone-400 ml-2 font-mono" code)))
(defcomp ~events-lookup-checkin-time (&key date-str) (defcomp ~events-lookup-checkin-time (&key (date-str :as string))
(div :class "text-xs text-blue-600 mt-1" (str "Checked in: " date-str))) (div :class "text-xs text-blue-600 mt-1" (str "Checked in: " date-str)))
(defcomp ~events-lookup-checkin-btn (&key checkin-url code csrf) (defcomp ~events-lookup-checkin-btn (&key (checkin-url :as string) (code :as string) (csrf :as string))
(form :sx-post checkin-url :sx-target (str "#checkin-action-" code) :sx-swap "innerHTML" (form :sx-post checkin-url :sx-target (str "#checkin-action-" code) :sx-swap "innerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" (button :type "submit"
@@ -166,20 +166,20 @@
(i :class "fa fa-times-circle text-3xl" :aria-hidden "true") (i :class "fa fa-times-circle text-3xl" :aria-hidden "true")
(div :class "text-sm font-medium mt-1" "Cancelled"))) (div :class "text-sm font-medium mt-1" "Cancelled")))
(defcomp ~events-lookup-card (&key info code action) (defcomp ~events-lookup-card (&key info (code :as string) action)
(div :class "rounded-lg border border-stone-200 bg-stone-50 p-4" (div :class "rounded-lg border border-stone-200 bg-stone-50 p-4"
(div :class "flex items-start justify-between gap-4" (div :class "flex items-start justify-between gap-4"
(div :class "flex-1" info) (div :class "flex-1" info)
(div :id (str "checkin-action-" code) action)))) (div :id (str "checkin-action-" code) action))))
(defcomp ~events-entry-tickets-admin-row (&key code code-short type-name badge action) (defcomp ~events-entry-tickets-admin-row (&key (code :as string) (code-short :as string) (type-name :as string) badge action)
(tr :class "hover:bg-stone-50" :id (str "entry-ticket-row-" code) (tr :class "hover:bg-stone-50" :id (str "entry-ticket-row-" code)
(td :class "px-4 py-2 font-mono text-xs" code-short) (td :class "px-4 py-2 font-mono text-xs" code-short)
(td :class "px-4 py-2" type-name) (td :class "px-4 py-2" type-name)
(td :class "px-4 py-2" badge) (td :class "px-4 py-2" badge)
(td :class "px-4 py-2" action))) (td :class "px-4 py-2" action)))
(defcomp ~events-entry-tickets-admin-checkin (&key checkin-url code csrf) (defcomp ~events-entry-tickets-admin-checkin (&key (checkin-url :as string) (code :as string) (csrf :as string))
(form :sx-post checkin-url :sx-target (str "#entry-ticket-row-" code) :sx-swap "outerHTML" (form :sx-post checkin-url :sx-target (str "#entry-ticket-row-" code) :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700" (button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
@@ -198,7 +198,7 @@
(defcomp ~events-entry-tickets-admin-empty () (defcomp ~events-entry-tickets-admin-empty ()
(div :class "text-center py-6 text-stone-500 text-sm" "No tickets for this entry")) (div :class "text-center py-6 text-stone-500 text-sm" "No tickets for this entry"))
(defcomp ~events-entry-tickets-admin-panel (&key entry-name count-label body) (defcomp ~events-entry-tickets-admin-panel (&key (entry-name :as string) (count-label :as string) body)
(div :class "space-y-4" (div :class "space-y-4"
(div :class "flex items-center justify-between" (div :class "flex items-center justify-between"
(h3 :class "text-lg font-semibold" (str "Tickets for: " entry-name)) (h3 :class "text-lg font-semibold" (str "Tickets for: " entry-name))
@@ -211,7 +211,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; My tickets panel from data ;; My tickets panel from data
(defcomp ~events-tickets-panel-from-data (&key list-container tickets) (defcomp ~events-tickets-panel-from-data (&key (list-container :as string) (tickets :as list?))
(~events-tickets-panel (~events-tickets-panel
:list-container list-container :list-container list-container
:has-tickets (not (empty? (or tickets (list)))) :has-tickets (not (empty? (or tickets (list))))
@@ -225,9 +225,9 @@
(or tickets (list)))))) (or tickets (list))))))
;; Ticket detail from data — uses lg badge variant ;; Ticket detail from data — uses lg badge variant
(defcomp ~events-ticket-detail-from-data (&key list-container back-href header-bg entry-name (defcomp ~events-ticket-detail-from-data (&key (list-container :as string) (back-href :as string) (header-bg :as string) (entry-name :as string)
state type-name code time-date time-range (state :as string) (type-name :as string?) (code :as string) (time-date :as string?) (time-range :as string?)
cal-name type-desc checkin-str qr-script) (cal-name :as string?) (type-desc :as string?) (checkin-str :as string?) (qr-script :as string))
(~events-ticket-detail (~events-ticket-detail
:list-container list-container :back-href back-href :list-container list-container :back-href back-href
:header-bg header-bg :entry-name entry-name :header-bg header-bg :entry-name entry-name
@@ -238,9 +238,9 @@
:checkin-str checkin-str :qr-script qr-script)) :checkin-str checkin-str :qr-script qr-script))
;; Ticket admin row from data — conditional action column ;; Ticket admin row from data — conditional action column
(defcomp ~events-ticket-admin-row-from-data (&key code code-short entry-name date-str (defcomp ~events-ticket-admin-row-from-data (&key (code :as string) (code-short :as string) (entry-name :as string) (date-str :as string?)
type-name state checkin-url csrf (type-name :as string) (state :as string) (checkin-url :as string) (csrf :as string)
checked-in-time) (checked-in-time :as string?))
(~events-ticket-admin-row (~events-ticket-admin-row
:code code :code-short code-short :code code :code-short code-short
:entry-name entry-name :entry-name entry-name
@@ -256,8 +256,8 @@
(true nil)))) (true nil))))
;; Ticket admin panel from data ;; Ticket admin panel from data
(defcomp ~events-ticket-admin-panel-from-data (&key list-container lookup-url tickets (defcomp ~events-ticket-admin-panel-from-data (&key (list-container :as string) (lookup-url :as string) (tickets :as list?)
total confirmed checked-in reserved) (total :as number?) (confirmed :as number?) (checked-in :as number?) (reserved :as number?))
(~events-ticket-admin-panel (~events-ticket-admin-panel
:list-container list-container :list-container list-container
:stats (<> :stats (<>
@@ -285,7 +285,7 @@
(or tickets (list)))))) (or tickets (list))))))
;; Entry tickets admin from data ;; Entry tickets admin from data
(defcomp ~events-entry-tickets-admin-from-data (&key entry-name count-label tickets csrf) (defcomp ~events-entry-tickets-admin-from-data (&key (entry-name :as string) (count-label :as string) (tickets :as list?) (csrf :as string))
(~events-entry-tickets-admin-panel (~events-entry-tickets-admin-panel
:entry-name entry-name :count-label count-label :entry-name entry-name :count-label count-label
:body (if (empty? (or tickets (list))) :body (if (empty? (or tickets (list)))
@@ -306,7 +306,7 @@
(or tickets (list)))))))) (or tickets (list))))))))
;; Checkin success row from data ;; Checkin success row from data
(defcomp ~events-checkin-success-row-from-data (&key code code-short entry-name date-str type-name time-str) (defcomp ~events-checkin-success-row-from-data (&key (code :as string) (code-short :as string) (entry-name :as string) (date-str :as string?) (type-name :as string) (time-str :as string))
(~events-checkin-success-row (~events-checkin-success-row
:code code :code-short code-short :code code :code-short code-short
:entry-name entry-name :entry-name entry-name
@@ -316,8 +316,8 @@
:time-str time-str)) :time-str time-str))
;; Ticket types table from data ;; Ticket types table from data
(defcomp ~events-ticket-types-table-from-data (&key list-container ticket-types action-btn add-url (defcomp ~events-ticket-types-table-from-data (&key (list-container :as string) (ticket-types :as list?) (action-btn :as string) (add-url :as string)
tr-cls pill-cls hx-select csrf-hdr) (tr-cls :as string) (pill-cls :as string) (hx-select :as string) (csrf-hdr :as string))
(~events-ticket-types-table (~events-ticket-types-table
:list-container list-container :list-container list-container
:rows (if (empty? (or ticket-types (list))) :rows (if (empty? (or ticket-types (list)))
@@ -333,9 +333,9 @@
:action-btn action-btn :add-url add-url)) :action-btn action-btn :add-url add-url))
;; Lookup result from data ;; Lookup result from data
(defcomp ~events-lookup-result-from-data (&key entry-name type-name date-str cal-name (defcomp ~events-lookup-result-from-data (&key (entry-name :as string) (type-name :as string?) (date-str :as string?) (cal-name :as string?)
state code checked-in-str (state :as string) (code :as string) (checked-in-str :as string?)
checkin-url csrf) (checkin-url :as string) (csrf :as string))
(~events-lookup-card (~events-lookup-card
:info (<> :info (<>
(~events-lookup-info :entry-name entry-name) (~events-lookup-info :entry-name entry-name)

View File

@@ -1,9 +1,9 @@
;; Notification components ;; Notification components
(defcomp ~federation-notification-preview (&key preview) (defcomp ~federation-notification-preview (&key (preview :as string))
(div :class "text-sm text-stone-500 mt-1 truncate" preview)) (div :class "text-sm text-stone-500 mt-1 truncate" preview))
(defcomp ~federation-notification-card (&key cls avatar from-name from-username from-domain action-text preview time) (defcomp ~federation-notification-card (&key (cls :as string) avatar (from-name :as string) (from-username :as string) (from-domain :as string) (action-text :as string) preview (time :as string))
(div :class cls (div :class cls
(div :class "flex items-start gap-3" (div :class "flex items-start gap-3"
avatar avatar
@@ -15,14 +15,14 @@
preview preview
(div :class "text-xs text-stone-400 mt-1" time))))) (div :class "text-xs text-stone-400 mt-1" time)))))
(defcomp ~federation-notifications-list (&key items) (defcomp ~federation-notifications-list (&key (items :as list))
(div :class "space-y-2" items)) (div :class "space-y-2" items))
(defcomp ~federation-notifications-page (&key notifs) (defcomp ~federation-notifications-page (&key notifs)
(h1 :class "text-2xl font-bold mb-6" "Notifications") notifs) (h1 :class "text-2xl font-bold mb-6" "Notifications") notifs)
;; Assembled notification card — replaces Python _notification_sx ;; Assembled notification card — replaces Python _notification_sx
(defcomp ~federation-notification-from-data (&key notif) (defcomp ~federation-notification-from-data (&key (notif :as dict))
(let* ((from-name (or (get notif "from_actor_name") "?")) (let* ((from-name (or (get notif "from_actor_name") "?"))
(from-username (or (get notif "from_actor_username") "")) (from-username (or (get notif "from_actor_username") ""))
(from-domain (or (get notif "from_actor_domain") "")) (from-domain (or (get notif "from_actor_domain") ""))
@@ -59,7 +59,7 @@
:time created))) :time created)))
;; Assembled notifications content — replaces Python _notifications_content_sx ;; Assembled notifications content — replaces Python _notifications_content_sx
(defcomp ~federation-notifications-content (&key notifications) (defcomp ~federation-notifications-content (&key (notifications :as list))
(~federation-notifications-page (~federation-notifications-page
:notifs (if (empty? notifications) :notifs (if (empty? notifications)
(~empty-state :message "No notifications yet." :cls "text-stone-500") (~empty-state :message "No notifications yet." :cls "text-stone-500")

View File

@@ -1,6 +1,6 @@
;; Profile and actor timeline components ;; Profile and actor timeline components
(defcomp ~federation-actor-profile-header (&key avatar display-name username domain summary follow) (defcomp ~federation-actor-profile-header (&key avatar (display-name :as string) (username :as string) (domain :as string) summary follow)
(div :class "bg-white rounded-lg shadow-sm border border-stone-200 p-6 mb-6" (div :class "bg-white rounded-lg shadow-sm border border-stone-200 p-6 mb-6"
(div :class "flex items-center gap-4" (div :class "flex items-center gap-4"
avatar avatar
@@ -14,35 +14,35 @@
header header
(div :id "timeline" timeline)) (div :id "timeline" timeline))
(defcomp ~federation-follow-form (&key action csrf actor-url label cls) (defcomp ~federation-follow-form (&key (action :as string) (csrf :as string) (actor-url :as string) (label :as string) (cls :as string))
(div :class "flex-shrink-0" (div :class "flex-shrink-0"
(form :method "post" :action action (form :method "post" :action action
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "actor_url" :value actor-url) (input :type "hidden" :name "actor_url" :value actor-url)
(button :type "submit" :class cls label)))) (button :type "submit" :class cls label))))
(defcomp ~federation-profile-summary (&key summary) (defcomp ~federation-profile-summary (&key (summary :as string))
(div :class "text-sm text-stone-600 mt-2" (~rich-text :html summary))) (div :class "text-sm text-stone-600 mt-2" (~rich-text :html summary)))
;; Public profile page ;; Public profile page
(defcomp ~federation-activity-obj-type (&key obj-type) (defcomp ~federation-activity-obj-type (&key (obj-type :as string))
(span :class "text-sm text-stone-500" obj-type)) (span :class "text-sm text-stone-500" obj-type))
(defcomp ~federation-activity-card (&key activity-type published obj-type) (defcomp ~federation-activity-card (&key (activity-type :as string) (published :as string) obj-type)
(div :class "bg-white rounded-lg shadow p-4" (div :class "bg-white rounded-lg shadow p-4"
(div :class "flex justify-between items-start" (div :class "flex justify-between items-start"
(span :class "font-medium" activity-type) (span :class "font-medium" activity-type)
(span :class "text-sm text-stone-400" published)) (span :class "text-sm text-stone-400" published))
obj-type)) obj-type))
(defcomp ~federation-activities-list (&key items) (defcomp ~federation-activities-list (&key (items :as list))
(div :class "space-y-4" items)) (div :class "space-y-4" items))
(defcomp ~federation-activities-empty () (defcomp ~federation-activities-empty ()
(p :class "text-stone-500" "No activities yet.")) (p :class "text-stone-500" "No activities yet."))
(defcomp ~federation-profile-page (&key display-name username domain summary activities-heading activities) (defcomp ~federation-profile-page (&key (display-name :as string) (username :as string) (domain :as string) summary (activities-heading :as string) activities)
(div :class "py-8" (div :class "py-8"
(div :class "bg-white rounded-lg shadow p-6 mb-6" (div :class "bg-white rounded-lg shadow p-6 mb-6"
(h1 :class "text-2xl font-bold" display-name) (h1 :class "text-2xl font-bold" display-name)
@@ -51,11 +51,11 @@
(h2 :class "text-xl font-bold mb-4" activities-heading) (h2 :class "text-xl font-bold mb-4" activities-heading)
activities)) activities))
(defcomp ~federation-profile-summary-text (&key text) (defcomp ~federation-profile-summary-text (&key (text :as string))
(p :class "mt-2" text)) (p :class "mt-2" text))
;; Assembled actor timeline content — replaces Python _actor_timeline_content_sx ;; Assembled actor timeline content — replaces Python _actor_timeline_content_sx
(defcomp ~federation-actor-timeline-content (&key remote-actor items is-following actor) (defcomp ~federation-actor-timeline-content (&key (remote-actor :as dict) (items :as list) (is-following :as boolean) actor)
(let* ((display-name (or (get remote-actor "display_name") (get remote-actor "preferred_username") "")) (let* ((display-name (or (get remote-actor "display_name") (get remote-actor "preferred_username") ""))
(icon-url (get remote-actor "icon_url")) (icon-url (get remote-actor "icon_url"))
(summary (get remote-actor "summary")) (summary (get remote-actor "summary"))
@@ -92,7 +92,7 @@
:before (get (last items) "before_cursor"))))))) :before (get (last items) "before_cursor")))))))
;; Data-driven activities list (replaces Python loop in render_profile_page) ;; Data-driven activities list (replaces Python loop in render_profile_page)
(defcomp ~federation-activities-from-data (&key activities) (defcomp ~federation-activities-from-data (&key (activities :as list))
(if (empty? (or activities (list))) (if (empty? (or activities (list)))
(~federation-activities-empty) (~federation-activities-empty)
(~federation-activities-list (~federation-activities-list

View File

@@ -1,37 +1,37 @@
;; Search and actor card components ;; Search and actor card components
;; Aliases — delegate to shared ~avatar ;; Aliases — delegate to shared ~avatar
(defcomp ~federation-actor-avatar-img (&key src cls) (defcomp ~federation-actor-avatar-img (&key (src :as string) (cls :as string))
(~avatar :src src :cls cls)) (~avatar :src src :cls cls))
(defcomp ~federation-actor-avatar-placeholder (&key cls initial) (defcomp ~federation-actor-avatar-placeholder (&key (cls :as string) (initial :as string))
(~avatar :cls cls :initial initial)) (~avatar :cls cls :initial initial))
(defcomp ~federation-actor-name-link (&key href name) (defcomp ~federation-actor-name-link (&key (href :as string) (name :as string))
(a :href href :class "font-semibold text-stone-900 hover:underline" name)) (a :href href :class "font-semibold text-stone-900 hover:underline" name))
(defcomp ~federation-actor-name-link-external (&key href name) (defcomp ~federation-actor-name-link-external (&key (href :as string) (name :as string))
(a :href href :target "_blank" :rel "noopener" (a :href href :target "_blank" :rel "noopener"
:class "font-semibold text-stone-900 hover:underline" name)) :class "font-semibold text-stone-900 hover:underline" name))
(defcomp ~federation-actor-summary (&key summary) (defcomp ~federation-actor-summary (&key (summary :as string))
(div :class "text-sm text-stone-600 mt-1 truncate" (~rich-text :html summary))) (div :class "text-sm text-stone-600 mt-1 truncate" (~rich-text :html summary)))
(defcomp ~federation-unfollow-button (&key action csrf actor-url) (defcomp ~federation-unfollow-button (&key (action :as string) (csrf :as string) (actor-url :as string))
(div :class "flex-shrink-0" (div :class "flex-shrink-0"
(form :method "post" :action action :sx-post action :sx-target "closest article" :sx-swap "outerHTML" (form :method "post" :action action :sx-post action :sx-target "closest article" :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "actor_url" :value actor-url) (input :type "hidden" :name "actor_url" :value actor-url)
(button :type "submit" :class "text-sm border border-stone-300 rounded px-3 py-1 hover:bg-stone-100" "Unfollow")))) (button :type "submit" :class "text-sm border border-stone-300 rounded px-3 py-1 hover:bg-stone-100" "Unfollow"))))
(defcomp ~federation-follow-button (&key action csrf actor-url label) (defcomp ~federation-follow-button (&key (action :as string) (csrf :as string) (actor-url :as string) (label :as string))
(div :class "flex-shrink-0" (div :class "flex-shrink-0"
(form :method "post" :action action :sx-post action :sx-target "closest article" :sx-swap "outerHTML" (form :method "post" :action action :sx-post action :sx-target "closest article" :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "actor_url" :value actor-url) (input :type "hidden" :name "actor_url" :value actor-url)
(button :type "submit" :class "text-sm bg-stone-800 text-white rounded px-3 py-1 hover:bg-stone-700" label)))) (button :type "submit" :class "text-sm bg-stone-800 text-white rounded px-3 py-1 hover:bg-stone-700" label))))
(defcomp ~federation-actor-card (&key cls id avatar name username domain summary button) (defcomp ~federation-actor-card (&key (cls :as string) (id :as string) avatar name (username :as string) (domain :as string) summary button)
(article :class cls :id id (article :class cls :id id
avatar avatar
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
@@ -41,7 +41,7 @@
button)) button))
;; Data-driven actor card (replaces Python _actor_card_sx loop) ;; Data-driven actor card (replaces Python _actor_card_sx loop)
(defcomp ~federation-actor-card-from-data (&key d has-actor csrf follow-url unfollow-url list-type) (defcomp ~federation-actor-card-from-data (&key (d :as dict) (has-actor :as boolean) (csrf :as string) (follow-url :as string) (unfollow-url :as string) (list-type :as string))
(let* ((icon-url (get d "icon_url")) (let* ((icon-url (get d "icon_url"))
(display-name (get d "display_name")) (display-name (get d "display_name"))
(username (get d "username")) (username (get d "username"))
@@ -72,8 +72,8 @@
:summary summary-sx :button button))) :summary summary-sx :button button)))
;; Data-driven actor list (replaces Python _search_results_sx / _actor_list_items_sx loops) ;; Data-driven actor list (replaces Python _search_results_sx / _actor_list_items_sx loops)
(defcomp ~federation-actor-list-from-data (&key actors next-url has-actor csrf (defcomp ~federation-actor-list-from-data (&key (actors :as list) (next-url :as string?) (has-actor :as boolean) (csrf :as string)
follow-url unfollow-url list-type) (follow-url :as string) (unfollow-url :as string) (list-type :as string))
(<> (<>
(map (lambda (d) (map (lambda (d)
(~federation-actor-card-from-data :d d :has-actor has-actor :csrf csrf (~federation-actor-card-from-data :d d :has-actor has-actor :csrf csrf
@@ -81,10 +81,10 @@
(or actors (list))) (or actors (list)))
(when next-url (~federation-scroll-sentinel :url next-url)))) (when next-url (~federation-scroll-sentinel :url next-url))))
(defcomp ~federation-search-info (&key cls text) (defcomp ~federation-search-info (&key (cls :as string) (text :as string))
(p :class cls text)) (p :class cls text))
(defcomp ~federation-search-page (&key search-url search-page-url query info results) (defcomp ~federation-search-page (&key (search-url :as string) (search-page-url :as string) (query :as string) info results)
(h1 :class "text-2xl font-bold mb-6" "Search") (h1 :class "text-2xl font-bold mb-6" "Search")
(form :method "get" :action search-url :class "mb-6" (form :method "get" :action search-url :class "mb-6"
:sx-get search-page-url :sx-target "#search-results" :sx-push-url search-url :sx-get search-page-url :sx-target "#search-results" :sx-push-url search-url
@@ -97,7 +97,7 @@
(div :id "search-results" results)) (div :id "search-results" results))
;; Following / Followers list page ;; Following / Followers list page
(defcomp ~federation-actor-list-page (&key title count-str items) (defcomp ~federation-actor-list-page (&key (title :as string) (count-str :as string) items)
(h1 :class "text-2xl font-bold mb-6" title " " (h1 :class "text-2xl font-bold mb-6" title " "
(span :class "text-stone-400 font-normal" count-str)) (span :class "text-stone-400 font-normal" count-str))
(div :id "actor-list" items)) (div :id "actor-list" items))
@@ -106,7 +106,7 @@
;; Assembled actor card — replaces Python _actor_card_sx ;; Assembled actor card — replaces Python _actor_card_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~federation-actor-card-from-data (&key a actor followed-urls list-type) (defcomp ~federation-actor-card-from-data (&key (a :as dict) actor (followed-urls :as list) (list-type :as string))
(let* ((display-name (or (get a "display_name") (get a "preferred_username") "")) (let* ((display-name (or (get a "display_name") (get a "preferred_username") ""))
(username (or (get a "preferred_username") "")) (username (or (get a "preferred_username") ""))
(domain (or (get a "domain") "")) (domain (or (get a "domain") ""))
@@ -146,7 +146,7 @@
:label (if (= list-type "followers") "Follow Back" "Follow"))))))) :label (if (= list-type "followers") "Follow Back" "Follow")))))))
;; Assembled search content — replaces Python _search_content_sx ;; Assembled search content — replaces Python _search_content_sx
(defcomp ~federation-search-content (&key query actors total followed-urls actor) (defcomp ~federation-search-content (&key (query :as string?) (actors :as list) (total :as number) (followed-urls :as list) actor)
(~federation-search-page (~federation-search-page
:search-url (url-for "social.defpage_search") :search-url (url-for "social.defpage_search")
:search-page-url (url-for "social.search_page") :search-page-url (url-for "social.search_page")
@@ -172,7 +172,7 @@
:url (url-for "social.search_page" :q query :page 2))))))) :url (url-for "social.search_page" :q query :page 2)))))))
;; Assembled following/followers content — replaces Python _following_content_sx etc. ;; Assembled following/followers content — replaces Python _following_content_sx etc.
(defcomp ~federation-following-content (&key actors total actor) (defcomp ~federation-following-content (&key (actors :as list) (total :as number) actor)
(~federation-actor-list-page (~federation-actor-list-page
:title "Following" :count-str (str "(" total ")") :title "Following" :count-str (str "(" total ")")
:items (when (not (empty? actors)) :items (when (not (empty? actors))
@@ -185,7 +185,7 @@
(~federation-scroll-sentinel (~federation-scroll-sentinel
:url (url-for "social.following_list_page" :page 2))))))) :url (url-for "social.following_list_page" :page 2)))))))
(defcomp ~federation-followers-content (&key actors total followed-urls actor) (defcomp ~federation-followers-content (&key (actors :as list) (total :as number) (followed-urls :as list) actor)
(~federation-actor-list-page (~federation-actor-list-page
:title "Followers" :count-str (str "(" total ")") :title "Followers" :count-str (str "(" total ")")
:items (when (not (empty? actors)) :items (when (not (empty? actors))

View File

@@ -2,11 +2,11 @@
;; --- Navigation --- ;; --- Navigation ---
(defcomp ~federation-nav-choose-username (&key url) (defcomp ~federation-nav-choose-username (&key (url :as string))
(nav :class "flex gap-3 text-sm items-center" (nav :class "flex gap-3 text-sm items-center"
(a :href url :class "px-2 py-1 rounded hover:bg-stone-200 font-bold" "Choose username"))) (a :href url :class "px-2 py-1 rounded hover:bg-stone-200 font-bold" "Choose username")))
(defcomp ~federation-nav-notification-link (&key href cls count-url) (defcomp ~federation-nav-notification-link (&key (href :as string) (cls :as string) (count-url :as string))
(a :href href :class cls "Notifications" (a :href href :class cls "Notifications"
(span :sx-get count-url :sx-trigger "load, every 30s" :sx-swap "innerHTML" (span :sx-get count-url :sx-trigger "load, every 30s" :sx-swap "innerHTML"
:class "absolute -top-2 -right-3 text-xs bg-red-500 text-white rounded-full px-1 empty:hidden"))) :class "absolute -top-2 -right-3 text-xs bg-red-500 text-white rounded-full px-1 empty:hidden")))
@@ -20,28 +20,28 @@
;; --- Post card --- ;; --- Post card ---
(defcomp ~federation-boost-label (&key name) (defcomp ~federation-boost-label (&key (name :as string))
(div :class "text-sm text-stone-500 mb-2" "Boosted by " name)) (div :class "text-sm text-stone-500 mb-2" "Boosted by " name))
;; Aliases — delegate to shared ~avatar ;; Aliases — delegate to shared ~avatar
(defcomp ~federation-avatar-img (&key src cls) (defcomp ~federation-avatar-img (&key (src :as string) (cls :as string))
(~avatar :src src :cls cls)) (~avatar :src src :cls cls))
(defcomp ~federation-avatar-placeholder (&key cls initial) (defcomp ~federation-avatar-placeholder (&key (cls :as string) (initial :as string))
(~avatar :cls cls :initial initial)) (~avatar :cls cls :initial initial))
(defcomp ~federation-content (&key content summary) (defcomp ~federation-content (&key (content :as string) (summary :as string?))
(if summary (if summary
(details :class "mt-2" (details :class "mt-2"
(summary :class "text-stone-500 cursor-pointer" "CW: " (~rich-text :html summary)) (summary :class "text-stone-500 cursor-pointer" "CW: " (~rich-text :html summary))
(div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content))) (div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content)))
(div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content)))) (div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content))))
(defcomp ~federation-original-link (&key url) (defcomp ~federation-original-link (&key (url :as string))
(a :href url :target "_blank" :rel "noopener" (a :href url :target "_blank" :rel "noopener"
:class "text-sm text-stone-400 hover:underline mt-1 inline-block" "original")) :class "text-sm text-stone-400 hover:underline mt-1 inline-block" "original"))
(defcomp ~federation-post-card (&key boost avatar actor-name actor-username domain time content original interactions) (defcomp ~federation-post-card (&key boost avatar (actor-name :as string) (actor-username :as string) (domain :as string) (time :as string) content original interactions)
(article :class "bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-4" (article :class "bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-4"
boost boost
(div :class "flex items-start gap-3" (div :class "flex items-start gap-3"
@@ -55,17 +55,17 @@
;; --- Interaction buttons --- ;; --- Interaction buttons ---
(defcomp ~federation-reply-link (&key url) (defcomp ~federation-reply-link (&key (url :as string))
(a :href url :class "hover:text-stone-700" "Reply")) (a :href url :class "hover:text-stone-700" "Reply"))
(defcomp ~federation-like-form (&key action target oid ainbox csrf cls icon count) (defcomp ~federation-like-form (&key (action :as string) (target :as string) (oid :as string) (ainbox :as string) (csrf :as string) (cls :as string) (icon :as string) count)
(form :sx-post action :sx-target target :sx-swap "innerHTML" (form :sx-post action :sx-target target :sx-swap "innerHTML"
(input :type "hidden" :name "object_id" :value oid) (input :type "hidden" :name "object_id" :value oid)
(input :type "hidden" :name "author_inbox" :value ainbox) (input :type "hidden" :name "author_inbox" :value ainbox)
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class cls (span icon) " " count))) (button :type "submit" :class cls (span icon) " " count)))
(defcomp ~federation-boost-form (&key action target oid ainbox csrf cls count) (defcomp ~federation-boost-form (&key (action :as string) (target :as string) (oid :as string) (ainbox :as string) (csrf :as string) (cls :as string) count)
(form :sx-post action :sx-target target :sx-swap "innerHTML" (form :sx-post action :sx-target target :sx-swap "innerHTML"
(input :type "hidden" :name "object_id" :value oid) (input :type "hidden" :name "object_id" :value oid)
(input :type "hidden" :name "author_inbox" :value ainbox) (input :type "hidden" :name "author_inbox" :value ainbox)
@@ -78,13 +78,13 @@
;; --- Timeline --- ;; --- Timeline ---
(defcomp ~federation-scroll-sentinel (&key url) (defcomp ~federation-scroll-sentinel (&key (url :as string))
(div :sx-get url :sx-trigger "revealed" :sx-swap "outerHTML")) (div :sx-get url :sx-trigger "revealed" :sx-swap "outerHTML"))
(defcomp ~federation-compose-button (&key url) (defcomp ~federation-compose-button (&key (url :as string))
(a :href url :class "bg-stone-800 text-white px-4 py-2 rounded hover:bg-stone-700" "Compose")) (a :href url :class "bg-stone-800 text-white px-4 py-2 rounded hover:bg-stone-700" "Compose"))
(defcomp ~federation-timeline-page (&key label compose timeline) (defcomp ~federation-timeline-page (&key (label :as string) compose timeline)
(div :class "flex items-center justify-between mb-6" (div :class "flex items-center justify-between mb-6"
(h1 :class "text-2xl font-bold" label " Timeline") (h1 :class "text-2xl font-bold" label " Timeline")
compose) compose)
@@ -92,9 +92,9 @@
;; --- Data-driven post card (replaces Python _post_card_sx loop) --- ;; --- Data-driven post card (replaces Python _post_card_sx loop) ---
(defcomp ~federation-post-card-from-data (&key d has-actor csrf (defcomp ~federation-post-card-from-data (&key (d :as dict) (has-actor :as boolean) (csrf :as string)
like-url unlike-url (like-url :as string) (unlike-url :as string)
boost-url unboost-url) (boost-url :as string) (unboost-url :as string))
(let* ((boosted-by (get d "boosted_by")) (let* ((boosted-by (get d "boosted_by"))
(actor-icon (get d "actor_icon")) (actor-icon (get d "actor_icon"))
(actor-name (get d "actor_name")) (actor-name (get d "actor_name"))
@@ -140,8 +140,8 @@
:interactions interactions))) :interactions interactions)))
;; Data-driven timeline items (replaces Python _timeline_items_sx loop) ;; Data-driven timeline items (replaces Python _timeline_items_sx loop)
(defcomp ~federation-timeline-items-from-data (&key items next-url has-actor csrf (defcomp ~federation-timeline-items-from-data (&key (items :as list) (next-url :as string?) (has-actor :as boolean) (csrf :as string)
like-url unlike-url boost-url unboost-url) (like-url :as string) (unlike-url :as string) (boost-url :as string) (unboost-url :as string))
(<> (<>
(map (lambda (d) (map (lambda (d)
(~federation-post-card-from-data :d d :has-actor has-actor :csrf csrf (~federation-post-card-from-data :d d :has-actor has-actor :csrf csrf
@@ -151,11 +151,11 @@
;; --- Compose --- ;; --- Compose ---
(defcomp ~federation-compose-reply (&key reply-to) (defcomp ~federation-compose-reply (&key (reply-to :as string))
(input :type "hidden" :name "in_reply_to" :value reply-to) (input :type "hidden" :name "in_reply_to" :value reply-to)
(div :class "text-sm text-stone-500" "Replying to " (span :class "font-mono" reply-to))) (div :class "text-sm text-stone-500" "Replying to " (span :class "font-mono" reply-to)))
(defcomp ~federation-compose-form (&key action csrf reply) (defcomp ~federation-compose-form (&key (action :as string) (csrf :as string) reply)
(h1 :class "text-2xl font-bold mb-6" "Compose") (h1 :class "text-2xl font-bold mb-6" "Compose")
(form :method "post" :action action :class "space-y-4" (form :method "post" :action action :class "space-y-4"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
@@ -208,7 +208,7 @@
;; Assembled post card — replaces Python _post_card_sx ;; Assembled post card — replaces Python _post_card_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~federation-post-card-from-data (&key item actor) (defcomp ~federation-post-card-from-data (&key (item :as dict) actor)
(let* ((boosted-by (get item "boosted_by")) (let* ((boosted-by (get item "boosted_by"))
(actor-icon (get item "actor_icon")) (actor-icon (get item "actor_icon"))
(actor-name (or (get item "actor_name") "?")) (actor-name (or (get item "actor_name") "?"))
@@ -267,7 +267,7 @@
;; Assembled timeline items — replaces Python _timeline_items_sx ;; Assembled timeline items — replaces Python _timeline_items_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~federation-timeline-items (&key items timeline-type actor next-url) (defcomp ~federation-timeline-items (&key (items :as list) (timeline-type :as string) actor (next-url :as string?))
(<> (<>
(map (lambda (item) (map (lambda (item)
(~federation-post-card-from-data :item item :actor actor)) (~federation-post-card-from-data :item item :actor actor))
@@ -276,7 +276,7 @@
(~federation-scroll-sentinel :url next-url)))) (~federation-scroll-sentinel :url next-url))))
;; Assembled timeline content — replaces Python _timeline_content_sx ;; Assembled timeline content — replaces Python _timeline_content_sx
(defcomp ~federation-timeline-content (&key items timeline-type actor) (defcomp ~federation-timeline-content (&key (items :as list) (timeline-type :as string) actor)
(let* ((label (if (= timeline-type "home") "Home" "Public"))) (let* ((label (if (= timeline-type "home") "Home" "Public")))
(~federation-timeline-page (~federation-timeline-page
:label label :label label
@@ -289,7 +289,7 @@
:before (get (last items) "before_cursor"))))))) :before (get (last items) "before_cursor")))))))
;; Assembled compose content — replaces Python _compose_content_sx ;; Assembled compose content — replaces Python _compose_content_sx
(defcomp ~federation-compose-content (&key reply-to) (defcomp ~federation-compose-content (&key (reply-to :as string?))
(~federation-compose-form (~federation-compose-form
:action (url-for "social.compose_submit") :action (url-for "social.compose_submit")
:csrf (csrf-token) :csrf (csrf-token)

View File

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

View File

@@ -1,6 +1,6 @@
;; Market product detail components ;; Market product detail components
(defcomp ~market-detail-gallery-inner (&key like image alt labels brand) (defcomp ~market-detail-gallery-inner (&key (like :as list?) (image :as string) (alt :as string) (labels :as list?) (brand :as string))
(<> like (<> like
(figure :class "inline-block" (figure :class "inline-block"
(div :class "relative w-full aspect-square" (div :class "relative w-full aspect-square"
@@ -18,79 +18,79 @@
:class "absolute right-2 top-1/2 -translate-y-1/2 z-10 grid place-items-center w-12 h-12 md:w-14 md:h-14 rounded-full bg-white/90 hover:bg-white shadow-lg text-3xl md:text-4xl" :class "absolute right-2 top-1/2 -translate-y-1/2 z-10 grid place-items-center w-12 h-12 md:w-14 md:h-14 rounded-full bg-white/90 hover:bg-white shadow-lg text-3xl md:text-4xl"
:title "Next" "\u203a"))) :title "Next" "\u203a")))
(defcomp ~market-detail-gallery (&key inner nav) (defcomp ~market-detail-gallery (&key (inner :as list) (nav :as list?))
(div :class "relative rounded-xl overflow-hidden bg-stone-100" (div :class "relative rounded-xl overflow-hidden bg-stone-100"
inner nav)) inner nav))
(defcomp ~market-detail-thumb (&key title src alt) (defcomp ~market-detail-thumb (&key (title :as string) (src :as string) (alt :as string))
(<> (button :type "button" :data-thumb "" (<> (button :type "button" :data-thumb ""
:class "shrink-0 rounded-lg overflow-hidden bg-stone-100 hover:opacity-90 ring-offset-2" :class "shrink-0 rounded-lg overflow-hidden bg-stone-100 hover:opacity-90 ring-offset-2"
:title title :title title
(img :src src :class "h-16 w-16 object-contain" :alt alt :loading "lazy" :decoding "async")) (img :src src :class "h-16 w-16 object-contain" :alt alt :loading "lazy" :decoding "async"))
(span :data-image-src src :class "hidden"))) (span :data-image-src src :class "hidden")))
(defcomp ~market-detail-thumbs (&key thumbs) (defcomp ~market-detail-thumbs (&key (thumbs :as list))
(div :class "flex flex-row justify-center" (div :class "flex flex-row justify-center"
(div :class "mt-3 flex gap-2 overflow-x-auto no-scrollbar" thumbs))) (div :class "mt-3 flex gap-2 overflow-x-auto no-scrollbar" thumbs)))
(defcomp ~market-detail-no-image (&key like) (defcomp ~market-detail-no-image (&key (like :as list?))
(div :class "relative aspect-square bg-stone-100 rounded-xl flex items-center justify-center text-stone-400" (div :class "relative aspect-square bg-stone-100 rounded-xl flex items-center justify-center text-stone-400"
like "No image")) like "No image"))
(defcomp ~market-detail-sticker (&key src name) (defcomp ~market-detail-sticker (&key (src :as string) (name :as string))
(img :src src :alt name :class "w-10 h-10")) (img :src src :alt name :class "w-10 h-10"))
(defcomp ~market-detail-stickers (&key items) (defcomp ~market-detail-stickers (&key (items :as list))
(div :class "p-2 flex flex-row justify-center gap-2" items)) (div :class "p-2 flex flex-row justify-center gap-2" items))
(defcomp ~market-detail-unit-price (&key price) (defcomp ~market-detail-unit-price (&key (price :as string))
(div (str "Unit price: " price))) (div (str "Unit price: " price)))
(defcomp ~market-detail-case-size (&key size) (defcomp ~market-detail-case-size (&key (size :as string))
(div (str "Case size: " size))) (div (str "Case size: " size)))
(defcomp ~market-detail-extras (&key inner) (defcomp ~market-detail-extras (&key (inner :as list))
(div :class "mt-2 space-y-1 text-sm text-stone-600" inner)) (div :class "mt-2 space-y-1 text-sm text-stone-600" inner))
(defcomp ~market-detail-desc-short (&key text) (defcomp ~market-detail-desc-short (&key (text :as string))
(p :class "leading-relaxed text-lg" text)) (p :class "leading-relaxed text-lg" text))
(defcomp ~market-detail-desc-html (&key html) (defcomp ~market-detail-desc-html (&key (html :as string))
(div :class "max-w-none text-sm leading-relaxed" (~rich-text :html html))) (div :class "max-w-none text-sm leading-relaxed" (~rich-text :html html)))
(defcomp ~market-detail-desc-wrapper (&key inner) (defcomp ~market-detail-desc-wrapper (&key (inner :as list))
(div :class "mt-4 text-stone-800 space-y-3" inner)) (div :class "mt-4 text-stone-800 space-y-3" inner))
(defcomp ~market-detail-section (&key title html) (defcomp ~market-detail-section (&key (title :as string) (html :as string))
(details :class "group rounded-xl border bg-white shadow-sm open:shadow p-0" (details :class "group rounded-xl border bg-white shadow-sm open:shadow p-0"
(summary :class "cursor-pointer select-none px-4 py-3 flex items-center justify-between" (summary :class "cursor-pointer select-none px-4 py-3 flex items-center justify-between"
(span :class "font-medium" title) (span :class "font-medium" title)
(span :class "ml-2 text-xl transition-transform group-open:rotate-180" "\u2304")) (span :class "ml-2 text-xl transition-transform group-open:rotate-180" "\u2304"))
(div :class "px-4 pb-4 max-w-none text-sm leading-relaxed" (~rich-text :html html)))) (div :class "px-4 pb-4 max-w-none text-sm leading-relaxed" (~rich-text :html html))))
(defcomp ~market-detail-sections (&key items) (defcomp ~market-detail-sections (&key (items :as list))
(div :class "mt-8 space-y-3" items)) (div :class "mt-8 space-y-3" items))
(defcomp ~market-detail-right-col (&key inner) (defcomp ~market-detail-right-col (&key (inner :as list))
(div :class "md:col-span-3" inner)) (div :class "md:col-span-3" inner))
(defcomp ~market-detail-layout (&key gallery stickers details) (defcomp ~market-detail-layout (&key (gallery :as list) (stickers :as list?) (details :as list))
(<> (div :class "mt-3 grid grid-cols-1 md:grid-cols-5 gap-6" :data-gallery-root "" (<> (div :class "mt-3 grid grid-cols-1 md:grid-cols-5 gap-6" :data-gallery-root ""
(div :class "md:col-span-2" gallery stickers) (div :class "md:col-span-2" gallery stickers)
details) details)
(div :class "pb-8"))) (div :class "pb-8")))
(defcomp ~market-landing-excerpt (&key text) (defcomp ~market-landing-excerpt (&key (text :as string))
(div :class "w-full text-center italic text-3xl p-2" text)) (div :class "w-full text-center italic text-3xl p-2" text))
(defcomp ~market-landing-image (&key src) (defcomp ~market-landing-image (&key (src :as string))
(div :class "mb-3 flex justify-center" (div :class "mb-3 flex justify-center"
(img :src src :alt "" :class "rounded-lg w-full md:w-3/4 object-cover"))) (img :src src :alt "" :class "rounded-lg w-full md:w-3/4 object-cover")))
(defcomp ~market-landing-html (&key html) (defcomp ~market-landing-html (&key (html :as string))
(div :class "blog-content p-2" (~rich-text :html html))) (div :class "blog-content p-2" (~rich-text :html html)))
(defcomp ~market-landing-content (&key inner) (defcomp ~market-landing-content (&key (inner :as list))
(<> (article :class "relative w-full" inner) (div :class "pb-8"))) (<> (article :class "relative w-full" inner) (div :class "pb-8")))
@@ -99,7 +99,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Gallery section from pre-computed data ;; Gallery section from pre-computed data
(defcomp ~market-detail-gallery-from-data (&key images labels brand like-data has-nav-buttons thumbs) (defcomp ~market-detail-gallery-from-data (&key (images :as list?) (labels :as list?) (brand :as string) (like-data :as dict?) (has-nav-buttons :as boolean) (thumbs :as list?))
(let ((like-sx (when like-data (let ((like-sx (when like-data
(~market-like-button (~market-like-button
:form-id (get like-data "form-id") :action (get like-data "action") :form-id (get like-data "form-id") :action (get like-data "action")
@@ -124,7 +124,7 @@
(~market-detail-no-image :like like-sx)))) (~market-detail-no-image :like like-sx))))
;; Right column details from data ;; Right column details from data
(defcomp ~market-detail-info-from-data (&key extras desc-short desc-html sections) (defcomp ~market-detail-info-from-data (&key (extras :as list?) (desc-short :as string?) (desc-html :as string?) (sections :as list?))
(~market-detail-right-col :inner (~market-detail-right-col :inner
(<> (<>
(when extras (when extras
@@ -145,9 +145,9 @@
sections))))))) sections)))))))
;; Full product detail layout from data ;; Full product detail layout from data
(defcomp ~market-product-detail-from-data (&key images labels brand like-data (defcomp ~market-product-detail-from-data (&key (images :as list?) (labels :as list?) (brand :as string) (like-data :as dict?)
has-nav-buttons thumbs sticker-items (has-nav-buttons :as boolean) (thumbs :as list?) (sticker-items :as list?)
extras desc-short desc-html sections) (extras :as list?) (desc-short :as string?) (desc-html :as string?) (sections :as list?))
(~market-detail-layout (~market-detail-layout
:gallery (~market-detail-gallery-from-data :gallery (~market-detail-gallery-from-data
:images images :labels labels :brand brand :like-data like-data :images images :labels labels :brand brand :like-data like-data

View File

@@ -1,21 +1,21 @@
;; Market meta/SEO components ;; Market meta/SEO components
(defcomp ~market-meta-title (&key title) (defcomp ~market-meta-title (&key (title :as string))
(title title)) (title title))
(defcomp ~market-meta-description (&key description) (defcomp ~market-meta-description (&key (description :as string))
(meta :name "description" :content description)) (meta :name "description" :content description))
(defcomp ~market-meta-canonical (&key href) (defcomp ~market-meta-canonical (&key (href :as string))
(link :rel "canonical" :href href)) (link :rel "canonical" :href href))
(defcomp ~market-meta-og (&key property content) (defcomp ~market-meta-og (&key (property :as string) (content :as string))
(meta :property property :content content)) (meta :property property :content content))
(defcomp ~market-meta-twitter (&key name content) (defcomp ~market-meta-twitter (&key (name :as string) (content :as string))
(meta :name name :content content)) (meta :name name :content content))
(defcomp ~market-meta-jsonld (&key json) (defcomp ~market-meta-jsonld (&key (json :as string))
(script :type "application/ld+json" (~rich-text :html json))) (script :type "application/ld+json" (~rich-text :html json)))
@@ -23,9 +23,10 @@
;; Composition: all product meta tags from data ;; Composition: all product meta tags from data
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~market-product-meta-from-data (&key title description canonical image-url (defcomp ~market-product-meta-from-data (&key (title :as string) (description :as string) (canonical :as string?)
site-title brand price price-currency (image-url :as string?)
jsonld-json) (site-title :as string) (brand :as string?) (price :as string?) (price-currency :as string?)
(jsonld-json :as string))
(<> (<>
(~market-meta-title :title title) (~market-meta-title :title title)
(~market-meta-description :description description) (~market-meta-description :description description)

View File

@@ -1,6 +1,6 @@
;; Market navigation components ;; Market navigation components
(defcomp ~market-category-link (&key href hx-select active select-colours label) (defcomp ~market-category-link (&key (href :as string) (hx-select :as string) (active :as boolean) (select-colours :as string) (label :as string))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
@@ -8,14 +8,14 @@
:class (str "block px-2 py-1 rounded text-center whitespace-normal break-words leading-snug bg-stone-200 text-black " select-colours) :class (str "block px-2 py-1 rounded text-center whitespace-normal break-words leading-snug bg-stone-200 text-black " select-colours)
label))) label)))
(defcomp ~market-desktop-category-nav (&key links admin) (defcomp ~market-desktop-category-nav (&key (links :as list) (admin :as list?))
(nav :class "hidden md:flex gap-4 text-sm ml-2 w-full justify-end items-center" (nav :class "hidden md:flex gap-4 text-sm ml-2 w-full justify-end items-center"
links admin)) links admin))
(defcomp ~market-mobile-nav-wrapper (&key items) (defcomp ~market-mobile-nav-wrapper (&key (items :as list))
(div :class "px-4 py-2" (div :class "divide-y" items))) (div :class "px-4 py-2" (div :class "divide-y" items)))
(defcomp ~market-mobile-all-link (&key href hx-select active select-colours) (defcomp ~market-mobile-all-link (&key (href :as string) (hx-select :as string) (active :as boolean) (select-colours :as string))
(a :role "option" :href href :sx-get href :sx-target "#main-panel" (a :role "option" :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:aria-selected (if active "true" "false") :aria-selected (if active "true" "false")
@@ -28,7 +28,7 @@
(path :fill-rule "evenodd" :clip-rule "evenodd" (path :fill-rule "evenodd" :clip-rule "evenodd"
:d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"))) :d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z")))
(defcomp ~market-mobile-cat-summary (&key bg-cls href hx-select select-colours cat-name count-label count-str chevron) (defcomp ~market-mobile-cat-summary (&key (bg-cls :as string) (href :as string) (hx-select :as string) (select-colours :as string) (cat-name :as string) (count-label :as string) (count-str :as string) (chevron :as list))
(summary :class (str "flex items-center justify-between cursor-pointer select-none block rounded-lg px-3 py-3 text-base hover:bg-stone-50" bg-cls) (summary :class (str "flex items-center justify-between cursor-pointer select-none block rounded-lg px-3 py-3 text-base hover:bg-stone-50" bg-cls)
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
@@ -37,7 +37,7 @@
(div :aria-label count-label count-str)) (div :aria-label count-label count-str))
chevron)) chevron))
(defcomp ~market-mobile-sub-link (&key select-colours active href hx-select label count-label count-str) (defcomp ~market-mobile-sub-link (&key (select-colours :as string) (active :as boolean) (href :as string) (hx-select :as string) (label :as string) (count-label :as string) (count-str :as string))
(a :class (str "snap-start px-2 py-3 rounded " select-colours " flex flex-row gap-2") (a :class (str "snap-start px-2 py-3 rounded " select-colours " flex flex-row gap-2")
:aria-selected (if active "true" "false") :aria-selected (if active "true" "false")
:href href :sx-get href :sx-target "#main-panel" :href href :sx-get href :sx-target "#main-panel"
@@ -45,20 +45,20 @@
(div label) (div label)
(div :aria-label count-label count-str))) (div :aria-label count-label count-str)))
(defcomp ~market-mobile-subs-panel (&key links) (defcomp ~market-mobile-subs-panel (&key (links :as list))
(div :class "pb-3 pl-2" (div :class "pb-3 pl-2"
(div :data-peek-viewport "" :data-peek-size-px "18" :data-peek-edge "bottom" :data-peek-mask "true" :class "m-2 bg-stone-100" (div :data-peek-viewport "" :data-peek-size-px "18" :data-peek-edge "bottom" :data-peek-mask "true" :class "m-2 bg-stone-100"
(div :data-peek-inner "" :class "grid grid-cols-1 gap-1 snap-y snap-mandatory pr-1" :aria-label "Subcategories" (div :data-peek-inner "" :class "grid grid-cols-1 gap-1 snap-y snap-mandatory pr-1" :aria-label "Subcategories"
links)))) links))))
(defcomp ~market-mobile-view-all (&key href hx-select) (defcomp ~market-mobile-view-all (&key (href :as string) (hx-select :as string))
(div :class "pb-3 pl-2" (div :class "pb-3 pl-2"
(a :class "px-2 py-1 rounded hover:bg-stone-100 block" (a :class "px-2 py-1 rounded hover:bg-stone-100 block"
:href href :sx-get href :sx-target "#main-panel" :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
"View all"))) "View all")))
(defcomp ~market-mobile-cat-details (&key open summary subs) (defcomp ~market-mobile-cat-details (&key (open :as boolean) (summary :as list) (subs :as list))
(details :class "group/cat py-1" :open open (details :class "group/cat py-1" :open open
summary subs)) summary subs))
@@ -67,7 +67,7 @@
;; Composition: mobile nav panel from pre-computed category data ;; 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) (defcomp ~market-mobile-nav-from-data (&key (categories :as list) (all-href :as string) (all-active :as boolean) (hx-select :as string) (select-colours :as string))
(~market-mobile-nav-wrapper :items (~market-mobile-nav-wrapper :items
(<> (<>
(~market-mobile-all-link :href all-href :hx-select hx-select (~market-mobile-all-link :href all-href :hx-select hx-select

View File

@@ -1,36 +1,36 @@
;; Market price display components ;; Market price display components
(defcomp ~market-price-special (&key price) (defcomp ~market-price-special (&key (price :as string))
(div :class "text-lg font-semibold text-emerald-700" price)) (div :class "text-lg font-semibold text-emerald-700" price))
(defcomp ~market-price-regular-strike (&key price) (defcomp ~market-price-regular-strike (&key (price :as string))
(div :class "text-sm line-through text-stone-500" price)) (div :class "text-sm line-through text-stone-500" price))
(defcomp ~market-price-regular (&key price) (defcomp ~market-price-regular (&key (price :as string))
(div :class "mt-1 text-lg font-semibold" price)) (div :class "mt-1 text-lg font-semibold" price))
(defcomp ~market-price-line (&key inner) (defcomp ~market-price-line (&key (inner :as list))
(div :class "mt-1 flex items-baseline gap-2 justify-center" inner)) (div :class "mt-1 flex items-baseline gap-2 justify-center" inner))
(defcomp ~market-header-price-special-label () (defcomp ~market-header-price-special-label ()
(div :class "text-md font-bold text-emerald-700" "Special price")) (div :class "text-md font-bold text-emerald-700" "Special price"))
(defcomp ~market-header-price-special (&key price) (defcomp ~market-header-price-special (&key (price :as string))
(div :class "text-xl font-semibold text-emerald-700" price)) (div :class "text-xl font-semibold text-emerald-700" price))
(defcomp ~market-header-price-strike (&key price) (defcomp ~market-header-price-strike (&key (price :as string))
(div :class "text-base text-md line-through text-stone-500" price)) (div :class "text-base text-md line-through text-stone-500" price))
(defcomp ~market-header-price-regular-label () (defcomp ~market-header-price-regular-label ()
(div :class "hidden md:block text-xl font-bold" "Our price")) (div :class "hidden md:block text-xl font-bold" "Our price"))
(defcomp ~market-header-price-regular (&key price) (defcomp ~market-header-price-regular (&key (price :as string))
(div :class "text-xl font-semibold" price)) (div :class "text-xl font-semibold" price))
(defcomp ~market-header-rrp (&key rrp) (defcomp ~market-header-rrp (&key (rrp :as string))
(div :class "text-base text-stone-400" (span "rrp:") " " (span rrp))) (div :class "text-base text-stone-400" (span "rrp:") " " (span rrp)))
(defcomp ~market-prices-row (&key inner) (defcomp ~market-prices-row (&key (inner :as list))
(div :class "flex flex-row items-center justify-between md:gap-2 md:px-2" inner)) (div :class "flex flex-row items-center justify-between md:gap-2 md:px-2" inner))
@@ -38,8 +38,9 @@
;; Composition: prices header + cart button from data ;; Composition: prices header + cart button from data
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~market-prices-header-from-data (&key cart-id cart-action csrf quantity cart-href (defcomp ~market-prices-header-from-data (&key (cart-id :as string) (cart-action :as string) (csrf :as string) (quantity :as number?)
sp-val sp-str rp-val rp-str rrp-str) (cart-href :as string)
(sp-val :as number?) (sp-str :as string?) (rp-val :as number?) (rp-str :as string?) (rrp-str :as string?))
(~market-prices-row :inner (~market-prices-row :inner
(<> (<>
(if quantity (if quantity
@@ -57,7 +58,7 @@
(when rrp-str (~market-header-rrp :rrp rrp-str))))) (when rrp-str (~market-header-rrp :rrp rrp-str)))))
;; Card price line from data (used in product cards) ;; Card price line from data (used in product cards)
(defcomp ~market-card-price-from-data (&key sp-val sp-str rp-val rp-str) (defcomp ~market-card-price-from-data (&key (sp-val :as number?) (sp-str :as string?) (rp-val :as number?) (rp-str :as string?))
(~market-price-line :inner (~market-price-line :inner
(<> (<>
(when sp-val (when sp-val

View File

@@ -1,6 +1,6 @@
;; Checkout return page components ;; Checkout return page components
(defcomp ~checkout-return-header (&key status) (defcomp ~checkout-return-header (&key (status :as string))
(header :class "mb-1 sm:mb-2 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4" (header :class "mb-1 sm:mb-2 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4"
(div :class "space-y-1" (div :class "space-y-1"
(h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" (h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight"
@@ -21,7 +21,7 @@
(div :class "rounded-2xl border border-dashed border-rose-300 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-800" (div :class "rounded-2xl border border-dashed border-rose-300 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-800"
"We couldn\u2019t find that order. If you reached this page from an old link, please start a new order."))) "We couldn\u2019t find that order. If you reached this page from an old link, please start a new order.")))
(defcomp ~checkout-return-failed (&key order-id) (defcomp ~checkout-return-failed (&key (order-id :as string))
(div :class "rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2" (div :class "rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2"
(p :class "font-medium" "Your payment was not completed.") (p :class "font-medium" "Your payment was not completed.")
(p "You can go back to your cart and try checkout again. If the problem persists, please contact us and mention order " (p "You can go back to your cart and try checkout again. If the problem persists, please contact us and mention order "
@@ -32,7 +32,7 @@
(p :class "font-medium" "All done!") (p :class "font-medium" "All done!")
(p "We\u2019ll start processing your order shortly."))) (p "We\u2019ll start processing your order shortly.")))
(defcomp ~checkout-return-ticket (&key name pill state type-name date-str code price) (defcomp ~checkout-return-ticket (&key (name :as string) (pill :as string) (state :as string) (type-name :as string?) (date-str :as string) (code :as string) (price :as string))
(li :class "px-4 py-3 flex items-start justify-between text-sm" (li :class "px-4 py-3 flex items-start justify-between text-sm"
(div (div
(div :class "font-medium flex items-center gap-2" (div :class "font-medium flex items-center gap-2"
@@ -48,7 +48,7 @@
(ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items))) (ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items)))
;; Data-driven ticket items (replaces Python loop) ;; Data-driven ticket items (replaces Python loop)
(defcomp ~checkout-return-tickets-from-data (&key tickets) (defcomp ~checkout-return-tickets-from-data (&key (tickets :as list))
(~checkout-return-tickets (~checkout-return-tickets
:items (<> (map (lambda (tk) :items (<> (map (lambda (tk)
(~checkout-return-ticket (~checkout-return-ticket

View File

@@ -3,13 +3,13 @@
;; --- orders layout: root + auth + orders rows --- ;; --- orders layout: root + auth + orders rows ---
(defcomp ~orders-layout-full (&key list-url) (defcomp ~orders-layout-full (&key (list-url :as string))
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~header-child-sx
:inner (<> (~auth-header-row-auto) :inner (<> (~auth-header-row-auto)
(~orders-header-row :list-url (or list-url "/")))))) (~orders-header-row :list-url (or list-url "/"))))))
(defcomp ~orders-layout-oob (&key list-url) (defcomp ~orders-layout-oob (&key (list-url :as string))
(<> (~auth-header-row-auto true) (<> (~auth-header-row-auto true)
(~oob-header-sx (~oob-header-sx
:parent-id "auth-header-child" :parent-id "auth-header-child"
@@ -21,7 +21,7 @@
;; --- order-detail layout: root + auth + orders + order rows --- ;; --- order-detail layout: root + auth + orders + order rows ---
(defcomp ~order-detail-layout-full (&key list-url detail-url) (defcomp ~order-detail-layout-full (&key (list-url :as string) (detail-url :as string))
(<> (~root-header-auto) (<> (~root-header-auto)
(~order-detail-header-stack (~order-detail-header-stack
:auth (~auth-header-row-auto) :auth (~auth-header-row-auto)
@@ -30,7 +30,7 @@
:link-href (or detail-url "/") :link-label "Order" :link-href (or detail-url "/") :link-label "Order"
:icon "fa fa-gbp")))) :icon "fa fa-gbp"))))
(defcomp ~order-detail-layout-oob (&key detail-url) (defcomp ~order-detail-layout-oob (&key (detail-url :as string))
(<> (~oob-header-sx (<> (~oob-header-sx
:parent-id "orders-header-child" :parent-id "orders-header-child"
:row (~menu-row-sx :id "order-row" :level 3 :colour "sky" :row (~menu-row-sx :id "order-row" :level 3 :colour "sky"

View File

@@ -14,7 +14,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-11T20:12:35Z"; var SX_VERSION = "2026-03-11T21:00:15Z";
function isNil(x) { return x === NIL || x === null || x === undefined; } function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } function isSxTruthy(x) { return x !== false && !isNil(x); }

View File

@@ -233,7 +233,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-render-island (define-async async-render-island
(fn (island (args :as list) (env :as dict) ctx) (fn ((island :as island) (args :as list) (env :as dict) ctx)
(let ((kwargs (dict)) (let ((kwargs (dict))
(children (list))) (children (list)))
(async-parse-kw-args args kwargs children env ctx) (async-parse-kw-args args kwargs children env ctx)

View File

@@ -19,7 +19,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-to-dom (define render-to-dom
(fn (expr (env :as dict) ns) (fn (expr (env :as dict) (ns :as string))
(set-render-active! true) (set-render-active! true)
(case (type-of expr) (case (type-of expr)
;; nil / boolean false / boolean true → empty fragment ;; nil / boolean false / boolean true → empty fragment
@@ -67,7 +67,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-list (define render-dom-list
(fn (expr (env :as dict) ns) (fn (expr (env :as dict) (ns :as string))
(let ((head (first expr))) (let ((head (first expr)))
(cond (cond
;; Symbol head — dispatch on name ;; Symbol head — dispatch on name
@@ -166,7 +166,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-element (define render-dom-element
(fn ((tag :as string) (args :as list) (env :as dict) ns) (fn ((tag :as string) (args :as list) (env :as dict) (ns :as string))
;; Detect namespace from tag ;; Detect namespace from tag
(let ((new-ns (cond (= tag "svg") SVG_NS (let ((new-ns (cond (= tag "svg") SVG_NS
(= tag "math") MATH_NS (= tag "math") MATH_NS
@@ -237,7 +237,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-component (define render-dom-component
(fn ((comp :as component) (args :as list) (env :as dict) ns) (fn ((comp :as component) (args :as list) (env :as dict) (ns :as string))
;; Parse kwargs and children, bind into component env, render body. ;; Parse kwargs and children, bind into component env, render body.
(let ((kwargs (dict)) (let ((kwargs (dict))
(children (list))) (children (list)))
@@ -284,7 +284,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-fragment (define render-dom-fragment
(fn ((args :as list) (env :as dict) ns) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((frag (create-fragment))) (let ((frag (create-fragment)))
(for-each (for-each
(fn (x) (dom-append frag (render-to-dom x env ns))) (fn (x) (dom-append frag (render-to-dom x env ns)))
@@ -339,7 +339,7 @@
(contains? RENDER_DOM_FORMS name))) (contains? RENDER_DOM_FORMS name)))
(define dispatch-render-form (define dispatch-render-form
(fn ((name :as string) expr (env :as dict) ns) (fn ((name :as string) expr (env :as dict) (ns :as string))
(cond (cond
;; if — reactive inside islands (re-renders when signal deps change) ;; if — reactive inside islands (re-renders when signal deps change)
(= name "if") (= name "if")
@@ -581,7 +581,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-lambda-dom (define render-lambda-dom
(fn ((f :as lambda) (args :as list) (env :as dict) ns) (fn ((f :as lambda) (args :as list) (env :as dict) (ns :as string))
;; Bind lambda params and render body as DOM ;; Bind lambda params and render body as DOM
(let ((local (env-merge (lambda-closure f) env))) (let ((local (env-merge (lambda-closure f) env)))
(for-each-indexed (for-each-indexed
@@ -605,7 +605,7 @@
;; - Conditional fragments: (when (deref sig) ...) → reactive show/hide ;; - Conditional fragments: (when (deref sig) ...) → reactive show/hide
(define render-dom-island (define render-dom-island
(fn (island (args :as list) (env :as dict) ns) (fn ((island :as island) (args :as list) (env :as dict) (ns :as string))
;; Parse kwargs and children (same as component) ;; Parse kwargs and children (same as component)
(let ((kwargs (dict)) (let ((kwargs (dict))
(children (list))) (children (list)))
@@ -679,7 +679,7 @@
;; Supports :tag keyword to change wrapper element (default "div"). ;; Supports :tag keyword to change wrapper element (default "div").
(define render-dom-lake (define render-dom-lake
(fn ((args :as list) (env :as dict) ns) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((lake-id nil) (let ((lake-id nil)
(lake-tag "div") (lake-tag "div")
(children (list))) (children (list)))
@@ -723,7 +723,7 @@
;; Stores the island env and transform on the element for morph retrieval. ;; Stores the island env and transform on the element for morph retrieval.
(define render-dom-marsh (define render-dom-marsh
(fn ((args :as list) (env :as dict) ns) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((marsh-id nil) (let ((marsh-id nil)
(marsh-tag "div") (marsh-tag "div")
(marsh-transform nil) (marsh-transform nil)
@@ -781,7 +781,7 @@
;; Marks the attribute name on the element via data-sx-reactive-attrs so ;; Marks the attribute name on the element via data-sx-reactive-attrs so
;; the morph algorithm knows not to overwrite it with server content. ;; the morph algorithm knows not to overwrite it with server content.
(define reactive-attr (define reactive-attr
(fn (el (attr-name :as string) compute-fn) (fn (el (attr-name :as string) (compute-fn :as lambda))
;; Mark this attribute as reactively managed ;; Mark this attribute as reactively managed
(let ((existing (or (dom-get-attr el "data-sx-reactive-attrs") "")) (let ((existing (or (dom-get-attr el "data-sx-reactive-attrs") ""))
(updated (if (empty? existing) attr-name (str existing "," attr-name)))) (updated (if (empty? existing) attr-name (str existing "," attr-name))))
@@ -802,7 +802,7 @@
;; reactive-fragment — conditionally render a fragment based on a signal ;; reactive-fragment — conditionally render a fragment based on a signal
;; Used for (when (deref sig) ...) or (if (deref sig) ...) inside an island. ;; Used for (when (deref sig) ...) or (if (deref sig) ...) inside an island.
(define reactive-fragment (define reactive-fragment
(fn (test-fn render-fn (env :as dict) ns) (fn ((test-fn :as lambda) (render-fn :as lambda) (env :as dict) (ns :as string))
(let ((marker (create-comment "island-fragment")) (let ((marker (create-comment "island-fragment"))
(current-nodes (list))) (current-nodes (list)))
(effect (fn () (effect (fn ()
@@ -824,7 +824,7 @@
;; and reorderings touch the DOM. Without keys, falls back to clear+rerender. ;; and reorderings touch the DOM. Without keys, falls back to clear+rerender.
(define render-list-item (define render-list-item
(fn (map-fn item (env :as dict) ns) (fn ((map-fn :as lambda) item (env :as dict) (ns :as string))
(if (lambda? map-fn) (if (lambda? map-fn)
(render-lambda-dom map-fn (list item) env ns) (render-lambda-dom map-fn (list item) env ns)
(render-to-dom (apply map-fn (list item)) env ns)))) (render-to-dom (apply map-fn (list item)) env ns))))
@@ -839,7 +839,7 @@
(if dk (str dk) (str "__idx_" index))))))) (if dk (str dk) (str "__idx_" index)))))))
(define reactive-list (define reactive-list
(fn (map-fn items-sig (env :as dict) ns) (fn ((map-fn :as lambda) (items-sig :as signal) (env :as dict) (ns :as string))
(let ((container (create-fragment)) (let ((container (create-fragment))
(marker (create-comment "island-list")) (marker (create-comment "island-list"))
(key-map (dict)) (key-map (dict))
@@ -925,7 +925,7 @@
;; Handles: input[text/number/email/...], textarea, select, checkbox, radio ;; Handles: input[text/number/email/...], textarea, select, checkbox, radio
(define bind-input (define bind-input
(fn (el sig) (fn (el (sig :as signal))
(let ((input-type (lower (or (dom-get-attr el "type") ""))) (let ((input-type (lower (or (dom-get-attr el "type") "")))
(is-checkbox (or (= input-type "checkbox") (is-checkbox (or (= input-type "checkbox")
(= input-type "radio")))) (= input-type "radio"))))
@@ -960,7 +960,7 @@
;; teardown. ;; teardown.
(define render-dom-portal (define render-dom-portal
(fn ((args :as list) (env :as dict) ns) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((selector (trampoline (eval-expr (first args) env))) (let ((selector (trampoline (eval-expr (first args) env)))
(target (or (dom-query selector) (target (or (dom-query selector)
(dom-ensure-element selector)))) (dom-ensure-element selector))))
@@ -1000,7 +1000,7 @@
;; Calling (retry) re-renders the body, replacing the fallback. ;; Calling (retry) re-renders the body, replacing the fallback.
(define render-dom-error-boundary (define render-dom-error-boundary
(fn ((args :as list) (env :as dict) ns) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((fallback-expr (first args)) (let ((fallback-expr (first args))
(body-exprs (rest args)) (body-exprs (rest args))
(container (dom-create-element "div" nil)) (container (dom-create-element "div" nil))

View File

@@ -151,7 +151,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define process-response-headers (define process-response-headers
(fn (get-header) (fn ((get-header :as lambda))
;; Extract all SX response header directives into a dict. ;; Extract all SX response header directives into a dict.
;; get-header is (fn (name) → string or nil). ;; get-header is (fn (name) → string or nil).
(dict (dict

View File

@@ -235,7 +235,7 @@
(define call-component (define call-component
(fn (comp (raw-args :as list) (env :as dict)) (fn ((comp :as component) (raw-args :as list) (env :as dict))
;; Parse keyword args and children from unevaluated arg list ;; Parse keyword args and children from unevaluated arg list
(let ((parsed (parse-keyword-args raw-args env)) (let ((parsed (parse-keyword-args raw-args env))
(kwargs (first parsed)) (kwargs (first parsed))

View File

@@ -57,7 +57,7 @@
(assert (nil? val) (str "Expected nil but got " (str val))))) (assert (nil? val) (str "Expected nil but got " (str val)))))
(define assert-type (define assert-type
(fn (expected-type val) (fn ((expected-type :as string) val)
(let ((actual-type (let ((actual-type
(if (nil? val) "nil" (if (nil? val) "nil"
(if (boolean? val) "boolean" (if (boolean? val) "boolean"
@@ -70,17 +70,17 @@
(str "Expected type " expected-type " but got " actual-type))))) (str "Expected type " expected-type " but got " actual-type)))))
(define assert-length (define assert-length
(fn (expected-len col) (fn ((expected-len :as number) (col :as list))
(assert (= (len col) expected-len) (assert (= (len col) expected-len)
(str "Expected length " expected-len " but got " (len col))))) (str "Expected length " expected-len " but got " (len col)))))
(define assert-contains (define assert-contains
(fn (item col) (fn (item (col :as list))
(assert (some (fn (x) (equal? x item)) col) (assert (some (fn (x) (equal? x item)) col)
(str "Expected collection to contain " (str item))))) (str "Expected collection to contain " (str item)))))
(define assert-throws (define assert-throws
(fn (thunk) (fn ((thunk :as lambda))
(let ((result (try-call thunk))) (let ((result (try-call thunk)))
(assert (not (get result "ok")) (assert (not (get result "ok"))
"Expected an error to be thrown but none was")))) "Expected an error to be thrown but none was"))))

View File

@@ -25,7 +25,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-sort (define z3-sort
(fn (sx-type) (fn ((sx-type :as string))
(case sx-type (case sx-type
"number" "Int" "number" "Int"
"boolean" "Bool" "boolean" "Bool"
@@ -40,7 +40,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-name (define z3-name
(fn (name) (fn ((name :as string))
(cond (cond
(= name "!=") "neq" (= name "!=") "neq"
(= name "+") "+" (= name "+") "+"
@@ -74,7 +74,7 @@
;; Operators that get renamed ;; Operators that get renamed
(define z3-rename-op (define z3-rename-op
(fn (op) (fn ((op :as string))
(case op (case op
"if" "ite" "if" "ite"
"str" "str.++" "str" "str.++"
@@ -176,7 +176,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-extract-kwargs (define z3-extract-kwargs
(fn (expr) (fn ((expr :as list))
;; Returns a dict of keyword args from a define-* form ;; Returns a dict of keyword args from a define-* form
;; (define-primitive "name" :params (...) :returns "type" ...) → {:params ... :returns ...} ;; (define-primitive "name" :params (...) :returns "type" ...) → {:params ... :returns ...}
(let ((result {}) (let ((result {})
@@ -184,7 +184,7 @@
(z3-extract-kwargs-loop items result)))) (z3-extract-kwargs-loop items result))))
(define z3-extract-kwargs-loop (define z3-extract-kwargs-loop
(fn (items result) (fn ((items :as list) (result :as dict))
(if (or (empty? items) (< (len items) 2)) (if (or (empty? items) (< (len items) 2))
result result
(if (= (type-of (first items)) "keyword") (if (= (type-of (first items)) "keyword")
@@ -199,12 +199,12 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-params-to-sorts (define z3-params-to-sorts
(fn (params) (fn ((params :as list))
;; Convert SX param list to list of (name sort) pairs, skipping &rest/&key ;; Convert SX param list to list of (name sort) pairs, skipping &rest/&key
(z3-params-loop params false (list)))) (z3-params-loop params false (list))))
(define z3-params-loop (define z3-params-loop
(fn (params skip-next acc) (fn ((params :as list) (skip-next :as boolean) (acc :as list))
(if (empty? params) (if (empty? params)
acc acc
(let ((p (first params)) (let ((p (first params))
@@ -227,7 +227,7 @@
(z3-params-loop rest-p false acc)))))) (z3-params-loop rest-p false acc))))))
(define z3-has-rest? (define z3-has-rest?
(fn (params) (fn ((params :as list))
(some (fn (p) (and (= (type-of p) "symbol") (= (symbol-name p) "&rest"))) (some (fn (p) (and (= (type-of p) "symbol") (= (symbol-name p) "&rest")))
params))) params)))
@@ -237,7 +237,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-translate-primitive (define z3-translate-primitive
(fn (expr) (fn ((expr :as list))
(let ((name (nth expr 1)) (let ((name (nth expr 1))
(kwargs (z3-extract-kwargs expr)) (kwargs (z3-extract-kwargs expr))
(params (or (get kwargs "params") (list))) (params (or (get kwargs "params") (list)))
@@ -282,7 +282,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-translate-io (define z3-translate-io
(fn (expr) (fn ((expr :as list))
(let ((name (nth expr 1)) (let ((name (nth expr 1))
(kwargs (z3-extract-kwargs expr)) (kwargs (z3-extract-kwargs expr))
(doc (or (get kwargs "doc") "")) (doc (or (get kwargs "doc") ""))
@@ -297,7 +297,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-translate-special-form (define z3-translate-special-form
(fn (expr) (fn ((expr :as list))
(let ((name (nth expr 1)) (let ((name (nth expr 1))
(kwargs (z3-extract-kwargs expr)) (kwargs (z3-extract-kwargs expr))
(doc (or (get kwargs "doc") ""))) (doc (or (get kwargs "doc") "")))
@@ -342,7 +342,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-translate-file (define z3-translate-file
(fn (exprs) (fn ((exprs :as list))
;; Filter to translatable forms and translate each ;; Filter to translatable forms and translate each
(let ((translatable (let ((translatable
(filter (filter

View File

@@ -6,7 +6,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Auth section nav items (newsletters link + account_nav slot) ;; Auth section nav items (newsletters link + account_nav slot)
(defcomp ~auth-nav-items (&key account-url select-colours account-nav) (defcomp ~auth-nav-items (&key (account-url :as string?) (select-colours :as string?) account-nav)
(<> (<>
(~nav-link :href (str (or account-url "") "/newsletters/") (~nav-link :href (str (or account-url "") "/newsletters/")
:label "newsletters" :label "newsletters"
@@ -14,7 +14,7 @@
(when account-nav account-nav))) (when account-nav account-nav)))
;; Auth header row — wraps ~menu-row-sx for account section ;; Auth header row — wraps ~menu-row-sx for account section
(defcomp ~auth-header-row (&key account-url select-colours account-nav oob) (defcomp ~auth-header-row (&key (account-url :as string?) (select-colours :as string?) account-nav (oob :as boolean?))
(~menu-row-sx :id "auth-row" :level 1 :colour "sky" (~menu-row-sx :id "auth-row" :level 1 :colour "sky"
:link-href (str (or account-url "") "/") :link-href (str (or account-url "") "/")
:link-label "account" :icon "fa-solid fa-user" :link-label "account" :icon "fa-solid fa-user"
@@ -24,7 +24,7 @@
:child-id "auth-header-child" :oob oob)) :child-id "auth-header-child" :oob oob))
;; Auth header row without nav (for cart service) ;; Auth header row without nav (for cart service)
(defcomp ~auth-header-row-simple (&key account-url oob) (defcomp ~auth-header-row-simple (&key (account-url :as string?) (oob :as boolean?))
(~menu-row-sx :id "auth-row" :level 1 :colour "sky" (~menu-row-sx :id "auth-row" :level 1 :colour "sky"
:link-href (str (or account-url "") "/") :link-href (str (or account-url "") "/")
:link-label "account" :icon "fa-solid fa-user" :link-label "account" :icon "fa-solid fa-user"
@@ -52,7 +52,7 @@
:account-nav (account-nav-ctx)))) :account-nav (account-nav-ctx))))
;; Orders header row ;; Orders header row
(defcomp ~orders-header-row (&key list-url) (defcomp ~orders-header-row (&key (list-url :as string))
(~menu-row-sx :id "orders-row" :level 2 :colour "sky" (~menu-row-sx :id "orders-row" :level 2 :colour "sky"
:link-href list-url :link-label "Orders" :icon "fa fa-gbp" :link-href list-url :link-label "Orders" :icon "fa fa-gbp"
:child-id "orders-header-child")) :child-id "orders-header-child"))
@@ -61,12 +61,12 @@
;; Auth forms — login flow, check email ;; Auth forms — login flow, check email
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~auth-error-banner (&key error) (defcomp ~auth-error-banner (&key (error :as string?))
(when error (when error
(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4" (div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4"
error))) error)))
(defcomp ~auth-login-form (&key error action csrf-token email) (defcomp ~auth-login-form (&key error (action :as string) (csrf-token :as string) (email :as string?))
(div :class "py-8 max-w-md mx-auto" (div :class "py-8 max-w-md mx-auto"
(h1 :class "text-2xl font-bold mb-6" "Sign in") (h1 :class "text-2xl font-bold mb-6" "Sign in")
error error
@@ -80,12 +80,12 @@
:class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition" :class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition"
"Send magic link")))) "Send magic link"))))
(defcomp ~auth-check-email-error (&key error) (defcomp ~auth-check-email-error (&key (error :as string?))
(when error (when error
(div :class "bg-yellow-50 border border-yellow-200 text-yellow-700 p-3 rounded mt-4" (div :class "bg-yellow-50 border border-yellow-200 text-yellow-700 p-3 rounded mt-4"
error))) error)))
(defcomp ~auth-check-email (&key email error) (defcomp ~auth-check-email (&key (email :as string) error)
(div :class "py-8 max-w-md mx-auto text-center" (div :class "py-8 max-w-md mx-auto text-center"
(h1 :class "text-2xl font-bold mb-4" "Check your email") (h1 :class "text-2xl font-bold mb-4" "Check your email")
(p :class "text-stone-600 mb-2" "We sent a sign-in link to " (strong email) ".") (p :class "text-stone-600 mb-2" "We sent a sign-in link to " (strong email) ".")

View File

@@ -1,6 +1,6 @@
(defcomp ~post-card (&key title slug href feature-image excerpt (defcomp ~post-card (&key (title :as string) (slug :as string) (href :as string) (feature-image :as string?)
status published-at updated-at publish-requested (excerpt :as string?) (status :as string?) (published-at :as string?) (updated-at :as string?)
hx-select like widgets at-bar) (publish-requested :as boolean?) (hx-select :as string?) like widgets at-bar)
(article :class "border-b pb-6 last:border-b-0 relative" (article :class "border-b pb-6 last:border-b-0 relative"
(when like like) (when like like)
(a :href href (a :href href
@@ -31,7 +31,8 @@
(when widgets widgets) (when widgets widgets)
(when at-bar at-bar))) (when at-bar at-bar)))
(defcomp ~order-summary-card (&key order-id created-at description status currency total-amount) (defcomp ~order-summary-card (&key (order-id :as string) (created-at :as string?) (description :as string?)
(status :as string?) (currency :as string?) (total-amount :as string?))
(div :class "rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6 space-y-2 text-xs sm:text-sm text-stone-800" (div :class "rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6 space-y-2 text-xs sm:text-sm text-stone-800"
(p (span :class "font-medium" "Order ID:") " " (span :class "font-mono" (str "#" order-id))) (p (span :class "font-medium" "Order ID:") " " (span :class "font-mono" (str "#" order-id)))
(p (span :class "font-medium" "Created:") " " (or created-at "\u2014")) (p (span :class "font-medium" "Created:") " " (or created-at "\u2014"))

View File

@@ -1,4 +1,5 @@
(defcomp ~search-mobile (&key current-local-href search search-count hx-select search-headers-mobile) (defcomp ~search-mobile (&key (current-local-href :as string) (search :as string?) (search-count :as number?)
(hx-select :as string?) (search-headers-mobile :as string?))
(div :id "search-mobile-wrapper" (div :id "search-mobile-wrapper"
:class "flex flex-row gap-2 items-center flex-1 min-w-0 pr-2" :class "flex flex-row gap-2 items-center flex-1 min-w-0 pr-2"
(input :id "search-mobile" (input :id "search-mobile"
@@ -20,7 +21,8 @@
:class (if (not search-count) "text-xl text-red-500" "") :class (if (not search-count) "text-xl text-red-500" "")
(when search (str search-count))))) (when search (str search-count)))))
(defcomp ~search-desktop (&key current-local-href search search-count hx-select search-headers-desktop) (defcomp ~search-desktop (&key (current-local-href :as string) (search :as string?) (search-count :as number?)
(hx-select :as string?) (search-headers-desktop :as string?))
(div :id "search-desktop-wrapper" (div :id "search-desktop-wrapper"
:class "flex flex-row gap-2 items-center" :class "flex flex-row gap-2 items-center"
(input :id "search-desktop" (input :id "search-desktop"
@@ -62,7 +64,8 @@
(div :id "filter-details-mobile" :style "display:contents" (div :id "filter-details-mobile" :style "display:contents"
(when filter-details filter-details)))) (when filter-details filter-details))))
(defcomp ~infinite-scroll (&key url page total-pages id-prefix colspan) (defcomp ~infinite-scroll (&key (url :as string) (page :as number) (total-pages :as number)
(id-prefix :as string) (colspan :as number))
(if (< page total-pages) (if (< page total-pages)
(tr :id (str id-prefix "-sentinel-" page) (tr :id (str id-prefix "-sentinel-" page)
:sx-get url :sx-get url
@@ -82,7 +85,7 @@
(tr (td :colspan colspan :class "px-3 py-4 text-center text-xs text-stone-400" (tr (td :colspan colspan :class "px-3 py-4 text-center text-xs text-stone-400"
"End of results")))) "End of results"))))
(defcomp ~status-pill (&key status size) (defcomp ~status-pill (&key (status :as string?) (size :as string?))
(let* ((s (or status "pending")) (let* ((s (or status "pending"))
(lower (lower s)) (lower (lower s))
(sz (or size "xs")) (sz (or size "xs"))

View File

@@ -1,4 +1,5 @@
(defcomp ~link-card (&key link title image icon subtitle detail data-app) (defcomp ~link-card (&key (link :as string) (title :as string) (image :as string?) (icon :as string?)
(subtitle :as string?) (detail :as string?) (data-app :as string?))
(a :href link (a :href link
:class "block rounded border border-stone-200 bg-white hover:bg-stone-50 transition-colors no-underline" :class "block rounded border border-stone-200 bg-white hover:bg-stone-50 transition-colors no-underline"
:data-fragment "link-card" :data-fragment "link-card"
@@ -16,7 +17,7 @@
(when detail (when detail
(div :class "text-xs text-stone-400 mt-1" detail)))))) (div :class "text-xs text-stone-400 mt-1" detail))))))
(defcomp ~cart-mini (&key cart-count blog-url cart-url oob) (defcomp ~cart-mini (&key (cart-count :as number) (blog-url :as string) (cart-url :as string) (oob :as string?))
(div :id "cart-mini" (div :id "cart-mini"
:sx-swap-oob oob :sx-swap-oob oob
(if (= cart-count 0) (if (= cart-count 0)
@@ -33,7 +34,7 @@
(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" (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))))) cart-count)))))
(defcomp ~auth-menu (&key user-email account-url) (defcomp ~auth-menu (&key (user-email :as string?) (account-url :as string))
(<> (<>
(span :id "auth-menu-desktop" :class "hidden md:inline-flex" (span :id "auth-menu-desktop" :class "hidden md:inline-flex"
(if user-email (if user-email
@@ -65,7 +66,7 @@
(i :class "fa-solid fa-key") (i :class "fa-solid fa-key")
(span "sign in or register")))))) (span "sign in or register"))))))
(defcomp ~account-nav-item (&key href label) (defcomp ~account-nav-item (&key (href :as string) (label :as string))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href (a :href href
:class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3" :class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3"

View File

@@ -48,19 +48,19 @@
:class "w-12 h-12 rotate-180 transition-transform group-open/root:block hidden self-start" :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")))) (path :d "M6 9l6 6 6-6" :fill "currentColor"))))
(defcomp ~post-label (&key feature-image title) (defcomp ~post-label (&key (feature-image :as string?) (title :as string))
(<> (when feature-image (<> (when feature-image
(img :src feature-image :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0")) (img :src feature-image :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"))
(span title))) (span title)))
(defcomp ~page-cart-badge (&key href count) (defcomp ~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" (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") (i :class "fa fa-shopping-cart" :aria-hidden "true")
(span count))) (span count)))
(defcomp ~header-row-sx (&key cart-mini blog-url site-title app-label (defcomp ~header-row-sx (&key cart-mini (blog-url :as string?) (site-title :as string?)
nav-tree auth-menu nav-panel (app-label :as string?) nav-tree auth-menu nav-panel
settings-url is-admin oob) (settings-url :as string?) (is-admin :as boolean?) (oob :as boolean?))
(<> (<>
(div :id "root-row" (div :id "root-row"
:sx-swap-oob (if oob "outerHTML" nil) :sx-swap-oob (if oob "outerHTML" nil)
@@ -85,8 +85,10 @@
; @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 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 ; @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 (defcomp ~menu-row-sx (&key (id :as string) (level :as number?) (colour :as string?)
selected hx-select nav child-id child oob external) (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")) (let* ((c (or colour "sky"))
(lv (or level 1)) (lv (or level 1))
(shade (str (- 500 (* lv 100))))) (shade (str (- 500 (* lv 100)))))
@@ -115,11 +117,11 @@
(div :id child-id :class "flex flex-col w-full items-center" (div :id child-id :class "flex flex-col w-full items-center"
(when child child)))))) (when child child))))))
(defcomp ~oob-header-sx (&key parent-id row) (defcomp ~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" (div :id parent-id :sx-swap-oob "outerHTML" :class "flex flex-col w-full items-center"
row)) row))
(defcomp ~header-child-sx (&key id inner) (defcomp ~header-child-sx (&key (id :as string?) inner)
(div :id (or id "root-header-child") :class "flex flex-col w-full items-center" inner)) (div :id (or id "root-header-child") :class "flex flex-col w-full items-center" inner))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
@@ -127,7 +129,8 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Labelled section: colour bar header + vertical nav items ;; Labelled section: colour bar header + vertical nav items
(defcomp ~mobile-menu-section (&key label href colour level items) (defcomp ~mobile-menu-section (&key (label :as string) (href :as string?) (colour :as string?)
(level :as number?) items)
(let* ((c (or colour "sky")) (let* ((c (or colour "sky"))
(lv (or level 1)) (lv (or level 1))
(shade (str (- 500 (* lv 100))))) (shade (str (- 500 (* lv 100)))))
@@ -153,8 +156,9 @@
;; nested component calls in _aser are serialized without expansion. ;; nested component calls in _aser are serialized without expansion.
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~root-header (&key cart-mini blog-url site-title app-label (defcomp ~root-header (&key cart-mini (blog-url :as string?) (site-title :as string?)
nav-tree auth-menu nav-panel settings-url is-admin oob) (app-label :as string?) nav-tree auth-menu nav-panel
(settings-url :as string?) (is-admin :as boolean?) (oob :as boolean?))
(~header-row-sx :cart-mini cart-mini :blog-url blog-url :site-title site-title (~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 :app-label app-label :nav-tree nav-tree :auth-menu auth-menu
:nav-panel nav-panel :settings-url settings-url :is-admin is-admin :nav-panel nav-panel :settings-url settings-url :is-admin is-admin
@@ -226,18 +230,18 @@
(~root-mobile-auto)))) (~root-mobile-auto))))
;; Post-admin layout — root + post header with nested admin row ;; Post-admin layout — root + post header with nested admin row
(defcomp ~layout-post-admin-full (&key selected) (defcomp ~layout-post-admin-full (&key (selected :as string?))
(let ((__admin-hdr (~post-admin-header-auto nil selected))) (let ((__admin-hdr (~post-admin-header-auto nil selected)))
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~header-child-sx
:inner (~post-header-auto nil))))) :inner (~post-header-auto nil)))))
(defcomp ~layout-post-admin-oob (&key selected) (defcomp ~layout-post-admin-oob (&key (selected :as string?))
(<> (~post-header-auto true) (<> (~post-header-auto true)
(~oob-header-sx :parent-id "post-header-child" (~oob-header-sx :parent-id "post-header-child"
:row (~post-admin-header-auto nil selected)))) :row (~post-admin-header-auto nil selected))))
(defcomp ~layout-post-admin-mobile (&key selected) (defcomp ~layout-post-admin-mobile (&key (selected :as string?))
(let ((__phctx (post-header-ctx))) (let ((__phctx (post-header-ctx)))
(<> (<>
(when (get __phctx "slug") (when (get __phctx "slug")
@@ -254,7 +258,7 @@
:items (~post-nav-auto))) :items (~post-nav-auto)))
(~root-mobile-auto)))) (~root-mobile-auto))))
(defcomp ~error-content (&key errnum message image) (defcomp ~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 "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 "font-bold text-2xl md:text-4xl text-red-500 mb-4" errnum)
(div :class "text-stone-600 mb-4" message) (div :class "text-stone-600 mb-4" message)
@@ -262,7 +266,7 @@
(div :class "flex justify-center" (div :class "flex justify-center"
(img :src image :width "300" :height "300"))))) (img :src image :width "300" :height "300")))))
(defcomp ~clear-oob-div (&key id) (defcomp ~clear-oob-div (&key (id :as string))
(div :id id :sx-swap-oob "outerHTML")) (div :id id :sx-swap-oob "outerHTML"))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
@@ -354,21 +358,22 @@
content)) 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 ; @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 ~admin-cog-button (&key href is-admin-page) (defcomp ~admin-cog-button (&key (href :as string) (is-admin-page :as boolean?))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href (a :href href
:class (str "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 " :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" "")) (if is-admin-page "!bg-stone-500 !text-white" ""))
(i :class "fa fa-cog" :aria-hidden "true")))) (i :class "fa fa-cog" :aria-hidden "true"))))
(defcomp ~post-admin-label (&key selected) (defcomp ~post-admin-label (&key (selected :as string?))
(<> (<>
(i :class "fa fa-shield-halved" :aria-hidden "true") (i :class "fa fa-shield-halved" :aria-hidden "true")
" admin" " admin"
(when selected (when selected
(span :class "text-white" selected)))) (span :class "text-white" selected))))
(defcomp ~nav-link (&key href hx-select label icon aclass select-colours is-selected) (defcomp ~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" (div :class "relative nav-group"
(a :href href (a :href href
:sx-get href :sx-get href

View File

@@ -2,32 +2,33 @@
;; The single place where raw! lives — for CMS content (Ghost post body, ;; The single place where raw! lives — for CMS content (Ghost post body,
;; product descriptions, etc.) that arrives as pre-rendered HTML. ;; product descriptions, etc.) that arrives as pre-rendered HTML.
(defcomp ~rich-text (&key html) (defcomp ~rich-text (&key (html :as string))
(raw! html)) (raw! html))
(defcomp ~error-inline (&key message) (defcomp ~error-inline (&key (message :as string))
(div :class "text-red-600 text-sm" message)) (div :class "text-red-600 text-sm" message))
(defcomp ~notification-badge (&key count) (defcomp ~notification-badge (&key (count :as number))
(span :class "bg-red-500 text-white text-xs rounded-full px-1.5 py-0.5" count)) (span :class "bg-red-500 text-white text-xs rounded-full px-1.5 py-0.5" count))
(defcomp ~cache-cleared (&key time-str) (defcomp ~cache-cleared (&key (time-str :as string))
(span :class "text-green-600 font-bold" "Cache cleared at " time-str)) (span :class "text-green-600 font-bold" "Cache cleared at " time-str))
(defcomp ~error-list (&key items) (defcomp ~error-list (&key (items :as list?))
(ul :class "list-disc pl-5 space-y-1 text-sm text-red-600" (ul :class "list-disc pl-5 space-y-1 text-sm text-red-600"
(when items items))) (when items items)))
(defcomp ~error-list-item (&key message) (defcomp ~error-list-item (&key (message :as string))
(li message)) (li message))
(defcomp ~fragment-error (&key service) (defcomp ~fragment-error (&key (service :as string))
(p :class "text-sm text-red-600" "Service " (b service) " is unavailable.")) (p :class "text-sm text-red-600" "Service " (b service) " is unavailable."))
(defcomp ~htmx-sentinel (&key id hx-get hx-trigger hx-swap class extra-attrs) (defcomp ~htmx-sentinel (&key (id :as string) (hx-get :as string) (hx-trigger :as string)
(hx-swap :as string) (class :as string?) extra-attrs)
(div :id id :sx-get hx-get :sx-trigger hx-trigger :sx-swap hx-swap :class class)) (div :id id :sx-get hx-get :sx-trigger hx-trigger :sx-swap hx-swap :class class))
(defcomp ~nav-group-link (&key href hx-select nav-class label) (defcomp ~nav-group-link (&key (href :as string) (hx-select :as string?) (nav-class :as string?) (label :as string))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-select hx-select :sx-swap "outerHTML"
@@ -38,7 +39,7 @@
;; Shared sentinel components — infinite scroll triggers ;; Shared sentinel components — infinite scroll triggers
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~sentinel-mobile (&key id next-url hyperscript) (defcomp ~sentinel-mobile (&key (id :as string) (next-url :as string) (hyperscript :as string?))
(div :id id :class "block md:hidden h-[60vh] opacity-0 pointer-events-none js-mobile-sentinel" (div :id id :class "block md:hidden h-[60vh] opacity-0 pointer-events-none js-mobile-sentinel"
:sx-get next-url :sx-trigger "intersect once delay:250ms, sentinelmobile:retry" :sx-get next-url :sx-trigger "intersect once delay:250ms, sentinelmobile:retry"
:sx-swap "outerHTML" :_ hyperscript :sx-swap "outerHTML" :_ hyperscript
@@ -49,7 +50,7 @@
(i :class "fa fa-exclamation-triangle text-2xl") (i :class "fa fa-exclamation-triangle text-2xl")
(p :class "mt-2" "Loading failed \u2014 retrying\u2026")))) (p :class "mt-2" "Loading failed \u2014 retrying\u2026"))))
(defcomp ~sentinel-desktop (&key id next-url hyperscript) (defcomp ~sentinel-desktop (&key (id :as string) (next-url :as string) (hyperscript :as string?))
(div :id id :class "hidden md:block h-4 opacity-0 pointer-events-none" (div :id id :class "hidden md:block h-4 opacity-0 pointer-events-none"
:sx-get next-url :sx-trigger "intersect once delay:250ms, sentinel:retry" :sx-get next-url :sx-trigger "intersect once delay:250ms, sentinel:retry"
:sx-swap "outerHTML" :_ hyperscript :sx-swap "outerHTML" :_ hyperscript
@@ -58,20 +59,20 @@
(div :class "animate-spin h-6 w-6 border-2 border-stone-300 border-t-stone-600 rounded-full")) (div :class "animate-spin h-6 w-6 border-2 border-stone-300 border-t-stone-600 rounded-full"))
(div :class "js-neterr hidden text-center py-2 text-stone-400 text-sm" "Retry\u2026"))) (div :class "js-neterr hidden text-center py-2 text-stone-400 text-sm" "Retry\u2026")))
(defcomp ~sentinel-simple (&key id next-url) (defcomp ~sentinel-simple (&key (id :as string) (next-url :as string))
(div :id id :class "h-4 opacity-0 pointer-events-none" (div :id id :class "h-4 opacity-0 pointer-events-none"
:sx-get next-url :sx-trigger "intersect once delay:250ms" :sx-swap "outerHTML" :sx-get next-url :sx-trigger "intersect once delay:250ms" :sx-swap "outerHTML"
:role "status" :aria-hidden "true" :role "status" :aria-hidden "true"
(div :class "text-center text-xs text-stone-400" "loading..."))) (div :class "text-center text-xs text-stone-400" "loading...")))
(defcomp ~end-of-results (&key cls) (defcomp ~end-of-results (&key (cls :as string?))
(div :class (or cls "col-span-full mt-4 text-center text-xs text-stone-400") "End of results")) (div :class (or cls "col-span-full mt-4 text-center text-xs text-stone-400") "End of results"))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Shared empty state — icon + message + optional action ;; Shared empty state — icon + message + optional action
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~empty-state (&key icon message cls &rest children) (defcomp ~empty-state (&key (icon :as string?) (message :as string) (cls :as string?) &rest children)
(div :class (or cls "p-8 text-center text-stone-400") (div :class (or cls "p-8 text-center text-stone-400")
(when icon (div (i :class (str icon " text-4xl mb-2") :aria-hidden "true"))) (when icon (div (i :class (str icon " text-4xl mb-2") :aria-hidden "true")))
(p message) (p message)
@@ -81,7 +82,7 @@
;; Shared badge — inline pill with configurable colours ;; Shared badge — inline pill with configurable colours
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~badge (&key label cls) (defcomp ~badge (&key (label :as string) (cls :as string?))
(span :class (str "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium " (or cls "bg-stone-100 text-stone-700")) (span :class (str "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium " (or cls "bg-stone-100 text-stone-700"))
label)) label))
@@ -89,8 +90,9 @@
;; Shared delete button with confirm dialog ;; Shared delete button with confirm dialog
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~delete-btn (&key url trigger-target title text confirm-text cancel-text (defcomp ~delete-btn (&key (url :as string) (trigger-target :as string) (title :as string?)
sx-headers cls) (text :as string?) (confirm-text :as string?) (cancel-text :as string?)
(sx-headers :as string?) (cls :as string?))
(button :type "button" (button :type "button"
:data-confirm "" :data-confirm-title (or title "Delete?") :data-confirm "" :data-confirm-title (or title "Delete?")
:data-confirm-text (or text "Are you sure?") :data-confirm-text (or text "Are you sure?")
@@ -108,7 +110,7 @@
;; Shared price display — special + regular with strikethrough ;; Shared price display — special + regular with strikethrough
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~price (&key special-price regular-price) (defcomp ~price (&key (special-price :as string?) (regular-price :as string?))
(div :class "mt-1 flex items-baseline gap-2 justify-center" (div :class "mt-1 flex items-baseline gap-2 justify-center"
(when special-price (div :class "text-lg font-semibold text-emerald-700" special-price)) (when special-price (div :class "text-lg font-semibold text-emerald-700" special-price))
(when (and special-price regular-price) (div :class "text-sm line-through text-stone-500" regular-price)) (when (and special-price regular-price) (div :class "text-sm line-through text-stone-500" regular-price))
@@ -118,7 +120,8 @@
;; Shared image-or-placeholder ;; Shared image-or-placeholder
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~img-or-placeholder (&key src alt size-cls placeholder-icon placeholder-text) (defcomp ~img-or-placeholder (&key (src :as string?) (alt :as string?) (size-cls :as string?)
(placeholder-icon :as string?) (placeholder-text :as string?))
(if src (if src
(img :src src :alt (or alt "") :class (or size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0")) (img :src src :alt (or alt "") :class (or size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0"))
(div :class (str (or size-cls "w-12 h-12 rounded-full") " bg-stone-200 flex items-center justify-center flex-shrink-0") (div :class (str (or size-cls "w-12 h-12 rounded-full") " bg-stone-200 flex items-center justify-center flex-shrink-0")
@@ -141,8 +144,9 @@
(path :stroke-linecap "round" :stroke-linejoin "round" (path :stroke-linecap "round" :stroke-linejoin "round"
:d "M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"))) :d "M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z")))
(defcomp ~view-toggle (&key list-href tile-href hx-select list-cls tile-cls (defcomp ~view-toggle (&key (list-href :as string) (tile-href :as string) (hx-select :as string?)
storage-key list-svg tile-svg) (list-cls :as string?) (tile-cls :as string?) (storage-key :as string?)
list-svg tile-svg)
(div :class "hidden md:flex justify-end px-3 pt-3 gap-1" (div :class "hidden md:flex justify-end px-3 pt-3 gap-1"
(a :href list-href :sx-get list-href :sx-target "#main-panel" :sx-select hx-select (a :href list-href :sx-get list-href :sx-target "#main-panel" :sx-select hx-select
:sx-swap "outerHTML" :sx-push-url "true" :class (str "p-1.5 rounded " list-cls) :title "List view" :sx-swap "outerHTML" :sx-push-url "true" :class (str "p-1.5 rounded " list-cls) :title "List view"
@@ -157,7 +161,9 @@
;; Shared CRUD admin panel — for calendars, markets, etc. ;; Shared CRUD admin panel — for calendars, markets, etc.
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~crud-create-form (&key create-url csrf errors-id list-id placeholder label btn-label) (defcomp ~crud-create-form (&key (create-url :as string) (csrf :as string) (errors-id :as string?)
(list-id :as string?) (placeholder :as string?) (label :as string?)
(btn-label :as string?))
(<> (<>
(div :id (or errors-id "crud-create-errors") :class "mt-2 text-sm text-red-600") (div :id (or errors-id "crud-create-errors") :class "mt-2 text-sm text-red-600")
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url (form :class "mt-4 flex gap-2 items-end" :sx-post create-url
@@ -171,13 +177,14 @@
:placeholder (or placeholder "Name"))) :placeholder (or placeholder "Name")))
(button :type "submit" :class "border rounded px-3 py-2" (or btn-label "Add"))))) (button :type "submit" :class "border rounded px-3 py-2" (or btn-label "Add")))))
(defcomp ~crud-panel (&key form list list-id) (defcomp ~crud-panel (&key form list (list-id :as string?))
(section :class "p-4" (section :class "p-4"
form form
(div :id (or list-id "crud-list") :class "mt-6" list))) (div :id (or list-id "crud-list") :class "mt-6" list)))
(defcomp ~crud-item (&key href name slug del-url csrf-hdr list-id (defcomp ~crud-item (&key (href :as string) (name :as string) (slug :as string) (del-url :as string)
confirm-title confirm-text) (csrf-hdr :as string) (list-id :as string?) (confirm-title :as string?)
(confirm-text :as string?))
(div :class "mt-6 border rounded-lg p-4" (div :class "mt-6 border rounded-lg p-4"
(div :class "flex items-center justify-between gap-3" (div :class "flex items-center justify-between gap-3"
(a :class "flex items-baseline gap-3" :href href (a :class "flex items-baseline gap-3" :href href
@@ -199,9 +206,10 @@
;; checkout prefix) used by blog, events, and cart admin panels. ;; checkout prefix) used by blog, events, and cart admin panels.
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~sumup-settings-form (&key update-url csrf merchant-code placeholder (defcomp ~sumup-settings-form (&key (update-url :as string) (csrf :as string?) (merchant-code :as string?)
input-cls sumup-configured checkout-prefix (placeholder :as string?) (input-cls :as string?)
panel-id sx-select) (sumup-configured :as boolean?) (checkout-prefix :as string?)
(panel-id :as string?) (sx-select :as string?))
(div :id (or panel-id "payments-panel") :class "space-y-4 p-4 bg-white rounded-lg border border-stone-200" (div :id (or panel-id "payments-panel") :class "space-y-4 p-4 bg-white rounded-lg border border-stone-200"
(h3 :class "text-lg font-semibold text-stone-800" (h3 :class "text-lg font-semibold text-stone-800"
(i :class "fa fa-credit-card text-purple-600 mr-1") " SumUp Payment") (i :class "fa fa-credit-card text-purple-600 mr-1") " SumUp Payment")
@@ -233,7 +241,7 @@
;; Shared avatar — image or initial-letter placeholder ;; Shared avatar — image or initial-letter placeholder
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~avatar (&key src cls initial) (defcomp ~avatar (&key (src :as string?) (cls :as string?) (initial :as string?))
(if src (if src
(img :src src :alt "" :class cls) (img :src src :alt "" :class cls)
(div :class cls initial))) (div :class cls initial)))
@@ -242,7 +250,9 @@
;; Shared scroll-nav wrapper — horizontal scrollable nav with arrows ;; Shared scroll-nav wrapper — horizontal scrollable nav with arrows
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~scroll-nav-wrapper (&key wrapper-id container-id arrow-cls left-hs scroll-hs right-hs items oob) (defcomp ~scroll-nav-wrapper (&key (wrapper-id :as string) (container-id :as string) (arrow-cls :as string?)
(left-hs :as string?) (scroll-hs :as string?) (right-hs :as string?)
items (oob :as boolean?))
(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl" (div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
:id wrapper-id :sx-swap-oob (if oob "outerHTML" nil) :id wrapper-id :sx-swap-oob (if oob "outerHTML" nil)
(button :class (str (or arrow-cls "nav-arrow") " hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded") (button :class (str (or arrow-cls "nav-arrow") " hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded")

View File

@@ -1,11 +1,12 @@
(defcomp ~calendar-entry-nav (&key href name date-str nav-class) (defcomp ~calendar-entry-nav (&key (href :as string) (name :as string) (date-str :as string) (nav-class :as string?))
(a :href href :class nav-class (a :href href :class nav-class
(div :class "w-8 h-8 rounded bg-stone-200 flex-shrink-0") (div :class "w-8 h-8 rounded bg-stone-200 flex-shrink-0")
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "font-medium truncate" name) (div :class "font-medium truncate" name)
(div :class "text-xs text-stone-600 truncate" date-str)))) (div :class "text-xs text-stone-600 truncate" date-str))))
(defcomp ~calendar-link-nav (&key href name nav-class is-selected select-colours) (defcomp ~calendar-link-nav (&key (href :as string) (name :as string) (nav-class :as string?)
(is-selected :as string?) (select-colours :as string?))
(a :href href (a :href href
:sx-get href :sx-get href
:sx-target "#main-panel" :sx-target "#main-panel"
@@ -17,12 +18,14 @@
(i :class "fa fa-calendar" :aria-hidden "true") (i :class "fa fa-calendar" :aria-hidden "true")
(span name))) (span name)))
(defcomp ~market-link-nav (&key href name nav-class select-colours) (defcomp ~market-link-nav (&key (href :as string) (name :as string) (nav-class :as string?)
(select-colours :as string?))
(a :href href :class (str (or nav-class "") " " (or select-colours "")) (a :href href :class (str (or nav-class "") " " (or select-colours ""))
(i :class "fa fa-shopping-bag" :aria-hidden "true") (i :class "fa fa-shopping-bag" :aria-hidden "true")
(span name))) (span name)))
(defcomp ~relation-nav (&key href name icon nav-class relation-type) (defcomp ~relation-nav (&key (href :as string) (name :as string) (icon :as string?)
(nav-class :as string?) (relation-type :as string?))
(a :href href :class (or nav-class "flex items-center gap-3 rounded-lg py-2 px-3 text-sm text-stone-700 hover:bg-stone-100 transition-colors") (a :href href :class (or nav-class "flex items-center gap-3 rounded-lg py-2 px-3 text-sm text-stone-700 hover:bg-stone-100 transition-colors")
(when icon (when icon
(div :class "w-8 h-8 rounded bg-stone-200 flex items-center justify-center flex-shrink-0" (div :class "w-8 h-8 rounded bg-stone-200 flex items-center justify-center flex-shrink-0"

View File

@@ -6,7 +6,8 @@
;; Order table rows ;; Order table rows
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-row-desktop (&key oid created desc total pill status url) (defcomp ~order-row-desktop (&key (oid :as string) (created :as string) (desc :as string) (total :as string)
(pill :as string) (status :as string) (url :as string))
(tr :class "hidden sm:table-row border-t border-stone-100 hover:bg-stone-50/60" (tr :class "hidden sm:table-row border-t border-stone-100 hover:bg-stone-50/60"
(td :class "px-3 py-2 align-top" (span :class "font-mono text-[11px] sm:text-xs" oid)) (td :class "px-3 py-2 align-top" (span :class "font-mono text-[11px] sm:text-xs" oid))
(td :class "px-3 py-2 align-top text-stone-700 text-xs sm:text-sm" created) (td :class "px-3 py-2 align-top text-stone-700 text-xs sm:text-sm" created)
@@ -16,7 +17,8 @@
(td :class "px-3 py-0.5 align-top text-right" (td :class "px-3 py-0.5 align-top text-right"
(a :href url :class "inline-flex items-center px-3 py-1.5 text-xs sm:text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition" "View")))) (a :href url :class "inline-flex items-center px-3 py-1.5 text-xs sm:text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition" "View"))))
(defcomp ~order-row-mobile (&key oid created total pill status url) (defcomp ~order-row-mobile (&key (oid :as string) (created :as string) (total :as string)
(pill :as string) (status :as string) (url :as string))
(tr :class "sm:hidden border-t border-stone-100" (tr :class "sm:hidden border-t border-stone-100"
(td :colspan "5" :class "px-3 py-3" (td :colspan "5" :class "px-3 py-3"
(div :class "flex flex-col gap-2 text-xs" (div :class "flex flex-col gap-2 text-xs"
@@ -61,13 +63,14 @@
;; Order detail panels ;; Order detail panels
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-item-image (&key src alt) (defcomp ~order-item-image (&key (src :as string) (alt :as string))
(img :src src :alt alt :class "w-full h-full object-contain object-center" :loading "lazy" :decoding "async")) (img :src src :alt alt :class "w-full h-full object-contain object-center" :loading "lazy" :decoding "async"))
(defcomp ~order-item-no-image () (defcomp ~order-item-no-image ()
(div :class "w-full h-full flex items-center justify-center text-[9px] text-stone-400" "No image")) (div :class "w-full h-full flex items-center justify-center text-[9px] text-stone-400" "No image"))
(defcomp ~order-item-row (&key href img title pid qty price) (defcomp ~order-item-row (&key (href :as string) img (title :as string) (pid :as string)
(qty :as string) (price :as string))
(li (a :class "w-full py-2 flex gap-3" :href href (li (a :class "w-full py-2 flex gap-3" :href href
(div :class "w-12 h-12 sm:w-14 sm:h-14 rounded-md bg-stone-100 flex-shrink-0 overflow-hidden" img) (div :class "w-12 h-12 sm:w-14 sm:h-14 rounded-md bg-stone-100 flex-shrink-0 overflow-hidden" img)
(div :class "flex-1 flex justify-between gap-3" (div :class "flex-1 flex justify-between gap-3"
@@ -83,7 +86,8 @@
(h2 :class "text-sm sm:text-base font-semibold mb-3" "Items") (h2 :class "text-sm sm:text-base font-semibold mb-3" "Items")
(ul :class "divide-y divide-stone-100 text-xs sm:text-sm" items))) (ul :class "divide-y divide-stone-100 text-xs sm:text-sm" items)))
(defcomp ~order-calendar-entry (&key name pill status date-str cost) (defcomp ~order-calendar-entry (&key (name :as string) (pill :as string) (status :as string)
(date-str :as string) (cost :as string))
(li :class "px-4 py-3 flex items-start justify-between text-sm" (li :class "px-4 py-3 flex items-start justify-between text-sm"
(div (div :class "font-medium flex items-center gap-2" (div (div :class "font-medium flex items-center gap-2"
name (span :class pill status)) name (span :class pill status))
@@ -98,11 +102,12 @@
(defcomp ~order-detail-panel (&key summary items calendar) (defcomp ~order-detail-panel (&key summary items calendar)
(div :class "max-w-full px-3 py-3 space-y-4" summary items calendar)) (div :class "max-w-full px-3 py-3 space-y-4" summary items calendar))
(defcomp ~order-pay-btn (&key url) (defcomp ~order-pay-btn (&key (url :as string))
(a :href url :class "inline-flex items-center px-3 py-2 text-xs sm:text-sm rounded-full border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 transition" (a :href url :class "inline-flex items-center px-3 py-2 text-xs sm:text-sm rounded-full border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 transition"
(i :class "fa fa-credit-card mr-2" :aria-hidden "true") "Open payment page")) (i :class "fa fa-credit-card mr-2" :aria-hidden "true") "Open payment page"))
(defcomp ~order-detail-filter (&key info list-url recheck-url csrf pay) (defcomp ~order-detail-filter (&key (info :as string) (list-url :as string) (recheck-url :as string)
(csrf :as string) pay)
(header :class "mb-6 sm:mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4" (header :class "mb-6 sm:mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4"
(div :class "space-y-1" (div :class "space-y-1"
(p :class "text-xs sm:text-sm text-stone-600" info)) (p :class "text-xs sm:text-sm text-stone-600" info))
@@ -124,7 +129,8 @@
;; Data-driven order rows (replaces Python loop) ;; Data-driven order rows (replaces Python loop)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-rows-from-data (&key orders page total-pages next-url) (defcomp ~order-rows-from-data (&key (orders :as list?) (page :as number) (total-pages :as number)
(next-url :as string?))
(<> (<>
(map (lambda (o) (map (lambda (o)
(<> (<>
@@ -144,7 +150,7 @@
;; Data-driven order items (replaces Python loop) ;; Data-driven order items (replaces Python loop)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-items-from-data (&key items) (defcomp ~order-items-from-data (&key (items :as list?))
(~order-items-panel (~order-items-panel
:items (<> (map (lambda (item) :items (<> (map (lambda (item)
(let* ((img (if (get item "product_image") (let* ((img (if (get item "product_image")
@@ -162,7 +168,7 @@
;; Data-driven calendar entries (replaces Python loop) ;; Data-driven calendar entries (replaces Python loop)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-calendar-from-data (&key entries) (defcomp ~order-calendar-from-data (&key (entries :as list?))
(~order-calendar-section (~order-calendar-section
:items (<> (map (lambda (e) :items (<> (map (lambda (e)
(~order-calendar-entry (~order-calendar-entry
@@ -180,7 +186,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Status pill class mapping ;; Status pill class mapping
(defcomp ~order-status-pill-cls (&key status) (defcomp ~order-status-pill-cls (&key (status :as string?))
(let* ((sl (lower (or status "")))) (let* ((sl (lower (or status ""))))
(cond (cond
((= sl "paid") "border-emerald-300 bg-emerald-50 text-emerald-700") ((= sl "paid") "border-emerald-300 bg-emerald-50 text-emerald-700")
@@ -188,7 +194,7 @@
(true "border-stone-300 bg-stone-50 text-stone-700")))) (true "border-stone-300 bg-stone-50 text-stone-700"))))
;; Single order row pair (desktop + mobile) — takes serialized order data dict ;; Single order row pair (desktop + mobile) — takes serialized order data dict
(defcomp ~order-row-pair (&key order detail-url-prefix) (defcomp ~order-row-pair (&key (order :as dict) (detail-url-prefix :as string))
(let* ((status (or (get order "status") "pending")) (let* ((status (or (get order "status") "pending"))
(pill-base (~order-status-pill-cls :status status)) (pill-base (~order-status-pill-cls :status status))
(oid (str "#" (get order "id"))) (oid (str "#" (get order "id")))
@@ -207,7 +213,8 @@
:status status :url url)))) :status status :url url))))
;; Assembled orders list content ;; Assembled orders list content
(defcomp ~orders-list-content (&key orders page total-pages rows-url detail-url-prefix) (defcomp ~orders-list-content (&key (orders :as list) (page :as number) (total-pages :as number)
(rows-url :as string) (detail-url-prefix :as string))
(if (empty? orders) (if (empty? orders)
(~order-empty-state) (~order-empty-state)
(~order-table (~order-table
@@ -223,7 +230,7 @@
(~order-end-row)))))) (~order-end-row))))))
;; Assembled order detail content — replaces Python _order_main_sx ;; Assembled order detail content — replaces Python _order_main_sx
(defcomp ~order-detail-content (&key order calendar-entries) (defcomp ~order-detail-content (&key (order :as dict) (calendar-entries :as list?))
(let* ((items (get order "items"))) (let* ((items (get order "items")))
(~order-detail-panel (~order-detail-panel
:summary (~order-summary-card :summary (~order-summary-card
@@ -265,7 +272,8 @@
calendar-entries)))))) calendar-entries))))))
;; Assembled order detail filter — replaces Python _order_filter_sx ;; Assembled order detail filter — replaces Python _order_filter_sx
(defcomp ~order-detail-filter-content (&key order list-url recheck-url pay-url csrf) (defcomp ~order-detail-filter-content (&key (order :as dict) (list-url :as string) (recheck-url :as string)
(pay-url :as string) (csrf :as string))
(let* ((status (or (get order "status") "pending")) (let* ((status (or (get order "status") "pending"))
(created (or (get order "created_at_formatted") "\u2014"))) (created (or (get order "created_at_formatted") "\u2014")))
(~order-detail-filter (~order-detail-filter
@@ -280,7 +288,7 @@
;; Checkout return components ;; Checkout return components
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~checkout-return-header (&key status) (defcomp ~checkout-return-header (&key (status :as string))
(header :class "mb-6 sm:mb-8" (header :class "mb-6 sm:mb-8"
(h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" "Payment complete") (h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" "Payment complete")
(p :class "text-xs sm:text-sm text-stone-600" (p :class "text-xs sm:text-sm text-stone-600"
@@ -290,7 +298,9 @@
(div :class "max-w-full px-3 py-3 space-y-4" (div :class "max-w-full px-3 py-3 space-y-4"
(p :class "text-sm text-stone-600" "Order not found."))) (p :class "text-sm text-stone-600" "Order not found.")))
(defcomp ~checkout-return-ticket (&key name pill state type-name date-str code price) (defcomp ~checkout-return-ticket (&key (name :as string) (pill :as string) (state :as string)
(type-name :as string?) (date-str :as string) (code :as string?)
(price :as string))
(li :class "px-4 py-3 flex items-start justify-between text-sm" (li :class "px-4 py-3 flex items-start justify-between text-sm"
(div (div
(div :class "font-medium flex items-center gap-2" (div :class "font-medium flex items-center gap-2"
@@ -305,7 +315,7 @@
(h2 :class "text-base sm:text-lg font-semibold" "Tickets") (h2 :class "text-base sm:text-lg font-semibold" "Tickets")
(ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items))) (ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items)))
(defcomp ~checkout-return-failed (&key order-id) (defcomp ~checkout-return-failed (&key (order-id :as string?))
(div :class "rounded-lg border border-rose-200 bg-rose-50 p-4 text-sm text-rose-900" (div :class "rounded-lg border border-rose-200 bg-rose-50 p-4 text-sm text-rose-900"
(p :class "font-medium" "Payment failed") (p :class "font-medium" "Payment failed")
(p "Please try again or contact support." (p "Please try again or contact support."
@@ -329,10 +339,10 @@
(h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" "Checkout error") (h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" "Checkout error")
(p :class "text-xs sm:text-sm text-stone-600" "We tried to start your payment with SumUp but hit a problem."))) (p :class "text-xs sm:text-sm text-stone-600" "We tried to start your payment with SumUp but hit a problem.")))
(defcomp ~checkout-error-order-id (&key oid) (defcomp ~checkout-error-order-id (&key (oid :as string))
(p :class "text-xs text-rose-800/80" "Order ID: " (span :class "font-mono" oid))) (p :class "text-xs text-rose-800/80" "Order ID: " (span :class "font-mono" oid)))
(defcomp ~checkout-error-content (&key msg order back-url) (defcomp ~checkout-error-content (&key (msg :as string) order (back-url :as string))
(div :class "max-w-full px-3 py-3 space-y-4" (div :class "max-w-full px-3 py-3 space-y-4"
(div :class "rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2" (div :class "rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2"
(p :class "font-medium" "Something went wrong.") (p :class "font-medium" "Something went wrong.")

View File

@@ -1,4 +1,4 @@
(defcomp ~base-shell (&key title asset-url &rest children) (defcomp ~base-shell (&key (title :as string) (asset-url :as string) &rest children)
(<> (<>
(raw! "<!doctype html>") (raw! "<!doctype html>")
(html :lang "en" (html :lang "en"
@@ -23,13 +23,13 @@
;; <script>__sxResolve("id", "(resolved sx ...)")</script> ;; <script>__sxResolve("id", "(resolved sx ...)")</script>
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~suspense (&key id fallback &rest children) (defcomp ~suspense (&key (id :as string) fallback &rest children)
(div :id (str "sx-suspense-" id) (div :id (str "sx-suspense-" id)
:data-suspense id :data-suspense id
:style "display:contents" :style "display:contents"
(if (not (empty? children)) children fallback))) (if (not (empty? children)) children fallback)))
(defcomp ~error-page (&key title message image asset-url) (defcomp ~error-page (&key (title :as string) (message :as string) (image :as string?) (asset-url :as string))
(~base-shell :title title :asset-url asset-url (~base-shell :title title :asset-url asset-url
(div :class "text-center p-8 max-w-lg mx-auto" (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 :class "font-bold text-2xl md:text-4xl text-red-500 mb-4"

View File

@@ -1,4 +1,4 @@
(defcomp ~relation-attach (&key create-url label icon) (defcomp ~relation-attach (&key (create-url :as string) (label :as string?) (icon :as string?))
(a :href create-url (a :href create-url
:sx-get create-url :sx-get create-url
:sx-target "#main-panel" :sx-target "#main-panel"
@@ -8,7 +8,7 @@
(when icon (i :class icon)) (when icon (i :class icon))
(span (or label "Add")))) (span (or label "Add"))))
(defcomp ~relation-detach (&key detach-url name) (defcomp ~relation-detach (&key (detach-url :as string) (name :as string?))
(button :sx-delete detach-url (button :sx-delete detach-url
:sx-confirm (str "Remove " (or name "this item") "?") :sx-confirm (str "Remove " (or name "this item") "?")
:class "text-red-500 hover:text-red-700 text-sm p-1 rounded hover:bg-red-50 transition-colors" :class "text-red-500 hover:text-red-700 text-sm p-1 rounded hover:bg-red-50 transition-colors"

View File

@@ -11,13 +11,13 @@
;; - Pre-rendered meta HTML from callers ;; - Pre-rendered meta HTML from callers
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~sx-page-shell (&key title meta-html csrf (defcomp ~sx-page-shell (&key (title :as string) (meta-html :as string?) (csrf :as string)
sx-css sx-css-classes (sx-css :as string?) (sx-css-classes :as string?)
component-hash component-defs (component-hash :as string?) (component-defs :as string?)
pages-sx page-sx (pages-sx :as string?) (page-sx :as string?)
asset-url sx-js-hash body-js-hash (asset-url :as string) (sx-js-hash :as string) (body-js-hash :as string?)
head-scripts inline-css inline-head-js (head-scripts :as list?) (inline-css :as string?) (inline-head-js :as string?)
init-sx body-scripts) (init-sx :as string?) (body-scripts :as list?))
(<> (<>
(raw! "<!doctype html>") (raw! "<!doctype html>")
(html :lang "en" (html :lang "en"

View File

@@ -6,14 +6,14 @@
;; --- Demo components with different affinities --- ;; --- Demo components with different affinities ---
(defcomp ~aff-demo-auto (&key label) (defcomp ~aff-demo-auto (&key (label :as string?))
(div :class "rounded border border-stone-200 bg-white p-4" (div :class "rounded border border-stone-200 bg-white p-4"
(div :class "flex items-center gap-2 mb-2" (div :class "flex items-center gap-2 mb-2"
(span :class "inline-block w-2 h-2 rounded-full bg-stone-400") (span :class "inline-block w-2 h-2 rounded-full bg-stone-400")
(span :class "text-sm font-mono text-stone-500" ":affinity :auto")) (span :class "text-sm font-mono text-stone-500" ":affinity :auto"))
(p :class "text-stone-800" (or label "Pure component — no IO calls. Auto-detected as client-renderable.")))) (p :class "text-stone-800" (or label "Pure component — no IO calls. Auto-detected as client-renderable."))))
(defcomp ~aff-demo-client (&key label) (defcomp ~aff-demo-client (&key (label :as string?))
:affinity :client :affinity :client
(div :class "rounded border border-blue-200 bg-blue-50 p-4" (div :class "rounded border border-blue-200 bg-blue-50 p-4"
(div :class "flex items-center gap-2 mb-2" (div :class "flex items-center gap-2 mb-2"
@@ -21,7 +21,7 @@
(span :class "text-sm font-mono text-blue-600" ":affinity :client")) (span :class "text-sm font-mono text-blue-600" ":affinity :client"))
(p :class "text-blue-800" (or label "Explicitly client-rendered — even IO calls would be proxied.")))) (p :class "text-blue-800" (or label "Explicitly client-rendered — even IO calls would be proxied."))))
(defcomp ~aff-demo-server (&key label) (defcomp ~aff-demo-server (&key (label :as string?))
:affinity :server :affinity :server
(div :class "rounded border border-amber-200 bg-amber-50 p-4" (div :class "rounded border border-amber-200 bg-amber-50 p-4"
(div :class "flex items-center gap-2 mb-2" (div :class "flex items-center gap-2 mb-2"

View File

@@ -3,8 +3,8 @@
;; Drill down into each bundle to see component tree; expand to see SX source. ;; Drill down into each bundle to see component tree; expand to see SX source.
;; @css bg-green-100 text-green-800 bg-violet-600 bg-stone-200 text-violet-600 text-stone-600 text-green-600 rounded-full h-2.5 grid-cols-3 bg-blue-100 text-blue-800 bg-amber-100 text-amber-800 grid-cols-4 marker:text-stone-400 bg-blue-50 bg-amber-50 text-blue-700 text-amber-700 border-blue-200 border-amber-200 bg-blue-500 bg-amber-500 ;; @css bg-green-100 text-green-800 bg-violet-600 bg-stone-200 text-violet-600 text-stone-600 text-green-600 rounded-full h-2.5 grid-cols-3 bg-blue-100 text-blue-800 bg-amber-100 text-amber-800 grid-cols-4 marker:text-stone-400 bg-blue-50 bg-amber-50 text-blue-700 text-amber-700 border-blue-200 border-amber-200 bg-blue-500 bg-amber-500
(defcomp ~bundle-analyzer-content (&key pages total-components total-macros (defcomp ~bundle-analyzer-content (&key (pages :as list) (total-components :as number) (total-macros :as number)
pure-count io-count) (pure-count :as number) (io-count :as number))
(~doc-page :title "Page Bundle Analyzer" (~doc-page :title "Page Bundle Analyzer"
(p :class "text-stone-600 mb-6" (p :class "text-stone-600 mb-6"
@@ -55,13 +55,13 @@
"walks all branches of control flow (if/when/cond/case), " "walks all branches of control flow (if/when/cond/case), "
"and includes macro definitions shared across components.")))) "and includes macro definitions shared across components."))))
(defcomp ~analyzer-stat (&key label value cls) (defcomp ~analyzer-stat (&key (label :as string) (value :as string) (cls :as string))
(div :class "rounded-lg border border-stone-200 p-4 text-center" (div :class "rounded-lg border border-stone-200 p-4 text-center"
(div :class (str "text-3xl font-bold " cls) value) (div :class (str "text-3xl font-bold " cls) value)
(div :class "text-sm text-stone-500 mt-1" label))) (div :class "text-sm text-stone-500 mt-1" label)))
(defcomp ~analyzer-row (&key name path needed direct total pct savings (defcomp ~analyzer-row (&key (name :as string) (path :as string) (needed :as number) (direct :as number) (total :as number) (pct :as number) (savings :as number)
io-refs pure-in-page io-in-page components) (io-refs :as list) (pure-in-page :as number) (io-in-page :as number) (components :as list))
(details :class "rounded border border-stone-200" (details :class "rounded border border-stone-200"
(summary :class "p-4 cursor-pointer hover:bg-stone-50 transition-colors" (summary :class "p-4 cursor-pointer hover:bg-stone-50 transition-colors"
(div :class "flex items-center justify-between mb-2" (div :class "flex items-center justify-between mb-2"
@@ -97,7 +97,7 @@
:source (get comp "source"))) :source (get comp "source")))
components))))) components)))))
(defcomp ~analyzer-component (&key comp-name is-pure io-refs deps source) (defcomp ~analyzer-component (&key (comp-name :as string) (is-pure :as boolean) (io-refs :as list) (deps :as list) (source :as string))
(details :class (str "rounded border " (details :class (str "rounded border "
(if is-pure "border-blue-200 bg-blue-50" "border-amber-200 bg-amber-50")) (if is-pure "border-blue-200 bg-blue-50" "border-amber-200 bg-amber-50"))
(summary :class "px-3 py-2 cursor-pointer hover:opacity-80 transition-opacity" (summary :class "px-3 py-2 cursor-pointer hover:opacity-80 transition-opacity"

View File

@@ -9,7 +9,7 @@
;; "sx:route client+data" — cache miss, fetched from server ;; "sx:route client+data" — cache miss, fetched from server
;; "sx:route client+cache" — cache hit, rendered from cached data ;; "sx:route client+cache" — cache hit, rendered from cached data
(defcomp ~data-test-content (&key server-time items phase transport) (defcomp ~data-test-content (&key (server-time :as string) (items :as list) (phase :as string) (transport :as string))
(div :class "space-y-8" (div :class "space-y-8"
(div :class "border-b border-stone-200 pb-6" (div :class "border-b border-stone-200 pb-6"
(h1 :class "text-2xl font-bold text-stone-900" "Data Test") (h1 :class "text-2xl font-bold text-stone-900" "Data Test")

View File

@@ -1,18 +1,18 @@
;; SX docs utility components ;; SX docs utility components
(defcomp ~doc-placeholder (&key id) (defcomp ~doc-placeholder (&key (id :as string))
(div :id id (div :id id
(div :class "bg-stone-100 rounded p-4 mt-3" (div :class "bg-stone-100 rounded p-4 mt-3"
(p :class "text-stone-400 italic text-sm" (p :class "text-stone-400 italic text-sm"
"Trigger the demo to see the actual content.")))) "Trigger the demo to see the actual content."))))
(defcomp ~doc-oob-code (&key target-id text) (defcomp ~doc-oob-code (&key (target-id :as string) (text :as string))
(div :id target-id :sx-swap-oob "innerHTML" (div :id target-id :sx-swap-oob "innerHTML"
(div :class "not-prose bg-stone-100 rounded p-4 mt-3" (div :class "not-prose bg-stone-100 rounded p-4 mt-3"
(pre :class "text-sm whitespace-pre-wrap break-words" (pre :class "text-sm whitespace-pre-wrap break-words"
(code text))))) (code text)))))
(defcomp ~doc-attr-table (&key title rows) (defcomp ~doc-attr-table (&key (title :as string) rows)
(div :class "space-y-3" (div :class "space-y-3"
(h3 :class "text-xl font-semibold text-stone-700" title) (h3 :class "text-xl font-semibold text-stone-700" title)
(div :class "overflow-x-auto rounded border border-stone-200" (div :class "overflow-x-auto rounded border border-stone-200"
@@ -23,7 +23,7 @@
(th :class "px-3 py-2 font-medium text-stone-600 text-center w-20" "In sx?"))) (th :class "px-3 py-2 font-medium text-stone-600 text-center w-20" "In sx?")))
(tbody rows))))) (tbody rows)))))
(defcomp ~doc-headers-table (&key title rows) (defcomp ~doc-headers-table (&key (title :as string) rows)
(div :class "space-y-3" (div :class "space-y-3"
(h3 :class "text-xl font-semibold text-stone-700" title) (h3 :class "text-xl font-semibold text-stone-700" title)
(div :class "overflow-x-auto rounded border border-stone-200" (div :class "overflow-x-auto rounded border border-stone-200"
@@ -34,7 +34,7 @@
(th :class "px-3 py-2 font-medium text-stone-600" "Description"))) (th :class "px-3 py-2 font-medium text-stone-600" "Description")))
(tbody rows))))) (tbody rows)))))
(defcomp ~doc-headers-row (&key name value description href) (defcomp ~doc-headers-row (&key (name :as string) (value :as string) (description :as string) (href :as string?))
(tr :class "border-b border-stone-100" (tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm whitespace-nowrap" (td :class "px-3 py-2 font-mono text-sm whitespace-nowrap"
(if href (if href
@@ -46,7 +46,7 @@
(td :class "px-3 py-2 font-mono text-sm text-stone-500" value) (td :class "px-3 py-2 font-mono text-sm text-stone-500" value)
(td :class "px-3 py-2 text-stone-700 text-sm" description))) (td :class "px-3 py-2 text-stone-700 text-sm" description)))
(defcomp ~doc-two-col-row (&key name description href) (defcomp ~doc-two-col-row (&key (name :as string) (description :as string) (href :as string?))
(tr :class "border-b border-stone-100" (tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm whitespace-nowrap" (td :class "px-3 py-2 font-mono text-sm whitespace-nowrap"
(if href (if href
@@ -57,7 +57,7 @@
(span :class "text-violet-700" name))) (span :class "text-violet-700" name)))
(td :class "px-3 py-2 text-stone-700 text-sm" description))) (td :class "px-3 py-2 text-stone-700 text-sm" description)))
(defcomp ~doc-two-col-table (&key title intro col1 col2 rows) (defcomp ~doc-two-col-table (&key (title :as string?) (intro :as string?) (col1 :as string?) (col2 :as string?) rows)
(div :class "space-y-3" (div :class "space-y-3"
(when title (h3 :class "text-xl font-semibold text-stone-700" title)) (when title (h3 :class "text-xl font-semibold text-stone-700" title))
(when intro (p :class "text-stone-600 mb-6" intro)) (when intro (p :class "text-stone-600 mb-6" intro))
@@ -82,7 +82,7 @@
;; Build attr table from a list of {name, desc, exists, href} dicts. ;; Build attr table from a list of {name, desc, exists, href} dicts.
;; Replaces _attr_table_sx() in utils.py. ;; Replaces _attr_table_sx() in utils.py.
(defcomp ~doc-attr-table-from-data (&key title attrs) (defcomp ~doc-attr-table-from-data (&key (title :as string) (attrs :as list))
(~doc-attr-table :title title (~doc-attr-table :title title
:rows (<> (map (fn (a) :rows (<> (map (fn (a)
(~doc-attr-row (~doc-attr-row
@@ -94,7 +94,7 @@
;; Build headers table from a list of {name, value, desc} dicts. ;; Build headers table from a list of {name, value, desc} dicts.
;; Replaces _headers_table_sx() in utils.py. ;; Replaces _headers_table_sx() in utils.py.
(defcomp ~doc-headers-table-from-data (&key title headers) (defcomp ~doc-headers-table-from-data (&key (title :as string) (headers :as list))
(~doc-headers-table :title title (~doc-headers-table :title title
:rows (<> (map (fn (h) :rows (<> (map (fn (h)
(~doc-headers-row (~doc-headers-row
@@ -106,7 +106,7 @@
;; Build two-col table from a list of {name, desc} dicts. ;; Build two-col table from a list of {name, desc} dicts.
;; Replaces the _reference_events_sx / _reference_js_api_sx builders. ;; Replaces the _reference_events_sx / _reference_js_api_sx builders.
(defcomp ~doc-two-col-table-from-data (&key title intro col1 col2 items) (defcomp ~doc-two-col-table-from-data (&key (title :as string?) (intro :as string?) (col1 :as string?) (col2 :as string?) (items :as list))
(~doc-two-col-table :title title :intro intro :col1 col1 :col2 col2 (~doc-two-col-table :title title :intro intro :col1 col1 :col2 col2
:rows (<> (map (fn (item) :rows (<> (map (fn (item)
(~doc-two-col-row (~doc-two-col-row
@@ -117,7 +117,7 @@
;; Build all primitives category tables from a {category: [prim, ...]} dict. ;; Build all primitives category tables from a {category: [prim, ...]} dict.
;; Replaces _primitives_section_sx() in utils.py. ;; Replaces _primitives_section_sx() in utils.py.
(defcomp ~doc-primitives-tables (&key primitives) (defcomp ~doc-primitives-tables (&key (primitives :as dict))
(<> (map (fn (cat) (<> (map (fn (cat)
(~doc-primitives-table (~doc-primitives-table
:category cat :category cat
@@ -125,14 +125,14 @@
(keys primitives)))) (keys primitives))))
;; Build all special form category sections from a {category: [form, ...]} dict. ;; Build all special form category sections from a {category: [form, ...]} dict.
(defcomp ~doc-special-forms-tables (&key forms) (defcomp ~doc-special-forms-tables (&key (forms :as dict))
(<> (map (fn (cat) (<> (map (fn (cat)
(~doc-special-forms-category (~doc-special-forms-category
:category cat :category cat
:forms (get forms cat))) :forms (get forms cat)))
(keys forms)))) (keys forms))))
(defcomp ~doc-special-forms-category (&key category forms) (defcomp ~doc-special-forms-category (&key (category :as string) (forms :as list))
(div :class "space-y-4" (div :class "space-y-4"
(h3 :class "text-xl font-semibold text-stone-800 border-b border-stone-200 pb-2" category) (h3 :class "text-xl font-semibold text-stone-800 border-b border-stone-200 pb-2" category)
(div :class "space-y-4" (div :class "space-y-4"
@@ -145,7 +145,7 @@
:example (get f "example"))) :example (get f "example")))
forms)))) forms))))
(defcomp ~doc-special-form-card (&key name syntax doc tail-position example) (defcomp ~doc-special-form-card (&key (name :as string) (syntax :as string) (doc :as string) (tail-position :as string) (example :as string))
(div :class "not-prose border border-stone-200 rounded-lg p-4 space-y-3" (div :class "not-prose border border-stone-200 rounded-lg p-4 space-y-3"
(div :class "flex items-baseline gap-3" (div :class "flex items-baseline gap-3"
(code :class "text-lg font-bold text-violet-700" name) (code :class "text-lg font-bold text-violet-700" name)

View File

@@ -1,10 +1,10 @@
;; Example page template and reference index ;; Example page template and reference index
;; Template receives data values (code strings, titles), calls highlight internally. ;; Template receives data values (code strings, titles), calls highlight internally.
(defcomp ~example-page-content (&key title description demo-description demo (defcomp ~example-page-content (&key (title :as string) (description :as string) (demo-description :as string?) demo
sx-code sx-lang handler-code handler-lang (sx-code :as string) (sx-lang :as string?) (handler-code :as string) (handler-lang :as string?)
comp-placeholder-id wire-placeholder-id wire-note (comp-placeholder-id :as string?) (wire-placeholder-id :as string?) (wire-note :as string?)
comp-heading handler-heading) (comp-heading :as string?) (handler-heading :as string?))
(~doc-page :title title (~doc-page :title title
(p :class "text-stone-600 mb-6" description) (p :class "text-stone-600 mb-6" description)
(~example-card :title "Demo" :description demo-description (~example-card :title "Demo" :description demo-description

View File

@@ -1,6 +1,6 @@
;; 404 Not Found page content ;; 404 Not Found page content
(defcomp ~not-found-content (&key path) (defcomp ~not-found-content (&key (path :as string?))
(div :class "max-w-3xl mx-auto px-4 py-12 text-center" (div :class "max-w-3xl mx-auto px-4 py-12 text-center"
(h1 :style (cssx (:text (colour "stone" 800) (size "3xl") (weight "bold"))) (h1 :style (cssx (:text (colour "stone" 800) (size "3xl") (weight "bold")))
"404") "404")

View File

@@ -9,7 +9,7 @@
;; Shared card component — used by both server and client results ;; Shared card component — used by both server and client results
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~demo-result-card (&key title ms desc theme &rest children) (defcomp ~demo-result-card (&key (title :as string) (ms :as number) (desc :as string) (theme :as string) &rest children)
(let ((border (if (= theme "blue") "border-blue-200 bg-blue-50/30" "border-stone-200")) (let ((border (if (= theme "blue") "border-blue-200 bg-blue-50/30" "border-stone-200"))
(title-c (if (= theme "blue") "text-blue-700" "text-stone-700")) (title-c (if (= theme "blue") "text-blue-700" "text-stone-700"))
(badge-c (if (= theme "blue") "text-blue-400" "text-stone-400")) (badge-c (if (= theme "blue") "text-blue-400" "text-stone-400"))

View File

@@ -2,7 +2,7 @@
;; Reader Macro Demo: #z3 — SX Spec to SMT-LIB (live translation via z3.sx) ;; Reader Macro Demo: #z3 — SX Spec to SMT-LIB (live translation via z3.sx)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~z3-example (&key sx-source smt-output) (defcomp ~z3-example (&key (sx-source :as string) (smt-output :as string))
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4" (div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(div (div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SX Source") (p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SX Source")

View File

@@ -103,7 +103,7 @@
(~doc-section :title "SX Diff Viewer" :id "diff-viewer" (~doc-section :title "SX Diff Viewer" :id "diff-viewer"
(p "Diffs rendered as SX components, not pre-formatted text:") (p "Diffs rendered as SX components, not pre-formatted text:")
(highlight ";; The diff viewer is a defcomp, composable like any other (highlight ";; The diff viewer is a defcomp, composable like any other
(defcomp ~diff-view (&key diff) (defcomp ~diff-view (&key (diff :as dict))
(map (fn (hunk) (map (fn (hunk)
(~diff-hunk (~diff-hunk
:file (get hunk \"file\") :file (get hunk \"file\")

View File

@@ -3,7 +3,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Helper: render a Phase 1 result row ;; Helper: render a Phase 1 result row
(defcomp ~prove-phase1-row (&key name status) (defcomp ~prove-phase1-row (&key (name :as string) (status :as string))
(tr :class "border-t border-stone-100" (tr :class "border-t border-stone-100"
(td :class "py-1.5 px-3 font-mono text-xs text-stone-700" name) (td :class "py-1.5 px-3 font-mono text-xs text-stone-700" name)
(td :class "py-1.5 px-3 text-xs" (td :class "py-1.5 px-3 text-xs"
@@ -12,7 +12,7 @@
(span :class "text-red-600 font-medium" status))))) (span :class "text-red-600 font-medium" status)))))
;; Helper: render a Phase 2 result row ;; Helper: render a Phase 2 result row
(defcomp ~prove-phase2-row (&key name status tested skipped counterexample) (defcomp ~prove-phase2-row (&key (name :as string) (status :as string) (tested :as number) (skipped :as number) (counterexample :as string?))
(tr :class "border-t border-stone-100" (tr :class "border-t border-stone-100"
(td :class "py-1.5 px-3 font-mono text-xs text-stone-700" name) (td :class "py-1.5 px-3 font-mono text-xs text-stone-700" name)
(td :class "py-1.5 px-3 text-xs" (td :class "py-1.5 px-3 text-xs"

View File

@@ -30,8 +30,8 @@
(~doc-page :title "JavaScript API" (~doc-page :title "JavaScript API"
table)) table))
(defcomp ~reference-attr-detail-content (&key title description demo (defcomp ~reference-attr-detail-content (&key (title :as string) (description :as string) demo
example-code handler-code wire-placeholder-id) (example-code :as string) (handler-code :as string?) (wire-placeholder-id :as string?))
(~doc-page :title title (~doc-page :title title
(p :class "text-stone-600 mb-6" description) (p :class "text-stone-600 mb-6" description)
(when demo (when demo
@@ -50,8 +50,8 @@
"Trigger the demo to see the raw response the server sends.") "Trigger the demo to see the raw response the server sends.")
(~doc-placeholder :id wire-placeholder-id))))) (~doc-placeholder :id wire-placeholder-id)))))
(defcomp ~reference-header-detail-content (&key title direction description (defcomp ~reference-header-detail-content (&key (title :as string) (direction :as string) (description :as string)
example-code demo) (example-code :as string?) demo)
(~doc-page :title title (~doc-page :title title
(let ((badge-class (if (= direction "request") (let ((badge-class (if (= direction "request")
"bg-blue-100 text-blue-700" "bg-blue-100 text-blue-700"
@@ -84,7 +84,7 @@
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Example usage") (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Example usage")
(~example-source :code (highlight example-code "lisp")))))) (~example-source :code (highlight example-code "lisp"))))))
(defcomp ~reference-attr-not-found (&key slug) (defcomp ~reference-attr-not-found (&key (slug :as string))
(~doc-page :title "Not Found" (~doc-page :title "Not Found"
(p :class "text-stone-600" (p :class "text-stone-600"
(str "No documentation found for \"" slug "\".")))) (str "No documentation found for \"" slug "\"."))))

View File

@@ -72,7 +72,7 @@
(li (strong "Swap: ") "On success, the rendered DOM replaces " (code "#main-panel") " contents, " (code "pushState") " updates the URL, and the console logs " (code "sx:route client /path") ".") (li (strong "Swap: ") "On success, the rendered DOM replaces " (code "#main-panel") " contents, " (code "pushState") " updates the URL, and the console logs " (code "sx:route client /path") ".")
(li (strong "Fallback: ") "If anything fails (no match, eval error, missing component), the click falls through to a standard server fetch. Console logs " (code "sx:route server /path") ". The user sees no difference."))))) (li (strong "Fallback: ") "If anything fails (no match, eval error, missing component), the click falls through to a standard server fetch. Console logs " (code "sx:route server /path") ". The user sees no difference.")))))
(defcomp ~routing-row (&key name path mode has-data content-expr reason) (defcomp ~routing-row (&key (name :as string) (path :as string) (mode :as string) (has-data :as boolean) (content-expr :as string?) (reason :as string?))
(div :class (str "rounded border p-3 flex items-center gap-3 " (div :class (str "rounded border p-3 flex items-center gap-3 "
(if (= mode "client") (if (= mode "client")
"border-green-200 bg-green-50" "border-green-200 bg-green-50"

View File

@@ -230,7 +230,7 @@ router.sx (standalone — pure string/list ops)")))
;; Overview pages (Core / Adapters) — show truncated previews of each file ;; Overview pages (Core / Adapters) — show truncated previews of each file
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~spec-overview-content (&key spec-title spec-files) (defcomp ~spec-overview-content (&key (spec-title :as string) (spec-files :as list))
(~doc-page :title (or spec-title "Specs") (~doc-page :title (or spec-title "Specs")
(p :class "text-stone-600 mb-6" (p :class "text-stone-600 mb-6"
(case spec-title (case spec-title
@@ -264,7 +264,7 @@ router.sx (standalone — pure string/list ops)")))
;; Detail page — full source of a single spec file ;; Detail page — full source of a single spec file
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~spec-detail-content (&key spec-title spec-desc spec-filename spec-source spec-prose) (defcomp ~spec-detail-content (&key (spec-title :as string) (spec-desc :as string) (spec-filename :as string) (spec-source :as string) (spec-prose :as string?))
(~doc-page :title spec-title (~doc-page :title spec-title
(div :class "flex items-baseline gap-3 mb-4" (div :class "flex items-baseline gap-3 mb-4"
(span :class "text-sm text-stone-400 font-mono" spec-filename) (span :class "text-sm text-stone-400 font-mono" spec-filename)
@@ -378,7 +378,7 @@ router.sx (standalone — pure string/list ops)")))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; @css bg-green-100 text-green-800 bg-green-50 border-green-200 text-green-700 ;; @css bg-green-100 text-green-800 bg-green-50 border-green-200 text-green-700
(defcomp ~bootstrapper-self-hosting-content (&key py-sx-source g0-output g1-output defines-matched defines-total g0-lines g0-bytes verification-status) (defcomp ~bootstrapper-self-hosting-content (&key (py-sx-source :as string) (g0-output :as string) (g1-output :as string) (defines-matched :as number) (defines-total :as number) (g0-lines :as number) (g0-bytes :as number) (verification-status :as string))
(~doc-page :title "Self-Hosting Bootstrapper (py.sx)" (~doc-page :title "Self-Hosting Bootstrapper (py.sx)"
(div :class "space-y-8" (div :class "space-y-8"
@@ -594,7 +594,7 @@ router.sx (standalone — pure string/list ops)")))
;; Not found ;; Not found
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~spec-not-found (&key slug) (defcomp ~spec-not-found (&key (slug :as string))
(~doc-page :title "Spec Not Found" (~doc-page :title "Spec Not Found"
(p :class "text-stone-600" (p :class "text-stone-600"
"No specification found for \"" slug "\". This spec may not exist yet."))) "No specification found for \"" slug "\". This spec may not exist yet.")))

View File

@@ -22,7 +22,7 @@
;; Generic streamed content chunk — rendered once per yield from the ;; Generic streamed content chunk — rendered once per yield from the
;; async generator. The :content expression receives different bindings ;; async generator. The :content expression receives different bindings
;; each time, and the _stream_id determines which ~suspense slot it fills. ;; each time, and the _stream_id determines which ~suspense slot it fills.
(defcomp ~streaming-demo-chunk (&key stream-label stream-color stream-message stream-time) (defcomp ~streaming-demo-chunk (&key (stream-label :as string) (stream-color :as string) (stream-message :as string) (stream-time :as string))
(let ((colors (get stream-colors stream-color))) (let ((colors (get stream-colors stream-color)))
(div :class (str "rounded-lg border p-5 space-y-3 " (get colors "border") " " (get colors "bg")) (div :class (str "rounded-lg border p-5 space-y-3 " (get colors "border") " " (get colors "bg"))
(div :class "flex items-center gap-2" (div :class "flex items-center gap-2"

View File

@@ -139,7 +139,7 @@ Per-spec platform functions:
;; Per-spec test page (reusable for eval, parser, router, render) ;; Per-spec test page (reusable for eval, parser, router, render)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~testing-spec-content (&key spec-name spec-title spec-desc spec-source framework-source server-results) (defcomp ~testing-spec-content (&key (spec-name :as string) (spec-title :as string) (spec-desc :as string) (spec-source :as string) (framework-source :as string) (server-results :as dict?))
(~doc-page :title spec-title (~doc-page :title spec-title
(div :class "space-y-8" (div :class "space-y-8"