@windstream/react-shared-components 0.1.64 → 0.1.68

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 (198) hide show
  1. package/README.md +635 -635
  2. package/dist/contentful/index.d.ts +23 -0
  3. package/dist/contentful/index.esm.js +3 -3
  4. package/dist/contentful/index.esm.js.map +1 -1
  5. package/dist/contentful/index.js +3 -3
  6. package/dist/contentful/index.js.map +1 -1
  7. package/dist/core.d.ts +4 -4
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.esm.js +13 -5
  10. package/dist/index.esm.js.map +1 -1
  11. package/dist/index.js +13 -5
  12. package/dist/index.js.map +1 -1
  13. package/dist/next/index.esm.js +2 -2
  14. package/dist/next/index.esm.js.map +1 -1
  15. package/dist/next/index.js +2 -2
  16. package/dist/next/index.js.map +1 -1
  17. package/dist/styles.css +1 -1
  18. package/dist/utils/index.esm.js +1 -1
  19. package/dist/utils/index.esm.js.map +1 -1
  20. package/dist/utils/index.js +1 -1
  21. package/dist/utils/index.js.map +1 -1
  22. package/package.json +185 -185
  23. package/src/components/accordion/Accordion.stories.tsx +230 -230
  24. package/src/components/accordion/index.tsx +70 -70
  25. package/src/components/accordion/types.ts +12 -12
  26. package/src/components/alert-card/AlertCard.stories.tsx +171 -171
  27. package/src/components/alert-card/index.tsx +41 -41
  28. package/src/components/alert-card/types.ts +13 -13
  29. package/src/components/brand-button/BrandButton.stories.tsx +223 -223
  30. package/src/components/brand-button/helpers.ts +35 -35
  31. package/src/components/brand-button/index.tsx +120 -120
  32. package/src/components/brand-button/types.ts +38 -38
  33. package/src/components/button/Button.stories.tsx +108 -108
  34. package/src/components/button/index.tsx +27 -27
  35. package/src/components/button/types.ts +14 -14
  36. package/src/components/call-button/CallButton.stories.tsx +324 -324
  37. package/src/components/call-button/index.tsx +106 -86
  38. package/src/components/call-button/types.ts +16 -11
  39. package/src/components/checkbox/Checkbox.stories.tsx +247 -247
  40. package/src/components/checkbox/index.tsx +197 -197
  41. package/src/components/checkbox/types.ts +27 -27
  42. package/src/components/checklist/Checklist.stories.tsx +150 -150
  43. package/src/components/checklist/index.tsx +61 -61
  44. package/src/components/checklist/types.ts +17 -17
  45. package/src/components/collapse/Collapse.stories.tsx +255 -255
  46. package/src/components/collapse/index.tsx +46 -46
  47. package/src/components/collapse/types.ts +6 -6
  48. package/src/components/divider/Divider.stories.tsx +205 -205
  49. package/src/components/divider/index.tsx +22 -22
  50. package/src/components/divider/type.ts +3 -3
  51. package/src/components/image/Image.stories.tsx +113 -113
  52. package/src/components/image/index.tsx +25 -25
  53. package/src/components/image/types.ts +40 -40
  54. package/src/components/input/Input.stories.tsx +325 -325
  55. package/src/components/input/index.tsx +177 -177
  56. package/src/components/input/types.ts +37 -37
  57. package/src/components/link/Link.stories.tsx +163 -163
  58. package/src/components/link/index.tsx +116 -109
  59. package/src/components/link/types.ts +25 -25
  60. package/src/components/list/List.stories.tsx +272 -272
  61. package/src/components/list/index.tsx +88 -88
  62. package/src/components/list/list-item/index.tsx +38 -38
  63. package/src/components/list/list-item/types.ts +13 -13
  64. package/src/components/list/types.ts +29 -29
  65. package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
  66. package/src/components/material-icon/constants.ts +99 -98
  67. package/src/components/material-icon/index.tsx +47 -47
  68. package/src/components/material-icon/types.ts +31 -31
  69. package/src/components/modal/Modal.stories.tsx +171 -171
  70. package/src/components/modal/index.tsx +164 -164
  71. package/src/components/modal/types.ts +24 -24
  72. package/src/components/next-image/index.tsx +61 -54
  73. package/src/components/next-image/types.ts +1 -1
  74. package/src/components/pagination/index.tsx +91 -91
  75. package/src/components/pagination/types.ts +6 -6
  76. package/src/components/radio-button/RadioButton.stories.tsx +307 -307
  77. package/src/components/radio-button/index.tsx +75 -75
  78. package/src/components/radio-button/types.ts +21 -21
  79. package/src/components/see-more/SeeMore.stories.tsx +181 -181
  80. package/src/components/see-more/index.tsx +44 -44
  81. package/src/components/see-more/types.ts +4 -4
  82. package/src/components/select/Select.stories.tsx +411 -411
  83. package/src/components/select/index.tsx +155 -155
  84. package/src/components/select/types.ts +36 -36
  85. package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
  86. package/src/components/select-plan-button/index.tsx +63 -63
  87. package/src/components/select-plan-button/types.ts +17 -17
  88. package/src/components/skeleton/Skeleton.stories.tsx +179 -179
  89. package/src/components/skeleton/index.tsx +61 -61
  90. package/src/components/skeleton/types.ts +4 -4
  91. package/src/components/spinner/Spinner.stories.tsx +335 -335
  92. package/src/components/spinner/index.tsx +44 -44
  93. package/src/components/spinner/types.ts +5 -5
  94. package/src/components/text/Text.stories.tsx +321 -321
  95. package/src/components/text/index.tsx +25 -25
  96. package/src/components/text/types.ts +45 -45
  97. package/src/components/tooltip/Tooltip.stories.tsx +219 -219
  98. package/src/components/tooltip/index.tsx +74 -74
  99. package/src/components/tooltip/types.ts +7 -7
  100. package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
  101. package/src/components/view-cart-button/index.tsx +42 -42
  102. package/src/components/view-cart-button/types.ts +5 -5
  103. package/src/contentful/blocks/accordion/Accordion.stories.mocks.tsx +128 -0
  104. package/src/contentful/blocks/accordion/Accordion.stories.tsx +106 -34
  105. package/src/contentful/blocks/accordion/index.tsx +112 -112
  106. package/src/contentful/blocks/accordion/types.ts +34 -34
  107. package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
  108. package/src/contentful/blocks/address-input-banner/types.ts +14 -14
  109. package/src/contentful/blocks/anchored-bottom-banner/index.tsx +70 -70
  110. package/src/contentful/blocks/anchored-bottom-banner/types.ts +10 -10
  111. package/src/contentful/blocks/blogs-grid/index.tsx +134 -134
  112. package/src/contentful/blocks/blogs-grid/types.ts +26 -26
  113. package/src/contentful/blocks/breadcrumbs/index.tsx +1 -1
  114. package/src/contentful/blocks/breadcrumbs/types.ts +6 -6
  115. package/src/contentful/blocks/button/Button.stories.tsx +40 -40
  116. package/src/contentful/blocks/button/index.tsx +131 -131
  117. package/src/contentful/blocks/button/types.ts +39 -39
  118. package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
  119. package/src/contentful/blocks/callout/index.tsx +88 -88
  120. package/src/contentful/blocks/callout/types.ts +15 -15
  121. package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
  122. package/src/contentful/blocks/cards/blog-card/types.ts +34 -34
  123. package/src/contentful/blocks/cards/index.tsx +13 -13
  124. package/src/contentful/blocks/cards/product-card/index.tsx +251 -251
  125. package/src/contentful/blocks/cards/product-card/types.ts +28 -28
  126. package/src/contentful/blocks/cards/simple-card/index.tsx +89 -89
  127. package/src/contentful/blocks/cards/simple-card/types.ts +28 -28
  128. package/src/contentful/blocks/cards/testimonial-card/index.tsx +90 -90
  129. package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
  130. package/src/contentful/blocks/cards/types.ts +1 -1
  131. package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
  132. package/src/contentful/blocks/carousel/helper.tsx +440 -440
  133. package/src/contentful/blocks/carousel/index.tsx +85 -85
  134. package/src/contentful/blocks/carousel/types.ts +144 -144
  135. package/src/contentful/blocks/cart-retention-banner/types.ts +11 -11
  136. package/src/contentful/blocks/comparison-table/index.tsx +27 -27
  137. package/src/contentful/blocks/comparison-table/types.ts +6 -6
  138. package/src/contentful/blocks/cookiebanner/index.tsx +146 -146
  139. package/src/contentful/blocks/cookiebanner/type.ts +7 -7
  140. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
  141. package/src/contentful/blocks/cta-callout/index.tsx +71 -71
  142. package/src/contentful/blocks/cta-callout/types.ts +26 -26
  143. package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
  144. package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
  145. package/src/contentful/blocks/email-input-block/index.tsx +117 -117
  146. package/src/contentful/blocks/email-input-block/types.ts +16 -16
  147. package/src/contentful/blocks/find-kinetic/FindKinetic.stories.tsx +23 -23
  148. package/src/contentful/blocks/find-kinetic/index.tsx +130 -130
  149. package/src/contentful/blocks/find-kinetic/types.ts +19 -19
  150. package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
  151. package/src/contentful/blocks/floating-banner/index.tsx +97 -97
  152. package/src/contentful/blocks/floating-banner/types.ts +22 -22
  153. package/src/contentful/blocks/footer/Footer.stories.tsx +204 -30
  154. package/src/contentful/blocks/footer/index.tsx +91 -91
  155. package/src/contentful/blocks/footer/types.ts +13 -13
  156. package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
  157. package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
  158. package/src/contentful/blocks/image-promo-bar/index.tsx +246 -246
  159. package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
  160. package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
  161. package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
  162. package/src/contentful/blocks/modal/constants.ts +53 -53
  163. package/src/contentful/blocks/modal/index.tsx +107 -107
  164. package/src/contentful/blocks/modal/types.ts +12 -12
  165. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +124 -113
  166. package/src/contentful/blocks/navigation/index.tsx +462 -394
  167. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
  168. package/src/contentful/blocks/navigation/types.ts +64 -41
  169. package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
  170. package/src/contentful/blocks/primary-hero/index.tsx +236 -236
  171. package/src/contentful/blocks/primary-hero/types.ts +37 -37
  172. package/src/contentful/blocks/search-block/index.tsx +90 -90
  173. package/src/contentful/blocks/search-block/types.ts +15 -15
  174. package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
  175. package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
  176. package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
  177. package/src/contentful/blocks/text/Text.stories.tsx +23 -23
  178. package/src/contentful/blocks/text/index.tsx +12 -12
  179. package/src/contentful/blocks/text/types.ts +1 -1
  180. package/src/contentful/index.ts +106 -106
  181. package/src/hooks/contentful/use-contentful-rich-text.tsx +310 -310
  182. package/src/hooks/contentful/use-processed-check-list.ts +63 -63
  183. package/src/hooks/use-body-scroll-lock.ts +34 -34
  184. package/src/hooks/use-carousel-swipe.ts +264 -264
  185. package/src/hooks/use-outside-click.ts +17 -17
  186. package/src/index.ts +101 -101
  187. package/src/next/index.ts +5 -5
  188. package/src/setupTests.ts +46 -46
  189. package/src/stories/DocsTemplate.tsx +24 -24
  190. package/src/styles/globals.css +343 -343
  191. package/src/types/global.d.ts +9 -9
  192. package/src/types/micro-components.ts +99 -99
  193. package/src/types/utm.ts +49 -49
  194. package/src/utils/contentful/to-document.ts +24 -24
  195. package/src/utils/cookie.ts +84 -84
  196. package/src/utils/cx.ts +49 -49
  197. package/src/utils/index.ts +38 -38
  198. package/src/utils/utm.ts +221 -221
@@ -1,264 +1,264 @@
1
- import React, {
2
- useCallback,
3
- useEffect,
4
- useMemo,
5
- useRef,
6
- useState,
7
- } from "react";
8
-
9
- /**
10
- * Configuration options for the carousel swipe behavior
11
- */
12
- export interface CarouselSwipeConfig {
13
- /** Total number of items in the carousel */
14
- itemCount: number;
15
- /** Percentage of card width to offset each slide (default: 105) */
16
- cardOffsetPercentage?: number;
17
- /** Percentage of container width needed to trigger slide change (default: 0.15) */
18
- swipeThreshold?: number;
19
- /** Mobile breakpoint in pixels (default: 768) */
20
- mobileBreakpoint?: number;
21
- /** Auto-scroll interval in milliseconds (default: 8000). Set to 0 to disable. */
22
- autoScrollInterval?: number;
23
- /** Enable auto-scroll (default: true) */
24
- enableAutoScroll?: boolean;
25
- }
26
-
27
- /**
28
- * Return value from the useCarouselSwipe hook
29
- */
30
- export interface CarouselSwipeReturn {
31
- /** Current active slide index */
32
- currentIndex: number;
33
- /** Current swipe offset in pixels */
34
- swipeOffset: number;
35
- /** Whether user is currently swiping */
36
- isSwiping: boolean;
37
- /** Whether viewport is mobile size */
38
- isMobile: boolean;
39
- /** Memoized container width */
40
- containerWidth: number;
41
- /** Ref to attach to the carousel container element */
42
- containerRef: React.RefObject<HTMLDivElement>;
43
- /** Navigate to next slide */
44
- nextSlide: () => void;
45
- /** Navigate to previous slide */
46
- prevSlide: () => void;
47
- /** Navigate to specific slide index */
48
- goToSlide: (index: number) => void;
49
- /** Touch start handler */
50
- handleTouchStart: (e: React.TouchEvent) => void;
51
- /** Touch move handler */
52
- handleTouchMove: (e: React.TouchEvent) => void;
53
- /** Touch end handler */
54
- handleTouchEnd: () => void;
55
- /** Constants used for calculations */
56
- constants: {
57
- CARD_OFFSET_PERCENTAGE: number;
58
- SWIPE_THRESHOLD: number;
59
- MOBILE_BREAKPOINT: number;
60
- AUTO_SCROLL_INTERVAL: number;
61
- };
62
- }
63
-
64
- /**
65
- * Custom hook for implementing swipe/touch gestures in carousels
66
- *
67
- * Features:
68
- * - Touch/swipe support with smooth finger-following
69
- * - Auto-scroll with pause on interaction
70
- * - Responsive mobile detection with resize listener
71
- * - Performance optimized with memoization
72
- * - Configurable thresholds and behavior
73
- *
74
- * @example
75
- * ```tsx
76
- * const carousel = useCarouselSwipe({
77
- * itemCount: items.length,
78
- * autoScrollInterval: 5000,
79
- * });
80
- *
81
- * return (
82
- * <div
83
- * ref={carousel.containerRef}
84
- * onTouchStart={carousel.handleTouchStart}
85
- * onTouchMove={carousel.handleTouchMove}
86
- * onTouchEnd={carousel.handleTouchEnd}
87
- * >
88
- * {items.map((item, index) => (
89
- * <div
90
- * key={index}
91
- * style={{
92
- * transform: `translateX(${calculateTransform(index, carousel)})`,
93
- * }}
94
- * >
95
- * {item}
96
- * </div>
97
- * ))}
98
- * </div>
99
- * );
100
- * ```
101
- */
102
- export function useCarouselSwipe(
103
- config: CarouselSwipeConfig
104
- ): CarouselSwipeReturn {
105
- const {
106
- itemCount,
107
- cardOffsetPercentage = 105,
108
- swipeThreshold = 0.15,
109
- mobileBreakpoint = 768,
110
- autoScrollInterval = 8000,
111
- enableAutoScroll = true,
112
- } = config;
113
-
114
- // State
115
- const [currentIndex, setCurrentIndex] = useState(0);
116
- const [swipeOffset, setSwipeOffset] = useState(0);
117
- const [isSwiping, setIsSwiping] = useState(false);
118
- const [containerWidth, setContainerWidth] = useState(window.innerWidth);
119
- // Performance: Store mobile state to avoid repeated window.innerWidth checks on every render
120
- // This prevents expensive DOM queries during swipe operations
121
- const [isMobile, setIsMobile] = useState(false);
122
-
123
- // Refs
124
- const timeoutRef = useRef<ReturnType<typeof setInterval> | null>(null);
125
- const touchStartX = useRef<number>(0);
126
- const containerRef = useRef<HTMLDivElement>(null);
127
-
128
- // Constants
129
- const constants = {
130
- CARD_OFFSET_PERCENTAGE: cardOffsetPercentage,
131
- SWIPE_THRESHOLD: swipeThreshold,
132
- MOBILE_BREAKPOINT: mobileBreakpoint,
133
- AUTO_SCROLL_INTERVAL: autoScrollInterval,
134
- };
135
-
136
- // Performance: Memoize container width to prevent recalculation on every render
137
- // This is especially important during swipe operations where the component re-renders
138
- // frequently. Without memoization, it'd query the DOM on every frame while swiping.
139
- useEffect(() => {
140
- const updateWidth = () => {
141
- setContainerWidth(containerRef.current?.offsetWidth || window.innerWidth);
142
- };
143
- updateWidth(); // Initial
144
- window.addEventListener("resize", updateWidth);
145
- return () => window.removeEventListener("resize", updateWidth);
146
- }, []);
147
-
148
- // Navigation functions
149
- const nextSlide = useCallback(() => {
150
- if (itemCount === 0) return;
151
- setCurrentIndex(prev => (prev + 1) % itemCount);
152
- }, [itemCount]);
153
-
154
- const prevSlide = useCallback(() => {
155
- if (itemCount === 0) return;
156
- setCurrentIndex(prev => (prev === 0 ? itemCount - 1 : prev - 1));
157
- }, [itemCount]);
158
-
159
- const goToSlide = useCallback(
160
- (index: number) => {
161
- if (index < 0 || index >= itemCount) return;
162
- setCurrentIndex(index);
163
- },
164
- [itemCount]
165
- );
166
-
167
- // Touch handlers for mobile swipe
168
- const handleTouchStart = useCallback((e: React.TouchEvent) => {
169
- touchStartX.current = e.touches[0].clientX;
170
- setIsSwiping(true);
171
- // Pause auto-scroll during user interaction
172
- if (timeoutRef.current) {
173
- clearInterval(timeoutRef.current);
174
- }
175
- }, []);
176
-
177
- const handleTouchMove = useCallback(
178
- (e: React.TouchEvent) => {
179
- if (!isSwiping) return;
180
- const currentX = e.touches[0].clientX;
181
- const diff = currentX - touchStartX.current;
182
- setSwipeOffset(diff);
183
- },
184
- [isSwiping]
185
- );
186
-
187
- const handleTouchEnd = useCallback(() => {
188
- setIsSwiping(false);
189
- // Use memoized containerWidth and constant threshold for performance
190
- const threshold = containerWidth * swipeThreshold;
191
-
192
- // Determine if swipe was strong enough to change slides
193
- if (swipeOffset > threshold) {
194
- prevSlide();
195
- } else if (swipeOffset < -threshold) {
196
- nextSlide();
197
- }
198
-
199
- // Reset swipe offset to return card to snapped position
200
- setSwipeOffset(0);
201
-
202
- // Restart auto-scroll after user interaction completes
203
- if (enableAutoScroll && autoScrollInterval > 0) {
204
- timeoutRef.current = setInterval(() => {
205
- nextSlide();
206
- }, autoScrollInterval);
207
- }
208
- }, [
209
- swipeOffset,
210
- containerWidth,
211
- swipeThreshold,
212
- prevSlide,
213
- nextSlide,
214
- enableAutoScroll,
215
- autoScrollInterval,
216
- ]);
217
-
218
- // Performance: Detect mobile viewport and update on resize
219
- // This replaces inline window.innerWidth checks that were running on every render.
220
- // By using state and a resize listener, we only check when the viewport actually changes.
221
- useEffect(() => {
222
- const checkMobile = () => {
223
- setIsMobile(window.innerWidth < mobileBreakpoint);
224
- };
225
-
226
- // Check immediately on mount
227
- checkMobile();
228
-
229
- // Update when window is resized (e.g., device rotation, browser resize)
230
- window.addEventListener("resize", checkMobile);
231
- return () => window.removeEventListener("resize", checkMobile);
232
- }, [mobileBreakpoint]);
233
-
234
- // Auto-scroll logic
235
- useEffect(() => {
236
- if (!enableAutoScroll || itemCount === 0 || autoScrollInterval === 0) {
237
- return;
238
- }
239
-
240
- timeoutRef.current = setInterval(() => {
241
- nextSlide();
242
- }, autoScrollInterval);
243
-
244
- return () => {
245
- if (timeoutRef.current) clearInterval(timeoutRef.current);
246
- };
247
- }, [nextSlide, itemCount, enableAutoScroll, autoScrollInterval]);
248
-
249
- return {
250
- currentIndex,
251
- swipeOffset,
252
- isSwiping,
253
- isMobile,
254
- containerWidth,
255
- containerRef,
256
- nextSlide,
257
- prevSlide,
258
- goToSlide,
259
- handleTouchStart,
260
- handleTouchMove,
261
- handleTouchEnd,
262
- constants,
263
- };
264
- }
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+
9
+ /**
10
+ * Configuration options for the carousel swipe behavior
11
+ */
12
+ export interface CarouselSwipeConfig {
13
+ /** Total number of items in the carousel */
14
+ itemCount: number;
15
+ /** Percentage of card width to offset each slide (default: 105) */
16
+ cardOffsetPercentage?: number;
17
+ /** Percentage of container width needed to trigger slide change (default: 0.15) */
18
+ swipeThreshold?: number;
19
+ /** Mobile breakpoint in pixels (default: 768) */
20
+ mobileBreakpoint?: number;
21
+ /** Auto-scroll interval in milliseconds (default: 8000). Set to 0 to disable. */
22
+ autoScrollInterval?: number;
23
+ /** Enable auto-scroll (default: true) */
24
+ enableAutoScroll?: boolean;
25
+ }
26
+
27
+ /**
28
+ * Return value from the useCarouselSwipe hook
29
+ */
30
+ export interface CarouselSwipeReturn {
31
+ /** Current active slide index */
32
+ currentIndex: number;
33
+ /** Current swipe offset in pixels */
34
+ swipeOffset: number;
35
+ /** Whether user is currently swiping */
36
+ isSwiping: boolean;
37
+ /** Whether viewport is mobile size */
38
+ isMobile: boolean;
39
+ /** Memoized container width */
40
+ containerWidth: number;
41
+ /** Ref to attach to the carousel container element */
42
+ containerRef: React.RefObject<HTMLDivElement>;
43
+ /** Navigate to next slide */
44
+ nextSlide: () => void;
45
+ /** Navigate to previous slide */
46
+ prevSlide: () => void;
47
+ /** Navigate to specific slide index */
48
+ goToSlide: (index: number) => void;
49
+ /** Touch start handler */
50
+ handleTouchStart: (e: React.TouchEvent) => void;
51
+ /** Touch move handler */
52
+ handleTouchMove: (e: React.TouchEvent) => void;
53
+ /** Touch end handler */
54
+ handleTouchEnd: () => void;
55
+ /** Constants used for calculations */
56
+ constants: {
57
+ CARD_OFFSET_PERCENTAGE: number;
58
+ SWIPE_THRESHOLD: number;
59
+ MOBILE_BREAKPOINT: number;
60
+ AUTO_SCROLL_INTERVAL: number;
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Custom hook for implementing swipe/touch gestures in carousels
66
+ *
67
+ * Features:
68
+ * - Touch/swipe support with smooth finger-following
69
+ * - Auto-scroll with pause on interaction
70
+ * - Responsive mobile detection with resize listener
71
+ * - Performance optimized with memoization
72
+ * - Configurable thresholds and behavior
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * const carousel = useCarouselSwipe({
77
+ * itemCount: items.length,
78
+ * autoScrollInterval: 5000,
79
+ * });
80
+ *
81
+ * return (
82
+ * <div
83
+ * ref={carousel.containerRef}
84
+ * onTouchStart={carousel.handleTouchStart}
85
+ * onTouchMove={carousel.handleTouchMove}
86
+ * onTouchEnd={carousel.handleTouchEnd}
87
+ * >
88
+ * {items.map((item, index) => (
89
+ * <div
90
+ * key={index}
91
+ * style={{
92
+ * transform: `translateX(${calculateTransform(index, carousel)})`,
93
+ * }}
94
+ * >
95
+ * {item}
96
+ * </div>
97
+ * ))}
98
+ * </div>
99
+ * );
100
+ * ```
101
+ */
102
+ export function useCarouselSwipe(
103
+ config: CarouselSwipeConfig
104
+ ): CarouselSwipeReturn {
105
+ const {
106
+ itemCount,
107
+ cardOffsetPercentage = 105,
108
+ swipeThreshold = 0.15,
109
+ mobileBreakpoint = 768,
110
+ autoScrollInterval = 8000,
111
+ enableAutoScroll = true,
112
+ } = config;
113
+
114
+ // State
115
+ const [currentIndex, setCurrentIndex] = useState(0);
116
+ const [swipeOffset, setSwipeOffset] = useState(0);
117
+ const [isSwiping, setIsSwiping] = useState(false);
118
+ const [containerWidth, setContainerWidth] = useState(window.innerWidth);
119
+ // Performance: Store mobile state to avoid repeated window.innerWidth checks on every render
120
+ // This prevents expensive DOM queries during swipe operations
121
+ const [isMobile, setIsMobile] = useState(false);
122
+
123
+ // Refs
124
+ const timeoutRef = useRef<ReturnType<typeof setInterval> | null>(null);
125
+ const touchStartX = useRef<number>(0);
126
+ const containerRef = useRef<HTMLDivElement>(null);
127
+
128
+ // Constants
129
+ const constants = {
130
+ CARD_OFFSET_PERCENTAGE: cardOffsetPercentage,
131
+ SWIPE_THRESHOLD: swipeThreshold,
132
+ MOBILE_BREAKPOINT: mobileBreakpoint,
133
+ AUTO_SCROLL_INTERVAL: autoScrollInterval,
134
+ };
135
+
136
+ // Performance: Memoize container width to prevent recalculation on every render
137
+ // This is especially important during swipe operations where the component re-renders
138
+ // frequently. Without memoization, it'd query the DOM on every frame while swiping.
139
+ useEffect(() => {
140
+ const updateWidth = () => {
141
+ setContainerWidth(containerRef.current?.offsetWidth || window.innerWidth);
142
+ };
143
+ updateWidth(); // Initial
144
+ window.addEventListener("resize", updateWidth);
145
+ return () => window.removeEventListener("resize", updateWidth);
146
+ }, []);
147
+
148
+ // Navigation functions
149
+ const nextSlide = useCallback(() => {
150
+ if (itemCount === 0) return;
151
+ setCurrentIndex(prev => (prev + 1) % itemCount);
152
+ }, [itemCount]);
153
+
154
+ const prevSlide = useCallback(() => {
155
+ if (itemCount === 0) return;
156
+ setCurrentIndex(prev => (prev === 0 ? itemCount - 1 : prev - 1));
157
+ }, [itemCount]);
158
+
159
+ const goToSlide = useCallback(
160
+ (index: number) => {
161
+ if (index < 0 || index >= itemCount) return;
162
+ setCurrentIndex(index);
163
+ },
164
+ [itemCount]
165
+ );
166
+
167
+ // Touch handlers for mobile swipe
168
+ const handleTouchStart = useCallback((e: React.TouchEvent) => {
169
+ touchStartX.current = e.touches[0].clientX;
170
+ setIsSwiping(true);
171
+ // Pause auto-scroll during user interaction
172
+ if (timeoutRef.current) {
173
+ clearInterval(timeoutRef.current);
174
+ }
175
+ }, []);
176
+
177
+ const handleTouchMove = useCallback(
178
+ (e: React.TouchEvent) => {
179
+ if (!isSwiping) return;
180
+ const currentX = e.touches[0].clientX;
181
+ const diff = currentX - touchStartX.current;
182
+ setSwipeOffset(diff);
183
+ },
184
+ [isSwiping]
185
+ );
186
+
187
+ const handleTouchEnd = useCallback(() => {
188
+ setIsSwiping(false);
189
+ // Use memoized containerWidth and constant threshold for performance
190
+ const threshold = containerWidth * swipeThreshold;
191
+
192
+ // Determine if swipe was strong enough to change slides
193
+ if (swipeOffset > threshold) {
194
+ prevSlide();
195
+ } else if (swipeOffset < -threshold) {
196
+ nextSlide();
197
+ }
198
+
199
+ // Reset swipe offset to return card to snapped position
200
+ setSwipeOffset(0);
201
+
202
+ // Restart auto-scroll after user interaction completes
203
+ if (enableAutoScroll && autoScrollInterval > 0) {
204
+ timeoutRef.current = setInterval(() => {
205
+ nextSlide();
206
+ }, autoScrollInterval);
207
+ }
208
+ }, [
209
+ swipeOffset,
210
+ containerWidth,
211
+ swipeThreshold,
212
+ prevSlide,
213
+ nextSlide,
214
+ enableAutoScroll,
215
+ autoScrollInterval,
216
+ ]);
217
+
218
+ // Performance: Detect mobile viewport and update on resize
219
+ // This replaces inline window.innerWidth checks that were running on every render.
220
+ // By using state and a resize listener, we only check when the viewport actually changes.
221
+ useEffect(() => {
222
+ const checkMobile = () => {
223
+ setIsMobile(window.innerWidth < mobileBreakpoint);
224
+ };
225
+
226
+ // Check immediately on mount
227
+ checkMobile();
228
+
229
+ // Update when window is resized (e.g., device rotation, browser resize)
230
+ window.addEventListener("resize", checkMobile);
231
+ return () => window.removeEventListener("resize", checkMobile);
232
+ }, [mobileBreakpoint]);
233
+
234
+ // Auto-scroll logic
235
+ useEffect(() => {
236
+ if (!enableAutoScroll || itemCount === 0 || autoScrollInterval === 0) {
237
+ return;
238
+ }
239
+
240
+ timeoutRef.current = setInterval(() => {
241
+ nextSlide();
242
+ }, autoScrollInterval);
243
+
244
+ return () => {
245
+ if (timeoutRef.current) clearInterval(timeoutRef.current);
246
+ };
247
+ }, [nextSlide, itemCount, enableAutoScroll, autoScrollInterval]);
248
+
249
+ return {
250
+ currentIndex,
251
+ swipeOffset,
252
+ isSwiping,
253
+ isMobile,
254
+ containerWidth,
255
+ containerRef,
256
+ nextSlide,
257
+ prevSlide,
258
+ goToSlide,
259
+ handleTouchStart,
260
+ handleTouchMove,
261
+ handleTouchEnd,
262
+ constants,
263
+ };
264
+ }
@@ -1,17 +1,17 @@
1
- import { RefObject, useEffect } from "react";
2
-
3
- export const useOutsideClick = (ref: RefObject<any>, callback: () => void) => {
4
- const handleClick = (e: MouseEvent) => {
5
- if (!ref?.current?.contains(e.target)) {
6
- callback();
7
- }
8
- };
9
-
10
- useEffect(() => {
11
- document.addEventListener("click", handleClick);
12
-
13
- return () => {
14
- document.removeEventListener("click", handleClick);
15
- };
16
- });
17
- };
1
+ import { RefObject, useEffect } from "react";
2
+
3
+ export const useOutsideClick = (ref: RefObject<any>, callback: () => void) => {
4
+ const handleClick = (e: MouseEvent) => {
5
+ if (!ref?.current?.contains(e.target)) {
6
+ callback();
7
+ }
8
+ };
9
+
10
+ useEffect(() => {
11
+ document.addEventListener("click", handleClick);
12
+
13
+ return () => {
14
+ document.removeEventListener("click", handleClick);
15
+ };
16
+ });
17
+ };