import { useWebHaptics } from '@/hooks/use-web-haptics'; import { cn } from '@/lib/utils'; import { __ } from '@/utils/i18n'; import { DndContext, type DragEndEvent, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, } from '@dnd-kit/core'; import { SortableContext, arrayMove, rectSortingStrategy, sortableKeyboardCoordinates, useSortable, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { GripVertical } from 'lucide-react'; import type { ReactNode } from 'react'; interface SortableGridProps { items: T[]; getId: (item: T) => string; /** * Renders one item. The provided drag handle must be placed inside the * card so it sits exactly where the card wants it (e.g. over the account * icon); it only becomes visible on hover via the wrapper's `group`. */ renderItem: (item: T, dragHandle: ReactNode) => ReactNode; onReorder: (orderedIds: string[]) => void; className?: string; /** Non-sortable content rendered inside the grid after the items. */ footer?: ReactNode; } export function SortableGrid({ items, getId, renderItem, onReorder, className, footer, }: SortableGridProps) { const ids = items.map(getId); const { trigger } = useWebHaptics(); // A small move starts the drag, so taps/clicks still work. Touch is handled // via pointer events and only the handle has touch-action: none, so the rest // of the card scrolls normally on mobile (no long-press, which blocked it). const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); function handleDragEnd(event: DragEndEvent): void { const { active, over } = event; if (!over || active.id === over.id) { return; } const oldIndex = ids.indexOf(String(active.id)); const newIndex = ids.indexOf(String(over.id)); if (oldIndex === -1 || newIndex === -1) { return; } onReorder(arrayMove(ids, oldIndex, newIndex)); } return (
{items.map((item) => ( trigger('selection')} > {(dragHandle) => renderItem(item, dragHandle)} ))} {footer}
); } function SortableItem({ id, onActivate, children, }: { id: string; onActivate: () => void; children: (dragHandle: ReactNode) => ReactNode; }) { const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging, } = useSortable({ id }); const dragHandle = ( ); return (
{children(dragHandle)}
); }