feat(yahweh): theme integration en banner + card + nakui-explorer consume themed
Iter 4 de integración nakui↔yahweh. Los widgets banner y card ofrecen variants _themed(cx, ...) que leen Theme::global(cx). Versiones sin theme preservadas para apps sin theme global. yahweh-widget-card: - Nueva dep yahweh-theme. - pub fn card_themed(cx: &App) -> Div: card() + bg(theme.bg_panel). yahweh-widget-banner: - Nueva dep yahweh-theme. - pub fn banner_themed(cx, kind, msg) -> Div: deriva (bg, fg) según kind + theme.is_dark. Info usa accent del theme; Success/Warning/ Error usan hue fijo (verde/amber/rojo) + lightness flippeada. - pub fn themed_colors(kind, theme) -> (Background, Hsla) helper. - 3 tests nuevos del derivation. nakui-explorer: - Nueva dep yahweh-theme. - main() instala Theme::install_default antes de open_window. - render: 5 vars rgb() locales → theme slots (bg_app/fg_text/etc). - card() → card_themed(cx). banner() → banner_themed(cx). - Accents semánticos del log (seed azul, morphism verde) quedan locales: son señales del dominio, no chrome. Tests: 112 → 115 (+3). Stack intacto. Beneficio: cambiar de Theme refleja en nakui-explorer automático. Próximo candidato: migrar MetaApp (paleta hardcoded de 6 colors). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,3 +7,4 @@ description = "Yahweh — widget banner: tira horizontal de status (info/success
|
||||
|
||||
[dependencies]
|
||||
gpui = { workspace = true }
|
||||
yahweh-theme = { path = "../../libs/theme" }
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use gpui::{div, prelude::*, px, Div, Rgba, SharedString};
|
||||
use gpui::{div, hsla, prelude::*, px, App, Background, Div, Hsla, Rgba, SharedString};
|
||||
use yahweh_theme::Theme;
|
||||
|
||||
/// Severidad / tono del banner. Determina los colores del fondo,
|
||||
/// texto y border (si aplica). El caller no debería mezclar
|
||||
@@ -83,6 +84,50 @@ pub fn banner(kind: Banner, message: impl Into<SharedString>) -> Div {
|
||||
.child(message.into())
|
||||
}
|
||||
|
||||
/// Variante themed de [`banner`]: deriva colores siguiendo el
|
||||
/// `Theme::global(cx).is_dark` (lightness flip dark ↔ light) +
|
||||
/// hue fijo por kind (verde para Success, amber para Warning,
|
||||
/// rojo para Error). Info usa `theme.bg_panel_alt` + `theme.accent`
|
||||
/// para integrarse al chrome del app.
|
||||
///
|
||||
/// Beneficio sobre [`banner`]: cuando el usuario cambia de theme
|
||||
/// claro a oscuro, los banners ajustan contraste sin esfuerzo.
|
||||
///
|
||||
/// Si la app no instaló un `Theme`, panicea (`Theme::global` lo
|
||||
/// requiere). Para apps sin theme, usar [`banner`] directo.
|
||||
pub fn banner_themed(cx: &App, kind: Banner, message: impl Into<SharedString>) -> Div {
|
||||
let theme = Theme::global(cx);
|
||||
let (bg, fg) = themed_colors(kind, theme);
|
||||
div()
|
||||
.px(px(12.))
|
||||
.py(px(6.))
|
||||
.bg(bg)
|
||||
.text_color(fg)
|
||||
.text_size(px(11.))
|
||||
.child(message.into())
|
||||
}
|
||||
|
||||
/// Deriva el par `(bg, fg)` para un kind dado contra el theme.
|
||||
/// Public para tests + para que los consumers puedan computar el
|
||||
/// par sin construir el div (ej. para custom layouts).
|
||||
pub fn themed_colors(kind: Banner, theme: &Theme) -> (Background, Hsla) {
|
||||
match kind {
|
||||
Banner::Info => (theme.bg_panel_alt.clone(), theme.accent),
|
||||
Banner::Success => derive_pair(120.0 / 360.0, theme.is_dark),
|
||||
Banner::Warning => derive_pair(40.0 / 360.0, theme.is_dark),
|
||||
Banner::Error => derive_pair(0.0 / 360.0, theme.is_dark),
|
||||
}
|
||||
}
|
||||
|
||||
/// Computa `(bg, fg)` para un hue fijo respetando dark/light mode:
|
||||
/// dark → bg low-lightness, fg high-lightness; light → invertido.
|
||||
fn derive_pair(hue: f32, is_dark: bool) -> (Background, Hsla) {
|
||||
let (bg_l, fg_l) = if is_dark { (0.18, 0.85) } else { (0.92, 0.20) };
|
||||
let bg_hsla = hsla(hue, 0.40, bg_l, 1.0);
|
||||
let fg_hsla = hsla(hue, 0.40, fg_l, 1.0);
|
||||
(bg_hsla.into(), fg_hsla)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -107,6 +152,43 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_pair_dark_uses_low_bg_and_high_fg() {
|
||||
let (_bg, fg) = derive_pair(0.0, true);
|
||||
// En dark mode, fg lightness es alta para contraste.
|
||||
assert!(
|
||||
fg.l > 0.7,
|
||||
"fg lightness debería ser alta en dark, got {}",
|
||||
fg.l
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_pair_light_uses_high_bg_and_low_fg() {
|
||||
let (_bg, fg) = derive_pair(0.0, false);
|
||||
// En light mode, fg lightness es baja para contraste.
|
||||
assert!(
|
||||
fg.l < 0.3,
|
||||
"fg lightness debería ser baja en light, got {}",
|
||||
fg.l
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_pair_distinguishes_kinds_by_hue() {
|
||||
// Success/Warning/Error tienen hue distinto; bg lightness
|
||||
// sigue al is_dark de igual forma cross-kind. Así verificar
|
||||
// que cambiar el hue cambia bg.h (no la lightness).
|
||||
let (_, fg_success) = derive_pair(120.0 / 360.0, true);
|
||||
let (_, fg_warning) = derive_pair(40.0 / 360.0, true);
|
||||
let (_, fg_error) = derive_pair(0.0, true);
|
||||
assert!(
|
||||
fg_success.h != fg_warning.h,
|
||||
"success y warning deben diferir en hue"
|
||||
);
|
||||
assert!(fg_warning.h != fg_error.h);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_kind_has_distinct_fg_color() {
|
||||
let fgs = [
|
||||
|
||||
Reference in New Issue
Block a user