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 {
363363 default badge border vanishes against a hovered inset row. */
364364 border-color: var(--fb-fg-muted);
365365 }
366+.badge-default {
367+ color: var(--fb-accent);
368+ border-color: var(--fb-accent);
369+}
366370
367371 /* Inline search row: input grows, button sits to its right with margin. */
368372 .search-row {
@@ -860,9 +864,7 @@ pre.code {
860864
861865 /* ---- Repo views ---- */
862866 .repo-head {
863- border-bottom: 1px solid var(--fb-border);
864- padding-bottom: 0.75rem;
865- margin-bottom: 1rem;
867+ margin-bottom: 1.25rem;
866868 }
867869 .repo-slug {
868870 display: flex;
@@ -885,6 +887,10 @@ pre.code {
885887 font-size: 0.7rem;
886888 padding: 0.12rem 0.55rem;
887889 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;
888894 }
889895 /* Tab strips (repo sub-nav, explore sub-nav): active-pill style. */
890896 .repo-tabs,
@@ -1131,13 +1137,13 @@ table.blob tr:target {
11311137 background: var(--fb-diff-add-bg);
11321138 }
11331139
1134-/* ---- Commit list ---- */
1135-.commit-list {
1140+/* ---- Entry rows (commits, branches, tags) ---- */
1141+.entry-list {
11361142 list-style: none;
11371143 margin: 0;
11381144 padding: 0;
11391145 }
1140-.commit-row {
1146+.entry-row {
11411147 display: flex;
11421148 align-items: center;
11431149 justify-content: space-between;
@@ -1145,13 +1151,13 @@ table.blob tr:target {
11451151 padding: 0.7rem 1rem;
11461152 border-bottom: 1px solid var(--fb-border);
11471153 }
1148-.commit-row:last-child {
1154+.entry-row:last-child {
11491155 border-bottom: none;
11501156 }
1151-.commit-row-body {
1157+.entry-row-body {
11521158 min-width: 0;
11531159 }
1154-.commit-row-msg {
1160+.entry-row-title {
11551161 display: block;
11561162 font-weight: 600;
11571163 color: var(--fb-fg);
@@ -1159,16 +1165,20 @@ table.blob tr:target {
11591165 text-overflow: ellipsis;
11601166 white-space: nowrap;
11611167 }
1162-.commit-row-msg:hover {
1168+.entry-row-title:hover {
11631169 color: var(--fb-accent);
11641170 }
1165-.commit-row-meta {
1171+.entry-row-meta {
11661172 display: flex;
11671173 gap: 0.4rem;
1174+ flex-wrap: wrap;
11681175 margin-top: 0.15rem;
11691176 font-size: 0.85em;
11701177 }
1171-.commit-row-actions {
1178+.entry-row-meta .ahead {
1179+ color: var(--fb-success);
1180+}
1181+.entry-row-actions {
11721182 display: flex;
11731183 align-items: center;
11741184 gap: 0.5rem;
crates/web/src/repo.rs +45 −24
@@ -631,21 +631,32 @@ async fn branches_view(
631631 ) -> AppResult<Response> {
632632 let branches = git_read(state, &ctx.repo.id, git::Repo::branches).await?;
633633 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();
634636 let body = html! {
635637 (repo_header(state, &ctx, Tab::Branches, &ctx.repo.default_branch, &branch_list))
636638 h2 { "Branches" }
637- table class="list" {
638- thead { tr { th { "Branch" } th { "Ahead" } th { "Behind" } th { "Updated" } } }
639- tbody {
640- @for b in &branches {
641- tr {
642- td {
643- a href={ "/" (ctx.owner.username) "/" (ctx.repo.path) "/-/tree/" (b.name) } { (b.name) }
644- @if b.is_default { " " span class="badge" { "default" } }
639+ ul class="entry-list card" {
640+ @for b in &branches {
641+ li class="entry-row" {
642+ div class="entry-row-body" {
643+ a class="entry-row-title" href=(format!("{base}/-/tree/{}", b.name)) { (b.name) }
644+ div class="entry-row-meta muted" {
645+ @if b.ahead > 0 {
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())
645659 }
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)) }
649660 }
650661 }
651662 }
@@ -667,17 +678,27 @@ async fn tags_view(
667678 ) -> AppResult<Response> {
668679 let tags = git_read(state, &ctx.repo.id, git::Repo::tags).await?;
669680 let branches = branch_names(state, &ctx.repo.id).await;
681+ let base = format!("/{}/{}", ctx.owner.username, ctx.repo.path);
670682 let body = html! {
671683 (repo_header(state, &ctx, Tab::Tags, &ctx.repo.default_branch, &branches))
672684 h2 { "Tags" }
673- table class="list" {
674- thead { tr { th { "Tag" } th { "Commit" } th { "Kind" } } }
675- tbody {
685+ @if tags.is_empty() {
686+ div class="card" { p class="muted" { "No tags." } }
687+ } @else {
688+ ul class="entry-list card" {
676689 @for t in &tags {
677- tr {
678- td { a href={ "/" (ctx.owner.username) "/" (ctx.repo.path) "/-/tree/" (t.name) } { (t.name) } }
679- td class="mono" { (t.oid.short()) }
680- td { (if t.annotated { "annotated" } else { "lightweight" }) }
690+ li class="entry-row" {
691+ div class="entry-row-body" {
692+ a class="entry-row-title" href=(format!("{base}/-/tree/{}", t.name)) { (t.name) }
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+ }
681702 }
682703 }
683704 }
@@ -742,23 +763,23 @@ async fn commits_view(
742763 let body = html! {
743764 (repo_header(state, &ctx, Tab::Commits, &rev_name, &branches))
744765 h2 { "Commits" }
745- ul class="commit-list card" {
766+ ul class="entry-list card" {
746767 @for (commit, sig) in commits.iter().zip(states.iter()) {
747768 @let commit_url = format!("{base}/-/commit/{}", commit.oid);
748769 @let signer = match sig {
749770 git::SignatureState::Verified { user_id, .. } => signers.get(user_id).map(String::as_str),
750771 _ => None,
751772 };
752- li class="commit-row" {
753- div class="commit-row-body" {
754- a class="commit-row-msg" href=(commit_url) { (commit.summary) }
755- div class="commit-row-meta muted" {
773+ li class="entry-row" {
774+ div class="entry-row-body" {
775+ a class="entry-row-title" href=(commit_url) { (commit.summary) }
776+ div class="entry-row-meta muted" {
756777 span { (commit.author.name) }
757778 span { "·" }
758779 span { (crate::activity::fmt_relative(commit.author.time_ms, now)) }
759780 }
760781 }
761- div class="commit-row-actions" {
782+ div class="entry-row-actions" {
762783 (commit_sig_cell(sig, signer, commit.author.time_ms))
763784 a class="mono commit-sha-pill" href=(commit_url) { (commit.oid.short()) }
764785 button class="icon-btn commit-copy" type="button"