@nativetail/ui 0.0.5 → 0.0.7
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/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
|
+
}
|