Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
assets/base.css +25 −12
| @@ -2219,6 +2219,21 @@ pre.code { | |||
| 2219 | .back-link { | 2219 | .back-link { |
| 2220 | font-size: 0.9em; | 2220 | font-size: 0.9em; |
| 2221 | } | 2221 | } |
| 2222 | /* Breadcrumb (Releases / name / Edit). */ | ||
| 2223 | .crumbs { | ||
| 2224 | display: flex; | ||
| 2225 | align-items: center; | ||
| 2226 | gap: 0.4rem; | ||
| 2227 | flex-wrap: wrap; | ||
| 2228 | margin-bottom: 1rem; | ||
| 2229 | font-size: 0.95em; | ||
| 2230 | } | ||
| 2231 | .crumbs a { | ||
| 2232 | color: var(--fb-accent); | ||
| 2233 | } | ||
| 2234 | .crumb-current { | ||
| 2235 | color: var(--fb-fg-muted); | ||
| 2236 | } | ||
| 2222 | .release { | 2237 | .release { |
| 2223 | margin-bottom: 1.25rem; | 2238 | margin-bottom: 1.25rem; |
| 2224 | } | 2239 | } |
| @@ -2329,21 +2344,19 @@ pre.code { | |||
| 2329 | opacity: 0; | 2344 | opacity: 0; |
| 2330 | pointer-events: none; | 2345 | pointer-events: none; |
| 2331 | } | 2346 | } |
| 2332 | /* The pending (not-yet-uploaded) file: editable name, size, remove. */ | ||
| 2333 | .asset-pending { | ||
| 2334 | display: flex; | ||
| 2335 | align-items: center; | ||
| 2336 | gap: 0.6rem; | ||
| 2337 | margin-bottom: 0.75rem; | ||
| 2338 | } | ||
| 2339 | .asset-pending-name { | ||
| 2340 | flex: 1; | ||
| 2341 | min-width: 0; | ||
| 2342 | font-family: var(--fb-font-mono); | ||
| 2343 | } | ||
| 2344 | .asset-upload .btn { | 2347 | .asset-upload .btn { |
| 2345 | margin-top: 0.75rem; | 2348 | margin-top: 0.75rem; |
| 2346 | } | 2349 | } |
| 2350 | /* The per-asset delete button sits in a card form; kill the stacked-form top | ||
| 2351 | margin so it centres in the row, and keep it compact. Specificity beats | ||
| 2352 | `.card form button[type="submit"]:not(.inline-btn)`. */ | ||
| 2353 | .card .asset-list .asset-row form button[type="submit"] { | ||
| 2354 | margin-top: 0; | ||
| 2355 | } | ||
| 2356 | .asset-row .icon-btn { | ||
| 2357 | width: 1.75rem; | ||
| 2358 | height: 1.75rem; | ||
| 2359 | } | ||
| 2347 | .release-form-actions { | 2360 | .release-form-actions { |
| 2348 | display: flex; | 2361 | display: flex; |
| 2349 | align-items: center; | 2362 | align-items: center; |
assets/fabrica.js +6 −16
| @@ -170,29 +170,19 @@ | |||
| 170 | } | 170 | } |
| 171 | }); | 171 | }); |
| 172 | 172 | ||
| 173 | // ---- Release asset dropzone (drag-drop / click, editable name + size) ---- | 173 | // ---- Release asset dropzone: pick or drop a file to upload it immediately ---- |
| 174 | document.querySelectorAll(".asset-upload").forEach(function (form) { | 174 | document.querySelectorAll(".asset-upload").forEach(function (form) { |
| 175 | var input = form.querySelector("input[type=file]"); | 175 | var input = form.querySelector("input[type=file]"); |
| 176 | var zone = form.querySelector(".dropzone"); | 176 | var zone = form.querySelector(".dropzone"); |
| 177 | var pending = form.querySelector(".asset-pending"); | 177 | if (!input || !zone) return; |
| 178 | if (!input || !zone || !pending) return; | 178 | var submit = form.querySelector("button[type=submit]"); |
| 179 | function show() { | 179 | if (submit) submit.hidden = true; // JS uploads on selection; button is no-JS only |
| 180 | var f = input.files[0]; | 180 | input.addEventListener("change", function () { if (input.files.length) form.submit(); }); |
| 181 | pending.hidden = !f; | ||
| 182 | if (!f) return; | ||
| 183 | pending.querySelector("input[type=text]").value = f.name; | ||
| 184 | pending.querySelector(".asset-size").textContent = | ||
| 185 | "(" + (f.size < 1048576 ? (f.size / 1024).toFixed(1) + " KB" : (f.size / 1048576).toFixed(2) + " MB") + ")"; | ||
| 186 | } | ||
| 187 | input.addEventListener("change", show); | ||
| 188 | pending.querySelector(".asset-remove").addEventListener("click", function () { | ||
| 189 | input.value = ""; pending.hidden = true; | ||
| 190 | }); | ||
| 191 | ["dragover", "dragenter", "dragleave", "drop"].forEach(function (ev) { | 181 | ["dragover", "dragenter", "dragleave", "drop"].forEach(function (ev) { |
| 192 | zone.addEventListener(ev, function (e) { | 182 | zone.addEventListener(ev, function (e) { |
| 193 | e.preventDefault(); | 183 | e.preventDefault(); |
| 194 | zone.classList.toggle("drag", ev === "dragover" || ev === "dragenter"); | 184 | zone.classList.toggle("drag", ev === "dragover" || ev === "dragenter"); |
| 195 | if (ev === "drop" && e.dataTransfer.files.length) { input.files = e.dataTransfer.files; show(); } | 185 | if (ev === "drop" && e.dataTransfer.files.length) { input.files = e.dataTransfer.files; form.submit(); } |
| 196 | }); | 186 | }); |
| 197 | }); | 187 | }); |
| 198 | }); | 188 | }); |
crates/web/src/releases.rs +20 −9
| @@ -147,7 +147,11 @@ pub(crate) async fn view( | |||
| 147 | .await; | 147 | .await; |
| 148 | let body = html! { | 148 | let body = html! { |
| 149 | (repo_header(state, &ctx, Tab::Releases, &ctx.repo.default_branch, &branches)) | 149 | (repo_header(state, &ctx, Tab::Releases, &ctx.repo.default_branch, &branches)) |
| 150 | p { a class="back-link" href=(format!("{base}/-/releases")) { "← Releases" } } | 150 | nav class="crumbs" aria-label="Breadcrumb" { |
| 151 | a href=(format!("{base}/-/releases")) { "Releases" } | ||
| 152 | span class="slug-sep" { "/" } | ||
| 153 | span class="crumb-current" { (if release.name.is_empty() { &release.tag } else { &release.name }) } | ||
| 154 | } | ||
| 151 | (card) | 155 | (card) |
| 152 | }; | 156 | }; |
| 153 | let title = format!( | 157 | let title = format!( |
| @@ -278,11 +282,23 @@ pub async fn edit_form( | |||
| 278 | .user_by_id(&repo.owner_id) | 282 | .user_by_id(&repo.owner_id) |
| 279 | .await? | 283 | .await? |
| 280 | .map_or(repo.owner_id.clone(), |u| u.username); | 284 | .map_or(repo.owner_id.clone(), |u| u.username); |
| 285 | let releases = format!("/{owner}/{}/-/releases", repo.path); | ||
| 281 | let back = format!("/{owner}/{}/-/releases/{}", repo.path, release.tag); | 286 | let back = format!("/{owner}/{}/-/releases/{}", repo.path, release.tag); |
| 287 | let crumb = if release.name.is_empty() { | ||
| 288 | &release.tag | ||
| 289 | } else { | ||
| 290 | &release.name | ||
| 291 | }; | ||
| 282 | let assets = state.store.list_release_assets(&release.id).await?; | 292 | let assets = state.store.list_release_assets(&release.id).await?; |
| 283 | let (jar, chrome) = build_chrome(&state, jar, Some(user), uri.path().to_string()); | 293 | let (jar, chrome) = build_chrome(&state, jar, Some(user), uri.path().to_string()); |
| 284 | let body = html! { | 294 | let body = html! { |
| 285 | p { a class="back-link" href=(back) { "← Back to release" } } | 295 | nav class="crumbs" aria-label="Breadcrumb" { |
| 296 | a href=(releases) { "Releases" } | ||
| 297 | span class="slug-sep" { "/" } | ||
| 298 | a href=(back) { (crumb) } | ||
| 299 | span class="slug-sep" { "/" } | ||
| 300 | span class="crumb-current" { "Edit" } | ||
| 301 | } | ||
| 286 | (release_form(&format!("/release/{}/edit", release.id), &chrome.csrf, Some(&release), &[], &[])) | 302 | (release_form(&format!("/release/{}/edit", release.id), &chrome.csrf, Some(&release), &[], &[])) |
| 287 | (assets_editor(&release.id, &chrome.csrf, &assets)) | 303 | (assets_editor(&release.id, &chrome.csrf, &assets)) |
| 288 | }; | 304 | }; |
| @@ -366,18 +382,13 @@ fn assets_editor(release_id: &str, csrf: &str, assets: &[store::ReleaseAsset]) - | |||
| 366 | } | 382 | } |
| 367 | form method="post" action=(format!("/release/{release_id}/asset")) enctype="multipart/form-data" class="asset-upload" { | 383 | form method="post" action=(format!("/release/{release_id}/asset")) enctype="multipart/form-data" class="asset-upload" { |
| 368 | input type="hidden" name="_csrf" value=(csrf); | 384 | input type="hidden" name="_csrf" value=(csrf); |
| 369 | // Populated by JS when a file is picked (editable name, size, | ||
| 370 | // remove); stays hidden without JS. | ||
| 371 | div class="asset-pending" hidden { | ||
| 372 | input type="text" name="name" class="asset-pending-name" aria-label="Asset name"; | ||
| 373 | span class="asset-size muted" {} | ||
| 374 | button type="button" class="asset-remove icon-btn" aria-label="Remove file" title="Remove file" { (icon(Icon::X)) } | ||
| 375 | } | ||
| 376 | label class="dropzone" { | 385 | label class="dropzone" { |
| 377 | input type="file" name="asset" class="dropzone-input" required; | 386 | input type="file" name="asset" class="dropzone-input" required; |
| 378 | (icon(Icon::Download)) | 387 | (icon(Icon::Download)) |
| 379 | span { "Attach a file by dropping it here or selecting it." } | 388 | span { "Attach a file by dropping it here or selecting it." } |
| 380 | } | 389 | } |
| 390 | // Hidden by JS, which uploads on selection; the button is the | ||
| 391 | // no-JS fallback. | ||
| 381 | button class="btn inline-btn" type="submit" { "Upload asset" } | 392 | button class="btn inline-btn" type="submit" { "Upload asset" } |
| 382 | } | 393 | } |
| 383 | } | 394 | } |
data/fabrica/releases/01kyf3cgb99w0wjas5a5jbx2pt +0 −0
Binary file not shown.