♻️ (bootsier): Reorganiza en capas token/class/bs

Sustituye `attrs/` y `classes/` por tres capas diferenciadas:
- `token/`: primitivas CSS (BreakPoint, Color, ScaleSize, etc.).
- `class/`: tipos compuestos (Border, Background, Margin, etc.).
- `bs/*/props.rs`: props tipadas para cada componente de Bootstrap.

El patrón de construcción es `suffix()` -> `push_to()` -> `to_class()`.
`push_to()` es `pub` en `class/` y `bs/*/props.rs` para que los temas
derivados puedan acceder a los mismos mecanismos de construcción.
This commit is contained in:
Manuel Cillero 2026-06-29 20:32:20 +02:00
parent 64db114eb0
commit f37db56ec5
47 changed files with 1307 additions and 1201 deletions

View file

@ -1,17 +0,0 @@
//! Colección de elementos auxiliares de Bootstrap para Bootsier.
mod breakpoint;
pub use breakpoint::BreakPoint;
mod color;
pub use color::{Color, Opacity};
pub use color::{ColorBg, ColorText};
mod layout;
pub use layout::{ScaleSize, Side};
mod border;
pub use border::BorderColor;
mod rounded;
pub use rounded::RoundedRadius;

View file

@ -1,86 +0,0 @@
use pagetop::prelude::*;
use crate::theme::attrs::Color;
/// Esquema de color para los bordes ([`classes::Border`](crate::theme::classes::Border)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum BorderColor {
/// No define ninguna clase.
#[default]
Default,
/// Genera la clase `border-{color}`.
Theme(Color),
/// Genera la clase `border-{color}-subtle` (un tono suavizado del color).
Subtle(Color),
/// Color negro.
Black,
/// Color blanco.
White,
}
impl BorderColor {
const BORDER: &str = "border";
const BORDER_PREFIX: &str = "border-";
/// Devuelve el sufijo de la clase `border-*`, o `None` si no define ninguna clase.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::Default => None,
Self::Theme(_) => Some(""),
Self::Subtle(_) => Some("-subtle"),
Self::Black => Some("-black"),
Self::White => Some("-white"),
}
}
/// Añade la clase `border-*` a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::Theme(c) | Self::Subtle(c) => {
classes.push_str(Self::BORDER_PREFIX);
classes.push_str(c.as_str());
}
_ => classes.push_str(Self::BORDER),
}
classes.push_str(suffix);
}
}
/// Devuelve la clase `border-*` correspondiente al color de borde.
///
/// # Ejemplos
///
/// ```rust,no_run
/// # use pagetop_bootsier::theme::*;
/// assert_eq!(BorderColor::Theme(Color::Primary).to_class(), "border-primary");
/// assert_eq!(BorderColor::Subtle(Color::Warning).to_class(), "border-warning-subtle");
/// assert_eq!(BorderColor::Black.to_class(), "border-black");
/// assert_eq!(BorderColor::Default.to_class(), "");
/// ```
pub fn to_class(self) -> String {
if let Some(suffix) = self.suffix() {
let base_len = match self {
Self::Theme(c) | Self::Subtle(c) => Self::BORDER_PREFIX.len() + c.as_str().len(),
_ => Self::BORDER.len(),
};
let mut class = String::with_capacity(base_len + suffix.len());
match self {
Self::Theme(c) | Self::Subtle(c) => {
class.push_str(Self::BORDER_PREFIX);
class.push_str(c.as_str());
}
_ => class.push_str(Self::BORDER),
}
class.push_str(suffix);
return class;
}
String::new()
}
}

View file

@ -1,114 +0,0 @@
use pagetop::prelude::*;
/// Define los puntos de ruptura (*breakpoints*) para aplicar diseño *responsive*.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum BreakPoint {
/// **Menos de 576px**. Dispositivos muy pequeños: teléfonos en modo vertical.
#[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,
}
impl BreakPoint {
/// Devuelve la identificación del punto de ruptura.
#[rustfmt::skip]
#[inline]
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::None => "",
Self::SM => "sm",
Self::MD => "md",
Self::LG => "lg",
Self::XL => "xl",
Self::XXL => "xxl",
}
}
/// Añade el punto de ruptura con un prefijo y un sufijo (opcional) separados por un guion `-` a
/// la cadena de clases.
///
/// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
/// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
#[inline]
pub(crate) fn push_class(self, classes: &mut String, prefix: &str, suffix: &str) {
if prefix.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::None => classes.push_str(prefix),
_ => {
classes.push_str(prefix);
classes.push('-');
classes.push_str(self.as_str());
}
}
if !suffix.is_empty() {
classes.push('-');
classes.push_str(suffix);
}
}
/// Devuelve la clase para el punto de ruptura, con un prefijo y un sufijo opcional, separados
/// por un guion `-`.
///
/// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
/// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
/// - Si `prefix` está vacío devuelve `""`.
///
/// # Ejemplos
///
/// ```rust,no_run
/// # use pagetop_bootsier::theme::*;
/// let bp = BreakPoint::MD;
/// assert_eq!(bp.class_with("col", ""), "col-md");
/// assert_eq!(bp.class_with("col", "6"), "col-md-6");
///
/// let bp = BreakPoint::None;
/// assert_eq!(bp.class_with("offcanvas", ""), "offcanvas");
/// assert_eq!(bp.class_with("col", "12"), "col-12");
///
/// let bp = BreakPoint::LG;
/// assert_eq!(bp.class_with("", "3"), "");
/// ```
#[doc(hidden)]
pub fn class_with(self, prefix: &str, suffix: &str) -> String {
if prefix.is_empty() {
return String::new();
}
let bp = self.as_str();
let has_bp = !bp.is_empty();
let has_suffix = !suffix.is_empty();
let mut len = prefix.len();
if has_bp {
len += 1 + bp.len();
}
if has_suffix {
len += 1 + suffix.len();
}
let mut class = String::with_capacity(len);
class.push_str(prefix);
if has_bp {
class.push('-');
class.push_str(bp);
}
if has_suffix {
class.push('-');
class.push_str(suffix);
}
class
}
}

View file

@ -1,117 +0,0 @@
use pagetop::prelude::*;
/// Radio para el redondeo de esquinas ([`classes::Rounded`](crate::theme::classes::Rounded)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum RoundedRadius {
/// No define ninguna clase.
#[default]
None,
/// Genera `rounded` (radio por defecto del tema).
Default,
/// Genera `rounded-0` (sin redondeo).
Zero,
/// Genera `rounded-1`.
Scale1,
/// Genera `rounded-2`.
Scale2,
/// Genera `rounded-3`.
Scale3,
/// Genera `rounded-4`.
Scale4,
/// Genera `rounded-5`.
Scale5,
/// Genera `rounded-circle`.
Circle,
/// Genera `rounded-pill`.
Pill,
}
impl RoundedRadius {
const ROUNDED: &str = "rounded";
/// Devuelve el sufijo para `*rounded-*`, o `None` si no define ninguna clase, o `""` para el
/// redondeo por defecto.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::None => None,
Self::Default => Some(""),
Self::Zero => Some("-0"),
Self::Scale1 => Some("-1"),
Self::Scale2 => Some("-2"),
Self::Scale3 => Some("-3"),
Self::Scale4 => Some("-4"),
Self::Scale5 => Some("-5"),
Self::Circle => Some("-circle"),
Self::Pill => Some("-pill"),
}
}
/// Añade el redondeo de esquinas a la cadena de clases usando el prefijo dado (`rounded-top`,
/// `rounded-bottom-start`, o vacío para `rounded-*`).
#[inline]
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
if prefix.is_empty() {
classes.push_str(Self::ROUNDED);
} else {
classes.push_str(prefix);
}
classes.push_str(suffix);
}
}
/// Devuelve la clase para el redondeo de esquinas con el prefijo dado (`rounded-top`,
/// `rounded-bottom-start`, o vacío para `rounded-*`).
///
/// # Ejemplos
///
/// ```rust,no_run
/// # use pagetop_bootsier::theme::*;
/// assert_eq!(RoundedRadius::Scale2.class_with(""), "rounded-2");
/// assert_eq!(RoundedRadius::Zero.class_with("rounded-top"), "rounded-top-0");
/// assert_eq!(RoundedRadius::Scale3.class_with("rounded-top-end"), "rounded-top-end-3");
/// assert_eq!(RoundedRadius::Circle.class_with(""), "rounded-circle");
/// assert_eq!(RoundedRadius::None.class_with("rounded-bottom-start"), "");
/// ```
#[doc(hidden)]
pub fn class_with(self, prefix: &str) -> String {
if let Some(suffix) = self.suffix() {
let base_len = if prefix.is_empty() {
Self::ROUNDED.len()
} else {
prefix.len()
};
let mut class = String::with_capacity(base_len + suffix.len());
if prefix.is_empty() {
class.push_str(Self::ROUNDED);
} else {
class.push_str(prefix);
}
class.push_str(suffix);
return class;
}
String::new()
}
/// Devuelve la clase `rounded-*` para el redondeo de esquinas.
///
/// # Ejemplos
///
/// ```rust,no_run
/// # use pagetop_bootsier::theme::*;
/// assert_eq!(RoundedRadius::Default.to_class(), "rounded");
/// assert_eq!(RoundedRadius::Zero.to_class(), "rounded-0");
/// assert_eq!(RoundedRadius::Scale3.to_class(), "rounded-3");
/// assert_eq!(RoundedRadius::Circle.to_class(), "rounded-circle");
/// assert_eq!(RoundedRadius::None.to_class(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
self.class_with("")
}
}

View file

@ -0,0 +1,53 @@
//! Tipos y componentes disponibles.
//!
//! A continuación, el apartado **Modules** incluye las definiciones necesarias para los componentes
//! que se muestran en el apartado **Structs**, mientras que en **Enums** se listan los elementos
//! auxiliares del tema utilizados en clases y componentes.
mod attrs;
pub use attrs::*;
mod classes;
pub use classes::*;
// Button.
mod button;
pub use button::{Button, ButtonAction};
// Container.
pub mod container;
#[doc(inline)]
pub use container::Container;
// Dropdown.
pub mod dropdown;
#[doc(inline)]
pub use dropdown::Dropdown;
// Form.
pub mod form;
#[doc(inline)]
pub use form::Form;
// Image.
pub mod image;
#[doc(inline)]
pub use image::Image;
// Nav.
pub mod nav;
#[doc(inline)]
pub use nav::Nav;
// Navbar.
pub mod navbar;
#[doc(inline)]
pub use navbar::Navbar;
// Offcanvas.
pub mod offcanvas;
#[doc(inline)]
pub use offcanvas::Offcanvas;
// Sidebar (componentes de navegación de AdminLTE).
pub mod sidebar;

View file

@ -1,9 +1,9 @@
//! Definiciones para crear contenedores de componentes ([`Container`]).
//!
//! Cada contenedor envuelve contenido usando la etiqueta semántica indicada por
//! [`container::Kind`](crate::theme::container::Kind).
//! [`container::Kind`](crate::theme::bs::container::Kind).
//!
//! Con [`container::Width`](crate::theme::container::Width) se puede definir el ancho y el
//! Con [`container::Width`](crate::theme::bs::container::Width) se puede definir el ancho y el
//! comportamiento *responsive* del contenedor. También permite aplicar utilidades de estilo para el
//! fondo, texto, borde o esquinas redondeadas.

View file

@ -2,10 +2,11 @@ use pagetop::prelude::*;
use crate::theme::*;
/// Componente para crear un **contenedor de componentes** ([`container`]).
/// Componente para crear un **contenedor de componentes**
/// ([`container`](crate::theme::bs::container)).
///
/// Envuelve un conjunto de componentes en un contenedor establecido que se crea aplicando uno de
/// los tipos definidos en [`container::Kind`].
/// los tipos definidos en [`container::Kind`](crate::theme::bs::container::Kind).
///
/// Si no contiene elementos, el componente **no se renderiza**.
///
@ -14,18 +15,18 @@ use crate::theme::*;
/// ```rust,no_run
/// use pagetop_bootsier::theme::*;
///
/// let main = Container::main()
/// let main = bs::Container::main()
/// .with_id("main-page")
/// .with_width(container::Width::From(BreakPoint::LG));
/// .with_width(bs::container::Width::From(token::BreakPoint::LG));
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Container {
/// Devuelve identificador, clases CSS y atributos HTML del componente.
props: Props,
/// Devuelve el tipo semántico del contenedor.
container_kind: container::Kind,
container_kind: bs::container::Kind,
/// Devuelve el comportamiento para el ancho del contenedor.
container_width: container::Width,
container_width: bs::container::Width,
/// Devuelve la lista de componentes (`children`) del contenedor.
children: Children,
}
@ -49,38 +50,38 @@ impl Component for Container {
return Ok(html! {});
}
let style = match self.container_width() {
container::Width::FluidMax(w) if w.is_measurable() => {
bs::container::Width::FluidMax(w) if w.is_measurable() => {
Some(util::join!("max-width: ", w.to_string(), ";"))
}
_ => None,
};
Ok(match self.container_kind() {
container::Kind::Default => html! {
bs::container::Kind::Default => html! {
div (self.props()) style=[style] {
(output)
}
},
container::Kind::Main => html! {
bs::container::Kind::Main => html! {
main (self.props()) style=[style] {
(output)
}
},
container::Kind::Header => html! {
bs::container::Kind::Header => html! {
header (self.props()) style=[style] {
(output)
}
},
container::Kind::Footer => html! {
bs::container::Kind::Footer => html! {
footer (self.props()) style=[style] {
(output)
}
},
container::Kind::Section => html! {
bs::container::Kind::Section => html! {
section (self.props()) style=[style] {
(output)
}
},
container::Kind::Article => html! {
bs::container::Kind::Article => html! {
article (self.props()) style=[style] {
(output)
}
@ -93,7 +94,7 @@ impl Container {
/// Crea un contenedor de tipo `Main` (`<main>`).
pub fn main() -> Self {
Self {
container_kind: container::Kind::Main,
container_kind: bs::container::Kind::Main,
..Default::default()
}
}
@ -101,7 +102,7 @@ impl Container {
/// Crea un contenedor de tipo `Header` (`<header>`).
pub fn header() -> Self {
Self {
container_kind: container::Kind::Header,
container_kind: bs::container::Kind::Header,
..Default::default()
}
}
@ -109,7 +110,7 @@ impl Container {
/// Crea un contenedor de tipo `Footer` (`<footer>`).
pub fn footer() -> Self {
Self {
container_kind: container::Kind::Footer,
container_kind: bs::container::Kind::Footer,
..Default::default()
}
}
@ -117,7 +118,7 @@ impl Container {
/// Crea un contenedor de tipo `Section` (`<section>`).
pub fn section() -> Self {
Self {
container_kind: container::Kind::Section,
container_kind: bs::container::Kind::Section,
..Default::default()
}
}
@ -125,7 +126,7 @@ impl Container {
/// Crea un contenedor de tipo `Article` (`<article>`).
pub fn article() -> Self {
Self {
container_kind: container::Kind::Article,
container_kind: bs::container::Kind::Article,
..Default::default()
}
}
@ -143,10 +144,10 @@ impl Container {
///
/// También acepta clases predefinidas para:
///
/// - Modificar el color de fondo ([`classes::Background`]).
/// - Definir la apariencia del texto ([`classes::Text`]).
/// - Establecer bordes ([`classes::Border`]).
/// - Redondear las esquinas ([`classes::Rounded`]).
/// - Modificar el color de fondo ([`Background`]).
/// - Definir la apariencia del texto ([`Text`]).
/// - Establecer bordes ([`Border`]).
/// - Redondear las esquinas ([`Rounded`]).
#[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op);
@ -155,7 +156,7 @@ impl Container {
/// Establece el comportamiento del ancho para el contenedor.
#[builder_fn]
pub fn with_width(mut self, width: container::Width) -> Self {
pub fn with_width(mut self, width: bs::container::Width) -> Self {
self.container_width = width;
self
}

View file

@ -1,10 +1,10 @@
use pagetop::prelude::*;
use crate::theme::attrs::BreakPoint;
use crate::theme::*;
// **< Kind >***************************************************************************************
/// Tipo de contenedor ([`Container`](crate::theme::Container)).
/// Tipo de contenedor ([`Container`](crate::theme::bs::Container)).
///
/// Permite aplicar la etiqueta HTML apropiada (`<main>`, `<header>`, etc.) manteniendo una API
/// común a todos los contenedores.
@ -27,7 +27,7 @@ pub enum Kind {
// **< Width >**************************************************************************************
/// Define cómo se comporta el ancho de un contenedor ([`Container`](crate::theme::Container)).
/// Define cómo se comporta el ancho de un contenedor ([`Container`](crate::theme::bs::Container)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Width {
/// Comportamiento por defecto, aplica los anchos máximos predefinidos para cada punto de
@ -36,7 +36,7 @@ pub enum Width {
Default,
/// Aplica los anchos máximos predefinidos a partir del punto de ruptura indicado. Por debajo de
/// ese punto de ruptura ocupa el 100% del ancho disponible.
From(BreakPoint),
From(token::BreakPoint),
/// Ocupa el 100% del ancho disponible siempre.
Fluid,
/// Ocupa el 100% del ancho disponible hasta un ancho máximo explícito.
@ -46,26 +46,22 @@ pub enum Width {
impl Width {
const CONTAINER: &str = "container";
/* Añade el comportamiento del contenedor a la cadena de clases según ancho (reservado).
/// Añade la clase asociada al ancho del contenedor a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
pub fn push_to(self, classes: &mut String) {
match self {
Self::Default => BreakPoint::None.push_class(classes, Self::CONTAINER, ""),
Self::From(bp) => bp.push_class(classes, Self::CONTAINER, ""),
Self::Default => token::BreakPoint::None.push_to(classes, Self::CONTAINER, ""),
Self::From(bp) => bp.push_to(classes, Self::CONTAINER, ""),
Self::Fluid | Self::FluidMax(_) => {
BreakPoint::None.push_class(classes, Self::CONTAINER, "fluid")
token::BreakPoint::None.push_to(classes, Self::CONTAINER, "fluid")
}
}
}
} */
/// Devuelve la clase asociada al comportamiento del contenedor según el ajuste de su ancho.
/// Devuelve la clase asociada al ancho del contenedor.
pub fn to_class(self) -> String {
match self {
Self::Default => BreakPoint::None.class_with(Self::CONTAINER, ""),
Self::From(bp) => bp.class_with(Self::CONTAINER, ""),
Self::Fluid | Self::FluidMax(_) => {
BreakPoint::None.class_with(Self::CONTAINER, "fluid")
}
}
let mut class = String::new();
self.push_to(&mut class);
class
}
}

View file

@ -1,6 +1,6 @@
//! Definiciones para crear menús desplegables ([`Dropdown`]).
//!
//! Cada [`dropdown::Item`](crate::theme::dropdown::Item) representa un elemento individual del
//! Cada [`dropdown::Item`](crate::theme::bs::dropdown::Item) representa un elemento individual del
//! desplegable [`Dropdown`], con distintos comportamientos según su finalidad, como enlaces de
//! navegación, botones de acción, encabezados o divisores visuales.
//!

View file

@ -3,19 +3,20 @@ use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
use crate::theme::*;
/// Componente para crear un **menú desplegable** ([`dropdown`]).
/// Componente para crear un **menú desplegable** ([`dropdown`](crate::theme::bs::dropdown)).
///
/// Renderiza un botón (único o desdoblado, ver [`with_button_split()`](Self::with_button_split))
/// para mostrar un menú desplegable de elementos [`dropdown::Item`], que se muestra u oculta según
/// la interacción del usuario. Admite variaciones para el tamaño y el color del botón, también para
/// la dirección de apertura, alineación o política de cierre.
/// con un menú desplegable de elementos [`dropdown::Item`](crate::theme::bs::dropdown::Item), que
/// se muestra u oculta según la interacción del usuario. Admite variaciones para el tamaño y el
/// color del botón, también para la dirección de apertura, alineación o política de cierre.
///
/// Si no tiene título (ver [`with_title()`](Self::with_title)) se muestra únicamente la lista de
/// elementos sin ningún botón para interactuar.
///
/// Si este componente se usa en un menú [`Nav`] (ver [`nav::Item::dropdown()`]) sólo se tendrán en
/// cuenta **el título** (si no existe le asigna uno por defecto) y **la lista de elementos**; el
/// resto de propiedades no afectarán a su representación en [`Nav`].
/// Si este componente se usa en un menú [`Nav`](crate::theme::bs::Nav) (ver
/// [`nav::Item::dropdown()`](crate::theme::bs::nav::Item::dropdown)) sólo se tendrán en cuenta **el
/// título** (si no existe le asigna uno por defecto) y **la lista de elementos**; el resto de
/// propiedades no afectarán a su representación en [`Nav`](crate::theme::bs::Nav).
///
/// Si no contiene elementos, el componente **no se renderiza**.
///
@ -25,16 +26,16 @@ use crate::theme::*;
/// use pagetop::prelude::*;
/// use pagetop_bootsier::theme::*;
///
/// let dd = Dropdown::new()
/// let dd = bs::Dropdown::new()
/// .with_title(L10n::n("Menu"))
/// .with_button_color(classes::ButtonColor::solid(Color::Secondary))
/// .with_auto_close(dropdown::AutoClose::ClickableInside)
/// .with_direction(dropdown::Direction::Dropend)
/// .with_item(dropdown::Item::link(L10n::n("Home"), |_| "/".into()))
/// .with_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into()))
/// .with_item(dropdown::Item::divider())
/// .with_item(dropdown::Item::header(L10n::n("User session")))
/// .with_item(dropdown::Item::button(L10n::n("Sign out")));
/// .with_button_color(class::ButtonColor::solid(token::Color::Secondary))
/// .with_auto_close(bs::dropdown::AutoClose::ClickableInside)
/// .with_direction(bs::dropdown::Direction::Dropend)
/// .with_item(bs::dropdown::Item::link(L10n::n("Home"), |_| "/".into()))
/// .with_item(bs::dropdown::Item::link_blank(L10n::n("Doc"), |_| "https://docs.rs".into()))
/// .with_item(bs::dropdown::Item::divider())
/// .with_item(bs::dropdown::Item::header(L10n::n("User session")))
/// .with_item(bs::dropdown::Item::button(L10n::n("Sign out")));
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Dropdown {
@ -43,21 +44,21 @@ pub struct Dropdown {
/// Devuelve el título del menú desplegable.
title: L10n,
/// Devuelve el tamaño configurado del botón.
button_size: classes::ButtonSize,
button_size: class::ButtonSize,
/// Devuelve el color/estilo configurado del botón.
button_color: classes::ButtonColor,
button_color: class::ButtonColor,
/// Devuelve si se debe desdoblar (*split*) el botón (botón de acción + *toggle*).
button_split: bool,
/// Devuelve si el botón del menú está integrado en un grupo de botones.
button_grouped: bool,
/// Devuelve la política de cierre automático del menú desplegado.
auto_close: dropdown::AutoClose,
auto_close: bs::dropdown::AutoClose,
/// Devuelve la dirección de despliegue configurada.
direction: dropdown::Direction,
direction: bs::dropdown::Direction,
/// Devuelve la configuración de alineación horizontal del menú desplegable.
menu_align: dropdown::MenuAlign,
menu_align: bs::dropdown::MenuAlign,
/// Devuelve la posición configurada para el menú desplegable.
menu_position: dropdown::MenuPosition,
menu_position: bs::dropdown::MenuPosition,
/// Devuelve la lista de elementos del menú.
items: Children,
}
@ -73,7 +74,7 @@ impl Component for Dropdown {
fn setup(&mut self, _cx: &Context) {
self.alter_prop(PropsOp::prepend_classes(
self.direction().class_with(*self.button_grouped()),
self.direction().to_class(*self.button_grouped()),
));
}
@ -91,20 +92,18 @@ impl Component for Dropdown {
div (self.props()) {
@if !title.is_empty() {
@let btn_base = {
let size = self.button_size().to_class();
let color = self.button_color().to_class();
let mut classes = String::from("btn");
if !size.is_empty() { classes.push(' '); classes.push_str(&size); }
if !color.is_empty() { classes.push(' '); classes.push_str(&color); }
self.button_size().push_to(&mut classes);
self.button_color().push_to(&mut classes);
classes
};
@let pos = self.menu_position();
@let offset = pos.data_offset();
@let reference = pos.data_reference();
@let auto_close = self.auto_close.as_str();
@let auto_close = self.auto_close().opt_str();
@let menu_classes = {
let mut classes = "dropdown-menu".to_string();
self.menu_align().push_class(&mut classes);
self.menu_align().push_to(&mut classes);
classes
};
@ -139,7 +138,7 @@ impl Component for Dropdown {
};
// Orden según dirección (en `dropstart` el *toggle* se sitúa antes).
@match self.direction() {
dropdown::Direction::Dropstart => {
bs::dropdown::Direction::Dropstart => {
(btn_toggle)
ul class=(&menu_classes) { (items) }
(btn)
@ -201,14 +200,14 @@ impl Dropdown {
/// Ajusta el tamaño del botón.
#[builder_fn]
pub fn with_button_size(mut self, size: classes::ButtonSize) -> Self {
pub fn with_button_size(mut self, size: class::ButtonSize) -> Self {
self.button_size = size;
self
}
/// Define el color/estilo del botón.
#[builder_fn]
pub fn with_button_color(mut self, color: classes::ButtonColor) -> Self {
pub fn with_button_color(mut self, color: class::ButtonColor) -> Self {
self.button_color = color;
self
}
@ -229,28 +228,28 @@ impl Dropdown {
/// Establece la política de cierre automático del menú desplegable.
#[builder_fn]
pub fn with_auto_close(mut self, auto_close: dropdown::AutoClose) -> Self {
pub fn with_auto_close(mut self, auto_close: bs::dropdown::AutoClose) -> Self {
self.auto_close = auto_close;
self
}
/// Establece la dirección de despliegue del menú.
#[builder_fn]
pub fn with_direction(mut self, direction: dropdown::Direction) -> Self {
pub fn with_direction(mut self, direction: bs::dropdown::Direction) -> Self {
self.direction = direction;
self
}
/// Configura la alineación horizontal (con posible comportamiento *responsive* adicional).
#[builder_fn]
pub fn with_menu_align(mut self, align: dropdown::MenuAlign) -> Self {
pub fn with_menu_align(mut self, align: bs::dropdown::MenuAlign) -> Self {
self.menu_align = align;
self
}
/// Configura la posición del menú.
#[builder_fn]
pub fn with_menu_position(mut self, position: dropdown::MenuPosition) -> Self {
pub fn with_menu_position(mut self, position: bs::dropdown::MenuPosition) -> Self {
self.menu_position = position;
self
}

View file

@ -2,8 +2,8 @@ use pagetop::prelude::*;
// **< ItemKind >***********************************************************************************
/// Tipos de [`dropdown::Item`](crate::theme::dropdown::Item) disponibles en un menú desplegable
/// [`Dropdown`](crate::theme::Dropdown).
/// Tipos de [`dropdown::Item`](crate::theme::bs::dropdown::Item) disponibles en un menú desplegable
/// [`Dropdown`](crate::theme::bs::Dropdown).
///
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
/// con él.
@ -35,11 +35,11 @@ pub enum ItemKind {
// **< Item >***************************************************************************************
/// Representa un **elemento individual** de un menú desplegable
/// [`Dropdown`](crate::theme::Dropdown).
/// [`Dropdown`](crate::theme::bs::Dropdown).
///
/// Cada instancia de [`dropdown::Item`](crate::theme::dropdown::Item) se traduce en un componente
/// visible que puede comportarse como texto, enlace, botón, encabezado o separador, según su
/// [`ItemKind`].
/// Cada instancia de [`dropdown::Item`](crate::theme::bs::dropdown::Item) se traduce en un
/// componente visible que puede comportarse como texto, enlace, botón, encabezado o separador,
/// según su [`ItemKind`].
///
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.

View file

@ -4,7 +4,7 @@ use crate::theme::*;
// **< AutoClose >**********************************************************************************
/// Estrategia para el cierre automático de un menú [`Dropdown`].
/// Estrategia para el cierre automático de un menú [`Dropdown`](crate::theme::bs::Dropdown).
///
/// Define cuándo se cierra el menú desplegado según la interacción del usuario.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
@ -25,7 +25,7 @@ impl AutoClose {
/// Devuelve el valor para `data-bs-auto-close`, o `None` si es el comportamiento por defecto.
#[rustfmt::skip]
#[inline]
pub(crate) const fn as_str(self) -> Option<&'static str> {
pub const fn opt_str(self) -> Option<&'static str> {
match self {
Self::Default => None,
Self::ClickableInside => Some("inside"),
@ -37,7 +37,7 @@ impl AutoClose {
// **< Direction >**********************************************************************************
/// Dirección de despliegue de un menú [`Dropdown`].
/// Dirección de despliegue de un menú [`Dropdown`](crate::theme::bs::Dropdown).
///
/// Controla desde qué posición se muestra el menú respecto al botón.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
@ -59,10 +59,13 @@ pub enum Direction {
}
impl Direction {
/// Mapea la dirección teniendo en cuenta si se agrupa con otros menús [`Dropdown`].
/// Devuelve la clase base asociada a la dirección del menú.
///
/// Cuando `grouped` es `true` y la variante es `Default`, devuelve `""` porque el grupo de
/// botones ya implica el contenedor; en caso contrario devuelve `"dropdown"`.
#[rustfmt::skip]
#[inline]
const fn as_str(self, grouped: bool) -> &'static str {
pub const fn as_str(self, grouped: bool) -> &'static str {
match self {
Self::Default if grouped => "",
Self::Default => "dropdown",
@ -74,10 +77,11 @@ impl Direction {
}
}
/// Añade la dirección de despliegue a la cadena de clases teniendo en cuenta si se agrupa con
/// otros menús [`Dropdown`].
/// Añade la clase asociada a la dirección del menú a la cadena de clases.
///
/// Ver [`as_str()`](Self::as_str) para el efecto del parámetro `grouped`.
#[inline]
pub(crate) fn push_class(self, classes: &mut String, grouped: bool) {
pub fn push_to(self, classes: &mut String, grouped: bool) {
if grouped {
if !classes.is_empty() {
classes.push(' ');
@ -93,19 +97,19 @@ impl Direction {
}
}
/// Devuelve la clase asociada a la dirección teniendo en cuenta si se agrupa con otros menús
/// [`Dropdown`], o `""` si no corresponde ninguna.
#[doc(hidden)]
pub fn class_with(self, grouped: bool) -> String {
/// Devuelve la clase asociada a la dirección del menú.
///
/// Ver [`as_str()`](Self::as_str) para el efecto del parámetro `grouped`.
pub fn to_class(self, grouped: bool) -> String {
let mut classes = String::new();
self.push_class(&mut classes, grouped);
self.push_to(&mut classes, grouped);
classes
}
}
// **< MenuAlign >**********************************************************************************
/// Alineación horizontal del menú desplegable [`Dropdown`].
/// Alineación horizontal del menú desplegable [`Dropdown`](crate::theme::bs::Dropdown).
///
/// Permite alinear el menú al inicio o al final del botón (respetando LTR/RTL) y añadirle una
/// alineación diferente a partir de un punto de ruptura ([`BreakPoint`]).
@ -115,80 +119,65 @@ pub enum MenuAlign {
#[default]
Start,
/// Alineación al inicio a partir del punto de ruptura indicado.
StartAt(BreakPoint),
StartAt(token::BreakPoint),
/// Alineación al inicio por defecto, y al final a partir de un punto de ruptura válido.
StartAndEnd(BreakPoint),
StartAndEnd(token::BreakPoint),
/// Alineación al final.
End,
/// Alineación al final a partir del punto de ruptura indicado.
EndAt(BreakPoint),
EndAt(token::BreakPoint),
/// Alineación al final por defecto, y al inicio a partir de un punto de ruptura válido.
EndAndStart(BreakPoint),
EndAndStart(token::BreakPoint),
}
impl MenuAlign {
/// Añade las clases de alineación a la cadena de clases (sin incluir la base `dropdown-menu`).
#[inline]
fn push_one(classes: &mut String, class: &str) {
if class.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(class);
}
/// Añade las clases de alineación a `classes` (sin incluir la base `dropdown-menu`).
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
pub fn push_to(self, classes: &mut String) {
match self {
// Alineación por defecto (start), no añade clases extra.
Self::Start => {}
// `dropdown-menu-{bp}-start`
Self::StartAt(bp) => {
let class = bp.class_with("dropdown-menu", "start");
Self::push_one(classes, &class);
bp.push_to(classes, "dropdown-menu", "start");
}
// `dropdown-menu-start` + `dropdown-menu-{bp}-end`
Self::StartAndEnd(bp) => {
Self::push_one(classes, "dropdown-menu-start");
let bp_class = bp.class_with("dropdown-menu", "end");
Self::push_one(classes, &bp_class);
token::BreakPoint::None.push_to(classes, "dropdown-menu", "start");
bp.push_to(classes, "dropdown-menu", "end");
}
// `dropdown-menu-end`
Self::End => {
Self::push_one(classes, "dropdown-menu-end");
token::BreakPoint::None.push_to(classes, "dropdown-menu", "end");
}
// `dropdown-menu-{bp}-end`
Self::EndAt(bp) => {
let class = bp.class_with("dropdown-menu", "end");
Self::push_one(classes, &class);
bp.push_to(classes, "dropdown-menu", "end");
}
// `dropdown-menu-end` + `dropdown-menu-{bp}-start`
Self::EndAndStart(bp) => {
Self::push_one(classes, "dropdown-menu-end");
let bp_class = bp.class_with("dropdown-menu", "start");
Self::push_one(classes, &bp_class);
token::BreakPoint::None.push_to(classes, "dropdown-menu", "end");
bp.push_to(classes, "dropdown-menu", "start");
}
}
}
/* Devuelve las clases de alineación sin incluir `dropdown-menu` (reservado).
/// Devuelve las clases de alineación del menú (sin incluir la base `dropdown-menu`).
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
self.push_to(&mut classes);
classes
} */
}
}
// **< MenuPosition >*******************************************************************************
/// Posición relativa del menú desplegable [`Dropdown`].
/// Posición relativa del menú desplegable [`Dropdown`](crate::theme::bs::Dropdown).
///
/// Permite indicar un desplazamiento (*offset*) manual o referenciar al elemento padre para el
/// cálculo de la posición.
@ -200,12 +189,12 @@ pub enum MenuPosition {
/// Desplazamiento manual en píxeles `(x, y)` aplicado al menú. Se admiten valores negativos.
Offset(i8, i8),
/// Posiciona el menú tomando como referencia el botón padre. Especialmente útil cuando
/// [`button_split()`](crate::theme::Dropdown::button_split) es `true`.
/// [`button_split()`](crate::theme::bs::Dropdown::button_split) es `true`.
Parent,
}
impl MenuPosition {
/// Devuelve el valor para `data-bs-offset` o `None` si no aplica.
// Devuelve el valor para `data-bs-offset` o `None` si no aplica.
#[inline]
pub(crate) fn data_offset(self) -> Option<String> {
match self {
@ -214,7 +203,7 @@ impl MenuPosition {
}
}
/// Devuelve el valor para `data-bs-reference` o `None` si no aplica.
// Devuelve el valor para `data-bs-reference` o `None` si no aplica.
#[inline]
pub(crate) fn data_reference(self) -> Option<&'static str> {
match self {

View file

@ -11,10 +11,10 @@ pub use pagetop::base::component::form::input::{Field, Kind, Mode};
/// enfocado.
///
/// ```rust,no_run
/// # use pagetop::locale::L10n;
/// # use pagetop::prelude::*;
/// use pagetop_bootsier::theme::*;
///
/// let nombre = form::input::Field::text()
/// let nombre = bs::form::input::Field::text()
/// .with_name("name")
/// .with_label(L10n::n("Name"))
/// .with_placeholder(L10n::n("Enter your name"))

View file

@ -11,17 +11,16 @@ pub use pagetop::base::component::form::select::{Entry, Field, Group, Item};
/// una selección activa.
///
/// ```rust,no_run
/// # use pagetop::locale::L10n;
/// # use pagetop::core::component::Component;
/// # use pagetop::prelude::*;
/// use pagetop_bootsier::theme::*;
///
/// let language = form::select::Field::new()
/// let language = bs::form::select::Field::new()
/// .with_name("language")
/// .with_label(L10n::n("Language"))
/// .with_floating_label(true)
/// .with_item(form::select::Item::new("", L10n::n("— Choose —")).with_selected(true))
/// .with_item(form::select::Item::new("es", L10n::n("Spanish")))
/// .with_item(form::select::Item::new("en", L10n::n("English")));
/// .with_item(bs::form::select::Item::new("", L10n::n("— Choose —")).with_selected(true))
/// .with_item(bs::form::select::Item::new("es", L10n::n("Spanish")))
/// .with_item(bs::form::select::Item::new("en", L10n::n("English")));
/// ```
pub trait SelectBootsier {
/// Establece si la etiqueta se muestra flotante sobre el campo.

View file

@ -11,11 +11,10 @@ pub use pagetop::base::component::form::Textarea;
/// una selección activa.
///
/// ```rust,no_run
/// # use pagetop::locale::L10n;
/// # use pagetop::core::component::Component;
/// # use pagetop::prelude::*;
/// use pagetop_bootsier::theme::*;
///
/// let comentario = form::Textarea::new()
/// let comentario = bs::form::Textarea::new()
/// .with_name("comment")
/// .with_label(L10n::n("Comment"))
/// .with_placeholder(L10n::n("Write here..."))

View file

@ -2,23 +2,24 @@ use pagetop::prelude::*;
use crate::theme::*;
/// Componente para renderizar una **imagen** ([`image`]).
/// Componente para renderizar una **imagen** ([`image`](crate::theme::bs::image)).
///
/// A una imagen se le puede:
///
/// - Establecer su contenido a partir del origen definido en [`image::Source`].
/// - Establecer su contenido a partir del origen definido en
/// [`image::Source`](crate::theme::bs::image::Source).
/// - Configurar sus **dimensiones** ([`with_size()`](Self::with_size)), **borde**
/// ([`classes::Border`](crate::theme::classes::Border)) y **redondeo de esquinas**
/// ([`classes::Rounded`](crate::theme::classes::Rounded)).
/// ([`Border`](crate::theme::class::Border)) y **redondeo de esquinas**
/// ([`Rounded`](crate::theme::class::Rounded)).
/// - Aplicar el texto alternativo `alt` con **localización** mediante [`L10n`].
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Image {
/// Devuelve identificador, clases CSS y atributos HTML del componente.
props: Props,
/// Devuelve las dimensiones de la imagen.
size: image::Size,
size: bs::image::Size,
/// Devuelve el origen de la imagen.
source: image::Source,
source: bs::image::Source,
/// Devuelve el texto alternativo localizado.
alternative: Attr<L10n>,
}
@ -42,7 +43,7 @@ impl Component for Image {
let alt_text = self.alternative().lookup(cx).unwrap_or_default();
let is_decorative = alt_text.is_empty();
let source = match self.source() {
image::Source::Logo(logo) => {
bs::image::Source::Logo(logo) => {
return Ok(html! {
span
(self.props())
@ -55,9 +56,9 @@ impl Component for Image {
}
});
}
image::Source::Responsive(source) => Some(source),
image::Source::Thumbnail(source) => Some(source),
image::Source::Plain(source) => Some(source),
bs::image::Source::Responsive(source) => Some(source),
bs::image::Source::Thumbnail(source) => Some(source),
bs::image::Source::Plain(source) => Some(source),
};
Ok(html! {
img
@ -71,7 +72,7 @@ impl Component for Image {
impl Image {
/// Crea rápidamente una imagen especificando su origen.
pub fn with(source: image::Source) -> Self {
pub fn with(source: bs::image::Source) -> Self {
Self::default().with_source(source)
}
@ -88,8 +89,8 @@ impl Image {
///
/// También acepta clases predefinidas para:
///
/// - Establecer bordes ([`classes::Border`]).
/// - Redondear las esquinas ([`classes::Rounded`]).
/// - Establecer bordes ([`Border`]).
/// - Redondear las esquinas ([`Rounded`]).
#[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op);
@ -98,14 +99,14 @@ impl Image {
/// Define las dimensiones de la imagen (auto, ancho/alto, ambos).
#[builder_fn]
pub fn with_size(mut self, size: image::Size) -> Self {
pub fn with_size(mut self, size: bs::image::Size) -> Self {
self.size = size;
self
}
/// Establece el origen de la imagen, influyendo en su disposición en el contenido.
#[builder_fn]
pub fn with_source(mut self, source: image::Source) -> Self {
pub fn with_source(mut self, source: bs::image::Source) -> Self {
self.source = source;
self
}

View file

@ -2,7 +2,7 @@ use pagetop::prelude::*;
// **< Size >***************************************************************************************
/// Define las **dimensiones** de una imagen ([`Image`](crate::theme::Image)).
/// Define las **dimensiones** de una imagen ([`Image`](crate::theme::bs::Image)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Size {
/// Ajuste automático por defecto.
@ -33,7 +33,7 @@ pub enum Size {
impl Size {
/// Devuelve el valor del atributo `style` en función del tamaño, o `None` si no aplica.
#[inline]
pub(crate) fn to_style(self) -> Option<String> {
pub fn to_style(self) -> Option<String> {
match self {
Self::Auto => None,
Self::Dimensions(w, h) => Some(format!("width: {w}; height: {h};")),
@ -46,7 +46,7 @@ impl Size {
// **< Source >*************************************************************************************
/// Especifica la **fuente** para publicar una imagen ([`Image`](crate::theme::Image)).
/// Especifica la **fuente** para publicar una imagen ([`Image`](crate::theme::bs::Image)).
#[derive(AutoDefault, Clone, Debug, PartialEq)]
pub enum Source {
/// Imagen con el logotipo de PageTop.
@ -96,7 +96,7 @@ impl Source {
/// Devuelve la clase base asociada a la imagen según la fuente.
#[inline]
fn as_str(&self) -> &'static str {
pub const fn as_str(&self) -> &'static str {
match self {
Source::Logo(_) | Source::Responsive(_) => Self::IMG_FLUID,
Source::Thumbnail(_) => Self::IMG_THUMBNAIL,
@ -104,9 +104,9 @@ impl Source {
}
}
/* Añade la clase base asociada a la imagen según la fuente a la cadena de clases (reservado).
/// Añade la clase asociada al tipo de imagen a la cadena de clases.
#[inline]
pub(crate) fn push_class(&self, classes: &mut String) {
pub fn push_to(&self, classes: &mut String) {
let s = self.as_str();
if s.is_empty() {
return;
@ -115,17 +115,12 @@ impl Source {
classes.push(' ');
}
classes.push_str(s);
} */
}
/// Devuelve la clase asociada a la imagen según la fuente.
/// Devuelve la clase asociada al tipo de imagen.
pub fn to_class(&self) -> String {
let s = self.as_str();
if s.is_empty() {
String::new()
} else {
let mut class = String::with_capacity(s.len());
class.push_str(s);
let mut class = String::new();
self.push_to(&mut class);
class
}
}
}

View file

@ -1,8 +1,8 @@
//! Definiciones para crear menús ([`Nav`]).
//!
//! Cada [`nav::Item`](crate::theme::nav::Item) representa un elemento individual del menú [`Nav`],
//! con distintos comportamientos según su finalidad, como enlaces de navegación o menús
//! desplegables [`Dropdown`](crate::theme::Dropdown).
//! Cada [`nav::Item`](crate::theme::bs::nav::Item) representa un elemento individual del menú
//! [`Nav`], con distintos comportamientos según su finalidad, como enlaces de navegación o menús
//! desplegables [`Dropdown`](crate::theme::bs::Dropdown).
//!
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y
//! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n).

View file

@ -2,11 +2,13 @@ use pagetop::prelude::*;
use crate::theme::*;
/// Componente para crear un **menú** ([`nav`]).
/// Componente para crear un **menú** ([`nav`](crate::theme::bs::nav)).
///
/// Presenta un menú con una lista de elementos usando una vista básica, o alguna de sus variantes
/// ([`nav::Kind`]) como *pestañas* (`Tabs`), *botones* (`Pills`) o *subrayado* (`Underline`).
/// También permite controlar su distribución y orientación ([`nav::Layout`](crate::theme::nav::Layout)).
/// ([`nav::Kind`](crate::theme::bs::nav::Kind)) como *pestañas* (`Tabs`), *botones* (`Pills`) o
/// *subrayado* (`Underline`).
/// También permite controlar su distribución y orientación
/// ([`nav::Layout`](crate::theme::bs::nav::Layout)).
///
/// Si no contiene elementos, el componente **no se renderiza**.
///
@ -16,28 +18,28 @@ use crate::theme::*;
/// use pagetop::prelude::*;
/// use pagetop_bootsier::theme::*;
///
/// let nav = Nav::tabs()
/// .with_layout(nav::Layout::End)
/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
/// .with_item(nav::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into()))
/// .with_item(nav::Item::dropdown(
/// Dropdown::new()
/// let nav = bs::Nav::tabs()
/// .with_layout(bs::nav::Layout::End)
/// .with_item(bs::nav::Item::link(L10n::n("Home"), |_| "/".into()))
/// .with_item(bs::nav::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into()))
/// .with_item(bs::nav::Item::dropdown(
/// bs::Dropdown::new()
/// .with_title(L10n::n("Options"))
/// .with_item(ChildOp::AddMany(vec![
/// dropdown::Item::link(L10n::n("Action"), |_| "/action".into()).into(),
/// dropdown::Item::link(L10n::n("Another"), |_| "/another".into()).into(),
/// bs::dropdown::Item::link(L10n::n("Action"), |_| "/action".into()).into(),
/// bs::dropdown::Item::link(L10n::n("Another"), |_| "/another".into()).into(),
/// ])),
/// ))
/// .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()));
/// .with_item(bs::nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()));
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Nav {
/// Devuelve identificador, clases CSS y atributos HTML del componente.
props: Props,
/// Devuelve el estilo visual seleccionado.
nav_kind: nav::Kind,
nav_kind: bs::nav::Kind,
/// Devuelve la distribución y orientación seleccionada.
nav_layout: nav::Layout,
nav_layout: bs::nav::Layout,
/// Devuelve la lista de elementos del menú.
items: Children,
}
@ -55,8 +57,8 @@ impl Component for Nav {
// Clases CSS por defecto para el menú, según el estilo y la distribución seleccionados.
self.alter_prop(PropsOp::prepend_classes({
let mut classes = "nav".to_string();
self.nav_kind().push_class(&mut classes);
self.nav_layout().push_class(&mut classes);
self.nav_kind().push_to(&mut classes);
self.nav_layout().push_to(&mut classes);
classes
}));
}
@ -78,17 +80,17 @@ impl Component for Nav {
impl Nav {
/// Crea un `Nav` usando pestañas para los elementos (*Tabs*).
pub fn tabs() -> Self {
Self::default().with_kind(nav::Kind::Tabs)
Self::default().with_kind(bs::nav::Kind::Tabs)
}
/// Crea un `Nav` usando botones para los elementos (*Pills*).
pub fn pills() -> Self {
Self::default().with_kind(nav::Kind::Pills)
Self::default().with_kind(bs::nav::Kind::Pills)
}
/// Crea un `Nav` usando elementos subrayados (*Underline*).
pub fn underline() -> Self {
Self::default().with_kind(nav::Kind::Underline)
Self::default().with_kind(bs::nav::Kind::Underline)
}
// **< Nav BUILDER >****************************************************************************
@ -109,14 +111,14 @@ impl Nav {
/// Cambia el estilo del menú (*Tabs*, *Pills*, *Underline* o *Default*).
#[builder_fn]
pub fn with_kind(mut self, kind: nav::Kind) -> Self {
pub fn with_kind(mut self, kind: bs::nav::Kind) -> Self {
self.nav_kind = kind;
self
}
/// Selecciona la distribución y orientación del menú.
#[builder_fn]
pub fn with_layout(mut self, layout: nav::Layout) -> Self {
pub fn with_layout(mut self, layout: bs::nav::Layout) -> Self {
self.nav_layout = layout;
self
}

View file

@ -5,8 +5,8 @@ use crate::theme::*;
// **< ItemKind >***********************************************************************************
/// Tipos de [`nav::Item`](crate::theme::nav::Item) disponibles en un menú
/// [`Nav`](crate::theme::Nav).
/// Tipos de [`nav::Item`](crate::theme::bs::nav::Item) disponibles en un menú
/// [`Nav`](crate::theme::bs::Nav).
///
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
/// con él.
@ -29,49 +29,30 @@ pub enum ItemKind {
/// Contenido HTML arbitrario. El componente [`Html`] se renderiza tal cual como elemento del
/// menú, sin añadir ningún comportamiento de navegación adicional.
Html(Embed<Html>),
/// Elemento que despliega un menú [`Dropdown`].
Dropdown(Embed<Dropdown>),
/// Elemento que despliega un menú [`Dropdown`](crate::theme::bs::Dropdown).
Dropdown(Embed<bs::Dropdown>),
}
impl ItemKind {
const ITEM: &str = "nav-item";
const DROPDOWN: &str = "nav-item dropdown";
// Devuelve las clases base asociadas al tipo de elemento.
/// Devuelve las clases base asociadas al tipo de elemento.
#[inline]
const fn as_str(&self) -> &'static str {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Void => "",
Self::Dropdown(_) => Self::DROPDOWN,
_ => Self::ITEM,
}
}
/* Añade las clases asociadas al tipo de elemento a la cadena de clases (reservado).
#[inline]
pub(crate) fn push_class(&self, classes: &mut String) {
let class = self.as_str();
if class.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(class);
} */
/// Devuelve las clases asociadas al tipo de elemento.
#[inline]
pub(crate) fn to_class(&self) -> String {
self.as_str().to_owned()
}
}
// **< Item >***************************************************************************************
/// Representa un **elemento individual** de un menú [`Nav`](crate::theme::Nav).
/// Representa un **elemento individual** de un menú [`Nav`](crate::theme::bs::Nav).
///
/// Cada instancia de [`nav::Item`](crate::theme::nav::Item) se traduce en un componente visible que
/// Cada instancia de [`nav::Item`](crate::theme::bs::nav::Item) se traduce en un componente visible que
/// puede comportarse como texto, enlace, contenido HTML o menú desplegable, según su [`ItemKind`].
///
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
@ -94,7 +75,7 @@ impl Component for Item {
}
fn setup(&mut self, _cx: &Context) {
self.alter_prop(PropsOp::prepend_classes(self.item_kind().to_class()));
self.alter_prop(PropsOp::prepend_classes(self.item_kind().as_str()));
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
@ -267,12 +248,13 @@ impl Item {
}
}
/// Crea un elemento de navegación que contiene un menú desplegable [`Dropdown`].
/// Crea un elemento de navegación que contiene un menú desplegable
/// [`Dropdown`](crate::theme::bs::Dropdown).
///
/// Sólo se tienen en cuenta **el título** (si no existe, se asigna uno por defecto) y **la
/// lista de elementos** del [`Dropdown`]; el resto de propiedades del componente no afectarán
/// a su representación en [`Nav`].
pub fn dropdown(menu: Dropdown) -> Self {
/// lista de elementos** del [`Dropdown`](crate::theme::bs::Dropdown); el resto de propiedades
/// del componente no afectarán a su representación en [`Nav`](crate::theme::bs::Nav).
pub fn dropdown(menu: bs::Dropdown) -> Self {
Self {
item_kind: ItemKind::Dropdown(Embed::with(menu)),
..Default::default()

View file

@ -2,7 +2,7 @@ use pagetop::prelude::*;
// **< Kind >***************************************************************************************
/// Define la variante de presentación de un menú [`Nav`](crate::theme::Nav).
/// Define la variante de presentación de un menú [`Nav`](crate::theme::bs::Nav).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Kind {
/// Estilo por defecto, lista de enlaces flexible y minimalista.
@ -24,7 +24,7 @@ impl Kind {
/// Devuelve la clase base asociada al tipo de menú, o una cadena vacía si no aplica.
#[rustfmt::skip]
#[inline]
const fn as_str(self) -> &'static str {
pub const fn as_str(self) -> &'static str {
match self {
Self::Default => "",
Self::Tabs => Self::TABS,
@ -35,7 +35,7 @@ impl Kind {
/// Añade la clase asociada al tipo de menú a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
pub fn push_to(self, classes: &mut String) {
let class = self.as_str();
if class.is_empty() {
return;
@ -46,16 +46,17 @@ impl Kind {
classes.push_str(class);
}
/* Devuelve la clase asociada al tipo de menú, o una cadena vacía si no aplica (reservado).
#[inline]
pub(crate) fn to_class(self) -> String {
self.as_str().to_owned()
} */
/// Devuelve la clase asociada al tipo de menú.
pub fn to_class(self) -> String {
let mut class = String::new();
self.push_to(&mut class);
class
}
}
// **< Layout >*************************************************************************************
/// Distribución y orientación de un menú [`Nav`](crate::theme::Nav).
/// Distribución y orientación de un menú [`Nav`](crate::theme::bs::Nav).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Layout {
/// Comportamiento por defecto, ancho definido por el contenido y sin alineación forzada.
@ -86,7 +87,7 @@ impl Layout {
/// Devuelve la clase base asociada a la distribución y orientación del menú.
#[rustfmt::skip]
#[inline]
const fn as_str(self) -> &'static str {
pub const fn as_str(self) -> &'static str {
match self {
Self::Default => "",
Self::Start => Self::START,
@ -100,7 +101,7 @@ impl Layout {
/// Añade la clase asociada a la distribución y orientación del menú a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
pub fn push_to(self, classes: &mut String) {
let class = self.as_str();
if class.is_empty() {
return;
@ -111,10 +112,10 @@ impl Layout {
classes.push_str(class);
}
/* Devuelve la clase asociada a la distribución y orientación del menú, o una cadena vacía si no
/// aplica (reservado).
#[inline]
pub(crate) fn to_class(self) -> String {
self.as_str().to_owned()
} */
/// Devuelve la clase asociada a la distribución y orientación del menú.
pub fn to_class(self) -> String {
let mut class = String::new();
self.push_to(&mut class);
class
}
}

View file

@ -0,0 +1,20 @@
//! Definiciones para crear barras de navegación ([`Navbar`]).
//!
//! Cada [`navbar::Item`](crate::theme::bs::navbar::Item) representa un elemento individual de la
//! barra de navegación [`Navbar`], con distintos comportamientos según su finalidad, como menús
//! [`Nav`](crate::theme::bs::Nav) o *textos localizados* usando [`L10n`](pagetop::locale::L10n).
//!
//! También puede añadir una marca de identidad ([`navbar::Brand`](crate::theme::bs::navbar::Brand))
//! que identifique la compañía, producto o nombre del proyecto asociado a la solución web.
mod props;
pub use props::{Layout, Position};
mod brand;
pub use brand::Brand;
mod component;
pub use component::Navbar;
mod item;
pub use item::Item;

View file

@ -2,7 +2,7 @@ use pagetop::prelude::*;
use crate::theme::*;
/// Marca de identidad para mostrar en una barra de navegación [`Navbar`].
/// Marca de identidad para mostrar en una barra de navegación [`Navbar`](crate::theme::bs::Navbar).
///
/// Representa la identidad del sitio con una imagen, título y eslogan:
///
@ -14,7 +14,7 @@ use crate::theme::*;
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Brand {
/// Devuelve la imagen de marca (si la hay).
image: Embed<Image>,
image: Embed<bs::Image>,
/// Devuelve el título de la identidad de marca.
#[default(_code = "L10n::n(&global::SETTINGS.app.name)")]
title: L10n,
@ -52,7 +52,7 @@ impl Brand {
/// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará.
#[builder_fn]
pub fn with_image(mut self, image: Option<Image>) -> Self {
pub fn with_image(mut self, image: Option<bs::Image>) -> Self {
self.image.alter_component(image);
self
}

View file

@ -6,11 +6,12 @@ use crate::theme::*;
const TOGGLE_COLLAPSE: &str = "collapse";
const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// Componente para crear una **barra de navegación** ([`navbar`]).
/// Componente para crear una **barra de navegación** ([`navbar`](crate::theme::bs::navbar)).
///
/// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con
/// botón de despliegue o dentro de un [`offcanvas`]), controladas por [`navbar::Layout`]. También
/// puede fijarse en la parte superior o inferior del documento mediante [`navbar::Position`].
/// botón de despliegue o dentro de un [`Offcanvas`](crate::theme::bs::Offcanvas)), controladas por
/// [`navbar::Layout`](crate::theme::bs::navbar::Layout). También puede fijarse en la parte superior
/// o inferior del documento mediante [`navbar::Position`](crate::theme::bs::navbar::Position).
///
/// Si no contiene elementos, el componente **no se renderiza**.
///
@ -22,12 +23,12 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// use pagetop::prelude::*;
/// use pagetop_bootsier::theme::*;
///
/// let navbar = Navbar::simple()
/// .with_item(navbar::Item::nav(
/// Nav::new()
/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
/// .with_item(nav::Item::link(L10n::n("About"), |_| "/about".into()))
/// .with_item(nav::Item::link(L10n::n("Contact"), |_| "/contact".into()))
/// let navbar = bs::Navbar::simple()
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::new()
/// .with_item(bs::nav::Item::link(L10n::n("Home"), |_| "/".into()))
/// .with_item(bs::nav::Item::link(L10n::n("About"), |_| "/about".into()))
/// .with_item(bs::nav::Item::link(L10n::n("Contact"), |_| "/contact".into()))
/// ));
/// ```
///
@ -36,13 +37,13 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let navbar = Navbar::simple_toggle()
/// .with_expand(BreakPoint::MD)
/// .with_item(navbar::Item::nav(
/// Nav::new()
/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
/// .with_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into()))
/// .with_item(nav::Item::link(L10n::n("Support"), |_| "/support".into()))
/// let navbar = bs::Navbar::simple_toggle()
/// .with_expand(token::BreakPoint::MD)
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::new()
/// .with_item(bs::nav::Item::link(L10n::n("Home"), |_| "/".into()))
/// .with_item(bs::nav::Item::link_blank(L10n::n("Doc"), |_| "https://docs.rs".into()))
/// .with_item(bs::nav::Item::link(L10n::n("Support"), |_| "/support".into()))
/// ));
/// ```
///
@ -51,25 +52,25 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let brand = navbar::Brand::new()
/// let brand = bs::navbar::Brand::new()
/// .with_title(L10n::n("PageTop"))
/// .with_route(Some(|cx| cx.route("/")));
///
/// let navbar = Navbar::brand_left(brand)
/// .with_item(navbar::Item::nav(
/// Nav::new()
/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
/// .with_item(nav::Item::dropdown(
/// Dropdown::new()
/// let navbar = bs::Navbar::brand_left(brand)
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::new()
/// .with_item(bs::nav::Item::link(L10n::n("Home"), |_| "/".into()))
/// .with_item(bs::nav::Item::dropdown(
/// bs::Dropdown::new()
/// .with_title(L10n::n("Tools"))
/// .with_item(dropdown::Item::link(
/// .with_item(bs::dropdown::Item::link(
/// L10n::n("Generator"), |_| "/tools/gen".into())
/// )
/// .with_item(dropdown::Item::link(
/// .with_item(bs::dropdown::Item::link(
/// L10n::n("Reports"), |_| "/tools/reports".into())
/// )
/// ))
/// .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()))
/// .with_item(bs::nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()))
/// ));
/// ```
///
@ -78,16 +79,16 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let brand = navbar::Brand::new()
/// let brand = bs::navbar::Brand::new()
/// .with_title(L10n::n("Intranet"))
/// .with_route(Some(|cx| cx.route("/")));
///
/// let navbar = Navbar::brand_right(brand)
/// .with_expand(BreakPoint::LG)
/// .with_item(navbar::Item::nav(
/// Nav::pills()
/// .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard".into()))
/// .with_item(nav::Item::link(L10n::n("Users"), |_| "/users".into()))
/// let navbar = bs::Navbar::brand_right(brand)
/// .with_expand(token::BreakPoint::LG)
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::pills()
/// .with_item(bs::nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard".into()))
/// .with_item(bs::nav::Item::link(L10n::n("Users"), |_| "/users".into()))
/// ));
/// ```
///
@ -96,22 +97,22 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let oc = Offcanvas::new()
/// let oc = bs::Offcanvas::new()
/// .with_id("main_offcanvas")
/// .with_title(L10n::n("Main menu"))
/// .with_placement(offcanvas::Placement::Start)
/// .with_backdrop(offcanvas::Backdrop::Enabled);
/// .with_placement(bs::offcanvas::Placement::Start)
/// .with_backdrop(bs::offcanvas::Backdrop::Enabled);
///
/// let navbar = Navbar::offcanvas(oc)
/// .with_item(navbar::Item::nav(
/// Nav::new()
/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
/// .with_item(nav::Item::link(L10n::n("Profile"), |_| "/profile".into()))
/// .with_item(nav::Item::dropdown(
/// Dropdown::new()
/// let navbar = bs::Navbar::offcanvas(oc)
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::new()
/// .with_item(bs::nav::Item::link(L10n::n("Home"), |_| "/".into()))
/// .with_item(bs::nav::Item::link(L10n::n("Profile"), |_| "/profile".into()))
/// .with_item(bs::nav::Item::dropdown(
/// bs::Dropdown::new()
/// .with_title(L10n::n("More"))
/// .with_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings".into()))
/// .with_item(dropdown::Item::link(L10n::n("Help"), |_| "/help".into()))
/// .with_item(bs::dropdown::Item::link(L10n::n("Settings"), |_| "/settings".into()))
/// .with_item(bs::dropdown::Item::link(L10n::n("Help"), |_| "/help".into()))
/// ))
/// ));
/// ```
@ -121,17 +122,17 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let brand = navbar::Brand::new()
/// let brand = bs::navbar::Brand::new()
/// .with_title(L10n::n("Main App"))
/// .with_route(Some(|cx| cx.route("/")));
///
/// let navbar = Navbar::brand_left(brand)
/// .with_position(navbar::Position::FixedTop)
/// .with_item(navbar::Item::nav(
/// Nav::new()
/// .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/".into()))
/// .with_item(nav::Item::link(L10n::n("Donors"), |_| "/donors".into()))
/// .with_item(nav::Item::link(L10n::n("Stock"), |_| "/stock".into()))
/// let navbar = bs::Navbar::brand_left(brand)
/// .with_position(bs::navbar::Position::FixedTop)
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::new()
/// .with_item(bs::nav::Item::link(L10n::n("Dashboard"), |_| "/".into()))
/// .with_item(bs::nav::Item::link(L10n::n("Donors"), |_| "/donors".into()))
/// .with_item(bs::nav::Item::link(L10n::n("Stock"), |_| "/stock".into()))
/// ));
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
@ -139,11 +140,11 @@ pub struct Navbar {
/// Devuelve identificador, clases CSS y atributos HTML del componente.
props: Props,
/// Devuelve el punto de ruptura configurado.
expand: BreakPoint,
expand: token::BreakPoint,
/// Devuelve la disposición configurada para la barra de navegación.
layout: navbar::Layout,
layout: bs::navbar::Layout,
/// Devuelve la posición configurada para la barra de navegación.
position: navbar::Position,
position: bs::navbar::Position,
/// Devuelve la lista de contenidos.
items: Children,
}
@ -164,8 +165,8 @@ impl Component for Navbar {
// Clases CSS por defecto para la barra de navegación.
self.alter_prop(PropsOp::prepend_classes({
let mut classes = "navbar".to_string();
self.expand().push_class(&mut classes, "navbar-expand", "");
self.position().push_class(&mut classes);
self.expand().push_to(&mut classes, "navbar-expand", "");
self.position().push_to(&mut classes);
classes
}));
}
@ -208,12 +209,12 @@ impl Component for Navbar {
div class="container-fluid" {
@match self.layout() {
// Barra más sencilla: sólo contenido.
navbar::Layout::Simple => {
bs::navbar::Layout::Simple => {
(items)
},
// Barra sencilla que se puede contraer/expandir.
navbar::Layout::SimpleToggle => {
bs::navbar::Layout::SimpleToggle => {
@let id_content = util::join!(id, "-content");
(button(cx, TOGGLE_COLLAPSE, &id_content))
@ -223,13 +224,13 @@ impl Component for Navbar {
},
// Barra con marca a la izquierda, siempre visible.
navbar::Layout::SimpleBrandLeft(brand) => {
bs::navbar::Layout::SimpleBrandLeft(brand) => {
(brand.render(cx))
(items)
},
// Barra con marca a la izquierda y botón a la derecha.
navbar::Layout::BrandLeft(brand) => {
bs::navbar::Layout::BrandLeft(brand) => {
@let id_content = util::join!(id, "-content");
(brand.render(cx))
@ -240,7 +241,7 @@ impl Component for Navbar {
},
// Barra con botón a la izquierda y marca a la derecha.
navbar::Layout::BrandRight(brand) => {
bs::navbar::Layout::BrandRight(brand) => {
@let id_content = util::join!(id, "-content");
(button(cx, TOGGLE_COLLAPSE, &id_content))
@ -251,7 +252,7 @@ impl Component for Navbar {
},
// Barra cuyo contenido se muestra en un offcanvas, sin marca.
navbar::Layout::Offcanvas(offcanvas) => {
bs::navbar::Layout::Offcanvas(offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(button(cx, TOGGLE_OFFCANVAS, &id_content))
@ -261,7 +262,7 @@ impl Component for Navbar {
},
// Barra con marca a la izquierda y contenido en offcanvas.
navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => {
bs::navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(brand.render(cx))
@ -272,7 +273,7 @@ impl Component for Navbar {
},
// Barra con contenido en offcanvas y marca a la derecha.
navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => {
bs::navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(button(cx, TOGGLE_OFFCANVAS, &id_content))
@ -291,45 +292,45 @@ impl Component for Navbar {
impl Navbar {
/// Crea una barra de navegación **simple**, sin marca y sin botón.
pub fn simple() -> Self {
Self::default().with_layout(navbar::Layout::Simple)
Self::default().with_layout(bs::navbar::Layout::Simple)
}
/// Crea una barra de navegación **simple pero colapsable**, con botón a la izquierda.
pub fn simple_toggle() -> Self {
Self::default().with_layout(navbar::Layout::SimpleToggle)
Self::default().with_layout(bs::navbar::Layout::SimpleToggle)
}
/// Crea una barra de navegación **con marca a la izquierda**, siempre visible.
pub fn simple_brand_left(brand: navbar::Brand) -> Self {
Self::default().with_layout(navbar::Layout::SimpleBrandLeft(Embed::with(brand)))
pub fn simple_brand_left(brand: bs::navbar::Brand) -> Self {
Self::default().with_layout(bs::navbar::Layout::SimpleBrandLeft(Embed::with(brand)))
}
/// Crea una barra de navegación con **marca a la izquierda** y **botón a la derecha**.
pub fn brand_left(brand: navbar::Brand) -> Self {
Self::default().with_layout(navbar::Layout::BrandLeft(Embed::with(brand)))
pub fn brand_left(brand: bs::navbar::Brand) -> Self {
Self::default().with_layout(bs::navbar::Layout::BrandLeft(Embed::with(brand)))
}
/// Crea una barra de navegación con **botón a la izquierda** y **marca a la derecha**.
pub fn brand_right(brand: navbar::Brand) -> Self {
Self::default().with_layout(navbar::Layout::BrandRight(Embed::with(brand)))
pub fn brand_right(brand: bs::navbar::Brand) -> Self {
Self::default().with_layout(bs::navbar::Layout::BrandRight(Embed::with(brand)))
}
/// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**.
pub fn offcanvas(oc: Offcanvas) -> Self {
Self::default().with_layout(navbar::Layout::Offcanvas(Embed::with(oc)))
pub fn offcanvas(oc: bs::Offcanvas) -> Self {
Self::default().with_layout(bs::navbar::Layout::Offcanvas(Embed::with(oc)))
}
/// Crea una barra de navegación con **marca a la izquierda** y contenido en **offcanvas**.
pub fn offcanvas_brand_left(brand: navbar::Brand, oc: Offcanvas) -> Self {
Self::default().with_layout(navbar::Layout::OffcanvasBrandLeft(
pub fn offcanvas_brand_left(brand: bs::navbar::Brand, oc: bs::Offcanvas) -> Self {
Self::default().with_layout(bs::navbar::Layout::OffcanvasBrandLeft(
Embed::with(brand),
Embed::with(oc),
))
}
/// Crea una barra de navegación con **marca a la derecha** y contenido en **offcanvas**.
pub fn offcanvas_brand_right(brand: navbar::Brand, oc: Offcanvas) -> Self {
Self::default().with_layout(navbar::Layout::OffcanvasBrandRight(
pub fn offcanvas_brand_right(brand: bs::navbar::Brand, oc: bs::Offcanvas) -> Self {
Self::default().with_layout(bs::navbar::Layout::OffcanvasBrandRight(
Embed::with(brand),
Embed::with(oc),
))
@ -348,8 +349,8 @@ impl Navbar {
///
/// También acepta clases predefinidas para:
///
/// - Modificar el color de fondo ([`classes::Background`]).
/// - Definir la apariencia del texto ([`classes::Text`]).
/// - Modificar el color de fondo ([`Background`]).
/// - Definir la apariencia del texto ([`Text`]).
#[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op);
@ -358,21 +359,21 @@ impl Navbar {
/// Define a partir de qué punto de ruptura la barra de navegación deja de colapsar.
#[builder_fn]
pub fn with_expand(mut self, bp: BreakPoint) -> Self {
pub fn with_expand(mut self, bp: token::BreakPoint) -> Self {
self.expand = bp;
self
}
/// Define el tipo de disposición que tendrá la barra de navegación.
#[builder_fn]
pub fn with_layout(mut self, layout: navbar::Layout) -> Self {
pub fn with_layout(mut self, layout: bs::navbar::Layout) -> Self {
self.layout = layout;
self
}
/// Define dónde se mostrará la barra de navegación dentro del documento.
#[builder_fn]
pub fn with_position(mut self, position: navbar::Position) -> Self {
pub fn with_position(mut self, position: bs::navbar::Position) -> Self {
self.position = position;
self
}

View file

@ -2,11 +2,12 @@ use pagetop::prelude::*;
use crate::theme::*;
/// Elementos que puede contener una barra de navegación [`Navbar`](crate::theme::Navbar).
/// Elementos que puede contener una barra de navegación [`Navbar`](crate::theme::bs::Navbar).
///
/// Cada variante determina qué se renderiza y cómo. Estos elementos se colocan **dentro del
/// contenido** de la barra (la parte colapsable, el *offcanvas* o el bloque simple), por lo que son
/// independientes de la marca o del botón que ya pueda definir el propio [`navbar::Layout`].
/// independientes de la marca o del botón que ya pueda definir el propio
/// [`navbar::Layout`](crate::theme::bs::navbar::Layout).
#[derive(AutoDefault, Clone, Debug)]
pub enum Item {
/// Sin contenido, no produce salida.
@ -14,12 +15,13 @@ pub enum Item {
Void,
/// Marca de identidad mostrada dentro del contenido de la barra de navegación.
///
/// Útil cuando el [`navbar::Layout`] no incluye marca, y se quiere incluir dentro del área
/// Útil cuando el [`navbar::Layout`](crate::theme::bs::navbar::Layout) no incluye marca, y se
/// quiere incluir dentro del área
/// colapsable/*offcanvas*. Si el *layout* ya muestra una marca, esta variante no la sustituye,
/// sólo añade otra dentro del bloque de contenidos.
Brand(Embed<navbar::Brand>),
/// Representa un menú de navegación [`Nav`](crate::theme::Nav).
Nav(Embed<Nav>),
Brand(Embed<bs::navbar::Brand>),
/// Representa un menú de navegación [`Nav`](crate::theme::bs::Nav).
Nav(Embed<bs::Nav>),
/// Representa un *texto localizado* libre.
Text(L10n),
}
@ -75,16 +77,16 @@ impl Component for Item {
}
impl Item {
/// Crea un elemento de tipo [`navbar::Brand`] para añadir en el contenido de [`Navbar`].
/// Crea un elemento de tipo [`navbar::Brand`](crate::theme::bs::navbar::Brand) para añadir en el contenido de [`Navbar`](crate::theme::bs::Navbar).
///
/// Pensado para barras colapsables u offcanvas donde se quiere que la marca aparezca en la zona
/// desplegable.
pub fn brand(brand: navbar::Brand) -> Self {
pub fn brand(brand: bs::navbar::Brand) -> Self {
Self::Brand(Embed::with(brand))
}
/// Crea un elemento de tipo [`Nav`] para añadir al contenido de [`Navbar`].
pub fn nav(item: Nav) -> Self {
/// Crea un elemento de tipo [`Nav`](crate::theme::bs::Nav) para añadir al contenido de [`Navbar`](crate::theme::bs::Navbar).
pub fn nav(item: bs::Nav) -> Self {
Self::Nav(Embed::with(item))
}

View file

@ -4,7 +4,8 @@ use crate::theme::*;
// **< Layout >*************************************************************************************
/// Representa los diferentes tipos de presentación de una barra de navegación [`Navbar`].
/// Representa los diferentes tipos de presentación de una barra de navegación
/// [`Navbar`](crate::theme::bs::Navbar).
#[derive(AutoDefault, Clone, Debug)]
pub enum Layout {
/// Barra simple, sin marca de identidad y sin botón de despliegue.
@ -19,29 +20,30 @@ pub enum Layout {
/// Barra simple, con marca de identidad a la izquierda y sin botón de despliegue.
///
/// La barra de navegación no se colapsa.
SimpleBrandLeft(Embed<navbar::Brand>),
SimpleBrandLeft(Embed<bs::navbar::Brand>),
/// Barra con marca de identidad a la izquierda y botón de despliegue a la derecha.
BrandLeft(Embed<navbar::Brand>),
BrandLeft(Embed<bs::navbar::Brand>),
/// Barra con botón de despliegue a la izquierda y marca de identidad a la derecha.
BrandRight(Embed<navbar::Brand>),
BrandRight(Embed<bs::navbar::Brand>),
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y sin marca de identidad.
Offcanvas(Embed<Offcanvas>),
/// Contenido en [`Offcanvas`](crate::theme::bs::Offcanvas), con botón de despliegue a la
/// izquierda y sin marca de identidad.
Offcanvas(Embed<bs::Offcanvas>),
/// Contenido en [`Offcanvas`], con marca de identidad a la izquierda y botón de despliegue a la
/// derecha.
OffcanvasBrandLeft(Embed<navbar::Brand>, Embed<Offcanvas>),
/// Contenido en [`Offcanvas`](crate::theme::bs::Offcanvas), con marca de identidad a la
/// izquierda y botón de despliegue a la derecha.
OffcanvasBrandLeft(Embed<bs::navbar::Brand>, Embed<bs::Offcanvas>),
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y marca de identidad a la
/// derecha.
OffcanvasBrandRight(Embed<navbar::Brand>, Embed<Offcanvas>),
/// Contenido en [`Offcanvas`](crate::theme::bs::Offcanvas), con botón de despliegue a la
/// izquierda y marca de identidad a la derecha.
OffcanvasBrandRight(Embed<bs::navbar::Brand>, Embed<bs::Offcanvas>),
}
// **< Position >***********************************************************************************
/// Posición global de una barra de navegación [`Navbar`] en el documento.
/// Posición global de una barra de navegación [`Navbar`](crate::theme::bs::Navbar) en el documento.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Position {
/// Barra normal, fluye con el documento.
@ -65,8 +67,9 @@ pub enum Position {
impl Position {
/// Devuelve la clase base asociada a la posición de la barra de navegación.
#[rustfmt::skip]
#[inline]
const fn as_str(self) -> &'static str {
pub const fn as_str(self) -> &'static str {
match self {
Self::Static => "",
Self::FixedTop => "fixed-top",
@ -78,7 +81,7 @@ impl Position {
/// Añade la clase asociada a la posición de la barra de navegación a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
pub fn push_to(self, classes: &mut String) {
let class = self.as_str();
if class.is_empty() {
return;
@ -89,10 +92,10 @@ impl Position {
classes.push_str(class);
}
/* Devuelve la clase asociada a la posición de la barra de navegación, o cadena vacía si no
/// aplica (reservado).
#[inline]
pub(crate) fn to_class(self) -> String {
self.as_str().to_string()
} */
/// Devuelve la clase asociada a la posición de la barra de navegación.
pub fn to_class(self) -> String {
let mut class = String::new();
self.push_to(&mut class);
class
}
}

View file

@ -3,7 +3,7 @@ use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
use crate::theme::*;
/// Componente para crear un **panel lateral deslizante** ([`offcanvas`]).
/// Componente para crear un **panel lateral deslizante** ([`offcanvas`](crate::theme::bs::offcanvas)).
///
/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes
/// características principales:
@ -27,18 +27,18 @@ use crate::theme::*;
/// use pagetop::prelude::*;
/// use pagetop_bootsier::theme::*;
///
/// let panel = Offcanvas::new()
/// let panel = bs::Offcanvas::new()
/// .with_id("offcanvas_example")
/// .with_title(L10n::n("Offcanvas title"))
/// .with_placement(offcanvas::Placement::End)
/// .with_backdrop(offcanvas::Backdrop::Enabled)
/// .with_body_scroll(offcanvas::BodyScroll::Enabled)
/// .with_visibility(offcanvas::Visibility::Default)
/// .with_child(Dropdown::new()
/// .with_placement(bs::offcanvas::Placement::End)
/// .with_backdrop(bs::offcanvas::Backdrop::Enabled)
/// .with_body_scroll(bs::offcanvas::BodyScroll::Enabled)
/// .with_visibility(bs::offcanvas::Visibility::Default)
/// .with_child(bs::Dropdown::new()
/// .with_title(L10n::n("Menu"))
/// .with_item(dropdown::Item::label(L10n::n("Label")))
/// .with_item(dropdown::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into()))
/// .with_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout".into()))
/// .with_item(bs::dropdown::Item::label(L10n::n("Label")))
/// .with_item(bs::dropdown::Item::link_blank(L10n::n("Doc"), |_| "https://docs.rs".into()))
/// .with_item(bs::dropdown::Item::link(L10n::n("Sign out"), |_| "/signout".into()))
/// );
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
@ -48,15 +48,15 @@ pub struct Offcanvas {
/// Devuelve el título del panel.
title: L10n,
/// Devuelve el punto de ruptura configurado para cambiar el comportamiento del panel.
breakpoint: BreakPoint,
breakpoint: token::BreakPoint,
/// Devuelve el comportamiento configurado para la capa de fondo.
backdrop: offcanvas::Backdrop,
backdrop: bs::offcanvas::Backdrop,
/// Indica si la página principal puede desplazarse mientras el panel está abierto.
body_scroll: offcanvas::BodyScroll,
body_scroll: bs::offcanvas::BodyScroll,
/// Devuelve la posición de inicio del panel.
placement: offcanvas::Placement,
placement: bs::offcanvas::Placement,
/// Devuelve el estado inicial del panel.
visibility: offcanvas::Visibility,
visibility: bs::offcanvas::Visibility,
/// Devuelve la lista de componentes (`children`) del panel.
children: Children,
}
@ -77,9 +77,9 @@ impl Component for Offcanvas {
// Clases CSS por defecto para el panel.
self.alter_prop(PropsOp::prepend_classes({
let mut classes = "offcanvas".to_string();
self.breakpoint().push_class(&mut classes, "offcanvas", "");
self.placement().push_class(&mut classes);
self.visibility().push_class(&mut classes);
self.breakpoint().push_to(&mut classes, "offcanvas", "");
self.placement().push_to(&mut classes);
self.visibility().push_to(&mut classes);
classes
}));
}
@ -119,11 +119,11 @@ impl Offcanvas {
/// ([`Offcanvas`]).
/// - **Por encima**, el contenido del panel se muestra tal cual, integrado en la página.
///
/// Por ejemplo, con `BreakPoint::LG`, será *offcanvas* en móviles y tabletas, y visible
/// Por ejemplo, con `BreakPoint::Lg`, será *offcanvas* en móviles y tabletas, y visible
/// directamente en pantallas grandes. Por defecto usa `BreakPoint::None` para que sea
/// *offcanvas* siempre.
#[builder_fn]
pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self {
pub fn with_breakpoint(mut self, bp: token::BreakPoint) -> Self {
self.breakpoint = bp;
self
}
@ -131,28 +131,28 @@ impl Offcanvas {
/// Ajusta la capa de fondo del panel para definir su comportamiento al hacer clic fuera del
/// panel.
#[builder_fn]
pub fn with_backdrop(mut self, backdrop: offcanvas::Backdrop) -> Self {
pub fn with_backdrop(mut self, backdrop: bs::offcanvas::Backdrop) -> Self {
self.backdrop = backdrop;
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: offcanvas::BodyScroll) -> Self {
pub fn with_body_scroll(mut self, scrolling: bs::offcanvas::BodyScroll) -> Self {
self.body_scroll = scrolling;
self
}
/// Indica desde qué borde de la ventana entra y se ancla el panel.
#[builder_fn]
pub fn with_placement(mut self, placement: offcanvas::Placement) -> Self {
pub fn with_placement(mut self, placement: bs::offcanvas::Placement) -> 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: offcanvas::Visibility) -> Self {
pub fn with_visibility(mut self, visibility: bs::offcanvas::Visibility) -> Self {
self.visibility = visibility;
self
}
@ -179,16 +179,8 @@ impl Offcanvas {
let id_label = util::join!(id, "-label");
let id_target = util::join!("#", id);
let body_scroll = match self.body_scroll() {
offcanvas::BodyScroll::Disabled => None,
offcanvas::BodyScroll::Enabled => Some("true"),
};
let backdrop = match self.backdrop() {
offcanvas::Backdrop::Disabled => Some("false"),
offcanvas::Backdrop::Enabled => None,
offcanvas::Backdrop::Static => Some("static"),
};
let body_scroll = self.body_scroll().opt_str();
let backdrop = self.backdrop().opt_str();
let title = self.title().using(cx);

View file

@ -3,7 +3,7 @@ use pagetop::prelude::*;
// **< Backdrop >***********************************************************************************
/// Comportamiento de la capa de fondo (*backdrop*) de un panel
/// [`Offcanvas`](crate::theme::Offcanvas) al deslizarse.
/// [`Offcanvas`](crate::theme::bs::Offcanvas) al deslizarse.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Backdrop {
/// Sin capa de fondo, la página principal permanece visible e interactiva.
@ -16,10 +16,23 @@ pub enum Backdrop {
Static,
}
impl Backdrop {
/// Devuelve el valor para `data-bs-backdrop`, o `None` si es el comportamiento por defecto.
#[rustfmt::skip]
#[inline]
pub const fn opt_str(self) -> Option<&'static str> {
match self {
Self::Disabled => Some("false"),
Self::Enabled => None,
Self::Static => Some("static"),
}
}
}
// **< BodyScroll >*********************************************************************************
/// Controla si la página principal puede desplazarse al abrir un panel
/// [`Offcanvas`](crate::theme::Offcanvas).
/// [`Offcanvas`](crate::theme::bs::Offcanvas).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum BodyScroll {
/// Opción por defecto, la página principal se bloquea centrando la interacción en el panel.
@ -29,9 +42,20 @@ pub enum BodyScroll {
Enabled,
}
impl BodyScroll {
/// Devuelve el valor para `data-bs-scroll`, o `None` si es el comportamiento por defecto.
#[inline]
pub const fn opt_str(self) -> Option<&'static str> {
match self {
Self::Disabled => None,
Self::Enabled => Some("true"),
}
}
}
// **< Placement >**********************************************************************************
/// Posición de aparición de un panel [`Offcanvas`](crate::theme::Offcanvas) al deslizarse.
/// Posición de aparición de un panel [`Offcanvas`](crate::theme::bs::Offcanvas) al deslizarse.
///
/// Define desde qué borde de la ventana entra y se ancla el panel.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
@ -51,7 +75,7 @@ impl Placement {
/// Devuelve la clase base asociada a la posición de aparición del panel.
#[rustfmt::skip]
#[inline]
const fn as_str(self) -> &'static str {
pub const fn as_str(self) -> &'static str {
match self {
Placement::Start => "offcanvas-start",
Placement::End => "offcanvas-end",
@ -62,23 +86,24 @@ impl Placement {
/// Añade la clase asociada a la posición de aparición del panel a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
pub fn push_to(self, classes: &mut String) {
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(self.as_str());
}
/* Devuelve la clase asociada a la posición de aparición del panel (reservado).
#[inline]
pub(crate) fn to_class(self) -> String {
self.as_str().to_owned()
} */
/// Devuelve la clase asociada a la posición de aparición del panel.
pub fn to_class(self) -> String {
let mut class = String::new();
self.push_to(&mut class);
class
}
}
// **< Visibility >*********************************************************************************
/// Estado inicial de un panel [`Offcanvas`](crate::theme::Offcanvas).
/// Estado inicial de un panel [`Offcanvas`](crate::theme::bs::Offcanvas).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Visibility {
/// El panel permanece oculto desde el principio.
@ -91,7 +116,7 @@ pub enum Visibility {
impl Visibility {
/// Devuelve la clase base asociada al estado inicial del panel.
#[inline]
const fn as_str(self) -> &'static str {
pub const fn as_str(self) -> &'static str {
match self {
Visibility::Default => "",
Visibility::Show => "show",
@ -100,7 +125,7 @@ impl Visibility {
/// Añade la clase asociada al estado inicial del panel a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
pub fn push_to(self, classes: &mut String) {
let class = self.as_str();
if class.is_empty() {
return;
@ -111,9 +136,10 @@ impl Visibility {
classes.push_str(class);
}
/* Devuelve la clase asociada al estado inicial, o una cadena vacía si no aplica (reservado).
#[inline]
pub(crate) fn to_class(self) -> String {
self.as_str().to_owned()
} */
/// Devuelve la clase asociada al estado inicial del panel.
pub fn to_class(self) -> String {
let mut class = String::new();
self.push_to(&mut class);
class
}
}

View file

@ -1,4 +1,4 @@
//! Conjunto de clases para aplicar en componentes del tema.
//! Define clases para aplicar en componentes del tema.
mod color;
pub use color::{Background, Text};

View file

@ -0,0 +1,170 @@
use pagetop::prelude::*;
use crate::theme::token::{ColorBorder, Opacity, ScaleSize, Side};
/// Clases para definir **bordes**.
///
/// Se puede:
///
/// - Iniciar un borde sin tamaño inicial (`Border::default()`).
/// - Crear un borde con tamaño por defecto (`Border::new()`).
/// - Ajustar el tamaño de cada **lado lógico** (`side`, respetando LTR/RTL).
/// - Asignar **un tamaño global** para todo el borde (`size`).
/// - Aplicar un **color** al borde (`ColorBorder`).
/// - Aplicar un nivel de **opacidad** (`Opacity`).
///
/// # Comportamiento aditivo / sustractivo
///
/// - **Aditivo**: basta con crear un borde sin tamaño con `class::Border::default()` para ir
/// añadiendo cada lado lógico con el tamaño deseado usando `token::ScaleSize::{One..Five}`.
///
/// - **Sustractivo**: se crea un borde con tamaño predefinido, por ejemplo usando
/// `class::Border::new()` o `class::Border::with(token::ScaleSize::Two)` y eliminar los lados
/// deseados con `token::ScaleSize::Zero`.
///
/// - **Anchos diferentes por lado**: usando `token::ScaleSize::{Zero..Five}` en cada lado deseado.
///
/// # Ejemplos
///
/// ```rust
/// use pagetop_bootsier::theme::*;
///
/// // Borde global.
/// let b = class::Border::with(token::ScaleSize::Two);
/// assert_eq!(b.to_class(), "border-2");
///
/// // Aditivo (sólo borde superior):
/// let b = class::Border::default().with_side(token::Side::Top, token::ScaleSize::One);
/// assert_eq!(b.to_class(), "border-top-1");
///
/// // Sustractivo (borde global menos el superior):
/// let b = class::Border::new().with_side(token::Side::Top, token::ScaleSize::Zero);
/// assert_eq!(b.to_class(), "border border-top-0");
///
/// // Ancho por lado (lado lógico inicial a 2 y final a 4):
/// let b = class::Border::default()
/// .with_side(token::Side::Start, token::ScaleSize::Two)
/// .with_side(token::Side::End, token::ScaleSize::Four);
/// assert_eq!(b.to_class(), "border-end-4 border-start-2");
///
/// // Combinado (ejemplo completo):
/// let b = class::Border::new() // Borde por defecto.
/// .with_side(token::Side::Top, token::ScaleSize::Zero) // Quita borde superior.
/// .with_side(token::Side::End, token::ScaleSize::Three) // Ancho 3 para lado lógico final.
/// .with_color(token::Color::Primary)
/// .with_opacity(token::Opacity::Half);
/// assert_eq!(b.to_class(), "border border-top-0 border-end-3 border-primary border-opacity-50");
/// ```
#[rustfmt::skip]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Border {
all : ScaleSize,
top : ScaleSize,
end : ScaleSize,
bottom : ScaleSize,
start : ScaleSize,
color : ColorBorder,
opacity: Opacity,
}
impl Border {
/// Prepara un borde del tamaño predefinido. Equivale a `border` (ancho por defecto del tema).
pub fn new() -> Self {
Self::with(ScaleSize::Auto)
}
/// Define un borde con un tamaño global (`size`) para todos los lados.
pub fn with(size: ScaleSize) -> Self {
Self::default().with_side(Side::All, size)
}
// **< Border BUILDER >*************************************************************************
/// Ajusta el tamaño del borde en el lado indicado (ver [`Side`](crate::theme::token::Side)).
pub fn with_side(mut self, side: Side, size: ScaleSize) -> Self {
match side {
Side::All => self.all = size,
Side::Top => self.top = size,
Side::Bottom => self.bottom = size,
Side::Start => self.start = size,
Side::End => self.end = size,
Side::LeftAndRight => {
self.start = size;
self.end = size;
}
Side::TopAndBottom => {
self.top = size;
self.bottom = size;
}
}
self
}
/// Establece el color del borde.
///
/// Acepta un tipo convertible en [`ColorBorder`]. Un [`Color`](crate::theme::token::Color) se
/// convierte automáticamente en [`ColorBorder::Theme`].
pub fn with_color(mut self, color: impl Into<ColorBorder>) -> Self {
self.color = color.into();
self
}
/// Establece la opacidad del borde.
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
self.opacity = opacity;
self
}
// **< Border HELPERS >*************************************************************************
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`, *color*
/// y *opacidad*; respetando LTR/RTL y omitiendo las definiciones vacías.
#[rustfmt::skip]
#[inline]
pub fn push_to(self, classes: &mut String) {
self.all .push_to(classes, "border");
self.top .push_to(classes, "border-top");
self.end .push_to(classes, "border-end");
self.bottom .push_to(classes, "border-bottom");
self.start .push_to(classes, "border-start");
self.color .push_to(classes);
self.opacity.push_to(classes, "border");
}
/// Devuelve las clases de borde como cadena (una combinación sencilla como `"border-2"`, o una
/// más compleja como `"border border-top-0 border-end-3 border-primary border-opacity-50"`).
///
/// Si no se define ningún tamaño, color ni opacidad, devuelve `""`.
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_to(&mut classes);
classes
}
}
impl From<ScaleSize> for Border {
/// Crea un [`Border`] con un tamaño global para todos los lados.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// // Convertir explícitamente con `From::from`:
/// let b = class::Border::from(token::ScaleSize::Two);
/// assert_eq!(b.to_class(), "border-2");
///
/// // Convertir implícitamente con `into()`:
/// let b: class::Border = token::ScaleSize::Auto.into();
/// assert_eq!(b.to_class(), "border");
/// ```
fn from(size: ScaleSize) -> Self {
Self::with(size)
}
}
impl Into<CowStr> for Border {
/// Permite pasar [`Border`] directamente a [`PropsOp`](pagetop::prelude::PropsOp).
fn into(self) -> CowStr {
self.to_class().into()
}
}

View file

@ -1,6 +1,6 @@
use pagetop::prelude::*;
use crate::theme::attrs::Color;
use crate::theme::token::Color;
// **< ButtonColor >********************************************************************************
@ -22,16 +22,16 @@ enum ButtonColorStyle {
/// use pagetop_bootsier::theme::*;
///
/// // Botón sólido.
/// let save = Button::submit(L10n::n("Save"))
/// .with_prop(PropsOp::add_classes(classes::ButtonColor::solid(Color::Primary)));
/// let save = bs::Button::submit(L10n::n("Save"))
/// .with_prop(PropsOp::add_classes(class::ButtonColor::solid(token::Color::Primary)));
///
/// // Botón con contorno.
/// let cancel = Button::plain(L10n::n("Cancel"))
/// .with_prop(PropsOp::add_classes(classes::ButtonColor::outline(Color::Secondary)));
/// let cancel = bs::Button::plain(L10n::n("Cancel"))
/// .with_prop(PropsOp::add_classes(class::ButtonColor::outline(token::Color::Secondary)));
///
/// // Botón tipo enlace.
/// let back = Button::plain(L10n::n("Back"))
/// .with_prop(PropsOp::add_classes(classes::ButtonColor::link()));
/// let back = bs::Button::plain(L10n::n("Back"))
/// .with_prop(PropsOp::add_classes(class::ButtonColor::link()));
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct ButtonColor {
@ -81,20 +81,35 @@ impl ButtonColor {
// **< ButtonColor HELPERS >********************************************************************
/// Añade la clase `btn-*` a la cadena de clases.
#[rustfmt::skip]
#[inline]
pub fn push_to(self, classes: &mut String) {
let (prefix, suffix) = match self.style {
ButtonColorStyle::None => return,
ButtonColorStyle::Solid => ("btn-", self.color.as_str()),
ButtonColorStyle::Outline => ("btn-outline-", self.color.as_str()),
ButtonColorStyle::Link => ("btn-link", ""),
};
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(prefix);
classes.push_str(suffix);
}
/// Devuelve la clase `btn-*` correspondiente al color del botón.
///
/// Si no se ha definido ningún estilo, devuelve `""`.
pub fn to_class(self) -> String {
match self.style {
ButtonColorStyle::None => String::new(),
ButtonColorStyle::Solid => format!("btn-{}", self.color.as_str()),
ButtonColorStyle::Outline => format!("btn-outline-{}", self.color.as_str()),
ButtonColorStyle::Link => "btn-link".to_owned(),
}
let mut class = String::new();
self.push_to(&mut class);
class
}
}
impl Into<CowStr> for ButtonColor {
/// Permite pasar [`ButtonColor`] directamente a [`PropsOp`](pagetop::prelude::PropsOp).
fn into(self) -> CowStr {
self.to_class().into()
}
@ -118,11 +133,11 @@ enum ButtonSizeVariant {
/// use pagetop::prelude::*;
/// use pagetop_bootsier::theme::*;
///
/// let small = Button::submit(L10n::n("Save"))
/// .with_prop(PropsOp::add_classes(classes::ButtonSize::small()));
/// let small = bs::Button::submit(L10n::n("Save"))
/// .with_prop(PropsOp::add_classes(class::ButtonSize::small()));
///
/// let large = Button::submit(L10n::n("Save"))
/// .with_prop(PropsOp::add_classes(classes::ButtonSize::large()));
/// let large = bs::Button::submit(L10n::n("Save"))
/// .with_prop(PropsOp::add_classes(class::ButtonSize::large()));
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct ButtonSize {
@ -151,19 +166,32 @@ impl ButtonSize {
// **< ButtonSize HELPERS >*********************************************************************
/// Añade la clase `btn-sm` o `btn-lg` a la cadena de clases.
#[inline]
pub fn push_to(self, classes: &mut String) {
let class = match self.size {
ButtonSizeVariant::None => return,
ButtonSizeVariant::Small => "btn-sm",
ButtonSizeVariant::Large => "btn-lg",
};
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(class);
}
/// Devuelve la clase `btn-sm` o `btn-lg` correspondiente al tamaño del botón.
///
/// Si no se ha definido ningún tamaño, devuelve `""`.
pub fn to_class(self) -> String {
match self.size {
ButtonSizeVariant::None => String::new(),
ButtonSizeVariant::Small => "btn-sm".to_owned(),
ButtonSizeVariant::Large => "btn-lg".to_owned(),
}
let mut class = String::new();
self.push_to(&mut class);
class
}
}
impl Into<CowStr> for ButtonSize {
/// Permite pasar [`ButtonSize`] directamente a [`PropsOp`](pagetop::prelude::PropsOp).
fn into(self) -> CowStr {
self.to_class().into()
}

View file

@ -1,6 +1,6 @@
use pagetop::prelude::*;
use crate::theme::attrs::{ColorBg, ColorText, Opacity};
use crate::theme::token::{ColorBg, ColorText, Opacity};
// **< Background >*********************************************************************************
@ -8,27 +8,28 @@ use crate::theme::attrs::{ColorBg, ColorText, Opacity};
///
/// # Ejemplos
///
/// ```
/// ```rust
/// use pagetop_bootsier::theme::*;
///
/// // Sin clases.
/// let s = classes::Background::new();
/// let s = class::Background::new();
/// assert_eq!(s.to_class(), "");
///
/// // Sólo color de fondo.
/// let s = classes::Background::with(ColorBg::Theme(Color::Primary));
/// // Sólo color de fondo (forma corta con Color).
/// let s = class::Background::with(token::Color::Primary);
/// assert_eq!(s.to_class(), "bg-primary");
///
/// // Color más opacidad.
/// let s = classes::Background::with(ColorBg::BodySecondary).with_opacity(Opacity::Half);
/// let s = class::Background::with(token::ColorBg::BodySecondary)
/// .with_opacity(token::Opacity::Half);
/// assert_eq!(s.to_class(), "bg-body-secondary bg-opacity-50");
///
/// // Usando `From<ColorBg>`.
/// let s: classes::Background = ColorBg::Black.into();
/// let s: class::Background = token::ColorBg::Black.into();
/// assert_eq!(s.to_class(), "bg-black");
///
/// // Usando `From<(ColorBg, Opacity)>`.
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
/// let s: class::Background = (token::ColorBg::White, token::Opacity::SemiTransparent).into();
/// assert_eq!(s.to_class(), "bg-white bg-opacity-25");
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
@ -44,15 +45,32 @@ impl Background {
}
/// Crea un estilo fijando el color de fondo (`bg-*`).
pub fn with(color: ColorBg) -> Self {
///
/// Acepta cualquier tipo convertible en [`ColorBg`]. Un [`Color`](crate::theme::token::Color)
/// se convierte automáticamente en [`ColorBg::Theme`]:
///
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// // Forma corta con Color:
/// let s = class::Background::with(token::Color::Primary);
/// assert_eq!(s.to_class(), "bg-primary");
///
/// // Forma explícita para variantes no temáticas:
/// let s = class::Background::with(token::ColorBg::Body);
/// assert_eq!(s.to_class(), "bg-body");
/// ```
pub fn with(color: impl Into<ColorBg>) -> Self {
Self::default().with_color(color)
}
// **< Background BUILDER >*********************************************************************
/// Establece el color de fondo (`bg-*`).
pub fn with_color(mut self, color: ColorBg) -> Self {
self.color = color;
///
/// Acepta cualquier tipo convertible en [`ColorBg`]. Un [`Color`](crate::theme::token::Color)
/// se convierte automáticamente en [`ColorBg::Theme`].
pub fn with_color(mut self, color: impl Into<ColorBg>) -> Self {
self.color = color.into();
self
}
@ -64,35 +82,34 @@ impl Background {
// **< Background HELPERS >*********************************************************************
/// Añade las clases de fondo a la cadena de clases.
///
/// Concatena, en este orden, color del fondo (`bg-*`) y opacidad (`bg-opacity-*`),
/// omitiendo los fragmentos vacíos.
/// Concatena, en este orden, color del fondo (`bg-*`) y opacidad (`bg-opacity-*`), omitiendo
/// los fragmentos vacíos.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
self.color.push_class(classes);
self.opacity.push_class(classes, "bg");
pub fn push_to(self, classes: &mut String) {
self.color.push_to(classes);
self.opacity.push_to(classes, "bg");
}
/// Devuelve las clases de fondo como cadena (`"bg-primary"`, `"bg-body-secondary bg-opacity-50"`, etc.).
/// Devuelve las clases de fondo como cadena (`"bg-primary"`,
/// `"bg-body-secondary bg-opacity-50"`, etc.).
///
/// Si no se define ni color ni opacidad, devuelve `""`.
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
self.push_to(&mut classes);
classes
}
}
impl From<(ColorBg, Opacity)> for Background {
/// Atajo para crear un [`classes::Background`](crate::theme::classes::Background) a partir del color de fondo y
/// la opacidad.
/// Crea la clase para un [`Background`](crate::theme::class::Background) a partir del color de
/// fondo y la opacidad.
///
/// # Ejemplo
///
/// ```
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
/// let s: class::Background = (token::ColorBg::White, token::Opacity::SemiTransparent).into();
/// assert_eq!(s.to_class(), "bg-white bg-opacity-25");
/// ```
fn from((color, opacity): (ColorBg, Opacity)) -> Self {
@ -101,13 +118,14 @@ impl From<(ColorBg, Opacity)> for Background {
}
impl From<ColorBg> for Background {
/// Atajo para crear un [`classes::Background`](crate::theme::classes::Background) a partir del color de fondo.
/// Crea la clase para un [`Background`](crate::theme::class::Background) a partir del color de
/// fondo.
///
/// # Ejemplo
///
/// ```
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// let s: classes::Background = ColorBg::Black.into();
/// let s: class::Background = token::ColorBg::Black.into();
/// assert_eq!(s.to_class(), "bg-black");
/// ```
fn from(color: ColorBg) -> Self {
@ -115,33 +133,44 @@ impl From<ColorBg> for Background {
}
}
impl Into<CowStr> for Background {
/// Permite pasar [`Background`] directamente a [`PropsOp`](pagetop::prelude::PropsOp).
fn into(self) -> CowStr {
self.to_class().into()
}
}
// **< Text >***************************************************************************************
/// Clases para establecer **color/opacidad del texto**.
///
/// # Ejemplos
///
/// ```
/// ```rust
/// use pagetop_bootsier::theme::*;
///
/// // Sin clases.
/// let s = classes::Text::new();
/// let s = class::Text::new();
/// assert_eq!(s.to_class(), "");
///
/// // Sólo color del texto.
/// let s = classes::Text::with(ColorText::Theme(Color::Primary));
/// // Sólo color del texto (forma corta con Color).
/// let s = class::Text::with(token::Color::Primary);
/// assert_eq!(s.to_class(), "text-primary");
///
/// // Color del texto y opacidad.
/// let s = classes::Text::new().with_color(ColorText::White).with_opacity(Opacity::SemiTransparent);
/// let s = class::Text::new().with_color(token::ColorText::White)
/// .with_opacity(token::Opacity::SemiTransparent);
/// assert_eq!(s.to_class(), "text-white text-opacity-25");
///
/// // Usando `From<ColorText>`.
/// let s: classes::Text = ColorText::Black.into();
/// let s: class::Text = token::ColorText::Black.into();
/// assert_eq!(s.to_class(), "text-black");
///
/// // Usando `From<(ColorText, Opacity)>`.
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
/// let s: class::Text = (
/// token::ColorText::Theme(token::Color::Danger),
/// token::Opacity::Opaque,
/// ).into();
/// assert_eq!(s.to_class(), "text-danger text-opacity-100");
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
@ -157,15 +186,32 @@ impl Text {
}
/// Crea un estilo fijando el color del texto (`text-*`).
pub fn with(color: ColorText) -> Self {
///
/// Acepta cualquier tipo convertible en [`ColorText`]. Un [`Color`](crate::theme::token::Color)
/// se convierte automáticamente en [`ColorText::Theme`]:
///
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// // Forma corta con Color:
/// let s = class::Text::with(token::Color::Danger);
/// assert_eq!(s.to_class(), "text-danger");
///
/// // Forma explícita para variantes no temáticas:
/// let s = class::Text::with(token::ColorText::Body);
/// assert_eq!(s.to_class(), "text-body");
/// ```
pub fn with(color: impl Into<ColorText>) -> Self {
Self::default().with_color(color)
}
// **< Text BUILDER >***************************************************************************
/// Establece el color del texto (`text-*`).
pub fn with_color(mut self, color: ColorText) -> Self {
self.color = color;
///
/// Acepta cualquier tipo convertible en [`ColorText`]. Un [`Color`](crate::theme::token::Color)
/// se convierte automáticamente en [`ColorText::Theme`].
pub fn with_color(mut self, color: impl Into<ColorText>) -> Self {
self.color = color.into();
self
}
@ -177,13 +223,11 @@ impl Text {
// **< Text HELPERS >***************************************************************************
/// Añade las clases de texto a la cadena de clases.
///
/// Concatena, en este orden, `text-*` y `text-opacity-*`, omitiendo los fragmentos vacíos.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
self.color.push_class(classes);
self.opacity.push_class(classes, "text");
pub fn push_to(self, classes: &mut String) {
self.color.push_to(classes);
self.opacity.push_to(classes, "text");
}
/// Devuelve las clases de texto como cadena (`"text-primary"`, `"text-white text-opacity-25"`,
@ -192,20 +236,23 @@ impl Text {
/// Si no se define ni color ni opacidad, devuelve `""`.
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
self.push_to(&mut classes);
classes
}
}
impl From<(ColorText, Opacity)> for Text {
/// Atajo para crear un [`classes::Text`](crate::theme::classes::Text) a partir del color del
/// texto y su opacidad.
/// Crea la clase para [`Text`](crate::theme::class::Text) a partir del color del texto y su
/// opacidad.
///
/// # Ejemplo
///
/// ```
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
/// let s: class::Text = (
/// token::ColorText::Theme(token::Color::Danger),
/// token::Opacity::Opaque,
/// ).into();
/// assert_eq!(s.to_class(), "text-danger text-opacity-100");
/// ```
fn from((color, opacity): (ColorText, Opacity)) -> Self {
@ -214,17 +261,23 @@ impl From<(ColorText, Opacity)> for Text {
}
impl From<ColorText> for Text {
/// Atajo para crear un [`classes::Text`](crate::theme::classes::Text) a partir del color del
/// texto.
/// Crea la clase para [`Text`](crate::theme::class::Text) a partir del color del texto.
///
/// # Ejemplo
///
/// ```
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// let s: classes::Text = ColorText::Black.into();
/// let s: class::Text = token::ColorText::Black.into();
/// assert_eq!(s.to_class(), "text-black");
/// ```
fn from(color: ColorText) -> Self {
Text::with(color)
}
}
impl Into<CowStr> for Text {
/// Permite pasar [`Text`] directamente a [`PropsOp`](pagetop::prelude::PropsOp).
fn into(self) -> CowStr {
self.to_class().into()
}
}

View file

@ -1,7 +1,6 @@
use pagetop::prelude::*;
use crate::theme::BreakPoint;
use crate::theme::attrs::{ScaleSize, Side};
use crate::theme::token::{BreakPoint, ScaleSize, Side};
// **< Margin >*************************************************************************************
@ -9,16 +8,17 @@ use crate::theme::attrs::{ScaleSize, Side};
///
/// # Ejemplos
///
/// ```rust,no_run
/// ```rust
/// use pagetop_bootsier::theme::*;
///
/// let m = classes::Margin::with(Side::Top, ScaleSize::Three);
/// let m = class::Margin::with(token::Side::Top, token::ScaleSize::Three);
/// assert_eq!(m.to_class(), "mt-3");
///
/// let m = classes::Margin::with(Side::Start, ScaleSize::Auto).with_breakpoint(BreakPoint::LG);
/// let m = class::Margin::with(token::Side::Start, token::ScaleSize::Auto)
/// .with_breakpoint(token::BreakPoint::LG);
/// assert_eq!(m.to_class(), "ms-lg-auto");
///
/// let m = classes::Margin::with(Side::All, ScaleSize::None);
/// let m = class::Margin::with(token::Side::All, token::ScaleSize::None);
/// assert_eq!(m.to_class(), "");
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
@ -49,7 +49,7 @@ impl Margin {
// **< Margin HELPERS >*************************************************************************
/// Devuelve el prefijo `m*` según el lado.
// Devuelve el prefijo `m*` según el lado.
#[rustfmt::skip]
#[inline]
const fn side_prefix(&self) -> &'static str {
@ -64,7 +64,7 @@ impl Margin {
}
}
/// Devuelve el sufijo del tamaño (`auto`, `0`..`5`), o `None` si no define clase.
// Devuelve el sufijo del tamaño (`auto`, `0`..`5`), o `None` si no define clase.
#[rustfmt::skip]
#[inline]
const fn size_suffix(&self) -> Option<&'static str> {
@ -80,14 +80,28 @@ impl Margin {
}
}
/// Devuelve la clase de **margin** como cadena (`"mt-3"`, `"ms-lg-auto"`, etc.).
/// Añade la clase de **margin** a la cadena de clases.
pub fn push_to(self, classes: &mut String) {
if let Some(size) = self.size_suffix() {
let side = self.side_prefix();
self.breakpoint.push_to(classes, side, size);
}
}
/// Devuelve la clase de *margin* como cadena (`"mt-3"`, `"ms-lg-auto"`, etc.).
///
/// Si `size` es `ScaleSize::None`, devuelve `""`.
pub fn to_class(self) -> String {
let Some(size) = self.size_suffix() else {
return String::new();
};
self.breakpoint.class_with(self.side_prefix(), size)
let mut class = String::new();
self.push_to(&mut class);
class
}
}
impl Into<CowStr> for Margin {
/// Permite pasar [`Margin`] directamente a [`PropsOp`](pagetop::prelude::PropsOp).
fn into(self) -> CowStr {
self.to_class().into()
}
}
@ -97,16 +111,17 @@ impl Margin {
///
/// # Ejemplos
///
/// ```rust,no_run
/// ```rust
/// use pagetop_bootsier::theme::*;
///
/// let p = classes::Padding::with(Side::LeftAndRight, ScaleSize::Two);
/// let p = class::Padding::with(token::Side::LeftAndRight, token::ScaleSize::Two);
/// assert_eq!(p.to_class(), "px-2");
///
/// let p = classes::Padding::with(Side::End, ScaleSize::Four).with_breakpoint(BreakPoint::SM);
/// let p = class::Padding::with(token::Side::End, token::ScaleSize::Four)
/// .with_breakpoint(token::BreakPoint::SM);
/// assert_eq!(p.to_class(), "pe-sm-4");
///
/// let p = classes::Padding::with(Side::All, ScaleSize::Auto);
/// let p = class::Padding::with(token::Side::All, token::ScaleSize::Auto);
/// assert_eq!(p.to_class(), ""); // `Auto` no aplica a padding.
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
@ -137,10 +152,10 @@ impl Padding {
// **< Padding HELPERS >************************************************************************
/// Devuelve el prefijo `p*` según el lado.
// Devuelve el prefijo `p*` según el lado.
#[rustfmt::skip]
#[inline]
const fn prefix(&self) -> &'static str {
const fn side_prefix(&self) -> &'static str {
match self.side {
Side::All => "p",
Side::Top => "pt",
@ -152,12 +167,12 @@ impl Padding {
}
}
/// Devuelve el sufijo del tamaño (`0`..`5`), o `None` si no define clase.
///
/// Nota: `ScaleSize::Auto` **no aplica** a padding ⇒ devuelve `None`.
// Devuelve el sufijo del tamaño (`0`..`5`), o None si no define clase.
//
// Nota: `ScaleSize::Auto` **no aplica** a *padding* => devuelve `None`.
#[rustfmt::skip]
#[inline]
const fn suffix(&self) -> Option<&'static str> {
const fn size_suffix(&self) -> Option<&'static str> {
match self.size {
ScaleSize::None => None,
ScaleSize::Auto => None,
@ -170,13 +185,27 @@ impl Padding {
}
}
/// Devuelve la clase de **padding** como cadena (`"px-2"`, `"pe-sm-4"`, etc.).
/// Añade la clase de **padding** a la cadena de clases.
pub fn push_to(self, classes: &mut String) {
if let Some(size) = self.size_suffix() {
let side = self.side_prefix();
self.breakpoint.push_to(classes, side, size);
}
}
/// Devuelve la clase de *padding* como cadena (`"px-2"`, `"pe-sm-4"`, etc.).
///
/// Si `size` es `ScaleSize::None` o `ScaleSize::Auto`, devuelve `""`.
pub fn to_class(self) -> String {
let Some(size) = self.suffix() else {
return String::new();
};
self.breakpoint.class_with(self.prefix(), size)
let mut class = String::new();
self.push_to(&mut class);
class
}
}
impl Into<CowStr> for Padding {
/// Permite pasar [`Padding`] directamente a [`PropsOp`](pagetop::prelude::PropsOp).
fn into(self) -> CowStr {
self.to_class().into()
}
}

View file

@ -1,6 +1,6 @@
use pagetop::prelude::*;
use crate::theme::attrs::RoundedRadius;
use crate::theme::token::RoundedRadius;
/// Clases para definir **esquinas redondeadas**.
///
@ -14,30 +14,30 @@ use crate::theme::attrs::RoundedRadius;
///
/// # Ejemplos
///
/// ```rust,no_run
/// ```rust
/// use pagetop_bootsier::theme::*;
///
/// // Radio global:
/// let r = classes::Rounded::with(RoundedRadius::Default);
/// let r = class::Rounded::with(token::RoundedRadius::Default);
/// assert_eq!(r.to_class(), "rounded");
///
/// // Sin redondeo:
/// let r = classes::Rounded::new();
/// let r = class::Rounded::new();
/// assert_eq!(r.to_class(), "");
///
/// // Radio en las esquinas de un lado lógico:
/// let r = classes::Rounded::new().with_end(RoundedRadius::Scale2);
/// let r = class::Rounded::new().with_end(token::RoundedRadius::Scale2);
/// assert_eq!(r.to_class(), "rounded-end-2");
///
/// // Radio en una esquina concreta:
/// let r = classes::Rounded::new().with_top_start(RoundedRadius::Scale3);
/// let r = class::Rounded::new().with_top_start(token::RoundedRadius::Scale3);
/// assert_eq!(r.to_class(), "rounded-top-start-3");
///
/// // Combinado (ejemplo completo):
/// let r = classes::Rounded::new()
/// .with_top(RoundedRadius::Default) // Añade redondeo arriba.
/// .with_bottom_start(RoundedRadius::Scale4) // Añade una esquina redondeada concreta.
/// .with_bottom_end(RoundedRadius::Circle); // Añade redondeo extremo en otra esquina.
/// let r = class::Rounded::new()
/// .with_top(token::RoundedRadius::Default) // Añade redondeo arriba.
/// .with_bottom_start(token::RoundedRadius::Scale4) // Añade una esquina redondeada concreta.
/// .with_bottom_end(token::RoundedRadius::Circle); // Añade redondeo extremo en otra esquina.
/// assert_eq!(r.to_class(), "rounded-top rounded-bottom-start-4 rounded-bottom-end-circle");
/// ```
#[rustfmt::skip]
@ -125,23 +125,21 @@ impl Rounded {
// **< Rounded HELPERS >************************************************************************
/// Añade las clases de redondeo a la cadena de clases.
///
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`,
/// `top-start`, `top-end`, `bottom-start` y `bottom-end`; respetando LTR/RTL y omitiendo las
/// definiciones vacías.
#[rustfmt::skip]
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
self.radius .push_class(classes, "");
self.top .push_class(classes, "rounded-top");
self.end .push_class(classes, "rounded-end");
self.bottom .push_class(classes, "rounded-bottom");
self.start .push_class(classes, "rounded-start");
self.top_start .push_class(classes, "rounded-top-start");
self.top_end .push_class(classes, "rounded-top-end");
self.bottom_start.push_class(classes, "rounded-bottom-start");
self.bottom_end .push_class(classes, "rounded-bottom-end");
pub fn push_to(self, classes: &mut String) {
self.radius .push_to(classes, "");
self.top .push_to(classes, "rounded-top");
self.end .push_to(classes, "rounded-end");
self.bottom .push_to(classes, "rounded-bottom");
self.start .push_to(classes, "rounded-start");
self.top_start .push_to(classes, "rounded-top-start");
self.top_end .push_to(classes, "rounded-top-end");
self.bottom_start.push_to(classes, "rounded-bottom-start");
self.bottom_end .push_to(classes, "rounded-bottom-end");
}
/// Devuelve las clases de redondeo como cadena (`"rounded"`,
@ -150,7 +148,14 @@ impl Rounded {
/// Si no se define ningún radio, devuelve `""`.
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
self.push_to(&mut classes);
classes
}
}
impl Into<CowStr> for Rounded {
/// Permite pasar [`Rounded`] directamente a [`PropsOp`](pagetop::prelude::PropsOp).
fn into(self) -> CowStr {
self.to_class().into()
}
}

View file

@ -1,162 +0,0 @@
use pagetop::prelude::*;
use crate::theme::attrs::{BorderColor, Opacity, ScaleSize, Side};
/// Clases para crear **bordes**.
///
/// Permite:
///
/// - Iniciar un borde sin tamaño inicial (`Border::default()`).
/// - Crear un borde con tamaño por defecto (`Border::new()`).
/// - Ajustar el tamaño de cada **lado lógico** (`side`, respetando LTR/RTL).
/// - Definir un tamaño **global** para todo el borde (`size`).
/// - Aplicar un **color** al borde (`BorderColor`).
/// - Aplicar un nivel de **opacidad** (`Opacity`).
///
/// # Comportamiento aditivo / sustractivo
///
/// - **Aditivo**: basta con crear un borde sin tamaño con `classes::Border::default()` para ir
/// añadiendo cada lado lógico con el tamaño deseado usando `ScaleSize::{One..Five}`.
///
/// - **Sustractivo**: se crea un borde con tamaño predefinido, p. ej. usando
/// `classes::Border::new()` o `classes::Border::with(ScaleSize::Two)` y eliminar los lados
/// deseados con `ScaleSize::Zero`.
///
/// - **Anchos diferentes por lado**: usando `ScaleSize::{Zero..Five}` en cada lado deseado.
///
/// # Ejemplos
///
/// ```rust,no_run
/// use pagetop_bootsier::theme::*;
///
/// // Borde global.
/// let b = classes::Border::with(ScaleSize::Two);
/// assert_eq!(b.to_class(), "border-2");
///
/// // Aditivo (sólo borde superior):
/// let b = classes::Border::default().with_side(Side::Top, ScaleSize::One);
/// assert_eq!(b.to_class(), "border-top-1");
///
/// // Sustractivo (borde global menos el superior):
/// let b = classes::Border::new().with_side(Side::Top, ScaleSize::Zero);
/// assert_eq!(b.to_class(), "border border-top-0");
///
/// // Ancho por lado (lado lógico inicial a 2 y final a 4):
/// let b = classes::Border::default()
/// .with_side(Side::Start, ScaleSize::Two)
/// .with_side(Side::End, ScaleSize::Four);
/// assert_eq!(b.to_class(), "border-end-4 border-start-2");
///
/// // Combinado (ejemplo completo):
/// let b = classes::Border::new() // Borde por defecto.
/// .with_side(Side::Top, ScaleSize::Zero) // Quita borde superior.
/// .with_side(Side::End, ScaleSize::Three) // Ancho 3 para el lado lógico final.
/// .with_color(BorderColor::Theme(Color::Primary))
/// .with_opacity(Opacity::Half);
/// assert_eq!(b.to_class(), "border border-top-0 border-end-3 border-primary border-opacity-50");
/// ```
#[rustfmt::skip]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Border {
all : ScaleSize,
top : ScaleSize,
end : ScaleSize,
bottom : ScaleSize,
start : ScaleSize,
color : BorderColor,
opacity: Opacity,
}
impl Border {
/// Prepara un borde del tamaño predefinido. Equivale a `border` (ancho por defecto del tema).
pub fn new() -> Self {
Self::with(ScaleSize::Auto)
}
/// Crea un borde **con un tamaño global** (`size`).
pub fn with(size: ScaleSize) -> Self {
Self::default().with_side(Side::All, size)
}
// **< Border BUILDER >*************************************************************************
pub fn with_side(mut self, side: Side, size: ScaleSize) -> Self {
match side {
Side::All => self.all = size,
Side::Top => self.top = size,
Side::Bottom => self.bottom = size,
Side::Start => self.start = size,
Side::End => self.end = size,
Side::LeftAndRight => {
self.start = size;
self.end = size;
}
Side::TopAndBottom => {
self.top = size;
self.bottom = size;
}
};
self
}
/// Establece el color del borde.
pub fn with_color(mut self, color: BorderColor) -> Self {
self.color = color;
self
}
/// Establece la opacidad del borde.
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
self.opacity = opacity;
self
}
// **< Border HELPERS >*************************************************************************
/// Añade las clases de borde a la cadena de clases.
///
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`,
/// *color* y *opacidad*; respetando LTR/RTL y omitiendo las definiciones vacías.
#[rustfmt::skip]
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
self.all .push_class(classes, "border");
self.top .push_class(classes, "border-top");
self.end .push_class(classes, "border-end");
self.bottom .push_class(classes, "border-bottom");
self.start .push_class(classes, "border-start");
self.color .push_class(classes);
self.opacity.push_class(classes, "border");
}
/// Devuelve las clases de borde como cadena (`"border-2"`,
/// `"border border-top-0 border-end-3 border-primary border-opacity-50"`, etc.).
///
/// Si no se define ningún tamaño, color ni opacidad, devuelve `""`.
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
classes
}
}
/// Atajo para crear un [`classes::Border`](crate::theme::classes::Border) a partir de un tamaño
/// [`ScaleSize`] aplicado a todo el borde.
///
/// # Ejemplos
///
/// ```rust,no_run
/// # use pagetop_bootsier::theme::*;
/// // Convertir explícitamente con `From::from`:
/// let b = classes::Border::from(ScaleSize::Two);
/// assert_eq!(b.to_class(), "border-2");
///
/// // Convertir implícitamente con `into()`:
/// let b: classes::Border = ScaleSize::Auto.into();
/// assert_eq!(b.to_class(), "border");
/// ```
impl From<ScaleSize> for Border {
fn from(size: ScaleSize) -> Self {
Self::with(size)
}
}

View file

@ -1,20 +0,0 @@
//! Definiciones para crear barras de navegación ([`Navbar`]).
//!
//! Cada [`navbar::Item`](crate::theme::navbar::Item) representa un elemento individual de la barra
//! de navegación [`Navbar`], con distintos comportamientos según su finalidad, como menús
//! [`Nav`](crate::theme::Nav) o *textos localizados* usando [`L10n`](pagetop::locale::L10n).
//!
//! También puede mostrar una marca de identidad ([`navbar::Brand`](crate::theme::navbar::Brand))
//! que identifique la compañía, producto o nombre del proyecto asociado a la solución web.
mod props;
pub use props::{Layout, Position};
mod brand;
pub use brand::Brand;
mod component;
pub use component::Navbar;
mod item;
pub use item::Item;

View file

@ -0,0 +1,28 @@
//! Tipos enumerados para construir clases del tema.
//!
//! Incluyen puntos de ruptura, colores, escalas de tamaño, lados, etc. Se pueden importar
//! globalmente junto al resto del tema:
//!
//! ```rust,no_run
//! use pagetop_bootsier::theme::*;
//!
//! let bg = class::Background::with(token::Color::Primary);
//! let border = class::Border::new()
//! .with_side(token::Side::Top, token::ScaleSize::Zero)
//! .with_color(token::Color::Danger);
//! ```
mod breakpoint;
pub use breakpoint::BreakPoint;
mod color;
pub use color::{Color, ColorBg, ColorText, Opacity};
mod border;
pub use border::ColorBorder;
mod rounded;
pub use rounded::RoundedRadius;
mod layout;
pub use layout::{ScaleSize, Side};

View file

@ -0,0 +1,103 @@
use pagetop::prelude::*;
use crate::theme::token::Color;
/// Esquema de color para los bordes ([`Border`](crate::theme::class::Border)).
///
/// - `Theme(Color)` y `Subtle(Color)` usan la paleta de colores temáticos ([`Color`]).
/// - `Black` y `White` son colores fijos independientes del tema.
/// - `Default` no genera ninguna clase.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ColorBorder {
/// No define ninguna clase.
#[default]
Default,
/// Genera la clase `border-{color}`.
Theme(Color),
/// Genera la clase `border-{color}-subtle` (un tono suavizado del color).
Subtle(Color),
/// Color negro.
Black,
/// Color blanco.
White,
}
impl ColorBorder {
// Devuelve el sufijo de la clase `border-*`, o `None` si no define ninguna clase.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::Default => None,
Self::Theme(_) => Some(""),
Self::Subtle(_) => Some("-subtle"),
Self::Black => Some("-black"),
Self::White => Some("-white"),
}
}
// Añade la clase `border-*` a la cadena de clases.
#[inline]
pub(crate) fn push_to(self, classes: &mut String) {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::Theme(c) | Self::Subtle(c) => {
classes.push_str("border-");
classes.push_str(c.as_str());
}
_ => classes.push_str("border"),
}
classes.push_str(suffix);
}
}
/// Devuelve la clase `border-*` correspondiente al color de borde.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// let theme = token::ColorBorder::Theme(token::Color::Primary).to_class();
/// assert_eq!(theme, "border-primary");
///
/// let subtle = token::ColorBorder::Subtle(token::Color::Warning).to_class();
/// assert_eq!(subtle, "border-warning-subtle");
///
/// let black = token::ColorBorder::Black.to_class();
/// assert_eq!(black, "border-black");
///
/// let none = token::ColorBorder::Default.to_class();
/// assert_eq!(none, "");
/// ```
#[inline]
pub fn to_class(self) -> String {
let mut class = String::new();
self.push_to(&mut class);
class
}
}
impl From<Color> for ColorBorder {
/// Convierte un [`Color`] en [`ColorBorder::Theme`].
///
/// Permite pasar un [`Color`] directamente donde se espera un [`ColorBorder`], sin necesidad
/// de envolver el valor en [`ColorBorder::Theme`]. Es el atajo habitual para los colores
/// temáticos.
///
/// Para los demás esquemas (`Subtle`, `Black`, `White`) se sigue usando [`ColorBorder`]
/// directamente.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// let border: token::ColorBorder = token::Color::Success.into();
/// assert_eq!(border.to_class(), "border-success");
/// ```
fn from(color: Color) -> Self {
Self::Theme(color)
}
}

View file

@ -0,0 +1,61 @@
use pagetop::prelude::*;
/// Define los puntos de ruptura (*breakpoints*) para aplicar diseño *responsive*.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum BreakPoint {
/// **Menos de 576px**. Dispositivos muy pequeños: teléfonos en modo vertical.
#[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,
}
impl BreakPoint {
/// Devuelve la identificación del punto de ruptura (`"sm"`, `"md"`, etc.), o `""` para `None`.
#[rustfmt::skip]
#[inline]
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "",
Self::SM => "sm",
Self::MD => "md",
Self::LG => "lg",
Self::XL => "xl",
Self::XXL => "xxl",
}
}
// Añade el punto de ruptura con un prefijo y un sufijo (opcional) a la cadena de clases.
//
// - Para `None`: `prefix` o `prefix-suffix` (si `suffix` no está vacío).
// - Para `SM..XXL`: `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
#[inline]
pub(crate) fn push_to(self, classes: &mut String, prefix: &str, suffix: &str) {
if prefix.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::None => classes.push_str(prefix),
_ => {
classes.push_str(prefix);
classes.push('-');
classes.push_str(self.as_str());
}
}
if !suffix.is_empty() {
classes.push('-');
classes.push_str(suffix);
}
}
}

View file

@ -6,9 +6,8 @@ use pagetop::prelude::*;
///
/// Equivalen a los nombres estándar definidos por Bootstrap (`primary`, `secondary`, `success`,
/// etc.). Este tipo enumerado sirve de referencia para componer las clases de color para el fondo
/// ([`classes::Background`](crate::theme::classes::Background)), los bordes
/// ([`classes::Border`](crate::theme::classes::Border)) o para el texto
/// ([`classes::Text`](crate::theme::classes::Text)).
/// ([`Background`](crate::theme::class::Background)), bordes
/// ([`Border`](crate::theme::class::Border)) o texto ([`Text`](crate::theme::class::Text)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Color {
#[default]
@ -23,10 +22,10 @@ pub enum Color {
}
impl Color {
/// Devuelve el nombre del color.
/// Devuelve el nombre del color Bootstrap (`"primary"`, `"danger"`, etc.).
#[rustfmt::skip]
#[inline]
pub(crate) const fn as_str(self) -> &'static str {
pub const fn as_str(self) -> &'static str {
match self {
Self::Primary => "primary",
Self::Secondary => "secondary",
@ -38,20 +37,6 @@ impl Color {
Self::Dark => "dark",
}
}
/// Devuelve la clase correspondiente al color.
///
/// # Ejemplos
///
/// ```rust,no_run
/// # use pagetop_bootsier::theme::*;
/// assert_eq!(Color::Primary.to_class(), "primary");
/// assert_eq!(Color::Danger.to_class(), "danger");
/// ```
#[inline]
pub fn to_class(self) -> String {
self.as_str().to_owned()
}
}
// **< Opacity >************************************************************************************
@ -59,9 +44,9 @@ impl Color {
/// Niveles de opacidad (`opacity-*`).
///
/// Se usa normalmente para graduar la transparencia del color de fondo `bg-opacity-*`
/// ([`classes::Background`](crate::theme::classes::Background)), de los bordes `border-opacity-*`
/// ([`classes::Border`](crate::theme::classes::Border)) o del texto `text-opacity-*`
/// ([`classes::Text`](crate::theme::classes::Text)).
/// ([`Background`](crate::theme::class::Background)), de los bordes `border-opacity-*`
/// ([`Border`](crate::theme::class::Border)) o del texto `text-opacity-*`
/// ([`Text`](crate::theme::class::Text)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Opacity {
/// No define ninguna clase.
@ -82,10 +67,7 @@ pub enum Opacity {
}
impl Opacity {
const OPACITY: &str = "opacity";
const OPACITY_PREFIX: &str = "-opacity";
/// Devuelve el sufijo para `*opacity-*`, o `None` si no define ninguna clase.
// Devuelve el sufijo para `*opacity-*`, o `None` si no define ninguna clase.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
@ -100,76 +82,50 @@ impl Opacity {
}
}
/// Añade la opacidad a la cadena de clases usando el prefijo dado (`bg`, `border`, `text`, o
/// vacío para `opacity-*`).
// Añade la opacidad a la cadena de clases usando el prefijo dado (`bg`, `border`, `text`, o
// vacío para `opacity-*`).
#[inline]
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
pub(crate) fn push_to(self, classes: &mut String, prefix: &str) {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
if prefix.is_empty() {
classes.push_str(Self::OPACITY);
classes.push_str("opacity");
} else {
classes.push_str(prefix);
classes.push_str(Self::OPACITY_PREFIX);
classes.push_str("-opacity");
}
classes.push_str(suffix);
}
}
/// Devuelve la clase de opacidad con el prefijo dado (`bg`, `border`, `text`, o vacío para
/// `opacity-*`).
///
/// # Ejemplos
///
/// ```rust,no_run
/// # use pagetop_bootsier::theme::*;
/// assert_eq!(Opacity::Opaque.class_with(""), "opacity-100");
/// assert_eq!(Opacity::Half.class_with("bg"), "bg-opacity-50");
/// assert_eq!(Opacity::SemiTransparent.class_with("text"), "text-opacity-25");
/// assert_eq!(Opacity::Default.class_with("bg"), "");
/// ```
#[doc(hidden)]
pub fn class_with(self, prefix: &str) -> String {
if let Some(suffix) = self.suffix() {
let base_len = if prefix.is_empty() {
Self::OPACITY.len()
} else {
prefix.len() + Self::OPACITY_PREFIX.len()
};
let mut class = String::with_capacity(base_len + suffix.len());
if prefix.is_empty() {
class.push_str(Self::OPACITY);
} else {
class.push_str(prefix);
class.push_str(Self::OPACITY_PREFIX);
}
class.push_str(suffix);
return class;
}
String::new()
}
/// Devuelve la clase de opacidad `opacity-*`.
///
/// # Ejemplos
///
/// ```rust,no_run
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// assert_eq!(Opacity::Opaque.to_class(), "opacity-100");
/// assert_eq!(Opacity::Half.to_class(), "opacity-50");
/// assert_eq!(Opacity::Default.to_class(), "");
/// assert_eq!(token::Opacity::Opaque.to_class(), "opacity-100");
/// assert_eq!(token::Opacity::Half.to_class(), "opacity-50");
/// assert_eq!(token::Opacity::Default.to_class(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
self.class_with("")
let mut class = String::new();
self.push_to(&mut class, "");
class
}
}
// **< ColorBg >************************************************************************************
/// Esquema de color para el fondo.
/// Esquema de color para el fondo ([`Background`](crate::theme::class::Background)).
///
/// - `Body`, `BodySecondary` y `BodyTertiary` siguen el esquema del tema (claro/oscuro).
/// - `Theme(Color)` y `Subtle(Color)` usan la paleta de colores temáticos ([`Color`]).
/// - `Black`, `White`, `Transparent` son colores fijos independientes del tema.
/// - `Default` no genera ninguna clase.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ColorBg {
/// No define ninguna clase.
@ -189,15 +145,12 @@ pub enum ColorBg {
Black,
/// Color blanco.
White,
/// No aplica ningún color de fondo (`bg-transparent`).
/// Fondo transparente (`bg-transparent`).
Transparent,
}
impl ColorBg {
const BG: &str = "bg";
const BG_PREFIX: &str = "bg-";
/// Devuelve el sufijo de la clase `bg-*`, o `None` si no define ninguna clase.
// Devuelve el sufijo de la clase `bg-*`, o `None` si no define ninguna clase.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
@ -214,19 +167,19 @@ impl ColorBg {
}
}
/// Añade la clase de fondo `bg-*` a la cadena de clases.
// Añade la clase de fondo `bg-*` a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
pub(crate) fn push_to(self, classes: &mut String) {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::Theme(c) | Self::Subtle(c) => {
classes.push_str(Self::BG_PREFIX);
classes.push_str("bg-");
classes.push_str(c.as_str());
}
_ => classes.push_str(Self::BG),
_ => classes.push_str("bg"),
}
classes.push_str(suffix);
}
@ -236,24 +189,60 @@ impl ColorBg {
///
/// # Ejemplos
///
/// ```rust,no_run
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// assert_eq!(ColorBg::Body.to_class(), "bg-body");
/// assert_eq!(ColorBg::Theme(Color::Primary).to_class(), "bg-primary");
/// assert_eq!(ColorBg::Subtle(Color::Warning).to_class(), "bg-warning-subtle");
/// assert_eq!(ColorBg::Transparent.to_class(), "bg-transparent");
/// assert_eq!(ColorBg::Default.to_class(), "");
/// let body = token::ColorBg::Body.to_class();
/// assert_eq!(body, "bg-body");
///
/// let theme = token::ColorBg::Theme(token::Color::Primary).to_class();
/// assert_eq!(theme, "bg-primary");
///
/// let subtle = token::ColorBg::Subtle(token::Color::Warning).to_class();
/// assert_eq!(subtle, "bg-warning-subtle");
///
/// let transparent = token::ColorBg::Transparent.to_class();
/// assert_eq!(transparent, "bg-transparent");
///
/// let none = token::ColorBg::Default.to_class();
/// assert_eq!(none, "");
/// ```
#[inline]
pub fn to_class(self) -> String {
let mut class = String::new();
self.push_class(&mut class);
self.push_to(&mut class);
class
}
}
impl From<Color> for ColorBg {
/// Convierte un [`Color`] en [`ColorBg::Theme`].
///
/// Permite pasar un [`Color`] directamente donde se espera un [`ColorBg`], sin necesidad de
/// envolver el valor en [`ColorBg::Theme`]. Es el atajo habitual para los colores temáticos.
///
/// Para los demás esquemas (`Body`, `Subtle`, `Black`, etc.) se sigue usando [`ColorBg`]
/// directamente.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// let bg: token::ColorBg = token::Color::Primary.into();
/// assert_eq!(bg.to_class(), "bg-primary");
/// ```
fn from(color: Color) -> Self {
Self::Theme(color)
}
}
// **< ColorText >**********************************************************************************
/// Esquema de color para el texto.
/// Esquema de color para el texto ([`Text`](crate::theme::class::Text)).
///
/// - `Body`, `BodyEmphasis`, `BodySecondary` y `BodyTertiary` siguen el tema (claro/oscuro).
/// - `Theme(Color)` y `Emphasis(Color)` usan la paleta de colores temáticos ([`Color`]).
/// - `Black` y `White` son colores fijos independientes del tema.
/// - `Default` no genera ninguna clase.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ColorText {
/// No define ninguna clase.
@ -261,7 +250,7 @@ pub enum ColorText {
Default,
/// Color predefinido del tema (`text-body`).
Body,
/// Color predefinido del tema (`text-body-emphasis`).
/// Color de mayor contraste según el tema (`text-body-emphasis`).
BodyEmphasis,
/// Color predefinido del tema (`text-body-secondary`).
BodySecondary,
@ -278,10 +267,7 @@ pub enum ColorText {
}
impl ColorText {
const TEXT: &str = "text";
const TEXT_PREFIX: &str = "text-";
/// Devuelve el sufijo de la clase `text-*`, o `None` si no define ninguna clase.
// Devuelve el sufijo de la clase `text-*`, o `None` si no define ninguna clase.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
@ -298,19 +284,19 @@ impl ColorText {
}
}
/// Añade la clase de texto `text-*` a la cadena de clases.
// Añade la clase de texto `text-*` a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
pub(crate) fn push_to(self, classes: &mut String) {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::Theme(c) | Self::Emphasis(c) => {
classes.push_str(Self::TEXT_PREFIX);
classes.push_str("text-");
classes.push_str(c.as_str());
}
_ => classes.push_str(Self::TEXT),
_ => classes.push_str("text"),
}
classes.push_str(suffix);
}
@ -320,17 +306,48 @@ impl ColorText {
///
/// # Ejemplos
///
/// ```rust,no_run
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// assert_eq!(ColorText::Body.to_class(), "text-body");
/// assert_eq!(ColorText::Theme(Color::Primary).to_class(), "text-primary");
/// assert_eq!(ColorText::Emphasis(Color::Danger).to_class(), "text-danger-emphasis");
/// assert_eq!(ColorText::Black.to_class(), "text-black");
/// assert_eq!(ColorText::Default.to_class(), "");
/// let body = token::ColorText::Body.to_class();
/// assert_eq!(body, "text-body");
///
/// let theme = token::ColorText::Theme(token::Color::Primary).to_class();
/// assert_eq!(theme, "text-primary");
///
/// let emphasis = token::ColorText::Emphasis(token::Color::Danger).to_class();
/// assert_eq!(emphasis, "text-danger-emphasis");
///
/// let black = token::ColorText::Black.to_class();
/// assert_eq!(black, "text-black");
///
/// let none = token::ColorText::Default.to_class();
/// assert_eq!(none, "");
/// ```
#[inline]
pub fn to_class(self) -> String {
let mut class = String::new();
self.push_class(&mut class);
self.push_to(&mut class);
class
}
}
impl From<Color> for ColorText {
/// Convierte un [`Color`] en [`ColorText::Theme`].
///
/// Permite pasar un [`Color`] directamente donde se espera un [`ColorText`], sin necesidad de
/// envolver el valor en [`ColorText::Theme`]. Es el atajo habitual para los colores temáticos.
///
/// Para los demás esquemas (`Body`, `Emphasis`, `Black`, etc.) se sigue usando [`ColorText`]
/// directamente.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// let text: token::ColorText = token::Color::Danger.into();
/// assert_eq!(text.to_class(), "text-danger");
/// ```
fn from(color: Color) -> Self {
Self::Theme(color)
}
}

View file

@ -2,7 +2,11 @@ use pagetop::prelude::*;
// **< ScaleSize >**********************************************************************************
/// Escala discreta de tamaños para definir clases utilitarias.
/// Escala discreta de tamaños para clases utilitarias de Bootstrap.
///
/// Se usa como parámetro de tamaño en [`Border`](crate::theme::class::Border),
/// [`Margin`](crate::theme::class::Margin) y [`Padding`](crate::theme::class::Padding). La
/// variante `Auto` no aplica en `Padding`.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ScaleSize {
/// Sin tamaño (no define ninguna clase).
@ -25,8 +29,8 @@ pub enum ScaleSize {
}
impl ScaleSize {
/// Devuelve el sufijo para el tamaño (`"-0"`, `"-1"`, etc.), o `None` si no define ninguna
/// clase, o `""` para el tamaño automático.
// Devuelve el sufijo de tamaño (`"-0"`, `"-1"`, etc.), o `None` si no define ninguna clase.
// `Auto` devuelve `Some("")` para que `push_to` emita sólo el prefijo, sin sufijo de tamaño.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
@ -42,9 +46,9 @@ impl ScaleSize {
}
}
/// Añade el tamaño a la cadena de clases usando el prefijo dado.
// Añade el tamaño a la cadena de clases usando el prefijo dado.
#[inline]
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
pub(crate) fn push_to(self, classes: &mut String, prefix: &str) {
if !prefix.is_empty() {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
@ -55,35 +59,14 @@ impl ScaleSize {
}
}
}
/* Devuelve la clase del tamaño para el prefijo, o una cadena vacía si no aplica (reservado).
///
/// # Ejemplo
///
/// ```rust,no_run
/// # use pagetop_bootsier::theme::*;
/// assert_eq!(ScaleSize::Auto.class_with("border"), "border");
/// assert_eq!(ScaleSize::Zero.class_with("m"), "m-0");
/// assert_eq!(ScaleSize::Three.class_with("p"), "p-3");
/// assert_eq!(ScaleSize::None.class_with("border"), "");
/// ```
#[doc(hidden)]
pub fn class_with(self, prefix: &str) -> String {
if !prefix.is_empty() {
if let Some(suffix) = self.suffix() {
let mut class = String::with_capacity(prefix.len() + suffix.len());
class.push_str(prefix);
class.push_str(suffix);
return class;
}
}
String::new()
} */
}
// **< Side >***************************************************************************************
/// Lados sobre los que aplicar una clase utilitaria (respetando LTR/RTL).
///
/// Se usa como selector de lado en [`Border`](crate::theme::class::Border),
/// [`Margin`](crate::theme::class::Margin) y [`Padding`](crate::theme::class::Padding).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Side {
/// Todos los lados.

View file

@ -0,0 +1,84 @@
use pagetop::prelude::*;
/// Radio para el redondeo de esquinas ([`Rounded`](crate::theme::class::Rounded)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum RoundedRadius {
/// No define ninguna clase.
#[default]
None,
/// Genera `rounded` (radio por defecto del tema).
Default,
/// Genera `rounded-0` (sin redondeo).
Zero,
/// Genera `rounded-1`.
Scale1,
/// Genera `rounded-2`.
Scale2,
/// Genera `rounded-3`.
Scale3,
/// Genera `rounded-4`.
Scale4,
/// Genera `rounded-5`.
Scale5,
/// Genera `rounded-circle`.
Circle,
/// Genera `rounded-pill`.
Pill,
}
impl RoundedRadius {
// Devuelve el sufijo para `*rounded-*`, o `None` si no define ninguna clase, o `""` para el
// redondeo por defecto.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::None => None,
Self::Default => Some(""),
Self::Zero => Some("-0"),
Self::Scale1 => Some("-1"),
Self::Scale2 => Some("-2"),
Self::Scale3 => Some("-3"),
Self::Scale4 => Some("-4"),
Self::Scale5 => Some("-5"),
Self::Circle => Some("-circle"),
Self::Pill => Some("-pill"),
}
}
// Añade el redondeo de esquinas a la cadena de clases usando el prefijo dado (`rounded-top`,
// `rounded-bottom-start`, o vacío para `rounded-*`).
#[inline]
pub(crate) fn push_to(self, classes: &mut String, prefix: &str) {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
if prefix.is_empty() {
classes.push_str("rounded");
} else {
classes.push_str(prefix);
}
classes.push_str(suffix);
}
}
/// Devuelve la clase `rounded-*` para el redondeo de esquinas.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::theme::*;
/// assert_eq!(token::RoundedRadius::Default.to_class(), "rounded");
/// assert_eq!(token::RoundedRadius::Zero.to_class(), "rounded-0");
/// assert_eq!(token::RoundedRadius::Scale3.to_class(), "rounded-3");
/// assert_eq!(token::RoundedRadius::Circle.to_class(), "rounded-circle");
/// assert_eq!(token::RoundedRadius::None.to_class(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
let mut class = String::new();
self.push_to(&mut class, "");
class
}
}