@umituz/react-native-design-system 2.6.34 → 2.6.38

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.6.34",
3
+ "version": "2.6.38",
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",
@@ -51,7 +51,6 @@
51
51
  "peerDependencies": {
52
52
  "@expo/vector-icons": ">=15.0.0",
53
53
  "@gorhom/bottom-sheet": ">=5.0.0",
54
- "@react-native-async-storage/async-storage": ">=2.0.0",
55
54
  "@react-native-community/datetimepicker": ">=8.0.0",
56
55
  "@react-navigation/bottom-tabs": ">=7.0.0",
57
56
  "@react-navigation/native": ">=7.0.0",
@@ -98,7 +97,6 @@
98
97
  "@eslint/js": "^9.39.2",
99
98
  "@expo/vector-icons": "^15.0.0",
100
99
  "@gorhom/bottom-sheet": "^5.0.0",
101
- "@react-native-async-storage/async-storage": "^2.2.0",
102
100
  "@react-native-community/datetimepicker": "^8.5.1",
103
101
  "@react-navigation/bottom-tabs": "^7.9.0",
104
102
  "@react-navigation/native": "^7.1.26",
@@ -113,7 +111,7 @@
113
111
  "@umituz/react-native-filesystem": "^2.1.7",
114
112
  "@umituz/react-native-haptics": "^1.0.2",
115
113
  "@umituz/react-native-localization": "^3.5.34",
116
- "@umituz/react-native-storage": "latest",
114
+ "@umituz/react-native-storage": "^2.6.20",
117
115
  "@umituz/react-native-uuid": "*",
118
116
  "eslint": "^9.39.2",
119
117
  "eslint-plugin-react": "^7.37.5",
@@ -43,7 +43,15 @@ export interface AtomicInputProps {
43
43
  /** Show character counter */
44
44
  showCharacterCount?: boolean;
45
45
  /** Keyboard type */
46
- keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'url' | 'number-pad' | 'decimal-pad';
46
+ keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'url' | 'number-pad' | 'decimal-pad' | 'web-search' | 'twitter' | 'numeric' | 'visible-password';
47
+ /** Return key type */
48
+ returnKeyType?: 'done' | 'go' | 'next' | 'search' | 'send';
49
+ /** Callback when submit button is pressed */
50
+ onSubmitEditing?: () => void;
51
+ /** Blur on submit */
52
+ blurOnSubmit?: boolean;
53
+ /** Auto focus */
54
+ autoFocus?: boolean;
47
55
  /** Auto-capitalize */
48
56
  autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
49
57
  /** Auto-correct */
@@ -78,7 +86,7 @@ export interface AtomicInputProps {
78
86
  * - Responsive sizing
79
87
  * - Full accessibility support
80
88
  */
81
- export const AtomicInput: React.FC<AtomicInputProps> = ({
89
+ export const AtomicInput = React.forwardRef<TextInput, AtomicInputProps>(({
82
90
  variant = 'outlined',
83
91
  state = 'default',
84
92
  size = 'md',
@@ -95,6 +103,10 @@ export const AtomicInput: React.FC<AtomicInputProps> = ({
95
103
  maxLength,
96
104
  showCharacterCount = false,
97
105
  keyboardType = 'default',
106
+ returnKeyType,
107
+ onSubmitEditing,
108
+ blurOnSubmit,
109
+ autoFocus,
98
110
  autoCapitalize = 'sentences',
99
111
  autoCorrect = true,
100
112
  disabled = false,
@@ -105,7 +117,7 @@ export const AtomicInput: React.FC<AtomicInputProps> = ({
105
117
  onFocus,
106
118
  multiline = false,
107
119
  numberOfLines,
108
- }) => {
120
+ }, ref) => {
109
121
  const tokens = useAppDesignTokens();
110
122
 
111
123
  const {
@@ -198,6 +210,7 @@ export const AtomicInput: React.FC<AtomicInputProps> = ({
198
210
  )}
199
211
 
200
212
  <TextInput
213
+ ref={ref}
201
214
  value={localValue}
202
215
  onChangeText={handleTextChange}
203
216
  placeholder={placeholder}
@@ -205,6 +218,10 @@ export const AtomicInput: React.FC<AtomicInputProps> = ({
205
218
  secureTextEntry={secureTextEntry && !isPasswordVisible}
206
219
  maxLength={maxLength}
207
220
  keyboardType={keyboardType}
221
+ returnKeyType={returnKeyType}
222
+ onSubmitEditing={onSubmitEditing}
223
+ blurOnSubmit={blurOnSubmit}
224
+ autoFocus={autoFocus}
208
225
  autoCapitalize={autoCapitalize}
209
226
  autoCorrect={autoCorrect}
210
227
  editable={!isDisabled}
@@ -274,7 +291,7 @@ export const AtomicInput: React.FC<AtomicInputProps> = ({
274
291
  )}
275
292
  </View>
276
293
  );
277
- };
294
+ });
278
295
 
279
296
  const styles = StyleSheet.create({
280
297
  container: {
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import {
3
+ KeyboardAvoidingView,
4
+ Platform,
5
+ StyleSheet,
6
+ type KeyboardAvoidingViewProps,
7
+ } from 'react-native';
8
+
9
+ export interface AtomicKeyboardAvoidingViewProps extends KeyboardAvoidingViewProps {
10
+ /**
11
+ * Optional offset to adjust the position of the content.
12
+ * On iOS, this is often necessary to account for headers, tabs, etc.
13
+ */
14
+ offset?: number;
15
+ }
16
+
17
+ /**
18
+ * AtomicKeyboardAvoidingView - A consistent wrapper for React Native's KeyboardAvoidingView
19
+ *
20
+ * Provides sensible defaults and OS-specific behaviors:
21
+ * - iOS: behavior="padding"
22
+ * - Android: behavior=undefined (handled by windowSoftInputMode="adjustResize")
23
+ */
24
+ export const AtomicKeyboardAvoidingView: React.FC<AtomicKeyboardAvoidingViewProps> = ({
25
+ children,
26
+ behavior,
27
+ style,
28
+ offset = 0,
29
+ ...props
30
+ }) => {
31
+ const defaultBehavior = Platform.OS === 'ios' ? 'padding' : undefined;
32
+
33
+ return (
34
+ <KeyboardAvoidingView
35
+ behavior={behavior ?? defaultBehavior}
36
+ style={[styles.container, style]}
37
+ keyboardVerticalOffset={offset}
38
+ {...props}
39
+ >
40
+ {children}
41
+ </KeyboardAvoidingView>
42
+ );
43
+ };
44
+
45
+ const styles = StyleSheet.create({
46
+ container: {
47
+ flex: 1,
48
+ },
49
+ });
@@ -1,32 +1,52 @@
1
- /**
2
- * AtomicTextArea - Multiline Text Input Component
3
- *
4
- * Atomic Design Level: ATOM
5
- * Purpose: Multiline text input across all apps
6
- */
7
-
8
- import React from 'react';
9
- import { View, TextInput, StyleSheet, ViewStyle } from 'react-native';
1
+ import React, { forwardRef } from 'react';
2
+ import { View, TextInput, StyleSheet, type ViewStyle, type StyleProp, type TextStyle } from 'react-native';
10
3
  import { useAppDesignTokens } from '../theme';
11
4
  import { AtomicText } from './AtomicText';
12
5
 
13
6
  export interface AtomicTextAreaProps {
7
+ /** Text area label */
14
8
  label?: string;
9
+ /** Current value */
15
10
  value?: string;
11
+ /** Value change callback */
16
12
  onChangeText?: (text: string) => void;
13
+ /** Placeholder text */
17
14
  placeholder?: string;
15
+ /** Helper text below input */
18
16
  helperText?: string;
17
+ /** Error message to display */
19
18
  errorText?: string;
19
+ /** Maximum character length */
20
20
  maxLength?: number;
21
+ /** Number of lines (default: 4) */
21
22
  numberOfLines?: number;
23
+ /** Alternative to numberOfLines */
22
24
  rows?: number;
25
+ /** Minimum height override */
23
26
  minHeight?: number;
27
+ /** Disabled state */
24
28
  disabled?: boolean;
25
- style?: ViewStyle;
29
+ /** Container style */
30
+ style?: StyleProp<ViewStyle>;
31
+ /** Input text style */
32
+ inputStyle?: StyleProp<TextStyle>;
33
+ /** Auto focus */
34
+ autoFocus?: boolean;
35
+ /** Return key type */
36
+ returnKeyType?: 'done' | 'go' | 'next' | 'search' | 'send';
37
+ /** Callback when submit button is pressed */
38
+ onSubmitEditing?: () => void;
39
+ /** Blur on submit */
40
+ blurOnSubmit?: boolean;
41
+ /** Test ID */
26
42
  testID?: string;
27
43
  }
28
44
 
29
- export const AtomicTextArea: React.FC<AtomicTextAreaProps> = ({
45
+ /**
46
+ * AtomicTextArea - Multiline Text Input Component
47
+ * Consistent with AtomicInput but optimized for multiline usage.
48
+ */
49
+ export const AtomicTextArea = forwardRef<TextInput, AtomicTextAreaProps>(({
30
50
  label,
31
51
  value,
32
52
  onChangeText,
@@ -39,8 +59,13 @@ export const AtomicTextArea: React.FC<AtomicTextAreaProps> = ({
39
59
  minHeight,
40
60
  disabled = false,
41
61
  style,
62
+ inputStyle,
63
+ autoFocus,
64
+ returnKeyType,
65
+ onSubmitEditing,
66
+ blurOnSubmit,
42
67
  testID,
43
- }) => {
68
+ }, ref) => {
44
69
  const lineCount = numberOfLines ?? rows;
45
70
  const calculatedMinHeight = minHeight ?? lineCount * 24;
46
71
  const tokens = useAppDesignTokens();
@@ -51,20 +76,26 @@ export const AtomicTextArea: React.FC<AtomicTextAreaProps> = ({
51
76
  {label && (
52
77
  <AtomicText
53
78
  type="labelMedium"
54
- style={[styles.label, { color: tokens.colors.textSecondary }]}
79
+ color={hasError ? 'error' : 'secondary'}
80
+ style={styles.label}
55
81
  >
56
82
  {label}
57
83
  </AtomicText>
58
84
  )}
59
85
  <TextInput
86
+ ref={ref}
60
87
  value={value}
61
88
  onChangeText={onChangeText}
62
89
  placeholder={placeholder}
63
- placeholderTextColor={tokens.colors.textTertiary}
90
+ placeholderTextColor={tokens.colors.textSecondary}
64
91
  maxLength={maxLength}
65
92
  numberOfLines={lineCount}
66
93
  multiline
67
94
  editable={!disabled}
95
+ autoFocus={autoFocus}
96
+ returnKeyType={returnKeyType}
97
+ onSubmitEditing={onSubmitEditing}
98
+ blurOnSubmit={blurOnSubmit}
68
99
  textAlignVertical="top"
69
100
  style={[
70
101
  styles.input,
@@ -73,39 +104,57 @@ export const AtomicTextArea: React.FC<AtomicTextAreaProps> = ({
73
104
  borderColor: hasError ? tokens.colors.error : tokens.colors.border,
74
105
  color: tokens.colors.textPrimary,
75
106
  minHeight: calculatedMinHeight,
107
+ padding: tokens.spacing.md,
108
+ borderRadius: tokens.borderRadius.md,
109
+ fontSize: 16,
76
110
  },
111
+ inputStyle,
77
112
  disabled && { opacity: 0.5 },
78
113
  ]}
79
114
  />
80
115
  {(helperText || errorText) && (
81
- <AtomicText
82
- type="bodySmall"
83
- style={[
84
- styles.helperText,
85
- { color: hasError ? tokens.colors.error : tokens.colors.textSecondary },
86
- ]}
87
- >
88
- {errorText || helperText}
89
- </AtomicText>
116
+ <View style={styles.helperRow}>
117
+ <AtomicText
118
+ type="bodySmall"
119
+ color={hasError ? 'error' : 'secondary'}
120
+ style={styles.helperText}
121
+ >
122
+ {errorText || helperText}
123
+ </AtomicText>
124
+ {maxLength && value !== undefined && (
125
+ <AtomicText
126
+ type="labelSmall"
127
+ color="secondary"
128
+ style={styles.characterCount}
129
+ >
130
+ {value.length}/{maxLength}
131
+ </AtomicText>
132
+ )}
133
+ </View>
90
134
  )}
91
135
  </View>
92
136
  );
93
- };
137
+ });
94
138
 
95
139
  const styles = StyleSheet.create({
96
140
  container: {
97
- marginBottom: 16,
141
+ width: '100%',
98
142
  },
99
143
  label: {
100
144
  marginBottom: 8,
101
145
  },
102
146
  input: {
103
147
  borderWidth: 1,
104
- borderRadius: 12,
105
- padding: 12,
106
- fontSize: 16,
107
148
  },
108
- helperText: {
149
+ helperRow: {
150
+ flexDirection: 'row',
151
+ justifyContent: 'space-between',
109
152
  marginTop: 4,
110
153
  },
154
+ helperText: {
155
+ flex: 1,
156
+ },
157
+ characterCount: {
158
+ marginLeft: 8,
159
+ },
111
160
  });
@@ -111,3 +111,6 @@ export { AtomicTouchable, type AtomicTouchableProps } from './AtomicTouchable';
111
111
 
112
112
  // StatusBar
113
113
  export { AtomicStatusBar, type AtomicStatusBarProps } from './status-bar';
114
+
115
+ // Keyboard Avoiding
116
+ export { AtomicKeyboardAvoidingView, type AtomicKeyboardAvoidingViewProps } from './AtomicKeyboardAvoidingView';
@@ -9,7 +9,7 @@
9
9
  * @layer infrastructure/services
10
10
  */
11
11
 
12
- import AsyncStorage from '@react-native-async-storage/async-storage';
12
+ import { storageRepository, unwrap } from '@umituz/react-native-storage';
13
13
  import { DeviceIdService } from './DeviceIdService';
14
14
 
15
15
  const STORAGE_KEY = '@device/persistent_id';
@@ -64,7 +64,8 @@ export class PersistentDeviceIdService {
64
64
  */
65
65
  private static async initializeDeviceId(): Promise<string> {
66
66
  try {
67
- const storedId = await AsyncStorage.getItem(STORAGE_KEY);
67
+ const result = await storageRepository.getString(STORAGE_KEY, '');
68
+ const storedId = unwrap(result, '');
68
69
 
69
70
  if (storedId) {
70
71
  cachedDeviceId = storedId;
@@ -72,7 +73,7 @@ export class PersistentDeviceIdService {
72
73
  }
73
74
 
74
75
  const newId = await this.createNewDeviceId();
75
- await AsyncStorage.setItem(STORAGE_KEY, newId);
76
+ await storageRepository.setString(STORAGE_KEY, newId);
76
77
  cachedDeviceId = newId;
77
78
 
78
79
  return newId;
@@ -101,8 +102,7 @@ export class PersistentDeviceIdService {
101
102
  */
102
103
  static async hasStoredId(): Promise<boolean> {
103
104
  try {
104
- const storedId = await AsyncStorage.getItem(STORAGE_KEY);
105
- return storedId !== null;
105
+ return await storageRepository.hasItem(STORAGE_KEY);
106
106
  } catch {
107
107
  return false;
108
108
  }
@@ -114,7 +114,7 @@ export class PersistentDeviceIdService {
114
114
  */
115
115
  static async clearStoredId(): Promise<void> {
116
116
  try {
117
- await AsyncStorage.removeItem(STORAGE_KEY);
117
+ await storageRepository.removeItem(STORAGE_KEY);
118
118
  cachedDeviceId = null;
119
119
  initializationPromise = null;
120
120
  } catch {
@@ -6,9 +6,10 @@
6
6
  */
7
7
 
8
8
  import React, { useMemo } from 'react';
9
- import { View, ScrollView, KeyboardAvoidingView, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
9
+ import { View, ScrollView, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
10
10
  import { useAppDesignTokens } from '../../theme';
11
11
  import { useResponsive } from '../../responsive';
12
+ import { AtomicKeyboardAvoidingView } from '../../atoms';
12
13
 
13
14
  export interface FormLayoutProps {
14
15
  /** Form fields and content */
@@ -103,9 +104,9 @@ export const FormLayout: React.FC<FormLayoutProps> = ({
103
104
  const mainContent = disableKeyboardAvoid ? (
104
105
  scrollableContent
105
106
  ) : (
106
- <KeyboardAvoidingView style={styles.container} behavior="padding">
107
+ <AtomicKeyboardAvoidingView style={styles.container}>
107
108
  {scrollableContent}
108
- </KeyboardAvoidingView>
109
+ </AtomicKeyboardAvoidingView>
109
110
  );
110
111
 
111
112
  return (
@@ -24,10 +24,11 @@
24
24
  */
25
25
 
26
26
  import React, { useMemo } from 'react';
27
- import { View, ScrollView, StyleSheet, KeyboardAvoidingView, type ViewStyle, type RefreshControlProps } from 'react-native';
27
+ import { View, ScrollView, StyleSheet, type ViewStyle, type RefreshControlProps } from 'react-native';
28
28
  import { SafeAreaView, useSafeAreaInsets, type Edge } from '../../safe-area';
29
29
  import { useAppDesignTokens } from '../../theme';
30
30
  import { getScreenLayoutConfig } from '../../responsive/responsiveLayout';
31
+ import { AtomicKeyboardAvoidingView } from '../../atoms';
31
32
 
32
33
  /**
33
34
  * NOTE: This component now works in conjunction with the SafeAreaProvider
@@ -206,12 +207,11 @@ export const ScreenLayout: React.FC<ScreenLayoutProps> = ({
206
207
  const ContentWrapper: React.FC<{ children: React.ReactNode }> = ({ children: wrapperChildren }) => {
207
208
  if (keyboardAvoiding) {
208
209
  return (
209
- <KeyboardAvoidingView
210
+ <AtomicKeyboardAvoidingView
210
211
  style={styles.keyboardAvoidingView}
211
- behavior="padding"
212
212
  >
213
213
  {wrapperChildren}
214
- </KeyboardAvoidingView>
214
+ </AtomicKeyboardAvoidingView>
215
215
  );
216
216
  }
217
217
  return <>{wrapperChildren}</>;
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { create } from 'zustand';
15
15
  import { persist, createJSONStorage } from 'zustand/middleware';
16
- import AsyncStorage from '@react-native-async-storage/async-storage';
16
+ import { storageRepository, unwrap, storageService } from '@umituz/react-native-storage';
17
17
  import type { CalendarEvent, CreateCalendarEventRequest, UpdateCalendarEventRequest } from '../../domain/entities/CalendarEvent.entity';
18
18
  import { CalendarService } from '../services/CalendarService';
19
19
 
@@ -101,16 +101,17 @@ export const useCalendarStore = create<CalendarState & { actions: CalendarAction
101
101
  loadEvents: async () => {
102
102
  set({ isLoading: true, error: null });
103
103
  try {
104
- const stored = await AsyncStorage.getItem(STORAGE_KEY);
105
- if (stored) {
106
- const parsed = JSON.parse(stored) as CalendarEvent[];
104
+ const result = await storageRepository.getItem<CalendarEvent[]>(STORAGE_KEY, []);
105
+ const events = unwrap(result, []);
106
+
107
+ if (events && events.length > 0) {
107
108
  // Restore Date objects
108
- const events = parsed.map((event) => ({
109
+ const hydratedEvents = events.map((event) => ({
109
110
  ...event,
110
111
  createdAt: new Date(event.createdAt),
111
112
  updatedAt: new Date(event.updatedAt),
112
113
  }));
113
- set({ events, isLoading: false });
114
+ set({ events: hydratedEvents, isLoading: false });
114
115
  } else {
115
116
  set({ isLoading: false });
116
117
  }
@@ -137,7 +138,7 @@ export const useCalendarStore = create<CalendarState & { actions: CalendarAction
137
138
  };
138
139
 
139
140
  const events = [...get().events, newEvent];
140
- await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(events));
141
+ await storageRepository.setItem(STORAGE_KEY, events);
141
142
  set({ events, isLoading: false });
142
143
  } catch {
143
144
  set({
@@ -164,7 +165,7 @@ export const useCalendarStore = create<CalendarState & { actions: CalendarAction
164
165
  return event;
165
166
  });
166
167
 
167
- await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(events));
168
+ await storageRepository.setItem(STORAGE_KEY, events);
168
169
  set({ events, isLoading: false });
169
170
  } catch {
170
171
  set({
@@ -181,7 +182,7 @@ export const useCalendarStore = create<CalendarState & { actions: CalendarAction
181
182
  set({ isLoading: true, error: null });
182
183
  try {
183
184
  const events = get().events.filter((event) => event.id !== id);
184
- await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(events));
185
+ await storageRepository.setItem(STORAGE_KEY, events);
185
186
  set({ events, isLoading: false });
186
187
  } catch {
187
188
  set({
@@ -294,7 +295,7 @@ export const useCalendarStore = create<CalendarState & { actions: CalendarAction
294
295
  clearAllEvents: async () => {
295
296
  set({ isLoading: true, error: null });
296
297
  try {
297
- await AsyncStorage.removeItem(STORAGE_KEY);
298
+ await storageRepository.removeItem(STORAGE_KEY);
298
299
  set({ events: [], isLoading: false });
299
300
  } catch {
300
301
  set({
@@ -307,7 +308,7 @@ export const useCalendarStore = create<CalendarState & { actions: CalendarAction
307
308
  }),
308
309
  {
309
310
  name: 'calendar-storage',
310
- storage: createJSONStorage(() => AsyncStorage),
311
+ storage: createJSONStorage(() => storageService),
311
312
  partialize: (state) => ({ events: state.events }),
312
313
  }
313
314
  )
@@ -6,7 +6,7 @@
6
6
  * Apps should use this for theme persistence.
7
7
  */
8
8
 
9
- import AsyncStorage from '@react-native-async-storage/async-storage';
9
+ import { storageRepository, unwrap } from '@umituz/react-native-storage';
10
10
  import type { ThemeMode } from '../../core/ColorPalette';
11
11
  import { DESIGN_CONSTANTS } from '../../core/constants/DesignConstants';
12
12
 
@@ -18,7 +18,9 @@ export class ThemeStorage {
18
18
  */
19
19
  static async getThemeMode(): Promise<ThemeMode | null> {
20
20
  try {
21
- const value = await AsyncStorage.getItem(STORAGE_KEY);
21
+ const result = await storageRepository.getString(STORAGE_KEY, '');
22
+ const value = unwrap(result, '');
23
+
22
24
  if (!value) {
23
25
  return null;
24
26
  }
@@ -45,7 +47,7 @@ export class ThemeStorage {
45
47
  throw new Error(`Invalid theme mode: ${mode}`);
46
48
  }
47
49
 
48
- await AsyncStorage.setItem(STORAGE_KEY, mode);
50
+ await storageRepository.setString(STORAGE_KEY, mode);
49
51
  } catch (error) {
50
52
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
51
53
  // Re-throw validation errors but swallow storage errors to prevent app crashes
@@ -60,7 +62,7 @@ export class ThemeStorage {
60
62
  */
61
63
  static async clearThemeMode(): Promise<void> {
62
64
  try {
63
- await AsyncStorage.removeItem(STORAGE_KEY);
65
+ await storageRepository.removeItem(STORAGE_KEY);
64
66
  } catch {
65
67
  // Don't throw - clearing storage is not critical
66
68
  }