Spaces:
Running on Zero
Running on Zero
Backtest Lab v1.0.0
Browse files- src/ui/components.py +1 -1
- src/ui/theme.py +264 -151
- tests/test_ui.py +118 -0
src/ui/components.py
CHANGED
|
@@ -112,7 +112,7 @@ def loading_stages(active: int = 0) -> str:
|
|
| 112 |
mark = "✓" if i < active else ("▸" if i == active else "·")
|
| 113 |
color = ("var(--accent-moss-strong)" if i < active else
|
| 114 |
"var(--text-primary)" if i == active else "var(--text-tertiary)")
|
| 115 |
-
rows.append(f'<div style="color:{color}
|
| 116 |
return f'<div class="bit-panel">{"".join(rows)}</div>'
|
| 117 |
|
| 118 |
|
|
|
|
| 112 |
mark = "✓" if i < active else ("▸" if i == active else "·")
|
| 113 |
color = ("var(--accent-moss-strong)" if i < active else
|
| 114 |
"var(--text-primary)" if i == active else "var(--text-tertiary)")
|
| 115 |
+
rows.append(f'<div class="bit-stage" style="color:{color}">{mark} {esc(label)}</div>')
|
| 116 |
return f'<div class="bit-panel">{"".join(rows)}</div>'
|
| 117 |
|
| 118 |
|
src/ui/theme.py
CHANGED
|
@@ -1,12 +1,28 @@
|
|
| 1 |
-
"""Gradio theme and CSS built from the Bit design system
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
from __future__ import annotations
|
|
@@ -17,11 +33,17 @@ import gradio as gr
|
|
| 17 |
|
| 18 |
ASSETS = Path(__file__).resolve().parent.parent.parent / "assets"
|
| 19 |
FONT_DIR = ASSETS / "fonts"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
def _font_face_css() -> str:
|
| 23 |
"""@font-face rules pointing at Gradio's static file route."""
|
| 24 |
faces = [
|
|
|
|
| 25 |
("Styrene A", "StyreneA-Light.otf", 300, "normal"),
|
| 26 |
("Styrene A", "StyreneA-Regular.otf", 400, "normal"),
|
| 27 |
("Styrene A", "StyreneA-Medium.otf", 500, "normal"),
|
|
@@ -41,27 +63,69 @@ def _font_face_css() -> str:
|
|
| 41 |
return "\n".join(out)
|
| 42 |
|
| 43 |
|
| 44 |
-
def
|
| 45 |
-
"""Inline the
|
| 46 |
parts = []
|
| 47 |
-
for name in
|
| 48 |
-
p =
|
| 49 |
if p.exists():
|
| 50 |
-
parts.append(p.read_text())
|
| 51 |
return "\n".join(parts)
|
| 52 |
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
SHELL_CSS = """
|
| 55 |
-
/* =================
|
| 56 |
.gradio-container{
|
| 57 |
-
max-width:100% !important; padding:0 !important;
|
| 58 |
background:var(--bg-canvas) !important;
|
| 59 |
font-family:var(--font-body); font-weight:var(--weight-body);
|
| 60 |
-
color:var(--text-primary); font-size:var(--text-
|
|
|
|
| 61 |
}
|
| 62 |
-
.gradio-container *{ border-radius:var(--radius-sm) !important; }
|
| 63 |
-
footer{ display:none !important; }
|
| 64 |
-
.gap.svelte-vt1mxs, .gradio-container .gap{ gap:var(--space-2) !important; }
|
| 65 |
|
| 66 |
/* ================= typography ================= */
|
| 67 |
.bit-h1,.bit-h2,.bit-h3{
|
|
@@ -71,26 +135,25 @@ footer{ display:none !important; }
|
|
| 71 |
}
|
| 72 |
.bit-h1{ font-size:var(--text-md); }
|
| 73 |
.bit-h2{ font-size:var(--text-md); }
|
| 74 |
-
.bit-h3{ font-size:var(--text-
|
| 75 |
.bit-micro{
|
| 76 |
-
font-family:var(--font-tiny); font-size:var(--text-
|
| 77 |
letter-spacing:var(--tracking-wider); text-transform:uppercase;
|
| 78 |
-
color:var(--text-tertiary); line-height:1.
|
| 79 |
}
|
| 80 |
.bit-spacer{ flex:1; }
|
| 81 |
|
| 82 |
/* ================= top bar ================= */
|
| 83 |
.bit-topbar{
|
| 84 |
-
display:flex; align-items:center; gap:
|
| 85 |
background:var(--bg-panel);
|
| 86 |
-
border-bottom:
|
| 87 |
-
padding:
|
| 88 |
-
position:sticky; top:0; z-index:var(--z-header);
|
| 89 |
}
|
| 90 |
.bit-mark{
|
| 91 |
-
width:
|
| 92 |
-
display:inline-block; flex:0 0
|
| 93 |
-
box-shadow:inset 0 0 0 3px var(--bg-panel), inset 0 0 0
|
| 94 |
}
|
| 95 |
.bit-wordmark{
|
| 96 |
font-family:var(--font-heading); font-size:var(--text-md); font-weight:500;
|
|
@@ -99,9 +162,9 @@ footer{ display:none !important; }
|
|
| 99 |
.bit-slash{ color:var(--text-tertiary); }
|
| 100 |
.bit-chip{
|
| 101 |
display:inline-flex; align-items:center; gap:6px; white-space:nowrap;
|
| 102 |
-
font-family:var(--font-mono); font-size:var(--text-
|
| 103 |
-
border:
|
| 104 |
-
padding:3px
|
| 105 |
}
|
| 106 |
.bit-chip-ok{ color:var(--accent-moss-strong); border-color:var(--accent-moss-dim); }
|
| 107 |
.bit-chip-run{ color:var(--accent-amber-strong); border-color:var(--accent-amber-dim); }
|
|
@@ -109,199 +172,201 @@ footer{ display:none !important; }
|
|
| 109 |
.bit-chip-accent{ color:var(--stone-950); background:var(--accent-amber);
|
| 110 |
border-color:var(--accent-amber); }
|
| 111 |
.bit-chip-ctx{ color:var(--text-tertiary); }
|
| 112 |
-
a.bit-link{ text-decoration:none; }
|
| 113 |
-
a.bit-link:hover{
|
| 114 |
-
|
|
|
|
|
|
|
| 115 |
|
| 116 |
/* ================= panels ================= */
|
| 117 |
.bit-panel{
|
| 118 |
-
background:var(--bg-panel);
|
| 119 |
-
|
| 120 |
-
padding:var(--space-4); margin-bottom:var(--space-3);
|
| 121 |
}
|
| 122 |
.bit-panel-head{
|
| 123 |
-
display:flex; align-items:baseline; gap:
|
| 124 |
-
border-bottom:
|
| 125 |
-
padding-bottom:
|
| 126 |
}
|
| 127 |
.bit-zone-title{
|
| 128 |
font-family:var(--font-heading); text-transform:uppercase;
|
| 129 |
letter-spacing:var(--tracking-wide); font-size:var(--text-md);
|
| 130 |
-
color:var(--text-primary); padding:
|
| 131 |
-
display:flex; align-items:baseline; gap:
|
| 132 |
}
|
| 133 |
.bit-zone-title .bit-micro{ margin-left:auto; }
|
| 134 |
|
| 135 |
/* ================= stat band ================= */
|
| 136 |
.bit-statband{
|
| 137 |
-
display:flex; flex-wrap:wrap;
|
| 138 |
-
|
| 139 |
-
background:var(--bg-panel); margin-bottom:var(--space-3);
|
| 140 |
}
|
| 141 |
.bit-stat{
|
| 142 |
-
flex:1 1
|
| 143 |
-
border-right:
|
| 144 |
-
border-bottom:
|
| 145 |
}
|
| 146 |
.bit-stat-label{
|
| 147 |
-
font-family:var(--font-tiny); font-size:var(--text-
|
| 148 |
letter-spacing:var(--tracking-wider); text-transform:uppercase;
|
| 149 |
color:var(--text-tertiary);
|
| 150 |
}
|
| 151 |
.bit-stat-value{
|
| 152 |
-
font-family:var(--font-mono); font-size:var(--text-
|
| 153 |
line-height:var(--leading-tight); color:var(--text-primary);
|
| 154 |
-
margin:
|
|
|
|
| 155 |
}
|
| 156 |
.bit-stat-sub{
|
| 157 |
-
font-family:var(--font-mono); font-size:var(--text-
|
| 158 |
color:var(--text-tertiary);
|
| 159 |
}
|
| 160 |
.bit-up{ color:var(--fin-up-strong); } .bit-down{ color:var(--fin-down-strong); }
|
| 161 |
|
| 162 |
/* ================= notes ================= */
|
| 163 |
.bit-note{
|
| 164 |
-
font-family:var(--font-mono); font-size:var(--text-
|
| 165 |
-
border-left:
|
| 166 |
-
background:var(--bg-raised); padding:
|
| 167 |
-
color:var(--text-secondary); margin-bottom:
|
| 168 |
}
|
| 169 |
.bit-note-danger{ border-left-color:var(--fin-down); }
|
| 170 |
.bit-tag{
|
| 171 |
font-family:var(--font-tiny); font-size:var(--text-2xs);
|
| 172 |
letter-spacing:var(--tracking-wider); border:1px solid var(--border-default);
|
| 173 |
-
padding:1px
|
| 174 |
}
|
| 175 |
|
| 176 |
/* ================= empty state ================= */
|
| 177 |
.bit-empty{
|
| 178 |
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
| 179 |
-
gap:
|
| 180 |
-
border:
|
| 181 |
}
|
| 182 |
-
.bit-empty-glyph{ font-size:
|
| 183 |
-
.bit-empty-copy{ color:var(--text-secondary); max-width:46ch; font-size:var(--text-
|
| 184 |
-
.bit-kbd-row{ display:flex; gap:
|
| 185 |
.bit-kbd{
|
| 186 |
-
font-family:var(--font-tiny); font-size:var(--text-
|
| 187 |
letter-spacing:var(--tracking-wider); color:var(--text-tertiary);
|
| 188 |
-
border:
|
| 189 |
}
|
| 190 |
|
| 191 |
/* ================= run manager ================= */
|
| 192 |
.bit-run-card{
|
| 193 |
-
border:
|
| 194 |
-
padding:
|
| 195 |
}
|
| 196 |
.bit-run-card-sel{ border-color:var(--accent-amber-dim); }
|
| 197 |
-
.bit-run-top{ display:flex; align-items:center; gap:
|
| 198 |
-
.bit-run-name{ font-size:var(--text-
|
| 199 |
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
| 200 |
-
.bit-run-mark{ margin-left:auto; color:var(--accent-amber-strong); font-size:
|
| 201 |
-
.bit-run-sharpe{ font-family:var(--font-mono); font-size:var(--text-
|
| 202 |
-
display:flex; gap:
|
| 203 |
.bit-run-ret{ margin-left:auto; color:var(--text-tertiary); }
|
| 204 |
-
.bit-gloss{ margin-bottom:
|
| 205 |
-
.bit-
|
|
|
|
| 206 |
|
| 207 |
/* ================= KPI row ================= */
|
| 208 |
-
.bit-kpi-row{ display:flex; flex-wrap:wrap; gap:
|
| 209 |
-
.bit-kpi{ min-width:
|
| 210 |
-
.bit-kpi-value{ font-family:var(--font-mono); font-size:var(--text-
|
| 211 |
line-height:1.1; color:var(--text-primary); }
|
| 212 |
|
| 213 |
/* ================= podium ================= */
|
| 214 |
-
.bit-podium-row{ display:flex; gap:
|
| 215 |
-
margin-bottom:var(--space-3); }
|
| 216 |
.bit-podium{
|
| 217 |
-
flex:1 1
|
| 218 |
-
background:var(--bg-panel); padding:
|
| 219 |
-
border-top:
|
| 220 |
}
|
| 221 |
.bit-podium-1{ border-top-color:var(--accent-amber); }
|
| 222 |
.bit-podium-2{ border-top-color:var(--accent-moss-strong); }
|
| 223 |
.bit-podium-3{ border-top-color:var(--mute-teal); }
|
| 224 |
-
.bit-podium-rank{ font-family:var(--font-tiny); font-size:var(--text-
|
| 225 |
color:var(--text-tertiary); letter-spacing:var(--tracking-wider); }
|
| 226 |
.bit-podium-name{ font-size:var(--text-md); color:var(--text-primary);
|
| 227 |
-
font-family:var(--font-heading); letter-spacing:var(--tracking-tight);
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
align-items:baseline; gap:
|
| 231 |
-
|
|
|
|
| 232 |
letter-spacing:var(--tracking-wider); color:var(--text-tertiary); }
|
| 233 |
|
| 234 |
/* ================= signal aggregator ================= */
|
| 235 |
-
.bit-sig-grid{ display:flex; gap:
|
| 236 |
-
.bit-sig-list{ flex:1 1
|
| 237 |
.bit-sig-row{
|
| 238 |
-
display:flex; align-items:center; gap:
|
| 239 |
-
border:
|
| 240 |
-
padding:5px
|
| 241 |
-
font-family:var(--font-mono); font-size:var(--text-
|
| 242 |
}
|
| 243 |
.bit-sig-name{ flex:1 1 auto; color:var(--text-secondary);
|
| 244 |
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
| 245 |
-
.bit-sig-dir{ width:
|
| 246 |
-
.bit-sig-edge{ width:
|
| 247 |
-
.bit-sig-acc,.bit-sig-w{ width:
|
| 248 |
.bit-consensus{
|
| 249 |
-
flex:0 0
|
| 250 |
-
background:var(--bg-raised); padding:
|
| 251 |
}
|
| 252 |
-
.bit-consensus-dir{ font-family:var(--font-heading); font-size:var(--text-
|
| 253 |
-
line-height:1.1; margin:
|
| 254 |
-
.bit-conf-track{ height:4px; background:var(--bg-sunken); margin:
|
| 255 |
.bit-conf-fill{ height:100%; }
|
| 256 |
|
| 257 |
-
/* ================= gradio
|
| 258 |
.gradio-container .tabs > .tab-nav{
|
| 259 |
-
border-bottom:
|
| 260 |
background:transparent !important; gap:0 !important; flex-wrap:wrap;
|
|
|
|
| 261 |
}
|
| 262 |
.gradio-container .tabs > .tab-nav > button{
|
| 263 |
font-family:var(--font-heading) !important; text-transform:uppercase;
|
| 264 |
-
letter-spacing:var(--tracking-wide); font-size:var(--text-
|
| 265 |
color:var(--text-tertiary) !important; background:transparent !important;
|
| 266 |
border:none !important; border-bottom:2px solid transparent !important;
|
| 267 |
-
padding:
|
| 268 |
}
|
| 269 |
.gradio-container .tabs > .tab-nav > button:hover{ color:var(--text-secondary) !important; }
|
| 270 |
.gradio-container .tabs > .tab-nav > button.selected{
|
| 271 |
color:var(--text-primary) !important;
|
| 272 |
border-bottom-color:var(--accent-amber) !important;
|
| 273 |
}
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
}
|
| 277 |
-
/* Field captions only -- scoped so it never swallows option text. */
|
| 278 |
.block > label > span, .block > .form > label > span{
|
| 279 |
-
font-family:var(--font-tiny) !important; font-size:var(--text-
|
| 280 |
letter-spacing:var(--tracking-wider) !important; text-transform:uppercase;
|
| 281 |
color:var(--text-tertiary) !important;
|
| 282 |
}
|
| 283 |
input, select, textarea{
|
| 284 |
-
font-family:var(--font-mono) !important; font-size:var(--text-
|
| 285 |
background:var(--bg-sunken) !important; color:var(--text-primary) !important;
|
| 286 |
-
border:
|
|
|
|
| 287 |
}
|
| 288 |
input:focus, select:focus, textarea:focus{
|
| 289 |
-
outline:
|
| 290 |
}
|
| 291 |
|
| 292 |
-
/* Radio / checkbox options
|
| 293 |
.gradio-container fieldset label,
|
| 294 |
.gradio-container .wrap label:has(input[type="radio"]),
|
| 295 |
.gradio-container .wrap label:has(input[type="checkbox"]){
|
| 296 |
background:transparent !important;
|
| 297 |
-
border:
|
| 298 |
color:var(--text-secondary) !important;
|
| 299 |
-
padding:
|
| 300 |
}
|
| 301 |
.gradio-container fieldset label span,
|
| 302 |
.gradio-container .wrap label:has(input[type="radio"]) span,
|
| 303 |
.gradio-container .wrap label:has(input[type="checkbox"]) span{
|
| 304 |
-
font-family:var(--font-mono) !important; font-size:var(--text-
|
| 305 |
letter-spacing:var(--tracking-normal) !important; text-transform:none !important;
|
| 306 |
color:var(--text-secondary) !important; opacity:1 !important;
|
| 307 |
}
|
|
@@ -319,52 +384,66 @@ input:focus, select:focus, textarea:focus{
|
|
| 319 |
accent-color:var(--accent-amber);
|
| 320 |
}
|
| 321 |
|
| 322 |
-
/* Accordions
|
| 323 |
-
.gradio-container .label-wrap
|
| 324 |
font-family:var(--font-heading) !important; text-transform:uppercase;
|
| 325 |
-
letter-spacing:var(--tracking-wide); font-size:var(--text-
|
| 326 |
color:var(--text-primary) !important;
|
| 327 |
-
border-bottom:
|
| 328 |
-
padding:
|
| 329 |
}
|
| 330 |
.bit-accordion{
|
| 331 |
border:none !important;
|
| 332 |
-
border-top:
|
| 333 |
-
padding:0 !important; margin
|
| 334 |
}
|
| 335 |
|
| 336 |
-
/* Buttons */
|
| 337 |
.bit-run-btn{
|
| 338 |
background:var(--accent-amber) !important; color:var(--stone-950) !important;
|
| 339 |
font-family:var(--font-heading) !important; text-transform:uppercase;
|
| 340 |
-
letter-spacing:var(--tracking-wide); border:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
}
|
| 342 |
-
.bit-run-btn:hover{ background:var(--accent-amber-strong) !important; }
|
| 343 |
.bit-ghost-btn{
|
| 344 |
background:transparent !important; color:var(--text-secondary) !important;
|
| 345 |
-
border:
|
| 346 |
-
font-family:var(--font-mono) !important; font-size:var(--text-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
}
|
| 348 |
-
.bit-ghost-btn:hover{ border-color:var(--border-strong) !important;
|
| 349 |
-
color:var(--text-primary) !important; }
|
| 350 |
|
| 351 |
/* Tables */
|
| 352 |
-
.bit-table table{
|
|
|
|
|
|
|
|
|
|
| 353 |
.bit-table thead th{
|
| 354 |
-
font-family:var(--font-tiny) !important; font-size:var(--text-
|
| 355 |
letter-spacing:var(--tracking-wider); text-transform:uppercase;
|
| 356 |
color:var(--text-tertiary) !important; background:var(--bg-raised) !important;
|
| 357 |
-
border-bottom:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
}
|
| 359 |
-
.bit-table tbody td{ border-color:var(--border-subtle) !important; }
|
| 360 |
|
| 361 |
/* Footer */
|
| 362 |
.bit-footer{
|
| 363 |
position:sticky; bottom:0; z-index:var(--z-header);
|
| 364 |
-
display:flex; justify-content:space-between; gap:
|
| 365 |
-
background:var(--bg-panel); border-top:
|
| 366 |
-
padding:
|
| 367 |
-
font-family:var(--font-mono); font-size:var(--text-
|
| 368 |
}
|
| 369 |
.bit-footer-right{ white-space:nowrap; }
|
| 370 |
|
|
@@ -378,28 +457,37 @@ input:focus, select:focus, textarea:focus{
|
|
| 378 |
|
| 379 |
|
| 380 |
def full_css() -> str:
|
| 381 |
-
|
|
|
|
|
|
|
| 382 |
|
| 383 |
|
| 384 |
def bit_theme() -> gr.Theme:
|
| 385 |
-
"""Gradio theme
|
| 386 |
|
| 387 |
-
|
| 388 |
-
|
|
|
|
|
|
|
| 389 |
"""
|
| 390 |
return gr.themes.Base(
|
| 391 |
primary_hue=gr.themes.colors.yellow,
|
| 392 |
secondary_hue=gr.themes.colors.lime,
|
| 393 |
neutral_hue=gr.themes.colors.stone,
|
| 394 |
-
font=[
|
| 395 |
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
|
|
|
|
|
|
|
|
|
|
| 396 |
).set(
|
|
|
|
| 397 |
body_background_fill="#161512",
|
| 398 |
body_text_color="#f7f4ec",
|
| 399 |
background_fill_primary="#1d1c18",
|
| 400 |
background_fill_secondary="#24221d",
|
| 401 |
-
block_background_fill="
|
| 402 |
block_border_color="#3d3a32",
|
|
|
|
| 403 |
block_label_text_color="#6f6a56",
|
| 404 |
border_color_primary="#3d3a32",
|
| 405 |
button_primary_background_fill="#af9209",
|
|
@@ -407,12 +495,37 @@ def bit_theme() -> gr.Theme:
|
|
| 407 |
button_secondary_background_fill="transparent",
|
| 408 |
button_secondary_text_color="#b6b09a",
|
| 409 |
input_background_fill="#000000",
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
)
|
| 414 |
|
| 415 |
|
| 416 |
def static_paths() -> list[str]:
|
| 417 |
-
"""Directories Gradio may serve (fonts, mark)."""
|
| 418 |
return [str(ASSETS)]
|
|
|
|
| 1 |
+
"""Gradio theme and CSS built from the Bit design system.
|
| 2 |
+
|
| 3 |
+
The design system ships six global stylesheets, listed in its own manifest as
|
| 4 |
+
`globalCssPaths`, and they are loaded here **in that order**:
|
| 5 |
+
|
| 6 |
+
tokens/fonts.css tokens/colors.css tokens/typography.css
|
| 7 |
+
tokens/spacing.css base.css styles.css
|
| 8 |
+
|
| 9 |
+
`base.css` is the important one and was previously vendored but never loaded.
|
| 10 |
+
It carries the design's global reset: square corners everywhere
|
| 11 |
+
(`border-radius:0 !important`), zeroed body margins, heading defaults, link and
|
| 12 |
+
focus treatment, the 8px pixel-font rule, and the scrollbar styling. Without it
|
| 13 |
+
the app inherited Gradio's rounded, roomy defaults and only *approximated* the
|
| 14 |
+
design.
|
| 15 |
+
|
| 16 |
+
Three things fight the design if left alone, and all three are handled here:
|
| 17 |
+
|
| 18 |
+
1. **Gradio's spacing scale.** Its defaults are `layout_gap: *spacing_xxl` and
|
| 19 |
+
`block_padding: *spacing_xl` — far looser than the design, which uses 12px
|
| 20 |
+
panel padding and 6–10px gaps. These are configured on the theme rather than
|
| 21 |
+
fought selector-by-selector.
|
| 22 |
+
2. **Gradio's type scale.** Its `body_text_size` is `*text_md` (16px). The
|
| 23 |
+
design's workhorse size is `--text-xs` (10px) with `--text-2xs` (8px) for
|
| 24 |
+
micro labels; `--text-base` (12px) appears exactly once in the whole design.
|
| 25 |
+
3. **Rounded corners.** Gradio rounds everything; the design is square.
|
| 26 |
"""
|
| 27 |
|
| 28 |
from __future__ import annotations
|
|
|
|
| 33 |
|
| 34 |
ASSETS = Path(__file__).resolve().parent.parent.parent / "assets"
|
| 35 |
FONT_DIR = ASSETS / "fonts"
|
| 36 |
+
TOKENS = ASSETS / "tokens"
|
| 37 |
+
|
| 38 |
+
# The design system's own manifest order. styles.css is only @import glue for
|
| 39 |
+
# the others, so it is resolved here rather than fetched.
|
| 40 |
+
GLOBAL_CSS = ("colors.css", "typography.css", "spacing.css", "base.css")
|
| 41 |
|
| 42 |
|
| 43 |
def _font_face_css() -> str:
|
| 44 |
"""@font-face rules pointing at Gradio's static file route."""
|
| 45 |
faces = [
|
| 46 |
+
("Styrene A", "StyreneA-Thin.otf", 100, "normal"),
|
| 47 |
("Styrene A", "StyreneA-Light.otf", 300, "normal"),
|
| 48 |
("Styrene A", "StyreneA-Regular.otf", 400, "normal"),
|
| 49 |
("Styrene A", "StyreneA-Medium.otf", 500, "normal"),
|
|
|
|
| 63 |
return "\n".join(out)
|
| 64 |
|
| 65 |
|
| 66 |
+
def _global_css() -> str:
|
| 67 |
+
"""Inline the design system's global stylesheets, verbatim and in order."""
|
| 68 |
parts = []
|
| 69 |
+
for name in GLOBAL_CSS:
|
| 70 |
+
p = TOKENS / name
|
| 71 |
if p.exists():
|
| 72 |
+
parts.append(f"/* ---- design system: {name} ---- */\n{p.read_text()}")
|
| 73 |
return "\n".join(parts)
|
| 74 |
|
| 75 |
|
| 76 |
+
# base.css sets `user-select:none` on everything and re-enables it only for
|
| 77 |
+
# inputs, code and `.bit-selectable`. That suits a trading dashboard but not a
|
| 78 |
+
# data tool: people need to select numbers out of the tables. This is the one
|
| 79 |
+
# deliberate departure from base.css, scoped to data surfaces only.
|
| 80 |
+
SELECTION_FIX = """
|
| 81 |
+
table, table *, .bit-table, .bit-table *, .prose, .prose *,
|
| 82 |
+
.bit-stat-value, .bit-stat-sub, .bit-sig-row, .bit-sig-row *,
|
| 83 |
+
.bit-podium, .bit-podium *, .bit-note, .bit-note *,
|
| 84 |
+
.markdown, .markdown * { user-select: text; }
|
| 85 |
+
"""
|
| 86 |
+
|
| 87 |
+
# --------------------------------------------------------------------------
|
| 88 |
+
# Gradio neutralisation: strip its layout spacing back to the design's scale.
|
| 89 |
+
# --------------------------------------------------------------------------
|
| 90 |
+
|
| 91 |
+
GRADIO_RESET = """
|
| 92 |
+
/* Gradio wraps every component in padded, gapped containers. Collapse them so
|
| 93 |
+
the design's own spacing is the only spacing on the page. */
|
| 94 |
+
.gradio-container{ max-width:100% !important; padding:0 !important; }
|
| 95 |
+
.gradio-container .block,
|
| 96 |
+
.gradio-container .form,
|
| 97 |
+
.gradio-container .styler,
|
| 98 |
+
.gradio-container .container,
|
| 99 |
+
.gradio-container .panel{
|
| 100 |
+
background:transparent !important; border:none !important;
|
| 101 |
+
box-shadow:none !important; padding:0 !important;
|
| 102 |
+
}
|
| 103 |
+
.gradio-container .gap{ gap:8px !important; }
|
| 104 |
+
.gradio-container .form > *{ margin:0 !important; }
|
| 105 |
+
.gradio-container .block > .wrap{ padding:0 !important; }
|
| 106 |
+
.gradio-container .tabitem{ padding:10px 0 0 !important; border:none !important; }
|
| 107 |
+
.gradio-container .column, .gradio-container .row{ gap:8px !important; }
|
| 108 |
+
.gradio-container .hide-container{ margin:0 !important; padding:0 !important; }
|
| 109 |
+
.gradio-container span.svelte-1gfkn6j, .gradio-container .block-title{ margin:0 !important; }
|
| 110 |
+
.gradio-container .wrap.svelte-1sk0pyu{ padding:0 !important; }
|
| 111 |
+
.gradio-container .icon-button-wrapper{ display:none !important; }
|
| 112 |
+
.gradio-container .block > label{ margin-bottom:2px !important; }
|
| 113 |
+
.gradio-container .prose :is(h1,h2,h3,h4){ margin:6px 0 4px !important; }
|
| 114 |
+
.gradio-container .prose p, .gradio-container .prose li{
|
| 115 |
+
margin:2px 0 !important; font-size:var(--text-xs) !important;
|
| 116 |
+
}
|
| 117 |
+
.gradio-container .prose ul{ margin:2px 0 !important; padding-left:16px !important; }
|
| 118 |
+
footer, .gradio-container footer{ display:none !important; }
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
SHELL_CSS = """
|
| 122 |
+
/* ================= shell ================= */
|
| 123 |
.gradio-container{
|
|
|
|
| 124 |
background:var(--bg-canvas) !important;
|
| 125 |
font-family:var(--font-body); font-weight:var(--weight-body);
|
| 126 |
+
color:var(--text-primary); font-size:var(--text-xs);
|
| 127 |
+
line-height:var(--leading-normal);
|
| 128 |
}
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
/* ================= typography ================= */
|
| 131 |
.bit-h1,.bit-h2,.bit-h3{
|
|
|
|
| 135 |
}
|
| 136 |
.bit-h1{ font-size:var(--text-md); }
|
| 137 |
.bit-h2{ font-size:var(--text-md); }
|
| 138 |
+
.bit-h3{ font-size:var(--text-sm); }
|
| 139 |
.bit-micro{
|
| 140 |
+
font-family:var(--font-tiny); font-size:var(--text-2xs);
|
| 141 |
letter-spacing:var(--tracking-wider); text-transform:uppercase;
|
| 142 |
+
color:var(--text-tertiary); line-height:1.7;
|
| 143 |
}
|
| 144 |
.bit-spacer{ flex:1; }
|
| 145 |
|
| 146 |
/* ================= top bar ================= */
|
| 147 |
.bit-topbar{
|
| 148 |
+
display:flex; align-items:center; gap:10px; flex-wrap:wrap;
|
| 149 |
background:var(--bg-panel);
|
| 150 |
+
border-bottom:1px solid var(--border-default);
|
| 151 |
+
padding:8px 12px; position:sticky; top:0; z-index:var(--z-header);
|
|
|
|
| 152 |
}
|
| 153 |
.bit-mark{
|
| 154 |
+
width:18px; height:18px; background:var(--accent-amber);
|
| 155 |
+
display:inline-block; flex:0 0 18px;
|
| 156 |
+
box-shadow:inset 0 0 0 3px var(--bg-panel), inset 0 0 0 5px var(--accent-amber);
|
| 157 |
}
|
| 158 |
.bit-wordmark{
|
| 159 |
font-family:var(--font-heading); font-size:var(--text-md); font-weight:500;
|
|
|
|
| 162 |
.bit-slash{ color:var(--text-tertiary); }
|
| 163 |
.bit-chip{
|
| 164 |
display:inline-flex; align-items:center; gap:6px; white-space:nowrap;
|
| 165 |
+
font-family:var(--font-mono); font-size:var(--text-2xs);
|
| 166 |
+
border:1px solid var(--border-default);
|
| 167 |
+
padding:3px 8px; color:var(--text-secondary); background:transparent;
|
| 168 |
}
|
| 169 |
.bit-chip-ok{ color:var(--accent-moss-strong); border-color:var(--accent-moss-dim); }
|
| 170 |
.bit-chip-run{ color:var(--accent-amber-strong); border-color:var(--accent-amber-dim); }
|
|
|
|
| 172 |
.bit-chip-accent{ color:var(--stone-950); background:var(--accent-amber);
|
| 173 |
border-color:var(--accent-amber); }
|
| 174 |
.bit-chip-ctx{ color:var(--text-tertiary); }
|
| 175 |
+
a.bit-link{ text-decoration:none; color:var(--text-secondary); }
|
| 176 |
+
a.bit-link:hover{
|
| 177 |
+
background:var(--accent-amber); color:var(--stone-950);
|
| 178 |
+
border-color:var(--accent-amber); text-decoration:none;
|
| 179 |
+
}
|
| 180 |
|
| 181 |
/* ================= panels ================= */
|
| 182 |
.bit-panel{
|
| 183 |
+
background:var(--bg-panel); border:1px solid var(--border-default);
|
| 184 |
+
padding:12px; margin-bottom:8px;
|
|
|
|
| 185 |
}
|
| 186 |
.bit-panel-head{
|
| 187 |
+
display:flex; align-items:baseline; gap:10px;
|
| 188 |
+
border-bottom:1px solid var(--border-subtle);
|
| 189 |
+
padding-bottom:6px; margin-bottom:8px;
|
| 190 |
}
|
| 191 |
.bit-zone-title{
|
| 192 |
font-family:var(--font-heading); text-transform:uppercase;
|
| 193 |
letter-spacing:var(--tracking-wide); font-size:var(--text-md);
|
| 194 |
+
color:var(--text-primary); padding:10px 2px 6px;
|
| 195 |
+
display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;
|
| 196 |
}
|
| 197 |
.bit-zone-title .bit-micro{ margin-left:auto; }
|
| 198 |
|
| 199 |
/* ================= stat band ================= */
|
| 200 |
.bit-statband{
|
| 201 |
+
display:flex; flex-wrap:wrap; border:1px solid var(--border-default);
|
| 202 |
+
background:var(--bg-panel); margin-bottom:8px;
|
|
|
|
| 203 |
}
|
| 204 |
.bit-stat{
|
| 205 |
+
flex:1 1 124px; padding:8px 12px;
|
| 206 |
+
border-right:1px solid var(--border-subtle);
|
| 207 |
+
border-bottom:1px solid var(--border-subtle);
|
| 208 |
}
|
| 209 |
.bit-stat-label{
|
| 210 |
+
font-family:var(--font-tiny); font-size:var(--text-2xs);
|
| 211 |
letter-spacing:var(--tracking-wider); text-transform:uppercase;
|
| 212 |
color:var(--text-tertiary);
|
| 213 |
}
|
| 214 |
.bit-stat-value{
|
| 215 |
+
font-family:var(--font-mono); font-size:var(--text-lg);
|
| 216 |
line-height:var(--leading-tight); color:var(--text-primary);
|
| 217 |
+
margin:2px 0 1px; letter-spacing:var(--tracking-tight);
|
| 218 |
+
font-variant-numeric:tabular-nums;
|
| 219 |
}
|
| 220 |
.bit-stat-sub{
|
| 221 |
+
font-family:var(--font-mono); font-size:var(--text-2xs);
|
| 222 |
color:var(--text-tertiary);
|
| 223 |
}
|
| 224 |
.bit-up{ color:var(--fin-up-strong); } .bit-down{ color:var(--fin-down-strong); }
|
| 225 |
|
| 226 |
/* ================= notes ================= */
|
| 227 |
.bit-note{
|
| 228 |
+
font-family:var(--font-mono); font-size:var(--text-2xs); line-height:1.6;
|
| 229 |
+
border-left:2px solid var(--accent-amber);
|
| 230 |
+
background:var(--bg-raised); padding:6px 10px;
|
| 231 |
+
color:var(--text-secondary); margin-bottom:6px;
|
| 232 |
}
|
| 233 |
.bit-note-danger{ border-left-color:var(--fin-down); }
|
| 234 |
.bit-tag{
|
| 235 |
font-family:var(--font-tiny); font-size:var(--text-2xs);
|
| 236 |
letter-spacing:var(--tracking-wider); border:1px solid var(--border-default);
|
| 237 |
+
padding:1px 5px; color:var(--text-tertiary); margin-left:4px;
|
| 238 |
}
|
| 239 |
|
| 240 |
/* ================= empty state ================= */
|
| 241 |
.bit-empty{
|
| 242 |
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
| 243 |
+
gap:10px; padding:60px 20px; text-align:center;
|
| 244 |
+
border:1px solid var(--border-default); background:var(--bg-panel);
|
| 245 |
}
|
| 246 |
+
.bit-empty-glyph{ font-size:30px; color:var(--accent-amber-dim); line-height:1; }
|
| 247 |
+
.bit-empty-copy{ color:var(--text-secondary); max-width:46ch; font-size:var(--text-xs); }
|
| 248 |
+
.bit-kbd-row{ display:flex; gap:6px; flex-wrap:wrap; justify-content:center; }
|
| 249 |
.bit-kbd{
|
| 250 |
+
font-family:var(--font-tiny); font-size:var(--text-2xs);
|
| 251 |
letter-spacing:var(--tracking-wider); color:var(--text-tertiary);
|
| 252 |
+
border:1px solid var(--border-default); padding:3px 7px;
|
| 253 |
}
|
| 254 |
|
| 255 |
/* ================= run manager ================= */
|
| 256 |
.bit-run-card{
|
| 257 |
+
border:1px solid var(--border-subtle);
|
| 258 |
+
padding:6px 8px; margin-bottom:4px; background:var(--bg-panel);
|
| 259 |
}
|
| 260 |
.bit-run-card-sel{ border-color:var(--accent-amber-dim); }
|
| 261 |
+
.bit-run-top{ display:flex; align-items:center; gap:6px; }
|
| 262 |
+
.bit-run-name{ font-size:var(--text-xs); color:var(--text-primary);
|
| 263 |
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
| 264 |
+
.bit-run-mark{ margin-left:auto; color:var(--accent-amber-strong); font-size:var(--text-2xs); }
|
| 265 |
+
.bit-run-sharpe{ font-family:var(--font-mono); font-size:var(--text-xs);
|
| 266 |
+
display:flex; gap:6px; }
|
| 267 |
.bit-run-ret{ margin-left:auto; color:var(--text-tertiary); }
|
| 268 |
+
.bit-gloss{ margin-bottom:6px; }
|
| 269 |
+
.bit-stage{ font-size:var(--text-xs); font-family:var(--font-mono); line-height:1.7; }
|
| 270 |
+
.bit-gloss-def{ font-size:var(--text-2xs); color:var(--text-secondary); line-height:1.6; }
|
| 271 |
|
| 272 |
/* ================= KPI row ================= */
|
| 273 |
+
.bit-kpi-row{ display:flex; flex-wrap:wrap; gap:12px; }
|
| 274 |
+
.bit-kpi{ min-width:72px; }
|
| 275 |
+
.bit-kpi-value{ font-family:var(--font-mono); font-size:var(--text-md);
|
| 276 |
line-height:1.1; color:var(--text-primary); }
|
| 277 |
|
| 278 |
/* ================= podium ================= */
|
| 279 |
+
.bit-podium-row{ display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px; }
|
|
|
|
| 280 |
.bit-podium{
|
| 281 |
+
flex:1 1 210px; border:1px solid var(--border-default);
|
| 282 |
+
background:var(--bg-panel); padding:10px 12px;
|
| 283 |
+
border-top:2px solid var(--border-strong);
|
| 284 |
}
|
| 285 |
.bit-podium-1{ border-top-color:var(--accent-amber); }
|
| 286 |
.bit-podium-2{ border-top-color:var(--accent-moss-strong); }
|
| 287 |
.bit-podium-3{ border-top-color:var(--mute-teal); }
|
| 288 |
+
.bit-podium-rank{ font-family:var(--font-tiny); font-size:var(--text-2xs);
|
| 289 |
color:var(--text-tertiary); letter-spacing:var(--tracking-wider); }
|
| 290 |
.bit-podium-name{ font-size:var(--text-md); color:var(--text-primary);
|
| 291 |
+
font-family:var(--font-heading); letter-spacing:var(--tracking-tight);
|
| 292 |
+
text-transform:none; }
|
| 293 |
+
.bit-podium-value{ font-family:var(--font-mono); font-size:var(--text-xl);
|
| 294 |
+
line-height:1.1; margin-top:6px; display:flex; align-items:baseline; gap:6px;
|
| 295 |
+
font-variant-numeric:tabular-nums; }
|
| 296 |
+
.bit-podium-unit{ font-family:var(--font-tiny); font-size:var(--text-2xs);
|
| 297 |
letter-spacing:var(--tracking-wider); color:var(--text-tertiary); }
|
| 298 |
|
| 299 |
/* ================= signal aggregator ================= */
|
| 300 |
+
.bit-sig-grid{ display:flex; gap:12px; flex-wrap:wrap; align-items:flex-start; }
|
| 301 |
+
.bit-sig-list{ flex:1 1 330px; min-width:0; }
|
| 302 |
.bit-sig-row{
|
| 303 |
+
display:flex; align-items:center; gap:10px;
|
| 304 |
+
border:1px solid var(--border-subtle);
|
| 305 |
+
padding:5px 10px; margin-bottom:3px;
|
| 306 |
+
font-family:var(--font-mono); font-size:var(--text-xs);
|
| 307 |
}
|
| 308 |
.bit-sig-name{ flex:1 1 auto; color:var(--text-secondary);
|
| 309 |
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
| 310 |
+
.bit-sig-dir{ width:70px; text-align:right; }
|
| 311 |
+
.bit-sig-edge{ width:60px; text-align:right; }
|
| 312 |
+
.bit-sig-acc,.bit-sig-w{ width:58px; text-align:right; color:var(--text-tertiary); }
|
| 313 |
.bit-consensus{
|
| 314 |
+
flex:0 0 200px; border:1px solid var(--accent-moss-dim);
|
| 315 |
+
background:var(--bg-raised); padding:10px 12px;
|
| 316 |
}
|
| 317 |
+
.bit-consensus-dir{ font-family:var(--font-heading); font-size:var(--text-xl);
|
| 318 |
+
line-height:1.1; margin:2px 0; letter-spacing:var(--tracking-tight); }
|
| 319 |
+
.bit-conf-track{ height:4px; background:var(--bg-sunken); margin:6px 0 4px; }
|
| 320 |
.bit-conf-fill{ height:100%; }
|
| 321 |
|
| 322 |
+
/* ================= gradio controls, restyled ================= */
|
| 323 |
.gradio-container .tabs > .tab-nav{
|
| 324 |
+
border-bottom:1px solid var(--border-default) !important;
|
| 325 |
background:transparent !important; gap:0 !important; flex-wrap:wrap;
|
| 326 |
+
margin-bottom:0 !important;
|
| 327 |
}
|
| 328 |
.gradio-container .tabs > .tab-nav > button{
|
| 329 |
font-family:var(--font-heading) !important; text-transform:uppercase;
|
| 330 |
+
letter-spacing:var(--tracking-wide); font-size:var(--text-xs) !important;
|
| 331 |
color:var(--text-tertiary) !important; background:transparent !important;
|
| 332 |
border:none !important; border-bottom:2px solid transparent !important;
|
| 333 |
+
padding:7px 12px !important; margin:0 !important;
|
| 334 |
}
|
| 335 |
.gradio-container .tabs > .tab-nav > button:hover{ color:var(--text-secondary) !important; }
|
| 336 |
.gradio-container .tabs > .tab-nav > button.selected{
|
| 337 |
color:var(--text-primary) !important;
|
| 338 |
border-bottom-color:var(--accent-amber) !important;
|
| 339 |
}
|
| 340 |
+
|
| 341 |
+
/* Field captions */
|
|
|
|
|
|
|
| 342 |
.block > label > span, .block > .form > label > span{
|
| 343 |
+
font-family:var(--font-tiny) !important; font-size:var(--text-2xs) !important;
|
| 344 |
letter-spacing:var(--tracking-wider) !important; text-transform:uppercase;
|
| 345 |
color:var(--text-tertiary) !important;
|
| 346 |
}
|
| 347 |
input, select, textarea{
|
| 348 |
+
font-family:var(--font-mono) !important; font-size:var(--text-xs) !important;
|
| 349 |
background:var(--bg-sunken) !important; color:var(--text-primary) !important;
|
| 350 |
+
border:1px solid var(--border-default) !important;
|
| 351 |
+
padding:5px 8px !important;
|
| 352 |
}
|
| 353 |
input:focus, select:focus, textarea:focus{
|
| 354 |
+
outline:2px solid var(--focus-ring) !important; outline-offset:-1px;
|
| 355 |
}
|
| 356 |
|
| 357 |
+
/* Radio / checkbox options as segmented chips */
|
| 358 |
.gradio-container fieldset label,
|
| 359 |
.gradio-container .wrap label:has(input[type="radio"]),
|
| 360 |
.gradio-container .wrap label:has(input[type="checkbox"]){
|
| 361 |
background:transparent !important;
|
| 362 |
+
border:1px solid var(--border-default) !important;
|
| 363 |
color:var(--text-secondary) !important;
|
| 364 |
+
padding:3px 8px !important; margin:2px !important;
|
| 365 |
}
|
| 366 |
.gradio-container fieldset label span,
|
| 367 |
.gradio-container .wrap label:has(input[type="radio"]) span,
|
| 368 |
.gradio-container .wrap label:has(input[type="checkbox"]) span{
|
| 369 |
+
font-family:var(--font-mono) !important; font-size:var(--text-2xs) !important;
|
| 370 |
letter-spacing:var(--tracking-normal) !important; text-transform:none !important;
|
| 371 |
color:var(--text-secondary) !important; opacity:1 !important;
|
| 372 |
}
|
|
|
|
| 384 |
accent-color:var(--accent-amber);
|
| 385 |
}
|
| 386 |
|
| 387 |
+
/* Accordions as the design's numbered sections */
|
| 388 |
+
.gradio-container .label-wrap{
|
| 389 |
font-family:var(--font-heading) !important; text-transform:uppercase;
|
| 390 |
+
letter-spacing:var(--tracking-wide); font-size:var(--text-xs) !important;
|
| 391 |
color:var(--text-primary) !important;
|
| 392 |
+
border-bottom:1px solid var(--border-subtle) !important;
|
| 393 |
+
padding:7px 0 !important; margin:0 !important;
|
| 394 |
}
|
| 395 |
.bit-accordion{
|
| 396 |
border:none !important;
|
| 397 |
+
border-top:1px solid var(--border-subtle) !important;
|
| 398 |
+
padding:0 !important; margin:0 0 2px !important;
|
| 399 |
}
|
| 400 |
|
| 401 |
+
/* Buttons -- base.css supplies the inverted hover treatment */
|
| 402 |
.bit-run-btn{
|
| 403 |
background:var(--accent-amber) !important; color:var(--stone-950) !important;
|
| 404 |
font-family:var(--font-heading) !important; text-transform:uppercase;
|
| 405 |
+
letter-spacing:var(--tracking-wide); border:1px solid var(--accent-amber) !important;
|
| 406 |
+
font-weight:500 !important; font-size:var(--text-xs) !important;
|
| 407 |
+
padding:7px 12px !important;
|
| 408 |
+
}
|
| 409 |
+
.bit-run-btn:hover{
|
| 410 |
+
background:var(--stone-950) !important; color:var(--accent-amber) !important;
|
| 411 |
}
|
|
|
|
| 412 |
.bit-ghost-btn{
|
| 413 |
background:transparent !important; color:var(--text-secondary) !important;
|
| 414 |
+
border:1px solid var(--border-default) !important;
|
| 415 |
+
font-family:var(--font-mono) !important; font-size:var(--text-2xs) !important;
|
| 416 |
+
padding:5px 10px !important;
|
| 417 |
+
}
|
| 418 |
+
.bit-ghost-btn:hover{
|
| 419 |
+
background:var(--accent-amber) !important; color:var(--stone-950) !important;
|
| 420 |
+
border-color:var(--accent-amber) !important;
|
| 421 |
}
|
|
|
|
|
|
|
| 422 |
|
| 423 |
/* Tables */
|
| 424 |
+
.bit-table table{
|
| 425 |
+
font-family:var(--font-mono) !important; font-size:var(--text-2xs) !important;
|
| 426 |
+
font-variant-numeric:tabular-nums;
|
| 427 |
+
}
|
| 428 |
.bit-table thead th{
|
| 429 |
+
font-family:var(--font-tiny) !important; font-size:var(--text-2xs) !important;
|
| 430 |
letter-spacing:var(--tracking-wider); text-transform:uppercase;
|
| 431 |
color:var(--text-tertiary) !important; background:var(--bg-raised) !important;
|
| 432 |
+
border-bottom:1px solid var(--border-default) !important;
|
| 433 |
+
padding:5px 8px !important;
|
| 434 |
+
}
|
| 435 |
+
.bit-table tbody td{
|
| 436 |
+
border-color:var(--border-subtle) !important; padding:4px 8px !important;
|
| 437 |
+
color:var(--text-secondary) !important;
|
| 438 |
}
|
|
|
|
| 439 |
|
| 440 |
/* Footer */
|
| 441 |
.bit-footer{
|
| 442 |
position:sticky; bottom:0; z-index:var(--z-header);
|
| 443 |
+
display:flex; justify-content:space-between; gap:12px;
|
| 444 |
+
background:var(--bg-panel); border-top:1px solid var(--border-default);
|
| 445 |
+
padding:6px 12px;
|
| 446 |
+
font-family:var(--font-mono); font-size:var(--text-2xs); color:var(--text-tertiary);
|
| 447 |
}
|
| 448 |
.bit-footer-right{ white-space:nowrap; }
|
| 449 |
|
|
|
|
| 457 |
|
| 458 |
|
| 459 |
def full_css() -> str:
|
| 460 |
+
"""Fonts, the design system's globals, the Gradio reset, then the shell."""
|
| 461 |
+
return "\n".join([_font_face_css(), _global_css(), SELECTION_FIX,
|
| 462 |
+
GRADIO_RESET, SHELL_CSS])
|
| 463 |
|
| 464 |
|
| 465 |
def bit_theme() -> gr.Theme:
|
| 466 |
+
"""Gradio theme configured to the design's scale.
|
| 467 |
|
| 468 |
+
Setting Gradio's own spacing/radius/text variables is what stops its
|
| 469 |
+
defaults from injecting padding and gaps the design never asked for. Doing
|
| 470 |
+
it here rather than with `!important` overrides means the generated CSS is
|
| 471 |
+
already right instead of being corrected afterwards.
|
| 472 |
"""
|
| 473 |
return gr.themes.Base(
|
| 474 |
primary_hue=gr.themes.colors.yellow,
|
| 475 |
secondary_hue=gr.themes.colors.lime,
|
| 476 |
neutral_hue=gr.themes.colors.stone,
|
| 477 |
+
font=["Styrene A", "system-ui", "sans-serif"],
|
| 478 |
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
|
| 479 |
+
spacing_size=gr.themes.sizes.spacing_sm,
|
| 480 |
+
radius_size=gr.themes.sizes.radius_none,
|
| 481 |
+
text_size=gr.themes.sizes.text_sm,
|
| 482 |
).set(
|
| 483 |
+
# Palette
|
| 484 |
body_background_fill="#161512",
|
| 485 |
body_text_color="#f7f4ec",
|
| 486 |
background_fill_primary="#1d1c18",
|
| 487 |
background_fill_secondary="#24221d",
|
| 488 |
+
block_background_fill="transparent",
|
| 489 |
block_border_color="#3d3a32",
|
| 490 |
+
block_border_width="0px",
|
| 491 |
block_label_text_color="#6f6a56",
|
| 492 |
border_color_primary="#3d3a32",
|
| 493 |
button_primary_background_fill="#af9209",
|
|
|
|
| 495 |
button_secondary_background_fill="transparent",
|
| 496 |
button_secondary_text_color="#b6b09a",
|
| 497 |
input_background_fill="#000000",
|
| 498 |
+
input_border_color="#3d3a32",
|
| 499 |
+
# Spacing -- the design uses 12px panel padding and 6-10px gaps.
|
| 500 |
+
layout_gap="8px",
|
| 501 |
+
block_padding="0px",
|
| 502 |
+
form_gap_width="0px",
|
| 503 |
+
block_label_margin="0px",
|
| 504 |
+
block_title_padding="0px",
|
| 505 |
+
input_padding="5px 8px",
|
| 506 |
+
checkbox_label_padding="3px 8px",
|
| 507 |
+
checkbox_label_gap="6px",
|
| 508 |
+
button_small_padding="5px 10px",
|
| 509 |
+
button_large_padding="7px 12px",
|
| 510 |
+
# Type -- 10px workhorse, matching the design's dominant size.
|
| 511 |
+
body_text_size="10px",
|
| 512 |
+
input_text_size="10px",
|
| 513 |
+
block_label_text_size="8px",
|
| 514 |
+
block_title_text_size="10px",
|
| 515 |
+
button_small_text_size="8px",
|
| 516 |
+
button_large_text_size="10px",
|
| 517 |
+
checkbox_label_text_size="8px",
|
| 518 |
+
prose_text_size="10px",
|
| 519 |
+
# Square, everywhere.
|
| 520 |
+
block_radius="0px",
|
| 521 |
+
input_radius="0px",
|
| 522 |
+
button_large_radius="0px",
|
| 523 |
+
button_small_radius="0px",
|
| 524 |
+
checkbox_border_radius="0px",
|
| 525 |
+
container_radius="0px",
|
| 526 |
)
|
| 527 |
|
| 528 |
|
| 529 |
def static_paths() -> list[str]:
|
| 530 |
+
"""Directories Gradio may serve (fonts, tokens, mark)."""
|
| 531 |
return [str(ASSETS)]
|
tests/test_ui.py
CHANGED
|
@@ -298,3 +298,121 @@ def test_default_three_year_daily_run_is_under_two_seconds():
|
|
| 298 |
t0 = time.perf_counter()
|
| 299 |
runtime.execute(req)
|
| 300 |
assert time.perf_counter() - t0 < 2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
t0 = time.perf_counter()
|
| 299 |
runtime.execute(req)
|
| 300 |
assert time.perf_counter() - t0 < 2.0
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
# --------------------------------------------------------------------------
|
| 304 |
+
# Design-system fidelity
|
| 305 |
+
#
|
| 306 |
+
# The original bug this guards: base.css was vendored into assets/ but never
|
| 307 |
+
# loaded, so the app silently inherited Gradio's rounded, roomy defaults and
|
| 308 |
+
# only approximated the design.
|
| 309 |
+
# --------------------------------------------------------------------------
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def test_every_global_stylesheet_the_design_system_declares_is_loaded():
|
| 313 |
+
"""The DS manifest lists six globalCssPaths; all of them must reach the page."""
|
| 314 |
+
from src.ui import theme
|
| 315 |
+
|
| 316 |
+
css = theme.full_css()
|
| 317 |
+
# colors / typography / spacing / base, each identified by a rule only it has
|
| 318 |
+
markers = {
|
| 319 |
+
"colors.css": "--accent-amber:#af9209",
|
| 320 |
+
"typography.css": "--text-2xs:8px",
|
| 321 |
+
"spacing.css": "--space-4:16px",
|
| 322 |
+
"base.css": "border-radius:0 !important",
|
| 323 |
+
}
|
| 324 |
+
for name, marker in markers.items():
|
| 325 |
+
assert marker in css, f"{name} is not loaded into the page"
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def test_base_css_square_corner_reset_is_present():
|
| 329 |
+
from src.ui import theme
|
| 330 |
+
|
| 331 |
+
assert "border-radius:0 !important" in theme.full_css()
|
| 332 |
+
# The app's own shell must not reintroduce a rounded default on top of the
|
| 333 |
+
# reset. base.css legitimately defines opt-in `.bit-rounded*` utilities, so
|
| 334 |
+
# only our shell layer is checked here.
|
| 335 |
+
assert "border-radius" not in theme.SHELL_CSS
|
| 336 |
+
assert "border-radius" not in theme.GRADIO_RESET
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def test_base_css_scrollbar_and_selection_rules_survive():
|
| 340 |
+
from src.ui import theme
|
| 341 |
+
|
| 342 |
+
css = theme.full_css()
|
| 343 |
+
assert "::-webkit-scrollbar" in css
|
| 344 |
+
assert "::selection" in css
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
def test_data_surfaces_stay_selectable_despite_base_css():
|
| 348 |
+
"""base.css sets user-select:none globally; tables must opt back in."""
|
| 349 |
+
from src.ui import theme
|
| 350 |
+
|
| 351 |
+
css = theme.full_css()
|
| 352 |
+
assert "user-select: text" in css
|
| 353 |
+
assert ".bit-table" in css.split("user-select: text")[0][-400:] or \
|
| 354 |
+
"table, table *" in css
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def test_gradio_layout_spacing_is_configured_not_left_default():
|
| 358 |
+
"""Gradio defaults to layout_gap *spacing_xxl and block_padding *spacing_xl."""
|
| 359 |
+
from src.ui import theme
|
| 360 |
+
|
| 361 |
+
t = theme.bit_theme()
|
| 362 |
+
assert t.layout_gap == "8px"
|
| 363 |
+
assert t.block_padding == "0px"
|
| 364 |
+
assert t.form_gap_width == "0px"
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def test_gradio_radius_is_squared_off():
|
| 368 |
+
from src.ui import theme
|
| 369 |
+
|
| 370 |
+
t = theme.bit_theme()
|
| 371 |
+
for attr in ("block_radius", "input_radius", "button_large_radius",
|
| 372 |
+
"button_small_radius", "container_radius"):
|
| 373 |
+
assert getattr(t, attr) == "0px", f"{attr} is not square"
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def test_type_scale_matches_the_designs_dominant_sizes():
|
| 377 |
+
"""The design's workhorse is --text-xs (10px) with --text-2xs (8px) micro."""
|
| 378 |
+
from src.ui import theme
|
| 379 |
+
|
| 380 |
+
t = theme.bit_theme()
|
| 381 |
+
assert t.body_text_size == "10px"
|
| 382 |
+
assert t.block_label_text_size == "8px"
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def test_no_hardcoded_pixel_font_sizes_outside_the_theme():
|
| 386 |
+
"""Component markup must size through tokens, never literal px."""
|
| 387 |
+
import re
|
| 388 |
+
from pathlib import Path
|
| 389 |
+
|
| 390 |
+
root = Path(__file__).resolve().parent.parent
|
| 391 |
+
for rel in ("src/ui/components.py", "src/ui/compare_tab.py", "app.py"):
|
| 392 |
+
text = (root / rel).read_text()
|
| 393 |
+
assert not re.search(r"font-size:\s*\d", text), \
|
| 394 |
+
f"{rel} hardcodes a pixel font-size instead of using a token"
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
def test_every_bit_class_used_in_markup_has_a_css_rule():
|
| 398 |
+
"""A class with no rule renders unstyled and silently breaks the design."""
|
| 399 |
+
import re
|
| 400 |
+
from pathlib import Path
|
| 401 |
+
|
| 402 |
+
from src.ui import theme
|
| 403 |
+
|
| 404 |
+
root = Path(__file__).resolve().parent.parent
|
| 405 |
+
used = set()
|
| 406 |
+
for rel in ("src/ui/components.py", "src/ui/compare_tab.py", "app.py"):
|
| 407 |
+
used |= set(re.findall(r"bit-[a-z0-9-]+", (root / rel).read_text()))
|
| 408 |
+
defined = set(re.findall(r"\.(bit-[a-z0-9-]+)", theme.full_css()))
|
| 409 |
+
missing = sorted(c for c in used if c not in defined and not c.endswith("-"))
|
| 410 |
+
assert not missing, f"classes used in markup but never styled: {missing}"
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
def test_fonts_are_served_for_every_weight_the_design_uses():
|
| 414 |
+
from src.ui import theme
|
| 415 |
+
|
| 416 |
+
css = theme.full_css()
|
| 417 |
+
assert css.count("@font-face") >= 5
|
| 418 |
+
assert "Styrene A" in css and "Mac Minecraft" in css
|