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

The owner's /notes page speaks the home page's language #9743

Merged opened this

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

Merged

mason/forge:mason/9731-the-owner-s-notes-page-speaks-the-home-p → main 3 commits fork 2 threads · 2 unresolved

  • Merged. mason/9731-the-owner-s-notes-page-speaks-the-home-p was merged into main as 2db5337.

Review

Sign in to review this change.
3 files changed +29 −17
docs/design/areas/notes.md +10
@@ -7,3 +7,13 @@
77 A note is whatever a person typed, so inside `[data-notes]` (an attribute, not a class) a row's title and meta wrap and
88 long words break anywhere, in place of the list's one-line ellipsis; the row aligns to its top so the badge and the
99 overrule button sit by the first line. No fixed width; the page does not scroll sideways. No new class.
10
11 ## Rows, not boxes (#9731)
12
13 The notes are a `listCard(rows, { extra: "bare" })`: rows on the page's one background with no frame (the shared
14 `.card.bare`, docs/design/areas/lists.md). A row's state is the words in its one `.note-line` ("Planned · 2 issues",
15 "Shipped in version …"), so the trailing badge is gone. The empty state is one muted line, not a card. The action,
16 `keepThought`, and the overrule button do not change. No new class.
17
18 The amber tone that building and in-review notes once carried is gone on purpose: the words are the state, and the page
19 has no other colour for them. The 390 px rule above still holds, now without the badge.
src/web/pages/notes.ts +4 −17
@@ -10,7 +10,7 @@
1010
1111 import { overruleNote } from "../../actions/note_overrule.ts";
1212 import { keepThought, THOUGHT_MAX } from "../../actions/thought.ts";
13 import { noteLine, type NotePhase } from "../../core/note_line.ts";
13 import { noteLine } from "../../core/note_line.ts";
1414 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
1515 import { publicForgeRepo } from "../../core/forge_repo.ts";
1616 import { thoughtStream } from "../../core/thought_stream.ts";
@@ -20,24 +20,12 @@ import { signInFirst } from "../chrome.ts";
2020 import { readForm } from "../form.ts";
2121 import { html } from "../html.ts";
2222 import { flashTo, page } from "../layout.ts";
23 import { badge, btn, empty, listCard, row, time, type Tone } from "../ui.ts";
23 import { btn, listCard, row, time } from "../ui.ts";
2424 import { cut } from "../../actions/ask_state_words.ts";
2525
2626 const HERE = "/notes";
2727 const SHOWN = 100;
2828
29 const TONE: Record<NotePhase, Tone> = {
30 waiting: "default",
31 declined: "default",
32 duplicate: "default",
33 snoozed: "default",
34 planned: "default",
35 building: "attn",
36 review: "attn",
37 shipped: "success",
38 superseded: "default",
39 };
40
4129 /** Static code, never built from user text. */
4230 export const notesJs = `
4331 const form = document.querySelector("form.note-box");
@@ -82,7 +70,6 @@ export async function notesView(ctx: Ctx): Promise<Response> {
8270 meta: html`<span class="note-line">${time(n.receivedAt)} · ${line.text}${line.links.length === 0 ? null : " · "}${line.links.map(

Removing the badge changes what a person sees (state is now only inside the meta line). Add a test that fails without it: the row has no badge and .note-line holds the state words.

8371 (l) => html`<a href="${l.href}">${l.label}</a>`,
8472 )}</span>`,
85 trail: badge(line.phase === "review" ? "In review" : line.phase[0]!.toUpperCase() + line.phase.slice(1), { tone: TONE[line.phase] }),
8673 ...(line.overrule === null
8774 ? {}
8875 : {
@@ -99,8 +86,8 @@ export async function notesView(ctx: Ctx): Promise<Response> {
9986 <div class="bar"><span class="muted small">Enter saves · Shift+Enter for a new line</span><span class="spacer"></span>${btn("Save", { variant: "primary" })}</div>
10087 </form>
10188 ${rows.length === 0
102 ? empty({ icon: "comment", title: "No notes yet", note: "Write an idea above. The crew plans it, builds it, and this page says where it is." })
103 : listCard(rows)}</div>`;
89 ? html`<p class="muted small">No notes yet. Write one above.</p>`
outdated

Principle 1 and 8: the empty line is two sentences that explain the page twice. Say one thing, e.g. "No notes yet. Write one above." Also this branch (empty vs bare list) has no test; add one.

90 : listCard(rows, { extra: "bare" })}</div>`;
10491 return timePhase(ctx.request, "render", () =>
10592 page({ ctx, title: "Notes", body, styleCss: NOTES_CSS, scriptJs: notesJs, description: "Write an idea; watch it become shipped work." }),
10693 );
test/notes.test.ts +15
@@ -92,6 +92,7 @@ describe("notes", () => {
9292 let servers: Servers;
9393 let alice: User;
9494 let bob: User;
95 let carol: User;
9596 let repo: Repo;
9697 const fresh = () => getRepoById(app.db, repo.id)!;
9798 const cookie = (u: User) => `forge_session=${createSession(app.db, u.id).token}`;
@@ -106,6 +107,7 @@ describe("notes", () => {
106107 };
107108 alice = await mk("alice", true);
108109 bob = await mk("bob");
110 carol = await mk("carol");
109111 const r = await createRepo(app, alice, { name: "forge", visibility: "private" });
110112 if (!r.ok) throw new Error(String(r.error));
111113 repo = r.value;
@@ -143,6 +145,19 @@ describe("notes", () => {
143145 expect(await (await get(bob)).text()).not.toContain("first idea");
144146 });
145147
148 test("notes are bare rows with the state in the line and no badge; none is one muted line (#9731)", async () => {
149 await post(bob, "an idea for rows");
150 const flat = (r: string) => r.slice(r.indexOf("<main"), r.indexOf("</main>")).replace(/\s+/g, " ");
151 const list = flat(await (await get(bob)).text());
152 expect(list).toContain('<div class="card bare"');
153 expect(list).not.toContain("badge");
154 expect(list).toMatch(/<span class="note-line">.*Waiting for the planner/);
155 const empty = flat(await (await get(carol)).text());
156 expect(empty).toContain('<p class="muted small">No notes yet. Write one above.</p>');
157 expect(empty).not.toContain("bare");
158 expect(empty).not.toContain('class="empty');
159 });
160
146161 test("refuses empty, oversized and cross-origin writes", async () => {
147162 const before = thoughtStream(app.db, alice).length;
148163 expect((await post(alice, " ")).status).toBe(303);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs