anvilsign in

collin/anvil · 41970969

ui: cleaner repo header + push-to-create snippet on new-repo page

Collin Richards · 2026-06-10 10:59 UTC · 41970969e589472fe4f04a2741e793f90efd39fa · parent af60789f · browse files

modifiedcrates/anvil-web/src/ui.rs+67 −27
⋯ 114 unchanged lines
115115 table.code { border-collapse:collapse; width:100%; font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
116116 table.code td { padding:0 10px; vertical-align:top; white-space:pre; }
117117 table.code td.ln { text-align:right; color:var(--muted); user-select:none; width:1%; border-right:1px solid var(--border); background:var(--code-bg); }
118+.cmds { background:var(--code-bg); border:1px solid var(--border); border-radius:6px; padding:12px 14px; margin:8px 0; font:12px/1.7 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; overflow-x:auto; }
118119 .clone { border:1px solid var(--border); border-radius:6px; padding:12px 16px; margin:16px 0; }
119120 .clone-head { display:flex; align-items:center; gap:12px; margin-bottom:8px; }
120121 .clone-tabs { display:flex; margin-left:auto; }
⋯ 26 unchanged lines
147148 .linkbtn:hover { text-decoration:underline; }
148149 .btn { display:inline-block; background:var(--accent); color:#fff; border:1px solid var(--accent); border-radius:6px; padding:5px 12px; font-size:13px; cursor:pointer; }
149150 .btn:hover { text-decoration:none; opacity:.92; }
150-.repo-nav { font-size:13px; }
151+/* Repo header: title (+ visibility badge) on the left, quick-nav on the right;
152+ wraps cleanly to its own line on narrow viewports instead of floating. */
153+.repo-head { display:flex; flex-wrap:wrap; align-items:baseline; justify-content:space-between; gap:6px 16px; margin:24px 0 4px; }
154+.repo-title { display:flex; align-items:baseline; flex-wrap:wrap; gap:8px; min-width:0; }
155+.repo-title h1 { margin:0; }
156+.repo-title .pill { font-size:11px; text-transform:uppercase; letter-spacing:.04em; align-self:center; }
157+.repo-nav { font-size:13px; display:flex; align-items:baseline; gap:8px; color:var(--muted); }
151158 .repo-nav a { color:var(--muted); }
152159 .repo-nav a:hover { color:var(--accent); text-decoration:none; }
160+.repo-nav .sep { color:var(--border); }
161+.repo-meta { display:flex; gap:8px; margin:8px 0; color:var(--muted); font-size:13px; }
162+.repo-meta b { font-weight:600; color:var(--fg); }
153163 .pill-group { display:inline-flex; }
154164 .pill-group > .pill { border-radius:0; margin-left:-1px; position:relative; }
155165 .pill-group > .pill:first-child { border-radius:2em 0 0 2em; margin-left:0; }
⋯ 499 unchanged lines
655665 }
656666
657667 /// `GET /new` — new-repository form (requires login).
658-async fn new_repo_form(CurrentUser(user): CurrentUser, csrf: Csrf) -> Response {
668+async fn new_repo_form(
669+ State(app): State<App>,
670+ CurrentUser(user): CurrentUser,
671+ csrf: Csrf,
672+) -> Response {
659673 let Some(user) = user else {
660674 return Redirect::to("/-/login").into_response();
661675 };
662- new_repo_page(&user, None, "", "", false, &csrf.0).into_response()
676+ let remote = push_remote_url(&app, &user.username, "");
677+ new_repo_page(&user, None, "", "", false, &remote, &csrf.0).into_response()
663678 }
664679
680+/// The remote URL to suggest for push-to-create: SSH when enabled (pushes
681+/// without a credential prompt), otherwise HTTP. `name` may be empty, in which
682+/// case a `<name>` placeholder is used.
683+fn push_remote_url(app: &App, owner: &str, name: &str) -> String {
684+ let name = if name.is_empty() { "<name>" } else { name };
685+ if app.config.ssh.enabled {
686+ app.config.ssh_clone_url(owner, name)
687+ } else {
688+ app.config.http_clone_url(owner, name)
689+ }
690+}
691+
665692 /// `POST /new` — create a repository owned by the current user.
666693 async fn new_repo_submit(
667694 State(app): State<App>,
⋯ 19 unchanged lines
687714 .await
688715 {
689716 Ok(repo) => Redirect::to(&format!("/{}/{}", user.username, repo.name)).into_response(),
690- Err(e) => (
691- StatusCode::BAD_REQUEST,
692- new_repo_page(
693- &user,
694- Some(&e.to_string()),
695- &form.name,
696- &form.description,
697- private,
698- &csrf.0,
699- ),
700- )
701- .into_response(),
717+ Err(e) => {
718+ let remote = push_remote_url(&app, &user.username, &form.name);
719+ (
720+ StatusCode::BAD_REQUEST,
721+ new_repo_page(
722+ &user,
723+ Some(&e.to_string()),
724+ &form.name,
725+ &form.description,
726+ private,
727+ &remote,
728+ &csrf.0,
729+ ),
730+ )
731+ .into_response()
732+ }
702733 }
703734 }
704735
⋯ 3 unchanged lines
708739 name: &str,
709740 description: &str,
710741 private: bool,
742+ remote: &str,
711743 csrf: &str,
712744 ) -> Markup {
713745 layout(
⋯ 10 unchanged lines
724756 p { button.btn type="submit" { "Create repository" } }
725757 }
726758 p.muted { "It will be created at " code { (user.username) "/" (if name.is_empty() { "<name>" } else { name }) } "." }
759+
760+ h2 { "…or push an existing repository" }
761+ p.muted { "Pushing to a name that doesn't exist yet creates the repository (private). No need for the form above." }
762+ pre.cmds { (format!("git remote add origin {remote}\ngit push -u origin main")) }
727763 },
728764 )
729765 }
⋯ 147 unchanged lines
877913
878914 let can_write = access::can_write(&meta, user.as_ref());
879915 let header = html! {
880- div style="display:flex;align-items:center;gap:8px" {
881- h1 style="margin-right:auto" {
882- a href=(format!("/{owner}")) { (owner) } " / " (repo)
883- @if meta.is_private { " " span.pill { "private" } }
916+ div.repo-head {
917+ span.repo-title {
918+ h1 { a href=(format!("/{owner}")) { (owner) } " / " (repo) }
919+ @if meta.is_private { span.pill { "private" } }
884920 }
885- span.repo-nav {
921+ nav.repo-nav {
886922 a href=(format!("/{owner}/{repo}/issues")) { "Issues" }
887- " · "
923+ span.sep { "·" }
888924 a href=(format!("/{owner}/{repo}/ci")) { "CI" }
889- " · "
925+ span.sep { "·" }
890926 a href=(format!("/{owner}/{repo}/pages")) { "Pages" }
891927 @if can_write {
892- " · "
928+ span.sep { "·" }
893929 a href=(format!("/{owner}/{repo}/settings")) { "Settings" }
894930 }
895931 }
896932 }
897933 @if !meta.description.is_empty() { p.muted { (meta.description) } }
898- p {
899- span.pill { (overview.branches.len()) " branches" }
900- " "
901- span.pill { (overview.tags.len()) " tags" }
934+ p.repo-meta {
935+ span { b { (overview.branches.len()) } " " (plural(overview.branches.len(), "branch", "branches")) }
936+ span { b { (overview.tags.len()) } " " (plural(overview.tags.len(), "tag", "tags")) }
902937 }
903938 (clone_box(&app, &owner, &repo))
904939 };
⋯ 210 unchanged lines
11151150 ))
11161151 }
11171152
1153+/// Pick the singular or plural noun for a count (`1 branch` / `2 branches`).
1154+fn plural<'a>(n: usize, one: &'a str, many: &'a str) -> &'a str {
1155+ if n == 1 { one } else { many }
1156+}
1157+
11181158 /// Whether a path should be treated as markdown (by extension).
11191159 fn is_markdown(path: &str) -> bool {
11201160 std::path::Path::new(path)
⋯ 736 unchanged lines