From 976be50a817673c5c9a001f8741c133eb1183e6a Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Wed, 23 Sep 2026 18:53:51 +0300 Subject: [PATCH] Polish round: node-delete chrome + rename alignment Owner polish dispatch (2026-09-23): - Right-click no longer pins a node or opens the detail card: the canvas pointerdown/up handlers now gate on the primary button, so the context menu arrives alone. - Node context menu: label is just "Delete" and the shell hugs its content (w-44 -> w-fit); the menu body keeps focus for arrow cycling but never shows the browser's default focus ring (outline-none on the MenuShell body - applies to every menu). - Delete confirm: "Delete node?" title in CMU Serif, red (text-destructive); the node name sits below in JetBrains Mono inline-code; the Delete button carries the destructive treatment (border + tint, fill deepens on hover). Rest of the dialog stays neutral. - Sidebar rename row: text and metadata stay at the idle row's x - the status spacer and the input's border/padding are gone; the underline is the only editing cue. --- components/delete-node-dialog.tsx | 19 +++++++++++++++++-- components/graph-canvas.ts | 6 ++++++ components/graph-view.tsx | 4 ++-- components/menu-shell.tsx | 7 ++++++- components/sidebar.tsx | 14 ++++++++++---- tests/delete-node-dialog.test.tsx | 8 +++++++- tests/session-context.test.tsx | 8 ++++++++ 7 files changed, 56 insertions(+), 10 deletions(-) diff --git a/components/delete-node-dialog.tsx b/components/delete-node-dialog.tsx index 8c71e9d..a663f93 100644 --- a/components/delete-node-dialog.tsx +++ b/components/delete-node-dialog.tsx @@ -79,7 +79,19 @@ export function DeleteNodeBody({ onClick={(e) => e.stopPropagation()} className="w-[32rem] max-w-[calc(100vw-2rem)] rounded-xl border bg-card p-5 shadow-xl" > -

Delete “{node.name}”?

+

Delete node?

+ {/* The node name below the title in JetBrains Mono inline-code + (owner, polish round): topic names are short technical + handles — mono suits them; the serif title keeps the + family voice ("Delete node?", like "Delete "session"?"). */} +
+ + {node.name} + +
{impact === "loading" && (

Reading what this would remove… @@ -158,7 +170,10 @@ export function DeleteNodeBody({ data-testid="node-delete-confirm" onClick={onRun} disabled={busy} - className="rounded-sm border border-border/60 px-3 py-1.5 text-xs text-foreground transition-colors hover:bg-accent disabled:opacity-40" + // Destructive red (owner, polish round) — the + // app's established destructive pair (chat's + // refusal banner): border + text, fill on hover. + className="rounded-sm border border-destructive/50 bg-destructive/10 px-3 py-1.5 text-xs text-destructive transition-colors hover:bg-destructive/20 disabled:opacity-40" > {busy ? "Deleting…" : "Delete"} diff --git a/components/graph-canvas.ts b/components/graph-canvas.ts index 375713a..2017262 100644 --- a/components/graph-canvas.ts +++ b/components/graph-canvas.ts @@ -868,6 +868,11 @@ export class GraphCanvasRenderer { }; private onPointerDown = (e: PointerEvent) => { + // Primary button only (owner, polish round): a right-press must not + // pin the node — right-click belongs to the context menu alone, and + // the right-pointerup must never read as a click (the detail card + // used to open under the menu). + if (e.button !== 0) return; const pos = this.eventPos(e); const node = this.pickAt(pos.x, pos.y); if (node && this.sim) { @@ -898,6 +903,7 @@ export class GraphCanvasRenderer { }; private onPointerUp = (e: PointerEvent) => { + if (e.button !== 0) return; // right-click is context-menu territory // Node press ending: click (no slop) → select; drag → release-and-relax. if (this.dragId) { const id = this.dragId; diff --git a/components/graph-view.tsx b/components/graph-view.tsx index d13b2d3..573ae62 100644 --- a/components/graph-view.tsx +++ b/components/graph-view.tsx @@ -1241,7 +1241,7 @@ export function GraphView() { anchorRef={ctxAnchorRef} testid="node-context-menu" preferredSide="down" - menuClass="flex w-44 flex-col gap-0.5 rounded-xl border bg-card p-2 text-xs shadow-xl" + menuClass="flex w-fit flex-col gap-0.5 rounded-xl border bg-card p-2 text-xs shadow-xl" > )} diff --git a/components/menu-shell.tsx b/components/menu-shell.tsx index 863c98b..d2115e5 100644 --- a/components/menu-shell.tsx +++ b/components/menu-shell.tsx @@ -246,7 +246,12 @@ export function MenuShell({ tabIndex={-1} onKeyDown={onBodyKeyDown} style={{ position: "fixed", zIndex: 60, ...placed.style, maxHeight: placed.maxHeight, overflowY: "auto" }} - className={menuClass} + // The body is the arrow machine's focus home (input-less menus + // land focus here on open) — the focus STAYS, but the browser's + // default blue ring around it must not draw (owner, polish + // round). Arrow cycling moves DOM focus to the buttons, which + // carry their own hover/highlight; the ring is pure noise. + className={`${menuClass} outline-none`} > {children} , diff --git a/components/sidebar.tsx b/components/sidebar.tsx index f968498..8550e1a 100644 --- a/components/sidebar.tsx +++ b/components/sidebar.tsx @@ -284,10 +284,16 @@ export function RenameRow({ chat, onCommit, onCancel }: { chat: ChatListItem; on return (

- - + {/* NOTHING MOVES (owner, polish round): an idle row renders no + status spacer (SessionStatus is null when not busy), so the + title starts at the row's own px-3 — 12px in. The old rename + row always carried a 14px spacer + gap + the input's border + and padding, landing the text ~41px in (and the meta with + it). The input here is borderless and padding-free at the + exact title x; the underline is the only editing cue. */} + {sessionMeta(chat)} diff --git a/tests/delete-node-dialog.test.tsx b/tests/delete-node-dialog.test.tsx index 1924874..8101bb7 100644 --- a/tests/delete-node-dialog.test.tsx +++ b/tests/delete-node-dialog.test.tsx @@ -73,7 +73,11 @@ describe("DeleteNodeDialog (thin SSR)", () => { it("the body at loading: the frame + the pre-flight read pending (effects never fire in SSR — the wrapper's own state)", () => { const html = bodyAt("loading"); assert.match(html, /data-testid="delete-node-dialog"/); - assert.match(html, /Delete.*Kepler.*three laws.*\?/); // the title names the node + // (owner, polish round) the serif title is just "Delete node?" in + // destructive red; the NAME rides below in JetBrains Mono code. + assert.match(html, /Delete node\?/); + assert.match(html, /text-destructive/); // the red title + assert.match(html, /data-testid="node-name-code"[^>]*>Kepler's three laws { assert.match(html, /chat_ghost/); assert.match(html, /cannot be undone/); assert.match(html, /data-testid="node-delete-confirm"/); + // the Delete button carries the destructive red (owner, polish round) + assert.match(html, /border-destructive\/50[^>]*text-destructive/); }); it("the body at failure: the 409 renders the activity-dock line with a Try-again", () => { diff --git a/tests/session-context.test.tsx b/tests/session-context.test.tsx index bdc379e..fbc229e 100644 --- a/tests/session-context.test.tsx +++ b/tests/session-context.test.tsx @@ -40,6 +40,14 @@ describe("RenameRow — the inline rename surface", () => { assert.ok(html.includes(`value="${chat.title}"`)); // the meta line rides along (row never changes height mid-edit) assert.ok(html.includes("session-rename-row")); + // (owner, polish round) NOTHING MOVES: no status spacer, no input + // border/padding — the draft text sits at the idle row's title x, + // and the input is the ONLY shift-proof editing cue (underline). + assert.ok(!html.includes("w-3.5 shrink-0"), "no always-on spacer (idle rows have none)"); + const input = html.split('data-testid="session-rename-input"')[1]?.split("/>")[0] ?? ""; + assert.ok(input.includes("px-0"), "input carries no left padding"); + assert.ok(input.includes("border-b"), "the underline is the editing cue"); + assert.ok(!input.includes("bg-background"), "no filled field box — text stays put"); }); });