WIP: Añade componente para la gestión de menús #8

Draft
manuelcillero wants to merge 54 commits from add-menu-component into main
29 changed files with 2608 additions and 9 deletions
Showing only changes of commit 82837c622e - Show all commits

View file

@ -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")
}

View file

@ -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,
}

View file

@ -68,10 +68,10 @@ use pagetop::prelude::*;
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
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<Markup, ErrorPage> {
```
*/
#![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 {

View file

@ -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

View file

@ -0,0 +1,5 @@
# Offcanvas
close = Close
# Navbar
toggle = Toggle navigation

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,5 @@
# Offcanvas
close = Cerrar
# Navbar
toggle = Mostrar/ocultar navegación

View file

@ -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

View file

@ -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,
};

View file

@ -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};

View file

@ -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<str>) -> 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()
)
}
}

View file

@ -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<str>) -> 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<str>) -> Option<String> {
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"),
}
}
}

View file

@ -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"),
}
}
}

View file

@ -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}"),
}
}
}

View file

@ -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<str>) -> 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()
)
}
}

View file

@ -0,0 +1,269 @@
use pagetop::prelude::*;
use crate::prelude::*;
/// Tipo de contenedor ([`Container`]).
///
/// Permite aplicar la etiqueta HTML apropiada (`<main>`, `<header>`, etc.) manteniendo una API
/// común a todos los contenedores.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub enum ContainerType {
/// Contenedor genérico (`<div>`).
#[default]
Default,
/// Contenido principal de la página (`<main>`).
Main,
/// Encabezado de la página o de sección (`<header>`).
Header,
/// Pie de la página o de sección (`<footer>`).
Footer,
/// Sección de contenido (`<section>`).
Section,
/// Artículo de contenido (`<article>`).
Article,
}
/// Componente genérico para crear un contenedor de componentes.
///
/// Envuelve el contenido con la etiqueta HTML indicada por [`ContainerType`]. Sólo se renderiza si
/// existen componentes hijos (*children*).
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Container {
id : AttrId,
classes : AttrClasses,
container_type: ContainerType,
breakpoint : BreakPoint,
children : Children,
bg_color : BgColor,
text_color : TextColor,
border : Border,
rounded : Rounded,
}
impl Component for Container {
fn new() -> Self {
Container::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(
ClassesOp::Prepend,
[
join_pair!("container", "-", self.breakpoint().to_string()),
self.bg_color().to_string(),
self.text_color().to_string(),
self.border().to_string(),
self.rounded().to_string(),
]
.join(" "),
);
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let output = self.children().render(cx);
if output.is_empty() {
return PrepareMarkup::None;
}
let style = match self.breakpoint() {
BreakPoint::FluidMax(w) if w.is_measurable() => {
Some(join!("max-width: ", w.to_string(), ";"))
}
_ => None,
};
match self.container_type() {
ContainerType::Default => PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
ContainerType::Main => PrepareMarkup::With(html! {
main id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
ContainerType::Header => PrepareMarkup::With(html! {
header id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
ContainerType::Footer => PrepareMarkup::With(html! {
footer id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
ContainerType::Section => PrepareMarkup::With(html! {
section id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
ContainerType::Article => PrepareMarkup::With(html! {
article id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
}
}
}
impl Container {
/// Crea un contenedor de tipo `Main` (`<main>`).
pub fn main() -> Self {
Container {
container_type: ContainerType::Main,
..Default::default()
}
}
/// Crea un contenedor de tipo `Header` (`<header>`).
pub fn header() -> Self {
Container {
container_type: ContainerType::Header,
..Default::default()
}
}
/// Crea un contenedor de tipo `Footer` (`<footer>`).
pub fn footer() -> Self {
Container {
container_type: ContainerType::Footer,
..Default::default()
}
}
/// Crea un contenedor de tipo `Section` (`<section>`).
pub fn section() -> Self {
Container {
container_type: ContainerType::Section,
..Default::default()
}
}
/// Crea un contenedor de tipo `Article` (`<article>`).
pub fn article() -> Self {
Container {
container_type: ContainerType::Article,
..Default::default()
}
}
// **< Container BUILDER >**********************************************************************
/// Establece el identificador único (`id`) del contenedor.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
/// Modifica la lista de clases CSS aplicadas al contenedor.
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
/// Establece el *punto de ruptura* del contenedor.
#[builder_fn]
pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self {
self.breakpoint = bp;
self
}
/// Añade un nuevo componente hijo al contenedor.
pub fn add_child(mut self, component: impl Component) -> Self {
self.children
.alter_child(ChildOp::Add(Child::with(component)));
self
}
/// Modifica la lista de hijos (`children`) aplicando una operación [`ChildOp`].
#[builder_fn]
pub fn with_child(mut self, op: ChildOp) -> Self {
self.children.alter_child(op);
self
}
/// Establece el color de fondo ([`BgColor`]).
#[builder_fn]
pub fn with_bg_color(mut self, color: BgColor) -> Self {
self.bg_color = color;
self
}
/// Establece el color del texto ([`TextColor`]).
#[builder_fn]
pub fn with_text_color(mut self, color: TextColor) -> Self {
self.text_color = color;
self
}
/// Atajo para definir los colores de fondo y texto a la vez.
#[builder_fn]
pub fn with_colors(mut self, bg_color: BgColor, text_color: TextColor) -> Self {
self.bg_color = bg_color;
self.text_color = text_color;
self
}
/// Establece el borde del contenedor ([`Border`]).
#[builder_fn]
pub fn with_border(mut self, border: Border) -> Self {
self.border = border;
self
}
/// Establece esquinas redondeadas para el contenedor.
#[builder_fn]
pub fn with_rounded(mut self, rounded: Rounded) -> Self {
self.rounded = rounded;
self
}
// **< Container GETTERS >**********************************************************************
/// Devuelve las clases CSS asociadas al contenedor.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
/// Devuelve el tipo semántico del contenedor.
pub fn container_type(&self) -> &ContainerType {
&self.container_type
}
/// Devuelve el *punto de ruptura* actualmente configurado.
pub fn breakpoint(&self) -> &BreakPoint {
&self.breakpoint
}
/// Devuelve la lista de hijos (`children`) del contenedor.
pub fn children(&self) -> &Children {
&self.children
}
/// Devuelve el color de fondo del contenedor.
pub fn bg_color(&self) -> &BgColor {
&self.bg_color
}
/// Devuelve el color del texto del contenedor.
pub fn text_color(&self) -> &TextColor {
&self.text_color
}
/// Devuelve el borde del contenedor.
pub fn border(&self) -> &Border {
&self.border
}
/// Devuelve las esquinas redondeadas del contenedor.
pub fn rounded(&self) -> &Rounded {
&self.rounded
}
}

View file

@ -0,0 +1,5 @@
mod component;
pub use component::Dropdown;
mod item;
pub use item::Item;

View file

@ -0,0 +1,99 @@
use pagetop::prelude::*;
use crate::prelude::*;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Dropdown {
id : AttrId,
classes: AttrClasses,
items : Children,
}
impl Component for Dropdown {
fn new() -> Self {
Dropdown::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(ClassesOp::Prepend, "dropdown");
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let items = self.items().render(cx);
if items.is_empty() {
return PrepareMarkup::None;
}
PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] {
button
type="button"
class="btn btn-secondary dropdown-toggle"
data-bs-toggle="dropdown"
aria-expanded="false"
{
("Dropdown button")
}
ul class="dropdown-menu" {
li {
a class="dropdown-item" href="#" {
("Action")
}
}
li {
a class="dropdown-item" href="#" {
("Another action")
}
}
li {
a class="dropdown-item" href="#" {
("Something else here")
}
}
}
}
})
}
}
impl Dropdown {
// **< Dropdown BUILDER >***********************************************************************
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
pub fn add_item(mut self, item: dropdown::Item) -> Self {
self.items.add(Child::with(item));
self
}
#[builder_fn]
pub fn with_items(mut self, op: TypedOp<dropdown::Item>) -> Self {
self.items.alter_typed(op);
self
}
// **< Dropdown GETTERS >***********************************************************************
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
pub fn items(&self) -> &Children {
&self.items
}
}

View file

@ -0,0 +1,109 @@
use pagetop::prelude::*;
// **< ItemType >***********************************************************************************
#[derive(AutoDefault)]
pub enum ItemType {
#[default]
Void,
Label(L10n),
Link(L10n, FnPathByContext),
LinkBlank(L10n, FnPathByContext),
}
// **< Item >***************************************************************************************
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Item {
item_type: ItemType,
}
impl Component for Item {
fn new() -> Self {
Item::default()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let description: Option<String> = None;
// Obtiene la URL actual desde `cx.request`.
let current_path = cx.request().map(|request| request.path());
match self.item_type() {
ItemType::Void => PrepareMarkup::None,
ItemType::Label(label) => PrepareMarkup::With(html! {
li class="dropdown-item" {
span title=[description] {
//(left_icon)
(label.using(cx))
//(right_icon)
}
}
}),
ItemType::Link(label, path) => {
let item_path = path(cx);
let (class, aria) = if current_path == Some(item_path) {
("dropdown-item active", Some("page"))
} else {
("dropdown-item", None)
};
PrepareMarkup::With(html! {
li class=(class) aria-current=[aria] {
a class="nav-link" href=(item_path) title=[description] {
//(left_icon)
(label.using(cx))
//(right_icon)
}
}
})
}
ItemType::LinkBlank(label, path) => {
let item_path = path(cx);
let (class, aria) = if current_path == Some(item_path) {
("dropdown-item active", Some("page"))
} else {
("dropdown-item", None)
};
PrepareMarkup::With(html! {
li class=(class) aria-current=[aria] {
a class="nav-link" href=(item_path) title=[description] target="_blank" {
//(left_icon)
(label.using(cx))
//(right_icon)
}
}
})
}
}
}
}
impl Item {
pub fn label(label: L10n) -> Self {
Item {
item_type: ItemType::Label(label),
..Default::default()
}
}
pub fn link(label: L10n, path: FnPathByContext) -> Self {
Item {
item_type: ItemType::Link(label, path),
..Default::default()
}
}
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
Item {
item_type: ItemType::LinkBlank(label, path),
..Default::default()
}
}
// Item GETTERS.
pub fn item_type(&self) -> &ItemType {
&self.item_type
}
}

View file

@ -0,0 +1,186 @@
use pagetop::prelude::*;
use crate::prelude::*;
#[derive(AutoDefault)]
pub enum ImageSource {
#[default]
//Logo(PageTopLogo),
Responsive(String),
Thumbnail(String),
Static(String),
}
#[derive(AutoDefault)]
pub enum ImageSize {
#[default]
Auto,
Dimensions(UnitValue, UnitValue),
Width(UnitValue),
Height(UnitValue),
Both(UnitValue),
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Image {
id : AttrId,
classes: AttrClasses,
source : ImageSource,
alt : AttrL10n,
size : ImageSize,
border : Border,
rounded: Rounded,
}
impl Component for Image {
fn new() -> Self {
Image::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(
ClassesOp::Prepend,
[
String::from(match self.source() {
//ImageSource::Logo(_) => "img-fluid",
ImageSource::Responsive(_) => "img-fluid",
ImageSource::Thumbnail(_) => "img-thumbnail",
_ => "",
}),
self.border().to_string(),
self.rounded().to_string(),
]
.join(" "),
);
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let dimensions = match self.size() {
ImageSize::Auto => None,
ImageSize::Dimensions(w, h) => {
let w = w.to_string();
let h = h.to_string();
Some(join!("width: ", w, "; height: ", h, ";"))
}
ImageSize::Width(w) => {
let w = w.to_string();
Some(join!("width: ", w, ";"))
}
ImageSize::Height(h) => {
let h = h.to_string();
Some(join!("height: ", h, ";"))
}
ImageSize::Both(v) => {
let v = v.to_string();
Some(join!("width: ", v, "; height: ", v, ";"))
}
};
let source = match self.source() {
/*
ImageSource::Logo(logo) => {
return PrepareMarkup::With(html! {
span
id=[self.id()]
class=[self.classes().get()]
style=[dimensions]
{
(logo.render(cx))
}
})
}
*/
ImageSource::Responsive(source) => Some(source),
ImageSource::Thumbnail(source) => Some(source),
ImageSource::Static(source) => Some(source),
};
PrepareMarkup::With(html! {
img
src=[source]
alt=[self.alternative().lookup(cx)]
id=[self.id()]
class=[self.classes().get()]
style=[dimensions] {}
})
}
}
impl Image {
pub fn with(source: ImageSource) -> Self {
Image::default().with_source(source)
}
// **< Image BUILDER >**************************************************************************
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
#[builder_fn]
pub fn with_source(mut self, source: ImageSource) -> Self {
self.source = source;
self
}
#[builder_fn]
pub fn with_alternative(mut self, alt: L10n) -> Self {
self.alt.alter_value(alt);
self
}
#[builder_fn]
pub fn with_size(mut self, size: ImageSize) -> Self {
self.size = size;
self
}
#[builder_fn]
pub fn with_border(mut self, border: Border) -> Self {
self.border = border;
self
}
#[builder_fn]
pub fn with_rounded(mut self, rounded: Rounded) -> Self {
self.rounded = rounded;
self
}
// **< Image GETTERS >**************************************************************************
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
pub fn source(&self) -> &ImageSource {
&self.source
}
pub fn alternative(&self) -> &AttrL10n {
&self.alt
}
pub fn size(&self) -> &ImageSize {
&self.size
}
pub fn border(&self) -> &Border {
&self.border
}
pub fn rounded(&self) -> &Rounded {
&self.rounded
}
}

View file

@ -0,0 +1,17 @@
mod component;
pub use component::{Navbar, NavbarToggler, NavbarType};
mod button_toggler;
pub use button_toggler::ButtonToggler;
mod content;
pub use content::{Content, ContentType};
mod brand;
pub use brand::Brand;
mod nav;
pub use nav::Nav;
mod item;
pub use item::{Item, ItemType};

View file

@ -0,0 +1,104 @@
use pagetop::prelude::*;
use crate::prelude::*;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Brand {
id : AttrId,
#[default(_code = "global::SETTINGS.app.name.to_owned()")]
app_name : String,
slogan : AttrL10n,
logo : Typed<Image>,
#[default(_code = "|_| \"/\"")]
home : FnPathByContext,
}
impl Component for Brand {
fn new() -> Self {
Brand::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let logo = self.logo().render(cx);
let home = self.home()(cx);
let title = &L10n::l("site_home").lookup(cx);
PrepareMarkup::With(html! {
div id=[self.id()] class="branding__container" {
div class="branding__content" {
@if !logo.is_empty() {
a class="branding__logo" href=(home) title=[title] rel="home" {
(logo)
}
}
div class="branding__text" {
a class="branding__name" href=(home) title=[title] rel="home" {
(self.app_name())
}
@if let Some(slogan) = self.slogan().lookup(cx) {
div class="branding__slogan" {
(slogan)
}
}
}
}
}
})
}
}
impl Brand {
// Brand BUILDER.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
#[builder_fn]
pub fn with_app_name(mut self, app_name: impl Into<String>) -> Self {
self.app_name = app_name.into();
self
}
#[builder_fn]
pub fn with_slogan(mut self, slogan: L10n) -> Self {
self.slogan.alter_value(slogan);
self
}
#[builder_fn]
pub fn with_logo(mut self, logo: Option<Image>) -> Self {
self.logo.alter_component(logo);
self
}
#[builder_fn]
pub fn with_home(mut self, home: FnPathByContext) -> Self {
self.home = home;
self
}
// Brand GETTERS.
pub fn app_name(&self) -> &String {
&self.app_name
}
pub fn slogan(&self) -> &AttrL10n {
&self.slogan
}
pub fn logo(&self) -> &Typed<Image> {
&self.logo
}
pub fn home(&self) -> &FnPathByContext {
&self.home
}
}

View file

@ -0,0 +1,73 @@
use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
use std::fmt;
#[derive(AutoDefault, PartialEq)]
pub(crate) enum Toggle {
#[default]
Collapse,
Offcanvas,
}
#[rustfmt::skip]
impl fmt::Display for Toggle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Toggle::Collapse => write!(f, "collapse"),
Toggle::Offcanvas => write!(f, "offcanvas"),
}
}
}
#[derive(AutoDefault)]
pub struct ButtonToggler;
impl Component for ButtonToggler {
fn new() -> Self {
ButtonToggler::default()
}
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! {
button
type="button"
class="navbar-toggler"
{
span class="navbar-toggler-icon" {}
}
})
}
}
impl ButtonToggler {
// ButtonToggler PRIVATE RENDER.
pub(crate) fn render(
&self,
cx: &mut Context,
id_content: String,
data_bs_toggle: Toggle,
) -> Markup {
let id_content_target = join!("#", id_content);
let aria_expanded = if data_bs_toggle == Toggle::Collapse {
Some("false")
} else {
None
};
html! {
button
type="button"
class="navbar-toggler"
data-bs-toggle=(data_bs_toggle)
data-bs-target=(id_content_target)
aria-controls=(id_content)
aria-expanded=[aria_expanded]
aria-label=[L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx)]
{
span class="navbar-toggler-icon" {}
}
}
}
}

View file

@ -0,0 +1,233 @@
use pagetop::prelude::*;
use crate::prelude::*;
use crate::LOCALES_BOOTSIER;
const TOGGLE_COLLAPSE: &str = "collapse";
const TOGGLE_OFFCANVAS: &str = "offcanvas";
#[derive(AutoDefault)]
pub enum NavbarToggler {
#[default]
Enabled,
Disabled,
}
#[derive(AutoDefault)]
pub enum NavbarType {
#[default]
None,
Nav(Typed<navbar::Nav>),
Offcanvas(Typed<Offcanvas>),
Text(L10n),
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Navbar {
id : AttrId,
classes : AttrClasses,
expand : BreakPoint,
toggler : NavbarToggler,
navbar_type: NavbarType,
contents : Children,
brand : Typed<navbar::Brand>,
}
impl Component for Navbar {
fn new() -> Self {
Navbar::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(
ClassesOp::Prepend,
[
"navbar".to_string(),
self.expand().try_class("navbar-expand").unwrap_or_default(),
]
.join(" "),
);
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let id = cx.required_id::<Self>(self.id());
let navbar_type = match self.navbar_type() {
NavbarType::None => return PrepareMarkup::None,
NavbarType::Nav(nav) => {
let id_content = join!(id, "-content");
match self.toggler() {
NavbarToggler::Enabled => self.toggler_wrapper(
TOGGLE_COLLAPSE,
L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx),
id_content,
self.brand().render(cx),
nav.render(cx),
),
NavbarToggler::Disabled => nav.render(cx),
}
}
NavbarType::Offcanvas(oc) => {
let id_content = oc.id().unwrap_or_default();
self.toggler_wrapper(
TOGGLE_OFFCANVAS,
L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx),
id_content,
self.brand().render(cx),
oc.render(cx),
)
}
NavbarType::Text(text) => html! {
span class="navbar-text" {
(text.using(cx))
}
},
};
self.nav_wrapper(id, self.brand().render(cx), navbar_type)
}
}
impl Navbar {
pub fn with_nav(nav: navbar::Nav) -> Self {
Navbar::default().with_navbar_type(NavbarType::Nav(Typed::with(nav)))
}
pub fn with_offcanvas(offcanvas: Offcanvas) -> Self {
Navbar::default().with_navbar_type(NavbarType::Offcanvas(Typed::with(offcanvas)))
}
// Navbar BUILDER.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
#[builder_fn]
pub fn with_expand(mut self, bp: BreakPoint) -> Self {
self.expand = bp;
self
}
#[builder_fn]
pub fn with_toggler(mut self, toggler: NavbarToggler) -> Self {
self.toggler = toggler;
self
}
#[builder_fn]
pub fn with_navbar_type(mut self, navbar_type: NavbarType) -> Self {
self.navbar_type = navbar_type;
self
}
pub fn with_content(mut self, content: navbar::Content) -> Self {
self.contents.add(Child::with(content));
self
}
#[builder_fn]
pub fn with_contents(mut self, op: TypedOp<navbar::Content>) -> Self {
self.contents.alter_typed(op);
self
}
#[builder_fn]
pub fn with_brand(mut self, brand: Option<navbar::Brand>) -> Self {
self.brand.alter_component(brand);
self
}
// Navbar GETTERS.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
pub fn expand(&self) -> &BreakPoint {
&self.expand
}
pub fn toggler(&self) -> &NavbarToggler {
&self.toggler
}
pub fn navbar_type(&self) -> &NavbarType {
&self.navbar_type
}
pub fn contents(&self) -> &Children {
&self.contents
}
pub fn brand(&self) -> &Typed<navbar::Brand> {
&self.brand
}
// Navbar HELPERS.
fn nav_wrapper(&self, id: String, brand: Markup, content: Markup) -> PrepareMarkup {
if content.is_empty() {
PrepareMarkup::None
} else {
PrepareMarkup::With(html! {
(brand)
nav id=(id) class=[self.classes().get()] {
div class="container-fluid" {
(content)
}
}
})
}
}
fn toggler_wrapper(
&self,
data_bs_toggle: &str,
aria_label: Option<String>,
id_content: String,
brand: Markup,
content: Markup,
) -> Markup {
if content.is_empty() {
html! {}
} else {
let id_content_target = join!("#", id_content);
let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE {
Some("false")
} else {
None
};
html! {
(brand)
button
type="button"
class="navbar-toggler"
data-bs-toggle=(data_bs_toggle)
data-bs-target=(id_content_target)
aria-controls=(id_content)
aria-expanded=[aria_expanded]
aria-label=[aria_label]
{
span class="navbar-toggler-icon" {}
}
div id=(id_content) class="collapse navbar-collapse" {
(content)
}
}
}
}
}

View file

@ -0,0 +1,69 @@
use pagetop::prelude::*;
use crate::theme::navbar;
#[derive(AutoDefault)]
pub enum ContentType {
#[default]
None,
Brand(Typed<navbar::Brand>),
Nav(Typed<navbar::Nav>),
Text(L10n),
}
// Item.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Content {
content: ContentType,
}
impl Component for Content {
fn new() -> Self {
Content::default()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
match self.content() {
ContentType::None => PrepareMarkup::None,
ContentType::Brand(brand) => PrepareMarkup::With(html! {
(brand.render(cx))
}),
ContentType::Nav(nav) => PrepareMarkup::With(html! {
(nav.render(cx))
}),
ContentType::Text(text) => PrepareMarkup::With(html! {
span class="navbar-text" {
(text.using(cx))
}
}),
}
}
}
impl Content {
pub fn brand(content: navbar::Brand) -> Self {
Content {
content: ContentType::Brand(Typed::with(content)),
}
}
pub fn nav(content: navbar::Nav) -> Self {
Content {
content: ContentType::Nav(Typed::with(content)),
}
}
pub fn text(content: L10n) -> Self {
Content {
content: ContentType::Text(content),
}
}
// Content GETTERS.
pub fn content(&self) -> &ContentType {
&self.content
}
}

View file

@ -0,0 +1,113 @@
use pagetop::prelude::*;
use crate::theme::Dropdown;
type Label = L10n;
#[derive(AutoDefault)]
pub enum ItemType {
#[default]
Void,
Label(Label),
Link(Label, FnPathByContext),
LinkBlank(Label, FnPathByContext),
Dropdown(Typed<Dropdown>),
}
// Item.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Item {
item_type: ItemType,
}
impl Component for Item {
fn new() -> Self {
Item::default()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let description: Option<String> = None;
// Obtiene la URL actual desde `cx.request`.
let current_path = cx.request().map(|request| request.path());
match self.item_type() {
ItemType::Void => PrepareMarkup::None,
ItemType::Label(label) => PrepareMarkup::With(html! {
li class="nav-item" {
span title=[description] {
//(left_icon)
(label.using(cx))
//(right_icon)
}
}
}),
ItemType::Link(label, path) => {
let item_path = path(cx);
let (class, aria) = if current_path == Some(item_path) {
("nav-item active", Some("page"))
} else {
("nav-item", None)
};
PrepareMarkup::With(html! {
li class=(class) aria-current=[aria] {
a class="nav-link" href=(item_path) title=[description] {
//(left_icon)
(label.using(cx))
//(right_icon)
}
}
})
}
ItemType::LinkBlank(label, path) => {
let item_path = path(cx);
let (class, aria) = if current_path == Some(item_path) {
("nav-item active", Some("page"))
} else {
("nav-item", None)
};
PrepareMarkup::With(html! {
li class=(class) aria-current=[aria] {
a class="nav-link" href=(item_path) title=[description] target="_blank" {
//(left_icon)
(label.using(cx))
//(right_icon)
}
}
})
}
ItemType::Dropdown(menu) => PrepareMarkup::With(html! { (menu.render(cx)) }),
}
}
}
impl Item {
pub fn label(label: L10n) -> Self {
Item {
item_type: ItemType::Label(label),
..Default::default()
}
}
pub fn link(label: L10n, path: FnPathByContext) -> Self {
Item {
item_type: ItemType::Link(label, path),
..Default::default()
}
}
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
Item {
item_type: ItemType::LinkBlank(label, path),
..Default::default()
}
}
// Item GETTERS.
pub fn item_type(&self) -> &ItemType {
&self.item_type
}
}

View file

@ -0,0 +1,75 @@
use pagetop::prelude::*;
use crate::theme::navbar;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Nav {
id : AttrId,
classes: AttrClasses,
items : Children,
}
impl Component for Nav {
fn new() -> Self {
Nav::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(ClassesOp::Prepend, "navbar-nav");
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let items = self.items().render(cx);
if items.is_empty() {
return PrepareMarkup::None;
}
PrepareMarkup::With(html! {
ul id=[self.id()] class=[self.classes().get()] {
(items)
}
})
}
}
impl Nav {
// Nav BUILDER.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
pub fn with_item(mut self, item: navbar::Item) -> Self {
self.items.add(Child::with(item));
self
}
#[builder_fn]
pub fn with_items(mut self, op: TypedOp<navbar::Item>) -> Self {
self.items.alter_typed(op);
self
}
// Nav GETTERS.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
pub fn items(&self) -> &Children {
&self.items
}
}

View file

@ -0,0 +1,302 @@
use pagetop::prelude::*;
use crate::prelude::*;
use crate::LOCALES_BOOTSIER;
use std::fmt;
// **< OffcanvasPlacement >*************************************************************************
/// Posición de aparición del panel **deslizante** ([`Offcanvas`]).
///
/// Define desde qué borde de la ventana entra y se ancla el panel.
#[derive(AutoDefault)]
pub enum OffcanvasPlacement {
/// Opción por defecto, desde el borde inicial según dirección de lectura (respetando LTR/RTL).
#[default]
Start,
/// Desde el borde final según dirección de lectura (respetando LTR/RTL).
End,
/// Desde la parte superior.
Top,
/// Desde la parte inferior.
Bottom,
}
#[rustfmt::skip]
impl fmt::Display for OffcanvasPlacement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OffcanvasPlacement::Start => f.write_str("offcanvas-start"),
OffcanvasPlacement::End => f.write_str("offcanvas-end"),
OffcanvasPlacement::Top => f.write_str("offcanvas-top"),
OffcanvasPlacement::Bottom => f.write_str("offcanvas-bottom"),
}
}
}
// **< OffcanvasVisibility >************************************************************************
/// Estado inicial del panel ([`Offcanvas`]).
#[derive(AutoDefault)]
pub enum OffcanvasVisibility {
/// El panel **permanece oculto** desde el principio.
#[default]
Default,
/// El panel **se muestra abierto** al cargar.
Show,
}
impl fmt::Display for OffcanvasVisibility {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OffcanvasVisibility::Default => Ok(()),
OffcanvasVisibility::Show => f.write_str("show"),
}
}
}
// **< OffcanvasBodyScroll >************************************************************************
/// Controla si la página principal puede **desplazarse** al abrir el panel ([`Offcanvas`]).
#[derive(AutoDefault)]
pub enum OffcanvasBodyScroll {
/// Opción por defecto, la página principal se **bloquea** centrando la interacción en el panel.
#[default]
Disabled,
/// **Permite** el desplazamiento de la página principal.
Enabled,
}
// **< OffcanvasBackdrop >**************************************************************************
/// Comportamiento de la **capa de fondo** (*backdrop*) del panel ([`Offcanvas`]) al desplegarse.
#[derive(AutoDefault)]
pub enum OffcanvasBackdrop {
/// **Sin capa** de fondo; la página principal permanece visible e interactiva.
Disabled,
/// Opción por defecto, se **oscurece** el fondo; un clic fuera del panel suele cerrarlo.
#[default]
Enabled,
/// Se muestra capa de fondo pero **no** se cierra al pulsar fuera (útil cuando se requiere
/// completar una acción antes de salir).
Static,
}
// **< Offcanvas >**********************************************************************************
/// Panel lateral **deslizante** para contenido complementario.
///
/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes
/// características principales:
///
/// - **Entrada configurable desde un borde** de la ventana.
/// - **Encabezado con título** y **botón de cierre** integrado.
/// - **Accesibilidad**: asocia título y controles a un identificador único y expone atributos
/// adecuados para lectores de pantalla y navegación por teclado.
/// - **Opcionalmente** bloquea el desplazamiento del documento y/o muestra una capa de fondo para
/// centrar la atención del usuario.
/// - **Responsive**: puede cambiar su comportamiento según el punto de ruptura indicado.
/// - **No se renderiza** si no tiene contenido.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Offcanvas {
id : AttrId,
classes : AttrClasses,
title : L10n,
breakpoint: BreakPoint,
placement : OffcanvasPlacement,
visibility: OffcanvasVisibility,
scrolling : OffcanvasBodyScroll,
backdrop : OffcanvasBackdrop,
children : Children,
}
impl Component for Offcanvas {
fn new() -> Self {
Offcanvas::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(
ClassesOp::Prepend,
[
self.breakpoint().to_class("offcanvas"),
self.placement().to_string(),
self.visibility().to_string(),
]
.join(" "),
);
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let body = self.children().render(cx);
if body.is_empty() {
return PrepareMarkup::None;
}
let id = cx.required_id::<Self>(self.id());
let id_label = join!(id, "-label");
let id_target = join!("#", id);
let body_scroll = match self.body_scroll() {
OffcanvasBodyScroll::Disabled => None,
OffcanvasBodyScroll::Enabled => Some("true"),
};
let backdrop = match self.backdrop() {
OffcanvasBackdrop::Disabled => Some("false"),
OffcanvasBackdrop::Enabled => None,
OffcanvasBackdrop::Static => Some("static"),
};
let title = self.title().using(cx);
PrepareMarkup::With(html! {
div
id=(id)
class=[self.classes().get()]
tabindex="-1"
data-bs-scroll=[body_scroll]
data-bs-backdrop=[backdrop]
aria-labelledby=(id_label)
{
div class="offcanvas-header" {
@if !title.is_empty() {
h5 class="offcanvas-title" id=(id_label) { (title) }
}
button
type="button"
class="btn-close"
data-bs-dismiss="offcanvas"
data-bs-target=(id_target)
aria-label=[L10n::t("close", &LOCALES_BOOTSIER).lookup(cx)]
{}
}
div class="offcanvas-body" {
(body)
}
}
})
}
}
impl Offcanvas {
// **< Offcanvas BUILDER >**********************************************************************
/// Establece el identificador único (`id`) del panel.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
/// Modifica la lista de clases CSS aplicadas al panel.
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
/// Establece el **título** del encabezado.
#[builder_fn]
pub fn with_title(mut self, title: L10n) -> Self {
self.title = title;
self
}
/// Configura el **punto de ruptura** para activar el comportamiento responsive del panel.
#[builder_fn]
pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self {
self.breakpoint = bp;
self
}
/// Indica la **posición** desde la que entra el panel.
#[builder_fn]
pub fn with_placement(mut self, placement: OffcanvasPlacement) -> Self {
self.placement = placement;
self
}
/// Fija el **estado inicial** del panel (oculto o visible al cargar).
#[builder_fn]
pub fn with_visibility(mut self, visibility: OffcanvasVisibility) -> Self {
self.visibility = visibility;
self
}
/// Permite o bloquea el **desplazamiento** de la página principal mientras el panel está
/// abierto.
#[builder_fn]
pub fn with_body_scroll(mut self, scrolling: OffcanvasBodyScroll) -> Self {
self.scrolling = scrolling;
self
}
/// Ajusta la **capa de fondo** del panel para definir su comportamiento al interactuar fuera.
#[builder_fn]
pub fn with_backdrop(mut self, backdrop: OffcanvasBackdrop) -> Self {
self.backdrop = backdrop;
self
}
/// Añade un nuevo componente hijo al panel.
pub fn add_child(mut self, child: impl Component) -> Self {
self.children.add(Child::with(child));
self
}
/// Modifica la lista de hijos (`children`) aplicando una operación [`ChildOp`].
#[builder_fn]
pub fn with_children(mut self, op: ChildOp) -> Self {
self.children.alter_child(op);
self
}
// **< Offcanvas GETTERS >**********************************************************************
/// Devuelve las clases CSS asociadas al panel.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
/// Devuelve el título del panel como [`L10n`].
pub fn title(&self) -> &L10n {
&self.title
}
/// Devuelve el punto de ruptura configurado.
pub fn breakpoint(&self) -> &BreakPoint {
&self.breakpoint
}
/// Devuelve la posición del panel.
pub fn placement(&self) -> &OffcanvasPlacement {
&self.placement
}
/// Devuelve el estado inicial del panel.
pub fn visibility(&self) -> &OffcanvasVisibility {
&self.visibility
}
/// Indica si la página principal puede desplazarse mientras el panel está abierto.
pub fn body_scroll(&self) -> &OffcanvasBodyScroll {
&self.scrolling
}
/// Devuelve la configuración de la capa de fondo.
pub fn backdrop(&self) -> &OffcanvasBackdrop {
&self.backdrop
}
/// Devuelve la lista de hijos (`children`) del panel.
pub fn children(&self) -> &Children {
&self.children
}
}