feat(tahuantinsuyu): "Guardar como…" en módulo Retorno planetario (F4)

Cierra la fase B con el botón pedido por el usuario: tener una
carta natal abierta, activar el módulo Retorno planetario con
edad N + cuerpo (ej. Sol, 34 años), y al click guardar la carta
resultante con sufijo automático `rs-34` en el mismo contacto.

Infraestructura nueva (extensible a otros overlays):
- `Control::Action { key, label }` en tahuantinsuyu-modules —
  un botón sin estado que el panel pinta como pill clickeable.
- `PanelEvent::Action { module_id, key }` que el panel emite
  al click y el shell despacha.
- `render_action` en tahuantinsuyu-panel: pill con bg_button
  + hover + border. Wrap en Div plano para tipo coherente.

Backend (eternal-bridge):
- Nueva función pública `compute_planetary_return_chart(chart,
  body, target_age_years, shift_days) -> (StoredBirthData,
  instant_label)` en `tahuantinsuyu-engine`. Reusa el cómputo
  ya existente del overlay: `next_return` + parser ISO-8601
  para extraer year/mm/dd/hh:mm:ss del instant del retorno.
  Hereda lat/lon/alt/TZ del natal — convención clásica del
  Solar return en la ciudad de nacimiento.

Flujo en el shell:
- Handler `on_panel_action` despacha por `(module_id, key)`. Hoy
  solo `planetary_return.save_as_free` está cableado; otros
  módulos overlay (progression, solar_arc, primary_directions,
  transit) son extensión natural — TODO.
- `save_planetary_return_as_free`:
  1) lee config (body, age, shift_days) del module_configs
  2) llama `compute_planetary_return_chart`
  3) construye un `Chart` clonando el natal con birth_data
     nuevo + label `{contacto} rs-34 · 2024-08-12 14:23 UTC`
     (sufijo según cuerpo: `rs` para Sol, `lunar` para Luna,
     nombre directo para los demás)
  4) inserta como FreeChart con id `free-{N}` y la
     selecciona para que el usuario la vea
- El usuario después puede usar el menú contextual de la
  free chart para "Guardar como…" → modal F3 → persiste
  bajo el contacto que elija (típicamente el del natal).

UX completa:
1. Tener natal abierta
2. Panel: módulo "Retorno planetario" → Activar + elegir
   cuerpo + slider edad
3. Click "💾 Guardar retorno como carta libre"
4. La nueva carta aparece en "Cartas libres" seleccionada
5. Click derecho → "Guardar como…" → elegir contacto +
   confirmar nombre

10 tests verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-19 00:01:49 +00:00
parent dd836522ab
commit 9db0591f28
5 changed files with 251 additions and 0 deletions
@@ -45,6 +45,13 @@ pub enum PanelEvent {
key: String,
value: serde_json::Value,
},
/// Click sobre un `Control::Action`. El shell decide qué hacer
/// (típicamente: capturar la carta derivada del overlay como
/// `FreeChart`).
Action {
module_id: String,
key: String,
},
}
/// Opción que el host inyecta al panel para que los `Control::ChartPicker`
@@ -544,9 +551,51 @@ impl ControlPanel {
default,
} => self.render_select(theme, module_id, key, label, options, default, cx),
Control::TextInput { label, default, .. } => display_row(theme, label, default),
Control::Action { key, label } => {
self.render_action(theme, module_id, key, label, cx)
}
}
}
fn render_action(
&self,
theme: &Theme,
module_id: &str,
key: &str,
label: &str,
cx: &mut Context<'_, Self>,
) -> gpui::Div {
let id_str: SharedString =
SharedString::from(format!("tts-action-{}-{}", module_id, key));
let id_for_listener = (module_id.to_string(), key.to_string());
let btn = div()
.id(gpui::ElementId::from(id_str))
.flex()
.flex_row()
.items_center()
.justify_center()
.px(px(10.0))
.py(px(5.0))
.rounded(px(6.0))
.bg(theme.bg_button())
.hover(|s| s.bg(theme.bg_button_hover()))
.border_1()
.border_color(theme.border)
.text_size(px(11.0))
.text_color(theme.fg_text)
.child(SharedString::from(label.to_string()))
.on_click(cx.listener(move |_this, _: &ClickEvent, _, cx| {
let (m, k) = id_for_listener.clone();
cx.emit(PanelEvent::Action {
module_id: m,
key: k,
});
}));
// Wrap en Div plano para que el tipo coincida con el resto
// de los renderers (`render_control` espera `gpui::Div`).
div().child(btn)
}
fn render_toggle(
&self,
theme: &Theme,