gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Sutichaykuna

feat(nakui-ui): atajo Esc para cancelar el modal de delete

`capture_key_down` en el root div: si event.keystroke.key=="escape" y
hay pending_delete, lo limpia y emite toast "delete cancelado (X)
[esc]". Capture phase (no bubble) intercepta el Esc antes de que
cualquier TextInput descendiente lo consuma. Sin pending el handler
es no-op, el evento sigue su flujo.

Hint visual en el banner: subtítulo amber tenue
"Esc para cancelar · click [Confirmar] para borrar" para que el
usuario descubra el atajo sin RTFM.

35 tests verdes. El handler son 8 líneas no-testeables sin GPUI cx;
type-check garantiza wireup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

9951307fd949612651cd6a16ecfa55706f3f25f4 · ruwaq Sergio · (5 killa ñawpaq) · tayta bcccf3b95 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 2

crates/apps/nakui-ui/src/main.rs

2 qillqakuna · +53 −2

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f313089..9e34033 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,34 @@ ratio/diff ver `git show <sha>`.
 
 ## 2026-05-09
 
+### feat(nakui-ui): atajo Esc para cancelar el modal de delete
+Cierra otro pendiente de UX. El banner de confirmación de delete
+ya tenía botones [Cancelar] / [Confirmar], pero la acción más
+natural para cancelar un dialog es Esc — y no la teníamos wireada.
+
+Cambios:
+- **`capture_key_down` en el root div** del `MetaUi::render`. Capture
+  phase (no bubble) para interceptar el Esc *antes* que cualquier
+  TextInput descendiente lo consuma. Sin pending el handler es
+  no-op y el evento sigue su flujo normal.
+- **Match `event.keystroke.key == "escape"`** + `pending_delete.take()`
+  → toast `"delete cancelado (Entity) [esc]"` (sufijo `[esc]` para
+  diferenciar visualmente del botón). Si no hay pending, return
+  temprano sin tocar nada.
+- **Hint visual en el banner**: subtítulo en amber tenue debajo del
+  título: `"Esc para cancelar · click [Confirmar] para borrar"`.
+  Que el usuario descubra el atajo sin RTFM.
+
+35 tests verdes — el handler de Esc es 8 líneas no-testeables sin
+GPUI cx (la lógica de pending_delete + toast vive dentro del
+listener); el wireup compila por type-check.
+
+Pendientes restantes:
+- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
+- **Snapshot durante runtime** (cada N writes, no sólo al startup).
+- **Validación cross-field** (UUID del EntityRef existe en la
+  entity referida).
+
 ### feat(nakui-ui): EntityRef validation en parse_field_value (UUID al submit)
 Cierra otro pendiente: `parse_field_value(FieldKind::EntityRef, raw)`
 devolvía `Ok(json!(raw))` blindly — el value entraba al log/store
diff --git a/crates/apps/nakui-ui/src/main.rs b/crates/apps/nakui-ui/src/main.rs
index c71dbea..fd7b56c 100644
--- a/crates/apps/nakui-ui/src/main.rs
+++ b/crates/apps/nakui-ui/src/main.rs
@@ -28,7 +28,7 @@ use std::sync::{Arc, Mutex};
 
 use gpui::{
     div, prelude::*, px, App, Application, Bounds, ClickEvent, Context, Entity, IntoElement,
-    Render, SharedString, Window, WindowBounds, WindowOptions,
+    KeyDownEvent, Render, SharedString, Window, WindowBounds, WindowOptions,
 };
 use nakui_core::delta::{FieldOp, FieldPath};
 use nakui_core::event_log::{
@@ -1037,6 +1037,21 @@ impl Render for MetaUi {
             .flex_col()
             .size_full()
             .bg(bg)
+            // Capture phase: el Esc llega al root ANTES que cualquier
+            // TextInput descendiente. Si hay un delete pendiente, lo
+            // cancelamos. Sin pending no hacemos nada (el evento sigue
+            // su flujo normal y el TextInput recibe el Esc bubble).
+            .capture_key_down(cx.listener(|this, event: &KeyDownEvent, _w, cx| {
+                if event.keystroke.key != "escape" {
+                    return;
+                }
+                if let Some((entity, _id)) = this.pending_delete.take() {
+                    this.toast = Some(SharedString::from(format!(
+                        "delete cancelado ({entity}) [esc]"
+                    )));
+                    cx.notify();
+                }
+            }))
             .when_some(error_banner, |d, b| d.child(b))
             .when_some(confirm_banner, |d, b| d.child(b))
             .child(
@@ -1089,7 +1104,15 @@ impl MetaUi {
                 .child(
                     div()
                         .flex_grow()
-                        .child(format!("¿Borrar {entity_owned} {id_short}?")),
+                        .flex()
+                        .flex_col()
+                        .child(format!("¿Borrar {entity_owned} {id_short}?"))
+                        .child(
+                            div()
+                                .text_size(px(10.))
+                                .text_color(gpui::rgb(0xc0a070))
+                                .child("Esc para cancelar · click [Confirmar] para borrar"),
+                        ),
                 )
                 .child(
                     div()