diff --git a/components/plan-block.tsx b/components/plan-block.tsx index 5480951..fd18911 100644 --- a/components/plan-block.tsx +++ b/components/plan-block.tsx @@ -143,16 +143,24 @@ export const TEXT_CAP_SCALE = 16.5 / 12.5; // ─── Status styling (the retired panel's color language, unchanged) ────────── +// The simple look (owner, plan-graph round 4): rectangular boxes, white +// outlines/arrows/text; no fill on visited, today's gray look on upcoming, +// and the current node carries a faint yellow glow (a blurred yellow fill +// bleeding over the edges). The IN-CHAT block is monochrome white instead +// — no status language; the popup/window show the progression. const BOX: Record = { - visited: "fill-phase-teach/10 stroke-phase-teach/50", - current: "fill-primary/10 stroke-primary", + visited: "fill-none stroke-white", + current: "fill-none stroke-white", + // The grayed-out not-yet-visited look stays as-is (owner). upcoming: "fill-card stroke-border", }; const TEXT: Record = { - visited: "fill-muted-foreground text-xs", - current: "fill-foreground text-xs font-medium", + visited: "fill-white text-xs", + current: "fill-white text-xs font-medium", upcoming: "fill-muted-foreground/80 text-xs", }; +const MONO_BOX = "fill-none stroke-white"; +const MONO_TEXT = "fill-white text-xs"; /** The diagram itself: positioned nodes + bezier edges. Pure-ish render — * every input is data, so SSR render tests pin the whole thing. */ @@ -166,6 +174,7 @@ export function PlanDiagram({ pad: padUnits, textCapScale, naturalSize, + monochrome, }: { nodes: readonly LayoutNode[]; edges: ReadonlyArray<{ from: string; to: string }>; @@ -184,6 +193,9 @@ export function PlanDiagram({ * the popup's transform math depends on it; w-full inside the popup's * shrink-to-fit wrapper resolved to the popup width and broke fit. */ naturalSize?: boolean; + /** The in-chat block's mode: every node white (fill-none, white stroke + * and text, no glow) — statuses never show in the transcript (owner). */ + monochrome?: boolean; }) { const layout = useMemo(() => blockLayout(nodes, edges), [nodes, edges]); // Pad only the full-extent render — the plan window's override viewBox is @@ -217,8 +229,16 @@ export function PlanDiagram({ markerHeight="6" orient="auto-start-reverse" > - + {/* Interim head — swapped for the owner's arrowhead SVG when it + lands (right-pointing, tight viewBox, tip at the right edge). */} + + {/* The current node's highlight: a faint yellow fill BLURRED so it + bleeds out over the node's edges (owner). Generous filter region + so the blur never clips at its own bounds. */} + + + {edges.map((e) => { const x1 = (layout.x.get(e.from) ?? 0) + NODE_W / 2; @@ -230,7 +250,7 @@ export function PlanDiagram({ ${e.to}`} d={`M ${x1} ${y1} C ${x1} ${mid}, ${x2} ${mid}, ${x2} ${y2}`} - className="fill-none stroke-muted-foreground/40" + className="fill-none stroke-white" strokeWidth={1.5} markerEnd={`url(#${markerId})`} /> @@ -238,7 +258,7 @@ export function PlanDiagram({ })} {nodes.map((n) => { const status = statusOf(n.id); - const glow = n.id === glowId; + const glow = n.id === glowId && !monochrome; const lines = wrapTitle(n.title); return ( {glow && ( - // The glow target: a soft primary halo breathing around the box. + // The current node's highlight: faint yellow, blurred so it + // bleeds over the edges (owner, plan-graph round 4). )} - + {/* Rectangular — no corner rounding (owner). */} + {lines.map((line, i) => ( - + {line} ))} @@ -303,6 +334,7 @@ export function PlanBlock({ data, onOpen }: { data: PlanBlockData; onOpen: () => markerId={markerId} pad={GRAPH_PAD} textCapScale={TEXT_CAP_SCALE} + monochrome /> ); @@ -416,6 +448,10 @@ export function PlanPopup({ messages, onClose }: { messages: readonly PopupMessa const [view, setView] = useState({ x: 0, y: 0, zoom: 1 }); const userAdjustedRef = useRef(false); const atFloor = view.zoom <= fitZoom + 1e-9; + // The transition stays off until the FIRST fit has been applied — the + // popup paints at the fit directly instead of flying in from the initial + // top-left zoom:1 state (owner, plan-graph round 4). + const [settled, setSettled] = useState(false); useEffect(() => { if (atFloor) userAdjustedRef.current = false; @@ -423,6 +459,7 @@ export function PlanPopup({ messages, onClose }: { messages: readonly PopupMessa useEffect(() => { if (userAdjustedRef.current || !size.w || !size.h) return; setView(fitGraphView(box, size, fitZoom)); + setSettled(true); }, [fitZoom, size, box]); useEffect(() => { @@ -526,7 +563,8 @@ export function PlanPopup({ messages, onClose }: { messages: readonly PopupMessa className="absolute top-0 left-0 origin-top-left" style={{ transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom})`, - transition: panning ? "none" : "transform 180ms ease-out", + transition: + panning || !settled ? "none" : "transform 180ms ease-out", }} > { assert.match(html, /data-testid="plan-node-a" data-status="visited"/); assert.match(html, /data-testid="plan-node-b" data-status="current"/); assert.match(html, /data-testid="plan-node-c" data-status="upcoming"/); - // The current node is the glow target. - assert.match(html, /stroke-primary\/70 animate-pulse/); + // The current node is the glow target: a faint yellow fill, blurred + // so it bleeds over the edges (owner, plan-graph round 4). + assert.match(html, /fill-yellow-400\/25 animate-pulse/); + assert.match(html, /feGaussianBlur/); assert.match(html, /live — statuses derive from your check outcomes/); });