Spaces:
Sleeping
Sleeping
| """ | |
| ui.py β Expert-level dark design system with fixed calendar + vivid components. | |
| """ | |
| BASE_CSS = """ | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700&family=JetBrains+Mono:wght@400;500&display=swap'); | |
| :root { | |
| --bg: #07070f; | |
| --bg2: #0d0d1a; | |
| --bg3: #13132a; | |
| --bg4: #1a1a35; | |
| --border: rgba(120,119,198,0.18); | |
| --border2: rgba(120,119,198,0.35); | |
| --accent: #7c7cf7; | |
| --accent2: #a78bfa; | |
| --green: #10d9a0; | |
| --amber: #f59e0b; | |
| --red: #f43f5e; | |
| --text: #eeeeff; | |
| --text2: rgba(238,238,255,0.75); | |
| --muted: rgba(238,238,255,0.45); | |
| --muted2: rgba(238,238,255,0.25); | |
| --muted3: rgba(238,238,255,0.12); | |
| --radius: 14px; | |
| --radius-sm:9px; | |
| --font-head:'Syne', sans-serif; | |
| --font-body:'DM Sans', sans-serif; | |
| --font-mono:'JetBrains Mono', monospace; | |
| } | |
| #MainMenu,footer,header, | |
| [data-testid="stToolbar"], | |
| [data-testid="stDecoration"], | |
| [data-testid="stSidebarNav"], | |
| section[data-testid="stSidebar"] { display:none!important; } | |
| html,body { background:#07070f!important; } | |
| .stApp, | |
| [data-testid="stAppViewContainer"], | |
| [data-testid="stAppViewContainer"]>section { | |
| background:#07070f!important; | |
| color:#eeeeff!important; | |
| font-family:'DM Sans',sans-serif!important; | |
| } | |
| [data-testid="stAppViewContainer"]>section>div.block-container { | |
| max-width:1220px!important; | |
| margin:0 auto!important; | |
| padding:0 28px 100px!important; | |
| background:transparent!important; | |
| } | |
| h1,h2,h3,h4 { font-family:'Syne',sans-serif!important; color:#eeeeff!important; } | |
| [data-testid="stWidgetLabel"] p, | |
| [data-testid="stWidgetLabel"] span, | |
| .stTextInput>label, .stNumberInput>label, | |
| .stSelectbox>label, .stMultiSelect>label, | |
| .stCheckbox>label, .stRadio>label { | |
| color:rgba(238,238,255,0.55)!important; | |
| font-size:0.70rem!important; font-weight:600!important; | |
| letter-spacing:1.8px!important; text-transform:uppercase!important; | |
| } | |
| div[data-baseweb="input"]>div, | |
| div[data-baseweb="textarea"]>div { | |
| background:#0f0f22!important; | |
| border:1.5px solid rgba(120,119,198,0.22)!important; | |
| border-radius:9px!important; transition:all 0.2s!important; | |
| } | |
| div[data-baseweb="input"]:focus-within>div, | |
| div[data-baseweb="textarea"]:focus-within>div { | |
| border-color:#7c7cf7!important; | |
| box-shadow:0 0 0 3px rgba(124,124,247,0.15)!important; | |
| background:#13132a!important; | |
| } | |
| div[data-baseweb="input"] input, | |
| div[data-baseweb="textarea"] textarea { | |
| background:transparent!important; color:#eeeeff!important; | |
| font-family:'DM Sans',sans-serif!important; font-size:0.92rem!important; | |
| } | |
| div[data-baseweb="input"] input::placeholder { color:rgba(238,238,255,0.22)!important; } | |
| div[data-baseweb="select"]>div { | |
| background:#0f0f22!important; | |
| border:1.5px solid rgba(120,119,198,0.22)!important; | |
| border-radius:9px!important; color:#eeeeff!important; transition:all 0.2s!important; | |
| } | |
| div[data-baseweb="select"]>div:focus-within { | |
| border-color:#7c7cf7!important; box-shadow:0 0 0 3px rgba(124,124,247,0.15)!important; | |
| } | |
| div[data-baseweb="select"] span { color:#eeeeff!important; } | |
| div[data-baseweb="popover"] { | |
| background:#0d0d1a!important; | |
| border:1px solid rgba(120,119,198,0.28)!important; | |
| border-radius:12px!important; box-shadow:0 24px 64px rgba(0,0,0,0.85)!important; | |
| } | |
| li[role="option"] { | |
| color:rgba(238,238,255,0.45)!important; | |
| font-family:'DM Sans',sans-serif!important; | |
| border-radius:6px!important; margin:2px 6px!important; | |
| } | |
| li[role="option"]:hover { background:rgba(124,124,247,0.18)!important; color:#eeeeff!important; } | |
| li[aria-selected="true"] { background:rgba(124,124,247,0.25)!important; color:#eeeeff!important; } | |
| div[data-baseweb="multi-select"]>div { | |
| background:#0f0f22!important; | |
| border:1.5px solid rgba(120,119,198,0.22)!important; | |
| border-radius:9px!important; min-height:46px!important; | |
| } | |
| span[data-baseweb="tag"] { | |
| background:rgba(124,124,247,0.20)!important; | |
| border:1px solid rgba(124,124,247,0.38)!important; border-radius:5px!important; | |
| } | |
| span[data-baseweb="tag"] span { color:#eeeeff!important; font-size:0.80rem!important; } | |
| [data-testid="stCheckbox"] label { | |
| color:rgba(238,238,255,0.75)!important; font-size:0.88rem!important; | |
| letter-spacing:0!important; text-transform:none!important; font-weight:400!important; | |
| } | |
| .stButton>button { | |
| background:linear-gradient(135deg,#5b5bd6,#7c3aed)!important; | |
| border:none!important; color:#fff!important; border-radius:9px!important; | |
| font-family:'DM Sans',sans-serif!important; font-size:0.87rem!important; | |
| font-weight:600!important; padding:11px 22px!important; | |
| letter-spacing:0.3px!important; transition:all 0.2s ease!important; | |
| box-shadow:0 4px 18px rgba(91,91,214,0.35)!important; | |
| } | |
| .stButton>button:hover { | |
| transform:translateY(-2px)!important; | |
| box-shadow:0 10px 34px rgba(91,91,214,0.56)!important; | |
| filter:brightness(1.10)!important; | |
| } | |
| .stButton>button:active { transform:translateY(0)!important; } | |
| .stTabs [data-baseweb="tab-list"] { | |
| background:#0d0d1a!important; border-radius:9px!important; | |
| padding:4px!important; gap:3px!important; | |
| border:1px solid rgba(120,119,198,0.18)!important; | |
| } | |
| .stTabs [data-baseweb="tab"] { | |
| background:transparent!important; color:rgba(238,238,255,0.45)!important; | |
| border-radius:6px!important; font-family:'DM Sans',sans-serif!important; | |
| font-size:0.84rem!important; font-weight:500!important; | |
| border:none!important; padding:9px 18px!important; transition:all 0.2s!important; | |
| } | |
| .stTabs [aria-selected="true"] { | |
| background:linear-gradient(135deg,#5b5bd6,#7c3aed)!important; | |
| color:#fff!important; box-shadow:0 2px 14px rgba(91,91,214,0.45)!important; | |
| } | |
| .stTabs [data-baseweb="tab-highlight"], | |
| .stTabs [data-baseweb="tab-border"] { display:none!important; } | |
| .stProgress>div>div { | |
| background:linear-gradient(90deg,#5b5bd6,#a78bfa)!important; border-radius:100px!important; | |
| } | |
| .stProgress>div { background:rgba(255,255,255,0.06)!important; border-radius:100px!important; } | |
| .stAlert { border-radius:9px!important; font-size:0.88rem!important; } | |
| hr { border-color:rgba(120,119,198,0.14)!important; margin:20px 0!important; } | |
| ::-webkit-scrollbar { width:5px; height:5px; } | |
| ::-webkit-scrollbar-track { background:#07070f; } | |
| ::-webkit-scrollbar-thumb { background:rgba(124,124,247,0.30); border-radius:100px; } | |
| ::-webkit-scrollbar-thumb:hover { background:rgba(124,124,247,0.55); } | |
| @keyframes fadeUp { | |
| from { opacity:0; transform:translateY(10px); } | |
| to { opacity:1; transform:translateY(0); } | |
| } | |
| </style> | |
| """ | |
| def inject_css(): | |
| import streamlit as st | |
| st.markdown(BASE_CSS, unsafe_allow_html=True) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # COMPONENTS | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def stat_card(icon, label, value, sub="", accent="#7c7cf7", glow=True): | |
| r, g, b = _hex_to_rgb_tuple(accent) | |
| glow_css = f"box-shadow:0 0 28px rgba({r},{g},{b},0.22);" if glow else "" | |
| return f""" | |
| <div style="background:#0f0f22;border:1px solid rgba({r},{g},{b},0.25); | |
| border-radius:14px;padding:22px 16px;text-align:center;position:relative; | |
| overflow:hidden;animation:fadeUp 0.4s ease;{glow_css}"> | |
| <div style="position:absolute;top:0;left:15%;right:15%;height:2px; | |
| background:linear-gradient(90deg,transparent,{accent},transparent);opacity:0.8"></div> | |
| <div style="font-size:1.55rem;margin-bottom:10px;line-height:1">{icon}</div> | |
| <div style="font-family:'Syne',sans-serif;font-size:2.1rem;font-weight:800; | |
| color:{accent};line-height:1;letter-spacing:-1.5px">{value}</div> | |
| <div style="font-size:0.59rem;font-weight:700;letter-spacing:2.5px; | |
| text-transform:uppercase;color:rgba(238,238,255,0.38);margin-top:8px">{label}</div> | |
| {f'<div style="font-size:0.72rem;color:rgba(238,238,255,0.22);margin-top:3px">{sub}</div>' if sub else ''} | |
| </div>""" | |
| def section_header(title, sub="", icon=""): | |
| return f""" | |
| <div style="display:flex;align-items:center;gap:13px;margin:30px 0 16px;animation:fadeUp 0.35s ease"> | |
| <div style="width:3px;height:22px;background:linear-gradient(180deg,#5b5bd6,#a78bfa); | |
| border-radius:2px;flex-shrink:0"></div> | |
| <div> | |
| <div style="font-family:'Syne',sans-serif;font-size:1.08rem;font-weight:700; | |
| color:#eeeeff;line-height:1.15">{icon+' ' if icon else ''}{title}</div> | |
| {f'<div style="font-size:0.74rem;color:rgba(238,238,255,0.42);margin-top:3px">{sub}</div>' if sub else ''} | |
| </div> | |
| </div>""" | |
| def badge(text, color="#7c7cf7"): | |
| return (f'<span style="display:inline-flex;align-items:center;background:{color}1a;' | |
| f'border:1px solid {color}44;border-radius:5px;padding:2px 10px;' | |
| f'font-size:0.70rem;font-weight:700;color:{color}">{text}</span>') | |
| def exercise_card(icon, name, sets, reps, rest, idx=0, desc=""): | |
| desc_html = (f'<div style="font-size:0.70rem;color:rgba(238,238,255,0.30);' | |
| f'margin-top:3px;line-height:1.5">{desc}</div>') if desc else "" | |
| sets_reps = (f'<span style="background:rgba(124,124,247,0.14);border:1px solid rgba(124,124,247,0.26);' | |
| f'border-radius:6px;padding:3px 10px;font-size:0.70rem;font-weight:700;' | |
| f'color:#a78bfa">{sets}x{reps}</span>') if sets else "" | |
| rest_badge = (f'<span style="background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.22);' | |
| f'border-radius:6px;padding:3px 10px;font-size:0.70rem;font-weight:600;' | |
| f'color:#f59e0b">{rest}</span>') if rest and rest != "." else "" | |
| return f""" | |
| <div style="display:flex;align-items:center;gap:12px;padding:10px; | |
| border-radius:9px;background:rgba(255,255,255,0.025); | |
| border:1px solid rgba(120,119,198,0.10);margin-bottom:5px"> | |
| <div style="width:34px;height:34px;border-radius:8px;flex-shrink:0; | |
| background:rgba(124,124,247,0.12);border:1px solid rgba(124,124,247,0.22); | |
| display:flex;align-items:center;justify-content:center;font-size:0.85rem">{icon}</div> | |
| <div style="flex:1;min-width:0"> | |
| <div style="font-size:0.85rem;font-weight:600;color:#eeeeff; | |
| white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{name}</div> | |
| {desc_html} | |
| </div> | |
| <div style="display:flex;gap:5px;flex-shrink:0">{sets_reps}{rest_badge}</div> | |
| </div>""" | |
| def progress_ring(pct, size=80, stroke=6, color="#7c7cf7"): | |
| r = (size - stroke) // 2 | |
| circ = 2 * 3.14159 * r | |
| offset = circ * (1 - pct / 100) | |
| return f""" | |
| <svg width="{size}" height="{size}" viewBox="0 0 {size} {size}"> | |
| <circle cx="{size//2}" cy="{size//2}" r="{r}" | |
| fill="none" stroke="rgba(124,124,247,0.12)" stroke-width="{stroke}"/> | |
| <circle cx="{size//2}" cy="{size//2}" r="{r}" | |
| fill="none" stroke="{color}" stroke-width="{stroke}" | |
| stroke-dasharray="{circ:.1f}" stroke-dashoffset="{offset:.1f}" | |
| stroke-linecap="round" transform="rotate(-90 {size//2} {size//2})"/> | |
| <text x="50%" y="50%" text-anchor="middle" dominant-baseline="central" | |
| font-family="Syne,sans-serif" font-size="{size//5}" font-weight="800" | |
| fill="{color}">{pct}%</text> | |
| </svg>""" | |
| def calendar_widget(tracking, num_days, today_str): | |
| """ | |
| Monthly calendar view showing full current month. | |
| Each cell displays the date number clearly, colored by workout status. | |
| """ | |
| from datetime import date, timedelta | |
| import calendar as cal_mod | |
| today = date.today() | |
| year = today.year | |
| month = today.month | |
| month_name = today.strftime("%B %Y") | |
| days_in_month = cal_mod.monthrange(year, month)[1] | |
| first_weekday = date(year, month, 1).weekday() # 0=Mon β¦ 6=Sun | |
| # ββ Day-of-week header ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| day_hdrs = "".join( | |
| f'<div style="text-align:center;font-size:0.63rem;font-weight:700;' | |
| f'letter-spacing:1px;text-transform:uppercase;' | |
| f'color:rgba(238,238,255,0.35);padding-bottom:6px">{d}</div>' | |
| for d in ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"] | |
| ) | |
| # ββ Blank leading cells βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| cells = "".join('<div></div>' for _ in range(first_weekday)) | |
| # ββ Day cells βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| for day_num in range(1, days_in_month + 1): | |
| d = date(year, month, day_num) | |
| ds = d.isoformat() | |
| s = tracking.get(ds, {}).get("status", "none") | |
| is_today = (d == today) | |
| is_future = (d > today) | |
| # colors | |
| if is_future: | |
| bg = "rgba(255,255,255,0.03)" | |
| num_col = "rgba(238,238,255,0.18)" | |
| bdr = "rgba(255,255,255,0.05)" | |
| elif s == "done": | |
| bg = "#3d3d9e" | |
| num_col = "#ffffff" | |
| bdr = "#5b5bd6" | |
| elif s == "skipped": | |
| bg = "rgba(245,158,11,0.22)" | |
| num_col = "#f5c842" | |
| bdr = "rgba(245,158,11,0.50)" | |
| elif d < today: # missed | |
| bg = "rgba(244,63,94,0.10)" | |
| num_col = "rgba(244,63,94,0.55)" | |
| bdr = "rgba(244,63,94,0.22)" | |
| else: # today, pending | |
| bg = "rgba(124,124,247,0.20)" | |
| num_col = "#c4b5fd" | |
| bdr = "rgba(124,124,247,0.60)" | |
| today_ring = ( | |
| "box-shadow:0 0 0 2px #7c7cf7,0 0 14px rgba(124,124,247,0.40);" | |
| ) if is_today else "" | |
| # small status dot below number | |
| dot = "" | |
| if s == "done": | |
| dot = '<div style="width:4px;height:4px;border-radius:50%;background:rgba(255,255,255,0.65);margin:2px auto 0"></div>' | |
| elif s == "skipped": | |
| dot = '<div style="width:4px;height:4px;border-radius:50%;background:#f59e0b;margin:2px auto 0"></div>' | |
| cells += f"""<div title="{ds}" | |
| style="background:{bg};border:1px solid {bdr};border-radius:7px; | |
| min-height:40px;display:flex;flex-direction:column;align-items:center; | |
| justify-content:center;cursor:pointer;padding:4px 2px;{today_ring} | |
| transition:transform 0.15s,opacity 0.15s" | |
| onmouseover="this.style.transform='scale(1.08)'" | |
| onmouseout="this.style.transform='scale(1)'"> | |
| <span style="font-size:0.84rem;font-weight:700;color:{num_col};line-height:1">{day_num}</span> | |
| {dot} | |
| </div>""" | |
| # ββ Legend + container ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| return f""" | |
| <div> | |
| <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px"> | |
| <span style="font-family:'Syne',sans-serif;font-size:0.90rem;font-weight:700;color:#eeeeff">{month_name}</span> | |
| <div style="display:flex;gap:12px"> | |
| <span style="display:flex;align-items:center;gap:5px;font-size:0.62rem;color:rgba(238,238,255,0.40)"> | |
| <span style="width:9px;height:9px;border-radius:2px;background:#3d3d9e;display:inline-block"></span>Done | |
| </span> | |
| <span style="display:flex;align-items:center;gap:5px;font-size:0.62rem;color:rgba(238,238,255,0.40)"> | |
| <span style="width:9px;height:9px;border-radius:2px;background:rgba(245,158,11,0.55);display:inline-block"></span>Skipped | |
| </span> | |
| <span style="display:flex;align-items:center;gap:5px;font-size:0.62rem;color:rgba(238,238,255,0.40)"> | |
| <span style="width:9px;height:9px;border-radius:2px;background:rgba(244,63,94,0.40);display:inline-block"></span>Missed | |
| </span> | |
| </div> | |
| </div> | |
| <div style="display:grid;grid-template-columns:repeat(7,1fr);gap:4px;margin-bottom:4px"> | |
| {day_hdrs} | |
| </div> | |
| <div style="display:grid;grid-template-columns:repeat(7,1fr);gap:4px"> | |
| {cells} | |
| </div> | |
| </div>""" | |
| # ββ internal helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _hex_to_rgb_tuple(hex_color): | |
| h = hex_color.lstrip('#') | |
| try: | |
| return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) | |
| except Exception: | |
| return 124, 124, 247 | |
| def _hex_to_rgb(hex_color): | |
| r, g, b = _hex_to_rgb_tuple(hex_color) | |
| return f"{r},{g},{b}" | |