(theme): Añade región Aside

Introduce `CoreRegions::Aside` como nueva región de plantilla, entre
Header y Content, y Template ahora envuelve el cuerpo en `div.wrapper`,
omitiendo el renderizado si no hay contenido.

`CoreRegion`, `ReservedRegion` y `CoreTemplate` pasan a plural
(`CoreRegions`, `ReservedRegions`, `CoreTemplates`) para no confundirlos
con otras definiciones, y se amplía la documentación del módulo theme.
This commit is contained in:
Manuel Cillero 2026-08-02 10:40:36 +02:00
parent c49bf8f56a
commit 1cab0ae9a0
10 changed files with 180 additions and 115 deletions

View file

@ -102,7 +102,7 @@ impl Extension for SuperMenu {
)), )),
)); ));
InRegion::Global(&CoreRegion::Header).add( InRegion::Global(&CoreRegions::Header).add(
bs::Container::new() bs::Container::new()
.with_width(bs::container::Width::FluidMax(UnitValue::RelRem(75.0))) .with_width(bs::container::Width::FluidMax(UnitValue::RelRem(75.0)))
.with_child(navbar_menu), .with_child(navbar_menu),

View file

@ -55,7 +55,7 @@ impl fmt::Debug for Region {
impl Default for Region { impl Default for Region {
fn default() -> Self { fn default() -> Self {
Region { Region {
region: &CoreRegion::Content, region: &CoreRegions::Content,
} }
} }
} }
@ -90,17 +90,33 @@ impl Component for Region {
} }
impl Region { impl Region {
/// Define el componente que renderizará [`CoreRegion::Header`]. /// Define el componente que renderizará [`CoreRegions::Header`].
pub fn header() -> Self { pub fn header() -> Self {
Region { Region {
region: &CoreRegion::Header, region: &CoreRegions::Header,
} }
} }
/// Define el componente que renderizará [`CoreRegion::Footer`]. /// Define el componente que renderizará [`CoreRegions::Aside`].
pub fn aside() -> Self {
Region {
region: &CoreRegions::Aside,
}
}
/// Define el componente que renderizará [`CoreRegions::Content`].
///
/// Equivale a [`Self::new()`] o [`Self::default()`], que ya usan esta región por defecto.
pub fn content() -> Self {
Region {
region: &CoreRegions::Content,
}
}
/// Define el componente que renderizará [`CoreRegions::Footer`].
pub fn footer() -> Self { pub fn footer() -> Self {
Region { Region {
region: &CoreRegion::Footer, region: &CoreRegions::Footer,
} }
} }

View file

@ -4,25 +4,32 @@ use std::fmt;
/// Componente que renderiza el cuerpo de una plantilla de regiones. /// Componente que renderiza el cuerpo de una plantilla de regiones.
/// ///
/// La composición por defecto usa el componente [`Region`](crate::base::component::layout::Region) /// La composición por defecto usa el componente [`Region`] para mostrar, en este orden, las
/// para mostrar, en este orden, las regiones [`CoreRegion::Header`], [`CoreRegion::Content`] y /// regiones [`CoreRegions::Header`], [`CoreRegions::Aside`], [`CoreRegions::Content`] y
/// [`CoreRegion::Footer`]. /// [`CoreRegions::Footer`], envueltas en un contenedor `div.wrapper` que un tema puede maquetar a
/// su gusto (por ejemplo, usando CSS Grid, para que `Aside` se muestre como columna lateral junto a
/// `Content`). Si `Aside`, o cualquier otra región, no tiene contenido, no se renderiza.
/// ///
/// No incluye las regiones reservadas /// No incluye las regiones reservadas ([`ReservedRegions::PageTop`] y
/// [`ReservedRegion::PageTop`](crate::response::ReservedRegion::PageTop) y /// [`ReservedRegions::PageBottom`]) porque el propio [`Page::render()`] las añade antes y después
/// [`ReservedRegion::PageBottom`](crate::response::ReservedRegion::PageBottom) porque el propio /// del resultado de [`Theme::render_page_body()`] para que se rendericen siempre,
/// [`Page::render()`](crate::response::Page::render) las añade antes y después del resultado de /// independientemente de la plantilla que se use.
/// [`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 /// 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 /// componente en [`Theme::handle_component()`] y hacer [`downcast_ref()`] sobre el [`TemplateRef`]
/// [`downcast_ref()`](crate::core::AnyCast::downcast_ref) sobre el [`TemplateRef`] que devuelve /// que devuelve [`Self::template()`], para compararlo con la variante deseada.
/// [`Self::template()`], para compararlo con la variante deseada.
/// ///
/// Como cualquier otro componente, participa también en el despacho de las /// Como cualquier otro componente, participa también en el despacho de las
/// [acciones de componentes](crate::base::action::component) para que otras extensiones puedan /// [acciones de componentes](crate::base::action::component) para que otras extensiones puedan
/// intervenir en su renderizado. /// intervenir en su renderizado.
///
/// [`Region`]: crate::base::component::layout::Region
/// [`ReservedRegions::PageTop`]: crate::response::ReservedRegions::PageTop
/// [`ReservedRegions::PageBottom`]: crate::response::ReservedRegions::PageBottom
/// [`Page::render()`]: crate::response::Page::render
/// [`Theme::render_page_body()`]: crate::core::theme::Theme::render_page_body
/// [`Theme::handle_component()`]: crate::core::theme::Theme::handle_component
/// [`downcast_ref()`]: crate::core::AnyCast::downcast_ref
#[derive(Clone, Getters)] #[derive(Clone, Getters)]
pub struct Template { pub struct Template {
/// Devuelve la plantilla subyacente. /// Devuelve la plantilla subyacente.
@ -41,7 +48,7 @@ impl fmt::Debug for Template {
impl Default for Template { impl Default for Template {
fn default() -> Self { fn default() -> Self {
Template { Template {
template: &CoreTemplate::Standard, template: &CoreTemplates::Standard,
} }
} }
} }
@ -58,19 +65,39 @@ impl Component for Template {
} }
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! { let body = html! {
(layout::Region::header().render(cx).await) (layout::Region::header().render(cx).await)
(layout::Region::default().render(cx).await) (layout::Region::aside().render(cx).await)
(layout::Region::content().render(cx).await)
(layout::Region::footer().render(cx).await) (layout::Region::footer().render(cx).await)
};
if body.is_empty() {
return Ok(html! {});
}
Ok(html! {
div.wrapper {
(body)
}
}) })
} }
} }
impl Template { impl Template {
/// Define el componente que renderizará [`CoreTemplate::Admin`]. /// Define el componente que renderizará [`CoreTemplates::Standard`].
///
/// Equivale a [`Self::new()`] o [`Self::default()`], que ya usan esta plantilla por defecto.
pub fn standard() -> Self {
Template {
template: &CoreTemplates::Standard,
}
}
/// Define el componente que renderizará [`CoreTemplates::Admin`].
pub fn admin() -> Self { pub fn admin() -> Self {
Template { Template {
template: &CoreTemplate::Admin, template: &CoreTemplates::Admin,
} }
} }

View file

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

View file

@ -2,7 +2,7 @@ use crate::auth::CurrentUser;
use crate::core::TypeInfo; use crate::core::TypeInfo;
use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage}; use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage};
use crate::core::theme::all::DEFAULT_THEME; use crate::core::theme::all::DEFAULT_THEME;
use crate::core::theme::{ChildrenInRegions, CoreRegion, CoreTemplate}; use crate::core::theme::{ChildrenInRegions, CoreRegions, CoreTemplates};
use crate::core::theme::{RegionRef, TemplateRef, ThemeRef}; use crate::core::theme::{RegionRef, TemplateRef, ThemeRef};
use crate::html::{Assets, Favicon, JavaScript, Preload, StyleSheet}; use crate::html::{Assets, Favicon, JavaScript, Preload, StyleSheet};
use crate::html::{Markup, Props, PropsOp, RoutePath, html}; use crate::html::{Markup, Props, PropsOp, RoutePath, html};
@ -77,7 +77,7 @@ pub enum ContextError {
/// # use pagetop_aliner::Aliner; /// # use pagetop_aliner::Aliner;
/// fn prepare_context<C: Contextual>(cx: C) -> C { /// fn prepare_context<C: Contextual>(cx: C) -> C {
/// cx.with_langid(&Locale::resolve("es-ES")) /// cx.with_langid(&Locale::resolve("es-ES"))
/// .with_template(&CoreTemplate::Standard) /// .with_template(&CoreTemplates::Standard)
/// .with_theme(&Aliner) /// .with_theme(&Aliner)
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico")))) /// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css"))) /// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
@ -330,7 +330,7 @@ pub struct Context {
impl Default for Context { impl Default for Context {
fn default() -> Self { fn default() -> Self {
Self::base(None, &CoreTemplate::Standard) Self::base(None, &CoreTemplates::Standard)
} }
} }
@ -369,15 +369,15 @@ impl Context {
/// Para un contexto sin petición (renderizar un componente de forma aislada, en tests o fuera /// Para un contexto sin petición (renderizar un componente de forma aislada, en tests o fuera
/// del ciclo de una petición web), usa [`Context::default()`]. /// del ciclo de una petición web), usa [`Context::default()`].
pub fn new(request: HttpRequest) -> Self { pub fn new(request: HttpRequest) -> Self {
Self::base(Some(request), &CoreTemplate::Standard) Self::base(Some(request), &CoreTemplates::Standard)
} }
/// Crea un nuevo contexto asociado a una petición HTTP, con la plantilla de administración. /// Crea un nuevo contexto asociado a una petición HTTP, con la plantilla de administración.
/// ///
/// El contexto inicializa el idioma, el tema y la plantilla [`CoreTemplate::Admin`], sin /// El contexto inicializa el idioma, el tema y la plantilla [`CoreTemplates::Admin`], sin
/// favicon ni otros recursos cargados. /// favicon ni otros recursos cargados.
pub fn admin(request: HttpRequest) -> Self { pub fn admin(request: HttpRequest) -> Self {
Self::base(Some(request), &CoreTemplate::Admin) Self::base(Some(request), &CoreTemplates::Admin)
} }
// Extrae el `CurrentUser` inyectado por middleware en las extensiones de la petición, o // Extrae el `CurrentUser` inyectado por middleware en las extensiones de la petición, o
@ -629,7 +629,8 @@ impl Contextual for Context {
#[builder_fn] #[builder_fn]
fn with_child(mut self, op: impl Into<ChildOp>) -> Self { fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
self.regions.alter_child_in(&CoreRegion::Content, op.into()); self.regions
.alter_child_in(&CoreRegions::Content, op.into());
self self
} }

View file

@ -44,15 +44,14 @@
//! Un tema puede personalizarse en tres pasos, cada uno necesario sólo si lo que ofrece PageTop por //! Un tema puede personalizarse en tres pasos, cada uno necesario sólo si lo que ofrece PageTop por
//! defecto no basta: //! defecto no basta:
//! //!
//! 1. **Definir regiones nuevas**. Por defecto, PageTop define [`CoreRegion`] (`Header`, `Content`, //! 1. **Definir regiones nuevas**. Por defecto, PageTop define [`CoreRegions`] (`Header`, `Aside`,
//! `Footer`) como las regiones de plantilla que se asumen siempre disponibles, y //! `Content`, `Footer`) como regiones de plantilla siempre disponibles, y [`ReservedRegions`]
//! [`ReservedRegion`](crate::response::ReservedRegion) (`PageTop`, `PageBottom`) como las //! (`PageTop`, `PageBottom`) como regiones reservadas que se renderizan al margen de cualquier
//! regiones reservadas que se renderizan al margen de cualquier plantilla. Un tema puede definir //! plantilla. Un tema puede definir su propio *enum* que implemente [`RegionName`] para
//! su propio *enum* que implemente [`RegionName`] para **añadir** nuevas regiones que PageTop no //! **añadir** nuevas regiones que PageTop no ofrece (como barras laterales, regiones específicas
//! ofrece (por ejemplo, una barra lateral). No es necesario redefinir las de [`CoreRegion`] ni //! para menús, sliders, cabeceras hero, etc.). No es necesario redefinir las de [`CoreRegions`]
//! las de [`ReservedRegion`](crate::response::ReservedRegion), que ya existen y se asume que //! ni las de [`ReservedRegions`], que ya existen y se asume que cualquier tema respeta.
//! cualquier tema respeta. //! 2. **Definir plantillas nuevas**. Por defecto existe [`CoreTemplates`], con las plantillas
//! 2. **Definir plantillas nuevas**. Por defecto existe [`CoreTemplate`], con las plantillas
//! `Standard` y `Admin` que usan `Page::new()` y `Page::admin()` respectivamente, y que son //! `Standard` y `Admin` que usan `Page::new()` y `Page::admin()` respectivamente, y que son
//! siempre las mismas: no hay un método de `Theme` para elegir una plantilla predeterminada //! siempre las mismas: no hay un método de `Theme` para elegir una plantilla predeterminada
//! distinta. Un tema puede definir su propio *enum* que implemente [`TemplateName`] para //! distinta. Un tema puede definir su propio *enum* que implemente [`TemplateName`] para
@ -66,7 +65,7 @@
//! ([`Region::region()`](crate::base::component::layout::Region::region) o //! ([`Region::region()`](crate::base::component::layout::Region::region) o
//! [`Template::template()`](crate::base::component::layout::Template::template)) con //! [`Template::template()`](crate::base::component::layout::Template::template)) con
//! [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia el tipo concreto (por //! [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia el tipo concreto (por
//! ejemplo, [`CoreTemplate`] o el propio *enum* del tema). `pagetop-bootsier` hace exactamente //! ejemplo, [`CoreTemplates`] o el propio *enum* del tema). `pagetop-bootsier` hace exactamente
//! esto para maquetar `Standard` y `Admin` de forma distinta, sin necesitar sus propias //! esto para maquetar `Standard` y `Admin` de forma distinta, sin necesitar sus propias
//! variantes de plantilla. //! variantes de plantilla.
//! //!
@ -96,7 +95,7 @@
//! //!
//! ```rust,no_run //! ```rust,no_run
//! # use pagetop::prelude::*; //! # use pagetop::prelude::*;
//! InRegion::Global(&CoreRegion::Footer).add(PoweredBy::new()); //! InRegion::Global(&CoreRegions::Footer).add(PoweredBy::new());
//! ``` //! ```
//! //!
//! El componente se guarda como **prototipo**: cada página recibe un clon fresco en el momento del //! El componente se guarda como **prototipo**: cada página recibe un clon fresco en el momento del
@ -108,6 +107,8 @@
//! [ciclo de renderizado](crate::core::component::ComponentRender). Esto permite registrarlo una //! [ciclo de renderizado](crate::core::component::ComponentRender). Esto permite registrarlo una
//! sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta de la petición o //! sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta de la petición o
//! si el usuario actual está autenticado. //! si el usuario actual está autenticado.
//!
//! [`ReservedRegions`]: crate::response::ReservedRegions
use crate::AutoDefault; use crate::AutoDefault;
use crate::core::AnyInfo; use crate::core::AnyInfo;
@ -117,26 +118,30 @@ use crate::locale::L10n;
/// Interfaz común para las regiones lógicas del `<body>`. /// Interfaz común para las regiones lógicas del `<body>`.
/// ///
/// Una `RegionName` representa un contenedor lógico identificado por un nombre de región. Su /// Una [`RegionName`] representa un contenedor lógico identificado por un nombre de región. Su
/// contenido se obtiene del [`Context`](crate::core::component::Context), donde los componentes /// contenido se obtiene del [`Context`], donde los componentes suelen registrarse usando
/// suelen registrarse usando implementaciones de métodos como /// implementaciones de métodos como [`Contextual::with_child_in()`].
/// [`Contextual::with_child_in()`](crate::core::component::Contextual::with_child_in).
/// ///
/// El contenido de una región viene determinado únicamente por su nombre, no por su tipo. Distintas /// El contenido de una región viene determinado únicamente por su nombre, no por su tipo. Distintas
/// implementaciones de [`RegionName`] que devuelvan el mismo nombre comparten el mismo conjunto de /// implementaciones de [`RegionName`] que devuelvan el mismo nombre comparten el mismo conjunto de
/// componentes registrados en el [`Context`](crate::core::component::Context). Un *enum* propio que /// componentes registrados en el [`Context`]. Un *enum* propio que implemente [`RegionName`] está
/// implemente [`RegionName`] está pensado para **añadir** regiones que PageTop no ofrece (con un /// pensado para **añadir** regiones que PageTop no ofrece (con un nombre propio que no colisione
/// nombre propio que no colisione con los de [`CoreRegion`] o /// con los de [`CoreRegions`] o [`ReservedRegions`]).
/// [`ReservedRegion`](crate::response::ReservedRegion)).
/// ///
/// El tema decide qué regiones mostrar en el `<body>`, normalmente usando una plantilla /// El tema decide qué regiones mostrar en el `<body>`, normalmente usando una plantilla
/// ([`TemplateName`]) al renderizar la página ([`Page`](crate::response::Page)). /// ([`TemplateName`]) al renderizar la página ([`Page`]).
/// ///
/// Requiere [`AnyInfo`] para que un [`RegionRef`] pueda recuperarse mediante /// Requiere [`AnyInfo`] para que un [`RegionRef`] pueda recuperarse mediante
/// [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia su tipo concreto (por /// [`AnyCast::downcast_ref()`] hacia su tipo concreto (por ejemplo, para que un tema distinga en
/// ejemplo, para que un tema distinga en /// [`Theme::handle_component()`] qué variante concreta está renderizando el componente [`Region`]).
/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) qué variante ///
/// concreta está renderizando el componente [`Region`](crate::base::component::layout::Region)). /// [`Context`]: crate::core::component::Context
/// [`Contextual::with_child_in()`]: crate::core::component::Contextual::with_child_in
/// [`ReservedRegions`]: crate::response::ReservedRegions
/// [`Page`]: crate::response::Page
/// [`AnyCast::downcast_ref()`]: crate::core::AnyCast::downcast_ref
/// [`Theme::handle_component()`]: crate::core::theme::Theme::handle_component
/// [`Region`]: crate::base::component::layout::Region
pub trait RegionName: Send + Sync + AnyInfo { pub trait RegionName: Send + Sync + AnyInfo {
/// Devuelve el nombre de la región. /// Devuelve el nombre de la región.
/// ///
@ -156,24 +161,34 @@ pub trait RegionName: Send + Sync + AnyInfo {
/// Referencia estática a una región. /// Referencia estática a una región.
pub type RegionRef = &'static dyn RegionName; pub type RegionRef = &'static dyn RegionName;
// **< CoreRegion >********************************************************************************* // **< CoreRegions >********************************************************************************
/// Regiones básicas que PageTop proporciona por defecto. /// Regiones básicas que PageTop proporciona por defecto.
/// ///
/// Comparten sus nombres (`"header"`, `"content"`, `"footer"`) con otras regiones que implementen /// Comparten sus nombres (`"header"`, `"aside"`, `"content"`, `"footer"`) con otras regiones que
/// [`RegionName`], por lo que comparten también el contenido registrado bajo esos nombres. Por /// implementen [`RegionName`], por lo que comparten también el contenido registrado bajo esos
/// defecto, son las regiones usadas por [`Template`](crate::base::component::layout::Template). /// nombres. Por defecto, son las regiones usadas por [`Template`].
/// ///
/// A estas regiones hay que sumar también las regiones internas reservadas por /// A estas regiones hay que sumar también las regiones internas reservadas por [`ReservedRegions`]
/// [`ReservedRegion`](crate::response::ReservedRegion) (`"page-top"` y `"page-bottom"`), que /// (`"page-top"` y `"page-bottom"`), que [`Page::render()`] renderiza en cualquier caso.
/// [`Page::render()`](crate::response::Page::render) renderiza en cualquier caso. ///
/// [`Template`]: crate::base::component::layout::Template
/// [`ReservedRegions`]: crate::response::ReservedRegions
/// [`Page::render()`]: crate::response::Page::render
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub enum CoreRegion { pub enum CoreRegions {
/// Región estándar para la **cabecera** del documento, de nombre `"header"`. /// Región estándar para la **cabecera** del documento, de nombre `"header"`.
/// ///
/// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc. /// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc.
Header, Header,
/// Región de **contenido secundario**, de nombre `"aside"`.
///
/// Se renderiza por defecto entre `Header` y `Content`. Un tema podría maquetarla, por ejemplo,
/// como columna lateral junto a `Content` y emplearla para menús secundarios o cualquier otro
/// contenido complementario al principal.
Aside,
/// Región principal de **contenido**, de nombre `"content"`. /// Región principal de **contenido**, de nombre `"content"`.
/// ///
/// Es la región donde se renderiza el contenido principal del documento. En general será la /// Es la región donde se renderiza el contenido principal del documento. En general será la
@ -187,11 +202,12 @@ pub enum CoreRegion {
Footer, Footer,
} }
impl RegionName for CoreRegion { impl RegionName for CoreRegions {
#[inline] #[inline]
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
match self { match self {
Self::Header => "header", Self::Header => "header",
Self::Aside => "aside",
Self::Content => "content", Self::Content => "content",
Self::Footer => "footer", Self::Footer => "footer",
} }
@ -200,9 +216,10 @@ impl RegionName for CoreRegion {
#[inline] #[inline]
fn label(&self) -> L10n { fn label(&self) -> L10n {
match self { match self {
Self::Header => L10n::l("region-header"), Self::Header => L10n::l("region_header"),
Self::Content => L10n::l("region-content"), Self::Aside => L10n::l("region_aside"),
Self::Footer => L10n::l("region-footer"), Self::Content => L10n::l("region_content"),
Self::Footer => L10n::l("region_footer"),
} }
} }
} }
@ -231,11 +248,11 @@ pub trait TemplateName: Send + Sync + AnyInfo {
/// Referencia estática a una plantilla. /// Referencia estática a una plantilla.
pub type TemplateRef = &'static dyn TemplateName; pub type TemplateRef = &'static dyn TemplateName;
// **< CoreTemplate >******************************************************************************* // **< CoreTemplates >******************************************************************************
/// Plantillas que PageTop proporciona por defecto. /// Plantillas que PageTop proporciona por defecto.
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub enum CoreTemplate { pub enum CoreTemplates {
/// Plantilla predeterminada, de nombre `"standard"`. /// Plantilla predeterminada, de nombre `"standard"`.
/// ///
/// Se emplea cuando no se selecciona ninguna otra plantilla explícitamente. /// Se emplea cuando no se selecciona ninguna otra plantilla explícitamente.
@ -248,7 +265,7 @@ pub enum CoreTemplate {
Admin, Admin,
} }
impl TemplateName for CoreTemplate { impl TemplateName for CoreTemplates {
#[inline] #[inline]
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
match self { match self {

View file

@ -3,7 +3,7 @@ use crate::base::component::{Html, Intro, IntroOpening, layout};
use crate::core::component::{ChildOp, Component, ComponentError, ComponentRender}; use crate::core::component::{ChildOp, Component, ComponentError, ComponentRender};
use crate::core::component::{Context, Contextual}; use crate::core::component::{Context, Contextual};
use crate::core::extension::Extension; use crate::core::extension::Extension;
use crate::core::theme::CoreRegion; use crate::core::theme::CoreRegions;
use crate::global; use crate::global;
use crate::html::{Markup, html}; use crate::html::{Markup, html};
use crate::locale::L10n; use crate::locale::L10n;
@ -84,9 +84,10 @@ pub trait Theme: Extension + Send + Sync {
/// regiones. /// regiones.
/// ///
/// Con la configuración por defecto, la plantilla estándar utiliza las regiones /// Con la configuración por defecto, la plantilla estándar utiliza las regiones
/// [`CoreRegion::Header`](crate::core::theme::CoreRegion::Header), /// [`CoreRegions::Header`](crate::core::theme::CoreRegions::Header),
/// [`CoreRegion::Content`](crate::core::theme::CoreRegion::Content) y /// [`CoreRegions::Aside`](crate::core::theme::CoreRegions::Aside),
/// [`CoreRegion::Footer`](crate::core::theme::CoreRegion::Footer) en ese orden. /// [`CoreRegions::Content`](crate::core::theme::CoreRegions::Content) y
/// [`CoreRegions::Footer`](crate::core::theme::CoreRegions::Footer) en ese orden.
/// ///
/// Los temas pueden sobrescribir este método para: /// Los temas pueden sobrescribir este método para:
/// ///
@ -198,7 +199,7 @@ pub trait Theme: Extension + Send + Sync {
/// component: &mut dyn Component, /// component: &mut dyn Component,
/// cx: &mut Context, /// cx: &mut Context,
/// ) -> Option<Result<Markup, ComponentError>> { /// ) -> Option<Result<Markup, ComponentError>> {
/// // Solo mutación: ajusta el componente y deja que otro nivel lo renderice. /// // Sólo mutación: ajusta el componente y deja que otro nivel lo renderice.
/// setup_component!(component, { /// setup_component!(component, {
/// Button => |btn| { btn.add_class("btn-primary"); }, /// Button => |btn| { btn.add_class("btn-primary"); },
/// }); /// });
@ -221,15 +222,15 @@ pub trait Theme: Extension + Send + Sync {
/// Contenido predefinido para la página de error "*403 - Forbidden*" (acceso denegado). /// Contenido predefinido para la página de error "*403 - Forbidden*" (acceso denegado).
/// ///
/// Normalmente se renderiza con la plantilla ya activa en la página (por ejemplo /// Normalmente se renderiza con la plantilla ya activa en la página (por ejemplo
/// [`CoreTemplate::Standard`](crate::core::theme::CoreTemplate::Standard)), para que el usuario /// [`CoreTemplates::Standard`](crate::core::theme::CoreTemplates::Standard)), para que el
/// no pierda el contexto de navegación del sitio. Los temas pueden sobrescribir este método /// usuario no pierda el contexto de navegación del sitio. Los temas pueden sobrescribir este
/// para personalizar completamente el diseño y el contenido de la página de error. /// método para personalizar completamente el diseño y el contenido de la página de error.
fn error_403(&self, page: &mut Page) { fn error_403(&self, page: &mut Page) {
if let Some(parent) = self.parent() { if let Some(parent) = self.parent() {
return parent.error_403(page); return parent.error_403(page);
} }
page.alter_title(L10n::l("error403_title")).alter_child_in( page.alter_title(L10n::l("error403_title")).alter_child_in(
&CoreRegion::Content, &CoreRegions::Content,
ChildOp::Prepend( ChildOp::Prepend(
Html::with(move |cx| { Html::with(move |cx| {
html! { html! {
@ -247,7 +248,7 @@ pub trait Theme: Extension + Send + Sync {
/// Contenido predefinido para la página de error "*404 - Not Found*" (recurso no encontrado). /// Contenido predefinido para la página de error "*404 - Not Found*" (recurso no encontrado).
/// ///
/// Normalmente se renderiza con la plantilla ya activa en la página (por ejemplo /// Normalmente se renderiza con la plantilla ya activa en la página (por ejemplo
/// [`CoreTemplate::Standard`](crate::core::theme::CoreTemplate::Standard)). Los temas pueden /// [`CoreTemplates::Standard`](crate::core::theme::CoreTemplates::Standard)). Los temas pueden
/// sobrescribir este método para personalizar completamente el diseño y el contenido de la /// sobrescribir este método para personalizar completamente el diseño y el contenido de la
/// página de error. /// página de error.
fn error_404(&self, page: &mut Page) { fn error_404(&self, page: &mut Page) {
@ -255,7 +256,7 @@ pub trait Theme: Extension + Send + Sync {
return parent.error_404(page); return parent.error_404(page);
} }
page.alter_title(L10n::l("error404_title")).alter_child_in( page.alter_title(L10n::l("error404_title")).alter_child_in(
&CoreRegion::Content, &CoreRegions::Content,
ChildOp::Prepend( ChildOp::Prepend(
Html::with(move |cx| { Html::with(move |cx| {
html! { html! {
@ -272,19 +273,15 @@ pub trait Theme: Extension + Send + Sync {
/// Permite al tema preparar y componer una página de **error fatal controlado**. /// Permite al tema preparar y componer una página de **error fatal controlado**.
/// ///
/// Esta función decide explícitamente devolver /// Devuelve explícitamente [`ErrorPage::BadRequest`], [`ErrorPage::InternalError`],
/// [`ErrorPage::BadRequest`](crate::response::ErrorPage::BadRequest), /// [`ErrorPage::ServiceUnavailable`] o [`ErrorPage::GatewayTimeout`], porque algo ha fallado,
/// [`ErrorPage::InternalError`](crate::response::ErrorPage::InternalError), /// pero el servidor sigue activo y el tema, el renderizado y el resto de componentes funcionan
/// [`ErrorPage::ServiceUnavailable`](crate::response::ErrorPage::ServiceUnavailable) o /// con normalidad.
/// [`ErrorPage::GatewayTimeout`](crate::response::ErrorPage::GatewayTimeout) porque algo ha
/// fallado, pero el servidor sigue activo y el tema, el renderizado y el resto de componentes
/// funcionan con normalidad.
/// ///
/// Por defecto, asigna el título al documento (`title`), se renderiza con la plantilla ya /// Por defecto, asigna el título al documento (`title`), se renderiza con la plantilla ya
/// activa en la página (normalmente /// activa en la página (normalmente [`CoreTemplates::Standard`]) y muestra un componente
/// [`CoreTemplate::Standard`](crate::core::theme::CoreTemplate::Standard)) y muestra un /// [`Intro`] con el código HTTP del error (`code`) y los mensajes proporcionados (`alert` y
/// componente [`Intro`] con el código HTTP del error (`code`) y los mensajes proporcionados /// `help`) como descripción del error.
/// (`alert` y `help`) como descripción del error.
/// ///
/// Este método no se utiliza en las implementaciones predefinidas de [`Self::error_403()`] ni /// Este método no se utiliza en las implementaciones predefinidas de [`Self::error_403()`] ni
/// [`Self::error_404()`], que definen su propio contenido específico. /// [`Self::error_404()`], que definen su propio contenido específico.
@ -296,12 +293,18 @@ pub trait Theme: Extension + Send + Sync {
/// ///
/// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la /// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la
/// página de error. /// página de error.
///
/// [`ErrorPage::BadRequest`]: crate::response::ErrorPage::BadRequest
/// [`ErrorPage::InternalError`]: crate::response::ErrorPage::InternalError
/// [`ErrorPage::ServiceUnavailable`]: crate::response::ErrorPage::ServiceUnavailable
/// [`ErrorPage::GatewayTimeout`]: crate::response::ErrorPage::GatewayTimeout
/// [`CoreTemplates::Standard`]: crate::core::theme::CoreTemplates::Standard
fn error_fatal(&self, page: &mut Page, code: StatusCode, title: L10n, alert: L10n, help: L10n) { fn error_fatal(&self, page: &mut Page, code: StatusCode, title: L10n, alert: L10n, help: L10n) {
if let Some(parent) = self.parent() { if let Some(parent) = self.parent() {
return parent.error_fatal(page, code, title, alert, help); return parent.error_fatal(page, code, title, alert, help);
} }
page.alter_title(title).alter_child_in( page.alter_title(title).alter_child_in(
&CoreRegion::Content, &CoreRegions::Content,
ChildOp::Prepend( ChildOp::Prepend(
Intro::new() Intro::new()
.with_title(L10n::l("error_code").with_arg("code", code.to_string())) .with_title(L10n::l("error_code").with_arg("code", code.to_string()))

View file

@ -1,5 +1,5 @@
use crate::core::component::{Child, ChildOp, Children, Component}; use crate::core::component::{Child, ChildOp, Children, Component};
use crate::core::theme::{CoreRegion, RegionRef, ThemeRef}; use crate::core::theme::{CoreRegions, RegionRef, ThemeRef};
use crate::{AutoDefault, UniqueId, builder_fn}; use crate::{AutoDefault, UniqueId, builder_fn};
use parking_lot::RwLock; use parking_lot::RwLock;
@ -132,13 +132,13 @@ impl ChildrenInRegions {
/// InRegion::Content.add(Html::with(|_| html! { "🎉 ¡Bienvenido!" })); /// InRegion::Content.add(Html::with(|_| html! { "🎉 ¡Bienvenido!" }));
/// ///
/// // Texto en la cabecera, visible en todos los temas. /// // Texto en la cabecera, visible en todos los temas.
/// InRegion::Global(&CoreRegion::Header).add(Html::with(|_| html! { "Publicidad" })); /// InRegion::Global(&CoreRegions::Header).add(Html::with(|_| html! { "Publicidad" }));
/// ``` /// ```
pub enum InRegion { pub enum InRegion {
/// Región principal de **contenido** por defecto. /// Región principal de **contenido** por defecto.
/// ///
/// Añade el componente a la región lógica de contenido principal de la aplicación. Internamente /// Añade el componente a la región lógica de contenido principal de la aplicación. Internamente
/// equivale a `InRegion::Global(&CoreRegion::Content)`. /// equivale a `InRegion::Global(&CoreRegions::Content)`.
Content, Content,
/// Región global compartida por todos los temas. /// Región global compartida por todos los temas.
/// ///
@ -173,19 +173,19 @@ impl InRegion {
/// })); /// }));
/// ///
/// // Texto en la cabecera. /// // Texto en la cabecera.
/// InRegion::Global(&CoreRegion::Header).add(Html::with(|_| { /// InRegion::Global(&CoreRegions::Header).add(Html::with(|_| {
/// html! { "Publicidad" } /// html! { "Publicidad" }
/// })); /// }));
/// ///
/// // Contenido sólo para la región del pie de página en un tema concreto. /// // Contenido sólo para la región del pie de página en un tema concreto.
/// InRegion::ForTheme(&theme::Basic, &CoreRegion::Footer).add(Html::with(|_| { /// InRegion::ForTheme(&theme::Basic, &CoreRegions::Footer).add(Html::with(|_| {
/// html! { "Aviso legal" } /// html! { "Aviso legal" }
/// })); /// }));
/// ``` /// ```
pub fn add(&self, component: impl Component) -> &Self { pub fn add(&self, component: impl Component) -> &Self {
let proto: Arc<dyn Component> = Arc::new(component); let proto: Arc<dyn Component> = Arc::new(component);
match self { match self {
InRegion::Content => Self::add_to_common(&CoreRegion::Content, proto), InRegion::Content => Self::add_to_common(&CoreRegions::Content, proto),
InRegion::Global(region) => Self::add_to_common(*region, proto), InRegion::Global(region) => Self::add_to_common(*region, proto),
InRegion::ForTheme(theme, region) => { InRegion::ForTheme(theme, region) => {
THEME_REGIONS THEME_REGIONS

View file

@ -10,7 +10,7 @@
//! composición del `<head>` y del `<body>`, y se ejecutan las acciones registradas por las //! composición del `<head>` y del `<body>`, y se ejecutan las acciones registradas por las
//! extensiones antes y después de generar los contenidos. //! extensiones antes y después de generar los contenidos.
//! //!
//! También define las regiones internas reservadas ([`ReservedRegion`]) que actúan como puntos de //! También define las regiones internas reservadas ([`ReservedRegions`]) que actúan como puntos de
//! anclaje globales al inicio y al final del `<body>`, fuera de las regiones que maqueta la //! anclaje globales al inicio y al final del `<body>`, fuera de las regiones que maqueta la
//! plantilla activa. //! plantilla activa.
@ -23,7 +23,7 @@ use crate::base::action;
use crate::base::component::layout; use crate::base::component::layout;
use crate::core::component::{AssetsOp, ChildOp, ComponentRender}; use crate::core::component::{AssetsOp, ChildOp, ComponentRender};
use crate::core::component::{Context, ContextError, Contextual}; use crate::core::component::{Context, ContextError, Contextual};
use crate::core::theme::{CoreRegion, RegionName, RegionRef, TemplateRef, ThemeRef}; use crate::core::theme::{CoreRegions, RegionName, RegionRef, TemplateRef, ThemeRef};
use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
use crate::html::{Attr, Props, PropsOp}; use crate::html::{Attr, Props, PropsOp};
use crate::html::{DOCTYPE, Markup, html}; use crate::html::{DOCTYPE, Markup, html};
@ -31,7 +31,7 @@ use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
use crate::web::HttpRequest; use crate::web::HttpRequest;
use crate::{AutoDefault, builder_fn}; use crate::{AutoDefault, builder_fn};
// **< ReservedRegion >***************************************************************************** // **< ReservedRegions >****************************************************************************
/// Regiones internas reservadas como puntos de anclaje globales. /// Regiones internas reservadas como puntos de anclaje globales.
/// ///
@ -41,7 +41,7 @@ use crate::{AutoDefault, builder_fn};
/// [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body). **No suelen usarse /// [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body). **No suelen usarse
/// como regiones "visibles" en los temas**, sino para inyectar contenido global o técnico. /// como regiones "visibles" en los temas**, sino para inyectar contenido global o técnico.
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub enum ReservedRegion { pub enum ReservedRegions {
/// Región interna situada al **inicio del `<body>`**, de nombre `"page-top"`. /// Región interna situada al **inicio del `<body>`**, de nombre `"page-top"`.
/// ///
/// Proporciona un contenedor donde las extensiones puedan inyectar elementos auxiliares antes /// Proporciona un contenedor donde las extensiones puedan inyectar elementos auxiliares antes
@ -63,7 +63,7 @@ pub enum ReservedRegion {
PageBottom, PageBottom,
} }
impl RegionName for ReservedRegion { impl RegionName for ReservedRegions {
#[inline] #[inline]
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
match self { match self {
@ -109,13 +109,13 @@ impl Page {
} }
} }
/// Crea una nueva instancia de página con la plantilla [`CoreTemplate::Admin`]. /// Crea una nueva instancia de página con la plantilla [`CoreTemplates::Admin`].
/// ///
/// Cada tema puede maquetarla de forma distinta capturando /// Cada tema puede maquetarla de forma distinta capturando
/// [`Template`](crate::base::component::layout::Template) en `handle_component()`, pero la /// [`Template`](crate::base::component::layout::Template) en `handle_component()`, pero la
/// plantilla en sí es la misma constante para cualquier tema. /// plantilla en sí es la misma constante para cualquier tema.
/// ///
/// [`CoreTemplate::Admin`]: crate::core::theme::CoreTemplate::Admin /// [`CoreTemplates::Admin`]: crate::core::theme::CoreTemplates::Admin
pub fn admin(request: HttpRequest) -> Self { pub fn admin(request: HttpRequest) -> Self {
Page { Page {
context: Context::admin(request), context: Context::admin(request),
@ -196,10 +196,10 @@ impl Page {
/// 2. Despacha [`action::page::BeforeRenderBody`] para que otras extensiones puedan realizar /// 2. Despacha [`action::page::BeforeRenderBody`] para que otras extensiones puedan realizar
/// ajustes previos sobre la página. /// ajustes previos sobre la página.
/// 3. **Construye el contenido del `<body>`**: /// 3. **Construye el contenido del `<body>`**:
/// - Renderiza la región reservada superior ([`ReservedRegion::PageTop`]). /// - Renderiza la región reservada superior ([`ReservedRegions::PageTop`]).
/// - Llama a [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body) para /// - Llama a [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body) para
/// renderizar las regiones del cuerpo principal de la página. /// renderizar las regiones del cuerpo principal de la página.
/// - Renderiza la región reservada inferior ([`ReservedRegion::PageBottom`]). /// - Renderiza la región reservada inferior ([`ReservedRegions::PageBottom`]).
/// 4. Ejecuta /// 4. Ejecuta
/// [`Theme::after_render_page_body()`](crate::core::theme::Theme::after_render_page_body) /// [`Theme::after_render_page_body()`](crate::core::theme::Theme::after_render_page_body)
/// para que el tema pueda aplicar ajustes finales. /// para que el tema pueda aplicar ajustes finales.
@ -221,9 +221,9 @@ impl Page {
// Renderiza el <body>. // Renderiza el <body>.
let body = html! { let body = html! {
(layout::Region::of(&ReservedRegion::PageTop).render(&mut self.context).await) (layout::Region::of(&ReservedRegions::PageTop).render(&mut self.context).await)
(self.context.theme().render_page_body(self).await) (self.context.theme().render_page_body(self).await)
(layout::Region::of(&ReservedRegion::PageBottom).render(&mut self.context).await) (layout::Region::of(&ReservedRegions::PageBottom).render(&mut self.context).await)
}; };
// Acciones específicas del tema después de renderizar el <body>. // Acciones específicas del tema después de renderizar el <body>.
@ -314,7 +314,8 @@ impl Contextual for Page {
#[builder_fn] #[builder_fn]
fn with_child(mut self, op: impl Into<ChildOp>) -> Self { fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
self.context.alter_child_in(&CoreRegion::Content, op.into()); self.context
.alter_child_in(&CoreRegions::Content, op.into());
self self
} }

View file

@ -11,7 +11,7 @@ async fn setup() {
// **< A theme that intercepts the `Template` component >******************************************* // **< A theme that intercepts the `Template` component >*******************************************
/// Replaces the default `Template` composition (`Header` + `Content` + `Footer`) with a fixed /// Replaces the default `Template` composition (`Header` + `Content` + `Footer`) with a fixed
/// marker string, for both `CoreTemplate::Standard` and `CoreTemplate::Admin`. Mirrors how /// marker string, for both `CoreTemplates::Standard` and `CoreTemplates::Admin`. Mirrors how
/// a real theme (e.g. `pagetop-bootsier`) tells its own layout apart from PageTop's default: by /// a real theme (e.g. `pagetop-bootsier`) tells its own layout apart from PageTop's default: by
/// intercepting the `Template` component in `handle_component()`, not by swapping which /// intercepting the `Template` component in `handle_component()`, not by swapping which
/// `TemplateRef` gets resolved. /// `TemplateRef` gets resolved.
@ -32,7 +32,7 @@ impl Theme for MarkerTheme {
_cx: &mut Context, _cx: &mut Context,
) -> Option<Result<Markup, ComponentError>> { ) -> Option<Result<Markup, ComponentError>> {
let template = (&*component).downcast_ref::<layout::Template>()?; let template = (&*component).downcast_ref::<layout::Template>()?;
template.template().downcast_ref::<CoreTemplate>()?; template.template().downcast_ref::<CoreTemplates>()?;
Some(Ok(html! { "marker-template-output" })) Some(Ok(html! { "marker-template-output" }))
} }
} }
@ -40,7 +40,7 @@ impl Theme for MarkerTheme {
// **< Default/Admin template identity is independent of the active theme >************************* // **< Default/Admin template identity is independent of the active theme >*************************
// //
// `Theme::default_template()`/`admin_template()` were removed: `Context::template()` always // `Theme::default_template()`/`admin_template()` were removed: `Context::template()` always
// resolves `Default`/`Admin` to the core `CoreTemplate::Standard`/`Admin` identity, regardless // resolves `Default`/`Admin` to the core `CoreTemplates::Standard`/`Admin` identity, regardless
// of which theme is active. Themes customize the actual rendering by intercepting the `Template` // of which theme is active. Themes customize the actual rendering by intercepting the `Template`
// component in `handle_component()` instead (see the tests further below). // component in `handle_component()` instead (see the tests further below).
@ -68,7 +68,7 @@ async fn explicit_template_is_not_overridden_by_a_later_with_theme() {
// A template explicitly set with `with_template()` prevails even if `with_theme()` is called // A template explicitly set with `with_template()` prevails even if `with_theme()` is called
// afterwards. // afterwards.
let cx = Context::default() let cx = Context::default()
.with_template(&CoreTemplate::Admin) .with_template(&CoreTemplates::Admin)
.with_theme(&pagetop::base::theme::Basic); .with_theme(&pagetop::base::theme::Basic);
assert_eq!(cx.template().name(), "admin"); assert_eq!(cx.template().name(), "admin");