traffic-scene / theme.py
0xdivin3's picture
Upload 8 files
72534cf verified
Raw
History Blame Contribute Delete
9.12 kB
"""
theme.py
--------
Visual design system for the Traffic Scene Interpretation System UI.
Design direction: a dark traffic-monitoring "control room" console rather
than a generic AI demo page. Colors are drawn from real traffic signals
(green/amber/red) so the congestion badge means something, not just
decorates. Stat readouts use a monospace face to evoke CCTV on-screen
text. Panels are captioned like camera feeds (e.g. "CAM 01 · RAW").
Token system:
Color bg-primary #0B0E13, bg-panel #141A22, border #232B35,
accent-amber #FFB020, signal-green #35D399, signal-red #FF5C5C,
text-primary #E8ECF1, text-muted #8B95A3
Type Display: Space Grotesk · Body: Inter · Data/mono: IBM Plex Mono
"""
import streamlit as st
CSS = """
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
:root {
--bg-primary: #0B0E13;
--bg-panel: #141A22;
--bg-panel-alt: #10151C;
--border-hairline: #232B35;
--accent-amber: #FFB020;
--signal-green: #35D399;
--signal-red: #FF5C5C;
--text-primary: #E8ECF1;
--text-muted: #8B95A3;
}
/* Base typography */
html, body, [class*="css"] {
font-family: 'Inter', sans-serif;
color: var(--text-primary);
}
/* Tighten default Streamlit top padding, but leave room for Streamlit's own
fixed toolbar (Deploy button etc.) so our masthead doesn't sit underneath it */
.block-container {
padding-top: 3.5rem;
max-width: 1100px;
}
/* ---------------- Masthead ---------------- */
.tsis-masthead {
display: flex;
align-items: center;
padding: 0.6rem 1rem;
background: var(--bg-panel-alt);
border: 1px solid var(--border-hairline);
border-radius: 4px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
letter-spacing: 0.04em;
color: var(--text-muted);
margin-bottom: 1.75rem;
}
.tsis-masthead .tsis-status {
color: var(--signal-green);
display: flex;
align-items: center;
gap: 0.4rem;
}
.tsis-status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--signal-green);
box-shadow: 0 0 6px var(--signal-green);
}
/* ---------------- Hero ---------------- */
.tsis-eyebrow {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent-amber);
margin-bottom: 0.4rem;
}
.tsis-title {
font-family: 'Space Grotesk', sans-serif;
font-weight: 700;
font-size: 2.1rem;
line-height: 1.15;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.tsis-subtitle {
font-family: 'Inter', sans-serif;
font-size: 0.95rem;
color: var(--text-muted);
max-width: 640px;
margin-bottom: 2rem;
}
/* ---------------- Feed panel captions ---------------- */
.tsis-feed-caption {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.7rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
border-bottom: 1px solid var(--border-hairline);
padding-bottom: 0.4rem;
margin-bottom: 0.6rem;
}
.tsis-feed-caption .tsis-tag {
color: var(--accent-amber);
}
/* ---------------- Bordered containers (image/summary panels) ---------------- */
[data-testid="stVerticalBlockBorderWrapper"] {
background: var(--bg-panel);
border: 1px solid var(--border-hairline) !important;
border-radius: 6px !important;
}
/* ---------------- Tabs styled like a camera/source selector ---------------- */
[data-testid="stTabs"] button [data-testid="stMarkdownContainer"] p {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
[data-testid="stTabs"] [data-baseweb="tab-list"] {
gap: 1.75rem;
border-bottom: 1px solid var(--border-hairline);
}
[data-testid="stTabs"] button {
padding: 0.5rem 0.15rem !important;
}
[data-testid="stTabs"] [aria-selected="true"] {
color: var(--accent-amber) !important;
}
/* The active-tab underline is a separate element from the tab button itself;
force its color explicitly instead of relying on the theme's primaryColor
cascading down (it doesn't reliably). */
[data-testid="stTabs"] [data-baseweb="tab-highlight"] {
background-color: var(--accent-amber) !important;
}
/* ---------------- File uploader ---------------- */
[data-testid="stFileUploaderDropzone"] {
background: var(--bg-panel-alt);
border: 1px dashed var(--border-hairline) !important;
border-radius: 6px;
}
/* ---------------- Metrics as instrument readouts ---------------- */
[data-testid="stMetric"] {
background: var(--bg-panel-alt);
border: 1px solid var(--border-hairline);
border-radius: 6px;
padding: 0.75rem 1rem;
}
[data-testid="stMetricLabel"] {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.68rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted) !important;
}
[data-testid="stMetricValue"] {
font-family: 'IBM Plex Mono', monospace;
color: var(--text-primary) !important;
}
/* ---------------- Stat chips (vehicle counts) ---------------- */
.tsis-chip-row { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.75rem; }
.tsis-chip {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
background: var(--bg-panel-alt);
border: 1px solid var(--border-hairline);
border-radius: 4px;
padding: 0.3rem 0.65rem;
color: var(--text-primary);
}
.tsis-chip .tsis-chip-label { color: var(--text-muted); margin-right: 0.35rem; }
/* ---------------- Congestion badge (traffic-signal colors) ---------------- */
.tsis-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
font-weight: 500;
padding: 0.45rem 0.85rem;
border-radius: 4px;
border: 1px solid var(--border-hairline);
background: var(--bg-panel-alt);
}
.tsis-badge-dot { width: 9px; height: 9px; border-radius: 50%; }
.tsis-badge-free .tsis-badge-dot { background: var(--signal-green); box-shadow: 0 0 6px var(--signal-green); }
.tsis-badge-free { color: var(--signal-green); }
.tsis-badge-moderate .tsis-badge-dot { background: var(--accent-amber); box-shadow: 0 0 6px var(--accent-amber); }
.tsis-badge-moderate { color: var(--accent-amber); }
.tsis-badge-congested .tsis-badge-dot { background: var(--signal-red); box-shadow: 0 0 6px var(--signal-red); }
.tsis-badge-congested { color: var(--signal-red); }
/* ---------------- Buttons ---------------- */
.stButton > button, .stDownloadButton > button {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
letter-spacing: 0.04em;
text-transform: uppercase;
background: transparent;
border: 1px solid var(--accent-amber);
color: var(--accent-amber);
border-radius: 4px;
}
.stButton > button:hover, .stDownloadButton > button:hover {
background: var(--accent-amber);
color: var(--bg-primary);
}
/* Section divider */
.tsis-hr {
border: none;
border-top: 1px solid var(--border-hairline);
margin: 1.75rem 0 1.25rem 0;
}
</style>
"""
def inject():
st.markdown(CSS, unsafe_allow_html=True)
def masthead():
st.markdown(
"""
<div class="tsis-masthead">
<div class="tsis-status"><span class="tsis-status-dot"></span> SYSTEM ONLINE</div>
</div>
""",
unsafe_allow_html=True,
)
def hero(eyebrow: str, title: str, subtitle: str):
st.markdown(
f"""
<div class="tsis-eyebrow">{eyebrow}</div>
<div class="tsis-title">{title}</div>
<div class="tsis-subtitle">{subtitle}</div>
""",
unsafe_allow_html=True,
)
def feed_caption(tag: str, label: str):
st.markdown(
f'<div class="tsis-feed-caption"><span class="tsis-tag">{tag}</span> &nbsp;{label}</div>',
unsafe_allow_html=True,
)
def chip_row(counts: dict):
if not counts:
st.markdown(
'<div class="tsis-chip-row"><div class="tsis-chip">'
'<span class="tsis-chip-label">STATUS</span>NO OBJECTS DETECTED</div></div>',
unsafe_allow_html=True,
)
return
chips = "".join(
f'<div class="tsis-chip"><span class="tsis-chip-label">{label.upper()}</span>{count}</div>'
for label, count in sorted(counts.items(), key=lambda x: -x[1])
)
st.markdown(f'<div class="tsis-chip-row">{chips}</div>', unsafe_allow_html=True)
def congestion_badge(label: str):
variant_map = {
"Free flowing": ("tsis-badge-free", "FREE FLOWING"),
"Moderate traffic": ("tsis-badge-moderate", "MODERATE TRAFFIC"),
"Congested": ("tsis-badge-congested", "CONGESTED"),
}
variant_class, display_text = variant_map.get(label, ("tsis-badge-moderate", label.upper()))
st.markdown(
f'<div class="tsis-badge {variant_class}"><span class="tsis-badge-dot"></span>{display_text}</div>',
unsafe_allow_html=True,
)