fabrica

hanna/fabrica

feat(web): full group breadcrumb in the repo header and group view

cd3d397 · hanna committed on 2026-07-25

The repo header now shows owner / group… / repo, each path segment linked — the
group segments open the group listing (subgroups + repos), which also gains a
clickable breadcrumb heading and the listing-card row style.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
8 files changed · +64 −6UnifiedSplit
crates/web/src/repo.rs +42 −6
@@ -1977,12 +1977,26 @@ async fn group_listing(
19771977 .filter(|r| r.path.starts_with(&prefix) && !r.path[prefix.len()..].contains('/'))
19781978 .collect();
19791979
1980+ let crumbs = path_crumbs(&owner.username, &group.path);
19801981 let body = html! {
1981- h1 { (owner.username) "/" (group.path) }
1982+ h1 class="repo-slug group-slug" {
1983+ (icon(Icon::Folder))
1984+ a href={ "/" (owner.username) } { (owner.username) }
1985+ @for (name, href) in &crumbs {
1986+ span class="slug-sep" { "/" }
1987+ a href=(href) { (name) }
1988+ }
1989+ }
19821990 @if !subgroups.is_empty() {
1983- section { h2 { "Subgroups" }
1984- ul class="repo-list" {
1985- @for g in &subgroups { li { a href={ "/" (owner.username) "/" (g.path) } { (g.path) } } }
1991+ section class="listing" { h2 { "Subgroups" }
1992+ ul class="repo-list card" {
1993+ @for g in &subgroups {
1994+ li {
1995+ a class="repo-row" href={ "/" (owner.username) "/" (g.path) } {
1996+ span class="repo-row-name" { (icon(Icon::Folder)) span { (g.path) } }
1997+ }
1998+ }
1999+ }
19862000 }
19872001 }
19882002 }
@@ -2246,6 +2260,23 @@ fn link_icon(url: &str) -> Icon {
22462260
22472261 /// The repo sub-header: slug, description, and a tab strip sharing its line with
22482262 /// the branch switcher and clone menu.
2263+/// Build breadcrumb `(segment, href)` pairs for each segment of a repo `path`
2264+/// under `owner`. Group segments link to their group listing; the last is the
2265+/// repo itself.
2266+fn path_crumbs(owner: &str, path: &str) -> Vec<(String, String)> {
2267+ let mut out = Vec::new();
2268+ let mut cumulative = String::new();
2269+ for seg in path.split('/') {
2270+ if cumulative.is_empty() {
2271+ cumulative = seg.to_string();
2272+ } else {
2273+ cumulative = format!("{cumulative}/{seg}");
2274+ }
2275+ out.push((seg.to_string(), format!("/{owner}/{cumulative}")));
2276+ }
2277+ out
2278+}
2279+
22492280 pub(crate) fn repo_header(
22502281 state: &AppState,
22512282 ctx: &RepoCtx,
@@ -2261,12 +2292,17 @@ pub(crate) fn repo_header(
22612292 }
22622293 }
22632294 };
2295+ // Breadcrumb: owner / group… / repo. Each path segment links to its own page
2296+ // (group segments render the group listing; the leaf is the repo).
2297+ let crumbs = path_crumbs(&ctx.owner.username, &ctx.repo.path);
22642298 html! {
22652299 div class="repo-head" {
22662300 h1 class="repo-slug" {
22672301 a href={ "/" (ctx.owner.username) } { (ctx.owner.username) }
2268- span class="slug-sep" { "/" }
2269- a href=(base) { (ctx.repo.name) }
2302+ @for (name, href) in &crumbs {
2303+ span class="slug-sep" { "/" }
2304+ a href=(href) { (name) }
2305+ }
22702306 (visibility_badge(ctx.repo.visibility))
22712307 (archived_badge(ctx.repo.archived_at.is_some()))
22722308 }
data/repos/01/01kydt075vpteqnjzyms8v3vtm.git/HEAD +1 −0
@@ -0,0 +1 @@
1+ref: refs/heads/main
data/repos/01/01kydt075vpteqnjzyms8v3vtm.git/config +9 −0
@@ -0,0 +1,9 @@
1+[core]
2+ bare = true
3+ repositoryformatversion = 0
4+ filemode = true
5+ logallrefupdates = true
6+[receive]
7+ denyNonFastForwards = false
8+[gc]
9+ auto = 0
data/repos/01/01kydt075vpteqnjzyms8v3vtm.git/description +1 −0
@@ -0,0 +1 @@
1+Unnamed repository; edit this file 'description' to name the repository.
data/repos/01/01kydt075vpteqnjzyms8v3vtm.git/hooks/README.sample +5 −0
@@ -0,0 +1,5 @@
1+#!/bin/sh
2+#
3+# Place appropriately named executable hook scripts into this directory
4+# to intercept various actions that git takes. See `git help hooks` for
5+# more information.
data/repos/01/01kydt075vpteqnjzyms8v3vtm.git/hooks/post-receive +2 −0
@@ -0,0 +1,2 @@
1+#!/bin/sh
2+exec "/nix/store/y5fybzdl01v4vgfy642hi70b83s976cq-fabrica-0.1.0/bin/.fabrica-wrapped" hook post-receive
data/repos/01/01kydt075vpteqnjzyms8v3vtm.git/hooks/pre-receive +2 −0
@@ -0,0 +1,2 @@
1+#!/bin/sh
2+exit 0
data/repos/01/01kydt075vpteqnjzyms8v3vtm.git/info/exclude +2 −0
@@ -0,0 +1,2 @@
1+# File patterns to ignore; see `git help ignore` for more information.
2+# Lines that start with '#' are comments.