From feccc4e17adf12f30494c5b1608aeb9fa745d940 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 15 Feb 2026 21:00:29 +0200 Subject: [PATCH] feat: shared layout animation - card expands into detail modal --- src/components/board/CardThumbnail.tsx | 1 + .../card-detail/CardDetailModal.tsx | 163 +++++++++++------- 2 files changed, 102 insertions(+), 62 deletions(-) diff --git a/src/components/board/CardThumbnail.tsx b/src/components/board/CardThumbnail.tsx index 941a1f8..01ad8d4 100644 --- a/src/components/board/CardThumbnail.tsx +++ b/src/components/board/CardThumbnail.tsx @@ -52,6 +52,7 @@ export function CardThumbnail({ card, boardLabels, columnId, onCardClick }: Card style={style} onClick={handleClick} className="w-full rounded-lg bg-pylon-surface shadow-sm text-left" + layoutId={`card-${card.id}`} variants={fadeSlideUp} initial={prefersReducedMotion ? false : "hidden"} animate="visible" diff --git a/src/components/card-detail/CardDetailModal.tsx b/src/components/card-detail/CardDetailModal.tsx index d200d94..35b0324 100644 --- a/src/components/card-detail/CardDetailModal.tsx +++ b/src/components/card-detail/CardDetailModal.tsx @@ -1,11 +1,5 @@ import { useState, useEffect, useRef } from "react"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, -} from "@/components/ui/dialog"; +import { AnimatePresence, motion } from "framer-motion"; import { Separator } from "@/components/ui/separator"; import { useBoardStore } from "@/stores/board-store"; import { MarkdownEditor } from "@/components/card-detail/MarkdownEditor"; @@ -13,6 +7,7 @@ import { ChecklistSection } from "@/components/card-detail/ChecklistSection"; import { LabelPicker } from "@/components/card-detail/LabelPicker"; import { DueDatePicker } from "@/components/card-detail/DueDatePicker"; import { AttachmentSection } from "@/components/card-detail/AttachmentSection"; +import { springs, staggerContainer, fadeSlideUp } from "@/lib/motion"; interface CardDetailModalProps { cardId: string | null; @@ -29,70 +24,115 @@ export function CardDetailModal({ cardId, onClose }: CardDetailModalProps) { const open = cardId != null && card != null; return ( - !isOpen && onClose()}> - - {card && cardId && ( - <> - {/* Hidden accessible description */} - - Card detail editor - + + {open && card && cardId && ( + <> + {/* Backdrop */} + -
- {/* Left panel: Title + Markdown (60%) */} -
- - + e.stopPropagation()} + > + {/* Close on Escape */} + + + {/* Hidden accessible description */} + Card detail editor + + + {/* Left panel: Title + Markdown (60%) */} + +
+ +
+ + +
+ + {/* Vertical separator */} + + + {/* Right sidebar (40%) */} + + + + + + -
- -
+ - {/* Vertical separator */} - + - {/* Right sidebar (40%) */} -
- + - + - + - - - - - - - - - - - -
-
- - )} -
-
+ + + + + + + )} + ); } +/* ---------- Escape key handler ---------- */ + +function EscapeHandler({ onClose }: { onClose: () => void }) { + useEffect(() => { + function handleKeyDown(e: KeyboardEvent) { + if (e.key === "Escape") onClose(); + } + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [onClose]); + return null; +} + /* ---------- Inline editable title ---------- */ interface InlineTitleProps { @@ -106,7 +146,6 @@ function InlineTitle({ cardId, title, updateCard }: InlineTitleProps) { const [draft, setDraft] = useState(title); const inputRef = useRef(null); - // Sync when title changes externally useEffect(() => { setDraft(title); }, [title]); @@ -152,12 +191,12 @@ function InlineTitle({ cardId, title, updateCard }: InlineTitleProps) { } return ( - setEditing(true)} className="cursor-pointer font-heading text-xl text-pylon-text transition-colors hover:text-pylon-accent" > {title} - + ); }