diff --git a/CHANGELOG.md b/CHANGELOG.md index bf095a2..2fc90ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,73 @@ ratio/diff ver `git show `. ## 2026-05-10 +### feat(yahweh): theme integration en `banner` + `card` + `nakui-explorer` consume themed +Iter 4 de la integración. Los widgets `banner` y `card` ahora +ofrecen variants `_themed(cx, ...)` que leen `Theme::global(cx)`. +Las versiones sin theme se preservan para apps sin theme global. +`nakui-explorer` migra a versiones themed + `Theme::install_default` +al boot — el chrome hardcoded del explorer (5 variables `let bg = +rgb(...)`) sale del theme. + +Cambios en `yahweh-widget-card`: +- **Nueva dep**: `yahweh-theme`. +- **`pub fn card_themed(cx: &App) -> Div`**: devuelve [`card`] + pre-aplicado con `bg(theme.bg_panel)`. El caller sigue componiendo + con borders, accents, children. + +Cambios en `yahweh-widget-banner`: +- **Nueva dep**: `yahweh-theme`. +- **`pub fn banner_themed(cx: &App, kind, message) -> Div`**: + deriva `(bg, fg)` según `kind` + `theme.is_dark`: + - `Info`: `theme.bg_panel_alt` + `theme.accent`. + - `Success` / `Warning` / `Error`: hue fijo (verde/amber/rojo) + + lightness flippeada según `is_dark` (dark = bg low, fg high; + light = invertido). +- **`pub fn themed_colors(kind, theme) -> (Background, Hsla)`**: + helper público para callers que quieren computar el par sin + construir el div. +- 3 tests nuevos del derivation: dark/light lightness contrast, + kinds distinguidos por hue. + +Migración de `nakui-explorer`: +- Nueva dep `yahweh-theme`. +- `main()` llama `Theme::install_default(cx)` antes de open_window + (el theme default es Nebula). +- `render`: + - 5 `let bg/text/text_dim/card_bg/border = rgb(...)` colors + locales → `theme.bg_app/fg_text/fg_muted/bg_panel/border`. + - `card().bg(card_bg)` → `card_themed(cx)` (borra los locales). + - `banner(Banner::Error, ...)` → `banner_themed(cx, Banner::Error, ...)`. + - Los accents `accent_seed` / `accent_morphism` se preservan + locales: son **señales semánticas del log** (azul=seed, + verde=morphism), no chrome del app. + +Distribución de tests: 112 → **115** (+3 del banner derivation). +Workspace stack pasó por la migración sin errores. + +Beneficio operativo: +- Cambiar de Theme (Nebula → Aurora → Solarized Light, etc.) ahora + refleja en `nakui-explorer` automáticamente. Antes había que + buscar y reemplazar los hex codes uno a uno. +- Apps que adopten el patrón `_themed` heredan el switcher de + theme cuando emerja. + +Decisiones: +- **Hue fijo por kind**: Success siempre verde, Error siempre rojo, + etc. La lightness se ajusta al theme; el hue se mantiene como + invariante semántico cross-theme. +- **API dual**: `banner` (defaults) + `banner_themed` (theme). + Apps sin theme global pueden seguir con la versión simple. +- **Acentos semánticos del explorer (seed/morphism) NO migran**: + pertenecen al dominio del log, no al chrome. + +Próximas integraciones pendientes: +- `MetaApp` (en `yahweh-widget-meta-form`) tiene su propia paleta + hardcoded de 6 colors que podría migrarse al theme. Scope mayor + que esta iter; queda como candidato. +- Theme switcher widget (botón/menú en chrome para ciclar themes). + Cuando emerja la necesidad real. + ### feat(yahweh-widget-card): container card-shape compartido para timeline entries Iteración 3 de la integración nakui ↔ yahweh. El "card visual" pattern (padding consistente + rounded + flex_col + gap) que vivía diff --git a/Cargo.lock b/Cargo.lock index e1a9803..ee7dc05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6460,6 +6460,7 @@ dependencies = [ "tempfile", "uuid", "yahweh-meta-runtime", + "yahweh-theme", "yahweh-widget-banner", "yahweh-widget-card", ] @@ -13005,6 +13006,7 @@ name = "yahweh-widget-banner" version = "0.1.0" dependencies = [ "gpui", + "yahweh-theme", ] [[package]] @@ -13012,6 +13014,7 @@ name = "yahweh-widget-card" version = "0.1.0" dependencies = [ "gpui", + "yahweh-theme", ] [[package]] diff --git a/crates/apps/nakui-explorer/Cargo.toml b/crates/apps/nakui-explorer/Cargo.toml index e2b394c..adbaa88 100644 --- a/crates/apps/nakui-explorer/Cargo.toml +++ b/crates/apps/nakui-explorer/Cargo.toml @@ -10,6 +10,7 @@ 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" } yahweh-widget-card = { path = "../../modules/ui_engine/widgets/card" } +yahweh-theme = { path = "../../modules/ui_engine/libs/theme" } 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 cfc5fe2..07a3f73 100644 --- a/crates/apps/nakui-explorer/src/main.rs +++ b/crates/apps/nakui-explorer/src/main.rs @@ -31,13 +31,18 @@ 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}; -use yahweh_widget_card::card; +use yahweh_theme::Theme; +use yahweh_widget_banner::{banner_themed, Banner}; +use yahweh_widget_card::card_themed; const REFRESH_INTERVAL: Duration = Duration::from_secs(2); fn main() { Application::new().run(|cx: &mut App| { + // Theme global instalado al boot — los widgets themed lo + // requieren, y simplifica el chrome del app a una paleta + // consistente. + Theme::install_default(cx); let bounds = Bounds::centered(None, gpui::size(px(900.), px(640.)), cx); cx.open_window( WindowOptions { @@ -139,14 +144,17 @@ fn load_log(path: &std::path::Path) -> Result, String> { } impl Render for Explorer { - fn render(&mut self, _w: &mut Window, _cx: &mut Context) -> impl IntoElement { - let bg = rgb(0x14171c); - let card_bg = rgb(0x1d2128); - let text_dim = rgb(0x9ba1ad); - let text = rgb(0xe6e8ec); + fn render(&mut self, _w: &mut Window, cx: &mut Context) -> impl IntoElement { + // Colores cromáticos del chrome del app vienen del Theme + // global (instalado en main). Los acentos por kind (seed + // azul, morphism verde) siguen siendo locales: son señales + // semánticas del log, no del chrome. + let theme = Theme::global(cx).clone(); + let bg = theme.bg_app.clone(); + let text = theme.fg_text; + let text_dim = theme.fg_muted; let accent_seed = rgb(0x88c0d0); let accent_morphism = rgb(0xa3be8c); - let _ = card_bg; let (seed_count, morphism_count, top_breakdown) = self.breakdown(); @@ -162,9 +170,9 @@ impl Render for Explorer { let header = div() .px(px(16.)) .py(px(12.)) - .bg(card_bg) + .bg(theme.bg_panel.clone()) .border_b_1() - .border_color(rgb(0x2a2f38)) + .border_color(theme.border) .text_color(text) .text_size(px(14.)) .child(header_text); @@ -184,20 +192,21 @@ impl Render for Explorer { div() .px(px(16.)) .py(px(6.)) - .bg(rgb(0x16191f)) + .bg(theme.bg_panel_alt.clone()) .text_color(text_dim) .text_size(px(11.)) .child(breakdown_line) }); - // 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.))); + // Banner de error themed: deriva (bg, fg) del Theme actual + // según `Banner::Error` + `is_dark`. Padding extra (16/8) + // del header preservado via overrides del builder. + let error_banner = self.error.as_ref().map(|e| { + banner_themed(cx, 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 @@ -225,8 +234,7 @@ impl Render for Explorer { .as_ref() .map(|h| format!("schema={}", short_hash(h))) .unwrap_or_else(|| "schema=(legacy)".into()); - card() - .bg(card_bg) + card_themed(cx) .border_l_4() .border_color(accent_seed) .child( @@ -291,8 +299,7 @@ impl Render for Explorer { .as_ref() .map(|h| format!("schema={}", short_hash(h))) .unwrap_or_else(|| "schema=(legacy)".into()); - card() - .bg(card_bg) + card_themed(cx) .border_l_4() .border_color(accent_morphism) .child( diff --git a/crates/modules/ui_engine/widgets/banner/Cargo.toml b/crates/modules/ui_engine/widgets/banner/Cargo.toml index 6b668f4..af38cf7 100644 --- a/crates/modules/ui_engine/widgets/banner/Cargo.toml +++ b/crates/modules/ui_engine/widgets/banner/Cargo.toml @@ -7,3 +7,4 @@ description = "Yahweh — widget banner: tira horizontal de status (info/success [dependencies] gpui = { workspace = true } +yahweh-theme = { path = "../../libs/theme" } diff --git a/crates/modules/ui_engine/widgets/banner/src/lib.rs b/crates/modules/ui_engine/widgets/banner/src/lib.rs index 0327b55..eee6c1c 100644 --- a/crates/modules/ui_engine/widgets/banner/src/lib.rs +++ b/crates/modules/ui_engine/widgets/banner/src/lib.rs @@ -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) -> 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) -> 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 = [ diff --git a/crates/modules/ui_engine/widgets/card/Cargo.toml b/crates/modules/ui_engine/widgets/card/Cargo.toml index e12afc8..7b32760 100644 --- a/crates/modules/ui_engine/widgets/card/Cargo.toml +++ b/crates/modules/ui_engine/widgets/card/Cargo.toml @@ -7,3 +7,4 @@ description = "Yahweh — widget card: container con padding + rounded + flex_co [dependencies] gpui = { workspace = true } +yahweh-theme = { path = "../../libs/theme" } diff --git a/crates/modules/ui_engine/widgets/card/src/lib.rs b/crates/modules/ui_engine/widgets/card/src/lib.rs index 6928ed7..883f971 100644 --- a/crates/modules/ui_engine/widgets/card/src/lib.rs +++ b/crates/modules/ui_engine/widgets/card/src/lib.rs @@ -30,7 +30,8 @@ #![forbid(unsafe_code)] -use gpui::{div, prelude::*, px, Div}; +use gpui::{div, prelude::*, px, App, Div}; +use yahweh_theme::Theme; /// Container card-shape: `flex_col` con padding `12/8`, `rounded(4)`, /// `gap(2)` interno entre children y `mb(4)` para separación @@ -52,6 +53,18 @@ pub fn card() -> Div { .gap(px(2.)) } +/// Variante themed: igual que [`card`] pero pre-aplica `bg(panel)` +/// del [`Theme`] global. El caller no necesita conocer la paleta — +/// el bg sigue al theme actual cuando éste cambia. +/// +/// Si la app no instaló un Theme, esta función panicea (gpui's +/// `cx.global::()` requiere el global instalado). Para apps +/// sin theme, usar [`card`] directo. +pub fn card_themed(cx: &App) -> Div { + let theme = Theme::global(cx); + card().bg(theme.bg_panel.clone()) +} + #[cfg(test)] mod tests { use super::*;