mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 10:13:47 +10:00
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.
This commit is contained in:
@@ -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 = (
|
||||
<DragOverlay dropAnimation={null}>{activeChip ? <ChipDragPreview chip={activeChip} /> : null}</DragOverlay>
|
||||
<DragOverlay dropAnimation={reduceMotion ? null : DRAG_SETTLE}>
|
||||
{activeChip ? <ChipDragPreview chip={activeChip} /> : null}
|
||||
</DragOverlay>
|
||||
);
|
||||
|
||||
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,
|
||||
|
||||
@@ -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",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-0.5 pe-1">
|
||||
|
||||
@@ -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",
|
||||
)}
|
||||
>
|
||||
<div className="group/row flex h-11 items-center gap-0.5 rounded-lg pe-1 hover:bg-hover">
|
||||
<button
|
||||
|
||||
@@ -14,6 +14,7 @@ import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy }
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { useReducedMotion } from "motion/react";
|
||||
import { useCallback, useId, useState } from "react";
|
||||
import { match } from "ts-pattern";
|
||||
import { templateLayouts } from "@reactive-resume/schema/templates";
|
||||
@@ -33,7 +34,7 @@ import { Icon } from "@reactive-resume/ui/components/icon";
|
||||
import { Switch } from "@reactive-resume/ui/components/switch";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { useCurrentResume, useUpdateResumeData } from "@/features/resume/builder/draft";
|
||||
import { EASE } from "@/libs/motion";
|
||||
import { DRAG_SETTLE } from "@/libs/motion";
|
||||
import { resolveLayoutSectionTitle } from "./title";
|
||||
import { filterVisibleLayoutSectionIds } from "./visibility";
|
||||
|
||||
@@ -86,6 +87,7 @@ const createDroppableId = (pageIndex: number, columnId: ColumnId): string => {
|
||||
|
||||
export function LayoutPages() {
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
const resume = useCurrentResume();
|
||||
const template = resume.data.metadata.template;
|
||||
@@ -263,7 +265,7 @@ export function LayoutPages() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DragOverlay dropAnimation={{ duration: 150, easing: `cubic-bezier(${EASE})` }}>
|
||||
<DragOverlay dropAnimation={reduceMotion ? null : DRAG_SETTLE}>
|
||||
{activeId ? <LayoutItemContent id={activeId} isOverlay /> : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
@@ -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 };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user