From 9657672ffd5eda3ac7cfa5a423ad6c6970392728 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 23 Nov 2025 14:11:13 +0100 Subject: [PATCH 01/47] =?UTF-8?q?=F0=9F=93=9D=20Mejora=20documentaci=C3=B3?= =?UTF-8?q?n=20generada=20por=20`builder=5Ffn`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- helpers/pagetop-macros/src/lib.rs | 87 ++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 19 deletions(-) diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index 194cd378..e1ea55cb 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -138,7 +138,7 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream { /// # } /// ``` /// -/// la macro rescribirá el método `with_` y generará un nuevo método `alter_`: +/// la macro reescribirá el método `with_` y generará un nuevo método `alter_`: /// /// ```rust /// # struct Example {value: Option}; @@ -157,7 +157,11 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream { /// ``` /// /// De esta forma, cada método *builder* `with_...()` generará automáticamente su correspondiente -/// método `alter_...()` para dejar modificar instancias existentes. +/// método `alter_...()` para modificar instancias existentes. +/// +/// La documentación del método `with_...()` incluirá también la firma resumida del método +/// `alter_...()` y un alias de búsqueda con su nombre, de tal manera que buscando `alter_...` en la +/// documentación se mostrará la entrada del método `with_...()`. #[proc_macro_attribute] pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { use syn::{parse2, FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type}; @@ -282,11 +286,11 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { } } - // Genera el nombre del método alter_...(). + // Genera el nombre del método `alter_...()`. let stem = with_name_str.strip_prefix("with_").expect("validated"); let alter_ident = Ident::new(&format!("alter_{stem}"), with_name.span()); - // Extrae genéricos y cláusulas where. + // Extrae genéricos y cláusulas `where`. let generics = &sig.generics; let where_clause = &sig.generics.where_clause; @@ -319,28 +323,70 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { v }; - // Filtra los atributos descartando `#[doc]` y `#[inline]` para el método `alter_...()`. - let non_doc_or_inline_attrs: Vec<_> = attrs - .iter() - .filter(|a| { - let p = a.path(); - !p.is_ident("doc") && !p.is_ident("inline") - }) - .cloned() - .collect(); + // Separa atributos de documentación y resto. + let mut doc_attrs = Vec::new(); + let mut other_attrs = Vec::new(); + let mut non_doc_or_inline_attrs = Vec::new(); - // Documentación del método alter_...(). - let doc = format!("Equivale a [`Self::{with_name_str}()`], pero fuera del patrón *builder*."); + for a in attrs.iter() { + let p = a.path(); + if p.is_ident("doc") { + doc_attrs.push(a.clone()); + } else { + other_attrs.push(a.clone()); + if !p.is_ident("inline") { + non_doc_or_inline_attrs.push(a.clone()); + } + } + } + + // Firma resumida de la función `alter_...()` para mostrarla en la doc de `with_...()`. + let alter_sig_tokens = if args.is_empty() { + // Sin argumentos sólo se muestra `&mut self` (puede que no tenga mucho sentido). + quote! { #vis_pub fn #alter_ident #generics (&mut self) -> &mut Self #where_clause } + } else { + // Con argumentos se muestra `&mut self, ...`. + quote! { #vis_pub fn #alter_ident #generics (&mut self, ...) -> &mut Self #where_clause } + }; + + // Normaliza espacios raros tipo `& mut`. + let alter_sig_str = alter_sig_tokens.to_string().replace("& mut", "&mut"); + + // Nombre de la función `alter_...()` como alias de búsqueda. + let alter_name_str = alter_ident.to_string(); + + // Texto introductorio para la documentación adicional de `with_...()`. + let with_alter_title = format!( + "# Añade método `{}()` generado por [`#[builder_fn]`](pagetop_macros::builder_fn)", + alter_name_str + ); + let with_alter_doc = concat!( + "Modifica la instancia actual (`&mut self`) con los mismos argumentos ", + "en lugar de consumirla." + ); + + // Atributos completos que se aplican siempre a `with_...()`. + let with_prefix = quote! { + #(#other_attrs)* + #(#doc_attrs)* + #[doc(alias = #alter_name_str)] + #[doc = ""] + #[doc = #with_alter_title] + #[doc = #with_alter_doc] + #[doc = "```text"] + #[doc = #alter_sig_str] + #[doc = "```"] + }; // Genera el código final. let expanded = match body_opt { None => { quote! { - #(#attrs)* + #with_prefix fn #with_name #generics (self, #(#args),*) -> Self #where_clause; #(#non_doc_or_inline_attrs)* - #[doc = #doc] + #[doc(hidden)] fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause; } } @@ -351,8 +397,10 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { } else { quote! { #[inline] } }; + let with_fn = if is_trait { quote! { + #with_prefix #force_inline #vis_pub fn #with_name #generics (self, #(#args),*) -> Self #where_clause { let mut s = self; @@ -362,6 +410,7 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { } } else { quote! { + #with_prefix #force_inline #vis_pub fn #with_name #generics (mut self, #(#args),*) -> Self #where_clause { self.#alter_ident(#(#call_idents),*); @@ -369,12 +418,12 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { } } }; + quote! { - #(#attrs)* #with_fn #(#non_doc_or_inline_attrs)* - #[doc = #doc] + #[doc(hidden)] #vis_pub fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause { #body } -- 2.47.2 From a2fe2114cdf04e5c5f95bb148528429515fcb7d9 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 23 Nov 2025 14:35:38 +0100 Subject: [PATCH 02/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(theme):=20Refactori?= =?UTF-8?q?za=20renderizado=20de=20temas=20base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-aliner/src/lib.rs | 8 +++- extensions/pagetop-bootsier/src/lib.rs | 12 +++++- src/base/component/intro.rs | 18 ++++----- src/base/theme/basic.rs | 6 ++- src/core/component/children.rs | 13 +++++++ src/core/theme/definition.rs | 54 +++++++++++++------------- static/css/basic.css | 9 +++-- static/css/intro.css | 6 +-- 8 files changed, 79 insertions(+), 47 deletions(-) diff --git a/extensions/pagetop-aliner/src/lib.rs b/extensions/pagetop-aliner/src/lib.rs index 4ae4121e..5e915578 100644 --- a/extensions/pagetop-aliner/src/lib.rs +++ b/extensions/pagetop-aliner/src/lib.rs @@ -104,12 +104,16 @@ impl Extension for Aliner { } impl Theme for Aliner { - fn after_render_page_body(&self, page: &mut Page) { + fn before_render_page_body(&self, page: &mut Page) { page.alter_param("include_basic_assets", true) .alter_assets(ContextOp::AddStyleSheet( StyleSheet::from("/aliner/css/styles.css") .with_version(env!("CARGO_PKG_VERSION")) .with_weight(-90), - )); + )) + .alter_child_in( + Region::FOOTER, + ChildOp::AddIfEmpty(Child::with(PoweredBy::new())), + ); } } diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index 0bf94f47..fb9b7206 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -117,7 +117,7 @@ impl Extension for Bootsier { } impl Theme for Bootsier { - fn after_render_page_body(&self, page: &mut Page) { + fn before_render_page_body(&self, page: &mut Page) { page.alter_assets(ContextOp::AddStyleSheet( StyleSheet::from("/bootsier/bs/bootstrap.min.css") .with_version(BOOTSTRAP_VERSION) @@ -129,4 +129,14 @@ impl Theme for Bootsier { .with_weight(-90), )); } + + fn render_page_body(&self, page: &mut Page) -> Markup { + theme::Container::new() + .with_id("container-wrapper") + .with_width(theme::container::Width::FluidMax( + config::SETTINGS.bootsier.max_width, + )) + .add_child(Template::named(page.template())) + .render(page.context()) + } } diff --git a/src/base/component/intro.rs b/src/base/component/intro.rs index 052f9c60..5de7349a 100644 --- a/src/base/component/intro.rs +++ b/src/base/component/intro.rs @@ -17,17 +17,17 @@ pub enum IntroOpening { Custom, } -/// Componente para presentar PageTop (como [`Welcome`](crate::base::extension::Welcome)), o mostrar -/// introducciones. +/// Componente para divulgar PageTop (como hace [`Welcome`](crate::base::extension::Welcome)), o +/// mostrar presentaciones. /// -/// Usa la imagen de PageTop para presentar contenidos con: +/// Usa la imagen de PageTop para mostrar: /// -/// - Una **imagen decorativa** (el *monster* de PageTop) antecediendo al contenido. -/// - Una vista destacada con **título + eslogan**. +/// - Una **figura decorativa** (que incluye el *monster* de PageTop) antecediendo al contenido. +/// - Una vista destacada del **título** de la página con un **eslogan** de presentación. /// - Un **botón opcional** de llamada a la acción con texto y enlace configurables. -/// - El **área de textos** con *badges* predefinidos (en modo [`IntroOpening::PageTop`]) y bloques -/// ([`Block`](crate::base::component::Block)) para crear párrafos vistosos de texto. Aunque -/// admite todo tipo de componentes. +/// - Un **área para la presentación de contenidos**, con *badges* informativos de PageTop (si se +/// opta por [`IntroOpening::PageTop`]) y bloques ([`Block`](crate::base::component::Block)) de +/// contenido libre para crear párrafos vistosos de texto. Aunque admite todo tipo de componentes. /// /// ### Ejemplos /// @@ -51,7 +51,7 @@ pub enum IntroOpening { /// ))); /// ``` /// -/// **Sin botón + modo *Custom* (sin *badges* predefinidos)** +/// **Sin botón y en modo *Custom* (sin *badges* predefinidos)** /// /// ```rust /// # use pagetop::prelude::*; diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index eb2274f6..63ebe7a3 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -12,6 +12,10 @@ impl Extension for Basic { impl Theme for Basic { fn before_render_page_body(&self, page: &mut Page) { - page.alter_param("include_basic_assets", true); + page.alter_param("include_basic_assets", true) + .alter_child_in( + Region::FOOTER, + ChildOp::AddIfEmpty(Child::with(PoweredBy::new())), + ); } } diff --git a/src/core/component/children.rs b/src/core/component/children.rs index b3670433..15a6de22 100644 --- a/src/core/component/children.rs +++ b/src/core/component/children.rs @@ -172,6 +172,7 @@ impl Typed { /// Operaciones para componentes hijo [`Child`] en una lista [`Children`]. pub enum ChildOp { Add(Child), + AddIfEmpty(Child), AddMany(Vec), InsertAfterId(&'static str, Child), InsertBeforeId(&'static str, Child), @@ -185,6 +186,7 @@ pub enum ChildOp { /// Operaciones con un componente hijo tipado [`Typed`] en una lista [`Children`]. pub enum TypedOp { Add(Typed), + AddIfEmpty(Typed), AddMany(Vec>), InsertAfterId(&'static str, Typed), InsertBeforeId(&'static str, Typed), @@ -230,6 +232,7 @@ impl Children { pub fn with_child(mut self, op: ChildOp) -> Self { match op { ChildOp::Add(any) => self.add(any), + ChildOp::AddIfEmpty(any) => self.add_if_empty(any), ChildOp::AddMany(many) => self.add_many(many), ChildOp::InsertAfterId(id, any) => self.insert_after_id(id, any), ChildOp::InsertBeforeId(id, any) => self.insert_before_id(id, any), @@ -246,6 +249,7 @@ impl Children { pub fn with_typed(mut self, op: TypedOp) -> Self { match op { TypedOp::Add(typed) => self.add(typed.into()), + TypedOp::AddIfEmpty(typed) => self.add_if_empty(typed.into()), TypedOp::AddMany(many) => self.add_many(many.into_iter().map(Typed::::into)), TypedOp::InsertAfterId(id, typed) => self.insert_after_id(id, typed.into()), TypedOp::InsertBeforeId(id, typed) => self.insert_before_id(id, typed.into()), @@ -266,6 +270,15 @@ impl Children { self } + /// Añade un componente hijo en la lista sólo si está vacía. + #[inline] + pub fn add_if_empty(&mut self, child: Child) -> &mut Self { + if self.0.is_empty() { + self.0.push(child); + } + self + } + // **< Children GETTERS >*********************************************************************** /// Devuelve el número de componentes hijo de la lista. diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index de11d1ba..dda58b18 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -83,12 +83,33 @@ 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 añade una serie de hojas de estilo básicas (`normalize.css`, - /// `root.css`, `basic.css`) cuando el parámetro `include_basic_assets` de la página está - /// activado. #[allow(unused_variables)] - fn after_render_page_body(&self, page: &mut Page) { + fn after_render_page_body(&self, page: &mut Page) {} + + /// Renderiza el contenido del `` de la página. + /// + /// Aunque en una página el `` se encuentra antes del ``, internamente se renderiza + /// después para contar con los ajustes que hayan ido acumulando los componentes. Por ejemplo, + /// permitiría añadir un archivo de iconos sólo si se ha incluido un icono en la página. + /// + /// Por defecto incluye: + /// + /// - La codificación (`charset="utf-8"`). + /// - El título, usando el título de la página si existe y, en caso contrario, sólo el nombre de + /// la aplicación. + /// - La descripción (``), si está definida. + /// - 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 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"); @@ -108,29 +129,6 @@ pub trait Theme: Extension + Send + Sync { .with_weight(-99), )); } - } - - /// Renderiza el contenido del `` de la página. - /// - /// Aunque en una página el `` se encuentra antes del ``, internamente se renderiza - /// después para contar con los ajustes que hayan ido acumulando los componentes. Por ejemplo, - /// permitiría añadir un archivo de iconos sólo si se ha incluido un icono en la página. - /// - /// Por defecto incluye: - /// - /// - La codificación (`charset="utf-8"`). - /// - El título, usando el título de la página si existe y, en caso contrario, sólo el nombre de - /// la aplicación. - /// - La descripción (``), si está definida. - /// - La etiqueta `viewport` básica para diseño adaptable. - /// - Los metadatos (`name`/`content`) y propiedades (`property`/`content`) declarados en la - /// página. - /// - Todos 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 { let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no"; html! { meta charset="utf-8"; diff --git a/static/css/basic.css b/static/css/basic.css index 058e1736..f87e6fdc 100644 --- a/static/css/basic.css +++ b/static/css/basic.css @@ -14,8 +14,11 @@ body { -webkit-tap-highlight-color: transparent; } -/* Page layout */ +/* + * Region Footer + */ -.region--footer { - padding-bottom: 2rem; +.region-footer { + padding: .75rem 0 3rem; + text-align: center; } diff --git a/static/css/intro.css b/static/css/intro.css index dbc72252..9c47c5c4 100644 --- a/static/css/intro.css +++ b/static/css/intro.css @@ -50,7 +50,7 @@ } /* - * Header + * Intro Header */ .intro-header { @@ -125,7 +125,7 @@ } /* - * Content + * Intro Content */ .intro-content { @@ -445,7 +445,7 @@ } /* - * Footer + * Intro Footer */ .intro-footer { -- 2.47.2 From 4ac7caddd4cfd6dfee6c6cf22bfaa5951e3d8532 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 23 Nov 2025 14:37:00 +0100 Subject: [PATCH 03/47] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Elimina=20m=C3=A9?= =?UTF-8?q?todos=20y=20definiciones=20obsoletas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/html.rs | 58 ++------------------------------------- src/html/attr_l10n.rs | 7 ----- src/locale.rs | 6 ---- src/prelude.rs | 12 ++------ src/response/page.rs | 29 -------------------- src/service.rs | 64 ------------------------------------------- src/util.rs | 10 ------- 7 files changed, 4 insertions(+), 182 deletions(-) diff --git a/src/html.rs b/src/html.rs index 82fdcd73..d94aeea8 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,5 +1,7 @@ //! HTML en código. +use crate::AutoDefault; + mod maud; pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, DOCTYPE}; @@ -14,78 +16,22 @@ pub use assets::{Asset, Assets}; mod logo; pub use logo::PageTopSvg; -// **< HTML DOCUMENT CONTEXT >********************************************************************** - -/// **Obsoleto desde la versión 0.5.0**: usar [`core::component::Context`] en su lugar. -#[deprecated(since = "0.5.0", note = "Moved to `pagetop::core::component::Context`")] -pub type Context = crate::core::component::Context; - -/// **Obsoleto desde la versión 0.5.0**: usar [`core::component::ContextOp`] en su lugar. -#[deprecated( - since = "0.5.0", - note = "Moved to `pagetop::core::component::ContextOp`" -)] -pub type ContextOp = crate::core::component::ContextOp; - -/// **Obsoleto desde la versión 0.5.0**: usar [`core::component::Contextual`] en su lugar. -#[deprecated( - since = "0.5.0", - note = "Moved to `pagetop::core::component::Contextual`" -)] -pub trait Contextual: crate::core::component::Contextual {} - -/// **Obsoleto desde la versión 0.5.0**: usar [`core::component::ContextError`] en su lugar. -#[deprecated( - since = "0.5.0", - note = "Moved to `pagetop::core::component::ContextError`" -)] -pub type ContextError = crate::core::component::ContextError; - -/// **Obsoleto desde la versión 0.5.0**: usar [`ContextOp`] en su lugar. -#[deprecated(since = "0.5.0", note = "Use `ContextOp` instead")] -pub type AssetsOp = crate::core::component::ContextOp; - // **< HTML ATTRIBUTES >**************************************************************************** mod attr_id; pub use attr_id::AttrId; -/// **Obsoleto desde la versión 0.4.0**: usar [`AttrId`] en su lugar. -#[deprecated(since = "0.4.0", note = "Use `AttrId` instead")] -pub type OptionId = AttrId; mod attr_name; pub use attr_name::AttrName; -/// **Obsoleto desde la versión 0.4.0**: usar [`AttrName`] en su lugar. -#[deprecated(since = "0.4.0", note = "Use `AttrName` instead")] -pub type OptionName = AttrName; mod attr_value; pub use attr_value::AttrValue; -/// **Obsoleto desde la versión 0.4.0**: usar [`AttrValue`] en su lugar. -#[deprecated(since = "0.4.0", note = "Use `AttrValue` instead")] -pub type OptionString = AttrValue; mod attr_l10n; pub use attr_l10n::AttrL10n; -/// **Obsoleto desde la versión 0.4.0**: usar [`AttrL10n`] en su lugar. -#[deprecated(since = "0.4.0", note = "Use `AttrL10n` instead")] -pub type OptionTranslated = AttrL10n; mod attr_classes; pub use attr_classes::{AttrClasses, ClassesOp}; -/// **Obsoleto desde la versión 0.4.0**: usar [`AttrClasses`] en su lugar. -#[deprecated(since = "0.4.0", note = "Use `AttrClasses` instead")] -pub type OptionClasses = AttrClasses; - -use crate::{core, AutoDefault}; - -/// **Obsoleto desde la versión 0.4.0**: usar [`Typed`](crate::core::component::Typed) en su lugar. -#[deprecated( - since = "0.4.0", - note = "Use `pagetop::core::component::Typed` instead" -)] -#[allow(type_alias_bounds)] -pub type OptionComponent = core::component::Typed; mod unit; pub use unit::UnitValue; diff --git a/src/html/attr_l10n.rs b/src/html/attr_l10n.rs index 86d1c4a3..d25869c5 100644 --- a/src/html/attr_l10n.rs +++ b/src/html/attr_l10n.rs @@ -1,4 +1,3 @@ -use crate::html::Markup; use crate::locale::{L10n, LangId}; use crate::{builder_fn, AutoDefault}; @@ -58,10 +57,4 @@ impl AttrL10n { pub fn value(&self, language: &impl LangId) -> String { self.0.lookup(language).unwrap_or_default() } - - /// **Obsoleto desde la versión 0.4.0**: no recomendado para atributos HTML. - #[deprecated(since = "0.4.0", note = "For attributes use `lookup()` or `value()`")] - pub fn to_markup(&self, language: &impl LangId) -> Markup { - self.0.using(language) - } } diff --git a/src/locale.rs b/src/locale.rs index 5c000f7c..9ee87d7f 100644 --- a/src/locale.rs +++ b/src/locale.rs @@ -463,12 +463,6 @@ impl L10n { pub fn using(&self, language: &impl LangId) -> Markup { PreEscaped(self.lookup(language).unwrap_or_default()) } - - /// **Obsoleto desde la versión 0.4.0**: usar [`using()`](Self::using) en su lugar. - #[deprecated(since = "0.4.0", note = "Use `using()` instead")] - pub fn to_markup(&self, language: &impl LangId) -> Markup { - self.using(language) - } } impl fmt::Debug for L10n { diff --git a/src/prelude.rs b/src/prelude.rs index 47fbbf64..15121036 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -15,8 +15,7 @@ pub use crate::include_config; // crate::locale pub use crate::include_locales; // crate::service -#[allow(deprecated)] -pub use crate::{include_files, include_files_service, static_files_service}; +pub use crate::static_files_service; // crate::core::action pub use crate::actions_boxed; @@ -28,14 +27,7 @@ pub use crate::global; pub use crate::trace; -// No se usa `pub use crate::html::*;` para evitar duplicar alias marcados como obsoletos -// (*deprecated*) porque han sido trasladados a `crate::core::component`. Cuando se retiren estos -// alias obsoletos se volverá a declarar como `pub use crate::html::*;`. -pub use crate::html::{ - display, html_private, Asset, Assets, AttrClasses, AttrId, AttrL10n, AttrName, AttrValue, - ClassesOp, Escaper, Favicon, JavaScript, Markup, PageTopSvg, PreEscaped, PrepareMarkup, - StyleSheet, TargetMedia, UnitValue, DOCTYPE, -}; +pub use crate::html::*; pub use crate::locale::*; diff --git a/src/response/page.rs b/src/response/page.rs index 7d7789d4..b5516b8a 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -95,19 +95,6 @@ impl Page { self } - /// **Obsoleto desde la versión 0.4.0**: usar [`add_child()`](Self::add_child) en su lugar. - #[deprecated(since = "0.4.0", note = "Use `add_child()` instead")] - pub fn with_component(self, component: impl Component) -> Self { - self.add_child(component) - } - - /// **Obsoleto desde la versión 0.4.0**: usar [`add_child_in()`](Self::add_child_in) en su - /// lugar. - #[deprecated(since = "0.4.0", note = "Use `add_child_in()` instead")] - pub fn with_component_in(self, region_key: &'static str, component: impl Component) -> Self { - self.add_child_in(region_key, component) - } - /// 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 @@ -122,22 +109,6 @@ impl Page { self } - /// **Obsoleto desde la versión 0.4.0**: usar [`with_child_in()`](Self::with_child_in) en su - /// lugar. - #[deprecated(since = "0.4.0", note = "Use `with_child_in()` instead")] - pub fn with_child_in_region(mut self, region_key: &'static str, op: ChildOp) -> Self { - self.alter_child_in(region_key, op); - self - } - - /// **Obsoleto desde la versión 0.4.0**: usar [`alter_child_in()`](Self::alter_child_in) en su - /// lugar. - #[deprecated(since = "0.4.0", note = "Use `alter_child_in()` instead")] - pub fn alter_child_in_region(&mut self, region_key: &'static str, op: ChildOp) -> &mut Self { - self.alter_child_in(region_key, op); - self - } - // **< Page GETTERS >*************************************************************************** /// Devuelve el título traducido para el idioma de la página, si existe. diff --git a/src/service.rs b/src/service.rs index 1b2f766c..cb69d76a 100644 --- a/src/service.rs +++ b/src/service.rs @@ -15,70 +15,6 @@ pub use pagetop_statics::ResourceFiles; #[doc(hidden)] pub use actix_web::test; -/// **Obsoleto desde la versión 0.3.0**: usar [`static_files_service!`](crate::static_files_service) -/// en su lugar. -#[deprecated(since = "0.3.0", note = "Use `static_files_service!` instead")] -#[macro_export] -macro_rules! include_files { - // Forma 1: incluye un conjunto de recursos por nombre. - ( $bundle:ident ) => { - $crate::util::paste! { - mod [] { - include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); - } - } - }; - // Forma 2: asigna a una variable estática $STATIC un conjunto de recursos. - ( $STATIC:ident => $bundle:ident ) => { - $crate::util::paste! { - mod [] { - include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); - } - pub static $STATIC: std::sync::LazyLock<$crate::StaticResources> = - std::sync::LazyLock::new( - $crate::StaticResources::new([]::$bundle) - ); - } - }; -} - -/// **Obsoleto desde la versión 0.3.0**: usar [`static_files_service!`](crate::static_files_service) -/// en su lugar. -#[deprecated(since = "0.3.0", note = "Use `static_files_service!` instead")] -#[macro_export] -macro_rules! include_files_service { - ( $scfg:ident, $bundle:ident => $route:expr $(, [$root:expr, $relative:expr])? ) => {{ - $crate::util::paste! { - let span = $crate::trace::debug_span!("Configuring static files ", path = $route); - let _ = span.in_scope(|| { - // Determina si se sirven recursos embebidos (`true`) o desde disco (`false`). - #[allow(unused_mut)] - let mut serve_embedded:bool = true; - $( - // Si `$root` y `$relative` no están vacíos, se comprueba la ruta absoluta. - if !$root.is_empty() && !$relative.is_empty() { - if let Ok(absolute) = $crate::util::absolute_dir($root, $relative) { - // Servimos directamente desde el sistema de ficheros. - $scfg.service($crate::service::ActixFiles::new( - $route, - absolute, - ).show_files_listing()); - serve_embedded = false - } - } - )? - // Si no se localiza el directorio, se exponen entonces los recursos embebidos. - if serve_embedded { - $scfg.service($crate::service::ResourceFiles::new( - $route, - []::$bundle(), - )); - } - }); - } - }}; -} - /// Configura un servicio web para publicar archivos estáticos. /// /// La macro ofrece tres modos para configurar el servicio: diff --git a/src/util.rs b/src/util.rs index 30b994f2..bfb50ec0 100644 --- a/src/util.rs +++ b/src/util.rs @@ -167,13 +167,3 @@ pub fn resolve_absolute_dir>(path: P) -> io::Result { }) } } - -/// **Obsoleto desde la versión 0.3.0**: usar [`resolve_absolute_dir()`] en su lugar. -#[deprecated(since = "0.3.0", note = "Use `resolve_absolute_dir()` instead")] -pub fn absolute_dir(root_path: P, relative_path: Q) -> io::Result -where - P: AsRef, - Q: AsRef, -{ - resolve_absolute_dir(root_path.as_ref().join(relative_path.as_ref())) -} -- 2.47.2 From 0849d23e3fc4784e126f325b87e4b2c56ce369ea Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 29 Nov 2025 13:55:35 +0100 Subject: [PATCH 04/47] =?UTF-8?q?=F0=9F=9A=A7=20A=C3=B1ade=20constante=20`?= =?UTF-8?q?PAGETOP=5FVERSION`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-aliner/src/lib.rs | 29 ++++++++++++++++-------- src/app.rs | 4 ++-- src/base/component/intro.rs | 2 +- src/base/theme/basic.rs | 19 ++++++++++++---- src/lib.rs | 34 ++++++++++++++++++++++++++++ src/prelude.rs | 2 ++ 6 files changed, 72 insertions(+), 18 deletions(-) diff --git a/extensions/pagetop-aliner/src/lib.rs b/extensions/pagetop-aliner/src/lib.rs index 5e915578..80e6ca15 100644 --- a/extensions/pagetop-aliner/src/lib.rs +++ b/extensions/pagetop-aliner/src/lib.rs @@ -105,15 +105,24 @@ impl Extension for Aliner { impl Theme for Aliner { fn before_render_page_body(&self, page: &mut Page) { - page.alter_param("include_basic_assets", true) - .alter_assets(ContextOp::AddStyleSheet( - StyleSheet::from("/aliner/css/styles.css") - .with_version(env!("CARGO_PKG_VERSION")) - .with_weight(-90), - )) - .alter_child_in( - Region::FOOTER, - ChildOp::AddIfEmpty(Child::with(PoweredBy::new())), - ); + 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/basic.css") + .with_version(PAGETOP_VERSION) + .with_weight(-99), + )) + .alter_assets(ContextOp::AddStyleSheet( + StyleSheet::from("/aliner/css/styles.css") + .with_version(env!("CARGO_PKG_VERSION")) + .with_weight(-99), + )) + .alter_child_in( + Region::FOOTER, + ChildOp::AddIfEmpty(Child::with(PoweredBy::new())), + ); } } diff --git a/src/app.rs b/src/app.rs index c8ffba11..6ecff369 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,7 +6,7 @@ use crate::core::{extension, extension::ExtensionRef}; use crate::html::Markup; use crate::response::page::{ErrorPage, ResultPage}; use crate::service::HttpRequest; -use crate::{global, locale, service, trace}; +use crate::{global, locale, service, trace, PAGETOP_VERSION}; use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle}; use actix_session::storage::CookieSessionStore; @@ -108,7 +108,7 @@ impl Application { println!( "{} {}\n", "Powered by PageTop".yellow(), - env!("CARGO_PKG_VERSION").yellow() + PAGETOP_VERSION.yellow() ); } } diff --git a/src/base/component/intro.rs b/src/base/component/intro.rs index 5de7349a..ea01ccc8 100644 --- a/src/base/component/intro.rs +++ b/src/base/component/intro.rs @@ -105,7 +105,7 @@ impl Component for Intro { fn setup_before_prepare(&mut self, cx: &mut Context) { cx.alter_assets(ContextOp::AddStyleSheet( - StyleSheet::from("/css/intro.css").with_version(env!("CARGO_PKG_VERSION")), + StyleSheet::from("/css/intro.css").with_version(PAGETOP_VERSION), )); } diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index 63ebe7a3..83dc4a8d 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -12,10 +12,19 @@ impl Extension for Basic { impl Theme for Basic { fn before_render_page_body(&self, page: &mut Page) { - page.alter_param("include_basic_assets", true) - .alter_child_in( - Region::FOOTER, - ChildOp::AddIfEmpty(Child::with(PoweredBy::new())), - ); + 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/basic.css") + .with_version(PAGETOP_VERSION) + .with_weight(-99), + )) + .alter_child_in( + Region::FOOTER, + ChildOp::AddIfEmpty(Child::with(PoweredBy::new())), + ); } } diff --git a/src/lib.rs b/src/lib.rs index a2683495..caa74536 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,6 +99,40 @@ use std::ops::Deref; // **< RE-EXPORTED >******************************************************************************** +/// Versión del *crate* `pagetop`, obtenida en tiempo de compilación (`CARGO_PKG_VERSION`). +/// +/// Útil para versionar recursos estáticos de PageTop desde otros *crates*. Por ejemplo: +/// +/// ```rust +/// use pagetop::prelude::*; +/// +/// pub struct MyTheme; +/// +/// impl Extension for MyTheme { +/// fn theme(&self) -> Option { +/// Some(&Self) +/// } +/// } +/// +/// impl Theme for MyTheme { +/// fn before_render_page_body(&self, page: &mut Page) { +/// page +/// .alter_assets(ContextOp::AddStyleSheet( +/// StyleSheet::from("/css/normalize.css").with_version("8.0.1"), +/// )) +/// .alter_assets(ContextOp::AddStyleSheet( +/// StyleSheet::from("/css/basic.css").with_version(PAGETOP_VERSION), +/// )) +/// .alter_assets(ContextOp::AddStyleSheet( +/// StyleSheet::from("/mytheme/styles.css").with_version(env!("CARGO_PKG_VERSION")), +/// )); +/// } +/// } +/// ``` +/// Donde `PAGETOP_VERSION` identifica la versión de PageTop y `env!("CARGO_PKG_VERSION")` hace +/// referencia a la versión del *crate* que lo usa. +pub const PAGETOP_VERSION: &str = env!("CARGO_PKG_VERSION"); + pub use pagetop_macros::{builder_fn, html, main, test, AutoDefault}; pub use pagetop_statics::{resource, StaticResource}; diff --git a/src/prelude.rs b/src/prelude.rs index 15121036..1d649fec 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -2,6 +2,8 @@ // RE-EXPORTED. +pub use crate::PAGETOP_VERSION; + pub use crate::{builder_fn, html, main, test}; pub use crate::{AutoDefault, StaticResources, UniqueId, Weight}; -- 2.47.2 From 2ce74fec8e8544a84ddd9949ba045a2a8166abe8 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 29 Nov 2025 14:42:05 +0100 Subject: [PATCH 05/47] =?UTF-8?q?=F0=9F=9A=A7=20Retoques=20menores=20en=20?= =?UTF-8?q?los=20comentarios=20del=20c=C3=B3digo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-bootsier/src/theme/navbar/component.rs | 2 +- src/base/component/html.rs | 2 +- src/base/component/intro.rs | 2 +- src/base/component/poweredby.rs | 4 ++-- src/html/unit.rs | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extensions/pagetop-bootsier/src/theme/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/navbar/component.rs index b40d06c2..225c2af5 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/component.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/component.rs @@ -73,7 +73,7 @@ impl Component for Navbar { return PrepareMarkup::None; } - // Asegura que la barra tiene un id estable para poder asociarlo al colapso/offcanvas. + // Asegura que la barra tiene un `id` para poder asociarlo al colapso/offcanvas. let id = cx.required_id::(self.id()); PrepareMarkup::With(html! { diff --git a/src/base/component/html.rs b/src/base/component/html.rs index a60d30f9..1cca9899 100644 --- a/src/base/component/html.rs +++ b/src/base/component/html.rs @@ -1,6 +1,6 @@ use crate::prelude::*; -/// Componente básico para renderizar dinámicamente código HTML recibiendo el contexto. +/// Componente básico que renderiza dinámicamente código HTML según el contexto. /// /// Este componente permite generar contenido HTML arbitrario, usando la macro `html!` y accediendo /// opcionalmente al contexto de renderizado. diff --git a/src/base/component/intro.rs b/src/base/component/intro.rs index ea01ccc8..7a3b2812 100644 --- a/src/base/component/intro.rs +++ b/src/base/component/intro.rs @@ -29,7 +29,7 @@ pub enum IntroOpening { /// opta por [`IntroOpening::PageTop`]) y bloques ([`Block`](crate::base::component::Block)) de /// contenido libre para crear párrafos vistosos de texto. Aunque admite todo tipo de componentes. /// -/// ### Ejemplos +/// # Ejemplos /// /// **Intro mínima por defecto** /// diff --git a/src/base/component/poweredby.rs b/src/base/component/poweredby.rs index 797253dc..5a2dc5b2 100644 --- a/src/base/component/poweredby.rs +++ b/src/base/component/poweredby.rs @@ -3,7 +3,7 @@ use crate::prelude::*; // Enlace a la página oficial de PageTop. const LINK: &str = "PageTop"; -/// Componente que informa del 'Powered by' (*Funciona con*) típica del pie de página. +/// Componente que muestra el típico mensaje *Powered by* (*Funciona con*) en el pie de página. /// /// Por defecto, usando [`default()`](Self::default) sólo se muestra un reconocimiento a PageTop. /// Sin embargo, se puede usar [`new()`](Self::new) para crear una instancia con un texto de @@ -17,7 +17,7 @@ impl Component for PoweredBy { /// Crea una nueva instancia de `PoweredBy`. /// /// El copyright se genera automáticamente con el año actual y el nombre de la aplicación - /// configurada en [`global::SETTINGS`]. + /// configurada en [`global::SETTINGS`], en el formato `YYYY © Nombre de la aplicación`. fn new() -> Self { let year = Utc::now().format("%Y").to_string(); let c = join!(year, " © ", global::SETTINGS.app.name); diff --git a/src/html/unit.rs b/src/html/unit.rs index 4a99de16..aec40372 100644 --- a/src/html/unit.rs +++ b/src/html/unit.rs @@ -167,7 +167,7 @@ impl fmt::Display for UnitValue { /// Convierte una cadena a [`UnitValue`] siguiendo una gramática CSS acotada. /// -/// ## Acepta +/// # Acepta /// /// - `""` para `UnitValue::None` /// - `"auto"` @@ -178,7 +178,7 @@ impl fmt::Display for UnitValue { /// /// (Se toleran espacios entre número y unidad: `"12 px"`, `"1.5 rem"`). /// -/// ## Ejemplo +/// # Ejemplo /// /// ```rust /// # use pagetop::prelude::*; @@ -188,7 +188,7 @@ impl fmt::Display for UnitValue { /// assert!(UnitValue::from_str("12").is_err()); /// ``` /// -/// ## Errores de interpretación +/// # Errores de interpretación /// /// - Falta la unidad cuando es necesaria (p. ej., `"12"`, excepto para el valor cero). /// - Decimales en valores que deben ser absolutos (p. ej. `"1.5px"`). @@ -262,7 +262,7 @@ impl FromStr for UnitValue { /// Deserializa desde una cadena usando la misma gramática que [`FromStr`]. /// -/// ### Ejemplo con `serde_json` +/// # Ejemplo con `serde_json` /// ```rust /// # use pagetop::prelude::*; /// use serde::Deserialize; -- 2.47.2 From bfdc0da407aacf38ae3590605192a411f692f016 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 29 Nov 2025 14:43:37 +0100 Subject: [PATCH 06/47] =?UTF-8?q?=F0=9F=9A=A7=20Mejora=20documentaci=C3=B3?= =?UTF-8?q?n=20generada=20por=20`builder=5Ffn`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- helpers/pagetop-macros/src/lib.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index e1ea55cb..5772a6ce 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -357,12 +357,17 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { // Texto introductorio para la documentación adicional de `with_...()`. let with_alter_title = format!( - "# Añade método `{}()` generado por [`#[builder_fn]`](pagetop_macros::builder_fn)", + "# {} el método `{}()` generado por [`#[builder_fn]`](pagetop_macros::builder_fn)", + if doc_attrs.is_empty() { + "Añade" + } else { + "También añade" + }, alter_name_str ); let with_alter_doc = concat!( - "Modifica la instancia actual (`&mut self`) con los mismos argumentos ", - "en lugar de consumirla." + "Modifica la instancia actual (`&mut self`) con los mismos argumentos, ", + "sin consumirla." ); // Atributos completos que se aplican siempre a `with_...()`. -- 2.47.2 From f2733bb25001e6a4df53cc588602fdc519ca2761 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 30 Nov 2025 00:16:54 +0100 Subject: [PATCH 07/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactoriza=20la=20g?= =?UTF-8?q?esti=C3=B3n=20de=20regiones=20y=20plantillas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-aliner/src/lib.rs | 2 +- src/base/component.rs | 38 ----- src/base/component/region.rs | 150 ------------------- src/base/component/template.rs | 84 ----------- src/base/theme/basic.rs | 2 +- src/core/component/context.rs | 35 +++-- src/core/extension/definition.rs | 52 ++++--- src/core/theme.rs | 210 ++++++++++++++++++++++++-- src/core/theme/definition.rs | 93 ++++++------ src/core/theme/regions.rs | 93 +++++++----- src/response/page.rs | 123 +++++++++++++--- src/response/page/error.rs | 24 ++- static/css/basic.css | 19 ++- static/css/components.css | 12 -- static/css/root.css | 212 --------------------------- 15 files changed, 494 insertions(+), 655 deletions(-) delete mode 100644 src/base/component/region.rs delete mode 100644 src/base/component/template.rs delete mode 100644 static/css/components.css delete mode 100644 static/css/root.css 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; -} -- 2.47.2 From 9c6888e378f598ca2c126beff855c07eecb3faa4 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 30 Nov 2025 10:53:49 +0100 Subject: [PATCH 08/47] =?UTF-8?q?=E2=9C=A8=20(bootsier):=20A=C3=B1ade=20pl?= =?UTF-8?q?antilla=20est=C3=A1ndar=20propia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/navbar-menus.rs | 2 +- extensions/pagetop-bootsier/src/lib.rs | 49 ++++++++++++++----- .../static/scss/_customs.scss | 6 +++ static/css/intro.css | 11 ++++- 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index 341d394a..079508e9 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -95,7 +95,7 @@ impl Extension for SuperMenu { })), )); - InRegion::Named("header").add(Child::with( + InRegion::Global(&DefaultRegion::Header).add(Child::with( Container::new() .with_width(container::Width::FluidMax(UnitValue::RelRem(75.0))) .add_child(navbar_menu), diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index fb9b7206..5c88959a 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -102,6 +102,34 @@ pub mod prelude { pub use crate::theme::*; } +/// Plantillas que Bootsier añade. +#[derive(AutoDefault)] +pub enum BootsierTemplate { + /// Plantilla predeterminada de Bootsier. + #[default] + Standard, +} + +impl Template for BootsierTemplate { + fn render(&'static self, cx: &mut Context) -> Markup { + match self { + Self::Standard => theme::Container::new() + .with_classes(ClassesOp::Add, "container-wrapper") + .with_width(theme::container::Width::FluidMax( + config::SETTINGS.bootsier.max_width, + )) + .add_child(Html::with(|cx| { + html! { + (DefaultRegion::Header.render(cx)) + (DefaultRegion::Content.render(cx)) + (DefaultRegion::Footer.render(cx)) + } + })), + } + .render(cx) + } +} + /// Implementa el tema. pub struct Bootsier; @@ -117,6 +145,11 @@ impl Extension for Bootsier { } impl Theme for Bootsier { + #[inline] + fn default_template(&self) -> TemplateRef { + &BootsierTemplate::Standard + } + fn before_render_page_body(&self, page: &mut Page) { page.alter_assets(ContextOp::AddStyleSheet( StyleSheet::from("/bootsier/bs/bootstrap.min.css") @@ -127,16 +160,10 @@ impl Theme for Bootsier { JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js") .with_version(BOOTSTRAP_VERSION) .with_weight(-90), - )); - } - - fn render_page_body(&self, page: &mut Page) -> Markup { - theme::Container::new() - .with_id("container-wrapper") - .with_width(theme::container::Width::FluidMax( - config::SETTINGS.bootsier.max_width, - )) - .add_child(Template::named(page.template())) - .render(page.context()) + )) + .alter_child_in( + &DefaultRegion::Footer, + ChildOp::AddIfEmpty(Child::with(PoweredBy::new())), + ); } } diff --git a/extensions/pagetop-bootsier/static/scss/_customs.scss b/extensions/pagetop-bootsier/static/scss/_customs.scss index 988d7055..45e41001 100644 --- a/extensions/pagetop-bootsier/static/scss/_customs.scss +++ b/extensions/pagetop-bootsier/static/scss/_customs.scss @@ -106,3 +106,9 @@ $utilities: map-merge( ), ) ); + +// Region Footer +.region-footer { + padding: .75rem 0 3rem; + text-align: center; +} diff --git a/static/css/intro.css b/static/css/intro.css index 9c47c5c4..e3de4153 100644 --- a/static/css/intro.css +++ b/static/css/intro.css @@ -17,13 +17,22 @@ --intro-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); } +body { + overflow-x: clip; +} + .intro { position: relative; min-width: 350px; color: var(--intro-color); background-color: var(--intro-bg-color); - width: 100%; + left: 50%; + right: 50%; + margin-left: -50vw; + margin-right: -50vw; + width: 100vw; + display: flex; flex-direction: column; justify-content: center; -- 2.47.2 From 12e617f35b5a593d5ec939385b551b7f87061f56 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 30 Nov 2025 11:11:39 +0100 Subject: [PATCH 09/47] =?UTF-8?q?=F0=9F=9A=A7=20Afina=20el=20mensaje=20gen?= =?UTF-8?q?erado=20por=20`builder=5Ffn`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- helpers/pagetop-macros/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index 5772a6ce..6fa12357 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -366,7 +366,7 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { alter_name_str ); let with_alter_doc = concat!( - "Modifica la instancia actual (`&mut self`) con los mismos argumentos, ", + "Permite modificar la instancia actual (`&mut self`) con los mismos argumentos, ", "sin consumirla." ); -- 2.47.2 From 76b980017d3d88e5513e7ce340cc813cd98f755f Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 30 Nov 2025 11:14:08 +0100 Subject: [PATCH 10/47] =?UTF-8?q?=F0=9F=92=84=20Mejora=20alineaci=C3=B3n?= =?UTF-8?q?=20del=20texto=20en=20ejemplos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/hello-name.rs | 6 +++++- examples/hello-world.rs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/hello-name.rs b/examples/hello-name.rs index c6a82aaf..b6f9c113 100644 --- a/examples/hello-name.rs +++ b/examples/hello-name.rs @@ -14,7 +14,11 @@ async fn hello_name( ) -> ResultPage { let name = path.into_inner(); Page::new(request) - .add_child(Html::with(move |_| html! { h1 { "Hello " (name) "!" } })) + .add_child(Html::with(move |_| { + html! { + h1 style="text-align: center;" { "Hello " (name) "!" } + } + })) .render() } diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 64817466..74727ac2 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -10,7 +10,11 @@ impl Extension for HelloWorld { async fn hello_world(request: HttpRequest) -> ResultPage { Page::new(request) - .add_child(Html::with(|_| html! { h1 { "Hello World!" } })) + .add_child(Html::with(|_| { + html! { + h1 style="text-align: center;" { "Hello World!" } + } + })) .render() } -- 2.47.2 From 1fa1ddf5285be06b25083ff2fe0d03ce1f2231ae Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 30 Nov 2025 11:14:34 +0100 Subject: [PATCH 11/47] =?UTF-8?q?=F0=9F=92=A1Retoques=20menores=20en=20com?= =?UTF-8?q?entarios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/base/component/html.rs | 9 ++++----- src/core/component/context.rs | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/base/component/html.rs b/src/base/component/html.rs index 1cca9899..48e47bbb 100644 --- a/src/base/component/html.rs +++ b/src/base/component/html.rs @@ -51,9 +51,8 @@ impl Html { /// Crea una instancia que generará el `Markup`, con acceso opcional al contexto. /// - /// El método [`prepare_component()`](crate::core::component::Component::prepare_component) - /// delega el renderizado en la función proporcionada, que recibe una referencia mutable al - /// contexto de renderizado ([`Context`]). + /// El método [`Self::prepare_component()`] delega el renderizado a la función que aquí se + /// proporciona, que recibe una referencia mutable al [`Context`]. pub fn with(f: F) -> Self where F: Fn(&mut Context) -> Markup + Send + Sync + 'static, @@ -64,8 +63,8 @@ impl Html { /// Sustituye la función que genera el `Markup`. /// /// Permite a otras extensiones modificar la función de renderizado que se ejecutará cuando - /// [`prepare_component()`](crate::core::component::Component::prepare_component) invoque esta - /// instancia. La nueva función también recibe una referencia al contexto ([`Context`]). + /// [`Self::prepare_component()`] invoque esta instancia. La nueva función también recibe una + /// referencia al [`Context`]. #[builder_fn] pub fn with_fn(mut self, f: F) -> Self where diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 922067f4..4f2cdf18 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -11,7 +11,7 @@ use crate::{builder_fn, join}; use std::any::Any; use std::collections::HashMap; -/// Operaciones para modificar recursos asociados al contexto ([`Context`]) de un documento. +/// Operaciones para modificar recursos asociados al [`Context`] de un documento. pub enum ContextOp { /// Define el *favicon* del documento. Sobrescribe cualquier valor anterior. SetFavicon(Option), @@ -386,7 +386,7 @@ impl Context { /// 3. Un idioma válido extraído de la cabecera `Accept-Language` del navegador. /// 4. Y si ninguna de las opciones anteriores aplica, se usa el idioma de respaldo (`"en-US"`). /// -/// Resulta útil para usar un contexto ([`Context`]) como fuente de traducción en +/// Resulta útil para usar el [`Context`] como fuente de traducción en /// [`L10n::lookup()`](crate::locale::L10n::lookup) o [`L10n::using()`](crate::locale::L10n::using). impl LangId for Context { fn langid(&self) -> &'static LanguageIdentifier { -- 2.47.2 From af26e6aef9ef1c7a65ac6910dfeed71bd297ca61 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 30 Nov 2025 11:42:03 +0100 Subject: [PATCH 12/47] =?UTF-8?q?=F0=9F=8C=90=20Normaliza=20textos=20y=20l?= =?UTF-8?q?ocalizaci=C3=B3n=20a=20*snake=5Fcase*?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/locale/en-US/bootsier.ftl | 10 +++++----- .../src/locale/en-US/regions.ftl | 18 +++++++++--------- .../src/locale/es-ES/bootsier.ftl | 10 +++++----- .../src/locale/es-ES/regions.ftl | 18 +++++++++--------- .../src/theme/image/component.rs | 2 +- .../pagetop-bootsier/src/theme/navbar.rs | 2 +- .../pagetop-bootsier/src/theme/navbar/item.rs | 4 ++-- src/core/extension/definition.rs | 4 ++-- src/core/theme.rs | 6 +++--- src/core/theme/definition.rs | 4 ++-- src/html/attr_l10n.rs | 2 +- src/locale.rs | 2 +- src/locale/en-US/test.ftl | 6 +++--- src/locale/es-ES/test.ftl | 6 +++--- tests/locale.rs | 8 ++++---- 15 files changed, 51 insertions(+), 51 deletions(-) diff --git a/extensions/pagetop-bootsier/src/locale/en-US/bootsier.ftl b/extensions/pagetop-bootsier/src/locale/en-US/bootsier.ftl index 0e8969cd..2454c84e 100644 --- a/extensions/pagetop-bootsier/src/locale/en-US/bootsier.ftl +++ b/extensions/pagetop-bootsier/src/locale/en-US/bootsier.ftl @@ -1,5 +1,5 @@ -e404-description = Oops! Page Not Found -e404-message = The page you are looking for may have been removed, had its name changed, or is temporarily unavailable. -e500-description = Oops! Unexpected Error -e500-message = We're having an issue. Please report this error to an administrator. -back-homepage = Back to homepage +e404_description = Oops! Page Not Found +e404_message = The page you are looking for may have been removed, had its name changed, or is temporarily unavailable. +e500_description = Oops! Unexpected Error +e500_message = We're having an issue. Please report this error to an administrator. +back_homepage = Back to homepage diff --git a/extensions/pagetop-bootsier/src/locale/en-US/regions.ftl b/extensions/pagetop-bootsier/src/locale/en-US/regions.ftl index f3b76e22..af830a4e 100644 --- a/extensions/pagetop-bootsier/src/locale/en-US/regions.ftl +++ b/extensions/pagetop-bootsier/src/locale/en-US/regions.ftl @@ -1,9 +1,9 @@ -header = Header -nav_branding = Navigation branding region -nav_main = Main navigation region -nav_additional = Additional navigation region (eg search form, social icons, etc) -breadcrumb = Breadcrumb -content = Main content -sidebar_first = Sidebar first -sidebar_second = Sidebar second -footer = Footer +region_header = Header +region_nav_branding = Navigation branding region +region_nav_main = Main navigation region +region_nav_additional = Additional navigation region (eg search form, social icons, etc) +region_breadcrumb = Breadcrumb +region_content = Main content +region_sidebar_first = Sidebar first +region_sidebar_second = Sidebar second +region_footer = Footer diff --git a/extensions/pagetop-bootsier/src/locale/es-ES/bootsier.ftl b/extensions/pagetop-bootsier/src/locale/es-ES/bootsier.ftl index 998b54f2..bd97c2ed 100644 --- a/extensions/pagetop-bootsier/src/locale/es-ES/bootsier.ftl +++ b/extensions/pagetop-bootsier/src/locale/es-ES/bootsier.ftl @@ -1,5 +1,5 @@ -e404-description = ¡Vaya! Página No Encontrada -e404-message = La página que está buscando puede haber sido eliminada, cambiada de nombre o no está disponible temporalmente. -e500-description = ¡Vaya! Error Inesperado -e500-message = Está ocurriendo una incidencia. Por favor, informe de este error a un administrador. -back-homepage = Volver al inicio +e404_description = ¡Vaya! Página No Encontrada +e404_message = La página que está buscando puede haber sido eliminada, cambiada de nombre o no está disponible temporalmente. +e500_description = ¡Vaya! Error Inesperado +e500_message = Está ocurriendo una incidencia. Por favor, informe de este error a un administrador. +back_homepage = Volver al inicio diff --git a/extensions/pagetop-bootsier/src/locale/es-ES/regions.ftl b/extensions/pagetop-bootsier/src/locale/es-ES/regions.ftl index 674fc4b1..e4665add 100644 --- a/extensions/pagetop-bootsier/src/locale/es-ES/regions.ftl +++ b/extensions/pagetop-bootsier/src/locale/es-ES/regions.ftl @@ -1,9 +1,9 @@ -header = Cabecera -nav_branding = Navegación y marca -nav_main = Navegación principal -nav_additional = Navegación adicional (p.e. formulario de búsqueda, iconos sociales, etc.) -breadcrumb = Ruta de posicionamiento -content = Contenido principal -sidebar_first = Barra lateral primera -sidebar_second = Barra lateral segunda -footer = Pie +region_header = Cabecera +region_nav_branding = Navegación y marca +region_nav_main = Navegación principal +region_nav_additional = Navegación adicional (p.e. formulario de búsqueda, iconos sociales, etc.) +region_breadcrumb = Ruta de posicionamiento +region_content = Contenido principal +region_sidebar_first = Barra lateral primera +region_sidebar_second = Barra lateral segunda +region_footer = Pie diff --git a/extensions/pagetop-bootsier/src/theme/image/component.rs b/extensions/pagetop-bootsier/src/theme/image/component.rs index bc3f2c9d..31fa7083 100644 --- a/extensions/pagetop-bootsier/src/theme/image/component.rs +++ b/extensions/pagetop-bootsier/src/theme/image/component.rs @@ -107,7 +107,7 @@ impl Image { self } - /// Define el texto alternativo localizado ([`L10n`]) para la imagen. + /// Define un *texto localizado* ([`L10n`]) alternativo para la imagen. /// /// Se recomienda siempre aportar un texto alternativo salvo que la imagen sea puramente /// decorativa. diff --git a/extensions/pagetop-bootsier/src/theme/navbar.rs b/extensions/pagetop-bootsier/src/theme/navbar.rs index 7b958d7f..bd605508 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar.rs @@ -2,7 +2,7 @@ //! //! Cada [`navbar::Item`](crate::theme::navbar::Item) representa un elemento individual de la barra //! de navegación [`Navbar`], con distintos comportamientos según su finalidad, como menús -//! [`Nav`](crate::theme::Nav) o textos localizados usando [`L10n`](pagetop::locale::L10n). +//! [`Nav`](crate::theme::Nav) o *textos localizados* usando [`L10n`](pagetop::locale::L10n). //! //! También puede mostrar una marca de identidad ([`navbar::Brand`](crate::theme::navbar::Brand)) //! que identifique la compañía, producto o nombre del proyecto asociado a la solución web. diff --git a/extensions/pagetop-bootsier/src/theme/navbar/item.rs b/extensions/pagetop-bootsier/src/theme/navbar/item.rs index 7e912a49..1b1c5204 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/item.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/item.rs @@ -20,7 +20,7 @@ pub enum Item { Brand(Typed), /// Representa un menú de navegación [`Nav`](crate::theme::Nav). Nav(Typed
-## Sobre PageTop +## 🧭 Sobre PageTop [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y configurables, basadas en HTML, CSS y JavaScript. -## Descripción general + +## 🗺️ Descripción general Este *crate* proporciona un conjunto básico de macros que se integran en las utilidades de PageTop para optimizar operaciones habituales relacionadas con la composición estructurada de texto, la concatenación de cadenas y el uso rápido de colecciones clave-valor. -## Créditos + +## 📚 Créditos Las macros para texto multilínea **`indoc!`**, **`formatdoc!`** y **`concatdoc!`** se reexportan del *crate* [indoc](https://crates.io/crates/indoc) de [David Tolnay](https://crates.io/users/dtolnay). @@ -38,14 +40,14 @@ La macro para generar identificadores dinámicos **`paste!`** se reexporta del * `paste!` de [David Tolnay](https://crates.io/users/dtolnay). -# 🚧 Advertencia +## 🚧 Advertencia **PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos hasta que se libere la versión **1.0.0**. -# 📜 Licencia +## 📜 Licencia El código está disponible bajo una doble licencia: diff --git a/helpers/pagetop-statics/README.md b/helpers/pagetop-statics/README.md index 99e24b4d..7466dc1f 100644 --- a/helpers/pagetop-statics/README.md +++ b/helpers/pagetop-statics/README.md @@ -11,19 +11,21 @@
-## Sobre PageTop +## 🧭 Sobre PageTop [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y configurables, basadas en HTML, CSS y JavaScript. -## Descripción general + +## 🗺️ Descripción general Este *crate* permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para servirlos de forma eficiente vía web, con detección de cambios que optimizan el tiempo de compilación. -## Créditos + +## 📚 Créditos Para ello, adapta el código de los *crates* [static-files](https://crates.io/crates/static_files) (versión [0.2.5](https://github.com/static-files-rs/static-files/tree/v0.2.5)) y @@ -35,14 +37,14 @@ Estas implementaciones se integran en PageTop para evitar que cada proyecto teng `static-files` manualmente como dependencia en su `Cargo.toml`. -# 🚧 Advertencia +## 🚧 Advertencia **PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos hasta que se libere la versión **1.0.0**. -# 📜 Licencia +## 📜 Licencia El código está disponible bajo una doble licencia: -- 2.47.2 From dd5cdb19cf4250a37ba47530937f4c2e09050c63 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 21 Dec 2025 09:47:35 +0100 Subject: [PATCH 34/47] =?UTF-8?q?=F0=9F=93=9D=20Actualiza=20las=20gu=C3=AD?= =?UTF-8?q?as=20de=20contribuci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 121 +++++++++------------------ MAINTAINERS.md | 211 ++++++++++++++++++------------------------------ 2 files changed, 113 insertions(+), 219 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2e2a263..e0e275a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,19 +15,10 @@ PageTop mantiene **un único repositorio oficial**: * **Repositorio oficial:** https://git.cillero.es/manuelcillero/pagetop * **Repositorio espejo:** https://github.com/manuelcillero/pagetop -El repositorio de GitHub actúa como espejo y punto de entrada para: - - * dar mayor visibilidad al proyecto, - * facilitar la participación de la comunidad, - * centralizar *issues* y *pull requests* externas. - > ⚠️ **Importante** -> Aunque GitHub permite abrir *pull requests*, **la integración del código se realiza únicamente en -> el repositorio oficial**. El repositorio de GitHub se sincroniza posteriormente para reflejar el -> mismo estado. - -En todos los casos, se respeta la **autoría original** de las contribuciones integradas, tanto en el -historial como en la documentación asociada al cambio. +> Aunque GitHub permite abrir *issues* y *pull requests*, **la integración del código se realiza +> únicamente en el repositorio oficial**. GitHub actúa como repositorio espejo que se sincroniza +> automáticamente para reflejar el mismo estado. ## 2. Issues (incidencias, propuestas, preguntas) @@ -50,8 +41,8 @@ Las *issues* se usan para: ### 3.1 Dónde abrirlas -Las *pull requests* se abren **en GitHub**, contra la rama `main`. GitHub es el punto de entrada -recomendado para contribuciones externas. +Las *pull requests* se abren **en GitHub**, normalmente contra la rama `main`. GitHub es el punto de +entrada recomendado para contribuciones externas. ### 3.2 Reglas generales para PRs @@ -63,68 +54,22 @@ recomendado para contribuciones externas. ### 3.3 Revisión y aceptación -Todas las PRs: +Todas las PRs son **revisadas manualmente** y pueden recibir comentarios o solicitudes de cambios. - * serán **revisadas manualmente**, - * pueden recibir comentarios o solicitudes de cambios, - * **no se integran directamente en GitHub**, ya que la integración se realiza en el repositorio - oficial para mantener coherencia y trazabilidad. - -Una PR aceptada: - - * se integra en el repositorio oficial (Forgejo), - * respetando **la autoría original del contribuidor**, - * normalmente mediante **squash merge** para mantener un historial limpio. +Las PRs aceptadas se integran en el repositorio oficial, nunca directamente en GitHub, preservando +siempre la **autoría original** del contribuidor. -## 4. Autoría y atribución +### 3.4. Cierre de Pull Requests y sincronización -PageTop cuida especialmente la atribución de contribuciones. +Una vez que el cambio ha sido integrado en el repositorio oficial: - * El **autor original del código se conserva** en el commit final integrado en Forgejo. - * Aunque el autor no tenga cuenta en Forgejo, su nombre y email quedarán reflejados. - * En GitHub, cuando es posible, la contribución quedará asociada al usuario original. - -Adicionalmente, el mensaje del commit puede incluir líneas `Co-authored-by` cuando proceda. + * La PR en GitHub se **cierra manualmente**. + * Se añade un **mensaje estándar de cierre** indicando que el cambio ha sido integrado. + * El repositorio de GitHub **se sincroniza automáticamente** como espejo. -## 5. Cierre de Pull Requests en GitHub - -Una vez que el cambio ha sido integrado en Forgejo: - - * La PR en GitHub se **cerrará manualmente** (no se mergea). - * Se añadirá un **mensaje estándar de cierre**, indicando: - * que el cambio ha sido integrado, - * la referencia al commit o versión, - * que GitHub es un repositorio espejo. - -Ejemplo de mensaje de cierre: - -> Este cambio ha sido integrado en el repositorio oficial (Forgejo). -> GitHub actúa como repositorio espejo, por lo que la PR se cierra sin merge. -> Gracias por tu contribución. - -Esto garantiza: - - * transparencia, - * trazabilidad, - * coherencia entre repositorios. - - -## 6. Sincronización entre Forgejo y GitHub - -Tras integrar cambios en Forgejo: - - * el repositorio de GitHub se **actualiza para reflejar el estado de Forgejo**, - * el historial de GitHub puede reescribirse para mantener coherencia. - -Por este motivo: - - * **no se deben hacer merges “definitivos” en GitHub**, - * GitHub no debe considerarse fuente de verdad del historial. - - -## 7. Estilo de código y calidad +## 4. Estilo de código y calidad * Sigue el estilo existente del proyecto. * Mantén los comentarios claros y precisos. @@ -132,18 +77,28 @@ Por este motivo: * Cambios públicos o estructurales deben ir acompañados de documentación. -## 8. Commits +## 5. Commits -Recomendaciones generales: +PageTop usa la especificación **gitmoji** para los mensajes de *commit*. El formato recomendado es: - * Mensajes claros y descriptivos. - * Un commit debe representar una unidad lógica de cambio. - * En contribuciones externas, el formato exacto del commit puede ajustarse durante la integración. + ` (ámbito opcional): ` -Durante la integración, los commits pueden ajustarse (rebase, squash o edición de mensajes) para -adaptarse al historial del proyecto. +Ejemplos: -## 9. Comunicación y respeto + * 📝 Actualiza la guía de contribución + * ✨ (locale): Refactoriza sistema de localización + * ♻️ (bootsier): Simplifica asignación de clases + +El emoji puede usarse en formato Unicode o como *shortcode*, por ejemplo `:sparkles:` en vez de ✨. + +Consulta la especificación oficial en https://gitmoji.dev/specification + +Durante la integración, los *commits* pueden ajustarse para adaptarse al historial del proyecto. + +Un *commit* debe representar una unidad lógica de cambio. Usa mensajes claros y descriptivos. + + +## 6. Comunicación y respeto PageTop sigue un enfoque profesional y colaborativo: @@ -151,13 +106,9 @@ PageTop sigue un enfoque profesional y colaborativo: * Acepta sugerencias técnicas como parte del proceso. * Recuerda que todas las contribuciones son revisadas con el objetivo de mejorar el proyecto. +Si tienes dudas sobre el proceso, abre una *issue* de tipo pregunta para tratar la cuestión en +comunidad. -## 10. Dudas +--- -Si tienes dudas sobre el proceso: - - * abre una *issue* de tipo pregunta, - * o inicia una discusión (si está habilitada). - -Gracias por contribuir a **PageTop** 🚀 Cada aportación, grande o pequeña, ayuda a que el proyecto -mejore. +Gracias por contribuir a **PageTop** 🚀 Cada aportación contribuye a mejorar el proyecto. diff --git a/MAINTAINERS.md b/MAINTAINERS.md index fbf69db3..25559841 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -10,204 +10,147 @@ usuarios externos**. Su objetivo es servir como **referencia operativa**, garant trazabilidad y preservación de la autoría en un entorno con repositorios espejo. -## 1. Repositorios y roles +## 1. Repositorios y principios PageTop mantiene **un único repositorio oficial**: * **Repositorio oficial:** https://git.cillero.es/manuelcillero/pagetop * **Repositorio espejo:** https://github.com/manuelcillero/pagetop -El repositorio de GitHub actúa como espejo y punto de entrada para: - - * dar mayor visibilidad al proyecto, - * facilitar la participación de la comunidad, - * centralizar *issues* y *pull requests* externas. - ### Principios clave * El repositorio oficial **es la única fuente de verdad** del historial. * **Nunca se realizan *merges* en GitHub**. + * Toda integración definitiva se realiza en el repositorio oficial. + * La autoría original debe preservarse siempre. ## 2. Configuración local recomendada -Configuración típica de *remotes*: +El remoto `github` debe configurarse únicamente para operaciones de lectura (*fetch*), con la URL de +*push* deshabilitada para evitar publicaciones accidentales en el repositorio espejo. -```bash -git remote -v -``` - -Ejemplo esperado: +Estado esperado de `git remote -v`: ```text origin git@git.cillero.es:manuelcillero/pagetop.git (fetch) origin git@git.cillero.es:manuelcillero/pagetop.git (push) -github git@github.com:manuelcillero/pagetop.git (fetch) -github git@github.com:manuelcillero/pagetop.git (push) +github git@github.com:manuelcillero/pagetop.git (fetch) +github DISABLED (push) ``` Convenciones usadas en este documento: -* `origin` -> Oficial -* `github` -> GitHub (espejo) + * `origin` -> Repositorio oficial + * `github` -> Repositorio espejo -## 3. Recepción de Pull Requests desde GitHub +## 3. Recepción y revisión de Pull Requests -Las contribuciones externas llegan como *pull requests* en GitHub, normalmente contra `main`. +Las PRs externas llegan por GitHub, normalmente contra la rama `main`. -### 3.1 Obtener la PR en local - -Opción habitual (ejemplo con PR #123): +Se asume que el repositorio local está configurado para recuperar PRs de GitHub como referencias +remotas (`refs/pull//head`): ```bash -git fetch github pull/123/head:pr-123 -git checkout pr-123 +git fetch github --prune +git checkout -b pr-123 github/pr/123 ``` -Alternativamente, si la rama del contribuidor es accesible directamente como referencia remota: +Antes de integrar: -```bash -git fetch github -git checkout nombre-de-la-rama -``` + * Revisar el código manualmente. + * Verificar formato, análisis y pruebas: + + ```bash + cargo fmt + cargo clippy + cargo test + ``` + + * Comprobar impacto en documentación. + * Evaluar coherencia con la arquitectura y el estilo del proyecto. + +Los cambios adicionales se solicitan o se aplican explicando claramente el motivo. -## 4. Revisión local +## 4. Estrategia de integración -Antes de integrar cualquier cambio: +La integración **se realiza siempre en el repositorio oficial** (`origin`). -* Revisar el código manualmente. -* Verificar compilación y pruebas: +### 4.1 Estrategia por defecto: *rebase* + *fast-forward* -```bash -cargo build -cargo test -``` +Esta es la **estrategia estándar y recomendada** en PageTop. Ventajas: -* Comprobar impacto en documentación. -* Evaluar coherencia con la arquitectura y el estilo del proyecto. - -Si se requieren cambios: - -* comentar en la PR, -* solicitar ajustes, -* o realizar modificaciones locales explicadas claramente. - - -## 5. Estrategia de integración - -La integración **se realiza siempre en el repositorio oficial**. - -### 5.1 Estrategia por defecto: *squash merge* - -Usada cuando: - -* la PR tiene varios commits intermedios, -* los commits no siguen el estilo del proyecto, -* se desea un historial compacto. + * conserva los commits originales, + * preserva la autoría real de cada cambio, + * mantiene un historial lineal y trazable, + * facilita auditoría y depuración. Procedimiento típico: +```bash +git checkout pr-123 +git rebase main + +# Resolver conflictos si los hay + +git checkout main +git merge --ff-only pr-123 +``` + +Si `merge --ff-only` falla, **no se debe continuar**, indica divergencias que deben resolverse antes +de integrar la PR. + +### 4.2 Estrategia excepcional: *Squash* + +Sólo debe usarse cuando esté justificado: + + * la PR contiene múltiples commits de prueba o ruido, + * el historial aportado no es significativo, + * el cambio es pequeño y autocontenido. + +En este caso, se debe **preservar explícitamente la autoría**: + ```bash git checkout main -git pull origin main git merge --squash pr-123 -``` - -Crear el commit final **preservando la autoría** (ver sección 6). - -### 5.2 Cherry-pick selectivo - -Usado cuando: - -* uno o varios commits son claros y autocontenidos, -* interesa conservar referencias explícitas. - -Ejemplo: - -```bash -git checkout main -git pull origin main -git cherry-pick -x -``` - - -## 6. Preservación de la autoría - -La autoría original **debe conservarse siempre**. - -### 6.1 Commit con autor explícito - -Ejemplo: - -```bash git commit --author="Nombre Apellido " ``` -El mantenedor figura como *committer*; el contribuidor como *author*. -### 6.2 Co-authored-by - -Cuando procede, puede añadirse al mensaje del commit: - -```text -Co-authored-by: Nombre Apellido -``` - - -## 7. Push al repositorio oficial - -Una vez integrado: +### 4.3. Publicación en el repositorio oficial ```bash git push origin main ``` -Este push representa **la integración definitiva**. +Este *push* representa la **integración definitiva** del cambio en la rama `main`. -## 8. Cierre de la Pull Request en GitHub +## 5. Cierre de la PR y sincronización -Tras integrar el cambio en el repositorio oficial: - -* **No se mergea la PR en GitHub**. -* Se cierra manualmente con un mensaje estándar. - -Ejemplo recomendado: +Tras integrar el cambio en el repositorio oficial, se cierra manualmente la PR en GitHub con un +mensaje estándar: ```text -Este cambio ha sido integrado en el repositorio oficial. -GitHub actúa como repositorio espejo, por lo que la PR se cierra sin merge. Gracias por tu contribución. + +Este cambio ha sido integrado en el repositorio oficial en `main` (``). +GitHub actúa como repositorio espejo sincronizado. ``` -## 9. Sincronización del repositorio oficial a GitHub +## 6. Principios de mantenimiento -El repositorio de GitHub se mantiene como **espejo automático** del repositorio oficial -mediante un **push mirror configurado**. - -No se realizan sincronizaciones manuales desde clones locales. - -### Consideraciones - - * El repositorio oficial es siempre la **fuente de verdad**. - * El historial de GitHub puede **reescribirse automáticamente** para reflejar el estado del - repositorio oficial. - * Todas las ramas que deban preservarse en GitHub **deben existir también en el repositorio - oficial**. - * GitHub no debe usarse como referencia del historial real. - - -## 10. Principios de mantenimiento - -* Priorizar **claridad y trazabilidad** frente a rapidez. -* Mantener un historial legible y significativo. -* Documentar cambios estructurales o públicos. -* Tratar las contribuciones externas con respeto y transparencia. + * Priorizar **claridad y trazabilidad** frente a rapidez. + * Mantener un historial legible y significativo. + * Documentar cambios estructurales o públicos. + * Tratar las contribuciones externas con respeto y transparencia. --- Este documento puede evolucionar con el proyecto. -Su objetivo no es imponer rigidez, sino **capturar el conocimiento operativo real** de PageTop. + +No se trata de imponer rigidez, sino de **capturar el conocimiento operativo real** de PageTop como +guía práctica para el mantenimiento. -- 2.47.2 From 47c47ba9a00ddea2717e8b88be11babe524438f7 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 21 Dec 2025 15:20:53 +0100 Subject: [PATCH 35/47] =?UTF-8?q?=F0=9F=93=9D=20Actualiza=20el=20formato?= =?UTF-8?q?=20de=20mensajes=20commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0e275a4..48fe96bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,13 +81,29 @@ Una vez que el cambio ha sido integrado en el repositorio oficial: PageTop usa la especificación **gitmoji** para los mensajes de *commit*. El formato recomendado es: - ` (ámbito opcional): ` + ```text + [(ámbito opcional):] + «LÍNEA EN BLANCO» + Cuerpo opcional + «LÍNEA EN BLANCO» + Nota(s) al pie opcional(es) para referencias, incidencias o cambios incompatibles + ``` -Ejemplos: +Ejemplos (no más de 50 caracteres en la primera línea, y no más de 80 en el resto): - * 📝 Actualiza la guía de contribución - * ✨ (locale): Refactoriza sistema de localización - * ♻️ (bootsier): Simplifica asignación de clases + * `📝 Actualiza la guía de contribución` + * `♻️ (locale): Refactoriza sistema de localización` + * Un mensaje completo: + ``` + 🎨 (bootsier): Mejora la asignación de clases + + - Simplifica la generación de clases CSS para componentes Bootstrap. + - Elimina duplicidades en enums de estilos y centraliza la lógica de composición + para reducir errores y facilitar mantenimiento. + - Alinea los nombres de variantes con la documentación pública. + + Refs: PR #123 + ``` El emoji puede usarse en formato Unicode o como *shortcode*, por ejemplo `:sparkles:` en vez de ✨. -- 2.47.2 From 25d32ec5dec49d361157e22869cb1f17d913728a Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 28 Dec 2025 13:18:18 +0100 Subject: [PATCH 36/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Cambia=20`Self::defa?= =?UTF-8?q?ult()`=20por=20`Tipo::default()`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/theme/container/component.rs | 12 +++++------ .../src/theme/dropdown/component.rs | 2 +- .../src/theme/dropdown/item.rs | 20 +++++++++---------- extensions/pagetop-bootsier/src/theme/icon.rs | 10 +++++----- .../src/theme/image/component.rs | 4 ++-- .../src/theme/nav/component.rs | 8 ++++---- .../pagetop-bootsier/src/theme/nav/item.rs | 16 +++++++-------- .../src/theme/navbar/brand.rs | 2 +- .../src/theme/navbar/component.rs | 18 ++++++++--------- .../pagetop-bootsier/src/theme/navbar/item.rs | 2 +- .../src/theme/offcanvas/component.rs | 2 +- src/base/component/block.rs | 2 +- src/base/component/html.rs | 5 +++-- src/base/component/intro.rs | 2 +- src/core/action/list.rs | 2 +- src/core/component/children.rs | 4 ++-- src/html/assets/favicon.rs | 2 +- src/html/assets/javascript.rs | 12 +++++------ src/html/assets/stylesheet.rs | 4 ++-- src/html/attr_classes.rs | 2 +- src/locale/l10n.rs | 6 +++--- 21 files changed, 69 insertions(+), 68 deletions(-) diff --git a/extensions/pagetop-bootsier/src/theme/container/component.rs b/extensions/pagetop-bootsier/src/theme/container/component.rs index 101d847c..56868fb5 100644 --- a/extensions/pagetop-bootsier/src/theme/container/component.rs +++ b/extensions/pagetop-bootsier/src/theme/container/component.rs @@ -22,7 +22,7 @@ pub struct Container { impl Component for Container { fn new() -> Self { - Container::default() + Self::default() } fn id(&self) -> Option { @@ -82,7 +82,7 @@ impl Component for Container { impl Container { /// Crea un contenedor de tipo `Main` (`
`). pub fn main() -> Self { - Container { + Self { container_kind: container::Kind::Main, ..Default::default() } @@ -90,7 +90,7 @@ impl Container { /// Crea un contenedor de tipo `Header` (`
`). pub fn header() -> Self { - Container { + Self { container_kind: container::Kind::Header, ..Default::default() } @@ -98,7 +98,7 @@ impl Container { /// Crea un contenedor de tipo `Footer` (`