@beyondcorp/beyond-ui-native 0.1.5 → 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/preset.mjs CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  preset_default,
4
4
  tokens
5
5
  } from "./chunk-RNHEGFYR.mjs";
6
+ import "./chunk-BJTO5JO5.mjs";
6
7
  export {
7
8
  beyondUIPreset,
8
9
  preset_default as default,
@@ -3,6 +3,7 @@ import {
3
3
  getAccessibilityProps,
4
4
  joinA11yLabels
5
5
  } from "./chunk-UTNW3LJA.mjs";
6
+ import "./chunk-BJTO5JO5.mjs";
6
7
  export {
7
8
  cn,
8
9
  getAccessibilityProps,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beyondcorp/beyond-ui-native",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Accessible, NativeWind-first React Native component library for Beyond Softwares",
5
5
  "author": "Beyond Corp",
6
6
  "license": "MIT",
@@ -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";
@@ -5,6 +5,35 @@ import { PortalHost } from "@rn-primitives/portal";
5
5
  import { ToastContext, ToastItem, ToastContextValue } from "../components/toast/types";
6
6
  import { ToastContainer } from "../components/toast/toast";
7
7
 
8
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
9
+ try {
10
+ const { LogBox } = require("react-native");
11
+ LogBox?.ignoreLogs?.([
12
+ "You are setting the style `{ shadowOffset: ... }` as a prop",
13
+ "You are setting the style",
14
+ ]);
15
+
16
+ if (!Boolean((console.error as any)?.__beyond_ui_filtered)) {
17
+ const originalError = console.error;
18
+ const filteredError = (...args: any[]) => {
19
+ const fullMessage = args
20
+ .map((arg) => (typeof arg === "string" ? arg : ""))
21
+ .join(" ");
22
+
23
+ if (
24
+ fullMessage.includes("shadowOffset") ||
25
+ fullMessage.includes("You are setting the style")
26
+ ) {
27
+ return;
28
+ }
29
+ originalError(...args);
30
+ };
31
+ (filteredError as any).__beyond_ui_filtered = true;
32
+ console.error = filteredError;
33
+ }
34
+ } catch (_) {}
35
+ }
36
+
8
37
  export type ColorScheme = "light" | "dark" | "system";
9
38
 
10
39
  export interface BeyondUIContextValue {