From 4ba66f5995202a1d71eaa1d6dd1386edf8980047 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Fri, 21 Aug 2026 11:18:16 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20(base):=20A=C3=B1ade=20BadgeKind?= =?UTF-8?q?=20para=20el=20tipo=20de=20Badge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/basic.css | 26 +++++ extensions/pagetop-bootsier/src/theme/bs.rs | 2 +- .../pagetop-bootsier/src/theme/bs/badge.rs | 49 +++------- src/base/component.rs | 2 +- src/base/component/badge.rs | 97 ++++++++++++++++++- tests/component_badge.rs | 97 +++++++++++++++++++ 6 files changed, 230 insertions(+), 43 deletions(-) create mode 100644 tests/component_badge.rs diff --git a/assets/css/basic.css b/assets/css/basic.css index cfb3d107..3e199708 100644 --- a/assets/css/basic.css +++ b/assets/css/basic.css @@ -44,6 +44,32 @@ body { -webkit-tap-highlight-color: transparent; } +/* + * Badge component + */ + +.badge { + display: inline-block; + padding: 0.3em 0.6em; + font-size: 0.8125rem; + font-weight: 600; + line-height: 1; + color: #fff; + background-color: var(--val-color--secondary); + border-radius: 0.375rem; + white-space: nowrap; +} +.badge-info, +.badge-warning { + color: #000; +} +.badge-primary { background-color: var(--val-color--primary); } +.badge-secondary { background-color: var(--val-color--secondary); } +.badge-success { background-color: var(--val-color--success); } +.badge-info { background-color: var(--val-color--info); } +.badge-warning { background-color: var(--val-color--warning); } +.badge-danger { background-color: var(--val-color--danger); } + /* * Form components */ diff --git a/extensions/pagetop-bootsier/src/theme/bs.rs b/extensions/pagetop-bootsier/src/theme/bs.rs index 34270f08..ae101e55 100644 --- a/extensions/pagetop-bootsier/src/theme/bs.rs +++ b/extensions/pagetop-bootsier/src/theme/bs.rs @@ -2,7 +2,7 @@ // Badge. pub(crate) mod badge; -pub use badge::{Badge, BadgeBootsier}; +pub use badge::{Badge, BadgeKind}; // Button. mod button; diff --git a/extensions/pagetop-bootsier/src/theme/bs/badge.rs b/extensions/pagetop-bootsier/src/theme/bs/badge.rs index 709a9dfa..615119c9 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/badge.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/badge.rs @@ -1,45 +1,18 @@ use pagetop::prelude::*; -use crate::theme::*; - -pub use pagetop::base::component::Badge; - -const EXTRA_TEXT_BG: &str = "bootsier.badge.text_bg"; - -/// Extensión de Bootsier para [`Badge`]. -/// -/// Proporciona el método [`with_text_bg()`](Self::with_text_bg) para fijar el color de fondo del -/// badge usando un color de texto con contraste suficiente garantizado. -/// -/// ```rust,no_run -/// use pagetop::prelude::*; -/// use pagetop_bootsier::theme::*; -/// -/// let admin = bs::Badge::labeled(Lc::n("Admin")).with_text_bg(ThemeColor::Danger); -/// ``` -pub trait BadgeBootsier { - #[builder_fn] - fn with_text_bg(self, color: ThemeColor) -> Self; -} - -impl BadgeBootsier for Badge { - /// Establece el color de fondo usando un color de texto con contraste garantizado. - /// - /// Igual a `with_prop(PropsOp::add_classes(class::TextColor::Bg(color)))`, pero sin el riesgo - /// de acumular más de una clase `text-bg-{color}` si se llama varias veces. - #[builder_fn] - fn with_text_bg(mut self, color: ThemeColor) -> Self { - self.alter_prop(PropsOp::set_extra(EXTRA_TEXT_BG, color)); - self - } -} +pub use pagetop::base::component::{Badge, BadgeKind}; // **< Badge SETUP >******************************************************************************** +#[rustfmt::skip] pub(crate) fn setup(badge: &mut Badge) { - let color = badge.props().extra_or(EXTRA_TEXT_BG, ThemeColor::Secondary); - badge.alter_prop(PropsOp::replace_classes( - "badge", - util::join!("badge ", class::TextColor::Bg(color).to_class()), - )); + let (old, new) = match badge.kind() { + BadgeKind::Primary => ("badge-primary", "text-bg-primary"), + BadgeKind::Secondary => ("badge-secondary", "text-bg-secondary"), + BadgeKind::Success => ("badge-success", "text-bg-success"), + BadgeKind::Info => ("badge-info", "text-bg-info"), + BadgeKind::Warning => ("badge-warning", "text-bg-warning"), + BadgeKind::Danger => ("badge-danger", "text-bg-danger"), + }; + badge.alter_prop(PropsOp::replace_classes(old, new)); } diff --git a/src/base/component.rs b/src/base/component.rs index e01ed9af..53ccafdf 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -3,7 +3,7 @@ pub mod layout; mod badge; -pub use badge::Badge; +pub use badge::{Badge, BadgeKind}; mod brand; pub use brand::Brand; diff --git a/src/base/component/badge.rs b/src/base/component/badge.rs index 098202cc..12fa7ca8 100644 --- a/src/base/component/badge.rs +++ b/src/base/component/badge.rs @@ -1,12 +1,31 @@ use crate::prelude::*; +// **< BadgeKind >********************************************************************************** + +/// Tipo de [`Badge`]. +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub enum BadgeKind { + Primary, + #[default] + Secondary, + Success, + Info, + Warning, + Danger, +} + +// **< Badge >************************************************************************************** + /// Componente para mostrar una **etiqueta corta informativa** (*badge*). /// /// # Ejemplo /// /// ```rust,no_run /// # use pagetop::prelude::*; -/// let badge = Badge::labeled(Lc::n("Admin")); +/// let badge = Badge::labeled(Lc::n("Admin")).with_kind(BadgeKind::Danger); +/// +/// // Equivalente usando el constructor directo del tipo. +/// let badge = Badge::danger(Lc::n("Admin")); /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Badge { @@ -14,6 +33,9 @@ pub struct Badge { props: Props, /// Devuelve la etiqueta del badge. label: Lc, + /// Devuelve el tipo del badge. + #[getters(copy)] + kind: BadgeKind, } #[async_trait] @@ -26,8 +48,16 @@ impl Component for Badge { self.props.get_id() } + #[rustfmt::skip] fn setup(&mut self, _cx: &Context) { - self.alter_prop(PropsOp::prepend_classes("badge")); + self.alter_prop(PropsOp::prepend_classes(match self.kind() { + BadgeKind::Primary => "badge badge-primary", + BadgeKind::Secondary => "badge badge-secondary", + BadgeKind::Success => "badge badge-success", + BadgeKind::Info => "badge badge-info", + BadgeKind::Warning => "badge badge-warning", + BadgeKind::Danger => "badge badge-danger", + })); } async fn prepare(&self, cx: &mut Context) -> Result { @@ -40,7 +70,7 @@ impl Component for Badge { } impl Badge { - /// Crea un badge a partir de la etiqueta indicada. + /// Crea un badge predeterminado (`BadgeKind::default()`) con la etiqueta indicada. pub fn labeled(label: Lc) -> Self { Self { label, @@ -48,6 +78,60 @@ impl Badge { } } + /// Crea un badge de tipo *primary* con la etiqueta indicada. + pub fn primary(label: Lc) -> Self { + Self { + label, + kind: BadgeKind::Primary, + ..Default::default() + } + } + + /// Crea un badge de tipo *secondary* con la etiqueta indicada. + pub fn secondary(label: Lc) -> Self { + Self { + label, + kind: BadgeKind::Secondary, + ..Default::default() + } + } + + /// Crea un badge de tipo *success* con la etiqueta indicada. + pub fn success(label: Lc) -> Self { + Self { + label, + kind: BadgeKind::Success, + ..Default::default() + } + } + + /// Crea un badge de tipo *info* con la etiqueta indicada. + pub fn info(label: Lc) -> Self { + Self { + label, + kind: BadgeKind::Info, + ..Default::default() + } + } + + /// Crea un badge de tipo *warning* con la etiqueta indicada. + pub fn warning(label: Lc) -> Self { + Self { + label, + kind: BadgeKind::Warning, + ..Default::default() + } + } + + /// Crea un badge de tipo *danger* con la etiqueta indicada. + pub fn danger(label: Lc) -> Self { + Self { + label, + kind: BadgeKind::Danger, + ..Default::default() + } + } + // **< Badge BUILDER >************************************************************************** /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. @@ -70,4 +154,11 @@ impl Badge { self.label = label; self } + + /// Establece el tipo del badge. + #[builder_fn] + pub fn with_kind(mut self, kind: BadgeKind) -> Self { + self.kind = kind; + self + } } diff --git a/tests/component_badge.rs b/tests/component_badge.rs new file mode 100644 index 00000000..f148c023 --- /dev/null +++ b/tests/component_badge.rs @@ -0,0 +1,97 @@ +use pagetop::prelude::*; + +#[pagetop::test] +async fn label_is_rendered_when_set() { + let mut badge = Badge::labeled(Lc::n("Admin")); + let html = badge.render(&mut Context::default()).await.into_string(); + + assert!(html.contains("Admin")); +} + +#[pagetop::test] +async fn label_can_be_cleared_with_lc_none() { + let mut badge = Badge::labeled(Lc::n("Admin")).with_label(Lc::none()); + let html = badge.render(&mut Context::default()).await.into_string(); + + assert!(!html.contains("Admin")); + // The badge itself must still render. + assert!(html.contains("")); +} + +#[pagetop::test] +async fn default_kind_is_secondary() { + let mut badge = Badge::labeled(Lc::n("Admin")); + let html = badge.render(&mut Context::default()).await.into_string(); + + assert!(html.contains(r#"class="badge badge-secondary""#)); +} + +#[pagetop::test] +async fn each_direct_constructor_sets_its_kind() { + let cases = [ + (Badge::primary(Lc::n("x")), "badge-primary"), + (Badge::secondary(Lc::n("x")), "badge-secondary"), + (Badge::success(Lc::n("x")), "badge-success"), + (Badge::info(Lc::n("x")), "badge-info"), + (Badge::warning(Lc::n("x")), "badge-warning"), + (Badge::danger(Lc::n("x")), "badge-danger"), + ]; + for (mut badge, expected_class) in cases { + let html = badge.render(&mut Context::default()).await.into_string(); + assert!( + html.contains(expected_class), + "expected `{expected_class}` in `{html}`" + ); + } +} + +#[pagetop::test] +async fn with_kind_overrides_the_default() { + let mut badge = Badge::labeled(Lc::n("Admin")).with_kind(BadgeKind::Danger); + let html = badge.render(&mut Context::default()).await.into_string(); + + assert!(html.contains(r#"class="badge badge-danger""#)); +} + +#[pagetop::test] +async fn direct_constructor_is_equivalent_to_labeled_with_kind() { + let mut from_constructor = Badge::danger(Lc::n("Admin")); + let mut from_builder = Badge::labeled(Lc::n("Admin")).with_kind(BadgeKind::Danger); + + assert_eq!( + from_constructor + .render(&mut Context::default()) + .await + .into_string(), + from_builder + .render(&mut Context::default()) + .await + .into_string(), + ); +} + +#[pagetop::test] +async fn with_id_sets_the_identifier() { + let mut badge = Badge::labeled(Lc::n("Admin")).with_id("my-badge"); + let html = badge.render(&mut Context::default()).await.into_string(); + + assert!(html.contains(r#"id="my-badge""#)); +} + +#[pagetop::test] +async fn with_prop_adds_extra_classes_alongside_the_kind_class() { + let mut badge = Badge::danger(Lc::n("Admin")).with_prop(PropsOp::add_classes("custom")); + let html = badge.render(&mut Context::default()).await.into_string(); + + assert!(html.contains("badge-danger")); + assert!(html.contains("custom")); +}