Public: anyone can read this; the crew can change it.
The asks page's needs-you section draws §5's components, and .needs-you leaves the markup #4258
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4189-the-asks-page-s-needs-you-section-draws → main 5 commits fork
5 files
changed +28
−19
docs/DESIGN.md +1 −1
| @@ -399,7 +399,7 @@ gone, so this list only shrinks. | ||
| 399 | 399 | - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r` |
| 400 | 400 | - **`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` |
| 401 | 401 | - **`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` `.txt` `.who` |
| 402 | - **`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` | |
| 402 | - **`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` | |
| 403 | 403 | - **`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 |
| 404 | 404 | - **`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` |
| 405 | 405 | - **`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` `.composer-bar` `.fcard-form` `.fcard-reason` `.fcard-reply` `.fcard-text` `.fcard-unblock` `.fcard-who` `.fcard-why` `.feed` `.feed-head` `.home-tabs` |
src/web/studio/asks_page.ts +1 −1
| @@ -479,7 +479,7 @@ export function needsYouSection( | ||
| 479 | 479 | : more("asks-more", rest.length, html`${rest.map((r) => mutableRow(ctx, r, picked))}`); |
| 480 | 480 | // The asks come first, so the first Tab after #needs-you lands on one; |
| 481 | 481 | // the bulk step acts on what was picked, after them. |
| 482 | const body = html`<section id="needs-you" class="needs-you stack"> | |
| 482 | const body = html`<section id="needs-you" class="stack"> | |
| 483 | 483 | <h2 id="asks-sentence">${askSentence(count)}</h2> |
| 484 | 484 | ${top?.banner ?? null} |
| 485 | 485 | ${needs.map(forgeCard)} ${first.map((r) => mutableRow(ctx, r, picked))} |
src/web/style/content.ts +13 −13
| @@ -1086,8 +1086,8 @@ textarea.editor:focus-visible { outline: none; } | ||
| 1086 | 1086 | } |
| 1087 | 1087 | /* What needs you, pinned on the home: the bulk step's select as wide as |
| 1088 | 1088 | its words, not the column. */ |
| 1089 | .needs-you #asks-bulk select { width: auto; max-width: 100%; } | |
| 1090 | .needs-you #asks-bulk .input[name="reason"] { flex: 1 1 12rem; width: auto; min-width: 0; } | |
| 1089 | #needs-you #asks-bulk select { width: auto; max-width: 100%; } | |
| 1090 | #needs-you #asks-bulk .input[name="reason"] { flex: 1 1 12rem; width: auto; min-width: 0; } | |
| 1091 | 1091 | /* A row's box sits in a label, so a phone's thumb has 44 px to hit. */ |
| 1092 | 1092 | .ask-pick-hit { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; } |
| 1093 | 1093 | /* A phone shows the top asks and one "N more" that opens in place to all |
| @@ -1096,19 +1096,19 @@ textarea.editor:focus-visible { outline: none; } | ||
| 1096 | 1096 | stay, so every ask is still a tap away. */ |
| 1097 | 1097 | @supports selector(::details-content) { |
| 1098 | 1098 | @media (max-width: 480px) { |
| 1099 | .needs-you #asks-more > summary { display: none; } | |
| 1100 | .needs-you #asks-more::details-content { content-visibility: visible; } | |
| 1101 | .needs-you #asks-more > .stack { margin-top: 0; } | |
| 1099 | #needs-you #asks-more > summary { display: none; } | |
| 1100 | #needs-you #asks-more::details-content { content-visibility: visible; } | |
| 1101 | #needs-you #asks-more > .stack { margin-top: 0; } | |
| 1102 | 1102 | } |
| 1103 | 1103 | @media (min-width: 481px) { |
| 1104 | .needs-you #asks-phone-more > summary { display: none; } | |
| 1105 | .needs-you #asks-phone-more::details-content { content-visibility: visible; } | |
| 1106 | .needs-you #asks-phone-more > .stack { margin-top: 0; } | |
| 1107 | .needs-you .ask-group > summary { display: none; } | |
| 1108 | .needs-you .ask-group::details-content { content-visibility: visible; } | |
| 1104 | #needs-you #asks-phone-more > summary { display: none; } | |
| 1105 | #needs-you #asks-phone-more::details-content { content-visibility: visible; } | |
| 1106 | #needs-you #asks-phone-more > .stack { margin-top: 0; } | |
| 1107 | #needs-you .ask-group > summary { display: none; } | |
| 1108 | #needs-you .ask-group::details-content { content-visibility: visible; } | |
| 1109 | 1109 | } |
| 1110 | 1110 | } |
| 1111 | .needs-you .ask-group > summary { padding: 10px 14px; cursor: pointer; } | |
| 1111 | #needs-you .ask-group > summary { padding: 10px 14px; cursor: pointer; } | |
| 1112 | 1112 | /* An ask's actions: the decider, then the rest. Wide: in a line, as ever. |
| 1113 | 1113 | The kind label is for a phone, where the sentence under the title goes. */ |
| 1114 | 1114 | .ask-acts { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 12px; } |
| @@ -1128,7 +1128,7 @@ textarea.editor:focus-visible { outline: none; } | ||
| 1128 | 1128 | go; the page's 16 px gutter stays), the title wraps under "kind · repo", |
| 1129 | 1129 | the decider sits at the trailing edge and the rest wait behind "More". */ |
| 1130 | 1130 | @media (max-width: 480px) { |
| 1131 | .needs-you .ask-card { | |
| 1131 | #needs-you .ask-card { | |
| 1132 | 1132 | margin-inline: -16px; border-inline: 0; border-radius: 0; box-shadow: none; background: none; |
| 1133 | 1133 | } |
| 1134 | 1134 | .ask-card .card-head { padding: 12px 16px 0; background: none; border: 0; gap: 4px 8px; align-items: baseline; } |
| @@ -1142,7 +1142,7 @@ textarea.editor:focus-visible { outline: none; } | ||
| 1142 | 1142 | .ask-card .ask-short { display: none; } |
| 1143 | 1143 | .ask-card .ask-waiting { order: 3; flex: 0 1 auto; margin-left: 0; min-width: 0; max-width: 100%; white-space: normal; } |
| 1144 | 1144 | .ask-card .ask-pick-hit { order: 0; } |
| 1145 | .needs-you .ask-pick-hit { min-width: 44px; min-height: 44px; } | |
| 1145 | #needs-you .ask-pick-hit { min-width: 44px; min-height: 44px; } | |
| 1146 | 1146 | .ask-card .ask-pick { min-width: 24px; min-height: 24px; } |
| 1147 | 1147 | .ask-card .ask-group > summary { padding: 10px 16px; min-height: 44px; } |
| 1148 | 1148 | .ask-card .ask-group .row { flex-wrap: wrap; padding-inline: 16px; } |
test/asks-rank.test.ts +6 −4
| @@ -254,8 +254,8 @@ describe("asks that earn their place", () => { | ||
| 254 | 254 | // Only where the browser can hold a details element open: an older one |
| 255 | 255 | // keeps both summaries, so no ask is out of reach. |
| 256 | 256 | const held = css.slice(css.indexOf("@supports selector(::details-content) {")); |
| 257 | expect(held).toContain("@media (max-width: 480px) {\n .needs-you #asks-more > summary { display: none; }"); | |
| 258 | expect(held).toContain("@media (min-width: 481px) {\n .needs-you #asks-phone-more > summary { display: none; }"); | |
| 257 | expect(held).toContain("@media (max-width: 480px) {\n #needs-you #asks-more > summary { display: none; }"); | |
| 258 | expect(held).toContain("@media (min-width: 481px) {\n #needs-you #asks-phone-more > summary { display: none; }"); | |
| 259 | 259 | expect(css.indexOf("#asks-more > summary")).toBeGreaterThan(css.indexOf("@supports selector(::details-content)")); |
| 260 | 260 | // Arriving at #needs-you, the first Tab lands on an ask, not the bulk step. |
| 261 | 261 | const bulk = asks.indexOf('id="asks-bulk"'); |
| @@ -350,7 +350,9 @@ describe("asks that earn their place", () => { | ||
| 350 | 350 | expect(asks).toMatch(/class="ask-acts">\s*<button[^>]*data-variant="primary"/); |
| 351 | 351 | const css = await (await fetch(`${servers.url}${home.match(/href="(\/[^"]*\.css[^"]*)"/)![1]}`)).text(); |
| 352 | 352 | expect(css).toContain(".ask-acts > .btn { min-height: 44px;"); |
| 353 | expect(css).toContain(".needs-you .ask-card {\n margin-inline: -16px;"); | |
| 353 | // The markup lost the .needs-you class (#4189): no selector may still need it. | |
| 354 | expect(css).not.toMatch(/\.needs-you\b/); | |
| 355 | expect(css).toContain("#needs-you .ask-card {\n margin-inline: -16px;"); | |
| 354 | 356 | }); |
| 355 | 357 | |
| 356 | 358 | test("a collapsed row that ranks for security names which of its asks it is", async () => { |
| @@ -368,7 +370,7 @@ describe("asks that earn their place", () => { | ||
| 368 | 370 | const css = await (await fetch(`${servers.url}${asks.match(/href="(\/[^"]*\.css[^"]*)"/)![1]}`)).text(); |
| 369 | 371 | const wide = css.slice(css.indexOf("@supports selector(::details-content) {")); |
| 370 | 372 | expect(wide.slice(wide.indexOf("@media (min-width: 481px)"))).toContain( |
| 371 | ".needs-you .ask-group > summary { display: none; }", | |
| 373 | "#needs-you .ask-group > summary { display: none; }", | |
| 372 | 374 | ); |
| 373 | 375 | }); |
| 374 | 376 | }); |
test/asks.test.ts +7
| @@ -166,6 +166,13 @@ describe("the asks page", () => { | ||
| 166 | 166 | expect(sentence(page)).toBe("1 thing needs you"); |
| 167 | 167 | expect(page).toContain("Bot did a thing"); |
| 168 | 168 | expect(headerCount(page)).toBe(1); |
| 169 | // The section is §5's .stack: the page-local .needs-you class is gone | |
| 170 | // from the markup, while the words, links and forms stay (#4189). | |
| 171 | expect(page).toContain('<section id="needs-you" class="stack">'); | |
| 172 | expect(page).not.toContain("needs-you "); | |
| 173 | expect(page).not.toMatch(/class="[^"]*\bneeds-you\b/); | |
| 174 | expect(page).toContain('href="/'); | |
| 175 | expect(page).toMatch(/<form [^>]*method="post"/); | |
| 169 | 176 | // The home counts it in the header's Asks link; the dev dashboard's |
| 170 | 177 | // own "needs you" line went with the dashboard (#625). |
| 171 | 178 | const home = await get(alice, "/asks"); |
new above
offline