@erlancarreira/evolution-chat 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/widget/index.cjs +23 -6
- package/widget/index.cjs.map +1 -1
- package/widget/index.js +23 -6
- package/widget/index.js.map +1 -1
- package/widget-embed/evolution-chat.iife.js +23 -6
package/package.json
CHANGED
package/widget/index.cjs
CHANGED
|
@@ -442,6 +442,8 @@ function injectWidgetStyles() {
|
|
|
442
442
|
var import_react = require("react");
|
|
443
443
|
var SESSION_STORAGE_KEY = "ecw:session";
|
|
444
444
|
var POLL_INTERVAL_MS = 5e3;
|
|
445
|
+
var TYPING_PRE_MESSAGE_MIN_MS = 700;
|
|
446
|
+
var TYPING_PRE_MESSAGE_MAX_MS = 2200;
|
|
445
447
|
var MIN_PHONE_DIGITS = 10;
|
|
446
448
|
var initialState = {
|
|
447
449
|
phase: "idle",
|
|
@@ -657,7 +659,20 @@ function useChat({ endpoint, realtime, typingEndpoint }) {
|
|
|
657
659
|
);
|
|
658
660
|
const handleEvent = (0, import_react.useCallback)((e) => {
|
|
659
661
|
if (e.type === "message" && e.message !== void 0) {
|
|
660
|
-
|
|
662
|
+
const msg = e.message;
|
|
663
|
+
if (msg.direction === "owner") {
|
|
664
|
+
dispatch({ type: "typing", from: "owner", isTyping: true });
|
|
665
|
+
const text = typeof msg.body === "string" ? msg.body : "";
|
|
666
|
+
const delay = Math.min(
|
|
667
|
+
TYPING_PRE_MESSAGE_MAX_MS,
|
|
668
|
+
Math.max(TYPING_PRE_MESSAGE_MIN_MS, text.length * 22)
|
|
669
|
+
);
|
|
670
|
+
setTimeout(() => {
|
|
671
|
+
dispatch({ type: "append", message: msg });
|
|
672
|
+
}, delay);
|
|
673
|
+
} else {
|
|
674
|
+
dispatch({ type: "append", message: msg });
|
|
675
|
+
}
|
|
661
676
|
} else if (e.type === "session" && e.status !== void 0) {
|
|
662
677
|
dispatch({ type: "session-status", status: e.status });
|
|
663
678
|
} else if (e.type === "typing" && e.from !== void 0) {
|
|
@@ -854,8 +869,8 @@ function StatusMark({ status }) {
|
|
|
854
869
|
const glyph = status === "pending" ? "\u2713" : status === "sent" ? "\u2713\u2713" : "\u26A0";
|
|
855
870
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: status === "failed" ? "ecw-status ecw-status--failed" : "ecw-status", "aria-hidden": "true", children: glyph });
|
|
856
871
|
}
|
|
857
|
-
function TypingIndicator({ label }) {
|
|
858
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className:
|
|
872
|
+
function TypingIndicator({ from, label }) {
|
|
873
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: `ecw-item ecw-item--${from}`, "aria-live": "polite", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-typing", role: "status", "aria-label": label, children: [
|
|
859
874
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-typing-dot" }),
|
|
860
875
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-typing-dot" }),
|
|
861
876
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-typing-dot" }),
|
|
@@ -961,7 +976,7 @@ function PreChatForm({ welcome, tr, error, sending, firstFieldRef, onSubmit }) {
|
|
|
961
976
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", children: tr("privacyNote") })
|
|
962
977
|
] });
|
|
963
978
|
}
|
|
964
|
-
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, onTyping, onSend, onRetry, onNewConversation }) {
|
|
979
|
+
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, visitorTyping, onTyping, onSend, onRetry, onNewConversation }) {
|
|
965
980
|
const uid = (0, import_react2.useId)();
|
|
966
981
|
const [draft, setDraft] = (0, import_react2.useState)("");
|
|
967
982
|
const typingTimer = (0, import_react2.useRef)(null);
|
|
@@ -1006,7 +1021,8 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
|
|
|
1006
1021
|
] })
|
|
1007
1022
|
] }, m.id)
|
|
1008
1023
|
),
|
|
1009
|
-
ownerTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { label: tr("typing") })
|
|
1024
|
+
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") })
|
|
1010
1026
|
] }),
|
|
1011
1027
|
!canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-closed", children: [
|
|
1012
1028
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", role: "status", children: tr("sessionClosed") }),
|
|
@@ -1061,7 +1077,7 @@ function ChatWidget(props) {
|
|
|
1061
1077
|
(0, import_react2.useEffect)(() => {
|
|
1062
1078
|
const list = listRef.current;
|
|
1063
1079
|
if (list !== null) list.scrollTop = list.scrollHeight;
|
|
1064
|
-
}, [state.messages, state.ownerTyping, state.open]);
|
|
1080
|
+
}, [state.messages, state.ownerTyping, state.visitorTyping, state.open]);
|
|
1065
1081
|
const handleClose = (0, import_react2.useCallback)(() => {
|
|
1066
1082
|
closePanel();
|
|
1067
1083
|
bubbleRef.current?.focus();
|
|
@@ -1105,6 +1121,7 @@ function ChatWidget(props) {
|
|
|
1105
1121
|
listRef,
|
|
1106
1122
|
firstFieldRef,
|
|
1107
1123
|
ownerTyping: state.ownerTyping,
|
|
1124
|
+
visitorTyping: state.visitorTyping,
|
|
1108
1125
|
onTyping: notifyTyping,
|
|
1109
1126
|
onSend: sendMessage,
|
|
1110
1127
|
onRetry: retryMessage,
|
package/widget/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/widget/index.ts","../src/widget/chat-widget.tsx","../src/widget/i18n.ts","../src/widget/styles.ts","../src/widget/use-chat.ts"],"sourcesContent":["// src/widget/index.ts — superfície pública do subpath \"@erlancarreira/evolution-chat/widget\".\n//\n// O consumidor (LMS) faz:\n// import { ChatWidget } from \"@erlancarreira/evolution-chat/widget\";\n// import type { ChatWidgetProps } from \"@erlancarreira/evolution-chat/widget\";\n// <ChatWidget endpoint=\"/api/chat\" locale=\"pt\" welcome=\"Oi!\" projectName=\"Aulivra\" realtime={supabaseRealtime} />\n//\n// O CSS é auto-injetado (`injectWidgetStyles` roda no mount do ChatWidget); quem quiser\n// servir por `<link>` importa o subpath \"./widget/styles.css\".\n\nexport { ChatWidget } from \"./chat-widget\";\nexport type { ChatWidgetProps, WidgetLocale } from \"./chat-widget\";\n\nexport { injectWidgetStyles, WIDGET_CSS } from \"./styles\";\n\nexport {\n WIDGET_KEYS,\n dictionaries,\n en,\n es,\n pt,\n t,\n} from \"./i18n\";\nexport type { WidgetDictionary, WidgetKey } from \"./i18n\";\n\nexport {\n isValidPhone,\n maskPhone,\n normalizePhone,\n useChat,\n SESSION_STORAGE_KEY,\n POLL_INTERVAL_MS,\n MIN_PHONE_DIGITS,\n} from \"./use-chat\";\nexport type { ChatPhase, ChatState, SessionInfo, UseChatOptions, UseChatResult } from \"./use-chat\";\n\n// Porta injetada — reexportada para o consumidor não descer ao subpath ./bridge.\nexport type { RealtimeHandle } from \"../bridge/types\";\n","// src/widget/chat-widget.tsx — ChatWidget: balão flutuante + painel de conversa.\n//\n// Sem Tailwind / lib de UI: toda a aparência vem de `injectWidgetStyles()` (classes\n// `.ecw-*`), e o único estilo inline é a CSS var `--ecw-accent` (cor do tema). O estado\n// inteiro vive em `use-chat.ts` (reducer + rede + realtime); aqui só render e foco.\n//\n// Acessibilidade (contrato da spec §2):\n// - balão: <button> com aria-label (i18n), aria-expanded e aria-haspopup=\"dialog\";\n// - painel: role=\"dialog\" aria-label=\"Chat\"; foco no primeiro campo ao abrir;\n// ESC fecha e devolve o foco ao balão; clique fora NÃO fecha (mobile-friendly);\n// - todo input tem <label htmlFor> (useId → múltiplas instâncias sem colisão);\n// - badge de não lidas refletido no nome acessível do balão;\n// - sessão closed/failed: composer desabilitado, aviso em role=\"status\" e botão real\n// \"nova conversa\" (o beco sem saída tem saída acessível por teclado);\n// - prefers-reduced-motion desliga animações (via CSS).\n\nimport {\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type CSSProperties,\n type ChangeEvent,\n type FormEvent,\n type ReactElement,\n type RefObject,\n} from \"react\";\nimport type { RealtimeHandle } from \"../bridge/types\";\nimport type { ChatMessage } from \"../types\";\nimport { t, type WidgetKey, type WidgetLocale } from \"./i18n\";\nimport { injectWidgetStyles } from \"./styles\";\nimport { isValidPhone, maskPhone, useChat } from \"./use-chat\";\n\nexport type { WidgetLocale };\n\nexport interface ChatWidgetProps {\n /** Caminho da rota de chat (GET histórico / POST start+send), ex.: \"/api/chat\". */\n endpoint: string;\n locale: WidgetLocale;\n /** Saudação exibida no pré-chat form. */\n welcome: string;\n projectName: string;\n /** Cor de tema; default \"#25D366\". */\n accentColor?: string;\n /** Porta do widget (DI — Strategy): subscribe/unsubscribe do canal da sessão. */\n realtime: RealtimeHandle;\n /** Override pontual de copy por chave i18n. */\n labels?: Partial<Record<string, string>>;\n}\n\nconst DEFAULT_ACCENT = \"#25D366\";\n\nfunction ChatIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M21 11.5a8.38 8.38 0 0 1-8.5 8.5c-1.5 0-2.9-.36-4.13-1L3 20l1.1-3.85A8.36 8.36 0 0 1 12.5 3 8.38 8.38 0 0 1 21 11.5z\" />\n </svg>\n );\n}\n\nfunction SendIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M22 2 11 13\" />\n <path d=\"M22 2 15 22l-4-9-9-4 20-7z\" />\n </svg>\n );\n}\n\nfunction formatTime(iso: string, locale: WidgetLocale): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return \"\";\n return new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" }).format(date);\n}\n\nfunction StatusMark({ status }: { status: ChatMessage[\"status\"] }): ReactElement {\n // ✓ = aceita pelo servidor (pending), ✓✓ = enviada, ⚠ = falhou.\n // Decorativo por design: o conjunto de chaves i18n é fixo (15) e o estado \"failed\" já\n // é anunciado de forma acessível pelo botão visível `retry` ao lado — um texto sr-only\n // reaproveitando \"send\"/\"sending\" soaria a ruído para quem usa leitor de tela.\n const glyph = status === \"pending\" ? \"✓\" : status === \"sent\" ? \"✓✓\" : \"⚠\";\n return (\n <span className={status === \"failed\" ? \"ecw-status ecw-status--failed\" : \"ecw-status\"} aria-hidden=\"true\">\n {glyph}\n </span>\n );\n}\n\n/** \"3 pontinhos\" de digitação: bolha com três pontos animados + rótulo acessível. */\nfunction TypingIndicator({ label }: { label: string }): ReactElement {\n return (\n <li className=\"ecw-item ecw-item--owner\" 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 /** 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 // 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 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.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 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/** 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 dispatch({ type: \"append\", message: e.message });\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, 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"]}
|
package/widget/index.js
CHANGED
|
@@ -407,6 +407,8 @@ function injectWidgetStyles() {
|
|
|
407
407
|
import { useCallback, useEffect, useReducer, useRef } from "react";
|
|
408
408
|
var SESSION_STORAGE_KEY = "ecw:session";
|
|
409
409
|
var POLL_INTERVAL_MS = 5e3;
|
|
410
|
+
var TYPING_PRE_MESSAGE_MIN_MS = 700;
|
|
411
|
+
var TYPING_PRE_MESSAGE_MAX_MS = 2200;
|
|
410
412
|
var MIN_PHONE_DIGITS = 10;
|
|
411
413
|
var initialState = {
|
|
412
414
|
phase: "idle",
|
|
@@ -622,7 +624,20 @@ function useChat({ endpoint, realtime, typingEndpoint }) {
|
|
|
622
624
|
);
|
|
623
625
|
const handleEvent = useCallback((e) => {
|
|
624
626
|
if (e.type === "message" && e.message !== void 0) {
|
|
625
|
-
|
|
627
|
+
const msg = e.message;
|
|
628
|
+
if (msg.direction === "owner") {
|
|
629
|
+
dispatch({ type: "typing", from: "owner", isTyping: true });
|
|
630
|
+
const text = typeof msg.body === "string" ? msg.body : "";
|
|
631
|
+
const delay = Math.min(
|
|
632
|
+
TYPING_PRE_MESSAGE_MAX_MS,
|
|
633
|
+
Math.max(TYPING_PRE_MESSAGE_MIN_MS, text.length * 22)
|
|
634
|
+
);
|
|
635
|
+
setTimeout(() => {
|
|
636
|
+
dispatch({ type: "append", message: msg });
|
|
637
|
+
}, delay);
|
|
638
|
+
} else {
|
|
639
|
+
dispatch({ type: "append", message: msg });
|
|
640
|
+
}
|
|
626
641
|
} else if (e.type === "session" && e.status !== void 0) {
|
|
627
642
|
dispatch({ type: "session-status", status: e.status });
|
|
628
643
|
} else if (e.type === "typing" && e.from !== void 0) {
|
|
@@ -819,8 +834,8 @@ function StatusMark({ status }) {
|
|
|
819
834
|
const glyph = status === "pending" ? "\u2713" : status === "sent" ? "\u2713\u2713" : "\u26A0";
|
|
820
835
|
return /* @__PURE__ */ jsx("span", { className: status === "failed" ? "ecw-status ecw-status--failed" : "ecw-status", "aria-hidden": "true", children: glyph });
|
|
821
836
|
}
|
|
822
|
-
function TypingIndicator({ label }) {
|
|
823
|
-
return /* @__PURE__ */ jsx("li", { className:
|
|
837
|
+
function TypingIndicator({ from, label }) {
|
|
838
|
+
return /* @__PURE__ */ jsx("li", { className: `ecw-item ecw-item--${from}`, "aria-live": "polite", children: /* @__PURE__ */ jsxs("div", { className: "ecw-typing", role: "status", "aria-label": label, children: [
|
|
824
839
|
/* @__PURE__ */ jsx("span", { className: "ecw-typing-dot" }),
|
|
825
840
|
/* @__PURE__ */ jsx("span", { className: "ecw-typing-dot" }),
|
|
826
841
|
/* @__PURE__ */ jsx("span", { className: "ecw-typing-dot" }),
|
|
@@ -926,7 +941,7 @@ function PreChatForm({ welcome, tr, error, sending, firstFieldRef, onSubmit }) {
|
|
|
926
941
|
/* @__PURE__ */ jsx("p", { className: "ecw-notice", children: tr("privacyNote") })
|
|
927
942
|
] });
|
|
928
943
|
}
|
|
929
|
-
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, onTyping, onSend, onRetry, onNewConversation }) {
|
|
944
|
+
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, visitorTyping, onTyping, onSend, onRetry, onNewConversation }) {
|
|
930
945
|
const uid = useId();
|
|
931
946
|
const [draft, setDraft] = useState("");
|
|
932
947
|
const typingTimer = useRef2(null);
|
|
@@ -971,7 +986,8 @@ function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFi
|
|
|
971
986
|
] })
|
|
972
987
|
] }, m.id)
|
|
973
988
|
),
|
|
974
|
-
ownerTyping && canCompose && /* @__PURE__ */ jsx(TypingIndicator, { label: tr("typing") })
|
|
989
|
+
ownerTyping && canCompose && /* @__PURE__ */ jsx(TypingIndicator, { from: "owner", label: tr("typing") }),
|
|
990
|
+
visitorTyping && canCompose && /* @__PURE__ */ jsx(TypingIndicator, { from: "visitor", label: tr("typing") })
|
|
975
991
|
] }),
|
|
976
992
|
!canCompose && /* @__PURE__ */ jsxs("div", { className: "ecw-closed", children: [
|
|
977
993
|
/* @__PURE__ */ jsx("p", { className: "ecw-notice", role: "status", children: tr("sessionClosed") }),
|
|
@@ -1026,7 +1042,7 @@ function ChatWidget(props) {
|
|
|
1026
1042
|
useEffect2(() => {
|
|
1027
1043
|
const list = listRef.current;
|
|
1028
1044
|
if (list !== null) list.scrollTop = list.scrollHeight;
|
|
1029
|
-
}, [state.messages, state.ownerTyping, state.open]);
|
|
1045
|
+
}, [state.messages, state.ownerTyping, state.visitorTyping, state.open]);
|
|
1030
1046
|
const handleClose = useCallback2(() => {
|
|
1031
1047
|
closePanel();
|
|
1032
1048
|
bubbleRef.current?.focus();
|
|
@@ -1070,6 +1086,7 @@ function ChatWidget(props) {
|
|
|
1070
1086
|
listRef,
|
|
1071
1087
|
firstFieldRef,
|
|
1072
1088
|
ownerTyping: state.ownerTyping,
|
|
1089
|
+
visitorTyping: state.visitorTyping,
|
|
1073
1090
|
onTyping: notifyTyping,
|
|
1074
1091
|
onSend: sendMessage,
|
|
1075
1092
|
onRetry: retryMessage,
|
package/widget/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/widget/chat-widget.tsx","../src/widget/i18n.ts","../src/widget/styles.ts","../src/widget/use-chat.ts"],"sourcesContent":["// src/widget/chat-widget.tsx — ChatWidget: balão flutuante + painel de conversa.\n//\n// Sem Tailwind / lib de UI: toda a aparência vem de `injectWidgetStyles()` (classes\n// `.ecw-*`), e o único estilo inline é a CSS var `--ecw-accent` (cor do tema). O estado\n// inteiro vive em `use-chat.ts` (reducer + rede + realtime); aqui só render e foco.\n//\n// Acessibilidade (contrato da spec §2):\n// - balão: <button> com aria-label (i18n), aria-expanded e aria-haspopup=\"dialog\";\n// - painel: role=\"dialog\" aria-label=\"Chat\"; foco no primeiro campo ao abrir;\n// ESC fecha e devolve o foco ao balão; clique fora NÃO fecha (mobile-friendly);\n// - todo input tem <label htmlFor> (useId → múltiplas instâncias sem colisão);\n// - badge de não lidas refletido no nome acessível do balão;\n// - sessão closed/failed: composer desabilitado, aviso em role=\"status\" e botão real\n// \"nova conversa\" (o beco sem saída tem saída acessível por teclado);\n// - prefers-reduced-motion desliga animações (via CSS).\n\nimport {\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type CSSProperties,\n type ChangeEvent,\n type FormEvent,\n type ReactElement,\n type RefObject,\n} from \"react\";\nimport type { RealtimeHandle } from \"../bridge/types\";\nimport type { ChatMessage } from \"../types\";\nimport { t, type WidgetKey, type WidgetLocale } from \"./i18n\";\nimport { injectWidgetStyles } from \"./styles\";\nimport { isValidPhone, maskPhone, useChat } from \"./use-chat\";\n\nexport type { WidgetLocale };\n\nexport interface ChatWidgetProps {\n /** Caminho da rota de chat (GET histórico / POST start+send), ex.: \"/api/chat\". */\n endpoint: string;\n locale: WidgetLocale;\n /** Saudação exibida no pré-chat form. */\n welcome: string;\n projectName: string;\n /** Cor de tema; default \"#25D366\". */\n accentColor?: string;\n /** Porta do widget (DI — Strategy): subscribe/unsubscribe do canal da sessão. */\n realtime: RealtimeHandle;\n /** Override pontual de copy por chave i18n. */\n labels?: Partial<Record<string, string>>;\n}\n\nconst DEFAULT_ACCENT = \"#25D366\";\n\nfunction ChatIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M21 11.5a8.38 8.38 0 0 1-8.5 8.5c-1.5 0-2.9-.36-4.13-1L3 20l1.1-3.85A8.36 8.36 0 0 1 12.5 3 8.38 8.38 0 0 1 21 11.5z\" />\n </svg>\n );\n}\n\nfunction SendIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M22 2 11 13\" />\n <path d=\"M22 2 15 22l-4-9-9-4 20-7z\" />\n </svg>\n );\n}\n\nfunction formatTime(iso: string, locale: WidgetLocale): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return \"\";\n return new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" }).format(date);\n}\n\nfunction StatusMark({ status }: { status: ChatMessage[\"status\"] }): ReactElement {\n // ✓ = aceita pelo servidor (pending), ✓✓ = enviada, ⚠ = falhou.\n // Decorativo por design: o conjunto de chaves i18n é fixo (15) e o estado \"failed\" já\n // é anunciado de forma acessível pelo botão visível `retry` ao lado — um texto sr-only\n // reaproveitando \"send\"/\"sending\" soaria a ruído para quem usa leitor de tela.\n const glyph = status === \"pending\" ? \"✓\" : status === \"sent\" ? \"✓✓\" : \"⚠\";\n return (\n <span className={status === \"failed\" ? \"ecw-status ecw-status--failed\" : \"ecw-status\"} aria-hidden=\"true\">\n {glyph}\n </span>\n );\n}\n\n/** \"3 pontinhos\" de digitação: bolha com três pontos animados + rótulo acessível. */\nfunction TypingIndicator({ label }: { label: string }): ReactElement {\n return (\n <li className=\"ecw-item ecw-item--owner\" 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 /** 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 // 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 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.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 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/** 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 dispatch({ type: \"append\", message: e.message });\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, 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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2S1B,IAAI,WAAW;AAOR,SAAS,qBAA2B;AACzC,MAAI,OAAO,aAAa,YAAa;AACrC,MAAI,YAAY,SAAS,eAAe,YAAY,MAAM,KAAM;AAChE,QAAM,WAAW,SAAS,eAAe,YAAY;AACrD,MAAI,aAAa,MAAM;AAErB,eAAW;AACX;AAAA,EACF;AACA,QAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,QAAM,KAAK;AACX,QAAM,cAAc;AACpB,WAAS,KAAK,YAAY,KAAK;AAC/B,aAAW;AACb;;;ACzTA,SAAS,aAAa,WAAW,YAAY,cAAc;AAKpD,IAAM,sBAAsB;AAE5B,IAAM,mBAAmB;AAEzB,IAAM,4BAA4B;AAClC,IAAM,4BAA4B;AAElC,IAAM,mBAAmB;AAyChC,IAAM,eAA0B;AAAA,EAC9B,OAAO;AAAA,EACP,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,UAAU,CAAC;AAAA,EACX,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,OAAO;AAAA,EACP,aAAa;AAAA,EACb,eAAe;AACjB;AAEA,SAAS,YAAY,OAAkB,SAAiC;AACtE,QAAM,QAAQ,MAAM,SAAS,UAAU,CAAC,MAAM,EAAE,OAAO,QAAQ,EAAE;AACjE,MAAI;AACJ,MAAI,SAAS,GAAG;AACd,eAAW,CAAC,GAAG,MAAM,QAAQ;AAC7B,aAAS,KAAK,IAAI;AAAA,EACpB,OAAO;AACL,eAAW,CAAC,GAAG,MAAM,UAAU,OAAO;AAAA,EACxC;AACA,QAAM,OAAO,MAAM,QAAQ,QAAQ,cAAc,UAAU,IAAI;AAC/D,QAAM,OAAkB,EAAE,GAAG,OAAO,UAAU,QAAQ,MAAM,SAAS,KAAK;AAE1E,MAAI,QAAQ,cAAc,QAAS,MAAK,cAAc;AACtD,SAAO;AACT;AAEO,SAAS,YAAY,OAAkB,QAA2B;AACvE,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK;AACH,aAAO;AAAA,QACL,GAAG;AAAA,QACH,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,OAAO,MAAM,UAAU,SAAS,SAAS,MAAM;AAAA,MACjD;AAAA,IACF,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,MAAM,MAAM;AAAA,IACjC,KAAK;AACH,aAAO;AAAA,QACL,GAAG;AAAA,QACH,OAAO;AAAA,QACP,SAAS,OAAO;AAAA,QAChB,OAAO,OAAO;AAAA,QACd,UAAU,OAAO;AAAA,MACnB;AAAA,IACF,KAAK,SAAS;AAGZ,UAAI,OAAkB;AAAA,QACpB,GAAG;AAAA,QACH,OAAO;AAAA,QACP,SAAS,OAAO;AAAA,QAChB,OAAO,OAAO;AAAA,QACd,UAAU,OAAO;AAAA,MACnB;AACA,iBAAW,WAAW,MAAM,UAAU;AACpC,cAAM,YAAY,OAAO,SAAS;AAAA,UAChC,CAAC,MAAM,EAAE,cAAc,aAAa,EAAE,SAAS,QAAQ;AAAA,QACzD;AACA,YAAI,QAAQ,WAAW,aAAa,CAAC,UAAW,QAAO,YAAY,MAAM,OAAO;AAAA,MAClF;AACA,aAAO;AAAA,IACT;AAAA,IACA,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,OAAO,QAAQ,SAAS,MAAM,OAAO,MAAM,UAAU,CAAC,GAAG,QAAQ,GAAG,OAAO,KAAK;AAAA,IACrG,KAAK;AACH,aAAO,YAAY,OAAO,OAAO,OAAO;AAAA,IAC1C,KAAK,SAAS;AACZ,UAAI,OAAO;AACX,iBAAW,WAAW,OAAO,SAAU,QAAO,YAAY,MAAM,OAAO;AACvE,aAAO;AAAA,IACT;AAAA,IACA,KAAK,WAAW;AACd,YAAM,WAAW,MAAM,SAAS,IAAI,CAAC,MAAO,EAAE,OAAO,OAAO,QAAQ,OAAO,UAAU,CAAE;AACvF,YAAM,OAAO,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,OAAO,QAAQ,EAAE;AAC5D,aAAO,EAAE,GAAG,OAAO,UAAU,OAAO,WAAW,CAAC,GAAG,UAAU,OAAO,OAAO,EAAE;AAAA,IAC/E;AAAA,IACA,KAAK;AACH,aAAO;AAAA,QACL,GAAG;AAAA,QACH,UAAU,MAAM,SAAS,IAAI,CAAC,MAAO,EAAE,OAAO,OAAO,KAAK,EAAE,GAAG,GAAG,QAAQ,OAAO,OAAO,IAAI,CAAE;AAAA,MAChG;AAAA,IACF,KAAK,kBAAkB;AACrB,UAAI,MAAM,YAAY,KAAM,QAAO;AACnC,aAAO,EAAE,GAAG,OAAO,SAAS,EAAE,GAAG,MAAM,SAAS,QAAQ,OAAO,OAAO,EAAE;AAAA,IAC1E;AAAA,IACA,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,SAAS,OAAO,MAAM;AAAA,IAC3C,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,OAAO,OAAO,MAAM;AAAA,IACzC,KAAK;AAEH,aAAO,OAAO,SAAS,UACnB,MAAM,gBAAgB,OAAO,WAC3B,QACA,EAAE,GAAG,OAAO,aAAa,OAAO,SAAS,IAC3C,MAAM,kBAAkB,OAAO,WAC7B,QACA,EAAE,GAAG,OAAO,eAAe,OAAO,SAAS;AAAA,EACrD;AACF;AAKO,SAAS,eAAe,OAAuB;AACpD,SAAO,MAAM,QAAQ,OAAO,EAAE;AAChC;AAGO,SAAS,UAAU,OAAuB;AAC/C,QAAM,SAAS,eAAe,KAAK,EAAE,MAAM,GAAG,EAAE;AAChD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,QAAM,MAAM,OAAO,MAAM,GAAG,CAAC;AAC7B,MAAI,OAAO,UAAU,EAAG,QAAO,IAAI,GAAG;AACtC,QAAM,OAAO,OAAO,MAAM,CAAC;AAC3B,MAAI,KAAK,UAAU,EAAG,QAAO,IAAI,GAAG,KAAK,IAAI;AAC7C,SAAO,IAAI,GAAG,KAAK,KAAK,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;AACtD;AAEO,SAAS,aAAa,OAAwB;AACnD,SAAO,eAAe,KAAK,EAAE,UAAU;AACzC;AASA,SAAS,oBAA0C;AACjD,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,UAAM,MAAM,OAAO,aAAa,QAAQ,mBAAmB;AAC3D,QAAI,QAAQ,KAAM,QAAO;AACzB,UAAM,SAAkB,KAAK,MAAM,GAAG;AACtC,QACE,WAAW,QACX,OAAO,WAAW,YAClB,OAAQ,OAAyB,UAAU,YAC1C,OAAyB,UAAU,IACpC;AACA,YAAM,EAAE,OAAO,KAAK,IAAI;AACxB,aAAO,EAAE,OAAO,MAAM,OAAO,SAAS,WAAW,OAAO,GAAG;AAAA,IAC7D;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,mBAAmB,SAA8B;AACxD,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,WAAO,aAAa,QAAQ,qBAAqB,KAAK,UAAU,OAAO,CAAC;AAAA,EAC1E,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,qBAA2B;AAClC,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,WAAO,aAAa,WAAW,mBAAmB;AAAA,EACpD,QAAQ;AAAA,EAER;AACF;AAkBA,SAAS,WAAW,UAAkB,OAAe,OAA8B;AACjF,QAAM,SAAS,IAAI,gBAAgB,EAAE,MAAM,CAAC;AAC5C,MAAI,UAAU,KAAM,QAAO,IAAI,SAAS,KAAK;AAC7C,QAAM,MAAM,SAAS,SAAS,GAAG,IAAI,MAAM;AAC3C,SAAO,GAAG,QAAQ,GAAG,GAAG,GAAG,OAAO,SAAS,CAAC;AAC9C;AAEA,SAAS,WAAW,UAAwC;AAC1D,MAAI,SAAwB;AAC5B,aAAW,KAAK,SAAU,KAAI,WAAW,QAAQ,EAAE,YAAY,OAAQ,UAAS,EAAE;AAClF,SAAO;AACT;AAyBA,IAAI,SAAS;AACb,SAAS,WAAW,MAA2B;AAC7C,YAAU;AACV,SAAO;AAAA,IACL,IAAI,OAAO,MAAM;AAAA,IACjB,WAAW;AAAA,IACX,WAAW;AAAA,IACX;AAAA,IACA,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,EACpC;AACF;AAEO,SAAS,QAAQ,EAAE,UAAU,UAAU,eAAe,GAAkC;AAC7F,QAAM,CAAC,OAAO,QAAQ,IAAI,WAAW,aAAa,YAAY;AAE9D,QAAM,YAAY,kBAAkB,GAAG,SAAS,QAAQ,QAAQ,EAAE,CAAC;AAGnE,QAAM,WAAW,OAAO,KAAK;AAC7B,YAAU,MAAM;AACd,aAAS,UAAU;AAAA,EACrB,GAAG,CAAC,KAAK,CAAC;AAIV,QAAM,cAAc,OAAO,QAAQ;AACnC,YAAU,MAAM;AACd,gBAAY,UAAU;AAAA,EACxB,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,UAAU,OAA8C,IAAI;AAElE,QAAM,WAAW,YAAY,MAAY;AACvC,QAAI,QAAQ,YAAY,MAAM;AAC5B,oBAAc,QAAQ,OAAO;AAC7B,cAAQ,UAAU;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,WAAW,YAAY,YAA2B;AACtD,UAAM,EAAE,OAAO,SAAS,IAAI,SAAS;AACrC,QAAI,UAAU,KAAM;AACpB,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,WAAW,UAAU,OAAO,WAAW,QAAQ,CAAC,GAAG;AAAA,QACzE,QAAQ;AAAA,QACR,SAAS,EAAE,QAAQ,mBAAmB;AAAA,MACxC,CAAC;AACD,UAAI,IAAI,WAAW,KAAK;AACtB,2BAAmB;AACnB,iBAAS;AACT,iBAAS,EAAE,MAAM,aAAa,CAAC;AAC/B;AAAA,MACF;AACA,UAAI,CAAC,IAAI,GAAI;AACb,YAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,UAAI,MAAM,QAAQ,KAAK,QAAQ,EAAG,UAAS,EAAE,MAAM,SAAS,UAAU,KAAK,SAAS,CAAC;AACrF,UAAI,KAAK,YAAY,OAAW,UAAS,EAAE,MAAM,kBAAkB,QAAQ,KAAK,QAAQ,OAAO,CAAC;AAAA,IAClG,QAAQ;AAAA,IAER;AAAA,EACF,GAAG,CAAC,UAAU,QAAQ,CAAC;AAEvB,QAAM,YAAY,YAAY,MAAY;AACxC,QAAI,QAAQ,YAAY,KAAM;AAC9B,YAAQ,UAAU,YAAY,MAAM;AAClC,WAAK,SAAS;AAAA,IAChB,GAAG,gBAAgB;AAAA,EACrB,GAAG,CAAC,QAAQ,CAAC;AAGb,QAAM,eAAe;AAAA,IACnB,CAAC,MAA+B;AAC9B,UAAI,MAAM,SAAU,WAAU;AAAA,UACzB,UAAS;AAAA,IAChB;AAAA,IACA,CAAC,WAAW,QAAQ;AAAA,EACtB;AAEA,QAAM,cAAc,YAAY,CAAC,MAAuB;AACtD,QAAI,EAAE,SAAS,aAAa,EAAE,YAAY,QAAW;AACnD,YAAM,MAAM,EAAE;AAId,UAAI,IAAI,cAAc,SAAS;AAC7B,iBAAS,EAAE,MAAM,UAAU,MAAM,SAAS,UAAU,KAAK,CAAC;AAC1D,cAAM,OAAO,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AACvD,cAAM,QAAQ,KAAK;AAAA,UACjB;AAAA,UACA,KAAK,IAAI,2BAA2B,KAAK,SAAS,EAAE;AAAA,QACtD;AACA,mBAAW,MAAM;AACf,mBAAS,EAAE,MAAM,UAAU,SAAS,IAAI,CAAC;AAAA,QAC3C,GAAG,KAAK;AAAA,MACV,OAAO;AACL,iBAAS,EAAE,MAAM,UAAU,SAAS,IAAI,CAAC;AAAA,MAC3C;AAAA,IACF,WAAW,EAAE,SAAS,aAAa,EAAE,WAAW,QAAW;AACzD,eAAS,EAAE,MAAM,kBAAkB,QAAQ,EAAE,OAAO,CAAC;AAAA,IACvD,WAAW,EAAE,SAAS,YAAY,EAAE,SAAS,QAAW;AACtD,eAAS,EAAE,MAAM,UAAU,MAAM,EAAE,MAAM,UAAU,EAAE,SAAS,CAAC;AAAA,IACjE;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,YAAU,MAAM;AACd,UAAM,SAAS,kBAAkB;AACjC,QAAI,WAAW,KAAM;AACrB,QAAI,YAAY;AAChB,UAAM,YAAY;AAChB,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,WAAW,UAAU,OAAO,OAAO,IAAI,GAAG;AAAA,UAChE,QAAQ;AAAA,UACR,SAAS,EAAE,QAAQ,mBAAmB;AAAA,QACxC,CAAC;AACD,YAAI,UAAW;AACf,YAAI,IAAI,WAAW,KAAK;AACtB,6BAAmB;AACnB,mBAAS,EAAE,MAAM,aAAa,CAAC;AAC/B;AAAA,QACF;AACA,YAAI,CAAC,IAAI,GAAI;AACb,cAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,YAAI,aAAa,KAAK,YAAY,OAAW;AAC7C,iBAAS;AAAA,UACP,MAAM;AAAA,UACN,SAAS,KAAK;AAAA,UACd,OAAO,OAAO;AAAA,UACd,UAAU,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC;AAAA,QAC5D,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF,GAAG;AACH,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAGb,YAAU,MAAM;AACd,UAAM,QAAQ,MAAM;AACpB,QAAI,UAAU,KAAM;AACpB,UAAM,cAAc,YAAY,QAAQ,UAAU,OAAO,aAAa,YAAY;AAClF,WAAO,MAAM;AACX,kBAAY;AACZ,eAAS;AAAA,IACX;AAAA,EACF,GAAG,CAAC,MAAM,OAAO,aAAa,cAAc,QAAQ,CAAC;AAErD,YAAU,MAAM,UAAU,CAAC,QAAQ,CAAC;AAEpC,QAAM,YAAY,YAAY,MAAY;AACxC,aAAS,EAAE,MAAM,OAAO,CAAC;AAAA,EAC3B,GAAG,CAAC,CAAC;AAEL,QAAM,aAAa,YAAY,MAAY;AACzC,aAAS,EAAE,MAAM,QAAQ,CAAC;AAAA,EAC5B,GAAG,CAAC,CAAC;AAEL,QAAM,cAAc,YAAY,MAAY;AAC1C,QAAI,SAAS,QAAQ,KAAM,UAAS,EAAE,MAAM,QAAQ,CAAC;AAAA,QAChD,UAAS,EAAE,MAAM,OAAO,CAAC;AAAA,EAChC,GAAG,CAAC,CAAC;AAEL,QAAM,cAAc;AAAA,IAClB,OAAO,OAAe,MAAc,OAA8B;AAChE,eAAS,EAAE,MAAM,WAAW,OAAO,KAAK,CAAC;AACzC,eAAS,EAAE,MAAM,SAAS,OAAO,KAAK,CAAC;AACvC,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,UAAU;AAAA,UAChC,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,EAAE,OAAO,SAAS,KAAK,CAAC;AAAA,QAC/C,CAAC;AACD,YAAI,CAAC,IAAI,IAAI;AACX,mBAAS,EAAE,MAAM,QAAQ,IAAI,QAAQ,SAAS,CAAC;AAC/C,mBAAS,EAAE,MAAM,SAAS,OAAO,YAAY,CAAC;AAC9C;AAAA,QACF;AACA,cAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,YAAI,KAAK,YAAY,OAAW,UAAS,EAAE,MAAM,WAAW,OAAO,IAAI,SAAS,KAAK,QAAQ,CAAC;AAAA,YACzF,UAAS,EAAE,MAAM,QAAQ,IAAI,QAAQ,SAAS,CAAC;AAAA,MACtD,QAAQ;AACN,iBAAS,EAAE,MAAM,QAAQ,IAAI,QAAQ,SAAS,CAAC;AAC/C,iBAAS,EAAE,MAAM,SAAS,OAAO,YAAY,CAAC;AAAA,MAChD,UAAE;AACA,iBAAS,EAAE,MAAM,WAAW,OAAO,MAAM,CAAC;AAAA,MAC5C;AAAA,IACF;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,cAAc;AAAA,IAClB,OAAO,SAAgC;AACrC,YAAM,OAAO,KAAK,KAAK;AACvB,YAAM,QAAQ,SAAS,QAAQ;AAC/B,UAAI,SAAS,MAAM,UAAU,KAAM;AACnC,YAAM,aAAa,WAAW,IAAI;AAClC,eAAS,EAAE,MAAM,UAAU,SAAS,WAAW,CAAC;AAChD,YAAM,YAAY,OAAO,MAAM,WAAW,EAAE;AAAA,IAC9C;AAAA,IACA,CAAC,WAAW;AAAA,EACd;AAEA,QAAM,eAAe;AAAA,IACnB,OAAO,OAA8B;AACnC,YAAM,QAAQ,SAAS,QAAQ;AAC/B,YAAM,SAAS,SAAS,QAAQ,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE;AAChE,UAAI,UAAU,QAAQ,WAAW,UAAa,OAAO,cAAc,UAAW;AAC9E,eAAS,EAAE,MAAM,QAAQ,IAAI,QAAQ,UAAU,CAAC;AAChD,YAAM,YAAY,OAAO,OAAO,MAAM,EAAE;AAAA,IAC1C;AAAA,IACA,CAAC,WAAW;AAAA,EACd;AAKA,QAAM,uBAAuB,YAAY,MAAY;AACnD,uBAAmB;AACnB,aAAS,EAAE,MAAM,aAAa,CAAC;AAAA,EACjC,GAAG,CAAC,CAAC;AAIL,QAAM,eAAe;AAAA,IACnB,OAAO,aAAqC;AAC1C,YAAM,QAAQ,SAAS,QAAQ;AAC/B,UAAI,UAAU,KAAM;AACpB,UAAI;AACF,cAAM,MAAM,WAAW;AAAA,UACrB,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,EAAE,OAAO,SAAS,CAAC;AAAA,QAC1C,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IACA,CAAC,SAAS;AAAA,EACZ;AAEA,QAAM,aAAa;AAAA,IACjB,OAAO,EAAE,MAAM,OAAO,SAAS,SAAS,MAAyF;AAG/H,UAAI,SAAS,KAAK,MAAM,GAAI;AAC5B,YAAM,SAAS,eAAe,KAAK;AACnC,UAAI,KAAK,KAAK,MAAM,MAAM,QAAQ,KAAK,MAAM,MAAM,OAAO,SAAS,kBAAkB;AACnF,iBAAS,EAAE,MAAM,SAAS,OAAO,OAAO,SAAS,mBAAmB,iBAAiB,YAAY,CAAC;AAClG;AAAA,MACF;AACA,YAAM,aAAa,WAAW,QAAQ,KAAK,CAAC;AAC5C,eAAS,EAAE,MAAM,UAAU,SAAS,WAAW,CAAC;AAChD,eAAS,EAAE,MAAM,WAAW,OAAO,KAAK,CAAC;AACzC,eAAS,EAAE,MAAM,SAAS,OAAO,KAAK,CAAC;AACvC,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,UAAU;AAAA,UAChC,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,EAAE,MAAM,KAAK,KAAK,GAAG,OAAO,QAAQ,SAAS,QAAQ,KAAK,GAAG,UAAU,GAAG,CAAC;AAAA,QAClG,CAAC;AACD,YAAI,CAAC,IAAI,IAAI;AACX,mBAAS,EAAE,MAAM,QAAQ,IAAI,WAAW,IAAI,QAAQ,SAAS,CAAC;AAC9D,mBAAS,EAAE,MAAM,SAAS,OAAO,YAAY,CAAC;AAC9C;AAAA,QACF;AACA,cAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,cAAM,UAAU,KAAK;AACrB,YAAI,YAAY,UAAa,OAAO,QAAQ,kBAAkB,YAAY,QAAQ,kBAAkB,IAAI;AACtG,mBAAS,EAAE,MAAM,QAAQ,IAAI,WAAW,IAAI,QAAQ,SAAS,CAAC;AAC9D,mBAAS,EAAE,MAAM,SAAS,OAAO,YAAY,CAAC;AAC9C;AAAA,QACF;AACA,cAAM,QAAQ,QAAQ;AACtB,2BAAmB,EAAE,OAAO,MAAM,QAAQ,KAAK,CAAC;AAChD,iBAAS;AAAA,UACP,MAAM;AAAA,UACN,SAAS,EAAE,MAAM,QAAQ,MAAM,QAAQ,QAAQ,QAAQ,aAAa,QAAQ,YAAY;AAAA,UACxF;AAAA,UACA,UAAU,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC;AAAA,QAC5D,CAAC;AAAA,MACH,QAAQ;AACN,iBAAS,EAAE,MAAM,QAAQ,IAAI,WAAW,IAAI,QAAQ,SAAS,CAAC;AAC9D,iBAAS,EAAE,MAAM,SAAS,OAAO,YAAY,CAAC;AAAA,MAChD,UAAE;AACA,iBAAS,EAAE,MAAM,WAAW,OAAO,MAAM,CAAC;AAAA,MAC5C;AAAA,IACF;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AAEA,SAAO,EAAE,OAAO,WAAW,YAAY,aAAa,YAAY,aAAa,cAAc,sBAAsB,aAAa;AAChI;;;AHrhBM,SAoNF,UApNE,KAOF,YAPE;AALN,IAAM,iBAAiB;AAEvB,SAAS,WAAyB;AAChC,SACE,oBAAC,SAAI,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAAO,WAAU,SACnJ,8BAAC,UAAK,GAAE,wHAAuH,GACjI;AAEJ;AAEA,SAAS,WAAyB;AAChC,SACE,qBAAC,SAAI,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAAO,WAAU,SACnJ;AAAA,wBAAC,UAAK,GAAE,eAAc;AAAA,IACtB,oBAAC,UAAK,GAAE,8BAA6B;AAAA,KACvC;AAEJ;AAEA,SAAS,WAAW,KAAa,QAA8B;AAC7D,QAAM,OAAO,IAAI,KAAK,GAAG;AACzB,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO;AACzC,SAAO,IAAI,KAAK,eAAe,QAAQ,EAAE,MAAM,WAAW,QAAQ,UAAU,CAAC,EAAE,OAAO,IAAI;AAC5F;AAEA,SAAS,WAAW,EAAE,OAAO,GAAoD;AAK/E,QAAM,QAAQ,WAAW,YAAY,WAAM,WAAW,SAAS,iBAAO;AACtE,SACE,oBAAC,UAAK,WAAW,WAAW,WAAW,kCAAkC,cAAc,eAAY,QAChG,iBACH;AAEJ;AAIA,SAAS,gBAAgB,EAAE,MAAM,MAAM,GAA+D;AACpG,SACE,oBAAC,QAAG,WAAW,sBAAsB,IAAI,IAAI,aAAU,UACrD,+BAAC,SAAI,WAAU,cAAa,MAAK,UAAS,cAAY,OACpD;AAAA,wBAAC,UAAK,WAAU,kBAAiB;AAAA,IACjC,oBAAC,UAAK,WAAU,kBAAiB;AAAA,IACjC,oBAAC,UAAK,WAAU,kBAAiB;AAAA,IACjC,oBAAC,UAAK,WAAU,eAAe,iBAAM;AAAA,KACvC,GACF;AAEJ;AAWA,SAAS,YAAY,EAAE,SAAS,IAAI,OAAO,SAAS,eAAe,SAAS,GAA4B;AACtG,QAAM,MAAM,MAAM;AAClB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,EAAE;AACnC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,EAAE;AACzC,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,EAAE;AAC3C,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AAEtD,QAAM,aAAa,aAAa,KAAK;AACrC,QAAM,WAAW,KAAK,KAAK,MAAM,MAAM,cAAc,QAAQ,KAAK,MAAM;AAExE,QAAM,eAAe,CAAC,MAAuB;AAC3C,MAAE,eAAe;AACjB,QAAI,CAAC,YAAY,QAAS;AAC1B,SAAK,SAAS,EAAE,MAAM,OAAO,SAAS,SAAS,CAAC;AAAA,EAClD;AAEA,SACE,qBAAC,UAAK,WAAU,YAAW,UAAU,cAAc,YAAU,MAC3D;AAAA,wBAAC,OAAE,WAAU,eAAe,mBAAQ;AAAA,IACpC,oBAAC,OAAE,WAAU,cAAc,aAAG,eAAe,GAAE;AAAA,IAE/C,qBAAC,SAAI,WAAU,aACb;AAAA,0BAAC,WAAM,WAAU,aAAY,SAAS,GAAG,GAAG,SAAU,aAAG,MAAM,GAAE;AAAA,MACjE;AAAA,QAAC;AAAA;AAAA,UACC,IAAI,GAAG,GAAG;AAAA,UACV,KAAK;AAAA,UACL,WAAU;AAAA,UACV,MAAK;AAAA,UACL,cAAa;AAAA,UACb,OAAO;AAAA,UACP,UAAU,CAAC,MAAM;AAAE,oBAAQ,EAAE,OAAO,KAAK;AAAA,UAAG;AAAA,UAC5C,UAAQ;AAAA;AAAA,MACV;AAAA,OACF;AAAA,IAEA,qBAAC,SAAI,WAAU,aACb;AAAA,0BAAC,WAAM,WAAU,aAAY,SAAS,GAAG,GAAG,UAAW,aAAG,OAAO,GAAE;AAAA,MACnE;AAAA,QAAC;AAAA;AAAA,UACC,IAAI,GAAG,GAAG;AAAA,UACV,WAAU;AAAA,UACV,MAAK;AAAA,UACL,WAAU;AAAA,UACV,cAAa;AAAA,UACb,aAAY;AAAA,UACZ,OAAO;AAAA,UACP,UAAU,CAAC,MAAM;AAAE,qBAAS,UAAU,EAAE,OAAO,KAAK,CAAC;AAAA,UAAG;AAAA,UACxD,QAAQ,MAAM;AAAE,4BAAgB,IAAI;AAAA,UAAG;AAAA,UACvC,gBAAc,gBAAgB,CAAC,aAAa,OAAO;AAAA,UACnD,oBAAkB,gBAAgB,CAAC,aAAa,GAAG,GAAG,eAAe;AAAA,UACrE,UAAQ;AAAA;AAAA,MACV;AAAA,MACC,gBAAgB,CAAC,cAChB,oBAAC,OAAE,WAAU,aAAY,IAAI,GAAG,GAAG,cAAc,MAAK,SAAS,aAAG,cAAc,GAAE;AAAA,OAEtF;AAAA,IAEA,qBAAC,SAAI,WAAU,aACb;AAAA,0BAAC,WAAM,WAAU,aAAY,SAAS,GAAG,GAAG,YAAa,aAAG,SAAS,GAAE;AAAA,MACvE;AAAA,QAAC;AAAA;AAAA,UACC,IAAI,GAAG,GAAG;AAAA,UACV,WAAU;AAAA,UACV,MAAM;AAAA,UACN,cAAa;AAAA,UACb,OAAO;AAAA,UACP,UAAU,CAAC,MAAM;AAAE,uBAAW,EAAE,OAAO,KAAK;AAAA,UAAG;AAAA,UAC/C,UAAQ;AAAA;AAAA,MACV;AAAA,OACF;AAAA,IAIA,qBAAC,SAAI,WAAU,UAAS,eAAY,QAClC;AAAA,0BAAC,WAAM,SAAS,GAAG,GAAG,YAAY,qBAAO;AAAA,MACzC;AAAA,QAAC;AAAA;AAAA,UACC,IAAI,GAAG,GAAG;AAAA,UACV,MAAK;AAAA,UACL,MAAK;AAAA,UACL,UAAU;AAAA,UACV,cAAa;AAAA,UACb,OAAO;AAAA,UACP,UAAU,CAAC,MAAM;AAAE,wBAAY,EAAE,OAAO,KAAK;AAAA,UAAG;AAAA;AAAA,MAClD;AAAA,OACF;AAAA,IAEC,UAAU,eACT,oBAAC,OAAE,WAAU,aAAY,MAAK,SAAS,aAAG,WAAW,GAAE;AAAA,IAGzD,oBAAC,YAAO,WAAU,cAAa,MAAK,UAAS,UAAU,CAAC,YAAY,SACjE,oBAAU,GAAG,SAAS,IAAI,GAAG,MAAM,GACtC;AAAA,IACA,oBAAC,OAAE,WAAU,cAAc,aAAG,aAAa,GAAE;AAAA,KAC/C;AAEJ;AAsBA,SAAS,UAAU,EAAE,UAAU,YAAY,SAAS,QAAQ,IAAI,SAAS,eAAe,aAAa,eAAe,UAAU,QAAQ,SAAS,kBAAkB,GAAiC;AAChM,QAAM,MAAM,MAAM;AAClB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AAIrC,QAAM,cAAcC,QAA6C,IAAI;AACrE,EAAAC,WAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,YAAY,YAAY,KAAM,cAAa,YAAY,OAAO;AAAA,IACpE;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,oBAAoB,CAAC,MAA2C;AACpE,aAAS,EAAE,OAAO,KAAK;AACvB,QAAI,CAAC,WAAY;AACjB,SAAK,SAAS,IAAI;AAClB,QAAI,YAAY,YAAY,KAAM,cAAa,YAAY,OAAO;AAClE,gBAAY,UAAU,WAAW,MAAM;AACrC,WAAK,SAAS,KAAK;AACnB,kBAAY,UAAU;AAAA,IACxB,GAAG,IAAI;AAAA,EACT;AAEA,QAAM,SAAS,CAAC,MAAuB;AACrC,MAAE,eAAe;AACjB,UAAM,OAAO,MAAM,KAAK;AACxB,QAAI,SAAS,MAAM,CAAC,WAAY;AAChC,QAAI,YAAY,YAAY,MAAM;AAChC,mBAAa,YAAY,OAAO;AAChC,kBAAY,UAAU;AAAA,IACxB;AACA,SAAK,SAAS,KAAK;AACnB,aAAS,EAAE;AACX,SAAK,OAAO,IAAI;AAAA,EAClB;AAEA,SACE,iCACE;AAAA,yBAAC,QAAG,KAAK,SAAS,WAAU,YACzB;AAAA,eAAS;AAAA,QAAI,CAAC,MACb,EAAE,cAAc,WACd,oBAAC,QAAc,WAAU,6BAA4B,MAAK,UACxD,8BAAC,SAAI,WAAU,cAAc,YAAE,MAAK,KAD7B,EAAE,EAEX,IAEA,qBAAC,QAAc,WAAW,sBAAsB,EAAE,SAAS,IACzD;AAAA,8BAAC,SAAI,WAAW,0BAA0B,EAAE,SAAS,IAAK,YAAE,MAAK;AAAA,UACjE,qBAAC,SAAI,WAAU,YACb;AAAA,gCAAC,UAAK,UAAU,EAAE,WAAY,qBAAW,EAAE,WAAW,MAAM,GAAE;AAAA,YAC7D,EAAE,cAAc,aAAa,oBAAC,cAAW,QAAQ,EAAE,QAAQ;AAAA,YAC3D,EAAE,WAAW,YACZ,oBAAC,YAAO,MAAK,UAAS,WAAU,aAAY,SAAS,MAAM;AAAE,mBAAK,QAAQ,EAAE,EAAE;AAAA,YAAG,GAC9E,aAAG,OAAO,GACb;AAAA,aAEJ;AAAA,aAVO,EAAE,EAWX;AAAA,MAEJ;AAAA,MACC,eAAe,cAAc,oBAAC,mBAAgB,MAAK,SAAQ,OAAO,GAAG,QAAQ,GAAG;AAAA,MAChF,iBAAiB,cAAc,oBAAC,mBAAgB,MAAK,WAAU,OAAO,GAAG,QAAQ,GAAG;AAAA,OACvF;AAAA,IACC,CAAC,cACA,qBAAC,SAAI,WAAU,cACb;AAAA,0BAAC,OAAE,WAAU,cAAa,MAAK,UAAU,aAAG,eAAe,GAAE;AAAA,MAC7D,oBAAC,YAAO,MAAK,UAAS,WAAU,cAAa,SAAS,mBACnD,aAAG,iBAAiB,GACvB;AAAA,OACF;AAAA,IAEF,qBAAC,UAAK,WAAU,gBAAe,UAAU,QACvC;AAAA,0BAAC,WAAM,WAAU,eAAc,SAAS,GAAG,GAAG,UAAW,aAAG,SAAS,GAAE;AAAA,MACvE;AAAA,QAAC;AAAA;AAAA,UACC,IAAI,GAAG,GAAG;AAAA,UACV,KAAK;AAAA,UACL,WAAU;AAAA,UACV,MAAK;AAAA,UACL,cAAa;AAAA,UACb,aAAa,GAAG,SAAS;AAAA,UACzB,OAAO;AAAA,UACP,UAAU;AAAA,UACV,UAAU,CAAC;AAAA;AAAA,MACb;AAAA,MACA,oBAAC,YAAO,WAAU,YAAW,MAAK,UAAS,UAAU,CAAC,cAAc,MAAM,KAAK,MAAM,MAAM,SAAS,cAAY,UAAU,GAAG,SAAS,IAAI,GAAG,MAAM,GAChJ,oBAAU,oBAAC,UAAK,WAAU,eAAe,aAAG,SAAS,GAAE,IAAU,oBAAC,YAAS,GAC9E;AAAA,OACF;AAAA,KACF;AAEJ;AAEO,SAAS,WAAW,OAAsC;AAC/D,QAAM,EAAE,UAAU,QAAQ,SAAS,aAAa,UAAU,OAAO,IAAI;AACrE,QAAM,cAAc,MAAM,eAAe;AAEzC,QAAM,KAAKC,aAAY,CAAC,QAA2B,EAAE,QAAQ,KAAK,MAAM,GAAG,CAAC,QAAQ,MAAM,CAAC;AAC3F,QAAM,EAAE,OAAO,YAAY,aAAa,YAAY,aAAa,cAAc,sBAAsB,aAAa,IAChH,QAAQ,EAAE,UAAU,SAAS,CAAC;AAEhC,QAAM,YAAYF,QAA0B,IAAI;AAChD,QAAM,UAAUA,QAAyB,IAAI;AAC7C,QAAM,gBAAgBA,QAAyB,IAAI;AAEnD,EAAAC,WAAU,MAAM;AACd,uBAAmB;AAAA,EACrB,GAAG,CAAC,CAAC;AAGL,EAAAA,WAAU,MAAM;AACd,QAAI,MAAM,KAAM,eAAc,SAAS,MAAM;AAAA,EAC/C,GAAG,CAAC,MAAM,MAAM,MAAM,KAAK,CAAC;AAG5B,EAAAA,WAAU,MAAM;AACd,QAAI,CAAC,MAAM,KAAM;AACjB,UAAM,YAAY,CAAC,MAA2B;AAC5C,UAAI,EAAE,QAAQ,SAAU;AACxB,iBAAW;AACX,gBAAU,SAAS,MAAM;AAAA,IAC3B;AACA,aAAS,iBAAiB,WAAW,SAAS;AAC9C,WAAO,MAAM;AACX,eAAS,oBAAoB,WAAW,SAAS;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,MAAM,MAAM,UAAU,CAAC;AAG3B,EAAAA,WAAU,MAAM;AACd,UAAM,OAAO,QAAQ;AACrB,QAAI,SAAS,KAAM,MAAK,YAAY,KAAK;AAAA,EAC3C,GAAG,CAAC,MAAM,UAAU,MAAM,aAAa,MAAM,eAAe,MAAM,IAAI,CAAC;AAEvE,QAAM,cAAcC,aAAY,MAAY;AAC1C,eAAW;AACX,cAAU,SAAS,MAAM;AAAA,EAC3B,GAAG,CAAC,UAAU,CAAC;AAIf,QAAM,aAAa,MAAM,SAAS,WAAW;AAE7C,QAAM,cAAc,MAAM,SAAS,IAAI,GAAG,GAAG,UAAU,CAAC,KAAK,MAAM,MAAM,MAAM,GAAG,UAAU;AAE5F,SACE,qBAAC,SAAI,WAAU,YAAW,OAAO,EAAE,gBAAgB,YAAY,GAC7D;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,WAAU;AAAA,QACV,cAAY;AAAA,QACZ,iBAAe,MAAM;AAAA,QACrB,iBAAc;AAAA,QACd,SAAS;AAAA,QAET;AAAA,8BAAC,YAAS;AAAA,UACT,MAAM,SAAS,KAAK,oBAAC,UAAK,WAAU,aAAa,gBAAM,QAAO;AAAA;AAAA;AAAA,IACjE;AAAA,IAEC,MAAM,QACL,qBAAC,aAAQ,WAAU,aAAY,MAAK,UAAS,cAAW,QACtD;AAAA,2BAAC,YAAO,WAAU,cAChB;AAAA,4BAAC,UAAK,WAAU,aAAa,uBAAY;AAAA,QACxC,MAAM,YAAY,QAAQ,qBAAC,UAAK,WAAU,YAAW;AAAA;AAAA,UAAE,MAAM,QAAQ;AAAA,WAAK;AAAA,QAC3E,oBAAC,YAAO,MAAK,UAAS,WAAU,aAAY,cAAY,GAAG,OAAO,GAAG,SAAS,aAC5E,8BAAC,UAAK,eAAY,QAAO,kBAAC,GAC5B;AAAA,SACF;AAAA,MAEC,MAAM,UAAU,SACf;AAAA,QAAC;AAAA;AAAA,UACC,UAAU,MAAM;AAAA,UAChB;AAAA,UACA,SAAS,MAAM;AAAA,UACf;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,aAAa,MAAM;AAAA,UACnB,eAAe,MAAM;AAAA,UACrB,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,mBAAmB;AAAA;AAAA,MACrB,IAEA;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,OAAO,MAAM;AAAA,UACb,SAAS,MAAM;AAAA,UACf;AAAA,UACA,UAAU;AAAA;AAAA,MACZ;AAAA,MAGF,qBAAC,YAAO,WAAU,cACf;AAAA,WAAG,WAAW;AAAA,QAAG;AAAA,QAClB;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,MAAK;AAAA,YACL,QAAO;AAAA,YACP,KAAI;AAAA,YACL;AAAA;AAAA,QAED;AAAA,SACF;AAAA,OACF;AAAA,KAEJ;AAEJ;","names":["useCallback","useEffect","useRef","useRef","useEffect","useCallback"]}
|
|
@@ -7673,6 +7673,8 @@ var EvolutionChatEmbed = (() => {
|
|
|
7673
7673
|
var import_react = __toESM(require_react(), 1);
|
|
7674
7674
|
var SESSION_STORAGE_KEY = "ecw:session";
|
|
7675
7675
|
var POLL_INTERVAL_MS = 5e3;
|
|
7676
|
+
var TYPING_PRE_MESSAGE_MIN_MS = 700;
|
|
7677
|
+
var TYPING_PRE_MESSAGE_MAX_MS = 2200;
|
|
7676
7678
|
var MIN_PHONE_DIGITS = 10;
|
|
7677
7679
|
var initialState = {
|
|
7678
7680
|
phase: "idle",
|
|
@@ -7888,7 +7890,20 @@ var EvolutionChatEmbed = (() => {
|
|
|
7888
7890
|
);
|
|
7889
7891
|
const handleEvent = (0, import_react.useCallback)((e) => {
|
|
7890
7892
|
if (e.type === "message" && e.message !== void 0) {
|
|
7891
|
-
|
|
7893
|
+
const msg = e.message;
|
|
7894
|
+
if (msg.direction === "owner") {
|
|
7895
|
+
dispatch({ type: "typing", from: "owner", isTyping: true });
|
|
7896
|
+
const text = typeof msg.body === "string" ? msg.body : "";
|
|
7897
|
+
const delay = Math.min(
|
|
7898
|
+
TYPING_PRE_MESSAGE_MAX_MS,
|
|
7899
|
+
Math.max(TYPING_PRE_MESSAGE_MIN_MS, text.length * 22)
|
|
7900
|
+
);
|
|
7901
|
+
setTimeout(() => {
|
|
7902
|
+
dispatch({ type: "append", message: msg });
|
|
7903
|
+
}, delay);
|
|
7904
|
+
} else {
|
|
7905
|
+
dispatch({ type: "append", message: msg });
|
|
7906
|
+
}
|
|
7892
7907
|
} else if (e.type === "session" && e.status !== void 0) {
|
|
7893
7908
|
dispatch({ type: "session-status", status: e.status });
|
|
7894
7909
|
} else if (e.type === "typing" && e.from !== void 0) {
|
|
@@ -8085,8 +8100,8 @@ var EvolutionChatEmbed = (() => {
|
|
|
8085
8100
|
const glyph = status === "pending" ? "\u2713" : status === "sent" ? "\u2713\u2713" : "\u26A0";
|
|
8086
8101
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: status === "failed" ? "ecw-status ecw-status--failed" : "ecw-status", "aria-hidden": "true", children: glyph });
|
|
8087
8102
|
}
|
|
8088
|
-
function TypingIndicator({ label }) {
|
|
8089
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className:
|
|
8103
|
+
function TypingIndicator({ from, label }) {
|
|
8104
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: `ecw-item ecw-item--${from}`, "aria-live": "polite", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-typing", role: "status", "aria-label": label, children: [
|
|
8090
8105
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-typing-dot" }),
|
|
8091
8106
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-typing-dot" }),
|
|
8092
8107
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ecw-typing-dot" }),
|
|
@@ -8192,7 +8207,7 @@ var EvolutionChatEmbed = (() => {
|
|
|
8192
8207
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", children: tr("privacyNote") })
|
|
8193
8208
|
] });
|
|
8194
8209
|
}
|
|
8195
|
-
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, onTyping, onSend, onRetry, onNewConversation }) {
|
|
8210
|
+
function ChatPanel({ messages, canCompose, sending, locale, tr, listRef, firstFieldRef, ownerTyping, visitorTyping, onTyping, onSend, onRetry, onNewConversation }) {
|
|
8196
8211
|
const uid = (0, import_react2.useId)();
|
|
8197
8212
|
const [draft, setDraft] = (0, import_react2.useState)("");
|
|
8198
8213
|
const typingTimer = (0, import_react2.useRef)(null);
|
|
@@ -8237,7 +8252,8 @@ var EvolutionChatEmbed = (() => {
|
|
|
8237
8252
|
] })
|
|
8238
8253
|
] }, m.id)
|
|
8239
8254
|
),
|
|
8240
|
-
ownerTyping && canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TypingIndicator, { label: tr("typing") })
|
|
8255
|
+
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") })
|
|
8241
8257
|
] }),
|
|
8242
8258
|
!canCompose && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ecw-closed", children: [
|
|
8243
8259
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ecw-notice", role: "status", children: tr("sessionClosed") }),
|
|
@@ -8292,7 +8308,7 @@ var EvolutionChatEmbed = (() => {
|
|
|
8292
8308
|
(0, import_react2.useEffect)(() => {
|
|
8293
8309
|
const list = listRef.current;
|
|
8294
8310
|
if (list !== null) list.scrollTop = list.scrollHeight;
|
|
8295
|
-
}, [state.messages, state.ownerTyping, state.open]);
|
|
8311
|
+
}, [state.messages, state.ownerTyping, state.visitorTyping, state.open]);
|
|
8296
8312
|
const handleClose = (0, import_react2.useCallback)(() => {
|
|
8297
8313
|
closePanel();
|
|
8298
8314
|
bubbleRef.current?.focus();
|
|
@@ -8336,6 +8352,7 @@ var EvolutionChatEmbed = (() => {
|
|
|
8336
8352
|
listRef,
|
|
8337
8353
|
firstFieldRef,
|
|
8338
8354
|
ownerTyping: state.ownerTyping,
|
|
8355
|
+
visitorTyping: state.visitorTyping,
|
|
8339
8356
|
onTyping: notifyTyping,
|
|
8340
8357
|
onSend: sendMessage,
|
|
8341
8358
|
onRetry: retryMessage,
|