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(
1977 .filter(|r| r.path.starts_with(&prefix) && !r.path[prefix.len()..].contains('/'))1977 .filter(|r| r.path.starts_with(&prefix) && !r.path[prefix.len()..].contains('/'))
1978 .collect();1978 .collect();
19791979
1980 let crumbs = path_crumbs(&owner.username, &group.path);
1980 let body = html! {1981 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 }
1982 @if !subgroups.is_empty() {1990 @if !subgroups.is_empty() {
1983 section { h2 { "Subgroups" }1991 section class="listing" { h2 { "Subgroups" }
1984 ul class="repo-list" {1992 ul class="repo-list card" {
1985 @for g in &subgroups { li { a href={ "/" (owner.username) "/" (g.path) } { (g.path) } } }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 }
1986 }2000 }
1987 }2001 }
1988 }2002 }
@@ -2246,6 +2260,23 @@ fn link_icon(url: &str) -> Icon {
22462260
2247/// The repo sub-header: slug, description, and a tab strip sharing its line with2261/// The repo sub-header: slug, description, and a tab strip sharing its line with
2248/// the branch switcher and clone menu.2262/// 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.
2266fn 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
2249pub(crate) fn repo_header(2280pub(crate) fn repo_header(
2250 state: &AppState,2281 state: &AppState,
2251 ctx: &RepoCtx,2282 ctx: &RepoCtx,
@@ -2261,12 +2292,17 @@ pub(crate) fn repo_header(
2261 }2292 }
2262 }2293 }
2263 };2294 };
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);
2264 html! {2298 html! {
2265 div class="repo-head" {2299 div class="repo-head" {
2266 h1 class="repo-slug" {2300 h1 class="repo-slug" {
2267 a href={ "/" (ctx.owner.username) } { (ctx.owner.username) }2301 a href={ "/" (ctx.owner.username) } { (ctx.owner.username) }
2268 span class="slug-sep" { "/" }2302 @for (name, href) in &crumbs {
2269 a href=(base) { (ctx.repo.name) }2303 span class="slug-sep" { "/" }
2304 a href=(href) { (name) }
2305 }
2270 (visibility_badge(ctx.repo.visibility))2306 (visibility_badge(ctx.repo.visibility))
2271 (archived_badge(ctx.repo.archived_at.is_some()))2307 (archived_badge(ctx.repo.archived_at.is_some()))
2272 }2308 }
data/repos/01/01kydt075vpteqnjzyms8v3vtm.git/HEAD +1 −0
@@ -0,0 +1 @@
1ref: 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 @@
1Unnamed 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
2exec "/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
2exit 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.