@nativetail/ui 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,252 @@
1
+ import { BlurView } from "expo-blur";
2
+ import { AnimatePresence } from "moti";
3
+ import {
4
+ mergeClasses,
5
+ Pressable,
6
+ PressableProps,
7
+ useTw,
8
+ View,
9
+ } from "@nativetail/core";
10
+ import React, {
11
+ ReactNode,
12
+ useCallback,
13
+ useEffect,
14
+ useRef,
15
+ useState,
16
+ } from "react";
17
+ import {
18
+ Dimensions,
19
+ I18nManager,
20
+ Modal,
21
+ View as NativeView,
22
+ StatusBar,
23
+ } from "react-native";
24
+ import { Blur } from "../blur";
25
+
26
+ type PositionType = {
27
+ width: number;
28
+ height: number;
29
+ top: number;
30
+ left: number;
31
+ bottom: number;
32
+ };
33
+ type DropdownState = {
34
+ isOpen: boolean;
35
+ toggle: () => void;
36
+ close: () => void;
37
+ open: () => void;
38
+ position: PositionType | null;
39
+ setPosition: (position: PositionType | null) => void;
40
+ };
41
+ const DropdownContext = React.createContext<DropdownState | null>(null);
42
+ const useDropdownContext = () => {
43
+ const context = React.useContext(DropdownContext);
44
+ if (!context) {
45
+ throw new Error("useDropdownContext must be used within a DropdownRoot");
46
+ }
47
+ return context;
48
+ };
49
+
50
+ const DropdownRoot = ({
51
+ className,
52
+ children,
53
+ }: {
54
+ className?: string;
55
+ children: ReactNode;
56
+ }) => {
57
+ const [open, setOpen] = useState(false);
58
+ const [position, setPosition] = useState<PositionType | null>(null);
59
+ return (
60
+ <DropdownContext.Provider
61
+ value={{
62
+ isOpen: open,
63
+ toggle: () => setOpen(!open),
64
+ close: () => setOpen(false),
65
+ open: () => setOpen(true),
66
+ position,
67
+ setPosition,
68
+ }}
69
+ >
70
+ <View className={className} animated>
71
+ {children}
72
+ </View>
73
+ </DropdownContext.Provider>
74
+ );
75
+ };
76
+ const DropdownTrigger = ({
77
+ className,
78
+ children,
79
+ containerClassName,
80
+ }: {
81
+ className?: string;
82
+ children: ReactNode;
83
+ containerClassName?: string;
84
+ }) => {
85
+ const toggle = useDropdownContext().toggle;
86
+ const ref = useRef<NativeView>(null);
87
+
88
+ const statusBarHeight: number = StatusBar.currentHeight || 0;
89
+ const setPosition = useDropdownContext().setPosition;
90
+
91
+ const { width: W, height: H } = Dimensions.get("window");
92
+ const _measure = useCallback(() => {
93
+ if (ref && ref?.current) {
94
+ ref.current.measureInWindow((pageX, pageY, width, height) => {
95
+ let isFull = false;
96
+
97
+ const top = isFull ? 20 : height + pageY + 2;
98
+ const bottom = H - top + height;
99
+ const left = I18nManager.isRTL ? W - width - pageX : pageX;
100
+
101
+ setPosition({
102
+ width: Math.floor(width),
103
+ top: Math.floor(top + statusBarHeight),
104
+ bottom: Math.floor(bottom - statusBarHeight),
105
+ left: Math.floor(left),
106
+ height: Math.floor(height),
107
+ });
108
+ });
109
+ }
110
+ }, [H, W]);
111
+ const handlePress = useCallback(() => {
112
+ if (__DEV__) {
113
+ _measure();
114
+ }
115
+ toggle();
116
+ }, [toggle, _measure]);
117
+ return (
118
+ <Pressable
119
+ className={className}
120
+ onPress={handlePress}
121
+ containerClassName={containerClassName}
122
+ onLayout={_measure}
123
+ ref={ref}
124
+ >
125
+ {children}
126
+ </Pressable>
127
+ );
128
+ };
129
+ let timeout: NodeJS.Timeout;
130
+ const DropdownMenu = ({
131
+ className,
132
+ children,
133
+ useBlur = true,
134
+ }: {
135
+ className?: string;
136
+ children: ReactNode;
137
+ useBlur?: boolean;
138
+ }) => {
139
+ const { isOpen, close } = useDropdownContext();
140
+ const position = useDropdownContext().position;
141
+ const left = position?.left || 0;
142
+ const top = position?.top || 0;
143
+ const menuX = left;
144
+ const menuY = top;
145
+ const [modalOpen, setModalOpen] = useState(isOpen);
146
+ useEffect(() => {
147
+ if (isOpen) {
148
+ setModalOpen(true);
149
+ } else {
150
+ timeout = setTimeout(
151
+ () => {
152
+ setModalOpen(false);
153
+ },
154
+ isOpen ? 0 : 200
155
+ );
156
+ }
157
+ return () => {
158
+ if (timeout) clearTimeout(timeout);
159
+ };
160
+ }, [isOpen]);
161
+
162
+ const onDidAnimate = useCallback(() => {
163
+ if (!isOpen) {
164
+ setModalOpen(false);
165
+ }
166
+ }, [isOpen]);
167
+ const tw = useTw();
168
+ const renderChildren = useCallback(() => {
169
+ return React.Children.map(children, (child, index) => {
170
+ return React.cloneElement(child as any, {
171
+ key: index,
172
+ last: index === React.Children.count(children) - 1,
173
+ first: index === 0,
174
+ });
175
+ });
176
+ }, [children]);
177
+ return (
178
+ <Modal
179
+ visible={modalOpen}
180
+ transparent
181
+ onRequestClose={close}
182
+ statusBarTranslucent
183
+ >
184
+ <Pressable className="flex-1 " onPress={close}>
185
+ <AnimatePresence exitBeforeEnter>
186
+ {isOpen && (
187
+ <View
188
+ className={mergeClasses(
189
+ "absolute in:scale-0 scale-100 out:scale-0 overflow-hidden z-10 bg-card/95 rounded-xl max-w-xs w-full border border-muted/15",
190
+ className
191
+ )}
192
+ onDidAnimate={onDidAnimate}
193
+ style={{
194
+ top: menuY,
195
+ left: menuX,
196
+ transformOrigin: "top left",
197
+ }}
198
+ animated
199
+ print
200
+ id={"1"}
201
+ >
202
+ {useBlur && (
203
+ <Blur style={tw`absolute top-0 left-0 rounded-xl flex-1 `} />
204
+ )}
205
+ {renderChildren()}
206
+ </View>
207
+ )}
208
+ </AnimatePresence>
209
+ </Pressable>
210
+ </Modal>
211
+ );
212
+ };
213
+ const DropdownItem = ({
214
+ className,
215
+ children,
216
+ last,
217
+ first,
218
+ ...props
219
+ }: {
220
+ className?: string;
221
+ last?: boolean;
222
+ children: ReactNode;
223
+ first?: boolean;
224
+ } & PressableProps) => {
225
+ const close = useDropdownContext().close;
226
+ return (
227
+ <Pressable
228
+ className={mergeClasses(
229
+ "w-full text-[16px] font-medium bg-card/15 active:bg-card py-2.5 px-4 flex-row items-center justify-between border-b border-b-transparent text-foreground",
230
+ first ? "rounded-t-xl" : "",
231
+ last ? "rounded-b-xl" : "border-b-muted/15",
232
+ className
233
+ )}
234
+ {...props}
235
+ onPress={() => {
236
+ close();
237
+ props.onPress && props.onPress();
238
+ }}
239
+ >
240
+ {children}
241
+ </Pressable>
242
+ );
243
+ };
244
+
245
+ const Dropdown = {
246
+ Root: DropdownRoot,
247
+ Trigger: DropdownTrigger,
248
+ Menu: DropdownMenu,
249
+ Item: DropdownItem,
250
+ };
251
+
252
+ export { Dropdown };
@@ -0,0 +1,14 @@
1
+ export * from "./dropdown";
2
+ export * from "./button";
3
+ export * from "./input";
4
+ export * from "./switch";
5
+ export * from "./toast";
6
+ export * from "./select";
7
+ export * from "./dialog";
8
+ export * from "./alert-dialog";
9
+ export * from "./bottom-sheet";
10
+ export * from "./actions-sheet";
11
+ export * from "./chip";
12
+ export * from "./blur";
13
+ export * from "./progress";
14
+ export * from "./counter";
@@ -0,0 +1,74 @@
1
+ import { cn, Text, TextInput, TextInputProps, View } from "@nativetail/core";
2
+ import { useCallback, useState } from "react";
3
+ import { create } from "zustand";
4
+
5
+ type FloatingInputProps = TextInputProps & {
6
+ containerClassName?: string;
7
+ label: string;
8
+ error?: string;
9
+ helperText?: string;
10
+ };
11
+ const useFocusSate = create<{
12
+ isFocused: boolean;
13
+ setIsFocused: (isFocused: boolean) => void;
14
+ }>((set) => ({
15
+ isFocused: false,
16
+ setIsFocused: (isFocused: boolean) => set({ isFocused }),
17
+ }));
18
+ export function FloatingInput({
19
+ value,
20
+ onChangeText,
21
+ containerClassName,
22
+ label,
23
+ error,
24
+ className,
25
+ ...props
26
+ }: FloatingInputProps) {
27
+ const onFocus = useCallback(() => {
28
+ useFocusSate.getState().setIsFocused(true);
29
+ }, []);
30
+ const onBlur = useCallback(() => {
31
+ useFocusSate.getState().setIsFocused(false);
32
+ }, []);
33
+ return (
34
+ <View
35
+ className={cn(
36
+ "w-full rounded-xl h-16 overflow-hidden border border-muted/15",
37
+ containerClassName
38
+ )}
39
+ >
40
+ <Label label={label} value={value} />
41
+
42
+ <TextInput
43
+ onFocus={onFocus}
44
+ onBlur={onBlur}
45
+ value={value}
46
+ onChangeText={onChangeText}
47
+ className={cn(
48
+ "flex-1 p-3 bg-card rounded-xl absolute w-full h-full -z-5 pt-5 text-foreground text-[16px]",
49
+ className
50
+ )}
51
+ {...props}
52
+ />
53
+ </View>
54
+ );
55
+ }
56
+
57
+ const Label = ({ label, value }: { label?: string; value?: string }) => {
58
+ const isFocused = useFocusSate((state) => state.isFocused);
59
+ const labelOnTop = isFocused || !!value;
60
+
61
+ return (
62
+ <View className="flex-1 p-3 justify-center" pointerEvents="none">
63
+ <Text
64
+ animated
65
+ className={cn(
66
+ "text-muted duration-75 ",
67
+ labelOnTop ? " -translate-y-16 text-xs" : "translate-y-0 text-[16px]"
68
+ )}
69
+ >
70
+ {label}
71
+ </Text>
72
+ </View>
73
+ );
74
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./floating-input";
2
+ export * from "./input";
@@ -0,0 +1,41 @@
1
+ import {
2
+ cn,
3
+ Text,
4
+ TextInput,
5
+ TextInputProps,
6
+ useTw,
7
+ View,
8
+ } from "@nativetail/core";
9
+
10
+ type InputProps = TextInputProps & {
11
+ containerClassName?: string;
12
+ label: string;
13
+ error?: string;
14
+ helperText?: string;
15
+ };
16
+ export function Input({
17
+ value,
18
+ onChangeText,
19
+ containerClassName,
20
+ label,
21
+ error,
22
+ className,
23
+ ...props
24
+ }: InputProps) {
25
+ const tw = useTw();
26
+ return (
27
+ <View className={cn("w-full gap-1", containerClassName)}>
28
+ <Text className={cn("text-muted/75 duration-75 ")}>{label}</Text>
29
+ <TextInput
30
+ value={value}
31
+ onChangeText={onChangeText}
32
+ className={cn(
33
+ "p-3 bg-card rounded-lg w-full border border-muted/15 h-14 text-foreground -z-5 text-[16px]",
34
+ className
35
+ )}
36
+ placeholderTextColor={tw.color("muted")}
37
+ {...props}
38
+ />
39
+ </View>
40
+ );
41
+ }
@@ -0,0 +1,28 @@
1
+ import { View, cn } from "@nativetail/core";
2
+ import React, { useMemo } from "react";
3
+ export type ProgressProps = {
4
+ containerClassName?: string;
5
+ progress: number;
6
+ max: number;
7
+ };
8
+ export function Progress({ containerClassName, progress, max }: ProgressProps) {
9
+ const percent = useMemo(() => {
10
+ return Math.round((progress / max) * 100);
11
+ }, [progress, max]);
12
+ return (
13
+ <View
14
+ className={cn(
15
+ "flex-row items-center rounded-full h-4 overflow-hidden bg-primary/15 border border-primary/35 ",
16
+ containerClassName
17
+ )}
18
+ >
19
+ <View
20
+ className={cn(
21
+ "w-full h-full bg-primary rounded-full",
22
+ `w-[${percent}%]`
23
+ )}
24
+ animated
25
+ />
26
+ </View>
27
+ );
28
+ }
@@ -0,0 +1,141 @@
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
+
6
+ type SelectProps = PressableProps & {
7
+ containerClassName?: string;
8
+ label: string;
9
+ error?: string;
10
+ helperText?: string;
11
+ value: string;
12
+ onChange: (value: string) => void;
13
+ placeholder?: string;
14
+ options: {
15
+ label: string;
16
+ value: string;
17
+ icon?: React.ReactNode;
18
+ }[];
19
+ };
20
+ export function Select({
21
+ containerClassName,
22
+ label,
23
+ error,
24
+ className,
25
+ value,
26
+ onChange,
27
+ helperText,
28
+ placeholder,
29
+ options,
30
+ ...props
31
+ }: SelectProps) {
32
+ const tw = useTw();
33
+ const renderOptions = useCallback(() => {
34
+ return options.map((option, index) => (
35
+ <SelectItem
36
+ label={option.label}
37
+ value={option.value}
38
+ icon={option.icon}
39
+ onChange={onChange}
40
+ isActive={value === option.value}
41
+ key={option.value}
42
+ first={index === 0}
43
+ last={index === options.length - 1}
44
+ />
45
+ ));
46
+ }, [options, value, onChange, tw]);
47
+ return (
48
+ <View className={cn("w-full gap-1", containerClassName)}>
49
+ <Text className={cn("text-muted/75 duration-75 ")}>{label}</Text>
50
+
51
+ <Dropdown.Root>
52
+ <SelectTrigger
53
+ options={options}
54
+ value={value}
55
+ placeholder={placeholder}
56
+ {...props}
57
+ />
58
+ <Dropdown.Menu>{renderOptions()}</Dropdown.Menu>
59
+ </Dropdown.Root>
60
+ </View>
61
+ );
62
+ }
63
+ const SelectTrigger = memo(
64
+ ({
65
+ options,
66
+ className,
67
+ value,
68
+ placeholder,
69
+ ...props
70
+ }: Partial<SelectProps>) => {
71
+ const selectedOption = useMemo(
72
+ () => options.find((option) => option.value === value),
73
+ [value]
74
+ );
75
+ const tw = useTw();
76
+ return (
77
+ <Dropdown.Trigger
78
+ className={cn(
79
+ "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]",
80
+ className
81
+ )}
82
+ {...props}
83
+ >
84
+ {selectedOption && (
85
+ <Text className="text-foreground">{selectedOption.label}</Text>
86
+ )}
87
+ {!selectedOption && placeholder && (
88
+ <Text className="text-muted">{placeholder}</Text>
89
+ )}
90
+ <Iconify
91
+ icon="solar:alt-arrow-down-outline"
92
+ size={20}
93
+ color={tw.color("foreground")}
94
+ />
95
+ </Dropdown.Trigger>
96
+ );
97
+ }
98
+ );
99
+
100
+ const SelectItem = memo(
101
+ ({
102
+ label,
103
+ value,
104
+ icon,
105
+ onChange,
106
+ isActive,
107
+ first,
108
+ last,
109
+ }: {
110
+ label: string;
111
+ value: string;
112
+ icon?: React.ReactNode;
113
+
114
+ onChange: (value: string) => void;
115
+ isActive?: boolean;
116
+ first?: boolean;
117
+ last?: boolean;
118
+ }) => {
119
+ const tw = useTw();
120
+ return (
121
+ <Dropdown.Item
122
+ key={value}
123
+ onPress={() => onChange(isActive ? "" : value)}
124
+ first={first}
125
+ last={last}
126
+ >
127
+ <View className="flex-row items-center gap-2">
128
+ <Text className="text-sm text-foreground">{label}</Text>
129
+ {icon}
130
+ </View>
131
+ {isActive && (
132
+ <Iconify
133
+ icon="lucide:check"
134
+ size={16}
135
+ color={tw.color("foreground")}
136
+ />
137
+ )}
138
+ </Dropdown.Item>
139
+ );
140
+ }
141
+ );
@@ -0,0 +1,31 @@
1
+ import { cn, View } from "@nativetail/core";
2
+ import { useCallback } from "react";
3
+
4
+ export type StepperProps = {
5
+ steps: string[];
6
+ activeStepIndex: number;
7
+ setActiveStepIndex?: (index: number) => void;
8
+ };
9
+ export const Stepper = (props: StepperProps) => {
10
+ const renderSteps = useCallback(() => {
11
+ return props.steps.map((step, index) => {
12
+ const isActive = index === props.activeStepIndex;
13
+ const isCompleted = index < props.activeStepIndex;
14
+ return (
15
+ <View key={index} className="flex-row items-center">
16
+ <View
17
+ className={cn("w-4 h-4 rounded-full", {
18
+ "bg-primary": isActive,
19
+ "bg-gray-300": !isActive && !isCompleted,
20
+ "bg-success": isCompleted,
21
+ })}
22
+ />
23
+ {index < props.steps.length - 1 && (
24
+ <View className="h-0.5 bg-gray-300 w-4" />
25
+ )}
26
+ </View>
27
+ );
28
+ });
29
+ }, [props.steps, props.activeStepIndex]);
30
+ return <View className="flex-row items-center">{renderSteps()}</View>;
31
+ };
@@ -0,0 +1,72 @@
1
+ import {
2
+ cn,
3
+ cva,
4
+ Pressable,
5
+ useTw,
6
+ VariantProps,
7
+ View,
8
+ } from "@nativetail/core";
9
+
10
+ const switchVariants = cva(
11
+ "rounded-full bg-card justify-center border p-0.5 items-start",
12
+ {
13
+ variants: {
14
+ size: {
15
+ small: "w-12 h-6",
16
+ medium: "w-16 h-8",
17
+ large: "w-24 h-12",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ size: "small",
22
+ },
23
+ }
24
+ );
25
+ type SwitchProps = VariantProps<typeof switchVariants> & {
26
+ checked: boolean;
27
+ onChange: (checked: boolean) => void;
28
+ containerClassName?: string;
29
+ containerActiveClass?: string;
30
+ indicatorClassName?: string;
31
+ };
32
+ export function Switch({
33
+ checked,
34
+ onChange,
35
+ containerClassName,
36
+ size,
37
+ indicatorClassName,
38
+ containerActiveClass,
39
+ ...props
40
+ }: SwitchProps) {
41
+ const className = switchVariants({
42
+ className: containerClassName,
43
+ size: size,
44
+ });
45
+ const tw = useTw();
46
+ const style = tw.style(className);
47
+ const containerWidth = Number(style.width) || 0;
48
+ const indicatorWidth = containerWidth * 0.48;
49
+ const x = `translate-x-${!checked ? 1 : indicatorWidth}`;
50
+ return (
51
+ <Pressable
52
+ className={cn(
53
+ className,
54
+ checked ? "bg-primary/35 " + containerActiveClass : ""
55
+ )}
56
+ aria-checked={checked}
57
+ accessibilityRole="switch"
58
+ aria-label="Switch"
59
+ {...props}
60
+ onPress={() => onChange(!checked)}
61
+ >
62
+ <View
63
+ className={cn(
64
+ `rounded-full bg-primary aspect-square h-full`,
65
+ indicatorClassName,
66
+ x
67
+ )}
68
+ animated
69
+ />
70
+ </Pressable>
71
+ );
72
+ }