diff --git a/assets/css/basic.css b/assets/css/basic.css index 3e199708..cfb3d107 100644 --- a/assets/css/basic.css +++ b/assets/css/basic.css @@ -44,32 +44,6 @@ 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 ae101e55..34270f08 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, BadgeKind}; +pub use badge::{Badge, BadgeBootsier}; // Button. mod button; diff --git a/extensions/pagetop-bootsier/src/theme/bs/badge.rs b/extensions/pagetop-bootsier/src/theme/bs/badge.rs index 615119c9..709a9dfa 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/badge.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/badge.rs @@ -1,18 +1,45 @@ use pagetop::prelude::*; -pub use pagetop::base::component::{Badge, BadgeKind}; +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 + } +} // **< Badge SETUP >******************************************************************************** -#[rustfmt::skip] pub(crate) fn setup(badge: &mut Badge) { - 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)); + 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()), + )); } diff --git a/src/base/component.rs b/src/base/component.rs index 53ccafdf..e01ed9af 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -3,7 +3,7 @@ pub mod layout; mod badge; -pub use badge::{Badge, BadgeKind}; +pub use badge::Badge; mod brand; pub use brand::Brand; diff --git a/src/base/component/badge.rs b/src/base/component/badge.rs index 12fa7ca8..098202cc 100644 --- a/src/base/component/badge.rs +++ b/src/base/component/badge.rs @@ -1,31 +1,12 @@ 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")).with_kind(BadgeKind::Danger); -/// -/// // Equivalente usando el constructor directo del tipo. -/// let badge = Badge::danger(Lc::n("Admin")); +/// let badge = Badge::labeled(Lc::n("Admin")); /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Badge { @@ -33,9 +14,6 @@ pub struct Badge { props: Props, /// Devuelve la etiqueta del badge. label: Lc, - /// Devuelve el tipo del badge. - #[getters(copy)] - kind: BadgeKind, } #[async_trait] @@ -48,16 +26,8 @@ impl Component for Badge { self.props.get_id() } - #[rustfmt::skip] fn setup(&mut self, _cx: &Context) { - 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", - })); + self.alter_prop(PropsOp::prepend_classes("badge")); } async fn prepare(&self, cx: &mut Context) -> Result { @@ -70,7 +40,7 @@ impl Component for Badge { } impl Badge { - /// Crea un badge predeterminado (`BadgeKind::default()`) con la etiqueta indicada. + /// Crea un badge a partir de la etiqueta indicada. pub fn labeled(label: Lc) -> Self { Self { label, @@ -78,60 +48,6 @@ 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))`. @@ -154,11 +70,4 @@ 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/src/core.rs b/src/core.rs index 9daf068d..03e32a94 100644 --- a/src/core.rs +++ b/src/core.rs @@ -109,7 +109,7 @@ impl TypeInfo { /// Proporciona información de tipo en tiempo de ejecución y conversión dinámica de tipos. /// -/// Este trait se implementa automáticamente para **todos** los tipos que implementen [`Any`], de +/// Este *trait* se implementa automáticamente para **todos** los tipos que implementen [`Any`], de /// modo que basta con traer [`AnyInfo`] al ámbito (`use crate::AnyInfo;`) para disponer de estos /// métodos adicionales, o usar el [`prelude`](crate::prelude) de PageTop. /// diff --git a/src/core/component/definition.rs b/src/core/component/definition.rs index e0c52207..d5aee749 100644 --- a/src/core/component/definition.rs +++ b/src/core/component/definition.rs @@ -18,7 +18,7 @@ pub trait ComponentClone { /// Define la función de renderizado para todos los componentes. /// -/// Este trait se implementa automáticamente en cualquier tipo (componente) que implemente +/// Este *trait* se implementa automáticamente en cualquier tipo (componente) que implemente /// [`Component`], por lo que no requiere ninguna codificación manual. #[async_trait] pub trait ComponentRender { diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs index 4b9b2370..18608e2f 100644 --- a/src/core/extension/definition.rs +++ b/src/core/extension/definition.rs @@ -8,7 +8,7 @@ use crate::web::Router; /// Interfaz común que debe implementar cualquier extensión de PageTop. /// -/// Este trait es fácil de implementar, basta con declarar una estructura sin campos para la +/// Este *trait* es fácil de implementar, basta con declarar una estructura sin campos para la /// extensión y sobrescribir los métodos que sean necesarios. Por ejemplo: /// /// ```rust,no_run diff --git a/src/core/theme.rs b/src/core/theme.rs index bad50216..fa9708cd 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -118,7 +118,7 @@ //! [`ReservedRegions`]: crate::response::ReservedRegions mod color; -pub use color::{ColorName, CoreColors, IntoColor}; +pub use color::{ColorName, CoreColors}; mod layout; pub use layout::{CoreRegions, RegionName, RegionRef}; diff --git a/src/core/theme/color.rs b/src/core/theme/color.rs index 62cf5cd0..1a4cb4a2 100644 --- a/src/core/theme/color.rs +++ b/src/core/theme/color.rs @@ -5,41 +5,13 @@ use crate::AutoDefault; /// Interfaz común para los colores de la paleta de un tema. /// /// PageTop ofrece una implementación predeterminada en [`CoreColors`], aunque probablemente cada -/// tema proporcionará su propia lista de colores implementando este trait. +/// tema proporcionará su propia lista de colores implementando este *trait*. pub trait ColorName { /// Devuelve el nombre asociado al color (p. ej. `"primary"`, `"danger"`, etc.). Normalmente se /// usará para generar la clase CSS del componente. fn name(self) -> &'static str; } -// **< IntoColor >********************************************************************************** - -/// Convierte un color, o su ausencia, en el nombre ya resuelto. -/// -/// Permite que un método como `with_color(color: impl IntoColor)` acepte indistintamente un color -/// directo (`CoreColors::Danger`) o uno opcional (`Some(CoreColors::Danger)`, o `None` para no -/// aplicar ninguno). -/// -/// Evita la ambigüedad de `impl From for T` junto con `impl From for Option` al -/// resolver un `C` genérico acotado por [`ColorName`]. Al ser [`IntoColor`] un trait propio, -/// ninguna implementación de [`ColorName`] cubre nunca `Option` y la resolución no es ambigua. -pub trait IntoColor { - /// Devuelve el nombre del color ya resuelto, o `None` si no se aplica ninguno. - fn into_color(self) -> Option<&'static str>; -} - -impl IntoColor for C { - fn into_color(self) -> Option<&'static str> { - Some(self.name()) - } -} - -impl IntoColor for Option { - fn into_color(self) -> Option<&'static str> { - self.map(ColorName::name) - } -} - // **< CoreColors >********************************************************************************* /// Paleta de colores predeterminada de PageTop. diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 70767cac..10563e35 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -19,7 +19,7 @@ use crate::web::http::StatusCode; /// error. El contenido de cada región depende del [`Context`](crate::core::component::Context) y de /// su nombre lógico. /// -/// Todos los métodos de este trait tienen una implementación por defecto, por lo que pueden +/// Todos los métodos de este *trait* tienen una implementación por defecto, por lo que pueden /// sobrescribirse selectivamente para crear nuevos temas con comportamientos distintos a los /// predeterminados. /// diff --git a/src/html/props.rs b/src/html/props.rs index 281a586d..04c35fe7 100644 --- a/src/html/props.rs +++ b/src/html/props.rs @@ -81,9 +81,9 @@ pub enum PropsError { /// /// Las variantes `*Extra` permiten añadir valores tipados usando una clave. Están pensadas para /// ampliar el comportamiento de componentes ya existentes. Como no es posible añadir campos a la -/// estructura de un componente ya definido, temas y extensiones pueden definir un trait con nuevos -/// métodos que leen y escriben valores extra en [`Props`]. Esos valores se interpretan como si -/// fueran valores internos del componente para tomar decisiones durante el renderizado. +/// estructura de un componente ya definido, temas y extensiones pueden definir un *trait* con +/// nuevos métodos que leen y escriben valores extra en [`Props`]. Esos valores se interpretan como +/// si fueran valores internos del componente para tomar decisiones durante el renderizado. #[derive(Clone, Debug)] pub enum PropsOp { /// Establece el identificador del componente normalizando el valor: recorta espacios, convierte @@ -374,7 +374,7 @@ impl PropsOp { /// Las variantes [`SetExtra`](PropsOp::SetExtra) y [`RemoveExtra`](PropsOp::RemoveExtra), usando /// [`PropsOp::set_extra()`] y [`PropsOp::remove_extra()`] respectivamente, permiten adjuntar /// valores tipados a un `Props`. Son útiles para que temas y extensiones amplíen el comportamiento -/// de componentes ya existentes mediante traits con nuevos métodos que lean y escriban esos +/// de componentes ya existentes mediante *traits* con nuevos métodos que lean y escriban esos /// valores. /// /// ```rust diff --git a/src/lib.rs b/src/lib.rs index 61df71e1..6b63b33f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,8 +125,8 @@ use std::ops::Deref; /// referencia a la versión del *crate* que lo usa. pub const PAGETOP_VERSION: &str = env!("CARGO_PKG_VERSION"); -/// Re-exporta el atributo [`async_trait`](https://docs.rs/async-trait) para implementar traits con -/// métodos `async`. +/// Re-exporta el atributo [`async_trait`](https://docs.rs/async-trait) para implementar *traits* +/// con métodos `async`. /// /// Si en el ámbito se declara `use pagetop::prelude::*` o `use pagetop::async_trait;` basta con /// usar el nombre corto `#[async_trait]`. Otra opción es usar su forma cualificada diff --git a/src/locale/definition.rs b/src/locale/definition.rs index 89108d5a..20d42068 100644 --- a/src/locale/definition.rs +++ b/src/locale/definition.rs @@ -15,7 +15,7 @@ static FALLBACK_LANGID: LazyLock = LazyLock::new(|| langid!( /// Representa el identificador de idioma [`LanguageIdentifier`] asociado a un recurso. /// -/// Este trait permite que distintas estructuras expongan su idioma de forma uniforme. Las +/// Este *trait* permite que distintas estructuras expongan su idioma de forma uniforme. Las /// implementaciones deben garantizar que siempre se devuelve un identificador de idioma válido. Si /// el recurso no tiene uno asignado, se puede devolver, si procede, el identificador de idioma por /// defecto de la aplicación ([`Locale::default_langid()`]). diff --git a/src/locale/request.rs b/src/locale/request.rs index 10aaa5d0..09404015 100644 --- a/src/locale/request.rs +++ b/src/locale/request.rs @@ -10,7 +10,7 @@ use super::{LangId, LanguageIdentifier, Locale}; /// `?lang=...`. El comportamiento concreto depende de la política global [`LangNegotiation`] /// configurada en la aplicación. /// -/// El idioma resultante se expone a través del trait [`LangId`], de modo que pueda usarse +/// El idioma resultante se expone a través del *trait* [`LangId`], de modo que pueda usarse /// [`RequestLocale`] como cualquier otra fuente de idioma en PageTop. /// /// [`LangNegotiation`]: crate::global::LangNegotiation diff --git a/tests/component_badge.rs b/tests/component_badge.rs deleted file mode 100644 index f148c023..00000000 --- a/tests/component_badge.rs +++ /dev/null @@ -1,97 +0,0 @@ -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")); -} diff --git a/tests/component_children.rs b/tests/component_children.rs index bce7d3da..bdec1ee4 100644 --- a/tests/component_children.rs +++ b/tests/component_children.rs @@ -37,10 +37,9 @@ impl TestComp { /// Creates a component with no id, with fixed output text. fn text(text: &str) -> Self { - Self { - text: text.to_string(), - ..Default::default() - } + let mut c = Self::default(); + c.text = text.to_string(); + c } } diff --git a/tests/component_template.rs b/tests/component_template.rs index e6e0afc4..fbf0bc8b 100644 --- a/tests/component_template.rs +++ b/tests/component_template.rs @@ -31,7 +31,7 @@ impl Theme for MarkerTheme { component: &mut dyn Component, _cx: &mut Context, ) -> Option> { - let template = (*component).downcast_ref::()?; + let template = (&*component).downcast_ref::()?; template.template().downcast_ref::()?; Some(Ok(html! { "marker-template-output" })) } diff --git a/tests/html_props_extras.rs b/tests/html_props_extras.rs index 784e849a..dd8ffad5 100644 --- a/tests/html_props_extras.rs +++ b/tests/html_props_extras.rs @@ -5,7 +5,7 @@ use pagetop::prelude::*; #[pagetop::test] async fn set_and_read_extra() { let props = Props::default().with_prop(PropsOp::set_extra("ext.flag", true)); - assert!(*props.extra::("ext.flag").unwrap()); + assert_eq!(*props.extra::("ext.flag").unwrap(), true); } #[pagetop::test] @@ -52,13 +52,13 @@ async fn extra_type_mismatch() { #[pagetop::test] async fn extra_or_returns_value_when_found() { let props = Props::default().with_prop(PropsOp::set_extra("ext.flag", true)); - assert!(props.extra_or("ext.flag", false)); + assert_eq!(props.extra_or("ext.flag", false), true); } #[pagetop::test] async fn extra_or_returns_default_on_missing() { let props = Props::default(); - assert!(!props.extra_or("ext.flag", false)); + assert_eq!(props.extra_or("ext.flag", false), false); } #[pagetop::test] @@ -72,7 +72,7 @@ async fn extra_or_returns_default_on_type_mismatch() { #[pagetop::test] async fn extra_or_default_returns_type_default_on_missing() { let props = Props::default(); - assert!(!props.extra_or_default::("ext.flag")); + assert_eq!(props.extra_or_default::("ext.flag"), false); assert_eq!(props.extra_or_default::("ext.count"), 0); } @@ -93,7 +93,7 @@ async fn extra_or_else_calls_closure_on_missing() { async fn clone_preserves_extras() { let props = Props::default().with_prop(PropsOp::set_extra("ext.flag", true)); let cloned = props.clone(); - assert!(*cloned.extra::("ext.flag").unwrap()); + assert_eq!(*cloned.extra::("ext.flag").unwrap(), true); } #[pagetop::test] @@ -103,7 +103,7 @@ async fn multiple_extras_with_different_types() { .with_prop(PropsOp::set_extra("ext.count", 42_u32)) .with_prop(PropsOp::set_extra("ext.label", "hello".to_string())); - assert!(*props.extra::("ext.flag").unwrap()); + assert_eq!(*props.extra::("ext.flag").unwrap(), true); assert_eq!(*props.extra::("ext.count").unwrap(), 42); assert_eq!(props.extra::("ext.label").unwrap(), "hello"); }