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,401 @@
1
+ import {AsYouType} from "libphonenumber-js";
2
+ import React, {ReactElement, useCallback, useMemo, useState} from "react";
3
+ import {
4
+ ActivityIndicator,
5
+ KeyboardTypeOptions,
6
+ Platform,
7
+ Pressable,
8
+ TextInput,
9
+ View,
10
+ } from "react-native";
11
+
12
+ import {Box} from "./Box";
13
+ import {TextFieldProps} from "./Common";
14
+ import {DateTimeActionSheet} from "./DateTimeActionSheet";
15
+ import dayjs from "./dayjsExtended";
16
+ import {DecimalRangeActionSheet} from "./DecimalRangeActionSheet";
17
+ import {HeightActionSheet} from "./HeightActionSheet";
18
+ import {Icon} from "./Icon";
19
+ import {NumberPickerActionSheet} from "./NumberPickerActionSheet";
20
+ import {Unifier} from "./Unifier";
21
+ import {WithLabel} from "./WithLabel";
22
+
23
+ const keyboardMap: {[id: string]: string | undefined} = {
24
+ date: "default",
25
+ email: "email-address",
26
+ number: "number-pad",
27
+ numberRange: "number-pad",
28
+ decimalRange: "decimal-pad",
29
+ decimal: "decimal-pad",
30
+ height: "default",
31
+ password: "default",
32
+ phoneNumber: "number-pad",
33
+ search: "default",
34
+ text: "default",
35
+ url: Platform.select({
36
+ ios: "url",
37
+ android: "default",
38
+ }),
39
+ username: "default",
40
+ };
41
+
42
+ // Not an exhaustive list of all the textContent types, but the ones we use.
43
+ const textContentMap: {
44
+ [id: string]: "none" | "emailAddress" | "password" | "username" | "URL" | undefined;
45
+ } = {
46
+ date: "none",
47
+ email: "emailAddress",
48
+ number: "none",
49
+ decimal: "none",
50
+ decimalRange: "none",
51
+ height: "none",
52
+ password: "password",
53
+ search: "none",
54
+ text: "none",
55
+ url: Platform.select({
56
+ ios: "URL",
57
+ android: "none",
58
+ }),
59
+ username: "username",
60
+ };
61
+
62
+ export function TextField({
63
+ blurOnSubmit = true,
64
+ value,
65
+ height: propsHeight,
66
+ onChange,
67
+ paddingX,
68
+ paddingY,
69
+ min,
70
+ max,
71
+ type = "text",
72
+ searching,
73
+ autoComplete,
74
+ autoFocus,
75
+ disabled,
76
+ errorMessage,
77
+ errorMessageColor,
78
+ inputRef,
79
+ multiline,
80
+ rows,
81
+ placeholder,
82
+ grow,
83
+ label,
84
+ labelColor,
85
+ returnKeyType,
86
+ onBlur,
87
+ style,
88
+ onEnter,
89
+ onSubmitEditing,
90
+ }: TextFieldProps): ReactElement {
91
+ const dateActionSheetRef: React.RefObject<any> = React.createRef();
92
+ const numberRangeActionSheetRef: React.RefObject<any> = React.createRef();
93
+ const decimalRangeActionSheetRef: React.RefObject<any> = React.createRef();
94
+ const weightActionSheetRef: React.RefObject<any> = React.createRef();
95
+
96
+ const [focused, setFocused] = useState(false);
97
+ const [height, setHeight] = useState(propsHeight || 40);
98
+ const [showDate, setShowDate] = useState(false);
99
+
100
+ const renderIcon = () => {
101
+ if (type !== "search") {
102
+ return null;
103
+ }
104
+ if (searching) {
105
+ return (
106
+ <Box marginRight={4}>
107
+ <ActivityIndicator color={Unifier.theme.primary} size="small" />
108
+ </Box>
109
+ );
110
+ } else {
111
+ return (
112
+ <Box marginRight={2}>
113
+ <Icon name="search" prefix="far" size="md" />
114
+ </Box>
115
+ );
116
+ }
117
+ };
118
+
119
+ let borderColor;
120
+ if (errorMessage) {
121
+ borderColor = Unifier.theme.red;
122
+ } else if (focused) {
123
+ borderColor = Unifier.theme.blue;
124
+ } else {
125
+ borderColor = Unifier.theme.gray;
126
+ }
127
+
128
+ const getHeight = useCallback(() => {
129
+ if (grow) {
130
+ return Math.max(40, height);
131
+ } else if (multiline) {
132
+ return height || "100%";
133
+ } else {
134
+ return 40;
135
+ }
136
+ }, [grow, height, multiline]);
137
+
138
+ const defaultTextInputStyles = useMemo(() => {
139
+ const defaultStyles = {
140
+ flex: 1,
141
+ paddingTop: 4,
142
+ paddingRight: 4,
143
+ paddingBottom: 4,
144
+ paddingLeft: 0,
145
+ height: getHeight(),
146
+ width: "100%",
147
+ color: Unifier.theme.darkGray,
148
+ fontFamily: Unifier.theme.primaryFont,
149
+ ...style,
150
+ };
151
+
152
+ if (Platform.OS === "web") {
153
+ defaultStyles.outline = 0;
154
+ }
155
+
156
+ return defaultStyles;
157
+ }, [getHeight, style]);
158
+
159
+ const isHandledByModal = [
160
+ "date",
161
+ "datetime",
162
+ "time",
163
+ "numberRange",
164
+ "decimalRange",
165
+ "height",
166
+ ].includes(type);
167
+
168
+ const isEditable = !disabled && !isHandledByModal;
169
+
170
+ const shouldAutocorrect = type === "text" && (!autoComplete || autoComplete === "on");
171
+
172
+ const keyboardType = keyboardMap[type];
173
+ const textContentType = textContentMap[type || "text"];
174
+
175
+ const withLabelProps = {
176
+ label,
177
+ labelColor,
178
+ };
179
+
180
+ const onTap = useCallback((): void => {
181
+ if (disabled) {
182
+ return;
183
+ }
184
+ if (["date", "datetime", "time"].includes(type)) {
185
+ setShowDate(true);
186
+ } else if (type === "numberRange") {
187
+ numberRangeActionSheetRef?.current?.show();
188
+ } else if (type === "decimalRange") {
189
+ decimalRangeActionSheetRef?.current?.show();
190
+ } else if (type === "height") {
191
+ weightActionSheetRef?.current?.show();
192
+ }
193
+ }, [decimalRangeActionSheetRef, disabled, numberRangeActionSheetRef, type, weightActionSheetRef]);
194
+
195
+ let displayValue = value;
196
+ if (displayValue) {
197
+ if (type === "date") {
198
+ // We get off by one errors because UTC midnight might be yesterday. So we add the timezone offset.
199
+ if (
200
+ dayjs.utc(value).hour() === 0 &&
201
+ dayjs.utc(value).minute() === 0 &&
202
+ dayjs.utc(value).second() === 0
203
+ ) {
204
+ const timezoneOffset = new Date().getTimezoneOffset();
205
+ displayValue = dayjs.utc(value).add(timezoneOffset, "minutes").format("MM/DD/YYYY");
206
+ } else {
207
+ displayValue = dayjs(value).format("MM/DD/YYYY");
208
+ }
209
+ } else if (type === "time") {
210
+ displayValue = dayjs(value).format("h:mm A");
211
+ } else if (type === "datetime") {
212
+ displayValue = dayjs(value).format("MM/DD/YYYY h:mm A");
213
+ } else if (type === "height") {
214
+ displayValue = `${Math.floor(Number(value) / 12)} ft, ${Number(value) % 12} in`;
215
+ } else if (type === "phoneNumber") {
216
+ // By default, if a value is something like `"(123)"`
217
+ // then Backspace would only erase the rightmost brace
218
+ // becoming something like `"(123"`
219
+ // which would give the same `"123"` value
220
+ // which would then be formatted back to `"(123)"`
221
+ // and so a user wouldn't be able to erase the phone number.
222
+ // This is the workaround for that.
223
+ const formattedPhoneNumber = new AsYouType("US").input(displayValue);
224
+ if (displayValue !== formattedPhoneNumber && displayValue.length !== 4) {
225
+ displayValue = formattedPhoneNumber;
226
+ }
227
+ }
228
+ } else {
229
+ // Set some default values for modal-edited fields so we don't go from uncontrolled to controlled when setting
230
+ // the date.
231
+ if (["date", "datetime", "time"].includes(type)) {
232
+ displayValue = "";
233
+ }
234
+ }
235
+
236
+ const Wrapper = isHandledByModal ? Pressable : View;
237
+
238
+ return (
239
+ <>
240
+ <WithLabel
241
+ label={errorMessage}
242
+ labelColor={errorMessageColor || "red"}
243
+ labelPlacement="after"
244
+ labelSize="sm"
245
+ >
246
+ <WithLabel {...withLabelProps}>
247
+ <Wrapper
248
+ style={{
249
+ flexDirection: "row",
250
+ justifyContent: "center",
251
+ alignItems: "center",
252
+ // height: multiline || grow ? undefined : 40,
253
+ minHeight: getHeight(),
254
+ width: "100%",
255
+ // Add padding so the border doesn't mess up layouts
256
+ paddingHorizontal: paddingX || focused ? 10 : 14,
257
+ paddingVertical: paddingY || focused ? 0 : 4,
258
+ borderColor,
259
+ borderWidth: focused ? 5 : 1,
260
+ borderRadius: 16,
261
+ backgroundColor: disabled ? Unifier.theme.gray : Unifier.theme.white,
262
+ overflow: "hidden",
263
+ }}
264
+ onPress={() => {
265
+ // This runs on web
266
+ onTap();
267
+ }}
268
+ onTouchStart={() => {
269
+ // This runs on mobile
270
+ onTap();
271
+ }}
272
+ >
273
+ {renderIcon()}
274
+ <TextInput
275
+ ref={(ref) => {
276
+ if (inputRef) {
277
+ inputRef(ref);
278
+ }
279
+ }}
280
+ autoCapitalize={type === "text" ? "sentences" : "none"}
281
+ autoCorrect={shouldAutocorrect}
282
+ autoFocus={autoFocus}
283
+ blurOnSubmit={blurOnSubmit}
284
+ editable={isEditable}
285
+ keyboardType={keyboardType as KeyboardTypeOptions}
286
+ multiline={multiline}
287
+ numberOfLines={rows || 4}
288
+ placeholder={placeholder}
289
+ placeholderTextColor={Unifier.theme.gray}
290
+ returnKeyType={type === "number" || type === "decimal" ? "done" : returnKeyType}
291
+ secureTextEntry={type === "password"}
292
+ style={defaultTextInputStyles}
293
+ textContentType={textContentType}
294
+ underlineColorAndroid="transparent"
295
+ value={displayValue}
296
+ onBlur={() => {
297
+ if (!isHandledByModal) {
298
+ setFocused(false);
299
+ }
300
+ if (onBlur) {
301
+ onBlur({value: value ?? ""});
302
+ }
303
+ // if (type === "date") {
304
+ // actionSheetRef?.current?.hide();
305
+ // }
306
+ }}
307
+ onChangeText={(text) => {
308
+ if (onChange) {
309
+ if (type === "phoneNumber") {
310
+ const formattedPhoneNumber = new AsYouType("US").input(text);
311
+ // another workaround for the same issue as above with backspacing phone numbers
312
+ if (formattedPhoneNumber === value) {
313
+ onChange({value: text});
314
+ } else {
315
+ onChange({value: formattedPhoneNumber});
316
+ }
317
+ } else {
318
+ onChange({value: text});
319
+ }
320
+ }
321
+ }}
322
+ onContentSizeChange={(event) => {
323
+ if (!grow) {
324
+ return;
325
+ }
326
+ setHeight(event.nativeEvent.contentSize.height);
327
+ }}
328
+ onFocus={() => {
329
+ if (!isHandledByModal) {
330
+ setFocused(true);
331
+ }
332
+ }}
333
+ onSubmitEditing={() => {
334
+ if (onEnter) {
335
+ onEnter();
336
+ }
337
+ if (onSubmitEditing) {
338
+ onSubmitEditing();
339
+ }
340
+ }}
341
+ />
342
+ </Wrapper>
343
+ </WithLabel>
344
+ </WithLabel>
345
+ {(type === "date" || type === "time" || type === "datetime") && (
346
+ <DateTimeActionSheet
347
+ actionSheetRef={dateActionSheetRef}
348
+ mode={type}
349
+ value={value}
350
+ visible={showDate}
351
+ onChange={(result) => {
352
+ onChange(result);
353
+ setShowDate(false);
354
+ setFocused(false);
355
+ }}
356
+ onDismiss={() => setShowDate(false)}
357
+ />
358
+ )}
359
+ {/* {type === "date" && showDate && ( */}
360
+ {/* <Box maxWidth={300}> */}
361
+ {/* /!* TODO: Calendar should disappear when you click away from it. *!/ */}
362
+ {/* <Calendar */}
363
+ {/* customHeader={CalendarHeader} */}
364
+ {/* initialDate={value} */}
365
+ {/* onDayPress={(day: any) => { */}
366
+ {/* onChange({value: day.dateString}); */}
367
+ {/* setShowDate(false); */}
368
+ {/* }} */}
369
+ {/* /> */}
370
+ {/* </Box> */}
371
+ {/* )} */}
372
+ {type === "numberRange" && value && (
373
+ <NumberPickerActionSheet
374
+ actionSheetRef={numberRangeActionSheetRef}
375
+ max={max || (min || 0) + 100}
376
+ min={min || 0}
377
+ value={value}
378
+ onChange={(result) => onChange(result)}
379
+ />
380
+ )}
381
+ {type === "decimalRange" && value && (
382
+ <DecimalRangeActionSheet
383
+ actionSheetRef={decimalRangeActionSheetRef}
384
+ max={max || (min || 0) + 100}
385
+ min={min || 0}
386
+ value={value}
387
+ onChange={(result) => onChange(result)}
388
+ />
389
+ )}
390
+ {type === "height" && (
391
+ <HeightActionSheet
392
+ actionSheetRef={weightActionSheetRef}
393
+ value={value}
394
+ onChange={(result) => {
395
+ onChange(result);
396
+ }}
397
+ />
398
+ )}
399
+ </>
400
+ );
401
+ }
@@ -0,0 +1,61 @@
1
+ import DateTimePicker from "@react-native-community/datetimepicker";
2
+ import React from "react";
3
+
4
+ import {ActionSheet} from "./ActionSheet";
5
+ import {Box} from "./Box";
6
+ import {Button} from "./Button";
7
+ import {OnChangeCallback} from "./Common";
8
+ import dayjs from "./dayjsExtended";
9
+
10
+ interface NumberPickerActionSheetProps {
11
+ value?: string;
12
+ mode?: "date" | "time";
13
+ onChange: OnChangeCallback;
14
+ actionSheetRef: React.RefObject<any>;
15
+ }
16
+
17
+ interface NumberPickerActionSheetState {}
18
+
19
+ export class NumberPickerActionSheet extends React.Component<
20
+ NumberPickerActionSheetProps,
21
+ NumberPickerActionSheetState
22
+ > {
23
+ constructor(props: NumberPickerActionSheetProps) {
24
+ super(props);
25
+ }
26
+
27
+ render() {
28
+ return (
29
+ <ActionSheet ref={this.props.actionSheetRef} bounceOnOpen gestureEnabled>
30
+ <Box marginBottom={8} paddingX={4} width="100%">
31
+ <Box alignItems="end" display="flex" width="100%">
32
+ <Box width="33%">
33
+ <Button
34
+ color="blue"
35
+ size="lg"
36
+ text="Save"
37
+ type="ghost"
38
+ onClick={() => {
39
+ this.props.actionSheetRef?.current?.setModalVisible(false);
40
+ }}
41
+ />
42
+ </Box>
43
+ </Box>
44
+ <DateTimePicker
45
+ display="spinner"
46
+ is24Hour
47
+ mode={this.props.mode}
48
+ testID="dateTimePicker"
49
+ value={dayjs(this.props.value).toDate()}
50
+ onChange={(event: any, date?: Date) => {
51
+ if (!date) {
52
+ return;
53
+ }
54
+ this.props.onChange({event, value: date.toString()});
55
+ }}
56
+ />
57
+ </Box>
58
+ </ActionSheet>
59
+ );
60
+ }
61
+ }
package/src/Toast.tsx ADDED
@@ -0,0 +1,106 @@
1
+ import React from "react";
2
+ import {Dimensions} from "react-native";
3
+ import {useToast as useRNToast} from "react-native-toast-notifications";
4
+
5
+ import {Box} from "./Box";
6
+ import {Button} from "./Button";
7
+ import {AllColors} from "./Common";
8
+ import {Icon} from "./Icon";
9
+ import {IconButton} from "./IconButton";
10
+ import {Text} from "./Text";
11
+
12
+ const TOAST_DURATION_MS = 3 * 1000;
13
+
14
+ export function useToast(): any {
15
+ const toast = useRNToast();
16
+ return {
17
+ show: (
18
+ text: string,
19
+ options?: {
20
+ variant?: "default" | "warning" | "error";
21
+ buttonText?: string;
22
+ buttonOnClick: () => void | Promise<void>;
23
+ persistent?: boolean;
24
+ onDismiss?: () => void | Promise<void>;
25
+ }
26
+ ): string => {
27
+ return toast.show(text, {
28
+ data: options,
29
+ // a duration of 0 keeps the toast up infinitely until hidden
30
+ duration: options?.persistent ? 0 : TOAST_DURATION_MS,
31
+ });
32
+ },
33
+ hide: (id: string) => toast.hide(id),
34
+ };
35
+ }
36
+
37
+ export function Toast({
38
+ message,
39
+ data,
40
+ }: {
41
+ message: string;
42
+ data: {
43
+ variant?: "default" | "warning" | "error";
44
+ buttonText?: string;
45
+ buttonOnClick?: () => void | Promise<void>;
46
+ persistent?: boolean;
47
+ onDismiss?: () => void;
48
+ };
49
+ }): React.ReactElement {
50
+ // margin 8 on either side, times the standard 4px we multiply by.
51
+ const width = Math.min(Dimensions.get("window").width - 16 * 4, 712);
52
+ const {variant, buttonText, buttonOnClick, persistent, onDismiss} = data ?? {};
53
+ let color: AllColors = "darkGray";
54
+ if (variant === "warning") {
55
+ color = "orange";
56
+ } else if (variant === "error") {
57
+ color = "red";
58
+ }
59
+ return (
60
+ <Box
61
+ alignItems="center"
62
+ color={color}
63
+ direction="row"
64
+ flex="shrink"
65
+ marginBottom={4}
66
+ marginLeft={8}
67
+ marginRight={8}
68
+ maxWidth={width}
69
+ padding={2}
70
+ paddingX={4}
71
+ paddingY={3}
72
+ rounding={4}
73
+ >
74
+ {Boolean(variant === "error") && (
75
+ <Box marginRight={4}>
76
+ <Icon color="white" name="exclamation-circle" size="lg" />
77
+ </Box>
78
+ )}
79
+ {Boolean(variant === "warning") && (
80
+ <Box marginRight={4}>
81
+ <Icon color="white" name="exclamation-triangle" size="lg" />
82
+ </Box>
83
+ )}
84
+ <Box alignItems="center" direction="column" flex="shrink" justifyContent="center">
85
+ <Text color="white" size="lg" weight="bold">
86
+ {message}
87
+ </Text>
88
+ </Box>
89
+ {Boolean(buttonOnClick && buttonText) && (
90
+ <Box alignItems="center" justifyContent="center" marginLeft={4}>
91
+ <Button color="lightGray" shape="pill" text={buttonText!} onClick={buttonOnClick} />
92
+ </Box>
93
+ )}
94
+ {Boolean(onDismiss && persistent) && (
95
+ <Box alignItems="center" justifyContent="center" marginLeft={4}>
96
+ <IconButton
97
+ accessibilityLabel="Dismiss notification"
98
+ icon="times"
99
+ iconColor="white"
100
+ onClick={onDismiss!}
101
+ />
102
+ </Box>
103
+ )}
104
+ </Box>
105
+ );
106
+ }