"""Shared visual theme — the "Spectrum" design system. Loads ``app/assets/design_system.css`` (the full token + component CSS from the design file), injects it once per render, and provides helper functions for emitting the correct HTML markup so the CSS classes actually match. Key architecture: Streamlit generates its own HTML, so Spectrum's component classes (.card, .screen-head, .verdict, .proposal-doc, etc.) only take effect when we explicitly emit matching HTML via st.markdown(unsafe_allow_html=True). Streamlit's functional widgets (buttons, file_uploader, text_input) are kept for interactivity but restyled via the CSS widget-override section. """ from __future__ import annotations from functools import lru_cache import streamlit as st import streamlit.components.v1 as components from app.paths import resource_path # Status-chip kinds → CSS class (legacy, used by a few callers). _CHIP_KINDS = { "ready": "ups-chip-ready", "info": "ups-chip-info", "missing": "ups-chip-missing", "neutral": "ups-chip-neutral", } # Per-step signature color: (H, C, H2) in oklch. Light default. STEP_COLORS: dict[str, tuple[float, float, float]] = { "setup": (286, 0.21, 312), "dossier": (248, 0.18, 226), "screenshot": (52, 0.17, 32), "confirmation": (352, 0.22, 330), "analysis": (352, 0.22, 330), "proposal": (168, 0.16, 150), } _DEFAULT_ACCENT = STEP_COLORS["setup"] STEP_EYEBROWS = { "setup": "Step 01 — Setup", "dossier": "Step 02 — Dossier", "screenshot": "Step 03 — Job Screenshot", "analysis": "Step 04 — Analysis", "proposal": "Step 05 — Proposal", } _FONTS_LINK = ( '' '' '' ) # Extra CSS layered on top of design_system.css to bridge Streamlit's DOM # with the design's component classes. _BRIDGE_CSS = """ """ @lru_cache(maxsize=1) def _design_css() -> str: """Read the Spectrum design system CSS once (cached), stripping stray style tags.""" try: path = resource_path("app", "assets", "design_system.css") css = path.read_text(encoding="utf-8") except Exception: return "" return css.replace("", "").replace("", unsafe_allow_html=True) st.markdown(_BRIDGE_CSS, unsafe_allow_html=True) def apply_step_accent(step_key: str, *, dark: bool = False) -> None: """Re-tint the whole screen to the current step's signature hue + theme. The design tokens live under ``[data-theme]`` on the parent ````, so this bridges from the component iframe to set it. It is hardened against the two things that made dark mode feel flaky: the parent DOM not being ready when the iframe first runs (so it retries briefly), and a write throwing in some embedding contexts (so it is wrapped in try/catch). It also sets ``color-scheme`` so native widgets (uploader, inputs) adopt the dark look. """ h, c, h2 = STEP_COLORS.get(step_key, _DEFAULT_ACCENT) theme_val = "dark" if dark else "light" components.html( f"""""", height=0, ) # ── Screen header ────────────────────────────────────────────────────────── def screen_head(step_key: str, title: str, subtitle: str) -> None: """Render the eyebrow + title + subtitle header matching the design prototype.""" eyebrow = STEP_EYEBROWS.get(step_key, "") st.markdown( f"""
{subtitle}
{body}
{escaped}
" paras_html = "".join(_para_html(p) for p in paragraphs) grounded = ( f'{title}
{subtitle}
{text}
', unsafe_allow_html=True) def sidebar_title(text: str) -> None: st.markdown(f'{text}
', unsafe_allow_html=True)