@cognigy/chat-components-vue 0.2.0 → 0.3.1

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 (34) hide show
  1. package/dist/chat-components-vue.css +1 -1
  2. package/dist/chat-components-vue.js +3744 -3642
  3. package/dist/components/Message.vue.d.ts +4 -0
  4. package/dist/components/common/ActionButton.vue.d.ts +2 -0
  5. package/dist/components/common/ActionButtons.vue.d.ts +2 -0
  6. package/dist/components/common/Typography.vue.d.ts +1 -1
  7. package/dist/components/messages/ListItem.vue.d.ts +1 -1
  8. package/dist/composables/useLiveRegion.d.ts +30 -0
  9. package/dist/index.d.ts +3 -2
  10. package/dist/types/index.d.ts +55 -0
  11. package/dist/utils/theme.d.ts +12 -1
  12. package/package.json +6 -2
  13. package/src/components/Message.vue +85 -53
  14. package/src/components/common/ActionButton.vue +53 -7
  15. package/src/components/common/ActionButtons.vue +4 -0
  16. package/src/components/common/ChatBubble.vue +8 -6
  17. package/src/components/common/ChatEvent.vue +5 -2
  18. package/src/components/common/TypingIndicator.vue +4 -1
  19. package/src/components/common/Typography.vue +56 -67
  20. package/src/components/messages/AudioMessage.vue +4 -1
  21. package/src/components/messages/DatePicker.vue +5 -27
  22. package/src/components/messages/FileMessage.vue +12 -3
  23. package/src/components/messages/Gallery.vue +96 -10
  24. package/src/components/messages/GalleryItem.vue +17 -5
  25. package/src/components/messages/ImageMessage.vue +20 -5
  26. package/src/components/messages/List.vue +54 -40
  27. package/src/components/messages/ListItem.vue +97 -63
  28. package/src/components/messages/TextMessage.vue +1 -1
  29. package/src/components/messages/TextWithButtons.vue +35 -11
  30. package/src/components/messages/VideoMessage.vue +35 -26
  31. package/src/composables/useLiveRegion.ts +101 -0
  32. package/src/index.ts +4 -1
  33. package/src/types/index.ts +71 -0
  34. package/src/utils/theme.ts +36 -1
@@ -29,7 +29,7 @@
29
29
  :custom-icon="DownloadIcon"
30
30
  :position="1"
31
31
  :total="1"
32
- :class-name="$style.downloadButtonWrapper"
32
+ :class-name="$style.downloadButton"
33
33
  class="webchat-buttons-template-button"
34
34
  />
35
35
 
@@ -88,7 +88,7 @@ import ActionButton from '../common/ActionButton.vue'
88
88
  import { DownloadIcon, CloseIcon } from '../../assets/svg'
89
89
  import type { IWebchatImageAttachment, IWebchatButton } from '../../types'
90
90
 
91
- const { message, config, action, onEmitAnalytics } = useMessageContext()
91
+ const { message, config, action, onEmitAnalytics, onImageClick } = useMessageContext()
92
92
 
93
93
  const $style = useCssModule()
94
94
 
@@ -144,6 +144,11 @@ const closeLabel = computed(() =>
144
144
 
145
145
  // Handlers
146
146
  const handleExpand = () => {
147
+ // Notify consumer via onImageClick callback
148
+ if (imageData.value.url) {
149
+ onImageClick?.(imageData.value.url)
150
+ }
151
+
147
152
  if (isDownloadable.value) {
148
153
  showLightbox.value = true
149
154
  // Focus the download button after lightbox opens
@@ -215,6 +220,10 @@ onUnmounted(() => {
215
220
  max-width: 295px;
216
221
  width: 100%;
217
222
  outline: none;
223
+ background-color: var(--cc-white, #ffffff);
224
+ box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
225
+ rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
226
+ rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
218
227
  }
219
228
 
220
229
  .wrapper .fixedImage,
@@ -238,7 +247,9 @@ onUnmounted(() => {
238
247
  .downloadable {
239
248
  background-color: var(--cc-white, #ffffff);
240
249
  cursor: pointer;
241
- border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
250
+ border: 1px solid var(--cc-border-media-card, var(--cc-black-80, rgba(0, 0, 0, 0.8)));
251
+ border-bottom: none;
252
+ overflow: hidden;
242
253
  }
243
254
 
244
255
  .downloadable img {
@@ -265,8 +276,12 @@ onUnmounted(() => {
265
276
  /* Base class for image containers */
266
277
  }
267
278
 
268
- .downloadButtonWrapper {
269
- padding: 16px;
279
+ .wrapper .downloadButton {
280
+ width: 100%;
281
+ box-sizing: border-box;
282
+ border: 1px solid var(--cc-border-media-card, var(--cc-black-80, rgba(0, 0, 0, 0.8)));
283
+ border-top: none;
284
+ border-radius: 0 0 var(--cc-bubble-border-radius, 15px) var(--cc-bubble-border-radius, 15px);
270
285
  }
271
286
 
272
287
  .brokenImage {
@@ -16,7 +16,9 @@
16
16
 
17
17
  <!-- Regular list items -->
18
18
  <ul
19
- :aria-labelledby="headerElement ? `listHeader-header-${listTemplateId}` : undefined"
19
+ :aria-labelledby="
20
+ headerElement ? `listHeader-header-${listTemplateId}` : undefined
21
+ "
20
22
  :class="$style.list"
21
23
  >
22
24
  <ListItem
@@ -26,7 +28,9 @@
26
28
  :headingLevel="headerElement ? 'h5' : 'h4'"
27
29
  :id="`${listTemplateId}-${index}`"
28
30
  :dividerBefore="index > 0"
29
- :dividerAfter="Boolean(globalButton && index === regularElements.length - 1)"
31
+ :dividerAfter="
32
+ Boolean(globalButton && index === regularElements.length - 1)
33
+ "
30
34
  />
31
35
  </ul>
32
36
 
@@ -39,6 +43,7 @@
39
43
  :containerClassName="$style.mainButtonWrapper"
40
44
  :config="config"
41
45
  :dataMessageId="dataMessageId"
46
+ :openXAppOverlay="openXAppOverlay"
42
47
  :onEmitAnalytics="onEmitAnalytics"
43
48
  size="large"
44
49
  />
@@ -46,86 +51,94 @@
46
51
  </template>
47
52
 
48
53
  <script setup lang="ts">
49
- import { computed, onMounted, useCssModule } from 'vue'
50
- import ListItem from './ListItem.vue'
51
- import ActionButtons from '../common/ActionButtons.vue'
52
- import { useMessageContext } from '../../composables/useMessageContext'
53
- import { getChannelPayload } from '../../utils/matcher'
54
- import { getRandomId } from '../../utils/helpers'
55
- import type { IWebchatTemplateAttachment } from '../../types'
54
+ import { computed, onMounted, useCssModule } from "vue";
55
+ import ListItem from "./ListItem.vue";
56
+ import ActionButtons from "../common/ActionButtons.vue";
57
+ import { useMessageContext } from "../../composables/useMessageContext";
58
+ import { getChannelPayload } from "../../utils/matcher";
59
+ import { getRandomId } from "../../utils/helpers";
60
+ import type { IWebchatTemplateAttachment } from "../../types";
56
61
 
57
- const $style = useCssModule()
62
+ const $style = useCssModule();
58
63
 
59
64
  // Message context
60
- const { message, config, action, onEmitAnalytics } = useMessageContext()
61
- const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
65
+ const { message, config, action, onEmitAnalytics, messageParams, openXAppOverlay } = useMessageContext();
66
+ const dataMessageId = window.__TEST_MESSAGE_ID__; // For testing
62
67
 
63
68
  // Get list data from message payload
64
- const payload = computed(() => getChannelPayload(message, config))
65
- const attachment = computed(() => payload.value?.message?.attachment as IWebchatTemplateAttachment | undefined)
69
+ const payload = computed(() => getChannelPayload(message, config));
70
+ const attachment = computed(
71
+ () =>
72
+ payload.value?.message?.attachment as IWebchatTemplateAttachment | undefined
73
+ );
66
74
 
67
75
  // Extract list elements and configuration
68
76
  const elements = computed(() => {
69
- return attachment.value?.payload?.elements || []
70
- })
77
+ return attachment.value?.payload?.elements || [];
78
+ });
71
79
 
72
80
  const topElementStyle = computed(() => {
73
- return attachment.value?.payload?.top_element_style
74
- })
81
+ return attachment.value?.payload?.top_element_style;
82
+ });
75
83
 
76
84
  const showTopElementLarge = computed(() => {
77
- return topElementStyle.value === 'large' || topElementStyle.value === true
78
- })
85
+ return topElementStyle.value === "large" || topElementStyle.value === true;
86
+ });
79
87
 
80
88
  // Split elements into header and regular items
81
89
  const headerElement = computed(() => {
82
- return showTopElementLarge.value ? elements.value[0] : null
83
- })
90
+ return showTopElementLarge.value ? elements.value[0] : null;
91
+ });
84
92
 
85
93
  const regularElements = computed(() => {
86
- return showTopElementLarge.value ? elements.value.slice(1) : elements.value
87
- })
94
+ return showTopElementLarge.value ? elements.value.slice(1) : elements.value;
95
+ });
88
96
 
89
97
  // Global button (first button in buttons array)
90
98
  const globalButton = computed(() => {
91
- const buttons = attachment.value?.payload?.buttons
92
- return buttons?.[0]
93
- })
99
+ const buttons = attachment.value?.payload?.buttons;
100
+ return buttons?.[0];
101
+ });
94
102
 
95
103
  // Should buttons be disabled
96
104
  const shouldBeDisabled = computed(() => {
97
- // TODO: Add conversation ended check when messageParams available
98
- return false
105
+ return messageParams?.isConversationEnded ?? false
99
106
  })
100
107
 
101
108
  // Generate unique ID for list
102
- const listTemplateId = getRandomId('webchatListTemplateRoot')
109
+ const listTemplateId = getRandomId("webchatListTemplateRoot");
103
110
 
104
111
  // Auto-focus first focusable element on mount
105
112
  onMounted(() => {
106
- if (!config?.settings?.widgetSettings?.enableAutoFocus) return
113
+ if (!config?.settings?.widgetSettings?.enableAutoFocus) return;
107
114
 
108
- const chatHistory = document.getElementById('webchatChatHistoryWrapperLiveLogPanel')
109
- if (!chatHistory?.contains(document.activeElement)) return
115
+ const chatHistory = document.getElementById(
116
+ "webchatChatHistoryWrapperLiveLogPanel"
117
+ );
118
+ if (!chatHistory?.contains(document.activeElement)) return;
110
119
 
111
120
  setTimeout(() => {
112
- const listTemplateRoot = document.getElementById(listTemplateId)
121
+ const listTemplateRoot = document.getElementById(listTemplateId);
113
122
  // Get the first focusable element within the list and add focus
114
123
  const focusable = listTemplateRoot?.querySelectorAll(
115
124
  'button, [href], [tabindex]:not([tabindex="-1"])'
116
- )
117
- const firstFocusable = focusable?.[0] as HTMLElement
118
- firstFocusable?.focus()
119
- }, 200)
120
- })
125
+ );
126
+ const firstFocusable = focusable?.[0] as HTMLElement;
127
+ firstFocusable?.focus();
128
+ }, 200);
129
+ });
121
130
  </script>
122
131
 
123
132
  <style module>
124
133
  .wrapper {
134
+ width: 295px;
125
135
  max-width: 295px;
126
136
  border-radius: var(--cc-bubble-border-radius, 15px);
127
- border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
128
137
  background-color: var(--cc-white, #ffffff);
138
+ color: rgba(0, 0, 0, 0.8);
139
+ box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
140
+ rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
141
+ rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
129
142
  }
130
143
 
131
144
  .wrapper .listItemRoot {
@@ -137,6 +150,7 @@ onMounted(() => {
137
150
  list-style: none;
138
151
  padding-inline-start: 0;
139
152
  margin-block: 0;
153
+ margin-left: 0;
140
154
  }
141
155
 
142
156
  .list .listItemRoot {
@@ -2,7 +2,10 @@
2
2
  <component
3
3
  :is="componentTag"
4
4
  :class="[isHeaderElement && $style.headerRoot, $style.listItemRoot]"
5
- :style="{ backgroundImage: isHeaderElement && element.image_url ? backgroundImage : undefined }"
5
+ :style="{
6
+ backgroundImage:
7
+ isHeaderElement && element.image_url ? backgroundImage : undefined,
8
+ }"
6
9
  :data-testid="isHeaderElement ? 'header-image' : 'list-item'"
7
10
  :id="id"
8
11
  >
@@ -13,17 +16,25 @@
13
16
  <div
14
17
  :class="contentClasses"
15
18
  :role="defaultActionUrl ? 'link' : undefined"
16
- :aria-label="defaultActionUrl ? `${titleHtml}. ${opensInNewTabLabel}` : undefined"
19
+ :aria-label="
20
+ defaultActionUrl ? `${titleHtml}. ${opensInNewTabLabel}` : undefined
21
+ "
17
22
  :aria-describedby="element.subtitle ? subtitleId : undefined"
18
23
  :tabindex="defaultActionUrl ? 0 : -1"
19
- :style="defaultActionUrl && !shouldBeDisabled ? { cursor: 'pointer' } : {}"
24
+ :style="
25
+ defaultActionUrl && !shouldBeDisabled ? { cursor: 'pointer' } : {}
26
+ "
20
27
  @click="handleClick"
21
28
  @keydown="handleKeyDown"
22
29
  >
23
30
  <!-- Header element content -->
24
31
  <div
25
32
  v-if="isHeaderElement"
26
- :class="['webchat-list-template-header-content', $style.headerContent, button && $style.headerContentWithButton]"
33
+ :class="[
34
+ 'webchat-list-template-header-content',
35
+ $style.headerContent,
36
+ button && $style.headerContentWithButton,
37
+ ]"
27
38
  >
28
39
  <!-- Title and subtitle -->
29
40
  <Typography
@@ -31,8 +42,10 @@
31
42
  :variant="isHeaderElement ? 'h2-semibold' : 'title1-semibold'"
32
43
  :component="headingLevel"
33
44
  :class="[
34
- isHeaderElement ? 'webchat-list-template-header-title' : 'webchat-list-template-element-title',
35
- subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle
45
+ isHeaderElement
46
+ ? 'webchat-list-template-header-title'
47
+ : 'webchat-list-template-element-title',
48
+ subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle,
36
49
  ]"
37
50
  :id="isHeaderElement ? `listHeader-${id}` : `listItemHeader-${id}`"
38
51
  v-html="titleHtml"
@@ -42,8 +55,10 @@
42
55
  v-if="subtitleHtml"
43
56
  variant="body-regular"
44
57
  :class="[
45
- isHeaderElement ? 'webchat-list-template-header-subtitle' : 'webchat-list-template-element-subtitle',
46
- $style.itemSubtitle
58
+ isHeaderElement
59
+ ? 'webchat-list-template-header-subtitle'
60
+ : 'webchat-list-template-element-subtitle',
61
+ $style.itemSubtitle,
47
62
  ]"
48
63
  :id="subtitleId"
49
64
  v-html="subtitleHtml"
@@ -53,7 +68,10 @@
53
68
  <!-- Regular list item content -->
54
69
  <div
55
70
  v-else
56
- :class="['webchat-list-template-element-content', $style.listItemContent]"
71
+ :class="[
72
+ 'webchat-list-template-element-content',
73
+ $style.listItemContent,
74
+ ]"
57
75
  >
58
76
  <div :class="$style.listItemText">
59
77
  <!-- Title and subtitle -->
@@ -63,7 +81,7 @@
63
81
  :component="headingLevel"
64
82
  :class="[
65
83
  'webchat-list-template-element-title',
66
- subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle
84
+ subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle,
67
85
  ]"
68
86
  :id="`listItemHeader-${id}`"
69
87
  v-html="titleHtml"
@@ -72,7 +90,10 @@
72
90
  <Typography
73
91
  v-if="subtitleHtml"
74
92
  variant="body-regular"
75
- :class="['webchat-list-template-element-subtitle', $style.itemSubtitle]"
93
+ :class="[
94
+ 'webchat-list-template-element-subtitle',
95
+ $style.itemSubtitle,
96
+ ]"
76
97
  :id="subtitleId"
77
98
  v-html="subtitleHtml"
78
99
  />
@@ -95,12 +116,22 @@
95
116
  v-if="button"
96
117
  :payload="[button]"
97
118
  :action="shouldBeDisabled ? undefined : action"
98
- :buttonClassName="isHeaderElement ? 'webchat-list-template-header-button' : 'webchat-list-template-element-button'"
99
- :containerClassName="isHeaderElement ? $style.listHeaderButtonWrapper : $style.listItemButtonWrapper"
119
+ :buttonClassName="
120
+ isHeaderElement
121
+ ? 'webchat-list-template-header-button'
122
+ : 'webchat-list-template-element-button'
123
+ "
124
+ :containerClassName="
125
+ isHeaderElement
126
+ ? $style.listHeaderButtonWrapper
127
+ : $style.listItemButtonWrapper
128
+ "
100
129
  :config="config"
101
130
  :dataMessageId="dataMessageId"
131
+ :openXAppOverlay="openXAppOverlay"
102
132
  :onEmitAnalytics="onEmitAnalytics"
103
133
  size="large"
134
+ :variant="isHeaderElement ? 'primary' : 'secondary'"
104
135
  />
105
136
 
106
137
  <!-- Divider after item -->
@@ -109,104 +140,106 @@
109
140
  </template>
110
141
 
111
142
  <script setup lang="ts">
112
- import { computed, useCssModule } from 'vue'
113
- import Typography from '../common/Typography.vue'
114
- import ActionButtons from '../common/ActionButtons.vue'
115
- import { useMessageContext } from '../../composables/useMessageContext'
116
- import { useSanitize } from '../../composables/useSanitize'
117
- import { getRandomId, getBackgroundImage } from '../../utils/helpers'
118
- import { sanitizeUrl } from '@braintree/sanitize-url'
119
- import type { IWebchatAttachmentElement } from '../../types'
143
+ import { computed, useCssModule } from "vue";
144
+ import Typography from "../common/Typography.vue";
145
+ import ActionButtons from "../common/ActionButtons.vue";
146
+ import { useMessageContext } from "../../composables/useMessageContext";
147
+ import { useSanitize } from "../../composables/useSanitize";
148
+ import { getRandomId, getBackgroundImage } from "../../utils/helpers";
149
+ import { sanitizeUrl } from "@braintree/sanitize-url";
150
+ import type { IWebchatAttachmentElement } from "../../types";
120
151
 
121
152
  interface Props {
122
- element: IWebchatAttachmentElement
123
- isHeaderElement?: boolean
124
- headingLevel?: 'h4' | 'h5'
125
- id: string
126
- dividerBefore?: boolean
127
- dividerAfter?: boolean
153
+ element: IWebchatAttachmentElement;
154
+ isHeaderElement?: boolean;
155
+ headingLevel?: "h4" | "h5";
156
+ id: string;
157
+ dividerBefore?: boolean;
158
+ dividerAfter?: boolean;
128
159
  }
129
160
 
130
161
  const props = withDefaults(defineProps<Props>(), {
131
162
  isHeaderElement: false,
132
- headingLevel: 'h4',
163
+ headingLevel: "h4",
133
164
  dividerBefore: false,
134
165
  dividerAfter: false,
135
- })
166
+ });
136
167
 
137
- const $style = useCssModule()
168
+ const $style = useCssModule();
138
169
 
139
170
  // Context
140
- const { action, config, onEmitAnalytics } = useMessageContext()
141
- const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
171
+ const { action, config, onEmitAnalytics, messageParams, openXAppOverlay } = useMessageContext();
172
+ const dataMessageId = window.__TEST_MESSAGE_ID__; // For testing
142
173
 
143
174
  // Sanitize HTML
144
- const { processHTML } = useSanitize()
145
- const titleHtml = computed(() => processHTML(props.element.title || ''))
146
- const subtitleHtml = computed(() => processHTML(props.element.subtitle || ''))
175
+ const { processHTML } = useSanitize();
176
+ const titleHtml = computed(() => processHTML(props.element.title || ""));
177
+ const subtitleHtml = computed(() => processHTML(props.element.subtitle || ""));
147
178
 
148
179
  // IDs for accessibility
149
- const subtitleId = getRandomId('webchatListTemplateHeaderSubtitle')
180
+ const subtitleId = getRandomId("webchatListTemplateHeaderSubtitle");
150
181
 
151
182
  // Background image
152
183
  const backgroundImage = computed(() => {
153
- if (!props.element.image_url) return undefined
154
- return getBackgroundImage(props.element.image_url)
155
- })
184
+ if (!props.element.image_url) return undefined;
185
+ return getBackgroundImage(props.element.image_url);
186
+ });
156
187
 
157
188
  // Button (only first button is used)
158
189
  const button = computed(() => {
159
- return props.element.buttons?.[0]
160
- })
190
+ return props.element.buttons?.[0];
191
+ });
161
192
 
162
193
  // Default action URL (clickable item)
163
194
  const defaultActionUrl = computed(() => {
164
- return props.element.default_action?.url
165
- })
195
+ return props.element.default_action?.url;
196
+ });
166
197
 
167
198
  // Should buttons be disabled
168
199
  const shouldBeDisabled = computed(() => {
169
- // TODO: Add conversation ended check when messageParams available
170
- return false
200
+ return messageParams?.isConversationEnded ?? false
171
201
  })
172
202
 
173
203
  // Translations
174
204
  const opensInNewTabLabel = computed(() => {
175
- return config?.settings?.customTranslations?.ariaLabels?.opensInNewTab || 'Opens in new tab'
176
- })
205
+ return (
206
+ config?.settings?.customTranslations?.ariaLabels?.opensInNewTab ||
207
+ "Opens in new tab"
208
+ );
209
+ });
177
210
 
178
211
  // Component tag (div for header, li for regular items)
179
212
  const componentTag = computed(() => {
180
- return props.isHeaderElement ? 'div' : 'li'
181
- })
213
+ return props.isHeaderElement ? "div" : "li";
214
+ });
182
215
 
183
216
  // Content classes
184
217
  const contentClasses = computed(() => {
185
218
  return props.isHeaderElement
186
- ? ['webchat-list-template-header', $style.headerContentWrapper]
187
- : ['webchat-list-template-element', $style.listItemWrapper]
188
- })
219
+ ? ["webchat-list-template-header", $style.headerContentWrapper]
220
+ : ["webchat-list-template-element", $style.listItemWrapper];
221
+ });
189
222
 
190
223
  // Handle item click (default action)
191
224
  const handleClick = () => {
192
- if (shouldBeDisabled.value || !defaultActionUrl.value) return
225
+ if (shouldBeDisabled.value || !defaultActionUrl.value) return;
193
226
 
194
227
  const url = config?.settings?.layout?.disableUrlButtonSanitization
195
228
  ? defaultActionUrl.value
196
- : sanitizeUrl(defaultActionUrl.value)
229
+ : sanitizeUrl(defaultActionUrl.value);
197
230
 
198
231
  // Prevent no-ops from sending you to a blank page
199
- if (url === 'about:blank') return
232
+ if (url === "about:blank") return;
200
233
 
201
- window.open(url)
202
- }
234
+ window.open(url);
235
+ };
203
236
 
204
237
  // Handle keyboard navigation
205
238
  const handleKeyDown = (event: KeyboardEvent) => {
206
- if (defaultActionUrl.value && event.key === 'Enter') {
207
- handleClick()
239
+ if (defaultActionUrl.value && event.key === "Enter") {
240
+ handleClick();
208
241
  }
209
- }
242
+ };
210
243
  </script>
211
244
 
212
245
  <style module>
@@ -215,7 +248,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
215
248
  }
216
249
 
217
250
  .divider {
218
- border-top: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
251
+ border-bottom: 1px solid var(--cc-list-divider-color, var(--cc-border-media-card, var(--cc-black-80, rgba(0, 0, 0, 0.8))));
219
252
  }
220
253
 
221
254
  /* Header element styles */
@@ -284,6 +317,8 @@ const handleKeyDown = (event: KeyboardEvent) => {
284
317
  .itemSubtitle {
285
318
  margin-top: 0px;
286
319
  margin-bottom: 0px;
320
+ color: rgba(0, 0, 0, 0.54);
321
+ white-space: pre-line;
287
322
  }
288
323
 
289
324
  /* Regular list item styles */
@@ -310,7 +345,6 @@ const handleKeyDown = (event: KeyboardEvent) => {
310
345
  align-items: center;
311
346
  gap: 16px;
312
347
  width: 100%;
313
- color: var(--cc-black-20, rgba(0, 0, 0, 0.2));
314
348
  }
315
349
 
316
350
  .listItemText {
@@ -318,7 +352,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
318
352
  }
319
353
 
320
354
  .listItemText > * {
321
- color: var(--cc-black-20, rgba(0, 0, 0, 0.2));
355
+ color: inherit;
322
356
  }
323
357
 
324
358
  .listItemImage {
@@ -122,7 +122,7 @@ const markdownClasses = computed(() => {
122
122
  }
123
123
 
124
124
  .text a:focus-visible {
125
- outline: 2px solid var(--cc-primary-color-focus);
125
+ outline: 2px solid var(--cc-primary-color-focus, var(--cc-primary-color));
126
126
  outline-offset: 2px;
127
127
  }
128
128
 
@@ -9,9 +9,9 @@
9
9
  ignoreLiveRegion
10
10
  />
11
11
 
12
- <!-- Buttons -->
12
+ <!-- Buttons (hidden while progressive rendering is animating) -->
13
13
  <ActionButtons
14
- v-if="buttons.length > 0"
14
+ v-if="buttons.length > 0 && !isStillAnimating"
15
15
  :payload="buttons"
16
16
  :action="modifiedAction"
17
17
  :buttonClassName="buttonClassName"
@@ -20,6 +20,7 @@
20
20
  :config="config"
21
21
  :onEmitAnalytics="onEmitAnalytics"
22
22
  :templateTextId="webchatButtonTemplateTextId"
23
+ :openXAppOverlay="openXAppOverlay"
23
24
  showUrlIcon
24
25
  />
25
26
  </div>
@@ -32,10 +33,10 @@ import ActionButtons from '../common/ActionButtons.vue'
32
33
  import { useMessageContext } from '../../composables/useMessageContext'
33
34
  import { getChannelPayload } from '../../utils/matcher'
34
35
  import { getRandomId } from '../../utils/helpers'
35
- import type { IWebchatTemplateAttachment, IWebchatButton, IWebchatQuickReply } from '../../types'
36
+ import type { IWebchatTemplateAttachment, IWebchatButton, IWebchatQuickReply, IStreamingMessage } from '../../types'
36
37
 
37
38
  // Message context
38
- const { message, config, action, onEmitAnalytics } = useMessageContext()
39
+ const { message, config, action, onEmitAnalytics, messageParams, openXAppOverlay } = useMessageContext()
39
40
 
40
41
  const $style = useCssModule()
41
42
 
@@ -67,13 +68,12 @@ const classType = computed(() => {
67
68
  return isQuickReplies.value ? 'quick-reply' : 'buttons'
68
69
  })
69
70
 
70
- // For quick replies, disable if there's already a reply
71
- // Note: In the React version, this uses messageParams.hasReply
72
- // For now, we'll just pass the action as-is since we don't have messageParams in Vue yet
71
+ // For quick replies, disable if there's already a reply or the conversation has ended
73
72
  const modifiedAction = computed(() => {
74
- // TODO: Implement disabling for quick replies when there's a user reply
75
- // This would require tracking conversation state
76
- return action
73
+ const shouldBeDisabled =
74
+ (isQuickReplies.value && messageParams?.hasReply) ||
75
+ messageParams?.isConversationEnded
76
+ return shouldBeDisabled ? undefined : action
77
77
  })
78
78
 
79
79
  // Generate unique ID for accessibility
@@ -92,6 +92,15 @@ const containerStyle = computed(() => {
92
92
  return {}
93
93
  })
94
94
 
95
+ // Direction mapping for button alignment
96
+ const directionMapping = config?.settings?.widgetSettings?.sourceDirectionMapping
97
+ const messageDirection = computed(() => {
98
+ if (message.source === 'user') return directionMapping?.user || 'outgoing'
99
+ if (message.source === 'bot') return directionMapping?.bot || 'incoming'
100
+ if (message.source === 'agent') return directionMapping?.agent || 'incoming'
101
+ return 'incoming'
102
+ })
103
+
95
104
  // Button class name
96
105
  const buttonClassName = computed(() => {
97
106
  return `${$style.button} webchat-${classType.value}-template-button`
@@ -99,7 +108,14 @@ const buttonClassName = computed(() => {
99
108
 
100
109
  // Container class name
101
110
  const containerClassName = computed(() => {
102
- return `${$style.buttons} webchat-${classType.value}-template-replies-container`
111
+ return `${$style.buttons} ${$style[messageDirection.value]} webchat-${classType.value}-template-replies-container`
112
+ })
113
+
114
+ // Progressive message rendering — hide buttons while message text is still animating
115
+ const isStillAnimating = computed(() => {
116
+ if (!config?.settings?.behavior?.progressiveMessageRendering) return false
117
+ const animState = (message as IStreamingMessage).animationState
118
+ return animState === 'animating' || animState === 'start'
103
119
  })
104
120
  </script>
105
121
 
@@ -116,4 +132,12 @@ const containerClassName = computed(() => {
116
132
  .button {
117
133
  /* Button styles are inherited from ActionButton component */
118
134
  }
135
+
136
+ .incoming {
137
+ justify-content: flex-start;
138
+ }
139
+
140
+ .outgoing {
141
+ justify-content: flex-end;
142
+ }
119
143
  </style>