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:
@@ -90,6 +90,11 @@ pub struct ControlPanel {
|
||||
/// Si hay un dropdown abierto, su (module_id, key). Mutuamente
|
||||
/// excluyente: solo uno abierto a la vez en todo el panel.
|
||||
dropdown_open: Option<(String, String)>,
|
||||
/// Overrides explícitos del estado expanded/collapsed por módulo.
|
||||
/// La semántica del default (sin override) está en
|
||||
/// [`Self::is_collapsed`]: natal y módulos enabled = expanded;
|
||||
/// el resto collapsed.
|
||||
collapse_overrides: HashMap<String, bool>,
|
||||
registry: Registry,
|
||||
}
|
||||
|
||||
@@ -106,10 +111,35 @@ impl ControlPanel {
|
||||
chart_options: Vec::new(),
|
||||
string_state: HashMap::new(),
|
||||
dropdown_open: None,
|
||||
collapse_overrides: HashMap::new(),
|
||||
registry: Registry::with_builtins(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decide si el card de un módulo debe pintarse collapsed (solo
|
||||
/// header) o expanded (header + controles). La regla: si el usuario
|
||||
/// puso un override explícito lo respetamos; sino, natal va
|
||||
/// expanded siempre y el resto solo si su toggle "enabled" es true.
|
||||
fn is_collapsed(&self, module_id: &str) -> bool {
|
||||
if let Some(v) = self.collapse_overrides.get(module_id) {
|
||||
return *v;
|
||||
}
|
||||
if module_id == "natal" {
|
||||
return false;
|
||||
}
|
||||
!self
|
||||
.toggle_state
|
||||
.get(&(module_id.to_string(), "enabled".to_string()))
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn toggle_collapsed(&mut self, module_id: String, cx: &mut Context<Self>) {
|
||||
let current = self.is_collapsed(&module_id);
|
||||
self.collapse_overrides.insert(module_id, !current);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_active_kind(&mut self, kind: Option<ChartKind>, cx: &mut Context<Self>) {
|
||||
if self.active_kind != kind {
|
||||
if let Some(k) = kind {
|
||||
@@ -410,29 +440,51 @@ impl ControlPanel {
|
||||
controls: &[Control],
|
||||
cx: &mut Context<Self>,
|
||||
) -> gpui::Div {
|
||||
let collapsed = self.is_collapsed(module_id);
|
||||
let chevron = if collapsed { "▸" } else { "▾" };
|
||||
let header_id: SharedString =
|
||||
SharedString::from(format!("tts-module-header-{}", module_id));
|
||||
let module_id_for_listener = module_id.to_string();
|
||||
let header = div()
|
||||
.id(gpui::ElementId::from(header_id))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(2.0))
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap(px(8.0))
|
||||
.hover(|s| s.bg(theme.bg_row_hover))
|
||||
.rounded(px(4.0))
|
||||
.px(px(4.0))
|
||||
.py(px(2.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(12.0))
|
||||
.text_color(theme.fg_text)
|
||||
.child(SharedString::from(label.to_string())),
|
||||
.text_size(px(11.0))
|
||||
.text_color(theme.fg_muted)
|
||||
.child(chevron),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(10.0))
|
||||
.text_color(theme.fg_muted)
|
||||
.child(SharedString::from(description.to_string())),
|
||||
);
|
||||
.flex()
|
||||
.flex_col()
|
||||
.flex_grow()
|
||||
.gap(px(2.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(12.0))
|
||||
.text_color(theme.fg_text)
|
||||
.child(SharedString::from(label.to_string())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(10.0))
|
||||
.text_color(theme.fg_muted)
|
||||
.child(SharedString::from(description.to_string())),
|
||||
),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
|
||||
this.toggle_collapsed(module_id_for_listener.clone(), cx);
|
||||
}));
|
||||
|
||||
let mut body = div().flex().flex_col().gap(px(4.0));
|
||||
for c in controls {
|
||||
body = body.child(self.render_control(theme, module_id, c, cx));
|
||||
}
|
||||
|
||||
div()
|
||||
let mut card = div()
|
||||
.min_w(px(260.0))
|
||||
.p(px(8.0))
|
||||
.rounded(px(6.0))
|
||||
@@ -442,8 +494,15 @@ impl ControlPanel {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(6.0))
|
||||
.child(header)
|
||||
.child(body)
|
||||
.child(header);
|
||||
if !collapsed {
|
||||
let mut body = div().flex().flex_col().gap(px(4.0));
|
||||
for c in controls {
|
||||
body = body.child(self.render_control(theme, module_id, c, cx));
|
||||
}
|
||||
card = card.child(body);
|
||||
}
|
||||
card
|
||||
}
|
||||
|
||||
fn render_control(
|
||||
|
||||
Reference in New Issue
Block a user