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

The inbox's ask cards draw §5's card and buttons #4658

Merged opened this

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

Merged

scout/forge:scout/4615-the-inbox-s-ask-cards-draw-5-s-card-and → main 4 commits fork

  • Merged. scout/4615-the-inbox-s-ask-cards-draw-5-s-card-and was merged into main as a7acd84.

Review

Sign in to review this change.
5 files changed +19 −105
docs/DESIGN.md −1
@@ -400,7 +400,6 @@ gone, so this list only shrinks.
400400 - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r`
401401 - **`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`
402402 - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs and checks (a pull's look is §5's **look**).
403 - **`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`
404403 - **`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
405404 - **`pages/map.ts`** — the map of what waits for what: `.map-ask` `.map-breath` `.map-canvas` `.map-card` `.map-dot` `.map-edge` `.map-edge-crit` `.map-edge-done` `.map-main` `.map-meta` `.map-node` `.map-rule` `.map-svg` `.map-title` `.map-tool` `.map-who` `.map-who-i` `.map-who-n` `.map-word` `.map-wrap`
406405 - **`studio/feed_style.ts`** — the home feed's cards and its composer (the apps rail is §5's card, row and btn; an admin's tick boxes and Archive are always shown, deliberately, with no Select toggle): `.composer` `.feed` `.feed-head`
src/web/studio/asks_page.ts +10 −33
@@ -79,9 +79,8 @@ const BULK_KINDS: ReadonlySet<Decision["kind"]> = new Set(["look", "review", "st
7979 /** A row's box for the bulk form, which sits apart (`form="asks-bulk"`): forms do not nest. */
8080 const pick = (d: Decision, picked: ReadonlySet<string>): Html | null =>
8181 BULK_KINDS.has(d.kind)
82 ? html`<label class="ask-pick-hit"><input
82 ? html`<label class="check"><input
8383 type="checkbox"
84 class="ask-pick"
8584 name="item"
8685 value="${d.id}"
8786 form="asks-bulk"
@@ -163,7 +162,7 @@ const clause = (
163162 which?: string,
164163 ): Html =>
165164 html`<p>
166 ${weight(c, which)}${d === undefined ? null : html`<span class="ask-short">${d.short.charAt(0).toUpperCase()}${d.short.slice(1)}. </span>`}<span class="muted"
165 ${weight(c, which)}${d === undefined ? null : html`${d.short.charAt(0).toUpperCase()}${d.short.slice(1)}. `}<span class="muted"
167166 >If nobody acts, ${ASK_WORDS[kind].ifNothing}.</span
168167 >
169168 </p>`;
@@ -182,22 +181,10 @@ const KIND_LABEL: Readonly<Record<Decision["kind"], string>> = {
182181 assigned: "Assigned",
183182 };
184183
185 /**
186 * An ask's actions: the one that decides, then the rest. A wide screen
187 * lays them in a line; a phone puts the decider at the trailing edge, 44 px
188 * tall, and the rest behind one tap ("More").
189 */
184 /** An ask's actions in a line: the one that decides, then the rest. */
190185 function acts(ctx: Ctx, d: Decision): Html {
191186 const { primary, others } = decisionButtons(ctx, d);
192 return html`<div class="ask-acts">
193 ${primary}
194 ${others.length === 0
195 ? null
196 : html`<details class="ask-more-acts">
197 <summary class="btn" data-size="sm">More</summary>
198 ${others}
199 </details>`}
200 </div>`;
187 return html`<div class="bar mt">${primary}${others}</div>`;
201188 }
202189
203190 /** What the crew advised on a pull waiting for a person, in one plain line; nothing when it wrote none. */
@@ -237,11 +224,9 @@ function decisionCard(ctx: Ctx, d: Decision, picked: ReadonlySet<string>): Html
237224 ${acts(ctx, d)}`,
238225 {
239226 id: askAnchor(d.id),
240 extra: "ask-card",
241227 head: html`${pick(d, picked)}
242228 <h2>${title}</h2>
243 <span class="ask-meta muted small"
244 ><b class="ask-kind">${KIND_LABEL[d.kind]}</b>${d.repo.ownerName}/${d.repo.name}</span
229 <span class="muted small"><b>${KIND_LABEL[d.kind]}</b> · ${d.repo.ownerName}/${d.repo.name}</span
245230 >${waitChip(d.waiting)}`,
246231 },
247232 );
@@ -249,7 +234,7 @@ function decisionCard(ctx: Ctx, d: Decision, picked: ReadonlySet<string>): Html
249234
250235 /** What the row waits for and on whom: "ready to land · on you". */
251236 const waitChip = (w: WaitingFor): Html =>
252 html`<span class="ask-waiting small" data-waiting="${w.word}"
237 html`<span class="badge" data-variant="outline" data-waiting="${w.word}"
253238 ><span class="sr-only">waiting for </span>${w.word} · on ${w.on}</span
254239 >`;
255240
@@ -290,24 +275,16 @@ function rowCard(ctx: Ctx, r: AskRow<Decision>, picked: ReadonlySet<string>): Ht
290275 meta: acts(ctx, d),
291276 });
292277 });
293 // On a phone a group is its head line, its asks one tap away, so two
294 // groups never push the composer past the fold; a wider screen holds the
295 // details open and shows every line, as before.
296278 return card(
297 html`<details class="ask-group">
298 <summary>Show all ${r.items.length}</summary>
299 <ul class="list">
300 ${lines}
301 </ul>
302 </details>`,
279 html`<ul class="list">
280 ${lines}
281 </ul>`,
303282 {
304283 pad: false,
305284 id: askAnchor(r.id),
306 extra: "ask-card",
307285 head: html`${BULK_KINDS.has(r.kind)
308 ? html`<label class="ask-pick-hit"><input
286 ? html`<label class="check"><input
309287 type="checkbox"
310 class="ask-pick"
311288 data-select-all
312289 aria-label="Select all ${r.items.length}"
313290 /></label>`
src/web/style/content.ts −52
@@ -976,8 +976,6 @@ textarea.editor:focus-visible { outline: none; }
976976 its words, not the column. */
977977 #needs-you #asks-bulk select { width: auto; max-width: 100%; }
978978 #needs-you #asks-bulk .input[name="reason"] { flex: 1 1 12rem; width: auto; min-width: 0; }
979 /* A row's box sits in a label, so a phone's thumb has 44 px to hit. */
980 .ask-pick-hit { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; }
981979 /* A phone shows the top asks and one "N more" that opens in place to all
982980 of them; a wider screen shows ASKS_SHOWN and its own "N more" past that.
983981 Only where a details element can be held open: elsewhere both summaries
@@ -992,58 +990,8 @@ textarea.editor:focus-visible { outline: none; }
992990 #needs-you #asks-phone-more > summary { display: none; }
993991 #needs-you #asks-phone-more::details-content { content-visibility: visible; }
994992 #needs-you #asks-phone-more > .stack { margin-top: 0; }
995 #needs-you .ask-group > summary { display: none; }
996 #needs-you .ask-group::details-content { content-visibility: visible; }
997993 }
998994 }
999 #needs-you .ask-group > summary { padding: 10px 14px; cursor: pointer; }
1000 /* An ask's actions: the decider, then the rest. Wide: in a line, as ever.
1001 The kind label is for a phone, where the sentence under the title goes. */
1002 .ask-acts { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 12px; }
1003 .ask-more-acts { display: contents; }
1004 .ask-more-acts > summary { list-style: none; }
1005 .ask-more-acts > summary::-webkit-details-marker { display: none; }
1006 .ask-kind { display: none; }
1007 .ask-waiting { display: inline-block; margin-left: 8px; padding: 0 6px; border: 1px solid var(--border); border-radius: 999px; color: var(--fg); white-space: nowrap; max-width: 100%; }
1008 .row .ask-acts { margin-top: 0; }
1009 @supports selector(::details-content) {
1010 @media (min-width: 481px) {
1011 .ask-more-acts > summary { display: none; }
1012 .ask-more-acts::details-content { content-visibility: visible; display: contents; }
1013 }
1014 }
1015 /* A phone: rows run edge to edge (the card's box, border and body padding
1016 go; the page's 16 px gutter stays), the title wraps under "kind · repo",
1017 the decider sits at the trailing edge and the rest wait behind "More". */
1018 @media (max-width: 480px) {
1019 #needs-you .ask-card {
1020 margin-inline: -16px; border-inline: 0; border-radius: 0; box-shadow: none; background: none;
1021 }
1022 .ask-card .card-head { padding: 12px 16px 0; background: none; border: 0; gap: 4px 8px; align-items: baseline; }
1023 .ask-card .card-body { padding: 4px 16px 12px; }
1024 .ask-card .card-foot { padding: 0 16px 12px; background: none; border: 0; }
1025 .ask-card .card-head h2 { order: 2; flex: 1 1 100%; min-width: 0; overflow-wrap: anywhere; font-size: 1.05em; }
1026 .ask-card .card-head h2 a { white-space: normal; overflow: visible; text-overflow: clip; }
1027 .ask-card .ask-meta { order: 1; flex: 1; min-width: 0; overflow-wrap: anywhere; }
1028 .ask-card .ask-kind { display: inline; margin-right: 6px; color: var(--fg); }
1029 .ask-card .ask-kind::after { content: " ·"; font-weight: 400; }
1030 .ask-card .ask-short { display: none; }
1031 .ask-card .ask-waiting { order: 3; flex: 0 1 auto; margin-left: 0; min-width: 0; max-width: 100%; white-space: normal; }
1032 .ask-card .ask-pick-hit { order: 0; }
1033 #needs-you .ask-pick-hit { min-width: 44px; min-height: 44px; }
1034 .ask-card .ask-pick { min-width: 24px; min-height: 24px; }
1035 .ask-card .ask-group > summary { padding: 10px 16px; min-height: 44px; }
1036 .ask-card .ask-group .row { flex-wrap: wrap; padding-inline: 16px; }
1037 .ask-card .ask-group .row-main { flex-basis: 100%; }
1038 .ask-card .ask-group .row-meta { flex: 1 1 100%; white-space: normal; }
1039 .ask-acts { flex-direction: row-reverse; justify-content: space-between; }
1040 .ask-acts > .btn { min-height: 44px; min-width: 96px; padding: 0 20px; flex: 0 0 auto; }
1041 .ask-more-acts { display: flex; flex-wrap: wrap; gap: 6px; }
1042 .ask-more-acts > summary { display: inline-flex; min-height: 44px; padding: 0 16px; }
1043 .ask-more-acts[open] { flex: 1 1 100%; order: 2; }
1044 .ask-more-acts[open] > summary { display: none; }
1045 .ask-more-acts > .btn { min-height: 44px; flex: 1 1 auto; }
1046 }
1047995 /* Reduced motion: the pulses hold still at a legible level, the scroll-tied
1048996 fade stays at its resting width, and these transitions land at once. */
1049997 @media (prefers-reduced-motion: reduce) {
test/asks-rank.test.ts +8 −18
@@ -339,20 +339,17 @@ describe("asks that earn their place", () => {
339339 recordAskLedger(app.db);
340340 expect((await mcpAsks()).measure.raised).toBe(6);
341341 });
342 test("a phone's ask is a full-width row: kind and repo on one line, the decider apart, the rest behind More", async () => {
342 test("an ask is §5's card with §5's buttons and no ask-* class", async () => {
343343 const home = await page("/asks");
344344 const asks = home.slice(home.indexOf('<section id="needs-you"'));
345 expect(asks).toContain("ask-card");
346 expect(asks).toContain('<b class="ask-kind">');
347 // No icon column: the row opens with its kind, not a picture.
348 expect(asks.slice(0, asks.indexOf("<h2", asks.indexOf("ask-card")))).not.toContain("<svg");
349 expect(asks).toContain('<details class="ask-more-acts">');
350 expect(asks).toMatch(/class="ask-acts">\s*<button[^>]*data-variant="primary"/);
345 expect(asks).toMatch(/<div\s+class="card"\s+id="look-/);
346 expect(asks).toContain("<b>Ready to ship</b> · ");
347 expect(asks).toMatch(/class="bar mt">\s*<button[^>]*data-variant="primary"/);
348 expect(asks).not.toMatch(/class="[^"]*\bask-(card|acts|kind|pick|group|more-acts|waiting|meta|short)\b/);
351349 const css = await (await fetch(`${servers.url}${home.match(/href="(\/[^"]*\.css[^"]*)"/)![1]}`)).text();
352 expect(css).toContain(".ask-acts > .btn { min-height: 44px;");
350 expect(css).not.toMatch(/\.ask-/);
353351 // The markup lost the .needs-you class (#4189): no selector may still need it.
354352 expect(css).not.toMatch(/\.needs-you\b/);
355 expect(css).toContain("#needs-you .ask-card {\n margin-inline: -16px;");
356353 });
357354
358355 test("a collapsed row that ranks for security names which of its asks it is", async () => {
@@ -362,15 +359,8 @@ describe("asks that earn their place", () => {
362359 const asks = await page("/asks");
363360 expect(asks).toContain("2 green changes wait for your look");
364361 expect(asks).toContain("<b>security</b>: \u201cWiden the board id\u201d");
365 // On a phone the row is its head line, its two asks one tap away; a
366 // wider screen holds them open (the lead's call: the fold wins).
367362 const group = asks.slice(asks.indexOf("2 green changes wait for your look"));
368 expect(group).toContain('<details class="ask-group">');
369 expect(group).toContain("<summary>Show all 2</summary>");
370 const css = await (await fetch(`${servers.url}${asks.match(/href="(\/[^"]*\.css[^"]*)"/)![1]}`)).text();
371 const wide = css.slice(css.indexOf("@supports selector(::details-content) {"));
372 expect(wide.slice(wide.indexOf("@media (min-width: 481px)"))).toContain(
373 "#needs-you .ask-group > summary { display: none; }",
374 );
363 expect(group).toContain('<ul class="list">');
364 expect(group).not.toContain("ask-group");
375365 });
376366 });
test/asks-waiting.test.ts +1 −1
@@ -180,7 +180,7 @@ describe("asks that wait on a person", () => {
180180 expect(page).toContain("Say what to change");
181181 expect(row).not.toContain("/settings/autonomy");
182182 expect(page).toContain(">Handed over<");
183 expect(page).toMatch(/class="ask-pick"[^>]*value="waiting:[^"]+"[^>]*data-kind="waiting"/);
183 expect(page).toMatch(/<label class="check"><input[^>]*name="item"[^>]*value="waiting:[^"]+"[^>]*data-kind="waiting"/);
184184 });
185185
186186 test("the MCP asks tool carries the waiting kind, its why and the recommendation", async () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs