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 | |||
| 115 | 115 | table.code { border-collapse:collapse; width:100%; font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; } | |
| 116 | 116 | table.code td { padding:0 10px; vertical-align:top; white-space:pre; } | |
| 117 | 117 | 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; } | |
| 118 | 119 | .clone { border:1px solid var(--border); border-radius:6px; padding:12px 16px; margin:16px 0; } | |
| 119 | 120 | .clone-head { display:flex; align-items:center; gap:12px; margin-bottom:8px; } | |
| 120 | 121 | .clone-tabs { display:flex; margin-left:auto; } | |
| ⋯ 26 unchanged lines | |||
| 147 | 148 | .linkbtn:hover { text-decoration:underline; } | |
| 148 | 149 | .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; } | |
| 149 | 150 | .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); } | |
| 151 | 158 | .repo-nav a { color:var(--muted); } | |
| 152 | 159 | .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); } | |
| 153 | 163 | .pill-group { display:inline-flex; } | |
| 154 | 164 | .pill-group > .pill { border-radius:0; margin-left:-1px; position:relative; } | |
| 155 | 165 | .pill-group > .pill:first-child { border-radius:2em 0 0 2em; margin-left:0; } | |
| ⋯ 499 unchanged lines | |||
| 655 | 665 | } | |
| 656 | 666 | ||
| 657 | 667 | /// `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 { | |
| 659 | 673 | let Some(user) = user else { | |
| 660 | 674 | return Redirect::to("/-/login").into_response(); | |
| 661 | 675 | }; | |
| 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() | |
| 663 | 678 | } | |
| 664 | 679 | ||
| 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 | + | ||
| 665 | 692 | /// `POST /new` — create a repository owned by the current user. | |
| 666 | 693 | async fn new_repo_submit( | |
| 667 | 694 | State(app): State<App>, | |
| ⋯ 19 unchanged lines | |||
| 687 | 714 | .await | |
| 688 | 715 | { | |
| 689 | 716 | 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 | + | } | |
| 702 | 733 | } | |
| 703 | 734 | } | |
| 704 | 735 | ||
| ⋯ 3 unchanged lines | |||
| 708 | 739 | name: &str, | |
| 709 | 740 | description: &str, | |
| 710 | 741 | private: bool, | |
| 742 | + | remote: &str, | |
| 711 | 743 | csrf: &str, | |
| 712 | 744 | ) -> Markup { | |
| 713 | 745 | layout( | |
| ⋯ 10 unchanged lines | |||
| 724 | 756 | p { button.btn type="submit" { "Create repository" } } | |
| 725 | 757 | } | |
| 726 | 758 | 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")) } | |
| 727 | 763 | }, | |
| 728 | 764 | ) | |
| 729 | 765 | } | |
| ⋯ 147 unchanged lines | |||
| 877 | 913 | ||
| 878 | 914 | let can_write = access::can_write(&meta, user.as_ref()); | |
| 879 | 915 | 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" } } | |
| 884 | 920 | } | |
| 885 | - | span.repo-nav { | |
| 921 | + | nav.repo-nav { | |
| 886 | 922 | a href=(format!("/{owner}/{repo}/issues")) { "Issues" } | |
| 887 | - | " · " | |
| 923 | + | span.sep { "·" } | |
| 888 | 924 | a href=(format!("/{owner}/{repo}/ci")) { "CI" } | |
| 889 | - | " · " | |
| 925 | + | span.sep { "·" } | |
| 890 | 926 | a href=(format!("/{owner}/{repo}/pages")) { "Pages" } | |
| 891 | 927 | @if can_write { | |
| 892 | - | " · " | |
| 928 | + | span.sep { "·" } | |
| 893 | 929 | a href=(format!("/{owner}/{repo}/settings")) { "Settings" } | |
| 894 | 930 | } | |
| 895 | 931 | } | |
| 896 | 932 | } | |
| 897 | 933 | @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")) } | |
| 902 | 937 | } | |
| 903 | 938 | (clone_box(&app, &owner, &repo)) | |
| 904 | 939 | }; | |
| ⋯ 210 unchanged lines | |||
| 1115 | 1150 | )) | |
| 1116 | 1151 | } | |
| 1117 | 1152 | ||
| 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 | + | ||
| 1118 | 1158 | /// Whether a path should be treated as markdown (by extension). | |
| 1119 | 1159 | fn is_markdown(path: &str) -> bool { | |
| 1120 | 1160 | std::path::Path::new(path) | |
| ⋯ 736 unchanged lines | |||