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:
@@ -6,6 +6,73 @@ ratio/diff ver `git show <sha>`.
|
||||
|
||||
## 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
|
||||
|
||||
Generated
+3
@@ -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]]
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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<Vec<LogEntry>, String> {
|
||||
}
|
||||
|
||||
impl Render for Explorer {
|
||||
fn render(&mut self, _w: &mut Window, _cx: &mut Context<Self>) -> 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<Self>) -> 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(
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -7,3 +7,4 @@ description = "Yahweh — widget card: container con padding + rounded + flex_co
|
||||
|
||||
[dependencies]
|
||||
gpui = { workspace = true }
|
||||
yahweh-theme = { path = "../../libs/theme" }
|
||||
|
||||
@@ -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::<Theme>()` 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::*;
|
||||
|
||||
Reference in New Issue
Block a user