mirror of https://github.com/buster-so/buster.git
custom dropdown component
This commit is contained in:
parent
e068c27b8c
commit
bbbbcaa1b8
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
description: Rules for the components ui directory
|
||||
globs: src/components/ui/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Component Directory Structure and Organization
|
||||
|
@ -65,6 +66,10 @@ globs: src/components/ui/**/*
|
|||
import { Title } from '@/components/ui';
|
||||
```
|
||||
|
||||
##
|
||||
I have a helper called `cn`. This is how I do a tailwind merge and classnames concatination. This is found in import { cn } from '@/lib/classMerge';
|
||||
|
||||
|
||||
## File Naming
|
||||
- Use PascalCase for component files
|
||||
- File name should match the component name
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"@manufac/echarts-simple-transform": "^2.0.11",
|
||||
"@million/lint": "^1.0.14",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
|
@ -2420,7 +2421,6 @@
|
|||
},
|
||||
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"extraneous": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -5622,6 +5622,36 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-checkbox": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.4.tgz",
|
||||
"integrity": "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-previous": "1.1.0",
|
||||
"@radix-ui/react-use-size": "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-collection": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz",
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"@manufac/echarts-simple-transform": "^2.0.11",
|
||||
"@million/lint": "^1.0.14",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Checkbox } from './Checkbox';
|
||||
|
||||
const meta: Meta<typeof Checkbox> = {
|
||||
title: 'Base/Checkbox',
|
||||
component: Checkbox,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['default'],
|
||||
description: 'The visual style of the checkbox'
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['default', 'sm', 'lg'],
|
||||
description: 'The size of the checkbox'
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Whether the checkbox is disabled'
|
||||
},
|
||||
checked: {
|
||||
control: 'select',
|
||||
options: [true, false, 'indeterminate'],
|
||||
description: 'Whether the checkbox is checked (controlled)'
|
||||
},
|
||||
defaultChecked: {
|
||||
control: 'boolean',
|
||||
description: 'The default checked state (uncontrolled)'
|
||||
},
|
||||
onCheckedChange: {
|
||||
description: 'Callback when the checked state changes'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Checkbox>;
|
||||
|
||||
// Default variant with all sizes
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
variant: 'default',
|
||||
size: 'sm'
|
||||
}
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
variant: 'default',
|
||||
size: 'lg'
|
||||
}
|
||||
};
|
||||
|
||||
// States
|
||||
export const Checked: Story = {
|
||||
args: {
|
||||
checked: true,
|
||||
size: 'default'
|
||||
}
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
size: 'default'
|
||||
}
|
||||
};
|
||||
|
||||
export const DisabledChecked: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
checked: true,
|
||||
size: 'default'
|
||||
}
|
||||
};
|
||||
|
||||
// Example with event handler
|
||||
export const WithOnChange: Story = {
|
||||
args: {
|
||||
onCheckedChange: (checked: boolean) => console.log('Checked:', checked)
|
||||
}
|
||||
};
|
||||
|
||||
// Example showing all sizes in a group
|
||||
export const AllSizes: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center gap-4">
|
||||
<Checkbox size="sm" />
|
||||
<Checkbox size="default" />
|
||||
<Checkbox size="lg" />
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
// Example showing different states
|
||||
export const AllStates: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Checkbox />
|
||||
<span className="text-sm">Default</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Checkbox checked />
|
||||
<span className="text-sm">Checked</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Checkbox disabled />
|
||||
<span className="text-sm">Disabled</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Checkbox disabled checked />
|
||||
<span className="text-sm">Disabled Checked</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
import * as React from 'react';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
import { Minus, Check } from '../icons';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
const checkboxVariants = cva(
|
||||
'peer relative h-4 w-4 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
' ring-offset-background focus-visible:ring-ring data-[state=unchecked]:border-gray-light data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:disabled:bg-primary data-[state=checked]:text-background'
|
||||
},
|
||||
size: {
|
||||
default: 'h-4 w-4 text-[10px]',
|
||||
sm: 'h-3 w-3 text-[8px]',
|
||||
lg: 'h-5 w-5 text-base'
|
||||
},
|
||||
disabled: {
|
||||
true: 'opacity-60 cursor-not-allowed ',
|
||||
false: 'cursor-pointer'
|
||||
},
|
||||
checked: {
|
||||
true: '',
|
||||
false: '',
|
||||
indeterminate: ''
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
disabled: false
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: 'default',
|
||||
checked: 'indeterminate',
|
||||
className:
|
||||
'bg-primary-light text-background border-primary-light hover:bg-primary! hover:border-primary! hover:disabled:bg-inherit'
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
type CheckboxVariants = VariantProps<typeof checkboxVariants>;
|
||||
|
||||
interface CheckboxProps
|
||||
extends Omit<
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
|
||||
keyof CheckboxVariants
|
||||
>,
|
||||
CheckboxVariants {
|
||||
indeterminate?: boolean;
|
||||
}
|
||||
|
||||
const Checkbox = React.forwardRef<React.ElementRef<typeof CheckboxPrimitive.Root>, CheckboxProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
checked,
|
||||
disabled = false,
|
||||
indeterminate,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
disabled={disabled || false}
|
||||
className={cn(checkboxVariants({ variant, size, disabled, checked }), className)}
|
||||
checked={checked || undefined}
|
||||
{...props}>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn('absolute inset-0 flex items-center justify-center')}>
|
||||
<div className="text-background flex">
|
||||
{checked === 'indeterminate' ? <Minus /> : <Check />}
|
||||
</div>
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
);
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox, type CheckboxProps };
|
|
@ -1,8 +1,9 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import { Dropdown, DropdownItems } from './Dropdown';
|
||||
import { Button } from '../buttons/Button';
|
||||
import { PaintRoller, Star, Storage } from '../icons';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import React from 'react';
|
||||
|
||||
const meta: Meta<typeof Dropdown> = {
|
||||
title: 'Base/Dropdown',
|
||||
|
@ -203,6 +204,17 @@ export const WithLoadingItems: Story = {
|
|||
id: '3',
|
||||
label: 'Another Normal',
|
||||
onClick: () => console.log('Another clicked')
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
id: '4',
|
||||
label: 'Option 4',
|
||||
onClick: () => console.log('Option 4 clicked')
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
label: 'Option 5',
|
||||
onClick: () => console.log('Option 5 clicked')
|
||||
}
|
||||
],
|
||||
children: <Button>Menu with Loading</Button>
|
||||
|
@ -210,9 +222,9 @@ export const WithLoadingItems: Story = {
|
|||
};
|
||||
|
||||
// Example with selection
|
||||
export const WithSelection: Story = {
|
||||
export const WithSelectionSingle: Story = {
|
||||
args: {
|
||||
selectType: true,
|
||||
selectType: 'single',
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
|
@ -236,6 +248,68 @@ export const WithSelection: Story = {
|
|||
}
|
||||
};
|
||||
|
||||
export const WithSelectionMultiple: Story = {
|
||||
render: () => {
|
||||
const [selectedIds, setSelectedIds] = React.useState<Set<string>>(new Set(['3']));
|
||||
|
||||
const items: DropdownItems = [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
selected: selectedIds.has('1'),
|
||||
onClick: () => console.log('Option 1 clicked')
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Option 2',
|
||||
selected: selectedIds.has('2'),
|
||||
onClick: () => console.log('Option 2 clicked')
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: 'Option 3',
|
||||
selected: selectedIds.has('3'),
|
||||
onClick: () => console.log('Option 3 clicked')
|
||||
},
|
||||
{ type: 'divider' as const },
|
||||
{
|
||||
id: '4',
|
||||
label: 'Option 4',
|
||||
selected: selectedIds.has('4'),
|
||||
onClick: () => console.log('Option 4 clicked')
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
label: 'Option 5',
|
||||
selected: selectedIds.has('5'),
|
||||
onClick: () => console.log('Option 5 clicked')
|
||||
}
|
||||
];
|
||||
|
||||
const handleSelect = (itemId: string) => {
|
||||
setSelectedIds((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(itemId)) {
|
||||
newSet.delete(itemId);
|
||||
} else {
|
||||
newSet.add(itemId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
selectType="multiple"
|
||||
items={items}
|
||||
menuHeader={{ placeholder: 'Search items...' }}
|
||||
onSelect={handleSelect}
|
||||
children={<Button>Selection Menu</Button>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Example with secondary labels
|
||||
export const WithSecondaryLabel: Story = {
|
||||
args: {
|
||||
|
|
|
@ -12,7 +12,8 @@ import {
|
|||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuCheckboxItem
|
||||
DropdownMenuCheckboxItemSingle,
|
||||
DropdownMenuCheckboxItemMultiple
|
||||
} from './DropdownBase';
|
||||
import { CircleSpinnerLoader } from '../loaders/CircleSpinnerLoader';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
|
@ -44,7 +45,7 @@ export type DropdownItems = (DropdownItem | DropdownDivider | React.ReactNode)[]
|
|||
|
||||
export interface DropdownProps extends DropdownMenuProps {
|
||||
items?: DropdownItems;
|
||||
selectType?: boolean;
|
||||
selectType?: 'single' | 'multiple' | 'none';
|
||||
menuHeader?: string | React.ReactNode | { placeholder: string };
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
|
@ -65,7 +66,7 @@ const dropdownItemKey = (item: DropdownItems[number], index: number) => {
|
|||
export const Dropdown: React.FC<DropdownProps> = React.memo(
|
||||
({
|
||||
items = [],
|
||||
selectType = false,
|
||||
selectType = 'none',
|
||||
menuHeader,
|
||||
minWidth = 240,
|
||||
maxWidth,
|
||||
|
@ -99,6 +100,29 @@ export const Dropdown: React.FC<DropdownProps> = React.memo(
|
|||
return filteredItems.length > 0 && filteredItems.some((item) => (item as DropdownItem).id);
|
||||
}, [filteredItems]);
|
||||
|
||||
const { selectedItems, unselectedItems } = useMemo(() => {
|
||||
if (selectType === 'multiple') {
|
||||
const [selectedItems, unselectedItems] = filteredItems.reduce(
|
||||
(acc, item) => {
|
||||
if ((item as DropdownItem).selected) {
|
||||
acc[0].push(item);
|
||||
} else {
|
||||
acc[1].push(item);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[[], []] as [typeof filteredItems, typeof filteredItems]
|
||||
);
|
||||
return { selectedItems, unselectedItems };
|
||||
}
|
||||
return {
|
||||
selectedItems: [],
|
||||
unselectedItems: []
|
||||
};
|
||||
}, [selectType, filteredItems]);
|
||||
|
||||
const dropdownItems = selectType === 'multiple' ? unselectedItems : filteredItems;
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} defaultOpen={open} onOpenChange={onOpenChange} {...props}>
|
||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||
|
@ -117,18 +141,33 @@ export const Dropdown: React.FC<DropdownProps> = React.memo(
|
|||
</>
|
||||
)}
|
||||
|
||||
<div className="max-h-[300px] overflow-y-auto">
|
||||
<div className="max-h-[350px] overflow-y-auto">
|
||||
{hasShownItem ? (
|
||||
filteredItems.map((item, index) => (
|
||||
<DropdownItemSelector
|
||||
item={item}
|
||||
index={index}
|
||||
selectType={selectType}
|
||||
onSelect={onSelect}
|
||||
closeOnSelect={closeOnSelect}
|
||||
key={dropdownItemKey(item, index)}
|
||||
/>
|
||||
))
|
||||
<>
|
||||
{selectedItems.map((item, index) => (
|
||||
<DropdownItemSelector
|
||||
item={item}
|
||||
index={index}
|
||||
selectType={selectType}
|
||||
onSelect={onSelect}
|
||||
closeOnSelect={closeOnSelect}
|
||||
key={dropdownItemKey(item, index)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{selectedItems.length > 0 && <DropdownMenuSeparator />}
|
||||
|
||||
{dropdownItems.map((item, index) => (
|
||||
<DropdownItemSelector
|
||||
item={item}
|
||||
index={index}
|
||||
selectType={selectType}
|
||||
onSelect={onSelect}
|
||||
closeOnSelect={closeOnSelect}
|
||||
key={dropdownItemKey(item, index)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<DropdownMenuItem disabled className="text-gray-light text-center">
|
||||
{emptyStateText}
|
||||
|
@ -192,7 +231,7 @@ const DropdownItem: React.FC<
|
|||
items,
|
||||
closeOnSelect,
|
||||
onSelect,
|
||||
selectType = false,
|
||||
selectType,
|
||||
secondaryLabel,
|
||||
truncate
|
||||
}) => {
|
||||
|
@ -228,15 +267,27 @@ const DropdownItem: React.FC<
|
|||
);
|
||||
}
|
||||
|
||||
if (selectType) {
|
||||
if (selectType === 'single') {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
<DropdownMenuCheckboxItemSingle
|
||||
checked={selected}
|
||||
disabled={disabled}
|
||||
onClick={onClickItem}
|
||||
closeOnSelect={closeOnSelect}>
|
||||
{content}
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuCheckboxItemSingle>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectType === 'multiple') {
|
||||
return (
|
||||
<DropdownMenuCheckboxItemMultiple
|
||||
checked={selected}
|
||||
disabled={disabled}
|
||||
onClick={onClickItem}
|
||||
closeOnSelect={closeOnSelect}>
|
||||
{content}
|
||||
</DropdownMenuCheckboxItemMultiple>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -316,28 +367,25 @@ interface DropdownMenuHeaderSearchProps {
|
|||
placeholder?: string;
|
||||
}
|
||||
|
||||
const DropdownMenuHeaderSearch: React.FC<DropdownMenuHeaderSearchProps> = ({
|
||||
text,
|
||||
onChange,
|
||||
placeholder
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Input
|
||||
autoFocus
|
||||
variant={'ghost'}
|
||||
placeholder={placeholder}
|
||||
value={text}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
{/*
|
||||
const DropdownMenuHeaderSearch: React.FC<DropdownMenuHeaderSearchProps> = React.memo(
|
||||
({ text, onChange, placeholder }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Input
|
||||
autoFocus
|
||||
variant={'ghost'}
|
||||
placeholder={placeholder}
|
||||
value={text}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
{/*
|
||||
<div className="flex pr-1 opacity-20 hover:opacity-100">
|
||||
<Button
|
||||
className="cursor-pointer"
|
||||
|
@ -346,6 +394,9 @@ const DropdownMenuHeaderSearch: React.FC<DropdownMenuHeaderSearchProps> = ({
|
|||
variant={'link'}
|
||||
size={'small'}></Button>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DropdownMenuHeaderSearch.displayName = 'DropdownMenuHeaderSearch';
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { Check, ChevronRight } from '../icons/NucleoIconOutlined';
|
||||
import { Check3 as Check, ChevronRight } from '../icons/NucleoIconOutlined';
|
||||
|
||||
import { cn } from '@/lib/classMerge';
|
||||
import { Checkbox } from '../checkbox/Checkbox';
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
|
@ -38,16 +39,15 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||
));
|
||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const baseContentClass = `bg-background text-foreground 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 rounded-md border p-1`;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-background text-foreground 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 rounded-md border p-1 shadow-lg',
|
||||
className
|
||||
)}
|
||||
className={cn(baseContentClass, 'shadow-lg', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
@ -61,10 +61,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-background text-foreground 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 rounded-md border p-1 shadow-md',
|
||||
className
|
||||
)}
|
||||
className={cn(baseContentClass, 'shadow-md', className)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
|
@ -100,7 +97,13 @@ const DropdownMenuItem = React.forwardRef<
|
|||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
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-50'
|
||||
);
|
||||
|
||||
const DropdownMenuCheckboxItemSingle = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> & {
|
||||
closeOnSelect?: boolean;
|
||||
|
@ -109,10 +112,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||
>(({ className, children, onClick, checked, closeOnSelect = true, selectType, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-item-hover focus:text-foreground relative flex cursor-pointer items-center rounded-sm py-1.5 pr-2 pl-8 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
className={cn(itemClass, 'data-[state=checked]:bg-item-hover', 'pr-6 pl-2', className)}
|
||||
checked={checked}
|
||||
onClick={(e) => {
|
||||
if (closeOnSelect) {
|
||||
|
@ -122,17 +122,55 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||
onClick?.(e);
|
||||
}}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
{children}
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<div className="flex h-4 w-4 items-center justify-center">
|
||||
<Check />
|
||||
</div>
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
DropdownMenuCheckboxItemSingle.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItemMultiple = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> & {
|
||||
closeOnSelect?: boolean;
|
||||
selectType?: boolean;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, children, onClick, checked = false, closeOnSelect = true, selectType, ...props },
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(itemClass, 'group pr-2 pl-7', className)}
|
||||
checked={checked}
|
||||
onClick={(e) => {
|
||||
if (closeOnSelect) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
onClick?.(e);
|
||||
}}
|
||||
{...props}>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute left-2 flex h-3.5 w-3.5 items-center justify-center opacity-0 group-hover:opacity-100',
|
||||
checked && 'opacity-100'
|
||||
)}>
|
||||
<Checkbox size="sm" checked={checked} />
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
);
|
||||
DropdownMenuCheckboxItemMultiple.displayName = 'DropdownMenuCheckboxItemMultiple';
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
|
@ -156,7 +194,7 @@ const DropdownMenuSeparator = React.forwardRef<
|
|||
ref={ref}
|
||||
className={cn(
|
||||
'bg-border dropdown-separator -mx-1 my-1 h-[0.5px]',
|
||||
'[&.dropdown-separator:has(+.dropdown-separator)]:hidden [&.dropdown-separator:last-child]:hidden',
|
||||
'[&.dropdown-separator:first-child]:hidden [&.dropdown-separator:has(+.dropdown-separator)]:hidden [&.dropdown-separator:last-child]:hidden',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
@ -176,7 +214,7 @@ export {
|
|||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuCheckboxItemSingle,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
|
@ -184,5 +222,6 @@ export {
|
|||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuCheckboxItemMultiple
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue