@erlancarreira/evolution-chat 0.1.0

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.
@@ -0,0 +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 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\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 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, onSend, onRetry, onNewConversation }: ChatPanelProps): ReactElement {\n const uid = useId();\n const [draft, setDraft] = useState(\"\");\n\n const submit = (e: FormEvent): void => {\n e.preventDefault();\n const text = draft.trim();\n if (text === \"\" || !canCompose) return;\n setDraft(\"\");\n void onSend(text);\n };\n\n return (\n <>\n <ul ref={listRef} className=\"ecw-list\">\n {messages.map((m) => (\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 </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={(e) => { setDraft(e.target.value); }}\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 } =\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.\n useEffect(() => {\n const list = listRef.current;\n if (list !== null) list.scrollTop = list.scrollHeight;\n }, [state.messages, 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 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\">{`${tr(\"poweredBy\")} Evolution Chat`}</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] 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};\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};\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};\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-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/* ── 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\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}\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\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};\n\nfunction withMessage(state: ChatState, message: ChatMessage): ChatState {\n const index = state.messages.findIndex((m) => m.id === message.id);\n if (index >= 0) {\n const messages = [...state.messages];\n messages[index] = message;\n return { ...state, messages };\n }\n const bump = state.open || message.direction !== \"owner\" ? 0 : 1;\n return { ...state, messages: [...state.messages, message], unread: state.unread + bump };\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 }\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}\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}\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 }: UseChatOptions): UseChatResult {\n const [state, dispatch] = useReducer(chatReducer, initialState);\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 }\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 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 };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACgBA,IAAAA,gBAUO;;;ACfA,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;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;AACb;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;AACb;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;AACb;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;;;AC/FO,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;AA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","names":["import_react"]}
@@ -0,0 +1,122 @@
1
+ import { ReactElement } from 'react';
2
+
3
+ type ChatMessageDirection = "visitor" | "owner";
4
+ type ChatMessageStatus = "pending" | "sent" | "failed";
5
+ type ChatSessionStatus = "active" | "closed" | "failed";
6
+ /** Mensagem persistida (domínio). */
7
+ interface ChatMessage {
8
+ id: string;
9
+ sessionId: string;
10
+ direction: ChatMessageDirection;
11
+ body: string;
12
+ status: ChatMessageStatus;
13
+ waMessageId: string | null;
14
+ createdAt: string;
15
+ }
16
+ /** Evento de tempo real publicado no canal broadcast `chat:<realtimeToken>`. */
17
+ type ChatEvent = {
18
+ type: "message";
19
+ message: ChatMessage;
20
+ } | {
21
+ type: "session";
22
+ status: ChatSessionStatus;
23
+ };
24
+
25
+ /**
26
+ * Assinatura client-side do mesmo canal (usada pelo widget — Task 9).
27
+ * `subscribe` devolve o unsubscribe; `onStatus` reporta conexão do transporte.
28
+ */
29
+ interface RealtimeHandle {
30
+ subscribe(realtimeToken: string, onEvent: (e: ChatEvent) => void, onStatus?: (s: "open" | "closed") => void): () => void;
31
+ }
32
+
33
+ type WidgetLocale = "pt" | "en" | "es";
34
+ declare const WIDGET_KEYS: readonly ["openChat", "close", "name", "phone", "message", "send", "sending", "welcomeNotice", "privacyNote", "invalidPhone", "sendError", "retry", "sessionClosed", "newConversation", "poweredBy"];
35
+ type WidgetKey = (typeof WIDGET_KEYS)[number];
36
+ /** Dicionário completo: `Record<WidgetKey, string>` força todas as chaves no compilador. */
37
+ type WidgetDictionary = Record<WidgetKey, string>;
38
+ declare const pt: WidgetDictionary;
39
+ declare const en: WidgetDictionary;
40
+ declare const es: WidgetDictionary;
41
+ declare const dictionaries: Record<WidgetLocale, WidgetDictionary>;
42
+ /**
43
+ * Traduz `key` para `locale`, permitindo override pontual de copy (`labels` do widget).
44
+ * Nunca lança: chave desconhecida devolve o texto de pt e, na ausência, a própria chave.
45
+ */
46
+ declare function t(locale: WidgetLocale, key: WidgetKey, overrides?: Partial<Record<string, string>>): string;
47
+
48
+ interface ChatWidgetProps {
49
+ /** Caminho da rota de chat (GET histórico / POST start+send), ex.: "/api/chat". */
50
+ endpoint: string;
51
+ locale: WidgetLocale;
52
+ /** Saudação exibida no pré-chat form. */
53
+ welcome: string;
54
+ projectName: string;
55
+ /** Cor de tema; default "#25D366". */
56
+ accentColor?: string;
57
+ /** Porta do widget (DI — Strategy): subscribe/unsubscribe do canal da sessão. */
58
+ realtime: RealtimeHandle;
59
+ /** Override pontual de copy por chave i18n. */
60
+ labels?: Partial<Record<string, string>>;
61
+ }
62
+ declare function ChatWidget(props: ChatWidgetProps): ReactElement;
63
+
64
+ declare const WIDGET_CSS = "/*\n * src/widget/styles.css \u2014 folha de estilos do ChatWidget (classes `.ecw-*`).\n *\n * Fonte can\u00F4nica do CSS INJETADO em runtime: src/widget/styles.ts espelha este arquivo\n * e injeta `<style id=\"ecw-styles\">` via `injectWidgetStyles()` (o widget \u00E9\n * auto-contido \u2014 o consumidor n\u00E3o precisa importar CSS). O teste de paridade em\n * test/widget/widget.test.tsx garante que os dois textos n\u00E3o 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\u00EDvel,\n * contraste AA sobre o accent (texto escuro no verde), e transi\u00E7\u00F5es 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/* \u2500\u2500 bal\u00E3o flutuante \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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/* \u2500\u2500 painel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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/* \u2500\u2500 lista de mensagens \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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-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/* \u2500\u2500 pr\u00E9-chat form \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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/* \u2500\u2500 composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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/* \u2500\u2500 sess\u00E3o encerrada / falha \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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\n/* \u2500\u2500 utilit\u00E1rios \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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";
65
+ /**
66
+ * Insere a folha do widget no `document.head` (idempotente: uma única tag
67
+ * `<style id="ecw-styles">` por documento, não importa quantas vezes seja chamada).
68
+ * No-op fora de browser (SSR / Node) — o consumidor renderiza com `<link>` do subpath.
69
+ */
70
+ declare function injectWidgetStyles(): void;
71
+
72
+ /** Chave de persistência da sessão no localStorage. */
73
+ declare const SESSION_STORAGE_KEY = "ecw:session";
74
+ /** Intervalo do fallback de polling quando o canal realtime fecha (ms). */
75
+ declare const POLL_INTERVAL_MS = 5000;
76
+ /** Mínimo de dígitos (com DDD) para um WhatsApp ser aceito no form. */
77
+ declare const MIN_PHONE_DIGITS = 10;
78
+ type ChatPhase = "idle" | "form" | "chat";
79
+ interface SessionInfo {
80
+ code: string;
81
+ status: ChatSessionStatus;
82
+ visitorName: string;
83
+ }
84
+ interface ChatState {
85
+ phase: ChatPhase;
86
+ open: boolean;
87
+ session: SessionInfo | null;
88
+ token: string | null;
89
+ messages: ChatMessage[];
90
+ unread: number;
91
+ sending: boolean;
92
+ /** Chave i18n do erro visível ("sendError" | "invalidPhone") ou null. */
93
+ error: string | null;
94
+ }
95
+ /** Só dígitos (o servidor espera DDI+DDD+número sem máscara). */
96
+ declare function normalizePhone(value: string): string;
97
+ /** Máscara BR parcial `(11) 99999-8888`, progressiva enquanto o usuário digita. */
98
+ declare function maskPhone(value: string): string;
99
+ declare function isValidPhone(value: string): boolean;
100
+ interface UseChatOptions {
101
+ endpoint: string;
102
+ realtime: RealtimeHandle;
103
+ }
104
+ interface UseChatResult {
105
+ state: ChatState;
106
+ openPanel(): void;
107
+ closePanel(): void;
108
+ togglePanel(): void;
109
+ submitForm(input: {
110
+ name: string;
111
+ phone: string;
112
+ message: string;
113
+ honeypot: string;
114
+ }): Promise<void>;
115
+ sendMessage(text: string): Promise<void>;
116
+ retryMessage(id: string): Promise<void>;
117
+ /** Descarta a sessão encerrada/falha (storage + estado) e volta ao pré-chat form. */
118
+ startNewConversation(): void;
119
+ }
120
+ declare function useChat({ endpoint, realtime }: UseChatOptions): UseChatResult;
121
+
122
+ export { type ChatPhase, type ChatState, ChatWidget, type ChatWidgetProps, MIN_PHONE_DIGITS, POLL_INTERVAL_MS, type RealtimeHandle, SESSION_STORAGE_KEY, type SessionInfo, type UseChatOptions, type UseChatResult, WIDGET_CSS, WIDGET_KEYS, type WidgetDictionary, type WidgetKey, type WidgetLocale, dictionaries, en, es, injectWidgetStyles, isValidPhone, maskPhone, normalizePhone, pt, t, useChat };
@@ -0,0 +1,122 @@
1
+ import { ReactElement } from 'react';
2
+
3
+ type ChatMessageDirection = "visitor" | "owner";
4
+ type ChatMessageStatus = "pending" | "sent" | "failed";
5
+ type ChatSessionStatus = "active" | "closed" | "failed";
6
+ /** Mensagem persistida (domínio). */
7
+ interface ChatMessage {
8
+ id: string;
9
+ sessionId: string;
10
+ direction: ChatMessageDirection;
11
+ body: string;
12
+ status: ChatMessageStatus;
13
+ waMessageId: string | null;
14
+ createdAt: string;
15
+ }
16
+ /** Evento de tempo real publicado no canal broadcast `chat:<realtimeToken>`. */
17
+ type ChatEvent = {
18
+ type: "message";
19
+ message: ChatMessage;
20
+ } | {
21
+ type: "session";
22
+ status: ChatSessionStatus;
23
+ };
24
+
25
+ /**
26
+ * Assinatura client-side do mesmo canal (usada pelo widget — Task 9).
27
+ * `subscribe` devolve o unsubscribe; `onStatus` reporta conexão do transporte.
28
+ */
29
+ interface RealtimeHandle {
30
+ subscribe(realtimeToken: string, onEvent: (e: ChatEvent) => void, onStatus?: (s: "open" | "closed") => void): () => void;
31
+ }
32
+
33
+ type WidgetLocale = "pt" | "en" | "es";
34
+ declare const WIDGET_KEYS: readonly ["openChat", "close", "name", "phone", "message", "send", "sending", "welcomeNotice", "privacyNote", "invalidPhone", "sendError", "retry", "sessionClosed", "newConversation", "poweredBy"];
35
+ type WidgetKey = (typeof WIDGET_KEYS)[number];
36
+ /** Dicionário completo: `Record<WidgetKey, string>` força todas as chaves no compilador. */
37
+ type WidgetDictionary = Record<WidgetKey, string>;
38
+ declare const pt: WidgetDictionary;
39
+ declare const en: WidgetDictionary;
40
+ declare const es: WidgetDictionary;
41
+ declare const dictionaries: Record<WidgetLocale, WidgetDictionary>;
42
+ /**
43
+ * Traduz `key` para `locale`, permitindo override pontual de copy (`labels` do widget).
44
+ * Nunca lança: chave desconhecida devolve o texto de pt e, na ausência, a própria chave.
45
+ */
46
+ declare function t(locale: WidgetLocale, key: WidgetKey, overrides?: Partial<Record<string, string>>): string;
47
+
48
+ interface ChatWidgetProps {
49
+ /** Caminho da rota de chat (GET histórico / POST start+send), ex.: "/api/chat". */
50
+ endpoint: string;
51
+ locale: WidgetLocale;
52
+ /** Saudação exibida no pré-chat form. */
53
+ welcome: string;
54
+ projectName: string;
55
+ /** Cor de tema; default "#25D366". */
56
+ accentColor?: string;
57
+ /** Porta do widget (DI — Strategy): subscribe/unsubscribe do canal da sessão. */
58
+ realtime: RealtimeHandle;
59
+ /** Override pontual de copy por chave i18n. */
60
+ labels?: Partial<Record<string, string>>;
61
+ }
62
+ declare function ChatWidget(props: ChatWidgetProps): ReactElement;
63
+
64
+ declare const WIDGET_CSS = "/*\n * src/widget/styles.css \u2014 folha de estilos do ChatWidget (classes `.ecw-*`).\n *\n * Fonte can\u00F4nica do CSS INJETADO em runtime: src/widget/styles.ts espelha este arquivo\n * e injeta `<style id=\"ecw-styles\">` via `injectWidgetStyles()` (o widget \u00E9\n * auto-contido \u2014 o consumidor n\u00E3o precisa importar CSS). O teste de paridade em\n * test/widget/widget.test.tsx garante que os dois textos n\u00E3o 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\u00EDvel,\n * contraste AA sobre o accent (texto escuro no verde), e transi\u00E7\u00F5es 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/* \u2500\u2500 bal\u00E3o flutuante \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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/* \u2500\u2500 painel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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/* \u2500\u2500 lista de mensagens \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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-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/* \u2500\u2500 pr\u00E9-chat form \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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/* \u2500\u2500 composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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/* \u2500\u2500 sess\u00E3o encerrada / falha \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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\n/* \u2500\u2500 utilit\u00E1rios \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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";
65
+ /**
66
+ * Insere a folha do widget no `document.head` (idempotente: uma única tag
67
+ * `<style id="ecw-styles">` por documento, não importa quantas vezes seja chamada).
68
+ * No-op fora de browser (SSR / Node) — o consumidor renderiza com `<link>` do subpath.
69
+ */
70
+ declare function injectWidgetStyles(): void;
71
+
72
+ /** Chave de persistência da sessão no localStorage. */
73
+ declare const SESSION_STORAGE_KEY = "ecw:session";
74
+ /** Intervalo do fallback de polling quando o canal realtime fecha (ms). */
75
+ declare const POLL_INTERVAL_MS = 5000;
76
+ /** Mínimo de dígitos (com DDD) para um WhatsApp ser aceito no form. */
77
+ declare const MIN_PHONE_DIGITS = 10;
78
+ type ChatPhase = "idle" | "form" | "chat";
79
+ interface SessionInfo {
80
+ code: string;
81
+ status: ChatSessionStatus;
82
+ visitorName: string;
83
+ }
84
+ interface ChatState {
85
+ phase: ChatPhase;
86
+ open: boolean;
87
+ session: SessionInfo | null;
88
+ token: string | null;
89
+ messages: ChatMessage[];
90
+ unread: number;
91
+ sending: boolean;
92
+ /** Chave i18n do erro visível ("sendError" | "invalidPhone") ou null. */
93
+ error: string | null;
94
+ }
95
+ /** Só dígitos (o servidor espera DDI+DDD+número sem máscara). */
96
+ declare function normalizePhone(value: string): string;
97
+ /** Máscara BR parcial `(11) 99999-8888`, progressiva enquanto o usuário digita. */
98
+ declare function maskPhone(value: string): string;
99
+ declare function isValidPhone(value: string): boolean;
100
+ interface UseChatOptions {
101
+ endpoint: string;
102
+ realtime: RealtimeHandle;
103
+ }
104
+ interface UseChatResult {
105
+ state: ChatState;
106
+ openPanel(): void;
107
+ closePanel(): void;
108
+ togglePanel(): void;
109
+ submitForm(input: {
110
+ name: string;
111
+ phone: string;
112
+ message: string;
113
+ honeypot: string;
114
+ }): Promise<void>;
115
+ sendMessage(text: string): Promise<void>;
116
+ retryMessage(id: string): Promise<void>;
117
+ /** Descarta a sessão encerrada/falha (storage + estado) e volta ao pré-chat form. */
118
+ startNewConversation(): void;
119
+ }
120
+ declare function useChat({ endpoint, realtime }: UseChatOptions): UseChatResult;
121
+
122
+ export { type ChatPhase, type ChatState, ChatWidget, type ChatWidgetProps, MIN_PHONE_DIGITS, POLL_INTERVAL_MS, type RealtimeHandle, SESSION_STORAGE_KEY, type SessionInfo, type UseChatOptions, type UseChatResult, WIDGET_CSS, WIDGET_KEYS, type WidgetDictionary, type WidgetKey, type WidgetLocale, dictionaries, en, es, injectWidgetStyles, isValidPhone, maskPhone, normalizePhone, pt, t, useChat };