@erlancarreira/evolution-chat 0.1.6 → 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 CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "@erlancarreira/evolution-chat",
3
- "version": "0.1.6",
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": ["api", "bridge", "next", "transports", "widget", "widget-embed", "README.md"],
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": { "optional": true },
23
- "react": { "optional": true },
24
- "react-dom": { "optional": true }
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,34 +976,66 @@ 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, visitorTyping, onTyping, onSend, onRetry, onNewConversation }) {
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
+ const typingRef = (0, import_react2.useRef)(false);
982
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);
983
987
  (0, import_react2.useEffect)(() => {
984
988
  return () => {
985
989
  if (typingTimer.current !== null) clearTimeout(typingTimer.current);
990
+ if (selfTimer.current !== null) clearTimeout(selfTimer.current);
986
991
  };
987
992
  }, []);
993
+ const stopTypingSignal = (0, import_react2.useCallback)(() => {
994
+ if (typingTimer.current !== null) {
995
+ clearTimeout(typingTimer.current);
996
+ typingTimer.current = null;
997
+ }
998
+ if (typingRef.current) {
999
+ typingRef.current = false;
1000
+ void onTyping(false);
1001
+ }
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
+ }, []);
988
1013
  const handleDraftChange = (e) => {
989
1014
  setDraft(e.target.value);
990
1015
  if (!canCompose) return;
991
- void onTyping(true);
1016
+ if (!typingRef.current) {
1017
+ typingRef.current = true;
1018
+ void onTyping(true);
1019
+ }
992
1020
  if (typingTimer.current !== null) clearTimeout(typingTimer.current);
993
- typingTimer.current = setTimeout(() => {
994
- void onTyping(false);
995
- typingTimer.current = null;
996
- }, 2500);
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);
1028
+ };
1029
+ const handleDraftBlur = () => {
1030
+ stopTypingSignal();
1031
+ stopSelfTyping();
997
1032
  };
998
1033
  const submit = (e) => {
999
1034
  e.preventDefault();
1000
1035
  const text = draft.trim();
1001
1036
  if (text === "" || !canCompose) return;
1002
- if (typingTimer.current !== null) {
1003
- clearTimeout(typingTimer.current);
1004
- typingTimer.current = null;
1005
- }
1006
- void onTyping(false);
1037
+ stopTypingSignal();
1038
+ stopSelfTyping();
1007
1039
  setDraft("");
1008
1040
  void onSend(text);
1009
1041
  };
@@ -1022,7 +1054,7 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
1022
1054
  ] }, m.id)
1023
1055
  ),
1024
1056
  ownerTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { from: "owner", label: tr("typing") }),
1025
- visitorTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { from: "visitor", label: tr("typing") })
1057
+ selfTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { from: "visitor", label: tr("typing") })
1026
1058
  ] }),
1027
1059
  !canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-closed", children: [
1028
1060
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", role: "status", children: tr("sessionClosed") }),
@@ -1041,6 +1073,7 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
1041
1073
  placeholder: tr("message"),
1042
1074
  value: draft,
1043
1075
  onChange: handleDraftChange,
1076
+ onBlur: handleDraftBlur,
1044
1077
  disabled: !canCompose
1045
1078
  }
1046
1079
  ),
@@ -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 // Debounce do sinal \"visitante digitando\": a cada tecla avisa true e agenda um false\n // para 2,5s de inatividade; ao enviar, cancela o timer e avisa false imediatamente.\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 handleDraftChange = (e: ChangeEvent<HTMLInputElement>): void => {\n setDraft(e.target.value);\n if (!canCompose) return;\n void onTyping(true);\n if (typingTimer.current !== null) clearTimeout(typingTimer.current);\n typingTimer.current = setTimeout(() => {\n void onTyping(false);\n typingTimer.current = null;\n }, 2500);\n };\n\n const submit = (e: FormEvent): void => {\n e.preventDefault();\n const text = draft.trim();\n if (text === \"\" || !canCompose) return;\n if (typingTimer.current !== null) {\n clearTimeout(typingTimer.current);\n typingTimer.current = null;\n }\n void onTyping(false);\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 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,34 +941,66 @@ 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, visitorTyping, onTyping, onSend, onRetry, onNewConversation }) {
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
+ const typingRef = useRef2(false);
947
948
  const typingTimer = useRef2(null);
949
+ const selfTypingRef = useRef2(false);
950
+ const selfTimer = useRef2(null);
951
+ const [selfTyping, setSelfTyping] = useState(false);
948
952
  useEffect2(() => {
949
953
  return () => {
950
954
  if (typingTimer.current !== null) clearTimeout(typingTimer.current);
955
+ if (selfTimer.current !== null) clearTimeout(selfTimer.current);
951
956
  };
952
957
  }, []);
958
+ const stopTypingSignal = useCallback2(() => {
959
+ if (typingTimer.current !== null) {
960
+ clearTimeout(typingTimer.current);
961
+ typingTimer.current = null;
962
+ }
963
+ if (typingRef.current) {
964
+ typingRef.current = false;
965
+ void onTyping(false);
966
+ }
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
+ }, []);
953
978
  const handleDraftChange = (e) => {
954
979
  setDraft(e.target.value);
955
980
  if (!canCompose) return;
956
- void onTyping(true);
981
+ if (!typingRef.current) {
982
+ typingRef.current = true;
983
+ void onTyping(true);
984
+ }
957
985
  if (typingTimer.current !== null) clearTimeout(typingTimer.current);
958
- typingTimer.current = setTimeout(() => {
959
- void onTyping(false);
960
- typingTimer.current = null;
961
- }, 2500);
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);
993
+ };
994
+ const handleDraftBlur = () => {
995
+ stopTypingSignal();
996
+ stopSelfTyping();
962
997
  };
963
998
  const submit = (e) => {
964
999
  e.preventDefault();
965
1000
  const text = draft.trim();
966
1001
  if (text === "" || !canCompose) return;
967
- if (typingTimer.current !== null) {
968
- clearTimeout(typingTimer.current);
969
- typingTimer.current = null;
970
- }
971
- void onTyping(false);
1002
+ stopTypingSignal();
1003
+ stopSelfTyping();
972
1004
  setDraft("");
973
1005
  void onSend(text);
974
1006
  };
@@ -987,7 +1019,7 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
987
1019
  ] }, m.id)
988
1020
  ),
989
1021
  ownerTyping && canCompose && /* @__PURE__ */ jsx(TypingIndicator, { from: "owner", label: tr("typing") }),
990
- visitorTyping && canCompose && /* @__PURE__ */ jsx(TypingIndicator, { from: "visitor", label: tr("typing") })
1022
+ selfTyping && canCompose && /* @__PURE__ */ jsx(TypingIndicator, { from: "visitor", label: tr("typing") })
991
1023
  ] }),
992
1024
  !canCompose && /* @__PURE__ */ jsxs("div", { className: "ecw-closed", children: [
993
1025
  /* @__PURE__ */ jsx("p", { className: "ecw-notice", role: "status", children: tr("sessionClosed") }),
@@ -1006,6 +1038,7 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
1006
1038
  placeholder: tr("message"),
1007
1039
  value: draft,
1008
1040
  onChange: handleDraftChange,
1041
+ onBlur: handleDraftBlur,
1009
1042
  disabled: !canCompose
1010
1043
  }
1011
1044
  ),
@@ -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 // Debounce do sinal \"visitante digitando\": a cada tecla avisa true e agenda um false\n // para 2,5s de inatividade; ao enviar, cancela o timer e avisa false imediatamente.\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 handleDraftChange = (e: ChangeEvent<HTMLInputElement>): void => {\n setDraft(e.target.value);\n if (!canCompose) return;\n void onTyping(true);\n if (typingTimer.current !== null) clearTimeout(typingTimer.current);\n typingTimer.current = setTimeout(() => {\n void onTyping(false);\n typingTimer.current = null;\n }, 2500);\n };\n\n const submit = (e: FormEvent): void => {\n e.preventDefault();\n const text = draft.trim();\n if (text === \"\" || !canCompose) return;\n if (typingTimer.current !== null) {\n clearTimeout(typingTimer.current);\n typingTimer.current = null;\n }\n void onTyping(false);\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 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,34 +8207,66 @@ 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, visitorTyping, onTyping, onSend, onRetry, onNewConversation }) {
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
+ const typingRef = (0, import_react2.useRef)(false);
8213
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);
8214
8218
  (0, import_react2.useEffect)(() => {
8215
8219
  return () => {
8216
8220
  if (typingTimer.current !== null) clearTimeout(typingTimer.current);
8221
+ if (selfTimer.current !== null) clearTimeout(selfTimer.current);
8217
8222
  };
8218
8223
  }, []);
8224
+ const stopTypingSignal = (0, import_react2.useCallback)(() => {
8225
+ if (typingTimer.current !== null) {
8226
+ clearTimeout(typingTimer.current);
8227
+ typingTimer.current = null;
8228
+ }
8229
+ if (typingRef.current) {
8230
+ typingRef.current = false;
8231
+ void onTyping(false);
8232
+ }
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
+ }, []);
8219
8244
  const handleDraftChange = (e) => {
8220
8245
  setDraft(e.target.value);
8221
8246
  if (!canCompose) return;
8222
- void onTyping(true);
8247
+ if (!typingRef.current) {
8248
+ typingRef.current = true;
8249
+ void onTyping(true);
8250
+ }
8223
8251
  if (typingTimer.current !== null) clearTimeout(typingTimer.current);
8224
- typingTimer.current = setTimeout(() => {
8225
- void onTyping(false);
8226
- typingTimer.current = null;
8227
- }, 2500);
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);
8259
+ };
8260
+ const handleDraftBlur = () => {
8261
+ stopTypingSignal();
8262
+ stopSelfTyping();
8228
8263
  };
8229
8264
  const submit = (e) => {
8230
8265
  e.preventDefault();
8231
8266
  const text = draft.trim();
8232
8267
  if (text === "" || !canCompose) return;
8233
- if (typingTimer.current !== null) {
8234
- clearTimeout(typingTimer.current);
8235
- typingTimer.current = null;
8236
- }
8237
- void onTyping(false);
8268
+ stopTypingSignal();
8269
+ stopSelfTyping();
8238
8270
  setDraft("");
8239
8271
  void onSend(text);
8240
8272
  };
@@ -8253,7 +8285,7 @@ var EvolutionChatEmbed = (() => {
8253
8285
  ] }, m.id)
8254
8286
  ),
8255
8287
  ownerTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { from: "owner", label: tr("typing") }),
8256
- visitorTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { from: "visitor", label: tr("typing") })
8288
+ selfTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { from: "visitor", label: tr("typing") })
8257
8289
  ] }),
8258
8290
  !canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-closed", children: [
8259
8291
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", role: "status", children: tr("sessionClosed") }),
@@ -8272,6 +8304,7 @@ var EvolutionChatEmbed = (() => {
8272
8304
  placeholder: tr("message"),
8273
8305
  value: draft,
8274
8306
  onChange: handleDraftChange,
8307
+ onBlur: handleDraftBlur,
8275
8308
  disabled: !canCompose
8276
8309
  }
8277
8310
  ),