@erlancarreira/evolution-chat 0.1.7 → 0.1.8
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/package.json +19 -5
- package/widget/index.cjs +24 -2
- package/widget/index.cjs.map +1 -1
- package/widget/index.js +24 -2
- package/widget/index.js.map +1 -1
- package/widget-embed/evolution-chat.iife.js +24 -2
package/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erlancarreira/evolution-chat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Chat widget + Evolution API bridge (WhatsApp) — reutilizável, hexagonal, SOLID",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./api/index.js",
|
|
8
8
|
"types": "./api/index.d.ts",
|
|
9
|
-
"files": [
|
|
9
|
+
"files": [
|
|
10
|
+
"api",
|
|
11
|
+
"bridge",
|
|
12
|
+
"next",
|
|
13
|
+
"transports",
|
|
14
|
+
"widget",
|
|
15
|
+
"widget-embed",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
10
18
|
"scripts": {
|
|
11
19
|
"build": "tsup",
|
|
12
20
|
"test": "vitest run",
|
|
@@ -19,9 +27,15 @@
|
|
|
19
27
|
"react-dom": ">=18.0.0"
|
|
20
28
|
},
|
|
21
29
|
"peerDependenciesMeta": {
|
|
22
|
-
"@supabase/supabase-js": {
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
"@supabase/supabase-js": {
|
|
31
|
+
"optional": true
|
|
32
|
+
},
|
|
33
|
+
"react": {
|
|
34
|
+
"optional": true
|
|
35
|
+
},
|
|
36
|
+
"react-dom": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
25
39
|
},
|
|
26
40
|
"devDependencies": {
|
|
27
41
|
"@testing-library/dom": "^10.4.0",
|
package/widget/index.cjs
CHANGED
|
@@ -976,14 +976,18 @@ function PreChatForm({ welcome, tr, error, sending, firstFieldRef, onSubmit }) {
|
|
|
976
976
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", children: tr("privacyNote") })
|
|
977
977
|
] });
|
|
978
978
|
}
|
|
979
|
-
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping,
|
|
979
|
+
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, onTyping, onSend, onRetry, onNewConversation }) {
|
|
980
980
|
const uid = (0, import_react2.useId)();
|
|
981
981
|
const [draft, setDraft] = (0, import_react2.useState)("");
|
|
982
982
|
const typingRef = (0, import_react2.useRef)(false);
|
|
983
983
|
const typingTimer = (0, import_react2.useRef)(null);
|
|
984
|
+
const selfTypingRef = (0, import_react2.useRef)(false);
|
|
985
|
+
const selfTimer = (0, import_react2.useRef)(null);
|
|
986
|
+
const [selfTyping, setSelfTyping] = (0, import_react2.useState)(false);
|
|
984
987
|
(0, import_react2.useEffect)(() => {
|
|
985
988
|
return () => {
|
|
986
989
|
if (typingTimer.current !== null) clearTimeout(typingTimer.current);
|
|
990
|
+
if (selfTimer.current !== null) clearTimeout(selfTimer.current);
|
|
987
991
|
};
|
|
988
992
|
}, []);
|
|
989
993
|
const stopTypingSignal = (0, import_react2.useCallback)(() => {
|
|
@@ -996,6 +1000,16 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
|
|
|
996
1000
|
void onTyping(false);
|
|
997
1001
|
}
|
|
998
1002
|
}, [onTyping]);
|
|
1003
|
+
const stopSelfTyping = (0, import_react2.useCallback)(() => {
|
|
1004
|
+
if (selfTimer.current !== null) {
|
|
1005
|
+
clearTimeout(selfTimer.current);
|
|
1006
|
+
selfTimer.current = null;
|
|
1007
|
+
}
|
|
1008
|
+
if (selfTypingRef.current) {
|
|
1009
|
+
selfTypingRef.current = false;
|
|
1010
|
+
setSelfTyping(false);
|
|
1011
|
+
}
|
|
1012
|
+
}, []);
|
|
999
1013
|
const handleDraftChange = (e) => {
|
|
1000
1014
|
setDraft(e.target.value);
|
|
1001
1015
|
if (!canCompose) return;
|
|
@@ -1005,15 +1019,23 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
|
|
|
1005
1019
|
}
|
|
1006
1020
|
if (typingTimer.current !== null) clearTimeout(typingTimer.current);
|
|
1007
1021
|
typingTimer.current = setTimeout(stopTypingSignal, 2500);
|
|
1022
|
+
if (!selfTypingRef.current) {
|
|
1023
|
+
selfTypingRef.current = true;
|
|
1024
|
+
setSelfTyping(true);
|
|
1025
|
+
}
|
|
1026
|
+
if (selfTimer.current !== null) clearTimeout(selfTimer.current);
|
|
1027
|
+
selfTimer.current = setTimeout(stopSelfTyping, 2500);
|
|
1008
1028
|
};
|
|
1009
1029
|
const handleDraftBlur = () => {
|
|
1010
1030
|
stopTypingSignal();
|
|
1031
|
+
stopSelfTyping();
|
|
1011
1032
|
};
|
|
1012
1033
|
const submit = (e) => {
|
|
1013
1034
|
e.preventDefault();
|
|
1014
1035
|
const text = draft.trim();
|
|
1015
1036
|
if (text === "" || !canCompose) return;
|
|
1016
1037
|
stopTypingSignal();
|
|
1038
|
+
stopSelfTyping();
|
|
1017
1039
|
setDraft("");
|
|
1018
1040
|
void onSend(text);
|
|
1019
1041
|
};
|
|
@@ -1032,7 +1054,7 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
|
|
|
1032
1054
|
] }, m.id)
|
|
1033
1055
|
),
|
|
1034
1056
|
ownerTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { from: "owner", label: tr("typing") }),
|
|
1035
|
-
|
|
1057
|
+
selfTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { from: "visitor", label: tr("typing") })
|
|
1036
1058
|
] }),
|
|
1037
1059
|
!canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-closed", children: [
|
|
1038
1060
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", role: "status", children: tr("sessionClosed") }),
|
package/widget/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/widget/index.ts","../src/widget/chat-widget.tsx","../src/widget/i18n.ts","../src/widget/styles.ts","../src/widget/use-chat.ts"],"sourcesContent":["// src/widget/index.ts — superfície pública do subpath \"@erlancarreira/evolution-chat/widget\".\n//\n// O consumidor (LMS) faz:\n// import { ChatWidget } from \"@erlancarreira/evolution-chat/widget\";\n// import type { ChatWidgetProps } from \"@erlancarreira/evolution-chat/widget\";\n// <ChatWidget endpoint=\"/api/chat\" locale=\"pt\" welcome=\"Oi!\" projectName=\"Aulivra\" realtime={supabaseRealtime} />\n//\n// O CSS é auto-injetado (`injectWidgetStyles` roda no mount do ChatWidget); quem quiser\n// servir por `<link>` importa o subpath \"./widget/styles.css\".\n\nexport { ChatWidget } from \"./chat-widget\";\nexport type { ChatWidgetProps, WidgetLocale } from \"./chat-widget\";\n\nexport { injectWidgetStyles, WIDGET_CSS } from \"./styles\";\n\nexport {\n WIDGET_KEYS,\n dictionaries,\n en,\n es,\n pt,\n t,\n} from \"./i18n\";\nexport type { WidgetDictionary, WidgetKey } from \"./i18n\";\n\nexport {\n isValidPhone,\n maskPhone,\n normalizePhone,\n useChat,\n SESSION_STORAGE_KEY,\n POLL_INTERVAL_MS,\n MIN_PHONE_DIGITS,\n} from \"./use-chat\";\nexport type { ChatPhase, ChatState, SessionInfo, UseChatOptions, UseChatResult } from \"./use-chat\";\n\n// Porta injetada — reexportada para o consumidor não descer ao subpath ./bridge.\nexport type { RealtimeHandle } from \"../bridge/types\";\n","// src/widget/chat-widget.tsx — ChatWidget: balão flutuante + painel de conversa.\n//\n// Sem Tailwind / lib de UI: toda a aparência vem de `injectWidgetStyles()` (classes\n// `.ecw-*`), e o único estilo inline é a CSS var `--ecw-accent` (cor do tema). O estado\n// inteiro vive em `use-chat.ts` (reducer + rede + realtime); aqui só render e foco.\n//\n// Acessibilidade (contrato da spec §2):\n// - balão: <button> com aria-label (i18n), aria-expanded e aria-haspopup=\"dialog\";\n// - painel: role=\"dialog\" aria-label=\"Chat\"; foco no primeiro campo ao abrir;\n// ESC fecha e devolve o foco ao balão; clique fora NÃO fecha (mobile-friendly);\n// - todo input tem <label htmlFor> (useId → múltiplas instâncias sem colisão);\n// - badge de não lidas refletido no nome acessível do balão;\n// - sessão closed/failed: composer desabilitado, aviso em role=\"status\" e botão real\n// \"nova conversa\" (o beco sem saída tem saída acessível por teclado);\n// - prefers-reduced-motion desliga animações (via CSS).\n\nimport {\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type CSSProperties,\n type ChangeEvent,\n type FormEvent,\n type ReactElement,\n type RefObject,\n} from \"react\";\nimport type { RealtimeHandle } from \"../bridge/types\";\nimport type { ChatMessage } from \"../types\";\nimport { t, type WidgetKey, type WidgetLocale } from \"./i18n\";\nimport { injectWidgetStyles } from \"./styles\";\nimport { isValidPhone, maskPhone, useChat } from \"./use-chat\";\n\nexport type { WidgetLocale };\n\nexport interface ChatWidgetProps {\n /** Caminho da rota de chat (GET histórico / POST start+send), ex.: \"/api/chat\". */\n endpoint: string;\n locale: WidgetLocale;\n /** Saudação exibida no pré-chat form. */\n welcome: string;\n projectName: string;\n /** Cor de tema; default \"#25D366\". */\n accentColor?: string;\n /** Porta do widget (DI — Strategy): subscribe/unsubscribe do canal da sessão. */\n realtime: RealtimeHandle;\n /** Override pontual de copy por chave i18n. */\n labels?: Partial<Record<string, string>>;\n}\n\nconst DEFAULT_ACCENT = \"#25D366\";\n\nfunction ChatIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M21 11.5a8.38 8.38 0 0 1-8.5 8.5c-1.5 0-2.9-.36-4.13-1L3 20l1.1-3.85A8.36 8.36 0 0 1 12.5 3 8.38 8.38 0 0 1 21 11.5z\" />\n </svg>\n );\n}\n\nfunction SendIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M22 2 11 13\" />\n <path d=\"M22 2 15 22l-4-9-9-4 20-7z\" />\n </svg>\n );\n}\n\nfunction formatTime(iso: string, locale: WidgetLocale): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return \"\";\n return new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" }).format(date);\n}\n\nfunction StatusMark({ status }: { status: ChatMessage[\"status\"] }): ReactElement {\n // ✓ = aceita pelo servidor (pending), ✓✓ = enviada, ⚠ = falhou.\n // Decorativo por design: o conjunto de chaves i18n é fixo (15) e o estado \"failed\" já\n // é anunciado de forma acessível pelo botão visível `retry` ao lado — um texto sr-only\n // reaproveitando \"send\"/\"sending\" soaria a ruído para quem usa leitor de tela.\n const glyph = status === \"pending\" ? \"✓\" : status === \"sent\" ? \"✓✓\" : \"⚠\";\n return (\n <span className={status === \"failed\" ? \"ecw-status ecw-status--failed\" : \"ecw-status\"} aria-hidden=\"true\">\n {glyph}\n </span>\n );\n}\n\n/** \"3 pontinhos\" de digitação: bolha com três pontos animados + rótulo acessível.\n * `from` alinha o indicador ao lado de quem representa (owner→esquerda, visitor→direita). */\nfunction TypingIndicator({ from, label }: { from: \"owner\" | \"visitor\"; label: string }): ReactElement {\n return (\n <li className={`ecw-item ecw-item--${from}`} aria-live=\"polite\">\n <div className=\"ecw-typing\" role=\"status\" aria-label={label}>\n <span className=\"ecw-typing-dot\" />\n <span className=\"ecw-typing-dot\" />\n <span className=\"ecw-typing-dot\" />\n <span className=\"ecw-sr-only\">{label}</span>\n </div>\n </li>\n );\n}\n\ninterface FormProps {\n welcome: string;\n tr: (k: WidgetKey) => string;\n error: string | null;\n sending: boolean;\n firstFieldRef: RefObject<HTMLInputElement>;\n onSubmit(values: { name: string; phone: string; message: string; honeypot: string }): Promise<void>;\n}\n\nfunction PreChatForm({ welcome, tr, error, sending, firstFieldRef, onSubmit }: FormProps): ReactElement {\n const uid = useId();\n const [name, setName] = useState(\"\");\n const [phone, setPhone] = useState(\"\");\n const [message, setMessage] = useState(\"\");\n const [honeypot, setHoneypot] = useState(\"\");\n const [phoneTouched, setPhoneTouched] = useState(false);\n\n const phoneValid = isValidPhone(phone);\n const complete = name.trim() !== \"\" && phoneValid && message.trim() !== \"\";\n\n const handleSubmit = (e: FormEvent): void => {\n e.preventDefault();\n if (!complete || sending) return;\n void onSubmit({ name, phone, message, honeypot });\n };\n\n return (\n <form className=\"ecw-form\" onSubmit={handleSubmit} noValidate>\n <p className=\"ecw-welcome\">{welcome}</p>\n <p className=\"ecw-notice\">{tr(\"welcomeNotice\")}</p>\n\n <div className=\"ecw-field\">\n <label className=\"ecw-label\" htmlFor={`${uid}-name`}>{tr(\"name\")}</label>\n <input\n id={`${uid}-name`}\n ref={firstFieldRef}\n className=\"ecw-input\"\n type=\"text\"\n autoComplete=\"name\"\n value={name}\n onChange={(e) => { setName(e.target.value); }}\n required\n />\n </div>\n\n <div className=\"ecw-field\">\n <label className=\"ecw-label\" htmlFor={`${uid}-phone`}>{tr(\"phone\")}</label>\n <input\n id={`${uid}-phone`}\n className=\"ecw-input\"\n type=\"tel\"\n inputMode=\"tel\"\n autoComplete=\"tel\"\n placeholder=\"(11) 99999-8888\"\n value={phone}\n onChange={(e) => { setPhone(maskPhone(e.target.value)); }}\n onBlur={() => { setPhoneTouched(true); }}\n aria-invalid={phoneTouched && !phoneValid ? true : undefined}\n aria-describedby={phoneTouched && !phoneValid ? `${uid}-phone-err` : undefined}\n required\n />\n {phoneTouched && !phoneValid && (\n <p className=\"ecw-error\" id={`${uid}-phone-err`} role=\"alert\">{tr(\"invalidPhone\")}</p>\n )}\n </div>\n\n <div className=\"ecw-field\">\n <label className=\"ecw-label\" htmlFor={`${uid}-message`}>{tr(\"message\")}</label>\n <textarea\n id={`${uid}-message`}\n className=\"ecw-input\"\n rows={3}\n autoComplete=\"off\"\n value={message}\n onChange={(e) => { setMessage(e.target.value); }}\n required\n />\n </div>\n\n {/* Honeypot anti-bot: invisível para humanos (display:none via .ecw-hp),\n fora da ordem de tabulação e escondido do leitor de tela. */}\n <div className=\"ecw-hp\" aria-hidden=\"true\">\n <label htmlFor={`${uid}-website`}>Website</label>\n <input\n id={`${uid}-website`}\n name=\"website\"\n type=\"text\"\n tabIndex={-1}\n autoComplete=\"off\"\n value={honeypot}\n onChange={(e) => { setHoneypot(e.target.value); }}\n />\n </div>\n\n {error === \"sendError\" && (\n <p className=\"ecw-error\" role=\"alert\">{tr(\"sendError\")}</p>\n )}\n\n <button className=\"ecw-submit\" type=\"submit\" disabled={!complete || sending}>\n {sending ? tr(\"sending\") : tr(\"send\")}\n </button>\n <p className=\"ecw-notice\">{tr(\"privacyNote\")}</p>\n </form>\n );\n}\n\ninterface ChatPanelProps {\n messages: ChatMessage[];\n /** Só `status === \"active\"` compõe; closed/failed mostram aviso + \"nova conversa\". */\n canCompose: boolean;\n sending: boolean;\n locale: WidgetLocale;\n tr: (k: WidgetKey) => string;\n listRef: RefObject<HTMLUListElement>;\n firstFieldRef: RefObject<HTMLInputElement>;\n /** True enquanto a dona da plataforma (atendente) digita → mostra os \"3 pontinhos\". */\n ownerTyping: boolean;\n /** True enquanto o VISITANTE (esta ponta) digita → \"3 pontinhos\" do lado do visitante. */\n visitorTyping: boolean;\n /** Avisa o servidor que o visitante está/parou de digitar. Best-effort. */\n onTyping(isTyping: boolean): Promise<void>;\n onSend(text: string): Promise<void>;\n onRetry(id: string): Promise<void>;\n onNewConversation(): void;\n}\n\nfunction ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, visitorTyping, onTyping, onSend, onRetry, onNewConversation }: ChatPanelProps): ReactElement {\n const uid = useId();\n const [draft, setDraft] = useState(\"\");\n\n // Sinal \"visitante digitando\": dispara `true` só na TRANSIÇÃO para digitando (não a\n // cada tecla) e `false` após 2,5s de inatividade, ao enviar ou ao perder o foco —\n // evita um POST por caractere. `onChange` já cobre keyup, cola e IME (mais robusto\n // que keyup puro), então sincroniza com a digitação real e não com o evento bruto.\n const typingRef = useRef(false);\n const typingTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(() => {\n return () => {\n if (typingTimer.current !== null) clearTimeout(typingTimer.current);\n };\n }, []);\n\n const stopTypingSignal = useCallback((): void => {\n if (typingTimer.current !== null) {\n clearTimeout(typingTimer.current);\n typingTimer.current = null;\n }\n if (typingRef.current) {\n typingRef.current = false;\n void onTyping(false);\n }\n }, [onTyping]);\n\n const handleDraftChange = (e: ChangeEvent<HTMLInputElement>): void => {\n setDraft(e.target.value);\n if (!canCompose) return;\n if (!typingRef.current) {\n typingRef.current = true;\n void onTyping(true);\n }\n if (typingTimer.current !== null) clearTimeout(typingTimer.current);\n typingTimer.current = setTimeout(stopTypingSignal, 2500);\n };\n\n const handleDraftBlur = (): void => {\n stopTypingSignal();\n };\n\n const submit = (e: FormEvent): void => {\n e.preventDefault();\n const text = draft.trim();\n if (text === \"\" || !canCompose) return;\n stopTypingSignal();\n setDraft(\"\");\n void onSend(text);\n };\n\n return (\n <>\n <ul ref={listRef} className=\"ecw-list\">\n {messages.map((m) =>\n m.direction === \"system\" ? (\n <li key={m.id} className=\"ecw-item ecw-item--system\" role=\"status\">\n <div className=\"ecw-system\">{m.body}</div>\n </li>\n ) : (\n <li key={m.id} className={`ecw-item ecw-item--${m.direction}`}>\n <div className={`ecw-bubble ecw-bubble--${m.direction}`}>{m.body}</div>\n <div className=\"ecw-meta\">\n <time dateTime={m.createdAt}>{formatTime(m.createdAt, locale)}</time>\n {m.direction === \"visitor\" && <StatusMark status={m.status} />}\n {m.status === \"failed\" && (\n <button type=\"button\" className=\"ecw-retry\" onClick={() => { void onRetry(m.id); }}>\n {tr(\"retry\")}\n </button>\n )}\n </div>\n </li>\n ),\n )}\n {ownerTyping && canCompose && <TypingIndicator from=\"owner\" label={tr(\"typing\")} />}\n {visitorTyping && canCompose && <TypingIndicator from=\"visitor\" label={tr(\"typing\")} />}\n </ul>\n {!canCompose && (\n <div className=\"ecw-closed\">\n <p className=\"ecw-notice\" role=\"status\">{tr(\"sessionClosed\")}</p>\n <button type=\"button\" className=\"ecw-submit\" onClick={onNewConversation}>\n {tr(\"newConversation\")}\n </button>\n </div>\n )}\n <form className=\"ecw-composer\" onSubmit={submit}>\n <label className=\"ecw-sr-only\" htmlFor={`${uid}-input`}>{tr(\"message\")}</label>\n <input\n id={`${uid}-input`}\n ref={firstFieldRef}\n className=\"ecw-input\"\n type=\"text\"\n autoComplete=\"off\"\n placeholder={tr(\"message\")}\n value={draft}\n onChange={handleDraftChange}\n onBlur={handleDraftBlur}\n disabled={!canCompose}\n />\n <button className=\"ecw-send\" type=\"submit\" disabled={!canCompose || draft.trim() === \"\" || sending} aria-label={sending ? tr(\"sending\") : tr(\"send\")}>\n {sending ? <span className=\"ecw-sr-only\">{tr(\"sending\")}</span> : <SendIcon />}\n </button>\n </form>\n </>\n );\n}\n\nexport function ChatWidget(props: ChatWidgetProps): ReactElement {\n const { endpoint, locale, welcome, projectName, realtime, labels } = props;\n const accentColor = props.accentColor ?? DEFAULT_ACCENT;\n\n const tr = useCallback((key: WidgetKey): string => t(locale, key, labels), [locale, labels]);\n const { state, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping } =\n useChat({ endpoint, realtime });\n\n const bubbleRef = useRef<HTMLButtonElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n const firstFieldRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n injectWidgetStyles();\n }, []);\n\n // Foco no primeiro campo ao abrir (form ou composer).\n useEffect(() => {\n if (state.open) firstFieldRef.current?.focus();\n }, [state.open, state.phase]);\n\n // ESC fecha o painel e devolve o foco ao balão.\n useEffect(() => {\n if (!state.open) return;\n const onKeyDown = (e: KeyboardEvent): void => {\n if (e.key !== \"Escape\") return;\n closePanel();\n bubbleRef.current?.focus();\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n document.removeEventListener(\"keydown\", onKeyDown);\n };\n }, [state.open, closePanel]);\n\n // Auto-scroll para a mensagem mais recente (ou os \"3 pontinhos\" de digitação).\n useEffect(() => {\n const list = listRef.current;\n if (list !== null) list.scrollTop = list.scrollHeight;\n }, [state.messages, state.ownerTyping, state.visitorTyping, state.open]);\n\n const handleClose = useCallback((): void => {\n closePanel();\n bubbleRef.current?.focus();\n }, [closePanel]);\n\n // Só uma sessão \"active\" aceita mensagem; \"closed\"/\"failed\" mostram o aviso com a ação\n // de nova conversa (que limpa o storage e devolve o visitante ao pré-chat form).\n const canCompose = state.session?.status === \"active\";\n\n const bubbleLabel = state.unread > 0 ? `${tr(\"openChat\")} (${state.unread})` : tr(\"openChat\");\n\n return (\n <div className=\"ecw-root\" style={{ \"--ecw-accent\": accentColor } as CSSProperties}>\n <button\n ref={bubbleRef}\n type=\"button\"\n className=\"ecw-button\"\n aria-label={bubbleLabel}\n aria-expanded={state.open}\n aria-haspopup=\"dialog\"\n onClick={togglePanel}\n >\n <ChatIcon />\n {state.unread > 0 && <span className=\"ecw-badge\">{state.unread}</span>}\n </button>\n\n {state.open && (\n <section className=\"ecw-panel\" role=\"dialog\" aria-label=\"Chat\">\n <header className=\"ecw-header\">\n <span className=\"ecw-title\">{projectName}</span>\n {state.session !== null && <span className=\"ecw-code\">#{state.session.code}</span>}\n <button type=\"button\" className=\"ecw-close\" aria-label={tr(\"close\")} onClick={handleClose}>\n <span aria-hidden=\"true\">×</span>\n </button>\n </header>\n\n {state.phase === \"chat\" ? (\n <ChatPanel\n messages={state.messages}\n canCompose={canCompose}\n sending={state.sending}\n locale={locale}\n tr={tr}\n listRef={listRef}\n firstFieldRef={firstFieldRef}\n ownerTyping={state.ownerTyping}\n visitorTyping={state.visitorTyping}\n onTyping={notifyTyping}\n onSend={sendMessage}\n onRetry={retryMessage}\n onNewConversation={startNewConversation}\n />\n ) : (\n <PreChatForm\n welcome={welcome}\n tr={tr}\n error={state.error}\n sending={state.sending}\n firstFieldRef={firstFieldRef}\n onSubmit={submitForm}\n />\n )}\n\n <footer className=\"ecw-footer\">\n {tr(\"poweredBy\")}{\" \"}\n <a\n className=\"ecw-powered-link\"\n href=\"https://erlancarreira.com.br\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Erlan Carreira\n </a>\n </footer>\n </section>\n )}\n </div>\n );\n}\n","// src/widget/i18n.ts — dicionários do widget (pt/en/es) + helper `t`.\n//\n// Contrato (Task 9): as 15 chaves abaixo são OBRIGATÓRIAS em todos os idiomas — o tipo\n// `WidgetDictionary = Record<WidgetKey, string>` torna a paridade verificável pelo\n// compilador, e o teste em test/widget/widget.test.tsx a pinar em runtime.\n//\n// `t(locale, key, overrides?)` resolve na ordem: override do consumidor → dicionário do\n// locale → pt (fallback seguro) → própria chave (nunca devolve undefined).\n\nexport type WidgetLocale = \"pt\" | \"en\" | \"es\";\n\nexport const WIDGET_KEYS = [\n \"openChat\",\n \"close\",\n \"name\",\n \"phone\",\n \"message\",\n \"send\",\n \"sending\",\n \"welcomeNotice\",\n \"privacyNote\",\n \"invalidPhone\",\n \"sendError\",\n \"retry\",\n \"sessionClosed\",\n \"newConversation\",\n \"poweredBy\",\n \"typing\",\n] as const;\n\nexport type WidgetKey = (typeof WIDGET_KEYS)[number];\n\n/** Dicionário completo: `Record<WidgetKey, string>` força todas as chaves no compilador. */\nexport type WidgetDictionary = Record<WidgetKey, string>;\n\nexport const pt: WidgetDictionary = {\n openChat: \"Abrir chat\",\n close: \"Fechar\",\n name: \"Nome\",\n phone: \"WhatsApp\",\n message: \"Mensagem\",\n send: \"Enviar\",\n sending: \"Enviando…\",\n welcomeNotice: \"Ao continuar, você entra no grupo de WhatsApp do site com o nosso time.\",\n privacyNote: \"Seus dados são usados apenas para o atendimento.\",\n invalidPhone: \"Informe um número válido com DDD (mínimo 10 dígitos).\",\n sendError: \"Não foi possível enviar. Tente novamente.\",\n retry: \"Tentar novamente\",\n sessionClosed: \"Esta conversa foi encerrada. Abra uma nova para continuar.\",\n newConversation: \"Iniciar nova conversa\",\n poweredBy: \"Powered by\",\n typing: \"digitando…\",\n};\n\nexport const en: WidgetDictionary = {\n openChat: \"Open chat\",\n close: \"Close\",\n name: \"Name\",\n phone: \"WhatsApp\",\n message: \"Message\",\n send: \"Send\",\n sending: \"Sending…\",\n welcomeNotice: \"When you continue, you join the site's WhatsApp group with our team.\",\n privacyNote: \"Your data is used only for support.\",\n invalidPhone: \"Enter a valid number with area code (at least 10 digits).\",\n sendError: \"Couldn't send. Please try again.\",\n retry: \"Try again\",\n sessionClosed: \"This conversation was closed. Start a new one to continue.\",\n newConversation: \"Start a new conversation\",\n poweredBy: \"Powered by\",\n typing: \"typing…\",\n};\n\nexport const es: WidgetDictionary = {\n openChat: \"Abrir chat\",\n close: \"Cerrar\",\n name: \"Nombre\",\n phone: \"WhatsApp\",\n message: \"Mensaje\",\n send: \"Enviar\",\n sending: \"Enviando…\",\n welcomeNotice: \"Al continuar, entras en el grupo de WhatsApp del sitio con nuestro equipo.\",\n privacyNote: \"Tus datos se usan solo para la atención.\",\n invalidPhone: \"Introduce un número válido con área (mínimo 10 dígitos).\",\n sendError: \"No se pudo enviar. Inténtalo de nuevo.\",\n retry: \"Reintentar\",\n sessionClosed: \"Esta conversación fue cerrada. Abre una nueva para continuar.\",\n newConversation: \"Iniciar nueva conversación\",\n poweredBy: \"Powered by\",\n typing: \"escribiendo…\",\n};\n\nexport const dictionaries: Record<WidgetLocale, WidgetDictionary> = { pt, en, es };\n\n/**\n * Traduz `key` para `locale`, permitindo override pontual de copy (`labels` do widget).\n * Nunca lança: chave desconhecida devolve o texto de pt e, na ausência, a própria chave.\n */\nexport function t(\n locale: WidgetLocale,\n key: WidgetKey,\n overrides?: Partial<Record<string, string>>,\n): string {\n const override = overrides?.[key];\n if (override !== undefined) return override;\n const dict: Partial<WidgetDictionary> = dictionaries[locale];\n return dict[key] ?? dictionaries.pt[key] ?? key;\n}\n","// src/widget/styles.ts — injeção idempotente da folha de estilos do widget.\n//\n// O widget é auto-contido: `injectWidgetStyles()` insere `<style id=\"ecw-styles\">` com as\n// classes `.ecw-*` uma única vez (múltiplas chamadas / múltiplas instâncias não duplicam).\n// O texto abaixo é o ESPELHO exato de `src/widget/styles.css` (arquivo publicado no\n// subpath \"./widget/styles.css\" para quem prefere `<link>`); o teste de paridade em\n// test/widget/widget.test.tsx falha se os dois divergirem.\n\nexport const WIDGET_CSS = `/*\n * src/widget/styles.css — folha de estilos do ChatWidget (classes \\`.ecw-*\\`).\n *\n * Fonte canônica do CSS INJETADO em runtime: src/widget/styles.ts espelha este arquivo\n * e injeta \\`<style id=\"ecw-styles\">\\` via \\`injectWidgetStyles()\\` (o widget é\n * auto-contido — o consumidor não precisa importar CSS). O teste de paridade em\n * test/widget/widget.test.tsx garante que os dois textos não divergem.\n * Consumidores que preferem \\`<link>\\` podem importar \"@erlancarreira/evolution-chat/widget/styles.css\".\n *\n * Acessibilidade: \\`color-scheme\\` (dark nativo), \\`:focus-visible\\` sempre visível,\n * contraste AA sobre o accent (texto escuro no verde), e transições desligadas com\n * \\`prefers-reduced-motion\\`.\n */\n\n.ecw-root {\n --ecw-accent: #25d366;\n --ecw-accent-ink: #06251a;\n --ecw-surface: #ffffff;\n --ecw-surface-2: #f1f4f7;\n --ecw-text: #14181d;\n --ecw-muted: #51606e;\n --ecw-border: #d7dde3;\n --ecw-danger: #b3261e;\n --ecw-shadow: 0 8px 28px rgb(9 20 28 / 22%);\n color-scheme: light dark;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n font-size: 14px;\n line-height: 1.45;\n color: var(--ecw-text);\n}\n\n@media (prefers-color-scheme: dark) {\n .ecw-root {\n --ecw-accent-ink: #04170f;\n --ecw-surface: #151a20;\n --ecw-surface-2: #232a33;\n --ecw-text: #e8edf2;\n --ecw-muted: #9aa7b4;\n --ecw-border: #2c343d;\n --ecw-danger: #ff9a8f;\n --ecw-shadow: 0 8px 28px rgb(0 0 0 / 45%);\n }\n}\n\n/* ── balão flutuante ─────────────────────────────────────────────────────── */\n.ecw-button {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 2147483000;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 56px;\n height: 56px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n box-shadow: var(--ecw-shadow);\n cursor: pointer;\n transition: transform 160ms ease, box-shadow 160ms ease;\n}\n.ecw-button:hover { transform: translateY(-2px) scale(1.03); }\n.ecw-button:active { transform: translateY(0) scale(0.98); }\n.ecw-button svg { width: 26px; height: 26px; }\n\n.ecw-badge {\n position: absolute;\n top: -4px;\n right: -4px;\n min-width: 22px;\n height: 22px;\n padding: 0 6px;\n border-radius: 11px;\n border: 2px solid var(--ecw-surface);\n background: var(--ecw-danger);\n color: #fff;\n font-size: 12px;\n font-weight: 700;\n line-height: 18px;\n text-align: center;\n}\n\n/* ── painel ──────────────────────────────────────────────────────────────── */\n.ecw-panel {\n position: fixed;\n right: 20px;\n bottom: 88px;\n z-index: 2147483000;\n display: flex;\n flex-direction: column;\n width: min(380px, calc(100vw - 24px));\n min-width: 320px;\n height: 480px;\n max-height: 70vh;\n overflow: hidden;\n border: 1px solid var(--ecw-border);\n border-radius: 14px;\n background: var(--ecw-surface);\n box-shadow: var(--ecw-shadow);\n animation: ecw-pop 160ms ease-out;\n}\n@keyframes ecw-pop { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }\n\n.ecw-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n}\n.ecw-title { flex: 1; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.ecw-code { font-size: 12px; font-weight: 600; opacity: 0.85; letter-spacing: 0.02em; }\n.ecw-close {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font-size: 18px;\n line-height: 1;\n cursor: pointer;\n}\n.ecw-close:hover { background: rgb(0 0 0 / 12%); }\n\n/* ── lista de mensagens ──────────────────────────────────────────────────── */\n.ecw-list {\n flex: 1;\n margin: 0;\n padding: 12px;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 10px;\n list-style: none;\n background: var(--ecw-surface-2);\n}\n.ecw-item { display: flex; flex-direction: column; max-width: 82%; }\n.ecw-item--visitor { align-self: flex-end; align-items: flex-end; }\n.ecw-item--owner { align-self: flex-start; align-items: flex-start; }\n.ecw-item--system { align-self: center; align-items: center; max-width: 92%; }\n.ecw-system {\n padding: 4px 10px;\n border-radius: 999px;\n background: var(--ecw-surface);\n border: 1px solid var(--ecw-border);\n color: var(--ecw-muted);\n font-size: 11px;\n text-align: center;\n overflow-wrap: anywhere;\n}\n.ecw-bubble {\n padding: 8px 12px;\n border-radius: 12px;\n overflow-wrap: anywhere;\n white-space: pre-wrap;\n}\n.ecw-bubble--visitor {\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n border-bottom-right-radius: 4px;\n}\n.ecw-bubble--owner {\n background: var(--ecw-surface);\n color: var(--ecw-text);\n border: 1px solid var(--ecw-border);\n border-bottom-left-radius: 4px;\n}\n.ecw-meta { display: flex; align-items: center; gap: 6px; margin-top: 2px; font-size: 11px; color: var(--ecw-muted); }\n.ecw-status--failed { color: var(--ecw-danger); }\n.ecw-retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--ecw-danger);\n font: inherit;\n font-size: 11px;\n text-decoration: underline;\n cursor: pointer;\n}\n\n/* ── pré-chat form ───────────────────────────────────────────────────────── */\n.ecw-form { display: flex; flex-direction: column; gap: 12px; padding: 16px 14px; overflow-y: auto; }\n.ecw-welcome { margin: 0; font-weight: 600; }\n.ecw-notice { margin: 0; font-size: 12px; color: var(--ecw-muted); }\n.ecw-field { display: flex; flex-direction: column; gap: 4px; }\n.ecw-label { font-size: 12px; font-weight: 600; color: var(--ecw-text); }\n.ecw-input {\n width: 100%;\n padding: 9px 10px;\n border: 1px solid var(--ecw-border);\n border-radius: 8px;\n background: var(--ecw-surface);\n color: var(--ecw-text);\n font: inherit;\n}\n.ecw-input:focus { outline: 2px solid var(--ecw-accent); outline-offset: 1px; }\n.ecw-input[aria-invalid=\"true\"] { border-color: var(--ecw-danger); }\n.ecw-error { margin: 0; font-size: 12px; color: var(--ecw-danger); }\n.ecw-submit {\n padding: 10px 14px;\n border: none;\n border-radius: 8px;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n font: inherit;\n font-weight: 700;\n cursor: pointer;\n}\n.ecw-submit:disabled { opacity: 0.55; cursor: not-allowed; }\n\n/* ── composer ────────────────────────────────────────────────────────────── */\n.ecw-composer { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ecw-border); background: var(--ecw-surface); }\n.ecw-composer .ecw-input { flex: 1; }\n.ecw-send {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n padding: 0;\n border: none;\n border-radius: 8px;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n cursor: pointer;\n}\n.ecw-send:disabled { opacity: 0.55; cursor: not-allowed; }\n.ecw-send svg { width: 18px; height: 18px; }\n\n/* ── indicador \"digitando…\" (3 pontinhos) ────────────────────────────────── */\n.ecw-typing {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 10px 14px;\n border-radius: 12px;\n border-bottom-left-radius: 4px;\n background: var(--ecw-surface);\n border: 1px solid var(--ecw-border);\n}\n.ecw-typing-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--ecw-muted);\n animation: ecw-typing-bounce 1.2s infinite ease-in-out;\n}\n.ecw-typing-dot:nth-child(2) { animation-delay: 0.18s; }\n.ecw-typing-dot:nth-child(3) { animation-delay: 0.36s; }\n@keyframes ecw-typing-bounce {\n 0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }\n 30% { transform: translateY(-4px); opacity: 1; }\n}\n\n/* ── sessão encerrada / falha ────────────────────────────────────────────── */\n.ecw-closed { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ecw-border); background: var(--ecw-surface); }\n\n.ecw-footer { padding: 6px 12px 10px; font-size: 11px; color: var(--ecw-muted); text-align: center; background: var(--ecw-surface); }\n.ecw-powered-link { color: var(--ecw-accent); font-weight: 600; text-decoration: none; }\n.ecw-powered-link:hover { text-decoration: underline; }\n\n/* ── utilitários ─────────────────────────────────────────────────────────── */\n.ecw-hp { display: none !important; }\n.ecw-sr-only {\n position: absolute;\n width: 1px; height: 1px;\n margin: -1px; padding: 0;\n overflow: hidden;\n clip: rect(0 0 0 0);\n white-space: nowrap;\n border: 0;\n}\n.ecw-button:focus-visible, .ecw-close:focus-visible, .ecw-submit:focus-visible,\n.ecw-send:focus-visible, .ecw-retry:focus-visible {\n outline: 3px solid #1b6fec;\n outline-offset: 2px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ecw-root *, .ecw-root *::before, .ecw-root *::after {\n animation: none !important;\n transition: none !important;\n }\n}\n\n@media (max-width: 420px) {\n .ecw-panel { right: 12px; left: 12px; width: auto; min-width: 0; bottom: 80px; }\n}\n`;\n\nlet injected = false;\n\n/**\n * Insere a folha do widget no `document.head` (idempotente: uma única tag\n * `<style id=\"ecw-styles\">` por documento, não importa quantas vezes seja chamada).\n * No-op fora de browser (SSR / Node) — o consumidor renderiza com `<link>` do subpath.\n */\nexport function injectWidgetStyles(): void {\n if (typeof document === \"undefined\") return;\n if (injected && document.getElementById(\"ecw-styles\") !== null) return;\n const existing = document.getElementById(\"ecw-styles\");\n if (existing !== null) {\n // Alguém (ex.: outra cópia do bundle) já injetou — marca e não duplica.\n injected = true;\n return;\n }\n const style = document.createElement(\"style\");\n style.id = \"ecw-styles\";\n style.textContent = WIDGET_CSS;\n document.head.appendChild(style);\n injected = true;\n}\n","// src/widget/use-chat.ts — máquina de estados do ChatWidget (hook puro, sem JSX).\n//\n// Fases: \"idle\" (painel nunca aberto, sem sessão) → \"form\" (pré-chat) → \"chat\" (sessão\n// ativa). A transição idle→form acontece na primeira abertura; form→chat no POST bem\n// sucedido; qualquer fase→form quando o GET devolve 404 (sessão expirada → storage limpo)\n// ou quando o visitante pede \"nova conversa\" numa sessão closed/failed (`startNewConversation`).\n//\n// Tempo real (Strategy, invisível ao usuário): com token em mãos, `realtime.subscribe`\n// anexa eventos; se o transporte cair (`onStatus(\"closed\")`), o hook liga um POLLING de\n// 5s no GET de histórico com `after=<última createdAt>` até o canal reabrir. Mensagens\n// `owner` que chegam com o painel fechado somam no badge de não lidas.\n//\n// Otimismo: POST anexa uma mensagem `tmp-…` com status \"pending\"; a resposta do servidor\n// SUBSTITUI o tmp (replace) — em falha, o tmp vira \"failed\" e ganha retry.\n\nimport { useCallback, useEffect, useReducer, useRef } from \"react\";\nimport type { ChatEvent, ChatMessage, ChatSessionStatus } from \"../types\";\nimport type { RealtimeHandle } from \"../bridge/types\";\n\n/** Chave de persistência da sessão no localStorage. */\nexport const SESSION_STORAGE_KEY = \"ecw:session\";\n/** Intervalo do fallback de polling quando o canal realtime fecha (ms). */\nexport const POLL_INTERVAL_MS = 5_000;\n/** Faixa (ms) do \"digitando…\" simulado exibido ANTES de uma mensagem recebida. */\nexport const TYPING_PRE_MESSAGE_MIN_MS = 700;\nexport const TYPING_PRE_MESSAGE_MAX_MS = 2200;\n/** Mínimo de dígitos (com DDD) para um WhatsApp ser aceito no form. */\nexport const MIN_PHONE_DIGITS = 10;\n\nexport type ChatPhase = \"idle\" | \"form\" | \"chat\";\n\nexport interface SessionInfo {\n code: string;\n status: ChatSessionStatus;\n visitorName: string;\n}\n\nexport interface ChatState {\n phase: ChatPhase;\n open: boolean;\n session: SessionInfo | null;\n token: string | null;\n messages: ChatMessage[];\n unread: number;\n sending: boolean;\n /** Chave i18n do erro visível (\"sendError\" | \"invalidPhone\") ou null. */\n error: string | null;\n /** True enquanto a DONA da plataforma (atendente) digita → \"3 pontinhos\" no widget. */\n ownerTyping: boolean;\n /** True enquanto o VISITANTE (esta ponta) digita — útil para um painel de atendente. */\n visitorTyping: boolean;\n}\n\ntype Action =\n | { type: \"open\" }\n | { type: \"close\" }\n | { type: \"restore\"; session: SessionInfo; token: string; messages: ChatMessage[] }\n | { type: \"start\"; session: SessionInfo; token: string; messages: ChatMessage[] }\n | { type: \"reset-form\" }\n | { type: \"append\"; message: ChatMessage }\n | { type: \"merge\"; messages: ChatMessage[] }\n | { type: \"replace\"; tmpId: string; message: ChatMessage }\n | { type: \"mark\"; id: string; status: ChatMessage[\"status\"] }\n | { type: \"session-status\"; status: ChatSessionStatus }\n | { type: \"sending\"; value: boolean }\n | { type: \"error\"; value: string | null }\n | { type: \"typing\"; from: \"owner\" | \"visitor\"; isTyping: boolean };\n\nconst initialState: ChatState = {\n phase: \"idle\",\n open: false,\n session: null,\n token: null,\n messages: [],\n unread: 0,\n sending: false,\n error: null,\n ownerTyping: false,\n visitorTyping: false,\n};\n\nfunction withMessage(state: ChatState, message: ChatMessage): ChatState {\n const index = state.messages.findIndex((m) => m.id === message.id);\n let messages: ChatMessage[];\n if (index >= 0) {\n messages = [...state.messages];\n messages[index] = message;\n } else {\n messages = [...state.messages, message];\n }\n const bump = state.open || message.direction !== \"owner\" ? 0 : 1;\n const next: ChatState = { ...state, messages, unread: state.unread + bump };\n // Quando chega uma mensagem da dona, ela parou de \"digitar\" (limpa os 3 pontinhos).\n if (message.direction === \"owner\") next.ownerTyping = false;\n return next;\n}\n\nexport function chatReducer(state: ChatState, action: Action): ChatState {\n switch (action.type) {\n case \"open\":\n return {\n ...state,\n open: true,\n unread: 0,\n phase: state.phase === \"idle\" ? \"form\" : state.phase,\n };\n case \"close\":\n return { ...state, open: false };\n case \"restore\":\n return {\n ...state,\n phase: \"chat\",\n session: action.session,\n token: action.token,\n messages: action.messages,\n };\n case \"start\": {\n // O POST devolve o histórico oficial (inclui a 1ª msg do visitante); qualquer tmp\n // \"pending\" ainda não confirmado pelo servidor é mantido no fim da lista.\n let next: ChatState = {\n ...state,\n phase: \"chat\",\n session: action.session,\n token: action.token,\n messages: action.messages,\n };\n for (const pending of state.messages) {\n const confirmed = action.messages.some(\n (m) => m.direction === \"visitor\" && m.body === pending.body,\n );\n if (pending.status === \"pending\" && !confirmed) next = withMessage(next, pending);\n }\n return next;\n }\n case \"reset-form\":\n return { ...state, phase: \"form\", session: null, token: null, messages: [], unread: 0, error: null };\n case \"append\":\n return withMessage(state, action.message);\n case \"merge\": {\n let next = state;\n for (const message of action.messages) next = withMessage(next, message);\n return next;\n }\n case \"replace\": {\n const messages = state.messages.map((m) => (m.id === action.tmpId ? action.message : m));\n const kept = messages.some((m) => m.id === action.message.id);\n return { ...state, messages: kept ? messages : [...messages, action.message] };\n }\n case \"mark\":\n return {\n ...state,\n messages: state.messages.map((m) => (m.id === action.id ? { ...m, status: action.status } : m)),\n };\n case \"session-status\": {\n if (state.session === null) return state;\n return { ...state, session: { ...state.session, status: action.status } };\n }\n case \"sending\":\n return { ...state, sending: action.value };\n case \"error\":\n return { ...state, error: action.value };\n case \"typing\":\n // Só transiciona se mudar de fato (evita re-render a cada batida de presença).\n return action.from === \"owner\"\n ? state.ownerTyping === action.isTyping\n ? state\n : { ...state, ownerTyping: action.isTyping }\n : state.visitorTyping === action.isTyping\n ? state\n : { ...state, visitorTyping: action.isTyping };\n }\n}\n\n// ─── helpers de telefone ──────────────────────────────────────────────────────\n\n/** Só dígitos (o servidor espera DDI+DDD+número sem máscara). */\nexport function normalizePhone(value: string): string {\n return value.replace(/\\D/g, \"\");\n}\n\n/** Máscara BR parcial `(11) 99999-8888`, progressiva enquanto o usuário digita. */\nexport function maskPhone(value: string): string {\n const digits = normalizePhone(value).slice(0, 11);\n if (digits.length === 0) return \"\";\n const ddd = digits.slice(0, 2);\n if (digits.length <= 2) return `(${ddd}`;\n const rest = digits.slice(2);\n if (rest.length <= 5) return `(${ddd}) ${rest}`;\n return `(${ddd}) ${rest.slice(0, 5)}-${rest.slice(5)}`;\n}\n\nexport function isValidPhone(value: string): boolean {\n return normalizePhone(value).length >= MIN_PHONE_DIGITS;\n}\n\n// ─── persistência ─────────────────────────────────────────────────────────────\n\ninterface StoredSession {\n token: string;\n code: string;\n}\n\nfunction readStoredSession(): StoredSession | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(SESSION_STORAGE_KEY);\n if (raw === null) return null;\n const parsed: unknown = JSON.parse(raw);\n if (\n parsed !== null &&\n typeof parsed === \"object\" &&\n typeof (parsed as StoredSession).token === \"string\" &&\n (parsed as StoredSession).token !== \"\"\n ) {\n const { token, code } = parsed as StoredSession;\n return { token, code: typeof code === \"string\" ? code : \"\" };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction writeStoredSession(session: StoredSession): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));\n } catch {\n /* storage cheio/bloqueado: sessão só não sobrevive ao reload */\n }\n}\n\nfunction clearStoredSession(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(SESSION_STORAGE_KEY);\n } catch {\n /* idem */\n }\n}\n\n// ─── contrato de rede (espelha createChatRoutes — Task 8) ────────────────────\n\ninterface HistoryResponse {\n session?: SessionInfo;\n messages?: ChatMessage[];\n}\n\ninterface StartResponse {\n session?: SessionInfo & { realtimeToken?: string };\n messages?: ChatMessage[];\n}\n\ninterface SendResponse {\n message?: ChatMessage;\n}\n\nfunction historyUrl(endpoint: string, token: string, after: string | null): string {\n const params = new URLSearchParams({ token });\n if (after !== null) params.set(\"after\", after);\n const sep = endpoint.includes(\"?\") ? \"&\" : \"?\";\n return `${endpoint}${sep}${params.toString()}`;\n}\n\nfunction lastCursor(messages: ChatMessage[]): string | null {\n let latest: string | null = null;\n for (const m of messages) if (latest === null || m.createdAt > latest) latest = m.createdAt;\n return latest;\n}\n\n// ─── hook ─────────────────────────────────────────────────────────────────────\n\nexport interface UseChatOptions {\n endpoint: string;\n realtime: RealtimeHandle;\n /** Rota que recebe o sinal \"visitante digitando\" (POST { token, isTyping }). Default: `${endpoint}/typing`. */\n typingEndpoint?: string;\n}\n\nexport interface UseChatResult {\n state: ChatState;\n openPanel(): void;\n closePanel(): void;\n togglePanel(): void;\n submitForm(input: { name: string; phone: string; message: string; honeypot: string }): Promise<void>;\n sendMessage(text: string): Promise<void>;\n retryMessage(id: string): Promise<void>;\n /** Descarta a sessão encerrada/falha (storage + estado) e volta ao pré-chat form. */\n startNewConversation(): void;\n /** Avisa o servidor que o visitante está (true) ou parou (false) de digitar. Best-effort. */\n notifyTyping(isTyping: boolean): Promise<void>;\n}\n\nlet tmpSeq = 0;\nfunction tmpMessage(body: string): ChatMessage {\n tmpSeq += 1;\n return {\n id: `tmp-${tmpSeq}`,\n sessionId: \"\",\n direction: \"visitor\",\n body,\n status: \"pending\",\n waMessageId: null,\n createdAt: new Date().toISOString(),\n };\n}\n\nexport function useChat({ endpoint, realtime, typingEndpoint }: UseChatOptions): UseChatResult {\n const [state, dispatch] = useReducer(chatReducer, initialState);\n\n const typingUrl = typingEndpoint ?? `${endpoint.replace(/\\/+$/, \"\")}/typing`;\n\n // Espelho do estado para callbacks estáveis (eventos realtime/poll chegam fora do React).\n const stateRef = useRef(state);\n useEffect(() => {\n stateRef.current = state;\n }, [state]);\n\n // A porta pode chegar como objeto novo a cada render do consumidor; a assinatura deve\n // depender apenas do token, nunca da identidade do handle.\n const realtimeRef = useRef(realtime);\n useEffect(() => {\n realtimeRef.current = realtime;\n }, [realtime]);\n\n const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const stopPoll = useCallback((): void => {\n if (pollRef.current !== null) {\n clearInterval(pollRef.current);\n pollRef.current = null;\n }\n }, []);\n\n const pollOnce = useCallback(async (): Promise<void> => {\n const { token, messages } = stateRef.current;\n if (token === null) return;\n try {\n const res = await fetch(historyUrl(endpoint, token, lastCursor(messages)), {\n method: \"GET\",\n headers: { accept: \"application/json\" },\n });\n if (res.status === 404) {\n clearStoredSession();\n stopPoll();\n dispatch({ type: \"reset-form\" });\n return;\n }\n if (!res.ok) return;\n const data = (await res.json()) as HistoryResponse;\n if (Array.isArray(data.messages)) dispatch({ type: \"merge\", messages: data.messages });\n if (data.session !== undefined) dispatch({ type: \"session-status\", status: data.session.status });\n } catch {\n /* offline: o próximo tick tenta de novo */\n }\n }, [endpoint, stopPoll]);\n\n const startPoll = useCallback((): void => {\n if (pollRef.current !== null) return;\n pollRef.current = setInterval(() => {\n void pollOnce();\n }, POLL_INTERVAL_MS);\n }, [pollOnce]);\n\n // Abertura/fechamento do canal realtime (Strategy): caiu → polling; voltou → para.\n const handleStatus = useCallback(\n (s: \"open\" | \"closed\"): void => {\n if (s === \"closed\") startPoll();\n else stopPoll();\n },\n [startPoll, stopPoll],\n );\n\n const handleEvent = useCallback((e: ChatEvent): void => {\n if (e.type === \"message\" && e.message !== undefined) {\n const msg = e.message;\n // Mensagem recebida da DONA (staff): como a instância conectada não emite presença\n // do próprio \"digitando\" (Evolution/Baileys), simulamos o \"3 pontinhos\" antes de\n // exibir a mensagem — dá o feedback visual de que a outra ponta está respondendo.\n if (msg.direction === \"owner\") {\n dispatch({ type: \"typing\", from: \"owner\", isTyping: true });\n const text = typeof msg.body === \"string\" ? msg.body : \"\";\n const delay = Math.min(\n TYPING_PRE_MESSAGE_MAX_MS,\n Math.max(TYPING_PRE_MESSAGE_MIN_MS, text.length * 22),\n );\n setTimeout(() => {\n dispatch({ type: \"append\", message: msg });\n }, delay);\n } else {\n dispatch({ type: \"append\", message: msg });\n }\n } else if (e.type === \"session\" && e.status !== undefined) {\n dispatch({ type: \"session-status\", status: e.status });\n } else if (e.type === \"typing\" && e.from !== undefined) {\n dispatch({ type: \"typing\", from: e.from, isTyping: e.isTyping });\n }\n }, []);\n\n // Boot: sessão persistida → GET de histórico (404 limpa o storage e volta ao form).\n useEffect(() => {\n const stored = readStoredSession();\n if (stored === null) return;\n let cancelled = false;\n void (async () => {\n try {\n const res = await fetch(historyUrl(endpoint, stored.token, null), {\n method: \"GET\",\n headers: { accept: \"application/json\" },\n });\n if (cancelled) return;\n if (res.status === 404) {\n clearStoredSession();\n dispatch({ type: \"reset-form\" });\n return;\n }\n if (!res.ok) return;\n const data = (await res.json()) as HistoryResponse;\n if (cancelled || data.session === undefined) return;\n dispatch({\n type: \"restore\",\n session: data.session,\n token: stored.token,\n messages: Array.isArray(data.messages) ? data.messages : [],\n });\n } catch {\n /* sem rede: mantém o form/idle; o retry fica com o usuário */\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [endpoint]);\n\n // Assinatura do canal enquanto houver token (re-assina quando a sessão muda).\n useEffect(() => {\n const token = state.token;\n if (token === null) return;\n const unsubscribe = realtimeRef.current.subscribe(token, handleEvent, handleStatus);\n return () => {\n unsubscribe();\n stopPoll();\n };\n }, [state.token, handleEvent, handleStatus, stopPoll]);\n\n useEffect(() => stopPoll, [stopPoll]);\n\n const openPanel = useCallback((): void => {\n dispatch({ type: \"open\" });\n }, []);\n\n const closePanel = useCallback((): void => {\n dispatch({ type: \"close\" });\n }, []);\n\n const togglePanel = useCallback((): void => {\n if (stateRef.current.open) dispatch({ type: \"close\" });\n else dispatch({ type: \"open\" });\n }, []);\n\n const postMessage = useCallback(\n async (token: string, body: string, id: string): Promise<void> => {\n dispatch({ type: \"sending\", value: true });\n dispatch({ type: \"error\", value: null });\n try {\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ token, message: body }),\n });\n if (!res.ok) {\n dispatch({ type: \"mark\", id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n return;\n }\n const data = (await res.json()) as SendResponse;\n if (data.message !== undefined) dispatch({ type: \"replace\", tmpId: id, message: data.message });\n else dispatch({ type: \"mark\", id, status: \"failed\" });\n } catch {\n dispatch({ type: \"mark\", id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n } finally {\n dispatch({ type: \"sending\", value: false });\n }\n },\n [endpoint],\n );\n\n const sendMessage = useCallback(\n async (text: string): Promise<void> => {\n const body = text.trim();\n const token = stateRef.current.token;\n if (body === \"\" || token === null) return;\n const optimistic = tmpMessage(body);\n dispatch({ type: \"append\", message: optimistic });\n await postMessage(token, body, optimistic.id);\n },\n [postMessage],\n );\n\n const retryMessage = useCallback(\n async (id: string): Promise<void> => {\n const token = stateRef.current.token;\n const target = stateRef.current.messages.find((m) => m.id === id);\n if (token === null || target === undefined || target.direction !== \"visitor\") return;\n dispatch({ type: \"mark\", id, status: \"pending\" });\n await postMessage(token, target.body, id);\n },\n [postMessage],\n );\n\n // Sessão closed/failed: o visitante resolve o beco sem saída pedindo uma conversa nova.\n // Limpa o storage (senão o próximo boot restauraria a mesma sessão morta) e volta ao\n // form; a assinatura realtime cai sozinha na limpeza do efeito, porque token → null.\n const startNewConversation = useCallback((): void => {\n clearStoredSession();\n dispatch({ type: \"reset-form\" });\n }, []);\n\n // Avisa o servidor que o visitante está (ou parou de) digitar. Best-effort: nunca\n // deve quebrar o fluxo de envio de mensagens — falhas são silenciadas.\n const notifyTyping = useCallback(\n async (isTyping: boolean): Promise<void> => {\n const token = stateRef.current.token;\n if (token === null) return;\n try {\n await fetch(typingUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ token, isTyping }),\n });\n } catch {\n /* offline/erro: digitação é cosmética, ignora */\n }\n },\n [typingUrl],\n );\n\n const submitForm = useCallback(\n async ({ name, phone, message, honeypot }: { name: string; phone: string; message: string; honeypot: string }): Promise<void> => {\n // Anti-bot silencioso: honeypot preenchido → nada sai do navegador (o servidor\n // também fingiria sucesso; aqui nem há request).\n if (honeypot.trim() !== \"\") return;\n const digits = normalizePhone(phone);\n if (name.trim() === \"\" || message.trim() === \"\" || digits.length < MIN_PHONE_DIGITS) {\n dispatch({ type: \"error\", value: digits.length < MIN_PHONE_DIGITS ? \"invalidPhone\" : \"sendError\" });\n return;\n }\n const optimistic = tmpMessage(message.trim());\n dispatch({ type: \"append\", message: optimistic });\n dispatch({ type: \"sending\", value: true });\n dispatch({ type: \"error\", value: null });\n try {\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ name: name.trim(), phone: digits, message: message.trim(), honeypot: \"\" }),\n });\n if (!res.ok) {\n dispatch({ type: \"mark\", id: optimistic.id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n return;\n }\n const data = (await res.json()) as StartResponse;\n const session = data.session;\n if (session === undefined || typeof session.realtimeToken !== \"string\" || session.realtimeToken === \"\") {\n dispatch({ type: \"mark\", id: optimistic.id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n return;\n }\n const token = session.realtimeToken;\n writeStoredSession({ token, code: session.code });\n dispatch({\n type: \"start\",\n session: { code: session.code, status: session.status, visitorName: session.visitorName },\n token,\n messages: Array.isArray(data.messages) ? data.messages : [],\n });\n } catch {\n dispatch({ type: \"mark\", id: optimistic.id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n } finally {\n dispatch({ type: \"sending\", value: false });\n }\n },\n [endpoint],\n );\n\n return { state, openPanel, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping };\n}\n"],"mappings":"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","names":["import_react"]}
|
|
1
|
+
{"version":3,"sources":["../src/widget/index.ts","../src/widget/chat-widget.tsx","../src/widget/i18n.ts","../src/widget/styles.ts","../src/widget/use-chat.ts"],"sourcesContent":["// src/widget/index.ts — superfície pública do subpath \"@erlancarreira/evolution-chat/widget\".\n//\n// O consumidor (LMS) faz:\n// import { ChatWidget } from \"@erlancarreira/evolution-chat/widget\";\n// import type { ChatWidgetProps } from \"@erlancarreira/evolution-chat/widget\";\n// <ChatWidget endpoint=\"/api/chat\" locale=\"pt\" welcome=\"Oi!\" projectName=\"Aulivra\" realtime={supabaseRealtime} />\n//\n// O CSS é auto-injetado (`injectWidgetStyles` roda no mount do ChatWidget); quem quiser\n// servir por `<link>` importa o subpath \"./widget/styles.css\".\n\nexport { ChatWidget } from \"./chat-widget\";\nexport type { ChatWidgetProps, WidgetLocale } from \"./chat-widget\";\n\nexport { injectWidgetStyles, WIDGET_CSS } from \"./styles\";\n\nexport {\n WIDGET_KEYS,\n dictionaries,\n en,\n es,\n pt,\n t,\n} from \"./i18n\";\nexport type { WidgetDictionary, WidgetKey } from \"./i18n\";\n\nexport {\n isValidPhone,\n maskPhone,\n normalizePhone,\n useChat,\n SESSION_STORAGE_KEY,\n POLL_INTERVAL_MS,\n MIN_PHONE_DIGITS,\n} from \"./use-chat\";\nexport type { ChatPhase, ChatState, SessionInfo, UseChatOptions, UseChatResult } from \"./use-chat\";\n\n// Porta injetada — reexportada para o consumidor não descer ao subpath ./bridge.\nexport type { RealtimeHandle } from \"../bridge/types\";\n","// src/widget/chat-widget.tsx — ChatWidget: balão flutuante + painel de conversa.\n//\n// Sem Tailwind / lib de UI: toda a aparência vem de `injectWidgetStyles()` (classes\n// `.ecw-*`), e o único estilo inline é a CSS var `--ecw-accent` (cor do tema). O estado\n// inteiro vive em `use-chat.ts` (reducer + rede + realtime); aqui só render e foco.\n//\n// Acessibilidade (contrato da spec §2):\n// - balão: <button> com aria-label (i18n), aria-expanded e aria-haspopup=\"dialog\";\n// - painel: role=\"dialog\" aria-label=\"Chat\"; foco no primeiro campo ao abrir;\n// ESC fecha e devolve o foco ao balão; clique fora NÃO fecha (mobile-friendly);\n// - todo input tem <label htmlFor> (useId → múltiplas instâncias sem colisão);\n// - badge de não lidas refletido no nome acessível do balão;\n// - sessão closed/failed: composer desabilitado, aviso em role=\"status\" e botão real\n// \"nova conversa\" (o beco sem saída tem saída acessível por teclado);\n// - prefers-reduced-motion desliga animações (via CSS).\n\nimport {\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type CSSProperties,\n type ChangeEvent,\n type FormEvent,\n type ReactElement,\n type RefObject,\n} from \"react\";\nimport type { RealtimeHandle } from \"../bridge/types\";\nimport type { ChatMessage } from \"../types\";\nimport { t, type WidgetKey, type WidgetLocale } from \"./i18n\";\nimport { injectWidgetStyles } from \"./styles\";\nimport { isValidPhone, maskPhone, useChat } from \"./use-chat\";\n\nexport type { WidgetLocale };\n\nexport interface ChatWidgetProps {\n /** Caminho da rota de chat (GET histórico / POST start+send), ex.: \"/api/chat\". */\n endpoint: string;\n locale: WidgetLocale;\n /** Saudação exibida no pré-chat form. */\n welcome: string;\n projectName: string;\n /** Cor de tema; default \"#25D366\". */\n accentColor?: string;\n /** Porta do widget (DI — Strategy): subscribe/unsubscribe do canal da sessão. */\n realtime: RealtimeHandle;\n /** Override pontual de copy por chave i18n. */\n labels?: Partial<Record<string, string>>;\n}\n\nconst DEFAULT_ACCENT = \"#25D366\";\n\nfunction ChatIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M21 11.5a8.38 8.38 0 0 1-8.5 8.5c-1.5 0-2.9-.36-4.13-1L3 20l1.1-3.85A8.36 8.36 0 0 1 12.5 3 8.38 8.38 0 0 1 21 11.5z\" />\n </svg>\n );\n}\n\nfunction SendIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M22 2 11 13\" />\n <path d=\"M22 2 15 22l-4-9-9-4 20-7z\" />\n </svg>\n );\n}\n\nfunction formatTime(iso: string, locale: WidgetLocale): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return \"\";\n return new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" }).format(date);\n}\n\nfunction StatusMark({ status }: { status: ChatMessage[\"status\"] }): ReactElement {\n // ✓ = aceita pelo servidor (pending), ✓✓ = enviada, ⚠ = falhou.\n // Decorativo por design: o conjunto de chaves i18n é fixo (15) e o estado \"failed\" já\n // é anunciado de forma acessível pelo botão visível `retry` ao lado — um texto sr-only\n // reaproveitando \"send\"/\"sending\" soaria a ruído para quem usa leitor de tela.\n const glyph = status === \"pending\" ? \"✓\" : status === \"sent\" ? \"✓✓\" : \"⚠\";\n return (\n <span className={status === \"failed\" ? \"ecw-status ecw-status--failed\" : \"ecw-status\"} aria-hidden=\"true\">\n {glyph}\n </span>\n );\n}\n\n/** \"3 pontinhos\" de digitação: bolha com três pontos animados + rótulo acessível.\n * `from` alinha o indicador ao lado de quem representa (owner→esquerda, visitor→direita). */\nfunction TypingIndicator({ from, label }: { from: \"owner\" | \"visitor\"; label: string }): ReactElement {\n return (\n <li className={`ecw-item ecw-item--${from}`} aria-live=\"polite\">\n <div className=\"ecw-typing\" role=\"status\" aria-label={label}>\n <span className=\"ecw-typing-dot\" />\n <span className=\"ecw-typing-dot\" />\n <span className=\"ecw-typing-dot\" />\n <span className=\"ecw-sr-only\">{label}</span>\n </div>\n </li>\n );\n}\n\ninterface FormProps {\n welcome: string;\n tr: (k: WidgetKey) => string;\n error: string | null;\n sending: boolean;\n firstFieldRef: RefObject<HTMLInputElement>;\n onSubmit(values: { name: string; phone: string; message: string; honeypot: string }): Promise<void>;\n}\n\nfunction PreChatForm({ welcome, tr, error, sending, firstFieldRef, onSubmit }: FormProps): ReactElement {\n const uid = useId();\n const [name, setName] = useState(\"\");\n const [phone, setPhone] = useState(\"\");\n const [message, setMessage] = useState(\"\");\n const [honeypot, setHoneypot] = useState(\"\");\n const [phoneTouched, setPhoneTouched] = useState(false);\n\n const phoneValid = isValidPhone(phone);\n const complete = name.trim() !== \"\" && phoneValid && message.trim() !== \"\";\n\n const handleSubmit = (e: FormEvent): void => {\n e.preventDefault();\n if (!complete || sending) return;\n void onSubmit({ name, phone, message, honeypot });\n };\n\n return (\n <form className=\"ecw-form\" onSubmit={handleSubmit} noValidate>\n <p className=\"ecw-welcome\">{welcome}</p>\n <p className=\"ecw-notice\">{tr(\"welcomeNotice\")}</p>\n\n <div className=\"ecw-field\">\n <label className=\"ecw-label\" htmlFor={`${uid}-name`}>{tr(\"name\")}</label>\n <input\n id={`${uid}-name`}\n ref={firstFieldRef}\n className=\"ecw-input\"\n type=\"text\"\n autoComplete=\"name\"\n value={name}\n onChange={(e) => { setName(e.target.value); }}\n required\n />\n </div>\n\n <div className=\"ecw-field\">\n <label className=\"ecw-label\" htmlFor={`${uid}-phone`}>{tr(\"phone\")}</label>\n <input\n id={`${uid}-phone`}\n className=\"ecw-input\"\n type=\"tel\"\n inputMode=\"tel\"\n autoComplete=\"tel\"\n placeholder=\"(11) 99999-8888\"\n value={phone}\n onChange={(e) => { setPhone(maskPhone(e.target.value)); }}\n onBlur={() => { setPhoneTouched(true); }}\n aria-invalid={phoneTouched && !phoneValid ? true : undefined}\n aria-describedby={phoneTouched && !phoneValid ? `${uid}-phone-err` : undefined}\n required\n />\n {phoneTouched && !phoneValid && (\n <p className=\"ecw-error\" id={`${uid}-phone-err`} role=\"alert\">{tr(\"invalidPhone\")}</p>\n )}\n </div>\n\n <div className=\"ecw-field\">\n <label className=\"ecw-label\" htmlFor={`${uid}-message`}>{tr(\"message\")}</label>\n <textarea\n id={`${uid}-message`}\n className=\"ecw-input\"\n rows={3}\n autoComplete=\"off\"\n value={message}\n onChange={(e) => { setMessage(e.target.value); }}\n required\n />\n </div>\n\n {/* Honeypot anti-bot: invisível para humanos (display:none via .ecw-hp),\n fora da ordem de tabulação e escondido do leitor de tela. */}\n <div className=\"ecw-hp\" aria-hidden=\"true\">\n <label htmlFor={`${uid}-website`}>Website</label>\n <input\n id={`${uid}-website`}\n name=\"website\"\n type=\"text\"\n tabIndex={-1}\n autoComplete=\"off\"\n value={honeypot}\n onChange={(e) => { setHoneypot(e.target.value); }}\n />\n </div>\n\n {error === \"sendError\" && (\n <p className=\"ecw-error\" role=\"alert\">{tr(\"sendError\")}</p>\n )}\n\n <button className=\"ecw-submit\" type=\"submit\" disabled={!complete || sending}>\n {sending ? tr(\"sending\") : tr(\"send\")}\n </button>\n <p className=\"ecw-notice\">{tr(\"privacyNote\")}</p>\n </form>\n );\n}\n\ninterface ChatPanelProps {\n messages: ChatMessage[];\n /** Só `status === \"active\"` compõe; closed/failed mostram aviso + \"nova conversa\". */\n canCompose: boolean;\n sending: boolean;\n locale: WidgetLocale;\n tr: (k: WidgetKey) => string;\n listRef: RefObject<HTMLUListElement>;\n firstFieldRef: RefObject<HTMLInputElement>;\n /** True enquanto a dona da plataforma (atendente) digita → mostra os \"3 pontinhos\". */\n ownerTyping: boolean;\n /** True enquanto o VISITANTE (esta ponta) digita → \"3 pontinhos\" do lado do visitante. */\n visitorTyping: boolean;\n /** Avisa o servidor que o visitante está/parou de digitar. Best-effort. */\n onTyping(isTyping: boolean): Promise<void>;\n onSend(text: string): Promise<void>;\n onRetry(id: string): Promise<void>;\n onNewConversation(): void;\n}\n\nfunction ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, onTyping, onSend, onRetry, onNewConversation }: ChatPanelProps): ReactElement {\n const uid = useId();\n const [draft, setDraft] = useState(\"\");\n\n // Sinal \"visitante digitando\" para a OUTRA ponta (painel de atendente): dispara\n // `true` só na TRANSIÇÃO para digitando (não a cada tecla) e `false` após 2,5s de\n // inatividade, ao enviar ou ao perder o foco — evita um POST por caractere. `onChange`\n // já cobre keyup, cola e IME (mais robusto que keyup puro).\n const typingRef = useRef(false);\n const typingTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Indicador de digitação DO PRÓPRIO visitante: renderizado LOCALMENTE (sem round-trip\n // de rede) para ser instantâneo/fluido. Grandes apps (WhatsApp, Messenger, Slack) nunca\n // mostram o \"digitando\" do próprio usuário com latência — a outra ponta recebe pela rede\n // (latência cross-dispositivo aceitável), mas o próprio usuário tem feedback imediato.\n const selfTypingRef = useRef(false);\n const selfTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const [selfTyping, setSelfTyping] = useState(false);\n\n useEffect(() => {\n return () => {\n if (typingTimer.current !== null) clearTimeout(typingTimer.current);\n if (selfTimer.current !== null) clearTimeout(selfTimer.current);\n };\n }, []);\n\n const stopTypingSignal = useCallback((): void => {\n if (typingTimer.current !== null) {\n clearTimeout(typingTimer.current);\n typingTimer.current = null;\n }\n if (typingRef.current) {\n typingRef.current = false;\n void onTyping(false);\n }\n }, [onTyping]);\n\n const stopSelfTyping = useCallback((): void => {\n if (selfTimer.current !== null) {\n clearTimeout(selfTimer.current);\n selfTimer.current = null;\n }\n if (selfTypingRef.current) {\n selfTypingRef.current = false;\n setSelfTyping(false);\n }\n }, []);\n\n const handleDraftChange = (e: ChangeEvent<HTMLInputElement>): void => {\n setDraft(e.target.value);\n if (!canCompose) return;\n // sinal para a OUTRA ponta (vai pela rede, com debounce de 2,5s).\n if (!typingRef.current) {\n typingRef.current = true;\n void onTyping(true);\n }\n if (typingTimer.current !== null) clearTimeout(typingTimer.current);\n typingTimer.current = setTimeout(stopTypingSignal, 2500);\n // indicador local do próprio visitante — instantâneo, sem esperar o servidor.\n if (!selfTypingRef.current) {\n selfTypingRef.current = true;\n setSelfTyping(true);\n }\n if (selfTimer.current !== null) clearTimeout(selfTimer.current);\n selfTimer.current = setTimeout(stopSelfTyping, 2500);\n };\n\n const handleDraftBlur = (): void => {\n stopTypingSignal();\n stopSelfTyping();\n };\n\n const submit = (e: FormEvent): void => {\n e.preventDefault();\n const text = draft.trim();\n if (text === \"\" || !canCompose) return;\n stopTypingSignal();\n stopSelfTyping();\n setDraft(\"\");\n void onSend(text);\n };\n\n return (\n <>\n <ul ref={listRef} className=\"ecw-list\">\n {messages.map((m) =>\n m.direction === \"system\" ? (\n <li key={m.id} className=\"ecw-item ecw-item--system\" role=\"status\">\n <div className=\"ecw-system\">{m.body}</div>\n </li>\n ) : (\n <li key={m.id} className={`ecw-item ecw-item--${m.direction}`}>\n <div className={`ecw-bubble ecw-bubble--${m.direction}`}>{m.body}</div>\n <div className=\"ecw-meta\">\n <time dateTime={m.createdAt}>{formatTime(m.createdAt, locale)}</time>\n {m.direction === \"visitor\" && <StatusMark status={m.status} />}\n {m.status === \"failed\" && (\n <button type=\"button\" className=\"ecw-retry\" onClick={() => { void onRetry(m.id); }}>\n {tr(\"retry\")}\n </button>\n )}\n </div>\n </li>\n ),\n )}\n {ownerTyping && canCompose && <TypingIndicator from=\"owner\" label={tr(\"typing\")} />}\n {selfTyping && canCompose && <TypingIndicator from=\"visitor\" label={tr(\"typing\")} />}\n </ul>\n {!canCompose && (\n <div className=\"ecw-closed\">\n <p className=\"ecw-notice\" role=\"status\">{tr(\"sessionClosed\")}</p>\n <button type=\"button\" className=\"ecw-submit\" onClick={onNewConversation}>\n {tr(\"newConversation\")}\n </button>\n </div>\n )}\n <form className=\"ecw-composer\" onSubmit={submit}>\n <label className=\"ecw-sr-only\" htmlFor={`${uid}-input`}>{tr(\"message\")}</label>\n <input\n id={`${uid}-input`}\n ref={firstFieldRef}\n className=\"ecw-input\"\n type=\"text\"\n autoComplete=\"off\"\n placeholder={tr(\"message\")}\n value={draft}\n onChange={handleDraftChange}\n onBlur={handleDraftBlur}\n disabled={!canCompose}\n />\n <button className=\"ecw-send\" type=\"submit\" disabled={!canCompose || draft.trim() === \"\" || sending} aria-label={sending ? tr(\"sending\") : tr(\"send\")}>\n {sending ? <span className=\"ecw-sr-only\">{tr(\"sending\")}</span> : <SendIcon />}\n </button>\n </form>\n </>\n );\n}\n\nexport function ChatWidget(props: ChatWidgetProps): ReactElement {\n const { endpoint, locale, welcome, projectName, realtime, labels } = props;\n const accentColor = props.accentColor ?? DEFAULT_ACCENT;\n\n const tr = useCallback((key: WidgetKey): string => t(locale, key, labels), [locale, labels]);\n const { state, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping } =\n useChat({ endpoint, realtime });\n\n const bubbleRef = useRef<HTMLButtonElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n const firstFieldRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n injectWidgetStyles();\n }, []);\n\n // Foco no primeiro campo ao abrir (form ou composer).\n useEffect(() => {\n if (state.open) firstFieldRef.current?.focus();\n }, [state.open, state.phase]);\n\n // ESC fecha o painel e devolve o foco ao balão.\n useEffect(() => {\n if (!state.open) return;\n const onKeyDown = (e: KeyboardEvent): void => {\n if (e.key !== \"Escape\") return;\n closePanel();\n bubbleRef.current?.focus();\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n document.removeEventListener(\"keydown\", onKeyDown);\n };\n }, [state.open, closePanel]);\n\n // Auto-scroll para a mensagem mais recente (ou os \"3 pontinhos\" de digitação).\n useEffect(() => {\n const list = listRef.current;\n if (list !== null) list.scrollTop = list.scrollHeight;\n }, [state.messages, state.ownerTyping, state.visitorTyping, state.open]);\n\n const handleClose = useCallback((): void => {\n closePanel();\n bubbleRef.current?.focus();\n }, [closePanel]);\n\n // Só uma sessão \"active\" aceita mensagem; \"closed\"/\"failed\" mostram o aviso com a ação\n // de nova conversa (que limpa o storage e devolve o visitante ao pré-chat form).\n const canCompose = state.session?.status === \"active\";\n\n const bubbleLabel = state.unread > 0 ? `${tr(\"openChat\")} (${state.unread})` : tr(\"openChat\");\n\n return (\n <div className=\"ecw-root\" style={{ \"--ecw-accent\": accentColor } as CSSProperties}>\n <button\n ref={bubbleRef}\n type=\"button\"\n className=\"ecw-button\"\n aria-label={bubbleLabel}\n aria-expanded={state.open}\n aria-haspopup=\"dialog\"\n onClick={togglePanel}\n >\n <ChatIcon />\n {state.unread > 0 && <span className=\"ecw-badge\">{state.unread}</span>}\n </button>\n\n {state.open && (\n <section className=\"ecw-panel\" role=\"dialog\" aria-label=\"Chat\">\n <header className=\"ecw-header\">\n <span className=\"ecw-title\">{projectName}</span>\n {state.session !== null && <span className=\"ecw-code\">#{state.session.code}</span>}\n <button type=\"button\" className=\"ecw-close\" aria-label={tr(\"close\")} onClick={handleClose}>\n <span aria-hidden=\"true\">×</span>\n </button>\n </header>\n\n {state.phase === \"chat\" ? (\n <ChatPanel\n messages={state.messages}\n canCompose={canCompose}\n sending={state.sending}\n locale={locale}\n tr={tr}\n listRef={listRef}\n firstFieldRef={firstFieldRef}\n ownerTyping={state.ownerTyping}\n visitorTyping={state.visitorTyping}\n onTyping={notifyTyping}\n onSend={sendMessage}\n onRetry={retryMessage}\n onNewConversation={startNewConversation}\n />\n ) : (\n <PreChatForm\n welcome={welcome}\n tr={tr}\n error={state.error}\n sending={state.sending}\n firstFieldRef={firstFieldRef}\n onSubmit={submitForm}\n />\n )}\n\n <footer className=\"ecw-footer\">\n {tr(\"poweredBy\")}{\" \"}\n <a\n className=\"ecw-powered-link\"\n href=\"https://erlancarreira.com.br\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Erlan Carreira\n </a>\n </footer>\n </section>\n )}\n </div>\n );\n}\n","// src/widget/i18n.ts — dicionários do widget (pt/en/es) + helper `t`.\n//\n// Contrato (Task 9): as 15 chaves abaixo são OBRIGATÓRIAS em todos os idiomas — o tipo\n// `WidgetDictionary = Record<WidgetKey, string>` torna a paridade verificável pelo\n// compilador, e o teste em test/widget/widget.test.tsx a pinar em runtime.\n//\n// `t(locale, key, overrides?)` resolve na ordem: override do consumidor → dicionário do\n// locale → pt (fallback seguro) → própria chave (nunca devolve undefined).\n\nexport type WidgetLocale = \"pt\" | \"en\" | \"es\";\n\nexport const WIDGET_KEYS = [\n \"openChat\",\n \"close\",\n \"name\",\n \"phone\",\n \"message\",\n \"send\",\n \"sending\",\n \"welcomeNotice\",\n \"privacyNote\",\n \"invalidPhone\",\n \"sendError\",\n \"retry\",\n \"sessionClosed\",\n \"newConversation\",\n \"poweredBy\",\n \"typing\",\n] as const;\n\nexport type WidgetKey = (typeof WIDGET_KEYS)[number];\n\n/** Dicionário completo: `Record<WidgetKey, string>` força todas as chaves no compilador. */\nexport type WidgetDictionary = Record<WidgetKey, string>;\n\nexport const pt: WidgetDictionary = {\n openChat: \"Abrir chat\",\n close: \"Fechar\",\n name: \"Nome\",\n phone: \"WhatsApp\",\n message: \"Mensagem\",\n send: \"Enviar\",\n sending: \"Enviando…\",\n welcomeNotice: \"Ao continuar, você entra no grupo de WhatsApp do site com o nosso time.\",\n privacyNote: \"Seus dados são usados apenas para o atendimento.\",\n invalidPhone: \"Informe um número válido com DDD (mínimo 10 dígitos).\",\n sendError: \"Não foi possível enviar. Tente novamente.\",\n retry: \"Tentar novamente\",\n sessionClosed: \"Esta conversa foi encerrada. Abra uma nova para continuar.\",\n newConversation: \"Iniciar nova conversa\",\n poweredBy: \"Powered by\",\n typing: \"digitando…\",\n};\n\nexport const en: WidgetDictionary = {\n openChat: \"Open chat\",\n close: \"Close\",\n name: \"Name\",\n phone: \"WhatsApp\",\n message: \"Message\",\n send: \"Send\",\n sending: \"Sending…\",\n welcomeNotice: \"When you continue, you join the site's WhatsApp group with our team.\",\n privacyNote: \"Your data is used only for support.\",\n invalidPhone: \"Enter a valid number with area code (at least 10 digits).\",\n sendError: \"Couldn't send. Please try again.\",\n retry: \"Try again\",\n sessionClosed: \"This conversation was closed. Start a new one to continue.\",\n newConversation: \"Start a new conversation\",\n poweredBy: \"Powered by\",\n typing: \"typing…\",\n};\n\nexport const es: WidgetDictionary = {\n openChat: \"Abrir chat\",\n close: \"Cerrar\",\n name: \"Nombre\",\n phone: \"WhatsApp\",\n message: \"Mensaje\",\n send: \"Enviar\",\n sending: \"Enviando…\",\n welcomeNotice: \"Al continuar, entras en el grupo de WhatsApp del sitio con nuestro equipo.\",\n privacyNote: \"Tus datos se usan solo para la atención.\",\n invalidPhone: \"Introduce un número válido con área (mínimo 10 dígitos).\",\n sendError: \"No se pudo enviar. Inténtalo de nuevo.\",\n retry: \"Reintentar\",\n sessionClosed: \"Esta conversación fue cerrada. Abre una nueva para continuar.\",\n newConversation: \"Iniciar nueva conversación\",\n poweredBy: \"Powered by\",\n typing: \"escribiendo…\",\n};\n\nexport const dictionaries: Record<WidgetLocale, WidgetDictionary> = { pt, en, es };\n\n/**\n * Traduz `key` para `locale`, permitindo override pontual de copy (`labels` do widget).\n * Nunca lança: chave desconhecida devolve o texto de pt e, na ausência, a própria chave.\n */\nexport function t(\n locale: WidgetLocale,\n key: WidgetKey,\n overrides?: Partial<Record<string, string>>,\n): string {\n const override = overrides?.[key];\n if (override !== undefined) return override;\n const dict: Partial<WidgetDictionary> = dictionaries[locale];\n return dict[key] ?? dictionaries.pt[key] ?? key;\n}\n","// src/widget/styles.ts — injeção idempotente da folha de estilos do widget.\n//\n// O widget é auto-contido: `injectWidgetStyles()` insere `<style id=\"ecw-styles\">` com as\n// classes `.ecw-*` uma única vez (múltiplas chamadas / múltiplas instâncias não duplicam).\n// O texto abaixo é o ESPELHO exato de `src/widget/styles.css` (arquivo publicado no\n// subpath \"./widget/styles.css\" para quem prefere `<link>`); o teste de paridade em\n// test/widget/widget.test.tsx falha se os dois divergirem.\n\nexport const WIDGET_CSS = `/*\n * src/widget/styles.css — folha de estilos do ChatWidget (classes \\`.ecw-*\\`).\n *\n * Fonte canônica do CSS INJETADO em runtime: src/widget/styles.ts espelha este arquivo\n * e injeta \\`<style id=\"ecw-styles\">\\` via \\`injectWidgetStyles()\\` (o widget é\n * auto-contido — o consumidor não precisa importar CSS). O teste de paridade em\n * test/widget/widget.test.tsx garante que os dois textos não divergem.\n * Consumidores que preferem \\`<link>\\` podem importar \"@erlancarreira/evolution-chat/widget/styles.css\".\n *\n * Acessibilidade: \\`color-scheme\\` (dark nativo), \\`:focus-visible\\` sempre visível,\n * contraste AA sobre o accent (texto escuro no verde), e transições desligadas com\n * \\`prefers-reduced-motion\\`.\n */\n\n.ecw-root {\n --ecw-accent: #25d366;\n --ecw-accent-ink: #06251a;\n --ecw-surface: #ffffff;\n --ecw-surface-2: #f1f4f7;\n --ecw-text: #14181d;\n --ecw-muted: #51606e;\n --ecw-border: #d7dde3;\n --ecw-danger: #b3261e;\n --ecw-shadow: 0 8px 28px rgb(9 20 28 / 22%);\n color-scheme: light dark;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n font-size: 14px;\n line-height: 1.45;\n color: var(--ecw-text);\n}\n\n@media (prefers-color-scheme: dark) {\n .ecw-root {\n --ecw-accent-ink: #04170f;\n --ecw-surface: #151a20;\n --ecw-surface-2: #232a33;\n --ecw-text: #e8edf2;\n --ecw-muted: #9aa7b4;\n --ecw-border: #2c343d;\n --ecw-danger: #ff9a8f;\n --ecw-shadow: 0 8px 28px rgb(0 0 0 / 45%);\n }\n}\n\n/* ── balão flutuante ─────────────────────────────────────────────────────── */\n.ecw-button {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 2147483000;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 56px;\n height: 56px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n box-shadow: var(--ecw-shadow);\n cursor: pointer;\n transition: transform 160ms ease, box-shadow 160ms ease;\n}\n.ecw-button:hover { transform: translateY(-2px) scale(1.03); }\n.ecw-button:active { transform: translateY(0) scale(0.98); }\n.ecw-button svg { width: 26px; height: 26px; }\n\n.ecw-badge {\n position: absolute;\n top: -4px;\n right: -4px;\n min-width: 22px;\n height: 22px;\n padding: 0 6px;\n border-radius: 11px;\n border: 2px solid var(--ecw-surface);\n background: var(--ecw-danger);\n color: #fff;\n font-size: 12px;\n font-weight: 700;\n line-height: 18px;\n text-align: center;\n}\n\n/* ── painel ──────────────────────────────────────────────────────────────── */\n.ecw-panel {\n position: fixed;\n right: 20px;\n bottom: 88px;\n z-index: 2147483000;\n display: flex;\n flex-direction: column;\n width: min(380px, calc(100vw - 24px));\n min-width: 320px;\n height: 480px;\n max-height: 70vh;\n overflow: hidden;\n border: 1px solid var(--ecw-border);\n border-radius: 14px;\n background: var(--ecw-surface);\n box-shadow: var(--ecw-shadow);\n animation: ecw-pop 160ms ease-out;\n}\n@keyframes ecw-pop { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }\n\n.ecw-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n}\n.ecw-title { flex: 1; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.ecw-code { font-size: 12px; font-weight: 600; opacity: 0.85; letter-spacing: 0.02em; }\n.ecw-close {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font-size: 18px;\n line-height: 1;\n cursor: pointer;\n}\n.ecw-close:hover { background: rgb(0 0 0 / 12%); }\n\n/* ── lista de mensagens ──────────────────────────────────────────────────── */\n.ecw-list {\n flex: 1;\n margin: 0;\n padding: 12px;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 10px;\n list-style: none;\n background: var(--ecw-surface-2);\n}\n.ecw-item { display: flex; flex-direction: column; max-width: 82%; }\n.ecw-item--visitor { align-self: flex-end; align-items: flex-end; }\n.ecw-item--owner { align-self: flex-start; align-items: flex-start; }\n.ecw-item--system { align-self: center; align-items: center; max-width: 92%; }\n.ecw-system {\n padding: 4px 10px;\n border-radius: 999px;\n background: var(--ecw-surface);\n border: 1px solid var(--ecw-border);\n color: var(--ecw-muted);\n font-size: 11px;\n text-align: center;\n overflow-wrap: anywhere;\n}\n.ecw-bubble {\n padding: 8px 12px;\n border-radius: 12px;\n overflow-wrap: anywhere;\n white-space: pre-wrap;\n}\n.ecw-bubble--visitor {\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n border-bottom-right-radius: 4px;\n}\n.ecw-bubble--owner {\n background: var(--ecw-surface);\n color: var(--ecw-text);\n border: 1px solid var(--ecw-border);\n border-bottom-left-radius: 4px;\n}\n.ecw-meta { display: flex; align-items: center; gap: 6px; margin-top: 2px; font-size: 11px; color: var(--ecw-muted); }\n.ecw-status--failed { color: var(--ecw-danger); }\n.ecw-retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--ecw-danger);\n font: inherit;\n font-size: 11px;\n text-decoration: underline;\n cursor: pointer;\n}\n\n/* ── pré-chat form ───────────────────────────────────────────────────────── */\n.ecw-form { display: flex; flex-direction: column; gap: 12px; padding: 16px 14px; overflow-y: auto; }\n.ecw-welcome { margin: 0; font-weight: 600; }\n.ecw-notice { margin: 0; font-size: 12px; color: var(--ecw-muted); }\n.ecw-field { display: flex; flex-direction: column; gap: 4px; }\n.ecw-label { font-size: 12px; font-weight: 600; color: var(--ecw-text); }\n.ecw-input {\n width: 100%;\n padding: 9px 10px;\n border: 1px solid var(--ecw-border);\n border-radius: 8px;\n background: var(--ecw-surface);\n color: var(--ecw-text);\n font: inherit;\n}\n.ecw-input:focus { outline: 2px solid var(--ecw-accent); outline-offset: 1px; }\n.ecw-input[aria-invalid=\"true\"] { border-color: var(--ecw-danger); }\n.ecw-error { margin: 0; font-size: 12px; color: var(--ecw-danger); }\n.ecw-submit {\n padding: 10px 14px;\n border: none;\n border-radius: 8px;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n font: inherit;\n font-weight: 700;\n cursor: pointer;\n}\n.ecw-submit:disabled { opacity: 0.55; cursor: not-allowed; }\n\n/* ── composer ────────────────────────────────────────────────────────────── */\n.ecw-composer { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ecw-border); background: var(--ecw-surface); }\n.ecw-composer .ecw-input { flex: 1; }\n.ecw-send {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n padding: 0;\n border: none;\n border-radius: 8px;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n cursor: pointer;\n}\n.ecw-send:disabled { opacity: 0.55; cursor: not-allowed; }\n.ecw-send svg { width: 18px; height: 18px; }\n\n/* ── indicador \"digitando…\" (3 pontinhos) ────────────────────────────────── */\n.ecw-typing {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 10px 14px;\n border-radius: 12px;\n border-bottom-left-radius: 4px;\n background: var(--ecw-surface);\n border: 1px solid var(--ecw-border);\n}\n.ecw-typing-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--ecw-muted);\n animation: ecw-typing-bounce 1.2s infinite ease-in-out;\n}\n.ecw-typing-dot:nth-child(2) { animation-delay: 0.18s; }\n.ecw-typing-dot:nth-child(3) { animation-delay: 0.36s; }\n@keyframes ecw-typing-bounce {\n 0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }\n 30% { transform: translateY(-4px); opacity: 1; }\n}\n\n/* ── sessão encerrada / falha ────────────────────────────────────────────── */\n.ecw-closed { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ecw-border); background: var(--ecw-surface); }\n\n.ecw-footer { padding: 6px 12px 10px; font-size: 11px; color: var(--ecw-muted); text-align: center; background: var(--ecw-surface); }\n.ecw-powered-link { color: var(--ecw-accent); font-weight: 600; text-decoration: none; }\n.ecw-powered-link:hover { text-decoration: underline; }\n\n/* ── utilitários ─────────────────────────────────────────────────────────── */\n.ecw-hp { display: none !important; }\n.ecw-sr-only {\n position: absolute;\n width: 1px; height: 1px;\n margin: -1px; padding: 0;\n overflow: hidden;\n clip: rect(0 0 0 0);\n white-space: nowrap;\n border: 0;\n}\n.ecw-button:focus-visible, .ecw-close:focus-visible, .ecw-submit:focus-visible,\n.ecw-send:focus-visible, .ecw-retry:focus-visible {\n outline: 3px solid #1b6fec;\n outline-offset: 2px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ecw-root *, .ecw-root *::before, .ecw-root *::after {\n animation: none !important;\n transition: none !important;\n }\n}\n\n@media (max-width: 420px) {\n .ecw-panel { right: 12px; left: 12px; width: auto; min-width: 0; bottom: 80px; }\n}\n`;\n\nlet injected = false;\n\n/**\n * Insere a folha do widget no `document.head` (idempotente: uma única tag\n * `<style id=\"ecw-styles\">` por documento, não importa quantas vezes seja chamada).\n * No-op fora de browser (SSR / Node) — o consumidor renderiza com `<link>` do subpath.\n */\nexport function injectWidgetStyles(): void {\n if (typeof document === \"undefined\") return;\n if (injected && document.getElementById(\"ecw-styles\") !== null) return;\n const existing = document.getElementById(\"ecw-styles\");\n if (existing !== null) {\n // Alguém (ex.: outra cópia do bundle) já injetou — marca e não duplica.\n injected = true;\n return;\n }\n const style = document.createElement(\"style\");\n style.id = \"ecw-styles\";\n style.textContent = WIDGET_CSS;\n document.head.appendChild(style);\n injected = true;\n}\n","// src/widget/use-chat.ts — máquina de estados do ChatWidget (hook puro, sem JSX).\n//\n// Fases: \"idle\" (painel nunca aberto, sem sessão) → \"form\" (pré-chat) → \"chat\" (sessão\n// ativa). A transição idle→form acontece na primeira abertura; form→chat no POST bem\n// sucedido; qualquer fase→form quando o GET devolve 404 (sessão expirada → storage limpo)\n// ou quando o visitante pede \"nova conversa\" numa sessão closed/failed (`startNewConversation`).\n//\n// Tempo real (Strategy, invisível ao usuário): com token em mãos, `realtime.subscribe`\n// anexa eventos; se o transporte cair (`onStatus(\"closed\")`), o hook liga um POLLING de\n// 5s no GET de histórico com `after=<última createdAt>` até o canal reabrir. Mensagens\n// `owner` que chegam com o painel fechado somam no badge de não lidas.\n//\n// Otimismo: POST anexa uma mensagem `tmp-…` com status \"pending\"; a resposta do servidor\n// SUBSTITUI o tmp (replace) — em falha, o tmp vira \"failed\" e ganha retry.\n\nimport { useCallback, useEffect, useReducer, useRef } from \"react\";\nimport type { ChatEvent, ChatMessage, ChatSessionStatus } from \"../types\";\nimport type { RealtimeHandle } from \"../bridge/types\";\n\n/** Chave de persistência da sessão no localStorage. */\nexport const SESSION_STORAGE_KEY = \"ecw:session\";\n/** Intervalo do fallback de polling quando o canal realtime fecha (ms). */\nexport const POLL_INTERVAL_MS = 5_000;\n/** Faixa (ms) do \"digitando…\" simulado exibido ANTES de uma mensagem recebida. */\nexport const TYPING_PRE_MESSAGE_MIN_MS = 700;\nexport const TYPING_PRE_MESSAGE_MAX_MS = 2200;\n/** Mínimo de dígitos (com DDD) para um WhatsApp ser aceito no form. */\nexport const MIN_PHONE_DIGITS = 10;\n\nexport type ChatPhase = \"idle\" | \"form\" | \"chat\";\n\nexport interface SessionInfo {\n code: string;\n status: ChatSessionStatus;\n visitorName: string;\n}\n\nexport interface ChatState {\n phase: ChatPhase;\n open: boolean;\n session: SessionInfo | null;\n token: string | null;\n messages: ChatMessage[];\n unread: number;\n sending: boolean;\n /** Chave i18n do erro visível (\"sendError\" | \"invalidPhone\") ou null. */\n error: string | null;\n /** True enquanto a DONA da plataforma (atendente) digita → \"3 pontinhos\" no widget. */\n ownerTyping: boolean;\n /** True enquanto o VISITANTE (esta ponta) digita — útil para um painel de atendente. */\n visitorTyping: boolean;\n}\n\ntype Action =\n | { type: \"open\" }\n | { type: \"close\" }\n | { type: \"restore\"; session: SessionInfo; token: string; messages: ChatMessage[] }\n | { type: \"start\"; session: SessionInfo; token: string; messages: ChatMessage[] }\n | { type: \"reset-form\" }\n | { type: \"append\"; message: ChatMessage }\n | { type: \"merge\"; messages: ChatMessage[] }\n | { type: \"replace\"; tmpId: string; message: ChatMessage }\n | { type: \"mark\"; id: string; status: ChatMessage[\"status\"] }\n | { type: \"session-status\"; status: ChatSessionStatus }\n | { type: \"sending\"; value: boolean }\n | { type: \"error\"; value: string | null }\n | { type: \"typing\"; from: \"owner\" | \"visitor\"; isTyping: boolean };\n\nconst initialState: ChatState = {\n phase: \"idle\",\n open: false,\n session: null,\n token: null,\n messages: [],\n unread: 0,\n sending: false,\n error: null,\n ownerTyping: false,\n visitorTyping: false,\n};\n\nfunction withMessage(state: ChatState, message: ChatMessage): ChatState {\n const index = state.messages.findIndex((m) => m.id === message.id);\n let messages: ChatMessage[];\n if (index >= 0) {\n messages = [...state.messages];\n messages[index] = message;\n } else {\n messages = [...state.messages, message];\n }\n const bump = state.open || message.direction !== \"owner\" ? 0 : 1;\n const next: ChatState = { ...state, messages, unread: state.unread + bump };\n // Quando chega uma mensagem da dona, ela parou de \"digitar\" (limpa os 3 pontinhos).\n if (message.direction === \"owner\") next.ownerTyping = false;\n return next;\n}\n\nexport function chatReducer(state: ChatState, action: Action): ChatState {\n switch (action.type) {\n case \"open\":\n return {\n ...state,\n open: true,\n unread: 0,\n phase: state.phase === \"idle\" ? \"form\" : state.phase,\n };\n case \"close\":\n return { ...state, open: false };\n case \"restore\":\n return {\n ...state,\n phase: \"chat\",\n session: action.session,\n token: action.token,\n messages: action.messages,\n };\n case \"start\": {\n // O POST devolve o histórico oficial (inclui a 1ª msg do visitante); qualquer tmp\n // \"pending\" ainda não confirmado pelo servidor é mantido no fim da lista.\n let next: ChatState = {\n ...state,\n phase: \"chat\",\n session: action.session,\n token: action.token,\n messages: action.messages,\n };\n for (const pending of state.messages) {\n const confirmed = action.messages.some(\n (m) => m.direction === \"visitor\" && m.body === pending.body,\n );\n if (pending.status === \"pending\" && !confirmed) next = withMessage(next, pending);\n }\n return next;\n }\n case \"reset-form\":\n return { ...state, phase: \"form\", session: null, token: null, messages: [], unread: 0, error: null };\n case \"append\":\n return withMessage(state, action.message);\n case \"merge\": {\n let next = state;\n for (const message of action.messages) next = withMessage(next, message);\n return next;\n }\n case \"replace\": {\n const messages = state.messages.map((m) => (m.id === action.tmpId ? action.message : m));\n const kept = messages.some((m) => m.id === action.message.id);\n return { ...state, messages: kept ? messages : [...messages, action.message] };\n }\n case \"mark\":\n return {\n ...state,\n messages: state.messages.map((m) => (m.id === action.id ? { ...m, status: action.status } : m)),\n };\n case \"session-status\": {\n if (state.session === null) return state;\n return { ...state, session: { ...state.session, status: action.status } };\n }\n case \"sending\":\n return { ...state, sending: action.value };\n case \"error\":\n return { ...state, error: action.value };\n case \"typing\":\n // Só transiciona se mudar de fato (evita re-render a cada batida de presença).\n return action.from === \"owner\"\n ? state.ownerTyping === action.isTyping\n ? state\n : { ...state, ownerTyping: action.isTyping }\n : state.visitorTyping === action.isTyping\n ? state\n : { ...state, visitorTyping: action.isTyping };\n }\n}\n\n// ─── helpers de telefone ──────────────────────────────────────────────────────\n\n/** Só dígitos (o servidor espera DDI+DDD+número sem máscara). */\nexport function normalizePhone(value: string): string {\n return value.replace(/\\D/g, \"\");\n}\n\n/** Máscara BR parcial `(11) 99999-8888`, progressiva enquanto o usuário digita. */\nexport function maskPhone(value: string): string {\n const digits = normalizePhone(value).slice(0, 11);\n if (digits.length === 0) return \"\";\n const ddd = digits.slice(0, 2);\n if (digits.length <= 2) return `(${ddd}`;\n const rest = digits.slice(2);\n if (rest.length <= 5) return `(${ddd}) ${rest}`;\n return `(${ddd}) ${rest.slice(0, 5)}-${rest.slice(5)}`;\n}\n\nexport function isValidPhone(value: string): boolean {\n return normalizePhone(value).length >= MIN_PHONE_DIGITS;\n}\n\n// ─── persistência ─────────────────────────────────────────────────────────────\n\ninterface StoredSession {\n token: string;\n code: string;\n}\n\nfunction readStoredSession(): StoredSession | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(SESSION_STORAGE_KEY);\n if (raw === null) return null;\n const parsed: unknown = JSON.parse(raw);\n if (\n parsed !== null &&\n typeof parsed === \"object\" &&\n typeof (parsed as StoredSession).token === \"string\" &&\n (parsed as StoredSession).token !== \"\"\n ) {\n const { token, code } = parsed as StoredSession;\n return { token, code: typeof code === \"string\" ? code : \"\" };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction writeStoredSession(session: StoredSession): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));\n } catch {\n /* storage cheio/bloqueado: sessão só não sobrevive ao reload */\n }\n}\n\nfunction clearStoredSession(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(SESSION_STORAGE_KEY);\n } catch {\n /* idem */\n }\n}\n\n// ─── contrato de rede (espelha createChatRoutes — Task 8) ────────────────────\n\ninterface HistoryResponse {\n session?: SessionInfo;\n messages?: ChatMessage[];\n}\n\ninterface StartResponse {\n session?: SessionInfo & { realtimeToken?: string };\n messages?: ChatMessage[];\n}\n\ninterface SendResponse {\n message?: ChatMessage;\n}\n\nfunction historyUrl(endpoint: string, token: string, after: string | null): string {\n const params = new URLSearchParams({ token });\n if (after !== null) params.set(\"after\", after);\n const sep = endpoint.includes(\"?\") ? \"&\" : \"?\";\n return `${endpoint}${sep}${params.toString()}`;\n}\n\nfunction lastCursor(messages: ChatMessage[]): string | null {\n let latest: string | null = null;\n for (const m of messages) if (latest === null || m.createdAt > latest) latest = m.createdAt;\n return latest;\n}\n\n// ─── hook ─────────────────────────────────────────────────────────────────────\n\nexport interface UseChatOptions {\n endpoint: string;\n realtime: RealtimeHandle;\n /** Rota que recebe o sinal \"visitante digitando\" (POST { token, isTyping }). Default: `${endpoint}/typing`. */\n typingEndpoint?: string;\n}\n\nexport interface UseChatResult {\n state: ChatState;\n openPanel(): void;\n closePanel(): void;\n togglePanel(): void;\n submitForm(input: { name: string; phone: string; message: string; honeypot: string }): Promise<void>;\n sendMessage(text: string): Promise<void>;\n retryMessage(id: string): Promise<void>;\n /** Descarta a sessão encerrada/falha (storage + estado) e volta ao pré-chat form. */\n startNewConversation(): void;\n /** Avisa o servidor que o visitante está (true) ou parou (false) de digitar. Best-effort. */\n notifyTyping(isTyping: boolean): Promise<void>;\n}\n\nlet tmpSeq = 0;\nfunction tmpMessage(body: string): ChatMessage {\n tmpSeq += 1;\n return {\n id: `tmp-${tmpSeq}`,\n sessionId: \"\",\n direction: \"visitor\",\n body,\n status: \"pending\",\n waMessageId: null,\n createdAt: new Date().toISOString(),\n };\n}\n\nexport function useChat({ endpoint, realtime, typingEndpoint }: UseChatOptions): UseChatResult {\n const [state, dispatch] = useReducer(chatReducer, initialState);\n\n const typingUrl = typingEndpoint ?? `${endpoint.replace(/\\/+$/, \"\")}/typing`;\n\n // Espelho do estado para callbacks estáveis (eventos realtime/poll chegam fora do React).\n const stateRef = useRef(state);\n useEffect(() => {\n stateRef.current = state;\n }, [state]);\n\n // A porta pode chegar como objeto novo a cada render do consumidor; a assinatura deve\n // depender apenas do token, nunca da identidade do handle.\n const realtimeRef = useRef(realtime);\n useEffect(() => {\n realtimeRef.current = realtime;\n }, [realtime]);\n\n const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const stopPoll = useCallback((): void => {\n if (pollRef.current !== null) {\n clearInterval(pollRef.current);\n pollRef.current = null;\n }\n }, []);\n\n const pollOnce = useCallback(async (): Promise<void> => {\n const { token, messages } = stateRef.current;\n if (token === null) return;\n try {\n const res = await fetch(historyUrl(endpoint, token, lastCursor(messages)), {\n method: \"GET\",\n headers: { accept: \"application/json\" },\n });\n if (res.status === 404) {\n clearStoredSession();\n stopPoll();\n dispatch({ type: \"reset-form\" });\n return;\n }\n if (!res.ok) return;\n const data = (await res.json()) as HistoryResponse;\n if (Array.isArray(data.messages)) dispatch({ type: \"merge\", messages: data.messages });\n if (data.session !== undefined) dispatch({ type: \"session-status\", status: data.session.status });\n } catch {\n /* offline: o próximo tick tenta de novo */\n }\n }, [endpoint, stopPoll]);\n\n const startPoll = useCallback((): void => {\n if (pollRef.current !== null) return;\n pollRef.current = setInterval(() => {\n void pollOnce();\n }, POLL_INTERVAL_MS);\n }, [pollOnce]);\n\n // Abertura/fechamento do canal realtime (Strategy): caiu → polling; voltou → para.\n const handleStatus = useCallback(\n (s: \"open\" | \"closed\"): void => {\n if (s === \"closed\") startPoll();\n else stopPoll();\n },\n [startPoll, stopPoll],\n );\n\n const handleEvent = useCallback((e: ChatEvent): void => {\n if (e.type === \"message\" && e.message !== undefined) {\n const msg = e.message;\n // Mensagem recebida da DONA (staff): como a instância conectada não emite presença\n // do próprio \"digitando\" (Evolution/Baileys), simulamos o \"3 pontinhos\" antes de\n // exibir a mensagem — dá o feedback visual de que a outra ponta está respondendo.\n if (msg.direction === \"owner\") {\n dispatch({ type: \"typing\", from: \"owner\", isTyping: true });\n const text = typeof msg.body === \"string\" ? msg.body : \"\";\n const delay = Math.min(\n TYPING_PRE_MESSAGE_MAX_MS,\n Math.max(TYPING_PRE_MESSAGE_MIN_MS, text.length * 22),\n );\n setTimeout(() => {\n dispatch({ type: \"append\", message: msg });\n }, delay);\n } else {\n dispatch({ type: \"append\", message: msg });\n }\n } else if (e.type === \"session\" && e.status !== undefined) {\n dispatch({ type: \"session-status\", status: e.status });\n } else if (e.type === \"typing\" && e.from !== undefined) {\n dispatch({ type: \"typing\", from: e.from, isTyping: e.isTyping });\n }\n }, []);\n\n // Boot: sessão persistida → GET de histórico (404 limpa o storage e volta ao form).\n useEffect(() => {\n const stored = readStoredSession();\n if (stored === null) return;\n let cancelled = false;\n void (async () => {\n try {\n const res = await fetch(historyUrl(endpoint, stored.token, null), {\n method: \"GET\",\n headers: { accept: \"application/json\" },\n });\n if (cancelled) return;\n if (res.status === 404) {\n clearStoredSession();\n dispatch({ type: \"reset-form\" });\n return;\n }\n if (!res.ok) return;\n const data = (await res.json()) as HistoryResponse;\n if (cancelled || data.session === undefined) return;\n dispatch({\n type: \"restore\",\n session: data.session,\n token: stored.token,\n messages: Array.isArray(data.messages) ? data.messages : [],\n });\n } catch {\n /* sem rede: mantém o form/idle; o retry fica com o usuário */\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [endpoint]);\n\n // Assinatura do canal enquanto houver token (re-assina quando a sessão muda).\n useEffect(() => {\n const token = state.token;\n if (token === null) return;\n const unsubscribe = realtimeRef.current.subscribe(token, handleEvent, handleStatus);\n return () => {\n unsubscribe();\n stopPoll();\n };\n }, [state.token, handleEvent, handleStatus, stopPoll]);\n\n useEffect(() => stopPoll, [stopPoll]);\n\n const openPanel = useCallback((): void => {\n dispatch({ type: \"open\" });\n }, []);\n\n const closePanel = useCallback((): void => {\n dispatch({ type: \"close\" });\n }, []);\n\n const togglePanel = useCallback((): void => {\n if (stateRef.current.open) dispatch({ type: \"close\" });\n else dispatch({ type: \"open\" });\n }, []);\n\n const postMessage = useCallback(\n async (token: string, body: string, id: string): Promise<void> => {\n dispatch({ type: \"sending\", value: true });\n dispatch({ type: \"error\", value: null });\n try {\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ token, message: body }),\n });\n if (!res.ok) {\n dispatch({ type: \"mark\", id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n return;\n }\n const data = (await res.json()) as SendResponse;\n if (data.message !== undefined) dispatch({ type: \"replace\", tmpId: id, message: data.message });\n else dispatch({ type: \"mark\", id, status: \"failed\" });\n } catch {\n dispatch({ type: \"mark\", id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n } finally {\n dispatch({ type: \"sending\", value: false });\n }\n },\n [endpoint],\n );\n\n const sendMessage = useCallback(\n async (text: string): Promise<void> => {\n const body = text.trim();\n const token = stateRef.current.token;\n if (body === \"\" || token === null) return;\n const optimistic = tmpMessage(body);\n dispatch({ type: \"append\", message: optimistic });\n await postMessage(token, body, optimistic.id);\n },\n [postMessage],\n );\n\n const retryMessage = useCallback(\n async (id: string): Promise<void> => {\n const token = stateRef.current.token;\n const target = stateRef.current.messages.find((m) => m.id === id);\n if (token === null || target === undefined || target.direction !== \"visitor\") return;\n dispatch({ type: \"mark\", id, status: \"pending\" });\n await postMessage(token, target.body, id);\n },\n [postMessage],\n );\n\n // Sessão closed/failed: o visitante resolve o beco sem saída pedindo uma conversa nova.\n // Limpa o storage (senão o próximo boot restauraria a mesma sessão morta) e volta ao\n // form; a assinatura realtime cai sozinha na limpeza do efeito, porque token → null.\n const startNewConversation = useCallback((): void => {\n clearStoredSession();\n dispatch({ type: \"reset-form\" });\n }, []);\n\n // Avisa o servidor que o visitante está (ou parou de) digitar. Best-effort: nunca\n // deve quebrar o fluxo de envio de mensagens — falhas são silenciadas.\n const notifyTyping = useCallback(\n async (isTyping: boolean): Promise<void> => {\n const token = stateRef.current.token;\n if (token === null) return;\n try {\n await fetch(typingUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ token, isTyping }),\n });\n } catch {\n /* offline/erro: digitação é cosmética, ignora */\n }\n },\n [typingUrl],\n );\n\n const submitForm = useCallback(\n async ({ name, phone, message, honeypot }: { name: string; phone: string; message: string; honeypot: string }): Promise<void> => {\n // Anti-bot silencioso: honeypot preenchido → nada sai do navegador (o servidor\n // também fingiria sucesso; aqui nem há request).\n if (honeypot.trim() !== \"\") return;\n const digits = normalizePhone(phone);\n if (name.trim() === \"\" || message.trim() === \"\" || digits.length < MIN_PHONE_DIGITS) {\n dispatch({ type: \"error\", value: digits.length < MIN_PHONE_DIGITS ? \"invalidPhone\" : \"sendError\" });\n return;\n }\n const optimistic = tmpMessage(message.trim());\n dispatch({ type: \"append\", message: optimistic });\n dispatch({ type: \"sending\", value: true });\n dispatch({ type: \"error\", value: null });\n try {\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ name: name.trim(), phone: digits, message: message.trim(), honeypot: \"\" }),\n });\n if (!res.ok) {\n dispatch({ type: \"mark\", id: optimistic.id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n return;\n }\n const data = (await res.json()) as StartResponse;\n const session = data.session;\n if (session === undefined || typeof session.realtimeToken !== \"string\" || session.realtimeToken === \"\") {\n dispatch({ type: \"mark\", id: optimistic.id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n return;\n }\n const token = session.realtimeToken;\n writeStoredSession({ token, code: session.code });\n dispatch({\n type: \"start\",\n session: { code: session.code, status: session.status, visitorName: session.visitorName },\n token,\n messages: Array.isArray(data.messages) ? data.messages : [],\n });\n } catch {\n dispatch({ type: \"mark\", id: optimistic.id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n } finally {\n dispatch({ type: \"sending\", value: false });\n }\n },\n [endpoint],\n );\n\n return { state, openPanel, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping };\n}\n"],"mappings":"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","names":["import_react"]}
|
package/widget/index.js
CHANGED
|
@@ -941,14 +941,18 @@ function PreChatForm({ welcome, tr, error, sending, firstFieldRef, onSubmit }) {
|
|
|
941
941
|
/* @__PURE__ */ jsx("p", { className: "ecw-notice", children: tr("privacyNote") })
|
|
942
942
|
] });
|
|
943
943
|
}
|
|
944
|
-
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping,
|
|
944
|
+
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, onTyping, onSend, onRetry, onNewConversation }) {
|
|
945
945
|
const uid = useId();
|
|
946
946
|
const [draft, setDraft] = useState("");
|
|
947
947
|
const typingRef = useRef2(false);
|
|
948
948
|
const typingTimer = useRef2(null);
|
|
949
|
+
const selfTypingRef = useRef2(false);
|
|
950
|
+
const selfTimer = useRef2(null);
|
|
951
|
+
const [selfTyping, setSelfTyping] = useState(false);
|
|
949
952
|
useEffect2(() => {
|
|
950
953
|
return () => {
|
|
951
954
|
if (typingTimer.current !== null) clearTimeout(typingTimer.current);
|
|
955
|
+
if (selfTimer.current !== null) clearTimeout(selfTimer.current);
|
|
952
956
|
};
|
|
953
957
|
}, []);
|
|
954
958
|
const stopTypingSignal = useCallback2(() => {
|
|
@@ -961,6 +965,16 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
|
|
|
961
965
|
void onTyping(false);
|
|
962
966
|
}
|
|
963
967
|
}, [onTyping]);
|
|
968
|
+
const stopSelfTyping = useCallback2(() => {
|
|
969
|
+
if (selfTimer.current !== null) {
|
|
970
|
+
clearTimeout(selfTimer.current);
|
|
971
|
+
selfTimer.current = null;
|
|
972
|
+
}
|
|
973
|
+
if (selfTypingRef.current) {
|
|
974
|
+
selfTypingRef.current = false;
|
|
975
|
+
setSelfTyping(false);
|
|
976
|
+
}
|
|
977
|
+
}, []);
|
|
964
978
|
const handleDraftChange = (e) => {
|
|
965
979
|
setDraft(e.target.value);
|
|
966
980
|
if (!canCompose) return;
|
|
@@ -970,15 +984,23 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
|
|
|
970
984
|
}
|
|
971
985
|
if (typingTimer.current !== null) clearTimeout(typingTimer.current);
|
|
972
986
|
typingTimer.current = setTimeout(stopTypingSignal, 2500);
|
|
987
|
+
if (!selfTypingRef.current) {
|
|
988
|
+
selfTypingRef.current = true;
|
|
989
|
+
setSelfTyping(true);
|
|
990
|
+
}
|
|
991
|
+
if (selfTimer.current !== null) clearTimeout(selfTimer.current);
|
|
992
|
+
selfTimer.current = setTimeout(stopSelfTyping, 2500);
|
|
973
993
|
};
|
|
974
994
|
const handleDraftBlur = () => {
|
|
975
995
|
stopTypingSignal();
|
|
996
|
+
stopSelfTyping();
|
|
976
997
|
};
|
|
977
998
|
const submit = (e) => {
|
|
978
999
|
e.preventDefault();
|
|
979
1000
|
const text = draft.trim();
|
|
980
1001
|
if (text === "" || !canCompose) return;
|
|
981
1002
|
stopTypingSignal();
|
|
1003
|
+
stopSelfTyping();
|
|
982
1004
|
setDraft("");
|
|
983
1005
|
void onSend(text);
|
|
984
1006
|
};
|
|
@@ -997,7 +1019,7 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
|
|
|
997
1019
|
] }, m.id)
|
|
998
1020
|
),
|
|
999
1021
|
ownerTyping && canCompose && /* @__PURE__ */ jsx(TypingIndicator, { from: "owner", label: tr("typing") }),
|
|
1000
|
-
|
|
1022
|
+
selfTyping && canCompose && /* @__PURE__ */ jsx(TypingIndicator, { from: "visitor", label: tr("typing") })
|
|
1001
1023
|
] }),
|
|
1002
1024
|
!canCompose && /* @__PURE__ */ jsxs("div", { className: "ecw-closed", children: [
|
|
1003
1025
|
/* @__PURE__ */ jsx("p", { className: "ecw-notice", role: "status", children: tr("sessionClosed") }),
|
package/widget/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/widget/chat-widget.tsx","../src/widget/i18n.ts","../src/widget/styles.ts","../src/widget/use-chat.ts"],"sourcesContent":["// src/widget/chat-widget.tsx — ChatWidget: balão flutuante + painel de conversa.\n//\n// Sem Tailwind / lib de UI: toda a aparência vem de `injectWidgetStyles()` (classes\n// `.ecw-*`), e o único estilo inline é a CSS var `--ecw-accent` (cor do tema). O estado\n// inteiro vive em `use-chat.ts` (reducer + rede + realtime); aqui só render e foco.\n//\n// Acessibilidade (contrato da spec §2):\n// - balão: <button> com aria-label (i18n), aria-expanded e aria-haspopup=\"dialog\";\n// - painel: role=\"dialog\" aria-label=\"Chat\"; foco no primeiro campo ao abrir;\n// ESC fecha e devolve o foco ao balão; clique fora NÃO fecha (mobile-friendly);\n// - todo input tem <label htmlFor> (useId → múltiplas instâncias sem colisão);\n// - badge de não lidas refletido no nome acessível do balão;\n// - sessão closed/failed: composer desabilitado, aviso em role=\"status\" e botão real\n// \"nova conversa\" (o beco sem saída tem saída acessível por teclado);\n// - prefers-reduced-motion desliga animações (via CSS).\n\nimport {\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type CSSProperties,\n type ChangeEvent,\n type FormEvent,\n type ReactElement,\n type RefObject,\n} from \"react\";\nimport type { RealtimeHandle } from \"../bridge/types\";\nimport type { ChatMessage } from \"../types\";\nimport { t, type WidgetKey, type WidgetLocale } from \"./i18n\";\nimport { injectWidgetStyles } from \"./styles\";\nimport { isValidPhone, maskPhone, useChat } from \"./use-chat\";\n\nexport type { WidgetLocale };\n\nexport interface ChatWidgetProps {\n /** Caminho da rota de chat (GET histórico / POST start+send), ex.: \"/api/chat\". */\n endpoint: string;\n locale: WidgetLocale;\n /** Saudação exibida no pré-chat form. */\n welcome: string;\n projectName: string;\n /** Cor de tema; default \"#25D366\". */\n accentColor?: string;\n /** Porta do widget (DI — Strategy): subscribe/unsubscribe do canal da sessão. */\n realtime: RealtimeHandle;\n /** Override pontual de copy por chave i18n. */\n labels?: Partial<Record<string, string>>;\n}\n\nconst DEFAULT_ACCENT = \"#25D366\";\n\nfunction ChatIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M21 11.5a8.38 8.38 0 0 1-8.5 8.5c-1.5 0-2.9-.36-4.13-1L3 20l1.1-3.85A8.36 8.36 0 0 1 12.5 3 8.38 8.38 0 0 1 21 11.5z\" />\n </svg>\n );\n}\n\nfunction SendIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M22 2 11 13\" />\n <path d=\"M22 2 15 22l-4-9-9-4 20-7z\" />\n </svg>\n );\n}\n\nfunction formatTime(iso: string, locale: WidgetLocale): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return \"\";\n return new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" }).format(date);\n}\n\nfunction StatusMark({ status }: { status: ChatMessage[\"status\"] }): ReactElement {\n // ✓ = aceita pelo servidor (pending), ✓✓ = enviada, ⚠ = falhou.\n // Decorativo por design: o conjunto de chaves i18n é fixo (15) e o estado \"failed\" já\n // é anunciado de forma acessível pelo botão visível `retry` ao lado — um texto sr-only\n // reaproveitando \"send\"/\"sending\" soaria a ruído para quem usa leitor de tela.\n const glyph = status === \"pending\" ? \"✓\" : status === \"sent\" ? \"✓✓\" : \"⚠\";\n return (\n <span className={status === \"failed\" ? \"ecw-status ecw-status--failed\" : \"ecw-status\"} aria-hidden=\"true\">\n {glyph}\n </span>\n );\n}\n\n/** \"3 pontinhos\" de digitação: bolha com três pontos animados + rótulo acessível.\n * `from` alinha o indicador ao lado de quem representa (owner→esquerda, visitor→direita). */\nfunction TypingIndicator({ from, label }: { from: \"owner\" | \"visitor\"; label: string }): ReactElement {\n return (\n <li className={`ecw-item ecw-item--${from}`} aria-live=\"polite\">\n <div className=\"ecw-typing\" role=\"status\" aria-label={label}>\n <span className=\"ecw-typing-dot\" />\n <span className=\"ecw-typing-dot\" />\n <span className=\"ecw-typing-dot\" />\n <span className=\"ecw-sr-only\">{label}</span>\n </div>\n </li>\n );\n}\n\ninterface FormProps {\n welcome: string;\n tr: (k: WidgetKey) => string;\n error: string | null;\n sending: boolean;\n firstFieldRef: RefObject<HTMLInputElement>;\n onSubmit(values: { name: string; phone: string; message: string; honeypot: string }): Promise<void>;\n}\n\nfunction PreChatForm({ welcome, tr, error, sending, firstFieldRef, onSubmit }: FormProps): ReactElement {\n const uid = useId();\n const [name, setName] = useState(\"\");\n const [phone, setPhone] = useState(\"\");\n const [message, setMessage] = useState(\"\");\n const [honeypot, setHoneypot] = useState(\"\");\n const [phoneTouched, setPhoneTouched] = useState(false);\n\n const phoneValid = isValidPhone(phone);\n const complete = name.trim() !== \"\" && phoneValid && message.trim() !== \"\";\n\n const handleSubmit = (e: FormEvent): void => {\n e.preventDefault();\n if (!complete || sending) return;\n void onSubmit({ name, phone, message, honeypot });\n };\n\n return (\n <form className=\"ecw-form\" onSubmit={handleSubmit} noValidate>\n <p className=\"ecw-welcome\">{welcome}</p>\n <p className=\"ecw-notice\">{tr(\"welcomeNotice\")}</p>\n\n <div className=\"ecw-field\">\n <label className=\"ecw-label\" htmlFor={`${uid}-name`}>{tr(\"name\")}</label>\n <input\n id={`${uid}-name`}\n ref={firstFieldRef}\n className=\"ecw-input\"\n type=\"text\"\n autoComplete=\"name\"\n value={name}\n onChange={(e) => { setName(e.target.value); }}\n required\n />\n </div>\n\n <div className=\"ecw-field\">\n <label className=\"ecw-label\" htmlFor={`${uid}-phone`}>{tr(\"phone\")}</label>\n <input\n id={`${uid}-phone`}\n className=\"ecw-input\"\n type=\"tel\"\n inputMode=\"tel\"\n autoComplete=\"tel\"\n placeholder=\"(11) 99999-8888\"\n value={phone}\n onChange={(e) => { setPhone(maskPhone(e.target.value)); }}\n onBlur={() => { setPhoneTouched(true); }}\n aria-invalid={phoneTouched && !phoneValid ? true : undefined}\n aria-describedby={phoneTouched && !phoneValid ? `${uid}-phone-err` : undefined}\n required\n />\n {phoneTouched && !phoneValid && (\n <p className=\"ecw-error\" id={`${uid}-phone-err`} role=\"alert\">{tr(\"invalidPhone\")}</p>\n )}\n </div>\n\n <div className=\"ecw-field\">\n <label className=\"ecw-label\" htmlFor={`${uid}-message`}>{tr(\"message\")}</label>\n <textarea\n id={`${uid}-message`}\n className=\"ecw-input\"\n rows={3}\n autoComplete=\"off\"\n value={message}\n onChange={(e) => { setMessage(e.target.value); }}\n required\n />\n </div>\n\n {/* Honeypot anti-bot: invisível para humanos (display:none via .ecw-hp),\n fora da ordem de tabulação e escondido do leitor de tela. */}\n <div className=\"ecw-hp\" aria-hidden=\"true\">\n <label htmlFor={`${uid}-website`}>Website</label>\n <input\n id={`${uid}-website`}\n name=\"website\"\n type=\"text\"\n tabIndex={-1}\n autoComplete=\"off\"\n value={honeypot}\n onChange={(e) => { setHoneypot(e.target.value); }}\n />\n </div>\n\n {error === \"sendError\" && (\n <p className=\"ecw-error\" role=\"alert\">{tr(\"sendError\")}</p>\n )}\n\n <button className=\"ecw-submit\" type=\"submit\" disabled={!complete || sending}>\n {sending ? tr(\"sending\") : tr(\"send\")}\n </button>\n <p className=\"ecw-notice\">{tr(\"privacyNote\")}</p>\n </form>\n );\n}\n\ninterface ChatPanelProps {\n messages: ChatMessage[];\n /** Só `status === \"active\"` compõe; closed/failed mostram aviso + \"nova conversa\". */\n canCompose: boolean;\n sending: boolean;\n locale: WidgetLocale;\n tr: (k: WidgetKey) => string;\n listRef: RefObject<HTMLUListElement>;\n firstFieldRef: RefObject<HTMLInputElement>;\n /** True enquanto a dona da plataforma (atendente) digita → mostra os \"3 pontinhos\". */\n ownerTyping: boolean;\n /** True enquanto o VISITANTE (esta ponta) digita → \"3 pontinhos\" do lado do visitante. */\n visitorTyping: boolean;\n /** Avisa o servidor que o visitante está/parou de digitar. Best-effort. */\n onTyping(isTyping: boolean): Promise<void>;\n onSend(text: string): Promise<void>;\n onRetry(id: string): Promise<void>;\n onNewConversation(): void;\n}\n\nfunction ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, visitorTyping, onTyping, onSend, onRetry, onNewConversation }: ChatPanelProps): ReactElement {\n const uid = useId();\n const [draft, setDraft] = useState(\"\");\n\n // Sinal \"visitante digitando\": dispara `true` só na TRANSIÇÃO para digitando (não a\n // cada tecla) e `false` após 2,5s de inatividade, ao enviar ou ao perder o foco —\n // evita um POST por caractere. `onChange` já cobre keyup, cola e IME (mais robusto\n // que keyup puro), então sincroniza com a digitação real e não com o evento bruto.\n const typingRef = useRef(false);\n const typingTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(() => {\n return () => {\n if (typingTimer.current !== null) clearTimeout(typingTimer.current);\n };\n }, []);\n\n const stopTypingSignal = useCallback((): void => {\n if (typingTimer.current !== null) {\n clearTimeout(typingTimer.current);\n typingTimer.current = null;\n }\n if (typingRef.current) {\n typingRef.current = false;\n void onTyping(false);\n }\n }, [onTyping]);\n\n const handleDraftChange = (e: ChangeEvent<HTMLInputElement>): void => {\n setDraft(e.target.value);\n if (!canCompose) return;\n if (!typingRef.current) {\n typingRef.current = true;\n void onTyping(true);\n }\n if (typingTimer.current !== null) clearTimeout(typingTimer.current);\n typingTimer.current = setTimeout(stopTypingSignal, 2500);\n };\n\n const handleDraftBlur = (): void => {\n stopTypingSignal();\n };\n\n const submit = (e: FormEvent): void => {\n e.preventDefault();\n const text = draft.trim();\n if (text === \"\" || !canCompose) return;\n stopTypingSignal();\n setDraft(\"\");\n void onSend(text);\n };\n\n return (\n <>\n <ul ref={listRef} className=\"ecw-list\">\n {messages.map((m) =>\n m.direction === \"system\" ? (\n <li key={m.id} className=\"ecw-item ecw-item--system\" role=\"status\">\n <div className=\"ecw-system\">{m.body}</div>\n </li>\n ) : (\n <li key={m.id} className={`ecw-item ecw-item--${m.direction}`}>\n <div className={`ecw-bubble ecw-bubble--${m.direction}`}>{m.body}</div>\n <div className=\"ecw-meta\">\n <time dateTime={m.createdAt}>{formatTime(m.createdAt, locale)}</time>\n {m.direction === \"visitor\" && <StatusMark status={m.status} />}\n {m.status === \"failed\" && (\n <button type=\"button\" className=\"ecw-retry\" onClick={() => { void onRetry(m.id); }}>\n {tr(\"retry\")}\n </button>\n )}\n </div>\n </li>\n ),\n )}\n {ownerTyping && canCompose && <TypingIndicator from=\"owner\" label={tr(\"typing\")} />}\n {visitorTyping && canCompose && <TypingIndicator from=\"visitor\" label={tr(\"typing\")} />}\n </ul>\n {!canCompose && (\n <div className=\"ecw-closed\">\n <p className=\"ecw-notice\" role=\"status\">{tr(\"sessionClosed\")}</p>\n <button type=\"button\" className=\"ecw-submit\" onClick={onNewConversation}>\n {tr(\"newConversation\")}\n </button>\n </div>\n )}\n <form className=\"ecw-composer\" onSubmit={submit}>\n <label className=\"ecw-sr-only\" htmlFor={`${uid}-input`}>{tr(\"message\")}</label>\n <input\n id={`${uid}-input`}\n ref={firstFieldRef}\n className=\"ecw-input\"\n type=\"text\"\n autoComplete=\"off\"\n placeholder={tr(\"message\")}\n value={draft}\n onChange={handleDraftChange}\n onBlur={handleDraftBlur}\n disabled={!canCompose}\n />\n <button className=\"ecw-send\" type=\"submit\" disabled={!canCompose || draft.trim() === \"\" || sending} aria-label={sending ? tr(\"sending\") : tr(\"send\")}>\n {sending ? <span className=\"ecw-sr-only\">{tr(\"sending\")}</span> : <SendIcon />}\n </button>\n </form>\n </>\n );\n}\n\nexport function ChatWidget(props: ChatWidgetProps): ReactElement {\n const { endpoint, locale, welcome, projectName, realtime, labels } = props;\n const accentColor = props.accentColor ?? DEFAULT_ACCENT;\n\n const tr = useCallback((key: WidgetKey): string => t(locale, key, labels), [locale, labels]);\n const { state, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping } =\n useChat({ endpoint, realtime });\n\n const bubbleRef = useRef<HTMLButtonElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n const firstFieldRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n injectWidgetStyles();\n }, []);\n\n // Foco no primeiro campo ao abrir (form ou composer).\n useEffect(() => {\n if (state.open) firstFieldRef.current?.focus();\n }, [state.open, state.phase]);\n\n // ESC fecha o painel e devolve o foco ao balão.\n useEffect(() => {\n if (!state.open) return;\n const onKeyDown = (e: KeyboardEvent): void => {\n if (e.key !== \"Escape\") return;\n closePanel();\n bubbleRef.current?.focus();\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n document.removeEventListener(\"keydown\", onKeyDown);\n };\n }, [state.open, closePanel]);\n\n // Auto-scroll para a mensagem mais recente (ou os \"3 pontinhos\" de digitação).\n useEffect(() => {\n const list = listRef.current;\n if (list !== null) list.scrollTop = list.scrollHeight;\n }, [state.messages, state.ownerTyping, state.visitorTyping, state.open]);\n\n const handleClose = useCallback((): void => {\n closePanel();\n bubbleRef.current?.focus();\n }, [closePanel]);\n\n // Só uma sessão \"active\" aceita mensagem; \"closed\"/\"failed\" mostram o aviso com a ação\n // de nova conversa (que limpa o storage e devolve o visitante ao pré-chat form).\n const canCompose = state.session?.status === \"active\";\n\n const bubbleLabel = state.unread > 0 ? `${tr(\"openChat\")} (${state.unread})` : tr(\"openChat\");\n\n return (\n <div className=\"ecw-root\" style={{ \"--ecw-accent\": accentColor } as CSSProperties}>\n <button\n ref={bubbleRef}\n type=\"button\"\n className=\"ecw-button\"\n aria-label={bubbleLabel}\n aria-expanded={state.open}\n aria-haspopup=\"dialog\"\n onClick={togglePanel}\n >\n <ChatIcon />\n {state.unread > 0 && <span className=\"ecw-badge\">{state.unread}</span>}\n </button>\n\n {state.open && (\n <section className=\"ecw-panel\" role=\"dialog\" aria-label=\"Chat\">\n <header className=\"ecw-header\">\n <span className=\"ecw-title\">{projectName}</span>\n {state.session !== null && <span className=\"ecw-code\">#{state.session.code}</span>}\n <button type=\"button\" className=\"ecw-close\" aria-label={tr(\"close\")} onClick={handleClose}>\n <span aria-hidden=\"true\">×</span>\n </button>\n </header>\n\n {state.phase === \"chat\" ? (\n <ChatPanel\n messages={state.messages}\n canCompose={canCompose}\n sending={state.sending}\n locale={locale}\n tr={tr}\n listRef={listRef}\n firstFieldRef={firstFieldRef}\n ownerTyping={state.ownerTyping}\n visitorTyping={state.visitorTyping}\n onTyping={notifyTyping}\n onSend={sendMessage}\n onRetry={retryMessage}\n onNewConversation={startNewConversation}\n />\n ) : (\n <PreChatForm\n welcome={welcome}\n tr={tr}\n error={state.error}\n sending={state.sending}\n firstFieldRef={firstFieldRef}\n onSubmit={submitForm}\n />\n )}\n\n <footer className=\"ecw-footer\">\n {tr(\"poweredBy\")}{\" \"}\n <a\n className=\"ecw-powered-link\"\n href=\"https://erlancarreira.com.br\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Erlan Carreira\n </a>\n </footer>\n </section>\n )}\n </div>\n );\n}\n","// src/widget/i18n.ts — dicionários do widget (pt/en/es) + helper `t`.\n//\n// Contrato (Task 9): as 15 chaves abaixo são OBRIGATÓRIAS em todos os idiomas — o tipo\n// `WidgetDictionary = Record<WidgetKey, string>` torna a paridade verificável pelo\n// compilador, e o teste em test/widget/widget.test.tsx a pinar em runtime.\n//\n// `t(locale, key, overrides?)` resolve na ordem: override do consumidor → dicionário do\n// locale → pt (fallback seguro) → própria chave (nunca devolve undefined).\n\nexport type WidgetLocale = \"pt\" | \"en\" | \"es\";\n\nexport const WIDGET_KEYS = [\n \"openChat\",\n \"close\",\n \"name\",\n \"phone\",\n \"message\",\n \"send\",\n \"sending\",\n \"welcomeNotice\",\n \"privacyNote\",\n \"invalidPhone\",\n \"sendError\",\n \"retry\",\n \"sessionClosed\",\n \"newConversation\",\n \"poweredBy\",\n \"typing\",\n] as const;\n\nexport type WidgetKey = (typeof WIDGET_KEYS)[number];\n\n/** Dicionário completo: `Record<WidgetKey, string>` força todas as chaves no compilador. */\nexport type WidgetDictionary = Record<WidgetKey, string>;\n\nexport const pt: WidgetDictionary = {\n openChat: \"Abrir chat\",\n close: \"Fechar\",\n name: \"Nome\",\n phone: \"WhatsApp\",\n message: \"Mensagem\",\n send: \"Enviar\",\n sending: \"Enviando…\",\n welcomeNotice: \"Ao continuar, você entra no grupo de WhatsApp do site com o nosso time.\",\n privacyNote: \"Seus dados são usados apenas para o atendimento.\",\n invalidPhone: \"Informe um número válido com DDD (mínimo 10 dígitos).\",\n sendError: \"Não foi possível enviar. Tente novamente.\",\n retry: \"Tentar novamente\",\n sessionClosed: \"Esta conversa foi encerrada. Abra uma nova para continuar.\",\n newConversation: \"Iniciar nova conversa\",\n poweredBy: \"Powered by\",\n typing: \"digitando…\",\n};\n\nexport const en: WidgetDictionary = {\n openChat: \"Open chat\",\n close: \"Close\",\n name: \"Name\",\n phone: \"WhatsApp\",\n message: \"Message\",\n send: \"Send\",\n sending: \"Sending…\",\n welcomeNotice: \"When you continue, you join the site's WhatsApp group with our team.\",\n privacyNote: \"Your data is used only for support.\",\n invalidPhone: \"Enter a valid number with area code (at least 10 digits).\",\n sendError: \"Couldn't send. Please try again.\",\n retry: \"Try again\",\n sessionClosed: \"This conversation was closed. Start a new one to continue.\",\n newConversation: \"Start a new conversation\",\n poweredBy: \"Powered by\",\n typing: \"typing…\",\n};\n\nexport const es: WidgetDictionary = {\n openChat: \"Abrir chat\",\n close: \"Cerrar\",\n name: \"Nombre\",\n phone: \"WhatsApp\",\n message: \"Mensaje\",\n send: \"Enviar\",\n sending: \"Enviando…\",\n welcomeNotice: \"Al continuar, entras en el grupo de WhatsApp del sitio con nuestro equipo.\",\n privacyNote: \"Tus datos se usan solo para la atención.\",\n invalidPhone: \"Introduce un número válido con área (mínimo 10 dígitos).\",\n sendError: \"No se pudo enviar. Inténtalo de nuevo.\",\n retry: \"Reintentar\",\n sessionClosed: \"Esta conversación fue cerrada. Abre una nueva para continuar.\",\n newConversation: \"Iniciar nueva conversación\",\n poweredBy: \"Powered by\",\n typing: \"escribiendo…\",\n};\n\nexport const dictionaries: Record<WidgetLocale, WidgetDictionary> = { pt, en, es };\n\n/**\n * Traduz `key` para `locale`, permitindo override pontual de copy (`labels` do widget).\n * Nunca lança: chave desconhecida devolve o texto de pt e, na ausência, a própria chave.\n */\nexport function t(\n locale: WidgetLocale,\n key: WidgetKey,\n overrides?: Partial<Record<string, string>>,\n): string {\n const override = overrides?.[key];\n if (override !== undefined) return override;\n const dict: Partial<WidgetDictionary> = dictionaries[locale];\n return dict[key] ?? dictionaries.pt[key] ?? key;\n}\n","// src/widget/styles.ts — injeção idempotente da folha de estilos do widget.\n//\n// O widget é auto-contido: `injectWidgetStyles()` insere `<style id=\"ecw-styles\">` com as\n// classes `.ecw-*` uma única vez (múltiplas chamadas / múltiplas instâncias não duplicam).\n// O texto abaixo é o ESPELHO exato de `src/widget/styles.css` (arquivo publicado no\n// subpath \"./widget/styles.css\" para quem prefere `<link>`); o teste de paridade em\n// test/widget/widget.test.tsx falha se os dois divergirem.\n\nexport const WIDGET_CSS = `/*\n * src/widget/styles.css — folha de estilos do ChatWidget (classes \\`.ecw-*\\`).\n *\n * Fonte canônica do CSS INJETADO em runtime: src/widget/styles.ts espelha este arquivo\n * e injeta \\`<style id=\"ecw-styles\">\\` via \\`injectWidgetStyles()\\` (o widget é\n * auto-contido — o consumidor não precisa importar CSS). O teste de paridade em\n * test/widget/widget.test.tsx garante que os dois textos não divergem.\n * Consumidores que preferem \\`<link>\\` podem importar \"@erlancarreira/evolution-chat/widget/styles.css\".\n *\n * Acessibilidade: \\`color-scheme\\` (dark nativo), \\`:focus-visible\\` sempre visível,\n * contraste AA sobre o accent (texto escuro no verde), e transições desligadas com\n * \\`prefers-reduced-motion\\`.\n */\n\n.ecw-root {\n --ecw-accent: #25d366;\n --ecw-accent-ink: #06251a;\n --ecw-surface: #ffffff;\n --ecw-surface-2: #f1f4f7;\n --ecw-text: #14181d;\n --ecw-muted: #51606e;\n --ecw-border: #d7dde3;\n --ecw-danger: #b3261e;\n --ecw-shadow: 0 8px 28px rgb(9 20 28 / 22%);\n color-scheme: light dark;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n font-size: 14px;\n line-height: 1.45;\n color: var(--ecw-text);\n}\n\n@media (prefers-color-scheme: dark) {\n .ecw-root {\n --ecw-accent-ink: #04170f;\n --ecw-surface: #151a20;\n --ecw-surface-2: #232a33;\n --ecw-text: #e8edf2;\n --ecw-muted: #9aa7b4;\n --ecw-border: #2c343d;\n --ecw-danger: #ff9a8f;\n --ecw-shadow: 0 8px 28px rgb(0 0 0 / 45%);\n }\n}\n\n/* ── balão flutuante ─────────────────────────────────────────────────────── */\n.ecw-button {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 2147483000;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 56px;\n height: 56px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n box-shadow: var(--ecw-shadow);\n cursor: pointer;\n transition: transform 160ms ease, box-shadow 160ms ease;\n}\n.ecw-button:hover { transform: translateY(-2px) scale(1.03); }\n.ecw-button:active { transform: translateY(0) scale(0.98); }\n.ecw-button svg { width: 26px; height: 26px; }\n\n.ecw-badge {\n position: absolute;\n top: -4px;\n right: -4px;\n min-width: 22px;\n height: 22px;\n padding: 0 6px;\n border-radius: 11px;\n border: 2px solid var(--ecw-surface);\n background: var(--ecw-danger);\n color: #fff;\n font-size: 12px;\n font-weight: 700;\n line-height: 18px;\n text-align: center;\n}\n\n/* ── painel ──────────────────────────────────────────────────────────────── */\n.ecw-panel {\n position: fixed;\n right: 20px;\n bottom: 88px;\n z-index: 2147483000;\n display: flex;\n flex-direction: column;\n width: min(380px, calc(100vw - 24px));\n min-width: 320px;\n height: 480px;\n max-height: 70vh;\n overflow: hidden;\n border: 1px solid var(--ecw-border);\n border-radius: 14px;\n background: var(--ecw-surface);\n box-shadow: var(--ecw-shadow);\n animation: ecw-pop 160ms ease-out;\n}\n@keyframes ecw-pop { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }\n\n.ecw-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n}\n.ecw-title { flex: 1; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.ecw-code { font-size: 12px; font-weight: 600; opacity: 0.85; letter-spacing: 0.02em; }\n.ecw-close {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font-size: 18px;\n line-height: 1;\n cursor: pointer;\n}\n.ecw-close:hover { background: rgb(0 0 0 / 12%); }\n\n/* ── lista de mensagens ──────────────────────────────────────────────────── */\n.ecw-list {\n flex: 1;\n margin: 0;\n padding: 12px;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 10px;\n list-style: none;\n background: var(--ecw-surface-2);\n}\n.ecw-item { display: flex; flex-direction: column; max-width: 82%; }\n.ecw-item--visitor { align-self: flex-end; align-items: flex-end; }\n.ecw-item--owner { align-self: flex-start; align-items: flex-start; }\n.ecw-item--system { align-self: center; align-items: center; max-width: 92%; }\n.ecw-system {\n padding: 4px 10px;\n border-radius: 999px;\n background: var(--ecw-surface);\n border: 1px solid var(--ecw-border);\n color: var(--ecw-muted);\n font-size: 11px;\n text-align: center;\n overflow-wrap: anywhere;\n}\n.ecw-bubble {\n padding: 8px 12px;\n border-radius: 12px;\n overflow-wrap: anywhere;\n white-space: pre-wrap;\n}\n.ecw-bubble--visitor {\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n border-bottom-right-radius: 4px;\n}\n.ecw-bubble--owner {\n background: var(--ecw-surface);\n color: var(--ecw-text);\n border: 1px solid var(--ecw-border);\n border-bottom-left-radius: 4px;\n}\n.ecw-meta { display: flex; align-items: center; gap: 6px; margin-top: 2px; font-size: 11px; color: var(--ecw-muted); }\n.ecw-status--failed { color: var(--ecw-danger); }\n.ecw-retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--ecw-danger);\n font: inherit;\n font-size: 11px;\n text-decoration: underline;\n cursor: pointer;\n}\n\n/* ── pré-chat form ───────────────────────────────────────────────────────── */\n.ecw-form { display: flex; flex-direction: column; gap: 12px; padding: 16px 14px; overflow-y: auto; }\n.ecw-welcome { margin: 0; font-weight: 600; }\n.ecw-notice { margin: 0; font-size: 12px; color: var(--ecw-muted); }\n.ecw-field { display: flex; flex-direction: column; gap: 4px; }\n.ecw-label { font-size: 12px; font-weight: 600; color: var(--ecw-text); }\n.ecw-input {\n width: 100%;\n padding: 9px 10px;\n border: 1px solid var(--ecw-border);\n border-radius: 8px;\n background: var(--ecw-surface);\n color: var(--ecw-text);\n font: inherit;\n}\n.ecw-input:focus { outline: 2px solid var(--ecw-accent); outline-offset: 1px; }\n.ecw-input[aria-invalid=\"true\"] { border-color: var(--ecw-danger); }\n.ecw-error { margin: 0; font-size: 12px; color: var(--ecw-danger); }\n.ecw-submit {\n padding: 10px 14px;\n border: none;\n border-radius: 8px;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n font: inherit;\n font-weight: 700;\n cursor: pointer;\n}\n.ecw-submit:disabled { opacity: 0.55; cursor: not-allowed; }\n\n/* ── composer ────────────────────────────────────────────────────────────── */\n.ecw-composer { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ecw-border); background: var(--ecw-surface); }\n.ecw-composer .ecw-input { flex: 1; }\n.ecw-send {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n padding: 0;\n border: none;\n border-radius: 8px;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n cursor: pointer;\n}\n.ecw-send:disabled { opacity: 0.55; cursor: not-allowed; }\n.ecw-send svg { width: 18px; height: 18px; }\n\n/* ── indicador \"digitando…\" (3 pontinhos) ────────────────────────────────── */\n.ecw-typing {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 10px 14px;\n border-radius: 12px;\n border-bottom-left-radius: 4px;\n background: var(--ecw-surface);\n border: 1px solid var(--ecw-border);\n}\n.ecw-typing-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--ecw-muted);\n animation: ecw-typing-bounce 1.2s infinite ease-in-out;\n}\n.ecw-typing-dot:nth-child(2) { animation-delay: 0.18s; }\n.ecw-typing-dot:nth-child(3) { animation-delay: 0.36s; }\n@keyframes ecw-typing-bounce {\n 0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }\n 30% { transform: translateY(-4px); opacity: 1; }\n}\n\n/* ── sessão encerrada / falha ────────────────────────────────────────────── */\n.ecw-closed { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ecw-border); background: var(--ecw-surface); }\n\n.ecw-footer { padding: 6px 12px 10px; font-size: 11px; color: var(--ecw-muted); text-align: center; background: var(--ecw-surface); }\n.ecw-powered-link { color: var(--ecw-accent); font-weight: 600; text-decoration: none; }\n.ecw-powered-link:hover { text-decoration: underline; }\n\n/* ── utilitários ─────────────────────────────────────────────────────────── */\n.ecw-hp { display: none !important; }\n.ecw-sr-only {\n position: absolute;\n width: 1px; height: 1px;\n margin: -1px; padding: 0;\n overflow: hidden;\n clip: rect(0 0 0 0);\n white-space: nowrap;\n border: 0;\n}\n.ecw-button:focus-visible, .ecw-close:focus-visible, .ecw-submit:focus-visible,\n.ecw-send:focus-visible, .ecw-retry:focus-visible {\n outline: 3px solid #1b6fec;\n outline-offset: 2px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ecw-root *, .ecw-root *::before, .ecw-root *::after {\n animation: none !important;\n transition: none !important;\n }\n}\n\n@media (max-width: 420px) {\n .ecw-panel { right: 12px; left: 12px; width: auto; min-width: 0; bottom: 80px; }\n}\n`;\n\nlet injected = false;\n\n/**\n * Insere a folha do widget no `document.head` (idempotente: uma única tag\n * `<style id=\"ecw-styles\">` por documento, não importa quantas vezes seja chamada).\n * No-op fora de browser (SSR / Node) — o consumidor renderiza com `<link>` do subpath.\n */\nexport function injectWidgetStyles(): void {\n if (typeof document === \"undefined\") return;\n if (injected && document.getElementById(\"ecw-styles\") !== null) return;\n const existing = document.getElementById(\"ecw-styles\");\n if (existing !== null) {\n // Alguém (ex.: outra cópia do bundle) já injetou — marca e não duplica.\n injected = true;\n return;\n }\n const style = document.createElement(\"style\");\n style.id = \"ecw-styles\";\n style.textContent = WIDGET_CSS;\n document.head.appendChild(style);\n injected = true;\n}\n","// src/widget/use-chat.ts — máquina de estados do ChatWidget (hook puro, sem JSX).\n//\n// Fases: \"idle\" (painel nunca aberto, sem sessão) → \"form\" (pré-chat) → \"chat\" (sessão\n// ativa). A transição idle→form acontece na primeira abertura; form→chat no POST bem\n// sucedido; qualquer fase→form quando o GET devolve 404 (sessão expirada → storage limpo)\n// ou quando o visitante pede \"nova conversa\" numa sessão closed/failed (`startNewConversation`).\n//\n// Tempo real (Strategy, invisível ao usuário): com token em mãos, `realtime.subscribe`\n// anexa eventos; se o transporte cair (`onStatus(\"closed\")`), o hook liga um POLLING de\n// 5s no GET de histórico com `after=<última createdAt>` até o canal reabrir. Mensagens\n// `owner` que chegam com o painel fechado somam no badge de não lidas.\n//\n// Otimismo: POST anexa uma mensagem `tmp-…` com status \"pending\"; a resposta do servidor\n// SUBSTITUI o tmp (replace) — em falha, o tmp vira \"failed\" e ganha retry.\n\nimport { useCallback, useEffect, useReducer, useRef } from \"react\";\nimport type { ChatEvent, ChatMessage, ChatSessionStatus } from \"../types\";\nimport type { RealtimeHandle } from \"../bridge/types\";\n\n/** Chave de persistência da sessão no localStorage. */\nexport const SESSION_STORAGE_KEY = \"ecw:session\";\n/** Intervalo do fallback de polling quando o canal realtime fecha (ms). */\nexport const POLL_INTERVAL_MS = 5_000;\n/** Faixa (ms) do \"digitando…\" simulado exibido ANTES de uma mensagem recebida. */\nexport const TYPING_PRE_MESSAGE_MIN_MS = 700;\nexport const TYPING_PRE_MESSAGE_MAX_MS = 2200;\n/** Mínimo de dígitos (com DDD) para um WhatsApp ser aceito no form. */\nexport const MIN_PHONE_DIGITS = 10;\n\nexport type ChatPhase = \"idle\" | \"form\" | \"chat\";\n\nexport interface SessionInfo {\n code: string;\n status: ChatSessionStatus;\n visitorName: string;\n}\n\nexport interface ChatState {\n phase: ChatPhase;\n open: boolean;\n session: SessionInfo | null;\n token: string | null;\n messages: ChatMessage[];\n unread: number;\n sending: boolean;\n /** Chave i18n do erro visível (\"sendError\" | \"invalidPhone\") ou null. */\n error: string | null;\n /** True enquanto a DONA da plataforma (atendente) digita → \"3 pontinhos\" no widget. */\n ownerTyping: boolean;\n /** True enquanto o VISITANTE (esta ponta) digita — útil para um painel de atendente. */\n visitorTyping: boolean;\n}\n\ntype Action =\n | { type: \"open\" }\n | { type: \"close\" }\n | { type: \"restore\"; session: SessionInfo; token: string; messages: ChatMessage[] }\n | { type: \"start\"; session: SessionInfo; token: string; messages: ChatMessage[] }\n | { type: \"reset-form\" }\n | { type: \"append\"; message: ChatMessage }\n | { type: \"merge\"; messages: ChatMessage[] }\n | { type: \"replace\"; tmpId: string; message: ChatMessage }\n | { type: \"mark\"; id: string; status: ChatMessage[\"status\"] }\n | { type: \"session-status\"; status: ChatSessionStatus }\n | { type: \"sending\"; value: boolean }\n | { type: \"error\"; value: string | null }\n | { type: \"typing\"; from: \"owner\" | \"visitor\"; isTyping: boolean };\n\nconst initialState: ChatState = {\n phase: \"idle\",\n open: false,\n session: null,\n token: null,\n messages: [],\n unread: 0,\n sending: false,\n error: null,\n ownerTyping: false,\n visitorTyping: false,\n};\n\nfunction withMessage(state: ChatState, message: ChatMessage): ChatState {\n const index = state.messages.findIndex((m) => m.id === message.id);\n let messages: ChatMessage[];\n if (index >= 0) {\n messages = [...state.messages];\n messages[index] = message;\n } else {\n messages = [...state.messages, message];\n }\n const bump = state.open || message.direction !== \"owner\" ? 0 : 1;\n const next: ChatState = { ...state, messages, unread: state.unread + bump };\n // Quando chega uma mensagem da dona, ela parou de \"digitar\" (limpa os 3 pontinhos).\n if (message.direction === \"owner\") next.ownerTyping = false;\n return next;\n}\n\nexport function chatReducer(state: ChatState, action: Action): ChatState {\n switch (action.type) {\n case \"open\":\n return {\n ...state,\n open: true,\n unread: 0,\n phase: state.phase === \"idle\" ? \"form\" : state.phase,\n };\n case \"close\":\n return { ...state, open: false };\n case \"restore\":\n return {\n ...state,\n phase: \"chat\",\n session: action.session,\n token: action.token,\n messages: action.messages,\n };\n case \"start\": {\n // O POST devolve o histórico oficial (inclui a 1ª msg do visitante); qualquer tmp\n // \"pending\" ainda não confirmado pelo servidor é mantido no fim da lista.\n let next: ChatState = {\n ...state,\n phase: \"chat\",\n session: action.session,\n token: action.token,\n messages: action.messages,\n };\n for (const pending of state.messages) {\n const confirmed = action.messages.some(\n (m) => m.direction === \"visitor\" && m.body === pending.body,\n );\n if (pending.status === \"pending\" && !confirmed) next = withMessage(next, pending);\n }\n return next;\n }\n case \"reset-form\":\n return { ...state, phase: \"form\", session: null, token: null, messages: [], unread: 0, error: null };\n case \"append\":\n return withMessage(state, action.message);\n case \"merge\": {\n let next = state;\n for (const message of action.messages) next = withMessage(next, message);\n return next;\n }\n case \"replace\": {\n const messages = state.messages.map((m) => (m.id === action.tmpId ? action.message : m));\n const kept = messages.some((m) => m.id === action.message.id);\n return { ...state, messages: kept ? messages : [...messages, action.message] };\n }\n case \"mark\":\n return {\n ...state,\n messages: state.messages.map((m) => (m.id === action.id ? { ...m, status: action.status } : m)),\n };\n case \"session-status\": {\n if (state.session === null) return state;\n return { ...state, session: { ...state.session, status: action.status } };\n }\n case \"sending\":\n return { ...state, sending: action.value };\n case \"error\":\n return { ...state, error: action.value };\n case \"typing\":\n // Só transiciona se mudar de fato (evita re-render a cada batida de presença).\n return action.from === \"owner\"\n ? state.ownerTyping === action.isTyping\n ? state\n : { ...state, ownerTyping: action.isTyping }\n : state.visitorTyping === action.isTyping\n ? state\n : { ...state, visitorTyping: action.isTyping };\n }\n}\n\n// ─── helpers de telefone ──────────────────────────────────────────────────────\n\n/** Só dígitos (o servidor espera DDI+DDD+número sem máscara). */\nexport function normalizePhone(value: string): string {\n return value.replace(/\\D/g, \"\");\n}\n\n/** Máscara BR parcial `(11) 99999-8888`, progressiva enquanto o usuário digita. */\nexport function maskPhone(value: string): string {\n const digits = normalizePhone(value).slice(0, 11);\n if (digits.length === 0) return \"\";\n const ddd = digits.slice(0, 2);\n if (digits.length <= 2) return `(${ddd}`;\n const rest = digits.slice(2);\n if (rest.length <= 5) return `(${ddd}) ${rest}`;\n return `(${ddd}) ${rest.slice(0, 5)}-${rest.slice(5)}`;\n}\n\nexport function isValidPhone(value: string): boolean {\n return normalizePhone(value).length >= MIN_PHONE_DIGITS;\n}\n\n// ─── persistência ─────────────────────────────────────────────────────────────\n\ninterface StoredSession {\n token: string;\n code: string;\n}\n\nfunction readStoredSession(): StoredSession | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(SESSION_STORAGE_KEY);\n if (raw === null) return null;\n const parsed: unknown = JSON.parse(raw);\n if (\n parsed !== null &&\n typeof parsed === \"object\" &&\n typeof (parsed as StoredSession).token === \"string\" &&\n (parsed as StoredSession).token !== \"\"\n ) {\n const { token, code } = parsed as StoredSession;\n return { token, code: typeof code === \"string\" ? code : \"\" };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction writeStoredSession(session: StoredSession): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));\n } catch {\n /* storage cheio/bloqueado: sessão só não sobrevive ao reload */\n }\n}\n\nfunction clearStoredSession(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(SESSION_STORAGE_KEY);\n } catch {\n /* idem */\n }\n}\n\n// ─── contrato de rede (espelha createChatRoutes — Task 8) ────────────────────\n\ninterface HistoryResponse {\n session?: SessionInfo;\n messages?: ChatMessage[];\n}\n\ninterface StartResponse {\n session?: SessionInfo & { realtimeToken?: string };\n messages?: ChatMessage[];\n}\n\ninterface SendResponse {\n message?: ChatMessage;\n}\n\nfunction historyUrl(endpoint: string, token: string, after: string | null): string {\n const params = new URLSearchParams({ token });\n if (after !== null) params.set(\"after\", after);\n const sep = endpoint.includes(\"?\") ? \"&\" : \"?\";\n return `${endpoint}${sep}${params.toString()}`;\n}\n\nfunction lastCursor(messages: ChatMessage[]): string | null {\n let latest: string | null = null;\n for (const m of messages) if (latest === null || m.createdAt > latest) latest = m.createdAt;\n return latest;\n}\n\n// ─── hook ─────────────────────────────────────────────────────────────────────\n\nexport interface UseChatOptions {\n endpoint: string;\n realtime: RealtimeHandle;\n /** Rota que recebe o sinal \"visitante digitando\" (POST { token, isTyping }). Default: `${endpoint}/typing`. */\n typingEndpoint?: string;\n}\n\nexport interface UseChatResult {\n state: ChatState;\n openPanel(): void;\n closePanel(): void;\n togglePanel(): void;\n submitForm(input: { name: string; phone: string; message: string; honeypot: string }): Promise<void>;\n sendMessage(text: string): Promise<void>;\n retryMessage(id: string): Promise<void>;\n /** Descarta a sessão encerrada/falha (storage + estado) e volta ao pré-chat form. */\n startNewConversation(): void;\n /** Avisa o servidor que o visitante está (true) ou parou (false) de digitar. Best-effort. */\n notifyTyping(isTyping: boolean): Promise<void>;\n}\n\nlet tmpSeq = 0;\nfunction tmpMessage(body: string): ChatMessage {\n tmpSeq += 1;\n return {\n id: `tmp-${tmpSeq}`,\n sessionId: \"\",\n direction: \"visitor\",\n body,\n status: \"pending\",\n waMessageId: null,\n createdAt: new Date().toISOString(),\n };\n}\n\nexport function useChat({ endpoint, realtime, typingEndpoint }: UseChatOptions): UseChatResult {\n const [state, dispatch] = useReducer(chatReducer, initialState);\n\n const typingUrl = typingEndpoint ?? `${endpoint.replace(/\\/+$/, \"\")}/typing`;\n\n // Espelho do estado para callbacks estáveis (eventos realtime/poll chegam fora do React).\n const stateRef = useRef(state);\n useEffect(() => {\n stateRef.current = state;\n }, [state]);\n\n // A porta pode chegar como objeto novo a cada render do consumidor; a assinatura deve\n // depender apenas do token, nunca da identidade do handle.\n const realtimeRef = useRef(realtime);\n useEffect(() => {\n realtimeRef.current = realtime;\n }, [realtime]);\n\n const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const stopPoll = useCallback((): void => {\n if (pollRef.current !== null) {\n clearInterval(pollRef.current);\n pollRef.current = null;\n }\n }, []);\n\n const pollOnce = useCallback(async (): Promise<void> => {\n const { token, messages } = stateRef.current;\n if (token === null) return;\n try {\n const res = await fetch(historyUrl(endpoint, token, lastCursor(messages)), {\n method: \"GET\",\n headers: { accept: \"application/json\" },\n });\n if (res.status === 404) {\n clearStoredSession();\n stopPoll();\n dispatch({ type: \"reset-form\" });\n return;\n }\n if (!res.ok) return;\n const data = (await res.json()) as HistoryResponse;\n if (Array.isArray(data.messages)) dispatch({ type: \"merge\", messages: data.messages });\n if (data.session !== undefined) dispatch({ type: \"session-status\", status: data.session.status });\n } catch {\n /* offline: o próximo tick tenta de novo */\n }\n }, [endpoint, stopPoll]);\n\n const startPoll = useCallback((): void => {\n if (pollRef.current !== null) return;\n pollRef.current = setInterval(() => {\n void pollOnce();\n }, POLL_INTERVAL_MS);\n }, [pollOnce]);\n\n // Abertura/fechamento do canal realtime (Strategy): caiu → polling; voltou → para.\n const handleStatus = useCallback(\n (s: \"open\" | \"closed\"): void => {\n if (s === \"closed\") startPoll();\n else stopPoll();\n },\n [startPoll, stopPoll],\n );\n\n const handleEvent = useCallback((e: ChatEvent): void => {\n if (e.type === \"message\" && e.message !== undefined) {\n const msg = e.message;\n // Mensagem recebida da DONA (staff): como a instância conectada não emite presença\n // do próprio \"digitando\" (Evolution/Baileys), simulamos o \"3 pontinhos\" antes de\n // exibir a mensagem — dá o feedback visual de que a outra ponta está respondendo.\n if (msg.direction === \"owner\") {\n dispatch({ type: \"typing\", from: \"owner\", isTyping: true });\n const text = typeof msg.body === \"string\" ? msg.body : \"\";\n const delay = Math.min(\n TYPING_PRE_MESSAGE_MAX_MS,\n Math.max(TYPING_PRE_MESSAGE_MIN_MS, text.length * 22),\n );\n setTimeout(() => {\n dispatch({ type: \"append\", message: msg });\n }, delay);\n } else {\n dispatch({ type: \"append\", message: msg });\n }\n } else if (e.type === \"session\" && e.status !== undefined) {\n dispatch({ type: \"session-status\", status: e.status });\n } else if (e.type === \"typing\" && e.from !== undefined) {\n dispatch({ type: \"typing\", from: e.from, isTyping: e.isTyping });\n }\n }, []);\n\n // Boot: sessão persistida → GET de histórico (404 limpa o storage e volta ao form).\n useEffect(() => {\n const stored = readStoredSession();\n if (stored === null) return;\n let cancelled = false;\n void (async () => {\n try {\n const res = await fetch(historyUrl(endpoint, stored.token, null), {\n method: \"GET\",\n headers: { accept: \"application/json\" },\n });\n if (cancelled) return;\n if (res.status === 404) {\n clearStoredSession();\n dispatch({ type: \"reset-form\" });\n return;\n }\n if (!res.ok) return;\n const data = (await res.json()) as HistoryResponse;\n if (cancelled || data.session === undefined) return;\n dispatch({\n type: \"restore\",\n session: data.session,\n token: stored.token,\n messages: Array.isArray(data.messages) ? data.messages : [],\n });\n } catch {\n /* sem rede: mantém o form/idle; o retry fica com o usuário */\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [endpoint]);\n\n // Assinatura do canal enquanto houver token (re-assina quando a sessão muda).\n useEffect(() => {\n const token = state.token;\n if (token === null) return;\n const unsubscribe = realtimeRef.current.subscribe(token, handleEvent, handleStatus);\n return () => {\n unsubscribe();\n stopPoll();\n };\n }, [state.token, handleEvent, handleStatus, stopPoll]);\n\n useEffect(() => stopPoll, [stopPoll]);\n\n const openPanel = useCallback((): void => {\n dispatch({ type: \"open\" });\n }, []);\n\n const closePanel = useCallback((): void => {\n dispatch({ type: \"close\" });\n }, []);\n\n const togglePanel = useCallback((): void => {\n if (stateRef.current.open) dispatch({ type: \"close\" });\n else dispatch({ type: \"open\" });\n }, []);\n\n const postMessage = useCallback(\n async (token: string, body: string, id: string): Promise<void> => {\n dispatch({ type: \"sending\", value: true });\n dispatch({ type: \"error\", value: null });\n try {\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ token, message: body }),\n });\n if (!res.ok) {\n dispatch({ type: \"mark\", id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n return;\n }\n const data = (await res.json()) as SendResponse;\n if (data.message !== undefined) dispatch({ type: \"replace\", tmpId: id, message: data.message });\n else dispatch({ type: \"mark\", id, status: \"failed\" });\n } catch {\n dispatch({ type: \"mark\", id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n } finally {\n dispatch({ type: \"sending\", value: false });\n }\n },\n [endpoint],\n );\n\n const sendMessage = useCallback(\n async (text: string): Promise<void> => {\n const body = text.trim();\n const token = stateRef.current.token;\n if (body === \"\" || token === null) return;\n const optimistic = tmpMessage(body);\n dispatch({ type: \"append\", message: optimistic });\n await postMessage(token, body, optimistic.id);\n },\n [postMessage],\n );\n\n const retryMessage = useCallback(\n async (id: string): Promise<void> => {\n const token = stateRef.current.token;\n const target = stateRef.current.messages.find((m) => m.id === id);\n if (token === null || target === undefined || target.direction !== \"visitor\") return;\n dispatch({ type: \"mark\", id, status: \"pending\" });\n await postMessage(token, target.body, id);\n },\n [postMessage],\n );\n\n // Sessão closed/failed: o visitante resolve o beco sem saída pedindo uma conversa nova.\n // Limpa o storage (senão o próximo boot restauraria a mesma sessão morta) e volta ao\n // form; a assinatura realtime cai sozinha na limpeza do efeito, porque token → null.\n const startNewConversation = useCallback((): void => {\n clearStoredSession();\n dispatch({ type: \"reset-form\" });\n }, []);\n\n // Avisa o servidor que o visitante está (ou parou de) digitar. Best-effort: nunca\n // deve quebrar o fluxo de envio de mensagens — falhas são silenciadas.\n const notifyTyping = useCallback(\n async (isTyping: boolean): Promise<void> => {\n const token = stateRef.current.token;\n if (token === null) return;\n try {\n await fetch(typingUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ token, isTyping }),\n });\n } catch {\n /* offline/erro: digitação é cosmética, ignora */\n }\n },\n [typingUrl],\n );\n\n const submitForm = useCallback(\n async ({ name, phone, message, honeypot }: { name: string; phone: string; message: string; honeypot: string }): Promise<void> => {\n // Anti-bot silencioso: honeypot preenchido → nada sai do navegador (o servidor\n // também fingiria sucesso; aqui nem há request).\n if (honeypot.trim() !== \"\") return;\n const digits = normalizePhone(phone);\n if (name.trim() === \"\" || message.trim() === \"\" || digits.length < MIN_PHONE_DIGITS) {\n dispatch({ type: \"error\", value: digits.length < MIN_PHONE_DIGITS ? \"invalidPhone\" : \"sendError\" });\n return;\n }\n const optimistic = tmpMessage(message.trim());\n dispatch({ type: \"append\", message: optimistic });\n dispatch({ type: \"sending\", value: true });\n dispatch({ type: \"error\", value: null });\n try {\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ name: name.trim(), phone: digits, message: message.trim(), honeypot: \"\" }),\n });\n if (!res.ok) {\n dispatch({ type: \"mark\", id: optimistic.id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n return;\n }\n const data = (await res.json()) as StartResponse;\n const session = data.session;\n if (session === undefined || typeof session.realtimeToken !== \"string\" || session.realtimeToken === \"\") {\n dispatch({ type: \"mark\", id: optimistic.id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n return;\n }\n const token = session.realtimeToken;\n writeStoredSession({ token, code: session.code });\n dispatch({\n type: \"start\",\n session: { code: session.code, status: session.status, visitorName: session.visitorName },\n token,\n messages: Array.isArray(data.messages) ? data.messages : [],\n });\n } catch {\n dispatch({ type: \"mark\", id: optimistic.id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n } finally {\n dispatch({ type: \"sending\", value: false });\n }\n },\n [endpoint],\n );\n\n return { state, openPanel, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping };\n}\n"],"mappings":";AAgBA;AAAA,EACE,eAAAA;AAAA,EACA,aAAAC;AAAA,EACA;AAAA,EACA,UAAAC;AAAA,EACA;AAAA,OAMK;;;AChBA,IAAM,cAAc;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,IAAM,KAAuB;AAAA,EAClC,UAAU;AAAA,EACV,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,eAAe;AAAA,EACf,aAAa;AAAA,EACb,cAAc;AAAA,EACd,WAAW;AAAA,EACX,OAAO;AAAA,EACP,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,QAAQ;AACV;AAEO,IAAM,KAAuB;AAAA,EAClC,UAAU;AAAA,EACV,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,eAAe;AAAA,EACf,aAAa;AAAA,EACb,cAAc;AAAA,EACd,WAAW;AAAA,EACX,OAAO;AAAA,EACP,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,QAAQ;AACV;AAEO,IAAM,KAAuB;AAAA,EAClC,UAAU;AAAA,EACV,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,eAAe;AAAA,EACf,aAAa;AAAA,EACb,cAAc;AAAA,EACd,WAAW;AAAA,EACX,OAAO;AAAA,EACP,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,QAAQ;AACV;AAEO,IAAM,eAAuD,EAAE,IAAI,IAAI,GAAG;AAM1E,SAAS,EACd,QACA,KACA,WACQ;AACR,QAAM,WAAW,YAAY,GAAG;AAChC,MAAI,aAAa,OAAW,QAAO;AACnC,QAAM,OAAkC,aAAa,MAAM;AAC3D,SAAO,KAAK,GAAG,KAAK,aAAa,GAAG,GAAG,KAAK;AAC9C;;;ACnGO,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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","names":["useCallback","useEffect","useRef","useRef","useEffect","useCallback"]}
|
|
1
|
+
{"version":3,"sources":["../src/widget/chat-widget.tsx","../src/widget/i18n.ts","../src/widget/styles.ts","../src/widget/use-chat.ts"],"sourcesContent":["// src/widget/chat-widget.tsx — ChatWidget: balão flutuante + painel de conversa.\n//\n// Sem Tailwind / lib de UI: toda a aparência vem de `injectWidgetStyles()` (classes\n// `.ecw-*`), e o único estilo inline é a CSS var `--ecw-accent` (cor do tema). O estado\n// inteiro vive em `use-chat.ts` (reducer + rede + realtime); aqui só render e foco.\n//\n// Acessibilidade (contrato da spec §2):\n// - balão: <button> com aria-label (i18n), aria-expanded e aria-haspopup=\"dialog\";\n// - painel: role=\"dialog\" aria-label=\"Chat\"; foco no primeiro campo ao abrir;\n// ESC fecha e devolve o foco ao balão; clique fora NÃO fecha (mobile-friendly);\n// - todo input tem <label htmlFor> (useId → múltiplas instâncias sem colisão);\n// - badge de não lidas refletido no nome acessível do balão;\n// - sessão closed/failed: composer desabilitado, aviso em role=\"status\" e botão real\n// \"nova conversa\" (o beco sem saída tem saída acessível por teclado);\n// - prefers-reduced-motion desliga animações (via CSS).\n\nimport {\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type CSSProperties,\n type ChangeEvent,\n type FormEvent,\n type ReactElement,\n type RefObject,\n} from \"react\";\nimport type { RealtimeHandle } from \"../bridge/types\";\nimport type { ChatMessage } from \"../types\";\nimport { t, type WidgetKey, type WidgetLocale } from \"./i18n\";\nimport { injectWidgetStyles } from \"./styles\";\nimport { isValidPhone, maskPhone, useChat } from \"./use-chat\";\n\nexport type { WidgetLocale };\n\nexport interface ChatWidgetProps {\n /** Caminho da rota de chat (GET histórico / POST start+send), ex.: \"/api/chat\". */\n endpoint: string;\n locale: WidgetLocale;\n /** Saudação exibida no pré-chat form. */\n welcome: string;\n projectName: string;\n /** Cor de tema; default \"#25D366\". */\n accentColor?: string;\n /** Porta do widget (DI — Strategy): subscribe/unsubscribe do canal da sessão. */\n realtime: RealtimeHandle;\n /** Override pontual de copy por chave i18n. */\n labels?: Partial<Record<string, string>>;\n}\n\nconst DEFAULT_ACCENT = \"#25D366\";\n\nfunction ChatIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M21 11.5a8.38 8.38 0 0 1-8.5 8.5c-1.5 0-2.9-.36-4.13-1L3 20l1.1-3.85A8.36 8.36 0 0 1 12.5 3 8.38 8.38 0 0 1 21 11.5z\" />\n </svg>\n );\n}\n\nfunction SendIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M22 2 11 13\" />\n <path d=\"M22 2 15 22l-4-9-9-4 20-7z\" />\n </svg>\n );\n}\n\nfunction formatTime(iso: string, locale: WidgetLocale): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return \"\";\n return new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" }).format(date);\n}\n\nfunction StatusMark({ status }: { status: ChatMessage[\"status\"] }): ReactElement {\n // ✓ = aceita pelo servidor (pending), ✓✓ = enviada, ⚠ = falhou.\n // Decorativo por design: o conjunto de chaves i18n é fixo (15) e o estado \"failed\" já\n // é anunciado de forma acessível pelo botão visível `retry` ao lado — um texto sr-only\n // reaproveitando \"send\"/\"sending\" soaria a ruído para quem usa leitor de tela.\n const glyph = status === \"pending\" ? \"✓\" : status === \"sent\" ? \"✓✓\" : \"⚠\";\n return (\n <span className={status === \"failed\" ? \"ecw-status ecw-status--failed\" : \"ecw-status\"} aria-hidden=\"true\">\n {glyph}\n </span>\n );\n}\n\n/** \"3 pontinhos\" de digitação: bolha com três pontos animados + rótulo acessível.\n * `from` alinha o indicador ao lado de quem representa (owner→esquerda, visitor→direita). */\nfunction TypingIndicator({ from, label }: { from: \"owner\" | \"visitor\"; label: string }): ReactElement {\n return (\n <li className={`ecw-item ecw-item--${from}`} aria-live=\"polite\">\n <div className=\"ecw-typing\" role=\"status\" aria-label={label}>\n <span className=\"ecw-typing-dot\" />\n <span className=\"ecw-typing-dot\" />\n <span className=\"ecw-typing-dot\" />\n <span className=\"ecw-sr-only\">{label}</span>\n </div>\n </li>\n );\n}\n\ninterface FormProps {\n welcome: string;\n tr: (k: WidgetKey) => string;\n error: string | null;\n sending: boolean;\n firstFieldRef: RefObject<HTMLInputElement>;\n onSubmit(values: { name: string; phone: string; message: string; honeypot: string }): Promise<void>;\n}\n\nfunction PreChatForm({ welcome, tr, error, sending, firstFieldRef, onSubmit }: FormProps): ReactElement {\n const uid = useId();\n const [name, setName] = useState(\"\");\n const [phone, setPhone] = useState(\"\");\n const [message, setMessage] = useState(\"\");\n const [honeypot, setHoneypot] = useState(\"\");\n const [phoneTouched, setPhoneTouched] = useState(false);\n\n const phoneValid = isValidPhone(phone);\n const complete = name.trim() !== \"\" && phoneValid && message.trim() !== \"\";\n\n const handleSubmit = (e: FormEvent): void => {\n e.preventDefault();\n if (!complete || sending) return;\n void onSubmit({ name, phone, message, honeypot });\n };\n\n return (\n <form className=\"ecw-form\" onSubmit={handleSubmit} noValidate>\n <p className=\"ecw-welcome\">{welcome}</p>\n <p className=\"ecw-notice\">{tr(\"welcomeNotice\")}</p>\n\n <div className=\"ecw-field\">\n <label className=\"ecw-label\" htmlFor={`${uid}-name`}>{tr(\"name\")}</label>\n <input\n id={`${uid}-name`}\n ref={firstFieldRef}\n className=\"ecw-input\"\n type=\"text\"\n autoComplete=\"name\"\n value={name}\n onChange={(e) => { setName(e.target.value); }}\n required\n />\n </div>\n\n <div className=\"ecw-field\">\n <label className=\"ecw-label\" htmlFor={`${uid}-phone`}>{tr(\"phone\")}</label>\n <input\n id={`${uid}-phone`}\n className=\"ecw-input\"\n type=\"tel\"\n inputMode=\"tel\"\n autoComplete=\"tel\"\n placeholder=\"(11) 99999-8888\"\n value={phone}\n onChange={(e) => { setPhone(maskPhone(e.target.value)); }}\n onBlur={() => { setPhoneTouched(true); }}\n aria-invalid={phoneTouched && !phoneValid ? true : undefined}\n aria-describedby={phoneTouched && !phoneValid ? `${uid}-phone-err` : undefined}\n required\n />\n {phoneTouched && !phoneValid && (\n <p className=\"ecw-error\" id={`${uid}-phone-err`} role=\"alert\">{tr(\"invalidPhone\")}</p>\n )}\n </div>\n\n <div className=\"ecw-field\">\n <label className=\"ecw-label\" htmlFor={`${uid}-message`}>{tr(\"message\")}</label>\n <textarea\n id={`${uid}-message`}\n className=\"ecw-input\"\n rows={3}\n autoComplete=\"off\"\n value={message}\n onChange={(e) => { setMessage(e.target.value); }}\n required\n />\n </div>\n\n {/* Honeypot anti-bot: invisível para humanos (display:none via .ecw-hp),\n fora da ordem de tabulação e escondido do leitor de tela. */}\n <div className=\"ecw-hp\" aria-hidden=\"true\">\n <label htmlFor={`${uid}-website`}>Website</label>\n <input\n id={`${uid}-website`}\n name=\"website\"\n type=\"text\"\n tabIndex={-1}\n autoComplete=\"off\"\n value={honeypot}\n onChange={(e) => { setHoneypot(e.target.value); }}\n />\n </div>\n\n {error === \"sendError\" && (\n <p className=\"ecw-error\" role=\"alert\">{tr(\"sendError\")}</p>\n )}\n\n <button className=\"ecw-submit\" type=\"submit\" disabled={!complete || sending}>\n {sending ? tr(\"sending\") : tr(\"send\")}\n </button>\n <p className=\"ecw-notice\">{tr(\"privacyNote\")}</p>\n </form>\n );\n}\n\ninterface ChatPanelProps {\n messages: ChatMessage[];\n /** Só `status === \"active\"` compõe; closed/failed mostram aviso + \"nova conversa\". */\n canCompose: boolean;\n sending: boolean;\n locale: WidgetLocale;\n tr: (k: WidgetKey) => string;\n listRef: RefObject<HTMLUListElement>;\n firstFieldRef: RefObject<HTMLInputElement>;\n /** True enquanto a dona da plataforma (atendente) digita → mostra os \"3 pontinhos\". */\n ownerTyping: boolean;\n /** True enquanto o VISITANTE (esta ponta) digita → \"3 pontinhos\" do lado do visitante. */\n visitorTyping: boolean;\n /** Avisa o servidor que o visitante está/parou de digitar. Best-effort. */\n onTyping(isTyping: boolean): Promise<void>;\n onSend(text: string): Promise<void>;\n onRetry(id: string): Promise<void>;\n onNewConversation(): void;\n}\n\nfunction ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, onTyping, onSend, onRetry, onNewConversation }: ChatPanelProps): ReactElement {\n const uid = useId();\n const [draft, setDraft] = useState(\"\");\n\n // Sinal \"visitante digitando\" para a OUTRA ponta (painel de atendente): dispara\n // `true` só na TRANSIÇÃO para digitando (não a cada tecla) e `false` após 2,5s de\n // inatividade, ao enviar ou ao perder o foco — evita um POST por caractere. `onChange`\n // já cobre keyup, cola e IME (mais robusto que keyup puro).\n const typingRef = useRef(false);\n const typingTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Indicador de digitação DO PRÓPRIO visitante: renderizado LOCALMENTE (sem round-trip\n // de rede) para ser instantâneo/fluido. Grandes apps (WhatsApp, Messenger, Slack) nunca\n // mostram o \"digitando\" do próprio usuário com latência — a outra ponta recebe pela rede\n // (latência cross-dispositivo aceitável), mas o próprio usuário tem feedback imediato.\n const selfTypingRef = useRef(false);\n const selfTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const [selfTyping, setSelfTyping] = useState(false);\n\n useEffect(() => {\n return () => {\n if (typingTimer.current !== null) clearTimeout(typingTimer.current);\n if (selfTimer.current !== null) clearTimeout(selfTimer.current);\n };\n }, []);\n\n const stopTypingSignal = useCallback((): void => {\n if (typingTimer.current !== null) {\n clearTimeout(typingTimer.current);\n typingTimer.current = null;\n }\n if (typingRef.current) {\n typingRef.current = false;\n void onTyping(false);\n }\n }, [onTyping]);\n\n const stopSelfTyping = useCallback((): void => {\n if (selfTimer.current !== null) {\n clearTimeout(selfTimer.current);\n selfTimer.current = null;\n }\n if (selfTypingRef.current) {\n selfTypingRef.current = false;\n setSelfTyping(false);\n }\n }, []);\n\n const handleDraftChange = (e: ChangeEvent<HTMLInputElement>): void => {\n setDraft(e.target.value);\n if (!canCompose) return;\n // sinal para a OUTRA ponta (vai pela rede, com debounce de 2,5s).\n if (!typingRef.current) {\n typingRef.current = true;\n void onTyping(true);\n }\n if (typingTimer.current !== null) clearTimeout(typingTimer.current);\n typingTimer.current = setTimeout(stopTypingSignal, 2500);\n // indicador local do próprio visitante — instantâneo, sem esperar o servidor.\n if (!selfTypingRef.current) {\n selfTypingRef.current = true;\n setSelfTyping(true);\n }\n if (selfTimer.current !== null) clearTimeout(selfTimer.current);\n selfTimer.current = setTimeout(stopSelfTyping, 2500);\n };\n\n const handleDraftBlur = (): void => {\n stopTypingSignal();\n stopSelfTyping();\n };\n\n const submit = (e: FormEvent): void => {\n e.preventDefault();\n const text = draft.trim();\n if (text === \"\" || !canCompose) return;\n stopTypingSignal();\n stopSelfTyping();\n setDraft(\"\");\n void onSend(text);\n };\n\n return (\n <>\n <ul ref={listRef} className=\"ecw-list\">\n {messages.map((m) =>\n m.direction === \"system\" ? (\n <li key={m.id} className=\"ecw-item ecw-item--system\" role=\"status\">\n <div className=\"ecw-system\">{m.body}</div>\n </li>\n ) : (\n <li key={m.id} className={`ecw-item ecw-item--${m.direction}`}>\n <div className={`ecw-bubble ecw-bubble--${m.direction}`}>{m.body}</div>\n <div className=\"ecw-meta\">\n <time dateTime={m.createdAt}>{formatTime(m.createdAt, locale)}</time>\n {m.direction === \"visitor\" && <StatusMark status={m.status} />}\n {m.status === \"failed\" && (\n <button type=\"button\" className=\"ecw-retry\" onClick={() => { void onRetry(m.id); }}>\n {tr(\"retry\")}\n </button>\n )}\n </div>\n </li>\n ),\n )}\n {ownerTyping && canCompose && <TypingIndicator from=\"owner\" label={tr(\"typing\")} />}\n {selfTyping && canCompose && <TypingIndicator from=\"visitor\" label={tr(\"typing\")} />}\n </ul>\n {!canCompose && (\n <div className=\"ecw-closed\">\n <p className=\"ecw-notice\" role=\"status\">{tr(\"sessionClosed\")}</p>\n <button type=\"button\" className=\"ecw-submit\" onClick={onNewConversation}>\n {tr(\"newConversation\")}\n </button>\n </div>\n )}\n <form className=\"ecw-composer\" onSubmit={submit}>\n <label className=\"ecw-sr-only\" htmlFor={`${uid}-input`}>{tr(\"message\")}</label>\n <input\n id={`${uid}-input`}\n ref={firstFieldRef}\n className=\"ecw-input\"\n type=\"text\"\n autoComplete=\"off\"\n placeholder={tr(\"message\")}\n value={draft}\n onChange={handleDraftChange}\n onBlur={handleDraftBlur}\n disabled={!canCompose}\n />\n <button className=\"ecw-send\" type=\"submit\" disabled={!canCompose || draft.trim() === \"\" || sending} aria-label={sending ? tr(\"sending\") : tr(\"send\")}>\n {sending ? <span className=\"ecw-sr-only\">{tr(\"sending\")}</span> : <SendIcon />}\n </button>\n </form>\n </>\n );\n}\n\nexport function ChatWidget(props: ChatWidgetProps): ReactElement {\n const { endpoint, locale, welcome, projectName, realtime, labels } = props;\n const accentColor = props.accentColor ?? DEFAULT_ACCENT;\n\n const tr = useCallback((key: WidgetKey): string => t(locale, key, labels), [locale, labels]);\n const { state, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping } =\n useChat({ endpoint, realtime });\n\n const bubbleRef = useRef<HTMLButtonElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n const firstFieldRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n injectWidgetStyles();\n }, []);\n\n // Foco no primeiro campo ao abrir (form ou composer).\n useEffect(() => {\n if (state.open) firstFieldRef.current?.focus();\n }, [state.open, state.phase]);\n\n // ESC fecha o painel e devolve o foco ao balão.\n useEffect(() => {\n if (!state.open) return;\n const onKeyDown = (e: KeyboardEvent): void => {\n if (e.key !== \"Escape\") return;\n closePanel();\n bubbleRef.current?.focus();\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n document.removeEventListener(\"keydown\", onKeyDown);\n };\n }, [state.open, closePanel]);\n\n // Auto-scroll para a mensagem mais recente (ou os \"3 pontinhos\" de digitação).\n useEffect(() => {\n const list = listRef.current;\n if (list !== null) list.scrollTop = list.scrollHeight;\n }, [state.messages, state.ownerTyping, state.visitorTyping, state.open]);\n\n const handleClose = useCallback((): void => {\n closePanel();\n bubbleRef.current?.focus();\n }, [closePanel]);\n\n // Só uma sessão \"active\" aceita mensagem; \"closed\"/\"failed\" mostram o aviso com a ação\n // de nova conversa (que limpa o storage e devolve o visitante ao pré-chat form).\n const canCompose = state.session?.status === \"active\";\n\n const bubbleLabel = state.unread > 0 ? `${tr(\"openChat\")} (${state.unread})` : tr(\"openChat\");\n\n return (\n <div className=\"ecw-root\" style={{ \"--ecw-accent\": accentColor } as CSSProperties}>\n <button\n ref={bubbleRef}\n type=\"button\"\n className=\"ecw-button\"\n aria-label={bubbleLabel}\n aria-expanded={state.open}\n aria-haspopup=\"dialog\"\n onClick={togglePanel}\n >\n <ChatIcon />\n {state.unread > 0 && <span className=\"ecw-badge\">{state.unread}</span>}\n </button>\n\n {state.open && (\n <section className=\"ecw-panel\" role=\"dialog\" aria-label=\"Chat\">\n <header className=\"ecw-header\">\n <span className=\"ecw-title\">{projectName}</span>\n {state.session !== null && <span className=\"ecw-code\">#{state.session.code}</span>}\n <button type=\"button\" className=\"ecw-close\" aria-label={tr(\"close\")} onClick={handleClose}>\n <span aria-hidden=\"true\">×</span>\n </button>\n </header>\n\n {state.phase === \"chat\" ? (\n <ChatPanel\n messages={state.messages}\n canCompose={canCompose}\n sending={state.sending}\n locale={locale}\n tr={tr}\n listRef={listRef}\n firstFieldRef={firstFieldRef}\n ownerTyping={state.ownerTyping}\n visitorTyping={state.visitorTyping}\n onTyping={notifyTyping}\n onSend={sendMessage}\n onRetry={retryMessage}\n onNewConversation={startNewConversation}\n />\n ) : (\n <PreChatForm\n welcome={welcome}\n tr={tr}\n error={state.error}\n sending={state.sending}\n firstFieldRef={firstFieldRef}\n onSubmit={submitForm}\n />\n )}\n\n <footer className=\"ecw-footer\">\n {tr(\"poweredBy\")}{\" \"}\n <a\n className=\"ecw-powered-link\"\n href=\"https://erlancarreira.com.br\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Erlan Carreira\n </a>\n </footer>\n </section>\n )}\n </div>\n );\n}\n","// src/widget/i18n.ts — dicionários do widget (pt/en/es) + helper `t`.\n//\n// Contrato (Task 9): as 15 chaves abaixo são OBRIGATÓRIAS em todos os idiomas — o tipo\n// `WidgetDictionary = Record<WidgetKey, string>` torna a paridade verificável pelo\n// compilador, e o teste em test/widget/widget.test.tsx a pinar em runtime.\n//\n// `t(locale, key, overrides?)` resolve na ordem: override do consumidor → dicionário do\n// locale → pt (fallback seguro) → própria chave (nunca devolve undefined).\n\nexport type WidgetLocale = \"pt\" | \"en\" | \"es\";\n\nexport const WIDGET_KEYS = [\n \"openChat\",\n \"close\",\n \"name\",\n \"phone\",\n \"message\",\n \"send\",\n \"sending\",\n \"welcomeNotice\",\n \"privacyNote\",\n \"invalidPhone\",\n \"sendError\",\n \"retry\",\n \"sessionClosed\",\n \"newConversation\",\n \"poweredBy\",\n \"typing\",\n] as const;\n\nexport type WidgetKey = (typeof WIDGET_KEYS)[number];\n\n/** Dicionário completo: `Record<WidgetKey, string>` força todas as chaves no compilador. */\nexport type WidgetDictionary = Record<WidgetKey, string>;\n\nexport const pt: WidgetDictionary = {\n openChat: \"Abrir chat\",\n close: \"Fechar\",\n name: \"Nome\",\n phone: \"WhatsApp\",\n message: \"Mensagem\",\n send: \"Enviar\",\n sending: \"Enviando…\",\n welcomeNotice: \"Ao continuar, você entra no grupo de WhatsApp do site com o nosso time.\",\n privacyNote: \"Seus dados são usados apenas para o atendimento.\",\n invalidPhone: \"Informe um número válido com DDD (mínimo 10 dígitos).\",\n sendError: \"Não foi possível enviar. Tente novamente.\",\n retry: \"Tentar novamente\",\n sessionClosed: \"Esta conversa foi encerrada. Abra uma nova para continuar.\",\n newConversation: \"Iniciar nova conversa\",\n poweredBy: \"Powered by\",\n typing: \"digitando…\",\n};\n\nexport const en: WidgetDictionary = {\n openChat: \"Open chat\",\n close: \"Close\",\n name: \"Name\",\n phone: \"WhatsApp\",\n message: \"Message\",\n send: \"Send\",\n sending: \"Sending…\",\n welcomeNotice: \"When you continue, you join the site's WhatsApp group with our team.\",\n privacyNote: \"Your data is used only for support.\",\n invalidPhone: \"Enter a valid number with area code (at least 10 digits).\",\n sendError: \"Couldn't send. Please try again.\",\n retry: \"Try again\",\n sessionClosed: \"This conversation was closed. Start a new one to continue.\",\n newConversation: \"Start a new conversation\",\n poweredBy: \"Powered by\",\n typing: \"typing…\",\n};\n\nexport const es: WidgetDictionary = {\n openChat: \"Abrir chat\",\n close: \"Cerrar\",\n name: \"Nombre\",\n phone: \"WhatsApp\",\n message: \"Mensaje\",\n send: \"Enviar\",\n sending: \"Enviando…\",\n welcomeNotice: \"Al continuar, entras en el grupo de WhatsApp del sitio con nuestro equipo.\",\n privacyNote: \"Tus datos se usan solo para la atención.\",\n invalidPhone: \"Introduce un número válido con área (mínimo 10 dígitos).\",\n sendError: \"No se pudo enviar. Inténtalo de nuevo.\",\n retry: \"Reintentar\",\n sessionClosed: \"Esta conversación fue cerrada. Abre una nueva para continuar.\",\n newConversation: \"Iniciar nueva conversación\",\n poweredBy: \"Powered by\",\n typing: \"escribiendo…\",\n};\n\nexport const dictionaries: Record<WidgetLocale, WidgetDictionary> = { pt, en, es };\n\n/**\n * Traduz `key` para `locale`, permitindo override pontual de copy (`labels` do widget).\n * Nunca lança: chave desconhecida devolve o texto de pt e, na ausência, a própria chave.\n */\nexport function t(\n locale: WidgetLocale,\n key: WidgetKey,\n overrides?: Partial<Record<string, string>>,\n): string {\n const override = overrides?.[key];\n if (override !== undefined) return override;\n const dict: Partial<WidgetDictionary> = dictionaries[locale];\n return dict[key] ?? dictionaries.pt[key] ?? key;\n}\n","// src/widget/styles.ts — injeção idempotente da folha de estilos do widget.\n//\n// O widget é auto-contido: `injectWidgetStyles()` insere `<style id=\"ecw-styles\">` com as\n// classes `.ecw-*` uma única vez (múltiplas chamadas / múltiplas instâncias não duplicam).\n// O texto abaixo é o ESPELHO exato de `src/widget/styles.css` (arquivo publicado no\n// subpath \"./widget/styles.css\" para quem prefere `<link>`); o teste de paridade em\n// test/widget/widget.test.tsx falha se os dois divergirem.\n\nexport const WIDGET_CSS = `/*\n * src/widget/styles.css — folha de estilos do ChatWidget (classes \\`.ecw-*\\`).\n *\n * Fonte canônica do CSS INJETADO em runtime: src/widget/styles.ts espelha este arquivo\n * e injeta \\`<style id=\"ecw-styles\">\\` via \\`injectWidgetStyles()\\` (o widget é\n * auto-contido — o consumidor não precisa importar CSS). O teste de paridade em\n * test/widget/widget.test.tsx garante que os dois textos não divergem.\n * Consumidores que preferem \\`<link>\\` podem importar \"@erlancarreira/evolution-chat/widget/styles.css\".\n *\n * Acessibilidade: \\`color-scheme\\` (dark nativo), \\`:focus-visible\\` sempre visível,\n * contraste AA sobre o accent (texto escuro no verde), e transições desligadas com\n * \\`prefers-reduced-motion\\`.\n */\n\n.ecw-root {\n --ecw-accent: #25d366;\n --ecw-accent-ink: #06251a;\n --ecw-surface: #ffffff;\n --ecw-surface-2: #f1f4f7;\n --ecw-text: #14181d;\n --ecw-muted: #51606e;\n --ecw-border: #d7dde3;\n --ecw-danger: #b3261e;\n --ecw-shadow: 0 8px 28px rgb(9 20 28 / 22%);\n color-scheme: light dark;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n font-size: 14px;\n line-height: 1.45;\n color: var(--ecw-text);\n}\n\n@media (prefers-color-scheme: dark) {\n .ecw-root {\n --ecw-accent-ink: #04170f;\n --ecw-surface: #151a20;\n --ecw-surface-2: #232a33;\n --ecw-text: #e8edf2;\n --ecw-muted: #9aa7b4;\n --ecw-border: #2c343d;\n --ecw-danger: #ff9a8f;\n --ecw-shadow: 0 8px 28px rgb(0 0 0 / 45%);\n }\n}\n\n/* ── balão flutuante ─────────────────────────────────────────────────────── */\n.ecw-button {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 2147483000;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 56px;\n height: 56px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n box-shadow: var(--ecw-shadow);\n cursor: pointer;\n transition: transform 160ms ease, box-shadow 160ms ease;\n}\n.ecw-button:hover { transform: translateY(-2px) scale(1.03); }\n.ecw-button:active { transform: translateY(0) scale(0.98); }\n.ecw-button svg { width: 26px; height: 26px; }\n\n.ecw-badge {\n position: absolute;\n top: -4px;\n right: -4px;\n min-width: 22px;\n height: 22px;\n padding: 0 6px;\n border-radius: 11px;\n border: 2px solid var(--ecw-surface);\n background: var(--ecw-danger);\n color: #fff;\n font-size: 12px;\n font-weight: 700;\n line-height: 18px;\n text-align: center;\n}\n\n/* ── painel ──────────────────────────────────────────────────────────────── */\n.ecw-panel {\n position: fixed;\n right: 20px;\n bottom: 88px;\n z-index: 2147483000;\n display: flex;\n flex-direction: column;\n width: min(380px, calc(100vw - 24px));\n min-width: 320px;\n height: 480px;\n max-height: 70vh;\n overflow: hidden;\n border: 1px solid var(--ecw-border);\n border-radius: 14px;\n background: var(--ecw-surface);\n box-shadow: var(--ecw-shadow);\n animation: ecw-pop 160ms ease-out;\n}\n@keyframes ecw-pop { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }\n\n.ecw-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n}\n.ecw-title { flex: 1; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.ecw-code { font-size: 12px; font-weight: 600; opacity: 0.85; letter-spacing: 0.02em; }\n.ecw-close {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font-size: 18px;\n line-height: 1;\n cursor: pointer;\n}\n.ecw-close:hover { background: rgb(0 0 0 / 12%); }\n\n/* ── lista de mensagens ──────────────────────────────────────────────────── */\n.ecw-list {\n flex: 1;\n margin: 0;\n padding: 12px;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 10px;\n list-style: none;\n background: var(--ecw-surface-2);\n}\n.ecw-item { display: flex; flex-direction: column; max-width: 82%; }\n.ecw-item--visitor { align-self: flex-end; align-items: flex-end; }\n.ecw-item--owner { align-self: flex-start; align-items: flex-start; }\n.ecw-item--system { align-self: center; align-items: center; max-width: 92%; }\n.ecw-system {\n padding: 4px 10px;\n border-radius: 999px;\n background: var(--ecw-surface);\n border: 1px solid var(--ecw-border);\n color: var(--ecw-muted);\n font-size: 11px;\n text-align: center;\n overflow-wrap: anywhere;\n}\n.ecw-bubble {\n padding: 8px 12px;\n border-radius: 12px;\n overflow-wrap: anywhere;\n white-space: pre-wrap;\n}\n.ecw-bubble--visitor {\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n border-bottom-right-radius: 4px;\n}\n.ecw-bubble--owner {\n background: var(--ecw-surface);\n color: var(--ecw-text);\n border: 1px solid var(--ecw-border);\n border-bottom-left-radius: 4px;\n}\n.ecw-meta { display: flex; align-items: center; gap: 6px; margin-top: 2px; font-size: 11px; color: var(--ecw-muted); }\n.ecw-status--failed { color: var(--ecw-danger); }\n.ecw-retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--ecw-danger);\n font: inherit;\n font-size: 11px;\n text-decoration: underline;\n cursor: pointer;\n}\n\n/* ── pré-chat form ───────────────────────────────────────────────────────── */\n.ecw-form { display: flex; flex-direction: column; gap: 12px; padding: 16px 14px; overflow-y: auto; }\n.ecw-welcome { margin: 0; font-weight: 600; }\n.ecw-notice { margin: 0; font-size: 12px; color: var(--ecw-muted); }\n.ecw-field { display: flex; flex-direction: column; gap: 4px; }\n.ecw-label { font-size: 12px; font-weight: 600; color: var(--ecw-text); }\n.ecw-input {\n width: 100%;\n padding: 9px 10px;\n border: 1px solid var(--ecw-border);\n border-radius: 8px;\n background: var(--ecw-surface);\n color: var(--ecw-text);\n font: inherit;\n}\n.ecw-input:focus { outline: 2px solid var(--ecw-accent); outline-offset: 1px; }\n.ecw-input[aria-invalid=\"true\"] { border-color: var(--ecw-danger); }\n.ecw-error { margin: 0; font-size: 12px; color: var(--ecw-danger); }\n.ecw-submit {\n padding: 10px 14px;\n border: none;\n border-radius: 8px;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n font: inherit;\n font-weight: 700;\n cursor: pointer;\n}\n.ecw-submit:disabled { opacity: 0.55; cursor: not-allowed; }\n\n/* ── composer ────────────────────────────────────────────────────────────── */\n.ecw-composer { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ecw-border); background: var(--ecw-surface); }\n.ecw-composer .ecw-input { flex: 1; }\n.ecw-send {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n padding: 0;\n border: none;\n border-radius: 8px;\n background: var(--ecw-accent);\n color: var(--ecw-accent-ink);\n cursor: pointer;\n}\n.ecw-send:disabled { opacity: 0.55; cursor: not-allowed; }\n.ecw-send svg { width: 18px; height: 18px; }\n\n/* ── indicador \"digitando…\" (3 pontinhos) ────────────────────────────────── */\n.ecw-typing {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 10px 14px;\n border-radius: 12px;\n border-bottom-left-radius: 4px;\n background: var(--ecw-surface);\n border: 1px solid var(--ecw-border);\n}\n.ecw-typing-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--ecw-muted);\n animation: ecw-typing-bounce 1.2s infinite ease-in-out;\n}\n.ecw-typing-dot:nth-child(2) { animation-delay: 0.18s; }\n.ecw-typing-dot:nth-child(3) { animation-delay: 0.36s; }\n@keyframes ecw-typing-bounce {\n 0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }\n 30% { transform: translateY(-4px); opacity: 1; }\n}\n\n/* ── sessão encerrada / falha ────────────────────────────────────────────── */\n.ecw-closed { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ecw-border); background: var(--ecw-surface); }\n\n.ecw-footer { padding: 6px 12px 10px; font-size: 11px; color: var(--ecw-muted); text-align: center; background: var(--ecw-surface); }\n.ecw-powered-link { color: var(--ecw-accent); font-weight: 600; text-decoration: none; }\n.ecw-powered-link:hover { text-decoration: underline; }\n\n/* ── utilitários ─────────────────────────────────────────────────────────── */\n.ecw-hp { display: none !important; }\n.ecw-sr-only {\n position: absolute;\n width: 1px; height: 1px;\n margin: -1px; padding: 0;\n overflow: hidden;\n clip: rect(0 0 0 0);\n white-space: nowrap;\n border: 0;\n}\n.ecw-button:focus-visible, .ecw-close:focus-visible, .ecw-submit:focus-visible,\n.ecw-send:focus-visible, .ecw-retry:focus-visible {\n outline: 3px solid #1b6fec;\n outline-offset: 2px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ecw-root *, .ecw-root *::before, .ecw-root *::after {\n animation: none !important;\n transition: none !important;\n }\n}\n\n@media (max-width: 420px) {\n .ecw-panel { right: 12px; left: 12px; width: auto; min-width: 0; bottom: 80px; }\n}\n`;\n\nlet injected = false;\n\n/**\n * Insere a folha do widget no `document.head` (idempotente: uma única tag\n * `<style id=\"ecw-styles\">` por documento, não importa quantas vezes seja chamada).\n * No-op fora de browser (SSR / Node) — o consumidor renderiza com `<link>` do subpath.\n */\nexport function injectWidgetStyles(): void {\n if (typeof document === \"undefined\") return;\n if (injected && document.getElementById(\"ecw-styles\") !== null) return;\n const existing = document.getElementById(\"ecw-styles\");\n if (existing !== null) {\n // Alguém (ex.: outra cópia do bundle) já injetou — marca e não duplica.\n injected = true;\n return;\n }\n const style = document.createElement(\"style\");\n style.id = \"ecw-styles\";\n style.textContent = WIDGET_CSS;\n document.head.appendChild(style);\n injected = true;\n}\n","// src/widget/use-chat.ts — máquina de estados do ChatWidget (hook puro, sem JSX).\n//\n// Fases: \"idle\" (painel nunca aberto, sem sessão) → \"form\" (pré-chat) → \"chat\" (sessão\n// ativa). A transição idle→form acontece na primeira abertura; form→chat no POST bem\n// sucedido; qualquer fase→form quando o GET devolve 404 (sessão expirada → storage limpo)\n// ou quando o visitante pede \"nova conversa\" numa sessão closed/failed (`startNewConversation`).\n//\n// Tempo real (Strategy, invisível ao usuário): com token em mãos, `realtime.subscribe`\n// anexa eventos; se o transporte cair (`onStatus(\"closed\")`), o hook liga um POLLING de\n// 5s no GET de histórico com `after=<última createdAt>` até o canal reabrir. Mensagens\n// `owner` que chegam com o painel fechado somam no badge de não lidas.\n//\n// Otimismo: POST anexa uma mensagem `tmp-…` com status \"pending\"; a resposta do servidor\n// SUBSTITUI o tmp (replace) — em falha, o tmp vira \"failed\" e ganha retry.\n\nimport { useCallback, useEffect, useReducer, useRef } from \"react\";\nimport type { ChatEvent, ChatMessage, ChatSessionStatus } from \"../types\";\nimport type { RealtimeHandle } from \"../bridge/types\";\n\n/** Chave de persistência da sessão no localStorage. */\nexport const SESSION_STORAGE_KEY = \"ecw:session\";\n/** Intervalo do fallback de polling quando o canal realtime fecha (ms). */\nexport const POLL_INTERVAL_MS = 5_000;\n/** Faixa (ms) do \"digitando…\" simulado exibido ANTES de uma mensagem recebida. */\nexport const TYPING_PRE_MESSAGE_MIN_MS = 700;\nexport const TYPING_PRE_MESSAGE_MAX_MS = 2200;\n/** Mínimo de dígitos (com DDD) para um WhatsApp ser aceito no form. */\nexport const MIN_PHONE_DIGITS = 10;\n\nexport type ChatPhase = \"idle\" | \"form\" | \"chat\";\n\nexport interface SessionInfo {\n code: string;\n status: ChatSessionStatus;\n visitorName: string;\n}\n\nexport interface ChatState {\n phase: ChatPhase;\n open: boolean;\n session: SessionInfo | null;\n token: string | null;\n messages: ChatMessage[];\n unread: number;\n sending: boolean;\n /** Chave i18n do erro visível (\"sendError\" | \"invalidPhone\") ou null. */\n error: string | null;\n /** True enquanto a DONA da plataforma (atendente) digita → \"3 pontinhos\" no widget. */\n ownerTyping: boolean;\n /** True enquanto o VISITANTE (esta ponta) digita — útil para um painel de atendente. */\n visitorTyping: boolean;\n}\n\ntype Action =\n | { type: \"open\" }\n | { type: \"close\" }\n | { type: \"restore\"; session: SessionInfo; token: string; messages: ChatMessage[] }\n | { type: \"start\"; session: SessionInfo; token: string; messages: ChatMessage[] }\n | { type: \"reset-form\" }\n | { type: \"append\"; message: ChatMessage }\n | { type: \"merge\"; messages: ChatMessage[] }\n | { type: \"replace\"; tmpId: string; message: ChatMessage }\n | { type: \"mark\"; id: string; status: ChatMessage[\"status\"] }\n | { type: \"session-status\"; status: ChatSessionStatus }\n | { type: \"sending\"; value: boolean }\n | { type: \"error\"; value: string | null }\n | { type: \"typing\"; from: \"owner\" | \"visitor\"; isTyping: boolean };\n\nconst initialState: ChatState = {\n phase: \"idle\",\n open: false,\n session: null,\n token: null,\n messages: [],\n unread: 0,\n sending: false,\n error: null,\n ownerTyping: false,\n visitorTyping: false,\n};\n\nfunction withMessage(state: ChatState, message: ChatMessage): ChatState {\n const index = state.messages.findIndex((m) => m.id === message.id);\n let messages: ChatMessage[];\n if (index >= 0) {\n messages = [...state.messages];\n messages[index] = message;\n } else {\n messages = [...state.messages, message];\n }\n const bump = state.open || message.direction !== \"owner\" ? 0 : 1;\n const next: ChatState = { ...state, messages, unread: state.unread + bump };\n // Quando chega uma mensagem da dona, ela parou de \"digitar\" (limpa os 3 pontinhos).\n if (message.direction === \"owner\") next.ownerTyping = false;\n return next;\n}\n\nexport function chatReducer(state: ChatState, action: Action): ChatState {\n switch (action.type) {\n case \"open\":\n return {\n ...state,\n open: true,\n unread: 0,\n phase: state.phase === \"idle\" ? \"form\" : state.phase,\n };\n case \"close\":\n return { ...state, open: false };\n case \"restore\":\n return {\n ...state,\n phase: \"chat\",\n session: action.session,\n token: action.token,\n messages: action.messages,\n };\n case \"start\": {\n // O POST devolve o histórico oficial (inclui a 1ª msg do visitante); qualquer tmp\n // \"pending\" ainda não confirmado pelo servidor é mantido no fim da lista.\n let next: ChatState = {\n ...state,\n phase: \"chat\",\n session: action.session,\n token: action.token,\n messages: action.messages,\n };\n for (const pending of state.messages) {\n const confirmed = action.messages.some(\n (m) => m.direction === \"visitor\" && m.body === pending.body,\n );\n if (pending.status === \"pending\" && !confirmed) next = withMessage(next, pending);\n }\n return next;\n }\n case \"reset-form\":\n return { ...state, phase: \"form\", session: null, token: null, messages: [], unread: 0, error: null };\n case \"append\":\n return withMessage(state, action.message);\n case \"merge\": {\n let next = state;\n for (const message of action.messages) next = withMessage(next, message);\n return next;\n }\n case \"replace\": {\n const messages = state.messages.map((m) => (m.id === action.tmpId ? action.message : m));\n const kept = messages.some((m) => m.id === action.message.id);\n return { ...state, messages: kept ? messages : [...messages, action.message] };\n }\n case \"mark\":\n return {\n ...state,\n messages: state.messages.map((m) => (m.id === action.id ? { ...m, status: action.status } : m)),\n };\n case \"session-status\": {\n if (state.session === null) return state;\n return { ...state, session: { ...state.session, status: action.status } };\n }\n case \"sending\":\n return { ...state, sending: action.value };\n case \"error\":\n return { ...state, error: action.value };\n case \"typing\":\n // Só transiciona se mudar de fato (evita re-render a cada batida de presença).\n return action.from === \"owner\"\n ? state.ownerTyping === action.isTyping\n ? state\n : { ...state, ownerTyping: action.isTyping }\n : state.visitorTyping === action.isTyping\n ? state\n : { ...state, visitorTyping: action.isTyping };\n }\n}\n\n// ─── helpers de telefone ──────────────────────────────────────────────────────\n\n/** Só dígitos (o servidor espera DDI+DDD+número sem máscara). */\nexport function normalizePhone(value: string): string {\n return value.replace(/\\D/g, \"\");\n}\n\n/** Máscara BR parcial `(11) 99999-8888`, progressiva enquanto o usuário digita. */\nexport function maskPhone(value: string): string {\n const digits = normalizePhone(value).slice(0, 11);\n if (digits.length === 0) return \"\";\n const ddd = digits.slice(0, 2);\n if (digits.length <= 2) return `(${ddd}`;\n const rest = digits.slice(2);\n if (rest.length <= 5) return `(${ddd}) ${rest}`;\n return `(${ddd}) ${rest.slice(0, 5)}-${rest.slice(5)}`;\n}\n\nexport function isValidPhone(value: string): boolean {\n return normalizePhone(value).length >= MIN_PHONE_DIGITS;\n}\n\n// ─── persistência ─────────────────────────────────────────────────────────────\n\ninterface StoredSession {\n token: string;\n code: string;\n}\n\nfunction readStoredSession(): StoredSession | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(SESSION_STORAGE_KEY);\n if (raw === null) return null;\n const parsed: unknown = JSON.parse(raw);\n if (\n parsed !== null &&\n typeof parsed === \"object\" &&\n typeof (parsed as StoredSession).token === \"string\" &&\n (parsed as StoredSession).token !== \"\"\n ) {\n const { token, code } = parsed as StoredSession;\n return { token, code: typeof code === \"string\" ? code : \"\" };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction writeStoredSession(session: StoredSession): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));\n } catch {\n /* storage cheio/bloqueado: sessão só não sobrevive ao reload */\n }\n}\n\nfunction clearStoredSession(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(SESSION_STORAGE_KEY);\n } catch {\n /* idem */\n }\n}\n\n// ─── contrato de rede (espelha createChatRoutes — Task 8) ────────────────────\n\ninterface HistoryResponse {\n session?: SessionInfo;\n messages?: ChatMessage[];\n}\n\ninterface StartResponse {\n session?: SessionInfo & { realtimeToken?: string };\n messages?: ChatMessage[];\n}\n\ninterface SendResponse {\n message?: ChatMessage;\n}\n\nfunction historyUrl(endpoint: string, token: string, after: string | null): string {\n const params = new URLSearchParams({ token });\n if (after !== null) params.set(\"after\", after);\n const sep = endpoint.includes(\"?\") ? \"&\" : \"?\";\n return `${endpoint}${sep}${params.toString()}`;\n}\n\nfunction lastCursor(messages: ChatMessage[]): string | null {\n let latest: string | null = null;\n for (const m of messages) if (latest === null || m.createdAt > latest) latest = m.createdAt;\n return latest;\n}\n\n// ─── hook ─────────────────────────────────────────────────────────────────────\n\nexport interface UseChatOptions {\n endpoint: string;\n realtime: RealtimeHandle;\n /** Rota que recebe o sinal \"visitante digitando\" (POST { token, isTyping }). Default: `${endpoint}/typing`. */\n typingEndpoint?: string;\n}\n\nexport interface UseChatResult {\n state: ChatState;\n openPanel(): void;\n closePanel(): void;\n togglePanel(): void;\n submitForm(input: { name: string; phone: string; message: string; honeypot: string }): Promise<void>;\n sendMessage(text: string): Promise<void>;\n retryMessage(id: string): Promise<void>;\n /** Descarta a sessão encerrada/falha (storage + estado) e volta ao pré-chat form. */\n startNewConversation(): void;\n /** Avisa o servidor que o visitante está (true) ou parou (false) de digitar. Best-effort. */\n notifyTyping(isTyping: boolean): Promise<void>;\n}\n\nlet tmpSeq = 0;\nfunction tmpMessage(body: string): ChatMessage {\n tmpSeq += 1;\n return {\n id: `tmp-${tmpSeq}`,\n sessionId: \"\",\n direction: \"visitor\",\n body,\n status: \"pending\",\n waMessageId: null,\n createdAt: new Date().toISOString(),\n };\n}\n\nexport function useChat({ endpoint, realtime, typingEndpoint }: UseChatOptions): UseChatResult {\n const [state, dispatch] = useReducer(chatReducer, initialState);\n\n const typingUrl = typingEndpoint ?? `${endpoint.replace(/\\/+$/, \"\")}/typing`;\n\n // Espelho do estado para callbacks estáveis (eventos realtime/poll chegam fora do React).\n const stateRef = useRef(state);\n useEffect(() => {\n stateRef.current = state;\n }, [state]);\n\n // A porta pode chegar como objeto novo a cada render do consumidor; a assinatura deve\n // depender apenas do token, nunca da identidade do handle.\n const realtimeRef = useRef(realtime);\n useEffect(() => {\n realtimeRef.current = realtime;\n }, [realtime]);\n\n const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const stopPoll = useCallback((): void => {\n if (pollRef.current !== null) {\n clearInterval(pollRef.current);\n pollRef.current = null;\n }\n }, []);\n\n const pollOnce = useCallback(async (): Promise<void> => {\n const { token, messages } = stateRef.current;\n if (token === null) return;\n try {\n const res = await fetch(historyUrl(endpoint, token, lastCursor(messages)), {\n method: \"GET\",\n headers: { accept: \"application/json\" },\n });\n if (res.status === 404) {\n clearStoredSession();\n stopPoll();\n dispatch({ type: \"reset-form\" });\n return;\n }\n if (!res.ok) return;\n const data = (await res.json()) as HistoryResponse;\n if (Array.isArray(data.messages)) dispatch({ type: \"merge\", messages: data.messages });\n if (data.session !== undefined) dispatch({ type: \"session-status\", status: data.session.status });\n } catch {\n /* offline: o próximo tick tenta de novo */\n }\n }, [endpoint, stopPoll]);\n\n const startPoll = useCallback((): void => {\n if (pollRef.current !== null) return;\n pollRef.current = setInterval(() => {\n void pollOnce();\n }, POLL_INTERVAL_MS);\n }, [pollOnce]);\n\n // Abertura/fechamento do canal realtime (Strategy): caiu → polling; voltou → para.\n const handleStatus = useCallback(\n (s: \"open\" | \"closed\"): void => {\n if (s === \"closed\") startPoll();\n else stopPoll();\n },\n [startPoll, stopPoll],\n );\n\n const handleEvent = useCallback((e: ChatEvent): void => {\n if (e.type === \"message\" && e.message !== undefined) {\n const msg = e.message;\n // Mensagem recebida da DONA (staff): como a instância conectada não emite presença\n // do próprio \"digitando\" (Evolution/Baileys), simulamos o \"3 pontinhos\" antes de\n // exibir a mensagem — dá o feedback visual de que a outra ponta está respondendo.\n if (msg.direction === \"owner\") {\n dispatch({ type: \"typing\", from: \"owner\", isTyping: true });\n const text = typeof msg.body === \"string\" ? msg.body : \"\";\n const delay = Math.min(\n TYPING_PRE_MESSAGE_MAX_MS,\n Math.max(TYPING_PRE_MESSAGE_MIN_MS, text.length * 22),\n );\n setTimeout(() => {\n dispatch({ type: \"append\", message: msg });\n }, delay);\n } else {\n dispatch({ type: \"append\", message: msg });\n }\n } else if (e.type === \"session\" && e.status !== undefined) {\n dispatch({ type: \"session-status\", status: e.status });\n } else if (e.type === \"typing\" && e.from !== undefined) {\n dispatch({ type: \"typing\", from: e.from, isTyping: e.isTyping });\n }\n }, []);\n\n // Boot: sessão persistida → GET de histórico (404 limpa o storage e volta ao form).\n useEffect(() => {\n const stored = readStoredSession();\n if (stored === null) return;\n let cancelled = false;\n void (async () => {\n try {\n const res = await fetch(historyUrl(endpoint, stored.token, null), {\n method: \"GET\",\n headers: { accept: \"application/json\" },\n });\n if (cancelled) return;\n if (res.status === 404) {\n clearStoredSession();\n dispatch({ type: \"reset-form\" });\n return;\n }\n if (!res.ok) return;\n const data = (await res.json()) as HistoryResponse;\n if (cancelled || data.session === undefined) return;\n dispatch({\n type: \"restore\",\n session: data.session,\n token: stored.token,\n messages: Array.isArray(data.messages) ? data.messages : [],\n });\n } catch {\n /* sem rede: mantém o form/idle; o retry fica com o usuário */\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [endpoint]);\n\n // Assinatura do canal enquanto houver token (re-assina quando a sessão muda).\n useEffect(() => {\n const token = state.token;\n if (token === null) return;\n const unsubscribe = realtimeRef.current.subscribe(token, handleEvent, handleStatus);\n return () => {\n unsubscribe();\n stopPoll();\n };\n }, [state.token, handleEvent, handleStatus, stopPoll]);\n\n useEffect(() => stopPoll, [stopPoll]);\n\n const openPanel = useCallback((): void => {\n dispatch({ type: \"open\" });\n }, []);\n\n const closePanel = useCallback((): void => {\n dispatch({ type: \"close\" });\n }, []);\n\n const togglePanel = useCallback((): void => {\n if (stateRef.current.open) dispatch({ type: \"close\" });\n else dispatch({ type: \"open\" });\n }, []);\n\n const postMessage = useCallback(\n async (token: string, body: string, id: string): Promise<void> => {\n dispatch({ type: \"sending\", value: true });\n dispatch({ type: \"error\", value: null });\n try {\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ token, message: body }),\n });\n if (!res.ok) {\n dispatch({ type: \"mark\", id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n return;\n }\n const data = (await res.json()) as SendResponse;\n if (data.message !== undefined) dispatch({ type: \"replace\", tmpId: id, message: data.message });\n else dispatch({ type: \"mark\", id, status: \"failed\" });\n } catch {\n dispatch({ type: \"mark\", id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n } finally {\n dispatch({ type: \"sending\", value: false });\n }\n },\n [endpoint],\n );\n\n const sendMessage = useCallback(\n async (text: string): Promise<void> => {\n const body = text.trim();\n const token = stateRef.current.token;\n if (body === \"\" || token === null) return;\n const optimistic = tmpMessage(body);\n dispatch({ type: \"append\", message: optimistic });\n await postMessage(token, body, optimistic.id);\n },\n [postMessage],\n );\n\n const retryMessage = useCallback(\n async (id: string): Promise<void> => {\n const token = stateRef.current.token;\n const target = stateRef.current.messages.find((m) => m.id === id);\n if (token === null || target === undefined || target.direction !== \"visitor\") return;\n dispatch({ type: \"mark\", id, status: \"pending\" });\n await postMessage(token, target.body, id);\n },\n [postMessage],\n );\n\n // Sessão closed/failed: o visitante resolve o beco sem saída pedindo uma conversa nova.\n // Limpa o storage (senão o próximo boot restauraria a mesma sessão morta) e volta ao\n // form; a assinatura realtime cai sozinha na limpeza do efeito, porque token → null.\n const startNewConversation = useCallback((): void => {\n clearStoredSession();\n dispatch({ type: \"reset-form\" });\n }, []);\n\n // Avisa o servidor que o visitante está (ou parou de) digitar. Best-effort: nunca\n // deve quebrar o fluxo de envio de mensagens — falhas são silenciadas.\n const notifyTyping = useCallback(\n async (isTyping: boolean): Promise<void> => {\n const token = stateRef.current.token;\n if (token === null) return;\n try {\n await fetch(typingUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ token, isTyping }),\n });\n } catch {\n /* offline/erro: digitação é cosmética, ignora */\n }\n },\n [typingUrl],\n );\n\n const submitForm = useCallback(\n async ({ name, phone, message, honeypot }: { name: string; phone: string; message: string; honeypot: string }): Promise<void> => {\n // Anti-bot silencioso: honeypot preenchido → nada sai do navegador (o servidor\n // também fingiria sucesso; aqui nem há request).\n if (honeypot.trim() !== \"\") return;\n const digits = normalizePhone(phone);\n if (name.trim() === \"\" || message.trim() === \"\" || digits.length < MIN_PHONE_DIGITS) {\n dispatch({ type: \"error\", value: digits.length < MIN_PHONE_DIGITS ? \"invalidPhone\" : \"sendError\" });\n return;\n }\n const optimistic = tmpMessage(message.trim());\n dispatch({ type: \"append\", message: optimistic });\n dispatch({ type: \"sending\", value: true });\n dispatch({ type: \"error\", value: null });\n try {\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ name: name.trim(), phone: digits, message: message.trim(), honeypot: \"\" }),\n });\n if (!res.ok) {\n dispatch({ type: \"mark\", id: optimistic.id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n return;\n }\n const data = (await res.json()) as StartResponse;\n const session = data.session;\n if (session === undefined || typeof session.realtimeToken !== \"string\" || session.realtimeToken === \"\") {\n dispatch({ type: \"mark\", id: optimistic.id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n return;\n }\n const token = session.realtimeToken;\n writeStoredSession({ token, code: session.code });\n dispatch({\n type: \"start\",\n session: { code: session.code, status: session.status, visitorName: session.visitorName },\n token,\n messages: Array.isArray(data.messages) ? data.messages : [],\n });\n } catch {\n dispatch({ type: \"mark\", id: optimistic.id, status: \"failed\" });\n dispatch({ type: \"error\", value: \"sendError\" });\n } finally {\n dispatch({ type: \"sending\", value: false });\n }\n },\n [endpoint],\n );\n\n return { state, openPanel, closePanel, togglePanel, submitForm, sendMessage, retryMessage, startNewConversation, notifyTyping };\n}\n"],"mappings":";AAgBA;AAAA,EACE,eAAAA;AAAA,EACA,aAAAC;AAAA,EACA;AAAA,EACA,UAAAC;AAAA,EACA;AAAA,OAMK;;;AChBA,IAAM,cAAc;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,IAAM,KAAuB;AAAA,EAClC,UAAU;AAAA,EACV,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,eAAe;AAAA,EACf,aAAa;AAAA,EACb,cAAc;AAAA,EACd,WAAW;AAAA,EACX,OAAO;AAAA,EACP,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,QAAQ;AACV;AAEO,IAAM,KAAuB;AAAA,EAClC,UAAU;AAAA,EACV,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,eAAe;AAAA,EACf,aAAa;AAAA,EACb,cAAc;AAAA,EACd,WAAW;AAAA,EACX,OAAO;AAAA,EACP,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,QAAQ;AACV;AAEO,IAAM,KAAuB;AAAA,EAClC,UAAU;AAAA,EACV,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,eAAe;AAAA,EACf,aAAa;AAAA,EACb,cAAc;AAAA,EACd,WAAW;AAAA,EACX,OAAO;AAAA,EACP,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,QAAQ;AACV;AAEO,IAAM,eAAuD,EAAE,IAAI,IAAI,GAAG;AAM1E,SAAS,EACd,QACA,KACA,WACQ;AACR,QAAM,WAAW,YAAY,GAAG;AAChC,MAAI,aAAa,OAAW,QAAO;AACnC,QAAM,OAAkC,aAAa,MAAM;AAC3D,SAAO,KAAK,GAAG,KAAK,aAAa,GAAG,GAAG,KAAK;AAC9C;;;ACnGO,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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","names":["useCallback","useEffect","useRef","useRef","useEffect","useCallback"]}
|
|
@@ -8207,14 +8207,18 @@ var EvolutionChatEmbed = (() => {
|
|
|
8207
8207
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", children: tr("privacyNote") })
|
|
8208
8208
|
] });
|
|
8209
8209
|
}
|
|
8210
|
-
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping,
|
|
8210
|
+
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, onTyping, onSend, onRetry, onNewConversation }) {
|
|
8211
8211
|
const uid = (0, import_react2.useId)();
|
|
8212
8212
|
const [draft, setDraft] = (0, import_react2.useState)("");
|
|
8213
8213
|
const typingRef = (0, import_react2.useRef)(false);
|
|
8214
8214
|
const typingTimer = (0, import_react2.useRef)(null);
|
|
8215
|
+
const selfTypingRef = (0, import_react2.useRef)(false);
|
|
8216
|
+
const selfTimer = (0, import_react2.useRef)(null);
|
|
8217
|
+
const [selfTyping, setSelfTyping] = (0, import_react2.useState)(false);
|
|
8215
8218
|
(0, import_react2.useEffect)(() => {
|
|
8216
8219
|
return () => {
|
|
8217
8220
|
if (typingTimer.current !== null) clearTimeout(typingTimer.current);
|
|
8221
|
+
if (selfTimer.current !== null) clearTimeout(selfTimer.current);
|
|
8218
8222
|
};
|
|
8219
8223
|
}, []);
|
|
8220
8224
|
const stopTypingSignal = (0, import_react2.useCallback)(() => {
|
|
@@ -8227,6 +8231,16 @@ var EvolutionChatEmbed = (() => {
|
|
|
8227
8231
|
void onTyping(false);
|
|
8228
8232
|
}
|
|
8229
8233
|
}, [onTyping]);
|
|
8234
|
+
const stopSelfTyping = (0, import_react2.useCallback)(() => {
|
|
8235
|
+
if (selfTimer.current !== null) {
|
|
8236
|
+
clearTimeout(selfTimer.current);
|
|
8237
|
+
selfTimer.current = null;
|
|
8238
|
+
}
|
|
8239
|
+
if (selfTypingRef.current) {
|
|
8240
|
+
selfTypingRef.current = false;
|
|
8241
|
+
setSelfTyping(false);
|
|
8242
|
+
}
|
|
8243
|
+
}, []);
|
|
8230
8244
|
const handleDraftChange = (e) => {
|
|
8231
8245
|
setDraft(e.target.value);
|
|
8232
8246
|
if (!canCompose) return;
|
|
@@ -8236,15 +8250,23 @@ var EvolutionChatEmbed = (() => {
|
|
|
8236
8250
|
}
|
|
8237
8251
|
if (typingTimer.current !== null) clearTimeout(typingTimer.current);
|
|
8238
8252
|
typingTimer.current = setTimeout(stopTypingSignal, 2500);
|
|
8253
|
+
if (!selfTypingRef.current) {
|
|
8254
|
+
selfTypingRef.current = true;
|
|
8255
|
+
setSelfTyping(true);
|
|
8256
|
+
}
|
|
8257
|
+
if (selfTimer.current !== null) clearTimeout(selfTimer.current);
|
|
8258
|
+
selfTimer.current = setTimeout(stopSelfTyping, 2500);
|
|
8239
8259
|
};
|
|
8240
8260
|
const handleDraftBlur = () => {
|
|
8241
8261
|
stopTypingSignal();
|
|
8262
|
+
stopSelfTyping();
|
|
8242
8263
|
};
|
|
8243
8264
|
const submit = (e) => {
|
|
8244
8265
|
e.preventDefault();
|
|
8245
8266
|
const text = draft.trim();
|
|
8246
8267
|
if (text === "" || !canCompose) return;
|
|
8247
8268
|
stopTypingSignal();
|
|
8269
|
+
stopSelfTyping();
|
|
8248
8270
|
setDraft("");
|
|
8249
8271
|
void onSend(text);
|
|
8250
8272
|
};
|
|
@@ -8263,7 +8285,7 @@ var EvolutionChatEmbed = (() => {
|
|
|
8263
8285
|
] }, m.id)
|
|
8264
8286
|
),
|
|
8265
8287
|
ownerTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { from: "owner", label: tr("typing") }),
|
|
8266
|
-
|
|
8288
|
+
selfTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { from: "visitor", label: tr("typing") })
|
|
8267
8289
|
] }),
|
|
8268
8290
|
!canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-closed", children: [
|
|
8269
8291
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", role: "status", children: tr("sessionClosed") }),
|