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

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-DXPSPUWF.js +110 -0
  2. package/dist/chunk-GY472G6E.js +2316 -0
  3. package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
  4. package/dist/flows/index.d.ts +311 -4
  5. package/dist/flows/index.js +1322 -55
  6. package/dist/index.d.ts +1074 -42
  7. package/dist/index.js +3571 -742
  8. package/dist/preview/index.d.ts +328 -8
  9. package/dist/preview/index.js +902 -115
  10. package/dist/styles.css +819 -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 +543 -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 +1003 -0
  70. package/src/flows/flowCanvasModel.test.ts +293 -0
  71. package/src/flows/flowCanvasModel.ts +342 -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 +40 -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 +783 -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
@@ -1,8 +1,8 @@
1
- import { useState } from 'react'
1
+ import { useState, type ReactNode } from 'react'
2
2
  import { Plus, Trash2, Save, X, AlertTriangle, AlertCircle } from 'lucide-react'
3
3
  import { FlowWhatsAppPreview } from './FlowWhatsAppPreview'
4
4
  import { nodeLabel } from './FlowNodeCard'
5
- import { CROSS_FLOW_PREFIX, CONDITION_OPERATORS } from './flowGraph'
5
+ import { CROSS_FLOW_PREFIX, CONDITION_OPERATORS, BUILT_IN_ACTION_KINDS } from './flowGraph'
6
6
  import { DEFAULT_FLOW_EDITOR_LABELS, type FlowEditorLabels } from './labels'
7
7
  import type { FlowGraphData, FlowNodeData, GraphIssue } from './flowGraph'
8
8
 
@@ -73,6 +73,14 @@ export interface FlowNodePanelProps {
73
73
  onChange: (updated: FlowNodeData) => void
74
74
  onDelete: (nodeId: string) => void
75
75
  labels?: Partial<FlowEditorLabels>
76
+ /**
77
+ * Seletor de arquivos do nó `send_media`, renderizado no lugar da mensagem direta.
78
+ *
79
+ * Slot, e não uma lista de arquivos por prop, porque a biblioteca é do host: upload, permissão e
80
+ * URL assinada são dele, e o painel não tem como buscar nada. Ausente, o nó continua editável —
81
+ * só não dá para anexar por aqui.
82
+ */
83
+ renderMediaPicker?: (node: FlowNodeData, graph: FlowGraphData) => ReactNode
76
84
  }
77
85
 
78
86
  // Paridade com financiamento-imobiliario-bot/apps/web/src/components/flows/FlowNodePanel.tsx —
@@ -86,18 +94,26 @@ export function FlowNodePanel({
86
94
  onChange,
87
95
  onDelete,
88
96
  labels: labelsOverride,
97
+ renderMediaPicker,
89
98
  }: FlowNodePanelProps) {
90
99
  const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride }
91
100
  const [draft, setDraft] = useState<FlowNodeData>(node)
92
101
  const otherNodeIds = Object.keys(graph.nodes).filter((id) => id !== node.id)
93
102
  const isFixedLogic = draft.type === 'entrada_choice'
94
103
  const isAction = draft.type === 'action'
104
+ const isSendMedia = isAction && draft.actionKind === BUILT_IN_ACTION_KINDS.SEND_MEDIA
95
105
  const isCondition = draft.type === 'condition'
96
106
  const isStart = graph.startNodeId === node.id
97
107
  const nodeIssues = issues.filter((i) => i.nodeId === node.id)
98
108
  // Chaves já usadas por perguntas deste fluxo — sugestão pro campo de variável da condição,
99
109
  // sem travar em texto livre (a variável pode ter vindo de outro fluxo ou de um cálculo derivado).
100
- const knownContextKeys = [...new Set(Object.values(graph.nodes).map((n) => n.contextKey).filter((key): key is string => !!key))]
110
+ const knownContextKeys = [
111
+ ...new Set(
112
+ Object.values(graph.nodes)
113
+ .map((n) => n.contextKey)
114
+ .filter((key): key is string => !!key),
115
+ ),
116
+ ]
101
117
  const conditionAnswerIds = isCondition ? ['true', 'false'] : (draft.options ?? []).map(([id]) => id)
102
118
 
103
119
  function updateNextString(value: string) {
@@ -106,7 +122,8 @@ export function FlowNodePanel({
106
122
 
107
123
  function updateNextByAnswer(answerId: string, value: string) {
108
124
  setDraft((prev) => {
109
- const current = typeof prev.next === 'object' && prev.next ? prev.next : { byAnswer: {}, default: otherNodeIds[0] ?? '' }
125
+ const current =
126
+ typeof prev.next === 'object' && prev.next ? prev.next : { byAnswer: {}, default: otherNodeIds[0] ?? '' }
110
127
  return { ...prev, next: { ...current, byAnswer: { ...current.byAnswer, [answerId]: value } } }
111
128
  })
112
129
  }
@@ -129,7 +146,10 @@ export function FlowNodePanel({
129
146
  }
130
147
 
131
148
  function addOption() {
132
- setDraft((prev) => ({ ...prev, options: [...(prev.options ?? []), [String((prev.options?.length ?? 0) + 1), 'Nova opção']] }))
149
+ setDraft((prev) => ({
150
+ ...prev,
151
+ options: [...(prev.options ?? []), [String((prev.options?.length ?? 0) + 1), 'Nova opção']],
152
+ }))
133
153
  }
134
154
 
135
155
  function removeOption(index: number) {
@@ -140,12 +160,16 @@ export function FlowNodePanel({
140
160
  return (
141
161
  <>
142
162
  {otherNodeIds.map((id) => (
143
- <option key={id} value={id}>{truncateLabel(nodeLabel(graph.nodes[id], labels))}</option>
163
+ <option key={id} value={id}>
164
+ {truncateLabel(nodeLabel(graph.nodes[id], labels))}
165
+ </option>
144
166
  ))}
145
167
  {otherFlows.length > 0 && (
146
168
  <optgroup label={labels.nodePanel.otherFlowsGroup}>
147
169
  {otherFlows.map((flow) => (
148
- <option key={flow.key} value={`${CROSS_FLOW_PREFIX}${flow.key}`}>{flow.label}</option>
170
+ <option key={flow.key} value={`${CROSS_FLOW_PREFIX}${flow.key}`}>
171
+ {flow.label}
172
+ </option>
149
173
  ))}
150
174
  </optgroup>
151
175
  )}
@@ -157,13 +181,17 @@ export function FlowNodePanel({
157
181
  <div className="fixed inset-y-0 right-0 w-96 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-xl z-50 flex flex-col">
158
182
  <div className="flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-700">
159
183
  <h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{labels.nodePanel.title}</h3>
160
- <button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={18} /></button>
184
+ <button data-cv-tooltip={labels.nodePanel.close} aria-label={labels.nodePanel.close} onClick={onClose} className="text-gray-400 hover:text-gray-600">
185
+ <X size={18} />
186
+ </button>
161
187
  </div>
162
188
 
163
189
  <div className="flex-1 overflow-y-auto p-4 space-y-4">
164
190
  {nodeIssues.length > 0 && (
165
191
  <div className="rounded-lg border border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/40 p-2.5 space-y-1.5">
166
- {nodeIssues.map((issue, i) => <IssueRow key={i} issue={issue} />)}
192
+ {nodeIssues.map((issue, i) => (
193
+ <IssueRow key={i} issue={issue} />
194
+ ))}
167
195
  </div>
168
196
  )}
169
197
 
@@ -183,23 +211,48 @@ export function FlowNodePanel({
183
211
  </p>
184
212
  )}
185
213
 
214
+ <div>
215
+ <label className="text-xs font-medium text-gray-500 dark:text-gray-400">
216
+ {labels.nodePanel.nodeName}
217
+ </label>
218
+ <input
219
+ value={draft.label ?? ''}
220
+ placeholder={labels.nodePanel.nodeNamePlaceholder}
221
+ onChange={(event) => setDraft((prev) => ({ ...prev, label: event.target.value || undefined }))}
222
+ className={`w-full mt-1 ${INPUT_CLASSNAME}`}
223
+ />
224
+ <p className="text-[11px] text-gray-400 dark:text-gray-500 mt-1">{labels.nodePanel.nodeNameHint}</p>
225
+ </div>
226
+
186
227
  {draft.contextKey && (
187
228
  <div>
188
- <label className="text-xs font-medium text-gray-500 dark:text-gray-400">{labels.nodePanel.contextKey}</label>
189
- <input value={draft.contextKey} disabled className="w-full mt-1 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 px-3 py-2.5 text-sm text-gray-500" />
229
+ <label className="text-xs font-medium text-gray-500 dark:text-gray-400">
230
+ {labels.nodePanel.contextKey}
231
+ </label>
232
+ <input
233
+ value={draft.contextKey}
234
+ disabled
235
+ className="w-full mt-1 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 px-3 py-2.5 text-sm text-gray-500"
236
+ />
190
237
  </div>
191
238
  )}
192
239
 
193
240
  {!isFixedLogic && !isAction && !isCondition && (
194
241
  <div>
195
- <label className="text-xs font-medium text-gray-500 dark:text-gray-400">{labels.nodePanel.questionType}</label>
242
+ <label className="text-xs font-medium text-gray-500 dark:text-gray-400">
243
+ {labels.nodePanel.questionType}
244
+ </label>
196
245
  <select
197
246
  value={draft.questionType ?? 'text'}
198
- onChange={(e) => setDraft((prev) => ({ ...prev, questionType: e.target.value as FlowNodeData['questionType'] }))}
247
+ onChange={(e) =>
248
+ setDraft((prev) => ({ ...prev, questionType: e.target.value as FlowNodeData['questionType'] }))
249
+ }
199
250
  className={`w-full mt-1 ${SELECT_CLASSNAME}`}
200
251
  >
201
252
  {Object.entries(labels.questionTypeLabels).map(([key, label]) => (
202
- <option key={key} value={key}>{label}</option>
253
+ <option key={key} value={key}>
254
+ {label}
255
+ </option>
203
256
  ))}
204
257
  </select>
205
258
  </div>
@@ -218,7 +271,9 @@ export function FlowNodePanel({
218
271
  {isCondition && (
219
272
  <div className="space-y-3">
220
273
  <div>
221
- <label className="text-xs font-medium text-gray-500 dark:text-gray-400">{labels.nodePanel.conditionVariable}</label>
274
+ <label className="text-xs font-medium text-gray-500 dark:text-gray-400">
275
+ {labels.nodePanel.conditionVariable}
276
+ </label>
222
277
  <input
223
278
  value={draft.conditionContextKey ?? ''}
224
279
  onChange={(e) => setDraft((prev) => ({ ...prev, conditionContextKey: e.target.value }))}
@@ -226,23 +281,36 @@ export function FlowNodePanel({
226
281
  className={`w-full mt-1 ${INPUT_CLASSNAME}`}
227
282
  />
228
283
  <datalist id="condition-context-keys">
229
- {knownContextKeys.map((key) => <option key={key} value={key} />)}
284
+ {knownContextKeys.map((key) => (
285
+ <option key={key} value={key} />
286
+ ))}
230
287
  </datalist>
231
288
  </div>
232
289
  <div>
233
- <label className="text-xs font-medium text-gray-500 dark:text-gray-400">{labels.nodePanel.conditionOperator}</label>
290
+ <label className="text-xs font-medium text-gray-500 dark:text-gray-400">
291
+ {labels.nodePanel.conditionOperator}
292
+ </label>
234
293
  <select
235
294
  value={draft.conditionOperator ?? '>'}
236
- onChange={(e) => setDraft((prev) => ({ ...prev, conditionOperator: e.target.value as FlowNodeData['conditionOperator'] }))}
295
+ onChange={(e) =>
296
+ setDraft((prev) => ({
297
+ ...prev,
298
+ conditionOperator: e.target.value as FlowNodeData['conditionOperator'],
299
+ }))
300
+ }
237
301
  className={`w-full mt-1 ${SELECT_CLASSNAME}`}
238
302
  >
239
303
  {CONDITION_OPERATORS.map((operator) => (
240
- <option key={operator} value={operator}>{labels.conditionOperatorLabels[operator] ?? operator}</option>
304
+ <option key={operator} value={operator}>
305
+ {labels.conditionOperatorLabels[operator] ?? operator}
306
+ </option>
241
307
  ))}
242
308
  </select>
243
309
  </div>
244
310
  <div>
245
- <label className="text-xs font-medium text-gray-500 dark:text-gray-400">{labels.nodePanel.conditionValue}</label>
311
+ <label className="text-xs font-medium text-gray-500 dark:text-gray-400">
312
+ {labels.nodePanel.conditionValue}
313
+ </label>
246
314
  <input
247
315
  value={draft.conditionValue ?? ''}
248
316
  onChange={(e) => setDraft((prev) => ({ ...prev, conditionValue: e.target.value }))}
@@ -262,7 +330,21 @@ export function FlowNodePanel({
262
330
  />
263
331
  )}
264
332
 
265
- {isAction && draft.actionKind !== 'send_product_list' && (
333
+ {/* Os arquivos moram na biblioteca, não no grafo — trocar o material não repassa pelo editor. */}
334
+ {isSendMedia && (
335
+ <div>
336
+ <label className="text-xs font-medium text-gray-500 dark:text-gray-400">{labels.nodePanel.media}</label>
337
+ <div className="mt-1">
338
+ {renderMediaPicker?.(node, graph) ?? (
339
+ <p className="text-xs text-gray-400">{labels.nodePanel.mediaUnavailable}</p>
340
+ )}
341
+ </div>
342
+ </div>
343
+ )}
344
+
345
+ {/* `send_media` fica de fora: o handler do módulo só envia os anexos, então um campo de
346
+ mensagem aqui seria texto que o cliente nunca recebe. A legenda é por arquivo. */}
347
+ {isAction && draft.actionKind !== 'send_product_list' && !isSendMedia && (
266
348
  <WhatsAppTextField
267
349
  label={labels.nodePanel.directMessage}
268
350
  value={draft.directMessage ?? ''}
@@ -276,18 +358,28 @@ export function FlowNodePanel({
276
358
  <div>
277
359
  <div className="flex items-center justify-between mb-1.5">
278
360
  <label className="text-xs font-medium text-gray-500 dark:text-gray-400">{labels.nodePanel.options}</label>
279
- <button onClick={addOption} className="text-xs text-blue-600 hover:underline flex items-center gap-1">
361
+ <button data-cv-tooltip={labels.nodePanel.addOption} aria-label={labels.nodePanel.addOption} onClick={addOption} className="text-xs text-blue-600 hover:underline flex items-center gap-1">
280
362
  <Plus size={12} /> {labels.nodePanel.addOption}
281
363
  </button>
282
364
  </div>
283
365
  <div className="space-y-2">
284
366
  {(draft.options ?? []).map(([id, label], i) => (
285
367
  <div key={i} className="flex items-center gap-2">
286
- <input value={id} onChange={(e) => updateOption(i, 0, e.target.value)} placeholder={labels.nodePanel.optionId}
287
- className={`w-16 ${INPUT_CLASSNAME}`} />
288
- <input value={label} onChange={(e) => updateOption(i, 1, e.target.value)} placeholder={labels.nodePanel.optionLabel}
289
- className={`flex-1 ${INPUT_CLASSNAME}`} />
290
- <button onClick={() => removeOption(i)} className="text-gray-400 hover:text-red-600"><Trash2 size={14} /></button>
368
+ <input
369
+ value={id}
370
+ onChange={(e) => updateOption(i, 0, e.target.value)}
371
+ placeholder={labels.nodePanel.optionId}
372
+ className={`w-16 ${INPUT_CLASSNAME}`}
373
+ />
374
+ <input
375
+ value={label}
376
+ onChange={(e) => updateOption(i, 1, e.target.value)}
377
+ placeholder={labels.nodePanel.optionLabel}
378
+ className={`flex-1 ${INPUT_CLASSNAME}`}
379
+ />
380
+ <button data-cv-tooltip={labels.nodePanel.removeOption} aria-label={labels.nodePanel.removeOption} onClick={() => removeOption(i)} className="text-gray-400 hover:text-red-600">
381
+ <Trash2 size={14} />
382
+ </button>
291
383
  </div>
292
384
  ))}
293
385
  </div>
@@ -299,8 +391,11 @@ export function FlowNodePanel({
299
391
  <label className="text-xs font-medium text-gray-500 dark:text-gray-400">{labels.nodePanel.next}</label>
300
392
  <p className="text-[11px] text-gray-400 dark:text-gray-500 mb-1">{labels.nodePanel.nextHint}</p>
301
393
  {typeof draft.next !== 'object' && !isCondition ? (
302
- <select value={typeof draft.next === 'string' ? draft.next : ''} onChange={(e) => updateNextString(e.target.value)}
303
- className={`w-full mt-1 ${SELECT_CLASSNAME}`}>
394
+ <select
395
+ value={typeof draft.next === 'string' ? draft.next : ''}
396
+ onChange={(e) => updateNextString(e.target.value)}
397
+ className={`w-full mt-1 ${SELECT_CLASSNAME}`}
398
+ >
304
399
  <option value="">—</option>
305
400
  {nextNodeOptions()}
306
401
  </select>
@@ -309,10 +404,17 @@ export function FlowNodePanel({
309
404
  {conditionAnswerIds.map((id) => (
310
405
  <div key={id} className="flex items-center gap-2">
311
406
  <span className="text-xs text-gray-500 w-32 shrink-0">
312
- {isCondition ? (id === 'true' ? labels.nodePanel.conditionTrue : labels.nodePanel.conditionFalse) : labels.nodePanel.nextByAnswer(id)}
407
+ {isCondition
408
+ ? id === 'true'
409
+ ? labels.nodePanel.conditionTrue
410
+ : labels.nodePanel.conditionFalse
411
+ : labels.nodePanel.nextByAnswer(id)}
313
412
  </span>
314
- <select value={draft.next && typeof draft.next === 'object' ? draft.next.byAnswer[id] ?? '' : ''} onChange={(e) => updateNextByAnswer(id, e.target.value)}
315
- className={`flex-1 ${SELECT_CLASSNAME}`}>
413
+ <select
414
+ value={draft.next && typeof draft.next === 'object' ? (draft.next.byAnswer[id] ?? '') : ''}
415
+ onChange={(e) => updateNextByAnswer(id, e.target.value)}
416
+ className={`flex-1 ${SELECT_CLASSNAME}`}
417
+ >
316
418
  <option value="">—</option>
317
419
  {nextNodeOptions()}
318
420
  </select>
@@ -322,8 +424,11 @@ export function FlowNodePanel({
322
424
  <span className="text-xs text-gray-500 w-32 shrink-0">
323
425
  {isCondition ? labels.nodePanel.conditionVariableMissing : labels.nodePanel.nextDefault}
324
426
  </span>
325
- <select value={draft.next && typeof draft.next === 'object' ? draft.next.default : ''} onChange={(e) => updateNextDefault(e.target.value)}
326
- className={`flex-1 ${SELECT_CLASSNAME}`}>
427
+ <select
428
+ value={draft.next && typeof draft.next === 'object' ? draft.next.default : ''}
429
+ onChange={(e) => updateNextDefault(e.target.value)}
430
+ className={`flex-1 ${SELECT_CLASSNAME}`}
431
+ >
327
432
  <option value="">—</option>
328
433
  {nextNodeOptions()}
329
434
  </select>
@@ -335,19 +440,25 @@ export function FlowNodePanel({
335
440
  </div>
336
441
 
337
442
  <div className="p-4 border-t border-gray-100 dark:border-gray-700 flex gap-2">
338
- <button onClick={() => onChange(draft)} className="flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
443
+ <button
444
+ data-cv-tooltip={labels.nodePanel.save} aria-label={labels.nodePanel.save}
445
+ onClick={() => onChange(draft)}
446
+ className="flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
447
+ >
339
448
  <Save size={14} /> {labels.nodePanel.save}
340
449
  </button>
341
450
  {!isStart && (
342
451
  <button
343
- onClick={() => { if (window.confirm(labels.nodePanel.deleteConfirm)) onDelete(node.id) }}
344
- title={labels.nodePanel.delete}
452
+ onClick={() => {
453
+ if (window.confirm(labels.nodePanel.deleteConfirm)) onDelete(node.id)
454
+ }}
455
+ data-cv-tooltip={labels.nodePanel.delete} aria-label={labels.nodePanel.delete}
345
456
  className="px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg"
346
457
  >
347
458
  <Trash2 size={14} />
348
459
  </button>
349
460
  )}
350
- <button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:underline">
461
+ <button data-cv-tooltip={labels.nodePanel.cancel} aria-label={labels.nodePanel.cancel} onClick={onClose} className="px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:underline">
351
462
  {labels.nodePanel.cancel}
352
463
  </button>
353
464
  </div>
@@ -58,6 +58,7 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
58
58
  return (
59
59
  <div ref={containerRef} className="relative">
60
60
  <button
61
+ data-cv-tooltip={labels.palette.title} aria-label={labels.palette.title}
61
62
  onClick={() => setOpen((v) => !v)}
62
63
  className="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700"
63
64
  >
@@ -68,17 +69,21 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
68
69
  <div className="absolute left-0 top-full mt-1.5 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg z-50 py-1">
69
70
  <div className="relative">
70
71
  <button
72
+ data-cv-tooltip={labels.palette.question} aria-label={labels.palette.question}
71
73
  onMouseEnter={() => setSubmenu('question')}
72
74
  onClick={() => setSubmenu(submenu === 'question' ? null : 'question')}
73
75
  className="w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
74
76
  >
75
- <span className="flex items-center gap-2"><MessageCircleQuestion size={15} className="text-blue-500" /> {labels.palette.question}</span>
77
+ <span className="flex items-center gap-2">
78
+ <MessageCircleQuestion size={15} className="text-blue-500" /> {labels.palette.question}
79
+ </span>
76
80
  <ChevronRight size={13} className="text-gray-400" />
77
81
  </button>
78
82
  {submenu === 'question' && (
79
83
  <div className="absolute left-full top-0 ml-1 w-56 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1">
80
84
  {QUESTION_TYPES.map((qt) => (
81
85
  <button
86
+ data-cv-tooltip={labels.questionTypeLabels[qt]} aria-label={labels.questionTypeLabels[qt]}
82
87
  key={qt}
83
88
  onClick={() => select({ kind: 'question', questionType: qt })}
84
89
  className="w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
@@ -91,6 +96,7 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
91
96
  </div>
92
97
 
93
98
  <button
99
+ data-cv-tooltip={labels.palette.decision} aria-label={labels.palette.decision}
94
100
  onMouseEnter={() => setSubmenu(null)}
95
101
  onClick={() => select({ kind: 'decision' })}
96
102
  className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
@@ -102,24 +108,28 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
102
108
  onMouseEnter={() => setSubmenu(null)}
103
109
  onClick={() => select({ kind: 'condition' })}
104
110
  className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
105
- title={labels.palette.conditionHint}
111
+ data-cv-tooltip={labels.palette.conditionHint} aria-label={labels.palette.conditionHint}
106
112
  >
107
113
  <Diamond size={15} className="text-cyan-500" /> {labels.palette.condition}
108
114
  </button>
109
115
 
110
116
  <div className="relative">
111
117
  <button
118
+ data-cv-tooltip={labels.palette.action} aria-label={labels.palette.action}
112
119
  onMouseEnter={() => setSubmenu('action')}
113
120
  onClick={() => setSubmenu(submenu === 'action' ? null : 'action')}
114
121
  className="w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
115
122
  >
116
- <span className="flex items-center gap-2"><Zap size={15} className="text-orange-500" /> {labels.palette.action}</span>
123
+ <span className="flex items-center gap-2">
124
+ <Zap size={15} className="text-orange-500" /> {labels.palette.action}
125
+ </span>
117
126
  <ChevronRight size={13} className="text-gray-400" />
118
127
  </button>
119
128
  {submenu === 'action' && (
120
129
  <div className="absolute left-full top-0 ml-1 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1">
121
130
  {resolvedActionOptions.map((option) => (
122
131
  <button
132
+ data-cv-tooltip={option.label} aria-label={option.label}
123
133
  key={option.actionKind}
124
134
  onClick={() => select({ kind: 'action', actionKind: option.actionKind })}
125
135
  className="w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
@@ -16,7 +16,7 @@ export function FlowPortalNode({ data }: NodeProps) {
16
16
  return (
17
17
  <button
18
18
  type="button"
19
- title={labels.crossFlowPortal.tooltip}
19
+ data-cv-tooltip={labels.crossFlowPortal.tooltip} aria-label={labels.crossFlowPortal.tooltip}
20
20
  onClick={onNavigate}
21
21
  className="relative flex items-center gap-1.5 rounded-full border-2 border-dashed border-cyan-400 dark:border-cyan-600 bg-cyan-50 dark:bg-cyan-950/40 px-3 py-1.5 text-xs font-medium text-cyan-700 dark:text-cyan-300 hover:bg-cyan-100 dark:hover:bg-cyan-950/70 transition-colors cursor-pointer"
22
22
  >
@@ -13,7 +13,11 @@ export interface FlowWhatsAppPreviewProps {
13
13
  // opções, botões (≤3) ou lista (4+) — para o editor mostrar exatamente o que o cliente verá.
14
14
  // Consome a mesma bolha/formatação do pacote (parseWhatsAppFormatting, T6.5) — T7.3 elimina a
15
15
  // duplicação de estilo que existia entre este preview e o MessageBubble do bot.
16
- export function FlowWhatsAppPreview({ body, options, labels = DEFAULT_FLOW_EDITOR_LABELS.nodePanel }: FlowWhatsAppPreviewProps) {
16
+ export function FlowWhatsAppPreview({
17
+ body,
18
+ options,
19
+ labels = DEFAULT_FLOW_EDITOR_LABELS.nodePanel,
20
+ }: FlowWhatsAppPreviewProps) {
17
21
  if (!body && (!options || options.length === 0)) {
18
22
  return <p className="text-xs text-gray-400 dark:text-gray-500 italic px-1">{labels.previewPlaceholder}</p>
19
23
  }
@@ -25,7 +29,11 @@ export function FlowWhatsAppPreview({ body, options, labels = DEFAULT_FLOW_EDITO
25
29
  <div className="rounded-xl bg-[#e5ddd5] dark:bg-gray-900 p-3 space-y-1.5">
26
30
  <div className="max-w-[85%]">
27
31
  <div className="rounded-lg rounded-tl-none bg-white dark:bg-gray-700 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 shadow-sm whitespace-pre-wrap break-words">
28
- {body ? parseWhatsAppFormatting(body) : <span className="italic text-gray-400">{labels.previewEmptyBody}</span>}
32
+ {body ? (
33
+ parseWhatsAppFormatting(body)
34
+ ) : (
35
+ <span className="italic text-gray-400">{labels.previewEmptyBody}</span>
36
+ )}
29
37
  {hasOptions && !usesButtons && (
30
38
  <div className="mt-2 -mx-3 -mb-2 border-t border-gray-100 dark:border-gray-600">
31
39
  <div className="flex items-center justify-center gap-1.5 py-2 text-sm font-medium text-cyan-600 dark:text-cyan-400">
@@ -38,7 +46,10 @@ export function FlowWhatsAppPreview({ body, options, labels = DEFAULT_FLOW_EDITO
38
46
  {hasOptions && usesButtons && (
39
47
  <div className="mt-1 space-y-1">
40
48
  {options!.map(([id, label]) => (
41
- <div key={id} className="rounded-lg bg-white dark:bg-gray-700 py-1.5 text-center text-sm font-medium text-cyan-600 dark:text-cyan-400 shadow-sm">
49
+ <div
50
+ key={id}
51
+ className="rounded-lg bg-white dark:bg-gray-700 py-1.5 text-center text-sm font-medium text-cyan-600 dark:text-cyan-400 shadow-sm"
52
+ >
42
53
  {label || <span className="italic text-gray-400">{labels.previewEmptyOption}</span>}
43
54
  </div>
44
55
  ))}