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,221 @@
1
+ import React, {ReactElement, useState} from "react";
2
+ import {Linking} from "react-native";
3
+
4
+ import {Box} from "./Box";
5
+ import {Button} from "./Button";
6
+ import {BoxProps} from "./Common";
7
+ import {Field, FieldProps} from "./Field";
8
+ import {Icon} from "./Icon";
9
+ import {Text} from "./Text";
10
+
11
+ export function formatAddress(address: any, asString = false): string {
12
+ let city = "";
13
+ if (address?.city) {
14
+ city = address?.state || address.zipcode ? `${address.city}, ` : `${address.city}`;
15
+ }
16
+
17
+ let state = "";
18
+ if (address?.state) {
19
+ state = address?.zipcode ? `${address.state} ` : `${address.state}`;
20
+ }
21
+
22
+ const zip = address?.zipcode || "";
23
+
24
+ const addressLineOne = address?.address1 ?? "";
25
+ const addressLineTwo = address?.address2 ?? "";
26
+ const addressLineThree = `${city}${state}${zip}`;
27
+
28
+ if (!asString) {
29
+ // Only add new lines if lines before and after are not empty to avoid awkward whitespace
30
+ return `${addressLineOne}${
31
+ addressLineOne && (addressLineTwo || addressLineThree) ? `\n` : ""
32
+ }${addressLineTwo}${addressLineTwo && addressLineThree ? `\n` : ""}${addressLineThree}`;
33
+ } else {
34
+ return `${addressLineOne}${
35
+ addressLineOne && (addressLineTwo || addressLineThree) ? `, ` : ""
36
+ }${addressLineTwo}${addressLineTwo && addressLineThree ? `, ` : ""}${addressLineThree}`;
37
+ }
38
+ }
39
+
40
+ export interface TapToEditProps extends Omit<FieldProps, "onChange" | "value"> {
41
+ title: string;
42
+ value: any;
43
+ // Not required if not editable.
44
+ setValue?: (value: any) => void;
45
+ // Not required if not editable.
46
+ onSave?: (value: any) => void | Promise<void>;
47
+ // Defaults to true
48
+ editable?: boolean;
49
+ // enable edit mode from outside the component
50
+ isEditing?: boolean;
51
+ // For changing how the non-editing row renders
52
+ rowBoxProps?: Partial<BoxProps>;
53
+ transform?: (value: any) => string;
54
+ fieldComponent?: (setValue: () => void) => ReactElement;
55
+ withConfirmation?: boolean;
56
+ confirmationText?: string;
57
+ confirmationHeading?: string;
58
+ }
59
+
60
+ export const TapToEdit = ({
61
+ value,
62
+ setValue,
63
+ placeholder,
64
+ title,
65
+ onSave,
66
+ editable = true,
67
+ isEditing = false,
68
+ rowBoxProps,
69
+ transform,
70
+ fieldComponent,
71
+ withConfirmation = false,
72
+ confirmationText = "Are you sure you want to save your changes?",
73
+ confirmationHeading = "Confirm",
74
+ ...fieldProps
75
+ }: TapToEditProps): ReactElement => {
76
+ const [editing, setEditing] = useState(false);
77
+ const [initialValue] = useState(value);
78
+
79
+ if (editable && !setValue) {
80
+ throw new Error("setValue is required if editable is true");
81
+ }
82
+
83
+ if (editable && (editing || isEditing)) {
84
+ return (
85
+ <Box direction="column">
86
+ {fieldComponent ? (
87
+ fieldComponent(setValue as any)
88
+ ) : (
89
+ <Field
90
+ label={title}
91
+ placeholder={placeholder}
92
+ value={value}
93
+ onChange={setValue}
94
+ {...fieldProps}
95
+ />
96
+ )}
97
+ {editing && !isEditing && (
98
+ <Box direction="row">
99
+ <Button
100
+ color="blue"
101
+ confirmationHeading={confirmationHeading}
102
+ confirmationText={confirmationText}
103
+ inline
104
+ text="Save"
105
+ withConfirmation={withConfirmation}
106
+ onClick={async (): Promise<void> => {
107
+ if (!onSave) {
108
+ console.error("No onSave provided for editable TapToEdit");
109
+ } else {
110
+ await onSave(value);
111
+ }
112
+ setEditing(false);
113
+ }}
114
+ />
115
+ <Box marginLeft={2}>
116
+ <Button
117
+ color="red"
118
+ inline
119
+ text="Cancel"
120
+ onClick={(): void => {
121
+ if (setValue) {
122
+ setValue(initialValue);
123
+ }
124
+ setEditing(false);
125
+ }}
126
+ />
127
+ </Box>
128
+ </Box>
129
+ )}
130
+ </Box>
131
+ );
132
+ } else {
133
+ let displayValue = value;
134
+ // If a transform props is present, that takes priority
135
+ if (transform) {
136
+ displayValue = transform(value);
137
+ } else {
138
+ // If no transform, try and display the value reasonably.
139
+ if (fieldProps?.type === "boolean") {
140
+ displayValue = value ? "Yes" : "No";
141
+ } else if (fieldProps?.type === "percent") {
142
+ // Prevent floating point errors from showing up by using parseFloat and precision. Pass through parseFloat again
143
+ // to trim off insignificant zeroes.
144
+ displayValue = `${parseFloat(parseFloat(String(value * 100)).toPrecision(7))}%`;
145
+ } else if (fieldProps?.type === "currency") {
146
+ // TODO: support currencies other than USD in Field and related components.
147
+ const formatter = new Intl.NumberFormat("en-US", {
148
+ style: "currency",
149
+ currency: "USD",
150
+ minimumFractionDigits: 2, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
151
+ });
152
+ displayValue = formatter.format(value);
153
+ } else if (fieldProps?.type === "multiselect") {
154
+ // ???
155
+ displayValue = value.join(", ");
156
+ } else if (fieldProps?.type === "url") {
157
+ // Show only the domain, full links are likely too long.
158
+ try {
159
+ const url = new URL(value);
160
+ displayValue = url?.hostname ?? value;
161
+ } catch (e) {
162
+ // Don't print an error message for empty values.
163
+ if (value) {
164
+ console.debug(`Invalid URL: ${value}`);
165
+ }
166
+ displayValue = value;
167
+ }
168
+ } else if (fieldProps?.type === "address") {
169
+ displayValue = formatAddress(value);
170
+ }
171
+ }
172
+
173
+ const openLink = (): void => {
174
+ if (fieldProps?.type === "url") {
175
+ Linking.openURL(value);
176
+ }
177
+ };
178
+
179
+ return (
180
+ <Box
181
+ direction="row"
182
+ justifyContent="between"
183
+ paddingX={3}
184
+ paddingY={2}
185
+ width="100%"
186
+ {...rowBoxProps}
187
+ >
188
+ <Box>
189
+ <Text weight="bold">{title}:</Text>
190
+ {fieldProps?.type === "address" && (
191
+ <Box
192
+ onClick={
193
+ () =>
194
+ Linking.openURL(
195
+ `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
196
+ formatAddress(value, true)
197
+ )}`
198
+ )
199
+ // eslint-disable-next-line react/jsx-curly-newline
200
+ }
201
+ >
202
+ <Text color="blue" underline={fieldProps?.type === "address"}>
203
+ Google Maps
204
+ </Text>
205
+ </Box>
206
+ )}
207
+ </Box>
208
+ <Box direction="row">
209
+ <Box onClick={fieldProps?.type === "url" ? openLink : undefined}>
210
+ <Text underline={fieldProps?.type === "url"}>{displayValue}</Text>
211
+ </Box>
212
+ {editable && (
213
+ <Box marginLeft={2} onClick={(): void => setEditing(true)}>
214
+ <Icon color="darkGray" name="edit" prefix="far" size="md" />
215
+ </Box>
216
+ )}
217
+ </Box>
218
+ </Box>
219
+ );
220
+ }
221
+ };
package/src/Text.tsx ADDED
@@ -0,0 +1,131 @@
1
+ import React from "react";
2
+ import {Text as NativeText, TextStyle} from "react-native";
3
+
4
+ import {AllColors, Font, TextSize} from "./Common";
5
+ import {Hyperlink} from "./Hyperlink";
6
+ import {Unifier} from "./Unifier";
7
+
8
+ export interface TextProps {
9
+ align?: "left" | "right" | "center" | "justify"; // default "left"
10
+ children?: React.ReactNode;
11
+ color?: AllColors;
12
+ inline?: boolean; // default false
13
+ italic?: boolean; // default false
14
+ overflow?: "normal" | "breakWord"; // deprecated
15
+ size?: TextSize; // default "md"
16
+ truncate?: boolean; // default false
17
+ font?: Font;
18
+ underline?: boolean;
19
+ numberOfLines?: number;
20
+ skipLinking?: boolean;
21
+ weight?: "bold" | "normal";
22
+ testID?: string;
23
+ }
24
+
25
+ const fontSizes = {
26
+ sm: 12,
27
+ md: 14,
28
+ lg: 16,
29
+ };
30
+
31
+ export function Text({
32
+ align = "left",
33
+ children,
34
+ color,
35
+ inline = false,
36
+ italic = false,
37
+ overflow,
38
+ size = "md",
39
+ truncate = false,
40
+ font,
41
+ underline,
42
+ numberOfLines,
43
+ skipLinking,
44
+ testID,
45
+ weight = "normal",
46
+ }: TextProps): React.ReactElement {
47
+ function propsToStyle(): any {
48
+ const style: TextStyle = {};
49
+ if (overflow) {
50
+ console.warn(
51
+ "Text overflow is deprecated. Use `truncate` to cut off the text and add ellipse, otherwise breakWord is the default."
52
+ );
53
+ }
54
+ let computedFont = "primary";
55
+ if (font === "primary" || !font) {
56
+ if (weight === "bold") {
57
+ computedFont = "primaryBoldFont";
58
+ } else {
59
+ computedFont = "primaryFont";
60
+ }
61
+ } else if (font === "secondary") {
62
+ if (weight === "bold") {
63
+ computedFont = "secondaryBoldFont";
64
+ } else {
65
+ computedFont = "secondaryFont";
66
+ }
67
+ } else if (font === "button") {
68
+ computedFont = "buttonFont";
69
+ } else if (font === "title") {
70
+ computedFont = "titleFont";
71
+ } else if (font === "accent") {
72
+ if (weight === "bold") {
73
+ computedFont = "accentBoldFont";
74
+ } else {
75
+ computedFont = "accentFont";
76
+ }
77
+ }
78
+ if (weight === "bold") {
79
+ style.fontWeight = "bold";
80
+ }
81
+
82
+ style.fontFamily = Unifier.theme[computedFont as keyof typeof Unifier.theme];
83
+
84
+ style.fontSize = fontSizes[size || "md"];
85
+ if (align) {
86
+ style.textAlign = align;
87
+ }
88
+ if (color) {
89
+ style.color = Unifier.theme[color];
90
+ } else {
91
+ style.color = Unifier.theme.darkGray;
92
+ }
93
+
94
+ if (italic) {
95
+ style.fontStyle = "italic";
96
+ }
97
+ if (underline) {
98
+ style.textDecorationLine = "underline";
99
+ }
100
+ // TODO: might be useful for wrapping/truncating
101
+ // if (numberOfLines !== 1 && !inline) {
102
+ // style.flexWrap = "wrap";
103
+ // }
104
+
105
+ return style;
106
+ }
107
+
108
+ let lines = 0;
109
+ if (numberOfLines && truncate && numberOfLines > 1) {
110
+ console.error(`Cannot truncate Text and have ${numberOfLines} lines`);
111
+ }
112
+ if (numberOfLines) {
113
+ lines = numberOfLines;
114
+ } else if (inline || truncate) {
115
+ lines = 1;
116
+ }
117
+ const inner = (
118
+ <NativeText numberOfLines={lines} style={propsToStyle()} testID={testID}>
119
+ {children}
120
+ </NativeText>
121
+ );
122
+ if (skipLinking) {
123
+ return inner;
124
+ } else {
125
+ return (
126
+ <Hyperlink linkDefault linkStyle={{textDecorationLine: "underline"}}>
127
+ {inner}
128
+ </Hyperlink>
129
+ );
130
+ }
131
+ }
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+
3
+ import {TextAreaProps} from "./Common";
4
+ import {TextField} from "./TextField";
5
+
6
+ export class TextArea extends React.Component<TextAreaProps, {}> {
7
+ constructor(props: TextAreaProps) {
8
+ super(props);
9
+ this.state = {};
10
+ }
11
+
12
+ render() {
13
+ const {height, ...props} = this.props;
14
+ return <TextField {...props} height={height ?? 100} multiline />;
15
+ }
16
+ }