diff --git a/extensions/pagetop-aliner/src/lib.rs b/extensions/pagetop-aliner/src/lib.rs index 80e6ca15..04b5ad1a 100644 --- a/extensions/pagetop-aliner/src/lib.rs +++ b/extensions/pagetop-aliner/src/lib.rs @@ -121,7 +121,7 @@ impl Theme for Aliner { .with_weight(-99), )) .alter_child_in( - Region::FOOTER, + &DefaultRegion::Footer, ChildOp::AddIfEmpty(Child::with(PoweredBy::new())), ); } diff --git a/src/base/component.rs b/src/base/component.rs index fa9ed2ad..7ea596d3 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -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; diff --git a/src/base/component/region.rs b/src/base/component/region.rs deleted file mode 100644 index 5dfa25ce..00000000 --- a/src/base/component/region.rs +++ /dev/null @@ -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 { - 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) -> 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, 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) -> 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 `
`, - /// 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 - } -} diff --git a/src/base/component/template.rs b/src/base/component/template.rs deleted file mode 100644 index 6c70d00e..00000000 --- a/src/base/component/template.rs +++ /dev/null @@ -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 { - 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) -> 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) -> Self { - self.name.alter_value(name); - self - } - - // **< Template GETTERS >*********************************************************************** - - /// Devuelve el nombre de la plantilla seleccionada. - pub fn name(&self) -> &AttrName { - &self.name - } -} diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index 83dc4a8d..ca3c4a82 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -23,7 +23,7 @@ impl Theme for Basic { .with_weight(-99), )) .alter_child_in( - Region::FOOTER, + &DefaultRegion::Footer, ChildOp::AddIfEmpty(Child::with(PoweredBy::new())), ); } diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 5cc5d2e8..922067f4 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -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(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, 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(&self, key: &'static str) -> Option<&T>; @@ -208,7 +207,7 @@ pub struct Context { request : Option, // 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, si se ha definido. stylesheets: Assets, // Hojas de estilo CSS. javascripts: Assets, // 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::::new(), javascripts: Assets::::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) -> 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, 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 } diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs index 520153d0..304eae87 100644 --- a/src/core/extension/definition.rs +++ b/src/core/extension/definition.rs @@ -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 { 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 { 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 { vec![] } } + +/// Representa una referencia a una extensión. +pub type ExtensionRef = &'static dyn Extension; diff --git a/src/core/theme.rs b/src/core/theme.rs index 8774276e..e238df79 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -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-"`). + 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 `
` con clases `"region region-"` y un `aria-label` basado en la + /// etiqueta localizada de la región: + /// + /// ```html + ///
+ /// + ///
+ /// ``` + /// + /// 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 `` +/// 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 `` 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}; diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index dda58b18..4ff38fc1 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -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 ``, cómo se presenta el `` mediante plantillas -/// ([`Template`]) y qué contenido mostrar en las páginas de error. +/// las páginas: cómo se renderiza el ``, cómo se presenta el `` 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 `` 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 `` 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 `` 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 `` 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 `` 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; diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 259417eb..52307287 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -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> = pub(crate) struct ChildrenInRegions(HashMap); impl ChildrenInRegions { - pub fn with(region_name: impl AsRef, 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, 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) -> 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)); } } diff --git a/src/response/page.rs b/src/response/page.rs index b5516b8a..c4534f4c 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -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 `` y del ``, 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 `` al ``. #[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 ``, - /// así como del ``, e inserta los atributos `lang` y `dir` en la etiqueta ``. + /// 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 ``. + /// 2. Despacha [`action::page::BeforeRenderBody`] para que otras extensiones puedan realizar + /// ajustes previos sobre la página. + /// 3. **Construye el contenido del ``**: + /// - 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 ``. + /// 6. Renderiza el `` 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 ``. + /// 8. Compone el documento HTML completo (``, ``, ``, ``) y + /// devuelve un [`ResultPage`] con el [`Markup`] final. pub fn render(&mut self) -> ResultPage { // Acciones específicas del tema antes de renderizar el . self.context.theme().before_render_page_body(self); @@ -165,9 +254,9 @@ impl Page { // Renderiza el . 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 . @@ -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, 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() } diff --git a/src/response/page/error.rs b/src/response/page/error.rs index 7a590e6e..7d6cf33b 100644 --- a/src/response/page/error.rs +++ b/src/response/page/error.rs @@ -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() { diff --git a/static/css/basic.css b/static/css/basic.css index f87e6fdc..6ffe4c6e 100644 --- a/static/css/basic.css +++ b/static/css/basic.css @@ -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; } diff --git a/static/css/components.css b/static/css/components.css deleted file mode 100644 index ec5d3f00..00000000 --- a/static/css/components.css +++ /dev/null @@ -1,12 +0,0 @@ -/* Icon component */ - -.icon { - width: 1rem; - height: 1rem; -} - -/* PoweredBy component */ - -.poweredby { - text-align: center; -} diff --git a/static/css/root.css b/static/css/root.css deleted file mode 100644 index aeab1c67..00000000 --- a/static/css/root.css +++ /dev/null @@ -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; -}