fabrica

hanna/fabrica

fix(web): fork icon, inline fork form, and cap fork visibility

2aa4fb0 · hanna committed on 2026-07-26

- Use a dedicated git-fork icon for the "forked from" line, the Fork
  button, and fork entries in repo listings (mirrors keep the refresh
  icon, plain repos the box).
- The fork confirm form puts its button inline to the right of the name.
- A fork can no longer be made more visible than its parent: settings
  clamps a fork's visibility to the parent's rank, so a private repo
  cannot be forked and then published.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
3 files changed · +36 −6UnifiedSplit
crates/model/src/entity.rs +11 −0
@@ -94,6 +94,17 @@ impl Visibility {
9494 pub fn is_private(self) -> bool {
9595 matches!(self, Self::Private)
9696 }
97+
98+ /// A rank where higher is more visible (`private` < `internal` < `public`).
99+ /// Used to cap a fork's visibility at its parent's.
100+ #[must_use]
101+ pub fn rank(self) -> u8 {
102+ match self {
103+ Self::Private => 0,
104+ Self::Internal => 1,
105+ Self::Public => 2,
106+ }
107+ }
97108 }
98109
99110 impl std::fmt::Display for Visibility {
crates/web/src/icons.rs +5 −0
@@ -40,6 +40,7 @@ pub enum Icon {
4040 Globe,
4141 Download,
4242 Mirror,
43+ GitFork,
4344 Github,
4445 Mastodon,
4546 }
@@ -123,6 +124,10 @@ impl Icon {
123124 Icon::Mirror => {
124125 r#"<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>"#
125126 }
127+ // Lucide "git-fork": two branches diverging from a commit.
128+ Icon::GitFork => {
129+ r#"<circle cx="12" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9"/><path d="M12 12v3"/>"#
130+ }
126131 Icon::Github => {
127132 r#"<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>"#
128133 }
crates/web/src/repo.rs +20 −6
@@ -1646,8 +1646,10 @@ pub async fn fork_confirm(
16461646 form method="post" action=(format!("/repo-fork/{}", source.id)) {
16471647 input type="hidden" name="_csrf" value=(chrome.csrf);
16481648 label for="fk-name" { "Repository name" }
1649- input type="text" id="fk-name" name="name" value=(source.name) required;
1650- button class="btn btn-primary inline-btn" type="submit" { "Fork repository" }
1649+ div class="admin-form-row admin-form-row-last" {
1650+ input type="text" id="fk-name" name="name" value=(source.name) required;
1651+ button class="btn btn-primary inline-btn" type="submit" { "Fork repository" }
1652+ }
16511653 }
16521654 }
16531655 }
@@ -1839,7 +1841,15 @@ pub async fn settings_general(
18391841 .store
18401842 .rename_repo(&repo.id, new_name, &new_path)
18411843 .await?;
1842- if let Some(vis) = Visibility::from_token(&form.visibility) {
1844+ if let Some(mut vis) = Visibility::from_token(&form.visibility) {
1845+ // A fork may never be more visible than its parent (no laundering a
1846+ // private repo into a public one).
1847+ if let Some(parent_id) = &repo.fork_parent_id
1848+ && let Some(parent) = state.store.repo_by_id(parent_id).await?
1849+ && vis.rank() > parent.visibility.rank()
1850+ {
1851+ vis = parent.visibility;
1852+ }
18431853 state.store.set_visibility(&repo.id, vis).await?;
18441854 }
18451855 state
@@ -2574,6 +2584,8 @@ pub(crate) struct RepoEntry {
25742584 pub(crate) archived: bool,
25752585 /// Whether the repo is a mirror (shows the mirror icon).
25762586 pub(crate) is_mirror: bool,
2587+ /// Whether the repo is a fork (shows the fork icon).
2588+ pub(crate) is_fork: bool,
25772589 /// Optional one-line description, shown under the name when present.
25782590 pub(crate) description: Option<String>,
25792591 }
@@ -2589,6 +2601,7 @@ impl RepoEntry {
25892601 updated: repo.pushed_at.unwrap_or(repo.updated_at),
25902602 archived: repo.archived_at.is_some(),
25912603 is_mirror: repo.is_mirror,
2604+ is_fork: repo.fork_parent_id.is_some(),
25922605 description: repo.description.clone(),
25932606 }
25942607 }
@@ -2604,6 +2617,7 @@ impl RepoEntry {
26042617 updated: repo.pushed_at.unwrap_or(repo.updated_at),
26052618 archived: repo.archived_at.is_some(),
26062619 is_mirror: repo.is_mirror,
2620+ is_fork: repo.fork_parent_id.is_some(),
26072621 description: repo.description.clone(),
26082622 }
26092623 }
@@ -2643,7 +2657,7 @@ pub(crate) fn repo_card(title: &str, empty: &str, entries: &[RepoEntry]) -> Mark
26432657 li {
26442658 a class="repo-row" href=(e.href) {
26452659 span class="repo-row-name" {
2646- (icon(if e.is_mirror { Icon::Mirror } else { Icon::Box }))
2660+ (icon(if e.is_mirror { Icon::Mirror } else if e.is_fork { Icon::GitFork } else { Icon::Box }))
26472661 span { (e.label) }
26482662 (visibility_badge(e.visibility))
26492663 (archived_badge(e.archived))
@@ -2820,7 +2834,7 @@ pub(crate) fn repo_header(
28202834 }
28212835 @if let Some((powner, ppath)) = &ctx.fork_parent {
28222836 p class="muted fork-note" {
2823- (icon(Icon::Mirror)) " forked from "
2837+ (icon(Icon::GitFork)) " forked from "
28242838 a href=(format!("/{powner}/{ppath}")) { (powner) "/" (ppath) }
28252839 }
28262840 }
@@ -2844,7 +2858,7 @@ pub(crate) fn repo_header(
28442858 div class="repo-actions" {
28452859 @if ctx.viewer_id.is_some() {
28462860 a class="btn" href=(format!("/repo-fork/{}", ctx.repo.id)) {
2847- (icon(Icon::GitBranch)) span { "Fork" }
2861+ (icon(Icon::GitFork)) span { "Fork" }
28482862 }
28492863 }
28502864 (branch_menu(&base, rev, branches))