ferns-ui 1.3.0 → 1.5.0

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.
@@ -333,3 +333,38 @@ export function getIsoDate(date: string | undefined): string | undefined {
333
333
  }
334
334
  return convertNullToUndefined(DateTime.fromISO(date).toUTC().toISO());
335
335
  }
336
+
337
+ const usTimezoneOptions = [
338
+ {label: "Eastern", value: "America/New_York"},
339
+ {label: "Central", value: "America/Chicago"},
340
+ {label: "Mountain", value: "America/Denver"},
341
+ {label: "Pacific", value: "America/Los_Angeles"},
342
+ {label: "Alaska", value: "America/Anchorage"},
343
+ {label: "Hawaii", value: "Pacific/Honolulu"},
344
+ {label: "Arizona", value: "America/Phoenix"},
345
+ ];
346
+
347
+ export function getTimezoneOptions(location: "USA" | "Worldwide", shortTimezone = false) {
348
+ let timezones: [string, string][];
349
+ if (location === "USA") {
350
+ timezones = usTimezoneOptions.map((tz) => [tz.label, tz.value]);
351
+ } else {
352
+ timezones = (Intl as any).supportedValuesOf("timeZone").map((tz: any) => {
353
+ return [tz, tz];
354
+ });
355
+ }
356
+ return timezones.map(([name, tz]) => {
357
+ const dateTime = DateTime.now().setZone(tz);
358
+ let tzAbbr = dateTime.toFormat("ZZZZ"); // Gets timezone abbreviation like "EST", "CST", etc.
359
+
360
+ // Special case for Arizona which returns MST during standard time
361
+ if (tz === "America/Phoenix") {
362
+ tzAbbr = "AZ";
363
+ }
364
+
365
+ return {
366
+ label: shortTimezone ? tzAbbr : name,
367
+ value: tz,
368
+ };
369
+ });
370
+ }
package/src/Field.tsx CHANGED
@@ -50,7 +50,7 @@ export const Field: FC<FieldProps> = ({type, ...rest}) => {
50
50
  } else if (type && ["date", "time", "datetime"].includes(type)) {
51
51
  return (
52
52
  <DateTimeField
53
- {...(rest as DateTimeFieldProps)}
53
+ {...(rest as DateTimeFieldProps as any)}
54
54
  type={type as "date" | "time" | "datetime"}
55
55
  />
56
56
  );
@@ -1,58 +1,124 @@
1
- import React from "react";
1
+ import React, {useCallback, useState} from "react";
2
2
  import {Pressable, View} from "react-native";
3
3
 
4
+ import {Badge} from "./Badge";
4
5
  import {SegmentedControlProps} from "./Common";
5
6
  import {Heading} from "./Heading";
7
+ import {Icon} from "./Icon";
6
8
  import {useTheme} from "./Theme";
7
9
 
8
- export const SegmentedControl = ({
10
+ export const SegmentedControl: React.FC<SegmentedControlProps> = ({
9
11
  items,
10
12
  onChange = () => {},
11
13
  size = "md",
12
14
  selectedIndex,
13
- }: SegmentedControlProps) => {
15
+ maxItems,
16
+ badges = [],
17
+ }) => {
14
18
  const height = size === "md" ? 36 : 44;
15
19
  const {theme} = useTheme();
20
+ const [startIndex, setStartIndex] = useState(0);
21
+
22
+ const handlePrevious = useCallback(() => {
23
+ setStartIndex((prev) => Math.max(0, prev - (maxItems ?? 4)));
24
+ }, []);
25
+
26
+ const handleNext = useCallback(() => {
27
+ setStartIndex((prev) => Math.min(items.length - (maxItems ?? items.length), prev + (maxItems ?? 4)));
28
+ }, [items.length, maxItems]);
29
+
30
+ const visibleItems = maxItems ? items.slice(startIndex, startIndex + maxItems) : items;
31
+ const visibleBadges = maxItems ? badges.slice(startIndex, startIndex + maxItems) : badges;
32
+ const canScrollLeft = startIndex > 0;
33
+ const canScrollRight = maxItems ? startIndex + maxItems < items.length : false;
34
+ const shouldShowScrollButtons = maxItems ? maxItems < items.length : false;
35
+
16
36
  return (
17
37
  <View
18
38
  style={{
19
39
  display: "flex",
20
- flexGrow: 1,
21
40
  flexDirection: "row",
22
- flexShrink: 1,
23
41
  alignItems: "center",
24
- gap: 4,
25
- height,
26
- maxHeight: height,
27
- borderRadius: theme.primitives.radius3xl,
28
- borderColor: theme.primitives.neutral300,
29
- borderWidth: 3,
30
- backgroundColor: theme.primitives.neutral300,
42
+ gap: 8,
31
43
  }}
32
44
  >
33
- {items.map((item, index) => (
34
- <Pressable
35
- key={index}
36
- aria-role="button"
45
+ {Boolean(shouldShowScrollButtons) && (
46
+ <Pressable disabled={!canScrollLeft} onPress={handlePrevious}>
47
+ <Icon
48
+ color={canScrollLeft ? "linkLight" : "extraLight"}
49
+ iconName="chevron-left"
50
+ size="lg"
51
+ />
52
+ </Pressable>
53
+ )}
54
+ <View
55
+ style={{
56
+ display: "flex",
57
+ flexGrow: 1,
58
+ flexDirection: "row",
59
+ flexShrink: 1,
60
+ alignItems: "center",
61
+ height,
62
+ maxHeight: height,
63
+ backgroundColor: theme.primitives.neutral300,
64
+ overflow: "hidden",
65
+ borderRadius: theme.primitives.radius3xl,
66
+ }}
67
+ >
68
+ <View
37
69
  style={{
38
70
  display: "flex",
39
- paddingHorizontal: size === "md" ? theme.spacing.sm : theme.spacing.md,
40
- justifyContent: "center",
41
- alignItems: "center",
42
- height: "100%",
43
- flexBasis: 0,
44
- gap: 12,
71
+ flexDirection: "row",
72
+ gap: 4,
45
73
  flexGrow: 1,
46
- flexShrink: 0,
47
- borderRadius: theme.primitives.radius3xl,
48
- backgroundColor: index === selectedIndex ? theme.surface.base : undefined,
49
- overflow: "hidden",
74
+ paddingHorizontal: 4,
75
+ height: height - 4,
50
76
  }}
51
- onPress={() => onChange(index)}
52
77
  >
53
- <Heading size="sm">{item}</Heading>
78
+ {visibleItems.map((item, index) => {
79
+ const actualIndex = startIndex + index;
80
+ return (
81
+ <Pressable
82
+ key={actualIndex}
83
+ aria-role="button"
84
+ style={{
85
+ display: "flex",
86
+ paddingHorizontal: 2,
87
+ justifyContent: "center",
88
+ alignItems: "center",
89
+ height: "100%",
90
+ flexDirection: "row",
91
+ gap: 8,
92
+ flexGrow: 1,
93
+ flexBasis: 0,
94
+ borderRadius: theme.primitives.radius3xl,
95
+ backgroundColor: actualIndex === selectedIndex ? theme.surface.base : undefined,
96
+ overflow: "hidden",
97
+ }}
98
+ onPress={() => onChange(actualIndex)}
99
+ >
100
+ <Heading size="sm">{item}</Heading>
101
+ {visibleBadges[index] && (
102
+ <Badge
103
+ status={visibleBadges[index].status ?? "info"}
104
+ value={visibleBadges[index].count}
105
+ variant="numberOnly"
106
+ />
107
+ )}
108
+ </Pressable>
109
+ );
110
+ })}
111
+ </View>
112
+ </View>
113
+ {Boolean(shouldShowScrollButtons) && (
114
+ <Pressable disabled={!canScrollRight} onPress={handleNext}>
115
+ <Icon
116
+ color={canScrollRight ? "linkLight" : "extraLight"}
117
+ iconName="chevron-right"
118
+ size="lg"
119
+ />
54
120
  </Pressable>
55
- ))}
121
+ )}
56
122
  </View>
57
123
  );
58
124
  };
@@ -1,37 +1,46 @@
1
1
  import React from "react";
2
2
 
3
- import {Box} from "./Box";
4
- import {TimezonePickerProps} from "./Common";
3
+ import {SelectFieldPropsBase} from "./Common";
4
+ import {getTimezoneOptions} from "./DateUtilities";
5
5
  import {SelectField} from "./SelectField";
6
6
 
7
- // TODO: Support world wide timezones
8
- const options = [
9
- {label: "Eastern", value: "America/New_York"},
10
- {label: "Central", value: "America/Chicago"},
11
- {label: "Mountain", value: "America/Denver"},
12
- {label: "Pacific", value: "America/Los_Angeles"},
13
- {label: "Alaska", value: "America/Anchorage"},
14
- {label: "Hawaii", value: "Pacific/Honolulu"},
15
- {label: "Arizona", value: "America/Phoenix"},
16
- ];
7
+ interface TimezonePickerProps extends Omit<SelectFieldPropsBase, "options"> {
8
+ timezone?: string;
9
+ onChange: (value: string) => void;
10
+ location?: "USA" | "Worldwide";
11
+ hideTitle?: boolean;
12
+ shortTimezone?: boolean;
13
+ }
17
14
 
18
- export const TimezonePicker = ({
15
+ export const TimezonePicker: React.FC<TimezonePickerProps> = ({
19
16
  timezone,
20
17
  onChange,
21
- showLabel,
22
- width = 100,
18
+ location = "USA",
19
+ hideTitle = false,
20
+ shortTimezone = false,
21
+ ...fieldProps
23
22
  }: TimezonePickerProps): React.ReactElement => {
24
- if (showLabel) {
25
- return (
26
- <Box maxWidth={width}>
27
- <SelectField options={options} title="Timezone" value={timezone} onChange={onChange} />
28
- </Box>
29
- );
30
- } else {
31
- return (
32
- <Box maxWidth={width}>
33
- <SelectField options={options} value={timezone} onChange={onChange} />
34
- </Box>
35
- );
23
+ // eslint-disable-next-line react/display-name
24
+ const tzOptions = React.useMemo(
25
+ () => getTimezoneOptions(location, shortTimezone),
26
+ [location, shortTimezone]
27
+ );
28
+
29
+ // Check that value is in the list of options
30
+ const valueIndex = tzOptions.findIndex((t) => t.value === timezone);
31
+ if (valueIndex === -1) {
32
+ console.warn(`${timezone} is not a valid timezone`);
36
33
  }
34
+
35
+ const title = hideTitle ? undefined : "Timezone";
36
+
37
+ return (
38
+ <SelectField
39
+ title={title}
40
+ {...fieldProps}
41
+ options={tzOptions}
42
+ value={timezone}
43
+ onChange={onChange}
44
+ />
45
+ );
37
46
  };