feat: add Framer Motion animations with spring physics and reduced-motion support

This commit is contained in:
Your Name
2026-02-15 19:17:52 +02:00
parent e020ba6e8b
commit e2ce484955
5 changed files with 30 additions and 12 deletions

View File

@@ -1,4 +1,5 @@
import { format, isPast, isToday } from "date-fns"; import { format, isPast, isToday } from "date-fns";
import { motion, useReducedMotion } from "framer-motion";
import { useSortable } from "@dnd-kit/sortable"; import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import type { Card, Label } from "@/types/board"; import type { Card, Label } from "@/types/board";
@@ -13,6 +14,8 @@ interface CardThumbnailProps {
} }
export function CardThumbnail({ card, boardLabels, columnId, onCardClick }: CardThumbnailProps) { export function CardThumbnail({ card, boardLabels, columnId, onCardClick }: CardThumbnailProps) {
const prefersReducedMotion = useReducedMotion();
const { const {
attributes, attributes,
listeners, listeners,
@@ -40,11 +43,14 @@ export function CardThumbnail({ card, boardLabels, columnId, onCardClick }: Card
} }
return ( return (
<button <motion.button
ref={setNodeRef} ref={setNodeRef}
style={style} style={style}
onClick={handleClick} onClick={handleClick}
className="w-full rounded-lg bg-pylon-surface p-3 shadow-sm text-left transition-all duration-200 hover:-translate-y-px hover:shadow-md" className="w-full rounded-lg bg-pylon-surface p-3 shadow-sm text-left transition-all duration-200 hover:-translate-y-px hover:shadow-md"
initial={prefersReducedMotion ? false : { opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
{...attributes} {...attributes}
{...listeners} {...listeners}
> >
@@ -77,6 +83,6 @@ export function CardThumbnail({ card, boardLabels, columnId, onCardClick }: Card
)} )}
</div> </div>
)} )}
</button> </motion.button>
); );
} }

View File

@@ -1,5 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { motion, useReducedMotion } from "framer-motion";
import { useDroppable } from "@dnd-kit/core"; import { useDroppable } from "@dnd-kit/core";
import { import {
SortableContext, SortableContext,
@@ -29,6 +30,7 @@ interface KanbanColumnProps {
export function KanbanColumn({ column, onCardClick }: KanbanColumnProps) { export function KanbanColumn({ column, onCardClick }: KanbanColumnProps) {
const [showAddCard, setShowAddCard] = useState(false); const [showAddCard, setShowAddCard] = useState(false);
const board = useBoardStore((s) => s.board); const board = useBoardStore((s) => s.board);
const prefersReducedMotion = useReducedMotion();
const width = WIDTH_MAP[column.width]; const width = WIDTH_MAP[column.width];
@@ -59,10 +61,13 @@ export function KanbanColumn({ column, onCardClick }: KanbanColumnProps) {
}; };
return ( return (
<div <motion.div
ref={setSortableNodeRef} ref={setSortableNodeRef}
style={style} style={style}
className="group/column flex shrink-0 flex-col rounded-lg bg-pylon-column" className="group/column flex shrink-0 flex-col rounded-lg bg-pylon-column"
initial={prefersReducedMotion ? false : { opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
{...attributes} {...attributes}
> >
{/* The column header is the drag handle for column reordering */} {/* The column header is the drag handle for column reordering */}
@@ -113,6 +118,6 @@ export function KanbanColumn({ column, onCardClick }: KanbanColumnProps) {
</Button> </Button>
</div> </div>
)} )}
</div> </motion.div>
); );
} }

View File

@@ -1,5 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { motion, useReducedMotion } from "framer-motion";
import { Trash2, Copy } from "lucide-react"; import { Trash2, Copy } from "lucide-react";
import { import {
ContextMenu, ContextMenu,
@@ -24,10 +25,12 @@ import { deleteBoard, loadBoard, saveBoard } from "@/lib/storage";
interface BoardCardProps { interface BoardCardProps {
board: BoardMeta; board: BoardMeta;
index?: number;
} }
export function BoardCard({ board }: BoardCardProps) { export function BoardCard({ board, index = 0 }: BoardCardProps) {
const [confirmDelete, setConfirmDelete] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false);
const prefersReducedMotion = useReducedMotion();
const setView = useAppStore((s) => s.setView); const setView = useAppStore((s) => s.setView);
const addRecentBoard = useAppStore((s) => s.addRecentBoard); const addRecentBoard = useAppStore((s) => s.addRecentBoard);
@@ -66,7 +69,11 @@ export function BoardCard({ board }: BoardCardProps) {
} }
return ( return (
<> <motion.div
initial={prefersReducedMotion ? false : { opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 25, delay: index * 0.05 }}
>
<ContextMenu> <ContextMenu>
<ContextMenuTrigger asChild> <ContextMenuTrigger asChild>
<button <button
@@ -141,6 +148,6 @@ export function BoardCard({ board }: BoardCardProps) {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</> </motion.div>
); );
} }

View File

@@ -60,8 +60,8 @@ export function BoardList() {
{/* Board grid */} {/* Board grid */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{boards.map((board) => ( {boards.map((board, index) => (
<BoardCard key={board.id} board={board} /> <BoardCard key={board.id} board={board} index={index} />
))} ))}
</div> </div>
</div> </div>

View File

@@ -134,10 +134,10 @@ function ChecklistRow({ item, onToggle, onUpdate, onDelete }: ChecklistRowProps)
setDraft(item.text); setDraft(item.text);
setEditing(true); setEditing(true);
}} }}
className={`flex-1 cursor-pointer text-sm ${ className={`flex-1 cursor-pointer text-sm transition-all duration-200 ${
item.checked item.checked
? "line-through text-pylon-text-secondary" ? "line-through text-pylon-text-secondary opacity-60"
: "text-pylon-text" : "text-pylon-text opacity-100"
}`} }`}
> >
{item.text} {item.text}