Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
19 changes: 12 additions & 7 deletions components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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("");
Expand Down Expand Up @@ -575,16 +577,19 @@ function SidebarNoteRow({
</button>
</>
)}
{menuOpen && (
{menuMounted && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setMenuOpen(false)}
/>
{menuOpen && (
<div
className="fixed inset-0 z-10"
onClick={() => setMenuOpen(false)}
/>
)}
<div
role="menu"
className={`absolute right-1 z-20 w-44 overflow-hidden rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] py-1 text-sm shadow-lg ${
flipUp ? "bottom-7" : "top-7"
data-closing={menuClosing || undefined}
className={`menu-pop absolute right-1 z-20 w-44 overflow-hidden rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] py-1 text-sm shadow-lg ${
flipUp ? "bottom-7 origin-bottom-right" : "top-7 origin-top-right"
}`}
>
{trashMode ? (
Expand Down
20 changes: 20 additions & 0 deletions lib/useMenuPresence.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
Binary file added screenshots/pr/menu-pop.webm
Binary file not shown.
Loading