Spaces:
Sleeping
Sleeping
| """ | |
| 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> {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, | |
| ) |