@adatechnology/conversations-ui 0.7.0 → 0.8.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 (47) hide show
  1. package/README.md +96 -1
  2. package/dist/{ConversationSimulatorPanel-CrfFjiWh.d.ts → ConversationSimulatorPanel-q-wVgBCN.d.ts} +1 -1
  3. package/dist/index.d.ts +4 -4
  4. package/dist/participant/index.d.ts +45 -3
  5. package/dist/participant/index.js +287 -112
  6. package/dist/preview/index.d.ts +3 -3
  7. package/dist/{quickReply.types-B4gmZsSr.d.ts → quickReply.types-DzWH3NYV.d.ts} +1 -1
  8. package/dist/styles.css +74 -29
  9. package/package.json +1 -1
  10. package/src/participant/ConversationThread.test.tsx +165 -0
  11. package/src/participant/ConversationThread.tsx +117 -0
  12. package/src/participant/ParticipantMessageBubble.tsx +7 -3
  13. package/src/participant/ParticipantThread.tsx +9 -0
  14. package/src/participant/ParticipantThreadHeader.tsx +10 -0
  15. package/src/participant/ParticipantThreadScreen.tsx +6 -46
  16. package/src/participant/ParticipantTimeline.tsx +6 -3
  17. package/src/participant/conversationThreadApi.test.ts +27 -0
  18. package/src/participant/conversationThreadApi.ts +24 -0
  19. package/src/participant/conversationThreadCss.test.ts +116 -0
  20. package/src/participant/conversationThreadSummary.ts +34 -0
  21. package/src/participant/index.ts +3 -0
  22. package/src/participant/participantContrast.test-helper.ts +116 -0
  23. package/src/participant/participantGolden.test.tsx +46 -0
  24. package/src/participant/participantGoldenScenarios.test-helper.tsx +57 -0
  25. package/src/participant/participantMessages.ts +4 -3
  26. package/src/participant/participantNewMessages.test.ts +21 -0
  27. package/src/participant/participantNewMessages.ts +8 -3
  28. package/src/participant/participantPerspective.test.ts +67 -0
  29. package/src/participant/participantPerspective.ts +50 -0
  30. package/src/participant/participantPerspectiveRender.test.tsx +94 -0
  31. package/src/participant/participantThemeContrast.test.ts +98 -0
  32. package/src/participant/test-fixtures/conversations-list.golden.html +1 -0
  33. package/src/participant/test-fixtures/conversations-selected.golden.html +1 -0
  34. package/src/participant/test-fixtures/thread-avatars-initials.golden.html +1 -0
  35. package/src/participant/test-fixtures/thread-closed.golden.html +1 -0
  36. package/src/participant/test-fixtures/thread-default.golden.html +1 -0
  37. package/src/participant/test-fixtures/thread-error.golden.html +1 -0
  38. package/src/participant/test-fixtures/thread-loading.golden.html +1 -0
  39. package/src/participant/test-fixtures/thread-plain.golden.html +1 -0
  40. package/src/participant/test-fixtures/thread-tail-off.golden.html +1 -0
  41. package/src/participant/useParticipantConversationView.ts +3 -0
  42. package/src/participant/useParticipantThread.ts +13 -2
  43. package/src/participant/useParticipantThreadController.ts +112 -0
  44. package/src/participant/useParticipantThreadScroll.ts +3 -1
  45. package/src/participant/useParticipantUnread.test.tsx +40 -0
  46. package/src/participant/useParticipantUnread.ts +38 -0
  47. package/src/styles.css +24 -12
package/README.md CHANGED
@@ -299,6 +299,99 @@ A bolha usa um renderizador próprio da visão do participante (`ParticipantMess
299
299
 
300
300
  **Valores copiáveis:** a detecção é conservadora. CPF e CNPJ só valem com dígitos verificadores corretos (sequências repetidas não valem), a chave exige exatamente 44 dígitos (grupos de 4 opcionais) e o telefone exige 10 a 15 dígitos (BR: DDD 11–99) **com formatação**: sem máscara, só vale com `+55`, DDD entre parênteses ou separador depois do DDD (`1234567890` solto, como em "pedido 1234567890", não é telefone); número internacional exige `+`. Valores monetários, datas e números curtos não são destacados. O toque copia, mostra "Copied" no próprio valor e anuncia na região `aria-live` única da conversa. Os rótulos `copyValue`, `valueCopied` e `externalLinkHint` vêm de `labels` (padrão em inglês). O SDK só escreve na área de transferência quando a pessoa toca: não envia nem armazena nada.
301
301
 
302
+ ### `ConversationThread`: uma conversa, vista por qualquer um dos lados
303
+
304
+ `ConversationThread` desenha **uma** conversa (um assunto) no mesmo desenho da visão do participante, sem lista
305
+ e sem inbox. Serve ao app do participante e, com `perspective="operator"`, ao painel de quem atende. O SDK não
306
+ conhece as ações do host: encerrar, reabrir ou qualquer outra entram pelo slot `headerActions`.
307
+
308
+ ```tsx
309
+ import { ConversationThread } from '@adatechnology/conversations-ui/participant'
310
+
311
+ <ConversationThread
312
+ api={operatorApi}
313
+ subject={{ subjectType: 'case', subjectId: caseId }}
314
+ perspective="operator"
315
+ title={caseTitle}
316
+ protocol={protocol}
317
+ channels={['app', 'whatsapp']}
318
+ status={isClosed ? 'closed' : 'open'}
319
+ counterpartLabel={`Participante: ${participantName}`}
320
+ headerActions={<button type="button" onClick={handleClose}>Fechar</button>}
321
+ avatars="initials"
322
+ quickReplies={quickReplies}
323
+ labels={{ messageInputPlaceholder: 'Mensagem ao participante' }}
324
+ />
325
+ ```
326
+
327
+ | Prop | Tipo | Efeito |
328
+ | ---- | ---- | ------ |
329
+ | `api` | `ConversationThreadApi` | `fetchMessages`, `sendMessage` e `resolveAttachmentUrl` obrigatórios; `markRead` e `subscribe` opcionais (sem `markRead` não há marcação de leitura nem `onMarkedRead`). Deve ser **estável** (constante de módulo ou `useMemo`): um objeto novo a cada render recria o adapter e a revalidação. É o mesmo adapter de `ParticipantConversationsApi`, sem `listConversations` e `openConversation` |
330
+ | `subject` | `ParticipantSubjectRef` | o assunto da conversa |
331
+ | `perspective` | `'participant' \| 'operator'` | padrão `participant` (comportamento da tela de sempre) |
332
+ | `title` | `string` | título do cabeçalho |
333
+ | `protocol?`, `channels?` | | selo copiável e selos de canal; ausentes não desenham nada |
334
+ | `status?` | `'open' \| 'closed'` | `closed` tira o compositor e mostra `labels.closedNotice` |
335
+ | `counterpartLabel?` | `string` | linha curta sob o título, montada pelo host |
336
+ | `headerActions?` | `ReactNode` | slot à direita do cabeçalho; ausente não desenha nada. Espera botões curtos ou só ícone, cada um com `aria-label`: com ações, a barra quebra de linha e o título mantém no mínimo `6rem` |
337
+ | `onBack?` | `() => void` | ausente = sem seta (o diálogo do host já tem o próprio fechar) |
338
+ | `quickReplies?` | `QuickReply[]` | chips acima do campo; tocar **preenche**, nunca envia |
339
+ | `renderSubjectIcon?`, `subjectGroups?` | | ícone do assunto no cabeçalho, como na lista |
340
+ | `avatars?`, `renderAuthorAvatar?`, `tail?` | | iguais aos de `ParticipantConversations` |
341
+ | `labels?`, `theme?`, `className?`, `locale?`, `channel?` | | iguais aos de `ParticipantConversations` |
342
+ | `pendingMessages?`, `onRetryPending?`, `onMarkedRead?` | | fila durável do host e aviso de leitura registrada |
343
+
344
+ **Perspectiva.** `direction` é sempre do ponto de vista da empresa. Em `participant`, "minha" é `inbound`.
345
+ Em `operator`, "minha" é `outbound`: a bolha própria vai à direita, os tiques de enviada, entregue e lida
346
+ aparecem nas mensagens do operador, e o avatar de iniciais e o rótulo de autor (`authorName`) aparecem nas da
347
+ contraparte (`inbound`). O envio, o rascunho local, os estados `sending`/`queued`/`failed` e o retry são os de
348
+ sempre. A leitura segue a perspectiva: o componente chama `markRead(subject)` quando há mensagens **da outra
349
+ parte** sem `readAt` e sem `status: 'read'`; o operador marca só as `inbound`. Sem `subscribe`, a conversa
350
+ revalida ao voltar o foco, a visibilidade e a conexão.
351
+
352
+ **Exemplo de adapter do operador.** O painel já tem as rotas dele; o adapter só traduz:
353
+
354
+ ```ts
355
+ import { z } from 'zod'
356
+
357
+ import { participantMessageSchema } from '@adatechnology/conversation-contracts'
358
+ import type { ConversationThreadApi } from '@adatechnology/conversations-ui/participant'
359
+
360
+ export const operatorThreadApi: ConversationThreadApi = {
361
+ fetchMessages: async ({ subjectType, subjectId }, params) =>
362
+ z.array(participantMessageSchema).parse(await http.get(`/conversations/${subjectType}/${subjectId}/messages`, { params })),
363
+ sendMessage: async ({ subject, clientMessageId, text, files }) => {
364
+ const body = await http.post(`/conversations/${subject.subjectType}/${subject.subjectId}/messages`, { text, files }, {
365
+ headers: { 'Idempotency-Key': clientMessageId },
366
+ })
367
+ return { outcome: 'sent', message: participantMessageSchema.parse(body) }
368
+ },
369
+ markRead: ({ subjectType, subjectId }) => http.post(`/conversations/${subjectType}/${subjectId}/read`),
370
+ resolveAttachmentUrl: async (attachment, disposition) => (await http.get(`/attachments/${attachment.id}/url`, { params: { disposition } })).url,
371
+ }
372
+ ```
373
+
374
+ **Tema claro do painel.** Os tokens padrão já são claros. Para um painel bege com acento laranja, basta mapear:
375
+
376
+ ```css
377
+ .panel-conversation {
378
+ --cv-p-surface: #f2efe9;
379
+ --cv-p-surface-raised: #ffffff;
380
+ --cv-p-text: #1f2a30;
381
+ --cv-p-text-muted: #59636a;
382
+ --cv-p-border: #cfc8bb;
383
+ --cv-p-accent: #a85a1c;
384
+ --cv-p-highlight: #fbf3ea;
385
+ }
386
+ ```
387
+
388
+ Os contrastes são verificados por teste (`participantThemeContrast.test.ts`) com os padrões claro e escuro e com este
389
+ mapeamento: texto, hora, autor, links e valores copiáveis nas duas bolhas a 4,5:1 ou mais; tiques, borda da bolha
390
+ própria e sublinhado dos valores copiáveis a 3:1 ou mais; o fundo pontilhado se mantém sutil (no máximo 2:1 contra a
391
+ superfície) e nenhum texto depende dele (o erro de carga ganha painel sólido). A borda da bolha **recebida** é
392
+ decorativa (cerca de 1,4:1): quem a distingue da página é o preenchimento, e o texto passa de 4,5:1. Ao mapear o
393
+ acento, mantenha-o a 4,5:1 contra `--cv-p-highlight`, que é onde ficam os links da bolha própria. Os contrastes só valem com `--cv-p-highlight` **opaco**: o padrão do tema escuro do pacote é translúcido (`rgb(213 138 71 / 10%)`), então o host que mapeia o token deve fornecer um valor opaco.
394
+
302
395
  ### Limitações conhecidas
303
396
 
304
397
  - O avatar alinha pelo topo da bolha, não pelo rabinho no canto inferior.
@@ -319,12 +412,14 @@ Defina no `.cv-p`, no wrapper (`className`) ou em qualquer ancestral. Há també
319
412
  | `--cv-p-border` | bordas | `#d5d9df` |
320
413
  | `--cv-p-accent` | destaque, botão primário, bolha própria | `#a85a1c` |
321
414
  | `--cv-p-accent-contrast` | texto sobre o destaque | `#ffffff` |
322
- | `--cv-p-highlight` | fundo de "espera sua resposta" | `#fbefe2` |
415
+ | `--cv-p-highlight` | fundo de "espera sua resposta" e da bolha própria | `#fcf3ea` (use cor opaca: é fundo de bolha, e translúcido deixa o wallpaper aparecer) |
323
416
  | `--cv-p-danger` | erro e falha de envio | `#c62828` |
324
417
  | `--cv-p-radius` | raio dos cantos | `0` |
325
418
  | `--cv-p-wallpaper` | fundo da lista de mensagens (`none` desliga) | pontos sutis |
326
419
  | `--cv-p-scrollbar-thumb` | polegar das barras de rolagem finas | `--cv-p-border` |
327
420
  | `--cv-p-scrollbar-thumb-hover` | polegar ao passar o mouse ou arrastar | `--cv-p-accent` |
421
+ | `--cv-p-tick` | tiques de enviada/entregue na bolha própria | `--cv-p-text-muted` |
422
+ | `--cv-p-tick-read` | tique de lida | `#0369a1` (escuro `#5cc8ff`) |
328
423
  | `--cv-p-channel-app` | cor do selo do canal app | o destaque |
329
424
  | `--cv-p-channel-whatsapp` | cor do selo do canal WhatsApp | `#20914f` |
330
425
 
@@ -1,6 +1,6 @@
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
+ import { M as MessagePayload, a as MessageTranscription, C as ConversationChannel, Q as QuickReply, b as QuickReplyInput, c as QuickReplyAttachment, S as StoredAttachmentSendResult, I as InteractiveSelection } from './quickReply.types-DzWH3NYV.js';
4
4
  import { InteractiveReplyOption, InboundMediaType } from '@adatechnology/meta-whatsapp-contracts/testing';
5
5
 
6
6
  type ResolveMediaUrl = (message: MessagePayload) => Promise<string | null>;
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as react from 'react';
2
2
  import react__default, { ReactNode, CSSProperties, UIEvent, FormEvent, RefObject, KeyboardEvent } from 'react';
3
- import { M as MessagePayload, I as InteractiveSelection, b as MessageTranscription, C as ConversationChannel, e as InteractivePayload, f as ConversationsFeatures, Q as QuickReply$1, T as TranscriptionMode, g as QueuedAttachment, c as QuickReplyInput, d as QuickReplyAttachment, h as ConversationVariable, i as ChannelFilter, j as ChannelFilterOption } from './quickReply.types-B4gmZsSr.js';
4
- export { k as CHANNEL_CAPABILITIES, l as CHANNEL_FILTER_ALL, m as CONVERSATION_CHANNEL, n as ChannelCapabilities, a as ConversationsTheme, o as ConversationsUIConfig, D as DEFAULT_CONVERSATION_CHANNEL, F as FormatContactHandleParams, H as HANDLE_KIND, p as HandleKind, q as InteractiveOption, r as InteractiveSection, s as QUICK_REPLY_ATTACHMENT_LIMIT, R as REOPEN_MECHANISM, t as ReopenMechanism, S as StoredAttachmentSendResult, u as TranscriptionStatus, v as capabilitiesOf, w as channelFiltersFor, x as contactFlag, y as formatContactHandle } from './quickReply.types-B4gmZsSr.js';
5
- import { R as ResolveMediaUrl, C as ConversationSummary, L as ListConversationsParams, b as ConversationsApi, S as SSEProvider, Q as ListDocumentsParams, d as ConversationDocument, U as ConversationTemplate, k as ConversationSimulatorClient, m as ConversationSimulatorPanelLabels, P as PreviewUploadedMedia } from './ConversationSimulatorPanel-CrfFjiWh.js';
6
- export { A as AudioRecorderButton, g as AudioRecorderButtonLabels, h as AudioRecorderButtonProps, V as CompanyDocument, W as CompanyDocumentPage, X as ConversationDocumentPage, a as ConversationEventSource, c as ConversationPage, D as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS, Y as DEFAULT_MAX_RECORDING_MILLISECONDS, Z as MediaRenderer, _ as MediaRendererProps, y as SendSimulatorMediaParams, z as SimulatorMediaKind } from './ConversationSimulatorPanel-CrfFjiWh.js';
3
+ import { M as MessagePayload, I as InteractiveSelection, a as MessageTranscription, C as ConversationChannel, e as InteractivePayload, f as ConversationsFeatures, Q as QuickReply$1, T as TranscriptionMode, g as QueuedAttachment, b as QuickReplyInput, c as QuickReplyAttachment, h as ConversationVariable, i as ChannelFilter, j as ChannelFilterOption } from './quickReply.types-DzWH3NYV.js';
4
+ export { k as CHANNEL_CAPABILITIES, l as CHANNEL_FILTER_ALL, m as CONVERSATION_CHANNEL, n as ChannelCapabilities, d as ConversationsTheme, o as ConversationsUIConfig, D as DEFAULT_CONVERSATION_CHANNEL, F as FormatContactHandleParams, H as HANDLE_KIND, p as HandleKind, q as InteractiveOption, r as InteractiveSection, s as QUICK_REPLY_ATTACHMENT_LIMIT, R as REOPEN_MECHANISM, t as ReopenMechanism, S as StoredAttachmentSendResult, u as TranscriptionStatus, v as capabilitiesOf, w as channelFiltersFor, x as contactFlag, y as formatContactHandle } from './quickReply.types-DzWH3NYV.js';
5
+ import { R as ResolveMediaUrl, C as ConversationSummary, L as ListConversationsParams, b as ConversationsApi, S as SSEProvider, Q as ListDocumentsParams, d as ConversationDocument, U as ConversationTemplate, k as ConversationSimulatorClient, m as ConversationSimulatorPanelLabels, P as PreviewUploadedMedia } from './ConversationSimulatorPanel-q-wVgBCN.js';
6
+ export { A as AudioRecorderButton, g as AudioRecorderButtonLabels, h as AudioRecorderButtonProps, V as CompanyDocument, W as CompanyDocumentPage, X as ConversationDocumentPage, a as ConversationEventSource, c as ConversationPage, D as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS, Y as DEFAULT_MAX_RECORDING_MILLISECONDS, Z as MediaRenderer, _ as MediaRendererProps, y as SendSimulatorMediaParams, z as SimulatorMediaKind } from './ConversationSimulatorPanel-q-wVgBCN.js';
7
7
  import { ChannelCapability } from '@adatechnology/conversation-contracts';
8
8
  export { ChannelCapability } from '@adatechnology/conversation-contracts';
9
9
  import '@adatechnology/meta-whatsapp-contracts/testing';
@@ -1,8 +1,8 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { ParticipantConversationSummary, ParticipantSubjectRef, ParticipantConversationPage, ParticipantMessage, ParticipantAttachment } from '@adatechnology/conversation-contracts';
3
+ import { ParticipantConversationSummary, ParticipantConversationPage, ParticipantSubjectRef, ParticipantMessage, ParticipantAttachment } from '@adatechnology/conversation-contracts';
4
4
  export { ParticipantAttachment, ParticipantConversationPage, ParticipantConversationSummary, ParticipantMessage, ParticipantSubjectRef } from '@adatechnology/conversation-contracts';
5
- import { C as ConversationChannel, a as ConversationsTheme, Q as QuickReply } from '../quickReply.types-B4gmZsSr.js';
5
+ import { C as ConversationChannel, d as ConversationsTheme, Q as QuickReply } from '../quickReply.types-DzWH3NYV.js';
6
6
 
7
7
  type ParticipantAuthorAvatarRenderer = (author: {
8
8
  name: string | null;
@@ -198,6 +198,48 @@ type ParticipantConversationsProps = {
198
198
  };
199
199
  declare function ParticipantConversations(props: ParticipantConversationsProps): react.JSX.Element;
200
200
 
201
+ /** What one conversation needs from the host: no list, no open-by-subject. `markRead` and `subscribe` are optional. */
202
+ type ConversationThreadApi = Pick<ParticipantConversationsApi, 'fetchMessages' | 'sendMessage' | 'resolveAttachmentUrl'> & Partial<Pick<ParticipantConversationsApi, 'markRead' | 'subscribe'>>;
203
+
204
+ /** Who is looking at the conversation. `participant` is the outside party; `operator` is the company. */
205
+ type ParticipantPerspective = 'participant' | 'operator';
206
+
207
+ type ConversationThreadProps = {
208
+ readonly api: ConversationThreadApi;
209
+ readonly subject: ParticipantSubjectRef;
210
+ /** `participant` (default) is the outside party; `operator` is the company, which owns the outbound messages. */
211
+ readonly perspective?: ParticipantPerspective;
212
+ readonly title: string;
213
+ readonly protocol?: string;
214
+ readonly channels?: ParticipantConversationSummary['channels'];
215
+ /** `closed` removes the composer and shows `labels.closedNotice`. */
216
+ readonly status?: 'open' | 'closed';
217
+ /** Short line under the title, written by the host (for example "Participant: Ana"). */
218
+ readonly counterpartLabel?: string;
219
+ /** Slot at the end of the header bar for the host's own actions (close, reopen...). The package knows none of them. */
220
+ readonly headerActions?: ReactNode;
221
+ /** Absent draws no back arrow. */
222
+ readonly onBack?: () => void;
223
+ readonly renderSubjectIcon?: ParticipantSubjectIconRenderer;
224
+ readonly subjectGroups?: readonly ParticipantSubjectGroup[];
225
+ /** Chips above the field: tapping one fills the text, it never sends. */
226
+ readonly quickReplies?: readonly QuickReply[];
227
+ readonly labels?: Partial<ParticipantConversationsLabels>;
228
+ readonly theme?: ConversationsTheme;
229
+ readonly className?: string;
230
+ readonly locale?: string;
231
+ readonly channel?: ConversationChannel;
232
+ readonly pendingMessages?: readonly ParticipantPendingMessage[];
233
+ readonly onRetryPending?: (clientMessageId: string) => void;
234
+ /** Tells the host the other side's messages were marked read, to refresh its own badges. */
235
+ readonly onMarkedRead?: (subject: ParticipantSubjectRef) => void;
236
+ readonly tail?: boolean;
237
+ readonly avatars?: 'initials';
238
+ readonly renderAuthorAvatar?: ParticipantAuthorAvatarRenderer;
239
+ };
240
+ /** One conversation about one subject, drawn like the participant app and seen from either side. */
241
+ declare function ConversationThread(props: ConversationThreadProps): react.JSX.Element;
242
+
201
243
  declare function groupParticipantConversations(params: GroupParticipantConversationsParams): ParticipantInboxView;
202
244
 
203
- export { DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, type GroupParticipantConversationsParams, type ParticipantAuthorAvatarRenderer, type ParticipantConversationEvent, ParticipantConversations, type ParticipantConversationsApi, type ParticipantConversationsClassNames, type ParticipantConversationsLabels, type ParticipantConversationsProps, type ParticipantInboxSection, type ParticipantInboxView, type ParticipantLocalPendingMessage, type ParticipantPendingMessage, type ParticipantSendInput, type ParticipantSendResult, type ParticipantSubjectGroup, type ParticipantSubjectIconRenderer, groupParticipantConversations };
245
+ export { ConversationThread, type ConversationThreadApi, type ConversationThreadProps, DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, type GroupParticipantConversationsParams, type ParticipantAuthorAvatarRenderer, type ParticipantConversationEvent, ParticipantConversations, type ParticipantConversationsApi, type ParticipantConversationsClassNames, type ParticipantConversationsLabels, type ParticipantConversationsProps, type ParticipantInboxSection, type ParticipantInboxView, type ParticipantLocalPendingMessage, type ParticipantPendingMessage, type ParticipantPerspective, type ParticipantSendInput, type ParticipantSendResult, type ParticipantSubjectGroup, type ParticipantSubjectIconRenderer, groupParticipantConversations };