feat(tahuantinsuyu): editor inline para cartas libres (F2)
Las cartas libres ahora se pueden editar en su totalidad (fecha,
hora, lugar, lat/lon/alt, TZ, label) desde el menú contextual.
La edición es **in-memory** — la carta se queda como libre tras
el cambio; para persistirla hay que usar "Guardar como…".
Tree:
- Nuevo `Modal::EditFreeChart { source_id, form, error }`
paralelo al `Modal::EditChart` existente. Reusa la misma
`ChartForm` (11 TextInputs: name + place + date + time + TZ
+ lat/lon/alt) y la misma función `render_chart_form` para
pintarlo. El title cambia a "Editar carta libre".
- `open_edit_free_chart_modal(source_id, w, cx)`: lee el entry
de `self.free_charts` (que ahora trae `birth_data` además
de id+label), pre-puebla el form, y abre el modal.
- Submit: `build_chart_from_form` parsea + valida; al éxito
emite nuevo evento `TreeEvent::FreeChartEditConfirmed
{ source_id, birth_data, label }`. Al error, conserva el
modal con la pill destructiva.
- City picker funciona como antes — el branch de
`apply_city_preset` se extendió para que reconozca
`Modal::EditFreeChart` además de Create/Edit.
Modelo:
- `FreeChartEntry` ahora incluye `birth_data: StoredBirthData`
además de id+label. El shell se lo pasa al setter; el tree
lo usa para pre-poblar el form sin tener que pedirlo al
shell.
Shell:
- `push_free_charts_to_tree` clona `birth_data` en cada entry.
- Handler `FreeChartEditConfirmed`: actualiza
`free_charts[id]` con los nuevos datos + label, re-publica
al tree, y si la carta editada era la activa, re-renderea
el wheel.
Menú contextual de "Cartas libres" / `<carta libre>` ahora:
- Editar datos…
- Guardar como…
- Borrar (no se ofrece sobre sky-now)
10 tests verdes (sin afectar lo testeado).
Próximo y último: F4 — botón "Guardar como…" en cada módulo
overlay (RS, prog, sa, gr) que captura la carta derivada con
un sufijo automático en el contacto original.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dd836522ab861a796d8ac54b01de05d2ef52ff27 · por sergio · 2026-05-18T21:51:51Z (hace 5 meses) · padre a83b0396c · ver el árbol en este commit
Delta semántico · 10
crates/apps/tahuantinsuyu/src/shell.rs
crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs
diff --git a/crates/apps/tahuantinsuyu/src/shell.rs b/crates/apps/tahuantinsuyu/src/shell.rs
index c803645..0cd033e 100644
--- a/crates/apps/tahuantinsuyu/src/shell.rs
+++ b/crates/apps/tahuantinsuyu/src/shell.rs
@@ -249,6 +249,7 @@ impl Shell {
entries.push(FreeChartEntry {
id: FreeChartId::sky_now(),
label: c.label.clone(),
+ birth_data: c.birth_data.clone(),
});
}
let mut others: Vec<(&FreeChartId, &Chart)> = self
@@ -261,6 +262,7 @@ impl Shell {
entries.push(FreeChartEntry {
id: id.clone(),
label: c.label.clone(),
+ birth_data: c.birth_data.clone(),
});
}
self.tree
@@ -502,6 +504,32 @@ impl Shell {
);
return;
}
+ TreeEvent::FreeChartEditConfirmed {
+ source_id,
+ birth_data,
+ label,
+ } => {
+ if let Some(chart) = self.free_charts.get_mut(source_id) {
+ chart.birth_data = birth_data.clone();
+ chart.label = label.clone();
+ }
+ self.push_free_charts_to_tree(cx);
+ // Si la carta editada era la activa, re-render.
+ if let Some(current) = self.current_chart.as_mut() {
+ // Heurística: comparamos por label (ya cambiado al
+ // que pidió el usuario). Si el label de la activa
+ // coincide, era esta carta.
+ if current.label == label.clone()
+ || current.birth_data.subject_name.as_deref() == Some("Cielo")
+ {
+ if let Some(updated) = self.free_charts.get(source_id) {
+ *current = updated.clone();
+ self.render_current(cx);
+ }
+ }
+ }
+ return;
+ }
TreeEvent::DeleteFreeChartRequested(id) => {
if id.is_sky_now() {
return; // no se borra el Cielo
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs
index 821b8b6..086d03d 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs
@@ -79,6 +79,13 @@ pub enum TreeEvent {
contact: Option<ContactId>,
new_contact_name: Option<String>,
},
+ /// Submit del modal "Editar datos" para una carta libre. El
+ /// shell aplica al mapa `free_charts` y re-renderea el wheel.
+ FreeChartEditConfirmed {
+ source_id: FreeChartId,
+ birth_data: StoredBirthData,
+ label: String,
+ },
}
// =====================================================================
@@ -157,6 +164,15 @@ enum Modal {
form: ChartForm,
error: Option<SharedString>,
},
+ /// Editar los datos (fecha/hora/lugar) de una carta libre.
+ /// Reusa el mismo `ChartForm` que `EditChart`. El submit emite
+ /// `FreeChartEditConfirmed` que el shell aplica al mapa
+ /// `free_charts` y re-renderea el wheel.
+ EditFreeChart {
+ source_id: FreeChartId,
+ form: ChartForm,
+ error: Option<SharedString>,
+ },
/// Guardar una carta libre como `Chart` persistido. El usuario
/// elige nombre + contacto destino (existente de la lista o
/// uno nuevo creado al vuelo). El shell escucha
@@ -227,13 +243,15 @@ pub struct TahuantinsuyuTree {
free_charts: Vec<FreeChartEntry>,
}
-/// Entrada de la sección "Cartas libres" — id + label visible. La
-/// estructura del Chart en sí vive en el shell (`free_charts` de
-/// `Shell`), no en el tree.
+/// Entrada de la sección "Cartas libres" — id + label visible +
+/// birth_data clonado (para pre-poblar el modal "Editar datos…").
+/// El Chart completo vive en el shell; el tree mantiene esta
+/// proyección compacta para no depender del shell en cada operación.
#[derive(Clone, Debug)]
pub struct FreeChartEntry {
pub id: FreeChartId,
pub label: String,
+ pub birth_data: StoredBirthData,
}
/// Preset de ciudad con datos canónicos para autocompletar lat/lon/tz
@@ -777,6 +795,7 @@ impl TahuantinsuyuTree {
let form = match self.modal.as_mut() {
Some(Modal::CreateChart { form, .. }) => form,
Some(Modal::EditChart { form, .. }) => form,
+ Some(Modal::EditFreeChart { form, .. }) => form,
_ => {
self.city_picker_open = false;
cx.notify();
@@ -878,6 +897,58 @@ impl TahuantinsuyuTree {
/// contactos es un snapshot recursivo de toda la jerarquía
/// (no solo el nivel raíz). El usuario elige uno existente o
/// deja en "Nuevo contacto" para que se cree uno al confirmar.
+ /// Abre el modal "Editar datos" para una carta libre. Pre-puebla
+ /// `ChartForm` con `birth_data` actual de la entry. Submit emite
+ /// `FreeChartEditConfirmed` que el shell aplica al mapa de
+ /// `free_charts` y re-renderea.
+ fn open_edit_free_chart_modal(
+ &mut self,
+ source_id: FreeChartId,
+ window: &mut Window,
+ cx: &mut Context<'_, Self>,
+ ) {
+ let entry = match self.free_charts.iter().find(|e| e.id == source_id) {
+ Some(e) => e.clone(),
+ None => return,
+ };
+ let bd = &entry.birth_data;
+ let form = ChartForm {
+ name: self.make_input("Etiqueta de la carta", &entry.label, window, cx),
+ place: self.make_input(
+ "Lugar (ciudad, país)",
+ bd.birthplace_label.as_deref().unwrap_or(""),
+ window,
+ cx,
+ ),
+ year: self.make_input("Año", &bd.year.to_string(), window, cx),
+ month: self.make_input("Mes", &bd.month.to_string(), window, cx),
+ day: self.make_input("Día", &bd.day.to_string(), window, cx),
+ hour: self.make_input("Hora (0-23)", &bd.hour.to_string(), window, cx),
+ minute: self.make_input("Minuto", &bd.minute.to_string(), window, cx),
+ tz_offset_min: self.make_input(
+ "TZ offset (min)",
+ &bd.tz_offset_minutes.to_string(),
+ window,
+ cx,
+ ),
+ lat: self.make_input("Latitud (°)", &format!("{}", bd.latitude_deg), window, cx),
+ lon: self.make_input(
+ "Longitud (°)",
+ &format!("{}", bd.longitude_deg),
+ window,
+ cx,
+ ),
+ alt: self.make_input("Altitud (m)", &format!("{}", bd.altitude_m), window, cx),
+ };
+ form.name.update(cx, |i, _| i.request_focus(window));
+ self.modal = Some(Modal::EditFreeChart {
+ source_id,
+ form,
+ error: None,
+ });
+ self.close_menu(cx);
+ }
+
/// Cambia `selected_contact` del modal `SaveFreeChart` activo
/// sin recrear los inputs. Permite alternar entre los botones
/// radio "contacto existente" y "Nuevo contacto…".
@@ -1230,6 +1301,32 @@ impl TahuantinsuyuTree {
}
}
}
+ Modal::EditFreeChart {
+ source_id,
+ form,
+ error: _,
+ } => {
+ let _ = value;
+ match build_chart_from_form(&form, cx) {
+ Ok((birth, label)) => {
+ cx.emit(TreeEvent::FreeChartEditConfirmed {
+ source_id,
+ birth_data: birth,
+ label,
+ });
+ self.modal = None;
+ cx.notify();
+ }
+ Err(msg) => {
+ self.modal = Some(Modal::EditFreeChart {
+ source_id,
+ form,
+ error: Some(SharedString::from(msg)),
+ });
+ cx.notify();
+ }
+ }
+ }
Modal::SaveFreeChart {
source_id,
name,
@@ -1617,6 +1714,14 @@ impl TahuantinsuyuTree {
}
MenuTarget::FreeChart(fid) => {
let is_sky = fid.is_sky_now();
+ let fid_edit = fid.clone();
+ items = items.child(
+ menu_item("tts-menu-edit-free", "Editar datos…", theme).on_click(
+ cx.listener(move |this, _: &ClickEvent, w, cx| {
+ this.open_edit_free_chart_modal(fid_edit.clone(), w, cx);
+ }),
+ ),
+ );
let fid_save = fid.clone();
items = items.child(
menu_item("tts-menu-save-free", "Guardar como…", theme).on_click(
@@ -1708,6 +1813,15 @@ impl TahuantinsuyuTree {
&self.city_atlas,
cx,
),
+ Modal::EditFreeChart { form, error, .. } => render_chart_form(
+ theme,
+ "Editar carta libre",
+ form,
+ error.clone(),
+ self.city_picker_open,
+ &self.city_atlas,
+ cx,
+ ),
Modal::SaveFreeChart {
source_id,
name,