diff --git a/src/App.css b/src/App.css
index 003de71..d00dd29 100644
--- a/src/App.css
+++ b/src/App.css
@@ -187,6 +187,15 @@ body,
}
}
+@keyframes sidebar-spring-folder-pulse {
+ 0% {
+ box-shadow: 0 0 0 0 rgba(51, 177, 255, 0.22);
+ }
+ 100% {
+ box-shadow: 0 0 0 6px rgba(51, 177, 255, 0);
+ }
+}
+
.sidebar-drop-pulse {
animation: sidebar-drop-pulse 0.58s ease-out 2 forwards;
}
@@ -195,6 +204,20 @@ body,
animation: sidebar-drop-edge-pulse 0.36s ease-out 2 forwards;
}
+.sidebar-drop-insertion-line {
+ background: var(--color-border-interactive);
+}
+
+.sidebar-drop-insertion-dot {
+ box-shadow: 0 0 0 1px color-mix(in oklab, var(--color-border-interactive) 55%, transparent);
+}
+
+.sidebar-folder-spring-pending {
+ border: 1px dashed color-mix(in oklab, var(--color-border-interactive) 70%, transparent);
+ background: color-mix(in oklab, var(--color-layer-hover-01) 78%, var(--color-border-interactive) 22%);
+ animation: sidebar-spring-folder-pulse 0.8s ease-out infinite;
+}
+
.sidebar-drag-ghost {
position: fixed;
top: -9999px;
@@ -236,7 +259,8 @@ body,
animation: none;
}
.sidebar-drop-pulse,
- .sidebar-drop-edge-pulse {
+ .sidebar-drop-edge-pulse,
+ .sidebar-folder-spring-pending {
animation: none;
}
}
diff --git a/src/components/Sidebar/DocumentItem.tsx b/src/components/Sidebar/DocumentItem.tsx
index e6ff459..a2ba6b0 100644
--- a/src/components/Sidebar/DocumentItem.tsx
+++ b/src/components/Sidebar/DocumentItem.tsx
@@ -150,11 +150,11 @@ export function DocumentItem(
{closestEdge && (
)}
diff --git a/src/components/Sidebar/SidebarLocationItem.tsx b/src/components/Sidebar/SidebarLocationItem.tsx
index 3bdd222..9cc1c83 100644
--- a/src/components/Sidebar/SidebarLocationItem.tsx
+++ b/src/components/Sidebar/SidebarLocationItem.tsx
@@ -87,6 +87,7 @@ type SidebarTreeContextValue = {
activeDropFolderPath?: string;
activeDropFolderEdge?: Edge | null;
activeDropFolderIntent?: "into" | "between";
+ pendingSpringFolderPath?: string | null;
activeDropDocumentPath?: string;
activeDropDocumentEdge?: Edge | null;
activeDropDocumentIsReorder?: boolean;
@@ -236,6 +237,7 @@ function NestedDirectoryItem({ node, level, expandedDirectories, onToggleDirecto
const folderEdge = isActiveFolderTarget ? dropIndicators.activeDropFolderEdge ?? null : null;
const showInsertionLine = isActiveFolderTarget && dropIndicators.activeDropFolderIntent === "between"
&& (folderEdge === "top" || folderEdge === "bottom");
+ const isSpringPending = dropIndicators.pendingSpringFolderPath === node.path && !isExpanded;
const edgeStyle = useMemo(() => {
if (!folderEdge) {
return {};
@@ -276,6 +278,7 @@ function NestedDirectoryItem({ node, level, expandedDirectories, onToggleDirecto
className={cn(
"pb-0.5",
isDropIntoTarget ? "rounded border-2 border-border-interactive bg-layer-hover-01" : "",
+ isSpringPending ? "sidebar-folder-spring-pending rounded" : "",
skipAnimation ? "" : "transition-[box-shadow,background-color] duration-150",
)}>
-
+
)
: null}
@@ -354,6 +357,7 @@ function SidebarLocationItemComponent(
) {
const { filenameVisibility, documentActions, onToggleLocation, openDocumentOperation } = useSidebarLocationContext();
const [expandedDirectories, setExpandedDirectories] = useState>(new Set());
+ const [pendingSpringFolderPath, setPendingSpringFolderPath] = useState(null);
const hoverExpandRef = useRef<{ path: string; timer: ReturnType } | null>(null);
const dragExpandedSnapshotRef = useRef | null>(null);
const autoExpandedDuringDragRef = useRef>(new Set());
@@ -420,10 +424,12 @@ function SidebarLocationItemComponent(
const clearHoverExpand = useCallback(() => {
if (!hoverExpandRef.current) {
+ setPendingSpringFolderPath(null);
return;
}
globalThis.clearTimeout(hoverExpandRef.current.timer);
hoverExpandRef.current = null;
+ setPendingSpringFolderPath(null);
}, []);
useEffect(() => {
@@ -441,6 +447,7 @@ function SidebarLocationItemComponent(
clearHoverExpand();
const folderPath = activeDropFolderPath;
+ setPendingSpringFolderPath(folderPath);
hoverExpandRef.current = {
path: folderPath,
timer: globalThis.setTimeout(() => {
@@ -453,6 +460,7 @@ function SidebarLocationItemComponent(
}
return new Set([...previous, folderPath]);
});
+ setPendingSpringFolderPath((currentPath) => currentPath === folderPath ? null : currentPath);
hoverExpandRef.current = null;
}, 800),
};
@@ -508,6 +516,7 @@ function SidebarLocationItemComponent(
activeDropFolderPath,
activeDropFolderEdge,
activeDropFolderIntent,
+ pendingSpringFolderPath,
activeDropDocumentPath,
activeDropDocumentEdge,
activeDropDocumentIsReorder,
@@ -522,6 +531,7 @@ function SidebarLocationItemComponent(
activeDropFolderEdge,
activeDropFolderIntent,
activeDropFolderPath,
+ pendingSpringFolderPath,
activeDragDocumentPath,
documentActions,
filenameVisibility,
diff --git a/src/components/Sidebar/useSidebarInternalDnD.ts b/src/components/Sidebar/useSidebarInternalDnD.ts
index e1851b5..9802ac0 100644
--- a/src/components/Sidebar/useSidebarInternalDnD.ts
+++ b/src/components/Sidebar/useSidebarInternalDnD.ts
@@ -154,6 +154,7 @@ export function useSidebarInternalDnD(
const dropZoneRef = useRef(null);
const nativeDragPosRef = useRef<{ x: number; y: number } | null>(null);
const resolvedDestinationRef = useRef(UNSET_DROP_DESTINATION);
+ const activeDragTitleRef = useRef(null);
const edgeScrollRafRef = useRef(null);
const edgeScrollDirectionRef = useRef<1 | -1 | 0>(0);
const dragLeaveClearTimerRef = useRef | null>(null);
@@ -275,13 +276,20 @@ export function useSidebarInternalDnD(
}, [cancelDeferredDragLeaveClear, stopEdgeScroll]);
useEffect(() => {
- const handleGlobalDragEnd = () => {
+ const handleGlobalDragEnd = (event: Event) => {
+ const dropEffect = (event as globalThis.DragEvent).dataTransfer?.dropEffect;
+ const dragTitle = activeDragTitleRef.current;
+ if (dropEffect === "none" && dragTitle) {
+ announce(`Cancelled drag for ${dragTitle}`);
+ }
+
setActiveDropTarget(null);
setIsDraggingInternal(false);
setDragGhostLabel(null);
setSuppressActiveDragSourceOpacity(false);
setActiveDragDocumentPath(null);
setActiveDragDocumentLocationId(null);
+ activeDragTitleRef.current = null;
resolvedDestinationRef.current = UNSET_DROP_DESTINATION;
cancelDeferredDragLeaveClear();
stopEdgeScroll();
@@ -325,6 +333,7 @@ export function useSidebarInternalDnD(
}
setIsDraggingInternal(true);
setDragGhostLabel(source.data.title);
+ activeDragTitleRef.current = source.data.title;
resolvedDestinationRef.current = UNSET_DROP_DESTINATION;
setActiveDropTarget(null);
setSuppressActiveDragSourceOpacity(false);
@@ -390,6 +399,7 @@ export function useSidebarInternalDnD(
setSuppressActiveDragSourceOpacity(false);
setActiveDragDocumentPath(null);
setActiveDragDocumentLocationId(null);
+ activeDragTitleRef.current = null;
cancelDeferredDragLeaveClear();
stopEdgeScroll();
@@ -670,6 +680,7 @@ export function useSidebarInternalDnD(
setSuppressActiveDragSourceOpacity(false);
setActiveDragDocumentPath(null);
setActiveDragDocumentLocationId(null);
+ activeDragTitleRef.current = null;
resolvedDestinationRef.current = UNSET_DROP_DESTINATION;
cancelDeferredDragLeaveClear();
stopEdgeScroll();