initial: standalone repo from monorepo split
This commit is contained in:
90
src/components/ui/action-menu.tsx
Normal file
90
src/components/ui/action-menu.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user