Files
resolutionflow/frontend/src/components/common/ActionMenu.tsx
Michael Chihlas 303a558432 refactor: replace hardcoded hex values with Tailwind semantic tokens
3,200+ hardcoded color values replaced with CSS variable-backed
Tailwind classes (bg-card, text-foreground, border-border, etc.).
Enables light mode via CSS variable swap. Only syntax highlighting
colors and intentional one-offs remain hardcoded (~15 values).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 04:34:35 -04:00

101 lines
2.8 KiB
TypeScript

import { useState, useRef, useEffect } from 'react'
import { MoreVertical } from 'lucide-react'
import { cn } from '@/lib/utils'
interface MenuAction {
label: string
icon?: React.ComponentType<{ className?: string }>
onClick: () => void
disabled?: boolean
variant?: 'default' | 'destructive'
}
interface ActionMenuProps {
actions: MenuAction[]
align?: 'left' | 'right' // default 'right'
}
export function ActionMenu({ actions, align = 'right' }: ActionMenuProps) {
const [isOpen, setIsOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!isOpen) return
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener('keydown', handleEscape)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('keydown', handleEscape)
}
}, [isOpen])
const handleItemClick = (action: MenuAction) => {
if (action.disabled) return
action.onClick()
setIsOpen(false)
}
return (
<div className="relative" ref={menuRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className={cn(
'rounded-md border border-border p-2 text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
aria-label="Actions"
>
<MoreVertical className="h-4 w-4" />
</button>
{isOpen && (
<div
className={cn(
'absolute z-50 mt-1 min-w-[180px] bg-card border border-border rounded-lg p-1',
align === 'right' ? 'right-0' : 'left-0'
)}
>
{actions.map((action, index) => {
const Icon = action.icon
return (
<button
key={index}
onClick={() => handleItemClick(action)}
disabled={action.disabled}
className={cn(
'flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm',
action.disabled
? 'cursor-not-allowed opacity-40'
: action.variant === 'destructive'
? 'text-red-400 hover:bg-accent hover:text-red-300'
: 'text-muted-foreground hover:bg-accent hover:text-foreground'
)}
>
{Icon && <Icon className="h-4 w-4" />}
{action.label}
</button>
)
})}
</div>
)}
</div>
)
}
export type { MenuAction, ActionMenuProps }
export default ActionMenu