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
package/src/Avatar.tsx
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import {ImageResult, manipulateAsync, SaveFormat} from "expo-image-manipulator";
|
|
3
|
+
import {launchImageLibraryAsync, MediaTypeOptions} from "expo-image-picker";
|
|
4
|
+
import React, {useEffect, useState} from "react";
|
|
5
|
+
import {Image, ImageResizeMode, Platform, Text, View} from "react-native";
|
|
6
|
+
|
|
7
|
+
import {Box} from "./Box";
|
|
8
|
+
import {AllColors, IconName, UnsignedUpTo12} from "./Common";
|
|
9
|
+
import {Icon} from "./Icon";
|
|
10
|
+
import {isMobileDevice} from "./MediaQuery";
|
|
11
|
+
import {Tooltip} from "./Tooltip";
|
|
12
|
+
import {Unifier} from "./Unifier";
|
|
13
|
+
|
|
14
|
+
const sizes = {
|
|
15
|
+
xs: 24,
|
|
16
|
+
sm: 32,
|
|
17
|
+
md: 48,
|
|
18
|
+
lg: 64,
|
|
19
|
+
xl: 120,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const sizeIconPadding: {[id: string]: UnsignedUpTo12} = {
|
|
23
|
+
xs: 0,
|
|
24
|
+
sm: 0,
|
|
25
|
+
md: 1,
|
|
26
|
+
lg: 1,
|
|
27
|
+
xl: 2,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const statusIcons: {[id: string]: {icon: IconName; color: AllColors; label: string}} = {
|
|
31
|
+
online: {icon: "circle", color: "green", label: "Online"},
|
|
32
|
+
offline: {icon: "circle", color: "gray", label: "Offline"},
|
|
33
|
+
doNotDisturb: {icon: "minus-circle", color: "red", label: "Do Not Disturb"},
|
|
34
|
+
away: {icon: "moon", color: "orange", label: "Away"},
|
|
35
|
+
meeting: {icon: "calendar", color: "orange", label: "In a Meeting"},
|
|
36
|
+
vacation: {icon: "plane", color: "orange", label: "On Vacation"},
|
|
37
|
+
sick: {icon: "clinic-medical", color: "orange", label: "Sick"},
|
|
38
|
+
outOfOffice: {icon: "clock", color: "orange", label: "Out of Office"},
|
|
39
|
+
commuting: {icon: "car", color: "orange", label: "Commuting"},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type AvatarStatus =
|
|
43
|
+
| "online"
|
|
44
|
+
| "offline"
|
|
45
|
+
| "doNotDisturb"
|
|
46
|
+
| "away"
|
|
47
|
+
| "meeting"
|
|
48
|
+
| "vacation"
|
|
49
|
+
| "sick"
|
|
50
|
+
| "outOfOffice"
|
|
51
|
+
| "commuting";
|
|
52
|
+
|
|
53
|
+
interface AvatarProps {
|
|
54
|
+
// Color for the background of the circle when no src picture is present.
|
|
55
|
+
backgroundColor?: AllColors;
|
|
56
|
+
// Color for the initials when no src picture is present.
|
|
57
|
+
textColor?: AllColors;
|
|
58
|
+
/**
|
|
59
|
+
* The name of the user. This is used for the placeholder treatment if an image is not available.
|
|
60
|
+
*/
|
|
61
|
+
name: string;
|
|
62
|
+
/**
|
|
63
|
+
* Override the generated initials from `name`.
|
|
64
|
+
*/
|
|
65
|
+
initials?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Adds a white border around Avatar so it's visible when displayed on other images.
|
|
68
|
+
*/
|
|
69
|
+
outline?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* xs: 24px, sm: 32px, md: 48px, lg: 64px, xl: 120px.
|
|
72
|
+
*/
|
|
73
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
74
|
+
/**
|
|
75
|
+
* The URL of the user's image.
|
|
76
|
+
*/
|
|
77
|
+
src?: string;
|
|
78
|
+
/**
|
|
79
|
+
* The fit for the image within the Avatar: "cover" | "contain" | "none".
|
|
80
|
+
* Default is undefined. See Image.tsx for more info
|
|
81
|
+
*/
|
|
82
|
+
imageFit?: "cover" | "contain" | "none";
|
|
83
|
+
/**
|
|
84
|
+
* Allow user to edit the image of the avatar
|
|
85
|
+
*/
|
|
86
|
+
editAvatarImage?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Function to handle the avatar image edit
|
|
89
|
+
*/
|
|
90
|
+
onChange?: (val: any) => void;
|
|
91
|
+
/**
|
|
92
|
+
* Resize image width. If only the width is provided, the image will preserve aspect ratio
|
|
93
|
+
*/
|
|
94
|
+
avatarImageWidth?: number;
|
|
95
|
+
/**
|
|
96
|
+
* Resize image height. If avatarImageWidth is also provided, the image aspect ratio may be distorted.
|
|
97
|
+
*/
|
|
98
|
+
avatarImageHeight?: number;
|
|
99
|
+
/**
|
|
100
|
+
* The image format that the image will be saved as after any edits by the expo-image-manipulator
|
|
101
|
+
*/
|
|
102
|
+
avatarImageFormat?: SaveFormat;
|
|
103
|
+
/**
|
|
104
|
+
* The status of the user to display with the avatar.
|
|
105
|
+
*/
|
|
106
|
+
status?: AvatarStatus;
|
|
107
|
+
/**
|
|
108
|
+
* If true, the status indicator will show a mobile icon instead of a dot, if status is one of
|
|
109
|
+
* "online", "away", "offline", or "doNotDisturb". Will show the normal status icon in other cases.
|
|
110
|
+
*/
|
|
111
|
+
statusMobile?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Text to show when hovering over the avatar image. Only works on web.
|
|
114
|
+
*/
|
|
115
|
+
statusText?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const Avatar = (props: AvatarProps): React.ReactElement => {
|
|
119
|
+
const [isImageLoaded, setIsImageLoaded] = useState(true);
|
|
120
|
+
const [hovered, setHovered] = useState(false);
|
|
121
|
+
const [src, setSrc] = useState(props.src ?? undefined);
|
|
122
|
+
const {
|
|
123
|
+
name,
|
|
124
|
+
initials,
|
|
125
|
+
outline,
|
|
126
|
+
size = "md",
|
|
127
|
+
imageFit = "contain",
|
|
128
|
+
editAvatarImage,
|
|
129
|
+
onChange,
|
|
130
|
+
avatarImageWidth = sizes[size],
|
|
131
|
+
avatarImageHeight,
|
|
132
|
+
avatarImageFormat = SaveFormat.PNG,
|
|
133
|
+
} = props;
|
|
134
|
+
const width = sizes[size];
|
|
135
|
+
const height = sizes[size];
|
|
136
|
+
const radius = sizes[size] / 2;
|
|
137
|
+
const fontSize = sizes[size] / 2;
|
|
138
|
+
const computedInitials =
|
|
139
|
+
initials ??
|
|
140
|
+
(name.match(/(^\S\S?|\s\S)?/g) as any)
|
|
141
|
+
.map((v: string) => v.trim())
|
|
142
|
+
.join("")
|
|
143
|
+
.match(/(^\S|\S$)?/g)
|
|
144
|
+
.join("")
|
|
145
|
+
.toLocaleUpperCase();
|
|
146
|
+
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
setSrc(props.src);
|
|
149
|
+
}, [props]);
|
|
150
|
+
|
|
151
|
+
if (editAvatarImage && !onChange) {
|
|
152
|
+
console.warn("Avatars with the editAvatarImage flag on should also have an onChange property.");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const handleImageError = () => {
|
|
156
|
+
setIsImageLoaded(false);
|
|
157
|
+
console.warn("Image load error");
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const pickImage = async () => {
|
|
161
|
+
// TODO: Add permission request to use camera to take a picture
|
|
162
|
+
const result = await launchImageLibraryAsync({
|
|
163
|
+
mediaTypes: MediaTypeOptions.Images,
|
|
164
|
+
allowsEditing: true,
|
|
165
|
+
base64: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (!result.canceled && result.assets) {
|
|
169
|
+
const resizedImage = await resizeImage(result.assets[0].uri);
|
|
170
|
+
setSrc(resizedImage.uri);
|
|
171
|
+
if (onChange) {
|
|
172
|
+
onChange({avatarImageFormat, ...resizedImage});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const resizeImage = async (imageUri: string): Promise<ImageResult> => {
|
|
178
|
+
return manipulateAsync(
|
|
179
|
+
imageUri,
|
|
180
|
+
[{resize: {width: avatarImageWidth, height: avatarImageHeight}}],
|
|
181
|
+
{format: avatarImageFormat}
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const renderEditIcon = () => {
|
|
186
|
+
if (editAvatarImage && hovered && Platform.OS === "web") {
|
|
187
|
+
return (
|
|
188
|
+
<Box
|
|
189
|
+
alignItems="center"
|
|
190
|
+
dangerouslySetInlineStyle={{
|
|
191
|
+
__style: {backgroundColor: "rgba(255,255,255,0.5)", borderRadius: radius},
|
|
192
|
+
}}
|
|
193
|
+
height={height}
|
|
194
|
+
justifyContent="center"
|
|
195
|
+
position="absolute"
|
|
196
|
+
// width={width}
|
|
197
|
+
zIndex={5}
|
|
198
|
+
onClick={pickImage}
|
|
199
|
+
>
|
|
200
|
+
<Icon color="darkGray" name="edit" size={size} />
|
|
201
|
+
</Box>
|
|
202
|
+
);
|
|
203
|
+
} else if (editAvatarImage && Platform.OS !== "web") {
|
|
204
|
+
return (
|
|
205
|
+
<Box
|
|
206
|
+
bottom
|
|
207
|
+
left={Boolean(props.status)}
|
|
208
|
+
paddingX={sizeIconPadding[size]}
|
|
209
|
+
position="absolute"
|
|
210
|
+
right={!Boolean(props.status)}
|
|
211
|
+
zIndex={5}
|
|
212
|
+
onClick={pickImage}
|
|
213
|
+
>
|
|
214
|
+
<Icon color="darkGray" name="edit" size={size} />
|
|
215
|
+
</Box>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const renderStatusIcon = () => {
|
|
222
|
+
if (!props.status) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
// eslint-disable-next-line prefer-const
|
|
226
|
+
let {icon, color} = statusIcons[props.status];
|
|
227
|
+
if (
|
|
228
|
+
props.statusMobile &&
|
|
229
|
+
["online", "away", "offline", "doNotDisturb"].includes(props.status)
|
|
230
|
+
) {
|
|
231
|
+
icon = "mobile-alt";
|
|
232
|
+
}
|
|
233
|
+
if (!icon || !color) {
|
|
234
|
+
console.warn(`Avatar: Invalid status ${props.status}`);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
return (
|
|
238
|
+
<Box bottom paddingX={sizeIconPadding[size]} position="absolute" right zIndex={5}>
|
|
239
|
+
<Icon color={color} name={icon} size={size} />
|
|
240
|
+
</Box>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const avatar = (
|
|
245
|
+
<Box height={height} position="relative" width={width}>
|
|
246
|
+
<Box
|
|
247
|
+
border={outline ? "white" : undefined}
|
|
248
|
+
height={height}
|
|
249
|
+
overflow="hidden"
|
|
250
|
+
position="relative"
|
|
251
|
+
rounding="circle"
|
|
252
|
+
width={width}
|
|
253
|
+
onHoverEnd={() => setHovered(false)}
|
|
254
|
+
onHoverStart={() => setHovered(true)}
|
|
255
|
+
>
|
|
256
|
+
{src && isImageLoaded ? (
|
|
257
|
+
// TODO: Make our Image component rounding work so that we can use it for Avatar. Currently it creates an
|
|
258
|
+
// unrounded box around the Image.
|
|
259
|
+
<Image
|
|
260
|
+
resizeMode={imageFit as ImageResizeMode}
|
|
261
|
+
source={{uri: src, cache: "force-cache"}}
|
|
262
|
+
style={{
|
|
263
|
+
borderRadius: radius,
|
|
264
|
+
height,
|
|
265
|
+
width,
|
|
266
|
+
display: "flex",
|
|
267
|
+
alignItems: "center",
|
|
268
|
+
justifyContent: "center",
|
|
269
|
+
overflow: "hidden",
|
|
270
|
+
}}
|
|
271
|
+
onError={handleImageError}
|
|
272
|
+
/>
|
|
273
|
+
) : (
|
|
274
|
+
<View
|
|
275
|
+
style={{
|
|
276
|
+
height,
|
|
277
|
+
width,
|
|
278
|
+
borderRadius: radius,
|
|
279
|
+
display: "flex",
|
|
280
|
+
alignItems: "center",
|
|
281
|
+
justifyContent: "center",
|
|
282
|
+
backgroundColor: props.backgroundColor
|
|
283
|
+
? Unifier.theme[props.backgroundColor]
|
|
284
|
+
: Unifier.theme.gray,
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
<Text style={{fontSize, color: props.textColor ?? Unifier.theme.darkGray}}>
|
|
288
|
+
{computedInitials}
|
|
289
|
+
</Text>
|
|
290
|
+
</View>
|
|
291
|
+
)}
|
|
292
|
+
</Box>
|
|
293
|
+
{/* Needs to come after the image so it renders on top. */}
|
|
294
|
+
{renderEditIcon()}
|
|
295
|
+
{renderStatusIcon()}
|
|
296
|
+
</Box>
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
let status = props.statusText;
|
|
300
|
+
if (!status && props.status) {
|
|
301
|
+
status = statusIcons[props.status]?.label;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (status) {
|
|
305
|
+
// Need to wrap the tooltip so it doesn't expand to 100% width and render the tooltip off. Don't show the
|
|
306
|
+
// tooltips on mobile because they intercept the edit avatar clicks.
|
|
307
|
+
return (
|
|
308
|
+
<Box width={width}>
|
|
309
|
+
<Tooltip idealDirection="top" text={isMobileDevice() ? undefined : status}>
|
|
310
|
+
{avatar}
|
|
311
|
+
</Tooltip>
|
|
312
|
+
</Box>
|
|
313
|
+
);
|
|
314
|
+
} else {
|
|
315
|
+
return avatar;
|
|
316
|
+
}
|
|
317
|
+
};
|
package/src/Badge.tsx
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {Text, View} from "react-native";
|
|
3
|
+
|
|
4
|
+
import {AllColors} from "./Common";
|
|
5
|
+
import {Unifier} from "./Unifier";
|
|
6
|
+
|
|
7
|
+
interface BadgeProps {
|
|
8
|
+
// The text to display inside the badge.
|
|
9
|
+
title: string;
|
|
10
|
+
// Position relative to the text. Top should only be used with headings.
|
|
11
|
+
position?: "top" | "middle"; // default "middle"
|
|
12
|
+
// Some default badge types. Occasionally, a custom badge might be required for different color schemes.
|
|
13
|
+
type?: "info" | "error" | "warning" | "success" | "neutral" | "custom"; // default "info
|
|
14
|
+
// If `type` is set to "custom", a custom theme color should be provided.
|
|
15
|
+
color?: AllColors;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const BADGE_COLORS: {[key: string]: AllColors} = {
|
|
19
|
+
info: "blue",
|
|
20
|
+
error: "red",
|
|
21
|
+
warning: "orange",
|
|
22
|
+
success: "springGreen",
|
|
23
|
+
neutral: "gray",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function Badge({
|
|
27
|
+
title,
|
|
28
|
+
position = "middle",
|
|
29
|
+
type = "info",
|
|
30
|
+
color,
|
|
31
|
+
}: BadgeProps): React.ReactElement {
|
|
32
|
+
if (color && type !== "custom") {
|
|
33
|
+
console.warn('Badge color only supported when `type` is set to "custom".');
|
|
34
|
+
}
|
|
35
|
+
const badgeColor = type === "custom" ? color! : BADGE_COLORS[type];
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<View
|
|
39
|
+
style={{
|
|
40
|
+
backgroundColor: Unifier.theme[badgeColor],
|
|
41
|
+
borderRadius: 2,
|
|
42
|
+
height: 14,
|
|
43
|
+
paddingTop: 2,
|
|
44
|
+
paddingBottom: 2,
|
|
45
|
+
paddingLeft: 4,
|
|
46
|
+
paddingRight: 4,
|
|
47
|
+
marginTop: -4,
|
|
48
|
+
marginLeft: 4,
|
|
49
|
+
display: "flex",
|
|
50
|
+
alignSelf: position === "middle" ? "center" : "flex-start",
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<Text
|
|
54
|
+
style={{
|
|
55
|
+
fontSize: 10,
|
|
56
|
+
flexWrap: "nowrap",
|
|
57
|
+
color: "white",
|
|
58
|
+
fontWeight: "bold",
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
{title}
|
|
62
|
+
</Text>
|
|
63
|
+
</View>
|
|
64
|
+
);
|
|
65
|
+
}
|
package/src/Banner.tsx
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import {Box} from "./Box";
|
|
4
|
+
import {BoxColor, ButtonColor, Rounding, TextColor} from "./Common";
|
|
5
|
+
import {IconButton} from "./IconButton";
|
|
6
|
+
import {Text} from "./Text";
|
|
7
|
+
import {Unifier} from "./Unifier";
|
|
8
|
+
|
|
9
|
+
// import {faTimesCircle, faArrowRight} from "@fortawesome/free-solid-svg-icons";
|
|
10
|
+
|
|
11
|
+
export interface BannerProps {
|
|
12
|
+
id: string;
|
|
13
|
+
text: string;
|
|
14
|
+
subtext?: string;
|
|
15
|
+
color?: BoxColor;
|
|
16
|
+
textColor?: TextColor;
|
|
17
|
+
negativeXMargin?: number;
|
|
18
|
+
bold?: boolean;
|
|
19
|
+
shape?: Rounding;
|
|
20
|
+
type?: "dismiss" | "action";
|
|
21
|
+
onClick?: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// library.add(faTimesCircle);
|
|
25
|
+
// library.add(faArrowRight);
|
|
26
|
+
|
|
27
|
+
interface BannerState {
|
|
28
|
+
show: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getKey(id: string) {
|
|
32
|
+
return `@ReactUnifier:${id}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const hideBanner = (id: string) => {
|
|
36
|
+
console.debug(`[banner] Hiding ${getKey(id)} `);
|
|
37
|
+
Unifier.storage.setItem(getKey(id), "true");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export class Banner extends React.Component<BannerProps, BannerState> {
|
|
41
|
+
state = {show: false};
|
|
42
|
+
|
|
43
|
+
async componentDidMount() {
|
|
44
|
+
const seen = await Unifier.storage.getItem(getKey(this.props.id));
|
|
45
|
+
console.debug(`[banner] ${getKey(this.props.id)} seen? ${seen}`);
|
|
46
|
+
this.setState({show: !seen});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
dismiss = () => {
|
|
50
|
+
hideBanner(this.props.id);
|
|
51
|
+
this.setState({show: false});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
render() {
|
|
55
|
+
if (!this.state.show) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const type = this.props.type || "dismiss";
|
|
59
|
+
|
|
60
|
+
if (type === "action" && !this.props.onClick) {
|
|
61
|
+
console.warn("Banners with type action require an onClick property.");
|
|
62
|
+
}
|
|
63
|
+
const negativeMargin = (this.props.negativeXMargin || 0) * -4;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Box
|
|
67
|
+
color={this.props.color || "secondaryDark"}
|
|
68
|
+
dangerouslySetInlineStyle={{
|
|
69
|
+
__style: {
|
|
70
|
+
marginLeft: negativeMargin,
|
|
71
|
+
marginRight: negativeMargin,
|
|
72
|
+
},
|
|
73
|
+
}}
|
|
74
|
+
direction="row"
|
|
75
|
+
justifyContent="between"
|
|
76
|
+
marginBottom={3}
|
|
77
|
+
marginTop={3}
|
|
78
|
+
paddingX={3}
|
|
79
|
+
paddingY={2}
|
|
80
|
+
rounding={this.props.shape}
|
|
81
|
+
shadow
|
|
82
|
+
width={Unifier.utils.dimensions().width || "100%"}
|
|
83
|
+
onClick={this.dismiss}
|
|
84
|
+
>
|
|
85
|
+
<Box alignItems="center" direction="column" flex="shrink" justifyContent="center">
|
|
86
|
+
<Box paddingY={1}>
|
|
87
|
+
<Text align="center" color={this.props.textColor || "white"} weight="bold">
|
|
88
|
+
{this.props.text}
|
|
89
|
+
</Text>
|
|
90
|
+
</Box>
|
|
91
|
+
{this.props.subtext && (
|
|
92
|
+
<Box paddingY={1}>
|
|
93
|
+
<Text align="center" color={this.props.textColor || "white"}>
|
|
94
|
+
{this.props.subtext}
|
|
95
|
+
</Text>
|
|
96
|
+
</Box>
|
|
97
|
+
)}
|
|
98
|
+
</Box>
|
|
99
|
+
<Box alignItems="center" display="block" justifyContent="center" width={40}>
|
|
100
|
+
{type === "dismiss" && (
|
|
101
|
+
<IconButton
|
|
102
|
+
accessibilityLabel=""
|
|
103
|
+
icon="times-circle"
|
|
104
|
+
// size="lg"
|
|
105
|
+
iconColor={(this.props.textColor || "white") as ButtonColor}
|
|
106
|
+
prefix="fas"
|
|
107
|
+
onClick={() => this.dismiss()}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
{type === "action" && (
|
|
111
|
+
<IconButton
|
|
112
|
+
accessibilityLabel=""
|
|
113
|
+
icon="arrow-right"
|
|
114
|
+
// size="lg"
|
|
115
|
+
iconColor={(this.props.textColor || "white") as ButtonColor}
|
|
116
|
+
prefix="fas"
|
|
117
|
+
onClick={() => this.props.onClick && this.props.onClick()}
|
|
118
|
+
/>
|
|
119
|
+
)}
|
|
120
|
+
</Box>
|
|
121
|
+
</Box>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {BlurView} from "@react-native-community/blur";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import {Platform, View} from "react-native";
|
|
4
|
+
|
|
5
|
+
import {Box} from "./Box";
|
|
6
|
+
import {BlurBoxProps} from "./Common";
|
|
7
|
+
|
|
8
|
+
export class BlurBox extends React.Component<BlurBoxProps, {}> {
|
|
9
|
+
renderBlur(children: any) {
|
|
10
|
+
if (Platform.OS === "ios") {
|
|
11
|
+
return (
|
|
12
|
+
<BlurView blurType={this.props.blurType || "regular"} style={{borderRadius: 12}}>
|
|
13
|
+
{children}
|
|
14
|
+
</BlurView>
|
|
15
|
+
);
|
|
16
|
+
} else {
|
|
17
|
+
return (
|
|
18
|
+
<View style={{backgroundColor: "rgba(82, 82, 82, 0.7)", borderRadius: 10}}>{children}</View>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
render() {
|
|
24
|
+
const {marginBottom, marginTop, margin, ...props} = this.props;
|
|
25
|
+
return (
|
|
26
|
+
<Box
|
|
27
|
+
margin={margin || 0}
|
|
28
|
+
marginBottom={marginBottom || 4}
|
|
29
|
+
marginTop={marginTop || 0}
|
|
30
|
+
width="100%"
|
|
31
|
+
>
|
|
32
|
+
{this.renderBlur(
|
|
33
|
+
<Box paddingX={4} {...props}>
|
|
34
|
+
{this.props.children}
|
|
35
|
+
</Box>
|
|
36
|
+
)}
|
|
37
|
+
</Box>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/BlurBox.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import {Box} from "./Box";
|
|
4
|
+
import {BlurBoxProps} from "./Common";
|
|
5
|
+
import {mergeInlineStyles} from "./Utilities";
|
|
6
|
+
|
|
7
|
+
export class BlurBox extends React.Component<BlurBoxProps, {}> {
|
|
8
|
+
render() {
|
|
9
|
+
const {marginBottom, marginTop, margin, ...props} = this.props;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Box
|
|
13
|
+
{...this.props}
|
|
14
|
+
dangerouslySetInlineStyle={mergeInlineStyles(this.props.dangerouslySetInlineStyle, {
|
|
15
|
+
// filter: "blur(4px)",
|
|
16
|
+
backdropFilter: "blur(4px)",
|
|
17
|
+
backgroundColor: "#111",
|
|
18
|
+
opacity: 0.8,
|
|
19
|
+
borderRadius: 12,
|
|
20
|
+
})}
|
|
21
|
+
margin={margin || 0}
|
|
22
|
+
marginBottom={marginBottom || 4}
|
|
23
|
+
marginTop={marginTop || 0}
|
|
24
|
+
>
|
|
25
|
+
<Box paddingX={4} {...props}>
|
|
26
|
+
{this.props.children}
|
|
27
|
+
</Box>
|
|
28
|
+
</Box>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/Body.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {ActivityIndicator, KeyboardAvoidingView} from "react-native";
|
|
3
|
+
|
|
4
|
+
import {Box} from "./Box";
|
|
5
|
+
import {BodyProps} from "./Common";
|
|
6
|
+
import {Unifier} from "./Unifier";
|
|
7
|
+
|
|
8
|
+
export class Body extends React.Component<BodyProps, {}> {
|
|
9
|
+
renderBody() {
|
|
10
|
+
return (
|
|
11
|
+
<Box avoidKeyboard height="100%" scroll={this.props.scroll}>
|
|
12
|
+
<Box
|
|
13
|
+
height={this.props.height || "100%"}
|
|
14
|
+
padding={this.props.padding !== undefined ? this.props.padding : 5}
|
|
15
|
+
>
|
|
16
|
+
{this.props.loading === true && (
|
|
17
|
+
<ActivityIndicator color={Unifier.theme.darkGray} size="large" />
|
|
18
|
+
)}
|
|
19
|
+
{this.props.children}
|
|
20
|
+
</Box>
|
|
21
|
+
</Box>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
render() {
|
|
26
|
+
if (this.props.avoidKeyboard === false) {
|
|
27
|
+
return this.renderBody();
|
|
28
|
+
} else {
|
|
29
|
+
return <KeyboardAvoidingView behavior="position">{this.renderBody()}</KeyboardAvoidingView>;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|