@nativetail/ui 0.0.5 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- package/package.json +3 -2
- package/src/components/actions-sheet/index.tsx +1 -1
- package/src/components/alert-dialog/index.tsx +1 -1
- package/src/components/bottom-sheet/index.tsx +2 -3
- package/src/components/button/index.tsx +65 -23
- package/src/components/chip/index.tsx +1 -1
- package/src/components/counter/index.tsx +4 -3
- package/src/components/dialog/index.tsx +20 -8
- package/src/components/dropdown/index.tsx +4 -2
- package/src/components/input/floating-input.tsx +58 -15
- package/src/components/input/input.tsx +47 -9
- package/src/components/input/pin-input.tsx +130 -35
- package/src/components/select/index.tsx +2 -141
- package/src/components/select/multi-select.tsx +208 -0
- package/src/components/select/select.tsx +181 -0
- package/src/components/toast/index.tsx +121 -39
- package/src/index.ts +1 -0
- package/src/utils/component-theme.tsx +51 -0
- package/tailwind.config.js +4 -0
@@ -0,0 +1,181 @@
|
|
1
|
+
import { cn, PressableProps, Text, useTw, View } from "@nativetail/core";
|
2
|
+
import { Dropdown } from "../dropdown";
|
3
|
+
import { memo, useCallback, useMemo } from "react";
|
4
|
+
import { Iconify } from "react-native-iconify";
|
5
|
+
import { Control, Controller, Path } from "react-hook-form";
|
6
|
+
|
7
|
+
type SelectProps<T extends Record<string, any>> = PressableProps & {
|
8
|
+
containerClassName?: string;
|
9
|
+
label?: string;
|
10
|
+
error?: string;
|
11
|
+
helperText?: string;
|
12
|
+
value?: string;
|
13
|
+
onChange?: (value: string) => void;
|
14
|
+
placeholder?: string;
|
15
|
+
options: {
|
16
|
+
label: string;
|
17
|
+
value: string;
|
18
|
+
icon?: React.ReactNode;
|
19
|
+
}[];
|
20
|
+
control?: Control<T, any>;
|
21
|
+
name?: Path<T>;
|
22
|
+
};
|
23
|
+
export const Select = <T extends Record<string, any>>({
|
24
|
+
name,
|
25
|
+
control,
|
26
|
+
...props
|
27
|
+
}: SelectProps<T>) => {
|
28
|
+
if (control) {
|
29
|
+
return (
|
30
|
+
<Controller
|
31
|
+
name={name}
|
32
|
+
control={control}
|
33
|
+
render={({ field, fieldState }) => {
|
34
|
+
return (
|
35
|
+
<BaseSelect
|
36
|
+
{...props}
|
37
|
+
value={field.value}
|
38
|
+
onChange={(text) => {
|
39
|
+
field.onChange(text);
|
40
|
+
}}
|
41
|
+
error={fieldState.error?.message}
|
42
|
+
/>
|
43
|
+
);
|
44
|
+
}}
|
45
|
+
/>
|
46
|
+
);
|
47
|
+
}
|
48
|
+
return <BaseSelect {...props} />;
|
49
|
+
};
|
50
|
+
function BaseSelect<T extends Record<string, any>>({
|
51
|
+
containerClassName,
|
52
|
+
label,
|
53
|
+
error,
|
54
|
+
className,
|
55
|
+
value,
|
56
|
+
onChange,
|
57
|
+
helperText,
|
58
|
+
placeholder,
|
59
|
+
options,
|
60
|
+
...props
|
61
|
+
}: SelectProps<T>) {
|
62
|
+
const tw = useTw();
|
63
|
+
const renderOptions = useCallback(() => {
|
64
|
+
return options.map((option, index) => (
|
65
|
+
<SelectItem
|
66
|
+
label={option.label}
|
67
|
+
value={option.value}
|
68
|
+
icon={option.icon}
|
69
|
+
onChange={onChange}
|
70
|
+
isActive={value === option.value}
|
71
|
+
key={option.value}
|
72
|
+
first={index === 0}
|
73
|
+
last={index === options.length - 1}
|
74
|
+
/>
|
75
|
+
));
|
76
|
+
}, [options, value, onChange, tw]);
|
77
|
+
return (
|
78
|
+
<View className={cn("w-full gap-1", containerClassName)}>
|
79
|
+
<Text className={cn("text-muted/75 duration-75 ")}>{label}</Text>
|
80
|
+
|
81
|
+
<Dropdown.Root>
|
82
|
+
<SelectTrigger
|
83
|
+
options={options}
|
84
|
+
value={value}
|
85
|
+
placeholder={placeholder}
|
86
|
+
error={error}
|
87
|
+
{...props}
|
88
|
+
/>
|
89
|
+
<Dropdown.Menu>{renderOptions()}</Dropdown.Menu>
|
90
|
+
</Dropdown.Root>
|
91
|
+
</View>
|
92
|
+
);
|
93
|
+
}
|
94
|
+
const SelectTrigger = memo(
|
95
|
+
<T extends Record<string, any>>({
|
96
|
+
options,
|
97
|
+
className,
|
98
|
+
value,
|
99
|
+
placeholder,
|
100
|
+
error,
|
101
|
+
...props
|
102
|
+
}: PressableProps & {
|
103
|
+
options: SelectProps<T>["options"];
|
104
|
+
value: SelectProps<T>["value"];
|
105
|
+
placeholder: SelectProps<T>["placeholder"];
|
106
|
+
error: SelectProps<T>["error"];
|
107
|
+
}) => {
|
108
|
+
const selectedOption = useMemo(
|
109
|
+
() => options.find((option) => option.value === value),
|
110
|
+
[value]
|
111
|
+
);
|
112
|
+
const tw = useTw();
|
113
|
+
return (
|
114
|
+
<>
|
115
|
+
<Dropdown.Trigger
|
116
|
+
className={cn(
|
117
|
+
"p-3 bg-card rounded-lg w-full border flex-row items-center justify-between border-muted/15 h-14 text-foreground -z-5 text-[16px]",
|
118
|
+
className
|
119
|
+
)}
|
120
|
+
{...props}
|
121
|
+
>
|
122
|
+
{selectedOption && (
|
123
|
+
<Text className="text-foreground">{selectedOption.label}</Text>
|
124
|
+
)}
|
125
|
+
{!selectedOption && placeholder && (
|
126
|
+
<Text className="text-muted">{placeholder}</Text>
|
127
|
+
)}
|
128
|
+
<Iconify
|
129
|
+
icon="solar:alt-arrow-down-outline"
|
130
|
+
size={20}
|
131
|
+
color={tw.color("foreground")}
|
132
|
+
/>
|
133
|
+
</Dropdown.Trigger>
|
134
|
+
{error && <Text className="text-sm text-danger">{error}</Text>}
|
135
|
+
</>
|
136
|
+
);
|
137
|
+
}
|
138
|
+
);
|
139
|
+
|
140
|
+
const SelectItem = memo(
|
141
|
+
({
|
142
|
+
label,
|
143
|
+
value,
|
144
|
+
icon,
|
145
|
+
onChange,
|
146
|
+
isActive,
|
147
|
+
first,
|
148
|
+
last,
|
149
|
+
}: {
|
150
|
+
label: string;
|
151
|
+
value: string;
|
152
|
+
icon?: React.ReactNode;
|
153
|
+
|
154
|
+
onChange: (value: string) => void;
|
155
|
+
isActive?: boolean;
|
156
|
+
first?: boolean;
|
157
|
+
last?: boolean;
|
158
|
+
}) => {
|
159
|
+
const tw = useTw();
|
160
|
+
return (
|
161
|
+
<Dropdown.Item
|
162
|
+
key={value}
|
163
|
+
onPress={() => onChange(isActive ? "" : value)}
|
164
|
+
first={first}
|
165
|
+
last={last}
|
166
|
+
>
|
167
|
+
<View className="flex-row items-center gap-2">
|
168
|
+
<Text className="text-sm text-foreground">{label}</Text>
|
169
|
+
{icon}
|
170
|
+
</View>
|
171
|
+
{isActive && (
|
172
|
+
<Iconify
|
173
|
+
icon="lucide:check"
|
174
|
+
size={16}
|
175
|
+
color={tw.color("foreground")}
|
176
|
+
/>
|
177
|
+
)}
|
178
|
+
</Dropdown.Item>
|
179
|
+
);
|
180
|
+
}
|
181
|
+
);
|
@@ -1,17 +1,24 @@
|
|
1
|
-
import { BlurView } from "expo-blur";
|
2
1
|
import { cn, Pressable, Text, useTw, View } from "@nativetail/core";
|
3
|
-
import { useEffect } from "react";
|
4
|
-
import { create } from "zustand";
|
5
|
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
6
2
|
import { AnimatePresence } from "moti";
|
7
|
-
import {
|
3
|
+
import { useEffect, useState } from "react";
|
4
|
+
import { Iconify } from "react-native-iconify";
|
5
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
6
|
+
import { create } from "zustand";
|
8
7
|
type ToastType = {
|
9
8
|
message: string;
|
10
9
|
content?: string;
|
11
10
|
id: string;
|
12
11
|
timeout?: number;
|
13
|
-
position?:
|
12
|
+
position?:
|
13
|
+
| "top-center"
|
14
|
+
| "top-left"
|
15
|
+
| "top-right"
|
16
|
+
| "bottom-left"
|
17
|
+
| "bottom-right"
|
18
|
+
| "bottom-center";
|
19
|
+
|
14
20
|
containerClassName?: string;
|
21
|
+
type?: "success" | "danger" | "info" | "warning";
|
15
22
|
};
|
16
23
|
type InsertToastType = Omit<ToastType, "id">;
|
17
24
|
type ToastStore = {
|
@@ -21,7 +28,17 @@ type ToastStore = {
|
|
21
28
|
};
|
22
29
|
const useToastState = create<ToastStore>((set) => ({
|
23
30
|
toasts: [],
|
24
|
-
addToast: (toast) =>
|
31
|
+
addToast: (toast) =>
|
32
|
+
set((state) => ({
|
33
|
+
toasts: [
|
34
|
+
...state.toasts,
|
35
|
+
{
|
36
|
+
type: "info",
|
37
|
+
position: "top-center",
|
38
|
+
...toast,
|
39
|
+
},
|
40
|
+
],
|
41
|
+
})),
|
25
42
|
removeToast: (id) =>
|
26
43
|
set((state) => ({
|
27
44
|
toasts: state.toasts.filter((toast) => toast.id !== id),
|
@@ -30,16 +47,44 @@ const useToastState = create<ToastStore>((set) => ({
|
|
30
47
|
let timeouts = new Map<string, NodeJS.Timeout>();
|
31
48
|
export const showToast = (toast: InsertToastType) => {
|
32
49
|
const id = Math.random().toString(36).substring(7);
|
50
|
+
console.log(id);
|
33
51
|
useToastState.getState().addToast({ ...toast, id });
|
34
52
|
return id;
|
35
53
|
};
|
36
54
|
export function Toaster() {
|
37
55
|
const toasts = useToastState((state) => state.toasts);
|
56
|
+
const topToasts = toasts.filter((toast) => toast.position.includes("top"));
|
57
|
+
const bottomToasts = toasts.filter((toast) =>
|
58
|
+
toast.position.includes("bottom")
|
59
|
+
);
|
60
|
+
|
61
|
+
const safeInsets = useSafeAreaInsets();
|
38
62
|
return (
|
39
|
-
<AnimatePresence exitBeforeEnter>
|
40
|
-
{
|
41
|
-
<
|
42
|
-
|
63
|
+
<AnimatePresence exitBeforeEnter presenceAffectsLayout>
|
64
|
+
{topToasts.length > 0 && (
|
65
|
+
<View
|
66
|
+
className={cn(
|
67
|
+
"absolute w-full top-0 left-0 justify-center z-50 gap-2",
|
68
|
+
`top-[${safeInsets.top + 10}px]`
|
69
|
+
)}
|
70
|
+
>
|
71
|
+
{topToasts.map((toast, index) => (
|
72
|
+
<Toast index={index} {...toast} key={toast.id} />
|
73
|
+
))}
|
74
|
+
</View>
|
75
|
+
)}
|
76
|
+
{bottomToasts.length > 0 && (
|
77
|
+
<View
|
78
|
+
className={cn(
|
79
|
+
"absolute w-full left-0 justify-center z-50 gap-2",
|
80
|
+
`bottom-[${safeInsets.bottom + 10}px]`
|
81
|
+
)}
|
82
|
+
>
|
83
|
+
{bottomToasts.map((toast, index) => (
|
84
|
+
<Toast key={toast.id} index={index} {...toast} />
|
85
|
+
))}
|
86
|
+
</View>
|
87
|
+
)}
|
43
88
|
</AnimatePresence>
|
44
89
|
);
|
45
90
|
}
|
@@ -49,52 +94,89 @@ const Toast = (
|
|
49
94
|
index: number;
|
50
95
|
}
|
51
96
|
) => {
|
97
|
+
const [open, setOpen] = useState(true);
|
52
98
|
const tw = useTw();
|
99
|
+
const closeToast = () => {
|
100
|
+
setOpen(false);
|
101
|
+
setTimeout(() => useToastState.getState().removeToast(toast.id), 150);
|
102
|
+
};
|
53
103
|
useEffect(() => {
|
54
|
-
const id = setTimeout(
|
55
|
-
useToastState.getState().removeToast(toast.id);
|
56
|
-
}, toast.timeout || 5000);
|
104
|
+
const id = setTimeout(closeToast, toast.timeout || 5000);
|
57
105
|
timeouts.set(toast.id, id);
|
58
106
|
return () => {
|
59
107
|
clearTimeout(timeouts.get(toast.id)!);
|
60
108
|
timeouts.delete(toast.id);
|
61
109
|
};
|
62
110
|
}, [toast.id]);
|
63
|
-
|
111
|
+
|
112
|
+
const Icons = {
|
113
|
+
success: (
|
114
|
+
<Iconify
|
115
|
+
icon="lets-icons:check-fill"
|
116
|
+
size={20}
|
117
|
+
color={tw.color("success")}
|
118
|
+
/>
|
119
|
+
),
|
120
|
+
danger: (
|
121
|
+
<Iconify icon="uis:times-circle" size={20} color={tw.color("danger")} />
|
122
|
+
),
|
123
|
+
info: (
|
124
|
+
<Iconify
|
125
|
+
icon="fluent:info-16-filled"
|
126
|
+
size={20}
|
127
|
+
color={tw.color("info")}
|
128
|
+
/>
|
129
|
+
),
|
130
|
+
warning: (
|
131
|
+
<Iconify
|
132
|
+
icon="fluent:warning-16-filled"
|
133
|
+
size={20}
|
134
|
+
color={tw.color("warning")}
|
135
|
+
/>
|
136
|
+
),
|
137
|
+
};
|
138
|
+
|
139
|
+
const Icon = Icons[toast.type];
|
140
|
+
const horizontalPositions = {
|
141
|
+
center: "items-center",
|
142
|
+
left: "items-start",
|
143
|
+
right: "items-end",
|
144
|
+
};
|
145
|
+
const horizontalPosition =
|
146
|
+
horizontalPositions[
|
147
|
+
toast.position.replace("top-", "").replace("bottom-", "")
|
148
|
+
];
|
64
149
|
return (
|
65
150
|
<View
|
66
|
-
className={cn(
|
67
|
-
"absolute w-full top-0 left-0 items-center justify-center z-50 ",
|
68
|
-
toast.position === "top"
|
69
|
-
? `top-[${safeInsets.top + 10}px]`
|
70
|
-
: `bottom-[${safeInsets.bottom + 10}px]`,
|
71
|
-
toast.containerClassName
|
72
|
-
)}
|
151
|
+
className={cn("w-full", horizontalPosition, toast.containerClassName)}
|
73
152
|
animated
|
74
153
|
>
|
75
154
|
<Pressable
|
76
155
|
onPress={() => {
|
77
|
-
|
156
|
+
if (open) closeToast();
|
78
157
|
}}
|
79
158
|
className="w-full items-center justify-center active:scale-95 scale-100 px-4"
|
80
159
|
>
|
81
|
-
<
|
82
|
-
|
83
|
-
|
84
|
-
|
160
|
+
<AnimatePresence presenceAffectsLayout exitBeforeEnter>
|
161
|
+
{open && (
|
162
|
+
<View
|
163
|
+
className={cn(
|
164
|
+
`bg-card/95 border border-muted/15 px-2 py-2 rounded-2xl overflow-hidden flex-row items-center gap-2 in:-translate-y-24 translate-y-0 out:-translate-y-24 in:scale-0 scale-100 out:scale-0`
|
165
|
+
)}
|
166
|
+
animated
|
167
|
+
>
|
168
|
+
<View className="in:scale-0 scale-100">{Icon}</View>
|
169
|
+
<View className="">
|
170
|
+
<Text className="font-medium text-sm text-foreground">
|
171
|
+
{toast.message}
|
172
|
+
</Text>
|
173
|
+
{toast.content && (
|
174
|
+
<Text className="text-xs text-muted">{toast.content}</Text>
|
175
|
+
)}
|
176
|
+
</View>
|
177
|
+
</View>
|
85
178
|
)}
|
86
|
-
|
87
|
-
>
|
88
|
-
<Blur
|
89
|
-
style={tw`absolute top-0 left-0 rounded-xl flex-1 bg-card/50 rounded-full`}
|
90
|
-
/>
|
91
|
-
<Text className="font-medium text-[16px] text-foreground">
|
92
|
-
{toast.message}
|
93
|
-
</Text>
|
94
|
-
{toast.content && (
|
95
|
-
<Text className="text-sm text-muted">{toast.content}</Text>
|
96
|
-
)}
|
97
|
-
</View>
|
179
|
+
</AnimatePresence>
|
98
180
|
</Pressable>
|
99
181
|
</View>
|
100
182
|
);
|
package/src/index.ts
CHANGED
@@ -0,0 +1,51 @@
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
2
|
+
import {
|
3
|
+
ActionSheetProps,
|
4
|
+
AlertDialogProps,
|
5
|
+
BottomSheetProps,
|
6
|
+
ButtonProps,
|
7
|
+
ChipProps,
|
8
|
+
CounterProps,
|
9
|
+
DialogProps,
|
10
|
+
FloatingInputProps,
|
11
|
+
InputProps,
|
12
|
+
PinInputProps,
|
13
|
+
ProgressProps,
|
14
|
+
} from "../components";
|
15
|
+
type ComponentsType = {
|
16
|
+
Button?: Partial<ButtonProps>;
|
17
|
+
Dialog?: Partial<DialogProps>;
|
18
|
+
AlertDialog?: Partial<AlertDialogProps>;
|
19
|
+
Input?: Partial<InputProps>;
|
20
|
+
FloatingInput?: Partial<FloatingInputProps>;
|
21
|
+
PinInput?: Partial<PinInputProps>;
|
22
|
+
BottomSheet?: Partial<BottomSheetProps>;
|
23
|
+
ActionSheet?: Partial<ActionSheetProps>;
|
24
|
+
Chip?: ChipProps;
|
25
|
+
Progress?: ProgressProps;
|
26
|
+
Counter?: CounterProps;
|
27
|
+
};
|
28
|
+
type ComponentThemeContextType = {
|
29
|
+
components: ComponentsType;
|
30
|
+
};
|
31
|
+
|
32
|
+
const ComponentThemeContext = createContext<ComponentThemeContextType>({
|
33
|
+
components: {},
|
34
|
+
});
|
35
|
+
export const useComponentTheme = () => {
|
36
|
+
const ctx = useContext(ComponentThemeContext);
|
37
|
+
return ctx.components;
|
38
|
+
};
|
39
|
+
export function ComponentThemeProvider({
|
40
|
+
children,
|
41
|
+
components,
|
42
|
+
}: {
|
43
|
+
children: React.ReactNode;
|
44
|
+
components: ComponentsType;
|
45
|
+
}) {
|
46
|
+
return (
|
47
|
+
<ComponentThemeContext.Provider value={{ components }}>
|
48
|
+
{children}
|
49
|
+
</ComponentThemeContext.Provider>
|
50
|
+
);
|
51
|
+
}
|