@bigbinary/neetoui-rn 2.3.4 → 3.0.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 (280) hide show
  1. package/README.md +118 -0
  2. package/lib/typescript/src/components/Alert/Alert.d.ts +32 -0
  3. package/lib/typescript/src/components/Alert/Alert.d.ts.map +1 -0
  4. package/lib/typescript/src/components/Alert/index.d.ts +2 -0
  5. package/lib/typescript/src/components/Alert/index.d.ts.map +1 -0
  6. package/lib/typescript/src/components/Avatar/Avatar.d.ts +21 -0
  7. package/lib/typescript/src/components/Avatar/Avatar.d.ts.map +1 -0
  8. package/lib/typescript/src/components/Avatar/index.d.ts +2 -0
  9. package/lib/typescript/src/components/Avatar/index.d.ts.map +1 -0
  10. package/lib/typescript/src/components/BottomSheet/BottomSheet.d.ts +61 -0
  11. package/lib/typescript/src/components/BottomSheet/BottomSheet.d.ts.map +1 -0
  12. package/lib/typescript/src/components/BottomSheet/index.d.ts +2 -0
  13. package/lib/typescript/src/components/BottomSheet/index.d.ts.map +1 -0
  14. package/lib/typescript/src/components/BottomSheetMenu/BottomSheetMenu.d.ts +28 -0
  15. package/lib/typescript/src/components/BottomSheetMenu/BottomSheetMenu.d.ts.map +1 -0
  16. package/lib/typescript/src/components/BottomSheetMenu/index.d.ts +2 -0
  17. package/lib/typescript/src/components/BottomSheetMenu/index.d.ts.map +1 -0
  18. package/lib/typescript/src/components/Button/Button.d.ts +79 -0
  19. package/lib/typescript/src/components/Button/Button.d.ts.map +1 -0
  20. package/lib/typescript/src/components/Button/index.d.ts +2 -0
  21. package/lib/typescript/src/components/Button/index.d.ts.map +1 -0
  22. package/lib/typescript/src/components/ChatInput/AttachmentsView.d.ts +11 -0
  23. package/lib/typescript/src/components/ChatInput/AttachmentsView.d.ts.map +1 -0
  24. package/lib/typescript/src/components/ChatInput/ChatInput.d.ts +78 -0
  25. package/lib/typescript/src/components/ChatInput/ChatInput.d.ts.map +1 -0
  26. package/lib/typescript/src/components/ChatInput/IconButton.d.ts +16 -0
  27. package/lib/typescript/src/components/ChatInput/IconButton.d.ts.map +1 -0
  28. package/lib/typescript/src/components/ChatInput/MentionsInput.d.ts +13 -0
  29. package/lib/typescript/src/components/ChatInput/MentionsInput.d.ts.map +1 -0
  30. package/lib/typescript/src/components/ChatInput/constants.d.ts +9 -0
  31. package/lib/typescript/src/components/ChatInput/constants.d.ts.map +1 -0
  32. package/lib/typescript/src/components/ChatInput/index.d.ts +4 -0
  33. package/lib/typescript/src/components/ChatInput/index.d.ts.map +1 -0
  34. package/lib/typescript/src/components/ChatInput/mentions.d.ts +26 -0
  35. package/lib/typescript/src/components/ChatInput/mentions.d.ts.map +1 -0
  36. package/lib/typescript/src/components/CheckBox/CheckBox.d.ts +13 -0
  37. package/lib/typescript/src/components/CheckBox/CheckBox.d.ts.map +1 -0
  38. package/lib/typescript/src/components/CheckBox/index.d.ts +2 -0
  39. package/lib/typescript/src/components/CheckBox/index.d.ts.map +1 -0
  40. package/lib/typescript/src/components/Container/Container.d.ts +9 -0
  41. package/lib/typescript/src/components/Container/Container.d.ts.map +1 -0
  42. package/lib/typescript/src/components/Container/index.d.ts +2 -0
  43. package/lib/typescript/src/components/Container/index.d.ts.map +1 -0
  44. package/lib/typescript/src/components/Divider/Divider.d.ts +7 -0
  45. package/lib/typescript/src/components/Divider/Divider.d.ts.map +1 -0
  46. package/lib/typescript/src/components/Divider/index.d.ts +2 -0
  47. package/lib/typescript/src/components/Divider/index.d.ts.map +1 -0
  48. package/lib/typescript/src/components/EmptyView/EmptyView.d.ts +26 -0
  49. package/lib/typescript/src/components/EmptyView/EmptyView.d.ts.map +1 -0
  50. package/lib/typescript/src/components/EmptyView/index.d.ts +2 -0
  51. package/lib/typescript/src/components/EmptyView/index.d.ts.map +1 -0
  52. package/lib/typescript/src/components/FlashList/FlashList.d.ts +14 -0
  53. package/lib/typescript/src/components/FlashList/FlashList.d.ts.map +1 -0
  54. package/lib/typescript/src/components/FlashList/index.d.ts +2 -0
  55. package/lib/typescript/src/components/FlashList/index.d.ts.map +1 -0
  56. package/lib/typescript/src/components/Header/BaseHeader.d.ts +5 -0
  57. package/lib/typescript/src/components/Header/BaseHeader.d.ts.map +1 -0
  58. package/lib/typescript/src/components/Header/Header.d.ts +21 -0
  59. package/lib/typescript/src/components/Header/Header.d.ts.map +1 -0
  60. package/lib/typescript/src/components/Header/ScreenHeader.d.ts +11 -0
  61. package/lib/typescript/src/components/Header/ScreenHeader.d.ts.map +1 -0
  62. package/lib/typescript/src/components/Header/index.d.ts +4 -0
  63. package/lib/typescript/src/components/Header/index.d.ts.map +1 -0
  64. package/lib/typescript/src/components/Input/Input.d.ts +15 -0
  65. package/lib/typescript/src/components/Input/Input.d.ts.map +1 -0
  66. package/lib/typescript/src/components/Input/index.d.ts +2 -0
  67. package/lib/typescript/src/components/Input/index.d.ts.map +1 -0
  68. package/lib/typescript/src/components/Loader/Loader.d.ts +12 -0
  69. package/lib/typescript/src/components/Loader/Loader.d.ts.map +1 -0
  70. package/lib/typescript/src/components/Loader/index.d.ts +2 -0
  71. package/lib/typescript/src/components/Loader/index.d.ts.map +1 -0
  72. package/lib/typescript/src/components/NeetoUIProvider/NeetoUIProvider.d.ts +16 -0
  73. package/lib/typescript/src/components/NeetoUIProvider/NeetoUIProvider.d.ts.map +1 -0
  74. package/lib/typescript/src/components/NeetoUIProvider/index.d.ts +2 -0
  75. package/lib/typescript/src/components/NeetoUIProvider/index.d.ts.map +1 -0
  76. package/lib/typescript/src/components/OnBoarding/OnBoarding.d.ts +22 -0
  77. package/lib/typescript/src/components/OnBoarding/OnBoarding.d.ts.map +1 -0
  78. package/lib/typescript/src/components/OnBoarding/index.d.ts +2 -0
  79. package/lib/typescript/src/components/OnBoarding/index.d.ts.map +1 -0
  80. package/lib/typescript/src/components/ParentView/ParentView.d.ts +13 -0
  81. package/lib/typescript/src/components/ParentView/ParentView.d.ts.map +1 -0
  82. package/lib/typescript/src/components/ParentView/index.d.ts +2 -0
  83. package/lib/typescript/src/components/ParentView/index.d.ts.map +1 -0
  84. package/lib/typescript/src/components/RadioButton/RadioButton.d.ts +12 -0
  85. package/lib/typescript/src/components/RadioButton/RadioButton.d.ts.map +1 -0
  86. package/lib/typescript/src/components/RadioButton/index.d.ts +2 -0
  87. package/lib/typescript/src/components/RadioButton/index.d.ts.map +1 -0
  88. package/lib/typescript/src/components/ScrollView/ScrollView.d.ts +8 -0
  89. package/lib/typescript/src/components/ScrollView/ScrollView.d.ts.map +1 -0
  90. package/lib/typescript/src/components/ScrollView/index.d.ts +2 -0
  91. package/lib/typescript/src/components/ScrollView/index.d.ts.map +1 -0
  92. package/lib/typescript/src/components/SearchBar/SearchBar.d.ts +12 -0
  93. package/lib/typescript/src/components/SearchBar/SearchBar.d.ts.map +1 -0
  94. package/lib/typescript/src/components/SearchBar/index.d.ts +2 -0
  95. package/lib/typescript/src/components/SearchBar/index.d.ts.map +1 -0
  96. package/lib/typescript/src/components/Skeleton/Skeleton.d.ts +41 -0
  97. package/lib/typescript/src/components/Skeleton/Skeleton.d.ts.map +1 -0
  98. package/lib/typescript/src/components/Skeleton/index.d.ts +2 -0
  99. package/lib/typescript/src/components/Skeleton/index.d.ts.map +1 -0
  100. package/lib/typescript/src/components/TextWithUrl/TextWithUrl.d.ts +11 -0
  101. package/lib/typescript/src/components/TextWithUrl/TextWithUrl.d.ts.map +1 -0
  102. package/lib/typescript/src/components/TextWithUrl/index.d.ts +2 -0
  103. package/lib/typescript/src/components/TextWithUrl/index.d.ts.map +1 -0
  104. package/lib/typescript/src/components/Toast/Toast.d.ts +13 -0
  105. package/lib/typescript/src/components/Toast/Toast.d.ts.map +1 -0
  106. package/lib/typescript/src/components/Toast/ToastGlyph.d.ts +4 -0
  107. package/lib/typescript/src/components/Toast/ToastGlyph.d.ts.map +1 -0
  108. package/lib/typescript/src/components/Toast/index.d.ts +2 -0
  109. package/lib/typescript/src/components/Toast/index.d.ts.map +1 -0
  110. package/lib/typescript/src/components/Touchable/Touchable.d.ts +23 -0
  111. package/lib/typescript/src/components/Touchable/Touchable.d.ts.map +1 -0
  112. package/lib/typescript/src/components/Touchable/index.d.ts +2 -0
  113. package/lib/typescript/src/components/Touchable/index.d.ts.map +1 -0
  114. package/lib/typescript/src/components/Typography/Typography.d.ts +64 -0
  115. package/lib/typescript/src/components/Typography/Typography.d.ts.map +1 -0
  116. package/lib/typescript/src/components/Typography/index.d.ts +2 -0
  117. package/lib/typescript/src/components/Typography/index.d.ts.map +1 -0
  118. package/lib/typescript/src/hooks/index.d.ts +4 -0
  119. package/lib/typescript/src/hooks/index.d.ts.map +1 -0
  120. package/lib/typescript/src/hooks/useDebounce.d.ts +2 -0
  121. package/lib/typescript/src/hooks/useDebounce.d.ts.map +1 -0
  122. package/lib/typescript/src/hooks/useNeetoTheme.d.ts +8 -0
  123. package/lib/typescript/src/hooks/useNeetoTheme.d.ts.map +1 -0
  124. package/lib/typescript/src/hooks/useRefreshByUser.d.ts +5 -0
  125. package/lib/typescript/src/hooks/useRefreshByUser.d.ts.map +1 -0
  126. package/lib/typescript/src/index.d.ts +31 -0
  127. package/lib/typescript/src/index.d.ts.map +1 -0
  128. package/lib/typescript/src/theme/palette.d.ts +28 -0
  129. package/lib/typescript/src/theme/palette.d.ts.map +1 -0
  130. package/lib/typescript/src/theme/palette.generated.d.ts +134 -0
  131. package/lib/typescript/src/theme/palette.generated.d.ts.map +1 -0
  132. package/lib/typescript/src/theme/tokens.d.ts +184 -0
  133. package/lib/typescript/src/theme/tokens.d.ts.map +1 -0
  134. package/lib/typescript/src/unistyles-init.d.ts +2 -0
  135. package/lib/typescript/src/unistyles-init.d.ts.map +1 -0
  136. package/lib/typescript/src/unistyles.d.ts +21 -0
  137. package/lib/typescript/src/unistyles.d.ts.map +1 -0
  138. package/lib/typescript/src/utils/index.d.ts +18 -0
  139. package/lib/typescript/src/utils/index.d.ts.map +1 -0
  140. package/lib/typescript/src/utils/scale.d.ts +2 -0
  141. package/lib/typescript/src/utils/scale.d.ts.map +1 -0
  142. package/package.json +125 -51
  143. package/src/components/Alert/Alert.tsx +222 -0
  144. package/src/components/Alert/index.ts +1 -0
  145. package/src/components/Avatar/Avatar.tsx +123 -0
  146. package/src/components/Avatar/index.ts +1 -0
  147. package/src/components/BottomSheet/BottomSheet.tsx +468 -0
  148. package/src/components/BottomSheet/index.ts +5 -0
  149. package/src/components/BottomSheetMenu/BottomSheetMenu.tsx +150 -0
  150. package/src/components/BottomSheetMenu/index.ts +5 -0
  151. package/src/components/Button/Button.tsx +134 -0
  152. package/src/components/Button/index.ts +1 -0
  153. package/src/components/ChatInput/AttachmentsView.tsx +68 -0
  154. package/src/components/ChatInput/ChatInput.tsx +393 -0
  155. package/src/components/ChatInput/IconButton.tsx +52 -0
  156. package/src/components/ChatInput/MentionsInput.tsx +126 -0
  157. package/src/components/ChatInput/constants.ts +18 -0
  158. package/src/components/ChatInput/index.ts +11 -0
  159. package/src/components/ChatInput/mentions.ts +91 -0
  160. package/src/components/CheckBox/CheckBox.tsx +86 -0
  161. package/src/components/CheckBox/index.ts +1 -0
  162. package/src/components/Container/Container.tsx +14 -0
  163. package/src/components/Container/index.ts +1 -0
  164. package/src/components/Divider/Divider.tsx +32 -0
  165. package/src/components/Divider/index.ts +1 -0
  166. package/src/components/EmptyView/EmptyView.tsx +80 -0
  167. package/src/components/EmptyView/index.ts +1 -0
  168. package/src/components/FlashList/FlashList.tsx +162 -0
  169. package/src/components/FlashList/index.ts +1 -0
  170. package/src/components/Header/BaseHeader.tsx +19 -0
  171. package/src/components/Header/Header.tsx +114 -0
  172. package/src/components/Header/ScreenHeader.tsx +34 -0
  173. package/src/components/Header/index.ts +3 -0
  174. package/src/components/Input/Input.tsx +122 -0
  175. package/src/components/Input/index.ts +1 -0
  176. package/src/components/Loader/Loader.tsx +93 -0
  177. package/src/components/Loader/index.ts +1 -0
  178. package/src/components/NeetoUIProvider/NeetoUIProvider.tsx +43 -0
  179. package/src/components/NeetoUIProvider/index.ts +1 -0
  180. package/src/components/OnBoarding/OnBoarding.tsx +177 -0
  181. package/src/components/OnBoarding/index.ts +5 -0
  182. package/src/components/ParentView/ParentView.tsx +98 -0
  183. package/src/components/ParentView/index.ts +1 -0
  184. package/src/components/RadioButton/RadioButton.tsx +80 -0
  185. package/src/components/RadioButton/index.ts +1 -0
  186. package/src/components/ScrollView/ScrollView.tsx +30 -0
  187. package/src/components/ScrollView/index.ts +1 -0
  188. package/src/components/SearchBar/SearchBar.tsx +122 -0
  189. package/src/components/SearchBar/index.ts +1 -0
  190. package/src/components/Skeleton/Skeleton.tsx +121 -0
  191. package/src/components/Skeleton/index.ts +1 -0
  192. package/src/components/TextWithUrl/TextWithUrl.tsx +54 -0
  193. package/src/components/TextWithUrl/index.ts +1 -0
  194. package/src/components/Toast/Toast.tsx +157 -0
  195. package/src/components/Toast/ToastGlyph.tsx +53 -0
  196. package/src/components/Toast/index.ts +1 -0
  197. package/src/components/Touchable/Touchable.tsx +54 -0
  198. package/src/components/Touchable/index.ts +1 -0
  199. package/src/components/Typography/Typography.tsx +76 -0
  200. package/src/components/Typography/index.ts +1 -0
  201. package/src/hooks/index.ts +3 -0
  202. package/src/hooks/useDebounce.ts +17 -0
  203. package/src/hooks/useNeetoTheme.ts +10 -0
  204. package/src/hooks/useRefreshByUser.ts +17 -0
  205. package/src/index.ts +68 -0
  206. package/src/theme/palette.generated.ts +141 -0
  207. package/src/theme/palette.ts +34 -0
  208. package/src/theme/tokens.ts +205 -0
  209. package/src/types/neeto-icons-rn.d.ts +33 -0
  210. package/src/unistyles-init.ts +16 -0
  211. package/src/unistyles.ts +34 -0
  212. package/src/utils/index.ts +41 -0
  213. package/src/utils/scale.ts +16 -0
  214. package/components/Accordion.js +0 -1
  215. package/components/ActionIcon.js +0 -1
  216. package/components/Alert.js +0 -1
  217. package/components/Avatar.js +0 -1
  218. package/components/Badge.js +0 -1
  219. package/components/BottomSheet.js +0 -1
  220. package/components/BottomTabBar.js +0 -1
  221. package/components/Button.js +0 -1
  222. package/components/ButtonGroup.js +0 -1
  223. package/components/Calendar.js +0 -1
  224. package/components/Card.js +0 -1
  225. package/components/Carousel.js +0 -1
  226. package/components/ChatInput/AttachmentsView.js +0 -1
  227. package/components/ChatInput/Badge.js +0 -1
  228. package/components/ChatInput/ChatInput.js +0 -1
  229. package/components/ChatInput/EmailFields.js +0 -1
  230. package/components/ChatInput/IconButton.js +0 -1
  231. package/components/ChatInput/MentionsInputWrapper.js +0 -1
  232. package/components/CheckBox.js +0 -1
  233. package/components/Chip.js +0 -1
  234. package/components/Container.js +0 -1
  235. package/components/Divider.js +0 -1
  236. package/components/FAB.js +0 -1
  237. package/components/FlashList.js +0 -1
  238. package/components/FlatList.js +0 -1
  239. package/components/Input.js +0 -1
  240. package/components/InputEmailChip.js +0 -1
  241. package/components/LineLoader.js +0 -1
  242. package/components/ListItem.js +0 -1
  243. package/components/Loader.js +0 -1
  244. package/components/MultiSelect.js +0 -1
  245. package/components/NotificationIcon.js +0 -1
  246. package/components/NotificationPreferenceList.js +0 -1
  247. package/components/OnBoarding.js +0 -1
  248. package/components/OrganizationItem.js +0 -1
  249. package/components/OtpInputs.js +0 -1
  250. package/components/ParentView.js +0 -1
  251. package/components/Popover.js +0 -1
  252. package/components/RadioButton.js +0 -1
  253. package/components/RichTextEditor.js +0 -1
  254. package/components/Ripple.js +0 -1
  255. package/components/ScrollView.js +0 -1
  256. package/components/SearchBar.js +0 -1
  257. package/components/SegmentedTopBar/Indicator.js +0 -1
  258. package/components/SegmentedTopBar/Tab.js +0 -1
  259. package/components/SegmentedTopBar/index.js +0 -1
  260. package/components/SocialButton.js +0 -1
  261. package/components/Toast.js +0 -1
  262. package/components/ToggleSwitch.js +0 -1
  263. package/components/TopBar.js +0 -1
  264. package/components/Touchable.js +0 -1
  265. package/components/TouchableWithoutFeedback.js +0 -1
  266. package/components/Typography.js +0 -1
  267. package/components/index.js +0 -1
  268. package/config/index.js +0 -1
  269. package/config/toasterConfig.js +0 -1
  270. package/contexts/index.js +0 -1
  271. package/hooks/index.js +0 -1
  272. package/hooks/useDebounce.js +0 -1
  273. package/hooks/useKeyboard.js +0 -1
  274. package/hooks/useRefreshByUser.js +0 -1
  275. package/index.d.ts +0 -630
  276. package/index.js +0 -1
  277. package/react-native.config.js +0 -10
  278. package/theme/index.js +0 -1
  279. package/utils/index.js +0 -1
  280. package/utils/scale.js +0 -1
@@ -0,0 +1,468 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type ComponentType,
8
+ type ReactNode,
9
+ type Ref,
10
+ } from "react";
11
+ import {
12
+ Keyboard,
13
+ StyleSheet as RNStyleSheet,
14
+ View,
15
+ type FlatList,
16
+ type StyleProp,
17
+ type TextStyle,
18
+ type ViewStyle,
19
+ } from "react-native";
20
+ import { Check } from "@bigbinary/neeto-icons-rn";
21
+ import {
22
+ BottomSheetBackdrop,
23
+ BottomSheetFlatList,
24
+ BottomSheetModal,
25
+ type BottomSheetBackdropProps,
26
+ type BottomSheetModalProps,
27
+ } from "@gorhom/bottom-sheet";
28
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
29
+ import { StyleSheet, useUnistyles } from "react-native-unistyles";
30
+
31
+ import { moderateScale } from "../../utils/scale";
32
+ import { Divider } from "../Divider";
33
+ import { Loader } from "../Loader";
34
+ import { SearchBar, type SearchBarProps } from "../SearchBar";
35
+ import { Touchable } from "../Touchable";
36
+ import { Typography } from "../Typography";
37
+
38
+ /**
39
+ * Contract every v2 ContentRow already satisfies — unchanged, so the 42
40
+ * existing call sites keep their row components as-is.
41
+ */
42
+ export type BottomSheetContentRowProps<T> = {
43
+ item: T;
44
+ index: number;
45
+ isSelected: boolean;
46
+ onPress: () => void;
47
+ };
48
+
49
+ export type BottomSheetProps<T> = {
50
+ isVisible: boolean;
51
+ /** Called whenever the sheet should close (backdrop, swipe, item press). */
52
+ hide: () => void;
53
+ data?: T[];
54
+ onItemPress?: (args: { index: number; item: T }) => void;
55
+ selectedItem?: T | null;
56
+ title?: string;
57
+ /**
58
+ * "checkbox" keeps the sheet open on item press and shows a Done button
59
+ * in the header (v2 behavior).
60
+ */
61
+ contentType?: "checkbox" | null;
62
+ onDonePress?: () => void;
63
+ ContentRow?: ComponentType<BottomSheetContentRowProps<T>>;
64
+ /** Rendered below the list, above the no-results / create-option rows. */
65
+ children?: ReactNode;
66
+ canSearch?: boolean;
67
+ searchBarProps?: SearchBarProps;
68
+ labelExtractor?: (item: T, index?: number) => string | undefined;
69
+ valueExtractor?: (item: T) => unknown;
70
+ shouldShowItemSeparator?: boolean;
71
+ shouldHideKeyboardOnScrollBegin?: boolean;
72
+ disabled?: boolean;
73
+ showCreateOption?: boolean;
74
+ showCreateOptionLoader?: boolean;
75
+ createOptionLabel?: string;
76
+ onPressCreateOption?: (searchText: string) => void;
77
+ CreateItemComponent?: ComponentType<{ searchText: string }>;
78
+ noResultsLabel?: string;
79
+ NoResultsComponent?: ComponentType;
80
+ onBackdropPress?: () => void;
81
+ maxHeight?: number;
82
+ style?: StyleProp<ViewStyle>;
83
+ titleContainerStyle?: StyleProp<ViewStyle>;
84
+ titleTextStyle?: StyleProp<TextStyle>;
85
+ flatListRef?: Ref<FlatList<T>>;
86
+ /** Escape hatch into @gorhom/bottom-sheet (replaces v2's modalParams). */
87
+ sheetProps?: Partial<BottomSheetModalProps>;
88
+ };
89
+
90
+ const DefaultContentRow = ({
91
+ label,
92
+ isSelected,
93
+ onPress,
94
+ }: {
95
+ label: string;
96
+ isSelected: boolean;
97
+ onPress: () => void;
98
+ }) => {
99
+ const { theme } = useUnistyles();
100
+
101
+ return (
102
+ <Touchable
103
+ accessibilityRole="button"
104
+ accessibilityState={{ selected: isSelected }}
105
+ style={styles.defaultRow}
106
+ onPress={onPress}
107
+ >
108
+ <Typography weight={isSelected ? "semibold" : undefined}>
109
+ {label}
110
+ </Typography>
111
+ {isSelected && (
112
+ <Check color={theme.colors.primary} size={moderateScale(20)} />
113
+ )}
114
+ </Touchable>
115
+ );
116
+ };
117
+
118
+ const ItemSeparator = () => <Divider style={styles.separator} />;
119
+
120
+ export const BottomSheet = <T,>({
121
+ isVisible,
122
+ hide,
123
+ data = [],
124
+ onItemPress,
125
+ selectedItem,
126
+ title,
127
+ contentType,
128
+ onDonePress,
129
+ ContentRow,
130
+ children,
131
+ canSearch = false,
132
+ searchBarProps,
133
+ labelExtractor,
134
+ valueExtractor,
135
+ shouldShowItemSeparator = true,
136
+ shouldHideKeyboardOnScrollBegin = true,
137
+ disabled = false,
138
+ showCreateOption = false,
139
+ showCreateOptionLoader = false,
140
+ createOptionLabel,
141
+ onPressCreateOption,
142
+ CreateItemComponent,
143
+ noResultsLabel,
144
+ NoResultsComponent,
145
+ onBackdropPress,
146
+ maxHeight,
147
+ style,
148
+ titleContainerStyle,
149
+ titleTextStyle,
150
+ flatListRef,
151
+ sheetProps,
152
+ }: BottomSheetProps<T>) => {
153
+ const { theme } = useUnistyles();
154
+ const insets = useSafeAreaInsets();
155
+ const sheetRef = useRef<BottomSheetModal>(null);
156
+ const [searchText, setSearchText] = useState("");
157
+
158
+ const isVisibleRef = useRef(isVisible);
159
+ isVisibleRef.current = isVisible;
160
+ const hasPresentedRef = useRef(false);
161
+
162
+ useEffect(() => {
163
+ if (isVisible) {
164
+ hasPresentedRef.current = true;
165
+ sheetRef.current?.present();
166
+ } else if (hasPresentedRef.current) {
167
+ // Never dismiss() a modal that was never presented: gorhom marks it
168
+ // DISMISSING, which makes the next present() unmount immediately.
169
+ sheetRef.current?.dismiss();
170
+ }
171
+ setSearchText("");
172
+ }, [isVisible]);
173
+
174
+ // Gesture-driven closes (swipe down, backdrop tap, Android back) end here;
175
+ // hand control back to the owner so its isVisible state stays in sync.
176
+ const handleDismiss = useCallback(() => {
177
+ setSearchText("");
178
+ if (!isVisibleRef.current) return;
179
+
180
+ hide();
181
+
182
+ // The sheet is controlled: gorhom closed it natively and React never
183
+ // heard about it, so only the owner clearing isVisible keeps the two in
184
+ // sync. If hide() does not do that, the next `setIsVisible(true)` is a
185
+ // no-op on an unchanged value — no re-render, no present(), and the
186
+ // sheet silently never reopens. Warn rather than re-present, which
187
+ // would fight the swipe the user just made.
188
+ if (__DEV__) {
189
+ setTimeout(() => {
190
+ if (!isVisibleRef.current) return;
191
+
192
+ // eslint-disable-next-line no-console
193
+ console.warn(
194
+ "neetoui-rn: BottomSheet was dismissed by a gesture but isVisible is still true. " +
195
+ "hide() must clear the state driving isVisible, or the sheet will not reopen."
196
+ );
197
+ }, 0);
198
+ }
199
+ }, [hide]);
200
+
201
+ const getLabel = useCallback(
202
+ // v2 fallback chain (label prop → extractor → item itself), || on
203
+ // purpose: empty-string labels fall through exactly as in v2.
204
+ (item: T, index: number): string =>
205
+ String(
206
+ (item as { label?: unknown })?.label ||
207
+ labelExtractor?.(item, index) ||
208
+ item
209
+ ),
210
+ [labelExtractor]
211
+ );
212
+
213
+ const generatedData = useMemo(() => {
214
+ if (!searchText.length) return data;
215
+
216
+ return data.filter((item, index) =>
217
+ getLabel(item, index).toLowerCase().includes(searchText.toLowerCase())
218
+ );
219
+ }, [data, getLabel, searchText]);
220
+
221
+ // v2: hide the create row once an exact (case-insensitive) match exists.
222
+ const hasExactMatch = useMemo(
223
+ () =>
224
+ generatedData.some(
225
+ (item, index) =>
226
+ getLabel(item, index).toLocaleLowerCase() ===
227
+ searchText.toLocaleLowerCase()
228
+ ),
229
+ [generatedData, getLabel, searchText]
230
+ );
231
+
232
+ // Presence checks rather than || chains: a row whose value is 0, "" or
233
+ // false is a legitimate value, and falling through to the item itself
234
+ // made those rows compare by object identity and never match.
235
+ const resolveValue = useCallback(
236
+ (candidate: T | null | undefined): unknown => {
237
+ if (candidate === null || candidate === undefined) return candidate;
238
+
239
+ if (typeof candidate === "object" && "value" in candidate) {
240
+ const { value } = candidate as { value?: unknown };
241
+ if (value !== undefined) return value;
242
+ }
243
+
244
+ const extracted = valueExtractor?.(candidate);
245
+
246
+ return extracted === undefined ? candidate : extracted;
247
+ },
248
+ [valueExtractor]
249
+ );
250
+
251
+ const isSelected = useCallback(
252
+ (item: T) => resolveValue(item) === resolveValue(selectedItem),
253
+ [resolveValue, selectedItem]
254
+ );
255
+
256
+ const renderBackdrop = useCallback(
257
+ (backdropProps: BottomSheetBackdropProps) => (
258
+ <BottomSheetBackdrop
259
+ {...backdropProps}
260
+ appearsOnIndex={0}
261
+ disappearsOnIndex={-1}
262
+ pressBehavior="close"
263
+ onPress={onBackdropPress}
264
+ />
265
+ ),
266
+ [onBackdropPress]
267
+ );
268
+
269
+ const header =
270
+ title || canSearch ? (
271
+ <View style={[styles.header, titleContainerStyle]}>
272
+ <View style={styles.titleRow}>
273
+ {!!title && (
274
+ <Typography style={titleTextStyle} variant="modalHeader">
275
+ {title}
276
+ </Typography>
277
+ )}
278
+ {!!contentType && (
279
+ <Touchable
280
+ accessibilityRole="button"
281
+ onPress={() => {
282
+ hide();
283
+ onDonePress?.();
284
+ }}
285
+ >
286
+ <Typography style={styles.doneLabel} variant="modalHeader">
287
+ Done
288
+ </Typography>
289
+ </Touchable>
290
+ )}
291
+ </View>
292
+ {canSearch && (
293
+ <SearchBar
294
+ debounceDelay={200}
295
+ placeholder="Search"
296
+ showCancelButton={false}
297
+ onCancel={() => setSearchText("")}
298
+ onChangeText={setSearchText}
299
+ {...searchBarProps}
300
+ />
301
+ )}
302
+ </View>
303
+ ) : null;
304
+
305
+ const showNoResults =
306
+ !!searchText && !generatedData.length && !showCreateOption;
307
+
308
+ const footer = (
309
+ <View>
310
+ {children}
311
+ {showNoResults &&
312
+ (NoResultsComponent ? (
313
+ <NoResultsComponent />
314
+ ) : (
315
+ <View style={styles.noResults}>
316
+ <Typography size="s" style={styles.mutedLabel} weight="semibold">
317
+ {noResultsLabel || "No options found"}
318
+ </Typography>
319
+ </View>
320
+ ))}
321
+ {!disabled &&
322
+ !hasExactMatch &&
323
+ (showCreateOptionLoader ? (
324
+ <Loader color={theme.colors.primary} />
325
+ ) : (
326
+ !!searchText &&
327
+ showCreateOption &&
328
+ (CreateItemComponent ? (
329
+ <CreateItemComponent searchText={searchText} />
330
+ ) : (
331
+ <Touchable
332
+ accessibilityRole="button"
333
+ style={styles.createRow}
334
+ onPress={() => onPressCreateOption?.(searchText)}
335
+ >
336
+ <Typography size="s" style={styles.mutedLabel}>
337
+ {createOptionLabel || `Create ${searchText} option`}
338
+ </Typography>
339
+ </Touchable>
340
+ ))
341
+ ))}
342
+ </View>
343
+ );
344
+
345
+ return (
346
+ <BottomSheetModal
347
+ backdropComponent={renderBackdrop}
348
+ backgroundStyle={styles.sheetBackground}
349
+ handleIndicatorStyle={styles.handleIndicator}
350
+ keyboardBehavior="interactive"
351
+ keyboardBlurBehavior="restore"
352
+ maxDynamicContentSize={maxHeight}
353
+ ref={sheetRef}
354
+ {...sheetProps}
355
+ // After the spread, and composed rather than replaced: this is the
356
+ // only channel through which a gesture close reaches the owner, so an
357
+ // escape-hatch onDismiss must not be able to displace it.
358
+ onDismiss={() => {
359
+ handleDismiss();
360
+ sheetProps?.onDismiss?.();
361
+ }}
362
+ >
363
+ <BottomSheetFlatList
364
+ data={generatedData}
365
+ keyboardShouldPersistTaps="handled"
366
+ // Keyed by value where there is one: with search filtering, index
367
+ // keys re-key every surviving row on each keystroke and remount a
368
+ // stateful ContentRow.
369
+ keyExtractor={(item, index) => {
370
+ const value = resolveValue(item);
371
+
372
+ return typeof value === "string" || typeof value === "number"
373
+ ? String(value)
374
+ : String(index);
375
+ }}
376
+ ListFooterComponent={footer}
377
+ ListHeaderComponent={header}
378
+ ref={flatListRef as never}
379
+ showsVerticalScrollIndicator={false}
380
+ stickyHeaderIndices={header ? [0] : undefined}
381
+ ItemSeparatorComponent={shouldShowItemSeparator ? ItemSeparator : null}
382
+ // Single flattened object, never an array: gorhom's content-container
383
+ // hook runs it through StyleSheet.compose, which rejects arrays on
384
+ // react-native-web. No theme tokens in here, so flattening is safe.
385
+ contentContainerStyle={RNStyleSheet.flatten([
386
+ {
387
+ paddingHorizontal: moderateScale(16),
388
+ paddingBottom: insets.bottom + moderateScale(16),
389
+ },
390
+ style,
391
+ ])}
392
+ renderItem={({ item, index }) => {
393
+ const selected = isSelected(item);
394
+ const onPress = () => {
395
+ // v2 order: close first (unless checkbox mode), then notify.
396
+ if (!contentType) hide();
397
+ onItemPress?.({ index, item });
398
+ };
399
+
400
+ return ContentRow ? (
401
+ <ContentRow
402
+ index={index}
403
+ isSelected={selected}
404
+ item={item}
405
+ onPress={onPress}
406
+ />
407
+ ) : (
408
+ <DefaultContentRow
409
+ isSelected={selected}
410
+ label={getLabel(item, index)}
411
+ onPress={onPress}
412
+ />
413
+ );
414
+ }}
415
+ onScrollBeginDrag={
416
+ shouldHideKeyboardOnScrollBegin ? () => Keyboard.dismiss() : undefined
417
+ }
418
+ />
419
+ </BottomSheetModal>
420
+ );
421
+ };
422
+
423
+ const styles = StyleSheet.create(theme => ({
424
+ sheetBackground: {
425
+ backgroundColor: theme.colors.background.primary,
426
+ borderTopLeftRadius: moderateScale(20),
427
+ borderTopRightRadius: moderateScale(20),
428
+ },
429
+ handleIndicator: {
430
+ backgroundColor: theme.colors.palette.grey400,
431
+ },
432
+ separator: {
433
+ backgroundColor: theme.colors.palette.grey200,
434
+ },
435
+ header: {
436
+ backgroundColor: theme.colors.background.primary,
437
+ gap: moderateScale(16),
438
+ paddingBottom: moderateScale(12),
439
+ },
440
+ titleRow: {
441
+ alignItems: "center",
442
+ flexDirection: "row",
443
+ justifyContent: "space-between",
444
+ },
445
+ doneLabel: {
446
+ color: theme.colors.palette.purple500,
447
+ },
448
+ defaultRow: {
449
+ alignItems: "center",
450
+ flexDirection: "row",
451
+ gap: moderateScale(8),
452
+ justifyContent: "space-between",
453
+ minHeight: moderateScale(44),
454
+ },
455
+ createRow: {
456
+ alignItems: "center",
457
+ height: moderateScale(40),
458
+ justifyContent: "center",
459
+ },
460
+ noResults: {
461
+ alignItems: "center",
462
+ marginBottom: moderateScale(2),
463
+ paddingVertical: moderateScale(2),
464
+ },
465
+ mutedLabel: {
466
+ color: theme.colors.text.secondary,
467
+ },
468
+ }));
@@ -0,0 +1,5 @@
1
+ export {
2
+ BottomSheet,
3
+ type BottomSheetProps,
4
+ type BottomSheetContentRowProps,
5
+ } from "./BottomSheet";
@@ -0,0 +1,150 @@
1
+ import { type ComponentType, type ReactNode } from "react";
2
+ import { Pressable, View } from "react-native";
3
+ import { StyleSheet, useUnistyles } from "react-native-unistyles";
4
+
5
+ import { moderateScale } from "../../utils/scale";
6
+ import { BottomSheet } from "../BottomSheet";
7
+ import { Typography } from "../Typography";
8
+
9
+ export type BottomSheetMenuItem = {
10
+ key: string;
11
+ label: string;
12
+ icon?: ComponentType<{ size?: number; color?: string }>;
13
+ /** Raw color override for icon + label (e.g. destructive red). */
14
+ color?: string;
15
+ disabled?: boolean;
16
+ onPress?: () => void;
17
+ };
18
+
19
+ export type BottomSheetMenuProps = {
20
+ isVisible: boolean;
21
+ onClose: () => void;
22
+ items: BottomSheetMenuItem[];
23
+ onItemPress?: (item: BottomSheetMenuItem) => void;
24
+ header?: ReactNode;
25
+ title?: string;
26
+ cancelLabel?: string;
27
+ };
28
+
29
+ /**
30
+ * Action-menu sheet (promoted from neeto-commons-rn, 10 call sites).
31
+ * Same props; rows render as the sheet's footer content.
32
+ */
33
+ export const BottomSheetMenu = ({
34
+ isVisible,
35
+ onClose,
36
+ items,
37
+ onItemPress,
38
+ header,
39
+ title,
40
+ cancelLabel = "Cancel",
41
+ }: BottomSheetMenuProps) => {
42
+ const { theme } = useUnistyles();
43
+
44
+ return (
45
+ <BottomSheet
46
+ data={[]}
47
+ hide={onClose}
48
+ isVisible={isVisible}
49
+ shouldShowItemSeparator={false}
50
+ style={styles.sheetContent}
51
+ onBackdropPress={onClose}
52
+ >
53
+ {!!header && <View style={styles.headerContainer}>{header}</View>}
54
+ {!!title && (
55
+ <View style={styles.titleContainer}>
56
+ <Typography size="m" style={styles.title} weight="medium">
57
+ {title}
58
+ </Typography>
59
+ </View>
60
+ )}
61
+ {items.map((item, index) => {
62
+ const Icon = item.icon;
63
+ const color = item.color ?? theme.colors.text.primary;
64
+
65
+ return (
66
+ <Pressable
67
+ accessibilityRole="button"
68
+ accessibilityState={{ disabled: item.disabled }}
69
+ disabled={item.disabled}
70
+ key={item.key}
71
+ style={[
72
+ styles.row,
73
+ item.disabled && styles.disabledRow,
74
+ index < items.length - 1 && styles.separator,
75
+ ]}
76
+ onPress={() => {
77
+ onClose();
78
+ onItemPress?.(item);
79
+ item.onPress?.();
80
+ }}
81
+ >
82
+ {Icon && <Icon color={color} size={moderateScale(20)} />}
83
+ <Typography color={color} size="m">
84
+ {item.label}
85
+ </Typography>
86
+ </Pressable>
87
+ );
88
+ })}
89
+ {!!cancelLabel && (
90
+ <Pressable
91
+ accessibilityRole="button"
92
+ style={styles.cancelButton}
93
+ onPress={onClose}
94
+ >
95
+ <Typography size="m" weight="semibold">
96
+ {cancelLabel}
97
+ </Typography>
98
+ </Pressable>
99
+ )}
100
+ </BottomSheet>
101
+ );
102
+ };
103
+
104
+ const styles = StyleSheet.create(theme => ({
105
+ sheetContent: {
106
+ paddingHorizontal: 0,
107
+ },
108
+ headerContainer: {
109
+ borderBottomColor: theme.colors.palette.grey200,
110
+ borderBottomWidth: moderateScale(1.5),
111
+ paddingBottom: moderateScale(12),
112
+ paddingHorizontal: moderateScale(16),
113
+ paddingTop: moderateScale(8),
114
+ },
115
+ titleContainer: {
116
+ borderBottomColor: theme.colors.palette.grey200,
117
+ borderBottomWidth: moderateScale(1.5),
118
+ paddingBottom: moderateScale(12),
119
+ paddingHorizontal: moderateScale(16),
120
+ },
121
+ title: {
122
+ alignSelf: "center",
123
+ color: theme.colors.text.muted,
124
+ },
125
+ row: {
126
+ alignItems: "center",
127
+ flexDirection: "row",
128
+ gap: moderateScale(12),
129
+ paddingHorizontal: moderateScale(16),
130
+ paddingVertical: moderateScale(14),
131
+ },
132
+ disabledRow: {
133
+ opacity: 0.5,
134
+ },
135
+ separator: {
136
+ borderBottomColor: theme.colors.palette.grey200,
137
+ borderBottomWidth: StyleSheet.hairlineWidth,
138
+ marginHorizontal: moderateScale(16),
139
+ paddingHorizontal: 0,
140
+ },
141
+ cancelButton: {
142
+ alignItems: "center",
143
+ backgroundColor: theme.colors.palette.grey100,
144
+ borderRadius: theme.radii.lg,
145
+ justifyContent: "center",
146
+ marginHorizontal: moderateScale(16),
147
+ marginTop: moderateScale(16),
148
+ paddingVertical: moderateScale(14),
149
+ },
150
+ }));
@@ -0,0 +1,5 @@
1
+ export {
2
+ BottomSheetMenu,
3
+ type BottomSheetMenuItem,
4
+ type BottomSheetMenuProps,
5
+ } from "./BottomSheetMenu";