diff --git a/assets/css/intro.css b/assets/css/intro.css index bceacb68..0f51eb51 100644 --- a/assets/css/intro.css +++ b/assets/css/intro.css @@ -447,12 +447,12 @@ body { } #intro-badges { - min-height: 3.2rem; + min-height: 3rem; text-align: center; visibility: hidden; } #intro-badges img { - margin-bottom: 0.25rem; + margin-bottom: 1.1rem; } #intro-release:not([src]) { display: none; diff --git a/examples/form-controls.rs b/examples/form-controls.rs index 29010de6..f8be00c7 100644 --- a/examples/form-controls.rs +++ b/examples/form-controls.rs @@ -145,12 +145,12 @@ async fn form_controls(request: HttpRequest) -> Result { // Botones de acción. .with_child(Button::submit(L10n::t("btn_submit", &LOC)).with_prop( PropsOp::add_classes(class::ButtonColor::solid( - ThemeColor::Primary, + token::Color::Primary, )), )) .with_child(Button::reset(L10n::t("btn_reset", &LOC)).with_prop( PropsOp::add_classes(class::ButtonColor::outline( - ThemeColor::Secondary, + token::Color::Secondary, )), )) .with_child( @@ -269,12 +269,12 @@ async fn form_controls(request: HttpRequest) -> Result { // Botones de acción. .with_child(Button::submit(L10n::t("btn_submit", &LOC)).with_prop( PropsOp::add_classes(class::ButtonColor::solid( - ThemeColor::Primary, + token::Color::Primary, )), )) .with_child(Button::reset(L10n::t("btn_reset", &LOC)).with_prop( PropsOp::add_classes(class::ButtonColor::outline( - ThemeColor::Secondary, + token::Color::Secondary, )), )) .with_child( @@ -423,12 +423,12 @@ fn form_lists() -> Form { // Botones de acción. .with_child( Button::submit(L10n::t("btn_submit", &LOC)).with_prop(PropsOp::add_classes( - class::ButtonColor::solid(ThemeColor::Primary), + class::ButtonColor::solid(token::Color::Primary), )), ) .with_child( Button::reset(L10n::t("btn_reset", &LOC)).with_prop(PropsOp::add_classes( - class::ButtonColor::outline(ThemeColor::Secondary), + class::ButtonColor::outline(token::Color::Secondary), )), ) .with_child( diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index fcc6e84c..a4c182fa 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -17,7 +17,7 @@ impl Extension for SuperMenu { async fn initialize(&self) { let navbar_menu = bs::Navbar::brand_left(bs::navbar::Brand::new()) - .with_expand(BreakPoint::LG) + .with_expand(token::BreakPoint::LG) .with_item(bs::navbar::Item::nav( bs::Nav::new() .with_item(bs::nav::Item::link(L10n::t("menus_item_link", &LOC), "/")) @@ -89,8 +89,8 @@ impl Extension for SuperMenu { .with_item(bs::navbar::Item::nav( bs::Nav::new() .with_prop(PropsOp::add_classes(class::Margin::with( - BoxSide::Start, - ScaleSize::Auto, + token::Side::Start, + token::ScaleSize::Auto, ))) .with_item(bs::nav::Item::link( L10n::t("menus_item_sign_up", &LOC), diff --git a/extensions/pagetop-bootsier/src/theme/class.rs b/extensions/pagetop-bootsier/src/theme/class.rs index 93c5e16a..51ab1ba0 100644 --- a/extensions/pagetop-bootsier/src/theme/class.rs +++ b/extensions/pagetop-bootsier/src/theme/class.rs @@ -1,29 +1,16 @@ //! Define clases para aplicar en componentes del tema. -//! -//! Incluyen puntos de ruptura, colores y niveles de opacidad, escalas de tamaño y lados, necesarios -//! para crear determinadas clases del tema: -//! -//! ```rust,no_run -//! use pagetop_bootsier::theme::*; -//! -//! let bg = class::Bg::with(ThemeColor::Primary); -//! let border = class::Border::new() -//! .with_side(BoxSide::Top, ScaleSize::Zero) -//! .with_color(ThemeColor::Danger); -//! ``` mod color; -pub use color::{Bg, BgColor}; -pub use color::{Text, TextColor}; +pub use color::{Background, Text}; mod button; -pub use button::{ButtonColor, ButtonColorStyle, ButtonSize, ButtonSizeKind}; +pub use button::{ButtonColor, ButtonSize}; mod border; -pub use border::{Border, BorderColor}; +pub use border::Border; mod rounded; -pub use rounded::{Rounded, RoundedRadius}; +pub use rounded::Rounded; mod layout; pub use layout::{Margin, Padding}; diff --git a/extensions/pagetop-bootsier/src/theme/class/border.rs b/extensions/pagetop-bootsier/src/theme/class/border.rs index b25780e9..1255fd21 100644 --- a/extensions/pagetop-bootsier/src/theme/class/border.rs +++ b/extensions/pagetop-bootsier/src/theme/class/border.rs @@ -1,114 +1,6 @@ use pagetop::prelude::*; -use crate::theme::{BoxSide, OpacityLevel, ScaleSize, ThemeColor}; - -// **< BorderColor >******************************************************************************** - -/// Esquema de color para los bordes ([`Border`]). -/// -/// - `Solid(ThemeColor)` y `Subtle(ThemeColor)` usan la paleta de colores temáticos -/// ([`ThemeColor`]). -/// - `Black` y `White` son colores fijos independientes del tema. -/// - `Default` no genera ninguna clase. -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum BorderColor { - /// No define ninguna clase. - #[default] - Default, - /// Genera la clase `border-{color}`. - Solid(ThemeColor), - /// Genera la clase `border-{color}-subtle` (un tono suavizado del color). - Subtle(ThemeColor), - /// Color negro. - Black, - /// Color blanco. - White, -} - -impl BorderColor { - // Devuelve el sufijo de la clase `border-*`, o `None` si no define ninguna clase. - #[rustfmt::skip] - #[inline] - const fn suffix(self) -> Option<&'static str> { - match self { - Self::Default => None, - Self::Solid(_) => Some(""), - Self::Subtle(_) => Some("-subtle"), - Self::Black => Some("-black"), - Self::White => Some("-white"), - } - } - - /// Añade la clase `border-*` a la cadena de clases. - #[inline] - pub fn push_to(self, classes: &mut String) { - if let Some(suffix) = self.suffix() { - if !classes.is_empty() { - classes.push(' '); - } - match self { - Self::Solid(c) | Self::Subtle(c) => { - classes.push_str("border-"); - classes.push_str(c.as_str()); - } - _ => classes.push_str("border"), - } - classes.push_str(suffix); - } - } - - /// Devuelve la clase `border-*` correspondiente al color de borde. - /// - /// # Ejemplos - /// - /// ```rust - /// # use pagetop_bootsier::theme::*; - /// let solid = class::BorderColor::Solid(ThemeColor::Primary).to_class(); - /// assert_eq!(solid, "border-primary"); - /// - /// let subtle = class::BorderColor::Subtle(ThemeColor::Warning).to_class(); - /// assert_eq!(subtle, "border-warning-subtle"); - /// - /// let black = class::BorderColor::Black.to_class(); - /// assert_eq!(black, "border-black"); - /// - /// let none = class::BorderColor::Default.to_class(); - /// assert_eq!(none, ""); - /// ``` - #[inline] - pub fn to_class(self) -> String { - let mut class = String::new(); - self.push_to(&mut class); - class - } -} - -impl From for BorderColor { - /// Convierte un [`ThemeColor`] en [`BorderColor::Solid`]. - /// - /// Es el atajo habitual para los colores temáticos. Para los demás esquemas (`Subtle`, `Black`, - /// `White`) sigue usando [`BorderColor`]. - /// - /// # Ejemplo - /// - /// ```rust - /// # use pagetop_bootsier::theme::*; - /// let border: class::BorderColor = ThemeColor::Success.into(); - /// assert_eq!(border.to_class(), "border-success"); - /// ``` - fn from(color: ThemeColor) -> Self { - Self::Solid(color) - } -} - -impl Into for BorderColor { - /// Permite pasar [`BorderColor`] directamente a [`PropsOp`](pagetop::prelude::PropsOp). - fn into(self) -> CowStr { - self.to_class().into() - } -} - -// **< Border >************************************************************************************* +use crate::theme::token::{ColorBorder, Opacity, ScaleSize, Side}; /// Clases para definir **bordes**. /// @@ -118,19 +10,19 @@ impl Into for BorderColor { /// - Crear un borde con tamaño por defecto (`Border::new()`). /// - Ajustar el tamaño de cada **lado lógico** (`side`, respetando LTR/RTL). /// - Asignar **un tamaño global** para todo el borde (`size`). -/// - Aplicar un **color** al borde (`BorderColor`). -/// - Aplicar un nivel de **opacidad** (`OpacityLevel`). +/// - Aplicar un **color** al borde (`ColorBorder`). +/// - Aplicar un nivel de **opacidad** (`Opacity`). /// /// # Comportamiento aditivo / sustractivo /// /// - **Aditivo**: basta con crear un borde sin tamaño con `class::Border::default()` para ir -/// añadiendo cada lado lógico con el tamaño deseado usando `ScaleSize::{One..Five}`. +/// añadiendo cada lado lógico con el tamaño deseado usando `token::ScaleSize::{One..Five}`. /// /// - **Sustractivo**: se crea un borde con tamaño predefinido, por ejemplo usando -/// `class::Border::new()` o `class::Border::with(ScaleSize::Two)` y eliminar los lados deseados -/// con `ScaleSize::Zero`. +/// `class::Border::new()` o `class::Border::with(token::ScaleSize::Two)` y eliminar los lados +/// deseados con `token::ScaleSize::Zero`. /// -/// - **Anchos diferentes por lado**: usando `ScaleSize::{Zero..Five}` en cada lado deseado. +/// - **Anchos diferentes por lado**: usando `token::ScaleSize::{Zero..Five}` en cada lado deseado. /// /// # Ejemplos /// @@ -138,29 +30,29 @@ impl Into for BorderColor { /// use pagetop_bootsier::theme::*; /// /// // Borde global. -/// let b = class::Border::with(ScaleSize::Two); +/// let b = class::Border::with(token::ScaleSize::Two); /// assert_eq!(b.to_class(), "border-2"); /// /// // Aditivo (sólo borde superior): -/// let b = class::Border::default().with_side(BoxSide::Top, ScaleSize::One); +/// let b = class::Border::default().with_side(token::Side::Top, token::ScaleSize::One); /// assert_eq!(b.to_class(), "border-top-1"); /// /// // Sustractivo (borde global menos el superior): -/// let b = class::Border::new().with_side(BoxSide::Top, ScaleSize::Zero); +/// let b = class::Border::new().with_side(token::Side::Top, token::ScaleSize::Zero); /// assert_eq!(b.to_class(), "border border-top-0"); /// /// // Ancho por lado (lado lógico inicial a 2 y final a 4): /// let b = class::Border::default() -/// .with_side(BoxSide::Start, ScaleSize::Two) -/// .with_side(BoxSide::End, ScaleSize::Four); +/// .with_side(token::Side::Start, token::ScaleSize::Two) +/// .with_side(token::Side::End, token::ScaleSize::Four); /// assert_eq!(b.to_class(), "border-end-4 border-start-2"); /// /// // Combinado (ejemplo completo): -/// let b = class::Border::new() // Borde por defecto. -/// .with_side(BoxSide::Top, ScaleSize::Zero) // Quita borde superior. -/// .with_side(BoxSide::End, ScaleSize::Three) // Ancho 3 para lado lógico final. -/// .with_color(ThemeColor::Primary) -/// .with_opacity(OpacityLevel::Half); +/// let b = class::Border::new() // Borde por defecto. +/// .with_side(token::Side::Top, token::ScaleSize::Zero) // Quita borde superior. +/// .with_side(token::Side::End, token::ScaleSize::Three) // Ancho 3 para lado lógico final. +/// .with_color(token::Color::Primary) +/// .with_opacity(token::Opacity::Half); /// assert_eq!(b.to_class(), "border border-top-0 border-end-3 border-primary border-opacity-50"); /// ``` #[rustfmt::skip] @@ -171,8 +63,8 @@ pub struct Border { end : ScaleSize, bottom : ScaleSize, start : ScaleSize, - color : BorderColor, - opacity: OpacityLevel, + color : ColorBorder, + opacity: Opacity, } impl Border { @@ -183,24 +75,24 @@ impl Border { /// Define un borde con un tamaño global (`size`) para todos los lados. pub fn with(size: ScaleSize) -> Self { - Self::default().with_side(BoxSide::All, size) + Self::default().with_side(Side::All, size) } // **< Border BUILDER >************************************************************************* - /// Ajusta el tamaño del borde en el lado indicado (ver [`BoxSide`](crate::theme::BoxSide)). - pub fn with_side(mut self, side: BoxSide, size: ScaleSize) -> Self { + /// Ajusta el tamaño del borde en el lado indicado (ver [`Side`](crate::theme::token::Side)). + pub fn with_side(mut self, side: Side, size: ScaleSize) -> Self { match side { - BoxSide::All => self.all = size, - BoxSide::Top => self.top = size, - BoxSide::Bottom => self.bottom = size, - BoxSide::Start => self.start = size, - BoxSide::End => self.end = size, - BoxSide::LeftAndRight => { + Side::All => self.all = size, + Side::Top => self.top = size, + Side::Bottom => self.bottom = size, + Side::Start => self.start = size, + Side::End => self.end = size, + Side::LeftAndRight => { self.start = size; self.end = size; } - BoxSide::TopAndBottom => { + Side::TopAndBottom => { self.top = size; self.bottom = size; } @@ -210,15 +102,15 @@ impl Border { /// Establece el color del borde. /// - /// Acepta un tipo convertible en [`BorderColor`]. Un [`ThemeColor`] se convierte - /// automáticamente en [`BorderColor::Solid`]. - pub fn with_color(mut self, color: impl Into) -> Self { + /// Acepta un tipo convertible en [`ColorBorder`]. Un [`Color`](crate::theme::token::Color) se + /// convierte automáticamente en [`ColorBorder::Theme`]. + pub fn with_color(mut self, color: impl Into) -> Self { self.color = color.into(); self } /// Establece la opacidad del borde. - pub fn with_opacity(mut self, opacity: OpacityLevel) -> Self { + pub fn with_opacity(mut self, opacity: Opacity) -> Self { self.opacity = opacity; self } @@ -258,11 +150,11 @@ impl From for Border { /// ```rust /// # use pagetop_bootsier::theme::*; /// // Convertir explícitamente con `From::from`: - /// let b = class::Border::from(ScaleSize::Two); + /// let b = class::Border::from(token::ScaleSize::Two); /// assert_eq!(b.to_class(), "border-2"); /// /// // Convertir implícitamente con `into()`: - /// let b: class::Border = ScaleSize::Auto.into(); + /// let b: class::Border = token::ScaleSize::Auto.into(); /// assert_eq!(b.to_class(), "border"); /// ``` fn from(size: ScaleSize) -> Self { diff --git a/extensions/pagetop-bootsier/src/theme/class/button.rs b/extensions/pagetop-bootsier/src/theme/class/button.rs index 733fd3d8..28ab792f 100644 --- a/extensions/pagetop-bootsier/src/theme/class/button.rs +++ b/extensions/pagetop-bootsier/src/theme/class/button.rs @@ -1,20 +1,15 @@ use pagetop::prelude::*; -use crate::theme::ThemeColor; +use crate::theme::token::Color; // **< ButtonColor >******************************************************************************** -/// Estilo visual aplicado al color de un botón ([`ButtonColor`]). #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum ButtonColorStyle { - /// Sin clase de color (estilo por defecto del tema). +enum ButtonColorStyle { #[default] None, - /// Botón sólido: genera la clase `btn-{color}`. Solid, - /// Botón con contorno: genera la clase `btn-outline-{color}`. Outline, - /// Botón tipo enlace: genera la clase `btn-link`. Link, } @@ -28,11 +23,11 @@ pub enum ButtonColorStyle { /// /// // Botón sólido. /// let save = bs::Button::submit(L10n::n("Save")) -/// .with_prop(PropsOp::add_classes(class::ButtonColor::solid(ThemeColor::Primary))); +/// .with_prop(PropsOp::add_classes(class::ButtonColor::solid(token::Color::Primary))); /// /// // Botón con contorno. /// let cancel = bs::Button::plain(L10n::n("Cancel")) -/// .with_prop(PropsOp::add_classes(class::ButtonColor::outline(ThemeColor::Secondary))); +/// .with_prop(PropsOp::add_classes(class::ButtonColor::outline(token::Color::Secondary))); /// /// // Botón tipo enlace. /// let back = bs::Button::plain(L10n::n("Back")) @@ -41,7 +36,7 @@ pub enum ButtonColorStyle { #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub struct ButtonColor { style: ButtonColorStyle, - color: ThemeColor, + color: Color, } impl ButtonColor { @@ -51,7 +46,7 @@ impl ButtonColor { } /// Botón sólido: genera la clase `btn-{color}`. - pub fn solid(color: ThemeColor) -> Self { + pub fn solid(color: Color) -> Self { Self { style: ButtonColorStyle::Solid, color, @@ -60,7 +55,7 @@ impl ButtonColor { } /// Botón con contorno: genera la clase `btn-outline-{color}`. - pub fn outline(color: ThemeColor) -> Self { + pub fn outline(color: Color) -> Self { Self { style: ButtonColorStyle::Outline, color, @@ -79,17 +74,11 @@ impl ButtonColor { // **< ButtonColor BUILDER >******************************************************************** /// Cambia el color aplicado al botón (`btn-*` o `btn-outline-*`). - pub fn with_color(mut self, color: ThemeColor) -> Self { + pub fn with_color(mut self, color: Color) -> Self { self.color = color; self } - /// Cambia el estilo aplicado al botón (sólido, contorno o enlace). - pub fn with_style(mut self, style: ButtonColorStyle) -> Self { - self.style = style; - self - } - // **< ButtonColor HELPERS >******************************************************************** /// Añade la clase `btn-*` a la cadena de clases. @@ -128,15 +117,11 @@ impl Into for ButtonColor { // **< ButtonSize >********************************************************************************* -/// Tamaño aplicado a un botón ([`ButtonSize`]). #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum ButtonSizeKind { - /// Sin clase de tamaño (tamaño por defecto del tema). +enum ButtonSizeVariant { #[default] None, - /// Botón compacto: genera la clase `btn-sm`. Small, - /// Botón grande: genera la clase `btn-lg`. Large, } @@ -156,7 +141,7 @@ pub enum ButtonSizeKind { /// ``` #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub struct ButtonSize { - size: ButtonSizeKind, + size: ButtonSizeVariant, } impl ButtonSize { @@ -168,34 +153,26 @@ impl ButtonSize { /// Botón compacto: genera la clase `btn-sm`. pub fn small() -> Self { Self { - size: ButtonSizeKind::Small, + size: ButtonSizeVariant::Small, } } /// Botón grande: genera la clase `btn-lg`. pub fn large() -> Self { Self { - size: ButtonSizeKind::Large, + size: ButtonSizeVariant::Large, } } - // **< ButtonSize BUILDER >********************************************************************* - - /// Cambia el tamaño aplicado al botón. - pub fn with_size(mut self, size: ButtonSizeKind) -> Self { - self.size = size; - self - } - // **< ButtonSize HELPERS >********************************************************************* /// Añade la clase `btn-sm` o `btn-lg` a la cadena de clases. #[inline] pub fn push_to(self, classes: &mut String) { let class = match self.size { - ButtonSizeKind::None => return, - ButtonSizeKind::Small => "btn-sm", - ButtonSizeKind::Large => "btn-lg", + ButtonSizeVariant::None => return, + ButtonSizeVariant::Small => "btn-sm", + ButtonSizeVariant::Large => "btn-lg", }; if !classes.is_empty() { classes.push(' '); diff --git a/extensions/pagetop-bootsier/src/theme/class/color.rs b/extensions/pagetop-bootsier/src/theme/class/color.rs index b2378a32..871567e2 100644 --- a/extensions/pagetop-bootsier/src/theme/class/color.rs +++ b/extensions/pagetop-bootsier/src/theme/class/color.rs @@ -1,130 +1,8 @@ use pagetop::prelude::*; -use crate::theme::{OpacityLevel, ThemeColor}; +use crate::theme::token::{ColorBg, ColorText, Opacity}; -// **< BgColor >************************************************************************************ - -/// Esquema de color para el fondo ([`Bg`]). -/// -/// - `Body`, `BodySecondary` y `BodyTertiary` siguen el esquema del tema (claro/oscuro). -/// - `Solid(ThemeColor)` y `Subtle(ThemeColor)` usan la paleta de colores temáticos -/// ([`ThemeColor`]). -/// - `Black`, `White`, `Transparent` son colores fijos independientes del tema. -/// - `Default` no genera ninguna clase. -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum BgColor { - /// No define ninguna clase. - #[default] - Default, - /// Fondo predefinido del tema (`bg-body`). - Body, - /// Fondo predefinido del tema (`bg-body-secondary`). - BodySecondary, - /// Fondo predefinido del tema (`bg-body-tertiary`). - BodyTertiary, - /// Genera la clase `bg-{color}` (p. ej., `bg-primary`). - Solid(ThemeColor), - /// Genera la clase `bg-{color}-subtle` (un tono suavizado del color). - Subtle(ThemeColor), - /// Color negro. - Black, - /// Color blanco. - White, - /// Fondo transparente (`bg-transparent`). - Transparent, -} - -impl BgColor { - // Devuelve el sufijo de la clase `bg-*`, o `None` si no define ninguna clase. - #[rustfmt::skip] - #[inline] - const fn suffix(self) -> Option<&'static str> { - match self { - Self::Default => None, - Self::Body => Some("-body"), - Self::BodySecondary => Some("-body-secondary"), - Self::BodyTertiary => Some("-body-tertiary"), - Self::Solid(_) => Some(""), - Self::Subtle(_) => Some("-subtle"), - Self::Black => Some("-black"), - Self::White => Some("-white"), - Self::Transparent => Some("-transparent"), - } - } - - /// Añade la clase de fondo `bg-*` a la cadena de clases. - #[inline] - pub fn push_to(self, classes: &mut String) { - if let Some(suffix) = self.suffix() { - if !classes.is_empty() { - classes.push(' '); - } - match self { - Self::Solid(c) | Self::Subtle(c) => { - classes.push_str("bg-"); - classes.push_str(c.as_str()); - } - _ => classes.push_str("bg"), - } - classes.push_str(suffix); - } - } - - /// Devuelve la clase `bg-*` correspondiente al fondo. - /// - /// # Ejemplos - /// - /// ```rust - /// # use pagetop_bootsier::theme::*; - /// let body = class::BgColor::Body.to_class(); - /// assert_eq!(body, "bg-body"); - /// - /// let solid = class::BgColor::Solid(ThemeColor::Primary).to_class(); - /// assert_eq!(solid, "bg-primary"); - /// - /// let subtle = class::BgColor::Subtle(ThemeColor::Warning).to_class(); - /// assert_eq!(subtle, "bg-warning-subtle"); - /// - /// let transparent = class::BgColor::Transparent.to_class(); - /// assert_eq!(transparent, "bg-transparent"); - /// - /// let none = class::BgColor::Default.to_class(); - /// assert_eq!(none, ""); - /// ``` - #[inline] - pub fn to_class(self) -> String { - let mut class = String::new(); - self.push_to(&mut class); - class - } -} - -impl From for BgColor { - /// Convierte un [`ThemeColor`] en [`BgColor::Solid`]. - /// - /// Es el atajo habitual para los colores temáticos. Para los demás esquemas (`Body`, `Subtle`, - /// `Black`, etc.) sigue usando [`BgColor`]. - /// - /// # Ejemplo - /// - /// ```rust - /// # use pagetop_bootsier::theme::*; - /// let bg: class::BgColor = ThemeColor::Primary.into(); - /// assert_eq!(bg.to_class(), "bg-primary"); - /// ``` - fn from(color: ThemeColor) -> Self { - Self::Solid(color) - } -} - -impl Into for BgColor { - /// Permite pasar [`BgColor`] directamente a [`PropsOp`](pagetop::prelude::PropsOp). - fn into(self) -> CowStr { - self.to_class().into() - } -} - -// **< Bg >***************************************************************************************** +// **< Background >********************************************************************************* /// Clases para establecer **color/opacidad del fondo**. /// @@ -134,32 +12,33 @@ impl Into for BgColor { /// use pagetop_bootsier::theme::*; /// /// // Sin clases. -/// let s = class::Bg::new(); +/// let s = class::Background::new(); /// assert_eq!(s.to_class(), ""); /// -/// // Sólo color de fondo (forma corta con ThemeColor). -/// let s = class::Bg::with(ThemeColor::Primary); +/// // Sólo color de fondo (forma corta con Color). +/// let s = class::Background::with(token::Color::Primary); /// assert_eq!(s.to_class(), "bg-primary"); /// /// // Color más opacidad. -/// let s = class::Bg::with(class::BgColor::BodySecondary).with_opacity(OpacityLevel::Half); +/// let s = class::Background::with(token::ColorBg::BodySecondary) +/// .with_opacity(token::Opacity::Half); /// assert_eq!(s.to_class(), "bg-body-secondary bg-opacity-50"); /// -/// // Usando `From`. -/// let s: class::Bg = class::BgColor::Black.into(); +/// // Usando `From`. +/// let s: class::Background = token::ColorBg::Black.into(); /// assert_eq!(s.to_class(), "bg-black"); /// -/// // Usando `From<(BgColor, OpacityLevel)>`. -/// let s: class::Bg = (class::BgColor::White, OpacityLevel::SemiTransparent).into(); +/// // Usando `From<(ColorBg, Opacity)>`. +/// let s: class::Background = (token::ColorBg::White, token::Opacity::SemiTransparent).into(); /// assert_eq!(s.to_class(), "bg-white bg-opacity-25"); /// ``` #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub struct Bg { - color: BgColor, - opacity: OpacityLevel, +pub struct Background { + color: ColorBg, + opacity: Opacity, } -impl Bg { +impl Background { /// Prepara un nuevo estilo para aplicar al fondo. pub fn new() -> Self { Self::default() @@ -167,41 +46,41 @@ impl Bg { /// Crea un estilo fijando el color de fondo (`bg-*`). /// - /// Acepta cualquier tipo convertible en [`BgColor`]. Un [`ThemeColor`] se convierte - /// automáticamente en [`BgColor::Solid`]: + /// Acepta cualquier tipo convertible en [`ColorBg`]. Un [`Color`](crate::theme::token::Color) + /// se convierte automáticamente en [`ColorBg::Theme`]: /// /// ```rust /// # use pagetop_bootsier::theme::*; - /// // Forma corta con ThemeColor: - /// let s = class::Bg::with(ThemeColor::Primary); + /// // Forma corta con Color: + /// let s = class::Background::with(token::Color::Primary); /// assert_eq!(s.to_class(), "bg-primary"); /// /// // Forma explícita para variantes no temáticas: - /// let s = class::Bg::with(class::BgColor::Body); + /// let s = class::Background::with(token::ColorBg::Body); /// assert_eq!(s.to_class(), "bg-body"); /// ``` - pub fn with(color: impl Into) -> Self { + pub fn with(color: impl Into) -> Self { Self::default().with_color(color) } - // **< Bg BUILDER >***************************************************************************** + // **< Background BUILDER >********************************************************************* /// Establece el color de fondo (`bg-*`). /// - /// Acepta cualquier tipo convertible en [`BgColor`]. Un [`ThemeColor`] se convierte - /// automáticamente en [`BgColor::Solid`]. - pub fn with_color(mut self, color: impl Into) -> Self { + /// Acepta cualquier tipo convertible en [`ColorBg`]. Un [`Color`](crate::theme::token::Color) + /// se convierte automáticamente en [`ColorBg::Theme`]. + pub fn with_color(mut self, color: impl Into) -> Self { self.color = color.into(); self } /// Establece la opacidad del fondo (`bg-opacity-*`). - pub fn with_opacity(mut self, opacity: OpacityLevel) -> Self { + pub fn with_opacity(mut self, opacity: Opacity) -> Self { self.opacity = opacity; self } - // **< Bg HELPERS >***************************************************************************** + // **< Background HELPERS >********************************************************************* /// Concatena, en este orden, color del fondo (`bg-*`) y opacidad (`bg-opacity-*`), omitiendo /// los fragmentos vacíos. @@ -222,173 +101,40 @@ impl Bg { } } -impl From<(BgColor, OpacityLevel)> for Bg { - /// Crea la clase para un [`Bg`](crate::theme::class::Bg) a partir del color de fondo y la - /// opacidad. +impl From<(ColorBg, Opacity)> for Background { + /// Crea la clase para un [`Background`](crate::theme::class::Background) a partir del color de + /// fondo y la opacidad. /// /// # Ejemplo /// /// ```rust /// # use pagetop_bootsier::theme::*; - /// let s: class::Bg = (class::BgColor::White, OpacityLevel::SemiTransparent).into(); + /// let s: class::Background = (token::ColorBg::White, token::Opacity::SemiTransparent).into(); /// assert_eq!(s.to_class(), "bg-white bg-opacity-25"); /// ``` - fn from((color, opacity): (BgColor, OpacityLevel)) -> Self { - Bg::with(color).with_opacity(opacity) + fn from((color, opacity): (ColorBg, Opacity)) -> Self { + Background::with(color).with_opacity(opacity) } } -impl From for Bg { - /// Crea la clase para un [`Bg`](crate::theme::class::Bg) a partir del color de fondo. +impl From for Background { + /// Crea la clase para un [`Background`](crate::theme::class::Background) a partir del color de + /// fondo. /// /// # Ejemplo /// /// ```rust /// # use pagetop_bootsier::theme::*; - /// let s: class::Bg = class::BgColor::Black.into(); + /// let s: class::Background = token::ColorBg::Black.into(); /// assert_eq!(s.to_class(), "bg-black"); /// ``` - fn from(color: BgColor) -> Self { - Bg::with(color) + fn from(color: ColorBg) -> Self { + Background::with(color) } } -impl Into for Bg { - /// Permite pasar [`Bg`] directamente a [`PropsOp`](pagetop::prelude::PropsOp). - fn into(self) -> CowStr { - self.to_class().into() - } -} - -// **< TextColor >********************************************************************************** - -/// Esquema de color para el texto ([`Text`]). -/// -/// - `Body`, `BodyEmphasis`, `BodySecondary` y `BodyTertiary` siguen el tema (claro/oscuro). -/// - `Solid(ThemeColor)` y `Emphasis(ThemeColor)` usan la paleta de colores temáticos -/// ([`ThemeColor`]). -/// - `Bg(ThemeColor)` genera la utilidad combinada `text-bg-{color}` (fondo más un color de texto -/// de contraste garantizado; no es una utilidad puramente de texto). -/// - `Black` y `White` son colores fijos independientes del tema. -/// - `Default` no genera ninguna clase. -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum TextColor { - /// No define ninguna clase. - #[default] - Default, - /// Color predefinido del tema (`text-body`). - Body, - /// Color de mayor contraste según el tema (`text-body-emphasis`). - BodyEmphasis, - /// Color predefinido del tema (`text-body-secondary`). - BodySecondary, - /// Color predefinido del tema (`text-body-tertiary`). - BodyTertiary, - /// Genera la clase `text-{color}`. - Solid(ThemeColor), - /// Genera la clase `text-{color}-emphasis` (mayor contraste acorde al tema). - Emphasis(ThemeColor), - /// Genera la clase `text-bg-{color}` (fondo con color de texto de contraste garantizado). - Bg(ThemeColor), - /// Color negro. - Black, - /// Color blanco. - White, -} - -impl TextColor { - // Devuelve el sufijo de la clase `text-*`, o `None` si no define ninguna clase. - #[rustfmt::skip] - #[inline] - const fn suffix(self) -> Option<&'static str> { - match self { - Self::Default => None, - Self::Body => Some("-body"), - Self::BodyEmphasis => Some("-body-emphasis"), - Self::BodySecondary => Some("-body-secondary"), - Self::BodyTertiary => Some("-body-tertiary"), - Self::Solid(_) => Some(""), - Self::Emphasis(_) => Some("-emphasis"), - Self::Bg(_) => Some(""), - Self::Black => Some("-black"), - Self::White => Some("-white"), - } - } - - /// Añade la clase de texto `text-*` a la cadena de clases. - #[inline] - pub fn push_to(self, classes: &mut String) { - if let Some(suffix) = self.suffix() { - if !classes.is_empty() { - classes.push(' '); - } - match self { - Self::Solid(c) | Self::Emphasis(c) => { - classes.push_str("text-"); - classes.push_str(c.as_str()); - } - Self::Bg(c) => { - classes.push_str("text-bg-"); - classes.push_str(c.as_str()); - } - _ => classes.push_str("text"), - } - classes.push_str(suffix); - } - } - - /// Devuelve la clase `text-*` correspondiente al color del texto. - /// - /// # Ejemplos - /// - /// ```rust - /// # use pagetop_bootsier::theme::*; - /// let body = class::TextColor::Body.to_class(); - /// assert_eq!(body, "text-body"); - /// - /// let solid = class::TextColor::Solid(ThemeColor::Primary).to_class(); - /// assert_eq!(solid, "text-primary"); - /// - /// let emphasis = class::TextColor::Emphasis(ThemeColor::Danger).to_class(); - /// assert_eq!(emphasis, "text-danger-emphasis"); - /// - /// let bg = class::TextColor::Bg(ThemeColor::Secondary).to_class(); - /// assert_eq!(bg, "text-bg-secondary"); - /// - /// let black = class::TextColor::Black.to_class(); - /// assert_eq!(black, "text-black"); - /// - /// let none = class::TextColor::Default.to_class(); - /// assert_eq!(none, ""); - /// ``` - #[inline] - pub fn to_class(self) -> String { - let mut class = String::new(); - self.push_to(&mut class); - class - } -} - -impl From for TextColor { - /// Convierte un [`ThemeColor`] en [`TextColor::Solid`]. - /// - /// Es el atajo habitual para los colores temáticos. Para los demás esquemas (`Body`, - /// `Emphasis`, `Black`, etc.) sigue usando [`TextColor`]. - /// - /// # Ejemplo - /// - /// ```rust - /// # use pagetop_bootsier::theme::*; - /// let text: class::TextColor = ThemeColor::Danger.into(); - /// assert_eq!(text.to_class(), "text-danger"); - /// ``` - fn from(color: ThemeColor) -> Self { - Self::Solid(color) - } -} - -impl Into for TextColor { - /// Permite pasar [`TextColor`] directamente a [`PropsOp`](pagetop::prelude::PropsOp). +impl Into for Background { + /// Permite pasar [`Background`] directamente a [`PropsOp`](pagetop::prelude::PropsOp). fn into(self) -> CowStr { self.to_class().into() } @@ -407,30 +153,30 @@ impl Into for TextColor { /// let s = class::Text::new(); /// assert_eq!(s.to_class(), ""); /// -/// // Sólo color del texto (forma corta con ThemeColor). -/// let s = class::Text::with(ThemeColor::Primary); +/// // Sólo color del texto (forma corta con Color). +/// let s = class::Text::with(token::Color::Primary); /// assert_eq!(s.to_class(), "text-primary"); /// /// // Color del texto y opacidad. -/// let s = class::Text::new().with_color(class::TextColor::White) -/// .with_opacity(OpacityLevel::SemiTransparent); +/// let s = class::Text::new().with_color(token::ColorText::White) +/// .with_opacity(token::Opacity::SemiTransparent); /// assert_eq!(s.to_class(), "text-white text-opacity-25"); /// -/// // Usando `From`. -/// let s: class::Text = class::TextColor::Black.into(); +/// // Usando `From`. +/// let s: class::Text = token::ColorText::Black.into(); /// assert_eq!(s.to_class(), "text-black"); /// -/// // Usando `From<(TextColor, OpacityLevel)>`. +/// // Usando `From<(ColorText, Opacity)>`. /// let s: class::Text = ( -/// class::TextColor::Solid(ThemeColor::Danger), -/// OpacityLevel::Opaque, +/// token::ColorText::Theme(token::Color::Danger), +/// token::Opacity::Opaque, /// ).into(); /// assert_eq!(s.to_class(), "text-danger text-opacity-100"); /// ``` #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub struct Text { - color: TextColor, - opacity: OpacityLevel, + color: ColorText, + opacity: Opacity, } impl Text { @@ -441,20 +187,20 @@ impl Text { /// Crea un estilo fijando el color del texto (`text-*`). /// - /// Acepta cualquier tipo convertible en [`TextColor`]. Un [`ThemeColor`] se convierte - /// automáticamente en [`TextColor::Solid`]: + /// Acepta cualquier tipo convertible en [`ColorText`]. Un [`Color`](crate::theme::token::Color) + /// se convierte automáticamente en [`ColorText::Theme`]: /// /// ```rust /// # use pagetop_bootsier::theme::*; - /// // Forma corta con ThemeColor: - /// let s = class::Text::with(ThemeColor::Danger); + /// // Forma corta con Color: + /// let s = class::Text::with(token::Color::Danger); /// assert_eq!(s.to_class(), "text-danger"); /// /// // Forma explícita para variantes no temáticas: - /// let s = class::Text::with(class::TextColor::Body); + /// let s = class::Text::with(token::ColorText::Body); /// assert_eq!(s.to_class(), "text-body"); /// ``` - pub fn with(color: impl Into) -> Self { + pub fn with(color: impl Into) -> Self { Self::default().with_color(color) } @@ -462,15 +208,15 @@ impl Text { /// Establece el color del texto (`text-*`). /// - /// Acepta cualquier tipo convertible en [`TextColor`]. Un [`ThemeColor`] se convierte - /// automáticamente en [`TextColor::Solid`]. - pub fn with_color(mut self, color: impl Into) -> Self { + /// Acepta cualquier tipo convertible en [`ColorText`]. Un [`Color`](crate::theme::token::Color) + /// se convierte automáticamente en [`ColorText::Theme`]. + pub fn with_color(mut self, color: impl Into) -> Self { self.color = color.into(); self } /// Establece la opacidad del texto (`text-opacity-*`). - pub fn with_opacity(mut self, opacity: OpacityLevel) -> Self { + pub fn with_opacity(mut self, opacity: Opacity) -> Self { self.opacity = opacity; self } @@ -495,7 +241,7 @@ impl Text { } } -impl From<(TextColor, OpacityLevel)> for Text { +impl From<(ColorText, Opacity)> for Text { /// Crea la clase para [`Text`](crate::theme::class::Text) a partir del color del texto y su /// opacidad. /// @@ -504,27 +250,27 @@ impl From<(TextColor, OpacityLevel)> for Text { /// ```rust /// # use pagetop_bootsier::theme::*; /// let s: class::Text = ( - /// class::TextColor::Solid(ThemeColor::Danger), - /// OpacityLevel::Opaque, + /// token::ColorText::Theme(token::Color::Danger), + /// token::Opacity::Opaque, /// ).into(); /// assert_eq!(s.to_class(), "text-danger text-opacity-100"); /// ``` - fn from((color, opacity): (TextColor, OpacityLevel)) -> Self { + fn from((color, opacity): (ColorText, Opacity)) -> Self { Text::with(color).with_opacity(opacity) } } -impl From for Text { +impl From for Text { /// Crea la clase para [`Text`](crate::theme::class::Text) a partir del color del texto. /// /// # Ejemplo /// /// ```rust /// # use pagetop_bootsier::theme::*; - /// let s: class::Text = class::TextColor::Black.into(); + /// let s: class::Text = token::ColorText::Black.into(); /// assert_eq!(s.to_class(), "text-black"); /// ``` - fn from(color: TextColor) -> Self { + fn from(color: ColorText) -> Self { Text::with(color) } } diff --git a/extensions/pagetop-bootsier/src/theme/class/layout.rs b/extensions/pagetop-bootsier/src/theme/class/layout.rs index 92b1906a..f00ffaba 100644 --- a/extensions/pagetop-bootsier/src/theme/class/layout.rs +++ b/extensions/pagetop-bootsier/src/theme/class/layout.rs @@ -1,6 +1,6 @@ use pagetop::prelude::*; -use crate::theme::{BoxSide, BreakPoint, ScaleSize}; +use crate::theme::token::{BreakPoint, ScaleSize, Side}; // **< Margin >************************************************************************************* @@ -11,19 +11,19 @@ use crate::theme::{BoxSide, BreakPoint, ScaleSize}; /// ```rust /// use pagetop_bootsier::theme::*; /// -/// let m = class::Margin::with(BoxSide::Top, ScaleSize::Three); +/// let m = class::Margin::with(token::Side::Top, token::ScaleSize::Three); /// assert_eq!(m.to_class(), "mt-3"); /// -/// let m = class::Margin::with(BoxSide::Start, ScaleSize::Auto) -/// .with_breakpoint(BreakPoint::LG); +/// let m = class::Margin::with(token::Side::Start, token::ScaleSize::Auto) +/// .with_breakpoint(token::BreakPoint::LG); /// assert_eq!(m.to_class(), "ms-lg-auto"); /// -/// let m = class::Margin::with(BoxSide::All, ScaleSize::None); +/// let m = class::Margin::with(token::Side::All, token::ScaleSize::None); /// assert_eq!(m.to_class(), ""); /// ``` #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub struct Margin { - side: BoxSide, + side: Side, size: ScaleSize, breakpoint: BreakPoint, } @@ -31,7 +31,7 @@ pub struct Margin { impl Margin { /// Crea un **margin** indicando lado(s) y tamaño. Por defecto no se aplica a ningún punto de /// ruptura. - pub fn with(side: BoxSide, size: ScaleSize) -> Self { + pub fn with(side: Side, size: ScaleSize) -> Self { Margin { side, size, @@ -54,13 +54,13 @@ impl Margin { #[inline] const fn side_prefix(&self) -> &'static str { match self.side { - BoxSide::All => "m", - BoxSide::Top => "mt", - BoxSide::Bottom => "mb", - BoxSide::Start => "ms", - BoxSide::End => "me", - BoxSide::LeftAndRight => "mx", - BoxSide::TopAndBottom => "my", + Side::All => "m", + Side::Top => "mt", + Side::Bottom => "mb", + Side::Start => "ms", + Side::End => "me", + Side::LeftAndRight => "mx", + Side::TopAndBottom => "my", } } @@ -114,19 +114,19 @@ impl Into for Margin { /// ```rust /// use pagetop_bootsier::theme::*; /// -/// let p = class::Padding::with(BoxSide::LeftAndRight, ScaleSize::Two); +/// let p = class::Padding::with(token::Side::LeftAndRight, token::ScaleSize::Two); /// assert_eq!(p.to_class(), "px-2"); /// -/// let p = class::Padding::with(BoxSide::End, ScaleSize::Four) -/// .with_breakpoint(BreakPoint::SM); +/// let p = class::Padding::with(token::Side::End, token::ScaleSize::Four) +/// .with_breakpoint(token::BreakPoint::SM); /// assert_eq!(p.to_class(), "pe-sm-4"); /// -/// let p = class::Padding::with(BoxSide::All, ScaleSize::Auto); +/// let p = class::Padding::with(token::Side::All, token::ScaleSize::Auto); /// assert_eq!(p.to_class(), ""); // `Auto` no aplica a padding. /// ``` #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub struct Padding { - side: BoxSide, + side: Side, size: ScaleSize, breakpoint: BreakPoint, } @@ -134,7 +134,7 @@ pub struct Padding { impl Padding { /// Crea un **padding** indicando lado(s) y tamaño. Por defecto no se aplica a ningún punto de /// ruptura. - pub fn with(side: BoxSide, size: ScaleSize) -> Self { + pub fn with(side: Side, size: ScaleSize) -> Self { Padding { side, size, @@ -157,13 +157,13 @@ impl Padding { #[inline] const fn side_prefix(&self) -> &'static str { match self.side { - BoxSide::All => "p", - BoxSide::Top => "pt", - BoxSide::Bottom => "pb", - BoxSide::Start => "ps", - BoxSide::End => "pe", - BoxSide::LeftAndRight => "px", - BoxSide::TopAndBottom => "py", + Side::All => "p", + Side::Top => "pt", + Side::Bottom => "pb", + Side::Start => "ps", + Side::End => "pe", + Side::LeftAndRight => "px", + Side::TopAndBottom => "py", } } diff --git a/extensions/pagetop-bootsier/src/theme/class/rounded.rs b/extensions/pagetop-bootsier/src/theme/class/rounded.rs index 4bd497eb..188aaf97 100644 --- a/extensions/pagetop-bootsier/src/theme/class/rounded.rs +++ b/extensions/pagetop-bootsier/src/theme/class/rounded.rs @@ -1,98 +1,6 @@ use pagetop::prelude::*; -// **< RoundedRadius >****************************************************************************** - -/// Radio para el redondeo de esquinas ([`Rounded`]). -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum RoundedRadius { - /// No define ninguna clase. - #[default] - None, - /// Genera `rounded` (radio por defecto del tema). - Default, - /// Genera `rounded-0` (sin redondeo). - Zero, - /// Genera `rounded-1`. - Scale1, - /// Genera `rounded-2`. - Scale2, - /// Genera `rounded-3`. - Scale3, - /// Genera `rounded-4`. - Scale4, - /// Genera `rounded-5`. - Scale5, - /// Genera `rounded-circle`. - Circle, - /// Genera `rounded-pill`. - Pill, -} - -impl RoundedRadius { - // Devuelve el sufijo para `*rounded-*`, o `None` si no define ninguna clase, o `""` para el - // redondeo por defecto. - #[rustfmt::skip] - #[inline] - const fn suffix(self) -> Option<&'static str> { - match self { - Self::None => None, - Self::Default => Some(""), - Self::Zero => Some("-0"), - Self::Scale1 => Some("-1"), - Self::Scale2 => Some("-2"), - Self::Scale3 => Some("-3"), - Self::Scale4 => Some("-4"), - Self::Scale5 => Some("-5"), - Self::Circle => Some("-circle"), - Self::Pill => Some("-pill"), - } - } - - /// Añade el redondeo de esquinas a la cadena de clases usando el prefijo dado (`rounded-top`, - /// `rounded-bottom-start`, o vacío para `rounded-*`). - #[inline] - pub fn push_to(self, classes: &mut String, prefix: &str) { - if let Some(suffix) = self.suffix() { - if !classes.is_empty() { - classes.push(' '); - } - if prefix.is_empty() { - classes.push_str("rounded"); - } else { - classes.push_str(prefix); - } - classes.push_str(suffix); - } - } - - /// Devuelve la clase `rounded-*` para el redondeo de esquinas. - /// - /// # Ejemplos - /// - /// ```rust - /// # use pagetop_bootsier::theme::*; - /// assert_eq!(class::RoundedRadius::Default.to_class(), "rounded"); - /// assert_eq!(class::RoundedRadius::Zero.to_class(), "rounded-0"); - /// assert_eq!(class::RoundedRadius::Scale3.to_class(), "rounded-3"); - /// assert_eq!(class::RoundedRadius::Circle.to_class(), "rounded-circle"); - /// assert_eq!(class::RoundedRadius::None.to_class(), ""); - /// ``` - #[inline] - pub fn to_class(self) -> String { - let mut class = String::new(); - self.push_to(&mut class, ""); - class - } -} - -impl Into for RoundedRadius { - /// Permite pasar [`RoundedRadius`] directamente a [`PropsOp`](pagetop::prelude::PropsOp). - fn into(self) -> CowStr { - self.to_class().into() - } -} - -// **< Rounded >************************************************************************************ +use crate::theme::token::RoundedRadius; /// Clases para definir **esquinas redondeadas**. /// @@ -110,7 +18,7 @@ impl Into for RoundedRadius { /// use pagetop_bootsier::theme::*; /// /// // Radio global: -/// let r = class::Rounded::with(class::RoundedRadius::Default); +/// let r = class::Rounded::with(token::RoundedRadius::Default); /// assert_eq!(r.to_class(), "rounded"); /// /// // Sin redondeo: @@ -118,18 +26,18 @@ impl Into for RoundedRadius { /// assert_eq!(r.to_class(), ""); /// /// // Radio en las esquinas de un lado lógico: -/// let r = class::Rounded::new().with_end(class::RoundedRadius::Scale2); +/// let r = class::Rounded::new().with_end(token::RoundedRadius::Scale2); /// assert_eq!(r.to_class(), "rounded-end-2"); /// /// // Radio en una esquina concreta: -/// let r = class::Rounded::new().with_top_start(class::RoundedRadius::Scale3); +/// let r = class::Rounded::new().with_top_start(token::RoundedRadius::Scale3); /// assert_eq!(r.to_class(), "rounded-top-start-3"); /// /// // Combinado (ejemplo completo): /// let r = class::Rounded::new() -/// .with_top(class::RoundedRadius::Default) // Añade redondeo arriba. -/// .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. +/// .with_top(token::RoundedRadius::Default) // Añade redondeo arriba. +/// .with_bottom_start(token::RoundedRadius::Scale4) // Añade una esquina redondeada concreta. +/// .with_bottom_end(token::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] diff --git a/extensions/pagetop-bootsier/src/theme/token.rs b/extensions/pagetop-bootsier/src/theme/token.rs index 6fdaf93e..9cb7c386 100644 --- a/extensions/pagetop-bootsier/src/theme/token.rs +++ b/extensions/pagetop-bootsier/src/theme/token.rs @@ -1,10 +1,28 @@ //! Tipos enumerados para construir clases del tema. +//! +//! Incluyen puntos de ruptura, colores, escalas de tamaño, lados, etc. Se pueden importar +//! globalmente junto al resto del tema: +//! +//! ```rust,no_run +//! use pagetop_bootsier::theme::*; +//! +//! let bg = class::Background::with(token::Color::Primary); +//! let border = class::Border::new() +//! .with_side(token::Side::Top, token::ScaleSize::Zero) +//! .with_color(token::Color::Danger); +//! ``` mod breakpoint; pub use breakpoint::BreakPoint; mod color; -pub use color::{OpacityLevel, ThemeColor}; +pub use color::{Color, ColorBg, ColorText, Opacity}; + +mod border; +pub use border::ColorBorder; + +mod rounded; +pub use rounded::RoundedRadius; mod layout; -pub use layout::{BoxSide, ScaleSize}; +pub use layout::{ScaleSize, Side}; diff --git a/extensions/pagetop-bootsier/src/theme/token/border.rs b/extensions/pagetop-bootsier/src/theme/token/border.rs new file mode 100644 index 00000000..c1c6d0d2 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/token/border.rs @@ -0,0 +1,103 @@ +use pagetop::prelude::*; + +use crate::theme::token::Color; + +/// Esquema de color para los bordes ([`Border`](crate::theme::class::Border)). +/// +/// - `Theme(Color)` y `Subtle(Color)` usan la paleta de colores temáticos ([`Color`]). +/// - `Black` y `White` son colores fijos independientes del tema. +/// - `Default` no genera ninguna clase. +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub enum ColorBorder { + /// No define ninguna clase. + #[default] + Default, + /// Genera la clase `border-{color}`. + Theme(Color), + /// Genera la clase `border-{color}-subtle` (un tono suavizado del color). + Subtle(Color), + /// Color negro. + Black, + /// Color blanco. + White, +} + +impl ColorBorder { + // Devuelve el sufijo de la clase `border-*`, o `None` si no define ninguna clase. + #[rustfmt::skip] + #[inline] + const fn suffix(self) -> Option<&'static str> { + match self { + Self::Default => None, + Self::Theme(_) => Some(""), + Self::Subtle(_) => Some("-subtle"), + Self::Black => Some("-black"), + Self::White => Some("-white"), + } + } + + // Añade la clase `border-*` a la cadena de clases. + #[inline] + pub(crate) fn push_to(self, classes: &mut String) { + if let Some(suffix) = self.suffix() { + if !classes.is_empty() { + classes.push(' '); + } + match self { + Self::Theme(c) | Self::Subtle(c) => { + classes.push_str("border-"); + classes.push_str(c.as_str()); + } + _ => classes.push_str("border"), + } + classes.push_str(suffix); + } + } + + /// Devuelve la clase `border-*` correspondiente al color de borde. + /// + /// # Ejemplos + /// + /// ```rust + /// # use pagetop_bootsier::theme::*; + /// let theme = token::ColorBorder::Theme(token::Color::Primary).to_class(); + /// assert_eq!(theme, "border-primary"); + /// + /// let subtle = token::ColorBorder::Subtle(token::Color::Warning).to_class(); + /// assert_eq!(subtle, "border-warning-subtle"); + /// + /// let black = token::ColorBorder::Black.to_class(); + /// assert_eq!(black, "border-black"); + /// + /// let none = token::ColorBorder::Default.to_class(); + /// assert_eq!(none, ""); + /// ``` + #[inline] + pub fn to_class(self) -> String { + let mut class = String::new(); + self.push_to(&mut class); + class + } +} + +impl From for ColorBorder { + /// Convierte un [`Color`] en [`ColorBorder::Theme`]. + /// + /// Permite pasar un [`Color`] directamente donde se espera un [`ColorBorder`], sin necesidad + /// de envolver el valor en [`ColorBorder::Theme`]. Es el atajo habitual para los colores + /// temáticos. + /// + /// Para los demás esquemas (`Subtle`, `Black`, `White`) se sigue usando [`ColorBorder`] + /// directamente. + /// + /// # Ejemplo + /// + /// ```rust + /// # use pagetop_bootsier::theme::*; + /// let border: token::ColorBorder = token::Color::Success.into(); + /// assert_eq!(border.to_class(), "border-success"); + /// ``` + fn from(color: Color) -> Self { + Self::Theme(color) + } +} diff --git a/extensions/pagetop-bootsier/src/theme/token/breakpoint.rs b/extensions/pagetop-bootsier/src/theme/token/breakpoint.rs index 31310760..15ecb6f1 100644 --- a/extensions/pagetop-bootsier/src/theme/token/breakpoint.rs +++ b/extensions/pagetop-bootsier/src/theme/token/breakpoint.rs @@ -1,6 +1,6 @@ use pagetop::prelude::*; -/// Puntos de ruptura (*breakpoints*) para aplicar diseño *responsive*. +/// Define los puntos de ruptura (*breakpoints*) para aplicar diseño *responsive*. #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum BreakPoint { /// **Menos de 576px**. Dispositivos muy pequeños: teléfonos en modo vertical. diff --git a/extensions/pagetop-bootsier/src/theme/token/color.rs b/extensions/pagetop-bootsier/src/theme/token/color.rs index e47e8219..cf90288a 100644 --- a/extensions/pagetop-bootsier/src/theme/token/color.rs +++ b/extensions/pagetop-bootsier/src/theme/token/color.rs @@ -1,17 +1,15 @@ use pagetop::prelude::*; -// **< ThemeColor >********************************************************************************* +// **< Color >************************************************************************************** /// Paleta de colores temáticos. /// /// Equivalen a los nombres estándar definidos por Bootstrap (`primary`, `secondary`, `success`, -/// etc.). Se utiliza para componer las clases de color de [`Bg`], [`Border`] o [`Text`]. -/// -/// [`Bg`]: crate::theme::class::Bg -/// [`Border`]: crate::theme::class::Border -/// [`Text`]: crate::theme::class::Text +/// etc.). Este tipo enumerado sirve de referencia para componer las clases de color para el fondo +/// ([`Background`](crate::theme::class::Background)), bordes +/// ([`Border`](crate::theme::class::Border)) o texto ([`Text`](crate::theme::class::Text)). #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum ThemeColor { +pub enum Color { #[default] Primary, Secondary, @@ -23,7 +21,7 @@ pub enum ThemeColor { Dark, } -impl ThemeColor { +impl Color { /// Devuelve el nombre del color Bootstrap (`"primary"`, `"danger"`, etc.). #[rustfmt::skip] #[inline] @@ -41,18 +39,16 @@ impl ThemeColor { } } -// **< OpacityLevel >******************************************************************************* +// **< Opacity >************************************************************************************ /// Niveles de opacidad (`opacity-*`). /// -/// Se utiliza para las clases que definen la transparencia de [`Bg`] (`bg-opacity-*`), [`Border`] -/// (`border-opacity-*`) o [`Text`] (`text-opacity-*`). -/// -/// [`Bg`]: crate::theme::class::Bg -/// [`Border`]: crate::theme::class::Border -/// [`Text`]: crate::theme::class::Text +/// Se usa normalmente para graduar la transparencia del color de fondo `bg-opacity-*` +/// ([`Background`](crate::theme::class::Background)), de los bordes `border-opacity-*` +/// ([`Border`](crate::theme::class::Border)) o del texto `text-opacity-*` +/// ([`Text`](crate::theme::class::Text)). #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum OpacityLevel { +pub enum Opacity { /// No define ninguna clase. #[default] Default, @@ -70,7 +66,7 @@ pub enum OpacityLevel { Transparent, } -impl OpacityLevel { +impl Opacity { // Devuelve el sufijo para `*opacity-*`, o `None` si no define ninguna clase. #[rustfmt::skip] #[inline] @@ -110,9 +106,9 @@ impl OpacityLevel { /// /// ```rust /// # use pagetop_bootsier::theme::*; - /// assert_eq!(OpacityLevel::Opaque.to_class(), "opacity-100"); - /// assert_eq!(OpacityLevel::Half.to_class(), "opacity-50"); - /// assert_eq!(OpacityLevel::Default.to_class(), ""); + /// assert_eq!(token::Opacity::Opaque.to_class(), "opacity-100"); + /// assert_eq!(token::Opacity::Half.to_class(), "opacity-50"); + /// assert_eq!(token::Opacity::Default.to_class(), ""); /// ``` #[inline] pub fn to_class(self) -> String { @@ -121,3 +117,237 @@ impl OpacityLevel { class } } + +// **< ColorBg >************************************************************************************ + +/// Esquema de color para el fondo ([`Background`](crate::theme::class::Background)). +/// +/// - `Body`, `BodySecondary` y `BodyTertiary` siguen el esquema del tema (claro/oscuro). +/// - `Theme(Color)` y `Subtle(Color)` usan la paleta de colores temáticos ([`Color`]). +/// - `Black`, `White`, `Transparent` son colores fijos independientes del tema. +/// - `Default` no genera ninguna clase. +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub enum ColorBg { + /// No define ninguna clase. + #[default] + Default, + /// Fondo predefinido del tema (`bg-body`). + Body, + /// Fondo predefinido del tema (`bg-body-secondary`). + BodySecondary, + /// Fondo predefinido del tema (`bg-body-tertiary`). + BodyTertiary, + /// Genera la clase `bg-{color}` (p. ej., `bg-primary`). + Theme(Color), + /// Genera la clase `bg-{color}-subtle` (un tono suavizado del color). + Subtle(Color), + /// Color negro. + Black, + /// Color blanco. + White, + /// Fondo transparente (`bg-transparent`). + Transparent, +} + +impl ColorBg { + // Devuelve el sufijo de la clase `bg-*`, o `None` si no define ninguna clase. + #[rustfmt::skip] + #[inline] + const fn suffix(self) -> Option<&'static str> { + match self { + Self::Default => None, + Self::Body => Some("-body"), + Self::BodySecondary => Some("-body-secondary"), + Self::BodyTertiary => Some("-body-tertiary"), + Self::Theme(_) => Some(""), + Self::Subtle(_) => Some("-subtle"), + Self::Black => Some("-black"), + Self::White => Some("-white"), + Self::Transparent => Some("-transparent"), + } + } + + // Añade la clase de fondo `bg-*` a la cadena de clases. + #[inline] + pub(crate) fn push_to(self, classes: &mut String) { + if let Some(suffix) = self.suffix() { + if !classes.is_empty() { + classes.push(' '); + } + match self { + Self::Theme(c) | Self::Subtle(c) => { + classes.push_str("bg-"); + classes.push_str(c.as_str()); + } + _ => classes.push_str("bg"), + } + classes.push_str(suffix); + } + } + + /// Devuelve la clase `bg-*` correspondiente al fondo. + /// + /// # Ejemplos + /// + /// ```rust + /// # use pagetop_bootsier::theme::*; + /// let body = token::ColorBg::Body.to_class(); + /// assert_eq!(body, "bg-body"); + /// + /// let theme = token::ColorBg::Theme(token::Color::Primary).to_class(); + /// assert_eq!(theme, "bg-primary"); + /// + /// let subtle = token::ColorBg::Subtle(token::Color::Warning).to_class(); + /// assert_eq!(subtle, "bg-warning-subtle"); + /// + /// let transparent = token::ColorBg::Transparent.to_class(); + /// assert_eq!(transparent, "bg-transparent"); + /// + /// let none = token::ColorBg::Default.to_class(); + /// assert_eq!(none, ""); + /// ``` + #[inline] + pub fn to_class(self) -> String { + let mut class = String::new(); + self.push_to(&mut class); + class + } +} + +impl From for ColorBg { + /// Convierte un [`Color`] en [`ColorBg::Theme`]. + /// + /// Permite pasar un [`Color`] directamente donde se espera un [`ColorBg`], sin necesidad de + /// envolver el valor en [`ColorBg::Theme`]. Es el atajo habitual para los colores temáticos. + /// + /// Para los demás esquemas (`Body`, `Subtle`, `Black`, etc.) se sigue usando [`ColorBg`] + /// directamente. + /// + /// # Ejemplo + /// + /// ```rust + /// # use pagetop_bootsier::theme::*; + /// let bg: token::ColorBg = token::Color::Primary.into(); + /// assert_eq!(bg.to_class(), "bg-primary"); + /// ``` + fn from(color: Color) -> Self { + Self::Theme(color) + } +} + +// **< ColorText >********************************************************************************** + +/// Esquema de color para el texto ([`Text`](crate::theme::class::Text)). +/// +/// - `Body`, `BodyEmphasis`, `BodySecondary` y `BodyTertiary` siguen el tema (claro/oscuro). +/// - `Theme(Color)` y `Emphasis(Color)` usan la paleta de colores temáticos ([`Color`]). +/// - `Black` y `White` son colores fijos independientes del tema. +/// - `Default` no genera ninguna clase. +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub enum ColorText { + /// No define ninguna clase. + #[default] + Default, + /// Color predefinido del tema (`text-body`). + Body, + /// Color de mayor contraste según el tema (`text-body-emphasis`). + BodyEmphasis, + /// Color predefinido del tema (`text-body-secondary`). + BodySecondary, + /// Color predefinido del tema (`text-body-tertiary`). + BodyTertiary, + /// Genera la clase `text-{color}`. + Theme(Color), + /// Genera la clase `text-{color}-emphasis` (mayor contraste acorde al tema). + Emphasis(Color), + /// Color negro. + Black, + /// Color blanco. + White, +} + +impl ColorText { + // Devuelve el sufijo de la clase `text-*`, o `None` si no define ninguna clase. + #[rustfmt::skip] + #[inline] + const fn suffix(self) -> Option<&'static str> { + match self { + Self::Default => None, + Self::Body => Some("-body"), + Self::BodyEmphasis => Some("-body-emphasis"), + Self::BodySecondary => Some("-body-secondary"), + Self::BodyTertiary => Some("-body-tertiary"), + Self::Theme(_) => Some(""), + Self::Emphasis(_) => Some("-emphasis"), + Self::Black => Some("-black"), + Self::White => Some("-white"), + } + } + + // Añade la clase de texto `text-*` a la cadena de clases. + #[inline] + pub(crate) fn push_to(self, classes: &mut String) { + if let Some(suffix) = self.suffix() { + if !classes.is_empty() { + classes.push(' '); + } + match self { + Self::Theme(c) | Self::Emphasis(c) => { + classes.push_str("text-"); + classes.push_str(c.as_str()); + } + _ => classes.push_str("text"), + } + classes.push_str(suffix); + } + } + + /// Devuelve la clase `text-*` correspondiente al color del texto. + /// + /// # Ejemplos + /// + /// ```rust + /// # use pagetop_bootsier::theme::*; + /// let body = token::ColorText::Body.to_class(); + /// assert_eq!(body, "text-body"); + /// + /// let theme = token::ColorText::Theme(token::Color::Primary).to_class(); + /// assert_eq!(theme, "text-primary"); + /// + /// let emphasis = token::ColorText::Emphasis(token::Color::Danger).to_class(); + /// assert_eq!(emphasis, "text-danger-emphasis"); + /// + /// let black = token::ColorText::Black.to_class(); + /// assert_eq!(black, "text-black"); + /// + /// let none = token::ColorText::Default.to_class(); + /// assert_eq!(none, ""); + /// ``` + #[inline] + pub fn to_class(self) -> String { + let mut class = String::new(); + self.push_to(&mut class); + class + } +} + +impl From for ColorText { + /// Convierte un [`Color`] en [`ColorText::Theme`]. + /// + /// Permite pasar un [`Color`] directamente donde se espera un [`ColorText`], sin necesidad de + /// envolver el valor en [`ColorText::Theme`]. Es el atajo habitual para los colores temáticos. + /// + /// Para los demás esquemas (`Body`, `Emphasis`, `Black`, etc.) se sigue usando [`ColorText`] + /// directamente. + /// + /// # Ejemplo + /// + /// ```rust + /// # use pagetop_bootsier::theme::*; + /// let text: token::ColorText = token::Color::Danger.into(); + /// assert_eq!(text.to_class(), "text-danger"); + /// ``` + fn from(color: Color) -> Self { + Self::Theme(color) + } +} diff --git a/extensions/pagetop-bootsier/src/theme/token/layout.rs b/extensions/pagetop-bootsier/src/theme/token/layout.rs index a5df6b7c..2e4f8bd6 100644 --- a/extensions/pagetop-bootsier/src/theme/token/layout.rs +++ b/extensions/pagetop-bootsier/src/theme/token/layout.rs @@ -2,14 +2,11 @@ use pagetop::prelude::*; // **< ScaleSize >********************************************************************************** -/// Escala discreta de tamaños para clases utilitarias. +/// Escala discreta de tamaños para clases utilitarias de Bootstrap. /// -/// Se usa como parámetro de tamaño para las clases de [`Border`], [`Margin`] y [`Padding`]. La +/// Se usa como parámetro de tamaño en [`Border`](crate::theme::class::Border), +/// [`Margin`](crate::theme::class::Margin) y [`Padding`](crate::theme::class::Padding). La /// variante `Auto` no aplica en `Padding`. -/// -/// [`Border`]: crate::theme::class::Border -/// [`Margin`]: crate::theme::class::Margin -/// [`Padding`]: crate::theme::class::Padding #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum ScaleSize { /// Sin tamaño (no define ninguna clase). @@ -64,17 +61,14 @@ impl ScaleSize { } } -// **< BoxSide >************************************************************************************ +// **< Side >*************************************************************************************** /// Lados sobre los que aplicar una clase utilitaria (respetando LTR/RTL). /// -/// Se usa como selector de lado para las clases de [`Border`], [`Margin`] y [`Padding`]. -/// -/// [`Border`]: crate::theme::class::Border -/// [`Margin`]: crate::theme::class::Margin -/// [`Padding`]: crate::theme::class::Padding +/// Se usa como selector de lado en [`Border`](crate::theme::class::Border), +/// [`Margin`](crate::theme::class::Margin) y [`Padding`](crate::theme::class::Padding). #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum BoxSide { +pub enum Side { /// Todos los lados. #[default] All, diff --git a/extensions/pagetop-bootsier/src/theme/token/rounded.rs b/extensions/pagetop-bootsier/src/theme/token/rounded.rs new file mode 100644 index 00000000..c1621b09 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/token/rounded.rs @@ -0,0 +1,84 @@ +use pagetop::prelude::*; + +/// Radio para el redondeo de esquinas ([`Rounded`](crate::theme::class::Rounded)). +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub enum RoundedRadius { + /// No define ninguna clase. + #[default] + None, + /// Genera `rounded` (radio por defecto del tema). + Default, + /// Genera `rounded-0` (sin redondeo). + Zero, + /// Genera `rounded-1`. + Scale1, + /// Genera `rounded-2`. + Scale2, + /// Genera `rounded-3`. + Scale3, + /// Genera `rounded-4`. + Scale4, + /// Genera `rounded-5`. + Scale5, + /// Genera `rounded-circle`. + Circle, + /// Genera `rounded-pill`. + Pill, +} + +impl RoundedRadius { + // Devuelve el sufijo para `*rounded-*`, o `None` si no define ninguna clase, o `""` para el + // redondeo por defecto. + #[rustfmt::skip] + #[inline] + const fn suffix(self) -> Option<&'static str> { + match self { + Self::None => None, + Self::Default => Some(""), + Self::Zero => Some("-0"), + Self::Scale1 => Some("-1"), + Self::Scale2 => Some("-2"), + Self::Scale3 => Some("-3"), + Self::Scale4 => Some("-4"), + Self::Scale5 => Some("-5"), + Self::Circle => Some("-circle"), + Self::Pill => Some("-pill"), + } + } + + // Añade el redondeo de esquinas a la cadena de clases usando el prefijo dado (`rounded-top`, + // `rounded-bottom-start`, o vacío para `rounded-*`). + #[inline] + pub(crate) fn push_to(self, classes: &mut String, prefix: &str) { + if let Some(suffix) = self.suffix() { + if !classes.is_empty() { + classes.push(' '); + } + if prefix.is_empty() { + classes.push_str("rounded"); + } else { + classes.push_str(prefix); + } + classes.push_str(suffix); + } + } + + /// Devuelve la clase `rounded-*` para el redondeo de esquinas. + /// + /// # Ejemplos + /// + /// ```rust + /// # use pagetop_bootsier::theme::*; + /// assert_eq!(token::RoundedRadius::Default.to_class(), "rounded"); + /// assert_eq!(token::RoundedRadius::Zero.to_class(), "rounded-0"); + /// assert_eq!(token::RoundedRadius::Scale3.to_class(), "rounded-3"); + /// assert_eq!(token::RoundedRadius::Circle.to_class(), "rounded-circle"); + /// assert_eq!(token::RoundedRadius::None.to_class(), ""); + /// ``` + #[inline] + pub fn to_class(self) -> String { + let mut class = String::new(); + self.push_to(&mut class, ""); + class + } +} diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 6b1af5d2..e988b6a3 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -455,7 +455,7 @@ impl Context { pub fn route(&self, path: impl Into) -> RoutePath { let mut route = path.into(); if !route.is_external() && self.locale.needs_lang_query() { - route.alter_param("lang", self.locale.lang_query_value()); + route.alter_param("lang", self.locale.langid().to_string()); } route } diff --git a/src/locale/request.rs b/src/locale/request.rs index 09404015..7f5167f0 100644 --- a/src/locale/request.rs +++ b/src/locale/request.rs @@ -7,53 +7,45 @@ use super::{LangId, LanguageIdentifier, Locale}; /// /// Determina qué idioma se usará para renderizar la respuesta asociada a una petición. También /// indica si es necesario propagar ese idioma en los enlaces usando el parámetro de *query* -/// `?lang=...`. El comportamiento concreto depende de la política global [`LangNegotiation`] -/// configurada en la aplicación. +/// `?lang=...`. El comportamiento concreto depende de la política global +/// [`LangNegotiation`](crate::global::LangNegotiation) configurada en la aplicación. /// /// El idioma resultante se expone a través del *trait* [`LangId`], de modo que pueda usarse /// [`RequestLocale`] como cualquier otra fuente de idioma en PageTop. -/// -/// [`LangNegotiation`]: crate::global::LangNegotiation pub struct RequestLocale { // Idioma elegido por la aplicación para esta petición, combinando la configuración, la cabecera // `Accept-Language` y/o el idioma de respaldo. base: &'static LanguageIdentifier, // Idioma finalmente aplicado a la petición (puede coincidir con `base` o no). effective: &'static LanguageIdentifier, - // Valor original de `?lang=...`, tal como llegó en la petición, si resultó en un idioma - // soportado. Permite propagarlo en los enlaces con su grafía original (p. ej. "en") en vez de - // expandirlo siempre a la forma canónica del idioma resuelto (p. ej. "en-US"). - query_lang: Option, } impl RequestLocale { /// Construye un `RequestLocale` a partir de una petición HTTP. /// - /// El idioma de la petición se decide según la estrategia definida por [`LangNegotiation`]: + /// El idioma de la petición se decide según la estrategia definida por + /// [`LangNegotiation`](crate::global::LangNegotiation): /// - /// - [`LangNegotiation::Full`] determina el idioma en este orden: + /// - [`LangNegotiation::Full`](crate::global::LangNegotiation::Full) determina el idioma en + /// este orden: /// 1. Parámetro de *query* `?lang=...`, si existe y corresponde a un idioma soportado. /// 2. [`Locale::try_langid()`], si la aplicación tiene un idioma por defecto válido. /// 3. Cabecera `Accept-Language`, si puede resolverse con [`Locale::resolve()`]. /// 4. Idioma de respaldo. /// - /// - [`LangNegotiation::NoQuery`] descarta el uso del parámetro `?lang=...` y determina el - /// idioma en este orden: + /// - [`LangNegotiation::NoQuery`](crate::global::LangNegotiation::NoQuery) descarta el uso del + /// parámetro `?lang=...` y determina el idioma en este orden: /// 1. [`Locale::try_langid()`], si la aplicación tiene un idioma por defecto válido. /// 2. Cabecera `Accept-Language`, si puede resolverse con [`Locale::resolve()`]. /// 3. Idioma de respaldo. /// - /// - [`LangNegotiation::ConfigOnly`] sólo usa la configuración de la aplicación mediante - /// [`Locale::default_langid()`], sin consultar la cabecera `Accept-Language` ni el parámetro - /// `?lang`. Este modo también aplica el idioma de respaldo si es necesario. + /// - [`LangNegotiation::ConfigOnly`](crate::global::LangNegotiation::ConfigOnly) sólo usa la + /// configuración de la aplicación mediante [`Locale::default_langid()`], sin consultar la + /// cabecera `Accept-Language` ni el parámetro `?lang`. Este modo también aplica el idioma de + /// respaldo si es necesario. /// /// En todos los casos, el idioma resultante es siempre un [`LanguageIdentifier`] soportado por /// la aplicación y será el que PageTop utilice para renderizar la respuesta de la petición. - /// - /// [`LangNegotiation`]: crate::global::LangNegotiation - /// [`LangNegotiation::Full`]: crate::global::LangNegotiation::Full - /// [`LangNegotiation::NoQuery`]: crate::global::LangNegotiation::NoQuery - /// [`LangNegotiation::ConfigOnly`]: crate::global::LangNegotiation::ConfigOnly pub fn from_request(request: Option<&HttpRequest>) -> Self { let mode = global::SETTINGS.app.lang_negotiation; @@ -106,44 +98,38 @@ impl RequestLocale { } }; - // Valor de `?lang=...`, si existe y corresponde a un idioma soportado. Se conserva junto al - // `LanguageIdentifier` resuelto para poder propagar más tarde su grafía original. - let query_lang: Option<(&'static LanguageIdentifier, String)> = match mode { + // Idioma aplicado a la petición tras considerar la *query* `?lang=...`. + let effective: &'static LanguageIdentifier = match mode { global::LangNegotiation::ConfigOnly | global::LangNegotiation::NoQuery => { // En estos modos no se permite que la URL modifique el idioma. - None + base } - global::LangNegotiation::Full => request - // Se obtiene el valor de `lang` de la petición, si existe. - .and_then(|req| { - req.query_string().split('&').find_map(|pair| { - let mut param = pair.splitn(2, '='); - match (param.next(), param.next()) { - (Some("lang"), Some(value)) if !value.is_empty() => Some(value), - _ => None, + global::LangNegotiation::Full => { + request + // Se obtiene el valor de `lang` de la petición, si existe. + .and_then(|req| { + req.query_string().split('&').find_map(|pair| { + let mut param = pair.splitn(2, '='); + match (param.next(), param.next()) { + (Some("lang"), Some(value)) if !value.is_empty() => Some(value), + _ => None, + } + }) + }) + // Se comprueba si es un idioma soportado. + .and_then(|language| { + if let Locale::Resolved(langid) = Locale::resolve(language) { + Some(langid) + } else { + None } }) - }) - // Se comprueba si es un idioma soportado. - .and_then(|language| { - if let Locale::Resolved(langid) = Locale::resolve(language) { - Some((langid, language.to_string())) - } else { - None - } - }), + // Si no hay `lang` o no es válido, se usa `base`. + .unwrap_or(base) + } }; - // Idioma aplicado a la petición tras considerar la *query* `?lang=...`. Si no hay `lang` o - // no es válido, se usa `base`. - let effective: &'static LanguageIdentifier = - query_lang.as_ref().map_or(base, |(langid, _)| *langid); - - RequestLocale { - base, - effective, - query_lang: query_lang.map(|(_, raw)| raw), - } + RequestLocale { base, effective } } /// Fuerza el idioma que se utilizará para las traducciones de esta petición. @@ -154,29 +140,26 @@ impl RequestLocale { #[inline] pub fn with_langid(&mut self, language: &impl LangId) -> &mut Self { self.effective = language.langid(); - self.query_lang = None; self } /// Indica si conviene propagar `lang=...` en los enlaces generados. /// - /// El comportamiento depende de la estrategia configurada en [`LangNegotiation`]: + /// El comportamiento depende de la estrategia configurada en + /// [`LangNegotiation`](crate::global::LangNegotiation): /// - /// - En modo [`LangNegotiation::Full`] devuelve `true` cuando la respuesta se está generando en - /// un idioma distinto del que la aplicación habría elegido automáticamente a partir de la - /// configuración, el navegador y el idioma de respaldo. En la práctica suele significar que - /// el usuario ha pedido expresamente otro idioma (por ejemplo, con `?lang=...`) o que se ha - /// forzado con [`with_langid()`](Self::with_langid), y por tanto es recomendable propagar - /// `lang=...` en los enlaces para mantener esa preferencia mientras se navega. + /// - En modo [`LangNegotiation::Full`](crate::global::LangNegotiation::Full) devuelve `true` + /// cuando la respuesta se está generando en un idioma distinto del que la aplicación habría + /// elegido automáticamente a partir de la configuración, el navegador y el idioma de + /// respaldo. En la práctica suele significar que el usuario ha pedido expresamente otro + /// idioma (por ejemplo, con `?lang=...`) o que se ha forzado con + /// [`with_langid()`](Self::with_langid), y por tanto es recomendable propagar `lang=...` en + /// los enlaces para mantener esa preferencia mientras se navega. /// - /// - En modos [`LangNegotiation::NoQuery`] y [`LangNegotiation::ConfigOnly`] siempre devuelve - /// `false`, ya que en estas estrategias la aplicación no utiliza el parámetro `?lang=...` - /// para seleccionar ni para propagar el idioma. - /// - /// [`LangNegotiation`]: crate::global::LangNegotiation - /// [`LangNegotiation::Full`]: crate::global::LangNegotiation::Full - /// [`LangNegotiation::NoQuery`]: crate::global::LangNegotiation::NoQuery - /// [`LangNegotiation::ConfigOnly`]: crate::global::LangNegotiation::ConfigOnly + /// - En modos [`LangNegotiation::NoQuery`](crate::global::LangNegotiation::NoQuery) y + /// [`LangNegotiation::ConfigOnly`](crate::global::LangNegotiation::ConfigOnly) siempre + /// devuelve `false`, ya que en estas estrategias la aplicación no utiliza el parámetro + /// `?lang=...` para seleccionar ni para propagar el idioma. #[inline] pub(crate) fn needs_lang_query(&self) -> bool { match global::SETTINGS.app.lang_negotiation { @@ -184,19 +167,6 @@ impl RequestLocale { global::LangNegotiation::NoQuery | global::LangNegotiation::ConfigOnly => false, } } - - /// Devuelve el valor a propagar en el parámetro `?lang=...` de los enlaces generados. - /// - /// Si el idioma efectivo procede del parámetro `?lang=...` de la petición, devuelve ese valor - /// tal como llegó, conservando su grafía original (p. ej. `"en"` o `"en-US"`). En caso - /// contrario (por ejemplo, si el idioma se forzó con [`with_langid()`](Self::with_langid)), - /// devuelve la forma canónica del idioma efectivo. - #[inline] - pub(crate) fn lang_query_value(&self) -> String { - self.query_lang - .clone() - .unwrap_or_else(|| self.effective.to_string()) - } } /// Permite a [`RequestLocale`] actuar como proveedor de idioma.