From eaf09b475ee4ec1e0b9f7593d0a8fbf227d5bc4c Mon Sep 17 00:00:00 2001 From: dtoro Date: Sun, 8 Mar 2026 08:26:19 +0100 Subject: [PATCH] imp animation --- src/components/graph/NodeStatusIndicator.tsx | 64 ++++++++++++++------ 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/src/components/graph/NodeStatusIndicator.tsx b/src/components/graph/NodeStatusIndicator.tsx index d3d023f..74c1d12 100644 --- a/src/components/graph/NodeStatusIndicator.tsx +++ b/src/components/graph/NodeStatusIndicator.tsx @@ -1,5 +1,6 @@ -import { useEffect, useId, useRef } from 'react' +import { useId, useLayoutEffect, useRef, useState } from 'react' import type { ReactNode } from 'react' +import { flushSync } from 'react-dom' import { cn } from '@/lib/utils' export type NodeStatus = 'loading' | 'success' | 'error' | 'initial' @@ -27,7 +28,8 @@ function dashOffsetAtProgress(progress: number, pathLength: number): number { return pathLength + 55 - (5 * (progress - 0.85)) / 0.15 } -/** One solid segment (half path length) moving along the border, with gradient opacity 0→1 along the segment */ +/** One solid segment (half path length) moving along the border, with gradient opacity 0→1 along the segment. + * Uses ResizeObserver so the border always matches the actual rendered node size (avoids min-width/min-height mismatch). */ function BorderLoadingIndicator({ children, width, @@ -37,29 +39,60 @@ function BorderLoadingIndicator({ width?: number height?: number }) { - const hasSize = width != null && height != null && width > 0 && height > 0 - const w = hasSize ? width + 4 : 0 - const h = hasSize ? height + 4 : 0 + const containerRef = useRef(null) + const [measured, setMeasured] = useState({ w: 0, h: 0 }) + const hasMeasured = measured.w > 0 && measured.h > 0 + const fallbackW = (width != null && height != null && width > 0 && height > 0) ? width + 4 : 0 + const fallbackH = (width != null && height != null && width > 0 && height > 0) ? height + 4 : 0 + const w = hasMeasured ? measured.w + 4 : fallbackW + const h = hasMeasured ? measured.h + 4 : fallbackH + const hasSize = w > 0 && h > 0 const pathD = hasSize ? `M ${R + 2} ${2} L ${w - R - 2} ${2} Q ${w - 2} ${2} ${w - 2} ${R + 2} L ${w - 2} ${h - R - 2} Q ${w - 2} ${h - 2} ${w - R - 2} ${h - 2} L ${R + 2} ${h - 2} Q ${2} ${h - 2} ${2} ${h - R - 2} L ${2} ${R + 2} Q ${2} ${2} ${R + 2} ${2} Z` : '' - const pathLength = hasSize ? 2 * (w + h) - 8 * R + 2 * Math.PI * R : 0 - const segmentLen = pathLength * 0.5 - const gapLen = pathLength * 0.5 + 80 - const gradientId = useId().replace(/:/g, '-') const pathRef = useRef(null) const gradientRef = useRef(null) const rafRef = useRef(0) const startTimeRef = useRef(0) - useEffect(() => { - if (!hasSize || pathLength <= 0) return + useLayoutEffect(() => { + const container = containerRef.current + if (!container) return + // Observe the first child (BaseNode) so we use its actual rendered size. + const target = + container.firstElementChild instanceof HTMLElement + ? container.firstElementChild + : container + const syncMeasure = () => { + const cw = (target as HTMLElement).offsetWidth + const ch = (target as HTMLElement).offsetHeight + if (cw > 0 && ch > 0) { + flushSync(() => setMeasured({ w: cw, h: ch })) + } + } + syncMeasure() + const ro = new ResizeObserver((entries) => { + const entry = entries[0] + if (!entry) return + const { width: cw, height: ch } = entry.contentRect + setMeasured({ w: Math.round(cw), h: Math.round(ch) }) + }) + ro.observe(target) + return () => ro.disconnect() + }, []) + + useLayoutEffect(() => { + if (!hasSize) return const pathEl = pathRef.current const gradientEl = gradientRef.current if (!pathEl || !gradientEl) return const totalLen = pathEl.getTotalLength() + const segmentLen = totalLen * 0.5 + const gapLen = totalLen * 0.5 + 80 + + pathEl.style.strokeDasharray = `${segmentLen} ${gapLen}` const tick = () => { const elapsed = (performance.now() - startTimeRef.current) % DURATION_MS @@ -83,10 +116,10 @@ function BorderLoadingIndicator({ startTimeRef.current = performance.now() rafRef.current = requestAnimationFrame(tick) return () => cancelAnimationFrame(rafRef.current) - }, [hasSize, pathLength, segmentLen]) + }, [hasSize, w, h]) return ( -
+
{children} {hasSize && ( )}