@@ -17,10 +17,15 @@ export default function DeliverableCard({ deliverable, isActive, index, projectC
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
items: [
|
||||
{ icon: '\u270e', label: 'Edit Deliverable', highlight: true, action: () => onEdit(deliverable) },
|
||||
{
|
||||
icon: '✎',
|
||||
label: 'Edit Deliverable',
|
||||
highlight: true,
|
||||
action: () => onEdit(deliverable),
|
||||
},
|
||||
{ separator: true },
|
||||
...STATUS_OPTIONS.map(s => ({
|
||||
icon: s.value === deliverable.status ? '\u25cf' : '\u25cb',
|
||||
icon: s.value === deliverable.status ? '●' : '○',
|
||||
label: `Mark ${s.label}`,
|
||||
action: async () => {
|
||||
storeUpdate(await updateDeliverable(deliverable.id, { status: s.value }))
|
||||
@@ -28,7 +33,7 @@ export default function DeliverableCard({ deliverable, isActive, index, projectC
|
||||
})),
|
||||
{ separator: true },
|
||||
{
|
||||
icon: '\u2715',
|
||||
icon: '✕',
|
||||
label: 'Delete Deliverable',
|
||||
danger: true,
|
||||
action: async () => {
|
||||
@@ -60,7 +65,6 @@ export default function DeliverableCard({ deliverable, isActive, index, projectC
|
||||
Selected
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-[10px] text-text-muted/50 font-mono">Deliverable {index + 1}</div>
|
||||
<div className="text-sm font-semibold text-text-primary leading-snug line-clamp-3">{deliverable.title}</div>
|
||||
<div className="text-xs text-text-muted/70 mt-auto pt-1">{formatDate(deliverable.due_date)}</div>
|
||||
@@ -68,7 +72,12 @@ export default function DeliverableCard({ deliverable, isActive, index, projectC
|
||||
</div>
|
||||
|
||||
{ctxMenu && (
|
||||
<ContextMenu x={ctxMenu.x} y={ctxMenu.y} items={ctxMenu.items} onClose={() => setCtxMenu(null)} />
|
||||
<ContextMenu
|
||||
x={ctxMenu.x}
|
||||
y={ctxMenu.y}
|
||||
items={ctxMenu.items}
|
||||
onClose={() => setCtxMenu(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
export default function ContextMenu({ x, y, items, onClose }) {
|
||||
const ref = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const onMouseDown = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose() }
|
||||
const onKey = (e) => { if (e.key === 'Escape') onClose() }
|
||||
const onMouseDown = (e) => {
|
||||
if (ref.current && !ref.current.contains(e.target)) onClose()
|
||||
}
|
||||
const onKey = (e) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
document.addEventListener('mousedown', onMouseDown)
|
||||
document.addEventListener('keydown', onKey)
|
||||
return () => {
|
||||
@@ -15,41 +20,41 @@ export default function ContextMenu({ x, y, items, onClose }) {
|
||||
}, [onClose])
|
||||
|
||||
// Keep menu inside viewport
|
||||
const W = 192
|
||||
const H = items.length * 34
|
||||
const W = 192
|
||||
const H = items.length * 34
|
||||
const adjX = Math.min(x, window.innerWidth - W - 8)
|
||||
const adjY = Math.min(y, window.innerHeight - H - 8)
|
||||
|
||||
return (
|
||||
// Portal to document.body — escapes any CSS transform stacking context
|
||||
// (e.g. the Drawer slide-in animation uses translateX which traps fixed children)
|
||||
return createPortal(
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ left: adjX, top: adjY }}
|
||||
className="fixed z-[200] min-w-[192px] bg-surface-elevated border border-surface-border rounded-xl shadow-2xl py-1.5 overflow-hidden"
|
||||
style={{ position: 'fixed', left: adjX, top: adjY }}
|
||||
className="z-[9999] bg-surface-elevated border border-surface-border rounded-xl shadow-2xl py-1.5 min-w-[192px]"
|
||||
>
|
||||
{items.map((item, i) =>
|
||||
item.separator ? (
|
||||
<div key={i} className="h-px bg-surface-border my-1 mx-2" />
|
||||
<div key={i} className="my-1 border-t border-surface-border/60" />
|
||||
) : (
|
||||
<button
|
||||
key={i}
|
||||
disabled={item.disabled}
|
||||
onClick={() => { item.action(); onClose() }}
|
||||
className={`w-full flex items-center gap-2.5 px-3 py-2 text-xs text-left transition-colors disabled:opacity-40 disabled:cursor-not-allowed
|
||||
${item.danger
|
||||
? 'text-red-400 hover:bg-red-500/10'
|
||||
: item.highlight
|
||||
? 'text-gold hover:bg-gold/10'
|
||||
: 'text-text-primary hover:bg-surface-border/40'
|
||||
}`}
|
||||
onClick={() => { item.action?.(); onClose() }}
|
||||
className={`w-full flex items-center gap-2.5 px-3 py-2 text-xs text-left transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${
|
||||
item.danger ? 'text-red-400 hover:bg-red-500/10' :
|
||||
item.highlight ? 'text-gold hover:bg-gold/10' :
|
||||
'text-text-primary hover:bg-surface-border/40'
|
||||
}`}
|
||||
>
|
||||
<span className="text-sm w-4 text-center leading-none flex-shrink-0">{item.icon}</span>
|
||||
<span>{item.label}</span>
|
||||
<span className="text-[11px] opacity-70 w-3.5 flex-shrink-0">{item.icon}</span>
|
||||
<span className="flex-1">{item.label}</span>
|
||||
{item.shortcut && (
|
||||
<span className="ml-auto text-text-muted/50 text-[10px]">{item.shortcut}</span>
|
||||
<span className="text-[9px] text-text-muted/40 font-mono ml-2">{item.shortcut}</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user