saiganesh2004 commited on
Commit
69f7c01
Β·
verified Β·
1 Parent(s): c25d60a

Update utils/ui.py

Browse files
Files changed (1) hide show
  1. utils/ui.py +214 -337
utils/ui.py CHANGED
@@ -2,113 +2,52 @@
2
  ui.py β€” Shared styles, CSS injection, and HTML component helpers.
3
  """
4
 
5
- FONTS = "@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&display=swap');"
6
-
7
- BASE_CSS = f"""<style>
8
- {FONTS}
9
- :root {{
10
- --bg:#07070f;
11
- --bg2:#0e0e1a;
12
- --bg3:#151525;
13
- --border:rgba(99,102,241,0.18);
14
- --border2:rgba(99,102,241,0.35);
15
- --accent:#6366f1;
16
- --accent2:#818cf8;
17
- --accent-g:linear-gradient(135deg,#6366f1,#8b5cf6);
18
- --text:#f1f0ff;
19
- --muted:rgba(241,240,255,0.42);
20
- --muted2:rgba(241,240,255,0.22);
21
- --success:#22d3a5;
22
- --warning:#f59e0b;
23
- --danger:#f43f5e;
24
- --radius:14px;
25
- --radius-sm:8px;
26
- }}
27
-
28
- /* ═══ BASE STYLES (apply to all pages) ═══ */
29
- html {{
30
- scroll-behavior: auto !important;
31
- background: var(--bg) !important;
32
- height: 100% !important;
33
- }}
34
-
35
- body {{
36
- background: var(--bg) !important;
37
- color: var(--text) !important;
38
- font-family: 'DM Sans', sans-serif !important;
39
- min-height: 100vh !important;
40
- margin: 0 !important;
41
- padding: 0 !important;
42
- overflow-x: hidden !important;
43
- }}
44
-
45
- /* ═══ DASHBOARD-SPECIFIC FIXES (only apply to dashboard pages) ═══ */
46
- [data-testid="stAppViewContainer"]:has([data-testid="stSidebar"]) {{
47
- overflow: hidden !important;
48
- position: fixed !important;
49
- top: 0 !important;
50
- left: 0 !important;
51
- right: 0 !important;
52
- bottom: 0 !important;
53
- }}
54
-
55
- [data-testid="stAppViewContainer"]:has([data-testid="stSidebar"]) > section {{
56
- height: 100vh !important;
57
- overflow-y: auto !important;
58
- overflow-x: hidden !important;
59
- }}
60
-
61
- /* Prevent keyboard scrolling ONLY on dashboard pages */
62
- [data-testid="stAppViewContainer"]:has([data-testid="stSidebar"]) input,
63
- [data-testid="stAppViewContainer"]:has([data-testid="stSidebar"]) textarea,
64
- [data-testid="stAppViewContainer"]:has([data-testid="stSidebar"]) select,
65
- [data-testid="stAppViewContainer"]:has([data-testid="stSidebar"]) button {{
66
- scroll-margin: 0 !important;
67
- scroll-snap-margin: 0 !important;
68
- }}
69
-
70
- /* ═══ NUCLEAR CHROME REMOVAL (all pages) ═══ */
71
  #MainMenu,footer,header,
72
  [data-testid="stToolbar"],
73
- [data-testid="stToolbarActions"],
74
- [data-testid="stHeader"],
75
  [data-testid="stDecoration"],
76
- [data-testid="stStatusWidget"],
77
  [data-testid="stSidebarNav"],
78
- [data-testid="collapsedControl"],
79
- [data-testid="baseButton-header"],
80
- .stDeployButton,
81
- .stHamburger,
82
- button[kind="header"] {{
83
- display:none!important;
84
- visibility:hidden!important;
85
- opacity:0!important;
86
- pointer-events:none!important;
87
- height:0!important;
88
- width:0!important;
89
- overflow:hidden!important;
90
- position:absolute!important;
91
- }}
92
-
93
- /* ═══ COMMON LAYOUT ═══ */
94
- .stApp {{
95
- background: var(--bg) !important;
96
- color: var(--text) !important;
97
- font-family: 'DM Sans', sans-serif !important;
98
- }}
99
-
100
- [data-testid="stAppViewContainer"] > section > div.block-container {{
101
- max-width: 1160px !important;
102
- width: 100% !important;
103
- margin: 0 auto !important;
104
- padding: 0 24px 80px !important;
105
- }}
106
-
107
- .main .block-container {{
108
- padding-top: 0 !important;
109
- }}
110
-
111
- /* ─── FORM ELEMENTS (all pages) ─── */
112
  [data-testid="stWidgetLabel"],
113
  [data-testid="stWidgetLabel"] p,
114
  [data-testid="stWidgetLabel"] span,
@@ -117,245 +56,168 @@ button[kind="header"] {{
117
  .stSelectbox>label,
118
  .stMultiSelect>label,
119
  .stCheckbox>label,
120
- .stRadio>label {{
121
- color: var(--text) !important;
122
- font-size: 0.78rem !important;
123
- font-weight: 600 !important;
124
- letter-spacing: 1.5px !important;
125
- text-transform: uppercase !important;
126
- opacity: 1 !important;
127
- }}
128
-
129
- /* Inputs */
130
  div[data-baseweb="input"]>div,
131
- div[data-baseweb="textarea"]>div {{
132
- background: var(--bg3) !important;
133
- border: 1.5px solid var(--border) !important;
134
- border-radius: var(--radius-sm) !important;
135
- transition: border-color 0.2s !important;
136
- }}
137
-
138
- div[data-baseweb="input"]:focus-within>div {{
139
- border-color: var(--accent) !important;
140
- box-shadow: 0 0 0 3px rgba(99,102,241,0.12) !important;
141
- }}
142
-
143
- div[data-baseweb="input"] input {{
144
- background: transparent !important;
145
- color: var(--text) !important;
146
- font-family: 'DM Sans', sans-serif !important;
147
- }}
148
-
149
- /* Select */
150
- div[data-baseweb="select"]>div {{
151
- background: var(--bg3) !important;
152
- border: 1.5px solid var(--border) !important;
153
- border-radius: var(--radius-sm) !important;
154
- color: var(--text) !important;
155
- }}
156
-
157
- div[data-baseweb="select"] span {{ color: var(--text) !important; }}
158
-
159
- div[data-baseweb="popover"] {{
160
- background: var(--bg2) !important;
161
- border: 1px solid var(--border) !important;
162
- border-radius: var(--radius) !important;
163
- box-shadow: 0 20px 60px rgba(0,0,0,0.8) !important;
164
- }}
165
-
166
- li[role="option"] {{
167
- color: var(--muted) !important;
168
- font-family: 'DM Sans', sans-serif !important;
169
- border-radius: 6px !important;
170
- margin: 2px 6px !important;
171
- }}
172
-
173
- li[role="option"]:hover {{ background: rgba(99,102,241,0.14) !important; color: var(--text) !important; }}
174
- li[aria-selected="true"] {{ background: rgba(99,102,241,0.22) !important; color: var(--text) !important; }}
175
-
176
- /* Multiselect */
177
- div[data-baseweb="multi-select"]>div {{
178
- background: var(--bg3) !important;
179
- border: 1.5px solid var(--border) !important;
180
- border-radius: var(--radius-sm) !important;
181
- min-height: 48px !important;
182
- }}
183
-
184
- span[data-baseweb="tag"] {{
185
- background: rgba(99,102,241,0.18) !important;
186
- border: 1px solid rgba(99,102,241,0.35) !important;
187
- border-radius: 5px !important;
188
- }}
189
-
190
- span[data-baseweb="tag"] span {{ color: var(--text) !important; font-size: 0.82rem !important; }}
191
-
192
- /* Buttons */
193
- .stButton>button {{
194
- background: var(--accent-g) !important;
195
- border: none !important;
196
- color: #fff !important;
197
- border-radius: var(--radius-sm) !important;
198
- font-family: 'DM Sans', sans-serif !important;
199
- font-size: 0.88rem !important;
200
- font-weight: 600 !important;
201
- padding: 10px 22px !important;
202
- transition: all 0.2s !important;
203
- box-shadow: 0 4px 18px rgba(99,102,241,0.30) !important;
204
- }}
205
-
206
- .stButton>button:hover {{
207
- opacity: 0.9 !important;
208
- transform: translateY(-2px) !important;
209
- box-shadow: 0 8px 28px rgba(99,102,241,0.45) !important;
210
- }}
211
-
212
- /* Tabs */
213
- .stTabs [data-baseweb="tab-list"] {{
214
- background: var(--bg2) !important;
215
- border-radius: var(--radius-sm) !important;
216
- padding: 4px !important;
217
- gap: 3px !important;
218
- border: 1px solid var(--border) !important;
219
- }}
220
-
221
- .stTabs [data-baseweb="tab"] {{
222
- background: transparent !important;
223
- color: var(--muted) !important;
224
- border-radius: 6px !important;
225
- font-family: 'DM Sans', sans-serif !important;
226
- font-size: 0.82rem !important;
227
- font-weight: 500 !important;
228
- border: none !important;
229
- padding: 8px 16px !important;
230
- }}
231
-
232
- .stTabs [aria-selected="true"] {{
233
- background: var(--accent-g) !important;
234
- color: #fff !important;
235
- box-shadow: 0 2px 10px rgba(99,102,241,0.35) !important;
236
- }}
237
-
238
  .stTabs [data-baseweb="tab-highlight"],
239
- .stTabs [data-baseweb="tab-border"] {{ display: none !important; }}
240
-
241
- /* Sidebar */
242
- section[data-testid="stSidebar"] {{
243
- display: block !important;
244
- background: var(--bg2) !important;
245
- border-right: 1px solid var(--border) !important;
246
- }}
247
-
248
- section[data-testid="stSidebar"]>div {{ padding: 20px 16px !important; }}
249
-
250
- /* Misc */
251
- hr {{ border-color: var(--border) !important; margin: 20px 0 !important; }}
252
- .stProgress>div>div {{ background: var(--accent-g) !important; border-radius: 100px !important; }}
253
- .stProgress>div {{ background: var(--bg3) !important; border-radius: 100px !important; }}
254
- .stSpinner>div {{ border-top-color: var(--accent) !important; }}
255
-
256
- [data-testid="stMetric"] {{
257
- background: var(--bg2) !important;
258
- border: 1px solid var(--border) !important;
259
- border-radius: var(--radius) !important;
260
- padding: 16px !important;
261
- }}
262
-
263
- [data-testid="stMetricValue"] {{
264
- font-family: 'Syne', sans-serif !important;
265
- color: var(--text) !important;
266
- }}
267
-
268
- [data-testid="stMetricLabel"] {{
269
- color: var(--muted) !important;
270
- font-size: 0.65rem !important;
271
- letter-spacing: 2px !important;
272
- text-transform: uppercase !important;
273
- }}
274
- </style>"""
275
 
276
  def inject_css():
277
  import streamlit as st
278
-
279
- # Inject the base CSS
280
  st.markdown(BASE_CSS, unsafe_allow_html=True)
281
-
282
- # JavaScript that runs after page load
283
- HIDE_CHROME_JS = """
284
- <script>
285
- (function() {
286
- // Check if we're on a dashboard page (has sidebar)
287
- function isDashboardPage() {
288
- return !!document.querySelector('[data-testid="stSidebar"]');
289
- }
290
-
291
- // Only apply keyboard prevention on dashboard pages
292
- if (isDashboardPage()) {
293
- document.addEventListener('keydown', function(e) {
294
- const keysToPrevent = [
295
- 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
296
- 'PageUp', 'PageDown', 'Home', 'End', ' ',
297
- 'Space', 'Spacebar'
298
- ];
299
-
300
- if (keysToPrevent.includes(e.key) || e.key === ' ') {
301
- e.preventDefault();
302
- return false;
303
- }
304
- }, false);
305
- }
306
-
307
- // Prevent scroll restoration on all pages
308
- if ('scrollRestoration' in history) {
309
- history.scrollRestoration = 'manual';
310
- }
311
-
312
- // Force scroll to top on all pages
313
- window.scrollTo(0, 0);
314
-
315
- // Hide Streamlit chrome on all pages
316
- var HIDE = [
317
- '#MainMenu','header','footer',
318
- '[data-testid="stHeader"]',
319
- '[data-testid="stToolbar"]',
320
- '[data-testid="stToolbarActions"]',
321
- '[data-testid="stDecoration"]',
322
- '[data-testid="stStatusWidget"]',
323
- '[data-testid="collapsedControl"]',
324
- '[data-testid="baseButton-header"]',
325
- '.stDeployButton',
326
- 'button[kind="header"]',
327
- '[data-testid="stSidebarNav"]'
328
- ];
329
-
330
- function nuke() {
331
- HIDE.forEach(function(sel) {
332
- document.querySelectorAll(sel).forEach(function(el) {
333
- el.style.cssText = 'display:none!important;visibility:hidden!important;' +
334
- 'opacity:0!important;height:0!important;width:0!important;' +
335
- 'position:absolute!important;pointer-events:none!important;overflow:hidden!important;';
336
- });
337
- });
338
- }
339
-
340
- nuke();
341
- new MutationObserver(nuke).observe(document.documentElement,
342
- {childList: true, subtree: true, attributes: false});
343
-
344
- // Ensure body is visible and properly positioned
345
- document.body.style.visibility = 'visible';
346
- document.body.style.position = 'relative';
347
- document.body.style.minHeight = '100vh';
348
-
349
- })();
350
- </script>
351
- """
352
-
353
- st.markdown(HIDE_CHROME_JS, unsafe_allow_html=True)
354
 
355
  def stat_card(icon, label, value, sub="", accent="var(--accent)"):
356
  return f"""
357
  <div style="background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);
358
- padding:20px 16px;text-align:center;position:relative;overflow:hidden">
 
359
  <div style="position:absolute;top:0;left:15%;right:15%;height:2px;
360
  background:linear-gradient(90deg,transparent,{accent},transparent)"></div>
361
  <div style="font-size:1.4rem;margin-bottom:8px">{icon}</div>
@@ -366,14 +228,29 @@ def stat_card(icon, label, value, sub="", accent="var(--accent)"):
366
  {f'<div style="font-size:0.72rem;color:var(--muted2);margin-top:3px">{sub}</div>' if sub else ''}
367
  </div>"""
368
 
369
- def section_header(title, sub=""):
370
  return f"""
371
  <div style="display:flex;align-items:center;gap:12px;margin:28px 0 16px">
372
- <div style="width:3px;height:22px;background:var(--accent-g);
373
- border-radius:2px;flex-shrink:0"></div>
374
  <div>
375
- <div style="font-family:'Syne',sans-serif;font-size:1.05rem;font-weight:700;
376
- color:var(--text)">{title}</div>
377
  {f'<div style="font-size:0.75rem;color:var(--muted);margin-top:1px">{sub}</div>' if sub else ''}
378
  </div>
379
  </div>"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  ui.py β€” Shared styles, CSS injection, and HTML component helpers.
3
  """
4
 
5
+ FONTS = """@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&display=swap');"""
6
+
7
+ # ── Design tokens ──────────────────────────────────────────────────────────────
8
+ BASE_CSS = """
9
+ <style>
10
+ """ + FONTS + """
11
+ :root {
12
+ --bg: #07070f;
13
+ --bg2: #0e0e1a;
14
+ --bg3: #151525;
15
+ --border: rgba(99,102,241,0.18);
16
+ --border2: rgba(99,102,241,0.35);
17
+ --accent: #6366f1;
18
+ --accent2: #818cf8;
19
+ --accent-g: linear-gradient(135deg,#6366f1,#8b5cf6);
20
+ --text: #f1f0ff;
21
+ --muted: rgba(241,240,255,0.42);
22
+ --muted2: rgba(241,240,255,0.22);
23
+ --success: #22d3a5;
24
+ --warning: #f59e0b;
25
+ --danger: #f43f5e;
26
+ --radius: 14px;
27
+ --radius-sm: 8px;
28
+ }
29
+
30
+ /* ── Reset chrome ── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  #MainMenu,footer,header,
32
  [data-testid="stToolbar"],
 
 
33
  [data-testid="stDecoration"],
 
34
  [data-testid="stSidebarNav"],
35
+ section[data-testid="stSidebar"] { display:none!important; }
36
+
37
+ html,body,.stApp,
38
+ [data-testid="stAppViewContainer"] {
39
+ background:var(--bg)!important;
40
+ color:var(--text)!important;
41
+ font-family:'DM Sans',sans-serif!important;
42
+ }
43
+
44
+ [data-testid="stAppViewContainer"]>section>div.block-container {
45
+ max-width:1160px!important;
46
+ margin:0 auto!important;
47
+ padding:0 24px 80px!important;
48
+ }
49
+
50
+ /* ── Labels ── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  [data-testid="stWidgetLabel"],
52
  [data-testid="stWidgetLabel"] p,
53
  [data-testid="stWidgetLabel"] span,
 
56
  .stSelectbox>label,
57
  .stMultiSelect>label,
58
  .stCheckbox>label,
59
+ .stRadio>label {
60
+ color:var(--text)!important;
61
+ font-size:0.78rem!important;
62
+ font-weight:600!important;
63
+ letter-spacing:1.5px!important;
64
+ text-transform:uppercase!important;
65
+ opacity:1!important;
66
+ }
67
+
68
+ /* ── Inputs ── */
69
  div[data-baseweb="input"]>div,
70
+ div[data-baseweb="textarea"]>div {
71
+ background:var(--bg3)!important;
72
+ border:1.5px solid var(--border)!important;
73
+ border-radius:var(--radius-sm)!important;
74
+ transition:border-color 0.2s!important;
75
+ }
76
+ div[data-baseweb="input"]:focus-within>div,
77
+ div[data-baseweb="textarea"]:focus-within>div {
78
+ border-color:var(--accent)!important;
79
+ box-shadow:0 0 0 3px rgba(99,102,241,0.12)!important;
80
+ }
81
+ div[data-baseweb="input"] input,
82
+ div[data-baseweb="textarea"] textarea {
83
+ background:transparent!important;
84
+ color:var(--text)!important;
85
+ font-family:'DM Sans',sans-serif!important;
86
+ }
87
+
88
+ /* ── Select ── */
89
+ div[data-baseweb="select"]>div {
90
+ background:var(--bg3)!important;
91
+ border:1.5px solid var(--border)!important;
92
+ border-radius:var(--radius-sm)!important;
93
+ color:var(--text)!important;
94
+ }
95
+ div[data-baseweb="select"]>div:focus-within {
96
+ border-color:var(--accent)!important;
97
+ }
98
+ div[data-baseweb="select"] span { color:var(--text)!important; }
99
+ div[data-baseweb="popover"] {
100
+ background:var(--bg2)!important;
101
+ border:1px solid var(--border)!important;
102
+ border-radius:var(--radius)!important;
103
+ box-shadow:0 20px 60px rgba(0,0,0,0.8)!important;
104
+ }
105
+ li[role="option"] {
106
+ color:var(--muted)!important;
107
+ font-family:'DM Sans',sans-serif!important;
108
+ border-radius:6px!important;
109
+ margin:2px 6px!important;
110
+ }
111
+ li[role="option"]:hover { background:rgba(99,102,241,0.14)!important; color:var(--text)!important; }
112
+ li[aria-selected="true"] { background:rgba(99,102,241,0.22)!important; color:var(--text)!important; }
113
+
114
+ /* ── Multiselect ── */
115
+ div[data-baseweb="multi-select"]>div {
116
+ background:var(--bg3)!important;
117
+ border:1.5px solid var(--border)!important;
118
+ border-radius:var(--radius-sm)!important;
119
+ min-height:48px!important;
120
+ }
121
+ span[data-baseweb="tag"] {
122
+ background:rgba(99,102,241,0.18)!important;
123
+ border:1px solid rgba(99,102,241,0.35)!important;
124
+ border-radius:5px!important;
125
+ }
126
+ span[data-baseweb="tag"] span { color:var(--text)!important; font-size:0.82rem!important; }
127
+
128
+ /* ── Buttons ── */
129
+ .stButton>button {
130
+ background:var(--accent-g)!important;
131
+ border:none!important;
132
+ color:#fff!important;
133
+ border-radius:var(--radius-sm)!important;
134
+ font-family:'DM Sans',sans-serif!important;
135
+ font-size:0.88rem!important;
136
+ font-weight:600!important;
137
+ padding:10px 22px!important;
138
+ letter-spacing:0.3px!important;
139
+ transition:all 0.2s!important;
140
+ box-shadow:0 4px 18px rgba(99,102,241,0.30)!important;
141
+ }
142
+ .stButton>button:hover {
143
+ opacity:0.9!important;
144
+ transform:translateY(-2px)!important;
145
+ box-shadow:0 8px 28px rgba(99,102,241,0.45)!important;
146
+ }
147
+
148
+ /* ── Tabs ── */
149
+ .stTabs [data-baseweb="tab-list"] {
150
+ background:var(--bg2)!important;
151
+ border-radius:var(--radius-sm)!important;
152
+ padding:4px!important;
153
+ gap:3px!important;
154
+ border:1px solid var(--border)!important;
155
+ }
156
+ .stTabs [data-baseweb="tab"] {
157
+ background:transparent!important;
158
+ color:var(--muted)!important;
159
+ border-radius:6px!important;
160
+ font-family:'DM Sans',sans-serif!important;
161
+ font-size:0.82rem!important;
162
+ font-weight:500!important;
163
+ border:none!important;
164
+ padding:8px 16px!important;
165
+ }
166
+ .stTabs [aria-selected="true"] {
167
+ background:var(--accent-g)!important;
168
+ color:#fff!important;
169
+ box-shadow:0 2px 10px rgba(99,102,241,0.35)!important;
170
+ }
 
 
 
 
 
 
171
  .stTabs [data-baseweb="tab-highlight"],
172
+ .stTabs [data-baseweb="tab-border"] { display:none!important; }
173
+
174
+ /* ── Progress ── */
175
+ .stProgress>div>div { background:var(--accent-g)!important; border-radius:100px!important; }
176
+ .stProgress>div { background:var(--bg3)!important; border-radius:100px!important; }
177
+
178
+ /* ── Spinner ── */
179
+ .stSpinner>div { border-top-color:var(--accent)!important; }
180
+
181
+ /* ── Metric ── */
182
+ [data-testid="stMetric"] {
183
+ background:var(--bg2)!important;
184
+ border:1px solid var(--border)!important;
185
+ border-radius:var(--radius)!important;
186
+ padding:16px!important;
187
+ }
188
+ [data-testid="stMetricValue"] {
189
+ font-family:'Syne',sans-serif!important;
190
+ color:var(--text)!important;
191
+ }
192
+ [data-testid="stMetricLabel"] {
193
+ color:var(--muted)!important;
194
+ font-size:0.65rem!important;
195
+ letter-spacing:2px!important;
196
+ text-transform:uppercase!important;
197
+ }
198
+
199
+ hr { border-color:var(--border)!important; margin:20px 0!important; }
200
+ </style>
201
+ """
 
 
 
 
 
 
202
 
203
  def inject_css():
204
  import streamlit as st
 
 
205
  st.markdown(BASE_CSS, unsafe_allow_html=True)
206
+
207
+ # ── Component builders ─────────────────────────────────────────────────────────
208
+ def card(content: str, cls: str = "") -> str:
209
+ return f"""<div style="background:var(--bg2);border:1px solid var(--border);
210
+ border-radius:var(--radius);padding:24px;{cls}">{content}</div>"""
211
+
212
+ def badge(text: str, color: str = "var(--accent)") -> str:
213
+ return (f'<span style="display:inline-block;background:{color}22;border:1px solid {color}55;'
214
+ f'border-radius:4px;padding:2px 9px;font-size:0.72rem;font-weight:600;color:{color}">{text}</span>')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
  def stat_card(icon, label, value, sub="", accent="var(--accent)"):
217
  return f"""
218
  <div style="background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);
219
+ padding:20px 16px;text-align:center;position:relative;overflow:hidden;
220
+ transition:transform 0.2s,border-color 0.2s">
221
  <div style="position:absolute;top:0;left:15%;right:15%;height:2px;
222
  background:linear-gradient(90deg,transparent,{accent},transparent)"></div>
223
  <div style="font-size:1.4rem;margin-bottom:8px">{icon}</div>
 
228
  {f'<div style="font-size:0.72rem;color:var(--muted2);margin-top:3px">{sub}</div>' if sub else ''}
229
  </div>"""
230
 
231
+ def section_header(title: str, sub: str = "") -> str:
232
  return f"""
233
  <div style="display:flex;align-items:center;gap:12px;margin:28px 0 16px">
234
+ <div style="width:3px;height:22px;background:var(--accent-g);border-radius:2px;flex-shrink:0"></div>
 
235
  <div>
236
+ <div style="font-family:'Syne',sans-serif;font-size:1.05rem;font-weight:700;color:var(--text)">{title}</div>
 
237
  {f'<div style="font-size:0.75rem;color:var(--muted);margin-top:1px">{sub}</div>' if sub else ''}
238
  </div>
239
  </div>"""
240
+
241
+ def topnav(username: str, page: str = "") -> str:
242
+ return f"""
243
+ <div style="display:flex;align-items:center;justify-content:space-between;
244
+ padding:14px 0 12px;border-bottom:1px solid var(--border);margin-bottom:24px;
245
+ position:sticky;top:0;z-index:100;background:rgba(7,7,15,0.95);
246
+ backdrop-filter:blur(20px)">
247
+ <div style="font-family:'Syne',sans-serif;font-size:1.5rem;font-weight:800;
248
+ background:var(--accent-g);-webkit-background-clip:text;-webkit-text-fill-color:transparent;
249
+ letter-spacing:1px">⚑ FitPro AI</div>
250
+ <div style="display:flex;align-items:center;gap:10px">
251
+ <span style="font-size:0.78rem;color:var(--muted)">
252
+ <span style="color:var(--text);font-weight:600">{username}</span>
253
+ </span>
254
+ {'<span style="font-size:0.65rem;font-weight:700;letter-spacing:2px;text-transform:uppercase;padding:3px 10px;border:1px solid var(--border2);border-radius:100px;color:var(--accent2)">'+page+'</span>' if page else ''}
255
+ </div>
256
+ </div>"""