From 039de2dde26f090a38e95c5236b48c92e4e23c1d Mon Sep 17 00:00:00 2001 From: Reece Date: Tue, 28 Oct 2025 19:38:53 +0000 Subject: [PATCH] Fixes --- .../pageEditor/DragDropGrid.module.css | 73 +++++ .../components/pageEditor/DragDropGrid.tsx | 298 +++++++++--------- .../core/components/pageEditor/PageEditor.tsx | 20 +- frontend/src/core/types/pageEditor.ts | 6 - 4 files changed, 235 insertions(+), 162 deletions(-) create mode 100644 frontend/src/core/components/pageEditor/DragDropGrid.module.css diff --git a/frontend/src/core/components/pageEditor/DragDropGrid.module.css b/frontend/src/core/components/pageEditor/DragDropGrid.module.css new file mode 100644 index 000000000..96e917140 --- /dev/null +++ b/frontend/src/core/components/pageEditor/DragDropGrid.module.css @@ -0,0 +1,73 @@ +.gridContainer { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; +} + +.virtualRows { + width: 100%; + position: relative; + margin: 0 auto; +} + +.virtualRow { + position: absolute; + top: 0; + left: 0; + width: 100%; +} + +.rowContent { + display: flex; + justify-content: flex-start; + height: 100%; + align-items: center; + position: relative; +} + +.selectionBox { + position: absolute; + border: 2px dashed #3b82f6; + background-color: rgba(59, 130, 246, 0.1); + pointer-events: none; +} + +.dropIndicator { + position: absolute; + width: 4px; + background-color: rgba(96, 165, 250, 0.8); + border-radius: 2px; + pointer-events: none; +} + +.dragOverlay { + position: relative; + cursor: grabbing; +} + +.dragOverlayBadge { + position: absolute; + top: -8px; + right: -8px; + background-color: #3b82f6; + color: #ffffff; + border-radius: 50%; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: bold; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + z-index: 1; +} + +.dragOverlayPreview { + display: flex; + align-items: center; + justify-content: center; + font-size: 48px; + opacity: 0.5; +} diff --git a/frontend/src/core/components/pageEditor/DragDropGrid.tsx b/frontend/src/core/components/pageEditor/DragDropGrid.tsx index 62e4f6f71..a73bea8c6 100644 --- a/frontend/src/core/components/pageEditor/DragDropGrid.tsx +++ b/frontend/src/core/components/pageEditor/DragDropGrid.tsx @@ -2,6 +2,7 @@ import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react' import { Box } from '@mantine/core'; import { useVirtualizer } from '@tanstack/react-virtual'; import { GRID_CONSTANTS } from './constants'; +import styles from './DragDropGrid.module.css'; import { Z_INDEX_SELECTION_BOX, Z_INDEX_DROP_INDICATOR, @@ -36,6 +37,8 @@ interface DragDropGridProps { type DropSide = 'left' | 'right' | null; +type ItemRect = { id: string; rect: DOMRect }; + interface DropHint { hoveredId: string | null; dropSide: DropSide; @@ -51,59 +54,87 @@ function resolveDropHint( return { hoveredId: null, dropSide: null }; } - const rows = new Map>(); + const items: ItemRect[] = Array.from(itemRefs.current.entries()) + .filter(([itemId, element]): element is HTMLDivElement => !!element && itemId !== activeId) + .map(([itemId, element]) => ({ + id: itemId, + rect: element.getBoundingClientRect(), + })) + .filter(({ rect }) => rect.width > 0 && rect.height > 0); - itemRefs.current.forEach((element, itemId) => { - if (!element || itemId === activeId) return; - - const rect = element.getBoundingClientRect(); - const rowCenter = rect.top + rect.height / 2; - - let row = rows.get(rowCenter); - if (!row) { - row = []; - rows.set(rowCenter, row); - } - row.push({ id: itemId, rect }); - }); - - let hoveredId: string | null = null; - let dropSide: DropSide = null; - - let closestRowY = 0; - let closestRowDistance = Infinity; - - rows.forEach((_items, rowY) => { - const distance = Math.abs(cursorY - rowY); - if (distance < closestRowDistance) { - closestRowDistance = distance; - closestRowY = rowY; - } - }); - - const closestRow = rows.get(closestRowY); - if (!closestRow || closestRow.length === 0) { + if (items.length === 0) { return { hoveredId: null, dropSide: null }; } - let closestDistance = Infinity; - closestRow.forEach(({ id, rect }) => { - const distanceToLeft = Math.abs(cursorX - rect.left); - const distanceToRight = Math.abs(cursorX - rect.right); + items.sort((a, b) => a.rect.top - b.rect.top); - if (distanceToLeft < closestDistance) { - closestDistance = distanceToLeft; - hoveredId = id; - dropSide = 'left'; + const rows: ItemRect[][] = []; + const rowTolerance = items[0].rect.height / 2; + + items.forEach((item) => { + const currentRow = rows[rows.length - 1]; + if (!currentRow) { + rows.push([item]); + return; } - if (distanceToRight < closestDistance) { - closestDistance = distanceToRight; - hoveredId = id; - dropSide = 'right'; + + const isSameRow = Math.abs(item.rect.top - currentRow[0].rect.top) <= rowTolerance; + if (isSameRow) { + currentRow.push(item); + } else { + rows.push([item]); } }); - return { hoveredId, dropSide }; + let targetRow: ItemRect[] | undefined; + let smallestRowDistance = Infinity; + + rows.forEach((row) => { + if (row.length === 0) { + return; + } + const top = row[0].rect.top; + const bottom = row[0].rect.bottom; + const centerY = top + (bottom - top) / 2; + const distance = Math.abs(cursorY - centerY); + if (distance < smallestRowDistance) { + smallestRowDistance = distance; + targetRow = row; + } + }); + + if (!targetRow || targetRow.length === 0) { + return { hoveredId: null, dropSide: null }; + } + + let hoveredItem = targetRow[0]; + let smallestHorizontalDistance = Infinity; + + targetRow.forEach((item) => { + const midpoint = item.rect.left + item.rect.width / 2; + const distance = Math.abs(cursorX - midpoint); + if (distance < smallestHorizontalDistance) { + smallestHorizontalDistance = distance; + hoveredItem = item; + } + }); + + const firstItem = targetRow[0]; + const lastItem = targetRow[targetRow.length - 1]; + + let dropSide: DropSide; + if (cursorX < firstItem.rect.left) { + hoveredItem = firstItem; + dropSide = 'left'; + } else if (cursorX > lastItem.rect.right) { + hoveredItem = lastItem; + dropSide = 'right'; + } else { + const midpoint = hoveredItem.rect.left + hoveredItem.rect.width / 2; + dropSide = cursorX >= midpoint ? 'right' : 'left'; + } + + return { hoveredId: hoveredItem.id, dropSide }; } function resolveTargetIndex( @@ -561,14 +592,10 @@ const DragDropGrid = ({ // 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: Z_INDEX_SELECTION_BOX, } : null; @@ -590,15 +617,10 @@ const DragDropGrid = ({ : itemRect.right - containerRect.left + itemGap / 2; return { - position: 'absolute' as const, left: `${left}px`, top: `${top}px`, - width: '4px', height: `${height}px`, - backgroundColor: 'rgba(96, 165, 250, 0.8)', - borderRadius: '2px', zIndex: Z_INDEX_DROP_INDICATOR, - pointerEvents: 'none' as const, }; }, [hoveredItemId, dropSide, activeId, itemGap, zoomLevel]); @@ -638,118 +660,94 @@ const DragDropGrid = ({ > - {/* Selection box overlay */} - {selectionBoxStyle &&
} + {selectionBoxStyle && ( +
+ )} - {/* Global drop indicator */} - {dropIndicatorStyle &&
} + {dropIndicatorStyle && ( +
+ )} -
- {rowVirtualizer.getVirtualItems().map((virtualRow) => { - const startIndex = virtualRow.index * itemsPerRow; - const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length); - const rowItems = visibleItems.slice(startIndex, endIndex); +
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const startIndex = virtualRow.index * itemsPerRow; + const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length); + const rowItems = visibleItems.slice(startIndex, endIndex); - return ( -
+ return (
- {rowItems.map((item, itemIndex) => { - const actualIndex = startIndex + itemIndex; - return ( - - ); - })} - +
+ {rowItems.map((item, itemIndex) => { + const actualIndex = startIndex + itemIndex; + return ( + + ); + })} +
-
- ); - })} -
- + ); + })} +
+ {/* Drag Overlay */} {activeId && ( -
- {/* Multi-page badge */} +
{boxSelectedPageIds.includes(activeId) && boxSelectedPageIds.length > 1 && (
{boxSelectedPageIds.length}
)} - {/* Just the thumbnail image */} {dragPreview ? ( ({ }} /> ) : ( -
+
??
)} diff --git a/frontend/src/core/components/pageEditor/PageEditor.tsx b/frontend/src/core/components/pageEditor/PageEditor.tsx index c017c845a..4a484d807 100644 --- a/frontend/src/core/components/pageEditor/PageEditor.tsx +++ b/frontend/src/core/components/pageEditor/PageEditor.tsx @@ -55,6 +55,17 @@ const PageEditor = ({ const [zoomLevel, setZoomLevel] = useState(1.0); const containerRef = useRef(null); const [isContainerHovered, setIsContainerHovered] = useState(false); + const rootFontSize = useMemo(() => { + if (typeof window === 'undefined') { + return 16; + } + const computed = getComputedStyle(document.documentElement).fontSize; + const parsed = parseFloat(computed); + return Number.isNaN(parsed) ? 16 : parsed; + }, []); + const itemGapPx = useMemo(() => { + return parseFloat(GRID_CONSTANTS.ITEM_GAP) * rootFontSize * zoomLevel; + }, [rootFontSize, zoomLevel]); // Zoom actions const zoomIn = useCallback(() => { @@ -306,9 +317,6 @@ const PageEditor = ({ // Grid container ref for positioning split indicators const gridContainerRef = useRef(null); - // State to trigger re-renders when container size changes - const [containerDimensions, setContainerDimensions] = useState({ width: 0, height: 0 }); - // Undo/Redo state const [canUndo, setCanUndo] = useState(false); const [canRedo, setCanRedo] = useState(false); @@ -1100,11 +1108,13 @@ const PageEditor = ({ if (sameRow) { lineLeft = (currentRect.right + nextRect.left) / 2; } else { - lineLeft = currentRect.right + nextRect.width * 0.1; + lineLeft = currentRect.right + itemGapPx / 2; } + } else { + lineLeft = currentRect.right + itemGapPx / 2; } } else { - lineLeft = currentRect.right + currentRect.width * 0.1; + lineLeft = currentRect.right + itemGapPx / 2; } return ( diff --git a/frontend/src/core/types/pageEditor.ts b/frontend/src/core/types/pageEditor.ts index 60c1e1988..9a34834ee 100644 --- a/frontend/src/core/types/pageEditor.ts +++ b/frontend/src/core/types/pageEditor.ts @@ -1,9 +1,4 @@ -<<<<<<< HEAD:frontend/src/core/types/pageEditor.ts import { FileId } from '@app/types/file'; -======= -import { FileId } from './file'; -import { PageBreakSettings } from '../components/pageEditor/commands/pageCommands'; ->>>>>>> feature/v2/selected-pageeditor:frontend/src/types/pageEditor.ts export interface PDFPage { id: string; @@ -16,7 +11,6 @@ export interface PDFPage { isBlankPage?: boolean; isPlaceholder?: boolean; originalFileId?: FileId; - pageBreakSettings?: PageBreakSettings; } export interface PDFDocument {