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 `. ## 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()