diff --git a/web/app/components/workflow/skill/file-tree/tree-node.tsx b/web/app/components/workflow/skill/file-tree/tree-node.tsx index fecd25c290..67efe17e15 100644 --- a/web/app/components/workflow/skill/file-tree/tree-node.tsx +++ b/web/app/components/workflow/skill/file-tree/tree-node.tsx @@ -42,7 +42,7 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps) handleKeyDown, } = useTreeNodeHandlers({ node }) - const { isDragOver, dragHandlers } = useFolderFileDrop(node) + const { isDragOver, isBlinking, dragHandlers } = useFolderFileDrop(node) const handleMoreClick = useCallback((e: React.MouseEvent) => { e.stopPropagation() @@ -63,8 +63,10 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps) 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-components-input-border-active', isSelected && 'bg-state-base-active', hasContextMenu && !isSelected && 'bg-state-base-hover', - // Drag over highlight for folders - use ring instead of border to avoid layout shift + // Drag over highlight for folders isDragOver && 'bg-state-accent-hover ring-1 ring-inset ring-state-accent-solid', + // Blink animation when about to auto-expand (VSCode-style) + isBlinking && 'animate-drag-blink', )} onKeyDown={handleKeyDown} onContextMenu={handleContextMenu} diff --git a/web/app/components/workflow/skill/hooks/use-folder-file-drop.ts b/web/app/components/workflow/skill/hooks/use-folder-file-drop.ts index 00a6a2f93a..b7a7997523 100644 --- a/web/app/components/workflow/skill/hooks/use-folder-file-drop.ts +++ b/web/app/components/workflow/skill/hooks/use-folder-file-drop.ts @@ -2,13 +2,14 @@ import type { NodeApi } from 'react-arborist' import type { TreeNodeData } from '../type' -import { useCallback, useEffect, useMemo, useRef } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useStore } from '@/app/components/workflow/store' import { isFileDrag } from '../utils/drag-utils' import { useFileDrop } from './use-file-drop' type UseFolderFileDropReturn = { isDragOver: boolean + isBlinking: boolean dragHandlers: { onDragEnter: (e: React.DragEvent) => void onDragOver: (e: React.DragEvent) => void @@ -17,6 +18,8 @@ type UseFolderFileDropReturn = { } } +// Blink starts at 1s, folder expands at 2s +const BLINK_START_DELAY_MS = 1000 const AUTO_EXPAND_DELAY_MS = 2000 export function useFolderFileDrop(node: NodeApi): UseFolderFileDropReturn { @@ -27,21 +30,42 @@ export function useFolderFileDrop(node: NodeApi): UseFolderFileDro const { handleDragOver, handleDrop } = useFileDrop() const expandTimerRef = useRef(null) + const blinkTimerRef = useRef(null) const dragCounterRef = useRef(0) + const [isBlinking, setIsBlinking] = useState(false) + + const clearBlinkTimer = useCallback(() => { + if (blinkTimerRef.current) { + clearTimeout(blinkTimerRef.current) + blinkTimerRef.current = null + } + setIsBlinking(false) + }, []) const clearExpandTimer = useCallback(() => { if (expandTimerRef.current) { clearTimeout(expandTimerRef.current) expandTimerRef.current = null } - }, []) + clearBlinkTimer() + }, [clearBlinkTimer]) const scheduleAutoExpand = useCallback(() => { + // Skip if not a folder or already open if (!isFolder || node.isOpen) return clearExpandTimer() + + // Start blinking after 1 second + blinkTimerRef.current = setTimeout(() => { + blinkTimerRef.current = null + setIsBlinking(true) + }, BLINK_START_DELAY_MS) + + // Expand folder after 2 seconds expandTimerRef.current = setTimeout(() => { expandTimerRef.current = null + setIsBlinking(false) if (!node.isOpen) node.open() }, AUTO_EXPAND_DELAY_MS) @@ -94,6 +118,7 @@ export function useFolderFileDrop(node: NodeApi): UseFolderFileDro return { isDragOver, + isBlinking, dragHandlers, } } diff --git a/web/tailwind-common-config.ts b/web/tailwind-common-config.ts index 6fd8c8fada..e120e7df87 100644 --- a/web/tailwind-common-config.ts +++ b/web/tailwind-common-config.ts @@ -141,6 +141,13 @@ const config = { }, animation: { 'spin-slow': 'spin 2s linear infinite', + 'drag-blink': 'drag-blink 400ms ease-in-out infinite', + }, + keyframes: { + 'drag-blink': { + '0%, 100%': { opacity: '1' }, + '50%': { opacity: '0' }, + }, }, }, },