diff --git a/app/components/dashboard/project-management/digital-innovation-page.tsx b/app/components/dashboard/project-management/digital-innovation-page.tsx index 9d3d3d7..dab091f 100644 --- a/app/components/dashboard/project-management/digital-innovation-page.tsx +++ b/app/components/dashboard/project-management/digital-innovation-page.tsx @@ -12,7 +12,6 @@ import { Zap, } from "lucide-react"; import moment from "moment-jalaali"; -import { formatNumber } from "~/lib/utils"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import toast from "react-hot-toast"; import { Badge } from "~/components/ui/badge"; @@ -35,7 +34,7 @@ import { TableRow, } from "~/components/ui/table"; import apiService from "~/lib/api"; -import { formatCurrency } from "~/lib/utils"; +import { formatCurrency, formatNumber } from "~/lib/utils"; import { DashboardLayout } from "../layout"; moment.loadPersian({ usePersianDigits: true }); @@ -370,7 +369,8 @@ export function DigitalInnovationPage() { const scrollContainer = scrollContainerRef.current; const handleScroll = () => { - if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) return; + if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) + return; // Clear previous timeout if (scrollTimeoutRef.current) { @@ -390,7 +390,9 @@ export function DigitalInnovationPage() { }; if (scrollContainer) { - scrollContainer.addEventListener("scroll", handleScroll, { passive: true }); + scrollContainer.addEventListener("scroll", handleScroll, { + passive: true, + }); } return () => { @@ -452,14 +454,12 @@ export function DigitalInnovationPage() { innovation_digital_function: {}, }); - - // let payload: DigitalInnovationMetrics = raw?.data; // console.log("*-*-*-*" +payload); // if (typeof payload === "string") { // try { // payload = JSON.parse(payload).innovation_digital_function; - + // } catch {} // } @@ -469,10 +469,10 @@ export function DigitalInnovationPage() { try { // مرحله اول: data رو از string به object تبدیل کن const parsedData = JSON.parse(raw.data); - + // مرحله دوم: innovation_digital_function رو که خودش string هست parse کن const arr = JSON.parse(parsedData.innovation_digital_function); - + // مرحله سوم: اولین خانه آرایه رو بردار if (Array.isArray(arr) && arr.length > 0) { payload = arr[0]; @@ -482,8 +482,6 @@ export function DigitalInnovationPage() { } } - - const parseNum = (v: unknown): number => { if (v == null) return 0; if (typeof v === "number") return v; @@ -601,7 +599,7 @@ export function DigitalInnovationPage() { variant="ghost" size="sm" onClick={() => handleProjectDetails(item)} - className="text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto cursor-pointer" + className="text-pr-green hover:text-emerald-300 underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto" > جزئیات بیشتر @@ -955,7 +953,7 @@ export function DigitalInnovationPage() { شرح پروژه -
+
{dialogInfo?.title} @@ -1070,7 +1068,7 @@ export function DigitalInnovationPage() {
-
+
diff --git a/app/components/dashboard/project-management/process-innovation-page.tsx b/app/components/dashboard/project-management/process-innovation-page.tsx index e0987b5..f992124 100644 --- a/app/components/dashboard/project-management/process-innovation-page.tsx +++ b/app/components/dashboard/project-management/process-innovation-page.tsx @@ -15,8 +15,9 @@ import moment from "moment-jalaali"; import { useCallback, useEffect, useRef, useState } from "react"; import toast from "react-hot-toast"; import { Badge } from "~/components/ui/badge"; -import { Button } from "~/components/ui/button"; import { BaseCard } from "~/components/ui/base-card"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent } from "~/components/ui/card"; import { Checkbox } from "~/components/ui/checkbox"; import { CustomBarChart } from "~/components/ui/custom-bar-chart"; import { @@ -36,7 +37,6 @@ import { import apiService from "~/lib/api"; import { formatNumber } from "~/lib/utils"; import { DashboardLayout } from "../layout"; -import { Card , CardContent} from "~/components/ui/card"; moment.loadPersian({ usePersianDigits: true }); interface ProcessInnovationData { @@ -176,7 +176,7 @@ export function ProcessInnovationPage() { stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum ), description: "دلار کاهش یافته", - icon: DollarSign , + icon: DollarSign, color: "text-emerald-400", }, frequentfailuresreduction: { @@ -551,7 +551,11 @@ export function ProcessInnovationPage() { ); case "title": - return {String(value)}; + return ( + + {String(value)} + + ); case "project_status": return (
@@ -567,7 +571,10 @@ export function ProcessInnovationPage() { ); case "project_rating": return ( - + {formatNumber(String(value))} ); @@ -595,7 +602,10 @@ export function ProcessInnovationPage() { {loading || statsLoading ? // Loading skeleton for stats cards - matching new design Array.from({ length: 4 }).map((_, index) => ( - +
{ // map percent values for each card key - const percentMap: Record = { + const percentMap: Record< + string, + number | string | undefined + > = { productionstopsprevention: stats.percentProductionStops, bottleneckremoval: stats.percentBottleneckRemoval, currencyreduction: stats.percentCurrencyReduction, @@ -640,7 +653,7 @@ export function ProcessInnovationPage() {

- {(card.value)} + {card.value}

{card.description} @@ -846,10 +859,12 @@ export function ProcessInnovationPage() { شرح پروژه -
+
{/* Project Description */}
-

{selectedProjectDetails?.title}

+

+ {selectedProjectDetails?.title} +

{selectedProjectDetails?.project_description || "-"}

diff --git a/app/components/dashboard/sidebar.tsx b/app/components/dashboard/sidebar.tsx index ab0f8c1..6780184 100644 --- a/app/components/dashboard/sidebar.tsx +++ b/app/components/dashboard/sidebar.tsx @@ -1,25 +1,13 @@ import { - Box, - Building2, ChevronDown, - ChevronRight, - FolderKanban, GalleryVerticalEnd, - Globe, - LayoutDashboard, - Leaf, - Lightbulb, + House, + LightbulbIcon, + ListTodo, LogOut, - MonitorSmartphone, - Package, + Radar, Settings, Star, - Workflow, - DiscAlbum, - House, - ListTodo, - LightbulbIcon, - Radar } from "lucide-react"; import React, { useState } from "react"; import { Link, useLocation } from "react-router"; @@ -49,7 +37,6 @@ interface MenuItem { } const menuItems: MenuItem[] = [ - { id: "dashboard", label: "صفحه اصلی", @@ -123,7 +110,6 @@ const menuItems: MenuItem[] = [ icon: null, href: "#", // This is not a route, it opens a popup }, - ]; const bottomMenuItems: MenuItem[] = [ @@ -172,7 +158,10 @@ export function Sidebar({ // Update header title based on current route // If a child route is active, use that child's label prefixed by parent label let activeTitle: string | undefined = undefined; - let activeIcon: React.ComponentType<{ className?: string }> | null | undefined = undefined; + let activeIcon: + | React.ComponentType<{ className?: string }> + | null + | undefined = undefined; menuItems.forEach((item) => { if (item.children) { const activeChild = item.children.find( @@ -190,7 +179,10 @@ export function Sidebar({ } }); if (onTitleChange) { - onTitleChange({ title: activeTitle ?? "صفحه اول", icon: activeIcon ?? null }); + onTitleChange({ + title: activeTitle ?? "صفحه اول", + icon: activeIcon ?? null, + }); } }; @@ -261,7 +253,7 @@ export function Sidebar({
- ) - + ); } return ( @@ -331,10 +322,10 @@ export function Sidebar({ "w-full text-right", // Disable pointer cursor when child is active (cannot collapse) item.children && - item.children.some( - (child) => child.href && location.pathname === child.href - ) && - "cursor-not-allowed" + item.children.some( + (child) => child.href && location.pathname === child.href + ) && + "cursor-not-allowed" )} onClick={handleClick} > @@ -435,7 +426,7 @@ export function Sidebar({ />
- اینوژن بندر امام + اینوژن بندر امام
نسخه ۰.۱
diff --git a/app/components/dashboard/strategic-alignment-popup.tsx b/app/components/dashboard/strategic-alignment-popup.tsx index 8769448..9e3fcae 100644 --- a/app/components/dashboard/strategic-alignment-popup.tsx +++ b/app/components/dashboard/strategic-alignment-popup.tsx @@ -1,25 +1,25 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useReducer, useState } from "react"; import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "~/components/ui/dialog"; -import { - BarChart, Bar, + BarChart, + CartesianGrid, + Cell, + LabelList, + ResponsiveContainer, XAxis, YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, - LabelList, - Cell, } from "recharts"; -import apiService from "~/lib/api"; +import { Dialog, DialogContent, DialogHeader } from "~/components/ui/dialog"; import { Skeleton } from "~/components/ui/skeleton"; +import apiService from "~/lib/api"; import { formatNumber } from "~/lib/utils"; import { ChartContainer } from "../ui/chart"; +import { + DropdownMenu, + DropdownMenuButton, + DropdownMenuContent, + DropdownMenuItem, +} from "../ui/dropdown-menu"; import { TruncatedText } from "../ui/truncatedText"; interface StrategicAlignmentData { @@ -28,6 +28,51 @@ interface StrategicAlignmentData { percentage?: number; } +interface DropDownConfig { + isOpen: boolean; + selectedValue: string; + dropDownItems: Array; +} + +type Action = + | { type: "OPEN" } + | { type: "CLOSE" } + | { type: "SETVALUE"; value: Array } + | { type: "SELECT"; value: string }; + +// const DropDownItems = [ +// { +// id: 0, +// key: "همه مضامین", +// Value: "همه مضامین", +// }, +// { +// id: 1, +// key: "ارزش های هم افزایی نوآورانه", +// Value: "همه مضامین", +// }, +// { +// id: 2, +// key: "ارزش های خودکفایی نوآوورانه", +// Value: "همه مضامین", +// }, +// { +// id: 3, +// key: "ارزش های فناوری های نوین", +// Value: "همه مضامین", +// }, +// { +// id: 4, +// key: "ارزش های توسعه منابع انسانی", +// Value: "همه مضامین", +// }, +// { +// id: 5, +// key: "ارزش های نوآوری سبز", +// Value: "همه مضامین", +// }, +// ]; + interface StrategicAlignmentPopupProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -41,11 +86,10 @@ const chartConfig = { }, }; - const maxHeight = 150; - const barHeights = () => Math.floor(Math.random() * maxHeight); +const maxHeight = 150; +const barHeights = () => Math.floor(Math.random() * maxHeight); const ChartSkeleton = () => ( -
{/* Chart bars */}
@@ -58,7 +102,7 @@ const ChartSkeleton = () => (
))}
- {/* Left space for Y-axis label */} + {/* Left space for Y-axis label */}
@@ -74,6 +118,11 @@ export function StrategicAlignmentPopup({ }: StrategicAlignmentPopupProps) { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); + const [state, dispatch] = useReducer(reducer, { + isOpen: false, + selectedValue: "همه مضامین", + dropDownItems: [], + }); useEffect(() => { if (open) { @@ -98,29 +147,12 @@ export function StrategicAlignmentPopup({ ? JSON.parse(response.data) : response.data; - const processedData = responseData - .map((item: any) => ({ - strategic_theme: item.strategic_theme || "N/A", - operational_fee_sum: Math.max(0, Number(item.operational_fee_sum)), - })) - .filter((item: StrategicAlignmentData) => item.strategic_theme !== ""); - - const total = processedData.reduce( - (acc: number, item: StrategicAlignmentData) => - acc + item.operational_fee_sum, - 0 + setBarItems(responseData); + const dropDownItems = responseData.map( + (item: any) => item.strategic_theme ); - const dataWithPercentage = processedData.map( - (item: StrategicAlignmentData) => ({ - ...item, - percentage: - total > 0 - ? Math.round((item.operational_fee_sum / total) * 100) - : 0, - }) - ); - setData(dataWithPercentage || []); + setDropDownValues(["همه مضامین", ...dropDownItems]); } catch (error) { console.error("Error fetching strategic alignment data:", error); } finally { @@ -128,19 +160,135 @@ export function StrategicAlignmentPopup({ } }; + const fetchDropDownItems = async (item: string) => { + try { + if (item !== "همه مضامین") { + const response = await apiService.select({ + ProcessName: "project", + OutputFields: [ + "value_technology_and_innovation", + "sum(operational_fee)", + ], + Conditions: [["strategic_theme", "=", item]], + GroupBy: ["value_technology_and_innovation"], + }); + + const responseData = + typeof response.data === "string" + ? JSON.parse(response.data) + : response.data; + setBarItems(responseData); + } else fetchData(); + } catch (error) { + console.error("Error fetching strategic alignment data:", error); + } finally { + setLoading(false); + } + }; + + function reducer(state: DropDownConfig, action: Action): DropDownConfig { + switch (action.type) { + case "OPEN": + return { ...state, isOpen: true }; + case "CLOSE": + return { ...state, isOpen: false }; + case "SETVALUE": + return { ...state, dropDownItems: action.value }; + case "SELECT": + return { ...state, selectedValue: action.value }; + default: + return state; + } + } + + const toggleMenuHandler = () => { + dispatch({ + type: "OPEN", + }); + }; + + const selectItem = (item: string) => { + dispatch({ + type: "SELECT", + value: item, + }); + + dispatch({ + type: "CLOSE", + }); + + fetchDropDownItems(item); + }; + + const setDropDownValues = (items: Array) => { + dispatch({ + type: "SETVALUE", + value: items, + }); + }; + + const setBarItems = (responseData: any) => { + const processedData = responseData + .map((item: any) => ({ + strategic_theme: + item.strategic_theme || item.value_technology_and_innovation || "N/A", + operational_fee_sum: Math.max(0, Number(item.operational_fee_sum)), + })) + .filter((item: StrategicAlignmentData) => item.strategic_theme !== ""); + + const total = processedData.reduce( + (acc: number, item: StrategicAlignmentData) => + acc + item.operational_fee_sum, + 0 + ); + + const dataWithPercentage = processedData.map( + (item: StrategicAlignmentData) => ({ + ...item, + percentage: + total > 0 ? Math.round((item.operational_fee_sum / total) * 100) : 0, + }) + ); + setData(dataWithPercentage || []); + }; + return ( - - میزان انطباق راهبردی + +
+
+ + {state.selectedValue} + + + {state.dropDownItems.map((item: string, key: number) => ( +
selectItem(item)} + key={`${key}-${item}`} + > + {item} +
+ ))} +
+
+
+
{loading ? ( ) : ( <> - - + + - - - + + ); }} @@ -179,37 +324,38 @@ export function StrategicAlignmentPopup({ tickFormatter={(value) => `${formatNumber(Math.round(value))}` } - - - label={{ - value: "تعداد برنامه ها" , - angle: -90, - position: "insideLeft", - fill: "#94a3b8", - fontSize: 11, - offset: 0, - dy: 0, - style: { textAnchor: "middle" }, - }} + label={{ + value: "تعداد برنامه ها", + angle: -90, + position: "insideLeft", + fill: "#94a3b8", + fontSize: 11, + offset: 0, + dy: 0, + style: { textAnchor: "middle" }, + }} /> - + {data.map((entry, index) => ( - + ))} `${formatNumber(Math.round(v))}`} + formatter={(v: number) => + `${formatNumber(Math.round(v))}` + } /> - diff --git a/app/components/ui/dialog.tsx b/app/components/ui/dialog.tsx index a15e864..295c0a1 100644 --- a/app/components/ui/dialog.tsx +++ b/app/components/ui/dialog.tsx @@ -1,18 +1,18 @@ -"use client" +"use client"; -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; +import * as React from "react"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; -const Dialog = DialogPrimitive.Root +const Dialog = DialogPrimitive.Root; -const DialogTrigger = DialogPrimitive.Trigger +const DialogTrigger = DialogPrimitive.Trigger; -const DialogPortal = DialogPrimitive.Portal +const DialogPortal = DialogPrimitive.Portal; -const DialogClose = DialogPrimitive.Close +const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< React.ElementRef, @@ -26,8 +26,8 @@ const DialogOverlay = React.forwardRef< )} {...props} /> -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, @@ -38,7 +38,7 @@ const DialogContent = React.forwardRef< -)) -DialogContent.displayName = DialogPrimitive.Content.displayName +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ className, @@ -59,13 +59,13 @@ const DialogHeader = ({ }: React.HTMLAttributes) => (
-) -DialogHeader.displayName = "DialogHeader" +); +DialogHeader.displayName = "DialogHeader"; const DialogFooter = ({ className, @@ -78,8 +78,8 @@ const DialogFooter = ({ )} {...props} /> -) -DialogFooter.displayName = "DialogFooter" +); +DialogFooter.displayName = "DialogFooter"; const DialogTitle = React.forwardRef< React.ElementRef, @@ -93,8 +93,8 @@ const DialogTitle = React.forwardRef< )} {...props} /> -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; const DialogDescription = React.forwardRef< React.ElementRef, @@ -105,18 +105,18 @@ const DialogDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, - DialogPortal, - DialogOverlay, DialogClose, - DialogTrigger, DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, DialogDescription, -} + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/app/components/ui/dropdown-menu.tsx b/app/components/ui/dropdown-menu.tsx index 8132094..d9325a2 100644 --- a/app/components/ui/dropdown-menu.tsx +++ b/app/components/ui/dropdown-menu.tsx @@ -1,27 +1,27 @@ -"use client" +"use client"; -import * as React from "react" -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { Check, ChevronRight, Circle } from "lucide-react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronDown, Circle } from "lucide-react"; +import * as React from "react"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; -const DropdownMenu = DropdownMenuPrimitive.Root +const DropdownMenu = DropdownMenuPrimitive.Root; -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; -const DropdownMenuGroup = DropdownMenuPrimitive.Group +const DropdownMenuGroup = DropdownMenuPrimitive.Group; -const DropdownMenuPortal = DropdownMenuPrimitive.Portal +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; -const DropdownMenuSub = DropdownMenuPrimitive.Sub +const DropdownMenuSub = DropdownMenuPrimitive.Sub; -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; const DropdownMenuSubTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, children, ...props }, ref) => ( {children} - -)) +)); DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName + DropdownMenuPrimitive.SubTrigger.displayName; const DropdownMenuSubContent = React.forwardRef< React.ElementRef, @@ -52,9 +51,9 @@ const DropdownMenuSubContent = React.forwardRef< )} {...props} /> -)) +)); DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName + DropdownMenuPrimitive.SubContent.displayName; const DropdownMenuContent = React.forwardRef< React.ElementRef, @@ -65,32 +64,32 @@ const DropdownMenuContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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 mt-1 rounded-xl border border-gray-500 bg-pr-gray p-1 text-popover-foreground shadow-md 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", className )} {...props} /> -)) -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; const DropdownMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( -)) -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; const DropdownMenuCheckboxItem = React.forwardRef< React.ElementRef, @@ -112,9 +111,9 @@ const DropdownMenuCheckboxItem = React.forwardRef< {children} -)) +)); DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName + DropdownMenuPrimitive.CheckboxItem.displayName; const DropdownMenuRadioItem = React.forwardRef< React.ElementRef, @@ -135,13 +134,13 @@ const DropdownMenuRadioItem = React.forwardRef< {children} -)) -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; const DropdownMenuLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( -)) -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; const DropdownMenuSeparator = React.forwardRef< React.ElementRef, @@ -165,8 +164,8 @@ const DropdownMenuSeparator = React.forwardRef< className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} /> -)) -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; const DropdownMenuShortcut = ({ className, @@ -177,24 +176,43 @@ const DropdownMenuShortcut = ({ className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} /> - ) -} -DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +const DropdownMenuButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuButton.displayName = "DropdownMenuButton"; export { DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, + DropdownMenuButton, DropdownMenuCheckboxItem, - DropdownMenuRadioItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, - DropdownMenuRadioGroup, -} + DropdownMenuTrigger, +};