moving menu
This commit is contained in:
parent
77b9572b58
commit
45e07deef8
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useMemo } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
|
|
@ -16,12 +16,12 @@ import {
|
|||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { X, Plus, Terminal as TerminalIcon } from 'lucide-react'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger
|
||||
} from '@/components/ui/context-menu'
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import type { TerminalTab } from '../../../shared/types'
|
||||
|
||||
interface SortableTabProps {
|
||||
|
|
@ -50,6 +50,8 @@ const TAB_COLORS = [
|
|||
{ label: 'Gray', value: '#9ca3af' }
|
||||
]
|
||||
|
||||
const CLOSE_ALL_CONTEXT_MENUS_EVENT = 'orca-close-all-context-menus'
|
||||
|
||||
function SortableTab({
|
||||
tab,
|
||||
tabCount,
|
||||
|
|
@ -72,10 +74,25 @@ function SortableTab({
|
|||
zIndex: isDragging ? 10 : undefined,
|
||||
opacity: isDragging ? 0.8 : 1
|
||||
}
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const [menuPoint, setMenuPoint] = useState({ x: 0, y: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
const closeMenu = (): void => setMenuOpen(false)
|
||||
window.addEventListener(CLOSE_ALL_CONTEXT_MENUS_EVENT, closeMenu)
|
||||
return () => window.removeEventListener(CLOSE_ALL_CONTEXT_MENUS_EVENT, closeMenu)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<>
|
||||
<div
|
||||
onContextMenuCapture={(event) => {
|
||||
event.preventDefault()
|
||||
window.dispatchEvent(new Event(CLOSE_ALL_CONTEXT_MENUS_EVENT))
|
||||
setMenuPoint({ x: event.clientX, y: event.clientY })
|
||||
setMenuOpen(true)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
|
|
@ -87,6 +104,7 @@ function SortableTab({
|
|||
: 'bg-card text-muted-foreground hover:text-foreground hover:bg-accent/50'
|
||||
}`}
|
||||
onPointerDown={(e) => {
|
||||
if (e.button !== 0) return
|
||||
onActivate(tab.id)
|
||||
listeners?.onPointerDown?.(e)
|
||||
}}
|
||||
|
|
@ -121,55 +139,69 @@ function SortableTab({
|
|||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
</div>
|
||||
|
||||
<ContextMenuContent className="w-56">
|
||||
<ContextMenuItem onClick={() => onClose(tab.id)}>Close</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => onCloseOthers(tab.id)} disabled={tabCount <= 1}>
|
||||
Close Others
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => onCloseToRight(tab.id)} disabled={!hasTabsToRight}>
|
||||
Close Tabs To The Right
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem
|
||||
onSelect={() => {
|
||||
const next = window.prompt('Change tab title', tab.customTitle ?? tab.title)
|
||||
if (next === null) return
|
||||
const trimmed = next.trim()
|
||||
onSetCustomTitle(tab.id, trimmed.length > 0 ? trimmed : null)
|
||||
}}
|
||||
>
|
||||
Change Title
|
||||
</ContextMenuItem>
|
||||
<div className="px-2 pt-1.5 pb-1">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-1.5">Tab Color</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{TAB_COLORS.map((color) => {
|
||||
const isSelected = tab.color === color.value
|
||||
return (
|
||||
<ContextMenuItem
|
||||
key={color.label}
|
||||
className={`relative h-4 w-4 min-w-4 p-0 rounded-full border ${
|
||||
isSelected ? 'ring-1 ring-foreground/70 ring-offset-1 ring-offset-popover' : ''
|
||||
} ${
|
||||
color.value ? 'border-transparent' : 'border-muted-foreground/50 bg-transparent'
|
||||
}`}
|
||||
style={color.value ? { backgroundColor: color.value } : undefined}
|
||||
onSelect={() => {
|
||||
onSetTabColor(tab.id, color.value)
|
||||
}}
|
||||
>
|
||||
{color.value === null && (
|
||||
<span className="absolute block h-px w-3 rotate-45 bg-muted-foreground/80" />
|
||||
)}
|
||||
</ContextMenuItem>
|
||||
)
|
||||
})}
|
||||
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen} modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
aria-hidden
|
||||
tabIndex={-1}
|
||||
className="pointer-events-none fixed size-px opacity-0"
|
||||
style={{ left: menuPoint.x, top: menuPoint.y }}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" sideOffset={0} align="start">
|
||||
<DropdownMenuItem onSelect={() => onClose(tab.id)}>Close</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => onCloseOthers(tab.id)} disabled={tabCount <= 1}>
|
||||
Close Others
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => onCloseToRight(tab.id)} disabled={!hasTabsToRight}>
|
||||
Close Tabs To The Right
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
const next = window.prompt('Change tab title', tab.customTitle ?? tab.title)
|
||||
if (next === null) return
|
||||
const trimmed = next.trim()
|
||||
onSetCustomTitle(tab.id, trimmed.length > 0 ? trimmed : null)
|
||||
}}
|
||||
>
|
||||
Change Title
|
||||
</DropdownMenuItem>
|
||||
<div className="px-2 pt-1.5 pb-1">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-1.5">Tab Color</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{TAB_COLORS.map((color) => {
|
||||
const isSelected = tab.color === color.value
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={color.label}
|
||||
className={`relative h-4 w-4 min-w-4 p-0 rounded-full border ${
|
||||
isSelected
|
||||
? 'ring-1 ring-foreground/70 ring-offset-1 ring-offset-popover'
|
||||
: ''
|
||||
} ${
|
||||
color.value
|
||||
? 'border-transparent'
|
||||
: 'border-muted-foreground/50 bg-transparent'
|
||||
}`}
|
||||
style={color.value ? { backgroundColor: color.value } : undefined}
|
||||
onSelect={() => {
|
||||
onSetTabColor(tab.id, color.value)
|
||||
}}
|
||||
>
|
||||
{color.value === null && (
|
||||
<span className="absolute block h-px w-3 rotate-45 bg-muted-foreground/80" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Restty, getBuiltinTheme } from 'restty'
|
||||
import { Clipboard, Copy, Eraser, PanelBottomOpen, PanelRightOpen, X } from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useAppStore } from '../store'
|
||||
|
||||
type PtyTransport = {
|
||||
|
|
@ -27,6 +36,8 @@ type PtyTransport = {
|
|||
destroy?: () => void | Promise<void>
|
||||
}
|
||||
|
||||
const CLOSE_ALL_CONTEXT_MENUS_EVENT = 'orca-close-all-context-menus'
|
||||
|
||||
const OSC_TITLE_RE = /\x1b\]([012]);([^\x07\x1b]*?)(?:\x07|\x1b\\)/g
|
||||
|
||||
function extractLastOscTitle(data: string): string | null {
|
||||
|
|
@ -149,7 +160,16 @@ export default function TerminalPane({
|
|||
}: TerminalPaneProps): React.JSX.Element {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const resttyRef = useRef<Restty | null>(null)
|
||||
const contextPaneIdRef = useRef<number | null>(null)
|
||||
const wasActiveRef = useRef(false)
|
||||
const [terminalMenuOpen, setTerminalMenuOpen] = useState(false)
|
||||
const [terminalMenuPoint, setTerminalMenuPoint] = useState({ x: 0, y: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
const closeMenu = (): void => setTerminalMenuOpen(false)
|
||||
window.addEventListener(CLOSE_ALL_CONTEXT_MENUS_EVENT, closeMenu)
|
||||
return () => window.removeEventListener(CLOSE_ALL_CONTEXT_MENUS_EVENT, closeMenu)
|
||||
}, [])
|
||||
|
||||
const updateTabTitle = useAppStore((s) => s.updateTabTitle)
|
||||
const updateTabPtyId = useAppStore((s) => s.updateTabPtyId)
|
||||
|
|
@ -194,6 +214,7 @@ export default function TerminalPane({
|
|||
createInitialPane: false,
|
||||
autoInit: false,
|
||||
shortcuts: { enabled: true },
|
||||
defaultContextMenu: false,
|
||||
appOptions: ({ id }) => {
|
||||
const onExit = (): void => {
|
||||
// Schedule close via parent
|
||||
|
|
@ -341,11 +362,134 @@ export default function TerminalPane({
|
|||
return () => window.removeEventListener('keydown', onKeyDown, { capture: true })
|
||||
}, [isActive])
|
||||
|
||||
const resolveMenuPane = () => {
|
||||
const restty = resttyRef.current
|
||||
if (!restty) return null
|
||||
const panes = restty.getPanes()
|
||||
|
||||
if (contextPaneIdRef.current !== null) {
|
||||
const clickedPane = panes.find((p) => p.id === contextPaneIdRef.current) ?? null
|
||||
if (clickedPane) return clickedPane
|
||||
}
|
||||
return restty.getActivePane() ?? panes[0] ?? null
|
||||
}
|
||||
|
||||
const handleCopy = async (): Promise<void> => {
|
||||
const pane = resolveMenuPane()
|
||||
if (!pane) return
|
||||
await pane.app.copySelectionToClipboard()
|
||||
}
|
||||
|
||||
const handlePaste = async (): Promise<void> => {
|
||||
const pane = resolveMenuPane()
|
||||
if (!pane) return
|
||||
await pane.app.pasteFromClipboard()
|
||||
}
|
||||
|
||||
const handleSplitRight = (): void => {
|
||||
const pane = resolveMenuPane()
|
||||
if (!pane) return
|
||||
resttyRef.current?.splitPane(pane.id, 'vertical')
|
||||
}
|
||||
|
||||
const handleSplitDown = (): void => {
|
||||
const pane = resolveMenuPane()
|
||||
if (!pane) return
|
||||
resttyRef.current?.splitPane(pane.id, 'horizontal')
|
||||
}
|
||||
|
||||
const handleClosePane = (): void => {
|
||||
const pane = resolveMenuPane()
|
||||
if (!pane) return
|
||||
const panes = resttyRef.current?.getPanes() ?? []
|
||||
if (panes.length <= 1) return
|
||||
resttyRef.current?.closePane(pane.id)
|
||||
}
|
||||
|
||||
const handleClearScreen = (): void => {
|
||||
const pane = resolveMenuPane()
|
||||
if (!pane) return
|
||||
pane.app.clearScreen()
|
||||
}
|
||||
|
||||
const canClosePane = (resttyRef.current?.getPanes().length ?? 1) > 1
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="absolute inset-0 min-h-0 min-w-0"
|
||||
style={{ display: isActive ? 'flex' : 'none' }}
|
||||
/>
|
||||
<>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="absolute inset-0 min-h-0 min-w-0"
|
||||
style={{ display: isActive ? 'flex' : 'none' }}
|
||||
onContextMenuCapture={(event) => {
|
||||
event.preventDefault()
|
||||
window.dispatchEvent(new Event(CLOSE_ALL_CONTEXT_MENUS_EVENT))
|
||||
|
||||
const restty = resttyRef.current
|
||||
if (!restty) {
|
||||
contextPaneIdRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
const target = event.target
|
||||
if (!(target instanceof Node)) {
|
||||
contextPaneIdRef.current = null
|
||||
return
|
||||
}
|
||||
const clickedPane =
|
||||
restty.getPanes().find((pane) => pane.container.contains(target)) ?? null
|
||||
contextPaneIdRef.current = clickedPane?.id ?? null
|
||||
|
||||
const bounds = event.currentTarget.getBoundingClientRect()
|
||||
setTerminalMenuPoint({ x: event.clientX - bounds.left, y: event.clientY - bounds.top })
|
||||
setTerminalMenuOpen(true)
|
||||
}}
|
||||
/>
|
||||
<DropdownMenu open={terminalMenuOpen} onOpenChange={setTerminalMenuOpen} modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
aria-hidden
|
||||
tabIndex={-1}
|
||||
className="pointer-events-none absolute size-px opacity-0"
|
||||
style={{ left: terminalMenuPoint.x, top: terminalMenuPoint.y }}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" sideOffset={0} align="start">
|
||||
<DropdownMenuItem onSelect={() => void handleCopy()}>
|
||||
<Copy />
|
||||
Copy
|
||||
<DropdownMenuShortcut>⌘C</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => void handlePaste()}>
|
||||
<Clipboard />
|
||||
Paste
|
||||
<DropdownMenuShortcut>⌘V</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={handleSplitRight}>
|
||||
<PanelRightOpen />
|
||||
Split Right
|
||||
<DropdownMenuShortcut>⌘D</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={handleSplitDown}>
|
||||
<PanelBottomOpen />
|
||||
Split Down
|
||||
<DropdownMenuShortcut>⌘⇧D</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
disabled={!canClosePane}
|
||||
onSelect={handleClosePane}
|
||||
>
|
||||
<X />
|
||||
Close Pane
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={handleClearScreen}>
|
||||
<Eraser />
|
||||
Clear Screen
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useCallback } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator
|
||||
} from '@/components/ui/context-menu'
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
FolderOpen,
|
||||
Copy,
|
||||
|
|
@ -25,11 +25,21 @@ interface Props {
|
|||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const CLOSE_ALL_CONTEXT_MENUS_EVENT = 'orca-close-all-context-menus'
|
||||
|
||||
const WorktreeContextMenu = React.memo(function WorktreeContextMenu({ worktree, children }: Props) {
|
||||
const updateWorktreeMeta = useAppStore((s) => s.updateWorktreeMeta)
|
||||
const removeWorktree = useAppStore((s) => s.removeWorktree)
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const closeTab = useAppStore((s) => s.closeTab)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const [menuPoint, setMenuPoint] = useState({ x: 0, y: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
const closeMenu = (): void => setMenuOpen(false)
|
||||
window.addEventListener(CLOSE_ALL_CONTEXT_MENUS_EVENT, closeMenu)
|
||||
return () => window.removeEventListener(CLOSE_ALL_CONTEXT_MENUS_EVENT, closeMenu)
|
||||
}, [])
|
||||
|
||||
const handleOpenInFinder = useCallback(() => {
|
||||
window.api.shell.openPath(worktree.path)
|
||||
|
|
@ -70,45 +80,67 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({ worktree,
|
|||
}, [worktree.id, removeWorktree])
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
||||
<ContextMenuContent className="w-52">
|
||||
<ContextMenuItem onClick={handleOpenInFinder}>
|
||||
<FolderOpen className="size-3.5" />
|
||||
Open in Finder
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={handleCopyPath}>
|
||||
<Copy className="size-3.5" />
|
||||
Copy Path
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onClick={handleToggleRead}>
|
||||
{worktree.isUnread ? <Eye className="size-3.5" /> : <EyeOff className="size-3.5" />}
|
||||
{worktree.isUnread ? 'Mark Read' : 'Mark Unread'}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={handleLinkIssue}>
|
||||
<Link className="size-3.5" />
|
||||
{worktree.linkedIssue ? 'Edit GH Issue' : 'Link GH Issue'}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={handleComment}>
|
||||
<MessageSquare className="size-3.5" />
|
||||
{worktree.comment ? 'Edit Comment' : 'Add Comment'}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onClick={handleCloseTerminals}>
|
||||
<XCircle className="size-3.5" />
|
||||
Close Terminals
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={handleArchive}>
|
||||
<Archive className="size-3.5" />
|
||||
Archive
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem variant="destructive" onClick={handleDelete}>
|
||||
<Trash2 className="size-3.5" />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
<>
|
||||
<div
|
||||
className="relative"
|
||||
onContextMenuCapture={(event) => {
|
||||
event.preventDefault()
|
||||
window.dispatchEvent(new Event(CLOSE_ALL_CONTEXT_MENUS_EVENT))
|
||||
const bounds = event.currentTarget.getBoundingClientRect()
|
||||
setMenuPoint({ x: event.clientX - bounds.left, y: event.clientY - bounds.top })
|
||||
setMenuOpen(true)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen} modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
aria-hidden
|
||||
tabIndex={-1}
|
||||
className="pointer-events-none absolute size-px opacity-0"
|
||||
style={{ left: menuPoint.x, top: menuPoint.y }}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-52" sideOffset={0} align="start">
|
||||
<DropdownMenuItem onSelect={handleOpenInFinder}>
|
||||
<FolderOpen className="size-3.5" />
|
||||
Open in Finder
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={handleCopyPath}>
|
||||
<Copy className="size-3.5" />
|
||||
Copy Path
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={handleToggleRead}>
|
||||
{worktree.isUnread ? <Eye className="size-3.5" /> : <EyeOff className="size-3.5" />}
|
||||
{worktree.isUnread ? 'Mark Read' : 'Mark Unread'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={handleLinkIssue}>
|
||||
<Link className="size-3.5" />
|
||||
{worktree.linkedIssue ? 'Edit GH Issue' : 'Link GH Issue'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={handleComment}>
|
||||
<MessageSquare className="size-3.5" />
|
||||
{worktree.comment ? 'Edit Comment' : 'Add Comment'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={handleCloseTerminals}>
|
||||
<XCircle className="size-3.5" />
|
||||
Close Terminals
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={handleArchive}>
|
||||
<Archive className="size-3.5" />
|
||||
Archive
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem variant="destructive" onSelect={handleDelete}>
|
||||
<Trash2 className="size-3.5" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { ContextMenu as ContextMenuPrimitive } from 'radix-ui'
|
|||
import { cn } from '@/lib/utils'
|
||||
|
||||
function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" modal={false} {...props} />
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({
|
||||
|
|
@ -45,7 +45,7 @@ function ContextMenuSubTrigger({
|
|||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
"flex cursor-default items-center rounded-[7px] px-2 py-1 text-[12px] leading-5 font-[450] outline-hidden select-none focus:bg-black/8 dark:focus:bg-white/14 focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-black/8 dark:data-[state=open]:bg-white/14 data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -64,7 +64,7 @@ function ContextMenuSubContent({
|
|||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
'z-50 min-w-[11rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-[11px] border border-black/12 bg-white/42 p-1 text-popover-foreground shadow-[0_16px_36px_rgba(15,23,42,0.18),inset_0_1px_0_rgba(255,255,255,0.4)] backdrop-blur-2xl dark:border-white/14 dark:bg-zinc-950/36 dark:shadow-[0_20px_44px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.06)] data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -81,7 +81,7 @@ function ContextMenuContent({
|
|||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
'z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
'z-50 max-h-(--radix-context-menu-content-available-height) min-w-[11rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-[11px] border border-black/12 bg-white/42 p-1 text-popover-foreground shadow-[0_16px_36px_rgba(15,23,42,0.18),inset_0_1px_0_rgba(255,255,255,0.4)] backdrop-blur-2xl dark:border-white/14 dark:bg-zinc-950/36 dark:shadow-[0_20px_44px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.06)] data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -105,7 +105,7 @@ function ContextMenuItem({
|
|||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
|
||||
"relative flex cursor-default items-center gap-2 rounded-[7px] px-2 py-1 text-[12px] leading-5 font-[450] outline-hidden select-none focus:bg-black/8 dark:focus:bg-white/14 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -123,7 +123,7 @@ function ContextMenuCheckboxItem({
|
|||
<ContextMenuPrimitive.CheckboxItem
|
||||
data-slot="context-menu-checkbox-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"relative flex cursor-default items-center gap-2 rounded-[7px] py-1 pr-2 pl-8 text-[12px] leading-5 font-[450] outline-hidden select-none focus:bg-black/8 dark:focus:bg-white/14 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
|
|
@ -148,7 +148,7 @@ function ContextMenuRadioItem({
|
|||
<ContextMenuPrimitive.RadioItem
|
||||
data-slot="context-menu-radio-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"relative flex cursor-default items-center gap-2 rounded-[7px] py-1 pr-2 pl-8 text-[12px] leading-5 font-[450] outline-hidden select-none focus:bg-black/8 dark:focus:bg-white/14 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -174,7 +174,10 @@ function ContextMenuLabel({
|
|||
<ContextMenuPrimitive.Label
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn('px-2 py-1.5 text-sm font-medium text-foreground data-[inset]:pl-8', className)}
|
||||
className={cn(
|
||||
'px-2 py-1 text-[11px] font-semibold text-muted-foreground data-[inset]:pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
@ -187,7 +190,7 @@ function ContextMenuSeparator({
|
|||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
||||
className={cn('my-1 h-px bg-border/70', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
@ -197,7 +200,7 @@ function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span
|
|||
return (
|
||||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
|
||||
className={cn('ml-auto text-[11px] tracking-normal text-muted-foreground/85', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ function DropdownMenuContent({
|
|||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
'z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[11rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-[11px] border border-black/12 bg-white/42 p-1 text-popover-foreground shadow-[0_16px_36px_rgba(15,23,42,0.18),inset_0_1px_0_rgba(255,255,255,0.4)] backdrop-blur-2xl dark:border-white/14 dark:bg-zinc-950/36 dark:shadow-[0_20px_44px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.06)] data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -59,7 +59,7 @@ function DropdownMenuItem({
|
|||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
|
||||
"relative flex cursor-default items-center gap-2 rounded-[7px] px-2 py-1 text-[12px] leading-5 font-[450] outline-hidden select-none focus:bg-black/8 dark:focus:bg-white/14 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -77,7 +77,7 @@ function DropdownMenuCheckboxItem({
|
|||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"relative flex cursor-default items-center gap-2 rounded-[7px] py-1 pr-2 pl-8 text-[12px] leading-5 font-[450] outline-hidden select-none focus:bg-black/8 dark:focus:bg-white/14 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
|
|
@ -108,7 +108,7 @@ function DropdownMenuRadioItem({
|
|||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"relative flex cursor-default items-center gap-2 rounded-[7px] py-1 pr-2 pl-8 text-[12px] leading-5 font-[450] outline-hidden select-none focus:bg-black/8 dark:focus:bg-white/14 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -134,7 +134,10 @@ function DropdownMenuLabel({
|
|||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', className)}
|
||||
className={cn(
|
||||
'px-2 py-1 text-[11px] font-semibold text-muted-foreground data-[inset]:pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
@ -147,7 +150,7 @@ function DropdownMenuSeparator({
|
|||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
||||
className={cn('my-1 h-px bg-border/70', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
@ -157,7 +160,7 @@ function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'spa
|
|||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
|
||||
className={cn('ml-auto text-[11px] tracking-normal text-muted-foreground/85', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
@ -180,7 +183,7 @@ function DropdownMenuSubTrigger({
|
|||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
"flex cursor-default items-center gap-2 rounded-[7px] px-2 py-1 text-[12px] leading-5 font-[450] outline-hidden select-none focus:bg-black/8 dark:focus:bg-white/14 focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-black/8 dark:data-[state=open]:bg-white/14 data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -199,7 +202,7 @@ function DropdownMenuSubContent({
|
|||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
'z-50 min-w-[11rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-[11px] border border-black/12 bg-white/42 p-1 text-popover-foreground shadow-[0_16px_36px_rgba(15,23,42,0.18),inset_0_1px_0_rgba(255,255,255,0.4)] backdrop-blur-2xl dark:border-white/14 dark:bg-zinc-950/36 dark:shadow-[0_20px_44px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.06)] data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
Loading…
Reference in New Issue