@cognigy/chat-components-vue 0.1.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.
Files changed (40) hide show
  1. package/dist/chat-components-vue.css +1 -1
  2. package/dist/chat-components-vue.js +12386 -5741
  3. package/dist/components/Message.vue.d.ts +4 -0
  4. package/dist/components/common/Typography.vue.d.ts +1 -1
  5. package/dist/components/messages/AdaptiveCardRenderer.vue.d.ts +35 -0
  6. package/dist/components/messages/ListItem.vue.d.ts +1 -1
  7. package/dist/composables/useLiveRegion.d.ts +30 -0
  8. package/dist/index.d.ts +3 -2
  9. package/dist/types/index.d.ts +105 -1
  10. package/dist/utils/helpers.d.ts +3 -2
  11. package/dist/utils/matcher.d.ts +3 -3
  12. package/dist/utils/theme.d.ts +12 -1
  13. package/package.json +8 -3
  14. package/src/components/Message.vue +98 -55
  15. package/src/components/common/ActionButton.vue +16 -7
  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/AdaptiveCard.vue +322 -225
  21. package/src/components/messages/AdaptiveCardRenderer.vue +260 -0
  22. package/src/components/messages/AudioMessage.vue +4 -1
  23. package/src/components/messages/DatePicker.vue +5 -27
  24. package/src/components/messages/FileMessage.vue +12 -3
  25. package/src/components/messages/Gallery.vue +96 -10
  26. package/src/components/messages/GalleryItem.vue +17 -5
  27. package/src/components/messages/ImageMessage.vue +20 -5
  28. package/src/components/messages/List.vue +56 -42
  29. package/src/components/messages/ListItem.vue +105 -68
  30. package/src/components/messages/TextMessage.vue +1 -1
  31. package/src/components/messages/TextWithButtons.vue +35 -11
  32. package/src/components/messages/VideoMessage.vue +35 -26
  33. package/src/composables/useCollation.ts +28 -45
  34. package/src/composables/useLiveRegion.ts +101 -0
  35. package/src/index.ts +4 -1
  36. package/src/types/index.ts +127 -2
  37. package/src/utils/helpers.ts +46 -24
  38. package/src/utils/matcher.ts +20 -6
  39. package/src/utils/sanitize.ts +1 -2
  40. package/src/utils/theme.ts +42 -1
@@ -3,6 +3,10 @@ declare const _default: import('vue').DefineComponent<MessageProps, {}, {}, {},
3
3
  action: import('..').MessageSender;
4
4
  config: import('..').ChatConfig;
5
5
  onEmitAnalytics: (event: string, payload?: unknown) => void;
6
+ openXAppOverlay: (url: string | undefined) => void;
7
+ onImageClick: (url: string) => void;
8
+ messageParams: import('..').MessageParams;
9
+ onSlideChange: (index: number, total: number) => void;
6
10
  theme: import('..').ChatTheme;
7
11
  prevMessage: import('@cognigy/socket-client').IMessage;
8
12
  disableHeader: boolean;
@@ -1,5 +1,5 @@
1
1
  import { CSSProperties } from 'vue';
2
- export type TagVariant = 'h1-semibold' | 'h2-regular' | 'h2-semibold' | 'title1-semibold' | 'title1-regular' | 'title2-semibold' | 'title2-regular' | 'body-regular' | 'body-semibold' | 'copy-medium' | 'cta-semibold';
2
+ export type TagVariant = "h1-semibold" | "h2-regular" | "h2-semibold" | "title1-semibold" | "title1-regular" | "title2-semibold" | "title2-regular" | "body-regular" | "body-semibold" | "copy-medium" | "cta-semibold";
3
3
  interface Props {
4
4
  variant?: TagVariant;
5
5
  component?: string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Host config input type - plain object that HostConfig constructor parses.
3
+ * We don't use Partial<HostConfig> because HostConfig properties are class instances,
4
+ * but the constructor accepts plain objects and converts them internally.
5
+ */
6
+ type HostConfigInput = Record<string, unknown>;
7
+ interface Props {
8
+ /**
9
+ * The Adaptive Card payload to render
10
+ */
11
+ payload?: Record<string, unknown>;
12
+ /**
13
+ * Host configuration for styling the card.
14
+ * Accepts a plain object matching the HostConfig structure - the library
15
+ * parses this internally when constructing the HostConfig instance.
16
+ */
17
+ hostConfig?: HostConfigInput;
18
+ /**
19
+ * When true, disables all inputs and buttons after rendering
20
+ * Useful for displaying submitted cards in chat history
21
+ */
22
+ readonly?: boolean;
23
+ /**
24
+ * Data to pre-fill into input fields
25
+ * Keys should match input element IDs in the card
26
+ * Used to show submitted values in chat history
27
+ */
28
+ inputData?: Record<string, unknown>;
29
+ }
30
+ declare const _default: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<Props> & Readonly<{}>, {
31
+ readonly: boolean;
32
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
33
+ targetRef: HTMLDivElement;
34
+ }, HTMLDivElement>;
35
+ export default _default;
@@ -2,7 +2,7 @@ import { IWebchatAttachmentElement } from '../../types';
2
2
  interface Props {
3
3
  element: IWebchatAttachmentElement;
4
4
  isHeaderElement?: boolean;
5
- headingLevel?: 'h4' | 'h5';
5
+ headingLevel?: "h4" | "h5";
6
6
  id: string;
7
7
  dividerBefore?: boolean;
8
8
  dividerAfter?: boolean;
@@ -0,0 +1,30 @@
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
+ interface UseLiveRegionOptions {
12
+ /** Text to announce to screen readers */
13
+ announcement: string;
14
+ /** Whether the announcement should be made (defaults to true) */
15
+ enabled?: boolean;
16
+ }
17
+ /**
18
+ * Composable that announces content to screen readers via an ARIA live region.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * // In a message component:
23
+ * useLiveRegion({
24
+ * announcement: `New message: ${messageText}`,
25
+ * enabled: !props.ignoreLiveRegion,
26
+ * })
27
+ * ```
28
+ */
29
+ export declare function useLiveRegion(options: UseLiveRegionOptions): void;
30
+ export {};
package/dist/index.d.ts CHANGED
@@ -24,10 +24,11 @@ export { useSanitize } from './composables/useSanitize';
24
24
  export { useImageContext, provideImageContext, ImageContextKey } from './composables/useImageContext';
25
25
  export { useChannelPayload, type UseChannelPayloadReturn } from './composables/useChannelPayload';
26
26
  export { useCollation, type UseCollationReturn, type CollatedMessage } from './composables/useCollation';
27
+ export { useLiveRegion } from './composables/useLiveRegion';
27
28
  export { DownloadIcon, CloseIcon, VideoPlayIcon, AudioPlayIcon, AudioPauseIcon, ArrowBackIcon, LinkIcon } from './assets/svg';
28
29
  export { match, getChannelPayload } from './utils/matcher';
29
30
  export { sanitizeHTMLWithConfig, sanitizeContent } from './utils/sanitize';
30
- export { configColorsToCssVariables } from './utils/theme';
31
+ export { configColorsToCssVariables, themeFontToCssVariable } from './utils/theme';
31
32
  export { getWebchatButtonLabel, interpolateString, getRandomId, moveFocusToMessageFocusTarget, replaceUrlsWithHTMLanchorElem, getBackgroundImage, getFileName, getFileExtension, getSizeLabel, isImageAttachment, VALID_IMAGE_MIME_TYPES } from './utils/helpers';
32
- export type { ChatConfig, ChatSettings, ChatTheme, MessageProps, MessageSender, MessagePlugin, MessagePluginOptions, MessageContext, IMessage, IWebchatButton, IWebchatQuickReply, IWebchatTemplateAttachment, IWebchatAttachmentElement, IWebchatAudioAttachment, IWebchatImageAttachment, IWebchatVideoAttachment, IUploadFileAttachmentData, IDatePickerData, IWebchatChannelPayload, } from './types';
33
+ export type { ChatConfig, ChatSettings, ChatTheme, MessageProps, MessageSender, MessagePlugin, MessagePluginOptions, MessageContext, MessageParams, IMessage, IStreamingMessage, IWebchatButton, IWebchatQuickReply, IWebchatTemplateAttachment, IWebchatAttachmentElement, IWebchatAudioAttachment, IWebchatImageAttachment, IWebchatVideoAttachment, IUploadFileAttachmentData, IDatePickerData, IWebchatChannelPayload, } from './types';
33
34
  export type { TagVariant } from './components/common/Typography.vue';
@@ -1,5 +1,23 @@
1
1
  import { IMessage, IWebchatQuickReply as QuickReply, IWebchatTemplateAttachment as TemplateAttachment, IWebchatAudioAttachment as AudioAttachment, IWebchatImageAttachment as ImageAttachment, IWebchatVideoAttachment as VideoAttachment } from '@cognigy/socket-client';
2
2
  import { Component } from 'vue';
3
+ /**
4
+ * Parameters about the current message state in the conversation.
5
+ * Used to control interactive element behavior (e.g., disabling quick replies after user reply).
6
+ */
7
+ export interface MessageParams {
8
+ /** Whether the user has already replied to this message (disables quick reply buttons) */
9
+ hasReply?: boolean;
10
+ /** Whether the conversation has ended (disables all interactive elements) */
11
+ isConversationEnded?: boolean;
12
+ }
13
+ /**
14
+ * Extended IMessage with streaming animation state.
15
+ * Used with progressive message rendering to conditionally show/hide elements
16
+ * based on whether the message is still being streamed.
17
+ */
18
+ export interface IStreamingMessage extends IMessage {
19
+ animationState?: 'start' | 'animating' | 'done';
20
+ }
3
21
  /**
4
22
  * Extended IMessage with optional runtime properties not in base interface.
5
23
  * The socket-client IMessage doesn't include 'id', but it may be present at runtime.
@@ -61,6 +79,13 @@ export interface ChatSettings {
61
79
  disableUrlButtonSanitization?: boolean;
62
80
  dynamicImageAspectRatio?: boolean;
63
81
  showEngagementInChat?: boolean;
82
+ /**
83
+ * Gallery layout variant:
84
+ * - 'default': Standard gallery with 206px wide slides
85
+ * - 'compact': Narrower slides with smaller navigation buttons
86
+ * - 'copilot': Adaptive sizing based on card count (1-card, 2-card, 3+ layouts)
87
+ */
88
+ galleryVariant?: 'default' | 'compact' | 'copilot';
64
89
  };
65
90
  colors?: {
66
91
  primaryColor?: string;
@@ -78,7 +103,19 @@ export interface ChatSettings {
78
103
  borderBotMessage?: string;
79
104
  borderUserMessage?: string;
80
105
  borderAgentMessage?: string;
106
+ borderMediaCard?: string;
107
+ listDividerColor?: string;
81
108
  textLinkColor?: string;
109
+ adaptiveCardTextColor?: string;
110
+ adaptiveCardInputColor?: string;
111
+ adaptiveCardInputBackground?: string;
112
+ adaptiveCardInputBorder?: string;
113
+ filePreviewBackground?: string;
114
+ filePreviewTextColor?: string;
115
+ filePreviewSecondaryTextColor?: string;
116
+ chatEventBackground?: string;
117
+ chatEventTextColor?: string;
118
+ messageShadow?: string;
82
119
  };
83
120
  behavior?: {
84
121
  renderMarkdown?: boolean;
@@ -86,6 +123,21 @@ export interface ChatSettings {
86
123
  messageDelay?: number;
87
124
  collateStreamedOutputs?: boolean;
88
125
  focusInputAfterPostback?: boolean;
126
+ /**
127
+ * Controls adaptive card interactivity.
128
+ * - `true`: All cards are readonly (presentation only), regardless of submitted data
129
+ * - `false` or `undefined`: Smart default - readonly only if card has submitted data
130
+ *
131
+ * Use `true` for chat history/transcript displays where no interaction is needed.
132
+ * Use `false`/omit for interactive chat interfaces with smart auto-detection.
133
+ */
134
+ adaptiveCardsReadonly?: boolean;
135
+ /**
136
+ * When true, buttons/quick replies are hidden while a message is still being streamed.
137
+ * Works with IStreamingMessage.animationState to delay showing interactive elements
138
+ * until the text animation is complete.
139
+ */
140
+ progressiveMessageRendering?: boolean;
89
141
  };
90
142
  widgetSettings?: {
91
143
  enableDefaultPreview?: boolean;
@@ -174,6 +226,14 @@ export interface MessageProps {
174
226
  disableHeader?: boolean;
175
227
  plugins?: MessagePlugin[];
176
228
  onEmitAnalytics?: (event: string, payload?: unknown) => void;
229
+ /** Conversation state parameters (hasReply, isConversationEnded) */
230
+ messageParams?: MessageParams;
231
+ /** Callback to open an xApp overlay (for openXApp button type) */
232
+ openXAppOverlay?: (url: string | undefined) => void;
233
+ /** Callback when gallery slide changes — receives (activeIndex, totalSlides) */
234
+ onSlideChange?: (index: number, total: number) => void;
235
+ /** Callback when an image is clicked — receives the image URL */
236
+ onImageClick?: (url: string) => void;
177
237
  }
178
238
  /**
179
239
  * Options for match rules and plugins
@@ -230,6 +290,14 @@ export interface MessageContext {
230
290
  theme?: ChatTheme;
231
291
  action?: MessageSender;
232
292
  onEmitAnalytics?: (event: string, payload?: unknown) => void;
293
+ /** Conversation state parameters (hasReply, isConversationEnded) */
294
+ messageParams?: MessageParams;
295
+ /** Callback to open an xApp overlay (for openXApp button type) */
296
+ openXAppOverlay?: (url: string | undefined) => void;
297
+ /** Callback when gallery slide changes — receives (activeIndex, totalSlides) */
298
+ onSlideChange?: (index: number, total: number) => void;
299
+ /** Callback when an image is clicked — receives the image URL */
300
+ onImageClick?: (url: string) => void;
233
301
  }
234
302
  /**
235
303
  * DatePicker plugin data interface
@@ -271,5 +339,41 @@ export interface IWebchatChannelPayload {
271
339
  quick_replies?: QuickReply[];
272
340
  attachment?: TemplateAttachment | AudioAttachment | ImageAttachment | VideoAttachment;
273
341
  };
274
- adaptiveCard?: unknown;
342
+ adaptiveCard?: Record<string, unknown>;
343
+ /** Submitted adaptive card data */
344
+ adaptiveCardData?: Record<string, unknown>;
345
+ data?: Record<string, unknown>;
346
+ formData?: Record<string, unknown>;
347
+ }
348
+ /**
349
+ * Cognigy message data structure
350
+ * Shape of message.data._cognigy
351
+ */
352
+ export interface ICognigyData {
353
+ _webchat?: IWebchatChannelPayload;
354
+ _defaultPreview?: IWebchatChannelPayload;
355
+ _facebook?: IWebchatChannelPayload;
356
+ }
357
+ /**
358
+ * Plugin payload structure for custom message types
359
+ */
360
+ export interface IPluginPayload {
361
+ type?: string;
362
+ payload?: Record<string, unknown>;
363
+ }
364
+ /**
365
+ * Extended message data with Cognigy-specific fields
366
+ * Use this when accessing message.data with Cognigy payload structures
367
+ */
368
+ export interface IMessageDataExtended {
369
+ _cognigy?: ICognigyData;
370
+ _plugin?: IPluginPayload;
371
+ /** Adaptive card submission request data */
372
+ request?: {
373
+ value?: Record<string, unknown>;
374
+ };
375
+ /** Direct adaptive card data fields (fallback locations) */
376
+ adaptiveCardData?: Record<string, unknown>;
377
+ data?: Record<string, unknown>;
378
+ formData?: Record<string, unknown>;
275
379
  }
@@ -24,6 +24,7 @@ export declare function moveFocusToMessageFocusTarget(dataMessageId: string): vo
24
24
  * - Works with URLs starting with http/https, www., or just domain/subdomain
25
25
  * - Will only match URLs at the beginning or following whitespace
26
26
  * - Will not work with emails
27
+ * - URLs are escaped to prevent injection attacks
27
28
  */
28
29
  export declare function replaceUrlsWithHTMLanchorElem(text: string): string;
29
30
  /**
@@ -47,9 +48,9 @@ export declare function getFileExtension(fileNameWithExtension: string): string
47
48
  */
48
49
  export declare function getSizeLabel(size: number): string;
49
50
  /**
50
- * Valid image MIME types for file attachments
51
+ * Valid image MIME types for file attachments (Set for O(1) lookup)
51
52
  */
52
- export declare const VALID_IMAGE_MIME_TYPES: string[];
53
+ export declare const VALID_IMAGE_MIME_TYPES: Set<string>;
53
54
  /**
54
55
  * Checks if attachment is a valid image type
55
56
  */
@@ -5,9 +5,9 @@ import { ChatConfig, MatchRule, MessagePlugin, MatchResult } from '../types';
5
5
  */
6
6
  export declare function getChannelPayload(message: IMessage, config?: ChatConfig): any;
7
7
  /**
8
- * Default match rules for internal message types.
9
- * These rules map message data structures to component names.
10
- * Components are resolved by name lookup in Message.vue.
8
+ * Returns a copy of the default match rules.
9
+ * Exposed for testing and extension purposes.
10
+ * Returns a new array to prevent mutation of internal rules.
11
11
  */
12
12
  export declare function createDefaultMatchRules(): MatchRule[];
13
13
  /**
@@ -1,4 +1,4 @@
1
- import { ChatSettings } from '../types';
1
+ import { ChatSettings, ChatTheme } from '../types';
2
2
  /**
3
3
  * Maps config.settings.colors to CSS custom properties.
4
4
  * Returns an object suitable for use as an inline style that sets CSS variables.
@@ -16,3 +16,14 @@ import { ChatSettings } from '../types';
16
16
  * ```
17
17
  */
18
18
  export declare function configColorsToCssVariables(colors?: ChatSettings['colors']): Record<string, string>;
19
+ /**
20
+ * Maps a ChatTheme's fontFamily to the --cc-font-family CSS variable.
21
+ * Consumers can set this to override the default Figtree font.
22
+ *
23
+ * Components should reference font-family as:
24
+ * font-family: var(--cc-font-family, 'Figtree', sans-serif);
25
+ *
26
+ * @param theme - The ChatTheme object
27
+ * @returns A record with the --cc-font-family CSS variable, or empty object
28
+ */
29
+ export declare function themeFontToCssVariable(theme?: ChatTheme): Record<string, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cognigy/chat-components-vue",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Vue 3 version of @cognigy/chat-components",
6
6
  "license": "MIT",
@@ -52,6 +52,9 @@
52
52
  "test:watch": "vitest",
53
53
  "test:ui": "vitest --ui",
54
54
  "test:coverage": "vitest run --coverage",
55
+ "test:e2e": "playwright test",
56
+ "test:e2e:update": "playwright test --update-snapshots",
57
+ "test:e2e:ui": "playwright test --ui",
55
58
  "lint": "eslint .",
56
59
  "type-check": "vue-tsc --noEmit",
57
60
  "prepare": "npm run build"
@@ -63,13 +66,15 @@
63
66
  "@braintree/sanitize-url": "^7.1.1",
64
67
  "@cognigy/socket-client": "5.0.0-beta.25",
65
68
  "@fontsource/figtree": "5.2.10",
69
+ "adaptivecards": "^3.0.5",
66
70
  "dompurify": "^3.0.11",
67
71
  "flatpickr": "^4.6.13",
68
72
  "markdown-it": "^14.1.0",
69
73
  "moment": "^2.30.1",
70
- "swiper": "^12.0.3"
74
+ "swiper": "^11.2.10"
71
75
  },
72
76
  "devDependencies": {
77
+ "@playwright/test": "^1.58.2",
73
78
  "@types/markdown-it": "^14.1.2",
74
79
  "@types/node": "^25.0.9",
75
80
  "@vitejs/plugin-vue": "^6.0.0",
@@ -78,7 +83,7 @@
78
83
  "eslint": "^9.0.0",
79
84
  "eslint-plugin-vue": "^10.7.0",
80
85
  "globals": "^17.0.0",
81
- "jsdom": "^27.0.0",
86
+ "jsdom": "^28.0.0",
82
87
  "npm-check-updates": "^19.3.1",
83
88
  "typescript": "^5.9.0",
84
89
  "typescript-eslint": "^8.53.1",
@@ -22,26 +22,32 @@
22
22
  </template>
23
23
 
24
24
  <script setup lang="ts">
25
- import { computed, useCssModule, type Component } from 'vue'
26
- import { match } from '../utils/matcher'
27
- import { configColorsToCssVariables } from '../utils/theme'
28
- import { provideMessageContext } from '../composables/useMessageContext'
29
- import { getMessageId, isMessagePlugin } from '../types'
30
- import type { MessageProps } from '../types'
31
-
32
- const $style = useCssModule()
25
+ import {
26
+ computed,
27
+ reactive,
28
+ watchEffect,
29
+ useCssModule,
30
+ type Component,
31
+ } from "vue";
32
+ import { match } from "../utils/matcher";
33
+ import {configColorsToCssVariables, themeFontToCssVariable} from "../utils/theme";
34
+ import { provideMessageContext } from "../composables/useMessageContext";
35
+ import { getMessageId, isMessagePlugin } from "../types";
36
+ import type { MessageProps, MessageContext } from "../types";
37
+
38
+ const $style = useCssModule();
33
39
 
34
40
  // Import all message type components
35
- import TextMessage from './messages/TextMessage.vue'
36
- import ImageMessage from './messages/ImageMessage.vue'
37
- import VideoMessage from './messages/VideoMessage.vue'
38
- import AudioMessage from './messages/AudioMessage.vue'
39
- import TextWithButtons from './messages/TextWithButtons.vue'
40
- import Gallery from './messages/Gallery.vue'
41
- import List from './messages/List.vue'
42
- import FileMessage from './messages/FileMessage.vue'
43
- import DatePicker from './messages/DatePicker.vue'
44
- import AdaptiveCard from './messages/AdaptiveCard.vue'
41
+ import TextMessage from "./messages/TextMessage.vue";
42
+ import ImageMessage from "./messages/ImageMessage.vue";
43
+ import VideoMessage from "./messages/VideoMessage.vue";
44
+ import AudioMessage from "./messages/AudioMessage.vue";
45
+ import TextWithButtons from "./messages/TextWithButtons.vue";
46
+ import Gallery from "./messages/Gallery.vue";
47
+ import List from "./messages/List.vue";
48
+ import FileMessage from "./messages/FileMessage.vue";
49
+ import DatePicker from "./messages/DatePicker.vue";
50
+ import AdaptiveCard from "./messages/AdaptiveCard.vue";
45
51
 
46
52
  const props = withDefaults(defineProps<MessageProps>(), {
47
53
  action: undefined,
@@ -51,70 +57,108 @@ const props = withDefaults(defineProps<MessageProps>(), {
51
57
  plugins: undefined,
52
58
  onEmitAnalytics: undefined,
53
59
  disableHeader: false,
54
- })
60
+ messageParams: undefined,
61
+ openXAppOverlay: undefined,
62
+ onSlideChange: undefined,
63
+ onImageClick: undefined,
64
+ });
55
65
 
56
66
  // Generate a unique message ID for accessibility
57
- const dataMessageId = computed(() => getMessageId(props.message))
67
+ const dataMessageId = computed(() => getMessageId(props.message));
58
68
 
59
69
  // Provide message context for child components
60
- provideMessageContext({
70
+ // Using reactive object synced via watchEffect for proper reactivity tracking
71
+ const messageContext = reactive<MessageContext>({
61
72
  message: props.message,
62
- config: props.config || {},
63
- action: props.action || (() => {}),
64
- onEmitAnalytics: props.onEmitAnalytics || (() => {}),
65
- })
73
+ config: props.config,
74
+ action: props.action,
75
+ onEmitAnalytics: props.onEmitAnalytics,
76
+ messageParams: props.messageParams,
77
+ openXAppOverlay: props.openXAppOverlay,
78
+ onSlideChange: props.onSlideChange,
79
+ onImageClick: props.onImageClick,
80
+ });
81
+
82
+ // Keep context in sync with props changes
83
+ watchEffect(() => {
84
+ messageContext.message = props.message
85
+ messageContext.config = props.config
86
+ messageContext.action = props.action
87
+ messageContext.onEmitAnalytics = props.onEmitAnalytics
88
+ messageContext.messageParams = props.messageParams
89
+ messageContext.openXAppOverlay = props.openXAppOverlay
90
+ messageContext.onSlideChange = props.onSlideChange
91
+ messageContext.onImageClick = props.onImageClick
92
+ });
93
+
94
+ provideMessageContext(messageContext);
66
95
 
67
96
  // Component map for internal match rules (maps rule names to Vue components)
68
97
  const componentMap: Record<string, Component> = {
69
- 'Text': TextMessage,
70
- 'Image': ImageMessage,
71
- 'Video': VideoMessage,
72
- 'Audio': AudioMessage,
73
- 'TextWithButtons': TextWithButtons,
74
- 'Gallery': Gallery,
75
- 'List': List,
76
- 'File': FileMessage,
77
- 'DatePicker': DatePicker,
78
- 'AdaptiveCard': AdaptiveCard,
79
- }
98
+ Text: TextMessage,
99
+ Image: ImageMessage,
100
+ Video: VideoMessage,
101
+ Audio: AudioMessage,
102
+ TextWithButtons: TextWithButtons,
103
+ Gallery: Gallery,
104
+ List: List,
105
+ File: FileMessage,
106
+ DatePicker: DatePicker,
107
+ AdaptiveCard: AdaptiveCard,
108
+ };
80
109
 
81
110
  // Match message to appropriate components
82
111
  const matchedComponents = computed(() => {
83
- const matched = match(props.message, props.config, props.plugins)
112
+ const matched = match(props.message, props.config, props.plugins);
84
113
 
85
114
  if (!Array.isArray(matched) || matched.length < 1) {
86
- return []
115
+ return [];
87
116
  }
88
117
 
89
118
  // Resolve components from match results
90
119
  return matched
91
- .map(rule => {
120
+ .map((rule) => {
92
121
  // External plugins (MessagePlugin) provide their own component
93
122
  if (isMessagePlugin(rule)) {
94
- return rule.component
123
+ return rule.component;
95
124
  }
96
125
  // Internal rules (MatchRule) use name lookup in componentMap
97
- return componentMap[rule.name] ?? null
126
+ return componentMap[rule.name] ?? null;
98
127
  })
99
- .filter((c): c is Component => c !== null)
128
+ .filter((c): c is Component => c !== null);
129
+ });
130
+
131
+ // Compute direction based on source + config mapping
132
+ const directionMapping = computed(() => props.config?.settings?.widgetSettings?.sourceDirectionMapping)
133
+
134
+ const messageDirection = computed(() => {
135
+ const source = props.message.source
136
+ if (source === 'user') return directionMapping.value?.user || 'outgoing'
137
+ if (source === 'bot') return directionMapping.value?.bot || 'incoming'
138
+ if (source === 'agent') return directionMapping.value?.agent || 'incoming'
139
+ return 'incoming'
100
140
  })
101
141
 
102
142
  // Root element classes
103
143
  const rootClasses = computed(() => {
104
144
  return [
105
145
  'webchat-message-row',
106
- props.message.source,
146
+ props.message.source, // keep global source class (for ChatBubble color selectors)
107
147
  $style.message,
108
- props.message.source === 'bot' && $style.bot,
109
- props.message.source === 'user' && $style.user,
110
- props.message.source === 'agent' && $style.agent,
148
+ props.message.source === "bot" && $style.bot,
149
+ props.message.source === "user" && $style.user,
150
+ props.message.source === "agent" && $style.agent,
151
+ $style[messageDirection.value], // incoming or outgoing alignment
111
152
  ].filter(Boolean)
112
153
  })
113
154
 
114
- // CSS variable injection from config colors
155
+ // CSS variable injection from config colors and theme font
115
156
  const cssVariableStyle = computed(() => {
116
- return configColorsToCssVariables(props.config?.settings?.colors)
117
- })
157
+ return {
158
+ ...configColorsToCssVariables(props.config?.settings?.colors),
159
+ ...themeFontToCssVariable(props.theme),
160
+ }
161
+ });
118
162
  </script>
119
163
 
120
164
  <style module>
@@ -123,20 +167,19 @@ const cssVariableStyle = computed(() => {
123
167
  flex-direction: column;
124
168
  gap: 8px;
125
169
  margin-bottom: 16px;
170
+ font-family: var(--cc-font-family, 'Figtree', sans-serif);
171
+ padding-left: 56px;
172
+ padding-right: 16px;
126
173
  }
127
174
 
128
- .message.bot {
175
+ .message.incoming {
129
176
  align-items: flex-start;
130
177
  }
131
178
 
132
- .message.user {
179
+ .message.outgoing {
133
180
  align-items: flex-end;
134
181
  }
135
182
 
136
- .message.agent {
137
- align-items: flex-start;
138
- }
139
-
140
183
  /* Screen reader only */
141
184
  .srOnly {
142
185
  position: absolute;
@@ -282,7 +282,7 @@ a.button {
282
282
  gap: 10px;
283
283
  text-decoration: none;
284
284
  background: var(--cc-primary-color);
285
- color: var(--cc-primary-contrast-color);
285
+ color: var(--cc-primary-contrast-color, #ffffff);
286
286
  border: none;
287
287
  outline: none;
288
288
  position: relative;
@@ -296,7 +296,7 @@ button.button svg,
296
296
  a.button svg,
297
297
  button.button path,
298
298
  a.button svg path {
299
- fill: var(--cc-primary-contrast-color);
299
+ fill: var(--cc-primary-contrast-color, #ffffff);
300
300
  width: 12px;
301
301
  }
302
302
 
@@ -304,16 +304,25 @@ button.button:hover,
304
304
  a.button:hover,
305
305
  button.button:focus,
306
306
  a.button:focus {
307
- background: var(--cc-primary-color-hover);
307
+ background: var(--cc-primary-color-hover, color-mix(in srgb, var(--cc-primary-color), black 15%));
308
308
  }
309
309
 
310
310
  /* Explicitly increase the specificity of the :focus-visible selector */
311
311
  [data-cognigy-webchat-root] button.button:focus-visible,
312
312
  [data-cognigy-webchat-root] a.button:focus-visible,
313
313
  [data-cognigy-webchat-root] a.button:global(.phone-number-or-url-anchor):focus-visible {
314
- outline: 2px solid var(--cc-primary-color-focus);
314
+ outline: 2px solid var(--cc-primary-color-focus, var(--cc-primary-color));
315
315
  outline-offset: 2px;
316
- box-shadow: 0 0 0 4px var(--cc-primary-contrast-color);
316
+ box-shadow: 0 0 0 4px var(--cc-primary-contrast-color, #ffffff);
317
+ }
318
+
319
+ /* Fallback for consumers without [data-cognigy-webchat-root] wrapper.
320
+ :where() has zero specificity, so the rules above win when the wrapper exists. */
321
+ :where(button.button):focus-visible,
322
+ :where(a.button):focus-visible {
323
+ outline: 2px solid var(--cc-primary-color-focus, var(--cc-primary-color));
324
+ outline-offset: 2px;
325
+ box-shadow: 0 0 0 4px var(--cc-primary-contrast-color, #ffffff);
317
326
  }
318
327
 
319
328
  button.button:disabled,
@@ -322,7 +331,7 @@ button.button:disabled:focus,
322
331
  a.button.disabled,
323
332
  a.button.disabled:hover,
324
333
  a.button.disabled:focus {
325
- background: var(--cc-primary-color-disabled);
334
+ background: var(--cc-primary-color-disabled, color-mix(in srgb, var(--cc-primary-color) 40%, #999));
326
335
  cursor: default;
327
336
  pointer-events: none;
328
337
  }
@@ -349,6 +358,6 @@ a.button.disabled:focus {
349
358
  left: 0;
350
359
  width: 40px;
351
360
  height: 100%;
352
- border-right: 2px solid var(--cc-primary-contrast-color);
361
+ border-right: 2px solid var(--cc-primary-contrast-color, #ffffff);
353
362
  }
354
363
  </style>