strem / components.py
Mohammedallyl's picture
Upload 17 files
79216dd verified
Raw History Blame Contribute Delete
18.2 kB
import streamlit as st
CHART_HEIGHT = 520
# ── DESIGN SYSTEM TOKENS β€” aligned with tijartek_dashboard_html.html ──
COLORS = {
# Core palette (matches HTML :root vars)
"primary": "#00c9b1", # --teal
"primary_dark": "#009985",
"accent": "#0085ff", # --blue
"accent2": "#ff4d6d", # --red
"accent3": "#f5c842", # --gold
"accent4": "#9b6dff", # --purple
"success": "#00e096", # --green
"warning": "#f5c842", # --gold
"danger": "#ff4d6d", # --red
# Backgrounds (matches HTML)
"bg_dark": "#030d1a", # --bg
"bg_card": "#0d2540", # --card
"bg_card2": "#0f2d4a", # --card2
"bg2": "#061526", # --bg2
"bg3": "#0a1f35", # --bg3
# Text
"text_primary": "#e8f4ff", # --text
"text_muted": "#7aadcc", # --text2
"text_dim": "#3d6880", # --text3
# Border
"border": "rgba(0,201,177,0.12)", # --border
}
PLOTLY_PALETTE = [
"#00c9b1", # teal
"#0085ff", # blue
"#f5c842", # gold
"#ff4d6d", # red
"#00e096", # green
"#9b6dff", # purple
"#38a8ff", # blue2
"#00e5cc", # teal2
]
CHART_LAYOUT = dict(
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font=dict(family="'DM Sans', sans-serif", color="#7aadcc", size=12),
title_font=dict(family="'Syne', sans-serif", color="#e8f4ff", size=15),
legend=dict(
bgcolor="rgba(13,37,64,0.92)",
bordercolor="rgba(0,201,177,0.18)",
borderwidth=1,
font=dict(color="#7aadcc", size=11),
),
xaxis=dict(
gridcolor="rgba(0,201,177,0.06)",
linecolor="rgba(0,201,177,0.12)",
tickfont=dict(color="#3d6880"),
),
yaxis=dict(
gridcolor="rgba(0,201,177,0.06)",
linecolor="rgba(0,201,177,0.12)",
tickfont=dict(color="#3d6880"),
),
margin=dict(l=16, r=16, t=56, b=16),
hoverlabel=dict(
bgcolor="#0d2540",
bordercolor="rgba(0,201,177,0.35)",
font=dict(color="#e8f4ff", size=12),
),
)
GLOBAL_CSS = """
<style>
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap');
/* ── CSS VARIABLES β€” mirrors tijartek_dashboard_html.html ── */
:root {
--bg: #030d1a;
--bg2: #061526;
--bg3: #0a1f35;
--card: #0d2540;
--card2: #0f2d4a;
--teal: #00c9b1;
--teal2: #00e5cc;
--blue: #0085ff;
--blue2: #38a8ff;
--gold: #f5c842;
--red: #ff4d6d;
--green: #00e096;
--purple: #9b6dff;
--text: #e8f4ff;
--text2: #7aadcc;
--text3: #3d6880;
--border: rgba(0,201,177,0.12);
--glow: 0 0 40px rgba(0,201,177,0.15);
--glow2: 0 0 20px rgba(0,133,255,0.2);
}
/* ── BASE RESET ── */
html, body, [class*="css"] {
font-family: 'DM Sans', sans-serif;
}
/* ── APP BACKGROUND β€” deep navy with subtle grid overlay ── */
.stApp {
background: var(--bg);
background-image:
linear-gradient(rgba(0,201,177,0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,201,177,0.025) 1px, transparent 1px),
radial-gradient(ellipse 70% 50% at 15% 5%, rgba(0,133,255,0.07) 0%, transparent 55%),
radial-gradient(ellipse 50% 40% at 85% 95%, rgba(0,201,177,0.05) 0%, transparent 55%);
background-size: 40px 40px, 40px 40px, 100% 100%, 100% 100%;
}
/* ── SIDEBAR ── */
section[data-testid="stSidebar"] {
background: var(--bg2) !important;
border-right: 1px solid var(--border) !important;
}
section[data-testid="stSidebar"] * { color: var(--text2) !important; }
section[data-testid="stSidebar"] h1,
section[data-testid="stSidebar"] h2,
section[data-testid="stSidebar"] h3 { color: var(--text) !important; }
/* ── METRIC CARDS β€” match .kpi cards in HTML ── */
[data-testid="metric-container"] {
background: var(--card) !important;
border: 1px solid var(--border) !important;
border-radius: 12px !important;
padding: 20px !important;
position: relative;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
[data-testid="metric-container"]:hover {
transform: translateY(-2px);
box-shadow: var(--glow) !important;
}
[data-testid="metric-container"]::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, var(--teal), transparent);
border-radius: 12px 12px 0 0;
}
[data-testid="stMetricLabel"] {
color: var(--text3) !important;
font-size: 0.68rem !important;
font-weight: 500 !important;
text-transform: uppercase;
letter-spacing: 0.1em;
}
[data-testid="stMetricValue"] {
color: var(--teal) !important;
font-family: 'Syne', sans-serif !important;
font-size: 1.75rem !important;
font-weight: 700 !important;
line-height: 1;
}
[data-testid="stMetricDelta"] {
font-size: 0.78rem !important;
color: var(--text2) !important;
}
/* ── KPI COLOR VARIANTS β€” accent top bars ── */
[data-testid="metric-container"].kpi-blue::before { background: linear-gradient(90deg, var(--blue), transparent); }
[data-testid="metric-container"].kpi-blue [data-testid="stMetricValue"] { color: var(--blue2) !important; }
[data-testid="metric-container"].kpi-gold::before { background: linear-gradient(90deg, var(--gold), transparent); }
[data-testid="metric-container"].kpi-gold [data-testid="stMetricValue"] { color: var(--gold) !important; }
[data-testid="metric-container"].kpi-red::before { background: linear-gradient(90deg, var(--red), transparent); }
[data-testid="metric-container"].kpi-red [data-testid="stMetricValue"] { color: var(--red) !important; }
[data-testid="metric-container"].kpi-green::before { background: linear-gradient(90deg, var(--green), transparent); }
[data-testid="metric-container"].kpi-green [data-testid="stMetricValue"]{ color: var(--green) !important; }
[data-testid="metric-container"].kpi-purple::before { background: linear-gradient(90deg, var(--purple), transparent); }
[data-testid="metric-container"].kpi-purple [data-testid="stMetricValue"]{ color: var(--purple) !important; }
/* ── PLOTLY CHARTS ── */
.js-plotly-plot .plotly { border-radius: 10px; }
/* ── SECTION HEADERS β€” .section-title / .page-title from HTML ── */
.tj-section-header {
display: flex;
align-items: center;
gap: 12px;
margin: 8px 0 4px 0;
}
.tj-section-header h2 {
font-family: 'Syne', sans-serif;
font-size: 1.5rem;
font-weight: 700;
color: var(--text);
margin: 0;
letter-spacing: -0.02em;
}
/* ── CARD CONTAINERS β€” match .card in HTML ── */
.tj-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 22px;
position: relative;
overflow: hidden;
margin-bottom: 16px;
}
.tj-card-title {
font-family: 'Syne', sans-serif;
font-weight: 600;
font-size: 13px;
color: var(--text);
margin-bottom: 4px;
}
.tj-card-sub {
font-size: 11px;
color: var(--text3);
margin-bottom: 16px;
}
/* ── FILTER BAR ── */
.tj-filter-bar {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 14px 20px;
margin-bottom: 20px;
}
/* ── INSIGHT BADGE β€” match .section-tag in HTML ── */
.tj-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(0,201,177,0.08);
border: 1px solid rgba(0,201,177,0.2);
color: var(--teal);
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 3px 10px;
border-radius: 20px;
margin-bottom: 6px;
}
/* ── PROGRESS BAR β€” match .progress-bar in HTML ── */
.tj-progress-wrap { margin-bottom: 10px; }
.tj-progress-label {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--text2);
margin-bottom: 5px;
}
.tj-progress-bar {
height: 4px;
background: var(--bg3);
border-radius: 2px;
overflow: hidden;
}
.tj-progress-fill {
height: 100%;
border-radius: 2px;
background: linear-gradient(90deg, var(--teal), var(--blue2));
}
.tj-progress-fill.gold { background: linear-gradient(90deg, var(--gold), #ff9f1c); }
.tj-progress-fill.red { background: linear-gradient(90deg, var(--red), #ff8c69); }
.tj-progress-fill.green { background: linear-gradient(90deg, var(--green), #00b4d8); }
.tj-progress-fill.purple { background: linear-gradient(90deg, var(--purple), var(--blue2)); }
/* ── ALERT ITEMS β€” match .alert-item in HTML ── */
.tj-alert {
display: flex;
align-items: center;
gap: 12px;
padding: 11px 14px;
border-radius: 8px;
background: var(--bg3);
border-left: 3px solid var(--red);
font-size: 12px;
color: var(--text2);
margin-bottom: 8px;
}
.tj-alert.warn { border-left-color: var(--gold); }
.tj-alert.info { border-left-color: var(--blue); }
.tj-alert.ok { border-left-color: var(--green); }
/* ── DATA TABLE β€” match .data-table in HTML ── */
.tj-table { width: 100%; border-collapse: collapse; }
.tj-table th {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text3);
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid var(--border);
}
.tj-table td {
font-size: 12px;
color: var(--text2);
padding: 10px 12px;
border-bottom: 1px solid rgba(0,201,177,0.04);
}
.tj-table tr:hover td { background: rgba(0,201,177,0.03); color: var(--text); }
/* ── RANK BADGES ── */
.tj-rank {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px; height: 20px;
border-radius: 50%;
font-size: 10px;
font-weight: 700;
background: var(--card2);
color: var(--text3);
}
.tj-rank.gold { background: rgba(245,200,66,0.18); color: var(--gold); }
.tj-rank.silver { background: rgba(122,173,204,0.15); color: var(--text2); }
.tj-rank.bronze { background: rgba(200,120,60,0.15); color: #c8783c; }
/* ── LIVE BADGE ── */
.tj-live {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--green);
font-weight: 500;
}
.tj-live-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--green);
animation: tjPulse 2s infinite;
}
@keyframes tjPulse {
0%,100% { opacity:1; transform:scale(1); }
50% { opacity:0.5; transform:scale(1.4); }
}
/* ── DIVIDER ── */
hr { border-color: rgba(0,201,177,0.08) !important; }
/* ── INPUTS ── */
.stTextInput input, .stNumberInput input {
background: var(--card2) !important;
border-color: var(--border) !important;
color: var(--text) !important;
border-radius: 8px !important;
}
.stSelectbox > div > div {
background: var(--card2) !important;
border-color: var(--border) !important;
color: var(--text) !important;
border-radius: 8px !important;
}
/* ── BUTTONS ── */
.stButton > button {
background: linear-gradient(135deg, var(--teal), var(--blue)) !important;
color: #030d1a !important;
border: none !important;
border-radius: 8px !important;
font-weight: 700 !important;
font-family: 'DM Sans', sans-serif !important;
letter-spacing: 0.02em;
transition: opacity 0.2s, transform 0.15s !important;
}
.stButton > button:hover {
opacity: 0.88 !important;
transform: translateY(-1px) !important;
}
/* ── EXPANDER ── */
.streamlit-expanderHeader {
background: var(--card) !important;
border: 1px solid var(--border) !important;
border-radius: 10px !important;
color: var(--text2) !important;
}
/* ── TABS ── */
[data-testid="stTabs"] [role="tablist"] {
background: var(--card) !important;
border-radius: 10px !important;
border: 1px solid var(--border) !important;
padding: 4px;
}
[data-testid="stTabs"] [role="tab"] {
color: var(--text3) !important;
font-family: 'DM Sans', sans-serif !important;
border-radius: 7px !important;
}
[data-testid="stTabs"] [role="tab"][aria-selected="true"] {
background: rgba(0,201,177,0.12) !important;
color: var(--teal) !important;
}
/* ── ALERT / INFO BOXES ── */
.stAlert {
border-radius: 10px !important;
background: var(--card) !important;
border-color: var(--border) !important;
}
/* ── CHAT MESSAGES ── */
[data-testid="stChatMessage"] {
background: var(--card) !important;
border: 1px solid var(--border) !important;
border-radius: 12px !important;
}
/* ── DATAFRAME / TABLE ── */
[data-testid="stDataFrame"] {
border: 1px solid var(--border) !important;
border-radius: 10px !important;
}
/* ── SCROLLBAR ── */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: var(--bg2); }
::-webkit-scrollbar-thumb { background: var(--text3); border-radius: 2px; }
/* ── SPINNER / STATUS ── */
.stSpinner > div { border-top-color: var(--teal) !important; }
/* ── PAGE LAYOUT HELPERS ── */
.block-container {
padding-top: 1.5rem !important;
padding-bottom: 2rem !important;
}
</style>
"""
def inject_theme():
"""Inject the global CSS design system (aligned with HTML dashboard)."""
st.markdown(GLOBAL_CSS, unsafe_allow_html=True)
def section_title(title: str, subtitle: str = ""):
"""Styled section header matching HTML .page-title / .section-title style."""
html = f"""
<div class="tj-section-header">
<h2>{title}</h2>
</div>"""
if subtitle:
html += f'<p style="color:#3d6880;font-size:0.83rem;margin:0 0 14px 0;letter-spacing:0.01em;">{subtitle}</p>'
st.markdown(html, unsafe_allow_html=True)
def render_kpi_row(metrics: list, num_cols: int = 4):
"""
Render a row of KPI metric cards.
Each metric dict may include: label, value, delta, color (teal/blue/gold/red/green/purple).
The color controls the top accent bar and value color via CSS class.
"""
cols = st.columns(num_cols)
for i, m in enumerate(metrics):
with cols[i % num_cols]:
color_cls = m.get("color", "")
# Inject a wrapper div with the appropriate kpi color class
if color_cls and color_cls != "teal":
st.markdown(
f'<style>[data-testid="metric-container"]:nth-child({i+1}) '
f'{{ border-top: 2px solid var(--{color_cls}) !important; }}</style>',
unsafe_allow_html=True,
)
st.metric(
label=m.get("label", "β€”"),
value=m.get("value", "β€”"),
delta=m.get("delta"),
)
st.markdown("<div style='margin-bottom:4px'></div>", unsafe_allow_html=True)
def filter_bar(label: str = "Filters"):
"""Styled container for filter rows."""
return st.container()
def insight_badge(text: str):
"""Render a small teal pill badge β€” matches .section-tag in HTML."""
st.markdown(f'<span class="tj-badge">{text}</span>', unsafe_allow_html=True)
def empty_state(message: str, icon: str = "πŸ“­"):
"""Placeholder card for missing data."""
st.markdown(
f"""
<div style='text-align:center;padding:40px 20px;background:var(--card);
border:1px dashed rgba(0,201,177,0.15);border-radius:14px;margin:8px 0;'>
<div style='font-size:2rem;margin-bottom:10px;opacity:0.5'>{icon}</div>
<p style='color:#3d6880;font-size:0.88rem;margin:0'>{message}</p>
</div>
""",
unsafe_allow_html=True,
)
def progress_row(label: str, value_label: str, pct: float, color: str = ""):
"""
Render a labeled progress bar row β€” matches HTML progress-bar pattern.
color: '', 'gold', 'red', 'green', 'purple'
pct: 0–100
"""
fill_class = f"tj-progress-fill {color}".strip()
st.markdown(
f"""
<div class="tj-progress-wrap">
<div class="tj-progress-label">
<span>{label}</span>
<span style="color:var(--teal);font-weight:600">{value_label}</span>
</div>
<div class="tj-progress-bar">
<div class="{fill_class}" style="width:{min(max(pct,0),100):.1f}%"></div>
</div>
</div>
""",
unsafe_allow_html=True,
)
def alert_row(message: str, level: str = "error"):
"""
Render a styled alert item β€” matches HTML .alert-item.
level: 'error' (red), 'warn' (gold), 'info' (blue), 'ok' (green)
"""
cls_map = {"error": "", "warn": "warn", "info": "info", "ok": "ok"}
icon_map = {"error": "⚠️", "warn": "⚑", "info": "ℹ️", "ok": "βœ…"}
cls = cls_map.get(level, "")
icon = icon_map.get(level, "β€’")
st.markdown(
f'<div class="tj-alert {cls}">'
f'<span style="font-size:14px">{icon}</span>'
f'<span>{message}</span>'
f'</div>',
unsafe_allow_html=True,
)
def rank_table_html(rows: list, headers: list) -> str:
"""
Build an HTML rank table string (use with st.markdown unsafe_allow_html=True).
rows: list of lists; first column treated as rank number (1-3 get gold/silver/bronze badges).
headers: list of column header strings.
"""
th = "".join(f"<th>{h}</th>" for h in headers)
tbody = ""
for i, row in enumerate(rows):
tds = ""
for j, cell in enumerate(row):
if j == 0:
if i == 0: badge_cls = "gold"
elif i == 1: badge_cls = "silver"
elif i == 2: badge_cls = "bronze"
else: badge_cls = ""
tds += f'<td><span class="tj-rank {badge_cls}">{cell}</span></td>'
else:
tds += f"<td>{cell}</td>"
tbody += f"<tr>{tds}</tr>"
return f'<table class="tj-table"><thead><tr>{th}</tr></thead><tbody>{tbody}</tbody></table>'