aboutsummaryrefslogtreecommitdiff
path: root/app.py
diff options
context:
space:
mode:
authorTyler <tyler@tylerhoang.xyz>2026-05-16 01:52:32 -0700
committerTyler <tyler@tylerhoang.xyz>2026-05-16 01:52:32 -0700
commit8e35de0d435fec5ac552783130ef04aee33159f4 (patch)
tree3f0a60d81e623c9b2d1710857da40e75ff018e15 /app.py
parentbbceb4c6798d43f4b32e73f38fc4907e00733244 (diff)
Sidebar chrome: vertical nav, live clock, brand v1.2, drop snapshot
Replace horizontal st.tabs() with session-state-driven vertical nav buttons in the sidebar (Workspace section). Remove company snapshot entirely — ticker info is covered by the persistent TickerHeader. Add NYSE live clock between brand and search. Update brand sub-label to "v 1.2". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'app.py')
-rw-r--r--app.py250
1 files changed, 116 insertions, 134 deletions
diff --git a/app.py b/app.py
index f8acd3c..156d0bd 100644
--- a/app.py
+++ b/app.py
@@ -489,6 +489,51 @@ hr {
font-family: var(--font-sans); font-size: 10px;
color: var(--fg-4); display: block; margin-top: 2px;
}
+
+/* ── Sidebar nav buttons ─────────────────────────────────────────────────── */
+.psm-nav-section {
+ font-family: 'IBM Plex Mono', monospace;
+ font-size: 9px; text-transform: uppercase;
+ letter-spacing: 0.15em; color: #5E5849;
+ padding: 14px 16px 4px;
+}
+section[data-testid="stSidebar"] [data-testid="stBaseButton-secondary"] {
+ background: transparent !important;
+ border: none !important;
+ border-radius: 0 !important;
+ box-shadow: none !important;
+ color: #8E8676 !important;
+ font-family: 'IBM Plex Mono', monospace !important;
+ font-size: 12px !important;
+ font-weight: 400 !important;
+ text-align: left !important;
+ justify-content: flex-start !important;
+ padding: 7px 16px !important;
+ letter-spacing: 0.02em !important;
+ border-left: 2px solid transparent !important;
+}
+section[data-testid="stSidebar"] [data-testid="stBaseButton-secondary"]:hover {
+ background: rgba(194,170,122,0.06) !important;
+ color: #C7C0AE !important;
+}
+section[data-testid="stSidebar"] [data-testid="stBaseButton-primary"] {
+ background: rgba(194,170,122,0.08) !important;
+ border: none !important;
+ border-radius: 0 !important;
+ box-shadow: none !important;
+ border-left: 2px solid #C2AA7A !important;
+ color: #C2AA7A !important;
+ font-family: 'IBM Plex Mono', monospace !important;
+ font-size: 12px !important;
+ font-weight: 400 !important;
+ text-align: left !important;
+ justify-content: flex-start !important;
+ padding: 7px 16px !important;
+ letter-spacing: 0.02em !important;
+}
+section[data-testid="stSidebar"] [data-testid="stBaseButton-primary"]:hover {
+ background: rgba(194,170,122,0.12) !important;
+}
</style>
""", unsafe_allow_html=True)
@@ -550,6 +595,7 @@ from components.filings import render_filings
from components.news import render_news
from components.options import render_options
from services.data_service import get_company_info, search_tickers, get_latest_price
+import streamlit.components.v1 as components
if "ticker" not in st.session_state:
@@ -558,6 +604,9 @@ if "ticker" not in st.session_state:
if "watchlist" not in st.session_state:
st.session_state["watchlist"] = []
+if "active_tab" not in st.session_state:
+ st.session_state["active_tab"] = "overview"
+
# ── Sidebar ───────────────────────────────────────────────────────────────────
@@ -593,11 +642,48 @@ with st.sidebar:
font-family: 'IBM Plex Mono', 'SF Mono', monospace;
font-size: 10px; color: #5E5849;
letter-spacing: 0.12em; text-transform: uppercase;
- ">Research Terminal</span>
+ ">v 1.2</span>
</div>
</div>
""", unsafe_allow_html=True)
+ _clock_html = (
+ "<style>"
+ "* { margin:0; padding:0; box-sizing:border-box; }"
+ "body { background:transparent; overflow:hidden; }"
+ ".psm-clock { display:flex; align-items:center; gap:7px;"
+ " font-family:'IBM Plex Mono',monospace; font-size:11px; color:#8E8676;"
+ " padding:6px 16px; }"
+ ".dot { width:6px; height:6px; border-radius:50%; flex-shrink:0; }"
+ ".dot.open { background:#4F8C5E; box-shadow:0 0 4px #4F8C5E; }"
+ ".dot.closed { background:#5E5849; }"
+ "#clock-status { color:#8E8676; }"
+ "#clock-time { color:#5E5849; margin-left:2px; }"
+ "</style>"
+ "<div class='psm-clock'>"
+ " <span id='clock-dot' class='dot closed'></span>"
+ " <span id='clock-status'>NYSE · Closed</span>"
+ " <span id='clock-time'>--:--:-- ET</span>"
+ "</div>"
+ "<script>"
+ "function tick() {"
+ " var now = new Date();"
+ " var et = new Date(now.toLocaleString('en-US', { timeZone: 'America/New_York' }));"
+ " var hm = et.getHours() * 60 + et.getMinutes();"
+ " var day = et.getDay();"
+ " var isOpen = (day >= 1 && day <= 5) && hm >= 570 && hm < 960;"
+ " var ts = et.toLocaleTimeString('en-US', {"
+ " hour:'2-digit', minute:'2-digit', second:'2-digit', hour12:false"
+ " });"
+ " document.getElementById('clock-dot').className = 'dot ' + (isOpen ? 'open' : 'closed');"
+ " document.getElementById('clock-status').textContent = isOpen ? 'NYSE · Open' : 'NYSE · Closed';"
+ " document.getElementById('clock-time').textContent = ts + ' ET';"
+ "}"
+ "tick(); setInterval(tick, 1000);"
+ "</script>"
+ )
+ components.html(_clock_html, height=32, scrolling=False)
+
with st.form("ticker_search_form", clear_on_submit=False):
query = st.text_input(
"Ticker or company",
@@ -626,117 +712,27 @@ with st.sidebar:
ticker = st.session_state["ticker"]
- # Company snapshot
- if ticker:
- st.divider()
- info = get_company_info(ticker)
- if info:
- co_name = info.get("longName", ticker)
- price = get_latest_price(ticker)
- prev_close = info.get("previousClose") or info.get("regularMarketPreviousClose")
- ticker_html = escape_html(ticker)
- co_name_html = escape_html(co_name)
-
- # Ticker + name
- st.markdown(f"""
- <div style="padding: 6px 0 4px;">
- <div style="
- font-family: 'EB Garamond', Georgia, serif;
- font-style: italic;
- font-size: 2rem; color: #F2ECDC;
- line-height: 0.95; letter-spacing: -0.025em;
- margin-bottom: 4px;
- ">{ticker_html}</div>
- <div style="
- font-family: 'IBM Plex Sans', sans-serif;
- font-size: 11px; color: #8E8676;
- letter-spacing: 0.01em;
- ">{co_name_html}</div>
- </div>
- """, unsafe_allow_html=True)
-
- # Price + change
- if price is not None:
- if prev_close and prev_close > 0:
- chg = price - prev_close
- chg_pct = chg / prev_close * 100
- sign = "+" if chg >= 0 else ""
- px_color = "#4F8C5E" if chg >= 0 else "#B5494B"
- st.markdown(f"""
- <div style="padding: 4px 0 8px;">
- <span style="
- font-family: 'IBM Plex Mono', monospace;
- font-size: 1.375rem; color: #F2ECDC;
- font-weight: 500; font-variant-numeric: tabular-nums;
- ">${price:,.2f}</span>
- <span style="
- font-family: 'IBM Plex Mono', monospace;
- font-size: 0.6875rem; color: {px_color};
- margin-left: 6px; font-variant-numeric: tabular-nums;
- ">{sign}{chg_pct:.2f}%</span>
- </div>
- """, unsafe_allow_html=True)
- else:
- st.markdown(f"""
- <div style="padding: 4px 0 8px;">
- <span style="
- font-family: 'IBM Plex Mono', monospace;
- font-size: 1.375rem; color: #F2ECDC;
- font-weight: 500; font-variant-numeric: tabular-nums;
- ">${price:,.2f}</span>
- </div>
- """, unsafe_allow_html=True)
-
- # Company meta
- _EXCHANGE_NAMES = {
- "NYQ": "NYSE", "NMS": "NASDAQ", "NGM": "NASDAQ",
- "NCM": "NASDAQ", "ASE": "AMEX", "PCX": "NYSE Arca",
- "BTS": "BATS", "TSX": "TSX", "LSE": "LSE",
- }
- raw_exchange = info.get("exchange", "")
- exchange = _EXCHANGE_NAMES.get(raw_exchange, raw_exchange) or "—"
- sector = info.get("sector", "—")
- currency = info.get("currency", "USD")
- employees = info.get("fullTimeEmployees")
- emp_str = f"{employees:,}" if isinstance(employees, int) else "—"
-
- rows = [
- ("Exchange", escape_html(exchange)),
- ("Sector", escape_html(sector)),
- ("Currency", escape_html(currency)),
- ("Employees", escape_html(emp_str)),
- ]
- rows_html = "".join(f"""
- <div style="display:flex;justify-content:space-between;align-items:baseline;">
- <span style="font-family:'IBM Plex Sans',sans-serif;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.12em;color:#5E5849;">{k}</span>
- <span style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#C7C0AE;text-align:right;max-width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{v}</span>
- </div>
- """ for k, v in rows)
-
- st.markdown(f"""
- <div style="
- display:flex;flex-direction:column;gap:6px;
- padding:8px 0 4px;
- border-top:1px solid #232934;
- ">{rows_html}</div>
- """, unsafe_allow_html=True)
-
- website = validate_outbound_url(info.get("website", ""))
- if website:
- st.markdown(f"""
- <div style="padding:6px 0 0;">
- <a href="{escape_html(website)}" target="_blank" rel="noopener noreferrer" style="
- font-family:'IBM Plex Sans',sans-serif;
- font-size:11px;color:#C2AA7A;
- text-decoration:none;
- border-bottom:1px solid #8F7A50;
- padding-bottom:1px;
- ">Website ↗</a>
- </div>
- """, unsafe_allow_html=True)
-
- elif ticker:
- st.caption(f"Viewing: **{ticker}**")
+ # ── Workspace nav ─────────────────────────────────────────────────────
+ st.markdown("<div class='psm-nav-section'>Workspace</div>", unsafe_allow_html=True)
+ _nav = [
+ ("overview", "◎ Overview"),
+ ("financials", "▦ Financials"),
+ ("valuation", "◈ Valuation"),
+ ("options", "◇ Options"),
+ ("insiders", "○ Insiders"),
+ ("filings", "▤ Filings"),
+ ("news", "◉ News"),
+ ]
+ for _tab_id, _tab_label in _nav:
+ _is_active = st.session_state["active_tab"] == _tab_id
+ if st.button(
+ _tab_label,
+ key=f"nav_{_tab_id}",
+ use_container_width=True,
+ type="primary" if _is_active else "secondary",
+ ):
+ st.session_state["active_tab"] = _tab_id
+ st.rerun()
# ── Save / Remove watchlist toggle ────────────────────────────────────
if ticker:
@@ -897,53 +893,39 @@ st.markdown(
unsafe_allow_html=True,
)
-tab_overview, tab_financials, tab_valuation, tab_options, tab_insiders, tab_filings, tab_news = st.tabs([
- "Overview",
- "Financials",
- "Valuation",
- "Options",
- "Insiders",
- "Filings",
- "News",
-])
+_active = st.session_state["active_tab"]
-with tab_overview:
+if _active == "overview":
try:
render_overview(ticker)
except Exception as e:
st.error(f"Overview failed to load: {e}")
-
-with tab_financials:
+elif _active == "financials":
try:
render_financials(ticker)
except Exception as e:
st.error(f"Financials failed to load: {e}")
-
-with tab_valuation:
+elif _active == "valuation":
try:
render_valuation(ticker)
except Exception as e:
st.error(f"Valuation failed to load: {e}")
-
-with tab_options:
+elif _active == "options":
try:
render_options(ticker)
except Exception as e:
st.error(f"Options data failed to load: {e}")
-
-with tab_insiders:
+elif _active == "insiders":
try:
render_insiders(ticker)
except Exception as e:
st.error(f"Insider data failed to load: {e}")
-
-with tab_filings:
+elif _active == "filings":
try:
render_filings(ticker)
except Exception as e:
st.error(f"Filings failed to load: {e}")
-
-with tab_news:
+elif _active == "news":
try:
render_news(ticker)
except Exception as e: