|
| 1 | +""" |
| 2 | +Stellium Web - Unified Birth Input Component |
| 3 | +
|
| 4 | +A birth input that can toggle between: |
| 5 | +1. Manual entry (name, date, time, location) |
| 6 | +2. Notable selection (search famous charts) |
| 7 | +""" |
| 8 | + |
| 9 | +from components.location_input import create_location_input |
| 10 | +from components.notable_selector import create_notable_autocomplete |
| 11 | +from components.time_input import ( |
| 12 | + create_time_input, |
| 13 | + parse_24h_to_components, |
| 14 | + parse_time_to_24h, |
| 15 | +) |
| 16 | +from config import COLORS |
| 17 | +from nicegui import ui |
| 18 | +from state import ChartState |
| 19 | + |
| 20 | + |
| 21 | +def create_unified_birth_input( |
| 22 | + state: ChartState, |
| 23 | + on_change=None, |
| 24 | + on_notable_select=None, |
| 25 | + label: str = "BIRTH DETAILS", |
| 26 | + show_notable_toggle: bool = True, |
| 27 | +): |
| 28 | + """ |
| 29 | + Create a unified birth input with manual/notable toggle. |
| 30 | +
|
| 31 | + Args: |
| 32 | + state: ChartState instance to bind to |
| 33 | + on_change: Callback when any field changes |
| 34 | + on_notable_select: Callback when a notable is selected (receives Notable object) |
| 35 | + label: Section label |
| 36 | + show_notable_toggle: Whether to show the toggle (disable for explore page) |
| 37 | +
|
| 38 | + Returns: |
| 39 | + Reference to the container element. |
| 40 | + """ |
| 41 | + |
| 42 | + # Local state for input mode |
| 43 | + mode_state = {"mode": "manual"} # "manual" or "notable" |
| 44 | + |
| 45 | + # UI references for refreshing |
| 46 | + refs = { |
| 47 | + "form_container": None, |
| 48 | + } |
| 49 | + |
| 50 | + def update_field(field: str, value): |
| 51 | + setattr(state, field, value) |
| 52 | + if on_change: |
| 53 | + on_change() |
| 54 | + |
| 55 | + def on_time_change(hour: str, minute: str, period: str): |
| 56 | + """Convert time components to 24h format and update state.""" |
| 57 | + time_24h = parse_time_to_24h(hour, minute, period) |
| 58 | + update_field("time", time_24h) |
| 59 | + |
| 60 | + def on_location_change(value: str): |
| 61 | + """Update location in state.""" |
| 62 | + update_field("location", value) |
| 63 | + |
| 64 | + def on_notable_selected(notable): |
| 65 | + """Handle notable selection - fill in all fields from notable.""" |
| 66 | + # Fill state from notable |
| 67 | + state.name = notable.name |
| 68 | + state.location = notable.location.name |
| 69 | + |
| 70 | + # Format date |
| 71 | + dt = notable.datetime.local_datetime |
| 72 | + state.date = dt.strftime("%Y-%m-%d") |
| 73 | + state.time = dt.strftime("%H:%M") |
| 74 | + state.time_unknown = False |
| 75 | + |
| 76 | + if on_change: |
| 77 | + on_change() |
| 78 | + |
| 79 | + if on_notable_select: |
| 80 | + on_notable_select(notable) |
| 81 | + |
| 82 | + # Refresh the form to show filled values |
| 83 | + refresh_form() |
| 84 | + |
| 85 | + ui.notify(f"Loaded: {notable.name}", type="positive") |
| 86 | + |
| 87 | + def on_mode_change(e): |
| 88 | + """Handle mode toggle change.""" |
| 89 | + mode_state["mode"] = e.value |
| 90 | + refresh_form() |
| 91 | + |
| 92 | + def refresh_form(): |
| 93 | + """Refresh the form based on current mode.""" |
| 94 | + if refs["form_container"]: |
| 95 | + refs["form_container"].clear() |
| 96 | + with refs["form_container"]: |
| 97 | + create_form_content() |
| 98 | + |
| 99 | + def create_form_content(): |
| 100 | + """Create the form content based on mode.""" |
| 101 | + # Parse existing time to components |
| 102 | + hour, minute, period = parse_24h_to_components(state.time) |
| 103 | + |
| 104 | + if mode_state["mode"] == "notable": |
| 105 | + # Notable search mode |
| 106 | + with ui.column().classes("gap-4 w-full"): |
| 107 | + ui.label("Search famous charts:").classes("text-sm").style( |
| 108 | + f"color: {COLORS['text_muted']}" |
| 109 | + ) |
| 110 | + create_notable_autocomplete( |
| 111 | + on_select=on_notable_selected, |
| 112 | + placeholder="Type a name...", |
| 113 | + ) |
| 114 | + |
| 115 | + # Show current selection if any |
| 116 | + if state.name: |
| 117 | + with ( |
| 118 | + ui.element("div") |
| 119 | + .classes("p-3 rounded mt-2") |
| 120 | + .style(f"background-color: {COLORS['cream']}; border: 1px solid {COLORS['border']};") |
| 121 | + ): |
| 122 | + ui.label("Selected:").classes("text-xs").style( |
| 123 | + f"color: {COLORS['text_muted']}" |
| 124 | + ) |
| 125 | + ui.label(state.name).classes("font-medium").style( |
| 126 | + f"color: {COLORS['text']}" |
| 127 | + ) |
| 128 | + if state.date: |
| 129 | + ui.label(f"{state.date} {state.time}").classes("text-sm").style( |
| 130 | + f"color: {COLORS['text_muted']}" |
| 131 | + ) |
| 132 | + if state.location: |
| 133 | + ui.label(state.location).classes("text-sm").style( |
| 134 | + f"color: {COLORS['accent']}" |
| 135 | + ) |
| 136 | + |
| 137 | + else: |
| 138 | + # Manual entry mode |
| 139 | + with ui.column().classes("gap-6 w-full"): |
| 140 | + # Name field |
| 141 | + with ui.row().classes("items-center gap-4 w-full"): |
| 142 | + ui.label("Name:").classes("w-28 flex-shrink-0 text-base").style( |
| 143 | + f"color: {COLORS['text']}" |
| 144 | + ) |
| 145 | + ui.input( |
| 146 | + value=state.name, |
| 147 | + placeholder="(optional)", |
| 148 | + on_change=lambda e: update_field("name", e.value), |
| 149 | + ).classes("minimal-input flex-grow").props("borderless dense") |
| 150 | + |
| 151 | + # Location field with autocomplete |
| 152 | + with ui.row().classes("items-center gap-4 w-full"): |
| 153 | + ui.label("Birth place:").classes("w-28 flex-shrink-0 text-base").style( |
| 154 | + f"color: {COLORS['text']}" |
| 155 | + ) |
| 156 | + with ui.element("div").classes("flex-grow"): |
| 157 | + create_location_input( |
| 158 | + value=state.location, |
| 159 | + placeholder="City, State, Country", |
| 160 | + on_change=on_location_change, |
| 161 | + ) |
| 162 | + |
| 163 | + # Date field |
| 164 | + with ui.row().classes("items-center gap-4 w-full"): |
| 165 | + ui.label("Birth date:").classes("w-28 flex-shrink-0 text-base").style( |
| 166 | + f"color: {COLORS['text']}" |
| 167 | + ) |
| 168 | + ui.input( |
| 169 | + value=state.date, |
| 170 | + placeholder="YYYY-MM-DD", |
| 171 | + on_change=lambda e: update_field("date", e.value), |
| 172 | + ).classes("minimal-input flex-grow").props("borderless dense") |
| 173 | + |
| 174 | + # Time field with hour/minute/AM-PM |
| 175 | + with ui.row().classes("items-center gap-4 w-full"): |
| 176 | + ui.label("Birth time:").classes("w-28 flex-shrink-0 text-base").style( |
| 177 | + f"color: {COLORS['text']}" |
| 178 | + ) |
| 179 | + with ui.element("div").classes("flex-grow"): |
| 180 | + create_time_input( |
| 181 | + hour_value=hour, |
| 182 | + minute_value=minute, |
| 183 | + period_value=period, |
| 184 | + on_change=on_time_change, |
| 185 | + disabled=state.time_unknown, |
| 186 | + ) |
| 187 | + |
| 188 | + # Unknown time checkbox |
| 189 | + ui.checkbox( |
| 190 | + "I don't know my birth time", |
| 191 | + value=state.time_unknown, |
| 192 | + on_change=lambda e: update_field("time_unknown", e.value), |
| 193 | + ).classes("mt-2").style(f"color: {COLORS['text_muted']}") |
| 194 | + |
| 195 | + # Build the UI |
| 196 | + with ui.element("div").classes("w-full") as container: |
| 197 | + # Section label with optional toggle |
| 198 | + with ui.row().classes("items-center justify-between w-full mb-4"): |
| 199 | + ui.label(f"☆ {label}").classes( |
| 200 | + "font-display text-xs tracking-[0.2em]" |
| 201 | + ).style(f"color: {COLORS['primary']}") |
| 202 | + |
| 203 | + if show_notable_toggle: |
| 204 | + ui.toggle( |
| 205 | + {"manual": "Enter", "notable": "Famous"}, |
| 206 | + value=mode_state["mode"], |
| 207 | + on_change=on_mode_change, |
| 208 | + ).props("dense no-caps") |
| 209 | + |
| 210 | + # Form container |
| 211 | + refs["form_container"] = ui.element("div").classes("w-full") |
| 212 | + with refs["form_container"]: |
| 213 | + create_form_content() |
| 214 | + |
| 215 | + return container |
0 commit comments