(theme): Añade componentes Region y Template

Introduce Region y Template (`base::component::layout`) para componer el
`<body>` de una página, con captura explícita vía `handle_component()`
en lugar de maquetado implícito:

- `RegionName`/`TemplateName` (`core/theme.rs`) como interfaces de
  identidad; `CoreRegion` (`Header`/`Content`/`Footer`) y `CoreTemplate`
  (`Standard`/`Admin`) como catálogo por defecto.
- `ReservedRegion` (`PageTop`/`PageBottom`, en `response/page.rs`) queda
  fuera de `CoreRegion`. `Page::render()` las renderiza siempre,
  envolviendo `Theme::render_page_body()`, con independencia de la
  plantilla o el tema activos.
- Elimina `TemplateSource` en `Context`: la plantilla pasa a ser un
  `TemplateRef` fijo, sin resolución por tema (ese mecanismo nunca llegó
  a implementarse).
- La personalización por tema es por captura (`handle_component()` +
  `downcast_ref()`), no por sustituir qué constante se resuelve.
- Corrige referencias obsoletas a `DefaultRegions` por `CoreRegion`.
- Sustituye `tests/theme_template.rs` (API anterior) por
  `tests/component_template.rs`, que cubre el patrón de captura con el
  API nuevo.
This commit is contained in:
Manuel Cillero 2026-07-26 09:51:27 +02:00
parent e7f2563967
commit 8573aca29e
13 changed files with 560 additions and 391 deletions

View file

@ -1,5 +1,7 @@
//! Componentes nativos proporcionados por PageTop.
pub mod layout;
mod block;
pub use block::Block;

View file

@ -0,0 +1,7 @@
//! Definiciones para la composición de documentos ([`Region`] y [`Template`]).
mod region;
pub use region::Region;
mod template;
pub use template::Template;

View file

@ -0,0 +1,111 @@
use crate::prelude::*;
use std::fmt;
/// Componente que renderiza una región del `<body>`.
///
/// No recibe ningún contenido de quien lo construye. Lo obtiene directamente del [`Context`] en el
/// momento de renderizarse (ver [`Context::render_region()`]). Si la región no tiene contenido, no
/// se renderiza nada.
///
/// Si un tema necesita maquetar una región determinada de forma distinta, puede capturar este
/// componente en [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) y hacer
/// [`downcast_ref()`](crate::core::AnyCast::downcast_ref) sobre el [`RegionRef`] que devuelve
/// [`Self::region()`], para compararlo con la variante deseada.
///
/// Como cualquier otro componente, participa también en el despacho de las
/// [acciones de componentes](crate::base::action::component) para que otras extensiones puedan
/// intervenir en su renderizado.
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// struct Sidebar;
///
/// impl RegionName for Sidebar {
/// fn name(&self) -> &'static str {
/// "sidebar"
/// }
///
/// fn label(&self) -> L10n {
/// L10n::n("Sidebar")
/// }
/// }
///
/// let header = layout::Region::header();
/// let sidebar = layout::Region::of(&Sidebar);
/// ```
#[derive(Clone, Getters)]
pub struct Region {
/// Devuelve la región subyacente.
#[getters(copy)]
region: RegionRef,
}
impl fmt::Debug for Region {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Region")
.field("region", &self.region().name())
.finish()
}
}
impl Default for Region {
fn default() -> Self {
Region {
region: &CoreRegion::Content,
}
}
}
#[async_trait]
impl Component for Region {
fn new() -> Self {
Self::default()
}
/// Devuelve el nombre de la región subyacente como identificador del componente.
fn id(&self) -> Option<String> {
Some(self.region().name().to_owned())
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let name = self.region().name();
let content = cx.render_region(self.region()).await;
Ok(html! {
@if !content.is_empty() {
div
id=[self.id()]
class=(util::join!("region region-", name))
role="region"
aria-label=[self.region().label().lookup(cx)]
{
(content)
}
}
})
}
}
impl Region {
/// Define el componente que renderizará [`CoreRegion::Header`].
pub fn header() -> Self {
Region {
region: &CoreRegion::Header,
}
}
/// Define el componente que renderizará [`CoreRegion::Footer`].
pub fn footer() -> Self {
Region {
region: &CoreRegion::Footer,
}
}
/// Define el componente que renderizará la región indicada.
pub fn of(region: RegionRef) -> Self {
Region { region }
}
}

View file

@ -0,0 +1,81 @@
use crate::prelude::*;
use std::fmt;
/// Componente que renderiza el cuerpo de una plantilla de regiones.
///
/// La composición por defecto usa el componente [`Region`](crate::base::component::layout::Region)
/// para mostrar, en este orden, las regiones [`CoreRegion::Header`], [`CoreRegion::Content`] y
/// [`CoreRegion::Footer`].
///
/// No incluye las regiones reservadas
/// [`ReservedRegion::PageTop`](crate::response::ReservedRegion::PageTop) y
/// [`ReservedRegion::PageBottom`](crate::response::ReservedRegion::PageBottom) porque el propio
/// [`Page::render()`](crate::response::Page::render) las añade antes y después del resultado de
/// [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body) para que se
/// rendericen siempre, independientemente de la plantilla que se use.
///
/// Si un tema necesita maquetar una plantilla determinada de forma distinta, puede capturar este
/// componente en [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) y hacer
/// [`downcast_ref()`](crate::core::AnyCast::downcast_ref) sobre el [`TemplateRef`] que devuelve
/// [`Self::template()`], para compararlo con la variante deseada.
///
/// Como cualquier otro componente, participa también en el despacho de las
/// [acciones de componentes](crate::base::action::component) para que otras extensiones puedan
/// intervenir en su renderizado.
#[derive(Clone, Getters)]
pub struct Template {
/// Devuelve la plantilla subyacente.
#[getters(copy)]
template: TemplateRef,
}
impl fmt::Debug for Template {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Template")
.field("template", &self.template().name())
.finish()
}
}
impl Default for Template {
fn default() -> Self {
Template {
template: &CoreTemplate::Standard,
}
}
}
#[async_trait]
impl Component for Template {
fn new() -> Self {
Self::default()
}
/// Devuelve el nombre de la plantilla subyacente como identificador del componente.
fn id(&self) -> Option<String> {
Some(self.template().name().to_owned())
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! {
(layout::Region::header().render(cx).await)
(layout::Region::default().render(cx).await)
(layout::Region::footer().render(cx).await)
})
}
}
impl Template {
/// Define el componente que renderizará [`CoreTemplate::Admin`].
pub fn admin() -> Self {
Template {
template: &CoreTemplate::Admin,
}
}
/// Define el componente que renderizará la plantilla indicada.
pub fn of(template: TemplateRef) -> Self {
Template { template }
}
}

View file

@ -25,7 +25,7 @@ impl Theme for Basic {
.with_weight(-99),
))
.alter_child_in(
&DefaultRegions::Footer,
&CoreRegion::Footer,
ChildOp::AddIfEmpty(PoweredBy::new().into()),
);
}