@xetwa/design-system 1.0.23 → 1.0.25

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": "@xetwa/design-system",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -15,7 +15,7 @@ const meta = {
15
15
  layout: 'padded',
16
16
  docs: {
17
17
  description: {
18
- component: 'O **AnswerOptionCard** é utilizado nos ecrãs de Lição/Quiz para exibir as múltiplas escolhas de um exercício.\n\n### Exemplo de Uso\n```tsx\n<AnswerOptionCard \n status="default"\n label="Boa noite"\n prefixNode={<CircleLetter letter="A" />}\n onPress={() => selecionar("A")}\n/>\n```',
18
+ component: 'O **AnswerOptionCard** é utilizado nos ecrãs de Lição/Quiz para exibir as múltiplas escolhas de um exercício.\n\n### Exemplo de Uso\n```tsx\n<AnswerOptionCard \n status="default"\n label="Boa noite"\n prefix="A"\n onPress={() => selecionar("A")}\n/>\n```',
19
19
  }
20
20
  }
21
21
  },
@@ -36,43 +36,13 @@ const meta = {
36
36
  export default meta;
37
37
  type Story = StoryObj<any>;
38
38
 
39
- const CircleLetter = ({ letter, selected = false, correct = false }: { letter: string; selected?: boolean; correct?: boolean }) => {
40
- let bgColor = theme.colors.brown[100];
41
- let textColor = theme.colors.brown[500];
42
39
 
43
- if (selected) {
44
- bgColor = theme.colors.primary;
45
- textColor = theme.colors.white;
46
- } else if (correct) {
47
- bgColor = theme.colors.success;
48
- textColor = theme.colors.white;
49
- }
50
-
51
- return (
52
- <View style={{
53
- width: 28,
54
- height: 28,
55
- borderRadius: 14,
56
- backgroundColor: bgColor,
57
- alignItems: 'center',
58
- justifyContent: 'center',
59
- }}>
60
- <Typography style={{
61
-
62
- fontSize: 14,
63
- color: textColor,
64
- }}>
65
- {letter}
66
- </Typography>
67
- </View>
68
- );
69
- };
70
40
 
71
41
  export const Default: Story = {
72
42
  args: {
73
43
  status: 'default',
74
44
  label: 'Boa noite',
75
- prefixNode: <CircleLetter letter="A" />,
45
+ prefix: 'A',
76
46
  },
77
47
  };
78
48
 
@@ -118,19 +88,19 @@ export const InteractiveDemo: Story = {
118
88
  <AnswerOptionCard
119
89
  status={getStatus('A')}
120
90
  label="Boa noite"
121
- prefixNode={<CircleLetter letter="A" selected={selected === 'A'} correct={statusMap.A === 'correct'} />}
91
+ prefix="A"
122
92
  onPress={() => handlePress('A')}
123
93
  />
124
94
  <AnswerOptionCard
125
95
  status={getStatus('B')}
126
96
  label="Bom dia / Olá"
127
- prefixNode={<CircleLetter letter="B" selected={selected === 'B'} correct={statusMap.B === 'correct'} />}
97
+ prefix="B"
128
98
  onPress={() => handlePress('B')}
129
99
  />
130
100
  <AnswerOptionCard
131
101
  status={getStatus('C')}
132
102
  label="Obrigado"
133
- prefixNode={<CircleLetter letter="C" selected={selected === 'C'} correct={statusMap.C === 'correct'} />}
103
+ prefix="C"
134
104
  onPress={() => handlePress('C')}
135
105
  />
136
106
 
@@ -172,11 +142,11 @@ export const Grid: Story = {
172
142
  },
173
143
  render: () => (
174
144
  <View style={{ gap: 16 }}>
175
- <AnswerOptionCard status="default" label="Boa noite" prefixNode={<CircleLetter letter="A" />} />
176
- <AnswerOptionCard status="selected" label="Bom dia / Olá" prefixNode={<CircleLetter letter="B" selected />} />
177
- <AnswerOptionCard status="correct" label="Bom dia / Olá" prefixNode={<CircleLetter letter="B" correct />} />
178
- <AnswerOptionCard status="wrong" label="Obrigado" prefixNode={<CircleLetter letter="C" />} />
179
- <AnswerOptionCard status="disabled" label="Por favor" prefixNode={<CircleLetter letter="D" />} />
145
+ <AnswerOptionCard status="default" label="Boa noite" prefix="A" />
146
+ <AnswerOptionCard status="selected" label="Bom dia / Olá" prefix="B" />
147
+ <AnswerOptionCard status="correct" label="Bom dia / Olá" prefix="B" />
148
+ <AnswerOptionCard status="wrong" label="Obrigado" prefix="C" />
149
+ <AnswerOptionCard status="disabled" label="Por favor" prefix="D" />
180
150
  </View>
181
151
  ),
182
152
  };
@@ -1,4 +1,5 @@
1
- import { View, StyleSheet, Pressable, Platform } from 'react-native';
1
+ import { View, StyleSheet, Pressable, Platform, Animated } from 'react-native';
2
+ import React from 'react';
2
3
  import type { ViewStyle } from 'react-native';
3
4
  import { theme } from '../../../styles/theme';
4
5
  import type { ReactNode } from 'react';
@@ -20,10 +21,10 @@ export interface AnswerOptionCardProps {
20
21
  prefixNode?: ReactNode;
21
22
  /** Letra ou número a exibir automaticamente do lado esquerdo. */
22
23
  prefix?: string | number;
23
- /** Nó renderizado à direita. Se omitido, renderiza automaticamente ícones de check/cross consoante o estado. */
24
- suffixNode?: ReactNode;
25
24
  /** Callback para o evento de clique. */
26
25
  onPress?: () => void;
26
+ /** Define o tamanho do componente. */
27
+ size?: 'sm' | 'md' | 'lg';
27
28
  /** Estilos adicionais para o container. */
28
29
  style?: ViewStyle | ViewStyle[];
29
30
  }
@@ -33,11 +34,30 @@ export const AnswerOptionCard = ({
33
34
  label,
34
35
  prefixNode,
35
36
  prefix,
36
- suffixNode,
37
37
  onPress,
38
+ size = 'md',
38
39
  style,
39
40
  }: AnswerOptionCardProps) => {
40
41
  const { scale, scaleText } = useResponsive();
42
+ const scaleAnim = React.useRef(new Animated.Value(1)).current;
43
+
44
+ React.useEffect(() => {
45
+ if (status !== 'default' && status !== 'disabled') {
46
+ Animated.sequence([
47
+ Animated.timing(scaleAnim, {
48
+ toValue: 0.95,
49
+ duration: 100,
50
+ useNativeDriver: true,
51
+ }),
52
+ Animated.spring(scaleAnim, {
53
+ toValue: 1,
54
+ friction: 4,
55
+ tension: 40,
56
+ useNativeDriver: true,
57
+ }),
58
+ ]).start();
59
+ }
60
+ }, [status, scaleAnim]);
41
61
 
42
62
  const getStyles = () => {
43
63
  switch (status) {
@@ -123,36 +143,88 @@ export const AnswerOptionCard = ({
123
143
  </View>
124
144
  );
125
145
  }
126
- return suffixNode;
146
+ return null;
147
+ };
148
+
149
+ const getPaddingVertical = () => {
150
+ switch (size) {
151
+ case 'lg': return scale(14);
152
+ case 'sm': return scale(8);
153
+ default: return scale(10);
154
+ }
155
+ };
156
+
157
+ const getPaddingHorizontal = () => {
158
+ switch (size) {
159
+ case 'lg': return scale(20);
160
+ case 'sm': return scale(12);
161
+ default: return scale(15);
162
+ }
163
+ };
164
+
165
+ const getMinHeight = () => {
166
+ switch (size) {
167
+ case 'lg': return scale(64);
168
+ case 'sm': return scale(48);
169
+ default: return scale(56);
170
+ }
171
+ };
172
+
173
+ const getBorderRadius = () => {
174
+ switch (size) {
175
+ case 'lg': return scale(16);
176
+ case 'sm': return scale(10);
177
+ default: return scale(12);
178
+ }
179
+ };
180
+
181
+ const getFontSize = () => {
182
+ switch (size) {
183
+ case 'lg': return scaleText(18);
184
+ case 'sm': return scaleText(14);
185
+ default: return scaleText(16);
186
+ }
127
187
  };
128
188
 
129
189
  return (
130
- <Pressable
131
- style={[
132
- styles.container,
133
- {
134
- borderColor: vStyles.borderColor,
135
- backgroundColor: vStyles.backgroundColor,
136
- paddingVertical: scale(10),
137
- paddingHorizontal: scale(15),
138
- borderRadius: scale(12),
139
- minHeight: scale(56),
140
- },
141
- webShadow,
142
- style,
143
- ]}
144
- onPress={status !== 'disabled' ? onPress : undefined}
145
- disabled={status === 'disabled' || !onPress}
146
- >
147
- <View style={styles.contentRow}>
148
- <View style={styles.leftSection}>
149
- {prefixNode && <View style={[styles.prefixContainer, { marginRight: scale(12) }]}>{prefixNode}</View>}
150
- {prefix && !prefixNode && (
151
- <View style={[styles.prefixContainer, { marginRight: scale(12) }]}>
190
+ <Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
191
+ <Pressable
192
+ style={[
193
+ styles.container,
194
+ {
195
+ borderColor: vStyles.borderColor,
196
+ backgroundColor: vStyles.backgroundColor,
197
+ paddingVertical: getPaddingVertical(),
198
+ paddingHorizontal: getPaddingHorizontal(),
199
+ borderRadius: getBorderRadius(),
200
+ minHeight: getMinHeight(),
201
+ },
202
+ webShadow,
203
+ style,
204
+ ]}
205
+ onPress={status !== 'disabled' ? onPress : undefined}
206
+ disabled={status === 'disabled' || !onPress}
207
+ >
208
+ <View style={styles.contentRow}>
209
+ <View style={styles.leftSection}>
210
+ {prefixNode && <View style={[styles.prefixContainer, { marginRight: scale(12) }]}>{prefixNode}</View>}
211
+ {prefix && !prefixNode && (
212
+ <View style={[
213
+ styles.prefixContainer,
214
+ {
215
+ marginRight: scale(12),
216
+ width: scale(28),
217
+ height: scale(28),
218
+ borderRadius: scale(14),
219
+ backgroundColor: status === 'correct' ? theme.colors.success : status === 'wrong' ? theme.colors.error : theme.colors.brown[100],
220
+ alignItems: 'center',
221
+ justifyContent: 'center'
222
+ }
223
+ ]}>
152
224
  <Typography style={{
153
- color: status === 'selected' || status === 'correct' ? theme.colors.primary : theme.colors.brown[400],
225
+ color: status === 'correct' || status === 'wrong' ? theme.colors.white : theme.colors.brown[500],
154
226
  fontWeight: 'bold',
155
- fontSize: scaleText(16)
227
+ fontSize: scaleText(14)
156
228
  }}>
157
229
  {prefix}
158
230
  </Typography>
@@ -163,7 +235,7 @@ export const AnswerOptionCard = ({
163
235
  styles.label,
164
236
  {
165
237
  color: status === 'disabled' ? theme.colors.brown[300] : vStyles.textColor,
166
- fontSize: scaleText(15),
238
+ fontSize: getFontSize(),
167
239
  }
168
240
  ]}
169
241
  >
@@ -173,6 +245,7 @@ export const AnswerOptionCard = ({
173
245
  {renderSuffix()}
174
246
  </View>
175
247
  </Pressable>
248
+ </Animated.View>
176
249
  );
177
250
  };
178
251