feat: add VSCode-style blink animation before folder auto-expand

When dragging files over a closed folder, the highlight now blinks
during the second half of the 2-second hover period to signal that
the folder is about to expand. This provides better visual feedback
similar to VSCode's drag-and-drop behavior.
This commit is contained in:
yyh
2026-01-19 18:21:24 +08:00
parent 83c458d2fe
commit 7756c151ed
3 changed files with 38 additions and 4 deletions

View File

@ -42,7 +42,7 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>)
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<TreeNodeData>)
'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}

View File

@ -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<TreeNodeData>): UseFolderFileDropReturn {
@ -27,21 +30,42 @@ export function useFolderFileDrop(node: NodeApi<TreeNodeData>): UseFolderFileDro
const { handleDragOver, handleDrop } = useFileDrop()
const expandTimerRef = useRef<NodeJS.Timeout | null>(null)
const blinkTimerRef = useRef<NodeJS.Timeout | null>(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<TreeNodeData>): UseFolderFileDro
return {
isDragOver,
isBlinking,
dragHandlers,
}
}

View File

@ -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' },
},
},
},
},