@adatechnology/conversations-ui 0.4.2 → 0.5.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.
Files changed (93) hide show
  1. package/README.md +169 -0
  2. package/dist/{ConversationSimulatorPanel-DnXElfY8.d.ts → ConversationSimulatorPanel-CrfFjiWh.d.ts} +2 -268
  3. package/dist/{chunk-PJNR6UMN.js → chunk-CBYEHPX6.js} +1 -63
  4. package/dist/{chunk-HMCOTZAX.js → chunk-HOKNKXIT.js} +1 -1
  5. package/dist/{chunk-4P5KMWOI.js → chunk-JXJCPN34.js} +327 -522
  6. package/dist/chunk-MRVFFJEH.js +64 -0
  7. package/dist/chunk-RJMSM3YA.js +249 -0
  8. package/dist/chunk-WSFGNR5R.js +55 -0
  9. package/dist/flows/index.js +5 -3
  10. package/dist/index.d.ts +9 -3
  11. package/dist/index.js +783 -807
  12. package/dist/participant/index.d.ts +162 -0
  13. package/dist/participant/index.js +1448 -0
  14. package/dist/preview/index.d.ts +4 -2
  15. package/dist/preview/index.js +4 -2
  16. package/dist/quickReply.types-B4gmZsSr.d.ts +274 -0
  17. package/dist/styles.css +591 -0
  18. package/package.json +8 -4
  19. package/src/ChannelIcon.tsx +2 -0
  20. package/src/MessageBubble.test.tsx +2 -2
  21. package/src/MessageText.test.tsx +59 -0
  22. package/src/MessageText.tsx +20 -12
  23. package/src/MessageTextCopiedBadge.tsx +7 -0
  24. package/src/StatusTicks.test.tsx +30 -0
  25. package/src/StatusTicks.tsx +24 -9
  26. package/src/buildOutput.test.ts +41 -0
  27. package/src/channelCapability.test.ts +17 -0
  28. package/src/conversationChannel.test.ts +30 -1
  29. package/src/conversationChannel.ts +19 -1
  30. package/src/participant/ParticipantAttachmentItem.tsx +77 -0
  31. package/src/participant/ParticipantComposer.test.tsx +97 -0
  32. package/src/participant/ParticipantComposer.tsx +167 -0
  33. package/src/participant/ParticipantConversations.test.tsx +90 -0
  34. package/src/participant/ParticipantConversations.tsx +42 -0
  35. package/src/participant/ParticipantConversationsScreen.tsx +67 -0
  36. package/src/participant/ParticipantInbox.test.tsx +147 -0
  37. package/src/participant/ParticipantInbox.tsx +168 -0
  38. package/src/participant/ParticipantLoadState.tsx +27 -0
  39. package/src/participant/ParticipantMessageBubble.test.tsx +165 -0
  40. package/src/participant/ParticipantMessageBubble.tsx +185 -0
  41. package/src/participant/ParticipantThread.test.tsx +132 -0
  42. package/src/participant/ParticipantThread.tsx +174 -0
  43. package/src/participant/ParticipantThreadScreen.tsx +135 -0
  44. package/src/participant/bindAttachmentUrlResolver.test.ts +25 -0
  45. package/src/participant/bindAttachmentUrlResolver.ts +7 -0
  46. package/src/participant/index.ts +25 -0
  47. package/src/participant/participant.types.ts +43 -0
  48. package/src/participant/participantApi.types.ts +59 -0
  49. package/src/participant/participantAttachmentUrl.test.ts +32 -0
  50. package/src/participant/participantAttachmentUrl.ts +10 -0
  51. package/src/participant/participantBubbleActions.test.ts +44 -0
  52. package/src/participant/participantBubbleActions.ts +40 -0
  53. package/src/participant/participantDrafts.test.ts +65 -0
  54. package/src/participant/participantDrafts.ts +50 -0
  55. package/src/participant/participantFileValidation.test.ts +86 -0
  56. package/src/participant/participantFileValidation.ts +72 -0
  57. package/src/participant/participantFixtures.test-helper.ts +41 -0
  58. package/src/participant/participantGrouping.test.ts +138 -0
  59. package/src/participant/participantGrouping.ts +77 -0
  60. package/src/participant/participantInboxState.test.ts +69 -0
  61. package/src/participant/participantInboxState.ts +74 -0
  62. package/src/participant/participantLabels.ts +86 -0
  63. package/src/participant/participantLoadView.test.ts +23 -0
  64. package/src/participant/participantLoadView.ts +21 -0
  65. package/src/participant/participantMessages.test.ts +128 -0
  66. package/src/participant/participantMessages.ts +73 -0
  67. package/src/participant/participantNewMessages.test.ts +54 -0
  68. package/src/participant/participantNewMessages.ts +29 -0
  69. package/src/participant/participantQuickReply.test.ts +23 -0
  70. package/src/participant/participantQuickReply.ts +6 -0
  71. package/src/participant/participantScroll.test.ts +69 -0
  72. package/src/participant/participantScroll.ts +43 -0
  73. package/src/participant/participantSendController.test.ts +201 -0
  74. package/src/participant/participantSendController.ts +134 -0
  75. package/src/participant/participantSendStates.ts +31 -0
  76. package/src/participant/participantTheme.ts +20 -0
  77. package/src/participant/participantThreadState.test.ts +75 -0
  78. package/src/participant/participantThreadState.ts +59 -0
  79. package/src/participant/shouldMarkRead.test.ts +24 -0
  80. package/src/participant/shouldMarkRead.ts +16 -0
  81. package/src/participant/useDocumentVisibility.ts +18 -0
  82. package/src/participant/useParticipantConversationView.ts +60 -0
  83. package/src/participant/useParticipantDraft.ts +45 -0
  84. package/src/participant/useParticipantInbox.ts +57 -0
  85. package/src/participant/useParticipantMarkRead.ts +26 -0
  86. package/src/participant/useParticipantRevalidation.ts +36 -0
  87. package/src/participant/useParticipantSend.ts +84 -0
  88. package/src/participant/useParticipantThread.ts +116 -0
  89. package/src/participant/useParticipantThreadScroll.ts +26 -0
  90. package/src/participant/useStickToBottom.ts +77 -0
  91. package/src/preview/ConversationSimulatorPanel.tsx +8 -0
  92. package/src/styles.css +160 -0
  93. package/src/types.ts +3 -1
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # @adatechnology/conversations-ui
2
+
3
+ Telas de conversa parametrizáveis por endpoint, tema e capacidades. Subpaths: `.` (inbox/mensagens),
4
+ `/flows`, `/whatsapp`, `/preview`, `/participant` e `/styles.css`.
5
+
6
+ ## Visão do participante (`/participant`)
7
+
8
+ Tela de conversa do lado de **quem conversa com a empresa** (app do motorista, portal do cliente). Não usa
9
+ `ConversationsProvider`, não puxa `@xyflow/react` nem o `ConversationsWorkspace`, e não depende de Tailwind:
10
+ todas as classes são `.cv-p-*` e vêm de `@adatechnology/conversations-ui/styles.css`.
11
+
12
+ `@adatechnology/conversations-ui/styles.css` é **requisito**: sem ele a tela fica sem layout e o `MessageText` e o
13
+ `StatusTicks` perdem a formatação (negrito, itálico, tachado, tiques coloridos). Importe-o uma vez na raiz do app.
14
+
15
+ `MessageText` **copia ao tocar** por padrão (como na 0.3.1); `copyOnClick={false}` desliga. O aviso de cópia usa
16
+ `copiedLabel` (padrão `Copied`; passe `copiedLabel="Copiado!"` para manter o português).
17
+
18
+ ### Uso mínimo
19
+
20
+ ```tsx
21
+ import { ParticipantConversations } from '@adatechnology/conversations-ui/participant'
22
+ import '@adatechnology/conversations-ui/styles.css'
23
+
24
+ const SUBJECT_GROUPS = [
25
+ { subjectType: 'invoice', label: 'Faturas' },
26
+ { subjectType: 'trip', label: 'Viagens' },
27
+ ] as const
28
+
29
+ function ConversationsRoute() {
30
+ const { subjectType, subjectId } = useParams()
31
+ const navigate = useNavigate()
32
+ const selected = subjectType && subjectId ? { subjectType, subjectId } : undefined
33
+
34
+ return (
35
+ <ParticipantConversations
36
+ api={participantApi}
37
+ subjectGroups={SUBJECT_GROUPS}
38
+ selected={selected}
39
+ onSelect={(subject) => navigate(`/conversas/${subject.subjectType}/${subject.subjectId}`)}
40
+ onBack={() => navigate('/conversas')}
41
+ />
42
+ )
43
+ }
44
+ ```
45
+
46
+ ### Contrato de roteamento
47
+
48
+ O componente é **controlado pela rota**: `selected === undefined` mostra a lista, definido mostra a conversa.
49
+ **Monte-o UMA vez acima das duas rotas** (`/conversas` e `/conversas/:subjectType/:subjectId`), com o mesmo
50
+ elemento. O cache da lista e os rascunhos por conversa (texto e anexos) vivem dentro dele; se cada rota
51
+ montasse a sua instância, voltar da conversa para a lista recarregaria tudo e perderia o rascunho.
52
+
53
+ ```tsx
54
+ <Route path="/conversas" element={<ConversationsRoute />} />
55
+ <Route path="/conversas/:subjectType/:subjectId" element={<ConversationsRoute />} />
56
+ ```
57
+
58
+ Capacidades opcionais existem **por ausência de prop**: sem `onBack` não há botão de voltar, sem
59
+ `renderSubjectCard` não há cartão do assunto, sem `onOpenSubject` o título não é clicável, sem
60
+ `quickReplies` não há faixa de respostas rápidas, sem `pendingMessages` não há fila do host. Sem
61
+ `api.subscribe`, a tela revalida ao montar e em `focus`, `visibilitychange` e `online`; com `subscribe`, revalida também em `online` (um evento pode ter se perdido) e marca como lido ao voltar a ficar visível. Sem
62
+ `api.openConversation`, uma conversa fora da primeira página da lista aparece como "não encontrada"
63
+ (`labels.notFound`).
64
+
65
+ ### Adapter REST de `ParticipantConversationsApi`
66
+
67
+ ```ts
68
+ import { z } from 'zod'
69
+
70
+ import { participantConversationPageSchema, participantMessageSchema } from '@adatechnology/conversation-contracts'
71
+ import type { ParticipantConversationsApi } from '@adatechnology/conversations-ui/participant'
72
+
73
+ async function request(path: string, init?: RequestInit): Promise<unknown> {
74
+ const response = await fetch(`/api/v1${path}`, { ...init, credentials: 'include' })
75
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
76
+ return ((await response.json()) as { data: unknown }).data
77
+ }
78
+
79
+ function conversationPath({ subjectType, subjectId }: { subjectType: string; subjectId: string }): string {
80
+ return `/conversations/${encodeURIComponent(subjectType)}/${encodeURIComponent(subjectId)}`
81
+ }
82
+
83
+ export const participantApi: ParticipantConversationsApi = {
84
+ listConversations: async ({ cursor } = {}) =>
85
+ participantConversationPageSchema.parse(
86
+ await request(`/conversations${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ''}`),
87
+ ),
88
+ fetchMessages: async (subject, { before, limit } = {}) =>
89
+ z.array(participantMessageSchema).parse(
90
+ await request(
91
+ `${conversationPath(subject)}/messages?limit=${limit ?? 30}${before ? `&before=${encodeURIComponent(before)}` : ''}`,
92
+ ),
93
+ ),
94
+ sendMessage: async ({ subject, clientMessageId, text }) => {
95
+ const message = participantMessageSchema.parse(
96
+ await request(`${conversationPath(subject)}/messages`, {
97
+ method: 'POST',
98
+ headers: { 'Content-Type': 'application/json', 'Idempotency-Key': clientMessageId },
99
+ body: JSON.stringify({ text }),
100
+ }),
101
+ )
102
+ return { outcome: 'sent', message }
103
+ },
104
+ markRead: async (subject) => {
105
+ await request(`${conversationPath(subject)}/read`, { method: 'POST' })
106
+ },
107
+ resolveAttachmentUrl: async (attachment) =>
108
+ z.object({ url: z.string() }).parse(await request(`/attachments/${encodeURIComponent(attachment.id)}/url`)).url,
109
+ }
110
+ ```
111
+
112
+ A resposta do servidor é entrada não confiável: valide com os schemas do `@adatechnology/conversation-contracts`
113
+ (`participantConversationPageSchema`, `participantMessageSchema`) e use `encodeURIComponent` em todo segmento de URL
114
+ que vem de `subjectType`, `subjectId`, cursor ou id.
115
+
116
+ **Eco de `clientMessageId` (obrigatório).** Toda mensagem criada por `sendMessage` volta com o `clientMessageId`
117
+ recebido (o mesmo valor do `Idempotency-Key`): é por ele que a bolha local é substituída pela mensagem do servidor.
118
+ Sem o eco, a bolha local fica duplicada ao lado da mensagem real. O servidor também precisa tratar com segurança
119
+ duas requisições simultâneas com a mesma chave (toque duplo, reenvio): uma cria, a outra devolve a mesma mensagem.
120
+
121
+ Ao tocar em enviar, o campo é limpo na hora e a **bolha passa a ser a dona** do texto e dos arquivos (o segundo
122
+ toque não envia nada). `sendMessage` que lança significa falha: a bolha mostra "reenviar" (repete o **mesmo**
123
+ `clientMessageId`; use-o como `Idempotency-Key`), "editar" (devolve texto e arquivos ao campo e remove a bolha) e
124
+ "descartar". Uma bolha que falhou sobrevive a sair e voltar da conversa. Para guardar na fila offline do host,
125
+ devolva `{ outcome: 'queued' }` e devolva a fila durável em `pendingMessages`: a bolha fica como "na fila" até o host
126
+ ou o servidor refletirem o mesmo `clientMessageId`. Falha de uma mensagem que veio do servidor mostra só "Falhou";
127
+ falha de uma pendente do host só ganha "reenviar" se `onRetryPending` for passado. O pacote não conhece IndexedDB.
128
+
129
+ Limites do host: devolver `queued` sem refletir a mensagem em `pendingMessages` deixa uma bolha "na fila" sem
130
+ nenhuma ação até a conversa desmontar (ela segura os `File`); `sendMessage` precisa terminar com timeout (uma
131
+ promise que nunca resolve trava o botão de enviar daquele assunto); e se o host tira a mensagem da fila antes de o
132
+ servidor refletir, deve emitir `conversation-changed` para a conversa recarregar.
133
+
134
+ `api` **precisa ser estável** (crie o adapter uma vez, fora do render, ou memorize-o): uma instância nova a cada
135
+ render refaz a carga da lista e reinscreve os eventos de `subscribe` a cada renderização. Métodos de adapter escritos como classe funcionam, o pacote preserva o `this`.
136
+
137
+ ### Aviso: `direction` é sempre na perspectiva da empresa
138
+
139
+ `ParticipantMessage.direction` descreve a mensagem **do ponto de vista da empresa**: `inbound` chegou à
140
+ empresa, ou seja, foi escrita pelo participante (é a bolha "minha"); `outbound` saiu da empresa. O adapter do
141
+ app **não inverte nada**.
142
+
143
+ ### Variáveis `--cv-p-*`
144
+
145
+ Defina no `.cv-p`, no wrapper (`className`) ou em qualquer ancestral. Há também a prop `theme`
146
+ (`primaryColor` → `--cv-p-accent`, `backgroundColor` → `--cv-p-surface`, `textPrimary` → `--cv-p-text`,
147
+ `textSecondary` → `--cv-p-text-muted`).
148
+
149
+ | Variável | Para que serve | Padrão (claro) |
150
+ | --- | --- | --- |
151
+ | `--cv-p-surface` | fundo da tela | `#ffffff` |
152
+ | `--cv-p-surface-raised` | fundo de bolhas recebidas e campos | `#f3f4f6` |
153
+ | `--cv-p-text` | texto principal | `#111827` |
154
+ | `--cv-p-text-muted` | texto secundário | `#5b6573` |
155
+ | `--cv-p-border` | bordas | `#d5d9df` |
156
+ | `--cv-p-accent` | destaque, botão primário, bolha própria | `#a85a1c` |
157
+ | `--cv-p-accent-contrast` | texto sobre o destaque | `#ffffff` |
158
+ | `--cv-p-highlight` | fundo de "espera sua resposta" | `#fbefe2` |
159
+ | `--cv-p-danger` | erro e falha de envio | `#c62828` |
160
+ | `--cv-p-radius` | raio dos cantos | `0` |
161
+
162
+ O tema escuro segue a classe `.dark` (mesma do restante do pacote). Os nomes `.cv-p-*` e `--cv-p-*` são API
163
+ pública e não mudam sem nova versão maior.
164
+
165
+ ### Aviso: `ConversationChannel` foi ampliado
166
+
167
+ `ConversationChannel` agora inclui `'app'` e `'portal'`. Quem tem um `switch` exaustivo sobre o tipo (sem
168
+ `default`) passa a ter erro de compilação até tratar os dois novos valores. Quem só escreve o tipo não é
169
+ afetado.
@@ -1,203 +1,8 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
+ import { M as MessagePayload, b as MessageTranscription, C as ConversationChannel, Q as QuickReply, c as QuickReplyInput, d as QuickReplyAttachment, S as StoredAttachmentSendResult, I as InteractiveSelection } from './quickReply.types-B4gmZsSr.js';
3
4
  import { InteractiveReplyOption, InboundMediaType } from '@adatechnology/meta-whatsapp-contracts/testing';
4
5
 
5
- interface ConversationsUIConfig {
6
- apiBaseUrl: string;
7
- theme?: ConversationsTheme;
8
- features?: ConversationsFeatures;
9
- }
10
- interface ConversationsTheme {
11
- primaryColor?: string;
12
- backgroundColor?: string;
13
- bubbleSent?: string;
14
- bubbleReceived?: string;
15
- textPrimary?: string;
16
- textSecondary?: string;
17
- }
18
- interface ConversationsFeatures {
19
- audio?: boolean;
20
- documents?: boolean;
21
- emoji?: boolean;
22
- darkMode?: boolean;
23
- }
24
- /**
25
- * Recorte do bloco `interactive` da Meta que a UI precisa para desenhar o menu. Fica solto (e não
26
- * espelhando o contrato inteiro) porque o que chega do banco é o payload cru já enviado ao
27
- * WhatsApp: qualquer campo que a UI não conheça é ignorado, nunca causa erro de render.
28
- */
29
- interface InteractiveOption {
30
- id: string;
31
- title: string;
32
- description?: string;
33
- }
34
- interface InteractiveSection {
35
- title?: string;
36
- rows?: InteractiveOption[];
37
- }
38
- interface InteractivePayload {
39
- type?: 'button' | 'list' | string;
40
- header?: {
41
- text?: string;
42
- };
43
- body?: {
44
- text?: string;
45
- };
46
- footer?: {
47
- text?: string;
48
- };
49
- action?: {
50
- /** Rótulo do botão que abre a lista — só existe em `type: 'list'`. */
51
- button?: string;
52
- sections?: InteractiveSection[];
53
- buttons?: {
54
- reply?: InteractiveOption;
55
- }[];
56
- };
57
- }
58
- /** Como o cliente respondeu a um menu: por botão ou por item de lista. */
59
- type InteractiveSelection = {
60
- readonly kind: 'button' | 'list';
61
- readonly option: InteractiveOption;
62
- };
63
- interface MessagePayload {
64
- id: string;
65
- type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'template' | 'interactive';
66
- /** Payload cru da mensagem. Em `type: 'interactive'`, carrega o menu que o cliente vê. */
67
- payload?: InteractivePayload | null;
68
- content?: string;
69
- caption?: string;
70
- mediaUrl?: string;
71
- base64?: string;
72
- uploadId?: string;
73
- mediaId?: string;
74
- mimeType?: string;
75
- filename?: string;
76
- sizeBytes?: number;
77
- direction: 'inbound' | 'outbound';
78
- sender: 'bot' | 'customer' | 'agent';
79
- timestamp: string;
80
- status?: 'sent' | 'delivered' | 'read' | 'failed';
81
- readAt?: string;
82
- agentName?: string | null;
83
- templateName?: string;
84
- /**
85
- * Veredito de moderação vindo do backend — a UI só exibe, nunca calcula. Dicionário no browser
86
- * seria peso morto e daria veredito diferente por versão de cliente.
87
- *
88
- * `null`/ausente = não avaliado (moderação desligada, ou mensagem anterior ao recurso), que é
89
- * diferente de avaliado e limpo.
90
- */
91
- moderation?: {
92
- isOffensive: boolean;
93
- terms: string[];
94
- } | null;
95
- /**
96
- * Transcrição do áudio, vinda do backend — a UI só exibe, nunca transcreve. Rodar STT no browser
97
- * exigiria baixar modelo por aba e daria resultado diferente por versão de cliente.
98
- *
99
- * `null`/ausente = não avaliado, que é diferente de `'done'` com texto vazio (áudio em silêncio,
100
- * já processado). É essa distinção que decide se o balão oferece "transcrever" ou "sem fala
101
- * detectada".
102
- */
103
- transcription?: MessageTranscription | null;
104
- isFirstInGroup?: boolean;
105
- isLastInGroup?: boolean;
106
- }
107
- type TranscriptionStatus = 'pending' | 'done' | 'failed' | 'unsupported';
108
- /**
109
- * Quando transcrever, escolhido nas configurações da empresa. Espelha o
110
- * `TranscriptionMode` de `@adatechnology/meta-whatsapp-contracts`; declarado aqui para o pacote de
111
- * UI não obrigar quem só desenha telas a instalar os contratos do backend.
112
- */
113
- type TranscriptionMode = 'auto' | 'onDemand';
114
- interface MessageTranscription {
115
- status: TranscriptionStatus;
116
- text?: string | null;
117
- /** ISO 639-1 ou nome do idioma, conforme o engine. Exibido como dica, não interpretado. */
118
- language?: string | null;
119
- engine?: string | null;
120
- }
121
-
122
- /**
123
- * Canal de origem da conversa e o que cada um permite.
124
- *
125
- * Existe porque as regras que a inbox precisa respeitar não são do WhatsApp, são **de cada canal**:
126
- * a janela de sessão, o jeito de reabrir a conversa e o formato do identificador do contato mudam
127
- * entre WhatsApp, Messenger, Instagram e chat de site. Tratar a regra do WhatsApp como universal
128
- * faria a UI bloquear o composer num chat de site, onde janela nenhuma existe.
129
- *
130
- * `whatsapp` é o padrão em todo lugar: instalações que ainda não informam canal continuam
131
- * funcionando exatamente como antes.
132
- */
133
- declare const CONVERSATION_CHANNEL: {
134
- readonly WHATSAPP: "whatsapp";
135
- readonly MESSENGER: "messenger";
136
- readonly INSTAGRAM: "instagram";
137
- readonly WEBCHAT: "webchat";
138
- readonly EMAIL: "email";
139
- };
140
- type ConversationChannel = (typeof CONVERSATION_CHANNEL)[keyof typeof CONVERSATION_CHANNEL];
141
- declare const DEFAULT_CONVERSATION_CHANNEL: ConversationChannel;
142
- /** Como o canal reabre uma conversa fora da janela de sessão. */
143
- declare const REOPEN_MECHANISM: {
144
- readonly TEMPLATE: "template";
145
- readonly TAG: "tag";
146
- readonly NONE: "none";
147
- };
148
- type ReopenMechanism = (typeof REOPEN_MECHANISM)[keyof typeof REOPEN_MECHANISM];
149
- /** Natureza do identificador do contato — decide como exibi-lo. */
150
- declare const HANDLE_KIND: {
151
- readonly PHONE: "phone";
152
- readonly USERNAME: "username";
153
- readonly SESSION: "session";
154
- readonly EMAIL: "email";
155
- };
156
- type HandleKind = (typeof HANDLE_KIND)[keyof typeof HANDLE_KIND];
157
- /**
158
- * Só o que é **exibição** do canal — rótulo, ícone, como reabrir, como mostrar o identificador.
159
- * Capacidade de **comportamento** (confirma leitura, tem janela e de quantas horas, aceita anexo e
160
- * áudio, precisa de transporte próprio) não mora mais aqui: para canal que
161
- * `@adatechnology/conversation-contracts` conhece, ela vem de `channelCapabilityFor` (RF10). Manter
162
- * os dois tipos de capacidade juntos foi o que criou a "janela" duplicada (`windowHours`) que a
163
- * spec 211 mandou apagar — a mesma verdade não pode morar em dois lugares.
164
- */
165
- type ChannelCapabilities = {
166
- readonly label: string;
167
- readonly icon: string;
168
- readonly reopenMechanism: ReopenMechanism;
169
- readonly handleKind: HandleKind;
170
- };
171
- declare const CHANNEL_CAPABILITIES: Readonly<Record<ConversationChannel, ChannelCapabilities>>;
172
- declare function capabilitiesOf(channel: ConversationChannel | undefined): ChannelCapabilities;
173
- declare const CHANNEL_FILTER_ALL = "all";
174
- type ChannelFilter = ConversationChannel | typeof CHANNEL_FILTER_ALL;
175
- type ChannelFilterOption = {
176
- readonly value: ChannelFilter;
177
- readonly label: string;
178
- };
179
- /**
180
- * Opções derivadas do que existe na lista, não do catálogo inteiro: oferecer Instagram numa conta
181
- * que só tem WhatsApp promete um recorte que nunca traz resultado.
182
- *
183
- * Devolve vazio com menos de dois canais — um filtro de opção única não filtra nada, e a barra só
184
- * ocuparia espaço. O host usa isso para esconder a seção.
185
- */
186
- declare function channelFiltersFor(conversations: readonly {
187
- readonly channel?: ConversationChannel | undefined;
188
- }[]): ChannelFilterOption[];
189
- type FormatContactHandleParams = {
190
- readonly handle: string;
191
- readonly channel?: ConversationChannel | undefined;
192
- };
193
- /**
194
- * Exibição do identificador conforme a natureza dele. Formatar tudo como telefone — o que a UI
195
- * fazia — transforma um `@perfil` do Instagram em dígitos sem sentido.
196
- */
197
- declare function formatContactHandle(params: FormatContactHandleParams): string;
198
- /** Bandeira só faz sentido quando o identificador é telefone. */
199
- declare function contactFlag(params: FormatContactHandleParams): string;
200
-
201
6
  type ResolveMediaUrl = (message: MessagePayload) => Promise<string | null>;
202
7
  interface MediaRendererProps {
203
8
  message: MessagePayload;
@@ -215,77 +20,6 @@ interface MediaRendererProps {
215
20
  }
216
21
  declare function MediaRenderer({ message, onLightbox, onResolveUrl, onTranscribeAudio, className, }: MediaRendererProps): react.JSX.Element | null;
217
22
 
218
- /**
219
- * Mensagem pronta cadastrada pelo produto e oferecida no composer.
220
- *
221
- * Difere do `QuickReply` dos chips (`MessageComposer`): aquela vive em código no host; esta vem da
222
- * API, tem atalho para o `/` e é editada pela tela de cadastro.
223
- */
224
- type QuickReply = {
225
- readonly id: string;
226
- /** Até 40 caracteres — é o que o atendente lê na lista. */
227
- readonly title: string;
228
- /** Casa `^[a-z0-9-]{1,20}$`: é digitado depois do `/`, sem acento nem espaço. */
229
- readonly shortcut: string;
230
- /** Até 1000 caracteres, com `{{marcador}}` onde entra o dado da conversa. */
231
- readonly body: string;
232
- /** Ausente em host sem anexos: o contrato continua o mesmo para quem não os oferece. */
233
- readonly attachments?: readonly QuickReplyAttachment[];
234
- };
235
- /** Teto de anexos por mensagem pronta — o mesmo que a API aplica, para a tela recusar antes. */
236
- declare const QUICK_REPLY_ATTACHMENT_LIMIT = 10;
237
- /** Arquivo já guardado pelo host; o `uploadId` é a única chave que o pacote devolve ao servidor. */
238
- type QuickReplyAttachment = {
239
- readonly uploadId: string;
240
- readonly filename: string;
241
- readonly mimeType: string;
242
- readonly sizeBytes: number;
243
- };
244
- /**
245
- * Item da fila do composer. `local` ainda não subiu (veio do clipe); `stored` já está no servidor
246
- * (veio de uma mensagem pronta) e é enviado por referência, sem baixar e subir de novo.
247
- */
248
- type QueuedAttachment = {
249
- readonly kind: 'local';
250
- readonly localId: string;
251
- readonly file: File;
252
- } | {
253
- readonly kind: 'stored';
254
- readonly uploadId: string;
255
- readonly filename: string;
256
- readonly mimeType: string;
257
- readonly sizeBytes: number;
258
- /** Carregada sob demanda só para imagem — a lista não pode abrir N URLs assinadas de uma vez. */
259
- readonly previewUrl?: string;
260
- };
261
- /** Resultado por arquivo: o envio em lote falha parcialmente, e só o que falhou fica na fila. */
262
- type StoredAttachmentSendResult = {
263
- readonly uploadId: string;
264
- readonly status: 'sent' | 'failed' | 'skipped';
265
- readonly errorCode?: string;
266
- };
267
- /** O que a tela de cadastro manda para criar ou atualizar; o `id` é do servidor. */
268
- type QuickReplyInput = {
269
- readonly title: string;
270
- readonly shortcut: string;
271
- readonly body: string;
272
- /** Ordem do cadastro é a ordem de envio; ausente não mexe nos anexos já gravados. */
273
- readonly attachmentUploadIds?: readonly string[];
274
- };
275
- /**
276
- * Dado da conversa que um texto pode citar. Uma lista só alimenta a prévia, a inserção e o botão de
277
- * variáveis — antes eram duas props de formato diferente, e as telas divergiam por isso.
278
- */
279
- type ConversationVariable = {
280
- readonly id: string;
281
- /** Rótulo do botão "Inserir variável" (ex.: "Nome do cliente"). */
282
- readonly label: string;
283
- /** Como aparece no texto: `{{nome}}`. */
284
- readonly marker: string;
285
- /** Vazio quando a conversa não tem o dado: a variável não é oferecida. */
286
- readonly value: string;
287
- };
288
-
289
23
  /**
290
24
  * Gravação de áudio no simulador, pelo microfone do próprio navegador.
291
25
  *
@@ -917,4 +651,4 @@ type ConversationSimulatorPanelProps = Omit<ConversationPreviewProps, 'placehold
917
651
  };
918
652
  declare function ConversationSimulatorPanel({ onClose, channel, displayHandle, displayNumber, labels, headerActions, ...previewProps }: ConversationSimulatorPanelProps): react.JSX.Element;
919
653
 
920
- export { type ListDocumentsParams as $, AudioRecorderButton as A, acceptsMediaKind as B, type ConversationSummary as C, DEFAULT_AUDIO_RECORDER_BUTTON_LABELS as D, assertPreviewEnvironment as E, createPreviewMediaPoster as F, createPreviewMediaUploader as G, createPreviewWebhookClient as H, isConversationSimulatorClient as I, mediaKindOf as J, mediaTypeOf as K, type ListConversationsParams as L, type MessagePayload as M, signPreviewPayload as N, simulatorPanelLabelsOf as O, type PreviewUploadedMedia$1 as P, toConversationSimulatorClient as Q, type ResolveMediaUrl as R, type SSEProvider as S, type ToSimulatorClientParams as T, type InteractiveSelection as U, type MessageTranscription as V, type ConversationChannel as W, type InteractivePayload as X, type ConversationsFeatures as Y, type QuickReply as Z, type TranscriptionMode as _, type ConversationEventSource as a, type ConversationTemplate as a0, type QueuedAttachment as a1, type QuickReplyInput as a2, type QuickReplyAttachment as a3, type ConversationVariable as a4, type ChannelFilter as a5, type ChannelFilterOption as a6, CHANNEL_CAPABILITIES as a7, CHANNEL_FILTER_ALL as a8, CONVERSATION_CHANNEL as a9, type ChannelCapabilities as aa, type CompanyDocument as ab, type CompanyDocumentPage as ac, type ConversationDocumentPage as ad, type ConversationsTheme as ae, type ConversationsUIConfig as af, DEFAULT_CONVERSATION_CHANNEL as ag, DEFAULT_MAX_RECORDING_MILLISECONDS as ah, type FormatContactHandleParams as ai, HANDLE_KIND as aj, type HandleKind as ak, type InteractiveOption as al, type InteractiveSection as am, MediaRenderer as an, type MediaRendererProps as ao, QUICK_REPLY_ATTACHMENT_LIMIT as ap, REOPEN_MECHANISM as aq, type ReopenMechanism as ar, type StoredAttachmentSendResult as as, type TranscriptionStatus as at, capabilitiesOf as au, channelFiltersFor as av, contactFlag as aw, formatContactHandle as ax, type ConversationsApi as b, type ConversationPage as c, type ConversationDocument as d, type SendPreviewMediaParams as e, type PreviewWebhookClient as f, type AudioRecorderButtonLabels as g, type AudioRecorderButtonProps as h, ConversationPreview as i, type ConversationPreviewProps as j, type ConversationSimulatorClient as k, ConversationSimulatorPanel as l, type ConversationSimulatorPanelLabels as m, type ConversationSimulatorPanelProps as n, type CreatePreviewMediaUploaderParams as o, type CreatePreviewWebhookClientParams as p, DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS as q, DEFAULT_MEDIA_UPLOAD_PATH as r, PreviewInProductionError as s, PreviewMediaUploadRejectedError as t, type PreviewMediaUploadRequest as u, type PreviewUploadedMedia as v, PreviewWebhookRejectedError as w, SIMULATOR_FILE_MEDIA_KINDS as x, type SendSimulatorMediaParams as y, type SimulatorMediaKind as z };
654
+ export { AudioRecorderButton as A, acceptsMediaKind as B, type ConversationSummary as C, DEFAULT_AUDIO_RECORDER_BUTTON_LABELS as D, assertPreviewEnvironment as E, createPreviewMediaPoster as F, createPreviewMediaUploader as G, createPreviewWebhookClient as H, isConversationSimulatorClient as I, mediaKindOf as J, mediaTypeOf as K, type ListConversationsParams as L, signPreviewPayload as M, simulatorPanelLabelsOf as N, toConversationSimulatorClient as O, type PreviewUploadedMedia$1 as P, type ListDocumentsParams as Q, type ResolveMediaUrl as R, type SSEProvider as S, type ToSimulatorClientParams as T, type ConversationTemplate as U, type CompanyDocument as V, type CompanyDocumentPage as W, type ConversationDocumentPage as X, DEFAULT_MAX_RECORDING_MILLISECONDS as Y, MediaRenderer as Z, type MediaRendererProps as _, type ConversationEventSource as a, type ConversationsApi as b, type ConversationPage as c, type ConversationDocument as d, type SendPreviewMediaParams as e, type PreviewWebhookClient as f, type AudioRecorderButtonLabels as g, type AudioRecorderButtonProps as h, ConversationPreview as i, type ConversationPreviewProps as j, type ConversationSimulatorClient as k, ConversationSimulatorPanel as l, type ConversationSimulatorPanelLabels as m, type ConversationSimulatorPanelProps as n, type CreatePreviewMediaUploaderParams as o, type CreatePreviewWebhookClientParams as p, DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS as q, DEFAULT_MEDIA_UPLOAD_PATH as r, PreviewInProductionError as s, PreviewMediaUploadRejectedError as t, type PreviewMediaUploadRequest as u, type PreviewUploadedMedia as v, PreviewWebhookRejectedError as w, SIMULATOR_FILE_MEDIA_KINDS as x, type SendSimulatorMediaParams as y, type SimulatorMediaKind as z };
@@ -217,72 +217,10 @@ function waToHTMLInline(text) {
217
217
  return escapeHtml(text).replace(/\*([^*\n]+)\*/g, "<strong>$1</strong>").replace(/_([^_\n]+)_/g, "<em>$1</em>").replace(/~([^~\n]+)~/g, "<del>$1</del>").replace(/`([^`\n]+)`/g, "<code>$1</code>");
218
218
  }
219
219
 
220
- // src/useDarkMode.ts
221
- import { useState, useEffect, useCallback } from "react";
222
- var STORAGE_KEY = "conversations-ui-dark-mode";
223
- function getInitialDark() {
224
- if (typeof window === "undefined") return false;
225
- const stored = localStorage.getItem(STORAGE_KEY);
226
- if (stored !== null) {
227
- return stored === "true";
228
- }
229
- return window.matchMedia("(prefers-color-scheme: dark)").matches;
230
- }
231
- function useIsDarkTheme() {
232
- const [isDark, setIsDark] = useState(
233
- () => typeof document !== "undefined" && document.documentElement.classList.contains("dark")
234
- );
235
- useEffect(() => {
236
- const root = document.documentElement;
237
- const observer = new MutationObserver(() => setIsDark(root.classList.contains("dark")));
238
- observer.observe(root, { attributes: true, attributeFilter: ["class"] });
239
- setIsDark(root.classList.contains("dark"));
240
- return () => observer.disconnect();
241
- }, []);
242
- return isDark;
243
- }
244
- function useDarkMode() {
245
- const [isDark, setIsDark] = useState(getInitialDark);
246
- useEffect(() => {
247
- const root = document.documentElement;
248
- if (isDark) {
249
- root.classList.add("dark");
250
- } else {
251
- root.classList.remove("dark");
252
- }
253
- localStorage.setItem(STORAGE_KEY, String(isDark));
254
- }, [isDark]);
255
- useEffect(() => {
256
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
257
- const handleChange = (event) => {
258
- const stored = localStorage.getItem(STORAGE_KEY);
259
- if (stored === null) {
260
- setIsDark(event.matches);
261
- }
262
- };
263
- mediaQuery.addEventListener("change", handleChange);
264
- return () => mediaQuery.removeEventListener("change", handleChange);
265
- }, []);
266
- const toggle = useCallback(() => {
267
- setIsDark((prev) => !prev);
268
- }, []);
269
- return { isDark, toggle };
270
- }
271
-
272
- // src/lib/cn.ts
273
- import { clsx } from "clsx";
274
- import { twMerge } from "tailwind-merge";
275
- function cn(...inputs) {
276
- return twMerge(clsx(inputs));
277
- }
278
-
279
220
  export {
280
- cn,
281
221
  parseWhatsAppFormatting,
282
222
  ZERO_WIDTH_SPACE,
283
223
  waToHTML,
284
224
  htmlToWA,
285
- waToHTMLInline,
286
- useIsDarkTheme,
287
- useDarkMode
225
+ waToHTMLInline
288
226
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  cn
3
- } from "./chunk-PJNR6UMN.js";
3
+ } from "./chunk-MRVFFJEH.js";
4
4
 
5
5
  // src/Tooltip.tsx
6
6
  import { useEffect, useRef, useState } from "react";