@envive-ai/react-toolkit-v3 0.3.26 → 0.3.28

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 (223) hide show
  1. package/dist/AnimatedText/AnimatedText.d.ts +3 -3
  2. package/dist/CSSVariablesEditor/CssVariablesEditorComponent.d.cts +2 -2
  3. package/dist/Carousel/Carousel.cjs +1 -1
  4. package/dist/Carousel/Carousel.d.cts +2 -2
  5. package/dist/Carousel/Carousel.d.ts +2 -2
  6. package/dist/Carousel/Carousel.js +1 -1
  7. package/dist/Carousel/components/Container.cjs +2 -2
  8. package/dist/Carousel/components/Container.js +2 -2
  9. package/dist/ChatFooter/ChatFooter.cjs +3 -1
  10. package/dist/ChatFooter/ChatFooter.d.cts +2 -2
  11. package/dist/ChatFooter/ChatFooter.d.ts +2 -2
  12. package/dist/ChatFooter/ChatFooter.js +3 -1
  13. package/dist/ChatFooter/components/Layout.cjs +4 -4
  14. package/dist/ChatFooter/components/Layout.d.cts +1 -0
  15. package/dist/ChatFooter/components/Layout.d.ts +1 -0
  16. package/dist/ChatFooter/components/Layout.js +4 -4
  17. package/dist/ChatFooter/components/index.d.cts +5 -3
  18. package/dist/ChatFooter/components/index.d.ts +7 -5
  19. package/dist/ChatFooter/types/types.d.cts +3 -2
  20. package/dist/ChatFooter/types/types.d.ts +3 -2
  21. package/dist/ChatHeader/ChatHeader.d.cts +2 -2
  22. package/dist/ChatHeader/ChatHeader.d.ts +2 -2
  23. package/dist/ChatHeader/components/Handle.cjs +2 -2
  24. package/dist/ChatHeader/components/Handle.js +2 -2
  25. package/dist/ChatHeader/components/Toggle.cjs +3 -3
  26. package/dist/ChatHeader/components/Toggle.js +3 -3
  27. package/dist/ChatPreview/ChatPreview.cjs +1 -1
  28. package/dist/ChatPreview/ChatPreview.d.cts +2 -2
  29. package/dist/ChatPreview/ChatPreview.d.ts +2 -2
  30. package/dist/ChatPreview/ChatPreview.js +1 -1
  31. package/dist/ChatPreviewComparison/ChatPreviewComparison.cjs +1 -1
  32. package/dist/ChatPreviewComparison/ChatPreviewComparison.d.cts +2 -2
  33. package/dist/ChatPreviewComparison/ChatPreviewComparison.d.ts +2 -2
  34. package/dist/ChatPreviewComparison/ChatPreviewComparison.js +1 -1
  35. package/dist/ChatPreviewComparison/components/Headline.cjs +2 -2
  36. package/dist/ChatPreviewComparison/components/Headline.js +2 -2
  37. package/dist/ChatPreviewComparison/components/Layout.cjs +4 -4
  38. package/dist/ChatPreviewComparison/components/Layout.js +4 -4
  39. package/dist/ChatPreviewComparison/components/Message.cjs +2 -2
  40. package/dist/ChatPreviewComparison/components/Message.js +2 -2
  41. package/dist/ChatPreviewLoading/ChatPreviewLoading.d.cts +2 -2
  42. package/dist/ChatPreviewLoading/ChatPreviewLoading.d.ts +2 -2
  43. package/dist/Container/Container.d.cts +56 -56
  44. package/dist/Container/Container.d.ts +220 -220
  45. package/dist/DesignTokens/DesignTokensComponent.d.ts +2 -2
  46. package/dist/Disclaimer/components/Container.cjs +2 -2
  47. package/dist/Disclaimer/components/Container.js +2 -2
  48. package/dist/DocumentRetrievalCard/DocumentRetrievalCard.d.cts +2 -2
  49. package/dist/DocumentRetrievalCard/DocumentRetrievalCard.d.ts +2 -2
  50. package/dist/DocumentRetrievalCard/components/Layout.cjs +2 -2
  51. package/dist/DocumentRetrievalCard/components/Layout.js +2 -2
  52. package/dist/DocumentRetrievalCard/components/ViewArticleButton/components/Icon.cjs +1 -1
  53. package/dist/DocumentRetrievalCard/components/ViewArticleButton/components/Icon.js +1 -1
  54. package/dist/FloatingButton/FloatingButton.d.cts +2 -2
  55. package/dist/FloatingButton/FloatingButton.d.ts +2 -2
  56. package/dist/FloatingChat/FloatingChat.d.cts +2 -2
  57. package/dist/FloatingChat/FloatingChat.d.ts +2 -2
  58. package/dist/FloatingChat/components/ChatMessages.cjs +1 -1
  59. package/dist/FloatingChat/components/ChatMessages.js +1 -1
  60. package/dist/FloatingChat/components/Layout.cjs +3 -3
  61. package/dist/FloatingChat/components/Layout.js +3 -3
  62. package/dist/FloatingChat/components/ResultsGridView.cjs +1 -1
  63. package/dist/FloatingChat/components/ResultsGridView.js +1 -1
  64. package/dist/FloatingChat/components/SalesAgentBadgeContent.cjs +6 -2
  65. package/dist/FloatingChat/components/SalesAgentBadgeContent.js +6 -2
  66. package/dist/FloatingChat/components/SalesAgentProductCardsCarousel.cjs +2 -1
  67. package/dist/FloatingChat/components/SalesAgentProductCardsCarousel.js +2 -1
  68. package/dist/FullPageSalesAgent/FullPageSalesAgent.cjs +126 -27
  69. package/dist/FullPageSalesAgent/FullPageSalesAgent.d.cts +5 -7
  70. package/dist/FullPageSalesAgent/FullPageSalesAgent.d.ts +5 -7
  71. package/dist/FullPageSalesAgent/FullPageSalesAgent.js +127 -28
  72. package/dist/FullPageSalesAgent/components/Layout.cjs +34 -27
  73. package/dist/FullPageSalesAgent/components/Layout.js +34 -27
  74. package/dist/FullPageSalesAgent/components/WelcomeOverlay.cjs +109 -0
  75. package/dist/FullPageSalesAgent/components/WelcomeOverlay.js +107 -0
  76. package/dist/FullPageSalesAgent/components/index.cjs +1 -0
  77. package/dist/FullPageSalesAgent/components/index.js +1 -0
  78. package/dist/FullPageSalesAgent/hooks/useGetFooterStyles.cjs +16 -6
  79. package/dist/FullPageSalesAgent/hooks/useGetFooterStyles.js +16 -6
  80. package/dist/FullPageSalesAgent/hooks/useWelcomeOverlayProducts.cjs +44 -0
  81. package/dist/FullPageSalesAgent/hooks/useWelcomeOverlayProducts.js +42 -0
  82. package/dist/Image/Image.d.cts +2 -2
  83. package/dist/Image/Image.d.ts +2 -2
  84. package/dist/ImageGallery/ImageGallery.d.cts +2 -2
  85. package/dist/ImageGallery/ImageGallery.d.ts +2 -2
  86. package/dist/ImageGallery/components/Layout.cjs +1 -1
  87. package/dist/ImageGallery/components/Layout.js +1 -1
  88. package/dist/MarkdownProcessor/MarkdownProcessor.d.cts +2 -2
  89. package/dist/MarkdownProcessor/MarkdownProcessor.d.ts +2 -2
  90. package/dist/Message/components/LinkButton.cjs +1 -1
  91. package/dist/Message/components/LinkButton.js +1 -1
  92. package/dist/OrderLookupCard/OrderLookupCard.cjs +1 -1
  93. package/dist/OrderLookupCard/OrderLookupCard.js +1 -1
  94. package/dist/ProductCard/ProductCard.cjs +2 -2
  95. package/dist/ProductCard/ProductCard.d.cts +2 -2
  96. package/dist/ProductCard/ProductCard.d.ts +2 -2
  97. package/dist/ProductCard/ProductCard.js +2 -2
  98. package/dist/PromptButton/PromptButton.d.cts +2 -2
  99. package/dist/PromptButton/PromptButton.d.ts +2 -2
  100. package/dist/PromptButton/hooks/useGetLayoutBaseProperties.cjs +1 -1
  101. package/dist/PromptButton/hooks/useGetLayoutBaseProperties.js +1 -1
  102. package/dist/PromptButtonCarouselWithImage/PromptButtonCarouselWithImage.d.cts +2 -2
  103. package/dist/PromptButtonCarouselWithImage/PromptButtonCarouselWithImage.d.ts +2 -2
  104. package/dist/PromptButtonCarouselWithImage/components/PromptButtonsCarousel.cjs +1 -1
  105. package/dist/PromptButtonCarouselWithImage/components/PromptButtonsCarousel.js +1 -1
  106. package/dist/PromptCarousel/PromptCarousel.cjs +3 -3
  107. package/dist/PromptCarousel/PromptCarousel.d.cts +2 -2
  108. package/dist/PromptCarousel/PromptCarousel.d.ts +2 -2
  109. package/dist/PromptCarousel/PromptCarousel.js +3 -3
  110. package/dist/ReviewCard/ReviewCard.d.ts +2 -2
  111. package/dist/ReviewCard/components/Container.cjs +2 -2
  112. package/dist/ReviewCard/components/Container.js +2 -2
  113. package/dist/ReviewCard/components/ReadMoreButton.cjs +1 -1
  114. package/dist/ReviewCard/components/ReadMoreButton.js +1 -1
  115. package/dist/ReviewCard/components/index.d.cts +4 -4
  116. package/dist/ReviewCard/components/index.d.ts +6 -6
  117. package/dist/SalesAgentProductCard/SalesAgentProductCard.d.cts +2 -2
  118. package/dist/SalesAgentProductCard/SalesAgentProductCard.d.ts +2 -2
  119. package/dist/SalesAgentProductCard/components/Container.cjs +2 -2
  120. package/dist/SalesAgentProductCard/components/Container.js +2 -2
  121. package/dist/SalesAgentProductCard/components/Price.cjs +2 -2
  122. package/dist/SalesAgentProductCard/components/Price.js +2 -2
  123. package/dist/SalesAgentProductCard/components/Rate.cjs +1 -1
  124. package/dist/SalesAgentProductCard/components/Rate.js +1 -1
  125. package/dist/SalesAgentProductCard/components/index.d.cts +8 -8
  126. package/dist/SalesAgentProductCard/components/index.d.ts +8 -8
  127. package/dist/SocialProof/SocialProof.cjs +5 -4
  128. package/dist/SocialProof/SocialProof.d.cts +2 -2
  129. package/dist/SocialProof/SocialProof.d.ts +2 -2
  130. package/dist/SocialProof/SocialProof.js +5 -4
  131. package/dist/SocialProof/components/Headline.cjs +4 -4
  132. package/dist/SocialProof/components/Headline.js +4 -4
  133. package/dist/SocialProof/components/LayoutFourHorizontal.cjs +1 -1
  134. package/dist/SocialProof/components/LayoutFourHorizontal.js +1 -1
  135. package/dist/SocialProof/components/LayoutSingle.cjs +3 -4
  136. package/dist/SocialProof/components/LayoutSingle.js +3 -4
  137. package/dist/SocialProof/components/Subheadline.cjs +1 -1
  138. package/dist/SocialProof/components/Subheadline.js +1 -1
  139. package/dist/SocialProof/hooks/useSocialProofCount.cjs +3 -2
  140. package/dist/SocialProof/hooks/useSocialProofCount.d.cts +2 -0
  141. package/dist/SocialProof/hooks/useSocialProofCount.d.ts +2 -0
  142. package/dist/SocialProof/hooks/useSocialProofCount.js +3 -2
  143. package/dist/SocialProof/types/types.d.cts +5 -0
  144. package/dist/SocialProof/types/types.d.ts +5 -0
  145. package/dist/SparkleAnimation/SparkleAnimation.d.cts +2 -2
  146. package/dist/SparkleAnimation/SparkleAnimation.d.ts +2 -2
  147. package/dist/Stack/Stack.d.cts +2 -2
  148. package/dist/Stack/Stack.d.ts +2 -2
  149. package/dist/TextField/TextField.cjs +10 -3
  150. package/dist/TextField/TextField.d.cts +1 -0
  151. package/dist/TextField/TextField.d.ts +1 -0
  152. package/dist/TextField/TextField.js +10 -3
  153. package/dist/TextField/components/Input.cjs +31 -2
  154. package/dist/TextField/components/Input.js +31 -2
  155. package/dist/TextField/components/Layout.cjs +7 -2
  156. package/dist/TextField/components/Layout.js +7 -2
  157. package/dist/TextField/hooks/useAutoResize.cjs +17 -0
  158. package/dist/TextField/hooks/useAutoResize.js +16 -0
  159. package/dist/TextField/hooks/usePlaceholderAnimation.cjs +58 -0
  160. package/dist/TextField/hooks/usePlaceholderAnimation.js +57 -0
  161. package/dist/TextField/hooks/useVoiceInput.cjs +9 -1
  162. package/dist/TextField/hooks/useVoiceInput.js +9 -1
  163. package/dist/TextField/types/index.d.cts +19 -1
  164. package/dist/TextField/types/index.d.ts +19 -1
  165. package/dist/TitledPromptCarousel/TitledPromptCarousel.cjs +1 -1
  166. package/dist/TitledPromptCarousel/TitledPromptCarousel.d.cts +2 -2
  167. package/dist/TitledPromptCarousel/TitledPromptCarousel.d.ts +2 -2
  168. package/dist/TitledPromptCarousel/TitledPromptCarousel.js +1 -1
  169. package/dist/Tokens/index.cjs +1 -1
  170. package/dist/Tokens/index.js +1 -1
  171. package/dist/TypingAnimation/TypingAnimation.cjs +1 -1
  172. package/dist/TypingAnimation/TypingAnimation.d.cts +2 -2
  173. package/dist/TypingAnimation/TypingAnimation.d.ts +2 -2
  174. package/dist/TypingAnimation/TypingAnimation.js +1 -1
  175. package/dist/TypingAnimation/hooks/useGetTypographyVariant.cjs +1 -1
  176. package/dist/TypingAnimation/hooks/useGetTypographyVariant.js +1 -1
  177. package/dist/Typography/Typography.d.cts +4 -4
  178. package/dist/Typography/Typography.d.ts +4 -4
  179. package/dist/WelcomeMessage/components/Container.cjs +2 -2
  180. package/dist/WelcomeMessage/components/Container.js +2 -2
  181. package/dist/WidgetTextField/WidgetTextField.cjs +4 -4
  182. package/dist/WidgetTextField/WidgetTextField.d.cts +2 -2
  183. package/dist/WidgetTextField/WidgetTextField.d.ts +2 -2
  184. package/dist/WidgetTextField/WidgetTextField.js +1 -1
  185. package/dist/WidgetTextField/components/Container.cjs +2 -2
  186. package/dist/WidgetTextField/components/Container.js +2 -2
  187. package/dist/WidgetTextField/components/Icon.cjs +1 -1
  188. package/dist/WidgetTextField/components/Icon.js +1 -1
  189. package/dist/WidgetWrapper/WidgetWrapper.d.cts +2 -2
  190. package/dist/WidgetWrapper/WidgetWrapper.d.ts +2 -2
  191. package/dist/WidgetWrapperWithTitle/WidgetWrapperWithTitle.d.cts +2 -2
  192. package/dist/WidgetWrapperWithTitle/WidgetWrapperWithTitle.d.ts +2 -2
  193. package/dist/styles.css +1 -1
  194. package/package.json +1 -1
  195. package/src/components/ChatFooter/ChatFooter.tsx +2 -0
  196. package/src/components/ChatFooter/components/Layout.tsx +3 -1
  197. package/src/components/ChatFooter/components/TextField.tsx +4 -1
  198. package/src/components/ChatFooter/types/types.ts +3 -2
  199. package/src/components/FloatingChat/components/SalesAgentBadgeContent.tsx +8 -0
  200. package/src/components/FloatingChat/components/SalesAgentProductCardsCarousel.tsx +3 -0
  201. package/src/components/FullPageSalesAgent/FullPageSalesAgent.tsx +207 -49
  202. package/src/components/FullPageSalesAgent/components/Layout.tsx +10 -3
  203. package/src/components/FullPageSalesAgent/components/WelcomeOverlay.tsx +143 -0
  204. package/src/components/FullPageSalesAgent/components/index.ts +2 -0
  205. package/src/components/FullPageSalesAgent/hooks/useGetFooterStyles.ts +15 -5
  206. package/src/components/FullPageSalesAgent/hooks/useWelcomeOverlayProducts.ts +52 -0
  207. package/src/components/PromptButton/hooks/useGetLayoutBaseProperties.ts +3 -1
  208. package/src/components/SalesAgentProductCard/components/Price.tsx +2 -2
  209. package/src/components/SalesAgentProductCard/components/Rate.tsx +1 -1
  210. package/src/components/SocialProof/SocialProof.tsx +4 -2
  211. package/src/components/SocialProof/components/Headline.tsx +1 -1
  212. package/src/components/SocialProof/components/LayoutSingle.tsx +6 -5
  213. package/src/components/SocialProof/hooks/useSocialProofCount.ts +8 -1
  214. package/src/components/SocialProof/types/types.ts +6 -0
  215. package/src/components/TextField/TextField.tsx +17 -3
  216. package/src/components/TextField/__tests__/VoiceInputButton.test.tsx +22 -58
  217. package/src/components/TextField/components/Input.tsx +43 -9
  218. package/src/components/TextField/components/Layout.tsx +7 -1
  219. package/src/components/TextField/hooks/useAutoResize.ts +14 -0
  220. package/src/components/TextField/hooks/usePlaceholderAnimation.ts +67 -0
  221. package/src/components/TextField/hooks/useTextFieldSubmit.ts +1 -1
  222. package/src/components/TextField/hooks/useVoiceInput.ts +8 -0
  223. package/src/components/TextField/types/index.ts +19 -1
@@ -36,6 +36,7 @@ export const SocialProof = ({
36
36
  } = widgetStyleProps ?? {};
37
37
 
38
38
  const {
39
+ numberOfCustomersText,
39
40
  customerQueryText,
40
41
  primaryButtonText,
41
42
  secondaryButtonTitleText,
@@ -47,9 +48,10 @@ export const SocialProof = ({
47
48
  voiceInputEnabled,
48
49
  } = widgetContentProps ?? {};
49
50
 
50
- const numberOfCustomersText = useSocialProofCount({
51
+ const socialProofCounter = useSocialProofCount({
51
52
  pageVariant,
52
53
  countKey,
54
+ numberOfCustomersText,
53
55
  id,
54
56
  });
55
57
 
@@ -91,7 +93,7 @@ export const SocialProof = ({
91
93
  const headline = (
92
94
  <SocialProofComponents.Headline
93
95
  theme={finalTheme}
94
- socialProofCounter={numberOfCustomersText}
96
+ socialProofCounter={socialProofCounter}
95
97
  socialProofHeadline={customerQueryText}
96
98
  primaryPromptButtonText={primaryButtonText}
97
99
  primaryPromptButtonVariant={primaryButtonVariant}
@@ -48,7 +48,7 @@ export const Headline = forwardRef<HTMLDivElement, HeadlineProps>(
48
48
  <Stack
49
49
  ref={ref}
50
50
  gap="4"
51
- className={classNames('envive-tw-h-fit', className)}
51
+ className={classNames('envive-tw-h-fit envive-tw-overflow-x-auto', className)}
52
52
  >
53
53
  <Stack gap="2">
54
54
  <Container>
@@ -1,12 +1,11 @@
1
1
  import { cloneElement } from 'react';
2
- import { useCheckIsMobile } from '../../utils/useCheckIsMobile';
3
2
  import { ImageGalleryImage, ImageGalleryLayout } from '../../ImageGallery/types/types';
4
- import { useGetContentSize } from '../../utils/useGetContentSize';
5
- import { ImageGallery } from './ImageGallery';
6
3
  import { Stack } from '../../Stack';
4
+ import { useCheckIsMobile } from '../../utils/useCheckIsMobile';
5
+ import { useGetContentSize } from '../../utils/useGetContentSize';
7
6
  import { DynamicLayout } from '../types/types';
8
- import { calculateWidthImageGallery } from '../utils/functions';
9
7
  import { HeadlineProps } from './Headline';
8
+ import { ImageGallery } from './ImageGallery';
10
9
 
11
10
  export type LayoutSingleProps = {
12
11
  dynamicLayout: DynamicLayout;
@@ -35,7 +34,9 @@ export const LayoutSingle = ({
35
34
  const { height, ref } = useGetContentSize({
36
35
  heightDefault: 245,
37
36
  });
38
- const imageGalleryWidth = calculateWidthImageGallery(height, 1, 1);
37
+
38
+ // For single image, image width must be fixed 80px.
39
+ const imageGalleryWidth = 80;
39
40
 
40
41
  return (
41
42
  <Stack
@@ -9,13 +9,16 @@ type SocialProofCount = Record<string, Record<string, number>>;
9
9
 
10
10
  interface UseSocialProofCountParams {
11
11
  pageVariant?: PageVariant;
12
+ numberOfCustomersText?: string;
12
13
  countKey?: string;
13
14
  id?: string;
14
15
  }
16
+ const DEFAULT_CUSTOMER_TEXT = '{count} customers';
15
17
 
16
18
  export const useSocialProofCount = ({
17
19
  pageVariant,
18
20
  countKey,
21
+ numberOfCustomersText = DEFAULT_CUSTOMER_TEXT,
19
22
  id,
20
23
  }: UseSocialProofCountParams): string => {
21
24
  const [customerCount, setCustomerCount] = useState<number>(() =>
@@ -63,5 +66,9 @@ export const useSocialProofCount = ({
63
66
  }
64
67
  }, [pageVariant, countKey, id]);
65
68
 
66
- return `${customerCount} customers`;
69
+ // For safety ensure that the replacement string occurs in the string for now
70
+ const finalCustomerText = numberOfCustomersText.includes('{count}')
71
+ ? numberOfCustomersText
72
+ : DEFAULT_CUSTOMER_TEXT;
73
+ return finalCustomerText.replace('{count}', customerCount.toString());
67
74
  };
@@ -82,6 +82,12 @@ export type WidgetContentProps = {
82
82
  */
83
83
  customerQueryText?: string;
84
84
 
85
+ /**
86
+ * Text displaying how many customers asked the customerQueryText question (e.g., "123 customers")
87
+ * Utilizes replacement to replace the value {count} with the replaced value
88
+ */
89
+ numberOfCustomersText?: string;
90
+
85
91
  /**
86
92
  * Text label for the primary prompt button.
87
93
  * Displayed prominently in the headline section.
@@ -8,6 +8,7 @@ import { useGetSkeletonProperties } from './hooks/useGetSkeletonProperties';
8
8
  import { useTextFieldFocus } from './hooks/useTextFieldFocus';
9
9
  import { useTextFieldSubmit } from './hooks/useTextFieldSubmit';
10
10
  import { useTextFieldValue } from './hooks/useTextFieldValue';
11
+ import { usePlaceholderAnimation } from './hooks/usePlaceholderAnimation';
11
12
  import { useVoiceInput } from './hooks/useVoiceInput';
12
13
  import type { TextFieldProps } from './types';
13
14
 
@@ -30,10 +31,13 @@ export const TextField = ({
30
31
  style,
31
32
  ariaLabel,
32
33
  isLoading = false,
34
+ multiline = false,
33
35
  enableVoiceInput = false,
34
36
  onTranscriptionStarted,
35
37
  onTranscriptionCompleted,
36
38
  }: TextFieldProps): JSX.Element => {
39
+ const resolvedPlaceholder = usePlaceholderAnimation(placeholder);
40
+
37
41
  const { currentValue, hasValue, handleChange, resetValue } = useTextFieldValue(
38
42
  controlledValue,
39
43
  onChange,
@@ -48,7 +52,7 @@ export const TextField = ({
48
52
  resetValue,
49
53
  );
50
54
 
51
- const handleVoiceTranscript = useCallback(
55
+ const handleVoiceTranscriptCompleted = useCallback(
52
56
  (transcript: string) => {
53
57
  resetValue();
54
58
  handleChange(transcript);
@@ -57,6 +61,13 @@ export const TextField = ({
57
61
  [handleChange, resetValue, onTranscriptionCompleted],
58
62
  );
59
63
 
64
+ const handleVoiceTranscription = useCallback(
65
+ (transcript: string) => {
66
+ handleChange(transcript);
67
+ },
68
+ [handleChange],
69
+ );
70
+
60
71
  const {
61
72
  isListening,
62
73
  handleToggleListening,
@@ -64,7 +75,8 @@ export const TextField = ({
64
75
  browserSupportsSpeechRecognition,
65
76
  } = useVoiceInput({
66
77
  onTranscriptionStarted,
67
- onTranscriptionCompleted: handleVoiceTranscript,
78
+ onTranscriptionCompleted: handleVoiceTranscriptCompleted,
79
+ onTranscription: handleVoiceTranscription,
68
80
  disabled,
69
81
  });
70
82
 
@@ -80,7 +92,7 @@ export const TextField = ({
80
92
  const input = (
81
93
  <TextFieldComponents.Input
82
94
  theme={resolvedTheme}
83
- placeholder={isLoading ? '' : placeholder}
95
+ placeholder={isLoading ? '' : resolvedPlaceholder}
84
96
  value={isLoading ? '' : currentValue}
85
97
  onChange={handleChange}
86
98
  onKeyDown={handleKeyDown}
@@ -89,6 +101,7 @@ export const TextField = ({
89
101
  disabled={disabled || isLoading}
90
102
  ariaLabel={ariaLabel}
91
103
  className={inputClassName}
104
+ multiline={multiline}
92
105
  />
93
106
  );
94
107
 
@@ -130,6 +143,7 @@ export const TextField = ({
130
143
  isFocused={isFocused}
131
144
  hasValue={hasValue}
132
145
  disabled={disabled}
146
+ multiline={multiline}
133
147
  id={id}
134
148
  testId={testId}
135
149
  className={classNames(className, skeletonClass, disabled && 'envive-tw-cursor-not-allowed')}
@@ -57,83 +57,62 @@ vi.mock('jotai', () => ({
57
57
  }));
58
58
 
59
59
  describe('useVoiceInput', () => {
60
- // Reset mocks before each test to ensure clean state
61
60
  beforeEach(() => {
62
61
  vi.clearAllMocks();
63
- // Reset the implementation to default successful behavior
64
62
  spies.mockStartListening.mockImplementation(() => Promise.resolve());
63
+ sharedState = '';
65
64
  });
66
65
 
67
66
  describe('Basic initialization', () => {
68
67
  it('should initialize with isListening as false', () => {
69
68
  const { result } = renderHook(() => useVoiceInput({}));
70
- // Voice input should not be active when the hook first mounts
71
69
  expect(result.current.isListening).toBe(false);
72
70
  });
73
71
 
74
72
  it('should report browser supports speech recognition', () => {
75
73
  const { result } = renderHook(() => useVoiceInput({}));
76
- // Check if the browser has speech recognition support
77
74
  expect(result.current.browserSupportsSpeechRecognition).toBe(true);
78
75
  });
79
76
  });
80
77
 
81
- describe('Start listening', () => {
82
- it('should start listening when button is clicked', async () => {
83
- const { result } = renderHook(() => useVoiceInput({}));
84
-
85
- await act(async () => {
86
- // Call handleToggleListening to start listening
87
- result.current.handleToggleListening();
88
- });
78
+ describe('onTranscriptionStarted callback', () => {
79
+ it('should accept onTranscriptionStarted prop', () => {
80
+ const onTranscriptionStarted = vi.fn();
89
81
 
90
- // Verify that listening state changed to active
91
- expect(result.current.isListening).toBe(true);
82
+ renderHook(() =>
83
+ useVoiceInput({
84
+ onTranscriptionStarted,
85
+ }),
86
+ );
92
87
  });
88
+ });
93
89
 
94
- it('should delete previous transcription when starting to listen', async () => {
95
- const { result } = renderHook(() => useVoiceInput({}));
96
-
97
- await act(async () => {
98
- result.current.handleToggleListening();
99
- });
90
+ describe('onTranscription callback', () => {
91
+ it('should accept onTranscription prop', () => {
92
+ const onTranscription = vi.fn();
100
93
 
101
- // Verify resetTranscript was called BEFORE starting new listening session
102
- // This ensures previous transcription is cleared when starting fresh
103
- expect(spies.mockResetTranscript).toHaveBeenCalled();
94
+ renderHook(() =>
95
+ useVoiceInput({
96
+ onTranscription,
97
+ }),
98
+ );
104
99
  });
105
100
  });
106
101
 
107
- describe('Stop listening', () => {
108
- it('should stop listening and call onTranscript with current transcript', async () => {
102
+ describe('onTranscriptionCompleted callback', () => {
103
+ it('should accept onTranscriptionCompleted prop', () => {
109
104
  const onTranscriptionCompleted = vi.fn();
110
105
 
111
- const { result } = renderHook(() =>
106
+ renderHook(() =>
112
107
  useVoiceInput({
113
108
  onTranscriptionCompleted,
114
109
  }),
115
110
  );
116
-
117
- // First toggle: start listening
118
- await act(async () => {
119
- result.current.handleToggleListening();
120
- });
121
-
122
- // Second toggle: stop listening - this should call onTranscript callback
123
- await act(async () => {
124
- result.current.handleToggleListening();
125
- });
126
-
127
- // Verify that stopListening was called to stop speech recognition
128
- expect(spies.mockStopListening).toHaveBeenCalled();
129
- // Verify that the onTranscript callback was called with current transcript
130
- expect(onTranscriptionCompleted).toHaveBeenCalledWith('');
131
111
  });
132
112
  });
133
113
 
134
114
  describe('Mic permission handling', () => {
135
115
  it('should handle mic permission denied error', async () => {
136
- // Configure the mock to throw an error simulating permission denial
137
116
  spies.mockStartListening.mockImplementationOnce(() => {
138
117
  throw new Error('Permission denied');
139
118
  });
@@ -144,43 +123,28 @@ describe('useVoiceInput', () => {
144
123
  result.current.handleToggleListening();
145
124
  });
146
125
 
147
- // The hook should catch the error and set isListening back to false
148
126
  expect(result.current.isListening).toBe(false);
149
127
  });
150
128
 
151
129
  it('should not start listening when disabled', () => {
152
- // When disabled prop is true, the hook should not attempt to start listening
153
130
  const { result } = renderHook(() => useVoiceInput({ disabled: true }));
154
131
 
155
132
  result.current.handleToggleListening();
156
133
 
157
- // isListening should remain false
158
134
  expect(result.current.isListening).toBe(false);
159
- // Verify that startListening was never called
160
135
  expect(spies.mockStartListening).not.toHaveBeenCalled();
161
136
  });
162
137
  });
163
138
 
164
139
  describe('Abort listening', () => {
165
- it('should abort listening and reset state', async () => {
140
+ it('should abort listening when handleAbortListening is called', async () => {
166
141
  const { result } = renderHook(() => useVoiceInput({}));
167
142
 
168
- // Start listening first
169
- await act(async () => {
170
- result.current.handleToggleListening();
171
- });
172
-
173
- expect(result.current.isListening).toBe(true);
174
-
175
- // Call abort to stop listening immediately without collecting transcript
176
143
  await act(async () => {
177
144
  result.current.handleAbortListening();
178
145
  });
179
146
 
180
- // Verify abortListening was called to immediately stop speech recognition
181
147
  expect(spies.mockAbortListening).toHaveBeenCalled();
182
- // Verify state is reset to false
183
- expect(result.current.isListening).toBe(false);
184
148
  });
185
149
  });
186
150
  });
@@ -1,6 +1,7 @@
1
1
  import classNames from 'classnames';
2
2
  import { getInputClasses } from '../utils/getInputClasses';
3
3
  import { createInputChangeHandler } from '../utils/createInputChangeHandler';
4
+ import { useAutoResize } from '../hooks/useAutoResize';
4
5
  import { Theme } from '../../../../tokens/theme/theme';
5
6
 
6
7
  type InputProps = {
@@ -8,10 +9,11 @@ type InputProps = {
8
9
  placeholder: string;
9
10
  value?: string;
10
11
  onChange?: (value: string) => void;
11
- onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
12
- onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
13
- onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
12
+ onKeyDown?: (event: React.KeyboardEvent<HTMLElement>) => void;
13
+ onFocus?: (event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
14
+ onBlur?: (event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
14
15
  disabled?: boolean;
16
+ multiline?: boolean;
15
17
  id?: string;
16
18
  testId?: string;
17
19
  className?: string;
@@ -28,6 +30,7 @@ export const Input = ({
28
30
  onFocus,
29
31
  onBlur,
30
32
  disabled = false,
33
+ multiline = false,
31
34
  id,
32
35
  testId,
33
36
  className,
@@ -36,6 +39,42 @@ export const Input = ({
36
39
  }: InputProps): JSX.Element => {
37
40
  const { inputClasses } = getInputClasses(theme);
38
41
  const handleChange = createInputChangeHandler(disabled, onChange);
42
+ const textareaRef = useAutoResize(multiline ? value : undefined);
43
+
44
+ const sharedClasses = classNames(
45
+ inputClasses,
46
+ 'focus-visible:envive-tw-shadow-none focus-visible:envive-tw-outline-none',
47
+ className,
48
+ disabled && 'envive-tw-cursor-not-allowed',
49
+ );
50
+
51
+ if (multiline) {
52
+ return (
53
+ <textarea
54
+ ref={textareaRef}
55
+ id={id}
56
+ data-testid={testId}
57
+ placeholder={placeholder}
58
+ value={value}
59
+ rows={1}
60
+ onChange={e => {
61
+ if (!disabled) onChange?.(e.target.value);
62
+ }}
63
+ onKeyDown={e => {
64
+ if (e.key === 'Enter' && !e.shiftKey) {
65
+ onKeyDown?.(e);
66
+ }
67
+ }}
68
+ onFocus={onFocus}
69
+ onBlur={onBlur}
70
+ disabled={disabled}
71
+ className={sharedClasses}
72
+ style={{ resize: 'none', overflow: 'hidden', padding: 0, ...style }}
73
+ aria-label={ariaLabel}
74
+ aria-disabled={disabled}
75
+ />
76
+ );
77
+ }
39
78
 
40
79
  return (
41
80
  <input
@@ -49,12 +88,7 @@ export const Input = ({
49
88
  onFocus={onFocus}
50
89
  onBlur={onBlur}
51
90
  disabled={disabled}
52
- className={classNames(
53
- inputClasses,
54
- 'focus-visible:envive-tw-shadow-none focus-visible:envive-tw-outline-none',
55
- className,
56
- disabled && 'envive-tw-cursor-not-allowed',
57
- )}
91
+ className={sharedClasses}
58
92
  style={style}
59
93
  aria-label={ariaLabel}
60
94
  aria-disabled={disabled}
@@ -11,6 +11,7 @@ type LayoutProps = {
11
11
  isFocused: boolean;
12
12
  hasValue: boolean;
13
13
  disabled?: boolean;
14
+ multiline?: boolean;
14
15
  id?: string;
15
16
  testId?: string;
16
17
  className?: string;
@@ -25,6 +26,7 @@ export const Layout = ({
25
26
  isFocused,
26
27
  hasValue,
27
28
  disabled = false,
29
+ multiline = false,
28
30
  id,
29
31
  testId,
30
32
  className,
@@ -61,7 +63,11 @@ export const Layout = ({
61
63
  disabled && containerDisabledClasses,
62
64
  className,
63
65
  )}
64
- style={style}
66
+ style={
67
+ multiline
68
+ ? { height: 'auto', minHeight: '40px', alignItems: 'flex-end', ...style }
69
+ : style
70
+ }
65
71
  >
66
72
  {input}
67
73
  {sendIcon}
@@ -0,0 +1,14 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ export const useAutoResize = (value: string | undefined): React.RefObject<HTMLTextAreaElement> => {
4
+ const ref = useRef<HTMLTextAreaElement>(null);
5
+
6
+ useEffect(() => {
7
+ const el = ref.current;
8
+ if (!el) return;
9
+ el.style.height = 'auto';
10
+ el.style.height = `${el.scrollHeight}px`;
11
+ }, [value]);
12
+
13
+ return ref;
14
+ };
@@ -0,0 +1,67 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ import { useEffect, useRef, useState } from 'react';
3
+
4
+ const DEFAULT_TYPING_DURATION = 400;
5
+ const DEFAULT_TRANSITION = 3000;
6
+ const CLEAR_PAUSE = 200;
7
+
8
+ export const usePlaceholderAnimation = (
9
+ placeholder: string | string[],
10
+ typingDuration = DEFAULT_TYPING_DURATION,
11
+ transition = DEFAULT_TRANSITION,
12
+ ): string => {
13
+ const [current, setCurrent] = useState(() => (Array.isArray(placeholder) ? '' : placeholder));
14
+ const timeoutsRef = useRef<NodeJS.Timeout[]>([]);
15
+ const isMountedRef = useRef(true);
16
+
17
+ useEffect(() => {
18
+ if (!Array.isArray(placeholder)) {
19
+ setCurrent(placeholder);
20
+ return () => {};
21
+ }
22
+
23
+ if (placeholder.length === 0) {
24
+ setCurrent('');
25
+ return () => {};
26
+ }
27
+
28
+ isMountedRef.current = true;
29
+ timeoutsRef.current = [];
30
+
31
+ const delay = (ms: number): Promise<void> =>
32
+ new Promise(resolve => {
33
+ const id = setTimeout(resolve, ms);
34
+ timeoutsRef.current.push(id);
35
+ });
36
+
37
+ const animateChars = async (text: string) => {
38
+ const charDelay = text.length > 1 ? typingDuration / (text.length - 1) : 0;
39
+ for (let i = 0; i < text.length; i += 1) {
40
+ await delay(charDelay);
41
+ if (isMountedRef.current) setCurrent(text.substring(0, i + 1));
42
+ }
43
+ };
44
+
45
+ const run = async () => {
46
+ while (isMountedRef.current) {
47
+ for (const element of placeholder) {
48
+ if (!isMountedRef.current) return;
49
+ await animateChars(element);
50
+ await delay(transition);
51
+ if (isMountedRef.current) setCurrent('');
52
+ await delay(CLEAR_PAUSE);
53
+ }
54
+ }
55
+ };
56
+
57
+ run();
58
+
59
+ return () => {
60
+ isMountedRef.current = false;
61
+ timeoutsRef.current.forEach(clearTimeout);
62
+ timeoutsRef.current = [];
63
+ };
64
+ }, [placeholder, typingDuration, transition]);
65
+
66
+ return current;
67
+ };
@@ -20,7 +20,7 @@ export const useTextFieldSubmit = (
20
20
  }, [onSubmit, hasValue, disabled, isLoading, currentValue, resetValue]);
21
21
 
22
22
  const handleKeyDown = useCallback(
23
- (event: React.KeyboardEvent<HTMLInputElement>) => {
23
+ (event: React.KeyboardEvent<HTMLElement>) => {
24
24
  if (event.key === 'Enter' && hasValue && !disabled && !isLoading) {
25
25
  event.preventDefault();
26
26
  handleSubmit();
@@ -6,6 +6,7 @@ import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognitio
6
6
  export type UseVoiceInputProps = {
7
7
  onTranscriptionStarted?: () => void;
8
8
  onTranscriptionCompleted?: (transcript: string) => void;
9
+ onTranscription?: (transcript: string) => void;
9
10
  disabled?: boolean;
10
11
  };
11
12
 
@@ -19,6 +20,7 @@ export type UseVoiceInputReturn = {
19
20
  export const useVoiceInput = ({
20
21
  onTranscriptionCompleted,
21
22
  onTranscriptionStarted,
23
+ onTranscription,
22
24
  disabled = false,
23
25
  }: UseVoiceInputProps): UseVoiceInputReturn => {
24
26
  const [listeningToSpeech, setListeningToSpeech] = useAtom(listeningToSpeechAtom);
@@ -78,6 +80,12 @@ export const useVoiceInput = ({
78
80
  }
79
81
  }, [listeningToSpeech]);
80
82
 
83
+ useEffect(() => {
84
+ if (!disabled && listeningToSpeech === 'start' && transcript) {
85
+ onTranscription?.(transcript);
86
+ }
87
+ }, [transcript, listeningToSpeech, disabled, onTranscription]);
88
+
81
89
  return {
82
90
  isListening: listeningToSpeech === 'start',
83
91
  handleToggleListening,
@@ -8,8 +8,9 @@ export type TextFieldProps = {
8
8
  theme?: Theme;
9
9
  /**
10
10
  * Placeholder text displayed when the input is empty.
11
+ * Pass an array of strings to cycle through them with a typing animation.
11
12
  */
12
- placeholder: string;
13
+ placeholder: string | string[];
13
14
  /**
14
15
  * Controlled value of the input. If not provided, the component manages its own state.
15
16
  */
@@ -47,11 +48,28 @@ export type TextFieldProps = {
47
48
  * @default false
48
49
  */
49
50
  isLoading?: boolean;
51
+ /**
52
+ * When true the input expands to multiple lines instead of scrolling horizontally.
53
+ * @default false
54
+ */
55
+ multiline?: boolean;
50
56
  /**
51
57
  * Enable voice input button
52
58
  * @default false
53
59
  */
54
60
  enableVoiceInput?: boolean;
61
+ /**
62
+ * Callback function invoked when voice transcription begins.
63
+ */
55
64
  onTranscriptionStarted?: () => void;
65
+ /**
66
+ * Callback function invoked when voice transcription completes.
67
+ * @param transcript - The transcribed text from voice input.
68
+ */
56
69
  onTranscriptionCompleted?: (transcript: string) => void;
70
+ /**
71
+ * Callback function invoked during voice transcription as audio is processed.
72
+ * @param transcript - The current transcribed text from voice input.
73
+ */
74
+ onTranscription?: (transcript: string) => void;
57
75
  };