@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,214 @@
1
+ <template>
2
+ <div :class="['webchat-carousel-template-frame', $style.slideItem]">
3
+ <!-- Top section with image and title overlay -->
4
+ <div :class="[$style.top, hasExtraInfo && $style.hasExtraInfo]">
5
+ <Typography
6
+ variant="body-semibold"
7
+ component="h4"
8
+ :class="'webchat-carousel-template-title'"
9
+ :id="titleId"
10
+ v-html="titleHtml"
11
+ />
12
+
13
+ <!-- Image or broken image placeholder -->
14
+ <span v-if="isImageBroken" :class="$style.brokenImage" />
15
+ <img
16
+ v-else
17
+ :src="slide.image_url"
18
+ :alt="slide.image_alt_text || ''"
19
+ :class="$style.slideImage"
20
+ @error="handleImageError"
21
+ />
22
+ </div>
23
+
24
+ <!-- Bottom section with subtitle and buttons -->
25
+ <div
26
+ v-if="hasExtraInfo"
27
+ :class="['webchat-carousel-template-content', $style.bottom]"
28
+ :role="defaultActionUrl ? 'link' : undefined"
29
+ :id="contentId"
30
+ :aria-describedby="defaultActionUrl && slide.subtitle ? subtitleId : undefined"
31
+ :aria-labelledby="defaultActionUrl && slide.title ? titleId : undefined"
32
+ :aria-label="defaultActionUrl ? `${titleHtml}. ${opensInNewTabLabel}` : undefined"
33
+ :tabindex="defaultActionUrl ? 0 : undefined"
34
+ @click="handleClick"
35
+ @keydown="handleKeyDown"
36
+ >
37
+ <!-- Subtitle -->
38
+ <Typography
39
+ v-if="slide.subtitle"
40
+ variant="body-regular"
41
+ :class="'webchat-carousel-template-subtitle'"
42
+ :id="subtitleId"
43
+ v-html="subtitleHtml"
44
+ />
45
+
46
+ <!-- Action buttons -->
47
+ <ActionButtons
48
+ v-if="slide.buttons && slide.buttons.length > 0"
49
+ :payload="slide.buttons"
50
+ :action="shouldBeDisabled ? undefined : action"
51
+ :buttonClassName="buttonClassName"
52
+ :buttonListItemClassName="$style.buttonListItem"
53
+ :config="config"
54
+ :dataMessageId="dataMessageId"
55
+ :onEmitAnalytics="onEmitAnalytics"
56
+ :templateTextId="slide.title ? titleId : undefined"
57
+ />
58
+ </div>
59
+ </div>
60
+ </template>
61
+
62
+ <script setup lang="ts">
63
+ import { ref, computed, useCssModule } from 'vue'
64
+ import Typography from '../common/Typography.vue'
65
+ import ActionButtons from '../common/ActionButtons.vue'
66
+ import { useMessageContext } from '../../composables/useMessageContext'
67
+ import { useSanitize } from '../../composables/useSanitize'
68
+ import { getRandomId } from '../../utils/helpers'
69
+ import { sanitizeUrl } from '@braintree/sanitize-url'
70
+ import type { IWebchatAttachmentElement } from '../../types'
71
+
72
+ interface Props {
73
+ slide: IWebchatAttachmentElement
74
+ contentId: string
75
+ }
76
+
77
+ const props = defineProps<Props>()
78
+
79
+ const $style = useCssModule()
80
+
81
+ // Context and config
82
+ const { action, config, onEmitAnalytics } = useMessageContext()
83
+ const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
84
+
85
+ // Sanitize HTML
86
+ const { processHTML } = useSanitize()
87
+ const titleHtml = computed(() => processHTML(props.slide.title || ''))
88
+ const subtitleHtml = computed(() => processHTML(props.slide.subtitle || ''))
89
+
90
+ // IDs for accessibility
91
+ const titleId = getRandomId('webchatCarouselTemplateTitle')
92
+ const subtitleId = getRandomId('webchatCarouselTemplateSubtitle')
93
+
94
+ // Image state
95
+ const isImageBroken = ref(false)
96
+ const handleImageError = () => {
97
+ isImageBroken.value = true
98
+ }
99
+
100
+ // Check if card has extra info (subtitle or buttons)
101
+ const hasExtraInfo = computed(() => {
102
+ return !!(props.slide.subtitle || (props.slide.buttons && props.slide.buttons.length > 0))
103
+ })
104
+
105
+ // Default action URL (clickable card)
106
+ const defaultActionUrl = computed(() => {
107
+ return props.slide.default_action?.url
108
+ })
109
+
110
+ // Should buttons be disabled
111
+ const shouldBeDisabled = computed(() => {
112
+ // TODO: Add conversation ended check when messageParams available
113
+ return false
114
+ })
115
+
116
+ // Translations
117
+ const opensInNewTabLabel = computed(() => {
118
+ return config?.settings?.customTranslations?.ariaLabels?.opensInNewTab || 'Opens in new tab'
119
+ })
120
+
121
+ // Button class name
122
+ const buttonClassName = computed(() => {
123
+ return 'webchat-carousel-template-button'
124
+ })
125
+
126
+ // Handle card click (default action)
127
+ const handleClick = () => {
128
+ if (!defaultActionUrl.value) return
129
+
130
+ const url = config?.settings?.layout?.disableUrlButtonSanitization
131
+ ? defaultActionUrl.value
132
+ : sanitizeUrl(defaultActionUrl.value)
133
+
134
+ // Prevent no-ops from sending you to a blank page
135
+ if (url === 'about:blank') return
136
+
137
+ window.open(url)
138
+ }
139
+
140
+ // Handle keyboard navigation
141
+ const handleKeyDown = (event: KeyboardEvent) => {
142
+ if (defaultActionUrl.value && event.key === 'Enter') {
143
+ handleClick()
144
+ }
145
+ }
146
+ </script>
147
+
148
+ <style module>
149
+ .slideItem {
150
+ position: relative;
151
+ width: 206px;
152
+ overflow: hidden;
153
+ }
154
+
155
+ .slideItem .top {
156
+ position: relative;
157
+ border-radius: var(--cc-bubble-border-radius, 15px);
158
+ }
159
+
160
+ .slideItem .bottom {
161
+ border-bottom-left-radius: var(--cc-bubble-border-radius, 15px);
162
+ border-bottom-right-radius: var(--cc-bubble-border-radius, 15px);
163
+ border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
164
+ background-color: var(--cc-white, #ffffff);
165
+ display: flex;
166
+ flex-direction: column;
167
+ gap: 8px;
168
+ padding: 8px;
169
+ cursor: pointer;
170
+ }
171
+
172
+ .slideItem .top img {
173
+ aspect-ratio: 206/150;
174
+ object-fit: cover;
175
+ object-position: left;
176
+ border-radius: var(--cc-bubble-border-radius, 15px);
177
+ width: 100%;
178
+ display: block;
179
+ outline: none;
180
+ }
181
+
182
+ .slideItem .brokenImage {
183
+ aspect-ratio: 206/150;
184
+ width: 100%;
185
+ display: block;
186
+ outline: none;
187
+ border-radius: var(--cc-bubble-border-radius, 15px);
188
+ background-color: var(--cc-black-80, rgba(0, 0, 0, 0.8));
189
+ }
190
+
191
+ .slideItem .hasExtraInfo,
192
+ .slideItem .hasExtraInfo img,
193
+ .slideItem .brokenImage {
194
+ border-bottom-left-radius: 0px;
195
+ border-bottom-right-radius: 0px;
196
+ }
197
+
198
+ .slideItem .top h4 {
199
+ position: absolute;
200
+ margin: 0px;
201
+ margin-inline-start: 8px;
202
+ bottom: 10px;
203
+ color: var(--cc-white, #ffffff);
204
+ }
205
+
206
+ .slideItem .bottom p {
207
+ padding: 0px;
208
+ margin: 0px;
209
+ }
210
+
211
+ .slideItem .bottom .buttonListItem {
212
+ width: 100%;
213
+ }
214
+ </style>
@@ -0,0 +1,368 @@
1
+ <template>
2
+ <div v-if="imageData.url" :class="$style.wrapper">
3
+ <!-- Thumbnail -->
4
+ <div
5
+ ref="buttonRef"
6
+ :class="imageClasses"
7
+ :tabindex="isDownloadable ? 0 : -1"
8
+ :role="isDownloadable ? 'button' : undefined"
9
+ :aria-label="isDownloadable ? viewImageLabel : undefined"
10
+ @click="handleExpand"
11
+ @keydown="handleKeyDown"
12
+ >
13
+ <span v-if="isImageBroken" :class="$style.brokenImage" />
14
+ <img
15
+ v-else
16
+ :src="imageData.url"
17
+ :alt="imageData.altText || ''"
18
+ @error="handleImageError"
19
+ />
20
+ </div>
21
+
22
+ <!-- Download button -->
23
+ <ActionButton
24
+ v-if="imageData.button"
25
+ :button="imageData.button"
26
+ :action="action"
27
+ :config="config"
28
+ :on-emit-analytics="onEmitAnalytics"
29
+ :custom-icon="DownloadIcon"
30
+ :position="1"
31
+ :total="1"
32
+ :class-name="$style.downloadButtonWrapper"
33
+ class="webchat-buttons-template-button"
34
+ />
35
+
36
+ <!-- Lightbox modal -->
37
+ <Teleport to="body">
38
+ <div
39
+ v-if="showLightbox"
40
+ role="dialog"
41
+ :aria-label="lightboxLabel"
42
+ :class="$style.lightboxWrapper"
43
+ >
44
+ <div :class="$style.lightboxContent" @click="handleClose">
45
+ <img
46
+ :class="$style.fullImage"
47
+ :alt="imageData.altText"
48
+ :src="imageData.url"
49
+ data-testid="image-lightbox"
50
+ @click.stop
51
+ @touchmove.prevent="handleClose"
52
+ />
53
+ </div>
54
+
55
+ <!-- Lightbox Header -->
56
+ <div :class="$style.lightboxHeader">
57
+ <div :class="$style.caption">{{ imageData.altText }}</div>
58
+ <div :class="$style.iconsGroup">
59
+ <button
60
+ ref="downloadButtonRef"
61
+ :class="$style.icon"
62
+ :aria-label="downloadLabel"
63
+ @click="handleDownload"
64
+ @keydown="handleKeyDownload"
65
+ >
66
+ <DownloadIcon />
67
+ </button>
68
+ <button
69
+ :class="$style.icon"
70
+ :aria-label="closeLabel"
71
+ @click="handleClose"
72
+ @keydown="handleKeyClose"
73
+ >
74
+ <CloseIcon />
75
+ </button>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </Teleport>
80
+ </div>
81
+ </template>
82
+
83
+ <script setup lang="ts">
84
+ import { ref, computed, onMounted, onUnmounted, nextTick, useCssModule } from 'vue'
85
+ import { useMessageContext } from '../../composables/useMessageContext'
86
+ import { getChannelPayload } from '../../utils/matcher'
87
+ import ActionButton from '../common/ActionButton.vue'
88
+ import { DownloadIcon, CloseIcon } from '../../assets/svg'
89
+ import type { IWebchatImageAttachment, IWebchatButton } from '../../types'
90
+
91
+ const { message, config, action, onEmitAnalytics } = useMessageContext()
92
+
93
+ const $style = useCssModule()
94
+
95
+ // State
96
+ const showLightbox = ref(false)
97
+ const isImageBroken = ref(false)
98
+ const buttonRef = ref<HTMLDivElement>()
99
+ const downloadButtonRef = ref<HTMLButtonElement>()
100
+
101
+ // Get image data from message payload
102
+ const payload = computed(() => getChannelPayload(message, config))
103
+ const imageData = computed(() => {
104
+ const attachment = payload.value?.message?.attachment as IWebchatImageAttachment
105
+ return {
106
+ url: attachment?.payload?.url || '',
107
+ altText: attachment?.payload?.altText,
108
+ buttons: attachment?.payload?.buttons,
109
+ button: attachment?.payload?.buttons?.[0],
110
+ }
111
+ })
112
+
113
+ // Check if image is downloadable (has a web_url button)
114
+ const isDownloadable = computed(() => {
115
+ const buttons = imageData.value.buttons as IWebchatButton[] | undefined
116
+ return buttons?.some(button => 'type' in button && button.type === 'web_url') ?? false
117
+ })
118
+
119
+ // Dynamic aspect ratio setting
120
+ const isDynamicRatio = computed(() => !!config?.settings?.layout?.dynamicImageAspectRatio)
121
+
122
+ const imageClasses = computed(() => {
123
+ return {
124
+ [$style.fixedImage]: isDynamicRatio.value,
125
+ [$style.flexImage]: !isDynamicRatio.value,
126
+ [$style.webchatMediaTemplateImage]: true,
127
+ [$style.downloadable]: isDownloadable.value,
128
+ }
129
+ })
130
+
131
+ // Translations
132
+ const viewImageLabel = computed(() =>
133
+ config?.settings?.customTranslations?.ariaLabels?.viewImageInFullsize || 'View full-size image'
134
+ )
135
+ const lightboxLabel = computed(() =>
136
+ config?.settings?.customTranslations?.ariaLabels?.fullSizeImageViewerTitle || 'Full-size image viewer'
137
+ )
138
+ const downloadLabel = computed(() =>
139
+ config?.settings?.customTranslations?.ariaLabels?.downloadFullsizeImage || 'Download full-size image'
140
+ )
141
+ const closeLabel = computed(() =>
142
+ config?.settings?.customTranslations?.ariaLabels?.closeFullsizeImageModal || 'Close full-size image viewer'
143
+ )
144
+
145
+ // Handlers
146
+ const handleExpand = () => {
147
+ if (isDownloadable.value) {
148
+ showLightbox.value = true
149
+ // Focus the download button after lightbox opens
150
+ nextTick(() => {
151
+ downloadButtonRef.value?.focus()
152
+ })
153
+ }
154
+ }
155
+
156
+ const handleClose = () => {
157
+ showLightbox.value = false
158
+ // Restore focus to thumbnail button
159
+ nextTick(() => {
160
+ buttonRef.value?.focus()
161
+ })
162
+ }
163
+
164
+ const handleDownload = () => {
165
+ window.open(imageData.value.url, '_blank')
166
+ }
167
+
168
+ const handleKeyDown = (event: KeyboardEvent) => {
169
+ if (event.key === 'Enter' || event.key === ' ') {
170
+ event.preventDefault()
171
+ handleExpand()
172
+ }
173
+ }
174
+
175
+ const handleKeyDownload = (event: KeyboardEvent) => {
176
+ if (event.key === 'Enter') {
177
+ handleDownload()
178
+ }
179
+ }
180
+
181
+ const handleKeyClose = (event: KeyboardEvent) => {
182
+ if (event.key === 'Tab' || event.shiftKey) {
183
+ downloadButtonRef.value?.focus()
184
+ event.preventDefault()
185
+ }
186
+ if (event.key === 'Enter') {
187
+ handleClose()
188
+ }
189
+ }
190
+
191
+ const handleImageError = () => {
192
+ isImageBroken.value = true
193
+ }
194
+
195
+ // Escape key listener for lightbox
196
+ const handleEscape = (event: KeyboardEvent) => {
197
+ if (event.code === 'Escape' && showLightbox.value) {
198
+ handleClose()
199
+ }
200
+ }
201
+
202
+ onMounted(() => {
203
+ window.addEventListener('keydown', handleEscape)
204
+ })
205
+
206
+ onUnmounted(() => {
207
+ window.removeEventListener('keydown', handleEscape)
208
+ })
209
+ </script>
210
+
211
+ <style module>
212
+ .wrapper {
213
+ position: relative;
214
+ border-radius: var(--cc-bubble-border-radius, 15px);
215
+ max-width: 295px;
216
+ width: 100%;
217
+ outline: none;
218
+ }
219
+
220
+ .wrapper .fixedImage,
221
+ .wrapper .flexImage {
222
+ border-top-left-radius: var(--cc-bubble-border-radius, 15px);
223
+ border-top-right-radius: var(--cc-bubble-border-radius, 15px);
224
+ }
225
+
226
+ .wrapper .fixedImage:focus-visible,
227
+ .wrapper .flexImage:focus-visible {
228
+ outline: 2px solid var(--cc-primary-color-focus, #1976d2);
229
+ }
230
+
231
+ .wrapper img {
232
+ border-radius: var(--cc-bubble-border-radius, 15px);
233
+ width: 100%;
234
+ display: block;
235
+ outline: none;
236
+ }
237
+
238
+ .downloadable {
239
+ background-color: var(--cc-white, #ffffff);
240
+ cursor: pointer;
241
+ border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
242
+ }
243
+
244
+ .downloadable img {
245
+ border-bottom-left-radius: 0px;
246
+ border-bottom-right-radius: 0px;
247
+ }
248
+
249
+ .downloadable img:hover,
250
+ .downloadable img:focus {
251
+ opacity: 0.8;
252
+ }
253
+
254
+ .flexImage img {
255
+ aspect-ratio: 16/9;
256
+ object-fit: cover;
257
+ object-position: left;
258
+ }
259
+
260
+ .fixedImage img {
261
+ height: auto;
262
+ }
263
+
264
+ .webchatMediaTemplateImage {
265
+ /* Base class for image containers */
266
+ }
267
+
268
+ .downloadButtonWrapper {
269
+ padding: 16px;
270
+ }
271
+
272
+ .brokenImage {
273
+ aspect-ratio: 16/9;
274
+ width: 100%;
275
+ display: block;
276
+ outline: none;
277
+ border-radius: var(--cc-bubble-border-radius, 15px);
278
+ background-color: var(--cc-black-80, rgba(0, 0, 0, 0.8));
279
+ }
280
+
281
+ /* Lightbox styles */
282
+ .lightboxWrapper {
283
+ position: fixed;
284
+ z-index: 5000;
285
+ left: 0;
286
+ top: 0;
287
+ width: 100%;
288
+ height: 100%;
289
+ background-color: rgba(0, 0, 0, 0.8);
290
+ touch-action: none;
291
+ overflow: hidden;
292
+ }
293
+
294
+ .lightboxContent {
295
+ position: relative;
296
+ height: 100%;
297
+ width: 100%;
298
+ }
299
+
300
+ .fullImage {
301
+ position: absolute;
302
+ top: 50%;
303
+ left: 50%;
304
+ max-width: 100%;
305
+ max-height: 100%;
306
+ height: auto;
307
+ transform: translate3d(-50%, -50%, 0);
308
+ overflow: hidden;
309
+ }
310
+
311
+ /* Lightbox Header */
312
+ .lightboxHeader {
313
+ position: absolute;
314
+ top: 0;
315
+ height: 56px;
316
+ width: 100%;
317
+ background-color: var(--cc-white, #ffffff);
318
+ overflow: hidden;
319
+ display: flex;
320
+ justify-content: space-between;
321
+ }
322
+
323
+ .caption {
324
+ display: flex;
325
+ align-items: center;
326
+ margin-left: 15px;
327
+ color: var(--cc-black-10, #1a1a1a);
328
+ font-weight: 700;
329
+ font-size: 16px;
330
+ }
331
+
332
+ .iconsGroup {
333
+ display: flex;
334
+ align-items: center;
335
+ margin-right: 10px;
336
+ }
337
+
338
+ .icon {
339
+ display: flex;
340
+ align-items: center;
341
+ justify-content: center;
342
+ box-sizing: border-box;
343
+ background-color: transparent;
344
+ border: none;
345
+ outline: none;
346
+ margin: 0;
347
+ transition: background-color 0.1s ease-out, color 0.1s ease-out, fill 0.1s ease-out;
348
+ color: var(--cc-black-10, #1a1a1a);
349
+ border-radius: 50%;
350
+ cursor: pointer;
351
+ width: 40px;
352
+ height: 40px;
353
+ }
354
+
355
+ .icon:hover,
356
+ .icon:focus {
357
+ background-color: var(--cc-black-95, #f5f5f5);
358
+ opacity: 0.85;
359
+ }
360
+
361
+ .icon:focus-visible {
362
+ border: 2px solid var(--cc-primary-color-focus, #1976d2);
363
+ }
364
+
365
+ .icon svg {
366
+ fill: var(--cc-black-10, #1a1a1a);
367
+ }
368
+ </style>