Files
mono/shared/sexp/helpers.py
giles 6c44a5f3d0 Add app label to root header and auto-reload sexp templates in dev
Show current subdomain name (blog, cart, events, etc.) next to the site
title in the root header row. Remove the redundant second "cart" menu row
from cart overview and checkout error pages.

Add dev-mode hot-reload for sexp templates: track file mtimes and re-read
changed files per-request when RELOAD=true, so .sexp edits are picked up
without restarting services.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 19:33:00 +00:00

155 lines
5.0 KiB
Python

"""
Shared helper functions for s-expression page rendering.
These are used by per-service sexp_components.py files to build common
page elements (headers, search, etc.) from template context.
"""
from __future__ import annotations
from typing import Any
from .jinja_bridge import render
from .page import SEARCH_HEADERS_MOBILE, SEARCH_HEADERS_DESKTOP
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)
if callable(fn):
return fn(path)
return str(fn or "") + path
def get_asset_url(ctx: dict) -> str:
"""Extract the asset URL base from context."""
au = ctx.get("asset_url")
if callable(au):
result = au("")
return result.rsplit("/", 1)[0] if "/" in result else result
return au or ""
def root_header_html(ctx: dict, *, oob: bool = False) -> str:
"""Build the root header row HTML."""
return render(
"header-row",
cart_mini_html=ctx.get("cart_mini_html", ""),
blog_url=call_url(ctx, "blog_url", ""),
site_title=ctx.get("base_title", ""),
app_label=ctx.get("app_label", ""),
nav_tree_html=ctx.get("nav_tree_html", ""),
auth_menu_html=ctx.get("auth_menu_html", ""),
nav_panel_html=ctx.get("nav_panel_html", ""),
oob=oob,
)
def search_mobile_html(ctx: dict) -> str:
"""Build mobile search input HTML."""
return render(
"search-mobile",
current_local_href=ctx.get("current_local_href", "/"),
search=ctx.get("search", ""),
search_count=ctx.get("search_count", ""),
hx_select=ctx.get("hx_select", "#main-panel"),
search_headers_mobile=SEARCH_HEADERS_MOBILE,
)
def search_desktop_html(ctx: dict) -> str:
"""Build desktop search input HTML."""
return render(
"search-desktop",
current_local_href=ctx.get("current_local_href", "/"),
search=ctx.get("search", ""),
search_count=ctx.get("search_count", ""),
hx_select=ctx.get("hx_select", "#main-panel"),
search_headers_desktop=SEARCH_HEADERS_DESKTOP,
)
def post_header_html(ctx: dict, *, oob: bool = False) -> str:
"""Build the post-level header row (level 1). Used by all apps + error pages."""
post = ctx.get("post") or {}
slug = post.get("slug", "")
if not slug:
return ""
title = (post.get("title") or "")[:160]
feature_image = post.get("feature_image")
label_html = render("post-label", feature_image=feature_image, title=title)
nav_parts: list[str] = []
page_cart_count = ctx.get("page_cart_count", 0)
if page_cart_count and page_cart_count > 0:
cart_href = call_url(ctx, "cart_url", f"/{slug}/")
nav_parts.append(render("page-cart-badge", href=cart_href, count=str(page_cart_count)))
container_nav = ctx.get("container_nav_html", "")
if container_nav:
nav_parts.append(container_nav)
admin_nav = ctx.get("post_admin_nav_html", "")
if admin_nav:
nav_parts.append(admin_nav)
nav_html = "".join(nav_parts)
link_href = call_url(ctx, "blog_url", f"/{slug}/")
return render("menu-row",
id="post-row", level=1,
link_href=link_href, link_label_html=label_html,
nav_html=nav_html, child_id="post-header-child",
oob=oob, external=True,
)
def oob_header_html(parent_id: str, child_id: str, row_html: str) -> str:
"""Wrap a header row in an OOB swap div with child placeholder."""
return render("oob-header",
parent_id=parent_id, child_id=child_id, row_html=row_html,
)
def header_child_html(inner_html: str, *, id: str = "root-header-child") -> str:
"""Wrap inner HTML in a header-child div."""
return render("header-child", id=id, inner_html=inner_html)
def error_content_html(errnum: str, message: str, image: str | None = None) -> str:
"""Render the error content block."""
return render("error-content", errnum=errnum, message=message, image=image)
def full_page(ctx: dict, *, header_rows_html: str,
filter_html: str = "", aside_html: str = "",
content_html: str = "", menu_html: str = "",
body_end_html: str = "", meta_html: str = "") -> str:
"""Render a full app page with the standard layout."""
return render(
"app-layout",
title=ctx.get("base_title", "Rose Ash"),
asset_url=get_asset_url(ctx),
meta_html=meta_html,
header_rows_html=header_rows_html,
menu_html=menu_html,
filter_html=filter_html,
aside_html=aside_html,
content_html=content_html,
body_end_html=body_end_html,
)
def oob_page(ctx: dict, *, oobs_html: str = "",
filter_html: str = "", aside_html: str = "",
content_html: str = "", menu_html: str = "") -> str:
"""Render an OOB response with standard swap targets."""
return render(
"oob-response",
oobs_html=oobs_html,
filter_html=filter_html,
aside_html=aside_html,
menu_html=menu_html,
content_html=content_html,
)