Refactor SX templates: shared components, Python migration, cleanup
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m0s

- Extract shared components (empty-state, delete-btn, sentinel, crud-*,
  view-toggle, img-or-placeholder, avatar, sumup-settings-form, auth
  forms, order tables/detail/checkout)
- Migrate all Python sx_call() callers to use shared components directly
- Remove 55+ thin wrapper defcomps from domain .sx files
- Remove trivial passthrough wrappers (blog-header-label, market-card-text, etc)
- Unify duplicate auth flows (account + federation) into shared/sx/templates/auth.sx
- Unify duplicate order views (cart + orders) into shared/sx/templates/orders.sx
- Disable static file caching in dev (SEND_FILE_MAX_AGE_DEFAULT=0)
- Add SX response validation and debug headers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 20:34:34 +00:00
parent 755313bd29
commit c0d369eb8e
58 changed files with 3473 additions and 1210 deletions

View File

@@ -47,12 +47,14 @@ def register():
if not markets:
return ""
styles = current_app.jinja_env.globals.get("styles", {})
nav_class = styles.get("nav_button_less_pad", "")
nav_class = styles.get("nav_button", "")
select_colours = current_app.jinja_env.globals.get("select_colours", "")
parts = []
for m in markets:
href = market_url(f"/{post_slug}/{m.slug}/")
parts.append(sx_call("market-link-nav",
href=href, name=m.name, nav_class=nav_class))
href=href, name=m.name, nav_class=nav_class,
select_colours=select_colours))
return "(<> " + " ".join(parts) + ")"
_handlers["container-nav"] = _container_nav_handler

View File

@@ -1,40 +0,0 @@
;; Market admin panel components (page-level admin for markets)
(defcomp ~market-admin-create-form (&key create-url csrf)
(<>
(div :id "market-create-errors" :class "mt-2 text-sm text-red-600")
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
:sx-on:beforeRequest "document.querySelector('#market-create-errors').textContent='';"
:sx-on:responseError "document.querySelector('#market-create-errors').textContent='Error'; if(event.detail.response){event.detail.response.clone().text().then(function(t){event.target.closest('form').querySelector('[id$=errors]').innerHTML=t})}"
(input :type "hidden" :name "csrf_token" :value csrf)
(div :class "flex-1"
(label :class "block text-sm text-gray-600" "Name")
(input :name "name" :type "text" :required true :class "w-full border rounded px-3 py-2"
:placeholder "e.g. Suma, Craft Fair"))
(button :type "submit" :class "border rounded px-3 py-2" "Add market"))))
(defcomp ~market-admin-panel (&key form list)
(section :class "p-4"
form
(div :id "markets-list" :class "mt-6" list)))
(defcomp ~market-admin-empty ()
(p :class "text-gray-500 mt-4" "No markets yet. Create one above."))
(defcomp ~market-admin-item (&key href name slug del-url csrf-hdr)
(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
:sx-get href :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true"
(h3 :class "font-semibold" name)
(h4 :class "text-gray-500" (str "/" slug "/")))
(button :class "text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400"
:data-confirm true :data-confirm-title "Delete market?"
:data-confirm-text "Products will be hidden (soft delete)"
:data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it"
:data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"
:sx-delete del-url :sx-trigger "confirmed"
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
:sx-headers csrf-hdr
(i :class "fa-solid fa-trash")))))

View File

@@ -19,9 +19,6 @@
(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-label-item (&key label)
(li label))
(defcomp ~market-card-sticker (&key src name ring-cls)
(img :src src :alt name :class (str "w-6 h-6" ring-cls)))
@@ -32,15 +29,9 @@
(defcomp ~market-card-highlight (&key pre mid post)
(<> pre (mark mid) post))
(defcomp ~market-card-text (&key text)
(<> text))
;; Price — single component accepts both prices, renders correctly
;; Price — delegates to shared ~price
(defcomp ~market-card-price (&key special-price regular-price)
(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))
(when (and (not special-price) regular-price) (div :class "mt-1 text-lg font-semibold" regular-price))))
(~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
@@ -104,31 +95,3 @@
(if desc-content desc-content (when desc desc)))
(if badge-content badge-content (when badge badge))))
(defcomp ~market-sentinel-mobile (&key id next-url hyperscript)
(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
:role "status" :aria-live "polite" :aria-hidden "true"
(div :class "js-loading text-center text-xs text-stone-400" "loading...")
(div :class "js-neterr hidden text-center text-xs text-stone-400" "Retrying...")))
(defcomp ~market-sentinel-desktop (&key id next-url hyperscript)
(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
:role "status" :aria-live "polite" :aria-hidden "true"
(div :class "js-loading text-center text-xs text-stone-400" "loading...")
(div :class "js-neterr hidden text-center text-xs text-stone-400" "Retrying...")))
(defcomp ~market-sentinel-end ()
(div :class "col-span-full mt-4 text-center text-xs text-stone-400" "End of results"))
(defcomp ~market-market-sentinel (&key id next-url)
(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...")))

View File

@@ -3,17 +3,9 @@
(defcomp ~market-markets-grid (&key cards)
(div :class "max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" cards))
(defcomp ~market-no-markets (&key message)
(div :class "px-3 py-12 text-center text-stone-400"
(i :class "fa fa-store text-4xl mb-3" :aria-hidden "true")
(p :class "text-lg" message)))
(defcomp ~market-product-grid (&key cards)
(<> (div :class "grid grid-cols-1 sm:grid-cols-3 md:grid-cols-6 gap-3" cards) (div :class "pb-8")))
(defcomp ~market-bottom-spacer ()
(div :class "pb-8"))
(defcomp ~market-like-toggle-button (&key colour action hx-headers label icon-cls)
(button :class (str "flex items-center gap-1 " colour " hover:text-red-600 transition-colors w-[1em] h-[1em]")
:sx-post action :sx-target "this" :sx-swap "outerHTML" :sx-push-url "false"

View File

@@ -6,9 +6,6 @@
(div :class "flex flex-col md:flex-row md:gap-2 text-xs"
(div top-slug) sub-div)))
(defcomp ~market-sub-slug (&key sub)
(div sub))
(defcomp ~market-product-label (&key title)
(<> (i :class "fa fa-shopping-bag" :aria-hidden "true") (div title)))

View File

@@ -10,6 +10,7 @@ from __future__ import annotations
import os
from typing import Any
from shared.sx.jinja_bridge import load_service_components
from shared.sx.parser import serialize
from shared.sx.helpers import (
call_url, get_asset_url, sx_call, SxExpr,
root_header_sx,
@@ -102,7 +103,7 @@ def _market_header_sx(ctx: dict, *, oob: bool = False) -> str:
sub_slug = ctx.get("sub_slug", "")
hx_select_search = ctx.get("hx_select_search", "#main-panel")
sub_div = sx_call("market-sub-slug", sub=sub_slug) if sub_slug else ""
sub_div = f'(div {serialize(sub_slug)})' if sub_slug else ""
label_sx = sx_call(
"market-shop-label",
title=market_title, top_slug=top_slug or "",
@@ -439,14 +440,14 @@ def _product_cards_sx(ctx: dict) -> str:
else:
next_qs = f"?page={page + 1}"
next_url = prefix + current_local_href + next_qs
parts.append(sx_call("market-sentinel-mobile",
parts.append(sx_call("sentinel-mobile",
id=f"sentinel-{page}-m", next_url=next_url,
hyperscript=_MOBILE_SENTINEL_HS))
parts.append(sx_call("market-sentinel-desktop",
parts.append(sx_call("sentinel-desktop",
id=f"sentinel-{page}-d", next_url=next_url,
hyperscript=_DESKTOP_SENTINEL_HS))
else:
parts.append(sx_call("market-sentinel-end"))
parts.append(sx_call("end-of-results"))
return "(<> " + " ".join(parts) + ")"
@@ -1170,7 +1171,7 @@ def _market_cards_sx(markets: list, page_info: dict, page: int, has_more: bool,
post_slug=post_slug) for m in markets]
if has_more:
parts.append(sx_call(
"market-market-sentinel",
"sentinel-simple",
id=f"sentinel-{page}", next_url=next_url,
))
return "(<> " + " ".join(parts) + ")"
@@ -1197,7 +1198,8 @@ def _markets_grid(cards_sx: str) -> str:
def _no_markets_sx(message: str = "No markets available") -> str:
"""Empty state for markets as sx."""
return sx_call("market-no-markets", message=message)
return sx_call("empty-state", icon="fa fa-store", message=message,
cls="px-3 py-12 text-center text-stone-400")
async def render_all_markets_page(ctx: dict, markets: list, has_more: bool,
@@ -1214,7 +1216,7 @@ async def render_all_markets_page(ctx: dict, markets: list, has_more: bool,
content = _markets_grid(cards)
else:
content = _no_markets_sx()
content = "(<> " + content + " " + sx_call("market-bottom-spacer") + ")"
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
hdr = root_header_sx(ctx)
return full_page_sx(ctx, header_rows=hdr, content=content)
@@ -1234,7 +1236,7 @@ async def render_all_markets_oob(ctx: dict, markets: list, has_more: bool,
content = _markets_grid(cards)
else:
content = _no_markets_sx()
content = "(<> " + content + " " + sx_call("market-bottom-spacer") + ")"
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
oobs = root_header_sx(ctx, oob=True)
return oob_page_sx(oobs=oobs, content=content)
@@ -1272,7 +1274,7 @@ async def render_page_markets_page(ctx: dict, markets: list, has_more: bool,
content = _markets_grid(cards)
else:
content = _no_markets_sx("No markets for this page")
content = "(<> " + content + " " + sx_call("market-bottom-spacer") + ")"
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
hdr = root_header_sx(ctx)
hdr = "(<> " + hdr + " " + header_child_sx(_post_header_sx(ctx)) + ")"
@@ -1296,7 +1298,7 @@ async def render_page_markets_oob(ctx: dict, markets: list, has_more: bool,
content = _markets_grid(cards)
else:
content = _no_markets_sx("No markets for this page")
content = "(<> " + content + " " + sx_call("market-bottom-spacer") + ")"
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
oobs = _oob_header_sx("post-header-child", "market-header-child", "")
oobs = "(<> " + oobs + " " + _post_header_sx(ctx, oob=True) + ")"
@@ -1535,12 +1537,17 @@ async def _markets_admin_panel_sx(ctx: dict) -> str:
form_html = ""
if can_create:
create_url = url_for("page_admin.create_market")
form_html = sx_call("market-admin-create-form",
create_url=create_url, csrf=csrf)
form_html = sx_call("crud-create-form",
create_url=create_url, csrf=csrf,
errors_id="market-create-errors",
list_id="markets-list",
placeholder="e.g. Suma, Craft Fair",
btn_label="Add market")
list_html = _markets_admin_list_sx(ctx, markets)
return sx_call("market-admin-panel",
form=SxExpr(form_html), list=SxExpr(list_html))
return sx_call("crud-panel",
form=SxExpr(form_html), list=SxExpr(list_html),
list_id="markets-list")
def _markets_admin_list_sx(ctx: dict, markets: list) -> str:
@@ -1552,7 +1559,9 @@ def _markets_admin_list_sx(ctx: dict, markets: list) -> str:
prefix = route_prefix()
if not markets:
return sx_call("market-admin-empty")
return sx_call("empty-state",
message="No markets yet. Create one above.",
cls="text-gray-500 mt-4")
parts = []
for m in markets:
@@ -1562,9 +1571,12 @@ def _markets_admin_list_sx(ctx: dict, markets: list) -> str:
href = prefix + f"/{post_slug}/{m_slug}/"
del_url = url_for("page_admin.delete_market", market_slug=m_slug)
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
parts.append(sx_call("market-admin-item",
parts.append(sx_call("crud-item",
href=href, name=m_name, slug=m_slug,
del_url=del_url, csrf_hdr=csrf_hdr))
del_url=del_url, csrf_hdr=csrf_hdr,
list_id="markets-list",
confirm_title="Delete market?",
confirm_text="Products will be hidden (soft delete)"))
return "".join(parts)