@cognigy/chat-components-vue 0.2.0 → 0.3.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/dist/chat-components-vue.css +1 -1
- package/dist/chat-components-vue.js +3730 -3634
- package/dist/components/Message.vue.d.ts +4 -0
- package/dist/components/common/Typography.vue.d.ts +1 -1
- package/dist/components/messages/ListItem.vue.d.ts +1 -1
- package/dist/composables/useLiveRegion.d.ts +30 -0
- package/dist/index.d.ts +3 -2
- package/dist/types/index.d.ts +55 -0
- package/dist/utils/theme.d.ts +12 -1
- package/package.json +6 -2
- package/src/components/Message.vue +85 -53
- package/src/components/common/ActionButton.vue +16 -7
- package/src/components/common/ChatBubble.vue +8 -6
- package/src/components/common/ChatEvent.vue +5 -2
- package/src/components/common/TypingIndicator.vue +4 -1
- package/src/components/common/Typography.vue +56 -67
- package/src/components/messages/AudioMessage.vue +4 -1
- package/src/components/messages/DatePicker.vue +5 -27
- package/src/components/messages/FileMessage.vue +12 -3
- package/src/components/messages/Gallery.vue +96 -10
- package/src/components/messages/GalleryItem.vue +17 -5
- package/src/components/messages/ImageMessage.vue +20 -5
- package/src/components/messages/List.vue +56 -42
- package/src/components/messages/ListItem.vue +105 -68
- package/src/components/messages/TextMessage.vue +1 -1
- package/src/components/messages/TextWithButtons.vue +35 -11
- package/src/components/messages/VideoMessage.vue +35 -26
- package/src/composables/useLiveRegion.ts +101 -0
- package/src/index.ts +4 -1
- package/src/types/index.ts +71 -0
- package/src/utils/theme.ts +36 -1
|
@@ -1,94 +1,94 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, useCssModule, useAttrs, CSSProperties } from
|
|
2
|
+
import { computed, useCssModule, useAttrs, CSSProperties } from "vue";
|
|
3
3
|
|
|
4
4
|
export type TagVariant =
|
|
5
|
-
|
|
|
6
|
-
|
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
17
|
-
type ColorVariant =
|
|
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:
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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:
|
|
62
|
-
secondary:
|
|
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
|
-
//
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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="
|
|
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="
|
|
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
|
-
|
|
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;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
:custom-icon="DownloadIcon"
|
|
30
30
|
:position="1"
|
|
31
31
|
:total="1"
|
|
32
|
-
:class-name="$style.
|
|
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
|
-
.
|
|
269
|
-
|
|
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 {
|