@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
@@ -64,23 +64,23 @@
64
64
  </video>
65
65
  </div>
66
66
 
67
- <!-- Download transcript button -->
68
- <div v-if="videoData.altText" :class="$style.downloadButtonWrapper">
69
- <button
70
- :class="[$style.downloadButton, 'webchat-buttons-template-button-video']"
71
- @click="downloadTranscript"
72
- >
73
- <DownloadIcon :class="$style.downloadIcon" />
74
- Download Transcript
75
- </button>
76
- <a
77
- ref="downloadLinkRef"
78
- :href="transcriptDataUrl"
79
- download="video-transcript.txt"
80
- style="display: none"
81
- aria-hidden="true"
82
- />
83
- </div>
67
+ <!-- Download transcript button (split-border: flush card extension below player) -->
68
+ <button
69
+ v-if="videoData.altText"
70
+ :class="[$style.downloadButton, 'webchat-buttons-template-button-video']"
71
+ @click="downloadTranscript"
72
+ >
73
+ <DownloadIcon :class="$style.downloadIcon" />
74
+ Download Transcript
75
+ </button>
76
+ <a
77
+ v-if="videoData.altText"
78
+ ref="downloadLinkRef"
79
+ :href="transcriptDataUrl"
80
+ download="video-transcript.txt"
81
+ style="display: none"
82
+ aria-hidden="true"
83
+ />
84
84
  </div>
85
85
  </template>
86
86
 
@@ -230,6 +230,10 @@ onMounted(() => {
230
230
  border-radius: var(--cc-bubble-border-radius, 15px);
231
231
  max-width: 295px;
232
232
  position: relative;
233
+ background-color: var(--cc-white, #ffffff);
234
+ box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
235
+ rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
236
+ rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
233
237
  }
234
238
 
235
239
  .player {
@@ -250,8 +254,13 @@ onMounted(() => {
250
254
  overflow: hidden;
251
255
  }
252
256
 
253
- .wrapperWithButton {
254
- border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
257
+ /* Split-border strategy: playerWrapper gets border with no bottom,
258
+ downloadButton gets border with no top seamless card appearance */
259
+ .wrapperWithButton .playerWrapper {
260
+ border: 1px solid var(--cc-border-media-card, var(--cc-black-80, rgba(0, 0, 0, 0.8)));
261
+ border-bottom: none;
262
+ border-radius: var(--cc-bubble-border-radius, 15px) var(--cc-bubble-border-radius, 15px) 0 0;
263
+ overflow: hidden;
255
264
  }
256
265
 
257
266
  .wrapperWithButton .player {
@@ -259,11 +268,9 @@ onMounted(() => {
259
268
  border-bottom-right-radius: 0px;
260
269
  }
261
270
 
262
- .downloadButtonWrapper {
263
- padding: 16px;
264
- background-color: var(--cc-white, #ffffff);
265
- border-bottom-left-radius: var(--cc-bubble-border-radius, 15px);
266
- border-bottom-right-radius: var(--cc-bubble-border-radius, 15px);
271
+ .wrapperWithButton .lightOverlay {
272
+ border-bottom-left-radius: 0px;
273
+ border-bottom-right-radius: 0px;
267
274
  }
268
275
 
269
276
  .downloadButton {
@@ -272,11 +279,13 @@ onMounted(() => {
272
279
  justify-content: center;
273
280
  gap: 10px;
274
281
  width: 100%;
282
+ box-sizing: border-box;
275
283
  padding: 12px;
276
284
  background-color: var(--cc-primary-color, #1976d2);
277
285
  color: var(--cc-primary-contrast-color, #ffffff);
278
- border: none;
279
- border-radius: 8px;
286
+ border: 1px solid var(--cc-border-media-card, var(--cc-black-80, rgba(0, 0, 0, 0.8)));
287
+ border-top: none;
288
+ border-radius: 0 0 var(--cc-bubble-border-radius, 15px) var(--cc-bubble-border-radius, 15px);
280
289
  cursor: pointer;
281
290
  font-size: 14px;
282
291
  font-weight: 600;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * useLiveRegion composable
3
+ *
4
+ * Manages an ARIA live region for screen reader announcements.
5
+ * Creates/reuses a single visually-hidden <div role="status" aria-live="polite"> element
6
+ * in the DOM, and announces content when messages arrive.
7
+ *
8
+ * This matches the React reference implementation's approach to accessibility
9
+ * announcements for message components.
10
+ */
11
+
12
+ import { onMounted, onUnmounted } from 'vue'
13
+
14
+ const LIVE_REGION_ID = 'webchat-live-region'
15
+
16
+ /**
17
+ * Get or create the shared live region element in the DOM
18
+ */
19
+ function getOrCreateLiveRegion(): HTMLDivElement {
20
+ let region = document.getElementById(LIVE_REGION_ID) as HTMLDivElement | null
21
+
22
+ if (!region) {
23
+ region = document.createElement('div')
24
+ region.id = LIVE_REGION_ID
25
+ region.setAttribute('role', 'status')
26
+ region.setAttribute('aria-live', 'polite')
27
+ region.setAttribute('aria-atomic', 'true')
28
+
29
+ // Visually hidden but accessible to screen readers
30
+ Object.assign(region.style, {
31
+ position: 'absolute',
32
+ width: '1px',
33
+ height: '1px',
34
+ padding: '0',
35
+ margin: '-1px',
36
+ overflow: 'hidden',
37
+ clip: 'rect(0, 0, 0, 0)',
38
+ whiteSpace: 'nowrap',
39
+ borderWidth: '0',
40
+ })
41
+
42
+ document.body.appendChild(region)
43
+ }
44
+
45
+ return region
46
+ }
47
+
48
+ /**
49
+ * Track reference count so we only clean up when no components are using the region
50
+ */
51
+ let refCount = 0
52
+
53
+ interface UseLiveRegionOptions {
54
+ /** Text to announce to screen readers */
55
+ announcement: string
56
+ /** Whether the announcement should be made (defaults to true) */
57
+ enabled?: boolean
58
+ }
59
+
60
+ /**
61
+ * Composable that announces content to screen readers via an ARIA live region.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * // In a message component:
66
+ * useLiveRegion({
67
+ * announcement: `New message: ${messageText}`,
68
+ * enabled: !props.ignoreLiveRegion,
69
+ * })
70
+ * ```
71
+ */
72
+ export function useLiveRegion(options: UseLiveRegionOptions): void {
73
+ onMounted(() => {
74
+ refCount++
75
+
76
+ if (options.enabled === false) return
77
+ if (!options.announcement) return
78
+
79
+ const region = getOrCreateLiveRegion()
80
+
81
+ // Clear then set to trigger screen reader re-announcement
82
+ region.textContent = ''
83
+ // Use requestAnimationFrame to ensure the empty text is processed first
84
+ requestAnimationFrame(() => {
85
+ region.textContent = options.announcement
86
+ })
87
+ })
88
+
89
+ onUnmounted(() => {
90
+ refCount--
91
+
92
+ // Clean up the live region when no components are using it
93
+ if (refCount <= 0) {
94
+ refCount = 0
95
+ const region = document.getElementById(LIVE_REGION_ID)
96
+ if (region) {
97
+ region.textContent = ''
98
+ }
99
+ }
100
+ })
101
+ }
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ export { useSanitize } from './composables/useSanitize'
35
35
  export { useImageContext, provideImageContext, ImageContextKey } from './composables/useImageContext'
36
36
  export { useChannelPayload, type UseChannelPayloadReturn } from './composables/useChannelPayload'
37
37
  export { useCollation, type UseCollationReturn, type CollatedMessage } from './composables/useCollation'
38
+ export { useLiveRegion } from './composables/useLiveRegion'
38
39
 
39
40
  // SVG Icons
40
41
  export { DownloadIcon, CloseIcon, VideoPlayIcon, AudioPlayIcon, AudioPauseIcon, ArrowBackIcon, LinkIcon } from './assets/svg'
@@ -42,7 +43,7 @@ export { DownloadIcon, CloseIcon, VideoPlayIcon, AudioPlayIcon, AudioPauseIcon,
42
43
  // Utilities
43
44
  export { match, getChannelPayload } from './utils/matcher'
44
45
  export { sanitizeHTMLWithConfig, sanitizeContent } from './utils/sanitize'
45
- export { configColorsToCssVariables } from './utils/theme'
46
+ export { configColorsToCssVariables, themeFontToCssVariable } from './utils/theme'
46
47
  export { getWebchatButtonLabel, interpolateString, getRandomId, moveFocusToMessageFocusTarget, replaceUrlsWithHTMLanchorElem, getBackgroundImage, getFileName, getFileExtension, getSizeLabel, isImageAttachment, VALID_IMAGE_MIME_TYPES } from './utils/helpers'
47
48
 
48
49
  // Types
@@ -55,7 +56,9 @@ export type {
55
56
  MessagePlugin,
56
57
  MessagePluginOptions,
57
58
  MessageContext,
59
+ MessageParams,
58
60
  IMessage,
61
+ IStreamingMessage,
59
62
  IWebchatButton,
60
63
  IWebchatQuickReply,
61
64
  IWebchatTemplateAttachment,
@@ -6,6 +6,30 @@
6
6
  import type { IMessage } from '@cognigy/socket-client'
7
7
  import type { Component } from 'vue'
8
8
 
9
+ // =============================================================================
10
+ // Message Parameter Types
11
+ // =============================================================================
12
+
13
+ /**
14
+ * Parameters about the current message state in the conversation.
15
+ * Used to control interactive element behavior (e.g., disabling quick replies after user reply).
16
+ */
17
+ export interface MessageParams {
18
+ /** Whether the user has already replied to this message (disables quick reply buttons) */
19
+ hasReply?: boolean
20
+ /** Whether the conversation has ended (disables all interactive elements) */
21
+ isConversationEnded?: boolean
22
+ }
23
+
24
+ /**
25
+ * Extended IMessage with streaming animation state.
26
+ * Used with progressive message rendering to conditionally show/hide elements
27
+ * based on whether the message is still being streamed.
28
+ */
29
+ export interface IStreamingMessage extends IMessage {
30
+ animationState?: 'start' | 'animating' | 'done'
31
+ }
32
+
9
33
  // =============================================================================
10
34
  // Extended Socket Client Types
11
35
  // =============================================================================
@@ -95,6 +119,13 @@ export interface ChatSettings {
95
119
  disableUrlButtonSanitization?: boolean
96
120
  dynamicImageAspectRatio?: boolean
97
121
  showEngagementInChat?: boolean
122
+ /**
123
+ * Gallery layout variant:
124
+ * - 'default': Standard gallery with 206px wide slides
125
+ * - 'compact': Narrower slides with smaller navigation buttons
126
+ * - 'copilot': Adaptive sizing based on card count (1-card, 2-card, 3+ layouts)
127
+ */
128
+ galleryVariant?: 'default' | 'compact' | 'copilot'
98
129
  }
99
130
  colors?: {
100
131
  // Primary action colors
@@ -118,6 +149,12 @@ export interface ChatSettings {
118
149
  borderUserMessage?: string
119
150
  borderAgentMessage?: string
120
151
 
152
+ // Media card borders (Image, Video, Audio, Gallery, List, TypingIndicator)
153
+ borderMediaCard?: string
154
+
155
+ // List divider color (separator between list items)
156
+ listDividerColor?: string
157
+
121
158
  // Link color
122
159
  textLinkColor?: string
123
160
 
@@ -126,6 +163,18 @@ export interface ChatSettings {
126
163
  adaptiveCardInputColor?: string
127
164
  adaptiveCardInputBackground?: string
128
165
  adaptiveCardInputBorder?: string
166
+
167
+ // File preview colors (non-image file attachment pills)
168
+ filePreviewBackground?: string
169
+ filePreviewTextColor?: string
170
+ filePreviewSecondaryTextColor?: string
171
+
172
+ // Chat event colors (event pill notifications)
173
+ chatEventBackground?: string
174
+ chatEventTextColor?: string
175
+
176
+ // Message shadow (applied to all message bubbles/cards)
177
+ messageShadow?: string
129
178
  }
130
179
  behavior?: {
131
180
  renderMarkdown?: boolean
@@ -142,6 +191,12 @@ export interface ChatSettings {
142
191
  * Use `false`/omit for interactive chat interfaces with smart auto-detection.
143
192
  */
144
193
  adaptiveCardsReadonly?: boolean
194
+ /**
195
+ * When true, buttons/quick replies are hidden while a message is still being streamed.
196
+ * Works with IStreamingMessage.animationState to delay showing interactive elements
197
+ * until the text animation is complete.
198
+ */
199
+ progressiveMessageRendering?: boolean
145
200
  }
146
201
  widgetSettings?: {
147
202
  enableDefaultPreview?: boolean
@@ -247,6 +302,14 @@ export interface MessageProps {
247
302
  disableHeader?: boolean
248
303
  plugins?: MessagePlugin[]
249
304
  onEmitAnalytics?: (event: string, payload?: unknown) => void
305
+ /** Conversation state parameters (hasReply, isConversationEnded) */
306
+ messageParams?: MessageParams
307
+ /** Callback to open an xApp overlay (for openXApp button type) */
308
+ openXAppOverlay?: (url: string | undefined) => void
309
+ /** Callback when gallery slide changes — receives (activeIndex, totalSlides) */
310
+ onSlideChange?: (index: number, total: number) => void
311
+ /** Callback when an image is clicked — receives the image URL */
312
+ onImageClick?: (url: string) => void
250
313
  }
251
314
 
252
315
  /**
@@ -318,6 +381,14 @@ export interface MessageContext {
318
381
  theme?: ChatTheme
319
382
  action?: MessageSender
320
383
  onEmitAnalytics?: (event: string, payload?: unknown) => void
384
+ /** Conversation state parameters (hasReply, isConversationEnded) */
385
+ messageParams?: MessageParams
386
+ /** Callback to open an xApp overlay (for openXApp button type) */
387
+ openXAppOverlay?: (url: string | undefined) => void
388
+ /** Callback when gallery slide changes — receives (activeIndex, totalSlides) */
389
+ onSlideChange?: (index: number, total: number) => void
390
+ /** Callback when an image is clicked — receives the image URL */
391
+ onImageClick?: (url: string) => void
321
392
  }
322
393
 
323
394
  /**
@@ -2,7 +2,7 @@
2
2
  * Theme utilities for config-driven CSS variable injection
3
3
  */
4
4
 
5
- import type { ChatSettings } from '../types'
5
+ import type { ChatSettings, ChatTheme } from '../types'
6
6
 
7
7
  /**
8
8
  * Maps config.settings.colors to CSS custom properties.
@@ -47,6 +47,12 @@ export function configColorsToCssVariables(
47
47
  '--cc-border-user-message': colors.borderUserMessage,
48
48
  '--cc-border-agent-message': colors.borderAgentMessage,
49
49
 
50
+ // Media card borders
51
+ '--cc-border-media-card': colors.borderMediaCard,
52
+
53
+ // List divider color
54
+ '--cc-list-divider-color': colors.listDividerColor,
55
+
50
56
  // Link color
51
57
  '--cc-text-link-color': colors.textLinkColor,
52
58
 
@@ -55,6 +61,18 @@ export function configColorsToCssVariables(
55
61
  '--cc-adaptive-card-input-color': colors.adaptiveCardInputColor,
56
62
  '--cc-adaptive-card-input-background': colors.adaptiveCardInputBackground,
57
63
  '--cc-adaptive-card-input-border': colors.adaptiveCardInputBorder,
64
+
65
+ // File preview colors
66
+ '--cc-file-preview-background': colors.filePreviewBackground,
67
+ '--cc-file-preview-text-color': colors.filePreviewTextColor,
68
+ '--cc-file-preview-secondary-text-color': colors.filePreviewSecondaryTextColor,
69
+
70
+ // Chat event colors
71
+ '--cc-chat-event-background': colors.chatEventBackground,
72
+ '--cc-chat-event-text-color': colors.chatEventTextColor,
73
+
74
+ // Message shadow
75
+ '--cc-message-shadow': colors.messageShadow,
58
76
  }
59
77
 
60
78
  // Filter out undefined values and return only defined CSS variables
@@ -62,3 +80,20 @@ export function configColorsToCssVariables(
62
80
  Object.entries(mapping).filter(([, value]) => value !== undefined)
63
81
  ) as Record<string, string>
64
82
  }
83
+
84
+ /**
85
+ * Maps a ChatTheme's fontFamily to the --cc-font-family CSS variable.
86
+ * Consumers can set this to override the default Figtree font.
87
+ *
88
+ * Components should reference font-family as:
89
+ * font-family: var(--cc-font-family, 'Figtree', sans-serif);
90
+ *
91
+ * @param theme - The ChatTheme object
92
+ * @returns A record with the --cc-font-family CSS variable, or empty object
93
+ */
94
+ export function themeFontToCssVariable(
95
+ theme?: ChatTheme
96
+ ): Record<string, string> {
97
+ if (!theme?.fontFamily) return {}
98
+ return { '--cc-font-family': theme.fontFamily }
99
+ }