mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 15:46:37 +08:00
feat(waf): 规则画布右键删除节点与连线
覆盖画布默认右键菜单;节点/连线右键弹出删除项,系统节点禁用。
This commit is contained in:
@@ -28,6 +28,7 @@ sidebar: false
|
|||||||
### 改进
|
### 改进
|
||||||
|
|
||||||
- WAF 规则编辑器支持为节点自定义显示名称,并从节点库拖放到画布指定位置添加节点。
|
- WAF 规则编辑器支持为节点自定义显示名称,并从节点库拖放到画布指定位置添加节点。
|
||||||
|
- WAF 规则画布支持右键删除节点或连线,并屏蔽浏览器默认右键菜单。
|
||||||
|
|
||||||
## [v3.4.0] - 2026-07-19
|
## [v3.4.0] - 2026-07-19
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { type DragEvent, useCallback, useEffect, useRef } from 'react';
|
import {
|
||||||
|
type DragEvent,
|
||||||
|
type MouseEvent as ReactMouseEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import {
|
import {
|
||||||
addEdge,
|
addEdge,
|
||||||
applyEdgeChanges,
|
applyEdgeChanges,
|
||||||
@@ -23,6 +30,7 @@ import '@xyflow/react/dist/style.css';
|
|||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import type { WAFRuleEdge, WAFRuleGraph } from '@/lib/services/openflare';
|
import type { WAFRuleEdge, WAFRuleGraph } from '@/lib/services/openflare';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
acceptedNodeChanges,
|
acceptedNodeChanges,
|
||||||
@@ -82,6 +90,11 @@ export function RuleFlowCanvas({
|
|||||||
const [edges, setEdges, applyFlowEdgeChanges] = useEdgesState<FlowEdge>(
|
const [edges, setEdges, applyFlowEdgeChanges] = useEdgesState<FlowEdge>(
|
||||||
buildFlowEdges(graph, selectedId, selectedEdgeId),
|
buildFlowEdges(graph, selectedId, selectedEdgeId),
|
||||||
);
|
);
|
||||||
|
const [contextMenu, setContextMenu] = useState<
|
||||||
|
| { kind: 'node'; id: string; x: number; y: number }
|
||||||
|
| { kind: 'edge'; id: string; x: number; y: number }
|
||||||
|
| null
|
||||||
|
>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNodes((current) => buildFlowNodes(graph, issues, selectedId, current));
|
setNodes((current) => buildFlowNodes(graph, issues, selectedId, current));
|
||||||
@@ -254,40 +267,114 @@ export function RuleFlowCanvas({
|
|||||||
? '系统节点不可删除'
|
? '系统节点不可删除'
|
||||||
: '请选择可删除项';
|
: '请选择可删除项';
|
||||||
|
|
||||||
const deleteSelection = useCallback(() => {
|
const deleteNodeById = useCallback(
|
||||||
if (canDeleteEdge && selectedEdgeId) {
|
(nodeId: string) => {
|
||||||
setEdges((current) =>
|
const target = graph.nodes.find((node) => node.id === nodeId);
|
||||||
current.filter((edge) => edge.id !== selectedEdgeId),
|
if (!target || ['start', 'allow'].includes(target.type)) return;
|
||||||
);
|
setNodes((current) => current.filter((node) => node.id !== nodeId));
|
||||||
onGraphChange(removeEdgeFromGraph(graph, selectedEdgeId));
|
|
||||||
onSelectEdge(undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (canDeleteNode && selectedId) {
|
|
||||||
setNodes((current) => current.filter((node) => node.id !== selectedId));
|
|
||||||
setEdges((current) =>
|
setEdges((current) =>
|
||||||
current.filter(
|
current.filter(
|
||||||
(edge) => edge.source !== selectedId && edge.target !== selectedId,
|
(edge) => edge.source !== nodeId && edge.target !== nodeId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
onGraphChange(removeNodeFromGraph(graph, selectedId));
|
onGraphChange(removeNodeFromGraph(graph, nodeId));
|
||||||
onSelect(undefined);
|
if (selectedId === nodeId) onSelect(undefined);
|
||||||
|
if (selectedEdgeId) onSelectEdge(undefined);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
graph,
|
||||||
|
onGraphChange,
|
||||||
|
onSelect,
|
||||||
|
onSelectEdge,
|
||||||
|
selectedEdgeId,
|
||||||
|
selectedId,
|
||||||
|
setEdges,
|
||||||
|
setNodes,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteEdgeById = useCallback(
|
||||||
|
(edgeId: string) => {
|
||||||
|
if (!graph.edges.some((edge) => edge.id === edgeId)) return;
|
||||||
|
setEdges((current) => current.filter((edge) => edge.id !== edgeId));
|
||||||
|
onGraphChange(removeEdgeFromGraph(graph, edgeId));
|
||||||
|
if (selectedEdgeId === edgeId) onSelectEdge(undefined);
|
||||||
|
},
|
||||||
|
[graph, onGraphChange, onSelectEdge, selectedEdgeId, setEdges],
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteSelection = useCallback(() => {
|
||||||
|
if (canDeleteEdge && selectedEdgeId) {
|
||||||
|
deleteEdgeById(selectedEdgeId);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (canDeleteNode && selectedId) deleteNodeById(selectedId);
|
||||||
}, [
|
}, [
|
||||||
canDeleteEdge,
|
canDeleteEdge,
|
||||||
canDeleteNode,
|
canDeleteNode,
|
||||||
graph,
|
deleteEdgeById,
|
||||||
onGraphChange,
|
deleteNodeById,
|
||||||
onSelect,
|
|
||||||
onSelectEdge,
|
|
||||||
selectedEdgeId,
|
selectedEdgeId,
|
||||||
selectedId,
|
selectedId,
|
||||||
setEdges,
|
|
||||||
setNodes,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const openNodeContextMenu = useCallback(
|
||||||
|
(event: ReactMouseEvent, node: FlowNode) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onSelectEdge(undefined);
|
||||||
|
onSelect(node.id);
|
||||||
|
setContextMenu({
|
||||||
|
kind: 'node',
|
||||||
|
id: node.id,
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onSelect, onSelectEdge],
|
||||||
|
);
|
||||||
|
|
||||||
|
const openEdgeContextMenu = useCallback(
|
||||||
|
(event: ReactMouseEvent, edge: FlowEdge) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onSelect(undefined);
|
||||||
|
onSelectEdge(edge.id);
|
||||||
|
setContextMenu({
|
||||||
|
kind: 'edge',
|
||||||
|
id: edge.id,
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onSelect, onSelectEdge],
|
||||||
|
);
|
||||||
|
|
||||||
|
const suppressBrowserContextMenu = useCallback(
|
||||||
|
(event: ReactMouseEvent | MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setContextMenu(null);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const contextNode =
|
||||||
|
contextMenu?.kind === 'node'
|
||||||
|
? graph.nodes.find((node) => node.id === contextMenu.id)
|
||||||
|
: undefined;
|
||||||
|
const contextCanDeleteNode = Boolean(
|
||||||
|
contextNode && !['start', 'allow'].includes(contextNode.type),
|
||||||
|
);
|
||||||
|
const contextCanDeleteEdge = Boolean(
|
||||||
|
contextMenu?.kind === 'edge' &&
|
||||||
|
graph.edges.some((edge) => edge.id === contextMenu.id),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='relative min-w-0 flex-1 bg-muted/20'>
|
<section
|
||||||
|
className='relative min-w-0 flex-1 bg-muted/20'
|
||||||
|
onContextMenu={suppressBrowserContextMenu}
|
||||||
|
>
|
||||||
<div className='absolute left-4 top-4 z-10 rounded-lg border bg-background/95 p-2 shadow-sm backdrop-blur'>
|
<div className='absolute left-4 top-4 z-10 rounded-lg border bg-background/95 p-2 shadow-sm backdrop-blur'>
|
||||||
<NodeLibrary />
|
<NodeLibrary />
|
||||||
</div>
|
</div>
|
||||||
@@ -321,25 +408,81 @@ export function RuleFlowCanvas({
|
|||||||
onNodeClick={(_, node) => {
|
onNodeClick={(_, node) => {
|
||||||
onSelectEdge(undefined);
|
onSelectEdge(undefined);
|
||||||
onSelect(node.id);
|
onSelect(node.id);
|
||||||
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
onEdgeClick={(_, edge) => {
|
onEdgeClick={(_, edge) => {
|
||||||
onSelect(undefined);
|
onSelect(undefined);
|
||||||
onSelectEdge(edge.id);
|
onSelectEdge(edge.id);
|
||||||
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
onPaneClick={() => {
|
onPaneClick={() => {
|
||||||
onSelect(undefined);
|
onSelect(undefined);
|
||||||
onSelectEdge(undefined);
|
onSelectEdge(undefined);
|
||||||
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
|
onMoveStart={() => setContextMenu(null)}
|
||||||
|
onNodeContextMenu={openNodeContextMenu}
|
||||||
|
onEdgeContextMenu={openEdgeContextMenu}
|
||||||
|
onPaneContextMenu={suppressBrowserContextMenu}
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
fitView
|
fitView
|
||||||
fitViewOptions={initialFitViewOptions}
|
fitViewOptions={initialFitViewOptions}
|
||||||
deleteKeyCode={['Backspace', 'Delete']}
|
deleteKeyCode={['Backspace', 'Delete']}
|
||||||
|
className='h-full w-full'
|
||||||
>
|
>
|
||||||
<Background gap={20} size={1} />
|
<Background gap={20} size={1} />
|
||||||
<MiniMap pannable zoomable />
|
<MiniMap pannable zoomable />
|
||||||
<Controls />
|
<Controls />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
{contextMenu && (
|
||||||
|
<div
|
||||||
|
className='fixed z-50 min-w-40 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md'
|
||||||
|
style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||||
|
role='menu'
|
||||||
|
>
|
||||||
|
{contextMenu.kind === 'node' && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
role='menuitem'
|
||||||
|
disabled={!contextCanDeleteNode}
|
||||||
|
className={cn(
|
||||||
|
'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none',
|
||||||
|
contextCanDeleteNode
|
||||||
|
? 'text-destructive hover:bg-destructive/10'
|
||||||
|
: 'cursor-not-allowed text-muted-foreground opacity-60',
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
deleteNodeById(contextMenu.id);
|
||||||
|
setContextMenu(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className='size-4' />
|
||||||
|
{contextCanDeleteNode ? '删除节点' : '系统节点不可删除'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{contextMenu.kind === 'edge' && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
role='menuitem'
|
||||||
|
disabled={!contextCanDeleteEdge}
|
||||||
|
className={cn(
|
||||||
|
'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none',
|
||||||
|
contextCanDeleteEdge
|
||||||
|
? 'text-destructive hover:bg-destructive/10'
|
||||||
|
: 'cursor-not-allowed text-muted-foreground opacity-60',
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
deleteEdgeById(contextMenu.id);
|
||||||
|
setContextMenu(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className='size-4' />
|
||||||
|
删除连线
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
||||||
|
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} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Trigger data-slot='context-menu-trigger' {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Group data-slot='context-menu-group' {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Portal data-slot='context-menu-portal' {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||||
|
return <ContextMenuPrimitive.Sub data-slot='context-menu-sub' {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.RadioGroup
|
||||||
|
data-slot='context-menu-radio-group'
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.SubTrigger
|
||||||
|
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",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className='ml-auto' />
|
||||||
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<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',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Portal>
|
||||||
|
<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',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</ContextMenuPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = 'default',
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
variant?: 'default' | 'destructive';
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Item
|
||||||
|
data-slot='context-menu-item'
|
||||||
|
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!",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<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",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className='size-4' />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<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",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className='size-2 fill-current' />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.RadioItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<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,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Separator
|
||||||
|
data-slot='context-menu-separator'
|
||||||
|
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuCheckboxItem,
|
||||||
|
ContextMenuRadioItem,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuShortcut,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuPortal,
|
||||||
|
ContextMenuSub,
|
||||||
|
ContextMenuSubContent,
|
||||||
|
ContextMenuSubTrigger,
|
||||||
|
ContextMenuRadioGroup,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user