@adatechnology/conversations-ui 0.4.1 → 0.5.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.
Files changed (93) hide show
  1. package/README.md +169 -0
  2. package/dist/{ConversationSimulatorPanel-DnXElfY8.d.ts → ConversationSimulatorPanel-CrfFjiWh.d.ts} +2 -268
  3. package/dist/{chunk-PJNR6UMN.js → chunk-CBYEHPX6.js} +1 -63
  4. package/dist/{chunk-HMCOTZAX.js → chunk-HOKNKXIT.js} +1 -1
  5. package/dist/{chunk-4P5KMWOI.js → chunk-JXJCPN34.js} +327 -522
  6. package/dist/chunk-MRVFFJEH.js +64 -0
  7. package/dist/chunk-RJMSM3YA.js +249 -0
  8. package/dist/chunk-WSFGNR5R.js +55 -0
  9. package/dist/flows/index.js +5 -3
  10. package/dist/index.d.ts +9 -3
  11. package/dist/index.js +783 -807
  12. package/dist/participant/index.d.ts +162 -0
  13. package/dist/participant/index.js +1448 -0
  14. package/dist/preview/index.d.ts +4 -2
  15. package/dist/preview/index.js +4 -2
  16. package/dist/quickReply.types-B4gmZsSr.d.ts +274 -0
  17. package/dist/styles.css +591 -0
  18. package/package.json +9 -5
  19. package/src/ChannelIcon.tsx +2 -0
  20. package/src/MessageBubble.test.tsx +2 -2
  21. package/src/MessageText.test.tsx +59 -0
  22. package/src/MessageText.tsx +20 -12
  23. package/src/MessageTextCopiedBadge.tsx +7 -0
  24. package/src/StatusTicks.test.tsx +30 -0
  25. package/src/StatusTicks.tsx +24 -9
  26. package/src/buildOutput.test.ts +41 -0
  27. package/src/channelCapability.test.ts +17 -0
  28. package/src/conversationChannel.test.ts +30 -1
  29. package/src/conversationChannel.ts +19 -1
  30. package/src/participant/ParticipantAttachmentItem.tsx +77 -0
  31. package/src/participant/ParticipantComposer.test.tsx +97 -0
  32. package/src/participant/ParticipantComposer.tsx +167 -0
  33. package/src/participant/ParticipantConversations.test.tsx +90 -0
  34. package/src/participant/ParticipantConversations.tsx +42 -0
  35. package/src/participant/ParticipantConversationsScreen.tsx +67 -0
  36. package/src/participant/ParticipantInbox.test.tsx +147 -0
  37. package/src/participant/ParticipantInbox.tsx +168 -0
  38. package/src/participant/ParticipantLoadState.tsx +27 -0
  39. package/src/participant/ParticipantMessageBubble.test.tsx +165 -0
  40. package/src/participant/ParticipantMessageBubble.tsx +185 -0
  41. package/src/participant/ParticipantThread.test.tsx +132 -0
  42. package/src/participant/ParticipantThread.tsx +174 -0
  43. package/src/participant/ParticipantThreadScreen.tsx +135 -0
  44. package/src/participant/bindAttachmentUrlResolver.test.ts +25 -0
  45. package/src/participant/bindAttachmentUrlResolver.ts +7 -0
  46. package/src/participant/index.ts +25 -0
  47. package/src/participant/participant.types.ts +43 -0
  48. package/src/participant/participantApi.types.ts +59 -0
  49. package/src/participant/participantAttachmentUrl.test.ts +32 -0
  50. package/src/participant/participantAttachmentUrl.ts +10 -0
  51. package/src/participant/participantBubbleActions.test.ts +44 -0
  52. package/src/participant/participantBubbleActions.ts +40 -0
  53. package/src/participant/participantDrafts.test.ts +65 -0
  54. package/src/participant/participantDrafts.ts +50 -0
  55. package/src/participant/participantFileValidation.test.ts +86 -0
  56. package/src/participant/participantFileValidation.ts +72 -0
  57. package/src/participant/participantFixtures.test-helper.ts +41 -0
  58. package/src/participant/participantGrouping.test.ts +138 -0
  59. package/src/participant/participantGrouping.ts +77 -0
  60. package/src/participant/participantInboxState.test.ts +69 -0
  61. package/src/participant/participantInboxState.ts +74 -0
  62. package/src/participant/participantLabels.ts +86 -0
  63. package/src/participant/participantLoadView.test.ts +23 -0
  64. package/src/participant/participantLoadView.ts +21 -0
  65. package/src/participant/participantMessages.test.ts +128 -0
  66. package/src/participant/participantMessages.ts +73 -0
  67. package/src/participant/participantNewMessages.test.ts +54 -0
  68. package/src/participant/participantNewMessages.ts +29 -0
  69. package/src/participant/participantQuickReply.test.ts +23 -0
  70. package/src/participant/participantQuickReply.ts +6 -0
  71. package/src/participant/participantScroll.test.ts +69 -0
  72. package/src/participant/participantScroll.ts +43 -0
  73. package/src/participant/participantSendController.test.ts +201 -0
  74. package/src/participant/participantSendController.ts +134 -0
  75. package/src/participant/participantSendStates.ts +31 -0
  76. package/src/participant/participantTheme.ts +20 -0
  77. package/src/participant/participantThreadState.test.ts +75 -0
  78. package/src/participant/participantThreadState.ts +59 -0
  79. package/src/participant/shouldMarkRead.test.ts +24 -0
  80. package/src/participant/shouldMarkRead.ts +16 -0
  81. package/src/participant/useDocumentVisibility.ts +18 -0
  82. package/src/participant/useParticipantConversationView.ts +60 -0
  83. package/src/participant/useParticipantDraft.ts +45 -0
  84. package/src/participant/useParticipantInbox.ts +57 -0
  85. package/src/participant/useParticipantMarkRead.ts +26 -0
  86. package/src/participant/useParticipantRevalidation.ts +36 -0
  87. package/src/participant/useParticipantSend.ts +84 -0
  88. package/src/participant/useParticipantThread.ts +116 -0
  89. package/src/participant/useParticipantThreadScroll.ts +26 -0
  90. package/src/participant/useStickToBottom.ts +77 -0
  91. package/src/preview/ConversationSimulatorPanel.tsx +8 -0
  92. package/src/styles.css +160 -0
  93. package/src/types.ts +3 -1
@@ -0,0 +1,84 @@
1
+ import { useCallback, useRef, type MutableRefObject } from 'react'
2
+
3
+ import type { ParticipantMessage, ParticipantSubjectRef } from '@adatechnology/conversation-contracts'
4
+
5
+ import type { ParticipantConversationsApi } from './participantApi.types'
6
+ import type { ParticipantSendContent } from './participantDrafts'
7
+ import {
8
+ createParticipantSendEntry,
9
+ findFailedEntry,
10
+ type ParticipantSendAction,
11
+ type ParticipantSendEntry,
12
+ } from './participantSendController'
13
+
14
+ export type ParticipantSendOutcome = 'sent' | 'queued' | 'failed'
15
+
16
+ export type UseParticipantSendParams = {
17
+ readonly apiRef: MutableRefObject<ParticipantConversationsApi>
18
+ readonly subject: ParticipantSubjectRef
19
+ readonly entries: readonly ParticipantSendEntry[]
20
+ readonly dispatchSend: (action: ParticipantSendAction) => void
21
+ readonly onSentMessage: (message: ParticipantMessage) => void
22
+ }
23
+
24
+ export type UseParticipantSendResult = {
25
+ readonly send: (content: ParticipantSendContent) => Promise<ParticipantSendOutcome>
26
+ readonly retry: (clientMessageId: string) => Promise<void>
27
+ }
28
+
29
+ export function useParticipantSend(params: UseParticipantSendParams): UseParticipantSendResult {
30
+ const { apiRef, subject, dispatchSend, onSentMessage } = params
31
+ const entriesRef = useRef(params.entries)
32
+ entriesRef.current = params.entries
33
+
34
+ const deliver = useCallback(
35
+ async (entry: ParticipantSendEntry): Promise<ParticipantSendOutcome> => {
36
+ const { clientMessageId } = entry
37
+ try {
38
+ const result = await apiRef.current.sendMessage({
39
+ subject: entry.subject,
40
+ clientMessageId,
41
+ ...(entry.text ? { text: entry.text } : {}),
42
+ ...(entry.files.length > 0 ? { files: entry.files } : {}),
43
+ })
44
+ if (result.outcome === 'sent') {
45
+ onSentMessage(result.message)
46
+ dispatchSend({ type: 'sent', clientMessageId, message: result.message })
47
+ return 'sent'
48
+ }
49
+ dispatchSend({ type: 'queued', clientMessageId })
50
+ return 'queued'
51
+ } catch {
52
+ dispatchSend({ type: 'failed', clientMessageId })
53
+ return 'failed'
54
+ }
55
+ },
56
+ [apiRef, dispatchSend, onSentMessage],
57
+ )
58
+
59
+ const send = useCallback(
60
+ async (content: ParticipantSendContent): Promise<ParticipantSendOutcome> => {
61
+ const entry = createParticipantSendEntry({
62
+ clientMessageId: crypto.randomUUID(),
63
+ subject,
64
+ content,
65
+ createdAt: new Date().toISOString(),
66
+ })
67
+ dispatchSend({ type: 'started', entry })
68
+ return deliver(entry)
69
+ },
70
+ [deliver, dispatchSend, subject],
71
+ )
72
+
73
+ const retry = useCallback(
74
+ async (clientMessageId: string): Promise<void> => {
75
+ const entry = findFailedEntry(entriesRef.current, clientMessageId)
76
+ if (!entry) return
77
+ dispatchSend({ type: 'retried', clientMessageId })
78
+ await deliver({ ...entry, state: 'sending' })
79
+ },
80
+ [deliver, dispatchSend],
81
+ )
82
+
83
+ return { send, retry }
84
+ }
@@ -0,0 +1,116 @@
1
+ import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'
2
+
3
+ import type { ParticipantMessage, ParticipantSubjectRef } from '@adatechnology/conversation-contracts'
4
+
5
+ import { channelCapabilityFor } from '../channelCapability'
6
+ import type { ConversationChannel } from '../conversationChannel'
7
+ import type {
8
+ ParticipantConversationEvent,
9
+ ParticipantConversationsApi,
10
+ ParticipantLocalPendingMessage,
11
+ ParticipantPendingMessage,
12
+ } from './participantApi.types'
13
+ import {
14
+ collectKnownClientMessageIds,
15
+ toLocalPending,
16
+ type ParticipantSendAction,
17
+ type ParticipantSendEntry,
18
+ } from './participantSendController'
19
+ import { INITIAL_PARTICIPANT_THREAD_STATE, participantThreadReducer, type ParticipantThreadState } from './participantThreadState'
20
+ import { useParticipantMarkRead } from './useParticipantMarkRead'
21
+ import { useParticipantRevalidation } from './useParticipantRevalidation'
22
+ import { useParticipantSend, type UseParticipantSendResult } from './useParticipantSend'
23
+
24
+ const THREAD_PAGE_SIZE = 30
25
+
26
+ export type UseParticipantThreadParams = {
27
+ /** Keep it stable: a new identity re-subscribes to events (it never resets the conversation). */
28
+ readonly api: ParticipantConversationsApi
29
+ readonly subject: ParticipantSubjectRef
30
+ readonly channel?: ConversationChannel
31
+ /** Unread count of this subject as the inbox knows it; drives markRead. */
32
+ readonly unreadCount?: number
33
+ readonly onMarkedRead?: (subject: ParticipantSubjectRef) => void
34
+ /** Sends of this subject, owned above the conversation so they survive leaving it. */
35
+ readonly sendEntries: readonly ParticipantSendEntry[]
36
+ readonly dispatchSend: (action: ParticipantSendAction) => void
37
+ readonly hostPending?: readonly ParticipantPendingMessage[]
38
+ }
39
+
40
+ export type UseParticipantThreadResult = ParticipantThreadState &
41
+ UseParticipantSendResult & {
42
+ readonly confirmsRead: boolean
43
+ readonly localPending: readonly ParticipantLocalPendingMessage[]
44
+ readonly refresh: () => Promise<void>
45
+ readonly loadOlder: () => Promise<void>
46
+ }
47
+
48
+ function describeError(error: unknown): string {
49
+ return error instanceof Error ? error.message : 'unknown error'
50
+ }
51
+
52
+ function isForSubject(event: ParticipantConversationEvent | undefined, subject: ParticipantSubjectRef): boolean {
53
+ if (!event || event.type === 'inbox-changed') return true
54
+ return event.subject.subjectType === subject.subjectType && event.subject.subjectId === subject.subjectId
55
+ }
56
+
57
+ export function useParticipantThread(params: UseParticipantThreadParams): UseParticipantThreadResult {
58
+ const { api, subject, channel, unreadCount = 0, onMarkedRead, sendEntries, dispatchSend, hostPending = [] } = params
59
+ const { subjectType, subjectId } = subject
60
+ const stableSubject = useMemo<ParticipantSubjectRef>(() => ({ subjectType, subjectId }), [subjectType, subjectId])
61
+ const [state, dispatch] = useReducer(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE)
62
+ const apiRef = useRef(api)
63
+ apiRef.current = api
64
+ const activeSubjectKey = useRef('')
65
+ const confirmsRead = channelCapabilityFor(channel).confirmsRead
66
+
67
+ const refresh = useCallback(async (): Promise<void> => {
68
+ const requestKey = `${subjectType}:${subjectId}`
69
+ dispatch({ type: 'started' })
70
+ try {
71
+ const messages = await apiRef.current.fetchMessages(stableSubject, { limit: THREAD_PAGE_SIZE })
72
+ if (activeSubjectKey.current === requestKey) {
73
+ dispatch({ type: 'loaded', messages, hasMore: messages.length >= THREAD_PAGE_SIZE })
74
+ }
75
+ } catch (error) {
76
+ if (activeSubjectKey.current === requestKey) dispatch({ type: 'failed', error: describeError(error) })
77
+ }
78
+ }, [stableSubject, subjectType, subjectId])
79
+
80
+ const oldestMessageId = state.messages[0]?.id
81
+ const loadOlder = useCallback(async (): Promise<void> => {
82
+ if (!oldestMessageId) return
83
+ const requestKey = `${subjectType}:${subjectId}`
84
+ try {
85
+ const messages = await apiRef.current.fetchMessages(stableSubject, { before: oldestMessageId, limit: THREAD_PAGE_SIZE })
86
+ if (activeSubjectKey.current === requestKey) {
87
+ dispatch({ type: 'olderLoaded', messages, hasMore: messages.length >= THREAD_PAGE_SIZE })
88
+ }
89
+ } catch (error) {
90
+ if (activeSubjectKey.current === requestKey) dispatch({ type: 'failed', error: describeError(error) })
91
+ }
92
+ }, [stableSubject, subjectType, subjectId, oldestMessageId])
93
+
94
+ const handleSentMessage = useCallback((message: ParticipantMessage) => dispatch({ type: 'sendConfirmed', message }), [])
95
+ const sending = useParticipantSend({ apiRef, subject: stableSubject, entries: sendEntries, dispatchSend, onSentMessage: handleSentMessage })
96
+
97
+ useEffect(() => {
98
+ activeSubjectKey.current = `${subjectType}:${subjectId}`
99
+ dispatch({ type: 'reset' })
100
+ void refresh()
101
+ }, [refresh, subjectType, subjectId])
102
+
103
+ useParticipantRevalidation(api, (event) => {
104
+ if (isForSubject(event, stableSubject)) void refresh()
105
+ })
106
+
107
+ useEffect(() => {
108
+ if (sendEntries.length === 0) return
109
+ dispatchSend({ type: 'reflected', knownClientMessageIds: collectKnownClientMessageIds({ messages: state.messages, hostPending }) })
110
+ }, [sendEntries, state.messages, hostPending, dispatchSend])
111
+
112
+ useParticipantMarkRead({ apiRef, subject: stableSubject, unreadCount, messageCount: state.messages.length, onMarkedRead })
113
+
114
+ const localPending = useMemo(() => toLocalPending(sendEntries), [sendEntries])
115
+ return { ...state, localPending, confirmsRead, refresh, loadOlder, ...sending }
116
+ }
@@ -0,0 +1,26 @@
1
+ import { useEffect, useRef } from 'react'
2
+
3
+ import { collectServerMessageIds, countNewIncomingMessages } from './participantNewMessages'
4
+ import type { ParticipantTimelineItem } from './participantMessages'
5
+ import { useStickToBottom, type ParticipantThreadScroll } from './useStickToBottom'
6
+
7
+ export type UseParticipantThreadScrollResult = {
8
+ readonly scroll: ParticipantThreadScroll
9
+ readonly newMessagesCount: number
10
+ }
11
+
12
+ function keyOf(item: ParticipantTimelineItem | undefined): string | undefined {
13
+ if (!item) return undefined
14
+ return item.kind === 'server' ? item.message.id : `pending:${item.pending.clientMessageId}`
15
+ }
16
+
17
+ export function useParticipantThreadScroll(items: readonly ParticipantTimelineItem[]): UseParticipantThreadScrollResult {
18
+ const { scroll, isNearBottom } = useStickToBottom({ firstKey: keyOf(items[0]), lastKey: keyOf(items[items.length - 1]) })
19
+ const seenIds = useRef<ReadonlySet<string>>(new Set())
20
+
21
+ useEffect(() => {
22
+ if (isNearBottom) seenIds.current = collectServerMessageIds(items)
23
+ }, [isNearBottom, items])
24
+
25
+ return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items) }
26
+ }
@@ -0,0 +1,77 @@
1
+ import { useCallback, useLayoutEffect, useRef, useState, type RefObject, type UIEvent } from 'react'
2
+
3
+ import { NEAR_BOTTOM_THRESHOLD_PX, resolveScrollAction, scrollAnchorAfterPrepend, shouldStickToBottom } from './participantScroll'
4
+
5
+ export type UseStickToBottomParams = {
6
+ readonly firstKey: string | undefined
7
+ readonly lastKey: string | undefined
8
+ }
9
+
10
+ export type ParticipantThreadScroll = {
11
+ readonly ref: RefObject<HTMLDivElement | null>
12
+ readonly onScroll: (event: UIEvent<HTMLDivElement>) => void
13
+ }
14
+
15
+ export type UseStickToBottomResult = {
16
+ readonly scroll: ParticipantThreadScroll
17
+ readonly isNearBottom: boolean
18
+ }
19
+
20
+ type Snapshot = {
21
+ hasPositioned: boolean
22
+ firstKey: string | undefined
23
+ lastKey: string | undefined
24
+ scrollTop: number
25
+ scrollHeight: number
26
+ }
27
+
28
+ export function useStickToBottom({ firstKey, lastKey }: UseStickToBottomParams): UseStickToBottomResult {
29
+ const ref = useRef<HTMLDivElement | null>(null)
30
+ const snapshot = useRef<Snapshot>({ hasPositioned: false, firstKey: undefined, lastKey: undefined, scrollTop: 0, scrollHeight: 0 })
31
+ const [isNearBottom, setIsNearBottom] = useState(true)
32
+ const isNearBottomRef = useRef(true)
33
+
34
+ const onScroll = useCallback((event: UIEvent<HTMLDivElement>): void => {
35
+ const { scrollTop, clientHeight, scrollHeight } = event.currentTarget
36
+ const isNear = shouldStickToBottom({ scrollTop, clientHeight, scrollHeight, threshold: NEAR_BOTTOM_THRESHOLD_PX })
37
+ snapshot.current.scrollTop = scrollTop
38
+ snapshot.current.scrollHeight = scrollHeight
39
+ isNearBottomRef.current = isNear
40
+ setIsNearBottom(isNear)
41
+ }, [])
42
+
43
+ useLayoutEffect(() => {
44
+ const element = ref.current
45
+ const previous = snapshot.current
46
+ if (!element) return
47
+ const action = resolveScrollAction({
48
+ hasPositioned: previous.hasPositioned,
49
+ previousFirstKey: previous.firstKey,
50
+ previousLastKey: previous.lastKey,
51
+ firstKey,
52
+ lastKey,
53
+ wasNearBottom: isNearBottomRef.current,
54
+ })
55
+ if (action === 'bottom') {
56
+ element.scrollTop = element.scrollHeight
57
+ isNearBottomRef.current = true
58
+ setIsNearBottom(true)
59
+ }
60
+ if (action === 'anchor') {
61
+ element.scrollTop = scrollAnchorAfterPrepend({
62
+ previousScrollTop: previous.scrollTop,
63
+ previousScrollHeight: previous.scrollHeight,
64
+ scrollHeight: element.scrollHeight,
65
+ })
66
+ }
67
+ snapshot.current = {
68
+ hasPositioned: previous.hasPositioned || action === 'bottom',
69
+ firstKey,
70
+ lastKey,
71
+ scrollTop: element.scrollTop,
72
+ scrollHeight: element.scrollHeight,
73
+ }
74
+ }, [firstKey, lastKey])
75
+
76
+ return { scroll: { ref, onScroll }, isNearBottom }
77
+ }
@@ -62,6 +62,14 @@ const SIMULATOR_PANEL_CHANNEL_WORDING: Readonly<Record<ConversationChannel, Chan
62
62
  destinationHint: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.destinationHint,
63
63
  placeholder: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.placeholder,
64
64
  },
65
+ [CONVERSATION_CHANNEL.APP]: {
66
+ destinationHint: 'entrega no app do contratante',
67
+ placeholder: 'Escreva como quem usa o app…',
68
+ },
69
+ [CONVERSATION_CHANNEL.PORTAL]: {
70
+ destinationHint: 'entrega no portal do contratante',
71
+ placeholder: 'Escreva como quem usa o portal…',
72
+ },
65
73
  [CONVERSATION_CHANNEL.WEBCHAT]: {
66
74
  destinationHint: 'entrega na API do chat do site',
67
75
  placeholder: 'Escreva como o visitante…',
package/src/styles.css CHANGED
@@ -1119,3 +1119,163 @@
1119
1119
  border-top-color: rgb(51 65 85);
1120
1120
  background: rgb(17 24 39);
1121
1121
  }
1122
+
1123
+ .cv-status-ticks { display: flex; align-items: center; line-height: 1; cursor: help; }
1124
+ .cv-status-ticks--queued,
1125
+ .cv-status-ticks--sent,
1126
+ .cv-status-ticks--delivered { color: rgb(0 0 0 / 40%); }
1127
+ .dark .cv-status-ticks--queued,
1128
+ .dark .cv-status-ticks--sent,
1129
+ .dark .cv-status-ticks--delivered { color: rgb(255 255 255 / 40%); }
1130
+ .cv-status-ticks--read { color: rgb(14 165 233); }
1131
+ .cv-status-ticks--failed { color: rgb(239 68 68); }
1132
+
1133
+ .cv-message-text { position: relative; font-size: 14.2px; line-height: 19px; white-space: pre-wrap; overflow-wrap: break-word; word-break: break-word; }
1134
+ .cv-message-text--copyable { user-select: all; cursor: pointer; }
1135
+ .cv-message-text strong { font-weight: 700; }
1136
+ .cv-message-text em { font-style: italic; }
1137
+ .cv-message-text del { text-decoration-line: line-through; }
1138
+ .cv-message-text__copied { position: absolute; top: 0; right: 0; transform: translateY(-100%); background: #3b4a54; color: #fff; font-size: 11px; padding: 0.125rem 0.375rem; border-radius: 0.25rem; box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); }
1139
+
1140
+ /* Participant view (.cv-p-*). Public theming API: override --cv-p-surface, --cv-p-surface-raised,
1141
+ --cv-p-text, --cv-p-text-muted, --cv-p-border, --cv-p-accent, --cv-p-accent-contrast,
1142
+ --cv-p-highlight, --cv-p-danger, --cv-p-radius on .cv-p or any ancestor. */
1143
+ .cv-p {
1144
+ --cv-p-i-surface: var(--cv-p-surface, #ffffff);
1145
+ --cv-p-i-surface-raised: var(--cv-p-surface-raised, #f3f4f6);
1146
+ --cv-p-i-text: var(--cv-p-text, #111827);
1147
+ --cv-p-i-text-muted: var(--cv-p-text-muted, #5b6573);
1148
+ --cv-p-i-border: var(--cv-p-border, #d5d9df);
1149
+ --cv-p-i-accent: var(--cv-p-accent, #a85a1c);
1150
+ --cv-p-i-accent-contrast: var(--cv-p-accent-contrast, #ffffff);
1151
+ --cv-p-i-highlight: var(--cv-p-highlight, #fbefe2);
1152
+ --cv-p-i-danger: var(--cv-p-danger, #c62828);
1153
+ --cv-p-i-radius: var(--cv-p-radius, 0);
1154
+ --cv-p-i-touch: 2.75rem;
1155
+ display: flex;
1156
+ flex-direction: column;
1157
+ min-height: 0;
1158
+ height: 100%;
1159
+ background: var(--cv-p-i-surface);
1160
+ color: var(--cv-p-i-text);
1161
+ font-family: 'Avenir Next', Avenir, 'Segoe UI', sans-serif;
1162
+ line-height: 1.4;
1163
+ box-sizing: border-box;
1164
+ }
1165
+ .dark .cv-p {
1166
+ --cv-p-i-surface: var(--cv-p-surface, #10222c);
1167
+ --cv-p-i-surface-raised: var(--cv-p-surface-raised, #1c2b33);
1168
+ --cv-p-i-text: var(--cv-p-text, #f0f2ee);
1169
+ --cv-p-i-text-muted: var(--cv-p-text-muted, #8fa3ad);
1170
+ --cv-p-i-border: var(--cv-p-border, rgb(143 163 173 / 26%));
1171
+ --cv-p-i-accent: var(--cv-p-accent, #d58a47);
1172
+ --cv-p-i-accent-contrast: var(--cv-p-accent-contrast, #10222c);
1173
+ --cv-p-i-highlight: var(--cv-p-highlight, rgb(213 138 71 / 10%));
1174
+ --cv-p-i-danger: var(--cv-p-danger, #ff6b63);
1175
+ }
1176
+ .cv-p *, .cv-p *::before, .cv-p *::after { box-sizing: border-box; }
1177
+ .cv-p :focus-visible { outline: 2px solid var(--cv-p-i-accent); outline-offset: 2px; }
1178
+ .cv-p-sr-only {
1179
+ position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
1180
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
1181
+ }
1182
+
1183
+ .cv-p-button, .cv-p-chip, .cv-p-row, .cv-p-thread__older, .cv-p-bubble__retry, .cv-p-bubble__action {
1184
+ font: inherit; color: inherit; cursor: pointer;
1185
+ }
1186
+ .cv-p-button {
1187
+ min-height: var(--cv-p-i-touch); min-width: var(--cv-p-i-touch); padding: 0 0.75rem;
1188
+ background: transparent; border: 1px solid var(--cv-p-i-border); border-radius: var(--cv-p-i-radius);
1189
+ display: inline-flex; align-items: center; justify-content: center;
1190
+ }
1191
+ .cv-p-button:disabled { opacity: 0.45; cursor: not-allowed; }
1192
+ .cv-p-button--primary { background: var(--cv-p-i-accent); border-color: var(--cv-p-i-accent); color: var(--cv-p-i-accent-contrast); font-weight: 600; }
1193
+ .cv-p-chip {
1194
+ flex: none; min-height: 2.75rem; padding: 0 0.875rem; display: inline-flex; align-items: center; gap: 0.4rem;
1195
+ background: transparent; border: 1px solid var(--cv-p-i-border); border-radius: var(--cv-p-i-radius); color: var(--cv-p-i-text-muted);
1196
+ font-size: 0.85rem;
1197
+ }
1198
+ .cv-p-chip[aria-pressed='true'] { background: var(--cv-p-i-text); border-color: var(--cv-p-i-text); color: var(--cv-p-i-surface); }
1199
+ .cv-p-chip__count {
1200
+ font: 0.7rem 'SFMono-Regular', Consolas, monospace; background: var(--cv-p-i-accent);
1201
+ color: var(--cv-p-i-accent-contrast); padding: 0 0.35rem;
1202
+ }
1203
+
1204
+ .cv-p-inbox { overflow-y: auto; }
1205
+ .cv-p-inbox__title { margin: 0; padding: 0.75rem 1rem; font-size: 1rem; border-bottom: 1px solid var(--cv-p-i-border); }
1206
+ .cv-p-filters { display: flex; gap: 0.5rem; padding: 0.625rem 1rem; overflow-x: auto; border-bottom: 1px solid var(--cv-p-i-border); }
1207
+ .cv-p-error { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; padding: 1.5rem 1rem; text-align: center; color: var(--cv-p-i-danger, #b91c1c); }
1208
+ .cv-p-error__message { margin: 0; }
1209
+ .cv-p-loading { display: flex; flex-direction: column; gap: 0.5rem; padding: 1rem; }
1210
+ .cv-p-skeleton { display: block; height: 3rem; border-radius: var(--cv-p-i-radius); background: var(--cv-p-i-surface-raised); }
1211
+ .cv-p-inbox__more { display: block; margin: 0.75rem auto; padding: 0 1rem; }
1212
+ .cv-p-empty { padding: 2rem 1rem; text-align: center; color: var(--cv-p-i-text-muted); }
1213
+ .cv-p-section__heading {
1214
+ display: flex; justify-content: space-between; margin: 0; padding: 0.875rem 1rem 0.375rem;
1215
+ font: 0.68rem 'SFMono-Regular', Consolas, monospace; letter-spacing: 0.1em; text-transform: uppercase;
1216
+ color: var(--cv-p-i-text-muted); list-style: none;
1217
+ }
1218
+ .cv-p-section--awaiting .cv-p-section__heading { color: var(--cv-p-i-accent); }
1219
+ .cv-p-section--closed > summary { cursor: pointer; min-height: var(--cv-p-i-touch); align-items: center; }
1220
+ .cv-p-section--closed > summary::-webkit-details-marker { display: none; }
1221
+
1222
+ .cv-p-row {
1223
+ width: 100%; min-height: var(--cv-p-i-touch); display: grid; grid-template-columns: 2.25rem 1fr auto; gap: 0.625rem;
1224
+ padding: 0.75rem 1rem; text-align: left; background: transparent; border: 0;
1225
+ border-bottom: 1px solid var(--cv-p-i-border); border-left: 3px solid transparent;
1226
+ }
1227
+ .cv-p-row--awaiting { border-left-color: var(--cv-p-i-accent); background: var(--cv-p-i-highlight); }
1228
+ .cv-p-row__icon {
1229
+ width: 2.25rem; height: 2.25rem; display: grid; place-items: center; border: 1px solid var(--cv-p-i-border);
1230
+ border-radius: var(--cv-p-i-radius); font: 0.7rem 'SFMono-Regular', Consolas, monospace; text-transform: uppercase; color: var(--cv-p-i-accent);
1231
+ }
1232
+ .cv-p-row__body { display: flex; flex-direction: column; min-width: 0; }
1233
+ .cv-p-row__kind { font: 0.64rem 'SFMono-Regular', Consolas, monospace; letter-spacing: 0.08em; text-transform: uppercase; color: var(--cv-p-i-text-muted); }
1234
+ .cv-p-row__title { font-weight: 600; font-size: 0.92rem; }
1235
+ .cv-p-row__preview { color: var(--cv-p-i-text-muted); font-size: 0.82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1236
+ .cv-p-row__meta { display: flex; flex-direction: column; align-items: flex-end; gap: 0.25rem; font: 0.7rem 'SFMono-Regular', Consolas, monospace; color: var(--cv-p-i-text-muted); }
1237
+ .cv-p-row__unread { min-width: 1.25rem; padding: 0 0.3rem; text-align: center; font-weight: 700; background: var(--cv-p-i-accent); color: var(--cv-p-i-accent-contrast); }
1238
+
1239
+ .cv-p-thread__header { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; border-bottom: 1px solid var(--cv-p-i-border); }
1240
+ .cv-p-thread__title { flex: 1; margin: 0; font-size: 1rem; min-width: 0; }
1241
+ .cv-p-thread__title-button { min-height: var(--cv-p-i-touch); padding: 0; background: none; border: 0; color: inherit; font: inherit; font-weight: 600; text-align: left; text-decoration: underline; cursor: pointer; }
1242
+ .cv-p-thread__subject-card { margin: 0.75rem 1rem 0; padding: 0.625rem 0.75rem; border: 1px solid var(--cv-p-i-border); background: var(--cv-p-i-surface-raised); border-radius: var(--cv-p-i-radius); font-size: 0.82rem; }
1243
+ .cv-p-thread__scroll { flex: 1; min-height: 0; overflow-y: auto; overflow-anchor: none; padding: 0.5rem 1rem 1rem; }
1244
+ .cv-p-thread__older { display: block; margin: 0 auto 0.5rem; min-height: var(--cv-p-i-touch); padding: 0 1rem; background: transparent; border: 1px solid var(--cv-p-i-border); border-radius: var(--cv-p-i-radius); }
1245
+ .cv-p-thread__messages { display: flex; flex-direction: column; gap: 0.5rem; }
1246
+ .cv-p-thread__live { padding: 0 1rem; font-size: 0.75rem; color: var(--cv-p-i-accent); }
1247
+ .cv-p-thread__live:empty { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
1248
+ .cv-p-thread__closed { margin: 0; padding: 1rem; text-align: center; color: var(--cv-p-i-text-muted); border-top: 1px solid var(--cv-p-i-border); }
1249
+ .cv-p-day { align-self: center; margin: 0.5rem 0; font: 0.65rem 'SFMono-Regular', Consolas, monospace; text-transform: uppercase; color: var(--cv-p-i-text-muted); }
1250
+
1251
+ .cv-p-bubble { max-width: 82%; align-self: flex-start; padding: 0.5rem 0.7rem; font-size: 0.88rem; background: var(--cv-p-i-surface-raised); border: 1px solid var(--cv-p-i-border); border-radius: var(--cv-p-i-radius); }
1252
+ .cv-p-bubble--mine { align-self: flex-end; background: var(--cv-p-i-highlight); border-color: var(--cv-p-i-accent); }
1253
+ .cv-p-bubble__author { display: block; margin-bottom: 0.15rem; font: 0.65rem 'SFMono-Regular', Consolas, monospace; text-transform: uppercase; color: var(--cv-p-i-accent); }
1254
+ .cv-p-bubble__meta { display: flex; justify-content: flex-end; align-items: center; gap: 0.35rem; margin-top: 0.25rem; font: 0.65rem 'SFMono-Regular', Consolas, monospace; color: var(--cv-p-i-text-muted); }
1255
+ .cv-p-bubble__status { display: inline-flex; align-items: center; gap: 0.25rem; }
1256
+ .cv-p-bubble__retry { min-height: var(--cv-p-i-touch); display: inline-flex; align-items: center; gap: 0.25rem; padding: 0 0.5rem; background: transparent; border: 1px solid var(--cv-p-i-danger); color: var(--cv-p-i-danger); border-radius: var(--cv-p-i-radius); font-size: 0.7rem; }
1257
+ .cv-p-bubble__actions { display: inline-flex; gap: 0.5rem; margin-top: 0.25rem; }
1258
+ .cv-p-bubble__action { min-height: var(--cv-p-i-touch); display: inline-flex; align-items: center; padding: 0 0.75rem; background: transparent; border: 1px solid var(--cv-p-i-border); color: inherit; border-radius: var(--cv-p-i-radius); font-size: 0.7rem; }
1259
+ .cv-p-attachment { display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.35rem; color: inherit; }
1260
+ .cv-p-attachment--document { flex-direction: row; justify-content: space-between; min-height: var(--cv-p-i-touch); align-items: center; text-decoration: underline; }
1261
+ .cv-p-attachment__image { display: block; max-width: 100%; max-height: 16rem; object-fit: contain; }
1262
+ .cv-p-attachment__audio { width: 100%; min-height: var(--cv-p-i-touch); }
1263
+ .cv-p-attachment__name, .cv-p-attachment__size { font-size: 0.75rem; overflow-wrap: anywhere; }
1264
+
1265
+ .cv-p-composer { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.5rem 1rem 1rem; border-top: 1px solid var(--cv-p-i-border); }
1266
+ .cv-p-composer__row { display: flex; align-items: flex-end; gap: 0.5rem; }
1267
+ .cv-p-composer__input { flex: 1; min-height: var(--cv-p-i-touch); max-height: 8rem; resize: none; padding: 0.7rem; font: inherit; font-size: 0.9rem; color: inherit; background: var(--cv-p-i-surface); border: 1px solid var(--cv-p-i-border); border-radius: var(--cv-p-i-radius); }
1268
+ .cv-p-composer__error { margin: 0; font-size: 0.8rem; color: var(--cv-p-i-danger); }
1269
+ .cv-p-quick { display: flex; gap: 0.5rem; overflow-x: auto; }
1270
+ .cv-p-files { display: flex; flex-direction: column; gap: 0.25rem; margin: 0; padding: 0; list-style: none; }
1271
+ .cv-p-files__item { display: flex; align-items: center; gap: 0.5rem; font-size: 0.8rem; }
1272
+ .cv-p-files__name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1273
+ .cv-p-files__size { color: var(--cv-p-i-text-muted); }
1274
+ .cv-p-files__remove { min-width: var(--cv-p-i-touch); min-height: var(--cv-p-i-touch); background: transparent; border: 0; color: inherit; font: inherit; cursor: pointer; }
1275
+
1276
+ @media (prefers-reduced-motion: no-preference) {
1277
+ .cv-p-row, .cv-p-button, .cv-p-chip { transition: background-color 0.12s, border-color 0.12s; }
1278
+ }
1279
+
1280
+ .cv-p-root { display: flex; flex-direction: column; min-height: 0; height: 100%; }
1281
+ .cv-p-root__screen { display: flex; flex-direction: column; flex: 1; min-height: 0; }
package/src/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { MessageDeliveryStatus } from '@adatechnology/conversation-contracts'
2
+
1
3
  export interface ConversationsUIConfig {
2
4
  apiBaseUrl: string
3
5
  theme?: ConversationsTheme
@@ -72,7 +74,7 @@ export interface MessagePayload {
72
74
  direction: 'inbound' | 'outbound'
73
75
  sender: 'bot' | 'customer' | 'agent'
74
76
  timestamp: string
75
- status?: 'sent' | 'delivered' | 'read' | 'failed'
77
+ status?: MessageDeliveryStatus
76
78
  readAt?: string
77
79
  agentName?: string | null
78
80
  templateName?: string