@beyondcorp/beyond-ui-native 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +109 -1
- package/dist/index.d.ts +109 -1
- package/dist/index.js +749 -315
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +769 -329
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/dashboard-layout/dashboard-layout.tsx +245 -0
- package/src/components/dashboard-layout/index.ts +1 -0
- package/src/components/modal/index.ts +1 -0
- package/src/components/modal/modal.tsx +227 -0
- package/src/components/night-mode-switch/index.ts +1 -0
- package/src/components/night-mode-switch/night-mode-switch.tsx +95 -0
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
ScrollView,
|
|
5
|
+
Pressable,
|
|
6
|
+
TextInput,
|
|
7
|
+
RefreshControl,
|
|
8
|
+
type ViewProps,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { cn } from "../../utilities/cn";
|
|
11
|
+
import { Text } from "../text/text";
|
|
12
|
+
import { Heading } from "../text/heading";
|
|
13
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../avatar/avatar";
|
|
14
|
+
import { NightModeSwitch } from "../night-mode-switch/night-mode-switch";
|
|
15
|
+
|
|
16
|
+
export interface DashboardBreadcrumb {
|
|
17
|
+
label: string;
|
|
18
|
+
onPress?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DashboardLayoutProps extends ViewProps {
|
|
22
|
+
/** Screen or dashboard title */
|
|
23
|
+
title?: string;
|
|
24
|
+
/** Subtitle or section caption */
|
|
25
|
+
subtitle?: string;
|
|
26
|
+
/** Optional breadcrumbs list */
|
|
27
|
+
breadcrumbs?: DashboardBreadcrumb[];
|
|
28
|
+
/** User information for the top bar profile */
|
|
29
|
+
user?: {
|
|
30
|
+
name?: string;
|
|
31
|
+
email?: string;
|
|
32
|
+
avatarUrl?: string;
|
|
33
|
+
role?: string;
|
|
34
|
+
};
|
|
35
|
+
onProfilePress?: () => void;
|
|
36
|
+
/** Notification button handler */
|
|
37
|
+
onNotificationPress?: () => void;
|
|
38
|
+
unreadNotificationsCount?: number;
|
|
39
|
+
/** Search bar configuration */
|
|
40
|
+
showSearch?: boolean;
|
|
41
|
+
searchPlaceholder?: string;
|
|
42
|
+
searchValue?: string;
|
|
43
|
+
onSearchChange?: (text: string) => void;
|
|
44
|
+
/** Night mode toggle display */
|
|
45
|
+
showNightModeSwitch?: boolean;
|
|
46
|
+
/** Navigation back action or drawer action */
|
|
47
|
+
onBackPress?: () => void;
|
|
48
|
+
backIcon?: React.ReactNode;
|
|
49
|
+
/** Custom actions rendered in the top header right section */
|
|
50
|
+
headerActions?: React.ReactNode;
|
|
51
|
+
/** Custom bottom bar or fixed footer */
|
|
52
|
+
footer?: React.ReactNode;
|
|
53
|
+
/** Whether the main content area scrolls */
|
|
54
|
+
scrollable?: boolean;
|
|
55
|
+
/** Pull to refresh handlers */
|
|
56
|
+
refreshing?: boolean;
|
|
57
|
+
onRefresh?: () => void;
|
|
58
|
+
/** Content wrapper class name */
|
|
59
|
+
contentClassName?: string;
|
|
60
|
+
children: React.ReactNode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
|
|
64
|
+
title,
|
|
65
|
+
subtitle,
|
|
66
|
+
breadcrumbs,
|
|
67
|
+
user,
|
|
68
|
+
onProfilePress,
|
|
69
|
+
onNotificationPress,
|
|
70
|
+
unreadNotificationsCount = 0,
|
|
71
|
+
showSearch = false,
|
|
72
|
+
searchPlaceholder = "Search...",
|
|
73
|
+
searchValue,
|
|
74
|
+
onSearchChange,
|
|
75
|
+
showNightModeSwitch = true,
|
|
76
|
+
onBackPress,
|
|
77
|
+
backIcon,
|
|
78
|
+
headerActions,
|
|
79
|
+
footer,
|
|
80
|
+
scrollable = true,
|
|
81
|
+
refreshing = false,
|
|
82
|
+
onRefresh,
|
|
83
|
+
contentClassName,
|
|
84
|
+
className,
|
|
85
|
+
children,
|
|
86
|
+
...props
|
|
87
|
+
}) => {
|
|
88
|
+
return (
|
|
89
|
+
<View
|
|
90
|
+
className={cn("flex-1 bg-neutral-50 dark:bg-neutral-950", className)}
|
|
91
|
+
{...props}
|
|
92
|
+
>
|
|
93
|
+
{/* Top Header Bar */}
|
|
94
|
+
<View className="px-5 pt-3 pb-3 border-b border-neutral-200 dark:border-neutral-800 bg-white/90 dark:bg-neutral-900/90 flex-row items-center justify-between z-10">
|
|
95
|
+
<View className="flex-row items-center gap-3 flex-1 mr-2">
|
|
96
|
+
{onBackPress && (
|
|
97
|
+
<Pressable
|
|
98
|
+
onPress={onBackPress}
|
|
99
|
+
className="w-9 h-9 rounded-full bg-neutral-100 dark:bg-neutral-800 items-center justify-center active:opacity-70"
|
|
100
|
+
accessibilityRole="button"
|
|
101
|
+
accessibilityLabel="Back"
|
|
102
|
+
>
|
|
103
|
+
{backIcon || (
|
|
104
|
+
<Text className="text-neutral-700 dark:text-neutral-200 font-bold text-base leading-none">
|
|
105
|
+
←
|
|
106
|
+
</Text>
|
|
107
|
+
)}
|
|
108
|
+
</Pressable>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{user && (
|
|
112
|
+
<Pressable
|
|
113
|
+
onPress={onProfilePress}
|
|
114
|
+
className="flex-row items-center gap-2 active:opacity-80"
|
|
115
|
+
accessibilityRole="button"
|
|
116
|
+
accessibilityLabel={`User profile: ${user.name || "User"}`}
|
|
117
|
+
>
|
|
118
|
+
<Avatar size="sm">
|
|
119
|
+
{user.avatarUrl && <AvatarImage source={{ uri: user.avatarUrl }} />}
|
|
120
|
+
<AvatarFallback text={(user.name || "U").slice(0, 2).toUpperCase()} />
|
|
121
|
+
</Avatar>
|
|
122
|
+
<View className="hidden sm:flex">
|
|
123
|
+
<Text className="text-xs font-semibold text-neutral-800 dark:text-neutral-200 leading-tight">
|
|
124
|
+
{user.name}
|
|
125
|
+
</Text>
|
|
126
|
+
{user.role && (
|
|
127
|
+
<Text className="text-[10px] text-neutral-500 dark:text-neutral-400 leading-tight">
|
|
128
|
+
{user.role}
|
|
129
|
+
</Text>
|
|
130
|
+
)}
|
|
131
|
+
</View>
|
|
132
|
+
</Pressable>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{(title || subtitle) && (
|
|
136
|
+
<View className="flex-1">
|
|
137
|
+
{breadcrumbs && breadcrumbs.length > 0 && (
|
|
138
|
+
<View className="flex-row items-center gap-1 mb-0.5">
|
|
139
|
+
{breadcrumbs.map((crumb, idx) => (
|
|
140
|
+
<React.Fragment key={idx}>
|
|
141
|
+
{idx > 0 && (
|
|
142
|
+
<Text className="text-[11px] text-neutral-400 dark:text-neutral-500">/</Text>
|
|
143
|
+
)}
|
|
144
|
+
<Text
|
|
145
|
+
onPress={crumb.onPress}
|
|
146
|
+
className={cn(
|
|
147
|
+
"text-[11px]",
|
|
148
|
+
crumb.onPress
|
|
149
|
+
? "text-primary-600 dark:text-primary-400 font-medium"
|
|
150
|
+
: "text-neutral-500 dark:text-neutral-400"
|
|
151
|
+
)}
|
|
152
|
+
>
|
|
153
|
+
{crumb.label}
|
|
154
|
+
</Text>
|
|
155
|
+
</React.Fragment>
|
|
156
|
+
))}
|
|
157
|
+
</View>
|
|
158
|
+
)}
|
|
159
|
+
{title && (
|
|
160
|
+
<Heading
|
|
161
|
+
level={3}
|
|
162
|
+
className="text-lg font-bold text-neutral-900 dark:text-neutral-50 leading-tight"
|
|
163
|
+
>
|
|
164
|
+
{title}
|
|
165
|
+
</Heading>
|
|
166
|
+
)}
|
|
167
|
+
{subtitle && (
|
|
168
|
+
<Text className="text-xs text-neutral-500 dark:text-neutral-400 leading-tight">
|
|
169
|
+
{subtitle}
|
|
170
|
+
</Text>
|
|
171
|
+
)}
|
|
172
|
+
</View>
|
|
173
|
+
)}
|
|
174
|
+
</View>
|
|
175
|
+
|
|
176
|
+
{/* Right header actions */}
|
|
177
|
+
<View className="flex-row items-center gap-2">
|
|
178
|
+
{headerActions}
|
|
179
|
+
|
|
180
|
+
{showNightModeSwitch && (
|
|
181
|
+
<NightModeSwitch size="sm" variant="ghost" />
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{onNotificationPress && (
|
|
185
|
+
<Pressable
|
|
186
|
+
onPress={onNotificationPress}
|
|
187
|
+
className="w-9 h-9 rounded-full bg-neutral-100 dark:bg-neutral-800 items-center justify-center relative active:opacity-70"
|
|
188
|
+
accessibilityRole="button"
|
|
189
|
+
accessibilityLabel="Notifications"
|
|
190
|
+
>
|
|
191
|
+
<Text className="text-sm">🔔</Text>
|
|
192
|
+
{unreadNotificationsCount > 0 && (
|
|
193
|
+
<View className="absolute -top-0.5 -right-0.5 min-w-[16px] h-4 px-1 rounded-full bg-danger items-center justify-center">
|
|
194
|
+
<Text className="text-[9px] font-bold text-white leading-none">
|
|
195
|
+
{unreadNotificationsCount > 9 ? "9+" : unreadNotificationsCount}
|
|
196
|
+
</Text>
|
|
197
|
+
</View>
|
|
198
|
+
)}
|
|
199
|
+
</Pressable>
|
|
200
|
+
)}
|
|
201
|
+
</View>
|
|
202
|
+
</View>
|
|
203
|
+
|
|
204
|
+
{/* Optional Search Bar */}
|
|
205
|
+
{showSearch && (
|
|
206
|
+
<View className="px-5 py-2 border-b border-neutral-100 dark:border-neutral-800/60 bg-neutral-50/50 dark:bg-neutral-900/50">
|
|
207
|
+
<View className="flex-row items-center bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl px-3 py-2">
|
|
208
|
+
<Text className="text-neutral-400 mr-2 text-sm">🔍</Text>
|
|
209
|
+
<TextInput
|
|
210
|
+
value={searchValue}
|
|
211
|
+
onChangeText={onSearchChange}
|
|
212
|
+
placeholder={searchPlaceholder}
|
|
213
|
+
placeholderTextColor="#9ca3af"
|
|
214
|
+
className="flex-1 text-sm text-neutral-900 dark:text-neutral-100 p-0"
|
|
215
|
+
/>
|
|
216
|
+
</View>
|
|
217
|
+
</View>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Main Body */}
|
|
221
|
+
{scrollable ? (
|
|
222
|
+
<ScrollView
|
|
223
|
+
className={cn("flex-1", contentClassName)}
|
|
224
|
+
contentContainerStyle={{ paddingBottom: 32 }}
|
|
225
|
+
showsVerticalScrollIndicator={false}
|
|
226
|
+
refreshControl={
|
|
227
|
+
onRefresh ? (
|
|
228
|
+
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
229
|
+
) : undefined
|
|
230
|
+
}
|
|
231
|
+
>
|
|
232
|
+
{children}
|
|
233
|
+
</ScrollView>
|
|
234
|
+
) : (
|
|
235
|
+
<View className={cn("flex-1", contentClassName)}>
|
|
236
|
+
{children}
|
|
237
|
+
</View>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
{/* Optional Fixed Footer / Navigation */}
|
|
241
|
+
{footer}
|
|
242
|
+
</View>
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
DashboardLayout.displayName = "DashboardLayout";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./dashboard-layout";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./modal";
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Modal as RNModal,
|
|
4
|
+
View,
|
|
5
|
+
Text,
|
|
6
|
+
Pressable,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
KeyboardAvoidingView,
|
|
9
|
+
Platform,
|
|
10
|
+
ScrollView,
|
|
11
|
+
type ViewProps,
|
|
12
|
+
type TextProps,
|
|
13
|
+
type PressableProps,
|
|
14
|
+
} from "react-native";
|
|
15
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
16
|
+
import { cn } from "../../utilities/cn";
|
|
17
|
+
|
|
18
|
+
export const modalVariants = cva(
|
|
19
|
+
"w-full rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-2xl relative",
|
|
20
|
+
{
|
|
21
|
+
variants: {
|
|
22
|
+
size: {
|
|
23
|
+
sm: "max-w-xs",
|
|
24
|
+
md: "max-w-md",
|
|
25
|
+
lg: "max-w-lg",
|
|
26
|
+
xl: "max-w-xl",
|
|
27
|
+
full: "max-w-[95%] max-h-[90%]",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
size: "md",
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export interface ModalContextValue {
|
|
37
|
+
open: boolean;
|
|
38
|
+
onOpenChange: (open: boolean) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const ModalContext = React.createContext<ModalContextValue | null>(null);
|
|
42
|
+
|
|
43
|
+
export function useModalContext(): ModalContextValue {
|
|
44
|
+
const context = React.useContext(ModalContext);
|
|
45
|
+
if (!context) {
|
|
46
|
+
throw new Error("Modal compound components must be used within a Modal component");
|
|
47
|
+
}
|
|
48
|
+
return context;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ModalProps extends VariantProps<typeof modalVariants> {
|
|
52
|
+
open?: boolean;
|
|
53
|
+
visible?: boolean; // alias for open for RN conventions
|
|
54
|
+
onOpenChange?: (open: boolean) => void;
|
|
55
|
+
onRequestClose?: () => void;
|
|
56
|
+
showCloseButton?: boolean;
|
|
57
|
+
closeOnOverlayPress?: boolean;
|
|
58
|
+
children: React.ReactNode;
|
|
59
|
+
className?: string;
|
|
60
|
+
overlayClassName?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function Modal({
|
|
64
|
+
open: controlledOpen,
|
|
65
|
+
visible,
|
|
66
|
+
onOpenChange,
|
|
67
|
+
onRequestClose,
|
|
68
|
+
showCloseButton = true,
|
|
69
|
+
closeOnOverlayPress = true,
|
|
70
|
+
size = "md",
|
|
71
|
+
children,
|
|
72
|
+
className,
|
|
73
|
+
overlayClassName,
|
|
74
|
+
}: ModalProps) {
|
|
75
|
+
const isOpen = (controlledOpen !== undefined ? controlledOpen : visible) ?? false;
|
|
76
|
+
|
|
77
|
+
const handleClose = React.useCallback(() => {
|
|
78
|
+
onOpenChange?.(false);
|
|
79
|
+
onRequestClose?.();
|
|
80
|
+
}, [onOpenChange, onRequestClose]);
|
|
81
|
+
|
|
82
|
+
if (!isOpen) return null;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<ModalContext.Provider value={{ open: isOpen, onOpenChange: (o) => !o && handleClose() }}>
|
|
86
|
+
<RNModal
|
|
87
|
+
transparent
|
|
88
|
+
visible={isOpen}
|
|
89
|
+
animationType="fade"
|
|
90
|
+
onRequestClose={handleClose}
|
|
91
|
+
statusBarTranslucent
|
|
92
|
+
>
|
|
93
|
+
<KeyboardAvoidingView
|
|
94
|
+
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
|
95
|
+
style={StyleSheet.absoluteFill}
|
|
96
|
+
>
|
|
97
|
+
{/* Backdrop Overlay */}
|
|
98
|
+
<Pressable
|
|
99
|
+
style={StyleSheet.absoluteFill}
|
|
100
|
+
className={cn("bg-black/60", overlayClassName)}
|
|
101
|
+
onPress={closeOnOverlayPress ? handleClose : undefined}
|
|
102
|
+
accessibilityRole="button"
|
|
103
|
+
accessibilityLabel="Close modal overlay"
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
{/* Modal Container */}
|
|
107
|
+
<View
|
|
108
|
+
pointerEvents="box-none"
|
|
109
|
+
style={StyleSheet.absoluteFill}
|
|
110
|
+
className="items-center justify-center p-4 z-50"
|
|
111
|
+
>
|
|
112
|
+
<View
|
|
113
|
+
role="dialog"
|
|
114
|
+
aria-modal={true}
|
|
115
|
+
className={cn(modalVariants({ size }), className)}
|
|
116
|
+
>
|
|
117
|
+
{showCloseButton && (
|
|
118
|
+
<Pressable
|
|
119
|
+
onPress={handleClose}
|
|
120
|
+
hitSlop={{ top: 12, right: 12, bottom: 12, left: 12 }}
|
|
121
|
+
className="absolute right-4 top-4 z-50 w-8 h-8 rounded-full bg-neutral-100 dark:bg-neutral-800 items-center justify-center active:opacity-70"
|
|
122
|
+
accessibilityRole="button"
|
|
123
|
+
accessibilityLabel="Close modal"
|
|
124
|
+
>
|
|
125
|
+
<Text className="text-neutral-500 dark:text-neutral-400 text-sm font-semibold leading-none">
|
|
126
|
+
✕
|
|
127
|
+
</Text>
|
|
128
|
+
</Pressable>
|
|
129
|
+
)}
|
|
130
|
+
{children}
|
|
131
|
+
</View>
|
|
132
|
+
</View>
|
|
133
|
+
</KeyboardAvoidingView>
|
|
134
|
+
</RNModal>
|
|
135
|
+
</ModalContext.Provider>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
Modal.displayName = "Modal";
|
|
139
|
+
|
|
140
|
+
export function ModalHeader({
|
|
141
|
+
className,
|
|
142
|
+
...props
|
|
143
|
+
}: ViewProps & { className?: string }) {
|
|
144
|
+
return (
|
|
145
|
+
<View
|
|
146
|
+
className={cn("flex-col space-y-1.5 pr-8", className)}
|
|
147
|
+
{...props}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
ModalHeader.displayName = "ModalHeader";
|
|
152
|
+
|
|
153
|
+
export const ModalTitle = React.forwardRef<Text, TextProps & { className?: string }>(
|
|
154
|
+
function ModalTitle({ className, ...props }, ref) {
|
|
155
|
+
return (
|
|
156
|
+
<Text
|
|
157
|
+
ref={ref}
|
|
158
|
+
role="heading"
|
|
159
|
+
aria-level={2}
|
|
160
|
+
className={cn(
|
|
161
|
+
"text-lg font-bold tracking-tight text-neutral-900 dark:text-neutral-50",
|
|
162
|
+
className
|
|
163
|
+
)}
|
|
164
|
+
{...props}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
ModalTitle.displayName = "ModalTitle";
|
|
170
|
+
|
|
171
|
+
export const ModalDescription = React.forwardRef<Text, TextProps & { className?: string }>(
|
|
172
|
+
function ModalDescription({ className, ...props }, ref) {
|
|
173
|
+
return (
|
|
174
|
+
<Text
|
|
175
|
+
ref={ref}
|
|
176
|
+
className={cn(
|
|
177
|
+
"text-sm text-neutral-500 dark:text-neutral-400 mt-1",
|
|
178
|
+
className
|
|
179
|
+
)}
|
|
180
|
+
{...props}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
ModalDescription.displayName = "ModalDescription";
|
|
186
|
+
|
|
187
|
+
export function ModalContent({
|
|
188
|
+
className,
|
|
189
|
+
children,
|
|
190
|
+
scrollable = false,
|
|
191
|
+
...props
|
|
192
|
+
}: ViewProps & { className?: string; scrollable?: boolean }) {
|
|
193
|
+
if (scrollable) {
|
|
194
|
+
return (
|
|
195
|
+
<ScrollView
|
|
196
|
+
showsVerticalScrollIndicator={false}
|
|
197
|
+
className={cn("py-3 max-h-[70vh]", className)}
|
|
198
|
+
{...props}
|
|
199
|
+
>
|
|
200
|
+
{children}
|
|
201
|
+
</ScrollView>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<View className={cn("py-3", className)} {...props}>
|
|
207
|
+
{children}
|
|
208
|
+
</View>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
ModalContent.displayName = "ModalContent";
|
|
212
|
+
|
|
213
|
+
export function ModalFooter({
|
|
214
|
+
className,
|
|
215
|
+
...props
|
|
216
|
+
}: ViewProps & { className?: string }) {
|
|
217
|
+
return (
|
|
218
|
+
<View
|
|
219
|
+
className={cn(
|
|
220
|
+
"flex-row flex-wrap items-center justify-end gap-2 pt-3 border-t border-neutral-100 dark:border-neutral-800 mt-2",
|
|
221
|
+
className
|
|
222
|
+
)}
|
|
223
|
+
{...props}
|
|
224
|
+
/>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
ModalFooter.displayName = "ModalFooter";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./night-mode-switch";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
Text,
|
|
5
|
+
type PressableProps,
|
|
6
|
+
type GestureResponderEvent,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
9
|
+
import { cn } from "../../utilities/cn";
|
|
10
|
+
import { useBeyondUI } from "../../providers/BeyondUIProvider";
|
|
11
|
+
|
|
12
|
+
export const nightModeSwitchVariants = cva(
|
|
13
|
+
"inline-flex items-center justify-center rounded-full active:opacity-75",
|
|
14
|
+
{
|
|
15
|
+
variants: {
|
|
16
|
+
variant: {
|
|
17
|
+
primary: "bg-primary-600 dark:bg-primary-500",
|
|
18
|
+
secondary: "bg-secondary-600 dark:bg-secondary-500",
|
|
19
|
+
ghost: "bg-transparent active:bg-neutral-100 dark:active:bg-neutral-800",
|
|
20
|
+
},
|
|
21
|
+
size: {
|
|
22
|
+
sm: "w-8 h-8",
|
|
23
|
+
md: "w-10 h-10",
|
|
24
|
+
lg: "w-12 h-12",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: {
|
|
28
|
+
variant: "ghost",
|
|
29
|
+
size: "md",
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export interface NightModeSwitchProps
|
|
35
|
+
extends Omit<PressableProps, "children">,
|
|
36
|
+
VariantProps<typeof nightModeSwitchVariants> {
|
|
37
|
+
ariaLabel?: string;
|
|
38
|
+
className?: string;
|
|
39
|
+
textClassName?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const NightModeSwitch = React.forwardRef<any, NightModeSwitchProps>(
|
|
43
|
+
function NightModeSwitch(
|
|
44
|
+
{
|
|
45
|
+
className,
|
|
46
|
+
textClassName,
|
|
47
|
+
variant,
|
|
48
|
+
size,
|
|
49
|
+
ariaLabel = "Toggle dark mode",
|
|
50
|
+
onPress,
|
|
51
|
+
...props
|
|
52
|
+
},
|
|
53
|
+
ref
|
|
54
|
+
) {
|
|
55
|
+
const { colorScheme, toggleColorScheme } = useBeyondUI();
|
|
56
|
+
const isDark = colorScheme === "dark";
|
|
57
|
+
|
|
58
|
+
const handlePress = (e: GestureResponderEvent) => {
|
|
59
|
+
toggleColorScheme();
|
|
60
|
+
onPress?.(e);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const iconSizeClasses = {
|
|
64
|
+
sm: "text-base",
|
|
65
|
+
md: "text-lg",
|
|
66
|
+
lg: "text-xl",
|
|
67
|
+
}[size || "md"];
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Pressable
|
|
71
|
+
ref={ref}
|
|
72
|
+
role="button"
|
|
73
|
+
accessibilityRole="button"
|
|
74
|
+
accessibilityLabel={ariaLabel}
|
|
75
|
+
accessibilityState={{ checked: isDark }}
|
|
76
|
+
className={cn(nightModeSwitchVariants({ variant, size }), className)}
|
|
77
|
+
onPress={handlePress}
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
<Text
|
|
81
|
+
className={cn(
|
|
82
|
+
iconSizeClasses,
|
|
83
|
+
variant === "ghost"
|
|
84
|
+
? "text-neutral-700 dark:text-neutral-200"
|
|
85
|
+
: "text-white",
|
|
86
|
+
textClassName
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{isDark ? "🌙" : "☀️"}
|
|
90
|
+
</Text>
|
|
91
|
+
</Pressable>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
NightModeSwitch.displayName = "NightModeSwitch";
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,9 @@ export * from "./components/accordion";
|
|
|
26
26
|
export * from "./components/bottom-sheet";
|
|
27
27
|
export * from "./components/select";
|
|
28
28
|
export * from "./components/data-table";
|
|
29
|
+
export * from "./components/modal";
|
|
30
|
+
export * from "./components/night-mode-switch";
|
|
31
|
+
export * from "./components/dashboard-layout";
|
|
29
32
|
|
|
30
33
|
// Screen Templates
|
|
31
34
|
export * from "./templates";
|