@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.
Files changed (83) hide show
  1. package/dist/{chunk-2B6HTSGM.mjs → chunk-RNHEGFYR.mjs} +16 -2
  2. package/dist/chunk-RNHEGFYR.mjs.map +1 -0
  3. package/dist/index.d.mts +2 -145
  4. package/dist/index.d.ts +2 -145
  5. package/dist/index.js +37 -8
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +23 -8
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/preset-BmUHLoAa.d.mts +274 -0
  10. package/dist/preset-BmUHLoAa.d.ts +274 -0
  11. package/dist/preset.d.mts +1 -117
  12. package/dist/preset.d.ts +1 -117
  13. package/dist/preset.js +19 -3
  14. package/dist/preset.js.map +1 -1
  15. package/dist/preset.mjs +5 -3
  16. package/package.json +11 -5
  17. package/src/components/accordion/accordion.tsx +245 -0
  18. package/src/components/accordion/index.ts +1 -0
  19. package/src/components/alert/alert.tsx +118 -0
  20. package/src/components/alert/index.ts +1 -0
  21. package/src/components/avatar/avatar.tsx +125 -0
  22. package/src/components/avatar/index.ts +1 -0
  23. package/src/components/badge/badge.tsx +89 -0
  24. package/src/components/badge/index.ts +1 -0
  25. package/src/components/bottom-sheet/bottom-sheet.tsx +336 -0
  26. package/src/components/bottom-sheet/index.ts +1 -0
  27. package/src/components/button/button.tsx +172 -0
  28. package/src/components/button/index.ts +1 -0
  29. package/src/components/card/card.tsx +121 -0
  30. package/src/components/card/index.ts +1 -0
  31. package/src/components/checkbox/checkbox.tsx +100 -0
  32. package/src/components/checkbox/index.ts +1 -0
  33. package/src/components/data-table/data-table.tsx +277 -0
  34. package/src/components/data-table/index.ts +1 -0
  35. package/src/components/dialog/dialog.tsx +278 -0
  36. package/src/components/dialog/index.ts +1 -0
  37. package/src/components/input/index.ts +1 -0
  38. package/src/components/input/input.tsx +130 -0
  39. package/src/components/radio/index.ts +1 -0
  40. package/src/components/radio/radio.tsx +117 -0
  41. package/src/components/select/index.ts +1 -0
  42. package/src/components/select/select.tsx +375 -0
  43. package/src/components/separator/index.ts +1 -0
  44. package/src/components/separator/separator.tsx +28 -0
  45. package/src/components/skeleton/index.ts +1 -0
  46. package/src/components/skeleton/skeleton.tsx +43 -0
  47. package/src/components/spinner/index.ts +1 -0
  48. package/src/components/spinner/spinner.tsx +46 -0
  49. package/src/components/stats-card/index.ts +1 -0
  50. package/src/components/stats-card/stats-card.tsx +92 -0
  51. package/src/components/switch/index.ts +1 -0
  52. package/src/components/switch/switch.tsx +80 -0
  53. package/src/components/tabs/index.ts +1 -0
  54. package/src/components/tabs/tabs.tsx +172 -0
  55. package/src/components/text/heading.tsx +50 -0
  56. package/src/components/text/index.ts +2 -0
  57. package/src/components/text/text.tsx +62 -0
  58. package/src/components/textarea/index.ts +1 -0
  59. package/src/components/textarea/textarea.tsx +87 -0
  60. package/src/components/toast/index.ts +2 -0
  61. package/src/components/toast/toast.tsx +136 -0
  62. package/src/components/toast/types.ts +32 -0
  63. package/src/index.ts +42 -0
  64. package/src/primitives/box.tsx +25 -0
  65. package/src/primitives/index.ts +2 -0
  66. package/src/primitives/pressable.tsx +49 -0
  67. package/src/providers/BeyondUIProvider.tsx +100 -0
  68. package/src/templates/auth/ForgotPasswordTemplate.tsx +132 -0
  69. package/src/templates/auth/SignInTemplate.tsx +213 -0
  70. package/src/templates/auth/SignUpTemplate.tsx +233 -0
  71. package/src/templates/auth/index.ts +3 -0
  72. package/src/templates/dashboard/DashboardTemplate.tsx +205 -0
  73. package/src/templates/dashboard/index.ts +1 -0
  74. package/src/templates/index.ts +3 -0
  75. package/src/templates/settings/SettingsTemplate.tsx +226 -0
  76. package/src/templates/settings/index.ts +1 -0
  77. package/src/theme/preset.ts +45 -0
  78. package/src/theme/tokens.ts +144 -0
  79. package/src/types/nativewind-env.d.ts +26 -0
  80. package/src/utilities/accessibility.ts +43 -0
  81. package/src/utilities/cn.ts +9 -0
  82. package/src/utilities/index.ts +2 -0
  83. package/dist/chunk-2B6HTSGM.mjs.map +0 -1
@@ -0,0 +1,172 @@
1
+ import * as React from "react";
2
+ import {
3
+ ActivityIndicator,
4
+ Pressable,
5
+ Text,
6
+ View,
7
+ type PressableProps,
8
+ type TextProps,
9
+ } from "react-native";
10
+ import { cva, type VariantProps } from "class-variance-authority";
11
+ import { cn } from "../../utilities/cn";
12
+
13
+ export const buttonVariants = cva(
14
+ "flex-row items-center justify-center rounded-xl transition-colors",
15
+ {
16
+ variants: {
17
+ variant: {
18
+ primary: "bg-primary-600 active:bg-primary-700 dark:bg-primary-600 dark:active:bg-primary-700",
19
+ secondary: "bg-secondary-600 active:bg-secondary-700 dark:bg-secondary-600 dark:active:bg-secondary-700",
20
+ danger: "bg-danger-600 active:bg-danger-700 dark:bg-danger-600 dark:active:bg-danger-700",
21
+ destructive: "bg-danger-600 active:bg-danger-700 dark:bg-danger-600 dark:active:bg-danger-700",
22
+ success: "bg-success-600 active:bg-success-700 dark:bg-success-600 dark:active:bg-success-700",
23
+ warning: "bg-warning-600 active:bg-warning-700 dark:bg-warning-600 dark:active:bg-warning-700",
24
+ outline: "border border-neutral-300 dark:border-neutral-700 bg-transparent active:bg-neutral-100 dark:active:bg-neutral-800",
25
+ ghost: "bg-transparent active:bg-neutral-100 dark:active:bg-neutral-800",
26
+ link: "bg-transparent underline",
27
+ },
28
+ size: {
29
+ sm: "min-h-8 px-3 py-1.5 rounded-lg",
30
+ md: "min-h-10 px-4 py-2 rounded-xl",
31
+ lg: "min-h-12 px-6 py-2.5 rounded-xl",
32
+ xl: "min-h-14 px-8 py-3 rounded-2xl",
33
+ icon: "min-h-10 min-w-10 p-0 rounded-xl",
34
+ },
35
+ },
36
+ defaultVariants: {
37
+ variant: "primary",
38
+ size: "md",
39
+ },
40
+ }
41
+ );
42
+
43
+ export const buttonTextVariants = cva("font-semibold text-center select-none", {
44
+ variants: {
45
+ variant: {
46
+ primary: "text-white",
47
+ secondary: "text-white",
48
+ danger: "text-white",
49
+ destructive: "text-white",
50
+ success: "text-white",
51
+ warning: "text-white",
52
+ outline: "text-neutral-800 dark:text-neutral-100",
53
+ ghost: "text-neutral-800 dark:text-neutral-100",
54
+ link: "text-primary-600 dark:text-primary-400 underline",
55
+ },
56
+ size: {
57
+ sm: "text-xs",
58
+ md: "text-sm",
59
+ lg: "text-base font-semibold",
60
+ xl: "text-lg font-bold",
61
+ icon: "text-sm",
62
+ },
63
+ },
64
+ defaultVariants: {
65
+ variant: "primary",
66
+ size: "md",
67
+ },
68
+ });
69
+
70
+ type ButtonVariantProps = VariantProps<typeof buttonVariants>;
71
+
72
+ export interface ButtonProps
73
+ extends Omit<PressableProps, "children">,
74
+ ButtonVariantProps {
75
+ children?: React.ReactNode;
76
+ loading?: boolean;
77
+ disabled?: boolean;
78
+ leftIcon?: React.ReactNode;
79
+ rightIcon?: React.ReactNode;
80
+ className?: string;
81
+ textClassName?: string;
82
+ }
83
+
84
+ interface ButtonContextValue {
85
+ variant: ButtonVariantProps["variant"];
86
+ size: ButtonVariantProps["size"];
87
+ }
88
+
89
+ const ButtonContext = React.createContext<ButtonContextValue>({
90
+ variant: "primary",
91
+ size: "md",
92
+ });
93
+
94
+ export const Button = React.forwardRef<View, ButtonProps>(function Button(
95
+ {
96
+ children,
97
+ variant = "primary",
98
+ size = "md",
99
+ loading = false,
100
+ disabled = false,
101
+ leftIcon,
102
+ rightIcon,
103
+ className,
104
+ textClassName,
105
+ ...props
106
+ },
107
+ ref
108
+ ) {
109
+ const isDisabled = disabled || loading;
110
+ const isLightText = variant === "outline" || variant === "ghost" || variant === "link";
111
+ const spinnerColor = isLightText ? "#2563eb" : "#ffffff";
112
+
113
+ return (
114
+ <ButtonContext.Provider value={{ variant, size }}>
115
+ <Pressable
116
+ ref={ref}
117
+ accessibilityRole="button"
118
+ accessibilityState={{ disabled: !!isDisabled, busy: loading }}
119
+ disabled={isDisabled}
120
+ className={cn(
121
+ buttonVariants({ variant, size }),
122
+ isDisabled && "opacity-50",
123
+ className
124
+ )}
125
+ {...props}
126
+ >
127
+ {loading ? (
128
+ <ActivityIndicator color={spinnerColor} className="mr-2" size="small" />
129
+ ) : leftIcon ? (
130
+ <View className="mr-2">{leftIcon}</View>
131
+ ) : null}
132
+
133
+ {typeof children === "string" || typeof children === "number" ? (
134
+ <Text
135
+ className={cn(
136
+ buttonTextVariants({ variant, size }),
137
+ textClassName
138
+ )}
139
+ >
140
+ {children}
141
+ </Text>
142
+ ) : (
143
+ children
144
+ )}
145
+
146
+ {rightIcon && !loading ? (
147
+ <View className="ml-2">{rightIcon}</View>
148
+ ) : null}
149
+ </Pressable>
150
+ </ButtonContext.Provider>
151
+ );
152
+ });
153
+
154
+ Button.displayName = "Button";
155
+
156
+ export function ButtonText({
157
+ children,
158
+ className,
159
+ ...props
160
+ }: TextProps & { className?: string }) {
161
+ const { variant, size } = React.useContext(ButtonContext);
162
+ return (
163
+ <Text
164
+ className={cn(buttonTextVariants({ variant, size }), className)}
165
+ {...props}
166
+ >
167
+ {children}
168
+ </Text>
169
+ );
170
+ }
171
+
172
+ ButtonText.displayName = "Button.Text";
@@ -0,0 +1 @@
1
+ export * from "./button";
@@ -0,0 +1,121 @@
1
+ import * as React from "react";
2
+ import { View, Text, type ViewProps, type TextProps } from "react-native";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "../../utilities/cn";
5
+
6
+ export const cardVariants = cva(
7
+ "w-full rounded-2xl border bg-white dark:bg-neutral-900 overflow-hidden",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "border-neutral-200 dark:border-neutral-800 shadow-sm",
12
+ elevated: "border-neutral-200/80 dark:border-neutral-800 shadow-md",
13
+ outlined: "border-2 border-primary-200 dark:border-primary-800",
14
+ ghost: "border-transparent bg-transparent shadow-none",
15
+ },
16
+ padding: {
17
+ none: "p-0",
18
+ sm: "p-3",
19
+ md: "p-4",
20
+ lg: "p-6",
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ variant: "default",
25
+ padding: "none",
26
+ },
27
+ }
28
+ );
29
+
30
+ export interface CardProps
31
+ extends ViewProps,
32
+ VariantProps<typeof cardVariants> {
33
+ className?: string;
34
+ }
35
+
36
+ export const Card = React.forwardRef<View, CardProps>(function Card(
37
+ { className, variant, padding, ...props },
38
+ ref
39
+ ) {
40
+ return (
41
+ <View
42
+ ref={ref}
43
+ className={cn(cardVariants({ variant, padding }), className)}
44
+ {...props}
45
+ />
46
+ );
47
+ });
48
+ Card.displayName = "Card";
49
+
50
+ export const CardHeader = React.forwardRef<
51
+ View,
52
+ ViewProps & { className?: string }
53
+ >(function CardHeader({ className, ...props }, ref) {
54
+ return (
55
+ <View
56
+ ref={ref}
57
+ className={cn("flex-col space-y-1.5 p-4 pb-2", className)}
58
+ {...props}
59
+ />
60
+ );
61
+ });
62
+ CardHeader.displayName = "Card.Header";
63
+
64
+ export const CardTitle = React.forwardRef<
65
+ Text,
66
+ TextProps & { className?: string }
67
+ >(function CardTitle({ className, ...props }, ref) {
68
+ return (
69
+ <Text
70
+ ref={ref}
71
+ className={cn(
72
+ "text-lg font-bold tracking-tight text-neutral-900 dark:text-neutral-50",
73
+ className
74
+ )}
75
+ {...props}
76
+ />
77
+ );
78
+ });
79
+ CardTitle.displayName = "Card.Title";
80
+
81
+ export const CardDescription = React.forwardRef<
82
+ Text,
83
+ TextProps & { className?: string }
84
+ >(function CardDescription({ className, ...props }, ref) {
85
+ return (
86
+ <Text
87
+ ref={ref}
88
+ className={cn("text-sm text-neutral-500 dark:text-neutral-400", className)}
89
+ {...props}
90
+ />
91
+ );
92
+ });
93
+ CardDescription.displayName = "Card.Description";
94
+
95
+ export const CardContent = React.forwardRef<
96
+ View,
97
+ ViewProps & { className?: string }
98
+ >(function CardContent({ className, ...props }, ref) {
99
+ return (
100
+ <View
101
+ ref={ref}
102
+ className={cn("p-4 pt-1", className)}
103
+ {...props}
104
+ />
105
+ );
106
+ });
107
+ CardContent.displayName = "Card.Content";
108
+
109
+ export const CardFooter = React.forwardRef<
110
+ View,
111
+ ViewProps & { className?: string }
112
+ >(function CardFooter({ className, ...props }, ref) {
113
+ return (
114
+ <View
115
+ ref={ref}
116
+ className={cn("flex-row items-center p-4 pt-0", className)}
117
+ {...props}
118
+ />
119
+ );
120
+ });
121
+ CardFooter.displayName = "Card.Footer";
@@ -0,0 +1 @@
1
+ export * from "./card";
@@ -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";