Move market composition from Python to .sx defcomps (Phase 3)

Python sxc/pages/ functions no longer build nested sx_call chains or
reference leaf component names. Instead they extract data (URLs, prices,
CSRF, cart state) and call a single top-level composition defcomp with
pure data values. The .sx defcomps handle all component-to-component
wiring, iteration (map), and conditional rendering.

New .sx composition defcomps:
- headers.sx: ~market-header-from-data, ~market-desktop-nav-from-data,
  ~market-product-header-from-data, ~market-product-admin-header-from-data
- prices.sx: ~market-prices-header-from-data, ~market-card-price-from-data
- navigation.sx: ~market-mobile-nav-from-data
- cards.sx: ~market-product-cards-content, ~market-card-from-data,
  ~market-cards-content, ~market-landing-from-data
- detail.sx: ~market-product-detail-from-data, ~market-detail-gallery-from-data,
  ~market-detail-info-from-data
- meta.sx: ~market-product-meta-from-data
- filters.sx: ~market-desktop-filter-from-data, ~market-mobile-chips-from-data,
  ~market-mobile-filter-content-from-data, plus 6 sub-composition defcomps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 01:11:57 +00:00
parent 36a0bd8577
commit e81d77437e
12 changed files with 961 additions and 781 deletions

View File

@@ -1,274 +1,197 @@
"""Layout registration + header builders."""
"""Layout registration + header data builders."""
from __future__ import annotations
from typing import Any
from shared.sx.parser import SxExpr
from shared.sx.helpers import sx_call
from .utils import _set_prices, _price_str
# ---------------------------------------------------------------------------
# Header helpers
# Header data extraction — pure data, no component references
# ---------------------------------------------------------------------------
def _market_header_sx(ctx: dict, *, oob: bool = False) -> str:
"""Build the market-level header row as sx call string."""
from quart import url_for
market_title = ctx.get("market_title", "")
top_slug = ctx.get("top_slug", "")
sub_slug = ctx.get("sub_slug", "")
hx_select_search = ctx.get("hx_select_search", "#main-panel")
label_sx = sx_call(
"market-shop-label",
title=market_title, top_slug=top_slug or "",
sub_div=sub_slug or None,
)
link_href = url_for("defpage_market_home")
# Build desktop nav from categories
categories = ctx.get("categories", {})
qs = ctx.get("qs", "")
nav_sx = _desktop_category_nav_sx(ctx, categories, qs, hx_select_search)
return sx_call(
"menu-row-sx",
id="market-row", level=2,
link_href=link_href, link_label_content=label_sx,
nav=nav_sx or None,
child_id="market-header-child", oob=oob,
)
def _desktop_category_nav_sx(ctx: dict, categories: dict, qs: str,
hx_select: str) -> str:
"""Build desktop category navigation links as sx."""
def _market_header_data(ctx: dict) -> dict:
"""Extract market header data for .sx composition."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
category_label = ctx.get("category_label", "")
hx_select = ctx.get("hx_select_search", "#main-panel")
select_colours = ctx.get("select_colours", "")
rights = ctx.get("rights", {})
qs = ctx.get("qs", "")
categories = ctx.get("categories", {})
all_href = prefix + url_for("market.browse.browse_all") + qs
all_active = (category_label == "All Products")
link_parts = [sx_call(
"market-category-link",
href=all_href, hx_select=hx_select, active=all_active,
select_colours=select_colours, label="All",
)]
cat_data = []
for cat, data in categories.items():
cat_href = prefix + url_for("market.browse.browse_top", top_slug=data["slug"]) + qs
cat_active = (cat == category_label)
link_parts.append(sx_call(
"market-category-link",
href=cat_href, hx_select=hx_select, active=cat_active,
select_colours=select_colours, label=cat,
))
cat_data.append({
"href": cat_href,
"active": cat == ctx.get("category_label", ""),
"label": cat,
})
links_sx = "(<> " + " ".join(link_parts) + ")"
admin_sx = ""
admin_href = ""
if rights and rights.get("admin"):
admin_href = prefix + url_for("defpage_market_admin")
admin_sx = sx_call("market-admin-link", href=admin_href, hx_select=hx_select)
return sx_call("market-desktop-category-nav",
links=SxExpr(links_sx),
admin=admin_sx or None)
return {
"market-title": ctx.get("market_title", ""),
"top-slug": ctx.get("top_slug", ""),
"sub-slug": ctx.get("sub_slug", ""),
"link-href": url_for("defpage_market_home"),
"categories": cat_data,
"hx-select": hx_select,
"select-colours": select_colours,
"all-href": all_href,
"all-active": ctx.get("category_label", "") == "All Products",
"admin-href": admin_href,
}
def _product_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str:
"""Build the product-level header row as sx call string."""
def _market_header_sx(ctx: dict, *, oob: bool = False) -> str:
"""Build market header as sx — delegates to .sx defcomp."""
data = _market_header_data(ctx)
return sx_call("market-header-from-data", oob=oob, **data)
def _product_header_data(ctx: dict, d: dict) -> dict:
"""Extract product header data for .sx composition."""
from quart import url_for
from shared.browser.app.csrf import generate_csrf_token
slug = d.get("slug", "")
title = d.get("title", "")
hx_select_search = ctx.get("hx_select_search", "#main-panel")
link_href = url_for("market.browse.product.product_detail", product_slug=slug)
label_sx = sx_call("market-product-label", title=title)
# Prices in nav area
pr = _set_prices(d)
hx_select = ctx.get("hx_select_search", "#main-panel")
cart = ctx.get("cart", [])
prices_nav = _prices_header_sx(d, pr, cart, slug, ctx)
rights = ctx.get("rights", {})
nav_parts = [prices_nav]
if rights and rights.get("admin"):
admin_href = url_for("market.browse.product.admin", product_slug=slug)
nav_parts.append(sx_call("market-admin-link", href=admin_href, hx_select=hx_select_search))
nav_sx = "(<> " + " ".join(nav_parts) + ")"
return sx_call(
"menu-row-sx",
id="product-row", level=3,
link_href=link_href, link_label_content=label_sx,
nav=SxExpr(nav_sx), child_id="product-header-child", oob=oob,
)
def _prices_header_sx(d: dict, pr: dict, cart: list, slug: str, ctx: dict) -> str:
"""Build prices + add-to-cart for product header row as sx."""
from quart import url_for
from shared.browser.app.csrf import generate_csrf_token
csrf = generate_csrf_token()
cart_action = url_for("market.browse.product.cart", product_slug=slug)
cart_url_fn = ctx.get("cart_url")
# Add-to-cart button
quantity = sum(ci.quantity for ci in cart if ci.product.slug == slug) if cart else 0
add_sx = _cart_add_sx(slug, quantity, cart_action, csrf, cart_url_fn)
parts = [add_sx]
sp_val, rp_val = pr.get("sp_val"), pr.get("rp_val")
if sp_val:
parts.append(sx_call("market-header-price-special-label"))
parts.append(sx_call("market-header-price-special",
price=_price_str(sp_val, pr["sp_raw"], pr["sp_cur"])))
if rp_val:
parts.append(sx_call("market-header-price-strike",
price=_price_str(rp_val, pr["rp_raw"], pr["rp_cur"])))
elif rp_val:
parts.append(sx_call("market-header-price-regular-label"))
parts.append(sx_call("market-header-price-regular",
price=_price_str(rp_val, pr["rp_raw"], pr["rp_cur"])))
# Price data
pr = _set_prices(d)
sp_str = _price_str(pr["sp_val"], pr["sp_raw"], pr["sp_cur"]) if pr["sp_val"] else ""
rp_str = _price_str(pr["rp_val"], pr["rp_raw"], pr["rp_cur"]) if pr["rp_val"] else ""
# RRP
rrp_str = ""
rrp_raw = d.get("rrp_raw")
rrp_val = d.get("rrp")
case_size = d.get("case_size_count") or 1
if rrp_raw and rrp_val:
rrp_str = f"{rrp_raw[0]}{rrp_val * case_size:.2f}"
parts.append(sx_call("market-header-rrp", rrp=rrp_str))
inner_sx = "(<> " + " ".join(parts) + ")"
return sx_call("market-prices-row", inner=SxExpr(inner_sx))
def _cart_add_sx(slug: str, quantity: int, action: str, csrf: str,
cart_url_fn: Any = None) -> str:
"""Build add-to-cart button or quantity controls as sx."""
if not quantity:
return sx_call(
"market-cart-add-empty",
cart_id=f"cart-{slug}", action=action, csrf=csrf,
)
# Cart state
csrf = generate_csrf_token()
cart_action = url_for("market.browse.product.cart", product_slug=slug)
quantity = sum(ci.quantity for ci in cart if ci.product.slug == slug) if cart else 0
cart_url_fn = ctx.get("cart_url")
cart_href = cart_url_fn("/") if callable(cart_url_fn) else "/"
return sx_call(
"market-cart-add-quantity",
cart_id=f"cart-{slug}", action=action, csrf=csrf,
minus_val=str(quantity - 1), plus_val=str(quantity + 1),
quantity=str(quantity), cart_href=cart_href,
)
admin_href = ""
if rights and rights.get("admin"):
admin_href = url_for("market.browse.product.admin", product_slug=slug)
return {
"title": d.get("title", ""),
"link-href": url_for("market.browse.product.product_detail", product_slug=slug),
"hx-select": hx_select,
"price-data": {
"cart-id": f"cart-{slug}",
"cart-action": cart_action,
"csrf": csrf,
"quantity": quantity,
"cart-href": cart_href,
"sp-val": pr["sp_val"] or "",
"sp-str": sp_str,
"rp-val": pr["rp_val"] or "",
"rp-str": rp_str,
"rrp-str": rrp_str,
},
"admin-href": admin_href,
}
def _product_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str:
"""Build product header as sx — delegates to .sx defcomp."""
data = _product_header_data(ctx, d)
return sx_call("market-product-header-from-data", oob=oob, **data)
def _product_admin_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str:
"""Build product admin header as sx — delegates to .sx defcomp."""
from quart import url_for
slug = d.get("slug", "")
link_href = url_for("market.browse.product.admin", product_slug=slug)
return sx_call("market-product-admin-header-from-data",
link_href=link_href, oob=oob)
# ---------------------------------------------------------------------------
# Mobile nav panel
# Mobile nav panel data extraction
# ---------------------------------------------------------------------------
def _mobile_nav_panel_sx(ctx: dict) -> str:
"""Build mobile nav panel with category accordion as sx."""
def _mobile_nav_data(ctx: dict) -> dict:
"""Extract mobile nav panel data for .sx composition."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
categories = ctx.get("categories", {})
qs = ctx.get("qs", "")
category_label = ctx.get("category_label", "")
top_slug = ctx.get("top_slug", "")
sub_slug = ctx.get("sub_slug", "")
hx_select = ctx.get("hx_select_search", "#main-panel")
select_colours = ctx.get("select_colours", "")
all_href = prefix + url_for("market.browse.browse_all") + qs
all_active = (category_label == "All Products")
item_parts = [sx_call(
"market-mobile-all-link",
href=all_href, hx_select=hx_select, active=all_active,
select_colours=select_colours,
)]
cat_data = []
for cat, data in categories.items():
cat_slug = data.get("slug", "")
cat_active = (top_slug == cat_slug.lower() if top_slug else False)
cat_active = top_slug == cat_slug.lower() if top_slug else False
cat_href = prefix + url_for("market.browse.browse_top", top_slug=cat_slug) + qs
bg_cls = " bg-stone-900 text-white hover:bg-stone-900" if cat_active else ""
chevron_sx = sx_call("market-mobile-chevron")
cat_count = data.get("count", 0)
summary_sx = sx_call(
"market-mobile-cat-summary",
bg_cls=bg_cls, href=cat_href, hx_select=hx_select,
select_colours=select_colours, cat_name=cat,
count_label=f"{cat_count} products", count_str=str(cat_count),
chevron=chevron_sx,
)
subs = data.get("subs", [])
subs_sx = ""
sub_data = []
if subs:
sub_link_parts = []
for sub in subs:
sub_href = prefix + url_for("market.browse.browse_sub", top_slug=cat_slug, sub_slug=sub["slug"]) + qs
sub_active = (cat_active and sub_slug == sub.get("slug"))
sub_href = prefix + url_for("market.browse.browse_sub",
top_slug=cat_slug, sub_slug=sub["slug"]) + qs
sub_active = cat_active and sub_slug == sub.get("slug")
sub_label = sub.get("html_label") or sub.get("name", "")
sub_count = sub.get("count", 0)
sub_link_parts.append(sx_call(
"market-mobile-sub-link",
select_colours=select_colours, active=sub_active,
href=sub_href, hx_select=hx_select, label=sub_label,
count_label=f"{sub_count} products", count_str=str(sub_count),
))
sub_links_sx = "(<> " + " ".join(sub_link_parts) + ")"
subs_sx = sx_call("market-mobile-subs-panel", links=SxExpr(sub_links_sx))
else:
view_href = prefix + url_for("market.browse.browse_top", top_slug=cat_slug) + qs
subs_sx = sx_call("market-mobile-view-all", href=view_href, hx_select=hx_select)
sub_data.append({
"href": sub_href,
"active": sub_active,
"label": sub_label,
"count": sub.get("count", 0),
})
item_parts.append(sx_call(
"market-mobile-cat-details",
open=cat_active or None,
summary=summary_sx,
subs=subs_sx,
))
cat_data.append({
"name": cat,
"href": cat_href,
"active": cat_active,
"count": data.get("count", 0),
"subs": sub_data if sub_data else None,
})
items_sx = "(<> " + " ".join(item_parts) + ")"
return sx_call("market-mobile-nav-wrapper", items=SxExpr(items_sx))
return {
"categories": cat_data,
"all-href": all_href,
"all-active": ctx.get("category_label", "") == "All Products",
"hx-select": hx_select,
"select-colours": select_colours,
}
def _mobile_nav_panel_sx(ctx: dict) -> str:
"""Build mobile nav panel as sx — delegates to .sx defcomp."""
data = _mobile_nav_data(ctx)
return sx_call("market-mobile-nav-from-data", **data)
# ---------------------------------------------------------------------------
# Product admin header
# ---------------------------------------------------------------------------
def _product_admin_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str:
"""Build product admin header row as sx."""
from quart import url_for
slug = d.get("slug", "")
link_href = url_for("market.browse.product.admin", product_slug=slug)
return sx_call(
"menu-row-sx",
id="product-admin-row", level=4,
link_href=link_href, link_label="admin!!", icon="fa fa-cog",
child_id="product-admin-header-child", oob=oob,
)
# ===========================================================================
# Layout registration — all layouts delegate to .sx defcomps
# ===========================================================================
# ---------------------------------------------------------------------------
def _register_market_layouts() -> None:
from shared.sx.layouts import register_sx_layout