diff --git a/src/App.tsx b/src/App.tsx index f40362d..779fd17 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -695,6 +695,12 @@ export function App() { setShowPrivacy(true); }, []); + /** Scroll to top whenever the view changes. */ + // eslint-disable-next-line react-hooks/exhaustive-deps -- activeTool and showPrivacy are intentional trigger deps + useEffect(() => { + window.scrollTo(0, 0); + }, [activeTool, showPrivacy]); + /** Metadata for the active tool (memoised to avoid redundant lookups). */ const activeMeta = useMemo( () => (activeTool ? (tools.find((t) => t.id === activeTool) ?? null) : null), diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index e43e5d6..1ec0756 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -102,7 +102,13 @@ export function Layout({ children, onHome, showBack, onPrivacy, badgeAccent }: L
{/* Brand + copyright */}
- + CloakPDF diff --git a/src/components/SortableGrid.tsx b/src/components/SortableGrid.tsx new file mode 100644 index 0000000..0448819 --- /dev/null +++ b/src/components/SortableGrid.tsx @@ -0,0 +1,122 @@ +/** + * SortableGrid — reusable drag-and-drop grid for page reordering. + * + * Renders an interleaved layout of drop-zones and items: + * [drop_0] [item_0] [drop_1] [item_1] ... [item_N-1] [drop_N] + * + * Drop-zones expand when hovering during a drag to show where the item will + * land. A floating TouchDragOverlay follows the finger on mobile. + * + * Usage: + * const drag = useSortableDrag(handleMove); + * + * } + * renderOverlay={(dragIndex) => } + * /> + */ + +import type { SortableDrag } from "../hooks/useSortableDrag.ts"; +import { TouchDragOverlay } from "./TouchDragOverlay.tsx"; + +interface SortableGridProps { + /** Total number of items in the list. */ + itemCount: number; + /** Drag state bag from useSortableDrag. */ + drag: SortableDrag; + /** Called when an item is dropped at a new slot (desktop HTML5 drag path). */ + onMove: (fromIndex: number, toSlot: number) => void; + /** + * Render a single item at the given slot. + * The item is responsible for spreading `drag.getItemProps(slot)` (or a + * subset of it) onto its root element to make it draggable. + */ + renderItem: (slot: number, isSource: boolean) => React.ReactNode; + /** Render the content shown inside the floating touch-drag overlay. */ + renderOverlay?: (dragIndex: number) => React.ReactNode; + /** Optional class override for the grid wrapper. */ + className?: string; +} + +export function SortableGrid({ + itemCount, + drag, + onMove, + renderItem, + renderOverlay, + className, +}: SortableGridProps) { + const { dragIndex, dragOverSlot, touchPos, setDragIndex, setDragOverSlot } = drag; + const isDragging = dragIndex !== null; + + const elements: React.ReactNode[] = []; + + for (let slot = 0; slot <= itemCount; slot++) { + const isAdjacentToDrag = dragIndex !== null && (slot === dragIndex || slot === dragIndex + 1); + + // ── Drop zone ── + elements.push( +
{ + if (isAdjacentToDrag) return; + e.preventDefault(); + setDragOverSlot(slot); + }} + onDragLeave={(e) => { + if (!e.currentTarget.contains(e.relatedTarget as Node)) { + if (dragOverSlot === slot) setDragOverSlot(null); + } + }} + onDrop={(e) => { + e.preventDefault(); + if (dragIndex === null || isAdjacentToDrag) return; + onMove(dragIndex, slot); + setDragIndex(null); + setDragOverSlot(null); + }} + className={`self-stretch flex items-center justify-center rounded-lg transition-all duration-200 ${ + isDragging && !isAdjacentToDrag + ? dragOverSlot === slot + ? "w-20 sm:w-24 bg-primary-50 dark:bg-primary-900/20" + : "w-3 sm:w-4" + : "w-0" + }`} + > + {isDragging && !isAdjacentToDrag && ( +
+ )} +
, + ); + + // ── Item ── + if (slot < itemCount) { + elements.push(renderItem(slot, dragIndex === slot)); + } + } + + return ( + <> +
+ {elements} +
+ + {dragIndex !== null && touchPos !== null && renderOverlay && ( + {renderOverlay(dragIndex)} + )} + + ); +} diff --git a/src/components/TouchDragOverlay.tsx b/src/components/TouchDragOverlay.tsx new file mode 100644 index 0000000..ebe3b61 --- /dev/null +++ b/src/components/TouchDragOverlay.tsx @@ -0,0 +1,36 @@ +/** + * TouchDragOverlay — floating preview that follows the finger during touch drag. + * + * On desktop, the HTML5 Drag API provides a built-in ghost image. On mobile, + * touch events have no equivalent, so this component renders a small + * semi-transparent thumbnail at the current touch position via a portal. + */ + +import { createPortal } from "react-dom"; + +interface TouchDragOverlayProps { + /** Current touch coordinates (null when not dragging via touch). */ + touchPos: { x: number; y: number } | null; + children: React.ReactNode; +} + +export function TouchDragOverlay({ touchPos, children }: TouchDragOverlayProps) { + if (!touchPos) return null; + + return createPortal( +
+ {children} +
, + document.body, + ); +} diff --git a/src/hooks/useSortableDrag.ts b/src/hooks/useSortableDrag.ts index 208ab92..c672b36 100644 --- a/src/hooks/useSortableDrag.ts +++ b/src/hooks/useSortableDrag.ts @@ -4,9 +4,11 @@ * Supports both the HTML5 drag API (desktop) and touch events (mobile). * * Components must: - * 1. Spread `getTouchHandlers(slot)` onto every draggable item. + * 1. Spread `getItemProps(slot)` onto every draggable item — this wires up + * both the HTML5 drag API and touch handlers in one call. * 2. Add `data-drop-slot={slot}` to every drop-zone div so touch tracking * can identify the target slot via `document.elementFromPoint`. + * (If using `SortableGrid`, drop-zones are handled automatically.) * * Touch behaviour: * - Drag activates after an 8 px movement threshold so short taps still @@ -21,6 +23,8 @@ import { useState, useEffect, useRef, useCallback } from "react"; export function useSortableDrag(onMove: (fromIndex: number, toSlot: number) => void) { const [dragIndex, setDragIndex] = useState(null); const [dragOverSlot, setDragOverSlot] = useState(null); + /** Current touch position — non-null only during an active touch drag. */ + const [touchPos, setTouchPos] = useState<{ x: number; y: number } | null>(null); // Refs so the document-level listeners don't need to be re-registered const touchStartSlot = useRef(null); @@ -29,7 +33,7 @@ export function useSortableDrag(onMove: (fromIndex: number, toSlot: number) => v const onMoveRef = useRef(onMove); onMoveRef.current = onMove; - /** Call this from each draggable item's onTouchStart. */ + /** Touch handlers for a single draggable item (used internally by getItemProps). */ const getTouchHandlers = useCallback( (slot: number) => ({ onTouchStart(e: React.TouchEvent) { @@ -47,6 +51,26 @@ export function useSortableDrag(onMove: (fromIndex: number, toSlot: number) => v [], ); + /** + * All props needed to make an element draggable (HTML5 + touch). + * Spread the result onto the draggable element: `{...getItemProps(slot)}` + */ + const getItemProps = useCallback( + (slot: number) => ({ + draggable: true as const, + onDragStart(e: React.DragEvent) { + e.dataTransfer.effectAllowed = "move"; + setDragIndex(slot); + }, + onDragEnd() { + setDragIndex(null); + setDragOverSlot(null); + }, + ...getTouchHandlers(slot), + }), + [getTouchHandlers], + ); + useEffect(() => { const handleTouchMove = (e: TouchEvent) => { if (touchStartSlot.current === null || touchStartPos.current === null) return; @@ -65,6 +89,9 @@ export function useSortableDrag(onMove: (fromIndex: number, toSlot: number) => v // Prevent page scroll while reordering. e.preventDefault(); + // Track finger position for the drag overlay. + setTouchPos({ x: touch.clientX, y: touch.clientY }); + // Find which drop-zone slot is under the finger. const el = document.elementFromPoint(touch.clientX, touch.clientY); const zone = el?.closest("[data-drop-slot]") as HTMLElement | null; @@ -97,6 +124,7 @@ export function useSortableDrag(onMove: (fromIndex: number, toSlot: number) => v touchStartPos.current = null; setDragIndex(null); setDragOverSlot(null); + setTouchPos(null); }; const handleTouchCancel = () => { @@ -105,6 +133,7 @@ export function useSortableDrag(onMove: (fromIndex: number, toSlot: number) => v touchStartPos.current = null; setDragIndex(null); setDragOverSlot(null); + setTouchPos(null); }; // Non-passive so we can call preventDefault() during active drag. @@ -119,5 +148,16 @@ export function useSortableDrag(onMove: (fromIndex: number, toSlot: number) => v }; }, []); // all mutable state accessed via refs — no deps needed - return { dragIndex, dragOverSlot, setDragIndex, setDragOverSlot, getTouchHandlers }; + return { + dragIndex, + dragOverSlot, + touchPos, + setDragIndex, + setDragOverSlot, + getItemProps, + getTouchHandlers, + }; } + +/** Convenience type for passing the full drag bag to SortableGrid. */ +export type SortableDrag = ReturnType; diff --git a/src/tools/AddBlankPage.tsx b/src/tools/AddBlankPage.tsx index a0ebf9f..37a3e12 100644 --- a/src/tools/AddBlankPage.tsx +++ b/src/tools/AddBlankPage.tsx @@ -8,6 +8,7 @@ import { useState, useCallback } from "react"; import { FileDropZone } from "../components/FileDropZone.tsx"; +import { SortableGrid } from "../components/SortableGrid.tsx"; import { categoryAccent, categoryGlow } from "../config/theme.ts"; import { downloadPdf, formatFileSize } from "../utils/file-helpers.ts"; import { useSortableDrag } from "../hooks/useSortableDrag.ts"; @@ -43,8 +44,7 @@ export default function AddBlankPage() { }, []); // Drag state (desktop + mobile touch) - const { dragIndex, dragOverSlot, setDragIndex, setDragOverSlot, getTouchHandlers } = - useSortableDrag(handleMove); + const drag = useSortableDrag(handleMove); const handleFile = useCallback(async (files: File[]) => { const pdf = files[0]; @@ -71,7 +71,7 @@ export default function AddBlankPage() { }, []); const hasBlankPages = items.some((it) => it.type === "blank"); - const isDragging = dragIndex !== null; + const isDragging = drag.dragIndex !== null; const handleAddBlank = useCallback(() => { setItems((prev) => [{ type: "blank", id: nextBlankId() }, ...prev]); @@ -79,9 +79,9 @@ export default function AddBlankPage() { const handleReset = useCallback(() => { setItems((prev) => prev.filter((it) => it.type === "original")); - setDragIndex(null); - setDragOverSlot(null); - }, [setDragIndex, setDragOverSlot]); + drag.setDragIndex(null); + drag.setDragOverSlot(null); + }, [drag]); const handleApply = useCallback(async () => { if (!file || !hasBlankPages) return; @@ -109,158 +109,6 @@ export default function AddBlankPage() { } }, [file, hasBlankPages, items]); - const renderItems = () => { - const elements: React.ReactNode[] = []; - - for (let slot = 0; slot <= items.length; slot++) { - // ── Drop zone ── - const isAdjacentToDrag = dragIndex !== null && (slot === dragIndex || slot === dragIndex + 1); - - elements.push( -
{ - if (isAdjacentToDrag) return; - e.preventDefault(); - setDragOverSlot(slot); - }} - onDragLeave={(e) => { - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - if (dragOverSlot === slot) setDragOverSlot(null); - } - }} - onDrop={(e) => { - e.preventDefault(); - if (dragIndex === null || isAdjacentToDrag) return; - setItems((prev) => { - const next = [...prev]; - const [moved] = next.splice(dragIndex, 1); - const adjustedSlot = dragIndex < slot ? slot - 1 : slot; - next.splice(adjustedSlot, 0, moved); - return next; - }); - setDragIndex(null); - setDragOverSlot(null); - }} - className={`self-stretch flex items-center justify-center rounded-lg transition-all duration-200 ${ - isDragging && !isAdjacentToDrag - ? dragOverSlot === slot - ? "w-20 sm:w-24 bg-primary-50 dark:bg-primary-900/20" - : "w-3 sm:w-4" - : "w-0" - }`} - > - {isDragging && !isAdjacentToDrag && ( -
- )} -
, - ); - - // ── Page card ── - if (slot < items.length) { - const item = items[slot]; - const isSource = dragIndex === slot; - - if (item.type === "blank") { - elements.push( -
{ - e.dataTransfer.effectAllowed = "move"; - setDragIndex(slot); - }} - onDragEnd={() => { - setDragIndex(null); - setDragOverSlot(null); - }} - {...getTouchHandlers(slot)} - className={`shrink-0 pt-2 pr-2 flex flex-col items-center gap-1.5 cursor-grab active:cursor-grabbing select-none transition-all duration-200 ${ - isSource ? "scale-95 opacity-30" : "scale-100 opacity-100" - }`} - > -
-
- + -
-
- New -
-
- Blank -
, - ); - } else { - elements.push( -
{ - e.dataTransfer.effectAllowed = "move"; - setDragIndex(slot); - }} - onDragEnd={() => { - setDragIndex(null); - setDragOverSlot(null); - }} - {...getTouchHandlers(slot)} - className={`shrink-0 pt-2 pr-2 flex flex-col items-center gap-1.5 cursor-grab active:cursor-grabbing select-none transition-all duration-200 ${ - isSource ? "scale-95 opacity-30" : "scale-100 opacity-100" - }`} - > -
-
- {`Page -
-
- {item.index + 1} -
-
- - Page {item.index + 1} - -
, - ); - } - } - } - - return elements; - }; - return (
{!file ? ( @@ -326,9 +174,118 @@ export default function AddBlankPage() {
-
- {renderItems()} -
+ { + const item = items[slot]; + + if (item.type === "blank") { + return ( +
+
+
+ + +
+
+ New +
+
+ Blank +
+ ); + } + + return ( +
+
+
+ {`Page +
+
+ {item.index + 1} +
+
+ + Page {item.index + 1} + +
+ ); + }} + renderOverlay={(idx) => { + const item = items[idx]; + if (item?.type === "blank") { + return ( +
+
+ + +
+
+ New +
+
+ ); + } + const pageIndex = (item as OriginalItem).index; + return ( +
+
+ +
+
+ {pageIndex + 1} +
+
+ ); + }} + /> {hasBlankPages && (

diff --git a/src/tools/DuplicatePage.tsx b/src/tools/DuplicatePage.tsx index 993f5ce..79066b7 100644 --- a/src/tools/DuplicatePage.tsx +++ b/src/tools/DuplicatePage.tsx @@ -8,6 +8,7 @@ import { useCallback, useState } from "react"; import { FileDropZone } from "../components/FileDropZone.tsx"; +import { SortableGrid } from "../components/SortableGrid.tsx"; import { categoryAccent, categoryGlow } from "../config/theme.ts"; import { downloadPdf, formatFileSize } from "../utils/file-helpers.ts"; import { useSortableDrag } from "../hooks/useSortableDrag.ts"; @@ -43,8 +44,7 @@ export default function DuplicatePage() { }, []); // Drag state (desktop + mobile touch) - const { dragIndex, dragOverSlot, setDragIndex, setDragOverSlot, getTouchHandlers } = - useSortableDrag(handleMove); + const drag = useSortableDrag(handleMove); const handleFile = useCallback(async (files: File[]) => { const pdf = files[0]; @@ -69,7 +69,7 @@ export default function DuplicatePage() { }, []); const hasCopies = items.some((it) => it.type === "copy"); - const isDragging = dragIndex !== null; + const isDragging = drag.dragIndex !== null; const handleDuplicatePage = useCallback((pageIndex: number, afterSlot: number) => { setItems((prev) => { @@ -81,9 +81,9 @@ export default function DuplicatePage() { const handleReset = useCallback(() => { setItems((prev) => prev.filter((it) => it.type === "original")); - setDragIndex(null); - setDragOverSlot(null); - }, [setDragIndex, setDragOverSlot]); + drag.setDragIndex(null); + drag.setDragOverSlot(null); + }, [drag]); const handleApply = useCallback(async () => { if (!file || !hasCopies) return; @@ -110,170 +110,6 @@ export default function DuplicatePage() { } }, [file, hasCopies, items]); - const renderItems = () => { - const elements: React.ReactNode[] = []; - - for (let slot = 0; slot <= items.length; slot++) { - // ── Drop zone ── - const isAdjacentToDrag = dragIndex !== null && (slot === dragIndex || slot === dragIndex + 1); - - elements.push( -

{ - if (isAdjacentToDrag) return; - e.preventDefault(); - setDragOverSlot(slot); - }} - onDragLeave={(e) => { - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - if (dragOverSlot === slot) setDragOverSlot(null); - } - }} - onDrop={(e) => { - e.preventDefault(); - if (dragIndex === null || isAdjacentToDrag) return; - setItems((prev) => { - const next = [...prev]; - const [moved] = next.splice(dragIndex, 1); - const adjustedSlot = dragIndex < slot ? slot - 1 : slot; - next.splice(adjustedSlot, 0, moved); - return next; - }); - setDragIndex(null); - setDragOverSlot(null); - }} - className={`self-stretch flex items-center justify-center rounded-lg transition-all duration-200 ${ - isDragging && !isAdjacentToDrag - ? dragOverSlot === slot - ? "w-20 sm:w-24 bg-primary-50 dark:bg-primary-900/20" - : "w-3 sm:w-4" - : "w-0" - }`} - > - {isDragging && !isAdjacentToDrag && ( -
- )} -
, - ); - - // ── Page card ── - if (slot < items.length) { - const item = items[slot]; - const isSource = dragIndex === slot; - - if (item.type === "copy") { - elements.push( -
{ - e.dataTransfer.effectAllowed = "move"; - setDragIndex(slot); - }} - onDragEnd={() => { - setDragIndex(null); - setDragOverSlot(null); - }} - {...getTouchHandlers(slot)} - className={`shrink-0 p-2 flex flex-col items-center gap-1.5 cursor-grab active:cursor-grabbing select-none transition-all duration-200 ${ - isSource ? "scale-95 opacity-30" : "scale-100 opacity-100" - }`} - > -
-
- {`Copy -
-
-
- Copy -
-
- - Copy of {item.sourceIndex + 1} - -
, - ); - } else { - elements.push( -
{ - if (!isDragging) handleDuplicatePage(item.index, slot); - }} - onDragStart={(e) => { - if (!hasCopies) return; - e.dataTransfer.effectAllowed = "move"; - setDragIndex(slot); - }} - onDragEnd={() => { - setDragIndex(null); - setDragOverSlot(null); - }} - {...(hasCopies ? getTouchHandlers(slot) : {})} - className={`shrink-0 p-2 flex flex-col items-center gap-1.5 select-none transition-all duration-200 cursor-pointer ${ - hasCopies ? "active:cursor-grabbing" : "hover:scale-105" - } ${isSource ? "scale-95 opacity-30" : "scale-100 opacity-100"}`} - > -
-
- {`Page -
-
- {item.index + 1} -
-
- - Page {item.index + 1} - -
, - ); - } - } - } - - return elements; - }; - return (
{!file ? ( @@ -331,9 +167,144 @@ export default function DuplicatePage() { )}
-
- {renderItems()} -
+ { + const item = items[slot]; + + if (item.type === "copy") { + return ( +
+
+
+ {`Copy +
+
+
+ Copy +
+
+ + Copy of {item.sourceIndex + 1} + +
+ ); + } + + // Original page card — conditionally draggable (only after copies exist). + // Uses getTouchHandlers directly instead of getItemProps for selective control. + return ( +
{ + if (!isDragging) handleDuplicatePage(item.index, slot); + }} + onDragStart={(e) => { + if (!hasCopies) return; + e.dataTransfer.effectAllowed = "move"; + drag.setDragIndex(slot); + }} + onDragEnd={() => { + drag.setDragIndex(null); + drag.setDragOverSlot(null); + }} + {...(hasCopies ? drag.getTouchHandlers(slot) : {})} + className={`shrink-0 p-2 flex flex-col items-center gap-1.5 select-none transition-all duration-200 cursor-pointer ${ + hasCopies ? "active:cursor-grabbing" : "hover:scale-105" + } ${isSource ? "scale-95 opacity-30" : "scale-100 opacity-100"}`} + > +
+
+ {`Page +
+
+ {item.index + 1} +
+
+ + Page {item.index + 1} + +
+ ); + }} + renderOverlay={(idx) => { + const item = items[idx]; + const srcIdx = + item?.type === "copy" + ? item.sourceIndex + : item?.type === "original" + ? item.index + : null; + if (srcIdx === null) return null; + const isCopy = item?.type === "copy"; + return ( +
+
+ + {isCopy && ( +
+ )} +
+
+ {isCopy ? "Copy" : srcIdx + 1} +
+
+ ); + }} + /> {hasCopies && (

diff --git a/src/tools/ReorderPages.tsx b/src/tools/ReorderPages.tsx index 1fb0949..eb40a62 100644 --- a/src/tools/ReorderPages.tsx +++ b/src/tools/ReorderPages.tsx @@ -10,6 +10,7 @@ import { useState, useCallback } from "react"; import { FileDropZone } from "../components/FileDropZone.tsx"; +import { SortableGrid } from "../components/SortableGrid.tsx"; import { categoryAccent, categoryGlow } from "../config/theme.ts"; import { reorderPages } from "../utils/pdf-operations.ts"; import { renderAllThumbnails } from "../utils/pdf-renderer.ts"; @@ -37,8 +38,7 @@ export default function ReorderPages() { }, []); // Drag state (desktop + mobile touch) - const { dragIndex, dragOverSlot, setDragIndex, setDragOverSlot, getTouchHandlers } = - useSortableDrag(handleMove); + const drag = useSortableDrag(handleMove); const handleFile = useCallback(async (files: File[]) => { const pdf = files[0]; @@ -79,133 +79,12 @@ export default function ReorderPages() { const handleReset = useCallback(() => { setOrder(thumbnails.map((_, i) => i)); - setDragIndex(null); - setDragOverSlot(null); - }, [thumbnails, setDragIndex, setDragOverSlot]); + drag.setDragIndex(null); + drag.setDragOverSlot(null); + }, [thumbnails, drag]); const isReordered = order.some((pageIdx, i) => pageIdx !== i); - const isDragging = dragIndex !== null; - - /** - * Build the interleaved layout: [drop_0] [page_0] [drop_1] [page_1] ... [page_N-1] [drop_N] - * - * When dragging, the drop-zone gaps expand and show a vertical bar indicator - * on hover—exactly like AddBlankPage. The source page card gets a highlighted - * ring to show it's selected. Dropping on a gap moves the page to that slot. - */ - const renderItems = () => { - const items: React.ReactNode[] = []; - - for (let slot = 0; slot <= order.length; slot++) { - // ── Drop zone ── - // Don't show drop zones immediately adjacent to the dragged card - // (dropping there would be a no-op). - const isAdjacentToDrag = dragIndex !== null && (slot === dragIndex || slot === dragIndex + 1); - - items.push( -

{ - if (isAdjacentToDrag) return; - e.preventDefault(); - setDragOverSlot(slot); - }} - onDragLeave={(e) => { - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - if (dragOverSlot === slot) setDragOverSlot(null); - } - }} - onDrop={(e) => { - e.preventDefault(); - if (dragIndex === null || isAdjacentToDrag) return; - setOrder((prev) => { - const next = [...prev]; - const [moved] = next.splice(dragIndex, 1); - // Adjust target: if we removed from before the slot the index shifts - const adjustedSlot = dragIndex < slot ? slot - 1 : slot; - next.splice(adjustedSlot, 0, moved); - return next; - }); - setDragIndex(null); - setDragOverSlot(null); - }} - className={`self-stretch flex items-center justify-center rounded-lg transition-all duration-200 ${ - isDragging && !isAdjacentToDrag - ? dragOverSlot === slot - ? "w-20 sm:w-24 bg-primary-50 dark:bg-primary-900/20" - : "w-3 sm:w-4" - : "w-0" - }`} - > - {isDragging && !isAdjacentToDrag && ( -
- )} -
, - ); - - // ── Page card ── - if (slot < order.length) { - const originalIndex = order[slot]; - const isSource = dragIndex === slot; - - items.push( -
{ - e.dataTransfer.effectAllowed = "move"; - setDragIndex(slot); - }} - onDragEnd={() => { - setDragIndex(null); - setDragOverSlot(null); - }} - {...getTouchHandlers(slot)} - className={`shrink-0 pt-2 pr-2 flex flex-col items-center gap-1.5 cursor-grab active:cursor-grabbing select-none transition-all duration-200 ${ - isSource ? "scale-95 opacity-30" : "scale-100 opacity-100" - }`} - > -
-
- {`Page -
-
- {originalIndex + 1} -
-
- - Page {originalIndex + 1} - -
, - ); - } - } - - return items; - }; + const isDragging = drag.dragIndex !== null; return (
@@ -262,9 +141,67 @@ export default function ReorderPages() { )}
-
- {renderItems()} -
+ { + const originalIndex = order[slot]; + return ( +
+
+
+ {`Page +
+
+ {originalIndex + 1} +
+
+ + Page {originalIndex + 1} + +
+ ); + }} + renderOverlay={(idx) => ( +
+
+ +
+
+ {order[idx] + 1} +
+
+ )} + /> {isReordered && (