@nativetail/ui 0.0.1
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/README.md +5 -0
- package/babel.config.js +3 -0
- package/package.json +50 -0
- package/src/components/actions-sheet/index.tsx +98 -0
- package/src/components/alert-dialog/index.tsx +58 -0
- package/src/components/blur/index.tsx +15 -0
- package/src/components/bottom-sheet/index.tsx +46 -0
- package/src/components/button/index.tsx +109 -0
- package/src/components/chip/index.tsx +74 -0
- package/src/components/counter/index.tsx +101 -0
- package/src/components/dialog/index.tsx +111 -0
- package/src/components/dropdown/index.tsx +252 -0
- package/src/components/index.ts +14 -0
- package/src/components/input/floating-input.tsx +74 -0
- package/src/components/input/index.ts +2 -0
- package/src/components/input/input.tsx +41 -0
- package/src/components/progress/index.tsx +28 -0
- package/src/components/select/index.tsx +141 -0
- package/src/components/stepper/index.tsx +31 -0
- package/src/components/switch/index.tsx +72 -0
- package/src/components/toast/index.tsx +101 -0
- package/src/index.ts +1 -0
- package/tailwind.config.js +25 -0
- package/tsconfig.json +18 -0
@@ -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,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
|
+
}
|