feat: add Framer Motion animations with spring physics and reduced-motion support
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user