@envive-ai/react-toolkit 0.1.10 → 0.1.11

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 (159) hide show
  1. package/dist/Accordion/index.cjs +3 -82
  2. package/dist/Accordion/index.d.cts +2 -2
  3. package/dist/Accordion/index.d.ts +2 -2
  4. package/dist/Accordion/index.js +2 -75
  5. package/dist/Accordion-Cp3Hx2zm.js +77 -0
  6. package/dist/Accordion-DBZpiQe7.cjs +88 -0
  7. package/dist/AppliedFiltersScrollbar/index.d.cts +2 -2
  8. package/dist/AppliedFiltersScrollbar/index.d.ts +2 -2
  9. package/dist/ButtonBase/index.cjs +2 -2
  10. package/dist/ButtonBase/index.js +2 -2
  11. package/dist/DynamicFiltersScrollbar/index.d.cts +2 -2
  12. package/dist/DynamicFiltersScrollbar/index.d.ts +2 -2
  13. package/dist/FilterScrollbar/index.cjs +1 -1
  14. package/dist/FilterScrollbar/index.d.cts +3 -3
  15. package/dist/FilterScrollbar/index.d.ts +3 -3
  16. package/dist/FilterScrollbar/index.js +1 -1
  17. package/dist/Headline/index.cjs +1 -1
  18. package/dist/Headline/index.d.cts +2 -2
  19. package/dist/Headline/index.d.ts +2 -2
  20. package/dist/Headline/index.js +1 -1
  21. package/dist/ImageWithFallback/index.d.cts +2 -2
  22. package/dist/ImageWithFallback/index.d.ts +2 -2
  23. package/dist/ModalSheet/index.cjs +3 -0
  24. package/dist/ModalSheet/index.d.cts +38 -0
  25. package/dist/ModalSheet/index.d.ts +38 -0
  26. package/dist/ModalSheet/index.js +3 -0
  27. package/dist/ModalSheet-CXZgyZ4a.cjs +335 -0
  28. package/dist/ModalSheet-CZd5pssv.js +324 -0
  29. package/dist/ProductCard/index.cjs +3 -3
  30. package/dist/ProductCard/index.d.cts +1 -1
  31. package/dist/ProductCard/index.d.ts +1 -1
  32. package/dist/ProductCard/index.js +3 -3
  33. package/dist/{ProductCard-Dkkhl9pk.js → ProductCard-D-lyh8uV.js} +3 -3
  34. package/dist/{ProductCard-CymUvJnC.cjs → ProductCard-F49krjHk.cjs} +4 -4
  35. package/dist/ProductGrid/index.cjs +4 -4
  36. package/dist/ProductGrid/index.d.cts +3 -3
  37. package/dist/ProductGrid/index.d.ts +3 -3
  38. package/dist/ProductGrid/index.js +4 -4
  39. package/dist/{ProductGrid-B3Ypqh7j.js → ProductGrid-FIf5wFQx.js} +1 -1
  40. package/dist/{ProductGrid-ZNRujkuN.cjs → ProductGrid-QeoaIfcq.cjs} +1 -1
  41. package/dist/RadioButton/index.cjs +6 -0
  42. package/dist/RadioButton/index.d.cts +30 -0
  43. package/dist/RadioButton/index.d.ts +30 -0
  44. package/dist/RadioButton/index.js +6 -0
  45. package/dist/RadioButton-Bf68dZl7.js +68 -0
  46. package/dist/RadioButton-DZ6QXkrN.cjs +77 -0
  47. package/dist/RadioButtonGroup/index.cjs +8 -0
  48. package/dist/RadioButtonGroup/index.d.cts +36 -0
  49. package/dist/RadioButtonGroup/index.d.ts +36 -0
  50. package/dist/RadioButtonGroup/index.js +7 -0
  51. package/dist/RadioButtonGroup-8k7hkJYB.js +37 -0
  52. package/dist/RadioButtonGroup-Dc_n5amh.cjs +51 -0
  53. package/dist/SearchAutocomplete/index.cjs +1 -1
  54. package/dist/SearchAutocomplete/index.js +1 -1
  55. package/dist/SearchFilter/index.cjs +9 -2
  56. package/dist/SearchFilter/index.d.cts +15 -9
  57. package/dist/SearchFilter/index.d.ts +11 -5
  58. package/dist/SearchFilter/index.js +9 -2
  59. package/dist/SearchFilter-BOwErEyI.js +258 -0
  60. package/dist/SearchFilter-BtLKnFMm.cjs +279 -0
  61. package/dist/SearchInput/index.cjs +3 -3
  62. package/dist/SearchInput/index.d.cts +1 -1
  63. package/dist/SearchInput/index.d.ts +2 -2
  64. package/dist/SearchInput/index.js +3 -3
  65. package/dist/{SearchInput-BT1rrc4F.js → SearchInput-BBaYEwkR.js} +2 -2
  66. package/dist/{SearchInput-CdR_nsR3.cjs → SearchInput-BTNvgrIa.cjs} +2 -2
  67. package/dist/SearchInputForm/index.cjs +4 -4
  68. package/dist/SearchInputForm/index.d.cts +1 -1
  69. package/dist/SearchInputForm/index.d.ts +1 -1
  70. package/dist/SearchInputForm/index.js +4 -4
  71. package/dist/SearchResultsContent/index.cjs +9 -9
  72. package/dist/SearchResultsContent/index.d.cts +4 -4
  73. package/dist/SearchResultsContent/index.d.ts +4 -4
  74. package/dist/SearchResultsContent/index.js +9 -9
  75. package/dist/SearchResultsFilterSidebar/index.cjs +15 -8
  76. package/dist/SearchResultsFilterSidebar/index.d.cts +1 -1
  77. package/dist/SearchResultsFilterSidebar/index.d.ts +1 -1
  78. package/dist/SearchResultsFilterSidebar/index.js +13 -6
  79. package/dist/SearchResultsStates/index.cjs +9 -9
  80. package/dist/SearchResultsStates/index.d.cts +6 -6
  81. package/dist/SearchResultsStates/index.d.ts +6 -6
  82. package/dist/SearchResultsStates/index.js +9 -9
  83. package/dist/{SearchResultsStates-BxJphoL-.js → SearchResultsStates-CB5k1xkK.js} +5 -5
  84. package/dist/{SearchResultsStates-ESlrrf20.cjs → SearchResultsStates-DywK7vvp.cjs} +5 -5
  85. package/dist/SparkleAnimation/index.cjs +1 -1
  86. package/dist/SparkleAnimation/index.d.cts +2 -2
  87. package/dist/SparkleAnimation/index.d.ts +2 -2
  88. package/dist/SparkleAnimation/index.js +1 -1
  89. package/dist/Spinner/index.cjs +1 -1
  90. package/dist/Spinner/index.d.cts +2 -2
  91. package/dist/Spinner/index.d.ts +2 -2
  92. package/dist/Spinner/index.js +1 -1
  93. package/dist/SuggestionButton/index.cjs +1 -1
  94. package/dist/SuggestionButton/index.d.cts +2 -2
  95. package/dist/SuggestionButton/index.d.ts +2 -2
  96. package/dist/SuggestionButton/index.js +1 -1
  97. package/dist/Text/index.d.cts +2 -2
  98. package/dist/Text/index.d.ts +3 -3
  99. package/dist/TextInput/index.cjs +1 -1
  100. package/dist/TextInput/index.d.ts +1 -1
  101. package/dist/TextInput/index.js +1 -1
  102. package/dist/ToggleButton/index.cjs +6 -0
  103. package/dist/ToggleButton/index.d.cts +30 -0
  104. package/dist/ToggleButton/index.d.ts +30 -0
  105. package/dist/ToggleButton/index.js +6 -0
  106. package/dist/ToggleButton-BKRR_-69.js +60 -0
  107. package/dist/ToggleButton-D90UO4qv.cjs +68 -0
  108. package/dist/{index-b4yAASuy.d.cts → index-1x_hMlEf.d.cts} +4 -4
  109. package/dist/{index-DV2K-9lE.d.cts → index-CgjZdKpL.d.cts} +5 -5
  110. package/dist/{index-agU5rfIs.d.ts → index-Cl4d_pDw.d.ts} +5 -5
  111. package/dist/{index-uU3sIiRx.d.ts → index-H_9LhS_1.d.cts} +2 -2
  112. package/dist/{index-DggZTKvc.d.ts → index-QMTPxKA9.d.ts} +4 -4
  113. package/dist/{index-Bpjv4lP2.d.cts → index-gfYBM7Ul.d.ts} +2 -2
  114. package/dist/{searchFilterSidebarVariants-BnIcOVWR.js → searchFilterSidebarVariants-CDFCHVeZ.js} +1 -1
  115. package/dist/{searchFilterSidebarVariants-DVZ7wRlb.cjs → searchFilterSidebarVariants-CtmuwSBQ.cjs} +1 -1
  116. package/package.json +20 -3
  117. package/src/components/AnimatedChevron/AnimatedChevron.tsx +37 -0
  118. package/src/components/ModalSheet/ModalSheet.tsx +42 -0
  119. package/src/components/ModalSheet/common/closeIcon.tsx +40 -0
  120. package/src/components/ModalSheet/common/enviveWatermark.tsx +30 -0
  121. package/src/components/ModalSheet/desktop/desktopHeader.tsx +24 -0
  122. package/src/components/ModalSheet/desktop/index.tsx +118 -0
  123. package/src/components/ModalSheet/index.ts +1 -0
  124. package/src/components/ModalSheet/mobile/index.tsx +117 -0
  125. package/src/components/ModalSheet/mobile/mobileHeader.tsx +42 -0
  126. package/src/components/ModalSheet/types.ts +33 -0
  127. package/src/components/RadioButton/RadioButton.tsx +88 -0
  128. package/src/components/RadioButton/index.ts +1 -0
  129. package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +72 -0
  130. package/src/components/RadioButtonGroup/index.ts +1 -0
  131. package/src/components/SearchFilter/SearchFilter.tsx +161 -64
  132. package/src/components/SearchFilter/SearchFilterFooter.tsx +55 -0
  133. package/src/components/SearchFilter/types.ts +2 -1
  134. package/src/components/SearchFilter/useHasFilterStateChanged.tsx +40 -0
  135. package/src/components/SearchFilter/utils.ts +15 -0
  136. package/src/components/SearchResultsFilterSidebar/SearchResultsFilter.tsx +2 -2
  137. package/src/components/ToggleButton/ToggleButton.tsx +98 -0
  138. package/src/components/ToggleButton/index.ts +1 -0
  139. package/dist/SearchFilter-CLYCSNAa.cjs +0 -137
  140. package/dist/SearchFilter-qQPpx-34.js +0 -117
  141. /package/dist/{ButtonBase-C_uKnl48.js → ButtonBase-0NN6wmX-.js} +0 -0
  142. /package/dist/{ButtonBase-DGbSm0SJ.js → ButtonBase-BAf-nlCm.js} +0 -0
  143. /package/dist/{ButtonBase-DbWQ25n-.cjs → ButtonBase-BIXx56hq.cjs} +0 -0
  144. /package/dist/{ButtonBase-DGpQBeLR.cjs → ButtonBase-Do88ndKa.cjs} +0 -0
  145. /package/dist/{DynamicFiltersScrollbar-Dev5vGsW.js → DynamicFiltersScrollbar-C4kdNSJ9.js} +0 -0
  146. /package/dist/{DynamicFiltersScrollbar-ChvmEiPB.cjs → DynamicFiltersScrollbar-CVw1PINp.cjs} +0 -0
  147. /package/dist/{Headline-DTaT30_m.js → Headline-DNEWF8ly.js} +0 -0
  148. /package/dist/{Headline-DSmu5Mg8.cjs → Headline-DTT4RSv2.cjs} +0 -0
  149. /package/dist/{SearchAutocomplete-C4RY0IoT.js → SearchAutocomplete-C6omCGJp.js} +0 -0
  150. /package/dist/{SearchAutocomplete-hQDnKtwQ.cjs → SearchAutocomplete-Cofuvwwp.cjs} +0 -0
  151. /package/dist/{SparkleAnimation-fQHP7b-R.js → SparkleAnimation-AM4XoegD.js} +0 -0
  152. /package/dist/{SparkleAnimation-BY5iw7s0.cjs → SparkleAnimation-Edzqyb48.cjs} +0 -0
  153. /package/dist/{Spinner-BqTt55uu.js → Spinner-CjGLIRgs.js} +0 -0
  154. /package/dist/{Spinner-DjK8ts9E.cjs → Spinner-DFor2Szi.cjs} +0 -0
  155. /package/dist/{TextInput-DIjjsSMg.js → TextInput-BVPdz7e8.js} +0 -0
  156. /package/dist/{TextInput-BJrdkZsM.cjs → TextInput-CnXhppYn.cjs} +0 -0
  157. /package/dist/{colorsConfig-CYZ8f_gj.js → colorsConfig-5Yf4nrEe.js} +0 -0
  158. /package/dist/{colorsConfig-CJTKbJsm.cjs → colorsConfig-DFL3mBwB.cjs} +0 -0
  159. /package/dist/{textVariantClasses-ypYGLq0h.d.ts → textVariantClasses-kyZtL8F5.d.ts} +0 -0
@@ -0,0 +1,117 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ import classNames from 'classnames';
4
+ import { motion } from 'framer-motion';
5
+
6
+ import { ModalSheetProps } from '../types';
7
+ import { ModalSheetMobileHeader } from './mobileHeader';
8
+
9
+ export const ModalSheetMobile = ({
10
+ children,
11
+ isOpen,
12
+ closeModal,
13
+ mobileFullHeight = 180,
14
+ animationDuration,
15
+ headerProps,
16
+ footerProps,
17
+ }: ModalSheetProps) => {
18
+ const [isHidden, setIsHidden] = useState(isOpen ? 'block' : 'none');
19
+ const [bottom, setBottom] = useState(isOpen ? 0 : 0 - mobileFullHeight);
20
+ const [overlayOpacity, setOverlayOpacity] = useState(isOpen ? 1 : 0);
21
+
22
+ useEffect(() => {
23
+ setIsHidden(isOpen ? 'block' : 'none');
24
+ setBottom(isOpen ? 0 : 0 - mobileFullHeight);
25
+ setOverlayOpacity(isOpen ? 1 : 0);
26
+ }, [mobileFullHeight, isOpen]);
27
+
28
+ // TODO: Support partial view
29
+
30
+ const overlayClassNames = classNames({
31
+ 'spiffy-modal-sheet-overlay': true,
32
+ 'spiffy-tw-fixed': true,
33
+ 'spiffy-tw-inset-0': true,
34
+ 'spiffy-tw-top-[0]': true,
35
+ 'spiffy-tw-bottom-[0]': true,
36
+ 'spiffy-tw-left-[0]': true,
37
+ 'spiffy-tw-right-[0]': true,
38
+ 'spiffy-tw-bg-black/30': true,
39
+ 'spiffy-tw-z-[100]': true,
40
+ });
41
+
42
+ const modalClassNames = classNames({
43
+ 'spiffy-modal-sheet': true,
44
+ 'spiffy-modal-sheet-open': isOpen,
45
+ 'spiffy-tw-w-full': true,
46
+ 'spiffy-tw-overflow-y-auto': true,
47
+ 'spiffy-tw-shadow-lg': true,
48
+ 'spiffy-tw-fixed': true,
49
+ 'spiffy-tw-bg-white': true,
50
+ 'spiffy-tw-border-box': true,
51
+ 'spiffy-tw-rounded-t-[16px]': true,
52
+ 'spiffy-tw-h-[89%]': true,
53
+ 'spiffy-tw-max-h-[89%]': true,
54
+ });
55
+
56
+ const contentClassNames = classNames({
57
+ 'spiffy-modal-sheet-content': true,
58
+ 'spiffy-tw-flex': true,
59
+ 'spiffy-tw-flex-col': true,
60
+ 'spiffy-tw-w-full': true,
61
+ 'spiffy-tw-h-full': true,
62
+ });
63
+
64
+ const headerClassNames = classNames({
65
+ 'spiffy-tw-sticky': true,
66
+ 'spiffy-tw-top-[0px]': true,
67
+ 'spiffy-tw-flex': true,
68
+ 'spiffy-tw-flex-col': true,
69
+ });
70
+ const { footerContent, footerClassName } = footerProps || {};
71
+
72
+ const footerClassNames = classNames(
73
+ {
74
+ 'spiffy-tw-sticky': true,
75
+ 'spiffy-tw-bottom-[0px]': true,
76
+ 'spiffy-modal-sheet-footer': true,
77
+ 'spiffy-tw-flex': true,
78
+ 'spiffy-tw-flex-col': true,
79
+ },
80
+ footerClassName,
81
+ );
82
+
83
+ return (
84
+ <div>
85
+ <motion.div
86
+ className={overlayClassNames}
87
+ animate={{ opacity: overlayOpacity, display: isHidden }}
88
+ onClick={closeModal}
89
+ >
90
+ <motion.div
91
+ role="dialog"
92
+ className={modalClassNames}
93
+ initial={{ bottom, display: isHidden }}
94
+ animate={{ bottom, display: isHidden }}
95
+ exit={{ bottom, display: isHidden }}
96
+ transition={{ duration: animationDuration, ease: 'easeIn' }}
97
+ onClick={(e) => e.stopPropagation()}
98
+ >
99
+ <div className={contentClassNames}>
100
+ <div className={headerClassNames}>
101
+ <ModalSheetMobileHeader
102
+ headerContent={headerProps.headerContent}
103
+ headerVariant={headerProps?.headerVariant}
104
+ handleHeaderClick={closeModal}
105
+ />
106
+ </div>
107
+ {children}
108
+ {/* this div will grow to fill the remaining space */}
109
+ <div className="spiffy-tw-flex-grow" />
110
+
111
+ {footerContent && <div className={footerClassNames}>{footerContent}</div>}
112
+ </div>
113
+ </motion.div>
114
+ </motion.div>
115
+ </div>
116
+ );
117
+ };
@@ -0,0 +1,42 @@
1
+ import classNames from 'classnames';
2
+ import { motion, useMotionValue } from 'framer-motion';
3
+ import { ModalSheetHeaderProps } from '../types';
4
+ import { AnimatedChevron } from 'src/components/AnimatedChevron/AnimatedChevron';
5
+
6
+ const modalSheetVariants = {
7
+ primary: {
8
+ 'spiffy-tw-bg-[--spiffy-colors-background-primary]': true,
9
+ 'spiffy-tw-text-[--spiffy-colors-text-light]': true,
10
+ },
11
+ };
12
+
13
+ export const ModalSheetMobileHeader = ({
14
+ headerVariant = 'primary',
15
+ headerContent,
16
+ handleHeaderClick,
17
+ chevronColor,
18
+ }: ModalSheetHeaderProps) => {
19
+ const animationKey = useMotionValue(-1);
20
+ const headerClassName = classNames({
21
+ 'spiffy-modal-sheet-header': true,
22
+ 'spiffy-tw-flex': true,
23
+ 'spiffy-tw-items-center': true,
24
+ 'spiffy-tw-justify-between': true,
25
+ 'spiffy-tw-w-full': true,
26
+ 'spiffy-tw-flex-col': true,
27
+ 'spiffy-tw-pt-[16px]': true,
28
+ 'spiffy-tw-shadow-[0px_-8px_16px_0px_#0000001A]': true,
29
+ ...modalSheetVariants[headerVariant],
30
+ });
31
+
32
+ return (
33
+ <motion.div
34
+ className={headerClassName}
35
+ onTouchStart={handleHeaderClick}
36
+ onMouseDown={handleHeaderClick}
37
+ >
38
+ <AnimatedChevron animationKey={animationKey} chevronColor={chevronColor} />
39
+ {headerContent}
40
+ </motion.div>
41
+ );
42
+ };
@@ -0,0 +1,33 @@
1
+ export type ModalSheetProps = {
2
+ children: React.ReactNode;
3
+ isOpen: boolean;
4
+ closeModal: () => void;
5
+ desktopWidth?: number;
6
+ mobileFullHeight?: number;
7
+ animationDuration?: number;
8
+ headerProps: ModalSheetHeaderProps;
9
+ footerProps?: ModalSheetFooterProps;
10
+ showOverlay?: boolean;
11
+ };
12
+
13
+ export type ModalSheetHeaderProps = {
14
+ headerVariant?: ModalSheetHeaderVariant;
15
+ headerContent: React.ReactNode;
16
+ handleHeaderClick?: () => void;
17
+ chevronColor: string;
18
+ };
19
+
20
+ export type ModalSheetFooterProps = {
21
+ footerContent: React.ReactNode;
22
+ footerClassName?: string;
23
+ };
24
+
25
+ export type ModalSheetHeaderVariant = 'primary';
26
+
27
+ export type CloseIconVariant = 'light' | 'tertiary' | 'dark';
28
+ export type ModalSheetCloseIconProps = {
29
+ closeIconVariant: CloseIconVariant;
30
+ closeIcon?: React.ReactNode;
31
+ closeModal?: () => void;
32
+ iconSize?: string;
33
+ };
@@ -0,0 +1,88 @@
1
+ import classNames from 'classnames';
2
+ import { Text } from 'src/components/Text';
3
+ import { useState } from 'react';
4
+ import { TestProps } from '@envive-ai/react-hooks/types';
5
+
6
+ interface RadioButtonProps extends TestProps {
7
+ isDisabled?: boolean;
8
+ label: string;
9
+ name: string;
10
+ value: string;
11
+ textButtonGap?: 'small' | 'medium' | 'large';
12
+ fillColor?: string;
13
+ hoverColor?: string;
14
+ uncheckedBorderColor?: string;
15
+ checked?: boolean;
16
+ onChange: (value: string) => void;
17
+ }
18
+
19
+ export const RadioButton = ({
20
+ isDisabled = false,
21
+ label,
22
+ name,
23
+ value,
24
+ textButtonGap = 'medium',
25
+ fillColor,
26
+ hoverColor,
27
+ uncheckedBorderColor,
28
+ checked = false,
29
+ onChange,
30
+ }: RadioButtonProps) => {
31
+ const [isHovered, setIsHovered] = useState(false);
32
+
33
+ const containerClassName = classNames({
34
+ 'spiffy-tw-flex': true,
35
+ 'spiffy-tw-items-center': true,
36
+ 'spiffy-tw-group': true,
37
+ 'spiffy-tw-gap-1': textButtonGap === 'small',
38
+ 'spiffy-tw-gap-2': textButtonGap === 'medium',
39
+ 'spiffy-tw-gap-4': textButtonGap === 'large',
40
+ 'spiffy-tw-cursor-pointer': !isDisabled,
41
+ 'spiffy-tw-opacity-40': isDisabled,
42
+ });
43
+
44
+ const getFillColor = () => {
45
+ if (isHovered && hoverColor) return hoverColor;
46
+ if (checked) return fillColor;
47
+ return uncheckedBorderColor;
48
+ };
49
+
50
+ const handleChange = () => {
51
+ if (!isDisabled) {
52
+ onChange(value);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <label
58
+ className={containerClassName}
59
+ onMouseEnter={() => !isDisabled && setIsHovered(true)}
60
+ onMouseLeave={() => setIsHovered(false)}
61
+ >
62
+ <div className="spiffy-tw-relative">
63
+ <input
64
+ type="radio"
65
+ name={name}
66
+ value={value}
67
+ checked={checked}
68
+ disabled={isDisabled}
69
+ onChange={handleChange}
70
+ className="spiffy-tw-sr-only"
71
+ />
72
+ <svg
73
+ xmlns="http://www.w3.org/2000/svg"
74
+ width="24"
75
+ height="24"
76
+ viewBox="0 0 24 24"
77
+ fill="none"
78
+ >
79
+ {/* Outer circle (border). Gray when unchecked */}
80
+ <circle cx="12" cy="12" r="11.5" fill="white" stroke={getFillColor()} />
81
+ {/* Inner filled circle (when checked) */}
82
+ {checked && <circle cx="12" cy="12" r="9" fill={getFillColor()} />}
83
+ </svg>
84
+ </div>
85
+ <Text variant="body3">{label}</Text>
86
+ </label>
87
+ );
88
+ };
@@ -0,0 +1 @@
1
+ export * from './RadioButton';
@@ -0,0 +1,72 @@
1
+ import { TestProps } from '@envive-ai/react-hooks/types';
2
+ import classNames from 'classnames';
3
+ import { RadioButton } from '../RadioButton';
4
+
5
+ export const SPIFFY_RADIO_BUTTON_GROUP_CLASS = 'spiffy-radio-button-group-class';
6
+
7
+ interface RadioButtonOption {
8
+ label: string;
9
+ value: string;
10
+ disabled?: boolean;
11
+ }
12
+
13
+ interface RadioButtonGroupProps extends TestProps {
14
+ name: string;
15
+ options: RadioButtonOption[];
16
+ fillColor?: string;
17
+ hoverColor?: string;
18
+ value?: string;
19
+ isDisabled?: boolean;
20
+ gap?: 'small' | 'medium' | 'large'; // Gap between each radio button
21
+ textButtonGap?: 'small' | 'medium' | 'large'; // Gap between radio button and text
22
+ onChange: (value: string) => void;
23
+ uncheckedBorderColor?: string;
24
+ }
25
+
26
+ export const RadioButtonGroup = ({
27
+ name,
28
+ options,
29
+ fillColor,
30
+ hoverColor,
31
+ uncheckedBorderColor,
32
+ value,
33
+ isDisabled = false,
34
+ gap = 'medium',
35
+ textButtonGap = 'medium',
36
+ onChange,
37
+ }: RadioButtonGroupProps) => {
38
+ const groupClassName = classNames({
39
+ [SPIFFY_RADIO_BUTTON_GROUP_CLASS]: true,
40
+ 'spiffy-tw-flex': true,
41
+ 'spiffy-tw-flex-col': true,
42
+ 'spiffy-tw-gap-1': gap === 'small',
43
+ 'spiffy-tw-gap-2': gap === 'medium',
44
+ 'spiffy-tw-gap-4': gap === 'large',
45
+ });
46
+
47
+ const handleChange = (selectedValue: string) => {
48
+ if (!isDisabled) {
49
+ onChange(selectedValue);
50
+ }
51
+ };
52
+
53
+ return (
54
+ <div className={groupClassName}>
55
+ {options.map((option) => (
56
+ <RadioButton
57
+ key={option.value}
58
+ name={name}
59
+ value={option.value}
60
+ fillColor={fillColor}
61
+ hoverColor={hoverColor}
62
+ uncheckedBorderColor={uncheckedBorderColor}
63
+ label={option.label}
64
+ checked={value === option.value}
65
+ isDisabled={isDisabled || option.disabled}
66
+ onChange={handleChange}
67
+ textButtonGap={textButtonGap}
68
+ />
69
+ ))}
70
+ </div>
71
+ );
72
+ };
@@ -0,0 +1 @@
1
+ export * from './RadioButtonGroup';
@@ -1,84 +1,181 @@
1
1
  import classNames from 'classnames';
2
- import { SearchFilterItem } from './SearchFilterItem';
3
- import { Text } from 'src/components/Text/Text';
4
- import { ButtonBase } from 'src/components/ButtonBase/ButtonBase';
5
2
  import { SearchFilterProps } from './types';
3
+ import { useCallback } from 'react';
4
+ import { SearchFilterHeader } from './SearchFilterHeader';
5
+ import { useHasFilterStateChanged } from './useHasFilterStateChanged';
6
+ import { getSelectedFilterItemsCount, getTotalSelectedFilterItemsCount } from './utils';
7
+ import { ModalSheet } from '../ModalSheet';
8
+ import { FilterItemProps, FilterProps } from '@envive-ai/react-hooks/types';
9
+ import { ToggleButton } from '../ToggleButton';
10
+ import { Accordion } from '../Accordion';
11
+ import { RadioButtonGroup } from '../RadioButtonGroup';
12
+ import { SearchFilterFooter } from './SearchFilterFooter';
13
+
14
+ const SortFilter = ({
15
+ filter,
16
+ selectFilterItem,
17
+ radioButtonFillColor,
18
+ radioButtonHoverColor,
19
+ radioButtonUncheckedBorderColor,
20
+ }: FilterProps) => {
21
+ const filterTitle = filter.displayName;
22
+
23
+ const options = filter.items.map((item) => ({
24
+ label: item.displayName,
25
+ value: item.filterItemId,
26
+ isSelected: item.isSelected,
27
+ }));
28
+
29
+ const content = (
30
+ <RadioButtonGroup
31
+ options={options}
32
+ name={filterTitle}
33
+ onChange={(value) => {
34
+ const selectedItem = filter.items.find((item) => item.filterItemId === value);
35
+ selectFilterItem({
36
+ filterId: filter.filterId,
37
+ filterItemId: value,
38
+ isSelected: true,
39
+ displayName: selectedItem?.displayName || value,
40
+ });
41
+ }}
42
+ gap="large"
43
+ textButtonGap="large"
44
+ value={filter.items.find((item) => item.isSelected)?.filterItemId}
45
+ fillColor={radioButtonFillColor}
46
+ hoverColor={radioButtonHoverColor}
47
+ uncheckedBorderColor={radioButtonUncheckedBorderColor}
48
+ />
49
+ );
50
+
51
+ return <Accordion title={filterTitle} content={content} />;
52
+ };
53
+
54
+ const FilterItem = ({ filterId, filterItem, selectFilterItem }: FilterItemProps) => (
55
+ <ToggleButton
56
+ label={filterItem.displayName}
57
+ secondaryLabel={`(${filterItem.productCount})`}
58
+ isSelected={filterItem.isSelected}
59
+ onClick={() =>
60
+ selectFilterItem({
61
+ filterId,
62
+ filterItemId: filterItem.filterItemId,
63
+ isSelected: !filterItem.isSelected,
64
+ displayName: filterItem.displayName,
65
+ })
66
+ }
67
+ variant="default"
68
+ />
69
+ );
70
+
71
+ const Filter = ({ filter, selectFilterItem }: FilterProps) => {
72
+ const filterContentClassName = classNames({
73
+ 'spiffy-tw-flex': true,
74
+ 'spiffy-tw-flex-wrap': true,
75
+ 'spiffy-tw-gap-[8px]': true,
76
+ 'spiffy-tw-pb-[1px]': true,
77
+ });
78
+
79
+ const selectedFilterItems = getSelectedFilterItemsCount(filter);
80
+
81
+ const filterTitle =
82
+ selectedFilterItems > 0 ? `${filter.displayName} (${selectedFilterItems})` : filter.displayName;
83
+ const content = (
84
+ <div className={filterContentClassName}>
85
+ {filter.items.map((item) => (
86
+ <FilterItem
87
+ key={item.filterItemId}
88
+ filterId={filter.filterId}
89
+ filterItem={item}
90
+ selectFilterItem={selectFilterItem}
91
+ />
92
+ ))}
93
+ </div>
94
+ );
95
+ return <Accordion title={filterTitle} content={content} />;
96
+ };
6
97
 
7
98
  export const SearchFilter = ({
8
- isOpen,
9
- setIsOpen,
10
99
  filters,
11
- // productCount,
100
+ productCount,
12
101
  selectFilterItem,
13
102
  clearAllFilters,
14
- // applyFiltersUnchangedClasses,
15
- // applyFiltersChangedClasses,
16
- filterButtonText,
103
+ isOpen,
104
+ setIsOpen,
105
+ applyFiltersUnchangedClasses,
106
+ applyFiltersChangedClasses,
107
+ headerContent,
108
+ footerContent,
109
+ headerClassName,
17
110
  radioButtonFillColor,
18
111
  radioButtonHoverColor,
19
112
  radioButtonUncheckedBorderColor,
20
- // filterCloseIconVariant,
21
- headerContent,
113
+ filterCloseIconVariant,
22
114
  }: SearchFilterProps) => {
23
- const modalClasses = classNames(
24
- 'spiffy-tw-fixed spiffy-tw-inset-0 spiffy-tw-z-50 spiffy-tw-flex spiffy-tw-transform spiffy-tw-transition-all',
25
- {
26
- 'spiffy-tw-translate-x-full': !isOpen,
27
- 'spiffy-tw-translate-x-0': isOpen,
28
- },
29
- );
115
+ const closeModal = useCallback(() => setIsOpen(false), [setIsOpen]);
116
+ const filterCount = getTotalSelectedFilterItemsCount(filters);
30
117
 
31
- const overlayClasses = classNames(
32
- 'spiffy-tw-absolute spiffy-tw-inset-0 spiffy-tw-bg-black spiffy-tw-bg-opacity-50',
33
- {
34
- 'spiffy-tw-opacity-0': !isOpen,
35
- 'spiffy-tw-opacity-100': isOpen,
36
- },
37
- );
118
+ const hasFiltersChanged = useHasFilterStateChanged(filters, isOpen);
38
119
 
39
- const sidebarClasses = classNames(
40
- 'spiffy-tw-relative spiffy-tw-ml-auto spiffy-tw-flex spiffy-tw-h-full spiffy-tw-w-full spiffy-tw-max-w-xs spiffy-tw-flex-col spiffy-tw-overflow-y-auto spiffy-tw-bg-[--spiffy-colors-background-light] spiffy-tw-py-4 spiffy-tw-shadow-xl',
41
- );
120
+ const filterWrapperClassName = classNames({
121
+ 'spiffy-tw-flex': true,
122
+ 'spiffy-tw-flex-col': true,
123
+ 'spiffy-tw-gap-[16px]': true,
124
+ 'spiffy-tw-px-[24px]': true,
125
+ 'spiffy-tw-py-[16px]': true,
126
+ 'spiffy-tw-max-h-screen': true,
127
+ 'spiffy-tw-overflow-y-auto': true,
128
+ });
42
129
 
43
130
  return (
44
- <div className={modalClasses}>
45
- <div className={overlayClasses} onClick={() => setIsOpen(false)} />
46
- <div className={sidebarClasses}>
47
- {headerContent}
48
- <div className="spiffy-tw-flex-1 spiffy-tw-px-4 spiffy-tw-py-6">
49
- {filters.map((filter) => (
50
- <div key={filter.filterId} className="spiffy-tw-mb-6">
51
- <Text variant="body2" className="spiffy-tw-font-medium spiffy-tw-mb-3">
52
- {filter.displayName}
53
- </Text>
54
- <ul>
55
- {filter.items.map((item) => (
56
- <SearchFilterItem
57
- key={item.filterItemId}
58
- filterItem={{ ...item, filterId: filter.filterId }}
59
- onSelectFilterItem={selectFilterItem}
60
- radioButtonFillColor={radioButtonFillColor}
61
- radioButtonHoverColor={radioButtonHoverColor}
62
- radioButtonUncheckedBorderColor={radioButtonUncheckedBorderColor}
63
- />
64
- ))}
65
- </ul>
66
- </div>
67
- ))}
68
- </div>
69
- <div className="spiffy-tw-border-t spiffy-tw-border-[--spiffy-colors-border-light] spiffy-tw-px-4 spiffy-tw-py-3">
70
- <ButtonBase
71
- onClick={clearAllFilters}
72
- text="Clear All"
73
- buttonClass="spiffy-tw-w-full spiffy-tw-mb-2"
131
+ <ModalSheet
132
+ isOpen={isOpen}
133
+ closeModal={closeModal}
134
+ headerProps={{
135
+ headerContent: headerContent || (
136
+ <SearchFilterHeader
137
+ closeModal={closeModal}
138
+ productCount={productCount}
139
+ headerClassName={headerClassName ?? ''}
140
+ filterCloseIconVariant={filterCloseIconVariant}
74
141
  />
75
- <ButtonBase
76
- onClick={() => setIsOpen(false)}
77
- text={filterButtonText}
78
- buttonClass="spiffy-tw-w-full"
142
+ ),
143
+ chevronColor: '#000',
144
+ }}
145
+ footerProps={{
146
+ footerContent: footerContent || (
147
+ <SearchFilterFooter
148
+ closeModal={closeModal}
149
+ clearAllFilters={clearAllFilters}
150
+ filterCount={filterCount}
151
+ hasFiltersChanged={hasFiltersChanged}
152
+ applyFiltersUnchangedClasses={applyFiltersUnchangedClasses}
153
+ applyFiltersChangedClasses={applyFiltersChangedClasses}
79
154
  />
80
- </div>
155
+ ),
156
+ }}
157
+ >
158
+ <div className={filterWrapperClassName}>
159
+ {filters
160
+ .filter((filter) => filter.displayName !== '')
161
+ .map((filter) => {
162
+ if (filter.filterId === 'sort') {
163
+ return (
164
+ <SortFilter
165
+ key={filter.filterId}
166
+ filter={filter}
167
+ selectFilterItem={selectFilterItem}
168
+ radioButtonFillColor={radioButtonFillColor}
169
+ radioButtonHoverColor={radioButtonHoverColor}
170
+ radioButtonUncheckedBorderColor={radioButtonUncheckedBorderColor}
171
+ />
172
+ );
173
+ }
174
+ return (
175
+ <Filter key={filter.filterId} filter={filter} selectFilterItem={selectFilterItem} />
176
+ );
177
+ })}
81
178
  </div>
82
- </div>
179
+ </ModalSheet>
83
180
  );
84
181
  };
@@ -0,0 +1,55 @@
1
+ import classNames from 'classnames';
2
+ import { ButtonBase } from '../ButtonBase';
3
+ import { FilterFooterProps } from '@envive-ai/react-hooks/types';
4
+
5
+ export const SearchFilterFooter = ({
6
+ closeModal,
7
+ clearAllFilters,
8
+ filterCount,
9
+ applyFiltersUnchangedClasses,
10
+ applyFiltersChangedClasses,
11
+ hasFiltersChanged,
12
+ }: FilterFooterProps) => {
13
+ const clearAllButtonEnabled = filterCount > 0;
14
+
15
+ const footerWrapperClassName = classNames({
16
+ 'spiffy-tw-flex': true,
17
+ 'spiffy-tw-flex-col': true,
18
+ 'spiffy-tw-items-center': true,
19
+ 'spiffy-tw-justify-between': true,
20
+ 'spiffy-tw-w-full': true,
21
+ 'spiffy-tw-border-t': true,
22
+ 'spiffy-tw-border-t-[--spiffy-colors-border-light]': true,
23
+ 'spiffy-tw-p-[16px]': true,
24
+ 'spiffy-tw-gap-[16px]': true,
25
+ });
26
+ const applyFilterButtonClassName = classNames(
27
+ 'spiffy-tw-flex spiffy-tw-flex-row spiffy-tw-justify-center spiffy-tw-rounded-[8px] spiffy-tw-w-full',
28
+ hasFiltersChanged ? applyFiltersChangedClasses : applyFiltersUnchangedClasses,
29
+ );
30
+ const clearAllButtonClassName = classNames({
31
+ 'spiffy-tw-flex': true,
32
+ 'spiffy-tw-flex-row': true,
33
+ 'spiffy-tw-justify-center': true,
34
+ 'spiffy-tw-w-full': true,
35
+ 'spiffy-tw-text-[--spiffy-colors-text-primary]': clearAllButtonEnabled,
36
+ 'spiffy-tw-text-[--spiffy-colors-text-secondary]': !clearAllButtonEnabled,
37
+ });
38
+
39
+ return (
40
+ <div className={footerWrapperClassName}>
41
+ <ButtonBase
42
+ isDisabled={!hasFiltersChanged}
43
+ buttonClass={applyFilterButtonClassName}
44
+ text="Apply Filters"
45
+ onClick={closeModal}
46
+ />
47
+ <ButtonBase
48
+ isDisabled={!clearAllButtonEnabled}
49
+ buttonClass={clearAllButtonClassName}
50
+ text="Clear All"
51
+ onClick={clearAllFilters}
52
+ />
53
+ </div>
54
+ );
55
+ };
@@ -1,4 +1,5 @@
1
1
  import {
2
+ CloseIconVariant,
2
3
  SearchFilterDatum,
3
4
  SearchFilterItemDatum,
4
5
  SelectFilterItem,
@@ -40,5 +41,5 @@ export type SearchFilterProps = {
40
41
  radioButtonFillColor: string;
41
42
  radioButtonHoverColor?: string;
42
43
  radioButtonUncheckedBorderColor?: string;
43
- // filterCloseIconVariant: CloseIconVariant;
44
+ filterCloseIconVariant: CloseIconVariant;
44
45
  };