mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-01-14 20:11:17 +01:00
improved
This commit is contained in:
parent
74e8388bce
commit
e7f7b7e201
58
frontend/package-lock.json
generated
58
frontend/package-lock.json
generated
@ -10,6 +10,7 @@
|
|||||||
"license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
|
"license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@embedpdf/core": "^1.3.14",
|
"@embedpdf/core": "^1.3.14",
|
||||||
"@embedpdf/engines": "^1.3.14",
|
"@embedpdf/engines": "^1.3.14",
|
||||||
"@embedpdf/plugin-annotation": "^1.3.14",
|
"@embedpdf/plugin-annotation": "^1.3.14",
|
||||||
@ -496,6 +497,63 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/accessibility/node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/core": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/core/node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/utilities": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/utilities/node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/@embedpdf/core": {
|
"node_modules/@embedpdf/core": {
|
||||||
"version": "1.3.14",
|
"version": "1.3.14",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.14.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.14.tgz",
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
"proxy": "http://localhost:8080",
|
"proxy": "http://localhost:8080",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@embedpdf/core": "^1.3.14",
|
"@embedpdf/core": "^1.3.14",
|
||||||
"@embedpdf/engines": "^1.3.14",
|
"@embedpdf/engines": "^1.3.14",
|
||||||
"@embedpdf/plugin-annotation": "^1.3.14",
|
"@embedpdf/plugin-annotation": "^1.3.14",
|
||||||
|
|||||||
@ -2,6 +2,20 @@ import React, { useRef, useEffect, useState, useCallback } from 'react';
|
|||||||
import { Box } from '@mantine/core';
|
import { Box } from '@mantine/core';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { GRID_CONSTANTS } from './constants';
|
import { GRID_CONSTANTS } from './constants';
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
DragStartEvent,
|
||||||
|
DragOverEvent,
|
||||||
|
DragOverlay,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
PointerSensor,
|
||||||
|
closestCenter,
|
||||||
|
useDndMonitor,
|
||||||
|
useDraggable,
|
||||||
|
useDroppable,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
|
||||||
interface DragDropItem {
|
interface DragDropItem {
|
||||||
id: string;
|
id: string;
|
||||||
@ -15,13 +29,93 @@ interface DragDropGridProps<T extends DragDropItem> {
|
|||||||
selectionMode: boolean;
|
selectionMode: boolean;
|
||||||
isAnimating: boolean;
|
isAnimating: boolean;
|
||||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
||||||
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>) => React.ReactNode;
|
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>, boxSelectedIds: string[], clearBoxSelection: () => void, getBoxSelection: () => string[], activeId: string | null, isOver: boolean, dropSide: 'left' | 'right' | null, dragHandleProps?: any) => React.ReactNode;
|
||||||
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
|
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
|
||||||
|
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lightweight wrapper that handles dnd-kit hooks for each visible item
|
||||||
|
interface DraggableItemProps<T extends DragDropItem> {
|
||||||
|
item: T;
|
||||||
|
index: number;
|
||||||
|
itemRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
|
||||||
|
boxSelectedPageIds: string[];
|
||||||
|
clearBoxSelection: () => void;
|
||||||
|
getBoxSelection: () => string[];
|
||||||
|
activeId: string | null;
|
||||||
|
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
||||||
|
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>, boxSelectedIds: string[], clearBoxSelection: () => void, getBoxSelection: () => string[], activeId: string | null, isOver: boolean, dropSide: 'left' | 'right' | null, dragHandleProps?: any) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DraggableItem = <T extends DragDropItem>({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, getThumbnailData, renderItem }: DraggableItemProps<T>) => {
|
||||||
|
const elementRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
const [dropSide, setDropSide] = React.useState<'left' | 'right' | null>(null);
|
||||||
|
|
||||||
|
const { attributes, listeners, setNodeRef: setDraggableRef } = useDraggable({
|
||||||
|
id: item.id,
|
||||||
|
data: {
|
||||||
|
index,
|
||||||
|
pageNumber: index + 1,
|
||||||
|
getThumbnail: () => {
|
||||||
|
if (getThumbnailData) {
|
||||||
|
const data = getThumbnailData(item.id);
|
||||||
|
if (data?.src) return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = itemRefs.current.get(item.id);
|
||||||
|
const imgElement = element?.querySelector('img.ph-no-capture') as HTMLImageElement;
|
||||||
|
if (imgElement?.src) {
|
||||||
|
return {
|
||||||
|
src: imgElement.src,
|
||||||
|
rotation: imgElement.dataset.originalRotation ? parseInt(imgElement.dataset.originalRotation) : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { setNodeRef: setDroppableRef, isOver } = useDroppable({
|
||||||
|
id: item.id,
|
||||||
|
data: { index, pageNumber: index + 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track cursor position when hovered to determine drop side
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isOver || !elementRef.current) {
|
||||||
|
setDropSide(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePointerMove = (e: PointerEvent) => {
|
||||||
|
if (!elementRef.current) return;
|
||||||
|
const rect = elementRef.current.getBoundingClientRect();
|
||||||
|
const midpoint = rect.left + rect.width / 2;
|
||||||
|
setDropSide(e.clientX < midpoint ? 'left' : 'right');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
|
return () => window.removeEventListener('pointermove', handlePointerMove);
|
||||||
|
}, [isOver]);
|
||||||
|
|
||||||
|
const setNodeRef = useCallback((element: HTMLDivElement | null) => {
|
||||||
|
elementRef.current = element;
|
||||||
|
setDraggableRef(element);
|
||||||
|
setDroppableRef(element);
|
||||||
|
}, [setDraggableRef, setDroppableRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderItem(item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, isOver, dropSide, { ref: setNodeRef, ...attributes, ...listeners })}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const DragDropGrid = <T extends DragDropItem>({
|
const DragDropGrid = <T extends DragDropItem>({
|
||||||
items,
|
items,
|
||||||
renderItem,
|
renderItem,
|
||||||
|
onReorderPages,
|
||||||
|
getThumbnailData,
|
||||||
}: DragDropGridProps<T>) => {
|
}: DragDropGridProps<T>) => {
|
||||||
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -29,6 +123,21 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
// Filter out placeholder items (invisible pages for deselected files)
|
// Filter out placeholder items (invisible pages for deselected files)
|
||||||
const visibleItems = items.filter(item => !item.isPlaceholder);
|
const visibleItems = items.filter(item => !item.isPlaceholder);
|
||||||
|
|
||||||
|
// Box selection state
|
||||||
|
const [boxSelectStart, setBoxSelectStart] = useState<{ x: number; y: number } | null>(null);
|
||||||
|
const [boxSelectEnd, setBoxSelectEnd] = useState<{ x: number; y: number } | null>(null);
|
||||||
|
const [isBoxSelecting, setIsBoxSelecting] = useState(false);
|
||||||
|
const [boxSelectedPageIds, setBoxSelectedPageIds] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Drag state
|
||||||
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
|
const [dragPreview, setDragPreview] = useState<{ src: string; rotation: number } | null>(null);
|
||||||
|
|
||||||
|
// Configure sensors for dnd-kit
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor)
|
||||||
|
);
|
||||||
|
|
||||||
// Responsive grid configuration
|
// Responsive grid configuration
|
||||||
const [itemsPerRow, setItemsPerRow] = useState(4);
|
const [itemsPerRow, setItemsPerRow] = useState(4);
|
||||||
const OVERSCAN = visibleItems.length > 1000 ? GRID_CONSTANTS.OVERSCAN_LARGE : GRID_CONSTANTS.OVERSCAN_SMALL;
|
const OVERSCAN = visibleItems.length > 1000 ? GRID_CONSTANTS.OVERSCAN_LARGE : GRID_CONSTANTS.OVERSCAN_SMALL;
|
||||||
@ -89,21 +198,199 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
overscan: OVERSCAN,
|
overscan: OVERSCAN,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Box selection handlers
|
||||||
|
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||||
|
// Only start box select if Ctrl/Cmd is held
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
const rect = containerRef.current?.getBoundingClientRect();
|
||||||
|
if (!rect) return;
|
||||||
|
|
||||||
|
// Clear previous box selection when starting new one
|
||||||
|
setIsBoxSelecting(true);
|
||||||
|
setBoxSelectStart({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
||||||
|
setBoxSelectEnd({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
||||||
|
setBoxSelectedPageIds([]);
|
||||||
|
} else {
|
||||||
|
// Clear box selection when clicking without Ctrl
|
||||||
|
if (boxSelectedPageIds.length > 0) {
|
||||||
|
setBoxSelectedPageIds([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [boxSelectedPageIds.length]);
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (!isBoxSelecting || !boxSelectStart) return;
|
||||||
|
|
||||||
|
const rect = containerRef.current?.getBoundingClientRect();
|
||||||
|
if (!rect) return;
|
||||||
|
|
||||||
|
setBoxSelectEnd({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
||||||
|
|
||||||
|
// Calculate which pages intersect with selection box
|
||||||
|
const boxLeft = Math.min(boxSelectStart.x, e.clientX - rect.left);
|
||||||
|
const boxRight = Math.max(boxSelectStart.x, e.clientX - rect.left);
|
||||||
|
const boxTop = Math.min(boxSelectStart.y, e.clientY - rect.top);
|
||||||
|
const boxBottom = Math.max(boxSelectStart.y, e.clientY - rect.top);
|
||||||
|
|
||||||
|
const selectedIds: string[] = [];
|
||||||
|
itemRefs.current.forEach((pageEl, pageId) => {
|
||||||
|
const pageRect = pageEl.getBoundingClientRect();
|
||||||
|
const pageLeft = pageRect.left - rect.left;
|
||||||
|
const pageRight = pageRect.right - rect.left;
|
||||||
|
const pageTop = pageRect.top - rect.top;
|
||||||
|
const pageBottom = pageRect.bottom - rect.top;
|
||||||
|
|
||||||
|
// Check if page intersects with selection box
|
||||||
|
const intersects = !(
|
||||||
|
pageRight < boxLeft ||
|
||||||
|
pageLeft > boxRight ||
|
||||||
|
pageBottom < boxTop ||
|
||||||
|
pageTop > boxBottom
|
||||||
|
);
|
||||||
|
|
||||||
|
if (intersects) {
|
||||||
|
selectedIds.push(pageId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setBoxSelectedPageIds(selectedIds);
|
||||||
|
}, [isBoxSelecting, boxSelectStart]);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
if (isBoxSelecting) {
|
||||||
|
// Keep box-selected pages highlighted (don't clear boxSelectedPageIds yet)
|
||||||
|
// They will remain highlighted until next interaction
|
||||||
|
setIsBoxSelecting(false);
|
||||||
|
setBoxSelectStart(null);
|
||||||
|
setBoxSelectEnd(null);
|
||||||
|
}
|
||||||
|
}, [isBoxSelecting]);
|
||||||
|
|
||||||
|
// Function to clear box selection (exposed to child components)
|
||||||
|
const clearBoxSelection = useCallback(() => {
|
||||||
|
setBoxSelectedPageIds([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Function to get current box selection (exposed to child components)
|
||||||
|
const getBoxSelection = useCallback(() => {
|
||||||
|
return boxSelectedPageIds;
|
||||||
|
}, [boxSelectedPageIds]);
|
||||||
|
|
||||||
|
// Handle drag start
|
||||||
|
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||||
|
const activeId = event.active.id as string;
|
||||||
|
setActiveId(activeId);
|
||||||
|
|
||||||
|
// Call the getter function to get fresh thumbnail data
|
||||||
|
const getThumbnail = event.active.data.current?.getThumbnail;
|
||||||
|
if (getThumbnail) {
|
||||||
|
const thumbnailData = getThumbnail();
|
||||||
|
if (thumbnailData?.src) {
|
||||||
|
setDragPreview({ src: thumbnailData.src, rotation: thumbnailData.rotation });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragPreview(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
// Handle drag cancel
|
||||||
|
const handleDragCancel = useCallback(() => {
|
||||||
|
setActiveId(null);
|
||||||
|
setDragPreview(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle drag end
|
||||||
|
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
|
||||||
|
setActiveId(null);
|
||||||
|
setDragPreview(null);
|
||||||
|
|
||||||
|
if (!over || active.id === over.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get data from hooks
|
||||||
|
const activeData = active.data.current;
|
||||||
|
const overData = over.data.current;
|
||||||
|
|
||||||
|
if (!activeData || !overData) return;
|
||||||
|
|
||||||
|
const sourcePageNumber = activeData.pageNumber;
|
||||||
|
let targetIndex = overData.index;
|
||||||
|
|
||||||
|
// Calculate drop position from final cursor position
|
||||||
|
const overElement = itemRefs.current.get(over.id as string);
|
||||||
|
if (overElement) {
|
||||||
|
const activatorEvent = (event as any).activatorEvent;
|
||||||
|
const cursorX = activatorEvent ? activatorEvent.clientX + (event.delta?.x || 0) : null;
|
||||||
|
if (cursorX) {
|
||||||
|
const rect = overElement.getBoundingClientRect();
|
||||||
|
const midpoint = rect.left + rect.width / 2;
|
||||||
|
if (cursorX >= midpoint) {
|
||||||
|
targetIndex = targetIndex + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this page is box-selected
|
||||||
|
const isBoxSelected = boxSelectedPageIds.includes(active.id as string);
|
||||||
|
const pagesToDrag = isBoxSelected && boxSelectedPageIds.length > 0 ? boxSelectedPageIds : undefined;
|
||||||
|
|
||||||
|
// Call reorder with page number and target index
|
||||||
|
onReorderPages(sourcePageNumber, targetIndex, pagesToDrag);
|
||||||
|
|
||||||
|
// Clear box selection after drag
|
||||||
|
if (pagesToDrag) {
|
||||||
|
clearBoxSelection();
|
||||||
|
}
|
||||||
|
}, [boxSelectedPageIds, onReorderPages, clearBoxSelection]);
|
||||||
|
|
||||||
// Calculate optimal width for centering
|
// Calculate optimal width for centering
|
||||||
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
const itemWidth = parseFloat(GRID_CONSTANTS.ITEM_WIDTH) * remToPx;
|
const itemWidth = parseFloat(GRID_CONSTANTS.ITEM_WIDTH) * remToPx;
|
||||||
const itemGap = parseFloat(GRID_CONSTANTS.ITEM_GAP) * remToPx;
|
const itemGap = parseFloat(GRID_CONSTANTS.ITEM_GAP) * remToPx;
|
||||||
const gridWidth = itemsPerRow * itemWidth + (itemsPerRow - 1) * itemGap;
|
const gridWidth = itemsPerRow * itemWidth + (itemsPerRow - 1) * itemGap;
|
||||||
|
|
||||||
|
// Calculate selection box dimensions
|
||||||
|
const selectionBoxStyle = isBoxSelecting && boxSelectStart && boxSelectEnd ? {
|
||||||
|
position: 'absolute' as const,
|
||||||
|
left: Math.min(boxSelectStart.x, boxSelectEnd.x),
|
||||||
|
top: Math.min(boxSelectStart.y, boxSelectEnd.y),
|
||||||
|
width: Math.abs(boxSelectEnd.x - boxSelectStart.x),
|
||||||
|
height: Math.abs(boxSelectEnd.y - boxSelectStart.y),
|
||||||
|
border: '2px dashed #3b82f6',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
pointerEvents: 'none' as const,
|
||||||
|
zIndex: 1000,
|
||||||
|
} : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<DndContext
|
||||||
ref={containerRef}
|
sensors={sensors}
|
||||||
style={{
|
collisionDetection={closestCenter}
|
||||||
// Basic container styles
|
onDragStart={handleDragStart}
|
||||||
width: '100%',
|
onDragEnd={handleDragEnd}
|
||||||
height: '100%',
|
onDragCancel={handleDragCancel}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
<Box
|
||||||
|
ref={containerRef}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
style={{
|
||||||
|
// Basic container styles
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Selection box overlay */}
|
||||||
|
{selectionBoxStyle && <div style={selectionBoxStyle} />}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||||
@ -143,10 +430,18 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
{rowItems.map((item, itemIndex) => {
|
{rowItems.map((item, itemIndex) => {
|
||||||
const actualIndex = startIndex + itemIndex;
|
const actualIndex = startIndex + itemIndex;
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={item.id}>
|
<DraggableItem
|
||||||
{/* Item */}
|
key={item.id}
|
||||||
{renderItem(item, actualIndex, itemRefs)}
|
item={item}
|
||||||
</React.Fragment>
|
index={actualIndex}
|
||||||
|
itemRefs={itemRefs}
|
||||||
|
boxSelectedPageIds={boxSelectedPageIds}
|
||||||
|
clearBoxSelection={clearBoxSelection}
|
||||||
|
getBoxSelection={getBoxSelection}
|
||||||
|
activeId={activeId}
|
||||||
|
getThumbnailData={getThumbnailData}
|
||||||
|
renderItem={renderItem}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
@ -156,6 +451,66 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Drag Overlay */}
|
||||||
|
<DragOverlay>
|
||||||
|
{activeId && (
|
||||||
|
<div style={{ position: 'relative', cursor: 'grabbing' }}>
|
||||||
|
{/* Multi-page badge */}
|
||||||
|
{boxSelectedPageIds.includes(activeId) && boxSelectedPageIds.length > 1 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
color: 'white',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
|
||||||
|
zIndex: 1001
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{boxSelectedPageIds.length}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Just the thumbnail image */}
|
||||||
|
{dragPreview ? (
|
||||||
|
<img
|
||||||
|
src={dragPreview.src}
|
||||||
|
alt="Dragging"
|
||||||
|
style={{
|
||||||
|
width: '20rem',
|
||||||
|
height: '20rem',
|
||||||
|
objectFit: 'contain',
|
||||||
|
transform: `rotate(${dragPreview.rotation}deg)`,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{
|
||||||
|
width: '20rem',
|
||||||
|
height: '20rem',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '48px',
|
||||||
|
opacity: 0.5,
|
||||||
|
}}>
|
||||||
|
📄
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DragOverlay>
|
||||||
|
</DndContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
|
|||||||
import { useFileState, useFileActions } from "../../contexts/FileContext";
|
import { useFileState, useFileActions } from "../../contexts/FileContext";
|
||||||
import { useNavigationGuard } from "../../contexts/NavigationContext";
|
import { useNavigationGuard } from "../../contexts/NavigationContext";
|
||||||
import { usePageEditor } from "../../contexts/PageEditorContext";
|
import { usePageEditor } from "../../contexts/PageEditorContext";
|
||||||
import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor";
|
import { PDFDocument, PDFPage, PageEditorFunctions } from "../../types/pageEditor";
|
||||||
import { pdfExportService } from "../../services/pdfExportService";
|
import { pdfExportService } from "../../services/pdfExportService";
|
||||||
import { documentManipulationService } from "../../services/documentManipulationService";
|
import { documentManipulationService } from "../../services/documentManipulationService";
|
||||||
import { exportProcessedDocumentsToFiles } from "../../services/pdfExportHelpers";
|
import { exportProcessedDocumentsToFiles } from "../../services/pdfExportHelpers";
|
||||||
@ -604,9 +604,14 @@ const PageEditor = ({
|
|||||||
const handleReorderPages = useCallback((sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => {
|
const handleReorderPages = useCallback((sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => {
|
||||||
if (!displayDocument) return;
|
if (!displayDocument) return;
|
||||||
|
|
||||||
|
console.log('=== HANDLE REORDER PAGES ===');
|
||||||
|
console.log('selectedPageIds:', selectedPageIds);
|
||||||
|
|
||||||
// Convert selectedPageIds to page numbers for the reorder command
|
// Convert selectedPageIds to page numbers for the reorder command
|
||||||
const selectedPages = selectedPageIds ? getPageNumbersFromIds(selectedPageIds) : undefined;
|
const selectedPages = selectedPageIds ? getPageNumbersFromIds(selectedPageIds) : undefined;
|
||||||
|
|
||||||
|
console.log('selectedPages (converted to numbers):', selectedPages);
|
||||||
|
|
||||||
const reorderCommand = new ReorderPagesCommand(
|
const reorderCommand = new ReorderPagesCommand(
|
||||||
sourcePageNumber,
|
sourcePageNumber,
|
||||||
targetIndex,
|
targetIndex,
|
||||||
@ -961,8 +966,17 @@ const PageEditor = ({
|
|||||||
selectionMode={selectionMode}
|
selectionMode={selectionMode}
|
||||||
isAnimating={isAnimating}
|
isAnimating={isAnimating}
|
||||||
onReorderPages={handleReorderPages}
|
onReorderPages={handleReorderPages}
|
||||||
renderItem={(page, index, refs) => {
|
getThumbnailData={(pageId) => {
|
||||||
|
const page = displayDocument.pages.find(p => p.id === pageId);
|
||||||
|
if (!page?.thumbnail) return null;
|
||||||
|
return {
|
||||||
|
src: page.thumbnail,
|
||||||
|
rotation: page.rotation || 0
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
renderItem={(page, index, refs, boxSelectedIds, clearBoxSelection, getBoxSelection, activeId, isOver, dropSide, dragHandleProps) => {
|
||||||
const fileColorIndex = page.originalFileId ? fileColorIndexMap.get(page.originalFileId) ?? 0 : 0;
|
const fileColorIndex = page.originalFileId ? fileColorIndexMap.get(page.originalFileId) ?? 0 : 0;
|
||||||
|
const isBoxSelected = boxSelectedIds.includes(page.id);
|
||||||
return (
|
return (
|
||||||
<PageThumbnail
|
<PageThumbnail
|
||||||
key={page.id}
|
key={page.id}
|
||||||
@ -975,7 +989,15 @@ const PageEditor = ({
|
|||||||
selectionMode={selectionMode}
|
selectionMode={selectionMode}
|
||||||
movingPage={movingPage}
|
movingPage={movingPage}
|
||||||
isAnimating={isAnimating}
|
isAnimating={isAnimating}
|
||||||
|
isBoxSelected={isBoxSelected}
|
||||||
|
boxSelectedPageIds={boxSelectedIds}
|
||||||
|
clearBoxSelection={clearBoxSelection}
|
||||||
|
getBoxSelection={getBoxSelection}
|
||||||
|
activeId={activeId}
|
||||||
|
isOver={isOver}
|
||||||
|
dropSide={dropSide}
|
||||||
pageRefs={refs}
|
pageRefs={refs}
|
||||||
|
dragHandleProps={dragHandleProps}
|
||||||
onReorderPages={handleReorderPages}
|
onReorderPages={handleReorderPages}
|
||||||
onTogglePage={togglePage}
|
onTogglePage={togglePage}
|
||||||
onAnimateReorder={animateReorder}
|
onAnimateReorder={animateReorder}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import RotateRightIcon from '@mui/icons-material/RotateRight';
|
|||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import ContentCutIcon from '@mui/icons-material/ContentCut';
|
import ContentCutIcon from '@mui/icons-material/ContentCut';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
||||||
import { PDFPage, PDFDocument } from '../../types/pageEditor';
|
import { PDFPage, PDFDocument } from '../../types/pageEditor';
|
||||||
import { useThumbnailGeneration } from '../../hooks/useThumbnailGeneration';
|
import { useThumbnailGeneration } from '../../hooks/useThumbnailGeneration';
|
||||||
import { useFilesModalContext } from '../../contexts/FilesModalContext';
|
import { useFilesModalContext } from '../../contexts/FilesModalContext';
|
||||||
@ -25,7 +24,15 @@ interface PageThumbnailProps {
|
|||||||
selectionMode: boolean;
|
selectionMode: boolean;
|
||||||
movingPage: number | null;
|
movingPage: number | null;
|
||||||
isAnimating: boolean;
|
isAnimating: boolean;
|
||||||
|
isBoxSelected?: boolean;
|
||||||
|
boxSelectedPageIds?: string[];
|
||||||
|
clearBoxSelection?: () => void;
|
||||||
|
getBoxSelection?: () => string[];
|
||||||
|
activeId: string | null;
|
||||||
|
isOver: boolean;
|
||||||
|
dropSide: 'left' | 'right' | null;
|
||||||
pageRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
|
pageRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
|
||||||
|
dragHandleProps?: any;
|
||||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
||||||
onTogglePage: (pageId: string) => void;
|
onTogglePage: (pageId: string) => void;
|
||||||
onAnimateReorder: () => void;
|
onAnimateReorder: () => void;
|
||||||
@ -52,7 +59,15 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
selectionMode,
|
selectionMode,
|
||||||
movingPage,
|
movingPage,
|
||||||
isAnimating,
|
isAnimating,
|
||||||
|
isBoxSelected = false,
|
||||||
|
boxSelectedPageIds = [],
|
||||||
|
clearBoxSelection,
|
||||||
|
getBoxSelection,
|
||||||
|
activeId,
|
||||||
|
isOver,
|
||||||
|
dropSide,
|
||||||
pageRefs,
|
pageRefs,
|
||||||
|
dragHandleProps,
|
||||||
onReorderPages,
|
onReorderPages,
|
||||||
onTogglePage,
|
onTogglePage,
|
||||||
onExecuteCommand,
|
onExecuteCommand,
|
||||||
@ -65,15 +80,17 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
splitPositions,
|
splitPositions,
|
||||||
onInsertFiles,
|
onInsertFiles,
|
||||||
}: PageThumbnailProps) => {
|
}: PageThumbnailProps) => {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||||
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
|
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
|
||||||
const lastClickTimeRef = useRef<number>(0);
|
const lastClickTimeRef = useRef<number>(0);
|
||||||
const dragElementRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
||||||
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
|
const elementRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const { getThumbnailFromCache, requestThumbnail} = useThumbnailGeneration();
|
||||||
const { openFilesModal } = useFilesModalContext();
|
const { openFilesModal } = useFilesModalContext();
|
||||||
|
|
||||||
|
// Check if this page is currently being dragged
|
||||||
|
const isDragging = activeId === page.id;
|
||||||
|
|
||||||
// Calculate document aspect ratio from first non-blank page
|
// Calculate document aspect ratio from first non-blank page
|
||||||
const getDocumentAspectRatio = useCallback(() => {
|
const getDocumentAspectRatio = useCallback(() => {
|
||||||
// Find first non-blank page with a thumbnail to get aspect ratio
|
// Find first non-blank page with a thumbnail to get aspect ratio
|
||||||
@ -130,63 +147,22 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
};
|
};
|
||||||
}, [page.id, page.thumbnail, originalFile, getThumbnailFromCache, requestThumbnail]);
|
}, [page.id, page.thumbnail, originalFile, getThumbnailFromCache, requestThumbnail]);
|
||||||
|
|
||||||
const pageElementRef = useCallback((element: HTMLDivElement | null) => {
|
// Merge refs - combine our ref tracking with dnd-kit's ref
|
||||||
|
const mergedRef = useCallback((element: HTMLDivElement | null) => {
|
||||||
|
// Track in our refs map
|
||||||
|
elementRef.current = element;
|
||||||
if (element) {
|
if (element) {
|
||||||
pageRefs.current.set(page.id, element);
|
pageRefs.current.set(page.id, element);
|
||||||
dragElementRef.current = element;
|
|
||||||
|
|
||||||
const dragCleanup = draggable({
|
|
||||||
element,
|
|
||||||
getInitialData: () => ({
|
|
||||||
pageNumber: page.pageNumber,
|
|
||||||
pageId: page.id,
|
|
||||||
selectedPageIds: [page.id]
|
|
||||||
}),
|
|
||||||
onDragStart: () => {
|
|
||||||
setIsDragging(true);
|
|
||||||
},
|
|
||||||
onDrop: ({ location }) => {
|
|
||||||
setIsDragging(false);
|
|
||||||
|
|
||||||
if (location.current.dropTargets.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dropTarget = location.current.dropTargets[0];
|
|
||||||
const targetData = dropTarget.data;
|
|
||||||
|
|
||||||
if (targetData.type === 'page') {
|
|
||||||
const targetPageNumber = targetData.pageNumber as number;
|
|
||||||
const targetIndex = pdfDocument.pages.findIndex(p => p.pageNumber === targetPageNumber);
|
|
||||||
if (targetIndex !== -1) {
|
|
||||||
onReorderPages(page.pageNumber, targetIndex, undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
element.style.cursor = 'grab';
|
|
||||||
|
|
||||||
const dropCleanup = dropTargetForElements({
|
|
||||||
element,
|
|
||||||
getData: () => ({
|
|
||||||
type: 'page',
|
|
||||||
pageNumber: page.pageNumber
|
|
||||||
}),
|
|
||||||
onDrop: (_) => {}
|
|
||||||
});
|
|
||||||
|
|
||||||
(element as any).__dragCleanup = () => {
|
|
||||||
dragCleanup();
|
|
||||||
dropCleanup();
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
pageRefs.current.delete(page.id);
|
pageRefs.current.delete(page.id);
|
||||||
if (dragElementRef.current && (dragElementRef.current as any).__dragCleanup) {
|
|
||||||
(dragElementRef.current as any).__dragCleanup();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [page.id, page.pageNumber, pageRefs, selectionMode, selectedPageIds, pdfDocument.pages, onReorderPages]);
|
|
||||||
|
// Call dnd-kit's ref if provided
|
||||||
|
if (dragHandleProps?.ref) {
|
||||||
|
dragHandleProps.ref(element);
|
||||||
|
}
|
||||||
|
}, [page.id, pageRefs, dragHandleProps]);
|
||||||
|
|
||||||
|
|
||||||
// DOM command handlers
|
// DOM command handlers
|
||||||
const handleRotateLeft = useCallback((e: React.MouseEvent) => {
|
const handleRotateLeft = useCallback((e: React.MouseEvent) => {
|
||||||
@ -262,19 +238,28 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
const deltaY = Math.abs(e.clientY - mouseStartPos.y);
|
const deltaY = Math.abs(e.clientY - mouseStartPos.y);
|
||||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||||
|
|
||||||
// If mouse moved less than 5 pixels, consider it a click (not a drag)
|
// If mouse moved less than 2 pixels, consider it a click (not a drag)
|
||||||
if (distance < 5 && !isDragging) {
|
if (distance < 2 && !isDragging) {
|
||||||
// Prevent rapid double-clicks from causing issues (debounce with 100ms threshold)
|
// Prevent rapid double-clicks from causing issues (debounce with 100ms threshold)
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastClickTimeRef.current > 100) {
|
if (now - lastClickTimeRef.current > 100) {
|
||||||
lastClickTimeRef.current = now;
|
lastClickTimeRef.current = now;
|
||||||
onTogglePage(page.id);
|
|
||||||
|
// Clear box selection when clicking on a non-selected page
|
||||||
|
if (!isBoxSelected && clearBoxSelection) {
|
||||||
|
clearBoxSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't toggle page selection if it's box-selected (just keep the box selection)
|
||||||
|
if (!isBoxSelected) {
|
||||||
|
onTogglePage(page.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsMouseDown(false);
|
setIsMouseDown(false);
|
||||||
setMouseStartPos(null);
|
setMouseStartPos(null);
|
||||||
}, [isMouseDown, mouseStartPos, isDragging, page.id, onTogglePage]);
|
}, [isMouseDown, mouseStartPos, isDragging, page.id, isBoxSelected, clearBoxSelection, onTogglePage]);
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(() => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
setIsMouseDown(false);
|
setIsMouseDown(false);
|
||||||
@ -283,9 +268,13 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
|
|
||||||
const fileColorBorder = page.isBlankPage ? 'transparent' : getFileColorWithOpacity(fileColorIndex, 0.3);
|
const fileColorBorder = page.isBlankPage ? 'transparent' : getFileColorWithOpacity(fileColorIndex, 0.3);
|
||||||
|
|
||||||
|
// Spread dragHandleProps but use our merged ref
|
||||||
|
const { ref: _, ...restDragProps } = dragHandleProps || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={pageElementRef}
|
ref={mergedRef}
|
||||||
|
{...restDragProps}
|
||||||
data-page-id={page.id}
|
data-page-id={page.id}
|
||||||
data-page-number={page.pageNumber}
|
data-page-number={page.pageNumber}
|
||||||
className={`
|
className={`
|
||||||
@ -303,15 +292,34 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
relative
|
relative
|
||||||
${isDragging ? 'opacity-50 scale-95' : ''}
|
${isDragging ? 'opacity-50 scale-95' : ''}
|
||||||
${movingPage === page.pageNumber ? 'page-moving' : ''}
|
${movingPage === page.pageNumber ? 'page-moving' : ''}
|
||||||
|
${isBoxSelected ? 'ring-4 ring-blue-400 ring-offset-2' : ''}
|
||||||
`}
|
`}
|
||||||
style={{
|
style={{
|
||||||
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out',
|
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out',
|
||||||
|
...(isBoxSelected && {
|
||||||
|
boxShadow: '0 0 0 4px rgba(59, 130, 246, 0.5)',
|
||||||
|
}),
|
||||||
}}
|
}}
|
||||||
draggable={false}
|
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
|
{/* Drop indicator - bar on left or right edge */}
|
||||||
|
{isOver && !isDragging && dropSide && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
[dropSide]: '-4px',
|
||||||
|
top: '0',
|
||||||
|
width: '4px',
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: '#22c55e',
|
||||||
|
borderRadius: '2px',
|
||||||
|
zIndex: 1000,
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{
|
{
|
||||||
<div
|
<div
|
||||||
className={styles.checkboxContainer}
|
className={styles.checkboxContainer}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user