Public: anyone can read this; the crew can change it.
The Later gate's proposal fits one screen at 390 px #9192
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/8971-the-later-gate-s-proposal-fits-one-scree → main 3 commits fork
3 files
changed +47
−14
docs/design/areas/held.md added +5
| @@ -0,0 +1,5 @@ | ||
| 1 | # Held (the Later gate's proposal) | |
| 2 | ||
| 3 | At rest the proposal is bounded, whatever the gate holds: the heading and a one-line lede, one `.bar` (counts by verdict, then a small "Apply the ticked" in `form.inline`), at most `THEMES_AT_REST` (6) collapsed `.disclosure` theme lines, one "N more themes" disclosure holding the rest, a "How to read this" disclosure holding the help, and "Closed by the review" when it has any. That is at most 6 + 3 one-line folds. No new component class; `form.inline` (content.ts), `.stack-tight` (base.ts), `.disclosure` and the `sm` button are the existing pieces. Written by `heldPage` in `src/web/pages/gate.ts`. | |
| 4 | ||
| 5 | The terminal's held screen (`src/tui/screens/held.ts`) is already a list with one tallied theme header per group and no prose to fold, so it takes no change. |
src/web/pages/gate.ts +28 −14
| @@ -99,6 +99,10 @@ function reviewRow(base: string, mayLift: boolean, item: Triaged): Html { | ||
| 99 | 99 | </li>`; |
| 100 | 100 | } |
| 101 | 101 | |
| 102 | /** Themes drawn at rest; the rest fold into one "N more themes" line, so the | |
| 103 | * proposal's height is bounded: heading, bar, this many lines, three folds. */ | |
| 104 | export const THEMES_AT_REST = 6; | |
| 105 | ||
| 102 | 106 | /** A group is one line: its name and its counts by verdict, opening to its issues. */ |
| 103 | 107 | function themeLine(base: string, mayLift: boolean, t: Theme): Html { |
| 104 | 108 | return disclosure( |
| @@ -131,26 +135,36 @@ export function heldPage(ctx: Ctx): Response { | ||
| 131 | 135 | const body = html`${repoHeader(ctx, repo, "issues", { compact: true })} |
| 132 | 136 | <div class="page-head mt"> |
| 133 | 137 | <h1>Held by the Later gate</h1> |
| 134 | <p class="muted"> | |
| 138 | <p class="muted small"> | |
| 135 | 139 | ${review.held} open ${review.held === 1 ? "issue waits" : "issues wait"} |
| 136 | 140 | behind <a href="${base}/issues/${gate.number}">the gate</a>, set by |
| 137 | 141 | ${gate.authorName} ${time(gate.createdAt)}. The crew picks none of |
| 138 | 142 | them until it is lifted. |
| 139 | 143 | </p> |
| 140 | 144 | </div> |
| 141 | <p class="muted small">${counts.close} to close · ${counts.keep} to keep · ${counts.merge} to merge</p> | |
| 142 | <p class="muted small"> | |
| 143 | Close is the default; a keep joins a bet or a pillar's work; a merge | |
| 144 | folds a duplicate in. Nothing changes until you approve. | |
| 145 | </p> | |
| 146 | ${mayApply && review.changes.length > 0 | |
| 147 | ? html`<form id="apply" method="post" action="${base}/held/apply" class="bar"> | |
| 148 | <input type="hidden" name="digest" value="${review.digest}" /> | |
| 149 | ${btn("Apply the ticked", { type: "submit", variant: "primary" })} | |
| 150 | </form>` | |
| 151 | : null} | |
| 152 | <div class="stack mt"> | |
| 153 | ${review.themes.map((t) => themeLine(base, mayLift, t))} | |
| 145 | <div class="bar"> | |
| 146 | <span class="muted small">${counts.close} to close · ${counts.keep} to keep · ${counts.merge} to merge</span> | |
| 147 | ${mayApply && review.changes.length > 0 | |
| 148 | ? html`<form id="apply" method="post" action="${base}/held/apply" class="inline"> | |
| 149 | <input type="hidden" name="digest" value="${review.digest}" /> | |
| 150 | ${btn("Apply the ticked", { type: "submit", variant: "primary", size: "sm" })} | |
| 151 | </form>` | |
| 152 | : null} | |
| 153 | </div> | |
| 154 | <div class="stack stack-tight"> | |
| 155 | ${review.themes.slice(0, THEMES_AT_REST).map((t) => themeLine(base, mayLift, t))} | |
| 156 | ${review.themes.length > THEMES_AT_REST | |
| 157 | ? disclosure( | |
| 158 | html`${review.themes.length - THEMES_AT_REST} more ${review.themes.length - THEMES_AT_REST === 1 ? "theme" : "themes"}`, | |
| 159 | html`<div class="stack stack-tight">${review.themes.slice(THEMES_AT_REST).map((t) => themeLine(base, mayLift, t))}</div>`, | |
| 160 | { small: true }, | |
| 161 | ) | |
| 162 | : null} | |
| 163 | ${disclosure( | |
| 164 | "How to read this", | |
| 165 | html`<p class="muted small">Close is the default; a keep joins a bet or a pillar's work; a merge folds a duplicate in. Nothing changes until you approve.</p>`, | |
| 166 | { small: true }, | |
| 167 | )} | |
| 154 | 168 | ${closed.length === 0 |
| 155 | 169 | ? null |
| 156 | 170 | : disclosure( |
test/later-gate.test.ts +14
| @@ -268,6 +268,20 @@ describe("the Later gate in plain sight", () => { | ||
| 268 | 268 | expect(rows[1]!.title).toStartWith("[x]"); |
| 269 | 269 | }); |
| 270 | 270 | |
| 271 | test("the held page at rest is bounded: counts and Apply on one bar, collapsed groups, extra themes folded, help folded", async () => { | |
| 272 | const animals = ["Quokka", "Wombat", "Dingo", "Emu", "Galah", "Numbat", "Thylacine", "Bilby"]; | |
| 273 | const tails = ["alpha", "bravo", "charlie"]; | |
| 274 | for (const w of animals) for (const t of tails) held(`${w} ${t}${w.toLowerCase()}`); | |
| 275 | const page = await get("/alice/engine/held"); | |
| 276 | expect(page.indexOf("to close")).toBeLessThan(page.indexOf("Apply the ticked")); | |
| 277 | expect(page.indexOf("Apply the ticked")).toBeLessThan(page.indexOf("Quokka alpha")); | |
| 278 | expect(page).toMatch(/<summary[^>]*>\s*How to read this[\s\S]*?Nothing changes until you approve/); | |
| 279 | expect(page).toMatch(/<summary[^>]*>\s*\d+ more themes/); | |
| 280 | const details = page.match(/<details[^>]*>/g)!; | |
| 281 | expect(details.length).toBeGreaterThan(6); | |
| 282 | for (const d of details) expect(d).not.toMatch(/\bopen\b/); | |
| 283 | }); | |
| 284 | ||
| 271 | 285 | test("one approval closes what is ticked, it stays findable, and one tap revives it", async () => { |
| 272 | 286 | const a = open("Idea nobody chose"); |
| 273 | 287 | const b = open("Idea to keep behind the gate"); |
new above
offline