@umituz/react-native-design-system 4.28.5 → 4.28.6
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 +1 -1
- package/src/atoms/AtomicInput.tsx +2 -2
- package/src/index.ts +1 -1
- package/src/molecules/Divider/Divider.tsx +2 -3
- package/src/molecules/Divider/types.ts +22 -5
- package/src/molecules/StepHeader/StepHeader.constants.ts +48 -0
- package/src/molecules/StepHeader/StepHeader.tsx +29 -23
- package/src/molecules/StepProgress/StepProgress.constants.ts +23 -0
- package/src/molecules/StepProgress/StepProgress.tsx +9 -6
- package/src/molecules/avatar/Avatar.constants.ts +20 -2
- package/src/molecules/avatar/Avatar.tsx +5 -3
- package/src/molecules/avatar/Avatar.utils.ts +4 -4
- package/src/molecules/avatar/AvatarGroup.tsx +2 -2
- package/src/molecules/listitem/styles/listItemStyles.ts +2 -3
- package/src/molecules/navigation/hooks/useAppFocusEffect.ts +14 -11
- package/src/molecules/navigation/hooks/useAppIsFocused.ts +1 -2
- package/src/molecules/navigation/hooks/useAppNavigation.ts +88 -118
- package/src/molecules/navigation/hooks/useAppRoute.ts +26 -27
- package/src/onboarding/domain/entities/ChatMessage.ts +19 -0
- package/src/onboarding/domain/entities/ChatStep.ts +72 -0
- package/src/onboarding/index.ts +29 -0
- package/src/onboarding/infrastructure/hooks/useChatAnimations.ts +145 -0
- package/src/onboarding/presentation/components/chat/ChatMessage.tsx +166 -0
- package/src/onboarding/presentation/components/chat/ChatOptionButton.tsx +145 -0
- package/src/onboarding/presentation/components/chat/TypingIndicator.tsx +99 -0
- package/src/onboarding/presentation/components/chat/index.ts +12 -0
- package/src/onboarding/presentation/hooks/useChatOnboarding.ts +278 -0
- package/src/onboarding/presentation/screens/ChatOnboardingScreen.tsx +276 -0
- package/src/utils/index.ts +13 -0
- package/src/utils/responsiveUtils.ts +110 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Onboarding Screen
|
|
3
|
+
*
|
|
4
|
+
* Chat-based onboarding experience for user engagement
|
|
5
|
+
* Fully configurable flow, optional mascot, lazy-loaded animations
|
|
6
|
+
*
|
|
7
|
+
* Responsive Design:
|
|
8
|
+
* - Uses useSafeAreaInsets() from react-native-safe-area-context for proper device compatibility
|
|
9
|
+
* - Flexible flex-based layouts that adapt to all screen sizes
|
|
10
|
+
* - KeyboardAvoidingView for proper keyboard handling on iOS/Android
|
|
11
|
+
* - Percentage-based maxWidth in chat messages for responsive text layout
|
|
12
|
+
* - Proper insets integration with device and safe-area modules
|
|
13
|
+
*
|
|
14
|
+
* Compatible with:
|
|
15
|
+
* - @umituz/react-native-design-system/device (useDeviceInfo)
|
|
16
|
+
* - @umituz/react-native-design-system/safe-area (useSafeAreaInsets)
|
|
17
|
+
* - All screen sizes (iPhone, iPad, Android phones/tablets)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import React, { memo } from "react";
|
|
21
|
+
import {
|
|
22
|
+
View,
|
|
23
|
+
StyleSheet,
|
|
24
|
+
ScrollView,
|
|
25
|
+
TextInput,
|
|
26
|
+
TouchableOpacity,
|
|
27
|
+
KeyboardAvoidingView,
|
|
28
|
+
Platform,
|
|
29
|
+
ViewStyle,
|
|
30
|
+
Text,
|
|
31
|
+
} from "react-native";
|
|
32
|
+
import { useResponsive } from "../../../responsive/useResponsive";
|
|
33
|
+
|
|
34
|
+
import type { ChatStep } from "../../domain/entities/ChatStep";
|
|
35
|
+
import { useChatOnboarding } from "../hooks/useChatOnboarding";
|
|
36
|
+
import { ChatMessageComponent, ChatOptionButton, TypingIndicator } from "../components/chat";
|
|
37
|
+
|
|
38
|
+
export interface ChatOnboardingScreenProps {
|
|
39
|
+
/** Chat onboarding flow configuration */
|
|
40
|
+
flow: Record<string, ChatStep>;
|
|
41
|
+
|
|
42
|
+
/** Initial step ID */
|
|
43
|
+
initialStepId?: string;
|
|
44
|
+
|
|
45
|
+
/** Completion callback */
|
|
46
|
+
onComplete?: () => void;
|
|
47
|
+
|
|
48
|
+
/** Skip callback */
|
|
49
|
+
onSkip?: () => void;
|
|
50
|
+
|
|
51
|
+
/** Show mascot (default: false) */
|
|
52
|
+
showMascot?: boolean;
|
|
53
|
+
|
|
54
|
+
/** Mascot component to render */
|
|
55
|
+
renderMascot?: (state: string) => React.ReactNode;
|
|
56
|
+
|
|
57
|
+
/** Theme colors */
|
|
58
|
+
theme?: {
|
|
59
|
+
/** Background color */
|
|
60
|
+
background?: string;
|
|
61
|
+
|
|
62
|
+
/** Message background color */
|
|
63
|
+
messageBackground?: string;
|
|
64
|
+
|
|
65
|
+
/** Message text color */
|
|
66
|
+
messageText?: string;
|
|
67
|
+
|
|
68
|
+
/** User message background color */
|
|
69
|
+
userMessageBackground?: string;
|
|
70
|
+
|
|
71
|
+
/** User message text color */
|
|
72
|
+
userMessageText?: string;
|
|
73
|
+
|
|
74
|
+
/** Button background color */
|
|
75
|
+
buttonBackground?: string;
|
|
76
|
+
|
|
77
|
+
/** Button text color */
|
|
78
|
+
buttonText?: string;
|
|
79
|
+
|
|
80
|
+
/** Button border color */
|
|
81
|
+
buttonBorder?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Custom styles */
|
|
85
|
+
style?: ViewStyle;
|
|
86
|
+
|
|
87
|
+
/** Delay between messages */
|
|
88
|
+
messageDelay?: number;
|
|
89
|
+
|
|
90
|
+
/** Render custom icon */
|
|
91
|
+
renderIcon?: (iconName: string) => React.ReactNode;
|
|
92
|
+
|
|
93
|
+
/** Storage key for progress persistence */
|
|
94
|
+
storageKey?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Chat onboarding screen component
|
|
99
|
+
*/
|
|
100
|
+
export const ChatOnboardingScreen = memo(
|
|
101
|
+
({
|
|
102
|
+
flow,
|
|
103
|
+
initialStepId,
|
|
104
|
+
onComplete,
|
|
105
|
+
onSkip,
|
|
106
|
+
showMascot = false,
|
|
107
|
+
renderMascot,
|
|
108
|
+
theme = {},
|
|
109
|
+
style,
|
|
110
|
+
messageDelay = 500,
|
|
111
|
+
renderIcon,
|
|
112
|
+
}: ChatOnboardingScreenProps) => {
|
|
113
|
+
const responsive = useResponsive();
|
|
114
|
+
|
|
115
|
+
const {
|
|
116
|
+
currentStep,
|
|
117
|
+
messages,
|
|
118
|
+
showOptions,
|
|
119
|
+
isProcessing,
|
|
120
|
+
showTypingIndicator,
|
|
121
|
+
handleOptionSelect,
|
|
122
|
+
handleSubmitName,
|
|
123
|
+
} = useChatOnboarding({
|
|
124
|
+
flow,
|
|
125
|
+
initialStepId,
|
|
126
|
+
onComplete,
|
|
127
|
+
onSkip,
|
|
128
|
+
messageDelay,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const [nameInput, setNameInput] = React.useState("");
|
|
132
|
+
|
|
133
|
+
const handleNameSubmit = () => {
|
|
134
|
+
handleSubmitName(nameInput);
|
|
135
|
+
setNameInput("");
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const containerStyle = [
|
|
139
|
+
styles.container,
|
|
140
|
+
{
|
|
141
|
+
backgroundColor: theme.background || "#FFFFFF",
|
|
142
|
+
paddingBottom: responsive.insets.bottom + 20,
|
|
143
|
+
paddingTop: responsive.insets.top + 20,
|
|
144
|
+
},
|
|
145
|
+
style,
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<KeyboardAvoidingView
|
|
150
|
+
style={containerStyle}
|
|
151
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
152
|
+
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 20}
|
|
153
|
+
>
|
|
154
|
+
{/* Mascot Section */}
|
|
155
|
+
{showMascot && renderMascot && currentStep?.mascotState && (
|
|
156
|
+
<View style={styles.mascotContainer}>{renderMascot(currentStep.mascotState)}</View>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{/* Messages Section */}
|
|
160
|
+
<ScrollView
|
|
161
|
+
style={styles.messagesContainer}
|
|
162
|
+
contentContainerStyle={styles.messagesContent}
|
|
163
|
+
showsVerticalScrollIndicator={false}
|
|
164
|
+
>
|
|
165
|
+
{messages.map((msg, index) => (
|
|
166
|
+
<ChatMessageComponent
|
|
167
|
+
key={`${currentStep?.id}-${index}`}
|
|
168
|
+
message={msg}
|
|
169
|
+
backgroundColor={theme.messageBackground}
|
|
170
|
+
textColor={theme.messageText}
|
|
171
|
+
userBackgroundColor={theme.userMessageBackground}
|
|
172
|
+
userTextColor={theme.userMessageText}
|
|
173
|
+
/>
|
|
174
|
+
))}
|
|
175
|
+
|
|
176
|
+
{showTypingIndicator && (
|
|
177
|
+
<TypingIndicator
|
|
178
|
+
backgroundColor={theme.messageBackground}
|
|
179
|
+
dotColor={theme.messageText}
|
|
180
|
+
/>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{/* Options Section */}
|
|
184
|
+
{showOptions && currentStep?.options && (
|
|
185
|
+
<View style={styles.optionsContainer}>
|
|
186
|
+
{currentStep.options.map((option, index) => (
|
|
187
|
+
<ChatOptionButton
|
|
188
|
+
key={`${option.value}-${index}`}
|
|
189
|
+
option={option}
|
|
190
|
+
onPress={handleOptionSelect}
|
|
191
|
+
backgroundColor={theme.buttonBackground}
|
|
192
|
+
textColor={theme.buttonText}
|
|
193
|
+
borderColor={theme.buttonBorder}
|
|
194
|
+
disabled={isProcessing}
|
|
195
|
+
renderIcon={renderIcon}
|
|
196
|
+
/>
|
|
197
|
+
))}
|
|
198
|
+
</View>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{/* Name Input Section */}
|
|
202
|
+
{currentStep?.isNameInput && (
|
|
203
|
+
<View style={styles.inputContainer}>
|
|
204
|
+
<TextInput
|
|
205
|
+
style={[styles.nameInput, { borderColor: theme.buttonBorder }]}
|
|
206
|
+
placeholder="Enter your name..."
|
|
207
|
+
placeholderTextColor={theme.messageText}
|
|
208
|
+
value={nameInput}
|
|
209
|
+
onChangeText={setNameInput}
|
|
210
|
+
onSubmitEditing={handleNameSubmit}
|
|
211
|
+
returnKeyType="done"
|
|
212
|
+
autoCapitalize="words"
|
|
213
|
+
/>
|
|
214
|
+
<TouchableOpacity
|
|
215
|
+
style={[styles.submitButton, { backgroundColor: theme.buttonBackground }]}
|
|
216
|
+
onPress={handleNameSubmit}
|
|
217
|
+
disabled={isProcessing || !nameInput.trim()}
|
|
218
|
+
>
|
|
219
|
+
<Text style={[styles.submitButtonText, { color: theme.buttonText }]}>Send</Text>
|
|
220
|
+
</TouchableOpacity>
|
|
221
|
+
</View>
|
|
222
|
+
)}
|
|
223
|
+
</ScrollView>
|
|
224
|
+
</KeyboardAvoidingView>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
ChatOnboardingScreen.displayName = "ChatOnboardingScreen";
|
|
230
|
+
|
|
231
|
+
const styles = StyleSheet.create({
|
|
232
|
+
container: {
|
|
233
|
+
flex: 1,
|
|
234
|
+
},
|
|
235
|
+
mascotContainer: {
|
|
236
|
+
alignItems: "center",
|
|
237
|
+
paddingVertical: 20,
|
|
238
|
+
},
|
|
239
|
+
messagesContainer: {
|
|
240
|
+
flex: 1,
|
|
241
|
+
paddingHorizontal: 16,
|
|
242
|
+
},
|
|
243
|
+
messagesContent: {
|
|
244
|
+
paddingTop: 10,
|
|
245
|
+
},
|
|
246
|
+
optionsContainer: {
|
|
247
|
+
marginTop: 16,
|
|
248
|
+
marginBottom: 16,
|
|
249
|
+
},
|
|
250
|
+
inputContainer: {
|
|
251
|
+
flexDirection: "row",
|
|
252
|
+
alignItems: "center",
|
|
253
|
+
marginTop: 12,
|
|
254
|
+
marginBottom: 16,
|
|
255
|
+
},
|
|
256
|
+
nameInput: {
|
|
257
|
+
flex: 1,
|
|
258
|
+
height: 48,
|
|
259
|
+
borderWidth: 1,
|
|
260
|
+
borderRadius: 12,
|
|
261
|
+
paddingHorizontal: 16,
|
|
262
|
+
fontSize: 16,
|
|
263
|
+
marginRight: 8,
|
|
264
|
+
},
|
|
265
|
+
submitButton: {
|
|
266
|
+
height: 48,
|
|
267
|
+
paddingHorizontal: 20,
|
|
268
|
+
borderRadius: 12,
|
|
269
|
+
justifyContent: "center",
|
|
270
|
+
alignItems: "center",
|
|
271
|
+
},
|
|
272
|
+
submitButtonText: {
|
|
273
|
+
fontSize: 16,
|
|
274
|
+
fontWeight: "600",
|
|
275
|
+
},
|
|
276
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utils Module
|
|
3
|
+
*
|
|
4
|
+
* Centralized utility functions for the design system.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
calculateResponsiveSize,
|
|
9
|
+
calculateResponsiveSizes,
|
|
10
|
+
calculateResponsiveSizeSubtle,
|
|
11
|
+
calculateLineHeight,
|
|
12
|
+
createResponsiveSizes,
|
|
13
|
+
} from './responsiveUtils';
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive Utility Functions
|
|
3
|
+
*
|
|
4
|
+
* Centralized responsive calculations to avoid code duplication.
|
|
5
|
+
* All components should use these utilities instead of manual calculations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Calculate responsive size from base value
|
|
10
|
+
* Multiplies base size by spacingMultiplier and floors the result
|
|
11
|
+
*
|
|
12
|
+
* @param baseSize - Base size in pixels (e.g., 16, 24, 32)
|
|
13
|
+
* @param spacingMultiplier - Multiplier from design tokens (0.9, 1.0, 1.2)
|
|
14
|
+
* @returns Responsive size in pixels (floored for integer values)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const responsive = useResponsive();
|
|
18
|
+
* const padding = calculateResponsiveSize(16, responsive.spacingMultiplier); // 14.4 → 14
|
|
19
|
+
*/
|
|
20
|
+
export const calculateResponsiveSize = (
|
|
21
|
+
baseSize: number,
|
|
22
|
+
spacingMultiplier: number
|
|
23
|
+
): number => {
|
|
24
|
+
return Math.floor(baseSize * spacingMultiplier);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Calculate multiple responsive sizes at once
|
|
29
|
+
* Useful for component configs with multiple size properties
|
|
30
|
+
*
|
|
31
|
+
* @param sizes - Object with base sizes
|
|
32
|
+
* @param spacingMultiplier - Multiplier from design tokens
|
|
33
|
+
* @returns Object with responsive sizes
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const baseConfig = { padding: 16, margin: 24, fontSize: 14 };
|
|
37
|
+
* const responsiveConfig = calculateResponsiveSizes(baseConfig, spacingMultiplier);
|
|
38
|
+
* // { padding: 14, margin: 21, fontSize: 12 }
|
|
39
|
+
*/
|
|
40
|
+
export const calculateResponsiveSizes = <T extends Record<string, number>>(
|
|
41
|
+
sizes: T,
|
|
42
|
+
spacingMultiplier: number
|
|
43
|
+
): { [K in keyof T]: number } => {
|
|
44
|
+
const result = {} as { [K in keyof T]: number };
|
|
45
|
+
|
|
46
|
+
for (const key in sizes) {
|
|
47
|
+
if (Object.prototype.hasOwnProperty.call(sizes, key)) {
|
|
48
|
+
result[key] = calculateResponsiveSize(sizes[key], spacingMultiplier);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Calculate responsive size with subtle scaling
|
|
57
|
+
* For values that shouldn't scale as much (e.g., borderWidth, borderRadius)
|
|
58
|
+
* Uses 0.8x of the spacing multiplier
|
|
59
|
+
*
|
|
60
|
+
* @param baseSize - Base size in pixels
|
|
61
|
+
* @param spacingMultiplier - Multiplier from design tokens
|
|
62
|
+
* @returns Responsive size with subtle scaling
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const borderWidth = calculateResponsiveSizeSubtle(2, spacingMultiplier); // 2 → 2 (small tablets)
|
|
66
|
+
*/
|
|
67
|
+
export const calculateResponsiveSizeSubtle = (
|
|
68
|
+
baseSize: number,
|
|
69
|
+
spacingMultiplier: number
|
|
70
|
+
): number => {
|
|
71
|
+
const subtleMultiplier = Math.max(1, spacingMultiplier * 0.8);
|
|
72
|
+
return Math.floor(baseSize * subtleMultiplier);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Calculate responsive line height from font size
|
|
77
|
+
* Standard 1.5x ratio for readability
|
|
78
|
+
*
|
|
79
|
+
* @param fontSize - Font size in pixels
|
|
80
|
+
* @param spacingMultiplier - Multiplier from design tokens
|
|
81
|
+
* @returns Line height in pixels
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* const lineHeight = calculateLineHeight(16, spacingMultiplier); // 24 → 20
|
|
85
|
+
*/
|
|
86
|
+
export const calculateLineHeight = (
|
|
87
|
+
fontSize: number,
|
|
88
|
+
spacingMultiplier: number
|
|
89
|
+
): number => {
|
|
90
|
+
return Math.floor(fontSize * spacingMultiplier * 1.5);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create responsive StyleSheet values
|
|
95
|
+
* Converts an object with base sizes to responsive sizes
|
|
96
|
+
*
|
|
97
|
+
* @param baseStyles - Object with base style values
|
|
98
|
+
* @param spacingMultiplier - Multiplier from design tokens
|
|
99
|
+
* @returns Responsive style values
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* const baseSizes = { width: 100, height: 50, padding: 16 };
|
|
103
|
+
* const responsiveSizes = createResponsiveSizes(baseSizes, spacingMultiplier);
|
|
104
|
+
*/
|
|
105
|
+
export const createResponsiveSizes = <T extends Record<string, number>>(
|
|
106
|
+
baseSizes: T,
|
|
107
|
+
spacingMultiplier: number
|
|
108
|
+
): T => {
|
|
109
|
+
return calculateResponsiveSizes(baseSizes, spacingMultiplier) as T;
|
|
110
|
+
};
|