feat(bcda): fhirworx house style for the manager — theme file, brand masthead and eyebrows, favicon
Some checks failed
CI / lint (push) Successful in 39s
CI / test (push) Failing after 2m8s
CI / notebooks-smoke (push) Successful in 1m36s
Infra CI / zotero (push) Successful in 15s
Infra CI / notebooks (push) Successful in 52s
Infra CI / docs (push) Successful in 1m31s
Infra CI / api (push) Successful in 1m2s
Infra CI / llm (push) Successful in 48s
Infra CI / mc (push) Failing after 57s
Infra CI / bcda (push) Successful in 1m29s
CI / lint (pull_request) Successful in 27s
CI / notebooks-smoke (pull_request) Successful in 1m46s
CI / test (pull_request) Failing after 3m23s
Infra CI / notebooks (pull_request) Successful in 1m27s
Infra CI / zotero (pull_request) Successful in 16s
Infra CI / docs (pull_request) Successful in 17s
Infra CI / bcda (pull_request) Successful in 17s
Infra CI / api (pull_request) Successful in 19s
Infra CI / mc (pull_request) Failing after 35s
Infra CI / llm (pull_request) Successful in 16s
Some checks failed
CI / lint (push) Successful in 39s
CI / test (push) Failing after 2m8s
CI / notebooks-smoke (push) Successful in 1m36s
Infra CI / zotero (push) Successful in 15s
Infra CI / notebooks (push) Successful in 52s
Infra CI / docs (push) Successful in 1m31s
Infra CI / api (push) Successful in 1m2s
Infra CI / llm (push) Successful in 48s
Infra CI / mc (push) Failing after 57s
Infra CI / bcda (push) Successful in 1m29s
CI / lint (pull_request) Successful in 27s
CI / notebooks-smoke (pull_request) Successful in 1m46s
CI / test (pull_request) Failing after 3m23s
Infra CI / notebooks (pull_request) Successful in 1m27s
Infra CI / zotero (pull_request) Successful in 16s
Infra CI / docs (pull_request) Successful in 17s
Infra CI / bcda (pull_request) Successful in 17s
Infra CI / api (pull_request) Successful in 19s
Infra CI / mc (pull_request) Failing after 35s
Infra CI / llm (pull_request) Successful in 16s
src/bcda/ui/theme.toml is a Streamlit theme file carrying the dashboard
tokens from assets/css/dashboard.css (cream paper, near-black ink, navy
primary and sidebar, warm beige surfaces, 2px radius, Playfair Display /
Source Serif 4 / JetBrains Mono). `python -m bcda.ui` passes it as
--theme.base unless the caller set one, so the container and
`stack bcda ui` look the same without a .streamlit/config.toml per cwd;
the image's streamlit.toml keeps only server settings.
bcda.ui.brand adds what a theme file cannot: the Google Fonts import,
the dashboard masthead (rule, display title, monospace eyebrow) at the
top of the sidebar, and brand.title() eyebrows above every page title.
app.py renders its own page links after the masthead (navigation
position="hidden") so the masthead comes first as on the dashboard. The
favicon is the repo's assets/icons/fav32.png, baked into the image.
Family names in the theme file stay double-quoted: Streamlit 1.66 emits
the body font unquoted, and an unquoted family with a digit ("Source
Serif 4") is an invalid font-family, so the browser silently fell back
to its default serif while the headings (quoted on their path) were
fine. Found by rendering the rolled container headlessly on the compose
network and reading computed styles.
tests/bcda/test_ui_brand.py pins the theme's hexes and font stacks to
dashboard.css and renders inject/masthead/title through AppTest;
test_ui_main covers the --theme.base handling.
refs #1139
This commit is contained in:
@@ -35,3 +35,7 @@ mirrors/
|
||||
# Streamlit server config COPYed by bcda.Dockerfile (repo-root context).
|
||||
!infra/images/bcda/
|
||||
!infra/images/bcda/**
|
||||
# Favicon COPYed by bcda.Dockerfile; dir entries first so BuildKit can descend.
|
||||
!assets/
|
||||
!assets/icons/
|
||||
!assets/icons/fav32.png
|
||||
|
||||
@@ -86,3 +86,9 @@ Tests: service with `httpx.MockTransport`, runner for thread-routed logs and sta
|
||||
### `/metrics` (2026-10-25)
|
||||
|
||||
`bcda.ui.metrics` follows the #579 convention (`perf.meter` → OTel meter + Prometheus reader when `STACK_TELEMETRY` is on, a no-op `MockMeter` otherwise). Streamlit cannot mount a route, so the manager publishes the process's Prometheus registry from a second port (`BCDA_METRICS_PORT`, 8502) via `prometheus_client.start_http_server`, and Prometheus scrapes `bcda:8502/metrics` directly like `api` and `llm`. Instruments: `stack_bcda_runs_total{kind,state}`, `stack_bcda_runs_inflight{kind}`, `stack_bcda_run_seconds{kind}`, `stack_bcda_page_views_total{page}`, and two observable gauges read at scrape time, `stack_bcda_store_jobs{status}` and `stack_bcda_store_bytes`. Run kinds are a closed set (export, adopt, load, vendor_check, other). The image installs the `perf` extra; the compose service joins the observability network and sets `OTEL_SERVICE_NAME=bcda`.
|
||||
|
||||
### Published and styled (2026-10-25)
|
||||
|
||||
Reachability needs more than `stack.toml` + Traefik + CoreDNS: the Cloudflare tunnel runs on a remote ingress list that overrides the wildcard in `infra/cloudflared/config.yml` (an unlisted host answers 530 at the edge), and the public CNAME to the tunnel is created from the same list. Both come from `SUBDOMAINS` in `dev/scripts/bootstrap_sso.py`, which gained `bcda` and a `--dns-only` mode that does just those two writes with a token holding Account:Cloudflare Tunnel:Edit and Zone:DNS:Edit for fhirworx.io. Resolvers that had asked before the record existed cache NXDOMAIN for the zone's negative TTL (1800 s).
|
||||
|
||||
House style: `src/bcda/ui/theme.toml` is a Streamlit theme file carrying the dashboard tokens (`assets/css/dashboard.css`: cream paper, near-black ink, navy primary and sidebar, warm beige surfaces, 2px radius, Playfair Display / Source Serif 4 / JetBrains Mono). `python -m bcda.ui` passes it as `--theme.base` so the container and `stack bcda ui` match without a per-cwd `.streamlit/config.toml`. `bcda.ui.brand` adds what a theme file cannot: the Google Fonts import, the dashboard masthead in the sidebar (rule, display title, monospace eyebrow) and `brand.title()` eyebrows above every page title; the favicon is the repo's `assets/icons/fav32.png`, baked into the image. `tests/bcda/test_ui_brand.py` fails if the theme's hexes or font stacks drift from `dashboard.css`.
|
||||
|
||||
@@ -23,9 +23,13 @@ ENV UV_PYTHON_PREFERENCE=only-system \
|
||||
RUN uv sync --no-dev --extra cli --extra bcda --extra perf && uv pip install -e .
|
||||
|
||||
COPY stack.toml ./
|
||||
# Favicon for st.set_page_config — bcda.ui.brand resolves assets/icons/fav32.png
|
||||
# relative to the package root (/app here, the repo root in a checkout).
|
||||
COPY assets/icons/fav32.png assets/icons/fav32.png
|
||||
|
||||
# Streamlit reads its server settings from here (headless, no usage
|
||||
# stats, bound to all interfaces, websocket-friendly behind Traefik).
|
||||
# stats, bound to all interfaces). The fhirworx theme is not here: the
|
||||
# entrypoint passes src/bcda/ui/theme.toml as --theme.base.
|
||||
COPY infra/images/bcda/streamlit.toml /app/.streamlit/config.toml
|
||||
|
||||
# 8501 Streamlit · 8502 Prometheus registry (/metrics, perf meter via STACK_TELEMETRY)
|
||||
|
||||
@@ -10,6 +10,3 @@ runOnSave = false
|
||||
|
||||
[browser]
|
||||
gatherUsageStats = false
|
||||
|
||||
[theme]
|
||||
base = "light"
|
||||
|
||||
@@ -9,6 +9,9 @@ runs land in the same registry.
|
||||
|
||||
python -m bcda.ui # :8501, metrics on BCDA_METRICS_PORT
|
||||
python -m bcda.ui --server.port 9000 # extra args go to `streamlit run`
|
||||
|
||||
The fhirworx theme file (``bcda/ui/theme.toml``) is passed as
|
||||
``--theme.base`` unless the arguments already set one.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -20,10 +23,15 @@ from pathlib import Path
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
from streamlit.web import cli as stcli
|
||||
|
||||
from bcda.ui import app, metrics
|
||||
from bcda.ui import app, brand, metrics
|
||||
|
||||
metrics.serve()
|
||||
args = list(sys.argv[1:] if argv is None else argv)
|
||||
if not any(a == "--theme.base" or a.startswith("--theme.base=") for a in args):
|
||||
# The fhirworx look (bcda/ui/theme.toml) travels with the package, so
|
||||
# the container and `stack bcda ui` render the same without a
|
||||
# .streamlit/config.toml in every cwd. A caller's --theme.base wins.
|
||||
args = ["--theme.base", str(brand.THEME_PATH), *args]
|
||||
sys.argv = ["streamlit", "run", str(Path(app.__file__)), *args]
|
||||
try:
|
||||
stcli.main()
|
||||
|
||||
@@ -8,7 +8,7 @@ from __future__ import annotations
|
||||
|
||||
import streamlit as st
|
||||
|
||||
from bcda.ui import metrics, views
|
||||
from bcda.ui import brand, metrics, views
|
||||
|
||||
PAGES = [
|
||||
st.Page(views.connect, title="Connect", icon="🔌", default=True),
|
||||
@@ -22,17 +22,24 @@ PAGES = [
|
||||
|
||||
|
||||
def main() -> None:
|
||||
st.set_page_config(page_title="BCDA manager", page_icon="🏥", layout="wide")
|
||||
st.set_page_config(page_title="BCDA manager", page_icon=brand.icon(), layout="wide")
|
||||
brand.inject() # web fonts + masthead/eyebrow rules; colours come from theme.toml
|
||||
metrics.serve() # /metrics on BCDA_METRICS_PORT when STACK_TELEMETRY is on; no-op otherwise
|
||||
conn = st.session_state.get("conn")
|
||||
# position="hidden" keeps Streamlit from drawing its own nav above our
|
||||
# sidebar content; the masthead goes first (as on the dashboard) and the
|
||||
# page links follow, in PAGES order.
|
||||
page = st.navigation(PAGES, position="hidden")
|
||||
with st.sidebar:
|
||||
st.markdown("### BCDA manager")
|
||||
brand.masthead()
|
||||
for p in PAGES:
|
||||
st.page_link(p, label=p.title, icon=p.icon)
|
||||
st.caption(
|
||||
f"**{conn.label}** · {conn.environment} · {conn.version}"
|
||||
if conn
|
||||
else "not connected"
|
||||
)
|
||||
st.navigation(PAGES).run()
|
||||
page.run()
|
||||
|
||||
|
||||
if (
|
||||
|
||||
93
src/bcda/ui/brand.py
Normal file
93
src/bcda/ui/brand.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Fhirworx house style for the BCDA manager.
|
||||
|
||||
Colour, radius and font *families* come from :data:`THEME_PATH`, a
|
||||
Streamlit theme file that ``python -m bcda.ui`` passes as
|
||||
``--theme.base`` (see :mod:`bcda.ui.__main__`). What a theme file cannot
|
||||
do — load the web fonts, draw the dashboard's masthead rule and
|
||||
monospace eyebrows — :func:`inject` adds as one ``<style>`` block, and
|
||||
:func:`title` / :func:`masthead` are the two page furniture helpers the
|
||||
views use. All values are read from the theme file so there is a single
|
||||
source for the look; ``tests/bcda/test_ui_brand.py`` keeps that file in
|
||||
step with ``assets/css/dashboard.css``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import tomllib
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import streamlit as st
|
||||
|
||||
THEME_PATH = Path(__file__).with_name("theme.toml")
|
||||
# Repo-root asset (``src/bcda/ui`` → parents[3]); the image COPYs it to the
|
||||
# same relative place under /app. Falls back to an emoji when absent.
|
||||
ICON_PATH = Path(__file__).resolve().parents[3] / "assets" / "icons" / "fav32.png"
|
||||
ICON_FALLBACK = "🏥"
|
||||
FONTS_URL = (
|
||||
"https://fonts.googleapis.com/css2"
|
||||
"?family=Playfair+Display:wght@600;700;800"
|
||||
"&family=Source+Serif+4:ital,wght@0,400;0,600;1,400"
|
||||
"&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def theme() -> dict[str, Any]:
|
||||
"""The ``[theme]`` table of :data:`THEME_PATH` (sidebar nested under ``sidebar``)."""
|
||||
return tomllib.loads(THEME_PATH.read_text())["theme"]
|
||||
|
||||
|
||||
def icon() -> str:
|
||||
return str(ICON_PATH) if ICON_PATH.is_file() else ICON_FALLBACK
|
||||
|
||||
|
||||
def css() -> str:
|
||||
t = theme()
|
||||
side = t["sidebar"]
|
||||
return f"""
|
||||
@import url('{FONTS_URL}');
|
||||
:root {{
|
||||
--fw-ink: {t["textColor"]}; --fw-muted: {t["grayColor"]}; --fw-rule: {t["borderColor"]};
|
||||
--fw-mono: {t["codeFont"]}; --fw-display: {t["headingFont"]};
|
||||
}}
|
||||
.fw-eyebrow {{
|
||||
display: block; font-family: var(--fw-mono); font-size: .7rem; font-weight: 500;
|
||||
text-transform: uppercase; letter-spacing: .12em; color: var(--fw-muted);
|
||||
margin: 0 0 -.35rem;
|
||||
}}
|
||||
.fw-rule {{ border: 0; border-top: 3px solid var(--fw-ink); margin: 0 0 .6rem; }}
|
||||
.fw-rule-thin {{ border: 0; border-top: 1px solid var(--fw-rule); margin: .6rem 0 .2rem; }}
|
||||
.fw-mast {{ padding: .4rem 0 .2rem; }}
|
||||
.fw-mast .fw-rule {{ border-top-color: {side["textColor"]}; }}
|
||||
.fw-mast .fw-rule-thin {{ border-top-color: {side["borderColor"]}; }}
|
||||
.fw-mast h1 {{
|
||||
font-family: var(--fw-display); font-weight: 800; font-size: 1.65rem; line-height: 1.1;
|
||||
letter-spacing: -.02em; color: {side["primaryColor"]}; margin: 0; padding: 0;
|
||||
}}
|
||||
.fw-mast .fw-eyebrow {{ color: {side["textColor"]}; margin: .25rem 0 0; }}
|
||||
[data-testid="stAppViewContainer"] h1 {{ letter-spacing: -.02em; }}
|
||||
"""
|
||||
|
||||
|
||||
def inject() -> None:
|
||||
"""Load the web fonts and the masthead/eyebrow rules once per page run."""
|
||||
st.html(f"<style>{css()}</style>")
|
||||
|
||||
|
||||
def masthead(product: str = "Fhirworx", eyebrow: str = "bcda · manager") -> None:
|
||||
"""The dashboard masthead, sized for the sidebar: rule, display title, eyebrow."""
|
||||
st.html(
|
||||
'<div class="fw-mast"><div class="fw-rule"></div>'
|
||||
f"<h1>{html.escape(product)}</h1>"
|
||||
f'<span class="fw-eyebrow">{html.escape(eyebrow)}</span>'
|
||||
'<div class="fw-rule-thin"></div></div>'
|
||||
)
|
||||
|
||||
|
||||
def title(text: str, eyebrow: str) -> None:
|
||||
"""A page title with the dashboard's monospace eyebrow above it."""
|
||||
st.html(f'<span class="fw-eyebrow">{html.escape(eyebrow)}</span>')
|
||||
st.title(text)
|
||||
51
src/bcda/ui/theme.toml
Normal file
51
src/bcda/ui/theme.toml
Normal file
@@ -0,0 +1,51 @@
|
||||
# Fhirworx theme for the BCDA manager (Streamlit ≥ 1.46 theme file).
|
||||
#
|
||||
# Source of truth is the dashboard stylesheet, assets/css/dashboard.css —
|
||||
# the same tokens the Gitea and marimo themes derive from: cream paper,
|
||||
# near-black ink, deep-navy primary and sidebar, warm beige surfaces, a
|
||||
# 2px "civic forms" radius. Fonts are the editorial trio (Playfair
|
||||
# Display headings, Source Serif 4 body, JetBrains Mono for eyebrows and
|
||||
# code); bcda.ui.brand imports them from Google Fonts at page start.
|
||||
# tests/bcda/test_ui_brand.py keeps the hexes in step with dashboard.css.
|
||||
#
|
||||
# Loaded by `python -m bcda.ui` as --theme.base <this file>, so the
|
||||
# container and `stack bcda ui` look the same.
|
||||
|
||||
[theme]
|
||||
base = "light"
|
||||
primaryColor = "#1C2B3A"
|
||||
backgroundColor = "#F7F5F0"
|
||||
secondaryBackgroundColor = "#EDEBE6"
|
||||
textColor = "#1A1A18"
|
||||
linkColor = "#2D5F7F"
|
||||
linkUnderline = true
|
||||
borderColor = "#D4D0C8"
|
||||
dataframeBorderColor = "#D4D0C8"
|
||||
dataframeHeaderBackgroundColor = "#EDEBE6"
|
||||
codeBackgroundColor = "#EDEBE6"
|
||||
redColor = "#C0392B"
|
||||
greenColor = "#2E8B6E"
|
||||
orangeColor = "#C8702A"
|
||||
blueColor = "#2D5F7F"
|
||||
grayColor = "#6B6B68"
|
||||
# Family names stay double-quoted: Streamlit 1.66 passes the body font
|
||||
# through unquoted, and an unquoted family with a digit ("Source Serif 4")
|
||||
# is an invalid CSS font-family — the browser silently falls back to its
|
||||
# default serif. Quoted, the same stack applies and loads.
|
||||
font = '"Source Serif 4", Georgia, serif'
|
||||
headingFont = '"Playfair Display", Georgia, serif'
|
||||
headingFontWeights = [800, 700, 600]
|
||||
codeFont = '"JetBrains Mono", "Fira Code", monospace'
|
||||
baseRadius = "2px"
|
||||
buttonRadius = "2px"
|
||||
showWidgetBorder = true
|
||||
showSidebarBorder = false
|
||||
|
||||
[theme.sidebar]
|
||||
backgroundColor = "#1C2B3A"
|
||||
secondaryBackgroundColor = "#253748"
|
||||
textColor = "#B8C5D0"
|
||||
primaryColor = "#F0F4F8"
|
||||
linkColor = "#F0F4F8"
|
||||
borderColor = "#2E4359"
|
||||
codeBackgroundColor = "#253748"
|
||||
@@ -14,7 +14,7 @@ from typing import Any
|
||||
import polars as pl
|
||||
import streamlit as st
|
||||
|
||||
from bcda.ui import metrics
|
||||
from bcda.ui import brand, metrics
|
||||
from bcda.ui import service as svc
|
||||
from bcda.ui.runner import runner
|
||||
|
||||
@@ -83,7 +83,7 @@ def runs_panel() -> None:
|
||||
|
||||
def connect() -> None:
|
||||
metrics.page_view("connect")
|
||||
st.title("Connect")
|
||||
brand.title("Connect", "sandbox · credentials · server")
|
||||
st.caption(
|
||||
"BCDA v3 (v1/v2 end 2027-07-30). `/auth` and `/data` are shared by every version; "
|
||||
"only the `/api/<version>/` segment differs."
|
||||
@@ -172,7 +172,7 @@ def connect() -> None:
|
||||
|
||||
def exports() -> None:
|
||||
metrics.page_view("exports")
|
||||
st.title("Exports")
|
||||
brand.title("Exports", "bulk export · $export · _typeFilter")
|
||||
conn = _require_conn()
|
||||
if conn is None:
|
||||
return
|
||||
@@ -324,7 +324,7 @@ def exports() -> None:
|
||||
|
||||
def store() -> None:
|
||||
metrics.page_view("store")
|
||||
st.title("Store")
|
||||
brand.title("Store", "jobs · exports on disk")
|
||||
path = svc.store_path()
|
||||
st.caption(
|
||||
f"`{path}` — append-only `jobs.parquet` / `files.parquet`; the last row per key is the current state."
|
||||
@@ -373,7 +373,7 @@ def store() -> None:
|
||||
|
||||
def load() -> None:
|
||||
metrics.page_view("load")
|
||||
st.title("Flatten + load")
|
||||
brand.title("Flatten + load", "ndjson → parquet → replica")
|
||||
st.caption(
|
||||
"Newest export per resource type → `bcda.*` in aco.duckdb (v2 or v3 dialect per resource, "
|
||||
"streamed from multi-part ndjson) → connector pipeline → `input_layer.*` → replica republished."
|
||||
@@ -402,7 +402,7 @@ def load() -> None:
|
||||
|
||||
def data() -> None:
|
||||
metrics.page_view("data")
|
||||
st.title("Data")
|
||||
brand.title("Data", "replica · tables · sql")
|
||||
try:
|
||||
con = svc.replica_connect()
|
||||
except Exception as exc: # noqa: BLE001 — no replica yet
|
||||
@@ -450,7 +450,7 @@ def data() -> None:
|
||||
|
||||
def dictionary() -> None:
|
||||
metrics.page_view("dictionary")
|
||||
st.title("Data dictionary + CCLF crosswalk")
|
||||
brand.title("Data dictionary + CCLF crosswalk", "v3 data dictionary · cclf")
|
||||
try:
|
||||
con = svc.replica_connect()
|
||||
except Exception: # noqa: BLE001
|
||||
@@ -527,7 +527,7 @@ def dictionary() -> None:
|
||||
|
||||
def upstream() -> None:
|
||||
metrics.page_view("upstream")
|
||||
st.title("Upstream: CMSgov/bcda-app + bcda.cms.gov")
|
||||
brand.title("Upstream: CMSgov/bcda-app + bcda.cms.gov", "vendored sources · drift")
|
||||
status = svc.vendor_status()
|
||||
if not status.get("available"):
|
||||
st.info(
|
||||
|
||||
92
tests/bcda/test_ui_brand.py
Normal file
92
tests/bcda/test_ui_brand.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""bcda.ui.brand — the fhirworx look: theme file, fonts, masthead, eyebrow titles."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
from streamlit.testing.v1 import AppTest
|
||||
|
||||
from bcda.ui import brand
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DASHBOARD_CSS = ROOT / "assets" / "css" / "dashboard.css"
|
||||
|
||||
# theme.toml key → dashboard.css custom property it must equal.
|
||||
TOKEN_MAP = {
|
||||
("backgroundColor",): "--background",
|
||||
("textColor",): "--foreground",
|
||||
("primaryColor",): "--primary",
|
||||
("secondaryBackgroundColor",): "--secondary",
|
||||
("borderColor",): "--border",
|
||||
("grayColor",): "--muted-foreground",
|
||||
("redColor",): "--destructive",
|
||||
("greenColor",): "--chart-4",
|
||||
("orangeColor",): "--chart-5",
|
||||
("sidebar", "backgroundColor"): "--sidebar",
|
||||
("sidebar", "textColor"): "--sidebar-foreground",
|
||||
("sidebar", "secondaryBackgroundColor"): "--sidebar-accent",
|
||||
}
|
||||
FONT_MAP = {
|
||||
"font": "--font-body",
|
||||
"headingFont": "--font-display",
|
||||
"codeFont": "--font-mono",
|
||||
}
|
||||
|
||||
|
||||
def _css_tokens() -> dict[str, str]:
|
||||
root = DASHBOARD_CSS.read_text().split(":root", 1)[1].split("}", 1)[0]
|
||||
return {
|
||||
m.group(1): m.group(2).strip()
|
||||
for m in re.finditer(r"(--[\w-]+)\s*:\s*([^;]+);", root)
|
||||
}
|
||||
|
||||
|
||||
def _theme() -> dict:
|
||||
return tomllib.loads(brand.THEME_PATH.read_text())["theme"]
|
||||
|
||||
|
||||
def test_theme_file_tracks_dashboard_css() -> None:
|
||||
css, theme = _css_tokens(), _theme()
|
||||
for path, var in TOKEN_MAP.items():
|
||||
node = theme
|
||||
for key in path:
|
||||
node = node[key]
|
||||
assert node.upper() == css[var].upper(), f"{'.'.join(path)} vs {var}"
|
||||
for key, var in FONT_MAP.items():
|
||||
# exact stack, quotes included — Streamlit drops an unquoted "Source Serif 4"
|
||||
assert theme[key] == css[var], key
|
||||
assert theme[key].startswith('"'), key
|
||||
assert theme["base"] == "light" and theme["baseRadius"] == "2px"
|
||||
assert brand.theme() == theme # cached loader returns the same table
|
||||
|
||||
|
||||
def test_css_and_icon() -> None:
|
||||
css = brand.css()
|
||||
assert brand.FONTS_URL in css and ".fw-eyebrow" in css and ".fw-mast h1" in css
|
||||
for family in ("Playfair+Display", "Source+Serif+4", "JetBrains+Mono"):
|
||||
assert family in brand.FONTS_URL
|
||||
# the repo ships the favicon; the fallback is an emoji when it does not
|
||||
assert brand.icon() == str(brand.ICON_PATH) and brand.ICON_PATH.is_file()
|
||||
assert brand.ICON_FALLBACK == "🏥"
|
||||
|
||||
|
||||
def test_inject_masthead_and_title_render() -> None:
|
||||
at = AppTest.from_string(
|
||||
"import streamlit as st\n"
|
||||
"from bcda.ui import brand\n"
|
||||
"brand.inject()\n"
|
||||
"with st.sidebar:\n"
|
||||
" brand.masthead(eyebrow='bcda <manager>')\n"
|
||||
"brand.title('Exports', 'bulk export · $export')\n"
|
||||
).run()
|
||||
assert not at.exception
|
||||
bodies = [h.value for h in at.html]
|
||||
assert any("<style>" in b and brand.FONTS_URL in b for b in bodies)
|
||||
mast = [h.value for h in at.sidebar.html]
|
||||
assert any(
|
||||
"fw-mast" in b and "Fhirworx" in b and "bcda <manager>" in b for b in mast
|
||||
)
|
||||
assert at.title[0].value == "Exports"
|
||||
assert any('class="fw-eyebrow"' in b and "bulk export" in b for b in bodies)
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import sys
|
||||
|
||||
from bcda.ui import __main__ as entry
|
||||
from bcda.ui import metrics
|
||||
from bcda.ui import brand, metrics
|
||||
|
||||
|
||||
def test_main_serves_metrics_then_boots_streamlit(monkeypatch) -> None:
|
||||
@@ -23,7 +23,19 @@ def test_main_serves_metrics_then_boots_streamlit(monkeypatch) -> None:
|
||||
assert calls[0] == "serve"
|
||||
argv = calls[1][1]
|
||||
assert argv[:2] == ["streamlit", "run"] and argv[2].endswith("bcda/ui/app.py")
|
||||
assert argv[3:] == ["--server.port", "9000"]
|
||||
# the fhirworx theme file rides along unless the caller picked a base
|
||||
assert argv[3:] == [
|
||||
"--theme.base",
|
||||
str(brand.THEME_PATH),
|
||||
"--server.port",
|
||||
"9000",
|
||||
]
|
||||
calls.clear()
|
||||
assert entry.main(["--theme.base=dark"]) == 0
|
||||
assert calls[1][1][3:] == ["--theme.base=dark"]
|
||||
calls.clear()
|
||||
assert entry.main(["--theme.base", "light"]) == 0
|
||||
assert calls[1][1][3:] == ["--theme.base", "light"]
|
||||
|
||||
|
||||
def test_main_passes_exit_code_and_default_argv(monkeypatch) -> None:
|
||||
@@ -35,7 +47,12 @@ def test_main_passes_exit_code_and_default_argv(monkeypatch) -> None:
|
||||
|
||||
monkeypatch.setattr("streamlit.web.cli.main", exits)
|
||||
assert entry.main() == 3
|
||||
assert sys.argv[3:] == ["--server.headless", "true"]
|
||||
assert sys.argv[3:] == [
|
||||
"--theme.base",
|
||||
str(brand.THEME_PATH),
|
||||
"--server.headless",
|
||||
"true",
|
||||
]
|
||||
|
||||
monkeypatch.setattr("streamlit.web.cli.main", lambda: None)
|
||||
assert entry.main([]) == 0
|
||||
|
||||
@@ -433,6 +433,17 @@ def test_app_shell_renders() -> None:
|
||||
).run()
|
||||
assert not at.exception
|
||||
assert any("not connected" in c.value for c in at.sidebar.caption)
|
||||
# masthead first, then one page link per page, in order
|
||||
assert any("fw-mast" in h.value for h in at.sidebar.html)
|
||||
assert [p.label for p in at.sidebar.page_link] == [
|
||||
"Connect",
|
||||
"Exports",
|
||||
"Store",
|
||||
"Flatten + load",
|
||||
"Data",
|
||||
"Dictionary + crosswalk",
|
||||
"Upstream",
|
||||
]
|
||||
at.session_state["conn"] = SMALL
|
||||
at.run()
|
||||
assert any("Small Model Entity" in c.value for c in at.sidebar.caption)
|
||||
|
||||
Reference in New Issue
Block a user