(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

at frontend-rewrite 99 lines 3.2 kB view raw
1import React, { useState, useRef, useEffect } from "react"; 2import { MoreHorizontal } from "lucide-react"; 3import { clsx } from "clsx"; 4 5export interface MoreMenuItem { 6 label: string; 7 icon?: React.ReactNode; 8 onClick: () => void; 9 variant?: "default" | "danger"; 10 disabled?: boolean; 11} 12 13interface MoreMenuProps { 14 items: MoreMenuItem[]; 15 className?: string; 16} 17 18export default function MoreMenu({ items, className }: MoreMenuProps) { 19 const [isOpen, setIsOpen] = useState(false); 20 const buttonRef = useRef<HTMLButtonElement>(null); 21 const menuRef = useRef<HTMLDivElement>(null); 22 23 useEffect(() => { 24 if (!isOpen) return; 25 26 const handleClickOutside = (e: MouseEvent) => { 27 if ( 28 menuRef.current && 29 !menuRef.current.contains(e.target as Node) && 30 buttonRef.current && 31 !buttonRef.current.contains(e.target as Node) 32 ) { 33 setIsOpen(false); 34 } 35 }; 36 37 const handleScroll = () => setIsOpen(false); 38 const handleEscape = (e: KeyboardEvent) => { 39 if (e.key === "Escape") setIsOpen(false); 40 }; 41 42 document.addEventListener("mousedown", handleClickOutside); 43 document.addEventListener("scroll", handleScroll, true); 44 document.addEventListener("keydown", handleEscape); 45 46 return () => { 47 document.removeEventListener("mousedown", handleClickOutside); 48 document.removeEventListener("scroll", handleScroll, true); 49 document.removeEventListener("keydown", handleEscape); 50 }; 51 }, [isOpen]); 52 53 if (items.length === 0) return null; 54 55 return ( 56 <div className={clsx("relative", className)}> 57 <button 58 ref={buttonRef} 59 onClick={() => setIsOpen(!isOpen)} 60 className="flex items-center px-2 py-1.5 rounded-lg text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-800 transition-all" 61 title="More options" 62 > 63 <MoreHorizontal size={16} /> 64 </button> 65 66 {isOpen && ( 67 <div 68 ref={menuRef} 69 className="absolute right-0 top-full mt-1 z-50 min-w-[180px] bg-white dark:bg-surface-900 border border-surface-200 dark:border-surface-700 rounded-xl shadow-lg py-1 animate-fade-in" 70 > 71 {items.map((item, i) => ( 72 <button 73 key={i} 74 onClick={() => { 75 item.onClick(); 76 setIsOpen(false); 77 }} 78 disabled={item.disabled} 79 className={clsx( 80 "w-full flex items-center gap-2.5 px-3.5 py-2 text-sm transition-colors text-left", 81 item.variant === "danger" 82 ? "text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20" 83 : "text-surface-700 dark:text-surface-300 hover:bg-surface-50 dark:hover:bg-surface-800", 84 item.disabled && "opacity-50 cursor-not-allowed", 85 )} 86 > 87 {item.icon && ( 88 <span className="flex-shrink-0 w-4 h-4 flex items-center justify-center"> 89 {item.icon} 90 </span> 91 )} 92 {item.label} 93 </button> 94 ))} 95 </div> 96 )} 97 </div> 98 ); 99}