From d1aabaecb3214c15172c3a52325373847a0df149 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Tue, 29 Sep 2026 12:50:15 +0200 Subject: [PATCH] feat(web): make every drag-to-reorder move, lift and settle the same way Builder sections and entries, keyword chips and layout pages now shuffle and drop with the shared DRAG_SETTLE timing (200ms, ease-in-out-strong). Dragged builder rows lift on a raised surface with a shadow instead of fading to 40%. Chips get a drop animation instead of vanishing, and both drag overlays skip the drop flight under reduced motion. --- apps/web/src/components/input/chip-input.tsx | 12 ++++++++++-- .../src/features/resume/editor/write/entry-card.tsx | 7 ++++--- .../src/features/resume/editor/write/section-row.tsx | 8 ++++++-- .../-sidebar/right/sections/layout/pages.tsx | 11 ++++++++--- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/input/chip-input.tsx b/apps/web/src/components/input/chip-input.tsx index 5f88b8280..410741e80 100644 --- a/apps/web/src/components/input/chip-input.tsx +++ b/apps/web/src/components/input/chip-input.tsx @@ -12,6 +12,7 @@ import { rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useS import { CSS } from "@dnd-kit/utilities"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; +import { useReducedMotion } from "motion/react"; import * as React from "react"; import { createPortal } from "react-dom"; import { Badge } from "@reactive-resume/ui/components/badge"; @@ -21,6 +22,7 @@ import { Input } from "@reactive-resume/ui/components/input"; import { Kbd } from "@reactive-resume/ui/components/kbd"; import { cn } from "@reactive-resume/utils/style"; import { useControlledState } from "@/hooks/use-controlled-state"; +import { DRAG_SETTLE } from "@/libs/motion"; const RETURN_KEY = "Enter"; const COMMA_KEY = ","; @@ -55,15 +57,21 @@ type ChipDragOverlayProps = { }; function ChipDragOverlay({ activeChip }: ChipDragOverlayProps) { + const reduceMotion = useReducedMotion(); const overlay = ( - {activeChip ? : null} + + {activeChip ? : null} + ); return createPortal(overlay, document.body); } function ChipItem({ id, chip, index, isEditing, onEdit, onRemove }: ChipItemProps) { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id, + transition: DRAG_SETTLE, + }); const style = { transition, diff --git a/apps/web/src/features/resume/editor/write/entry-card.tsx b/apps/web/src/features/resume/editor/write/entry-card.tsx index 065cf4eea..2001767f4 100644 --- a/apps/web/src/features/resume/editor/write/entry-card.tsx +++ b/apps/web/src/features/resume/editor/write/entry-card.tsx @@ -25,6 +25,7 @@ import { toast } from "@reactive-resume/ui/components/toast"; import { useBreakpoint } from "@reactive-resume/ui/hooks/use-breakpoint"; import { cn } from "@reactive-resume/utils/style"; import { useCurrentResume, useResumeStore, useUpdateResumeData } from "@/features/resume/builder/draft"; +import { DRAG_SETTLE } from "@/libs/motion"; import { getCompatibleMoveTargets, getSourceSectionTitle, moveItem } from "@/libs/resume/move-item"; import { useEditorStore } from "../store"; import { EntryFields } from "./entries"; @@ -65,7 +66,7 @@ export function EntryCard({ section, entryId, index, count, page, locked, onMove const open = useEditorStore((state) => state.selection?.kind === "item" && state.selection.itemId === entryId); const autoFocus = useEditorStore((state) => state.focusEntryId === entryId); const select = useEditorStore((state) => state.select); - const sortable = useSortable({ id: entryId, disabled: locked }); + const sortable = useSortable({ id: entryId, disabled: locked, transition: DRAG_SETTLE }); const isPhone = useBreakpoint() === "mobile"; // The first field took focus as it mounted; later openings shouldn't steal focus again. @@ -101,9 +102,9 @@ export function EntryCard({ section, entryId, index, count, page, locked, onMove data-entry-id={entryId} style={{ transform: CSS.Translate.toString(sortable.transform), transition: sortable.transition }} className={cn( - "group/entry relative scroll-mt-[60px] rounded-[10px] border border-line bg-surface transition-[border-color,box-shadow] duration-quick", + "group/entry relative scroll-mt-[60px] rounded-[10px] border border-line bg-surface transition-[border-color,box-shadow] duration-quick ease-enter", open && "border-accent shadow-e1", - sortable.isDragging && "z-10 opacity-40", + sortable.isDragging && "z-10 bg-raised shadow-e2", )} >
diff --git a/apps/web/src/features/resume/editor/write/section-row.tsx b/apps/web/src/features/resume/editor/write/section-row.tsx index adabe064f..53840044a 100644 --- a/apps/web/src/features/resume/editor/write/section-row.tsx +++ b/apps/web/src/features/resume/editor/write/section-row.tsx @@ -28,6 +28,7 @@ import { cn } from "@reactive-resume/utils/style"; import { IconPicker } from "@/components/input/icon-picker"; import { useCurrentBuilderResumeSelector, useResumeStore, useUpdateResumeData } from "@/features/resume/builder/draft"; import { usePrompt } from "@/hooks/use-confirm"; +import { DRAG_SETTLE } from "@/libs/motion"; import { getSectionTitle } from "@/libs/resume/section"; import { pendingProposals } from "../proposals/proposals"; import { useEditorStore } from "../store"; @@ -64,7 +65,7 @@ export function SectionRow({ section, locked, onMove, children }: SectionRowProp const open = useEditorStore((state) => state.openSections.includes(section.id)); const setOpen = useEditorStore((state) => state.setSectionOpen); const updateResumeData = useUpdateResumeData(); - const sortable = useSortable({ id: section.id, disabled: locked }); + const sortable = useSortable({ id: section.id, disabled: locked, transition: DRAG_SETTLE }); const toggleHidden = () => updateResumeData( @@ -87,7 +88,10 @@ export function SectionRow({ section, locked, onMove, children }: SectionRowProp id={`sidebar-${section.id}`} data-section-id={section.id} style={{ transform: CSS.Translate.toString(sortable.transform), transition: sortable.transition }} - className={cn("relative scroll-mt-[60px] rounded-lg", sortable.isDragging && "z-10 opacity-40")} + className={cn( + "relative scroll-mt-[60px] rounded-lg transition-shadow duration-quick ease-enter", + sortable.isDragging && "z-10 bg-raised shadow-e2", + )} >
- + {activeId ? : null} @@ -407,7 +409,10 @@ type SortableLayoutItemProps = { }; function SortableLayoutItem({ id, pageIndex, columnId }: SortableLayoutItemProps) { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id, + transition: DRAG_SETTLE, + }); const style: CSSProperties = { transform: CSS.Transform.toString(transform), transition };