Replace env free-variable threading with IO-primitive auto-fetch macros
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m38s

Layout components now self-resolve context (cart-mini, auth-menu, nav-tree,
rights, URLs) via new IO primitives (root-header-ctx, select-colours,
account-nav-ctx, app-rights) and defmacro wrappers (~root-header-auto,
~auth-header-row-auto, ~root-mobile-auto). This eliminates _ctx_to_env(),
HELPER_CSS_CLASSES, and verbose :key threading across all 10 services.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 18:20:57 +00:00
parent 8be00df6d9
commit 7fda7a8027
41 changed files with 551 additions and 523 deletions

View File

@@ -1,33 +1,20 @@
;; Account layout defcomps — read ctx values from env free variables.
;; Account layout defcomps — fully self-contained via IO primitives.
;; Registered via register_sx_layout("account", ...) in __init__.py.
;; Free variables come from _ctx_to_env() in shared/sx/helpers.py.
;; Full page: root header + auth header row in header-child
(defcomp ~account-layout-full ()
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx
:inner (~auth-header-row :account-url account-url
:select-colours select-colours
:account-nav account-nav))))
:inner (~auth-header-row-auto))))
;; OOB (HTMX): auth row + root header, both with oob=true
(defcomp ~account-layout-oob ()
(<> (~auth-header-row :account-url account-url
:select-colours select-colours
:account-nav account-nav
:oob true)
(~root-header :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
:oob true)))
(<> (~auth-header-row-auto true)
(~root-header-auto true)))
;; Mobile menu: auth section + root nav
(defcomp ~account-layout-mobile ()
(<> (~mobile-menu-section
:label "account" :href "/" :level 1 :colour "sky"
:items (~auth-nav-items :account-url account-url
:select-colours select-colours
:account-nav account-nav))
(~root-mobile :nav-tree nav-tree :auth-menu auth-menu)))
:items (~auth-nav-items-auto))
(~root-mobile-auto)))

View File

@@ -170,6 +170,15 @@
(summary :class "cursor-pointer px-4 py-3 font-medium text-sm bg-stone-100 hover:bg-stone-200 select-none" title)
(div :class "p-4 overflow-x-auto text-xs" content)))
(defcomp ~blog-preview-rendered (&key html)
(div :class "blog-content prose max-w-none" (raw! html)))
(defcomp ~blog-preview-empty ()
(div :class "p-8 text-stone-500" "No content to preview."))
(defcomp ~blog-admin-placeholder ()
(div :class "pb-8"))
;; ---------------------------------------------------------------------------
;; Data-driven content defcomps (called from defpages with service data)
;; ---------------------------------------------------------------------------

View File

@@ -1,37 +1,38 @@
;; Blog layout defcomps — root header from env free variables,
;; blog-specific headers passed as &key params.
;; Blog layout defcomps — fully self-contained via IO primitives.
;; --- Blog layout (root + invisible blog header) ---
;; --- Blog header (invisible row for blog-header-child swap target) ---
(defcomp ~blog-layout-full (&key blog-header)
(<> (~root-header :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)
blog-header))
(defcomp ~blog-header (&key oob)
(~menu-row-sx :id "blog-row" :level 1
:link-label-content (div)
:child-id "blog-header-child" :oob oob))
;; --- Blog layout (root + blog header) ---
(defcomp ~blog-layout-full ()
(<> (~root-header-auto)
(~blog-header)))
;; --- Settings layout (root + settings header) ---
(defcomp ~settings-layout-full (&key settings-header)
(<> (~root-header :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)
(<> (~root-header-auto)
settings-header))
;; --- Sub-settings layout (root + settings + sub row) ---
(defcomp ~sub-settings-layout-full (&key settings-header sub-header)
(<> (~root-header :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)
(<> (~root-header-auto)
settings-header sub-header))
(defcomp ~sub-settings-layout-oob (&key settings-header-oob sub-header-oob)
(<> settings-header-oob sub-header-oob))
;; --- Settings nav links (replaces Python _settings_nav_sx loop) ---
;; --- Settings nav links — uses (select-colours) IO primitive ---
(defcomp ~blog-settings-nav (&key select-colours)
(let* ((links (list
(defcomp ~blog-settings-nav ()
(let* ((sc (select-colours))
(links (list
(dict :endpoint "menu_items.defpage_menu_items_page" :icon "fa fa-bars" :label "Menu Items")
(dict :endpoint "snippets.defpage_snippets_page" :icon "fa fa-puzzle-piece" :label "Snippets")
(dict :endpoint "blog.tag_groups_admin.defpage_tag_groups_page" :icon "fa fa-tags" :label "Tag Groups")
@@ -41,7 +42,7 @@
:href (url-for (get lnk "endpoint"))
:icon (get lnk "icon")
:label (get lnk "label")
:select-colours (or select-colours "")))
:select-colours (or sc "")))
links))))
;; --- Editor panel wrapper ---

View File

@@ -140,7 +140,8 @@ async def _h_editor_page_content(**kw):
async def _h_post_admin_content(slug=None, **kw):
await _ensure_post_data(slug)
return '(div :class "pb-8")'
from shared.sx.helpers import render_to_sx
return await render_to_sx("blog-admin-placeholder")
async def _h_post_data_content(slug=None, **kw):
@@ -264,7 +265,7 @@ async def _h_post_preview_content(slug=None, **kw):
from quart import g
from shared.services.registry import services
from shared.sx.helpers import render_to_sx
from shared.sx.parser import SxExpr, serialize as sx_serialize
from shared.sx.parser import SxExpr
preview = await services.blog_page.preview_data(g.s)
@@ -276,16 +277,16 @@ async def _h_post_preview_content(slug=None, **kw):
sections.append(await render_to_sx("blog-preview-section",
title="Lexical JSON", content=SxExpr(preview["json_pretty"])))
if preview.get("sx_rendered"):
rendered_sx = f'(div :class "blog-content prose max-w-none" (raw! {sx_serialize(preview["sx_rendered"])}))'
rendered_sx = await render_to_sx("blog-preview-rendered", html=preview["sx_rendered"])
sections.append(await render_to_sx("blog-preview-section",
title="SX Rendered", content=SxExpr(rendered_sx)))
if preview.get("lex_rendered"):
rendered_sx = f'(div :class "blog-content prose max-w-none" (raw! {sx_serialize(preview["lex_rendered"])}))'
rendered_sx = await render_to_sx("blog-preview-rendered", html=preview["lex_rendered"])
sections.append(await render_to_sx("blog-preview-section",
title="Lexical Rendered", content=SxExpr(rendered_sx)))
if not sections:
return '(div :class "p-8 text-stone-500" "No content to preview.")'
return await render_to_sx("blog-preview-empty")
inner = " ".join(sections)
return await render_to_sx("blog-preview-panel", sections=SxExpr(f"(<> {inner})"))

View File

@@ -8,15 +8,6 @@ from typing import Any
# Header helpers (moved from sx_components — thin render_to_sx wrappers)
# ---------------------------------------------------------------------------
async def _blog_header_sx(ctx: dict, *, oob: bool = False) -> str:
from shared.sx.helpers import render_to_sx
from shared.sx.parser import SxExpr
return await render_to_sx("menu-row-sx",
id="blog-row", level=1,
link_label_content=SxExpr("(div)"),
child_id="blog-header-child", oob=oob)
async def _settings_header_sx(ctx: dict, *, oob: bool = False) -> str:
from shared.sx.helpers import render_to_sx
from shared.sx.parser import SxExpr
@@ -36,8 +27,7 @@ async def _settings_header_sx(ctx: dict, *, oob: bool = False) -> str:
async def _settings_nav_sx(ctx: dict) -> str:
from shared.sx.helpers import render_to_sx
return await render_to_sx("blog-settings-nav",
select_colours=ctx.get("select_colours", ""))
return await render_to_sx("blog-settings-nav")
async def _sub_settings_header_sx(row_id: str, child_id: str, href: str,
@@ -76,33 +66,29 @@ def _register_blog_layouts() -> None:
# --- Blog layout (root + blog header) ---
async def _blog_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("blog-layout-full", _ctx_to_env(ctx),
blog_header=SxExpr(await _blog_header_sx(ctx)))
from shared.sx.helpers import render_to_sx_with_env
return await render_to_sx_with_env("blog-layout-full", {})
async def _blog_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
from shared.sx.parser import SxExpr
rows = await render_to_sx_with_env("blog-layout-full", _ctx_to_env(ctx),
blog_header=SxExpr(await _blog_header_sx(ctx)))
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
rows = await render_to_sx_with_env("blog-layout-full", {})
return await oob_header_sx("root-header-child", "blog-header-child", rows)
# --- Settings layout (root + settings header) ---
async def _settings_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("settings-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("settings-layout-full", {},
settings_header=SxExpr(await _settings_header_sx(ctx)))
async def _settings_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
rows = await render_to_sx_with_env("settings-layout-full", _ctx_to_env(ctx),
rows = await render_to_sx_with_env("settings-layout-full", {},
settings_header=SxExpr(await _settings_header_sx(ctx)))
return await oob_header_sx("root-header-child", "root-settings-header-child", rows)
@@ -115,10 +101,10 @@ async def _settings_mobile(ctx: dict, **kw: Any) -> str:
async def _sub_settings_full(ctx: dict, row_id: str, child_id: str,
endpoint: str, icon: str, label: str) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
from quart import url_for as qurl
return await render_to_sx_with_env("sub-settings-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("sub-settings-layout-full", {},
settings_header=SxExpr(await _settings_header_sx(ctx)),
sub_header=SxExpr(await _sub_settings_header_sx(
row_id, child_id, qurl(endpoint), icon, label, ctx)))
@@ -190,10 +176,10 @@ async def _tag_groups_oob(ctx: dict, **kw: Any) -> str:
async def _tag_group_edit_full(ctx: dict, **kw: Any) -> str:
from quart import request, url_for as qurl
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
g_id = (request.view_args or {}).get("id")
return await render_to_sx_with_env("sub-settings-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("sub-settings-layout-full", {},
settings_header=SxExpr(await _settings_header_sx(ctx)),
sub_header=SxExpr(await _sub_settings_header_sx(
"tag-groups-row", "tag-groups-header-child",

View File

@@ -3,6 +3,11 @@
(defcomp ~cart-page-label-img (&key src)
(img :src src :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"))
(defcomp ~cart-page-label (&key feature-image title)
(<> (when feature-image
(~cart-page-label-img :src feature-image))
(span title)))
(defcomp ~cart-all-carts-link (&key href)
(a :href href :class "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition"
(i :class "fa fa-arrow-left text-xs" :aria-hidden "true") "All carts"))

View File

@@ -1,12 +1,9 @@
;; Cart layout defcomps — root header from env free variables,
;; cart-specific headers passed as &key params.
;; Cart layout defcomps — fully self-contained via IO primitives.
;; --- cart-page layout: root + cart row + page-cart row ---
(defcomp ~cart-page-layout-full (&key cart-row page-cart-row)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx
:inner (<> cart-row
(~header-child-sx :id "cart-header-child" :inner page-cart-row)))))
@@ -19,40 +16,31 @@
;; --- cart-admin layout: root + post header + admin header ---
(defcomp ~cart-admin-layout-full (&key post-header admin-header)
(<> (~root-header :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)
(<> (~root-header-auto)
post-header admin-header))
;; --- orders-within-cart: root + auth-simple + orders ---
(defcomp ~cart-orders-layout-full (&key list-url)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx
:inner (<> (~auth-header-row-simple :account-url account-url)
:inner (<> (~auth-header-row-simple-auto)
(~header-child-sx :id "auth-header-child"
:inner (~orders-header-row :list-url list-url))))))
(defcomp ~cart-orders-layout-oob (&key list-url)
(<> (~auth-header-row-simple :account-url account-url :oob true)
(<> (~auth-header-row-simple-auto true)
(~oob-header-sx
:parent-id "auth-header-child"
:row (~orders-header-row :list-url list-url))
(~root-header :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
:oob true)))
(~root-header-auto true)))
;; --- order-detail-within-cart: root + auth-simple + orders + order ---
(defcomp ~cart-order-detail-layout-full (&key list-url detail-url order-label)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx
:inner (<> (~auth-header-row-simple :account-url account-url)
:inner (<> (~auth-header-row-simple-auto)
(~header-child-sx :id "auth-header-child"
:inner (<> (~orders-header-row :list-url list-url)
(~header-child-sx :id "orders-header-child"
@@ -67,10 +55,7 @@
:row (~menu-row-sx :id "order-row" :level 3 :colour "sky"
:link-href detail-url :link-label order-label
:icon "fa fa-gbp" :oob true))
(~root-header :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
:oob true)))
(~root-header-auto true)))
;; --- orders rows wrapper (for infinite scroll) ---

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
from typing import Any
from markupsafe import escape
from shared.sx.parser import SxExpr
@@ -73,11 +72,9 @@ async def _page_cart_header_sx(ctx: dict, page_post: Any, *, oob: bool = False)
from shared.sx.helpers import render_to_sx, call_url
slug = page_post.slug if page_post else ""
title = ((page_post.title if page_post else None) or "")[:160]
label_parts = []
if page_post and page_post.feature_image:
label_parts.append(await render_to_sx("cart-page-label-img", src=page_post.feature_image))
label_parts.append(f'(span "{escape(title)}")')
label_sx = "(<> " + " ".join(label_parts) + ")"
label_sx = await render_to_sx("cart-page-label",
feature_image=page_post.feature_image if page_post else None,
title=title)
nav_sx = await render_to_sx("cart-all-carts-link", href=call_url(ctx, "cart_url", "/"))
return await render_to_sx(
"menu-row-sx",
@@ -101,9 +98,9 @@ async def _cart_page_admin_header_sx(ctx: dict, page_post: Any, *, oob: bool = F
# ---------------------------------------------------------------------------
async def _cart_page_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
page_post = ctx.get("page_post")
env = _ctx_to_env(ctx)
env = {}
return await render_to_sx_with_env("cart-page-layout-full", env,
cart_row=SxExpr(await _cart_header_sx(ctx)),
page_cart_row=SxExpr(await _page_cart_header_sx(ctx, page_post)),
@@ -111,9 +108,9 @@ async def _cart_page_full(ctx: dict, **kw: Any) -> str:
async def _cart_page_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, root_header_sx
from shared.sx.helpers import render_to_sx_with_env, root_header_sx
page_post = ctx.get("page_post")
env = _ctx_to_env(ctx, oob=True)
env = {}
return await render_to_sx_with_env("cart-page-layout-oob", env,
root_header_oob=SxExpr(await root_header_sx(ctx, oob=True)),
cart_row_oob=SxExpr(await _cart_header_sx(ctx, oob=True)),
@@ -122,10 +119,10 @@ async def _cart_page_oob(ctx: dict, **kw: Any) -> str:
async def _cart_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
page_post = ctx.get("page_post")
selected = kw.get("selected", "")
env = _ctx_to_env(ctx)
env = {}
return await render_to_sx_with_env("cart-admin-layout-full", env,
post_header=SxExpr(await _post_header_sx(ctx, page_post)),
admin_header=SxExpr(await _cart_page_admin_header_sx(ctx, page_post, selected=selected)),

View File

@@ -7,7 +7,7 @@ from .utils import _serialize_order, _serialize_calendar_entry
async def render_orders_page(ctx, orders, page, total_pages, search, search_count, url_for_fn, qs_fn):
from shared.sx.helpers import render_to_sx, render_to_sx_with_env, _ctx_to_env, search_desktop_sx, search_mobile_sx, full_page_sx
from shared.sx.helpers import render_to_sx, render_to_sx_with_env, search_desktop_sx, search_mobile_sx, full_page_sx
from shared.utils import route_prefix
ctx["search"] = search
ctx["search_count"] = search_count
@@ -17,7 +17,7 @@ async def render_orders_page(ctx, orders, page, total_pages, search, search_coun
order_dicts = [_serialize_order(o) for o in orders]
content = await render_to_sx("orders-list-content", orders=order_dicts,
page=page, total_pages=total_pages, rows_url=list_url, detail_url_prefix=detail_url_prefix)
header_rows = await render_to_sx_with_env("cart-orders-layout-full", _ctx_to_env(ctx),
header_rows = await render_to_sx_with_env("cart-orders-layout-full", {},
list_url=list_url,
)
filt = await render_to_sx("order-list-header", search_mobile=SxExpr(await search_mobile_sx(ctx)))
@@ -49,7 +49,7 @@ async def render_orders_rows(ctx, orders, page, total_pages, url_for_fn, qs_fn):
async def render_orders_oob(ctx, orders, page, total_pages, search, search_count, url_for_fn, qs_fn):
from shared.sx.helpers import render_to_sx, render_to_sx_with_env, _ctx_to_env, search_desktop_sx, search_mobile_sx, oob_page_sx
from shared.sx.helpers import render_to_sx, render_to_sx_with_env, search_desktop_sx, search_mobile_sx, oob_page_sx
from shared.utils import route_prefix
ctx["search"] = search
ctx["search_count"] = search_count
@@ -59,7 +59,7 @@ async def render_orders_oob(ctx, orders, page, total_pages, search, search_count
order_dicts = [_serialize_order(o) for o in orders]
content = await render_to_sx("orders-list-content", orders=order_dicts,
page=page, total_pages=total_pages, rows_url=list_url, detail_url_prefix=detail_url_prefix)
oobs = await render_to_sx_with_env("cart-orders-layout-oob", _ctx_to_env(ctx, oob=True),
oobs = await render_to_sx_with_env("cart-orders-layout-oob", {},
list_url=list_url,
)
filt = await render_to_sx("order-list-header", search_mobile=SxExpr(await search_mobile_sx(ctx)))
@@ -67,7 +67,7 @@ async def render_orders_oob(ctx, orders, page, total_pages, search, search_count
async def render_order_page(ctx, order, calendar_entries, url_for_fn):
from shared.sx.helpers import render_to_sx, render_to_sx_with_env, _ctx_to_env, full_page_sx
from shared.sx.helpers import render_to_sx, render_to_sx_with_env, full_page_sx
from shared.utils import route_prefix
from shared.browser.app.csrf import generate_csrf_token
pfx = route_prefix()
@@ -80,7 +80,7 @@ async def render_order_page(ctx, order, calendar_entries, url_for_fn):
main = await render_to_sx("order-detail-content", order=order_data, calendar_entries=cal_data)
filt = await render_to_sx("order-detail-filter-content", order=order_data,
list_url=list_url, recheck_url=recheck_url, pay_url=pay_url, csrf=generate_csrf_token())
header_rows = await render_to_sx_with_env("cart-order-detail-layout-full", _ctx_to_env(ctx),
header_rows = await render_to_sx_with_env("cart-order-detail-layout-full", {},
list_url=list_url, detail_url=detail_url,
order_label=f"Order {order.id}",
)
@@ -88,7 +88,7 @@ async def render_order_page(ctx, order, calendar_entries, url_for_fn):
async def render_order_oob(ctx, order, calendar_entries, url_for_fn):
from shared.sx.helpers import render_to_sx, render_to_sx_with_env, _ctx_to_env, oob_page_sx
from shared.sx.helpers import render_to_sx, render_to_sx_with_env, oob_page_sx
from shared.utils import route_prefix
from shared.browser.app.csrf import generate_csrf_token
pfx = route_prefix()
@@ -101,7 +101,7 @@ async def render_order_oob(ctx, order, calendar_entries, url_for_fn):
main = await render_to_sx("order-detail-content", order=order_data, calendar_entries=cal_data)
filt = await render_to_sx("order-detail-filter-content", order=order_data,
list_url=list_url, recheck_url=recheck_url, pay_url=pay_url, csrf=generate_csrf_token())
oobs = await render_to_sx_with_env("cart-order-detail-layout-oob", _ctx_to_env(ctx, oob=True),
oobs = await render_to_sx_with_env("cart-order-detail-layout-oob", {},
detail_url=detail_url,
order_label=f"Order {order.id}",
)
@@ -109,11 +109,11 @@ async def render_order_oob(ctx, order, calendar_entries, url_for_fn):
async def render_checkout_error_page(ctx, error=None, order=None):
from shared.sx.helpers import render_to_sx, render_to_sx_with_env, _ctx_to_env, full_page_sx
from shared.sx.helpers import render_to_sx, render_to_sx_with_env, full_page_sx
from shared.infrastructure.urls import cart_url
err_msg = error or "Unexpected error while creating the hosted checkout session."
order_sx = await render_to_sx("checkout-error-order-id", oid=f"#{order.id}") if order else None
hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
hdr = await render_to_sx_with_env("layout-root-full", {})
filt = await render_to_sx("checkout-error-header")
content = await render_to_sx("checkout-error-content", msg=err_msg,
order=SxExpr(order_sx) if order_sx else None, back_url=cart_url("/"))

View File

@@ -24,3 +24,10 @@
(div :id (str "entry-title-" entry-id) :class "flex gap-1 items-center"
title times))
(defcomp ~events-slot-label (&key name description)
(div :class "flex flex-col md:flex-row md:gap-2 items-center"
(div :class "flex flex-row items-center gap-2"
(i :class "fa fa-clock")
(div :class "shrink-0" name))
(p :class "text-stone-500 whitespace-pre-line break-all w-full" description)))

View File

@@ -1,13 +1,11 @@
;; Events layout defcomps — root header from env free variables,
;; Events layout defcomps — root header via ~root-header-auto,
;; events-specific headers passed as &key params.
;; --- Calendar admin layout: root + post + child(admin + cal + cal-admin) ---
(defcomp ~events-cal-admin-layout-full (&key post-header admin-header
calendar-header calendar-admin-header)
(<> (~root-header :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)
(<> (~root-header-auto)
post-header
(~header-child-sx :inner (<> admin-header calendar-header calendar-admin-header))))
@@ -23,9 +21,7 @@
(defcomp ~events-slot-layout-full (&key post-header admin-header
calendar-header calendar-admin-header slot-header)
(<> (~root-header :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)
(<> (~root-header-auto)
post-header
(~header-child-sx :inner (<> admin-header calendar-header calendar-admin-header slot-header))))
@@ -36,9 +32,7 @@
(defcomp ~events-day-admin-layout-full (&key post-header admin-header
calendar-header day-header day-admin-header)
(<> (~root-header :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)
(<> (~root-header-auto)
post-header
(~header-child-sx :inner (<> admin-header calendar-header day-header day-admin-header))))
@@ -48,9 +42,7 @@
;; --- Entry layout: root + child(post + cal + day + entry) ---
(defcomp ~events-entry-layout-full (&key post-header calendar-header day-header entry-header)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx :inner (<> post-header calendar-header day-header entry-header))))
(defcomp ~events-entry-layout-oob (&key day-oob entry-oob-wrap clear-oob)
@@ -61,9 +53,7 @@
(defcomp ~events-entry-admin-layout-full (&key post-header admin-header
calendar-header day-header
entry-header entry-admin-header)
(<> (~root-header :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)
(<> (~root-header-auto)
post-header
(~header-child-sx :inner (<> admin-header calendar-header day-header
entry-header entry-admin-header))))
@@ -76,9 +66,7 @@
(defcomp ~events-ticket-types-layout-full (&key post-header calendar-header day-header
entry-header entry-admin-header
ticket-types-header)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx :inner (<> post-header calendar-header day-header
entry-header entry-admin-header ticket-types-header))))
@@ -90,9 +78,7 @@
(defcomp ~events-ticket-type-layout-full (&key post-header calendar-header day-header
entry-header entry-admin-header
ticket-types-header ticket-type-header)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx :inner (<> post-header calendar-header day-header
entry-header entry-admin-header
ticket-types-header ticket-type-header))))
@@ -103,9 +89,7 @@
;; --- Markets layout: root + child(post + markets) ---
(defcomp ~events-markets-layout-full (&key post-header markets-header)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx :inner (<> post-header markets-header))))
(defcomp ~events-markets-layout-oob (&key post-oob markets-oob-wrap)

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from shared.sx.helpers import (
call_url, render_to_sx, render_to_sx_with_env, _ctx_to_env,
call_url, render_to_sx, render_to_sx_with_env,
post_admin_header_sx,
)
from shared.sx.parser import SxExpr

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from markupsafe import escape
from shared.sx.helpers import render_to_sx, render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx, render_to_sx_with_env
from shared.sx.parser import SxExpr
from .utils import (

View File

@@ -228,11 +228,11 @@ def _register_events_layouts() -> None:
# --- Calendar admin layout (root + post + child(post-admin + calendar + cal-admin)) ---
async def _cal_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-cal-admin-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-cal-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
@@ -241,11 +241,11 @@ async def _cal_admin_full(ctx: dict, **kw: Any) -> str:
async def _cal_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-cal-admin-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-cal-admin-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_oob=SxExpr(await _calendar_header_sx(ctx, oob=True)),
cal_admin_oob_wrap=SxExpr(await oob_header_sx("calendar-header-child",
@@ -264,11 +264,11 @@ async def _slots_full(ctx: dict, **kw: Any) -> str:
async def _slots_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-slots-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-slots-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_admin_oob=SxExpr(await _calendar_admin_header_sx(ctx, oob=True)),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
@@ -281,11 +281,11 @@ async def _slots_oob(ctx: dict, **kw: Any) -> str:
# --- Slot detail layout (extends cal-admin with slot header) ---
async def _slot_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-slot-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-slot-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
@@ -295,11 +295,11 @@ async def _slot_full(ctx: dict, **kw: Any) -> str:
async def _slot_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-slot-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-slot-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_admin_oob=SxExpr(await _calendar_admin_header_sx(ctx, oob=True)),
slot_oob_wrap=SxExpr(await oob_header_sx("calendar-admin-header-child",
@@ -315,11 +315,11 @@ async def _slot_oob(ctx: dict, **kw: Any) -> str:
# --- Day admin layout (root + post + post-admin + child(cal + day + day-admin)) ---
async def _day_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-day-admin-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-day-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
@@ -329,11 +329,11 @@ async def _day_admin_full(ctx: dict, **kw: Any) -> str:
async def _day_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-day-admin-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-day-admin-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_oob=SxExpr(await _calendar_header_sx(ctx, oob=True)),
day_admin_oob_wrap=SxExpr(await oob_header_sx("day-header-child",
@@ -349,9 +349,9 @@ async def _day_admin_oob(ctx: dict, **kw: Any) -> str:
# --- Entry layout (root + child(post + cal + day + entry), + menu) ---
async def _entry_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-entry-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-entry-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
day_header=SxExpr(await _day_header_sx(ctx)),
@@ -360,9 +360,9 @@ async def _entry_full(ctx: dict, **kw: Any) -> str:
async def _entry_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-entry-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-entry-layout-oob", {},
day_oob=SxExpr(await _day_header_sx(ctx, oob=True)),
entry_oob_wrap=SxExpr(await oob_header_sx("day-header-child",
"entry-header-child", await _entry_header_html(ctx))),
@@ -376,11 +376,11 @@ async def _entry_oob(ctx: dict, **kw: Any) -> str:
# --- Entry admin layout (root + post + child(post-admin + cal + day + entry + entry-admin), + menu) ---
async def _entry_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-entry-admin-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-entry-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
@@ -391,11 +391,11 @@ async def _entry_admin_full(ctx: dict, **kw: Any) -> str:
async def _entry_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-entry-admin-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-entry-admin-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
entry_oob=SxExpr(await _entry_header_html(ctx, oob=True)),
entry_admin_oob_wrap=SxExpr(await oob_header_sx("entry-header-child",
@@ -412,9 +412,9 @@ async def _entry_admin_oob(ctx: dict, **kw: Any) -> str:
# --- Ticket types layout (extends entry admin with ticket-types header, + menu) ---
async def _ticket_types_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-types-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-ticket-types-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
day_header=SxExpr(await _day_header_sx(ctx)),
@@ -425,9 +425,9 @@ async def _ticket_types_full(ctx: dict, **kw: Any) -> str:
async def _ticket_types_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-types-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-ticket-types-layout-oob", {},
entry_admin_oob=SxExpr(await _entry_admin_header_html(ctx, oob=True)),
ticket_types_oob_wrap=SxExpr(await oob_header_sx("entry-admin-header-child",
"ticket_types-header-child", await _ticket_types_header_html(ctx))),
@@ -437,9 +437,9 @@ async def _ticket_types_oob(ctx: dict, **kw: Any) -> str:
# --- Ticket type detail layout (extends ticket types with ticket-type header, + menu) ---
async def _ticket_type_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-type-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-ticket-type-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
day_header=SxExpr(await _day_header_sx(ctx)),
@@ -451,9 +451,9 @@ async def _ticket_type_full(ctx: dict, **kw: Any) -> str:
async def _ticket_type_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-type-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-ticket-type-layout-oob", {},
ticket_types_oob=SxExpr(await _ticket_types_header_html(ctx, oob=True)),
ticket_type_oob_wrap=SxExpr(await oob_header_sx("ticket_types-header-child",
"ticket_type-header-child", await _ticket_type_header_html(ctx))),
@@ -463,18 +463,18 @@ async def _ticket_type_oob(ctx: dict, **kw: Any) -> str:
# --- Markets layout (root + child(post + markets)) ---
async def _markets_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-markets-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-markets-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
markets_header=SxExpr(await _markets_header_sx(ctx)),
)
async def _markets_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-markets-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-markets-layout-oob", {},
post_oob=SxExpr(await _post_header_sx(ctx, oob=True)),
markets_oob_wrap=SxExpr(await oob_header_sx("post-header-child",
"markets-header-child", await _markets_header_sx(ctx))),

View File

@@ -35,11 +35,11 @@ def _register_events_layouts() -> None:
# --- Calendar admin layout (root + post + child(post-admin + calendar + cal-admin)) ---
async def _cal_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-cal-admin-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-cal-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
@@ -48,11 +48,11 @@ async def _cal_admin_full(ctx: dict, **kw: Any) -> str:
async def _cal_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-cal-admin-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-cal-admin-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_oob=SxExpr(await _calendar_header_sx(ctx, oob=True)),
cal_admin_oob_wrap=SxExpr(await oob_header_sx("calendar-header-child",
@@ -71,11 +71,11 @@ async def _slots_full(ctx: dict, **kw: Any) -> str:
async def _slots_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-slots-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-slots-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_admin_oob=SxExpr(await _calendar_admin_header_sx(ctx, oob=True)),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
@@ -88,11 +88,11 @@ async def _slots_oob(ctx: dict, **kw: Any) -> str:
# --- Slot detail layout (extends cal-admin with slot header) ---
async def _slot_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-slot-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-slot-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
@@ -102,11 +102,11 @@ async def _slot_full(ctx: dict, **kw: Any) -> str:
async def _slot_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-slot-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-slot-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_admin_oob=SxExpr(await _calendar_admin_header_sx(ctx, oob=True)),
slot_oob_wrap=SxExpr(await oob_header_sx("calendar-admin-header-child",
@@ -122,11 +122,11 @@ async def _slot_oob(ctx: dict, **kw: Any) -> str:
# --- Day admin layout (root + post + post-admin + child(cal + day + day-admin)) ---
async def _day_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-day-admin-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-day-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
@@ -136,11 +136,11 @@ async def _day_admin_full(ctx: dict, **kw: Any) -> str:
async def _day_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-day-admin-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-day-admin-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_oob=SxExpr(await _calendar_header_sx(ctx, oob=True)),
day_admin_oob_wrap=SxExpr(await oob_header_sx("day-header-child",
@@ -156,9 +156,9 @@ async def _day_admin_oob(ctx: dict, **kw: Any) -> str:
# --- Entry layout (root + child(post + cal + day + entry), + menu) ---
async def _entry_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-entry-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-entry-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
day_header=SxExpr(await _day_header_sx(ctx)),
@@ -167,9 +167,9 @@ async def _entry_full(ctx: dict, **kw: Any) -> str:
async def _entry_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-entry-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-entry-layout-oob", {},
day_oob=SxExpr(await _day_header_sx(ctx, oob=True)),
entry_oob_wrap=SxExpr(await oob_header_sx("day-header-child",
"entry-header-child", await _entry_header_html(ctx))),
@@ -183,11 +183,11 @@ async def _entry_oob(ctx: dict, **kw: Any) -> str:
# --- Entry admin layout (root + post + child(post-admin + cal + day + entry + entry-admin), + menu) ---
async def _entry_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-entry-admin-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-entry-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
@@ -198,11 +198,11 @@ async def _entry_admin_full(ctx: dict, **kw: Any) -> str:
async def _entry_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-entry-admin-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-entry-admin-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
entry_oob=SxExpr(await _entry_header_html(ctx, oob=True)),
entry_admin_oob_wrap=SxExpr(await oob_header_sx("entry-header-child",
@@ -219,9 +219,9 @@ async def _entry_admin_oob(ctx: dict, **kw: Any) -> str:
# --- Ticket types layout (extends entry admin with ticket-types header, + menu) ---
async def _ticket_types_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-types-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-ticket-types-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
day_header=SxExpr(await _day_header_sx(ctx)),
@@ -232,9 +232,9 @@ async def _ticket_types_full(ctx: dict, **kw: Any) -> str:
async def _ticket_types_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-types-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-ticket-types-layout-oob", {},
entry_admin_oob=SxExpr(await _entry_admin_header_html(ctx, oob=True)),
ticket_types_oob_wrap=SxExpr(await oob_header_sx("entry-admin-header-child",
"ticket_types-header-child", await _ticket_types_header_html(ctx))),
@@ -244,9 +244,9 @@ async def _ticket_types_oob(ctx: dict, **kw: Any) -> str:
# --- Ticket type detail layout (extends ticket types with ticket-type header, + menu) ---
async def _ticket_type_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-type-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-ticket-type-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
day_header=SxExpr(await _day_header_sx(ctx)),
@@ -258,9 +258,9 @@ async def _ticket_type_full(ctx: dict, **kw: Any) -> str:
async def _ticket_type_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-type-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-ticket-type-layout-oob", {},
ticket_types_oob=SxExpr(await _ticket_types_header_html(ctx, oob=True)),
ticket_type_oob_wrap=SxExpr(await oob_header_sx("ticket_types-header-child",
"ticket_type-header-child", await _ticket_type_header_html(ctx))),
@@ -270,18 +270,18 @@ async def _ticket_type_oob(ctx: dict, **kw: Any) -> str:
# --- Markets layout (root + child(post + markets)) ---
async def _markets_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-markets-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("events-markets-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
markets_header=SxExpr(await _markets_header_sx(ctx)),
)
async def _markets_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-markets-layout-oob", _ctx_to_env(ctx, oob=True),
return await render_to_sx_with_env("events-markets-layout-oob", {},
post_oob=SxExpr(await _post_header_sx(ctx, oob=True)),
markets_oob_wrap=SxExpr(await oob_header_sx("post-header-child",
"markets-header-child", await _markets_header_sx(ctx))),

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from shared.sx.helpers import (
render_to_sx_with_env, _ctx_to_env,
render_to_sx_with_env,
post_admin_header_sx, oob_header_sx,
header_child_sx, full_page_sx, oob_page_sx,
)
@@ -44,7 +44,7 @@ async def render_all_events_page(ctx: dict, entries, has_more, pending_tickets,
ctx, entries, has_more, pending_tickets, page_info,
page, view, ticket_url, next_url, events_url,
)
hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
hdr = await render_to_sx_with_env("layout-root-full", {})
return await full_page_sx(ctx, header_rows=hdr, content=content)
@@ -105,7 +105,7 @@ async def render_page_summary_page(ctx: dict, entries, has_more, pending_tickets
is_page_scoped=True, post=post,
)
hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
hdr = await render_to_sx_with_env("layout-root-full", {})
hdr += await header_child_sx(await _post_header_sx(ctx))
return await full_page_sx(ctx, header_rows=hdr, content=content)
@@ -160,7 +160,7 @@ async def render_calendars_page(ctx: dict) -> str:
content = await _calendars_main_panel_sx(ctx)
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
root_hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
root_hdr = await render_to_sx_with_env("layout-root-full", {})
post_hdr = await _post_header_sx(ctx)
admin_hdr = await post_admin_header_sx(ctx, slug, selected="calendars")
return await full_page_sx(ctx, header_rows=root_hdr + post_hdr + admin_hdr, content=content)
@@ -184,7 +184,7 @@ async def render_calendars_oob(ctx: dict) -> str:
async def render_calendar_page(ctx: dict) -> str:
"""Full page: calendar month view."""
content = await _calendar_main_panel_html(ctx)
hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
hdr = await render_to_sx_with_env("layout-root-full", {})
child = await _post_header_sx(ctx) + await _calendar_header_sx(ctx)
hdr += await header_child_sx(child)
return await full_page_sx(ctx, header_rows=hdr, content=content)
@@ -208,7 +208,7 @@ async def render_calendar_oob(ctx: dict) -> str:
async def render_day_page(ctx: dict) -> str:
"""Full page: day detail."""
content = await _day_main_panel_html(ctx)
hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
hdr = await render_to_sx_with_env("layout-root-full", {})
child = (await _post_header_sx(ctx)
+ await _calendar_header_sx(ctx) + await _day_header_sx(ctx))
hdr += await header_child_sx(child)

View File

@@ -1,7 +1,6 @@
"""Slot panels, forms, edit/add, slot picker JS."""
from __future__ import annotations
from markupsafe import escape
from shared.sx.helpers import render_to_sx
from shared.sx.parser import SxExpr
@@ -156,20 +155,12 @@ async def _slot_header_html(ctx: dict, *, oob: bool = False) -> str:
if not slot:
return ""
# Label: icon + name + description
desc = getattr(slot, "description", "") or ""
label_inner = (
f'<div class="flex flex-col md:flex-row md:gap-2 items-center">'
f'<div class="flex flex-row items-center gap-2">'
f'<i class="fa fa-clock"></i>'
f'<div class="shrink-0">{escape(slot.name)}</div>'
f'</div>'
f'<p class="text-stone-500 whitespace-pre-line break-all w-full">{escape(desc)}</p>'
f'</div>'
)
label_sx = await render_to_sx("events-slot-label",
name=slot.name, description=desc)
return await render_to_sx("menu-row-sx", id="slot-row", level=5,
link_label_content=SxExpr(label_inner),
link_label_content=SxExpr(label_sx),
child_id="slot-header-child", oob=oob)

View File

@@ -17,7 +17,8 @@ from shared.sx.parser import SxExpr
def _clear_oob(*ids: str) -> str:
"""Generate OOB swaps to remove orphaned header rows/children."""
return "".join(f'(div :id "{i}" :hx-swap-oob "outerHTML")' for i in ids)
from shared.sx.helpers import sx_call
return "".join(sx_call("clear-oob-div", id=i) for i in ids)
# All possible header row/child IDs at each depth (deepest first)

View File

@@ -3,9 +3,7 @@
;; Full page: root header + social header in header-child
(defcomp ~social-layout-full ()
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx
:inner (~federation-social-header
:nav (~federation-social-nav :actor actor)))))
@@ -16,7 +14,4 @@
:parent-id "root-header-child"
:row (~federation-social-header
:nav (~federation-social-nav :actor actor)))
(~root-header :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
:oob true)))
(~root-header-auto true)))

View File

@@ -25,11 +25,10 @@ def _serialize_remote_actor(a) -> dict:
async def _social_page(ctx: dict, actor, *, content: str,
title: str = "Rose Ash", meta_html: str = "") -> str:
"""Build a full social page with social header."""
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, full_page_sx
from shared.sx.helpers import render_to_sx_with_env, full_page_sx
from markupsafe import escape
env = _ctx_to_env(ctx)
env["actor"] = _serialize_actor(actor) if actor else None
env = {"actor": _serialize_actor(actor) if actor else None}
header_rows = await render_to_sx_with_env("social-layout-full", env)
return await full_page_sx(ctx, header_rows=header_rows, content=content,
meta_html=meta_html or f'<title>{escape(title)}</title>')
@@ -58,14 +57,12 @@ def _actor_data(ctx: dict) -> dict | None:
async def _social_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
env = _ctx_to_env(ctx)
env["actor"] = kw.get("actor") or _actor_data(ctx)
from shared.sx.helpers import render_to_sx_with_env
env = {"actor": kw.get("actor") or _actor_data(ctx)}
return await render_to_sx_with_env("social-layout-full", env)
async def _social_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
env = _ctx_to_env(ctx, oob=True)
env["actor"] = kw.get("actor") or _actor_data(ctx)
from shared.sx.helpers import render_to_sx_with_env
env = {"actor": kw.get("actor") or _actor_data(ctx)}
return await render_to_sx_with_env("social-layout-oob", env)

View File

@@ -1,11 +1,14 @@
;; Market grid and layout components
(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))
(<> (div :class "max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" cards) (div :class "pb-8")))
(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-admin-content-wrap (&key inner)
(div :id "main-panel" inner))
(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

@@ -4,7 +4,7 @@
(div :class "font-bold text-xl flex-shrink-0 flex gap-2 items-center"
(div (i :class "fa fa-shop") " " title)
(div :class "flex flex-col md:flex-row md:gap-2 text-xs"
(div top-slug) sub-div)))
(div top-slug) (when sub-div (div sub-div)))))
(defcomp ~market-product-label (&key title)
(<> (i :class "fa fa-shopping-bag" :aria-hidden "true") (div title)))

View File

@@ -1,12 +1,10 @@
;; Market layout defcomps — root header from env free variables,
;; Market layout defcomps — root header via ~root-header-auto,
;; market-specific headers passed as &key params.
;; --- Browse layout: root + post header + market header ---
(defcomp ~market-browse-layout-full (&key post-header market-header)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx :inner (<> post-header market-header))))
(defcomp ~market-browse-layout-oob (&key oob-header post-header-oob clear-oob)
@@ -15,25 +13,19 @@
;; --- Product layout: root + post + market + product ---
(defcomp ~market-product-layout-full (&key post-header market-header product-header)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx :inner (<> post-header market-header product-header))))
;; --- Product admin layout: root + post + market + product + admin ---
(defcomp ~market-product-admin-layout-full (&key post-header market-header product-header admin-header)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx :inner (<> post-header market-header product-header admin-header))))
;; --- Market admin layout: root + post + market + market-admin ---
(defcomp ~market-admin-layout-full (&key post-header market-header admin-header)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx :inner (<> post-header market-header admin-header))))
(defcomp ~market-admin-layout-oob (&key market-header-oob admin-oob-header clear-oob)

View File

@@ -122,8 +122,7 @@ async def _h_all_markets_content(**kw):
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
cards = await _market_cards_sx(markets, page_info, page, has_more, next_url)
content = await _markets_grid(cards)
return "(<> " + content + " " + '(div :class "pb-8")' + ")"
return await _markets_grid(cards)
async def _h_page_markets_content(slug=None, **kw):
@@ -146,15 +145,16 @@ async def _h_page_markets_content(slug=None, **kw):
cards = await _market_cards_sx(markets, {}, page, has_more, next_url,
show_page_badge=False, post_slug=post_slug)
content = await _markets_grid(cards)
return "(<> " + content + " " + '(div :class "pb-8")' + ")"
return await _markets_grid(cards)
async def _h_page_admin_content(slug=None, **kw):
from shared.sx.page import get_template_context
from shared.sx.helpers import render_to_sx
from shared.sx.parser import SxExpr
ctx = await get_template_context()
content = await _markets_admin_panel_sx(ctx)
return '(div :id "main-panel" ' + content + ')'
return await render_to_sx("market-admin-content-wrap", inner=SxExpr(content))
async def _h_market_home_content(page_slug=None, market_slug=None, **kw):

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from shared.sx.parser import serialize, SxExpr
from shared.sx.parser import SxExpr
from shared.sx.helpers import (
render_to_sx,
post_header_sx as _post_header_sx,
@@ -28,11 +28,10 @@ async 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 = f'(div {serialize(sub_slug)})' if sub_slug else ""
label_sx = await render_to_sx(
"market-shop-label",
title=market_title, top_slug=top_slug or "",
sub_div=SxExpr(sub_div) if sub_div else None,
sub_div=sub_slug or None,
)
link_href = url_for("defpage_market_home")
@@ -294,8 +293,8 @@ def _register_market_layouts() -> None:
async def _market_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
return await render_to_sx_with_env("market-browse-layout-full", _ctx_to_env(ctx),
from shared.sx.helpers import render_to_sx_with_env
return await render_to_sx_with_env("market-browse-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
market_header=SxExpr(await _market_header_sx(ctx)))
@@ -315,9 +314,9 @@ async def _market_mobile(ctx: dict, **kw: Any) -> str:
async def _market_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
selected = kw.get("selected", "")
return await render_to_sx_with_env("market-admin-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("market-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
market_header=SxExpr(await _market_header_sx(ctx)),
admin_header=SxExpr(await _market_admin_header_sx(ctx, selected=selected)))

View File

@@ -35,8 +35,8 @@ async def render_browse_page(ctx: dict) -> str:
cards = await _product_cards_sx(ctx)
content = await _product_grid(cards)
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
hdr = await render_to_sx_with_env("market-browse-layout-full", _ctx_to_env(ctx),
from shared.sx.helpers import render_to_sx_with_env
hdr = await render_to_sx_with_env("market-browse-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
market_header=SxExpr(await _market_header_sx(ctx)))
menu = await _mobile_nav_panel_sx(ctx)
@@ -81,8 +81,8 @@ async def render_product_page(ctx: dict, d: dict) -> str:
content = await _product_detail_sx(d, ctx)
meta = await _product_meta_sx(d, ctx)
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
hdr = await render_to_sx_with_env("market-product-layout-full", _ctx_to_env(ctx),
from shared.sx.helpers import render_to_sx_with_env
hdr = await render_to_sx_with_env("market-product-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
market_header=SxExpr(await _market_header_sx(ctx)),
product_header=SxExpr(await _product_header_sx(ctx, d)))
@@ -112,8 +112,8 @@ async def render_product_admin_page(ctx: dict, d: dict) -> str:
"""Full page: product admin."""
content = await _product_detail_sx(d, ctx)
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
hdr = await render_to_sx_with_env("market-product-admin-layout-full", _ctx_to_env(ctx),
from shared.sx.helpers import render_to_sx_with_env
hdr = await render_to_sx_with_env("market-product-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
market_header=SxExpr(await _market_header_sx(ctx)),
product_header=SxExpr(await _product_header_sx(ctx, d)),

View File

@@ -22,8 +22,9 @@ _MARKET_DEEP_IDS = [
def _clear_deeper_oob(*keep_ids: str) -> str:
"""Clear all market header rows/children NOT in keep_ids."""
from shared.sx.helpers import sx_call
to_clear = [i for i in _MARKET_DEEP_IDS if i not in keep_ids]
return " ".join(f'(div :id "{i}" :sx-swap-oob "outerHTML")' for i in to_clear)
return " ".join(sx_call("clear-oob-div", id=i) for i in to_clear)
# ---------------------------------------------------------------------------

View File

@@ -1,44 +1,30 @@
;; Orders layout defcomps — read ctx values from env free variables.
;; Orders layout defcomps — fully self-contained via IO primitives.
;; Registered via register_sx_layout("orders", ...) in __init__.py.
;; --- orders layout: root + auth + orders rows ---
(defcomp ~orders-layout-full (&key list-url)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx
:inner (<> (~auth-header-row :account-url account-url
:select-colours select-colours
:account-nav account-nav)
:inner (<> (~auth-header-row-auto)
(~orders-header-row :list-url (or list-url "/"))))))
(defcomp ~orders-layout-oob (&key list-url)
(<> (~auth-header-row :account-url account-url
:select-colours select-colours
:account-nav account-nav
:oob true)
(<> (~auth-header-row-auto true)
(~oob-header-sx
:parent-id "auth-header-child"
:row (~orders-header-row :list-url (or list-url "/")))
(~root-header :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
:oob true)))
(~root-header-auto true)))
(defcomp ~orders-layout-mobile ()
(~root-mobile :nav-tree nav-tree :auth-menu auth-menu))
(~root-mobile-auto))
;; --- order-detail layout: root + auth + orders + order rows ---
(defcomp ~order-detail-layout-full (&key list-url detail-url)
(<> (~root-header :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)
(<> (~root-header-auto)
(~order-detail-header-stack
:auth (~auth-header-row :account-url account-url
:select-colours select-colours
:account-nav account-nav)
:auth (~auth-header-row-auto)
:orders (~orders-header-row :list-url (or list-url "/"))
:order (~menu-row-sx :id "order-row" :level 3 :colour "sky"
:link-href (or detail-url "/") :link-label "Order"
@@ -50,10 +36,7 @@
:row (~menu-row-sx :id "order-row" :level 3 :colour "sky"
:link-href (or detail-url "/") :link-label "Order"
:icon "fa fa-gbp" :oob true))
(~root-header :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
:oob true)))
(~root-header-auto true)))
(defcomp ~order-detail-layout-mobile ()
(~root-mobile :nav-tree nav-tree :auth-menu auth-menu))
(~root-mobile-auto))

View File

@@ -16,34 +16,6 @@ from .page import SEARCH_HEADERS_MOBILE, SEARCH_HEADERS_DESKTOP
from .parser import SxExpr
# ---------------------------------------------------------------------------
# Pre-computed CSS classes for inline sx built by Python helpers
# ---------------------------------------------------------------------------
# These :class strings appear in post_header_sx / post_admin_header_sx etc.
# They're static — scan once at import time so they aren't re-scanned per request.
_HELPER_CLASS_SOURCES = [
':class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"',
':class "relative nav-group"',
':class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3"',
':class "!bg-stone-500 !text-white"',
':class "fa fa-cog"',
':class "fa fa-shield-halved"',
':class "text-white"',
':class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded !bg-stone-500 !text-white p-3"',
]
def _scan_helper_classes() -> frozenset[str]:
"""Scan the static class strings from helper functions once."""
from .css_registry import scan_classes_from_sx
combined = " ".join(_HELPER_CLASS_SOURCES)
return frozenset(scan_classes_from_sx(combined))
HELPER_CSS_CLASSES: frozenset[str] = _scan_helper_classes()
def call_url(ctx: dict, key: str, path: str = "/") -> str:
"""Call a URL helper from context (e.g., blog_url, account_url)."""
fn = ctx.get(key)
@@ -141,12 +113,8 @@ async def _post_nav_items_sx(ctx: dict) -> str:
container_nav = str(ctx.get("container_nav") or "").strip()
# Skip empty fragment wrappers like "(<> )"
if container_nav and container_nav.replace("(<>", "").replace(")", "").strip():
parts.append(
f'(div :id "entries-calendars-nav-wrapper"'
f' :class "flex flex-col sm:flex-row sm:items-center gap-2'
f' border-r border-stone-200 mr-2 sm:max-w-2xl"'
f' {container_nav})'
)
parts.append(await render_to_sx("container-nav-wrapper",
content=SxExpr(container_nav)))
# Admin cog
admin_nav = ctx.get("post_admin_nav")
@@ -157,15 +125,9 @@ async def _post_nav_items_sx(ctx: dict) -> str:
from quart import request
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
is_admin_page = ctx.get("is_admin_section") or "/admin" in request.path
sel_cls = "!bg-stone-500 !text-white" if is_admin_page else ""
base_cls = ("justify-center cursor-pointer flex flex-row"
" items-center gap-2 rounded bg-stone-200 text-black p-3")
admin_nav = (
f'(div :class "relative nav-group"'
f' (a :href "{admin_href}"'
f' :class "{base_cls} {sel_cls}"'
f' (i :class "fa fa-cog" :aria-hidden "true")))'
)
admin_nav = await render_to_sx("admin-cog-button",
href=admin_href,
is_admin_page=is_admin_page or None)
if admin_nav:
parts.append(admin_nav)
return "(<> " + " ".join(parts) + ")" if parts else ""
@@ -282,10 +244,8 @@ async def post_admin_header_sx(ctx: dict, slug: str, *, oob: bool = False,
selected: str = "", admin_href: str = "") -> str:
"""Post admin header row as sx wire format."""
# Label
label_parts = ['(i :class "fa fa-shield-halved" :aria-hidden "true")', '" admin"']
if selected:
label_parts.append(f'(span :class "text-white" "{escape(selected)}")')
label_sx = "(<> " + " ".join(label_parts) + ")"
label_sx = await render_to_sx("post-admin-label",
selected=str(escape(selected)) if selected else None)
nav_sx = await _post_admin_nav_items_sx(ctx, slug, selected) or None
@@ -385,44 +345,6 @@ def _build_component_ast(__name: str, **kwargs: Any) -> list:
return ast
def _ctx_to_env(ctx: dict, *, oob: bool = False) -> dict:
"""Convert template context dict → SX evaluation env dict.
Applies ``_as_sx()`` to HTML fragments, ``call_url()`` to URL helpers,
extracts rights/admin flags. Returns kebab-case keys matching SX
symbol conventions so .sx defcomps can read them as free variables.
"""
rights = ctx.get("rights") or {}
is_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
env = {
# Root header values (match ~header-row-sx &key params)
"cart-mini": _as_sx(ctx.get("cart_mini")),
"blog-url": call_url(ctx, "blog_url", ""),
"site-title": ctx.get("base_title", ""),
"app-label": ctx.get("app_label", ""),
"nav-tree": _as_sx(ctx.get("nav_tree")),
"auth-menu": _as_sx(ctx.get("auth_menu")),
"nav-panel": _as_sx(ctx.get("nav_panel")),
"settings-url": call_url(ctx, "blog_url", "/settings/") if is_admin else "",
"is-admin": is_admin,
"oob": oob,
# URL helpers (pre-resolved to strings)
"account-url": call_url(ctx, "account_url", ""),
"events-url": call_url(ctx, "events_url", ""),
"market-url": call_url(ctx, "market_url", ""),
"cart-url": call_url(ctx, "cart_url", ""),
# Common values
"select-colours": ctx.get("select_colours", ""),
"rights": rights,
# Fragments (used by various services)
"container-nav": _as_sx(ctx.get("container_nav")),
"account-nav": _as_sx(ctx.get("account_nav")),
# Post context
"post": ctx.get("post") or {},
}
return env
async def render_to_sx_with_env(__name: str, extra_env: dict, **kwargs: Any) -> str:
"""Like ``render_to_sx`` but merges *extra_env* into the evaluation
environment before eval. Used by ``register_sx_layout`` so .sx
@@ -584,8 +506,6 @@ def sx_response(source: str, status: int = 200,
cumulative_classes: set[str] = set()
if registry_loaded():
new_classes = scan_classes_from_sx(source)
# Include pre-computed helper classes (menu bars, admin nav, etc.)
new_classes.update(HELPER_CSS_CLASSES)
if comp_defs:
# Scan only the component definitions actually being sent
new_classes.update(scan_classes_from_sx(comp_defs))
@@ -717,8 +637,6 @@ def sx_page(ctx: dict, page_sx: str, *,
for val in _COMPONENT_ENV.values():
if isinstance(val, Component) and val.css_classes:
classes.update(val.css_classes)
# Include pre-computed helper classes (menu bars, admin nav, etc.)
classes.update(HELPER_CSS_CLASSES)
# Page sx is unique per request — scan it
classes.update(scan_classes_from_sx(page_sx))
# Always include body classes

View File

@@ -146,30 +146,27 @@ def register_sx_layout(name: str, full_defcomp: str, oob_defcomp: str,
mobile_defcomp: str | None = None) -> None:
"""Register a layout that delegates entirely to .sx defcomps.
The defcomps read ctx values as free variables from the evaluation
environment (populated by ``_ctx_to_env``). Python layouts become
one-liners::
Layout defcomps use IO primitives (via auto-fetching macros) to
self-populate — no Python env injection needed. Any extra kwargs
from the caller are passed as kebab-case env entries::
register_sx_layout("account", "account-layout-full",
"account-layout-oob", "account-layout-mobile")
"""
from .helpers import render_to_sx_with_env, _ctx_to_env
from .helpers import render_to_sx_with_env
async def full_fn(ctx: dict, **kw: Any) -> str:
env = _ctx_to_env(ctx)
env.update({k.replace("_", "-"): v for k, v in kw.items()})
env = {k.replace("_", "-"): v for k, v in kw.items()}
return await render_to_sx_with_env(full_defcomp, env)
async def oob_fn(ctx: dict, **kw: Any) -> str:
env = _ctx_to_env(ctx, oob=True)
env.update({k.replace("_", "-"): v for k, v in kw.items()})
env = {k.replace("_", "-"): v for k, v in kw.items()}
return await render_to_sx_with_env(oob_defcomp, env)
mobile_fn = None
if mobile_defcomp:
async def mobile_fn(ctx: dict, **kw: Any) -> str:
env = _ctx_to_env(ctx)
env.update({k.replace("_", "-"): v for k, v in kw.items()})
env = {k.replace("_", "-"): v for k, v in kw.items()}
return await render_to_sx_with_env(mobile_defcomp, env)
register_layout(Layout(name, full_fn, oob_fn, mobile_fn))

View File

@@ -45,6 +45,10 @@ IO_PRIMITIVES: frozenset[str] = frozenset({
"abort",
"url-for",
"route-prefix",
"root-header-ctx",
"select-colours",
"account-nav-ctx",
"app-rights",
})
@@ -378,6 +382,106 @@ async def _io_route_prefix(
return route_prefix()
async def _io_root_header_ctx(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
) -> dict[str, Any]:
"""``(root-header-ctx)`` → dict with all root header values.
Fetches cart-mini, auth-menu, nav-tree fragments and computes
settings-url / is-admin from rights. Result is cached on ``g``
per request so multiple calls (e.g. header + mobile) are free.
"""
from quart import g, current_app, request
cached = getattr(g, "_root_header_ctx", None)
if cached is not None:
return cached
from shared.infrastructure.fragments import fetch_fragments
from shared.infrastructure.cart_identity import current_cart_identity
from shared.infrastructure.urls import app_url
from shared.config import config
from .types import NIL
user = getattr(g, "user", None)
ident = current_cart_identity()
cart_params: dict[str, Any] = {}
if ident["user_id"] is not None:
cart_params["user_id"] = ident["user_id"]
if ident["session_id"] is not None:
cart_params["session_id"] = ident["session_id"]
auth_params: dict[str, Any] = {}
if user and getattr(user, "email", None):
auth_params["email"] = user.email
nav_params = {"app_name": current_app.name, "path": request.path}
cart_mini, auth_menu, nav_tree = await fetch_fragments([
("cart", "cart-mini", cart_params or None),
("account", "auth-menu", auth_params or None),
("blog", "nav-tree", nav_params),
])
rights = getattr(g, "rights", None) or {}
is_admin = (
rights.get("admin", False)
if isinstance(rights, dict)
else getattr(rights, "admin", False)
)
result = {
"cart-mini": cart_mini or NIL,
"blog-url": app_url("blog", ""),
"site-title": config()["title"],
"app-label": current_app.name,
"nav-tree": nav_tree or NIL,
"auth-menu": auth_menu or NIL,
"nav-panel": NIL,
"settings-url": app_url("blog", "/settings/") if is_admin else "",
"is-admin": is_admin,
}
g._root_header_ctx = result
return result
async def _io_select_colours(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
) -> str:
"""``(select-colours)`` → the shared select/hover CSS class string."""
from quart import current_app
return current_app.jinja_env.globals.get("select_colours", "")
async def _io_account_nav_ctx(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
) -> Any:
"""``(account-nav-ctx)`` → account nav fragments as SxExpr, or NIL.
Reads ``g.account_nav`` (set by account service's before_request hook),
wrapping HTML strings in ``~rich-text`` for SX rendering.
"""
from quart import g
from .types import NIL
from .parser import SxExpr
val = getattr(g, "account_nav", None)
if not val:
return NIL
if isinstance(val, SxExpr):
return val
# HTML string → wrap for SX rendering
escaped = str(val).replace("\\", "\\\\").replace('"', '\\"')
return SxExpr(f'(~rich-text :html "{escaped}")')
async def _io_app_rights(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
) -> dict[str, Any]:
"""``(app-rights)`` → user rights dict from ``g.rights``."""
from quart import g
return getattr(g, "rights", None) or {}
_IO_HANDLERS: dict[str, Any] = {
"frag": _io_frag,
"query": _io_query,
@@ -394,4 +498,8 @@ _IO_HANDLERS: dict[str, Any] = {
"abort": _io_abort,
"url-for": _io_url_for,
"route-prefix": _io_route_prefix,
"root-header-ctx": _io_root_header_ctx,
"select-colours": _io_select_colours,
"account-nav-ctx": _io_account_nav_ctx,
"app-rights": _io_app_rights,
}

View File

@@ -30,6 +30,27 @@
:link-label "account" :icon "fa-solid fa-user"
:child-id "auth-header-child" :oob oob))
;; Auto-fetching auth header — uses IO primitives, no free variables needed.
;; Expands inline (defmacro) so IO calls resolve in _aser mode.
(defmacro ~auth-header-row-auto (oob)
(quasiquote
(~auth-header-row :account-url (app-url "account" "")
:select-colours (select-colours)
:account-nav (account-nav-ctx)
:oob (unquote oob))))
(defmacro ~auth-header-row-simple-auto (oob)
(quasiquote
(~auth-header-row-simple :account-url (app-url "account" "")
:oob (unquote oob))))
;; Auto-fetching auth nav items — for mobile menus
(defmacro ~auth-nav-items-auto ()
(quasiquote
(~auth-nav-items :account-url (app-url "account" "")
:select-colours (select-colours)
:account-nav (account-nav-ctx))))
;; Orders header row
(defcomp ~orders-header-row (&key list-url)
(~menu-row-sx :id "orders-row" :level 2 :colour "sky"

View File

@@ -162,24 +162,46 @@
(defcomp ~root-mobile (&key nav-tree auth-menu)
(~mobile-root-nav :nav-tree nav-tree :auth-menu auth-menu))
;; ---------------------------------------------------------------------------
;; Auto-fetching header/mobile macros — use IO primitives to self-populate.
;; These expand inline so IO calls resolve in _aser mode within layout bodies.
;; Replaces the 10-parameter ~root-header boilerplate in layout defcomps.
;; ---------------------------------------------------------------------------
(defmacro ~root-header-auto (oob)
(quasiquote
(let ((__rhctx (root-header-ctx)))
(~header-row-sx :cart-mini (get __rhctx "cart-mini")
:blog-url (get __rhctx "blog-url")
:site-title (get __rhctx "site-title")
:app-label (get __rhctx "app-label")
:nav-tree (get __rhctx "nav-tree")
:auth-menu (get __rhctx "auth-menu")
:nav-panel (get __rhctx "nav-panel")
:settings-url (get __rhctx "settings-url")
:is-admin (get __rhctx "is-admin")
:oob (unquote oob)))))
(defmacro ~root-mobile-auto ()
(quasiquote
(let ((__rhctx (root-header-ctx)))
(~mobile-root-nav :nav-tree (get __rhctx "nav-tree")
:auth-menu (get __rhctx "auth-menu")))))
;; ---------------------------------------------------------------------------
;; Built-in layout defcomps — used by register_sx_layout("root", ...)
;; Free variables (cart-mini, blog-url, etc.) come from _ctx_to_env().
;; These use ~root-header-auto / ~root-mobile-auto macros (IO primitives).
;; ---------------------------------------------------------------------------
(defcomp ~layout-root-full ()
(~root-header :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))
(~root-header-auto))
(defcomp ~layout-root-oob ()
(~oob-header-sx :parent-id "root-header-child"
:row (~root-header :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)))
:row (~root-header-auto true)))
(defcomp ~layout-root-mobile ()
(~root-mobile :nav-tree nav-tree :auth-menu auth-menu))
(~root-mobile-auto))
(defcomp ~error-content (&key errnum message image)
(div :class "text-center p-8 max-w-lg mx-auto"
@@ -189,6 +211,33 @@
(div :class "flex justify-center"
(img :src image :width "300" :height "300")))))
(defcomp ~clear-oob-div (&key id)
(div :id id :sx-swap-oob "outerHTML"))
;; ---------------------------------------------------------------------------
;; Shared nav helpers — used by post_header_sx / post_admin_header_sx
;; ---------------------------------------------------------------------------
(defcomp ~container-nav-wrapper (&key content)
(div :id "entries-calendars-nav-wrapper"
:class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
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)
(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)
(<>
(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)
(div :class "relative nav-group"
(a :href href

65
sx/sx/docs.sx Normal file
View File

@@ -0,0 +1,65 @@
;; SX docs utility components
(defcomp ~doc-placeholder (&key id)
(div :id id
(div :class "bg-stone-50 border border-stone-200 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)
(div :id target-id :sx-swap-oob "innerHTML"
(div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3 overflow-x-auto"
(pre :class "text-sm whitespace-pre-wrap"
(code text)))))
(defcomp ~doc-attr-table (&key title 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"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-50"
(th :class "px-3 py-2 font-medium text-stone-600" "Attribute")
(th :class "px-3 py-2 font-medium text-stone-600" "Description")
(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)
(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"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-50"
(th :class "px-3 py-2 font-medium text-stone-600" "Header")
(th :class "px-3 py-2 font-medium text-stone-600" "Value")
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
(tbody rows)))))
(defcomp ~doc-headers-row (&key name value description)
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" name)
(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)
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" name)
(td :class "px-3 py-2 text-stone-700 text-sm" description)))
(defcomp ~doc-two-col-table (&key title intro col1 col2 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))
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-50"
(th :class "px-3 py-2 font-medium text-stone-600" (or col1 "Name"))
(th :class "px-3 py-2 font-medium text-stone-600" (or col2 "Description"))))
(tbody rows)))))
(defcomp ~sx-docs-label ()
(span :class "font-mono" "(<x>)"))
(defcomp ~doc-clear-cache-btn ()
(button :onclick "localStorage.removeItem('sx-components-hash');localStorage.removeItem('sx-components-src');var e=Sx.getEnv();Object.keys(e).forEach(function(k){if(k.charAt(0)==='~')delete e[k]});var b=this;b.textContent='Cleared!';setTimeout(function(){b.textContent='Clear component cache'},2000)"
:class "text-xs text-stone-400 hover:text-stone-600 border border-stone-200 rounded px-2 py-1 transition-colors"
"Clear component cache"))

View File

@@ -1,12 +1,10 @@
;; SX docs layout defcomps — root header from env free variables,
;; SX docs layout defcomps — root header via ~root-header-auto,
;; sx-specific headers passed as &key params.
;; --- SX home layout: root + sx menu row ---
(defcomp ~sx-layout-full (&key sx-row)
(<> (~root-header :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)
(<> (~root-header-auto)
sx-row))
(defcomp ~sx-layout-oob (&key root-header sx-row)
@@ -15,7 +13,5 @@
;; --- SX section layout: root + sx row (with child sub-row) ---
(defcomp ~sx-section-layout-full (&key sx-row)
(<> (~root-header :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)
(<> (~root-header-auto)
sx-row))

View File

@@ -419,60 +419,46 @@ async def _reference_attrs_sx() -> str:
)
def _reference_headers_sx() -> str:
async def _reference_headers_sx() -> str:
from content.pages import REQUEST_HEADERS, RESPONSE_HEADERS
req_table = await _headers_table_sx("Request Headers", REQUEST_HEADERS)
resp_table = await _headers_table_sx("Response Headers", RESPONSE_HEADERS)
return (
f'(~doc-page :title "Headers"'
f' (p :class "text-stone-600 mb-6"'
f' "sx uses custom HTTP headers to coordinate between client and server.")'
f' (div :class "space-y-8"'
f' {_headers_table_sx("Request Headers", REQUEST_HEADERS)}'
f' {_headers_table_sx("Response Headers", RESPONSE_HEADERS)}))'
f' {req_table}'
f' {resp_table}))'
)
def _reference_events_sx() -> str:
async def _reference_events_sx() -> str:
from shared.sx.helpers import render_to_sx
from shared.sx.parser import SxExpr
from content.pages import EVENTS
rows = []
for name, desc in EVENTS:
rows.append(
f'(tr :class "border-b border-stone-100"'
f' (td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" "{name}")'
f' (td :class "px-3 py-2 text-stone-700 text-sm" "{desc}"))'
)
return (
f'(~doc-page :title "Events"'
f' (p :class "text-stone-600 mb-6"'
f' "sx fires custom DOM events at various points in the request lifecycle.")'
f' (div :class "overflow-x-auto rounded border border-stone-200"'
f' (table :class "w-full text-left text-sm"'
f' (thead (tr :class "border-b border-stone-200 bg-stone-50"'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Event")'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Description")))'
f' (tbody {" ".join(rows)}))))'
)
rows.append(await render_to_sx("doc-two-col-row", name=name, description=desc))
rows_sx = "(<> " + " ".join(rows) + ")"
table = await render_to_sx("doc-two-col-table",
intro="sx fires custom DOM events at various points in the request lifecycle.",
col1="Event", col2="Description", rows=SxExpr(rows_sx))
return f'(~doc-page :title "Events" {table})'
def _reference_js_api_sx() -> str:
async def _reference_js_api_sx() -> str:
from shared.sx.helpers import render_to_sx
from shared.sx.parser import SxExpr
from content.pages import JS_API
rows = []
for name, desc in JS_API:
rows.append(
f'(tr :class "border-b border-stone-100"'
f' (td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" "{name}")'
f' (td :class "px-3 py-2 text-stone-700 text-sm" "{desc}"))'
)
return (
f'(~doc-page :title "JavaScript API"'
f' (p :class "text-stone-600 mb-6"'
f' "The client-side sx.js library exposes a public API for programmatic use.")'
f' (div :class "overflow-x-auto rounded border border-stone-200"'
f' (table :class "w-full text-left text-sm"'
f' (thead (tr :class "border-b border-stone-200 bg-stone-50"'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Method")'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Description")))'
f' (tbody {" ".join(rows)}))))'
)
rows.append(await render_to_sx("doc-two-col-row", name=name, description=desc))
rows_sx = "(<> " + " ".join(rows) + ")"
table = await render_to_sx("doc-two-col-table",
intro="The client-side sx.js library exposes a public API for programmatic use.",
col1="Method", col2="Description", rows=SxExpr(rows_sx))
return f'(~doc-page :title "JavaScript API" {table})'
def _protocol_wire_format_sx() -> str:

View File

@@ -15,30 +15,30 @@ def _register_sx_layouts() -> None:
async def _sx_full_headers(ctx: dict, **kw: Any) -> str:
"""Full headers for sx home page: root + sx menu row."""
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
main_nav = await _main_nav_sx(kw.get("section"))
sx_row = await _sx_header_sx(main_nav)
return await render_to_sx_with_env("sx-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("sx-layout-full", {},
sx_row=SxExpr(sx_row))
async def _sx_oob_headers(ctx: dict, **kw: Any) -> str:
"""OOB headers for sx home page."""
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
main_nav = await _main_nav_sx(kw.get("section"))
sx_row = await _sx_header_sx(main_nav)
rows = await render_to_sx_with_env("sx-layout-full", _ctx_to_env(ctx),
rows = await render_to_sx_with_env("sx-layout-full", {},
sx_row=SxExpr(sx_row))
return await oob_header_sx("root-header-child", "sx-header-child", rows)
async def _sx_section_full_headers(ctx: dict, **kw: Any) -> str:
"""Full headers for sx section pages: root + sx row + sub row."""
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
section = kw.get("section", "")
@@ -50,13 +50,13 @@ async def _sx_section_full_headers(ctx: dict, **kw: Any) -> str:
main_nav = await _main_nav_sx(section)
sub_row = await _sub_row_sx(sub_label, sub_href, sub_nav, selected)
sx_row = await _sx_header_sx(main_nav, child=sub_row)
return await render_to_sx_with_env("sx-section-layout-full", _ctx_to_env(ctx),
return await render_to_sx_with_env("sx-section-layout-full", {},
sx_row=SxExpr(sx_row))
async def _sx_section_oob_headers(ctx: dict, **kw: Any) -> str:
"""OOB headers for sx section pages."""
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
section = kw.get("section", "")
@@ -68,7 +68,7 @@ async def _sx_section_oob_headers(ctx: dict, **kw: Any) -> str:
main_nav = await _main_nav_sx(section)
sub_row = await _sub_row_sx(sub_label, sub_href, sub_nav, selected)
sx_row = await _sx_header_sx(main_nav, child=sub_row)
rows = await render_to_sx_with_env("sx-section-layout-full", _ctx_to_env(ctx),
rows = await render_to_sx_with_env("sx-section-layout-full", {},
sx_row=SxExpr(sx_row))
return await oob_header_sx("root-header-child", "sx-header-child", rows)

View File

@@ -18,10 +18,8 @@ def _example_code(code: str, language: str = "lisp") -> str:
def _placeholder(div_id: str) -> str:
"""Empty placeholder that will be filled by OOB swap on interaction."""
return (f'(div :id "{div_id}"'
f' (div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3"'
f' (p :class "text-stone-400 italic text-sm"'
f' "Trigger the demo to see the actual content.")))')
from shared.sx.helpers import sx_call
return sx_call("doc-placeholder", id=div_id)
def _component_source_text(*names: str) -> str:
@@ -45,23 +43,14 @@ def _component_source_text(*names: str) -> str:
def _oob_code(target_id: str, text: str) -> str:
"""OOB swap that displays plain code in a styled block."""
escaped = text.replace('\\', '\\\\').replace('"', '\\"')
return (f'(div :id "{target_id}" :sx-swap-oob "innerHTML"'
f' (div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3 overflow-x-auto"'
f' (pre :class "text-sm whitespace-pre-wrap"'
f' (code "{escaped}"))))')
from shared.sx.helpers import sx_call
return sx_call("doc-oob-code", target_id=target_id, text=text)
def _clear_components_btn() -> str:
"""Button that clears the client-side component cache (localStorage + in-memory)."""
js = ("localStorage.removeItem('sx-components-hash');"
"localStorage.removeItem('sx-components-src');"
"var e=Sx.getEnv();Object.keys(e).forEach(function(k){if(k.charAt(0)==='~')delete e[k]});"
"var b=this;b.textContent='Cleared!';setTimeout(function(){b.textContent='Clear component cache'},2000)")
return (f'(button :onclick "{js}"'
f' :class "text-xs text-stone-400 hover:text-stone-600 border border-stone-200'
f' rounded px-2 py-1 transition-colors"'
f' "Clear component cache")')
from shared.sx.helpers import sx_call
return sx_call("doc-clear-cache-btn")
def _full_wire_text(sx_src: str, *comp_names: str) -> str:

View File

@@ -36,40 +36,18 @@ async def _attr_table_sx(title: str, attrs: list[tuple[str, str, bool]]) -> str:
rows.append(await render_to_sx("doc-attr-row", attr=attr, description=desc,
exists="true" if exists else None,
href=href))
return (
f'(div :class "space-y-3"'
f' (h3 :class "text-xl font-semibold text-stone-700" "{title}")'
f' (div :class "overflow-x-auto rounded border border-stone-200"'
f' (table :class "w-full text-left text-sm"'
f' (thead (tr :class "border-b border-stone-200 bg-stone-50"'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Attribute")'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Description")'
f' (th :class "px-3 py-2 font-medium text-stone-600 text-center w-20" "In sx?")))'
f' (tbody {" ".join(rows)}))))'
)
rows_sx = "(<> " + " ".join(rows) + ")"
return await render_to_sx("doc-attr-table", title=title, rows=SxExpr(rows_sx))
def _headers_table_sx(title: str, headers: list[tuple[str, str, str]]) -> str:
async def _headers_table_sx(title: str, headers: list[tuple[str, str, str]]) -> str:
"""Build a headers reference table."""
rows = []
for name, value, desc in headers:
rows.append(
f'(tr :class "border-b border-stone-100"'
f' (td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" "{name}")'
f' (td :class "px-3 py-2 font-mono text-sm text-stone-500" "{value}")'
f' (td :class "px-3 py-2 text-stone-700 text-sm" "{desc}"))'
)
return (
f'(div :class "space-y-3"'
f' (h3 :class "text-xl font-semibold text-stone-700" "{title}")'
f' (div :class "overflow-x-auto rounded border border-stone-200"'
f' (table :class "w-full text-left text-sm"'
f' (thead (tr :class "border-b border-stone-200 bg-stone-50"'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Header")'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Value")'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Description")))'
f' (tbody {" ".join(rows)}))))'
)
rows.append(await render_to_sx("doc-headers-row",
name=name, value=value, description=desc))
rows_sx = "(<> " + " ".join(rows) + ")"
return await render_to_sx("doc-headers-table", title=title, rows=SxExpr(rows_sx))
async def _primitives_section_sx() -> str:
@@ -86,10 +64,11 @@ async def _primitives_section_sx() -> str:
async def _sx_header_sx(nav: str | None = None, *, child: str | None = None) -> str:
"""Build the sx docs menu-row."""
label_sx = await render_to_sx("sx-docs-label")
return await render_to_sx("menu-row-sx",
id="sx-row", level=1, colour="violet",
link_href="/", link_label="sx",
link_label_content=SxExpr('(span :class "font-mono" "(<x>)")'),
link_label_content=SxExpr(label_sx),
nav=SxExpr(nav) if nav else None,
child_id="sx-header-child",
child=SxExpr(child) if child else None,

View File

@@ -22,9 +22,7 @@
;; Layout: full page header stack (reads root header values from env free variables)
(defcomp ~test-layout-full (&key services active-service)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx
:inner (~test-header-row :services services :active-service active-service))))
@@ -93,9 +91,7 @@
;; Detail page header stack (reads root header values from env free variables)
(defcomp ~test-detail-layout-full (&key services test-nodeid test-label)
(<> (~root-header :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)
(<> (~root-header-auto)
(~header-child-sx
:inner (<> (~test-header-row :services services)
(~header-child-sx :id "test-header-child"

View File

@@ -5,7 +5,7 @@ import os
from datetime import datetime
from shared.sx.jinja_bridge import load_service_components
from shared.sx.helpers import render_to_sx, SxExpr, render_to_sx_with_env, _ctx_to_env, full_page_sx
from shared.sx.helpers import render_to_sx, SxExpr, render_to_sx_with_env, full_page_sx
# Load test-specific .sx components at import time
load_service_components(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
@@ -99,7 +99,7 @@ async def render_dashboard_page_sx(ctx: dict, result: dict | None,
inner = await render_to_sx("test-results-partial",
summary_data=summary_data, sections=sections, has_failures=has_failures)
content = await render_to_sx("test-results-wrap", running=running, inner=SxExpr(inner))
hdr = await render_to_sx_with_env("test-layout-full", _ctx_to_env(ctx),
hdr = await render_to_sx_with_env("test-layout-full", {},
services=_service_list(),
active_service=active_service,
)
@@ -131,7 +131,7 @@ async def render_results_partial_sx(result: dict | None, running: bool,
async def render_test_detail_page_sx(ctx: dict, test: dict) -> str:
"""Full page: test detail (sx wire format)."""
hdr = await render_to_sx_with_env("test-detail-layout-full", _ctx_to_env(ctx),
hdr = await render_to_sx_with_env("test-detail-layout-full", {},
services=_service_list(),
test_nodeid=test["nodeid"],
test_label=test["nodeid"].rsplit("::", 1)[-1],