From 715cf6be030ba776946eb5866dad7ac28d961d9b Mon Sep 17 00:00:00 2001 From: Sergio Date: Sun, 10 May 2026 09:57:32 +0000 Subject: [PATCH] feat(yahweh-widget-banner): widget compartido para toasts/errores cross-app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CHANGELOG.md | 49 +++++++ Cargo.lock | 9 ++ Cargo.toml | 1 + crates/apps/nakui-explorer/Cargo.toml | 1 + crates/apps/nakui-explorer/src/main.rs | 18 +-- .../ui_engine/widgets/banner/Cargo.toml | 9 ++ .../ui_engine/widgets/banner/src/lib.rs | 125 ++++++++++++++++++ .../ui_engine/widgets/meta-form/Cargo.toml | 1 + .../ui_engine/widgets/meta-form/src/lib.rs | 27 ++-- 9 files changed, 213 insertions(+), 27 deletions(-) create mode 100644 crates/modules/ui_engine/widgets/banner/Cargo.toml create mode 100644 crates/modules/ui_engine/widgets/banner/src/lib.rs 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 `. ## 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) -> 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 Render for MetaApp { 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()