@adatechnology/conversations-ui 0.1.0-rc.4 → 0.1.0-rc.41

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 (149) hide show
  1. package/dist/ConversationSimulatorPanel--5fIzXWY.d.ts +804 -0
  2. package/dist/chunk-BJNRLLDO.js +2708 -0
  3. package/dist/chunk-DKPXKQGC.js +110 -0
  4. package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
  5. package/dist/flows/index.d.ts +422 -5
  6. package/dist/flows/index.js +2389 -678
  7. package/dist/index.d.ts +1171 -42
  8. package/dist/index.js +3755 -843
  9. package/dist/preview/index.d.ts +157 -42
  10. package/dist/preview/index.js +772 -191
  11. package/dist/styles.css +893 -0
  12. package/package.json +9 -8
  13. package/src/AudioPlayer.tsx +8 -0
  14. package/src/AudioRecorderButton.test.tsx +30 -0
  15. package/src/AudioRecorderButton.tsx +248 -0
  16. package/src/AudioTranscription.test.tsx +115 -0
  17. package/src/AudioTranscription.tsx +252 -0
  18. package/src/Avatar.tsx +14 -3
  19. package/src/ConversationContextPanel.tsx +218 -44
  20. package/src/ConversationDocumentsPanel.tsx +347 -24
  21. package/src/ConversationHeader.test.tsx +66 -0
  22. package/src/ConversationHeader.tsx +163 -45
  23. package/src/ConversationListItem.tsx +19 -2
  24. package/src/ConversationLocalesProvider.tsx +28 -0
  25. package/src/ConversationRow.tsx +31 -7
  26. package/src/DarkModeToggle.test.tsx +76 -0
  27. package/src/DarkModeToggle.tsx +92 -0
  28. package/src/DocumentsLibrary.tsx +382 -0
  29. package/src/EmojiPicker.tsx +70 -55
  30. package/src/FileIcon.test.ts +83 -0
  31. package/src/FileIcon.tsx +88 -11
  32. package/src/InteractiveMessage.test.tsx +41 -0
  33. package/src/InteractiveMessage.tsx +146 -0
  34. package/src/Lightbox.tsx +18 -3
  35. package/src/MediaRenderer.tsx +92 -16
  36. package/src/MessageBubble.test.tsx +41 -0
  37. package/src/MessageBubble.tsx +75 -6
  38. package/src/MessageComposer.test.tsx +35 -0
  39. package/src/MessageComposer.tsx +155 -19
  40. package/src/RichMessageComposer.test.tsx +113 -0
  41. package/src/RichMessageComposer.tsx +551 -0
  42. package/src/SimpleEmojiPicker.tsx +5 -3
  43. package/src/StatusTicks.tsx +1 -1
  44. package/src/Toast.tsx +4 -0
  45. package/src/Tooltip.test.ts +42 -0
  46. package/src/Tooltip.tsx +167 -0
  47. package/src/Wallpaper.test.tsx +21 -0
  48. package/src/Wallpaper.tsx +67 -7
  49. package/src/WhatsAppMessageEditor.tsx +34 -7
  50. package/src/WindowExpiredNotice.tsx +12 -4
  51. package/src/audioRecorderFormat.test.ts +67 -0
  52. package/src/buildOutput.test.ts +79 -0
  53. package/src/composer.constant.ts +33 -0
  54. package/src/conversationTranscript.test.ts +57 -0
  55. package/src/conversationTranscript.ts +29 -4
  56. package/src/conversationWindow.ts +7 -5
  57. package/src/documentTypeLabel.test.ts +57 -0
  58. package/src/documents/DocumentsWorkspace.tsx +550 -0
  59. package/src/documents/index.ts +8 -0
  60. package/src/documents/labels.ts +92 -0
  61. package/src/emojiCatalog.test.ts +35 -0
  62. package/src/emojiCatalog.ts +189 -0
  63. package/src/flows/FlowConnectionEdge.tsx +104 -0
  64. package/src/flows/FlowGroupHeader.tsx +12 -2
  65. package/src/flows/FlowLegend.tsx +125 -0
  66. package/src/flows/FlowMapCanvas.tsx +15 -12
  67. package/src/flows/FlowMapNode.tsx +4 -1
  68. package/src/flows/FlowNodeCard.tsx +219 -34
  69. package/src/flows/FlowNodePanel.tsx +153 -39
  70. package/src/flows/FlowPalette.tsx +106 -69
  71. package/src/flows/FlowPortalNode.tsx +1 -1
  72. package/src/flows/FlowWhatsAppPreview.tsx +14 -3
  73. package/src/flows/FlowsWorkspace.tsx +1219 -0
  74. package/src/flows/flowCanvasModel.test.ts +456 -0
  75. package/src/flows/flowCanvasModel.ts +378 -0
  76. package/src/flows/flowEditorOps.test.ts +276 -0
  77. package/src/flows/flowEditorOps.ts +202 -0
  78. package/src/flows/flowGraph.ts +78 -53
  79. package/src/flows/index.ts +51 -2
  80. package/src/flows/labels.ts +180 -0
  81. package/src/flows/workspaceContract.test.ts +126 -0
  82. package/src/hooks/useContainerWidth.ts +35 -0
  83. package/src/hooks/useConversationActions.ts +56 -0
  84. package/src/hooks/useConversationDocuments.ts +11 -7
  85. package/src/hooks/useConversationList.ts +15 -9
  86. package/src/hooks/useConversationMessages.ts +2 -2
  87. package/src/hooks/useConversationRealtime.ts +10 -8
  88. package/src/hooks/useScrollToLatestMessage.ts +127 -0
  89. package/src/hooks/useUrlFilterState.ts +107 -0
  90. package/src/icon.constant.ts +12 -0
  91. package/src/index.ts +129 -13
  92. package/src/lib/cn.test.ts +29 -0
  93. package/src/lib/composer-formatting.test.ts +78 -0
  94. package/src/lib/composer-formatting.ts +145 -0
  95. package/src/lib/createMediaUrlResolver.ts +33 -0
  96. package/src/lib/paginated.test.ts +33 -0
  97. package/src/lib/paginated.ts +26 -0
  98. package/src/lib/whatsapp-formatting.test.tsx +37 -0
  99. package/src/lib/whatsapp-formatting.tsx +28 -3
  100. package/src/listing/index.tsx +202 -0
  101. package/src/pagination.constant.ts +10 -0
  102. package/src/preview/ConversationPreview.tsx +225 -17
  103. package/src/preview/ConversationSimulatorClient.ts +143 -0
  104. package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
  105. package/src/preview/ConversationSimulatorPanel.tsx +131 -0
  106. package/src/preview/MediaTypesPreview.tsx +87 -0
  107. package/src/preview/conversationPreviewFailures.test.ts +64 -0
  108. package/src/preview/createMockConversationsApi.ts +175 -15
  109. package/src/preview/createPreviewBridgeClient.test.ts +92 -0
  110. package/src/preview/createPreviewBridgeClient.ts +124 -0
  111. package/src/preview/createPreviewMediaUploader.ts +82 -0
  112. package/src/preview/createPreviewWebhookClient.test.ts +96 -0
  113. package/src/preview/createPreviewWebhookClient.ts +127 -4
  114. package/src/preview/index.ts +51 -3
  115. package/src/preview/mediaTypeOf.test.ts +15 -0
  116. package/src/preview/mockDocumentsSearch.test.ts +57 -0
  117. package/src/preview/preview.test.ts +5 -3
  118. package/src/preview/previewFileSamples.test.ts +151 -0
  119. package/src/preview/previewFileSamples.ts +74 -0
  120. package/src/preview/previewFixtures.ts +288 -1
  121. package/src/preview/previewMediaSource.test.ts +62 -0
  122. package/src/preview/previewMediaSource.ts +91 -0
  123. package/src/preview/previewMediaUploader.test.ts +61 -0
  124. package/src/providers/ConversationsProvider.tsx +8 -6
  125. package/src/providers/types.ts +185 -10
  126. package/src/quickReply.test.ts +58 -0
  127. package/src/settings/MessagesWorkspace.tsx +571 -0
  128. package/src/settings/TopicsForm.tsx +2 -0
  129. package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
  130. package/src/settings/TranscriptionSettingsForm.tsx +190 -0
  131. package/src/settings/WelcomeFarewellForm.tsx +1 -0
  132. package/src/settings/WhatsAppCreateTemplateForm.tsx +4 -1
  133. package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
  134. package/src/settings/WhatsAppTemplatesSettings.test.tsx +61 -0
  135. package/src/settings/WhatsAppTemplatesSettings.tsx +22 -2
  136. package/src/styles.css +858 -0
  137. package/src/types.ts +64 -1
  138. package/src/useWaitingNotifications.ts +74 -29
  139. package/src/workspace/BulkTemplateModal.tsx +132 -0
  140. package/src/workspace/ConversationPane.tsx +432 -0
  141. package/src/workspace/ConversationsInboxList.tsx +194 -0
  142. package/src/workspace/ConversationsWorkspace.tsx +423 -0
  143. package/src/workspace/index.ts +17 -0
  144. package/src/workspace/labels.test.ts +17 -0
  145. package/src/workspace/labels.ts +85 -0
  146. package/src/workspace/useConversationsInbox.ts +332 -0
  147. package/dist/chunk-4R6Y43DQ.js +0 -726
  148. package/dist/chunk-NV2RZ5KT.js +0 -56
  149. package/dist/types-C0PtaO7S.d.ts +0 -207
@@ -0,0 +1,127 @@
1
+ import { useCallback, useLayoutEffect, useRef, useState, type RefObject, type UIEvent } from 'react'
2
+
3
+ /**
4
+ * Mantém a conversa no fim, como todo mensageiro.
5
+ *
6
+ * Abrir uma conversa no topo do histórico é errado por um motivo simples: o que interessa ao
7
+ * atendente é a última mensagem, e ele teria que rolar por semanas de conversa para chegar nela.
8
+ *
9
+ * Existe no pacote, e não em cada host, porque a regra tem duas sutilezas que se descobre só
10
+ * errando — o salto instantâneo na troca de conversa e o respeito a quem rolou para trás — e
11
+ * reimplementá-las em cada inbox garante que uma delas fique de fora.
12
+ */
13
+
14
+ /**
15
+ * Distância do fim, em pixels, dentro da qual ainda consideramos o operador "acompanhando".
16
+ *
17
+ * Não é zero porque o navegador arredonda `scrollTop` fracionário em telas com zoom ou densidade
18
+ * alta: exigir o fim exato faria o painel achar que o operador rolou para trás sem ele ter tocado
19
+ * em nada, e a próxima mensagem não apareceria.
20
+ */
21
+ const NEAR_BOTTOM_THRESHOLD_PX = 120
22
+
23
+ /**
24
+ * Toda rolagem aqui é `'auto'` — nunca `'smooth'`.
25
+ *
26
+ * Tentamos suave para mensagem nova e medimos: em ambiente onde a rolagem suave está desligada, o
27
+ * `scrollTo({ behavior: 'smooth' })` **não faz nada e não avisa** — a mensagem nova simplesmente não
28
+ * entra na vista. E não dá para detectar isso pelo `prefers-reduced-motion`: no navegador em que
29
+ * reproduzimos, a media query respondia `false` e o smooth continuava sendo no-op.
30
+ *
31
+ * Trocar uma animação cosmética por garantia de que o operador vê a mensagem é barato: seguir uma
32
+ * mensagem nova salta a altura de uma bolha, que é quase imperceptível de qualquer forma.
33
+ */
34
+ const SCROLL_BEHAVIOR: ScrollBehavior = 'auto'
35
+
36
+ export type UseScrollToLatestMessageParams = {
37
+ /** Troca de conversa. Muda ⇒ salto instantâneo para o fim. */
38
+ readonly conversationId: string | undefined
39
+ /** Quantidade de mensagens carregadas. Cresce ⇒ acompanha o fim, se o operador estiver lá. */
40
+ readonly messageCount: number
41
+ }
42
+
43
+ export type UseScrollToLatestMessageResult = {
44
+ /** Vai no elemento que rola — tipicamente o `ConversationWallpaper`. */
45
+ readonly containerRef: RefObject<HTMLDivElement | null>
46
+ /** Ligue no `onScroll` do mesmo elemento: é o que detecta o operador lendo o histórico. */
47
+ readonly handleScroll: (event: UIEvent<HTMLDivElement>) => void
48
+ /** `true` quando o operador rolou para trás — serve a um botão "ir para a última". */
49
+ readonly isAwayFromBottom: boolean
50
+ readonly scrollToBottom: (behavior?: ScrollBehavior) => void
51
+ }
52
+
53
+ export function useScrollToLatestMessage({
54
+ conversationId,
55
+ messageCount,
56
+ }: UseScrollToLatestMessageParams): UseScrollToLatestMessageResult {
57
+ const containerRef = useRef<HTMLDivElement | null>(null)
58
+ const [isAwayFromBottom, setIsAwayFromBottom] = useState(false)
59
+
60
+ /**
61
+ * Espelha `isAwayFromBottom` para o efeito de mensagens novas ler o valor atual sem depender dele.
62
+ *
63
+ * Se o efeito dependesse do estado, cada rolagem do operador o re-disparava e o puxava de volta
64
+ * para o fim — exatamente o que a checagem existe para evitar.
65
+ */
66
+ const isAwayFromBottomRef = useRef(false)
67
+
68
+ const scrollToBottom = useCallback((behavior: ScrollBehavior = SCROLL_BEHAVIOR) => {
69
+ const container = containerRef.current
70
+ if (!container) return
71
+ container.scrollTo({ top: container.scrollHeight, behavior })
72
+ }, [])
73
+
74
+ const handleScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
75
+ const target = event.currentTarget
76
+ const distanceFromBottom = target.scrollHeight - target.scrollTop - target.clientHeight
77
+ const away = distanceFromBottom > NEAR_BOTTOM_THRESHOLD_PX
78
+
79
+ isAwayFromBottomRef.current = away
80
+ setIsAwayFromBottom((current) => (current === away ? current : away))
81
+ }, [])
82
+
83
+ /**
84
+ * Qual conversa já recebeu o salto de abertura.
85
+ *
86
+ * É o que separa "abriu a conversa" de "chegou mensagem", e não dá para usar só `conversationId`
87
+ * num efeito próprio: quando a conversa troca, a lista de mensagens ainda está vazia, então um
88
+ * salto ali rola um container sem conteúdo e não faz nada. O salto real precisa esperar a primeira
89
+ * leva de mensagens — e foi exatamente isso que deixou a conversa abrindo no topo.
90
+ */
91
+ const jumpedForConversationRef = useRef<string | undefined>(undefined)
92
+
93
+ useLayoutEffect(() => {
94
+ const isNewConversation = jumpedForConversationRef.current !== conversationId
95
+
96
+ if (isNewConversation) {
97
+ // Zera antes de qualquer coisa: "rolado para trás" da conversa anterior não pode bloquear o
98
+ // salto de abertura desta.
99
+ isAwayFromBottomRef.current = false
100
+ setIsAwayFromBottom(false)
101
+
102
+ // Sem mensagens ainda — o salto acontece quando a primeira leva chegar.
103
+ if (messageCount === 0) return
104
+
105
+ jumpedForConversationRef.current = conversationId
106
+ /**
107
+ * `'auto'`, sempre. Animar a rolagem por meses de histórico demora, mostra um borrão de
108
+ * mensagens antigas que ninguém pediu, e some por completo onde a rolagem suave está desligada
109
+ * (`prefers-reduced-motion`, alguns navegadores automatizados) — a conversa simplesmente
110
+ * abriria no topo. Abertura é salto, não animação.
111
+ */
112
+ scrollToBottom(SCROLL_BEHAVIOR)
113
+ return
114
+ }
115
+
116
+ /**
117
+ * Mensagem nova: acompanha, mas só se o operador já estava no fim.
118
+ *
119
+ * Puxar a rolagem de quem está lendo o histórico é pior do que não mostrar a mensagem — ele perde
120
+ * a posição e não sabe por quê. Quem rolou para trás recebe `isAwayFromBottom` e decide.
121
+ */
122
+ if (isAwayFromBottomRef.current) return
123
+ scrollToBottom(SCROLL_BEHAVIOR)
124
+ }, [conversationId, messageCount, scrollToBottom])
125
+
126
+ return { containerRef, handleScroll, isAwayFromBottom, scrollToBottom }
127
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Estado de listagem espelhado na URL — ordenação, filtros e paginação (regra `web.md` §7).
3
+ *
4
+ * Escrito sobre `history.replaceState` e não sobre um router: o pacote roda em três produtos com
5
+ * routers diferentes, e exigir um deles arrastaria dependência de framework para dentro do módulo.
6
+ * `replaceState` também é o comportamento certo aqui — filtrar não é navegar, e cada tecla digitada
7
+ * na busca não deve virar uma entrada no botão "voltar".
8
+ */
9
+
10
+ import { useCallback, useEffect, useState } from 'react'
11
+
12
+ const LIST_SEPARATOR = ','
13
+
14
+ function readParams(): URLSearchParams {
15
+ if (typeof window === 'undefined') return new URLSearchParams()
16
+ return new URLSearchParams(window.location.search)
17
+ }
18
+
19
+ function writeParam(key: string, value: string | undefined): void {
20
+ if (typeof window === 'undefined') return
21
+ const params = readParams()
22
+ if (value === undefined || value === '') params.delete(key)
23
+ else params.set(key, value)
24
+ const query = params.toString()
25
+ window.history.replaceState(null, '', query ? `${window.location.pathname}?${query}` : window.location.pathname)
26
+ }
27
+
28
+ /** `enabled: false` mantém o mesmo contrato de estado sem tocar na URL — para uso em preview e teste. */
29
+ export interface UrlStateOptions {
30
+ readonly enabled?: boolean
31
+ }
32
+
33
+ export function useUrlStringState(
34
+ key: string,
35
+ initial: string,
36
+ { enabled = true }: UrlStateOptions = {},
37
+ ): [string, (next: string) => void] {
38
+ const [value, setValue] = useState(() => (enabled ? (readParams().get(key) ?? initial) : initial))
39
+
40
+ const update = useCallback(
41
+ (next: string) => {
42
+ setValue(next)
43
+ if (enabled) writeParam(key, next === initial ? undefined : next)
44
+ },
45
+ [enabled, key, initial],
46
+ )
47
+
48
+ return [value, update]
49
+ }
50
+
51
+ export function useUrlNumberState(
52
+ key: string,
53
+ initial: number,
54
+ { enabled = true }: UrlStateOptions = {},
55
+ ): [number, (next: number) => void] {
56
+ const [value, setValue] = useState(() => {
57
+ if (!enabled) return initial
58
+ const raw = Number(readParams().get(key))
59
+ return Number.isFinite(raw) && raw > 0 ? raw : initial
60
+ })
61
+
62
+ const update = useCallback(
63
+ (next: number) => {
64
+ setValue(next)
65
+ if (enabled) writeParam(key, next === initial ? undefined : String(next))
66
+ },
67
+ [enabled, key, initial],
68
+ )
69
+
70
+ return [value, update]
71
+ }
72
+
73
+ export function useUrlArrayState(
74
+ key: string,
75
+ { enabled = true }: UrlStateOptions = {},
76
+ ): [readonly string[], (next: readonly string[]) => void] {
77
+ const [value, setValue] = useState<readonly string[]>(() => {
78
+ if (!enabled) return []
79
+ const raw = readParams().get(key)
80
+ return raw ? raw.split(LIST_SEPARATOR).filter(Boolean) : []
81
+ })
82
+
83
+ const update = useCallback(
84
+ (next: readonly string[]) => {
85
+ setValue(next)
86
+ if (enabled) writeParam(key, next.length > 0 ? next.join(LIST_SEPARATOR) : undefined)
87
+ },
88
+ [enabled, key],
89
+ )
90
+
91
+ return [value, update]
92
+ }
93
+
94
+ /**
95
+ * Espera o usuário parar de digitar antes de deixar o valor chegar à query. Sem isto, cada tecla
96
+ * na busca vira uma chamada de rede e uma reescrita de URL.
97
+ */
98
+ export function useDebouncedValue<TValue>(value: TValue, delayMs = 300): TValue {
99
+ const [debounced, setDebounced] = useState(value)
100
+
101
+ useEffect(() => {
102
+ const timer = setTimeout(() => setDebounced(value), delayMs)
103
+ return () => clearTimeout(timer)
104
+ }, [value, delayMs])
105
+
106
+ return debounced
107
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Tamanhos de ícone da UI de conversas, em px, na escala de 4 da grade.
3
+ *
4
+ * Ícone vem da biblioteca (lucide), nunca emoji. Emoji desenha diferente em cada sistema
5
+ * operacional, não herda `currentColor` — então não acompanha o estado do controle (ativo,
6
+ * desabilitado, variante destrutiva) — e não escala com o token de tipografia. Numa mesma faixa,
7
+ * metade emoji e metade SVG lê como duas famílias de botão.
8
+ */
9
+
10
+ export const ICON_SIZE_PILL = 12
11
+ export const ICON_SIZE_INLINE = 14
12
+ export const ICON_SIZE_ACTION = 16
package/src/index.ts CHANGED
@@ -1,18 +1,37 @@
1
1
  export { MessageBubble } from './MessageBubble'
2
+ export { InteractiveMessage, DEFAULT_INTERACTIVE_MESSAGE_LABELS } from './InteractiveMessage'
2
3
  export { ConversationWallpaper } from './Wallpaper'
3
4
  export { ConversationLocalesProvider, useConversationLocales } from './ConversationLocalesProvider'
4
5
  export { AudioPlayer } from './AudioPlayer'
5
- export { EmojiPicker } from './EmojiPicker'
6
- export { MessageComposer } from './MessageComposer'
7
- export { WhatsAppMessageEditor } from './WhatsAppMessageEditor'
6
+ export { AudioTranscription } from './AudioTranscription'
7
+ export { EmojiPicker, DEFAULT_EMOJI_PICKER_LABELS } from './EmojiPicker'
8
+ export { EMOJI_CATEGORIES, searchEmojis } from './emojiCatalog'
9
+ export { MessageComposer, DEFAULT_MESSAGE_COMPOSER_LABELS, DEFAULT_ACCEPTED_FILE_TYPES } from './MessageComposer'
10
+ export { RichMessageComposer, RICH_COMPOSER_ACTION, DEFAULT_RICH_COMPOSER_TOOLTIPS } from './RichMessageComposer'
11
+ export type {
12
+ RichMessageComposerProps,
13
+ RichComposerAction,
14
+ RichComposerTooltips,
15
+ RichComposerQuickReply,
16
+ RichComposerVariable,
17
+ RichMessageComposerHandle,
18
+ } from './RichMessageComposer'
19
+ export {
20
+ AudioRecorderButton,
21
+ DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
22
+ DEFAULT_MAX_RECORDING_MILLISECONDS,
23
+ } from './AudioRecorderButton'
24
+ export type { AudioRecorderButtonProps, AudioRecorderButtonLabels } from './AudioRecorderButton'
25
+ export { WhatsAppMessageEditor, DEFAULT_WHATSAPP_MESSAGE_EDITOR_LABELS } from './WhatsAppMessageEditor'
8
26
  export { SimpleEmojiPicker } from './SimpleEmojiPicker'
9
27
  export { DateDivider } from './DateDivider'
10
- export { Avatar } from './Avatar'
11
- export { ConversationListItem } from './ConversationListItem'
28
+ export { Avatar, DEFAULT_AVATAR_LABELS } from './Avatar'
29
+ export { ConversationListItem, DEFAULT_CONVERSATION_LIST_ITEM_LABELS } from './ConversationListItem'
12
30
  export { ToastProvider, useToast, toast } from './Toast'
31
+ export { TooltipLayer, TOOLTIP_ATTRIBUTE } from './Tooltip'
13
32
 
14
33
  export { StatusTicks } from './StatusTicks'
15
- export { Lightbox } from './Lightbox'
34
+ export { Lightbox, DEFAULT_LIGHTBOX_LABELS } from './Lightbox'
16
35
  export { MediaRenderer } from './MediaRenderer'
17
36
  export { FileIcon } from './FileIcon'
18
37
  export { MessageText } from './MessageText'
@@ -60,11 +79,34 @@ export { ConversationContextPanel, DEFAULT_CONVERSATION_CONTEXT_LABELS } from '.
60
79
  export type {
61
80
  ConversationContextPanelProps,
62
81
  ConversationContextEntry,
82
+ ConversationContextStatus,
63
83
  ConversationContextPanelLabels,
64
84
  ConversationContextPanelClassNames,
65
85
  } from './ConversationContextPanel'
66
86
  export { WindowExpiredNotice, isWindowBlocking, DEFAULT_WINDOW_EXPIRED_LABELS } from './WindowExpiredNotice'
67
87
  export type { WindowExpiredNoticeProps, WindowExpiredNoticeLabels } from './WindowExpiredNotice'
88
+ export { DocumentsLibrary, DEFAULT_DOCUMENTS_LIBRARY_LABELS } from './DocumentsLibrary'
89
+ export type { DocumentsLibraryProps, DocumentsLibraryLabels, DocumentsLibraryClassNames } from './DocumentsLibrary'
90
+ export { DocumentsWorkspace, DEFAULT_DOCUMENTS_WORKSPACE_LABELS } from './documents'
91
+ export type {
92
+ DocumentsWorkspaceProps,
93
+ DocumentsWorkspaceLabels,
94
+ DocumentsWorkspaceClassNames,
95
+ DocumentsFiltersContext,
96
+ } from './documents'
97
+ export { SortableHead, MultiSelectFilter, BulkActionBar, ListingPagination } from './listing'
98
+ export type {
99
+ SortableHeadProps,
100
+ MultiSelectFilterProps,
101
+ BulkActionBarProps,
102
+ ListingPaginationProps,
103
+ FilterOption,
104
+ SortDirection,
105
+ } from './listing'
106
+ export { useUrlStringState, useUrlNumberState, useUrlArrayState, useDebouncedValue } from './hooks/useUrlFilterState'
107
+ export type { UrlStateOptions } from './hooks/useUrlFilterState'
108
+ export { DOCUMENT_SOURCE_FILTER } from './ConversationDocumentsPanel'
109
+ export type { DocumentSourceFilter } from './ConversationDocumentsPanel'
68
110
  export type { ConversationDocumentsPanelClassNames } from './ConversationDocumentsPanel'
69
111
  export { ConversationDocumentsPanel, DEFAULT_CONVERSATION_DOCUMENTS_LABELS } from './ConversationDocumentsPanel'
70
112
  export type { ConversationDocumentsPanelProps, ConversationDocumentsPanelLabels } from './ConversationDocumentsPanel'
@@ -72,8 +114,15 @@ export { buildTranscriptText, buildTranscriptFilename, downloadTextFile } from '
72
114
  export type { BuildTranscriptTextParams } from './conversationTranscript'
73
115
 
74
116
  export { useDarkMode, useIsDarkTheme } from './useDarkMode'
117
+ export { DarkModeToggle, DEFAULT_DARK_MODE_TOGGLE_LABELS } from './DarkModeToggle'
118
+ export type { DarkModeToggleProps, DarkModeToggleLabels } from './DarkModeToggle'
75
119
  export { useIsNarrow, NARROW_MAX_WIDTH_PX } from './useIsNarrow'
76
120
  export { useWaitingNotifications } from './useWaitingNotifications'
121
+ export type {
122
+ UseWaitingNotificationsLabels,
123
+ UseWaitingNotificationsParams,
124
+ UseWaitingNotificationsResult,
125
+ } from './useWaitingNotifications'
77
126
 
78
127
  export { ConversationsProvider, useConversations } from './providers/ConversationsProvider'
79
128
 
@@ -82,6 +131,7 @@ export { ConversationsProvider, useConversations } from './providers/Conversatio
82
131
  export { WhatsAppTemplateSettingsForm } from './settings/WhatsAppTemplateSettingsForm'
83
132
  export { WhatsAppCreateTemplateForm } from './settings/WhatsAppCreateTemplateForm'
84
133
  export { WelcomeFarewellForm } from './settings/WelcomeFarewellForm'
134
+ export { TranscriptionSettingsForm } from './settings/TranscriptionSettingsForm'
85
135
  export {
86
136
  WhatsAppTemplatesSettings,
87
137
  TEMPLATE_SETTINGS_TAB,
@@ -89,40 +139,70 @@ export {
89
139
  } from './settings/WhatsAppTemplatesSettings'
90
140
  export { TopicsForm } from './settings/TopicsForm'
91
141
 
142
+ // Tela composta de Mensagens: junta os formulários acima com abas, estado e salvamento, para o
143
+ // host não remontar essa mesma colagem em cada produto (foi assim que eles divergiram).
144
+ export { MessagesWorkspace } from './settings/MessagesWorkspace'
145
+ export type {
146
+ MessagesWorkspaceProps,
147
+ MessagesWorkspaceApi,
148
+ MessagesWorkspaceLabels,
149
+ MessagesWorkspaceTemplateRole,
150
+ BotMessages,
151
+ TemplateSettings,
152
+ TranscriptionSettings,
153
+ } from './settings/MessagesWorkspace'
154
+
92
155
  // Camada headless (T6.9) — hooks de dados/ações independentes de qualquer tela, para o
93
156
  // produto montar sua própria UI sobre eles. Requerem <ConversationsProvider> como ancestral.
94
157
  export { useConversationMessages } from './hooks/useConversationMessages'
158
+ export { useScrollToLatestMessage } from './hooks/useScrollToLatestMessage'
95
159
  export { useConversationList } from './hooks/useConversationList'
96
160
  export { useConversationContext } from './hooks/useConversationContext'
97
161
  export { useConversationDocuments } from './hooks/useConversationDocuments'
98
162
  export { useConversationRealtime, useGlobalRealtime } from './hooks/useConversationRealtime'
163
+ export { useConversationActions, useInboxActions } from './hooks/useConversationActions'
99
164
 
100
165
  export { parseWhatsAppFormatting, waToHTML, htmlToWA, waToHTMLInline } from './lib/whatsapp-formatting'
101
166
  export { formatPhone, phoneInitials } from './lib/phone'
102
- export { formatTimestamp, formatFileSize } from './lib/format'
167
+ // `formatDateTime` e `isSameDay` eram usados pelas bolhas e pelo divisor de data; exportá-los
168
+ // evita que cada host mantenha a própria cópia e acabe com timeline e transcript divergindo.
169
+ export { formatTimestamp, formatFileSize, formatDateTime, isSameDay } from './lib/format'
103
170
 
104
171
  export type { MessagePayload, ConversationsUIConfig, ConversationsTheme, ConversationsFeatures } from './types'
172
+ export type { InteractivePayload, InteractiveSection, InteractiveOption, InteractiveSelection } from './types'
173
+ export type { MessageTranscription, TranscriptionStatus, TranscriptionMode } from './types'
105
174
  export type {
106
175
  ConversationsApi,
107
176
  SSEProvider,
108
177
  ConversationEventSource,
109
178
  ConversationSummary,
110
179
  ConversationDocument,
180
+ ConversationPage,
181
+ ConversationDocumentPage,
182
+ CompanyDocument,
183
+ CompanyDocumentPage,
184
+ ConversationTemplate,
185
+ ListConversationsParams,
186
+ ListDocumentsParams,
111
187
  } from './providers/types'
188
+ export type { UseConversationActionsResult, UseInboxActionsResult } from './hooks/useConversationActions'
112
189
 
113
190
  export type { MessageBubbleProps } from './MessageBubble'
114
191
  export type { ConversationWallpaperProps } from './Wallpaper'
115
192
  export type { ConversationLocales, ConversationLocalesProviderProps } from './ConversationLocalesProvider'
116
193
  export type { AudioPlayerProps } from './AudioPlayer'
117
- export type { EmojiPickerProps } from './EmojiPicker'
118
- export type { MessageComposerProps, MessageComposerClassNames } from './MessageComposer'
119
- export type { WhatsAppMessageEditorProps } from './WhatsAppMessageEditor'
194
+ export type { AudioTranscriptionProps } from './AudioTranscription'
195
+ export type { EmojiPickerProps, EmojiPickerLabels } from './EmojiPicker'
196
+ export type { EmojiEntry, EmojiCategory } from './emojiCatalog'
197
+ export type { InteractiveMessageProps, InteractiveMessageLabels } from './InteractiveMessage'
198
+ export type { MessageComposerProps, MessageComposerClassNames, MessageComposerLabels } from './MessageComposer'
199
+ export type { WhatsAppMessageEditorProps, WhatsAppMessageEditorLabels } from './WhatsAppMessageEditor'
120
200
  export type { SimpleEmojiPickerProps } from './SimpleEmojiPicker'
121
201
  export type { DateDividerProps, DateDividerClassNames } from './DateDivider'
122
- export type { AvatarProps } from './Avatar'
123
- export type { ConversationListItemProps } from './ConversationListItem'
202
+ export type { AvatarProps, AvatarLabels } from './Avatar'
203
+ export type { ConversationListItemProps, ConversationListItemLabels } from './ConversationListItem'
124
204
  export type { StatusTicksProps } from './StatusTicks'
125
- export type { LightboxProps } from './Lightbox'
205
+ export type { LightboxProps, LightboxLabels } from './Lightbox'
126
206
  export type { MediaRendererProps, ResolveMediaUrl } from './MediaRenderer'
127
207
  export type { FileIconProps } from './FileIcon'
128
208
  export type { MessageTextProps } from './MessageText'
@@ -141,6 +221,10 @@ export type {
141
221
  WhatsAppCreateTemplateFormLabels,
142
222
  } from './settings/WhatsAppCreateTemplateForm'
143
223
  export type { WelcomeFarewellFormProps, WelcomeFarewellFormLabels } from './settings/WelcomeFarewellForm'
224
+ export type {
225
+ TranscriptionSettingsFormProps,
226
+ TranscriptionSettingsFormLabels,
227
+ } from './settings/TranscriptionSettingsForm'
144
228
  export type {
145
229
  WhatsAppTemplatesSettingsProps,
146
230
  WhatsAppTemplatesSettingsLabels,
@@ -151,6 +235,38 @@ export type { TopicsFormProps, TopicItem, TopicsFormLabels } from './settings/To
151
235
  export type { UseConversationMessagesResult } from './hooks/useConversationMessages'
152
236
  export type { UseConversationListParams, UseConversationListResult } from './hooks/useConversationList'
153
237
  export type { UseConversationContextResult } from './hooks/useConversationContext'
238
+ export type { UseScrollToLatestMessageParams, UseScrollToLatestMessageResult } from './hooks/useScrollToLatestMessage'
154
239
  export type { UseConversationDocumentsParams, UseConversationDocumentsResult } from './hooks/useConversationDocuments'
155
240
  export type { ConversationRealtimeHandler } from './hooks/useConversationRealtime'
156
241
  export type { AsyncResourceState } from './hooks/useAsyncResource'
242
+ export { createMediaUrlResolver } from './lib/createMediaUrlResolver'
243
+ export type { ConversationHeaderUtility } from './ConversationHeader'
244
+ export { applyQuickReplyVariables, resolveQuickReply } from './MessageComposer'
245
+ export type { QuickReply } from './MessageComposer'
246
+
247
+ // Tela de atendimento completa. Fica no export principal — e não num subpath — porque é a
248
+ // composição padrão do pacote: quem instala conversas quer esta tela, e as peças continuam
249
+ // exportadas ao lado para quem precisar montar outra.
250
+ export { ConversationsWorkspace, ConversationPane, ConversationsInboxList } from './workspace'
251
+ export { useConversationsInbox, CONVERSATIONS_PER_PAGE, DEFAULT_CONVERSATIONS_WORKSPACE_LABELS } from './workspace'
252
+ export type {
253
+ ConversationsWorkspaceProps,
254
+ ConversationsWorkspaceSimulator,
255
+ SimulatorTransportFactory,
256
+ SimulatorTransportParams,
257
+ ConversationsWorkspaceLabels,
258
+ ConversationPaneProps,
259
+ ConversationsInboxListProps,
260
+ UseConversationsInboxParams,
261
+ UseConversationsInboxResult,
262
+ } from './workspace'
263
+
264
+ // A porta do simulador entra no export principal como TIPO: `simulator.transports` é tipado por ela,
265
+ // e obrigar o host a abrir o subpath `/preview` só para declarar a fábrica seria pedir que ele
266
+ // importasse fixtures para escrever uma assinatura. `export type` é apagado no build — o `preview/`
267
+ // continua fora do bundle de quem só declara o transporte.
268
+ export type {
269
+ ConversationSimulatorClient,
270
+ SendSimulatorMediaParams,
271
+ SimulatorMediaKind,
272
+ } from './preview/ConversationSimulatorClient'
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+
3
+ import { cn } from './cn'
4
+
5
+ describe('cn', () => {
6
+ // O contrato de `classNames`/`className` dos componentes depende disto: se o override não vencer,
7
+ // a API de customização é decorativa. Concatenar falharia justo no caso comum — o Tailwind emite
8
+ // `px-2` antes de `px-4`, então a base ganharia de quem quer apertar o espaçamento.
9
+ it('o override do host vence a classe base em conflito', () => {
10
+ expect(cn('px-4 py-3', 'px-2')).toBe('py-3 px-2')
11
+ })
12
+
13
+ it('mantém o que não conflita', () => {
14
+ expect(cn('flex items-center gap-3', 'gap-1')).toBe('flex items-center gap-1')
15
+ })
16
+
17
+ it('preserva classes próprias do pacote, que o merge não conhece', () => {
18
+ expect(cn('cv-row flex', 'bg-red-50')).toBe('cv-row flex bg-red-50')
19
+ })
20
+
21
+ it('ignora ausente, falso e vazio', () => {
22
+ expect(cn('border-b', undefined, false, '')).toBe('border-b')
23
+ })
24
+
25
+ it('aplica condicional na ordem recebida', () => {
26
+ const isCompact = false
27
+ expect(cn('mt-2', isCompact && 'mt-8', 'mt-4')).toBe('mt-4')
28
+ })
29
+ })
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+
3
+ import {
4
+ FORMATTING_ACTION,
5
+ FORMATTING_SEPARATOR,
6
+ activeFormattingIn,
7
+ canExecuteFormattingCommand,
8
+ isFormattingActive,
9
+ toggleFormattingCommand,
10
+ } from './composer-formatting'
11
+ import { ZERO_WIDTH_SPACE, htmlToWA, waToHTML } from './whatsapp-formatting'
12
+
13
+ describe('isFormattingActive', () => {
14
+ it('reconhece cada formatação dentro do conjunto ativo', () => {
15
+ const active = [FORMATTING_ACTION.BOLD, FORMATTING_ACTION.MONOSPACE].join(FORMATTING_SEPARATOR)
16
+
17
+ expect(isFormattingActive({ active, action: FORMATTING_ACTION.BOLD })).toBe(true)
18
+ expect(isFormattingActive({ active, action: FORMATTING_ACTION.MONOSPACE })).toBe(true)
19
+ expect(isFormattingActive({ active, action: FORMATTING_ACTION.ITALIC })).toBe(false)
20
+ })
21
+
22
+ it('não acende nada quando o conjunto está vazio', () => {
23
+ expect(isFormattingActive({ active: '', action: FORMATTING_ACTION.BOLD })).toBe(false)
24
+ })
25
+ })
26
+
27
+ describe('activeFormattingIn', () => {
28
+ it('não acende nada quando não há seleção no documento', () => {
29
+ const editor = { contains: () => false } as unknown as HTMLElement
30
+
31
+ expect(activeFormattingIn(editor)).toBe('')
32
+ })
33
+ })
34
+
35
+ describe('toggleFormattingCommand', () => {
36
+ it('avisa que não executou onde o comando não existe, para o campo cair no wrap manual', () => {
37
+ if (canExecuteFormattingCommand()) return
38
+ expect(toggleFormattingCommand(FORMATTING_ACTION.BOLD)).toBe(false)
39
+ })
40
+
41
+ it('não tem comando nativo para monoespaçado', () => {
42
+ expect(toggleFormattingCommand(FORMATTING_ACTION.MONOSPACE)).toBe(false)
43
+ })
44
+ })
45
+
46
+ describe('waToHTML dentro do campo editável', () => {
47
+ it('escreve o tachado como <s>, que é o que o navegador consegue desfazer', () => {
48
+ expect(waToHTML('~riscado~')).toBe('<s>riscado</s>')
49
+ })
50
+
51
+ it('mantém o ida e volta do tachado', () => {
52
+ expect(htmlToWA(waToHTML('~riscado~'))).toBe('~riscado~')
53
+ })
54
+ })
55
+
56
+ describe('htmlToWA e a âncora do cursor', () => {
57
+ it('descarta o código que só tem a âncora, para não sair um par de crases vazio', () => {
58
+ expect(htmlToWA(`oi <code>${ZERO_WIDTH_SPACE}</code>`)).toBe('oi')
59
+ })
60
+
61
+ it('tira a âncora de dentro do código que o operador chegou a preencher', () => {
62
+ expect(htmlToWA(`<code>${ZERO_WIDTH_SPACE}npm run dev</code>`)).toBe('`npm run dev`')
63
+ })
64
+
65
+ it('não deixa a âncora vazar para o texto enviado', () => {
66
+ expect(htmlToWA(`oi${ZERO_WIDTH_SPACE} tudo bem`)).toBe('oi tudo bem')
67
+ })
68
+ })
69
+
70
+ describe('htmlToWA com a marcação do navegador', () => {
71
+ it('converte o <strike> do execCommand para o tachado do WhatsApp', () => {
72
+ expect(htmlToWA('<strike>cancelado</strike>')).toBe('~cancelado~')
73
+ })
74
+
75
+ it('converte o <b> e o <i> do navegador junto com as nossas tags', () => {
76
+ expect(htmlToWA('<b>oi</b> <i>tudo</i> <strong>bem</strong>')).toBe('*oi* _tudo_ *bem*')
77
+ })
78
+ })