@adatechnology/conversations-ui 0.1.0-rc.3 → 0.1.0-rc.31

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 (143) hide show
  1. package/dist/chunk-CUYYYZWD.js +2317 -0
  2. package/dist/chunk-DXPSPUWF.js +110 -0
  3. package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
  4. package/dist/flows/index.d.ts +338 -4
  5. package/dist/flows/index.js +1331 -55
  6. package/dist/index.d.ts +1074 -42
  7. package/dist/index.js +3572 -742
  8. package/dist/preview/index.d.ts +328 -8
  9. package/dist/preview/index.js +902 -115
  10. package/dist/styles.css +899 -0
  11. package/dist/types-De5aN-E_.d.ts +502 -0
  12. package/package.json +3 -3
  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 +42 -0
  25. package/src/ConversationRow.tsx +31 -7
  26. package/src/DocumentsLibrary.tsx +382 -0
  27. package/src/EmojiPicker.tsx +70 -55
  28. package/src/FileIcon.test.ts +83 -0
  29. package/src/FileIcon.tsx +88 -11
  30. package/src/InteractiveMessage.test.tsx +41 -0
  31. package/src/InteractiveMessage.tsx +146 -0
  32. package/src/Lightbox.tsx +18 -3
  33. package/src/MediaRenderer.tsx +98 -22
  34. package/src/MessageBubble.test.tsx +41 -0
  35. package/src/MessageBubble.tsx +75 -6
  36. package/src/MessageComposer.test.tsx +35 -0
  37. package/src/MessageComposer.tsx +155 -19
  38. package/src/RichMessageComposer.test.tsx +113 -0
  39. package/src/RichMessageComposer.tsx +551 -0
  40. package/src/SimpleEmojiPicker.tsx +5 -3
  41. package/src/StatusTicks.tsx +1 -1
  42. package/src/Toast.tsx +4 -0
  43. package/src/Tooltip.test.ts +42 -0
  44. package/src/Tooltip.tsx +164 -0
  45. package/src/Wallpaper.test.tsx +21 -0
  46. package/src/Wallpaper.tsx +67 -7
  47. package/src/WhatsAppMessageEditor.tsx +28 -4
  48. package/src/WindowExpiredNotice.tsx +12 -4
  49. package/src/audioRecorderFormat.test.ts +67 -0
  50. package/src/buildOutput.test.ts +79 -0
  51. package/src/composer.constant.ts +33 -0
  52. package/src/conversationTranscript.test.ts +57 -0
  53. package/src/conversationTranscript.ts +29 -4
  54. package/src/conversationWindow.ts +7 -5
  55. package/src/documentTypeLabel.test.ts +57 -0
  56. package/src/documents/DocumentsWorkspace.tsx +550 -0
  57. package/src/documents/index.ts +8 -0
  58. package/src/documents/labels.ts +92 -0
  59. package/src/emojiCatalog.test.ts +35 -0
  60. package/src/emojiCatalog.ts +189 -0
  61. package/src/flows/FlowGroupHeader.tsx +12 -2
  62. package/src/flows/FlowMapCanvas.tsx +15 -12
  63. package/src/flows/FlowMapNode.tsx +4 -1
  64. package/src/flows/FlowNodeCard.tsx +35 -8
  65. package/src/flows/FlowNodePanel.tsx +149 -38
  66. package/src/flows/FlowPalette.tsx +13 -3
  67. package/src/flows/FlowPortalNode.tsx +1 -1
  68. package/src/flows/FlowWhatsAppPreview.tsx +14 -3
  69. package/src/flows/FlowsWorkspace.tsx +1023 -0
  70. package/src/flows/flowCanvasModel.test.ts +312 -0
  71. package/src/flows/flowCanvasModel.ts +368 -0
  72. package/src/flows/flowEditorOps.test.ts +241 -0
  73. package/src/flows/flowEditorOps.ts +177 -0
  74. package/src/flows/flowGraph.ts +6 -6
  75. package/src/flows/index.ts +47 -1
  76. package/src/flows/labels.ts +141 -0
  77. package/src/flows/workspaceContract.test.ts +95 -0
  78. package/src/hooks/useContainerWidth.ts +35 -0
  79. package/src/hooks/useConversationActions.ts +56 -0
  80. package/src/hooks/useConversationDocuments.ts +11 -7
  81. package/src/hooks/useConversationList.ts +15 -9
  82. package/src/hooks/useConversationMessages.ts +2 -2
  83. package/src/hooks/useConversationRealtime.ts +10 -8
  84. package/src/hooks/useScrollToLatestMessage.ts +127 -0
  85. package/src/hooks/useUrlFilterState.ts +107 -0
  86. package/src/icon.constant.ts +12 -0
  87. package/src/index.ts +114 -13
  88. package/src/lib/cn.test.ts +29 -0
  89. package/src/lib/composer-formatting.test.ts +78 -0
  90. package/src/lib/composer-formatting.ts +145 -0
  91. package/src/lib/createMediaUrlResolver.ts +33 -0
  92. package/src/lib/paginated.test.ts +33 -0
  93. package/src/lib/paginated.ts +26 -0
  94. package/src/lib/whatsapp-formatting.test.tsx +37 -0
  95. package/src/lib/whatsapp-formatting.tsx +28 -3
  96. package/src/listing/index.tsx +202 -0
  97. package/src/pagination.constant.ts +10 -0
  98. package/src/preview/ConversationPreview.tsx +199 -13
  99. package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
  100. package/src/preview/ConversationSimulatorPanel.tsx +89 -0
  101. package/src/preview/MediaTypesPreview.tsx +87 -0
  102. package/src/preview/conversationPreviewFailures.test.ts +64 -0
  103. package/src/preview/createMockConversationsApi.ts +175 -15
  104. package/src/preview/createPreviewBridgeClient.test.ts +92 -0
  105. package/src/preview/createPreviewBridgeClient.ts +124 -0
  106. package/src/preview/createPreviewMediaUploader.ts +82 -0
  107. package/src/preview/createPreviewWebhookClient.test.ts +96 -0
  108. package/src/preview/createPreviewWebhookClient.ts +127 -4
  109. package/src/preview/index.ts +33 -3
  110. package/src/preview/mediaTypeOf.test.ts +15 -0
  111. package/src/preview/mockDocumentsSearch.test.ts +57 -0
  112. package/src/preview/preview.test.ts +5 -3
  113. package/src/preview/previewFileSamples.test.ts +151 -0
  114. package/src/preview/previewFileSamples.ts +74 -0
  115. package/src/preview/previewFixtures.ts +288 -1
  116. package/src/preview/previewMediaSource.test.ts +62 -0
  117. package/src/preview/previewMediaSource.ts +91 -0
  118. package/src/preview/previewMediaUploader.test.ts +61 -0
  119. package/src/providers/ConversationsProvider.tsx +8 -6
  120. package/src/providers/types.ts +185 -10
  121. package/src/quickReply.test.ts +58 -0
  122. package/src/settings/MessagesWorkspace.tsx +468 -0
  123. package/src/settings/TopicsForm.tsx +2 -0
  124. package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
  125. package/src/settings/TranscriptionSettingsForm.tsx +190 -0
  126. package/src/settings/WelcomeFarewellForm.tsx +1 -0
  127. package/src/settings/WhatsAppCreateTemplateForm.tsx +4 -1
  128. package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
  129. package/src/settings/WhatsAppTemplatesSettings.tsx +9 -1
  130. package/src/styles.css +862 -0
  131. package/src/types.ts +64 -1
  132. package/src/useWaitingNotifications.ts +74 -29
  133. package/src/workspace/BulkTemplateModal.tsx +132 -0
  134. package/src/workspace/ConversationPane.tsx +432 -0
  135. package/src/workspace/ConversationsInboxList.tsx +194 -0
  136. package/src/workspace/ConversationsWorkspace.tsx +346 -0
  137. package/src/workspace/index.ts +12 -0
  138. package/src/workspace/labels.test.ts +17 -0
  139. package/src/workspace/labels.ts +85 -0
  140. package/src/workspace/useConversationsInbox.ts +332 -0
  141. package/dist/chunk-N7B24WYD.js +0 -719
  142. package/dist/chunk-NV2RZ5KT.js +0 -56
  143. package/dist/types-C0PtaO7S.d.ts +0 -207
@@ -3,6 +3,7 @@ import type { FlowConditionOperator, FlowNodeType, FlowQuestionType } from './fl
3
3
  export interface FlowEditorLabels {
4
4
  legend: Record<FlowNodeType, string>
5
5
  startNodeTooltip: string
6
+ detachedNodeTooltip: string
6
7
  liveCountTooltip: (count: number) => string
7
8
  edgeFallbackLabel: string
8
9
  // Rótulo por `actionKind` — o host estende esse mapa para registrar seus próprios kinds
@@ -13,10 +14,15 @@ export interface FlowEditorLabels {
13
14
  nodePanel: {
14
15
  title: string
15
16
  contextKey: string
17
+ nodeName: string
18
+ nodeNamePlaceholder: string
19
+ nodeNameHint: string
16
20
  questionType: string
17
21
  question: string
18
22
  options: string
19
23
  addOption: string
24
+ removeOption: string
25
+ close: string
20
26
  optionId: string
21
27
  optionLabel: string
22
28
  next: string
@@ -47,6 +53,8 @@ export interface FlowEditorLabels {
47
53
  conditionTrue: string
48
54
  conditionFalse: string
49
55
  conditionVariableMissing: string
56
+ media: string
57
+ mediaUnavailable: string
50
58
  }
51
59
  palette: {
52
60
  title: string
@@ -59,6 +67,8 @@ export interface FlowEditorLabels {
59
67
  flowMap: {
60
68
  nodeCount: (count: number) => string
61
69
  openFlow: string
70
+ toggleToMap: string
71
+ toggleToDetail: string
62
72
  }
63
73
  flowGroup: {
64
74
  focus: string
@@ -68,6 +78,67 @@ export interface FlowEditorLabels {
68
78
  tooltip: string
69
79
  goesTo: (label: string) => string
70
80
  }
81
+ collectionChain: {
82
+ feeds: (label: string) => string
83
+ }
84
+ /**
85
+ * Texto dos problemas encontrados por `validateGraph`. Vive aqui, e não no host, porque a tela
86
+ * composta é quem valida — deixar de fora obrigaria todo produto a repassar o mesmo mapa de
87
+ * funções só para a barra de erros aparecer.
88
+ */
89
+ validation: FlowValidationLabels
90
+ /** Barra de cima, estados de carregamento e ações do editor inteiro. */
91
+ workspace: {
92
+ title: string
93
+ subtitle: string
94
+ loading: string
95
+ loadError: string
96
+ saveGraph: string
97
+ saving: string
98
+ saveSuccess: string
99
+ saveError: string
100
+ organize: string
101
+ organizeTooltip: string
102
+ discardChanges: string
103
+ discardTooltip: string
104
+ discardConfirm: string
105
+ unsavedChangesConfirm: string
106
+ }
107
+ flowManager: {
108
+ newFlow: string
109
+ createTitle: string
110
+ key: string
111
+ keyHint: string
112
+ keyInvalid: string
113
+ label: string
114
+ showInMenu: string
115
+ menuOptionLabel: string
116
+ create: string
117
+ creating: string
118
+ deleteFlow: string
119
+ deleteConfirm: (label: string) => string
120
+ createError: string
121
+ deleteError: string
122
+ }
123
+ }
124
+
125
+ export interface FlowValidationLabels {
126
+ title: string
127
+ errors: (count: number) => string
128
+ warnings: (count: number) => string
129
+ noStart: string
130
+ brokenRef: (from: string, to: string) => string
131
+ choiceWithoutOptions: (id: string) => string
132
+ duplicatedOptionId: (id: string, optionId: string) => string
133
+ optionWithoutTarget: (id: string, optionLabel: string) => string
134
+ tooManyOptions: (id: string, count: number) => string
135
+ buttonTitleTooLong: (id: string, label: string) => string
136
+ listTitleTooLong: (id: string, label: string) => string
137
+ bodyTooLong: (id: string) => string
138
+ unreachable: (id: string) => string
139
+ deadEndQuestion: (id: string) => string
140
+ conditionIncomplete: (id: string) => string
141
+ conditionBranchMissing: (id: string, branch: string) => string
71
142
  }
72
143
 
73
144
  // Paridade de texto com financiamento-imobiliario-bot/apps/web/src/locales/modules/flows.ts —
@@ -81,12 +152,14 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
81
152
  condition: 'Condição',
82
153
  },
83
154
  startNodeTooltip: 'Início do fluxo',
155
+ detachedNodeTooltip: 'Sem ligação de entrada — o bot não chega neste nó. Puxe um fio de outro card até ele.',
84
156
  liveCountTooltip: (count) => `${count} conversa(s) ativa(s) aqui agora`,
85
157
  edgeFallbackLabel: 'outro',
86
158
  actionKindLabels: {
87
159
  handoff: 'Encaminhar para atendimento',
88
160
  rate_limited_handoff: 'Encaminhar (limite de simulações atingido)',
89
161
  send_product_list: 'Enviar catálogo de produtos',
162
+ send_media: 'Enviar arquivos da biblioteca',
90
163
  },
91
164
  conditionOperatorLabels: {
92
165
  '>': 'maior que',
@@ -108,10 +181,15 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
108
181
  nodePanel: {
109
182
  title: 'Editar nó',
110
183
  contextKey: 'Chave (contexto)',
184
+ nodeName: 'Nome do nó (opcional)',
185
+ nodeNamePlaceholder: 'Ex.: Enviar tabela de preços',
186
+ nodeNameHint: 'Só aparece no editor — o cliente não vê.',
111
187
  questionType: 'Tipo de resposta',
112
188
  question: 'Texto da pergunta',
113
189
  options: 'Opções (choice)',
114
190
  addOption: 'Adicionar opção',
191
+ removeOption: 'Remover opção',
192
+ close: 'Fechar painel',
115
193
  optionId: 'Valor',
116
194
  optionLabel: 'Texto exibido',
117
195
  next: 'Próximo nó',
@@ -143,6 +221,8 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
143
221
  conditionTrue: 'Se verdadeiro →',
144
222
  conditionFalse: 'Se falso →',
145
223
  conditionVariableMissing: 'Se a variável ainda não foi coletada →',
224
+ media: 'Arquivos enviados neste ponto',
225
+ mediaUnavailable: 'A biblioteca de arquivos não está disponível neste painel.',
146
226
  },
147
227
  palette: {
148
228
  title: 'Adicionar ao fluxo',
@@ -155,6 +235,8 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
155
235
  flowMap: {
156
236
  nodeCount: (count) => `${count} nó(s)`,
157
237
  openFlow: 'Abrir fluxo',
238
+ toggleToMap: 'Mapa de fluxos',
239
+ toggleToDetail: 'Voltar ao editor',
158
240
  },
159
241
  flowGroup: {
160
242
  focus: 'Focar neste fluxo',
@@ -164,6 +246,61 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
164
246
  tooltip: 'Clique para abrir esse fluxo aqui do lado, ligado ao ponto de onde ele é chamado',
165
247
  goesTo: (label) => `↪ Vai para: ${label}`,
166
248
  },
249
+ collectionChain: {
250
+ feeds: (label) => `Alimenta: ${label}`,
251
+ },
252
+ validation: {
253
+ title: 'Antes de publicar',
254
+ errors: (count) => `${count} erro(s) — corrija antes de salvar`,
255
+ warnings: (count) => `${count} aviso(s)`,
256
+ noStart: 'O fluxo precisa de um nó inicial válido.',
257
+ brokenRef: (from, to) => `Nó "${from}": ligação aponta para "${to}", que não existe.`,
258
+ choiceWithoutOptions: (id) => `Nó "${id}": escolha sem nenhuma opção.`,
259
+ duplicatedOptionId: (id, optionId) => `Nó "${id}": valor de opção "${optionId}" duplicado.`,
260
+ optionWithoutTarget: (id, label) => `Nó "${id}": opção "${label}" não tem destino definido.`,
261
+ tooManyOptions: (id, count) => `Nó "${id}": ${count} opções — o WhatsApp aceita no máximo 10 em lista.`,
262
+ buttonTitleTooLong: (id, label) => `Nó "${id}": botão "${label}" passa de 20 caracteres.`,
263
+ listTitleTooLong: (id, label) => `Nó "${id}": item de lista "${label}" passa de 24 caracteres.`,
264
+ bodyTooLong: (id) => `Nó "${id}": texto passa de 1024 caracteres.`,
265
+ unreachable: (id) => `Nó "${id}" é inalcançável a partir do início do fluxo.`,
266
+ deadEndQuestion: (id) => `Nó "${id}": pergunta sem próximo passo definido.`,
267
+ conditionIncomplete: (id) => `Nó "${id}": condição incompleta — defina variável, operador e valor.`,
268
+ conditionBranchMissing: (id, branch) =>
269
+ `Nó "${id}": ramo "${branch === 'true' ? 'Verdadeiro' : 'Falso'}" sem destino definido.`,
270
+ },
271
+ workspace: {
272
+ title: 'Fluxos do Bot',
273
+ subtitle: 'Blueprint visual dos fluxos de conversa, sincronizado com o que está em produção.',
274
+ loading: 'Carregando fluxos…',
275
+ loadError: 'Não foi possível carregar os fluxos.',
276
+ saveGraph: 'Publicar alterações',
277
+ saving: 'Publicando…',
278
+ saveSuccess: 'Fluxo publicado! O bot já está usando a versão nova.',
279
+ saveError: 'Não foi possível salvar — verifique se todos os destinos apontam para nós existentes.',
280
+ organize: 'Organizar',
281
+ organizeTooltip: 'Reorganiza os nós automaticamente e salva as novas posições',
282
+ discardChanges: 'Desfazer alterações',
283
+ discardTooltip: 'Devolve os fluxos abertos ao que está publicado, descartando o que não foi salvo',
284
+ discardConfirm: 'Descartar todas as alterações não publicadas e voltar ao fluxo que está no ar?',
285
+ unsavedChangesConfirm:
286
+ 'Você tem alterações não publicadas neste fluxo. Trocar de fluxo agora descarta essas edições. Continuar?',
287
+ },
288
+ flowManager: {
289
+ newFlow: 'Novo fluxo',
290
+ createTitle: 'Criar novo fluxo',
291
+ key: 'Identificador único',
292
+ keyHint: 'letras minúsculas, números e _ (ex.: promocoes_semana)',
293
+ keyInvalid: 'Use apenas letras minúsculas, números e _ (2 a 40 caracteres)',
294
+ label: 'Nome exibido',
295
+ showInMenu: 'Exibir como opção no menu principal do bot',
296
+ menuOptionLabel: 'Texto da opção no menu',
297
+ create: 'Criar fluxo',
298
+ creating: 'Criando…',
299
+ deleteFlow: 'Excluir fluxo',
300
+ deleteConfirm: (label) => `Excluir o fluxo "${label}"? Esta ação não pode ser desfeita.`,
301
+ createError: 'Não foi possível criar o fluxo.',
302
+ deleteError: 'Não foi possível excluir o fluxo.',
303
+ },
167
304
  }
168
305
 
169
306
  export function mergeFlowEditorLabels(override?: Partial<FlowEditorLabels>): FlowEditorLabels {
@@ -183,5 +320,9 @@ export function mergeFlowEditorLabels(override?: Partial<FlowEditorLabels>): Flo
183
320
  flowMap: { ...DEFAULT_FLOW_EDITOR_LABELS.flowMap, ...override.flowMap },
184
321
  flowGroup: { ...DEFAULT_FLOW_EDITOR_LABELS.flowGroup, ...override.flowGroup },
185
322
  crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal },
323
+ collectionChain: { ...DEFAULT_FLOW_EDITOR_LABELS.collectionChain, ...override.collectionChain },
324
+ validation: { ...DEFAULT_FLOW_EDITOR_LABELS.validation, ...override.validation },
325
+ workspace: { ...DEFAULT_FLOW_EDITOR_LABELS.workspace, ...override.workspace },
326
+ flowManager: { ...DEFAULT_FLOW_EDITOR_LABELS.flowManager, ...override.flowManager },
186
327
  }
187
328
  }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Copyright (c) 2026 Ada Technology. MIT License.
3
+ *
4
+ * O subpath `/flows` precisa exportar a TELA, não só as peças.
5
+ *
6
+ * Existe porque a ausência disso já custou: exportando apenas `FlowMapCanvas`, `FlowPalette` e
7
+ * `FlowNodePanel`, o financiamento montou a tela por conta — 973 linhas de página mais um fork local
8
+ * dos componentes, que ficou atrás do pacote. O quickcart, para ter a mesma tela, teria que copiar o
9
+ * arquivo. É exatamente a divergência que `pluggable-module.md` §4 proíbe.
10
+ *
11
+ * O teste é de superfície, não de comportamento: não prova que o canvas desenha certo, prova que
12
+ * existe UM lugar onde a tela mora, e que ela aceita customização por contrato em vez de por fork.
13
+ */
14
+
15
+ import { describe, expect, it } from 'bun:test'
16
+
17
+ import * as flows from './index'
18
+ import { DEFAULT_FLOW_EDITOR_LABELS, mergeFlowEditorLabels } from './labels'
19
+
20
+ const WORKSPACE_SOURCE = `${import.meta.dir}/FlowsWorkspace.tsx`
21
+
22
+ describe('superfície composta', () => {
23
+ it('exporta a tela inteira', () => {
24
+ expect(typeof flows.FlowsWorkspace).toBe('function')
25
+ })
26
+
27
+ it('exporta as peças também — quem precisa de layout próprio não fica sem saída', () => {
28
+ // Workspace é o caminho recomendado, não uma prisão: um produto com layout radicalmente
29
+ // diferente compõe as peças, e isso é melhor que forkar o pacote.
30
+ for (const piece of ['FlowMapCanvas', 'FlowPalette', 'FlowNodePanel', 'FlowWhatsAppPreview']) {
31
+ expect(typeof (flows as Record<string, unknown>)[piece], piece).toBe('function')
32
+ }
33
+ })
34
+
35
+ it('as operações de grafo saem puras, sem passar pela tela', () => {
36
+ // São as que a tela consome de verdade (ver os imports em `FlowsWorkspace.tsx`). Exportá-las sem
37
+ // usá-las seria pior que não ter teste: teste verde sobre código que não roda em produção.
38
+ for (const operation of ['resolveConnection', 'applyConnection', 'removeNodeAndCleanRefs', 'mergedFlowKeysFrom']) {
39
+ expect(typeof (flows as Record<string, unknown>)[operation], operation).toBe('function')
40
+ }
41
+ })
42
+ })
43
+
44
+ describe('contrato de customização', () => {
45
+ it('nenhuma capacidade em forma de flag booleana `hasX`', async () => {
46
+ /**
47
+ * Capacidade opcional é por AUSÊNCIA de prop. `hasDelete` seria um segundo jeito de dizer o que
48
+ * `deletableFlowKeys` já diz, e dois jeitos divergem — alguém liga a flag sem a lista e a tela
49
+ * desenha um botão que não exclui nada.
50
+ */
51
+ const content = await Bun.file(WORKSPACE_SOURCE).text()
52
+
53
+ expect(content).not.toMatch(/readonly has[A-Z]/)
54
+ })
55
+
56
+ it('aceita labels, className e os slots de render', async () => {
57
+ const content = await Bun.file(WORKSPACE_SOURCE).text()
58
+
59
+ expect(content).toContain('labels?: Partial<')
60
+ // `className` é o que deixa o produto posicionar a tela no layout dele sem tocar no pacote.
61
+ expect(content).toContain('className?: string')
62
+ expect(content).toContain('renderMediaPicker?:')
63
+ })
64
+
65
+ it('nenhum texto visível escrito no componente — tudo passa por labels', async () => {
66
+ const content = await Bun.file(WORKSPACE_SOURCE).text()
67
+ /**
68
+ * Texto entre tags JSX que não seja `{...}`, que é o que `web.md` §6 proíbe.
69
+ *
70
+ * O `\s*` nas pontas não é detalhe: a primeira versão deste regex no notification-ui exigia o
71
+ * texto colado nas tags, e o Prettier põe o conteúdo em linha própria — o teste passava com
72
+ * `>\n Configurações\n<` no meio do componente, provando nada.
73
+ */
74
+ const hardcoded = content.match(/>\s*[A-Za-zÀ-ÿ][A-Za-zÀ-ÿ ]{3,}\s*</g)
75
+
76
+ expect(hardcoded, `texto fixo: ${hardcoded?.join(' | ')}`).toBeNull()
77
+ })
78
+
79
+ it('o produto sobrescreve UM texto sem perder os outros do mesmo grupo', () => {
80
+ const merged = mergeFlowEditorLabels({ workspace: { ...DEFAULT_FLOW_EDITOR_LABELS.workspace, title: 'Jornadas' } })
81
+
82
+ expect(merged.workspace.title).toBe('Jornadas')
83
+ expect(merged.workspace.saveGraph).toBe(DEFAULT_FLOW_EDITOR_LABELS.workspace.saveGraph)
84
+ })
85
+
86
+ it('grupo novo de label entra no merge profundo', () => {
87
+ // Esquecer o grupo no `mergeFlowEditorLabels` deixa o override apagar os irmãos dele, e o
88
+ // sintoma é texto sumindo da tela — não erro.
89
+ for (const group of ['workspace', 'flowManager', 'validation', 'collectionChain'] as const) {
90
+ const merged = mergeFlowEditorLabels({ [group]: {} })
91
+
92
+ expect(Object.keys(merged[group]).length, group).toBe(Object.keys(DEFAULT_FLOW_EDITOR_LABELS[group]).length)
93
+ }
94
+ })
95
+ })
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Largura do próprio elemento, para decidir layout pelo espaço que ele tem — e não pelo tamanho da
3
+ * janela.
4
+ *
5
+ * Os breakpoints do Tailwind (`sm:`, `lg:`) leem a janela, e é aí que erram nestas telas: abrir a
6
+ * prévia do simulador, ou a lista de conversas ao lado, estreita a coluna sem a janela mudar de
7
+ * tamanho. O layout continua achando que está no desktop e espreme o conteúdo flexível — o campo de
8
+ * texto do composer e o nome do cliente no cabeçalho — para caber os botões, que não cedem.
9
+ *
10
+ * `undefined` enquanto não mediu (SSR e teste de markup incluídos): quem consome trata ausência de
11
+ * medida como espaçoso, que é o layout completo.
12
+ */
13
+
14
+ import { useEffect, useState, type RefObject } from 'react'
15
+
16
+ export function useContainerWidth(ref: RefObject<HTMLElement | null>): number | undefined {
17
+ const [width, setWidth] = useState<number | undefined>(undefined)
18
+
19
+ useEffect(() => {
20
+ const element = ref.current
21
+ if (!element || typeof ResizeObserver === 'undefined') return
22
+
23
+ // Sempre a mesma medida nos dois caminhos: o `contentRect` do observer desconta o padding e o
24
+ // `getBoundingClientRect` não. Misturar os dois desloca o limiar pela largura do padding — no
25
+ // cabeçalho, 32px — e o layout decide uma coisa ao montar e outra ao redimensionar.
26
+ const measure = () => setWidth(element.getBoundingClientRect().width)
27
+
28
+ measure()
29
+ const observer = new ResizeObserver(measure)
30
+ observer.observe(element)
31
+ return () => observer.disconnect()
32
+ }, [ref])
33
+
34
+ return width
35
+ }
@@ -0,0 +1,56 @@
1
+ import { useMemo } from 'react'
2
+ import { useConversations } from '../providers/ConversationsProvider'
3
+ import type { ConversationTemplate } from '../providers/types'
4
+
5
+ export interface UseConversationActionsResult {
6
+ /** `undefined` quando a API do host não implementa a operação — a UI esconde a afordância. */
7
+ takeover: (() => Promise<void>) | undefined
8
+ release: (() => Promise<void>) | undefined
9
+ finalize: (() => Promise<void>) | undefined
10
+ }
11
+
12
+ /**
13
+ * Ações de atendimento de UMA conversa, já ligadas ao id.
14
+ *
15
+ * Separado de `useConversationMessages` porque assumir e devolver conversa também acontece a
16
+ * partir da lista, onde nenhuma thread está aberta — embutir nas mensagens obrigaria a carregar
17
+ * a thread inteira só para desenhar um botão na linha.
18
+ */
19
+ export function useConversationActions(conversationId: string): UseConversationActionsResult {
20
+ const context = useConversations()
21
+ if (!context) {
22
+ throw new Error('useConversationActions requires an ancestor <ConversationsProvider>')
23
+ }
24
+ const { api } = context
25
+
26
+ return useMemo(
27
+ () => ({
28
+ takeover: api.takeover ? () => api.takeover!(conversationId) : undefined,
29
+ release: api.release ? () => api.release!(conversationId) : undefined,
30
+ finalize: api.finalize ? () => api.finalize!(conversationId) : undefined,
31
+ }),
32
+ [api, conversationId],
33
+ )
34
+ }
35
+
36
+ export interface UseInboxActionsResult {
37
+ markAllRead: (() => Promise<void>) | undefined
38
+ listTemplates: (() => Promise<ConversationTemplate[]>) | undefined
39
+ }
40
+
41
+ /** Ações que valem para a caixa inteira, sem conversa selecionada. */
42
+ export function useInboxActions(): UseInboxActionsResult {
43
+ const context = useConversations()
44
+ if (!context) {
45
+ throw new Error('useInboxActions requires an ancestor <ConversationsProvider>')
46
+ }
47
+ const { api } = context
48
+
49
+ return useMemo(
50
+ () => ({
51
+ markAllRead: api.markAllRead ? () => api.markAllRead!() : undefined,
52
+ listTemplates: api.listTemplates ? () => api.listTemplates!() : undefined,
53
+ }),
54
+ [api],
55
+ )
56
+ }
@@ -1,14 +1,14 @@
1
1
  import { useConversations } from '../providers/ConversationsProvider'
2
2
  import { useAsyncResource } from './useAsyncResource'
3
- import type { ConversationDocument } from '../providers/types'
3
+ import { documentsOf, totalOf } from '../lib/paginated'
4
+ import type { ConversationDocument, ListDocumentsParams } from '../providers/types'
4
5
 
5
- export interface UseConversationDocumentsParams {
6
- search?: string
7
- page?: number
8
- }
6
+ export type UseConversationDocumentsParams = ListDocumentsParams
9
7
 
10
8
  export interface UseConversationDocumentsResult {
11
9
  documents: ConversationDocument[]
10
+ /** Total no servidor. Cai para o tamanho da página quando a API devolve só o array. */
11
+ total: number
12
12
  loading: boolean
13
13
  error: Error | undefined
14
14
  refetch: () => Promise<void>
@@ -28,8 +28,12 @@ export function useConversationDocuments(
28
28
 
29
29
  const { data, loading, error, refetch } = useAsyncResource(
30
30
  () => (conversationId ? api.getDocuments(conversationId, params) : Promise.resolve([])),
31
- [conversationId, params?.search, params?.page],
31
+ [conversationId, params?.search, params?.page, params?.limit, params?.source, params?.sortDirection],
32
32
  )
33
33
 
34
- return { documents: data ?? [], loading, error, refetch }
34
+ if (data === undefined) {
35
+ return { documents: [], total: 0, loading, error, refetch }
36
+ }
37
+
38
+ return { documents: documentsOf(data), total: totalOf(data), loading, error, refetch }
35
39
  }
@@ -1,16 +1,14 @@
1
1
  import { useConversations } from '../providers/ConversationsProvider'
2
2
  import { useAsyncResource } from './useAsyncResource'
3
- import type { ConversationSummary } from '../providers/types'
3
+ import { conversationsOf, totalOf } from '../lib/paginated'
4
+ import type { ConversationSummary, ListConversationsParams } from '../providers/types'
4
5
 
5
- export interface UseConversationListParams {
6
- page?: number
7
- limit?: number
8
- waitingHuman?: boolean
9
- search?: string
10
- }
6
+ export type UseConversationListParams = ListConversationsParams
11
7
 
12
8
  export interface UseConversationListResult {
13
9
  conversations: ConversationSummary[]
10
+ /** Total no servidor. Cai para o tamanho da página quando a API devolve só o array. */
11
+ total: number
14
12
  loading: boolean
15
13
  error: Error | undefined
16
14
  refetch: () => Promise<void>
@@ -23,10 +21,18 @@ export function useConversationList(params?: UseConversationListParams): UseConv
23
21
  }
24
22
  const { api } = context
25
23
 
24
+ // `filters` é objeto novo a cada render do host; serializar evita refetch em laço sem obrigar
25
+ // o consumidor a memoizar — omissão que só apareceria como loop de rede em produção.
26
+ const filtersKey = JSON.stringify(params?.filters ?? {})
27
+
26
28
  const { data, loading, error, refetch } = useAsyncResource(
27
29
  () => api.fetchConversations(params),
28
- [params?.page, params?.limit, params?.waitingHuman, params?.search],
30
+ [params?.page, params?.limit, params?.waitingHuman, params?.search, filtersKey],
29
31
  )
30
32
 
31
- return { conversations: data ?? [], loading, error, refetch }
33
+ if (data === undefined) {
34
+ return { conversations: [], total: 0, loading, error, refetch }
35
+ }
36
+
37
+ return { conversations: conversationsOf(data), total: totalOf(data), loading, error, refetch }
32
38
  }
@@ -10,7 +10,7 @@ export interface UseConversationMessagesResult {
10
10
  refetch: () => Promise<void>
11
11
  sendMessage: (text: string) => Promise<MessagePayload>
12
12
  sendMedia: (data: { base64: string; mimeType: string; filename: string; caption?: string }) => Promise<MessagePayload>
13
- sendTemplate: (data: { templateName: string; languageCode?: string; bodyParams?: string[] }) => Promise<void>
13
+ sendTemplate: (data: { templateName?: string; languageCode?: string; bodyParams?: string[] }) => Promise<void>
14
14
  markRead: () => Promise<void>
15
15
  }
16
16
 
@@ -51,7 +51,7 @@ export function useConversationMessages(
51
51
  )
52
52
 
53
53
  const sendTemplate = useCallback(
54
- async (templateData: { templateName: string; languageCode?: string; bodyParams?: string[] }) => {
54
+ async (templateData: { templateName?: string; languageCode?: string; bodyParams?: string[] }) => {
55
55
  await api.sendTemplate(conversationId, templateData)
56
56
  await refetch()
57
57
  },
@@ -10,14 +10,16 @@ export function useConversationRealtime(
10
10
  conversationId: string | undefined,
11
11
  onEvent: ConversationRealtimeHandler,
12
12
  ): void {
13
- const context = useConversations()
13
+ const sse = useConversations()?.sse
14
14
  const onEventRef = useRef(onEvent)
15
15
  onEventRef.current = onEvent
16
16
 
17
+ // Depende da porta SSE, não do objeto de contexto inteiro: reabrir o stream é caro (um ticket por
18
+ // abertura) e o contexto é o que mais muda de identidade quando o host renderiza.
17
19
  useEffect(() => {
18
- if (!context || !conversationId) return
20
+ if (!sse || !conversationId) return
19
21
 
20
- const source = context.sse.connectConversationStream(conversationId)
22
+ const source = sse.connectConversationStream(conversationId)
21
23
  const handler = (event: MessageEvent) => onEventRef.current(event)
22
24
  source.addEventListener('message', handler)
23
25
 
@@ -25,20 +27,20 @@ export function useConversationRealtime(
25
27
  source.removeEventListener('message', handler)
26
28
  source.close()
27
29
  }
28
- }, [context, conversationId])
30
+ }, [sse, conversationId])
29
31
  }
30
32
 
31
33
  // Assina o stream global (ex: novas conversas entrando na fila, notificações cross-conversa)
32
34
  // — mesma porta SSEProvider, sem conversationId.
33
35
  export function useGlobalRealtime(onEvent: ConversationRealtimeHandler): void {
34
- const context = useConversations()
36
+ const sse = useConversations()?.sse
35
37
  const onEventRef = useRef(onEvent)
36
38
  onEventRef.current = onEvent
37
39
 
38
40
  useEffect(() => {
39
- if (!context) return
41
+ if (!sse) return
40
42
 
41
- const source = context.sse.connectGlobalStream()
43
+ const source = sse.connectGlobalStream()
42
44
  const handler = (event: MessageEvent) => onEventRef.current(event)
43
45
  source.addEventListener('message', handler)
44
46
 
@@ -46,5 +48,5 @@ export function useGlobalRealtime(onEvent: ConversationRealtimeHandler): void {
46
48
  source.removeEventListener('message', handler)
47
49
  source.close()
48
50
  }
49
- }, [context])
51
+ }, [sse])
50
52
  }