@xetwa/design-system 1.0.36 → 1.0.37
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
|
@@ -10,13 +10,13 @@ const meta = {
|
|
|
10
10
|
layout: 'padded',
|
|
11
11
|
docs: {
|
|
12
12
|
description: {
|
|
13
|
-
component: 'O **ModuleCard** exibe as lições no percurso principal do utilizador (a "Tree" de aprendizagem).\n\nPossui diferentes estados (`active`, `locked`, `completed`, `dark`) e pode incluir barras de progresso fragmentadas.\n\n### Exemplo de Uso\n```tsx\n<ModuleCard \n status="active"\n number={2}\n title="No mercado de Dakar"\n subtitle="Regatear, números, comida"\n progress={0.6}\n segments={5}\n tagLabel="EM CURSO"\n/>\n```',
|
|
13
|
+
component: 'O **ModuleCard** exibe as lições no percurso principal do utilizador (a "Tree" de aprendizagem).\n\nPossui diferentes estados (`active`, `locked`, `completed`, `dark`, `active-dark`) e pode incluir barras de progresso fragmentadas.\n\n### Animações\n- **active**: O cartão inteiro é animado e reativo ao toque.\n- **active-dark**: O cartão é fixo, apenas os botões de ação ("Agendar Aula" e "Rever") são animados individualmente.\n- **completed**: O cartão é fixo, apenas o botão de revisão ("Rever Módulo" ou o ícone) tem animação individual.\n\n### Exemplo de Uso\n```tsx\n<ModuleCard \n status="active"\n number={2}\n title="No mercado de Dakar"\n subtitle="Regatear, números, comida"\n progress={0.6}\n segments={5}\n tagLabel="EM CURSO"\n/>\n```',
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
tags: ['autodocs'],
|
|
18
18
|
argTypes: {
|
|
19
|
-
status: { control: 'radio', options: ['active', 'locked', 'completed', 'dark'] },
|
|
19
|
+
status: { control: 'radio', options: ['active', 'locked', 'completed', 'dark', 'active-dark'] },
|
|
20
20
|
number: { control: 'number' },
|
|
21
21
|
title: { control: 'text' },
|
|
22
22
|
subtitle: { control: 'text' },
|
|
@@ -68,6 +68,26 @@ export const DarkLocked: Story = {
|
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
export const ActiveDark: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
status: 'active-dark',
|
|
74
|
+
title: 'No mercado de Dakar',
|
|
75
|
+
subtitle: 'Regatear, números, comida',
|
|
76
|
+
subtext: '• 3/5',
|
|
77
|
+
tagLabel: 'AULA MARCADA',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const Completed: Story = {
|
|
82
|
+
args: {
|
|
83
|
+
status: 'completed',
|
|
84
|
+
number: 1,
|
|
85
|
+
title: 'Saudações & teranga',
|
|
86
|
+
subtitle: 'Concluído',
|
|
87
|
+
subtext: '• 5/5',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
71
91
|
export const Grid: Story = {
|
|
72
92
|
args: {
|
|
73
93
|
status: 'active',
|
|
@@ -103,6 +123,13 @@ export const Grid: Story = {
|
|
|
103
123
|
title="Aula ao vivo + Teste final"
|
|
104
124
|
subtitle="Acaba os 4 módulos para desbloquear"
|
|
105
125
|
/>
|
|
126
|
+
<ModuleCard
|
|
127
|
+
status="active-dark"
|
|
128
|
+
title="Aula de revisão agendada"
|
|
129
|
+
subtitle="Conversação e prática"
|
|
130
|
+
subtext="• 4/4"
|
|
131
|
+
tagLabel="AULA MARCADA"
|
|
132
|
+
/>
|
|
106
133
|
</View>
|
|
107
134
|
),
|
|
108
135
|
};
|
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import { View, StyleSheet, Pressable } from 'react-native';
|
|
1
|
+
import { View, StyleSheet, Pressable, Animated } from 'react-native';
|
|
2
2
|
import type { ViewStyle } from 'react-native';
|
|
3
3
|
import { theme } from '../../../styles/theme';
|
|
4
4
|
import { Tag } from '../../DataDisplay/Tag/Tag';
|
|
5
5
|
import { ProgressBar } from '../../DataDisplay/ProgressBar/ProgressBar';
|
|
6
6
|
import { Badge } from '../../DataDisplay/Badge/Badge';
|
|
7
|
-
import { Lock, Check, GraduationCap } from 'lucide-react-native';
|
|
7
|
+
import { Lock, Check, GraduationCap, RotateCcw, CalendarPlus } from 'lucide-react-native';
|
|
8
8
|
import { useResponsive } from '../../../hooks/useResponsive';
|
|
9
9
|
import { Typography } from '../../Typography/Typography';
|
|
10
|
+
import React, { useRef } from 'react';
|
|
10
11
|
|
|
11
|
-
export type ModuleCardStatus = 'active' | 'locked' | 'completed' | 'dark';
|
|
12
|
+
export type ModuleCardStatus = 'active' | 'locked' | 'completed' | 'dark' | 'active-dark';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Cartão que representa uma lição ou módulo no caminho de aprendizagem.
|
|
16
|
+
*
|
|
17
|
+
* Regras de Animação e Clique:
|
|
18
|
+
* - `active`: O cartão como um todo tem animação de escala e é clicável via `onPress`.
|
|
19
|
+
* - `active-dark`: O cartão é estático. Apenas os botões internos de "Agendar" (`onSchedulePress`) e "Rever" (`onReviewPress`) têm animação individual.
|
|
20
|
+
* - `completed`: O cartão é estático. Apenas o ícone/botão superior direito de "Rever" (`onReviewPress`) tem animação individual.
|
|
21
|
+
* - `locked` / `dark`: O cartão não é interativo.
|
|
15
22
|
*/
|
|
16
23
|
export interface ModuleCardProps {
|
|
17
|
-
/** Estado atual da lição (ativa, trancada, concluída,
|
|
24
|
+
/** Estado atual da lição (ativa, trancada, concluída, etc). */
|
|
18
25
|
status: ModuleCardStatus;
|
|
19
26
|
/** Número da lição (ex: 1, 2, 3). */
|
|
20
27
|
number?: number;
|
|
@@ -38,6 +45,10 @@ export interface ModuleCardProps {
|
|
|
38
45
|
/** Cor customizada para o subtítulo. */
|
|
39
46
|
subtitleColor?: string;
|
|
40
47
|
style?: ViewStyle | ViewStyle[];
|
|
48
|
+
/** Função de clique no botão de Agendar (modo active/dark). */
|
|
49
|
+
onSchedulePress?: () => void;
|
|
50
|
+
/** Função de clique no botão de Rever (modo completed/active/dark). */
|
|
51
|
+
onReviewPress?: () => void;
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
export const ModuleCard = ({
|
|
@@ -54,11 +65,53 @@ export const ModuleCard = ({
|
|
|
54
65
|
subtitleColor,
|
|
55
66
|
onPress,
|
|
56
67
|
style,
|
|
68
|
+
onSchedulePress,
|
|
69
|
+
onReviewPress,
|
|
57
70
|
}: ModuleCardProps) => {
|
|
58
71
|
const { scale, scaleText, width } = useResponsive();
|
|
59
|
-
|
|
60
72
|
const isMobileScreen = width < 400;
|
|
61
73
|
|
|
74
|
+
const scheduleScale = useRef(new Animated.Value(1)).current;
|
|
75
|
+
const reviewScale = useRef(new Animated.Value(1)).current;
|
|
76
|
+
const completedReviewScale = useRef(new Animated.Value(1)).current;
|
|
77
|
+
const scaleAnim = useRef(new Animated.Value(1)).current;
|
|
78
|
+
|
|
79
|
+
const handlePressIn = () => {
|
|
80
|
+
if (status !== 'active') return;
|
|
81
|
+
Animated.timing(scaleAnim, {
|
|
82
|
+
toValue: 0.95,
|
|
83
|
+
duration: 100,
|
|
84
|
+
useNativeDriver: true,
|
|
85
|
+
}).start();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handlePressOut = () => {
|
|
89
|
+
if (status !== 'active') return;
|
|
90
|
+
Animated.spring(scaleAnim, {
|
|
91
|
+
toValue: 1,
|
|
92
|
+
friction: 4,
|
|
93
|
+
tension: 40,
|
|
94
|
+
useNativeDriver: true,
|
|
95
|
+
}).start();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const animateBtnIn = (anim: Animated.Value) => {
|
|
99
|
+
Animated.timing(anim, {
|
|
100
|
+
toValue: 0.9,
|
|
101
|
+
duration: 100,
|
|
102
|
+
useNativeDriver: true,
|
|
103
|
+
}).start();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const animateBtnOut = (anim: Animated.Value) => {
|
|
107
|
+
Animated.spring(anim, {
|
|
108
|
+
toValue: 1,
|
|
109
|
+
friction: 4,
|
|
110
|
+
tension: 40,
|
|
111
|
+
useNativeDriver: true,
|
|
112
|
+
}).start();
|
|
113
|
+
};
|
|
114
|
+
|
|
62
115
|
const getContainerStyle = () => {
|
|
63
116
|
switch (status) {
|
|
64
117
|
case 'active':
|
|
@@ -81,6 +134,7 @@ export const ModuleCard = ({
|
|
|
81
134
|
...theme.shadows.sm,
|
|
82
135
|
};
|
|
83
136
|
case 'dark':
|
|
137
|
+
case 'active-dark':
|
|
84
138
|
return {
|
|
85
139
|
borderColor: theme.colors.brown[800],
|
|
86
140
|
borderWidth: 0,
|
|
@@ -96,6 +150,7 @@ export const ModuleCard = ({
|
|
|
96
150
|
case 'locked':
|
|
97
151
|
return theme.colors.brown[400];
|
|
98
152
|
case 'dark':
|
|
153
|
+
case 'active-dark':
|
|
99
154
|
return theme.colors.white;
|
|
100
155
|
default:
|
|
101
156
|
return theme.colors.brown[800];
|
|
@@ -104,7 +159,7 @@ export const ModuleCard = ({
|
|
|
104
159
|
|
|
105
160
|
const containerStyle = getContainerStyle();
|
|
106
161
|
const titleStyleColor = getTextColor();
|
|
107
|
-
const subtitleStyleColor = subtitleColor || (status === 'completed' ? theme.colors.success : status === 'locked' || status === 'dark' ? theme.colors.brown[300] : theme.colors.brown[500]);
|
|
162
|
+
const subtitleStyleColor = subtitleColor || (status === 'completed' ? theme.colors.success : status === 'locked' || status === 'dark' || status === 'active-dark' ? theme.colors.brown[300] : theme.colors.brown[500]);
|
|
108
163
|
|
|
109
164
|
const activeShadow = status === 'active' ? {
|
|
110
165
|
shadowColor: theme.colors.primary,
|
|
@@ -151,6 +206,8 @@ export const ModuleCard = ({
|
|
|
151
206
|
</View>
|
|
152
207
|
);
|
|
153
208
|
}
|
|
209
|
+
|
|
210
|
+
|
|
154
211
|
|
|
155
212
|
return (
|
|
156
213
|
<Badge
|
|
@@ -166,62 +223,99 @@ export const ModuleCard = ({
|
|
|
166
223
|
const cardPaddingH = size === 'xl' ? scale(20) : size === 'lg' ? scale(18) : isMobileScreen ? scale(12) : scale(15);
|
|
167
224
|
|
|
168
225
|
const titleVariant = size === 'xl' ? 'h1' : size === 'lg' ? 'h2' : 'h3';
|
|
226
|
+
|
|
169
227
|
const subtitleVariant = size === 'xl' ? 'bodyLg' : size === 'lg' ? 'bodyMd' : 'bodySm';
|
|
170
228
|
|
|
229
|
+
let displaySubtext = subtext;
|
|
230
|
+
if (status === 'active-dark') {
|
|
231
|
+
if (subtext) {
|
|
232
|
+
if (subtext.trim().startsWith('•')) {
|
|
233
|
+
displaySubtext = `Aula de revisão ${subtext.trim()}`;
|
|
234
|
+
} else {
|
|
235
|
+
displaySubtext = `Aula de revisão • ${subtext}`;
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
displaySubtext = 'Aula de revisão';
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
171
242
|
return (
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
243
|
+
<Animated.View style={[{ transform: [{ scale: scaleAnim }] }, style]}>
|
|
244
|
+
<Pressable
|
|
245
|
+
style={[
|
|
246
|
+
styles.container,
|
|
247
|
+
containerStyle,
|
|
248
|
+
status === 'active' && activeShadow,
|
|
249
|
+
{
|
|
250
|
+
paddingVertical: cardPaddingV,
|
|
251
|
+
paddingHorizontal: cardPaddingH,
|
|
252
|
+
borderRadius: scale(18),
|
|
253
|
+
},
|
|
254
|
+
]}
|
|
255
|
+
onPress={onPress}
|
|
256
|
+
onPressIn={handlePressIn}
|
|
257
|
+
onPressOut={handlePressOut}
|
|
258
|
+
disabled={status === 'locked'}
|
|
259
|
+
>
|
|
187
260
|
<View style={styles.topRow}>
|
|
188
261
|
<View style={styles.leftCol}>
|
|
189
262
|
{renderLeftIconBox()}
|
|
190
263
|
</View>
|
|
191
264
|
|
|
192
265
|
<View style={[styles.content, { marginLeft: colGap }]}>
|
|
193
|
-
<Typography
|
|
194
|
-
|
|
195
|
-
{
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
266
|
+
<Typography
|
|
267
|
+
variant={titleVariant}
|
|
268
|
+
style={[
|
|
269
|
+
styles.title,
|
|
270
|
+
{
|
|
271
|
+
color: titleStyleColor,
|
|
272
|
+
marginBottom: scale(2),
|
|
273
|
+
}
|
|
274
|
+
]}
|
|
275
|
+
>
|
|
200
276
|
{title}
|
|
201
277
|
</Typography>
|
|
202
278
|
|
|
203
|
-
{(subtitle || subtext) && (
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
279
|
+
{(subtitle || subtext || displaySubtext) && (
|
|
280
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: scale(2) }}>
|
|
281
|
+
<Typography variant={subtitleVariant} style={[
|
|
282
|
+
styles.subtitle,
|
|
283
|
+
{
|
|
284
|
+
color: subtitleStyleColor,
|
|
285
|
+
flexShrink: 1, // Para o texto encolher se a tag ocupar espaço
|
|
286
|
+
}
|
|
287
|
+
]}>
|
|
288
|
+
{status !== 'active-dark' && subtitle}
|
|
289
|
+
{status !== 'active-dark' && subtitle && displaySubtext && ' '}
|
|
290
|
+
{displaySubtext && <Typography variant={subtitleVariant} style={styles.subtext}>{displaySubtext}</Typography>}
|
|
291
|
+
</Typography>
|
|
292
|
+
{tagLabel && (status === 'active-dark' || status === 'active') && (
|
|
293
|
+
<Tag
|
|
294
|
+
label={tagLabel}
|
|
295
|
+
variant="primary"
|
|
296
|
+
size={isMobileScreen ? 'xs' : 'sm'}
|
|
297
|
+
style={{ marginLeft: scale(8) }}
|
|
298
|
+
/>
|
|
299
|
+
)}
|
|
300
|
+
</View>
|
|
214
301
|
)}
|
|
215
302
|
</View>
|
|
216
303
|
|
|
217
|
-
{(
|
|
304
|
+
{(status === 'locked' || status === 'dark' || status === 'completed') && (
|
|
218
305
|
<View style={[styles.rightContent, { marginLeft: colGap }]}>
|
|
219
|
-
{
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
306
|
+
{status === 'completed' && (
|
|
307
|
+
<Animated.View style={{ transform: [{ scale: completedReviewScale }] }}>
|
|
308
|
+
<Pressable
|
|
309
|
+
style={styles.reviewIconButton}
|
|
310
|
+
onPress={onReviewPress || (() => {})}
|
|
311
|
+
onPressIn={() => animateBtnIn(completedReviewScale)}
|
|
312
|
+
onPressOut={() => animateBtnOut(completedReviewScale)}
|
|
313
|
+
hitSlop={10}
|
|
314
|
+
>
|
|
315
|
+
<RotateCcw stroke={theme.colors.primary} strokeWidth={2.5} size={isMobileScreen ? scale(20) : scale(24)} />
|
|
316
|
+
<Typography variant="label" style={[styles.reviewIconText, { fontSize: scaleText(9) }]}>REVER</Typography>
|
|
317
|
+
</Pressable>
|
|
318
|
+
</Animated.View>
|
|
225
319
|
)}
|
|
226
320
|
{(status === 'locked' || status === 'dark') && (
|
|
227
321
|
<Lock
|
|
@@ -243,7 +337,40 @@ export const ModuleCard = ({
|
|
|
243
337
|
/>
|
|
244
338
|
</View>
|
|
245
339
|
)}
|
|
340
|
+
|
|
341
|
+
{status === 'active-dark' && (
|
|
342
|
+
<View style={styles.compactDarkFooter}>
|
|
343
|
+
<Animated.View style={{ transform: [{ scale: scheduleScale }] }}>
|
|
344
|
+
<Pressable
|
|
345
|
+
onPress={onSchedulePress || (() => {})}
|
|
346
|
+
onPressIn={() => animateBtnIn(scheduleScale)}
|
|
347
|
+
onPressOut={() => animateBtnOut(scheduleScale)}
|
|
348
|
+
hitSlop={8}
|
|
349
|
+
style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: theme.colors.primary, paddingHorizontal: scale(14), paddingVertical: scale(6), borderRadius: 100 }}
|
|
350
|
+
>
|
|
351
|
+
<CalendarPlus stroke={theme.colors.white} strokeWidth={2.5} size={scale(14)} />
|
|
352
|
+
<Typography variant="label" style={{ color: theme.colors.white, fontSize: scaleText(12), fontWeight: 'bold', marginLeft: scale(6) }}>Agendar Aula</Typography>
|
|
353
|
+
</Pressable>
|
|
354
|
+
</Animated.View>
|
|
355
|
+
|
|
356
|
+
<Animated.View style={{ transform: [{ scale: reviewScale }] }}>
|
|
357
|
+
<Pressable
|
|
358
|
+
style={styles.compactReviewBtn}
|
|
359
|
+
onPress={onReviewPress || (() => {})}
|
|
360
|
+
onPressIn={() => animateBtnIn(reviewScale)}
|
|
361
|
+
onPressOut={() => animateBtnOut(reviewScale)}
|
|
362
|
+
hitSlop={8}
|
|
363
|
+
>
|
|
364
|
+
<RotateCcw stroke={theme.colors.primary} strokeWidth={2.5} size={scale(16)} />
|
|
365
|
+
<Typography variant="label" style={[styles.reviewIconText, { color: theme.colors.primary, fontSize: scaleText(10), marginLeft: scale(4), marginTop: 0 }]}>REVER</Typography>
|
|
366
|
+
</Pressable>
|
|
367
|
+
</Animated.View>
|
|
368
|
+
</View>
|
|
369
|
+
)}
|
|
370
|
+
|
|
371
|
+
|
|
246
372
|
</Pressable>
|
|
373
|
+
</Animated.View>
|
|
247
374
|
);
|
|
248
375
|
};
|
|
249
376
|
|
|
@@ -304,4 +431,35 @@ const styles = StyleSheet.create({
|
|
|
304
431
|
marginLeft: 12,
|
|
305
432
|
flexShrink: 0,
|
|
306
433
|
},
|
|
434
|
+
reviewIconButton: {
|
|
435
|
+
alignItems: 'center',
|
|
436
|
+
justifyContent: 'center',
|
|
437
|
+
padding: 4,
|
|
438
|
+
},
|
|
439
|
+
reviewIconText: {
|
|
440
|
+
color: theme.colors.primary,
|
|
441
|
+
marginTop: 2,
|
|
442
|
+
textAlign: 'center',
|
|
443
|
+
},
|
|
444
|
+
compactDarkFooter: {
|
|
445
|
+
flexDirection: 'row',
|
|
446
|
+
alignItems: 'center',
|
|
447
|
+
justifyContent: 'space-between',
|
|
448
|
+
marginTop: 10,
|
|
449
|
+
paddingTop: 8,
|
|
450
|
+
borderTopWidth: 1,
|
|
451
|
+
borderTopColor: 'rgba(255,255,255,0.1)',
|
|
452
|
+
},
|
|
453
|
+
compactScheduleBtn: {
|
|
454
|
+
flexDirection: 'row',
|
|
455
|
+
alignItems: 'center',
|
|
456
|
+
},
|
|
457
|
+
compactReviewBtn: {
|
|
458
|
+
flexDirection: 'row',
|
|
459
|
+
alignItems: 'center',
|
|
460
|
+
backgroundColor: 'rgba(255,255,255,0.05)',
|
|
461
|
+
paddingHorizontal: 8,
|
|
462
|
+
paddingVertical: 4,
|
|
463
|
+
borderRadius: 8,
|
|
464
|
+
},
|
|
307
465
|
});
|