fix(ui): improve drag selection scroll behavior, add right-click to unpin sidebar items, close preview on back navigation, fix shared files alignment
Some checks failed
Automated Container Build / build-and-push (push) Failing after 9s

This commit is contained in:
Elijah 2026-05-23 17:00:09 -07:00
parent fbad71cbf2
commit 59407278d1
2 changed files with 52 additions and 19 deletions

View file

@ -220,6 +220,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
const handlePopState = (e: PopStateEvent) => {
if (e.state && e.state.path) setCurrentPath(e.state.path);
else setCurrentPath(new URLSearchParams(window.location.search).get('path') || '.');
setPreviewFile(null);
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
@ -273,7 +274,9 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
const [pinnedOrder, setPinnedOrder] = useState<string[]>([]);
const [dragOverPinnedItem, setDragOverPinnedItem] = useState<string | null>(null);
const [pinnedRefreshCounter, setPinnedRefreshCounter] = useState(0);
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
const [selectionBox, setSelectionBox] = useState<{ startX: number; startY: number; currentX: number; currentY: number } | null>(null);
const lastMousePos = useRef<{clientX: number, clientY: number} | null>(null);
const [initialSelectedFiles, setInitialSelectedFiles] = useState<Set<string>>(new Set());
const searchTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [downloadToken, setDownloadToken] = useState<string>('');
@ -527,12 +530,18 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
if (target.closest('button') || target.closest('input') || target.closest('[data-file-item]')) {
return;
}
const rect = mainContainerRef.current!.getBoundingClientRect();
const scrollLeft = mainContainerRef.current!.scrollLeft;
const scrollTop = mainContainerRef.current!.scrollTop;
setSelectionBox({
startX: e.clientX,
startY: e.clientY,
currentX: e.clientX,
currentY: e.clientY,
startX: e.clientX - rect.left + scrollLeft,
startY: e.clientY - rect.top + scrollTop,
currentX: e.clientX - rect.left + scrollLeft,
currentY: e.clientY - rect.top + scrollTop,
});
lastMousePos.current = { clientX: e.clientX, clientY: e.clientY };
if (e.shiftKey || e.ctrlKey || e.metaKey) {
setInitialSelectedFiles(new Set(selectedFiles));
} else {
@ -541,26 +550,35 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
}
}
function handlePointerMove(e: React.PointerEvent) {
if (!selectionBox) return;
setSelectionBox(prev => prev ? { ...prev, currentX: e.clientX, currentY: e.clientY } : null);
function updateSelection(clientX: number, clientY: number) {
setSelectionBox(prev => {
if (!prev || !mainContainerRef.current) return null;
const rect = mainContainerRef.current.getBoundingClientRect();
const scrollLeft = mainContainerRef.current.scrollLeft;
const scrollTop = mainContainerRef.current.scrollTop;
const next = { ...prev, currentX: clientX - rect.left + scrollLeft, currentY: clientY - rect.top + scrollTop };
if (mainContainerRef.current) {
const boxLeft = Math.min(selectionBox.startX, e.clientX);
const boxRight = Math.max(selectionBox.startX, e.clientX);
const boxTop = Math.min(selectionBox.startY, e.clientY);
const boxBottom = Math.max(selectionBox.startY, e.clientY);
const boxLeft = Math.min(next.startX, next.currentX);
const boxRight = Math.max(next.startX, next.currentX);
const boxTop = Math.min(next.startY, next.currentY);
const boxBottom = Math.max(next.startY, next.currentY);
const viewportBoxLeft = boxLeft + rect.left - scrollLeft;
const viewportBoxRight = boxRight + rect.left - scrollLeft;
const viewportBoxTop = boxTop + rect.top - scrollTop;
const viewportBoxBottom = boxBottom + rect.top - scrollTop;
const fileNodes = mainContainerRef.current.querySelectorAll('[data-file-item]');
const newSelected = new Set(initialSelectedFiles);
fileNodes.forEach(node => {
const rect = node.getBoundingClientRect();
const fileRect = node.getBoundingClientRect();
const intersects = !(
rect.right < boxLeft ||
rect.left > boxRight ||
rect.bottom < boxTop ||
rect.top > boxBottom
fileRect.right < viewportBoxLeft ||
fileRect.left > viewportBoxRight ||
fileRect.bottom < viewportBoxTop ||
fileRect.top > viewportBoxBottom
);
const filePath = node.getAttribute('data-file-path');
@ -574,6 +592,19 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
});
setSelectedFiles(newSelected);
return next;
});
}
function handlePointerMove(e: React.PointerEvent) {
if (!selectionBox) return;
lastMousePos.current = { clientX: e.clientX, clientY: e.clientY };
updateSelection(e.clientX, e.clientY);
}
function handleScroll(e: React.UIEvent) {
if (selectionBox && lastMousePos.current) {
updateSelection(lastMousePos.current.clientX, lastMousePos.current.clientY);
}
}
@ -1270,6 +1301,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
borderTop: dragOverPinnedItem === file.path ? '2px solid var(--color-accent)' : '2px solid transparent',
}}
onClick={() => { setActiveSection('files'); navigateTo(file.path); }}
onContextMenu={(e) => handleContextMenu(e, file)}
>
<div className="w-4 h-4 flex items-center justify-center flex-shrink-0 [&>svg]:w-4 [&>svg]:h-4">
{getFileIcon(file)}
@ -1490,10 +1522,11 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerLeave={handlePointerUp}
onScroll={handleScroll}
>
{selectionBox && (
<div
className="fixed z-50 pointer-events-none"
className="absolute z-50 pointer-events-none"
style={{
left: Math.min(selectionBox.startX, selectionBox.currentX),
top: Math.min(selectionBox.startY, selectionBox.currentY),