feat(yahweh): caret blinking + slots ornament en theme + MetaApp full themed

Iters 8-9 combinadas. Tres mejoras pequeñas que cierran la
integración del theme:

1) text-input caret blinking: caret_visible bool toggea cada 500ms
   via cx.spawn loop. _blink_task se mantiene en self para que
   drop cancele. render dibuja | sólo si focused && caret_visible.

2) yahweh-theme: 5 slots ornament secundario como methods (no
   fields) derivados de is_dark via ornament_slots() helper:
   bg_input/bg_button/bg_button_hover/accent_destructive/
   bg_destructive_hover. No requiere modificar los 6 presets.

3) MetaApp ornament cleanup: 11 rgb(0x...) hardcoded → slots del
   theme. Sidebar menu items, list row separator/buttons, icon ✕
   delete y su hover, EntityRef selector hovers, form submit
   button + fallback input bg, confirm modal hint y hovers.

Pattern: let X = theme.slot() antes de las closures + move |d|
d.bg(X) en hover/when para tomar ownership.

Antes MetaApp tenía la paleta principal themed (iter 5) pero el
ornament secundario seguía hardcoded. Ahora el theme switcher
cambia absolutamente todo el chrome.

Tests: 117 verdes. Downstream compila. Smoke nakui-ui: bootstrap
OK.

Limitación: nouser-explorer todavía hardcoded — próxima iter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-10 11:05:39 +00:00
parent fc4da751ca
commit ea074d7d57
4 changed files with 199 additions and 24 deletions
@@ -531,6 +531,11 @@ impl<B: MetaBackend> MetaApp<B> {
let (confirm_bg, confirm_text) = themed_colors(Banner::Error, theme);
let cancel_bg: gpui::Background = theme.bg_panel_alt.clone();
let cancel_text = theme.fg_text;
// Hover colors capturados antes de las closures para que el
// move |d| d.bg(...) los cierre.
let cancel_hover = theme.bg_button_hover();
let confirm_hover = theme.bg_destructive_hover();
let hint_color = theme.fg_muted;
Some(
div()
@@ -552,7 +557,7 @@ impl<B: MetaBackend> MetaApp<B> {
.child(
div()
.text_size(px(10.))
.text_color(gpui::rgb(0xc0a070))
.text_color(hint_color)
.child("Esc para cancelar · click [Confirmar] para borrar"),
),
)
@@ -566,7 +571,7 @@ impl<B: MetaBackend> MetaApp<B> {
.py(px(4.))
.bg(cancel_bg)
.text_color(cancel_text)
.hover(|d| d.bg(gpui::rgb(0x3a3f48)))
.hover(move |d| d.bg(cancel_hover))
.child("Cancelar")
.on_click(cx.listener(move |this, _e: &ClickEvent, _w, cx| {
this.pending_delete = None;
@@ -586,7 +591,7 @@ impl<B: MetaBackend> MetaApp<B> {
.py(px(4.))
.bg(confirm_bg)
.text_color(confirm_text)
.hover(|d| d.bg(gpui::rgb(0x8a2828)))
.hover(move |d| d.bg(confirm_hover))
.child("Confirmar")
.on_click(cx.listener(move |this, _e: &ClickEvent, _w, cx| {
// Limpiar primero para que un fallo del
@@ -625,6 +630,10 @@ impl<B: MetaBackend> MetaApp<B> {
text_dim: gpui::Hsla,
accent_active: gpui::Hsla,
) -> gpui::Div {
// Slots ornament del theme para los menu items de abajo.
let theme = Theme::global(cx);
let menu_active_bg = theme.bg_row_active;
let menu_hover_bg = theme.bg_row_hover;
let mut sidebar = div()
.w(px(240.))
.h_full()
@@ -696,10 +705,8 @@ impl<B: MetaBackend> MetaApp<B> {
.py(px(6.))
.text_size(px(12.))
.text_color(if is_active { accent_active } else { text_dim })
.when(is_active, |d| {
d.bg(gpui::rgb(0x232a36)).text_color(text)
})
.hover(|d| d.bg(gpui::rgb(0x1f2630)))
.when(is_active, move |d| d.bg(menu_active_bg).text_color(text))
.hover(move |d| d.bg(menu_hover_bg))
.child(label)
.on_click(cx.listener(move |this, _e: &ClickEvent, _w, cx| {
this.select_view(mod_idx, view_key.clone(), cx);
@@ -771,6 +778,14 @@ impl<B: MetaBackend> MetaApp<B> {
text_dim: gpui::Hsla,
accent: gpui::Hsla,
) -> gpui::Div {
// Ornament secundarios del theme para hovers, row separators,
// botones inline (edit ✎, delete ✕).
let theme = Theme::global(cx);
let row_separator = theme.bg_row_active;
let action_bg = theme.bg_button();
let action_hover = theme.bg_button_hover();
let destructive_fg = theme.accent_destructive();
let destructive_hover = theme.bg_destructive_hover();
let mut header = div()
.flex()
.flex_row()
@@ -799,11 +814,11 @@ impl<B: MetaBackend> MetaApp<B> {
)))
.px(px(10.))
.py(px(4.))
.bg(gpui::rgb(0x232a36))
.bg(action_bg)
.text_color(accent)
.text_size(px(11.))
.rounded(px(3.))
.hover(|d| d.bg(gpui::rgb(0x2c3540)))
.hover(move |d| d.bg(action_hover))
.child(label)
.on_click(cx.listener(move |this, _e: &ClickEvent, _w, cx| {
this.apply_action(action_clone.clone(), cx);
@@ -848,7 +863,7 @@ impl<B: MetaBackend> MetaApp<B> {
.flex_row()
.py(px(6.))
.border_b_1()
.border_color(gpui::rgb(0x232a36))
.border_color(row_separator)
.text_color(text)
.text_size(px(12.));
for c in &lv.columns {
@@ -883,7 +898,7 @@ impl<B: MetaBackend> MetaApp<B> {
.px(px(6.))
.text_color(accent)
.text_size(px(13.))
.hover(|d| d.bg(gpui::rgb(0x2c3540)))
.hover(move |d| d.bg(action_hover))
.child("")
.on_click(cx.listener(move |this, _e: &ClickEvent, _w, cx| {
this.open_edit(mod_idx, entity_for_edit.clone(), id_copy, cx);
@@ -895,9 +910,9 @@ impl<B: MetaBackend> MetaApp<B> {
"row-del-{mod_idx}-{id_copy}"
)))
.px(px(6.))
.text_color(gpui::rgb(0xd07070))
.text_color(destructive_fg)
.text_size(px(13.))
.hover(|d| d.bg(gpui::rgb(0x4a2020)))
.hover(move |d| d.bg(destructive_hover))
.child("")
.on_click(cx.listener(move |this, _e: &ClickEvent, _w, cx| {
// Marca para borrar en lugar de borrar
@@ -951,6 +966,10 @@ impl<B: MetaBackend> MetaApp<B> {
accent: gpui::Hsla,
) -> gpui::Div {
let _ = text;
// Slots ornament para hover/selected del selector + border.
let theme = Theme::global(cx);
let row_active = theme.bg_row_active;
let row_hover = theme.bg_row_hover;
let rows = self.list_rows(&target_entity);
let current = self
.form_inputs
@@ -962,7 +981,7 @@ impl<B: MetaBackend> MetaApp<B> {
.mt(px(4.))
.pl(px(8.))
.border_l_2()
.border_color(gpui::rgb(0x2a2f38))
.border_color(theme.border)
.flex()
.flex_col()
.gap(px(2.));
@@ -1002,8 +1021,8 @@ impl<B: MetaBackend> MetaApp<B> {
.py(px(2.))
.text_size(px(11.))
.text_color(if is_selected { accent } else { text_dim })
.when(is_selected, |d| d.bg(gpui::rgb(0x232a36)))
.hover(|d| d.bg(gpui::rgb(0x1f2630)))
.when(is_selected, move |d| d.bg(row_active))
.hover(move |d| d.bg(row_hover))
.child(label)
.on_click(cx.listener(move |this, _e: &ClickEvent, _w, cx| {
if let Some(input) = this.form_inputs.get(&field_for_click) {
@@ -1028,6 +1047,11 @@ impl<B: MetaBackend> MetaApp<B> {
text_dim: gpui::Hsla,
accent: gpui::Hsla,
) -> gpui::Div {
// Slots ornament para el botón submit + bg de fallback inputs.
let theme = Theme::global(cx);
let submit_bg = theme.bg_button();
let submit_hover = theme.bg_button_hover();
let input_bg = theme.bg_input();
// En modo edit, el título refleja eso para que el user no
// se confunda creyendo que hace alta nueva.
let title = match self.editing.as_ref() {
@@ -1068,7 +1092,7 @@ impl<B: MetaBackend> MetaApp<B> {
div()
.px(px(8.))
.py(px(6.))
.bg(gpui::rgb(0x171a20))
.bg(input_bg)
.text_color(text_dim)
.child("(input no inicializado)"),
);
@@ -1127,11 +1151,11 @@ impl<B: MetaBackend> MetaApp<B> {
.id(SharedString::from(format!("form-submit-{mod_idx}")))
.px(px(12.))
.py(px(6.))
.bg(gpui::rgb(0x2c3540))
.bg(submit_bg)
.text_color(accent)
.text_size(px(12.))
.rounded(px(3.))
.hover(|d| d.bg(gpui::rgb(0x3a4555)))
.hover(move |d| d.bg(submit_hover))
.child(submit_label)
.on_click(cx.listener(move |this, _e: &ClickEvent, _w, cx| {
this.apply_action(submit_action.clone(), cx);