@adatechnology/conversations-ui 0.1.0-rc.26 → 0.1.0-rc.28
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.
- package/dist/chunk-DXPSPUWF.js +110 -0
- package/dist/{chunk-TV4OQRGH.js → chunk-UZJBYD5O.js} +123 -45
- package/dist/{chunk-2AYDBWNE.js → chunk-WCBDXZ3X.js} +13 -3
- package/dist/flows/index.d.ts +75 -2
- package/dist/flows/index.js +176 -95
- package/dist/index.d.ts +37 -10
- package/dist/index.js +694 -297
- package/dist/preview/index.js +3 -3
- package/dist/styles.css +119 -6
- package/package.json +2 -2
- package/src/AudioPlayer.tsx +8 -0
- package/src/AudioRecorderButton.test.tsx +3 -3
- package/src/AudioRecorderButton.tsx +3 -3
- package/src/AudioTranscription.tsx +4 -1
- package/src/Avatar.tsx +1 -1
- package/src/ConversationContextPanel.tsx +2 -2
- package/src/ConversationDocumentsPanel.tsx +11 -6
- package/src/ConversationHeader.test.tsx +66 -0
- package/src/ConversationHeader.tsx +147 -47
- package/src/ConversationListItem.tsx +3 -2
- package/src/ConversationRow.tsx +31 -7
- package/src/DocumentsLibrary.tsx +9 -4
- package/src/EmojiPicker.tsx +2 -1
- package/src/InteractiveMessage.tsx +3 -0
- package/src/Lightbox.tsx +1 -1
- package/src/MediaRenderer.tsx +2 -0
- package/src/MessageBubble.test.tsx +41 -0
- package/src/MessageBubble.tsx +27 -8
- package/src/MessageComposer.tsx +11 -8
- package/src/RichMessageComposer.test.tsx +42 -12
- package/src/RichMessageComposer.tsx +216 -45
- package/src/SimpleEmojiPicker.tsx +5 -3
- package/src/StatusTicks.tsx +1 -1
- package/src/Toast.tsx +4 -0
- package/src/Tooltip.test.ts +42 -0
- package/src/Tooltip.tsx +164 -0
- package/src/WhatsAppMessageEditor.tsx +4 -4
- package/src/WindowExpiredNotice.tsx +12 -4
- package/src/buildOutput.test.ts +79 -0
- package/src/composer.constant.ts +33 -0
- package/src/conversationWindow.ts +7 -5
- package/src/documents/DocumentsWorkspace.tsx +10 -6
- package/src/flows/FlowGroupHeader.tsx +2 -2
- package/src/flows/FlowMapNode.tsx +2 -1
- package/src/flows/FlowNodeCard.tsx +3 -3
- package/src/flows/FlowNodePanel.tsx +6 -5
- package/src/flows/FlowPalette.tsx +7 -1
- package/src/flows/FlowPortalNode.tsx +1 -1
- package/src/flows/FlowsWorkspace.tsx +69 -102
- package/src/flows/flowEditorOps.test.ts +241 -0
- package/src/flows/flowEditorOps.ts +177 -0
- package/src/flows/flowGraph.ts +1 -1
- package/src/flows/index.ts +16 -0
- package/src/flows/labels.ts +4 -0
- package/src/flows/workspaceContract.test.ts +95 -0
- package/src/hooks/useContainerWidth.ts +35 -0
- package/src/hooks/useConversationRealtime.ts +10 -8
- package/src/icon.constant.ts +12 -0
- package/src/index.ts +1 -0
- package/src/lib/composer-formatting.test.ts +78 -0
- package/src/lib/composer-formatting.ts +145 -0
- package/src/lib/whatsapp-formatting.test.tsx +37 -0
- package/src/lib/whatsapp-formatting.tsx +28 -3
- package/src/listing/index.tsx +5 -1
- package/src/pagination.constant.ts +10 -0
- package/src/preview/ConversationSimulatorPanel.tsx +1 -1
- package/src/providers/ConversationsProvider.tsx +8 -6
- package/src/settings/MessagesWorkspace.tsx +3 -0
- package/src/settings/TopicsForm.tsx +2 -0
- package/src/settings/TranscriptionSettingsForm.test.tsx +1 -1
- package/src/settings/TranscriptionSettingsForm.tsx +1 -0
- package/src/settings/WelcomeFarewellForm.tsx +1 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +1 -0
- package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
- package/src/settings/WhatsAppTemplatesSettings.tsx +2 -0
- package/src/styles.css +100 -4
- package/src/workspace/BulkTemplateModal.tsx +4 -2
- package/src/workspace/ConversationPane.tsx +49 -19
- package/src/workspace/ConversationsInboxList.tsx +17 -8
- package/src/workspace/ConversationsWorkspace.tsx +24 -16
- package/src/workspace/useConversationsInbox.ts +5 -2
package/dist/preview/index.js
CHANGED
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
DocumentsLibrary,
|
|
9
9
|
MessageBubble,
|
|
10
10
|
MessageComposer
|
|
11
|
-
} from "../chunk-
|
|
12
|
-
import "../chunk-
|
|
11
|
+
} from "../chunk-UZJBYD5O.js";
|
|
12
|
+
import "../chunk-WCBDXZ3X.js";
|
|
13
13
|
|
|
14
14
|
// src/preview/previewStore.ts
|
|
15
15
|
var GLOBAL_CHANNEL = "global";
|
|
@@ -1059,7 +1059,7 @@ function ConversationSimulatorPanel({
|
|
|
1059
1059
|
] }),
|
|
1060
1060
|
/* @__PURE__ */ jsxs2("div", { className: "cv-simulator-panel__actions", children: [
|
|
1061
1061
|
headerActions,
|
|
1062
|
-
/* @__PURE__ */ jsx2("button", { type: "button", onClick: onClose,
|
|
1062
|
+
/* @__PURE__ */ jsx2("button", { type: "button", onClick: onClose, "data-cv-tooltip": text.close, "aria-label": text.close, className: "cv-simulator-panel__close", children: /* @__PURE__ */ jsx2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("path", { d: "M18 6 6 18M6 6l12 12" }) }) })
|
|
1063
1063
|
] })
|
|
1064
1064
|
] }),
|
|
1065
1065
|
/* @__PURE__ */ jsx2("div", { className: "cv-simulator-panel__body", children: /* @__PURE__ */ jsx2(ConversationPreview, { ...previewProps, placeholder: text.placeholder }) })
|
package/dist/styles.css
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cg fill='none' stroke='%2319232a' stroke-width='1.2' opacity='0.9'%3E%3Ccircle cx='15' cy='15' r='3'/%3E%3Cpath d='M40 10 q5 8 0 16 q-5 -8 0 -16z'/%3E%3Ccircle cx='70' cy='28' r='2'/%3E%3Cpath d='M18 55 l6 6 m-6 0 l6 -6'/%3E%3Ccircle cx='55' cy='68' r='2.5'/%3E%3Cpath d='M85 58 q6 6 0 12 q-6 -6 0 -12z'/%3E%3Ccircle cx='8' cy='85' r='2'/%3E%3Cpath d='M65 90 l5 5 m-5 0 l5 -5'/%3E%3C/g%3E%3C/svg%3E");
|
|
11
11
|
}
|
|
12
12
|
.cv-header-action {
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: 0.375rem;
|
|
13
16
|
border: 1px solid rgb(203 213 225);
|
|
14
17
|
border-radius: 0.375rem;
|
|
15
18
|
padding: 0.375rem 0.75rem;
|
|
@@ -26,6 +29,9 @@
|
|
|
26
29
|
color: rgb(226 232 240);
|
|
27
30
|
}
|
|
28
31
|
.cv-header-icon {
|
|
32
|
+
display: inline-flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: center;
|
|
29
35
|
border: 1px solid rgb(203 213 225);
|
|
30
36
|
border-radius: 0.5rem;
|
|
31
37
|
padding: 0.375rem 0.5rem;
|
|
@@ -128,6 +134,9 @@
|
|
|
128
134
|
background: rgb(30 41 59);
|
|
129
135
|
}
|
|
130
136
|
.cv-back {
|
|
137
|
+
display: inline-flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: center;
|
|
131
140
|
border: 1px solid rgb(203 213 225);
|
|
132
141
|
border-radius: 0.5rem;
|
|
133
142
|
padding: 0.25rem 0.5rem;
|
|
@@ -144,6 +153,11 @@
|
|
|
144
153
|
border-color: rgb(71 85 105);
|
|
145
154
|
color: rgb(226 232 240);
|
|
146
155
|
}
|
|
156
|
+
.cv-stat {
|
|
157
|
+
display: inline-flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
gap: 0.25rem;
|
|
160
|
+
}
|
|
147
161
|
.cv-only-wide {
|
|
148
162
|
display: none;
|
|
149
163
|
}
|
|
@@ -366,6 +380,9 @@
|
|
|
366
380
|
gap: 0.5rem;
|
|
367
381
|
}
|
|
368
382
|
.cv-workspace-toggle {
|
|
383
|
+
display: inline-flex;
|
|
384
|
+
align-items: center;
|
|
385
|
+
gap: 0.375rem;
|
|
369
386
|
border: 1px solid rgb(229 231 235);
|
|
370
387
|
border-radius: 0.375rem;
|
|
371
388
|
padding: 0.5rem 0.75rem;
|
|
@@ -550,18 +567,26 @@
|
|
|
550
567
|
}
|
|
551
568
|
.cv-workspace-chips {
|
|
552
569
|
display: flex;
|
|
553
|
-
flex-wrap:
|
|
570
|
+
flex-wrap: nowrap;
|
|
554
571
|
align-items: center;
|
|
555
|
-
gap: 0.
|
|
572
|
+
gap: 0.375rem;
|
|
556
573
|
font-size: 0.75rem;
|
|
574
|
+
overflow-x: auto;
|
|
575
|
+
scrollbar-width: none;
|
|
576
|
+
}
|
|
577
|
+
.cv-workspace-chips::-webkit-scrollbar {
|
|
578
|
+
display: none;
|
|
557
579
|
}
|
|
558
580
|
.cv-workspace-chips__legend {
|
|
559
581
|
color: rgb(107 114 128);
|
|
582
|
+
flex-shrink: 0;
|
|
560
583
|
}
|
|
561
584
|
.cv-workspace-chip {
|
|
562
585
|
display: flex;
|
|
586
|
+
flex-shrink: 0;
|
|
563
587
|
align-items: center;
|
|
564
588
|
gap: 0.25rem;
|
|
589
|
+
white-space: nowrap;
|
|
565
590
|
border-radius: 0.375rem;
|
|
566
591
|
padding: 0.25rem 0.5rem;
|
|
567
592
|
}
|
|
@@ -571,10 +596,53 @@
|
|
|
571
596
|
.dark .cv-workspace-chip--on {
|
|
572
597
|
background: rgb(51 65 85);
|
|
573
598
|
}
|
|
574
|
-
.cv-workspace-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
599
|
+
.cv-workspace-chip--fresh {
|
|
600
|
+
color: rgb(22 163 74);
|
|
601
|
+
}
|
|
602
|
+
.cv-workspace-chip--warning {
|
|
603
|
+
color: rgb(202 138 4);
|
|
604
|
+
}
|
|
605
|
+
.cv-workspace-chip--critical {
|
|
606
|
+
color: rgb(220 38 38);
|
|
607
|
+
}
|
|
608
|
+
.cv-workspace-chip--expired {
|
|
609
|
+
color: rgb(107 114 128);
|
|
610
|
+
}
|
|
611
|
+
.cv-workspace-chip--fresh.cv-workspace-chip--on {
|
|
612
|
+
background: rgb(220 252 231);
|
|
613
|
+
}
|
|
614
|
+
.cv-workspace-chip--warning.cv-workspace-chip--on {
|
|
615
|
+
background: rgb(254 249 195);
|
|
616
|
+
}
|
|
617
|
+
.cv-workspace-chip--critical.cv-workspace-chip--on {
|
|
618
|
+
background: rgb(254 226 226);
|
|
619
|
+
}
|
|
620
|
+
.cv-workspace-chip--expired.cv-workspace-chip--on {
|
|
621
|
+
background: rgb(229 231 235);
|
|
622
|
+
}
|
|
623
|
+
.dark .cv-workspace-chip--fresh {
|
|
624
|
+
color: rgb(74 222 128);
|
|
625
|
+
}
|
|
626
|
+
.dark .cv-workspace-chip--warning {
|
|
627
|
+
color: rgb(250 204 21);
|
|
628
|
+
}
|
|
629
|
+
.dark .cv-workspace-chip--critical {
|
|
630
|
+
color: rgb(248 113 113);
|
|
631
|
+
}
|
|
632
|
+
.dark .cv-workspace-chip--expired {
|
|
633
|
+
color: rgb(148 163 184);
|
|
634
|
+
}
|
|
635
|
+
.dark .cv-workspace-chip--fresh.cv-workspace-chip--on {
|
|
636
|
+
background: rgb(20 83 45);
|
|
637
|
+
}
|
|
638
|
+
.dark .cv-workspace-chip--warning.cv-workspace-chip--on {
|
|
639
|
+
background: rgb(113 63 18);
|
|
640
|
+
}
|
|
641
|
+
.dark .cv-workspace-chip--critical.cv-workspace-chip--on {
|
|
642
|
+
background: rgb(127 29 29);
|
|
643
|
+
}
|
|
644
|
+
.dark .cv-workspace-chip--expired.cv-workspace-chip--on {
|
|
645
|
+
background: rgb(51 65 85);
|
|
578
646
|
}
|
|
579
647
|
.cv-workspace-selectall {
|
|
580
648
|
display: flex;
|
|
@@ -913,3 +981,48 @@
|
|
|
913
981
|
.dark .cv-listing-perpage {
|
|
914
982
|
border-color: rgb(71 85 105);
|
|
915
983
|
}
|
|
984
|
+
.cv-tooltip {
|
|
985
|
+
position: fixed;
|
|
986
|
+
z-index: 9999;
|
|
987
|
+
max-width: min(16rem, 90vw);
|
|
988
|
+
transform: translate(-50%, -100%);
|
|
989
|
+
border-radius: 0.375rem;
|
|
990
|
+
background: rgb(17 24 39 / 95%);
|
|
991
|
+
padding: 0.3125rem 0.5rem;
|
|
992
|
+
font-size: 0.75rem;
|
|
993
|
+
line-height: 1.2;
|
|
994
|
+
color: #fff;
|
|
995
|
+
text-align: center;
|
|
996
|
+
box-shadow: 0 4px 12px rgb(0 0 0 / 18%);
|
|
997
|
+
pointer-events: none;
|
|
998
|
+
animation: cv-tooltip-in 90ms ease-out;
|
|
999
|
+
}
|
|
1000
|
+
.cv-tooltip--bottom {
|
|
1001
|
+
transform: translate(-50%, 0);
|
|
1002
|
+
}
|
|
1003
|
+
.dark .cv-tooltip {
|
|
1004
|
+
background: rgb(243 244 246 / 96%);
|
|
1005
|
+
color: rgb(17 24 39);
|
|
1006
|
+
}
|
|
1007
|
+
@keyframes cv-tooltip-in {
|
|
1008
|
+
from {
|
|
1009
|
+
opacity: 0;
|
|
1010
|
+
}
|
|
1011
|
+
to {
|
|
1012
|
+
opacity: 1;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1016
|
+
.cv-tooltip {
|
|
1017
|
+
animation: none;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
.cv-composer-bar {
|
|
1021
|
+
border-top: 1px solid rgb(229 231 235);
|
|
1022
|
+
background: rgb(240 242 245);
|
|
1023
|
+
padding: 0.625rem 0.75rem;
|
|
1024
|
+
}
|
|
1025
|
+
.dark .cv-composer-bar {
|
|
1026
|
+
border-top-color: rgb(51 65 85);
|
|
1027
|
+
background: rgb(17 24 39);
|
|
1028
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adatechnology/conversations-ui",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.28",
|
|
4
4
|
"description": "WhatsApp conversation UI components — parametrizável por endpoint, tema e feature flags",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@types/react-dom": "^18 || ^19"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
|
-
"build": "tsup src/index.ts src/flows/index.ts src/channel/index.ts src/preview/index.ts src/styles.css --dts --format esm --external react --external react-dom --external @xyflow/react",
|
|
54
|
+
"build": "tsup src/index.ts src/flows/index.ts src/channel/index.ts src/preview/index.ts src/styles.css --dts --clean --format esm --external react --external react-dom --external @xyflow/react",
|
|
55
55
|
"build:watch": "tsup src/index.ts src/flows/index.ts src/channel/index.ts src/styles.css --watch --dts --format esm --external react --external react-dom --external @xyflow/react",
|
|
56
56
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
57
57
|
"test": "bun test"
|
package/src/AudioPlayer.tsx
CHANGED
|
@@ -15,6 +15,13 @@ const BARS = Array.from({ length: 30 }, (_, i) => {
|
|
|
15
15
|
return heights[i % heights.length]
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
+
// O player não recebe bag de rótulos do host: são duas palavras, e criar um contrato de labels só
|
|
19
|
+
// para elas custaria mais do que resolve.
|
|
20
|
+
const AUDIO_PLAYER_LABELS = {
|
|
21
|
+
play: 'Reproduzir',
|
|
22
|
+
pause: 'Pausar',
|
|
23
|
+
} as const
|
|
24
|
+
|
|
18
25
|
function fmt(sec: number): string {
|
|
19
26
|
if (!isFinite(sec)) return '0:00'
|
|
20
27
|
const m = Math.floor(sec / 60)
|
|
@@ -77,6 +84,7 @@ export function AudioPlayer({ src, isMine = false }: AudioPlayerProps) {
|
|
|
77
84
|
<audio ref={audioRef} src={src} preload="metadata" />
|
|
78
85
|
|
|
79
86
|
<button
|
|
87
|
+
data-cv-tooltip={playing ? AUDIO_PLAYER_LABELS.pause : AUDIO_PLAYER_LABELS.play} aria-label={playing ? AUDIO_PLAYER_LABELS.pause : AUDIO_PLAYER_LABELS.play}
|
|
80
88
|
onClick={toggle}
|
|
81
89
|
className={`flex-shrink-0 w-9 h-9 rounded-full flex items-center justify-center transition-colors ${playBtn}`}
|
|
82
90
|
>
|
|
@@ -9,8 +9,8 @@ describe('AudioRecorderButton', () => {
|
|
|
9
9
|
it('começa oferecendo gravar, sem painel de revisão à vista', () => {
|
|
10
10
|
const markup = renderToStaticMarkup(<AudioRecorderButton onRecorded={noop} />)
|
|
11
11
|
|
|
12
|
-
expect(markup).toContain(`
|
|
13
|
-
expect(markup).not.toContain(`
|
|
12
|
+
expect(markup).toContain(`data-cv-tooltip="${DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.start}"`)
|
|
13
|
+
expect(markup).not.toContain(`data-cv-tooltip="${DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.send}"`)
|
|
14
14
|
expect(markup).not.toContain('<audio')
|
|
15
15
|
})
|
|
16
16
|
|
|
@@ -19,7 +19,7 @@ describe('AudioRecorderButton', () => {
|
|
|
19
19
|
<AudioRecorderButton onRecorded={noop} labels={{ start: 'Gravar recado' }} />,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
-
expect(markup).toContain('
|
|
22
|
+
expect(markup).toContain('data-cv-tooltip="Gravar recado"')
|
|
23
23
|
})
|
|
24
24
|
|
|
25
25
|
it('mantém a caixa de 40px do botão de enviar para a barra não pular', () => {
|
|
@@ -205,7 +205,7 @@ export function AudioRecorderButton({
|
|
|
205
205
|
<button
|
|
206
206
|
type="button"
|
|
207
207
|
onClick={discard}
|
|
208
|
-
|
|
208
|
+
data-cv-tooltip={labelOf('discard')}
|
|
209
209
|
aria-label={labelOf('discard')}
|
|
210
210
|
className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full text-gray-500 transition-colors hover:bg-gray-100 hover:text-red-500 dark:hover:bg-gray-700"
|
|
211
211
|
>
|
|
@@ -214,7 +214,7 @@ export function AudioRecorderButton({
|
|
|
214
214
|
<button
|
|
215
215
|
type="button"
|
|
216
216
|
onClick={confirm}
|
|
217
|
-
|
|
217
|
+
data-cv-tooltip={labelOf('send')}
|
|
218
218
|
aria-label={labelOf('send')}
|
|
219
219
|
className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white transition-colors hover:bg-emerald-600"
|
|
220
220
|
>
|
|
@@ -226,7 +226,7 @@ export function AudioRecorderButton({
|
|
|
226
226
|
type="button"
|
|
227
227
|
disabled={disabled || pending !== undefined}
|
|
228
228
|
onClick={() => (isRecording ? stop() : void start())}
|
|
229
|
-
|
|
229
|
+
data-cv-tooltip={toggleLabel}
|
|
230
230
|
aria-label={toggleLabel}
|
|
231
231
|
aria-pressed={isRecording}
|
|
232
232
|
/* Mesma caixa do botão de enviar: o microfone ocupa o lugar dele enquanto o campo está
|
|
@@ -136,7 +136,7 @@ export function AudioTranscription({ transcription, onTranscribe, isMine = false
|
|
|
136
136
|
{hasText && (
|
|
137
137
|
<button
|
|
138
138
|
onClick={handleCopy}
|
|
139
|
-
|
|
139
|
+
data-cv-tooltip={locales.copy}
|
|
140
140
|
aria-label={hasCopied ? locales.copied : locales.copy}
|
|
141
141
|
className="ml-auto flex flex-shrink-0 items-center gap-1 rounded px-1.5 py-0.5 text-[11px] text-gray-500 transition-colors hover:bg-black/5 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-200"
|
|
142
142
|
>
|
|
@@ -182,6 +182,7 @@ export function AudioTranscription({ transcription, onTranscribe, isMine = false
|
|
|
182
182
|
tela, não sobre o que o operador cola no sistema interno. */}
|
|
183
183
|
{isLong && (
|
|
184
184
|
<button
|
|
185
|
+
data-cv-tooltip={isExpanded ? locales.showLess : locales.showMore} aria-label={isExpanded ? locales.showLess : locales.showMore}
|
|
185
186
|
onClick={() => setIsExpanded((current) => !current)}
|
|
186
187
|
className="mt-1 text-[11px] font-medium text-gray-500 underline decoration-dotted hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
|
187
188
|
>
|
|
@@ -194,6 +195,7 @@ export function AudioTranscription({ transcription, onTranscribe, isMine = false
|
|
|
194
195
|
{/* Retranscrever fica escondido atrás do hover do balão: é ação rara e paga cota de engine. */}
|
|
195
196
|
{onTranscribe && (
|
|
196
197
|
<button
|
|
198
|
+
data-cv-tooltip={isTranscribing ? locales.transcribing : locales.retry} aria-label={isTranscribing ? locales.transcribing : locales.retry}
|
|
197
199
|
onClick={handleTranscribe}
|
|
198
200
|
disabled={isTranscribing}
|
|
199
201
|
className="mt-1 flex items-center gap-1 text-[11px] text-gray-400 opacity-0 transition-opacity hover:text-gray-600 focus:opacity-100 group-hover:opacity-100 disabled:opacity-50 dark:text-gray-500 dark:hover:text-gray-300"
|
|
@@ -234,6 +236,7 @@ function TranscribeButton({
|
|
|
234
236
|
}) {
|
|
235
237
|
return (
|
|
236
238
|
<button
|
|
239
|
+
data-cv-tooltip={label} aria-label={label}
|
|
237
240
|
onClick={onClick}
|
|
238
241
|
disabled={isBusy || isDisabled}
|
|
239
242
|
className="flex items-center gap-1.5 rounded px-1.5 py-0.5 text-[11px] text-gray-500 transition-colors hover:bg-black/5 hover:text-gray-700 disabled:opacity-60 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-200"
|
package/src/Avatar.tsx
CHANGED
|
@@ -81,7 +81,7 @@ export function Avatar({ name, avatarUrl, size = 'md', className = '', labels }:
|
|
|
81
81
|
return (
|
|
82
82
|
<div
|
|
83
83
|
className={`${sizeClass} ${bgColor} rounded-full flex items-center justify-center text-white font-semibold flex-shrink-0 ${className}`}
|
|
84
|
-
|
|
84
|
+
data-cv-tooltip={name}
|
|
85
85
|
>
|
|
86
86
|
{getInitials(name)}
|
|
87
87
|
</div>
|
|
@@ -156,7 +156,7 @@ export function ConversationContextPanel({
|
|
|
156
156
|
type="button"
|
|
157
157
|
onClick={() => setManualOpen(!open)}
|
|
158
158
|
aria-expanded={open}
|
|
159
|
-
|
|
159
|
+
data-cv-tooltip={open ? labels.collapse : labels.expand} aria-label={open ? labels.collapse : labels.expand}
|
|
160
160
|
className={cn(
|
|
161
161
|
'flex w-full cursor-pointer items-center justify-between gap-2 px-4 py-2.5 text-left transition-colors hover:bg-blue-100/40 dark:hover:bg-blue-900/20',
|
|
162
162
|
classNames?.toggle,
|
|
@@ -263,7 +263,7 @@ export function ConversationContextPanel({
|
|
|
263
263
|
? 'text-gray-900 dark:text-gray-100'
|
|
264
264
|
: 'italic font-normal text-gray-400 dark:text-gray-500',
|
|
265
265
|
)}
|
|
266
|
-
|
|
266
|
+
data-cv-tooltip={entry.value ?? labels.notCollected}
|
|
267
267
|
>
|
|
268
268
|
{entry.value ?? labels.notCollected}
|
|
269
269
|
</dd>
|
|
@@ -13,6 +13,7 @@ import { useConversationDocuments } from './hooks/useConversationDocuments'
|
|
|
13
13
|
import { useConversations } from './providers/ConversationsProvider'
|
|
14
14
|
import { FileIcon } from './FileIcon'
|
|
15
15
|
import { cn } from './lib/cn'
|
|
16
|
+
import { PAGINATION_LABELS } from './pagination.constant'
|
|
16
17
|
import { formatDateTime, formatFileSize } from './lib/format'
|
|
17
18
|
|
|
18
19
|
/** Origens que o filtro oferece. `all` não é origem — é a ausência de filtro. */
|
|
@@ -239,6 +240,7 @@ export function ConversationDocumentsPanel({
|
|
|
239
240
|
</select>
|
|
240
241
|
|
|
241
242
|
<button
|
|
243
|
+
data-cv-tooltip={sortDirection === 'desc' ? labels.sortMostRecent : labels.sortOldest} aria-label={sortDirection === 'desc' ? labels.sortMostRecent : labels.sortOldest}
|
|
242
244
|
type="button"
|
|
243
245
|
onClick={() => applyFilter(() => setSortDirection(sortDirection === 'desc' ? 'asc' : 'desc'))}
|
|
244
246
|
className={cn('cv-header-action inline-flex items-center gap-1', classNames?.sortButton)}
|
|
@@ -250,6 +252,7 @@ export function ConversationDocumentsPanel({
|
|
|
250
252
|
{/* Só aparece com filtro ativo: botão que não faz nada visível é ruído. */}
|
|
251
253
|
{hasFilters ? (
|
|
252
254
|
<button
|
|
255
|
+
data-cv-tooltip={labels.clearFilters} aria-label={labels.clearFilters}
|
|
253
256
|
type="button"
|
|
254
257
|
onClick={() =>
|
|
255
258
|
applyFilter(() => {
|
|
@@ -291,7 +294,7 @@ export function ConversationDocumentsPanel({
|
|
|
291
294
|
{labels.selectAll}
|
|
292
295
|
</label>
|
|
293
296
|
{selectedIds.length > 0 ? (
|
|
294
|
-
<button type="button" onClick={() => void handleDownloadSelected()} className="cv-header-action">
|
|
297
|
+
<button data-cv-tooltip={labels.downloadSelected(selectedIds.length)} aria-label={labels.downloadSelected(selectedIds.length)} type="button" onClick={() => void handleDownloadSelected()} className="cv-header-action">
|
|
295
298
|
{labels.downloadSelected(selectedIds.length)}
|
|
296
299
|
</button>
|
|
297
300
|
) : null}
|
|
@@ -347,7 +350,7 @@ export function ConversationDocumentsPanel({
|
|
|
347
350
|
{/* `title` no nome: truncado, o atendente não tem outro jeito de ler inteiro. */}
|
|
348
351
|
<div
|
|
349
352
|
className={cn('truncate text-sm font-medium', classNames?.filename)}
|
|
350
|
-
|
|
353
|
+
data-cv-tooltip={document.filename}
|
|
351
354
|
>
|
|
352
355
|
{document.filename}
|
|
353
356
|
</div>
|
|
@@ -366,7 +369,7 @@ export function ConversationDocumentsPanel({
|
|
|
366
369
|
<button
|
|
367
370
|
type="button"
|
|
368
371
|
onClick={() => void handleOpen(document.id, 'inline')}
|
|
369
|
-
|
|
372
|
+
data-cv-tooltip={labels.view}
|
|
370
373
|
aria-label={`${labels.view}: ${document.filename}`}
|
|
371
374
|
className={cn('cv-header-icon', classNames?.viewButton)}
|
|
372
375
|
>
|
|
@@ -375,7 +378,7 @@ export function ConversationDocumentsPanel({
|
|
|
375
378
|
<button
|
|
376
379
|
type="button"
|
|
377
380
|
onClick={() => void handleOpen(document.id, 'attachment')}
|
|
378
|
-
|
|
381
|
+
data-cv-tooltip={labels.download}
|
|
379
382
|
aria-label={`${labels.download}: ${document.filename}`}
|
|
380
383
|
className={cn('cv-header-icon', classNames?.downloadButton)}
|
|
381
384
|
>
|
|
@@ -401,7 +404,8 @@ export function ConversationDocumentsPanel({
|
|
|
401
404
|
type="button"
|
|
402
405
|
onClick={() => setPage(page - 1)}
|
|
403
406
|
disabled={page <= 1}
|
|
404
|
-
aria-label={
|
|
407
|
+
aria-label={PAGINATION_LABELS.previous}
|
|
408
|
+
data-cv-tooltip={PAGINATION_LABELS.previous}
|
|
405
409
|
className="cv-header-icon disabled:opacity-40"
|
|
406
410
|
>
|
|
407
411
|
‹
|
|
@@ -411,7 +415,8 @@ export function ConversationDocumentsPanel({
|
|
|
411
415
|
type="button"
|
|
412
416
|
onClick={() => setPage(page + 1)}
|
|
413
417
|
disabled={page >= lastPage}
|
|
414
|
-
aria-label={
|
|
418
|
+
aria-label={PAGINATION_LABELS.next}
|
|
419
|
+
data-cv-tooltip={PAGINATION_LABELS.next}
|
|
415
420
|
className="cv-header-icon disabled:opacity-40"
|
|
416
421
|
>
|
|
417
422
|
›
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
|
|
4
|
+
import { ConversationHeader } from './ConversationHeader'
|
|
5
|
+
import type { ConversationSummary } from './providers/types'
|
|
6
|
+
|
|
7
|
+
const noop = () => {}
|
|
8
|
+
|
|
9
|
+
const CONVERSATION: ConversationSummary = {
|
|
10
|
+
id: 'conv-1',
|
|
11
|
+
clientName: 'Anderson Silva',
|
|
12
|
+
whatsappNumber: '5511988887777',
|
|
13
|
+
channel: 'whatsapp',
|
|
14
|
+
mode: 'human',
|
|
15
|
+
assignedUserId: null,
|
|
16
|
+
waitingHuman: false,
|
|
17
|
+
unread: 0,
|
|
18
|
+
currentState: 'idle',
|
|
19
|
+
lastAt: '2026-01-01T12:00:00.000Z',
|
|
20
|
+
lastInboundAt: null,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('ConversationHeader sem medida da faixa', () => {
|
|
24
|
+
// Sem `ResizeObserver` (SSR e este teste de markup) o cabeçalho nasce completo. O recolhimento é
|
|
25
|
+
// medido no próprio elemento, e é verificado no navegador.
|
|
26
|
+
it('mostra utilitários e ações na faixa, sem duplicar no menu', () => {
|
|
27
|
+
const markup = renderToStaticMarkup(
|
|
28
|
+
<ConversationHeader
|
|
29
|
+
conversation={CONVERSATION}
|
|
30
|
+
onDownload={noop}
|
|
31
|
+
onOpenDocuments={noop}
|
|
32
|
+
onReturnToBot={noop}
|
|
33
|
+
onFinish={noop}
|
|
34
|
+
/>,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
expect(markup).toContain('aria-label="Baixar conversa"')
|
|
38
|
+
expect(markup).toContain('aria-label="Finalizar"')
|
|
39
|
+
expect(markup).not.toContain('aria-label="Mais ações"')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('escreve o canal e o modo por extenso quando há espaço', () => {
|
|
43
|
+
const markup = renderToStaticMarkup(<ConversationHeader conversation={CONVERSATION} />)
|
|
44
|
+
|
|
45
|
+
expect(markup).toContain('atendimento humano')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Emoji desenha diferente em cada sistema e não herda a cor do controle: os ícones do cabeçalho
|
|
49
|
+
// são SVG da biblioteca, como no resto do produto.
|
|
50
|
+
it('desenha os ícones como SVG, sem emoji no meio dos rótulos', () => {
|
|
51
|
+
const markup = renderToStaticMarkup(
|
|
52
|
+
<ConversationHeader
|
|
53
|
+
conversation={CONVERSATION}
|
|
54
|
+
onBack={noop}
|
|
55
|
+
onDownload={noop}
|
|
56
|
+
onOpenDocuments={noop}
|
|
57
|
+
onReturnToBot={noop}
|
|
58
|
+
onFinish={noop}
|
|
59
|
+
/>,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
expect(markup).toContain('lucide-download')
|
|
63
|
+
expect(markup).not.toContain('✕')
|
|
64
|
+
expect(markup).not.toContain('⬇️')
|
|
65
|
+
})
|
|
66
|
+
})
|