feat(tahuantinsuyu): fase 20 — accordion + lunar shift + CompositeModule + 90 ciudades
Cuatro features que cierran el set inicial de funcionalidades de
fase 1:
## D — Acordeón colapsable en el panel
Cuando hay 8 módulos en el panel se llenaba de cards. Ahora cada card
es expandible/colapsable por click en el header. Defaults:
- Natal siempre expanded
- Módulos con toggle "enabled" = true → expanded
- Resto → collapsed
El usuario puede forzar cualquiera vía override (collapse_overrides
HashMap). Chevron ▾/▸ a la izquierda del header. Hover sobre el
header lo resalta para invitar al click.
## B — Lunar return shift (navegación mensual)
PipelineRequest::PlanetaryReturn gana campo `shift_days: i64` (range
±180). El bridge lo suma a after_seconds del search anchor antes de
next_return. Para Solar return típicamente 0 (mantiene comportamiento).
Para Moon return, mover el slider ±28 días salta al retorno lunar
anterior o siguiente, permitiendo navegar mes a mes la lunación que
le toca al sujeto cumplido N años. PlanetaryReturnModule.controls()
agrega un slider "Shift días (lunar nav)". El badge del overlay
muestra "Moon return 38a +14d" cuando shift_days != 0. Helper
`planetary_return_request(body, age)` para callers que no necesitan
shift (zero default).
## C — CompositeModule
Carta compuesta (midpoint Davison) entre la natal del sujeto y otra
carta partner. Cada placement compuesto es el angular midpoint entre
los dos correspondientes. Engine: `PipelineRequest::Composite {
partner_chart: Box<Chart> }` + build_composite_overlay que llama
`eternal_astrology::composite()`. Renderiza placements en
`radii.composite = r * 0.32` (entre solar_arc 0.40 y aspects 0.24,
re-balanced). Módulo `composite::CompositeModule` con toggle +
ChartPicker (mismo patrón que synastry).
Shell: resolve_composite_partner reusa el fallback al primer hermano
del contacto, igual que synastry.
## A — 90 ciudades expandidas + dropdown scrollable
CITY_PRESETS pasa de 25 a 90 ciudades cubriendo:
- Latinoamérica (35): todas las capitales + grandes ciudades de AR/
VE/CO/PE/CL/EC/UY/PY/BO/MX/CU/PR/CR/PA/SV/GT/HN/NI/DO/BR
- España (5) + Europa (20): Madrid/Barcelona/Sevilla/Valencia/Bilbao
+ London/Paris/Berlin/München/Roma/Milano/Amsterdam/Bruxelles/Wien/
Zürich/Lisboa/Dublin/Stockholm/Oslo/København/Helsinki/Warszawa/
Praha/Budapest/Athina/İstanbul/Moskva
- USA + Canadá (12): NY/LA/Chicago/Miami/Houston/SF/Seattle/Boston/
DC + Toronto/Montreal/Vancouver
- Asia (16): Tokyo/Beijing/Shanghai/HK/Singapore/Seoul/Bangkok/
Jakarta/Manila/Mumbai/Delhi/Bangalore/Karachi/Tehran/Dubai/Tel Aviv
- África (6): Cairo/Lagos/Nairobi/Johannesburg/Cape Town/Casablanca
- Oceanía (3): Sydney/Melbourne/Auckland
El popup del dropdown ahora es scrollable (h=360px, overflow_y_scroll)
con id estable para no perder scroll position entre re-renders.
cargo check verde, 8 tests engine + 1 test modules (8 módulos
aplicables a ChartKind::Natal) verdes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -256,6 +256,13 @@ impl Shell {
|
||||
if module_enabled(&self.module_configs, "midpoints") {
|
||||
requests.push(PipelineRequest::Midpoints);
|
||||
}
|
||||
if module_enabled(&self.module_configs, "composite") {
|
||||
if let Some(partner) = self.resolve_composite_partner() {
|
||||
requests.push(PipelineRequest::Composite {
|
||||
partner_chart: Box::new(partner),
|
||||
});
|
||||
}
|
||||
}
|
||||
if module_enabled(&self.module_configs, "planetary_return") {
|
||||
let age = self.module_age_or_current("planetary_return");
|
||||
let body = self
|
||||
@@ -265,9 +272,17 @@ impl Shell {
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("sun")
|
||||
.to_string();
|
||||
let shift_days = self
|
||||
.module_configs
|
||||
.get("planetary_return")
|
||||
.and_then(|c| c.get("shift_days"))
|
||||
.and_then(|v| v.as_f64())
|
||||
.map(|v| v as i64)
|
||||
.unwrap_or(0);
|
||||
requests.push(PipelineRequest::PlanetaryReturn {
|
||||
body,
|
||||
target_age_years: age,
|
||||
shift_days,
|
||||
});
|
||||
}
|
||||
requests
|
||||
@@ -294,6 +309,20 @@ impl Shell {
|
||||
siblings.into_iter().find(|c| c.id != current.id)
|
||||
}
|
||||
|
||||
/// Resuelve el partner para Composite — mismo patrón que Synastry:
|
||||
/// 1) lee module_configs["composite"]["partner_chart_id"] y resuelve
|
||||
/// el chart; 2) fallback al primer hermano del contacto actual.
|
||||
fn resolve_composite_partner(&self) -> Option<Chart> {
|
||||
let manual = self
|
||||
.module_configs
|
||||
.get("composite")
|
||||
.and_then(|c| c.get("partner_chart_id"))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<tahuantinsuyu_model::ChartId>().ok())
|
||||
.and_then(|id| self.store.get_chart(id).ok());
|
||||
manual.or_else(|| self.find_synastry_partner_auto())
|
||||
}
|
||||
|
||||
/// Deriva las `NatalOptions` activas a partir del `module_configs["natal"]`.
|
||||
/// Si la entry no existe, devuelve defaults (majors=true, minors=false,
|
||||
/// multiplier=1.0).
|
||||
|
||||
Reference in New Issue
Block a user