| from __future__ import annotations |
|
|
| from collections.abc import Iterable |
|
|
| import streamlit as st |
|
|
| CSS = """ |
| <style> |
| :root { |
| --app-bg: #0b1020; |
| --panel: #111827; |
| --border: rgba(148, 163, 184, 0.28); |
| --muted: #94a3b8; |
| } |
| |
| .stApp { |
| background: linear-gradient(180deg, #08111f 0%, #0b1020 45%, #0f172a 100%); |
| } |
| |
| .block-container { |
| max-width: 1480px; |
| padding-top: 1.25rem; |
| padding-bottom: 3rem; |
| } |
| |
| [data-testid="stSidebar"] { |
| background: #0b1220; |
| border-right: 1px solid var(--border); |
| } |
| |
| h1, h2, h3 { |
| letter-spacing: -0.02em; |
| } |
| |
| [data-testid="stMetric"] { |
| background: rgba(15, 23, 42, 0.72); |
| border: 1px solid var(--border); |
| border-radius: 16px; |
| padding: 14px 14px 12px 14px; |
| } |
| |
| [data-testid="stDataFrame"] { |
| border-radius: 12px; |
| } |
| |
| .small-note { |
| color: var(--muted); |
| font-size: 0.92rem; |
| line-height: 1.5; |
| } |
| </style> |
| """ |
|
|
|
|
| def inject_css() -> None: |
| """Inject static CSS only; do not interpolate user or dataset values into this markup.""" |
| st.markdown(CSS, unsafe_allow_html=True) |
|
|
|
|
| def badge(text: str, kind: str = "info") -> str: |
| """Return a plain-text badge label for native Streamlit rendering.""" |
| prefix = { |
| "good": "✅", |
| "warn": "⚠️", |
| "bad": "🚨", |
| "info": "ℹ️", |
| }.get(kind, "•") |
| return f"{prefix} {text}" |
|
|
|
|
| def hero(title: str, subtitle: str, badges: Iterable[str] | None = None) -> None: |
| st.title(title) |
| st.markdown(subtitle) |
| if badges: |
| st.caption(" · ".join(str(item) for item in badges)) |
| st.divider() |
|
|
|
|
| def decision_strip(items: list[tuple[str, str, str]]) -> None: |
| """Render the decision brief with native Streamlit elements. |
| |
| This keeps the decision brief stable across Streamlit versions. |
| """ |
| st.subheader("Decision brief") |
| cols = st.columns(len(items)) |
| for col, (label, value, hint) in zip(cols, items, strict=False): |
| with col: |
| with st.container(border=True): |
| st.caption(label) |
| st.markdown(f"**{value}**") |
| st.caption(hint) |
|
|
|
|
| def metric_card(label: str, value: str, caption: str, status: str = "info") -> None: |
| st.metric(label=label, value=value) |
| st.caption(f"{status.title()} · {caption}") |
|
|
|
|
| def status_kind(status: str) -> str: |
| s = status.lower() |
| if any(x in s for x in ["stable", "good", "healthy", "pass"]): |
| return "good" |
| if any(x in s for x in ["risk", "high", "bad", "fail"]): |
| return "bad" |
| if any(x in s for x in ["watch", "review", "warn"]): |
| return "warn" |
| return "info" |
|
|
|
|
| def section_title(title: str, subtitle: str = "", chip: str = "") -> None: |
| label = f" — {chip}" if chip else "" |
| st.markdown(f"## {title}{label}") |
| if subtitle: |
| st.caption(subtitle) |
|
|
|
|
| def callout(kind: str, title: str, body: str) -> None: |
| text = f"**{title}**\n\n{body}" |
| if kind in {"good", "success", "pass"}: |
| st.success(text) |
| elif kind in {"warn", "warning", "review"}: |
| st.warning(text) |
| elif kind in {"bad", "error", "risk", "fail"}: |
| st.error(text) |
| else: |
| st.info(text) |
|
|
|
|
| def trace_box(title: str, body: str) -> None: |
| with st.container(border=True): |
| st.markdown(f"**{title}**") |
| st.write(body) |
|
|