91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
import { ActionIcon } from './action-icon';
|
|
import type { IconName } from './action-icon';
|
|
|
|
export interface ActionMenuItem {
|
|
icon: IconName;
|
|
label: string;
|
|
onClick: () => void;
|
|
variant?: 'default' | 'danger';
|
|
disabled?: boolean;
|
|
}
|
|
|
|
interface ActionMenuProps {
|
|
items: ActionMenuItem[];
|
|
}
|
|
|
|
export function ActionMenu({ items }: ActionMenuProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
|
|
const close = useCallback(() => setOpen(false), []);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
function handleClickOutside(e: MouseEvent) {
|
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
close();
|
|
}
|
|
}
|
|
function handleEscape(e: KeyboardEvent) {
|
|
if (e.key === 'Escape') close();
|
|
}
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
document.addEventListener('keydown', handleEscape);
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
document.removeEventListener('keydown', handleEscape);
|
|
};
|
|
}, [open, close]);
|
|
|
|
return (
|
|
<div ref={menuRef} className="relative inline-block">
|
|
<ActionIcon
|
|
icon="more"
|
|
variant="ghost"
|
|
label="Actions"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setOpen((prev) => !prev);
|
|
}}
|
|
/>
|
|
{open && (
|
|
<div
|
|
role="menu"
|
|
className="absolute right-0 top-full mt-1 min-w-[160px] bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-lg py-1 z-50 animate-in fade-in"
|
|
>
|
|
{items.map((item, i) => (
|
|
<button
|
|
key={i}
|
|
role="menuitem"
|
|
disabled={item.disabled}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
item.onClick();
|
|
close();
|
|
}}
|
|
className={`w-full flex items-center gap-2.5 px-3 py-2 text-sm transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed ${
|
|
item.variant === 'danger'
|
|
? 'text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20'
|
|
: 'text-surface-700 dark:text-surface-300 hover:bg-surface-50 dark:hover:bg-surface-700'
|
|
}`}
|
|
>
|
|
<ActionIcon
|
|
icon={item.icon}
|
|
variant={item.variant === 'danger' ? 'danger' : 'ghost'}
|
|
label={item.label}
|
|
size="sm"
|
|
onClick={() => {}}
|
|
className="pointer-events-none"
|
|
/>
|
|
<span>{item.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|