diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index 5c9ab44f..fcc6e84c 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -102,7 +102,7 @@ impl Extension for SuperMenu { )), )); - InRegion::Global(&CoreRegions::Header).add( + InRegion::Global(&CoreRegion::Header).add( bs::Container::new() .with_width(bs::container::Width::FluidMax(UnitValue::RelRem(75.0))) .with_child(navbar_menu), diff --git a/extensions/pagetop-bootsier/src/theme.rs b/extensions/pagetop-bootsier/src/theme.rs index aa9a3ed8..22e55792 100644 --- a/extensions/pagetop-bootsier/src/theme.rs +++ b/extensions/pagetop-bootsier/src/theme.rs @@ -1,22 +1,38 @@ -//! Definiciones y plantillas del tema Bootsier. +//! Definiciones y componentes del tema. +//! +//! En esta página, el apartado **Modules** incluye las definiciones necesarias para los componentes +//! que se muestran en el apartado **Structs**, mientras que en **Enums** se listan los elementos +//! auxiliares del tema utilizados en clases y componentes. -pub mod bs; +mod attrs; +pub use attrs::*; -pub mod class; +pub mod classes; -mod token; -pub use token::*; +// Button. +mod button; +pub use button::{Button, ButtonAction}; +// Container. +pub mod container; +#[doc(inline)] +pub use container::Container; + +// Dropdown. +pub mod dropdown; +#[doc(inline)] +pub use dropdown::Dropdown; + +// Form. +pub mod form; +#[doc(inline)] +pub use form::Form; #[doc(hidden)] -pub use bs::badge::BadgeBootsier; +pub use form::input::InputBootsier; #[doc(hidden)] -pub use bs::container::ContainerBootsier; +pub use form::select::SelectBootsier; #[doc(hidden)] -pub use bs::form::input::InputBootsier; -#[doc(hidden)] -pub use bs::form::select::SelectBootsier; -#[doc(hidden)] -pub use bs::form::textarea::TextareaBootsier; +pub use form::textarea::TextareaBootsier; // Image. pub mod image; diff --git a/extensions/pagetop-bootsier/src/theme/bs.rs b/extensions/pagetop-bootsier/src/theme/bs.rs index 34270f08..ae1657fd 100644 --- a/extensions/pagetop-bootsier/src/theme/bs.rs +++ b/extensions/pagetop-bootsier/src/theme/bs.rs @@ -1,9 +1,5 @@ //! Componentes proporcionados por el tema. -// Badge. -pub(crate) mod badge; -pub use badge::{Badge, BadgeBootsier}; - // Button. mod button; pub use button::{Button, ButtonAction}; @@ -24,12 +20,6 @@ pub use dropdown::Dropdown; pub mod form; #[doc(inline)] pub use form::Form; -#[doc(inline)] -pub use form::input::InputBootsier; -#[doc(inline)] -pub use form::select::SelectBootsier; -#[doc(inline)] -pub use form::textarea::TextareaBootsier; // Image. pub mod image; diff --git a/extensions/pagetop-bootsier/src/theme/bs/badge.rs b/extensions/pagetop-bootsier/src/theme/bs/badge.rs deleted file mode 100644 index 95f4ef50..00000000 --- a/extensions/pagetop-bootsier/src/theme/bs/badge.rs +++ /dev/null @@ -1,47 +0,0 @@ -use pagetop::prelude::*; - -use crate::theme::*; - -pub use pagetop::base::component::Badge; - -const EXTRA_TEXT_BG: &str = "bootsier.badge.text_bg"; - -/// Extensión de Bootsier para [`Badge`]. -/// -/// Proporciona el método [`with_text_bg()`](Self::with_text_bg) para fijar el color de fondo del -/// badge usando un color de texto con contraste suficiente garantizado. -/// -/// ```rust,no_run -/// use pagetop::prelude::*; -/// use pagetop_bootsier::theme::*; -/// -/// let admin = bs::Badge::new() -/// .with_label(L10n::n("Admin")) -/// .with_text_bg(ThemeColor::Danger); -/// ``` -pub trait BadgeBootsier { - #[builder_fn] - fn with_text_bg(self, color: ThemeColor) -> Self; -} - -impl BadgeBootsier for Badge { - /// Establece el color de fondo usando un color de texto con contraste garantizado. - /// - /// Igual a `with_prop(PropsOp::add_classes(class::TextColor::Bg(color)))`, pero sin el riesgo - /// de acumular más de una clase `text-bg-{color}` si se llama varias veces. - #[builder_fn] - fn with_text_bg(mut self, color: ThemeColor) -> Self { - self.alter_prop(PropsOp::set_extra(EXTRA_TEXT_BG, color)); - self - } -} - -// **< Badge SETUP >******************************************************************************** - -pub(crate) fn setup(badge: &mut Badge) { - let color = badge.props().extra_or(EXTRA_TEXT_BG, ThemeColor::Secondary); - badge.alter_prop(PropsOp::replace_classes( - "badge", - util::join!("badge ", class::TextColor::Bg(color).to_class()), - )); -} diff --git a/extensions/pagetop-bootsier/src/theme/bs/container.rs b/extensions/pagetop-bootsier/src/theme/bs/container.rs index 891888cb..e780c9a3 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/container.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/container.rs @@ -10,12 +10,10 @@ const EXTRA_WIDTH: &str = "bootsier.container.width"; /// Extensión de Bootsier para [`Container`]. /// -/// Permite establecer el comportamiento del ancho del contenedor usando el método -/// [`with_width()`](Self::with_width). +/// Permite definir el comportamiento del ancho del contenedor usando el método +/// [`with_width()`](Self::with_width). También acepta clases predefinidas para: /// -/// También habilita al componente para aceptar clases predefinidas para: -/// -/// - Modificar el color de fondo ([`Bg`](crate::theme::class::Bg)). +/// - Modificar el color de fondo ([`Background`](crate::theme::class::Background)). /// - Definir la apariencia del texto ([`Text`](crate::theme::class::Text)). /// - Establecer bordes ([`Border`](crate::theme::class::Border)). /// - Redondear las esquinas ([`Rounded`](crate::theme::class::Rounded)). @@ -26,11 +24,11 @@ const EXTRA_WIDTH: &str = "bootsier.container.width"; /// /// let main = bs::Container::main() /// .with_id("main-page") -/// .with_width(bs::container::Width::From(BreakPoint::LG)) -/// .with_prop(PropsOp::add_classes(class::Bg::with(ThemeColor::Light))) -/// .with_prop(PropsOp::add_classes(class::Text::with(ThemeColor::Dark))) -/// .with_prop(PropsOp::add_classes(class::Border::with(ScaleSize::One))) -/// .with_prop(PropsOp::add_classes(class::Rounded::new())); +/// .with_width(bs::container::Width::From(token::BreakPoint::LG)) +/// .with_prop(PropsOp::add_classes(class::Background::with(token::Color::Light))) +/// .with_prop(PropsOp::add_classes(class::Text::with(token::Color::Dark))) +/// .with_prop(PropsOp::add_classes(class::Border::with(token::ScaleSize::One))) +/// .with_prop(PropsOp::add_classes(class::Rounded::with(token::RoundedRadius::Default))); /// ``` pub trait ContainerBootsier { /// Establece el comportamiento del ancho para el contenedor. @@ -38,15 +36,12 @@ pub trait ContainerBootsier { /// Determina si el contenedor aplica los anchos máximos predefinidos para cada punto de /// ruptura, o si ocupa siempre el 100% del ancho disponible, o lo hace hasta un ancho máximo /// explícito. Ver [`Width`] para las variantes disponibles. - #[builder_fn] fn with_width(self, width: Width) -> Self; } impl ContainerBootsier for Container { - #[builder_fn] - fn with_width(mut self, width: Width) -> Self { - self.alter_prop(PropsOp::set_extra(EXTRA_WIDTH, width)); - self + fn with_width(self, width: Width) -> Self { + self.with_prop(PropsOp::set_extra(EXTRA_WIDTH, width)) } } @@ -61,7 +56,7 @@ pub enum Width { Default, /// Aplica los anchos máximos predefinidos a partir del punto de ruptura indicado. Por debajo de /// ese punto de ruptura ocupa el 100% del ancho disponible. - From(BreakPoint), + From(token::BreakPoint), /// Ocupa el 100% del ancho disponible siempre. Fluid, /// Ocupa el 100% del ancho disponible hasta un ancho máximo explícito. @@ -75,10 +70,10 @@ impl Width { #[inline] pub fn push_to(self, classes: &mut String) { match self { - Self::Default => BreakPoint::None.push_to(classes, Self::CONTAINER, ""), + Self::Default => token::BreakPoint::None.push_to(classes, Self::CONTAINER, ""), Self::From(bp) => bp.push_to(classes, Self::CONTAINER, ""), Self::Fluid | Self::FluidMax(_) => { - BreakPoint::None.push_to(classes, Self::CONTAINER, "fluid") + token::BreakPoint::None.push_to(classes, Self::CONTAINER, "fluid") } } } diff --git a/extensions/pagetop-bootsier/src/theme/bs/form/input.rs b/extensions/pagetop-bootsier/src/theme/bs/form/input.rs index 6c7261f6..4c85a1ce 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/form/input.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/form/input.rs @@ -28,15 +28,12 @@ pub trait InputBootsier { /// Cuando está activo, la etiqueta se superpone al campo y asciende al enfocarlo o cuando tiene /// contenido. Requiere que el campo tenga un atributo `placeholder` definido; si no se /// especifica, se fuerza `placeholder=""` antes del renderizado. - #[builder_fn] fn with_floating_label(self, floating: bool) -> Self; } impl InputBootsier for Field { - #[builder_fn] - fn with_floating_label(mut self, floating: bool) -> Self { - self.alter_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating)); - self + fn with_floating_label(self, floating: bool) -> Self { + self.with_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating)) } } @@ -55,22 +52,14 @@ pub(crate) fn setup(field: &mut Field) { pub(crate) fn render(field: &Field, cx: &mut Context) -> Result { let container_id = field.id(); let input_id = container_id.as_deref().map(|id| util::join!(id, "-input")); + let floating = field.props().extra_or(EXTRA_FLOATING_LABEL, false); let input_class = if *field.plaintext() { "form-control-plaintext" } else { "form-control" }; - let strict = field.kind().is_strict(); - let masked = *field.kind() == Kind::StrictPassword; - let autocomplete = if strict { - Some(form::Autocomplete::Off) - } else { - field.autocomplete().get() - }; - - // La etiqueta flotante requiere `placeholder` para animar la etiqueta. - let floating = field.props().extra_or(EXTRA_FLOATING_LABEL, false); - // Si no está definido, se fuerza `placeholder=""`. + // La etiqueta flotante requiere `placeholder` para animar la etiqueta; si no está definido, se + // fuerza `placeholder=""`. let placeholder = if floating { Some(field.placeholder().lookup(cx).unwrap_or_default()) } else { @@ -92,7 +81,6 @@ pub(crate) fn render(field: &Field, cx: &mut Context) -> Result html! {}, }; - Ok(html! { div (field.props()) { @if !floating { (label) } @@ -106,13 +94,9 @@ pub(crate) fn render(field: &Field, cx: &mut Context) -> Result Self; } impl SelectBootsier for Field { - #[builder_fn] - fn with_floating_label(mut self, floating: bool) -> Self { - self.alter_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating)); - self + fn with_floating_label(self, floating: bool) -> Self { + self.with_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating)) } } diff --git a/extensions/pagetop-bootsier/src/theme/bs/form/textarea.rs b/extensions/pagetop-bootsier/src/theme/bs/form/textarea.rs index 0238d832..04892cfb 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/form/textarea.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/form/textarea.rs @@ -31,15 +31,12 @@ pub trait TextareaBootsier { /// /// Si se usa la etiqueta flotante, se anula el valor establecido con /// [`with_rows()`](form::Textarea::with_rows) antes del renderizado. - #[builder_fn] fn with_floating_label(self, floating: bool) -> Self; } impl TextareaBootsier for Textarea { - #[builder_fn] - fn with_floating_label(mut self, floating: bool) -> Self { - self.alter_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating)); - self + fn with_floating_label(self, floating: bool) -> Self { + self.with_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating)) } } diff --git a/extensions/pagetop-bootsier/src/theme/class/rounded.rs b/extensions/pagetop-bootsier/src/theme/class/rounded.rs index 426763d3..4bd497eb 100644 --- a/extensions/pagetop-bootsier/src/theme/class/rounded.rs +++ b/extensions/pagetop-bootsier/src/theme/class/rounded.rs @@ -104,48 +104,32 @@ impl Into for RoundedRadius { /// - Ajustar el radio de las **esquinas concretas** (`top-start`, `top-end`, `bottom-start`, /// `bottom-end`, **en este orden**, respetando LTR/RTL). /// -/// # Comportamiento aditivo / sustractivo -/// -/// - **Aditivo**: se parte de [`Rounded::default()`] (sin redondeo) y se van añadiendo lados o -/// esquinas concretas con el radio deseado. -/// -/// - **Sustractivo**: se parte de [`Rounded::new()`] o [`Rounded::with`] (radio global ya aplicado) -/// y se anulan lados o esquinas concretas con [`RoundedRadius::Zero`]. -/// /// # Ejemplos /// /// ```rust /// use pagetop_bootsier::theme::*; /// -/// // Radio global por defecto, equivalente a `Rounded::with(RoundedRadius::Default)`: -/// let r = class::Rounded::new(); +/// // Radio global: +/// let r = class::Rounded::with(class::RoundedRadius::Default); /// assert_eq!(r.to_class(), "rounded"); /// -/// // Radio global explícito: -/// let r = class::Rounded::with(class::RoundedRadius::Scale3); -/// assert_eq!(r.to_class(), "rounded-3"); -/// -/// // Sin redondeo (comportamiento de `Default`): -/// let r = class::Rounded::default(); +/// // Sin redondeo: +/// let r = class::Rounded::new(); /// assert_eq!(r.to_class(), ""); /// -/// // Aditivo (radio en las esquinas de un lado lógico): -/// let r = class::Rounded::default().with_end(class::RoundedRadius::Scale2); +/// // Radio en las esquinas de un lado lógico: +/// let r = class::Rounded::new().with_end(class::RoundedRadius::Scale2); /// assert_eq!(r.to_class(), "rounded-end-2"); /// -/// // Aditivo (radio en una esquina concreta): -/// let r = class::Rounded::default().with_top_start(class::RoundedRadius::Scale3); +/// // Radio en una esquina concreta: +/// let r = class::Rounded::new().with_top_start(class::RoundedRadius::Scale3); /// assert_eq!(r.to_class(), "rounded-top-start-3"); /// -/// // Sustractivo (radio global menos la esquina superior-inicial): -/// let r = class::Rounded::new().with_top_start(class::RoundedRadius::Zero); -/// assert_eq!(r.to_class(), "rounded rounded-top-start-0"); -/// /// // Combinado (ejemplo completo): -/// let r = class::Rounded::default() +/// let r = class::Rounded::new() /// .with_top(class::RoundedRadius::Default) // Añade redondeo arriba. -/// .with_bottom_start(class::RoundedRadius::Scale4) // Añade esquina redondeada concreta. -/// .with_bottom_end(class::RoundedRadius::Circle); // Añade redondeo máximo en otra esquina. +/// .with_bottom_start(class::RoundedRadius::Scale4) // Añade una esquina redondeada concreta. +/// .with_bottom_end(class::RoundedRadius::Circle); // Añade redondeo extremo en otra esquina. /// assert_eq!(r.to_class(), "rounded-top rounded-bottom-start-4 rounded-bottom-end-circle"); /// ``` #[rustfmt::skip] @@ -163,13 +147,12 @@ pub struct Rounded { } impl Rounded { - /// Prepara las esquinas con el **radio de redondeo por defecto** (`rounded`), como - /// [`Self::with`] con [`RoundedRadius::Default`]. + /// Prepara las esquinas **sin redondeo global** de partida. pub fn new() -> Self { - Self::default().with_radius(RoundedRadius::Default) + Self::default() } - /// Crea las esquinas con un **radio global** explícito (`radius`). + /// Crea las esquinas **con redondeo global** (`radius`). pub fn with(radius: RoundedRadius) -> Self { Self::default().with_radius(radius) } diff --git a/src/base/component.rs b/src/base/component.rs index 5a5dc8d1..fe3c734c 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -2,9 +2,6 @@ pub mod layout; -mod badge; -pub use badge::Badge; - mod block; pub use block::Block; diff --git a/src/base/component/badge.rs b/src/base/component/badge.rs deleted file mode 100644 index eb4bf173..00000000 --- a/src/base/component/badge.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::prelude::*; - -/// Componente para mostrar una **etiqueta corta informativa** (*badge*). -/// -/// # Ejemplo -/// -/// ```rust,no_run -/// use pagetop::prelude::*; -/// -/// let badge = Badge::new().with_label(L10n::n("Admin")); -/// ``` -#[derive(AutoDefault, Clone, Debug, Getters)] -pub struct Badge { - /// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente. - props: Props, - /// Devuelve la etiqueta del badge. - label: L10n, -} - -#[async_trait] -impl Component for Badge { - fn new() -> Self { - Self::default() - } - - fn id(&self) -> Option { - self.props.get_id() - } - - fn setup(&mut self, _cx: &Context) { - self.alter_prop(PropsOp::prepend_classes("badge")); - } - - async fn prepare(&self, cx: &mut Context) -> Result { - Ok(html! { - span (self.props()) { - (self.label().using(cx)) - } - }) - } -} - -impl Badge { - // **< Badge BUILDER >************************************************************************** - - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. - #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); - self - } - - /// Modifica identificador, clases CSS o atributos HTML del componente. - #[builder_fn] - pub fn with_prop(mut self, op: PropsOp) -> Self { - self.props.alter_prop(op); - self - } - - /// Establece la etiqueta del badge. - #[builder_fn] - pub fn with_label(mut self, label: L10n) -> Self { - self.label = label; - self - } -} diff --git a/src/base/component/form/check.rs b/src/base/component/form/check.rs index 03e9249d..d7718044 100644 --- a/src/base/component/form/check.rs +++ b/src/base/component/form/check.rs @@ -9,6 +9,10 @@ use crate::prelude::*; /// Representa cada casilla de un grupo de casillas de verificación, con una etiqueta localizable /// visible. Puede marcarse como seleccionada o deshabilitada de forma independiente al resto. /// +/// El parámetro `name` de [`form::check::Item::new()`](Item::new) se combina con el `name` del +/// grupo para componer el atributo `name` de la casilla. Por ejemplo, si el grupo tiene +/// `name=interests` y el ítem se crea con `name=tech`, la casilla tendrá `name=interests_tech`. +/// /// # Ejemplo /// /// ```rust,no_run @@ -18,8 +22,8 @@ use crate::prelude::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Item { - /// Devuelve el valor enviado al servidor cuando la casilla está marcada. - value: AttrValue, + /// Devuelve el nombre que se combina con el del grupo para componer el atributo `name`. + name: AttrValue, /// Devuelve la etiqueta de la casilla. label: L10n, /// Devuelve si la casilla debe aparecer marcada por defecto. @@ -29,10 +33,13 @@ pub struct Item { } impl Item { - /// Crea una nueva casilla con el valor y la etiqueta indicados. - pub fn new(value: impl AsRef, label: L10n) -> Self { + /// Crea una nueva casilla con el nombre y la etiqueta indicados. + /// + /// El parámetro `name` se combina con el del grupo para componer el atributo `name` de la + /// casilla. + pub fn new(name: impl AsRef, label: L10n) -> Self { Self { - value: AttrValue::new(value), + name: AttrValue::new(name), label, checked: false, disabled: false, @@ -58,10 +65,15 @@ impl Item { /// Componente para crear un **grupo de casillas de verificación**. /// -/// Renderiza una lista de opciones de la que el usuario puede marcar cero, una o varias. Todas las -/// casillas comparten el mismo `name` (el del grupo); cada una envía como valor el de su -/// [`form::check::Item`] cuando está marcada. Las opciones se añaden con [`with_item()`]. Si se -/// activa el modo en línea con [`with_inline()`], las casillas se disponen horizontalmente. +/// Renderiza un conjunto de casillas de verificación donde cada casilla puede marcarse de forma +/// independiente. Las casillas se añaden con [`with_item()`](Field::with_item) usando instancias +/// de [`form::check::Item`]. Si se activa el modo en línea con +/// [`with_inline()`](Field::with_inline), las casillas se disponen horizontalmente. +/// +/// El atributo `name` de cada casilla se construye automáticamente combinando el `name` del grupo +/// y el `name` del [`form::check::Item`] con un guion bajo. Por ejemplo, para el grupo con +/// `name=interests` y casillas con `name=art` y `name=tech`, se genera `name=interests_art` y +/// `name=interests_tech`. /// /// # Ejemplo /// @@ -76,29 +88,26 @@ impl Item { /// .with_item(form::check::Item::new("science", L10n::n("Science")).with_checked(true)); /// ``` /// -/// El navegador envía una entrada por cada casilla marcada, todas bajo la misma clave (por ejemplo, -/// si el usuario marca "Technology" y "Science", `interests=tech&interests=science`) y ninguna si -/// no marca ninguna. El servidor no necesita conocer de antemano qué opciones existían, lo que hace -/// de `Field` la opción adecuada también para listas de opciones dinámicas (por ejemplo, cargadas -/// de una base de datos). `axum::extract::Form` (basado en `serde_urlencoded`) no deserializa -/// claves repetidas en un `Vec`; hace falta un extractor que sí lo haga, como [`serde_qs`]: +/// Cada `name` debe ser único y válido como identificador de campo. Cuando el usuario marca una +/// casilla, el navegador envía algo como `interests_tech=true`; mientras que si no la marca, no +/// envía nada. En el servidor cada campo se deserializa como `bool` con `#[serde(default)]`: /// /// ```rust,ignore /// #[derive(serde::Deserialize)] /// struct FormData { /// #[serde(default)] -/// interests: Vec, // ["tech", "science"], o [] si no se marcó ninguna. +/// interests_art: bool, +/// #[serde(default)] +/// interests_tech: bool, +/// #[serde(default)] +/// interests_science: bool, /// } /// ``` -/// -/// [`with_item()`]: Field::with_item -/// [`with_inline()`]: Field::with_inline -/// [`serde_qs`]: https://docs.rs/serde_qs #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Field { /// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente. props: Props, - /// Devuelve el nombre compartido por todas las casillas del grupo. + /// Devuelve el nombre base compartido por todas las casillas del grupo. name: AttrName, /// Devuelve la etiqueta del grupo. label: Attr, @@ -155,13 +164,18 @@ impl Component for Field { @for (item, i) in self.items().iter().zip(1..) { @let i = i.to_string(); @let item_id = util::join!(&container_id, "-check-", &i); + @let item_name = if let Some(item_name) = item.name().get() { + util::join!(&name, "_", &item_name) + } else { + util::join!(&name, "_", &i) + }; div class=(item_classes) { input type="checkbox" id=(&item_id) class="form-check-input" - name=(&name) - value=[item.value().get()] + name=(&item_name) + value="true" checked[*item.checked()] disabled[*item.disabled() || *self.disabled()]; label class="form-check-label" for=(&item_id) { @@ -194,11 +208,14 @@ impl Field { self } - /// Establece el nombre compartido por todas las casillas del grupo. + /// Establece el nombre base para el grupo de casillas. /// - /// Todas las casillas [`form::check::Item`](Item) del grupo llevarán este mismo `name`. Si se - /// omite, se asigna un nombre generado automáticamente. Para deserializar los campos en el - /// servidor es recomendable establecer un `name` explícito. + /// Se combina con el `name` de cada [`form::check::Item`](Item) para generar el atributo `name` + /// de cada casilla de verificación. Por ejemplo, con `name=interests` en el grupo y `name=tech` + /// en el ítem, se genera `name=interests_tech`. + /// + /// Si se omite, se asigna un nombre generado automáticamente. Para deserializar los campos en + /// el servidor es recomendable establecer un `name` explícito. #[builder_fn] pub fn with_name(mut self, name: impl AsRef) -> Self { self.name.alter_name(name); diff --git a/src/base/component/form/component.rs b/src/base/component/form/component.rs index 8745f497..7d9ca342 100644 --- a/src/base/component/form/component.rs +++ b/src/base/component/form/component.rs @@ -46,8 +46,8 @@ use crate::base::component::form; pub struct Form { /// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente. props: Props, - /// Devuelve la ruta de destino del formulario. - action: Route, + /// Devuelve la URL/ruta de destino del formulario. + action: AttrValue, /// Devuelve el método para enviar el formulario. method: form::Method, /// Devuelve el juego de caracteres aceptado por el formulario. @@ -79,7 +79,7 @@ impl Component for Form { Ok(html! { form (self.props()) - action=[self.action().try_resolve(cx)] + action=[self.action().get()] method=[method] accept-charset=[self.charset().get()] { @@ -106,13 +106,10 @@ impl Form { self } - /// Establece la ruta de destino del formulario. - /// - /// Acepta un literal, un `String`, o una [`Route`] explícita construida con [`Route::with()`] - /// para rutas que dependan del contexto de renderizado. + /// Establece la URL/ruta de destino del formulario. #[builder_fn] - pub fn with_action(mut self, action: impl Into) -> Self { - self.action = action.into(); + pub fn with_action(mut self, action: impl AsRef) -> Self { + self.action.alter_str(action); self } diff --git a/src/base/component/form/input.rs b/src/base/component/form/input.rs index c8946085..565c055c 100644 --- a/src/base/component/form/input.rs +++ b/src/base/component/form/input.rs @@ -9,18 +9,9 @@ use std::fmt; /// Tipo de campo para un [`form::input::Field`]. /// /// Determina el tipo de entrada que acepta, así como el comportamiento del navegador al interactuar -/// con el campo. Implícitamente se aplica al crear el control usando [`text()`] o [`password()`], -/// [`strict_text()`] o [`strict_password()`], [`search()`], [`email()`], [`telephone()`] o -/// [`url()`]. -/// -/// [`text()`]: Field::text -/// [`password()`]: Field::password -/// [`strict_text()`]: Field::strict_text -/// [`strict_password()`]: Field::strict_password -/// [`search()`]: Field::search -/// [`email()`]: Field::email -/// [`telephone()`]: Field::telephone -/// [`url()`]: Field::url +/// con el campo. Implícitamente se aplica al crear el control: [`text()`](Field::text), +/// [`password()`](Field::password), [`search()`](Field::search), [`email()`](Field::email), +/// [`telephone()`](Field::telephone) o [`url()`](Field::url). #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum Kind { /// Entrada de texto genérico (`type="text"`). Es el tipo por defecto. @@ -28,14 +19,6 @@ pub enum Kind { Text, /// Entrada de una contraseña (`type="password"`). El contenido aparece enmascarado. Password, - /// Texto genérico blindado contra el autorrelleno del navegador (`type="text"`). - /// - /// Ver [`Field::strict_text()`]. - StrictText, - /// Contraseña blindada contra el autorrelleno del navegador (`type="text"`). - /// - /// Ver [`Field::strict_password()`]. - StrictPassword, /// Campo de búsqueda (`type="search"`). Es un tipo semántico para los cuadros de búsqueda. Search, /// Entrada de un correo electrónico (`type="email"`). Permite validar el formato del correo. @@ -46,20 +29,10 @@ pub enum Kind { Url, } -impl Kind { - /// Devuelve `true` si el tipo aplica el conjunto de medidas contra el autorrelleno del - /// navegador. - /// - /// Ver [`Field::strict_text()`] y [`Field::strict_password()`]. - pub fn is_strict(self) -> bool { - matches!(self, Kind::StrictText | Kind::StrictPassword) - } -} - impl fmt::Display for Kind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { - Kind::Text | Kind::StrictText | Kind::StrictPassword => "text", + Kind::Text => "text", Kind::Password => "password", Kind::Search => "search", Kind::Email => "email", @@ -121,8 +94,6 @@ impl fmt::Display for Mode { /// /// - [`Field::text()`]: campo de texto genérico (`type="text"`, por defecto). /// - [`Field::password()`]: contraseña (`type="password"`). -/// - [`Field::strict_text()`]: texto genérico blindado contra el autorrelleno del navegador. -/// - [`Field::strict_password()`]: contraseña blindada contra el autorrelleno del navegador. /// - [`Field::search()`]: búsqueda (`type="search"`). /// - [`Field::email()`]: correo electrónico (`type="email"`). /// - [`Field::telephone()`]: teléfono (`type="tel"`). @@ -205,9 +176,6 @@ impl Component for Field { } // Clases CSS del contenedor del campo de texto. - if self.kind().is_strict() { - self.alter_prop(PropsOp::prepend_classes("form-field-strict")); - } self.alter_prop(PropsOp::prepend_classes(util::join!( "form-field form-field-", self.kind().to_string() @@ -222,13 +190,6 @@ impl Component for Field { } else { "form-control" }; - let strict = self.kind().is_strict(); - let masked = *self.kind() == Kind::StrictPassword; - let autocomplete = if strict { - Some(form::Autocomplete::Off) - } else { - self.autocomplete().get() - }; Ok(html! { div (self.props()) { @@ -255,13 +216,9 @@ impl Component for Field { maxlength=[self.maxlength().get()] placeholder=[self.placeholder().lookup(cx)] inputmode=[self.inputmode().get()] - autocomplete=[autocomplete] - spellcheck=[strict.then_some("false")] - autocorrect=[strict.then_some("off")] - style=[masked.then_some("-webkit-text-security: disc; text-security: disc;")] + autocomplete=[self.autocomplete().get()] autofocus[*self.autofocus()] - readonly[*self.readonly() || *self.plaintext() || strict] - onfocus=[strict.then_some("this.removeAttribute('readonly')")] + readonly[*self.readonly() || *self.plaintext()] required[*self.required()] disabled[*self.disabled()]; @if let Some(description) = self.help_text().lookup(cx) { @@ -293,55 +250,6 @@ impl Field { } } - /// Crea un campo de **texto genérico blindado** contra el autorrelleno del navegador. - /// - /// Algunos navegadores rellenan los campos de texto con datos ya guardados (como usuario, - /// email, etc.) aplicando heurísticas por nombre o posición, incluso con `autocomplete="off"`. - /// Este método fuerza `autocomplete="off"`, desactiva corrección ortográfica y autocorrección, - /// y permanece de sólo lectura hasta que el usuario hace foco (ya que normalmente el navegador - /// respeta el `readonly` en la carga aunque ignore `autocomplete="off"`). - /// - /// Soporte por motor de navegador (Blink: Chrome, Edge, Opera; Gecko: Firefox; WebKit: Safari): - /// - /// | Medida | Blink | Gecko | WebKit | - /// |-------------------------------|------------|------------|--------------------------| - /// | `autocomplete="off"` ignorado | Sí | Sí | Lo respeta mejor | - /// | `spellcheck="false"` | Sí | Sí | Sí | - /// | `autocorrect="off"` | Sin efecto | Sin efecto | Sí (origen del atributo) | - /// - /// El truco `readonly` + `onfocus` compensa específicamente la agresividad de Chrome/Blink al - /// autorrellenar campos nada más cargar la página; su fiabilidad varía entre versiones y no - /// está documentada de forma oficial. Ninguna de estas medidas es infalible frente a gestores - /// de contraseñas de terceros, que usan heurísticas propias. - /// - /// Ver también [`Field::strict_password()`]. - pub fn strict_text() -> Self { - Self { - kind: Kind::StrictText, - ..Default::default() - } - } - - /// Crea un campo de **contraseña blindada** contra el autorrelleno del navegador. - /// - /// Aplica las mismas medidas que [`Field::strict_text()`] y, además, enmascara el contenido - /// visualmente vía CSS (usando `-webkit-text-security`) en lugar de usar `type="password"`. Al - /// no ser un campo de contraseña para el navegador, evita el gestor nativo de contraseñas, el - /// icono de mostrar/ocultar y el aviso de guardar la contraseña. - /// - /// Útil en formularios de acceso donde ese comportamiento no es deseable (como puestos - /// compartidos o paneles administrativos sensibles). - /// - /// La propiedad `-webkit-text-security` está soportada de forma nativa en Blink (Chrome, Edge, - /// Opera) y en WebKit (Safari, su origen); Firefox lo soporta desde la versión 114 (2023) con - /// el mismo nombre. - pub fn strict_password() -> Self { - Self { - kind: Kind::StrictPassword, - ..Default::default() - } - } - /// Crea un campo de **búsqueda** (`type="search"`). /// /// Semánticamente equivalente a `text` pero optimizado para búsquedas: algunos navegadores diff --git a/src/base/component/layout/region.rs b/src/base/component/layout/region.rs index cd9d4c53..72c6cc96 100644 --- a/src/base/component/layout/region.rs +++ b/src/base/component/layout/region.rs @@ -55,7 +55,7 @@ impl fmt::Debug for Region { impl Default for Region { fn default() -> Self { Region { - region: &CoreRegions::Content, + region: &CoreRegion::Content, } } } @@ -90,33 +90,17 @@ impl Component for Region { } impl Region { - /// Define el componente que renderizará [`CoreRegions::Header`]. + /// Define el componente que renderizará [`CoreRegion::Header`]. pub fn header() -> Self { Region { - region: &CoreRegions::Header, + region: &CoreRegion::Header, } } - /// Define el componente que renderizará [`CoreRegions::Aside`]. - pub fn aside() -> Self { - Region { - region: &CoreRegions::Aside, - } - } - - /// Define el componente que renderizará [`CoreRegions::Content`]. - /// - /// Equivale a [`Self::new()`] o [`Self::default()`], que ya usan esta región por defecto. - pub fn content() -> Self { - Region { - region: &CoreRegions::Content, - } - } - - /// Define el componente que renderizará [`CoreRegions::Footer`]. + /// Define el componente que renderizará [`CoreRegion::Footer`]. pub fn footer() -> Self { Region { - region: &CoreRegions::Footer, + region: &CoreRegion::Footer, } } diff --git a/src/base/component/layout/template.rs b/src/base/component/layout/template.rs index 66c21beb..54fd3f64 100644 --- a/src/base/component/layout/template.rs +++ b/src/base/component/layout/template.rs @@ -4,32 +4,25 @@ use std::fmt; /// Componente que renderiza el cuerpo de una plantilla de regiones. /// -/// La composición por defecto usa el componente [`Region`] para mostrar, en este orden, las -/// regiones [`CoreRegions::Header`], [`CoreRegions::Aside`], [`CoreRegions::Content`] y -/// [`CoreRegions::Footer`], envueltas en un contenedor `div.wrapper` que un tema puede maquetar a -/// su gusto (por ejemplo, usando CSS Grid, para que `Aside` se muestre como columna lateral junto a -/// `Content`). Si `Aside`, o cualquier otra región, no tiene contenido, no se renderiza. +/// La composición por defecto usa el componente [`Region`](crate::base::component::layout::Region) +/// para mostrar, en este orden, las regiones [`CoreRegion::Header`], [`CoreRegion::Content`] y +/// [`CoreRegion::Footer`]. /// -/// No incluye las regiones reservadas ([`ReservedRegions::PageTop`] y -/// [`ReservedRegions::PageBottom`]) porque el propio [`Page::render()`] las añade antes y después -/// del resultado de [`Theme::render_page_body()`] para que se rendericen siempre, -/// independientemente de la plantilla que se use. +/// No incluye las regiones reservadas +/// [`ReservedRegion::PageTop`](crate::response::ReservedRegion::PageTop) y +/// [`ReservedRegion::PageBottom`](crate::response::ReservedRegion::PageBottom) porque el propio +/// [`Page::render()`](crate::response::Page::render) las añade antes y después del resultado de +/// [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body) para que se +/// rendericen siempre, independientemente de la plantilla que se use. /// /// Si un tema necesita maquetar una plantilla determinada de forma distinta, puede capturar este -/// componente en [`Theme::handle_component()`] y hacer [`downcast_ref()`] sobre el [`TemplateRef`] -/// que devuelve [`Self::template()`], para compararlo con la variante deseada. +/// componente en [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) y hacer +/// [`downcast_ref()`](crate::core::AnyCast::downcast_ref) sobre el [`TemplateRef`] que devuelve +/// [`Self::template()`], para compararlo con la variante deseada. /// /// Como cualquier otro componente, participa también en el despacho de las /// [acciones de componentes](crate::base::action::component) para que otras extensiones puedan /// intervenir en su renderizado. -/// -/// [`Region`]: crate::base::component::layout::Region -/// [`ReservedRegions::PageTop`]: crate::response::ReservedRegions::PageTop -/// [`ReservedRegions::PageBottom`]: crate::response::ReservedRegions::PageBottom -/// [`Page::render()`]: crate::response::Page::render -/// [`Theme::render_page_body()`]: crate::core::theme::Theme::render_page_body -/// [`Theme::handle_component()`]: crate::core::theme::Theme::handle_component -/// [`downcast_ref()`]: crate::core::AnyCast::downcast_ref #[derive(Clone, Getters)] pub struct Template { /// Devuelve la plantilla subyacente. @@ -48,7 +41,7 @@ impl fmt::Debug for Template { impl Default for Template { fn default() -> Self { Template { - template: &CoreTemplates::Standard, + template: &CoreTemplate::Standard, } } } @@ -65,39 +58,19 @@ impl Component for Template { } async fn prepare(&self, cx: &mut Context) -> Result { - let body = html! { - (layout::Region::header().render(cx).await) - (layout::Region::aside().render(cx).await) - (layout::Region::content().render(cx).await) - (layout::Region::footer().render(cx).await) - }; - - if body.is_empty() { - return Ok(html! {}); - } - Ok(html! { - div.wrapper { - (body) - } + (layout::Region::header().render(cx).await) + (layout::Region::default().render(cx).await) + (layout::Region::footer().render(cx).await) }) } } impl Template { - /// Define el componente que renderizará [`CoreTemplates::Standard`]. - /// - /// Equivale a [`Self::new()`] o [`Self::default()`], que ya usan esta plantilla por defecto. - pub fn standard() -> Self { - Template { - template: &CoreTemplates::Standard, - } - } - - /// Define el componente que renderizará [`CoreTemplates::Admin`]. + /// Define el componente que renderizará [`CoreTemplate::Admin`]. pub fn admin() -> Self { Template { - template: &CoreTemplates::Admin, + template: &CoreTemplate::Admin, } } diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index 48de920b..a1929526 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -25,7 +25,7 @@ impl Theme for Basic { .with_weight(-99), )) .alter_child_in( - &CoreRegions::Footer, + &CoreRegion::Footer, ChildOp::AddIfEmpty(PoweredBy::new().into()), ); } diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 6a9cfc03..6b1af5d2 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -2,7 +2,7 @@ use crate::auth::CurrentUser; use crate::core::TypeInfo; use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage}; use crate::core::theme::all::DEFAULT_THEME; -use crate::core::theme::{ChildrenInRegions, CoreRegions, CoreTemplates}; +use crate::core::theme::{ChildrenInRegions, CoreRegion, CoreTemplate}; use crate::core::theme::{RegionRef, TemplateRef, ThemeRef}; use crate::html::{Assets, Favicon, JavaScript, Preload, StyleSheet}; use crate::html::{Markup, Props, PropsOp, RoutePath, html}; @@ -77,7 +77,7 @@ pub enum ContextError { /// # use pagetop_aliner::Aliner; /// fn prepare_context(cx: C) -> C { /// cx.with_langid(&Locale::resolve("es-ES")) -/// .with_template(&CoreTemplates::Standard) +/// .with_template(&CoreTemplate::Standard) /// .with_theme(&Aliner) /// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico")))) /// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css"))) @@ -330,7 +330,7 @@ pub struct Context { impl Default for Context { fn default() -> Self { - Self::base(None, &CoreTemplates::Standard) + Self::base(None, &CoreTemplate::Standard) } } @@ -369,15 +369,15 @@ impl Context { /// Para un contexto sin petición (renderizar un componente de forma aislada, en tests o fuera /// del ciclo de una petición web), usa [`Context::default()`]. pub fn new(request: HttpRequest) -> Self { - Self::base(Some(request), &CoreTemplates::Standard) + Self::base(Some(request), &CoreTemplate::Standard) } /// Crea un nuevo contexto asociado a una petición HTTP, con la plantilla de administración. /// - /// El contexto inicializa el idioma, el tema y la plantilla [`CoreTemplates::Admin`], sin + /// El contexto inicializa el idioma, el tema y la plantilla [`CoreTemplate::Admin`], sin /// favicon ni otros recursos cargados. pub fn admin(request: HttpRequest) -> Self { - Self::base(Some(request), &CoreTemplates::Admin) + Self::base(Some(request), &CoreTemplate::Admin) } // Extrae el `CurrentUser` inyectado por middleware en las extensiones de la petición, o @@ -629,8 +629,7 @@ impl Contextual for Context { #[builder_fn] fn with_child(mut self, op: impl Into) -> Self { - self.regions - .alter_child_in(&CoreRegions::Content, op.into()); + self.regions.alter_child_in(&CoreRegion::Content, op.into()); self } diff --git a/src/core/theme.rs b/src/core/theme.rs index bdc3ac81..f89746d9 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -44,14 +44,15 @@ //! Un tema puede personalizarse en tres pasos, cada uno necesario sólo si lo que ofrece PageTop por //! defecto no basta: //! -//! 1. **Definir regiones nuevas**. Por defecto, PageTop define [`CoreRegions`] (`Header`, `Aside`, -//! `Content`, `Footer`) como regiones de plantilla siempre disponibles, y [`ReservedRegions`] -//! (`PageTop`, `PageBottom`) como regiones reservadas que se renderizan al margen de cualquier -//! plantilla. Un tema puede definir su propio *enum* que implemente [`RegionName`] para -//! **añadir** nuevas regiones que PageTop no ofrece (como barras laterales, regiones específicas -//! para menús, sliders, cabeceras hero, etc.). No es necesario redefinir las de [`CoreRegions`] -//! ni las de [`ReservedRegions`], que ya existen y se asume que cualquier tema respeta. -//! 2. **Definir plantillas nuevas**. Por defecto existe [`CoreTemplates`], con las plantillas +//! 1. **Definir regiones nuevas**. Por defecto, PageTop define [`CoreRegion`] (`Header`, `Content`, +//! `Footer`) como las regiones de plantilla que se asumen siempre disponibles, y +//! [`ReservedRegion`](crate::response::ReservedRegion) (`PageTop`, `PageBottom`) como las +//! regiones reservadas que se renderizan al margen de cualquier plantilla. Un tema puede definir +//! su propio *enum* que implemente [`RegionName`] para **añadir** nuevas regiones que PageTop no +//! ofrece (por ejemplo, una barra lateral). No es necesario redefinir las de [`CoreRegion`] ni +//! las de [`ReservedRegion`](crate::response::ReservedRegion), que ya existen y se asume que +//! cualquier tema respeta. +//! 2. **Definir plantillas nuevas**. Por defecto existe [`CoreTemplate`], con las plantillas //! `Standard` y `Admin` que usan `Page::new()` y `Page::admin()` respectivamente, y que son //! siempre las mismas: no hay un método de `Theme` para elegir una plantilla predeterminada //! distinta. Un tema puede definir su propio *enum* que implemente [`TemplateName`] para @@ -65,7 +66,7 @@ //! ([`Region::region()`](crate::base::component::layout::Region::region) o //! [`Template::template()`](crate::base::component::layout::Template::template)) con //! [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia el tipo concreto (por -//! ejemplo, [`CoreTemplates`] o el propio *enum* del tema). `pagetop-bootsier` hace exactamente +//! ejemplo, [`CoreTemplate`] o el propio *enum* del tema). `pagetop-bootsier` hace exactamente //! esto para maquetar `Standard` y `Admin` de forma distinta, sin necesitar sus propias //! variantes de plantilla. //! @@ -95,7 +96,7 @@ //! //! ```rust,no_run //! # use pagetop::prelude::*; -//! InRegion::Global(&CoreRegions::Footer).add(PoweredBy::new()); +//! InRegion::Global(&CoreRegion::Footer).add(PoweredBy::new()); //! ``` //! //! El componente se guarda como **prototipo**: cada página recibe un clon fresco en el momento del @@ -107,8 +108,6 @@ //! [ciclo de renderizado](crate::core::component::ComponentRender). Esto permite registrarlo una //! sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta de la petición o //! si el usuario actual está autenticado. -//! -//! [`ReservedRegions`]: crate::response::ReservedRegions use crate::AutoDefault; use crate::core::AnyInfo; @@ -118,30 +117,26 @@ use crate::locale::L10n; /// Interfaz común para las regiones lógicas del ``. /// -/// Una [`RegionName`] 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()`]. +/// Una `RegionName` representa un contenedor lógico identificado por un nombre de región. Su +/// contenido se obtiene del [`Context`](crate::core::component::Context), donde los componentes +/// 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 [`RegionName`] que devuelvan el mismo nombre comparten el mismo conjunto de -/// componentes registrados en el [`Context`]. Un *enum* propio que implemente [`RegionName`] está -/// pensado para **añadir** regiones que PageTop no ofrece (con un nombre propio que no colisione -/// con los de [`CoreRegions`] o [`ReservedRegions`]). +/// componentes registrados en el [`Context`](crate::core::component::Context). Un *enum* propio que +/// implemente [`RegionName`] está pensado para **añadir** regiones que PageTop no ofrece (con un +/// nombre propio que no colisione con los de [`CoreRegion`] o +/// [`ReservedRegion`](crate::response::ReservedRegion)). /// /// El tema decide qué regiones mostrar en el ``, normalmente usando una plantilla -/// ([`TemplateName`]) al renderizar la página ([`Page`]). +/// ([`TemplateName`]) al renderizar la página ([`Page`](crate::response::Page)). /// /// Requiere [`AnyInfo`] para que un [`RegionRef`] pueda recuperarse mediante -/// [`AnyCast::downcast_ref()`] hacia su tipo concreto (por ejemplo, para que un tema distinga en -/// [`Theme::handle_component()`] qué variante concreta está renderizando el componente [`Region`]). -/// -/// [`Context`]: crate::core::component::Context -/// [`Contextual::with_child_in()`]: crate::core::component::Contextual::with_child_in -/// [`ReservedRegions`]: crate::response::ReservedRegions -/// [`Page`]: crate::response::Page -/// [`AnyCast::downcast_ref()`]: crate::core::AnyCast::downcast_ref -/// [`Theme::handle_component()`]: crate::core::theme::Theme::handle_component -/// [`Region`]: crate::base::component::layout::Region +/// [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia su tipo concreto (por +/// ejemplo, para que un tema distinga en +/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) qué variante +/// concreta está renderizando el componente [`Region`](crate::base::component::layout::Region)). pub trait RegionName: Send + Sync + AnyInfo { /// Devuelve el nombre de la región. /// @@ -161,34 +156,24 @@ pub trait RegionName: Send + Sync + AnyInfo { /// Referencia estática a una región. pub type RegionRef = &'static dyn RegionName; -// **< CoreRegions >******************************************************************************** +// **< CoreRegion >********************************************************************************* /// Regiones básicas que PageTop proporciona por defecto. /// -/// Comparten sus nombres (`"header"`, `"aside"`, `"content"`, `"footer"`) con otras regiones que -/// implementen [`RegionName`], por lo que comparten también el contenido registrado bajo esos -/// nombres. Por defecto, son las regiones usadas por [`Template`]. +/// Comparten sus nombres (`"header"`, `"content"`, `"footer"`) con otras regiones que implementen +/// [`RegionName`], por lo que comparten también el contenido registrado bajo esos nombres. Por +/// defecto, son las regiones usadas por [`Template`](crate::base::component::layout::Template). /// -/// A estas regiones hay que sumar también las regiones internas reservadas por [`ReservedRegions`] -/// (`"page-top"` y `"page-bottom"`), que [`Page::render()`] renderiza en cualquier caso. -/// -/// [`Template`]: crate::base::component::layout::Template -/// [`ReservedRegions`]: crate::response::ReservedRegions -/// [`Page::render()`]: crate::response::Page::render +/// A estas regiones hay que sumar también las regiones internas reservadas por +/// [`ReservedRegion`](crate::response::ReservedRegion) (`"page-top"` y `"page-bottom"`), que +/// [`Page::render()`](crate::response::Page::render) renderiza en cualquier caso. #[derive(AutoDefault)] -pub enum CoreRegions { +pub enum CoreRegion { /// 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 de **contenido secundario**, de nombre `"aside"`. - /// - /// Se renderiza por defecto entre `Header` y `Content`. Un tema podría maquetarla, por ejemplo, - /// como columna lateral junto a `Content` y emplearla para menús secundarios o cualquier otro - /// contenido complementario al principal. - Aside, - /// Región principal de **contenido**, de nombre `"content"`. /// /// Es la región donde se renderiza el contenido principal del documento. En general será la @@ -202,12 +187,11 @@ pub enum CoreRegions { Footer, } -impl RegionName for CoreRegions { +impl RegionName for CoreRegion { #[inline] fn name(&self) -> &'static str { match self { Self::Header => "header", - Self::Aside => "aside", Self::Content => "content", Self::Footer => "footer", } @@ -216,10 +200,9 @@ impl RegionName for CoreRegions { #[inline] fn label(&self) -> L10n { match self { - Self::Header => L10n::l("region_header"), - Self::Aside => L10n::l("region_aside"), - Self::Content => L10n::l("region_content"), - Self::Footer => L10n::l("region_footer"), + Self::Header => L10n::l("region-header"), + Self::Content => L10n::l("region-content"), + Self::Footer => L10n::l("region-footer"), } } } @@ -248,11 +231,11 @@ pub trait TemplateName: Send + Sync + AnyInfo { /// Referencia estática a una plantilla. pub type TemplateRef = &'static dyn TemplateName; -// **< CoreTemplates >****************************************************************************** +// **< CoreTemplate >******************************************************************************* /// Plantillas que PageTop proporciona por defecto. #[derive(AutoDefault)] -pub enum CoreTemplates { +pub enum CoreTemplate { /// Plantilla predeterminada, de nombre `"standard"`. /// /// Se emplea cuando no se selecciona ninguna otra plantilla explícitamente. @@ -265,7 +248,7 @@ pub enum CoreTemplates { Admin, } -impl TemplateName for CoreTemplates { +impl TemplateName for CoreTemplate { #[inline] fn name(&self) -> &'static str { match self { diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 822cd9a9..3a120c4e 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -3,7 +3,7 @@ use crate::base::component::{Html, Intro, IntroOpening, layout}; use crate::core::component::{ChildOp, Component, ComponentError, ComponentRender}; use crate::core::component::{Context, Contextual}; use crate::core::extension::Extension; -use crate::core::theme::CoreRegions; +use crate::core::theme::CoreRegion; use crate::global; use crate::html::{Markup, html}; use crate::locale::L10n; @@ -84,10 +84,9 @@ pub trait Theme: Extension + Send + Sync { /// regiones. /// /// Con la configuración por defecto, la plantilla estándar utiliza las regiones - /// [`CoreRegions::Header`](crate::core::theme::CoreRegions::Header), - /// [`CoreRegions::Aside`](crate::core::theme::CoreRegions::Aside), - /// [`CoreRegions::Content`](crate::core::theme::CoreRegions::Content) y - /// [`CoreRegions::Footer`](crate::core::theme::CoreRegions::Footer) en ese orden. + /// [`CoreRegion::Header`](crate::core::theme::CoreRegion::Header), + /// [`CoreRegion::Content`](crate::core::theme::CoreRegion::Content) y + /// [`CoreRegion::Footer`](crate::core::theme::CoreRegion::Footer) en ese orden. /// /// Los temas pueden sobrescribir este método para: /// @@ -199,7 +198,7 @@ pub trait Theme: Extension + Send + Sync { /// component: &mut dyn Component, /// cx: &mut Context, /// ) -> Option> { - /// // Sólo mutación: ajusta el componente y deja que otro nivel lo renderice. + /// // Solo mutación: ajusta el componente y deja que otro nivel lo renderice. /// setup_component!(component, { /// Button => |btn| { btn.add_class("btn-primary"); }, /// }); @@ -222,15 +221,15 @@ pub trait Theme: Extension + Send + Sync { /// Contenido predefinido para la página de error "*403 - Forbidden*" (acceso denegado). /// /// Normalmente se renderiza con la plantilla ya activa en la página (por ejemplo - /// [`CoreTemplates::Standard`](crate::core::theme::CoreTemplates::Standard)), para que el - /// usuario no pierda el contexto de navegación del sitio. Los temas pueden sobrescribir este - /// método para personalizar completamente el diseño y el contenido de la página de error. + /// [`CoreTemplate::Standard`](crate::core::theme::CoreTemplate::Standard)), para que el usuario + /// no pierda el contexto de navegación del sitio. Los temas pueden sobrescribir este método + /// para personalizar completamente el diseño y el contenido de la página de error. fn error_403(&self, page: &mut Page) { if let Some(parent) = self.parent() { return parent.error_403(page); } page.alter_title(L10n::l("error403_title")).alter_child_in( - &CoreRegions::Content, + &CoreRegion::Content, ChildOp::Prepend( Html::with(move |cx| { html! { @@ -248,7 +247,7 @@ pub trait Theme: Extension + Send + Sync { /// Contenido predefinido para la página de error "*404 - Not Found*" (recurso no encontrado). /// /// Normalmente se renderiza con la plantilla ya activa en la página (por ejemplo - /// [`CoreTemplates::Standard`](crate::core::theme::CoreTemplates::Standard)). Los temas pueden + /// [`CoreTemplate::Standard`](crate::core::theme::CoreTemplate::Standard)). Los temas pueden /// sobrescribir este método para personalizar completamente el diseño y el contenido de la /// página de error. fn error_404(&self, page: &mut Page) { @@ -256,7 +255,7 @@ pub trait Theme: Extension + Send + Sync { return parent.error_404(page); } page.alter_title(L10n::l("error404_title")).alter_child_in( - &CoreRegions::Content, + &CoreRegion::Content, ChildOp::Prepend( Html::with(move |cx| { html! { @@ -273,15 +272,19 @@ pub trait Theme: Extension + Send + Sync { /// Permite al tema preparar y componer una página de **error fatal controlado**. /// - /// Devuelve explícitamente [`ErrorPage::BadRequest`], [`ErrorPage::InternalError`], - /// [`ErrorPage::ServiceUnavailable`] o [`ErrorPage::GatewayTimeout`], porque algo ha fallado, - /// pero el servidor sigue activo y el tema, el renderizado y el resto de componentes funcionan - /// con normalidad. + /// Esta función decide explícitamente devolver + /// [`ErrorPage::BadRequest`](crate::response::ErrorPage::BadRequest), + /// [`ErrorPage::InternalError`](crate::response::ErrorPage::InternalError), + /// [`ErrorPage::ServiceUnavailable`](crate::response::ErrorPage::ServiceUnavailable) o + /// [`ErrorPage::GatewayTimeout`](crate::response::ErrorPage::GatewayTimeout) porque algo ha + /// fallado, pero el servidor sigue activo y el tema, el renderizado y el resto de componentes + /// funcionan con normalidad. /// /// Por defecto, asigna el título al documento (`title`), se renderiza con la plantilla ya - /// activa en la página (normalmente [`CoreTemplates::Standard`]) y muestra un componente - /// [`Intro`] con el código HTTP del error (`code`) y los mensajes proporcionados (`alert` y - /// `help`) como descripción del error. + /// activa en la página (normalmente + /// [`CoreTemplate::Standard`](crate::core::theme::CoreTemplate::Standard)) y muestra un + /// componente [`Intro`] con el código HTTP del error (`code`) y los mensajes proporcionados + /// (`alert` y `help`) como descripción del error. /// /// Este método no se utiliza en las implementaciones predefinidas de [`Self::error_403()`] ni /// [`Self::error_404()`], que definen su propio contenido específico. @@ -293,18 +296,12 @@ pub trait Theme: Extension + Send + Sync { /// /// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la /// página de error. - /// - /// [`ErrorPage::BadRequest`]: crate::response::ErrorPage::BadRequest - /// [`ErrorPage::InternalError`]: crate::response::ErrorPage::InternalError - /// [`ErrorPage::ServiceUnavailable`]: crate::response::ErrorPage::ServiceUnavailable - /// [`ErrorPage::GatewayTimeout`]: crate::response::ErrorPage::GatewayTimeout - /// [`CoreTemplates::Standard`]: crate::core::theme::CoreTemplates::Standard fn error_fatal(&self, page: &mut Page, code: StatusCode, title: L10n, alert: L10n, help: L10n) { if let Some(parent) = self.parent() { return parent.error_fatal(page, code, title, alert, help); } page.alter_title(title).alter_child_in( - &CoreRegions::Content, + &CoreRegion::Content, ChildOp::Prepend( Intro::new() .with_title(L10n::l("error_code").with_arg("code", code.to_string())) diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 4c8db31f..9543b31b 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -1,5 +1,5 @@ use crate::core::component::{Child, ChildOp, Children, Component}; -use crate::core::theme::{CoreRegions, RegionRef, ThemeRef}; +use crate::core::theme::{CoreRegion, RegionRef, ThemeRef}; use crate::{AutoDefault, UniqueId, builder_fn}; use parking_lot::RwLock; @@ -132,13 +132,13 @@ impl ChildrenInRegions { /// InRegion::Content.add(Html::with(|_| html! { "🎉 ¡Bienvenido!" })); /// /// // Texto en la cabecera, visible en todos los temas. -/// InRegion::Global(&CoreRegions::Header).add(Html::with(|_| html! { "Publicidad" })); +/// InRegion::Global(&CoreRegion::Header).add(Html::with(|_| html! { "Publicidad" })); /// ``` pub enum InRegion { /// Región principal de **contenido** por defecto. /// /// Añade el componente a la región lógica de contenido principal de la aplicación. Internamente - /// equivale a `InRegion::Global(&CoreRegions::Content)`. + /// equivale a `InRegion::Global(&CoreRegion::Content)`. Content, /// Región global compartida por todos los temas. /// @@ -173,19 +173,19 @@ impl InRegion { /// })); /// /// // Texto en la cabecera. - /// InRegion::Global(&CoreRegions::Header).add(Html::with(|_| { + /// InRegion::Global(&CoreRegion::Header).add(Html::with(|_| { /// html! { "Publicidad" } /// })); /// /// // Contenido sólo para la región del pie de página en un tema concreto. - /// InRegion::ForTheme(&theme::Basic, &CoreRegions::Footer).add(Html::with(|_| { + /// InRegion::ForTheme(&theme::Basic, &CoreRegion::Footer).add(Html::with(|_| { /// html! { "Aviso legal" } /// })); /// ``` pub fn add(&self, component: impl Component) -> &Self { let proto: Arc = Arc::new(component); match self { - InRegion::Content => Self::add_to_common(&CoreRegions::Content, proto), + InRegion::Content => Self::add_to_common(&CoreRegion::Content, proto), InRegion::Global(region) => Self::add_to_common(*region, proto), InRegion::ForTheme(theme, region) => { THEME_REGIONS diff --git a/src/response/page.rs b/src/response/page.rs index 500e5b9d..d57e8288 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -10,7 +10,7 @@ //! composición del `` y del ``, y se ejecutan las acciones registradas por las //! extensiones antes y después de generar los contenidos. //! -//! También define las regiones internas reservadas ([`ReservedRegions`]) que actúan como puntos de +//! También define las regiones internas reservadas ([`ReservedRegion`]) que actúan como puntos de //! anclaje globales al inicio y al final del ``, fuera de las regiones que maqueta la //! plantilla activa. @@ -23,7 +23,7 @@ use crate::base::action; use crate::base::component::layout; use crate::core::component::{AssetsOp, ChildOp, ComponentRender}; use crate::core::component::{Context, ContextError, Contextual}; -use crate::core::theme::{CoreRegions, RegionName, RegionRef, TemplateRef, ThemeRef}; +use crate::core::theme::{CoreRegion, RegionName, RegionRef, TemplateRef, ThemeRef}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{Attr, Props, PropsOp}; use crate::html::{DOCTYPE, Markup, html}; @@ -31,7 +31,7 @@ use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; use crate::web::HttpRequest; use crate::{AutoDefault, builder_fn}; -// **< ReservedRegions >**************************************************************************** +// **< ReservedRegion >***************************************************************************** /// Regiones internas reservadas como puntos de anclaje globales. /// @@ -41,7 +41,7 @@ use crate::{AutoDefault, builder_fn}; /// [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body). **No suelen usarse /// como regiones "visibles" en los temas**, sino para inyectar contenido global o técnico. #[derive(AutoDefault)] -pub enum ReservedRegions { +pub enum ReservedRegion { /// Región interna situada al **inicio del ``**, de nombre `"page-top"`. /// /// Proporciona un contenedor donde las extensiones puedan inyectar elementos auxiliares antes @@ -63,7 +63,7 @@ pub enum ReservedRegions { PageBottom, } -impl RegionName for ReservedRegions { +impl RegionName for ReservedRegion { #[inline] fn name(&self) -> &'static str { match self { @@ -109,13 +109,13 @@ impl Page { } } - /// Crea una nueva instancia de página con la plantilla [`CoreTemplates::Admin`]. + /// Crea una nueva instancia de página con la plantilla [`CoreTemplate::Admin`]. /// /// Cada tema puede maquetarla de forma distinta capturando /// [`Template`](crate::base::component::layout::Template) en `handle_component()`, pero la /// plantilla en sí es la misma constante para cualquier tema. /// - /// [`CoreTemplates::Admin`]: crate::core::theme::CoreTemplates::Admin + /// [`CoreTemplate::Admin`]: crate::core::theme::CoreTemplate::Admin pub fn admin(request: HttpRequest) -> Self { Page { context: Context::admin(request), @@ -196,10 +196,10 @@ impl Page { /// 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 ([`ReservedRegions::PageTop`]). + /// - 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 ([`ReservedRegions::PageBottom`]). + /// - 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. @@ -221,9 +221,9 @@ impl Page { // Renderiza el . let body = html! { - (layout::Region::of(&ReservedRegions::PageTop).render(&mut self.context).await) + (layout::Region::of(&ReservedRegion::PageTop).render(&mut self.context).await) (self.context.theme().render_page_body(self).await) - (layout::Region::of(&ReservedRegions::PageBottom).render(&mut self.context).await) + (layout::Region::of(&ReservedRegion::PageBottom).render(&mut self.context).await) }; // Acciones específicas del tema después de renderizar el . @@ -314,8 +314,7 @@ impl Contextual for Page { #[builder_fn] fn with_child(mut self, op: impl Into) -> Self { - self.context - .alter_child_in(&CoreRegions::Content, op.into()); + self.context.alter_child_in(&CoreRegion::Content, op.into()); self } diff --git a/tests/component_template.rs b/tests/component_template.rs index fbf0bc8b..725cd8a4 100644 --- a/tests/component_template.rs +++ b/tests/component_template.rs @@ -11,7 +11,7 @@ async fn setup() { // **< A theme that intercepts the `Template` component >******************************************* /// Replaces the default `Template` composition (`Header` + `Content` + `Footer`) with a fixed -/// marker string, for both `CoreTemplates::Standard` and `CoreTemplates::Admin`. Mirrors how +/// marker string, for both `CoreTemplate::Standard` and `CoreTemplate::Admin`. Mirrors how /// a real theme (e.g. `pagetop-bootsier`) tells its own layout apart from PageTop's default: by /// intercepting the `Template` component in `handle_component()`, not by swapping which /// `TemplateRef` gets resolved. @@ -32,7 +32,7 @@ impl Theme for MarkerTheme { _cx: &mut Context, ) -> Option> { let template = (&*component).downcast_ref::()?; - template.template().downcast_ref::()?; + template.template().downcast_ref::()?; Some(Ok(html! { "marker-template-output" })) } } @@ -40,7 +40,7 @@ impl Theme for MarkerTheme { // **< Default/Admin template identity is independent of the active theme >************************* // // `Theme::default_template()`/`admin_template()` were removed: `Context::template()` always -// resolves `Default`/`Admin` to the core `CoreTemplates::Standard`/`Admin` identity, regardless +// resolves `Default`/`Admin` to the core `CoreTemplate::Standard`/`Admin` identity, regardless // of which theme is active. Themes customize the actual rendering by intercepting the `Template` // component in `handle_component()` instead (see the tests further below). @@ -68,7 +68,7 @@ async fn explicit_template_is_not_overridden_by_a_later_with_theme() { // A template explicitly set with `with_template()` prevails even if `with_theme()` is called // afterwards. let cx = Context::default() - .with_template(&CoreTemplates::Admin) + .with_template(&CoreTemplate::Admin) .with_theme(&pagetop::base::theme::Basic); assert_eq!(cx.template().name(), "admin");