diff --git a/account/sx/layouts.sx b/account/sx/layouts.sx
new file mode 100644
index 0000000..06af8df
--- /dev/null
+++ b/account/sx/layouts.sx
@@ -0,0 +1,27 @@
+;; Account layout defcomps — read ctx values from env free variables.
+;; Registered via register_sx_layout("account", ...) in __init__.py.
+
+;; Full page: root header + auth header row in header-child
+(defcomp ~account-layout-full ()
+ (<> (~root-header)
+ (~header-child-sx
+ :inner (~auth-header-row :account-url account-url
+ :select-colours select-colours
+ :account-nav account-nav))))
+
+;; 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 :oob 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)))
diff --git a/account/sxc/pages/__init__.py b/account/sxc/pages/__init__.py
index 7f624ed..79a123d 100644
--- a/account/sxc/pages/__init__.py
+++ b/account/sxc/pages/__init__.py
@@ -1,8 +1,6 @@
"""Account defpage setup — registers layouts and loads .sx pages."""
from __future__ import annotations
-from typing import Any
-
def setup_account_pages() -> None:
"""Register account-specific layouts and load page definitions."""
@@ -16,76 +14,6 @@ def _load_account_page_files() -> None:
load_page_dir(os.path.dirname(__file__), "account")
-# ---------------------------------------------------------------------------
-# Layouts
-# ---------------------------------------------------------------------------
-
def _register_account_layouts() -> None:
- from shared.sx.layouts import register_custom_layout
- register_custom_layout("account", _account_full, _account_oob, _account_mobile)
-
-
-async def _account_full(ctx: dict, **kw: Any) -> str:
- from shared.sx.helpers import root_header_sx, header_child_sx, render_to_sx
-
- root_hdr = await root_header_sx(ctx)
- auth_hdr = await render_to_sx("auth-header-row",
- account_url=_call_url(ctx, "account_url", ""),
- select_colours=ctx.get("select_colours", ""),
- account_nav=_as_sx_nav(ctx),
- )
- hdr_child = await header_child_sx(auth_hdr)
- return "(<> " + root_hdr + " " + hdr_child + ")"
-
-
-async def _account_oob(ctx: dict, **kw: Any) -> str:
- from shared.sx.helpers import root_header_sx, render_to_sx
-
- auth_hdr = await render_to_sx("auth-header-row",
- account_url=_call_url(ctx, "account_url", ""),
- select_colours=ctx.get("select_colours", ""),
- account_nav=_as_sx_nav(ctx),
- oob=True,
- )
- return "(<> " + auth_hdr + " " + await root_header_sx(ctx, oob=True) + ")"
-
-
-async def _account_mobile(ctx: dict, **kw: Any) -> str:
- from shared.sx.helpers import mobile_menu_sx, mobile_root_nav_sx, render_to_sx
- from shared.sx.parser import SxExpr
-
- ctx = _inject_account_nav(ctx)
- nav_items = await render_to_sx("auth-nav-items",
- account_url=_call_url(ctx, "account_url", ""),
- select_colours=ctx.get("select_colours", ""),
- account_nav=_as_sx_nav(ctx),
- )
- auth_section = await render_to_sx("mobile-menu-section",
- label="account", href="/", level=1, colour="sky",
- items=SxExpr(nav_items))
- return mobile_menu_sx(auth_section, await mobile_root_nav_sx(ctx))
-
-
-def _call_url(ctx: dict, key: str, path: str = "/") -> str:
- fn = ctx.get(key)
- if callable(fn):
- return fn(path)
- return str(fn or "") + path
-
-
-def _inject_account_nav(ctx: dict) -> dict:
- """Ensure account_nav is in ctx from g.account_nav."""
- if "account_nav" not in ctx:
- from quart import g
- ctx = dict(ctx)
- ctx["account_nav"] = getattr(g, "account_nav", "")
- return ctx
-
-
-def _as_sx_nav(ctx: dict) -> Any:
- """Convert account_nav fragment to SxExpr for use in component calls."""
- from shared.sx.helpers import _as_sx
- ctx = _inject_account_nav(ctx)
- return _as_sx(ctx.get("account_nav"))
-
-
+ from shared.sx.layouts import register_sx_layout
+ register_sx_layout("account", "account-layout-full", "account-layout-oob", "account-layout-mobile")
diff --git a/federation/sx/layouts.sx b/federation/sx/layouts.sx
new file mode 100644
index 0000000..321eb55
--- /dev/null
+++ b/federation/sx/layouts.sx
@@ -0,0 +1,17 @@
+;; Federation layout defcomps — read ctx values from env free variables.
+;; `actor` is injected into env by the layout registration in __init__.py.
+
+;; Full page: root header + social header in header-child
+(defcomp ~social-layout-full ()
+ (<> (~root-header)
+ (~header-child-sx
+ :inner (~federation-social-header
+ :nav (~federation-social-nav :actor actor)))))
+
+;; OOB (HTMX): social header oob + root header oob
+(defcomp ~social-layout-oob ()
+ (<> (~oob-header-sx
+ :parent-id "root-header-child"
+ :row (~federation-social-header
+ :nav (~federation-social-nav :actor actor)))
+ (~root-header :oob true)))
diff --git a/federation/sxc/pages/__init__.py b/federation/sxc/pages/__init__.py
index c8c25ed..b8af21f 100644
--- a/federation/sxc/pages/__init__.py
+++ b/federation/sxc/pages/__init__.py
@@ -17,7 +17,7 @@ def _load_federation_page_files() -> None:
# ---------------------------------------------------------------------------
-# Layouts
+# Layouts — .sx defcomps read free variables from env
# ---------------------------------------------------------------------------
def _register_federation_layouts() -> None:
@@ -25,36 +25,30 @@ def _register_federation_layouts() -> None:
register_custom_layout("social", _social_full, _social_oob)
-async def _social_full(ctx: dict, **kw: Any) -> str:
- from shared.sx.helpers import root_header_sx, header_child_sx, render_to_sx
- from shared.sx.parser import SxExpr
-
+def _actor_data(ctx: dict) -> dict | None:
actor = ctx.get("actor")
- actor_data = _serialize_actor(actor) if actor else None
- nav = await render_to_sx("federation-social-nav", actor=actor_data)
- social_hdr = await render_to_sx("federation-social-header", nav=SxExpr(nav))
- root_hdr = await root_header_sx(ctx)
- child = await header_child_sx(social_hdr)
- return "(<> " + root_hdr + " " + child + ")"
+ if not actor:
+ return None
+ from services.federation_page import _serialize_actor
+ return _serialize_actor(actor)
+
+
+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)
+ 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 root_header_sx, render_to_sx
- from shared.sx.parser import SxExpr
-
- actor = ctx.get("actor")
- actor_data = _serialize_actor(actor) if actor else None
- nav = await render_to_sx("federation-social-nav", actor=actor_data)
- social_hdr = await render_to_sx("federation-social-header", nav=SxExpr(nav))
- child_oob = await render_to_sx("oob-header-sx",
- parent_id="root-header-child",
- row=SxExpr(social_hdr))
- root_hdr_oob = await root_header_sx(ctx, oob=True)
- return "(<> " + child_oob + " " + root_hdr_oob + ")"
+ 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)
+ return await render_to_sx_with_env("social-layout-oob", env)
# ---------------------------------------------------------------------------
-# Serializers and helpers — still used by layouts and route handlers
+# Helpers still used by route handlers
# ---------------------------------------------------------------------------
def _serialize_actor(actor) -> dict | None:
@@ -78,16 +72,12 @@ 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, root_header_sx, header_child_sx, full_page_sx
- from shared.sx.parser import SxExpr
+ from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, full_page_sx
from markupsafe import escape
- actor_data = _serialize_actor(actor)
- nav = await render_to_sx("federation-social-nav", actor=actor_data)
- social_hdr = await render_to_sx("federation-social-header", nav=SxExpr(nav))
- hdr = await root_header_sx(ctx)
- child = await header_child_sx(social_hdr)
- header_rows = "(<> " + hdr + " " + child + ")"
+ env = _ctx_to_env(ctx)
+ 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)}')
diff --git a/orders/sx/layouts.sx b/orders/sx/layouts.sx
new file mode 100644
index 0000000..d141577
--- /dev/null
+++ b/orders/sx/layouts.sx
@@ -0,0 +1,49 @@
+;; Orders layout defcomps — read ctx values from env free variables.
+;; Registered via register_sx_layout("orders", ...) in __init__.py.
+
+;; --- orders layout: root + auth + orders rows ---
+
+(defcomp ~orders-layout-full (&key list-url)
+ (<> (~root-header)
+ (~header-child-sx
+ :inner (<> (~auth-header-row :account-url account-url
+ :select-colours select-colours
+ :account-nav account-nav)
+ (~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)
+ (~oob-header-sx
+ :parent-id "auth-header-child"
+ :row (~orders-header-row :list-url (or list-url "/")))
+ (~root-header :oob true)))
+
+(defcomp ~orders-layout-mobile ()
+ (~root-mobile))
+
+;; --- order-detail layout: root + auth + orders + order rows ---
+
+(defcomp ~order-detail-layout-full (&key list-url detail-url)
+ (<> (~root-header)
+ (~order-detail-header-stack
+ :auth (~auth-header-row :account-url account-url
+ :select-colours select-colours
+ :account-nav account-nav)
+ :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"
+ :icon "fa fa-gbp"))))
+
+(defcomp ~order-detail-layout-oob (&key detail-url)
+ (<> (~oob-header-sx
+ :parent-id "orders-header-child"
+ :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 :oob true)))
+
+(defcomp ~order-detail-layout-mobile ()
+ (~root-mobile))
diff --git a/orders/sxc/pages/__init__.py b/orders/sxc/pages/__init__.py
index 97b25ef..15314af 100644
--- a/orders/sxc/pages/__init__.py
+++ b/orders/sxc/pages/__init__.py
@@ -1,8 +1,6 @@
"""Orders defpage setup — registers layouts and loads .sx pages."""
from __future__ import annotations
-from typing import Any
-
def setup_orders_pages() -> None:
"""Register orders-specific layouts and load page definitions."""
@@ -17,110 +15,7 @@ def _load_orders_page_files() -> None:
load_page_dir(os.path.dirname(__file__), "orders")
-# ---------------------------------------------------------------------------
-# Layouts
-# ---------------------------------------------------------------------------
-
def _register_orders_layouts() -> None:
- from shared.sx.layouts import register_custom_layout
- register_custom_layout("orders", _orders_full, _orders_oob, _orders_mobile)
- register_custom_layout("order-detail", _order_detail_full, _order_detail_oob, _order_detail_mobile)
-
-
-async def _orders_full(ctx: dict, **kw: Any) -> str:
- from shared.sx.helpers import root_header_sx, header_child_sx, call_url, render_to_sx
-
- list_url = kw.get("list_url", "/")
- account_url = call_url(ctx, "account_url", "")
- root_hdr = await root_header_sx(ctx)
- auth_hdr = await render_to_sx("auth-header-row",
- account_url=account_url,
- select_colours=ctx.get("select_colours", ""),
- account_nav=_as_sx_nav(ctx),
- )
- orders_hdr = await render_to_sx("orders-header-row", list_url=list_url)
- inner = "(<> " + auth_hdr + " " + orders_hdr + ")"
- return "(<> " + root_hdr + " " + await header_child_sx(inner) + ")"
-
-
-async def _orders_oob(ctx: dict, **kw: Any) -> str:
- from shared.sx.helpers import root_header_sx, render_to_sx
- from shared.sx.helpers import call_url
- from shared.sx.parser import SxExpr
-
- list_url = kw.get("list_url", "/")
- account_url = call_url(ctx, "account_url", "")
- auth_hdr = await render_to_sx("auth-header-row",
- account_url=account_url,
- select_colours=ctx.get("select_colours", ""),
- account_nav=_as_sx_nav(ctx),
- oob=True,
- )
- orders_hdr = await render_to_sx("orders-header-row", list_url=list_url)
- auth_child_oob = await render_to_sx("oob-header-sx",
- parent_id="auth-header-child",
- row=SxExpr(orders_hdr))
- root_hdr = await root_header_sx(ctx, oob=True)
- return "(<> " + auth_hdr + " " + auth_child_oob + " " + root_hdr + ")"
-
-
-async def _orders_mobile(ctx: dict, **kw: Any) -> str:
- from shared.sx.helpers import mobile_menu_sx, mobile_root_nav_sx
- return mobile_menu_sx(await mobile_root_nav_sx(ctx))
-
-
-async def _order_detail_full(ctx: dict, **kw: Any) -> str:
- from shared.sx.helpers import root_header_sx, render_to_sx
- from shared.sx.helpers import call_url
- from shared.sx.parser import SxExpr
-
- list_url = kw.get("list_url", "/")
- detail_url = kw.get("detail_url", "/")
- account_url = call_url(ctx, "account_url", "")
- root_hdr = await root_header_sx(ctx)
- order_row = await render_to_sx(
- "menu-row-sx",
- id="order-row", level=3, colour="sky", link_href=detail_url,
- link_label="Order", icon="fa fa-gbp",
- )
- auth_hdr = await render_to_sx("auth-header-row",
- account_url=account_url,
- select_colours=ctx.get("select_colours", ""),
- account_nav=_as_sx_nav(ctx),
- )
- orders_hdr = await render_to_sx("orders-header-row", list_url=list_url)
- detail_header = await render_to_sx(
- "order-detail-header-stack",
- auth=SxExpr(auth_hdr),
- orders=SxExpr(orders_hdr),
- order=SxExpr(order_row),
- )
- return "(<> " + root_hdr + " " + detail_header + ")"
-
-
-async def _order_detail_oob(ctx: dict, **kw: Any) -> str:
- from shared.sx.helpers import root_header_sx, render_to_sx
- from shared.sx.parser import SxExpr
-
- detail_url = kw.get("detail_url", "/")
- order_row_oob = await render_to_sx(
- "menu-row-sx",
- id="order-row", level=3, colour="sky", link_href=detail_url,
- link_label="Order", icon="fa fa-gbp", oob=True,
- )
- header_child_oob = await render_to_sx("oob-header-sx",
- parent_id="orders-header-child",
- row=SxExpr(order_row_oob))
- root_hdr = await root_header_sx(ctx, oob=True)
- return "(<> " + header_child_oob + " " + root_hdr + ")"
-
-
-async def _order_detail_mobile(ctx: dict, **kw: Any) -> str:
- from shared.sx.helpers import mobile_menu_sx, mobile_root_nav_sx
- return mobile_menu_sx(await mobile_root_nav_sx(ctx))
-
-
-def _as_sx_nav(ctx: dict) -> Any:
- """Convert account_nav fragment to SxExpr for use in component calls."""
- from shared.sx.helpers import _as_sx
- return _as_sx(ctx.get("account_nav"))
+ from shared.sx.layouts import register_sx_layout
+ register_sx_layout("orders", "orders-layout-full", "orders-layout-oob", "orders-layout-mobile")
+ register_sx_layout("order-detail", "order-detail-layout-full", "order-detail-layout-oob", "order-detail-layout-mobile")
diff --git a/shared/sx/helpers.py b/shared/sx/helpers.py
index 30c76f3..bd7aebe 100644
--- a/shared/sx/helpers.py
+++ b/shared/sx/helpers.py
@@ -385,6 +385,59 @@ 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
+ defcomps can read ctx values as free variables.
+ """
+ from .jinja_bridge import get_component_env, _get_request_context
+ from .async_eval import async_eval_to_sx
+
+ ast = _build_component_ast(__name, **kwargs)
+ env = dict(get_component_env())
+ env.update(extra_env)
+ ctx = _get_request_context()
+ return await async_eval_to_sx(ast, env, ctx)
+
+
async def render_to_sx(__name: str, **kwargs: Any) -> str:
"""Call a defcomp and get SX wire format back. No SX string literals.
diff --git a/shared/sx/layouts.py b/shared/sx/layouts.py
index 7ba5f4c..df69649 100644
--- a/shared/sx/layouts.py
+++ b/shared/sx/layouts.py
@@ -15,7 +15,7 @@ from typing import Any, Callable, Awaitable
from .helpers import (
root_header_sx, post_header_sx, post_admin_header_sx,
- oob_header_sx, header_child_sx,
+ oob_header_sx,
mobile_menu_sx, mobile_root_nav_sx,
post_mobile_nav_sx, post_admin_mobile_nav_sx,
)
@@ -87,15 +87,6 @@ def get_layout(name: str) -> Layout | None:
# Built-in layouts
# ---------------------------------------------------------------------------
-async def _root_full(ctx: dict, **kw: Any) -> str:
- return await root_header_sx(ctx)
-
-
-async def _root_oob(ctx: dict, **kw: Any) -> str:
- root_hdr = await root_header_sx(ctx)
- return await oob_header_sx("root-header-child", "root-header-child", root_hdr)
-
-
async def _post_full(ctx: dict, **kw: Any) -> str:
root_hdr = await root_header_sx(ctx)
post_hdr = await post_header_sx(ctx)
@@ -127,10 +118,6 @@ async def _post_admin_oob(ctx: dict, **kw: Any) -> str:
return "(<> " + post_hdr + " " + admin_oob + ")"
-async def _root_mobile(ctx: dict, **kw: Any) -> str:
- return await mobile_root_nav_sx(ctx)
-
-
async def _post_mobile(ctx: dict, **kw: Any) -> str:
return mobile_menu_sx(await post_mobile_nav_sx(ctx), await mobile_root_nav_sx(ctx))
@@ -145,11 +132,53 @@ async def _post_admin_mobile(ctx: dict, **kw: Any) -> str:
)
-register_layout(Layout("root", _root_full, _root_oob, _root_mobile))
register_layout(Layout("post", _post_full, _post_oob, _post_mobile))
register_layout(Layout("post-admin", _post_admin_full, _post_admin_oob, _post_admin_mobile))
+# ---------------------------------------------------------------------------
+# register_sx_layout — declarative layout from .sx defcomp names
+# ---------------------------------------------------------------------------
+# (defined below, used immediately after for built-in "root" layout)
+# ---------------------------------------------------------------------------
+
+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::
+
+ register_sx_layout("account", "account-layout-full",
+ "account-layout-oob", "account-layout-mobile")
+ """
+ from .helpers import render_to_sx_with_env, _ctx_to_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()})
+ 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()})
+ 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()})
+ return await render_to_sx_with_env(mobile_defcomp, env)
+
+ register_layout(Layout(name, full_fn, oob_fn, mobile_fn))
+
+
+# Register built-in "root" layout via .sx defcomps
+register_sx_layout("root", "layout-root-full", "layout-root-oob", "layout-root-mobile")
+
+
# ---------------------------------------------------------------------------
# Callable layout — services register custom Python layout functions
# ---------------------------------------------------------------------------
diff --git a/shared/sx/templates/layout.sx b/shared/sx/templates/layout.sx
index ba7571e..11b1dd4 100644
--- a/shared/sx/templates/layout.sx
+++ b/shared/sx/templates/layout.sx
@@ -146,6 +146,33 @@
(when auth-menu
(div :class "p-3 border-t border-stone-200" auth-menu))))
+;; ---------------------------------------------------------------------------
+;; Root header/mobile wrappers — read ctx values from env free variables
+;; Used by register_sx_layout so .sx defcomps compose without Python
+;; ---------------------------------------------------------------------------
+
+(defcomp ~root-header (&key oob)
+ (~header-row-sx :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 oob))
+
+(defcomp ~root-mobile ()
+ (~mobile-root-nav :nav-tree nav-tree :auth-menu auth-menu))
+
+;; ---------------------------------------------------------------------------
+;; Built-in layout defcomps — used by register_sx_layout("root", ...)
+;; ---------------------------------------------------------------------------
+
+(defcomp ~layout-root-full ()
+ (~root-header))
+
+(defcomp ~layout-root-oob ()
+ (~oob-header-sx :parent-id "root-header-child" :row (~root-header)))
+
+(defcomp ~layout-root-mobile ()
+ (~root-mobile))
+
(defcomp ~error-content (&key errnum message image)
(div :class "text-center p-8 max-w-lg mx-auto"
(div :class "font-bold text-2xl md:text-4xl text-red-500 mb-4" errnum)