@atlure/ui 0.4.0 → 0.6.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.
- package/README.md +39 -6
- package/package.json +5 -6
- package/src/components/alert-dialog/alert-dialog.tsx +59 -0
- package/src/components/alert-dialog/utils.ts +5 -0
- package/src/components/avatar/avatar-group.tsx +52 -0
- package/src/components/avatar/avatar.tsx +54 -11
- package/src/components/badge/urgency-badge.tsx +21 -0
- package/src/components/calendar/calendar.tsx +189 -0
- package/src/components/checkbox/checkbox.tsx +26 -9
- package/src/components/chip/chip.tsx +66 -0
- package/src/components/date-range-picker/date-range-picker.tsx +104 -0
- package/src/components/dialog/dialog.tsx +137 -0
- package/src/components/format/date-label.tsx +41 -0
- package/src/components/format/distance-label.tsx +13 -0
- package/src/components/format/duration-label.tsx +13 -0
- package/src/components/format/money-label.tsx +20 -0
- package/src/components/picker/picker.tsx +104 -0
- package/src/components/progress/progress.tsx +95 -0
- package/src/components/progress/utils.ts +10 -0
- package/src/components/radio-group/radio-group-context.tsx +27 -0
- package/src/components/radio-group/radio-group.tsx +150 -0
- package/src/components/screen-header/screen-header.tsx +62 -0
- package/src/components/segmented-control/segmented-control.tsx +67 -0
- package/src/components/select/select.tsx +134 -0
- package/src/components/settings-row/settings-row.tsx +52 -0
- package/src/components/sheet/sheet.tsx +139 -0
- package/src/components/sheet/utils.ts +55 -0
- package/src/components/slider/format-label.ts +3 -0
- package/src/components/slider/hooks/use-slider-drag.ts +47 -0
- package/src/components/slider/range-slider.tsx +203 -0
- package/src/components/slider/slider.tsx +148 -0
- package/src/components/slider/utils.ts +57 -0
- package/src/components/star-rating/star-rating.tsx +98 -0
- package/src/components/switch/switch.tsx +34 -6
- package/src/components/tabs/tabs-context.tsx +32 -0
- package/src/components/tabs/tabs.tsx +239 -0
- package/src/components/time-picker/time-picker.tsx +131 -0
- package/src/components/toast/toast-context.tsx +26 -0
- package/src/components/toast/toast-provider.tsx +70 -0
- package/src/components/toast/toast.tsx +41 -0
- package/src/components/toast/utils.ts +2 -0
- package/src/index.ts +54 -0
- package/src/lib/calendar.ts +204 -0
- package/src/lib/format/date.ts +46 -0
- package/src/lib/format/distance.ts +36 -0
- package/src/lib/format/duration.ts +28 -0
- package/src/lib/format/money.ts +20 -0
- package/src/lib/locale.tsx +36 -0
- package/src/lib/portal.tsx +54 -0
- package/src/lib/touch-target.ts +5 -1
- package/src/lib/use-sheet.ts +18 -0
- package/src/lib/use-toast.ts +17 -0
- package/src/variants/avatar-variants.ts +68 -15
- package/src/variants/badge-variants.ts +8 -0
- package/src/variants/calendar-variants.ts +26 -0
- package/src/variants/checkbox-variants.ts +5 -14
- package/src/variants/chip-variants.ts +38 -0
- package/src/variants/dialog-variants.ts +11 -0
- package/src/variants/index.ts +9 -0
- package/src/variants/overlay-variants.ts +1 -0
- package/src/variants/progress-variants.ts +18 -0
- package/src/variants/radio-group-variants.ts +48 -0
- package/src/variants/screen-header-variants.ts +33 -0
- package/src/variants/segmented-control-variants.ts +41 -0
- package/src/variants/select-variants.ts +48 -0
- package/src/variants/settings-row-variants.ts +18 -0
- package/src/variants/sheet-variants.ts +10 -0
- package/src/variants/slider-variants.ts +8 -0
- package/src/variants/star-rating-variants.ts +28 -0
- package/src/variants/switch-variants.ts +26 -4
- package/src/variants/tabs-variants.ts +46 -0
- package/src/variants/toast-variants.ts +31 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { daysBetween } from "../../lib/calendar";
|
|
5
|
+
import { cn } from "../../lib/cn";
|
|
6
|
+
import { formatDateRange, type DateStyle } from "../../lib/format/date";
|
|
7
|
+
import { useLocale } from "../../lib/locale";
|
|
8
|
+
import {
|
|
9
|
+
dateRangePickerContainerClassName,
|
|
10
|
+
dateRangePickerSummaryClassName,
|
|
11
|
+
} from "../../variants/calendar-variants";
|
|
12
|
+
import { Calendar } from "../calendar/calendar";
|
|
13
|
+
import { Text } from "../text/text";
|
|
14
|
+
|
|
15
|
+
export interface DateRange {
|
|
16
|
+
start?: string;
|
|
17
|
+
end?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DateRangePickerProps {
|
|
21
|
+
yearMonth: string;
|
|
22
|
+
onYearMonthChange?: (yearMonth: string) => void;
|
|
23
|
+
range?: DateRange;
|
|
24
|
+
onRangeChange: (range: { start: string; end: string }) => void;
|
|
25
|
+
minNights?: number;
|
|
26
|
+
maxNights?: number;
|
|
27
|
+
disabledDates?: (iso: string) => boolean;
|
|
28
|
+
minDate?: string;
|
|
29
|
+
maxDate?: string;
|
|
30
|
+
markers?: readonly string[];
|
|
31
|
+
accessibilityLabel?: string;
|
|
32
|
+
summaryAccessibilityLabel?: string;
|
|
33
|
+
summaryDateStyle?: DateStyle;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function DateRangePicker({
|
|
38
|
+
yearMonth,
|
|
39
|
+
onYearMonthChange,
|
|
40
|
+
range,
|
|
41
|
+
onRangeChange,
|
|
42
|
+
minNights,
|
|
43
|
+
maxNights,
|
|
44
|
+
disabledDates,
|
|
45
|
+
minDate,
|
|
46
|
+
maxDate,
|
|
47
|
+
markers,
|
|
48
|
+
accessibilityLabel,
|
|
49
|
+
summaryAccessibilityLabel = "Selected date range",
|
|
50
|
+
summaryDateStyle = "medium",
|
|
51
|
+
className,
|
|
52
|
+
}: DateRangePickerProps) {
|
|
53
|
+
const { locale } = useLocale();
|
|
54
|
+
const [inProgressStart, setInProgressStart] = useState<string | undefined>(undefined);
|
|
55
|
+
|
|
56
|
+
const handleSelect = (iso: string) => {
|
|
57
|
+
if (inProgressStart === undefined) {
|
|
58
|
+
setInProgressStart(iso);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setInProgressStart(undefined);
|
|
63
|
+
|
|
64
|
+
const [start, end] =
|
|
65
|
+
inProgressStart < iso ? [inProgressStart, iso] : [iso, inProgressStart];
|
|
66
|
+
const nights = daysBetween(start, end);
|
|
67
|
+
|
|
68
|
+
if (nights <= 0) return;
|
|
69
|
+
if (minNights !== undefined && nights < minNights) return;
|
|
70
|
+
if (maxNights !== undefined && nights > maxNights) return;
|
|
71
|
+
|
|
72
|
+
onRangeChange({ start, end });
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<View
|
|
77
|
+
accessibilityLabel={accessibilityLabel}
|
|
78
|
+
className={cn(dateRangePickerContainerClassName, className)}
|
|
79
|
+
>
|
|
80
|
+
<Calendar
|
|
81
|
+
yearMonth={yearMonth}
|
|
82
|
+
onMonthChange={onYearMonthChange}
|
|
83
|
+
onSelect={handleSelect}
|
|
84
|
+
disabledDates={disabledDates}
|
|
85
|
+
minDate={minDate}
|
|
86
|
+
maxDate={maxDate}
|
|
87
|
+
markers={markers}
|
|
88
|
+
highlight={{
|
|
89
|
+
start: range?.start,
|
|
90
|
+
end: range?.end,
|
|
91
|
+
inProgressStart,
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
{range?.start !== undefined && range.end !== undefined && (
|
|
95
|
+
<Text
|
|
96
|
+
accessibilityLabel={summaryAccessibilityLabel}
|
|
97
|
+
className={dateRangePickerSummaryClassName}
|
|
98
|
+
>
|
|
99
|
+
{formatDateRange(range.start, range.end, locale, summaryDateStyle)}
|
|
100
|
+
</Text>
|
|
101
|
+
)}
|
|
102
|
+
</View>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { type ReactNode, useEffect, useId } from "react";
|
|
2
|
+
import { BackHandler, Modal, Platform, Pressable, View } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import { Portal, usePortalRegistry } from "../../lib/portal";
|
|
6
|
+
import {
|
|
7
|
+
dialogContentClassName,
|
|
8
|
+
dialogDescriptionClassName,
|
|
9
|
+
dialogFooterClassName,
|
|
10
|
+
dialogHeaderClassName,
|
|
11
|
+
dialogOverlayClassName,
|
|
12
|
+
dialogTitleClassName,
|
|
13
|
+
} from "../../variants/dialog-variants";
|
|
14
|
+
import { overlayBackdropClassName } from "../../variants/overlay-variants";
|
|
15
|
+
import { Text } from "../text/text";
|
|
16
|
+
|
|
17
|
+
export interface DialogProps {
|
|
18
|
+
isOpen: boolean;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
backdropAccessibilityLabel?: string;
|
|
21
|
+
accessibilityLabel?: string;
|
|
22
|
+
isDismissible?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Dialog({
|
|
28
|
+
isOpen,
|
|
29
|
+
onClose,
|
|
30
|
+
backdropAccessibilityLabel,
|
|
31
|
+
accessibilityLabel,
|
|
32
|
+
isDismissible = true,
|
|
33
|
+
className,
|
|
34
|
+
children,
|
|
35
|
+
}: DialogProps) {
|
|
36
|
+
const { activeOverlayId, requestSlot, releaseSlot } = usePortalRegistry();
|
|
37
|
+
const overlayId = useId();
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!isOpen) return;
|
|
41
|
+
|
|
42
|
+
requestSlot(overlayId);
|
|
43
|
+
return () => releaseSlot(overlayId);
|
|
44
|
+
}, [isOpen, overlayId, requestSlot, releaseSlot]);
|
|
45
|
+
|
|
46
|
+
const isVisible = isOpen && activeOverlayId === overlayId;
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!isVisible || !isDismissible || Platform.OS !== "android") return;
|
|
50
|
+
|
|
51
|
+
const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
|
|
52
|
+
onClose();
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return () => subscription.remove();
|
|
57
|
+
}, [isVisible, isDismissible, onClose]);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Portal>
|
|
61
|
+
<Modal
|
|
62
|
+
visible={isVisible}
|
|
63
|
+
transparent
|
|
64
|
+
animationType="none"
|
|
65
|
+
onRequestClose={isDismissible ? onClose : undefined}
|
|
66
|
+
>
|
|
67
|
+
<View className={dialogOverlayClassName}>
|
|
68
|
+
{isDismissible ? (
|
|
69
|
+
<Pressable
|
|
70
|
+
accessibilityRole="button"
|
|
71
|
+
accessibilityLabel={backdropAccessibilityLabel}
|
|
72
|
+
className={overlayBackdropClassName}
|
|
73
|
+
onPress={onClose}
|
|
74
|
+
/>
|
|
75
|
+
) : (
|
|
76
|
+
<View className={overlayBackdropClassName} />
|
|
77
|
+
)}
|
|
78
|
+
<View
|
|
79
|
+
accessibilityViewIsModal
|
|
80
|
+
accessibilityLabel={accessibilityLabel}
|
|
81
|
+
className={cn(dialogContentClassName, className)}
|
|
82
|
+
>
|
|
83
|
+
{children}
|
|
84
|
+
</View>
|
|
85
|
+
</View>
|
|
86
|
+
</Modal>
|
|
87
|
+
</Portal>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function DialogHeader({
|
|
92
|
+
className,
|
|
93
|
+
children,
|
|
94
|
+
}: {
|
|
95
|
+
className?: string;
|
|
96
|
+
children: ReactNode;
|
|
97
|
+
}) {
|
|
98
|
+
return <View className={cn(dialogHeaderClassName, className)}>{children}</View>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function DialogTitle({ className, children }: { className?: string; children: string }) {
|
|
102
|
+
return (
|
|
103
|
+
<Text role="heading" aria-level={2} className={cn(dialogTitleClassName, className)}>
|
|
104
|
+
{children}
|
|
105
|
+
</Text>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function DialogDescription({
|
|
110
|
+
className,
|
|
111
|
+
children,
|
|
112
|
+
}: {
|
|
113
|
+
className?: string;
|
|
114
|
+
children: string;
|
|
115
|
+
}) {
|
|
116
|
+
return <Text className={cn(dialogDescriptionClassName, className)}>{children}</Text>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function DialogContent({
|
|
120
|
+
className,
|
|
121
|
+
children,
|
|
122
|
+
}: {
|
|
123
|
+
className?: string;
|
|
124
|
+
children: ReactNode;
|
|
125
|
+
}) {
|
|
126
|
+
return <View className={className}>{children}</View>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function DialogFooter({
|
|
130
|
+
className,
|
|
131
|
+
children,
|
|
132
|
+
}: {
|
|
133
|
+
className?: string;
|
|
134
|
+
children: ReactNode;
|
|
135
|
+
}) {
|
|
136
|
+
return <View className={cn(dialogFooterClassName, className)}>{children}</View>;
|
|
137
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatDate,
|
|
3
|
+
formatDateRange,
|
|
4
|
+
formatRelativeDate,
|
|
5
|
+
type DateStyle,
|
|
6
|
+
} from "../../lib/format/date";
|
|
7
|
+
import { useLocale } from "../../lib/locale";
|
|
8
|
+
import { Text, type TextProps } from "../text/text";
|
|
9
|
+
|
|
10
|
+
export interface DateLabelProps extends Omit<TextProps, "children"> {
|
|
11
|
+
value: string;
|
|
12
|
+
dateStyle?: DateStyle;
|
|
13
|
+
relative?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function DateLabel({ value, dateStyle = "medium", relative, ...textProps }: DateLabelProps) {
|
|
17
|
+
const { locale } = useLocale();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Text {...textProps}>
|
|
21
|
+
{relative === true ? formatRelativeDate(value, locale) : formatDate(value, locale, dateStyle)}
|
|
22
|
+
</Text>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DateRangeLabelProps extends Omit<TextProps, "children"> {
|
|
27
|
+
start: string;
|
|
28
|
+
end: string;
|
|
29
|
+
dateStyle?: DateStyle;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function DateRangeLabel({
|
|
33
|
+
start,
|
|
34
|
+
end,
|
|
35
|
+
dateStyle = "medium",
|
|
36
|
+
...textProps
|
|
37
|
+
}: DateRangeLabelProps) {
|
|
38
|
+
const { locale } = useLocale();
|
|
39
|
+
|
|
40
|
+
return <Text {...textProps}>{formatDateRange(start, end, locale, dateStyle)}</Text>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { formatDistance } from "../../lib/format/distance";
|
|
2
|
+
import { useLocale } from "../../lib/locale";
|
|
3
|
+
import { Text, type TextProps } from "../text/text";
|
|
4
|
+
|
|
5
|
+
export interface DistanceLabelProps extends Omit<TextProps, "children"> {
|
|
6
|
+
meters: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function DistanceLabel({ meters, ...textProps }: DistanceLabelProps) {
|
|
10
|
+
const { locale, measurementSystem } = useLocale();
|
|
11
|
+
|
|
12
|
+
return <Text {...textProps}>{formatDistance(meters, locale, measurementSystem)}</Text>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { formatDuration } from "../../lib/format/duration";
|
|
2
|
+
import { useLocale } from "../../lib/locale";
|
|
3
|
+
import { Text, type TextProps } from "../text/text";
|
|
4
|
+
|
|
5
|
+
export interface DurationLabelProps extends Omit<TextProps, "children"> {
|
|
6
|
+
minutes: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function DurationLabel({ minutes, ...textProps }: DurationLabelProps) {
|
|
10
|
+
const { locale } = useLocale();
|
|
11
|
+
|
|
12
|
+
return <Text {...textProps}>{formatDuration(minutes, locale)}</Text>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Money } from "@atlure/types";
|
|
2
|
+
|
|
3
|
+
import { formatMoney, formatMoneyRate, type RateUnit } from "../../lib/format/money";
|
|
4
|
+
import { useLocale } from "../../lib/locale";
|
|
5
|
+
import { Text, type TextProps } from "../text/text";
|
|
6
|
+
|
|
7
|
+
export interface MoneyLabelProps extends Omit<TextProps, "children"> {
|
|
8
|
+
value: Money;
|
|
9
|
+
per?: RateUnit;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function MoneyLabel({ value, per, ...textProps }: MoneyLabelProps) {
|
|
13
|
+
const { locale } = useLocale();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Text {...textProps}>
|
|
17
|
+
{per === undefined ? formatMoney(value, locale) : formatMoneyRate(value, locale, per)}
|
|
18
|
+
</Text>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Pressable, ScrollView, View } from "react-native";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { touchTargetHitSlop } from "../../lib/touch-target";
|
|
5
|
+
import { useSheet } from "../../lib/use-sheet";
|
|
6
|
+
import { inputVariants, type InputVariantProps } from "../../variants/input-variants";
|
|
7
|
+
import {
|
|
8
|
+
selectOptionListClassName,
|
|
9
|
+
selectTriggerClassName,
|
|
10
|
+
selectValueVariants,
|
|
11
|
+
} from "../../variants/select-variants";
|
|
12
|
+
import { SelectItem, type SelectOption, type SelectValue } from "../select/select";
|
|
13
|
+
import { Sheet } from "../sheet/sheet";
|
|
14
|
+
import { Text } from "../text/text";
|
|
15
|
+
|
|
16
|
+
export interface PickerProps<TValue extends SelectValue> {
|
|
17
|
+
options: readonly SelectOption<TValue>[];
|
|
18
|
+
values: readonly NoInfer<TValue>[];
|
|
19
|
+
onValuesChange: (values: NoInfer<TValue>[]) => void;
|
|
20
|
+
placeholder: string;
|
|
21
|
+
accessibilityLabel: string;
|
|
22
|
+
backdropAccessibilityLabel: string;
|
|
23
|
+
size?: NonNullable<InputVariantProps["size"]>;
|
|
24
|
+
isDisabled?: boolean;
|
|
25
|
+
isInvalid?: boolean;
|
|
26
|
+
snapPoints?: readonly number[];
|
|
27
|
+
bottomInset?: number;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function Picker<TValue extends SelectValue>({
|
|
32
|
+
options,
|
|
33
|
+
values,
|
|
34
|
+
onValuesChange,
|
|
35
|
+
placeholder,
|
|
36
|
+
accessibilityLabel,
|
|
37
|
+
backdropAccessibilityLabel,
|
|
38
|
+
size = "md",
|
|
39
|
+
isDisabled = false,
|
|
40
|
+
isInvalid = false,
|
|
41
|
+
snapPoints,
|
|
42
|
+
bottomInset,
|
|
43
|
+
className,
|
|
44
|
+
}: PickerProps<TValue>) {
|
|
45
|
+
const sheet = useSheet();
|
|
46
|
+
const selectedLabels = options
|
|
47
|
+
.filter((option) => values.includes(option.value))
|
|
48
|
+
.map((option) => option.label);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<Pressable
|
|
53
|
+
accessibilityRole="combobox"
|
|
54
|
+
accessibilityLabel={accessibilityLabel}
|
|
55
|
+
accessibilityState={{ disabled: isDisabled, expanded: sheet.isOpen }}
|
|
56
|
+
accessibilityValue={{ text: selectedLabels.join(", ") || placeholder }}
|
|
57
|
+
aria-disabled={isDisabled}
|
|
58
|
+
aria-expanded={sheet.isOpen}
|
|
59
|
+
aria-invalid={isInvalid}
|
|
60
|
+
aria-multiselectable
|
|
61
|
+
disabled={isDisabled}
|
|
62
|
+
hitSlop={touchTargetHitSlop(size)}
|
|
63
|
+
onPress={sheet.open}
|
|
64
|
+
className={cn(
|
|
65
|
+
inputVariants({ size, isInvalid, isDisabled }),
|
|
66
|
+
selectTriggerClassName,
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
<Text className={selectValueVariants({ isPlaceholder: selectedLabels.length === 0 })}>
|
|
71
|
+
{selectedLabels.join(", ") || placeholder}
|
|
72
|
+
</Text>
|
|
73
|
+
</Pressable>
|
|
74
|
+
<Sheet
|
|
75
|
+
isOpen={sheet.isOpen}
|
|
76
|
+
onClose={sheet.close}
|
|
77
|
+
accessibilityLabel={accessibilityLabel}
|
|
78
|
+
backdropAccessibilityLabel={backdropAccessibilityLabel}
|
|
79
|
+
snapPoints={snapPoints}
|
|
80
|
+
bottomInset={bottomInset}
|
|
81
|
+
>
|
|
82
|
+
<ScrollView className={selectOptionListClassName}>
|
|
83
|
+
<View accessibilityRole="menu">
|
|
84
|
+
{options.map((option) => (
|
|
85
|
+
<SelectItem
|
|
86
|
+
key={option.value}
|
|
87
|
+
label={option.label}
|
|
88
|
+
isSelected={values.includes(option.value)}
|
|
89
|
+
isDisabled={option.isDisabled}
|
|
90
|
+
onPress={() =>
|
|
91
|
+
onValuesChange(
|
|
92
|
+
values.includes(option.value)
|
|
93
|
+
? values.filter((selected) => selected !== option.value)
|
|
94
|
+
: [...values, option.value],
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
/>
|
|
98
|
+
))}
|
|
99
|
+
</View>
|
|
100
|
+
</ScrollView>
|
|
101
|
+
</Sheet>
|
|
102
|
+
</>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { Animated, Easing, View, type ViewProps } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import {
|
|
6
|
+
progressFillClassName,
|
|
7
|
+
progressTrackVariants,
|
|
8
|
+
type ProgressVariantProps,
|
|
9
|
+
} from "../../variants/progress-variants";
|
|
10
|
+
import { useReducedMotion } from "../skeleton/hooks/use-reduced-motion";
|
|
11
|
+
import {
|
|
12
|
+
clampProgress,
|
|
13
|
+
PROGRESS_INDETERMINATE_DURATION,
|
|
14
|
+
PROGRESS_INDETERMINATE_WIDTH_RATIO,
|
|
15
|
+
PROGRESS_MAX,
|
|
16
|
+
PROGRESS_MIN,
|
|
17
|
+
} from "./utils";
|
|
18
|
+
|
|
19
|
+
export interface ProgressProps extends Omit<ViewProps, "children"> {
|
|
20
|
+
value?: number;
|
|
21
|
+
isIndeterminate?: boolean;
|
|
22
|
+
size?: NonNullable<ProgressVariantProps["size"]>;
|
|
23
|
+
accessibilityLabel: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Progress({
|
|
27
|
+
value = 0,
|
|
28
|
+
isIndeterminate = false,
|
|
29
|
+
size = "md",
|
|
30
|
+
accessibilityLabel,
|
|
31
|
+
className,
|
|
32
|
+
...viewProps
|
|
33
|
+
}: ProgressProps) {
|
|
34
|
+
const isReducedMotion = useReducedMotion();
|
|
35
|
+
const slide = useRef(new Animated.Value(0)).current;
|
|
36
|
+
const isAnimated = isIndeterminate && !isReducedMotion;
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!isAnimated) return;
|
|
40
|
+
|
|
41
|
+
slide.setValue(0);
|
|
42
|
+
const animation = Animated.loop(
|
|
43
|
+
Animated.timing(slide, {
|
|
44
|
+
toValue: 1,
|
|
45
|
+
duration: PROGRESS_INDETERMINATE_DURATION,
|
|
46
|
+
easing: Easing.linear,
|
|
47
|
+
useNativeDriver: true,
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
animation.start();
|
|
52
|
+
|
|
53
|
+
return () => animation.stop();
|
|
54
|
+
}, [isAnimated, slide]);
|
|
55
|
+
|
|
56
|
+
const clampedValue = clampProgress(value);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<View
|
|
60
|
+
accessibilityRole="progressbar"
|
|
61
|
+
accessibilityLabel={accessibilityLabel}
|
|
62
|
+
accessibilityValue={
|
|
63
|
+
isIndeterminate
|
|
64
|
+
? undefined
|
|
65
|
+
: { min: PROGRESS_MIN, max: PROGRESS_MAX, now: clampedValue }
|
|
66
|
+
}
|
|
67
|
+
accessibilityState={{ busy: isIndeterminate }}
|
|
68
|
+
aria-busy={isIndeterminate}
|
|
69
|
+
aria-valuemin={isIndeterminate ? undefined : PROGRESS_MIN}
|
|
70
|
+
aria-valuemax={isIndeterminate ? undefined : PROGRESS_MAX}
|
|
71
|
+
aria-valuenow={isIndeterminate ? undefined : clampedValue}
|
|
72
|
+
className={cn(progressTrackVariants({ size }), className)}
|
|
73
|
+
{...viewProps}
|
|
74
|
+
>
|
|
75
|
+
{isIndeterminate ? (
|
|
76
|
+
<Animated.View
|
|
77
|
+
className={progressFillClassName}
|
|
78
|
+
style={{
|
|
79
|
+
width: `${PROGRESS_INDETERMINATE_WIDTH_RATIO * PROGRESS_MAX}%`,
|
|
80
|
+
transform: [
|
|
81
|
+
{
|
|
82
|
+
translateX: slide.interpolate({
|
|
83
|
+
inputRange: [0, 1],
|
|
84
|
+
outputRange: ["0%", `${PROGRESS_MAX / PROGRESS_INDETERMINATE_WIDTH_RATIO}%`],
|
|
85
|
+
}),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
) : (
|
|
91
|
+
<View className={progressFillClassName} style={{ width: `${clampedValue}%` }} />
|
|
92
|
+
)}
|
|
93
|
+
</View>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const PROGRESS_MIN = 0;
|
|
2
|
+
export const PROGRESS_MAX = 100;
|
|
3
|
+
export const PROGRESS_INDETERMINATE_DURATION = 1200;
|
|
4
|
+
export const PROGRESS_INDETERMINATE_WIDTH_RATIO = 0.4;
|
|
5
|
+
|
|
6
|
+
export function clampProgress(value: number): number {
|
|
7
|
+
if (Number.isNaN(value)) return PROGRESS_MIN;
|
|
8
|
+
|
|
9
|
+
return Math.min(Math.max(value, PROGRESS_MIN), PROGRESS_MAX);
|
|
10
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
export interface RadioGroupControl {
|
|
4
|
+
selectedValue: string | undefined;
|
|
5
|
+
isDisabled: boolean;
|
|
6
|
+
select: (value: string) => void;
|
|
7
|
+
registerValue: (value: string) => () => void;
|
|
8
|
+
selectAdjacent: (fromValue: string, direction: 1 | -1) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const RadioGroupContext = createContext<RadioGroupControl | undefined>(undefined);
|
|
12
|
+
|
|
13
|
+
export function RadioGroupProvider({
|
|
14
|
+
control,
|
|
15
|
+
children,
|
|
16
|
+
}: {
|
|
17
|
+
control: RadioGroupControl;
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
return <RadioGroupContext.Provider value={control}>{children}</RadioGroupContext.Provider>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useRadioGroupControl(): RadioGroupControl | undefined {
|
|
24
|
+
return useContext(RadioGroupContext);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { RadioGroupContext };
|