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:
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user