⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc
crates/modules/ui_engine/widgets/banner/src/lib.rs
History
Who touched what
raw
feat(yahweh): theme integration en banner + card + nakui-explorer consume themed
1//! `yahweh-widget-banner` — tiras horizontales de status.
2//!
3//! Cuatro variants con paleta consistente entre apps:
4//!
5//! - [`Banner::Info`] — azul tenue, mensajes neutros.
6//! - [`Banner::Success`] — verde, confirmaciones de op exitosa
7//! (toasts típicos).
8//! - [`Banner::Warning`] — amber, llamadas de atención (modales
9//! de confirmación, condiciones de "por las dudas").
10//! - [`Banner::Error`] — rojo, errores fatales o de carga.
11//!
12//! Diseño: una `Div` GPUI con paddings + colors hardcoded por
13//! variant. El caller añade niños via el builder de div (`.child(...)`,
14//! `.flex()`, etc.) para customizar más allá del default.
15//!
16//! # Ejemplo
17//!
18//! ```ignore
19//! use yahweh_widget_banner::{banner, Banner};
20//!
21//! // Toast simple (success):
22//! let toast = banner(Banner::Success, "guardado");
23//!
24//! // Banner de error con extra child:
25//! let err = banner(Banner::Error, "no pude leer log").child(
26//! div().text_size(px(10.)).child("(timeout 3s)")
27//! );
28//! ```
29
30#![forbid(unsafe_code)]
31
32use gpui::{div, hsla, prelude::*, px, App, Background, Div, Hsla, Rgba, SharedString};
33use yahweh_theme::Theme;
34
35/// Severidad / tono del banner. Determina los colores del fondo,
36/// texto y border (si aplica). El caller no debería mezclar
37/// kinds en un mismo banner — usar la composición de divs si
38/// hace falta una vista híbrida.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Banner {
41 Info,
42 Success,
43 Warning,
44 Error,
45}
46
47impl Banner {
48 /// Color de fondo del banner (sin alpha).
49 pub fn bg(self) -> Rgba {
50 match self {
51 Banner::Info => gpui::rgb(0x1d2a3a),
52 Banner::Success => gpui::rgb(0x2d3a2a),
53 Banner::Warning => gpui::rgb(0x4a3a1a),
54 Banner::Error => gpui::rgb(0x4a2020),
55 }
56 }
57
58 /// Color del texto principal del banner.
59 pub fn fg(self) -> Rgba {
60 match self {
61 Banner::Info => gpui::rgb(0xc0d0e0),
62 Banner::Success => gpui::rgb(0xc0e0a0),
63 Banner::Warning => gpui::rgb(0xf0e0a0),
64 Banner::Error => gpui::rgb(0xffd0d0),
65 }
66 }
67}
68
69/// Construye un banner con el `kind` indicado y `message` como
70/// texto principal. Devuelve un [`Div`] al que el caller puede
71/// agregar children, `id`, handlers, etc.
72///
73/// Padding y text_size son los defaults estándar del repo
74/// (`px(12./6.)` en cada axis, `px(11.)` para el texto). Para un
75/// banner más grande/chico, llamar `.text_size(...)` / `.px(...)`
76/// sobre el resultado.
77pub fn banner(kind: Banner, message: impl Into<SharedString>) -> Div {
78 div()
79 .px(px(12.))
80 .py(px(6.))
81 .bg(kind.bg())
82 .text_color(kind.fg())
83 .text_size(px(11.))
84 .child(message.into())
85}
86
87/// Variante themed de [`banner`]: deriva colores siguiendo el
88/// `Theme::global(cx).is_dark` (lightness flip dark ↔ light) +
89/// hue fijo por kind (verde para Success, amber para Warning,
90/// rojo para Error). Info usa `theme.bg_panel_alt` + `theme.accent`
91/// para integrarse al chrome del app.
92///
93/// Beneficio sobre [`banner`]: cuando el usuario cambia de theme
94/// claro a oscuro, los banners ajustan contraste sin esfuerzo.
95///
96/// Si la app no instaló un `Theme`, panicea (`Theme::global` lo
97/// requiere). Para apps sin theme, usar [`banner`] directo.
98pub fn banner_themed(cx: &App, kind: Banner, message: impl Into<SharedString>) -> Div {
99 let theme = Theme::global(cx);
100 let (bg, fg) = themed_colors(kind, theme);
101 div()
102 .px(px(12.))
103 .py(px(6.))
104 .bg(bg)
105 .text_color(fg)
106 .text_size(px(11.))
107 .child(message.into())
108}
109
110/// Deriva el par `(bg, fg)` para un kind dado contra el theme.
111/// Public para tests + para que los consumers puedan computar el
112/// par sin construir el div (ej. para custom layouts).
113pub fn themed_colors(kind: Banner, theme: &Theme) -> (Background, Hsla) {
114 match kind {
115 Banner::Info => (theme.bg_panel_alt.clone(), theme.accent),
116 Banner::Success => derive_pair(120.0 / 360.0, theme.is_dark),
117 Banner::Warning => derive_pair(40.0 / 360.0, theme.is_dark),
118 Banner::Error => derive_pair(0.0 / 360.0, theme.is_dark),
119 }
120}
121
122/// Computa `(bg, fg)` para un hue fijo respetando dark/light mode:
123/// dark → bg low-lightness, fg high-lightness; light → invertido.
124fn derive_pair(hue: f32, is_dark: bool) -> (Background, Hsla) {
125 let (bg_l, fg_l) = if is_dark { (0.18, 0.85) } else { (0.92, 0.20) };
126 let bg_hsla = hsla(hue, 0.40, bg_l, 1.0);
127 let fg_hsla = hsla(hue, 0.40, fg_l, 1.0);
128 (bg_hsla.into(), fg_hsla)
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn each_kind_has_distinct_bg_color() {
137 // Sanity: ningún kind comparte bg con otro. Si emerge una
138 // versión "low-contrast" de algún kind, abrir en otro
139 // variant en vez de re-usar el color.
140 let bgs = [
141 Banner::Info.bg(),
142 Banner::Success.bg(),
143 Banner::Warning.bg(),
144 Banner::Error.bg(),
145 ];
146 let mut seen = std::collections::BTreeSet::new();
147 for b in &bgs {
148 assert!(
149 seen.insert((b.r * 1000.0) as u32 + (b.g * 1000.0) as u32 * 1000),
150 "bg colors collision"
151 );
152 }
153 }
154
155 #[test]
156 fn derive_pair_dark_uses_low_bg_and_high_fg() {
157 let (_bg, fg) = derive_pair(0.0, true);
158 // En dark mode, fg lightness es alta para contraste.
159 assert!(
160 fg.l > 0.7,
161 "fg lightness debería ser alta en dark, got {}",
162 fg.l
163 );
164 }
165
166 #[test]
167 fn derive_pair_light_uses_high_bg_and_low_fg() {
168 let (_bg, fg) = derive_pair(0.0, false);
169 // En light mode, fg lightness es baja para contraste.
170 assert!(
171 fg.l < 0.3,
172 "fg lightness debería ser baja en light, got {}",
173 fg.l
174 );
175 }
176
177 #[test]
178 fn derive_pair_distinguishes_kinds_by_hue() {
179 // Success/Warning/Error tienen hue distinto; bg lightness
180 // sigue al is_dark de igual forma cross-kind. Así verificar
181 // que cambiar el hue cambia bg.h (no la lightness).
182 let (_, fg_success) = derive_pair(120.0 / 360.0, true);
183 let (_, fg_warning) = derive_pair(40.0 / 360.0, true);
184 let (_, fg_error) = derive_pair(0.0, true);
185 assert!(
186 fg_success.h != fg_warning.h,
187 "success y warning deben diferir en hue"
188 );
189 assert!(fg_warning.h != fg_error.h);
190 }
191
192 #[test]
193 fn each_kind_has_distinct_fg_color() {
194 let fgs = [
195 Banner::Info.fg(),
196 Banner::Success.fg(),
197 Banner::Warning.fg(),
198 Banner::Error.fg(),
199 ];
200 let mut seen = std::collections::BTreeSet::new();
201 for f in &fgs {
202 assert!(
203 seen.insert((f.r * 1000.0) as u32 + (f.g * 1000.0) as u32 * 1000)
204 );
205 }
206 }
207}