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

Design: a new forge's claim link and the client list use the design system, and claim-link-box/forges-sent leave §5d #4060

Merged opened this

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

Merged

scout/forge:scout/3871-design-a-new-forge-s-claim-link-and-the → main 12 commits fork

  • Merged. scout/3871-design-a-new-forge-s-claim-link-and-the was merged into main as 862f485.

Review

Sign in to review this change.
5 files changed +39 −47
docs/DESIGN.md +1 −1
@@ -398,7 +398,7 @@ gone, so this list only shrinks.
398398 - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r`
399399 - **`style/content.ts`, diff viewer** — the diff table: its gutters, line kinds, file head and inline review comments: `.add` `.del` `.dh` `.diff` `.diff-body` `.diff-head` `.diff-note` `.diff-path` `.diff-stat` `.diff-stub` `.diff-summary` `.diff-table` `.dl` `.dl-review` `.dn` `.dn-comment` `.nonl` `.rc-hidden` `.rc-resolve` `.rc-resolved` `.wd`
400400 - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs and checks (a pull's look is §5's **look**): `.ln-forge` `.ln-pulse` `.ln-say` `.ln-tool` `.ln-you` `.log-tail-row` `.pinned-failures` `.pinned-slot` `.run-jump` `.run-live` `.run-steer` `.tests-bar` `.tests-words` `.txt` `.who`
401 - **`style/content.ts`, search results** — search results, the inbox's ask cards, the client list and the claim link: `.ask-acts` `.ask-card` `.ask-group` `.ask-kind` `.ask-meta` `.ask-more-acts` `.ask-pick` `.ask-pick-hit` `.ask-short` `.ask-waiting` `.claim-link-box` `.forges-sent` `.live-toggle` `.needs-you` `.no-hits`
401 - **`style/content.ts`, search results** — search results, the inbox's ask cards: `.ask-acts` `.ask-card` `.ask-group` `.ask-kind` `.ask-meta` `.ask-more-acts` `.ask-pick` `.ask-pick-hit` `.ask-short` `.ask-waiting` `.live-toggle` `.needs-you` `.no-hits`
402402 - **`pages/board.ts`** — the board itself is §5's **board**; the spend tables `spend.ts` draws wear §5's `.table` and `.stack`, no class of their own
403403 - **`pages/map.ts`** — the map of what waits for what: `.map-ask` `.map-breath` `.map-canvas` `.map-card` `.map-counts` `.map-dot` `.map-edge` `.map-edge-crit` `.map-edge-done` `.map-empty` `.map-list` `.map-main` `.map-meta` `.map-node` `.map-pillars` `.map-rule` `.map-svg` `.map-title` `.map-tool` `.map-tools` `.map-who` `.map-who-i` `.map-who-n` `.map-word` `.map-wrap`
404404 - **`studio/feed_style.ts`** — the home feed's cards, its composer and the apps rail: `.composer` `.composer-bar` `.fcard-form` `.fcard-reason` `.fcard-reply` `.fcard-text` `.fcard-unblock` `.fcard-who` `.fcard-why` `.feed` `.feed-head` `.kpi-spark` `.home-app` `.home-apps` `.home-apps-verbs` `.home-pick` `.home-rail` `.home-tabs`
src/web/pages/forges.ts +15 −24
@@ -14,7 +14,7 @@ import { html, type Html } from "../html.ts";
1414 import { page } from "../layout.ts";
1515 import { reissueDoor } from "../../spawn/door.ts";
1616 import { readForm } from "../form.ts";
17 import { btn, card, empty, time } from "../ui.ts";
17 import { badge, btn, card, empty, row, time } from "../ui.ts";
1818 import { noRoom } from "../../spawn/door.ts";
1919 import { claimLinkBody } from "./new-forge.ts";
2020
@@ -31,6 +31,7 @@ function runningNote(r: ClientRow): Html | null {
3131 function forgeRow(r: ClientRow): Html {
3232 const state = claimState(r);
3333 const meta = [
34 state === "unknown" ? html`it did not answer just now` : null,
3435 r.for === null ? null : html`for ${r.for}`,
3536 r.apps.length === 0 ? null : html`${r.apps.join(", ")}`,
3637 runningNote(r),
@@ -39,28 +40,18 @@ function forgeRow(r: ClientRow): Html {
3940 ? null
4041 : html`last activity ${time(r.lastActivityAt)}`,
4142 ].filter((m): m is Html => m !== null);
42 return html`<li data-forge="${r.name}" data-claimed="${state}">
43 <div class="row">
44 <span class="row-main">
45 <a class="row-title" href="${r.url}">${r.name}</a>
46 <span class="row-meta"
47 >${meta.map((m, i) => html`${i > 0 ? " · " : ""}${m}`)}</span
48 >
49 </span>
50 <span class="row-trail"
51 ><span
52 class="pill"
53 ${state === "unknown" ? html`title="It did not answer just now"` : null}
54 ${state === "yes" ? html`data-tone="accent"` : null}
55 >${CLAIM_WORDS[state]}</span
56 >${state === "no"
57 ? html`<form method="post" action="/forges/${r.name}/claim-link">
58 ${btn("Send a fresh link", { size: "sm", title: "A new claim link; the old one stops working" })}
59 </form>`
60 : null}</span
61 >
62 </div>
63 </li>`;
43 return row({
44 title: html`<a href="${r.url}">${r.name}</a>`,
45 meta: html`${meta.map((m, i) => html`${i > 0 ? " · " : ""}${m}`)}`,
46 data: { forge: r.name, claimed: state },
47 trail: html`${badge(CLAIM_WORDS[state], {
48 ...(state === "yes" ? { tone: "success" as const } : { variant: "outline" as const }),
49 })}${state === "no"
50 ? html`<form method="post" action="/forges/${r.name}/claim-link">
51 ${btn("Send a fresh link", { size: "sm", title: "A new claim link; the old one stops working" })}
52 </form>`
53 : null}`,
54 });
6455 }
6556
6657 // GET /forges — an admin's only.
@@ -92,7 +83,7 @@ export async function forgesPage(ctx: Ctx): Promise<Response> {
9283 action,
9384 })
9485 : card(
95 html`<ul class="list forges-sent">
86 html`<ul class="list setting-list">
9687 ${rows.map(forgeRow)}
9788 </ul>`,
9889 { pad: false },
src/web/pages/new-forge.ts +8 −6
@@ -26,7 +26,7 @@ import { notFoundPage, pageHead } from "../chrome.ts";
2626 import { field, readForm, type Form } from "../form.ts";
2727 import { html, type Html } from "../html.ts";
2828 import { page } from "../layout.ts";
29 import { btn, card, field as formField } from "../ui.ts";
29 import { btn, card, field as formField, row } from "../ui.ts";
3030
3131 interface Values {
3232 readonly name: string;
@@ -355,11 +355,13 @@ export function claimLinkBody(link: string, note?: string): Html {
355355 <p class="muted small mb">
356356 Send it as you would a password: to them alone, not to a group.
357357 </p>
358 <p class="claim-link-box"><code class="mono" id="claim-link">${link}</code></p>
359 <p class="mt">
360 ${btn("Copy the link", { type: "button", copy: "claim-link", icon: "copy", variant: "primary", size: "sm" })}
361 ${btn("Forges you sent", { href: "/forges", size: "sm" })}
362 </p>
358 ${card(html`<ul class="list setting-list">${
359 row({
360 title: html`<code class="mono" id="claim-link">${link}</code>`,
361 trail: btn("Copy the link", { type: "button", copy: "claim-link", icon: "copy", variant: "primary", size: "sm" }),
362 })
363 }</ul>`, { pad: false })}
364 <p class="mt">${btn("Forges you sent", { href: "/forges", size: "sm" })}</p>
363365 ${note === undefined ? null : html`<p class="small mt">${note}</p>`}`;
364366 }
365367
src/web/style/content.ts −16
@@ -1108,22 +1108,6 @@ textarea.editor:focus-visible { outline: none; }
11081108 border: 1px dashed var(--border); border-radius: var(--radius);
11091109 }
11101110 .no-hits .ico { color: var(--faint); flex: none; }
1111 /* The client list (#776): whom a forge is for is the point of the row, so
1112 its line wraps rather than being cut off on a phone. */
1113 .forges-sent .row-meta { white-space: normal; }
1114 .forges-sent .row-trail { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
1115 .forges-sent .row-trail form { margin: 0; }
1116 .forges-sent a.row-title { color: var(--fg); }
1117 @media (max-width: 560px) {
1118 .forges-sent .row { flex-wrap: wrap; }
1119 .forges-sent .row-trail { flex-basis: 100%; justify-content: flex-start; }
1120 }
1121 /* "+ New forge" (#426): the claim link, shown once, impossible to miss. */
1122 .claim-link-box {
1123 margin: 0; padding: 12px 14px; font-size: 15px;
1124 border: 1px solid var(--border); border-radius: var(--radius);
1125 background: var(--muted); overflow-wrap: anywhere;
1126 }
11271111
11281112 }
11291113 /* What needs you, pinned on the home: the bulk step's select as wide as
test/client-list.test.ts +15
@@ -198,8 +198,10 @@ describe("the client list", () => {
198198 expect(maria).toContain("alice/bakery");
199199 expect(maria).toContain('href="https://maria--forge.test"');
200200 expect(maria).toMatch(/data-claimed="no"/);
201 expect(html).not.toContain("forges-sent");
201202 const lou = html.slice(html.indexOf('data-forge="lou"'));
202203 expect(lou).toMatch(/data-claimed="unknown"/);
204 expect(lou).toContain("it did not answer just now");
203205 // The claim link itself is never kept, so never shown here.
204206 expect(html).not.toContain(claimUrl.split("#")[1]!);
205207 });
@@ -259,3 +261,16 @@ describe("the client list", () => {
259261 expect(rows.lou.claimed).toBeNull();
260262 });
261263 });
264
265 describe("the claim link's markup", () => {
266 test("is drawn from the design system and stays one tap to copy", async () => {
267 const { claimLinkBody } = await import("../src/web/pages/new-forge.ts");
268 const out = claimLinkBody("https://x.test/claim#abc").text;
269 expect(out).not.toContain("claim-link-box");
270 expect(out).toContain('id="claim-link"');
271 expect(out).toContain('class="row"');
272 // The long link wraps rather than ellipsizes: only .setting-list lifts nowrap.
273 expect(out).toContain("list setting-list");
274 expect(out).toContain("Copy the link");
275 });
276 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs