From e5431bc7d753f626b0e5d41b68b81d4c4046da70 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Thu, 13 Aug 2026 17:35:23 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(base):=20A=C3=B1ade=20componente?= =?UTF-8?q?=20Messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Muestra los mensajes acumulados en el Context por severidad. --- assets/css/basic.css | 33 +++++++++++++ src/base/component.rs | 3 ++ src/base/component/messages.rs | 88 ++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 src/base/component/messages.rs diff --git a/assets/css/basic.css b/assets/css/basic.css index 22a0a13a..13e663cf 100644 --- a/assets/css/basic.css +++ b/assets/css/basic.css @@ -16,6 +16,10 @@ --val-color--text--muted: color-mix(in srgb, var(--val-color--text) 50%, var(--val-color--bg)); --val-color--border: #212529; --val-color--primary: #0d6efd; + --val-color--secondary: #6c757d; + --val-color--light: #dee2e6; + --val-color--info: #0dcaf0; + --val-color--warning: #ffc107; --val-color--danger: #dc3545; --val-color--switch-off: #adb5bd; } @@ -137,6 +141,35 @@ input:disabled + label { color: var(--val-color--text--muted); } +/* + * Messages component + */ + +.message { + padding: 0.75rem 1rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.375rem; +} + +.message-info { + color: color-mix(in srgb, var(--val-color--info) 40%, black); + background-color: color-mix(in srgb, var(--val-color--info) 15%, var(--val-color--bg)); + border-color: color-mix(in srgb, var(--val-color--info) 40%, var(--val-color--bg)); +} + +.message-warning { + color: color-mix(in srgb, var(--val-color--warning) 40%, black); + background-color: color-mix(in srgb, var(--val-color--warning) 15%, var(--val-color--bg)); + border-color: color-mix(in srgb, var(--val-color--warning) 40%, var(--val-color--bg)); +} + +.message-error { + color: color-mix(in srgb, var(--val-color--danger) 40%, black); + background-color: color-mix(in srgb, var(--val-color--danger) 15%, var(--val-color--bg)); + border-color: color-mix(in srgb, var(--val-color--danger) 40%, var(--val-color--bg)); +} + /* * Table component */ diff --git a/src/base/component.rs b/src/base/component.rs index 687d2995..e2f290a6 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -29,6 +29,9 @@ pub use html::Html; mod intro; pub use intro::{Intro, IntroOpening}; +mod messages; +pub use messages::Messages; + mod pager; pub use pager::{Pager, PagerAlign, PagerVisibility}; diff --git a/src/base/component/messages.rs b/src/base/component/messages.rs new file mode 100644 index 00000000..0522f4a8 --- /dev/null +++ b/src/base/component/messages.rs @@ -0,0 +1,88 @@ +use crate::prelude::*; + +/// Componente que muestra los **mensajes de usuario** acumulados en el contexto. +/// +/// No recibe ningún contenido de quien lo construye. Lo obtiene directamente del [`Context`] (ver +/// [`Context::push_message()`](Context::push_message)) en el momento de renderizarse (ver +/// [`Context::messages()`]). Si no hay ninguno acumulado, no se renderiza nada. +/// +/// # Clases CSS +/// +/// Cada nivel de severidad renderiza siempre las mismas clases fijas: `.message.message-info`, +/// `.message.message-warning` o `.message.message-error`. Será el tema activo quien decide el +/// aspecto visual de cada nivel, proporcionando las reglas CSS para esas clases. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// +/// async fn save_settings(cx: &mut Context) { +/// cx.push_message(MessageLevel::Info, Lc::n("Settings saved.")); +/// } +/// +/// async fn homepage(request: HttpRequest) -> Result { +/// Page::new(request) +/// .with_child(Messages::new()) +/// // ... resto del contenido de la página ... +/// .render() +/// .await +/// } +/// ``` +#[derive(AutoDefault, Clone, Debug, Getters)] +pub struct Messages { + /// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente. + props: Props, +} + +#[async_trait] +impl Component for Messages { + fn new() -> Self { + Self::default() + } + + fn id(&self) -> Option { + self.props.get_id() + } + + fn setup(&mut self, _cx: &Context) { + self.alter_prop(PropsOp::prepend_classes("messages")); + } + + async fn prepare(&self, cx: &mut Context) -> Result { + if !cx.has_messages() { + return Ok(html! {}); + } + Ok(html! { + div (self.props()) role="alert" { + @for message in cx.messages() { + div class=(match message.level() { + MessageLevel::Info => "message message-info", + MessageLevel::Warning => "message message-warning", + MessageLevel::Error => "message message-error", + }) { + (message.text().using(cx)) + } + } + } + }) + } +} + +impl Messages { + // **< Messages BUILDER >*********************************************************************** + + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); + self + } + + /// Modifica identificador, clases CSS, atributos HTML o valores extra del componente. + #[builder_fn] + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); + self + } +}