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.
- package/package.json +3 -4
- package/src/ActionSheet.tsx +1231 -0
- package/src/Avatar.tsx +317 -0
- package/src/Badge.tsx +65 -0
- package/src/Banner.tsx +124 -0
- package/src/BlurBox.native.tsx +40 -0
- package/src/BlurBox.tsx +31 -0
- package/src/Body.tsx +32 -0
- package/src/Box.tsx +308 -0
- package/src/Button.tsx +219 -0
- package/src/Card.tsx +23 -0
- package/src/CheckBox.tsx +118 -0
- package/src/Common.ts +2743 -0
- package/src/Constants.ts +53 -0
- package/src/CustomSelect.tsx +85 -0
- package/src/DateTimeActionSheet.tsx +409 -0
- package/src/DateTimeField.android.tsx +101 -0
- package/src/DateTimeField.ios.tsx +83 -0
- package/src/DateTimeField.tsx +69 -0
- package/src/DecimalRangeActionSheet.tsx +113 -0
- package/src/ErrorBoundary.tsx +37 -0
- package/src/ErrorPage.tsx +44 -0
- package/src/FernsProvider.tsx +21 -0
- package/src/Field.tsx +299 -0
- package/src/FieldWithLabels.tsx +36 -0
- package/src/FlatList.tsx +2 -0
- package/src/Form.tsx +182 -0
- package/src/HeaderButtons.tsx +107 -0
- package/src/Heading.tsx +53 -0
- package/src/HeightActionSheet.tsx +104 -0
- package/src/Hyperlink.tsx +181 -0
- package/src/Icon.tsx +24 -0
- package/src/IconButton.tsx +165 -0
- package/src/Image.tsx +50 -0
- package/src/ImageBackground.tsx +14 -0
- package/src/InfoTooltipButton.tsx +23 -0
- package/src/Layer.tsx +17 -0
- package/src/Link.tsx +17 -0
- package/src/Mask.tsx +21 -0
- package/src/MediaQuery.ts +46 -0
- package/src/Meta.tsx +9 -0
- package/src/Modal.tsx +248 -0
- package/src/ModalSheet.tsx +58 -0
- package/src/NumberPickerActionSheet.tsx +66 -0
- package/src/Page.tsx +133 -0
- package/src/Permissions.ts +44 -0
- package/src/PickerSelect.tsx +553 -0
- package/src/Pill.tsx +24 -0
- package/src/Pog.tsx +87 -0
- package/src/ProgressBar.tsx +55 -0
- package/src/ScrollView.tsx +2 -0
- package/src/SegmentedControl.tsx +102 -0
- package/src/SelectList.tsx +89 -0
- package/src/SideDrawer.tsx +62 -0
- package/src/Spinner.tsx +20 -0
- package/src/SplitPage.native.tsx +160 -0
- package/src/SplitPage.tsx +302 -0
- package/src/Switch.tsx +19 -0
- package/src/Table.tsx +87 -0
- package/src/TableHeader.tsx +36 -0
- package/src/TableHeaderCell.tsx +76 -0
- package/src/TableRow.tsx +87 -0
- package/src/TapToEdit.tsx +221 -0
- package/src/Text.tsx +131 -0
- package/src/TextArea.tsx +16 -0
- package/src/TextField.tsx +401 -0
- package/src/TextFieldNumberActionSheet.tsx +61 -0
- package/src/Toast.tsx +106 -0
- package/src/Tooltip.tsx +269 -0
- package/src/UnifiedScreens.ts +24 -0
- package/src/Unifier.ts +371 -0
- package/src/Utilities.tsx +159 -0
- package/src/WithLabel.tsx +57 -0
- package/src/dayjsExtended.ts +10 -0
- package/src/index.tsx +1346 -0
- package/src/polyfill.d.ts +11 -0
- 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
|
+
}
|
package/src/TextArea.tsx
ADDED
|
@@ -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
|
+
}
|