@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,239 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Pressable,
|
|
5
|
+
type PressableProps,
|
|
6
|
+
ScrollView,
|
|
7
|
+
View,
|
|
8
|
+
type ViewProps,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
|
|
11
|
+
import { cn } from "../../lib/cn";
|
|
12
|
+
import {
|
|
13
|
+
tabsContentVariants,
|
|
14
|
+
tabsIndicatorClassName,
|
|
15
|
+
tabsIndicatorHeight,
|
|
16
|
+
tabsListClassName,
|
|
17
|
+
tabsTriggerLabelVariants,
|
|
18
|
+
tabsTriggerVariants,
|
|
19
|
+
} from "../../variants/tabs-variants";
|
|
20
|
+
import { Text } from "../text/text";
|
|
21
|
+
import { TabsProvider, useTabsControl, type TabsTriggerLayout } from "./tabs-context";
|
|
22
|
+
|
|
23
|
+
const indicatorTransitionDurationMs = 180;
|
|
24
|
+
|
|
25
|
+
export interface TabsProps extends Omit<ViewProps, "children"> {
|
|
26
|
+
value?: string;
|
|
27
|
+
defaultValue: string;
|
|
28
|
+
onValueChange?: (value: string) => void;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function Tabs({
|
|
33
|
+
value,
|
|
34
|
+
defaultValue,
|
|
35
|
+
onValueChange,
|
|
36
|
+
className,
|
|
37
|
+
children,
|
|
38
|
+
...viewProps
|
|
39
|
+
}: TabsProps) {
|
|
40
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
41
|
+
const activeValue = value ?? uncontrolledValue;
|
|
42
|
+
|
|
43
|
+
const [activatedValues, setActivatedValues] = useState<ReadonlySet<string>>(
|
|
44
|
+
() => new Set([activeValue]),
|
|
45
|
+
);
|
|
46
|
+
const [triggerLayouts, setTriggerLayouts] = useState<ReadonlyMap<string, TabsTriggerLayout>>(
|
|
47
|
+
() => new Map(),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
setActivatedValues((previous) =>
|
|
52
|
+
previous.has(activeValue) ? previous : new Set(previous).add(activeValue),
|
|
53
|
+
);
|
|
54
|
+
}, [activeValue]);
|
|
55
|
+
|
|
56
|
+
const select = useCallback(
|
|
57
|
+
(nextValue: string) => {
|
|
58
|
+
if (value === undefined) {
|
|
59
|
+
setUncontrolledValue(nextValue);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onValueChange?.(nextValue);
|
|
63
|
+
},
|
|
64
|
+
[value, onValueChange],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const reportTriggerLayout = useCallback((triggerValue: string, layout: TabsTriggerLayout) => {
|
|
68
|
+
setTriggerLayouts((previous) => {
|
|
69
|
+
const existing = previous.get(triggerValue);
|
|
70
|
+
|
|
71
|
+
if (existing?.x === layout.x && existing.width === layout.width) {
|
|
72
|
+
return previous;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return new Map(previous).set(triggerValue, layout);
|
|
76
|
+
});
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
const control = useMemo(
|
|
80
|
+
() => ({
|
|
81
|
+
activeValue,
|
|
82
|
+
hasBeenActivated: (candidate: string) => activatedValues.has(candidate),
|
|
83
|
+
select,
|
|
84
|
+
triggerLayouts,
|
|
85
|
+
reportTriggerLayout,
|
|
86
|
+
}),
|
|
87
|
+
[activeValue, activatedValues, select, triggerLayouts, reportTriggerLayout],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<View className={cn("flex-1 flex-col", className)} {...viewProps}>
|
|
92
|
+
<TabsProvider control={control}>{children}</TabsProvider>
|
|
93
|
+
</View>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface TabsListProps extends Omit<ViewProps, "children"> {
|
|
98
|
+
accessibilityLabel: string;
|
|
99
|
+
isScrollable?: boolean;
|
|
100
|
+
children: ReactNode;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function TabsList({
|
|
104
|
+
accessibilityLabel,
|
|
105
|
+
isScrollable = true,
|
|
106
|
+
className,
|
|
107
|
+
children,
|
|
108
|
+
...viewProps
|
|
109
|
+
}: TabsListProps) {
|
|
110
|
+
const control = useTabsControl();
|
|
111
|
+
const indicatorOffset = useRef(new Animated.Value(0)).current;
|
|
112
|
+
const indicatorWidth = useRef(new Animated.Value(0)).current;
|
|
113
|
+
|
|
114
|
+
const activeLayout = control?.triggerLayouts.get(control.activeValue);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (activeLayout === undefined) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const animation = Animated.parallel([
|
|
122
|
+
Animated.timing(indicatorOffset, {
|
|
123
|
+
toValue: activeLayout.x,
|
|
124
|
+
duration: indicatorTransitionDurationMs,
|
|
125
|
+
useNativeDriver: false,
|
|
126
|
+
}),
|
|
127
|
+
Animated.timing(indicatorWidth, {
|
|
128
|
+
toValue: activeLayout.width,
|
|
129
|
+
duration: indicatorTransitionDurationMs,
|
|
130
|
+
useNativeDriver: false,
|
|
131
|
+
}),
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
animation.start();
|
|
135
|
+
|
|
136
|
+
return () => animation.stop();
|
|
137
|
+
}, [activeLayout, indicatorOffset, indicatorWidth]);
|
|
138
|
+
|
|
139
|
+
const list = (
|
|
140
|
+
<View
|
|
141
|
+
accessibilityRole="tablist"
|
|
142
|
+
role="tablist"
|
|
143
|
+
accessibilityLabel={accessibilityLabel}
|
|
144
|
+
className={cn(tabsListClassName, className)}
|
|
145
|
+
{...viewProps}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
<Animated.View
|
|
149
|
+
style={{
|
|
150
|
+
position: "absolute",
|
|
151
|
+
bottom: 0,
|
|
152
|
+
height: tabsIndicatorHeight,
|
|
153
|
+
left: indicatorOffset,
|
|
154
|
+
width: indicatorWidth,
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<View className={tabsIndicatorClassName} />
|
|
158
|
+
</Animated.View>
|
|
159
|
+
</View>
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (!isScrollable) {
|
|
163
|
+
return list;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
168
|
+
{list}
|
|
169
|
+
</ScrollView>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface TabsTriggerProps
|
|
174
|
+
extends Omit<
|
|
175
|
+
PressableProps,
|
|
176
|
+
"children" | "disabled" | "onPress" | "accessibilityState" | "accessibilityRole"
|
|
177
|
+
> {
|
|
178
|
+
value: string;
|
|
179
|
+
label: string;
|
|
180
|
+
isDisabled?: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function TabsTrigger({
|
|
184
|
+
value,
|
|
185
|
+
label,
|
|
186
|
+
isDisabled = false,
|
|
187
|
+
accessibilityLabel,
|
|
188
|
+
className,
|
|
189
|
+
...pressableProps
|
|
190
|
+
}: TabsTriggerProps) {
|
|
191
|
+
const control = useTabsControl();
|
|
192
|
+
const isActive = control?.activeValue === value;
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<Pressable
|
|
196
|
+
accessibilityRole="tab"
|
|
197
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
198
|
+
accessibilityState={{ selected: isActive, disabled: isDisabled }}
|
|
199
|
+
aria-selected={isActive}
|
|
200
|
+
aria-disabled={isDisabled}
|
|
201
|
+
disabled={isDisabled}
|
|
202
|
+
onPress={() => control?.select(value)}
|
|
203
|
+
onLayout={(event) => {
|
|
204
|
+
const { x, width } = event.nativeEvent.layout;
|
|
205
|
+
control?.reportTriggerLayout(value, { x, width });
|
|
206
|
+
}}
|
|
207
|
+
className={cn(tabsTriggerVariants({ isDisabled }), className)}
|
|
208
|
+
{...pressableProps}
|
|
209
|
+
>
|
|
210
|
+
<Text className={tabsTriggerLabelVariants({ isActive })}>{label}</Text>
|
|
211
|
+
</Pressable>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface TabsContentProps extends Omit<ViewProps, "children"> {
|
|
216
|
+
value: string;
|
|
217
|
+
children: ReactNode;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function TabsContent({ value, className, children, ...viewProps }: TabsContentProps) {
|
|
221
|
+
const control = useTabsControl();
|
|
222
|
+
|
|
223
|
+
if (control?.hasBeenActivated(value) !== true) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const isActive = control.activeValue === value;
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<View
|
|
231
|
+
role="tabpanel"
|
|
232
|
+
aria-hidden={!isActive}
|
|
233
|
+
className={cn(tabsContentVariants({ isActive }), className)}
|
|
234
|
+
{...viewProps}
|
|
235
|
+
>
|
|
236
|
+
{children}
|
|
237
|
+
</View>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Pressable, ScrollView, View } from "react-native";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
formatHourLabel,
|
|
6
|
+
formatTimeValue,
|
|
7
|
+
isLocaleHour12,
|
|
8
|
+
padTwoDigits,
|
|
9
|
+
parseTimeValue,
|
|
10
|
+
} from "../../lib/calendar";
|
|
11
|
+
import { cn } from "../../lib/cn";
|
|
12
|
+
import { useLocale } from "../../lib/locale";
|
|
13
|
+
import {
|
|
14
|
+
timePickerBodyClassName,
|
|
15
|
+
timePickerColumnClassName,
|
|
16
|
+
timePickerOptionClassName,
|
|
17
|
+
timePickerOptionLabelClassName,
|
|
18
|
+
timePickerOptionSelectedClassName,
|
|
19
|
+
} from "../../variants/calendar-variants";
|
|
20
|
+
import { Sheet } from "../sheet/sheet";
|
|
21
|
+
import { Text } from "../text/text";
|
|
22
|
+
|
|
23
|
+
export interface TimePickerProps {
|
|
24
|
+
isOpen: boolean;
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
value?: string;
|
|
27
|
+
onChange: (value: string) => void;
|
|
28
|
+
minuteStep?: number;
|
|
29
|
+
hour12?: boolean;
|
|
30
|
+
bottomInset?: number;
|
|
31
|
+
accessibilityLabel: string;
|
|
32
|
+
backdropAccessibilityLabel: string;
|
|
33
|
+
hourColumnAccessibilityLabel?: string;
|
|
34
|
+
minuteColumnAccessibilityLabel?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const HOURS_IN_DAY = 24;
|
|
38
|
+
const MINUTES_IN_HOUR = 60;
|
|
39
|
+
|
|
40
|
+
export function TimePicker({
|
|
41
|
+
isOpen,
|
|
42
|
+
onClose,
|
|
43
|
+
value,
|
|
44
|
+
onChange,
|
|
45
|
+
minuteStep = 15,
|
|
46
|
+
hour12,
|
|
47
|
+
bottomInset,
|
|
48
|
+
accessibilityLabel,
|
|
49
|
+
backdropAccessibilityLabel,
|
|
50
|
+
hourColumnAccessibilityLabel = "Hour",
|
|
51
|
+
minuteColumnAccessibilityLabel = "Minute",
|
|
52
|
+
}: TimePickerProps) {
|
|
53
|
+
const { locale } = useLocale();
|
|
54
|
+
const useHour12 = hour12 ?? isLocaleHour12(locale);
|
|
55
|
+
|
|
56
|
+
const parsed = value !== undefined ? parseTimeValue(value) : undefined;
|
|
57
|
+
const selectedHour = parsed?.hour;
|
|
58
|
+
const selectedMinute = parsed?.minute;
|
|
59
|
+
|
|
60
|
+
const hours = useMemo(() => Array.from({ length: HOURS_IN_DAY }, (_, i) => i), []);
|
|
61
|
+
const minutes = useMemo(
|
|
62
|
+
() =>
|
|
63
|
+
Array.from({ length: Math.ceil(MINUTES_IN_HOUR / minuteStep) }, (_, i) => i * minuteStep),
|
|
64
|
+
[minuteStep],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const emit = (hour: number, minute: number) => onChange(formatTimeValue(hour, minute));
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Sheet
|
|
71
|
+
isOpen={isOpen}
|
|
72
|
+
onClose={onClose}
|
|
73
|
+
accessibilityLabel={accessibilityLabel}
|
|
74
|
+
backdropAccessibilityLabel={backdropAccessibilityLabel}
|
|
75
|
+
bottomInset={bottomInset}
|
|
76
|
+
>
|
|
77
|
+
<View className={timePickerBodyClassName}>
|
|
78
|
+
<ScrollView
|
|
79
|
+
accessibilityLabel={hourColumnAccessibilityLabel}
|
|
80
|
+
className={timePickerColumnClassName}
|
|
81
|
+
>
|
|
82
|
+
{hours.map((hour) => {
|
|
83
|
+
const isActive = hour === selectedHour;
|
|
84
|
+
return (
|
|
85
|
+
<Pressable
|
|
86
|
+
key={hour}
|
|
87
|
+
accessibilityRole="button"
|
|
88
|
+
accessibilityLabel={formatHourLabel(hour, useHour12, locale)}
|
|
89
|
+
accessibilityState={{ selected: isActive }}
|
|
90
|
+
aria-selected={isActive}
|
|
91
|
+
onPress={() => emit(hour, selectedMinute ?? 0)}
|
|
92
|
+
className={cn(
|
|
93
|
+
timePickerOptionClassName,
|
|
94
|
+
isActive && timePickerOptionSelectedClassName,
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
<Text className={timePickerOptionLabelClassName}>
|
|
98
|
+
{formatHourLabel(hour, useHour12, locale)}
|
|
99
|
+
</Text>
|
|
100
|
+
</Pressable>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
</ScrollView>
|
|
104
|
+
<ScrollView
|
|
105
|
+
accessibilityLabel={minuteColumnAccessibilityLabel}
|
|
106
|
+
className={timePickerColumnClassName}
|
|
107
|
+
>
|
|
108
|
+
{minutes.map((minute) => {
|
|
109
|
+
const isActive = minute === selectedMinute;
|
|
110
|
+
return (
|
|
111
|
+
<Pressable
|
|
112
|
+
key={minute}
|
|
113
|
+
accessibilityRole="button"
|
|
114
|
+
accessibilityLabel={padTwoDigits(minute)}
|
|
115
|
+
accessibilityState={{ selected: isActive }}
|
|
116
|
+
aria-selected={isActive}
|
|
117
|
+
onPress={() => emit(selectedHour ?? 0, minute)}
|
|
118
|
+
className={cn(
|
|
119
|
+
timePickerOptionClassName,
|
|
120
|
+
isActive && timePickerOptionSelectedClassName,
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
<Text className={timePickerOptionLabelClassName}>{padTwoDigits(minute)}</Text>
|
|
124
|
+
</Pressable>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</ScrollView>
|
|
128
|
+
</View>
|
|
129
|
+
</Sheet>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
|
|
3
|
+
import type { ToastVariantProps } from "../../variants/toast-variants";
|
|
4
|
+
|
|
5
|
+
export type ToastVariant = NonNullable<ToastVariantProps["variant"]>;
|
|
6
|
+
|
|
7
|
+
export interface ToastRequest {
|
|
8
|
+
message: string;
|
|
9
|
+
variant?: ToastVariant;
|
|
10
|
+
duration?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface QueuedToast extends ToastRequest {
|
|
14
|
+
id: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ToastQueue {
|
|
18
|
+
toasts: readonly QueuedToast[];
|
|
19
|
+
show: (toast: ToastRequest) => string;
|
|
20
|
+
dismiss: (id: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ToastContext = createContext<ToastQueue | null>(null);
|
|
24
|
+
|
|
25
|
+
export const MISSING_TOAST_PROVIDER_MESSAGE =
|
|
26
|
+
"No <ToastProvider> found. Render <ToastProvider> once at the app root so toasts have somewhere to queue.";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { spacing } from "@atlure/tokens";
|
|
2
|
+
import { type ReactNode, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { AccessibilityInfo, View } from "react-native";
|
|
4
|
+
|
|
5
|
+
import { Portal } from "../../lib/portal";
|
|
6
|
+
import { toastViewportClassName } from "../../variants/toast-variants";
|
|
7
|
+
import { Toast } from "./toast";
|
|
8
|
+
import { type QueuedToast, ToastContext, type ToastRequest } from "./toast-context";
|
|
9
|
+
import { TOAST_DEFAULT_DURATION } from "./utils";
|
|
10
|
+
|
|
11
|
+
export interface ToastProviderProps {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
bottomInset?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ToastProvider({ children, bottomInset = 0 }: ToastProviderProps) {
|
|
17
|
+
const [toasts, setToasts] = useState<QueuedToast[]>([]);
|
|
18
|
+
const idPrefix = useId();
|
|
19
|
+
const nextIdRef = useRef(0);
|
|
20
|
+
|
|
21
|
+
const dismiss = useCallback((id: string) => {
|
|
22
|
+
setToasts((queued) => queued.filter((toast) => toast.id !== id));
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const show = useCallback(
|
|
26
|
+
(request: ToastRequest) => {
|
|
27
|
+
nextIdRef.current += 1;
|
|
28
|
+
const id = `${idPrefix}${nextIdRef.current}`;
|
|
29
|
+
|
|
30
|
+
setToasts((queued) => [...queued, { ...request, id }]);
|
|
31
|
+
AccessibilityInfo.announceForAccessibility(request.message);
|
|
32
|
+
|
|
33
|
+
return id;
|
|
34
|
+
},
|
|
35
|
+
[idPrefix],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const queue = useMemo(() => ({ toasts, show, dismiss }), [toasts, show, dismiss]);
|
|
39
|
+
const visibleToast = toasts[0] ?? null;
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (visibleToast === null) return;
|
|
43
|
+
|
|
44
|
+
const duration = visibleToast.duration ?? TOAST_DEFAULT_DURATION;
|
|
45
|
+
const timeout = setTimeout(() => dismiss(visibleToast.id), duration);
|
|
46
|
+
|
|
47
|
+
return () => clearTimeout(timeout);
|
|
48
|
+
}, [visibleToast, dismiss]);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<ToastContext.Provider value={queue}>
|
|
52
|
+
{children}
|
|
53
|
+
{visibleToast === null ? null : (
|
|
54
|
+
<Portal>
|
|
55
|
+
<View
|
|
56
|
+
className={toastViewportClassName}
|
|
57
|
+
style={{ paddingBottom: spacing.md + bottomInset }}
|
|
58
|
+
pointerEvents="box-none"
|
|
59
|
+
>
|
|
60
|
+
<Toast
|
|
61
|
+
message={visibleToast.message}
|
|
62
|
+
variant={visibleToast.variant}
|
|
63
|
+
onDismiss={() => dismiss(visibleToast.id)}
|
|
64
|
+
/>
|
|
65
|
+
</View>
|
|
66
|
+
</Portal>
|
|
67
|
+
)}
|
|
68
|
+
</ToastContext.Provider>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { PanResponder, View } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import { toastMessageVariants, toastVariants } from "../../variants/toast-variants";
|
|
6
|
+
import { Text } from "../text/text";
|
|
7
|
+
import type { ToastVariant } from "./toast-context";
|
|
8
|
+
import { TOAST_SWIPE_DISMISS_DISTANCE } from "./utils";
|
|
9
|
+
|
|
10
|
+
export interface ToastProps {
|
|
11
|
+
message: string;
|
|
12
|
+
variant?: ToastVariant;
|
|
13
|
+
onDismiss: () => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Toast({ message, variant = "default", onDismiss, className }: ToastProps) {
|
|
18
|
+
const panResponder = useMemo(
|
|
19
|
+
() =>
|
|
20
|
+
PanResponder.create({
|
|
21
|
+
onMoveShouldSetPanResponder: (_event, gesture) =>
|
|
22
|
+
Math.abs(gesture.dx) > TOAST_SWIPE_DISMISS_DISTANCE,
|
|
23
|
+
onPanResponderRelease: (_event, gesture) => {
|
|
24
|
+
if (Math.abs(gesture.dx) > TOAST_SWIPE_DISMISS_DISTANCE) onDismiss();
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
[onDismiss],
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View
|
|
32
|
+
accessibilityRole="alert"
|
|
33
|
+
accessibilityLiveRegion="polite"
|
|
34
|
+
aria-live="polite"
|
|
35
|
+
className={cn(toastVariants({ variant }), className)}
|
|
36
|
+
{...panResponder.panHandlers}
|
|
37
|
+
>
|
|
38
|
+
<Text className={toastMessageVariants({ variant })}>{message}</Text>
|
|
39
|
+
</View>
|
|
40
|
+
);
|
|
41
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,48 +1,102 @@
|
|
|
1
1
|
/* atlure-generated-barrel-do-not-edit */
|
|
2
2
|
|
|
3
|
+
export * from "./components/alert-dialog/alert-dialog";
|
|
3
4
|
export * from "./components/avatar/avatar";
|
|
5
|
+
export * from "./components/avatar/avatar-group";
|
|
4
6
|
export * from "./components/badge/badge";
|
|
7
|
+
export * from "./components/badge/urgency-badge";
|
|
5
8
|
export * from "./components/button/button";
|
|
6
9
|
export * from "./components/button/types";
|
|
10
|
+
export * from "./components/calendar/calendar";
|
|
7
11
|
export * from "./components/card/card";
|
|
8
12
|
export * from "./components/card/card-section";
|
|
9
13
|
export * from "./components/checkbox/checkbox";
|
|
14
|
+
export * from "./components/chip/chip";
|
|
15
|
+
export * from "./components/date-range-picker/date-range-picker";
|
|
16
|
+
export * from "./components/dialog/dialog";
|
|
10
17
|
export * from "./components/empty-state/empty-state";
|
|
11
18
|
export * from "./components/error-state/error-state";
|
|
12
19
|
export * from "./components/form-field/form-field";
|
|
13
20
|
export * from "./components/form-field/form-field-context";
|
|
14
21
|
export * from "./components/form-scroll-view/form-scroll-view";
|
|
22
|
+
export * from "./components/format/date-label";
|
|
23
|
+
export * from "./components/format/distance-label";
|
|
24
|
+
export * from "./components/format/duration-label";
|
|
25
|
+
export * from "./components/format/money-label";
|
|
15
26
|
export * from "./components/icon-button/icon-button";
|
|
16
27
|
export * from "./components/input/input";
|
|
17
28
|
export * from "./components/label/label";
|
|
18
29
|
export * from "./components/list-row/list-row";
|
|
30
|
+
export * from "./components/picker/picker";
|
|
31
|
+
export * from "./components/progress/progress";
|
|
32
|
+
export * from "./components/radio-group/radio-group";
|
|
33
|
+
export * from "./components/radio-group/radio-group-context";
|
|
34
|
+
export * from "./components/screen-header/screen-header";
|
|
19
35
|
export * from "./components/screen-state/screen-state";
|
|
20
36
|
export * from "./components/search-bar/search-bar";
|
|
37
|
+
export * from "./components/segmented-control/segmented-control";
|
|
38
|
+
export * from "./components/select/select";
|
|
21
39
|
export * from "./components/separator/separator";
|
|
40
|
+
export * from "./components/settings-row/settings-row";
|
|
41
|
+
export * from "./components/sheet/sheet";
|
|
22
42
|
export * from "./components/skeleton/skeleton";
|
|
23
43
|
export * from "./components/skeleton/skeleton-compositions";
|
|
44
|
+
export * from "./components/slider/format-label";
|
|
45
|
+
export * from "./components/slider/range-slider";
|
|
46
|
+
export * from "./components/slider/slider";
|
|
24
47
|
export * from "./components/spinner/spinner";
|
|
48
|
+
export * from "./components/star-rating/star-rating";
|
|
25
49
|
export * from "./components/switch/switch";
|
|
50
|
+
export * from "./components/tabs/tabs";
|
|
51
|
+
export * from "./components/tabs/tabs-context";
|
|
26
52
|
export * from "./components/text/text";
|
|
27
53
|
export * from "./components/text/text-class-context";
|
|
28
54
|
export * from "./components/textarea/textarea";
|
|
55
|
+
export * from "./components/time-picker/time-picker";
|
|
56
|
+
export * from "./components/toast/toast";
|
|
57
|
+
export * from "./components/toast/toast-context";
|
|
58
|
+
export * from "./components/toast/toast-provider";
|
|
59
|
+
export * from "./lib/calendar";
|
|
29
60
|
export * from "./lib/cn";
|
|
61
|
+
export * from "./lib/format/date";
|
|
62
|
+
export * from "./lib/format/distance";
|
|
63
|
+
export * from "./lib/format/duration";
|
|
64
|
+
export * from "./lib/format/money";
|
|
65
|
+
export * from "./lib/locale";
|
|
66
|
+
export * from "./lib/portal";
|
|
30
67
|
export * from "./lib/touch-target";
|
|
68
|
+
export * from "./lib/use-sheet";
|
|
69
|
+
export * from "./lib/use-toast";
|
|
31
70
|
export * from "./variants/avatar-variants";
|
|
32
71
|
export * from "./variants/badge-variants";
|
|
33
72
|
export * from "./variants/button-variants";
|
|
73
|
+
export * from "./variants/calendar-variants";
|
|
34
74
|
export * from "./variants/card-variants";
|
|
35
75
|
export * from "./variants/checkbox-variants";
|
|
76
|
+
export * from "./variants/chip-variants";
|
|
77
|
+
export * from "./variants/dialog-variants";
|
|
36
78
|
export * from "./variants/form-field-variants";
|
|
37
79
|
export * from "./variants/form-scroll-view-variants";
|
|
38
80
|
export * from "./variants/input-variants";
|
|
39
81
|
export * from "./variants/label-variants";
|
|
40
82
|
export * from "./variants/list-row-variants";
|
|
83
|
+
export * from "./variants/overlay-variants";
|
|
84
|
+
export * from "./variants/progress-variants";
|
|
85
|
+
export * from "./variants/radio-group-variants";
|
|
86
|
+
export * from "./variants/screen-header-variants";
|
|
41
87
|
export * from "./variants/search-bar-variants";
|
|
88
|
+
export * from "./variants/segmented-control-variants";
|
|
89
|
+
export * from "./variants/select-variants";
|
|
42
90
|
export * from "./variants/separator-variants";
|
|
91
|
+
export * from "./variants/settings-row-variants";
|
|
92
|
+
export * from "./variants/sheet-variants";
|
|
43
93
|
export * from "./variants/skeleton-variants";
|
|
94
|
+
export * from "./variants/slider-variants";
|
|
44
95
|
export * from "./variants/spinner-variants";
|
|
96
|
+
export * from "./variants/star-rating-variants";
|
|
45
97
|
export * from "./variants/state-variants";
|
|
46
98
|
export * from "./variants/switch-variants";
|
|
99
|
+
export * from "./variants/tabs-variants";
|
|
47
100
|
export * from "./variants/text-variants";
|
|
48
101
|
export * from "./variants/textarea-variants";
|
|
102
|
+
export * from "./variants/toast-variants";
|