Files
resolutionflow/frontend/src/components/common/ContextMenu.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

105 lines
2.7 KiB
TypeScript

import { useEffect, useRef, useCallback } from 'react'
import { cn } from '@/lib/utils'
export interface ContextMenuPosition {
x: number
y: number
}
export interface ContextMenuItem {
id: string
label: string
icon?: React.ReactNode
onClick: () => void
variant?: 'default' | 'danger'
separator?: boolean
}
interface ContextMenuProps {
position: ContextMenuPosition
items: ContextMenuItem[]
onClose: () => void
}
export function ContextMenu({ position, items, onClose }: ContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null)
const handleClickOutside = useCallback(
(e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
onClose()
}
},
[onClose]
)
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
},
[onClose]
)
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('keydown', handleKeyDown)
}
}, [handleClickOutside, handleKeyDown])
// Adjust position to stay within viewport
useEffect(() => {
if (!menuRef.current) return
const rect = menuRef.current.getBoundingClientRect()
const el = menuRef.current
if (position.x + rect.width > window.innerWidth) {
el.style.left = `${position.x - rect.width}px`
}
if (position.y + rect.height > window.innerHeight) {
el.style.top = `${position.y - rect.height}px`
}
}, [position])
return (
<div
ref={menuRef}
style={{
position: 'fixed',
zIndex: 100,
left: position.x,
top: position.y,
}}
className="min-w-[200px] rounded-xl border border-border bg-card p-1 shadow-lg"
>
{items.map((item) => (
<div key={item.id}>
{item.separator && (
<div className="my-1 border-t border-border" />
)}
<button
onClick={() => {
item.onClick()
onClose()
}}
className={cn(
'flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm transition-colors',
item.variant === 'danger'
? 'text-rose-400 hover:bg-rose-500/10'
: 'text-foreground hover:bg-border'
)}
>
{item.icon && (
<span className="flex h-4 w-4 items-center justify-center text-muted-foreground">
{item.icon}
</span>
)}
{item.label}
</button>
</div>
))}
</div>
)
}