Bit-Trading-Company commited on
Commit
e74c9bf
·
verified ·
1 Parent(s): 15ff349

CI deploy local

Browse files
.pytest_cache/v/cache/lastfailed CHANGED
@@ -1,3 +1,4 @@
1
  {
2
- "tests/test_pipeline.py::test_adapter_family[someone/my-model-tags2-chronos]": true
 
3
  }
 
1
  {
2
+ "tests/test_pipeline.py::test_adapter_family[someone/my-model-tags2-chronos]": true,
3
+ "tests/test_ui.py::test_sidebar_renders_collapsed_by_default": true
4
  }
.pytest_cache/v/cache/nodeids CHANGED
@@ -164,6 +164,7 @@
164
  "tests/test_ui.py::test_every_icon_the_nav_uses_is_vendored",
165
  "tests/test_ui.py::test_fonts_are_declared",
166
  "tests/test_ui.py::test_gradio_own_main_padding_is_reset",
 
167
  "tests/test_ui.py::test_header_result_count_reflects_matches_not_the_cap",
168
  "tests/test_ui.py::test_model_id_is_escaped_everywhere_it_appears",
169
  "tests/test_ui.py::test_no_sparkline_without_two_snapshots",
@@ -179,6 +180,7 @@
179
  "tests/test_ui.py::test_row_renders_a_dash_when_there_is_no_trend",
180
  "tests/test_ui.py::test_search_bar_is_live_not_enter_only",
181
  "tests/test_ui.py::test_sidebar_renders_collapsed_by_default",
 
182
  "tests/test_ui.py::test_theme_uses_the_designs_dense_type_scale",
183
  "tests/test_ui.py::test_trending_panel_admits_when_it_has_no_data",
184
  "tests/test_ui.py::test_uncapped_render_uses_the_designs_wording",
 
164
  "tests/test_ui.py::test_every_icon_the_nav_uses_is_vendored",
165
  "tests/test_ui.py::test_fonts_are_declared",
166
  "tests/test_ui.py::test_gradio_own_main_padding_is_reset",
167
+ "tests/test_ui.py::test_header_and_sidebar_are_sticky",
168
  "tests/test_ui.py::test_header_result_count_reflects_matches_not_the_cap",
169
  "tests/test_ui.py::test_model_id_is_escaped_everywhere_it_appears",
170
  "tests/test_ui.py::test_no_sparkline_without_two_snapshots",
 
180
  "tests/test_ui.py::test_row_renders_a_dash_when_there_is_no_trend",
181
  "tests/test_ui.py::test_search_bar_is_live_not_enter_only",
182
  "tests/test_ui.py::test_sidebar_renders_collapsed_by_default",
183
+ "tests/test_ui.py::test_sidebar_toggle_costs_no_round_trip",
184
  "tests/test_ui.py::test_theme_uses_the_designs_dense_type_scale",
185
  "tests/test_ui.py::test_trending_panel_admits_when_it_has_no_data",
186
  "tests/test_ui.py::test_uncapped_render_uses_the_designs_wording",
app.py CHANGED
@@ -307,8 +307,6 @@ def apply_action(state: dict, raw: str) -> dict:
307
  state = _submit_suggestion(state)
308
 
309
  # ---- shared chrome ------------------------------------------------
310
- elif key == "sidebar":
311
- state["sidebar_collapsed"] = not state.get("sidebar_collapsed", True)
312
  elif key == "palette":
313
  if value == "open":
314
  # Seed the palette with whatever is already in the search box, so
 
307
  state = _submit_suggestion(state)
308
 
309
  # ---- shared chrome ------------------------------------------------
 
 
310
  elif key == "palette":
311
  if value == "open":
312
  # Seed the palette with whatever is already in the search box, so
src/atlas.py CHANGED
@@ -394,10 +394,9 @@ def default_state() -> dict:
394
  "suggest_note": "",
395
  "suggest_ok": False,
396
 
397
- # Chrome shared with the other Spaces. The rail opens collapsed: on a
398
- # dashboard whose content is a wide table, 216px of navigation is
399
- # 216px not spent on data.
400
- "sidebar_collapsed": True,
401
  "palette_open": False,
402
  "palette_q": "",
403
  "soon_module": "",
 
394
  "suggest_note": "",
395
  "suggest_ok": False,
396
 
397
+ # Chrome shared with the other Spaces. The sidebar's collapsed state is
398
+ # NOT here: it is a client-side CSS attribute, so toggling it costs no
399
+ # round trip. See bit_ui.sidebar.
 
400
  "palette_open": False,
401
  "palette_q": "",
402
  "soon_module": "",
src/ui/chrome.py CHANGED
@@ -32,7 +32,10 @@ ATLAS_KEYS = frozenset({
32
  "suggestbox", # the suggestion input (arrives as suggestbox=<text>)
33
  })
34
 
35
- ALLOWED_KEYS = bridge.SHARED_KEYS | ATLAS_KEYS
 
 
 
36
 
37
  emit = bridge.make_emitter(ALLOWED_KEYS)
38
 
@@ -50,6 +53,13 @@ def parse_action(raw):
50
  # bit_ui.theme. What is left is what only this Space has.
51
 
52
  ATLAS_CSS = """
 
 
 
 
 
 
 
53
  /* The model table scrolls horizontally inside its own panel rather than
54
  making the page scroll sideways. */
55
  .bit-table-scroll { overflow-x: auto; }
@@ -65,9 +75,100 @@ ATLAS_CSS = """
65
  .bit-foot-grid { grid-template-columns: 1fr !important; }
66
  }
67
 
68
- /* A table row is clickable; make that legible to the keyboard too. */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  .bit-row:focus-visible { outline: 2px solid var(--accent-amber);
70
  outline-offset: -2px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  """
72
 
73
 
 
32
  "suggestbox", # the suggestion input (arrives as suggestbox=<text>)
33
  })
34
 
35
+ # `sidebar` is deliberately absent: collapsing is a client-side attribute flip
36
+ # in bit_ui.bridge, not a server action. Re-adding it would put a 1.16 MB
37
+ # re-render behind a purely visual toggle.
38
+ ALLOWED_KEYS = (bridge.SHARED_KEYS - {"sidebar"}) | ATLAS_KEYS
39
 
40
  emit = bridge.make_emitter(ALLOWED_KEYS)
41
 
 
53
  # bit_ui.theme. What is left is what only this Space has.
54
 
55
  ATLAS_CSS = """
56
+ /* The header sticks under nothing, so top:0; the rail is z-index 30 and wins
57
+ where they meet. A solid background matters: rows scroll underneath it. */
58
+ .bit-header {
59
+ position: sticky; top: 0; z-index: 20;
60
+ background: var(--bg-panel);
61
+ }
62
+
63
  /* The model table scrolls horizontally inside its own panel rather than
64
  making the page scroll sideways. */
65
  .bit-table-scroll { overflow-x: auto; }
 
75
  .bit-foot-grid { grid-template-columns: 1fr !important; }
76
  }
77
 
78
+ /* ------------------------------------------------------------------ *
79
+ * Model table rows.
80
+ *
81
+ * These are classes rather than inline styles for one reason: the row is
82
+ * the only markup that repeats 300 times. The design's inline styles came
83
+ * to 2,644 characters per row -- ~793 KB of identical repetition per
84
+ * render, and 95% of the whole page payload. Every Gradio action re-sends
85
+ * the page, so that weight was paid on every filter, sort and click.
86
+ *
87
+ * The VALUES below are the design's, unchanged. Only where they live has
88
+ * moved. Everything that varies per row is an enumerable variant
89
+ * (verified / selected / licence bucket / age band / trend direction), so
90
+ * no row needs an inline style at all.
91
+ * ------------------------------------------------------------------ */
92
+
93
+ .bit-row {
94
+ display: grid;
95
+ grid-template-columns: minmax(230px,2.4fr) 104px 92px 148px 60px 116px 104px 112px;
96
+ align-items: center;
97
+ border-bottom: 1px solid var(--border-subtle);
98
+ border-left: 2px solid transparent;
99
+ background: transparent;
100
+ cursor: pointer;
101
+ }
102
+ .bit-row:hover { background: var(--bg-raised); }
103
+ .bit-row-verified { border-left-color: var(--accent-amber); }
104
+ .bit-row-selected { background: var(--bg-raised); }
105
  .bit-row:focus-visible { outline: 2px solid var(--accent-amber);
106
  outline-offset: -2px; }
107
+
108
+ .bit-cell { padding: 6px 8px; border-right: 1px solid var(--border-subtle);
109
+ min-width: 0; }
110
+ .bit-cell-end { border-right: none; }
111
+ .bit-cell-r { text-align: right; }
112
+
113
+ .bit-model { display: flex; align-items: center; gap: 8px; }
114
+ .bit-avatar {
115
+ flex: 0 0 auto; width: 20px; height: 20px; display: flex;
116
+ align-items: center; justify-content: center; font-size: 9px;
117
+ background: var(--stone-700); color: var(--text-secondary);
118
+ }
119
+ .bit-row-verified .bit-avatar { background: var(--accent-amber);
120
+ color: var(--stone-950); }
121
+ .bit-model-text { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
122
+ .bit-id { font-size: var(--text-xs); color: var(--text-primary);
123
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
124
+ .bit-sub { font-size: var(--text-2xs); color: var(--text-tertiary);
125
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
126
+
127
+ .bit-tag {
128
+ display: inline-flex; align-items: center; gap: 3px; padding: 1px 5px;
129
+ border: 1px solid var(--border-default); color: var(--text-secondary);
130
+ font-family: var(--font-mono); font-size: var(--text-2xs);
131
+ white-space: nowrap; max-width: 100%; overflow: hidden;
132
+ }
133
+ .bit-tag-asset { border-color: var(--border-subtle); background: var(--bg-raised); }
134
+ .bit-tag-lic-permissive { border-color: var(--accent-moss-dim);
135
+ color: var(--accent-moss-strong); }
136
+ .bit-tag-lic-restricted { border-color: var(--accent-amber-dim);
137
+ color: var(--accent-amber-strong); }
138
+ .bit-tag-lic-none { border-color: var(--mute-red); color: var(--mute-red); }
139
+
140
+ .bit-dl { display: flex; align-items: center; gap: 7px;
141
+ justify-content: flex-end; }
142
+ .bit-num { font-size: var(--text-xs); color: var(--text-primary);
143
+ white-space: nowrap; }
144
+ .bit-likes { font-size: var(--text-xs); color: var(--text-secondary); }
145
+ .bit-spark { flex: 0 0 auto; }
146
+ .bit-spark-up path { stroke: var(--fin-up); }
147
+ .bit-spark-down path { stroke: var(--fin-down); }
148
+ .bit-nodata { font-size: var(--text-2xs); color: var(--text-tertiary);
149
+ width: 44px; text-align: center; flex: 0 0 auto; }
150
+
151
+ .bit-agecell { display: flex; align-items: center; gap: 5px; }
152
+ .bit-age { font-size: var(--text-xs); white-space: nowrap; }
153
+ .bit-age-glyph { font-size: var(--text-2xs); }
154
+ .bit-age-fresh, .bit-age-fresh .bit-age { color: var(--fin-up); }
155
+ .bit-age-warn, .bit-age-warn .bit-age { color: var(--accent-amber-strong); }
156
+ .bit-age-stale, .bit-age-stale .bit-age { color: var(--fin-down); }
157
+ .bit-age-none, .bit-age-none .bit-age { color: var(--text-tertiary); }
158
+
159
+ .bit-badges { display: flex; align-items: center; gap: 4px; }
160
+ .bit-badge {
161
+ display: inline-flex; align-items: center; gap: 3px; padding: 1px 4px;
162
+ border: 1px solid var(--border-default); background: transparent;
163
+ color: var(--text-secondary); font-family: var(--font-mono);
164
+ font-size: var(--text-2xs); white-space: nowrap;
165
+ }
166
+ .bit-badge-verified { border-color: var(--accent-amber);
167
+ background: var(--accent-amber-dim);
168
+ color: var(--text-primary); }
169
+ .bit-badge-flag { border-color: var(--accent-amber-dim);
170
+ color: var(--accent-amber-strong); }
171
+ .bit-badge-unclear { color: var(--text-tertiary); }
172
  """
173
 
174
 
src/ui/shell.py CHANGED
@@ -147,7 +147,7 @@ def header(index, tape_rows) -> str:
147
 
148
  total = index.total
149
  return f"""
150
- <header style="position:sticky;top:0;z-index:20;background:var(--bg-panel);
151
  border-bottom:1px solid var(--border-default)">
152
  <div style="display:flex;align-items:center;gap:10px;padding:8px 14px;
153
  min-height:48px;flex-wrap:wrap">
@@ -473,29 +473,46 @@ def _badges(row) -> str:
473
  out = []
474
  if row.get("verified"):
475
  out.append(("✓", "Verified by a Bit Trading researcher",
476
- "var(--accent-amber)", "var(--accent-amber-dim)",
477
- "var(--text-primary)"))
478
  flags = row.get("red_flags") or []
479
  if len(flags):
480
- tip = "; ".join(str(f) for f in flags)
481
- out.append(("⚠", tip, "var(--accent-amber-dim)", "transparent",
482
- "var(--accent-amber-strong)"))
483
  if row.get("has_eval"):
484
- out.append(("▤", "Model card claims evaluation on a named test set",
485
- "var(--border-default)", "transparent", "var(--text-secondary)"))
486
  if row.get("relevant") == "unclear":
487
  out.append(("?", "The classifier could not read this card confidently",
488
- "var(--border-default)", "transparent", "var(--text-tertiary)"))
489
  return "".join(
490
- f'<span title="{a(tip)}" style="display:inline-flex;align-items:center;'
491
- f'gap:3px;padding:1px 4px;border:1px solid {border};background:{background};'
492
- f'color:{colour};font-family:var(--font-mono);font-size:var(--text-2xs);'
493
- f'white-space:nowrap">{e(glyph)}</span>'
494
- for glyph, tip, border, background, colour in out
495
  )
496
 
497
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  def _table_row(row, index, selected: bool) -> str:
 
 
 
 
 
 
 
499
  model_id = row.get("id", "")
500
  labels = index.taxonomy.get("task_labels", {})
501
  assets = index.taxonomy.get("asset_labels", {})
@@ -503,95 +520,72 @@ def _table_row(row, index, selected: bool) -> str:
503
 
504
  series = index.trends.get(model_id)
505
  path = sparkline(series)
506
- change = None
507
- if series:
508
  from ..atlas import growth
509
  change = growth(series)
510
- if path:
511
- colour = "var(--fin-up)" if (change or 0) >= 0 else "var(--fin-down)"
512
  spark = (f'<svg width="44" height="14" viewBox="0 0 64 18" '
513
  f'preserveAspectRatio="none" aria-hidden="true" '
514
- f'style="flex:0 0 auto"><path d="{a(path)}" style="fill:none;'
515
- f'stroke:{colour};stroke-width:1.5"></path></svg>')
516
  else:
517
  # No second snapshot yet: there is no trend to draw, and drawing one
518
  # anyway would be inventing data.
519
- spark = (f'<span class="mono-data" title="Not enough weekly snapshots yet" '
520
- f'style="font-size:var(--text-2xs);color:var(--text-tertiary);'
521
- f'width:44px;text-align:center;flex:0 0 auto">{DASH}</span>')
522
-
523
- glyph, colour, border, tip = license_style(row.get("license_bucket"))
524
- licence_text = row.get("license") or "none"
525
- age_colour, age_glyph = fmt.age_tone(row.get("last_modified"))
 
526
  base = row.get("base_model") or ""
527
  sub = f"from {base}" if base else "no declared base model"
528
 
 
 
 
 
 
 
529
  return (
530
  f'<div role="row" tabindex="0" data-bit="{a(emit("open", model_id))}" '
531
- f'class="bit-hover-row" '
532
- f'style="display:grid;grid-template-columns:{GRID};align-items:center;'
533
- f'border-bottom:1px solid var(--border-subtle);cursor:pointer;'
534
- f'background:{"var(--bg-raised)" if selected else "transparent"};'
535
- f'border-left:2px solid '
536
- f'{"var(--accent-amber)" if verified else "transparent"}">'
537
-
538
- f'<div style="display:flex;align-items:center;gap:8px;padding:6px 8px;'
539
- f'min-width:0;border-right:1px solid var(--border-subtle)">'
540
- f'<span class="mono-data" aria-hidden="true" style="flex:0 0 auto;width:20px;'
541
- f'height:20px;display:flex;align-items:center;justify-content:center;'
542
- f'background:{"var(--accent-amber)" if verified else "var(--stone-700)"};'
543
- f'color:{"var(--stone-950)" if verified else "var(--text-secondary)"};'
544
- f'font-size:9px">{e(fmt.initials(model_id))}</span>'
545
- f'<span style="min-width:0;display:flex;flex-direction:column;gap:1px">'
546
- f'<span class="mono-data" style="font-size:var(--text-xs);'
547
- f'color:var(--text-primary);white-space:nowrap;overflow:hidden;'
548
- f'text-overflow:ellipsis">{e(model_id)}</span>'
549
- f'<span style="font-size:var(--text-2xs);color:var(--text-tertiary);'
550
- f'white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{e(sub)}</span>'
551
- f'</span></div>'
552
 
553
- f'<div style="padding:6px 8px;border-right:1px solid var(--border-subtle);'
554
- f'min-width:0"><span style="display:inline-flex;align-items:center;gap:3px;'
555
- f'padding:1px 5px;border:1px solid var(--border-default);'
556
- f'color:var(--text-secondary);font-family:var(--font-mono);'
557
- f'font-size:var(--text-2xs);white-space:nowrap;max-width:100%;'
558
- f'overflow:hidden">{e(task_glyph(row.get("task")))} '
 
 
 
559
  f'{e(labels.get(row.get("task"), row.get("task")))}</span></div>'
560
 
561
- f'<div style="padding:6px 8px;border-right:1px solid var(--border-subtle);'
562
- f'min-width:0"><span style="display:inline-flex;align-items:center;gap:3px;'
563
- f'padding:1px 5px;border:1px solid var(--border-subtle);'
564
- f'background:var(--bg-raised);color:var(--text-secondary);'
565
- f'font-family:var(--font-mono);font-size:var(--text-2xs);white-space:nowrap;'
566
- f'max-width:100%;overflow:hidden">{e(asset_glyph(row.get("asset_class")))} '
567
  f'{e(assets.get(row.get("asset_class"), row.get("asset_class")))}</span></div>'
568
 
569
- f'<div style="display:flex;align-items:center;gap:7px;padding:6px 8px;'
570
- f'border-right:1px solid var(--border-subtle);justify-content:flex-end">'
571
- f'<span class="mono-data" style="font-size:var(--text-xs);'
572
- f'color:var(--text-primary);white-space:nowrap">'
573
  f'{e(fmt.num(row.get("downloads_30d")))}</span>{spark}</div>'
574
 
575
- f'<div style="padding:6px 8px;border-right:1px solid var(--border-subtle);'
576
- f'text-align:right"><span class="mono-data" style="font-size:var(--text-xs);'
577
- f'color:var(--text-secondary)">{e(fmt.num(row.get("likes")))}</span></div>'
578
 
579
- f'<div style="padding:6px 8px;border-right:1px solid var(--border-subtle);'
580
- f'min-width:0"><span title="{a(tip)}" style="display:inline-flex;'
581
- f'align-items:center;gap:4px;padding:1px 5px;border:1px solid {border};'
582
- f'color:{colour};font-family:var(--font-mono);font-size:var(--text-2xs);'
583
- f'white-space:nowrap;max-width:100%;overflow:hidden">{e(glyph)} '
584
- f'{e(licence_text)}</span></div>'
585
-
586
- f'<div style="display:flex;align-items:center;gap:5px;padding:6px 8px;'
587
- f'border-right:1px solid var(--border-subtle)">'
588
- f'<span class="mono-data" aria-hidden="true" style="font-size:var(--text-2xs);'
589
- f'color:{age_colour}">{e(age_glyph)}</span>'
590
- f'<span class="mono-data" style="font-size:var(--text-xs);color:{age_colour};'
591
- f'white-space:nowrap">{e(fmt.age_label(row.get("last_modified")))}</span></div>'
592
-
593
- f'<div style="display:flex;align-items:center;gap:4px;padding:6px 8px">'
594
- f'{_badges(row)}</div>'
595
  f'</div>'
596
  )
597
 
@@ -1390,9 +1384,7 @@ def page(index, state, view) -> str:
1390
  <div class="bit-atlas bit-app" data-theme="dark"
1391
  style="background:var(--bg-canvas);color:var(--text-primary);display:flex;
1392
  align-items:stretch;font-family:var(--font-body);font-size:var(--text-base)">
1393
- {bit_sidebar.render(navigation, active='Finance Atlas',
1394
- collapsed=bool(state.get('sidebar_collapsed', True)),
1395
- emit=emit)}
1396
  <div style="flex:1 1 auto;min-width:0;display:flex;flex-direction:column">
1397
  {header(index, view['tape'])}
1398
  <main style="flex:1 1 auto;min-width:0;padding:12px;display:flex;
 
147
 
148
  total = index.total
149
  return f"""
150
+ <header class="bit-header" style="background:var(--bg-panel);
151
  border-bottom:1px solid var(--border-default)">
152
  <div style="display:flex;align-items:center;gap:10px;padding:8px 14px;
153
  min-height:48px;flex-wrap:wrap">
 
473
  out = []
474
  if row.get("verified"):
475
  out.append(("✓", "Verified by a Bit Trading researcher",
476
+ "bit-badge-verified"))
 
477
  flags = row.get("red_flags") or []
478
  if len(flags):
479
+ out.append(("⚠", "; ".join(str(f) for f in flags), "bit-badge-flag"))
 
 
480
  if row.get("has_eval"):
481
+ out.append(("▤", "Model card claims evaluation on a named test set", ""))
 
482
  if row.get("relevant") == "unclear":
483
  out.append(("?", "The classifier could not read this card confidently",
484
+ "bit-badge-unclear"))
485
  return "".join(
486
+ f'<span title="{a(tip)}" class="bit-badge {variant}">{e(glyph)}</span>'
487
+ for glyph, tip, variant in out
 
 
 
488
  )
489
 
490
 
491
+ LICENSE_BUCKET_CLASS = {
492
+ "permissive": "bit-tag-lic-permissive",
493
+ "restricted": "bit-tag-lic-restricted",
494
+ "none": "bit-tag-lic-none",
495
+ }
496
+
497
+
498
+ def _age_class(months) -> str:
499
+ if months is None:
500
+ return "bit-age-none"
501
+ if months < 3:
502
+ return "bit-age-fresh"
503
+ if months < 12:
504
+ return "bit-age-warn"
505
+ return "bit-age-stale"
506
+
507
+
508
  def _table_row(row, index, selected: bool) -> str:
509
+ """One row of the model index.
510
+
511
+ Rendered with CSS classes rather than the design's inline styles. This is
512
+ the only markup that repeats 300 times, and inline it came to 2,644
513
+ characters a row -- ~793 KB per render, 95% of the page, re-sent on every
514
+ action. The design's values are unchanged; see `chrome.ATLAS_CSS`.
515
+ """
516
  model_id = row.get("id", "")
517
  labels = index.taxonomy.get("task_labels", {})
518
  assets = index.taxonomy.get("asset_labels", {})
 
520
 
521
  series = index.trends.get(model_id)
522
  path = sparkline(series)
523
+ if path:
 
524
  from ..atlas import growth
525
  change = growth(series)
526
+ direction = "bit-spark-up" if (change or 0) >= 0 else "bit-spark-down"
 
527
  spark = (f'<svg width="44" height="14" viewBox="0 0 64 18" '
528
  f'preserveAspectRatio="none" aria-hidden="true" '
529
+ f'class="bit-spark {direction}"><path d="{a(path)}" '
530
+ f'fill="none" stroke-width="1.5"></path></svg>')
531
  else:
532
  # No second snapshot yet: there is no trend to draw, and drawing one
533
  # anyway would be inventing data.
534
+ spark = (f'<span class="mono-data bit-nodata" '
535
+ f'title="Not enough weekly snapshots yet">{DASH}</span>')
536
+
537
+ glyph, _colour, _border, tip = license_style(row.get("license_bucket"))
538
+ lic_class = LICENSE_BUCKET_CLASS.get(row.get("license_bucket"),
539
+ "bit-tag-lic-none")
540
+ age_class = _age_class(fmt.months_since(row.get("last_modified")))
541
+ _, age_glyph = fmt.age_tone(row.get("last_modified"))
542
  base = row.get("base_model") or ""
543
  sub = f"from {base}" if base else "no declared base model"
544
 
545
+ classes = "bit-row"
546
+ if verified:
547
+ classes += " bit-row-verified"
548
+ if selected:
549
+ classes += " bit-row-selected"
550
+
551
  return (
552
  f'<div role="row" tabindex="0" data-bit="{a(emit("open", model_id))}" '
553
+ f'class="{classes}">'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
 
555
+ f'<div class="bit-cell bit-model">'
556
+ f'<span class="mono-data bit-avatar" aria-hidden="true">'
557
+ f'{e(fmt.initials(model_id))}</span>'
558
+ f'<span class="bit-model-text">'
559
+ f'<span class="mono-data bit-id">{e(model_id)}</span>'
560
+ f'<span class="bit-sub">{e(sub)}</span></span></div>'
561
+
562
+ f'<div class="bit-cell"><span class="bit-tag">'
563
+ f'{e(task_glyph(row.get("task")))} '
564
  f'{e(labels.get(row.get("task"), row.get("task")))}</span></div>'
565
 
566
+ f'<div class="bit-cell"><span class="bit-tag bit-tag-asset">'
567
+ f'{e(asset_glyph(row.get("asset_class")))} '
 
 
 
 
568
  f'{e(assets.get(row.get("asset_class"), row.get("asset_class")))}</span></div>'
569
 
570
+ f'<div class="bit-cell bit-dl">'
571
+ f'<span class="mono-data bit-num">'
 
 
572
  f'{e(fmt.num(row.get("downloads_30d")))}</span>{spark}</div>'
573
 
574
+ f'<div class="bit-cell bit-cell-r">'
575
+ f'<span class="mono-data bit-likes">'
576
+ f'{e(fmt.num(row.get("likes")))}</span></div>'
577
 
578
+ f'<div class="bit-cell"><span class="bit-tag {lic_class}" '
579
+ f'title="{a(tip)}">{e(glyph)} '
580
+ f'{e(row.get("license") or "none")}</span></div>'
581
+
582
+ f'<div class="bit-cell bit-agecell {age_class}">'
583
+ f'<span class="mono-data bit-age-glyph" aria-hidden="true">'
584
+ f'{e(age_glyph)}</span>'
585
+ f'<span class="mono-data bit-age">'
586
+ f'{e(fmt.age_label(row.get("last_modified")))}</span></div>'
587
+
588
+ f'<div class="bit-cell bit-cell-end bit-badges">{_badges(row)}</div>'
 
 
 
 
 
589
  f'</div>'
590
  )
591
 
 
1384
  <div class="bit-atlas bit-app" data-theme="dark"
1385
  style="background:var(--bg-canvas);color:var(--text-primary);display:flex;
1386
  align-items:stretch;font-family:var(--font-body);font-size:var(--text-base)">
1387
+ {bit_sidebar.render(navigation, active='Finance Atlas', emit=emit)}
 
 
1388
  <div style="flex:1 1 auto;min-width:0;display:flex;flex-direction:column">
1389
  {header(index, view['tape'])}
1390
  <main style="flex:1 1 auto;min-width:0;padding:12px;display:flex;
tests/test_ui.py CHANGED
@@ -353,17 +353,35 @@ def test_gradio_own_main_padding_is_reset():
353
  assert "main.fillable" in css
354
 
355
 
356
- def test_sidebar_renders_collapsed_by_default(index):
 
 
 
 
 
357
  from bit_ui import nav as bit_nav
358
 
359
- state = atlas.default_state()
360
- assert state["sidebar_collapsed"] is True
361
  view = {"rows": [], "matched": 0, "truncated": 0, "hidden": 0, "tape": [],
362
  "trending": [], "links": {}, "nav": bit_nav.default_document(),
363
  "palette_groups": []}
364
- html = shell.page(index, state, view)
365
- assert 'data-collapsed="1"' in html
366
- assert 'data-bit="sidebar:toggle"' in html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
 
368
 
369
  def test_search_bar_is_live_not_enter_only(index):
 
353
  assert "main.fillable" in css
354
 
355
 
356
+ def test_sidebar_toggle_costs_no_round_trip(index):
357
+ """Collapsing is a client-side attribute flip, not a server action.
358
+
359
+ Every action re-renders the whole page: 1.16 MB here, 95% of it the model
360
+ table. Putting that behind a purely visual toggle measured at 1.5-2.2s.
361
+ """
362
  from bit_ui import nav as bit_nav
363
 
 
 
364
  view = {"rows": [], "matched": 0, "truncated": 0, "hidden": 0, "tape": [],
365
  "trending": [], "links": {}, "nav": bit_nav.default_document(),
366
  "palette_groups": []}
367
+ html = shell.page(index, atlas.default_state(), view)
368
+
369
+ assert "data-bit-sidebar-toggle" in html
370
+ assert "sidebar:toggle" not in html
371
+ assert "sidebar" not in chrome.ALLOWED_KEYS, (
372
+ "the sidebar action is back on the allow-list")
373
+
374
+
375
+ def test_header_and_sidebar_are_sticky(index):
376
+ from bit_ui import nav as bit_nav
377
+
378
+ css = chrome.full_css()
379
+ assert ".bit-header" in css and "position: sticky" in css
380
+
381
+ view = {"rows": [], "matched": 0, "truncated": 0, "hidden": 0, "tape": [],
382
+ "trending": [], "links": {}, "nav": bit_nav.default_document(),
383
+ "palette_groups": []}
384
+ assert 'class="bit-header"' in shell.page(index, atlas.default_state(), view)
385
 
386
 
387
  def test_search_bar_is_live_not_enter_only(index):
vendor/bit-ui/bit_ui/bridge.py CHANGED
@@ -167,7 +167,40 @@ BRIDGE_LOAD_JS = """
167
  }
168
  window.__bitSend = send;
169
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  document.addEventListener('click', function (e) {
 
 
 
 
 
 
 
 
 
171
  var el = e.target && e.target.closest ? e.target.closest('[data-bit]') : null;
172
  if (!el) return;
173
  var action = el.getAttribute('data-bit');
@@ -271,9 +304,22 @@ BRIDGE_LOAD_JS = """
271
  try { el.focus(); if (el.select) el.select(); } catch (err) {}
272
  }, 30);
273
  }
274
- new MutationObserver(applyAutofocus)
 
 
 
 
 
 
 
 
 
 
 
 
275
  .observe(document.body, { childList: true, subtree: true });
276
  applyAutofocus();
 
277
  }
278
  """.replace("__ELEM_ID__", ACTION_ELEMENT_ID).replace(
279
  "__TRIGGER_ID__", TRIGGER_ELEMENT_ID)
 
167
  }
168
  window.__bitSend = send;
169
 
170
+ // The sidebar collapse is pure presentation, so it is handled entirely
171
+ // here: flip an attribute on <body> and stop. No server call, no re-render.
172
+ // Round-tripping it meant re-sending the whole page (1.16 MB on the Atlas,
173
+ // 95% of it the model table) to move a rail 160px -- 1.5-2.2s for something
174
+ // that should be instant.
175
+ //
176
+ // <body> and not a wrapper inside the app: everything inside the gr.HTML
177
+ // sink is replaced on the next re-render, which would reset the rail.
178
+ var SIDEBAR_KEY = 'bit-sidebar';
179
+ function setSidebar(expanded) {
180
+ if (expanded) { document.body.setAttribute('data-sidebar', 'expanded'); }
181
+ else { document.body.removeAttribute('data-sidebar'); }
182
+ var btn = document.querySelector('[data-bit-sidebar-toggle]');
183
+ if (btn) {
184
+ btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
185
+ btn.setAttribute('title', (expanded ? 'Collapse' : 'Expand') + ' sidebar');
186
+ }
187
+ try { localStorage.setItem(SIDEBAR_KEY, expanded ? 'expanded' : 'collapsed'); }
188
+ catch (err) {}
189
+ }
190
+ try {
191
+ if (localStorage.getItem(SIDEBAR_KEY) === 'expanded') setSidebar(true);
192
+ } catch (err) {}
193
+
194
  document.addEventListener('click', function (e) {
195
+ var toggle = e.target && e.target.closest
196
+ ? e.target.closest('[data-bit-sidebar-toggle]') : null;
197
+ if (toggle) {
198
+ e.preventDefault();
199
+ e.stopPropagation();
200
+ setSidebar(document.body.getAttribute('data-sidebar') !== 'expanded');
201
+ return;
202
+ }
203
+
204
  var el = e.target && e.target.closest ? e.target.closest('[data-bit]') : null;
205
  if (!el) return;
206
  var action = el.getAttribute('data-bit');
 
304
  try { el.focus(); if (el.select) el.select(); } catch (err) {}
305
  }, 30);
306
  }
307
+ // A re-render replaces the toggle button, so its ARIA has to be restored
308
+ // from the attribute that survived on <body>.
309
+ function syncSidebarButton() {
310
+ var btn = document.querySelector('[data-bit-sidebar-toggle]');
311
+ if (!btn) return;
312
+ var expanded = document.body.getAttribute('data-sidebar') === 'expanded';
313
+ if (btn.getAttribute('aria-expanded') !== String(expanded)) {
314
+ btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
315
+ btn.setAttribute('title', (expanded ? 'Collapse' : 'Expand') + ' sidebar');
316
+ }
317
+ }
318
+
319
+ new MutationObserver(function () { applyAutofocus(); syncSidebarButton(); })
320
  .observe(document.body, { childList: true, subtree: true });
321
  applyAutofocus();
322
+ syncSidebarButton();
323
  }
324
  """.replace("__ELEM_ID__", ACTION_ELEMENT_ID).replace(
325
  "__TRIGGER_ID__", TRIGGER_ELEMENT_ID)
vendor/bit-ui/bit_ui/sidebar.py CHANGED
@@ -4,11 +4,25 @@ Renders the design's own `App Sidebar.dc.html` markup from a nav document
4
  (`nav.py`) plus two pieces of per-Space state: which item is active, and
5
  whether the rail is collapsed.
6
 
7
- ## Collapsed by default
8
 
9
- The design supports both widths (216px expanded, 56px collapsed) and the Spaces
10
- open collapsed. On a dashboard whose content is a wide table, 216px of
11
- navigation is 216px not spent on data, and the rail is one click away.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  ## No `vh` units
14
 
@@ -24,6 +38,17 @@ forever into nothing.
24
 
25
  `max-height` is safe because it only ever caps. `height` and `min-height` are
26
  not, and are not used.
 
 
 
 
 
 
 
 
 
 
 
27
  """
28
 
29
  from __future__ import annotations
@@ -36,7 +61,7 @@ COLLAPSED_W = "56px"
36
 
37
  # The Bit Trading mark, from the design.
38
  BIT_MARK = (
39
- '<svg viewBox="0 0 100 100" width="{size}" height="{size}" role="img" '
40
  'aria-label="The Bit Trading Company" style="flex:0 0 auto;display:block">'
41
  '<path fill="var(--accent-amber)" fill-rule="evenodd" '
42
  'd="M0 0H100V100H0Z M50 10H90V90H50Z M22 42H38V58H22Z M62 42H78V58H62Z">'
@@ -44,8 +69,13 @@ BIT_MARK = (
44
  )
45
 
46
 
47
- def _item(item: dict, active: bool, collapsed: bool, emit) -> str:
48
- """One nav row: a link when live, a button when it opens something."""
 
 
 
 
 
49
  name = item.get("name", "")
50
  status = item.get("status", "soon")
51
 
@@ -58,100 +88,74 @@ def _item(item: dict, active: bool, collapsed: bool, emit) -> str:
58
  else:
59
  mark = "transparent"
60
 
61
- justify = "center" if collapsed else "flex-start"
62
- label_display = "none" if collapsed else "block"
63
-
64
  style = (
65
- f"display:flex;align-items:center;justify-content:{justify};gap:10px;"
66
- f"padding:6px 12px;color:{colour};background:{background};"
67
- f"text-decoration:none;border-left:2px solid {mark};white-space:nowrap;"
68
- f"overflow:hidden;width:100%;border-top:none;border-right:none;"
69
- f"border-bottom:none;font:inherit;text-align:left;cursor:pointer"
70
  )
71
  inner = (
72
  f'{icon(item.get("icon") or "cube", "14px")}'
73
- f'<span style="font-family:var(--font-styrene);font-size:var(--text-sm);'
74
- f'text-transform:uppercase;letter-spacing:var(--tracking-wide);'
75
- f'display:{label_display};min-width:0;overflow:hidden;'
76
- f'text-overflow:ellipsis;white-space:nowrap">{e(name)}</span>'
77
  )
78
- # Collapsed, the label is gone, so the tooltip is the only affordance left.
79
- title = name if not collapsed else f"{name}" + (
80
- "" if status == "live" else " — coming soon")
81
 
82
  if status == "live" and item.get("url"):
83
  current = ' aria-current="page"' if active else ""
84
  return (f'<a href="{a(item["url"])}" target="_blank" '
85
  f'rel="noopener noreferrer" title="{a(title)}"{current} '
86
- f'class="bit-hover-nav" style="{style}">{inner}</a>')
87
 
88
  action = ("contact:open" if item.get("action") == "contact"
89
  else f"soon:{name}")
90
  return (f'<button type="button" data-bit="{a(emit(action))}" '
91
- f'title="{a(title)}" class="bit-hover-nav" '
92
- f'style="{style};background:{background}">{inner}</button>')
93
 
94
 
95
  def render(navigation: dict, active: str = "", collapsed: bool = True,
96
  emit=None) -> str:
97
  """The sidebar markup.
98
 
 
 
 
 
99
  `emit` builds a `data-bit` attribute value for an action; the Space passes
100
  its own bridge's `emit` so every action stays on that Space's allow-list.
101
  """
102
  if emit is None:
103
  from .bridge import emit as emit # noqa: PLW0127
104
 
105
- width = COLLAPSED_W if collapsed else EXPANDED_W
106
- label_display = "none" if collapsed else "block"
107
- brand_text = "none" if collapsed else "flex"
108
- brand_dir = "column" if collapsed else "row"
109
- brand_btn_ml = "0" if collapsed else "auto"
110
- toggle_rot = "0deg" if collapsed else "180deg"
111
- toggle_title = "Expand sidebar" if collapsed else "Collapse sidebar"
112
- mark_size = "22" if collapsed else "32"
113
-
114
  brand = navigation.get("brand") or {}
115
  brand_href = brand.get("href") or ""
116
- mark = BIT_MARK.format(size=mark_size)
117
  if brand_href:
118
  mark = (f'<a href="{a(brand_href)}" target="_blank" '
119
  f'rel="noopener noreferrer" title="{a(brand.get("title") or "")}" '
120
- f'style="display:flex;align-items:center;justify-content:center;'
121
- f'text-decoration:none">{mark}</a>')
122
 
123
  groups = []
124
  for group in navigation.get("groups") or ():
125
  rows = "".join(
126
- _item(item, item.get("name") == active, collapsed, emit)
127
  for item in group.get("items") or ()
128
  )
129
  groups.append(
130
- f'<div style="padding:10px 12px 4px;font-family:var(--font-styrene);'
131
- f'font-size:var(--text-2xs);letter-spacing:var(--tracking-wider);'
132
- f'text-transform:uppercase;color:var(--text-tertiary);'
133
- f'white-space:nowrap;overflow:hidden;display:{label_display}">'
134
- f'{e(group["label"])}</div>{rows}'
135
  )
136
 
137
  assistant = navigation.get("assistant") or {}
138
- ase_size = "16px" if collapsed else "32px"
139
  ase = ""
140
  if assistant.get("name"):
141
  ase = (
142
  f'<div style="border-top:1px solid var(--border-default);'
143
  f'background:var(--bg-raised)">'
144
  f'<button type="button" data-bit="{a(emit("soon:" + assistant["name"]))}" '
145
- f'title="{a(assistant["name"])} — coming soon" class="bit-hover-ase" '
146
- f'style="width:100%;display:flex;flex-direction:{brand_dir};'
147
- f'align-items:center;gap:10px;padding:12px;background:transparent;'
148
- f'border:none;border-left:2px solid var(--accent-amber-dim);'
149
- f'cursor:pointer;text-align:left">'
150
- f'<span aria-hidden="true" style="flex:0 0 auto;display:block;'
151
- f'width:{ase_size};height:{ase_size};background:currentColor;'
152
- f'color:var(--text-secondary);border-radius:50%;opacity:0.8"></span>'
153
- f'<span style="display:{brand_text};flex-direction:column;gap:2px;'
154
- f'min-width:0">'
155
  f'<span style="font-family:var(--font-styrene);font-size:var(--text-md);'
156
  f'text-transform:uppercase;letter-spacing:var(--tracking-wide);'
157
  f'color:var(--text-secondary);white-space:nowrap;margin-top:2px">'
@@ -163,19 +167,10 @@ def render(navigation: dict, active: str = "", collapsed: bool = True,
163
  )
164
 
165
  return f"""
166
- <aside class="bit-sidebar bit-scroll" data-collapsed="{'1' if collapsed else '0'}"
167
- style="width:{width};flex:0 0 {width};align-self:stretch;
168
- border-right:1px solid var(--border-default);background:var(--bg-panel);
169
- position:sticky;top:0;max-height:100vh;overflow-y:auto;overflow-x:hidden;
170
- display:flex;flex-direction:column;font-family:var(--font-body);
171
- font-size:var(--text-base);color:var(--text-primary);
172
- transition:width 0.25s ease,flex-basis 0.25s ease">
173
-
174
- <div style="display:flex;flex-direction:{brand_dir};align-items:center;gap:10px;
175
- padding:12px;border-bottom:1px solid var(--border-default);min-height:49px">
176
  {mark}
177
- <div style="display:{brand_text};flex-direction:column;gap:1px;min-width:0;
178
- overflow:hidden">
179
  <span style="font-family:var(--font-styrene);font-size:var(--text-2xs);
180
  text-transform:uppercase;letter-spacing:var(--tracking-wide);
181
  color:var(--text-tertiary);white-space:nowrap">{e(brand.get('label') or '')}</span>
@@ -184,27 +179,19 @@ def render(navigation: dict, active: str = "", collapsed: bool = True,
184
  font-size:var(--text-lg);line-height:var(--leading-tight);
185
  white-space:nowrap">{e(brand.get('sub') or '')}</span>
186
  </div>
187
- <button type="button" data-bit="{a(emit('sidebar:toggle'))}"
188
- title="{a(toggle_title)}" aria-label="{a(toggle_title)}"
189
- aria-expanded="{'false' if collapsed else 'true'}"
190
- class="bit-hover-text"
191
- style="margin-left:{brand_btn_ml};background:transparent;border:none;
192
- color:var(--text-tertiary);width:22px;height:22px;display:flex;
193
- align-items:center;justify-content:center;cursor:pointer;flex:0 0 auto;
194
- transition:transform 0.25s ease;transform:rotate({toggle_rot})">
195
  {icon('chevronRight', '12px')}
196
  </button>
197
  </div>
198
 
199
- <nav aria-label="Bit Trading Company modules"
200
- style="padding:8px 0;display:flex;flex-direction:column;gap:2px">
201
- {''.join(groups)}
202
- </nav>
203
 
204
  <div style="margin-top:auto">
205
  {ase}
206
- <div style="padding:10px 12px;border-top:1px solid var(--border-default);
207
- display:{label_display}">
208
  <div class="pixel-text" style="color:var(--text-tertiary)">{e(_build_line())}</div>
209
  </div>
210
  </div>
 
4
  (`nav.py`) plus two pieces of per-Space state: which item is active, and
5
  whether the rail is collapsed.
6
 
7
+ ## Collapsing is CSS, not a round trip
8
 
9
+ The rail renders once, in both states, and `body[data-sidebar]` decides which
10
+ one is visible. The toggle flips that attribute in the browser and stops --
11
+ there is no server call, so it is instant.
12
+
13
+ This matters more than it sounds. Every Gradio action re-renders the whole page
14
+ and ships it back: on the Atlas that is ~1.16 MB, 95% of which is the model
15
+ table. Round-tripping the sidebar meant re-transmitting 300 table rows to move a
16
+ rail 160px, which measured at 1.5-2.2s. The design's own component collapses in
17
+ local client state for exactly this reason.
18
+
19
+ The attribute lives on `<body>`, deliberately **outside** the `gr.HTML` sink.
20
+ Anything inside it is replaced wholesale on the next re-render, which would
21
+ snap the rail back to collapsed the first time someone touched a filter.
22
+
23
+ No attribute means collapsed, so the server's first paint needs no JavaScript
24
+ and there is nothing to flash. The Spaces open collapsed: on a dashboard whose
25
+ content is a wide table, 216px of navigation is 216px not spent on data.
26
 
27
  ## No `vh` units
28
 
 
38
 
39
  `max-height` is safe because it only ever caps. `height` and `min-height` are
40
  not, and are not used.
41
+
42
+ ## Sticky
43
+
44
+ `position:sticky; top:0` with `align-self:flex-start`. The `flex-start` is
45
+ load-bearing: a stretched flex item is already as tall as its container, so it
46
+ has no room to stick within and the property silently does nothing.
47
+
48
+ Sticky works on the Space's own URL. On the huggingface.co embed the iframe is
49
+ `scrolling="no"` and sized to the content, so the *parent* page scrolls and
50
+ nothing inside the iframe can stick to the viewport. That is a platform
51
+ limitation, not a bug here.
52
  """
53
 
54
  from __future__ import annotations
 
61
 
62
  # The Bit Trading mark, from the design.
63
  BIT_MARK = (
64
+ '<svg viewBox="0 0 100 100" role="img" class="bit-mark" '
65
  'aria-label="The Bit Trading Company" style="flex:0 0 auto;display:block">'
66
  '<path fill="var(--accent-amber)" fill-rule="evenodd" '
67
  'd="M0 0H100V100H0Z M50 10H90V90H50Z M22 42H38V58H22Z M62 42H78V58H62Z">'
 
69
  )
70
 
71
 
72
+ def _item(item: dict, active: bool, emit) -> str:
73
+ """One nav row: a link when live, a button when it opens something.
74
+
75
+ Rendered once for both rail widths. `justify-content` and the label's
76
+ `display` are CSS, keyed on `body[data-sidebar]`, so collapsing does not
77
+ need a re-render.
78
+ """
79
  name = item.get("name", "")
80
  status = item.get("status", "soon")
81
 
 
88
  else:
89
  mark = "transparent"
90
 
 
 
 
91
  style = (
92
+ f"gap:10px;padding:6px 12px;color:{colour};background:{background};"
93
+ f"border-left:2px solid {mark}"
 
 
 
94
  )
95
  inner = (
96
  f'{icon(item.get("icon") or "cube", "14px")}'
97
+ f'<span class="bit-nav-label">{e(name)}</span>'
 
 
 
98
  )
99
+ # Collapsed, the label is hidden, so the tooltip is the only affordance
100
+ # left -- and it is the same element either way, so it is always set.
101
+ title = name if status == "live" else f"{name} — coming soon"
102
 
103
  if status == "live" and item.get("url"):
104
  current = ' aria-current="page"' if active else ""
105
  return (f'<a href="{a(item["url"])}" target="_blank" '
106
  f'rel="noopener noreferrer" title="{a(title)}"{current} '
107
+ f'class="bit-nav-item bit-hover-nav" style="{style}">{inner}</a>')
108
 
109
  action = ("contact:open" if item.get("action") == "contact"
110
  else f"soon:{name}")
111
  return (f'<button type="button" data-bit="{a(emit(action))}" '
112
+ f'title="{a(title)}" class="bit-nav-item bit-hover-nav" '
113
+ f'style="{style}">{inner}</button>')
114
 
115
 
116
  def render(navigation: dict, active: str = "", collapsed: bool = True,
117
  emit=None) -> str:
118
  """The sidebar markup.
119
 
120
+ `collapsed` sets only the toggle button's initial ARIA state; the visible
121
+ width is CSS. It is kept as a parameter so a Space with a different default
122
+ can express it, and so the rendered ARIA matches the CSS default.
123
+
124
  `emit` builds a `data-bit` attribute value for an action; the Space passes
125
  its own bridge's `emit` so every action stays on that Space's allow-list.
126
  """
127
  if emit is None:
128
  from .bridge import emit as emit # noqa: PLW0127
129
 
 
 
 
 
 
 
 
 
 
130
  brand = navigation.get("brand") or {}
131
  brand_href = brand.get("href") or ""
132
+ mark = BIT_MARK
133
  if brand_href:
134
  mark = (f'<a href="{a(brand_href)}" target="_blank" '
135
  f'rel="noopener noreferrer" title="{a(brand.get("title") or "")}" '
136
+ f'class="bit-brand-mark">{mark}</a>')
 
137
 
138
  groups = []
139
  for group in navigation.get("groups") or ():
140
  rows = "".join(
141
+ _item(item, item.get("name") == active, emit)
142
  for item in group.get("items") or ()
143
  )
144
  groups.append(
145
+ f'<div class="bit-nav-group">{e(group["label"])}</div>{rows}'
 
 
 
 
146
  )
147
 
148
  assistant = navigation.get("assistant") or {}
 
149
  ase = ""
150
  if assistant.get("name"):
151
  ase = (
152
  f'<div style="border-top:1px solid var(--border-default);'
153
  f'background:var(--bg-raised)">'
154
  f'<button type="button" data-bit="{a(emit("soon:" + assistant["name"]))}" '
155
+ f'title="{a(assistant["name"])} — coming soon" '
156
+ f'class="bit-ase bit-hover-ase">'
157
+ f'<span aria-hidden="true" class="bit-ase-mark"></span>'
158
+ f'<span class="bit-brand-text">'
 
 
 
 
 
 
159
  f'<span style="font-family:var(--font-styrene);font-size:var(--text-md);'
160
  f'text-transform:uppercase;letter-spacing:var(--tracking-wide);'
161
  f'color:var(--text-secondary);white-space:nowrap;margin-top:2px">'
 
167
  )
168
 
169
  return f"""
170
+ <aside class="bit-sidebar bit-scroll" aria-label="Modules">
171
+ <div class="bit-brand">
 
 
 
 
 
 
 
 
172
  {mark}
173
+ <div class="bit-brand-text">
 
174
  <span style="font-family:var(--font-styrene);font-size:var(--text-2xs);
175
  text-transform:uppercase;letter-spacing:var(--tracking-wide);
176
  color:var(--text-tertiary);white-space:nowrap">{e(brand.get('label') or '')}</span>
 
179
  font-size:var(--text-lg);line-height:var(--leading-tight);
180
  white-space:nowrap">{e(brand.get('sub') or '')}</span>
181
  </div>
182
+ <button type="button" data-bit-sidebar-toggle="1" class="bit-sidebar-toggle"
183
+ title="{'Expand' if collapsed else 'Collapse'} sidebar"
184
+ aria-label="Toggle sidebar" aria-controls="bit-sidebar"
185
+ aria-expanded="{'false' if collapsed else 'true'}">
 
 
 
 
186
  {icon('chevronRight', '12px')}
187
  </button>
188
  </div>
189
 
190
+ <nav class="bit-nav">{''.join(groups)}</nav>
 
 
 
191
 
192
  <div style="margin-top:auto">
193
  {ase}
194
+ <div class="bit-sidebar-foot">
 
195
  <div class="pixel-text" style="color:var(--text-tertiary)">{e(_build_line())}</div>
196
  </div>
197
  </div>
vendor/bit-ui/bit_ui/theme.py CHANGED
@@ -232,6 +232,109 @@ CHROME_CSS = """
232
  .bit-sidebar { transition: none !important; }
233
  }
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  /* ---------------- focus ---------------- */
236
  .bit-app :is(button, a, input, textarea, [role="switch"],
237
  [role="checkbox"]):focus-visible {
 
232
  .bit-sidebar { transition: none !important; }
233
  }
234
 
235
+ /* ------------------------------------------------------------------ *
236
+ * Sidebar: collapse is CSS, driven by body[data-sidebar].
237
+ *
238
+ * The rail renders once in both states and this decides which is visible,
239
+ * so collapsing costs no server round trip. The attribute is on <body>
240
+ * because everything inside the gr.HTML sink is replaced on the next
241
+ * re-render, which would snap the rail back.
242
+ *
243
+ * Absent attribute == collapsed, so the first paint needs no JavaScript.
244
+ * ------------------------------------------------------------------ */
245
+
246
+ .bit-sidebar {
247
+ width: 56px; flex: 0 0 56px;
248
+ border-right: 1px solid var(--border-default);
249
+ background: var(--bg-panel);
250
+ /* Sticky needs flex-start: a stretched item is already full height and has
251
+ no room to stick within, so the property would silently do nothing. */
252
+ position: sticky; top: 0; align-self: flex-start;
253
+ max-height: 100vh; overflow-y: auto; overflow-x: hidden;
254
+ display: flex; flex-direction: column;
255
+ font-family: var(--font-body); font-size: var(--text-base);
256
+ color: var(--text-primary);
257
+ transition: width 0.18s ease, flex-basis 0.18s ease;
258
+ z-index: 30;
259
+ }
260
+ body[data-sidebar="expanded"] .bit-sidebar { width: 216px; flex-basis: 216px; }
261
+
262
+ .bit-brand {
263
+ display: flex; flex-direction: column; align-items: center; gap: 10px;
264
+ padding: 12px; border-bottom: 1px solid var(--border-default);
265
+ min-height: 49px;
266
+ }
267
+ body[data-sidebar="expanded"] .bit-brand { flex-direction: row; }
268
+
269
+ .bit-brand-mark { display: flex; align-items: center; justify-content: center;
270
+ text-decoration: none; }
271
+ .bit-mark { width: 22px; height: 22px; flex: 0 0 auto; display: block;
272
+ transition: width 0.18s ease, height 0.18s ease; }
273
+ body[data-sidebar="expanded"] .bit-mark { width: 32px; height: 32px; }
274
+
275
+ /* Labels stay in the DOM when collapsed -- screen readers and the tooltip
276
+ both need them; only the visual box is removed. */
277
+ .bit-brand-text { display: none; flex-direction: column; gap: 1px;
278
+ min-width: 0; overflow: hidden; }
279
+ body[data-sidebar="expanded"] .bit-brand-text { display: flex; }
280
+
281
+ .bit-sidebar-toggle {
282
+ margin-left: 0; background: transparent; border: none;
283
+ color: var(--text-tertiary); width: 22px; height: 22px; display: flex;
284
+ align-items: center; justify-content: center; cursor: pointer;
285
+ flex: 0 0 auto; transition: transform 0.18s ease, color 0.18s ease;
286
+ transform: rotate(0deg);
287
+ }
288
+ body[data-sidebar="expanded"] .bit-sidebar-toggle {
289
+ margin-left: auto; transform: rotate(180deg);
290
+ }
291
+ .bit-sidebar-toggle:hover { color: var(--text-primary); }
292
+
293
+ .bit-nav { padding: 8px 0; display: flex; flex-direction: column; gap: 2px; }
294
+
295
+ .bit-nav-group {
296
+ display: none; padding: 10px 12px 4px; font-family: var(--font-styrene);
297
+ font-size: var(--text-2xs); letter-spacing: var(--tracking-wider);
298
+ text-transform: uppercase; color: var(--text-tertiary);
299
+ white-space: nowrap; overflow: hidden;
300
+ }
301
+ body[data-sidebar="expanded"] .bit-nav-group { display: block; }
302
+
303
+ .bit-nav-item {
304
+ display: flex; align-items: center; justify-content: center;
305
+ text-decoration: none; white-space: nowrap; overflow: hidden; width: 100%;
306
+ border-top: none; border-right: none; border-bottom: none;
307
+ font: inherit; text-align: left; cursor: pointer;
308
+ }
309
+ body[data-sidebar="expanded"] .bit-nav-item { justify-content: flex-start; }
310
+
311
+ .bit-nav-label {
312
+ display: none; font-family: var(--font-styrene); font-size: var(--text-sm);
313
+ text-transform: uppercase; letter-spacing: var(--tracking-wide);
314
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
315
+ }
316
+ body[data-sidebar="expanded"] .bit-nav-label { display: block; }
317
+
318
+ .bit-ase {
319
+ width: 100%; display: flex; flex-direction: column; align-items: center;
320
+ gap: 10px; padding: 12px; background: transparent; border: none;
321
+ border-left: 2px solid var(--accent-amber-dim); cursor: pointer;
322
+ text-align: left;
323
+ }
324
+ body[data-sidebar="expanded"] .bit-ase { flex-direction: row; }
325
+ .bit-ase-mark {
326
+ flex: 0 0 auto; display: block; width: 16px; height: 16px;
327
+ background: currentColor; color: var(--text-secondary); border-radius: 50%;
328
+ opacity: 0.8; transition: width 0.18s ease, height 0.18s ease;
329
+ }
330
+ body[data-sidebar="expanded"] .bit-ase-mark { width: 32px; height: 32px; }
331
+
332
+ .bit-sidebar-foot {
333
+ display: none; padding: 10px 12px;
334
+ border-top: 1px solid var(--border-default);
335
+ }
336
+ body[data-sidebar="expanded"] .bit-sidebar-foot { display: block; }
337
+
338
  /* ---------------- focus ---------------- */
339
  .bit-app :is(button, a, input, textarea, [role="switch"],
340
  [role="checkbox"]):focus-visible {