@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.
- 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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
Pressable,
|
|
6
|
+
type PressableProps,
|
|
7
|
+
type GestureResponderEvent,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
import { cn } from "../../utilities/cn";
|
|
10
|
+
|
|
11
|
+
export interface CheckboxProps extends Omit<PressableProps, "onPress"> {
|
|
12
|
+
checked?: boolean;
|
|
13
|
+
defaultChecked?: boolean;
|
|
14
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
indicatorClassName?: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
labelClassName?: string;
|
|
20
|
+
onPress?: (e: GestureResponderEvent) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Checkbox = React.forwardRef<
|
|
24
|
+
React.ElementRef<typeof Pressable>,
|
|
25
|
+
CheckboxProps
|
|
26
|
+
>(function Checkbox(
|
|
27
|
+
{
|
|
28
|
+
checked: controlledChecked,
|
|
29
|
+
defaultChecked = false,
|
|
30
|
+
onCheckedChange,
|
|
31
|
+
disabled = false,
|
|
32
|
+
className,
|
|
33
|
+
indicatorClassName,
|
|
34
|
+
label,
|
|
35
|
+
labelClassName,
|
|
36
|
+
onPress,
|
|
37
|
+
...props
|
|
38
|
+
},
|
|
39
|
+
ref
|
|
40
|
+
) {
|
|
41
|
+
const [uncontrolledChecked, setUncontrolledChecked] = React.useState(defaultChecked);
|
|
42
|
+
const isControlled = controlledChecked !== undefined;
|
|
43
|
+
const isChecked = isControlled ? controlledChecked : uncontrolledChecked;
|
|
44
|
+
|
|
45
|
+
const handlePress = (e: GestureResponderEvent) => {
|
|
46
|
+
if (disabled) return;
|
|
47
|
+
const nextChecked = !isChecked;
|
|
48
|
+
if (!isControlled) {
|
|
49
|
+
setUncontrolledChecked(nextChecked);
|
|
50
|
+
}
|
|
51
|
+
onCheckedChange?.(nextChecked);
|
|
52
|
+
onPress?.(e);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const content = (
|
|
56
|
+
<Pressable
|
|
57
|
+
ref={ref}
|
|
58
|
+
role="checkbox"
|
|
59
|
+
accessibilityRole="checkbox"
|
|
60
|
+
accessibilityState={{ checked: isChecked, disabled: !!disabled }}
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
onPress={handlePress}
|
|
63
|
+
className={cn(
|
|
64
|
+
"h-6 w-6 shrink-0 items-center justify-center rounded-md border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-900",
|
|
65
|
+
isChecked && "bg-primary-600 border-primary-600 dark:bg-primary-600 dark:border-primary-600",
|
|
66
|
+
disabled && "opacity-50",
|
|
67
|
+
className
|
|
68
|
+
)}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{isChecked ? (
|
|
72
|
+
<View className={cn("items-center justify-center", indicatorClassName)}>
|
|
73
|
+
<Text className="text-white text-xs font-bold leading-none">✓</Text>
|
|
74
|
+
</View>
|
|
75
|
+
) : null}
|
|
76
|
+
</Pressable>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (!label) {
|
|
80
|
+
return content;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<View className="flex-row items-center gap-2.5">
|
|
85
|
+
{content}
|
|
86
|
+
<Text
|
|
87
|
+
onPress={!disabled ? handlePress : undefined}
|
|
88
|
+
className={cn(
|
|
89
|
+
"text-sm font-medium text-neutral-800 dark:text-neutral-200",
|
|
90
|
+
disabled && "opacity-50",
|
|
91
|
+
labelClassName
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
{label}
|
|
95
|
+
</Text>
|
|
96
|
+
</View>
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
Checkbox.displayName = "Checkbox";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./checkbox";
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
ScrollView,
|
|
5
|
+
Pressable,
|
|
6
|
+
type ViewProps,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
import { cn } from "../../utilities/cn";
|
|
9
|
+
import { Text } from "../text/text";
|
|
10
|
+
import { Input } from "../input/input";
|
|
11
|
+
import { Button } from "../button/button";
|
|
12
|
+
import { Card, CardContent } from "../card/card";
|
|
13
|
+
import { Skeleton } from "../skeleton/skeleton";
|
|
14
|
+
|
|
15
|
+
export interface Column<T> {
|
|
16
|
+
key: keyof T | string;
|
|
17
|
+
title: string;
|
|
18
|
+
sortable?: boolean;
|
|
19
|
+
align?: "left" | "center" | "right";
|
|
20
|
+
width?: number;
|
|
21
|
+
render?: (value: any, item: T) => React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DataTableProps<T extends Record<string, any>> extends ViewProps {
|
|
25
|
+
columns: Column<T>[];
|
|
26
|
+
data: T[];
|
|
27
|
+
keyExtractor?: (item: T, index: number) => string;
|
|
28
|
+
mode?: "table" | "card";
|
|
29
|
+
searchable?: boolean;
|
|
30
|
+
searchPlaceholder?: string;
|
|
31
|
+
searchKeys?: (keyof T)[];
|
|
32
|
+
loading?: boolean;
|
|
33
|
+
pageSize?: number;
|
|
34
|
+
emptyText?: string;
|
|
35
|
+
onRowPress?: (item: T) => void;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function DataTable<T extends Record<string, any>>({
|
|
40
|
+
columns,
|
|
41
|
+
data,
|
|
42
|
+
keyExtractor = (_, i) => String(i),
|
|
43
|
+
mode = "card",
|
|
44
|
+
searchable = true,
|
|
45
|
+
searchPlaceholder = "Search records...",
|
|
46
|
+
searchKeys,
|
|
47
|
+
loading = false,
|
|
48
|
+
pageSize = 5,
|
|
49
|
+
emptyText = "No records found",
|
|
50
|
+
onRowPress,
|
|
51
|
+
className,
|
|
52
|
+
...props
|
|
53
|
+
}: DataTableProps<T>) {
|
|
54
|
+
const [search, setSearch] = React.useState("");
|
|
55
|
+
const [sortKey, setSortKey] = React.useState<string | null>(null);
|
|
56
|
+
const [sortDirection, setSortDirection] = React.useState<"asc" | "desc">("asc");
|
|
57
|
+
const [currentPage, setCurrentPage] = React.useState(1);
|
|
58
|
+
|
|
59
|
+
// Filter
|
|
60
|
+
const filteredData = React.useMemo(() => {
|
|
61
|
+
if (!search.trim()) return data;
|
|
62
|
+
const query = search.toLowerCase();
|
|
63
|
+
const keys = searchKeys || (columns.map((c) => c.key) as (keyof T)[]);
|
|
64
|
+
|
|
65
|
+
return data.filter((item) => {
|
|
66
|
+
return keys.some((k) => {
|
|
67
|
+
const val = item[k];
|
|
68
|
+
if (val == null) return false;
|
|
69
|
+
return String(val).toLowerCase().includes(query);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}, [data, search, searchKeys, columns]);
|
|
73
|
+
|
|
74
|
+
// Sort
|
|
75
|
+
const sortedData = React.useMemo(() => {
|
|
76
|
+
if (!sortKey) return filteredData;
|
|
77
|
+
return [...filteredData].sort((a, b) => {
|
|
78
|
+
const valA = a[sortKey];
|
|
79
|
+
const valB = b[sortKey];
|
|
80
|
+
if (valA === valB) return 0;
|
|
81
|
+
if (valA == null) return 1;
|
|
82
|
+
if (valB == null) return -1;
|
|
83
|
+
|
|
84
|
+
if (typeof valA === "number" && typeof valB === "number") {
|
|
85
|
+
return sortDirection === "asc" ? valA - valB : valB - valA;
|
|
86
|
+
}
|
|
87
|
+
const strA = String(valA).toLowerCase();
|
|
88
|
+
const strB = String(valB).toLowerCase();
|
|
89
|
+
return sortDirection === "asc"
|
|
90
|
+
? strA.localeCompare(strB)
|
|
91
|
+
: strB.localeCompare(strA);
|
|
92
|
+
});
|
|
93
|
+
}, [filteredData, sortKey, sortDirection]);
|
|
94
|
+
|
|
95
|
+
// Pagination
|
|
96
|
+
const totalPages = Math.max(1, Math.ceil(sortedData.length / pageSize));
|
|
97
|
+
const paginatedData = React.useMemo(() => {
|
|
98
|
+
const start = (currentPage - 1) * pageSize;
|
|
99
|
+
return sortedData.slice(start, start + pageSize);
|
|
100
|
+
}, [sortedData, currentPage, pageSize]);
|
|
101
|
+
|
|
102
|
+
const handleSort = (colKey: string) => {
|
|
103
|
+
if (sortKey === colKey) {
|
|
104
|
+
if (sortDirection === "asc") {
|
|
105
|
+
setSortDirection("desc");
|
|
106
|
+
} else {
|
|
107
|
+
setSortKey(null);
|
|
108
|
+
setSortDirection("asc");
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
setSortKey(colKey);
|
|
112
|
+
setSortDirection("asc");
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<View className={cn("w-full gap-4", className)} {...props}>
|
|
118
|
+
{/* Search Input */}
|
|
119
|
+
{searchable && (
|
|
120
|
+
<Input
|
|
121
|
+
placeholder={searchPlaceholder}
|
|
122
|
+
value={search}
|
|
123
|
+
onChangeText={(val) => {
|
|
124
|
+
setSearch(val);
|
|
125
|
+
setCurrentPage(1);
|
|
126
|
+
}}
|
|
127
|
+
className="bg-card"
|
|
128
|
+
/>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Loading Skeleton */}
|
|
132
|
+
{loading ? (
|
|
133
|
+
<View className="gap-3">
|
|
134
|
+
{[1, 2, 3].map((idx) => (
|
|
135
|
+
<Skeleton key={idx} className="h-20 w-full rounded-2xl" />
|
|
136
|
+
))}
|
|
137
|
+
</View>
|
|
138
|
+
) : paginatedData.length === 0 ? (
|
|
139
|
+
<Card className="items-center justify-center p-8">
|
|
140
|
+
<Text className="text-muted text-base">{emptyText}</Text>
|
|
141
|
+
</Card>
|
|
142
|
+
) : mode === "card" ? (
|
|
143
|
+
/* Mobile Card Mode */
|
|
144
|
+
<View className="gap-3">
|
|
145
|
+
{paginatedData.map((item, idx) => (
|
|
146
|
+
<Pressable
|
|
147
|
+
key={keyExtractor(item, idx)}
|
|
148
|
+
disabled={!onRowPress}
|
|
149
|
+
onPress={() => onRowPress?.(item)}
|
|
150
|
+
className="active:opacity-80"
|
|
151
|
+
>
|
|
152
|
+
<Card className="p-4 gap-2">
|
|
153
|
+
{columns.map((col) => {
|
|
154
|
+
const rawVal = item[col.key as string];
|
|
155
|
+
const renderedVal = col.render ? col.render(rawVal, item) : String(rawVal ?? "—");
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<View
|
|
159
|
+
key={String(col.key)}
|
|
160
|
+
className="flex-row items-center justify-between py-1 border-b border-border/40 last:border-b-0"
|
|
161
|
+
>
|
|
162
|
+
<Text className="text-xs font-semibold text-muted uppercase tracking-wider">
|
|
163
|
+
{col.title}
|
|
164
|
+
</Text>
|
|
165
|
+
<View className="shrink">
|
|
166
|
+
{typeof renderedVal === "string" ? (
|
|
167
|
+
<Text className="text-sm font-medium text-foreground">
|
|
168
|
+
{renderedVal}
|
|
169
|
+
</Text>
|
|
170
|
+
) : (
|
|
171
|
+
renderedVal
|
|
172
|
+
)}
|
|
173
|
+
</View>
|
|
174
|
+
</View>
|
|
175
|
+
);
|
|
176
|
+
})}
|
|
177
|
+
</Card>
|
|
178
|
+
</Pressable>
|
|
179
|
+
))}
|
|
180
|
+
</View>
|
|
181
|
+
) : (
|
|
182
|
+
/* Horizontal Table Mode */
|
|
183
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
184
|
+
<View className="rounded-2xl border border-border bg-card overflow-hidden">
|
|
185
|
+
{/* Header */}
|
|
186
|
+
<View className="flex-row bg-muted/10 border-b border-border">
|
|
187
|
+
{columns.map((col) => (
|
|
188
|
+
<Pressable
|
|
189
|
+
key={String(col.key)}
|
|
190
|
+
disabled={!col.sortable}
|
|
191
|
+
onPress={() => handleSort(String(col.key))}
|
|
192
|
+
style={{ width: col.width || 120 }}
|
|
193
|
+
className="px-4 py-3 flex-row items-center justify-between"
|
|
194
|
+
>
|
|
195
|
+
<Text className="text-xs font-bold text-foreground uppercase tracking-wider">
|
|
196
|
+
{col.title}
|
|
197
|
+
</Text>
|
|
198
|
+
{col.sortable && (
|
|
199
|
+
<Text className="text-xs text-muted">
|
|
200
|
+
{sortKey === col.key
|
|
201
|
+
? sortDirection === "asc"
|
|
202
|
+
? "▲"
|
|
203
|
+
: "▼"
|
|
204
|
+
: "⇅"}
|
|
205
|
+
</Text>
|
|
206
|
+
)}
|
|
207
|
+
</Pressable>
|
|
208
|
+
))}
|
|
209
|
+
</View>
|
|
210
|
+
|
|
211
|
+
{/* Rows */}
|
|
212
|
+
{paginatedData.map((item, idx) => (
|
|
213
|
+
<Pressable
|
|
214
|
+
key={keyExtractor(item, idx)}
|
|
215
|
+
disabled={!onRowPress}
|
|
216
|
+
onPress={() => onRowPress?.(item)}
|
|
217
|
+
className="flex-row border-b border-border/60 active:bg-muted/10"
|
|
218
|
+
>
|
|
219
|
+
{columns.map((col) => {
|
|
220
|
+
const rawVal = item[col.key as string];
|
|
221
|
+
const renderedVal = col.render ? col.render(rawVal, item) : String(rawVal ?? "—");
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<View
|
|
225
|
+
key={String(col.key)}
|
|
226
|
+
style={{ width: col.width || 120 }}
|
|
227
|
+
className="px-4 py-3 justify-center"
|
|
228
|
+
>
|
|
229
|
+
{typeof renderedVal === "string" ? (
|
|
230
|
+
<Text className="text-sm text-foreground">{renderedVal}</Text>
|
|
231
|
+
) : (
|
|
232
|
+
renderedVal
|
|
233
|
+
)}
|
|
234
|
+
</View>
|
|
235
|
+
);
|
|
236
|
+
})}
|
|
237
|
+
</Pressable>
|
|
238
|
+
))}
|
|
239
|
+
</View>
|
|
240
|
+
</ScrollView>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{/* Pagination Footer */}
|
|
244
|
+
{totalPages > 1 && (
|
|
245
|
+
<View className="flex-row items-center justify-between pt-2">
|
|
246
|
+
<Text className="text-xs text-muted">
|
|
247
|
+
Showing {(currentPage - 1) * pageSize + 1} -{" "}
|
|
248
|
+
{Math.min(currentPage * pageSize, sortedData.length)} of{" "}
|
|
249
|
+
{sortedData.length}
|
|
250
|
+
</Text>
|
|
251
|
+
|
|
252
|
+
<View className="flex-row items-center gap-2">
|
|
253
|
+
<Button
|
|
254
|
+
variant="outline"
|
|
255
|
+
size="sm"
|
|
256
|
+
disabled={currentPage <= 1}
|
|
257
|
+
onPress={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
|
258
|
+
>
|
|
259
|
+
Prev
|
|
260
|
+
</Button>
|
|
261
|
+
<Text className="text-xs font-medium text-foreground px-1">
|
|
262
|
+
{currentPage} / {totalPages}
|
|
263
|
+
</Text>
|
|
264
|
+
<Button
|
|
265
|
+
variant="outline"
|
|
266
|
+
size="sm"
|
|
267
|
+
disabled={currentPage >= totalPages}
|
|
268
|
+
onPress={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
|
269
|
+
>
|
|
270
|
+
Next
|
|
271
|
+
</Button>
|
|
272
|
+
</View>
|
|
273
|
+
</View>
|
|
274
|
+
)}
|
|
275
|
+
</View>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./data-table";
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
View,
|
|
5
|
+
Text,
|
|
6
|
+
Pressable,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
type ViewProps,
|
|
9
|
+
type TextProps,
|
|
10
|
+
type PressableProps,
|
|
11
|
+
type GestureResponderEvent,
|
|
12
|
+
} from "react-native";
|
|
13
|
+
import { cn } from "../../utilities/cn";
|
|
14
|
+
|
|
15
|
+
export interface DialogContextValue {
|
|
16
|
+
open: boolean;
|
|
17
|
+
onOpenChange: (open: boolean) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DialogContext = React.createContext<DialogContextValue | null>(null);
|
|
21
|
+
|
|
22
|
+
export function useDialogContext(): DialogContextValue {
|
|
23
|
+
const context = React.useContext(DialogContext);
|
|
24
|
+
if (!context) {
|
|
25
|
+
throw new Error("Dialog compound components must be used within a Dialog component");
|
|
26
|
+
}
|
|
27
|
+
return context;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DialogProps {
|
|
31
|
+
open?: boolean;
|
|
32
|
+
defaultOpen?: boolean;
|
|
33
|
+
onOpenChange?: (open: boolean) => void;
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function Dialog({
|
|
38
|
+
open: controlledOpen,
|
|
39
|
+
defaultOpen = false,
|
|
40
|
+
onOpenChange,
|
|
41
|
+
children,
|
|
42
|
+
}: DialogProps) {
|
|
43
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
|
|
44
|
+
const isControlled = controlledOpen !== undefined;
|
|
45
|
+
const open = isControlled ? controlledOpen : uncontrolledOpen;
|
|
46
|
+
|
|
47
|
+
const handleOpenChange = React.useCallback(
|
|
48
|
+
(nextOpen: boolean) => {
|
|
49
|
+
if (!isControlled) {
|
|
50
|
+
setUncontrolledOpen(nextOpen);
|
|
51
|
+
}
|
|
52
|
+
onOpenChange?.(nextOpen);
|
|
53
|
+
},
|
|
54
|
+
[isControlled, onOpenChange]
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<DialogContext.Provider value={{ open, onOpenChange: handleOpenChange }}>
|
|
59
|
+
{children}
|
|
60
|
+
</DialogContext.Provider>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
Dialog.displayName = "Dialog";
|
|
64
|
+
|
|
65
|
+
export interface DialogTriggerProps extends PressableProps {
|
|
66
|
+
asChild?: boolean;
|
|
67
|
+
className?: string;
|
|
68
|
+
children: React.ReactNode;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const DialogTrigger = React.forwardRef<View, DialogTriggerProps>(
|
|
72
|
+
function DialogTrigger({ asChild, children, onPress, ...props }, ref) {
|
|
73
|
+
const { open, onOpenChange } = useDialogContext();
|
|
74
|
+
|
|
75
|
+
const handlePress = (e: GestureResponderEvent) => {
|
|
76
|
+
onOpenChange(!open);
|
|
77
|
+
onPress?.(e);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (asChild && React.isValidElement(children)) {
|
|
81
|
+
return React.cloneElement(children as React.ReactElement<any>, {
|
|
82
|
+
ref,
|
|
83
|
+
onPress: (e: GestureResponderEvent) => {
|
|
84
|
+
(children.props as any)?.onPress?.(e);
|
|
85
|
+
handlePress(e);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Pressable ref={ref} onPress={handlePress} role="button" {...props}>
|
|
92
|
+
{children}
|
|
93
|
+
</Pressable>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
DialogTrigger.displayName = "DialogTrigger";
|
|
98
|
+
|
|
99
|
+
export interface DialogPortalProps {
|
|
100
|
+
children: React.ReactNode;
|
|
101
|
+
forceMount?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function DialogPortal({ children, forceMount }: DialogPortalProps) {
|
|
105
|
+
const { open, onOpenChange } = useDialogContext();
|
|
106
|
+
|
|
107
|
+
if (!open && !forceMount) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Modal
|
|
113
|
+
transparent
|
|
114
|
+
visible={open}
|
|
115
|
+
animationType="fade"
|
|
116
|
+
onRequestClose={() => onOpenChange(false)}
|
|
117
|
+
statusBarTranslucent
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
</Modal>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
DialogPortal.displayName = "DialogPortal";
|
|
124
|
+
|
|
125
|
+
export interface DialogOverlayProps extends ViewProps {
|
|
126
|
+
className?: string;
|
|
127
|
+
closeOnPress?: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const DialogOverlay = React.forwardRef<View, DialogOverlayProps>(
|
|
131
|
+
function DialogOverlay({ className, closeOnPress = true, ...props }, ref) {
|
|
132
|
+
const { onOpenChange } = useDialogContext();
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<Pressable
|
|
136
|
+
ref={ref}
|
|
137
|
+
style={StyleSheet.absoluteFill}
|
|
138
|
+
className={cn("z-50 bg-black/60", className)}
|
|
139
|
+
onPress={() => {
|
|
140
|
+
if (closeOnPress) {
|
|
141
|
+
onOpenChange(false);
|
|
142
|
+
}
|
|
143
|
+
}}
|
|
144
|
+
{...props}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
DialogOverlay.displayName = "DialogOverlay";
|
|
150
|
+
|
|
151
|
+
export interface DialogContentProps extends ViewProps {
|
|
152
|
+
className?: string;
|
|
153
|
+
overlayClassName?: string;
|
|
154
|
+
children: React.ReactNode;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const DialogContent = React.forwardRef<View, DialogContentProps>(
|
|
158
|
+
function DialogContent({ className, overlayClassName, children, ...props }, ref) {
|
|
159
|
+
return (
|
|
160
|
+
<DialogPortal>
|
|
161
|
+
<View
|
|
162
|
+
pointerEvents="box-none"
|
|
163
|
+
style={StyleSheet.absoluteFill}
|
|
164
|
+
className="z-50 items-center justify-center p-4"
|
|
165
|
+
>
|
|
166
|
+
<DialogOverlay className={overlayClassName} />
|
|
167
|
+
<View
|
|
168
|
+
ref={ref}
|
|
169
|
+
role="dialog"
|
|
170
|
+
aria-modal={true}
|
|
171
|
+
className={cn(
|
|
172
|
+
"z-50 w-full max-w-lg rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-2xl",
|
|
173
|
+
className
|
|
174
|
+
)}
|
|
175
|
+
{...props}
|
|
176
|
+
>
|
|
177
|
+
{children}
|
|
178
|
+
</View>
|
|
179
|
+
</View>
|
|
180
|
+
</DialogPortal>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
DialogContent.displayName = "DialogContent";
|
|
185
|
+
|
|
186
|
+
export function DialogHeader({
|
|
187
|
+
className,
|
|
188
|
+
...props
|
|
189
|
+
}: ViewProps & { className?: string }) {
|
|
190
|
+
return (
|
|
191
|
+
<View
|
|
192
|
+
className={cn("flex-col space-y-2 text-center sm:text-left", className)}
|
|
193
|
+
{...props}
|
|
194
|
+
/>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
DialogHeader.displayName = "DialogHeader";
|
|
198
|
+
|
|
199
|
+
export const DialogTitle = React.forwardRef<Text, TextProps & { className?: string }>(
|
|
200
|
+
function DialogTitle({ className, ...props }, ref) {
|
|
201
|
+
return (
|
|
202
|
+
<Text
|
|
203
|
+
ref={ref}
|
|
204
|
+
role="heading"
|
|
205
|
+
aria-level={2}
|
|
206
|
+
className={cn(
|
|
207
|
+
"text-xl font-bold tracking-tight text-neutral-900 dark:text-neutral-50",
|
|
208
|
+
className
|
|
209
|
+
)}
|
|
210
|
+
{...props}
|
|
211
|
+
/>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
DialogTitle.displayName = "DialogTitle";
|
|
216
|
+
|
|
217
|
+
export const DialogDescription = React.forwardRef<Text, TextProps & { className?: string }>(
|
|
218
|
+
function DialogDescription({ className, ...props }, ref) {
|
|
219
|
+
return (
|
|
220
|
+
<Text
|
|
221
|
+
ref={ref}
|
|
222
|
+
className={cn(
|
|
223
|
+
"text-sm text-neutral-500 dark:text-neutral-400 mt-1",
|
|
224
|
+
className
|
|
225
|
+
)}
|
|
226
|
+
{...props}
|
|
227
|
+
/>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
DialogDescription.displayName = "DialogDescription";
|
|
232
|
+
|
|
233
|
+
export function DialogFooter({
|
|
234
|
+
className,
|
|
235
|
+
...props
|
|
236
|
+
}: ViewProps & { className?: string }) {
|
|
237
|
+
return (
|
|
238
|
+
<View
|
|
239
|
+
className={cn("flex-row justify-end space-x-2 mt-6", className)}
|
|
240
|
+
{...props}
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
DialogFooter.displayName = "DialogFooter";
|
|
245
|
+
|
|
246
|
+
export interface DialogCloseProps extends PressableProps {
|
|
247
|
+
asChild?: boolean;
|
|
248
|
+
className?: string;
|
|
249
|
+
children: React.ReactNode;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export const DialogClose = React.forwardRef<View, DialogCloseProps>(
|
|
253
|
+
function DialogClose({ asChild, children, onPress, ...props }, ref) {
|
|
254
|
+
const { onOpenChange } = useDialogContext();
|
|
255
|
+
|
|
256
|
+
const handlePress = (e: GestureResponderEvent) => {
|
|
257
|
+
onOpenChange(false);
|
|
258
|
+
onPress?.(e);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
if (asChild && React.isValidElement(children)) {
|
|
262
|
+
return React.cloneElement(children as React.ReactElement<any>, {
|
|
263
|
+
ref,
|
|
264
|
+
onPress: (e: GestureResponderEvent) => {
|
|
265
|
+
(children.props as any)?.onPress?.(e);
|
|
266
|
+
handlePress(e);
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<Pressable ref={ref} onPress={handlePress} role="button" {...props}>
|
|
273
|
+
{children}
|
|
274
|
+
</Pressable>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
DialogClose.displayName = "DialogClose";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./dialog";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./input";
|