diff options
| author | Tyler <tyler@tylerhoang.xyz> | 2026-05-16 01:52:32 -0700 |
|---|---|---|
| committer | Tyler <tyler@tylerhoang.xyz> | 2026-05-16 01:52:32 -0700 |
| commit | 8e35de0d435fec5ac552783130ef04aee33159f4 (patch) | |
| tree | 3f0a60d81e623c9b2d1710857da40e75ff018e15 | |
| parent | bbceb4c6798d43f4b32e73f38fc4907e00733244 (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>
| -rw-r--r-- | app.py | 250 |
1 files changed, 116 insertions, 134 deletions
@@ -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: |
