aboutsummaryrefslogtreecommitdiff
path: root/src/components/ContextMenu
diff options
context:
space:
mode:
authorzwlucas <lucas.fariamo08@gmail.com>2026-06-05 20:52:54 +0000
committerzwlucas <lucas.fariamo08@gmail.com>2026-06-05 20:52:54 +0000
commit09f964451d7d92e9891430ec4595c1276d486aab (patch)
tree28da4483f5c28924a8c47fceb648b1baebe88224 /src/components/ContextMenu
downloadyace-09f964451d7d92e9891430ec4595c1276d486aab.tar.gz
yace-09f964451d7d92e9891430ec4595c1276d486aab.zip
feat: uploadHEADmaster
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'src/components/ContextMenu')
-rw-r--r--src/components/ContextMenu/ContextMenu.tsx184
1 files changed, 184 insertions, 0 deletions
diff --git a/src/components/ContextMenu/ContextMenu.tsx b/src/components/ContextMenu/ContextMenu.tsx
new file mode 100644
index 0000000..2c67116
--- /dev/null
+++ b/src/components/ContextMenu/ContextMenu.tsx
@@ -0,0 +1,184 @@
+import { useEffect, useRef, useState } from 'react'
+import { useContextMenuStore, type ContextMenuIcon } from '@/stores/useContextMenuStore'
+
+function MenuIcon({ icon }: { icon: ContextMenuIcon }) {
+ const c = 'currentColor'
+ switch (icon) {
+ case 'undo':
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <polyline points="1 4 1 10 7 10" /><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
+ </svg>
+ )
+ case 'redo':
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.13-9.36L23 10" />
+ </svg>
+ )
+ case 'cut':
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <circle cx="6" cy="6" r="3" /><circle cx="6" cy="18" r="3" /><line x1="8.12" y1="8.12" x2="15.88" y2="15.88" /><line x1="15.88" y1="8.12" x2="8.12" y2="15.88" />
+ </svg>
+ )
+ case 'copy':
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
+ </svg>
+ )
+ case 'paste':
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" /><rect x="8" y="2" width="8" height="4" rx="1" ry="1" />
+ </svg>
+ )
+ case 'selectAll':
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" /><line x1="9" y1="3" x2="9" y2="21" /><line x1="15" y1="3" x2="15" y2="21" /><line x1="3" y1="9" x2="21" y2="9" /><line x1="3" y1="15" x2="21" y2="15" />
+ </svg>
+ )
+ case 'close':
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
+ </svg>
+ )
+ case 'rename':
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
+ </svg>
+ )
+ case 'trash':
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <polyline points="3 6 5 6 21 6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
+ </svg>
+ )
+ case 'folder':
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
+ </svg>
+ )
+ case 'file':
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14,2 14,8 20,8" />
+ </svg>
+ )
+ case 'link':
+ return (
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
+ </svg>
+ )
+ }
+}
+
+export default function ContextMenu() {
+ const isOpen = useContextMenuStore((s) => s.isOpen)
+ const x = useContextMenuStore((s) => s.x)
+ const y = useContextMenuStore((s) => s.y)
+ const items = useContextMenuStore((s) => s.items)
+ const close = useContextMenuStore((s) => s.close)
+
+ const menuRef = useRef<HTMLDivElement>(null)
+ const [adjustedPos, setAdjustedPos] = useState({ x, y })
+ const [selectedIndex, setSelectedIndex] = useState(0)
+
+ const visibleItems = items.filter((i) => !i.separator)
+ const totalItems = visibleItems.length
+
+ useEffect(() => {
+ if (!isOpen || !menuRef.current) return
+ const rect = menuRef.current.getBoundingClientRect()
+ const vw = window.innerWidth
+ const vh = window.innerHeight
+ setAdjustedPos({
+ x: x + rect.width > vw ? vw - rect.width - 12 : x,
+ y: y + rect.height > vh ? vh - rect.height - 12 : y,
+ })
+ setSelectedIndex(0)
+ }, [isOpen, x, y])
+
+ useEffect(() => {
+ if (!isOpen) return
+ function handleKeyDown(e: KeyboardEvent) {
+ if (e.key === 'Escape') { e.preventDefault(); close(); return }
+ if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex((i) => Math.min(i + 1, totalItems - 1)); return }
+ if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex((i) => Math.max(i - 1, 0)); return }
+ if (e.key === 'Enter') {
+ e.preventDefault()
+ const item = visibleItems[selectedIndex]
+ if (item && !item.disabled) { item.action(); close() }
+ }
+ }
+ window.addEventListener('keydown', handleKeyDown)
+ return () => window.removeEventListener('keydown', handleKeyDown)
+ }, [isOpen, selectedIndex, totalItems, visibleItems, close])
+
+ useEffect(() => {
+ if (!isOpen || !menuRef.current) return
+ function handleClickOutside(e: MouseEvent) {
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
+ close()
+ }
+ }
+ requestAnimationFrame(() => window.addEventListener('mousedown', handleClickOutside))
+ return () => window.removeEventListener('mousedown', handleClickOutside)
+ }, [isOpen, close])
+
+ if (!isOpen) return null
+
+ return (
+ <div
+ ref={menuRef}
+ className="fixed z-50 min-w-[180px] py-1 rounded-xl shadow-glass overflow-hidden context-menu"
+ style={{
+ left: adjustedPos.x,
+ top: adjustedPos.y,
+ backgroundColor: 'var(--color-panel-bg)',
+ border: '1px solid var(--color-panel-border)',
+ }}
+ >
+ {items.map((item) => {
+ if (item.separator) {
+ return (
+ <div key={item.id} className="my-1 mx-2" style={{ height: '1px', backgroundColor: 'var(--color-panel-border)' }} />
+ )
+ }
+ const idx = visibleItems.indexOf(item)
+ const isSelected = idx === selectedIndex
+ return (
+ <button
+ key={item.id}
+ role="menuitem"
+ disabled={item.disabled}
+ className="w-full flex items-center gap-2.5 px-3 py-1.5 text-ui-sm text-left transition-colors duration-fast disabled:opacity-30 disabled:cursor-default"
+ style={{
+ backgroundColor: isSelected ? 'var(--color-panel-hover)' : 'transparent',
+ color: item.disabled ? 'var(--color-text-muted)' : 'var(--color-text-secondary)',
+ }}
+ onMouseEnter={() => setSelectedIndex(idx)}
+ onClick={() => { if (!item.disabled) { item.action(); close() } }}
+ >
+ {item.icon && (
+ <span className="w-[14px] shrink-0 text-text-muted">
+ <MenuIcon icon={item.icon} />
+ </span>
+ )}
+ {!item.icon && <span className="w-[14px] shrink-0" />}
+ <span className="flex-1 truncate">{item.label}</span>
+ {item.shortcut && (
+ <span className="text-ui-xs text-text-muted ml-4">{item.shortcut}</span>
+ )}
+ </button>
+ )
+ })}
+ </div>
+ )
+}