@greatapps/greatchat-ui 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,218 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import type { ColumnDef } from "@tanstack/react-table";
5
+ import type { Contact } from "../types";
6
+ import type { GchatHookConfig } from "../hooks/types";
7
+ import { useContacts, useDeleteContact } from "../hooks/use-contacts";
8
+ import { ContactFormDialog } from "./contact-form-dialog";
9
+ import { DataTable } from "./data-table";
10
+ import { Input } from "./ui/input";
11
+ import {
12
+ Tooltip,
13
+ TooltipTrigger,
14
+ TooltipContent,
15
+ } from "./ui/tooltip";
16
+ import {
17
+ AlertDialog,
18
+ AlertDialogAction,
19
+ AlertDialogCancel,
20
+ AlertDialogContent,
21
+ AlertDialogDescription,
22
+ AlertDialogFooter,
23
+ AlertDialogHeader,
24
+ AlertDialogTitle,
25
+ } from "./ui/alert-dialog";
26
+ import { Button } from "./ui/button";
27
+ import { Pencil, Trash2, Search } from "lucide-react";
28
+ import { format } from "date-fns";
29
+ import { ptBR } from "date-fns/locale";
30
+ import { toast } from "sonner";
31
+
32
+ function useColumns(
33
+ onEdit: (contact: Contact) => void,
34
+ onDelete: (id: number) => void,
35
+ ): ColumnDef<Contact>[] {
36
+ return [
37
+ {
38
+ accessorKey: "name",
39
+ header: "Nome",
40
+ cell: ({ row }) => (
41
+ <span className="font-medium">{row.original.name}</span>
42
+ ),
43
+ sortingFn: (rowA, rowB) =>
44
+ rowA.original.name.toLowerCase().localeCompare(rowB.original.name.toLowerCase()),
45
+ },
46
+ {
47
+ accessorKey: "phone_number",
48
+ header: "Telefone",
49
+ cell: ({ row }) => row.original.phone_number || "—",
50
+ sortingFn: (rowA, rowB) => {
51
+ const a = rowA.original.phone_number || "";
52
+ const b = rowB.original.phone_number || "";
53
+ return a.localeCompare(b);
54
+ },
55
+ },
56
+ {
57
+ accessorKey: "identifier",
58
+ header: "Identificador",
59
+ cell: ({ row }) => (
60
+ <span className="text-muted-foreground text-xs font-mono">
61
+ {row.original.identifier || "—"}
62
+ </span>
63
+ ),
64
+ sortingFn: (rowA, rowB) => {
65
+ const a = rowA.original.identifier || "";
66
+ const b = rowB.original.identifier || "";
67
+ return a.localeCompare(b);
68
+ },
69
+ },
70
+ {
71
+ accessorKey: "datetime_add",
72
+ header: "Criado em",
73
+ cell: ({ row }) => (
74
+ <span className="text-muted-foreground text-sm">
75
+ {format(new Date(row.original.datetime_add), "dd/MM/yyyy", { locale: ptBR })}
76
+ </span>
77
+ ),
78
+ },
79
+ {
80
+ id: "actions",
81
+ size: 80,
82
+ enableSorting: false,
83
+ cell: ({ row }) => (
84
+ <div className="flex items-center gap-1">
85
+ <Tooltip>
86
+ <TooltipTrigger asChild>
87
+ <Button
88
+ variant="ghost"
89
+ size="icon"
90
+ className="h-8 w-8"
91
+ onClick={() => onEdit(row.original)}
92
+ >
93
+ <Pencil className="h-4 w-4" />
94
+ </Button>
95
+ </TooltipTrigger>
96
+ <TooltipContent>Editar</TooltipContent>
97
+ </Tooltip>
98
+ <Tooltip>
99
+ <TooltipTrigger asChild>
100
+ <Button
101
+ variant="ghost"
102
+ size="icon"
103
+ className="h-8 w-8 text-destructive hover:text-destructive"
104
+ onClick={() => onDelete(row.original.id)}
105
+ >
106
+ <Trash2 className="h-4 w-4" />
107
+ </Button>
108
+ </TooltipTrigger>
109
+ <TooltipContent>Excluir</TooltipContent>
110
+ </Tooltip>
111
+ </div>
112
+ ),
113
+ },
114
+ ];
115
+ }
116
+
117
+ export interface ContactsTableProps {
118
+ config: GchatHookConfig;
119
+ }
120
+
121
+ export function ContactsTable({ config }: ContactsTableProps) {
122
+ const [search, setSearch] = useState("");
123
+ const [page, setPage] = useState(1);
124
+
125
+ const queryParams = useMemo(() => {
126
+ const params: Record<string, string> = {
127
+ limit: "15",
128
+ page: String(page),
129
+ };
130
+ if (search) {
131
+ params.search = search;
132
+ }
133
+ return params;
134
+ }, [search, page]);
135
+
136
+ const { data, isLoading } = useContacts(config, queryParams);
137
+ const deleteContact = useDeleteContact(config);
138
+ const [editContact, setEditContact] = useState<Contact | null>(null);
139
+ const [deleteId, setDeleteId] = useState<number | null>(null);
140
+
141
+ const contacts = data?.data || [];
142
+ const total = data?.total || 0;
143
+
144
+ const columns = useColumns(
145
+ (contact) => setEditContact(contact),
146
+ (id) => setDeleteId(id),
147
+ );
148
+
149
+ function handleDelete() {
150
+ if (!deleteId) return;
151
+ deleteContact.mutate(deleteId, {
152
+ onSuccess: () => {
153
+ toast.success("Contato excluído");
154
+ setDeleteId(null);
155
+ },
156
+ onError: () => toast.error("Erro ao excluir contato"),
157
+ });
158
+ }
159
+
160
+ function handleSearchChange(value: string) {
161
+ setSearch(value);
162
+ setPage(1);
163
+ }
164
+
165
+ return (
166
+ <>
167
+ <div className="flex items-center gap-3">
168
+ <div className="relative flex-1 max-w-md">
169
+ <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
170
+ <Input
171
+ placeholder="Buscar por nome, telefone ou identificador..."
172
+ value={search}
173
+ onChange={(e) => handleSearchChange(e.target.value)}
174
+ className="pl-9"
175
+ />
176
+ </div>
177
+ </div>
178
+
179
+ <DataTable
180
+ columns={columns}
181
+ data={contacts}
182
+ isLoading={isLoading}
183
+ emptyMessage="Nenhum contato encontrado"
184
+ total={total}
185
+ page={page}
186
+ onPageChange={setPage}
187
+ pageSize={15}
188
+ />
189
+
190
+ <ContactFormDialog
191
+ open={!!editContact}
192
+ onOpenChange={(open) => !open && setEditContact(null)}
193
+ contact={editContact ?? undefined}
194
+ config={config}
195
+ />
196
+
197
+ <AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
198
+ <AlertDialogContent>
199
+ <AlertDialogHeader>
200
+ <AlertDialogTitle>Excluir contato?</AlertDialogTitle>
201
+ <AlertDialogDescription>
202
+ Esta ação não pode ser desfeita. O contato será removido permanentemente.
203
+ </AlertDialogDescription>
204
+ </AlertDialogHeader>
205
+ <AlertDialogFooter>
206
+ <AlertDialogCancel>Cancelar</AlertDialogCancel>
207
+ <AlertDialogAction
208
+ onClick={handleDelete}
209
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
210
+ >
211
+ Excluir
212
+ </AlertDialogAction>
213
+ </AlertDialogFooter>
214
+ </AlertDialogContent>
215
+ </AlertDialog>
216
+ </>
217
+ );
218
+ }
@@ -0,0 +1,185 @@
1
+ "use client";
2
+
3
+ import {
4
+ type ColumnDef,
5
+ type SortingState,
6
+ flexRender,
7
+ getCoreRowModel,
8
+ getSortedRowModel,
9
+ useReactTable,
10
+ } from "@tanstack/react-table";
11
+ import { useState } from "react";
12
+ import {
13
+ Table,
14
+ TableBody,
15
+ TableCell,
16
+ TableHead,
17
+ TableHeader,
18
+ TableRow,
19
+ } from "./ui/table";
20
+ import { Button } from "./ui/button";
21
+ import { Skeleton } from "./ui/skeleton";
22
+ import { ChevronLeft, ChevronRight, ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react";
23
+ import { cn } from "../lib/utils";
24
+
25
+ export interface DataTableProps<TData, TValue> {
26
+ columns: ColumnDef<TData, TValue>[];
27
+ data: TData[];
28
+ isLoading?: boolean;
29
+ emptyMessage?: string;
30
+ total?: number;
31
+ page?: number;
32
+ onPageChange?: (page: number) => void;
33
+ pageSize?: number;
34
+ onRowClick?: (row: TData) => void;
35
+ selectedRowId?: string | number | null;
36
+ getRowId?: (row: TData) => string | number;
37
+ compact?: boolean;
38
+ }
39
+
40
+ export function DataTable<TData, TValue>({
41
+ columns,
42
+ data,
43
+ isLoading,
44
+ emptyMessage = "Nenhum registro encontrado",
45
+ total,
46
+ page = 1,
47
+ onPageChange,
48
+ pageSize = 15,
49
+ onRowClick,
50
+ selectedRowId,
51
+ getRowId,
52
+ compact,
53
+ }: DataTableProps<TData, TValue>) {
54
+ const [sorting, setSorting] = useState<SortingState>([]);
55
+
56
+ const table = useReactTable({
57
+ data,
58
+ columns,
59
+ getCoreRowModel: getCoreRowModel(),
60
+ getSortedRowModel: getSortedRowModel(),
61
+ onSortingChange: setSorting,
62
+ state: { sorting },
63
+ });
64
+
65
+ const totalPages = total ? Math.ceil(total / pageSize) : 1;
66
+ const showPagination = onPageChange && total && total > pageSize;
67
+
68
+ return (
69
+ <div className="space-y-4">
70
+ <div className="rounded-md border">
71
+ <Table>
72
+ <TableHeader>
73
+ {table.getHeaderGroups().map((headerGroup) => (
74
+ <TableRow key={headerGroup.id}>
75
+ {headerGroup.headers.map((header) => (
76
+ <TableHead
77
+ key={header.id}
78
+ className={cn(compact && "py-1.5 text-xs")}
79
+ style={{ width: header.getSize() !== 150 ? header.getSize() : undefined }}
80
+ >
81
+ {header.isPlaceholder ? null : header.column.getCanSort() ? (
82
+ <button
83
+ type="button"
84
+ className="flex items-center gap-1 hover:text-foreground -ml-1 px-1 py-0.5 rounded cursor-pointer select-none"
85
+ onClick={header.column.getToggleSortingHandler()}
86
+ >
87
+ {flexRender(header.column.columnDef.header, header.getContext())}
88
+ {{
89
+ asc: <ArrowUp className="h-3.5 w-3.5" />,
90
+ desc: <ArrowDown className="h-3.5 w-3.5" />,
91
+ }[header.column.getIsSorted() as string] ?? (
92
+ <ArrowUpDown className="h-3.5 w-3.5 text-muted-foreground/50" />
93
+ )}
94
+ </button>
95
+ ) : (
96
+ flexRender(header.column.columnDef.header, header.getContext())
97
+ )}
98
+ </TableHead>
99
+ ))}
100
+ </TableRow>
101
+ ))}
102
+ </TableHeader>
103
+ <TableBody>
104
+ {isLoading ? (
105
+ Array.from({ length: compact ? 3 : 5 }).map((_, i) => (
106
+ <TableRow key={i}>
107
+ {columns.map((_, j) => (
108
+ <TableCell key={j} className={cn(compact && "py-1.5")}>
109
+ <Skeleton className="h-4 w-full max-w-[120px]" />
110
+ </TableCell>
111
+ ))}
112
+ </TableRow>
113
+ ))
114
+ ) : table.getRowModel().rows.length === 0 ? (
115
+ <TableRow>
116
+ <TableCell
117
+ colSpan={columns.length}
118
+ className={cn(
119
+ "text-center text-muted-foreground",
120
+ compact ? "py-4" : "py-8",
121
+ )}
122
+ >
123
+ {emptyMessage}
124
+ </TableCell>
125
+ </TableRow>
126
+ ) : (
127
+ table.getRowModel().rows.map((row) => {
128
+ const rowId = getRowId ? getRowId(row.original) : undefined;
129
+ const isSelected = selectedRowId != null && rowId != null && rowId === selectedRowId;
130
+
131
+ return (
132
+ <TableRow
133
+ key={row.id}
134
+ className={cn(
135
+ onRowClick && "cursor-pointer",
136
+ isSelected && "bg-accent",
137
+ )}
138
+ onClick={() => onRowClick?.(row.original)}
139
+ >
140
+ {row.getVisibleCells().map((cell) => (
141
+ <TableCell key={cell.id} className={cn(compact && "py-1.5")}>
142
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
143
+ </TableCell>
144
+ ))}
145
+ </TableRow>
146
+ );
147
+ })
148
+ )}
149
+ </TableBody>
150
+ </Table>
151
+ </div>
152
+
153
+ {showPagination && (
154
+ <div className="flex items-center justify-between px-2">
155
+ <p className="text-sm text-muted-foreground">
156
+ {total} registro{total !== 1 ? "s" : ""}
157
+ </p>
158
+ <div className="flex items-center gap-2">
159
+ <Button
160
+ variant="outline"
161
+ size="sm"
162
+ onClick={() => onPageChange(page - 1)}
163
+ disabled={page <= 1}
164
+ >
165
+ <ChevronLeft className="h-4 w-4" />
166
+ Anterior
167
+ </Button>
168
+ <span className="text-sm text-muted-foreground">
169
+ {page} de {totalPages}
170
+ </span>
171
+ <Button
172
+ variant="outline"
173
+ size="sm"
174
+ onClick={() => onPageChange(page + 1)}
175
+ disabled={page >= totalPages}
176
+ >
177
+ Próximo
178
+ <ChevronRight className="h-4 w-4" />
179
+ </Button>
180
+ </div>
181
+ </div>
182
+ )}
183
+ </div>
184
+ );
185
+ }
@@ -0,0 +1,174 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback } from "react";
4
+ import type { Inbox, InboxMessage } from "../types";
5
+ import type { GchatHookConfig } from "../hooks/types";
6
+ import {
7
+ useInboxes,
8
+ useInboxStats,
9
+ useUpdateInbox,
10
+ useDeleteInbox,
11
+ } from "../hooks/use-inboxes";
12
+ import {
13
+ useInboxMessages,
14
+ useSendMessage,
15
+ useRetryMessage,
16
+ useRevokeMessage,
17
+ useEditMessage,
18
+ } from "../hooks/use-inbox-messages";
19
+ import { useGetContact } from "../hooks/use-contacts";
20
+ import { InboxSidebar } from "./inbox-sidebar";
21
+ import { ChatView } from "./chat-view";
22
+ import { ContactInfoPanel } from "./contact-info-panel";
23
+ import { Button } from "./ui/button";
24
+ import {
25
+ Select,
26
+ SelectContent,
27
+ SelectItem,
28
+ SelectTrigger,
29
+ SelectValue,
30
+ } from "./ui/select";
31
+ import { MessageCircle, User } from "lucide-react";
32
+
33
+ export interface InboxPageProps {
34
+ config: GchatHookConfig;
35
+ filterChannelId?: number | null;
36
+ }
37
+
38
+ export function InboxPage({ config, filterChannelId }: InboxPageProps) {
39
+ const [selectedInbox, setSelectedInbox] = useState<Inbox | null>(null);
40
+ const [showContactPanel, setShowContactPanel] = useState(false);
41
+
42
+ const { data: inboxes, isLoading: inboxesLoading } = useInboxes(config);
43
+ const { data: messages = [], isLoading: messagesLoading } =
44
+ useInboxMessages(config, selectedInbox?.id ?? null);
45
+ const { data: contact, isLoading: contactLoading } = useGetContact(
46
+ config,
47
+ showContactPanel && selectedInbox?.id_contact
48
+ ? selectedInbox.id_contact
49
+ : null,
50
+ );
51
+
52
+ const sendMessage = useSendMessage(config);
53
+ const retryMessage = useRetryMessage(config);
54
+ const revokeMessage = useRevokeMessage(config);
55
+ const editMessage = useEditMessage(config);
56
+ const updateInbox = useUpdateInbox(config);
57
+ const deleteInbox = useDeleteInbox(config);
58
+
59
+ const handleSend = useCallback(
60
+ (content: string) => {
61
+ if (!selectedInbox) return;
62
+ sendMessage.mutate({ idInbox: selectedInbox.id, content });
63
+ },
64
+ [selectedInbox, sendMessage],
65
+ );
66
+
67
+ const handleStatusChange = useCallback(
68
+ (status: "open" | "pending" | "resolved") => {
69
+ if (!selectedInbox) return;
70
+ updateInbox.mutate({ id: selectedInbox.id, body: { status } });
71
+ },
72
+ [selectedInbox, updateInbox],
73
+ );
74
+
75
+ const handleRevoke = useCallback(
76
+ (message: InboxMessage) => {
77
+ if (!selectedInbox) return;
78
+ revokeMessage.mutate({ id: message.id, idInbox: selectedInbox.id });
79
+ },
80
+ [selectedInbox, revokeMessage],
81
+ );
82
+
83
+ const handleEdit = useCallback(
84
+ (message: InboxMessage, newContent: string) => {
85
+ if (!selectedInbox) return;
86
+ editMessage.mutate({
87
+ id: message.id,
88
+ idInbox: selectedInbox.id,
89
+ content: newContent,
90
+ });
91
+ },
92
+ [selectedInbox, editMessage],
93
+ );
94
+
95
+ return (
96
+ <div className="flex h-full">
97
+ <div className="w-[360px] shrink-0 border-r flex flex-col">
98
+ <InboxSidebar
99
+ inboxes={inboxes}
100
+ isLoading={inboxesLoading}
101
+ selectedInboxId={selectedInbox?.id ?? null}
102
+ onSelectInbox={(inbox) => {
103
+ setSelectedInbox(inbox);
104
+ setShowContactPanel(false);
105
+ }}
106
+ onDeleteInbox={(id) => deleteInbox.mutate(id)}
107
+ filterChannelId={filterChannelId}
108
+ />
109
+ </div>
110
+
111
+ <div className="flex-1 flex min-w-0">
112
+ {selectedInbox ? (
113
+ <>
114
+ <div className="flex-1 flex flex-col min-w-0">
115
+ <ChatView
116
+ messages={messages}
117
+ isLoading={messagesLoading}
118
+ onSend={handleSend}
119
+ onRetry={retryMessage}
120
+ onRevoke={handleRevoke}
121
+ onEdit={handleEdit}
122
+ renderHeader={
123
+ <div className="flex items-center justify-between border-b px-4 py-3">
124
+ <span className="text-sm font-medium">
125
+ {selectedInbox.contact_name || "Conversa"}
126
+ </span>
127
+ <div className="flex items-center gap-2">
128
+ <Select
129
+ value={selectedInbox.status}
130
+ onValueChange={handleStatusChange}
131
+ >
132
+ <SelectTrigger className="w-[130px] h-8 text-xs">
133
+ <SelectValue placeholder="Status" />
134
+ </SelectTrigger>
135
+ <SelectContent>
136
+ <SelectItem value="open">Aberta</SelectItem>
137
+ <SelectItem value="pending">Pendente</SelectItem>
138
+ <SelectItem value="resolved">Resolvida</SelectItem>
139
+ </SelectContent>
140
+ </Select>
141
+ {selectedInbox.id_contact && (
142
+ <Button
143
+ variant="ghost"
144
+ size="icon"
145
+ className="h-8 w-8"
146
+ onClick={() => setShowContactPanel((v) => !v)}
147
+ >
148
+ <User className="h-4 w-4" />
149
+ </Button>
150
+ )}
151
+ </div>
152
+ </div>
153
+ }
154
+ />
155
+ </div>
156
+
157
+ {showContactPanel && selectedInbox.id_contact && (
158
+ <ContactInfoPanel
159
+ contact={contact ?? null}
160
+ isLoading={contactLoading}
161
+ onClose={() => setShowContactPanel(false)}
162
+ />
163
+ )}
164
+ </>
165
+ ) : (
166
+ <div className="flex-1 flex flex-col items-center justify-center text-muted-foreground gap-3">
167
+ <MessageCircle className="h-12 w-12 opacity-20" />
168
+ <p className="text-sm">Selecione uma conversa para começar</p>
169
+ </div>
170
+ )}
171
+ </div>
172
+ </div>
173
+ );
174
+ }
@@ -0,0 +1,94 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../../lib/utils"
4
+
5
+ function Card({
6
+ className,
7
+ size = "default",
8
+ ...props
9
+ }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
10
+ return (
11
+ <div
12
+ data-slot="card"
13
+ data-size={size}
14
+ className={cn("ring-foreground/10 bg-card text-card-foreground gap-6 overflow-hidden rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+
20
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
21
+ return (
22
+ <div
23
+ data-slot="card-header"
24
+ className={cn(
25
+ "gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
26
+ className
27
+ )}
28
+ {...props}
29
+ />
30
+ )
31
+ }
32
+
33
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
34
+ return (
35
+ <div
36
+ data-slot="card-title"
37
+ className={cn("text-base leading-normal font-medium group-data-[size=sm]/card:text-sm", className)}
38
+ {...props}
39
+ />
40
+ )
41
+ }
42
+
43
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
44
+ return (
45
+ <div
46
+ data-slot="card-description"
47
+ className={cn("text-muted-foreground text-sm", className)}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="card-action"
57
+ className={cn(
58
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
67
+ return (
68
+ <div
69
+ data-slot="card-content"
70
+ className={cn("px-6 group-data-[size=sm]/card:px-4", className)}
71
+ {...props}
72
+ />
73
+ )
74
+ }
75
+
76
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
77
+ return (
78
+ <div
79
+ data-slot="card-footer"
80
+ className={cn("rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center", className)}
81
+ {...props}
82
+ />
83
+ )
84
+ }
85
+
86
+ export {
87
+ Card,
88
+ CardHeader,
89
+ CardFooter,
90
+ CardTitle,
91
+ CardAction,
92
+ CardDescription,
93
+ CardContent,
94
+ }
@@ -0,0 +1,24 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Label as LabelPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "../../lib/utils"
7
+
8
+ function Label({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
12
+ return (
13
+ <LabelPrimitive.Root
14
+ data-slot="label"
15
+ className={cn(
16
+ "gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ export { Label }