Send a node's emit pulse round the rim instead of scaling it out
The ring hung off the node itself, and `inset` on an absolute child resolves against the padding box — so on the brain's thick status ring it landed inside the outer edge and painted over the status the ring carries. Both node shapes now hang it off a border-less wrapper, where the same offsets clear the rim whatever border the node draws (measured: 4px on all four sides, on a 1px card border and a 7px neuron ring alike). The shape is a conic-gradient arc masked to the padding band, running one lap in `--duration-pulse` and going out on the way past. Note: BrainNode.tsx also carries an unrelated in-flight change from a concurrent session (the `seen` ref gating the pulse on a real emit); the two could not be separated in one file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -138,103 +138,106 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
|
||||
const style = STATUS_STYLES[status as keyof typeof STATUS_STYLES]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative min-w-[168px] max-w-[220px] rounded-lg border border-border bg-card px-3 py-2.5 shadow-e1 transition-shadow",
|
||||
selected && "border-primary shadow-e2",
|
||||
)}
|
||||
>
|
||||
// The pulse ring measures itself from here rather than from the card, so
|
||||
// a border can never land on top of it — see `.node-pulse` in flow.css.
|
||||
<div className="relative rounded-lg">
|
||||
{/* Remounting on each emit is what restarts the animation. */}
|
||||
{emits > 0 ? <span key={emits} className="node-pulse" /> : null}
|
||||
<div
|
||||
className={cn(
|
||||
"relative min-w-[168px] max-w-[220px] rounded-lg border border-border bg-card px-3 py-2.5 shadow-e1 transition-shadow",
|
||||
selected && "border-primary shadow-e2",
|
||||
)}
|
||||
>
|
||||
<PortHandles
|
||||
specs={definition.requires ?? []}
|
||||
type="target"
|
||||
position={vertical ? Position.Top : Position.Left}
|
||||
/>
|
||||
|
||||
<PortHandles
|
||||
specs={definition.requires ?? []}
|
||||
type="target"
|
||||
position={vertical ? Position.Top : Position.Left}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="flex size-7 shrink-0 items-center justify-center rounded-sm bg-primary/10 text-primary">
|
||||
<Icon className="size-4" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-medium">
|
||||
{definition.title || definition.id}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="flex size-7 shrink-0 items-center justify-center rounded-sm bg-primary/10 text-primary">
|
||||
<Icon className="size-4" />
|
||||
</span>
|
||||
<span className="block truncate text-xs font-medium uppercase tracking-[0.5px] text-muted-foreground">
|
||||
{typeLabel}
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-medium">
|
||||
{definition.title || definition.id}
|
||||
</span>
|
||||
<span className="block truncate text-xs font-medium uppercase tracking-[0.5px] text-muted-foreground">
|
||||
{typeLabel}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
{status === "running" ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="nodrag nopan -my-1 size-6 shrink-0 text-muted-foreground hover:text-destructive"
|
||||
aria-label="Stop this node"
|
||||
data-testid="node-cancel"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
// Best effort by nature: it may well have finished between
|
||||
// the render and the click, which is the outcome asked for.
|
||||
FlowsService.cancelNode({
|
||||
name: flow,
|
||||
nodeId: definition.id,
|
||||
}).catch(() => {})
|
||||
}}
|
||||
>
|
||||
<Square />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Stop this node</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{status === "running" ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="nodrag nopan -my-1 size-6 shrink-0 text-muted-foreground hover:text-destructive"
|
||||
aria-label="Stop this node"
|
||||
data-testid="node-cancel"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
// Best effort by nature: it may well have finished between
|
||||
// the render and the click, which is the outcome asked for.
|
||||
FlowsService.cancelNode({
|
||||
name: flow,
|
||||
nodeId: definition.id,
|
||||
}).catch(() => {})
|
||||
}}
|
||||
>
|
||||
<Square />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Stop this node</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
{status === "error" && onShowLogs ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
// `nodrag` keeps xyflow from reading the press as a drag; the
|
||||
// click itself is stopped so the node panel stays closed.
|
||||
className="nodrag nopan -my-1 size-6 shrink-0 text-muted-foreground hover:text-destructive"
|
||||
aria-label="Show what this node printed"
|
||||
data-testid="node-traceback"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onShowLogs(definition.id)
|
||||
}}
|
||||
>
|
||||
<Bug />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Show the traceback</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{status === "error" && onShowLogs ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
// `nodrag` keeps xyflow from reading the press as a drag; the
|
||||
// click itself is stopped so the node panel stays closed.
|
||||
className="nodrag nopan -my-1 size-6 shrink-0 text-muted-foreground hover:text-destructive"
|
||||
aria-label="Show what this node printed"
|
||||
data-testid="node-traceback"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onShowLogs(definition.id)
|
||||
}}
|
||||
>
|
||||
<Bug />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Show the traceback</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
{style ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
role="img"
|
||||
className={cn("size-2 shrink-0 rounded-full", style.dot)}
|
||||
aria-label={style.label}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-h-60 max-w-xs overflow-y-auto whitespace-pre-line break-words">
|
||||
{problem || style.label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{style ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
role="img"
|
||||
className={cn("size-2 shrink-0 rounded-full", style.dot)}
|
||||
aria-label={style.label}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-h-60 max-w-xs overflow-y-auto whitespace-pre-line break-words">
|
||||
{problem || style.label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<PortHandles
|
||||
specs={definition.provides ?? []}
|
||||
type="source"
|
||||
position={vertical ? Position.Bottom : Position.Right}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PortHandles
|
||||
specs={definition.provides ?? []}
|
||||
type="source"
|
||||
position={vertical ? Position.Bottom : Position.Right}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user