From 55ed714d77b8d6d28a7ab84b617b74bb0aa21031 Mon Sep 17 00:00:00 2001 From: Ty Tremblay Date: Fri, 20 Dec 2024 10:31:21 -0500 Subject: [PATCH] add dropdown menu --- package.json | 1 + pnpm-lock.yaml | 170 +++++++++++++++ src/components/ConfigEditor.tsx | 87 ++++---- src/components/QR/QRModal.tsx | 7 +- .../CommitAndResetSection/ResetButton.tsx | 2 + src/components/inputs/TimerInput.tsx | 11 +- src/components/ui/dropdown-menu.tsx | 199 ++++++++++++++++++ 7 files changed, 431 insertions(+), 46 deletions(-) create mode 100644 src/components/ui/dropdown-menu.tsx diff --git a/package.json b/package.json index 2bc43aa..c610cd5 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-aspect-ratio": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-dialog": "^1.1.3", + "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-select": "^2.1.3", "@radix-ui/react-slider": "^1.2.2", "@radix-ui/react-slot": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67d14e2..ed3d1f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@radix-ui/react-dialog': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.4 + version: 2.1.4(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-select': specifier: ^2.1.3 version: 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -599,6 +602,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.3': + resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} + 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 + + '@radix-ui/react-dropdown-menu@2.1.4': + resolution: {integrity: sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==} + 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 + '@radix-ui/react-focus-guards@1.1.1': resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} peerDependencies: @@ -630,6 +659,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-menu@2.1.4': + resolution: {integrity: sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==} + 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 + '@radix-ui/react-popper@1.2.1': resolution: {integrity: sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==} peerDependencies: @@ -1678,6 +1720,16 @@ packages: '@types/react': optional: true + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-remove-scroll@2.6.0: resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==} engines: {node: '>=10'} @@ -1688,6 +1740,16 @@ packages: '@types/react': optional: true + react-remove-scroll@2.6.2: + resolution: {integrity: sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-side-effect@2.1.2: resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==} peerDependencies: @@ -1703,6 +1765,16 @@ packages: '@types/react': optional: true + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -1874,6 +1946,16 @@ packages: '@types/react': optional: true + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + use-sidecar@1.1.2: resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} @@ -2394,6 +2476,34 @@ snapshots: '@types/react': 18.3.16 '@types/react-dom': 18.3.5(@types/react@18.3.16) + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.16)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.16)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.16)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.3.16 + '@types/react-dom': 18.3.5(@types/react@18.3.16) + + '@radix-ui/react-dropdown-menu@2.1.4(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.16)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.16)(react@18.2.0) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.16)(react@18.2.0) + '@radix-ui/react-menu': 2.1.4(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.16)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.3.16 + '@types/react-dom': 18.3.5(@types/react@18.3.16) + '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.16)(react@18.2.0)': dependencies: react: 18.2.0 @@ -2418,6 +2528,32 @@ snapshots: optionalDependencies: '@types/react': 18.3.16 + '@radix-ui/react-menu@2.1.4(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.16)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.16)(react@18.2.0) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.16)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.16)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.16)(react@18.2.0) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-roving-focus': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.16)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.16)(react@18.2.0) + aria-hidden: 1.2.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.6.2(@types/react@18.3.16)(react@18.2.0) + optionalDependencies: + '@types/react': 18.3.16 + '@types/react-dom': 18.3.5(@types/react@18.3.16) + '@radix-ui/react-popper@1.2.1(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -3307,6 +3443,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.16 + react-remove-scroll-bar@2.3.8(@types/react@18.3.16)(react@18.2.0): + dependencies: + react: 18.2.0 + react-style-singleton: 2.2.3(@types/react@18.3.16)(react@18.2.0) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.16 + react-remove-scroll@2.6.0(@types/react@18.3.16)(react@18.2.0): dependencies: react: 18.2.0 @@ -3318,6 +3462,17 @@ snapshots: optionalDependencies: '@types/react': 18.3.16 + react-remove-scroll@2.6.2(@types/react@18.3.16)(react@18.2.0): + dependencies: + react: 18.2.0 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.16)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.3.16)(react@18.2.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.16)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.3.16)(react@18.2.0) + optionalDependencies: + '@types/react': 18.3.16 + react-side-effect@2.1.2(react@18.2.0): dependencies: react: 18.2.0 @@ -3331,6 +3486,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.16 + react-style-singleton@2.2.3(@types/react@18.3.16)(react@18.2.0): + dependencies: + get-nonce: 1.0.1 + react: 18.2.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.16 + react@18.2.0: dependencies: loose-envify: 1.4.0 @@ -3545,6 +3708,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.16 + use-callback-ref@1.3.3(@types/react@18.3.16)(react@18.2.0): + dependencies: + react: 18.2.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.16 + use-sidecar@1.1.2(@types/react@18.3.16)(react@18.2.0): dependencies: detect-node-es: 1.1.0 diff --git a/src/components/ConfigEditor.tsx b/src/components/ConfigEditor.tsx index d22c6e6..e6eccb1 100644 --- a/src/components/ConfigEditor.tsx +++ b/src/components/ConfigEditor.tsx @@ -1,6 +1,6 @@ import { Button } from '@/components/ui/button'; import Editor, { useMonaco } from '@monaco-editor/react'; -import { Upload } from 'lucide-react'; +import { Menu, Save } from 'lucide-react'; import { useEffect, useMemo, useRef, useState } from 'react'; import schema from '../assets/schema.json'; import { @@ -10,6 +10,12 @@ import { useQRScoutState, } from '../store/store'; import { Config } from './inputs/BaseInputProps'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from './ui/dropdown-menu'; import { Input } from './ui/input'; /** @@ -52,13 +58,15 @@ export function ConfigEditor(props: ConfigEditorProps) { const monaco = useMonaco(); const formData = useQRScoutState(state => state.formData); const config = useMemo(() => getConfig(), [formData]); - const [currentConfigText, setCurrentConfigText] = useState(''); + const [currentConfigText, setCurrentConfigText] = useState( + JSON.stringify(config, null, 2), + ); const [errorCount, setErrorCount] = useState(0); const fileInputRef = useRef(null); - const handleButtonClick = () => { - fileInputRef.current?.click(); - }; + useEffect(() => { + setCurrentConfigText(JSON.stringify(config, null, 2)); + }, [config]); useEffect(() => { monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({ @@ -72,12 +80,13 @@ export function ConfigEditor(props: ConfigEditorProps) { ], }); }, [monaco]); + return (
{ const severeErrors = markers.filter(m => m.severity > 4); @@ -86,38 +95,42 @@ export function ConfigEditor(props: ConfigEditorProps) { onChange={value => value && setCurrentConfigText(value)} />
-
-
- - -
-
- - uploadConfig(e)} - className="hidden" - aria-hidden="true" - /> -
+
+ + + + + + resetToDefaultConfig()}> + Reset To Default Config + + downloadConfig(config)}> + Download Config + + fileInputRef.current?.click()}> + Upload Config + -
- - -
+ uploadConfig(e)} + className="hidden" + aria-hidden="true" + /> +
+
+
); diff --git a/src/components/QR/QRModal.tsx b/src/components/QR/QRModal.tsx index 3933a52..a11603b 100644 --- a/src/components/QR/QRModal.tsx +++ b/src/components/QR/QRModal.tsx @@ -1,4 +1,4 @@ -import { Copy } from 'lucide-react'; +import { Copy, QrCode } from 'lucide-react'; import { QRCodeSVG } from 'qrcode.react'; import { useMemo } from 'react'; import { getFieldValue, useQRScoutState } from '../../store/store'; @@ -31,7 +31,10 @@ export function QRModal(props: QRModalProps) { return ( - + diff --git a/src/components/Sections/CommitAndResetSection/ResetButton.tsx b/src/components/Sections/CommitAndResetSection/ResetButton.tsx index dde5810..b3f582c 100644 --- a/src/components/Sections/CommitAndResetSection/ResetButton.tsx +++ b/src/components/Sections/CommitAndResetSection/ResetButton.tsx @@ -1,4 +1,5 @@ import { Button } from '@/components/ui/button'; +import { ListRestart } from 'lucide-react'; import { resetFields } from '../../../store/store'; export type ResetButtonProps = { @@ -12,6 +13,7 @@ export function ResetButton(props: ResetButtonProps) { onClick={() => resetFields()} disabled={props.disabled} > + Reset Form ); diff --git a/src/components/inputs/TimerInput.tsx b/src/components/inputs/TimerInput.tsx index f8b2efb..9a067ab 100644 --- a/src/components/inputs/TimerInput.tsx +++ b/src/components/inputs/TimerInput.tsx @@ -47,12 +47,9 @@ export default function TimerInput(props: ConfigurableInputProps) { toggleTimer(!isRunning); } - function clearTimer(update: boolean = false) { - if (update) { - setTimes(old => [...old, time / 100]); - } + function lap() { + setTimes([...times, time / 100]); setTime(0); - toggleTimer(false); } useEffect(() => { @@ -83,10 +80,10 @@ export default function TimerInput(props: ConfigurableInputProps) { )} - -
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..9ff6568 --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,199 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + svg]:size-4 [&>svg]:shrink-0", + inset && "pl-8", + className + )} + {...props} + /> +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}