Files
stack/tests/bcda/test_ui_brand.py
kert 6c218b5898
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
feat(bcda): fhirworx house style for the manager — theme file, brand masthead and eyebrows, favicon
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
2026-10-25 15:21:30 -04:00

93 lines
3.2 KiB
Python

"""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 &lt;manager&gt;" 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)