fabrica

hanna/fabrica

feat(web): drop repo-header rule, unify commit/branch/tag row styling

cf2f7fd · hanna committed on 2026-07-25

Remove the divider line under the repo header (the active tab already marks
the section) and restyle the branches and tags views as card-based row lists
sharing the commit list's .entry-* styles (title, meta, sha pill). Also
properly centre the private pill against the slug text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
2 files changed · +67 −36UnifiedSplit
assets/base.css +22 −12
@@ -363,6 +363,10 @@ select:focus {
363 default badge border vanishes against a hovered inset row. */363 default badge border vanishes against a hovered inset row. */
364 border-color: var(--fb-fg-muted);364 border-color: var(--fb-fg-muted);
365}365}
366.badge-default {
367 color: var(--fb-accent);
368 border-color: var(--fb-accent);
369}
366370
367/* Inline search row: input grows, button sits to its right with margin. */371/* Inline search row: input grows, button sits to its right with margin. */
368.search-row {372.search-row {
@@ -860,9 +864,7 @@ pre.code {
860864
861/* ---- Repo views ---- */865/* ---- Repo views ---- */
862.repo-head {866.repo-head {
863 border-bottom: 1px solid var(--fb-border);867 margin-bottom: 1.25rem;
864 padding-bottom: 0.75rem;
865 margin-bottom: 1rem;
866}868}
867.repo-slug {869.repo-slug {
868 display: flex;870 display: flex;
@@ -885,6 +887,10 @@ pre.code {
885 font-size: 0.7rem;887 font-size: 0.7rem;
886 padding: 0.12rem 0.55rem;888 padding: 0.12rem 0.55rem;
887 align-self: center;889 align-self: center;
890 /* Nudge down: flex-centring sits on the em-box centre, which is above the
891 optical centre of the lowercase slug text. */
892 position: relative;
893 top: 0.12rem;
888}894}
889/* Tab strips (repo sub-nav, explore sub-nav): active-pill style. */895/* Tab strips (repo sub-nav, explore sub-nav): active-pill style. */
890.repo-tabs,896.repo-tabs,
@@ -1131,13 +1137,13 @@ table.blob tr:target {
1131 background: var(--fb-diff-add-bg);1137 background: var(--fb-diff-add-bg);
1132}1138}
11331139
1134/* ---- Commit list ---- */1140/* ---- Entry rows (commits, branches, tags) ---- */
1135.commit-list {1141.entry-list {
1136 list-style: none;1142 list-style: none;
1137 margin: 0;1143 margin: 0;
1138 padding: 0;1144 padding: 0;
1139}1145}
1140.commit-row {1146.entry-row {
1141 display: flex;1147 display: flex;
1142 align-items: center;1148 align-items: center;
1143 justify-content: space-between;1149 justify-content: space-between;
@@ -1145,13 +1151,13 @@ table.blob tr:target {
1145 padding: 0.7rem 1rem;1151 padding: 0.7rem 1rem;
1146 border-bottom: 1px solid var(--fb-border);1152 border-bottom: 1px solid var(--fb-border);
1147}1153}
1148.commit-row:last-child {1154.entry-row:last-child {
1149 border-bottom: none;1155 border-bottom: none;
1150}1156}
1151.commit-row-body {1157.entry-row-body {
1152 min-width: 0;1158 min-width: 0;
1153}1159}
1154.commit-row-msg {1160.entry-row-title {
1155 display: block;1161 display: block;
1156 font-weight: 600;1162 font-weight: 600;
1157 color: var(--fb-fg);1163 color: var(--fb-fg);
@@ -1159,16 +1165,20 @@ table.blob tr:target {
1159 text-overflow: ellipsis;1165 text-overflow: ellipsis;
1160 white-space: nowrap;1166 white-space: nowrap;
1161}1167}
1162.commit-row-msg:hover {1168.entry-row-title:hover {
1163 color: var(--fb-accent);1169 color: var(--fb-accent);
1164}1170}
1165.commit-row-meta {1171.entry-row-meta {
1166 display: flex;1172 display: flex;
1167 gap: 0.4rem;1173 gap: 0.4rem;
1174 flex-wrap: wrap;
1168 margin-top: 0.15rem;1175 margin-top: 0.15rem;
1169 font-size: 0.85em;1176 font-size: 0.85em;
1170}1177}
1171.commit-row-actions {1178.entry-row-meta .ahead {
1179 color: var(--fb-success);
1180}
1181.entry-row-actions {
1172 display: flex;1182 display: flex;
1173 align-items: center;1183 align-items: center;
1174 gap: 0.5rem;1184 gap: 0.5rem;
crates/web/src/repo.rs +45 −24
@@ -631,21 +631,32 @@ async fn branches_view(
631) -> AppResult<Response> {631) -> AppResult<Response> {
632 let branches = git_read(state, &ctx.repo.id, git::Repo::branches).await?;632 let branches = git_read(state, &ctx.repo.id, git::Repo::branches).await?;
633 let branch_list: Vec<String> = branches.iter().map(|b| b.name.clone()).collect();633 let branch_list: Vec<String> = branches.iter().map(|b| b.name.clone()).collect();
634 let base = format!("/{}/{}", ctx.owner.username, ctx.repo.path);
635 let now = crate::now_ms();
634 let body = html! {636 let body = html! {
635 (repo_header(state, &ctx, Tab::Branches, &ctx.repo.default_branch, &branch_list))637 (repo_header(state, &ctx, Tab::Branches, &ctx.repo.default_branch, &branch_list))
636 h2 { "Branches" }638 h2 { "Branches" }
637 table class="list" {639 ul class="entry-list card" {
638 thead { tr { th { "Branch" } th { "Ahead" } th { "Behind" } th { "Updated" } } }640 @for b in &branches {
639 tbody {641 li class="entry-row" {
640 @for b in &branches {642 div class="entry-row-body" {
641 tr {643 a class="entry-row-title" href=(format!("{base}/-/tree/{}", b.name)) { (b.name) }
642 td {644 div class="entry-row-meta muted" {
643 a href={ "/" (ctx.owner.username) "/" (ctx.repo.path) "/-/tree/" (b.name) } { (b.name) }645 @if b.ahead > 0 {
644 @if b.is_default { " " span class="badge" { "default" } }646 span class="ahead" { "↑" (b.ahead) }
647 }
648 @if b.behind > 0 {
649 span class="behind" { "↓" (b.behind) }
650 }
651 span { "Updated " (crate::activity::fmt_relative(b.tip.author.time_ms, now)) }
652 span { "by " (b.tip.author.name) }
653 }
654 }
655 div class="entry-row-actions" {
656 @if b.is_default { span class="badge badge-default" { "default" } }
657 a class="mono commit-sha-pill" href=(format!("{base}/-/commit/{}", b.oid)) {
658 (b.oid.short())
645 }659 }
646 td { @if b.ahead > 0 { span style="color:var(--fb-success)" { (b.ahead) } } @else { "0" } }
647 td { (b.behind) }
648 td class="muted" { (b.tip.author.name) " · " (fmt_date(b.tip.author.time_ms)) }
649 }660 }
650 }661 }
651 }662 }
@@ -667,17 +678,27 @@ async fn tags_view(
667) -> AppResult<Response> {678) -> AppResult<Response> {
668 let tags = git_read(state, &ctx.repo.id, git::Repo::tags).await?;679 let tags = git_read(state, &ctx.repo.id, git::Repo::tags).await?;
669 let branches = branch_names(state, &ctx.repo.id).await;680 let branches = branch_names(state, &ctx.repo.id).await;
681 let base = format!("/{}/{}", ctx.owner.username, ctx.repo.path);
670 let body = html! {682 let body = html! {
671 (repo_header(state, &ctx, Tab::Tags, &ctx.repo.default_branch, &branches))683 (repo_header(state, &ctx, Tab::Tags, &ctx.repo.default_branch, &branches))
672 h2 { "Tags" }684 h2 { "Tags" }
673 table class="list" {685 @if tags.is_empty() {
674 thead { tr { th { "Tag" } th { "Commit" } th { "Kind" } } }686 div class="card" { p class="muted" { "No tags." } }
675 tbody {687 } @else {
688 ul class="entry-list card" {
676 @for t in &tags {689 @for t in &tags {
677 tr {690 li class="entry-row" {
678 td { a href={ "/" (ctx.owner.username) "/" (ctx.repo.path) "/-/tree/" (t.name) } { (t.name) } }691 div class="entry-row-body" {
679 td class="mono" { (t.oid.short()) }692 a class="entry-row-title" href=(format!("{base}/-/tree/{}", t.name)) { (t.name) }
680 td { (if t.annotated { "annotated" } else { "lightweight" }) }693 div class="entry-row-meta muted" {
694 span { (if t.annotated { "annotated" } else { "lightweight" }) }
695 }
696 }
697 div class="entry-row-actions" {
698 a class="mono commit-sha-pill" href=(format!("{base}/-/commit/{}", t.oid)) {
699 (t.oid.short())
700 }
701 }
681 }702 }
682 }703 }
683 }704 }
@@ -742,23 +763,23 @@ async fn commits_view(
742 let body = html! {763 let body = html! {
743 (repo_header(state, &ctx, Tab::Commits, &rev_name, &branches))764 (repo_header(state, &ctx, Tab::Commits, &rev_name, &branches))
744 h2 { "Commits" }765 h2 { "Commits" }
745 ul class="commit-list card" {766 ul class="entry-list card" {
746 @for (commit, sig) in commits.iter().zip(states.iter()) {767 @for (commit, sig) in commits.iter().zip(states.iter()) {
747 @let commit_url = format!("{base}/-/commit/{}", commit.oid);768 @let commit_url = format!("{base}/-/commit/{}", commit.oid);
748 @let signer = match sig {769 @let signer = match sig {
749 git::SignatureState::Verified { user_id, .. } => signers.get(user_id).map(String::as_str),770 git::SignatureState::Verified { user_id, .. } => signers.get(user_id).map(String::as_str),
750 _ => None,771 _ => None,
751 };772 };
752 li class="commit-row" {773 li class="entry-row" {
753 div class="commit-row-body" {774 div class="entry-row-body" {
754 a class="commit-row-msg" href=(commit_url) { (commit.summary) }775 a class="entry-row-title" href=(commit_url) { (commit.summary) }
755 div class="commit-row-meta muted" {776 div class="entry-row-meta muted" {
756 span { (commit.author.name) }777 span { (commit.author.name) }
757 span { "·" }778 span { "·" }
758 span { (crate::activity::fmt_relative(commit.author.time_ms, now)) }779 span { (crate::activity::fmt_relative(commit.author.time_ms, now)) }
759 }780 }
760 }781 }
761 div class="commit-row-actions" {782 div class="entry-row-actions" {
762 (commit_sig_cell(sig, signer, commit.author.time_ms))783 (commit_sig_cell(sig, signer, commit.author.time_ms))
763 a class="mono commit-sha-pill" href=(commit_url) { (commit.oid.short()) }784 a class="mono commit-sha-pill" href=(commit_url) { (commit.oid.short()) }
764 button class="icon-btn commit-copy" type="button"785 button class="icon-btn commit-copy" type="button"