Spaces:
Sleeping
Sleeping
Download dashboard/theme.py from Viney/readthrough: direct link, hf CLI and curl.
- Browser
- Download file 24.5 kB
-
https://huggingface.co/spaces/Viney/readthrough/resolve/main/dashboard/theme.py
- Command line
-
hf download hf://spaces/Viney/readthrough/dashboard/theme.py
-
curl -L -o theme.py https://huggingface.co/spaces/Viney/readthrough/resolve/main/dashboard/theme.py
24.5 kB
| """Centralized design tokens and CSS injection for Readthrough. | |
| The palette is the published research note's, so the app and the document | |
| it serves are one design. dashboard/note_css.py is the source of truth for | |
| the values; the constants below restate them as literals because plotly | |
| cannot resolve a CSS variable. | |
| """ | |
| from __future__ import annotations | |
| import streamlit as st | |
| from dashboard.note_css import FONT_IMPORT, NOTE_CSS, TOKENS_LIGHT | |
| # ββ Brand βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| BRAND_GREEN = "#1E6B55" | |
| BRAND_GREEN_DARK = "#134A3B" | |
| # ββ Surfaces ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| BG = "#F5F6F3" | |
| BG_MUTED = "#F5F6F3" | |
| BORDER = "#D5DAD4" | |
| BORDER_STRONG = "#8C968F" | |
| # ββ Text ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEXT = "#15201B" | |
| TEXT_MUTED = "#3E4A44" | |
| TEXT_FAINT = "#8C968F" | |
| # ββ Semantic palette β the note's own tokens ββββββββββββββββββββββββββββββββββ | |
| # These are literals rather than var() references because plotly consumes them | |
| # directly and cannot resolve a CSS variable. Values track dashboard/note_css.py. | |
| GREEN = "#1E6B55" | |
| BULL = "#1E6B55" | |
| BULL_BG = "#DCEBE3" | |
| BULL_BORDER = "#1E6B55" | |
| RED = "#A63A2B" | |
| BEAR = "#A63A2B" | |
| BEAR_BG = "#F3DDD8" | |
| BEAR_BORDER = "#A63A2B" | |
| AMBER = "#B8791F" | |
| WARN = "#B8791F" | |
| WARN_BG = "#F4E8D2" | |
| WARN_BORDER = "#B8791F" | |
| BLUE = "#3E4A44" | |
| INFO = "#3E4A44" | |
| INFO_BG = "#ECEEE9" | |
| INFO_BORDER = "#D5DAD4" | |
| GRAY = "#3E4A44" | |
| PURPLE = "#1E6B55" | |
| AI_BG = "#DCEBE3" | |
| AI_BORDER = "#1E6B55" | |
| AI_COLOR = "#1E6B55" | |
| AI_BADGE_BG = "#DCEBE3" | |
| IMPACT_HIGH_BG = "#15201B" # near-white chip β highest contrast on dark | |
| IMPACT_HIGH_TEXT = "#F5F6F3" | |
| IMPACT_MED_BG = "#8C968F" # slate-400 | |
| IMPACT_MED_TEXT = "#F5F6F3" | |
| IMPACT_LOW_BG = "transparent" | |
| IMPACT_LOW_TEXT = "#8C968F" # slate-400 | |
| IMPACT_LOW_BORDER = "#8C968F" # slate-600 | |
| # ββ Plotly rgba aliases βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| GREEN_A53 = "rgba(30,107,85,0.55)" | |
| GREEN_A40 = "rgba(30,107,85,0.42)" | |
| RED_A40 = "rgba(166,58,43,0.42)" | |
| PURPLE_A53 = "rgba(19,74,59,0.55)" | |
| BLUE_A53 = "rgba(62,74,68,0.55)" | |
| # ββ Radius ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| RADIUS_CARD = "12px" | |
| RADIUS_BTN = "8px" | |
| RADIUS_CHIP = "6px" | |
| # ββ Typographic scale βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| FS_PAGE = "1.55rem" # ~25px page/screen title | |
| FS_SECTION = "1.05rem" # ~17px section sub-header | |
| FS_CARD = "0.95rem" # ~15px card headline (semibold) | |
| FS_BODY = "0.9rem" # ~14.5px body text | |
| FS_META = "0.75rem" # ~12px meta/attribution line | |
| FS_EYEBROW = "0.6rem" # uppercase micro-label (was scattered 0.56β0.72rem) | |
| FS_SECTION_LABEL = "0.78rem" | |
| # ββ Spacing scale βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SPACE_1 = "4px" | |
| SPACE_2 = "8px" | |
| SPACE_3 = "12px" | |
| SPACE_4 = "16px" | |
| SPACE_5 = "20px" | |
| SPACE_6 = "28px" # intentionally 8px jump β used for section-break vertical rhythm | |
| # NAV_GROUPS now lives in dashboard.nav β imported from there in app.py. | |
| # Kept as a re-export for any legacy callers. | |
| def _lazy_nav_groups(): # noqa: F401 β accessed via dashboard.nav.NAV_GROUPS | |
| from dashboard.nav import NAV_GROUPS # noqa: F401 | |
| return NAV_GROUPS | |
| # ββ Logo SVG β geometric P in emerald rounded square βββββββββββββββββββββββββ | |
| # The counter of the P is punched out in the canvas colour so the mark reads | |
| # correctly on the dark sidebar (it used to be punched out in white). | |
| LOGO_SVG = """<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <rect width="36" height="36" rx="9" fill="#1E6B55"/> | |
| <path d="M12 26V11H19.5C20.9 11 22.1 11.5 23.1 12.5C24.1 13.5 24.6 14.7 24.6 16.2C24.6 17.7 24.1 18.9 23.1 19.9C22.1 20.9 20.9 21.4 19.5 21.4H15.5V26H12Z" fill="#F5F6F3"/> | |
| <path d="M15.5 18.2H19C19.8 18.2 20.4 17.9 20.9 17.4C21.4 16.9 21.6 16.2 21.6 15.4C21.6 14.6 21.4 14 20.9 13.5C20.4 13 19.8 12.7 19 12.7H15.5V18.2Z" fill="#1E6B55"/> | |
| </svg>""" | |
| def _register_plotly_template() -> None: | |
| """Make dark the plotly default so charts inherit it without per-figure work. | |
| Individual figures still override plot/paper background and font colour; | |
| what this template actually buys is the styling nothing sets by hand β | |
| hover labels and the modebar, which otherwise render light-on-light. | |
| """ | |
| try: | |
| import plotly.graph_objects as go | |
| import plotly.io as pio | |
| except Exception: # plotly missing β charts are optional, styling is not fatal | |
| return | |
| # Already the default: nothing to do, and importantly nothing to validate. | |
| # Assigning it again makes plotly deep-copy and re-validate the template, | |
| # which reaches into numpy β see the guard in app.py for why that is a | |
| # hazard on a rerun. | |
| if pio.templates.default == "readthrough_paper": | |
| return | |
| if "readthrough_paper" not in pio.templates: | |
| pio.templates["readthrough_paper"] = go.layout.Template( | |
| layout=dict( | |
| paper_bgcolor=BG, | |
| plot_bgcolor=BG, | |
| font=dict(family="IBM Plex Sans", color=TEXT_MUTED, size=11), | |
| hoverlabel=dict( | |
| bgcolor="#ECEEE9", | |
| bordercolor=BORDER_STRONG, | |
| font=dict(family="IBM Plex Sans", color=TEXT, size=12), | |
| ), | |
| xaxis=dict(gridcolor="#ECEEE9", zerolinecolor="#D5DAD4", | |
| linecolor=BORDER, tickfont=dict(color=TEXT_MUTED)), | |
| yaxis=dict(gridcolor="#ECEEE9", zerolinecolor="#D5DAD4", | |
| linecolor=BORDER, tickfont=dict(color=TEXT_MUTED)), | |
| colorway=[BRAND_GREEN, BLUE, PURPLE, AMBER, RED, "#8C968F"], | |
| ) | |
| ) | |
| pio.templates.default = "readthrough_paper" | |
| def inject_global_css() -> None: | |
| """Inject the note's design system app-wide. Call once from app.py. | |
| Loads the three families the note is set in, defines its palette as CSS | |
| variables, ships the note stylesheet itself, then restates Streamlit's | |
| own chrome in those same variables so the app and the document it serves | |
| are one design rather than two. | |
| """ | |
| _register_plotly_template() | |
| st.html( | |
| """ | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <style>""" | |
| + FONT_IMPORT | |
| + TOKENS_LIGHT | |
| + NOTE_CSS | |
| + """ | |
| /* ββ Typography βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| html, body, .stApp, .main, [data-testid="stAppViewContainer"], | |
| [data-testid="stSidebar"], button, input, select, textarea { | |
| font-family: "IBM Plex Sans", -apple-system, "Segoe UI", Helvetica, Arial, sans-serif !important; | |
| } | |
| html, body, .stApp, .main, [data-testid="stAppViewContainer"], | |
| [data-testid="stSidebar"] { | |
| line-height: 1.55 !important; | |
| } | |
| /* Headings take the note's serif, as they do in the document itself. */ | |
| .stApp h1, .stApp h2, .stApp h3, | |
| [data-testid="stSidebar"] h1, [data-testid="stSidebar"] h2 { | |
| font-family: "Newsreader", Georgia, "Times New Roman", serif !important; | |
| font-weight: 500 !important; | |
| letter-spacing: -0.005em !important; | |
| } | |
| /* The note is one document: Streamlit's gutters would inset it inside | |
| its own .wrap, so the page gives it the full column and .wrap does | |
| the centring, exactly as it does when published. */ | |
| .main .block-container:has(.rt-note) { | |
| max-width: none !important; | |
| padding-left: 0 !important; | |
| padding-right: 0 !important; | |
| } | |
| /* ββ Hide default Streamlit chrome ββββββββββββββββββββββββββββββββββββ */ | |
| #MainMenu { visibility: hidden; } | |
| header[data-testid="stHeader"] { background: transparent !important; } | |
| footer { visibility: hidden; } | |
| /* ββ Canvas background ββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .stApp, [data-testid="stAppViewContainer"] { | |
| background: #F5F6F3 !important; | |
| } | |
| /* ββ Sidebar ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stSidebar"] { | |
| background-color: #F5F6F3 !important; | |
| border-right: 1px solid #D5DAD4 !important; | |
| } | |
| /* ββ Logo row (custom div) ββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .primer-logo-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 20px 16px 16px; | |
| border-bottom: 1px solid #D5DAD4; | |
| margin-bottom: 12px; | |
| } | |
| .primer-logo-wordmark { | |
| font-size: 1rem; | |
| font-weight: 700; | |
| color: #15201B; | |
| letter-spacing: -0.02em; | |
| line-height: 1.2; | |
| } | |
| .primer-logo-tagline { | |
| font-size: 0.65rem; | |
| color: #8C968F; | |
| font-weight: 400; | |
| margin-top: 1px; | |
| } | |
| /* ββ Text inputs ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stTextInput"] input { | |
| border-radius: 8px !important; | |
| border: 1px solid #D5DAD4 !important; | |
| font-size: 0.9rem !important; | |
| background: #F5F6F3 !important; | |
| color: #15201B !important; | |
| } | |
| [data-testid="stTextInput"] input:focus { | |
| border-color: #1E6B55 !important; | |
| box-shadow: 0 0 0 3px rgba(30,107,85,0.18) !important; | |
| } | |
| /* ββ Primary button β inverted (near-white), not green. | |
| Emerald stays reserved for active-nav and positive semantics, so the | |
| CTA must not compete with it. ββββββββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stBaseButton-primary"] { | |
| background: #15201B !important; | |
| color: #F5F6F3 !important; | |
| border-radius: 8px !important; | |
| border: none !important; | |
| font-weight: 500 !important; | |
| font-size: 0.875rem !important; | |
| letter-spacing: 0.01em !important; | |
| } | |
| [data-testid="stBaseButton-primary"]:hover { | |
| background: #ECEEE9 !important; | |
| border: none !important; | |
| } | |
| /* ββ Secondary / default buttons ββββββββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stBaseButton-secondary"] { | |
| border-radius: 8px !important; | |
| border: 1px solid #D5DAD4 !important; | |
| background: #ECEEE9 !important; | |
| font-weight: 500 !important; | |
| color: #15201B !important; | |
| } | |
| [data-testid="stBaseButton-secondary"]:hover { | |
| background: #ECEEE9 !important; | |
| border-color: #8C968F !important; | |
| } | |
| /* ββ Download button ββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stDownloadButton"] button { | |
| border-radius: 8px !important; | |
| border: 1px solid #D5DAD4 !important; | |
| background: #ECEEE9 !important; | |
| font-weight: 500 !important; | |
| color: #15201B !important; | |
| } | |
| /* ββ Bordered containers ββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stVerticalBlockBorderWrapper"] { | |
| border-radius: 12px !important; | |
| border: 1px solid #D5DAD4 !important; | |
| box-shadow: 0 1px 2px rgba(21,32,27,0.06), 0 4px 12px rgba(21,32,27,0.04) !important; | |
| background: #F5F6F3 !important; | |
| padding: 20px 24px !important; | |
| } | |
| /* ββ Custom card shadow class βββββββββββββββββββββββββββββββββββββββββ */ | |
| .primer-card { | |
| box-shadow: 0 1px 2px rgba(21,32,27,0.06), 0 4px 12px rgba(21,32,27,0.04); | |
| } | |
| /* ββ Section cards (HTML-rendered sections) βββββββββββββββββββββββββββββββ */ | |
| .primer-section-card { | |
| background: #F5F6F3; | |
| border: 1px solid #D5DAD4; | |
| border-radius: 12px; | |
| padding: 22px 26px; | |
| margin: 0 0 18px; | |
| box-shadow: 0 1px 2px rgba(21,32,27,0.06), 0 4px 12px rgba(21,32,27,0.04); | |
| } | |
| /* ββ Dividers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| hr { border-color: #D5DAD4 !important; } | |
| /* ββ Status box βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stStatus"] { | |
| border-radius: 8px !important; | |
| border: 1px solid #D5DAD4 !important; | |
| } | |
| /* ββ Expanders ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stExpander"] { | |
| border: 1px solid #D5DAD4 !important; | |
| border-radius: 8px !important; | |
| box-shadow: none !important; | |
| } | |
| /* ββ Inner tabs (e.g. Numbers chart tabs) βββββββββββββββββββββββββββββ */ | |
| [data-testid="stTabs"] [role="tab"] { | |
| font-size: 0.82rem !important; | |
| font-weight: 500 !important; | |
| color: #3E4A44 !important; | |
| } | |
| [data-testid="stTabs"] [role="tab"][aria-selected="true"] { | |
| color: #15201B !important; | |
| } | |
| [data-testid="stTabs"] [role="tablist"] { | |
| border-bottom: 1px solid #D5DAD4 !important; | |
| } | |
| /* ββ Captions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .stCaption { color: #8C968F !important; font-size: 0.78rem !important; } | |
| /* ββ Alerts / info boxes ββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stAlert"] { | |
| border-radius: 8px !important; | |
| } | |
| /* ββ Sidebar nav group expanders (deep-dive accordion) ββββββββββββββββ */ | |
| [data-testid="stSidebar"] [data-testid="stExpander"] { | |
| border: none !important; | |
| border-top: 1px solid #D5DAD4 !important; | |
| border-radius: 0 !important; | |
| box-shadow: none !important; | |
| background: transparent !important; | |
| margin-top: 2px !important; | |
| } | |
| [data-testid="stSidebar"] [data-testid="stExpander"] summary { | |
| font-size: 0.6rem !important; | |
| font-weight: 700 !important; | |
| text-transform: uppercase !important; | |
| letter-spacing: 0.1em !important; | |
| color: #8C968F !important; | |
| padding: 6px 2px !important; | |
| } | |
| [data-testid="stSidebar"] [data-testid="stExpander"] summary:hover { | |
| color: #3E4A44 !important; | |
| } | |
| [data-testid="stSidebar"] [data-testid="stExpander"] summary svg { | |
| width: 12px !important; | |
| height: 12px !important; | |
| color: #8C968F !important; | |
| } | |
| /* ββ Content max-width ββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .main .block-container { | |
| max-width: 1280px !important; | |
| padding-top: 24px !important; | |
| } | |
| /* ββ Prose width (text blocks) βββββββββββββββββββββββββββββββββββββββββββ */ | |
| .primer-prose { | |
| max-width: 700px; | |
| } | |
| /* ββ Sticky summary bar βββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .primer-sticky-bar { | |
| position: sticky; | |
| top: 0; | |
| z-index: 50; | |
| background: rgba(245,246,243,0.88); | |
| backdrop-filter: blur(6px); | |
| -webkit-backdrop-filter: blur(6px); | |
| border-bottom: 1px solid #D5DAD4; | |
| padding: 10px 0; | |
| margin: 0 0 16px; | |
| } | |
| /* ββ Chat messages ββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stChatMessage"] { | |
| border-radius: 10px !important; | |
| border: 1px solid #D5DAD4 !important; | |
| background: #F5F6F3 !important; | |
| padding: 12px 14px !important; | |
| margin-bottom: 8px !important; | |
| box-shadow: 0 1px 2px rgba(21,32,27,0.05) !important; | |
| } | |
| [data-testid="stChatMessage"][data-message-author-role="user"] { | |
| background: #F5F6F3 !important; | |
| border-color: #D5DAD4 !important; | |
| } | |
| [data-testid="stChatMessage"][data-message-author-role="assistant"] { | |
| background: #DCEBE3 !important; | |
| border-color: #1E6B55 !important; | |
| } | |
| /* ββ Chat input βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| [data-testid="stChatInput"] { | |
| border-radius: 10px !important; | |
| border: 1px solid #D5DAD4 !important; | |
| box-shadow: 0 1px 3px rgba(21,32,27,0.06) !important; | |
| } | |
| [data-testid="stChatInput"]:focus-within { | |
| border-color: #1E6B55 !important; | |
| box-shadow: 0 0 0 3px rgba(30,107,85,0.18) !important; | |
| } | |
| [data-testid="stChatInput"] textarea { | |
| font-family: "IBM Plex Sans", -apple-system, "Segoe UI", Helvetica, Arial, sans-serif !important; | |
| font-size: 0.9rem !important; | |
| color: #15201B !important; | |
| } | |
| /* ββ Widget chrome ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Streamlit paints a handful of widgets from its own theme rather than | |
| from our tokens. Those are pinned here so no widget arrives in a | |
| colour the note does not contain. */ | |
| /* Select / multiselect / number inputs */ | |
| [data-baseweb="select"] > div, | |
| [data-testid="stSelectbox"] div[role="combobox"], | |
| [data-testid="stNumberInput"] input { | |
| background-color: #ECEEE9 !important; | |
| border-color: #D5DAD4 !important; | |
| color: #15201B !important; | |
| } | |
| [data-baseweb="popover"] [role="listbox"], | |
| [data-baseweb="menu"] { | |
| background-color: #ECEEE9 !important; | |
| border: 1px solid #D5DAD4 !important; | |
| } | |
| [data-baseweb="menu"] li:hover { background-color: #ECEEE9 !important; } | |
| /* Text input placeholders */ | |
| [data-testid="stTextInput"] input::placeholder, | |
| [data-testid="stChatInput"] textarea::placeholder { | |
| color: #8C968F !important; | |
| } | |
| /* Inline code + code blocks */ | |
| code, [data-testid="stCode"], pre { | |
| background-color: #ECEEE9 !important; | |
| color: #134A3B !important; | |
| } | |
| /* Links β emerald, never the browser default blue */ | |
| a, a:visited { color: #1E6B55 !important; } | |
| a:hover { color: #134A3B !important; } | |
| /* Tables / dataframes */ | |
| [data-testid="stDataFrame"], [data-testid="stTable"] { | |
| background-color: #F5F6F3 !important; | |
| } | |
| [data-testid="stDataFrame"] * { border-color: #D5DAD4 !important; } | |
| /* Alerts β Streamlit ships its own pastels; restate them as the note's tints */ | |
| [data-testid="stAlert"] { border: 1px solid #D5DAD4 !important; } | |
| [data-testid="stAlertContentInfo"] { background-color: #ECEEE9 !important; color: #3E4A44 !important; } | |
| [data-testid="stAlertContentSuccess"] { background-color: #DCEBE3 !important; color: #134A3B !important; } | |
| [data-testid="stAlertContentWarning"] { background-color: #F4E8D2 !important; color: #B8791F !important; } | |
| [data-testid="stAlertContentError"] { background-color: #F3DDD8 !important; color: #A63A2B !important; } | |
| /* Active tab underline β Streamlit draws it in primaryColor */ | |
| [data-testid="stTabs"] [role="tab"][aria-selected="true"]::after, | |
| [data-testid="stTabs"] [data-baseweb="tab-highlight"] { | |
| background-color: #1E6B55 !important; | |
| } | |
| /* Spinner + progress use primaryColor too */ | |
| [data-testid="stSpinner"] i { border-top-color: #1E6B55 !important; } | |
| /* Scrollbars β drawn in the rule colour so they read as part of the page */ | |
| * { scrollbar-color: #8C968F #F5F6F3; scrollbar-width: thin; } | |
| ::-webkit-scrollbar { width: 10px; height: 10px; } | |
| ::-webkit-scrollbar-track { background: #F5F6F3; } | |
| ::-webkit-scrollbar-thumb { | |
| background: #8C968F; border-radius: 6px; border: 2px solid #F5F6F3; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { background: #8C968F; } | |
| /* ββ Inline "show the rest" disclosures (redline passages) ββββββββββ */ | |
| details > summary { list-style: none; } | |
| details > summary::-webkit-details-marker { display: none; } | |
| details > summary::marker { content: ""; } | |
| details[open] > summary .primer-more { display: none; } | |
| details > summary:hover .primer-more { color: #1E6B55 !important; } | |
| /* Tell the UA the page is light so form controls, selection and | |
| autofill are drawn to match rather than inverted. */ | |
| :root { color-scheme: light; } | |
| ::selection { background: rgba(30,107,85,0.22); color: #15201B; } | |
| </style> | |
| """, | |
| ) | |