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>
This commit is contained in:
@@ -6,6 +6,55 @@ ratio/diff ver `git show <sha>`.
|
|||||||
|
|
||||||
## 2026-05-10
|
## 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`
|
### feat(yahweh): `MockBackend` público + tests E2E del widget con `gpui::TestAppContext`
|
||||||
Cierra el ciclo de testabilidad del widget metainterfaz. Hasta
|
Cierra el ciclo de testabilidad del widget metainterfaz. Hasta
|
||||||
ahora los tests del trait `MetaBackend` vivían como impl privada
|
ahora los tests del trait `MetaBackend` vivían como impl privada
|
||||||
|
|||||||
Generated
+9
@@ -6460,6 +6460,7 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"uuid",
|
"uuid",
|
||||||
"yahweh-meta-runtime",
|
"yahweh-meta-runtime",
|
||||||
|
"yahweh-widget-banner",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -12998,6 +12999,13 @@ dependencies = [
|
|||||||
"gpui",
|
"gpui",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yahweh-widget-banner"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"gpui",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yahweh-widget-container-core"
|
name = "yahweh-widget-container-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -13016,6 +13024,7 @@ dependencies = [
|
|||||||
"yahweh-meta-runtime",
|
"yahweh-meta-runtime",
|
||||||
"yahweh-meta-schema",
|
"yahweh-meta-schema",
|
||||||
"yahweh-theme",
|
"yahweh-theme",
|
||||||
|
"yahweh-widget-banner",
|
||||||
"yahweh-widget-text-input",
|
"yahweh-widget-text-input",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ members = [
|
|||||||
"crates/modules/ui_engine/widgets/tiled",
|
"crates/modules/ui_engine/widgets/tiled",
|
||||||
"crates/modules/ui_engine/widgets/text_input",
|
"crates/modules/ui_engine/widgets/text_input",
|
||||||
"crates/modules/ui_engine/widgets/meta-form",
|
"crates/modules/ui_engine/widgets/meta-form",
|
||||||
|
"crates/modules/ui_engine/widgets/banner",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/nakui/ — ERP matemático (nakui absorbido)
|
# modules/nakui/ — ERP matemático (nakui absorbido)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ description = "Explorador GPUI del event log de Nakui: timeline de seeds + morph
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
nakui-core = { path = "../../modules/nakui/core" }
|
nakui-core = { path = "../../modules/nakui/core" }
|
||||||
yahweh-meta-runtime = { path = "../../modules/ui_engine/libs/meta-runtime" }
|
yahweh-meta-runtime = { path = "../../modules/ui_engine/libs/meta-runtime" }
|
||||||
|
yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" }
|
||||||
gpui = { workspace = true }
|
gpui = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
uuid = { workspace = true, features = ["serde"] }
|
uuid = { workspace = true, features = ["serde"] }
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use nakui_core::event_log::{EventLog, LogEntry};
|
use nakui_core::event_log::{EventLog, LogEntry};
|
||||||
use yahweh_meta_runtime::{preview_value, short_hash, short_uuid};
|
use yahweh_meta_runtime::{preview_value, short_hash, short_uuid};
|
||||||
|
use yahweh_widget_banner::{banner, Banner};
|
||||||
|
|
||||||
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
|
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
|
||||||
|
|
||||||
@@ -188,15 +189,14 @@ impl Render for Explorer {
|
|||||||
.child(breakdown_line)
|
.child(breakdown_line)
|
||||||
});
|
});
|
||||||
|
|
||||||
let error_banner = self.error.as_ref().map(|e| {
|
// Banner de error vía widget compartido yahweh-widget-banner.
|
||||||
div()
|
// Padding extra (px 16/8) por convención del explorer; el
|
||||||
.px(px(16.))
|
// default del widget es 12/6 — el override mantiene la
|
||||||
.py(px(8.))
|
// visual del header.
|
||||||
.bg(rgb(0x4a2020))
|
let error_banner = self
|
||||||
.text_color(rgb(0xffd0d0))
|
.error
|
||||||
.text_size(px(12.))
|
.as_ref()
|
||||||
.child(e.clone())
|
.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
|
// Renderea las últimas N entries (la timeline crece hacia abajo
|
||||||
// en append-order; mostramos las más recientes primero para
|
// en append-order; mostramos las más recientes primero para
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ uuid = { workspace = true, features = ["serde"] }
|
|||||||
yahweh-meta-runtime = { path = "../../libs/meta-runtime" }
|
yahweh-meta-runtime = { path = "../../libs/meta-runtime" }
|
||||||
yahweh-meta-schema = { path = "../../libs/meta-schema" }
|
yahweh-meta-schema = { path = "../../libs/meta-schema" }
|
||||||
yahweh-theme = { path = "../../libs/theme" }
|
yahweh-theme = { path = "../../libs/theme" }
|
||||||
|
yahweh-widget-banner = { path = "../banner" }
|
||||||
yahweh-widget-text-input = { path = "../text_input" }
|
yahweh-widget-text-input = { path = "../text_input" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ use yahweh_meta_runtime::{
|
|||||||
MetaBackend, WriteOutcome,
|
MetaBackend, WriteOutcome,
|
||||||
};
|
};
|
||||||
use yahweh_meta_schema::{Action, FieldKind, FieldSpec, FormView, ListView, Module, View};
|
use yahweh_meta_schema::{Action, FieldKind, FieldSpec, FormView, ListView, Module, View};
|
||||||
|
use yahweh_widget_banner::{banner, Banner};
|
||||||
use yahweh_widget_text_input::TextInput;
|
use yahweh_widget_text_input::TextInput;
|
||||||
|
|
||||||
/// Estado del runtime de UI. Toda la persistencia/ejecución está
|
/// 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 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 main_panel = self.render_main(cx, panel, border, text, text_dim, accent);
|
||||||
let confirm_banner = self.render_confirm_delete_banner(cx);
|
let confirm_banner = self.render_confirm_delete_banner(cx);
|
||||||
let toast_div = self.toast.as_ref().map(|t| {
|
let toast_div = self
|
||||||
div()
|
.toast
|
||||||
.px(px(12.))
|
.as_ref()
|
||||||
.py(px(6.))
|
.map(|t| banner(Banner::Success, t.clone()));
|
||||||
.bg(gpui::rgb(0x2d3a2a))
|
let error_banner = self
|
||||||
.text_color(gpui::rgb(0xc0e0a0))
|
.load_error
|
||||||
.text_size(px(11.))
|
.as_ref()
|
||||||
.child(t.clone())
|
.map(|e| banner(Banner::Error, e.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())
|
|
||||||
});
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
|
|||||||
Reference in New Issue
Block a user