♻️ (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:
parent
748bd81bf1
commit
2e39af0856
33 changed files with 1607 additions and 647 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
104
extensions/pagetop-bootsier/src/theme/aux/layout.rs
Normal file
104
extensions/pagetop-bootsier/src/theme/aux/layout.rs
Normal 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,
|
||||
}
|
||||
|
|
@ -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("")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue