Public: anyone can read this; the crew can change it.
A spawned forge's first-login next steps speak the home page's language #9846
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/9837-a-spawned-forge-s-first-login-next-steps → main 4 commits fork 2 threads · 2 unresolved
5 files
changed +39
−26
docs/design/areas/home.md +2
| @@ -5,6 +5,8 @@ | ||
| 5 | 5 | - `.kpi-row` — a modifier on `.stats` for the home's figure tiles, not a second layout: it only changes the phone case. `.stats` alone (`minmax(160px, 1fr)`) falls to one column at 360px (two 160px tiles and the gap need 332px of 328px), stacking nine 104px tiles to ~1,100px; under 480px `.kpi-row` is two equal `minmax(0, 1fr)` columns instead. Tile height stays fixed (`.stat`), so nothing shifts. |
| 6 | 6 | - `.home-panels-slot` — home's place for the pinned panels, right after the KPIs (`pinnedSlot` in `src/web/studio/pinned.ts`). Only while it still waits for `/live/home/panels` (`data-refresh`) it holds 240px, about one panel row, with the 16px below it that `.bgrid` keeps (the system has no spacing tokens, only the 4px scale in §4, and both sit on it), so the charts arriving after first paint shift nothing; the refresh script drops the attribute when the fragment lands, is empty, or fails, and the slot collapses to its content. |
| 7 | 7 | |
| 8 | - `.step-body` — the first-login next steps (`src/web/pages/next-steps.ts`, #9837) are one plain section: `<h2>`, then a `ul.list` row per step (title, Done trail) with the pending step's body (form, app buttons, Tell the crew) as a block under its row, indented past the lead icon (44px, 16px on a phone) on the 4px scale. No box, no border; the bold title marks the primary step and the one `variant="primary"` button is its accent. | |
| 9 | ||
| 8 | 10 | The signed-in home's door line (Notes, Talk) is `.home-doors`: accent underlined links, each a 44px tap target (#9584). |
| 9 | 11 | |
| 10 | 12 | `/explore` (#9645) speaks the same language: the kinds (All, Apps, People, Forges) are `tabs()` as on home; sort and time stay one `seg` each in a small line (they stay boxed: they are two-to-three-way switches whose current choice must read at a glance, and a plain link row would blur it from the tabs above), with "Show archived" a link (`aria-pressed`) wearing `.home-doors`' 44px hit area, no box; the rows are a `.card.bare` list, rows and space on the page's one background, no box. Row words, ranking, "Make it yours" and the URL line are unchanged, so the terminal's explore search keeps the same words. No new class. |
src/web/pages/next-steps.ts +20 −25
| @@ -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 { badge, btn, card, row } from "../ui.ts"; | |
| 23 | import { badge, btn, 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"; |
| @@ -61,12 +61,13 @@ function crewAtWork( | ||
| 61 | 61 | return { lines, busy }; |
| 62 | 62 | } |
| 63 | 63 | |
| 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. */ | |
| 64 | /** One step: its row (title, "Done" trail; the primary one in bold), then its | |
| 65 | * body as a block of its own under the title. `.row-meta` is one clipped line, | |
| 66 | * no place for a form. */ | |
| 66 | 67 | function stepBlock(step: NextStep, primary: boolean, body: Html | null): Html { |
| 67 | 68 | const head = row({ |
| 68 | 69 | lead: step.done ? icon("check") : icon("issue"), |
| 69 | title: html`${primary ? html`<b>${step.title}</b>` : step.title}`, | |
| 70 | title: primary ? html`<b>${step.title}</b>` : html`${step.title}`, | |
| 70 | 71 | ...(step.done ? { trail: badge("Done", { tone: "success" }) } : {}), |
| 71 | 72 | data: { |
| 72 | 73 | step: step.key, |
| @@ -77,7 +78,7 @@ function stepBlock(step: NextStep, primary: boolean, body: Html | null): Html { | ||
| 77 | 78 | return html`<ul class="list"> |
| 78 | 79 | ${head} |
| 79 | 80 | </ul> |
| 80 | ${!step.done && body ? html`<div class="card-body">${body}</div>` : null}`; | |
| 81 | ${!step.done && body ? html`<div class="step-body">${body}</div>` : null}`; | |
| 81 | 82 | } |
| 82 | 83 | |
| 83 | 84 | /** The card, or nothing: not a spawned forge, not its owner, or every step done. */ |
| @@ -117,26 +118,20 @@ export async function nextStepsCard( | ||
| 117 | 118 | })} |
| 118 | 119 | </p>`, |
| 119 | 120 | }; |
| 120 | return card( | |
| 121 | html`${crew.lines.length > 0 | |
| 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"> | |
| 130 | ${crew.lines} | |
| 131 | </ul>` | |
| 132 | : null} | |
| 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 | }, | |
| 139 | ); | |
| 121 | return html`<section id="next-steps"> | |
| 122 | <h2>Your next steps</h2> | |
| 123 | ${crew.lines.length > 0 | |
| 124 | ? html`<p> | |
| 125 | ${crew.busy | |
| 126 | ? "Your crew is already at work:" | |
| 127 | : "Your crew has its direction:"} | |
| 128 | </p> | |
| 129 | <ul class="list"> | |
outdated
This |
||
| 130 | ${crew.lines} | |
| 131 | </ul>` | |
| 132 | : null} | |
| 133 | ${steps.map((s) => stepBlock(s, s.key === primary, bodies[s.key]))} | |
| 134 | </section>`; | |
| 140 | 135 | } |
| 141 | 136 | |
| 142 | 137 | /** The steps once per request, for the card and for the lean home to share. */ |
src/web/style/content.ts +4
| @@ -84,6 +84,10 @@ a.where-place:hover, button.where-place:hover { | ||
| 84 | 84 | #live-chips a:hover, #live-chips a:focus-visible, .home-doors a:hover, .home-doors a:focus-visible { text-decoration-thickness: 2px; } |
| 85 | 85 | #live-chips a { max-width: 100%; overflow-wrap: anywhere; } |
| 86 | 86 | .home-doors { display: flex; flex-wrap: wrap; gap: 4px 12px; } |
| 87 | .step-body { padding: 4px 16px 16px 44px; } | |
| 88 | .step-body > :first-child { margin-top: 0; } | |
| 89 | .step-body > :last-child { margin-bottom: 0; } | |
| 90 | @media (max-width: 719px) { .step-body { padding-left: 16px; } } | |
| 87 | 91 | .tell-box .input { min-width: 0; max-width: 100%; } |
| 88 | 92 | .row-trail form { display: inline-flex; } |
| 89 | 93 |
test/first-login.test.ts +1 −1
| @@ -220,7 +220,7 @@ describe("the first login to a spawned forge", () => { | ||
| 220 | 220 | // A pending step's body is a block of its own, never inside the row's |
| 221 | 221 | // one-line, clipped `.row-meta`. |
| 222 | 222 | expect(home).not.toContain("row-meta"); |
| 223 | expect(home).toMatch(/<div class="card-body">\s*<p class="muted small">/); | |
| 223 | expect(home).toMatch(/<div class="step-body">\s*<p class="muted small">/); | |
| 224 | 224 | // Each app with its way in. |
| 225 | 225 | expect(home).toContain('href="/marias/bakery"'); |
| 226 | 226 | }); |
test/next-steps-card.test.ts +12
| @@ -180,6 +180,18 @@ describe("the next-steps card of a spawned forge", () => { | ||
| 180 | 180 | expect([...at].sort((a, b) => a - b)).toEqual(at); |
| 181 | 181 | }); |
| 182 | 182 | |
| 183 | test("the steps are one plain section, not boxed in cards", async () => { | |
| 184 | const card = cardOf(await page(owner)); | |
| 185 | expect(card).not.toContain('class="card'); | |
| 186 | expect(card).not.toContain("card-body"); | |
| 187 | // A pending step's body is a block under its row, never an inline sibling of the title. | |
| 188 | expect(card).toMatch(/<\/ul>\s*<div class="step-body">/); | |
| 189 | // The first pending step, and only it, is the primary one. | |
| 190 | expect(card).toMatch(/data-step="everyday"[^>]*data-primary/); | |
| 191 | expect(card.match(/data-primary/g)?.length).toBe(1); | |
| 192 | expect(keys(await page(owner))).toEqual(["everyday", "apps", "tell"]); | |
| 193 | }); | |
| 194 | ||
| 183 | 195 | test("each step's way in answers 200 for the admin", async () => { |
| 184 | 196 | const home = await page(owner); |
| 185 | 197 | const card = cardOf(home); |
Principle 3 (generous rhythm) and 7 (rows over boxes): the icon, title, badge and body are inline siblings in one
<li>, and global CSS strips list markers, so the steps run together and read as unordered. Userow()for the head and put the body in a block with spacing; no card needed.