From 8a7b6b30d9a657dc4a856dd79a2c1a748cc32806 Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Tue, 6 Oct 2026 10:04:26 +0300 Subject: [PATCH] minimal home: the buttons carve out of the drag band; own-line summary (release polish 2.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner report (Electron): the Chains and Import-notes buttons were not clickable — clicking dragged the window. Root cause: the chrome column lacked lh-no-drag. The drag mask resolves by DOM order, and the graph chrome's column carries the carve exactly for this reason (the 2026-09-17 mask law). The minimal home's column now carries the same class on the same shape — byte-for-byte the graph chrome's law — and the pointer-events juggling is gone (it was browser-thinking; Electron needs the region carve, not the pointer filter). Summary refinement (owner, same round): every count on its OWN line — no dot separators; the two blocks (propositions/chains, teaching/ practice sessions) part with one empty line (a dedicated h-4 spacer); imported notes render as a COUNT ("4 imported notes"), never the file list. KnowledgeSummary's contract changes sources: string[] → importedNotes: number. 1419 tests (own-line pins: no " · " anywhere, h-4 parting, count-not- paths, singulars); tsc, lint, build green. --- components/minimal-home.tsx | 70 +++++++++++++++++------------- tests/render-minimal-home.test.tsx | 54 ++++++++++++++++------- 2 files changed, 77 insertions(+), 47 deletions(-) diff --git a/components/minimal-home.tsx b/components/minimal-home.tsx index 3f28dcc..b115185 100644 --- a/components/minimal-home.tsx +++ b/components/minimal-home.tsx @@ -33,39 +33,47 @@ export function KnowledgeSummary({ chains, teachSessions, practiceSessions, - sources, + importedNotes, }: { propositions: number; chains: number; teachSessions: number; practiceSessions: number; - sources: string[]; + importedNotes: number; }) { - const empty = propositions === 0 && chains === 0 && teachSessions === 0 && practiceSessions === 0 && sources.length === 0; + const empty = + propositions === 0 && chains === 0 && teachSessions === 0 && practiceSessions === 0 && importedNotes === 0; return ( -
+
+ {/* Own-line law (owner, 2026-10-06): every count sits on its own + row — no dot separators; the two blocks (graph, sessions) + part with one empty line; the notes COUNT (never the file + list) rides after the same parting. */} +
+

+ {propositions} {propositions === 1 ? "proposition" : "propositions"} +

+

+ {chains} {chains === 1 ? "chain" : "chains"} +

+
+ {/* The chrome survivors in the graph view's own places (owner, 2026-10-06): the top-right column — actions row (Chains + Import, the same 36px band pills), the cost pill BELOW it. - Node search and the render-settings gear are canvas things - and stay gone; Import ALWAYS renders (the entry-point law). */} -
-
+ The column carries lh-no-drag EXACTLY like the graph + chrome's (the DOM-order mask law): without it the drag band + swallows the clicks in Electron — the buttons dragged the + window instead. Node search and the render-settings gear are + canvas things and stay gone; Import ALWAYS renders (the + entry law). */} +
+
- {costs && costs.runs.length > 0 && ( -
- -
- )} + {costs && costs.runs.length > 0 && }
{/* The chain table — the STANDARD overlay, unchanged, behind its diff --git a/tests/render-minimal-home.test.tsx b/tests/render-minimal-home.test.tsx index 810581a..71b6c16 100644 --- a/tests/render-minimal-home.test.tsx +++ b/tests/render-minimal-home.test.tsx @@ -1,6 +1,8 @@ /** The minimal home's KnowledgeSummary — the quiet centered summary of * the knowledge base (owner, 2026-10-06: "very quiet, very minimal"). - * Pure body: props in, markup out. */ + * Pure body: props in, markup out. Own-line law (same day refinement): + * every count on its own row, NO dot separators, blocks parted by an + * empty line, imported notes as a COUNT — never the file list. */ import assert from "node:assert/strict"; import { renderToStaticMarkup } from "react-dom/server"; import React from "react"; @@ -14,40 +16,60 @@ function render(props: Partial[0]> = {}) { chains: 5, teachSessions: 3, practiceSessions: 2, - sources: [], + importedNotes: 4, ...props, }), ); } describe("KnowledgeSummary — the quiet home summary (2026-10-06)", () => { - it("renders the knowledge base in numbers: propositions · chains, teaching · practice sessions", () => { + it("every count on its OWN line — no dot separators (owner refinement, same day)", () => { const html = render(); - assert.ok(html.includes("12 propositions"), "propositions count"); - assert.ok(html.includes("5 chains"), "chains count"); - assert.ok(html.includes("3 teaching sessions"), "teaching count (plural)"); - assert.ok(html.includes("2 practice sessions"), "practice count (plural)"); - assert.ok(!html.includes("import notes"), "no empty-state hint when there is content"); + for (const line of ["12 propositions", "5 chains", "3 teaching sessions", "2 practice sessions", "4 imported notes"]) { + const i = html.indexOf(line); + assert.ok(i !== -1, `${line} renders`); + // own-line: the count is its own text node — the char after the + // label is a tag boundary (

), never " · " + const after = html.slice(i + line.length, i + line.length + 2); + assert.ok(after.startsWith("<"), `${line} ends its own element, not a dot-joined run`); + } + assert.ok(!html.includes(" · "), "no dot separators anywhere"); + }); + + it("the two blocks part with an empty line — graph counts, then session counts", () => { + const html = render(); + const graphBlock = html.indexOf("12 propositions"); + const sessionsBlock = html.indexOf("3 teaching sessions"); + assert.ok(graphBlock !== -1 && sessionsBlock !== -1); + assert.ok(sessionsBlock > graphBlock, "sessions come after the graph block"); + // the parting is a dedicated spacer element (h-4), not a paragraph + assert.ok(html.includes('class="h-4"'), "one empty line between blocks"); }); it("singularizes: one of each reads as single, not '1 sessions'", () => { - const html = render({ propositions: 1, chains: 1, teachSessions: 1, practiceSessions: 1 }); - assert.ok(html.includes("1 proposition ·")); + const html = render({ propositions: 1, chains: 1, teachSessions: 1, practiceSessions: 1, importedNotes: 1 }); + assert.ok(html.includes("1 proposition")); assert.ok(html.includes("1 chain")); assert.ok(html.includes("1 teaching session")); assert.ok(html.includes("1 practice session")); + assert.ok(html.includes("1 imported note")); }); - it("lists the imported sources — quiet truncated rows (owner: 'a list of sources that have been imported')", () => { - const html = render({ sources: ["/notes/databases.md", "/notes/laravel.md"] }); - assert.ok(html.includes("/notes/databases.md")); - assert.ok(html.includes("/notes/laravel.md")); - assert.ok(html.includes('class="w-full truncate'), "each row truncates — long paths never blow the column"); + it("imported notes are a COUNT — never the file list (owner: 'the number of imported notes')", () => { + const html = render({ importedNotes: 7 }); + assert.ok(html.includes("7 imported notes")); + assert.ok(!html.includes(".md"), "no file paths render"); + assert.ok(!html.includes("truncate"), "no truncating source rows"); }); it("the empty knowledge base says so, quietly — import is the entry point", () => { - const html = render({ propositions: 0, chains: 0, teachSessions: 0, practiceSessions: 0 }); + const html = render({ propositions: 0, chains: 0, teachSessions: 0, practiceSessions: 0, importedNotes: 0 }); assert.ok(html.includes("Nothing here yet")); assert.ok(html.includes("import notes"), "the hint names the entry point"); }); + + it("non-empty shows no empty-state hint", () => { + const html = render(); + assert.ok(!html.includes("Nothing here yet")); + }); });