@beyondcorp/beyond-ui-native 0.1.2 → 0.1.3

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.
Files changed (67) hide show
  1. package/package.json +11 -5
  2. package/src/components/accordion/accordion.tsx +245 -0
  3. package/src/components/accordion/index.ts +1 -0
  4. package/src/components/alert/alert.tsx +118 -0
  5. package/src/components/alert/index.ts +1 -0
  6. package/src/components/avatar/avatar.tsx +125 -0
  7. package/src/components/avatar/index.ts +1 -0
  8. package/src/components/badge/badge.tsx +89 -0
  9. package/src/components/badge/index.ts +1 -0
  10. package/src/components/bottom-sheet/bottom-sheet.tsx +336 -0
  11. package/src/components/bottom-sheet/index.ts +1 -0
  12. package/src/components/button/button.tsx +172 -0
  13. package/src/components/button/index.ts +1 -0
  14. package/src/components/card/card.tsx +121 -0
  15. package/src/components/card/index.ts +1 -0
  16. package/src/components/checkbox/checkbox.tsx +100 -0
  17. package/src/components/checkbox/index.ts +1 -0
  18. package/src/components/data-table/data-table.tsx +277 -0
  19. package/src/components/data-table/index.ts +1 -0
  20. package/src/components/dialog/dialog.tsx +278 -0
  21. package/src/components/dialog/index.ts +1 -0
  22. package/src/components/input/index.ts +1 -0
  23. package/src/components/input/input.tsx +130 -0
  24. package/src/components/radio/index.ts +1 -0
  25. package/src/components/radio/radio.tsx +117 -0
  26. package/src/components/select/index.ts +1 -0
  27. package/src/components/select/select.tsx +375 -0
  28. package/src/components/separator/index.ts +1 -0
  29. package/src/components/separator/separator.tsx +28 -0
  30. package/src/components/skeleton/index.ts +1 -0
  31. package/src/components/skeleton/skeleton.tsx +43 -0
  32. package/src/components/spinner/index.ts +1 -0
  33. package/src/components/spinner/spinner.tsx +46 -0
  34. package/src/components/stats-card/index.ts +1 -0
  35. package/src/components/stats-card/stats-card.tsx +92 -0
  36. package/src/components/switch/index.ts +1 -0
  37. package/src/components/switch/switch.tsx +80 -0
  38. package/src/components/tabs/index.ts +1 -0
  39. package/src/components/tabs/tabs.tsx +172 -0
  40. package/src/components/text/heading.tsx +50 -0
  41. package/src/components/text/index.ts +2 -0
  42. package/src/components/text/text.tsx +62 -0
  43. package/src/components/textarea/index.ts +1 -0
  44. package/src/components/textarea/textarea.tsx +87 -0
  45. package/src/components/toast/index.ts +2 -0
  46. package/src/components/toast/toast.tsx +136 -0
  47. package/src/components/toast/types.ts +32 -0
  48. package/src/index.ts +42 -0
  49. package/src/primitives/box.tsx +25 -0
  50. package/src/primitives/index.ts +2 -0
  51. package/src/primitives/pressable.tsx +49 -0
  52. package/src/providers/BeyondUIProvider.tsx +100 -0
  53. package/src/templates/auth/ForgotPasswordTemplate.tsx +132 -0
  54. package/src/templates/auth/SignInTemplate.tsx +213 -0
  55. package/src/templates/auth/SignUpTemplate.tsx +233 -0
  56. package/src/templates/auth/index.ts +3 -0
  57. package/src/templates/dashboard/DashboardTemplate.tsx +205 -0
  58. package/src/templates/dashboard/index.ts +1 -0
  59. package/src/templates/index.ts +3 -0
  60. package/src/templates/settings/SettingsTemplate.tsx +226 -0
  61. package/src/templates/settings/index.ts +1 -0
  62. package/src/theme/preset.ts +45 -0
  63. package/src/theme/tokens.ts +144 -0
  64. package/src/types/nativewind-env.d.ts +26 -0
  65. package/src/utilities/accessibility.ts +43 -0
  66. package/src/utilities/cn.ts +9 -0
  67. 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,375 @@
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-card px-4 py-3 active:opacity-80",
16
+ {
17
+ variants: {
18
+ variant: {
19
+ default: "border-border",
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-foreground">{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 ? "text-foreground font-medium" : "text-muted"
153
+ )}
154
+ >
155
+ {displayLabel || placeholder}
156
+ </Text>
157
+ <Text className="text-muted text-xs">▼</Text>
158
+ </Pressable>
159
+ )}
160
+
161
+ {error ? (
162
+ <Text className="text-xs text-danger font-medium">{error}</Text>
163
+ ) : helperText ? (
164
+ <Text className="text-xs text-muted">{helperText}</Text>
165
+ ) : null}
166
+
167
+ {/* Modal Options Picker if options prop was passed */}
168
+ {options && (
169
+ <Modal
170
+ transparent
171
+ visible={open}
172
+ animationType="fade"
173
+ onRequestClose={() => setOpen(false)}
174
+ statusBarTranslucent
175
+ >
176
+ <View className="flex-1 justify-end bg-black/60">
177
+ <Pressable className="flex-1" onPress={() => setOpen(false)} />
178
+ <View className="w-full bg-card rounded-t-3xl border-t border-border p-6 max-h-[75%]">
179
+ <View className="flex-row items-center justify-between pb-4 border-b border-border mb-3">
180
+ <Text className="text-lg font-bold text-foreground">
181
+ {label || placeholder}
182
+ </Text>
183
+ <Pressable onPress={() => setOpen(false)} className="p-1">
184
+ <Text className="text-muted text-lg font-semibold">✕</Text>
185
+ </Pressable>
186
+ </View>
187
+
188
+ {searchable && (
189
+ <View className="mb-4">
190
+ <TextInput
191
+ value={searchQuery}
192
+ onChangeText={setSearchQuery}
193
+ placeholder="Search options..."
194
+ placeholderTextColor="#9ca3af"
195
+ className="h-11 px-4 rounded-xl border border-border bg-background text-foreground text-base"
196
+ />
197
+ </View>
198
+ )}
199
+
200
+ <ScrollView className="max-h-80" showsVerticalScrollIndicator={false}>
201
+ {filteredOptions.length === 0 ? (
202
+ <View className="py-8 items-center">
203
+ <Text className="text-muted text-sm">No options found</Text>
204
+ </View>
205
+ ) : (
206
+ filteredOptions.map((opt) => {
207
+ const isSelected = opt.value === currentValue;
208
+ return (
209
+ <Pressable
210
+ key={opt.value}
211
+ disabled={opt.disabled}
212
+ onPress={() => handleSelect(opt.value)}
213
+ className={cn(
214
+ "flex-row items-center justify-between py-3.5 px-3 rounded-xl active:bg-muted/10",
215
+ isSelected && "bg-primary/10",
216
+ opt.disabled && "opacity-40"
217
+ )}
218
+ >
219
+ <Text
220
+ className={cn(
221
+ "text-base",
222
+ isSelected
223
+ ? "text-primary font-semibold"
224
+ : "text-foreground font-normal"
225
+ )}
226
+ >
227
+ {opt.label}
228
+ </Text>
229
+ {isSelected && (
230
+ <Text className="text-primary font-bold text-base">✓</Text>
231
+ )}
232
+ </Pressable>
233
+ );
234
+ })
235
+ )}
236
+ </ScrollView>
237
+ </View>
238
+ </View>
239
+ </Modal>
240
+ )}
241
+ </View>
242
+ </SelectContext.Provider>
243
+ );
244
+ };
245
+ Select.displayName = "Select";
246
+
247
+ // --- Compound Subcomponents ---
248
+
249
+ export interface SelectTriggerProps extends ViewProps {
250
+ className?: string;
251
+ children: React.ReactNode;
252
+ }
253
+
254
+ export const SelectTrigger: React.FC<SelectTriggerProps> = ({
255
+ className,
256
+ children,
257
+ ...props
258
+ }) => {
259
+ const { setOpen, open } = useSelectContext();
260
+ return (
261
+ <Pressable
262
+ onPress={() => setOpen(true)}
263
+ accessibilityRole="combobox"
264
+ accessibilityState={{ expanded: open }}
265
+ className={cn(
266
+ "flex-row items-center justify-between rounded-xl border border-border bg-card px-4 py-3 active:opacity-80",
267
+ className
268
+ )}
269
+ {...props}
270
+ >
271
+ {children}
272
+ <Text className="text-muted text-xs">▼</Text>
273
+ </Pressable>
274
+ );
275
+ };
276
+ SelectTrigger.displayName = "SelectTrigger";
277
+
278
+ export interface SelectValueProps {
279
+ placeholder?: string;
280
+ className?: string;
281
+ }
282
+
283
+ export const SelectValue: React.FC<SelectValueProps> = ({
284
+ placeholder = "Select...",
285
+ className,
286
+ }) => {
287
+ const { value } = useSelectContext();
288
+ return (
289
+ <Text
290
+ className={cn(
291
+ "text-base",
292
+ value ? "text-foreground font-medium" : "text-muted",
293
+ className
294
+ )}
295
+ >
296
+ {value || placeholder}
297
+ </Text>
298
+ );
299
+ };
300
+ SelectValue.displayName = "SelectValue";
301
+
302
+ export interface SelectContentProps {
303
+ title?: string;
304
+ children: React.ReactNode;
305
+ }
306
+
307
+ export const SelectContent: React.FC<SelectContentProps> = ({ title, children }) => {
308
+ const { open, setOpen } = useSelectContext();
309
+ if (!open) return null;
310
+
311
+ return (
312
+ <Modal
313
+ transparent
314
+ visible={open}
315
+ animationType="fade"
316
+ onRequestClose={() => setOpen(false)}
317
+ statusBarTranslucent
318
+ >
319
+ <View className="flex-1 justify-end bg-black/60">
320
+ <Pressable className="flex-1" onPress={() => setOpen(false)} />
321
+ <View className="w-full bg-card rounded-t-3xl border-t border-border p-6 max-h-[75%]">
322
+ <View className="flex-row items-center justify-between pb-3 border-b border-border mb-3">
323
+ <Text className="text-lg font-bold text-foreground">{title || "Select"}</Text>
324
+ <Pressable onPress={() => setOpen(false)} className="p-1">
325
+ <Text className="text-muted text-lg font-semibold">✕</Text>
326
+ </Pressable>
327
+ </View>
328
+ <ScrollView className="max-h-80">{children}</ScrollView>
329
+ </View>
330
+ </View>
331
+ </Modal>
332
+ );
333
+ };
334
+ SelectContent.displayName = "SelectContent";
335
+
336
+ export interface SelectItemProps {
337
+ value: string;
338
+ className?: string;
339
+ children: React.ReactNode;
340
+ }
341
+
342
+ export const SelectItem: React.FC<SelectItemProps> = ({
343
+ value: itemValue,
344
+ className,
345
+ children,
346
+ }) => {
347
+ const { value: selectedValue, onSelect } = useSelectContext();
348
+ const isSelected = selectedValue === itemValue;
349
+
350
+ return (
351
+ <Pressable
352
+ onPress={() => onSelect(itemValue)}
353
+ className={cn(
354
+ "flex-row items-center justify-between py-3.5 px-3 rounded-xl active:bg-muted/10",
355
+ isSelected && "bg-primary/10",
356
+ className
357
+ )}
358
+ >
359
+ {typeof children === "string" ? (
360
+ <Text
361
+ className={cn(
362
+ "text-base",
363
+ isSelected ? "text-primary font-semibold" : "text-foreground font-normal"
364
+ )}
365
+ >
366
+ {children}
367
+ </Text>
368
+ ) : (
369
+ children
370
+ )}
371
+ {isSelected && <Text className="text-primary font-bold text-base">✓</Text>}
372
+ </Pressable>
373
+ );
374
+ };
375
+ 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";
@@ -0,0 +1 @@
1
+ export * from "./spinner";