@envive-ai/react-widgets 0.1.1 → 0.1.2-arthur-2

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 (42) hide show
  1. package/dist/SearchResults/index-DCTxvwmv.d.cts +6 -0
  2. package/dist/{index.cjs → SearchResults/index.cjs} +2 -24
  3. package/dist/SearchZeroState/index-DSFtalZR.d.ts +27 -0
  4. package/dist/SearchZeroState/index-bEcxYOSF.d.cts +27 -0
  5. package/dist/SearchZeroState/index.cjs +3049 -0
  6. package/dist/SearchZeroState/index.js +3045 -0
  7. package/dist/SuggestionBar/index-DZU9kbWS.d.cts +39 -0
  8. package/dist/SuggestionBar/index-DyXd4-b7.d.ts +39 -0
  9. package/dist/SuggestionBar/index.cjs +5 -0
  10. package/dist/SuggestionBar/index.js +4 -0
  11. package/dist/SuggestionBar-BOThXJvJ.cjs +453 -0
  12. package/dist/SuggestionBar-DeMmAK4M.js +131 -0
  13. package/dist/SuggestionButtonContainer/index-B_X537jw.d.cts +20 -0
  14. package/dist/SuggestionButtonContainer/index-vwelzDzM.d.ts +20 -0
  15. package/dist/SuggestionButtonContainer/index.cjs +3 -0
  16. package/dist/SuggestionButtonContainer/index.js +3 -0
  17. package/dist/SuggestionButtonContainer-BeWPpeQk.cjs +173 -0
  18. package/dist/SuggestionButtonContainer-CZhOkZaJ.js +167 -0
  19. package/dist/chunk-DWy1uDak.cjs +39 -0
  20. package/package.json +18 -6
  21. package/src/SearchZeroState/SearchIcon.tsx +57 -0
  22. package/src/SearchZeroState/SearchOverlay.tsx +81 -0
  23. package/src/SearchZeroState/SearchZeroState.tsx +264 -0
  24. package/src/SearchZeroState/SearchZeroStateWidget.tsx +33 -0
  25. package/src/SearchZeroState/components/RecommendedProducts.tsx +118 -0
  26. package/src/SearchZeroState/index.ts +8 -0
  27. package/src/SearchZeroState/overlay/overlayHostLocator.ts +19 -0
  28. package/src/SearchZeroState/types.ts +9 -0
  29. package/src/SearchZeroState/zeroStateSearchVariants.ts +24 -0
  30. package/src/SuggestionBar/SuggestionBar.tsx +139 -0
  31. package/src/SuggestionBar/index.ts +2 -0
  32. package/src/SuggestionBar/types.ts +4 -0
  33. package/src/SuggestionButtonContainer/SuggestionButtonContainer.tsx +141 -0
  34. package/src/SuggestionButtonContainer/index.ts +2 -0
  35. package/src/SuggestionButtonContainer/types.ts +16 -0
  36. package/src/stories/SearchZeroState.stories.tsx +44 -0
  37. package/src/stories/SuggestionBar.stories.tsx +46 -0
  38. package/src/util/useHorizontalScrollAnimation.ts +121 -0
  39. package/src/util/useReducedMotionWithOverride.ts +24 -0
  40. package/dist/index-VWNd4lyI.d.cts +0 -6
  41. /package/dist/{index-BPfKr14f.d.ts → SearchResults/index-CYPV3XE0.d.ts} +0 -0
  42. /package/dist/{index.js → SearchResults/index.js} +0 -0
@@ -0,0 +1,141 @@
1
+ import { useRef } from 'react';
2
+ import { SuggestionButton } from '@envive-ai/react-toolkit/SuggestionButton';
3
+ import { SpiffyWidgets } from '@envive-ai/react-hooks/application/models';
4
+ import { SUGGESTION_BAR_BUTTON_TESTID } from '@envive-ai/react-hooks/config';
5
+ import { useIsSmallScreen } from '@envive-ai/react-hooks/hooks/IsSmallScreen';
6
+ import { useTrackComponentVisibleEvent } from '@envive-ai/react-hooks/hooks/TrackComponentVisibleEvent';
7
+ import { useHorizontalScrollAnimation } from 'src/util/useHorizontalScrollAnimation';
8
+ import { SuggestionButtonContainerProps } from './types';
9
+
10
+ // ButtonContainer props
11
+ interface ButtonContainerProps {
12
+ children: React.ReactNode;
13
+ }
14
+
15
+ // ButtonContainer is reused twice within SuggestionBarV2, so we declare it as a seperate functional component
16
+ function ButtonContainer({ children }: ButtonContainerProps) {
17
+ return (
18
+ <div
19
+ className="spiffy-tw-flex
20
+ spiffy-tw-flex-row
21
+ spiffy-tw-items-center
22
+ spiffy-tw-space-x-2
23
+ spiffy-tw-h-full
24
+ spiffy-tw-pl-0"
25
+ >
26
+ {children}
27
+ </div>
28
+ );
29
+ }
30
+
31
+
32
+ const SuggestionButtonContainer: React.FC<SuggestionButtonContainerProps> = ({
33
+ buttonVariation,
34
+ hoverButtonVariation,
35
+ buttonTexts,
36
+ boldFirstButton = false,
37
+ twoRowsOnMobile = false,
38
+ animationSpeed = 'none',
39
+ buttonBorderRadius = 'lg',
40
+ scrollContainerRef,
41
+ onButtonClick,
42
+ }: Readonly<SuggestionButtonContainerProps>) => {
43
+ // Refs
44
+ const componentVisibleTriggerRef = useRef<HTMLDivElement>(null);
45
+
46
+ // Hooks
47
+ useHorizontalScrollAnimation({
48
+ scrollContainerRef,
49
+ animationSpeed,
50
+ })
51
+ const isSmallScreen = useIsSmallScreen();
52
+
53
+ const isAnimated = animationSpeed !== 'none';
54
+
55
+ // Track component visibility
56
+ useTrackComponentVisibleEvent(
57
+ SpiffyWidgets.SuggestionBar,
58
+ componentVisibleTriggerRef,
59
+ { animated: isAnimated },
60
+ );
61
+
62
+ const visibleButtonsFirstRow = buttonTexts.slice(
63
+ 0,
64
+ twoRowsOnMobile && isSmallScreen ? Math.ceil((buttonTexts.length + 1) / 2) : undefined,
65
+ );
66
+
67
+ const visibleButtonsSecondRow = buttonTexts.slice(
68
+ Math.ceil((buttonTexts.length + 1) / 2),
69
+ buttonTexts.length,
70
+ );
71
+
72
+ return (
73
+ <div className="spiffy-tw-overflow-x-scroll spiffy-tw-no-scrollbar spiffy-tw-w-full spiffy-tw-whitespace-nowrap">
74
+ <ButtonContainer>
75
+ {visibleButtonsFirstRow.map((suggestion, i) => (
76
+ <SuggestionButton
77
+ key={i}
78
+ variant={buttonVariation}
79
+ hoverVariant={hoverButtonVariation}
80
+ isDisabled={false}
81
+ content={suggestion}
82
+ boldText={boldFirstButton && i === 0}
83
+ borderRadius={buttonBorderRadius}
84
+ onClick={() => onButtonClick(suggestion)}
85
+ dataTestId={SUGGESTION_BAR_BUTTON_TESTID}
86
+ />
87
+ ))}
88
+
89
+ {isAnimated &&
90
+ buttonTexts.map((suggestion, i) => (
91
+ <SuggestionButton
92
+ key={`animation-dupe-${i}`}
93
+ variant={buttonVariation}
94
+ hoverVariant={hoverButtonVariation}
95
+ isDisabled={false}
96
+ content={suggestion}
97
+ boldText={boldFirstButton && i === 0}
98
+ borderRadius={buttonBorderRadius}
99
+ onClick={() => onButtonClick(suggestion)}
100
+ dataTestId={SUGGESTION_BAR_BUTTON_TESTID}
101
+ />
102
+ ))}
103
+ </ButtonContainer>
104
+ {twoRowsOnMobile && isSmallScreen && (
105
+ <div className="spiffy-tw-mt-1.5">
106
+ <ButtonContainer>
107
+ {visibleButtonsSecondRow.map((suggestion, i) => (
108
+ <SuggestionButton
109
+ key={i}
110
+ variant={buttonVariation}
111
+ hoverVariant={hoverButtonVariation}
112
+ isDisabled={false}
113
+ content={suggestion}
114
+ boldText={boldFirstButton && i === 0}
115
+ borderRadius={buttonBorderRadius}
116
+ onClick={() => onButtonClick(suggestion)}
117
+ dataTestId={SUGGESTION_BAR_BUTTON_TESTID}
118
+ />
119
+ ))}
120
+ {isAnimated &&
121
+ visibleButtonsSecondRow.map((suggestion, i) => (
122
+ <SuggestionButton
123
+ key={`animation-dupe-${i}`}
124
+ variant={buttonVariation}
125
+ hoverVariant={hoverButtonVariation}
126
+ isDisabled={false}
127
+ content={suggestion}
128
+ boldText={boldFirstButton && i === 0}
129
+ borderRadius={buttonBorderRadius}
130
+ onClick={() => onButtonClick(suggestion)}
131
+ dataTestId={SUGGESTION_BAR_BUTTON_TESTID}
132
+ />
133
+ ))}
134
+ </ButtonContainer>
135
+ </div>
136
+ )}
137
+ </div>
138
+ );
139
+ }
140
+
141
+ export { SuggestionButtonContainer };
@@ -0,0 +1,2 @@
1
+ export * from './SuggestionButtonContainer';
2
+ export * from './types';
@@ -0,0 +1,16 @@
1
+ import { SuggestionButtonVariant } from "@envive-ai/react-hooks/contexts/types";
2
+ import { TestProps } from "@envive-ai/react-hooks/types";
3
+
4
+
5
+ // SuggestionButtonContainer Props
6
+ export interface SuggestionButtonContainerProps extends TestProps {
7
+ buttonVariation: SuggestionButtonVariant;
8
+ hoverButtonVariation: SuggestionButtonVariant;
9
+ buttonTexts: string[];
10
+ onButtonClick: (text: string) => void;
11
+ scrollContainerRef: React.RefObject<HTMLDivElement>;
12
+ boldFirstButton?: boolean | undefined;
13
+ twoRowsOnMobile?: boolean | undefined;
14
+ animationSpeed?: 'standard' | 'slow' | 'none';
15
+ buttonBorderRadius?: 'sm' | 'md' | 'lg';
16
+ }
@@ -0,0 +1,44 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+
3
+ import { SearchZeroState } from 'src/SearchZeroState';
4
+ import { SearchEntryPointWidgetConfig, WidgetType } from '@envive-ai/react-hooks/contexts/types';
5
+
6
+ const meta = {
7
+ title: 'Widgets/Search/SearchZeroState',
8
+ // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ layout: 'fullscreen',
12
+ },
13
+ args: {
14
+ searchInputVariant: 'standard',
15
+ searchBoxPlaceholder: '',
16
+ widgetConfigId: '',
17
+ type: WidgetType.SearchZeroStateEntryPoint,
18
+ searchZeroStateVariant: 'backgroundTertiary',
19
+ suggestionButtonConfig: {
20
+ variant: 'primary',
21
+ hoverVariant: 'primary',
22
+ borderRadius: 'sm',
23
+ },
24
+ layout: 'input',
25
+ initialIsOpen: false,
26
+ },
27
+ argTypes: {
28
+ initialIsOpen: {
29
+ control: 'boolean',
30
+ },
31
+ searchZeroStateVariant: {
32
+ control: 'select',
33
+ options: ['backgroundTertiary', 'backgroundPrimary', 'backgroundSecondary', 'backgroundQuaternary'],
34
+ },
35
+ },
36
+ render: (args: SearchEntryPointWidgetConfig & { initialIsOpen: boolean }) => (
37
+ <SearchZeroState widgetConfig={{ ...args }} initialIsOpen={args.initialIsOpen} />
38
+ ),
39
+ } satisfies Meta;
40
+
41
+ export default meta;
42
+ type Story = StoryObj<typeof meta>;
43
+
44
+ export const Default: Story = {};
@@ -0,0 +1,46 @@
1
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
2
+ import { SuggestionBar, SuggestionBarLocationForMetrics } from 'src/SuggestionBar';
3
+
4
+ const meta: Meta<typeof SuggestionBar> = {
5
+ title: 'Common/SuggestionBar',
6
+ component: SuggestionBar,
7
+ parameters: {
8
+ layout: 'centered',
9
+ },
10
+ tags: ['autodocs'],
11
+ argTypes: {
12
+ animationSpeed: {
13
+ control: 'select',
14
+ options: ['none', 'standard', 'slow'],
15
+ },
16
+ },
17
+ render: (args) => (
18
+ <div style={{ width: '300px', height: '100px' }}>
19
+ <SuggestionBar {...args} />
20
+ </div>
21
+ ),
22
+ };
23
+
24
+ export default meta;
25
+ type Story = StoryObj<typeof SuggestionBar>;
26
+
27
+ export const Default: Story = {
28
+ args: {
29
+ id: 'suggestion-bar',
30
+ locationForMetrics: SuggestionBarLocationForMetrics.SUGGESTION_BAR_TOP,
31
+ buttonTexts: ['Button 1', 'Button 2', 'Button 3'],
32
+ buttonVariation: 'primary',
33
+ hoverButtonVariation: 'primary',
34
+ animationSpeed: 'none',
35
+ handleReply: () => {},
36
+ boldFirstButton: false,
37
+ },
38
+
39
+ };
40
+
41
+ export const Animated: Story = {
42
+ args: {
43
+ ...Default.args,
44
+ animationSpeed: 'standard',
45
+ },
46
+ };
@@ -0,0 +1,121 @@
1
+ import { useEffect, RefObject, useRef } from 'react';
2
+ import { useReducedMotionWithOverride } from 'src/util/useReducedMotionWithOverride';
3
+
4
+ // IMPORTANT: All refs passed to this hook must be mutable (not Readonly)
5
+ interface UseHorizontalScrollAnimationOptions {
6
+ scrollContainerRef: RefObject<HTMLDivElement>;
7
+ animationSpeed?: 'standard' | 'slow' | 'none';
8
+ }
9
+
10
+ // When using this hook, ensure that scrouuContainerRef is not undefined. It is allowed to
11
+ // prevent issued elsewhere in the codebase, but it should be dealt with whenever you use this hook.
12
+ export function useHorizontalScrollAnimation({
13
+ scrollContainerRef,
14
+ animationSpeed = 'standard',
15
+ }: UseHorizontalScrollAnimationOptions): void {
16
+ const reducedMotion = useReducedMotionWithOverride();
17
+ const resumeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
18
+ const scrollAnimationRef = useRef<number | null>(null);
19
+
20
+ const pauseOnHover = true; // This is unlikely to need to be configurable but I'm throwing it up here just in case.
21
+
22
+ // Animation constants: time-based
23
+ let PIXELS_PER_SECOND = 40;
24
+ switch (animationSpeed) {
25
+ case 'standard':
26
+ PIXELS_PER_SECOND = 40;
27
+ break;
28
+ case 'slow':
29
+ PIXELS_PER_SECOND = 25;
30
+ break;
31
+ case 'none':
32
+ PIXELS_PER_SECOND = 0;
33
+ break;
34
+ default:
35
+ PIXELS_PER_SECOND = 40;
36
+ }
37
+ const RESUME_DELAY_MS = 2000;
38
+ const isAnimated = animationSpeed !== 'none';
39
+
40
+ useEffect(() => {
41
+ if (!isAnimated || reducedMotion || !scrollContainerRef) {
42
+ return () => {};
43
+ }
44
+
45
+ const container = scrollContainerRef.current;
46
+ if (!container) {
47
+ return () => {};
48
+ }
49
+ if (container.scrollWidth <= container.clientWidth) {
50
+ return () => {};
51
+ }
52
+
53
+ let isPaused = false;
54
+ let lastTimestamp: number | null = null;
55
+ let accumulatedScroll = 0; // Accumulate fractional pixels
56
+
57
+ const step = (timestamp: number) => {
58
+ if (lastTimestamp === null) lastTimestamp = timestamp;
59
+ if (!isPaused) {
60
+ const delta = timestamp - lastTimestamp;
61
+ lastTimestamp = timestamp;
62
+
63
+ // Accumulate the scroll amount (including fractional pixels)
64
+ accumulatedScroll += PIXELS_PER_SECOND * (delta / 1000);
65
+
66
+ // Only apply whole pixel movements
67
+ const pixelsToScroll = Math.floor(accumulatedScroll);
68
+ if (pixelsToScroll > 0) {
69
+ container.scrollLeft += pixelsToScroll;
70
+ accumulatedScroll -= pixelsToScroll; // Keep the fractional remainder
71
+
72
+ if (Math.ceil(container.scrollLeft) >= container.scrollWidth - container.clientWidth) {
73
+ container.scrollLeft = 0; // Reset scroll to create a looping effect
74
+ accumulatedScroll = 0; // Reset accumulated scroll when looping
75
+ }
76
+ }
77
+ }
78
+ scrollAnimationRef.current = requestAnimationFrame(step);
79
+ };
80
+
81
+ // Start animation
82
+ scrollAnimationRef.current = requestAnimationFrame(step);
83
+
84
+ // Pause/resume logic
85
+ const pauseAnimation = () => {
86
+ isPaused = true;
87
+ if (resumeTimeoutRef.current) {
88
+ clearTimeout(resumeTimeoutRef.current);
89
+ resumeTimeoutRef.current = null;
90
+ }
91
+ };
92
+ const scheduleResumeAnimation = () => {
93
+ resumeTimeoutRef.current = setTimeout(() => {
94
+ isPaused = false;
95
+ lastTimestamp = null; // Reset timestamp so delta is correct after pause
96
+ }, RESUME_DELAY_MS);
97
+ };
98
+
99
+ if (pauseOnHover) {
100
+ container.addEventListener('mouseenter', pauseAnimation);
101
+ container.addEventListener('mouseleave', scheduleResumeAnimation);
102
+ container.addEventListener('touchstart', pauseAnimation);
103
+ container.addEventListener('touchend', scheduleResumeAnimation);
104
+ }
105
+
106
+ return function cleanup() {
107
+ if (scrollAnimationRef.current) {
108
+ cancelAnimationFrame(scrollAnimationRef.current);
109
+ }
110
+ if (pauseOnHover) {
111
+ container.removeEventListener('mouseenter', pauseAnimation);
112
+ container.removeEventListener('mouseleave', scheduleResumeAnimation);
113
+ container.removeEventListener('touchstart', pauseAnimation);
114
+ container.removeEventListener('touchend', scheduleResumeAnimation);
115
+ }
116
+ if (resumeTimeoutRef.current) {
117
+ clearTimeout(resumeTimeoutRef.current);
118
+ }
119
+ };
120
+ }, [isAnimated, reducedMotion, PIXELS_PER_SECOND, pauseOnHover, scrollContainerRef]);
121
+ }
@@ -0,0 +1,24 @@
1
+ import { useReducedMotionConfig } from 'framer-motion';
2
+ import { useMemo } from 'react';
3
+
4
+ // TODO: Should this be moved?
5
+ declare global {
6
+ interface Window {
7
+ _spiffy?: {
8
+ reducedMotionOverride?: boolean;
9
+ };
10
+ }
11
+ }
12
+
13
+ export const useReducedMotionWithOverride = () => {
14
+ const reducedMotionConfig = useReducedMotionConfig();
15
+ const reducedMotion = useMemo(() => {
16
+ if (window?._spiffy?.reducedMotionOverride) {
17
+ return window?._spiffy?.reducedMotionOverride;
18
+ }
19
+
20
+ return reducedMotionConfig;
21
+ }, [reducedMotionConfig]);
22
+
23
+ return reducedMotion;
24
+ };
@@ -1,6 +0,0 @@
1
- import * as react_jsx_runtime0 from "react/jsx-runtime";
2
-
3
- //#region src/SearchResults/SearchResultsWidget.d.ts
4
- declare const SearchResultsWidget: () => react_jsx_runtime0.JSX.Element;
5
- //#endregion
6
- export { SearchResultsWidget };
File without changes