Skip to content

Commit 732d5bf

Browse files
committed
Add webapp wrapper for the package
1 parent beacfda commit 732d5bf

21 files changed

Lines changed: 5648 additions & 0 deletions

web/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Stellium Webapp
2+
3+
## Code Structure
4+
5+
```text
6+
stellium/
7+
├── src/stellium/ # Existing library
8+
├── tests/
9+
├── examples/
10+
├── docs/
11+
├── web/ # 👈 New web app
12+
│ ├── main.py # Entry point
13+
│ ├── config.py # Colors, fonts, constants
14+
│ ├── state.py # Reactive state management
15+
│ ├── pages/
16+
│ │ ├── __init__.py
17+
│ │ ├── home.py # Landing/home page
18+
│ │ ├── natal.py # Natal chart builder
19+
│ │ ├── synastry.py # Synastry/comparison charts
20+
│ │ └── explore.py # Notable births browser
21+
│ ├── components/
22+
│ │ ├── __init__.py
23+
│ │ ├── header.py # Site header/nav
24+
│ │ ├── birth_input.py # Birth data form
25+
│ │ ├── chart_options.py # House systems, components, etc.
26+
│ │ ├── chart_display.py # SVG chart viewer
27+
│ │ └── code_preview.py # "View as Python" panel
28+
│ ├── static/
29+
│ │ └── .gitkeep
30+
│ └── requirements.txt
31+
├── pyproject.toml
32+
└── README.md
33+
```

web/components/birth_input.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""
2+
Stellium Web - Birth Input Component
3+
4+
Form for entering birth data with the astro-charts conversational style.
5+
Now uses the unified component with manual/notable toggle.
6+
"""
7+
8+
from components.birth_input_unified import create_unified_birth_input
9+
from state import ChartState
10+
11+
12+
def create_birth_input_form(state: ChartState, on_change=None):
13+
"""
14+
Create the birth data input form.
15+
16+
Args:
17+
state: ChartState instance to bind to
18+
on_change: Optional callback when any field changes
19+
"""
20+
create_unified_birth_input(
21+
state=state,
22+
on_change=on_change,
23+
label="BIRTH DETAILS",
24+
show_notable_toggle=True,
25+
)
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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

Comments
 (0)