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

The Later gate's proposal fits one screen at 390 px #9192

Merged opened this

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

Merged

scout/forge:scout/8971-the-later-gate-s-proposal-fits-one-scree → main 3 commits fork

  • Merged. scout/8971-the-later-gate-s-proposal-fits-one-scree was merged into main as faf8ad5.

Review

Sign in to review this change.
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 {
9999 </li>`;
100100 }
101101
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
102106 /** A group is one line: its name and its counts by verdict, opening to its issues. */
103107 function themeLine(base: string, mayLift: boolean, t: Theme): Html {
104108 return disclosure(
@@ -131,26 +135,36 @@ export function heldPage(ctx: Ctx): Response {
131135 const body = html`${repoHeader(ctx, repo, "issues", { compact: true })}
132136 <div class="page-head mt">
133137 <h1>Held by the Later gate</h1>
134 <p class="muted">
138 <p class="muted small">
135139 ${review.held} open ${review.held === 1 ? "issue waits" : "issues wait"}
136140 behind <a href="${base}/issues/${gate.number}">the gate</a>, set by
137141 ${gate.authorName} ${time(gate.createdAt)}. The crew picks none of
138142 them until it is lifted.
139143 </p>
140144 </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 )}
154168 ${closed.length === 0
155169 ? null
156170 : disclosure(
test/later-gate.test.ts +14
@@ -268,6 +268,20 @@ describe("the Later gate in plain sight", () => {
268268 expect(rows[1]!.title).toStartWith("[x]");
269269 });
270270
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
271285 test("one approval closes what is ticked, it stays findable, and one tap revives it", async () => {
272286 const a = open("Idea nobody chose");
273287 const b = open("Idea to keep behind the gate");

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs