@beyondcorp/beyond-ui-native 0.1.2 → 0.1.4
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/dist/index.js +78 -61
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +78 -61
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -5
- package/src/components/accordion/accordion.tsx +245 -0
- package/src/components/accordion/index.ts +1 -0
- package/src/components/alert/alert.tsx +118 -0
- package/src/components/alert/index.ts +1 -0
- package/src/components/avatar/avatar.tsx +125 -0
- package/src/components/avatar/index.ts +1 -0
- package/src/components/badge/badge.tsx +89 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/bottom-sheet/bottom-sheet.tsx +336 -0
- package/src/components/bottom-sheet/index.ts +1 -0
- package/src/components/button/button.tsx +172 -0
- package/src/components/button/index.ts +1 -0
- package/src/components/card/card.tsx +121 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/checkbox/checkbox.tsx +100 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/data-table/data-table.tsx +276 -0
- package/src/components/data-table/index.ts +1 -0
- package/src/components/dialog/dialog.tsx +278 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/input.tsx +130 -0
- package/src/components/radio/index.ts +1 -0
- package/src/components/radio/radio.tsx +117 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/select/select.tsx +377 -0
- package/src/components/separator/index.ts +1 -0
- package/src/components/separator/separator.tsx +28 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.tsx +43 -0
- package/src/components/spinner/index.ts +1 -0
- package/src/components/spinner/spinner.tsx +46 -0
- package/src/components/stats-card/index.ts +1 -0
- package/src/components/stats-card/stats-card.tsx +92 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.tsx +80 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.tsx +172 -0
- package/src/components/text/heading.tsx +50 -0
- package/src/components/text/index.ts +2 -0
- package/src/components/text/text.tsx +62 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.tsx +87 -0
- package/src/components/toast/index.ts +2 -0
- package/src/components/toast/toast.tsx +136 -0
- package/src/components/toast/types.ts +32 -0
- package/src/index.ts +42 -0
- package/src/primitives/box.tsx +25 -0
- package/src/primitives/index.ts +2 -0
- package/src/primitives/pressable.tsx +49 -0
- package/src/providers/BeyondUIProvider.tsx +119 -0
- package/src/templates/auth/ForgotPasswordTemplate.tsx +132 -0
- package/src/templates/auth/SignInTemplate.tsx +213 -0
- package/src/templates/auth/SignUpTemplate.tsx +233 -0
- package/src/templates/auth/index.ts +3 -0
- package/src/templates/dashboard/DashboardTemplate.tsx +204 -0
- package/src/templates/dashboard/index.ts +1 -0
- package/src/templates/index.ts +3 -0
- package/src/templates/settings/SettingsTemplate.tsx +226 -0
- package/src/templates/settings/index.ts +1 -0
- package/src/theme/preset.ts +45 -0
- package/src/theme/tokens.ts +144 -0
- package/src/types/nativewind-env.d.ts +26 -0
- package/src/utilities/accessibility.ts +43 -0
- package/src/utilities/cn.ts +9 -0
- package/src/utilities/index.ts +2 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Text,
|
|
4
|
+
TextInput,
|
|
5
|
+
View,
|
|
6
|
+
type TextInputProps,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
9
|
+
import { cn } from "../../utilities/cn";
|
|
10
|
+
|
|
11
|
+
export const inputVariants = cva(
|
|
12
|
+
"w-full rounded-xl border bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100 px-3.5 flex-row items-center",
|
|
13
|
+
{
|
|
14
|
+
variants: {
|
|
15
|
+
inputSize: {
|
|
16
|
+
sm: "min-h-9 py-1 text-sm",
|
|
17
|
+
md: "min-h-11 py-2 text-base",
|
|
18
|
+
lg: "min-h-14 py-3 text-lg",
|
|
19
|
+
},
|
|
20
|
+
status: {
|
|
21
|
+
default: "border-neutral-300 dark:border-neutral-700 focus:border-primary-500",
|
|
22
|
+
error: "border-danger-500 dark:border-danger-500",
|
|
23
|
+
success: "border-success-500 dark:border-success-500",
|
|
24
|
+
},
|
|
25
|
+
disabled: {
|
|
26
|
+
true: "opacity-50 bg-neutral-100 dark:bg-neutral-800",
|
|
27
|
+
false: "",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
inputSize: "md",
|
|
32
|
+
status: "default",
|
|
33
|
+
disabled: false,
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export interface InputProps
|
|
39
|
+
extends Omit<TextInputProps, "editable">,
|
|
40
|
+
VariantProps<typeof inputVariants> {
|
|
41
|
+
label?: string;
|
|
42
|
+
error?: string;
|
|
43
|
+
helperText?: string;
|
|
44
|
+
leftIcon?: React.ReactNode;
|
|
45
|
+
rightIcon?: React.ReactNode;
|
|
46
|
+
leftElement?: React.ReactNode;
|
|
47
|
+
rightElement?: React.ReactNode;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
containerClassName?: string;
|
|
50
|
+
className?: string;
|
|
51
|
+
labelClassName?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const Input = React.forwardRef<TextInput, InputProps>(function Input(
|
|
55
|
+
{
|
|
56
|
+
label,
|
|
57
|
+
error,
|
|
58
|
+
helperText,
|
|
59
|
+
leftIcon,
|
|
60
|
+
rightIcon,
|
|
61
|
+
leftElement,
|
|
62
|
+
rightElement,
|
|
63
|
+
disabled = false,
|
|
64
|
+
inputSize = "md",
|
|
65
|
+
containerClassName,
|
|
66
|
+
className,
|
|
67
|
+
labelClassName,
|
|
68
|
+
placeholderTextColor,
|
|
69
|
+
accessibilityLabel,
|
|
70
|
+
...props
|
|
71
|
+
},
|
|
72
|
+
ref
|
|
73
|
+
) {
|
|
74
|
+
const status = error ? "error" : "default";
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<View className={cn("w-full gap-1.5", containerClassName)}>
|
|
78
|
+
{label ? (
|
|
79
|
+
<Text
|
|
80
|
+
className={cn(
|
|
81
|
+
"text-sm font-medium text-neutral-800 dark:text-neutral-200",
|
|
82
|
+
labelClassName
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{label}
|
|
86
|
+
</Text>
|
|
87
|
+
) : null}
|
|
88
|
+
|
|
89
|
+
<View
|
|
90
|
+
className={cn(
|
|
91
|
+
inputVariants({ inputSize, status, disabled }),
|
|
92
|
+
className
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
{leftIcon || leftElement ? (
|
|
96
|
+
<View className="mr-2">{leftIcon ?? leftElement}</View>
|
|
97
|
+
) : null}
|
|
98
|
+
|
|
99
|
+
<TextInput
|
|
100
|
+
ref={ref}
|
|
101
|
+
editable={!disabled}
|
|
102
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
103
|
+
accessibilityHint={error}
|
|
104
|
+
aria-invalid={!!error}
|
|
105
|
+
placeholderTextColor={
|
|
106
|
+
placeholderTextColor ?? "#9ca3af" // neutral-400
|
|
107
|
+
}
|
|
108
|
+
className="flex-1 py-1 text-neutral-900 dark:text-neutral-100"
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
|
|
112
|
+
{rightIcon || rightElement ? (
|
|
113
|
+
<View className="ml-2">{rightIcon ?? rightElement}</View>
|
|
114
|
+
) : null}
|
|
115
|
+
</View>
|
|
116
|
+
|
|
117
|
+
{error ? (
|
|
118
|
+
<Text accessibilityRole="alert" className="text-xs font-medium text-danger-600 dark:text-danger-400">
|
|
119
|
+
{error}
|
|
120
|
+
</Text>
|
|
121
|
+
) : helperText ? (
|
|
122
|
+
<Text className="text-xs text-neutral-500 dark:text-neutral-400">
|
|
123
|
+
{helperText}
|
|
124
|
+
</Text>
|
|
125
|
+
) : null}
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
Input.displayName = "Input";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./radio";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { View, Text, Pressable, type ViewProps } from "react-native";
|
|
3
|
+
import { cn } from "../../utilities/cn";
|
|
4
|
+
|
|
5
|
+
interface RadioGroupContextValue {
|
|
6
|
+
value?: string;
|
|
7
|
+
onValueChange?: (value: string) => void;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const RadioGroupContext = React.createContext<RadioGroupContextValue>({});
|
|
12
|
+
|
|
13
|
+
export interface RadioGroupProps extends ViewProps {
|
|
14
|
+
value?: string;
|
|
15
|
+
defaultValue?: string;
|
|
16
|
+
onValueChange?: (value: string) => void;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function RadioGroup({
|
|
22
|
+
value: controlledValue,
|
|
23
|
+
defaultValue,
|
|
24
|
+
onValueChange,
|
|
25
|
+
disabled = false,
|
|
26
|
+
className,
|
|
27
|
+
children,
|
|
28
|
+
...props
|
|
29
|
+
}: RadioGroupProps) {
|
|
30
|
+
const [internalValue, setInternalValue] = React.useState(defaultValue);
|
|
31
|
+
const isControlled = controlledValue !== undefined;
|
|
32
|
+
const currentValue = isControlled ? controlledValue : internalValue;
|
|
33
|
+
|
|
34
|
+
const handleValueChange = (val: string) => {
|
|
35
|
+
if (!isControlled) setInternalValue(val);
|
|
36
|
+
onValueChange?.(val);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<RadioGroupContext.Provider
|
|
41
|
+
value={{
|
|
42
|
+
value: currentValue,
|
|
43
|
+
onValueChange: handleValueChange,
|
|
44
|
+
disabled,
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<View className={cn("gap-3", className)} {...props}>
|
|
48
|
+
{children}
|
|
49
|
+
</View>
|
|
50
|
+
</RadioGroupContext.Provider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface RadioProps {
|
|
55
|
+
value: string;
|
|
56
|
+
label?: string;
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
className?: string;
|
|
59
|
+
indicatorClassName?: string;
|
|
60
|
+
labelClassName?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function Radio({
|
|
64
|
+
value,
|
|
65
|
+
label,
|
|
66
|
+
disabled: itemDisabled,
|
|
67
|
+
className,
|
|
68
|
+
indicatorClassName,
|
|
69
|
+
labelClassName,
|
|
70
|
+
}: RadioProps) {
|
|
71
|
+
const group = React.useContext(RadioGroupContext);
|
|
72
|
+
const isSelected = group.value === value;
|
|
73
|
+
const isDisabled = group.disabled || itemDisabled;
|
|
74
|
+
|
|
75
|
+
const handlePress = () => {
|
|
76
|
+
if (!isDisabled) {
|
|
77
|
+
group.onValueChange?.(value);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Pressable
|
|
83
|
+
onPress={handlePress}
|
|
84
|
+
disabled={isDisabled}
|
|
85
|
+
accessibilityRole="radio"
|
|
86
|
+
accessibilityState={{ selected: isSelected, disabled: isDisabled }}
|
|
87
|
+
className={cn("flex-row items-center gap-2.5", isDisabled && "opacity-50")}
|
|
88
|
+
>
|
|
89
|
+
<View
|
|
90
|
+
className={cn(
|
|
91
|
+
"h-5 w-5 rounded-full border border-neutral-300 dark:border-neutral-700 items-center justify-center bg-white dark:bg-neutral-900",
|
|
92
|
+
isSelected && "border-primary-600 dark:border-primary-600",
|
|
93
|
+
className
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
{isSelected ? (
|
|
97
|
+
<View
|
|
98
|
+
className={cn(
|
|
99
|
+
"h-2.5 w-2.5 rounded-full bg-primary-600 dark:bg-primary-600",
|
|
100
|
+
indicatorClassName
|
|
101
|
+
)}
|
|
102
|
+
/>
|
|
103
|
+
) : null}
|
|
104
|
+
</View>
|
|
105
|
+
{label ? (
|
|
106
|
+
<Text
|
|
107
|
+
className={cn(
|
|
108
|
+
"text-sm font-medium text-neutral-800 dark:text-neutral-200",
|
|
109
|
+
labelClassName
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
{label}
|
|
113
|
+
</Text>
|
|
114
|
+
) : null}
|
|
115
|
+
</Pressable>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./select";
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
View,
|
|
5
|
+
Pressable,
|
|
6
|
+
ScrollView,
|
|
7
|
+
TextInput,
|
|
8
|
+
type ViewProps,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
11
|
+
import { cn } from "../../utilities/cn";
|
|
12
|
+
import { Text } from "../text/text";
|
|
13
|
+
|
|
14
|
+
const selectTriggerVariants = cva(
|
|
15
|
+
"flex-row items-center justify-between rounded-xl border bg-white dark:bg-neutral-900 px-4 py-3 active:opacity-80",
|
|
16
|
+
{
|
|
17
|
+
variants: {
|
|
18
|
+
variant: {
|
|
19
|
+
default: "border-neutral-300 dark:border-neutral-700",
|
|
20
|
+
error: "border-danger",
|
|
21
|
+
success: "border-success",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
sm: "h-9 px-3 py-1.5",
|
|
25
|
+
md: "h-12 px-4 py-3",
|
|
26
|
+
lg: "h-14 px-5 py-4",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
variant: "default",
|
|
31
|
+
size: "md",
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export interface SelectOption {
|
|
37
|
+
label: string;
|
|
38
|
+
value: string;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface SelectContextValue {
|
|
43
|
+
open: boolean;
|
|
44
|
+
setOpen: (open: boolean) => void;
|
|
45
|
+
value?: string;
|
|
46
|
+
onSelect: (val: string) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const SelectContext = React.createContext<SelectContextValue | null>(null);
|
|
50
|
+
|
|
51
|
+
function useSelectContext() {
|
|
52
|
+
const context = React.useContext(SelectContext);
|
|
53
|
+
if (!context) {
|
|
54
|
+
throw new Error("Select compound subcomponents must be used within a Select");
|
|
55
|
+
}
|
|
56
|
+
return context;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SelectProps extends VariantProps<typeof selectTriggerVariants> {
|
|
60
|
+
options?: SelectOption[];
|
|
61
|
+
value?: string;
|
|
62
|
+
defaultValue?: string;
|
|
63
|
+
onValueChange?: (value: string) => void;
|
|
64
|
+
placeholder?: string;
|
|
65
|
+
label?: string;
|
|
66
|
+
helperText?: string;
|
|
67
|
+
error?: string;
|
|
68
|
+
disabled?: boolean;
|
|
69
|
+
searchable?: boolean;
|
|
70
|
+
className?: string;
|
|
71
|
+
children?: React.ReactNode;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const Select: React.FC<SelectProps> = ({
|
|
75
|
+
options,
|
|
76
|
+
value: controlledValue,
|
|
77
|
+
defaultValue,
|
|
78
|
+
onValueChange,
|
|
79
|
+
placeholder = "Select an option...",
|
|
80
|
+
label,
|
|
81
|
+
helperText,
|
|
82
|
+
error,
|
|
83
|
+
disabled = false,
|
|
84
|
+
searchable = false,
|
|
85
|
+
variant,
|
|
86
|
+
size,
|
|
87
|
+
className,
|
|
88
|
+
children,
|
|
89
|
+
}) => {
|
|
90
|
+
const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
|
|
91
|
+
const [open, setOpen] = React.useState(false);
|
|
92
|
+
const [searchQuery, setSearchQuery] = React.useState("");
|
|
93
|
+
|
|
94
|
+
const isControlled = controlledValue !== undefined;
|
|
95
|
+
const currentValue = isControlled ? controlledValue : uncontrolledValue;
|
|
96
|
+
|
|
97
|
+
const handleSelect = React.useCallback(
|
|
98
|
+
(newVal: string) => {
|
|
99
|
+
if (!isControlled) {
|
|
100
|
+
setUncontrolledValue(newVal);
|
|
101
|
+
}
|
|
102
|
+
onValueChange?.(newVal);
|
|
103
|
+
setOpen(false);
|
|
104
|
+
setSearchQuery("");
|
|
105
|
+
},
|
|
106
|
+
[isControlled, onValueChange]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const selectedOption = options?.find((o) => o.value === currentValue);
|
|
110
|
+
const displayLabel = selectedOption ? selectedOption.label : "";
|
|
111
|
+
|
|
112
|
+
const filteredOptions = React.useMemo(() => {
|
|
113
|
+
if (!options) return [];
|
|
114
|
+
if (!searchQuery) return options;
|
|
115
|
+
return options.filter((o) =>
|
|
116
|
+
o.label.toLowerCase().includes(searchQuery.toLowerCase())
|
|
117
|
+
);
|
|
118
|
+
}, [options, searchQuery]);
|
|
119
|
+
|
|
120
|
+
const activeVariant = error ? "error" : variant;
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<SelectContext.Provider
|
|
124
|
+
value={{
|
|
125
|
+
open,
|
|
126
|
+
setOpen,
|
|
127
|
+
value: currentValue,
|
|
128
|
+
onSelect: handleSelect,
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<View className={cn("w-full gap-1.5", className)}>
|
|
132
|
+
{label && (
|
|
133
|
+
<Text className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{label}</Text>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{children ? (
|
|
137
|
+
children
|
|
138
|
+
) : (
|
|
139
|
+
<Pressable
|
|
140
|
+
disabled={disabled}
|
|
141
|
+
onPress={() => setOpen(true)}
|
|
142
|
+
accessibilityRole="combobox"
|
|
143
|
+
accessibilityState={{ disabled, expanded: open }}
|
|
144
|
+
className={cn(
|
|
145
|
+
selectTriggerVariants({ variant: activeVariant, size }),
|
|
146
|
+
disabled && "opacity-50"
|
|
147
|
+
)}
|
|
148
|
+
>
|
|
149
|
+
<Text
|
|
150
|
+
className={cn(
|
|
151
|
+
"text-base",
|
|
152
|
+
displayLabel
|
|
153
|
+
? "text-neutral-900 dark:text-neutral-100 font-medium"
|
|
154
|
+
: "text-neutral-500 dark:text-neutral-400 font-normal"
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
{displayLabel || placeholder}
|
|
158
|
+
</Text>
|
|
159
|
+
<Text className="text-neutral-500 dark:text-neutral-400 text-xs">▼</Text>
|
|
160
|
+
</Pressable>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
{error ? (
|
|
164
|
+
<Text className="text-xs text-danger font-medium">{error}</Text>
|
|
165
|
+
) : helperText ? (
|
|
166
|
+
<Text className="text-xs text-neutral-500 dark:text-neutral-400">{helperText}</Text>
|
|
167
|
+
) : null}
|
|
168
|
+
|
|
169
|
+
{/* Modal Options Picker if options prop was passed */}
|
|
170
|
+
{options && (
|
|
171
|
+
<Modal
|
|
172
|
+
transparent
|
|
173
|
+
visible={open}
|
|
174
|
+
animationType="fade"
|
|
175
|
+
onRequestClose={() => setOpen(false)}
|
|
176
|
+
statusBarTranslucent
|
|
177
|
+
>
|
|
178
|
+
<View className="flex-1 justify-end bg-black/60">
|
|
179
|
+
<Pressable className="flex-1" onPress={() => setOpen(false)} />
|
|
180
|
+
<View className="w-full bg-white dark:bg-neutral-900 rounded-t-3xl border-t border-neutral-200 dark:border-neutral-800 p-6 max-h-[75%]">
|
|
181
|
+
<View className="flex-row items-center justify-between pb-4 border-b border-neutral-200 dark:border-neutral-800 mb-3">
|
|
182
|
+
<Text className="text-lg font-bold text-neutral-900 dark:text-neutral-50">
|
|
183
|
+
{label || placeholder}
|
|
184
|
+
</Text>
|
|
185
|
+
<Pressable onPress={() => setOpen(false)} className="p-1">
|
|
186
|
+
<Text className="text-neutral-500 dark:text-neutral-400 text-lg font-semibold">✕</Text>
|
|
187
|
+
</Pressable>
|
|
188
|
+
</View>
|
|
189
|
+
|
|
190
|
+
{searchable && (
|
|
191
|
+
<View className="mb-4">
|
|
192
|
+
<TextInput
|
|
193
|
+
value={searchQuery}
|
|
194
|
+
onChangeText={setSearchQuery}
|
|
195
|
+
placeholder="Search options..."
|
|
196
|
+
placeholderTextColor="#9ca3af"
|
|
197
|
+
className="h-11 px-4 rounded-xl border border-neutral-300 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 text-base"
|
|
198
|
+
/>
|
|
199
|
+
</View>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
<ScrollView className="max-h-80" showsVerticalScrollIndicator={false}>
|
|
203
|
+
{filteredOptions.length === 0 ? (
|
|
204
|
+
<View className="py-8 items-center">
|
|
205
|
+
<Text className="text-neutral-500 dark:text-neutral-400 text-sm">No options found</Text>
|
|
206
|
+
</View>
|
|
207
|
+
) : (
|
|
208
|
+
filteredOptions.map((opt) => {
|
|
209
|
+
const isSelected = opt.value === currentValue;
|
|
210
|
+
return (
|
|
211
|
+
<Pressable
|
|
212
|
+
key={opt.value}
|
|
213
|
+
disabled={opt.disabled}
|
|
214
|
+
onPress={() => handleSelect(opt.value)}
|
|
215
|
+
className={cn(
|
|
216
|
+
"flex-row items-center justify-between py-3.5 px-3 rounded-xl active:bg-neutral-100 dark:active:bg-neutral-800",
|
|
217
|
+
isSelected && "bg-primary-50 dark:bg-primary-950/40",
|
|
218
|
+
opt.disabled && "opacity-40"
|
|
219
|
+
)}
|
|
220
|
+
>
|
|
221
|
+
<Text
|
|
222
|
+
className={cn(
|
|
223
|
+
"text-base",
|
|
224
|
+
isSelected
|
|
225
|
+
? "text-primary-600 dark:text-primary-400 font-semibold"
|
|
226
|
+
: "text-neutral-900 dark:text-neutral-100 font-normal"
|
|
227
|
+
)}
|
|
228
|
+
>
|
|
229
|
+
{opt.label}
|
|
230
|
+
</Text>
|
|
231
|
+
{isSelected && (
|
|
232
|
+
<Text className="text-primary-600 dark:text-primary-400 font-bold text-base">✓</Text>
|
|
233
|
+
)}
|
|
234
|
+
</Pressable>
|
|
235
|
+
);
|
|
236
|
+
})
|
|
237
|
+
)}
|
|
238
|
+
</ScrollView>
|
|
239
|
+
</View>
|
|
240
|
+
</View>
|
|
241
|
+
</Modal>
|
|
242
|
+
)}
|
|
243
|
+
</View>
|
|
244
|
+
</SelectContext.Provider>
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
Select.displayName = "Select";
|
|
248
|
+
|
|
249
|
+
// --- Compound Subcomponents ---
|
|
250
|
+
|
|
251
|
+
export interface SelectTriggerProps extends ViewProps {
|
|
252
|
+
className?: string;
|
|
253
|
+
children: React.ReactNode;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export const SelectTrigger: React.FC<SelectTriggerProps> = ({
|
|
257
|
+
className,
|
|
258
|
+
children,
|
|
259
|
+
...props
|
|
260
|
+
}) => {
|
|
261
|
+
const { setOpen, open } = useSelectContext();
|
|
262
|
+
return (
|
|
263
|
+
<Pressable
|
|
264
|
+
onPress={() => setOpen(true)}
|
|
265
|
+
accessibilityRole="combobox"
|
|
266
|
+
accessibilityState={{ expanded: open }}
|
|
267
|
+
className={cn(
|
|
268
|
+
"flex-row items-center justify-between rounded-xl border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-4 py-3 active:opacity-80",
|
|
269
|
+
className
|
|
270
|
+
)}
|
|
271
|
+
{...props}
|
|
272
|
+
>
|
|
273
|
+
{children}
|
|
274
|
+
<Text className="text-neutral-500 dark:text-neutral-400 text-xs">▼</Text>
|
|
275
|
+
</Pressable>
|
|
276
|
+
);
|
|
277
|
+
};
|
|
278
|
+
SelectTrigger.displayName = "SelectTrigger";
|
|
279
|
+
|
|
280
|
+
export interface SelectValueProps {
|
|
281
|
+
placeholder?: string;
|
|
282
|
+
className?: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export const SelectValue: React.FC<SelectValueProps> = ({
|
|
286
|
+
placeholder = "Select...",
|
|
287
|
+
className,
|
|
288
|
+
}) => {
|
|
289
|
+
const { value } = useSelectContext();
|
|
290
|
+
return (
|
|
291
|
+
<Text
|
|
292
|
+
className={cn(
|
|
293
|
+
"text-base",
|
|
294
|
+
value ? "text-neutral-900 dark:text-neutral-100 font-medium" : "text-neutral-500 dark:text-neutral-400",
|
|
295
|
+
className
|
|
296
|
+
)}
|
|
297
|
+
>
|
|
298
|
+
{value || placeholder}
|
|
299
|
+
</Text>
|
|
300
|
+
);
|
|
301
|
+
};
|
|
302
|
+
SelectValue.displayName = "SelectValue";
|
|
303
|
+
|
|
304
|
+
export interface SelectContentProps {
|
|
305
|
+
title?: string;
|
|
306
|
+
children: React.ReactNode;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export const SelectContent: React.FC<SelectContentProps> = ({ title, children }) => {
|
|
310
|
+
const { open, setOpen } = useSelectContext();
|
|
311
|
+
if (!open) return null;
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<Modal
|
|
315
|
+
transparent
|
|
316
|
+
visible={open}
|
|
317
|
+
animationType="fade"
|
|
318
|
+
onRequestClose={() => setOpen(false)}
|
|
319
|
+
statusBarTranslucent
|
|
320
|
+
>
|
|
321
|
+
<View className="flex-1 justify-end bg-black/60">
|
|
322
|
+
<Pressable className="flex-1" onPress={() => setOpen(false)} />
|
|
323
|
+
<View className="w-full bg-white dark:bg-neutral-900 rounded-t-3xl border-t border-neutral-200 dark:border-neutral-800 p-6 max-h-[75%]">
|
|
324
|
+
<View className="flex-row items-center justify-between pb-3 border-b border-neutral-200 dark:border-neutral-800 mb-3">
|
|
325
|
+
<Text className="text-lg font-bold text-neutral-900 dark:text-neutral-100">{title || "Select"}</Text>
|
|
326
|
+
<Pressable onPress={() => setOpen(false)} className="p-1">
|
|
327
|
+
<Text className="text-neutral-500 dark:text-neutral-400 text-lg font-semibold">✕</Text>
|
|
328
|
+
</Pressable>
|
|
329
|
+
</View>
|
|
330
|
+
<ScrollView className="max-h-80">{children}</ScrollView>
|
|
331
|
+
</View>
|
|
332
|
+
</View>
|
|
333
|
+
</Modal>
|
|
334
|
+
);
|
|
335
|
+
};
|
|
336
|
+
SelectContent.displayName = "SelectContent";
|
|
337
|
+
|
|
338
|
+
export interface SelectItemProps {
|
|
339
|
+
value: string;
|
|
340
|
+
className?: string;
|
|
341
|
+
children: React.ReactNode;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export const SelectItem: React.FC<SelectItemProps> = ({
|
|
345
|
+
value: itemValue,
|
|
346
|
+
className,
|
|
347
|
+
children,
|
|
348
|
+
}) => {
|
|
349
|
+
const { value: selectedValue, onSelect } = useSelectContext();
|
|
350
|
+
const isSelected = selectedValue === itemValue;
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<Pressable
|
|
354
|
+
onPress={() => onSelect(itemValue)}
|
|
355
|
+
className={cn(
|
|
356
|
+
"flex-row items-center justify-between py-3.5 px-3 rounded-xl active:bg-neutral-100 dark:active:bg-neutral-800",
|
|
357
|
+
isSelected && "bg-primary-50 dark:bg-primary-950/40",
|
|
358
|
+
className
|
|
359
|
+
)}
|
|
360
|
+
>
|
|
361
|
+
{typeof children === "string" ? (
|
|
362
|
+
<Text
|
|
363
|
+
className={cn(
|
|
364
|
+
"text-base",
|
|
365
|
+
isSelected ? "text-primary-600 dark:text-primary-400 font-semibold" : "text-neutral-900 dark:text-neutral-100 font-normal"
|
|
366
|
+
)}
|
|
367
|
+
>
|
|
368
|
+
{children}
|
|
369
|
+
</Text>
|
|
370
|
+
) : (
|
|
371
|
+
children
|
|
372
|
+
)}
|
|
373
|
+
{isSelected && <Text className="text-primary-600 dark:text-primary-400 font-bold text-base">✓</Text>}
|
|
374
|
+
</Pressable>
|
|
375
|
+
);
|
|
376
|
+
};
|
|
377
|
+
SelectItem.displayName = "SelectItem";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./separator";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { cn } from "../../utilities/cn";
|
|
4
|
+
|
|
5
|
+
export interface SeparatorProps extends ViewProps {
|
|
6
|
+
orientation?: "horizontal" | "vertical";
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Separator({
|
|
11
|
+
orientation = "horizontal",
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
}: SeparatorProps) {
|
|
15
|
+
return (
|
|
16
|
+
<View
|
|
17
|
+
accessibilityRole="none"
|
|
18
|
+
className={cn(
|
|
19
|
+
"bg-neutral-200 dark:bg-neutral-800",
|
|
20
|
+
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Separator.displayName = "Separator";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./skeleton";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Animated, type ViewProps } from "react-native";
|
|
3
|
+
import { cn } from "../../utilities/cn";
|
|
4
|
+
|
|
5
|
+
export interface SkeletonProps extends ViewProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Skeleton({ className, style, ...props }: SkeletonProps) {
|
|
10
|
+
const pulseAnim = React.useRef(new Animated.Value(0.4)).current;
|
|
11
|
+
|
|
12
|
+
React.useEffect(() => {
|
|
13
|
+
const animation = Animated.loop(
|
|
14
|
+
Animated.sequence([
|
|
15
|
+
Animated.timing(pulseAnim, {
|
|
16
|
+
toValue: 1,
|
|
17
|
+
duration: 900,
|
|
18
|
+
useNativeDriver: true,
|
|
19
|
+
}),
|
|
20
|
+
Animated.timing(pulseAnim, {
|
|
21
|
+
toValue: 0.4,
|
|
22
|
+
duration: 900,
|
|
23
|
+
useNativeDriver: true,
|
|
24
|
+
}),
|
|
25
|
+
])
|
|
26
|
+
);
|
|
27
|
+
animation.start();
|
|
28
|
+
return () => animation.stop();
|
|
29
|
+
}, [pulseAnim]);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Animated.View
|
|
33
|
+
style={[{ opacity: pulseAnim }, style]}
|
|
34
|
+
className={cn(
|
|
35
|
+
"rounded-xl bg-neutral-200 dark:bg-neutral-800",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Skeleton.displayName = "Skeleton";
|