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

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

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

View File

@@ -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)

View File

@@ -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))

View File

@@ -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

View File

@@ -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))

View File

@@ -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"))))

View File

@@ -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

View File

@@ -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) ".")

View File

@@ -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"))

View File

@@ -1,4 +1,5 @@
(defcomp ~search-mobile (&key current-local-href search search-count hx-select search-headers-mobile)
(defcomp ~search-mobile (&key (current-local-href :as string) (search :as string?) (search-count :as number?)
(hx-select :as string?) (search-headers-mobile :as string?))
(div :id "search-mobile-wrapper"
: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"))

View File

@@ -1,4 +1,5 @@
(defcomp ~link-card (&key link title image icon subtitle detail data-app)
(defcomp ~link-card (&key (link :as string) (title :as string) (image :as string?) (icon :as string?)
(subtitle :as string?) (detail :as string?) (data-app :as string?))
(a :href link
: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"

View File

@@ -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

View File

@@ -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")

View File

@@ -1,11 +1,12 @@
(defcomp ~calendar-entry-nav (&key href name date-str nav-class)
(defcomp ~calendar-entry-nav (&key (href :as string) (name :as string) (date-str :as string) (nav-class :as string?))
(a :href href :class nav-class
(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"

View File

@@ -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.")

View File

@@ -1,4 +1,4 @@
(defcomp ~base-shell (&key title asset-url &rest children)
(defcomp ~base-shell (&key (title :as string) (asset-url :as string) &rest children)
(<>
(raw! "<!doctype html>")
(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"

View File

@@ -1,4 +1,4 @@
(defcomp ~relation-attach (&key create-url label icon)
(defcomp ~relation-attach (&key (create-url :as string) (label :as string?) (icon :as string?))
(a :href create-url
: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"

View File

@@ -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"