@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
@@ -68,9 +68,12 @@ const eventClasses = computed(() => {
68
68
 
69
69
  .eventPillTextWrapper {
70
70
  border-radius: 15px;
71
- background: var(--cc-black-80, rgba(0, 0, 0, 0.8));
72
- color: var(--cc-black-20, rgba(0, 0, 0, 0.2));
71
+ background: var(--cc-chat-event-background, rgba(0, 0, 0, 0.8));
72
+ color: var(--cc-chat-event-text-color, #ffffff);
73
73
  padding: 8px 12px;
74
+ box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
75
+ rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
76
+ rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
74
77
  }
75
78
 
76
79
  .eventText {
@@ -71,7 +71,7 @@ const indicatorClasses = computed(() => {
71
71
  align-items: center;
72
72
  background-color: var(--cc-white, #ffffff);
73
73
  border-radius: 15px;
74
- border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
74
+ border: 1px solid var(--cc-border-media-card, var(--cc-black-80, rgba(0, 0, 0, 0.8)));
75
75
  box-sizing: border-box;
76
76
  display: flex;
77
77
  gap: 6px;
@@ -82,6 +82,9 @@ const indicatorClasses = computed(() => {
82
82
  width: 62px;
83
83
  margin-block: var(--webchat-message-margin-block, 24px);
84
84
  margin-inline: var(--webchat-message-margin-inline, 20px);
85
+ box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
86
+ rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
87
+ rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
85
88
  }
86
89
 
87
90
  .typingIndicator.disableBorder {
@@ -1,94 +1,94 @@
1
1
  <script setup lang="ts">
2
- import { computed, useCssModule, useAttrs, CSSProperties } from 'vue'
2
+ import { computed, useCssModule, useAttrs, CSSProperties } from "vue";
3
3
 
4
4
  export type TagVariant =
5
- | 'h1-semibold'
6
- | 'h2-regular'
7
- | 'h2-semibold'
8
- | 'title1-semibold'
9
- | 'title1-regular'
10
- | 'title2-semibold'
11
- | 'title2-regular'
12
- | 'body-regular'
13
- | 'body-semibold'
14
- | 'copy-medium'
15
- | 'cta-semibold'
16
-
17
- type ColorVariant = 'primary' | 'secondary'
5
+ | "h1-semibold"
6
+ | "h2-regular"
7
+ | "h2-semibold"
8
+ | "title1-semibold"
9
+ | "title1-regular"
10
+ | "title2-semibold"
11
+ | "title2-regular"
12
+ | "body-regular"
13
+ | "body-semibold"
14
+ | "copy-medium"
15
+ | "cta-semibold";
16
+
17
+ type ColorVariant = "primary" | "secondary";
18
18
 
19
19
  interface Props {
20
- variant?: TagVariant
21
- component?: string
22
- className?: string
23
- style?: CSSProperties
24
- color?: string
25
- id?: string
26
- ariaHidden?: boolean
27
- tabIndex?: number
20
+ variant?: TagVariant;
21
+ component?: string;
22
+ className?: string;
23
+ style?: CSSProperties;
24
+ color?: string;
25
+ id?: string;
26
+ ariaHidden?: boolean;
27
+ tabIndex?: number;
28
28
  }
29
29
 
30
30
  const props = withDefaults(defineProps<Props>(), {
31
- variant: 'body-regular',
31
+ variant: "body-regular",
32
32
  component: undefined,
33
- className: '',
33
+ className: "",
34
34
  style: undefined,
35
35
  color: undefined,
36
36
  id: undefined,
37
37
  ariaHidden: undefined,
38
38
  tabIndex: undefined,
39
- })
39
+ });
40
40
 
41
- const attrs = useAttrs()
42
- const styles = useCssModule()
41
+ const attrs = useAttrs();
42
+ const styles = useCssModule();
43
43
 
44
44
  // Mapping between variants and default HTML tags
45
45
  const variantsMapping: Record<TagVariant, string> = {
46
- 'h1-semibold': 'h1',
47
- 'h2-regular': 'h2',
48
- 'h2-semibold': 'h2',
49
- 'title1-semibold': 'h3',
50
- 'title1-regular': 'h4',
51
- 'title2-semibold': 'h5',
52
- 'title2-regular': 'h6',
53
- 'body-semibold': 'p',
54
- 'body-regular': 'p',
55
- 'copy-medium': 'p',
56
- 'cta-semibold': 'p',
57
- }
46
+ "h1-semibold": "h1",
47
+ "h2-regular": "h2",
48
+ "h2-semibold": "h2",
49
+ "title1-semibold": "h3",
50
+ "title1-regular": "h4",
51
+ "title2-semibold": "h5",
52
+ "title2-regular": "h6",
53
+ "body-semibold": "p",
54
+ "body-regular": "p",
55
+ "copy-medium": "p",
56
+ "cta-semibold": "p",
57
+ };
58
58
 
59
59
  // Color mapping for predefined colors
60
60
  const colorsMapping: Record<ColorVariant, string> = {
61
- primary: 'var(--cc-primary-color)',
62
- secondary: 'var(--cc-secondary-color)',
63
- }
61
+ primary: "var(--cc-primary-color)",
62
+ secondary: "var(--cc-secondary-color)",
63
+ };
64
64
 
65
65
  // Determine which HTML element to render
66
66
  const componentTag = computed(() => {
67
- return props.component ?? variantsMapping[props.variant]
68
- })
67
+ return props.component ?? variantsMapping[props.variant];
68
+ });
69
69
 
70
70
  // Compute the color value
71
71
  const typographyColor = computed(() => {
72
- if (!props.color) return undefined
73
- return colorsMapping[props.color as ColorVariant] ?? props.color
74
- })
72
+ if (!props.color) return undefined;
73
+ return colorsMapping[props.color as ColorVariant] ?? props.color;
74
+ });
75
75
 
76
76
  // Compute CSS classes
77
77
  const componentClasses = computed(() => {
78
- const classes = [styles[props.variant]]
79
- if (props.className) classes.push(props.className)
80
- if (props.color) classes.push(props.color)
81
- return classes.filter(Boolean).join(' ')
82
- })
78
+ const classes = [styles[props.variant]];
79
+ if (props.className) classes.push(props.className);
80
+ if (props.color) classes.push(props.color);
81
+ return classes.filter(Boolean).join(" ");
82
+ });
83
83
 
84
84
  // Compute merged styles
85
85
  const componentStyle = computed(() => {
86
- const mergedStyle: CSSProperties = { ...props.style }
86
+ const mergedStyle: CSSProperties = { ...props.style };
87
87
  if (typographyColor.value) {
88
- mergedStyle.color = typographyColor.value
88
+ mergedStyle.color = typographyColor.value;
89
89
  }
90
- return mergedStyle
91
- })
90
+ return mergedStyle;
91
+ });
92
92
  </script>
93
93
 
94
94
  <template>
@@ -107,7 +107,6 @@ const componentStyle = computed(() => {
107
107
 
108
108
  <style module>
109
109
  .h1-semibold {
110
- font-family: Figtree;
111
110
  font-size: 2.125rem; /* 34px */
112
111
  font-style: normal;
113
112
  font-weight: 600;
@@ -115,7 +114,6 @@ const componentStyle = computed(() => {
115
114
  }
116
115
 
117
116
  .h2-regular {
118
- font-family: Figtree;
119
117
  font-size: 1.125rem; /* 18px */
120
118
  font-style: normal;
121
119
  font-weight: 400;
@@ -123,7 +121,6 @@ const componentStyle = computed(() => {
123
121
  }
124
122
 
125
123
  .h2-semibold {
126
- font-family: Figtree;
127
124
  font-size: 1.125rem; /* 18px */
128
125
  font-style: normal;
129
126
  font-weight: 600;
@@ -131,7 +128,6 @@ const componentStyle = computed(() => {
131
128
  }
132
129
 
133
130
  .title1-regular {
134
- font-family: Figtree;
135
131
  font-size: 1rem; /* 16px */
136
132
  font-style: normal;
137
133
  font-weight: 400;
@@ -139,7 +135,6 @@ const componentStyle = computed(() => {
139
135
  }
140
136
 
141
137
  .title1-semibold {
142
- font-family: Figtree;
143
138
  font-size: 1rem; /* 16px */
144
139
  font-style: normal;
145
140
  font-weight: 600;
@@ -147,7 +142,6 @@ const componentStyle = computed(() => {
147
142
  }
148
143
 
149
144
  .title2-regular {
150
- font-family: Figtree;
151
145
  font-size: 0.75rem; /* 12px */
152
146
  font-style: normal;
153
147
  font-weight: 400;
@@ -155,7 +149,6 @@ const componentStyle = computed(() => {
155
149
  }
156
150
 
157
151
  .title2-semibold {
158
- font-family: Figtree;
159
152
  font-size: 0.75rem; /* 12px */
160
153
  font-style: normal;
161
154
  font-weight: 600;
@@ -163,7 +156,6 @@ const componentStyle = computed(() => {
163
156
  }
164
157
 
165
158
  .body-regular {
166
- font-family: Figtree;
167
159
  font-size: 0.875rem; /* 14px */
168
160
  font-style: normal;
169
161
  font-weight: 400;
@@ -171,7 +163,6 @@ const componentStyle = computed(() => {
171
163
  }
172
164
 
173
165
  .body-semibold {
174
- font-family: Figtree;
175
166
  font-size: 0.875rem; /* 14px */
176
167
  font-style: normal;
177
168
  font-weight: 600;
@@ -179,7 +170,6 @@ const componentStyle = computed(() => {
179
170
  }
180
171
 
181
172
  .copy-medium {
182
- font-family: Figtree;
183
173
  font-size: 0.75rem; /* 12px */
184
174
  font-style: normal;
185
175
  font-weight: 500;
@@ -187,7 +177,6 @@ const componentStyle = computed(() => {
187
177
  }
188
178
 
189
179
  .cta-semibold {
190
- font-family: Figtree;
191
180
  font-size: 0.875rem; /* 14px */
192
181
  font-style: normal;
193
182
  font-weight: 600;
@@ -270,8 +270,11 @@ onMounted(() => {
270
270
  gap: 10px;
271
271
  background-color: var(--cc-white, #ffffff);
272
272
  border-radius: var(--cc-bubble-border-radius, 15px);
273
- border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
273
+ border: 1px solid var(--cc-border-media-card, var(--cc-black-80, rgba(0, 0, 0, 0.8)));
274
274
  padding: 0px 12px;
275
+ box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
276
+ rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
277
+ rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
275
278
  }
276
279
 
277
280
  .audioWrapper .controls {
@@ -21,16 +21,6 @@
21
21
  </Typography>
22
22
  </div>
23
23
 
24
- <!-- Event name/label (optional) -->
25
- <div
26
- v-if="eventName"
27
- :class="['webchat-date-picker-event', $style.eventName]"
28
- data-testid="datepicker-event"
29
- >
30
- <Typography variant="copy-medium" component="span" :class="$style.eventLabel">
31
- {{ eventName }}
32
- </Typography>
33
- </div>
34
24
  </div>
35
25
  </template>
36
26
 
@@ -59,16 +49,9 @@ const buttonText = computed(() => {
59
49
  return datePickerData.value?.openPickerButtonText || 'Pick date'
60
50
  })
61
51
 
62
- // Event name/label
63
- const eventName = computed(() => {
64
- return datePickerData.value?.eventName
65
- })
66
-
67
- // Check if button should be disabled (for presentation purposes)
68
- // In a real implementation, this would check conversation state
52
+ // Disable button when a date has already been selected
69
53
  const isDisabled = computed(() => {
70
- // Could be extended to check message.data.hasReply or other flags
71
- return false
54
+ return !!selectedDate.value
72
55
  })
73
56
 
74
57
  // Get selected date if available (from message text or reply)
@@ -93,6 +76,9 @@ const selectedDate = computed(() => {
93
76
  flex-direction: column;
94
77
  gap: 8px;
95
78
  align-items: flex-start;
79
+ box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
80
+ rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
81
+ rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
96
82
  }
97
83
 
98
84
  .button {
@@ -124,12 +110,4 @@ const selectedDate = computed(() => {
124
110
  border-radius: var(--cc-bubble-border-radius, 15px);
125
111
  }
126
112
 
127
- .eventName {
128
- padding: 4px 8px;
129
- }
130
-
131
- .eventLabel {
132
- color: var(--cc-black-40, rgba(0, 0, 0, 0.4));
133
- font-style: italic;
134
- }
135
113
  </style>
@@ -149,6 +149,9 @@ const nonImages = computed(() => {
149
149
  object-position: center center;
150
150
  animation: webchatImagePreviewPopIn 0.2s ease-out;
151
151
  transform-origin: center;
152
+ box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
153
+ rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
154
+ rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
152
155
  }
153
156
 
154
157
  .smallImagePreview {
@@ -159,6 +162,9 @@ const nonImages = computed(() => {
159
162
  object-position: center center;
160
163
  animation: webchatImagePreviewPopIn 0.2s ease-out;
161
164
  transform-origin: center;
165
+ box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
166
+ rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
167
+ rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
162
168
  }
163
169
 
164
170
  .filePreview {
@@ -167,8 +173,11 @@ const nonImages = computed(() => {
167
173
  padding: 8px 12px;
168
174
  border-radius: var(--cc-bubble-border-radius, 15px);
169
175
  height: 33px;
170
- background-color: var(--cc-black-95, rgba(0, 0, 0, 0.95));
176
+ background-color: var(--cc-file-preview-background, rgba(0, 0, 0, 0.95));
171
177
  max-width: 295px;
178
+ box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
179
+ rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
180
+ rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
172
181
  }
173
182
 
174
183
  .filePreview .fileNameWrapper {
@@ -176,7 +185,7 @@ const nonImages = computed(() => {
176
185
  flex-direction: row;
177
186
  align-items: center;
178
187
  max-width: 200px;
179
- color: var(--cc-black-10, rgba(0, 0, 0, 0.1));
188
+ color: var(--cc-file-preview-text-color, #ffffff);
180
189
  }
181
190
 
182
191
  .filePreview .fileName {
@@ -190,6 +199,6 @@ const nonImages = computed(() => {
190
199
  }
191
200
 
192
201
  .filePreview .fileSize {
193
- color: var(--cc-black-40, rgba(0, 0, 0, 0.4));
202
+ color: var(--cc-file-preview-secondary-text-color, rgba(255, 255, 255, 0.6));
194
203
  }
195
204
  </style>
@@ -1,9 +1,9 @@
1
1
  <template>
2
- <div v-if="elements.length > 0">
2
+ <div v-if="elements.length > 0" :class="$style.galleryRoot">
3
3
  <!-- Single card (no carousel) -->
4
4
  <div
5
5
  v-if="elements.length === 1"
6
- :class="['webchat-carousel-template-root', $style.wrapper]"
6
+ :class="['webchat-carousel-template-root', $style.wrapper, variantClass]"
7
7
  data-testid="gallery-message"
8
8
  >
9
9
  <GalleryItem :slide="elements[0]" :contentId="`${carouselContentId}-0`" />
@@ -21,22 +21,23 @@
21
21
  }"
22
22
  :pagination="{ clickable: true }"
23
23
  :a11y="{ slideLabelMessage }"
24
- :class="['webchat-carousel-template-root', $style.wrapper]"
24
+ :class="swiperClasses"
25
25
  data-testid="gallery-message"
26
+ @slideChange="handleSlideChange"
26
27
  >
27
28
  <SwiperSlide
28
29
  v-for="(element, index) in elements"
29
30
  :key="index"
30
- style="width: 206px"
31
+ :style="slideStyle"
31
32
  >
32
33
  <GalleryItem :slide="element" :contentId="`${carouselContentId}-${index}`" />
33
34
  </SwiperSlide>
34
35
 
35
- <!-- Navigation buttons -->
36
- <button class="gallery-button-prev">
36
+ <!-- Navigation buttons (click.stop prevents event bubbling to parent card) -->
37
+ <button class="gallery-button-prev" @click.stop>
37
38
  <ArrowBackIcon />
38
39
  </button>
39
- <button class="gallery-button-next">
40
+ <button class="gallery-button-next" @click.stop>
40
41
  <ArrowBackIcon />
41
42
  </button>
42
43
  </Swiper>
@@ -44,14 +45,16 @@
44
45
  </template>
45
46
 
46
47
  <script setup lang="ts">
47
- import { computed, onMounted, useCssModule } from 'vue'
48
+ import { computed, onMounted, useCssModule, nextTick } from 'vue'
48
49
  import { Swiper, SwiperSlide } from 'swiper/vue'
50
+ import type { Swiper as SwiperType } from 'swiper'
49
51
  import { Navigation, Pagination, A11y } from 'swiper/modules'
50
52
  import GalleryItem from './GalleryItem.vue'
51
53
  import { ArrowBackIcon } from '../../assets/svg'
52
54
  import { useMessageContext } from '../../composables/useMessageContext'
53
55
  import { getChannelPayload } from '../../utils/matcher'
54
56
  import { getRandomId } from '../../utils/helpers'
57
+ import { getMessageId } from '../../types'
55
58
  import type { IWebchatTemplateAttachment } from '../../types'
56
59
 
57
60
  // Import Swiper styles
@@ -66,7 +69,7 @@ const $style = useCssModule()
66
69
  const modules = [Navigation, Pagination, A11y]
67
70
 
68
71
  // Message context
69
- const { message, config } = useMessageContext()
72
+ const { message, config, onSlideChange } = useMessageContext()
70
73
 
71
74
  // Get gallery elements from message payload
72
75
  const payload = computed(() => getChannelPayload(message, config))
@@ -78,6 +81,40 @@ const elements = computed(() => {
78
81
  // Generate unique ID for content
79
82
  const carouselContentId = getRandomId('webchatCarouselContentButton')
80
83
 
84
+ // Message ID for DOM queries (aria-live removal)
85
+ const dataMessageId = computed(() => getMessageId(message))
86
+
87
+ // Gallery variant from config
88
+ const variant = computed(() => config?.settings?.layout?.galleryVariant ?? 'default')
89
+
90
+ const variantClass = computed(() => {
91
+ if (variant.value === 'compact') return $style.compact
92
+ if (variant.value === 'copilot') return $style.copilot
93
+ return undefined
94
+ })
95
+
96
+ // Swiper classes (includes variant)
97
+ const swiperClasses = computed(() => {
98
+ return ['webchat-carousel-template-root', $style.wrapper, variantClass.value].filter(Boolean)
99
+ })
100
+
101
+ // Slide width based on variant
102
+ const slideStyle = computed(() => {
103
+ if (variant.value === 'compact') return { width: '170px' }
104
+ if (variant.value === 'copilot') {
105
+ // Copilot: adaptive sizing based on card count
106
+ if (elements.value.length === 1) return { width: '260px' }
107
+ if (elements.value.length === 2) return { width: '220px' }
108
+ return { width: '190px' }
109
+ }
110
+ return { width: '206px' }
111
+ })
112
+
113
+ // Handle slide change event from Swiper
114
+ const handleSlideChange = (swiper: SwiperType) => {
115
+ onSlideChange?.(swiper.activeIndex, elements.value.length)
116
+ }
117
+
81
118
  // Slide label for accessibility
82
119
  const slideLabelMessage = computed(() => {
83
120
  const slide = config?.settings?.customTranslations?.ariaLabels?.slide
@@ -95,8 +132,17 @@ const slideLabelMessage = computed(() => {
95
132
  return `${slide}: ${customSlidePosition}`
96
133
  })
97
134
 
98
- // Auto-focus first button/card on mount
135
+ // Auto-focus first button/card on mount + remove aria-live from swiper-wrapper
99
136
  onMounted(() => {
137
+ // Remove aria-live from swiper-wrapper (matches React behavior — prevents noisy screen reader announcements)
138
+ nextTick(() => {
139
+ const messageEl = document.querySelector(`[data-message-id="${dataMessageId.value}"]`)
140
+ const swiperWrapper = messageEl?.querySelector('.swiper-wrapper')
141
+ if (swiperWrapper) {
142
+ swiperWrapper.removeAttribute('aria-live')
143
+ }
144
+ })
145
+
100
146
  if (!config?.settings?.widgetSettings?.enableAutoFocus) return
101
147
 
102
148
  const chatHistory = document.getElementById('webchatChatHistoryWrapperLiveLogPanel')
@@ -116,6 +162,10 @@ onMounted(() => {
116
162
  </script>
117
163
 
118
164
  <style module>
165
+ .galleryRoot {
166
+ width: 100%;
167
+ }
168
+
119
169
  .slideItem {
120
170
  position: relative;
121
171
  width: 206px;
@@ -293,4 +343,40 @@ onMounted(() => {
293
343
  :global(article) :global(.swiper).wrapper :global(.swiper-pagination-horizontal.swiper-pagination-bullets .swiper-pagination-bullet) {
294
344
  margin: 0 4px;
295
345
  }
346
+
347
+ /*
348
+ ** GALLERY VARIANT: COMPACT
349
+ ** Narrower slides with smaller navigation buttons
350
+ */
351
+ :global(article) :global(.swiper).compact :global(.gallery-button-prev),
352
+ :global(article) :global(.swiper).compact :global(.gallery-button-next) {
353
+ width: 24px;
354
+ height: 24px;
355
+ top: calc(150px / 2 - 6px);
356
+ }
357
+
358
+ :global(article) :global(.swiper).compact :global(.gallery-button-prev) svg,
359
+ :global(article) :global(.swiper).compact :global(.gallery-button-next) svg {
360
+ width: 10px;
361
+ height: 10px;
362
+ }
363
+
364
+ /*
365
+ ** GALLERY VARIANT: COPILOT
366
+ ** Adaptive sizing based on card count with adjusted spacing
367
+ */
368
+ :global(article) :global(.swiper).copilot {
369
+ padding-left: 12px;
370
+ padding-right: 12px;
371
+ margin-left: -12px;
372
+ margin-right: -12px;
373
+ }
374
+
375
+ :global(article) :global(.swiper).copilot :global(.gallery-button-prev) {
376
+ left: 12px;
377
+ }
378
+
379
+ :global(article) :global(.swiper).copilot :global(.gallery-button-next) {
380
+ right: 12px;
381
+ }
296
382
  </style>
@@ -16,8 +16,8 @@
16
16
  v-else
17
17
  :src="slide.image_url"
18
18
  :alt="slide.image_alt_text || ''"
19
- :class="$style.slideImage"
20
19
  @error="handleImageError"
20
+ @click="handleImageClick"
21
21
  />
22
22
  </div>
23
23
 
@@ -54,6 +54,7 @@
54
54
  :dataMessageId="dataMessageId"
55
55
  :onEmitAnalytics="onEmitAnalytics"
56
56
  :templateTextId="slide.title ? titleId : undefined"
57
+ :openXAppOverlay="openXAppOverlay"
57
58
  />
58
59
  </div>
59
60
  </div>
@@ -79,7 +80,7 @@ const props = defineProps<Props>()
79
80
  const $style = useCssModule()
80
81
 
81
82
  // Context and config
82
- const { action, config, onEmitAnalytics } = useMessageContext()
83
+ const { action, config, onEmitAnalytics, messageParams, openXAppOverlay, onImageClick } = useMessageContext()
83
84
  const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
84
85
 
85
86
  // Sanitize HTML
@@ -97,6 +98,13 @@ const handleImageError = () => {
97
98
  isImageBroken.value = true
98
99
  }
99
100
 
101
+ // Handle image click — notify consumer via onImageClick callback
102
+ const handleImageClick = () => {
103
+ if (props.slide.image_url) {
104
+ onImageClick?.(props.slide.image_url)
105
+ }
106
+ }
107
+
100
108
  // Check if card has extra info (subtitle or buttons)
101
109
  const hasExtraInfo = computed(() => {
102
110
  return !!(props.slide.subtitle || (props.slide.buttons && props.slide.buttons.length > 0))
@@ -109,8 +117,7 @@ const defaultActionUrl = computed(() => {
109
117
 
110
118
  // Should buttons be disabled
111
119
  const shouldBeDisabled = computed(() => {
112
- // TODO: Add conversation ended check when messageParams available
113
- return false
120
+ return messageParams?.isConversationEnded ?? false
114
121
  })
115
122
 
116
123
  // Translations
@@ -150,6 +157,11 @@ const handleKeyDown = (event: KeyboardEvent) => {
150
157
  position: relative;
151
158
  width: 206px;
152
159
  overflow: hidden;
160
+ border-radius: var(--cc-bubble-border-radius, 15px);
161
+ background-color: var(--cc-white, #ffffff);
162
+ box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
163
+ rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
164
+ rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
153
165
  }
154
166
 
155
167
  .slideItem .top {
@@ -160,7 +172,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
160
172
  .slideItem .bottom {
161
173
  border-bottom-left-radius: var(--cc-bubble-border-radius, 15px);
162
174
  border-bottom-right-radius: var(--cc-bubble-border-radius, 15px);
163
- border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
175
+ border: 1px solid var(--cc-border-media-card, var(--cc-black-80, rgba(0, 0, 0, 0.8)));
164
176
  background-color: var(--cc-white, #ffffff);
165
177
  display: flex;
166
178
  flex-direction: column;