Public: anyone can read this; the crew can change it.
Each proposed bet card fits one screen at 390 px with no horizontal scroll #9191
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/8970-each-proposed-bet-card-fits-one-screen-a → main 4 commits fork 3 threads · 3 unresolved
6 files
changed +66
−6
docs/design/areas/bets.md +17
| @@ -9,3 +9,20 @@ proposed appetite, and a second `<details>` ("Change the share") holding the | ||
| 9 | 9 | share field. When `mayBack` refuses (5 running, or the week would be passed) |
| 10 | 10 | the button is `disabled` and the reason is a `muted` note beside it. No new |
| 11 | 11 | class; the card is stacked blocks, so at 390 px it needs no horizontal scroll. |
| 12 | ||
| 13 | ### One screen at 390 px (#8970) | |
| 14 | ||
| 15 | At rest the card shows the name, at most three journeys (the rest behind a | |
| 16 | "N more" disclosure), the issue count, the button and the cut button; the | |
| 17 | share field stays folded. The card carries `data-bet-card` (an attribute, not | |
| 18 | a class) which wraps long words anywhere and tightens block margins, so a long | |
| 19 | name or journey neither scrolls sideways nor pushes the button off the screen. | |
| 20 | Desk and phone looks are attached to the pull from its preview. | |
| 21 | ||
| 22 | **Departure, named.** `data-bet-card` is not a variant of `card()`: the card | |
| 23 | itself is the stock one. The attribute only scopes three rules inside it | |
| 24 | (wrap anywhere, `8px` block margins, `16px` list indent, all from the 4px | |
| 25 | scale in DESIGN §4; there are no spacing custom properties to use). It is an | |
| 26 | attribute because the area adds no component class. The preview seed holds a | |
| 27 | third proposed bet with a long name and an unbroken address as a journey, so | |
| 28 | the worst case is on `/preview-admin/sample/bets`. |
src/spawn/preview-seed.ts +8 −2
| @@ -323,13 +323,13 @@ async function seedWave(app: App, admin: User, repo: Repo, pullTitle: string): P | ||
| 323 | 323 | |
| 324 | 324 | /** One bet backed at 20% of the week and one proposed, over the open issues. */ |
| 325 | 325 | function seedBets(app: App, admin: User, repo: Repo, open: readonly Issue[]): Result<void, string> { |
| 326 | const bet = (name: string, from: number) => | |
| 326 | const bet = (name: string, from: number, journeys = "Someone can see it") => | |
| 327 | 327 | insertBet( |
| 328 | 328 | app.db, |
| 329 | 329 | { |
| 330 | 330 | repoId: repo.id, |
| 331 | 331 | name, |
| 332 | journeys: "Someone can see it", | |
| 332 | journeys, | |
| 333 | 333 | appetite: 20, |
| 334 | 334 | leadId: null, |
| 335 | 335 | issueIds: open.slice(from, from + 2).map((i) => i.id), |
| @@ -338,6 +338,12 @@ function seedBets(app: App, admin: User, repo: Repo, open: readonly Issue[]): Re | ||
| 338 | 338 | ); |
| 339 | 339 | const backed = bet("Show the week", 2); |
| 340 | 340 | bet("Price the list", 4); |
| 341 | // The worst case for the phone: a long name, an unbroken word, more journeys than fit at rest (#8970). | |
| 342 | bet( | |
| 343 | "Make every long and winding bet name wrap instead of pushing the page sideways", | |
| 344 | 0, | |
| 345 | ["Someone opens https://example.com/a/very/long/unbroken/address/that/has/no/spaces/at/all/anywhere", "Second journey", "Third journey", "Fourth journey", "Fifth journey"].join("\n"), | |
| 346 | ); | |
| 341 | 347 | const result = backBet(app, admin, repo, backed.id, 20); |
| 342 | 348 | return result.ok ? ok() : err(`backing a bet: ${result.error.message}`); |
| 343 | 349 | } |
src/web/pages/bets.ts +8 −3
| @@ -71,6 +71,9 @@ function oneButton(repo: Repo, b: Bet, verb: string, label: string, variant: "da | ||
| 71 | 71 | return html`<form method="post" action="${post(repo, b, verb)}">${btn(label, { variant })}</form>`; |
| 72 | 72 | } |
| 73 | 73 | |
| 74 | /** Journeys shown at rest; the rest sit behind a disclosure so the card stays one screen tall. */ | |
| 75 | const SHOWN_JOURNEYS = 3; | |
| 76 | ||
| 74 | 77 | /** |
| 75 | 78 | * A proposed bet as one card for the owner: name, journeys, its issues as a |
| 76 | 79 | * count that opens the list, and one button that backs it at the proposed |
| @@ -87,8 +90,10 @@ function proposedCard(ctx: Ctx, repo: Repo, b: Bet, backed: readonly Bet[]): Htm | ||
| 87 | 90 | <ul>${scopes.map((s) => html`<li><a href="${repoBase(repo)}/issues/${s.number}">#${s.number} ${s.title}</a></li>`)}</ul> |
| 88 | 91 | </details>`; |
| 89 | 92 | return card( |
| 90 | html`<h2><a href="${betHref(repo, b.id)}">${b.name}</a></h2> | |
| 91 | ${journeys.length === 0 ? null : html`<ul>${journeys.map((j) => html`<li>${j}</li>`)}</ul>`} | |
| 93 | html`<div data-bet-card><h2><a href="${betHref(repo, b.id)}">${b.name}</a></h2> | |
| 94 | ${journeys.length === 0 ? null : html`<ul>${journeys.slice(0, SHOWN_JOURNEYS).map((j) => html`<li>${j}</li>`)}</ul>`} | |
| 95 | ${journeys.length <= SHOWN_JOURNEYS ? null : html`<details><summary>${journeys.length - SHOWN_JOURNEYS} more ${journeys.length - SHOWN_JOURNEYS === 1 ? "journey" : "journeys"}</summary> | |
| 96 | <ul>${journeys.slice(SHOWN_JOURNEYS).map((j) => html`<li>${j}</li>`)}</ul></details>`} | |
| 92 | 97 | ${issues} |
| 93 | 98 | <form method="post" action="${post(repo, b, "back")}"> |
| 94 | 99 | <input type="hidden" name="share" value="${String(b.appetite)}"> |
| @@ -96,7 +101,7 @@ function proposedCard(ctx: Ctx, repo: Repo, b: Bet, backed: readonly Bet[]): Htm | ||
| 96 | 101 | ${why === null ? null : html`<p class="muted" role="note">${why[0]!.toUpperCase() + why.slice(1)}.</p>`} |
| 97 | 102 | </form> |
| 98 | 103 | <details class="mt"><summary>Change the share</summary>${backForm(repo, b, "Back it")}</details> |
| 99 | <div class="mt">${oneButton(repo, b, "cut", "Cut it", "danger")}</div>`, | |
| 104 | <div class="mt">${oneButton(repo, b, "cut", "Cut it", "danger")}</div></div>`, | |
| 100 | 105 | ); |
| 101 | 106 | } |
| 102 | 107 |
src/web/style/content.ts +3 −1
| @@ -145,7 +145,9 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); } | ||
| 145 | 145 | font-variant-numeric: tabular-nums; letter-spacing: 0.14em; |
| 146 | 146 | text-align: center; user-select: all; word-break: break-all; |
| 147 | 147 | } |
| 148 | [data-apps] { overflow-wrap: anywhere; } | |
| 148 | [data-apps], [data-bet-card] { overflow-wrap: anywhere; } | |
| 149 | [data-bet-card] h2, [data-bet-card] ul, [data-bet-card] p { margin-block: 8px; } | |
outdated
Literal 6px and 18px: use the spacing tokens, or say why not. |
||
| 150 | [data-bet-card] ul { padding-inline-start: 16px; } | |
| 149 | 151 | |
| 150 | 152 | .bar > .device-code { flex: 1 1 auto; } |
| 151 | 153 |
test/bets.test.ts +29
| @@ -579,3 +579,32 @@ describe("a proposed bet is one card backed with one tap", () => { | ||
| 579 | 579 | expect(shown).toContain("5 bets already run"); |
| 580 | 580 | }); |
| 581 | 581 | }); |
| 582 | ||
| 583 | describe("a proposed bet card stays one screen tall (#8970)", () => { | |
| 584 | const card = async (journeys: string) => { | |
| 585 | app.db.query("DELETE FROM bet WHERE repo_id = ?1").run(repo.id); | |
| 586 | const cookie = await signIn("alice"); | |
| 587 | const b = fresh2("Many"); | |
| 588 | app.db.query("UPDATE bet SET journeys = ?1 WHERE id = ?2").run(journeys, b.id); | |
| 589 | return await (await fetch(`${servers.url}/alice/notes/bets`, { headers: { cookie } })).text(); | |
| 590 | }; | |
| 591 | test("three journeys at rest; the rest sit inside a details that names them", async () => { | |
| 592 | const out = await card("j1\nj2\nj3\nj4\nj5"); | |
| 593 | expect(out).toContain("data-bet-card"); | |
outdated
Assert j4 and j5 sit inside the details and not at rest; add the 3-or-fewer case with no disclosure. |
||
| 594 | const [rest, folded] = out.split(/<details>\s*<summary>2 more journeys<\/summary>/); | |
| 595 | expect(folded).toBeDefined(); | |
| 596 | for (const j of ["j1", "j2", "j3"]) expect(rest).toContain(`<li>${j}</li>`); | |
| 597 | for (const j of ["j4", "j5"]) { | |
| 598 | expect(rest).not.toContain(`<li>${j}</li>`); | |
| 599 | expect(folded!.split("</details>")[0]).toContain(`<li>${j}</li>`); | |
| 600 | } | |
| 601 | }); | |
| 602 | test("three or fewer journeys have no disclosure", async () => { | |
| 603 | const out = await card("j1\nj2\nj3"); | |
| 604 | expect(out).toContain("<li>j3</li>"); | |
| 605 | expect(out).not.toMatch(/more journeys?</); | |
| 606 | }); | |
| 607 | test("one folded journey reads in the singular", async () => { | |
| 608 | expect(await card("j1\nj2\nj3\nj4")).toContain("1 more journey</summary>"); | |
| 609 | }); | |
| 610 | }); |
test/looks-owner.test.ts +1
| @@ -134,6 +134,7 @@ describe("the preview seed", () => { | ||
| 134 | 134 | expect(bets).toEqual([ |
| 135 | 135 | ["Show the week", "backed", 20], |
| 136 | 136 | ["Price the list", "proposed", 20], |
| 137 | ["Make every long and winding bet name wrap instead of pushing the page sideways", "proposed", 20], | |
| 137 | 138 | ]); |
| 138 | 139 | |
| 139 | 140 | // The sample account that prices the week has no grant, so no run or |
new above
offline
Summary reads "2 more" with no noun. Use "2 more journeys".