ferns-ui 1.0.0-beta.0 → 1.0.1

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/src/Common.ts CHANGED
@@ -31,7 +31,7 @@ export interface AccordionProps {
31
31
  /**
32
32
  * The subtitle of the information modal.
33
33
  */
34
- infoModalSubTitle?: ModalProps["subTitle"];
34
+ infoModalSubtitle?: ModalProps["subtitle"];
35
35
 
36
36
  /**
37
37
  * The text content of the information modal.
@@ -49,6 +49,11 @@ export interface AccordionProps {
49
49
  */
50
50
  isCollapsed?: boolean;
51
51
 
52
+ /*
53
+ * The subtitle showed below the title of the accordion.
54
+ */
55
+ subtitle?: string;
56
+
52
57
  /**
53
58
  * The title of the accordion.
54
59
  */
@@ -1296,6 +1301,26 @@ export interface AvatarProps {
1296
1301
  }
1297
1302
 
1298
1303
  export interface BadgeProps {
1304
+ /**
1305
+ * When status is "custom", determines the badge's background color.
1306
+ */
1307
+ customBackgroundColor?: string;
1308
+ /**
1309
+ * When status is "custom", determines the badge's border color.
1310
+ */
1311
+ customBorderColor?: string;
1312
+ /**
1313
+ * When status is "custom", determines the badge's icon color
1314
+ */
1315
+ customIconColor?: IconColor;
1316
+ /**
1317
+ * When status is "custom", determines the badge's icon
1318
+ */
1319
+ customIconName?: IconName;
1320
+ /**
1321
+ * When status is "custom", determines the badge's text color.
1322
+ */
1323
+ customTextColor?: string;
1299
1324
  /**
1300
1325
  * The name of the icon to display in the badge.
1301
1326
  */
@@ -1320,7 +1345,7 @@ export interface BadgeProps {
1320
1345
  * The status of the badge. Determines its color and appearance.
1321
1346
  * @default "info"
1322
1347
  */
1323
- status?: "info" | "error" | "warning" | "success" | "neutral";
1348
+ status?: "info" | "error" | "warning" | "success" | "neutral" | "custom";
1324
1349
 
1325
1350
  /**
1326
1351
  * The text or number to display inside the badge.
@@ -1711,7 +1736,7 @@ export interface ModalProps {
1711
1736
  /**
1712
1737
  * The subtitle of the modal.
1713
1738
  */
1714
- subTitle?: string;
1739
+ subtitle?: string;
1715
1740
  /**
1716
1741
  * The text content of the modal.
1717
1742
  */
@@ -2103,28 +2128,61 @@ export type TapToEditProps =
2103
2128
  export interface BaseTapToEditProps extends Omit<FieldProps, "onChange" | "value"> {
2104
2129
  title: string;
2105
2130
  value: any;
2106
- // Not required if not editable.
2131
+
2132
+ /**
2133
+ * Not required if not editable.
2134
+ */
2107
2135
  setValue?: (value: any) => void;
2108
- // Not required if not editable.
2136
+
2137
+ /**
2138
+ * Not required if not editable.
2139
+ */
2109
2140
  onSave?: (value: any) => void | Promise<void>;
2110
- // Defaults to true
2141
+
2142
+ /**
2143
+ * If false, the field will not be editable and will be disabled
2144
+ * @default true
2145
+ */
2111
2146
  editable?: boolean;
2112
- // enable edit mode from outside the component
2147
+
2148
+ /**
2149
+ * Enable edit mode from outside the component.
2150
+ */
2113
2151
  isEditing?: boolean;
2114
- // For changing how the non-editing row renders
2115
- rowBoxProps?: Partial<BoxProps>;
2116
2152
  transform?: (value: any) => string;
2117
- fieldComponent?: (setValue: () => void) => ReactElement;
2153
+ /**
2154
+ * Show a confirmation modal before saving the value.
2155
+ * @default false
2156
+ */
2118
2157
  withConfirmation?: boolean;
2158
+
2159
+ /**
2160
+ * The text content of the confirmation modal.
2161
+ * @default "Are you sure you want save your changes?"
2162
+ */
2119
2163
  confirmationText?: string;
2120
- confirmationHeading?: string;
2121
- description?: string;
2164
+
2165
+ /**
2166
+ * The title of the confirmation modal.
2167
+ * @default "Confirm"
2168
+ */
2169
+ confirmationTitle?: string;
2170
+
2171
+ /**
2172
+ * Field helperText, a description of the field surfaced in the UI
2173
+ * @default "Confirm"
2174
+ */
2175
+ helperText?: string;
2176
+
2177
+ /**
2178
+ * Only display the helperText in the UI while editing. if false, the helperText is always shown below the value.
2179
+ * @default true
2180
+ */
2181
+ onlyShowHelperTextWhileEditing?: boolean;
2182
+
2122
2183
  // openApi to supported in future
2123
2184
  // openApiModel?: string;
2124
2185
  // openApiField?: string;
2125
- showDescriptionAsTooltip?: boolean;
2126
- // Default true. If false, description is shown below the value always.
2127
- onlyShowDescriptionWhileEditing?: boolean;
2128
2186
  }
2129
2187
 
2130
2188
  export interface APIError {
@@ -2343,6 +2401,12 @@ export interface SelectFieldPropsBase {
2343
2401
  */
2344
2402
  helperText?: string;
2345
2403
 
2404
+ /**
2405
+ * The function to call when the selected value changes.
2406
+ * If requireValue is false and value is undefined, onChange will return empty string.
2407
+ */
2408
+ onChange: (value: string) => void;
2409
+
2346
2410
  /**
2347
2411
  * The options available for selection in the select field.
2348
2412
  * Each option should have a label and a value.
@@ -2371,11 +2435,6 @@ export interface SelectFieldPropsWithoutRequire extends SelectFieldPropsBase {
2371
2435
  * The current value of the select field.
2372
2436
  */
2373
2437
  value?: string;
2374
-
2375
- /**
2376
- * The function to call when the selected value changes.
2377
- */
2378
- onChange: (value: string | undefined) => void;
2379
2438
  }
2380
2439
 
2381
2440
  export interface SelectFieldPropsWithRequire extends SelectFieldPropsBase {
@@ -340,7 +340,7 @@ const DateCalendar = ({
340
340
 
341
341
  // Check if the date is T00:00:00.000Z (it should be), otherwise treat it as a date in the
342
342
  // current timezone.
343
- const dt = DateTime.fromISO(date).setZone("UTC");
343
+ const dt = DateTime.fromISO(date);
344
344
  let dateString: string;
345
345
  if (dt.hour === 0 && dt.minute === 0 && dt.second === 0) {
346
346
  dateString = dt.toISO()!;
@@ -26,7 +26,7 @@ export const DateTimeField = ({
26
26
  return printDateAndTime(val, {timezone, showTimezone: true});
27
27
  case "date":
28
28
  default:
29
- return printDate(val, {timezone, showTimezone: true});
29
+ return printDate(val, {ignoreTime: true});
30
30
  }
31
31
  },
32
32
  [timezone, type]
@@ -108,9 +108,9 @@ export const DateTimeField = ({
108
108
  const month = cleanedInput.slice(0, 2);
109
109
  const day = cleanedInput.slice(2, 4);
110
110
  const year = cleanedInput.slice(4, 8);
111
- parsedDate = DateTime.fromFormat(`${month}${day}${year}`, "MMddyyyy", {
112
- zone: timezone,
113
- });
111
+ parsedDate = DateTime.fromFormat(`${month}${day}${year}`, "MMddyyyy", {zone: timezone})
112
+ .startOf("day")
113
+ .toUTC(0, {keepLocalTime: true});
114
114
  }
115
115
 
116
116
  if (parsedDate?.isValid) {
package/src/Field.tsx CHANGED
@@ -48,7 +48,12 @@ export const Field: FC<FieldProps> = ({type, ...rest}) => {
48
48
  } else if (type === "boolean") {
49
49
  return <BooleanField {...(rest as BooleanFieldProps)} />;
50
50
  } else if (type && ["date", "time", "datetime"].includes(type)) {
51
- return <DateTimeField {...(rest as DateTimeFieldProps)} />;
51
+ return (
52
+ <DateTimeField
53
+ {...(rest as DateTimeFieldProps)}
54
+ type={type as "date" | "time" | "datetime"}
55
+ />
56
+ );
52
57
  } else if (type === "address") {
53
58
  return <AddressField {...(rest as AddressFieldProps)} />;
54
59
  } else if (type === "customSelect") {
@@ -15,7 +15,7 @@ import {isNative} from "./Utilities";
15
15
  type ConfirmationModalProps = {
16
16
  visible: boolean;
17
17
  title: string;
18
- subTitle?: string;
18
+ subtitle?: string;
19
19
  text: string;
20
20
  onConfirm: () => void;
21
21
  onCancel: () => void;
@@ -24,7 +24,7 @@ type ConfirmationModalProps = {
24
24
  const ConfirmationModal: FC<ConfirmationModalProps> = ({
25
25
  visible,
26
26
  title,
27
- subTitle,
27
+ subtitle,
28
28
  text,
29
29
  onConfirm,
30
30
  onCancel,
@@ -35,7 +35,7 @@ const ConfirmationModal: FC<ConfirmationModalProps> = ({
35
35
  primaryButtonText="Confirm"
36
36
  secondaryButtonOnClick={onCancel}
37
37
  secondaryButtonText="Cancel"
38
- subTitle={subTitle}
38
+ subtitle={subtitle}
39
39
  title={title}
40
40
  visible={visible}
41
41
  onDismiss={onCancel}
@@ -173,7 +173,7 @@ const IconButtonComponent: FC<IconButtonProps> = ({
173
173
  )}
174
174
  {withConfirmation && (
175
175
  <ConfirmationModal
176
- subTitle={undefined}
176
+ subtitle={undefined}
177
177
  text={confirmationText}
178
178
  title={confirmationHeading}
179
179
  visible={showConfirmation}
package/src/Modal.tsx CHANGED
@@ -32,7 +32,7 @@ const getModalSize = (size: "sm" | "md" | "lg"): DimensionValue => {
32
32
  const ModalContent: FC<{
33
33
  children?: ModalProps["children"];
34
34
  title?: ModalProps["title"];
35
- subTitle?: ModalProps["subTitle"];
35
+ subtitle?: ModalProps["subtitle"];
36
36
  text?: ModalProps["text"];
37
37
  primaryButtonText?: ModalProps["primaryButtonText"];
38
38
  primaryButtonDisabled?: ModalProps["primaryButtonDisabled"];
@@ -46,7 +46,7 @@ const ModalContent: FC<{
46
46
  }> = ({
47
47
  children,
48
48
  title,
49
- subTitle,
49
+ subtitle,
50
50
  text,
51
51
  primaryButtonText,
52
52
  primaryButtonDisabled,
@@ -113,14 +113,14 @@ const ModalContent: FC<{
113
113
  <Heading size="lg">{title}</Heading>
114
114
  </View>
115
115
  )}
116
- {subTitle && (
116
+ {subtitle && (
117
117
  <View
118
118
  accessibilityHint="Modal Sub Heading Text"
119
- accessibilityLabel={subTitle}
119
+ accessibilityLabel={subtitle}
120
120
  accessibilityRole="text"
121
- style={{alignSelf: "flex-start", marginTop: subTitle ? 8 : 0}}
121
+ style={{alignSelf: "flex-start", marginTop: subtitle ? 8 : 0}}
122
122
  >
123
- <Text size="lg">{subTitle}</Text>
123
+ <Text size="lg">{subtitle}</Text>
124
124
  </View>
125
125
  )}
126
126
  {text && (
@@ -172,7 +172,7 @@ export const Modal: FC<ModalProps> = ({
172
172
  primaryButtonText,
173
173
  secondaryButtonText,
174
174
  size = "sm",
175
- subTitle,
175
+ subtitle,
176
176
  text,
177
177
  title,
178
178
  visible,
@@ -201,7 +201,7 @@ export const Modal: FC<ModalProps> = ({
201
201
 
202
202
  const modalContentProps = {
203
203
  title,
204
- subTitle,
204
+ subtitle,
205
205
  text,
206
206
  primaryButtonText,
207
207
  primaryButtonDisabled,
package/src/Page.tsx CHANGED
@@ -61,7 +61,7 @@ export class Page extends React.Component<PageProps, {}> {
61
61
  <Box
62
62
  alignSelf="center"
63
63
  avoidKeyboard
64
- color={this.props.color || "neutralLight"}
64
+ color={this.props.color || "base"}
65
65
  direction={this.props.direction || "column"}
66
66
  display={this.props.display || "flex"}
67
67
  flex="grow"
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import {Pressable, View} from "react-native";
3
3
 
4
4
  import {SegmentedControlProps} from "./Common";
5
- import {Text} from "./Text";
5
+ import {Heading} from "./Heading";
6
6
  import {useTheme} from "./Theme";
7
7
 
8
8
  export const SegmentedControl = ({
@@ -23,6 +23,7 @@ export const SegmentedControl = ({
23
23
  alignItems: "center",
24
24
  gap: 4,
25
25
  height,
26
+ maxHeight: height,
26
27
  borderRadius: theme.primitives.radius3xl,
27
28
  borderColor: theme.primitives.neutral300,
28
29
  borderWidth: 3,
@@ -52,7 +53,7 @@ export const SegmentedControl = ({
52
53
  }}
53
54
  onPress={() => onChange(index)}
54
55
  >
55
- <Text>{item}</Text>
56
+ <Heading size="sm">{item}</Heading>
56
57
  </Pressable>
57
58
  ))}
58
59
  </View>
@@ -28,8 +28,8 @@ export const SelectField: FC<SelectFieldProps> = ({
28
28
  placeholder={!requireValue ? clearOption : {}}
29
29
  value={value ?? ""}
30
30
  onValueChange={(v) => {
31
- if (v === "" && !requireValue) {
32
- (onChange as (val: string | undefined) => void)(undefined);
31
+ if (v === undefined || v === "") {
32
+ onChange("");
33
33
  } else {
34
34
  onChange(v);
35
35
  }
package/src/TapToEdit.tsx CHANGED
@@ -1,45 +1,33 @@
1
1
  import React, {ReactElement, useEffect, useState} from "react";
2
- import {Linking} from "react-native";
2
+ import {Linking, View} from "react-native";
3
3
 
4
4
  import {Box} from "./Box";
5
5
  import {Button} from "./Button";
6
- import {AddressInterface, BoxProps, FieldProps, TapToEditProps} from "./Common";
6
+ import {AddressInterface, FieldProps, TapToEditProps} from "./Common";
7
7
  import {Field} from "./Field";
8
8
  import {Icon} from "./Icon";
9
9
  // import {useOpenAPISpec} from "./OpenAPIContext";
10
10
  import {Text} from "./Text";
11
- import {Tooltip} from "./Tooltip";
12
11
 
13
12
  const TapToEditTitle = ({
14
13
  title,
15
- description,
16
- showDescriptionAsTooltip,
17
- onlyShowDescriptionWhileEditing,
14
+ helperText,
15
+ onlyShowHelperTextWhileEditing,
18
16
  }: {
19
- onlyShowDescriptionWhileEditing?: boolean;
20
- showDescriptionAsTooltip?: boolean;
17
+ onlyShowHelperTextWhileEditing?: boolean;
21
18
  title: string;
22
- description?: string;
19
+ helperText?: string;
23
20
  }): ReactElement => {
24
- const Title = (
25
- <Box flex="grow" justifyContent="center">
26
- <Text bold>{title}:</Text>
27
- {Boolean(description && !showDescriptionAsTooltip && !onlyShowDescriptionWhileEditing) && (
21
+ return (
22
+ <View style={{flex: 1, justifyContent: "center"}}>
23
+ <Text bold>{title}</Text>
24
+ {Boolean(helperText && !onlyShowHelperTextWhileEditing) && (
28
25
  <Text color="secondaryLight" size="sm">
29
- {description}
26
+ {helperText}
30
27
  </Text>
31
28
  )}
32
- </Box>
29
+ </View>
33
30
  );
34
- if (showDescriptionAsTooltip) {
35
- return (
36
- <Tooltip idealPosition="top" text={description}>
37
- {Title}
38
- </Tooltip>
39
- );
40
- } else {
41
- return Title;
42
- }
43
31
  };
44
32
 
45
33
  export function formatAddress(address: AddressInterface, asString = false): string {
@@ -87,20 +75,17 @@ export const TapToEdit = ({
87
75
  onSave,
88
76
  editable = true,
89
77
  isEditing = false,
90
- rowBoxProps,
91
78
  transform,
92
- fieldComponent,
93
79
  withConfirmation = false,
94
80
  confirmationText = "Are you sure you want to save your changes?",
95
- confirmationHeading = "Confirm",
96
- description: propsDescription,
97
- showDescriptionAsTooltip = false,
98
- onlyShowDescriptionWhileEditing = true,
81
+ confirmationTitle = "Confirm",
82
+ helperText: propsHelperText,
83
+ onlyShowHelperTextWhileEditing = true,
99
84
  ...fieldProps
100
85
  }: TapToEditProps): ReactElement => {
101
86
  const [editing, setEditing] = useState(false);
102
87
  const [initialValue, setInitialValue] = useState();
103
- const description: string | undefined = propsDescription;
88
+ const helperText: string | undefined = propsHelperText;
104
89
  // setInitialValue is called after initial render to handle the case where the value is updated
105
90
  useEffect(() => {
106
91
  setInitialValue(value);
@@ -114,37 +99,22 @@ export const TapToEdit = ({
114
99
 
115
100
  if (editable && (editing || isEditing)) {
116
101
  return (
117
- <Box direction="column">
118
- {fieldComponent ? (
119
- fieldComponent(setValue as any)
120
- ) : (
102
+ <View style={{flexDirection: "column", width: "100%"}}>
103
+ <View style={{flex: 1, justifyContent: "center"}}>
104
+ <Text bold>{title}</Text>
105
+ </View>
106
+ <View style={{gap: 16}}>
121
107
  <Field
122
- helperText={description}
123
- label={title}
108
+ grow={fieldProps?.type === "textarea" ? fieldProps.grow ?? true : undefined}
109
+ helperText={helperText}
110
+ row={fieldProps?.type === "textarea" ? 5 : undefined}
124
111
  type={(fieldProps?.type ?? "text") as NonNullable<FieldProps["type"]>}
125
112
  value={value}
126
113
  onChange={setValue ?? (() => {})}
127
114
  {...(fieldProps as any)}
128
115
  />
129
- )}
130
- {editing && !isEditing && (
131
- <Box direction="row">
132
- <Button
133
- confirmationText={confirmationText}
134
- modalTitle={confirmationHeading}
135
- text="Save"
136
- withConfirmation={withConfirmation}
137
- onClick={async (): Promise<void> => {
138
- if (!onSave) {
139
- console.error("No onSave provided for editable TapToEdit");
140
- } else {
141
- setInitialValue(value);
142
- await onSave(value);
143
- }
144
- setEditing(false);
145
- }}
146
- />
147
- <Box marginLeft={2}>
116
+ {editing && !isEditing && (
117
+ <View style={{flexDirection: "row", justifyContent: "flex-end", gap: 16}}>
148
118
  <Button
149
119
  text="Cancel"
150
120
  variant="muted"
@@ -155,10 +125,27 @@ export const TapToEdit = ({
155
125
  setEditing(false);
156
126
  }}
157
127
  />
158
- </Box>
159
- </Box>
160
- )}
161
- </Box>
128
+ <View style={{marginLeft: 8}}>
129
+ <Button
130
+ confirmationText={confirmationText}
131
+ modalTitle={confirmationTitle}
132
+ text="Save"
133
+ withConfirmation={withConfirmation}
134
+ onClick={async (): Promise<void> => {
135
+ if (!onSave) {
136
+ console.error("No onSave provided for editable TapToEdit");
137
+ } else {
138
+ setInitialValue(value);
139
+ await onSave(value);
140
+ }
141
+ setEditing(false);
142
+ }}
143
+ />
144
+ </View>
145
+ </View>
146
+ )}
147
+ </View>
148
+ </View>
162
149
  );
163
150
  } else {
164
151
  let displayValue = value;
@@ -169,6 +156,7 @@ export const TapToEdit = ({
169
156
  // If no transform, try and display the value reasonably.
170
157
  if (fieldProps?.type === "boolean") {
171
158
  displayValue = value ? "Yes" : "No";
159
+ // TODO: put transform back in after field types are updated
172
160
  // } else if (fieldProps?.type === "percent") {
173
161
  // // Prevent floating point errors from showing up by using parseFloat and precision.
174
162
  // // Pass through parseFloat again to trim off insignificant zeroes.
@@ -217,23 +205,27 @@ export const TapToEdit = ({
217
205
  // For textarea to display correctly, we place the title on its own line, then the text
218
206
  // on the next line. This is because the textarea will take up the full width of the row.
219
207
  return (
220
- <Box
221
- alignItems={fieldProps?.type === "textarea" ? "start" : "center"}
222
- direction={fieldProps?.type === "textarea" ? "column" : "row"}
223
- justifyContent="between"
224
- paddingX={3}
225
- paddingY={2}
226
- width="100%"
227
- {...(rowBoxProps as Exclude<BoxProps, "onClick">)}
208
+ <View
209
+ style={{
210
+ alignItems: fieldProps?.type === "textarea" ? "flex-start" : "center",
211
+ flexDirection: fieldProps?.type === "textarea" ? "column" : "row",
212
+ justifyContent: "space-between",
213
+ width: "100%",
214
+ }}
228
215
  >
229
- <Box direction="row" width="100%">
216
+ <View style={{flexDirection: "row", width: "100%", gap: 16}}>
230
217
  <TapToEditTitle
231
- description={description}
232
- onlyShowDescriptionWhileEditing={onlyShowDescriptionWhileEditing}
233
- showDescriptionAsTooltip={showDescriptionAsTooltip}
218
+ helperText={helperText}
219
+ onlyShowHelperTextWhileEditing={onlyShowHelperTextWhileEditing}
234
220
  title={title}
235
221
  />
236
- <Box direction="row" flex="grow" justifyContent="end" marginLeft={2}>
222
+ <View
223
+ style={{
224
+ flexDirection: "row",
225
+ flex: 1,
226
+ justifyContent: "flex-end",
227
+ }}
228
+ >
237
229
  <Box
238
230
  accessibilityHint=""
239
231
  accessibilityLabel="Link"
@@ -257,30 +249,16 @@ export const TapToEdit = ({
257
249
  <Icon iconName="pencil" size="md" />
258
250
  </Box>
259
251
  )}
260
- </Box>
261
- </Box>
252
+ </View>
253
+ </View>
262
254
  {fieldProps?.type === "textarea" && (
263
- <>
264
- <Box marginTop={2} paddingY={2} width="100%">
265
- <Text align="left" underline={isClickable}>
266
- {displayValue}
267
- </Text>
268
- </Box>
269
- {editable && (
270
- <Box
271
- accessibilityHint=""
272
- accessibilityLabel="Edit"
273
- alignSelf="end"
274
- marginLeft={2}
275
- width={16}
276
- onClick={(): void => setEditing(true)}
277
- >
278
- <Icon color="primary" iconName="pencil" size="md" />
279
- </Box>
280
- )}
281
- </>
255
+ <View style={{marginTop: 8, paddingVertical: 8, width: "100%"}}>
256
+ <Text align="left" underline={isClickable}>
257
+ {displayValue}
258
+ </Text>
259
+ </View>
282
260
  )}
283
- </Box>
261
+ </View>
284
262
  );
285
263
  }
286
264
  };