From bbceb4c6798d43f4b32e73f38fc4907e00733244 Mon Sep 17 00:00:00 2001 From: Tyler Date: Sat, 16 May 2026 01:40:02 -0700 Subject: Rewrite watchlist as custom HTML component with proper styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces st.button rows with a components.html iframe that renders sym·price·Δ% in a 3-column grid with hairline dividers, monospace fonts, and colored change percentages. Adds hidden text_input click receiver so row clicks update session state. Fixes toggle button to show toast when watchlist cap is hit and removes now-redundant psm-watch-toggle CSS. Co-Authored-By: Claude Sonnet 4.6 --- components/watchlist.py | 184 +++++++++++++++++++++++------------------------- 1 file changed, 87 insertions(+), 97 deletions(-) (limited to 'components') diff --git a/components/watchlist.py b/components/watchlist.py index 0b50d3b..64f57f8 100644 --- a/components/watchlist.py +++ b/components/watchlist.py @@ -1,66 +1,9 @@ """Watchlist sidebar component — session-scoped personal watchlist.""" import streamlit as st +import streamlit.components.v1 as components from services.data_service import get_latest_price, get_company_info from utils.security import escape_html -_WATCHLIST_CSS_INJECTED = False - - -def _inject_css(): - global _WATCHLIST_CSS_INJECTED - if _WATCHLIST_CSS_INJECTED: - return - _WATCHLIST_CSS_INJECTED = True - st.markdown(""" - -""", unsafe_allow_html=True) - def render_watchlist(): """Render the personal watchlist section in the sidebar. @@ -71,56 +14,103 @@ def render_watchlist(): Must be called from within a `with st.sidebar:` block. """ - _inject_css() + watchlist = st.session_state.get("watchlist", []) - watchlist: list[str] = st.session_state.get("watchlist", []) + # Process any pending click from the JS click handler + clicked = st.session_state.get("_wl_click", "") + if clicked and clicked in watchlist: + st.session_state["ticker"] = clicked + st.session_state["_wl_click"] = "" + st.rerun() - st.markdown('
Watchlist
', unsafe_allow_html=True) - - if not watchlist: - st.markdown( - '
No saved tickers
', - unsafe_allow_html=True, - ) - return + # Hidden click receiver — JS will set this input's value on row click + st.text_input("wl_click_receiver", key="_wl_click", label_visibility="collapsed") + # Build rows HTML (string concatenation, NOT f-strings — JS {} would break) + rows_html = "" for sym in watchlist: try: price = get_latest_price(sym) info = get_company_info(sym) or {} prev = info.get("regularMarketPreviousClose") or info.get("previousClose") - + sym_e = escape_html(sym) if price is not None and prev and prev > 0: chg_pct = (price - prev) / prev * 100 - sign = "+" if chg_pct >= 0 else "" - chg_color = "#4F8C5E" if chg_pct >= 0 else "#B5494B" - row_label = ( - escape_html(sym) - + " $" + f"{price:,.2f}" - + " " + sign + f"{chg_pct:.2f}%" - ) + sign = "+" if chg_pct >= 0 else "−" + chg_cls = "pos" if chg_pct >= 0 else "neg" + chg_str = sign + f"{abs(chg_pct):.2f}%" + px_str = "$" + f"{price:,.2f}" elif price is not None: - chg_color = "#8E8676" - row_label = escape_html(sym) + " $" + f"{price:,.2f}" + chg_cls = "flat" + chg_str = "—" + px_str = "$" + f"{price:,.2f}" else: - chg_color = "#8E8676" - row_label = escape_html(sym) + " —" - - # Render a styled container div then the Streamlit button inside it - st.markdown('
', unsafe_allow_html=True) - if st.button(row_label, key="watch_row_" + sym, use_container_width=True): - st.session_state["ticker"] = sym - st.rerun() - st.markdown("
", unsafe_allow_html=True) - + chg_cls = "flat" + chg_str = "—" + px_str = "—" + rows_html += ( + "
" + "" + sym_e + "" + "" + px_str + "" + "" + chg_str + "" + "
" + ) except Exception: - # Never let one broken ticker blow up the whole watchlist - sym_safe = escape_html(sym) - st.markdown( - '
', - unsafe_allow_html=True, + sym_e = escape_html(sym) + rows_html += ( + "
" + "" + sym_e + "" + "" + "" + "
" ) - if st.button(sym_safe + " (error)", key="watch_row_" + sym, use_container_width=True): - st.session_state["ticker"] = sym - st.rerun() - st.markdown("
", unsafe_allow_html=True) + + if not rows_html: + content_html = "
No saved tickers
" + else: + content_html = rows_html + + height = 28 + max(len(watchlist), 1) * 34 + 16 # label + rows + buffer + + # Build the full HTML block (string concat, not f-string — JS {} would break) + html = ( + "" + "Watchlist" + + content_html + + "" + ) + + components.html(html, height=height, scrolling=False) -- cgit v1.3-2-g0d8e