mirror of https://github.com/buster-so/buster.git
make a context menu
This commit is contained in:
parent
43d4ded84e
commit
4dcbda8c13
|
@ -12,7 +12,7 @@
|
|||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@faker-js/faker": "^9.5.0",
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@fluentui/react-context-selector": "^9.1.72",
|
||||
"@manufac/echarts-simple-transform": "^2.0.11",
|
||||
"@million/lint": "^1.0.14",
|
||||
|
@ -20,6 +20,7 @@
|
|||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
|
@ -32,8 +33,8 @@
|
|||
"@supabase/ssr": "^0.5.2",
|
||||
"@supabase/supabase-js": "^2.49.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tanstack/react-query": "^5.66.9",
|
||||
"@tanstack/react-query-devtools": "^5.66.9",
|
||||
"@tanstack/react-query": "^5.66.11",
|
||||
"@tanstack/react-query-devtools": "^5.66.11",
|
||||
"@vercel/speed-insights": "^1.2.0",
|
||||
"ahooks": "^3.8.4",
|
||||
"antd": "5.23.3",
|
||||
|
@ -93,7 +94,7 @@
|
|||
"react-markdown": "^9.0.3",
|
||||
"react-material-symbols": "^4.4.0",
|
||||
"react-monaco-editor": "^0.58.0",
|
||||
"react-scan": "^0.2.3",
|
||||
"react-scan": "^0.2.8",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.25",
|
||||
"rehype-raw": "^7.0.0",
|
||||
|
@ -133,7 +134,7 @@
|
|||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"chromatic": "^11.25.2",
|
||||
"chromatic": "^11.26.1",
|
||||
"cypress": "^13.17.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
|
@ -145,7 +146,7 @@
|
|||
"monaco-editor-webpack-plugin": "^7.1.0",
|
||||
"postcss": "8.5.3",
|
||||
"sass": "^1.85.1",
|
||||
"storybook": "^8.6.0",
|
||||
"storybook": "^8.6.2",
|
||||
"tailwind-scrollbar": "^4.0.1",
|
||||
"tailwindcss": "^4.0.9",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
@ -2427,6 +2428,7 @@
|
|||
},
|
||||
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"extraneous": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -3207,9 +3209,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@faker-js/faker": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.5.0.tgz",
|
||||
"integrity": "sha512-3qbjLv+fzuuCg3umxc9/7YjrEXNaKwHgmig949nfyaTx8eL4FAsvFbu+1JcFUj1YAXofhaDn6JdEUBTYuk0Ssw==",
|
||||
"version": "9.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.5.1.tgz",
|
||||
"integrity": "sha512-0fzMEDxkExR2cn731kpDaCCnBGBUOIXEi2S1N5l8Hltp6aPf4soTMJ+g4k8r2sI5oB+rpwIW8Uy/6jkwGpnWPg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
@ -5770,6 +5772,34 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context-menu": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.6.tgz",
|
||||
"integrity": "sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-menu": "2.1.6",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||
|
@ -7916,9 +7946,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.66.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.4.tgz",
|
||||
"integrity": "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA==",
|
||||
"version": "5.66.11",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.11.tgz",
|
||||
"integrity": "sha512-ZEYxgHUcohj3sHkbRaw0gYwFxjY5O6M3IXOYXEun7E1rqNhsP8fOtqjJTKPZpVHcdIdrmX4lzZctT4+pts0OgA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
@ -7936,12 +7966,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.66.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.9.tgz",
|
||||
"integrity": "sha512-NRI02PHJsP5y2gAuWKP+awamTIBFBSKMnO6UVzi03GTclmHHHInH5UzVgzi5tpu4+FmGfsdT7Umqegobtsp23A==",
|
||||
"version": "5.66.11",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.11.tgz",
|
||||
"integrity": "sha512-uPDiQbZScWkAeihmZ9gAm3wOBA1TmLB1KCB1fJ1hIiEKq3dTT+ja/aYM7wGUD+XiEsY4sDSE7p8VIz/21L2Dow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.66.4"
|
||||
"@tanstack/query-core": "5.66.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
@ -7952,9 +7982,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query-devtools": {
|
||||
"version": "5.66.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.66.9.tgz",
|
||||
"integrity": "sha512-70G6AR35he53SYUcUK6EdqNR18zejCv1rM6900gjZP408EAex56YLwVSeijzk9lWeU2J42G9Fjh0i1WngUTsgw==",
|
||||
"version": "5.66.11",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.66.11.tgz",
|
||||
"integrity": "sha512-a+zr2TN4dKpxVlJ9YBOC5YmpGWp2Ez2ZfIzsorVbrs/u2R+bVkLrU1u5e8WHzLdf6tXYueATqgeXWLHrvi4Dig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-devtools": "5.65.0"
|
||||
|
@ -7964,7 +7994,7 @@
|
|||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-query": "^5.66.9",
|
||||
"@tanstack/react-query": "^5.66.11",
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
|
@ -11420,9 +11450,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/chromatic": {
|
||||
"version": "11.25.2",
|
||||
"resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.25.2.tgz",
|
||||
"integrity": "sha512-/9eQWn6BU1iFsop86t8Au21IksTRxwXAl7if8YHD05L2AbuMjClLWZo5cZojqrJHGKDhTqfrC2X2xE4uSm0iKw==",
|
||||
"version": "11.26.1",
|
||||
"resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.26.1.tgz",
|
||||
"integrity": "sha512-kVMTigrKI7TOOV04i1lTTIVJsmQ+fj6ZFXyZ3LcdCioOrxO/zCVB1y74iX0iKS++cpi3bJcG+UszkmvptGDEuA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
@ -22523,9 +22553,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-scan": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-scan/-/react-scan-0.2.3.tgz",
|
||||
"integrity": "sha512-hLUj/yIMCUI/v6Mcym5qABFM1wC4R9TEVzDc6qWWckmEQnMMMPcOApKaq95NpKY+qpP1HPZyb7R25uR/rCipEw==",
|
||||
"version": "0.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react-scan/-/react-scan-0.2.8.tgz",
|
||||
"integrity": "sha512-+6Gvu9b0UMmzV0JkigA7Y2YcjQABiNrweP9l9j8nrutN5OAYLRe4JgfwiUohPFngMD+Y6I5N0kW+okXhvVLGUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
|
|
|
@ -0,0 +1,219 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
|
||||
import { Check, ShapeCircle as Circle } from '@/components/ui/icons';
|
||||
import { CaretRight } from '@/components/ui/icons/NucleoIconFilled';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { DropdownMenuLink } from '../dropdown/DropdownBase';
|
||||
|
||||
/*
|
||||
The styling of this and the dropdown is the same. TODO: Refactor to share styles
|
||||
*/
|
||||
|
||||
const ContextMenu = ContextMenuPrimitive.Root;
|
||||
|
||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
||||
|
||||
const ContextMenuGroup = ContextMenuPrimitive.Group;
|
||||
|
||||
const ContextMenuPortal = ContextMenuPrimitive.Portal;
|
||||
|
||||
const ContextMenuSub = ContextMenuPrimitive.Sub;
|
||||
|
||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||
|
||||
const ContextMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-item-hover data-[state=open]:bg-item-hover flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<div className="text-icon-color text-3xs ml-auto">
|
||||
<CaretRight />
|
||||
</div>
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
));
|
||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const baseContentClass = cn(
|
||||
`data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 min-w-[8rem] overflow-hidden `,
|
||||
'bg-background text-foreground ',
|
||||
'rounded-md border p-1'
|
||||
);
|
||||
|
||||
const ContextMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(baseContentClass, 'shadow-lg', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const ContextMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(baseContentClass, 'shadow', className)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
));
|
||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
|
||||
|
||||
const ContextMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
truncate?: boolean;
|
||||
}
|
||||
>(({ className, inset, truncate, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-item-hover focus:text-foreground relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-60 [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
truncate && 'overflow-hidden',
|
||||
'group',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
|
||||
|
||||
const itemClass = cn(
|
||||
'focus:bg-item-hover focus:text-foreground',
|
||||
'relative flex cursor-pointer items-center rounded-sm py-1.5 text-sm transition-colors outline-none select-none',
|
||||
'data-[disabled]:pointer-events-none data-[disabled]:opacity-60',
|
||||
'gap-1.5'
|
||||
);
|
||||
|
||||
const ContextMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> & {
|
||||
truncate?: boolean;
|
||||
}
|
||||
>(({ className, children, checked, truncate, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
itemClass,
|
||||
'data-[state=checked]:bg-item-hover',
|
||||
'pr-6 pl-2',
|
||||
truncate && 'overflow-hidden',
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<div className="flex h-4 w-4 items-center justify-center">
|
||||
<Check />
|
||||
</div>
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const ContextMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<div className="fill-content h-2 w-2">
|
||||
<Circle />
|
||||
</div>
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
));
|
||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const ContextMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('text-gray-dark px-2 py-1.5 text-sm', inset && 'pl-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
|
||||
|
||||
const ContextMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('bg-border -mx-1 my-1 h-[0.5px]', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
|
||||
|
||||
const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span className={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...props} />
|
||||
);
|
||||
};
|
||||
ContextMenuShortcut.displayName = 'ContextMenuShortcut';
|
||||
|
||||
const ContextMenuLink: React.FC<{
|
||||
className?: string;
|
||||
link: string;
|
||||
linkIcon?: 'arrow-right' | 'arrow-external' | 'caret-right';
|
||||
}> = ({ className, link, linkIcon = 'arrow-external' }) => {
|
||||
return <DropdownMenuLink className={className} link={link} linkIcon={linkIcon} />;
|
||||
};
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
ContextMenuLink
|
||||
};
|
|
@ -0,0 +1,341 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ContextMenu, ContextProps } from './ContextMenu';
|
||||
import {
|
||||
Window,
|
||||
WindowSettings,
|
||||
WindowUser,
|
||||
WindowEdit,
|
||||
File,
|
||||
WindowDownload,
|
||||
CircleCopy
|
||||
} from '../icons/NucleoIconOutlined';
|
||||
import React from 'react';
|
||||
|
||||
const meta: Meta<typeof ContextMenu> = {
|
||||
title: 'UI/Context/ContextMenu',
|
||||
component: ContextMenu,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
argTypes: {
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
defaultValue: false
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ContextMenu>;
|
||||
|
||||
// Basic example with simple items
|
||||
export const Basic: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: 'Edit',
|
||||
onClick: () => alert('Edit clicked'),
|
||||
icon: <WindowEdit />
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
onClick: () => alert('Settings clicked'),
|
||||
icon: <WindowSettings />
|
||||
},
|
||||
{
|
||||
label: 'Logout',
|
||||
onClick: () => alert('Logout clicked'),
|
||||
icon: <Window />
|
||||
}
|
||||
]
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="flex h-[200px] w-[200px] items-center justify-center rounded-md border border-dashed">
|
||||
<ContextMenu items={args.items} disabled={args.disabled}>
|
||||
<div className="h-full w-full bg-gray-200 p-4 text-center">
|
||||
Right-click here to open context menu
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
// Example with dividers and shortcuts
|
||||
export const WithDividersAndShortcuts: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: 'Profile',
|
||||
onClick: () => alert('Profile clicked'),
|
||||
icon: <WindowUser />,
|
||||
shortcut: '⌘P'
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
onClick: () => alert('Settings clicked'),
|
||||
icon: <WindowSettings />,
|
||||
shortcut: '⌘S'
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: 'Logout',
|
||||
onClick: () => alert('Logout clicked'),
|
||||
icon: <Window />,
|
||||
shortcut: '⌘L'
|
||||
}
|
||||
]
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="flex h-[200px] w-[200px] items-center justify-center rounded-md border border-dashed">
|
||||
<ContextMenu items={args.items} disabled={args.disabled}>
|
||||
<div className="h-full w-full bg-gray-200 p-4 text-center">
|
||||
Right-click here to open context menu
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
// Example with nested items
|
||||
export const WithNestedItems: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: 'File',
|
||||
icon: <File />,
|
||||
items: [
|
||||
{
|
||||
label: 'New',
|
||||
onClick: () => alert('New file clicked')
|
||||
},
|
||||
{
|
||||
label: 'Open',
|
||||
onClick: () => alert('Open file clicked')
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
onClick: () => alert('Save file clicked'),
|
||||
shortcut: '⌘S'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: <WindowEdit />,
|
||||
items: [
|
||||
{
|
||||
label: 'Copy',
|
||||
onClick: () => alert('Copy clicked'),
|
||||
icon: <CircleCopy />,
|
||||
shortcut: '⌘C'
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: () => alert('Delete clicked'),
|
||||
icon: <Window />,
|
||||
shortcut: '⌫'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="flex h-[200px] w-[200px] items-center justify-center rounded-md border border-dashed">
|
||||
<ContextMenu items={args.items} disabled={args.disabled}>
|
||||
<div className="h-full w-full bg-gray-200 p-4 text-center">
|
||||
Right-click here to open context menu
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
// Example with disabled items
|
||||
export const WithDisabledItems: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: 'Edit',
|
||||
onClick: () => alert('Edit clicked'),
|
||||
icon: <WindowEdit />
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: () => alert('Delete clicked'),
|
||||
icon: <Window />,
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'Download',
|
||||
onClick: () => alert('Download clicked'),
|
||||
icon: <WindowDownload />
|
||||
}
|
||||
]
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="flex h-[200px] w-[200px] items-center justify-center rounded-md border border-dashed">
|
||||
<ContextMenu items={args.items} disabled={args.disabled}>
|
||||
<div className="h-full w-full bg-gray-200 p-4 text-center">
|
||||
Right-click here to open context menu
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
// Example with loading state
|
||||
export const WithLoadingItems: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: 'Normal Item',
|
||||
onClick: () => alert('Normal clicked')
|
||||
},
|
||||
{
|
||||
label: 'Loading Item',
|
||||
loading: true,
|
||||
onClick: () => alert('Loading clicked')
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: 'Another Item',
|
||||
onClick: () => alert('Another clicked')
|
||||
}
|
||||
]
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="flex h-[200px] w-[200px] items-center justify-center rounded-md border border-dashed">
|
||||
<ContextMenu items={args.items} disabled={args.disabled}>
|
||||
<div className="h-full w-full bg-gray-200 p-4 text-center">
|
||||
Right-click here to open context menu
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
// Example with selection
|
||||
export const WithSelection: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: 'Option 1',
|
||||
onClick: () => alert('Option 1 clicked'),
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
label: 'Option 2',
|
||||
onClick: () => alert('Option 2 clicked'),
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
label: 'Option 3',
|
||||
onClick: () => alert('Option 3 clicked'),
|
||||
selected: false
|
||||
}
|
||||
]
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="flex h-[200px] w-[200px] items-center justify-center rounded-md border border-dashed">
|
||||
<ContextMenu items={args.items} disabled={args.disabled}>
|
||||
<div className="h-full w-full bg-gray-200 p-4 text-center">
|
||||
Right-click here to open context menu
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
// Example with secondary labels and truncation
|
||||
export const WithSecondaryLabels: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: 'Document 1',
|
||||
secondaryLabel: 'Last edited 2 days ago',
|
||||
onClick: () => alert('Document 1 clicked'),
|
||||
icon: <File />
|
||||
},
|
||||
{
|
||||
label: 'Document with a very long name that should be truncated',
|
||||
secondaryLabel: 'Last edited yesterday',
|
||||
truncate: true,
|
||||
onClick: () => alert('Document 2 clicked'),
|
||||
icon: <File />
|
||||
},
|
||||
{
|
||||
label: 'Document 3',
|
||||
secondaryLabel: 'Last edited just now',
|
||||
onClick: () => alert('Document 3 clicked'),
|
||||
icon: <File />
|
||||
}
|
||||
]
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="flex h-[200px] w-[200px] items-center justify-center rounded-md border border-dashed">
|
||||
<ContextMenu items={args.items} disabled={args.disabled}>
|
||||
<div className="h-full w-full bg-gray-200 p-4 text-center">
|
||||
Right-click here to open context menu
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
// Example with links
|
||||
export const WithLinks: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: 'Documentation',
|
||||
link: 'https://example.com/docs',
|
||||
linkIcon: 'arrow-external'
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
link: '/settings',
|
||||
linkIcon: 'arrow-right'
|
||||
},
|
||||
{
|
||||
label: 'Profile',
|
||||
link: '/profile',
|
||||
linkIcon: 'caret-right'
|
||||
}
|
||||
]
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="flex h-[200px] w-[200px] items-center justify-center rounded-md border border-dashed">
|
||||
<ContextMenu items={args.items} disabled={args.disabled} className="">
|
||||
<div className="h-full w-full bg-gray-200 p-4 text-center">
|
||||
Right-click here to open context menu
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
// Example with custom width
|
||||
export const CustomWidth: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: 'This is a menu item with a very long label that might need to be constrained',
|
||||
onClick: () => alert('Long item clicked')
|
||||
},
|
||||
{
|
||||
label: 'Short item',
|
||||
onClick: () => alert('Short item clicked')
|
||||
}
|
||||
]
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="flex h-[200px] w-[200px] items-center justify-center rounded-md border border-dashed">
|
||||
<ContextMenu items={args.items} disabled={args.disabled} className="min-w-[400px]">
|
||||
<div className="h-full w-full bg-gray-200 p-4 text-center">
|
||||
Right-click here to open context menu
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
)
|
||||
};
|
|
@ -0,0 +1,176 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
ContextMenu as ContextMenuPrimitive,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem as ContextMenuItemPrimitive,
|
||||
ContextMenuLabel,
|
||||
ContextMenuRadioGroup,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuLink,
|
||||
ContextMenuPortal
|
||||
} from './ContextBase';
|
||||
import { ContextMenuProps } from '@radix-ui/react-context-menu';
|
||||
import CircleSpinnerLoader from '../loaders/CircleSpinnerLoader';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
|
||||
export interface ContextMenuItem {
|
||||
label: React.ReactNode | string;
|
||||
truncate?: boolean;
|
||||
secondaryLabel?: string;
|
||||
showIndex?: boolean;
|
||||
shortcut?: string;
|
||||
onClick?: () => void;
|
||||
icon?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
selected?: boolean; //if a boolean is provided, it will render a checkboxitem component
|
||||
items?: ContextMenuItem[];
|
||||
link?: string;
|
||||
linkIcon?: 'arrow-right' | 'arrow-external' | 'caret-right';
|
||||
}
|
||||
|
||||
export interface ContextMenuDivider {
|
||||
type: 'divider';
|
||||
}
|
||||
|
||||
export type ContextMenuItems = (ContextMenuItem | ContextMenuDivider | React.ReactNode)[];
|
||||
|
||||
export interface ContextProps extends ContextMenuProps {
|
||||
items: ContextMenuItems;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const contextMenuItemKey = (item: ContextMenuItems[number], index: number) => {
|
||||
if ((item as ContextMenuDivider).type === 'divider') return `divider-${index}`;
|
||||
return `item-${index}`;
|
||||
};
|
||||
|
||||
export const ContextMenu: React.FC<ContextProps> = React.memo(
|
||||
({ items, className, disabled, children, dir, modal }) => {
|
||||
return (
|
||||
<ContextMenuPrimitive dir={dir} modal={modal}>
|
||||
<ContextMenuTrigger disabled={disabled} asChild>
|
||||
{children}
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent className={cn('max-w-72 min-w-44', className)}>
|
||||
{items.map((item, index) => (
|
||||
<ContextMenuItemSelector
|
||||
key={contextMenuItemKey(item, index)}
|
||||
item={item}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</ContextMenuContent>
|
||||
</ContextMenuPrimitive>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const ContextMenuItemSelector: React.FC<{
|
||||
item: ContextMenuItems[number];
|
||||
index: number;
|
||||
}> = React.memo(({ item, index }) => {
|
||||
if ((item as ContextMenuDivider).type === 'divider') {
|
||||
return <ContextMenuSeparator key={contextMenuItemKey(item, index)} />;
|
||||
}
|
||||
|
||||
if (typeof item === 'object' && React.isValidElement(item)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return <ContextMenuItemComponent {...(item as ContextMenuItem)} index={index} />;
|
||||
});
|
||||
|
||||
ContextMenuItemSelector.displayName = 'ContextMenuItemSelector';
|
||||
|
||||
const ContextMenuItemComponent: React.FC<ContextMenuItem & { index: number }> = ({
|
||||
label,
|
||||
showIndex,
|
||||
shortcut,
|
||||
onClick,
|
||||
items,
|
||||
icon,
|
||||
disabled,
|
||||
loading,
|
||||
selected,
|
||||
secondaryLabel,
|
||||
truncate,
|
||||
link,
|
||||
linkIcon,
|
||||
index
|
||||
}) => {
|
||||
const isSubItem = items && items.length > 0;
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{showIndex && <span className="text-gray-light">{index}</span>}
|
||||
{icon && !loading && <span className="text-icon-color">{icon}</span>}
|
||||
{icon && loading && <CircleSpinnerLoader size={9} />}
|
||||
<div className={cn('flex flex-col gap-y-1', truncate && 'overflow-hidden')}>
|
||||
<span className={cn(truncate && 'truncate', 'flex items-center gap-x-1')}>
|
||||
{label}
|
||||
{!icon && loading && <CircleSpinnerLoader size={9} />}
|
||||
</span>
|
||||
{secondaryLabel && <span className="text-gray-light text2xs">{secondaryLabel}</span>}
|
||||
</div>
|
||||
{shortcut && <ContextMenuShortcut>{shortcut}</ContextMenuShortcut>}
|
||||
{link && (
|
||||
<ContextMenuLink className="-mr-1 ml-auto opacity-100" link={link} linkIcon={linkIcon} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (isSubItem) {
|
||||
return <ContextSubMenuWrapper items={items}>{content}</ContextSubMenuWrapper>;
|
||||
}
|
||||
|
||||
const isSelectable = typeof selected === 'boolean';
|
||||
|
||||
if (isSelectable) {
|
||||
return (
|
||||
<ContextMenuCheckboxItem
|
||||
truncate={truncate}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
checked={selected}>
|
||||
{content}
|
||||
</ContextMenuCheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenuItemPrimitive truncate={truncate} disabled={disabled} onClick={onClick}>
|
||||
{content}
|
||||
</ContextMenuItemPrimitive>
|
||||
);
|
||||
};
|
||||
|
||||
const ContextSubMenuWrapper = React.memo(
|
||||
({ items, children }: { items: ContextMenuItems | undefined; children: React.ReactNode }) => {
|
||||
return (
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger>{children}</ContextMenuSubTrigger>
|
||||
<ContextMenuPortal>
|
||||
<ContextMenuSubContent>
|
||||
{items?.map((item, index) => (
|
||||
<ContextMenuItemSelector
|
||||
key={contextMenuItemKey(item, index)}
|
||||
item={item}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuPortal>
|
||||
</ContextMenuSub>
|
||||
);
|
||||
}
|
||||
);
|
||||
ContextSubMenuWrapper.displayName = 'ContextSubMenuWrapper';
|
|
@ -51,8 +51,6 @@ export interface DropdownProps extends DropdownMenuProps {
|
|||
items: DropdownItems;
|
||||
selectType?: 'single' | 'multiple' | 'none';
|
||||
menuHeader?: string | React.ReactNode; //if string it will render a search box
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
closeOnSelect?: boolean;
|
||||
onSelect?: (itemId: string) => void;
|
||||
align?: 'start' | 'center' | 'end';
|
||||
|
@ -73,8 +71,6 @@ export const Dropdown: React.FC<DropdownProps> = React.memo(
|
|||
items = [],
|
||||
selectType = 'none',
|
||||
menuHeader,
|
||||
minWidth = 240,
|
||||
maxWidth,
|
||||
closeOnSelect = true,
|
||||
onSelect,
|
||||
children,
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ConfigProvider, Menu, MenuProps } from 'antd';
|
|||
import { createStyles } from 'antd-style';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import React, { forwardRef, useMemo } from 'react';
|
||||
import { BusterListContextMenu, BusterMenuItemType } from './interfaces';
|
||||
import { BusterListContextMenu } from './interfaces';
|
||||
import { MenuItemType } from 'antd/es/menu/interface';
|
||||
|
||||
export interface BusterListContentMenuProps {
|
||||
|
@ -23,7 +23,7 @@ export const BusterListContentMenu = forwardRef<HTMLDivElement, BusterListConten
|
|||
...(item as MenuItemType),
|
||||
popupClassName: cx(styles.popUpClassMenu),
|
||||
onClick: () => {
|
||||
(item as BusterMenuItemType)?.onClick?.(id);
|
||||
item?.onClick?.(id);
|
||||
}
|
||||
};
|
||||
}) || [],
|
||||
|
@ -67,7 +67,7 @@ export const BusterListContentMenu = forwardRef<HTMLDivElement, BusterListConten
|
|||
);
|
||||
BusterListContentMenu.displayName = 'BusterListContentMenu';
|
||||
|
||||
const useStyles = createStyles(({ token, prefixCls, css }) => ({
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
popUpClassMenu: css`
|
||||
.busterv2-menu {
|
||||
border: 0.5px solid ${token.colorBorder};
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { useMemoizedFn } from 'ahooks';
|
||||
import React from 'react';
|
||||
import { MemoizedCheckbox } from './MemoizedCheckbox';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { WIDTH_OF_CHECKBOX_COLUMN } from './config';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
|
||||
export const CheckboxColumn: React.FC<{
|
||||
checkStatus: 'checked' | 'unchecked' | 'indeterminate' | undefined;
|
||||
onChange: (v: boolean) => void;
|
||||
className?: string;
|
||||
}> = React.memo(({ checkStatus, onChange, className = '' }) => {
|
||||
const { styles, cx } = useStyles();
|
||||
const showBox = checkStatus === 'checked'; //|| checkStatus === 'indeterminate';
|
||||
|
||||
const onClickStopPropagation = useMemoizedFn((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
|
@ -19,11 +18,13 @@ export const CheckboxColumn: React.FC<{
|
|||
return (
|
||||
<div
|
||||
onClick={onClickStopPropagation}
|
||||
className={cx(
|
||||
style={{
|
||||
width: `${WIDTH_OF_CHECKBOX_COLUMN}px`,
|
||||
minWidth: `${WIDTH_OF_CHECKBOX_COLUMN}px`
|
||||
}}
|
||||
className={cn(
|
||||
className,
|
||||
styles.checkboxColumn,
|
||||
'flex items-center justify-center opacity-0',
|
||||
'group-hover:opacity-100',
|
||||
'flex items-center justify-center pr-0 pl-1 opacity-0 group-hover:opacity-100',
|
||||
showBox ? 'opacity-100' : ''
|
||||
)}>
|
||||
<MemoizedCheckbox
|
||||
|
@ -35,15 +36,3 @@ export const CheckboxColumn: React.FC<{
|
|||
);
|
||||
});
|
||||
CheckboxColumn.displayName = 'CheckboxColumn';
|
||||
|
||||
export const useStyles = createStyles(({ css, token }) => ({
|
||||
checkboxColumn: css`
|
||||
padding-left: 4px;
|
||||
padding-right: 0px;
|
||||
width: ${WIDTH_OF_CHECKBOX_COLUMN}px;
|
||||
min-width: ${WIDTH_OF_CHECKBOX_COLUMN}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
}));
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import { MenuProps } from 'antd';
|
||||
import type { MenuDividerType } from 'antd/es/menu/interface';
|
||||
import type {
|
||||
MenuItemType as RcMenuItemType,
|
||||
SubMenuType as RcSubMenuType
|
||||
} from 'rc-menu/lib/interface';
|
||||
|
||||
import React from 'react';
|
||||
import { Dropdown } from '../../dropdown';
|
||||
export interface BusterListProps {
|
||||
columns: BusterListColumn[];
|
||||
hideLastRowBorder?: boolean;
|
||||
|
@ -49,25 +43,6 @@ export interface BusterListSectionRow {
|
|||
}
|
||||
|
||||
//CONTEXT MENU INTERFACES
|
||||
export interface BusterListContextMenu extends Omit<MenuProps, 'items'> {
|
||||
items: BusterListMenuItemType[];
|
||||
export interface BusterListContextMenu {
|
||||
items: any[];
|
||||
}
|
||||
|
||||
export interface BusterMenuItemType extends Omit<RcMenuItemType, 'onSelect' | 'onClick'> {
|
||||
danger?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
title?: string;
|
||||
onClick?: (id: string) => void;
|
||||
key: string;
|
||||
}
|
||||
export interface SubMenuType<T extends BusterMenuItemType = BusterMenuItemType>
|
||||
extends Omit<RcSubMenuType, 'children' | 'onClick'> {
|
||||
icon?: React.ReactNode;
|
||||
theme?: 'dark' | 'light';
|
||||
children: BusterListMenuItemType<T>[];
|
||||
}
|
||||
export type BusterListMenuItemType<T extends BusterMenuItemType = BusterMenuItemType> =
|
||||
| T
|
||||
| SubMenuType<T>
|
||||
| MenuDividerType
|
||||
| null;
|
||||
|
|
Loading…
Reference in New Issue