diff --git a/app/globals.css b/app/globals.css index 99ee3de..8440565 100644 --- a/app/globals.css +++ b/app/globals.css @@ -380,3 +380,27 @@ textarea:focus-visible { animation: none; } } + +/* The row-options menu pops from its anchor corner and slips away on close + (the anchor corner comes from an origin-* utility on the element). */ +@keyframes menuPopIn { + from { opacity: 0; transform: scale(0.92); } + to { opacity: 1; transform: scale(1); } +} +@keyframes menuPopOut { + from { opacity: 1; transform: scale(1); } + to { opacity: 0; transform: scale(0.96); } +} +.menu-pop { + animation: menuPopIn 130ms cubic-bezier(0.2, 0.7, 0.2, 1); +} +.menu-pop[data-closing] { + animation: menuPopOut 90ms ease-in forwards; + pointer-events: none; +} +@media (prefers-reduced-motion: reduce) { + .menu-pop, + .menu-pop[data-closing] { + animation: none; + } +} diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 9803ae6..dfdf152 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -7,6 +7,7 @@ import { ColorSwatchRow } from "@/components/ColorSwatchRow"; import { noteColorVar } from "@/lib/noteColors"; import { downloadNoteBody, downloadNotePdf } from "@/lib/downloadNote"; import { SyncStatus } from "@/lib/useNotes"; +import { useMenuPresence } from "@/lib/useMenuPresence"; import { Note } from "@/lib/types"; import { DotsIcon, @@ -459,6 +460,7 @@ function SidebarNoteRow({ onColor: (color: string | null) => void; }) { const [menuOpen, setMenuOpen] = useState(false); + const { mounted: menuMounted, closing: menuClosing } = useMenuPresence(menuOpen); const [flipUp, setFlipUp] = useState(false); const [isRenaming, setIsRenaming] = useState(false); const [renameValue, setRenameValue] = useState(""); @@ -575,16 +577,19 @@ function SidebarNoteRow({ )} - {menuOpen && ( + {menuMounted && ( <> -
setMenuOpen(false)} - /> + {menuOpen && ( +
setMenuOpen(false)} + /> + )}
{trashMode ? ( diff --git a/lib/useMenuPresence.ts b/lib/useMenuPresence.ts new file mode 100644 index 0000000..fcfa666 --- /dev/null +++ b/lib/useMenuPresence.ts @@ -0,0 +1,20 @@ +import { useEffect, useState } from "react"; + +/** + * Keeps a popup mounted briefly after `open` flips false so its exit + * animation can play. `closing` is true during that grace period — put it on + * a data attribute and drive the animation from CSS. + */ +export function useMenuPresence(open: boolean, closeMs = 90) { + const [mounted, setMounted] = useState(open); + useEffect(() => { + if (open) { + setMounted(true); + return; + } + if (!mounted) return; + const timer = window.setTimeout(() => setMounted(false), closeMs); + return () => window.clearTimeout(timer); + }, [open, mounted, closeMs]); + return { mounted, closing: mounted && !open }; +} diff --git a/screenshots/pr/menu-pop.webm b/screenshots/pr/menu-pop.webm new file mode 100644 index 0000000..ca4c7d5 Binary files /dev/null and b/screenshots/pr/menu-pop.webm differ