@cognigy/chat-components-vue 0.1.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 (81) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +178 -0
  3. package/dist/assets/svg/ArrowBackIcon.vue.d.ts +9 -0
  4. package/dist/assets/svg/AudioPauseIcon.vue.d.ts +2 -0
  5. package/dist/assets/svg/AudioPlayIcon.vue.d.ts +2 -0
  6. package/dist/assets/svg/CloseIcon.vue.d.ts +2 -0
  7. package/dist/assets/svg/DownloadIcon.vue.d.ts +2 -0
  8. package/dist/assets/svg/VideoPlayIcon.vue.d.ts +9 -0
  9. package/dist/assets/svg/index.d.ts +7 -0
  10. package/dist/chat-components-vue.css +1 -0
  11. package/dist/chat-components-vue.js +9858 -0
  12. package/dist/components/Message.vue.d.ts +11 -0
  13. package/dist/components/common/ActionButton.vue.d.ts +59 -0
  14. package/dist/components/common/ActionButtons.vue.d.ts +36 -0
  15. package/dist/components/common/ChatBubble.vue.d.ts +22 -0
  16. package/dist/components/common/ChatEvent.vue.d.ts +20 -0
  17. package/dist/components/common/LinkIcon.vue.d.ts +2 -0
  18. package/dist/components/common/TypingIndicator.vue.d.ts +21 -0
  19. package/dist/components/common/Typography.vue.d.ts +38 -0
  20. package/dist/components/messages/AdaptiveCard.vue.d.ts +2 -0
  21. package/dist/components/messages/AudioMessage.vue.d.ts +5 -0
  22. package/dist/components/messages/DatePicker.vue.d.ts +2 -0
  23. package/dist/components/messages/FileMessage.vue.d.ts +2 -0
  24. package/dist/components/messages/Gallery.vue.d.ts +2 -0
  25. package/dist/components/messages/GalleryItem.vue.d.ts +7 -0
  26. package/dist/components/messages/ImageMessage.vue.d.ts +5 -0
  27. package/dist/components/messages/List.vue.d.ts +2 -0
  28. package/dist/components/messages/ListItem.vue.d.ts +16 -0
  29. package/dist/components/messages/TextMessage.vue.d.ts +15 -0
  30. package/dist/components/messages/TextWithButtons.vue.d.ts +2 -0
  31. package/dist/components/messages/VideoMessage.vue.d.ts +5 -0
  32. package/dist/composables/useChannelPayload.d.ts +47 -0
  33. package/dist/composables/useCollation.d.ts +47 -0
  34. package/dist/composables/useImageContext.d.ts +13 -0
  35. package/dist/composables/useMessageContext.d.ts +18 -0
  36. package/dist/composables/useSanitize.d.ts +8 -0
  37. package/dist/index.d.ts +33 -0
  38. package/dist/types/index.d.ts +275 -0
  39. package/dist/utils/helpers.d.ts +56 -0
  40. package/dist/utils/matcher.d.ts +20 -0
  41. package/dist/utils/sanitize.d.ts +28 -0
  42. package/dist/utils/theme.d.ts +18 -0
  43. package/package.json +94 -0
  44. package/src/assets/svg/ArrowBackIcon.vue +30 -0
  45. package/src/assets/svg/AudioPauseIcon.vue +20 -0
  46. package/src/assets/svg/AudioPlayIcon.vue +19 -0
  47. package/src/assets/svg/CloseIcon.vue +10 -0
  48. package/src/assets/svg/DownloadIcon.vue +10 -0
  49. package/src/assets/svg/VideoPlayIcon.vue +25 -0
  50. package/src/assets/svg/index.ts +7 -0
  51. package/src/components/Message.vue +152 -0
  52. package/src/components/common/ActionButton.vue +354 -0
  53. package/src/components/common/ActionButtons.vue +170 -0
  54. package/src/components/common/ChatBubble.vue +109 -0
  55. package/src/components/common/ChatEvent.vue +84 -0
  56. package/src/components/common/LinkIcon.vue +34 -0
  57. package/src/components/common/TypingIndicator.vue +202 -0
  58. package/src/components/common/Typography.vue +196 -0
  59. package/src/components/messages/AdaptiveCard.vue +292 -0
  60. package/src/components/messages/AudioMessage.vue +391 -0
  61. package/src/components/messages/DatePicker.vue +135 -0
  62. package/src/components/messages/FileMessage.vue +195 -0
  63. package/src/components/messages/Gallery.vue +296 -0
  64. package/src/components/messages/GalleryItem.vue +214 -0
  65. package/src/components/messages/ImageMessage.vue +368 -0
  66. package/src/components/messages/List.vue +149 -0
  67. package/src/components/messages/ListItem.vue +344 -0
  68. package/src/components/messages/TextMessage.vue +203 -0
  69. package/src/components/messages/TextWithButtons.vue +119 -0
  70. package/src/components/messages/VideoMessage.vue +343 -0
  71. package/src/composables/useChannelPayload.ts +101 -0
  72. package/src/composables/useCollation.ts +163 -0
  73. package/src/composables/useImageContext.ts +27 -0
  74. package/src/composables/useMessageContext.ts +41 -0
  75. package/src/composables/useSanitize.ts +25 -0
  76. package/src/index.ts +71 -0
  77. package/src/types/index.ts +373 -0
  78. package/src/utils/helpers.ts +164 -0
  79. package/src/utils/matcher.ts +283 -0
  80. package/src/utils/sanitize.ts +133 -0
  81. package/src/utils/theme.ts +58 -0
@@ -0,0 +1,135 @@
1
+ <template>
2
+ <div v-if="isDatePickerMessage" data-testid="datepicker-message" :class="$style.wrapper">
3
+ <!-- Date picker button -->
4
+ <button
5
+ :class="['webchat-date-picker-button', $style.button]"
6
+ :disabled="isDisabled"
7
+ data-testid="datepicker-button"
8
+ type="button"
9
+ >
10
+ {{ buttonText }}
11
+ </button>
12
+
13
+ <!-- Selected date display (if already selected) -->
14
+ <div
15
+ v-if="selectedDate"
16
+ :class="['webchat-date-picker-selected', $style.selectedDate]"
17
+ data-testid="datepicker-selected"
18
+ >
19
+ <Typography variant="body-regular" component="span">
20
+ Selected: {{ selectedDate }}
21
+ </Typography>
22
+ </div>
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
+ </div>
35
+ </template>
36
+
37
+ <script setup lang="ts">
38
+ import { computed } from 'vue'
39
+ import Typography from '../common/Typography.vue'
40
+ import { useMessageContext } from '../../composables/useMessageContext'
41
+ import type { IDatePickerData } from '../../types'
42
+
43
+ // Message context
44
+ const { message } = useMessageContext()
45
+
46
+ // Check if this is a date picker message
47
+ const isDatePickerMessage = computed(() => {
48
+ return message?.data?._plugin?.type === 'date-picker'
49
+ })
50
+
51
+ // Get date picker data
52
+ const datePickerData = computed((): IDatePickerData | null => {
53
+ if (!isDatePickerMessage.value) return null
54
+ return (message.data?._plugin?.data as IDatePickerData) || null
55
+ })
56
+
57
+ // Button text
58
+ const buttonText = computed(() => {
59
+ return datePickerData.value?.openPickerButtonText || 'Pick date'
60
+ })
61
+
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
69
+ const isDisabled = computed(() => {
70
+ // Could be extended to check message.data.hasReply or other flags
71
+ return false
72
+ })
73
+
74
+ // Get selected date if available (from message text or reply)
75
+ const selectedDate = computed(() => {
76
+ // If message has text, assume it's the selected date
77
+ if (message.text && message.text.trim()) {
78
+ return message.text
79
+ }
80
+
81
+ // Could also check for default date
82
+ if (datePickerData.value?.defaultDate) {
83
+ return datePickerData.value.defaultDate
84
+ }
85
+
86
+ return null
87
+ })
88
+ </script>
89
+
90
+ <style module>
91
+ .wrapper {
92
+ display: flex;
93
+ flex-direction: column;
94
+ gap: 8px;
95
+ align-items: flex-start;
96
+ }
97
+
98
+ .button {
99
+ min-width: 120px;
100
+ padding: 10px 16px;
101
+ border: none;
102
+ border-radius: var(--cc-button-border-radius, 8px);
103
+ background-color: var(--cc-primary-color, #1976d2);
104
+ color: var(--cc-white, #ffffff);
105
+ font-size: 14px;
106
+ font-weight: 600;
107
+ cursor: pointer;
108
+ transition: all 0.2s ease;
109
+ }
110
+
111
+ .button:hover:not(:disabled) {
112
+ opacity: 0.9;
113
+ }
114
+
115
+ .button:disabled {
116
+ opacity: 0.5;
117
+ cursor: not-allowed;
118
+ }
119
+
120
+ .selectedDate {
121
+ padding: 8px 12px;
122
+ background-color: var(--cc-primary-color, #1976d2);
123
+ color: var(--cc-white, #ffffff);
124
+ border-radius: var(--cc-bubble-border-radius, 15px);
125
+ }
126
+
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
+ </style>
@@ -0,0 +1,195 @@
1
+ <template>
2
+ <div v-if="hasAttachments">
3
+ <div
4
+ :class="['webchat-media-files-template-root', $style.filesWrapper]"
5
+ >
6
+ <!-- Image attachments -->
7
+ <div
8
+ v-if="images.length > 0"
9
+ :class="['webchat-media-template-image-container', $style.filePreviewContainer]"
10
+ >
11
+ <a
12
+ v-for="(attachment, index) in images"
13
+ :key="index"
14
+ :href="attachment.url"
15
+ target="_blank"
16
+ rel="noopener noreferrer"
17
+ :style="{ textDecoration: 'none' }"
18
+ >
19
+ <img
20
+ :src="attachment.url"
21
+ :alt="`${attachment.fileName} (${getSizeLabel(attachment.size)})`"
22
+ :title="`${attachment.fileName} (${getSizeLabel(attachment.size)})`"
23
+ :class="[
24
+ 'webchat-media-template-image',
25
+ attachments.length > 1 ? $style.smallImagePreview : $style.imagePreview
26
+ ]"
27
+ data-testid="image-attachment"
28
+ />
29
+ </a>
30
+ </div>
31
+
32
+ <!-- Non-image file attachments -->
33
+ <div
34
+ v-if="nonImages.length > 0"
35
+ :class="['webchat-media-template-files-container', $style.filePreviewContainer]"
36
+ >
37
+ <a
38
+ v-for="(attachment, index) in nonImages"
39
+ :key="index"
40
+ :href="attachment.url"
41
+ target="_blank"
42
+ rel="noopener noreferrer"
43
+ :style="{ textDecoration: 'none' }"
44
+ >
45
+ <div
46
+ :class="['webchat-media-template-file', $style.filePreview]"
47
+ data-testid="file-attachment"
48
+ >
49
+ <div :class="$style.fileNameWrapper">
50
+ <Typography
51
+ component="span"
52
+ variant="title2-regular"
53
+ :class="$style.fileName"
54
+ >
55
+ {{ getFileName(attachment.fileName) }}
56
+ </Typography>
57
+ <Typography
58
+ component="span"
59
+ variant="title2-regular"
60
+ :class="$style.fileExtension"
61
+ >
62
+ {{ getFileExtension(attachment.fileName) }}
63
+ </Typography>
64
+ </div>
65
+ <Typography
66
+ component="span"
67
+ variant="title2-regular"
68
+ :class="$style.fileSize"
69
+ >
70
+ {{ getSizeLabel(attachment.size) }}
71
+ </Typography>
72
+ </div>
73
+ </a>
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Text content below files -->
78
+ <TextMessage
79
+ v-if="text"
80
+ :content="text"
81
+ />
82
+ </div>
83
+ </template>
84
+
85
+ <script setup lang="ts">
86
+ import { computed } from 'vue'
87
+ import Typography from '../common/Typography.vue'
88
+ import TextMessage from './TextMessage.vue'
89
+ import { useMessageContext } from '../../composables/useMessageContext'
90
+ import { getFileName, getFileExtension, getSizeLabel, isImageAttachment } from '../../utils/helpers'
91
+ import type { IUploadFileAttachmentData } from '../../types'
92
+
93
+ // Message context
94
+ const { message } = useMessageContext()
95
+
96
+ // Get attachments and text from message
97
+ const attachments = computed(() => {
98
+ return (message.data?.attachments as IUploadFileAttachmentData[]) || []
99
+ })
100
+
101
+ const text = computed(() => message.text)
102
+
103
+ const hasAttachments = computed(() => {
104
+ return attachments.value && attachments.value.length > 0
105
+ })
106
+
107
+ // Sort attachments by file type, valid images first
108
+ const images = computed(() => {
109
+ return attachments.value.filter(attachment => isImageAttachment(attachment.mimeType))
110
+ })
111
+
112
+ const nonImages = computed(() => {
113
+ return attachments.value.filter(attachment => !isImageAttachment(attachment.mimeType))
114
+ })
115
+ </script>
116
+
117
+ <style module>
118
+ .filesWrapper {
119
+ display: flex;
120
+ flex-direction: column;
121
+ align-items: flex-start;
122
+ gap: 8px;
123
+ margin-bottom: 8px;
124
+ }
125
+
126
+ .filePreviewContainer {
127
+ display: flex;
128
+ flex-direction: row;
129
+ flex-wrap: wrap;
130
+ gap: 8px;
131
+ }
132
+
133
+ @keyframes webchatImagePreviewPopIn {
134
+ from {
135
+ opacity: 0;
136
+ transform: scale(0.95);
137
+ }
138
+ to {
139
+ opacity: 1;
140
+ transform: scale(1);
141
+ }
142
+ }
143
+
144
+ .imagePreview {
145
+ border-radius: var(--cc-bubble-border-radius, 15px);
146
+ max-height: 256px;
147
+ max-width: 295px;
148
+ object-fit: cover;
149
+ object-position: center center;
150
+ animation: webchatImagePreviewPopIn 0.2s ease-out;
151
+ transform-origin: center;
152
+ }
153
+
154
+ .smallImagePreview {
155
+ border-radius: var(--cc-bubble-border-radius, 15px);
156
+ height: 128px;
157
+ width: 128px;
158
+ object-fit: cover;
159
+ object-position: center center;
160
+ animation: webchatImagePreviewPopIn 0.2s ease-out;
161
+ transform-origin: center;
162
+ }
163
+
164
+ .filePreview {
165
+ display: flex;
166
+ gap: 12px;
167
+ padding: 8px 12px;
168
+ border-radius: var(--cc-bubble-border-radius, 15px);
169
+ height: 33px;
170
+ background-color: var(--cc-black-95, rgba(0, 0, 0, 0.95));
171
+ max-width: 295px;
172
+ }
173
+
174
+ .filePreview .fileNameWrapper {
175
+ display: flex;
176
+ flex-direction: row;
177
+ align-items: center;
178
+ max-width: 200px;
179
+ color: var(--cc-black-10, rgba(0, 0, 0, 0.1));
180
+ }
181
+
182
+ .filePreview .fileName {
183
+ white-space: nowrap;
184
+ text-overflow: ellipsis;
185
+ overflow: hidden;
186
+ }
187
+
188
+ .filePreview .fileExtension {
189
+ max-width: 60px;
190
+ }
191
+
192
+ .filePreview .fileSize {
193
+ color: var(--cc-black-40, rgba(0, 0, 0, 0.4));
194
+ }
195
+ </style>
@@ -0,0 +1,296 @@
1
+ <template>
2
+ <div v-if="elements.length > 0">
3
+ <!-- Single card (no carousel) -->
4
+ <div
5
+ v-if="elements.length === 1"
6
+ :class="['webchat-carousel-template-root', $style.wrapper]"
7
+ data-testid="gallery-message"
8
+ >
9
+ <GalleryItem :slide="elements[0]" :contentId="`${carouselContentId}-0`" />
10
+ </div>
11
+
12
+ <!-- Multiple cards (carousel with Swiper) -->
13
+ <Swiper
14
+ v-else
15
+ :modules="modules"
16
+ :space-between="8"
17
+ slides-per-view="auto"
18
+ :navigation="{
19
+ prevEl: '.gallery-button-prev',
20
+ nextEl: '.gallery-button-next',
21
+ }"
22
+ :pagination="{ clickable: true }"
23
+ :a11y="{ slideLabelMessage }"
24
+ :class="['webchat-carousel-template-root', $style.wrapper]"
25
+ data-testid="gallery-message"
26
+ >
27
+ <SwiperSlide
28
+ v-for="(element, index) in elements"
29
+ :key="index"
30
+ style="width: 206px"
31
+ >
32
+ <GalleryItem :slide="element" :contentId="`${carouselContentId}-${index}`" />
33
+ </SwiperSlide>
34
+
35
+ <!-- Navigation buttons -->
36
+ <button class="gallery-button-prev">
37
+ <ArrowBackIcon />
38
+ </button>
39
+ <button class="gallery-button-next">
40
+ <ArrowBackIcon />
41
+ </button>
42
+ </Swiper>
43
+ </div>
44
+ </template>
45
+
46
+ <script setup lang="ts">
47
+ import { computed, onMounted, useCssModule } from 'vue'
48
+ import { Swiper, SwiperSlide } from 'swiper/vue'
49
+ import { Navigation, Pagination, A11y } from 'swiper/modules'
50
+ import GalleryItem from './GalleryItem.vue'
51
+ import { ArrowBackIcon } from '../../assets/svg'
52
+ import { useMessageContext } from '../../composables/useMessageContext'
53
+ import { getChannelPayload } from '../../utils/matcher'
54
+ import { getRandomId } from '../../utils/helpers'
55
+ import type { IWebchatTemplateAttachment } from '../../types'
56
+
57
+ // Import Swiper styles
58
+ import 'swiper/css'
59
+ import 'swiper/css/navigation'
60
+ import 'swiper/css/pagination'
61
+ import 'swiper/css/a11y'
62
+
63
+ const $style = useCssModule()
64
+
65
+ // Swiper modules
66
+ const modules = [Navigation, Pagination, A11y]
67
+
68
+ // Message context
69
+ const { message, config } = useMessageContext()
70
+
71
+ // Get gallery elements from message payload
72
+ const payload = computed(() => getChannelPayload(message, config))
73
+ const elements = computed(() => {
74
+ const attachment = payload.value?.message?.attachment as IWebchatTemplateAttachment | undefined
75
+ return attachment?.payload?.elements || []
76
+ })
77
+
78
+ // Generate unique ID for content
79
+ const carouselContentId = getRandomId('webchatCarouselContentButton')
80
+
81
+ // Slide label for accessibility
82
+ const slideLabelMessage = computed(() => {
83
+ const slide = config?.settings?.customTranslations?.ariaLabels?.slide
84
+ const actionButtonPositionText = config?.settings?.customTranslations?.ariaLabels?.actionButtonPositionText
85
+
86
+ if (!slide || !actionButtonPositionText) {
87
+ return 'Slide {{index}} of {{slidesLength}}'
88
+ }
89
+
90
+ // Replace {position} and {total} with {{index}} and {{slidesLength}} for Swiper
91
+ const customSlidePosition = actionButtonPositionText
92
+ .replace('{position}', '{{index}}')
93
+ .replace('{total}', '{{slidesLength}}')
94
+
95
+ return `${slide}: ${customSlidePosition}`
96
+ })
97
+
98
+ // Auto-focus first button/card on mount
99
+ onMounted(() => {
100
+ if (!config?.settings?.widgetSettings?.enableAutoFocus) return
101
+
102
+ const chatHistory = document.getElementById('webchatChatHistoryWrapperLiveLogPanel')
103
+ if (!chatHistory?.contains(document.activeElement)) return
104
+
105
+ setTimeout(() => {
106
+ const firstCardContent = document.getElementById(`${carouselContentId}-0`)
107
+ const firstButton = firstCardContent?.getElementsByTagName('button')?.[0]
108
+
109
+ if (firstCardContent?.getAttribute('role') === 'link') {
110
+ firstCardContent?.focus()
111
+ } else if (firstButton) {
112
+ firstButton?.focus()
113
+ }
114
+ }, 200)
115
+ })
116
+ </script>
117
+
118
+ <style module>
119
+ .slideItem {
120
+ position: relative;
121
+ width: 206px;
122
+ overflow: hidden;
123
+ }
124
+
125
+ /*
126
+ ** SWIPER MAIN
127
+ ** The following styles are a porting from the original swiper/css and related modules.
128
+ ** The idea is to integrate with modules CSS in order to increase the specificity
129
+ ** and avoid conflicts on consumer apps
130
+ */
131
+ :global(article) :global(.swiper).wrapper {
132
+ margin-left: -20px;
133
+ margin-right: -20px;
134
+ padding-left: 20px;
135
+ padding-right: 20px;
136
+ padding-bottom: 22px;
137
+ padding-top: 0px;
138
+ position: relative;
139
+ overflow: hidden;
140
+ list-style: none;
141
+ /* Fix of Webkit flickering */
142
+ z-index: 1;
143
+ display: block;
144
+ }
145
+
146
+ :global(article) :global(.swiper).wrapper :global(.swiper-wrapper) {
147
+ position: relative;
148
+ width: 100%;
149
+ z-index: 1;
150
+ display: flex;
151
+ transition-property: transform;
152
+ transition-timing-function: initial;
153
+ box-sizing: content-box;
154
+ }
155
+
156
+ :global(article) :global(.swiper).wrapper :global(.swiper-android .swiper-slide),
157
+ :global(article) :global(.swiper).wrapper :global(.swiper-ios .swiper-slide),
158
+ :global(article) :global(.swiper).wrapper :global(.swiper-wrapper) {
159
+ transform: translate3d(0px, 0, 0);
160
+ }
161
+
162
+ :global(article) :global(.swiper).wrapper :global(.swiper-horizontal) {
163
+ touch-action: pan-y;
164
+ }
165
+
166
+ :global(article) :global(.swiper).wrapper :global(.swiper-slide) {
167
+ flex-shrink: 0;
168
+ width: 100%;
169
+ height: 100%;
170
+ position: relative;
171
+ transition-property: transform;
172
+ display: block;
173
+ }
174
+
175
+ :global(article) :global(.swiper).wrapper :global(.swiper-slide-invisible-blank) {
176
+ visibility: hidden;
177
+ }
178
+
179
+ /*
180
+ ** SWIPER NAVIGATION (Buttons)
181
+ */
182
+ :global(article) :global(.swiper).wrapper :global(.gallery-button-prev),
183
+ :global(article) :global(.swiper).wrapper :global(.gallery-button-next) {
184
+ position: absolute;
185
+ top: calc(150px / 2 - 8px);
186
+ z-index: 10;
187
+ cursor: pointer;
188
+ width: 30px;
189
+ height: 30px;
190
+ background-color: rgba(0, 0, 0, 0.5);
191
+ border: none;
192
+ border-radius: 50%;
193
+ display: flex;
194
+ align-items: center;
195
+ justify-content: center;
196
+ }
197
+
198
+ :global(article) :global(.swiper).wrapper :global(.gallery-button-prev) {
199
+ left: 20px;
200
+ }
201
+
202
+ :global(article) :global(.swiper).wrapper :global(.gallery-button-next) {
203
+ right: 20px;
204
+ transform: rotate(180deg);
205
+ }
206
+
207
+ :global(article) :global(.swiper).wrapper :global(.gallery-button-prev:dir(rtl)) {
208
+ left: unset;
209
+ right: 20px;
210
+ transform: rotate(180deg);
211
+ }
212
+
213
+ :global(article) :global(.swiper).wrapper :global(.gallery-button-next:dir(rtl)) {
214
+ right: unset;
215
+ left: 20px;
216
+ transform: rotate(0deg);
217
+ }
218
+
219
+ :global(article) :global(.swiper).wrapper :global(.swiper-button-disabled) {
220
+ opacity: 0;
221
+ }
222
+
223
+ /*
224
+ ** SWIPER PAGINATION (Dots)
225
+ */
226
+ :global(article) :global(.swiper).wrapper :global(.swiper-pagination) {
227
+ position: absolute;
228
+ text-align: center;
229
+ transition: 300ms opacity;
230
+ transform: translate3d(0, 0, 0);
231
+ z-index: 10;
232
+ }
233
+
234
+ :global(article) :global(.swiper).wrapper :global(.swiper-pagination.swiper-pagination-hidden) {
235
+ opacity: 0;
236
+ }
237
+
238
+ :global(article) :global(.swiper).wrapper :global(.swiper-pagination-disabled > .swiper-pagination),
239
+ :global(article) :global(.swiper).wrapper :global(.swiper-pagination.swiper-pagination-disabled) {
240
+ display: none !important;
241
+ }
242
+
243
+ :global(article) :global(.swiper).wrapper :global(.swiper-horizontal > .swiper-pagination-bullets),
244
+ :global(article) :global(.swiper).wrapper :global(.swiper-pagination-bullets.swiper-pagination-horizontal) {
245
+ bottom: 4px;
246
+ left: 0;
247
+ width: 100%;
248
+ }
249
+
250
+ :global(article) :global(.swiper).wrapper :global(.swiper-pagination-bullet) {
251
+ width: 6px;
252
+ height: 6px;
253
+ display: inline-block;
254
+ border-radius: 50%;
255
+ background: var(--cc-black-50, rgba(0, 0, 0, 0.5));
256
+ opacity: 1;
257
+ }
258
+
259
+ :global(article) :global(.swiper).wrapper :global(.swiper-pagination-bullet):focus {
260
+ background-color: var(--cc-primary-color-focus, #1976d2);
261
+ box-shadow: 0 0 0 4px var(--cc-primary-color-opacity-10, rgba(25, 118, 210, 0.1));
262
+ outline: none;
263
+ }
264
+
265
+ :global(article) :global(.swiper).wrapper :global(.swiper-pagination-bullet):focus-visible {
266
+ outline: 2px solid var(--cc-primary-color-focus, #1976d2);
267
+ outline-offset: 2px;
268
+ }
269
+
270
+ :global(article) :global(.swiper).wrapper :global(button.swiper-pagination-bullet) {
271
+ border: none;
272
+ margin: 0;
273
+ padding: 0;
274
+ box-shadow: none;
275
+ -webkit-appearance: none;
276
+ appearance: none;
277
+ }
278
+
279
+ :global(article) :global(.swiper).wrapper :global(.swiper-pagination-clickable .swiper-pagination-bullet) {
280
+ cursor: pointer;
281
+ }
282
+
283
+ :global(article) :global(.swiper).wrapper :global(.swiper-pagination-bullet:only-child) {
284
+ display: none !important;
285
+ }
286
+
287
+ :global(article) :global(.swiper).wrapper :global(.swiper-pagination-bullet-active) {
288
+ opacity: 1;
289
+ background-color: black;
290
+ }
291
+
292
+ :global(article) :global(.swiper).wrapper :global(.swiper-horizontal > .swiper-pagination-bullets .swiper-pagination-bullet),
293
+ :global(article) :global(.swiper).wrapper :global(.swiper-pagination-horizontal.swiper-pagination-bullets .swiper-pagination-bullet) {
294
+ margin: 0 4px;
295
+ }
296
+ </style>