ferns-ui 0.36.4 → 0.36.5

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 (77) hide show
  1. package/package.json +3 -4
  2. package/src/ActionSheet.tsx +1231 -0
  3. package/src/Avatar.tsx +317 -0
  4. package/src/Badge.tsx +65 -0
  5. package/src/Banner.tsx +124 -0
  6. package/src/BlurBox.native.tsx +40 -0
  7. package/src/BlurBox.tsx +31 -0
  8. package/src/Body.tsx +32 -0
  9. package/src/Box.tsx +308 -0
  10. package/src/Button.tsx +219 -0
  11. package/src/Card.tsx +23 -0
  12. package/src/CheckBox.tsx +118 -0
  13. package/src/Common.ts +2743 -0
  14. package/src/Constants.ts +53 -0
  15. package/src/CustomSelect.tsx +85 -0
  16. package/src/DateTimeActionSheet.tsx +409 -0
  17. package/src/DateTimeField.android.tsx +101 -0
  18. package/src/DateTimeField.ios.tsx +83 -0
  19. package/src/DateTimeField.tsx +69 -0
  20. package/src/DecimalRangeActionSheet.tsx +113 -0
  21. package/src/ErrorBoundary.tsx +37 -0
  22. package/src/ErrorPage.tsx +44 -0
  23. package/src/FernsProvider.tsx +21 -0
  24. package/src/Field.tsx +299 -0
  25. package/src/FieldWithLabels.tsx +36 -0
  26. package/src/FlatList.tsx +2 -0
  27. package/src/Form.tsx +182 -0
  28. package/src/HeaderButtons.tsx +107 -0
  29. package/src/Heading.tsx +53 -0
  30. package/src/HeightActionSheet.tsx +104 -0
  31. package/src/Hyperlink.tsx +181 -0
  32. package/src/Icon.tsx +24 -0
  33. package/src/IconButton.tsx +165 -0
  34. package/src/Image.tsx +50 -0
  35. package/src/ImageBackground.tsx +14 -0
  36. package/src/InfoTooltipButton.tsx +23 -0
  37. package/src/Layer.tsx +17 -0
  38. package/src/Link.tsx +17 -0
  39. package/src/Mask.tsx +21 -0
  40. package/src/MediaQuery.ts +46 -0
  41. package/src/Meta.tsx +9 -0
  42. package/src/Modal.tsx +248 -0
  43. package/src/ModalSheet.tsx +58 -0
  44. package/src/NumberPickerActionSheet.tsx +66 -0
  45. package/src/Page.tsx +133 -0
  46. package/src/Permissions.ts +44 -0
  47. package/src/PickerSelect.tsx +553 -0
  48. package/src/Pill.tsx +24 -0
  49. package/src/Pog.tsx +87 -0
  50. package/src/ProgressBar.tsx +55 -0
  51. package/src/ScrollView.tsx +2 -0
  52. package/src/SegmentedControl.tsx +102 -0
  53. package/src/SelectList.tsx +89 -0
  54. package/src/SideDrawer.tsx +62 -0
  55. package/src/Spinner.tsx +20 -0
  56. package/src/SplitPage.native.tsx +160 -0
  57. package/src/SplitPage.tsx +302 -0
  58. package/src/Switch.tsx +19 -0
  59. package/src/Table.tsx +87 -0
  60. package/src/TableHeader.tsx +36 -0
  61. package/src/TableHeaderCell.tsx +76 -0
  62. package/src/TableRow.tsx +87 -0
  63. package/src/TapToEdit.tsx +221 -0
  64. package/src/Text.tsx +131 -0
  65. package/src/TextArea.tsx +16 -0
  66. package/src/TextField.tsx +401 -0
  67. package/src/TextFieldNumberActionSheet.tsx +61 -0
  68. package/src/Toast.tsx +106 -0
  69. package/src/Tooltip.tsx +269 -0
  70. package/src/UnifiedScreens.ts +24 -0
  71. package/src/Unifier.ts +371 -0
  72. package/src/Utilities.tsx +159 -0
  73. package/src/WithLabel.tsx +57 -0
  74. package/src/dayjsExtended.ts +10 -0
  75. package/src/index.tsx +1346 -0
  76. package/src/polyfill.d.ts +11 -0
  77. package/src/tableContext.tsx +80 -0
@@ -0,0 +1,2 @@
1
+ import {ScrollView} from "react-native";
2
+ export {ScrollView};
@@ -0,0 +1,102 @@
1
+ import React from "react";
2
+
3
+ import {Box} from "./Box";
4
+ import {SegmentedControlProps} from "./Common";
5
+ import {Text} from "./Text";
6
+
7
+ export const SegmentedControl = ({
8
+ items,
9
+ onChange = () => {},
10
+ selectedItemIndexes = undefined,
11
+ selectedItemIndex = undefined,
12
+ multiselect = false,
13
+ selectLimit = 1,
14
+ }: SegmentedControlProps) => {
15
+ const renderItem = (item: string | React.ReactNode) => {
16
+ return (
17
+ <Text align="center" weight="bold">
18
+ {item}
19
+ </Text>
20
+ );
21
+ // if (typeof item === "string") {
22
+ // return <Text weight="bold">{item}</Text>;
23
+ // } else {
24
+ // return item;
25
+ // }
26
+ };
27
+
28
+ if (selectedItemIndex === undefined && selectedItemIndexes === undefined) {
29
+ console.warn("One of the following must be defined: selectedItemIndex, selectedItemIndexes");
30
+ return null;
31
+ }
32
+
33
+ if (!multiselect && selectedItemIndexes?.length && selectedItemIndexes?.length > 1) {
34
+ console.warn("Multiple selections not allowed without multiselect flag");
35
+ return null;
36
+ }
37
+
38
+ if (selectedItemIndexes?.length && selectedItemIndexes?.length > selectLimit) {
39
+ console.warn("The number of selected items exceeds the limit");
40
+ return null;
41
+ }
42
+
43
+ const isTabActive = (index: any) => {
44
+ return selectedItemIndex === index || selectedItemIndexes?.includes(index)
45
+ ? "white"
46
+ : "lightGray";
47
+ };
48
+
49
+ return (
50
+ <Box
51
+ color="lightGray"
52
+ direction="row"
53
+ display="flex"
54
+ height={40}
55
+ justifyContent="between"
56
+ // padding={1}
57
+ rounding={3}
58
+ width="100%"
59
+ >
60
+ {items.map((item, index) => (
61
+ <Box
62
+ key={index}
63
+ color={isTabActive(index)}
64
+ height="100%"
65
+ // paddingX={2}
66
+ rounding={3}
67
+ width={`${100 / items.length}%`}
68
+ >
69
+ <Box
70
+ alignItems="center"
71
+ display="flex"
72
+ height="100%"
73
+ justifyContent="center"
74
+ paddingX={1}
75
+ width="100%"
76
+ onClick={() => {
77
+ if (
78
+ selectedItemIndexes?.length === selectLimit &&
79
+ !selectedItemIndexes?.includes(index)
80
+ ) {
81
+ return;
82
+ }
83
+ if (multiselect) {
84
+ if (selectedItemIndexes?.includes(index)) {
85
+ onChange({activeIndex: selectedItemIndexes.filter((i) => i !== index)});
86
+ } else {
87
+ const currentIndexes = [...(selectedItemIndexes as number[])];
88
+ currentIndexes?.push(index);
89
+ onChange({activeIndex: currentIndexes?.sort() as number[]});
90
+ }
91
+ } else {
92
+ onChange({activeIndex: index});
93
+ }
94
+ }}
95
+ >
96
+ {renderItem(item)}
97
+ </Box>
98
+ </Box>
99
+ ))}
100
+ </Box>
101
+ );
102
+ };
@@ -0,0 +1,89 @@
1
+ import React from "react";
2
+ import {Platform} from "react-native";
3
+
4
+ import {FieldWithLabelsProps, StyleProp} from "./Common";
5
+ import {Icon} from "./Icon";
6
+ import {RNPickerSelect} from "./PickerSelect";
7
+ import {Unifier} from "./Unifier";
8
+ import {WithLabel} from "./WithLabel";
9
+
10
+ // Use "" if you want to have an "unset" value.
11
+ export type SelectListOptions = {label: string; value: string}[];
12
+ export interface SelectListProps extends FieldWithLabelsProps {
13
+ id?: string;
14
+ name?: string;
15
+ options: SelectListOptions;
16
+ onChange: (value: string) => void;
17
+ value?: string;
18
+ disabled?: boolean;
19
+ size?: "md" | "lg";
20
+ // TODO: Implement placeholder prop for Select Field for both Android and iOS
21
+ placeholder?: string;
22
+ style?: StyleProp;
23
+ }
24
+
25
+ export function SelectList({
26
+ options,
27
+ value,
28
+ onChange,
29
+ label,
30
+ labelColor,
31
+ style,
32
+ placeholder,
33
+ disabled,
34
+ }: SelectListProps) {
35
+ const withLabelProps = {label, labelColor};
36
+
37
+ let backgroundColor = style?.backgroundColor || Unifier.theme.white;
38
+ if (disabled) {
39
+ backgroundColor = Unifier.theme.lightGray;
40
+ }
41
+
42
+ return (
43
+ <WithLabel {...withLabelProps}>
44
+ <RNPickerSelect
45
+ Icon={() => {
46
+ // Icon only needed for iOS, web and android use default icons
47
+ return Platform.OS === "ios" ? (
48
+ <Icon color="darkGray" name="angle-down" size="md" />
49
+ ) : null;
50
+ }}
51
+ disabled={disabled}
52
+ items={options}
53
+ placeholder={placeholder ? {label: placeholder, value: ""} : {}}
54
+ style={{
55
+ viewContainer: {
56
+ flexDirection: style?.flexDirection || "row",
57
+ justifyContent: style?.justifyContent || "center",
58
+ alignItems: style?.alignItems || "center",
59
+ minHeight: style?.minHeight || 50,
60
+ width: style?.width || "100%",
61
+ borderColor: style?.borderColor || Unifier.theme.gray,
62
+ borderWidth: style?.borderWidth || 1,
63
+ borderRadius: style?.borderRadius || 5,
64
+ backgroundColor,
65
+ },
66
+ inputIOS: {
67
+ paddingVertical: 12,
68
+ paddingHorizontal: 10,
69
+ paddingRight: 30,
70
+ },
71
+ iconContainer: {
72
+ top: 13,
73
+ right: 10,
74
+ bottom: 12,
75
+ paddingLeft: 40,
76
+ },
77
+ inputWeb: {
78
+ // Add padding so the border doesn't mess up layouts
79
+ paddingHorizontal: style?.paddingHorizontal || 6,
80
+ paddingVertical: style?.paddingVertical || 4,
81
+ borderRadius: style?.borderRadius || 5,
82
+ },
83
+ }}
84
+ value={value}
85
+ onValueChange={onChange}
86
+ />
87
+ </WithLabel>
88
+ );
89
+ }
@@ -0,0 +1,62 @@
1
+ import React, {ReactElement} from "react";
2
+ import {Platform, SafeAreaView, StyleProp, ViewStyle} from "react-native";
3
+ import {Drawer} from "react-native-drawer-layout";
4
+
5
+ import {isMobileDevice} from "./MediaQuery";
6
+
7
+ export interface SideDrawerProps {
8
+ // Position of the drawer relative to the child
9
+ position?: "right" | "left";
10
+ // Used to open/hide drawer. Use the onClose and onOpen props to control state
11
+ isOpen: boolean;
12
+ // Content within the drawer
13
+ renderContent: () => ReactElement | ReactElement[];
14
+ // TODO: Allow the hardware back button on Android to close the SideDrawer
15
+ onClose?: () => void;
16
+ onOpen?: () => void;
17
+ drawerType?: "front" | "back" | "slide" | "permanent";
18
+ // Content that is wrapped by the drawer. The drawer will use the height of the child it wraps. Can be overwritten via styles prop
19
+ children?: ReactElement;
20
+ drawerStyles?: StyleProp<ViewStyle>;
21
+ }
22
+
23
+ const DEFAULT_STYLES: StyleProp<ViewStyle> = {
24
+ width: Platform.OS === "web" ? "40%" : "95%",
25
+ backgroundColor: "lightgray",
26
+ borderWidth: 1,
27
+ borderColor: "gray",
28
+ overflow: isMobileDevice() ? undefined : "scroll",
29
+ };
30
+
31
+ export const SideDrawer = ({
32
+ position = "left",
33
+ isOpen,
34
+ renderContent,
35
+ onClose = () => {},
36
+ onOpen = () => {},
37
+ drawerType = "front",
38
+ children,
39
+ drawerStyles = {},
40
+ }: SideDrawerProps): ReactElement => {
41
+ const renderDrawerContent = (): ReactElement => {
42
+ return <SafeAreaView>{renderContent()}</SafeAreaView>;
43
+ };
44
+
45
+ return (
46
+ <Drawer
47
+ drawerPosition={position}
48
+ drawerStyle={[
49
+ DEFAULT_STYLES,
50
+ drawerStyles,
51
+ {display: Platform.OS === "web" && !isOpen ? "none" : "flex"},
52
+ ]}
53
+ drawerType={drawerType}
54
+ open={isOpen}
55
+ renderDrawerContent={renderDrawerContent}
56
+ onClose={onClose}
57
+ onOpen={onOpen}
58
+ >
59
+ {children}
60
+ </Drawer>
61
+ );
62
+ };
@@ -0,0 +1,20 @@
1
+ import React, {ReactElement, useEffect, useState} from "react";
2
+ import {ActivityIndicator} from "react-native";
3
+
4
+ import {SpinnerProps} from "./Common";
5
+
6
+ export function Spinner({size, color}: SpinnerProps): ReactElement | null {
7
+ const [show, setShow] = useState(false);
8
+
9
+ // The delay is for perceived performance. You don't want to show a spinner when you're doing a quick action.
10
+ useEffect(() => {
11
+ const timer = setTimeout(() => setShow(true), 300);
12
+ return () => clearTimeout(timer);
13
+ }, []);
14
+
15
+ if (!show) {
16
+ return null;
17
+ }
18
+ const spinnerSize: "small" | "large" = size === "sm" ? "small" : "large";
19
+ return <ActivityIndicator color={color || "darkGray"} size={spinnerSize} />;
20
+ }
@@ -0,0 +1,160 @@
1
+ // TODO: Update SplitPage native to have desktop UX for tablet sized screens
2
+ import React, {useCallback, useEffect, useState} from "react";
3
+ import flattenChildren from "react-keyed-flatten-children";
4
+ import {Dimensions, ListRenderItemInfo, View} from "react-native";
5
+ import {SwiperFlatList} from "react-native-swiper-flatlist";
6
+
7
+ import {Box} from "./Box";
8
+ import {SplitPageProps} from "./Common";
9
+ import {FlatList} from "./FlatList";
10
+ import {IconButton} from "./IconButton";
11
+ import {Spinner} from "./Spinner";
12
+ import {Unifier} from "./Unifier";
13
+
14
+ export const SplitPage = ({
15
+ children,
16
+ loading = false,
17
+ color,
18
+ keyboardOffset,
19
+ renderListViewItem,
20
+ renderListViewHeader,
21
+ renderContent,
22
+ onSelectionChange = () => {},
23
+ listViewData,
24
+ listViewExtraData,
25
+ bottomNavBarHeight,
26
+ showItemList,
27
+ }: SplitPageProps) => {
28
+ const [selectedId, setSelectedId] = useState<number | undefined>(undefined);
29
+
30
+ // flattenChildren is necessary to pull children from a React Fragment. Without this,
31
+ // splitPage would only recognize the fragment container instead stripping it to render
32
+ // the children individually
33
+ const elementArray = flattenChildren(children);
34
+ const {width} = Dimensions.get("window");
35
+
36
+ const onItemSelect = useCallback(
37
+ (item: ListRenderItemInfo<any>) => {
38
+ setSelectedId(item.index);
39
+ onSelectionChange(item);
40
+ },
41
+ [onSelectionChange]
42
+ );
43
+
44
+ const onItemDeselect = useCallback(() => {
45
+ setSelectedId(undefined);
46
+ onSelectionChange(undefined);
47
+ }, [onSelectionChange]);
48
+
49
+ useEffect(() => {
50
+ if (showItemList) {
51
+ onItemDeselect();
52
+ }
53
+ }, [showItemList, onItemDeselect]);
54
+
55
+ if (!children && !renderContent) {
56
+ console.warn("A child node is required");
57
+ return null;
58
+ }
59
+
60
+ const renderItem = (itemInfo: ListRenderItemInfo<any>) => {
61
+ return (
62
+ <Box
63
+ onClick={() => {
64
+ Unifier.utils.haptic();
65
+ onItemSelect(itemInfo);
66
+ }}
67
+ >
68
+ {renderListViewItem(itemInfo)}
69
+ </Box>
70
+ );
71
+ };
72
+
73
+ const renderList = () => {
74
+ if (selectedId !== undefined) {
75
+ return null;
76
+ }
77
+
78
+ return (
79
+ <View
80
+ style={{
81
+ width: "100%",
82
+ maxWidth: "100%",
83
+ flexGrow: 1,
84
+ flexShrink: 0,
85
+ display: "flex",
86
+ flexDirection: "column",
87
+ paddingBottom: bottomNavBarHeight,
88
+ }}
89
+ >
90
+ {renderListViewHeader && renderListViewHeader()}
91
+ <FlatList
92
+ data={listViewData}
93
+ extraData={listViewExtraData}
94
+ keyExtractor={(item) => item.id}
95
+ renderItem={renderItem}
96
+ />
97
+ </View>
98
+ );
99
+ };
100
+
101
+ const renderListContent = () => {
102
+ if (selectedId === undefined) {
103
+ return null;
104
+ }
105
+ return (
106
+ <Box flex="grow" padding={2}>
107
+ <Box width="100%">
108
+ <IconButton
109
+ accessibilityLabel="close"
110
+ icon="times"
111
+ iconColor="darkGray"
112
+ onClick={() => onItemDeselect()}
113
+ />
114
+ </Box>
115
+ {renderContent && renderContent(selectedId)}
116
+ </Box>
117
+ );
118
+ };
119
+
120
+ const renderChildrenContent = () => {
121
+ if (selectedId === undefined) {
122
+ return null;
123
+ }
124
+ return (
125
+ <SwiperFlatList
126
+ nestedScrollEnabled
127
+ renderAll
128
+ showPagination={elementArray.length > 1}
129
+ style={{width: "100%"}}
130
+ >
131
+ {elementArray.map((element, i) => {
132
+ return (
133
+ <View
134
+ key={i}
135
+ style={{width, height: elementArray.length > 1 ? "95%" : "100%", padding: 4}}
136
+ >
137
+ {element}
138
+ </View>
139
+ );
140
+ })}
141
+ </SwiperFlatList>
142
+ );
143
+ };
144
+
145
+ const renderMainContent = renderContent ? renderListContent() : renderChildrenContent();
146
+
147
+ return (
148
+ <Box
149
+ avoidKeyboard
150
+ color={color || "lightGray"}
151
+ flex="grow"
152
+ height="100%"
153
+ keyboardOffset={keyboardOffset}
154
+ width="100%"
155
+ >
156
+ {loading === true && <Spinner color={Unifier.theme.darkGray as any} size="md" />}
157
+ {selectedId === undefined ? renderList() : renderMainContent}
158
+ </Box>
159
+ );
160
+ };