@beyondcorp/beyond-ui-native 0.1.1 → 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.
- package/dist/{chunk-2B6HTSGM.mjs → chunk-RNHEGFYR.mjs} +16 -2
- package/dist/chunk-RNHEGFYR.mjs.map +1 -0
- package/dist/index.d.mts +2 -145
- package/dist/index.d.ts +2 -145
- package/dist/index.js +37 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +23 -8
- package/dist/index.mjs.map +1 -1
- package/dist/preset-BmUHLoAa.d.mts +274 -0
- package/dist/preset-BmUHLoAa.d.ts +274 -0
- package/dist/preset.d.mts +1 -117
- package/dist/preset.d.ts +1 -117
- package/dist/preset.js +19 -3
- package/dist/preset.js.map +1 -1
- package/dist/preset.mjs +5 -3
- 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 +277 -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 +375 -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 +100 -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 +205 -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
- package/dist/chunk-2B6HTSGM.mjs.map +0 -1
|
@@ -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";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ActivityIndicator, View, type ViewProps } from "react-native";
|
|
3
|
+
import { cn } from "../../utilities/cn";
|
|
4
|
+
|
|
5
|
+
export type SpinnerColor =
|
|
6
|
+
| "primary"
|
|
7
|
+
| "secondary"
|
|
8
|
+
| "danger"
|
|
9
|
+
| "success"
|
|
10
|
+
| "warning"
|
|
11
|
+
| "white"
|
|
12
|
+
| "muted";
|
|
13
|
+
|
|
14
|
+
export interface SpinnerProps extends ViewProps {
|
|
15
|
+
size?: "sm" | "md" | "lg";
|
|
16
|
+
color?: SpinnerColor;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const colorMap: Record<SpinnerColor, string> = {
|
|
21
|
+
primary: "#2563eb",
|
|
22
|
+
secondary: "#475569",
|
|
23
|
+
danger: "#dc2626",
|
|
24
|
+
success: "#16a34a",
|
|
25
|
+
warning: "#d97706",
|
|
26
|
+
white: "#ffffff",
|
|
27
|
+
muted: "#94a3b8",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function Spinner({
|
|
31
|
+
size = "md",
|
|
32
|
+
color = "primary",
|
|
33
|
+
className,
|
|
34
|
+
...props
|
|
35
|
+
}: SpinnerProps) {
|
|
36
|
+
const nativeSize = size === "lg" ? "large" : "small";
|
|
37
|
+
const hexColor = colorMap[color] ?? colorMap.primary;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View className={cn("items-center justify-center", className)} {...props}>
|
|
41
|
+
<ActivityIndicator size={nativeSize} color={hexColor} />
|
|
42
|
+
</View>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Spinner.displayName = "Spinner";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./stats-card";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { View, Text, type ViewProps } from "react-native";
|
|
3
|
+
import { cn } from "../../utilities/cn";
|
|
4
|
+
import { Card, CardContent } from "../card/card";
|
|
5
|
+
|
|
6
|
+
export type TrendDirection = "up" | "down" | "neutral";
|
|
7
|
+
|
|
8
|
+
export interface StatsCardProps extends ViewProps {
|
|
9
|
+
title: string;
|
|
10
|
+
value: string | number;
|
|
11
|
+
trend?: {
|
|
12
|
+
direction: TrendDirection;
|
|
13
|
+
value: string | number;
|
|
14
|
+
label?: string;
|
|
15
|
+
};
|
|
16
|
+
icon?: React.ReactNode;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function StatsCard({
|
|
21
|
+
title,
|
|
22
|
+
value,
|
|
23
|
+
trend,
|
|
24
|
+
icon,
|
|
25
|
+
className,
|
|
26
|
+
...props
|
|
27
|
+
}: StatsCardProps) {
|
|
28
|
+
const isUp = trend?.direction === "up";
|
|
29
|
+
const isDown = trend?.direction === "down";
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Card className={cn("w-full shadow-sm", className)} {...props}>
|
|
33
|
+
<CardContent className="p-4">
|
|
34
|
+
<View className="flex-row items-center justify-between mb-2">
|
|
35
|
+
<Text className="text-sm font-medium text-neutral-500 dark:text-neutral-400">
|
|
36
|
+
{title}
|
|
37
|
+
</Text>
|
|
38
|
+
{icon ? (
|
|
39
|
+
<View className="h-9 w-9 rounded-xl bg-neutral-100 dark:bg-neutral-800 items-center justify-center">
|
|
40
|
+
{icon}
|
|
41
|
+
</View>
|
|
42
|
+
) : null}
|
|
43
|
+
</View>
|
|
44
|
+
|
|
45
|
+
<Text className="text-2xl font-extrabold tracking-tight text-neutral-900 dark:text-neutral-50 mb-2">
|
|
46
|
+
{value}
|
|
47
|
+
</Text>
|
|
48
|
+
|
|
49
|
+
{trend ? (
|
|
50
|
+
<View className="flex-row items-center gap-1.5 flex-wrap">
|
|
51
|
+
<View
|
|
52
|
+
className={cn(
|
|
53
|
+
"flex-row items-center rounded-md px-1.5 py-0.5",
|
|
54
|
+
isUp && "bg-success-100 dark:bg-success-950/60",
|
|
55
|
+
isDown && "bg-danger-100 dark:bg-danger-950/60",
|
|
56
|
+
!isUp && !isDown && "bg-neutral-100 dark:bg-neutral-800"
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
<Text
|
|
60
|
+
className={cn(
|
|
61
|
+
"text-xs font-bold mr-1",
|
|
62
|
+
isUp && "text-success-700 dark:text-success-300",
|
|
63
|
+
isDown && "text-danger-700 dark:text-danger-300",
|
|
64
|
+
!isUp && !isDown && "text-neutral-600 dark:text-neutral-400"
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
{isUp ? "↑" : isDown ? "↓" : "—"}
|
|
68
|
+
</Text>
|
|
69
|
+
<Text
|
|
70
|
+
className={cn(
|
|
71
|
+
"text-xs font-semibold",
|
|
72
|
+
isUp && "text-success-700 dark:text-success-300",
|
|
73
|
+
isDown && "text-danger-700 dark:text-danger-300",
|
|
74
|
+
!isUp && !isDown && "text-neutral-600 dark:text-neutral-400"
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{trend.value}
|
|
78
|
+
</Text>
|
|
79
|
+
</View>
|
|
80
|
+
{trend.label ? (
|
|
81
|
+
<Text className="text-xs text-neutral-500 dark:text-neutral-400">
|
|
82
|
+
{trend.label}
|
|
83
|
+
</Text>
|
|
84
|
+
) : null}
|
|
85
|
+
</View>
|
|
86
|
+
) : null}
|
|
87
|
+
</CardContent>
|
|
88
|
+
</Card>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
StatsCard.displayName = "StatsCard";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./switch";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
View,
|
|
5
|
+
type GestureResponderEvent,
|
|
6
|
+
type PressableProps,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
import { cn } from "../../utilities/cn";
|
|
9
|
+
|
|
10
|
+
export interface SwitchProps extends Omit<PressableProps, "onPress"> {
|
|
11
|
+
checked?: boolean;
|
|
12
|
+
defaultChecked?: boolean;
|
|
13
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
thumbClassName?: string;
|
|
17
|
+
onPress?: (event: GestureResponderEvent) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Switch = React.forwardRef<
|
|
21
|
+
React.ElementRef<typeof Pressable>,
|
|
22
|
+
SwitchProps
|
|
23
|
+
>(function Switch(
|
|
24
|
+
{
|
|
25
|
+
checked: controlledChecked,
|
|
26
|
+
defaultChecked = false,
|
|
27
|
+
onCheckedChange,
|
|
28
|
+
disabled = false,
|
|
29
|
+
className,
|
|
30
|
+
thumbClassName,
|
|
31
|
+
onPress,
|
|
32
|
+
...props
|
|
33
|
+
},
|
|
34
|
+
ref
|
|
35
|
+
) {
|
|
36
|
+
const [uncontrolledChecked, setUncontrolledChecked] = React.useState(defaultChecked);
|
|
37
|
+
const isControlled = controlledChecked !== undefined;
|
|
38
|
+
const isChecked = isControlled ? controlledChecked : uncontrolledChecked;
|
|
39
|
+
|
|
40
|
+
const handlePress = (e: GestureResponderEvent) => {
|
|
41
|
+
if (disabled) return;
|
|
42
|
+
const nextChecked = !isChecked;
|
|
43
|
+
if (!isControlled) {
|
|
44
|
+
setUncontrolledChecked(nextChecked);
|
|
45
|
+
}
|
|
46
|
+
onCheckedChange?.(nextChecked);
|
|
47
|
+
onPress?.(e);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Pressable
|
|
52
|
+
ref={ref}
|
|
53
|
+
role="switch"
|
|
54
|
+
accessibilityRole="switch"
|
|
55
|
+
accessibilityState={{ checked: isChecked, disabled: !!disabled }}
|
|
56
|
+
disabled={disabled}
|
|
57
|
+
onPress={handlePress}
|
|
58
|
+
className={cn(
|
|
59
|
+
"h-7 w-12 shrink-0 flex-row items-center rounded-full p-0.5 transition-colors",
|
|
60
|
+
isChecked
|
|
61
|
+
? "bg-primary-600 dark:bg-primary-600"
|
|
62
|
+
: "bg-neutral-300 dark:bg-neutral-700",
|
|
63
|
+
disabled && "opacity-50",
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
{...props}
|
|
67
|
+
>
|
|
68
|
+
<View
|
|
69
|
+
role="presentation"
|
|
70
|
+
className={cn(
|
|
71
|
+
"h-6 w-6 rounded-full bg-white shadow-sm transition-transform",
|
|
72
|
+
isChecked ? "translate-x-5" : "translate-x-0",
|
|
73
|
+
thumbClassName
|
|
74
|
+
)}
|
|
75
|
+
/>
|
|
76
|
+
</Pressable>
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
Switch.displayName = "Switch";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./tabs";
|