diff --git a/extensions/pagetop-bootsier/build.rs b/extensions/pagetop-bootsier/build.rs index 5945660..a96301c 100644 --- a/extensions/pagetop-bootsier/build.rs +++ b/extensions/pagetop-bootsier/build.rs @@ -15,5 +15,5 @@ fn main() -> std::io::Result<()> { fn bootstrap_js_files(path: &Path) -> bool { // No filtra durante el desarrollo, solo en la compilación "release". env::var("PROFILE").unwrap_or_else(|_| "release".to_string()) != "release" - || path.file_name().map_or(false, |n| n == "bootstrap.min.js") + || path.file_name().is_some_and(|n| n == "bootstrap.min.js") } diff --git a/extensions/pagetop-bootsier/src/config.rs b/extensions/pagetop-bootsier/src/config.rs new file mode 100644 index 0000000..6c2365b --- /dev/null +++ b/extensions/pagetop-bootsier/src/config.rs @@ -0,0 +1,41 @@ +//! Opciones de configuración del tema. +//! +//! Ejemplo: +//! +//! ```toml +//! [bootsier] +//! max_width = "90rem" +//! ``` +//! +//! Uso: +//! +//! ```rust +//! # use pagetop::prelude::*; +//! use pagetop_bootsier::config; +//! +//! assert_eq!(config::SETTINGS.bootsier.max_width, UnitValue::Px(1440)); +//! ``` +//! +//! Consulta [`pagetop::config`] para ver cómo PageTop lee los archivos de configuración y aplica +//! los valores a los ajustes. + +use pagetop::prelude::*; + +use serde::Deserialize; + +include_config!(SETTINGS: Settings => [ + // [bootsier] + "bootsier.max_width" => "1440px", +]); + +#[derive(Debug, Deserialize)] +/// Tipos para la sección [`[bootsier]`](Bootsier) de [`SETTINGS`]. +pub struct Settings { + pub bootsier: Bootsier, +} +#[derive(Debug, Deserialize)] +/// Sección `[bootsier]` de la configuración. Forma parte de [`Settings`]. +pub struct Bootsier { + /// Ancho máximo predeterminado para la página, por ejemplo "100%" o "90rem". + pub max_width: UnitValue, +} diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index 777f71e..a0e6c70 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -68,10 +68,10 @@ use pagetop::prelude::*; async fn homepage(request: HttpRequest) -> ResultPage { Page::new(request) .with_theme("Bootsier") - .add_component( + .add_child( Block::new() .with_title(L10n::l("sample_title")) - .add_component(Html::with(|cx| html! { + .add_child(Html::with(|cx| html! { p { (L10n::l("sample_content").using(cx)) } })), ) @@ -80,18 +80,32 @@ async fn homepage(request: HttpRequest) -> ResultPage { ``` */ +#![doc( + html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" +)] + use pagetop::prelude::*; -/// El tema usa las mismas regiones predefinidas por [`ThemeRegion`]. -pub type BootsierRegion = ThemeRegion; +include_locales!(LOCALES_BOOTSIER); // Versión de la librería Bootstrap. const BOOTSTRAP_VERSION: &str = "5.3.8"; -/// Tema basado en [Bootstrap](https://getbootstrap.com/) para los componentes base de PageTop. -/// -/// Ofrece composición de páginas *responsive*, utilidades y componentes listos para usar, con -/// estilos coherentes y enfoque en accesibilidad. +pub mod config; + +pub mod theme; + +/// *Prelude* del tema. +pub mod prelude { + pub use crate::config::*; + pub use crate::theme::aux::*; + pub use crate::theme::*; +} + +/// El tema usa las mismas regiones predefinidas por [`ThemeRegion`]. +pub type BootsierRegion = ThemeRegion; + +/// Implementa el tema. pub struct Bootsier; impl Extension for Bootsier { diff --git a/extensions/pagetop-bootsier/src/locale/en-US/bootsier.ftl b/extensions/pagetop-bootsier/src/locale/en-US/bootsier.ftl new file mode 100644 index 0000000..0e8969c --- /dev/null +++ b/extensions/pagetop-bootsier/src/locale/en-US/bootsier.ftl @@ -0,0 +1,5 @@ +e404-description = Oops! Page Not Found +e404-message = The page you are looking for may have been removed, had its name changed, or is temporarily unavailable. +e500-description = Oops! Unexpected Error +e500-message = We're having an issue. Please report this error to an administrator. +back-homepage = Back to homepage diff --git a/extensions/pagetop-bootsier/src/locale/en-US/components.ftl b/extensions/pagetop-bootsier/src/locale/en-US/components.ftl new file mode 100644 index 0000000..83dde39 --- /dev/null +++ b/extensions/pagetop-bootsier/src/locale/en-US/components.ftl @@ -0,0 +1,5 @@ +# Offcanvas +close = Close + +# Navbar +toggle = Toggle navigation diff --git a/extensions/pagetop-bootsier/src/locale/en-US/regions.ftl b/extensions/pagetop-bootsier/src/locale/en-US/regions.ftl new file mode 100644 index 0000000..f3b76e2 --- /dev/null +++ b/extensions/pagetop-bootsier/src/locale/en-US/regions.ftl @@ -0,0 +1,9 @@ +header = Header +nav_branding = Navigation branding region +nav_main = Main navigation region +nav_additional = Additional navigation region (eg search form, social icons, etc) +breadcrumb = Breadcrumb +content = Main content +sidebar_first = Sidebar first +sidebar_second = Sidebar second +footer = Footer diff --git a/extensions/pagetop-bootsier/src/locale/es-ES/bootsier.ftl b/extensions/pagetop-bootsier/src/locale/es-ES/bootsier.ftl new file mode 100644 index 0000000..998b54f --- /dev/null +++ b/extensions/pagetop-bootsier/src/locale/es-ES/bootsier.ftl @@ -0,0 +1,5 @@ +e404-description = ¡Vaya! Página No Encontrada +e404-message = La página que está buscando puede haber sido eliminada, cambiada de nombre o no está disponible temporalmente. +e500-description = ¡Vaya! Error Inesperado +e500-message = Está ocurriendo una incidencia. Por favor, informe de este error a un administrador. +back-homepage = Volver al inicio diff --git a/extensions/pagetop-bootsier/src/locale/es-ES/components.ftl b/extensions/pagetop-bootsier/src/locale/es-ES/components.ftl new file mode 100644 index 0000000..1ae9788 --- /dev/null +++ b/extensions/pagetop-bootsier/src/locale/es-ES/components.ftl @@ -0,0 +1,5 @@ +# Offcanvas +close = Cerrar + +# Navbar +toggle = Mostrar/ocultar navegación diff --git a/extensions/pagetop-bootsier/src/locale/es-ES/regions.ftl b/extensions/pagetop-bootsier/src/locale/es-ES/regions.ftl new file mode 100644 index 0000000..674fc4b --- /dev/null +++ b/extensions/pagetop-bootsier/src/locale/es-ES/regions.ftl @@ -0,0 +1,9 @@ +header = Cabecera +nav_branding = Navegación y marca +nav_main = Navegación principal +nav_additional = Navegación adicional (p.e. formulario de búsqueda, iconos sociales, etc.) +breadcrumb = Ruta de posicionamiento +content = Contenido principal +sidebar_first = Barra lateral primera +sidebar_second = Barra lateral segunda +footer = Pie diff --git a/extensions/pagetop-bootsier/src/theme.rs b/extensions/pagetop-bootsier/src/theme.rs new file mode 100644 index 0000000..51073d4 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme.rs @@ -0,0 +1,27 @@ +//! Definiciones y componentes del tema. + +pub mod aux; + +// Container. +mod container; +pub use container::{Container, ContainerType}; + +// Dropdown. +pub mod dropdown; +#[doc(inline)] +pub use dropdown::Dropdown; + +// Image. +mod image; +pub use image::{Image, ImageSize}; + +// Navbar. +pub mod navbar; +#[doc(inline)] +pub use navbar::{Navbar, NavbarToggler}; + +// Offcanvas. +mod offcanvas; +pub use offcanvas::{ + Offcanvas, OffcanvasBackdrop, OffcanvasBodyScroll, OffcanvasPlacement, OffcanvasVisibility, +}; diff --git a/extensions/pagetop-bootsier/src/theme/aux.rs b/extensions/pagetop-bootsier/src/theme/aux.rs new file mode 100644 index 0000000..72ebf06 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/aux.rs @@ -0,0 +1,18 @@ +//! Coleción de elementos auxiliares de Bootstrap para Bootsier. + +mod breakpoint; +pub use breakpoint::BreakPoint; + +mod color; +pub use color::Color; +pub use color::{BgColor, BorderColor, TextColor}; + +mod opacity; +pub use opacity::Opacity; +pub use opacity::{BgOpacity, BorderOpacity, TextOpacity}; + +mod border; +pub use border::{Border, BorderSize}; + +mod rounded; +pub use rounded::{Rounded, RoundedRadius}; diff --git a/extensions/pagetop-bootsier/src/theme/aux/border.rs b/extensions/pagetop-bootsier/src/theme/aux/border.rs new file mode 100644 index 0000000..840f03c --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/aux/border.rs @@ -0,0 +1,202 @@ +use pagetop::prelude::*; + +use crate::prelude::*; + +use std::fmt; + +// **< BorderSize >********************************************************************************* + +/// Tamaño (**ancho**) para los bordes ([`Border`]). +/// +/// Mapea a `border`, `border-0` y `border-{1..5}`: +/// +/// - `None` no añade clase (devuelve `""`). +/// - `Default` genera `border` (borde por defecto del tema). +/// - `Zero` genera `border-0` (sin borde). +/// - `Scale{1..5}` genera `border-{1..5}` (ancho creciente). +#[derive(AutoDefault)] +pub enum BorderSize { + #[default] + None, + Default, + Zero, + Scale1, + Scale2, + Scale3, + Scale4, + Scale5, +} + +impl BorderSize { + #[rustfmt::skip] + fn to_class(&self, prefix: impl AsRef) -> String { + match self { + Self::None => String::new(), + Self::Default => String::from(prefix.as_ref()), + Self::Zero => join!(prefix, "-0"), + Self::Scale1 => join!(prefix, "-1"), + Self::Scale2 => join!(prefix, "-2"), + Self::Scale3 => join!(prefix, "-3"), + Self::Scale4 => join!(prefix, "-4"), + Self::Scale5 => join!(prefix, "-5"), + } + } +} + +impl fmt::Display for BorderSize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_class("border")) + } +} + +// **< Border >************************************************************************************* + +/// Agrupa propiedades 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** (`BorderOpacity`). +/// +/// # Comportamiento aditivo / sustractivo +/// +/// - **Aditivo**: basta con crear un borde sin tamaño con `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 +/// `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 = Border::with(BorderSize::Scale2); +/// assert_eq!(b.to_string(), "border-2"); +/// ``` +/// +/// **Aditivo (solo borde superior):** +/// ```rust +/// # use pagetop_bootsier::prelude::*; +/// let b = 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 = 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 = 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 = 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(BorderOpacity::Theme(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: BorderOpacity, +} + +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: BorderOpacity) -> Self { + self.opacity = opacity; + self + } +} + +impl fmt::Display for Border { + /// Concatena cada definición en el orden: *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, + "{}", + join_opt!([ + 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_string(), + ]; " ") + .unwrap_or_default() + ) + } +} diff --git a/extensions/pagetop-bootsier/src/theme/aux/breakpoint.rs b/extensions/pagetop-bootsier/src/theme/aux/breakpoint.rs new file mode 100644 index 0000000..963bb3b --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/aux/breakpoint.rs @@ -0,0 +1,127 @@ +use pagetop::prelude::*; + +use std::fmt; + +/// Define los puntos de ruptura (*breakpoints*) para aplicar diseño *responsive*. +/// +/// - `"sm"`, `"md"`, `"lg"`, `"xl"` o `"xxl"` para los puntos de ruptura `SM`, `MD`, `LG`, `XL` o +/// `XXL`, respectivamente. +/// - `""` (cadena vacía) para `None`. +/// - `"fluid"` para las variantes `Fluid` y `FluidMax(_)`, útil para modelar `container-fluid` en +/// [`Container`](crate::theme::Container). Se debe tener en cuenta que `"fluid"` **no** es un +/// sufijo de *breakpoint*. Para construir clases válidas con prefijo (p. ej., `"col-md"`), se +/// recomienda usar `to_class()` (Self::to_class) o `try_class()` (Self::try_class). +/// +/// # Ejemplos +/// +/// ```rust +/// # use pagetop_bootsier::prelude::*; +/// assert_eq!(BreakPoint::MD.to_string(), "md"); +/// assert_eq!(BreakPoint::None.to_string(), ""); +/// assert_eq!(BreakPoint::Fluid.to_string(), "fluid"); +/// +/// // Forma correcta para clases con prefijo: +/// //assert_eq!(BreakPoint::MD.to_class("col"), "col-md"); +/// //assert_eq!(BreakPoint::Fluid.to_class("offcanvas"), "offcanvas"); +/// ``` +#[rustfmt::skip] +#[derive(AutoDefault)] +pub enum BreakPoint { + /// **Menos de 576px**. Dispositivos muy pequeños: teléfonos en modo vertical. + #[default] + None, + /// **576px o más** - Dispositivos pequeños: teléfonos en modo horizontal. + SM, + /// **768px o más** - Dispositivos medianos: tabletas. + MD, + /// **992px o más** - Dispositivos grandes: puestos de escritorio. + LG, + /// **1200px o más** - Dispositivos muy grandes: puestos de escritorio grandes. + XL, + /// **1400px o más** - Dispositivos extragrandes: puestos de escritorio más grandes. + XXL, + /// Para [`Container`](crate::theme::Container), ocupa el 100% del ancho del dispositivo. + Fluid, + /// Para [`Container`](crate::theme::Container), ocupa el 100% del ancho del dispositivo hasta + /// un ancho máximo indicado. + FluidMax(UnitValue) +} + +impl BreakPoint { + /// Comprueba si es un punto de ruptura efectivo. + /// + /// Devuelve `true` si el valor es `SM`, `MD`, `LG`, `XL` o `XXL`; y `false` en otro caso. + #[inline] + pub const fn is_breakpoint(&self) -> bool { + matches!(self, Self::SM | Self::MD | Self::LG | Self::XL | Self::XXL) + } + + /// Genera un nombre de clase CSS basado en el punto de ruptura. + /// + /// Si es un punto de ruptura efectivo (ver [`is_breakpoint()`](Self::is_breakpoint), concatena + /// el prefijo, un guion (`-`) y el sufijo asociado. En otro caso devuelve únicamente el + /// prefijo. + /// + /// # Ejemplo + /// + /// ```rust + /// let breakpoint = BreakPoint::MD; + /// let class = breakpoint.to_class("col"); + /// assert_eq!(class, "col-md".to_string()); + /// + /// let breakpoint = BreakPoint::Fluid; + /// let class = breakpoint.to_class("offcanvas"); + /// assert_eq!(class, "offcanvas".to_string()); + /// ``` + #[inline] + pub fn to_class(&self, prefix: impl AsRef) -> String { + if self.is_breakpoint() { + join!(prefix, "-", self.to_string()) + } else { + String::from(prefix.as_ref()) + } + } + + /// Intenta generar un nombre de clase CSS basado en el punto de ruptura. + /// + /// Si es un punto de ruptura efectivo (ver [`is_breakpoint()`](Self::is_breakpoint), devuelve + /// `Some(String)` concatenando el prefijo, un guion (`-`) y el sufijo asociado. En otro caso + /// devuelve `None`. + /// + /// # Ejemplo + /// + /// ```rust + /// let breakpoint = BreakPoint::MD; + /// let class = breakpoint.try_class("col"); + /// assert_eq!(class, Some("col-md".to_string())); + /// + /// let breakpoint = BreakPoint::Fluid; + /// let class = breakpoint.try_class("navbar-expanded"); + /// assert_eq!(class, None); + /// ``` + #[inline] + pub fn try_class(&self, prefix: impl AsRef) -> Option { + if self.is_breakpoint() { + Some(join!(prefix, "-", self.to_string())) + } else { + None + } + } +} + +#[rustfmt::skip] +impl fmt::Display for BreakPoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => Ok(()), + Self::SM => f.write_str("sm"), + Self::MD => f.write_str("md"), + Self::LG => f.write_str("lg"), + Self::XL => f.write_str("xl"), + Self::XXL => f.write_str("xxl"), + // Devuelven "fluid" (para modelar `container-fluid`). + Self::Fluid => f.write_str("fluid"), + Self::FluidMax(_) => f.write_str("fluid"), + } + } +} diff --git a/extensions/pagetop-bootsier/src/theme/aux/color.rs b/extensions/pagetop-bootsier/src/theme/aux/color.rs new file mode 100644 index 0000000..3a28855 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/aux/color.rs @@ -0,0 +1,155 @@ +use pagetop::prelude::*; + +use std::fmt; + +// **< Color >************************************************************************************** + +/// Paleta de colores **temáticos**. +/// +/// Equivalente a los nombres estándar de Bootstrap (`primary`, `secondary`, `success`, etc.). Sirve +/// como base para componer clases de fondo ([`BgColor`]), borde ([`BorderColor`]) y texto +/// ([`TextColor`]). +#[derive(AutoDefault)] +pub enum Color { + #[default] + Primary, + Secondary, + Success, + Info, + Warning, + Danger, + Light, + Dark, +} + +#[rustfmt::skip] +impl fmt::Display for Color { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Primary => f.write_str("primary"), + Self::Secondary => f.write_str("secondary"), + Self::Success => f.write_str("success"), + Self::Info => f.write_str("info"), + Self::Warning => f.write_str("warning"), + Self::Danger => f.write_str("danger"), + Self::Light => f.write_str("light"), + Self::Dark => f.write_str("dark"), + } + } +} + +// **< BgColor >************************************************************************************ + +/// Colores de fondo (`bg-*`). +/// +/// - `Default` no añade clase (devuelve `""` para facilitar la composición de clases). +/// - `Body*` usa fondos predefinidos del tema (`bg-body`, `bg-body-secondary`, `bg-body-tertiary`). +/// - `Theme(Color)` genera `bg-{color}` (p. ej., `bg-primary`). +/// - `Subtle(Color)` genera `bg-{color}-subtle` (tono suave). +/// - `Black` y `White` son colores explícitos. +/// - `Transparent` no aplica color de fondo (`bg-transparent`). +#[derive(AutoDefault)] +pub enum BgColor { + #[default] + Default, + Body, + BodySecondary, + BodyTertiary, + Theme(Color), + Subtle(Color), + Black, + White, + Transparent, +} + +#[rustfmt::skip] +impl fmt::Display for BgColor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Default => Ok(()), + Self::Body => f.write_str("bg-body"), + Self::BodySecondary => f.write_str("bg-body-secondary"), + Self::BodyTertiary => f.write_str("bg-body-tertiary"), + Self::Theme(c) => write!(f, "bg-{c}"), + Self::Subtle(c) => write!(f, "bg-{c}-subtle"), + Self::Black => f.write_str("bg-black"), + Self::White => f.write_str("bg-white"), + Self::Transparent => f.write_str("bg-transparent"), + } + } +} + +// **< BorderColor >******************************************************************************** + +/// Colores (`border-*`) para los bordes ([`Border`](crate::theme::aux::Border)). +/// +/// - `Default` no añade clase (devuelve `""` para facilitar la composición de clases). +/// - `Theme(Color)` genera `border-{color}`. +/// - `Subtle(Color)` genera `border-{color}-subtle` (versión suavizada). +/// - `Black` y `White` son colores explícitos. +#[derive(AutoDefault)] +pub enum BorderColor { + #[default] + Default, + Theme(Color), + Subtle(Color), + Black, + White, +} + +#[rustfmt::skip] +impl fmt::Display for BorderColor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Default => Ok(()), + Self::Theme(c) => write!(f, "border-{c}"), + Self::Subtle(c) => write!(f, "border-{c}-subtle"), + Self::Black => f.write_str("border-black"), + Self::White => f.write_str("border-white"), + } + } +} + +// **< TextColor >********************************************************************************** + +/// Colores de texto y fondos de texto (`text-*`). +/// +/// - `Default` no añade clase (devuelve `""` para facilitar la composición de clases). +/// - `Body*` aplica colores predefinidos del tema (`text-body`, `text-body-emphasis`, +/// `text-body-secondary`, `text-body-tertiary`). +/// - `Theme(Color)` genera `text-{color}`. +/// - `Emphasis(Color)` genera `text-{color}-emphasis` (contraste mayor acorde al tema). +/// - `Background(Color)` genera `text-bg-{color}` (para color de fondo del texto). +/// - `Black` y `White` son colores explícitos. +#[derive(AutoDefault)] +pub enum TextColor { + #[default] + Default, + Body, + BodyEmphasis, + BodySecondary, + BodyTertiary, + Theme(Color), + Emphasis(Color), + Background(Color), + Black, + White, +} + +#[rustfmt::skip] +impl fmt::Display for TextColor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Default => Ok(()), + Self::Body => f.write_str("text-body"), + Self::BodyEmphasis => f.write_str("text-body-emphasis"), + Self::BodySecondary => f.write_str("text-body-secondary"), + Self::BodyTertiary => f.write_str("text-body-tertiary"), + Self::Theme(c) => write!(f, "text-{c}"), + Self::Emphasis(c) => write!(f, "text-{c}-emphasis"), + Self::Background(c) => write!(f, "text-bg-{c}"), + Self::Black => f.write_str("text-black"), + Self::White => f.write_str("text-white"), + } + } +} diff --git a/extensions/pagetop-bootsier/src/theme/aux/opacity.rs b/extensions/pagetop-bootsier/src/theme/aux/opacity.rs new file mode 100644 index 0000000..96724d9 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/aux/opacity.rs @@ -0,0 +1,109 @@ +use pagetop::prelude::*; + +use std::fmt; + +// **< Opacity >************************************************************************************ + +/// Niveles de **opacidad** (`opacity-*`). +/// +/// Se usa para modular la transparencia del color de fondo `bg-opacity-*` ([`BgOpacity`]), borde +/// `border-opacity-*` ([`BorderOpacity`]) o texto `text-opacity-*` ([`TextOpacity`]), según las +/// siguientes equivalencias: +/// +/// - `Opaque` => `opacity-100` (100% de opacidad). +/// - `SemiOpaque` => `opacity-75` (75%). +/// - `Half` => `opacity-50` (50%). +/// - `SemiTransparent` => `opacity-25` (25%). +/// - `AlmostTransparent` => `opacity-10` (10%). +/// - `Transparent` => `opacity-0` (0%, totalmente transparente). +#[rustfmt::skip] +#[derive(AutoDefault)] +pub enum Opacity { + #[default] + Opaque, // 100% + SemiOpaque, // 75% + Half, // 50% + SemiTransparent, // 25% + AlmostTransparent, // 10% + Transparent, // 0% +} + +#[rustfmt::skip] +impl fmt::Display for Opacity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Opaque => f.write_str("opacity-100"), + Self::SemiOpaque => f.write_str("opacity-75"), + Self::Half => f.write_str("opacity-50"), + Self::SemiTransparent => f.write_str("opacity-25"), + Self::AlmostTransparent => f.write_str("opacity-10"), + Self::Transparent => f.write_str("opacity-0"), + } + } +} + +// **< BgOpacity >********************************************************************************** + +/// Opacidad para el fondo (`bg-opacity-*`). +/// +/// - `Default` no añade clase (devuelve `""` para facilitar la composición de clases). +/// - `Theme(Opacity)` genera `bg-{opacity}` (p. ej., `bg-opacity-50`). +#[derive(AutoDefault)] +pub enum BgOpacity { + #[default] + Default, + Theme(Opacity), +} + +impl fmt::Display for BgOpacity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Default => Ok(()), + Self::Theme(o) => write!(f, "bg-{o}"), + } + } +} + +// **< BorderOpacity >****************************************************************************** + +/// Opacidad (`border-opacity-*`) para los bordes ([`Border`](crate::theme::aux::Border)). +/// +/// - `Default` no añade clase (devuelve `""` para facilitar la composición de clases). +/// - `Theme(Opacity)` genera `border-{opacity}` (p. ej., `border-opacity-25`). +#[derive(AutoDefault)] +pub enum BorderOpacity { + #[default] + Default, + Theme(Opacity), +} + +impl fmt::Display for BorderOpacity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Default => Ok(()), + Self::Theme(o) => write!(f, "border-{o}"), + } + } +} + +// **< TextOpacity >******************************************************************************** + +/// Opacidad para el texto (`text-opacity-*`). +/// +/// - `Default` no añade clase (devuelve `""` para facilitar la composición de clases). +/// - `Theme(Opacity)` genera `text-{opacity}` (p. ej., `text-opacity-100`). +#[derive(AutoDefault)] +pub enum TextOpacity { + #[default] + Default, + Theme(Opacity), +} + +impl fmt::Display for TextOpacity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Default => Ok(()), + Self::Theme(o) => write!(f, "text-{o}"), + } + } +} diff --git a/extensions/pagetop-bootsier/src/theme/aux/rounded.rs b/extensions/pagetop-bootsier/src/theme/aux/rounded.rs new file mode 100644 index 0000000..f7f6e3f --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/aux/rounded.rs @@ -0,0 +1,214 @@ +use pagetop::prelude::*; + +use std::fmt; + +// **< RoundedRadius >****************************************************************************** + +/// Radio (**redondeo**) para las esquinas ([`Rounded`]). +/// +/// Mapea a `rounded`, `rounded-0`, `rounded-{1..5}`, `rounded-circle` y `rounded-pill`. +/// +/// - `None` no añade clase (devuelve `""`). +/// - `Default` genera `rounded` (radio por defecto del tema). +/// - `Zero` genera `rounded-0` (sin redondeo). +/// - `Scale{1..5}` genera `rounded-{1..5}` (radio creciente). +/// - `Circle` genera `rounded-circle`. +/// - `Pill` genera `rounded-pill`. +#[derive(AutoDefault)] +pub enum RoundedRadius { + #[default] + None, + Default, + Zero, + Scale1, + Scale2, + Scale3, + Scale4, + Scale5, + Circle, + Pill, +} + +impl RoundedRadius { + #[rustfmt::skip] + fn to_class(&self, base: impl AsRef) -> String { + match self { + RoundedRadius::None => String::new(), + RoundedRadius::Default => String::from(base.as_ref()), + RoundedRadius::Zero => join!(base, "-0"), + RoundedRadius::Scale1 => join!(base, "-1"), + RoundedRadius::Scale2 => join!(base, "-2"), + RoundedRadius::Scale3 => join!(base, "-3"), + RoundedRadius::Scale4 => join!(base, "-4"), + RoundedRadius::Scale5 => join!(base, "-5"), + RoundedRadius::Circle => join!(base, "-circle"), + RoundedRadius::Pill => join!(base, "-pill"), + } + } +} + +impl fmt::Display for RoundedRadius { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_class("rounded")) + } +} + +// **< Rounded >************************************************************************************ + +/// Agrupa propiedades para crear **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 = Rounded::with(RoundedRadius::Default); +/// assert_eq!(r.to_string(), "rounded"); +/// ``` +/// +/// **Sin redondeo:** +/// ```rust +/// # use pagetop_bootsier::prelude::*; +/// let r = Rounded::new(); +/// assert_eq!(r.to_string(), ""); +/// ``` +/// +/// **Radio en las esquinas de un lado lógico:** +/// ```rust +/// # use pagetop_bootsier::prelude::*; +/// let r = 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 = 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 = 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 cada definición en el orden: *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, + "{}", + join_opt!([ + 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"), + ]; " ") + .unwrap_or_default() + ) + } +} diff --git a/extensions/pagetop-bootsier/src/theme/container.rs b/extensions/pagetop-bootsier/src/theme/container.rs new file mode 100644 index 0000000..65ce923 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/container.rs @@ -0,0 +1,269 @@ +use pagetop::prelude::*; + +use crate::prelude::*; + +/// Tipo de contenedor ([`Container`]). +/// +/// Permite aplicar la etiqueta HTML apropiada (`
`, `
`, etc.) manteniendo una API +/// común a todos los contenedores. +#[rustfmt::skip] +#[derive(AutoDefault)] +pub enum ContainerType { + /// Contenedor genérico (`
`). + #[default] + Default, + /// Contenido principal de la página (`
`). + Main, + /// Encabezado de la página o de sección (`
`). + Header, + /// Pie de la página o de sección (`