@greatapps/greatchat-ui 0.1.0 → 0.1.2
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.
- package/dist/index.d.ts +283 -1
- package/dist/index.js +1472 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/components/chat-input.tsx +54 -0
- package/src/components/chat-view.tsx +135 -0
- package/src/components/index.ts +8 -0
- package/src/components/message-bubble.tsx +394 -0
- package/src/components/ui/alert-dialog.tsx +167 -0
- package/src/components/ui/badge.tsx +44 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/dropdown-menu.tsx +173 -0
- package/src/components/ui/select.tsx +156 -0
- package/src/components/ui/skeleton.tsx +16 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/hooks/index.ts +14 -0
- package/src/hooks/types.ts +40 -0
- package/src/hooks/use-channels.ts +163 -0
- package/src/hooks/use-contacts.ts +94 -0
- package/src/hooks/use-inbox-messages.ts +405 -0
- package/src/hooks/use-inboxes.ts +127 -0
- package/src/index.ts +8 -0
- package/src/utils/format-date.ts +13 -0
- package/src/utils/group-messages.ts +22 -0
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { InboxMessage } from "../types";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
import { formatMessageTime } from "../utils/format-date";
|
|
5
|
+
import {
|
|
6
|
+
Check,
|
|
7
|
+
CheckCheck,
|
|
8
|
+
AlertCircle,
|
|
9
|
+
Clock,
|
|
10
|
+
Video,
|
|
11
|
+
File,
|
|
12
|
+
MapPin,
|
|
13
|
+
Headphones,
|
|
14
|
+
RotateCcw,
|
|
15
|
+
MoreHorizontal,
|
|
16
|
+
Trash2,
|
|
17
|
+
Pencil,
|
|
18
|
+
Ban,
|
|
19
|
+
} from "lucide-react";
|
|
20
|
+
import {
|
|
21
|
+
DropdownMenu,
|
|
22
|
+
DropdownMenuContent,
|
|
23
|
+
DropdownMenuItem,
|
|
24
|
+
DropdownMenuTrigger,
|
|
25
|
+
} from "./ui/dropdown-menu";
|
|
26
|
+
import {
|
|
27
|
+
AlertDialog,
|
|
28
|
+
AlertDialogAction,
|
|
29
|
+
AlertDialogCancel,
|
|
30
|
+
AlertDialogContent,
|
|
31
|
+
AlertDialogDescription,
|
|
32
|
+
AlertDialogFooter,
|
|
33
|
+
AlertDialogHeader,
|
|
34
|
+
AlertDialogTitle,
|
|
35
|
+
} from "./ui/alert-dialog";
|
|
36
|
+
import { Button } from "./ui/button";
|
|
37
|
+
import { Textarea } from "./ui/textarea";
|
|
38
|
+
|
|
39
|
+
const statusIcons: Record<string, React.ReactNode> = {
|
|
40
|
+
pending: <Clock className="h-3 w-3 animate-pulse" />,
|
|
41
|
+
sent: <Check className="h-3 w-3" />,
|
|
42
|
+
delivered: <CheckCheck className="h-3 w-3" />,
|
|
43
|
+
read: <CheckCheck className="h-3 w-3 text-blue-500" />,
|
|
44
|
+
failed: <AlertCircle className="h-3 w-3 text-destructive" />,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function parseMetadata(metadata: string | null): Record<string, unknown> {
|
|
48
|
+
if (!metadata) return {};
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(metadata);
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function MediaContent({ message }: { message: InboxMessage }) {
|
|
57
|
+
switch (message.content_type) {
|
|
58
|
+
case "image":
|
|
59
|
+
return (
|
|
60
|
+
<div className="space-y-1">
|
|
61
|
+
{message.content_url && (
|
|
62
|
+
<img
|
|
63
|
+
src={message.content_url}
|
|
64
|
+
alt="Imagem"
|
|
65
|
+
className="max-w-[240px] rounded-md"
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
{message.content && <p className="text-sm">{message.content}</p>}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
case "audio":
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex items-center gap-2">
|
|
74
|
+
<Headphones className="h-4 w-4" />
|
|
75
|
+
<span className="text-sm">Mensagem de áudio</span>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
case "video":
|
|
79
|
+
return (
|
|
80
|
+
<div className="flex items-center gap-2">
|
|
81
|
+
<Video className="h-4 w-4" />
|
|
82
|
+
<span className="text-sm">Vídeo</span>
|
|
83
|
+
{message.content && <p className="text-sm">{message.content}</p>}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
case "document": {
|
|
87
|
+
const meta = parseMetadata(message.metadata);
|
|
88
|
+
return (
|
|
89
|
+
<div className="flex items-center gap-2">
|
|
90
|
+
<File className="h-4 w-4" />
|
|
91
|
+
<span className="text-sm">
|
|
92
|
+
{(meta.filename as string) || "Documento"}
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
case "location":
|
|
98
|
+
return (
|
|
99
|
+
<div className="flex items-center gap-2">
|
|
100
|
+
<MapPin className="h-4 w-4" />
|
|
101
|
+
<span className="text-sm">Localização</span>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
default:
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface MessageBubbleProps {
|
|
110
|
+
message: InboxMessage;
|
|
111
|
+
onRetry?: (message: InboxMessage) => void;
|
|
112
|
+
onRevoke?: (message: InboxMessage) => void;
|
|
113
|
+
onEdit?: (message: InboxMessage, newContent: string) => void;
|
|
114
|
+
renderActions?: (message: InboxMessage) => React.ReactNode;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function MessageBubble({
|
|
118
|
+
message,
|
|
119
|
+
onRetry,
|
|
120
|
+
onRevoke,
|
|
121
|
+
onEdit,
|
|
122
|
+
renderActions,
|
|
123
|
+
}: MessageBubbleProps) {
|
|
124
|
+
const [showRevokeDialog, setShowRevokeDialog] = useState(false);
|
|
125
|
+
const [editing, setEditing] = useState(false);
|
|
126
|
+
const [editContent, setEditContent] = useState(message.content || "");
|
|
127
|
+
|
|
128
|
+
const isOutbound = message.direction === "outbound";
|
|
129
|
+
const isPending = message.status === "pending";
|
|
130
|
+
const isFailed = message.status === "failed";
|
|
131
|
+
const time = formatMessageTime(message.datetime_add);
|
|
132
|
+
|
|
133
|
+
const meta = parseMetadata(message.metadata);
|
|
134
|
+
const isRevoked = meta.revoked === true;
|
|
135
|
+
const isEdited = meta.edited === true;
|
|
136
|
+
|
|
137
|
+
const agentLabel =
|
|
138
|
+
isOutbound && message.source !== "contact"
|
|
139
|
+
? (meta.agent_name as string) ||
|
|
140
|
+
(message.source === "bot" ? "Bot" : "Agente")
|
|
141
|
+
: null;
|
|
142
|
+
|
|
143
|
+
const canAct =
|
|
144
|
+
isOutbound &&
|
|
145
|
+
message.id > 0 &&
|
|
146
|
+
!isPending &&
|
|
147
|
+
!isFailed &&
|
|
148
|
+
!isRevoked &&
|
|
149
|
+
!!message.external_id;
|
|
150
|
+
|
|
151
|
+
const canEdit = canAct && message.content_type === "text";
|
|
152
|
+
|
|
153
|
+
function handleSaveEdit() {
|
|
154
|
+
const trimmed = editContent.trim();
|
|
155
|
+
if (!trimmed || trimmed === message.content) {
|
|
156
|
+
setEditing(false);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
onEdit?.(message, trimmed);
|
|
160
|
+
setEditing(false);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (isRevoked) {
|
|
164
|
+
return (
|
|
165
|
+
<div
|
|
166
|
+
className={cn("flex", isOutbound ? "justify-end" : "justify-start")}
|
|
167
|
+
>
|
|
168
|
+
<div className="max-w-[75%]">
|
|
169
|
+
<div
|
|
170
|
+
className={cn(
|
|
171
|
+
"rounded-lg px-3 py-2",
|
|
172
|
+
isOutbound ? "bg-primary/40" : "bg-muted/60",
|
|
173
|
+
)}
|
|
174
|
+
>
|
|
175
|
+
<div className="flex items-center gap-1.5">
|
|
176
|
+
<Ban
|
|
177
|
+
className={cn(
|
|
178
|
+
"h-3.5 w-3.5",
|
|
179
|
+
isOutbound
|
|
180
|
+
? "text-primary-foreground/50"
|
|
181
|
+
: "text-muted-foreground",
|
|
182
|
+
)}
|
|
183
|
+
/>
|
|
184
|
+
<p
|
|
185
|
+
className={cn(
|
|
186
|
+
"text-sm italic",
|
|
187
|
+
isOutbound
|
|
188
|
+
? "text-primary-foreground/50"
|
|
189
|
+
: "text-muted-foreground",
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
Mensagem apagada
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
<div
|
|
196
|
+
className={cn(
|
|
197
|
+
"mt-1 flex items-center justify-end gap-1",
|
|
198
|
+
isOutbound
|
|
199
|
+
? "text-primary-foreground/40"
|
|
200
|
+
: "text-muted-foreground/60",
|
|
201
|
+
)}
|
|
202
|
+
>
|
|
203
|
+
<span className="text-[10px]">{time}</span>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<div
|
|
213
|
+
className={cn(
|
|
214
|
+
"flex group",
|
|
215
|
+
isOutbound ? "justify-end" : "justify-start",
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
<div className="max-w-[75%] relative">
|
|
219
|
+
{canAct && (
|
|
220
|
+
<div className="absolute -top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity z-10">
|
|
221
|
+
<DropdownMenu>
|
|
222
|
+
<DropdownMenuTrigger asChild>
|
|
223
|
+
<button className="h-6 w-6 rounded-full bg-background/80 shadow-sm flex items-center justify-center hover:bg-background">
|
|
224
|
+
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
|
|
225
|
+
</button>
|
|
226
|
+
</DropdownMenuTrigger>
|
|
227
|
+
<DropdownMenuContent align="end" className="w-40">
|
|
228
|
+
{canEdit && (
|
|
229
|
+
<DropdownMenuItem
|
|
230
|
+
onSelect={(e) => {
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
setEditing(true);
|
|
233
|
+
setEditContent(message.content || "");
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
<Pencil className="h-4 w-4 mr-2" />
|
|
237
|
+
Editar
|
|
238
|
+
</DropdownMenuItem>
|
|
239
|
+
)}
|
|
240
|
+
<DropdownMenuItem
|
|
241
|
+
onSelect={(e) => {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
setShowRevokeDialog(true);
|
|
244
|
+
}}
|
|
245
|
+
className="text-destructive focus:text-destructive"
|
|
246
|
+
>
|
|
247
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
248
|
+
Apagar para todos
|
|
249
|
+
</DropdownMenuItem>
|
|
250
|
+
</DropdownMenuContent>
|
|
251
|
+
</DropdownMenu>
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
|
|
255
|
+
{renderActions?.(message)}
|
|
256
|
+
|
|
257
|
+
<div
|
|
258
|
+
className={cn(
|
|
259
|
+
"rounded-lg px-3 py-2",
|
|
260
|
+
isOutbound ? "bg-primary text-primary-foreground" : "bg-muted",
|
|
261
|
+
isPending && "opacity-70",
|
|
262
|
+
isFailed && "bg-destructive/10 border border-destructive/30",
|
|
263
|
+
)}
|
|
264
|
+
>
|
|
265
|
+
{agentLabel && (
|
|
266
|
+
<p
|
|
267
|
+
className={cn(
|
|
268
|
+
"mb-0.5 text-[10px] font-medium",
|
|
269
|
+
isFailed
|
|
270
|
+
? "text-destructive/70"
|
|
271
|
+
: isOutbound
|
|
272
|
+
? "text-primary-foreground/70"
|
|
273
|
+
: "text-muted-foreground",
|
|
274
|
+
)}
|
|
275
|
+
>
|
|
276
|
+
{agentLabel}
|
|
277
|
+
</p>
|
|
278
|
+
)}
|
|
279
|
+
|
|
280
|
+
{editing ? (
|
|
281
|
+
<div className="space-y-2">
|
|
282
|
+
<Textarea
|
|
283
|
+
value={editContent}
|
|
284
|
+
onChange={(e) => setEditContent(e.target.value)}
|
|
285
|
+
rows={2}
|
|
286
|
+
className="min-h-[36px] max-h-[120px] resize-none bg-background text-foreground text-sm"
|
|
287
|
+
autoFocus
|
|
288
|
+
onKeyDown={(e) => {
|
|
289
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
290
|
+
e.preventDefault();
|
|
291
|
+
handleSaveEdit();
|
|
292
|
+
}
|
|
293
|
+
if (e.key === "Escape") {
|
|
294
|
+
setEditing(false);
|
|
295
|
+
}
|
|
296
|
+
}}
|
|
297
|
+
/>
|
|
298
|
+
<div className="flex justify-end gap-1.5">
|
|
299
|
+
<Button
|
|
300
|
+
size="sm"
|
|
301
|
+
variant="ghost"
|
|
302
|
+
className="h-6 text-xs px-2"
|
|
303
|
+
onClick={() => setEditing(false)}
|
|
304
|
+
>
|
|
305
|
+
Cancelar
|
|
306
|
+
</Button>
|
|
307
|
+
<Button
|
|
308
|
+
size="sm"
|
|
309
|
+
className="h-6 text-xs px-2"
|
|
310
|
+
onClick={handleSaveEdit}
|
|
311
|
+
disabled={
|
|
312
|
+
!editContent.trim() ||
|
|
313
|
+
editContent.trim() === message.content
|
|
314
|
+
}
|
|
315
|
+
>
|
|
316
|
+
Salvar
|
|
317
|
+
</Button>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
) : message.content_type === "text" ? (
|
|
321
|
+
<p
|
|
322
|
+
className={cn(
|
|
323
|
+
"text-sm whitespace-pre-wrap break-words",
|
|
324
|
+
isFailed && "text-foreground",
|
|
325
|
+
)}
|
|
326
|
+
>
|
|
327
|
+
{message.content}
|
|
328
|
+
</p>
|
|
329
|
+
) : (
|
|
330
|
+
<MediaContent message={message} />
|
|
331
|
+
)}
|
|
332
|
+
|
|
333
|
+
{!editing && (
|
|
334
|
+
<div
|
|
335
|
+
className={cn(
|
|
336
|
+
"mt-1 flex items-center justify-end gap-1",
|
|
337
|
+
isFailed
|
|
338
|
+
? "text-destructive"
|
|
339
|
+
: isOutbound
|
|
340
|
+
? "text-primary-foreground/60"
|
|
341
|
+
: "text-muted-foreground",
|
|
342
|
+
)}
|
|
343
|
+
>
|
|
344
|
+
{isEdited && (
|
|
345
|
+
<span className="text-[10px] italic">editada</span>
|
|
346
|
+
)}
|
|
347
|
+
<span className="text-[10px]">{time}</span>
|
|
348
|
+
{isOutbound && statusIcons[message.status]}
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
{isFailed && (
|
|
354
|
+
<div className="mt-1 flex items-center gap-1.5 justify-end px-1">
|
|
355
|
+
<AlertCircle className="h-3 w-3 shrink-0 text-destructive" />
|
|
356
|
+
<span className="text-[11px] text-destructive">
|
|
357
|
+
{message._error || "Erro ao enviar"}
|
|
358
|
+
</span>
|
|
359
|
+
{onRetry && (
|
|
360
|
+
<button
|
|
361
|
+
onClick={() => onRetry(message)}
|
|
362
|
+
className="inline-flex items-center gap-0.5 text-[11px] text-destructive font-medium hover:text-destructive/80 hover:underline cursor-pointer ml-1"
|
|
363
|
+
>
|
|
364
|
+
<RotateCcw className="h-3 w-3" />
|
|
365
|
+
Reenviar
|
|
366
|
+
</button>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<AlertDialog open={showRevokeDialog} onOpenChange={setShowRevokeDialog}>
|
|
373
|
+
<AlertDialogContent>
|
|
374
|
+
<AlertDialogHeader>
|
|
375
|
+
<AlertDialogTitle>Apagar mensagem?</AlertDialogTitle>
|
|
376
|
+
<AlertDialogDescription>
|
|
377
|
+
A mensagem será apagada para todos no WhatsApp. Ela continuará
|
|
378
|
+
visível aqui como "mensagem apagada".
|
|
379
|
+
</AlertDialogDescription>
|
|
380
|
+
</AlertDialogHeader>
|
|
381
|
+
<AlertDialogFooter>
|
|
382
|
+
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
|
383
|
+
<AlertDialogAction
|
|
384
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
385
|
+
onClick={() => onRevoke?.(message)}
|
|
386
|
+
>
|
|
387
|
+
Apagar para todos
|
|
388
|
+
</AlertDialogAction>
|
|
389
|
+
</AlertDialogFooter>
|
|
390
|
+
</AlertDialogContent>
|
|
391
|
+
</AlertDialog>
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
import { Button } from "./button";
|
|
6
|
+
|
|
7
|
+
function AlertDialog({
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
|
10
|
+
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function AlertDialogTrigger({
|
|
14
|
+
...props
|
|
15
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
|
16
|
+
return (
|
|
17
|
+
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function AlertDialogPortal({
|
|
22
|
+
...props
|
|
23
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
|
24
|
+
return (
|
|
25
|
+
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function AlertDialogOverlay({
|
|
30
|
+
className,
|
|
31
|
+
...props
|
|
32
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
|
33
|
+
return (
|
|
34
|
+
<AlertDialogPrimitive.Overlay
|
|
35
|
+
data-slot="alert-dialog-overlay"
|
|
36
|
+
className={cn(
|
|
37
|
+
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50",
|
|
38
|
+
className,
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function AlertDialogContent({
|
|
46
|
+
className,
|
|
47
|
+
...props
|
|
48
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
|
49
|
+
return (
|
|
50
|
+
<AlertDialogPortal>
|
|
51
|
+
<AlertDialogOverlay />
|
|
52
|
+
<AlertDialogPrimitive.Content
|
|
53
|
+
data-slot="alert-dialog-content"
|
|
54
|
+
className={cn(
|
|
55
|
+
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 gap-6 rounded-xl p-6 ring-1 duration-100 sm:max-w-lg fixed top-1/2 left-1/2 z-50 grid w-full max-w-xs -translate-x-1/2 -translate-y-1/2 outline-none",
|
|
56
|
+
className,
|
|
57
|
+
)}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
</AlertDialogPortal>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function AlertDialogHeader({
|
|
65
|
+
className,
|
|
66
|
+
...props
|
|
67
|
+
}: React.ComponentProps<"div">) {
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
data-slot="alert-dialog-header"
|
|
71
|
+
className={cn("grid gap-1.5 text-center sm:text-left", className)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function AlertDialogFooter({
|
|
78
|
+
className,
|
|
79
|
+
...props
|
|
80
|
+
}: React.ComponentProps<"div">) {
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
data-slot="alert-dialog-footer"
|
|
84
|
+
className={cn(
|
|
85
|
+
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
86
|
+
className,
|
|
87
|
+
)}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function AlertDialogTitle({
|
|
94
|
+
className,
|
|
95
|
+
...props
|
|
96
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
|
97
|
+
return (
|
|
98
|
+
<AlertDialogPrimitive.Title
|
|
99
|
+
data-slot="alert-dialog-title"
|
|
100
|
+
className={cn("text-lg font-medium", className)}
|
|
101
|
+
{...props}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function AlertDialogDescription({
|
|
107
|
+
className,
|
|
108
|
+
...props
|
|
109
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
|
110
|
+
return (
|
|
111
|
+
<AlertDialogPrimitive.Description
|
|
112
|
+
data-slot="alert-dialog-description"
|
|
113
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
114
|
+
{...props}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function AlertDialogAction({
|
|
120
|
+
className,
|
|
121
|
+
variant = "default",
|
|
122
|
+
size = "default",
|
|
123
|
+
...props
|
|
124
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
|
|
125
|
+
Partial<Pick<React.ComponentProps<typeof Button>, "variant" | "size">>) {
|
|
126
|
+
return (
|
|
127
|
+
<Button variant={variant} size={size} asChild>
|
|
128
|
+
<AlertDialogPrimitive.Action
|
|
129
|
+
data-slot="alert-dialog-action"
|
|
130
|
+
className={cn(className)}
|
|
131
|
+
{...props}
|
|
132
|
+
/>
|
|
133
|
+
</Button>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function AlertDialogCancel({
|
|
138
|
+
className,
|
|
139
|
+
variant = "outline",
|
|
140
|
+
size = "default",
|
|
141
|
+
...props
|
|
142
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
|
|
143
|
+
Partial<Pick<React.ComponentProps<typeof Button>, "variant" | "size">>) {
|
|
144
|
+
return (
|
|
145
|
+
<Button variant={variant} size={size} asChild>
|
|
146
|
+
<AlertDialogPrimitive.Cancel
|
|
147
|
+
data-slot="alert-dialog-cancel"
|
|
148
|
+
className={cn(className)}
|
|
149
|
+
{...props}
|
|
150
|
+
/>
|
|
151
|
+
</Button>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export {
|
|
156
|
+
AlertDialog,
|
|
157
|
+
AlertDialogAction,
|
|
158
|
+
AlertDialogCancel,
|
|
159
|
+
AlertDialogContent,
|
|
160
|
+
AlertDialogDescription,
|
|
161
|
+
AlertDialogFooter,
|
|
162
|
+
AlertDialogHeader,
|
|
163
|
+
AlertDialogOverlay,
|
|
164
|
+
AlertDialogPortal,
|
|
165
|
+
AlertDialogTitle,
|
|
166
|
+
AlertDialogTrigger,
|
|
167
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { Slot } from "radix-ui";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none overflow-hidden",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground",
|
|
13
|
+
secondary: "bg-secondary text-secondary-foreground",
|
|
14
|
+
destructive:
|
|
15
|
+
"bg-destructive/10 text-destructive dark:bg-destructive/20",
|
|
16
|
+
outline: "border-border text-foreground",
|
|
17
|
+
ghost: "hover:bg-muted hover:text-muted-foreground",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
variant: "default",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function Badge({
|
|
27
|
+
className,
|
|
28
|
+
variant = "default",
|
|
29
|
+
asChild = false,
|
|
30
|
+
...props
|
|
31
|
+
}: React.ComponentProps<"span"> &
|
|
32
|
+
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
33
|
+
const Comp = asChild ? Slot.Root : "span";
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Comp
|
|
37
|
+
data-slot="badge"
|
|
38
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { Slot } from "radix-ui";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border border-transparent text-sm font-medium focus-visible:ring-3 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none select-none",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
13
|
+
outline:
|
|
14
|
+
"border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 shadow-xs",
|
|
15
|
+
secondary:
|
|
16
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
17
|
+
ghost:
|
|
18
|
+
"hover:bg-muted hover:text-foreground dark:hover:bg-muted/50",
|
|
19
|
+
destructive:
|
|
20
|
+
"bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:bg-destructive/20 text-destructive",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-9 gap-1.5 px-2.5",
|
|
25
|
+
xs: "h-6 gap-1 px-2 text-xs [&_svg:not([class*='size-'])]:size-3",
|
|
26
|
+
sm: "h-8 gap-1 px-2.5",
|
|
27
|
+
lg: "h-10 gap-1.5 px-2.5",
|
|
28
|
+
icon: "size-9",
|
|
29
|
+
"icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
|
|
30
|
+
"icon-sm": "size-8",
|
|
31
|
+
"icon-lg": "size-10",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: {
|
|
35
|
+
variant: "default",
|
|
36
|
+
size: "default",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
function Button({
|
|
42
|
+
className,
|
|
43
|
+
variant = "default",
|
|
44
|
+
size = "default",
|
|
45
|
+
asChild = false,
|
|
46
|
+
...props
|
|
47
|
+
}: React.ComponentProps<"button"> &
|
|
48
|
+
VariantProps<typeof buttonVariants> & {
|
|
49
|
+
asChild?: boolean;
|
|
50
|
+
}) {
|
|
51
|
+
const Comp = asChild ? Slot.Root : "button";
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Comp
|
|
55
|
+
data-slot="button"
|
|
56
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { Button, buttonVariants };
|