fix: botão de baximizar e minimizar

This commit is contained in:
Alessandro Gonçaalves 2025-11-10 15:43:15 -03:00
parent b29fa44a71
commit 79773d0c18
4 changed files with 499 additions and 255 deletions

14
package-lock.json generated
View File

@ -34,6 +34,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^3.3.1",
"vaul": "^1.1.2",
"xlsx": "^0.18.5"
},
"devDependencies": {
@ -11778,6 +11779,19 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/vaul": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
"integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-dialog": "^1.1.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/when-exit": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.4.tgz",

View File

@ -35,6 +35,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^3.3.1",
"vaul": "^1.1.2",
"xlsx": "^0.18.5"
},
"devDependencies": {

View File

@ -33,7 +33,17 @@ import {
SelectItem,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Download, Filter, X, Search, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Download, Filter, X, Search, ArrowUpDown, ArrowUp, ArrowDown, Maximize2, Minimize2 } from "lucide-react";
import * as XLSX from "xlsx";
interface AnaliticoItem {
@ -295,6 +305,7 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
const [loading, setLoading] = React.useState(false);
const [globalFilter, setGlobalFilter] = React.useState("");
const [open, setOpen] = React.useState(false);
const [drawerOpen, setDrawerOpen] = React.useState(false);
const [columnFilters, setColumnFilters] = React.useState<Record<string, string[]>>({});
const [columnSorts, setColumnSorts] = React.useState<Record<string, 'asc' | 'desc' | null>>({});
const [conditions, setConditions] = React.useState([
@ -810,6 +821,324 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
setGlobalFilter("");
};
// Função para renderizar o conteúdo principal do componente (reutilizável)
const renderAnaliticoContent = (isMaximized: boolean = false) => {
return (
<>
{/* Filtros Externos Ativos */}
{(filtrosExternos.dataInicio || filtrosExternos.centroCusto || filtrosExternos.codigoGrupo || filtrosExternos.codigoConta) && (
<div className="flex items-center gap-2 mb-4">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span className="text-sm font-medium text-blue-900">Filtros aplicados pela tabela DRE Gerencial:</span>
<div className="flex flex-wrap gap-2 text-xs text-blue-800">
{filtrosExternos.dataInicio && filtrosExternos.dataFim && (
<span className="px-2 py-1 bg-blue-100 rounded">
Período: {filtrosExternos.dataInicio} a {filtrosExternos.dataFim}
</span>
)}
{filtrosExternos.centroCusto && (
<span className="px-2 py-1 bg-blue-100 rounded">
Centro: {filtrosExternos.centroCusto}
</span>
)}
{filtrosExternos.codigoGrupo && (
<span className="px-2 py-1 bg-blue-100 rounded">
Grupo: {filtrosExternos.codigoGrupo}
</span>
)}
{filtrosExternos.codigoConta && (
<span className="px-2 py-1 bg-blue-100 rounded">
Conta: {filtrosExternos.codigoConta}
</span>
)}
</div>
</div>
)}
{/* Controls */}
<div className="flex gap-2 flex-wrap mb-4">
{data.length > 0 && (
<Button
onClick={clearAllFilters}
variant="outline"
size="sm"
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-red-50 hover:border-red-300 text-gray-700"
>
<X className="h-4 w-4" />
Limpar Filtros
{getFilterCount() > 0 && (
<span className="bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center font-semibold">
{getFilterCount()}
</span>
)}
</Button>
)}
<Button
onClick={exportToExcel}
variant="outline"
size="sm"
disabled={sortedAndFilteredData.length === 0}
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-green-50 hover:border-green-300 text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Download className="h-4 w-4" />
Exportar XLSX
</Button>
</div>
{/* DataGridPro */}
<Card className={`w-full shadow-lg rounded-2xl ${isMaximized ? 'h-[calc(96vh-280px)]' : 'h-[40vh]'}`} style={{ overflowAnchor: 'none' }}>
<CardContent className="p-4 h-full" style={{ overflowAnchor: 'none' }}>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-4">
<h2 className="text-lg font-semibold">
Total de Registros: <span className="text-blue-600">{sortedAndFilteredData.length}</span>
</h2>
<div className="text-sm text-gray-600">
Valor Total: <span className={`font-bold ${valorTotal < 0 ? 'text-red-600' : 'text-green-600'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(valorTotal)}
</span>
</div>
</div>
</div>
<div style={{ height: "calc(100% - 2rem)", width: "100%", position: "relative", display: "flex", flexDirection: "column" }}>
<DataGridPremium
key={`datagrid-${sortedAndFilteredData.length}-${Object.keys(columnFilters).length}`}
rows={sortedAndFilteredData}
columns={columns}
loading={loading}
disableRowSelectionOnClick
density="compact"
slots={{ toolbar: GridToolbar }}
disableColumnMenu={true}
disableColumnSorting={true}
pagination={false}
disableVirtualization={false}
getRowId={(row: any) => row.id || `row-${row.recnum || Math.random()}`}
sx={{
overflowAnchor: 'none',
height: "100%",
display: "flex",
flexDirection: "column",
"& .MuiDataGrid-root": {
display: "flex",
flexDirection: "column",
height: "100%",
flex: 1,
overflow: "hidden",
},
"& .MuiDataGrid-main": {
overflow: "hidden !important",
position: "relative",
height: "100%",
display: "flex",
flexDirection: "column",
flex: 1,
},
// Container dos headers - DEVE FICAR FIXO (não rola)
"& .MuiDataGrid-container--top": {
overflow: "hidden !important",
position: "relative",
zIndex: 100,
backgroundColor: "#f9fafb",
flexShrink: 0,
flexGrow: 0,
},
"& .MuiDataGrid-columnHeaders": {
position: "relative !important",
backgroundColor: "#f9fafb !important",
zIndex: 100,
borderBottom: "1px solid #e5e7eb",
},
"& .MuiDataGrid-columnHeader": {
backgroundColor: "#f9fafb !important",
},
"& .MuiDataGrid-columnHeaderRow": {
backgroundColor: "#f9fafb !important",
},
"& .MuiDataGrid-columnHeadersInner": {
backgroundColor: "#f9fafb !important",
},
"& .MuiDataGrid-cell": {
borderBottom: "1px solid #f0f0f0",
fontSize: "0.875rem",
},
// Container do virtualScroller - deve ter scroll e ocupar espaço restante
"& .MuiDataGrid-container--bottom": {
flex: 1,
overflow: "hidden !important",
position: "relative",
minHeight: 0,
display: "flex",
flexDirection: "column",
},
// Apenas o virtualScroller deve ter scroll
"& .MuiDataGrid-virtualScroller": {
overflowY: "auto !important",
overflowX: "auto !important",
height: "100% !important",
flex: 1,
overflowAnchor: 'none',
// Garantir que a barra de scroll seja visível
scrollbarWidth: "thin",
"&::-webkit-scrollbar": {
height: "8px",
width: "8px",
},
"&::-webkit-scrollbar-track": {
background: "#f1f1f1",
},
"&::-webkit-scrollbar-thumb": {
background: "#888",
borderRadius: "4px",
},
"&::-webkit-scrollbar-thumb:hover": {
background: "#555",
},
},
"& .MuiDataGrid-virtualScrollerContent": {
minWidth: "max-content",
},
"& .MuiDataGrid-row": {
minWidth: "max-content",
},
"& .MuiDataGrid-toolbarContainer": {
backgroundColor: "#f8fafc",
borderBottom: "1px solid #e5e7eb",
padding: "8px 16px",
},
"& .MuiDataGrid-scrollbar": {
display: "none",
},
// Ocultar todos os ícones nativos das colunas
"& .MuiDataGrid-columnHeaderMenuContainer": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderMenuButton": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderSortIcon": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderSortIconContainer": {
display: "none !important",
},
"& .MuiDataGrid-iconButtonContainer": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderSeparator": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderSortButton": {
display: "none !important",
},
// Ocultar qualquer ícone de menu adicional
"& .MuiDataGrid-menuIcon": {
display: "none !important",
},
"& .MuiDataGrid-menuIconButton": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderMenuIcon": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderMenuIconButton": {
display: "none !important",
},
"& .MuiDataGrid-menuContainer": {
display: "none !important",
},
"& .MuiDataGrid-menu": {
display: "none !important",
},
// Ocultar footer de paginação
"& .MuiDataGrid-footerContainer": {
display: "none !important",
},
"& .MuiDataGrid-pagination": {
display: "none !important",
},
"& .MuiTablePagination-root": {
display: "none !important",
},
// Garantir que nosso botão customizado apareça
"& .MuiDataGrid-columnHeaderTitleContainer": {
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
},
}}
/>
{/* Card de Agregação Customizado */}
{sortedAndFilteredData.length > 0 && (
<div
ref={setAggregationCardRef}
className="w-full bg-gray-50 border-t border-gray-200 sticky bottom-0 z-50 shadow-lg"
style={{ overflowAnchor: 'none' }}
>
<div className="px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-6">
<div className="text-sm font-medium text-gray-700">
Vl.Realizado:
<span className={`ml-2 font-semibold ${columnTotals.valor < 0 ? 'text-red-600' : 'text-gray-900'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valor)}
</span>
</div>
<div className="text-sm font-medium text-gray-700">
Vl.Previsto:
<span className={`ml-2 font-semibold ${columnTotals.valor_previsto < 0 ? 'text-red-600' : 'text-gray-900'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valor_previsto)}
</span>
</div>
<div className="text-sm font-medium text-gray-700">
Vl.Confirmado:
<span className={`ml-2 font-semibold ${columnTotals.valor_confirmado < 0 ? 'text-red-600' : 'text-gray-900'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valor_confirmado)}
</span>
</div>
<div className="text-sm font-medium text-gray-700">
Vl.Pago:
<span className={`ml-2 font-semibold ${columnTotals.valor_pago < 0 ? 'text-red-600' : 'text-gray-900'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valor_pago)}
</span>
</div>
</div>
<div className="text-sm text-gray-500">
Total de Registros: <span className="font-semibold text-blue-600">{sortedAndFilteredData.length}</span>
</div>
</div>
</div>
</div>
)}
</div>
</CardContent>
</Card>
</>
);
};
return (
<div className="w-full max-w-none mx-auto p-2" style={{ overflowAnchor: 'none' }}>
{/* Header Section */}
@ -914,263 +1243,53 @@ export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
<Download className="h-4 w-4" />
Exportar XLSX
</Button>
</div>
</div>
</div>
</div>
{/* DataGridPro */}
<Card className="w-full h-[40vh] shadow-lg rounded-2xl" style={{ overflowAnchor: 'none' }}>
<CardContent className="p-4 h-full" style={{ overflowAnchor: 'none' }}>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-4">
<h2 className="text-lg font-semibold">
Total de Registros: <span className="text-blue-600">{sortedAndFilteredData.length}</span>
</h2>
<div className="text-sm text-gray-600">
Valor Total: <span className={`font-bold ${valorTotal < 0 ? 'text-red-600' : 'text-green-600'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(valorTotal)}
</span>
</div>
</div>
</div>
<div style={{ height: "calc(100% - 2rem)", width: "100%", position: "relative", display: "flex", flexDirection: "column" }}>
<DataGridPremium
key={`datagrid-${sortedAndFilteredData.length}-${Object.keys(columnFilters).length}`}
rows={sortedAndFilteredData}
columns={columns}
loading={loading}
disableRowSelectionOnClick
density="compact"
slots={{ toolbar: GridToolbar }}
disableColumnMenu={true}
disableColumnSorting={true}
pagination={false}
disableVirtualization={false}
getRowId={(row: any) => row.id || `row-${row.recnum || Math.random()}`}
sx={{
overflowAnchor: 'none',
height: "100%",
display: "flex",
flexDirection: "column",
"& .MuiDataGrid-root": {
display: "flex",
flexDirection: "column",
height: "100%",
flex: 1,
overflow: "hidden",
},
"& .MuiDataGrid-main": {
overflow: "hidden !important",
position: "relative",
height: "100%",
display: "flex",
flexDirection: "column",
flex: 1,
},
// Container dos headers - DEVE FICAR FIXO (não rola)
"& .MuiDataGrid-container--top": {
overflow: "hidden !important",
position: "relative",
zIndex: 100,
backgroundColor: "#f9fafb",
flexShrink: 0,
flexGrow: 0,
},
"& .MuiDataGrid-columnHeaders": {
position: "relative !important",
backgroundColor: "#f9fafb !important",
zIndex: 100,
borderBottom: "1px solid #e5e7eb",
},
"& .MuiDataGrid-columnHeader": {
backgroundColor: "#f9fafb !important",
},
"& .MuiDataGrid-columnHeaderRow": {
backgroundColor: "#f9fafb !important",
},
"& .MuiDataGrid-columnHeadersInner": {
backgroundColor: "#f9fafb !important",
},
"& .MuiDataGrid-cell": {
borderBottom: "1px solid #f0f0f0",
fontSize: "0.875rem",
},
// Container do virtualScroller - deve ter scroll e ocupar espaço restante
"& .MuiDataGrid-container--bottom": {
flex: 1,
overflow: "hidden !important",
position: "relative",
minHeight: 0,
display: "flex",
flexDirection: "column",
},
// Apenas o virtualScroller deve ter scroll
"& .MuiDataGrid-virtualScroller": {
overflowY: "auto !important",
overflowX: "auto !important",
height: "100% !important",
flex: 1,
overflowAnchor: 'none',
// Garantir que a barra de scroll seja visível
scrollbarWidth: "thin",
"&::-webkit-scrollbar": {
height: "8px",
width: "8px",
},
"&::-webkit-scrollbar-track": {
background: "#f1f1f1",
},
"&::-webkit-scrollbar-thumb": {
background: "#888",
borderRadius: "4px",
},
"&::-webkit-scrollbar-thumb:hover": {
background: "#555",
},
},
"& .MuiDataGrid-virtualScrollerContent": {
minWidth: "max-content",
},
"& .MuiDataGrid-row": {
minWidth: "max-content",
},
"& .MuiDataGrid-toolbarContainer": {
backgroundColor: "#f8fafc",
borderBottom: "1px solid #e5e7eb",
padding: "8px 16px",
},
"& .MuiDataGrid-scrollbar": {
display: "none",
},
// Ocultar todos os ícones nativos das colunas
"& .MuiDataGrid-columnHeaderMenuContainer": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderMenuButton": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderSortIcon": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderSortIconContainer": {
display: "none !important",
},
"& .MuiDataGrid-iconButtonContainer": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderSeparator": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderSortButton": {
display: "none !important",
},
// Ocultar qualquer ícone de menu adicional
"& .MuiDataGrid-menuIcon": {
display: "none !important",
},
"& .MuiDataGrid-menuIconButton": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderMenuIcon": {
display: "none !important",
},
"& .MuiDataGrid-columnHeaderMenuIconButton": {
display: "none !important",
},
"& .MuiDataGrid-menuContainer": {
display: "none !important",
},
"& .MuiDataGrid-menu": {
display: "none !important",
},
// Ocultar footer de paginação
"& .MuiDataGrid-footerContainer": {
display: "none !important",
},
"& .MuiDataGrid-pagination": {
display: "none !important",
},
"& .MuiTablePagination-root": {
display: "none !important",
},
// Garantir que nosso botão customizado apareça
"& .MuiDataGrid-columnHeaderTitleContainer": {
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
},
}}
/>
{/* Card de Agregação Customizado - Usando position: sticky */}
{sortedAndFilteredData.length > 0 && (
<div
ref={setAggregationCardRef}
className="w-full bg-gray-50 border-t border-gray-200 sticky bottom-0 z-50 shadow-lg"
style={{ overflowAnchor: 'none' }}
<Drawer open={drawerOpen} onOpenChange={setDrawerOpen}>
<DrawerTrigger asChild>
<Button
variant="outline"
size="sm"
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-blue-50 hover:border-blue-300 text-gray-700"
>
<div className="px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-6">
<div className="text-sm font-medium text-gray-700">
Vl.Realizado:
<span className={`ml-2 font-semibold ${columnTotals.valor < 0 ? 'text-red-600' : 'text-gray-900'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valor)}
</span>
<Maximize2 className="h-4 w-4" />
Maximizar
</Button>
</DrawerTrigger>
<DrawerContent className="max-h-[96vh] h-[96vh]">
<DrawerHeader className="flex-shrink-0">
<div className="flex items-center justify-between">
<div className="flex-1">
<DrawerTitle className="text-2xl font-bold text-gray-900">
Análise Analítica{filtros.linhaSelecionada ? ` - ${filtros.linhaSelecionada}` : ""}
</DrawerTitle>
<DrawerDescription>
Relatório detalhado de transações - Versão Maximizada
</DrawerDescription>
</div>
<DrawerClose asChild>
<Button
variant="outline"
size="sm"
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-red-50 hover:border-red-300 text-gray-700"
>
<Minimize2 className="h-4 w-4" />
Minimizar
</Button>
</DrawerClose>
</div>
</DrawerHeader>
<div className="flex-1 overflow-y-auto px-4 pb-4">
{renderAnaliticoContent(true)}
</div>
</DrawerContent>
</Drawer>
</div>
</div>
</div>
<div className="text-sm font-medium text-gray-700">
Vl.Previsto:
<span className={`ml-2 font-semibold ${columnTotals.valor_previsto < 0 ? 'text-red-600' : 'text-gray-900'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valor_previsto)}
</span>
</div>
<div className="text-sm font-medium text-gray-700">
Vl.Confirmado:
<span className={`ml-2 font-semibold ${columnTotals.valor_confirmado < 0 ? 'text-red-600' : 'text-gray-900'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valor_confirmado)}
</span>
</div>
<div className="text-sm font-medium text-gray-700">
Vl.Pago:
<span className={`ml-2 font-semibold ${columnTotals.valor_pago < 0 ? 'text-red-600' : 'text-gray-900'}`}>
{new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(columnTotals.valor_pago)}
</span>
</div>
</div>
<div className="text-sm text-gray-500">
Total de Registros: <span className="font-semibold text-blue-600">{sortedAndFilteredData.length}</span>
</div>
</div>
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* Conteúdo Principal - Versão Normal */}
{renderAnaliticoContent(false)}
{/* Advanced Filters Dialog */}
<Dialog open={open} onOpenChange={setOpen}>

View File

@ -0,0 +1,110 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = DrawerPrimitive.Root
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerClose = DrawerPrimitive.Close
const DrawerPortal = DrawerPrimitive.Portal
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}