diff --git a/account/sx/layouts.sx b/account/sx/layouts.sx index 19c028a..10052e5 100644 --- a/account/sx/layouts.sx +++ b/account/sx/layouts.sx @@ -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))) diff --git a/blog/sx/admin.sx b/blog/sx/admin.sx index b78233d..95172ec 100644 --- a/blog/sx/admin.sx +++ b/blog/sx/admin.sx @@ -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) ;; --------------------------------------------------------------------------- diff --git a/blog/sx/layouts.sx b/blog/sx/layouts.sx index 9058011..a1fe5e5 100644 --- a/blog/sx/layouts.sx +++ b/blog/sx/layouts.sx @@ -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 --- diff --git a/blog/sxc/pages/helpers.py b/blog/sxc/pages/helpers.py index 4c75d33..bc4b900 100644 --- a/blog/sxc/pages/helpers.py +++ b/blog/sxc/pages/helpers.py @@ -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})")) diff --git a/blog/sxc/pages/layouts.py b/blog/sxc/pages/layouts.py index aeb22c6..36eb7f3 100644 --- a/blog/sxc/pages/layouts.py +++ b/blog/sxc/pages/layouts.py @@ -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", diff --git a/cart/sx/header.sx b/cart/sx/header.sx index 692be37..1fc17bf 100644 --- a/cart/sx/header.sx +++ b/cart/sx/header.sx @@ -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")) diff --git a/cart/sx/layouts.sx b/cart/sx/layouts.sx index bb34148..1bdfaf9 100644 --- a/cart/sx/layouts.sx +++ b/cart/sx/layouts.sx @@ -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) --- diff --git a/cart/sxc/pages/layouts.py b/cart/sxc/pages/layouts.py index 1591129..9b4c981 100644 --- a/cart/sxc/pages/layouts.py +++ b/cart/sxc/pages/layouts.py @@ -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)), diff --git a/cart/sxc/pages/renders.py b/cart/sxc/pages/renders.py index f53027e..125d53c 100644 --- a/cart/sxc/pages/renders.py +++ b/cart/sxc/pages/renders.py @@ -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("/")) diff --git a/events/sx/header.sx b/events/sx/header.sx index 1aec230..0f62026 100644 --- a/events/sx/header.sx +++ b/events/sx/header.sx @@ -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))) + diff --git a/events/sx/layouts.sx b/events/sx/layouts.sx index 2ba7825..81ff1a8 100644 --- a/events/sx/layouts.sx +++ b/events/sx/layouts.sx @@ -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) diff --git a/events/sxc/pages/calendar.py b/events/sxc/pages/calendar.py index ab22d5f..28bb587 100644 --- a/events/sxc/pages/calendar.py +++ b/events/sxc/pages/calendar.py @@ -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 diff --git a/events/sxc/pages/entries.py b/events/sxc/pages/entries.py index 4516acb..5c9bdec 100644 --- a/events/sxc/pages/entries.py +++ b/events/sxc/pages/entries.py @@ -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 ( diff --git a/events/sxc/pages/helpers.py b/events/sxc/pages/helpers.py index 91be5d1..8604e05 100644 --- a/events/sxc/pages/helpers.py +++ b/events/sxc/pages/helpers.py @@ -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))), diff --git a/events/sxc/pages/layouts.py b/events/sxc/pages/layouts.py index 3532431..82717ab 100644 --- a/events/sxc/pages/layouts.py +++ b/events/sxc/pages/layouts.py @@ -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))), diff --git a/events/sxc/pages/renders.py b/events/sxc/pages/renders.py index 8dbd156..1684fc6 100644 --- a/events/sxc/pages/renders.py +++ b/events/sxc/pages/renders.py @@ -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) diff --git a/events/sxc/pages/slots.py b/events/sxc/pages/slots.py index f9a2db4..b3ad565 100644 --- a/events/sxc/pages/slots.py +++ b/events/sxc/pages/slots.py @@ -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'
' - f'
' - f'' - f'
{escape(slot.name)}
' - f'
' - f'

{escape(desc)}

' - f'
' - ) + 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) diff --git a/events/sxc/pages/utils.py b/events/sxc/pages/utils.py index 67c8271..286615b 100644 --- a/events/sxc/pages/utils.py +++ b/events/sxc/pages/utils.py @@ -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) diff --git a/federation/sx/layouts.sx b/federation/sx/layouts.sx index c2587da..03a3a0d 100644 --- a/federation/sx/layouts.sx +++ b/federation/sx/layouts.sx @@ -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))) diff --git a/federation/sxc/pages/utils.py b/federation/sxc/pages/utils.py index 97b1f01..9e15547 100644 --- a/federation/sxc/pages/utils.py +++ b/federation/sxc/pages/utils.py @@ -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'{escape(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) diff --git a/market/sx/grids.sx b/market/sx/grids.sx index 4617af0..1546c18 100644 --- a/market/sx/grids.sx +++ b/market/sx/grids.sx @@ -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" diff --git a/market/sx/headers.sx b/market/sx/headers.sx index 181a6bd..b88c567 100644 --- a/market/sx/headers.sx +++ b/market/sx/headers.sx @@ -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))) diff --git a/market/sx/layouts.sx b/market/sx/layouts.sx index 3fe0d5f..2893b51 100644 --- a/market/sx/layouts.sx +++ b/market/sx/layouts.sx @@ -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) diff --git a/market/sxc/pages/helpers.py b/market/sxc/pages/helpers.py index d6183b4..8f826c3 100644 --- a/market/sxc/pages/helpers.py +++ b/market/sxc/pages/helpers.py @@ -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): diff --git a/market/sxc/pages/layouts.py b/market/sxc/pages/layouts.py index 0f0f563..60fddd4 100644 --- a/market/sxc/pages/layouts.py +++ b/market/sxc/pages/layouts.py @@ -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))) diff --git a/market/sxc/pages/renders.py b/market/sxc/pages/renders.py index 1178bd7..63e3067 100644 --- a/market/sxc/pages/renders.py +++ b/market/sxc/pages/renders.py @@ -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)), diff --git a/market/sxc/pages/utils.py b/market/sxc/pages/utils.py index 25caa65..6ccc33c 100644 --- a/market/sxc/pages/utils.py +++ b/market/sxc/pages/utils.py @@ -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) # --------------------------------------------------------------------------- diff --git a/orders/sx/layouts.sx b/orders/sx/layouts.sx index 16867d0..308487a 100644 --- a/orders/sx/layouts.sx +++ b/orders/sx/layouts.sx @@ -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)) diff --git a/shared/sx/helpers.py b/shared/sx/helpers.py index fb4dd73..820a0f0 100644 --- a/shared/sx/helpers.py +++ b/shared/sx/helpers.py @@ -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 diff --git a/shared/sx/layouts.py b/shared/sx/layouts.py index df69649..bfef411 100644 --- a/shared/sx/layouts.py +++ b/shared/sx/layouts.py @@ -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)) diff --git a/shared/sx/primitives_io.py b/shared/sx/primitives_io.py index dc066f7..c7b0053 100644 --- a/shared/sx/primitives_io.py +++ b/shared/sx/primitives_io.py @@ -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, } diff --git a/shared/sx/templates/auth.sx b/shared/sx/templates/auth.sx index 084a2b9..2f3ed2a 100644 --- a/shared/sx/templates/auth.sx +++ b/shared/sx/templates/auth.sx @@ -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" diff --git a/shared/sx/templates/layout.sx b/shared/sx/templates/layout.sx index 0ab4679..d1b3c85 100644 --- a/shared/sx/templates/layout.sx +++ b/shared/sx/templates/layout.sx @@ -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 diff --git a/sx/sx/docs.sx b/sx/sx/docs.sx new file mode 100644 index 0000000..1747e19 --- /dev/null +++ b/sx/sx/docs.sx @@ -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" "()")) + +(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")) diff --git a/sx/sx/layouts.sx b/sx/sx/layouts.sx index 0a32026..eeecb56 100644 --- a/sx/sx/layouts.sx +++ b/sx/sx/layouts.sx @@ -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)) diff --git a/sx/sxc/pages/essays.py b/sx/sxc/pages/essays.py index 1685783..eea1e3a 100644 --- a/sx/sxc/pages/essays.py +++ b/sx/sxc/pages/essays.py @@ -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: diff --git a/sx/sxc/pages/layouts.py b/sx/sxc/pages/layouts.py index 809fae0..2112dbe 100644 --- a/sx/sxc/pages/layouts.py +++ b/sx/sxc/pages/layouts.py @@ -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) diff --git a/sx/sxc/pages/renders.py b/sx/sxc/pages/renders.py index 0a5a77a..ada6cee 100644 --- a/sx/sxc/pages/renders.py +++ b/sx/sxc/pages/renders.py @@ -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: diff --git a/sx/sxc/pages/utils.py b/sx/sxc/pages/utils.py index 7853078..d11069e 100644 --- a/sx/sxc/pages/utils.py +++ b/sx/sxc/pages/utils.py @@ -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" "()")'), + 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, diff --git a/test/sx/components.sx b/test/sx/components.sx index 9e9ac41..691d736 100644 --- a/test/sx/components.sx +++ b/test/sx/components.sx @@ -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" diff --git a/test/sxc/pages/renders.py b/test/sxc/pages/renders.py index c907963..7f46738 100644 --- a/test/sxc/pages/renders.py +++ b/test/sxc/pages/renders.py @@ -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],