FINESE_SCHOOL / src /ui /theme.py
Jack-ki1's picture
Update src/ui/theme.py
ead2535 verified
Raw
History Blame Contribute Delete
11.2 kB
"""
F1 Predictor 2026 - Theme System
Consolidated: colors, CSS, page config, plotly helpers, team colors
"""
import streamlit as st
import streamlit.components.v1 as components
# ── Color Palette ───────────────────────────────────────────
# STRICT: skyblue, darkblue, darkgray, black, red, green ONLY
COLORS = {
"skyblue": "#3B82F6",
"skyblue_dark": "#2563EB",
"skyblue_light": "#60A5FA",
"darkblue": "#1E3A8A",
"darkgray": "#475569",
"black": "#0F172A",
"red": "#EF4444",
"green": "#10B981",
"amber": "#F59E0B",
"cyan": "#06B6D4",
"gold": "#FBBF24",
"silver": "#9CA3AF",
"bronze": "#D97706",
"bg": "#F8FAFC",
"bg_card": "#F1F5F9",
"border": "#E2E8F0",
"text": "#0F172A",
"text_secondary": "#475569",
"text_muted": "#94A3B8",
"team_red_bull": "#1E41FF",
"team_ferrari": "#DC0000",
"team_mercedes": "#00D2BE",
"team_mclaren": "#FF8000",
"team_aston_martin": "#006F62",
"team_alpine": "#0093CC",
"team_williams": "#005AFF",
"team_rb": "#2B4562",
"team_kick_sauber": "#00E701",
"team_haas": "#B6BABD",
}
TEAM_COLOR_MAP = {
"red_bull": "#1E41FF", "ferrari": "#DC0000", "mercedes": "#00D2BE",
"mclaren": "#FF8000", "aston_martin": "#006F62", "alpine": "#0093CC",
"williams": "#005AFF", "rb": "#2B4562", "kick_sauber": "#00E701",
"haas": "#B6BABD", "red bull": "#1E41FF", "aston martin": "#006F62",
"kick sauber": "#00E701", "racing bulls": "#2B4562", "sauber": "#00E701",
"visa cash app rb": "#2B4562",
}
def bar_colors_from_teams(predictions):
"""Generate bar colors based on team affiliation."""
return [TEAM_COLOR_MAP.get(
p.get("team", "").lower().strip().replace(" ", "_").replace("-", "_"),
"#3B82F6"
) for p in predictions]
def f1_plotly_layout(title="", show_legend=False):
"""Return a Plotly layout dict styled for the F1 light theme."""
return {
"paper_bgcolor": "rgba(0,0,0,0)",
"plot_bgcolor": "rgba(0,0,0,0)",
"font": {"color": "#0F172A", "family": "Inter, sans-serif"},
"title": {"text": title, "font": {"color": "#0F172A", "size": 18}},
"xaxis": {
"gridcolor": "rgba(71,85,105,0.15)", "linecolor": "rgba(71,85,105,0.3)",
"tickfont": {"color": "#475569"}, "title": {"font": {"color": "#475569"}},
},
"yaxis": {
"gridcolor": "rgba(71,85,105,0.15)", "linecolor": "rgba(71,85,105,0.3)",
"tickfont": {"color": "#475569"}, "title": {"font": {"color": "#475569"}},
},
"legend": {"font": {"color": "#475569"}, "bgcolor": "rgba(0,0,0,0)"} if show_legend else {},
"margin": {"l": 60, "r": 20, "t": 60, "b": 60},
}
# ── Complete CSS ──────────────────────────────────────────
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
html, body, .stApp, [data-testid="stAppViewContainer"], .main, .block-container {
background: #F8FAFC !important; background-color: #F8FAFC !important;
}
body {
font-family: 'Inter', -apple-system, sans-serif; color: #0F172A; line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
#MainMenu, header, footer, .stApp > header, [data-testid="stToolbar"] {
display: none !important;
}
/* Top Navigation Bar */
.top-nav-bar {
display: flex;
justify-content: center;
gap: 1rem;
background: #F8FAFC;
border-bottom: 1px solid #E2E8F0;
padding: 0.75rem 2rem;
position: sticky;
top: 0;
z-index: 1000;
backdrop-filter: blur(10px);
}
.nav-item {
padding: 0.5rem 1rem;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
color: #475569;
border: 1px solid transparent;
text-decoration: none;
display: inline-block;
}
.nav-item.active {
background: linear-gradient(135deg, #3B82F6, #2563EB);
color: #F8FAFC;
border: 1px solid #3B82F6;
}
.nav-item:hover:not(.active) {
background: #F1F5F9;
border: 1px solid #E2E8F0;
}
/* Buttons */
.stButton > button {
background: linear-gradient(135deg, #3B82F6, #2563EB) !important;
color: #F8FAFC !important; border: none !important; border-radius: 8px !important;
padding: 0.6rem 1.5rem !important; font-weight: 600 !important;
transition: all 0.2s ease !important;
}
.stButton > button:hover {
transform: translateY(-1px) !important;
box-shadow: 0 4px 12px rgba(59,130,246,0.3) !important;
}
/* Inputs */
.stSelectbox > div > div, .stNumberInput > div > div, .stSlider > div > div {
background: #F1F5F9 !important; border: 1px solid #E2E8F0 !important;
border-radius: 8px !important;
}
.stSelectbox label, .stNumberInput label, .stSlider label, .stCheckbox label {
color: #475569 !important; font-weight: 600 !important;
}
/* Metrics */
[data-testid="stMetric"] {
background: #F1F5F9 !important; border: 1px solid #E2E8F0 !important;
border-radius: 10px !important; padding: 1rem !important;
}
[data-testid="stMetric"] label { color: #94A3B8 !important; font-size: 0.75rem !important; text-transform: uppercase; letter-spacing: 0.5px; }
[data-testid="stMetric"] .css-1xarl3l { color: #0F172A !important; font-size: 1.5rem !important; font-weight: 800 !important; }
/* Tabs */
.stTabs [data-baseweb="tab-list"] {
background: #F1F5F9 !important; border-radius: 10px 10px 0 0 !important;
border-bottom: 2px solid #E2E8F0 !important;
}
.stTabs [data-baseweb="tab"] { color: #94A3B8 !important; font-weight: 500 !important; }
.stTabs [aria-selected="true"] { color: #3B82F6 !important; border-bottom: 2px solid #3B82F6 !important; }
/* DataFrames */
.stDataFrame {
background: #F1F5F9 !important; border-radius: 10px !important;
border: 1px solid #E2E8F0 !important;
}
.stDataFrame th { background: #E2E8F0 !important; color: #0F172A !important; font-weight: 700 !important; }
.stDataFrame td { color: #1E293B !important; }
.stDataFrame tr:hover td { background: #F8FAFC !important; }
/* Expanders */
.stExpander {
background: #F1F5F9 !important; border: 1px solid #E2E8F0 !important;
border-radius: 10px !important;
}
/* Sidebar */
section[data-testid="stSidebar"] {
background: #F1F5F9 !important; border-right: 1px solid #E2E8F0 !important;
}
section[data-testid="stSidebar"] .stMarkdown, section[data-testid="stSidebar"] label {
color: #0F172A !important;
}
/* Sidebar Toggle Button - POSITIONED & VISIBLE */
button[kind="icon"][data-testid="collapsedControl"],
[data-testid="collapsedControl"],
.stSidebarToggle {
background-color: #EF4444 !important;
border: 2px solid #DC2626 !important;
border-radius: 6px !important;
box-shadow: 0 2px 8px rgba(239,68,68,0.3) !important;
position: fixed !important;
top: 10px !important;
left: 250px !important;
z-index: 9999 !important;
width: 40px !important;
height: 40px !important;
min-width: 40px !important;
min-height: 40px !important;
padding: 8px !important;
opacity: 1 !important;
visibility: visible !important;
}
button[kind="icon"][data-testid="collapsedControl"]:hover,
[data-testid="collapsedControl"]:hover,
.stSidebarToggle:hover {
background-color: #DC2626 !important;
transform: scale(1.05);
}
button[kind="icon"][data-testid="collapsedControl"] svg,
[data-testid="collapsedControl"] svg,
.stSidebarToggle svg {
fill: #FFFFFF !important;
stroke: #FFFFFF !important;
}
/* Radio */
.stRadio > div { background: #F1F5F9 !important; border-radius: 8px !important; padding: 0.5rem !important; }
.stRadio label span { color: #475569 !important; }
/* Alerts */
.stAlert { background: #F1F5F9 !important; border: 1px solid #E2E8F0 !important; border-radius: 10px !important; }
.stAlert [data-testid="stAlertContent"] { color: #0F172A !important; }
/* Scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #F8FAFC; }
::-webkit-scrollbar-thumb { background: #CBD5E0; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
/* Plotly */
.js-plotly-plot .plotly { border-radius: 10px; }
"""
def apply_theme():
"""Inject theme CSS into Streamlit."""
js_code = """
<script>
(function() {
function adjustSidebarTogglePosition() {
// Find the sidebar toggle button
const toggleButtons = document.querySelectorAll('button[kind="icon"][data-testid="collapsedControl"]');
const sidebar = document.querySelector('section[data-testid="stSidebar"]');
if (toggleButtons.length > 0 && sidebar) {
const toggle = toggleButtons[0];
// Get sidebar width to determine if it's expanded or collapsed
const computedStyle = window.getComputedStyle(sidebar);
const sidebarWidth = parseFloat(computedStyle.width);
// If sidebar width is small (~4.5rem or ~72px), it's collapsed
// If wider (~260px+), it's expanded
if (sidebarWidth > 200) {
// Sidebar is expanded - position toggle at 250px
toggle.style.left = '250px';
} else {
// Sidebar is collapsed - position toggle at 10px
toggle.style.left = '10px';
}
}
}
// Run immediately
adjustSidebarTogglePosition();
// Run when the page loads
window.addEventListener('load', adjustSidebarTogglePosition);
// Run periodically to catch sidebar state changes
setInterval(adjustSidebarTogglePosition, 500);
// Watch for DOM mutations that might affect sidebar
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' &&
(mutation.attributeName === 'style' || mutation.target.matches('[data-testid="stSidebar"]'))) {
setTimeout(adjustSidebarTogglePosition, 100);
}
});
});
observer.observe(document.body, {
attributes: true,
subtree: true,
attributeFilter: ['style', 'class']
});
})();
</script>
"""
st.markdown(f"<style>{CSS}</style>", unsafe_allow_html=True)
st.components.v1.html(js_code, height=0)
def apply_page_config():
"""Set Streamlit page config."""
st.set_page_config(
page_title="F1 Predictor 2026",
page_icon="🏎️",
layout="wide",
initial_sidebar_state="expanded",
)