Gate the flow node pulse on a real emit
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKL7sUgNWhukDEz95vSMQv
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
||||
Terminal,
|
||||
Timer,
|
||||
} from "lucide-react"
|
||||
import { memo } from "react"
|
||||
import { memo, useEffect, useRef, useState } from "react"
|
||||
|
||||
import { FlowsService, type MessageSpec, type NodeDef_Input } from "@/client"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { useIsMobile } from "@/hooks/useMobile"
|
||||
import { duration } from "@/lib/motion"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { portOf } from "./deriveEdges"
|
||||
import { useNodeEmits, useNodeStatus } from "./liveStore"
|
||||
@@ -119,6 +120,22 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
|
||||
// The graph runs top to bottom on a phone, so the ports have to face that
|
||||
// way too — see DESIGN-GUIDELINES.md → Responsive.
|
||||
const vertical = useIsMobile()
|
||||
// Whether a pulse is playing right now; the ring is mounted only while it is.
|
||||
const [firing, setFiring] = useState(false)
|
||||
// The count outlives this component: the store is module-level, and a
|
||||
// snapshot restores what the engine counted before the page even loaded. So
|
||||
// the number we mount with is history, and only a change on top of it is
|
||||
// something that just happened.
|
||||
const seen = useRef(emits)
|
||||
|
||||
useEffect(() => {
|
||||
if (emits === seen.current) return
|
||||
seen.current = emits
|
||||
setFiring(true)
|
||||
const timer = setTimeout(() => setFiring(false), duration.pulse * 1000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [emits])
|
||||
|
||||
const Icon =
|
||||
NODE_ICONS[definition.type as keyof typeof NODE_ICONS] ??
|
||||
// A connector's own type cannot be in the map above, and a device is
|
||||
@@ -141,8 +158,10 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
|
||||
// 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}
|
||||
{/* Remounting on each emit is what restarts the animation, so this is
|
||||
gated on the pulse rather than on the count: a node mounting with a
|
||||
count already in the store would otherwise play it once for free. */}
|
||||
{firing ? <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",
|
||||
|
||||
Reference in New Issue
Block a user