@xetwa/design-system 1.0.2

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.
Files changed (168) hide show
  1. package/.github/workflows/publish.yml +30 -0
  2. package/.storybook/main.ts +17 -0
  3. package/.storybook/preview.tsx +65 -0
  4. package/eslint.config.js +22 -0
  5. package/index.html +13 -0
  6. package/package.json +55 -0
  7. package/public/favicon.svg +1 -0
  8. package/public/icons.svg +24 -0
  9. package/src/App.css +184 -0
  10. package/src/App.tsx +122 -0
  11. package/src/BordasSombras.mdx +343 -0
  12. package/src/Colors.mdx +393 -0
  13. package/src/GrelhaLayout.mdx +375 -0
  14. package/src/Icons.mdx +42 -0
  15. package/src/MovimentoBreakpoints.mdx +379 -0
  16. package/src/Spacing.mdx +231 -0
  17. package/src/Typography.mdx +321 -0
  18. package/src/assets/Mascots/vava/estaticos/vava-acenar.svg +3 -0
  19. package/src/assets/Mascots/vava/estaticos/vava-acertou.svg +3 -0
  20. package/src/assets/Mascots/vava/estaticos/vava-celebrar.svg +3 -0
  21. package/src/assets/Mascots/vava/estaticos/vava-com-autocolantes.svg +3 -0
  22. package/src/assets/Mascots/vava/estaticos/vava-dormir.svg +3 -0
  23. package/src/assets/Mascots/vava/estaticos/vava-errou.svg +3 -0
  24. package/src/assets/Mascots/vava/estaticos/vava-feliz.svg +3 -0
  25. package/src/assets/Mascots/vava/estaticos/vava-neutra.svg +3 -0
  26. package/src/assets/Mascots/vava/estaticos/vava-original.svg +3 -0
  27. package/src/assets/Mascots/vava/estaticos/vava-pensar.svg +3 -0
  28. package/src/assets/Mascots/vava/estaticos/vava-surpresa.svg +3 -0
  29. package/src/assets/Mascots/yaya/avatar/yaya-avatar-falar.svg +7 -0
  30. package/src/assets/Mascots/yaya/avatar/yaya-avatar-feliz.svg +7 -0
  31. package/src/assets/Mascots/yaya/avatar/yaya-avatar-transparente.svg +6 -0
  32. package/src/assets/Mascots/yaya/estaticos/yaya-acertou.svg +13 -0
  33. package/src/assets/Mascots/yaya/estaticos/yaya-celebrar.svg +13 -0
  34. package/src/assets/Mascots/yaya/estaticos/yaya-dormir.svg +13 -0
  35. package/src/assets/Mascots/yaya/estaticos/yaya-errou.svg +13 -0
  36. package/src/assets/Mascots/yaya/estaticos/yaya-falar.svg +13 -0
  37. package/src/assets/Mascots/yaya/estaticos/yaya-feliz.svg +13 -0
  38. package/src/assets/Mascots/yaya/estaticos/yaya-neutra.svg +13 -0
  39. package/src/assets/Mascots/yaya/estaticos/yaya-pensar.svg +13 -0
  40. package/src/assets/Mascots/yaya/estaticos/yaya-surpresa.svg +13 -0
  41. package/src/assets/hero.png +0 -0
  42. package/src/assets/react.svg +1 -0
  43. package/src/assets/vite.svg +1 -0
  44. package/src/components/Buttons/BackButton/BackButton.stories.tsx +86 -0
  45. package/src/components/Buttons/BackButton/BackButton.tsx +107 -0
  46. package/src/components/Buttons/FabButton/FabButton.stories.tsx +74 -0
  47. package/src/components/Buttons/FabButton/FabButton.tsx +161 -0
  48. package/src/components/Buttons/IconButton/IconButton.stories.tsx +99 -0
  49. package/src/components/Buttons/IconButton/IconButton.tsx +189 -0
  50. package/src/components/Buttons/JourneyButton/JourneyButton.stories.tsx +80 -0
  51. package/src/components/Buttons/JourneyButton/JourneyButton.tsx +194 -0
  52. package/src/components/Buttons/MainButton/MainButton.stories.tsx +144 -0
  53. package/src/components/Buttons/MainButton/MainButton.tsx +258 -0
  54. package/src/components/Buttons/WordChip/WordChip.stories.tsx +84 -0
  55. package/src/components/Buttons/WordChip/WordChip.tsx +157 -0
  56. package/src/components/Buttons/WordChip/index.ts +1 -0
  57. package/src/components/Cards/AnswerOptionCard/AnswerOptionCard.stories.tsx +181 -0
  58. package/src/components/Cards/AnswerOptionCard/AnswerOptionCard.tsx +201 -0
  59. package/src/components/Cards/BaseCard/BaseCard.stories.tsx +148 -0
  60. package/src/components/Cards/BaseCard/BaseCard.tsx +181 -0
  61. package/src/components/Cards/BoardingPassCard/BoardingPassCard.stories.tsx +205 -0
  62. package/src/components/Cards/BoardingPassCard/BoardingPassCard.tsx +384 -0
  63. package/src/components/Cards/BoardingPassCard/FlightIcon.tsx +21 -0
  64. package/src/components/Cards/BoardingPassCard/FlightIcon.web.tsx +18 -0
  65. package/src/components/Cards/CountryCard/CountryCard.stories.tsx +97 -0
  66. package/src/components/Cards/CountryCard/CountryCard.tsx +185 -0
  67. package/src/components/Cards/LevelProgressCard/LevelProgressCard.stories.tsx +61 -0
  68. package/src/components/Cards/LevelProgressCard/LevelProgressCard.tsx +143 -0
  69. package/src/components/Cards/ModuleCard/ModuleCard.stories.tsx +108 -0
  70. package/src/components/Cards/ModuleCard/ModuleCard.tsx +304 -0
  71. package/src/components/Cards/ModuleCompletionCard/ModuleCompletionCard.stories.tsx +53 -0
  72. package/src/components/Cards/ModuleCompletionCard/ModuleCompletionCard.tsx +167 -0
  73. package/src/components/Cards/Passport/PassportCover.tsx +114 -0
  74. package/src/components/Cards/Passport/PassportIDPage.tsx +217 -0
  75. package/src/components/Cards/Passport/PassportStampPage.tsx +253 -0
  76. package/src/components/Cards/Passport/PassportViewer.stories.tsx +95 -0
  77. package/src/components/Cards/Passport/PassportViewer.tsx +328 -0
  78. package/src/components/Cards/PlanCard/PlanCard.stories.tsx +159 -0
  79. package/src/components/Cards/PlanCard/PlanCard.tsx +317 -0
  80. package/src/components/Cards/PricingCard/PricingCard.stories.tsx +83 -0
  81. package/src/components/Cards/PricingCard/PricingCard.tsx +144 -0
  82. package/src/components/Cards/SelectionCard/SelectionCard.stories.tsx +77 -0
  83. package/src/components/Cards/SelectionCard/SelectionCard.tsx +111 -0
  84. package/src/components/Cards/TeacherCard/TeacherCard.stories.tsx +126 -0
  85. package/src/components/Cards/TeacherCard/TeacherCard.tsx +236 -0
  86. package/src/components/Charts/WeeklyProgressChart/WeeklyProgressChart.stories.tsx +89 -0
  87. package/src/components/Charts/WeeklyProgressChart/WeeklyProgressChart.tsx +179 -0
  88. package/src/components/Charts/WeeklyProgressChart/index.ts +1 -0
  89. package/src/components/DataDisplay/Avatar/Avatar.stories.tsx +85 -0
  90. package/src/components/DataDisplay/Avatar/Avatar.tsx +118 -0
  91. package/src/components/DataDisplay/Avatar/index.ts +1 -0
  92. package/src/components/DataDisplay/Badge/Badge.stories.tsx +62 -0
  93. package/src/components/DataDisplay/Badge/Badge.tsx +146 -0
  94. package/src/components/DataDisplay/Flag/Flag.stories.tsx +98 -0
  95. package/src/components/DataDisplay/Flag/Flag.tsx +347 -0
  96. package/src/components/DataDisplay/Flag/Flag.web.tsx +334 -0
  97. package/src/components/DataDisplay/InstructionBubble/InstructionBubble.stories.tsx +80 -0
  98. package/src/components/DataDisplay/InstructionBubble/InstructionBubble.tsx +86 -0
  99. package/src/components/DataDisplay/LevelBadge/LevelBadge.stories.tsx +99 -0
  100. package/src/components/DataDisplay/LevelBadge/LevelBadge.tsx +118 -0
  101. package/src/components/DataDisplay/ProgressBar/ProgressBar.stories.tsx +95 -0
  102. package/src/components/DataDisplay/ProgressBar/ProgressBar.tsx +117 -0
  103. package/src/components/DataDisplay/StatBadge/StatBadge.stories.tsx +82 -0
  104. package/src/components/DataDisplay/StatBadge/StatBadge.tsx +135 -0
  105. package/src/components/DataDisplay/StatBadge/index.ts +1 -0
  106. package/src/components/DataDisplay/Tag/Tag.stories.tsx +63 -0
  107. package/src/components/DataDisplay/Tag/Tag.tsx +148 -0
  108. package/src/components/Feedback/FeedbackBottomSheet/FeedbackBottomSheet.stories.tsx +74 -0
  109. package/src/components/Feedback/FeedbackBottomSheet/FeedbackBottomSheet.tsx +200 -0
  110. package/src/components/Forms/Checkbox/Checkbox.stories.tsx +60 -0
  111. package/src/components/Forms/Checkbox/Checkbox.tsx +127 -0
  112. package/src/components/Forms/Checkbox/index.ts +1 -0
  113. package/src/components/Forms/DaySelector/DaySelector.stories.tsx +62 -0
  114. package/src/components/Forms/DaySelector/DaySelector.tsx +133 -0
  115. package/src/components/Forms/Radio/Radio.stories.tsx +61 -0
  116. package/src/components/Forms/Radio/Radio.tsx +141 -0
  117. package/src/components/Forms/Radio/index.ts +1 -0
  118. package/src/components/Forms/SegmentedToggle/SegmentedToggle.stories.tsx +67 -0
  119. package/src/components/Forms/SegmentedToggle/SegmentedToggle.tsx +150 -0
  120. package/src/components/Forms/SegmentedToggle/index.ts +1 -0
  121. package/src/components/Forms/Select/Select.stories.tsx +59 -0
  122. package/src/components/Forms/Select/Select.tsx +255 -0
  123. package/src/components/Forms/Select/index.ts +1 -0
  124. package/src/components/Forms/Switch/Switch.stories.tsx +57 -0
  125. package/src/components/Forms/Switch/Switch.tsx +120 -0
  126. package/src/components/Forms/Switch/index.ts +1 -0
  127. package/src/components/Forms/TimeSlot/TimeSlot.stories.tsx +62 -0
  128. package/src/components/Forms/TimeSlot/TimeSlot.tsx +116 -0
  129. package/src/components/Headers/CountryHeader/CountryHeader.stories.tsx +114 -0
  130. package/src/components/Headers/CountryHeader/CountryHeader.tsx +147 -0
  131. package/src/components/Headers/ProfileHeader/ProfileHeader.stories.tsx +141 -0
  132. package/src/components/Headers/ProfileHeader/ProfileHeader.tsx +233 -0
  133. package/src/components/Inputs/FillInTheBlank/FillInTheBlank.stories.tsx +333 -0
  134. package/src/components/Inputs/FillInTheBlank/FillInTheBlank.tsx +287 -0
  135. package/src/components/Inputs/FillInTheBlank/index.ts +1 -0
  136. package/src/components/Inputs/TextInput/TextInput.stories.tsx +103 -0
  137. package/src/components/Inputs/TextInput/TextInput.tsx +187 -0
  138. package/src/components/Mascots/Vava/Vava.stories.tsx +114 -0
  139. package/src/components/Mascots/Vava/Vava.tsx +276 -0
  140. package/src/components/Mascots/Yaya/Yaya.stories.tsx +98 -0
  141. package/src/components/Mascots/Yaya/Yaya.tsx +233 -0
  142. package/src/components/Navigation/BottomTabBar/BottomTabBar.stories.tsx +68 -0
  143. package/src/components/Navigation/BottomTabBar/BottomTabBar.tsx +85 -0
  144. package/src/components/Navigation/BottomTabBar/BottomTabBarItem.tsx +68 -0
  145. package/src/components/Navigation/BottomTabBar/index.ts +2 -0
  146. package/src/components/Navigation/PaginationDots/PaginationDots.stories.tsx +62 -0
  147. package/src/components/Navigation/PaginationDots/PaginationDots.tsx +81 -0
  148. package/src/components/Navigation/PaginationDots/index.ts +1 -0
  149. package/src/components/Navigation/ProgressMap/ProgressMap.stories.tsx +116 -0
  150. package/src/components/Navigation/ProgressMap/ProgressMap.tsx +827 -0
  151. package/src/components/Navigation/WorldProgressMap/WorldProgressMap.stories.tsx +169 -0
  152. package/src/components/Navigation/WorldProgressMap/WorldProgressMap.tsx +790 -0
  153. package/src/components/Typography/Typography.stories.tsx +127 -0
  154. package/src/components/Typography/Typography.tsx +53 -0
  155. package/src/hooks/useResponsive.ts +41 -0
  156. package/src/hooks/useTheme.ts +32 -0
  157. package/src/index.css +355 -0
  158. package/src/index.ts +28 -0
  159. package/src/main.tsx +10 -0
  160. package/src/mocks/codegenNativeComponent.ts +5 -0
  161. package/src/styles/countryThemes.ts +79 -0
  162. package/src/styles/theme.ts +297 -0
  163. package/src/utils/iconMap.ts +23 -0
  164. package/tsconfig.app.json +25 -0
  165. package/tsconfig.json +7 -0
  166. package/tsconfig.node.json +24 -0
  167. package/vite.config.ts +51 -0
  168. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,304 @@
1
+ import { View, StyleSheet, Pressable } from 'react-native';
2
+ import type { ViewStyle } from 'react-native';
3
+ import { theme } from '../../../styles/theme';
4
+ import { Tag } from '../../DataDisplay/Tag/Tag';
5
+ import { ProgressBar } from '../../DataDisplay/ProgressBar/ProgressBar';
6
+ import { Badge } from '../../DataDisplay/Badge/Badge';
7
+ import { Lock, Check, GraduationCap } from 'lucide-react-native';
8
+ import { useResponsive } from '../../../hooks/useResponsive';
9
+ import { Typography } from '../../Typography/Typography';
10
+
11
+ export type ModuleCardStatus = 'active' | 'locked' | 'completed' | 'dark';
12
+
13
+ /**
14
+ * Cartão que representa uma lição ou módulo no caminho de aprendizagem.
15
+ */
16
+ export interface ModuleCardProps {
17
+ /** Estado atual da lição (ativa, trancada, concluída, ou o módulo "dark" final). */
18
+ status: ModuleCardStatus;
19
+ /** Número da lição (ex: 1, 2, 3). */
20
+ number?: number;
21
+ /** Título principal da lição. */
22
+ title: string;
23
+ /** Subtítulo descritivo. */
24
+ subtitle?: string;
25
+ /** Texto secundário menor que o subtítulo. */
26
+ subtext?: string;
27
+ /** Etiqueta superior (ex: 'EM CURSO'). */
28
+ tagLabel?: string;
29
+ /** Progresso (0 a 1). */
30
+ progress?: number;
31
+ /** Número de segmentos na barra de progresso. */
32
+ segments?: number;
33
+ onPress?: () => void;
34
+ /** Cor customizada para o título. */
35
+ titleColor?: string;
36
+ /** Cor customizada para o subtítulo. */
37
+ subtitleColor?: string;
38
+ style?: ViewStyle | ViewStyle[];
39
+ }
40
+
41
+ export const ModuleCard = ({
42
+ status,
43
+ number,
44
+ title,
45
+ subtitle,
46
+ subtext,
47
+ tagLabel,
48
+ progress,
49
+ segments,
50
+ titleColor,
51
+ subtitleColor,
52
+ onPress,
53
+ style,
54
+ }: ModuleCardProps) => {
55
+ const { scale, scaleText, width } = useResponsive();
56
+
57
+ const isMobileScreen = width < 400;
58
+
59
+ const getContainerStyle = () => {
60
+ switch (status) {
61
+ case 'active':
62
+ return {
63
+ borderColor: theme.colors.primary,
64
+ borderWidth: 2,
65
+ backgroundColor: theme.colors.white,
66
+ };
67
+ case 'locked':
68
+ return {
69
+ borderColor: theme.colors.brown[100],
70
+ borderWidth: 1.5,
71
+ backgroundColor: 'rgba(255, 255, 255, 0.7)',
72
+ };
73
+ case 'completed':
74
+ return {
75
+ borderColor: theme.colors.brown[100],
76
+ borderWidth: 1.5,
77
+ backgroundColor: theme.colors.white,
78
+ ...theme.shadows.sm,
79
+ };
80
+ case 'dark':
81
+ return {
82
+ borderColor: theme.colors.brown[800],
83
+ borderWidth: 0,
84
+ backgroundColor: theme.colors.brown[800],
85
+ ...theme.shadows.sm,
86
+ };
87
+ }
88
+ };
89
+
90
+ const getTextColor = () => {
91
+ if (titleColor) return titleColor;
92
+ switch (status) {
93
+ case 'locked':
94
+ return theme.colors.brown[400];
95
+ case 'dark':
96
+ return theme.colors.white;
97
+ default:
98
+ return theme.colors.brown[800];
99
+ }
100
+ };
101
+
102
+ const containerStyle = getContainerStyle();
103
+ const titleStyleColor = getTextColor();
104
+ const subtitleStyleColor = subtitleColor || (status === 'completed' ? theme.colors.success : status === 'locked' || status === 'dark' ? theme.colors.brown[300] : theme.colors.brown[500]);
105
+
106
+ const activeShadow = status === 'active' ? {
107
+ shadowColor: theme.colors.primary,
108
+ shadowOffset: { width: 0, height: 4 },
109
+ shadowOpacity: 0.14,
110
+ shadowRadius: 14,
111
+ elevation: 4,
112
+ } : {};
113
+
114
+ const renderLeftIconBox = () => {
115
+ const bSize = isMobileScreen ? 'xl' : 'xxl';
116
+ if (status === 'locked') {
117
+ return (
118
+ <Badge
119
+ variant="disabled"
120
+ size={bSize}
121
+ content={number}
122
+ />
123
+ );
124
+ }
125
+ if (status === 'completed') {
126
+ const checkSize = isMobileScreen ? scale(16) : scale(22);
127
+ return (
128
+ <Badge
129
+ variant="success"
130
+ size={bSize}
131
+ content={<Check stroke={theme.colors.white} strokeWidth={2.4} size={checkSize} />}
132
+ />
133
+ );
134
+ }
135
+ if (status === 'dark') {
136
+ const boxSize = isMobileScreen ? scale(34) : scale(44);
137
+ const capSize = isMobileScreen ? scale(18) : scale(24);
138
+ return (
139
+ <View style={[
140
+ styles.darkIconBox,
141
+ {
142
+ width: boxSize,
143
+ height: boxSize,
144
+ borderRadius: isMobileScreen ? scale(9) : scale(12),
145
+ }
146
+ ]}>
147
+ <GraduationCap stroke={theme.colors.primary} strokeWidth={2.2} size={capSize} />
148
+ </View>
149
+ );
150
+ }
151
+ return (
152
+ <Badge
153
+ variant="primary"
154
+ size={bSize}
155
+ content={number}
156
+ />
157
+ );
158
+ };
159
+
160
+ const colGap = isMobileScreen ? scale(8) : scale(12);
161
+
162
+ return (
163
+ <Pressable
164
+ style={[
165
+ styles.container,
166
+ containerStyle,
167
+ status === 'active' && activeShadow,
168
+ {
169
+ paddingVertical: isMobileScreen ? scale(10) : scale(14),
170
+ paddingHorizontal: isMobileScreen ? scale(12) : scale(15),
171
+ borderRadius: scale(18),
172
+ },
173
+ style,
174
+ ]}
175
+ onPress={onPress}
176
+ disabled={!onPress || (status === 'locked' && !onPress)}
177
+ >
178
+ <View style={styles.topRow}>
179
+ <View style={styles.leftCol}>
180
+ {renderLeftIconBox()}
181
+ </View>
182
+
183
+ <View style={[styles.content, { marginLeft: colGap }]}>
184
+ <Typography style={[
185
+ styles.title,
186
+ {
187
+ color: titleStyleColor,
188
+ fontSize: isMobileScreen ? scaleText(14) : scaleText(16),
189
+ marginBottom: scale(2),
190
+ }
191
+ ]}>
192
+ {title}
193
+ </Typography>
194
+
195
+ {(subtitle || subtext) && (
196
+ <Typography style={[
197
+ styles.subtitle,
198
+ {
199
+ color: subtitleStyleColor,
200
+ fontSize: isMobileScreen ? scaleText(10) : scaleText(12),
201
+ }
202
+ ]}>
203
+ {subtitle}
204
+ {subtitle && subtext && ' '}
205
+ {subtext && <Typography style={styles.subtext}>{subtext}</Typography>}
206
+ </Typography>
207
+ )}
208
+ </View>
209
+
210
+ {(tagLabel || status === 'locked' || status === 'dark') && (
211
+ <View style={[styles.rightContent, { marginLeft: colGap }]}>
212
+ {tagLabel && status === 'active' && (
213
+ <Tag
214
+ label={tagLabel}
215
+ variant="primary"
216
+ size={isMobileScreen ? 'xs' : 'sm'}
217
+ />
218
+ )}
219
+ {(status === 'locked' || status === 'dark') && (
220
+ <Lock
221
+ stroke={status === 'dark' ? 'rgba(255,255,255,0.4)' : '#c3b1a4'}
222
+ strokeWidth={2.2}
223
+ size={isMobileScreen ? scale(20) : scale(24)}
224
+ />
225
+ )}
226
+ </View>
227
+ )}
228
+ </View>
229
+
230
+ {progress !== undefined && segments !== undefined && (
231
+ <View style={[styles.progressContainer, { marginTop: isMobileScreen ? scale(12) : scale(16) }]}>
232
+ <ProgressBar
233
+ progress={progress}
234
+ segments={segments}
235
+ size={isMobileScreen ? 'xs' : 'xs'}
236
+ />
237
+ </View>
238
+ )}
239
+ </Pressable>
240
+ );
241
+ };
242
+
243
+ const styles = StyleSheet.create({
244
+ container: {
245
+ width: '100%',
246
+ maxWidth: 450,
247
+ alignSelf: 'center',
248
+ flexDirection: 'column',
249
+ paddingVertical: 14,
250
+ paddingHorizontal: 15,
251
+ borderRadius: 18,
252
+ borderStyle: 'solid',
253
+ },
254
+ topRow: {
255
+ flexDirection: 'row',
256
+ alignItems: 'center',
257
+ width: '100%',
258
+ },
259
+ leftCol: {
260
+ flexShrink: 0,
261
+ justifyContent: 'center',
262
+ alignItems: 'center',
263
+ },
264
+ content: {
265
+ flex: 1,
266
+ flexShrink: 1,
267
+ flexBasis: 0,
268
+ minWidth: 0,
269
+ justifyContent: 'center',
270
+ marginLeft: 12,
271
+ },
272
+ darkIconBox: {
273
+ width: 44,
274
+ height: 44,
275
+ borderRadius: 12,
276
+ alignItems: 'center',
277
+ justifyContent: 'center',
278
+ borderWidth: 2,
279
+ borderColor: theme.colors.primary,
280
+ borderStyle: 'dashed' as const,
281
+ },
282
+ title: {
283
+
284
+ fontSize: 16,
285
+ marginBottom: 4,
286
+ },
287
+ subtitle: {
288
+
289
+ fontSize: 12,
290
+ },
291
+ subtext: {
292
+ color: theme.colors.brown[400],
293
+ },
294
+ progressContainer: {
295
+ marginTop: 10,
296
+ width: '100%',
297
+ },
298
+ rightContent: {
299
+ justifyContent: 'center',
300
+ alignItems: 'flex-end',
301
+ marginLeft: 12,
302
+ flexShrink: 0,
303
+ },
304
+ });
@@ -0,0 +1,53 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { View } from 'react-native';
3
+ import { ModuleCompletionCard } from './ModuleCompletionCard';
4
+
5
+ const meta = {
6
+ title: 'Components/Cards/ModuleCompletionCard',
7
+ component: ModuleCompletionCard,
8
+ parameters: {
9
+ layout: 'padded',
10
+ docs: {
11
+ description: {
12
+ component: `O **ModuleCompletionCard** funciona como um "passaporte carimbado" exibido ao utilizador sempre que ele conclui um módulo.
13
+ A componente suporta a injeção da bandeira usando a nossa \`Flag\` através do \`countryCode\`, textos dinâmicos e um carimbo de "VALIDÉ" rotacionado com estilo autêntico de passaporte.
14
+
15
+ ### Exemplo de Uso
16
+ \`\`\`tsx
17
+ <ModuleCompletionCard
18
+ countryCode="SN"
19
+ title="Sénégal"
20
+ subtitle="Niveau A1 · complet"
21
+ showStamp={true}
22
+ stampTitle="VALIDÉ"
23
+ stampSubtitle="DAKAR · A1"
24
+ />
25
+ \`\`\`
26
+ `
27
+ }
28
+ }
29
+ },
30
+ tags: ['autodocs'],
31
+ decorators: [
32
+ (Story) => (
33
+ <View style={{ padding: 0, alignItems: 'center', justifyContent: 'center', flex: 1 }}>
34
+ <Story />
35
+ </View>
36
+ ),
37
+ ],
38
+ } satisfies Meta<any>;
39
+
40
+ export default meta;
41
+ type Story = StoryObj<any>;
42
+
43
+ export const BoardingPassStamp: Story = {
44
+ name: "Cartão de Passaporte Carimbado",
45
+ args: {
46
+ countryCode: 'SN',
47
+ title: 'Sénégal',
48
+ subtitle: 'Niveau A1 · complet',
49
+ showStamp: true,
50
+ stampTitle: 'VALIDÉ',
51
+ stampSubtitle: 'DAKAR · A1',
52
+ },
53
+ };
@@ -0,0 +1,167 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { View, StyleSheet, Animated } from 'react-native';
3
+ import type { ViewStyle } from 'react-native';
4
+ import { theme } from '../../../styles/theme';
5
+ import { useResponsive } from '../../../hooks/useResponsive';
6
+ import { Flag } from '../../DataDisplay/Flag/Flag';
7
+ import { Typography } from '../../Typography/Typography';
8
+
9
+ export interface ModuleCompletionCardProps {
10
+ /** Código ISO ou nome do país para renderizar a bandeira (ex: 'SN' ou 'Sénégal') */
11
+ countryCode: string;
12
+ /** Nome do país ou cidade */
13
+ title: string;
14
+ /** Nível e estado (ex: "Niveau A1 · complet") */
15
+ subtitle: string;
16
+ /** Se true, o carimbo é exibido */
17
+ showStamp?: boolean;
18
+ /** Título do carimbo (ex: "VALIDÉ") */
19
+ stampTitle?: string;
20
+ /** Subtítulo do carimbo (ex: "DAKAR · A1") */
21
+ stampSubtitle?: string;
22
+ /** Estilos customizados */
23
+ style?: ViewStyle | ViewStyle[];
24
+ }
25
+
26
+ export const ModuleCompletionCard = ({
27
+ countryCode,
28
+ title,
29
+ subtitle,
30
+ showStamp = true,
31
+ stampTitle = 'VALIDÉ',
32
+ stampSubtitle,
33
+ style,
34
+ }: ModuleCompletionCardProps) => {
35
+ const { scale, scaleText } = useResponsive();
36
+
37
+ const stampScaleAnim = useRef(new Animated.Value(3)).current;
38
+ const stampOpacityAnim = useRef(new Animated.Value(0)).current;
39
+
40
+ useEffect(() => {
41
+ if (showStamp) {
42
+ Animated.parallel([
43
+ Animated.spring(stampScaleAnim, {
44
+ toValue: 1,
45
+ friction: 6,
46
+ tension: 150,
47
+ useNativeDriver: true,
48
+ }),
49
+ Animated.timing(stampOpacityAnim, {
50
+ toValue: 1,
51
+ duration: 250,
52
+ useNativeDriver: true,
53
+ }),
54
+ ]).start();
55
+ } else {
56
+ // Reset instantâneo se for escondido
57
+ stampScaleAnim.setValue(3);
58
+ stampOpacityAnim.setValue(0);
59
+ }
60
+ }, [showStamp, stampScaleAnim, stampOpacityAnim]);
61
+
62
+ return (
63
+ <View style={[styles.container, { paddingTop: scale(56), paddingBottom: scale(64), paddingHorizontal: scale(24), borderRadius: scale(24) }, style]}>
64
+ {/* Container da Bandeira */}
65
+ <View style={[styles.flagWrapper, { width: scale(84), height: scale(84), borderRadius: scale(42), borderWidth: scale(3), marginBottom: scale(16) }]}>
66
+ <View style={[styles.flagInner, { borderRadius: scale(40) }]}>
67
+ <Flag countryCode={countryCode} size={scale(84)} />
68
+ </View>
69
+ </View>
70
+
71
+ {/* Título e Subtítulo */}
72
+ <Typography style={[styles.title, { fontSize: scaleText(24), lineHeight: scaleText(28), marginBottom: scale(4) }]}>
73
+ {title}
74
+ </Typography>
75
+ <Typography style={[styles.subtitle, { fontSize: scaleText(13) }]}>
76
+ {subtitle}
77
+ </Typography>
78
+
79
+ {/* Carimbo */}
80
+ {showStamp && (
81
+ <Animated.View style={[
82
+ styles.stampContainer,
83
+ {
84
+ right: scale(18),
85
+ bottom: scale(25),
86
+ borderWidth: scale(2.5),
87
+ borderRadius: scale(8),
88
+ paddingVertical: scale(6),
89
+ paddingHorizontal: scale(12),
90
+ opacity: stampOpacityAnim,
91
+ transform: [
92
+ { rotate: '-15deg' },
93
+ { scale: stampScaleAnim },
94
+ ]
95
+ }
96
+ ]}>
97
+ <Typography style={[styles.stampTitle, { fontSize: scaleText(18), lineHeight: scaleText(20) }]}>
98
+ {stampTitle}
99
+ </Typography>
100
+ {stampSubtitle && (
101
+ <Typography style={[styles.stampSubtitle, { fontSize: scaleText(10) }]}>
102
+ {stampSubtitle}
103
+ </Typography>
104
+ )}
105
+ </Animated.View>
106
+ )}
107
+ </View>
108
+ );
109
+ };
110
+
111
+ const styles = StyleSheet.create({
112
+ container: {
113
+ backgroundColor: '#ffffff',
114
+ alignItems: 'center',
115
+ justifyContent: 'center',
116
+ width: '100%',
117
+ maxWidth: 280,
118
+ maxHeight: 380,
119
+ alignSelf: 'center',
120
+ position: 'relative',
121
+ overflow: 'visible',
122
+ ...theme.shadows.lg, // Stronger shadow for the green background contrast
123
+ },
124
+ flagWrapper: {
125
+ borderColor: '#e8883e', // Orange border a bit darker/thinner
126
+ alignItems: 'center',
127
+ justifyContent: 'center',
128
+ overflow: 'hidden',
129
+ },
130
+ flagInner: {
131
+ width: '100%',
132
+ height: '100%',
133
+ overflow: 'hidden',
134
+ alignItems: 'center',
135
+ justifyContent: 'center',
136
+ },
137
+ title: {
138
+
139
+ color: '#3f2620',
140
+ textAlign: 'center',
141
+ },
142
+ subtitle: {
143
+
144
+ color: '#9a8478',
145
+ textAlign: 'center',
146
+ },
147
+ stampContainer: {
148
+ position: 'absolute',
149
+ borderColor: '#d94141', // Red stamp border
150
+ transform: [{ rotate: '-15deg' }],
151
+ alignItems: 'center',
152
+ justifyContent: 'center',
153
+ backgroundColor: 'transparent',
154
+ },
155
+ stampTitle: {
156
+
157
+ color: '#d94141', // Red stamp text
158
+ textTransform: 'uppercase',
159
+ },
160
+ stampSubtitle: {
161
+
162
+ color: '#d94141',
163
+ textTransform: 'uppercase',
164
+ marginTop: 2,
165
+ letterSpacing: 0.5,
166
+ },
167
+ });
@@ -0,0 +1,114 @@
1
+
2
+ import { View, StyleSheet } from 'react-native';
3
+ import Svg, { Circle, Rect, Path, Ellipse, Line } from 'react-native-svg';
4
+ import { theme } from '../../../styles/theme';
5
+ import { Typography } from '../../Typography/Typography';
6
+ import { useResponsive } from '../../../hooks/useResponsive';
7
+
8
+ export const PassportCover = () => {
9
+ const { scale, scaleText } = useResponsive();
10
+
11
+ // Biometric chip logo standard
12
+ const ChipLogo = () => (
13
+ <Svg width={scale(32)} height={scale(24)} viewBox="0 0 32 24" fill="none">
14
+ <Rect x="1" y="1" width="30" height="22" rx="4" stroke={"#c3b1a4"} strokeWidth="1.5" />
15
+ <Rect x="8" y="6" width="16" height="12" rx="2" stroke={"#c3b1a4"} strokeWidth="1.5" />
16
+ <Line x1="1" y1="12" x2="8" y2="12" stroke={"#c3b1a4"} strokeWidth="1.5" />
17
+ <Line x1="24" y1="12" x2="31" y2="12" stroke={"#c3b1a4"} strokeWidth="1.5" />
18
+ </Svg>
19
+ );
20
+
21
+ return (
22
+ <View style={styles.container}>
23
+ <View style={[styles.innerBorder, {
24
+ margin: scale(16),
25
+ padding: scale(24),
26
+ borderWidth: scale(1.5),
27
+ borderRadius: scale(8)
28
+ }]}>
29
+
30
+ <Typography style={[styles.republique, { fontSize: scaleText(12), marginBottom: scale(48) }]}>
31
+ RÉPUBLIQUE DE LA XETWA
32
+ </Typography>
33
+
34
+ {/* Central Logo placeholder - Minimalist Globe */}
35
+ <View style={styles.logoContainer}>
36
+ <Svg width={scale(120)} height={scale(120)} viewBox="0 0 120 120" fill="none">
37
+ {/* Outer ring */}
38
+ <Circle cx="60" cy="60" r="48" stroke={theme.colors.primary} strokeWidth="2.5" />
39
+ {/* Vertical ellipse */}
40
+ <Ellipse cx="60" cy="60" rx="20" ry="48" stroke={theme.colors.primary} strokeWidth="2.5" />
41
+ {/* Horizontal line */}
42
+ <Line x1="12" y1="60" x2="108" y2="60" stroke={theme.colors.primary} strokeWidth="2.5" />
43
+ {/* Shapes / Continents */}
44
+ <Path d="M40 30 Q50 20 60 40 Q40 50 40 30 Z" fill={theme.colors.primary} />
45
+ <Path d="M70 65 Q90 50 85 80 Q60 90 70 65 Z" fill={theme.colors.primary} />
46
+ <Circle cx="80" cy="40" r="4" fill={theme.colors.primary} />
47
+ </Svg>
48
+ </View>
49
+
50
+ <View style={styles.titleContainer}>
51
+ <Typography style={[styles.title, { fontSize: scaleText(32) }]}>
52
+ PASSEPORT
53
+ </Typography>
54
+ <Typography style={[styles.subtitle, { fontSize: scaleText(10), marginTop: scale(8) }]}>
55
+ FRANCOPHONIE · MONDE
56
+ </Typography>
57
+ </View>
58
+
59
+ <View style={[styles.chipContainer, { marginTop: scale(32) }]}>
60
+ <ChipLogo />
61
+ </View>
62
+
63
+ </View>
64
+ </View>
65
+ );
66
+ };
67
+
68
+ const styles = StyleSheet.create({
69
+ container: {
70
+ flex: 1,
71
+ backgroundColor: '#4e3227', // Dark brown from the design
72
+ borderRadius: 16,
73
+ overflow: 'hidden',
74
+ shadowColor: '#301c14',
75
+ shadowOffset: { width: 0, height: 12 },
76
+ shadowOpacity: 0.4,
77
+ shadowRadius: 24,
78
+ elevation: 8,
79
+ },
80
+ innerBorder: {
81
+ flex: 1,
82
+ borderColor: '#7a5b4a',
83
+ alignItems: 'center',
84
+ justifyContent: 'space-between',
85
+ },
86
+ republique: {
87
+
88
+ color: theme.colors.brown[100],
89
+ letterSpacing: 2,
90
+ textAlign: 'center',
91
+ },
92
+ logoContainer: {
93
+ alignItems: 'center',
94
+ justifyContent: 'center',
95
+ flex: 1,
96
+ },
97
+ titleContainer: {
98
+ alignItems: 'center',
99
+ },
100
+ title: {
101
+
102
+ color: theme.colors.brown[100],
103
+ letterSpacing: 1,
104
+ },
105
+ subtitle: {
106
+
107
+ color: '#7a5b4a',
108
+ letterSpacing: 3,
109
+ },
110
+ chipContainer: {
111
+ alignItems: 'center',
112
+ justifyContent: 'flex-end',
113
+ }
114
+ });