moving menu

This commit is contained in:
Neil 2026-03-17 18:46:47 -07:00
parent 77b9572b58
commit 45e07deef8
5 changed files with 341 additions and 127 deletions

View File

@ -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>
</>
)
}

View File

@ -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>
</>
)
}

View File

@ -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>
</>
)
})

View File

@ -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}
/>
)

View File

@ -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}