@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.
@@ -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
  };