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
+77
View File
@@ -1300,6 +1300,83 @@ impl Shell {
// Fase 7: encender/apagar módulos enteros desde un
// header con switch (vs. el toggle por-control de hoy).
}
PanelEvent::Action { module_id, key } => {
self.on_panel_action(module_id.clone(), key.clone(), cx);
}
}
}
/// Click sobre un `Control::Action` del panel. Por ahora maneja:
/// - planetary_return.save_as_free → captura la carta del
/// retorno actual (cuerpo + edad) como FreeChart con sufijo
/// `rs-{N}` / `lunar-{N}` / etc. según el cuerpo elegido.
///
/// Otros módulos overlay (progression, solar_arc, primary_directions)
/// son extensión natural — TODO.
fn on_panel_action(&mut self, module_id: String, key: String, cx: &mut Context<Self>) {
if module_id == "planetary_return" && key == "save_as_free" {
self.save_planetary_return_as_free(cx);
}
}
/// Computa la carta del retorno planetario actual (con cuerpo +
/// edad del módulo) y la inserta como FreeChart. El usuario
/// puede después "Guardar como…" para persistirla bajo un
/// contacto (típicamente el mismo del natal).
fn save_planetary_return_as_free(&mut self, cx: &mut Context<Self>) {
let Some(natal) = self.current_chart.as_ref() else {
eprintln!("[shell] save_planetary_return: sin carta activa");
return;
};
if natal.id == ChartId::default() {
eprintln!("[shell] save_planetary_return: la carta activa es libre, no natal");
return;
}
let cfg = self
.module_configs
.get("planetary_return")
.cloned()
.unwrap_or(serde_json::json!({}));
let body = cfg
.get("body")
.and_then(|v| v.as_str())
.unwrap_or("sun")
.to_string();
let age = self.module_age_or_current("planetary_return");
let shift_days = cfg
.get("shift_days")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as i64;
// Pedimos al engine la fecha exacta del retorno. La engine
// expone `compute_planetary_return_chart` que devuelve un
// `StoredBirthData` listo para reusar como carta natal.
match tahuantinsuyu_engine::compute_planetary_return_chart(
natal, &body, age, shift_days,
) {
Ok((birth, instant_label)) => {
let suffix = match body.as_str() {
"sun" => "rs",
"moon" => "lunar",
other => other,
};
let label = format!(
"{} {}-{:.0}a · {}",
natal.label, suffix, age, instant_label
);
let id = FreeChartId(format!("free-{}", self.next_free_id));
self.next_free_id += 1;
let mut chart = natal.clone();
chart.id = ChartId::default();
chart.label = label;
chart.birth_data = birth;
self.free_charts.insert(id.clone(), chart);
self.push_free_charts_to_tree(cx);
self.apply_selection(TreeSelection::FreeChart(id), cx);
}
Err(e) => {
eprintln!("[shell] compute_planetary_return_chart: {}", e);
}
}
}
}