@better-zap/react 0.0.1

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.mjs ADDED
@@ -0,0 +1,578 @@
1
+ import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
2
+ import { clsx } from "clsx";
3
+ import { twMerge } from "tailwind-merge";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { HugeiconsIcon } from "@hugeicons/react";
6
+ import { Add01Icon, ArrowLeft02Icon, Clock01Icon, InformationCircleIcon, Message01Icon, Mic01Icon, Search01Icon, Sent02Icon, SmileIcon, UserIcon } from "@hugeicons/core-free-icons";
7
+ import { cva } from "class-variance-authority";
8
+ //#region src/react/utils.ts
9
+ function cn(...inputs) {
10
+ return twMerge(clsx(inputs));
11
+ }
12
+ //#endregion
13
+ //#region src/react/whatsapp-dashboard.tsx
14
+ const MOBILE_BREAKPOINT = 1024;
15
+ function useIsMobile() {
16
+ const [isMobile, setIsMobile] = useState(false);
17
+ useEffect(() => {
18
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
19
+ const onChange = (e) => {
20
+ setIsMobile(e.matches);
21
+ };
22
+ onChange(mql);
23
+ mql.addEventListener("change", onChange);
24
+ return () => mql.removeEventListener("change", onChange);
25
+ }, []);
26
+ return isMobile;
27
+ }
28
+ const WhatsappDashboardContext = createContext(null);
29
+ function useWhatsappDashboard() {
30
+ const ctx = useContext(WhatsappDashboardContext);
31
+ if (!ctx) throw new Error("useWhatsappDashboard must be used within <WhatsappDashboard>");
32
+ return ctx;
33
+ }
34
+ function WhatsappDashboard({ children, className, defaultMobileView = "list", ...props }) {
35
+ const isMobile = useIsMobile();
36
+ const [mobileView, setMobileView] = useState(defaultMobileView);
37
+ const handleSetMobileView = useCallback((view) => {
38
+ setMobileView(view);
39
+ }, []);
40
+ return /* @__PURE__ */ jsx(WhatsappDashboardContext.Provider, {
41
+ value: {
42
+ isMobile,
43
+ mobileView,
44
+ setMobileView: handleSetMobileView
45
+ },
46
+ children: /* @__PURE__ */ jsx("div", {
47
+ className: cn("bg-background flex h-full w-full overflow-hidden", className),
48
+ ...props,
49
+ children
50
+ })
51
+ });
52
+ }
53
+ //#endregion
54
+ //#region src/react/message-bubble.tsx
55
+ const bubbleVariants = cva("relative shadow-[0_1px_0.5px_rgba(11,20,26,0.13)] rounded-lg px-3 py-2 max-w-[65%]", { variants: { variant: {
56
+ outgoing: "bg-green-100 text-green-900 rounded-tr-none",
57
+ incoming: "bg-gray-100 text-gray-900 rounded-tl-none",
58
+ failed: "bg-red-100 text-red-900 rounded-tr-none border border-red-200"
59
+ } } });
60
+ const statusVariants = cva("text-[13px] leading-none", {
61
+ variants: { variant: {
62
+ default: "opacity-60",
63
+ read: "text-blue-500",
64
+ failed: "text-red-500"
65
+ } },
66
+ defaultVariants: { variant: "default" }
67
+ });
68
+ function MessageBubble({ content, sender, timestamp, status, templateName, label, className, ...props }) {
69
+ const isIncoming = sender === "user";
70
+ const isFailed = status === "failed";
71
+ const bubbleVariant = isIncoming ? "incoming" : isFailed ? "failed" : "outgoing";
72
+ const statusVariant = status === "read" ? "read" : isFailed ? "failed" : "default";
73
+ const displayContent = content || (templateName ? `[Template: ${templateName}]` : "[Conteúdo não disponível]");
74
+ return /* @__PURE__ */ jsx("div", {
75
+ className: cn("flex w-full mb-1", isIncoming ? "justify-start" : "justify-end", className),
76
+ ...props,
77
+ children: /* @__PURE__ */ jsxs("div", {
78
+ className: bubbleVariants({ variant: bubbleVariant }),
79
+ children: [
80
+ label && !isIncoming && /* @__PURE__ */ jsx("span", {
81
+ className: "mb-1 block text-xs font-medium opacity-70",
82
+ children: label
83
+ }),
84
+ templateName && !label && /* @__PURE__ */ jsx("div", {
85
+ className: "mb-1 border-b border-black/5 pb-1",
86
+ children: /* @__PURE__ */ jsxs("span", {
87
+ className: "text-[11px] font-medium opacity-70",
88
+ children: ["📋 ", templateName]
89
+ })
90
+ }),
91
+ /* @__PURE__ */ jsxs("div", {
92
+ className: "text-[14.5px] leading-normal whitespace-pre-wrap break-words select-text",
93
+ children: [
94
+ /* @__PURE__ */ jsx(FormattedMessage, { text: displayContent }),
95
+ /* @__PURE__ */ jsxs("div", {
96
+ className: "float-right -mb-1 ml-2 mt-1.5 flex items-center justify-end gap-1 shrink-0 h-[15px] select-none",
97
+ children: [timestamp && /* @__PURE__ */ jsx("span", {
98
+ className: "text-[11px] opacity-60 leading-none whitespace-nowrap",
99
+ children: timestamp
100
+ }), !isIncoming && status && /* @__PURE__ */ jsx("span", {
101
+ className: statusVariants({ variant: statusVariant }),
102
+ children: status === "read" || status === "delivered" ? "✓✓" : isFailed ? "✕" : "✓"
103
+ })]
104
+ }),
105
+ /* @__PURE__ */ jsx("div", { className: "clear-both" })
106
+ ]
107
+ })
108
+ ]
109
+ })
110
+ });
111
+ }
112
+ function FormattedMessage({ text }) {
113
+ if (!text) return null;
114
+ return /* @__PURE__ */ jsx(Fragment, { children: text.split(/(\n)/g).map((line, lineIndex) => {
115
+ if (line === "\n") return /* @__PURE__ */ jsx("br", {}, lineIndex);
116
+ if (!line) return null;
117
+ const parts = line.split(/(```[\s\S]*?```|`[^`]+`|\*[^\s*](?:[^*]*[^\s*])?\*|_[^\s_](?:[^_]*[^\s_])?_|~[^\s~](?:[^~]*[^\s~])?~|https?:\/\/[^\s]+)/g);
118
+ return /* @__PURE__ */ jsx(React.Fragment, { children: parts.map((part, index) => {
119
+ if (!part) return null;
120
+ if (part.startsWith("```") && part.endsWith("```")) return /* @__PURE__ */ jsx("code", {
121
+ className: "my-1 block whitespace-pre-wrap rounded bg-black/5 px-1 py-0.5 font-mono text-[13px]",
122
+ children: part.slice(3, -3)
123
+ }, index);
124
+ if (part.startsWith("`") && part.endsWith("`")) return /* @__PURE__ */ jsx("code", {
125
+ className: "rounded bg-black/5 px-1 font-mono text-[13px] text-[#df0165]",
126
+ children: part.slice(1, -1)
127
+ }, index);
128
+ if (part.startsWith("*") && part.endsWith("*")) return /* @__PURE__ */ jsx("strong", {
129
+ className: "font-bold",
130
+ children: part.slice(1, -1)
131
+ }, index);
132
+ if (part.startsWith("_") && part.endsWith("_")) return /* @__PURE__ */ jsx("em", {
133
+ className: "italic",
134
+ children: part.slice(1, -1)
135
+ }, index);
136
+ if (part.startsWith("~") && part.endsWith("~")) return /* @__PURE__ */ jsx("del", {
137
+ className: "text-gray-500 line-through",
138
+ children: part.slice(1, -1)
139
+ }, index);
140
+ if (part.match(/^https?:\/\/[^\s]+$/)) return /* @__PURE__ */ jsx("a", {
141
+ href: part,
142
+ target: "_blank",
143
+ rel: "noopener noreferrer",
144
+ className: "text-[#027eb5] hover:underline",
145
+ children: part
146
+ }, index);
147
+ return /* @__PURE__ */ jsx("span", { children: part }, index);
148
+ }) }, lineIndex);
149
+ }) });
150
+ }
151
+ //#endregion
152
+ //#region src/date.ts
153
+ function getDisplayDate(dateStr) {
154
+ const dateObj = new Date(dateStr);
155
+ const now = /* @__PURE__ */ new Date();
156
+ const isToday = dateObj.toDateString() === now.toDateString();
157
+ const yesterday = new Date(now);
158
+ yesterday.setDate(yesterday.getDate() - 1);
159
+ const isYesterday = dateObj.toDateString() === yesterday.toDateString();
160
+ if (isToday) return "HOJE";
161
+ else if (isYesterday) return "ONTEM";
162
+ else return dateObj.toLocaleDateString("pt-BR", {
163
+ day: "2-digit",
164
+ month: "2-digit",
165
+ year: "numeric"
166
+ });
167
+ }
168
+ //#endregion
169
+ //#region src/react/message-view.tsx
170
+ function MessageView({ children, className, ...props }) {
171
+ const { isMobile, mobileView } = useWhatsappDashboard();
172
+ const hasContent = React.Children.count(children) > 0;
173
+ const isVisible = !isMobile || mobileView === "chat";
174
+ if (!hasContent) {
175
+ if (isMobile) return null;
176
+ return /* @__PURE__ */ jsx(MessageViewEmpty, {
177
+ className,
178
+ ...props
179
+ });
180
+ }
181
+ return /* @__PURE__ */ jsxs("div", {
182
+ className: cn("relative flex flex-1 flex-col bg-[#efeae2]", className),
183
+ style: isVisible ? void 0 : { display: "none" },
184
+ ...props,
185
+ children: [/* @__PURE__ */ jsx("div", { style: {
186
+ position: "absolute",
187
+ backgroundImage: `url(${new URL("./wpp-bg.webp", import.meta.url).href})`,
188
+ backgroundRepeat: "repeat",
189
+ opacity: .15,
190
+ inset: 0
191
+ } }), /* @__PURE__ */ jsx("div", {
192
+ className: "relative flex flex-1 flex-col min-h-0",
193
+ children
194
+ })]
195
+ });
196
+ }
197
+ function MessageViewHeader({ conversation, onBack, onInfoClick, className }) {
198
+ const { isMobile, setMobileView } = useWhatsappDashboard();
199
+ const handleBack = () => {
200
+ setMobileView("list");
201
+ onBack?.();
202
+ };
203
+ return /* @__PURE__ */ jsxs("div", {
204
+ className: cn("flex h-16 shrink-0 items-center justify-between border-b bg-[#f0f2f5] px-4 z-20", className),
205
+ children: [/* @__PURE__ */ jsxs("div", {
206
+ className: "flex items-center gap-3",
207
+ children: [isMobile && /* @__PURE__ */ jsx("button", {
208
+ type: "button",
209
+ className: "inline-flex h-9 w-9 items-center justify-center rounded-md hover:bg-black/5",
210
+ onClick: handleBack,
211
+ children: /* @__PURE__ */ jsx(HugeiconsIcon, {
212
+ icon: ArrowLeft02Icon,
213
+ size: 20
214
+ })
215
+ }), /* @__PURE__ */ jsxs("div", {
216
+ className: "flex flex-col",
217
+ children: [/* @__PURE__ */ jsx("h2", {
218
+ className: "text-[15px] font-medium text-[#111b21] leading-tight",
219
+ children: conversation.contactName || conversation.phone
220
+ }), conversation.contactName && /* @__PURE__ */ jsx("span", {
221
+ className: "text-sm text-[#667781] leading-tight",
222
+ children: conversation.phone
223
+ })]
224
+ })]
225
+ }), /* @__PURE__ */ jsx("button", {
226
+ type: "button",
227
+ className: "inline-flex h-9 w-9 items-center justify-center rounded-md hover:bg-black/5",
228
+ onClick: onInfoClick,
229
+ children: /* @__PURE__ */ jsx(HugeiconsIcon, {
230
+ icon: InformationCircleIcon,
231
+ size: 20
232
+ })
233
+ })]
234
+ });
235
+ }
236
+ const SCROLL_TOP_THRESHOLD = 50;
237
+ function MessageViewContent({ children, autoScroll = true, onScrollTop, className, ...props }) {
238
+ const scrollRef = useRef(null);
239
+ const prevScrollHeightRef = useRef(0);
240
+ useEffect(() => {
241
+ if (autoScroll && scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
242
+ }, [children, autoScroll]);
243
+ useEffect(() => {
244
+ const el = scrollRef.current;
245
+ if (!el) return;
246
+ const newScrollHeight = el.scrollHeight;
247
+ const prevScrollHeight = prevScrollHeightRef.current;
248
+ if (prevScrollHeight > 0 && newScrollHeight > prevScrollHeight) {
249
+ const addedHeight = newScrollHeight - prevScrollHeight;
250
+ if (el.scrollTop < SCROLL_TOP_THRESHOLD + addedHeight) el.scrollTop = addedHeight;
251
+ }
252
+ prevScrollHeightRef.current = newScrollHeight;
253
+ });
254
+ const handleScroll = useCallback(() => {
255
+ const el = scrollRef.current;
256
+ if (!el || !onScrollTop) return;
257
+ if (el.scrollTop <= SCROLL_TOP_THRESHOLD) onScrollTop();
258
+ }, [onScrollTop]);
259
+ return /* @__PURE__ */ jsx("div", {
260
+ ref: scrollRef,
261
+ className: cn("flex flex-1 flex-col overflow-y-auto p-4 pb-0 chat-scrollbar", className),
262
+ onScroll: handleScroll,
263
+ ...props,
264
+ children
265
+ });
266
+ }
267
+ function MessageList({ messages, renderMessageLabel, className }) {
268
+ const messageGroups = useMemo(() => {
269
+ const groups = [];
270
+ let currentGroup = null;
271
+ messages.forEach((msg) => {
272
+ const displayDate = getDisplayDate(msg.sentAt);
273
+ if (!currentGroup || currentGroup.date !== displayDate) {
274
+ currentGroup = {
275
+ date: displayDate,
276
+ messages: []
277
+ };
278
+ groups.push(currentGroup);
279
+ }
280
+ currentGroup.messages.push(msg);
281
+ });
282
+ return groups;
283
+ }, [messages]);
284
+ return /* @__PURE__ */ jsx("div", {
285
+ className: cn("flex flex-col pb-4", className),
286
+ children: messageGroups.map((group) => /* @__PURE__ */ jsxs("div", {
287
+ className: "flex flex-col gap-1",
288
+ children: [/* @__PURE__ */ jsx(DateDivider, { date: group.date }), group.messages.map((msg) => /* @__PURE__ */ jsx(MessageBubble, {
289
+ content: msg.content || "",
290
+ sender: msg.direction === "incoming" ? "user" : "bot",
291
+ timestamp: new Date(msg.sentAt).toLocaleTimeString("pt-BR", {
292
+ hour: "2-digit",
293
+ minute: "2-digit"
294
+ }),
295
+ status: msg.status,
296
+ templateName: msg.templateName || void 0,
297
+ label: renderMessageLabel?.(msg)
298
+ }, msg.id))]
299
+ }, group.date))
300
+ });
301
+ }
302
+ function DateDivider({ date }) {
303
+ return /* @__PURE__ */ jsx("div", {
304
+ className: "sticky top-0 z-10 flex justify-center w-full py-2 pointer-events-none",
305
+ children: /* @__PURE__ */ jsx("span", {
306
+ className: "bg-white border border-[#e9edef] shadow-[0_1px_0.5px_rgba(11,20,26,0.13)] text-[#54656f] text-[12.5px] font-medium px-3 py-1.5 rounded-lg uppercase pointer-events-auto",
307
+ children: date
308
+ })
309
+ });
310
+ }
311
+ function MessageViewEmpty({ className, ...props }) {
312
+ return /* @__PURE__ */ jsxs("div", {
313
+ className: cn("relative flex flex-1 flex-col items-center justify-center bg-[#f8f9fa] text-[#667781] p-6", className),
314
+ ...props,
315
+ children: [
316
+ /* @__PURE__ */ jsx("div", {
317
+ className: "mb-8 flex h-48 w-48 items-center justify-center rounded-full bg-[#f0f2f5] shadow-sm",
318
+ children: /* @__PURE__ */ jsx(HugeiconsIcon, {
319
+ icon: Message01Icon,
320
+ size: 80,
321
+ className: "text-[#bbc5cb]"
322
+ })
323
+ }),
324
+ /* @__PURE__ */ jsx("h1", {
325
+ className: "mb-3 text-2xl font-semibold text-[#41525d]",
326
+ children: "Better Zap"
327
+ }),
328
+ /* @__PURE__ */ jsxs("div", {
329
+ className: "max-w-sm space-y-3 text-center",
330
+ children: [/* @__PURE__ */ jsxs("p", {
331
+ className: "text-[15px] leading-relaxed",
332
+ children: [
333
+ "Esta é uma interface dedicada para visualização e monitoramento de mensagens da ",
334
+ /* @__PURE__ */ jsx("strong", { children: "API Oficial do WhatsApp" }),
335
+ "."
336
+ ]
337
+ }), /* @__PURE__ */ jsx("p", {
338
+ className: "text-sm opacity-80",
339
+ children: "Acompanhe o histórico de conversas, verifique o status de entrega e gerencie as interações do Cloud API de forma profissional."
340
+ })]
341
+ })
342
+ ]
343
+ });
344
+ }
345
+ //#endregion
346
+ //#region src/react/conversation-search.tsx
347
+ function ConversationSearch({ value, onChange, className }) {
348
+ return /* @__PURE__ */ jsx("div", {
349
+ className: cn("border-b border-[#e9edef] shrink-0 p-2", className),
350
+ children: /* @__PURE__ */ jsxs("div", {
351
+ className: "flex items-center bg-[#f0f2f5] rounded-full px-4 h-[38px] gap-3 focus-within:bg-white focus-within:ring-1 focus-within:ring-green-200 focus-within:shadow-md transition-all border border-transparent focus-within:border-transparent",
352
+ children: [/* @__PURE__ */ jsx(HugeiconsIcon, {
353
+ icon: Search01Icon,
354
+ size: 18,
355
+ className: "text-[#54656f] shrink-0"
356
+ }), /* @__PURE__ */ jsx("input", {
357
+ type: "text",
358
+ placeholder: "Buscar conversa",
359
+ value,
360
+ onChange: (e) => onChange(e.target.value),
361
+ className: "flex-1 border-none bg-transparent text-[15px] text-[#111b21] focus:outline-none h-full placeholder:text-[#667781]"
362
+ })]
363
+ })
364
+ });
365
+ }
366
+ //#endregion
367
+ //#region src/react/conversation-list.tsx
368
+ function ConversationList({ conversations, isLoading, isError, selectedConversationId, onSelect, className }) {
369
+ const { isMobile, mobileView, setMobileView } = useWhatsappDashboard();
370
+ const [search, setSearch] = useState("");
371
+ const filtered = conversations.filter((c) => c.phone.includes(search) || c.contactName?.toLowerCase().includes(search.toLowerCase()));
372
+ const handleSelect = (id) => {
373
+ onSelect(id);
374
+ setMobileView("chat");
375
+ };
376
+ const isVisible = !isMobile || mobileView === "list";
377
+ return /* @__PURE__ */ jsxs("div", {
378
+ className: cn("flex flex-col h-full bg-white border-r border-[#e9edef]", isMobile ? "w-full" : "min-w-[320px] max-w-105", className),
379
+ style: isVisible ? void 0 : { display: "none" },
380
+ children: [/* @__PURE__ */ jsx(ConversationSearch, {
381
+ value: search,
382
+ onChange: setSearch
383
+ }), /* @__PURE__ */ jsx("div", {
384
+ className: "flex-1 overflow-y-auto overflow-x-hidden chat-scrollbar",
385
+ children: isLoading ? /* @__PURE__ */ jsx("div", {
386
+ className: "flex items-center justify-center h-full text-sm text-[#667781]",
387
+ children: "Carregando..."
388
+ }) : isError ? /* @__PURE__ */ jsx("div", {
389
+ className: "flex items-center justify-center h-full text-sm text-red-500",
390
+ children: "Erro ao carregar conversas"
391
+ }) : filtered.length === 0 ? /* @__PURE__ */ jsxs("div", {
392
+ className: "flex flex-col items-center justify-center h-full gap-2 text-[#667781]",
393
+ children: [/* @__PURE__ */ jsx(HugeiconsIcon, {
394
+ icon: Message01Icon,
395
+ size: 32
396
+ }), /* @__PURE__ */ jsx("p", {
397
+ className: "text-sm",
398
+ children: "Nenhuma conversa encontrada"
399
+ })]
400
+ }) : filtered.map((conversation) => /* @__PURE__ */ jsx(ConversationItem, {
401
+ conversation,
402
+ isSelected: selectedConversationId === conversation.id,
403
+ onClick: () => handleSelect(conversation.id)
404
+ }, conversation.id))
405
+ })]
406
+ });
407
+ }
408
+ function ConversationItem({ conversation, isSelected, onClick }) {
409
+ return /* @__PURE__ */ jsxs("button", {
410
+ onClick,
411
+ "data-selected": isSelected,
412
+ className: "group flex items-center w-full h-[72px] px-3 gap-3 transition-all cursor-pointer text-left relative overflow-hidden hover:bg-[#f5f6f6] data-[selected=true]:bg-[#075e54] data-[selected=true]:hover:bg-[#064940]",
413
+ children: [/* @__PURE__ */ jsx("div", {
414
+ className: "w-[49px] h-[49px] rounded-full bg-[#dfe5e7] flex items-center justify-center shrink-0 group-data-[selected=true]:bg-white/20",
415
+ children: /* @__PURE__ */ jsx(HugeiconsIcon, {
416
+ icon: UserIcon,
417
+ size: 28,
418
+ className: "text-[#aebac1] group-data-[selected=true]:text-white/80"
419
+ })
420
+ }), /* @__PURE__ */ jsxs("div", {
421
+ className: "flex-1 min-w-0 border-b border-[#f2f2f2] h-full flex flex-col justify-center pr-1 group-last:border-none group-data-[selected=true]:border-transparent",
422
+ children: [/* @__PURE__ */ jsxs("div", {
423
+ className: "flex justify-between items-baseline mb-0.5",
424
+ children: [/* @__PURE__ */ jsx("span", {
425
+ className: "text-[17px] font-normal text-[#111b21] truncate group-data-[selected=true]:text-white",
426
+ children: conversation.contactName || formatPhone(conversation.phone)
427
+ }), /* @__PURE__ */ jsx("span", {
428
+ className: "text-xs text-[#667781] shrink-0 group-data-[selected=true]:text-white/90",
429
+ children: formatTime(conversation.lastMessageAt)
430
+ })]
431
+ }), /* @__PURE__ */ jsxs("div", {
432
+ className: "flex justify-between items-center gap-2",
433
+ children: [/* @__PURE__ */ jsxs("p", {
434
+ className: "text-[14px] text-[#667781] truncate group-data-[selected=true]:text-white/90",
435
+ children: [conversation.lastDirection === "incoming" ? "" : "Você: ", conversation.lastMessagePreview || "Sem mensagem"]
436
+ }), conversation.unreadCount > 0 && /* @__PURE__ */ jsx("span", {
437
+ className: "bg-[#25d366] text-white text-[11px] font-bold rounded-full min-w-[20px] h-[20px] flex items-center justify-center px-1.5 shrink-0 group-data-[selected=true]:bg-white",
438
+ children: conversation.unreadCount
439
+ })]
440
+ })]
441
+ })]
442
+ });
443
+ }
444
+ function formatPhone(phone) {
445
+ if (phone.length === 13 && phone.startsWith("55")) return `(${phone.slice(2, 4)}) ${phone.slice(4, 9)}-${phone.slice(9)}`;
446
+ return phone;
447
+ }
448
+ function formatTime(dateStr) {
449
+ try {
450
+ const date = new Date(dateStr);
451
+ const now = /* @__PURE__ */ new Date();
452
+ if (date.toDateString() === now.toDateString()) return date.toLocaleTimeString("pt-BR", {
453
+ hour: "2-digit",
454
+ minute: "2-digit"
455
+ });
456
+ const yesterday = new Date(now);
457
+ yesterday.setDate(yesterday.getDate() - 1);
458
+ if (date.toDateString() === yesterday.toDateString()) return "Ontem";
459
+ return date.toLocaleDateString("pt-BR", {
460
+ day: "2-digit",
461
+ month: "2-digit"
462
+ });
463
+ } catch {
464
+ return "";
465
+ }
466
+ }
467
+ //#endregion
468
+ //#region src/react/message-input.tsx
469
+ function MessageInput({ onSend, disabled, placeholder = "Digite uma mensagem", className, contextWindowOpen = true }) {
470
+ const [text, setText] = useState("");
471
+ const [isSending, setIsSending] = useState(false);
472
+ const textareaRef = useRef(null);
473
+ const isDisabled = disabled || isSending || !contextWindowOpen;
474
+ const handleSend = async () => {
475
+ const trimmedText = text.trim();
476
+ if (!trimmedText || isDisabled) return;
477
+ setIsSending(true);
478
+ try {
479
+ await onSend(trimmedText);
480
+ setText("");
481
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
482
+ } finally {
483
+ setIsSending(false);
484
+ }
485
+ };
486
+ const handleKeyDown = (e) => {
487
+ if (e.key === "Enter" && !e.shiftKey) {
488
+ e.preventDefault();
489
+ handleSend();
490
+ }
491
+ };
492
+ useEffect(() => {
493
+ if (textareaRef.current) {
494
+ textareaRef.current.style.height = "auto";
495
+ textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
496
+ }
497
+ }, [text]);
498
+ const hasText = text.trim().length > 0;
499
+ if (!contextWindowOpen) return /* @__PURE__ */ jsx("div", {
500
+ className: cn("w-full flex flex-col p-4 z-10 shrink-0", className),
501
+ children: /* @__PURE__ */ jsxs("div", {
502
+ className: "flex items-center gap-2 px-4 py-3 min-h-[62px] bg-[#fef3c7] rounded-2xl border border-[#f59e0b]/20",
503
+ children: [/* @__PURE__ */ jsx(HugeiconsIcon, {
504
+ icon: Clock01Icon,
505
+ size: 20,
506
+ className: "text-[#92400e] shrink-0"
507
+ }), /* @__PURE__ */ jsx("span", {
508
+ className: "text-[13px] leading-[18px] text-[#92400e]",
509
+ children: "A janela de 24h expirou. Mensagens de texto livre só podem ser enviadas dentro de 24h após a última mensagem do contato."
510
+ })]
511
+ })
512
+ });
513
+ return /* @__PURE__ */ jsx("div", {
514
+ className: cn("w-full flex flex-col p-4 z-10 shrink-0", className),
515
+ children: /* @__PURE__ */ jsxs("div", {
516
+ className: "flex items-center gap-1 px-2 py-1 min-h-[62px] bg-white rounded-2xl shadow-lg border border-gray-100",
517
+ children: [
518
+ /* @__PURE__ */ jsxs("div", {
519
+ className: "flex items-center gap-1 text-[#54656f] shrink-0",
520
+ children: [/* @__PURE__ */ jsx("button", {
521
+ type: "button",
522
+ "aria-label": "Emojis",
523
+ className: "p-2 rounded-full hover:bg-black/5 transition-colors",
524
+ title: "Emojis",
525
+ children: /* @__PURE__ */ jsx(HugeiconsIcon, {
526
+ icon: SmileIcon,
527
+ size: 24
528
+ })
529
+ }), /* @__PURE__ */ jsx("button", {
530
+ type: "button",
531
+ "aria-label": "Anexar arquivo",
532
+ className: "p-2 rounded-full hover:bg-black/5 transition-colors",
533
+ title: "Anexar",
534
+ children: /* @__PURE__ */ jsx(HugeiconsIcon, {
535
+ icon: Add01Icon,
536
+ size: 24
537
+ })
538
+ })]
539
+ }),
540
+ /* @__PURE__ */ jsx("div", {
541
+ className: "flex-1 flex items-center min-h-[42px] py-3 px-2",
542
+ children: /* @__PURE__ */ jsx("textarea", {
543
+ ref: textareaRef,
544
+ className: "flex-1 bg-transparent border-none text-[15px] leading-[22px] text-[#111b21] resize-none focus:outline-none max-h-[120px] placeholder:text-[#8696a0]",
545
+ name: "message",
546
+ "aria-label": "Mensagem",
547
+ autoComplete: "off",
548
+ placeholder: isSending ? "Enviando..." : placeholder,
549
+ value: text,
550
+ onChange: (e) => setText(e.target.value),
551
+ onKeyDown: handleKeyDown,
552
+ rows: 1,
553
+ disabled: isDisabled
554
+ })
555
+ }),
556
+ /* @__PURE__ */ jsx("div", {
557
+ className: "flex items-center pr-1 shrink-0",
558
+ children: /* @__PURE__ */ jsx("button", {
559
+ type: "button",
560
+ "aria-label": hasText ? "Enviar" : "Gravar áudio",
561
+ className: cn("p-2 rounded-full transition-colors disabled:opacity-40 disabled:cursor-not-allowed", hasText ? "text-[#00a884]" : "text-[#54656f] hover:bg-black/5"),
562
+ onClick: hasText ? handleSend : void 0,
563
+ disabled: isDisabled,
564
+ children: hasText ? /* @__PURE__ */ jsx(HugeiconsIcon, {
565
+ icon: Sent02Icon,
566
+ size: 24
567
+ }) : /* @__PURE__ */ jsx(HugeiconsIcon, {
568
+ icon: Mic01Icon,
569
+ size: 24
570
+ })
571
+ })
572
+ })
573
+ ]
574
+ })
575
+ });
576
+ }
577
+ //#endregion
578
+ export { ConversationItem, ConversationList, FormattedMessage, MessageBubble, MessageInput, MessageList, MessageView, MessageViewContent, MessageViewEmpty, MessageViewHeader, WhatsappDashboard, cn, useWhatsappDashboard };
@@ -0,0 +1,11 @@
1
+ /*
2
+ * Tailwind CSS source manifest for @better-zap/react components.
3
+ *
4
+ * Consuming apps should import this file (e.g. `@import "@better-zap/react/tailwind.css"`)
5
+ * so that Tailwind's JIT scanner picks up every utility class used by the components.
6
+ *
7
+ * The `@source "."` directive tells Tailwind to scan all recognised file types
8
+ * in this directory, and the path resolves relative to THIS file — so it works
9
+ * regardless of where the consuming app lives in the monorepo.
10
+ */
11
+ @source ".";
Binary file
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@better-zap/react",
3
+ "version": "0.0.1",
4
+ "description": "React components for Better Zap.",
5
+ "license": "ISC",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.mjs",
9
+ "types": "./dist/index.d.mts",
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.mts",
18
+ "import": "./dist/index.mjs",
19
+ "require": "./dist/index.cjs"
20
+ },
21
+ "./tailwind.css": "./dist/tailwind.css",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "sideEffects": [
25
+ "./dist/tailwind.css"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@hugeicons/core-free-icons": "^3.1.1",
35
+ "@hugeicons/react": "^1.1.4",
36
+ "class-variance-authority": "^0.7.1",
37
+ "clsx": "^2.1.1",
38
+ "tailwind-merge": "^3.0.0",
39
+ "better-zap": "0.0.1"
40
+ },
41
+ "peerDependencies": {
42
+ "react": "^19.0.0",
43
+ "react-dom": "^19.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/react": "^19.0.0",
47
+ "@types/react-dom": "^19.0.0",
48
+ "tsdown": "^0.21.2",
49
+ "typescript": "^5.9.3",
50
+ "vitest": "^4.1.0"
51
+ },
52
+ "scripts": {
53
+ "build": "tsdown",
54
+ "typecheck": "tsc --noEmit",
55
+ "test": "vitest run --passWithNoTests"
56
+ }
57
+ }