♻️ Refactoriza la gestión de regiones y plantillas

This commit is contained in:
Manuel Cillero 2025-11-30 00:16:54 +01:00
parent 0746bbbee7
commit 9f79e4179a
15 changed files with 494 additions and 655 deletions

View file

@ -121,7 +121,7 @@ impl Theme for Aliner {
.with_weight(-99),
))
.alter_child_in(
Region::FOOTER,
&DefaultRegion::Footer,
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
);
}

View file

@ -1,46 +1,8 @@
//! Componentes nativos proporcionados por PageTop.
//!
//! Conviene destacar que PageTop distingue entre:
//!
//! - **Componentes estructurales** que definen el esqueleto de un documento HTML, como [`Template`]
//! y [`Region`], utilizados por [`Page`](crate::response::page::Page) para generar la estructura
//! final.
//! - **Componentes de contenido** (menús, barras, tarjetas, etc.), que se incluyen en las regiones
//! gestionadas por los componentes estructurales.
//!
//! El componente [`Template`] describe cómo maquetar el cuerpo del documento a partir de varias
//! regiones lógicas ([`Region`]). En función de la plantilla seleccionada, determina qué regiones
//! se renderizan y en qué orden. Por ejemplo, la plantilla predeterminada [`Template::DEFAULT`]
//! utiliza las regiones [`Region::HEADER`], [`Region::CONTENT`] y [`Region::FOOTER`].
//!
//! Un componente [`Region`] es un contenedor lógico asociado a un nombre de región. Su contenido se
//! obtiene del [`Context`](crate::core::component::Context), donde los componentes se registran
//! mediante [`Contextual::with_child_in()`](crate::core::component::Contextual::with_child_in) y
//! otros mecanismos similares, y se integra en el documento a través de [`Template`].
//!
//! Por su parte, una página ([`Page`](crate::response::page::Page)) representa un documento HTML
//! completo. Implementa [`Contextual`](crate::core::component::Contextual) para mantener su propio
//! [`Context`](crate::core::component::Context), donde gestiona el tema activo, la plantilla
//! seleccionada y los componentes asociados a cada región, y se encarga de generar la estructura
//! final de la página.
//!
//! De este modo, temas y extensiones colaboran sobre una estructura común: las aplicaciones
//! registran componentes en el [`Context`](crate::core::component::Context), las plantillas
//! organizan las regiones y las páginas generan el documento HTML resultante.
//!
//! Los temas pueden sobrescribir [`Template`] para exponer nuevas plantillas o adaptar las
//! predeterminadas, y lo mismo con [`Region`] para añadir regiones adicionales o personalizar su
//! representación.
mod html;
pub use html::Html;
mod region;
pub use region::Region;
mod template;
pub use template::Template;
mod block;
pub use block::Block;

View file

@ -1,150 +0,0 @@
use crate::prelude::*;
/// Componente estructural que renderiza el contenido de una región del documento.
///
/// `Region` actúa como un contenedor lógico asociado a un nombre de región. Su contenido se obtiene
/// del contexto de renderizado ([`Context`]), donde los componentes suelen registrarse con métodos
/// como [`Contextual::with_child_in()`]. Cada región puede integrarse posteriormente en el cuerpo
/// del documento mediante [`Template`], normalmente desde una página ([`Page`]).
#[derive(AutoDefault)]
pub struct Region {
#[default(AttrName::new(Self::DEFAULT))]
name: AttrName,
#[default(L10n::l("region-content"))]
label: L10n,
}
impl Component for Region {
fn new() -> Self {
Region::default()
}
fn id(&self) -> Option<String> {
self.name.get()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let Some(name) = self.name().get() else {
return PrepareMarkup::None;
};
let output = cx.render_region(&name);
if output.is_empty() {
return PrepareMarkup::None;
}
PrepareMarkup::With(html! {
div
id=[self.id()]
class=(join!("region region-", &name))
role="region"
aria-label=[self.label().lookup(cx)]
{
(output)
}
})
}
}
impl Region {
/// Región especial situada al **inicio del documento**.
///
/// Su función es proporcionar un punto estable donde las extensiones puedan inyectar contenido
/// global antes de renderizar el resto de regiones principales (cabecera, contenido, etc.).
///
/// No suele utilizarse en los temas como una región “visible” dentro del maquetado habitual,
/// sino como punto de anclaje para elementos auxiliares, marcadores técnicos, inicializadores o
/// contenido de depuración que deban situarse en la parte superior del documento.
///
/// Se considera una región **reservada** para este tipo de usos globales.
pub const PAGETOP: &str = "page-top";
/// Región estándar para la **cabecera** del documento.
///
/// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc.
pub const HEADER: &str = "header";
/// Región principal de **contenido**.
///
/// Es la región donde se espera que se renderice el contenido principal de la página (p. ej.
/// cuerpo de la ruta actual, bloques centrales, vistas principales, etc.). En muchos temas será
/// la región mínima imprescindible para que la página tenga sentido.
pub const CONTENT: &str = "content";
/// Región estándar para el **pie de página**.
///
/// Suele contener información legal, enlaces secundarios, créditos, etc.
pub const FOOTER: &str = "footer";
/// Región especial situada al **final del documento**.
///
/// Pensada para proporcionar un punto estable donde las extensiones puedan inyectar contenido
/// global después de renderizar el resto de regiones principales (cabecera, contenido, etc.).
///
/// No suele utilizarse en los temas como una región “visible” dentro del maquetado habitual,
/// sino como punto de anclaje para elementos auxiliares asociados a comportamientos dinámicos
/// que deban situarse en la parte inferior del documento.
///
/// Igual que [`Self::PAGETOP`], se considera una región **reservada** para este tipo de usos
/// globales.
pub const PAGEBOTTOM: &str = "page-bottom";
/// Región por defecto que se asigna cuando no se especifica ningún nombre.
///
/// Por diseño, la región por defecto es la de contenido principal ([`Self::CONTENT`]), de
/// manera que un tema sencillo pueda limitarse a definir una sola región funcional.
pub const DEFAULT: &str = Self::CONTENT;
/// Prepara una región para el nombre indicado.
///
/// El valor de `name` se utiliza como nombre de la región y como identificador (`id`) del
/// contenedor. Al renderizarse, este componente mostrará el contenido registrado en el contexto
/// bajo ese nombre.
pub fn named(name: impl AsRef<str>) -> Self {
Region {
name: AttrName::new(name),
label: L10n::default(),
}
}
/// Prepara una región para el nombre indicado con una etiqueta de accesibilidad.
///
/// El valor de `name` se utiliza como nombre de la región y como identificador (`id`) del
/// contenedor, mientras que `label` será el texto localizado que se usará como `aria-label` del
/// contenedor.
pub fn labeled(name: impl AsRef<str>, label: L10n) -> Self {
Region {
name: AttrName::new(name),
label,
}
}
// **< Region BUILDER >*************************************************************************
/// Establece o modifica el nombre de la región.
#[builder_fn]
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
self.name.alter_value(name);
self
}
/// Establece la etiqueta localizada de la región.
///
/// Esta etiqueta se utiliza como `aria-label` del contenedor predefinido `<div role="region">`,
/// lo que mejora la accesibilidad para lectores de pantalla y otras tecnologías de apoyo.
#[builder_fn]
pub fn with_label(mut self, label: L10n) -> Self {
self.label = label;
self
}
// **< Region GETTERS >*************************************************************************
/// Devuelve el nombre de la región.
pub fn name(&self) -> &AttrName {
&self.name
}
/// Devuelve la etiqueta localizada asociada a la región.
pub fn label(&self) -> &L10n {
&self.label
}
}

View file

@ -1,84 +0,0 @@
use crate::prelude::*;
/// Componente estructural para renderizar plantillas de contenido.
///
/// `Template` describe cómo se compone el cuerpo del documento a partir de varias regiones lógicas
/// ([`Region`]). En función de su nombre, decide qué regiones se renderizan y en qué orden.
///
/// Normalmente se invoca desde una página ([`Page`]), que consulta el nombre de plantilla guardado
/// en el [`Context`] y delega en `Template` la composición de las regiones que forman el cuerpo del
/// documento.
///
/// Los temas pueden sobrescribir este componente para exponer sus propias plantillas o adaptar las
/// plantillas predeterminadas.
#[derive(AutoDefault)]
pub struct Template {
#[default(AttrName::new(Self::DEFAULT))]
name: AttrName,
}
impl Component for Template {
fn new() -> Self {
Template::default()
}
fn id(&self) -> Option<String> {
self.name.get()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let Some(name) = self.name().get() else {
return PrepareMarkup::None;
};
match name.as_str() {
Self::DEFAULT | Self::ERROR => PrepareMarkup::With(html! {
(Region::labeled(Region::HEADER, L10n::l("region-header")).render(cx))
(Region::default().render(cx))
(Region::labeled(Region::FOOTER, L10n::l("region-footer")).render(cx))
}),
_ => PrepareMarkup::None,
}
}
}
impl Template {
/// Nombre de la plantilla predeterminada.
///
/// Por defecto define una estructura básica con las regiones [`Region::HEADER`],
/// [`Region::CONTENT`] y [`Region::FOOTER`], en ese orden. Esta plantilla se usa cuando no se
/// selecciona ninguna otra de forma explícita (ver [`Contextual::with_template()`]).
pub const DEFAULT: &str = "default";
/// Nombre de la plantilla de error.
///
/// Se utiliza para páginas de error u otros estados excepcionales. Por defecto reutiliza
/// la misma estructura que [`Self::DEFAULT`], pero permite a temas y extensiones distinguir
/// el contexto de error para aplicar estilos o contenidos específicos.
pub const ERROR: &str = "error";
/// Selecciona la plantilla asociada al nombre indicado.
///
/// El valor de `name` se utiliza como nombre de la plantilla y como identificador (`id`) del
/// componente.
pub fn named(name: impl AsRef<str>) -> Self {
Template {
name: AttrName::new(name),
}
}
// **< Template BUILDER >***********************************************************************
/// Establece o modifica el nombre de la plantilla seleccionada.
#[builder_fn]
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
self.name.alter_value(name);
self
}
// **< Template GETTERS >***********************************************************************
/// Devuelve el nombre de la plantilla seleccionada.
pub fn name(&self) -> &AttrName {
&self.name
}
}

View file

@ -23,7 +23,7 @@ impl Theme for Basic {
.with_weight(-99),
))
.alter_child_in(
Region::FOOTER,
&DefaultRegion::Footer,
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
);
}

View file

@ -1,7 +1,6 @@
use crate::base::component::Template;
use crate::core::component::ChildOp;
use crate::core::theme::all::DEFAULT_THEME;
use crate::core::theme::{ChildrenInRegions, ThemeRef};
use crate::core::theme::{ChildrenInRegions, RegionRef, TemplateRef, ThemeRef};
use crate::core::TypeInfo;
use crate::html::{html, Markup};
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
@ -68,7 +67,7 @@ pub enum ContextError {
/// fn prepare_context<C: Contextual>(cx: C) -> C {
/// cx.with_langid(&LangMatch::resolve("es-ES"))
/// .with_theme(&Aliner)
/// .with_template(Template::DEFAULT)
/// .with_template(&DefaultTemplate::Standard)
/// .with_assets(ContextOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
/// .with_assets(ContextOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
/// .with_assets(ContextOp::AddJavaScript(JavaScript::defer("/js/app.js")))
@ -92,7 +91,7 @@ pub trait Contextual: LangId {
/// Especifica la plantilla para renderizar el documento.
#[builder_fn]
fn with_template(self, template_name: &'static str) -> Self;
fn with_template(self, template: TemplateRef) -> Self;
/// Añade o modifica un parámetro dinámico del contexto.
#[builder_fn]
@ -102,9 +101,9 @@ pub trait Contextual: LangId {
#[builder_fn]
fn with_assets(self, op: ContextOp) -> Self;
/// Opera con [`ChildOp`] en una región (`region_name`) del documento.
/// Opera con [`ChildOp`] en una región del documento.
#[builder_fn]
fn with_child_in(self, region_name: impl AsRef<str>, op: ChildOp) -> Self;
fn with_child_in(self, region_ref: RegionRef, op: ChildOp) -> Self;
// **< Contextual GETTERS >*********************************************************************
@ -114,8 +113,8 @@ pub trait Contextual: LangId {
/// Devuelve el tema que se usará para renderizar el documento.
fn theme(&self) -> ThemeRef;
/// Devuelve el nombre de la plantilla usada para renderizar el documento.
fn template(&self) -> &str;
/// Devuelve la plantilla configurada para renderizar el documento.
fn template(&self) -> TemplateRef;
/// Recupera un parámetro como [`Option`].
fn param<T: 'static>(&self, key: &'static str) -> Option<&T>;
@ -208,7 +207,7 @@ pub struct Context {
request : Option<HttpRequest>, // Solicitud HTTP de origen.
langid : &'static LanguageIdentifier, // Identificador de idioma.
theme : ThemeRef, // Referencia al tema usado para renderizar.
template : &'static str, // Nombre de la plantilla usada para renderizar.
template : TemplateRef, // Plantilla usada para renderizar.
favicon : Option<Favicon>, // Favicon, si se ha definido.
stylesheets: Assets<StyleSheet>, // Hojas de estilo CSS.
javascripts: Assets<JavaScript>, // Scripts JavaScript.
@ -248,7 +247,7 @@ impl Context {
request,
langid,
theme : *DEFAULT_THEME,
template : Template::DEFAULT,
template : DEFAULT_THEME.default_template(),
favicon : None,
stylesheets: Assets::<StyleSheet>::new(),
javascripts: Assets::<JavaScript>::new(),
@ -286,10 +285,10 @@ impl Context {
markup
}
/// Renderiza los componentes de la región `region_name`.
pub fn render_region(&mut self, region_name: impl AsRef<str>) -> Markup {
/// Renderiza los componentes de una región.
pub fn render_region(&mut self, region_ref: RegionRef) -> Markup {
self.regions
.children_for(self.theme, region_name)
.children_for(self.theme, region_ref)
.render(self)
}
@ -417,8 +416,8 @@ impl Contextual for Context {
}
#[builder_fn]
fn with_template(mut self, template_name: &'static str) -> Self {
self.template = template_name;
fn with_template(mut self, template: TemplateRef) -> Self {
self.template = template;
self
}
@ -474,8 +473,8 @@ impl Contextual for Context {
}
#[builder_fn]
fn with_child_in(mut self, region_name: impl AsRef<str>, op: ChildOp) -> Self {
self.regions.alter_child_in(region_name, op);
fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
self.regions.alter_child_in(region_ref, op);
self
}
@ -489,7 +488,7 @@ impl Contextual for Context {
self.theme
}
fn template(&self) -> &str {
fn template(&self) -> TemplateRef {
self.template
}

View file

@ -4,24 +4,23 @@ use crate::core::AnyInfo;
use crate::locale::L10n;
use crate::{actions_boxed, service};
/// Representa una referencia a una extensión.
///
/// Las extensiones se definen como instancias estáticas globales para poder acceder a ellas desde
/// cualquier hilo de la ejecución sin necesidad de sincronización adicional.
pub type ExtensionRef = &'static dyn Extension;
/// Interfaz común que debe implementar cualquier extensión de PageTop.
///
/// Este *trait* es fácil de implementar, basta con declarar una estructura de tamaño cero para la
/// extensión y sobreescribir los métodos que sea necesario.
/// Este *trait* es fácil de implementar, basta con declarar una estructura sin campos para la
/// extensión y sobrescribir los métodos que sean necesarios. Por ejemplo:
///
/// ```rust
/// # use pagetop::prelude::*;
/// pub struct Blog;
///
/// impl Extension for Blog {
/// fn name(&self) -> L10n { L10n::n("Blog") }
/// fn description(&self) -> L10n { L10n::n("Blog system") }
/// fn name(&self) -> L10n {
/// L10n::n("Blog")
/// }
///
/// fn description(&self) -> L10n {
/// L10n::n("Blog system")
/// }
/// }
/// ```
pub trait Extension: AnyInfo + Send + Sync {
@ -34,14 +33,19 @@ pub trait Extension: AnyInfo + Send + Sync {
}
/// Descripción corta localizada de la extensión para paneles, listados, etc.
///
/// Por defecto devuelve un valor vacío (`L10n::default()`).
fn description(&self) -> L10n {
L10n::default()
}
/// Devuelve una referencia a esta misma extensión cuando se trata de un tema.
/// Devuelve una referencia a esta misma extensión cuando actúa como un tema.
///
/// Para ello, debe implementar [`Extension`] y también [`Theme`](crate::core::theme::Theme). Si
/// la extensión no es un tema, este método devuelve `None` por defecto.
/// Para ello, la implementación concreta debe ser una extensión que también implemente
/// [`Theme`](crate::core::theme::Theme). Por defecto, asume que la extensión no es un tema y
/// devuelve `None`.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
@ -61,17 +65,17 @@ pub trait Extension: AnyInfo + Send + Sync {
/// Otras extensiones que deben habilitarse **antes** de esta.
///
/// PageTop las resolverá automáticamente respetando el orden durante el arranque de la
/// aplicación.
/// PageTop resolverá automáticamente estas dependencias respetando el orden durante el arranque
/// de la aplicación.
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![]
}
/// Devuelve la lista de acciones que la extensión va a registrar.
/// Devuelve la lista de acciones que la extensión registra.
///
/// Estas [acciones](crate::core::action) se despachan por orden de registro o por
/// [peso](crate::Weight), permitiendo personalizar el comportamiento de la aplicación en puntos
/// específicos.
/// [peso](crate::Weight) (ver [`actions_boxed!`](crate::actions_boxed)), permitiendo
/// personalizar el comportamiento de la aplicación en puntos específicos.
fn actions(&self) -> Vec<ActionBox> {
actions_boxed![]
}
@ -85,6 +89,8 @@ pub trait Extension: AnyInfo + Send + Sync {
/// Configura los servicios web de la extensión, como rutas, *middleware*, acceso a ficheros
/// estáticos, etc., usando [`ServiceConfig`](crate::service::web::ServiceConfig).
///
/// # Ejemplo
///
/// ```rust,ignore
/// # use pagetop::prelude::*;
/// pub struct ExtensionSample;
@ -98,11 +104,15 @@ pub trait Extension: AnyInfo + Send + Sync {
#[allow(unused_variables)]
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {}
/// Permite crear extensiones para deshabilitar y desinstalar recursos de otras de versiones
/// anteriores de la aplicación.
/// Permite declarar extensiones destinadas a deshabilitar o desinstalar recursos de otras
/// extensiones asociadas a versiones anteriores de la aplicación.
///
/// Actualmente no se usa, pero se deja como *placeholder* para futuras implementaciones.
/// Actualmente PageTop no utiliza este método, pero se reserva como *placeholder* para futuras
/// implementaciones.
fn drop_extensions(&self) -> Vec<ExtensionRef> {
vec![]
}
}
/// Representa una referencia a una extensión.
pub type ExtensionRef = &'static dyn Extension;

View file

@ -1,18 +1,206 @@
//! API para añadir y gestionar nuevos temas.
//!
//! En PageTop un tema es la *piel* de la aplicación. Es responsable último de los estilos,
//! tipografías, espaciados y cualquier otro detalle visual o interactivo (animaciones, scripts de
//! interfaz, etc.).
//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension) y
//! también [`Theme`], de modo que [`Extension::theme()`](crate::core::extension::Extension::theme)
//! permita identificar y registrar los temas disponibles.
//!
//! Un tema determina el aspecto final de un documento HTML sin alterar la lógica interna de los
//! componentes ni la estructura del documento, que queda definida por la plantilla
//! ([`Template`](crate::base::component::Template)) utilizada por cada página.
//! Un tema es la *piel* de la aplicación: define estilos, tipografías, espaciados o comportamientos
//! interactivos. Para ello utiliza plantillas ([`Template`]) que describen cómo maquetar el cuerpo
//! del documento a partir de varias regiones ([`Region`]). Cada región es un contenedor lógico
//! identificado por un nombre, cuyo contenido se obtiene del [`Context`] de la página.
//!
//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension), por
//! lo que se instancian, declaran dependencias y se inician igual que cualquier otra extensión.
//! También deben implementar [`Theme`] y sobrescribir el método
//! [`Extension::theme()`](crate::core::extension::Extension::theme) para que PageTop pueda
//! registrarlos como temas.
//! Una página ([`Page`](crate::response::page::Page)) representa un documento HTML completo.
//! Implementa [`Contextual`](crate::core::component::Contextual) para gestionar su propio
//! [`Context`], donde mantiene el tema activo, la plantilla seleccionada y los componentes
//! asociados a cada región.
//!
//! De este modo, temas y extensiones colaboran sobre una estructura común: las aplicaciones
//! registran componentes en el [`Context`], las plantillas organizan las regiones y las páginas
//! generan el documento HTML resultante.
//!
//! Los temas pueden definir sus propias implementaciones de [`Template`] y [`Region`] (por ejemplo,
//! mediante *enums* adicionales) para añadir nuevas plantillas o exponer regiones específicas.
use crate::core::component::Context;
use crate::html::{html, Markup};
use crate::locale::L10n;
use crate::{join, AutoDefault};
// **< Region >*************************************************************************************
/// Interfaz común para las regiones lógicas de un documento.
///
/// Una `Region` representa un contenedor lógico identificado por un nombre de región. Su contenido
/// se obtiene del [`Context`], donde los componentes suelen registrarse usando implementaciones de
/// métodos como [`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
/// implementaciones de [`Region`] que devuelvan el mismo nombre compartirán el mismo conjunto de
/// componentes registrados en el [`Context`], aunque cada región puede renderizar ese contenido de
/// forma diferente. Por ejemplo, [`DefaultRegion::Header`] y `BootsierRegion::Header` mostrarían
/// los mismos componentes si ambas devuelven el nombre `"header"`, pero podrían maquetarse de
/// manera distinta.
///
/// El tema decide qué regiones mostrar en el cuerpo del documento, normalmente usando una plantilla
/// ([`Template`]) al renderizar la página ([`Page`](crate::response::page::Page)).
pub trait Region {
/// Devuelve el nombre de la región.
///
/// Este nombre es el identificador lógico de la región y se usa como clave en el [`Context`]
/// para recuperar y renderizar el contenido registrado bajo ese nombre. Cualquier
/// implementación de [`Region`] que devuelva el mismo nombre compartirá el mismo conjunto de
/// componentes.
///
/// En la implementación predeterminada de [`Self::render()`] también se utiliza para construir
/// las clases del contenedor de la región (`"region region-<name>"`).
fn name(&self) -> &'static str;
/// Devuelve la etiqueta de accesibilidad localizada asociada a la región.
///
/// En la implementación predeterminada de [`Self::render()`], este valor se usa como
/// `aria-label` del contenedor de la región.
fn label(&self) -> L10n;
/// Renderiza el contenedor de la región.
///
/// Por defecto, recupera del [`Context`] el contenido de la región y, si no está vacío, lo
/// envuelve en un `<div>` con clases `"region region-<name>"` y un `aria-label` basado en la
/// etiqueta localizada de la región:
///
/// ```html
/// <div class="region region-<name>" role="region" aria-label="<label>">
/// <!-- Componentes de la región "name" -->
/// </div>
/// ```
///
/// Se puede sobrescribir este método para modificar la estructura del contenedor, las clases
/// utilizadas o la semántica del marcado generado para cada región.
fn render(&'static self, cx: &mut Context) -> Markup
where
Self: Sized,
{
html! {
@let region = cx.render_region(self);
@if !region.is_empty() {
div
class=(join!("region region-", self.name()))
role="region"
aria-label=[self.label().lookup(cx)]
{
(region)
}
}
}
}
}
/// Referencia estática a una región.
pub type RegionRef = &'static dyn Region;
// **< DefaultRegion >******************************************************************************
/// Regiones básicas que PageTop proporciona por defecto.
///
/// Estas regiones comparten sus nombres (`"header"`, `"content"`, `"footer"`) con cualquier región
/// equivalente definida por otros temas, por lo que comparten también el contenido registrado bajo
/// esos nombres.
#[derive(AutoDefault)]
pub enum DefaultRegion {
/// Región estándar para la **cabecera** del documento, de nombre `"header"`.
///
/// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc.
Header,
/// Región principal de **contenido**, de nombre `"content"`.
///
/// Es la región donde se renderiza el contenido principal del documento. En general será la
/// región mínima imprescindible para que una página tenga sentido.
#[default]
Content,
/// Región estándar para el **pie de página**, de nombre `"footer"`.
///
/// Suele contener información legal, enlaces secundarios, créditos, etc.
Footer,
}
impl Region for DefaultRegion {
#[inline]
fn name(&self) -> &'static str {
match self {
Self::Header => "header",
Self::Content => "content",
Self::Footer => "footer",
}
}
#[inline]
fn label(&self) -> L10n {
match self {
Self::Header => L10n::l("region-header"),
Self::Content => L10n::l("region-content"),
Self::Footer => L10n::l("region-footer"),
}
}
}
// **< Template >***********************************************************************************
/// Interfaz común para definir plantillas de contenido.
///
/// Una `Template` puede proporcionar una o más variantes para decidir la composición del `<body>`
/// de una página ([`Page`](crate::response::page::Page)). El tema utiliza esta información para
/// determinar qué regiones ([`Region`]) deben renderizarse y en qué orden.
pub trait Template {
/// Renderiza el contenido de la plantilla.
///
/// Por defecto, renderiza las regiones básicas de [`DefaultRegion`] en este orden:
/// [`DefaultRegion::Header`], [`DefaultRegion::Content`] y [`DefaultRegion::Footer`].
///
/// Se puede sobrescribir este método para:
///
/// - Cambiar el conjunto de regiones que se renderizan según variantes de la plantilla.
/// - Alterar el orden de dichas regiones.
/// - Envolver las regiones en contenedores adicionales.
/// - Implementar distribuciones específicas (por ejemplo, con barras laterales).
///
/// Este método se invoca normalmente desde [`Theme::render_page_body()`] para generar el
/// contenido del `<body>` de una página según la plantilla devuelta por el contexto de la
/// propia página ([`Contextual::template()`](crate::core::component::Contextual::template())).
fn render(&'static self, cx: &mut Context) -> Markup {
html! {
(DefaultRegion::Header.render(cx))
(DefaultRegion::Content.render(cx))
(DefaultRegion::Footer.render(cx))
}
}
}
/// Referencia estática a una plantilla.
pub type TemplateRef = &'static dyn Template;
// **< DefaultTemplate >****************************************************************************
/// Plantillas que PageTop proporciona por defecto.
#[derive(AutoDefault)]
pub enum DefaultTemplate {
/// Plantilla predeterminada.
///
/// Utiliza la implementación por defecto de [`Template::render()`] y se emplea cuando no se
/// selecciona ninguna otra plantilla explícitamente.
#[default]
Standard,
/// Plantilla de error.
///
/// Se utiliza para páginas de error u otros estados excepcionales. Por defecto utiliza la misma
/// implementación de [`Template::render()`] que [`Self::Standard`].
Error,
}
impl Template for DefaultTemplate {}
// **< Definitions >********************************************************************************
mod definition;
pub use definition::{Theme, ThemeRef};

View file

@ -1,30 +1,26 @@
use crate::base::component::Template;
use crate::core::component::{ComponentRender, ContextOp, Contextual};
use crate::core::component::Contextual;
use crate::core::extension::Extension;
use crate::global;
use crate::html::{html, Markup, StyleSheet};
use crate::html::{html, Markup};
use crate::locale::L10n;
use crate::prelude::{DefaultTemplate, TemplateRef};
use crate::response::page::Page;
/// Referencia estática a un tema.
///
/// Los temas son también extensiones. Por tanto, deben declararse como **instancias estáticas** que
/// implementen [`Theme`] y, a su vez, [`Extension`]. Estas instancias se exponen usando
/// [`Extension::theme()`](crate::core::extension::Extension::theme).
pub type ThemeRef = &'static dyn Theme;
/// Interfaz común que debe implementar cualquier tema de PageTop.
///
/// Un tema es una [`Extension`](crate::core::extension::Extension) que define el aspecto general de
/// las páginas: cómo se renderiza el `<head>`, cómo se presenta el `<body>` mediante plantillas
/// ([`Template`]) y qué contenido mostrar en las páginas de error.
/// las páginas: cómo se renderiza el `<head>`, cómo se presenta el `<body>` usando plantillas
/// ([`Template`](crate::core::theme::Template)) que maquetan regiones
/// ([`Region`](crate::core::theme::Region)) y qué contenido mostrar en las páginas de error. El
/// contenido de cada región depende del [`Context`](crate::core::component::Context) y de su nombre
/// lógico.
///
/// Todos los métodos de este *trait* tienen una implementación por defecto, por lo que pueden
/// sobrescribirse selectivamente para crear nuevos temas con comportamientos distintos a los
/// predeterminados.
///
/// El único método **obligatorio** de `Extension` para un tema es [`theme()`](Extension::theme),
/// que debe devolver una referencia estática al propio tema:
/// que debe devolver una referencia al propio tema:
///
/// ```rust
/// # use pagetop::prelude::*;
@ -47,32 +43,55 @@ pub type ThemeRef = &'static dyn Theme;
/// impl Theme for MyTheme {}
/// ```
pub trait Theme: Extension + Send + Sync {
/// Devuelve la plantilla ([`Template`](crate::core::theme::Template)) que el propio tema
/// propone como predeterminada.
///
/// Se utiliza al inicializar un [`Context`](crate::core::component::Context) o una página
/// ([`Page`](crate::response::page::Page)) por si no se elige ninguna otra plantilla con
/// [`Contextual::with_template()`](crate::core::component::Contextual::with_template).
///
/// La implementación por defecto devuelve la plantilla estándar ([`DefaultTemplate::Standard`])
/// con una estructura básica para la página. Los temas pueden sobrescribir este método para
/// seleccionar otra plantilla predeterminada o una plantilla propia.
#[inline]
fn default_template(&self) -> TemplateRef {
&DefaultTemplate::Standard
}
/// Acciones específicas del tema antes de renderizar el `<body>` de la página.
///
/// Se invoca antes de que se procese la plantilla ([`Template`]) asociada a la página
/// ([`Page::template()`](crate::response::page::Page::template)). Es un buen lugar para
/// inicializar o ajustar recursos en función del contexto de la página, por ejemplo:
/// Es un buen lugar para inicializar o ajustar recursos en función del contexto de la página,
/// por ejemplo:
///
/// - Añadir metadatos o propiedades a la página.
/// - Añadir metadatos o propiedades a la cabecera de la página.
/// - Preparar atributos compartidos.
/// - Registrar *assets* condicionales en el contexto.
///
/// La implementación por defecto no realiza ninguna acción.
#[allow(unused_variables)]
fn before_render_page_body(&self, page: &mut Page) {}
/// Renderiza el contenido del `<body>` de la página.
///
/// Por defecto, delega en la plantilla ([`Template`]) asociada a la página
/// ([`Page::template()`](crate::response::page::Page::template)). La plantilla se encarga de
/// procesar las regiones y renderizar los componentes registrados en el contexto.
/// La implementación predeterminada delega en la plantilla asociada a la página, obtenida desde
/// su [`Context`](crate::core::component::Context), y llama a
/// [`Template::render()`](crate::core::theme::Template::render) para componer el `<body>` a
/// partir de las regiones.
///
/// Con la configuración por defecto, la plantilla estándar utiliza las regiones
/// [`DefaultRegion::Header`](crate::core::theme::DefaultRegion::Header),
/// [`DefaultRegion::Content`](crate::core::theme::DefaultRegion::Content) y
/// [`DefaultRegion::Footer`](crate::core::theme::DefaultRegion::Footer) en ese orden.
///
/// Los temas pueden sobrescribir este método para:
///
/// - Forzar una plantilla concreta en determinadas páginas.
/// - Envolver el contenido en marcadores adicionales.
/// - Consultar la plantilla de la página y variar la composición según su nombre.
/// - Envolver el contenido en contenedores adicionales.
/// - Implementar lógicas de composición alternativas.
#[inline]
fn render_page_body(&self, page: &mut Page) -> Markup {
Template::named(page.template()).render(page.context())
page.template().render(page.context())
}
/// Acciones específicas del tema después de renderizar el `<body>` de la página.
@ -83,6 +102,8 @@ pub trait Theme: Extension + Send + Sync {
/// - Realizar *tracing* o recopilar métricas.
/// - Aplicar ajustes finales al estado de la página antes de producir el `<head>` o la
/// respuesta final.
///
/// La implementación por defecto no realiza ninguna acción.
#[allow(unused_variables)]
fn after_render_page_body(&self, page: &mut Page) {}
@ -101,34 +122,11 @@ pub trait Theme: Extension + Send + Sync {
/// - La etiqueta `viewport` básica para diseño adaptable.
/// - Los metadatos (`name`/`content`) y propiedades (`property`/`content`) declarados en la
/// página.
/// - Los *assets* registrados en el contexto de la página. Si el parámetro
/// `include_basic_assets` está activado, añade de serie las siguientes hojas de estilo
/// básicas: `normalize.css`, `root.css`, `basic.css`, útiles para temas sencillos o de uso
/// general.
/// - Los *assets* registrados en el contexto de la página.
///
/// Los temas pueden sobrescribir este método para añadir etiquetas adicionales (por ejemplo,
/// *favicons* personalizados, manifest, etiquetas de analítica, etc.).
#[inline]
fn render_page_head(&self, page: &mut Page) -> Markup {
if page.param_or("include_basic_assets", false) {
let pkg_version = env!("CARGO_PKG_VERSION");
page.alter_assets(ContextOp::AddStyleSheet(
StyleSheet::from("/css/normalize.css")
.with_version("8.0.1")
.with_weight(-99),
))
.alter_assets(ContextOp::AddStyleSheet(
StyleSheet::from("/css/root.css")
.with_version(pkg_version)
.with_weight(-99),
))
.alter_assets(ContextOp::AddStyleSheet(
StyleSheet::from("/css/basic.css")
.with_version(pkg_version)
.with_weight(-99),
));
}
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
html! {
meta charset="utf-8";
@ -173,3 +171,6 @@ pub trait Theme: Extension + Send + Sync {
html! { div { h1 { (L10n::l("error404_notice").using(page)) } } }
}
}
/// Referencia estática a un tema.
pub type ThemeRef = &'static dyn Theme;

View file

@ -1,6 +1,5 @@
use crate::base::component::Region;
use crate::core::component::{Child, ChildOp, Children};
use crate::core::theme::ThemeRef;
use crate::core::theme::{DefaultRegion, RegionRef, ThemeRef};
use crate::{builder_fn, AutoDefault, UniqueId};
use parking_lot::RwLock;
@ -21,24 +20,23 @@ static COMMON_REGIONS: LazyLock<RwLock<ChildrenInRegions>> =
pub(crate) struct ChildrenInRegions(HashMap<String, Children>);
impl ChildrenInRegions {
pub fn with(region_name: impl AsRef<str>, child: Child) -> Self {
Self::default().with_child_in(region_name, ChildOp::Add(child))
pub fn with(region_ref: RegionRef, child: Child) -> Self {
Self::default().with_child_in(region_ref, ChildOp::Add(child))
}
#[builder_fn]
pub fn with_child_in(mut self, region_name: impl AsRef<str>, op: ChildOp) -> Self {
let name = region_name.as_ref();
if let Some(region) = self.0.get_mut(name) {
pub fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
if let Some(region) = self.0.get_mut(region_ref.name()) {
region.alter_child(op);
} else {
self.0
.insert(name.to_owned(), Children::new().with_child(op));
.insert(region_ref.name().to_owned(), Children::new().with_child(op));
}
self
}
pub fn children_for(&self, theme_ref: ThemeRef, region_name: impl AsRef<str>) -> Children {
let name = region_name.as_ref();
pub fn children_for(&self, theme_ref: ThemeRef, region_ref: RegionRef) -> Children {
let name = region_ref.name();
let common = COMMON_REGIONS.read();
let themed = THEME_REGIONS.read();
@ -50,20 +48,36 @@ impl ChildrenInRegions {
}
}
/// Permite añadir componentes a regiones globales o específicas de un tema.
/// Añade componentes a regiones globales o específicas de un tema.
///
/// Según la variante, se pueden añadir componentes ([`add()`](Self::add)) que permanecerán
/// disponibles durante toda la ejecución.
///
/// Estos componentes se renderizarán automáticamente al procesar los documentos HTML que incluyen
/// estas regiones, como las páginas de contenido ([`Page`](crate::response::page::Page)).
/// Cada variante indica la región en la que se añade el componente usando [`Self::add()`]. Los
/// componentes añadidos se mantienen durante toda la ejecución y se inyectan automáticamente al
/// renderizar los documentos HTML que utilizan esas regiones, como las páginas de contenido
/// ([`Page`](crate::response::page::Page)).
pub enum InRegion {
/// Región de contenido por defecto.
Default,
/// Región identificada por el nombre proporcionado.
Named(&'static str),
/// Región identificada por su nombre para un tema concreto.
OfTheme(&'static str, ThemeRef),
/// Región principal de **contenido** por defecto.
///
/// Añade el componente a la región lógica de contenido principal de la aplicación. Por
/// convención, esta región corresponde a [`DefaultRegion::Content`], cuyo nombre es
/// `"content"`. Cualquier tema que renderice esa misma región de contenido, ya sea usando
/// directamente [`DefaultRegion::Content`] o cualquier otra implementación de
/// [`Region`](crate::core::theme::Region) que devuelva ese mismo nombre, mostrará los
/// componentes registrados aquí, aunque lo harán según su propio método de renderizado
/// ([`Region::render()`](crate::core::theme::Region::render)).
Content,
/// Región global compartida por todos los temas.
///
/// Los componentes añadidos aquí se asocian al nombre de la región indicado por [`RegionRef`],
/// es decir, al valor devuelto por [`Region::name()`](crate::core::theme::Region::name) para
/// esa región. Se mostrarán en cualquier tema cuya plantilla renderice una región que devuelva
/// ese mismo nombre.
Global(RegionRef),
/// Región asociada a un tema concreto.
///
/// Los componentes sólo se renderizarán cuando el documento se procese con el tema indicado y
/// se utilice la región referenciada. Resulta útil para añadir contenido específico en un tema
/// sin afectar a otros.
ForTheme(ThemeRef, RegionRef),
}
impl InRegion {
@ -73,28 +87,33 @@ impl InRegion {
///
/// ```rust
/// # use pagetop::prelude::*;
/// // Banner global, en la región por defecto de cualquier página.
/// InRegion::Default.add(Child::with(Html::with(|_|
/// html! { ("🎉 ¡Bienvenido!") }
/// )));
/// // Banner global en la región por defecto.
/// InRegion::Content.add(Child::with(Html::with(|_| {
/// html! { "🎉 ¡Bienvenido!" }
/// })));
///
/// // Texto en la región "sidebar".
/// InRegion::Named("sidebar").add(Child::with(Html::with(|_|
/// html! { ("Publicidad") }
/// )));
/// // Texto en la cabecera.
/// InRegion::Global(&DefaultRegion::Header).add(Child::with(Html::with(|_| {
/// html! { "Publicidad" }
/// })));
///
/// // Contenido sólo para la región del pie de página en un tema concreto.
/// InRegion::ForTheme(&theme::Basic, &DefaultRegion::Footer).add(Child::with(Html::with(|_| {
/// html! { "Aviso legal" }
/// })));
/// ```
pub fn add(&self, child: Child) -> &Self {
match self {
InRegion::Default => Self::add_to_common(Region::DEFAULT, child),
InRegion::Named(region_name) => Self::add_to_common(region_name, child),
InRegion::OfTheme(region_name, theme_ref) => {
InRegion::Content => Self::add_to_common(&DefaultRegion::Content, child),
InRegion::Global(region_ref) => Self::add_to_common(*region_ref, child),
InRegion::ForTheme(theme_ref, region_ref) => {
let mut regions = THEME_REGIONS.write();
if let Some(r) = regions.get_mut(&theme_ref.type_id()) {
r.alter_child_in(region_name, ChildOp::Add(child));
r.alter_child_in(*region_ref, ChildOp::Add(child));
} else {
regions.insert(
theme_ref.type_id(),
ChildrenInRegions::with(region_name, child),
ChildrenInRegions::with(*region_ref, child),
);
}
}
@ -103,9 +122,9 @@ impl InRegion {
}
#[inline]
fn add_to_common(region_name: &str, child: Child) {
fn add_to_common(region_ref: RegionRef, child: Child) {
COMMON_REGIONS
.write()
.alter_child_in(region_name, ChildOp::Add(child));
.alter_child_in(region_ref, ChildOp::Add(child));
}
}

View file

@ -1,13 +1,27 @@
//! Responde a una petición web generando una página HTML completa.
//!
//! Este módulo define [`Page`], que representa una página HTML lista para renderizar. Cada página
//! se construye a partir de un [`Context`] propio, donde se registran el tema activo, la plantilla
//! ([`Template`](crate::core::theme::Template)) que define la disposición de las regiones
//! ([`Region`]), los componentes asociados y los recursos adicionales (hojas de estilo, scripts,
//! *favicon*, etc.).
//!
//! El renderizado ([`Page::render()`]) delega en el tema ([`Theme`](crate::core::theme::Theme)) la
//! composición del `<head>` y del `<body>`, y se ejecutan las acciones registradas por las
//! extensiones antes y después de generar los contenidos.
//!
//! También introduce regiones internas reservadas ([`ReservedRegion`]) que actúan como puntos de
//! anclaje globales al inicio y al final del documento.
mod error;
pub use error::ErrorPage;
pub use actix_web::Result as ResultPage;
use crate::base::action;
use crate::base::component::Region;
use crate::core::component::{Child, ChildOp, Component, ComponentRender};
use crate::core::component::{Child, ChildOp, Component};
use crate::core::component::{Context, ContextOp, Contextual};
use crate::core::theme::ThemeRef;
use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef};
use crate::html::{html, Markup, DOCTYPE};
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
use crate::html::{AttrClasses, ClassesOp};
@ -16,6 +30,57 @@ use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
use crate::service::HttpRequest;
use crate::{builder_fn, AutoDefault};
// **< ReservedRegion >*****************************************************************************
/// Regiones internas reservadas como puntos de anclaje globales.
///
/// Representan contenedores especiales situados al inicio y al final de un documento. Están
/// pensadas para proporcionar regiones donde inyectar contenido global o técnico. No suelen usarse
/// como regiones visibles en los temas.
pub enum ReservedRegion {
/// Región interna situada al **inicio del documento**.
///
/// Su función es proporcionar un contenedor donde las extensiones puedan inyectar contenido
/// global antes del resto de regiones principales (cabecera, contenido, etc.).
///
/// No suele utilizarse en los temas como una región “visible” dentro del maquetado habitual,
/// sino como punto de anclaje para elementos auxiliares, marcadores técnicos, inicializadores o
/// contenido de depuración que deban situarse en la parte superior del documento.
///
/// Se considera una región **reservada** para este tipo de usos globales.
PageTop,
/// Región interna situada al **final del documento**.
///
/// Pensada para proporcionar un contenedor donde las extensiones puedan inyectar contenido
/// global después del resto de regiones principales (cabecera, contenido, etc.).
///
/// No suele utilizarse en los temas como una región “visible” dentro del maquetado habitual,
/// sino como punto de anclaje para elementos auxiliares asociados a comportamientos dinámicos
/// que deban situarse en la parte inferior del documento.
///
/// Igual que [`Self::PageTop`], se considera una región **reservada** para este tipo de usos
/// globales.
PageBottom,
}
impl Region for ReservedRegion {
#[inline]
fn name(&self) -> &'static str {
match self {
Self::PageTop => "page-top",
Self::PageBottom => "page-bottom",
}
}
#[inline]
fn label(&self) -> L10n {
L10n::default()
}
}
// **< Page >***************************************************************************************
/// Representa una página HTML completa lista para renderizar.
///
/// Una instancia de `Page` se compone dinámicamente permitiendo establecer título, descripción,
@ -77,7 +142,7 @@ impl Page {
/// Añade una entrada `<meta property="..." content="...">` al `<head>`.
#[builder_fn]
pub fn with_property(mut self, property: &'static str, content: &'static str) -> Self {
self.metadata.push((property, content));
self.properties.push((property, content));
self
}
@ -97,15 +162,17 @@ impl Page {
/// Añade un componente hijo a la región de contenido por defecto.
pub fn add_child(mut self, component: impl Component) -> Self {
self.context
.alter_child_in(Region::DEFAULT, ChildOp::Add(Child::with(component)));
self.context.alter_child_in(
&DefaultRegion::Content,
ChildOp::Add(Child::with(component)),
);
self
}
/// Añade un componente hijo en la región `region_name` de la página.
pub fn add_child_in(mut self, region_name: &'static str, component: impl Component) -> Self {
pub fn add_child_in(mut self, region_ref: RegionRef, component: impl Component) -> Self {
self.context
.alter_child_in(region_name, ChildOp::Add(Child::with(component)));
.alter_child_in(region_ref, ChildOp::Add(Child::with(component)));
self
}
@ -154,8 +221,30 @@ impl Page {
/// Renderiza la página completa en formato HTML.
///
/// Ejecuta las acciones correspondientes antes y después de renderizar el `<body>`,
/// así como del `<head>`, e inserta los atributos `lang` y `dir` en la etiqueta `<html>`.
/// El proceso de renderizado de la página sigue esta secuencia:
///
/// 1. Ejecuta
/// [`Theme::before_render_page_body()`](crate::core::theme::Theme::before_render_page_body)
/// para que el tema pueda ejecutar acciones específicas antes de renderizar el `<body>`.
/// 2. Despacha [`action::page::BeforeRenderBody`] para que otras extensiones puedan realizar
/// ajustes previos sobre la página.
/// 3. **Construye el contenido del `<body>`**:
/// - Renderiza la región reservada superior ([`ReservedRegion::PageTop`]).
/// - Llama a [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body) para
/// renderizar las regiones del cuerpo principal de la página.
/// - Renderiza la región reservada inferior ([`ReservedRegion::PageBottom`]).
/// 4. Ejecuta
/// [`Theme::after_render_page_body()`](crate::core::theme::Theme::after_render_page_body)
/// para que el tema pueda aplicar ajustes finales.
/// 5. Despacha [`action::page::AfterRenderBody`] para permitir que otras extensiones realicen
/// sus últimos ajustes tras generar el `<body>`.
/// 6. Renderiza el `<head>` llamando a
/// [`Theme::render_page_head()`](crate::core::theme::Theme::render_page_head).
/// 7. Obtiene el idioma y la dirección del texto a partir de
/// [`Context::langid()`](crate::core::component::Context::langid) e inserta los atributos
/// `lang` y `dir` en la etiqueta `<html>`.
/// 8. Compone el documento HTML completo (`<!DOCTYPE html>`, `<html>`, `<head>`, `<body>`) y
/// devuelve un [`ResultPage`] con el [`Markup`] final.
pub fn render(&mut self) -> ResultPage<Markup, ErrorPage> {
// Acciones específicas del tema antes de renderizar el <body>.
self.context.theme().before_render_page_body(self);
@ -165,9 +254,9 @@ impl Page {
// Renderiza el <body>.
let body = html! {
(Region::named(Region::PAGETOP).render(&mut self.context))
(ReservedRegion::PageTop.render(&mut self.context))
(self.context.theme().render_page_body(self))
(Region::named(Region::PAGEBOTTOM).render(&mut self.context))
(ReservedRegion::PageBottom.render(&mut self.context))
};
// Acciones específicas del tema después de renderizar el <body>.
@ -228,8 +317,8 @@ impl Contextual for Page {
}
#[builder_fn]
fn with_template(mut self, template_name: &'static str) -> Self {
self.context.alter_template(template_name);
fn with_template(mut self, template: TemplateRef) -> Self {
self.context.alter_template(template);
self
}
@ -246,8 +335,8 @@ impl Contextual for Page {
}
#[builder_fn]
fn with_child_in(mut self, region_name: impl AsRef<str>, op: ChildOp) -> Self {
self.context.alter_child_in(region_name, op);
fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
self.context.alter_child_in(region_ref, op);
self
}
@ -261,7 +350,7 @@ impl Contextual for Page {
self.context.theme()
}
fn template(&self) -> &str {
fn template(&self) -> TemplateRef {
self.context.template()
}

View file

@ -1,5 +1,6 @@
use crate::base::component::{Html, Template};
use crate::base::component::Html;
use crate::core::component::Contextual;
use crate::core::theme::DefaultTemplate;
use crate::locale::L10n;
use crate::response::ResponseError;
use crate::service::http::{header::ContentType, StatusCode};
@ -7,8 +8,21 @@ use crate::service::{HttpRequest, HttpResponse};
use super::Page;
use std::fmt::{self, Display};
use std::fmt;
/// Página de error asociada a un código de estado HTTP.
///
/// Este enumerado agrupa los distintos tipos de error que pueden devolverse como página HTML
/// completa. Cada variante encapsula la solicitud original ([`HttpRequest`]) y se corresponde con
/// un código de estado concreto.
///
/// Para algunos errores (como [`ErrorPage::AccessDenied`] y [`ErrorPage::NotFound`]) se construye
/// una [`Page`] usando la plantilla de error del tema activo ([`DefaultTemplate::Error`]), lo que
/// permite personalizar el contenido del mensaje. En el resto de casos se devuelve un cuerpo HTML
/// mínimo basado en una descripción genérica del error.
///
/// `ErrorPage` implementa [`ResponseError`], por lo que puede utilizarse directamente como tipo de
/// error en los controladores HTTP.
#[derive(Debug)]
pub enum ErrorPage {
NotModified(HttpRequest),
@ -20,7 +34,7 @@ pub enum ErrorPage {
Timeout(HttpRequest),
}
impl Display for ErrorPage {
impl fmt::Display for ErrorPage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
// Error 304.
@ -33,7 +47,7 @@ impl Display for ErrorPage {
let error403 = error_page.theme().error403(&mut error_page);
if let Ok(page) = error_page
.with_title(L10n::n("Error FORBIDDEN"))
.with_template(Template::ERROR)
.with_template(&DefaultTemplate::Error)
.add_child(Html::with(move |_| error403.clone()))
.render()
{
@ -48,7 +62,7 @@ impl Display for ErrorPage {
let error404 = error_page.theme().error404(&mut error_page);
if let Ok(page) = error_page
.with_title(L10n::n("Error RESOURCE NOT FOUND"))
.with_template(Template::ERROR)
.with_template(&DefaultTemplate::Error)
.add_child(Html::with(move |_| error404.clone()))
.render()
{

View file

@ -1,16 +1,31 @@
:root {
/* Font families */
--val-font-sans: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif;
--val-font-serif: Georgia,"Times New Roman",serif;
--val-font-monospace: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
--val-font-family: var(--val-font-sans);
/* Font size */
--val-fs--base: 1rem;
/* Font weight */
--val-fw--base: 400;
/* Line height */
--val-lh--base: 1.5;
/* Colors */
--val-color--bg: #fafafa;
--val-color--text: #212529;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: var(--val-font-family);
font-size: var(--val-fs--base);
font-weight: var(--val-fw--base);
line-height: var(--val-lh--base);
color: var(--val-color--text);
background-color: var(--val-color--bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}

View file

@ -1,12 +0,0 @@
/* Icon component */
.icon {
width: 1rem;
height: 1rem;
}
/* PoweredBy component */
.poweredby {
text-align: center;
}

View file

@ -1,212 +0,0 @@
:root {
--val-font-sans: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
--val-font-serif: "Lora","georgia",serif;
--val-font-monospace: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
--val-font-family: var(--val-font-sans);
/* Font size */
--val-fs--x3l: 2.5rem;
--val-fs--x2l: 2rem;
--val-fs--xl: 1.75rem;
--val-fs--l: 1.5rem;
--val-fs--m: 1.25rem;
--val-fs--base: 1rem;
--val-fs--s: 0.875rem;
--val-fs--xs: 0.75rem;
--val-fs--x2s: 0.5625rem;
--val-fs--x3s: 0.375rem;
/* Font weight */
--val-fw--light: 300;
--val-fw--base: 400;
--val-fw--bold: 500;
/* Line height */
--val-lh--base: 1.5;
--val-lh--header: 1.2;
--val-max-width: 90rem;
/*
--val-color-rgb: 33,37,41;
--val-main--bg-rgb: 255,255,255;
--val-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--line-height-base: 1.6875rem;
--line-height-s: 1.125rem;
--max-bg-color: 98.125rem;
*/
--val-gap: 1.125rem;
/*
--content-left: 5.625rem;
--site-header-height-wide: var(--val-gap10);
--container-padding: var(--val-gap);
*/
}
/*
@media (min-width: 75rem) {
:root {
--container-padding:var(--val-gap2);
}
}
:root {
--scrollbar-width: 0px;
--grid-col-count: 6;
--grid-gap: var(--val-gap);
--grid-gap-count: calc(var(--grid-col-count) - 1);
--grid-full-width: calc(100vw - var(--val-gap2) - var(--scrollbar-width));
--grid-col-width: calc((var(--grid-full-width) - (var(--grid-gap-count) * var(--grid-gap))) / var(--grid-col-count));
}
@media (min-width: 43.75rem) {
:root {
--grid-col-count:14;
--grid-gap: var(--val-gap2);
}
}
@media (min-width: 62.5rem) {
:root {
--scrollbar-width:0.9375rem;
}
}
@media (min-width: 75rem) {
:root {
--grid-full-width:calc(100vw - var(--scrollbar-width) - var(--content-left) - var(--val-gap4));
}
}
@media (min-width: 90rem) {
:root {
--grid-full-width:calc(var(--max-width) - var(--val-gap4));
}
}
*/
:root {
--val-gap-0-15: calc(0.15 * var(--val-gap));
--val-gap-0-25: calc(0.25 * var(--val-gap));
--val-gap-0-35: calc(0.35 * var(--val-gap));
--val-gap-0-5: calc(0.5 * var(--val-gap));
--val-gap-0-75: calc(0.75 * var(--val-gap));
--val-gap-1-5: calc(1.5 * var(--val-gap));
--val-gap-2: calc(2 * var(--val-gap));
--primary-hue: 216;
--primary-sat: 60%;
--val-color--primary: hsl(var(--primary-hue), var(--primary-sat), 50%);
--val-color--primary-light: hsl(var(--primary-hue), var(--primary-sat), 60%);
--val-color--primary-dark: hsl(var(--primary-hue), var(--primary-sat), 40%);
--val-color--primary-link: hsl(var(--primary-hue), var(--primary-sat), 55%);
--val-color--primary-link-hover: hsl(var(--primary-hue), var(--primary-sat), 30%);
--val-color--primary-link-active: hsl(var(--primary-hue), var(--primary-sat), 70%);
--info-hue: 190;
--info-sat: 90%;
--val-color--info: hsl(var(--info-hue), var(--info-sat), 54%);
--val-color--info-light: hsl(var(--info-hue), var(--info-sat), 70%);
--val-color--info-dark: hsl(var(--info-hue), var(--info-sat), 45%);
--val-color--info-link: hsl(var(--info-hue), var(--info-sat), 30%);
--val-color--info-link-hover: hsl(var(--info-hue), var(--info-sat), 20%);
--val-color--info-link-active: hsl(var(--info-hue), var(--info-sat), 40%);
--success-hue: 150;
--success-sat: 50%;
--val-color--success: hsl(var(--success-hue), var(--success-sat), 50%);
--val-color--success-light: hsl(var(--success-hue), var(--success-sat), 68%);
--val-color--success-dark: hsl(var(--success-hue), var(--success-sat), 38%);
--val-color--success-link: hsl(var(--success-hue), var(--success-sat), 26%);
--val-color--success-link-hover: hsl(var(--success-hue), var(--success-sat), 18%);
--val-color--success-link-active: hsl(var(--success-hue), var(--success-sat), 36%);
--warning-hue: 44;
--warning-sat: 100%;
--val-color--warning: hsl(var(--warning-hue), var(--warning-sat), 50%);
--val-color--warning-light: hsl(var(--warning-hue), var(--warning-sat), 60%);
--val-color--warning-dark: hsl(var(--warning-hue), var(--warning-sat), 40%);
--val-color--warning-link: hsl(var(--warning-hue), var(--warning-sat), 30%);
--val-color--warning-link-hover: hsl(var(--warning-hue), var(--warning-sat), 20%);
--val-color--warning-link-active: hsl(var(--warning-hue), var(--warning-sat), 38%);
--danger-hue: 348;
--danger-sat: 86%;
--val-color--danger: hsl(var(--danger-hue), var(--danger-sat), 50%);
--val-color--danger-light: hsl(var(--danger-hue), var(--danger-sat), 60%);
--val-color--danger-dark: hsl(var(--danger-hue), var(--danger-sat), 35%);
--val-color--danger-link: hsl(var(--danger-hue), var(--danger-sat), 25%);
--val-color--danger-link-hover: hsl(var(--danger-hue), var(--danger-sat), 10%);
--val-color--danger-link-active: hsl(var(--danger-hue), var(--danger-sat), 30%);
--light-hue: 0;
--light-sat: 0%;
--val-color--light: hsl(var(--light-hue), var(--light-sat), 96%);
--val-color--light-light: hsl(var(--light-hue), var(--light-sat), 98%);
--val-color--light-dark: hsl(var(--light-hue), var(--light-sat), 92%);
--dark-hue: 0;
--dark-sat: 0%;
--val-color--dark: hsl(var(--dark-hue), var(--dark-sat), 25%);
--val-color--dark-light: hsl(var(--dark-hue), var(--dark-sat), 40%);
--val-color--dark-dark: hsl(var(--dark-hue), var(--dark-sat), 8%);
--val-color--dark-link: hsl(var(--dark-hue), var(--dark-sat), 90%);
--val-color--dark-link-hover: hsl(var(--dark-hue), var(--dark-sat), 100%);
--val-color--dark-link-active: hsl(var(--dark-hue), var(--dark-sat), 70%);
--gray-hue: 201;
--gray-sat: 15%;
--val-color--gray-5: hsl(var(--gray-hue), var(--gray-sat), 5%);
--val-color--gray-10: hsl(var(--gray-hue), var(--gray-sat) ,11%);
--val-color--gray-20: hsl(var(--gray-hue), var(--gray-sat),20%);
--val-color--gray-45: hsl(var(--gray-hue), var(--gray-sat), 44%);
--val-color--gray-60: hsl(var(--gray-hue), var(--gray-sat), 57%);
--val-color--gray-65: hsl(var(--gray-hue), var(--gray-sat), 63%);
--val-color--gray-70: hsl(var(--gray-hue), var(--gray-sat), 72%);
--val-color--gray-90: hsl(var(--gray-hue), var(--gray-sat), 88%);
--val-color--gray-95: hsl(var(--gray-hue), var(--gray-sat), 93%);
--val-color--gray-100: hsl(var(--gray-hue), var(--gray-sat), 97%);
--val-color--bg: #fafafa;
--val-color--text: #212529;
--val-color--white: #fff;
/*
--color-text-neutral-soft: var(--color--gray-45);
--color-text-neutral-medium: var(--color--gray-20);
--color-text-neutral-loud: var(--color--gray-5);
--color-text-primary-medium: var(--val-color--primary-40);
--color-text-primary-loud: var(--val-color--primary-30);
--color--black: #000;
*/
/*
--color--red: #e33f1e;
--color--gold: #fdca40;
--color--green: #3fa21c;
--header-height-wide-when-fixed: calc(6 * var(--val-gap));
--mobile-nav-width: 31.25rem;
--val-menu--border-radius: 0.625rem;
*/
--val-border-radius: 0.375rem;
/* Menu component */
--val-menu--color-bg: var(--val-color--bg);
--val-menu--color-highlight: #e91e63;
--val-menu--color-border: rgba(0, 0, 0, 0.1);
--val-menu--color-shadow: rgba(0, 0, 0, 0.06);
--val-menu--line-padding: 0.625rem;
--val-menu--line-height: calc(1.875rem + 1px);
--val-menu--item-height: calc(var(--val-menu--line-padding) + var(--val-menu--line-height));
--val-menu--item-width-min: 14rem;
--val-menu--item-width-max: 20rem;
--val-menu--item-gap: 1rem;
--val-menu--trigger-width: 2.675rem;
--val-menu--side-width: 20rem;
}