@nativetail/ui 0.0.6 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 { Blur } from "../blur";
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?: "top" | "bottom";
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) => set((state) => ({ toasts: [...state.toasts, 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
- {toasts.map((toast, index) => (
41
- <Toast key={toast.id} index={index} {...toast} />
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
- const safeInsets = useSafeAreaInsets();
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
- useToastState.getState().removeToast(toast.id);
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
- <View
82
- className={cn(
83
- `bg-card/95 border border-muted/15 px-6 py-3 in:opacity-0 opacity-100 in:-translate-y-16 out:-translate-y-16 out:opacity-0 in:scale-0 scale-100 out:scale-0 rounded-full overflow-hidden max-w-sm w-full `,
84
- `translate-y-0`
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
- animated
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
@@ -1 +1,2 @@
1
1
  export * from "./components";
2
+ export * from "./utils/component-theme";
@@ -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
+ }
@@ -20,6 +20,10 @@ module.exports = {
20
20
  warning: '#FFC107',
21
21
  info: '#00BCD4',
22
22
  },
23
+ borderRadius: {
24
+ DEFAULT: '16px',
25
+ "sm": "24px"
26
+ }
23
27
  },
24
28
  },
25
29
  };