diff options
| author | Tyler <tyler@tylerhoang.xyz> | 2026-05-13 22:39:50 -0700 |
|---|---|---|
| committer | Tyler <tyler@tylerhoang.xyz> | 2026-05-13 22:39:50 -0700 |
| commit | a457bea95358825e55dbc7f48d57183004121109 (patch) | |
| tree | eafafbfa017228bc1efd14a0c63f150576ace9f2 | |
| parent | 6f1c3a6b73572b3ccc5281dba45de3bba5528f5f (diff) | |
Apply Prism design system — brass/ink palette, EB Garamond + IBM Plex typography
Implements the design kit: champagne brass accent (#C2AA7A), deep midnight
backgrounds, EB Garamond italic headings, IBM Plex Sans/Mono body and numbers,
terminal-density KPI cards, restyled sidebar brand mark, flat pill tabs, and a
global Plotly theme so all charts inherit the dark palette automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | .streamlit/config.toml | 8 | ||||
| -rw-r--r-- | app.py | 658 | ||||
| -rw-r--r-- | components/filings.py | 35 | ||||
| -rw-r--r-- | components/insiders.py | 10 | ||||
| -rw-r--r-- | components/market_bar.py | 54 | ||||
| -rw-r--r-- | components/options.py | 4 | ||||
| -rw-r--r-- | components/overview.py | 57 | ||||
| -rw-r--r-- | components/top_movers.py | 42 | ||||
| -rw-r--r-- | components/valuation.py | 14 |
9 files changed, 691 insertions, 191 deletions
diff --git a/.streamlit/config.toml b/.streamlit/config.toml index e3fbcf9..16c0c8e 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -1,9 +1,9 @@ [theme] base = "dark" -primaryColor = "#4F8EF7" -backgroundColor = "#0E1117" -secondaryBackgroundColor = "#161C27" -textColor = "#FAFAFA" +primaryColor = "#C2AA7A" +backgroundColor = "#0B0E13" +secondaryBackgroundColor = "#11151C" +textColor = "#C7C0AE" font = "sans serif" [server] @@ -11,50 +11,414 @@ st.set_page_config( initial_sidebar_state="expanded", ) -# ── Global CSS ──────────────────────────────────────────────────────────────── +# ── Design system CSS ───────────────────────────────────────────────────────── st.markdown(""" <style> - /* Tighten metric cards */ - [data-testid="metric-container"] { - padding: 0.4rem 0.6rem !important; - } - [data-testid="stMetricLabel"] { - font-size: 0.72rem !important; - font-weight: 500; - color: #9aa0b0 !important; - letter-spacing: 0.03em; - text-transform: uppercase; - } - [data-testid="stMetricValue"] { - font-size: 1.1rem !important; - font-weight: 600; - letter-spacing: -0.01em; - } - [data-testid="stMetricDelta"] svg { width: 0.75rem; height: 0.75rem; } - [data-testid="stMetricDelta"] > div { font-size: 0.78rem !important; } +@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400;1,500;1,600&family=IBM+Plex+Mono:wght@300;400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap'); - /* Tighter headings */ - h1 { font-size: 1.5rem !important; font-weight: 700 !important; } - h2 { font-size: 1.25rem !important; font-weight: 600 !important; } - h3 { font-size: 1.05rem !important; font-weight: 600 !important; } +/* ── Tokens ─────────────────────────────────────────────────────────────── */ +:root { + --ink-0: #0B0E13; + --ink-1: #11151C; + --ink-2: #181D26; + --ink-3: #222934; + --ink-4: #2C3340; + --line-1: #232934; + --line-2: #2E3645; + --line-3: #3D4658; + --fg-1: #F2ECDC; + --fg-2: #C7C0AE; + --fg-3: #8E8676; + --fg-4: #5E5849; + --brass: #C2AA7A; + --brass-bright: #DCC79E; + --brass-deep: #8F7A50; + --brass-ink: #17120A; + --positive: #4F8C5E; + --positive-bg: #15241A; + --negative: #B5494B; + --negative-bg: #2A1517; + --warning: #C49545; + --font-display: 'EB Garamond', Georgia, serif; + --font-sans: 'IBM Plex Sans', 'Helvetica Neue', system-ui, sans-serif; + --font-mono: 'IBM Plex Mono', 'SF Mono', Menlo, monospace; +} - /* Tighter sidebar text */ - [data-testid="stSidebar"] .stCaption p { - font-size: 0.78rem !important; - line-height: 1.5 !important; - } +/* ── Base ───────────────────────────────────────────────────────────────── */ +html, body, [class*="css"] { + font-family: var(--font-sans) !important; + -webkit-font-smoothing: antialiased; +} - /* Slim dividers */ - hr { margin: 0.5rem 0 !important; } +.stApp { background: var(--ink-0) !important; } - /* Tab labels */ - [data-testid="stTab"] p { font-size: 0.85rem !important; font-weight: 500; } +/* ── Sidebar shell ──────────────────────────────────────────────────────── */ +[data-testid="stSidebar"] { + background: var(--ink-1) !important; + border-right: 1px solid var(--line-1) !important; +} - /* Top padding — enough to clear Streamlit's sticky toolbar */ - .block-container { padding-top: 3.5rem !important; } +[data-testid="stSidebar"] > div:first-child { + padding-top: 0 !important; +} + +[data-testid="stSidebarCollapseButton"] { opacity: 0.4; } + +/* ── Sidebar text ───────────────────────────────────────────────────────── */ +[data-testid="stSidebar"] p { + font-family: var(--font-sans) !important; + font-size: 0.8125rem !important; + color: var(--fg-3) !important; + line-height: 1.45 !important; +} + +[data-testid="stSidebar"] .stCaption p { + font-size: 0.75rem !important; + color: var(--fg-3) !important; +} + +[data-testid="stSidebar"] strong { color: var(--fg-1) !important; } + +[data-testid="stSidebar"] hr { + border: none !important; + border-top: 1px solid var(--line-1) !important; + margin: 0.375rem 0 !important; +} + +[data-testid="stSidebar"] h3 { + font-family: var(--font-display) !important; + font-size: 1.125rem !important; + color: var(--fg-1) !important; + font-weight: 500 !important; + letter-spacing: -0.01em !important; + margin: 0 !important; +} + +/* ── Sidebar inputs ─────────────────────────────────────────────────────── */ +[data-testid="stSidebar"] .stTextInput input { + background: var(--ink-2) !important; + border: 1px solid var(--line-2) !important; + border-radius: 2px !important; + font-family: var(--font-mono) !important; + font-size: 0.8125rem !important; + color: var(--fg-1) !important; +} + +[data-testid="stSidebar"] .stTextInput input::placeholder { color: var(--fg-4) !important; } + +[data-testid="stSidebar"] .stTextInput input:focus { + border-color: var(--brass-deep) !important; + box-shadow: none !important; +} + +[data-testid="stSidebar"] .stTextInput label { + font-family: var(--font-sans) !important; + font-size: 10px !important; + font-weight: 600 !important; + text-transform: uppercase !important; + letter-spacing: 0.14em !important; + color: var(--fg-4) !important; +} + +/* ── Selectbox ──────────────────────────────────────────────────────────── */ +[data-baseweb="select"] > div { + background: var(--ink-2) !important; + border: 1px solid var(--line-2) !important; + border-radius: 2px !important; + font-family: var(--font-mono) !important; + font-size: 0.8125rem !important; + color: var(--fg-1) !important; +} + +[data-baseweb="select"] [data-baseweb="menu"] { + background: var(--ink-2) !important; + border: 1px solid var(--line-2) !important; + border-radius: 2px !important; +} + +[data-baseweb="select"] li { + font-family: var(--font-mono) !important; + font-size: 0.8125rem !important; + color: var(--fg-2) !important; +} + +/* ── Buttons ────────────────────────────────────────────────────────────── */ +button[kind="primary"], +[data-testid="stFormSubmitButton"] button { + background: var(--brass) !important; + color: var(--brass-ink) !important; + border: none !important; + border-radius: 2px !important; + font-family: var(--font-sans) !important; + font-size: 0.75rem !important; + font-weight: 600 !important; + letter-spacing: 0.1em !important; + text-transform: uppercase !important; + transition: background 0.12s ease !important; +} + +button[kind="primary"]:hover, +[data-testid="stFormSubmitButton"] button:hover { + background: var(--brass-bright) !important; + border: none !important; +} + +button[kind="secondary"] { + background: var(--ink-3) !important; + color: var(--fg-2) !important; + border: 1px solid var(--line-2) !important; + border-radius: 2px !important; + font-family: var(--font-sans) !important; + font-size: 0.75rem !important; + font-weight: 500 !important; + letter-spacing: 0.06em !important; + text-transform: uppercase !important; +} + +button[kind="secondary"]:hover { + background: var(--ink-4) !important; + border-color: var(--line-3) !important; + color: var(--fg-1) !important; +} + +/* ── Main content area ──────────────────────────────────────────────────── */ +.block-container { + padding-top: 1.25rem !important; + padding-bottom: 3rem !important; +} + +/* ── Tabs ───────────────────────────────────────────────────────────────── */ +.stTabs [data-baseweb="tab-list"] { + background: var(--ink-2) !important; + border: 1px solid var(--line-2) !important; + border-radius: 2px !important; + padding: 2px !important; + gap: 2px !important; +} + +.stTabs [data-baseweb="tab"] { + background: transparent !important; + border-radius: 2px !important; + font-family: var(--font-mono) !important; + font-size: 11px !important; + color: var(--fg-4) !important; + padding: 4px 10px !important; + border: none !important; + transition: color 0.1s, background 0.1s !important; +} + +.stTabs [data-baseweb="tab"]:hover { color: var(--fg-2) !important; } + +.stTabs [aria-selected="true"] { + background: var(--ink-4) !important; + color: var(--fg-1) !important; +} + +.stTabs [data-baseweb="tab-border"], +.stTabs [data-baseweb="tab-highlight"] { display: none !important; } + +/* ── Metric cards ───────────────────────────────────────────────────────── */ +[data-testid="metric-container"] { + background: var(--ink-1) !important; + border: 1px solid var(--line-1) !important; + border-radius: 2px !important; + padding: 12px 16px !important; +} + +[data-testid="stMetricLabel"] p { + font-family: var(--font-sans) !important; + font-size: 10px !important; + font-weight: 600 !important; + text-transform: uppercase !important; + letter-spacing: 0.14em !important; + color: var(--fg-4) !important; +} + +[data-testid="stMetricValue"] { + font-family: var(--font-mono) !important; + font-size: 1.125rem !important; + color: var(--fg-1) !important; + font-weight: 500 !important; + font-variant-numeric: tabular-nums !important; + letter-spacing: -0.01em !important; +} + +[data-testid="stMetricDelta"] { + font-family: var(--font-mono) !important; + font-size: 0.75rem !important; + font-variant-numeric: tabular-nums !important; +} + +[data-testid="stMetricDelta"] svg { width: 0.7rem; height: 0.7rem; } + +/* ── Expanders ──────────────────────────────────────────────────────────── */ +[data-testid="stExpander"] { + background: var(--ink-1) !important; + border: 1px solid var(--line-1) !important; + border-radius: 2px !important; +} + +[data-testid="stExpander"] summary { + font-family: var(--font-display) !important; + font-size: 1.0625rem !important; + color: var(--fg-1) !important; + font-weight: 500 !important; + letter-spacing: -0.01em !important; +} + +[data-testid="stExpander"] summary:hover { color: var(--brass-bright) !important; } + +/* ── Headings ───────────────────────────────────────────────────────────── */ +h1, h2, h3 { + font-family: var(--font-display) !important; + color: var(--fg-1) !important; + font-weight: 500 !important; + letter-spacing: -0.01em !important; +} + +h1 { font-size: 1.875rem !important; line-height: 1.1 !important; } +h2 { font-size: 1.5rem !important; line-height: 1.2 !important; } +h3 { font-size: 1.125rem !important; line-height: 1.2 !important; } + +h4 { + font-family: var(--font-sans) !important; + font-size: 0.9375rem !important; + font-weight: 600 !important; + color: var(--fg-1) !important; + letter-spacing: -0.01em !important; +} + +h5, h6 { + font-family: var(--font-sans) !important; + font-size: 10px !important; + font-weight: 600 !important; + text-transform: uppercase !important; + letter-spacing: 0.14em !important; + color: var(--fg-4) !important; +} + +/* ── Body text ──────────────────────────────────────────────────────────── */ +p, .stMarkdown p { + font-family: var(--font-sans) !important; + font-size: 0.875rem !important; + color: var(--fg-2) !important; + line-height: 1.45 !important; +} + +.stCaption p { + font-family: var(--font-sans) !important; + font-size: 0.75rem !important; + color: var(--fg-3) !important; +} + +/* ── Code ───────────────────────────────────────────────────────────────── */ +code { + font-family: var(--font-mono) !important; + background: var(--ink-3) !important; + color: var(--fg-1) !important; + padding: 2px 6px !important; + border-radius: 2px !important; + border: 1px solid var(--line-1) !important; + font-size: 0.875em !important; +} + +/* ── Dividers ───────────────────────────────────────────────────────────── */ +hr { + border: none !important; + border-top: 1px solid var(--line-1) !important; + margin: 0.625rem 0 !important; +} + +/* ── DataFrames ─────────────────────────────────────────────────────────── */ +[data-testid="stDataFrame"] { + border: 1px solid var(--line-1) !important; + border-radius: 2px !important; +} + +/* ── Number inputs ──────────────────────────────────────────────────────── */ +.stNumberInput input { + background: var(--ink-2) !important; + border: 1px solid var(--line-2) !important; + border-radius: 2px !important; + font-family: var(--font-mono) !important; + font-size: 0.8125rem !important; + color: var(--fg-1) !important; + font-variant-numeric: tabular-nums !important; +} + +.stNumberInput label { + font-family: var(--font-sans) !important; + font-size: 10px !important; + font-weight: 600 !important; + text-transform: uppercase !important; + letter-spacing: 0.12em !important; + color: var(--fg-4) !important; +} + +/* ── Alerts ─────────────────────────────────────────────────────────────── */ +[data-testid="stAlertContainer"] { + border-radius: 2px !important; + font-family: var(--font-sans) !important; + font-size: 0.875rem !important; +} + +/* ── Spinner ────────────────────────────────────────────────────────────── */ +.stSpinner > div { border-top-color: var(--brass) !important; } + +/* ── Scrollbars ─────────────────────────────────────────────────────────── */ +::-webkit-scrollbar { width: 5px; height: 5px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--ink-3); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: var(--ink-4); } </style> """, unsafe_allow_html=True) +import plotly.graph_objects as go +import plotly.io as pio + +# ── Plotly theme ────────────────────────────────────────────────────────────── +_prism_layout = go.Layout( + paper_bgcolor="#0B0E13", + plot_bgcolor="#0B0E13", + font=dict(family="IBM Plex Mono, SF Mono, Menlo, monospace", color="#C7C0AE", size=11), + title=dict( + font=dict(family="EB Garamond, Georgia, serif", color="#F2ECDC", size=18), + x=0, + ), + colorway=["#C2AA7A", "#4F8C5E", "#4A78B5", "#B5494B", "#C49545", "#8B7FBF"], + xaxis=dict( + gridcolor="#232934", + linecolor="#232934", + tickfont=dict(family="IBM Plex Mono, monospace", color="#5E5849", size=10), + title=dict(font=dict(color="#8E8676", size=11)), + showgrid=True, + zeroline=False, + ), + yaxis=dict( + gridcolor="#232934", + linecolor="#232934", + tickfont=dict(family="IBM Plex Mono, monospace", color="#5E5849", size=10), + title=dict(font=dict(color="#8E8676", size=11)), + showgrid=True, + zeroline=False, + ), + legend=dict( + bgcolor="rgba(17,21,28,0.85)", + bordercolor="#232934", + borderwidth=1, + font=dict(family="IBM Plex Sans, sans-serif", color="#C7C0AE", size=11), + ), + margin=dict(l=48, r=16, t=32, b=40), + hoverlabel=dict( + bgcolor="#181D26", + bordercolor="#2E3645", + font=dict(family="IBM Plex Mono, monospace", color="#F2ECDC", size=11), + ), +) +_prism_template = go.layout.Template(layout=_prism_layout) +pio.templates["prism"] = _prism_template +pio.templates.default = "prism" + from components.market_bar import render_market_bar from components.top_movers import render_top_movers from components.overview import render_overview @@ -71,21 +435,49 @@ if "ticker" not in st.session_state: st.session_state["ticker"] = None -# ── Sidebar ────────────────────────────────────────────────────────────────── +# ── Sidebar ─────────────────────────────────────────────────────────────────── with st.sidebar: - col_logo, col_title = st.columns([1, 2]) - with col_logo: - st.image("assets/logo.png", width=60) - with col_title: - st.markdown("### Prism") - st.caption("Financial Analysis Dashboard") - st.divider() + # Brand mark + st.markdown(""" + <div style=" + padding: 12px 16px; + border-bottom: 1px solid #232934; + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 4px; + "> + <div style=" + width: 28px; height: 28px; + border-radius: 50%; + border: 1px solid #C2AA7A; + display: flex; align-items: center; justify-content: center; + font-family: 'EB Garamond', Georgia, serif; + font-style: italic; font-size: 16px; + color: #C2AA7A; letter-spacing: -0.05em; + flex-shrink: 0; + ">P</div> + <div style="display: flex; flex-direction: column; gap: 1px; overflow: hidden;"> + <span style=" + font-family: 'EB Garamond', Georgia, serif; + font-size: 18px; color: #F2ECDC; + font-weight: 500; letter-spacing: -0.01em; + line-height: 1; white-space: nowrap; + ">Prism</span> + <span style=" + font-family: 'IBM Plex Mono', 'SF Mono', monospace; + font-size: 10px; color: #5E5849; + letter-spacing: 0.12em; text-transform: uppercase; + ">Research Terminal</span> + </div> + </div> + """, unsafe_allow_html=True) with st.form("ticker_search_form", clear_on_submit=False): query = st.text_input( - "Search company or ticker", - placeholder="e.g. Apple, AAPL, MSFT…", + "Ticker or company", + placeholder="AAPL, Apple, MSFT…", key="search_query", ).strip() @@ -108,50 +500,117 @@ with st.sidebar: if submitted and selected_symbol: st.session_state["ticker"] = selected_symbol - if st.session_state["ticker"]: - st.caption(f"Currently viewing: **{st.session_state['ticker']}**") - ticker = st.session_state["ticker"] - # Quick company info in sidebar - st.divider() + # Company snapshot if ticker: + st.divider() info = get_company_info(ticker) - if ticker and info: - st.caption(info.get("longName", ticker)) - price = get_latest_price(ticker) - prev_close = info.get("previousClose") or info.get("regularMarketPreviousClose") - 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 "" - color = "#2ecc71" if chg >= 0 else "#e74c3c" - st.markdown( - f"<span style='font-size:1.3rem;font-weight:700'>${price:,.2f}</span>" - f" <span style='font-size:0.82rem;color:{color}'>{sign}{chg:+.2f} ({sign}{chg_pct:.2f}%)</span>", - unsafe_allow_html=True, - ) - else: - st.markdown( - f"<span style='font-size:1.3rem;font-weight:700'>${price:,.2f}</span>", - unsafe_allow_html=True, - ) - _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 "—" - st.caption(f"Exchange: {exchange}") - st.caption(f"Currency: {info.get('currency', 'USD')}") - st.caption(f"Sector: {info.get('sector', '—')}") - employees = info.get("fullTimeEmployees") - st.caption(f"Employees: {employees:,}" if isinstance(employees, int) else "Employees: —") - website = info.get("website") - if website: - st.markdown(f"[🌐 Website]({website})") + if info: + co_name = info.get("longName", ticker) + price = get_latest_price(ticker) + prev_close = info.get("previousClose") or info.get("regularMarketPreviousClose") + + # 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}</div> + <div style=" + font-family: 'IBM Plex Sans', sans-serif; + font-size: 11px; color: #8E8676; + letter-spacing: 0.01em; + ">{co_name}</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", exchange), + ("Sector", sector), + ("Currency", currency), + ("Employees", 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 = info.get("website", "") + if website: + st.markdown(f""" + <div style="padding:6px 0 0;"> + <a href="{website}" target="_blank" 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}**") # ── Market Bar ──────────────────────────────────────────────────────────────── @@ -171,17 +630,32 @@ st.divider() # ── Main Content ────────────────────────────────────────────────────────────── if not ticker: - st.info("Search for a company or ticker in the sidebar to get started.") + st.markdown(""" + <div style="padding:48px 0 32px;text-align:center;"> + <div style=" + font-family:'EB Garamond',Georgia,serif; + font-style:italic;font-size:2.375rem; + color:#F2ECDC;font-weight:400; + letter-spacing:-0.01em;line-height:1.1; + margin-bottom:12px; + ">Search for a ticker to begin.</div> + <div style=" + font-family:'IBM Plex Sans',sans-serif; + font-size:0.875rem;color:#5E5849; + letter-spacing:0.01em; + ">Enter a company name or symbol in the sidebar.</div> + </div> + """, unsafe_allow_html=True) st.stop() tab_overview, tab_financials, tab_valuation, tab_options, tab_insiders, tab_filings, tab_news = st.tabs([ - "📈 Overview", - "📊 Financials", - "💰 Valuation", - "🎯 Options", - "👤 Insiders", - "📁 Filings", - "📰 News", + "Overview", + "Financials", + "Valuation", + "Options", + "Insiders", + "Filings", + "News", ]) with tab_overview: diff --git a/components/filings.py b/components/filings.py index a1e4417..9e3b156 100644 --- a/components/filings.py +++ b/components/filings.py @@ -17,9 +17,9 @@ _FORM_DESCRIPTIONS = { } _FORM_COLORS = { - "10-K": "rgba(79,142,247,0.15)", - "10-Q": "rgba(130,224,170,0.15)", - "8-K": "rgba(247,162,79,0.15)", + "10-K": "rgba(74,120,181,0.15)", + "10-Q": "rgba(79,140,94,0.15)", + "8-K": "rgba(196,149,69,0.15)", } @@ -74,18 +74,35 @@ def render_filings(ticker: str): left, right = st.columns([5, 1]) with left: st.markdown( - f"<div style='background:{color};padding:6px 10px;border-radius:4px;margin-bottom:2px'>" - f"<strong>{form}</strong> · " - f"<span style='color:#9aa0b0;font-size:0.82rem'>{date}</span><br>" - f"<span style='font-size:0.85rem'>{title}</span>" + f"<div style='" + f"background:{color};" + f"border:1px solid rgba(194,170,122,0.12);" + f"padding:8px 12px;border-radius:2px;margin-bottom:2px;" + f"display:flex;align-items:baseline;gap:10px;" + f"'>" + f"<span style='" + f"font-family:IBM Plex Mono,monospace;" + f"font-size:11px;color:#C2AA7A;" + f"background:rgba(194,170,122,0.07);" + f"border:1px solid rgba(194,170,122,0.25);" + f"padding:2px 6px;border-radius:2px;" + f"white-space:nowrap;" + f"'>{form}</span>" + f"<span style='font-family:IBM Plex Sans,sans-serif;font-size:0.8125rem;color:#F2ECDC;'>{title}</span>" + f"<span style='font-family:IBM Plex Mono,monospace;font-size:11px;color:#5E5849;margin-left:auto;white-space:nowrap;'>{date}</span>" f"</div>", unsafe_allow_html=True, ) with right: - # Prefer the actual filing doc over the Yahoo index page doc_url = exhibits.get(form) or edgar_url if doc_url: st.markdown( - f"<div style='padding-top:8px'><a href='{doc_url}' target='_blank'>🔗 View</a></div>", + f"<div style='padding-top:8px;text-align:right;'>" + f"<a href='{doc_url}' target='_blank' style='" + f"font-family:IBM Plex Sans,sans-serif;" + f"font-size:11px;color:#C2AA7A;" + f"text-decoration:none;" + f"border-bottom:1px solid #8F7A50;" + f"'>View ↗</a></div>", unsafe_allow_html=True, ) diff --git a/components/insiders.py b/components/insiders.py index 07bc3e3..bdb1818 100644 --- a/components/insiders.py +++ b/components/insiders.py @@ -83,18 +83,16 @@ def render_insiders(ticker: str): fig = go.Figure() fig.add_trace(go.Bar( x=months, y=[monthly[m]["Buy"] / 1e6 for m in months], - name="Buys", marker_color="#2ecc71", + name="Buys", marker_color="#4F8C5E", )) fig.add_trace(go.Bar( x=months, y=[-monthly[m]["Sell"] / 1e6 for m in months], - name="Sells", marker_color="#e74c3c", + name="Sells", marker_color="#B5494B", )) fig.update_layout( title="Monthly Insider Net Activity ($M)", barmode="relative", yaxis_title="Value ($M)", - plot_bgcolor="rgba(0,0,0,0)", - paper_bgcolor="rgba(0,0,0,0)", margin=dict(l=0, r=0, t=40, b=0), height=280, legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1), @@ -133,9 +131,9 @@ def render_insiders(ticker: str): def _color_type(row): if row["Type"] == "Buy": - return [""] * 3 + ["background-color: rgba(46,204,113,0.15)"] + [""] * 2 + return [""] * 3 + ["background-color: #15241A; color: #4F8C5E"] + [""] * 2 if row["Type"] == "Sell": - return [""] * 3 + ["background-color: rgba(231,76,60,0.15)"] + [""] * 2 + return [""] * 3 + ["background-color: #2A1517; color: #B5494B"] + [""] * 2 return [""] * len(row) st.dataframe( diff --git a/components/market_bar.py b/components/market_bar.py index 411b232..e3accc5 100644 --- a/components/market_bar.py +++ b/components/market_bar.py @@ -23,46 +23,38 @@ def render_market_bar(): """ <style> .prism-market-card { - background: rgba(22, 28, 39, 0.92); - border: 1px solid rgba(255,255,255,0.06); - border-radius: 14px; - padding: 0.8rem 0.95rem; - min-height: 96px; + background: #11151C; + border: 1px solid #232934; + border-radius: 2px; + padding: 12px 16px; } .prism-market-label { - color: #9aa0b0; - font-size: 0.78rem; + font-family: 'IBM Plex Sans', sans-serif; + font-size: 10px; font-weight: 600; - letter-spacing: 0.06em; + letter-spacing: 0.14em; text-transform: uppercase; - margin-bottom: 0.45rem; + color: #5E5849; + margin-bottom: 6px; } .prism-market-value { - color: #f3f6fb; - font-size: 1.5rem; - font-weight: 700; - line-height: 1.15; - margin-bottom: 0.45rem; + font-family: 'IBM Plex Mono', monospace; + font-variant-numeric: tabular-nums; + font-size: 1.25rem; + font-weight: 500; + color: #F2ECDC; + line-height: 1.1; + margin-bottom: 4px; } .prism-market-delta { - display: inline-block; - border-radius: 999px; - font-size: 0.78rem; - font-weight: 600; - padding: 0.18rem 0.5rem; - } - .prism-market-delta.positive { - color: #7ce3a1; - background: rgba(28, 131, 72, 0.18); - } - .prism-market-delta.negative { - color: #ff8a8a; - background: rgba(180, 47, 47, 0.18); - } - .prism-market-delta.neutral { - color: #c6cfdd; - background: rgba(198, 207, 221, 0.12); + font-family: 'IBM Plex Mono', monospace; + font-variant-numeric: tabular-nums; + font-size: 11px; + font-weight: 500; } + .prism-market-delta.positive { color: #4F8C5E; } + .prism-market-delta.negative { color: #B5494B; } + .prism-market-delta.neutral { color: #5E5849; } </style> """, unsafe_allow_html=True, diff --git a/components/options.py b/components/options.py index e9bf016..0acce31 100644 --- a/components/options.py +++ b/components/options.py @@ -104,7 +104,7 @@ def render_options(ticker: str): y=calls_atm["impliedVolatility"] * 100, name="Calls IV", mode="lines+markers", - line=dict(color="#4F8EF7", width=2), + line=dict(color="#C2AA7A", width=2), marker=dict(size=4), )) if not puts_atm.empty and "impliedVolatility" in puts_atm.columns: @@ -113,7 +113,7 @@ def render_options(ticker: str): y=puts_atm["impliedVolatility"] * 100, name="Puts IV", mode="lines+markers", - line=dict(color="#F7A24F", width=2), + line=dict(color="#C49545", width=2), marker=dict(size=4), )) if current_price: diff --git a/components/overview.py b/components/overview.py index 1bb65c2..9a0d162 100644 --- a/components/overview.py +++ b/components/overview.py @@ -118,22 +118,22 @@ def _score_card(info: dict) -> None: return color_map = { - "green": ("rgba(46,204,113,0.15)", "#7ce3a1"), - "yellow": ("rgba(243,156,18,0.15)", "#f0c040"), - "red": ("rgba(231,76,60,0.15)", "#ff8a8a"), - "neutral": ("rgba(255,255,255,0.05)", "#9aa0b0"), + "green": ("#15241A", "#4F8C5E"), + "yellow": ("#2A1F0F", "#C49545"), + "red": ("#2A1517", "#B5494B"), + "neutral": ("#181D26", "#5E5849"), } cards_html = "" for label, color, value, desc in signals: bg, fg = color_map[color] cards_html += ( - f'<div style="background:{bg};border:1px solid {fg}44;border-radius:10px;' - f'padding:0.5rem 0.75rem;flex:1;min-width:110px;">' - f'<div style="font-size:0.68rem;font-weight:600;color:#9aa0b0;text-transform:uppercase;' - f'letter-spacing:0.05em;margin-bottom:0.15rem;">{label}</div>' - f'<div style="font-size:0.85rem;font-weight:700;color:{fg};">{value}</div>' - f'<div style="font-size:0.68rem;color:#9aa0b0;margin-top:0.1rem;">{desc}</div>' + f'<div style="background:{bg};border:1px solid {fg}55;border-radius:2px;' + f'padding:8px 12px;flex:1;min-width:110px;">' + f'<div style="font-family:IBM Plex Sans,sans-serif;font-size:10px;font-weight:600;color:#5E5849;text-transform:uppercase;' + f'letter-spacing:0.12em;margin-bottom:3px;">{label}</div>' + f'<div style="font-family:IBM Plex Mono,monospace;font-size:0.875rem;font-weight:500;color:{fg};font-variant-numeric:tabular-nums;">{value}</div>' + f'<div style="font-family:IBM Plex Sans,sans-serif;font-size:0.75rem;color:#8E8676;margin-top:2px;">{desc}</div>' f'</div>' ) @@ -159,23 +159,28 @@ def _render_52w_bar(info: dict) -> None: st.markdown( f""" - <div style="margin:0.4rem 0 0.9rem 0;"> - <div style="display:flex;justify-content:space-between;font-size:0.72rem;color:#9aa0b0;margin-bottom:0.35rem;"> - <span>52W Low: {fmt_currency(low)}</span> - <span style="color:#c6cfdd;font-weight:600;"> - {fmt_currency(price)} · {pct:.0f}% of range + <div style="margin:8px 0 16px 0;"> + <div style="display:flex;justify-content:space-between; + font-family:'IBM Plex Mono',monospace;font-size:11px; + font-variant-numeric:tabular-nums; + color:#8E8676;margin-bottom:6px;"> + <span>{fmt_currency(low)}</span> + <span style="color:#C2AA7A;font-weight:500;"> + {fmt_currency(price)} · {pct:.0f}% </span> - <span>52W High: {fmt_currency(high)}</span> + <span>{fmt_currency(high)}</span> </div> - <div style="position:relative;height:7px;background:rgba(255,255,255,0.08);border-radius:4px;overflow:visible;"> + <div style="position:relative;height:3px;background:#222934;border-radius:999px;overflow:visible;"> <div style="position:absolute;left:0;width:{pct}%;height:100%; - background:linear-gradient(to right,#e74c3c,#f0b27a,#2ecc71);border-radius:4px;"></div> - <div style="position:absolute;left:calc({pct}% - 2px);top:-4px;width:4px;height:15px; - background:#fff;border-radius:2px;box-shadow:0 0 5px rgba(0,0,0,0.5);"></div> + background:#C2AA7A;border-radius:999px;"></div> + <div style="position:absolute;left:calc({pct}% - 1px);top:-4px;width:2px;height:11px; + background:#DCC79E;border-radius:1px;"></div> </div> - <div style="display:flex;justify-content:space-between;font-size:0.68rem;color:#9aa0b0;margin-top:0.3rem;"> - <span>+{from_low_pct:.1f}% above low</span> - <span>{to_high_pct:.1f}% below high</span> + <div style="display:flex;justify-content:space-between; + font-family:'IBM Plex Mono',monospace;font-size:10px; + color:#5E5849;margin-top:5px;"> + <span>+{from_low_pct:.1f}% from low</span> + <span>{to_high_pct:.1f}% to high</span> </div> </div> """, @@ -286,10 +291,10 @@ def _render_relative_chart(ticker: str, info: dict, period: str): y=subject_series.values, mode="lines", name=ticker.upper(), - line=dict(color="#4F8EF7", width=2.5), + line=dict(color="#C2AA7A", width=2.5), )) - palette = ["#7ce3a1", "#F7A24F", "#c084fc", "#ff8a8a", "#9ad1ff"] + palette = ["#7ce3a1", "#C49545", "#c084fc", "#ff8a8a", "#9ad1ff"] plotted = 1 for idx, label in enumerate(selected_labels): symbol = option_map[label] @@ -424,7 +429,7 @@ def render_overview(ticker: str): y=hist["Close"], mode="lines", name="Close", - line=dict(color="#4F8EF7", width=2), + line=dict(color="#C2AA7A", width=2), fill="tozeroy", fillcolor="rgba(79,142,247,0.08)", )) diff --git a/components/top_movers.py b/components/top_movers.py index 5589df6..db95592 100644 --- a/components/top_movers.py +++ b/components/top_movers.py @@ -28,30 +28,39 @@ def _inject_styles(): padding: 0.18rem 0; } .prism-mover-symbol { - font-size: 1rem; - font-weight: 700; + font-family: 'IBM Plex Sans', sans-serif; + font-size: 0.875rem; + font-weight: 600; + color: #F2ECDC; line-height: 1.1; } .prism-mover-name { - color: #9aa0b0; - font-size: 0.84rem; + font-family: 'IBM Plex Sans', sans-serif; + color: #8E8676; + font-size: 0.8125rem; line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .prism-mover-price { - font-size: 0.98rem; + font-family: 'IBM Plex Mono', monospace; + font-variant-numeric: tabular-nums; + font-size: 0.8125rem; + color: #C7C0AE; line-height: 1.1; } .prism-mover-change { - font-size: 0.98rem; - font-weight: 600; + font-family: 'IBM Plex Mono', monospace; + font-variant-numeric: tabular-nums; + font-size: 0.8125rem; + font-weight: 500; line-height: 1.1; } .prism-mover-change-meta { - font-size: 0.74rem; - color: #9aa0b0; + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + color: #5E5849; margin-left: 0.2rem; } @media (max-width: 900px) { @@ -91,7 +100,7 @@ def _mover_row_html(q: dict) -> str: try: chg_f = float(chg_pct) - color = "#2ecc71" if chg_f >= 0 else "#e74c3c" + color = "#4F8C5E" if chg_f >= 0 else "#B5494B" sign = "+" if chg_f >= 0 else "" pct_str = f"{sign}{chg_f:.2f}%" except Exception: @@ -143,11 +152,16 @@ def _render_mover_tab(screen: str, state_key: str): @st.fragment def render_top_movers(): _inject_styles() - st.markdown("#### 🔥 Top Movers") + st.markdown(""" + <div style=" + font-family:'IBM Plex Sans',sans-serif; + font-size:10px;font-weight:600; + text-transform:uppercase;letter-spacing:0.14em; + color:#5E5849;margin-bottom:8px; + ">Top Movers</div> + """, unsafe_allow_html=True) - tab_gainers, tab_losers, tab_active = st.tabs([ - "📈 Gainers", "📉 Losers", "⚡ Most Active" - ]) + tab_gainers, tab_losers, tab_active = st.tabs(["Gainers", "Losers", "Most Active"]) screens = { "gainers": "day_gainers", diff --git a/components/valuation.py b/components/valuation.py index 0095f41..a141846 100644 --- a/components/valuation.py +++ b/components/valuation.py @@ -586,7 +586,7 @@ def _render_dcf_model(ctx: dict): fig = go.Figure(go.Bar( x=years + ["Terminal Value"], y=[(v / 1e9) for v in discounted] + [terminal_pv / 1e9], - marker_color=["#4F8EF7"] * len(years) + ["#F7A24F"], + marker_color=["#C2AA7A"] * len(years) + ["#C49545"], text=[f"${v / 1e9:.2f}B" for v in discounted] + [f"${terminal_pv / 1e9:.2f}B"], textposition="outside", )) @@ -1235,7 +1235,7 @@ def _render_analyst_targets(ticker: str): st.write("") - colors = ["#2ecc71", "#82e0aa", "#f0b27a", "#e59866", "#e74c3c"] + colors = ["#4F8C5E", "#4F8C5E", "#C49545", "#8F7A50", "#B5494B"] fig = go.Figure(go.Bar( x=list(counts.keys()), y=list(counts.values()), @@ -1314,14 +1314,14 @@ def _render_earnings_history(ticker: str): y=df_chart["epsActual"], name="Actual EPS", mode="lines+markers", - line=dict(color="#4F8EF7", width=2), + line=dict(color="#C2AA7A", width=2), )) fig.add_trace(go.Scatter( x=df_chart.index.astype(str), y=df_chart["epsEstimate"], name="Estimated EPS", mode="lines+markers", - line=dict(color="#F7A24F", width=2, dash="dash"), + line=dict(color="#C49545", width=2, dash="dash"), )) fig.update_layout( title="EPS: Actual vs. Estimate", @@ -1351,7 +1351,7 @@ _HIST_RATIO_OPTIONS = { } _CHART_COLORS = [ - "#4F8EF7", "#F7A24F", "#2ecc71", "#e74c3c", + "#C2AA7A", "#C49545", "#4F8C5E", "#B5494B", "#9b59b6", "#1abc9c", "#f39c12", "#e67e22", ] @@ -1534,7 +1534,7 @@ def _render_forward_estimates(ticker: str): y=hist["epsActual"], name="EPS Actual", mode="lines+markers", - line=dict(color="#4F8EF7", width=2), + line=dict(color="#C2AA7A", width=2), )) if fwd_dates: @@ -1560,7 +1560,7 @@ def _render_forward_estimates(ticker: str): y=fwd_eps, name="EPS Est. (Avg)", mode="lines+markers", - line=dict(color="#F7A24F", width=2, dash="dash"), + line=dict(color="#C49545", width=2, dash="dash"), )) fig.update_layout( |
