From fdeacc42f579f18f418d52939062735ae6911d98 Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Mon, 14 Sep 2026 06:22:26 +0300 Subject: [PATCH] Plan-graph refinement round 1 (owner): chat-font zoom cap + stroke-safe padding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In-chat full plan block: - TEXT_CAP_SCALE (16.5/12.5): node text renders at text-xs in svg units; the block now carries maxWidth = extent × cap + mx-auto, so it still fills the column whenever the column is narrower than the cap, but never scales node text past the chat prose size (--lh-prose-size). - GRAPH_PAD (8 svg units) around the full-extent viewBox: centered strokes (half outside the geometry) and the −4 glow halo were clipping at the frame edges — the padded origin (-8,-8) keeps all ink visible. Only the full-extent render pads; the plan window's override viewport and the popup's fit margins are untouched. Test pins updated: mx-auto + max-width:232.32px + viewBox='-8 -8 176 152'. --- components/plan-block.tsx | 43 +++++++++++++++++++++++++++++--- tests/render-plan-block.test.tsx | 13 ++++++---- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/components/plan-block.tsx b/components/plan-block.tsx index 77db98b..0015962 100644 --- a/components/plan-block.tsx +++ b/components/plan-block.tsx @@ -129,6 +129,17 @@ export function wrapTitle(title: string, maxChars = 20, maxLines = 2): string[] return lines; } +// ─── Geometry caps (owner, plan-graph refinement round 1) ───────────────────── + +/** SVG-unit padding around the layout extent: strokes are centered on the + * geometry (half bleeds outside the extent) and the glow halo reaches −4 — + * without this pad the outermost node borders clip at the svg edge. */ +export const GRAPH_PAD = 8; +/** Node titles render at text-xs (12.5px) in svg units. The in-chat block + * scales up to fill the column but NEVER past the chat prose size + * (--lh-prose-size, 16.5px) — the width cap that makes that hold. */ +export const TEXT_CAP_SCALE = 16.5 / 12.5; + // ─── Status styling (the retired panel's color language, unchanged) ────────── const BOX: Record = { @@ -151,6 +162,8 @@ export function PlanDiagram({ glowId, markerId, viewBoxOverride, + pad: padUnits, + textCapScale, }: { nodes: readonly LayoutNode[]; edges: ReadonlyArray<{ from: string; to: string }>; @@ -159,12 +172,26 @@ export function PlanDiagram({ markerId: string; /** A windowed viewBox (the plan window) instead of the full extent. */ viewBoxOverride?: string; + /** Pads the viewBox by this many svg units so centered strokes and the + * glow halo never clip at the extent edges (full-extent renders only). */ + pad?: number; + /** Caps the rendered width at extent × scale — the chat-font-size zoom + * ceiling for the in-chat block (fills the column, never past prose). */ + textCapScale?: number; }) { const layout = useMemo(() => blockLayout(nodes, edges), [nodes, edges]); + // Pad only the full-extent render — the plan window's override viewBox is + // a deliberate viewport crop, and the popup keeps its own fit margins. + const pad = viewBoxOverride ? 0 : (padUnits ?? 0); + const w = Math.max(layout.width, 1); + const h = Math.max(layout.height, 1); + const vbW = w + 2 * pad; + const vbH = h + 2 * pad; return ( @@ -254,7 +281,17 @@ export function PlanBlock({ data, onOpen }: { data: PlanBlockData; onOpen: () => title="Open the full plan" className="block w-full text-left" > - + {/* pad keeps centered strokes + the glow halo inside the frame; the + cap keeps node text at or below chat prose size while still filling + the column when it can (owner, plan-graph refinement round 1). */} + ); } diff --git a/tests/render-plan-block.test.tsx b/tests/render-plan-block.test.tsx index 92a271b..4824b0e 100644 --- a/tests/render-plan-block.test.tsx +++ b/tests/render-plan-block.test.tsx @@ -82,11 +82,14 @@ describe("plan block render", () => { assert.match(html, /data-testid="plan-block-full"/); // Borderless: the wrapper carries no border/rounded chrome. assert.ok(!/data-testid="plan-block-full"[^>]*border/.test(html), "no wrapping chrome"); - // Fills the column: the svg is w-full with the layout extent as viewBox - // (leftmost/rightmost nodes hug the edges), height natural. - assert.match(html, /]*class="block h-auto w-full"/); - // Two stacked nodes: 2*NODE_H + GAP_Y = 136 natural height, width = one node column. - assert.match(html, /viewBox="0 0 160 136"/); + // Fills the column UP TO the chat-font cap: the svg is w-full + + // mx-auto with maxWidth = extent × TEXT_CAP_SCALE, so node text never + // renders past the 16.5px prose size (owner, plan-graph round 1). + assert.match(html, /]*class="block h-auto w-full mx-auto"/); + assert.match(html, /max-width:232\.32px/); // (160+16) × 1.32 + // Two stacked nodes: 2*NODE_H + GAP_Y = 136 extent + GRAPH_PAD on + // every side (strokes/glow never clip at the edges). + assert.match(html, /viewBox="-8 -8 176 152"/); // Snapshot statuses render as carried. assert.match(html, /data-testid="plan-node-a" data-status="visited"/); assert.match(html, /data-testid="plan-node-b" data-status="upcoming"/);