@adatechnology/conversations-ui 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -20
- package/dist/{chunk-JXJCPN34.js → chunk-FAQLC3TX.js} +1 -1
- package/dist/{chunk-RJMSM3YA.js → chunk-O6457XHK.js} +17 -11
- package/dist/chunk-OVFCLRQQ.js +84 -0
- package/dist/index.d.ts +14 -4
- package/dist/index.js +3 -3
- package/dist/participant/index.d.ts +25 -1
- package/dist/participant/index.js +454 -194
- package/dist/preview/index.js +2 -2
- package/dist/styles.css +98 -0
- package/package.json +2 -2
- package/src/MessageBubble.test.tsx +2 -2
- package/src/MessageText.test.tsx +27 -10
- package/src/MessageText.tsx +25 -17
- package/src/MessageTextCopiedBadge.tsx +10 -2
- package/src/StatusTicks.test.tsx +27 -6
- package/src/StatusTicks.tsx +30 -15
- package/src/legacyChannels.golden.test.ts +89 -0
- package/src/legacyDefaults.golden.test.tsx +127 -0
- package/src/legacyStyles.fixture.css +1121 -0
- package/src/legacyStyles.golden.test.ts +121 -0
- package/src/messageTextCopyInteraction.test.ts +75 -0
- package/src/messageTextCopyInteraction.ts +37 -0
- package/src/participant/ParticipantChannelBadges.test.tsx +59 -0
- package/src/participant/ParticipantChannelBadges.tsx +75 -0
- package/src/participant/ParticipantConversations.tsx +13 -6
- package/src/participant/ParticipantConversationsScreen.tsx +18 -7
- package/src/participant/ParticipantInbox.test.tsx +99 -2
- package/src/participant/ParticipantInbox.tsx +27 -46
- package/src/participant/ParticipantInboxRow.tsx +75 -0
- package/src/participant/ParticipantInboxSearchField.tsx +34 -0
- package/src/participant/ParticipantMessageBubble.test.tsx +2 -0
- package/src/participant/ParticipantMessageBubble.tsx +3 -3
- package/src/participant/ParticipantProtocolBadge.tsx +74 -0
- package/src/participant/ParticipantProtocolView.test.tsx +208 -0
- package/src/participant/ParticipantThread.test.tsx +19 -2
- package/src/participant/ParticipantThread.tsx +25 -15
- package/src/participant/index.ts +1 -0
- package/src/participant/participant.types.ts +9 -0
- package/src/participant/participantChannels.test.ts +42 -0
- package/src/participant/participantChannels.ts +33 -0
- package/src/participant/participantGrouping.test.ts +101 -3
- package/src/participant/participantGrouping.ts +20 -7
- package/src/participant/participantLabels.ts +29 -0
- package/src/participant/participantProtocol.test.ts +104 -0
- package/src/participant/participantProtocol.ts +52 -0
- package/src/participant/participantProtocolCopy.test.ts +45 -0
- package/src/participant/participantProtocolCopy.ts +32 -0
- package/src/styles.css +19 -1
- package/dist/chunk-WSFGNR5R.js +0 -55
package/dist/preview/index.js
CHANGED
|
@@ -16,9 +16,9 @@ import {
|
|
|
16
16
|
mediaTypeOf,
|
|
17
17
|
simulatorPanelLabelsOf,
|
|
18
18
|
toConversationSimulatorClient
|
|
19
|
-
} from "../chunk-
|
|
19
|
+
} from "../chunk-FAQLC3TX.js";
|
|
20
20
|
import "../chunk-MRVFFJEH.js";
|
|
21
|
-
import "../chunk-
|
|
21
|
+
import "../chunk-O6457XHK.js";
|
|
22
22
|
import "../chunk-CBYEHPX6.js";
|
|
23
23
|
|
|
24
24
|
// src/preview/previewStore.ts
|
package/dist/styles.css
CHANGED
|
@@ -1527,6 +1527,46 @@
|
|
|
1527
1527
|
text-transform: uppercase;
|
|
1528
1528
|
color: var(--cv-p-i-accent);
|
|
1529
1529
|
}
|
|
1530
|
+
.cv-p-inbox__icon {
|
|
1531
|
+
color: var(--cv-p-i-accent);
|
|
1532
|
+
font-size: 1rem;
|
|
1533
|
+
}
|
|
1534
|
+
.cv-p-inbox__icon svg {
|
|
1535
|
+
width: 1.25rem;
|
|
1536
|
+
height: 1.25rem;
|
|
1537
|
+
}
|
|
1538
|
+
.cv-p-row__tags {
|
|
1539
|
+
display: flex;
|
|
1540
|
+
flex-wrap: wrap;
|
|
1541
|
+
align-items: center;
|
|
1542
|
+
gap: 0.25rem 0.5rem;
|
|
1543
|
+
}
|
|
1544
|
+
.cv-p-channels {
|
|
1545
|
+
display: inline-flex;
|
|
1546
|
+
flex-wrap: wrap;
|
|
1547
|
+
gap: 0.25rem;
|
|
1548
|
+
margin: 0;
|
|
1549
|
+
padding: 0;
|
|
1550
|
+
list-style: none;
|
|
1551
|
+
}
|
|
1552
|
+
.cv-p-channel {
|
|
1553
|
+
display: inline-grid;
|
|
1554
|
+
place-items: center;
|
|
1555
|
+
width: 1.375rem;
|
|
1556
|
+
height: 1.375rem;
|
|
1557
|
+
border: 1px solid var(--cv-p-i-border);
|
|
1558
|
+
border-radius: var(--cv-p-i-radius);
|
|
1559
|
+
color: var(--cv-p-i-text-muted);
|
|
1560
|
+
}
|
|
1561
|
+
.cv-p-channel svg {
|
|
1562
|
+
display: block;
|
|
1563
|
+
}
|
|
1564
|
+
.cv-p-channel--whatsapp {
|
|
1565
|
+
color: var(--cv-p-channel-whatsapp, #20914f);
|
|
1566
|
+
}
|
|
1567
|
+
.cv-p-channel--app {
|
|
1568
|
+
color: var(--cv-p-channel-app, var(--cv-p-i-accent));
|
|
1569
|
+
}
|
|
1530
1570
|
.cv-p-row__body {
|
|
1531
1571
|
display: flex;
|
|
1532
1572
|
flex-direction: column;
|
|
@@ -1578,6 +1618,12 @@
|
|
|
1578
1618
|
padding: 0.5rem 1rem;
|
|
1579
1619
|
border-bottom: 1px solid var(--cv-p-i-border);
|
|
1580
1620
|
}
|
|
1621
|
+
.cv-p-thread__heading {
|
|
1622
|
+
flex: 1;
|
|
1623
|
+
min-width: 0;
|
|
1624
|
+
display: flex;
|
|
1625
|
+
flex-direction: column;
|
|
1626
|
+
}
|
|
1581
1627
|
.cv-p-thread__title {
|
|
1582
1628
|
flex: 1;
|
|
1583
1629
|
margin: 0;
|
|
@@ -1823,7 +1869,59 @@
|
|
|
1823
1869
|
font: inherit;
|
|
1824
1870
|
cursor: pointer;
|
|
1825
1871
|
}
|
|
1872
|
+
.cv-p-protocol {
|
|
1873
|
+
min-width: 0;
|
|
1874
|
+
overflow-wrap: anywhere;
|
|
1875
|
+
font:
|
|
1876
|
+
0.7rem "SFMono-Regular",
|
|
1877
|
+
Consolas,
|
|
1878
|
+
monospace;
|
|
1879
|
+
letter-spacing: 0.04em;
|
|
1880
|
+
color: var(--cv-p-i-text-muted);
|
|
1881
|
+
}
|
|
1882
|
+
.cv-p-protocol--header {
|
|
1883
|
+
display: flex;
|
|
1884
|
+
align-items: center;
|
|
1885
|
+
gap: 0.5rem;
|
|
1886
|
+
}
|
|
1887
|
+
.cv-p-protocol__copy {
|
|
1888
|
+
min-height: var(--cv-p-i-touch);
|
|
1889
|
+
min-width: var(--cv-p-i-touch);
|
|
1890
|
+
padding: 0 0.75rem;
|
|
1891
|
+
background: transparent;
|
|
1892
|
+
border: 1px solid var(--cv-p-i-border);
|
|
1893
|
+
border-radius: var(--cv-p-i-radius);
|
|
1894
|
+
color: var(--cv-p-i-text);
|
|
1895
|
+
font: inherit;
|
|
1896
|
+
font-size: 0.75rem;
|
|
1897
|
+
cursor: pointer;
|
|
1898
|
+
}
|
|
1899
|
+
.cv-p-search {
|
|
1900
|
+
padding: 0.625rem 1rem;
|
|
1901
|
+
border-bottom: 1px solid var(--cv-p-i-border);
|
|
1902
|
+
}
|
|
1903
|
+
.cv-p-search__input {
|
|
1904
|
+
width: 100%;
|
|
1905
|
+
box-sizing: border-box;
|
|
1906
|
+
min-height: var(--cv-p-i-touch);
|
|
1907
|
+
padding: 0 0.75rem;
|
|
1908
|
+
font: inherit;
|
|
1909
|
+
font-size: 0.9rem;
|
|
1910
|
+
color: var(--cv-p-i-text);
|
|
1911
|
+
background: var(--cv-p-i-surface);
|
|
1912
|
+
border: 1px solid var(--cv-p-i-border);
|
|
1913
|
+
border-radius: var(--cv-p-i-radius);
|
|
1914
|
+
}
|
|
1915
|
+
.cv-p-search__input:focus-visible,
|
|
1916
|
+
.cv-p-protocol__copy:focus-visible {
|
|
1917
|
+
outline: 2px solid var(--cv-p-i-accent);
|
|
1918
|
+
outline-offset: 2px;
|
|
1919
|
+
}
|
|
1826
1920
|
@media (prefers-reduced-motion: no-preference) {
|
|
1921
|
+
.cv-p-protocol__copy,
|
|
1922
|
+
.cv-p-search__input {
|
|
1923
|
+
transition: border-color 0.12s, background-color 0.12s;
|
|
1924
|
+
}
|
|
1827
1925
|
.cv-p-row,
|
|
1828
1926
|
.cv-p-button,
|
|
1829
1927
|
.cv-p-chip {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adatechnology/conversations-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "WhatsApp conversation UI components — parametrizável por endpoint, tema e feature flags",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"clsx": "^2.1.1",
|
|
36
36
|
"lucide-react": "^1.21.0",
|
|
37
37
|
"tailwind-merge": "^3.6.0",
|
|
38
|
-
"@adatechnology/conversation-contracts": "0.
|
|
38
|
+
"@adatechnology/conversation-contracts": "0.5.0",
|
|
39
39
|
"@adatechnology/meta-whatsapp-contracts": "0.6.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
@@ -56,13 +56,13 @@ describe('MessageBubble e o selo de lida por canal (D4, CA04)', () => {
|
|
|
56
56
|
it('email não confirma leitura — o selo de lida não aparece mesmo com status "read"', () => {
|
|
57
57
|
const markup = renderToStaticMarkup(<MessageBubble message={TEXT_MESSAGE} isMine channel="email" />)
|
|
58
58
|
|
|
59
|
-
expect(markup).not.toContain('
|
|
59
|
+
expect(markup).not.toContain('text-sky-500')
|
|
60
60
|
})
|
|
61
61
|
|
|
62
62
|
it('whatsapp confirma leitura — o selo de lida aparece normalmente', () => {
|
|
63
63
|
const markup = renderToStaticMarkup(<MessageBubble message={TEXT_MESSAGE} isMine channel="whatsapp" />)
|
|
64
64
|
|
|
65
|
-
expect(markup).toContain('
|
|
65
|
+
expect(markup).toContain('text-sky-500')
|
|
66
66
|
})
|
|
67
67
|
|
|
68
68
|
it('sem channel, o padrão continua sendo whatsapp — comportamento de antes desta mudança', () => {
|
package/src/MessageText.test.tsx
CHANGED
|
@@ -15,8 +15,8 @@ const TEXT_MESSAGE: MessagePayload = {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
describe('MessageText', () => {
|
|
18
|
-
it('usa a classe cv-message-text e nenhuma utilitária Tailwind', () => {
|
|
19
|
-
const markup = renderToStaticMarkup(<MessageText message={TEXT_MESSAGE} />)
|
|
18
|
+
it('com appearance="stylesheet" usa a classe cv-message-text e nenhuma utilitária Tailwind', () => {
|
|
19
|
+
const markup = renderToStaticMarkup(<MessageText message={TEXT_MESSAGE} appearance="stylesheet" />)
|
|
20
20
|
|
|
21
21
|
expect(markup).toContain('cv-message-text')
|
|
22
22
|
expect(markup).not.toContain('whitespace-pre-wrap')
|
|
@@ -25,8 +25,8 @@ describe('MessageText', () => {
|
|
|
25
25
|
expect(markup).not.toContain('[&_')
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
it('o badge usa a classe cv-message-text__copied e nenhuma utilitária Tailwind', () => {
|
|
29
|
-
const markup = renderToStaticMarkup(<MessageTextCopiedBadge />)
|
|
28
|
+
it('o badge em stylesheet usa a classe cv-message-text__copied e nenhuma utilitária Tailwind', () => {
|
|
29
|
+
const markup = renderToStaticMarkup(<MessageTextCopiedBadge appearance="stylesheet" />)
|
|
30
30
|
|
|
31
31
|
expect(markup).toContain('cv-message-text__copied')
|
|
32
32
|
expect(markup).not.toContain('absolute')
|
|
@@ -35,16 +35,33 @@ describe('MessageText', () => {
|
|
|
35
35
|
expect(markup).not.toContain('-translate-y-full')
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
it('por padrão
|
|
38
|
+
it('por padrão é o div da 0.4.2: classes Tailwind, sem role nem tabindex', () => {
|
|
39
39
|
const markup = renderToStaticMarkup(<MessageText message={TEXT_MESSAGE} />)
|
|
40
40
|
|
|
41
|
+
expect(markup).toContain('select-all')
|
|
42
|
+
expect(markup).toContain('whitespace-pre-wrap')
|
|
43
|
+
expect(markup).not.toContain('role=')
|
|
44
|
+
expect(markup).not.toContain('tabindex')
|
|
45
|
+
expect(markup).not.toContain('cv-message-text')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('com accessibleCopy liga role=button e tabindex', () => {
|
|
49
|
+
const markup = renderToStaticMarkup(<MessageText message={TEXT_MESSAGE} accessibleCopy />)
|
|
50
|
+
|
|
41
51
|
expect(markup).toContain('role="button"')
|
|
42
52
|
expect(markup).toContain('tabindex="0"')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('em stylesheet a classe copiável (user-select) só aparece quando copia', () => {
|
|
56
|
+
const markup = renderToStaticMarkup(<MessageText message={TEXT_MESSAGE} appearance="stylesheet" />)
|
|
57
|
+
|
|
43
58
|
expect(markup).toContain('cv-message-text--copyable')
|
|
44
59
|
})
|
|
45
60
|
|
|
46
|
-
it('com copyOnClick={false} não copia: sem role, sem tabindex e sem classe copiável', () => {
|
|
47
|
-
const markup = renderToStaticMarkup(
|
|
61
|
+
it('com copyOnClick={false} em stylesheet não copia: sem role, sem tabindex e sem classe copiável', () => {
|
|
62
|
+
const markup = renderToStaticMarkup(
|
|
63
|
+
<MessageText message={TEXT_MESSAGE} copyOnClick={false} accessibleCopy appearance="stylesheet" />,
|
|
64
|
+
)
|
|
48
65
|
|
|
49
66
|
expect(markup).not.toContain('role="button"')
|
|
50
67
|
expect(markup).not.toContain('tabindex')
|
|
@@ -52,8 +69,8 @@ describe('MessageText', () => {
|
|
|
52
69
|
expect(markup).toContain('class="cv-message-text"')
|
|
53
70
|
})
|
|
54
71
|
|
|
55
|
-
it('o badge usa "
|
|
56
|
-
expect(renderToStaticMarkup(<MessageTextCopiedBadge />)).toContain('
|
|
57
|
-
expect(renderToStaticMarkup(<MessageTextCopiedBadge label="
|
|
72
|
+
it('o badge usa "Copiado!" por padrão e aceita outro rótulo', () => {
|
|
73
|
+
expect(renderToStaticMarkup(<MessageTextCopiedBadge />)).toContain('Copiado!')
|
|
74
|
+
expect(renderToStaticMarkup(<MessageTextCopiedBadge label="Copied" />)).toContain('Copied')
|
|
58
75
|
})
|
|
59
76
|
})
|
package/src/MessageText.tsx
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
|
-
import { useCallback, useState
|
|
1
|
+
import { useCallback, useState } from 'react'
|
|
2
2
|
import { CHAT_TEXT_SECONDARY_CLASS } from './theme'
|
|
3
3
|
import type { MessagePayload } from './types'
|
|
4
4
|
import { parseWhatsAppFormatting } from './lib/whatsapp-formatting'
|
|
5
|
-
import {
|
|
5
|
+
import { resolveCopyInteraction } from './messageTextCopyInteraction'
|
|
6
|
+
import { MessageTextCopiedBadge, type MessageTextAppearance } from './MessageTextCopiedBadge'
|
|
6
7
|
|
|
7
8
|
export interface MessageTextProps {
|
|
8
9
|
message: MessagePayload
|
|
9
|
-
/** Defaults to true (tap copies the text, as in 0.
|
|
10
|
+
/** Defaults to true (tap copies the text, as in 0.4.2); false disables copying. */
|
|
10
11
|
copyOnClick?: boolean
|
|
11
|
-
/**
|
|
12
|
+
/** Opt-in: exposes the copy target as a keyboard-operable button (role, tabIndex, Enter/Space). Defaults to false. */
|
|
13
|
+
accessibleCopy?: boolean
|
|
14
|
+
/** Text of the badge shown after copying. Defaults to 'Copiado!', as in 0.4.2. */
|
|
12
15
|
copiedLabel?: string
|
|
16
|
+
/** 'tailwind' (default) keeps the 0.4.2 utility classes; 'stylesheet' uses the .cv-* rules from styles.css. */
|
|
17
|
+
appearance?: MessageTextAppearance
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
const TAILWIND_CLASS =
|
|
21
|
+
'text-[14.2px] leading-[19px] whitespace-pre-wrap break-words select-all [&_strong]:font-bold [&_em]:italic [&_del]:line-through'
|
|
22
|
+
|
|
23
|
+
function resolveClassName(appearance: MessageTextAppearance, copyOnClick: boolean): string {
|
|
24
|
+
if (appearance === 'tailwind') return TAILWIND_CLASS
|
|
25
|
+
return copyOnClick ? 'cv-message-text cv-message-text--copyable' : 'cv-message-text'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function MessageText({
|
|
29
|
+
message, copyOnClick = true, accessibleCopy = false, copiedLabel, appearance = 'tailwind',
|
|
30
|
+
}: MessageTextProps) {
|
|
16
31
|
const [copied, setCopied] = useState(false)
|
|
17
32
|
|
|
18
33
|
const handleCopy = useCallback(async () => {
|
|
@@ -22,28 +37,21 @@ export function MessageText({ message, copyOnClick = true, copiedLabel }: Messag
|
|
|
22
37
|
setCopied(true)
|
|
23
38
|
setTimeout(() => setCopied(false), 2000)
|
|
24
39
|
} catch {
|
|
25
|
-
// Clipboard not available
|
|
26
40
|
}
|
|
27
41
|
}, [copyOnClick, message.content])
|
|
28
42
|
|
|
29
|
-
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
30
|
-
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
31
|
-
event.preventDefault()
|
|
32
|
-
void handleCopy()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
43
|
if (message.type === 'template')
|
|
36
44
|
return <p className={`text-sm italic ${CHAT_TEXT_SECONDARY_CLASS}`}>{message.content ?? 'Template message'}</p>
|
|
37
45
|
|
|
46
|
+
const interactionProps = resolveCopyInteraction({ copyOnClick, accessibleCopy, onCopy: handleCopy })
|
|
47
|
+
|
|
38
48
|
return (
|
|
39
49
|
<div
|
|
40
|
-
className={copyOnClick
|
|
41
|
-
{...
|
|
42
|
-
? { role: 'button', tabIndex: 0, onClick: handleCopy, onKeyDown: handleKeyDown }
|
|
43
|
-
: {})}
|
|
50
|
+
className={resolveClassName(appearance, copyOnClick)}
|
|
51
|
+
{...interactionProps}
|
|
44
52
|
>
|
|
45
53
|
<div>{parseWhatsAppFormatting(message.content ?? '')}</div>
|
|
46
|
-
{copied && <MessageTextCopiedBadge label={copiedLabel} />}
|
|
54
|
+
{copied && <MessageTextCopiedBadge label={copiedLabel} appearance={appearance} />}
|
|
47
55
|
</div>
|
|
48
56
|
)
|
|
49
57
|
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
export type MessageTextAppearance = 'tailwind' | 'stylesheet'
|
|
2
|
+
|
|
1
3
|
export interface MessageTextCopiedBadgeProps {
|
|
2
4
|
label?: string
|
|
5
|
+
/** 'tailwind' (default) keeps the 0.4.2 utility classes; 'stylesheet' uses the .cv-* rules from styles.css. */
|
|
6
|
+
appearance?: MessageTextAppearance
|
|
3
7
|
}
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
const TAILWIND_CLASS =
|
|
10
|
+
'absolute top-0 right-0 -translate-y-full bg-[#3b4a54] text-white text-[11px] px-1.5 py-0.5 rounded shadow-lg'
|
|
11
|
+
|
|
12
|
+
export function MessageTextCopiedBadge({ label = 'Copiado!', appearance = 'tailwind' }: MessageTextCopiedBadgeProps) {
|
|
13
|
+
const className = appearance === 'stylesheet' ? 'cv-message-text__copied' : TAILWIND_CLASS
|
|
14
|
+
return <span className={className}>{label}</span>
|
|
7
15
|
}
|
package/src/StatusTicks.test.tsx
CHANGED
|
@@ -6,22 +6,43 @@ import { StatusTicks } from './StatusTicks'
|
|
|
6
6
|
const TAILWIND_UTILITIES = ['text-sky-500', 'text-red-500', 'text-black/40', 'flex', 'cursor-help', 'leading-none']
|
|
7
7
|
|
|
8
8
|
describe('StatusTicks', () => {
|
|
9
|
-
it('
|
|
10
|
-
const markup = renderToStaticMarkup(<StatusTicks status="
|
|
9
|
+
it('em stylesheet um status desconhecido cai em sent (um tique)', () => {
|
|
10
|
+
const markup = renderToStaticMarkup(<StatusTicks status="xyz" appearance="stylesheet" />)
|
|
11
|
+
|
|
12
|
+
expect(markup).toContain('cv-status-ticks--sent')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('em stylesheet renderiza o estado queued com relógio e rótulo acessível', () => {
|
|
16
|
+
const markup = renderToStaticMarkup(<StatusTicks status="queued" appearance="stylesheet" />)
|
|
11
17
|
|
|
12
18
|
expect(markup).toContain('cv-status-ticks--queued')
|
|
13
19
|
expect(markup).toContain('aria-label="Queued"')
|
|
14
20
|
})
|
|
15
21
|
|
|
16
|
-
it('
|
|
17
|
-
const markup = renderToStaticMarkup(<StatusTicks status="
|
|
22
|
+
it('em stylesheet o rótulo de queued é configurável', () => {
|
|
23
|
+
const markup = renderToStaticMarkup(<StatusTicks status="queued" appearance="stylesheet" queuedLabel="Na fila" />)
|
|
24
|
+
|
|
25
|
+
expect(markup).toContain('aria-label="Na fila"')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('em stylesheet trata bounced como failed', () => {
|
|
29
|
+
const markup = renderToStaticMarkup(<StatusTicks status="bounced" appearance="stylesheet" />)
|
|
18
30
|
|
|
19
31
|
expect(markup).toContain('cv-status-ticks--failed')
|
|
20
32
|
expect(markup).not.toContain('cv-status-ticks--bounced')
|
|
21
33
|
})
|
|
22
34
|
|
|
23
|
-
it
|
|
24
|
-
const markup = renderToStaticMarkup(<StatusTicks status=
|
|
35
|
+
it('por padrão mantém as utilitárias Tailwind e nenhum aria-label (0.4.2)', () => {
|
|
36
|
+
const markup = renderToStaticMarkup(<StatusTicks status="read" />)
|
|
37
|
+
|
|
38
|
+
expect(markup).toContain('text-sky-500')
|
|
39
|
+
expect(markup).toContain('cursor-help')
|
|
40
|
+
expect(markup).not.toContain('cv-status-ticks')
|
|
41
|
+
expect(markup).not.toContain('aria-label')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it.each(['sent', 'delivered', 'read', 'failed', 'queued'])('em stylesheet usa só classes cv-* em %s', (status) => {
|
|
45
|
+
const markup = renderToStaticMarkup(<StatusTicks status={status} appearance="stylesheet" />)
|
|
25
46
|
|
|
26
47
|
expect(markup).toContain(`cv-status-ticks--${status}`)
|
|
27
48
|
for (const utility of TAILWIND_UTILITIES) expect(markup).not.toContain(utility)
|
package/src/StatusTicks.tsx
CHANGED
|
@@ -3,9 +3,20 @@ import { AlertTriangle, Clock } from 'lucide-react'
|
|
|
3
3
|
export interface StatusTicksProps {
|
|
4
4
|
status: string
|
|
5
5
|
title?: string
|
|
6
|
+
/** 'tailwind' (default) keeps the 0.4.2 markup; 'stylesheet' uses .cv-status-ticks--* and adds queued/bounced handling. */
|
|
7
|
+
appearance?: 'tailwind' | 'stylesheet'
|
|
8
|
+
/** Accessible label of the queued state (stylesheet appearance only). Defaults to 'Queued'. */
|
|
9
|
+
queuedLabel?: string
|
|
6
10
|
}
|
|
7
11
|
|
|
8
|
-
const
|
|
12
|
+
const TAILWIND_COLOR_CLASS: Record<string, string> = {
|
|
13
|
+
sent: 'text-black/40 dark:text-white/40',
|
|
14
|
+
delivered: 'text-black/40 dark:text-white/40',
|
|
15
|
+
read: 'text-sky-500',
|
|
16
|
+
failed: 'text-red-500',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const STYLESHEET_MODIFIER: Record<string, string> = {
|
|
9
20
|
queued: 'queued',
|
|
10
21
|
sent: 'sent',
|
|
11
22
|
delivered: 'delivered',
|
|
@@ -14,8 +25,6 @@ const STATUS_MODIFIER: Record<string, string> = {
|
|
|
14
25
|
bounced: 'failed',
|
|
15
26
|
}
|
|
16
27
|
|
|
17
|
-
const QUEUED_LABEL = 'Queued'
|
|
18
|
-
|
|
19
28
|
function Ticks({ double }: { double: boolean }) {
|
|
20
29
|
return (
|
|
21
30
|
<svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -30,17 +39,11 @@ function Ticks({ double }: { double: boolean }) {
|
|
|
30
39
|
)
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return <Ticks double={modifier !== 'sent'} />
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function StatusTicks({ status, title }: StatusTicksProps) {
|
|
42
|
-
const modifier = STATUS_MODIFIER[status] ?? 'sent'
|
|
43
|
-
const ariaLabel = modifier === 'queued' ? QUEUED_LABEL : undefined
|
|
42
|
+
function StylesheetStatusTicks({ status, title, queuedLabel }: { status: string; title?: string; queuedLabel: string }) {
|
|
43
|
+
const modifier = STYLESHEET_MODIFIER[status] ?? 'sent'
|
|
44
|
+
const ariaLabel = modifier === 'queued' ? queuedLabel : undefined
|
|
45
|
+
const icon =
|
|
46
|
+
modifier === 'failed' ? <AlertTriangle size={11} /> : modifier === 'queued' ? <Clock size={11} /> : <Ticks double={modifier !== 'sent'} />
|
|
44
47
|
|
|
45
48
|
return (
|
|
46
49
|
<span
|
|
@@ -48,7 +51,19 @@ export function StatusTicks({ status, title }: StatusTicksProps) {
|
|
|
48
51
|
data-cv-tooltip={title ?? ariaLabel}
|
|
49
52
|
aria-label={ariaLabel}
|
|
50
53
|
>
|
|
51
|
-
|
|
54
|
+
{icon}
|
|
55
|
+
</span>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function StatusTicks({ status, title, appearance = 'tailwind', queuedLabel = 'Queued' }: StatusTicksProps) {
|
|
60
|
+
if (appearance === 'stylesheet') return <StylesheetStatusTicks status={status} title={title} queuedLabel={queuedLabel} />
|
|
61
|
+
|
|
62
|
+
const colorClass = TAILWIND_COLOR_CLASS[status] ?? TAILWIND_COLOR_CLASS.sent
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<span className={`cursor-help leading-none flex items-center ${colorClass}`} data-cv-tooltip={title}>
|
|
66
|
+
{status === 'failed' ? <AlertTriangle size={11} /> : <Ticks double={status !== 'sent'} />}
|
|
52
67
|
</span>
|
|
53
68
|
)
|
|
54
69
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import type { ChannelCapability } from '@adatechnology/conversation-contracts'
|
|
4
|
+
|
|
5
|
+
import { channelCapabilityFor } from './channelCapability'
|
|
6
|
+
import { CHANNEL_FILTER_ALL, channelFiltersFor, type ConversationChannel } from './conversationChannel'
|
|
7
|
+
|
|
8
|
+
const MEGABYTE = 1024 * 1024
|
|
9
|
+
|
|
10
|
+
// Values published by conversation-contracts 0.3.0 (b9f1ef6), read by the 0.4.2 UI.
|
|
11
|
+
const LEGACY_WHATSAPP_CAPABILITY: ChannelCapability = {
|
|
12
|
+
confirmsRead: true,
|
|
13
|
+
sessionWindowHours: 24,
|
|
14
|
+
attachments: { accepted: true, maxBytes: 100 * MEGABYTE, maxTotalBytes: null },
|
|
15
|
+
audio: { plays: true, records: true },
|
|
16
|
+
quickReplies: true,
|
|
17
|
+
requiresTransport: false,
|
|
18
|
+
reachableStatuses: ['queued', 'sent', 'delivered', 'read', 'failed'],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const LEGACY_EMAIL_CAPABILITY: ChannelCapability = {
|
|
22
|
+
confirmsRead: false,
|
|
23
|
+
sessionWindowHours: null,
|
|
24
|
+
attachments: { accepted: true, maxBytes: 10 * MEGABYTE, maxTotalBytes: 25 * MEGABYTE },
|
|
25
|
+
audio: { plays: true, records: true },
|
|
26
|
+
quickReplies: true,
|
|
27
|
+
requiresTransport: true,
|
|
28
|
+
reachableStatuses: ['queued', 'sent', 'delivered', 'failed', 'bounced'],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const LEGACY_WEBCHAT_CAPABILITY: ChannelCapability = {
|
|
32
|
+
confirmsRead: false,
|
|
33
|
+
sessionWindowHours: null,
|
|
34
|
+
attachments: { accepted: true, maxBytes: 10 * MEGABYTE, maxTotalBytes: null },
|
|
35
|
+
audio: { plays: false, records: false },
|
|
36
|
+
quickReplies: true,
|
|
37
|
+
requiresTransport: false,
|
|
38
|
+
reachableStatuses: ['queued', 'delivered', 'failed'],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('channelCapabilityFor mantém os valores da 0.4.2', () => {
|
|
42
|
+
it('G9: whatsapp', () => {
|
|
43
|
+
expect(channelCapabilityFor('whatsapp')).toEqual(LEGACY_WHATSAPP_CAPABILITY)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('G9: email', () => {
|
|
47
|
+
expect(channelCapabilityFor('email')).toEqual(LEGACY_EMAIL_CAPABILITY)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('G9: webchat', () => {
|
|
51
|
+
expect(channelCapabilityFor('webchat')).toEqual(LEGACY_WEBCHAT_CAPABILITY)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('G9: messenger cai na capacidade do WhatsApp', () => {
|
|
55
|
+
expect(channelCapabilityFor('messenger')).toEqual(LEGACY_WHATSAPP_CAPABILITY)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('G9: instagram cai na capacidade do WhatsApp', () => {
|
|
59
|
+
expect(channelCapabilityFor('instagram')).toEqual(LEGACY_WHATSAPP_CAPABILITY)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('G9: canal ausente cai na capacidade do WhatsApp', () => {
|
|
63
|
+
expect(channelCapabilityFor(undefined)).toEqual(LEGACY_WHATSAPP_CAPABILITY)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('channelFiltersFor com canais antigos mantém lista e ordem da 0.4.2', () => {
|
|
68
|
+
it('G9: os cinco canais antigos saem na ordem do catálogo, não na de chegada', () => {
|
|
69
|
+
const channels: ConversationChannel[] = ['email', 'webchat', 'instagram', 'messenger', 'whatsapp']
|
|
70
|
+
|
|
71
|
+
expect(channelFiltersFor(channels.map((channel) => ({ channel })))).toEqual([
|
|
72
|
+
{ value: CHANNEL_FILTER_ALL, label: 'Todos' },
|
|
73
|
+
{ value: 'whatsapp', label: 'WhatsApp' },
|
|
74
|
+
{ value: 'messenger', label: 'Messenger' },
|
|
75
|
+
{ value: 'instagram', label: 'Instagram' },
|
|
76
|
+
{ value: 'webchat', label: 'Chat do site' },
|
|
77
|
+
{ value: 'email', label: 'E-mail' },
|
|
78
|
+
])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('G9: conversa sem canal conta como WhatsApp e um canal só não gera filtro', () => {
|
|
82
|
+
expect(channelFiltersFor([{}, { channel: 'whatsapp' }])).toEqual([])
|
|
83
|
+
expect(channelFiltersFor([{}, { channel: 'email' }])).toEqual([
|
|
84
|
+
{ value: CHANNEL_FILTER_ALL, label: 'Todos' },
|
|
85
|
+
{ value: 'whatsapp', label: 'WhatsApp' },
|
|
86
|
+
{ value: 'email', label: 'E-mail' },
|
|
87
|
+
])
|
|
88
|
+
})
|
|
89
|
+
})
|