gioser

tawasuyu/brahman

Rust 231TOML 70JSON 14HTML 12905 Definitions

feat(yahweh-widget-banner): widget compartido para toasts/errores cross-app

Patrón visual común a yahweh-widget-meta-form (toast success +
error banner) y nakui-explorer (error banner): div con bg + text
colored según severidad. Antes duplicado con colores hardcoded en
cada consumer.

Crate nuevo crates/modules/ui_engine/widgets/banner:
- pub enum Banner { Info, Success, Warning, Error } con bg()/fg()
  hardcoded por variant.
- pub fn banner(kind, message) -> Div: padding/text_size defaults;
  caller compone con .child()/.px()/.on_click()/etc.
- 2 tests sanity (no color collisions).

Migración:
- yahweh-widget-meta-form: 12 líneas hardcoded → 2 llamadas a
  banner().
- nakui-explorer: error banner usa banner() + override de
  padding custom del header.

Tests stack: 109 → 111 (+2). Cada crate compila individualmente.

Próximo natural: confirm_delete_banner (Warning + botones) puede
extraerse como modal-banner cuando emerja segundo consumer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

715cf6be030ba776946eb5866dad7ac28d961d9b · by Sergio · (5 months ago) · parent 3e4278d76 · browse the tree at this commit

Semantic delta · 9

crates/apps/nakui-explorer/src/main.rs

crates/modules/ui_engine/widgets/banner/src/lib.rs

crates/modules/ui_engine/widgets/meta-form/src/lib.rs

9 files · +213 −27

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a63751..d141805 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,55 @@ ratio/diff ver `git show <sha>`.
 
 ## 2026-05-10
 
+### feat(yahweh-widget-banner): widget compartido para toasts/errores cross-app
+Patrón visual común a `yahweh-widget-meta-form` (toast success +
+error_banner) y `nakui-explorer` (error_banner): un `div` con bg
++ text colored según severidad. Antes vivía duplicado con colores
+hardcoded en cada consumer; ahora hay un widget yahweh con presets
+consistentes.
+
+Crate nuevo: `crates/modules/ui_engine/widgets/banner/`
+(`yahweh-widget-banner`):
+- **Dep**: solo `gpui` (sin nakui, sin runtime). Reusable por
+  cualquier app GPUI que necesite tiras de status.
+- **`pub enum Banner`** con 4 variants:
+  - `Info` (azul tenue, mensajes neutros).
+  - `Success` (verde, confirmaciones).
+  - `Warning` (amber, llamadas de atención).
+  - `Error` (rojo, errores fatales).
+- **Métodos `Banner::bg()` y `Banner::fg()`**: paleta hardcoded por
+  variant (sin tema dinámico todavía — cuando emerja, se
+  inyecta vía `yahweh-theme`).
+- **`pub fn banner(kind, message) -> Div`**: constructor que
+  devuelve el div ya con padding/text_size defaults; el caller
+  puede agregar children, override pads/sizes, attach handlers.
+- 2 tests sanity: ningún kind comparte bg, ningún kind comparte fg.
+
+Migración de consumers:
+- **`yahweh-widget-meta-form`**: nueva dep `yahweh-widget-banner`.
+  El `toast_div` (Success) y `error_banner` (Error) en
+  `MetaApp::render` pasan de 2x6 líneas hardcoded a una llamada
+  a `banner(...)` cada uno (~12 líneas → 2).
+- **`nakui-explorer`**: nueva dep. El error banner local pasa a
+  `banner(Banner::Error, e).px(16).py(8).text_size(12)` —
+  preserva el padding/size custom del header del explorer via
+  override builder.
+
+Tests stack: 109 → **111 verdes** (+2 del crate banner).
+
+Beneficio operativo:
+- Si emerge un tercer consumer, importa la dep + 1 llamada.
+- Cambiar la paleta de un kind = un cambio en un solo lugar
+  (ej. ajustar tono del Error o el contraste del Warning).
+- Composición preservada: el `banner()` devuelve un `Div` directo,
+  el caller modifica con builder calls (`.child()`, `.px()`,
+  `.on_click()`, etc.) sin rewrap.
+
+Próximo candidato natural: el `confirm_delete_banner` de MetaApp
+es Banner::Warning + 2 botones embedded. Cuando emerja un segundo
+consumer de modal-style banners, extraer un widget compositivo
+arriba del `Banner` base.
+
 ### feat(yahweh): `MockBackend` público + tests E2E del widget con `gpui::TestAppContext`
 Cierra el ciclo de testabilidad del widget metainterfaz. Hasta
 ahora los tests del trait `MetaBackend` vivían como impl privada
diff --git a/Cargo.lock b/Cargo.lock
index 3e62271..797eb7c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6460,6 +6460,7 @@ dependencies = [
  "tempfile",
  "uuid",
  "yahweh-meta-runtime",
+ "yahweh-widget-banner",
 ]
 
 [[package]]
@@ -12998,6 +12999,13 @@ dependencies = [
  "gpui",
 ]
 
+[[package]]
+name = "yahweh-widget-banner"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+]
+
 [[package]]
 name = "yahweh-widget-container-core"
 version = "0.1.0"
@@ -13016,6 +13024,7 @@ dependencies = [
  "yahweh-meta-runtime",
  "yahweh-meta-schema",
  "yahweh-theme",
+ "yahweh-widget-banner",
  "yahweh-widget-text-input",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index 6c1a7ce..6367f2d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -63,6 +63,7 @@ members = [
     "crates/modules/ui_engine/widgets/tiled",
     "crates/modules/ui_engine/widgets/text_input",
     "crates/modules/ui_engine/widgets/meta-form",
+    "crates/modules/ui_engine/widgets/banner",
 
     # ============================================================
     # modules/nakui/ — ERP matemático (nakui absorbido)
diff --git a/crates/apps/nakui-explorer/Cargo.toml b/crates/apps/nakui-explorer/Cargo.toml
index 321bd06..046a982 100644
--- a/crates/apps/nakui-explorer/Cargo.toml
+++ b/crates/apps/nakui-explorer/Cargo.toml
@@ -8,6 +8,7 @@ description = "Explorador GPUI del event log de Nakui: timeline de seeds + morph
 [dependencies]
 nakui-core = { path = "../../modules/nakui/core" }
 yahweh-meta-runtime = { path = "../../modules/ui_engine/libs/meta-runtime" }
+yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" }
 gpui = { workspace = true }
 serde_json = { workspace = true }
 uuid = { workspace = true, features = ["serde"] }
diff --git a/crates/apps/nakui-explorer/src/main.rs b/crates/apps/nakui-explorer/src/main.rs
index 9ff22a4..51fa042 100644
--- a/crates/apps/nakui-explorer/src/main.rs
+++ b/crates/apps/nakui-explorer/src/main.rs
@@ -31,6 +31,7 @@ use gpui::{
 };
 use nakui_core::event_log::{EventLog, LogEntry};
 use yahweh_meta_runtime::{preview_value, short_hash, short_uuid};
+use yahweh_widget_banner::{banner, Banner};
 
 const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
 
@@ -188,15 +189,14 @@ impl Render for Explorer {
                 .child(breakdown_line)
         });
 
-        let error_banner = self.error.as_ref().map(|e| {
-            div()
-                .px(px(16.))
-                .py(px(8.))
-                .bg(rgb(0x4a2020))
-                .text_color(rgb(0xffd0d0))
-                .text_size(px(12.))
-                .child(e.clone())
-        });
+        // Banner de error vía widget compartido yahweh-widget-banner.
+        // Padding extra (px 16/8) por convención del explorer; el
+        // default del widget es 12/6 — el override mantiene la
+        // visual del header.
+        let error_banner = self
+            .error
+            .as_ref()
+            .map(|e| banner(Banner::Error, e.clone()).px(px(16.)).py(px(8.)).text_size(px(12.)));
 
         // Renderea las últimas N entries (la timeline crece hacia abajo
         // en append-order; mostramos las más recientes primero para
diff --git a/crates/modules/ui_engine/widgets/banner/Cargo.toml b/crates/modules/ui_engine/widgets/banner/Cargo.toml
new file mode 100644
index 0000000..6b668f4
--- /dev/null
+++ b/crates/modules/ui_engine/widgets/banner/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "yahweh-widget-banner"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+description = "Yahweh — widget banner: tira horizontal de status (info/success/warning/error). Reusable cross-app para toasts, errores, mensajes informativos."
+
+[dependencies]
+gpui = { workspace = true }
diff --git a/crates/modules/ui_engine/widgets/banner/src/lib.rs b/crates/modules/ui_engine/widgets/banner/src/lib.rs
new file mode 100644
index 0000000..0327b55
--- /dev/null
+++ b/crates/modules/ui_engine/widgets/banner/src/lib.rs
@@ -0,0 +1,125 @@
+//! `yahweh-widget-banner` — tiras horizontales de status.
+//!
+//! Cuatro variants con paleta consistente entre apps:
+//!
+//! - [`Banner::Info`] — azul tenue, mensajes neutros.
+//! - [`Banner::Success`] — verde, confirmaciones de op exitosa
+//!   (toasts típicos).
+//! - [`Banner::Warning`] — amber, llamadas de atención (modales
+//!   de confirmación, condiciones de "por las dudas").
+//! - [`Banner::Error`] — rojo, errores fatales o de carga.
+//!
+//! Diseño: una `Div` GPUI con paddings + colors hardcoded por
+//! variant. El caller añade niños via el builder de div (`.child(...)`,
+//! `.flex()`, etc.) para customizar más allá del default.
+//!
+//! # Ejemplo
+//!
+//! ```ignore
+//! use yahweh_widget_banner::{banner, Banner};
+//!
+//! // Toast simple (success):
+//! let toast = banner(Banner::Success, "guardado");
+//!
+//! // Banner de error con extra child:
+//! let err = banner(Banner::Error, "no pude leer log").child(
+//!     div().text_size(px(10.)).child("(timeout 3s)")
+//! );
+//! ```
+
+#![forbid(unsafe_code)]
+
+use gpui::{div, prelude::*, px, Div, Rgba, SharedString};
+
+/// Severidad / tono del banner. Determina los colores del fondo,
+/// texto y border (si aplica). El caller no debería mezclar
+/// kinds en un mismo banner — usar la composición de divs si
+/// hace falta una vista híbrida.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Banner {
+    Info,
+    Success,
+    Warning,
+    Error,
+}
+
+impl Banner {
+    /// Color de fondo del banner (sin alpha).
+    pub fn bg(self) -> Rgba {
+        match self {
+            Banner::Info => gpui::rgb(0x1d2a3a),
+            Banner::Success => gpui::rgb(0x2d3a2a),
+            Banner::Warning => gpui::rgb(0x4a3a1a),
+            Banner::Error => gpui::rgb(0x4a2020),
+        }
+    }
+
+    /// Color del texto principal del banner.
+    pub fn fg(self) -> Rgba {
+        match self {
+            Banner::Info => gpui::rgb(0xc0d0e0),
+            Banner::Success => gpui::rgb(0xc0e0a0),
+            Banner::Warning => gpui::rgb(0xf0e0a0),
+            Banner::Error => gpui::rgb(0xffd0d0),
+        }
+    }
+}
+
+/// Construye un banner con el `kind` indicado y `message` como
+/// texto principal. Devuelve un [`Div`] al que el caller puede
+/// agregar children, `id`, handlers, etc.
+///
+/// Padding y text_size son los defaults estándar del repo
+/// (`px(12./6.)` en cada axis, `px(11.)` para el texto). Para un
+/// banner más grande/chico, llamar `.text_size(...)` / `.px(...)`
+/// sobre el resultado.
+pub fn banner(kind: Banner, message: impl Into<SharedString>) -> Div {
+    div()
+        .px(px(12.))
+        .py(px(6.))
+        .bg(kind.bg())
+        .text_color(kind.fg())
+        .text_size(px(11.))
+        .child(message.into())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn each_kind_has_distinct_bg_color() {
+        // Sanity: ningún kind comparte bg con otro. Si emerge una
+        // versión "low-contrast" de algún kind, abrir en otro
+        // variant en vez de re-usar el color.
+        let bgs = [
+            Banner::Info.bg(),
+            Banner::Success.bg(),
+            Banner::Warning.bg(),
+            Banner::Error.bg(),
+        ];
+        let mut seen = std::collections::BTreeSet::new();
+        for b in &bgs {
+            assert!(
+                seen.insert((b.r * 1000.0) as u32 + (b.g * 1000.0) as u32 * 1000),
+                "bg colors collision"
+            );
+        }
+    }
+
+    #[test]
+    fn each_kind_has_distinct_fg_color() {
+        let fgs = [
+            Banner::Info.fg(),
+            Banner::Success.fg(),
+            Banner::Warning.fg(),
+            Banner::Error.fg(),
+        ];
+        let mut seen = std::collections::BTreeSet::new();
+        for f in &fgs {
+            assert!(
+                seen.insert((f.r * 1000.0) as u32 + (f.g * 1000.0) as u32 * 1000)
+            );
+        }
+    }
+}
diff --git a/crates/modules/ui_engine/widgets/meta-form/Cargo.toml b/crates/modules/ui_engine/widgets/meta-form/Cargo.toml
index 003d2a5..c1868d4 100644
--- a/crates/modules/ui_engine/widgets/meta-form/Cargo.toml
+++ b/crates/modules/ui_engine/widgets/meta-form/Cargo.toml
@@ -12,6 +12,7 @@ uuid = { workspace = true, features = ["serde"] }
 yahweh-meta-runtime = { path = "../../libs/meta-runtime" }
 yahweh-meta-schema = { path = "../../libs/meta-schema" }
 yahweh-theme = { path = "../../libs/theme" }
+yahweh-widget-banner = { path = "../banner" }
 yahweh-widget-text-input = { path = "../text_input" }
 
 [dev-dependencies]
diff --git a/crates/modules/ui_engine/widgets/meta-form/src/lib.rs b/crates/modules/ui_engine/widgets/meta-form/src/lib.rs
index 387c566..5f60583 100644
--- a/crates/modules/ui_engine/widgets/meta-form/src/lib.rs
+++ b/crates/modules/ui_engine/widgets/meta-form/src/lib.rs
@@ -34,6 +34,7 @@ use yahweh_meta_runtime::{
     MetaBackend, WriteOutcome,
 };
 use yahweh_meta_schema::{Action, FieldKind, FieldSpec, FormView, ListView, Module, View};
+use yahweh_widget_banner::{banner, Banner};
 use yahweh_widget_text_input::TextInput;
 
 /// Estado del runtime de UI. Toda la persistencia/ejecución está
@@ -450,24 +451,14 @@ impl<B: MetaBackend> Render for MetaApp<B> {
         let sidebar = self.render_sidebar(cx, panel, border, text, text_dim, accent_active);
         let main_panel = self.render_main(cx, panel, border, text, text_dim, accent);
         let confirm_banner = self.render_confirm_delete_banner(cx);
-        let toast_div = self.toast.as_ref().map(|t| {
-            div()
-                .px(px(12.))
-                .py(px(6.))
-                .bg(gpui::rgb(0x2d3a2a))
-                .text_color(gpui::rgb(0xc0e0a0))
-                .text_size(px(11.))
-                .child(t.clone())
-        });
-        let error_banner = self.load_error.as_ref().map(|e| {
-            div()
-                .px(px(12.))
-                .py(px(6.))
-                .bg(gpui::rgb(0x4a2020))
-                .text_color(gpui::rgb(0xffd0d0))
-                .text_size(px(11.))
-                .child(e.clone())
-        });
+        let toast_div = self
+            .toast
+            .as_ref()
+            .map(|t| banner(Banner::Success, t.clone()));
+        let error_banner = self
+            .load_error
+            .as_ref()
+            .map(|e| banner(Banner::Error, e.clone()));
 
         div()
             .flex()