The streaming page's component scan only covered the suspense placeholders, missing transitive deps from layout headers (e.g. ~cart-mini, ~auth-menu). Add layout.component_names to Layout class and include them plus page content_expr in the scan source. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
151 lines
5.4 KiB
Python
151 lines
5.4 KiB
Python
"""
|
|
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", "component_names")
|
|
|
|
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,
|
|
component_names: list[str] | None = None,
|
|
):
|
|
self.name = name
|
|
self._full_fn = full_fn
|
|
self._oob_fn = oob_fn
|
|
self._mobile_fn = mobile_fn
|
|
self.component_names = component_names or []
|
|
|
|
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"<Layout:{self.name}>"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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
|
|
comp_names = [f"~{full_defcomp}", f"~{oob_defcomp}"]
|
|
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)
|
|
comp_names.append(f"~{mobile_defcomp}")
|
|
|
|
register_layout(Layout(name, full_fn, oob_fn, mobile_fn, comp_names))
|
|
|
|
|
|
# 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))
|