@umituz/react-native-design-system 2.3.4 → 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "2.3.4",
3
+ "version": "2.3.7",
4
4
  "description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive and safe area utilities",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -63,7 +63,9 @@
63
63
  "react-native-svg": ">=15.0.0",
64
64
  "zustand": ">=5.0.0",
65
65
  "expo-device": ">=5.0.0",
66
- "expo-application": ">=5.0.0"
66
+ "expo-application": ">=5.0.0",
67
+ "@umituz/react-native-uuid": "latest",
68
+ "expo-crypto": ">=13.0.0"
67
69
  },
68
70
  "peerDependenciesMeta": {
69
71
  "expo-linear-gradient": {
@@ -107,7 +109,9 @@
107
109
  "zustand": "^5.0.2",
108
110
  "expo-device": "~7.0.2",
109
111
  "expo-application": "~5.9.1",
110
- "expo-localization": "~16.0.1"
112
+ "expo-localization": "~16.0.1",
113
+ "@umituz/react-native-uuid": "latest",
114
+ "expo-crypto": "~14.0.0"
111
115
  },
112
116
  "publishConfig": {
113
117
  "access": "public"
@@ -60,6 +60,10 @@ export interface AtomicInputProps {
60
60
  onBlur?: () => void;
61
61
  /** Focus callback */
62
62
  onFocus?: () => void;
63
+ /** Multiline input support */
64
+ multiline?: boolean;
65
+ /** Number of lines for multiline input */
66
+ numberOfLines?: number;
63
67
  }
64
68
 
65
69
  /**
@@ -99,6 +103,8 @@ export const AtomicInput: React.FC<AtomicInputProps> = ({
99
103
  testID,
100
104
  onBlur,
101
105
  onFocus,
106
+ multiline = false,
107
+ numberOfLines,
102
108
  }) => {
103
109
  const tokens = useAppDesignTokens();
104
110
 
@@ -202,6 +208,8 @@ export const AtomicInput: React.FC<AtomicInputProps> = ({
202
208
  autoCapitalize={autoCapitalize}
203
209
  autoCorrect={autoCorrect}
204
210
  editable={!isDisabled}
211
+ multiline={multiline}
212
+ numberOfLines={numberOfLines}
205
213
  style={textInputStyle}
206
214
  onBlur={() => {
207
215
  setIsFocused(false);
package/src/index.ts CHANGED
@@ -243,10 +243,25 @@ export {
243
243
  Grid,
244
244
  List,
245
245
  Container,
246
+ // Alerts
247
+ AlertBanner,
248
+ AlertToast,
249
+ AlertInline,
250
+ AlertModal,
251
+ AlertContainer,
252
+ AlertProvider,
253
+ useAlert,
254
+ alertService,
255
+ AlertType,
256
+ AlertMode,
257
+ AlertPosition,
246
258
  type BaseModalProps,
247
259
  type GridProps,
248
260
  type ListProps,
249
261
  type ContainerProps,
262
+ type Alert,
263
+ type AlertAction,
264
+ type AlertOptions,
250
265
  } from './molecules';
251
266
 
252
267
  // =============================================================================
@@ -0,0 +1,188 @@
1
+ /**
2
+ * AlertBanner Component
3
+ *
4
+ * Displays a banner-style alert at the top or bottom of the screen.
5
+ * Full-width notification bar for important messages.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { StyleSheet, View, Pressable } from 'react-native';
10
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
11
+ import { AtomicText, AtomicIcon } from '../../atoms';
12
+ import { useAppDesignTokens } from '../../theme';
13
+ import { Alert, AlertType, AlertPosition } from './AlertTypes';
14
+ import { useAlertStore } from './AlertStore';
15
+
16
+ interface AlertBannerProps {
17
+ alert: Alert;
18
+ }
19
+
20
+ export function AlertBanner({ alert }: AlertBannerProps) {
21
+ const dismissAlert = useAlertStore((state) => state.dismissAlert);
22
+ const insets = useSafeAreaInsets();
23
+ const tokens = useAppDesignTokens();
24
+
25
+ const handleDismiss = () => {
26
+ if (alert.dismissible) {
27
+ dismissAlert(alert.id);
28
+ alert.onDismiss?.();
29
+ }
30
+ };
31
+
32
+ const getBackgroundColor = (type: AlertType): string => {
33
+ switch (type) {
34
+ case AlertType.SUCCESS:
35
+ return tokens.colors.success;
36
+ case AlertType.ERROR:
37
+ return tokens.colors.error;
38
+ case AlertType.WARNING:
39
+ return tokens.colors.warning;
40
+ case AlertType.INFO:
41
+ return tokens.colors.info;
42
+ default:
43
+ return tokens.colors.backgroundSecondary;
44
+ }
45
+ };
46
+
47
+ const backgroundColor = getBackgroundColor(alert.type);
48
+ const textColor = tokens.colors.textInverse;
49
+ const isTop = alert.position === AlertPosition.TOP;
50
+
51
+ return (
52
+ <View
53
+ style={[
54
+ styles.container,
55
+ {
56
+ backgroundColor,
57
+ paddingTop: isTop ? insets.top + tokens.spacing.sm : tokens.spacing.sm,
58
+ paddingBottom: isTop ? tokens.spacing.sm : insets.bottom + tokens.spacing.sm,
59
+ paddingHorizontal: tokens.spacing.md,
60
+ },
61
+ ]}
62
+ testID={alert.testID}
63
+ >
64
+ <View style={styles.content}>
65
+ <View style={styles.row}>
66
+ {alert.icon && (
67
+ <View style={[styles.iconContainer, { marginRight: tokens.spacing.sm }]}>
68
+ <AtomicIcon
69
+ name={alert.icon}
70
+ customSize={20}
71
+ customColor={textColor}
72
+ />
73
+ </View>
74
+ )}
75
+
76
+ <View style={styles.textContainer}>
77
+ <AtomicText
78
+ type="bodyMedium"
79
+ style={[styles.title, { color: textColor }]}
80
+ numberOfLines={1}
81
+ >
82
+ {alert.title}
83
+ </AtomicText>
84
+
85
+ {alert.message && (
86
+ <AtomicText
87
+ type="bodySmall"
88
+ style={[
89
+ styles.message,
90
+ { color: textColor, marginTop: tokens.spacing.xs },
91
+ ]}
92
+ numberOfLines={2}
93
+ >
94
+ {alert.message}
95
+ </AtomicText>
96
+ )}
97
+ </View>
98
+
99
+ {alert.dismissible && (
100
+ <Pressable
101
+ onPress={handleDismiss}
102
+ style={[styles.closeButton, { marginLeft: tokens.spacing.sm }]}
103
+ hitSlop={8}
104
+ >
105
+ <AtomicIcon name="close" customSize={20} customColor={textColor} />
106
+ </Pressable>
107
+ )}
108
+ </View>
109
+
110
+ {alert.actions && alert.actions.length > 0 && (
111
+ <View style={[styles.actionsContainer, { marginTop: tokens.spacing.sm }]}>
112
+ {alert.actions.map((action) => (
113
+ <Pressable
114
+ key={action.id}
115
+ onPress={async () => {
116
+ await action.onPress();
117
+ if (action.closeOnPress ?? true) {
118
+ handleDismiss();
119
+ }
120
+ }}
121
+ style={[
122
+ styles.actionButton,
123
+ {
124
+ paddingVertical: tokens.spacing.xs,
125
+ paddingHorizontal: tokens.spacing.sm,
126
+ marginRight: tokens.spacing.xs,
127
+ },
128
+ ]}
129
+ >
130
+ <AtomicText
131
+ type="bodySmall"
132
+ style={[
133
+ styles.actionText,
134
+ { color: textColor },
135
+ ]}
136
+ >
137
+ {action.label}
138
+ </AtomicText>
139
+ </Pressable>
140
+ ))}
141
+ </View>
142
+ )}
143
+ </View>
144
+ </View>
145
+ );
146
+ }
147
+
148
+ const styles = StyleSheet.create({
149
+ container: {
150
+ width: '100%',
151
+ },
152
+ content: {
153
+ paddingVertical: 4,
154
+ },
155
+ row: {
156
+ flexDirection: 'row',
157
+ alignItems: 'center',
158
+ },
159
+ iconContainer: {
160
+ justifyContent: 'center',
161
+ alignItems: 'center',
162
+ },
163
+ textContainer: {
164
+ flex: 1,
165
+ },
166
+ title: {
167
+ fontWeight: '700',
168
+ },
169
+ message: {
170
+ opacity: 0.9,
171
+ },
172
+ closeButton: {
173
+ justifyContent: 'center',
174
+ alignItems: 'center',
175
+ },
176
+ actionsContainer: {
177
+ flexDirection: 'row',
178
+ flexWrap: 'wrap',
179
+ },
180
+ actionButton: {
181
+ justifyContent: 'center',
182
+ alignItems: 'center',
183
+ },
184
+ actionText: {
185
+ fontWeight: '700',
186
+ textDecorationLine: 'underline',
187
+ },
188
+ });
@@ -0,0 +1,71 @@
1
+ /**
2
+ * AlertContainer Component
3
+ */
4
+
5
+ import React from 'react';
6
+ import { View, StyleSheet } from 'react-native';
7
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
8
+ import { useAppDesignTokens } from '../../theme';
9
+ import { useAlertStore } from './AlertStore';
10
+ import { AlertToast } from './AlertToast';
11
+ import { AlertBanner } from './AlertBanner';
12
+ import { AlertModal } from './AlertModal';
13
+ import { AlertMode } from './AlertTypes';
14
+
15
+ export const AlertContainer: React.FC = () => {
16
+ const alerts = useAlertStore((state) => state.alerts);
17
+ const insets = useSafeAreaInsets();
18
+ const tokens = useAppDesignTokens();
19
+
20
+ const toasts = alerts.filter((a) => a.mode === AlertMode.TOAST);
21
+ const banners = alerts.filter((a) => a.mode === AlertMode.BANNER);
22
+ const modals = alerts.filter((a) => a.mode === AlertMode.MODAL);
23
+
24
+ return (
25
+ <View style={styles.container} pointerEvents="box-none">
26
+ {/* Banners at top */}
27
+ <View style={[styles.bannerContainer, { paddingTop: insets.top }]}>
28
+ {banners.map((alert) => (
29
+ <AlertBanner key={alert.id} alert={alert} />
30
+ ))}
31
+ </View>
32
+
33
+ {/* Toasts at top or bottom (default top) */}
34
+ <View style={[
35
+ styles.toastContainer,
36
+ {
37
+ top: insets.top + (banners.length > 0 ? tokens.spacing.xl * 2 : tokens.spacing.lg),
38
+ paddingHorizontal: tokens.spacing.md,
39
+ }
40
+ ]}>
41
+ {toasts.map((alert) => (
42
+ <View key={alert.id} style={{ marginBottom: tokens.spacing.sm, width: '100%' }}>
43
+ <AlertToast alert={alert} />
44
+ </View>
45
+ ))}
46
+ </View>
47
+
48
+ {/* Modals on top of everything */}
49
+ {modals.map((alert) => (
50
+ <AlertModal key={alert.id} alert={alert} />
51
+ ))}
52
+ </View>
53
+ );
54
+ };
55
+
56
+ const styles = StyleSheet.create({
57
+ container: {
58
+ ...StyleSheet.absoluteFillObject,
59
+ zIndex: 9999,
60
+ },
61
+ bannerContainer: {
62
+ width: '100%',
63
+ },
64
+ toastContainer: {
65
+ position: 'absolute',
66
+ left: 0,
67
+ right: 0,
68
+ alignItems: 'center',
69
+ zIndex: 10000,
70
+ },
71
+ });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * AlertInline Component
3
+ */
4
+
5
+ import React from 'react';
6
+ import { StyleSheet, View } from 'react-native';
7
+ import { AtomicText } from '../../atoms';
8
+ import { useAppDesignTokens } from '../../theme';
9
+ import { Alert, AlertType } from './AlertTypes';
10
+
11
+ interface AlertInlineProps {
12
+ alert: Alert;
13
+ }
14
+
15
+ export const AlertInline: React.FC<AlertInlineProps> = ({ alert }) => {
16
+ const tokens = useAppDesignTokens();
17
+
18
+ const getBorderColor = () => {
19
+ switch (alert.type) {
20
+ case AlertType.SUCCESS: return tokens.colors.success;
21
+ case AlertType.ERROR: return tokens.colors.error;
22
+ case AlertType.WARNING: return tokens.colors.warning;
23
+ case AlertType.INFO: return tokens.colors.info;
24
+ default: return tokens.colors.border;
25
+ }
26
+ };
27
+
28
+ const getBackgroundColor = () => {
29
+ switch (alert.type) {
30
+ case AlertType.SUCCESS: return tokens.colors.success + '15';
31
+ case AlertType.ERROR: return tokens.colors.error + '15';
32
+ case AlertType.WARNING: return tokens.colors.warning + '15';
33
+ case AlertType.INFO: return tokens.colors.info + '15';
34
+ default: return tokens.colors.backgroundSecondary;
35
+ }
36
+ };
37
+
38
+ return (
39
+ <View style={[
40
+ styles.container,
41
+ {
42
+ borderColor: getBorderColor(),
43
+ backgroundColor: getBackgroundColor(),
44
+ borderRadius: tokens.borders.radius.sm,
45
+ padding: tokens.spacing.md,
46
+ marginVertical: tokens.spacing.sm,
47
+ }
48
+ ]}>
49
+ <AtomicText type="bodyMedium" style={{ color: tokens.colors.textPrimary, fontWeight: '700' }}>
50
+ {alert.title}
51
+ </AtomicText>
52
+ {alert.message && (
53
+ <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary, marginTop: tokens.spacing.xs }}>
54
+ {alert.message}
55
+ </AtomicText>
56
+ )}
57
+ </View>
58
+ );
59
+ };
60
+
61
+ const styles = StyleSheet.create({
62
+ container: {
63
+ borderWidth: 1,
64
+ width: '100%',
65
+ },
66
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * AlertModal Component
3
+ */
4
+
5
+ import React from 'react';
6
+ import { StyleSheet, View, Modal, Pressable } from 'react-native';
7
+ import { AtomicText, AtomicButton } from '../../atoms';
8
+ import { useAppDesignTokens } from '../../theme';
9
+ import { Alert, AlertType } from './AlertTypes';
10
+ import { useAlertStore } from './AlertStore';
11
+
12
+ interface AlertModalProps {
13
+ alert: Alert;
14
+ }
15
+
16
+ export const AlertModal: React.FC<AlertModalProps> = ({ alert }) => {
17
+ const dismissAlert = useAlertStore((state) => state.dismissAlert);
18
+ const tokens = useAppDesignTokens();
19
+
20
+ const handleClose = () => {
21
+ dismissAlert(alert.id);
22
+ alert.onDismiss?.();
23
+ };
24
+
25
+ const getHeaderColor = () => {
26
+ switch (alert.type) {
27
+ case AlertType.SUCCESS: return tokens.colors.success;
28
+ case AlertType.ERROR: return tokens.colors.error;
29
+ case AlertType.WARNING: return tokens.colors.warning;
30
+ case AlertType.INFO: return tokens.colors.info;
31
+ default: return tokens.colors.primary;
32
+ }
33
+ };
34
+
35
+ return (
36
+ <Modal
37
+ visible
38
+ transparent
39
+ animationType="fade"
40
+ onRequestClose={handleClose}
41
+ >
42
+ <View style={styles.overlay}>
43
+ <Pressable
44
+ style={styles.backdrop}
45
+ onPress={alert.dismissible ? handleClose : undefined}
46
+ />
47
+ <View style={[
48
+ styles.modal,
49
+ {
50
+ backgroundColor: tokens.colors.backgroundPrimary,
51
+ borderRadius: tokens.borders.radius.lg,
52
+ borderWidth: 1,
53
+ borderColor: tokens.colors.border,
54
+ }
55
+ ]}>
56
+ <View style={[styles.header, { backgroundColor: getHeaderColor() }]}>
57
+ <AtomicText type="titleLarge" style={{ color: tokens.colors.textInverse }}>
58
+ {alert.title}
59
+ </AtomicText>
60
+ </View>
61
+
62
+ <View style={[styles.content, { padding: tokens.spacing.lg }]}>
63
+ {alert.message && (
64
+ <AtomicText type="bodyMedium" style={{ color: tokens.colors.textPrimary, textAlign: 'center' }}>
65
+ {alert.message}
66
+ </AtomicText>
67
+ )}
68
+
69
+ <View style={[styles.actions, { marginTop: tokens.spacing.lg, gap: tokens.spacing.sm }]}>
70
+ {alert.actions.map((action) => (
71
+ <AtomicButton
72
+ key={action.id}
73
+ title={action.label}
74
+ variant={action.style === 'destructive' ? 'danger' : action.style === 'secondary' ? 'secondary' : 'primary'}
75
+ onPress={async () => {
76
+ await action.onPress();
77
+ if (action.closeOnPress ?? true) {
78
+ handleClose();
79
+ }
80
+ }}
81
+ fullWidth
82
+ />
83
+ ))}
84
+ {alert.actions.length === 0 && (
85
+ <AtomicButton
86
+ title="Close"
87
+ onPress={handleClose}
88
+ fullWidth
89
+ />
90
+ )}
91
+ </View>
92
+ </View>
93
+ </View>
94
+ </View>
95
+ </Modal>
96
+ );
97
+ };
98
+
99
+ const styles = StyleSheet.create({
100
+ overlay: {
101
+ flex: 1,
102
+ justifyContent: 'center',
103
+ alignItems: 'center',
104
+ padding: 20,
105
+ },
106
+ backdrop: {
107
+ ...StyleSheet.absoluteFillObject,
108
+ backgroundColor: 'rgba(0,0,0,0.6)',
109
+ },
110
+ modal: {
111
+ width: '100%',
112
+ maxWidth: 400,
113
+ overflow: 'hidden',
114
+ },
115
+ header: {
116
+ padding: 20,
117
+ alignItems: 'center',
118
+ },
119
+ content: {
120
+ alignItems: 'center',
121
+ },
122
+ actions: {
123
+ width: '100%',
124
+ },
125
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * AlertProvider Component
3
+ */
4
+
5
+ import React from 'react';
6
+ import { AlertContainer } from './AlertContainer';
7
+
8
+ export const AlertProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
9
+ return (
10
+ <>
11
+ {children}
12
+ <AlertContainer />
13
+ </>
14
+ );
15
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Alert Service
3
+ */
4
+
5
+ import { generateUUID } from '@umituz/react-native-uuid';
6
+ import { Alert, AlertType, AlertMode, AlertOptions, AlertPosition } from './AlertTypes';
7
+
8
+ export class AlertService {
9
+ /**
10
+ * Creates a base Alert object with defaults
11
+ */
12
+ static createAlert(
13
+ type: AlertType,
14
+ defaultMode: AlertMode,
15
+ title: string,
16
+ message?: string,
17
+ options?: AlertOptions
18
+ ): Alert {
19
+ const id = generateUUID();
20
+ const mode = options?.mode || defaultMode;
21
+
22
+ // Default position based on mode
23
+ const defaultPosition = mode === AlertMode.BANNER ? AlertPosition.TOP : AlertPosition.TOP;
24
+
25
+ return {
26
+ id,
27
+ type,
28
+ mode,
29
+ title,
30
+ message,
31
+ position: options?.position || defaultPosition,
32
+ icon: options?.icon,
33
+ actions: options?.actions || [],
34
+ dismissible: options?.dismissible ?? true,
35
+ duration: options?.duration,
36
+ onDismiss: options?.onDismiss,
37
+ testID: options?.testID,
38
+ createdAt: Date.now(),
39
+ };
40
+ }
41
+
42
+ static createSuccessAlert(title: string, message?: string, options?: AlertOptions): Alert {
43
+ return this.createAlert(AlertType.SUCCESS, AlertMode.TOAST, title, message, options);
44
+ }
45
+
46
+ static createErrorAlert(title: string, message?: string, options?: AlertOptions): Alert {
47
+ return this.createAlert(AlertType.ERROR, AlertMode.TOAST, title, message, options);
48
+ }
49
+
50
+ static createWarningAlert(title: string, message?: string, options?: AlertOptions): Alert {
51
+ return this.createAlert(AlertType.WARNING, AlertMode.TOAST, title, message, options);
52
+ }
53
+
54
+ static createInfoAlert(title: string, message?: string, options?: AlertOptions): Alert {
55
+ return this.createAlert(AlertType.INFO, AlertMode.TOAST, title, message, options);
56
+ }
57
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Alert Store
3
+ */
4
+
5
+ import { create } from 'zustand';
6
+ import { Alert } from './AlertTypes';
7
+
8
+ interface AlertState {
9
+ alerts: Alert[];
10
+ addAlert: (alert: Alert) => void;
11
+ dismissAlert: (id: string) => void;
12
+ clearAlerts: () => void;
13
+ }
14
+
15
+ export const useAlertStore = create<AlertState>((set) => ({
16
+ alerts: [],
17
+ addAlert: (alert) => set((state) => ({ alerts: [...state.alerts, alert] })),
18
+ dismissAlert: (id) => set((state) => ({
19
+ alerts: state.alerts.filter((a) => a.id !== id)
20
+ })),
21
+ clearAlerts: () => set({ alerts: [] }),
22
+ }));
@@ -0,0 +1,215 @@
1
+ /**
2
+ * AlertToast Component
3
+ *
4
+ * Displays a toast-style alert.
5
+ * Floats on top of content.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { StyleSheet, View, Pressable, StyleProp, ViewStyle } from 'react-native';
10
+ import { AtomicText, AtomicIcon } from '../../atoms';
11
+ import { useAppDesignTokens } from '../../theme';
12
+ import { Alert, AlertType } from './AlertTypes';
13
+ import { useAlertStore } from './AlertStore';
14
+
15
+ interface AlertToastProps {
16
+ alert: Alert;
17
+ }
18
+
19
+ export function AlertToast({ alert }: AlertToastProps) {
20
+ const dismissAlert = useAlertStore((state) => state.dismissAlert);
21
+ const tokens = useAppDesignTokens();
22
+
23
+ const handleDismiss = () => {
24
+ if (alert.dismissible) {
25
+ dismissAlert(alert.id);
26
+ alert.onDismiss?.();
27
+ }
28
+ };
29
+
30
+ const getBackgroundColor = (type: AlertType): string => {
31
+ switch (type) {
32
+ case AlertType.SUCCESS:
33
+ return tokens.colors.success;
34
+ case AlertType.ERROR:
35
+ return tokens.colors.error;
36
+ case AlertType.WARNING:
37
+ return tokens.colors.warning;
38
+ case AlertType.INFO:
39
+ return tokens.colors.info;
40
+ default:
41
+ return tokens.colors.backgroundSecondary;
42
+ }
43
+ };
44
+
45
+ const getActionButtonStyle = (style: 'primary' | 'secondary' | 'destructive' | undefined): StyleProp<ViewStyle> => {
46
+ switch (style) {
47
+ case 'primary':
48
+ return { backgroundColor: tokens.colors.backgroundPrimary };
49
+ case 'secondary':
50
+ return {
51
+ backgroundColor: 'transparent',
52
+ borderWidth: 1,
53
+ borderColor: tokens.colors.textInverse,
54
+ };
55
+ case 'destructive':
56
+ return { backgroundColor: tokens.colors.error };
57
+ default:
58
+ return { backgroundColor: tokens.colors.backgroundSecondary };
59
+ }
60
+ };
61
+
62
+ const getActionTextColor = (style: 'primary' | 'secondary' | 'destructive' | undefined): string => {
63
+ switch (style) {
64
+ case 'primary':
65
+ return tokens.colors.textPrimary;
66
+ case 'secondary':
67
+ return tokens.colors.textInverse;
68
+ case 'destructive':
69
+ return tokens.colors.textInverse;
70
+ default:
71
+ return tokens.colors.textPrimary;
72
+ }
73
+ };
74
+
75
+ const backgroundColor = getBackgroundColor(alert.type);
76
+ const textColor = tokens.colors.textInverse;
77
+
78
+ return (
79
+ <View
80
+ style={[
81
+ styles.container,
82
+ {
83
+ backgroundColor,
84
+ padding: tokens.spacing.md,
85
+ borderRadius: tokens.borders.radius.md,
86
+ },
87
+ ]}
88
+ testID={alert.testID}
89
+ >
90
+ <Pressable onPress={handleDismiss} style={styles.content}>
91
+ <View style={styles.row}>
92
+ {alert.icon && (
93
+ <View style={[styles.iconContainer, { marginRight: tokens.spacing.sm }]}>
94
+ <AtomicIcon
95
+ name={alert.icon}
96
+ customSize={20}
97
+ customColor={textColor}
98
+ />
99
+ </View>
100
+ )}
101
+
102
+ <View style={styles.textContainer}>
103
+ <AtomicText
104
+ type="bodyMedium"
105
+ style={[styles.title, { color: textColor }]}
106
+ numberOfLines={2}
107
+ >
108
+ {alert.title}
109
+ </AtomicText>
110
+
111
+ {alert.message && (
112
+ <AtomicText
113
+ type="bodySmall"
114
+ style={[
115
+ styles.message,
116
+ { color: textColor, marginTop: tokens.spacing.xs },
117
+ ]}
118
+ numberOfLines={3}
119
+ >
120
+ {alert.message}
121
+ </AtomicText>
122
+ )}
123
+ </View>
124
+
125
+ {alert.dismissible && (
126
+ <Pressable
127
+ onPress={handleDismiss}
128
+ style={[styles.closeButton, { marginLeft: tokens.spacing.sm }]}
129
+ hitSlop={8}
130
+ >
131
+ <AtomicIcon name="close" customSize={20} customColor={textColor} />
132
+ </Pressable>
133
+ )}
134
+ </View>
135
+
136
+ {alert.actions && alert.actions.length > 0 && (
137
+ <View style={[styles.actionsContainer, { marginTop: tokens.spacing.sm }]}>
138
+ {alert.actions.map((action) => (
139
+ <Pressable
140
+ key={action.id}
141
+ onPress={async () => {
142
+ await action.onPress();
143
+ if (action.closeOnPress ?? true) {
144
+ handleDismiss();
145
+ }
146
+ }}
147
+ style={[
148
+ styles.actionButton,
149
+ {
150
+ paddingVertical: tokens.spacing.xs,
151
+ paddingHorizontal: tokens.spacing.sm,
152
+ marginRight: tokens.spacing.xs,
153
+ borderRadius: tokens.borders.radius.sm,
154
+ },
155
+ getActionButtonStyle(action.style),
156
+ ]}
157
+ >
158
+ <AtomicText
159
+ type="bodySmall"
160
+ style={[
161
+ styles.actionText,
162
+ { color: getActionTextColor(action.style) },
163
+ ]}
164
+ >
165
+ {action.label}
166
+ </AtomicText>
167
+ </Pressable>
168
+ ))}
169
+ </View>
170
+ )}
171
+ </Pressable>
172
+ </View>
173
+ );
174
+ }
175
+
176
+ const styles = StyleSheet.create({
177
+ container: {
178
+ width: '100%',
179
+ },
180
+ content: {
181
+ flex: 1,
182
+ },
183
+ row: {
184
+ flexDirection: 'row',
185
+ alignItems: 'center',
186
+ },
187
+ iconContainer: {
188
+ justifyContent: 'center',
189
+ alignItems: 'center',
190
+ },
191
+ textContainer: {
192
+ flex: 1,
193
+ },
194
+ title: {
195
+ fontWeight: '700',
196
+ },
197
+ message: {
198
+ opacity: 0.9,
199
+ },
200
+ closeButton: {
201
+ justifyContent: 'center',
202
+ alignItems: 'center',
203
+ },
204
+ actionsContainer: {
205
+ flexDirection: 'row',
206
+ flexWrap: 'wrap',
207
+ },
208
+ actionButton: {
209
+ justifyContent: 'center',
210
+ alignItems: 'center',
211
+ },
212
+ actionText: {
213
+ fontWeight: '700',
214
+ },
215
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Alert Entity and Types
3
+ */
4
+
5
+ export enum AlertType {
6
+ SUCCESS = 'success',
7
+ ERROR = 'error',
8
+ WARNING = 'warning',
9
+ INFO = 'info',
10
+ }
11
+
12
+ export enum AlertMode {
13
+ TOAST = 'toast',
14
+ BANNER = 'banner',
15
+ MODAL = 'modal',
16
+ INLINE = 'inline',
17
+ }
18
+
19
+ export enum AlertPosition {
20
+ TOP = 'top',
21
+ BOTTOM = 'bottom',
22
+ CENTER = 'center',
23
+ }
24
+
25
+ export interface AlertAction {
26
+ id: string;
27
+ label: string;
28
+ onPress: () => Promise<void> | void;
29
+ style?: 'primary' | 'secondary' | 'destructive';
30
+ closeOnPress?: boolean;
31
+ }
32
+
33
+ export interface AlertOptions {
34
+ mode?: AlertMode;
35
+ duration?: number;
36
+ dismissible?: boolean;
37
+ onDismiss?: () => void;
38
+ icon?: string;
39
+ actions?: AlertAction[];
40
+ testID?: string;
41
+ position?: AlertPosition;
42
+ }
43
+
44
+ export interface Alert {
45
+ id: string;
46
+ type: AlertType;
47
+ mode: AlertMode;
48
+ title: string;
49
+ message?: string;
50
+ position: AlertPosition;
51
+ icon?: string;
52
+ actions: AlertAction[];
53
+ dismissible: boolean;
54
+ duration?: number;
55
+ createdAt: number;
56
+ testID?: string;
57
+ onDismiss?: () => void;
58
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Alerts Molecule - Public API
3
+ */
4
+
5
+ export * from './AlertTypes';
6
+ export { useAlertStore } from './AlertStore';
7
+ export { AlertService } from './AlertService';
8
+ export { AlertBanner } from './AlertBanner';
9
+ export { AlertToast } from './AlertToast';
10
+ export { AlertInline } from './AlertInline';
11
+ export { AlertModal } from './AlertModal';
12
+ export { AlertContainer } from './AlertContainer';
13
+ export { AlertProvider } from './AlertProvider';
14
+ export { useAlert } from './useAlert';
15
+
16
+ import { AlertService } from './AlertService';
17
+ import { useAlertStore } from './AlertStore';
18
+ import { AlertOptions } from './AlertTypes';
19
+
20
+ /**
21
+ * Convenience alert service for use outside of components
22
+ */
23
+ export const alertService = {
24
+ error: (title: string, message?: string, options?: AlertOptions) => {
25
+ const alert = AlertService.createErrorAlert(title, message, options);
26
+ useAlertStore.getState().addAlert(alert);
27
+ return alert.id;
28
+ },
29
+ success: (title: string, message?: string, options?: AlertOptions) => {
30
+ const alert = AlertService.createSuccessAlert(title, message, options);
31
+ useAlertStore.getState().addAlert(alert);
32
+ return alert.id;
33
+ },
34
+ warning: (title: string, message?: string, options?: AlertOptions) => {
35
+ const alert = AlertService.createWarningAlert(title, message, options);
36
+ useAlertStore.getState().addAlert(alert);
37
+ return alert.id;
38
+ },
39
+ info: (title: string, message?: string, options?: AlertOptions) => {
40
+ const alert = AlertService.createInfoAlert(title, message, options);
41
+ useAlertStore.getState().addAlert(alert);
42
+ return alert.id;
43
+ },
44
+ dismiss: (id: string) => {
45
+ useAlertStore.getState().dismissAlert(id);
46
+ },
47
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * useAlert Hook
3
+ */
4
+
5
+ import { useCallback } from 'react';
6
+ import { useAlertStore } from './AlertStore';
7
+ import { AlertService } from './AlertService';
8
+ import { AlertType, AlertMode, AlertOptions } from './AlertTypes';
9
+
10
+ export interface UseAlertReturn {
11
+ show: (type: AlertType, mode: AlertMode, title: string, message?: string, options?: AlertOptions) => string;
12
+ showSuccess: (title: string, message?: string, options?: AlertOptions) => string;
13
+ showError: (title: string, message?: string, options?: AlertOptions) => string;
14
+ showWarning: (title: string, message?: string, options?: AlertOptions) => string;
15
+ showInfo: (title: string, message?: string, options?: AlertOptions) => string;
16
+ dismissAlert: (id: string) => void;
17
+ clearAlerts: () => void;
18
+ }
19
+
20
+ export const useAlert = (): UseAlertReturn => {
21
+ const { addAlert, dismissAlert, clearAlerts } = useAlertStore();
22
+
23
+ const show = useCallback((
24
+ type: AlertType,
25
+ mode: AlertMode,
26
+ title: string,
27
+ message?: string,
28
+ options?: AlertOptions
29
+ ) => {
30
+ const alert = AlertService.createAlert(type, mode, title, message, options);
31
+ addAlert(alert);
32
+ return alert.id;
33
+ }, [addAlert]);
34
+
35
+ const showSuccess = useCallback((title: string, message?: string, options?: AlertOptions) => {
36
+ const alert = AlertService.createSuccessAlert(title, message, options);
37
+ addAlert(alert);
38
+ return alert.id;
39
+ }, [addAlert]);
40
+
41
+ const showError = useCallback((title: string, message?: string, options?: AlertOptions) => {
42
+ const alert = AlertService.createErrorAlert(title, message, options);
43
+ addAlert(alert);
44
+ return alert.id;
45
+ }, [addAlert]);
46
+
47
+ const showWarning = useCallback((title: string, message?: string, options?: AlertOptions) => {
48
+ const alert = AlertService.createWarningAlert(title, message, options);
49
+ addAlert(alert);
50
+ return alert.id;
51
+ }, [addAlert]);
52
+
53
+ const showInfo = useCallback((title: string, message?: string, options?: AlertOptions) => {
54
+ const alert = AlertService.createInfoAlert(title, message, options);
55
+ addAlert(alert);
56
+ return alert.id;
57
+ }, [addAlert]);
58
+
59
+ return {
60
+ show,
61
+ showSuccess,
62
+ showError,
63
+ showWarning,
64
+ showInfo,
65
+ dismissAlert,
66
+ clearAlerts,
67
+ };
68
+ };
@@ -27,3 +27,6 @@ export * from "./StepProgress";
27
27
  export { Grid, type GridProps } from './Grid';
28
28
  export { List, type ListProps } from './List';
29
29
  export { Container, type ContainerProps } from './Container';
30
+
31
+ // Alerts
32
+ export * from './alerts';
@@ -24,10 +24,13 @@
24
24
  */
25
25
 
26
26
  import React, { useMemo } from 'react';
27
- import { View, ScrollView, StyleSheet, type ViewStyle } from 'react-native';
28
- import { SafeAreaView } from 'react-native-safe-area-context';
27
+ import { View, ScrollView, StyleSheet, type ViewStyle, RefreshControlProps } from 'react-native';
28
+ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
29
29
  import type { Edge } from 'react-native-safe-area-context';
30
30
  import { useAppDesignTokens } from '../theme';
31
+ import { useResponsive } from '../responsive/useResponsive';
32
+ import { getResponsiveMaxWidth } from '../responsive/responsiveSizing';
33
+ import { getResponsiveHorizontalPadding } from '../responsive/responsiveLayout';
31
34
 
32
35
  /**
33
36
  * NOTE: This component now works in conjunction with the SafeAreaProvider
@@ -124,6 +127,20 @@ export interface ScreenLayoutProps {
124
127
  */
125
128
  accessible?: boolean;
126
129
 
130
+ /**
131
+ * Enable responsive content width and centering (default: true)
132
+ */
133
+ responsiveEnabled?: boolean;
134
+
135
+ /**
136
+ * Maximum content width override
137
+ */
138
+ maxWidth?: number;
139
+
140
+ /**
141
+ * RefreshControl component for pull-to-refresh
142
+ */
143
+ refreshControl?: React.ReactElement<RefreshControlProps>;
127
144
  }
128
145
 
129
146
  export const ScreenLayout: React.FC<ScreenLayoutProps> = ({
@@ -138,25 +155,41 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = ({
138
155
  testID,
139
156
  hideScrollIndicator = false,
140
157
  keyboardAvoiding = false,
158
+ responsiveEnabled = true,
159
+ maxWidth,
160
+ refreshControl,
141
161
  }) => {
142
162
  // Automatically uses current theme from global store
143
163
  const tokens = useAppDesignTokens();
164
+ const { isTabletDevice } = useResponsive();
165
+ const insets = useSafeAreaInsets();
166
+
167
+ const finalMaxWidth = maxWidth || (responsiveEnabled ? getResponsiveMaxWidth() : undefined);
168
+ const horizontalPadding = responsiveEnabled ? getResponsiveHorizontalPadding(tokens.spacing.md, insets) : tokens.spacing.md;
169
+
144
170
  const styles = useMemo(() => StyleSheet.create({
145
171
  container: {
146
172
  flex: 1,
147
173
  },
174
+ responsiveWrapper: {
175
+ flex: 1,
176
+ width: '100%',
177
+ maxWidth: finalMaxWidth,
178
+ alignSelf: 'center',
179
+ },
148
180
  content: {
149
181
  flex: 1,
182
+ paddingHorizontal: horizontalPadding,
150
183
  },
151
184
  scrollView: {
152
185
  flex: 1,
153
186
  },
154
187
  scrollContent: {
155
188
  flexGrow: 1,
156
- paddingHorizontal: tokens.spacing.md,
189
+ paddingHorizontal: horizontalPadding,
157
190
  paddingBottom: tokens.spacing.lg,
158
191
  },
159
- }), [tokens]);
192
+ }), [tokens, finalMaxWidth, horizontalPadding]);
160
193
 
161
194
  const bgColor = backgroundColor || tokens.colors.backgroundPrimary;
162
195
 
@@ -168,11 +201,13 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = ({
168
201
  edges={edges}
169
202
  testID={testID}
170
203
  >
171
- {header}
172
- <View style={[styles.content, contentContainerStyle]}>
173
- {children}
204
+ <View style={styles.responsiveWrapper}>
205
+ {header}
206
+ <View style={[styles.content, contentContainerStyle]}>
207
+ {children}
208
+ </View>
209
+ {footer}
174
210
  </View>
175
- {footer}
176
211
  </SafeAreaView>
177
212
  );
178
213
  }
@@ -184,16 +219,19 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = ({
184
219
  edges={edges}
185
220
  testID={testID}
186
221
  >
187
- {header}
188
- <ScrollView
189
- style={styles.scrollView}
190
- contentContainerStyle={[styles.scrollContent, contentContainerStyle]}
191
- showsVerticalScrollIndicator={!hideScrollIndicator}
192
- keyboardShouldPersistTaps={keyboardAvoiding ? 'handled' : 'never'}
193
- >
194
- {children}
195
- </ScrollView>
196
- {footer}
222
+ <View style={styles.responsiveWrapper}>
223
+ {header}
224
+ <ScrollView
225
+ style={styles.scrollView}
226
+ contentContainerStyle={[styles.scrollContent, contentContainerStyle]}
227
+ showsVerticalScrollIndicator={!hideScrollIndicator}
228
+ keyboardShouldPersistTaps={keyboardAvoiding ? 'handled' : 'never'}
229
+ refreshControl={refreshControl}
230
+ >
231
+ {children}
232
+ </ScrollView>
233
+ {footer}
234
+ </View>
197
235
  </SafeAreaView>
198
236
  );
199
237
  };