fabrica

hanna/fabrica

feat(web): branch switcher, commits tab, signature popover, squarer pills

9755dfe · hanna committed on 2026-07-25

Make Commits a repo tab (Code · Commits · Branches · Tags) and turn the
branch button into a no-JS dropdown that switches branches (new cheap
git::Repo::branch_names). Add a GitHub-style hover/focus popover on verified
commit-list badges (signer, key fingerprint, verified date). On the commit
view, drop the redundant Verified pill and move the short id / author / date
to the right of the title. Square off badges and tab pills to match the
buttons, and align the profile social-link icons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
4 files changed · +292 −34UnifiedSplit
assets/base.css +126 −4
@@ -346,7 +346,7 @@ select:focus {
346346 gap: 0.25rem;
347347 padding: 0.05rem 0.45rem;
348348 font-size: 0.8em;
349- border-radius: 999px;
349+ border-radius: var(--fb-radius);
350350 border: 1px solid var(--fb-border);
351351 }
352352 .badge-verified {
@@ -619,7 +619,7 @@ select:focus {
619619 .profile-pronouns {
620620 padding: 0.05rem 0.5rem;
621621 border: 1px solid var(--fb-border);
622- border-radius: 999px;
622+ border-radius: var(--fb-radius);
623623 font-size: 0.82em;
624624 }
625625 .profile-bio {
@@ -653,8 +653,13 @@ select:focus {
653653 color: var(--fb-fg-muted);
654654 }
655655 .profile-links a {
656- color: var(--fb-accent);
656+ display: inline-flex;
657+ align-items: center;
658+ gap: 0.5rem;
657659 min-width: 0;
660+ color: var(--fb-accent);
661+}
662+.profile-links a span {
658663 overflow: hidden;
659664 text-overflow: ellipsis;
660665 white-space: nowrap;
@@ -892,7 +897,7 @@ pre.code {
892897 align-items: center;
893898 gap: 0.4rem;
894899 padding: 0.4rem 0.9rem;
895- border-radius: 999px;
900+ border-radius: var(--fb-radius);
896901 color: var(--fb-fg-muted);
897902 }
898903 .repo-tabs a:hover,
@@ -969,6 +974,63 @@ pre.code {
969974 font-family: var(--fb-font-mono);
970975 font-size: 0.8em;
971976 }
977+
978+/* Branch switcher dropdown. */
979+.branch-menu {
980+ position: relative;
981+}
982+.branch-menu > summary {
983+ list-style: none;
984+ cursor: pointer;
985+}
986+.branch-menu > summary::-webkit-details-marker {
987+ display: none;
988+}
989+.branch-menu > summary .icon:last-child {
990+ margin-left: 0.1rem;
991+}
992+.branch-pop {
993+ position: absolute;
994+ left: 0;
995+ top: calc(100% + 0.35rem);
996+ z-index: 30;
997+ width: 17rem;
998+ max-width: 88vw;
999+ max-height: 20rem;
1000+ overflow-y: auto;
1001+ padding: 0.35rem;
1002+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35);
1003+}
1004+.branch-pop-head {
1005+ padding: 0.3rem 0.5rem;
1006+ font-size: 0.78em;
1007+ font-weight: 600;
1008+}
1009+.branch-pop a {
1010+ display: block;
1011+ padding: 0.35rem 0.5rem;
1012+ border-radius: var(--fb-radius);
1013+ color: var(--fb-fg);
1014+ font-family: var(--fb-font-mono);
1015+ font-size: 0.88em;
1016+ overflow: hidden;
1017+ text-overflow: ellipsis;
1018+ white-space: nowrap;
1019+}
1020+.branch-pop a:hover {
1021+ background: var(--fb-bg-inset);
1022+ text-decoration: none;
1023+}
1024+.branch-pop a[aria-current="page"] {
1025+ color: var(--fb-accent);
1026+ font-weight: 600;
1027+}
1028+.branch-pop-all {
1029+ margin-top: 0.25rem;
1030+ border-top: 1px solid var(--fb-border);
1031+ color: var(--fb-accent) !important;
1032+ font-family: var(--fb-font-sans) !important;
1033+}
9721034 .breadcrumb {
9731035 margin-bottom: 0.75rem;
9741036 font-family: var(--fb-font-mono);
@@ -1126,6 +1188,50 @@ table.blob tr:target {
11261188 height: 1.75rem;
11271189 }
11281190
1191+/* Verified-signature hover popover in the commits list. */
1192+.sig-pop-wrap {
1193+ position: relative;
1194+ display: inline-flex;
1195+}
1196+.sig-pop {
1197+ position: absolute;
1198+ right: 0;
1199+ top: calc(100% + 0.35rem);
1200+ z-index: 40;
1201+ width: 21rem;
1202+ max-width: 88vw;
1203+ display: none;
1204+ text-align: left;
1205+ white-space: normal;
1206+ cursor: default;
1207+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35);
1208+}
1209+.sig-pop-wrap:hover .sig-pop,
1210+.sig-pop-wrap:focus-within .sig-pop {
1211+ display: block;
1212+}
1213+.sig-pop-title {
1214+ font-weight: 600;
1215+ margin-bottom: 0.6rem;
1216+}
1217+.sig-pop-signer {
1218+ margin-bottom: 0.6rem;
1219+}
1220+.sig-pop-fp {
1221+ font-size: 0.85em;
1222+ color: var(--fb-fg-muted);
1223+ margin-bottom: 0.4rem;
1224+}
1225+.sig-pop-fp .mono {
1226+ display: block;
1227+ margin-top: 0.15rem;
1228+ color: var(--fb-fg);
1229+ word-break: break-all;
1230+}
1231+.sig-pop-when {
1232+ font-size: 0.85em;
1233+}
1234+
11291235 /* Commit signature detail bar. */
11301236 .sig-detail {
11311237 display: flex;
@@ -1157,6 +1263,22 @@ table.blob tr:target {
11571263 .commit-meta {
11581264 margin-bottom: 1rem;
11591265 }
1266+.commit-title-row {
1267+ display: flex;
1268+ align-items: baseline;
1269+ justify-content: space-between;
1270+ gap: 1rem;
1271+ flex-wrap: wrap;
1272+}
1273+.commit-title {
1274+ margin: 0;
1275+ min-width: 0;
1276+}
1277+.commit-title-meta {
1278+ margin: 0;
1279+ font-size: 0.85em;
1280+ text-align: right;
1281+}
11601282 .commit-body {
11611283 margin: 0.5rem 0;
11621284 padding: 0.5rem 0.75rem;
crates/git/src/repo.rs +24 −0
@@ -211,6 +211,30 @@ impl Repo {
211211 Ok(out)
212212 }
213213
214+ /// The local branch names, default first then alphabetical. Cheaper than
215+ /// [`Self::branches`] — it skips the ahead/behind graph walk, for the branch
216+ /// switcher.
217+ ///
218+ /// # Errors
219+ ///
220+ /// Returns [`GitError::Libgit2`] on a libgit2 failure.
221+ pub fn branch_names(&self) -> Result<Vec<String>, GitError> {
222+ let default = self.head_branch()?;
223+ let mut names: Vec<String> = Vec::new();
224+ for branch in self.inner.branches(Some(BranchType::Local))? {
225+ let (branch, _) = branch?;
226+ if let Some(name) = branch.name()?.map(str::to_string) {
227+ names.push(name);
228+ }
229+ }
230+ names.sort_by(|a, b| {
231+ let da = default.as_deref() == Some(a.as_str());
232+ let db = default.as_deref() == Some(b.as_str());
233+ db.cmp(&da).then_with(|| a.cmp(b))
234+ });
235+ Ok(names)
236+ }
237+
214238 /// The tags, peeled to the commit each ultimately points at, annotated tags
215239 /// carrying their message and tagger.
216240 ///
crates/web/src/icons.rs +6 −0
@@ -17,6 +17,8 @@ pub enum Icon {
1717 Folder,
1818 File,
1919 GitBranch,
20+ History,
21+ ChevronDown,
2022 Search,
2123 Copy,
2224 Box,
@@ -55,6 +57,10 @@ impl Icon {
5557 Icon::GitBranch => {
5658 r#"<path d="M15 6a9 9 0 0 0-9 9V3"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/>"#
5759 }
60+ Icon::History => {
61+ r#"<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>"#
62+ }
63+ Icon::ChevronDown => r#"<path d="m6 9 6 6 6-6"/>"#,
5864 Icon::Search => r#"<path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/>"#,
5965 Icon::Copy => {
6066 r#"<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>"#
crates/web/src/repo.rs +136 −30
@@ -11,6 +11,7 @@
1111 //! resolved once through [`auth::access`]; no access to a private repo renders
1212 //! `404`, never `403`, so existence never leaks.
1313
14+use std::collections::HashMap;
1415 use std::path::Path as FsPath;
1516
1617 use axum::extract::{Path, Query, State};
@@ -58,6 +59,7 @@ const README_NAMES: &[&str] = &["README.md", "README", "readme.md", "README.txt"
5859 #[derive(Clone, Copy, PartialEq, Eq)]
5960 enum Tab {
6061 Code,
62+ Commits,
6163 Branches,
6264 Tags,
6365 Search,
@@ -92,6 +94,14 @@ where
9294 .map_err(AppError::from)
9395 }
9496
97+/// The local branch names for the switcher, empty on any error (an empty repo or
98+/// a failed read simply yields no dropdown).
99+async fn branch_names(state: &AppState, repo_id: &str) -> Vec<String> {
100+ git_read(state, repo_id, git::Repo::branch_names)
101+ .await
102+ .unwrap_or_default()
103+}
104+
95105 /// Resolve `(owner, path)` to a repo and check access, or `Ok(None)` if the owner
96106 /// or repo does not exist (so the caller can try a group listing).
97107 ///
@@ -267,7 +277,14 @@ async fn dispatch_view(
267277 "branches" => branches_view(state, viewer, jar, uri, ctx).await,
268278 "tags" => tags_view(state, viewer, jar, uri, ctx).await,
269279 "search" => {
270- let header = repo_header(state, &ctx, Tab::Search, &ctx.repo.default_branch);
280+ let branches = branch_names(state, &ctx.repo.id).await;
281+ let header = repo_header(
282+ state,
283+ &ctx,
284+ Tab::Search,
285+ &ctx.repo.default_branch,
286+ &branches,
287+ );
271288 let raw_query = dq.q.clone().unwrap_or_default();
272289 crate::search::in_repo(state, viewer, jar, uri, &ctx, &raw_query, header).await
273290 }
@@ -322,9 +339,10 @@ async fn repo_home(
322339 })
323340 .await?;
324341 let readme = load_readme(state, &ctx.repo.id, &rev, &entries).await;
342+ let branches = branch_names(state, &ctx.repo.id).await;
325343 let base = format!("/{}/{}", ctx.owner.username, ctx.repo.path);
326344 html! {
327- (repo_header(state, &ctx, Tab::Code, &rev_name))
345+ (repo_header(state, &ctx, Tab::Code, &rev_name, &branches))
328346 form class="search-row repo-codesearch" method="get" action=(format!("{base}/-/search")) {
329347 input type="search" name="q" placeholder="Search this repository…"
330348 aria-label="Search this repository";
@@ -373,7 +391,7 @@ async fn load_readme(
373391 /// The "empty repository" landing with clone instructions.
374392 fn empty_repo_body(state: &AppState, ctx: &RepoCtx) -> Markup {
375393 html! {
376- (repo_header(state, ctx, Tab::Code, &ctx.repo.default_branch))
394+ (repo_header(state, ctx, Tab::Code, &ctx.repo.default_branch, &[]))
377395 div class="card" {
378396 h2 { "This repository is empty" }
379397 p class="muted" { "Push a commit to get started:" }
@@ -408,8 +426,9 @@ async fn tree_view(
408426 })
409427 .await?;
410428
429+ let branches = branch_names(state, &ctx.repo.id).await;
411430 let body = html! {
412- (repo_header(state, &ctx, Tab::Code, &rev_name))
431+ (repo_header(state, &ctx, Tab::Code, &rev_name, &branches))
413432 (breadcrumb(&ctx.owner.username, &ctx.repo.path, &rev_name, &path))
414433 (tree_table(&ctx.owner.username, &ctx.repo.path, &rev_name, &path, &entries))
415434 };
@@ -462,8 +481,9 @@ async fn blob_view(
462481 attr_lang.as_deref(),
463482 source_view,
464483 );
484+ let branches = branch_names(state, &ctx.repo.id).await;
465485 let body = html! {
466- (repo_header(state, &ctx, Tab::Code, &rev_name))
486+ (repo_header(state, &ctx, Tab::Code, &rev_name, &branches))
467487 (breadcrumb(&ctx.owner.username, &ctx.repo.path, &rev_name, &path))
468488 (content_body)
469489 };
@@ -610,8 +630,9 @@ async fn branches_view(
610630 ctx: RepoCtx,
611631 ) -> AppResult<Response> {
612632 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();
613634 let body = html! {
614- (repo_header(state, &ctx, Tab::Branches, &ctx.repo.default_branch))
635+ (repo_header(state, &ctx, Tab::Branches, &ctx.repo.default_branch, &branch_list))
615636 h2 { "Branches" }
616637 table class="list" {
617638 thead { tr { th { "Branch" } th { "Ahead" } th { "Behind" } th { "Updated" } } }
@@ -645,8 +666,9 @@ async fn tags_view(
645666 ctx: RepoCtx,
646667 ) -> AppResult<Response> {
647668 let tags = git_read(state, &ctx.repo.id, git::Repo::tags).await?;
669+ let branches = branch_names(state, &ctx.repo.id).await;
648670 let body = html! {
649- (repo_header(state, &ctx, Tab::Tags, &ctx.repo.default_branch))
671+ (repo_header(state, &ctx, Tab::Tags, &ctx.repo.default_branch, &branches))
650672 h2 { "Tags" }
651673 table class="list" {
652674 thead { tr { th { "Tag" } th { "Commit" } th { "Kind" } } }
@@ -703,14 +725,30 @@ async fn commits_view(
703725 })
704726 .await?;
705727
728+ // Resolve the usernames of verified signers for the hover popovers.
729+ let mut signers: HashMap<String, String> = HashMap::new();
730+ for s in &states {
731+ if let git::SignatureState::Verified { user_id, .. } = s
732+ && !signers.contains_key(user_id)
733+ && let Ok(Some(u)) = state.store.user_by_id(user_id).await
734+ {
735+ signers.insert(user_id.clone(), u.username);
736+ }
737+ }
738+
706739 let base = format!("/{}/{}", ctx.owner.username, ctx.repo.path);
707740 let now = crate::now_ms();
741+ let branches = branch_names(state, &ctx.repo.id).await;
708742 let body = html! {
709- (repo_header(state, &ctx, Tab::Code, &rev_name))
743+ (repo_header(state, &ctx, Tab::Commits, &rev_name, &branches))
710744 h2 { "Commits" }
711745 ul class="commit-list card" {
712746 @for (commit, sig) in commits.iter().zip(states.iter()) {
713747 @let commit_url = format!("{base}/-/commit/{}", commit.oid);
748+ @let signer = match sig {
749+ git::SignatureState::Verified { user_id, .. } => signers.get(user_id).map(String::as_str),
750+ _ => None,
751+ };
714752 li class="commit-row" {
715753 div class="commit-row-body" {
716754 a class="commit-row-msg" href=(commit_url) { (commit.summary) }
@@ -721,7 +759,7 @@ async fn commits_view(
721759 }
722760 }
723761 div class="commit-row-actions" {
724- (signature_badge(sig))
762+ (commit_sig_cell(sig, signer, commit.author.time_ms))
725763 a class="mono commit-sha-pill" href=(commit_url) { (commit.oid.short()) }
726764 button class="icon-btn commit-copy" type="button"
727765 data-clipboard=(commit.oid.to_string()) aria-label="Copy commit id" {
@@ -774,10 +812,11 @@ async fn commit_view(
774812 (a + fa, d + fd)
775813 });
776814 let repo_base = format!("/{}/{}", ctx.owner.username, ctx.repo.path);
815+ let branches = branch_names(state, &ctx.repo.id).await;
777816
778817 let body = html! {
779- (repo_header(state, &ctx, Tab::Code, &ctx.repo.default_branch))
780- (commit_meta(&detail, &sig))
818+ (repo_header(state, &ctx, Tab::Code, &ctx.repo.default_branch, &branches))
819+ (commit_meta(&detail))
781820 (signature_detail(&sig, signer.as_deref()))
782821 div class="diff-toolbar" {
783822 span {
@@ -910,19 +949,22 @@ fn render_one_file(
910949 }
911950 }
912951
913-/// The commit metadata block: subject, signature badge, sha, author, and body.
914-fn commit_meta(detail: &git::CommitDetail, sig: &git::SignatureState) -> Markup {
952+/// The commit metadata block: the subject with the short id, author, and commit
953+/// date to its right, and the body below.
954+fn commit_meta(detail: &git::CommitDetail) -> Markup {
915955 let (subject, body) = detail
916956 .message
917957 .split_once("\n\n")
918958 .map_or((detail.message.as_str(), ""), |(s, b)| (s.trim(), b));
919959 html! {
920960 div class="commit-meta" {
921- h2 { (subject.lines().next().unwrap_or("")) " " (signature_badge(sig)) }
922- p class="muted" {
923- span class="mono" { (detail.oid.short()) }
924- " · " (detail.author.name)
925- " committed on " (fmt_date(detail.committer.time_ms))
961+ div class="commit-title-row" {
962+ h2 class="commit-title" { (subject.lines().next().unwrap_or("")) }
963+ p class="commit-title-meta muted" {
964+ span class="mono" { (detail.oid.short()) }
965+ " · " (detail.author.name)
966+ " committed on " (fmt_date(detail.committer.time_ms))
967+ }
926968 }
927969 @if !body.trim().is_empty() {
928970 pre class="commit-body" { (body.trim_end()) }
@@ -1240,8 +1282,14 @@ fn mastodon_link(handle: &str) -> Markup {
12401282 }
12411283
12421284 /// The repo sub-header: slug, description, and a tab strip sharing its line with
1243-/// the branch / commits / clone actions.
1244-fn repo_header(state: &AppState, ctx: &RepoCtx, active: Tab, rev: &str) -> Markup {
1285+/// the branch switcher and clone menu.
1286+fn repo_header(
1287+ state: &AppState,
1288+ ctx: &RepoCtx,
1289+ active: Tab,
1290+ rev: &str,
1291+ branches: &[String],
1292+) -> Markup {
12451293 let base = format!("/{}/{}", ctx.owner.username, ctx.repo.path);
12461294 let tab = |t: Tab, label: &str, glyph: Icon, href: String| {
12471295 html! {
@@ -1262,14 +1310,12 @@ fn repo_header(state: &AppState, ctx: &RepoCtx, active: Tab, rev: &str) -> Marku
12621310 div class="repo-nav" {
12631311 nav class="tabs repo-tabs" {
12641312 (tab(Tab::Code, "Code", Icon::File, base.clone()))
1313+ (tab(Tab::Commits, "Commits", Icon::History, format!("{base}/-/commits/{rev}")))
12651314 (tab(Tab::Branches, "Branches", Icon::GitBranch, format!("{base}/-/branches")))
12661315 (tab(Tab::Tags, "Tags", Icon::Box, format!("{base}/-/tags")))
12671316 }
12681317 div class="repo-actions" {
1269- a class="btn" href=(format!("{base}/-/branches")) {
1270- (icon(Icon::GitBranch)) span { (rev) }
1271- }
1272- a class="btn" href=(format!("{base}/-/commits/{rev}")) { "Commits" }
1318+ (branch_menu(&base, rev, branches))
12731319 (clone_menu(state, ctx))
12741320 }
12751321 }
@@ -1277,6 +1323,30 @@ fn repo_header(state: &AppState, ctx: &RepoCtx, active: Tab, rev: &str) -> Marku
12771323 }
12781324 }
12791325
1326+/// The branch switcher: a no-JS `details` menu linking to each branch's tree.
1327+/// Falls back to a plain link to the branches page when the list is empty.
1328+fn branch_menu(base: &str, rev: &str, branches: &[String]) -> Markup {
1329+ html! {
1330+ @if branches.is_empty() {
1331+ a class="btn" href=(format!("{base}/-/branches")) {
1332+ (icon(Icon::GitBranch)) span { (rev) }
1333+ }
1334+ } @else {
1335+ details class="branch-menu" {
1336+ summary class="btn" { (icon(Icon::GitBranch)) span { (rev) } (icon(Icon::ChevronDown)) }
1337+ div class="branch-pop card" {
1338+ div class="branch-pop-head muted" { "Switch branch" }
1339+ @for name in branches {
1340+ a href=(format!("{base}/-/tree/{name}"))
1341+ aria-current=[(name == rev).then_some("page")] { (name) }
1342+ }
1343+ a class="branch-pop-all" href=(format!("{base}/-/branches")) { "View all branches" }
1344+ }
1345+ }
1346+ }
1347+ }
1348+}
1349+
12801350 /// The Clone dropdown: a no-JS `details` menu offering the HTTPS and SSH URLs.
12811351 fn clone_menu(state: &AppState, ctx: &RepoCtx) -> Markup {
12821352 let https = clone_https(state, ctx);
@@ -1406,6 +1476,46 @@ fn entry_icon(kind: git::EntryKind) -> Icon {
14061476 }
14071477 }
14081478
1479+/// The uppercase label for a key kind.
1480+fn key_kind_label(kind: model::KeyKind) -> &'static str {
1481+ match kind {
1482+ model::KeyKind::Ssh => "SSH",
1483+ model::KeyKind::Gpg => "GPG",
1484+ }
1485+}
1486+
1487+/// A commit's signature cell for the commits list: a verified signature gets a
1488+/// hover/focus popover with the signer, key fingerprint, and verified date; any
1489+/// other state falls back to the plain badge.
1490+fn commit_sig_cell(state: &git::SignatureState, signer: Option<&str>, time_ms: i64) -> Markup {
1491+ match state {
1492+ git::SignatureState::Verified {
1493+ kind,
1494+ fingerprint,
1495+ user_id,
1496+ ..
1497+ } => html! {
1498+ span class="sig-pop-wrap" tabindex="0" {
1499+ (signature_badge(state))
1500+ div class="sig-pop card" {
1501+ div class="sig-pop-title" {
1502+ "This commit was signed with a verified signature."
1503+ }
1504+ div class="sig-pop-signer" {
1505+ "Signed by " strong { (signer.unwrap_or(user_id)) }
1506+ }
1507+ div class="sig-pop-fp" {
1508+ (key_kind_label(*kind)) " key fingerprint:"
1509+ span class="mono" { (fingerprint) }
1510+ }
1511+ div class="muted sig-pop-when" { "Verified on " (fmt_joined(time_ms)) }
1512+ }
1513+ }
1514+ },
1515+ _ => signature_badge(state),
1516+ }
1517+}
1518+
14091519 /// A signature badge (green Verified / amber Unverified) with a descriptive title.
14101520 fn signature_badge(state: &git::SignatureState) -> Markup {
14111521 match state {
@@ -1429,10 +1539,6 @@ fn signature_badge(state: &git::SignatureState) -> Markup {
14291539 /// The detail bar under a commit's metadata describing its signature: who signed
14301540 /// it and with which key fingerprint. Renders nothing for an unsigned commit.
14311541 fn signature_detail(state: &git::SignatureState, signer: Option<&str>) -> Markup {
1432- let kind_label = |k: model::KeyKind| match k {
1433- model::KeyKind::Ssh => "SSH",
1434- model::KeyKind::Gpg => "GPG",
1435- };
14361542 match state {
14371543 git::SignatureState::Unsigned => html! {},
14381544 git::SignatureState::Verified {
@@ -1446,13 +1552,13 @@ fn signature_detail(state: &git::SignatureState, signer: Option<&str>) -> Markup
14461552 "Signed by " strong { (signer.unwrap_or(user_id)) }
14471553 }
14481554 span class="sig-fp mono" {
1449- (kind_label(*kind)) " key fingerprint: " (fingerprint)
1555+ (key_kind_label(*kind)) " key fingerprint: " (fingerprint)
14501556 }
14511557 }
14521558 },
14531559 git::SignatureState::UnknownKey { kind, fingerprint } => html! {
14541560 div class="sig-detail sig-unverified" {
1455- span { "Signed by an unregistered " (kind_label(*kind)) " key" }
1561+ span { "Signed by an unregistered " (key_kind_label(*kind)) " key" }
14561562 span class="sig-fp mono" { (fingerprint) }
14571563 }
14581564 },