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

View file

@ -59,7 +59,7 @@ export default function SharedFilesPage() {
return ( return (
<div className="flex-1 overflow-y-auto p-6"> <div className="flex-1 overflow-y-auto p-6">
<div className="max-w-5xl mx-auto"> <div className="w-full">
<h2 className="text-xl font-semibold mb-6" style={{ color: 'var(--color-text-primary)' }}>Shared Files</h2> <h2 className="text-xl font-semibold mb-6" style={{ color: 'var(--color-text-primary)' }}>Shared Files</h2>
{shares.length === 0 ? ( {shares.length === 0 ? (