@windstream/react-shared-components 0.1.91 → 0.1.93

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 (214) hide show
  1. package/README.md +635 -635
  2. package/dist/contentful/index.d.ts +1 -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 +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.esm.js +5 -13
  10. package/dist/index.esm.js.map +1 -1
  11. package/dist/index.js +5 -13
  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/animation-wrapper/index.tsx +129 -129
  30. package/src/components/animation-wrapper/types.ts +11 -11
  31. package/src/components/brand-button/BrandButton.stories.tsx +223 -223
  32. package/src/components/brand-button/helpers.ts +35 -35
  33. package/src/components/brand-button/index.tsx +120 -120
  34. package/src/components/brand-button/types.ts +38 -38
  35. package/src/components/button/Button.stories.tsx +108 -108
  36. package/src/components/button/index.tsx +27 -27
  37. package/src/components/button/types.ts +14 -14
  38. package/src/components/call-button/CallButton.stories.tsx +324 -324
  39. package/src/components/call-button/index.tsx +106 -106
  40. package/src/components/call-button/types.ts +16 -16
  41. package/src/components/checkbox/Checkbox.stories.tsx +247 -247
  42. package/src/components/checkbox/index.tsx +197 -197
  43. package/src/components/checkbox/types.ts +27 -27
  44. package/src/components/checklist/Checklist.stories.tsx +150 -150
  45. package/src/components/checklist/index.tsx +61 -61
  46. package/src/components/checklist/types.ts +17 -17
  47. package/src/components/collapse/Collapse.stories.tsx +255 -255
  48. package/src/components/collapse/index.tsx +46 -46
  49. package/src/components/collapse/types.ts +6 -6
  50. package/src/components/divider/Divider.stories.tsx +205 -205
  51. package/src/components/divider/index.tsx +22 -22
  52. package/src/components/divider/type.ts +3 -3
  53. package/src/components/image/Image.stories.tsx +113 -113
  54. package/src/components/image/index.tsx +25 -25
  55. package/src/components/image/types.ts +40 -40
  56. package/src/components/input/Input.stories.tsx +325 -325
  57. package/src/components/input/index.tsx +177 -177
  58. package/src/components/input/types.ts +37 -37
  59. package/src/components/link/Link.stories.tsx +163 -163
  60. package/src/components/link/index.tsx +116 -116
  61. package/src/components/link/types.ts +25 -25
  62. package/src/components/list/List.stories.tsx +272 -272
  63. package/src/components/list/index.tsx +88 -88
  64. package/src/components/list/list-item/index.tsx +38 -38
  65. package/src/components/list/list-item/types.ts +13 -13
  66. package/src/components/list/types.ts +29 -29
  67. package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
  68. package/src/components/material-icon/constants.ts +99 -99
  69. package/src/components/material-icon/index.tsx +47 -47
  70. package/src/components/material-icon/types.ts +31 -31
  71. package/src/components/modal/Modal.stories.tsx +171 -171
  72. package/src/components/modal/index.tsx +164 -164
  73. package/src/components/modal/types.ts +24 -24
  74. package/src/components/next-image/index.tsx +74 -74
  75. package/src/components/next-image/types.ts +1 -1
  76. package/src/components/pagination/index.tsx +91 -91
  77. package/src/components/pagination/types.ts +6 -6
  78. package/src/components/radio-button/RadioButton.stories.tsx +307 -307
  79. package/src/components/radio-button/index.tsx +75 -75
  80. package/src/components/radio-button/types.ts +21 -21
  81. package/src/components/see-more/SeeMore.stories.tsx +181 -181
  82. package/src/components/see-more/index.tsx +44 -44
  83. package/src/components/see-more/types.ts +4 -4
  84. package/src/components/select/Select.stories.tsx +411 -411
  85. package/src/components/select/index.tsx +155 -155
  86. package/src/components/select/types.ts +36 -36
  87. package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
  88. package/src/components/select-plan-button/index.tsx +63 -63
  89. package/src/components/select-plan-button/types.ts +17 -17
  90. package/src/components/skeleton/Skeleton.stories.tsx +179 -179
  91. package/src/components/skeleton/index.tsx +61 -61
  92. package/src/components/skeleton/types.ts +4 -4
  93. package/src/components/spinner/Spinner.stories.tsx +335 -335
  94. package/src/components/spinner/index.tsx +44 -44
  95. package/src/components/spinner/types.ts +5 -5
  96. package/src/components/text/Text.stories.tsx +321 -321
  97. package/src/components/text/index.tsx +25 -25
  98. package/src/components/text/types.ts +45 -45
  99. package/src/components/tooltip/Tooltip.stories.tsx +219 -219
  100. package/src/components/tooltip/index.tsx +74 -74
  101. package/src/components/tooltip/types.ts +7 -7
  102. package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
  103. package/src/components/view-cart-button/index.tsx +42 -42
  104. package/src/components/view-cart-button/types.ts +5 -5
  105. package/src/contentful/blocks/accordion/Accordion.stories.mocks.tsx +128 -128
  106. package/src/contentful/blocks/accordion/Accordion.stories.tsx +98 -98
  107. package/src/contentful/blocks/accordion/index.tsx +112 -112
  108. package/src/contentful/blocks/accordion/types.ts +34 -34
  109. package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
  110. package/src/contentful/blocks/address-input-banner/types.ts +14 -14
  111. package/src/contentful/blocks/anchored-bottom-banner/index.tsx +181 -181
  112. package/src/contentful/blocks/anchored-bottom-banner/types.ts +13 -13
  113. package/src/contentful/blocks/blogs-grid/BlogGrid.stories.mocks.tsx +144 -144
  114. package/src/contentful/blocks/blogs-grid/BlogGrid.stories.tsx +156 -156
  115. package/src/contentful/blocks/blogs-grid/index.tsx +134 -134
  116. package/src/contentful/blocks/blogs-grid/types.ts +26 -26
  117. package/src/contentful/blocks/blogs-grid-base/index.tsx +119 -119
  118. package/src/contentful/blocks/blogs-grid-base/types.ts +36 -36
  119. package/src/contentful/blocks/breadcrumbs/BreadcrumbNavigation.stories.tsx +147 -147
  120. package/src/contentful/blocks/breadcrumbs/index.tsx +95 -95
  121. package/src/contentful/blocks/breadcrumbs/types.ts +8 -8
  122. package/src/contentful/blocks/button/Button.stories.tsx +40 -40
  123. package/src/contentful/blocks/button/index.tsx +131 -131
  124. package/src/contentful/blocks/button/types.ts +39 -39
  125. package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
  126. package/src/contentful/blocks/callout/index.tsx +277 -277
  127. package/src/contentful/blocks/callout/types.ts +78 -78
  128. package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
  129. package/src/contentful/blocks/cards/blog-card/index.tsx +129 -129
  130. package/src/contentful/blocks/cards/blog-card/types.ts +34 -34
  131. package/src/contentful/blocks/cards/floating-image-card/index.tsx +119 -119
  132. package/src/contentful/blocks/cards/floating-image-card/types.ts +30 -30
  133. package/src/contentful/blocks/cards/full-image-card/index.tsx +130 -130
  134. package/src/contentful/blocks/cards/full-image-card/types.ts +29 -29
  135. package/src/contentful/blocks/cards/index.tsx +13 -13
  136. package/src/contentful/blocks/cards/product-card/index.tsx +251 -251
  137. package/src/contentful/blocks/cards/product-card/types.ts +28 -28
  138. package/src/contentful/blocks/cards/simple-card/index.tsx +325 -325
  139. package/src/contentful/blocks/cards/simple-card/types.ts +71 -71
  140. package/src/contentful/blocks/cards/testimonial-card/index.tsx +90 -90
  141. package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
  142. package/src/contentful/blocks/cards/types.ts +1 -1
  143. package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
  144. package/src/contentful/blocks/carousel/helper.tsx +494 -494
  145. package/src/contentful/blocks/carousel/index.tsx +87 -87
  146. package/src/contentful/blocks/carousel/types.ts +145 -145
  147. package/src/contentful/blocks/cart-retention-banner/index.tsx +109 -109
  148. package/src/contentful/blocks/cart-retention-banner/types.ts +11 -11
  149. package/src/contentful/blocks/comparison-table/index.tsx +29 -29
  150. package/src/contentful/blocks/comparison-table/types.ts +6 -6
  151. package/src/contentful/blocks/cookiebanner/index.tsx +146 -146
  152. package/src/contentful/blocks/cookiebanner/type.ts +7 -7
  153. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
  154. package/src/contentful/blocks/cta-callout/index.tsx +73 -73
  155. package/src/contentful/blocks/cta-callout/types.ts +26 -26
  156. package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
  157. package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
  158. package/src/contentful/blocks/email-input-block/index.tsx +116 -116
  159. package/src/contentful/blocks/email-input-block/types.ts +16 -16
  160. package/src/contentful/blocks/find-kinetic/FindKinetic.stories.tsx +23 -23
  161. package/src/contentful/blocks/find-kinetic/index.tsx +138 -130
  162. package/src/contentful/blocks/find-kinetic/types.ts +20 -19
  163. package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
  164. package/src/contentful/blocks/floating-banner/index.tsx +97 -97
  165. package/src/contentful/blocks/floating-banner/types.ts +22 -22
  166. package/src/contentful/blocks/footer/Footer.stories.tsx +317 -317
  167. package/src/contentful/blocks/footer/index.tsx +91 -91
  168. package/src/contentful/blocks/footer/types.ts +13 -13
  169. package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
  170. package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
  171. package/src/contentful/blocks/image-promo-bar/index.tsx +246 -246
  172. package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
  173. package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
  174. package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
  175. package/src/contentful/blocks/modal/constants.ts +53 -53
  176. package/src/contentful/blocks/modal/index.tsx +108 -107
  177. package/src/contentful/blocks/modal/types.ts +12 -12
  178. package/src/contentful/blocks/navigation/Navigation.stories.mocks.tsx +78 -0
  179. package/src/contentful/blocks/navigation/Navigation.stories.tsx +138 -0
  180. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +141 -139
  181. package/src/contentful/blocks/navigation/index.tsx +569 -568
  182. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
  183. package/src/contentful/blocks/navigation/types.ts +71 -71
  184. package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
  185. package/src/contentful/blocks/primary-hero/index.tsx +236 -236
  186. package/src/contentful/blocks/primary-hero/types.ts +37 -37
  187. package/src/contentful/blocks/search-block/index.tsx +90 -90
  188. package/src/contentful/blocks/search-block/types.ts +15 -15
  189. package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
  190. package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
  191. package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
  192. package/src/contentful/blocks/text/Text.stories.tsx +23 -23
  193. package/src/contentful/blocks/text/index.tsx +12 -12
  194. package/src/contentful/blocks/text/types.ts +1 -1
  195. package/src/contentful/index.ts +105 -105
  196. package/src/hooks/contentful/use-contentful-rich-text.tsx +309 -309
  197. package/src/hooks/contentful/use-processed-check-list.ts +63 -63
  198. package/src/hooks/use-body-scroll-lock.ts +34 -34
  199. package/src/hooks/use-carousel-swipe.ts +264 -264
  200. package/src/hooks/use-outside-click.ts +17 -17
  201. package/src/index.ts +107 -107
  202. package/src/next/index.ts +5 -5
  203. package/src/setupTests.ts +46 -46
  204. package/src/stories/DocsTemplate.tsx +24 -24
  205. package/src/styles/globals.css +343 -343
  206. package/src/types/global.d.ts +9 -9
  207. package/src/types/micro-components.ts +99 -99
  208. package/src/types/utm.ts +49 -49
  209. package/src/utils/contentful/to-document.ts +24 -24
  210. package/src/utils/cookie.ts +84 -84
  211. package/src/utils/cx.ts +49 -49
  212. package/src/utils/index.ts +41 -41
  213. package/src/utils/speed-card-bg.ts +24 -24
  214. 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
+ };