Plan-graph refinement round 6 (owner): grayed arrows, static glow, glow-sized frame pad
- Arrows into not-yet-visited nodes are grayed out like the nodes (stroke-border line + a fill-border copy of the arrowhead); the in-chat block stays all-white. - The yellow glow is static — the pulse is gone (owner). - Smaller blur: stdDeviation 7 → 5, glow rect margin 10 → 8. - The glow clipped at the frame on narrow graphs: its visible reach (GLOW_MARGIN + 3·GLOW_BLUR = 23 units) far exceeded GRAPH_PAD (8). Surfaces that show the glow now pad by GLOW_REACH — the popup's viewBox AND its fit box both use POPUP_PAD = GLOW_REACH, so the blur never clips; the monochrome in-chat block stays tight at GRAPH_PAD.
This commit is contained in:
+42
-13
@@ -136,6 +136,14 @@ export function wrapTitle(title: string, maxChars = 20, maxLines = 2): string[]
|
|||||||
* geometry (half bleeds outside the extent) and the glow halo reaches −4 —
|
* geometry (half bleeds outside the extent) and the glow halo reaches −4 —
|
||||||
* without this pad the outermost node borders clip at the svg edge. */
|
* without this pad the outermost node borders clip at the svg edge. */
|
||||||
export const GRAPH_PAD = 8;
|
export const GRAPH_PAD = 8;
|
||||||
|
/** The current node's yellow glow: a fill blurred so it bleeds over the
|
||||||
|
* node's edges (owner). A Gaussian's visible spread is ≈3σ, so the glow
|
||||||
|
* reaches GLOW_MARGIN + 3·GLOW_BLUR past the node — any surface that
|
||||||
|
* shows the glow must pad its viewBox by GLOW_REACH or the blur clips
|
||||||
|
* at the frame edge (owner: extend the container padding to fit it). */
|
||||||
|
export const GLOW_MARGIN = 8;
|
||||||
|
export const GLOW_BLUR = 5;
|
||||||
|
export const GLOW_REACH = GLOW_MARGIN + 3 * GLOW_BLUR;
|
||||||
/** Node titles render at text-xs (12.5px) in svg units. The in-chat block
|
/** 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
|
* 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. */
|
* (--lh-prose-size, 16.5px) — the width cap that makes that hold. */
|
||||||
@@ -253,11 +261,25 @@ export function PlanDiagram({
|
|||||||
line ends AT the notch — the tip beyond it is solid fill. */}
|
line ends AT the notch — the tip beyond it is solid fill. */}
|
||||||
<path d={ARROW_HEAD} className="fill-white" />
|
<path d={ARROW_HEAD} className="fill-white" />
|
||||||
</marker>
|
</marker>
|
||||||
|
{/* Same head, grayed — arrows into not-yet-visited nodes are
|
||||||
|
grayed out like the nodes themselves (owner, round 6). */}
|
||||||
|
<marker
|
||||||
|
id={`${markerId}-dim`}
|
||||||
|
viewBox="0 0 50.2 104"
|
||||||
|
refX={27.2}
|
||||||
|
refY={52}
|
||||||
|
markerWidth={HEAD_W}
|
||||||
|
markerHeight={HEAD_H}
|
||||||
|
markerUnits="userSpaceOnUse"
|
||||||
|
orient="auto"
|
||||||
|
>
|
||||||
|
<path d={ARROW_HEAD} className="fill-border" />
|
||||||
|
</marker>
|
||||||
{/* The current node's highlight: a faint yellow fill BLURRED so it
|
{/* The current node's highlight: a faint yellow fill BLURRED so it
|
||||||
bleeds out over the node's edges (owner). Generous filter region
|
bleeds out over the node's edges (owner). Generous filter region
|
||||||
so the blur never clips at its own bounds. */}
|
so the blur never clips at its own bounds. */}
|
||||||
<filter id={`${markerId}-glow`} x="-60%" y="-60%" width="220%" height="220%">
|
<filter id={`${markerId}-glow`} x="-60%" y="-60%" width="220%" height="220%">
|
||||||
<feGaussianBlur stdDeviation={7} />
|
<feGaussianBlur stdDeviation={GLOW_BLUR} />
|
||||||
</filter>
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
{edges.map((e) => {
|
{edges.map((e) => {
|
||||||
@@ -268,13 +290,16 @@ export function PlanDiagram({
|
|||||||
// node border while the line stops at the notch mouth.
|
// node border while the line stops at the notch mouth.
|
||||||
const y2 = (layout.y.get(e.to) ?? 0) - HEAD_TIP_OVERHANG;
|
const y2 = (layout.y.get(e.to) ?? 0) - HEAD_TIP_OVERHANG;
|
||||||
const mid = (y1 + y2) / 2;
|
const mid = (y1 + y2) / 2;
|
||||||
|
// An arrow into a not-yet-visited node is grayed out like the
|
||||||
|
// node (owner, round 6); the in-chat block stays all-white.
|
||||||
|
const dim = !monochrome && statusOf(e.to) === "upcoming";
|
||||||
return (
|
return (
|
||||||
<path
|
<path
|
||||||
key={`${e.from}->${e.to}`}
|
key={`${e.from}->${e.to}`}
|
||||||
d={`M ${x1} ${y1} C ${x1} ${mid}, ${x2} ${mid}, ${x2} ${y2}`}
|
d={`M ${x1} ${y1} C ${x1} ${mid}, ${x2} ${mid}, ${x2} ${y2}`}
|
||||||
className="fill-none stroke-white"
|
className={dim ? "fill-none stroke-border" : "fill-none stroke-white"}
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
markerEnd={`url(#${markerId})`}
|
markerEnd={`url(#${markerId}${dim ? "-dim" : ""})`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -291,13 +316,13 @@ export function PlanDiagram({
|
|||||||
>
|
>
|
||||||
{glow && (
|
{glow && (
|
||||||
// The current node's highlight: faint yellow, blurred so it
|
// The current node's highlight: faint yellow, blurred so it
|
||||||
// bleeds over the edges (owner, plan-graph round 4).
|
// bleeds over the edges — static, no pulse (owner, round 6).
|
||||||
<rect
|
<rect
|
||||||
x={-10}
|
x={-GLOW_MARGIN}
|
||||||
y={-10}
|
y={-GLOW_MARGIN}
|
||||||
width={NODE_W + 20}
|
width={NODE_W + 2 * GLOW_MARGIN}
|
||||||
height={NODE_H + 20}
|
height={NODE_H + 2 * GLOW_MARGIN}
|
||||||
className="fill-yellow-400/25 animate-pulse"
|
className="fill-yellow-400/25"
|
||||||
filter={`url(#${markerId}-glow)`}
|
filter={`url(#${markerId}-glow)`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -391,6 +416,9 @@ export function latestTree(messages: readonly PopupMessage[]): PlanTree | null {
|
|||||||
* wide graphs; the fit itself is the floor now. */
|
* wide graphs; the fit itself is the floor now. */
|
||||||
const ZOOM_MAX = 4;
|
const ZOOM_MAX = 4;
|
||||||
const FIT_PAD = 16;
|
const FIT_PAD = 16;
|
||||||
|
/** The popup shows the glow, so its frame pads by the glow's reach —
|
||||||
|
* the in-chat block (monochrome, no glow) stays tight at GRAPH_PAD. */
|
||||||
|
const POPUP_PAD = GLOW_REACH;
|
||||||
const FIT_ZOOM_MAX = 1.5;
|
const FIT_ZOOM_MAX = 1.5;
|
||||||
|
|
||||||
interface GraphView {
|
interface GraphView {
|
||||||
@@ -459,10 +487,11 @@ export function PlanPopup({ messages, onClose }: { messages: readonly PopupMessa
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const layout = useMemo(() => (tree ? blockLayout(tree.nodes, tree.edges) : null), [tree]);
|
const layout = useMemo(() => (tree ? blockLayout(tree.nodes, tree.edges) : null), [tree]);
|
||||||
// The fit box is the PADDED ink box — the diagram renders with GRAPH_PAD
|
// The fit box is the PADDED ink box — the diagram renders with POPUP_PAD
|
||||||
// so strokes/glow never clip, and the fit must account for it.
|
// (= the glow's reach) so the blurred glow never clips at the frame, and
|
||||||
|
// the fit must account for that padding.
|
||||||
const box = useMemo(
|
const box = useMemo(
|
||||||
() => (layout ? { w: layout.width + 2 * GRAPH_PAD, h: layout.height + 2 * GRAPH_PAD } : { w: 0, h: 0 }),
|
() => (layout ? { w: layout.width + 2 * POPUP_PAD, h: layout.height + 2 * POPUP_PAD } : { w: 0, h: 0 }),
|
||||||
[layout],
|
[layout],
|
||||||
);
|
);
|
||||||
const fitZoom = useMemo(() => fitZoomFor(box, size), [box, size]);
|
const fitZoom = useMemo(() => fitZoomFor(box, size), [box, size]);
|
||||||
@@ -595,7 +624,7 @@ export function PlanPopup({ messages, onClose }: { messages: readonly PopupMessa
|
|||||||
statusOf={statusOf}
|
statusOf={statusOf}
|
||||||
glowId={statuses?.current ?? null}
|
glowId={statuses?.current ?? null}
|
||||||
markerId={markerId}
|
markerId={markerId}
|
||||||
pad={GRAPH_PAD}
|
pad={POPUP_PAD}
|
||||||
naturalSize
|
naturalSize
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -139,9 +139,12 @@ describe("plan popup (live derivation)", () => {
|
|||||||
assert.match(html, /data-testid="plan-node-b" data-status="current"/);
|
assert.match(html, /data-testid="plan-node-b" data-status="current"/);
|
||||||
assert.match(html, /data-testid="plan-node-c" data-status="upcoming"/);
|
assert.match(html, /data-testid="plan-node-c" data-status="upcoming"/);
|
||||||
// The current node is the glow target: a faint yellow fill, blurred
|
// The current node is the glow target: a faint yellow fill, blurred
|
||||||
// so it bleeds over the edges (owner, plan-graph round 4).
|
// so it bleeds over the edges — static, no pulse (owner, round 6).
|
||||||
assert.match(html, /fill-yellow-400\/25 animate-pulse/);
|
assert.match(html, /fill-yellow-400\/25/);
|
||||||
assert.match(html, /feGaussianBlur/);
|
assert.match(html, /feGaussianBlur/);
|
||||||
|
assert.doesNotMatch(html, /animate-pulse/);
|
||||||
|
// Arrows into not-yet-visited nodes are grayed out (owner, round 6).
|
||||||
|
assert.match(html, /marker-end="url\(#.*-dim\)"/);
|
||||||
assert.match(html, /live — statuses derive from your check outcomes/);
|
assert.match(html, /live — statuses derive from your check outcomes/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user