From 2501119ac207e588b8f7faec39a42f93b34173db Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 19 May 2026 00:21:52 +0000 Subject: [PATCH] feat(ui): replace Fraunces with Geist per geist_spec.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switches the type system to the single-family Geist spec referenced in ``Business/DataTools/geist_spec.md`` and the matching ``datatools_layout_redesign2.html`` mockup. Editorial-serif headings are out; the product now reads as modern SaaS-tool typography per the spec's positioning note (§10). src/gui/theme.py (new) Implements geist_spec.md §3 verbatim — preconnect + Google Fonts link for Geist (400/500/600/700) and Geist Mono (400/500), the canonical ``:root`` token table (§7) plus severity extensions, and the type scale (§4): h1 32/600/-0.035em, h2 22/600/-0.025em, h3 18/500/-0.018em, h4 15/500/-0.012em, body 14/400, caption 12.5/400, mono 0.92× ss02. ``apply_theme()`` is the single entry point. Two deviations from the spec, both anticipated by spec §6.1: - ``font-family: var(--font-sans) !important`` on the base rule. Streamlit applies ``font-family: "Source Sans"`` directly to ``[data-testid="stMarkdownContainer"]`` and a few widget wrappers at equal-or-higher specificity than the spec's selector list, so plain inheritance loses the cascade. - The base selector list explicitly enumerates ``stSidebarNav``, ``stMarkdownContainer``, ``stVerticalBlock`` and a few siblings so Streamlit's per-widget font reset doesn't reach descendant text. src/gui/components/_legacy.py - ``_DESIGN_TOKENS_CSS`` no longer redeclares fonts or the heading rules — those are theme.py's job (spec §9 says the spec is type-only; everything below is component chrome). - Token references switched from ``--dt-*`` to the spec names (``--ink``, ``--bg``, ``--surface``, ``--border``, ``--accent``, ``--font-sans``, ``--font-mono``, …). - Sidebar section-label rule tightened to 11.5px / 500 to match the "Eyebrow" row in spec §4. - Primary-button text color now also targets every descendant (``button[kind="primary"] *``) so the inner ``stMarkdownContainer > p`` doesn't pick up ``color: var(--ink)`` from the base rule and render near-invisible ink-on-ink. - ``hide_streamlit_chrome`` now calls ``apply_theme`` before injecting component CSS so the base tokens are defined first. Acceptance criteria from spec §8 verified at 1920×1050: - h1 computes ``font-family: Geist``, ``font-weight: 600``, ``letter-spacing: -1.12px`` (= 32px × -0.035em), size ``32px``. - Body ``

`` inside ``stMarkdownContainer``: Geist 400 / 14px. - Caption: Geist 400 / 12.5px. - Inline mono filenames: Geist Mono in accent-fill chip. - No Source Sans Pro leaks into any text the user reads. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gui/components/_legacy.py | 260 +++++++++++++++------------------- src/gui/theme.py | 184 ++++++++++++++++++++++++ 2 files changed, 296 insertions(+), 148 deletions(-) create mode 100644 src/gui/theme.py diff --git a/src/gui/components/_legacy.py b/src/gui/components/_legacy.py index 82cf53a..cb64e08 100644 --- a/src/gui/components/_legacy.py +++ b/src/gui/components/_legacy.py @@ -137,75 +137,55 @@ hr { margin-top: 0.4rem !important; margin-bottom: 0.4rem !important; } """ -# Warm editorial palette + typography, lifted from the -# ``datatools_layout_redesign.html`` mockup. Applied on every page via -# ``hide_streamlit_chrome``. Tokens are scoped through CSS custom -# properties so individual rules read cleanly and a future tweak only -# has to touch the ``:root`` block. +# Component-level styling that rides on top of the canonical typography +# + color tokens declared in ``src/gui/theme.py`` (``apply_theme``). +# This block does NOT redeclare the type scale or the ``--font-sans`` / +# ``--ink`` etc. variables — that is theme.py's job per +# ``geist_spec.md`` §9 ("Out of scope: button/input/widget styling. +# Type only."). Everything below extends the spec with widget chrome +# (buttons, sidebar, file uploader, expanders, alerts) that the mockup +# wants but the spec leaves unowned. +# +# Reads from theme.py's :root: ``--font-sans``, ``--font-mono``, +# ``--ink``, ``--ink-secondary``, ``--ink-tertiary``, ``--bg``, +# ``--surface``, ``--surface-hover``, ``--border``, ``--border-strong``, +# ``--accent``, ``--accent-hover``, ``--accent-fill``, the severity +# extensions ``--warn(-fill)`` / ``--info(-fill)`` / ``--success(-fill)`` +# / ``--danger(-fill)``, and the radius scale ``--r-sm/md/lg``. _DESIGN_TOKENS_CSS = """ @@ -526,6 +485,11 @@ def hide_streamlit_chrome(*, gate_license: bool = True) -> None: can render its own form without recursion. """ st.markdown(_HIDE_CHROME_CSS, unsafe_allow_html=True) + # ``apply_theme`` injects the canonical typography + color tokens + # (geist_spec.md §3). Must run BEFORE ``_DESIGN_TOKENS_CSS`` so the + # component CSS below can read its ``--font-sans`` / ``--ink`` etc. + from src.gui.theme import apply_theme + apply_theme() st.markdown(_DESIGN_TOKENS_CSS, unsafe_allow_html=True) # ``st.markdown`` doesn't execute embedded scripts; ship the # Upload→Import rewriter through an iframe component the same way diff --git a/src/gui/theme.py b/src/gui/theme.py new file mode 100644 index 0000000..7bf8725 --- /dev/null +++ b/src/gui/theme.py @@ -0,0 +1,184 @@ +"""Typography + color theme injected into every Streamlit page. + +Implements ``geist_spec.md`` §3 verbatim. Single source of truth for +the type scale and the base color tokens. Component-specific styling +(buttons, sidebar, file uploader, expanders, alerts, …) lives in +``components/_legacy.py:_DESIGN_TOKENS_CSS`` and reads the +``--font-sans`` / ``--font-mono`` / ``--ink`` / ``--bg`` / ``--surface`` +/ ``--border`` / ``--accent`` / ``--accent-fill`` variables this +module declares. + +The spec wants this called at the top of every page right after +``st.set_page_config()``. In this codebase every page already calls +``hide_streamlit_chrome()`` for that purpose, so :func:`apply_theme` +is invoked from there — one place, every page. +""" + +from __future__ import annotations + +import streamlit as st + + +_GOOGLE_FONTS_URL = ( + "https://fonts.googleapis.com/css2" + "?family=Geist:wght@400;500;600;700" + "&family=Geist+Mono:wght@400;500" + "&display=swap" +) + + +# Spec §3 + §7. Heading rules track the canonical type scale in spec §4: +# h1 32/600/-0.035em, h2 22/600/-0.025em, h3 18/500/-0.018em, h4 15/500/ +# -0.012em, body 14/400, caption 12.5/400, mono inherits × 0.92. +# The severity extension tokens (--warn / --info / --success / --danger +# + their fills) are NOT in spec §7 but live here so the component CSS +# in _legacy.py has a single :root table to read from. +_CSS = f""" + + + + + +""" + + +def apply_theme() -> None: + """Inject typography + color CSS. Call once at the top of every page, + immediately after :func:`streamlit.set_page_config`. + """ + st.markdown(_CSS, unsafe_allow_html=True)