react-native-my-survey-sdk 2.0.6 → 2.0.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.
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ presets: ['module:metro-react-native-babel-preset'],
3
+ plugins: ['react-native-reanimated/plugin'],
4
+ };
package/package.json CHANGED
@@ -1,19 +1,27 @@
1
1
  {
2
2
  "name": "react-native-my-survey-sdk",
3
- "version": "2.0.6",
4
- "description": "Survey SDK for xebo.ai",
5
- "main": "index.js",
3
+ "version": "2.0.7",
4
+ "description": "Xebo survey collection SDK for React Native",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
6
7
  "scripts": {
7
8
  "test": "echo \"Error: no test specified\" && exit 1"
8
9
  },
9
- "author": "sheetal",
10
+ "author": "Xebo",
10
11
  "license": "ISC",
11
- "dependencies": {
12
- "react-native-webview": "^13.16.0"
13
- },
12
+ "dependencies": {},
14
13
  "peerDependencies": {
15
- "@react-navigation/native": ">=6.0.0",
16
14
  "react": ">=18.2.0",
17
- "react-native": ">=0.71.0"
15
+ "react-native": ">=0.71.0",
16
+ "@react-native-async-storage/async-storage": ">=1.0.0",
17
+ "@react-native-community/netinfo": ">=11.0.0",
18
+ "react-native-modal": ">=13.0.0",
19
+ "react-native-reanimated": ">=3.0.0"
20
+ },
21
+ "devDependencies": {
22
+ "@babel/runtime": "^7.29.2",
23
+ "@types/react": "^18.2.0",
24
+ "@types/react-native": "^0.73.0",
25
+ "typescript": "^5.3.0"
18
26
  }
19
27
  }
@@ -0,0 +1,197 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ StyleSheet,
7
+ ScrollView,
8
+ Animated,
9
+ } from 'react-native';
10
+ import { XeboQuestion, XeboAnswer, XeboChoice } from '../models/XeboModels';
11
+ import { getTheme } from '../theme/XeboTheme';
12
+
13
+ interface Props {
14
+ question: XeboQuestion;
15
+ isLastQuestion: boolean;
16
+ onAnswer: (answer: XeboAnswer) => void;
17
+ }
18
+
19
+ export const XeboDropdownView: React.FC<Props> = ({ question, isLastQuestion, onAnswer }) => {
20
+ const theme = getTheme();
21
+ const [selectedChoice, setSelectedChoice] = useState<XeboChoice | null>(null);
22
+ const [isOpen, setIsOpen] = useState(false);
23
+ const [error, setError] = useState('');
24
+ const shakeAnim = useRef(new Animated.Value(0)).current;
25
+
26
+ const shake = () => {
27
+ Animated.sequence([
28
+ Animated.timing(shakeAnim, { toValue: 8, duration: 60, useNativeDriver: true }),
29
+ Animated.timing(shakeAnim, { toValue: -8, duration: 60, useNativeDriver: true }),
30
+ Animated.timing(shakeAnim, { toValue: 6, duration: 60, useNativeDriver: true }),
31
+ Animated.timing(shakeAnim, { toValue: 0, duration: 60, useNativeDriver: true }),
32
+ ]).start();
33
+ };
34
+
35
+ const handleSelect = (choice: XeboChoice) => {
36
+ setSelectedChoice(choice);
37
+ setIsOpen(false);
38
+ setError('');
39
+ };
40
+
41
+ const handleNext = () => {
42
+ if (question.isRequired && !selectedChoice) {
43
+ setError('Please select an option.');
44
+ shake();
45
+ return;
46
+ }
47
+ onAnswer({
48
+ questionId: question.id,
49
+ value: selectedChoice ? [{ rowId: selectedChoice.id, text: selectedChoice.text }] : [],
50
+ });
51
+ };
52
+
53
+ return (
54
+ <View style={styles.container}>
55
+ <Text style={[styles.title, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
56
+ {question.title}
57
+ </Text>
58
+ {!!question.subtitle && (
59
+ <Text style={[styles.subtitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
60
+ {question.subtitle}
61
+ </Text>
62
+ )}
63
+
64
+ {/* Dropdown trigger */}
65
+ <TouchableOpacity
66
+ style={[styles.trigger, { borderColor: isOpen ? theme.primaryColor : '#E5E7EB', borderRadius: theme.cornerRadius }]}
67
+ onPress={() => setIsOpen(prev => !prev)}
68
+ activeOpacity={0.8}
69
+ >
70
+ <Text
71
+ style={[
72
+ styles.triggerText,
73
+ { color: selectedChoice ? theme.textColor : '#9CA3AF', fontFamily: theme.fontFamily },
74
+ ]}
75
+ >
76
+ {selectedChoice ? selectedChoice.text : (question.placeholder || 'Select an option')}
77
+ </Text>
78
+ <Text style={[styles.chevron, { color: theme.textColor }]}>{isOpen ? '▲' : '▼'}</Text>
79
+ </TouchableOpacity>
80
+
81
+ {/* Options list */}
82
+ {isOpen && (
83
+ <View style={[styles.optionsList, { borderColor: theme.primaryColor, borderRadius: theme.cornerRadius }]}>
84
+ <ScrollView nestedScrollEnabled style={styles.optionsScroll} showsVerticalScrollIndicator={false}>
85
+ {(question.options ?? []).map(choice => (
86
+ <TouchableOpacity
87
+ key={choice.id}
88
+ style={[
89
+ styles.optionRow,
90
+ selectedChoice?.id === choice.id && { backgroundColor: `${theme.primaryColor}15` },
91
+ ]}
92
+ onPress={() => handleSelect(choice)}
93
+ activeOpacity={0.7}
94
+ >
95
+ <Text
96
+ style={[
97
+ styles.optionText,
98
+ {
99
+ color: selectedChoice?.id === choice.id ? theme.primaryColor : theme.textColor,
100
+ fontFamily: theme.fontFamily,
101
+ fontWeight: selectedChoice?.id === choice.id ? '600' : '400',
102
+ },
103
+ ]}
104
+ >
105
+ {choice.text}
106
+ </Text>
107
+ </TouchableOpacity>
108
+ ))}
109
+ </ScrollView>
110
+ </View>
111
+ )}
112
+
113
+ {!!error && <Text style={styles.errorText}>{error}</Text>}
114
+
115
+ {selectedChoice && (
116
+ <Animated.View style={[styles.buttonWrap, { transform: [{ translateX: shakeAnim }] }]}>
117
+ <TouchableOpacity
118
+ style={[styles.button, { backgroundColor: theme.primaryColor, borderRadius: theme.cornerRadius }]}
119
+ onPress={handleNext}
120
+ activeOpacity={0.8}
121
+ >
122
+ <Text style={[styles.buttonText, { fontFamily: theme.fontFamily }]}>
123
+ {isLastQuestion ? 'Submit' : 'Next'}
124
+ </Text>
125
+ </TouchableOpacity>
126
+ </Animated.View>
127
+ )}
128
+ </View>
129
+ );
130
+ };
131
+
132
+ const styles = StyleSheet.create({
133
+ container: {
134
+ flex: 1,
135
+ paddingHorizontal: 20,
136
+ paddingTop: 16,
137
+ paddingBottom: 24,
138
+ },
139
+ title: {
140
+ fontSize: 18,
141
+ fontWeight: '700',
142
+ marginBottom: 6,
143
+ },
144
+ subtitle: {
145
+ fontSize: 14,
146
+ opacity: 0.6,
147
+ marginBottom: 16,
148
+ },
149
+ trigger: {
150
+ flexDirection: 'row',
151
+ alignItems: 'center',
152
+ borderWidth: 1.5,
153
+ padding: 14,
154
+ marginBottom: 4,
155
+ },
156
+ triggerText: {
157
+ fontSize: 15,
158
+ flex: 1,
159
+ },
160
+ chevron: {
161
+ fontSize: 12,
162
+ },
163
+ optionsList: {
164
+ borderWidth: 1.5,
165
+ maxHeight: 220,
166
+ marginBottom: 12,
167
+ },
168
+ optionsScroll: {
169
+ flexGrow: 0,
170
+ },
171
+ optionRow: {
172
+ padding: 14,
173
+ borderBottomWidth: 1,
174
+ borderBottomColor: '#F3F4F6',
175
+ },
176
+ optionText: {
177
+ fontSize: 15,
178
+ },
179
+ errorText: {
180
+ color: '#EF4444',
181
+ fontSize: 13,
182
+ marginBottom: 8,
183
+ },
184
+ buttonWrap: {
185
+ marginTop: 'auto',
186
+ },
187
+ button: {
188
+ paddingVertical: 14,
189
+ alignItems: 'center',
190
+ marginTop: 8,
191
+ },
192
+ buttonText: {
193
+ color: '#FFFFFF',
194
+ fontSize: 16,
195
+ fontWeight: '600',
196
+ },
197
+ });
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+ import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
3
+ import { XeboSurveyPage } from '../models/XeboModels';
4
+ import { getTheme } from '../theme/XeboTheme';
5
+
6
+ interface Props {
7
+ introPage: XeboSurveyPage;
8
+ onStart: () => void;
9
+ }
10
+
11
+ export const XeboIntroView: React.FC<Props> = ({ introPage, onStart }) => {
12
+ const theme = getTheme();
13
+
14
+ return (
15
+ <View style={styles.container}>
16
+ {!!introPage.content && (
17
+ <Text style={[styles.content, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
18
+ {introPage.content}
19
+ </Text>
20
+ )}
21
+
22
+ <TouchableOpacity
23
+ style={[styles.button, { backgroundColor: theme.primaryColor, borderRadius: theme.cornerRadius }]}
24
+ onPress={onStart}
25
+ activeOpacity={0.8}
26
+ >
27
+ <Text style={[styles.buttonText, { fontFamily: theme.fontFamily }]}>
28
+ {introPage.buttonText || "Let's get started"}
29
+ </Text>
30
+ </TouchableOpacity>
31
+ </View>
32
+ );
33
+ };
34
+
35
+ const styles = StyleSheet.create({
36
+ container: {
37
+ flex: 1,
38
+ paddingHorizontal: 20,
39
+ paddingTop: 16,
40
+ paddingBottom: 24,
41
+ alignItems: 'stretch',
42
+ },
43
+ content: {
44
+ fontSize: 16,
45
+ lineHeight: 24,
46
+ marginBottom: 32,
47
+ },
48
+ button: {
49
+ paddingVertical: 14,
50
+ alignItems: 'center',
51
+ marginTop: 'auto',
52
+ },
53
+ buttonText: {
54
+ color: '#FFFFFF',
55
+ fontSize: 16,
56
+ fontWeight: '600',
57
+ },
58
+ });
@@ -0,0 +1,166 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ StyleSheet,
7
+ ScrollView,
8
+ Animated,
9
+ } from 'react-native';
10
+ import { XeboQuestion, XeboAnswer } from '../models/XeboModels';
11
+ import { getTheme } from '../theme/XeboTheme';
12
+
13
+ interface Props {
14
+ question: XeboQuestion;
15
+ isLastQuestion: boolean;
16
+ onAnswer: (answer: XeboAnswer) => void;
17
+ }
18
+
19
+ const DETRACTOR_COLOR = '#F24236';
20
+ const PASSIVE_COLOR = '#FFC208';
21
+ const PROMOTER_COLOR = '#2ECC70';
22
+
23
+ function npsColor(score: number, npsLabels?: XeboQuestion['npsLabels']): string {
24
+ if (score <= 6) return npsLabels?.detractorColor ?? DETRACTOR_COLOR;
25
+ if (score <= 8) return npsLabels?.passiveColor ?? PASSIVE_COLOR;
26
+ return npsLabels?.promoterColor ?? PROMOTER_COLOR;
27
+ }
28
+
29
+ function isColorDark(hex: string): boolean {
30
+ const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
31
+ if (!m) return false;
32
+ const r = parseInt(m[1], 16) / 255;
33
+ const g = parseInt(m[2], 16) / 255;
34
+ const b = parseInt(m[3], 16) / 255;
35
+ return 0.299 * r + 0.587 * g + 0.114 * b < 0.5;
36
+ }
37
+
38
+ export const XeboMultiNPSView: React.FC<Props> = ({ question, isLastQuestion, onAnswer }) => {
39
+ const theme = getTheme();
40
+ const rows = question.options ?? [];
41
+ const [scores, setScores] = useState<Record<string, number>>({});
42
+ const [error, setError] = useState('');
43
+ const shakeAnim = useRef(new Animated.Value(0)).current;
44
+
45
+ const shake = () => {
46
+ Animated.sequence([
47
+ Animated.timing(shakeAnim, { toValue: 8, duration: 60, useNativeDriver: true }),
48
+ Animated.timing(shakeAnim, { toValue: -8, duration: 60, useNativeDriver: true }),
49
+ Animated.timing(shakeAnim, { toValue: 6, duration: 60, useNativeDriver: true }),
50
+ Animated.timing(shakeAnim, { toValue: 0, duration: 60, useNativeDriver: true }),
51
+ ]).start();
52
+ };
53
+
54
+ const setScore = (rowId: string, score: number) => {
55
+ setScores(prev => ({ ...prev, [rowId]: score }));
56
+ setError('');
57
+ };
58
+
59
+ const handleNext = () => {
60
+ if (question.isRequired && rows.some(r => scores[r.id] === undefined)) {
61
+ setError('Please rate all items.');
62
+ shake();
63
+ return;
64
+ }
65
+
66
+ onAnswer({
67
+ questionId: question.id,
68
+ value: rows
69
+ .filter(r => scores[r.id] !== undefined)
70
+ .map(r => ({
71
+ rowId: r.id,
72
+ colId: (question.columns?.[scores[r.id]]?.id ?? scores[r.id].toString()),
73
+ })),
74
+ });
75
+ };
76
+
77
+ return (
78
+ <View style={styles.container}>
79
+ <Text style={[styles.title, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
80
+ {question.title}
81
+ </Text>
82
+ {!!question.subtitle && (
83
+ <Text style={[styles.subtitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
84
+ {question.subtitle}
85
+ </Text>
86
+ )}
87
+
88
+ <ScrollView style={styles.scroll} showsVerticalScrollIndicator={false}>
89
+ {rows.map(row => (
90
+ <View key={row.id} style={styles.rowWrap}>
91
+ <Text style={[styles.rowLabel, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
92
+ {row.text}
93
+ </Text>
94
+ {/* Single-row NPS scale with always-visible category colors */}
95
+ <View style={styles.scaleRow}>
96
+ {Array.from({ length: 11 }, (_, i) => {
97
+ const selected = scores[row.id] === i;
98
+ const color = npsColor(i, question.npsLabels);
99
+ return (
100
+ <TouchableOpacity
101
+ key={i}
102
+ style={[
103
+ styles.scoreButton,
104
+ {
105
+ backgroundColor: color,
106
+ borderColor: selected ? '#000000' : color,
107
+ borderWidth: selected ? 2.5 : 1.5,
108
+ },
109
+ ]}
110
+ onPress={() => setScore(row.id, i)}
111
+ activeOpacity={0.8}
112
+ >
113
+ <Text style={[
114
+ styles.scoreText,
115
+ { color: isColorDark(color) ? '#FFFFFF' : selected ? '#111111' : '#444444' },
116
+ ]}>
117
+ {i}
118
+ </Text>
119
+ </TouchableOpacity>
120
+ );
121
+ })}
122
+ </View>
123
+ </View>
124
+ ))}
125
+ </ScrollView>
126
+
127
+ {!!error && <Text style={styles.errorText}>{error}</Text>}
128
+
129
+ <Animated.View style={{ transform: [{ translateX: shakeAnim }] }}>
130
+ <TouchableOpacity
131
+ style={[styles.button, { backgroundColor: theme.primaryColor, borderRadius: theme.cornerRadius }]}
132
+ onPress={handleNext}
133
+ activeOpacity={0.8}
134
+ >
135
+ <Text style={[styles.buttonText, { fontFamily: theme.fontFamily }]}>
136
+ {isLastQuestion ? 'Submit' : 'Next'}
137
+ </Text>
138
+ </TouchableOpacity>
139
+ </Animated.View>
140
+ </View>
141
+ );
142
+ };
143
+
144
+ const styles = StyleSheet.create({
145
+ container: { flex: 1, paddingHorizontal: 20, paddingTop: 16, paddingBottom: 24 },
146
+ title: { fontSize: 18, fontWeight: '700', marginBottom: 6 },
147
+ subtitle: { fontSize: 14, opacity: 0.6, marginBottom: 16 },
148
+ scroll: { flex: 1, marginBottom: 12 },
149
+ rowWrap: { marginBottom: 20 },
150
+ rowLabel: { fontSize: 15, fontWeight: '500', marginBottom: 8 },
151
+ scaleRow: {
152
+ flexDirection: 'row',
153
+ gap: 3,
154
+ },
155
+ scoreButton: {
156
+ flex: 1,
157
+ height: 30,
158
+ borderRadius: 5,
159
+ alignItems: 'center',
160
+ justifyContent: 'center',
161
+ },
162
+ scoreText: { fontSize: 11, fontWeight: '700' },
163
+ errorText: { color: '#EF4444', fontSize: 13, marginBottom: 8 },
164
+ button: { paddingVertical: 14, alignItems: 'center', marginTop: 4 },
165
+ buttonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600' },
166
+ });
@@ -0,0 +1,165 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ StyleSheet,
7
+ ScrollView,
8
+ Animated,
9
+ } from 'react-native';
10
+ import { XeboQuestion, XeboAnswer } from '../models/XeboModels';
11
+ import { getTheme } from '../theme/XeboTheme';
12
+
13
+ interface Props {
14
+ question: XeboQuestion;
15
+ isLastQuestion: boolean;
16
+ onAnswer: (answer: XeboAnswer) => void;
17
+ }
18
+
19
+ export const XeboMultiRatingView: React.FC<Props> = ({ question, isLastQuestion, onAnswer }) => {
20
+ const theme = getTheme();
21
+ const rows = question.options ?? [];
22
+ const totalSteps = question.columns?.length ?? 5;
23
+ const ratingColor = question.ratingColor ?? theme.primaryColor;
24
+ const ratingStyle = question.ratingStyle ?? 'star';
25
+
26
+ const [ratings, setRatings] = useState<Record<string, number>>({});
27
+ const [error, setError] = useState('');
28
+ const shakeAnim = useRef(new Animated.Value(0)).current;
29
+
30
+ const shake = () => {
31
+ Animated.sequence([
32
+ Animated.timing(shakeAnim, { toValue: 8, duration: 60, useNativeDriver: true }),
33
+ Animated.timing(shakeAnim, { toValue: -8, duration: 60, useNativeDriver: true }),
34
+ Animated.timing(shakeAnim, { toValue: 6, duration: 60, useNativeDriver: true }),
35
+ Animated.timing(shakeAnim, { toValue: 0, duration: 60, useNativeDriver: true }),
36
+ ]).start();
37
+ };
38
+
39
+ const setRating = (rowId: string, index: number) => {
40
+ setRatings(prev => ({ ...prev, [rowId]: index }));
41
+ setError('');
42
+ };
43
+
44
+ const isCumulative = ratingStyle === 'star' || ratingStyle === 'heart';
45
+
46
+ const handleNext = () => {
47
+ if (question.isRequired && rows.some(r => ratings[r.id] === undefined)) {
48
+ setError('Please rate all items.');
49
+ shake();
50
+ return;
51
+ }
52
+
53
+ onAnswer({
54
+ questionId: question.id,
55
+ value: rows
56
+ .filter(r => ratings[r.id] !== undefined)
57
+ .map(r => ({
58
+ rowId: r.id,
59
+ colId: question.columns?.[ratings[r.id]]?.id ?? ratings[r.id].toString(),
60
+ })),
61
+ });
62
+ };
63
+
64
+ const starIcon = (filled: boolean) =>
65
+ ratingStyle === 'heart' ? (filled ? '♥' : '♡') : filled ? '★' : '☆';
66
+
67
+ return (
68
+ <View style={styles.container}>
69
+ <Text style={[styles.title, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
70
+ {question.title}
71
+ </Text>
72
+ {!!question.subtitle && (
73
+ <Text style={[styles.subtitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
74
+ {question.subtitle}
75
+ </Text>
76
+ )}
77
+
78
+ <ScrollView style={styles.scroll} showsVerticalScrollIndicator={false}>
79
+ {rows.map(row => (
80
+ <View key={row.id} style={styles.rowWrap}>
81
+ <Text style={[styles.rowLabel, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
82
+ {row.text}
83
+ </Text>
84
+ <View style={styles.iconsRow}>
85
+ {Array.from({ length: totalSteps }, (_, i) => {
86
+ const isFilled = isCumulative
87
+ ? ratings[row.id] !== undefined && i <= ratings[row.id]
88
+ : i === ratings[row.id];
89
+ return (
90
+ <TouchableOpacity
91
+ key={i}
92
+ onPress={() => setRating(row.id, i)}
93
+ activeOpacity={0.7}
94
+ style={styles.iconButton}
95
+ >
96
+ {ratingStyle === 'circle' ? (
97
+ <View
98
+ style={[
99
+ styles.circle,
100
+ { backgroundColor: isFilled ? ratingColor : '#E5E7EB' },
101
+ ]}
102
+ />
103
+ ) : ratingStyle === 'tile' ? (
104
+ <View
105
+ style={[
106
+ styles.tile,
107
+ { backgroundColor: isFilled ? ratingColor : '#E5E7EB' },
108
+ ]}
109
+ >
110
+ <Text style={[styles.tileText, { color: isFilled ? '#FFF' : '#6B7280' }]}>
111
+ {i + 1}
112
+ </Text>
113
+ </View>
114
+ ) : (
115
+ <Text style={{ fontSize: 24, color: isFilled ? ratingColor : '#E5E7EB' }}>
116
+ {starIcon(isFilled)}
117
+ </Text>
118
+ )}
119
+ </TouchableOpacity>
120
+ );
121
+ })}
122
+ </View>
123
+ </View>
124
+ ))}
125
+ </ScrollView>
126
+
127
+ {!!error && <Text style={styles.errorText}>{error}</Text>}
128
+
129
+ <Animated.View style={{ transform: [{ translateX: shakeAnim }] }}>
130
+ <TouchableOpacity
131
+ style={[styles.button, { backgroundColor: theme.primaryColor, borderRadius: theme.cornerRadius }]}
132
+ onPress={handleNext}
133
+ activeOpacity={0.8}
134
+ >
135
+ <Text style={[styles.buttonText, { fontFamily: theme.fontFamily }]}>
136
+ {isLastQuestion ? 'Submit' : 'Next'}
137
+ </Text>
138
+ </TouchableOpacity>
139
+ </Animated.View>
140
+ </View>
141
+ );
142
+ };
143
+
144
+ const styles = StyleSheet.create({
145
+ container: { flex: 1, paddingHorizontal: 20, paddingTop: 16, paddingBottom: 24 },
146
+ title: { fontSize: 18, fontWeight: '700', marginBottom: 6 },
147
+ subtitle: { fontSize: 14, opacity: 0.6, marginBottom: 16 },
148
+ scroll: { flex: 1, marginBottom: 12 },
149
+ rowWrap: { marginBottom: 20 },
150
+ rowLabel: { fontSize: 15, fontWeight: '500', marginBottom: 8 },
151
+ iconsRow: { flexDirection: 'row', flexWrap: 'wrap' },
152
+ iconButton: { marginRight: 6, marginBottom: 6 },
153
+ circle: { width: 28, height: 28, borderRadius: 14 },
154
+ tile: {
155
+ width: 32,
156
+ height: 32,
157
+ borderRadius: 6,
158
+ alignItems: 'center',
159
+ justifyContent: 'center',
160
+ },
161
+ tileText: { fontSize: 12, fontWeight: '700' },
162
+ errorText: { color: '#EF4444', fontSize: 13, marginBottom: 8 },
163
+ button: { paddingVertical: 14, alignItems: 'center', marginTop: 4 },
164
+ buttonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600' },
165
+ });