@utilitywarehouse/hearth-react-native 0.4.2 → 0.6.0

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 (207) hide show
  1. package/.storybook/main.ts +21 -1
  2. package/.turbo/turbo-build.log +1 -1
  3. package/.turbo/turbo-lint.log +1 -1
  4. package/CHANGELOG.md +52 -0
  5. package/build/components/Alert/AlertTitle.js +6 -6
  6. package/build/components/Badge/Badge.js +3 -3
  7. package/build/components/Badge/Badge.props.d.ts +1 -0
  8. package/build/components/Button/ButtonRoot.js +4 -0
  9. package/build/components/Button/ButtonText.js +2 -2
  10. package/build/components/Card/CardRoot.js +1 -1
  11. package/build/components/Carousel/Carousel.context.d.ts +4 -0
  12. package/build/components/Carousel/Carousel.context.js +4 -0
  13. package/build/components/Carousel/Carousel.d.ts +6 -0
  14. package/build/components/Carousel/Carousel.js +278 -0
  15. package/build/components/Carousel/Carousel.props.d.ts +65 -0
  16. package/build/components/Carousel/Carousel.props.js +1 -0
  17. package/build/components/Carousel/CarouselControlItem.d.ts +24 -0
  18. package/build/components/Carousel/CarouselControlItem.js +64 -0
  19. package/build/components/Carousel/CarouselControls.d.ts +4 -0
  20. package/build/components/Carousel/CarouselControls.js +74 -0
  21. package/build/components/Carousel/CarouselItem.d.ts +6 -0
  22. package/build/components/Carousel/CarouselItem.js +38 -0
  23. package/build/components/Carousel/index.d.ts +5 -0
  24. package/build/components/Carousel/index.js +5 -0
  25. package/build/components/Container/Container.d.ts +6 -0
  26. package/build/components/Container/Container.js +40 -0
  27. package/build/components/Container/Container.props.d.ts +85 -0
  28. package/build/components/Container/Container.props.js +1 -0
  29. package/build/components/Container/index.d.ts +2 -0
  30. package/build/components/Container/index.js +1 -0
  31. package/build/components/DescriptionList/DescriptionList.d.ts +1 -1
  32. package/build/components/DescriptionList/DescriptionList.js +2 -2
  33. package/build/components/DescriptionList/DescriptionList.props.d.ts +1 -8
  34. package/build/components/DescriptionList/DescriptionListItem.d.ts +1 -1
  35. package/build/components/DescriptionList/DescriptionListItem.js +4 -3
  36. package/build/components/DescriptionList/DescriptionListItem.props.d.ts +3 -8
  37. package/build/components/IndicatorIconButton/IndicatorIconButton.d.ts +6 -0
  38. package/build/components/IndicatorIconButton/IndicatorIconButton.js +26 -0
  39. package/build/components/IndicatorIconButton/IndicatorIconButton.props.d.ts +8 -0
  40. package/build/components/IndicatorIconButton/IndicatorIconButton.props.js +1 -0
  41. package/build/components/IndicatorIconButton/index.d.ts +2 -0
  42. package/build/components/IndicatorIconButton/index.js +1 -0
  43. package/build/components/Link/LinkText.js +3 -3
  44. package/build/components/List/List.context.d.ts +0 -2
  45. package/build/components/List/List.d.ts +1 -1
  46. package/build/components/List/List.js +5 -5
  47. package/build/components/List/List.props.d.ts +1 -9
  48. package/build/components/List/ListAction/ListAction.d.ts +18 -0
  49. package/build/components/List/ListAction/ListAction.js +103 -0
  50. package/build/components/List/ListAction/ListAction.props.d.ts +8 -0
  51. package/build/components/List/ListAction/ListAction.props.js +1 -0
  52. package/build/components/List/ListAction/ListActionContent.d.ts +6 -0
  53. package/build/components/List/ListAction/ListActionContent.js +14 -0
  54. package/build/components/List/ListAction/ListActionText.d.ts +6 -0
  55. package/build/components/List/ListAction/ListActionText.js +7 -0
  56. package/build/components/List/ListAction/ListActionTrailingContent.d.ts +6 -0
  57. package/build/components/List/ListAction/ListActionTrailingContent.js +5 -0
  58. package/build/components/List/ListAction/ListActionTrailingIcon.d.ts +9 -0
  59. package/build/components/List/ListAction/ListActionTrailingIcon.js +18 -0
  60. package/build/components/List/ListAction/index.d.ts +6 -0
  61. package/build/components/List/ListAction/index.js +5 -0
  62. package/build/components/List/ListItem/ListItem.context.d.ts +1 -1
  63. package/build/components/List/ListItem/ListItem.props.d.ts +9 -5
  64. package/build/components/List/ListItem/ListItemRoot.d.ts +1 -1
  65. package/build/components/List/ListItem/ListItemRoot.js +10 -12
  66. package/build/components/List/ListItem/index.d.ts +4 -4
  67. package/build/components/List/ListItem/index.js +3 -3
  68. package/build/components/List/index.d.ts +1 -0
  69. package/build/components/List/index.js +1 -0
  70. package/build/components/ProgressStepper/ProgressStep.d.ts +10 -0
  71. package/build/components/ProgressStepper/ProgressStep.js +100 -0
  72. package/build/components/ProgressStepper/ProgressStepper.d.ts +6 -0
  73. package/build/components/ProgressStepper/ProgressStepper.js +22 -0
  74. package/build/components/ProgressStepper/ProgressStepper.props.d.ts +22 -0
  75. package/build/components/ProgressStepper/ProgressStepper.props.js +1 -0
  76. package/build/components/ProgressStepper/ProgressStepperRoot.d.ts +6 -0
  77. package/build/components/ProgressStepper/ProgressStepperRoot.js +16 -0
  78. package/build/components/ProgressStepper/index.d.ts +3 -0
  79. package/build/components/ProgressStepper/index.js +2 -0
  80. package/build/components/SectionHeader/SectionHeader.d.ts +1 -1
  81. package/build/components/SectionHeader/SectionHeader.js +6 -3
  82. package/build/components/SectionHeader/SectionHeader.props.d.ts +9 -16
  83. package/build/components/SectionHeader/SectionHeaderTrailingContent.d.ts +6 -0
  84. package/build/components/SectionHeader/SectionHeaderTrailingContent.js +13 -0
  85. package/build/components/SectionHeader/index.d.ts +1 -0
  86. package/build/components/SectionHeader/index.js +1 -0
  87. package/build/components/Tabs/Tab.js +2 -2
  88. package/build/components/ThemedImage/ThemedImage.d.ts +12 -0
  89. package/build/components/ThemedImage/ThemedImage.js +27 -0
  90. package/build/components/ThemedImage/ThemedImage.props.d.ts +13 -0
  91. package/build/components/ThemedImage/ThemedImage.props.js +1 -0
  92. package/build/components/ThemedImage/index.d.ts +2 -0
  93. package/build/components/ThemedImage/index.js +1 -0
  94. package/build/components/ToggleButton/ToggleButtonText.js +2 -2
  95. package/build/components/UnstyledIconButton/UnstyledIconButton.props.d.ts +4 -1
  96. package/build/components/index.d.ts +5 -0
  97. package/build/components/index.js +5 -0
  98. package/build/core/themes.d.ts +12 -24
  99. package/build/hooks/useStyleProps.js +1 -1
  100. package/build/tokens/components/dark/button.d.ts +1 -1
  101. package/build/tokens/components/dark/button.js +1 -1
  102. package/build/tokens/components/dark/dialog.d.ts +1 -0
  103. package/build/tokens/components/dark/dialog.js +1 -0
  104. package/build/tokens/components/dark/illustrations.d.ts +1 -0
  105. package/build/tokens/components/dark/illustrations.js +1 -0
  106. package/build/tokens/components/dark/toast.d.ts +4 -1
  107. package/build/tokens/components/dark/toast.js +4 -1
  108. package/build/tokens/components/light/button.d.ts +1 -1
  109. package/build/tokens/components/light/button.js +1 -1
  110. package/build/tokens/components/light/dialog.d.ts +1 -0
  111. package/build/tokens/components/light/dialog.js +1 -0
  112. package/build/tokens/components/light/illustrations.d.ts +1 -0
  113. package/build/tokens/components/light/illustrations.js +1 -0
  114. package/build/tokens/components/light/toast.d.ts +4 -1
  115. package/build/tokens/components/light/toast.js +4 -1
  116. package/build/tokens/layout.d.ts +6 -12
  117. package/build/tokens/layout.js +3 -6
  118. package/docs/components/AllComponents.web.tsx +122 -5
  119. package/docs/components/BadgeList.tsx +20 -56
  120. package/docs/components/SwitchList.tsx +4 -8
  121. package/docs/getting-started.mdx +30 -14
  122. package/docs/introduction.mdx +1 -1
  123. package/docs/layout-components.docs.mdx +30 -0
  124. package/package.json +6 -4
  125. package/src/components/Alert/AlertTitle.tsx +7 -7
  126. package/src/components/Badge/Badge.props.ts +1 -0
  127. package/src/components/Badge/Badge.tsx +3 -2
  128. package/src/components/Button/ButtonRoot.tsx +4 -0
  129. package/src/components/Button/ButtonText.tsx +3 -3
  130. package/src/components/Card/CardRoot.tsx +2 -0
  131. package/src/components/Carousel/Carousel.context.tsx +8 -0
  132. package/src/components/Carousel/Carousel.docs.mdx +389 -0
  133. package/src/components/Carousel/Carousel.props.ts +89 -0
  134. package/src/components/Carousel/Carousel.stories.tsx +317 -0
  135. package/src/components/Carousel/Carousel.tsx +444 -0
  136. package/src/components/Carousel/CarouselControlItem.tsx +87 -0
  137. package/src/components/Carousel/CarouselControls.tsx +150 -0
  138. package/src/components/Carousel/CarouselItem.tsx +68 -0
  139. package/src/components/Carousel/index.ts +6 -0
  140. package/src/components/Container/Container.docs.mdx +168 -0
  141. package/src/components/Container/Container.props.ts +89 -0
  142. package/src/components/Container/Container.stories.tsx +274 -0
  143. package/src/components/Container/Container.tsx +52 -0
  144. package/src/components/Container/index.tsx +2 -0
  145. package/src/components/DescriptionList/DescriptionList.docs.mdx +24 -27
  146. package/src/components/DescriptionList/DescriptionList.props.ts +1 -8
  147. package/src/components/DescriptionList/DescriptionList.stories.tsx +13 -19
  148. package/src/components/DescriptionList/DescriptionList.tsx +2 -14
  149. package/src/components/DescriptionList/DescriptionListItem.props.ts +3 -8
  150. package/src/components/DescriptionList/DescriptionListItem.tsx +13 -21
  151. package/src/components/IndicatorIconButton/IndicatorIconButton.docs.mdx +85 -0
  152. package/src/components/IndicatorIconButton/IndicatorIconButton.props.ts +12 -0
  153. package/src/components/IndicatorIconButton/IndicatorIconButton.stories.tsx +142 -0
  154. package/src/components/IndicatorIconButton/IndicatorIconButton.tsx +36 -0
  155. package/src/components/IndicatorIconButton/index.tsx +2 -0
  156. package/src/components/Link/LinkText.tsx +4 -4
  157. package/src/components/List/List.context.ts +0 -1
  158. package/src/components/List/List.docs.mdx +376 -179
  159. package/src/components/List/List.props.ts +1 -9
  160. package/src/components/List/List.stories.tsx +289 -38
  161. package/src/components/List/List.tsx +5 -26
  162. package/src/components/List/ListAction/ListAction.props.ts +10 -0
  163. package/src/components/List/ListAction/ListAction.tsx +133 -0
  164. package/src/components/List/ListAction/ListActionContent.tsx +21 -0
  165. package/src/components/List/ListAction/ListActionText.tsx +14 -0
  166. package/src/components/List/ListAction/ListActionTrailingContent.tsx +9 -0
  167. package/src/components/List/ListAction/ListActionTrailingIcon.tsx +32 -0
  168. package/src/components/List/ListAction/index.ts +6 -0
  169. package/src/components/List/ListItem/ListItem.context.ts +1 -1
  170. package/src/components/List/ListItem/ListItem.props.ts +9 -5
  171. package/src/components/List/ListItem/ListItemRoot.tsx +18 -14
  172. package/src/components/List/ListItem/index.ts +4 -4
  173. package/src/components/List/index.ts +1 -0
  174. package/src/components/ProgressStepper/ProgressStep.tsx +134 -0
  175. package/src/components/ProgressStepper/ProgressStepper.docs.mdx +87 -0
  176. package/src/components/ProgressStepper/ProgressStepper.props.ts +27 -0
  177. package/src/components/ProgressStepper/ProgressStepper.stories.tsx +108 -0
  178. package/src/components/ProgressStepper/ProgressStepper.tsx +26 -0
  179. package/src/components/ProgressStepper/ProgressStepperRoot.tsx +32 -0
  180. package/src/components/ProgressStepper/index.ts +3 -0
  181. package/src/components/SectionHeader/SectionHeader.props.ts +9 -16
  182. package/src/components/SectionHeader/SectionHeader.stories.tsx +28 -18
  183. package/src/components/SectionHeader/SectionHeader.tsx +18 -19
  184. package/src/components/SectionHeader/SectionHeaderTrailingContent.tsx +20 -0
  185. package/src/components/SectionHeader/Sectionheader.docs.mdx +9 -24
  186. package/src/components/SectionHeader/index.ts +1 -0
  187. package/src/components/Switch/Switch.docs.mdx +0 -4
  188. package/src/components/Tabs/Tab.tsx +4 -2
  189. package/src/components/ThemedImage/ThemedImage.docs.mdx +208 -0
  190. package/src/components/ThemedImage/ThemedImage.props.ts +15 -0
  191. package/src/components/ThemedImage/ThemedImage.stories.tsx +175 -0
  192. package/src/components/ThemedImage/ThemedImage.tsx +34 -0
  193. package/src/components/ThemedImage/index.tsx +2 -0
  194. package/src/components/ToggleButton/ToggleButtonText.tsx +3 -3
  195. package/src/components/UnstyledIconButton/UnstyledIconButton.props.ts +2 -1
  196. package/src/components/index.ts +5 -0
  197. package/src/hooks/useStyleProps.ts +1 -1
  198. package/src/tokens/components/dark/button.ts +1 -1
  199. package/src/tokens/components/dark/dialog.ts +1 -0
  200. package/src/tokens/components/dark/illustrations.ts +1 -0
  201. package/src/tokens/components/dark/toast.ts +4 -1
  202. package/src/tokens/components/light/button.ts +1 -1
  203. package/src/tokens/components/light/dialog.ts +1 -0
  204. package/src/tokens/components/light/illustrations.ts +1 -0
  205. package/src/tokens/components/light/toast.ts +4 -1
  206. package/src/tokens/layout.ts +3 -6
  207. package/src/vite-env.d.ts +6 -0
@@ -0,0 +1,444 @@
1
+ import {
2
+ Children,
3
+ cloneElement,
4
+ ReactElement,
5
+ useCallback,
6
+ useEffect,
7
+ useImperativeHandle,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
12
+ import {
13
+ FlatList,
14
+ NativeScrollEvent,
15
+ NativeSyntheticEvent,
16
+ Platform,
17
+ ScrollView,
18
+ View,
19
+ ViewStyle,
20
+ ViewToken,
21
+ } from 'react-native';
22
+ import { StyleSheet } from 'react-native-unistyles';
23
+
24
+ import CarouselContext from './Carousel.context';
25
+ import { CarouselItemProps, CarouselProps } from './Carousel.props';
26
+ import { CarouselControls } from './CarouselControls';
27
+ import { CarouselItem } from './CarouselItem';
28
+
29
+ const Carousel = ({
30
+ centered = false,
31
+ children,
32
+ disabled = false,
33
+ inactiveItemOpacity = 1,
34
+ itemWidth,
35
+ onSnapToItem,
36
+ showOverflow = false,
37
+ style,
38
+ width,
39
+ itemsStyle,
40
+ activeIndex: initialActiveIndex = 0,
41
+ showControls = true,
42
+ showNavigation = false,
43
+ controlsStyle,
44
+ controlsItemStyle,
45
+ controlsActiveItemStyle,
46
+ controlsAccessibilityHidden = true,
47
+ inverted = false,
48
+ ref,
49
+ ...props
50
+ }: CarouselProps) => {
51
+ const [activeIndex, setActiveIndex] = useState(initialActiveIndex);
52
+ const [numItems, setNumItems] = useState(0);
53
+
54
+ const scrollViewRef = useRef<ScrollView>(null);
55
+ const flatListRef = useRef<FlatList>(null);
56
+ const isWeb = Platform.OS === 'web';
57
+ const isProgrammaticScroll = useRef(false);
58
+
59
+ // Expose scroll methods through ref
60
+ useImperativeHandle(
61
+ ref,
62
+ () => ({
63
+ scrollToIndex: ({ index, animated = true }: { index: number; animated?: boolean | null }) => {
64
+ if (isWeb && scrollViewRef.current) {
65
+ isProgrammaticScroll.current = true;
66
+ const itemWidthValue = itemWidth || width;
67
+ const offset = centered ? (width - (itemWidth || width)) / 2 : 0;
68
+ const scrollX = index * itemWidthValue - offset;
69
+ scrollViewRef.current.scrollTo({ x: scrollX, animated: animated ?? true });
70
+ setTimeout(() => {
71
+ isProgrammaticScroll.current = false;
72
+ }, 500);
73
+ } else if (flatListRef.current) {
74
+ flatListRef.current.scrollToIndex({ index, animated: animated ?? true });
75
+ }
76
+ setActiveIndex(index);
77
+ },
78
+ scrollToOffset: ({
79
+ offset,
80
+ animated = true,
81
+ }: {
82
+ offset: number;
83
+ animated?: boolean | null;
84
+ }) => {
85
+ if (isWeb && scrollViewRef.current) {
86
+ isProgrammaticScroll.current = true;
87
+ scrollViewRef.current.scrollTo({ x: offset, animated: animated ?? true });
88
+ setTimeout(() => {
89
+ isProgrammaticScroll.current = false;
90
+ }, 500);
91
+ } else if (flatListRef.current) {
92
+ flatListRef.current.scrollToOffset({ offset, animated: animated ?? true });
93
+ }
94
+ },
95
+ }),
96
+ [isWeb, itemWidth, width, centered]
97
+ );
98
+
99
+ // Recursively check if CarouselControls exists in the children tree
100
+ const hasCarouselControlsInTree = (node: React.ReactNode): boolean => {
101
+ if (!node) return false;
102
+
103
+ if (typeof node === 'object' && 'type' in node) {
104
+ if (
105
+ node.type === CarouselControls ||
106
+ (node.type as any)?.displayName === 'CarouselControls'
107
+ ) {
108
+ return true;
109
+ }
110
+
111
+ // Check children recursively
112
+ if ('props' in node && node.props && typeof node.props === 'object') {
113
+ const props = node.props as { children?: React.ReactNode };
114
+ if (props.children) {
115
+ const childArray = Children.toArray(props.children);
116
+ return childArray.some(child => hasCarouselControlsInTree(child));
117
+ }
118
+ }
119
+ }
120
+
121
+ return false;
122
+ };
123
+
124
+ // Filter only CarouselItem children for the carousel
125
+ const { carouselItems, otherChildren, hasCarouselControls } = useMemo(() => {
126
+ const childrenArray = Children.toArray(children);
127
+ const carouselItems: Array<ReactElement<CarouselItemProps>> = [];
128
+ const otherChildren: Array<React.ReactNode> = [];
129
+
130
+ childrenArray.forEach(child => {
131
+ if (
132
+ typeof child === 'object' &&
133
+ 'type' in child &&
134
+ (child.type === CarouselItem || (child.type as any)?.displayName === 'CarouselItem')
135
+ ) {
136
+ carouselItems.push(child as ReactElement<CarouselItemProps>);
137
+ } else {
138
+ otherChildren.push(child);
139
+ }
140
+ });
141
+
142
+ // Recursively check if any CarouselControls exist in the tree
143
+ const hasCarouselControls = childrenArray.some(child => hasCarouselControlsInTree(child));
144
+
145
+ return { carouselItems, otherChildren, hasCarouselControls };
146
+ }, [children]);
147
+
148
+ const innerMargin: number = width - (itemWidth || width);
149
+ const containerStyles: ViewStyle = {
150
+ marginHorizontal: centered ? innerMargin / 2 : 0,
151
+ overflow: showOverflow ? 'visible' : 'hidden',
152
+ };
153
+
154
+ const context = useMemo(
155
+ () => ({
156
+ activeIndex,
157
+ numItems: carouselItems.length,
158
+ setActiveIndex,
159
+ setNumItems,
160
+ controlsAccessibilityHidden,
161
+ inverted,
162
+ disabled,
163
+ }),
164
+ [
165
+ activeIndex,
166
+ carouselItems.length,
167
+ setActiveIndex,
168
+ setNumItems,
169
+ controlsAccessibilityHidden,
170
+ inverted,
171
+ disabled,
172
+ ]
173
+ );
174
+
175
+ // On web, we need to prevent overflow from expanding the container
176
+ const webContainerStyles: ViewStyle = isWeb
177
+ ? {
178
+ width,
179
+ maxWidth: width,
180
+ overflow: 'hidden', // Always clip on web to prevent expansion
181
+ }
182
+ : {};
183
+
184
+ // For web centered layouts, add padding to content container
185
+ const webContentContainerStyle: ViewStyle =
186
+ isWeb && centered
187
+ ? {
188
+ paddingHorizontal: innerMargin / 2,
189
+ }
190
+ : {};
191
+
192
+ useEffect(() => {
193
+ setNumItems((carouselItems || []).length);
194
+ }, [carouselItems, setNumItems]);
195
+
196
+ // Set initial scroll position on web
197
+ useEffect(() => {
198
+ if (isWeb && scrollViewRef.current && initialActiveIndex > 0) {
199
+ // Use setTimeout to ensure ScrollView is mounted
200
+ setTimeout(() => {
201
+ if (scrollViewRef.current) {
202
+ const itemWidthValue = itemWidth || width;
203
+ const offset = centered ? innerMargin / 2 : 0;
204
+ const scrollX = initialActiveIndex * itemWidthValue - offset;
205
+ scrollViewRef.current.scrollTo({ x: scrollX, animated: false });
206
+ }
207
+ }, 0);
208
+ }
209
+ }, [isWeb, initialActiveIndex, itemWidth, width, centered, innerMargin]);
210
+
211
+ // Scroll to active index when it changes (for programmatic navigation)
212
+ useEffect(() => {
213
+ if (!isProgrammaticScroll.current) {
214
+ isProgrammaticScroll.current = true;
215
+
216
+ if (isWeb && scrollViewRef.current) {
217
+ const itemWidthValue = itemWidth || width;
218
+ const offset = centered ? innerMargin / 2 : 0;
219
+ const scrollX = activeIndex * itemWidthValue - offset;
220
+
221
+ scrollViewRef.current.scrollTo({ x: scrollX, animated: true });
222
+ } else if (
223
+ !isWeb &&
224
+ flatListRef.current &&
225
+ activeIndex >= 0 &&
226
+ activeIndex < carouselItems.length
227
+ ) {
228
+ flatListRef.current.scrollToIndex({ index: activeIndex, animated: true });
229
+ }
230
+
231
+ setTimeout(() => {
232
+ isProgrammaticScroll.current = false;
233
+ }, 500);
234
+ }
235
+ }, [activeIndex, isWeb, itemWidth, width, centered, innerMargin, carouselItems.length]);
236
+
237
+ // Web scroll handler - track scroll position
238
+ const handleWebScroll = useCallback(
239
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
240
+ if (!isWeb || isProgrammaticScroll.current) return;
241
+
242
+ const scrollX = event.nativeEvent.contentOffset.x;
243
+ const itemWidthValue = itemWidth || width;
244
+
245
+ // For centered layouts, account for the padding offset
246
+ const offset = centered ? innerMargin / 2 : 0;
247
+ const index = Math.round((scrollX + offset) / itemWidthValue);
248
+
249
+ if (index >= 0 && index < carouselItems.length && index !== activeIndex) {
250
+ setActiveIndex?.(index);
251
+ }
252
+ },
253
+ [
254
+ activeIndex,
255
+ centered,
256
+ innerMargin,
257
+ isWeb,
258
+ itemWidth,
259
+ carouselItems.length,
260
+ setActiveIndex,
261
+ width,
262
+ ]
263
+ );
264
+
265
+ // Web scroll end handler - update when scroll completes
266
+ const handleWebScrollEnd = useCallback(
267
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
268
+ if (!isWeb || isProgrammaticScroll.current) return;
269
+
270
+ const scrollX = event.nativeEvent.contentOffset.x;
271
+ const itemWidthValue = itemWidth || width;
272
+
273
+ // For centered layouts, account for the padding offset
274
+ const offset = centered ? innerMargin / 2 : 0;
275
+ const index = Math.round((scrollX + offset) / itemWidthValue);
276
+
277
+ if (index >= 0 && index < carouselItems.length) {
278
+ setActiveIndex?.(index);
279
+ onSnapToItem?.(index);
280
+ }
281
+ },
282
+ [
283
+ centered,
284
+ innerMargin,
285
+ isWeb,
286
+ itemWidth,
287
+ carouselItems.length,
288
+ onSnapToItem,
289
+ setActiveIndex,
290
+ width,
291
+ ]
292
+ );
293
+
294
+ // Native viewable items handler
295
+ const handleViewableItemsChanged = useCallback(
296
+ ({ viewableItems }: { viewableItems: Array<ViewToken> }) => {
297
+ if (!viewableItems.length) {
298
+ return;
299
+ }
300
+
301
+ const index = viewableItems[viewableItems.length - 1].index || 0;
302
+
303
+ setActiveIndex?.(index);
304
+ onSnapToItem?.(index);
305
+ },
306
+ [onSnapToItem, setActiveIndex]
307
+ );
308
+
309
+ const controls = (
310
+ <CarouselControls
311
+ style={[styles.controls, controlsStyle]}
312
+ itemStyle={controlsItemStyle}
313
+ activeItemStyle={controlsActiveItemStyle}
314
+ showNavigation={showNavigation}
315
+ accessibilityHidden={controlsAccessibilityHidden}
316
+ />
317
+ );
318
+
319
+ // Render for web using ScrollView with scroll snap
320
+ if (isWeb) {
321
+ return (
322
+ <CarouselContext.Provider value={context}>
323
+ <View style={style}>
324
+ <ScrollView
325
+ horizontal
326
+ onScroll={handleWebScroll}
327
+ onMomentumScrollEnd={handleWebScrollEnd}
328
+ onScrollEndDrag={handleWebScrollEnd}
329
+ ref={scrollViewRef as any}
330
+ scrollEnabled={!disabled}
331
+ pointerEvents={disabled ? 'none' : 'auto'}
332
+ scrollEventThrottle={16}
333
+ showsHorizontalScrollIndicator={false}
334
+ snapToInterval={itemWidth || width}
335
+ snapToAlignment={centered ? 'center' : 'start'}
336
+ decelerationRate="fast"
337
+ style={[styles.webContainer, webContainerStyles, itemsStyle]}
338
+ contentContainerStyle={[styles.webContentContainer, webContentContainerStyle]}
339
+ {...props}
340
+ >
341
+ {carouselItems.map((item, index) =>
342
+ cloneElement(item, {
343
+ active: index === activeIndex,
344
+ inactiveOpacity: inactiveItemOpacity,
345
+ key: item?.key || item.props?.id || index,
346
+ width: itemWidth || width,
347
+ style: [
348
+ item.props?.style,
349
+ styles.webItem,
350
+ centered ? styles.webItemCentered : styles.webItemStart,
351
+ ],
352
+ })
353
+ )}
354
+ </ScrollView>
355
+ {showControls && !hasCarouselControls && controls}
356
+ {otherChildren}
357
+ </View>
358
+ </CarouselContext.Provider>
359
+ );
360
+ }
361
+
362
+ // Render for native using FlatList
363
+ return (
364
+ <CarouselContext.Provider value={context}>
365
+ <View style={style}>
366
+ <FlatList<ReactElement<CarouselItemProps>>
367
+ ref={flatListRef as any}
368
+ bounces={false} // Prevents bouncing at the start and end of carousel scrolling (iOS only)
369
+ data={carouselItems}
370
+ decelerationRate="fast"
371
+ disableIntervalMomentum // Prevents scrolling more than one item at a time
372
+ getItemLayout={(_, index) => ({
373
+ length: itemWidth || width,
374
+ offset: (itemWidth || width) * index,
375
+ index,
376
+ })}
377
+ horizontal
378
+ initialScrollIndex={activeIndex}
379
+ pagingEnabled
380
+ onViewableItemsChanged={handleViewableItemsChanged}
381
+ overScrollMode="never" // Prevents stretching of first and last items when reaching each end of the carousel (Android only)
382
+ removeClippedSubviews={!showOverflow}
383
+ renderItem={({ index, item }) =>
384
+ cloneElement(item, {
385
+ active: index === activeIndex,
386
+ inactiveOpacity: inactiveItemOpacity,
387
+ key: item?.key || item.props?.id || index,
388
+ width: itemWidth || width,
389
+ })
390
+ }
391
+ scrollEnabled={!disabled}
392
+ showsHorizontalScrollIndicator={false}
393
+ snapToInterval={itemWidth || width}
394
+ snapToAlignment="center"
395
+ style={[itemsStyle, containerStyles]}
396
+ viewabilityConfig={{ itemVisiblePercentThreshold: 51 }}
397
+ {...props}
398
+ />
399
+ {showControls && !hasCarouselControls && controls}
400
+ {otherChildren}
401
+ </View>
402
+ </CarouselContext.Provider>
403
+ );
404
+ };
405
+
406
+ Carousel.displayName = 'Carousel';
407
+
408
+ const styles = StyleSheet.create(theme => ({
409
+ controls: {
410
+ marginTop: theme.space['200'],
411
+ },
412
+ webContainer: {
413
+ _web: {
414
+ scrollSnapType: 'x mandatory',
415
+ WebkitOverflowScrolling: 'touch',
416
+ overflowX: 'scroll',
417
+ overflowY: 'hidden',
418
+ },
419
+ },
420
+ webContentContainer: {
421
+ _web: {
422
+ display: 'flex',
423
+ flexDirection: 'row',
424
+ },
425
+ },
426
+ webItem: {
427
+ _web: {
428
+ scrollSnapStop: 'always',
429
+ flexShrink: 0,
430
+ },
431
+ },
432
+ webItemCentered: {
433
+ _web: {
434
+ scrollSnapAlign: 'center',
435
+ },
436
+ },
437
+ webItemStart: {
438
+ _web: {
439
+ scrollSnapAlign: 'start',
440
+ },
441
+ },
442
+ }));
443
+
444
+ export default Carousel;
@@ -0,0 +1,87 @@
1
+ import { createPressable } from '@gluestack-ui/pressable';
2
+ import { useContext } from 'react';
3
+ import { Pressable, View } from 'react-native';
4
+ import { StyleSheet } from 'react-native-unistyles';
5
+ import CarouselContext from './Carousel.context';
6
+ import { CarouselControlsItemProps } from './Carousel.props';
7
+
8
+ export const CarouselControlItemRoot = ({
9
+ active,
10
+ style,
11
+ index,
12
+ activeStyle,
13
+ onPress,
14
+ disabled,
15
+ ...props
16
+ }: CarouselControlsItemProps & { states?: { active?: boolean; disabled?: boolean } }) => {
17
+ const { inverted, numItems } = useContext(CarouselContext);
18
+ styles.useVariants({ active, inverted });
19
+ return (
20
+ <Pressable
21
+ style={[styles.item, active && activeStyle]}
22
+ onPress={onPress}
23
+ disabled={disabled}
24
+ accessibilityLabel={`Page ${index + 1} of ${numItems}`}
25
+ {...props}
26
+ >
27
+ <View style={[styles.circle, style]} />
28
+ </Pressable>
29
+ );
30
+ };
31
+
32
+ const CarouselControlItem = createPressable({
33
+ Root: CarouselControlItemRoot,
34
+ });
35
+
36
+ CarouselControlItem.displayName = 'CarouselControlItem';
37
+
38
+ const styles = StyleSheet.create(theme => ({
39
+ item: {
40
+ width: 24,
41
+ height: 24,
42
+ alignItems: 'center',
43
+ justifyContent: 'center',
44
+ },
45
+ circle: {
46
+ width: theme.components.carouselControl.size,
47
+ height: theme.components.carouselControl.size,
48
+ borderRadius: theme.borderRadius.full,
49
+ borderColor: theme.color.interactive.functional.border.subtle,
50
+ borderWidth: theme.borderWidth[2],
51
+ backgroundColor: 'transparent',
52
+ paddingTop: theme.space['100'],
53
+ overflow: 'hidden',
54
+ _hover: {
55
+ backgroundColor: theme.color.interactive.functional.surface.subtle.hover,
56
+ },
57
+ variants: {
58
+ active: {
59
+ true: {
60
+ backgroundColor: theme.color.interactive.functional.foreground.subtle,
61
+ _hover: {
62
+ backgroundColor: theme.color.interactive.functional.foreground.subtle,
63
+ },
64
+ },
65
+ },
66
+ inverted: {
67
+ true: {
68
+ borderColor: theme.color.interactive.functional.border.inverted,
69
+ _hover: {
70
+ backgroundColor: theme.color.interactive.functional.surface.subtle.inverted.hover,
71
+ },
72
+ },
73
+ },
74
+ },
75
+ compoundVariants: [
76
+ {
77
+ active: true,
78
+ inverted: true,
79
+ styles: {
80
+ backgroundColor: theme.color.interactive.functional.foreground.inverted,
81
+ },
82
+ },
83
+ ],
84
+ },
85
+ }));
86
+
87
+ export default CarouselControlItem;
@@ -0,0 +1,150 @@
1
+ import { nanoid } from 'nanoid/non-secure';
2
+ import { FC, useContext, useEffect, useMemo } from 'react';
3
+ import { StyleSheet } from 'react-native-unistyles';
4
+ import { View } from 'react-native';
5
+ import {
6
+ ChevronLeftSmallIcon,
7
+ ChevronRightSmallIcon,
8
+ } from '@utilitywarehouse/hearth-react-native-icons';
9
+ import { UnstyledIconButton } from '../UnstyledIconButton';
10
+ import CarouselContext from './Carousel.context';
11
+ import { CarouselControlsProps } from './Carousel.props';
12
+ import CarouselControlItem from './CarouselControlItem';
13
+
14
+ export const CarouselControls: FC<CarouselControlsProps> = ({
15
+ testID = 'pagination',
16
+ style,
17
+ itemStyle,
18
+ activeItemStyle,
19
+ showNavigation = false,
20
+ onPressPrev,
21
+ onPressNext,
22
+ accessibilityHidden,
23
+ ...props
24
+ }) => {
25
+ const context = useContext(CarouselContext);
26
+ const {
27
+ activeIndex = 0,
28
+ numItems = 0,
29
+ setActiveIndex,
30
+ controlsAccessibilityHidden,
31
+ disabled = false,
32
+ } = context;
33
+
34
+ const isAccessibilityHidden = accessibilityHidden ?? controlsAccessibilityHidden ?? true;
35
+
36
+ styles.useVariants({ showNavigation });
37
+
38
+ useEffect(() => {
39
+ if (!Object.keys(context).length) {
40
+ console.warn(
41
+ 'CarouselControls must be a child of Carousel. Pagination will not be displayed.'
42
+ );
43
+ }
44
+ }, [context]);
45
+
46
+ const keys = useMemo(() => {
47
+ return Array(numItems)
48
+ .fill(null)
49
+ .map(() => nanoid());
50
+ }, [numItems]);
51
+
52
+ const handlePrev = () => {
53
+ if (activeIndex > 0) {
54
+ setActiveIndex(activeIndex - 1);
55
+ onPressPrev?.();
56
+ }
57
+ };
58
+
59
+ const handleNext = () => {
60
+ if (activeIndex < numItems - 1) {
61
+ setActiveIndex(activeIndex + 1);
62
+ onPressNext?.();
63
+ }
64
+ };
65
+
66
+ const handleItemPress = (index: number) => {
67
+ setActiveIndex(index);
68
+ };
69
+
70
+ if (!Object.keys(context).length) {
71
+ return null;
72
+ }
73
+
74
+ return (
75
+ <View
76
+ style={[styles.root, style]}
77
+ testID={testID}
78
+ importantForAccessibility={isAccessibilityHidden ? 'no-hide-descendants' : 'auto'}
79
+ accessibilityElementsHidden={isAccessibilityHidden}
80
+ {...props}
81
+ >
82
+ {showNavigation && (
83
+ <UnstyledIconButton
84
+ icon={ChevronLeftSmallIcon}
85
+ onPress={handlePrev}
86
+ disabled={disabled || activeIndex === 0}
87
+ accessibilityLabel="Previous"
88
+ style={styles.button}
89
+ inverted={context.inverted}
90
+ />
91
+ )}
92
+ <View style={styles.dotsContainer}>
93
+ {keys.map((_, index) => (
94
+ <CarouselControlItem
95
+ active={index === activeIndex}
96
+ index={index}
97
+ key={keys[index]}
98
+ style={itemStyle}
99
+ activeStyle={activeItemStyle}
100
+ onPress={() => handleItemPress(index)}
101
+ disabled={disabled}
102
+ />
103
+ ))}
104
+ </View>
105
+ {showNavigation && (
106
+ <UnstyledIconButton
107
+ icon={ChevronRightSmallIcon}
108
+ onPress={handleNext}
109
+ disabled={disabled || activeIndex === numItems - 1}
110
+ accessibilityLabel="Next"
111
+ style={styles.button}
112
+ inverted={context.inverted}
113
+ />
114
+ )}
115
+ </View>
116
+ );
117
+ };
118
+
119
+ const styles = StyleSheet.create(theme => ({
120
+ root: {
121
+ width: '100%',
122
+ alignSelf: 'center',
123
+ flexDirection: 'row',
124
+ alignItems: 'center',
125
+ gap: theme.components.carouselControl.gap,
126
+ justifyContent: 'space-between',
127
+ variants: {
128
+ showNavigation: {
129
+ true: {
130
+ justifyContent: 'space-between',
131
+ },
132
+ false: {
133
+ justifyContent: 'center',
134
+ },
135
+ },
136
+ },
137
+ },
138
+ dotsContainer: {
139
+ flexDirection: 'row',
140
+ gap: theme.components.carouselControl.gap,
141
+ },
142
+ button: {
143
+ width: 24,
144
+ height: 24,
145
+ },
146
+ }));
147
+
148
+ CarouselControls.displayName = 'CarouselControls';
149
+
150
+ export default CarouselControls;