Plan-graph refinement round 1 (owner): chat-font zoom cap + stroke-safe padding
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'.
This commit is contained in:
@@ -129,6 +129,17 @@ export function wrapTitle(title: string, maxChars = 20, maxLines = 2): string[]
|
|||||||
return lines;
|
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) ──────────
|
// ─── Status styling (the retired panel's color language, unchanged) ──────────
|
||||||
|
|
||||||
const BOX: Record<PlanNodeStatus, string> = {
|
const BOX: Record<PlanNodeStatus, string> = {
|
||||||
@@ -151,6 +162,8 @@ export function PlanDiagram({
|
|||||||
glowId,
|
glowId,
|
||||||
markerId,
|
markerId,
|
||||||
viewBoxOverride,
|
viewBoxOverride,
|
||||||
|
pad: padUnits,
|
||||||
|
textCapScale,
|
||||||
}: {
|
}: {
|
||||||
nodes: readonly LayoutNode[];
|
nodes: readonly LayoutNode[];
|
||||||
edges: ReadonlyArray<{ from: string; to: string }>;
|
edges: ReadonlyArray<{ from: string; to: string }>;
|
||||||
@@ -159,12 +172,26 @@ export function PlanDiagram({
|
|||||||
markerId: string;
|
markerId: string;
|
||||||
/** A windowed viewBox (the plan window) instead of the full extent. */
|
/** A windowed viewBox (the plan window) instead of the full extent. */
|
||||||
viewBoxOverride?: string;
|
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]);
|
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 (
|
return (
|
||||||
<svg
|
<svg
|
||||||
viewBox={viewBoxOverride ?? `0 0 ${Math.max(layout.width, 1)} ${Math.max(layout.height, 1)}`}
|
viewBox={viewBoxOverride ?? `${-pad} ${-pad} ${vbW} ${vbH}`}
|
||||||
className="block h-auto w-full"
|
className={`block h-auto w-full${textCapScale ? " mx-auto" : ""}`}
|
||||||
|
style={textCapScale ? { maxWidth: `${Math.round(vbW * textCapScale * 100) / 100}px` } : undefined}
|
||||||
role="img"
|
role="img"
|
||||||
aria-label="Plan diagram"
|
aria-label="Plan diagram"
|
||||||
>
|
>
|
||||||
@@ -254,7 +281,17 @@ export function PlanBlock({ data, onOpen }: { data: PlanBlockData; onOpen: () =>
|
|||||||
title="Open the full plan"
|
title="Open the full plan"
|
||||||
className="block w-full text-left"
|
className="block w-full text-left"
|
||||||
>
|
>
|
||||||
<PlanDiagram nodes={data.nodes} edges={data.edges} statusOf={statusOf} markerId={markerId} />
|
{/* 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). */}
|
||||||
|
<PlanDiagram
|
||||||
|
nodes={data.nodes}
|
||||||
|
edges={data.edges}
|
||||||
|
statusOf={statusOf}
|
||||||
|
markerId={markerId}
|
||||||
|
pad={GRAPH_PAD}
|
||||||
|
textCapScale={TEXT_CAP_SCALE}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,11 +82,14 @@ describe("plan block render", () => {
|
|||||||
assert.match(html, /data-testid="plan-block-full"/);
|
assert.match(html, /data-testid="plan-block-full"/);
|
||||||
// Borderless: the wrapper carries no border/rounded chrome.
|
// Borderless: the wrapper carries no border/rounded chrome.
|
||||||
assert.ok(!/data-testid="plan-block-full"[^>]*border/.test(html), "no wrapping 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
|
// Fills the column UP TO the chat-font cap: the svg is w-full +
|
||||||
// (leftmost/rightmost nodes hug the edges), height natural.
|
// mx-auto with maxWidth = extent × TEXT_CAP_SCALE, so node text never
|
||||||
assert.match(html, /<svg[^>]*class="block h-auto w-full"/);
|
// renders past the 16.5px prose size (owner, plan-graph round 1).
|
||||||
// Two stacked nodes: 2*NODE_H + GAP_Y = 136 natural height, width = one node column.
|
assert.match(html, /<svg[^>]*class="block h-auto w-full mx-auto"/);
|
||||||
assert.match(html, /viewBox="0 0 160 136"/);
|
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.
|
// Snapshot statuses render as carried.
|
||||||
assert.match(html, /data-testid="plan-node-a" data-status="visited"/);
|
assert.match(html, /data-testid="plan-node-a" data-status="visited"/);
|
||||||
assert.match(html, /data-testid="plan-node-b" data-status="upcoming"/);
|
assert.match(html, /data-testid="plan-node-b" data-status="upcoming"/);
|
||||||
|
|||||||
Reference in New Issue
Block a user