@utilitywarehouse/hearth-react-native 0.27.3 → 0.28.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 (118) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +18 -19
  3. package/CHANGELOG.md +110 -0
  4. package/build/components/Combobox/Combobox.context.d.ts +13 -0
  5. package/build/components/Combobox/Combobox.context.js +9 -0
  6. package/build/components/Combobox/Combobox.d.ts +6 -0
  7. package/build/components/Combobox/Combobox.js +246 -0
  8. package/build/components/Combobox/Combobox.props.d.ts +180 -0
  9. package/build/components/Combobox/Combobox.props.js +1 -0
  10. package/build/components/Combobox/ComboboxOption.d.ts +6 -0
  11. package/build/components/Combobox/ComboboxOption.js +56 -0
  12. package/build/components/Combobox/index.d.ts +4 -0
  13. package/build/components/Combobox/index.js +3 -0
  14. package/build/components/Modal/Modal.js +26 -42
  15. package/build/components/Modal/Modal.web.js +3 -3
  16. package/build/components/Pagination/Pagination.d.ts +6 -0
  17. package/build/components/Pagination/Pagination.js +125 -0
  18. package/build/components/Pagination/Pagination.props.d.ts +26 -0
  19. package/build/components/Pagination/Pagination.props.js +1 -0
  20. package/build/components/Pagination/Pagination.utils.d.ts +2 -0
  21. package/build/components/Pagination/Pagination.utils.js +20 -0
  22. package/build/components/Pagination/Pagination.utils.test.d.ts +1 -0
  23. package/build/components/Pagination/Pagination.utils.test.js +16 -0
  24. package/build/components/Pagination/index.d.ts +2 -0
  25. package/build/components/Pagination/index.js +1 -0
  26. package/build/components/SafeAreaView/SafeAreaView.d.ts +5 -0
  27. package/build/components/SafeAreaView/SafeAreaView.js +117 -0
  28. package/build/components/SafeAreaView/SafeAreaView.props.d.ts +17 -0
  29. package/build/components/SafeAreaView/SafeAreaView.props.js +1 -0
  30. package/build/components/SafeAreaView/index.d.ts +2 -0
  31. package/build/components/SafeAreaView/index.js +1 -0
  32. package/build/components/Select/Select.js +3 -2
  33. package/build/components/Table/Table.context.d.ts +12 -0
  34. package/build/components/Table/Table.context.js +9 -0
  35. package/build/components/Table/Table.d.ts +6 -0
  36. package/build/components/Table/Table.js +71 -0
  37. package/build/components/Table/Table.props.d.ts +56 -0
  38. package/build/components/Table/Table.props.js +1 -0
  39. package/build/components/Table/Table.utils.d.ts +5 -0
  40. package/build/components/Table/Table.utils.js +48 -0
  41. package/build/components/Table/Table.utils.test.d.ts +1 -0
  42. package/build/components/Table/Table.utils.test.js +71 -0
  43. package/build/components/Table/TableBody.d.ts +6 -0
  44. package/build/components/Table/TableBody.js +16 -0
  45. package/build/components/Table/TableCell.d.ts +10 -0
  46. package/build/components/Table/TableCell.js +44 -0
  47. package/build/components/Table/TableHeader.d.ts +6 -0
  48. package/build/components/Table/TableHeader.js +24 -0
  49. package/build/components/Table/TableHeaderCell.d.ts +10 -0
  50. package/build/components/Table/TableHeaderCell.js +97 -0
  51. package/build/components/Table/TablePagination.d.ts +6 -0
  52. package/build/components/Table/TablePagination.js +7 -0
  53. package/build/components/Table/TableRow.d.ts +8 -0
  54. package/build/components/Table/TableRow.js +25 -0
  55. package/build/components/Table/index.d.ts +8 -0
  56. package/build/components/Table/index.js +7 -0
  57. package/build/components/Timeline/Timeline.d.ts +6 -0
  58. package/build/components/Timeline/Timeline.js +34 -0
  59. package/build/components/Timeline/Timeline.props.d.ts +47 -0
  60. package/build/components/Timeline/Timeline.props.js +1 -0
  61. package/build/components/Timeline/TimelineItem.d.ts +6 -0
  62. package/build/components/Timeline/TimelineItem.js +235 -0
  63. package/build/components/Timeline/index.d.ts +3 -0
  64. package/build/components/Timeline/index.js +2 -0
  65. package/build/components/index.d.ts +5 -0
  66. package/build/components/index.js +5 -0
  67. package/build/tokens/components/dark/timeline.d.ts +2 -2
  68. package/build/tokens/components/dark/timeline.js +2 -2
  69. package/docs/components/AllComponents.web.tsx +106 -23
  70. package/docs/llm-docs/unistyles-llms-full.txt +1132 -534
  71. package/docs/llm-docs/unistyles-llms-small.txt +37 -37
  72. package/package.json +2 -2
  73. package/src/components/Combobox/Combobox.context.ts +26 -0
  74. package/src/components/Combobox/Combobox.docs.mdx +277 -0
  75. package/src/components/Combobox/Combobox.figma.tsx +60 -0
  76. package/src/components/Combobox/Combobox.props.ts +187 -0
  77. package/src/components/Combobox/Combobox.stories.tsx +233 -0
  78. package/src/components/Combobox/Combobox.tsx +446 -0
  79. package/src/components/Combobox/ComboboxOption.tsx +100 -0
  80. package/src/components/Combobox/index.ts +9 -0
  81. package/src/components/Modal/Modal.tsx +52 -74
  82. package/src/components/Modal/Modal.web.tsx +3 -3
  83. package/src/components/Pagination/Pagination.docs.mdx +99 -0
  84. package/src/components/Pagination/Pagination.figma.tsx +20 -0
  85. package/src/components/Pagination/Pagination.props.ts +28 -0
  86. package/src/components/Pagination/Pagination.stories.tsx +88 -0
  87. package/src/components/Pagination/Pagination.tsx +248 -0
  88. package/src/components/Pagination/Pagination.utils.test.ts +20 -0
  89. package/src/components/Pagination/Pagination.utils.ts +37 -0
  90. package/src/components/Pagination/index.ts +2 -0
  91. package/src/components/SafeAreaView/SafeAreaView.props.ts +20 -0
  92. package/src/components/SafeAreaView/SafeAreaView.tsx +173 -0
  93. package/src/components/SafeAreaView/index.ts +2 -0
  94. package/src/components/Select/Select.tsx +30 -27
  95. package/src/components/Table/Table.context.tsx +23 -0
  96. package/src/components/Table/Table.docs.mdx +239 -0
  97. package/src/components/Table/Table.figma.tsx +65 -0
  98. package/src/components/Table/Table.props.ts +65 -0
  99. package/src/components/Table/Table.stories.tsx +399 -0
  100. package/src/components/Table/Table.tsx +127 -0
  101. package/src/components/Table/Table.utils.test.ts +82 -0
  102. package/src/components/Table/Table.utils.ts +72 -0
  103. package/src/components/Table/TableBody.tsx +25 -0
  104. package/src/components/Table/TableCell.tsx +67 -0
  105. package/src/components/Table/TableHeader.tsx +41 -0
  106. package/src/components/Table/TableHeaderCell.tsx +136 -0
  107. package/src/components/Table/TablePagination.tsx +10 -0
  108. package/src/components/Table/TableRow.tsx +42 -0
  109. package/src/components/Table/index.ts +16 -0
  110. package/src/components/Timeline/Timeline.docs.mdx +177 -0
  111. package/src/components/Timeline/Timeline.figma.tsx +89 -0
  112. package/src/components/Timeline/Timeline.props.ts +51 -0
  113. package/src/components/Timeline/Timeline.stories.tsx +102 -0
  114. package/src/components/Timeline/Timeline.tsx +48 -0
  115. package/src/components/Timeline/TimelineItem.tsx +293 -0
  116. package/src/components/Timeline/index.ts +9 -0
  117. package/src/components/index.ts +5 -0
  118. package/src/tokens/components/dark/timeline.ts +2 -2
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { TickSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
+ import { Pressable, View } from 'react-native';
4
+ import { StyleSheet } from 'react-native-unistyles';
5
+ import { BodyText } from '../BodyText';
6
+ import { Icon } from '../Icon';
7
+ import { useComboboxContext } from './Combobox.context';
8
+ const ComboboxOption = ({ label, value, leadingIcon: LeftIcon, trailingIcon: RightIcon, selected, disabled, onPress, }) => {
9
+ const { selectedValue, selectOption } = useComboboxContext();
10
+ const isSelected = selected !== undefined ? selected : selectedValue === value;
11
+ styles.useVariants({ disabled });
12
+ const handlePress = () => {
13
+ if (disabled) {
14
+ return;
15
+ }
16
+ if (onPress) {
17
+ onPress(value);
18
+ return;
19
+ }
20
+ selectOption({ label, value });
21
+ };
22
+ return (_jsxs(Pressable, { onPress: handlePress, disabled: disabled, style: ({ pressed }) => [styles.container, pressed && styles.pressed], children: [!!LeftIcon && (_jsx(View, { children: _jsx(Icon, { as: LeftIcon, style: styles.icon }) })), _jsx(View, { style: styles.labelContainer, children: _jsx(BodyText, { children: label }) }), isSelected && (_jsx(View, { children: _jsx(Icon, { as: TickSmallIcon, style: styles.icon }) })), !!RightIcon && !isSelected && (_jsx(View, { children: _jsx(Icon, { as: RightIcon, style: styles.icon }) }))] }));
23
+ };
24
+ const styles = StyleSheet.create(theme => ({
25
+ container: {
26
+ flexDirection: 'row',
27
+ alignItems: 'center',
28
+ gap: theme.components.select.dropdown.item.gap,
29
+ borderRadius: theme.components.select.dropdown.item.borderRadius,
30
+ paddingVertical: theme.components.select.dropdown.item.padding,
31
+ paddingHorizontal: theme.components.select.dropdown.padding,
32
+ variants: {
33
+ disabled: {
34
+ true: {
35
+ opacity: theme.opacity.disabled,
36
+ },
37
+ },
38
+ },
39
+ _web: {
40
+ _hover: {
41
+ backgroundColor: theme.color.interactive.functional.surface.subtle.hover,
42
+ },
43
+ },
44
+ },
45
+ icon: {
46
+ color: theme.color.interactive.functional.foreground.subtle,
47
+ },
48
+ pressed: {
49
+ backgroundColor: theme.color.interactive.functional.surface.subtle.active,
50
+ },
51
+ labelContainer: {
52
+ flex: 1,
53
+ },
54
+ }));
55
+ ComboboxOption.displayName = 'ComboboxOption';
56
+ export default ComboboxOption;
@@ -0,0 +1,4 @@
1
+ export { default as Combobox } from './Combobox';
2
+ export { ComboboxContext, useComboboxContext } from './Combobox.context';
3
+ export { default as ComboboxOption } from './ComboboxOption';
4
+ export type { default as ComboboxProps, ComboboxOptionItemProps, ComboboxOptionProps, ComboboxRenderContentProps, } from './Combobox.props';
@@ -0,0 +1,3 @@
1
+ export { default as Combobox } from './Combobox';
2
+ export { ComboboxContext, useComboboxContext } from './Combobox.context';
3
+ export { default as ComboboxOption } from './ComboboxOption';
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { BottomSheetFooter, } from '@gorhom/bottom-sheet';
3
3
  import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
4
- import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
5
- import { AccessibilityInfo, Dimensions, Platform, ScrollView, View, findNodeHandle, } from 'react-native';
4
+ import { useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
5
+ import { AccessibilityInfo, Platform, ScrollView, View, findNodeHandle } from 'react-native';
6
6
  import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming, } from 'react-native-reanimated';
7
7
  import { StyleSheet } from 'react-native-unistyles';
8
8
  import { useTheme } from '../../hooks';
@@ -11,6 +11,7 @@ import { BodyText } from '../BodyText';
11
11
  import { BottomSheetModal, BottomSheetScrollView } from '../BottomSheet';
12
12
  import { Button } from '../Button';
13
13
  import { Heading } from '../Heading';
14
+ import { SafeAreaView } from '../SafeAreaView';
14
15
  import { Spinner } from '../Spinner';
15
16
  import { UnstyledIconButton } from '../UnstyledIconButton';
16
17
  const Modal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress = true, closeOnSecondaryButtonPress = true, loading, loadingHeading = 'Loading...', fullscreen = false, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, inNavModal = false, stickyFooter = true, background = 'default', scrollable = true, ...props }) => {
@@ -21,13 +22,6 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
21
22
  const backgroundOpacity = useSharedValue(0);
22
23
  const pretendContentTranslateY = useSharedValue(20);
23
24
  const isBrandBackground = background === 'brand';
24
- const [inNavModalHeight, setInNavModalHeight] = useState();
25
- const isNavModalFullScreen = useMemo(() => {
26
- if (!inNavModalHeight || !inNavModal)
27
- return false;
28
- const screenHeight = Dimensions.get('window').height;
29
- return inNavModalHeight >= screenHeight;
30
- }, [inNavModalHeight, inNavModal]);
31
25
  const triggerCloseAnimation = useCallback(() => {
32
26
  if (Platform.OS === 'android' && inNavModal) {
33
27
  pretendContentTranslateY.value = withTiming(20, {
@@ -39,7 +33,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
39
33
  easing: Easing.in(Easing.quad),
40
34
  });
41
35
  }
42
- }, [Platform.OS, inNavModal, pretendContentTranslateY, backgroundOpacity]);
36
+ }, [inNavModal, pretendContentTranslateY, backgroundOpacity]);
43
37
  useImperativeHandle(ref, () => ({
44
38
  ...bottomSheetModalRef.current,
45
39
  triggerCloseAnimation,
@@ -84,28 +78,28 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
84
78
  }
85
79
  props.onChange?.(index, position, type);
86
80
  };
87
- const handleCloseButtonPress = () => {
81
+ const handleCloseButtonPress = useCallback(() => {
88
82
  bottomSheetModalRef.current?.dismiss();
89
83
  if (onPressCloseButton) {
90
84
  onPressCloseButton();
91
85
  }
92
- };
93
- const handlePrimaryButtonPress = () => {
86
+ }, [onPressCloseButton]);
87
+ const handlePrimaryButtonPress = useCallback(() => {
94
88
  if (onPressPrimaryButton) {
95
89
  onPressPrimaryButton();
96
90
  }
97
91
  if (closeOnPrimaryButtonPress) {
98
92
  bottomSheetModalRef.current?.dismiss();
99
93
  }
100
- };
101
- const handleSecondaryButtonPress = () => {
94
+ }, [closeOnPrimaryButtonPress, onPressPrimaryButton]);
95
+ const handleSecondaryButtonPress = useCallback(() => {
102
96
  if (onPressSecondaryButton) {
103
97
  onPressSecondaryButton();
104
98
  }
105
99
  if (closeOnSecondaryButtonPress) {
106
100
  bottomSheetModalRef.current?.dismiss();
107
101
  }
108
- };
102
+ }, [closeOnSecondaryButtonPress, onPressSecondaryButton]);
109
103
  const noButtons = !onPressPrimaryButton && !onPressSecondaryButton;
110
104
  styles.useVariants({
111
105
  loading,
@@ -114,11 +108,19 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
114
108
  stickyFooter,
115
109
  showHandle: props.showHandle,
116
110
  background: isBrandBackground ? 'brand' : 'primary',
117
- ...(inNavModal && {
118
- fullscreen: isNavModalFullScreen,
119
- }),
120
111
  });
121
- const footer = (_jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, inverted: isBrandBackground && inNavModal, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, inverted: isBrandBackground && inNavModal, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] }));
112
+ const footer = useMemo(() => (_jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, inverted: isBrandBackground && inNavModal, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, inverted: isBrandBackground && inNavModal, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] })), [
113
+ handlePrimaryButtonPress,
114
+ handleSecondaryButtonPress,
115
+ inNavModal,
116
+ isBrandBackground,
117
+ onPressPrimaryButton,
118
+ onPressSecondaryButton,
119
+ primaryButtonProps,
120
+ primaryButtonText,
121
+ secondaryButtonProps,
122
+ secondaryButtonText,
123
+ ]);
122
124
  const InNavModalContainer = scrollable ? ScrollView : View;
123
125
  const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg", color: isBrandBackground && inNavModal ? theme.color.icon.inverted : undefined }), _jsx(Heading, { size: "lg", textAlign: "center", inverted: isBrandBackground && inNavModal, children: loadingHeading })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description && !image ? (_jsx(BodyText, { accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted: isBrandBackground && inNavModal, ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [image, _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] })] })) : null, inNavModal && (_jsxs(InNavModalContainer, { style: {
124
126
  flex: stickyFooter ? 1 : 0,
@@ -126,20 +128,11 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
126
128
  }, ...(scrollable ? { contentContainerStyle: { paddingHorizontal: 1 } } : {}), children: [children, !stickyFooter ? (_jsx(View, { style: styles.inNavModalFooterContainer, children: footer })) : null] })), !inNavModal && children, ((!stickyFooter && !inNavModal) || (inNavModal && stickyFooter)) && !noButtons
127
129
  ? footer
128
130
  : null] })) }));
129
- const renderFooter = useCallback((props) => (_jsx(BottomSheetFooter, { ...props, children: _jsx(View, { style: styles.footerWrap, children: footer }) })), [
130
- onPressPrimaryButton,
131
- primaryButtonText,
132
- onPressSecondaryButton,
133
- secondaryButtonText,
134
- primaryButtonProps,
135
- secondaryButtonProps,
136
- ]);
137
- return inNavModal ? (_jsxs(View, { onLayout: e => {
138
- setInNavModalHeight(e.nativeEvent.layout.height);
139
- }, style: {
131
+ const renderFooter = useCallback((props) => (_jsx(BottomSheetFooter, { ...props, children: _jsx(View, { style: styles.footerWrap, children: footer }) })), [footer]);
132
+ return inNavModal ? (_jsxs(View, { style: {
140
133
  flex: 1,
141
134
  backgroundColor: theme.color.background[isBrandBackground ? 'brand' : 'primary'],
142
- }, children: [Platform.OS === 'android' ? (_jsx(Animated.View, { style: [styles.androidContainer, animatedBackgroundStyle], children: _jsx(Animated.View, { style: [styles.pretendContent, animatedPretendContentStyle] }) })) : null, _jsx(Animated.View, { style: [styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle], children: _jsx(View, { style: styles.inNavModalContent, children: content }) })] })) : (_jsxs(BottomSheetModal, { ref: bottomSheetModalRef, enableDynamicSizing: true, snapPoints: image || fullscreen ? ['90%'] : props.snapPoints, showHandle: typeof loading !== 'undefined' && loading ? false : props.showHandle, accessible: false, style: styles.modal, footerComponent: stickyFooter && !noButtons ? renderFooter : undefined, ...props, onChange: handleChange, children: [loading ? _jsx(View, { style: styles.loadingTop }) : null, _jsx(BottomSheetScrollView, { contentContainerStyle: styles.scrollView, ref: scrollViewRef, children: content })] }));
135
+ }, children: [Platform.OS === 'android' ? (_jsx(Animated.View, { style: [styles.androidContainer, animatedBackgroundStyle], children: _jsx(Animated.View, { style: [styles.pretendContent, animatedPretendContentStyle] }) })) : null, _jsx(Animated.View, { style: [styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle], children: _jsx(SafeAreaView, { edges: ['top', 'bottom'], style: styles.inNavModalContent, children: content }) })] })) : (_jsxs(BottomSheetModal, { ref: bottomSheetModalRef, enableDynamicSizing: true, snapPoints: image || fullscreen ? ['90%'] : props.snapPoints, showHandle: typeof loading !== 'undefined' && loading ? false : props.showHandle, accessible: false, style: styles.modal, footerComponent: stickyFooter && !noButtons ? renderFooter : undefined, ...props, onChange: handleChange, children: [loading ? _jsx(View, { style: styles.loadingTop }) : null, _jsx(BottomSheetScrollView, { contentContainerStyle: styles.scrollView, ref: scrollViewRef, children: content })] }));
143
136
  };
144
137
  const styles = StyleSheet.create((theme, rt) => ({
145
138
  modal: {
@@ -245,7 +238,7 @@ const styles = StyleSheet.create((theme, rt) => ({
245
238
  borderTopLeftRadius: theme.components.modal.borderRadius,
246
239
  borderTopRightRadius: theme.components.modal.borderRadius,
247
240
  backgroundColor: theme.color.surface.neutral.strong,
248
- paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
241
+ padding: theme.components.bottomSheet.padding,
249
242
  variants: {
250
243
  background: {
251
244
  primary: {},
@@ -253,15 +246,6 @@ const styles = StyleSheet.create((theme, rt) => ({
253
246
  backgroundColor: theme.color.background.brand,
254
247
  },
255
248
  },
256
- fullscreen: {
257
- true: {
258
- padding: theme.components.bottomSheet.padding,
259
- paddingTop: rt.insets.top,
260
- },
261
- false: {
262
- padding: theme.components.bottomSheet.padding,
263
- },
264
- },
265
249
  },
266
250
  },
267
251
  inNavModalFooterContainer: {
@@ -141,13 +141,13 @@ const styles = StyleSheet.create((theme, rt) => ({
141
141
  borderTopRightRadius: theme.components.modal.borderRadius,
142
142
  backgroundColor: theme.color.surface.neutral.strong,
143
143
  gap: theme.components.modal.gap,
144
- padding: theme.components.modal.padding,
144
+ padding: theme.components.bottomSheet.padding,
145
145
  paddingBottom: theme.components.modal.padding + rt.insets.bottom,
146
146
  },
147
147
  androidContainer: {
148
148
  height: rt.insets.top + 18,
149
- paddingLeft: theme.components.modal.padding,
150
- paddingRight: theme.components.modal.padding,
149
+ paddingLeft: theme.components.bottomSheet.padding,
150
+ paddingRight: theme.components.bottomSheet.padding,
151
151
  justifyContent: 'flex-end',
152
152
  },
153
153
  pretendContent: {
@@ -0,0 +1,6 @@
1
+ import type PaginationProps from './Pagination.props';
2
+ declare const Pagination: {
3
+ ({ currentPage, totalPages, onPageChange, condensed, hideSkipButtons, style, ...props }: PaginationProps): import("react/jsx-runtime").JSX.Element;
4
+ displayName: string;
5
+ };
6
+ export default Pagination;
@@ -0,0 +1,125 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronLeftSmallIcon, ChevronRightSmallIcon, SkipFirstSmallIcon, SkipLastSmallIcon, } from '@utilitywarehouse/hearth-react-native-icons';
3
+ import { useState } from 'react';
4
+ import { Pressable, View } from 'react-native';
5
+ import { StyleSheet } from 'react-native-unistyles';
6
+ import { BodyText } from '../BodyText';
7
+ import { UnstyledIconButton } from '../UnstyledIconButton';
8
+ import { ELLIPSIS, generatePageNumbers } from './Pagination.utils';
9
+ const PaginationItem = ({ label, selected = false, onPress }) => {
10
+ const [isFocused, setIsFocused] = useState(false);
11
+ styles.useVariants({ selected });
12
+ return (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: `Go to page ${label}`, accessibilityState: { selected }, onBlur: () => setIsFocused(false), onFocus: () => setIsFocused(true), onPress: onPress, style: ({ pressed }) => [
13
+ styles.pageItem,
14
+ pressed && !selected && styles.pageItemPressed,
15
+ isFocused && styles.pageItemFocused,
16
+ ], children: _jsx(BodyText, { size: "md", style: styles.pageItemText, children: label }) }));
17
+ };
18
+ const PaginationArrowButton = ({ accessibilityLabel, disabled, icon, onPress, }) => {
19
+ return (_jsx(UnstyledIconButton, { accessibilityLabel: accessibilityLabel, disabled: disabled, icon: icon, onPress: onPress, size: "sm", style: styles.arrowButton }));
20
+ };
21
+ const Pagination = ({ currentPage, totalPages, onPageChange, condensed = false, hideSkipButtons = false, style, ...props }) => {
22
+ const pages = generatePageNumbers(currentPage, totalPages);
23
+ const handleFirst = () => {
24
+ if (currentPage !== 1) {
25
+ onPageChange(1);
26
+ }
27
+ };
28
+ const handlePrevious = () => {
29
+ if (currentPage > 1) {
30
+ onPageChange(currentPage - 1);
31
+ }
32
+ };
33
+ const handleNext = () => {
34
+ if (currentPage < totalPages) {
35
+ onPageChange(currentPage + 1);
36
+ }
37
+ };
38
+ const handleLast = () => {
39
+ if (currentPage !== totalPages) {
40
+ onPageChange(totalPages);
41
+ }
42
+ };
43
+ return (_jsxs(View, { accessibilityLabel: "Pagination", style: [styles.container, style], ...props, children: [_jsxs(View, { style: styles.controllers, children: [!hideSkipButtons ? (_jsx(PaginationArrowButton, { accessibilityLabel: "Go to first page", disabled: currentPage === 1, icon: SkipFirstSmallIcon, onPress: handleFirst })) : null, _jsx(PaginationArrowButton, { accessibilityLabel: "Go to previous page", disabled: currentPage === 1, icon: ChevronLeftSmallIcon, onPress: handlePrevious })] }), condensed ? (_jsxs(BodyText, { size: "md", children: ["Page ", currentPage, " of ", totalPages] })) : (_jsx(View, { style: styles.pages, children: pages.map((page, index) => {
44
+ if (page === ELLIPSIS) {
45
+ return (_jsx(BodyText, { size: "md", style: styles.ellipsis, children: ELLIPSIS }, `ellipsis-${index}`));
46
+ }
47
+ return (_jsx(PaginationItem, { label: page, onPress: () => onPageChange(page), selected: page === currentPage }, page));
48
+ }) })), _jsxs(View, { style: styles.controllers, children: [_jsx(PaginationArrowButton, { accessibilityLabel: "Go to next page", disabled: currentPage === totalPages, icon: ChevronRightSmallIcon, onPress: handleNext }), !hideSkipButtons ? (_jsx(PaginationArrowButton, { accessibilityLabel: "Go to last page", disabled: currentPage === totalPages, icon: SkipLastSmallIcon, onPress: handleLast })) : null] })] }));
49
+ };
50
+ PaginationItem.displayName = 'PaginationItem';
51
+ Pagination.displayName = 'Pagination';
52
+ const styles = StyleSheet.create(theme => ({
53
+ container: {
54
+ width: '100%',
55
+ minHeight: theme.components.pagination.item.height,
56
+ flexDirection: 'row',
57
+ alignItems: 'center',
58
+ justifyContent: 'space-between',
59
+ gap: theme.components.pagination.gap,
60
+ },
61
+ controllers: {
62
+ flexDirection: 'row',
63
+ alignItems: 'center',
64
+ gap: theme.components.pagination.gap,
65
+ },
66
+ arrowButton: {
67
+ width: theme.components.pagination.item.width,
68
+ height: theme.components.pagination.item.height,
69
+ },
70
+ pages: {
71
+ flex: 1,
72
+ flexDirection: 'row',
73
+ alignItems: 'center',
74
+ justifyContent: 'center',
75
+ flexWrap: 'wrap',
76
+ gap: theme.components.pagination.gap,
77
+ },
78
+ pageItem: {
79
+ width: theme.components.pagination.item.width,
80
+ height: theme.components.pagination.item.height,
81
+ borderRadius: theme.components.pagination.item.radius,
82
+ alignItems: 'center',
83
+ justifyContent: 'center',
84
+ backgroundColor: 'transparent',
85
+ _web: {
86
+ _hover: {
87
+ backgroundColor: theme.color.interactive.functional.surface.subtle.hover,
88
+ },
89
+ },
90
+ variants: {
91
+ selected: {
92
+ true: {
93
+ backgroundColor: theme.color.interactive.brand.surface.strong.default,
94
+ },
95
+ },
96
+ },
97
+ },
98
+ pageItemPressed: {
99
+ backgroundColor: theme.color.interactive.functional.surface.subtle.active,
100
+ },
101
+ pageItemFocused: {
102
+ outlineWidth: 2,
103
+ outlineOffset: -2,
104
+ outlineColor: theme.color.border.strong,
105
+ outlineStyle: 'solid',
106
+ },
107
+ pageItemText: {
108
+ variants: {
109
+ selected: {
110
+ true: {
111
+ color: theme.color.text.inverted,
112
+ },
113
+ false: {
114
+ color: theme.color.text.primary,
115
+ },
116
+ },
117
+ },
118
+ },
119
+ ellipsis: {
120
+ minWidth: theme.components.pagination.item.width,
121
+ textAlign: 'center',
122
+ color: theme.color.text.primary,
123
+ },
124
+ }));
125
+ export default Pagination;
@@ -0,0 +1,26 @@
1
+ import type { ViewProps } from 'react-native';
2
+ export interface PaginationProps extends ViewProps {
3
+ /**
4
+ * The current active page number (1-indexed).
5
+ */
6
+ currentPage: number;
7
+ /**
8
+ * The total number of pages.
9
+ */
10
+ totalPages: number;
11
+ /**
12
+ * Callback fired when the page changes.
13
+ */
14
+ onPageChange: (page: number) => void;
15
+ /**
16
+ * Whether to show condensed copy instead of individual page items.
17
+ * @default false
18
+ */
19
+ condensed?: boolean;
20
+ /**
21
+ * Whether to hide the first and last page controls.
22
+ * @default false
23
+ */
24
+ hideSkipButtons?: boolean;
25
+ }
26
+ export default PaginationProps;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare const ELLIPSIS = "...";
2
+ export declare const generatePageNumbers: (currentPage: number, totalPages: number) => Array<number | typeof ELLIPSIS>;
@@ -0,0 +1,20 @@
1
+ export const ELLIPSIS = '...';
2
+ const MAX_VISIBLE_ITEMS = 7;
3
+ export const generatePageNumbers = (currentPage, totalPages) => {
4
+ if (totalPages <= MAX_VISIBLE_ITEMS) {
5
+ return Array.from({ length: totalPages }, (_, index) => index + 1);
6
+ }
7
+ const pages = [];
8
+ const isNearStart = currentPage <= 4;
9
+ const isNearEnd = currentPage > totalPages - 4;
10
+ if (isNearStart) {
11
+ pages.push(1, 2, 3, 4, 5, ELLIPSIS, totalPages);
12
+ return pages;
13
+ }
14
+ if (isNearEnd) {
15
+ pages.push(1, ELLIPSIS, totalPages - 4, totalPages - 3, totalPages - 2, totalPages - 1, totalPages);
16
+ return pages;
17
+ }
18
+ pages.push(1, ELLIPSIS, currentPage - 1, currentPage, currentPage + 1, ELLIPSIS, totalPages);
19
+ return pages;
20
+ };
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ELLIPSIS, generatePageNumbers } from './Pagination.utils';
3
+ describe('generatePageNumbers', () => {
4
+ it('returns all pages when the total fits within the visible limit', () => {
5
+ expect(generatePageNumbers(1, 7)).toEqual([1, 2, 3, 4, 5, 6, 7]);
6
+ });
7
+ it('returns the leading window when the current page is near the start', () => {
8
+ expect(generatePageNumbers(2, 10)).toEqual([1, 2, 3, 4, 5, ELLIPSIS, 10]);
9
+ });
10
+ it('returns the centered window when the current page is in the middle', () => {
11
+ expect(generatePageNumbers(5, 10)).toEqual([1, ELLIPSIS, 4, 5, 6, ELLIPSIS, 10]);
12
+ });
13
+ it('returns the trailing window when the current page is near the end', () => {
14
+ expect(generatePageNumbers(9, 10)).toEqual([1, ELLIPSIS, 6, 7, 8, 9, 10]);
15
+ });
16
+ });
@@ -0,0 +1,2 @@
1
+ export { default as Pagination } from './Pagination';
2
+ export type { default as PaginationProps } from './Pagination.props';
@@ -0,0 +1 @@
1
+ export { default as Pagination } from './Pagination';
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import SafeAreaViewProps from './SafeAreaView.props';
4
+ declare const SafeAreaView: React.ForwardRefExoticComponent<SafeAreaViewProps & React.RefAttributes<View>>;
5
+ export default SafeAreaView;
@@ -0,0 +1,117 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { StyleSheet as RNStyleSheet, useWindowDimensions, View, } from 'react-native';
4
+ import { UnistylesRuntime } from '../../core';
5
+ const DEFAULT_EDGES = ['top', 'right', 'bottom', 'left'];
6
+ const EMPTY_INSETS = { top: 0, right: 0, bottom: 0, left: 0 };
7
+ const EDGE_EPSILON = 1;
8
+ const getNumericStyleValue = (value) => {
9
+ return typeof value === 'number' ? value : 0;
10
+ };
11
+ const getStyleInsetValue = (style, mode, edge) => {
12
+ const prefix = mode === 'margin' ? 'margin' : 'padding';
13
+ if (!style) {
14
+ // No style specified at all; treat as zero inset for safe-area calculations.
15
+ return 0;
16
+ }
17
+ if (edge === 'top') {
18
+ const raw = style[`${prefix}Top`] ?? style[`${prefix}Vertical`] ?? style[prefix];
19
+ if (raw == null) {
20
+ return 0;
21
+ }
22
+ return getNumericStyleValue(raw);
23
+ }
24
+ if (edge === 'bottom') {
25
+ const raw = style[`${prefix}Bottom`] ?? style[`${prefix}Vertical`] ?? style[prefix];
26
+ if (raw == null) {
27
+ return 0;
28
+ }
29
+ return getNumericStyleValue(raw);
30
+ }
31
+ if (edge === 'left') {
32
+ const raw = style[`${prefix}Left`] ?? style[`${prefix}Horizontal`] ?? style[prefix];
33
+ if (raw == null) {
34
+ return 0;
35
+ }
36
+ return getNumericStyleValue(raw);
37
+ }
38
+ const raw = style[`${prefix}Right`] ?? style[`${prefix}Horizontal`] ?? style[prefix];
39
+ if (raw == null) {
40
+ return 0;
41
+ }
42
+ return getNumericStyleValue(raw);
43
+ };
44
+ const SafeAreaView = forwardRef(({ children, edges = DEFAULT_EDGES, mode = 'padding', onLayout, style, ...props }, ref) => {
45
+ const viewRef = useRef(null);
46
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
47
+ const [edgeInsets, setEdgeInsets] = useState(EMPTY_INSETS);
48
+ const flattenedStyle = useMemo(() => RNStyleSheet.flatten(style), [style]);
49
+ const updateEdgeInsets = useCallback(() => {
50
+ const currentView = viewRef.current;
51
+ if (!currentView) {
52
+ return;
53
+ }
54
+ currentView.measureInWindow((x, y, width, height) => {
55
+ const runtimeInsets = UnistylesRuntime.insets ?? EMPTY_INSETS;
56
+ const nextEdgeInsets = {
57
+ top: edges.includes('top') ? Math.max(runtimeInsets.top - Math.max(y, 0), 0) : 0,
58
+ right: edges.includes('right')
59
+ ? Math.max(runtimeInsets.right - Math.max(windowWidth - (x + width), 0), 0)
60
+ : 0,
61
+ bottom: edges.includes('bottom')
62
+ ? Math.max(runtimeInsets.bottom - Math.max(windowHeight - (y + height), 0), 0)
63
+ : 0,
64
+ left: edges.includes('left') ? Math.max(runtimeInsets.left - Math.max(x, 0), 0) : 0,
65
+ };
66
+ setEdgeInsets(previousInsets => {
67
+ const hasChanged = Object.keys(nextEdgeInsets).some(edge => Math.abs(previousInsets[edge] - nextEdgeInsets[edge]) > EDGE_EPSILON);
68
+ return hasChanged ? nextEdgeInsets : previousInsets;
69
+ });
70
+ });
71
+ }, [edges, windowHeight, windowWidth]);
72
+ useEffect(() => {
73
+ const frame = requestAnimationFrame(updateEdgeInsets);
74
+ return () => cancelAnimationFrame(frame);
75
+ }, [updateEdgeInsets]);
76
+ const handleRef = useCallback((node) => {
77
+ viewRef.current = node;
78
+ if (typeof ref === 'function') {
79
+ ref(node);
80
+ }
81
+ else if (ref) {
82
+ ref.current = node;
83
+ }
84
+ }, [ref]);
85
+ const handleLayout = useCallback((event) => {
86
+ onLayout?.(event);
87
+ requestAnimationFrame(updateEdgeInsets);
88
+ }, [onLayout, updateEdgeInsets]);
89
+ const safeAreaStyle = useMemo(() => {
90
+ const nextStyle = {};
91
+ if (mode === 'padding') {
92
+ nextStyle.paddingTop = getStyleInsetValue(flattenedStyle, mode, 'top') + edgeInsets.top;
93
+ nextStyle.paddingRight =
94
+ getStyleInsetValue(flattenedStyle, mode, 'right') + edgeInsets.right;
95
+ nextStyle.paddingBottom =
96
+ getStyleInsetValue(flattenedStyle, mode, 'bottom') + edgeInsets.bottom;
97
+ nextStyle.paddingLeft = getStyleInsetValue(flattenedStyle, mode, 'left') + edgeInsets.left;
98
+ return nextStyle;
99
+ }
100
+ nextStyle.marginTop = getStyleInsetValue(flattenedStyle, mode, 'top') + edgeInsets.top;
101
+ nextStyle.marginRight = getStyleInsetValue(flattenedStyle, mode, 'right') + edgeInsets.right;
102
+ nextStyle.marginBottom =
103
+ getStyleInsetValue(flattenedStyle, mode, 'bottom') + edgeInsets.bottom;
104
+ nextStyle.marginLeft = getStyleInsetValue(flattenedStyle, mode, 'left') + edgeInsets.left;
105
+ return nextStyle;
106
+ }, [
107
+ edgeInsets.bottom,
108
+ edgeInsets.left,
109
+ edgeInsets.right,
110
+ edgeInsets.top,
111
+ flattenedStyle,
112
+ mode,
113
+ ]);
114
+ return (_jsx(View, { ref: handleRef, onLayout: handleLayout, style: [style, safeAreaStyle], ...props, children: children }));
115
+ });
116
+ SafeAreaView.displayName = 'SafeAreaView';
117
+ export default SafeAreaView;
@@ -0,0 +1,17 @@
1
+ import { ViewProps } from 'react-native';
2
+ export type SafeAreaEdge = 'top' | 'right' | 'bottom' | 'left';
3
+ interface SafeAreaViewProps extends ViewProps {
4
+ /**
5
+ * Which edges should receive safe area compensation.
6
+ *
7
+ * @default ['top', 'right', 'bottom', 'left']
8
+ */
9
+ edges?: SafeAreaEdge[];
10
+ /**
11
+ * Whether safe area compensation should be applied as padding or margin.
12
+ *
13
+ * @default 'padding'
14
+ */
15
+ mode?: 'padding' | 'margin';
16
+ }
17
+ export default SafeAreaViewProps;
@@ -0,0 +1,2 @@
1
+ export { default as SafeAreaView } from './SafeAreaView';
2
+ export type { default as SafeAreaViewProps, SafeAreaEdge } from './SafeAreaView.props';
@@ -0,0 +1 @@
1
+ export { default as SafeAreaView } from './SafeAreaView';
@@ -11,6 +11,7 @@ import { Icon } from '../Icon';
11
11
  import { Input } from '../Input';
12
12
  import { SelectContext } from './Select.context';
13
13
  import SelectOption from './SelectOption';
14
+ import { SafeAreaView } from '../SafeAreaView';
14
15
  const Select = ({ options = [], value, onValueChange, label, labelVariant = 'body', placeholder = 'Select an option', disabled = false, leadingIcon: LeadingIcon, validationStatus = 'initial', helperText, helperIcon, invalidText, validText, required = true, children, bottomSheetProps, menuHeading, readonly = false, emptyText = 'No options available', listProps, searchable = false, searchPlaceholder = 'Search', ...rest }) => {
15
16
  const formFieldContext = useFormFieldContext();
16
17
  const validationStatusFromContext = formFieldContext?.validationStatus ?? validationStatus;
@@ -72,11 +73,11 @@ const Select = ({ options = [], value, onValueChange, label, labelVariant = 'bod
72
73
  ], children: [!!LeadingIcon && (_jsx(View, { children: (() => {
73
74
  const IconAny = Icon;
74
75
  return _jsx(IconAny, { as: LeadingIcon, style: styles.icon });
75
- })() })), _jsx(View, { style: styles.optionContainer, children: _jsx(BodyText, { numberOfLines: 1, style: styles.placeholderText, children: selectedOption?.label || selectedLabel || placeholder }) }), _jsx(View, { children: _jsx(Icon, { as: ExpandSmallIcon, style: styles.icon }) })] }) }), _jsx(BottomSheetModal, { ref: bottomSheetModalRef, snapPoints: ['25%', '40%', '80%'], onChange: handleClose, enableDynamicSizing: false, ...bottomSheetProps, children: _jsxs(SelectContext.Provider, { value: {
76
+ })() })), _jsx(View, { style: styles.optionContainer, children: _jsx(BodyText, { numberOfLines: 1, style: styles.placeholderText, children: selectedOption?.label || selectedLabel || placeholder }) }), _jsx(View, { children: _jsx(Icon, { as: ExpandSmallIcon, style: styles.icon }) })] }) }), _jsx(BottomSheetModal, { ref: bottomSheetModalRef, snapPoints: ['25%', '40%', '80%'], onChange: handleClose, enableDynamicSizing: false, ...bottomSheetProps, children: _jsx(SelectContext.Provider, { value: {
76
77
  selectedValue: value,
77
78
  onValueChange,
78
79
  close: closeBottomSheet,
79
- }, children: [menuHeading && (_jsx(View, { style: styles.headingContainer, children: _jsx(DetailText, { size: "lg", children: menuHeading }) })), searchable && (_jsx(View, { style: styles.searchContainer, children: _jsx(Input, { placeholder: searchPlaceholder, value: search, inBottomSheet: true, onChangeText: setSearch, type: "search" }) })), children ? (_jsx(BottomSheetScrollView, { children: children })) : (_jsx(BottomSheetFlatList, { data: filteredOptions, keyExtractor: (option) => option.value, renderItem: renderSelectOption, ListEmptyComponent: renderEmptyComponent, ...listProps }))] }) })] }));
80
+ }, children: _jsxs(SafeAreaView, { edges: ['top'], style: { flex: 1 }, children: [menuHeading && (_jsx(View, { style: styles.headingContainer, children: _jsx(DetailText, { size: "lg", children: menuHeading }) })), searchable && (_jsx(View, { style: styles.searchContainer, children: _jsx(Input, { placeholder: searchPlaceholder, value: search, inBottomSheet: true, onChangeText: setSearch, type: "search" }) })), children ? (_jsx(BottomSheetScrollView, { children: children })) : (_jsx(BottomSheetFlatList, { data: filteredOptions, keyExtractor: (option) => option.value, renderItem: renderSelectOption, ListEmptyComponent: renderEmptyComponent, ...listProps }))] }) }) })] }));
80
81
  };
81
82
  const styles = StyleSheet.create(theme => ({
82
83
  container: {