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:
@@ -1,12 +1,12 @@
|
||||
;; Auth page components (device auth — account-specific)
|
||||
;; 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
|
||||
(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4"
|
||||
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"
|
||||
(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.")
|
||||
@@ -29,21 +29,21 @@
|
||||
|
||||
;; 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
|
||||
:error (when error (~auth-error-banner :error error))
|
||||
:action (url-for "auth.start_login")
|
||||
:csrf-token (csrf-token)
|
||||
:email (or email "")))
|
||||
|
||||
(defcomp ~account-device-content (&key error code)
|
||||
(defcomp ~account-device-content (&key (error :as string?) (code :as string?))
|
||||
(~account-device-form
|
||||
:error (when error (~account-device-error :error error))
|
||||
:action (url-for "auth.device_submit")
|
||||
:csrf-token (csrf-token)
|
||||
: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
|
||||
:email (escape (or email ""))
|
||||
:error (when email-error
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
;; Account dashboard components
|
||||
|
||||
(defcomp ~account-error-banner (&key error)
|
||||
(defcomp ~account-error-banner (&key (error :as string))
|
||||
(when error
|
||||
(div :class "rounded-lg border border-red-200 bg-red-50 text-red-800 px-4 py-3 text-sm"
|
||||
error)))
|
||||
|
||||
(defcomp ~account-user-email (&key email)
|
||||
(defcomp ~account-user-email (&key (email :as string))
|
||||
(when 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
|
||||
(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"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf-token)
|
||||
(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"
|
||||
(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"
|
||||
name))
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
labels)))
|
||||
|
||||
;; 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))
|
||||
(csrf (csrf-token)))
|
||||
(~account-main-panel
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
;; Newsletter management components
|
||||
|
||||
(defcomp ~account-newsletter-desc (&key description)
|
||||
(defcomp ~account-newsletter-desc (&key (description :as string))
|
||||
(when 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"
|
||||
(button :sx-post url :sx-headers hdrs :sx-target target :sx-swap "outerHTML"
|
||||
:class cls :role "switch" :aria-checked checked
|
||||
(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 "min-w-0 flex-1"
|
||||
(p :class "text-sm font-medium text-stone-800" name)
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
;; Assembled newsletters content — replaces Python _newsletters_panel_sx
|
||||
;; 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)))
|
||||
(if (empty? newsletter-list)
|
||||
(~account-newsletter-empty)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
;; 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 "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"
|
||||
@@ -19,10 +19,10 @@
|
||||
:sx-headers hx-headers :class "text-sm border border-stone-300 rounded px-2 py-1"
|
||||
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))
|
||||
|
||||
(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-1 min-w-0"
|
||||
(div :class "font-medium truncate" name)
|
||||
@@ -42,7 +42,7 @@
|
||||
(div :id "menu-item-form" :class "mb-6")
|
||||
(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 "text-stone-400 cursor-move" (i :class "fa fa-grip-vertical"))
|
||||
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"
|
||||
: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"
|
||||
icon
|
||||
(div :class "flex-1"
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
;; 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"
|
||||
(input :type "checkbox" :name "tag_ids" :value tag-id :checked checked :class "rounded border-stone-300")
|
||||
img (span name)))
|
||||
@@ -114,7 +114,7 @@
|
||||
(defcomp ~blog-tag-checkbox-image (&key src)
|
||||
(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"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(div :class "space-y-3"
|
||||
@@ -133,7 +133,7 @@
|
||||
(div :class "flex gap-3"
|
||||
(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"
|
||||
:onsubmit "return confirm('Delete this tag group? Tags will not be deleted.')"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
(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)))
|
||||
|
||||
(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"
|
||||
(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 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)))
|
||||
|
||||
;; 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
|
||||
(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)))
|
||||
@@ -45,12 +45,12 @@
|
||||
(span :class "text-stone-700" name)))
|
||||
|
||||
;; Card — accepts pure data
|
||||
(defcomp ~blog-card (&key slug href hx-select title
|
||||
feature-image excerpt
|
||||
status is-draft publish-requested status-timestamp
|
||||
liked like-url csrf-token
|
||||
has-like
|
||||
tags authors widget)
|
||||
(defcomp ~blog-card (&key (slug :as string) (href :as string) (hx-select :as string?) (title :as string)
|
||||
(feature-image :as string?) (excerpt :as string?)
|
||||
status (is-draft :as boolean) (publish-requested :as boolean) (status-timestamp :as string?)
|
||||
(liked :as boolean) (like-url :as string?) (csrf-token :as string?)
|
||||
(has-like :as boolean)
|
||||
(tags :as list?) (authors :as list?) widget)
|
||||
(article :class "border-b pb-6 last:border-b-0 relative"
|
||||
(when has-like
|
||||
(~blog-like-button
|
||||
@@ -80,9 +80,9 @@
|
||||
(ul :class "flex flex-wrap gap-2 text-sm"
|
||||
(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
|
||||
is-draft publish-requested status-timestamp
|
||||
excerpt tags authors)
|
||||
(defcomp ~blog-card-tile (&key (href :as string) (hx-select :as string?) (feature-image :as string?) (title :as string)
|
||||
(is-draft :as boolean) (publish-requested :as boolean) (status-timestamp :as string?)
|
||||
(excerpt :as string?) (tags :as list?) (authors :as list?))
|
||||
(article :class "relative"
|
||||
(a :href href :sx-get href :sx-target "#main-panel"
|
||||
: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))))))))
|
||||
|
||||
;; 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)
|
||||
(if (= view "tile")
|
||||
@@ -131,7 +131,7 @@
|
||||
sentinel))
|
||||
|
||||
;; 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)
|
||||
(~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"
|
||||
(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"
|
||||
(a :href href :sx-get href :sx-target "#main-panel"
|
||||
:sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
;; 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"
|
||||
: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"
|
||||
@@ -20,7 +20,7 @@
|
||||
(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)))
|
||||
|
||||
(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))
|
||||
|
||||
(defcomp ~blog-detail-chrome (&key like excerpt at-bar)
|
||||
@@ -43,10 +43,10 @@
|
||||
;; Data-driven composition — replaces _post_main_panel_sx
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~blog-post-detail-content (&key slug is-draft publish-requested can-edit edit-href
|
||||
is-page has-user liked like-url csrf
|
||||
custom-excerpt tags authors
|
||||
feature-image html-content sx-content)
|
||||
(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 :as boolean) (has-user :as boolean) (liked :as boolean) (like-url :as string?) (csrf :as string?)
|
||||
(custom-excerpt :as string?) (tags :as list?) (authors :as list?)
|
||||
(feature-image :as string?) (html-content :as string?) (sx-content :as string?))
|
||||
(let* ((hx-select "#main-panel")
|
||||
(draft-sx (when is-draft
|
||||
(~blog-detail-draft
|
||||
@@ -70,7 +70,7 @@
|
||||
:html-content html-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)
|
||||
(title page-title)
|
||||
|
||||
@@ -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"
|
||||
(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]"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(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))))
|
||||
|
||||
;; Edit form — pre-populated version for /<slug>/admin/edit/
|
||||
(defcomp ~blog-editor-edit-form (&key csrf updated-at title-val excerpt-val
|
||||
feature-image feature-image-caption
|
||||
sx-content-val lexical-json
|
||||
has-sx title-placeholder
|
||||
status already-emailed
|
||||
(defcomp ~blog-editor-edit-form (&key (csrf :as string) (updated-at :as string) (title-val :as string?) (excerpt-val :as string?)
|
||||
(feature-image :as string?) (feature-image-caption :as string?)
|
||||
(sx-content-val :as string?) (lexical-json :as string?)
|
||||
(has-sx :as boolean) (title-placeholder :as string)
|
||||
(status :as string) (already-emailed :as boolean)
|
||||
newsletter-options footer-extra)
|
||||
(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")
|
||||
@@ -153,14 +153,14 @@
|
||||
" sync();"
|
||||
"})();"))
|
||||
|
||||
(defcomp ~blog-editor-styles (&key css-href)
|
||||
(defcomp ~blog-editor-styles (&key (css-href :as string))
|
||||
(<> (link :rel "stylesheet" :href css-href)
|
||||
(style
|
||||
"#lexical-editor { display: flow-root; }"
|
||||
"#lexical-editor [data-kg-card=\"html\"] * { float: none !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)
|
||||
(when sx-editor-js-src (script :src sx-editor-js-src))
|
||||
(script init-js)))
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
;; 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"
|
||||
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
|
||||
: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"
|
||||
: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 "
|
||||
@@ -61,7 +61,7 @@
|
||||
(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))))
|
||||
|
||||
(defcomp ~blog-filter-summary (&key text)
|
||||
(defcomp ~blog-filter-summary (&key (text :as string))
|
||||
(span :class "text-sm text-stone-600" text))
|
||||
|
||||
;; Data-driven tag groups filter (replaces Python _tag_groups_filter_sx loop)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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"
|
||||
(if (= width "wide") " kg-width-wide"
|
||||
(if (= width "full") " kg-width-full" "")))
|
||||
@@ -19,7 +19,7 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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"
|
||||
(div :class "kg-gallery-container"
|
||||
(map (lambda (row)
|
||||
@@ -48,7 +48,7 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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"
|
||||
(~rich-text :html html)
|
||||
(when caption (figcaption caption))))
|
||||
@@ -56,7 +56,7 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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"
|
||||
(a :class "kg-bookmark-container" :href url
|
||||
(div :class "kg-bookmark-content"
|
||||
@@ -75,7 +75,7 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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"))
|
||||
(when emoji (div :class "kg-callout-emoji" emoji))
|
||||
(div :class "kg-callout-text" (or content ""))))
|
||||
@@ -83,14 +83,14 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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"))
|
||||
(a :href url :class "kg-btn kg-btn-accent" (or text ""))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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-toggle-heading"
|
||||
(h4 :class "kg-toggle-heading-text" (or heading ""))
|
||||
@@ -101,7 +101,7 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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"
|
||||
(if thumbnail
|
||||
(img :src thumbnail :alt "audio-thumbnail" :class "kg-audio-thumbnail")
|
||||
@@ -124,7 +124,7 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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"
|
||||
(if (= width "wide") " kg-width-wide"
|
||||
(if (= width "full") " kg-width-full" "")))
|
||||
@@ -136,7 +136,7 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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"
|
||||
(a :class "kg-file-card-container" :href src :download (or filename "")
|
||||
(div :class "kg-file-card-contents"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
;; 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"
|
||||
:sx-headers {:Content-Type "application/json"} :sx-encoding "json" :class "space-y-3"
|
||||
(label :class "flex items-center gap-3 cursor-pointer"
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
;; 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"
|
||||
(div (span :class "font-medium" name)
|
||||
(span :class "text-stone-400 text-sm ml-2" (str "/" slug "/")))
|
||||
@@ -93,11 +93,11 @@
|
||||
|
||||
;; 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")
|
||||
(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"
|
||||
: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?"
|
||||
@@ -150,7 +150,7 @@
|
||||
;; 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"
|
||||
(summary :class "p-4 cursor-pointer hover:bg-stone-50 flex items-center gap-3"
|
||||
(if image
|
||||
@@ -182,11 +182,11 @@
|
||||
;; 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
|
||||
: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
|
||||
(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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
;; 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"
|
||||
(div (div :class "font-medium" name)
|
||||
(div :class "text-xs text-stone-500" date-str))
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
;; 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"))
|
||||
|
||||
(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))
|
||||
|
||||
(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))
|
||||
|
||||
(defcomp ~cart-item-no-price ()
|
||||
@@ -17,13 +17,13 @@
|
||||
(i :class "fa-solid fa-triangle-exclamation text-[0.6rem]" :aria-hidden "true")
|
||||
" 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))
|
||||
|
||||
(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))
|
||||
|
||||
(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"
|
||||
(div :class "w-full sm:w-32 shrink-0 flex justify-center sm:block" (when img img))
|
||||
(div :class "flex-1 min-w-0"
|
||||
@@ -54,7 +54,7 @@
|
||||
summary))))
|
||||
|
||||
;; 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") ""))
|
||||
(title (or (get item "title") ""))
|
||||
(image (get item "image"))
|
||||
@@ -96,7 +96,7 @@
|
||||
(~cart-item-line-total :text (str "Line total: " symbol (format-decimal line-total 2)))))))
|
||||
|
||||
;; 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))
|
||||
(~cart-cal-section
|
||||
:items (map (lambda (e)
|
||||
@@ -108,7 +108,7 @@
|
||||
entries))))
|
||||
|
||||
;; 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))
|
||||
(let* ((csrf (csrf-token))
|
||||
(qty-url (url-for "cart_global.update_ticket_quantity")))
|
||||
@@ -137,7 +137,7 @@
|
||||
ticket-groups)))))
|
||||
|
||||
;; 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
|
||||
:item-count (str item-count)
|
||||
:subtotal (str symbol (format-decimal grand-total 2))
|
||||
@@ -148,7 +148,7 @@
|
||||
(~cart-checkout-signin :href login-href))))
|
||||
|
||||
;; 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)))
|
||||
(empty? (or cal-entries (list)))
|
||||
(empty? (or ticket-groups (list))))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
;; 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"
|
||||
(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"
|
||||
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"))
|
||||
|
||||
(defcomp ~cart-mp-subtitle (&key title)
|
||||
(defcomp ~cart-mp-subtitle (&key (title :as string))
|
||||
(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"
|
||||
(div :class "flex items-start gap-4"
|
||||
img
|
||||
@@ -25,7 +25,7 @@
|
||||
(div :class "text-lg font-bold text-stone-900" total)
|
||||
(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 "flex items-start gap-4"
|
||||
(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"))))
|
||||
|
||||
;; 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"))
|
||||
(product-count (or (get grp "product_count") 0))
|
||||
(calendar-count (or (get grp "calendar_count") 0))
|
||||
@@ -85,7 +85,7 @@
|
||||
:total (str "\u00a3" (format-decimal total 2))))))
|
||||
|
||||
;; 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)
|
||||
(~cart-empty)
|
||||
(~cart-overview-panel
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
;; 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"
|
||||
(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"
|
||||
(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"
|
||||
(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"))))
|
||||
|
||||
(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"
|
||||
(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")
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
;; 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))
|
||||
|
||||
(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))
|
||||
|
||||
(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"
|
||||
(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"
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
;; 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
|
||||
: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)))
|
||||
|
||||
(defcomp ~events-calendar-weekday (&key name)
|
||||
(defcomp ~events-calendar-weekday (&key (name :as string))
|
||||
(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))
|
||||
|
||||
(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"
|
||||
: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)
|
||||
(span :class "truncate" name)
|
||||
(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 "flex justify-between items-center"
|
||||
(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))))
|
||||
|
||||
;; Calendar grid from data — all iteration in sx
|
||||
(defcomp ~events-calendar-grid-from-data (&key pill-cls month-name year
|
||||
prev-year-href prev-month-href
|
||||
next-month-href next-year-href
|
||||
weekday-names cells)
|
||||
(defcomp ~events-calendar-grid-from-data (&key (pill-cls :as string) (month-name :as string) (year :as string)
|
||||
(prev-year-href :as string) (prev-month-href :as string)
|
||||
(next-month-href :as string) (next-year-href :as string)
|
||||
(weekday-names :as list) (cells :as list))
|
||||
(~events-calendar-grid
|
||||
:arrows (<>
|
||||
(~events-calendar-nav-arrow :pill-cls pill-cls :href prev-year-href :label "\u00ab")
|
||||
@@ -66,7 +66,7 @@
|
||||
(get cell "badges"))))))
|
||||
(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"
|
||||
(if 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"
|
||||
(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"
|
||||
:class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
|
||||
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"
|
||||
(form :sx-post save-url :sx-target "#calendar-description" :sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
;; 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"
|
||||
(div :class "flex-1 min-w-0"
|
||||
(div :class "font-medium truncate" name)
|
||||
@@ -12,7 +12,7 @@
|
||||
(div :class "flex overflow-x-auto gap-1 scrollbar-thin"
|
||||
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
|
||||
(table :class "w-full text-sm border table-fixed"
|
||||
(thead :class "bg-stone-100"
|
||||
@@ -32,27 +32,27 @@
|
||||
(defcomp ~events-day-empty-row ()
|
||||
(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"
|
||||
(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))))
|
||||
|
||||
(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"
|
||||
(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)
|
||||
(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))))
|
||||
|
||||
(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)))
|
||||
|
||||
(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)))
|
||||
|
||||
(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"
|
||||
(div :class "font-medium text-green-600" price-str)
|
||||
(div :class "text-stone-600" count-str))))
|
||||
@@ -63,7 +63,7 @@
|
||||
(defcomp ~events-day-row-actions ()
|
||||
(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))
|
||||
|
||||
(defcomp ~events-day-admin-panel ()
|
||||
@@ -77,14 +77,14 @@
|
||||
:id "day-entries-nav-wrapper" :sx-swap-oob "true"
|
||||
(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
|
||||
(div :class "flex-1 min-w-0"
|
||||
(div :class "font-medium truncate" name)
|
||||
(div :class "text-xs text-stone-600 truncate" time-str))))
|
||||
|
||||
;; 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
|
||||
:list-container list-container
|
||||
:rows (if (empty? (or rows (list)))
|
||||
@@ -112,7 +112,7 @@
|
||||
:pre-action pre-action :add-url add-url))
|
||||
|
||||
;; 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)))
|
||||
(~events-day-entries-nav-oob-empty)
|
||||
(~events-day-entries-nav-oob
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
;; 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"
|
||||
(div :class "flex items-start justify-between gap-4"
|
||||
(div :class "flex-1 min-w-0"
|
||||
@@ -12,7 +12,7 @@
|
||||
badge
|
||||
(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
|
||||
(h1 :class "text-2xl font-bold mb-6" "My Tickets")
|
||||
(if has-tickets
|
||||
@@ -22,9 +22,9 @@
|
||||
(p :class "text-lg" "No tickets yet")
|
||||
(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
|
||||
type-name code time-date time-range cal-name
|
||||
type-desc checkin-str qr-script)
|
||||
(defcomp ~events-ticket-detail (&key (list-container :as string) (back-href :as string) (header-bg :as string) (entry-name :as string) badge
|
||||
(type-name :as string?) (code :as string) (time-date :as string?) (time-range :as string?) (cal-name :as string?)
|
||||
(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")
|
||||
(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")
|
||||
@@ -54,25 +54,25 @@
|
||||
(script :src "https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js")
|
||||
(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 "text-2xl font-bold " text-cls) value)
|
||||
(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))
|
||||
|
||||
(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"
|
||||
(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"
|
||||
(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"
|
||||
(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)
|
||||
(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)
|
||||
@@ -80,7 +80,7 @@
|
||||
(td :class "px-4 py-3" badge)
|
||||
(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
|
||||
(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)
|
||||
@@ -113,11 +113,11 @@
|
||||
(tbody :class "divide-y divide-stone-100" rows))
|
||||
(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"
|
||||
(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)
|
||||
(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)
|
||||
@@ -127,29 +127,29 @@
|
||||
(span :class "text-xs text-blue-600"
|
||||
(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"
|
||||
(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))
|
||||
|
||||
(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))
|
||||
|
||||
(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))
|
||||
|
||||
(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))
|
||||
|
||||
(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)))
|
||||
|
||||
(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)))
|
||||
|
||||
(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"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(button :type "submit"
|
||||
@@ -166,20 +166,20 @@
|
||||
(i :class "fa fa-times-circle text-3xl" :aria-hidden "true")
|
||||
(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 "flex items-start justify-between gap-4"
|
||||
(div :class "flex-1" info)
|
||||
(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)
|
||||
(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" badge)
|
||||
(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"
|
||||
(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"
|
||||
@@ -198,7 +198,7 @@
|
||||
(defcomp ~events-entry-tickets-admin-empty ()
|
||||
(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 "flex items-center justify-between"
|
||||
(h3 :class "text-lg font-semibold" (str "Tickets for: " entry-name))
|
||||
@@ -211,7 +211,7 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; 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
|
||||
:list-container list-container
|
||||
:has-tickets (not (empty? (or tickets (list))))
|
||||
@@ -225,9 +225,9 @@
|
||||
(or tickets (list))))))
|
||||
|
||||
;; Ticket detail from data — uses lg badge variant
|
||||
(defcomp ~events-ticket-detail-from-data (&key list-container back-href header-bg entry-name
|
||||
state type-name code time-date time-range
|
||||
cal-name type-desc checkin-str qr-script)
|
||||
(defcomp ~events-ticket-detail-from-data (&key (list-container :as string) (back-href :as string) (header-bg :as string) (entry-name :as string)
|
||||
(state :as string) (type-name :as string?) (code :as string) (time-date :as string?) (time-range :as string?)
|
||||
(cal-name :as string?) (type-desc :as string?) (checkin-str :as string?) (qr-script :as string))
|
||||
(~events-ticket-detail
|
||||
:list-container list-container :back-href back-href
|
||||
:header-bg header-bg :entry-name entry-name
|
||||
@@ -238,9 +238,9 @@
|
||||
:checkin-str checkin-str :qr-script qr-script))
|
||||
|
||||
;; Ticket admin row from data — conditional action column
|
||||
(defcomp ~events-ticket-admin-row-from-data (&key code code-short entry-name date-str
|
||||
type-name state checkin-url csrf
|
||||
checked-in-time)
|
||||
(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 :as string) (state :as string) (checkin-url :as string) (csrf :as string)
|
||||
(checked-in-time :as string?))
|
||||
(~events-ticket-admin-row
|
||||
:code code :code-short code-short
|
||||
:entry-name entry-name
|
||||
@@ -256,8 +256,8 @@
|
||||
(true nil))))
|
||||
|
||||
;; Ticket admin panel from data
|
||||
(defcomp ~events-ticket-admin-panel-from-data (&key list-container lookup-url tickets
|
||||
total confirmed checked-in reserved)
|
||||
(defcomp ~events-ticket-admin-panel-from-data (&key (list-container :as string) (lookup-url :as string) (tickets :as list?)
|
||||
(total :as number?) (confirmed :as number?) (checked-in :as number?) (reserved :as number?))
|
||||
(~events-ticket-admin-panel
|
||||
:list-container list-container
|
||||
:stats (<>
|
||||
@@ -285,7 +285,7 @@
|
||||
(or tickets (list))))))
|
||||
|
||||
;; 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
|
||||
:entry-name entry-name :count-label count-label
|
||||
:body (if (empty? (or tickets (list)))
|
||||
@@ -306,7 +306,7 @@
|
||||
(or tickets (list))))))))
|
||||
|
||||
;; 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
|
||||
:code code :code-short code-short
|
||||
:entry-name entry-name
|
||||
@@ -316,8 +316,8 @@
|
||||
:time-str time-str))
|
||||
|
||||
;; Ticket types table from data
|
||||
(defcomp ~events-ticket-types-table-from-data (&key list-container ticket-types action-btn add-url
|
||||
tr-cls pill-cls hx-select csrf-hdr)
|
||||
(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 :as string) (pill-cls :as string) (hx-select :as string) (csrf-hdr :as string))
|
||||
(~events-ticket-types-table
|
||||
:list-container list-container
|
||||
:rows (if (empty? (or ticket-types (list)))
|
||||
@@ -333,9 +333,9 @@
|
||||
:action-btn action-btn :add-url add-url))
|
||||
|
||||
;; Lookup result from data
|
||||
(defcomp ~events-lookup-result-from-data (&key entry-name type-name date-str cal-name
|
||||
state code checked-in-str
|
||||
checkin-url csrf)
|
||||
(defcomp ~events-lookup-result-from-data (&key (entry-name :as string) (type-name :as string?) (date-str :as string?) (cal-name :as string?)
|
||||
(state :as string) (code :as string) (checked-in-str :as string?)
|
||||
(checkin-url :as string) (csrf :as string))
|
||||
(~events-lookup-card
|
||||
:info (<>
|
||||
(~events-lookup-info :entry-name entry-name)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
;; 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))
|
||||
|
||||
(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 "flex items-start gap-3"
|
||||
avatar
|
||||
@@ -15,14 +15,14 @@
|
||||
preview
|
||||
(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))
|
||||
|
||||
(defcomp ~federation-notifications-page (&key notifs)
|
||||
(h1 :class "text-2xl font-bold mb-6" "Notifications") notifs)
|
||||
|
||||
;; 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") "?"))
|
||||
(from-username (or (get notif "from_actor_username") ""))
|
||||
(from-domain (or (get notif "from_actor_domain") ""))
|
||||
@@ -59,7 +59,7 @@
|
||||
:time created)))
|
||||
|
||||
;; 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
|
||||
:notifs (if (empty? notifications)
|
||||
(~empty-state :message "No notifications yet." :cls "text-stone-500")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
;; 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 "flex items-center gap-4"
|
||||
avatar
|
||||
@@ -14,35 +14,35 @@
|
||||
header
|
||||
(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"
|
||||
(form :method "post" :action action
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(input :type "hidden" :name "actor_url" :value actor-url)
|
||||
(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)))
|
||||
|
||||
;; 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))
|
||||
|
||||
(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 "flex justify-between items-start"
|
||||
(span :class "font-medium" activity-type)
|
||||
(span :class "text-sm text-stone-400" published))
|
||||
obj-type))
|
||||
|
||||
(defcomp ~federation-activities-list (&key items)
|
||||
(defcomp ~federation-activities-list (&key (items :as list))
|
||||
(div :class "space-y-4" items))
|
||||
|
||||
(defcomp ~federation-activities-empty ()
|
||||
(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 "bg-white rounded-lg shadow p-6 mb-6"
|
||||
(h1 :class "text-2xl font-bold" display-name)
|
||||
@@ -51,11 +51,11 @@
|
||||
(h2 :class "text-xl font-bold mb-4" activities-heading)
|
||||
activities))
|
||||
|
||||
(defcomp ~federation-profile-summary-text (&key text)
|
||||
(defcomp ~federation-profile-summary-text (&key (text :as string))
|
||||
(p :class "mt-2" text))
|
||||
|
||||
;; 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") ""))
|
||||
(icon-url (get remote-actor "icon_url"))
|
||||
(summary (get remote-actor "summary"))
|
||||
@@ -92,7 +92,7 @@
|
||||
:before (get (last items) "before_cursor")))))))
|
||||
|
||||
;; 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)))
|
||||
(~federation-activities-empty)
|
||||
(~federation-activities-list
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
;; Search and actor card components
|
||||
|
||||
;; 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))
|
||||
|
||||
(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))
|
||||
|
||||
(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))
|
||||
|
||||
(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"
|
||||
: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)))
|
||||
|
||||
(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"
|
||||
(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 "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"))))
|
||||
|
||||
(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"
|
||||
(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 "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))))
|
||||
|
||||
(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
|
||||
avatar
|
||||
(div :class "flex-1 min-w-0"
|
||||
@@ -41,7 +41,7 @@
|
||||
button))
|
||||
|
||||
;; 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"))
|
||||
(display-name (get d "display_name"))
|
||||
(username (get d "username"))
|
||||
@@ -72,8 +72,8 @@
|
||||
:summary summary-sx :button button)))
|
||||
|
||||
;; 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
|
||||
follow-url unfollow-url list-type)
|
||||
(defcomp ~federation-actor-list-from-data (&key (actors :as list) (next-url :as string?) (has-actor :as boolean) (csrf :as string)
|
||||
(follow-url :as string) (unfollow-url :as string) (list-type :as string))
|
||||
(<>
|
||||
(map (lambda (d)
|
||||
(~federation-actor-card-from-data :d d :has-actor has-actor :csrf csrf
|
||||
@@ -81,10 +81,10 @@
|
||||
(or actors (list)))
|
||||
(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))
|
||||
|
||||
(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")
|
||||
(form :method "get" :action search-url :class "mb-6"
|
||||
:sx-get search-page-url :sx-target "#search-results" :sx-push-url search-url
|
||||
@@ -97,7 +97,7 @@
|
||||
(div :id "search-results" results))
|
||||
|
||||
;; 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 " "
|
||||
(span :class "text-stone-400 font-normal" count-str))
|
||||
(div :id "actor-list" items))
|
||||
@@ -106,7 +106,7 @@
|
||||
;; 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") ""))
|
||||
(username (or (get a "preferred_username") ""))
|
||||
(domain (or (get a "domain") ""))
|
||||
@@ -146,7 +146,7 @@
|
||||
:label (if (= list-type "followers") "Follow Back" "Follow")))))))
|
||||
|
||||
;; 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
|
||||
:search-url (url-for "social.defpage_search")
|
||||
:search-page-url (url-for "social.search_page")
|
||||
@@ -172,7 +172,7 @@
|
||||
:url (url-for "social.search_page" :q query :page 2)))))))
|
||||
|
||||
;; 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
|
||||
:title "Following" :count-str (str "(" total ")")
|
||||
:items (when (not (empty? actors))
|
||||
@@ -185,7 +185,7 @@
|
||||
(~federation-scroll-sentinel
|
||||
: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
|
||||
:title "Followers" :count-str (str "(" total ")")
|
||||
:items (when (not (empty? actors))
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
;; --- 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"
|
||||
(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"
|
||||
(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")))
|
||||
@@ -20,28 +20,28 @@
|
||||
|
||||
;; --- 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))
|
||||
|
||||
;; 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))
|
||||
|
||||
(defcomp ~federation-avatar-placeholder (&key cls initial)
|
||||
(defcomp ~federation-avatar-placeholder (&key (cls :as string) (initial :as string))
|
||||
(~avatar :cls cls :initial initial))
|
||||
|
||||
(defcomp ~federation-content (&key content summary)
|
||||
(defcomp ~federation-content (&key (content :as string) (summary :as string?))
|
||||
(if summary
|
||||
(details :class "mt-2"
|
||||
(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))))
|
||||
|
||||
(defcomp ~federation-original-link (&key url)
|
||||
(defcomp ~federation-original-link (&key (url :as string))
|
||||
(a :href url :target "_blank" :rel "noopener"
|
||||
: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"
|
||||
boost
|
||||
(div :class "flex items-start gap-3"
|
||||
@@ -55,17 +55,17 @@
|
||||
|
||||
;; --- 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"))
|
||||
|
||||
(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"
|
||||
(input :type "hidden" :name "object_id" :value oid)
|
||||
(input :type "hidden" :name "author_inbox" :value ainbox)
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(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"
|
||||
(input :type "hidden" :name "object_id" :value oid)
|
||||
(input :type "hidden" :name "author_inbox" :value ainbox)
|
||||
@@ -78,13 +78,13 @@
|
||||
|
||||
;; --- 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"))
|
||||
|
||||
(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"))
|
||||
|
||||
(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"
|
||||
(h1 :class "text-2xl font-bold" label " Timeline")
|
||||
compose)
|
||||
@@ -92,9 +92,9 @@
|
||||
|
||||
;; --- Data-driven post card (replaces Python _post_card_sx loop) ---
|
||||
|
||||
(defcomp ~federation-post-card-from-data (&key d has-actor csrf
|
||||
like-url unlike-url
|
||||
boost-url unboost-url)
|
||||
(defcomp ~federation-post-card-from-data (&key (d :as dict) (has-actor :as boolean) (csrf :as string)
|
||||
(like-url :as string) (unlike-url :as string)
|
||||
(boost-url :as string) (unboost-url :as string))
|
||||
(let* ((boosted-by (get d "boosted_by"))
|
||||
(actor-icon (get d "actor_icon"))
|
||||
(actor-name (get d "actor_name"))
|
||||
@@ -140,8 +140,8 @@
|
||||
:interactions interactions)))
|
||||
|
||||
;; Data-driven timeline items (replaces Python _timeline_items_sx loop)
|
||||
(defcomp ~federation-timeline-items-from-data (&key items next-url has-actor csrf
|
||||
like-url unlike-url boost-url unboost-url)
|
||||
(defcomp ~federation-timeline-items-from-data (&key (items :as list) (next-url :as string?) (has-actor :as boolean) (csrf :as string)
|
||||
(like-url :as string) (unlike-url :as string) (boost-url :as string) (unboost-url :as string))
|
||||
(<>
|
||||
(map (lambda (d)
|
||||
(~federation-post-card-from-data :d d :has-actor has-actor :csrf csrf
|
||||
@@ -151,11 +151,11 @@
|
||||
|
||||
;; --- 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)
|
||||
(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")
|
||||
(form :method "post" :action action :class "space-y-4"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
@@ -208,7 +208,7 @@
|
||||
;; 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"))
|
||||
(actor-icon (get item "actor_icon"))
|
||||
(actor-name (or (get item "actor_name") "?"))
|
||||
@@ -267,7 +267,7 @@
|
||||
;; 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)
|
||||
(~federation-post-card-from-data :item item :actor actor))
|
||||
@@ -276,7 +276,7 @@
|
||||
(~federation-scroll-sentinel :url next-url))))
|
||||
|
||||
;; 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")))
|
||||
(~federation-timeline-page
|
||||
:label label
|
||||
@@ -289,7 +289,7 @@
|
||||
:before (get (last items) "before_cursor")))))))
|
||||
|
||||
;; 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
|
||||
:action (url-for "social.compose_submit")
|
||||
:csrf (csrf-token)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
;; 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 ""
|
||||
: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"
|
||||
(figure :class "inline-block 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)))
|
||||
(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 "p-2 flex flex-col items-center justify-center gap-2 text-red-500 h-full relative"
|
||||
(div :class "text-stone-400 text-xs" "No image")
|
||||
(when labels (ul :class "flex flex-row gap-1" (map (lambda (l) (li l)) labels)))
|
||||
(div :class "text-stone-900 text-center line-clamp-3 break-words [overflow-wrap:anywhere]" brand))))
|
||||
|
||||
(defcomp ~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)))
|
||||
|
||||
(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"
|
||||
(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))
|
||||
|
||||
;; 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))
|
||||
|
||||
;; Main product card — accepts pure data, composes sub-components
|
||||
(defcomp ~market-product-card (&key href hx-select
|
||||
has-like liked slug csrf like-action
|
||||
image labels brand brand-highlight
|
||||
special-price regular-price
|
||||
cart-action quantity cart-href
|
||||
stickers
|
||||
title has-highlight search-pre search-mid search-post)
|
||||
(defcomp ~market-product-card (&key (href :as string) (hx-select :as string)
|
||||
(has-like :as boolean) (liked :as boolean?) (slug :as string) (csrf :as string) (like-action :as string?)
|
||||
(image :as string?) (labels :as list?) (brand :as string) (brand-highlight :as string?)
|
||||
(special-price :as string?) (regular-price :as string?)
|
||||
(cart-action :as string) (quantity :as number?) (cart-href :as string)
|
||||
(stickers :as list?)
|
||||
(title :as string) (has-highlight :as boolean) (search-pre :as string?) (search-mid :as string?) (search-post :as string?))
|
||||
(div :class "flex flex-col rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden relative"
|
||||
(when has-like
|
||||
(~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)
|
||||
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"
|
||||
(form :id form-id :action action :method "post"
|
||||
:sx-post action :sx-target (str "#like-" slug) :sx-swap "outerHTML"
|
||||
@@ -73,22 +73,22 @@
|
||||
(button :type "submit" :class "cursor-pointer"
|
||||
(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"
|
||||
(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))
|
||||
|
||||
(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))
|
||||
|
||||
(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"
|
||||
(a :href href :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200"
|
||||
title)))
|
||||
|
||||
(defcomp ~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"
|
||||
(div
|
||||
(if title-content title-content (when title title))
|
||||
@@ -101,8 +101,8 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Product cards grid with infinite scroll sentinels
|
||||
(defcomp ~market-product-cards-content (&key products page total-pages next-url
|
||||
mobile-sentinel-hs desktop-sentinel-hs)
|
||||
(defcomp ~market-product-cards-content (&key (products :as list) (page :as number) (total-pages :as number) (next-url :as string)
|
||||
(mobile-sentinel-hs :as string?) (desktop-sentinel-hs :as string?))
|
||||
(<>
|
||||
(map (lambda (p)
|
||||
(~market-product-card
|
||||
@@ -126,7 +126,7 @@
|
||||
(~end-of-results))))
|
||||
|
||||
;; Single market card from data (handles conditional title/desc/badge)
|
||||
(defcomp ~market-card-from-data (&key name description href show-badge badge-href badge-title)
|
||||
(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
|
||||
:title-content (if href
|
||||
(~market-market-card-title-link :href href :name name)
|
||||
@@ -137,7 +137,7 @@
|
||||
(~market-market-card-badge :href badge-href :title badge-title))))
|
||||
|
||||
;; Market cards list with infinite scroll sentinel
|
||||
(defcomp ~market-cards-content (&key markets page has-more next-url)
|
||||
(defcomp ~market-cards-content (&key (markets :as list) (page :as number) (has-more :as boolean) (next-url :as string))
|
||||
(<>
|
||||
(map (lambda (m)
|
||||
(~market-card-from-data
|
||||
@@ -149,7 +149,7 @@
|
||||
(~sentinel-simple :id (str "sentinel-" page) :next-url next-url))))
|
||||
|
||||
;; Market landing page content from data
|
||||
(defcomp ~market-landing-from-data (&key excerpt feature-image html)
|
||||
(defcomp ~market-landing-from-data (&key (excerpt :as string?) (feature-image :as string?) (html :as string?))
|
||||
(~market-landing-content :inner
|
||||
(<> (when excerpt (~market-landing-excerpt :text excerpt))
|
||||
(when feature-image (~market-landing-image :src feature-image))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
;; 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
|
||||
(figure :class "inline-block"
|
||||
(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"
|
||||
: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"
|
||||
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 ""
|
||||
:class "shrink-0 rounded-lg overflow-hidden bg-stone-100 hover:opacity-90 ring-offset-2"
|
||||
:title title
|
||||
(img :src src :class "h-16 w-16 object-contain" :alt alt :loading "lazy" :decoding "async"))
|
||||
(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 "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"
|
||||
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"))
|
||||
|
||||
(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))
|
||||
|
||||
(defcomp ~market-detail-unit-price (&key price)
|
||||
(defcomp ~market-detail-unit-price (&key (price :as string))
|
||||
(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)))
|
||||
|
||||
(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))
|
||||
|
||||
(defcomp ~market-detail-desc-short (&key text)
|
||||
(defcomp ~market-detail-desc-short (&key (text :as string))
|
||||
(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)))
|
||||
|
||||
(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))
|
||||
|
||||
(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"
|
||||
(summary :class "cursor-pointer select-none px-4 py-3 flex items-center justify-between"
|
||||
(span :class "font-medium" title)
|
||||
(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))))
|
||||
|
||||
(defcomp ~market-detail-sections (&key items)
|
||||
(defcomp ~market-detail-sections (&key (items :as list))
|
||||
(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))
|
||||
|
||||
(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 "md:col-span-2" gallery stickers)
|
||||
details)
|
||||
(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))
|
||||
|
||||
(defcomp ~market-landing-image (&key src)
|
||||
(defcomp ~market-landing-image (&key (src :as string))
|
||||
(div :class "mb-3 flex justify-center"
|
||||
(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)))
|
||||
|
||||
(defcomp ~market-landing-content (&key inner)
|
||||
(defcomp ~market-landing-content (&key (inner :as list))
|
||||
(<> (article :class "relative w-full" inner) (div :class "pb-8")))
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; 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
|
||||
(~market-like-button
|
||||
:form-id (get like-data "form-id") :action (get like-data "action")
|
||||
@@ -124,7 +124,7 @@
|
||||
(~market-detail-no-image :like like-sx))))
|
||||
|
||||
;; Right column details from data
|
||||
(defcomp ~market-detail-info-from-data (&key extras desc-short desc-html sections)
|
||||
(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
|
||||
(<>
|
||||
(when extras
|
||||
@@ -145,9 +145,9 @@
|
||||
sections)))))))
|
||||
|
||||
;; Full product detail layout from data
|
||||
(defcomp ~market-product-detail-from-data (&key images labels brand like-data
|
||||
has-nav-buttons thumbs sticker-items
|
||||
extras desc-short desc-html sections)
|
||||
(defcomp ~market-product-detail-from-data (&key (images :as list?) (labels :as list?) (brand :as string) (like-data :as dict?)
|
||||
(has-nav-buttons :as boolean) (thumbs :as list?) (sticker-items :as list?)
|
||||
(extras :as list?) (desc-short :as string?) (desc-html :as string?) (sections :as list?))
|
||||
(~market-detail-layout
|
||||
:gallery (~market-detail-gallery-from-data
|
||||
:images images :labels labels :brand brand :like-data like-data
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
;; Market meta/SEO components
|
||||
|
||||
(defcomp ~market-meta-title (&key title)
|
||||
(defcomp ~market-meta-title (&key (title :as string))
|
||||
(title title))
|
||||
|
||||
(defcomp ~market-meta-description (&key description)
|
||||
(defcomp ~market-meta-description (&key (description :as string))
|
||||
(meta :name "description" :content description))
|
||||
|
||||
(defcomp ~market-meta-canonical (&key href)
|
||||
(defcomp ~market-meta-canonical (&key (href :as string))
|
||||
(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))
|
||||
|
||||
(defcomp ~market-meta-twitter (&key name content)
|
||||
(defcomp ~market-meta-twitter (&key (name :as string) (content :as string))
|
||||
(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)))
|
||||
|
||||
|
||||
@@ -23,9 +23,10 @@
|
||||
;; Composition: all product meta tags from data
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~market-product-meta-from-data (&key title description canonical image-url
|
||||
site-title brand price price-currency
|
||||
jsonld-json)
|
||||
(defcomp ~market-product-meta-from-data (&key (title :as string) (description :as string) (canonical :as string?)
|
||||
(image-url :as string?)
|
||||
(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-description :description description)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
;; 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"
|
||||
(a :href href :sx-get href :sx-target "#main-panel"
|
||||
: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)
|
||||
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"
|
||||
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)))
|
||||
|
||||
(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"
|
||||
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
|
||||
:aria-selected (if active "true" "false")
|
||||
@@ -28,7 +28,7 @@
|
||||
(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")))
|
||||
|
||||
(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)
|
||||
(a :href href :sx-get href :sx-target "#main-panel"
|
||||
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
|
||||
@@ -37,7 +37,7 @@
|
||||
(div :aria-label count-label count-str))
|
||||
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")
|
||||
:aria-selected (if active "true" "false")
|
||||
:href href :sx-get href :sx-target "#main-panel"
|
||||
@@ -45,20 +45,20 @@
|
||||
(div label)
|
||||
(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 :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"
|
||||
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"
|
||||
(a :class "px-2 py-1 rounded hover:bg-stone-100 block"
|
||||
:href href :sx-get href :sx-target "#main-panel"
|
||||
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
|
||||
"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
|
||||
summary subs))
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
;; 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-all-link :href all-href :hx-select hx-select
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
;; 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))
|
||||
|
||||
(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))
|
||||
|
||||
(defcomp ~market-price-regular (&key price)
|
||||
(defcomp ~market-price-regular (&key (price :as string))
|
||||
(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))
|
||||
|
||||
(defcomp ~market-header-price-special-label ()
|
||||
(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))
|
||||
|
||||
(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))
|
||||
|
||||
(defcomp ~market-header-price-regular-label ()
|
||||
(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))
|
||||
|
||||
(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)))
|
||||
|
||||
(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))
|
||||
|
||||
|
||||
@@ -38,8 +38,9 @@
|
||||
;; Composition: prices header + cart button from data
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~market-prices-header-from-data (&key cart-id cart-action csrf quantity cart-href
|
||||
sp-val sp-str rp-val rp-str rrp-str)
|
||||
(defcomp ~market-prices-header-from-data (&key (cart-id :as string) (cart-action :as string) (csrf :as string) (quantity :as number?)
|
||||
(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
|
||||
(<>
|
||||
(if quantity
|
||||
@@ -57,7 +58,7 @@
|
||||
(when rrp-str (~market-header-rrp :rrp rrp-str)))))
|
||||
|
||||
;; Card price line from data (used in product cards)
|
||||
(defcomp ~market-card-price-from-data (&key sp-val sp-str rp-val rp-str)
|
||||
(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
|
||||
(<>
|
||||
(when sp-val
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
;; 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"
|
||||
(div :class "space-y-1"
|
||||
(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"
|
||||
"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"
|
||||
(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 "
|
||||
@@ -32,7 +32,7 @@
|
||||
(p :class "font-medium" "All done!")
|
||||
(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"
|
||||
(div
|
||||
(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)))
|
||||
|
||||
;; 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
|
||||
:items (<> (map (lambda (tk)
|
||||
(~checkout-return-ticket
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
;; --- 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)
|
||||
(~header-child-sx
|
||||
:inner (<> (~auth-header-row-auto)
|
||||
(~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)
|
||||
(~oob-header-sx
|
||||
:parent-id "auth-header-child"
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
;; --- 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)
|
||||
(~order-detail-header-stack
|
||||
:auth (~auth-header-row-auto)
|
||||
@@ -30,7 +30,7 @@
|
||||
:link-href (or detail-url "/") :link-label "Order"
|
||||
: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
|
||||
:parent-id "orders-header-child"
|
||||
:row (~menu-row-sx :id "order-row" :level 3 :colour "sky"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
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 isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(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))
|
||||
(children (list)))
|
||||
(async-parse-kw-args args kwargs children env ctx)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-to-dom
|
||||
(fn (expr (env :as dict) ns)
|
||||
(fn (expr (env :as dict) (ns :as string))
|
||||
(set-render-active! true)
|
||||
(case (type-of expr)
|
||||
;; nil / boolean false / boolean true → empty fragment
|
||||
@@ -67,7 +67,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define render-dom-list
|
||||
(fn (expr (env :as dict) ns)
|
||||
(fn (expr (env :as dict) (ns :as string))
|
||||
(let ((head (first expr)))
|
||||
(cond
|
||||
;; Symbol head — dispatch on name
|
||||
@@ -166,7 +166,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(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
|
||||
(let ((new-ns (cond (= tag "svg") SVG_NS
|
||||
(= tag "math") MATH_NS
|
||||
@@ -237,7 +237,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(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.
|
||||
(let ((kwargs (dict))
|
||||
(children (list)))
|
||||
@@ -284,7 +284,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(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)))
|
||||
(for-each
|
||||
(fn (x) (dom-append frag (render-to-dom x env ns)))
|
||||
@@ -339,7 +339,7 @@
|
||||
(contains? RENDER_DOM_FORMS name)))
|
||||
|
||||
(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
|
||||
;; if — reactive inside islands (re-renders when signal deps change)
|
||||
(= name "if")
|
||||
@@ -581,7 +581,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(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
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(for-each-indexed
|
||||
@@ -605,7 +605,7 @@
|
||||
;; - Conditional fragments: (when (deref sig) ...) → reactive show/hide
|
||||
|
||||
(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)
|
||||
(let ((kwargs (dict))
|
||||
(children (list)))
|
||||
@@ -679,7 +679,7 @@
|
||||
;; Supports :tag keyword to change wrapper element (default "div").
|
||||
|
||||
(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)
|
||||
(lake-tag "div")
|
||||
(children (list)))
|
||||
@@ -723,7 +723,7 @@
|
||||
;; Stores the island env and transform on the element for morph retrieval.
|
||||
|
||||
(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)
|
||||
(marsh-tag "div")
|
||||
(marsh-transform nil)
|
||||
@@ -781,7 +781,7 @@
|
||||
;; Marks the attribute name on the element via data-sx-reactive-attrs so
|
||||
;; the morph algorithm knows not to overwrite it with server content.
|
||||
(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
|
||||
(let ((existing (or (dom-get-attr el "data-sx-reactive-attrs") ""))
|
||||
(updated (if (empty? existing) attr-name (str existing "," attr-name))))
|
||||
@@ -802,7 +802,7 @@
|
||||
;; reactive-fragment — conditionally render a fragment based on a signal
|
||||
;; Used for (when (deref sig) ...) or (if (deref sig) ...) inside an island.
|
||||
(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"))
|
||||
(current-nodes (list)))
|
||||
(effect (fn ()
|
||||
@@ -824,7 +824,7 @@
|
||||
;; and reorderings touch the DOM. Without keys, falls back to clear+rerender.
|
||||
|
||||
(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)
|
||||
(render-lambda-dom 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)))))))
|
||||
|
||||
(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))
|
||||
(marker (create-comment "island-list"))
|
||||
(key-map (dict))
|
||||
@@ -925,7 +925,7 @@
|
||||
;; Handles: input[text/number/email/...], textarea, select, checkbox, radio
|
||||
|
||||
(define bind-input
|
||||
(fn (el sig)
|
||||
(fn (el (sig :as signal))
|
||||
(let ((input-type (lower (or (dom-get-attr el "type") "")))
|
||||
(is-checkbox (or (= input-type "checkbox")
|
||||
(= input-type "radio"))))
|
||||
@@ -960,7 +960,7 @@
|
||||
;; teardown.
|
||||
|
||||
(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)))
|
||||
(target (or (dom-query selector)
|
||||
(dom-ensure-element selector))))
|
||||
@@ -1000,7 +1000,7 @@
|
||||
;; Calling (retry) re-renders the body, replacing the fallback.
|
||||
|
||||
(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))
|
||||
(body-exprs (rest args))
|
||||
(container (dom-create-element "div" nil))
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define process-response-headers
|
||||
(fn (get-header)
|
||||
(fn ((get-header :as lambda))
|
||||
;; Extract all SX response header directives into a dict.
|
||||
;; get-header is (fn (name) → string or nil).
|
||||
(dict
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
|
||||
|
||||
(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
|
||||
(let ((parsed (parse-keyword-args raw-args env))
|
||||
(kwargs (first parsed))
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
(assert (nil? val) (str "Expected nil but got " (str val)))))
|
||||
|
||||
(define assert-type
|
||||
(fn (expected-type val)
|
||||
(fn ((expected-type :as string) val)
|
||||
(let ((actual-type
|
||||
(if (nil? val) "nil"
|
||||
(if (boolean? val) "boolean"
|
||||
@@ -70,17 +70,17 @@
|
||||
(str "Expected type " expected-type " but got " actual-type)))))
|
||||
|
||||
(define assert-length
|
||||
(fn (expected-len col)
|
||||
(fn ((expected-len :as number) (col :as list))
|
||||
(assert (= (len col) expected-len)
|
||||
(str "Expected length " expected-len " but got " (len col)))))
|
||||
|
||||
(define assert-contains
|
||||
(fn (item col)
|
||||
(fn (item (col :as list))
|
||||
(assert (some (fn (x) (equal? x item)) col)
|
||||
(str "Expected collection to contain " (str item)))))
|
||||
|
||||
(define assert-throws
|
||||
(fn (thunk)
|
||||
(fn ((thunk :as lambda))
|
||||
(let ((result (try-call thunk)))
|
||||
(assert (not (get result "ok"))
|
||||
"Expected an error to be thrown but none was"))))
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define z3-sort
|
||||
(fn (sx-type)
|
||||
(fn ((sx-type :as string))
|
||||
(case sx-type
|
||||
"number" "Int"
|
||||
"boolean" "Bool"
|
||||
@@ -40,7 +40,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define z3-name
|
||||
(fn (name)
|
||||
(fn ((name :as string))
|
||||
(cond
|
||||
(= name "!=") "neq"
|
||||
(= name "+") "+"
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
;; Operators that get renamed
|
||||
(define z3-rename-op
|
||||
(fn (op)
|
||||
(fn ((op :as string))
|
||||
(case op
|
||||
"if" "ite"
|
||||
"str" "str.++"
|
||||
@@ -176,7 +176,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define z3-extract-kwargs
|
||||
(fn (expr)
|
||||
(fn ((expr :as list))
|
||||
;; Returns a dict of keyword args from a define-* form
|
||||
;; (define-primitive "name" :params (...) :returns "type" ...) → {:params ... :returns ...}
|
||||
(let ((result {})
|
||||
@@ -184,7 +184,7 @@
|
||||
(z3-extract-kwargs-loop items result))))
|
||||
|
||||
(define z3-extract-kwargs-loop
|
||||
(fn (items result)
|
||||
(fn ((items :as list) (result :as dict))
|
||||
(if (or (empty? items) (< (len items) 2))
|
||||
result
|
||||
(if (= (type-of (first items)) "keyword")
|
||||
@@ -199,12 +199,12 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define z3-params-to-sorts
|
||||
(fn (params)
|
||||
(fn ((params :as list))
|
||||
;; Convert SX param list to list of (name sort) pairs, skipping &rest/&key
|
||||
(z3-params-loop params false (list))))
|
||||
|
||||
(define z3-params-loop
|
||||
(fn (params skip-next acc)
|
||||
(fn ((params :as list) (skip-next :as boolean) (acc :as list))
|
||||
(if (empty? params)
|
||||
acc
|
||||
(let ((p (first params))
|
||||
@@ -227,7 +227,7 @@
|
||||
(z3-params-loop rest-p false acc))))))
|
||||
|
||||
(define z3-has-rest?
|
||||
(fn (params)
|
||||
(fn ((params :as list))
|
||||
(some (fn (p) (and (= (type-of p) "symbol") (= (symbol-name p) "&rest")))
|
||||
params)))
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define z3-translate-primitive
|
||||
(fn (expr)
|
||||
(fn ((expr :as list))
|
||||
(let ((name (nth expr 1))
|
||||
(kwargs (z3-extract-kwargs expr))
|
||||
(params (or (get kwargs "params") (list)))
|
||||
@@ -282,7 +282,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define z3-translate-io
|
||||
(fn (expr)
|
||||
(fn ((expr :as list))
|
||||
(let ((name (nth expr 1))
|
||||
(kwargs (z3-extract-kwargs expr))
|
||||
(doc (or (get kwargs "doc") ""))
|
||||
@@ -297,7 +297,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define z3-translate-special-form
|
||||
(fn (expr)
|
||||
(fn ((expr :as list))
|
||||
(let ((name (nth expr 1))
|
||||
(kwargs (z3-extract-kwargs expr))
|
||||
(doc (or (get kwargs "doc") "")))
|
||||
@@ -342,7 +342,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define z3-translate-file
|
||||
(fn (exprs)
|
||||
(fn ((exprs :as list))
|
||||
;; Filter to translatable forms and translate each
|
||||
(let ((translatable
|
||||
(filter
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; 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/")
|
||||
:label "newsletters"
|
||||
@@ -14,7 +14,7 @@
|
||||
(when account-nav account-nav)))
|
||||
|
||||
;; 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"
|
||||
:link-href (str (or account-url "") "/")
|
||||
:link-label "account" :icon "fa-solid fa-user"
|
||||
@@ -24,7 +24,7 @@
|
||||
:child-id "auth-header-child" :oob oob))
|
||||
|
||||
;; 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"
|
||||
:link-href (str (or account-url "") "/")
|
||||
:link-label "account" :icon "fa-solid fa-user"
|
||||
@@ -52,7 +52,7 @@
|
||||
:account-nav (account-nav-ctx))))
|
||||
|
||||
;; 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"
|
||||
:link-href list-url :link-label "Orders" :icon "fa fa-gbp"
|
||||
:child-id "orders-header-child"))
|
||||
@@ -61,12 +61,12 @@
|
||||
;; Auth forms — login flow, check email
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~auth-error-banner (&key error)
|
||||
(defcomp ~auth-error-banner (&key (error :as string?))
|
||||
(when error
|
||||
(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4"
|
||||
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"
|
||||
(h1 :class "text-2xl font-bold mb-6" "Sign in")
|
||||
error
|
||||
@@ -80,12 +80,12 @@
|
||||
:class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition"
|
||||
"Send magic link"))))
|
||||
|
||||
(defcomp ~auth-check-email-error (&key error)
|
||||
(defcomp ~auth-check-email-error (&key (error :as string?))
|
||||
(when error
|
||||
(div :class "bg-yellow-50 border border-yellow-200 text-yellow-700 p-3 rounded mt-4"
|
||||
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"
|
||||
(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) ".")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
(defcomp ~post-card (&key title slug href feature-image excerpt
|
||||
status published-at updated-at publish-requested
|
||||
hx-select like widgets at-bar)
|
||||
(defcomp ~post-card (&key (title :as string) (slug :as string) (href :as string) (feature-image :as string?)
|
||||
(excerpt :as string?) (status :as string?) (published-at :as string?) (updated-at :as string?)
|
||||
(publish-requested :as boolean?) (hx-select :as string?) like widgets at-bar)
|
||||
(article :class "border-b pb-6 last:border-b-0 relative"
|
||||
(when like like)
|
||||
(a :href href
|
||||
@@ -31,7 +31,8 @@
|
||||
(when widgets widgets)
|
||||
(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"
|
||||
(p (span :class "font-medium" "Order ID:") " " (span :class "font-mono" (str "#" order-id)))
|
||||
(p (span :class "font-medium" "Created:") " " (or created-at "\u2014"))
|
||||
|
||||
@@ -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"
|
||||
:class "flex flex-row gap-2 items-center flex-1 min-w-0 pr-2"
|
||||
(input :id "search-mobile"
|
||||
@@ -20,7 +21,8 @@
|
||||
:class (if (not search-count) "text-xl text-red-500" "")
|
||||
(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"
|
||||
:class "flex flex-row gap-2 items-center"
|
||||
(input :id "search-desktop"
|
||||
@@ -62,7 +64,8 @@
|
||||
(div :id "filter-details-mobile" :style "display:contents"
|
||||
(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)
|
||||
(tr :id (str id-prefix "-sentinel-" page)
|
||||
:sx-get url
|
||||
@@ -82,7 +85,7 @@
|
||||
(tr (td :colspan colspan :class "px-3 py-4 text-center text-xs text-stone-400"
|
||||
"End of results"))))
|
||||
|
||||
(defcomp ~status-pill (&key status size)
|
||||
(defcomp ~status-pill (&key (status :as string?) (size :as string?))
|
||||
(let* ((s (or status "pending"))
|
||||
(lower (lower s))
|
||||
(sz (or size "xs"))
|
||||
|
||||
@@ -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
|
||||
:class "block rounded border border-stone-200 bg-white hover:bg-stone-50 transition-colors no-underline"
|
||||
:data-fragment "link-card"
|
||||
@@ -16,7 +17,7 @@
|
||||
(when 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"
|
||||
:sx-swap-oob oob
|
||||
(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"
|
||||
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"
|
||||
(if user-email
|
||||
@@ -65,7 +66,7 @@
|
||||
(i :class "fa-solid fa-key")
|
||||
(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"
|
||||
(a :href href
|
||||
:class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3"
|
||||
|
||||
@@ -48,19 +48,19 @@
|
||||
:class "w-12 h-12 rotate-180 transition-transform group-open/root:block hidden self-start"
|
||||
(path :d "M6 9l6 6 6-6" :fill "currentColor"))))
|
||||
|
||||
(defcomp ~post-label (&key feature-image title)
|
||||
(defcomp ~post-label (&key (feature-image :as string?) (title :as string))
|
||||
(<> (when feature-image
|
||||
(img :src feature-image :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"))
|
||||
(span title)))
|
||||
|
||||
(defcomp ~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"
|
||||
(i :class "fa fa-shopping-cart" :aria-hidden "true")
|
||||
(span count)))
|
||||
|
||||
(defcomp ~header-row-sx (&key cart-mini blog-url site-title app-label
|
||||
nav-tree auth-menu nav-panel
|
||||
settings-url is-admin oob)
|
||||
(defcomp ~header-row-sx (&key cart-mini (blog-url :as string?) (site-title :as string?)
|
||||
(app-label :as string?) nav-tree auth-menu nav-panel
|
||||
(settings-url :as string?) (is-admin :as boolean?) (oob :as boolean?))
|
||||
(<>
|
||||
(div :id "root-row"
|
||||
:sx-swap-oob (if oob "outerHTML" nil)
|
||||
@@ -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 aria-selected:bg-violet-200 aria-selected:text-violet-900 aria-selected:bg-stone-500 aria-selected:text-white
|
||||
(defcomp ~menu-row-sx (&key id level colour link-href link-label link-label-content icon
|
||||
selected hx-select nav child-id child oob external)
|
||||
(defcomp ~menu-row-sx (&key (id :as string) (level :as number?) (colour :as string?)
|
||||
(link-href :as string) (link-label :as string?) link-label-content
|
||||
(icon :as string?) (selected :as string?) (hx-select :as string?)
|
||||
nav (child-id :as string?) child (oob :as boolean?) (external :as boolean?))
|
||||
(let* ((c (or colour "sky"))
|
||||
(lv (or level 1))
|
||||
(shade (str (- 500 (* lv 100)))))
|
||||
@@ -115,11 +117,11 @@
|
||||
(div :id child-id :class "flex flex-col w-full items-center"
|
||||
(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"
|
||||
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))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -127,7 +129,8 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; 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"))
|
||||
(lv (or level 1))
|
||||
(shade (str (- 500 (* lv 100)))))
|
||||
@@ -153,8 +156,9 @@
|
||||
;; nested component calls in _aser are serialized without expansion.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~root-header (&key cart-mini blog-url site-title app-label
|
||||
nav-tree auth-menu nav-panel settings-url is-admin oob)
|
||||
(defcomp ~root-header (&key cart-mini (blog-url :as string?) (site-title :as string?)
|
||||
(app-label :as string?) nav-tree auth-menu nav-panel
|
||||
(settings-url :as string?) (is-admin :as boolean?) (oob :as boolean?))
|
||||
(~header-row-sx :cart-mini cart-mini :blog-url blog-url :site-title site-title
|
||||
:app-label app-label :nav-tree nav-tree :auth-menu auth-menu
|
||||
:nav-panel nav-panel :settings-url settings-url :is-admin is-admin
|
||||
@@ -226,18 +230,18 @@
|
||||
(~root-mobile-auto))))
|
||||
|
||||
;; 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)))
|
||||
(<> (~root-header-auto)
|
||||
(~header-child-sx
|
||||
: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)
|
||||
(~oob-header-sx :parent-id "post-header-child"
|
||||
: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)))
|
||||
(<>
|
||||
(when (get __phctx "slug")
|
||||
@@ -254,7 +258,7 @@
|
||||
:items (~post-nav-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 "font-bold text-2xl md:text-4xl text-red-500 mb-4" errnum)
|
||||
(div :class "text-stone-600 mb-4" message)
|
||||
@@ -262,7 +266,7 @@
|
||||
(div :class "flex justify-center"
|
||||
(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"))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -354,21 +358,22 @@
|
||||
content))
|
||||
|
||||
; @css justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 !bg-stone-500 !text-white
|
||||
(defcomp ~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"
|
||||
(a :href href
|
||||
:class (str "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 "
|
||||
(if is-admin-page "!bg-stone-500 !text-white" ""))
|
||||
(i :class "fa fa-cog" :aria-hidden "true"))))
|
||||
|
||||
(defcomp ~post-admin-label (&key selected)
|
||||
(defcomp ~post-admin-label (&key (selected :as string?))
|
||||
(<>
|
||||
(i :class "fa fa-shield-halved" :aria-hidden "true")
|
||||
" admin"
|
||||
(when selected
|
||||
(span :class "text-white" selected))))
|
||||
|
||||
(defcomp ~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"
|
||||
(a :href href
|
||||
:sx-get href
|
||||
|
||||
@@ -2,32 +2,33 @@
|
||||
|
||||
;; The single place where raw! lives — for CMS content (Ghost post body,
|
||||
;; product descriptions, etc.) that arrives as pre-rendered HTML.
|
||||
(defcomp ~rich-text (&key html)
|
||||
(defcomp ~rich-text (&key (html :as string))
|
||||
(raw! html))
|
||||
|
||||
(defcomp ~error-inline (&key message)
|
||||
(defcomp ~error-inline (&key (message :as string))
|
||||
(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))
|
||||
|
||||
(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))
|
||||
|
||||
(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"
|
||||
(when items items)))
|
||||
|
||||
(defcomp ~error-list-item (&key message)
|
||||
(defcomp ~error-list-item (&key (message :as string))
|
||||
(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."))
|
||||
|
||||
(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))
|
||||
|
||||
(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"
|
||||
(a :href href :sx-get href :sx-target "#main-panel"
|
||||
:sx-select hx-select :sx-swap "outerHTML"
|
||||
@@ -38,7 +39,7 @@
|
||||
;; 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"
|
||||
:sx-get next-url :sx-trigger "intersect once delay:250ms, sentinelmobile:retry"
|
||||
:sx-swap "outerHTML" :_ hyperscript
|
||||
@@ -49,7 +50,7 @@
|
||||
(i :class "fa fa-exclamation-triangle text-2xl")
|
||||
(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"
|
||||
:sx-get next-url :sx-trigger "intersect once delay:250ms, sentinel:retry"
|
||||
: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 "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"
|
||||
:sx-get next-url :sx-trigger "intersect once delay:250ms" :sx-swap "outerHTML"
|
||||
:role "status" :aria-hidden "true"
|
||||
(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"))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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")
|
||||
(when icon (div (i :class (str icon " text-4xl mb-2") :aria-hidden "true")))
|
||||
(p message)
|
||||
@@ -81,7 +82,7 @@
|
||||
;; 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"))
|
||||
label))
|
||||
|
||||
@@ -89,8 +90,9 @@
|
||||
;; Shared delete button with confirm dialog
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~delete-btn (&key url trigger-target title text confirm-text cancel-text
|
||||
sx-headers cls)
|
||||
(defcomp ~delete-btn (&key (url :as string) (trigger-target :as string) (title :as string?)
|
||||
(text :as string?) (confirm-text :as string?) (cancel-text :as string?)
|
||||
(sx-headers :as string?) (cls :as string?))
|
||||
(button :type "button"
|
||||
:data-confirm "" :data-confirm-title (or title "Delete?")
|
||||
:data-confirm-text (or text "Are you sure?")
|
||||
@@ -108,7 +110,7 @@
|
||||
;; 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"
|
||||
(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))
|
||||
@@ -118,7 +120,8 @@
|
||||
;; 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
|
||||
(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")
|
||||
@@ -141,8 +144,9 @@
|
||||
(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")))
|
||||
|
||||
(defcomp ~view-toggle (&key list-href tile-href hx-select list-cls tile-cls
|
||||
storage-key list-svg tile-svg)
|
||||
(defcomp ~view-toggle (&key (list-href :as string) (tile-href :as string) (hx-select :as string?)
|
||||
(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"
|
||||
(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"
|
||||
@@ -157,7 +161,9 @@
|
||||
;; 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")
|
||||
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url
|
||||
@@ -171,13 +177,14 @@
|
||||
:placeholder (or placeholder "Name")))
|
||||
(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"
|
||||
form
|
||||
(div :id (or list-id "crud-list") :class "mt-6" list)))
|
||||
|
||||
(defcomp ~crud-item (&key href name slug del-url csrf-hdr list-id
|
||||
confirm-title confirm-text)
|
||||
(defcomp ~crud-item (&key (href :as string) (name :as string) (slug :as string) (del-url :as string)
|
||||
(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 "flex items-center justify-between gap-3"
|
||||
(a :class "flex items-baseline gap-3" :href href
|
||||
@@ -199,9 +206,10 @@
|
||||
;; checkout prefix) used by blog, events, and cart admin panels.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~sumup-settings-form (&key update-url csrf merchant-code placeholder
|
||||
input-cls sumup-configured checkout-prefix
|
||||
panel-id sx-select)
|
||||
(defcomp ~sumup-settings-form (&key (update-url :as string) (csrf :as string?) (merchant-code :as string?)
|
||||
(placeholder :as string?) (input-cls :as string?)
|
||||
(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"
|
||||
(h3 :class "text-lg font-semibold text-stone-800"
|
||||
(i :class "fa fa-credit-card text-purple-600 mr-1") " SumUp Payment")
|
||||
@@ -233,7 +241,7 @@
|
||||
;; 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
|
||||
(img :src src :alt "" :class cls)
|
||||
(div :class cls initial)))
|
||||
@@ -242,7 +250,9 @@
|
||||
;; 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"
|
||||
: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")
|
||||
|
||||
@@ -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
|
||||
(div :class "w-8 h-8 rounded bg-stone-200 flex-shrink-0")
|
||||
(div :class "flex-1 min-w-0"
|
||||
(div :class "font-medium truncate" name)
|
||||
(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
|
||||
:sx-get href
|
||||
:sx-target "#main-panel"
|
||||
@@ -17,12 +18,14 @@
|
||||
(i :class "fa fa-calendar" :aria-hidden "true")
|
||||
(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 ""))
|
||||
(i :class "fa fa-shopping-bag" :aria-hidden "true")
|
||||
(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")
|
||||
(when icon
|
||||
(div :class "w-8 h-8 rounded bg-stone-200 flex items-center justify-center flex-shrink-0"
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
;; 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"
|
||||
(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)
|
||||
@@ -16,7 +17,8 @@
|
||||
(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"))))
|
||||
|
||||
(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"
|
||||
(td :colspan "5" :class "px-3 py-3"
|
||||
(div :class "flex flex-col gap-2 text-xs"
|
||||
@@ -61,13 +63,14 @@
|
||||
;; 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"))
|
||||
|
||||
(defcomp ~order-item-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
|
||||
(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"
|
||||
@@ -83,7 +86,8 @@
|
||||
(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)))
|
||||
|
||||
(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"
|
||||
(div (div :class "font-medium flex items-center gap-2"
|
||||
name (span :class pill status))
|
||||
@@ -98,11 +102,12 @@
|
||||
(defcomp ~order-detail-panel (&key 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"
|
||||
(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"
|
||||
(div :class "space-y-1"
|
||||
(p :class "text-xs sm:text-sm text-stone-600" info))
|
||||
@@ -124,7 +129,8 @@
|
||||
;; 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)
|
||||
(<>
|
||||
@@ -144,7 +150,7 @@
|
||||
;; 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
|
||||
:items (<> (map (lambda (item)
|
||||
(let* ((img (if (get item "product_image")
|
||||
@@ -162,7 +168,7 @@
|
||||
;; 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
|
||||
:items (<> (map (lambda (e)
|
||||
(~order-calendar-entry
|
||||
@@ -180,7 +186,7 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; 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 ""))))
|
||||
(cond
|
||||
((= 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"))))
|
||||
|
||||
;; 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"))
|
||||
(pill-base (~order-status-pill-cls :status status))
|
||||
(oid (str "#" (get order "id")))
|
||||
@@ -207,7 +213,8 @@
|
||||
:status status :url url))))
|
||||
|
||||
;; 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)
|
||||
(~order-empty-state)
|
||||
(~order-table
|
||||
@@ -223,7 +230,7 @@
|
||||
(~order-end-row))))))
|
||||
|
||||
;; 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")))
|
||||
(~order-detail-panel
|
||||
:summary (~order-summary-card
|
||||
@@ -265,7 +272,8 @@
|
||||
calendar-entries))))))
|
||||
|
||||
;; 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"))
|
||||
(created (or (get order "created_at_formatted") "\u2014")))
|
||||
(~order-detail-filter
|
||||
@@ -280,7 +288,7 @@
|
||||
;; Checkout return components
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~checkout-return-header (&key status)
|
||||
(defcomp ~checkout-return-header (&key (status :as string))
|
||||
(header :class "mb-6 sm:mb-8"
|
||||
(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"
|
||||
@@ -290,7 +298,9 @@
|
||||
(div :class "max-w-full px-3 py-3 space-y-4"
|
||||
(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"
|
||||
(div
|
||||
(div :class "font-medium flex items-center gap-2"
|
||||
@@ -305,7 +315,7 @@
|
||||
(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)))
|
||||
|
||||
(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"
|
||||
(p :class "font-medium" "Payment failed")
|
||||
(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")
|
||||
(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)))
|
||||
|
||||
(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 "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.")
|
||||
|
||||
@@ -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>")
|
||||
(html :lang "en"
|
||||
@@ -23,13 +23,13 @@
|
||||
;; <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)
|
||||
:data-suspense id
|
||||
:style "display:contents"
|
||||
(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
|
||||
(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"
|
||||
|
||||
@@ -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
|
||||
:sx-get create-url
|
||||
:sx-target "#main-panel"
|
||||
@@ -8,7 +8,7 @@
|
||||
(when icon (i :class icon))
|
||||
(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
|
||||
: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"
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
;; - Pre-rendered meta HTML from callers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~sx-page-shell (&key title meta-html csrf
|
||||
sx-css sx-css-classes
|
||||
component-hash component-defs
|
||||
pages-sx page-sx
|
||||
asset-url sx-js-hash body-js-hash
|
||||
head-scripts inline-css inline-head-js
|
||||
init-sx body-scripts)
|
||||
(defcomp ~sx-page-shell (&key (title :as string) (meta-html :as string?) (csrf :as string)
|
||||
(sx-css :as string?) (sx-css-classes :as string?)
|
||||
(component-hash :as string?) (component-defs :as string?)
|
||||
(pages-sx :as string?) (page-sx :as string?)
|
||||
(asset-url :as string) (sx-js-hash :as string) (body-js-hash :as string?)
|
||||
(head-scripts :as list?) (inline-css :as string?) (inline-head-js :as string?)
|
||||
(init-sx :as string?) (body-scripts :as list?))
|
||||
(<>
|
||||
(raw! "<!doctype html>")
|
||||
(html :lang "en"
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
|
||||
;; --- 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 "flex items-center gap-2 mb-2"
|
||||
(span :class "inline-block w-2 h-2 rounded-full bg-stone-400")
|
||||
(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."))))
|
||||
|
||||
(defcomp ~aff-demo-client (&key label)
|
||||
(defcomp ~aff-demo-client (&key (label :as string?))
|
||||
:affinity :client
|
||||
(div :class "rounded border border-blue-200 bg-blue-50 p-4"
|
||||
(div :class "flex items-center gap-2 mb-2"
|
||||
@@ -21,7 +21,7 @@
|
||||
(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."))))
|
||||
|
||||
(defcomp ~aff-demo-server (&key label)
|
||||
(defcomp ~aff-demo-server (&key (label :as string?))
|
||||
:affinity :server
|
||||
(div :class "rounded border border-amber-200 bg-amber-50 p-4"
|
||||
(div :class "flex items-center gap-2 mb-2"
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
;; 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
|
||||
|
||||
(defcomp ~bundle-analyzer-content (&key pages total-components total-macros
|
||||
pure-count io-count)
|
||||
(defcomp ~bundle-analyzer-content (&key (pages :as list) (total-components :as number) (total-macros :as number)
|
||||
(pure-count :as number) (io-count :as number))
|
||||
(~doc-page :title "Page Bundle Analyzer"
|
||||
|
||||
(p :class "text-stone-600 mb-6"
|
||||
@@ -55,13 +55,13 @@
|
||||
"walks all branches of control flow (if/when/cond/case), "
|
||||
"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 (str "text-3xl font-bold " cls) value)
|
||||
(div :class "text-sm text-stone-500 mt-1" label)))
|
||||
|
||||
(defcomp ~analyzer-row (&key name path needed direct total pct savings
|
||||
io-refs pure-in-page io-in-page components)
|
||||
(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 :as list) (pure-in-page :as number) (io-in-page :as number) (components :as list))
|
||||
(details :class "rounded border border-stone-200"
|
||||
(summary :class "p-4 cursor-pointer hover:bg-stone-50 transition-colors"
|
||||
(div :class "flex items-center justify-between mb-2"
|
||||
@@ -97,7 +97,7 @@
|
||||
:source (get comp "source")))
|
||||
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 "
|
||||
(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"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
;; "sx:route client+data" — cache miss, fetched from server
|
||||
;; "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 "border-b border-stone-200 pb-6"
|
||||
(h1 :class "text-2xl font-bold text-stone-900" "Data Test")
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
;; SX docs utility components
|
||||
|
||||
(defcomp ~doc-placeholder (&key id)
|
||||
(defcomp ~doc-placeholder (&key (id :as string))
|
||||
(div :id id
|
||||
(div :class "bg-stone-100 rounded p-4 mt-3"
|
||||
(p :class "text-stone-400 italic text-sm"
|
||||
"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 :class "not-prose bg-stone-100 rounded p-4 mt-3"
|
||||
(pre :class "text-sm whitespace-pre-wrap break-words"
|
||||
(code text)))))
|
||||
|
||||
(defcomp ~doc-attr-table (&key title rows)
|
||||
(defcomp ~doc-attr-table (&key (title :as string) rows)
|
||||
(div :class "space-y-3"
|
||||
(h3 :class "text-xl font-semibold text-stone-700" title)
|
||||
(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?")))
|
||||
(tbody rows)))))
|
||||
|
||||
(defcomp ~doc-headers-table (&key title rows)
|
||||
(defcomp ~doc-headers-table (&key (title :as string) rows)
|
||||
(div :class "space-y-3"
|
||||
(h3 :class "text-xl font-semibold text-stone-700" title)
|
||||
(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")))
|
||||
(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"
|
||||
(td :class "px-3 py-2 font-mono text-sm whitespace-nowrap"
|
||||
(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 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"
|
||||
(td :class "px-3 py-2 font-mono text-sm whitespace-nowrap"
|
||||
(if href
|
||||
@@ -57,7 +57,7 @@
|
||||
(span :class "text-violet-700" name)))
|
||||
(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"
|
||||
(when title (h3 :class "text-xl font-semibold text-stone-700" title))
|
||||
(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.
|
||||
;; 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
|
||||
:rows (<> (map (fn (a)
|
||||
(~doc-attr-row
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
;; Build headers table from a list of {name, value, desc} dicts.
|
||||
;; 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
|
||||
:rows (<> (map (fn (h)
|
||||
(~doc-headers-row
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
;; Build two-col table from a list of {name, desc} dicts.
|
||||
;; 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
|
||||
:rows (<> (map (fn (item)
|
||||
(~doc-two-col-row
|
||||
@@ -117,7 +117,7 @@
|
||||
|
||||
;; Build all primitives category tables from a {category: [prim, ...]} dict.
|
||||
;; Replaces _primitives_section_sx() in utils.py.
|
||||
(defcomp ~doc-primitives-tables (&key primitives)
|
||||
(defcomp ~doc-primitives-tables (&key (primitives :as dict))
|
||||
(<> (map (fn (cat)
|
||||
(~doc-primitives-table
|
||||
:category cat
|
||||
@@ -125,14 +125,14 @@
|
||||
(keys primitives))))
|
||||
|
||||
;; 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)
|
||||
(~doc-special-forms-category
|
||||
:category cat
|
||||
:forms (get forms cat)))
|
||||
(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"
|
||||
(h3 :class "text-xl font-semibold text-stone-800 border-b border-stone-200 pb-2" category)
|
||||
(div :class "space-y-4"
|
||||
@@ -145,7 +145,7 @@
|
||||
:example (get f "example")))
|
||||
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 "flex items-baseline gap-3"
|
||||
(code :class "text-lg font-bold text-violet-700" name)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
;; Example page template and reference index
|
||||
;; Template receives data values (code strings, titles), calls highlight internally.
|
||||
|
||||
(defcomp ~example-page-content (&key title description demo-description demo
|
||||
sx-code sx-lang handler-code handler-lang
|
||||
comp-placeholder-id wire-placeholder-id wire-note
|
||||
comp-heading handler-heading)
|
||||
(defcomp ~example-page-content (&key (title :as string) (description :as string) (demo-description :as string?) demo
|
||||
(sx-code :as string) (sx-lang :as string?) (handler-code :as string) (handler-lang :as string?)
|
||||
(comp-placeholder-id :as string?) (wire-placeholder-id :as string?) (wire-note :as string?)
|
||||
(comp-heading :as string?) (handler-heading :as string?))
|
||||
(~doc-page :title title
|
||||
(p :class "text-stone-600 mb-6" description)
|
||||
(~example-card :title "Demo" :description demo-description
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
;; 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"
|
||||
(h1 :style (cssx (:text (colour "stone" 800) (size "3xl") (weight "bold")))
|
||||
"404")
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
;; 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"))
|
||||
(title-c (if (= theme "blue") "text-blue-700" "text-stone-700"))
|
||||
(badge-c (if (= theme "blue") "text-blue-400" "text-stone-400"))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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
|
||||
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SX Source")
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
(~doc-section :title "SX Diff Viewer" :id "diff-viewer"
|
||||
(p "Diffs rendered as SX components, not pre-formatted text:")
|
||||
(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)
|
||||
(~diff-hunk
|
||||
:file (get hunk \"file\")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; 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"
|
||||
(td :class "py-1.5 px-3 font-mono text-xs text-stone-700" name)
|
||||
(td :class "py-1.5 px-3 text-xs"
|
||||
@@ -12,7 +12,7 @@
|
||||
(span :class "text-red-600 font-medium" status)))))
|
||||
|
||||
;; 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"
|
||||
(td :class "py-1.5 px-3 font-mono text-xs text-stone-700" name)
|
||||
(td :class "py-1.5 px-3 text-xs"
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
(~doc-page :title "JavaScript API"
|
||||
table))
|
||||
|
||||
(defcomp ~reference-attr-detail-content (&key title description demo
|
||||
example-code handler-code wire-placeholder-id)
|
||||
(defcomp ~reference-attr-detail-content (&key (title :as string) (description :as string) demo
|
||||
(example-code :as string) (handler-code :as string?) (wire-placeholder-id :as string?))
|
||||
(~doc-page :title title
|
||||
(p :class "text-stone-600 mb-6" description)
|
||||
(when demo
|
||||
@@ -50,8 +50,8 @@
|
||||
"Trigger the demo to see the raw response the server sends.")
|
||||
(~doc-placeholder :id wire-placeholder-id)))))
|
||||
|
||||
(defcomp ~reference-header-detail-content (&key title direction description
|
||||
example-code demo)
|
||||
(defcomp ~reference-header-detail-content (&key (title :as string) (direction :as string) (description :as string)
|
||||
(example-code :as string?) demo)
|
||||
(~doc-page :title title
|
||||
(let ((badge-class (if (= direction "request")
|
||||
"bg-blue-100 text-blue-700"
|
||||
@@ -84,7 +84,7 @@
|
||||
(h3 :class "text-lg font-semibold text-stone-700 mt-6" "Example usage")
|
||||
(~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"
|
||||
(p :class "text-stone-600"
|
||||
(str "No documentation found for \"" slug "\"."))))
|
||||
|
||||
@@ -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 "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 "
|
||||
(if (= mode "client")
|
||||
"border-green-200 bg-green-50"
|
||||
|
||||
@@ -230,7 +230,7 @@ router.sx (standalone — pure string/list ops)")))
|
||||
;; 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")
|
||||
(p :class "text-stone-600 mb-6"
|
||||
(case spec-title
|
||||
@@ -264,7 +264,7 @@ router.sx (standalone — pure string/list ops)")))
|
||||
;; 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
|
||||
(div :class "flex items-baseline gap-3 mb-4"
|
||||
(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
|
||||
|
||||
(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)"
|
||||
(div :class "space-y-8"
|
||||
|
||||
@@ -594,7 +594,7 @@ router.sx (standalone — pure string/list ops)")))
|
||||
;; Not found
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~spec-not-found (&key slug)
|
||||
(defcomp ~spec-not-found (&key (slug :as string))
|
||||
(~doc-page :title "Spec Not Found"
|
||||
(p :class "text-stone-600"
|
||||
"No specification found for \"" slug "\". This spec may not exist yet.")))
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
;; Generic streamed content chunk — rendered once per yield from the
|
||||
;; async generator. The :content expression receives different bindings
|
||||
;; 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)))
|
||||
(div :class (str "rounded-lg border p-5 space-y-3 " (get colors "border") " " (get colors "bg"))
|
||||
(div :class "flex items-center gap-2"
|
||||
|
||||
@@ -139,7 +139,7 @@ Per-spec platform functions:
|
||||
;; 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
|
||||
(div :class "space-y-8"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user