diff --git a/cart/sexp/sexp_components.py b/cart/sexp/sexp_components.py index 7f9321e..d4b59ea 100644 --- a/cart/sexp/sexp_components.py +++ b/cart/sexp/sexp_components.py @@ -511,17 +511,13 @@ async def render_overview_page(ctx: dict, page_groups: list) -> str: """Full page: cart overview.""" main = _overview_main_panel_html(page_groups, ctx) hdr = root_header_html(ctx) - hdr += render("cart-header-child", inner_html=_cart_header_html(ctx)) return full_page(ctx, header_rows_html=hdr, content_html=main) async def render_overview_oob(ctx: dict, page_groups: list) -> str: """OOB response for cart overview.""" main = _overview_main_panel_html(page_groups, ctx) - oobs = ( - _cart_header_html(ctx, oob=True) - + root_header_html(ctx, oob=True) - ) + oobs = root_header_html(ctx, oob=True) return oob_page(ctx, oobs_html=oobs, content_html=main) @@ -717,7 +713,6 @@ def _checkout_error_content_html(error: str | None, order: Any | None) -> str: async def render_checkout_error_page(ctx: dict, error: str | None = None, order: Any | None = None) -> str: """Full page: checkout error.""" hdr = root_header_html(ctx) - hdr += render("cart-header-child", inner_html=_cart_header_html(ctx)) filt = _checkout_error_filter_html() content = _checkout_error_content_html(error, order) return full_page(ctx, header_rows_html=hdr, filter_html=filt, content_html=content) diff --git a/shared/infrastructure/context.py b/shared/infrastructure/context.py index 09f268b..cf05dc6 100644 --- a/shared/infrastructure/context.py +++ b/shared/infrastructure/context.py @@ -98,6 +98,7 @@ async def base_context() -> dict: "qs_filter": _qs_filter_fn(), "print": print, "base_url": base_url, + "app_label": current_app.name, "base_title": config()["title"], "hx_select": hx_select, "hx_select_search": hx_select_search, diff --git a/shared/infrastructure/factory.py b/shared/infrastructure/factory.py index 280122d..4248340 100644 --- a/shared/infrastructure/factory.py +++ b/shared/infrastructure/factory.py @@ -114,6 +114,14 @@ def create_base_app( setup_sexp_bridge(app) load_shared_components() load_relation_registry() + + # Dev-mode: auto-reload sexp templates when files change on disk + if os.getenv("RELOAD") == "true": + from shared.sexp.jinja_bridge import reload_if_changed + + @app.before_request + async def _sexp_hot_reload(): + reload_if_changed() errors(app) # Auto-register OAuth client blueprint for non-account apps diff --git a/shared/sexp/components.py b/shared/sexp/components.py index 8e103d2..f84e90c 100644 --- a/shared/sexp/components.py +++ b/shared/sexp/components.py @@ -9,10 +9,11 @@ from __future__ import annotations import os -from .jinja_bridge import load_sexp_dir +from .jinja_bridge import load_sexp_dir, watch_sexp_dir def load_shared_components() -> None: """Register all shared s-expression components.""" templates_dir = os.path.join(os.path.dirname(__file__), "templates") load_sexp_dir(templates_dir) + watch_sexp_dir(templates_dir) diff --git a/shared/sexp/helpers.py b/shared/sexp/helpers.py index 59f17aa..650ca96 100644 --- a/shared/sexp/helpers.py +++ b/shared/sexp/helpers.py @@ -36,6 +36,7 @@ def root_header_html(ctx: dict, *, oob: bool = False) -> str: 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", ""), diff --git a/shared/sexp/jinja_bridge.py b/shared/sexp/jinja_bridge.py index 646d3a7..fd62049 100644 --- a/shared/sexp/jinja_bridge.py +++ b/shared/sexp/jinja_bridge.py @@ -53,11 +53,49 @@ def load_sexp_dir(directory: str) -> None: register_components(f.read()) +# --------------------------------------------------------------------------- +# Dev-mode auto-reload of sexp templates +# --------------------------------------------------------------------------- + +_watched_dirs: list[str] = [] +_file_mtimes: dict[str, float] = {} + + +def watch_sexp_dir(directory: str) -> None: + """Register a directory for dev-mode file watching.""" + _watched_dirs.append(directory) + # Seed mtimes + for fp in sorted( + glob.glob(os.path.join(directory, "*.sexp")) + + glob.glob(os.path.join(directory, "*.sexpr")) + ): + _file_mtimes[fp] = os.path.getmtime(fp) + + +def reload_if_changed() -> None: + """Re-read sexp files if any have changed on disk. Called per-request in dev.""" + changed = False + for directory in _watched_dirs: + for fp in sorted( + glob.glob(os.path.join(directory, "*.sexp")) + + glob.glob(os.path.join(directory, "*.sexpr")) + ): + mtime = os.path.getmtime(fp) + if fp not in _file_mtimes or _file_mtimes[fp] != mtime: + _file_mtimes[fp] = mtime + changed = True + if changed: + _COMPONENT_ENV.clear() + for directory in _watched_dirs: + load_sexp_dir(directory) + + def load_service_components(service_dir: str) -> None: """Load service-specific s-expression components from {service_dir}/sexp/.""" sexp_dir = os.path.join(service_dir, "sexp") if os.path.isdir(sexp_dir): load_sexp_dir(sexp_dir) + watch_sexp_dir(sexp_dir) def register_components(sexp_source: str) -> None: diff --git a/shared/sexp/templates/layout.sexp b/shared/sexp/templates/layout.sexp index cc34cf3..8406958 100644 --- a/shared/sexp/templates/layout.sexp +++ b/shared/sexp/templates/layout.sexp @@ -96,7 +96,7 @@ :class "w-12 h-12 rotate-180 transition-transform group-open/root:block hidden self-start" (path :d "M6 9l6 6 6-6" :fill "currentColor")))) -(defcomp ~header-row (&key cart-mini-html blog-url site-title +(defcomp ~header-row (&key cart-mini-html blog-url site-title app-label nav-tree-html auth-menu-html nav-panel-html settings-url is-admin oob) (<> @@ -106,8 +106,10 @@ (div :class "w-full flex flex-row items-top" (when cart-mini-html (raw! cart-mini-html)) (div :class "font-bold text-5xl flex-1" - (a :href (or blog-url "/") :class "flex justify-center md:justify-start" - (h1 (or site-title "")))) + (a :href (or blog-url "/") :class "flex justify-center md:justify-start items-baseline gap-2" + (h1 (or site-title "")) + (when app-label + (span :class "!text-2xl font-normal text-white" app-label)))) (nav :class "hidden md:flex gap-4 text-sm ml-2 justify-end items-center flex-0" (when nav-tree-html (raw! nav-tree-html)) (when auth-menu-html (raw! auth-menu-html))