fabrica

hanna/fabrica

fix(web): collapsible language bar above the tree; key badge order

6100f0d · hanna committed on 2026-07-25

Move the language bar above the file tree (under the repo search) and make it a
collapsible <details> — the stripe is always shown, the named legend expands on
click. Reset the legend list's default left padding. Reorder the key row so the
verified/unverified badge sits between the type badge and the key name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
3 files changed · +25 −13UnifiedSplit
assets/base.css +11 −3
@@ -601,9 +601,16 @@ body.drawer-open .drawer-backdrop {
601 flex: none;601 flex: none;
602}602}
603603
604/* Repository language breakdown bar. */604/* Repository language breakdown bar (a collapsible <details>). */
605.lang-bar-wrap {605.lang-bar-wrap {
606 margin: 1rem 0;606 margin: 0 0 1rem;
607}
608.lang-bar-summary {
609 list-style: none;
610 cursor: pointer;
611}
612.lang-bar-summary::-webkit-details-marker {
613 display: none;
607}614}
608.lang-bar {615.lang-bar {
609 display: flex;616 display: flex;
@@ -620,10 +627,11 @@ body.drawer-open .drawer-backdrop {
620}627}
621.lang-legend {628.lang-legend {
622 list-style: none;629 list-style: none;
630 padding: 0;
623 display: flex;631 display: flex;
624 flex-wrap: wrap;632 flex-wrap: wrap;
625 gap: 0.35rem 1.1rem;633 gap: 0.35rem 1.1rem;
626 margin-top: 0.75rem;634 margin: 0.75rem 0 0;
627}635}
628.lang-legend li {636.lang-legend li {
629 display: flex;637 display: flex;
crates/web/src/pages.rs +1 −1
@@ -1859,12 +1859,12 @@ fn keys_section(csrf: &str, keys: &[model::Key]) -> Markup {
1859 div class="entry-row-body" {1859 div class="entry-row-body" {
1860 span class="entry-row-title" {1860 span class="entry-row-title" {
1861 span class="badge" { (k.kind.as_str()) }1861 span class="badge" { (k.kind.as_str()) }
1862 " " (k.name.as_deref().unwrap_or("(unnamed)"))
1863 @if k.verified() {1862 @if k.verified() {
1864 " " span class="badge badge-verified" { "verified" }1863 " " span class="badge badge-verified" { "verified" }
1865 } @else {1864 } @else {
1866 " " span class="badge badge-unverified" { "unverified" }1865 " " span class="badge badge-unverified" { "unverified" }
1867 }1866 }
1867 " " (k.name.as_deref().unwrap_or("(unnamed)"))
1868 }1868 }
1869 div class="entry-row-meta muted mono" { (k.fingerprint) }1869 div class="entry-row-meta muted mono" { (k.fingerprint) }
1870 @if !k.verified() {1870 @if !k.verified() {
crates/web/src/repo.rs +13 −9
@@ -463,15 +463,19 @@ fn language_bar(langs: &[crate::languages::LangStat]) -> Markup {
463 let (head, tail) = langs.split_at(langs.len().min(MAX_SHOWN));463 let (head, tail) = langs.split_at(langs.len().min(MAX_SHOWN));
464 let other: f64 = tail.iter().map(|l| l.percent).sum();464 let other: f64 = tail.iter().map(|l| l.percent).sum();
465 let pct = |p: f64| format!("{p:.1}%");465 let pct = |p: f64| format!("{p:.1}%");
466 // A `<details>`: the bar is the always-visible summary; the legend expands on
467 // click and works without JavaScript.
466 html! {468 html! {
467 div class="card lang-bar-wrap" {469 details class="card lang-bar-wrap" {
468 div class="lang-bar" aria-hidden="true" {470 summary class="lang-bar-summary" title="Show languages" {
469 @for l in head {471 div class="lang-bar" aria-hidden="true" {
470 span class="lang-seg"472 @for l in head {
471 style=(format!("width:{:.2}%;background:{}", l.percent, l.color)) {}473 span class="lang-seg"
472 }474 style=(format!("width:{:.2}%;background:{}", l.percent, l.color)) {}
473 @if other > 0.0 {475 }
474 span class="lang-seg" style=(format!("width:{other:.2}%;background:#8b8b8b")) {}476 @if other > 0.0 {
477 span class="lang-seg" style=(format!("width:{other:.2}%;background:#8b8b8b")) {}
478 }
475 }479 }
476 }480 }
477 ul class="lang-legend" {481 ul class="lang-legend" {
@@ -535,10 +539,10 @@ async fn repo_home(
535 aria-label="Search this repository";539 aria-label="Search this repository";
536 button class="btn" type="submit" { (icon(Icon::Search)) span { "Search" } }540 button class="btn" type="submit" { (icon(Icon::Search)) span { "Search" } }
537 }541 }
538 (tree_table(&ctx.owner.username, &ctx.repo.path, &rev_name, "", &entries))
539 @if !langs.is_empty() {542 @if !langs.is_empty() {
540 (language_bar(&langs))543 (language_bar(&langs))
541 }544 }
545 (tree_table(&ctx.owner.username, &ctx.repo.path, &rev_name, "", &entries))
542 @if let Some(rendered) = readme {546 @if let Some(rendered) = readme {
543 div class="card markdown readme" { (rendered) }547 div class="card markdown readme" { (rendered) }
544 }548 }