Spaces:
Running
Running
CI deploy local
Browse files- .pytest_cache/v/cache/lastfailed +2 -1
- .pytest_cache/v/cache/nodeids +2 -0
- app.py +0 -2
- src/atlas.py +3 -4
- src/ui/chrome.py +103 -2
- src/ui/shell.py +78 -86
- tests/test_ui.py +24 -6
- vendor/bit-ui/bit_ui/bridge.py +47 -1
- vendor/bit-ui/bit_ui/sidebar.py +67 -80
- vendor/bit-ui/bit_ui/theme.py +103 -0
.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
|
| 398 |
-
#
|
| 399 |
-
#
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
"
|
| 477 |
-
"var(--text-primary)"))
|
| 478 |
flags = row.get("red_flags") or []
|
| 479 |
if len(flags):
|
| 480 |
-
|
| 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 |
-
"
|
| 489 |
return "".join(
|
| 490 |
-
f'<span title="{a(tip)}"
|
| 491 |
-
|
| 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 |
-
|
| 507 |
-
if series:
|
| 508 |
from ..atlas import growth
|
| 509 |
change = growth(series)
|
| 510 |
-
|
| 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'
|
| 515 |
-
f'stroke
|
| 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
|
| 520 |
-
f'
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
|
|
|
| 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="
|
| 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
|
| 554 |
-
f'
|
| 555 |
-
f'
|
| 556 |
-
f'
|
| 557 |
-
f'
|
| 558 |
-
f'
|
|
|
|
|
|
|
|
|
|
| 559 |
f'{e(labels.get(row.get("task"), row.get("task")))}</span></div>'
|
| 560 |
|
| 561 |
-
f'<div
|
| 562 |
-
f'
|
| 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
|
| 570 |
-
f'
|
| 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
|
| 576 |
-
f'
|
| 577 |
-
f'
|
| 578 |
|
| 579 |
-
f'<div
|
| 580 |
-
f'
|
| 581 |
-
f'
|
| 582 |
-
|
| 583 |
-
f'
|
| 584 |
-
f'
|
| 585 |
-
|
| 586 |
-
f'<
|
| 587 |
-
f'
|
| 588 |
-
|
| 589 |
-
f'
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 365 |
-
|
| 366 |
-
assert
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
##
|
| 8 |
|
| 9 |
-
The
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 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,
|
| 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"
|
| 66 |
-
f"
|
| 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
|
| 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
|
| 79 |
-
|
| 80 |
-
|
| 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}
|
| 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
|
| 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'
|
| 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,
|
| 127 |
for item in group.get("items") or ()
|
| 128 |
)
|
| 129 |
groups.append(
|
| 130 |
-
f'<div
|
| 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"
|
| 146 |
-
f'
|
| 147 |
-
f'
|
| 148 |
-
f'
|
| 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"
|
| 167 |
-
|
| 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
|
| 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="
|
| 188 |
-
title="{
|
| 189 |
-
aria-
|
| 190 |
-
|
| 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
|
| 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
|
| 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 {
|