Public: anyone can read this; the crew can change it.

A repository's branches page speaks the home page's language #9750

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/9746-a-repository-s-branches-page-speaks-the → main 3 commits fork 4 threads · 4 unresolved

  • Merged. mason/9746-a-repository-s-branches-page-speaks-the was merged into main as 50f445d.

Review

Sign in to review this change.
3 files changed +61 −70
docs/design/areas/branches.md added +3
@@ -0,0 +1,3 @@
1 # Branches
2
3 The branches page (`src/web/pages/branches.ts`) is a plain list of rows. The name is the row's one accent link; Pin and New pull are quiet text links in the trail, and `.branch-del` is the Delete link, muted until hover or focus turns it `--danger`. On a phone the trail drops under the row's text and wraps.
src/web/pages/branches.ts +50 −70
@@ -33,11 +33,9 @@ import { localScope } from "../scope.ts";
3333 import {
3434 badge,
3535 btn,
36 card,
3736 empty,
3837 field,
3938 icon,
40 listCard,
4139 selectField,
4240 sha,
4341 time,
@@ -210,32 +208,17 @@ function branchRow(
210208 </span>
211209 <span class="row-trail">
212210 ${divergence(row)}
213 ${when(canWrite, () =>
214 btn("Pin", {
215 href: `${base}/builds/new?sha=${row.oid}&source=${encodeURIComponent(`branch ${row.name}`)}`,
216 size: "sm",
217 variant: "outline",
218 icon: "pin",
219 label: `Pin a build of ${row.name}`,
220 }),
211 ${when(
212 canWrite,
213 () => html`<a class="small" href="${base}/builds/new?sha=${row.oid}&source=${encodeURIComponent(`branch ${row.name}`)}" aria-label="Pin a build of ${row.name}">Pin</a>`,
221214 )}
222 ${when(!row.isDefault && canWrite, () =>
223 btn("New pull", {
224 href: `${base}/pulls/new?head=${encoded}`,
225 size: "sm",
226 variant: "outline",
227 icon: "pull",
228 }),
215 ${when(
outdated

Principle 7: a row with three always-visible buttons is a toolbar, not a row. Show Pin and New pull as quiet text links, or fold them into the branch's own page, so the name is the one link. Verify this trail at 360px.

216 !row.isDefault && canWrite,
217 () => html`<a class="small" href="${base}/pulls/new?head=${encoded}" aria-label="Open a pull from ${row.name}">New pull</a>`,
229218 )}
230 ${when(!row.isDefault && canWrite, () =>
231 btn("Delete", {
232 href: `/-/confirm?to=${encodeURIComponent(
233 `${base}/branches/${encodeURIComponent(row.name)}/delete`,
234 )}`,
235 size: "sm",
236 variant: "danger",
237 label: `Delete branch ${row.name}`,
238 }),
219 ${when(
220 !row.isDefault && canWrite,
221 () => html`<a class="small branch-del" href="/-/confirm?to=${encodeURIComponent(`${base}/branches/${encodeURIComponent(row.name)}/delete`)}" aria-label="Delete branch ${row.name}">Delete</a>`,
239222 )}
240223 </span>
241224 </div>
@@ -243,32 +226,27 @@ function branchRow(
243226 }
244227
245228 function createForm(scope: RepoScope, refs: string[], from: string): Html {
246 return card(
247 html`<form method="post" action="${scope.base}/branches" class="field-row">
248 ${field({
249 label: "New branch",
250 name: "name",
251 placeholder: "feature/thing",
252 mono: true,
253 required: true,
254 autocomplete: "off",
255 })}
256 ${selectField({
257 label: "From",
258 name: "from",
259 options: refs.map((r) => ({
260 value: r,
261 label: r,
262 selected: r === from,
263 })),
264 })}
265 ${btn("Create branch", { variant: "primary", icon: "plus" })}
266 </form>`,
267 {
268 head: html`${icon("branch")}
269 <h2>Create a branch</h2>`,
270 },
271 );
229 return html`<h2 class="mt">Create a branch</h2>
230 <form method="post" action="${scope.base}/branches" class="field-row">
231 ${field({
232 label: "New branch",
233 name: "name",
234 placeholder: "feature/thing",
outdated

Delete as ghost looks the same as Pin. Keep a danger cue (muted text with a danger colour on hover/focus) so a thumb does not hit it by mistake.

235 mono: true,
236 required: true,
237 autocomplete: "off",
238 })}
239 ${selectField({
240 label: "From",
241 name: "from",
242 options: refs.map((r) => ({
243 value: r,
244 label: r,
outdated

Principle 3: a bare h2 with the form right under it is fine, but the create form comes before the list with no spacing class, while the list h2 has mt. Make the rhythm the same for both sections.

245 selected: r === from,
246 })),
247 })}
248 ${btn("Create branch", { variant: "primary", icon: "plus" })}
249 </form>`;
272250 }
273251
274252 // GET /:owner/:name/branches
@@ -308,30 +286,32 @@ export async function renderBranches(
308286 ${when(scope.caps.write && scope.repo !== undefined, () =>
309287 createForm(scope, all.length === 0 ? [startFrom] : all, startFrom),
310288 )}
289 ${rows.length === 0 && scope.kind === "local"
290 ? html`<p class="small"><a href="${scope.base}/builds">Builds</a></p>`
291 : null}
311292 ${rows.length === 0
312293 ? empty({
313294 icon: "branch",
314295 title: "No branches yet",
315296 note: "Push one, or create the first from the form above.",
316297 })
317 : listCard(
318 rows.map((row) => branchRow(scope, scope.repo, row)),
319 {
320 head: html`<h2>
321 ${rows.length} branch${rows.length === 1 ? "" : "es"}
322 </h2>
323 ${when(scope.kind === "local", () => html`<span class="spacer"></span>${btn("Builds", { href: `${scope.base}/builds`, size: "sm", variant: "ghost", icon: "pin" })}`)}
324 ${when(
325 !compared && scope.kind === "local",
326 () =>
327 html`<span class="spacer"></span
328 ><span class="muted small"
329 >Too many branches to compare — ahead/behind is
330 omitted.</span
331 >`,
332 )}`,
333 },
334 )}
298 : html`<h2 class="mt">
299 ${rows.length} branch${rows.length === 1 ? "" : "es"}
300 ${when(
301 scope.kind === "local",
302 () => html`<a class="small" href="${scope.base}/builds">Builds</a>`,
303 )}
304 </h2>
305 ${when(
306 !compared && scope.kind === "local",
307 () =>
308 html`<p class="muted small">
309 Too many branches to compare — ahead/behind is omitted.
310 </p>`,
311 )}
312 <ul class="list">
313 ${rows.map((row) => branchRow(scope, scope.repo, row))}
314 </ul>`}
335315 </div>
336316 ${when(scope.kind === "local", () => liveMount({ repo: scope.base }))}`;
337317
src/web/style/content.ts +8
@@ -964,6 +964,14 @@ textarea.editor:focus-visible { outline: none; }
964964
965965 /* ---- branches ---- */
966966 .branch-row .row-title { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
967 .branch-row .row-meta { white-space: normal; overflow-wrap: anywhere; }
968 .branch-row .row-trail { flex: 0 1 auto; flex-wrap: wrap; gap: 4px 12px; }
969 .branch-row .branch-del { color: var(--muted-fg); }
970 .branch-row .branch-del:hover, .branch-row .branch-del:focus-visible { color: var(--danger); }
971 @media (max-width: 719px) {
972 .branch-row { flex-wrap: wrap; }
973 .branch-row .row-trail { flex: 1 1 100%; justify-content: flex-start; }
974 }
967975 .branch-div { display: inline-flex; gap: 8px; font-size: 12.5px; }
968976 .branch-div .ahead { color: var(--add-fg); }
969977 .branch-div .behind { color: var(--del-fg); }

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs