Bit-Trading-Company commited on
Commit
bc0c291
·
verified ·
1 Parent(s): fbfed0d

Backtest Lab v1.0.0

Browse files
Files changed (3) hide show
  1. src/ui/components.py +1 -1
  2. src/ui/theme.py +264 -151
  3. 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};font-size:11px">{mark} {esc(label)}</div>')
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 tokens.
2
-
3
- `assets/tokens/*.css` is vendored verbatim from the design system and imported
4
- here rather than retyped, so token values cannot drift. This module adds only
5
- the app-shell layout the design specifies (three zones, panels, chips, stat
6
- band) on top of those tokens.
7
-
8
- Fonts are served as static files. If they fail to load the page degrades to the
9
- system sans / mono stack rather than breaking.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 _tokens_css() -> str:
45
- """Inline the vendored token files so there is one source of truth."""
46
  parts = []
47
- for name in ("colors.css", "typography.css", "spacing.css"):
48
- p = ASSETS / "tokens" / name
49
  if p.exists():
50
- parts.append(p.read_text())
51
  return "\n".join(parts)
52
 
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  SHELL_CSS = """
55
- /* ================= app shell ================= */
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-base);
 
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-base); }
75
  .bit-micro{
76
- font-family:var(--font-tiny); font-size:var(--text-xs);
77
  letter-spacing:var(--tracking-wider); text-transform:uppercase;
78
- color:var(--text-tertiary); line-height:1.6;
79
  }
80
  .bit-spacer{ flex:1; }
81
 
82
  /* ================= top bar ================= */
83
  .bit-topbar{
84
- display:flex; align-items:center; gap:var(--space-3); flex-wrap:wrap;
85
  background:var(--bg-panel);
86
- border-bottom:var(--border-width) solid var(--border-default);
87
- padding:var(--space-3) var(--space-4);
88
- position:sticky; top:0; z-index:var(--z-header);
89
  }
90
  .bit-mark{
91
- width:20px; height:20px; background:var(--accent-amber);
92
- display:inline-block; flex:0 0 20px;
93
- box-shadow:inset 0 0 0 3px var(--bg-panel), inset 0 0 0 6px var(--accent-amber);
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-sm);
103
- border:var(--border-width) solid var(--border-default);
104
- padding:3px var(--space-2); color:var(--text-secondary); background:transparent;
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{ color:var(--accent-amber-strong);
114
- border-color:var(--accent-amber-dim); }
 
 
115
 
116
  /* ================= panels ================= */
117
  .bit-panel{
118
- background:var(--bg-panel);
119
- border:var(--border-width) solid var(--border-default);
120
- padding:var(--space-4); margin-bottom:var(--space-3);
121
  }
122
  .bit-panel-head{
123
- display:flex; align-items:baseline; gap:var(--space-3);
124
- border-bottom:var(--border-width) solid var(--border-subtle);
125
- padding-bottom:var(--space-2); margin-bottom:var(--space-3);
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:var(--space-3) var(--space-1) var(--space-2);
131
- display:flex; align-items:baseline; gap:var(--space-2);
132
  }
133
  .bit-zone-title .bit-micro{ margin-left:auto; }
134
 
135
  /* ================= stat band ================= */
136
  .bit-statband{
137
- display:flex; flex-wrap:wrap;
138
- border:var(--border-width) solid var(--border-default);
139
- background:var(--bg-panel); margin-bottom:var(--space-3);
140
  }
141
  .bit-stat{
142
- flex:1 1 132px; padding:var(--space-3) var(--space-4);
143
- border-right:var(--border-width) solid var(--border-subtle);
144
- border-bottom:var(--border-width) solid var(--border-subtle);
145
  }
146
  .bit-stat-label{
147
- font-family:var(--font-tiny); font-size:var(--text-xs);
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-xl);
153
  line-height:var(--leading-tight); color:var(--text-primary);
154
- margin:3px 0 2px; letter-spacing:var(--tracking-tight);
 
155
  }
156
  .bit-stat-sub{
157
- font-family:var(--font-mono); font-size:var(--text-xs);
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-sm); line-height:1.5;
165
- border-left:var(--border-width-strong) solid var(--accent-amber);
166
- background:var(--bg-raised); padding:var(--space-2) var(--space-3);
167
- color:var(--text-secondary); margin-bottom:var(--space-2);
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 4px; color:var(--text-tertiary); margin-left:4px;
174
  }
175
 
176
  /* ================= empty state ================= */
177
  .bit-empty{
178
  display:flex; flex-direction:column; align-items:center; justify-content:center;
179
- gap:var(--space-3); padding:var(--space-8) var(--space-4); text-align:center;
180
- border:var(--border-width) dashed var(--border-default); background:var(--bg-panel);
181
  }
182
- .bit-empty-glyph{ font-size:34px; color:var(--accent-amber-dim); line-height:1; }
183
- .bit-empty-copy{ color:var(--text-secondary); max-width:46ch; font-size:var(--text-base); }
184
- .bit-kbd-row{ display:flex; gap:var(--space-2); flex-wrap:wrap; justify-content:center; }
185
  .bit-kbd{
186
- font-family:var(--font-tiny); font-size:var(--text-xs);
187
  letter-spacing:var(--tracking-wider); color:var(--text-tertiary);
188
- border:var(--border-width) solid var(--border-default); padding:3px 7px;
189
  }
190
 
191
  /* ================= run manager ================= */
192
  .bit-run-card{
193
- border:var(--border-width) solid var(--border-subtle);
194
- padding:var(--space-2); margin-bottom:var(--space-1); background:var(--bg-panel);
195
  }
196
  .bit-run-card-sel{ border-color:var(--accent-amber-dim); }
197
- .bit-run-top{ display:flex; align-items:center; gap:var(--space-2); }
198
- .bit-run-name{ font-size:var(--text-sm); color:var(--text-primary);
199
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
200
- .bit-run-mark{ margin-left:auto; color:var(--accent-amber-strong); font-size:11px; }
201
- .bit-run-sharpe{ font-family:var(--font-mono); font-size:var(--text-base);
202
- display:flex; gap:var(--space-2); }
203
  .bit-run-ret{ margin-left:auto; color:var(--text-tertiary); }
204
- .bit-gloss{ margin-bottom:var(--space-2); }
205
- .bit-gloss-def{ font-size:var(--text-sm); color:var(--text-secondary); line-height:1.45; }
 
206
 
207
  /* ================= KPI row ================= */
208
- .bit-kpi-row{ display:flex; flex-wrap:wrap; gap:var(--space-4); }
209
- .bit-kpi{ min-width:78px; }
210
- .bit-kpi-value{ font-family:var(--font-mono); font-size:var(--text-lg);
211
  line-height:1.1; color:var(--text-primary); }
212
 
213
  /* ================= podium ================= */
214
- .bit-podium-row{ display:flex; gap:var(--space-3); flex-wrap:wrap;
215
- margin-bottom:var(--space-3); }
216
  .bit-podium{
217
- flex:1 1 200px; border:var(--border-width) solid var(--border-default);
218
- background:var(--bg-panel); padding:var(--space-3);
219
- border-top:var(--border-width-strong) solid var(--border-strong);
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-xs);
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
- .bit-podium-value{ font-family:var(--font-mono); font-size:var(--text-2xl);
229
- line-height:1.1; margin-top:var(--space-2); display:flex;
230
- align-items:baseline; gap:var(--space-2); }
231
- .bit-podium-unit{ font-family:var(--font-tiny); font-size:var(--text-xs);
 
232
  letter-spacing:var(--tracking-wider); color:var(--text-tertiary); }
233
 
234
  /* ================= signal aggregator ================= */
235
- .bit-sig-grid{ display:flex; gap:var(--space-4); flex-wrap:wrap; align-items:flex-start; }
236
- .bit-sig-list{ flex:1 1 340px; min-width:0; }
237
  .bit-sig-row{
238
- display:flex; align-items:center; gap:var(--space-3);
239
- border:var(--border-width) solid var(--border-subtle);
240
- padding:5px var(--space-2); margin-bottom:3px;
241
- font-family:var(--font-mono); font-size:var(--text-sm);
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:74px; text-align:right; }
246
- .bit-sig-edge{ width:64px; text-align:right; }
247
- .bit-sig-acc,.bit-sig-w{ width:64px; text-align:right; color:var(--text-tertiary); }
248
  .bit-consensus{
249
- flex:0 0 210px; border:var(--border-width) solid var(--accent-moss-dim);
250
- background:var(--bg-raised); padding:var(--space-3);
251
  }
252
- .bit-consensus-dir{ font-family:var(--font-heading); font-size:var(--text-2xl);
253
- line-height:1.1; margin:var(--space-1) 0; letter-spacing:var(--tracking-tight); }
254
- .bit-conf-track{ height:4px; background:var(--bg-sunken); margin:var(--space-2) 0 4px; }
255
  .bit-conf-fill{ height:100%; }
256
 
257
- /* ================= gradio overrides ================= */
258
  .gradio-container .tabs > .tab-nav{
259
- border-bottom:var(--border-width) solid var(--border-default) !important;
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-base) !important;
265
  color:var(--text-tertiary) !important; background:transparent !important;
266
  border:none !important; border-bottom:2px solid transparent !important;
267
- padding:var(--space-2) var(--space-4) !important;
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
- .gradio-container .form, .gradio-container .block{
275
- background:transparent !important; border:none !important;
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-xs) !important;
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-base) !important;
285
  background:var(--bg-sunken) !important; color:var(--text-primary) !important;
286
- border:var(--border-width) solid var(--border-default) !important;
 
287
  }
288
  input:focus, select:focus, textarea:focus{
289
- outline:1px solid var(--focus-ring) !important; outline-offset:-1px;
290
  }
291
 
292
- /* Radio / checkbox options read as segmented chips. */
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:var(--border-width) solid var(--border-default) !important;
298
  color:var(--text-secondary) !important;
299
- padding:4px 10px !important; margin:2px !important;
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-base) !important;
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 read as the design's numbered sections. */
323
- .gradio-container .label-wrap, .bit-accordion > .label-wrap{
324
  font-family:var(--font-heading) !important; text-transform:uppercase;
325
- letter-spacing:var(--tracking-wide); font-size:var(--text-base) !important;
326
  color:var(--text-primary) !important;
327
- border-bottom:var(--border-width) solid var(--border-subtle) !important;
328
- padding:var(--space-2) 0 !important;
329
  }
330
  .bit-accordion{
331
  border:none !important;
332
- border-top:var(--border-width) solid var(--border-subtle) !important;
333
- padding:0 !important; margin-bottom:var(--space-1) !important;
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:none !important; font-weight:500 !important;
 
 
 
 
 
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:var(--border-width) solid var(--border-default) !important;
346
- font-family:var(--font-mono) !important; font-size:var(--text-sm) !important;
 
 
 
 
 
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{ font-family:var(--font-mono) !important; font-size:var(--text-sm) !important; }
 
 
 
353
  .bit-table thead th{
354
- font-family:var(--font-tiny) !important; font-size:var(--text-xs) !important;
355
  letter-spacing:var(--tracking-wider); text-transform:uppercase;
356
  color:var(--text-tertiary) !important; background:var(--bg-raised) !important;
357
- border-bottom:var(--border-width) solid var(--border-default) !important;
 
 
 
 
 
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:var(--space-4);
365
- background:var(--bg-panel); border-top:var(--border-width) solid var(--border-default);
366
- padding:var(--space-2) var(--space-4);
367
- font-family:var(--font-mono); font-size:var(--text-sm); color:var(--text-tertiary);
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
- return "\n".join([_font_face_css(), _tokens_css(), SHELL_CSS])
 
 
382
 
383
 
384
  def bit_theme() -> gr.Theme:
385
- """Gradio theme aligned to the token palette.
386
 
387
- The CSS above does the detailed work; this makes Gradio's own generated
388
- chrome start from the right colors instead of fighting it everywhere.
 
 
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=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
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="#1d1c18",
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
- block_radius="4px",
411
- button_large_radius="4px",
412
- button_small_radius="4px",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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