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

The /boards forms lay out with §5's `.bar`, and `.bform` leaves the stylesheet #5758

Merged opened this

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

Merged

scout/forge:scout/4751-the-boards-forms-lay-out-with-5-s-bar-an → main 2 commits fork 3 threads · 3 unresolved

  • Merged. scout/4751-the-boards-forms-lay-out-with-5-s-bar-an was merged into main as cf7dd3d.

Review

Sign in to review this change.
5 files changed +14 −19
docs/design/areas/boards.md −1
@@ -1,3 +1,2 @@
11 # Boards

This file is now only a heading. Remove it, or use it for the next boards-specific component.

22
3 - `.bform` — the boards form: one row of labelled fields and a button that makes, edits or acts on a board, wrapping onto more lines as the width runs out, with the button sized to its words. Written by `boardsList` (make a board), `panelForm` and `manage` (add or edit a panel; rename, settings, duplicate, delete) in `src/web/pages/boards-view.ts`, and by `boardsPage` (the query form and pin to a board) in `src/web/pages/boards.ts`. Styled in `src/web/style/content.ts` (`.bform` and its `.qf`, `.field` and `.btn` children).
src/web/pages/boards-view.ts +6 −6
@@ -83,7 +83,7 @@ export async function boardsList(ctx: Ctx): Promise<Response> {
8383 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
8484 const listed = await timePhaseAsync(ctx.request, "boards", () => listAllBoards(ctx.app, ctx.auth.user));
8585 if (!listed.ok) return notFoundPage(ctx);
86 const make = html`<form method="post" action="/boards/new" class="bform">
86 const make = html`<form method="post" action="/boards/new" class="bar">
8787 ${field({ label: "New board", name: "title", id: "new-board-title", required: true, maxlength: MAX_TITLE, placeholder: "Title" })}
8888 ${btn("Make board", { variant: "primary" })}
8989 </form>`;
@@ -116,7 +116,7 @@ export async function boardsList(ctx: Ctx): Promise<Response> {
116116
117117 function panelForm(slug: string, op: string, p: Panel | undefined, index: number, fields: Html): Html {
118118 const id = op === "edit" ? `panel-${String(index)}` : "panel-new";
119 return html`<form method="post" action="/boards/${slug}/${op}" class="bform">
119 return html`<form method="post" action="/boards/${slug}/${op}" class="bar">
120120 ${op === "edit" ? html`<input type="hidden" name="index" value="${String(index)}" />` : null}
121121 ${field({ label: "Panel title", name: "title", id: `${id}-title`, required: true, maxlength: MAX_TITLE, value: p?.title ?? "", placeholder: "Panel title" })}
122122 ${selectField({ label: "Width", name: "w", id: `${id}-w`, options: sizes(MAX_W, "wide", p?.w ?? 1) })}
@@ -164,11 +164,11 @@ function manage(slug: string, board: Board, adding: Html, pins: ReturnType<typeo
164164 () => card(panelForm(slug, "add", undefined, 0, adding), { head: html`<h2>Add a panel</h2>` }),
165165 )}
166166 ${card(
167 html`<form method="post" action="/boards/${slug}/rename" class="bform">
167 html`<div class="bar"><form method="post" action="/boards/${slug}/rename" class="bar">
168168 ${field({ label: "Board title", name: "title", id: "board-title", required: true, maxlength: MAX_TITLE, value: board.title })}
169169 ${btn("Rename")}
170170 </form>
171 <form method="post" action="/boards/${slug}/settings" class="bform">
171 <form method="post" action="/boards/${slug}/settings" class="bar">
172172 ${selectField({ label: "Range", name: "range", id: "board-range", options: RANGES.map((r) => ({ value: r, label: r, selected: r === board.range })) })}
173173 ${selectField({ label: "Live", name: "refresh", id: "board-refresh", options: [
174174 { value: "off", label: "off", selected: board.refresh === "off" },
@@ -176,8 +176,8 @@ function manage(slug: string, board: Board, adding: Html, pins: ReturnType<typeo
176176 ] })}
177177 ${btn("Save settings")}
178178 </form>
179 <form method="post" action="/boards/${slug}/duplicate" class="bform">${btn("Duplicate")}</form>
180 <form method="post" action="/boards/${slug}/delete" class="bform">${btn("Delete this board", { variant: "danger" })}</form>`,
179 <form method="post" action="/boards/${slug}/duplicate">${btn("Duplicate")}</form>

Bare forms here get no gap, and .bar no longer supplies the old 8px bottom margin. Duplicate and Delete touch the forms above and each other. Wrap the two in a .bar so they get its gap and wrap on narrow screens.

180 <form method="post" action="/boards/${slug}/delete">${btn("Delete this board", { variant: "danger" })}</form></div>`,
181181 { head: html`<h2>Board</h2>` },
182182 )}`;
183183 }
src/web/pages/boards.ts +2 −2
@@ -214,7 +214,7 @@ export async function boardsPage(ctx: Ctx): Promise<Response> {
214214 );
215215
216216 const form = card(
217 html`<form method="get" action="/boards/data" class="bform">
217 html`<form method="get" action="/boards/data" class="bar">
218218 ${queryFields(p)}
219219 ${btn("Draw", { variant: "primary" })}
220220 </form>`,
@@ -257,7 +257,7 @@ export async function boardsPage(ctx: Ctx): Promise<Response> {
257257 boards.length === 0 || !answer.ok
258258 ? null
259259 : card(
260 html`<form method="post" action="/boards/pin" class="bform">
260 html`<form method="post" action="/boards/pin" class="bar">
261261 ${PANEL_FIELDS.map(
262262 (k) => html`<input type="hidden" name="${k}" value="${p[k as keyof Params]}" />`,
263263 )}
src/web/style/content.ts −3
@@ -194,9 +194,6 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
194194 .chart-box.is-table { aspect-ratio: auto; height: 260px; overflow: auto; }
195195 .bp-h2 .chart-box.is-table { height: 400px; }
196196 .bp-h3 .chart-box.is-table { height: 540px; }
197 .bform { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px 12px; margin-bottom: 8px; }
198 .bform .field { margin: 0; flex: 1 1 140px; min-width: 0; }
199 .bform .btn { flex: none; }
200197 #summary { display: flex; flex-wrap: wrap; gap: 6px; }
201198 #summary .pill { max-width: 100%; white-space: normal; }
202199 .bgrid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
test/boards-selectors.test.ts +6 −7
@@ -56,7 +56,8 @@ test("every query selector has a visible label and sits outside .row-actions", a
5656 const form = html.match(/<form method="get" action="\/boards\/data"[^>]*>([\s\S]*?)<\/form>/);
5757 expect(form).not.toBeNull();
5858 expect(form![0]).not.toContain("row-actions");
59 expect(form![0]).toContain('class="bform"');
59 expect(form![0]).toContain('class="bar"');
outdated

Missing semicolon, unlike the line below it. The type check did not run either, so run bun x tsc --noEmit after bun install.

60 expect(html).not.toContain("bform");
6061 for (const name of ["chart", "source", "aggregate", "column", "by", "series", "from", "to", "fcolumn", "fop", "fvalue"]) {
6162 const m = form![1]!.match(new RegExp(`<label for="f-${name}-\\d+">([^<]+)</label>\\s*<(?:select|input)[^>]* name="${name}"`));
6263 expect(m, name).not.toBeNull();
@@ -64,12 +65,10 @@ test("every query selector has a visible label and sits outside .row-actions", a
6465 }
6566 });
6667
67 test("the selector styles never hide or collapse a control", () => {
68 const css = components + content;
69 const rules = [...css.matchAll(/(^|\})\s*([^{}]*\.bform[^{}]*)\{([^}]*)\}/g)].map((m) => m[3]!);
70 expect(rules.length).toBeGreaterThan(0);
71 for (const body of rules) {
72 expect(body).not.toMatch(/opacity:\s*0\b|display:\s*none|visibility:\s*hidden|height:\s*0\b/);
68 test("no .bform rule is left in the stylesheet or the boards pages", async () => {
69 expect(components + content).not.toContain("bform");
70 for (const f of ["boards-view.ts", "boards.ts"]) {
71 expect(await Bun.file(`${import.meta.dir}/../src/web/pages/${f}`).text()).not.toContain("bform");
7372 }
7473 });
7574

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs