""" Named layout presets for defpage. Each layout generates header rows for full-page and OOB rendering. Built-in layouts delegate to .sx defcomps via ``register_sx_layout``. Services register custom layouts via ``register_custom_layout``. Layouts are registered in ``_LAYOUT_REGISTRY`` and looked up by ``get_layout()`` at request time. """ from __future__ import annotations from typing import Any, Callable, Awaitable # --------------------------------------------------------------------------- # Layout protocol # --------------------------------------------------------------------------- class Layout: """A named layout that generates header rows for full and OOB rendering.""" __slots__ = ("name", "_full_fn", "_oob_fn", "_mobile_fn") def __init__( self, name: str, full_fn: Callable[..., str | Awaitable[str]], oob_fn: Callable[..., str | Awaitable[str]], mobile_fn: Callable[..., str | Awaitable[str]] | None = None, ): self.name = name self._full_fn = full_fn self._oob_fn = oob_fn self._mobile_fn = mobile_fn async def full_headers(self, ctx: dict, **kwargs: Any) -> str: result = self._full_fn(ctx, **kwargs) if hasattr(result, "__await__"): result = await result return result async def oob_headers(self, ctx: dict, **kwargs: Any) -> str: result = self._oob_fn(ctx, **kwargs) if hasattr(result, "__await__"): result = await result return result async def mobile_menu(self, ctx: dict, **kwargs: Any) -> str: if self._mobile_fn is None: return "" result = self._mobile_fn(ctx, **kwargs) if hasattr(result, "__await__"): result = await result return result def __repr__(self) -> str: return f"" # --------------------------------------------------------------------------- # Registry # --------------------------------------------------------------------------- _LAYOUT_REGISTRY: dict[str, Layout] = {} def register_layout(layout: Layout) -> None: """Register a layout preset.""" _LAYOUT_REGISTRY[layout.name] = layout def get_layout(name: str) -> Layout | None: """Look up a layout by name.""" return _LAYOUT_REGISTRY.get(name) # Built-in post/post-admin layouts are registered below via register_sx_layout, # after that function is defined. # --------------------------------------------------------------------------- # 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. 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 async def full_fn(ctx: dict, **kw: Any) -> str: 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 = {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 = {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 layouts via .sx defcomps register_sx_layout("root", "layout-root-full", "layout-root-oob", "layout-root-mobile") register_sx_layout("post", "layout-post-full", "layout-post-oob", "layout-post-mobile") register_sx_layout("post-admin", "layout-post-admin-full", "layout-post-admin-oob", "layout-post-admin-mobile") # --------------------------------------------------------------------------- # Callable layout — services register custom Python layout functions # --------------------------------------------------------------------------- _CUSTOM_LAYOUTS: dict[str, tuple] = {} # name → (full_fn, oob_fn) def register_custom_layout(name: str, full_fn: Callable[..., str | Awaitable[str]], oob_fn: Callable[..., str | Awaitable[str]], mobile_fn: Callable[..., str | Awaitable[str]] | None = None) -> None: """Register a custom layout function. Used by services with non-standard header patterns:: register_custom_layout("sx-section", full_fn=my_full_headers, oob_fn=my_oob_headers, mobile_fn=my_mobile_menu) """ register_layout(Layout(name, full_fn, oob_fn, mobile_fn))