♻️ (bootsier): Refactoriza la gestión de clases

- Mejora la legibilidad del código.
- Simplifica las alteraciones de clases en los componentes `Container`,
  `Dropdown`, `Image`, `Nav`, `Navbar` y `Offcanvas` usando métodos
  dedicados para generar clases en función de sus propiedades.
- Mejora los enums añadiendo métodos que devuelven sus clases
  asociadas, reduciendo código repetitivo.
- Elimina el trait `JoinClasses` y su implementación, integrando la
  lógica de unión de clases directamente en los componentes.
This commit is contained in:
Manuel Cillero 2025-11-15 13:16:15 +01:00
parent 748bd81bf1
commit 2e39af0856
33 changed files with 1607 additions and 647 deletions

View file

@ -2,12 +2,8 @@ use pagetop::prelude::*;
use crate::theme::aux::Color;
use std::fmt;
// **< BorderColor >********************************************************************************
/// Colores `border-*` para los bordes ([`classes::Border`](crate::theme::classes::Border)).
#[derive(AutoDefault)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum BorderColor {
/// No define ninguna clase.
#[default]
@ -22,60 +18,70 @@ pub enum BorderColor {
White,
}
#[rustfmt::skip]
impl fmt::Display for BorderColor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Default => Ok(()),
Self::Theme(c) => write!(f, "border-{c}"),
Self::Subtle(c) => write!(f, "border-{c}-subtle"),
Self::Black => f.write_str("border-black"),
Self::White => f.write_str("border-white"),
}
}
}
impl BorderColor {
const BORDER: &str = "border";
const BORDER_PREFIX: &str = "border-";
// **< BorderSize >*********************************************************************************
/// Tamaño para el ancho de los bordes ([`classes::Border`](crate::theme::classes::Border)).
///
/// Mapea a `border`, `border-0` y `border-{1..5}`:
///
/// - `None` no añade ninguna clase.
/// - `Default` genera `border` (borde por defecto del tema).
/// - `Zero` genera `border-0` (sin borde).
/// - `Scale{1..5}` genera `border-{1..5}` (ancho creciente).
#[derive(AutoDefault)]
pub enum BorderSize {
#[default]
None,
Default,
Zero,
Scale1,
Scale2,
Scale3,
Scale4,
Scale5,
}
impl BorderSize {
// Devuelve el sufijo de la clase `border-*`, o `None` si no define ninguna clase.
#[rustfmt::skip]
pub(crate) fn to_class(&self, prefix: impl AsRef<str>) -> String {
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::None => String::new(),
Self::Default => String::from(prefix.as_ref()),
Self::Zero => join!(prefix, "-0"),
Self::Scale1 => join!(prefix, "-1"),
Self::Scale2 => join!(prefix, "-2"),
Self::Scale3 => join!(prefix, "-3"),
Self::Scale4 => join!(prefix, "-4"),
Self::Scale5 => join!(prefix, "-5"),
Self::Default => None,
Self::Theme(_) => Some(""),
Self::Subtle(_) => Some("-subtle"),
Self::Black => Some("-black"),
Self::White => Some("-white"),
}
}
}
impl fmt::Display for BorderSize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_class("border"))
// 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
/// # use pagetop_bootsier::prelude::*;
/// 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(), "");
/// ```
#[inline]
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,9 +1,7 @@
use pagetop::prelude::*;
use std::fmt;
/// Define los puntos de ruptura (*breakpoints*) para aplicar diseño *responsive*.
#[derive(AutoDefault)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum BreakPoint {
/// **Menos de 576px**. Dispositivos muy pequeños: teléfonos en modo vertical.
#[default]
@ -21,71 +19,96 @@ pub enum BreakPoint {
}
impl BreakPoint {
// Devuelve la identificación del punto de ruptura.
#[rustfmt::skip]
#[inline]
const fn suffix(&self) -> Option<&'static str> {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::None => None,
Self::SM => Some("sm"),
Self::MD => Some("md"),
Self::LG => Some("lg"),
Self::XL => Some("xl"),
Self::XXL => Some("xxl"),
Self::None => "",
Self::SM => "sm",
Self::MD => "md",
Self::LG => "lg",
Self::XL => "xl",
Self::XXL => "xxl",
}
}
/// Genera un nombre de clase CSS basado en el punto de ruptura.
///
/// Si es un punto de ruptura efectivo concatena el prefijo, un guion (`-`) y el sufijo
/// asociado. Para `None` devuelve sólo el prefijo.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let breakpoint = BreakPoint::MD;
/// assert_eq!(breakpoint.to_class("col"), "col-md");
///
/// let breakpoint = BreakPoint::None;
/// assert_eq!(breakpoint.to_class("offcanvas"), "offcanvas");
/// ```
// 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 fn to_class(&self, prefix: impl AsRef<str>) -> String {
join_pair!(prefix, "-", self.suffix().unwrap_or_default())
}
/// Intenta generar un nombre de clase CSS basado en el punto de ruptura.
///
/// Si es un punto de ruptura efectivo devuelve `Some(String)` concatenando el prefijo, un guion
/// (`-`) y el sufijo asociado. En otro caso devuelve `None`.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let breakpoint = BreakPoint::MD;
/// let class = breakpoint.try_class("col");
/// assert_eq!(class, Some("col-md".to_string()));
///
/// let breakpoint = BreakPoint::None;
/// let class = breakpoint.try_class("navbar-expand");
/// assert_eq!(class, None);
/// ```
#[inline]
pub fn try_class(&self, prefix: impl AsRef<str>) -> Option<String> {
self.suffix().map(|suffix| join_pair!(prefix, "-", suffix))
}
}
impl fmt::Display for BreakPoint {
/// Implementa [`Display`](std::fmt::Display) para asociar `"sm"`, `"md"`, `"lg"`, `"xl"` o
/// `"xxl"` a los puntos de ruptura `BreakPoint::SM`, `MD`, `LG`, `XL` o `XXL`, respectivamente.
/// Y `""` (cadena vacía) a `BreakPoint::None`.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(suffix) = self.suffix() {
f.write_str(suffix)
} else {
Ok(())
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
// # use pagetop_bootsier::prelude::*;
// 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"), "");
// ```
#[inline]
pub(crate) 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

@ -2,12 +2,10 @@ use pagetop::prelude::*;
use crate::theme::aux::Color;
use std::fmt;
// **< ButtonColor >********************************************************************************
/// Variantes de color `btn-*` para botones.
#[derive(AutoDefault)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ButtonColor {
/// No define ninguna clase.
#[default]
@ -20,14 +18,72 @@ pub enum ButtonColor {
Link,
}
#[rustfmt::skip]
impl fmt::Display for ButtonColor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
impl ButtonColor {
const BTN_PREFIX: &str = "btn-";
const BTN_OUTLINE_PREFIX: &str = "btn-outline-";
const BTN_LINK: &str = "btn-link";
// Añade la clase `btn-*` a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
if let Self::Default = self {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::Default => Ok(()),
Self::Background(c) => write!(f, "btn-{c}"),
Self::Outline(c) => write!(f, "btn-outline-{c}"),
Self::Link => f.write_str("btn-link"),
Self::Default => unreachable!(),
Self::Background(c) => {
classes.push_str(Self::BTN_PREFIX);
classes.push_str(c.as_str());
}
Self::Outline(c) => {
classes.push_str(Self::BTN_OUTLINE_PREFIX);
classes.push_str(c.as_str());
}
Self::Link => {
classes.push_str(Self::BTN_LINK);
}
}
}
/// Devuelve la clase `btn-*` correspondiente al color del botón.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// assert_eq!(
/// ButtonColor::Background(Color::Primary).to_class(),
/// "btn-primary"
/// );
/// assert_eq!(
/// ButtonColor::Outline(Color::Danger).to_class(),
/// "btn-outline-danger"
/// );
/// assert_eq!(ButtonColor::Link.to_class(), "btn-link");
/// assert_eq!(ButtonColor::Default.to_class(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
match self {
Self::Default => String::new(),
Self::Background(c) => {
let color = c.as_str();
let mut class = String::with_capacity(Self::BTN_PREFIX.len() + color.len());
class.push_str(Self::BTN_PREFIX);
class.push_str(color);
class
}
Self::Outline(c) => {
let color = c.as_str();
let mut class = String::with_capacity(Self::BTN_OUTLINE_PREFIX.len() + color.len());
class.push_str(Self::BTN_OUTLINE_PREFIX);
class.push_str(color);
class
}
Self::Link => Self::BTN_LINK.to_string(),
}
}
}
@ -35,7 +91,7 @@ impl fmt::Display for ButtonColor {
// **< ButtonSize >*********************************************************************************
/// Tamaño visual de un botón.
#[derive(AutoDefault)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ButtonSize {
/// Tamaño por defecto del tema (no añade clase).
#[default]
@ -46,13 +102,42 @@ pub enum ButtonSize {
Large,
}
#[rustfmt::skip]
impl fmt::Display for ButtonSize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
impl ButtonSize {
const BTN_SM: &str = "btn-sm";
const BTN_LG: &str = "btn-lg";
// Añade la clase de tamaño `btn-sm` o `btn-lg` a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
if let Self::Default = self {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::Default => Ok(()),
Self::Small => f.write_str("btn-sm"),
Self::Large => f.write_str("btn-lg"),
Self::Default => unreachable!(),
Self::Small => classes.push_str(Self::BTN_SM),
Self::Large => classes.push_str(Self::BTN_LG),
}
}
/// Devuelve la clase `btn-sm` o `btn-lg` correspondiente al tamaño del botón.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// assert_eq!(ButtonSize::Small.to_class(), "btn-sm");
/// assert_eq!(ButtonSize::Large.to_class(), "btn-lg");
/// assert_eq!(ButtonSize::Default.to_class(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
match self {
Self::Default => String::new(),
Self::Small => Self::BTN_SM.to_string(),
Self::Large => Self::BTN_LG.to_string(),
}
}
}

View file

@ -1,7 +1,5 @@
use pagetop::prelude::*;
use std::fmt;
// **< Color >**************************************************************************************
/// Paleta de colores temáticos.
@ -11,7 +9,7 @@ use std::fmt;
/// ([`classes::Background`](crate::theme::classes::Background)), bordes
/// ([`classes::Border`](crate::theme::classes::Border)) y texto
/// ([`classes::Text`](crate::theme::classes::Text)).
#[derive(AutoDefault)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Color {
#[default]
Primary,
@ -24,20 +22,45 @@ pub enum Color {
Dark,
}
#[rustfmt::skip]
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
impl Color {
// Devuelve el nombre del color.
#[rustfmt::skip]
#[inline]
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Primary => f.write_str("primary"),
Self::Secondary => f.write_str("secondary"),
Self::Success => f.write_str("success"),
Self::Info => f.write_str("info"),
Self::Warning => f.write_str("warning"),
Self::Danger => f.write_str("danger"),
Self::Light => f.write_str("light"),
Self::Dark => f.write_str("dark"),
Self::Primary => "primary",
Self::Secondary => "secondary",
Self::Success => "success",
Self::Info => "info",
Self::Warning => "warning",
Self::Danger => "danger",
Self::Light => "light",
Self::Dark => "dark",
}
}
/* Añade el nombre del color a la cadena de clases (reservado).
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(self.as_str());
} */
/// Devuelve la clase correspondiente al color.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// 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 >************************************************************************************
@ -48,7 +71,7 @@ impl fmt::Display for Color {
/// ([`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)).
#[derive(AutoDefault)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Opacity {
/// No define ninguna clase.
#[default]
@ -68,39 +91,95 @@ 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.
#[rustfmt::skip]
#[inline]
const fn suffix(&self) -> &'static str {
const fn suffix(self) -> Option<&'static str> {
match self {
Self::Default => "",
Self::Opaque => "opacity-100",
Self::SemiOpaque => "opacity-75",
Self::Half => "opacity-50",
Self::SemiTransparent => "opacity-25",
Self::AlmostTransparent => "opacity-10",
Self::Transparent => "opacity-0",
Self::Default => None,
Self::Opaque => Some("-100"),
Self::SemiOpaque => Some("-75"),
Self::Half => Some("-50"),
Self::SemiTransparent => Some("-25"),
Self::AlmostTransparent => Some("-10"),
Self::Transparent => Some("-0"),
}
}
// 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 to_class(&self, prefix: impl AsRef<str>) -> String {
match self {
Self::Default => String::new(),
_ => join_pair!(prefix, "-", self.suffix()),
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::OPACITY);
} else {
classes.push_str(prefix);
classes.push_str(Self::OPACITY_PREFIX);
}
classes.push_str(suffix);
}
}
}
impl fmt::Display for Opacity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.suffix())
// Devuelve la clase de opacidad con el prefijo dado (`bg`, `border`, `text`, o vacío para
// `opacity-*`).
//
// # Ejemplos
//
// ```rust
// # use pagetop_bootsier::prelude::*;
// 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"), "");
// ```
#[inline]
pub(crate) 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
/// # use pagetop_bootsier::prelude::*;
/// assert_eq!(Opacity::Opaque.to_class(), "opacity-100");
/// assert_eq!(Opacity::Half.to_class(), "opacity-50");
/// assert_eq!(Opacity::Default.to_class(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
self.class_with("")
}
}
// **< ColorBg >************************************************************************************
/// Colores `bg-*` para el fondo.
#[derive(AutoDefault)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ColorBg {
/// No define ninguna clase.
#[default]
@ -123,27 +202,83 @@ pub enum ColorBg {
Transparent,
}
#[rustfmt::skip]
impl fmt::Display for ColorBg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
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.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::Default => Ok(()),
Self::Body => f.write_str("bg-body"),
Self::BodySecondary => f.write_str("bg-body-secondary"),
Self::BodyTertiary => f.write_str("bg-body-tertiary"),
Self::Theme(c) => write!(f, "bg-{c}"),
Self::Subtle(c) => write!(f, "bg-{c}-subtle"),
Self::Black => f.write_str("bg-black"),
Self::White => f.write_str("bg-white"),
Self::Transparent => f.write_str("bg-transparent"),
Self::Default => None,
Self::Body => Some("-body"),
Self::BodySecondary => Some("-body-secondary"),
Self::BodyTertiary => Some("-body-tertiary"),
Self::Theme(_) => Some(""),
Self::Subtle(_) => Some("-subtle"),
Self::Black => Some("-black"),
Self::White => Some("-white"),
Self::Transparent => Some("-transparent"),
}
}
// Añade la clase de fondo `bg-*` a la cadena de clases.
#[inline]
pub(crate) fn push_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::BG_PREFIX);
classes.push_str(c.as_str());
}
_ => classes.push_str(Self::BG),
}
classes.push_str(suffix);
}
}
/// Devuelve la clase `bg-*` correspondiente al fondo.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// 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(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
if let Some(suffix) = self.suffix() {
let base_len = match self {
Self::Theme(c) | Self::Subtle(c) => Self::BG_PREFIX.len() + c.as_str().len(),
_ => Self::BG.len(),
};
let mut class = String::with_capacity(base_len + suffix.len());
match self {
Self::Theme(c) | Self::Subtle(c) => {
class.push_str(Self::BG_PREFIX);
class.push_str(c.as_str());
}
_ => class.push_str(Self::BG),
}
class.push_str(suffix);
return class;
}
String::new()
}
}
// **< ColorText >**********************************************************************************
/// Colores `text-*` para el texto.
#[derive(AutoDefault)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ColorText {
/// No define ninguna clase.
#[default]
@ -166,19 +301,75 @@ pub enum ColorText {
White,
}
#[rustfmt::skip]
impl fmt::Display for ColorText {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
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.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::Default => Ok(()),
Self::Body => f.write_str("text-body"),
Self::BodyEmphasis => f.write_str("text-body-emphasis"),
Self::BodySecondary => f.write_str("text-body-secondary"),
Self::BodyTertiary => f.write_str("text-body-tertiary"),
Self::Theme(c) => write!(f, "text-{c}"),
Self::Emphasis(c) => write!(f, "text-{c}-emphasis"),
Self::Black => f.write_str("text-black"),
Self::White => f.write_str("text-white"),
Self::Default => None,
Self::Body => Some("-body"),
Self::BodyEmphasis => Some("-body-emphasis"),
Self::BodySecondary => Some("-body-secondary"),
Self::BodyTertiary => Some("-body-tertiary"),
Self::Theme(_) => Some(""),
Self::Emphasis(_) => Some("-emphasis"),
Self::Black => Some("-black"),
Self::White => Some("-white"),
}
}
// Añade la clase de texto `text-*` a la cadena de clases.
#[inline]
pub(crate) fn push_class(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(c.as_str());
}
_ => classes.push_str(Self::TEXT),
}
classes.push_str(suffix);
}
}
/// Devuelve la clase `text-*` correspondiente al color del texto.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// 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(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
if let Some(suffix) = self.suffix() {
let base_len = match self {
Self::Theme(c) | Self::Emphasis(c) => Self::TEXT_PREFIX.len() + c.as_str().len(),
_ => Self::TEXT.len(),
};
let mut class = String::with_capacity(base_len + suffix.len());
match self {
Self::Theme(c) | Self::Emphasis(c) => {
class.push_str(Self::TEXT_PREFIX);
class.push_str(c.as_str());
}
_ => class.push_str(Self::TEXT),
}
class.push_str(suffix);
return class;
}
String::new()
}
}

View file

@ -0,0 +1,104 @@
use pagetop::prelude::*;
// **< ScaleSize >**********************************************************************************
/// Escala discreta de tamaños para definir clases utilitarias.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ScaleSize {
/// Sin tamaño (no define ninguna clase).
#[default]
None,
/// Tamaño automático.
Auto,
/// Escala cero.
Zero,
/// Escala uno.
One,
/// Escala dos.
Two,
/// Escala tres.
Three,
/// Escala cuatro.
Four,
/// Escala cinco.
Five,
}
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.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::None => None,
Self::Auto => Some(""),
Self::Zero => Some("-0"),
Self::One => Some("-1"),
Self::Two => Some("-2"),
Self::Three => Some("-3"),
Self::Four => Some("-4"),
Self::Five => Some("-5"),
}
}
// 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) {
if !prefix.is_empty() {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(prefix);
classes.push_str(suffix);
}
}
}
/* Devuelve la clase del tamaño para el prefijo, o una cadena vacía si no aplica (reservado).
//
// # Ejemplo
//
// ```rust
// # use pagetop_bootsier::prelude::*;
// 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"), "");
// ```
#[inline]
pub(crate) 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).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Side {
/// Todos los lados.
#[default]
All,
/// Lado superior.
Top,
/// Lado inferior.
Bottom,
/// Lado lógico de inicio (respetando RTL).
Start,
/// Lado lógico de fin (respetando RTL).
End,
/// Lados lógicos laterales (abreviatura *x*).
LeftAndRight,
/// Lados superior e inferior (abreviatura *y*).
TopAndBottom,
}

View file

@ -1,52 +1,117 @@
use pagetop::prelude::*;
use std::fmt;
/// Radio para el redondeo de esquinas ([`classes::Rounded`](crate::theme::classes::Rounded)).
///
/// Mapea a `rounded`, `rounded-0`, `rounded-{1..5}`, `rounded-circle` y `rounded-pill`.
///
/// - `None` no añade ninguna clase.
/// - `Default` genera `rounded` (radio por defecto del tema).
/// - `Zero` genera `rounded-0` (sin redondeo).
/// - `Scale{1..5}` genera `rounded-{1..5}` (radio creciente).
/// - `Circle` genera `rounded-circle`.
/// - `Pill` genera `rounded-pill`.
#[derive(AutoDefault)]
#[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]
pub(crate) fn to_class(&self, prefix: impl AsRef<str>) -> String {
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
RoundedRadius::None => String::new(),
RoundedRadius::Default => String::from(prefix.as_ref()),
RoundedRadius::Zero => join!(prefix, "-0"),
RoundedRadius::Scale1 => join!(prefix, "-1"),
RoundedRadius::Scale2 => join!(prefix, "-2"),
RoundedRadius::Scale3 => join!(prefix, "-3"),
RoundedRadius::Scale4 => join!(prefix, "-4"),
RoundedRadius::Scale5 => join!(prefix, "-5"),
RoundedRadius::Circle => join!(prefix, "-circle"),
RoundedRadius::Pill => join!(prefix, "-pill"),
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"),
}
}
}
impl fmt::Display for RoundedRadius {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_class("rounded"))
// 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
// # use pagetop_bootsier::prelude::*;
// 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"), "");
// ```
#[inline]
pub(crate) 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
/// # use pagetop_bootsier::prelude::*;
/// 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("")
}
}