Public: anyone can read this; the crew can change it.
Design: a spawned forge's next-steps card uses the design system, and the next-step* classes leave §5d #3815
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3802-design-a-spawned-forge-s-next-steps-card → main 3 commits fork
5 files
changed +45
−54
docs/DESIGN.md +1 −1
Diff not shown — a line 2,062 characters wide (limit 2,000). +1 −1
View filedocs/design/baseline.json −9
| @@ -17,15 +17,6 @@ | ||
| 17 | 17 | "focus": 2, |
| 18 | 18 | "input": 1, |
| 19 | 19 | "target": 1 |
| 20 | }, | |
| 21 | "src/web/style/content.ts": { | |
| 22 | "colour": 0, | |
| 23 | "size": 0, | |
| 24 | "motion": 0, | |
| 25 | "forced": 0, | |
| 26 | "focus": 0, | |
| 27 | "input": 0, | |
| 28 | "target": 0 | |
| 29 | 20 | } |
| 30 | 21 | } |
| 31 | 22 | } |
src/web/pages/next-steps.ts +37 −33
| @@ -20,7 +20,7 @@ import { getUserById, type User } from "../../core/users.ts"; | ||
| 20 | 20 | import type { Ctx } from "../../http/context.ts"; |
| 21 | 21 | import { html, type Html } from "../html.ts"; |
| 22 | 22 | import { icon } from "../icons.ts"; |
| 23 | import { btn, card } from "../ui.ts"; | |
| 23 | import { badge, btn, card, row } from "../ui.ts"; | |
| 24 | 24 | import { tellHref } from "../studio/nouns.ts"; |
| 25 | 25 | import { everydayAsk } from "./everyday.ts"; |
| 26 | 26 | import { EVERYDAY_WHY } from "../../core/everyday.ts"; |
| @@ -52,32 +52,32 @@ function crewAtWork( | ||
| 52 | 52 | ? "is about to start on" |
| 53 | 53 | : "has been given"; |
| 54 | 54 | lines.push( |
| 55 | html`<li> | |
| 56 | <b>${agent.username}</b> ${doing} | |
| 57 | <a href="${tellHref(`/${repo.ownerName}/${repo.name}`, issue.number)}" | |
| 58 | >${issue.title}</a | |
| 59 | > | |
| 60 | </li>`, | |
| 55 | row({ | |
| 56 | href: tellHref(`/${repo.ownerName}/${repo.name}`, issue.number), | |
| 57 | title: html`<b>${agent.username}</b> ${doing} ${issue.title}`, | |
| 58 | }), | |
| 61 | 59 | ); |
| 62 | 60 | } |
| 63 | 61 | return { lines, busy }; |
| 64 | 62 | } |
| 65 | 63 | |
| 66 | function stepRow(step: NextStep, primary: boolean, body: Html | null): Html { | |
| 67 | return html`<li | |
| 68 | class="next-step" | |
| 69 | data-step="${step.key}" | |
| 70 | data-done="${step.done ? "yes" : "no"}" | |
| 71 | ${primary ? html`data-primary=""` : null} | |
| 72 | > | |
| 73 | <span class="next-step-mark" aria-hidden="true" | |
| 74 | >${step.done ? icon("check") : icon("issue")}</span | |
| 75 | > | |
| 76 | <span class="next-step-main"> | |
| 77 | <span class="next-step-title">${step.title}</span> | |
| 78 | ${step.done ? null : body} | |
| 79 | </span> | |
| 80 | </li>`; | |
| 64 | /** The step's row, then its body below it as a padded block of its own: | |
| 65 | * `.row-meta` is one clipped line, no place for a form. */ | |
| 66 | function stepBlock(step: NextStep, primary: boolean, body: Html | null): Html { | |
| 67 | const head = row({ | |
| 68 | lead: step.done ? icon("check") : icon("issue"), | |
| 69 | title: html`${primary ? html`<b>${step.title}</b>` : step.title}`, | |
| 70 | ...(step.done ? { trail: badge("Done", { tone: "success" }) } : {}), | |
| 71 | data: { | |
| 72 | step: step.key, | |
| 73 | done: step.done ? "yes" : "no", | |
| 74 | ...(primary ? { primary: "" } : {}), | |
| 75 | }, | |
| 76 | }); | |
| 77 | return html`<ul class="list"> | |
| 78 | ${head} | |
| 79 | </ul> | |
| 80 | ${!step.done && body ? html`<div class="card-body">${body}</div>` : null}`; | |
| 81 | 81 | } |
| 82 | 82 | |
| 83 | 83 | /** The card, or nothing: not a spawned forge, not its owner, or every step done. */ |
| @@ -98,7 +98,7 @@ export async function nextStepsCard( | ||
| 98 | 98 | ? html`<p class="muted small"> |
| 99 | 99 | Your apps arrive here as the crew sets them up. |
| 100 | 100 | </p>` |
| 101 | : html`<p class="next-step-apps"> | |
| 101 | : html`<p> | |
| 102 | 102 | ${repos.map( |
| 103 | 103 | (r) => |
| 104 | 104 | html`${btn(r.name, { |
| @@ -119,19 +119,23 @@ export async function nextStepsCard( | ||
| 119 | 119 | }; |
| 120 | 120 | return card( |
| 121 | 121 | html`${crew.lines.length > 0 |
| 122 | ? html`<p class="mb"> | |
| 123 | ${crew.busy | |
| 124 | ? "Your crew is already at work:" | |
| 125 | : "Your crew has its direction:"} | |
| 126 | </p> | |
| 127 | <ul class="next-crew mb"> | |
| 122 | ? html`<div class="card-body"> | |
| 123 | <p> | |
| 124 | ${crew.busy | |
| 125 | ? "Your crew is already at work:" | |
| 126 | : "Your crew has its direction:"} | |
| 127 | </p> | |
| 128 | </div> | |
| 129 | <ul class="list"> | |
| 128 | 130 | ${crew.lines} |
| 129 | 131 | </ul>` |
| 130 | 132 | : null} |
| 131 | <ol class="next-steps"> | |
| 132 | ${steps.map((s) => stepRow(s, s.key === primary, bodies[s.key]))} | |
| 133 | </ol>`, | |
| 134 | { id: "next-steps", head: html`<h2>Your next steps</h2>` }, | |
| 133 | ${steps.map((s) => stepBlock(s, s.key === primary, bodies[s.key]))}`, | |
| 134 | { | |
| 135 | id: "next-steps", | |
| 136 | head: html`<h2>Your next steps</h2>`, | |
| 137 | pad: false, | |
| 138 | }, | |
| 135 | 139 | ); |
| 136 | 140 | } |
| 137 | 141 |
src/web/style/content.ts −10
| @@ -336,16 +336,6 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); } | ||
| 336 | 336 | /* A subscription's headroom for people: a small number beside its windows. */ |
| 337 | 337 | .headroom { display: flex; align-items: center; gap: 6px; } |
| 338 | 338 | .headroom .input { width: 5em; } |
| 339 | /* A spawned forge's first steps (#600): three rows, the next one leading. */ | |
| 340 | .next-crew { margin: 0 0 12px 1.1em; } | |
| 341 | .next-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; } | |
| 342 | .next-step { display: grid; grid-template-columns: 1.4em minmax(0, 1fr); gap: 8px; align-items: start; } | |
| 343 | .next-step-mark { color: var(--faint); padding-top: 2px; } | |
| 344 | .next-step[data-done="yes"] .next-step-mark { color: var(--success, var(--accent)); } | |
| 345 | .next-step[data-done="yes"] .next-step-title { color: var(--muted-fg); text-decoration: line-through; } | |
| 346 | .next-step[data-primary] .next-step-title { font-weight: 600; } | |
| 347 | .next-step-main { display: flex; flex-direction: column; gap: 6px; min-width: 0; max-width: 36rem; } | |
| 348 | .next-step-apps { display: flex; flex-wrap: wrap; gap: 6px; } | |
| 349 | 339 | /* The everyday-account ask on the home: a form's width, not the page's. */ |
| 350 | 340 | .everyday-ask { max-width: 34rem; margin-bottom: 16px; } |
| 351 | 341 | /* On a phone an account's controls go under its name, not over it. */ |
test/first-login.test.ts +7 −1
| @@ -212,7 +212,13 @@ describe("the first login to a spawned forge", () => { | ||
| 212 | 212 | expect(home).toContain("Your crew has its direction:"); |
| 213 | 213 | expect(home).not.toContain("already at work"); |
| 214 | 214 | expect(home).toContain("atlas"); |
| 215 | expect(home).toMatch(/href="\/marias\/bakery\/issues\/\d+"[^>]*>[^<]*Run Maria/); | |
| 215 | expect(home).toMatch(/href="\/marias\/bakery\/issues\/\d+"[^>]*>[\s\S]*?Run Maria/); | |
| 216 | for (const old of ["next-step", "next-crew"]) expect(home).not.toContain(`class="${old}`); | |
| 217 | expect(home).not.toMatch(/class="next-step/); | |
| 218 | // A pending step's body is a block of its own, never inside the row's | |
| 219 | // one-line, clipped `.row-meta`. | |
| 220 | expect(home).not.toContain("row-meta"); | |
| 221 | expect(home).toMatch(/<div class="card-body">\s*<p class="muted small">/); | |
| 216 | 222 | // Each app with its way in. |
| 217 | 223 | expect(home).toContain('href="/marias/bakery"'); |
| 218 | 224 | }); |
new above
offline