Añade clases de fondo, texto, bordes y esquinas

- Refactoriza el componente contenedor `Container` para usar estas
  clases y aplicar los nuevos enums `Kind` y `Width` para mejorar el
  comportamiento semántico y *responsive*.
- Actualiza los componentes `Dropdown`, `Image`, `Nav`, `Navbar` y
  `Offcanvas` para usar los nuevos métodos de unión de clases.
- Elimina propiedades de estilo redundantes de los componentes
  `Navbar` e `Image`, simplificando sus interfaces.
This commit is contained in:
Manuel Cillero 2025-11-10 07:45:05 +01:00
parent 6365e1a077
commit 39033ef641
23 changed files with 1041 additions and 994 deletions

View file

@ -0,0 +1,155 @@
use pagetop::prelude::*;
use crate::theme::aux::{BorderColor, BorderSize, Opacity};
use std::fmt;
/// Clases para crear **bordes**.
///
/// Permite:
///
/// - Definir un tamaño **global** para todo el borde (`size`).
/// - Ajustar el tamaño de cada **lado lógico** (`top`, `end`, `bottom`, `start`, **en este orden**,
/// respetando LTR/RTL).
/// - Aplicar un **color** al borde (`BorderColor`).
/// - Aplicar un nivel de **opacidad** (`Opacity`).
///
/// # Comportamiento aditivo / sustractivo
///
/// - **Aditivo**: basta con crear un borde sin tamaño con `classes::Border::new()` para ir
/// añadiendo cada lado lógico con el tamaño deseado usando `BorderSize::Scale{1..5}`.
///
/// - **Sustractivo**: se crea un borde con tamaño predefinido, p. ej. utilizando
/// `classes::Border::with(BorderSize::Scale2)` y eliminar los lados deseados con `BorderSize::Zero`.
///
/// - **Anchos diferentes por lado**: usando `BorderSize::Scale{1..5}` en cada lado deseado.
///
/// # Ejemplos
///
/// **Borde global:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::with(BorderSize::Scale2);
/// assert_eq!(b.to_string(), "border-2");
/// ```
///
/// **Aditivo (solo borde superior):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::new().with_top(BorderSize::Scale1);
/// assert_eq!(b.to_string(), "border-top-1");
/// ```
///
/// **Sustractivo (borde global menos el superior):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::with(BorderSize::Default).with_top(BorderSize::Zero);
/// assert_eq!(b.to_string(), "border border-top-0");
/// ```
///
/// **Ancho por lado (lado lógico inicial a 2 y final a 4):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::new().with_start(BorderSize::Scale2).with_end(BorderSize::Scale4);
/// assert_eq!(b.to_string(), "border-end-4 border-start-2");
/// ```
///
/// **Combinado (ejemplo completo):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::with(BorderSize::Default) // Borde global por defecto.
/// .with_top(BorderSize::Zero) // Quita borde superior.
/// .with_end(BorderSize::Scale3) // Ancho 3 para el lado lógico final.
/// .with_color(BorderColor::Theme(Color::Primary))
/// .with_opacity(Opacity::Half);
///
/// assert_eq!(b.to_string(), "border border-top-0 border-end-3 border-primary border-opacity-50");
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Border {
size : BorderSize,
top : BorderSize,
end : BorderSize,
bottom : BorderSize,
start : BorderSize,
color : BorderColor,
opacity: Opacity,
}
impl Border {
/// Prepara un borde **sin tamaño global** de partida.
pub fn new() -> Self {
Self::default()
}
/// Crea un borde **con tamaño global** (`size`).
pub fn with(size: BorderSize) -> Self {
Self::default().with_size(size)
}
// **< Border BUILDER >*************************************************************************
/// Establece el tamaño global del borde (`border*`).
pub fn with_size(mut self, size: BorderSize) -> Self {
self.size = size;
self
}
/// Establece el tamaño del borde superior (`border-top-*`).
pub fn with_top(mut self, size: BorderSize) -> Self {
self.top = size;
self
}
/// Establece el tamaño del borde en el lado lógico final (`border-end-*`). Respeta LTR/RTL.
pub fn with_end(mut self, size: BorderSize) -> Self {
self.end = size;
self
}
/// Establece el tamaño del borde inferior (`border-bottom-*`).
pub fn with_bottom(mut self, size: BorderSize) -> Self {
self.bottom = size;
self
}
/// Establece el tamaño del borde en el lado lógico inicial (`border-start-*`). Respeta LTR/RTL.
pub fn with_start(mut self, size: BorderSize) -> Self {
self.start = size;
self
}
/// Establece el color del borde.
pub fn with_color(mut self, color: BorderColor) -> Self {
self.color = color;
self
}
/// Establece la opacidad del borde.
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
self.opacity = opacity;
self
}
}
impl fmt::Display for Border {
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`, *color*
/// y *opacidad*; respetando LTR/RTL y omitiendo las definiciones vacías.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
[
self.size.to_string(),
self.top.to_class("border-top"),
self.end.to_class("border-end"),
self.bottom.to_class("border-bottom"),
self.start.to_class("border-start"),
self.color.to_string(),
self.opacity.to_class("border"),
]
.join_classes()
)
}
}

View file

@ -0,0 +1,207 @@
use pagetop::prelude::*;
use crate::theme::aux::{ColorBg, ColorText, Opacity};
use std::fmt;
// **< Bg >*****************************************************************************************
/// Clases para establecer **color/opacidad del fondo**.
///
/// # Ejemplos
///
/// ```
/// # use pagetop_bootsier::prelude::*;
/// // Sin clases.
/// let s = classes::Background::new();
/// assert_eq!(s.to_string(), "");
///
/// // Sólo color de fondo.
/// let s = classes::Background::with(ColorBg::Theme(Color::Primary));
/// assert_eq!(s.to_string(), "bg-primary");
///
/// // Color más opacidad.
/// let s = classes::Background::with(ColorBg::BodySecondary).with_opacity(Opacity::Half);
/// assert_eq!(s.to_string(), "bg-body-secondary bg-opacity-50");
///
/// // Usando `From<ColorBg>`.
/// let s: classes::Background = ColorBg::Black.into();
/// assert_eq!(s.to_string(), "bg-black");
///
/// // Usando `From<(ColorBg, Opacity)>`.
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
/// assert_eq!(s.to_string(), "bg-white bg-opacity-25");
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Background {
color : ColorBg,
opacity: Opacity,
}
impl Background {
/// Prepara un nuevo estilo para aplicar al fondo.
pub fn new() -> Self {
Self::default()
}
/// Crea un estilo fijando el color de fondo (`bg-*`).
pub fn with(color: ColorBg) -> Self {
Self::default().with_color(color)
}
// **< Bg BUILDER >*****************************************************************************
/// Establece el color de fondo (`bg-*`).
pub fn with_color(mut self, color: ColorBg) -> Self {
self.color = color;
self
}
/// Establece la opacidad del fondo (`bg-opacity-*`).
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
self.opacity = opacity;
self
}
}
impl fmt::Display for Background {
/// Concatena, en este orden, color del fondo (`bg-*`) y opacidad (`bg-opacity-*`), omitiendo
/// las definiciones vacías.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let classes = [self.color.to_string(), self.opacity.to_class("bg")].join_classes();
write!(f, "{classes}")
}
}
impl From<(ColorBg, Opacity)> for Background {
/// Atajo para crear un [`classes::Background`](crate::theme::classes::Background) a partir del color de fondo y
/// la opacidad.
///
/// # Ejemplo
///
/// ```
/// # use pagetop_bootsier::prelude::*;
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
/// assert_eq!(s.to_string(), "bg-white bg-opacity-25");
/// ```
fn from((color, opacity): (ColorBg, Opacity)) -> Self {
Background::with(color).with_opacity(opacity)
}
}
impl From<ColorBg> for Background {
/// Atajo para crear un [`classes::Background`](crate::theme::classes::Background) a partir del color de fondo.
///
/// # Ejemplo
///
/// ```
/// # use pagetop_bootsier::prelude::*;
/// let s: classes::Background = ColorBg::Black.into();
/// assert_eq!(s.to_string(), "bg-black");
/// ```
fn from(color: ColorBg) -> Self {
Background::with(color)
}
}
// **< Text >***************************************************************************************
/// Clases para establecer **color/opacidad del texto**.
///
/// # Ejemplos
///
/// ```
/// # use pagetop_bootsier::prelude::*;
/// // Sin clases.
/// let s = classes::Text::new();
/// assert_eq!(s.to_string(), "");
///
/// // Sólo color del texto.
/// let s = classes::Text::with(ColorText::Theme(Color::Primary));
/// assert_eq!(s.to_string(), "text-primary");
///
/// // Color del texto y opacidad.
/// let s = classes::Text::new().with_color(ColorText::White).with_opacity(Opacity::SemiTransparent);
/// assert_eq!(s.to_string(), "text-white text-opacity-25");
///
/// // Usando `From<ColorText>`.
/// let s: classes::Text = ColorText::Black.into();
/// assert_eq!(s.to_string(), "text-black");
///
/// // Usando `From<(ColorText, Opacity)>`.
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
/// assert_eq!(s.to_string(), "text-danger text-opacity-100");
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Text {
color : ColorText,
opacity: Opacity,
}
impl Text {
/// Prepara un nuevo estilo para aplicar al texto.
pub fn new() -> Self {
Self::default()
}
/// Crea un estilo fijando el color del texto (`text-*`).
pub fn with(color: ColorText) -> Self {
Self::default().with_color(color)
}
// **< Text BUILDER >***************************************************************************
/// Establece el color del texto (`text-*`).
pub fn with_color(mut self, color: ColorText) -> Self {
self.color = color;
self
}
/// Establece la opacidad del texto (`text-opacity-*`).
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
self.opacity = opacity;
self
}
}
impl fmt::Display for Text {
/// Concatena, en este orden, `text-*` y `text-opacity-*`, omitiendo las definiciones vacías.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let classes = [self.color.to_string(), self.opacity.to_class("text")].join_classes();
write!(f, "{classes}")
}
}
impl From<(ColorText, Opacity)> for Text {
/// Atajo para crear un [`classes::Text`](crate::theme::classes::Text) a partir del color del
/// texto y su opacidad.
///
/// # Ejemplo
///
/// ```
/// # use pagetop_bootsier::prelude::*;
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
/// assert_eq!(s.to_string(), "text-danger text-opacity-100");
/// ```
fn from((color, opacity): (ColorText, Opacity)) -> Self {
Text::with(color).with_opacity(opacity)
}
}
impl From<ColorText> for Text {
/// Atajo para crear un [`classes::Text`](crate::theme::classes::Text) a partir del color del
/// texto.
///
/// # Ejemplo
///
/// ```
/// # use pagetop_bootsier::prelude::*;
/// let s: classes::Text = ColorText::Black.into();
/// assert_eq!(s.to_string(), "text-black");
/// ```
fn from(color: ColorText) -> Self {
Text::with(color)
}
}

View file

@ -0,0 +1,163 @@
use pagetop::prelude::*;
use crate::theme::aux::RoundedRadius;
use std::fmt;
/// Clases para definir **esquinas redondeadas**.
///
/// Permite:
///
/// - Definir un radio **global para todas las esquinas** (`radius`).
/// - Ajustar el radio asociado a las **esquinas de cada lado lógico** (`top`, `end`, `bottom`,
/// `start`, **en este orden**, respetando LTR/RTL).
/// - Ajustar el radio de las **esquinas concretas** (`top-start`, `top-end`, `bottom-start`,
/// `bottom-end`, **en este orden**, respetando LTR/RTL).
///
/// # Ejemplos
///
/// **Radio global:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::with(RoundedRadius::Default);
/// assert_eq!(r.to_string(), "rounded");
/// ```
///
/// **Sin redondeo:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::new();
/// assert_eq!(r.to_string(), "");
/// ```
///
/// **Radio en las esquinas de un lado lógico:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::new().with_end(RoundedRadius::Scale2);
/// assert_eq!(r.to_string(), "rounded-end-2");
/// ```
///
/// **Radio en una esquina concreta:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::new().with_top_start(RoundedRadius::Scale3);
/// assert_eq!(r.to_string(), "rounded-top-start-3");
/// ```
///
/// **Combinado (ejemplo completo):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::new()
/// .with_top(RoundedRadius::Default) // Añade redondeo arriba.
/// .with_bottom_start(RoundedRadius::Scale4) // Añade una esquina redondeada concreta.
/// .with_bottom_end(RoundedRadius::Circle); // Añade redondeo extremo en otra esquina.
///
/// assert_eq!(r.to_string(), "rounded-top rounded-bottom-start-4 rounded-bottom-end-circle");
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Rounded {
radius : RoundedRadius,
top : RoundedRadius,
end : RoundedRadius,
bottom : RoundedRadius,
start : RoundedRadius,
top_start : RoundedRadius,
top_end : RoundedRadius,
bottom_start: RoundedRadius,
bottom_end : RoundedRadius,
}
impl Rounded {
/// Prepara las esquinas **sin redondeo global** de partida.
pub fn new() -> Self {
Self::default()
}
/// Crea las esquinas **con redondeo global** (`radius`).
pub fn with(radius: RoundedRadius) -> Self {
Self::default().with_radius(radius)
}
// **< Rounded BUILDER >************************************************************************
/// Establece el radio global de las esquinas (`rounded*`).
pub fn with_radius(mut self, radius: RoundedRadius) -> Self {
self.radius = radius;
self
}
/// Establece el radio en las esquinas del lado superior (`rounded-top-*`).
pub fn with_top(mut self, radius: RoundedRadius) -> Self {
self.top = radius;
self
}
/// Establece el radio en las esquinas del lado lógico final (`rounded-end-*`). Respeta LTR/RTL.
pub fn with_end(mut self, radius: RoundedRadius) -> Self {
self.end = radius;
self
}
/// Establece el radio en las esquinas del lado inferior (`rounded-bottom-*`).
pub fn with_bottom(mut self, radius: RoundedRadius) -> Self {
self.bottom = radius;
self
}
/// Establece el radio en las esquinas del lado lógico inicial (`rounded-start-*`). Respeta
/// LTR/RTL.
pub fn with_start(mut self, radius: RoundedRadius) -> Self {
self.start = radius;
self
}
/// Establece el radio en la esquina superior-inicial (`rounded-top-start-*`). Respeta LTR/RTL.
pub fn with_top_start(mut self, radius: RoundedRadius) -> Self {
self.top_start = radius;
self
}
/// Establece el radio en la esquina superior-final (`rounded-top-end-*`). Respeta LTR/RTL.
pub fn with_top_end(mut self, radius: RoundedRadius) -> Self {
self.top_end = radius;
self
}
/// Establece el radio en la esquina inferior-inicial (`rounded-bottom-start-*`). Respeta
/// LTR/RTL.
pub fn with_bottom_start(mut self, radius: RoundedRadius) -> Self {
self.bottom_start = radius;
self
}
/// Establece el radio en la esquina inferior-final (`rounded-bottom-end-*`). Respeta LTR/RTL.
pub fn with_bottom_end(mut self, radius: RoundedRadius) -> Self {
self.bottom_end = radius;
self
}
}
impl fmt::Display for Rounded {
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`,
/// `top-start`, `top-end`, `bottom-start` y `bottom-end`; respetando LTR/RTL y omitiendo las
/// definiciones vacías.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
[
self.radius.to_string(),
self.top.to_class("rounded-top"),
self.end.to_class("rounded-end"),
self.bottom.to_class("rounded-bottom"),
self.start.to_class("rounded-start"),
self.top_start.to_class("rounded-top-start"),
self.top_end.to_class("rounded-top-end"),
self.bottom_start.to_class("rounded-bottom-start"),
self.bottom_end.to_class("rounded-bottom-end"),
]
.join_classes()
)
}
}