@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,217 @@
1
+ import { View, StyleSheet, Platform } from 'react-native';
2
+ import Svg, { Line, Path } from 'react-native-svg';
3
+ import { theme } from '../../../styles/theme';
4
+ import { Typography } from '../../Typography/Typography';
5
+ import { useResponsive } from '../../../hooks/useResponsive';
6
+
7
+ export interface PassportUserData {
8
+ surname: string;
9
+ givenName: string;
10
+ nationality: string;
11
+ level: string;
12
+ birthDate: string;
13
+ birthPlace: string;
14
+ issueDate: string;
15
+ passportNo: string;
16
+ signature: string;
17
+ mrzLine1: string;
18
+ mrzLine2: string;
19
+ }
20
+
21
+ export interface PassportIDPageProps {
22
+ user: PassportUserData;
23
+ }
24
+
25
+ export const PassportIDPage = ({ user }: PassportIDPageProps) => {
26
+ const { scale, scaleText } = useResponsive();
27
+
28
+ // Simple barcode generator for visual effect
29
+ const Barcode = () => (
30
+ <Svg width="100%" height={scale(24)} viewBox="0 0 100 24" preserveAspectRatio="none">
31
+ {Array.from({ length: 30 }).map((_, i) => {
32
+ const x = i * 3.3 + (Math.random() * 1.5);
33
+ const w = 1 + Math.random() * 2;
34
+ return <Line key={i} x1={x} y1="0" x2={x} y2="24" stroke={theme.colors.brown[300]} strokeWidth={w} />;
35
+ })}
36
+ </Svg>
37
+ );
38
+
39
+ return (
40
+ <View style={styles.container}>
41
+ {/* Dark spine/binder on the left */}
42
+ <View style={[styles.spine, { width: scale(32) }]} />
43
+
44
+ {/* Main page content */}
45
+ <View style={styles.pageContent}>
46
+ {/* Header */}
47
+ <View style={[styles.header, { padding: scale(12) }]}>
48
+ <Typography style={[styles.headerTitle, { fontSize: scaleText(14) }]}>PASSEPORT</Typography>
49
+ <Typography style={[styles.headerTitle, { fontSize: scaleText(10) }]}>XTW · TYPE P</Typography>
50
+ </View>
51
+
52
+ <View style={[styles.body, { padding: scale(16) }]}>
53
+
54
+ <View style={[styles.row, { gap: scale(16) }]}>
55
+ {/* Left Column: Photo & Barcode */}
56
+ <View style={[styles.photoColumn, { width: scale(100) }]}>
57
+ <View style={[styles.photoPlaceholder, { height: scale(120), borderRadius: scale(8), marginBottom: scale(8) }]}>
58
+ {/* Smiley face SVG */}
59
+ <Svg width={scale(60)} height={scale(60)} viewBox="0 0 60 60" fill="none">
60
+ <Path d="M30 60C46.5685 60 60 46.5685 60 30C60 13.4315 46.5685 0 30 0C13.4315 0 0 13.4315 0 30C0 46.5685 13.4315 60 30 60Z" fill={theme.colors.brown[800]} />
61
+ <Path d="M16 24C18.2091 24 20 22.2091 20 20C20 17.7909 18.2091 16 16 16C13.7909 16 12 17.7909 12 20C12 22.2091 13.7909 24 16 24Z" fill="white" />
62
+ <Path d="M44 24C46.2091 24 48 22.2091 48 20C48 17.7909 46.2091 16 44 16C41.7909 16 40 17.7909 40 20C40 22.2091 41.7909 24 44 24Z" fill="white" />
63
+ <Path d="M14 36C14 36 22 46 30 46C38 46 46 36 46 36L46 42C46 42 38 52 30 52C22 52 14 42 14 42L14 36Z" fill={theme.colors.brown[100]} />
64
+ </Svg>
65
+ </View>
66
+ <Barcode />
67
+ </View>
68
+
69
+ {/* Right Column: User Data */}
70
+ <View style={styles.dataColumn}>
71
+ <View style={{ marginBottom: scale(12) }}>
72
+ <Typography style={[styles.label, { fontSize: scaleText(9) }]}>NOM / SURNAME</Typography>
73
+ <Typography style={[styles.value, { fontSize: scaleText(16) }]}>{user.surname}</Typography>
74
+ </View>
75
+
76
+ <View style={{ marginBottom: scale(12) }}>
77
+ <Typography style={[styles.label, { fontSize: scaleText(9) }]}>PRÉNOM / GIVEN NAME</Typography>
78
+ <Typography style={[styles.value, { fontSize: scaleText(16) }]}>{user.givenName}</Typography>
79
+ </View>
80
+
81
+ <View style={[styles.row, { justifyContent: 'space-between' }]}>
82
+ <View>
83
+ <Typography style={[styles.label, { fontSize: scaleText(9) }]}>NATIONALITÉ</Typography>
84
+ <Typography style={[styles.value, { fontSize: scaleText(12) }]}>{user.nationality}</Typography>
85
+ </View>
86
+ <View>
87
+ <Typography style={[styles.label, { fontSize: scaleText(9) }]}>NIVEAU</Typography>
88
+ <Typography style={[styles.value, { fontSize: scaleText(12), color: theme.colors.primary }]}>{user.level}</Typography>
89
+ </View>
90
+ </View>
91
+ </View>
92
+ </View>
93
+
94
+ {/* Lower Data Row */}
95
+ <View style={[styles.row, { marginTop: scale(24), justifyContent: 'space-between' }]}>
96
+ <View>
97
+ <Typography style={[styles.label, { fontSize: scaleText(9) }]}>DATE DE NAISS.</Typography>
98
+ <Typography style={[styles.value, { fontSize: scaleText(12) }]}>{user.birthDate}</Typography>
99
+ </View>
100
+ <View>
101
+ <Typography style={[styles.label, { fontSize: scaleText(9) }]}>LIEU</Typography>
102
+ <Typography style={[styles.value, { fontSize: scaleText(12) }]}>{user.birthPlace}</Typography>
103
+ </View>
104
+ <View>
105
+ <Typography style={[styles.label, { fontSize: scaleText(9) }]}>ÉMIS LE</Typography>
106
+ <Typography style={[styles.value, { fontSize: scaleText(12) }]}>{user.issueDate}</Typography>
107
+ </View>
108
+ </View>
109
+
110
+ {/* Passport Number and Signature */}
111
+ <View style={[styles.row, { marginTop: scale(24), alignItems: 'flex-end', justifyContent: 'space-between' }]}>
112
+ <View>
113
+ <Typography style={[styles.label, { fontSize: scaleText(9) }]}>PASSEPORT N°</Typography>
114
+ <Typography style={[styles.value, { fontSize: scaleText(18), color: theme.colors.primary }]}>{user.passportNo}</Typography>
115
+ </View>
116
+ <View>
117
+ <Typography style={[styles.signature, { fontSize: scaleText(24) }]}>{user.signature}</Typography>
118
+ </View>
119
+ </View>
120
+ </View>
121
+
122
+ {/* MRZ Zone at the bottom */}
123
+ <View style={[styles.mrzContainer, { padding: scale(16), paddingTop: scale(12) }]}>
124
+ <Typography style={[styles.mrzText, { fontSize: scaleText(11) }]}>{user.mrzLine1}</Typography>
125
+ <Typography style={[styles.mrzText, { fontSize: scaleText(11) }]}>{user.mrzLine2}</Typography>
126
+ </View>
127
+
128
+ </View>
129
+ </View>
130
+ );
131
+ };
132
+
133
+ const styles = StyleSheet.create({
134
+ container: {
135
+ flex: 1,
136
+ flexDirection: 'row',
137
+ backgroundColor: '#efe2d6', // Cream page background
138
+ borderRadius: 16,
139
+ overflow: 'hidden',
140
+ shadowColor: '#51332d',
141
+ shadowOffset: { width: 0, height: 12 },
142
+ shadowOpacity: 0.15,
143
+ shadowRadius: 24,
144
+ elevation: 8,
145
+ },
146
+ spine: {
147
+ backgroundColor: '#4e3227', // Dark brown binding
148
+ height: '100%',
149
+ borderRightWidth: 1,
150
+ borderRightColor: 'rgba(0,0,0,0.2)',
151
+ },
152
+ pageContent: {
153
+ flex: 1,
154
+ flexDirection: 'column',
155
+ },
156
+ header: {
157
+ flexDirection: 'row',
158
+ justifyContent: 'space-between',
159
+ alignItems: 'center',
160
+ backgroundColor: '#4e3227', // Dark brown header
161
+ borderTopRightRadius: 16,
162
+ },
163
+ headerTitle: {
164
+
165
+ color: theme.colors.brown[100],
166
+ letterSpacing: 1,
167
+ },
168
+ body: {
169
+ flex: 1,
170
+ },
171
+ row: {
172
+ flexDirection: 'row',
173
+ },
174
+ photoColumn: {
175
+ flexDirection: 'column',
176
+ },
177
+ photoPlaceholder: {
178
+ backgroundColor: theme.colors.primary, // Orange background
179
+ alignItems: 'center',
180
+ justifyContent: 'center',
181
+ },
182
+ dataColumn: {
183
+ flex: 1,
184
+ flexDirection: 'column',
185
+ justifyContent: 'space-between',
186
+ paddingVertical: 4,
187
+ },
188
+ label: {
189
+
190
+ color: '#a48b7b', // Soft brown
191
+ letterSpacing: 0.5,
192
+ textTransform: 'uppercase',
193
+ },
194
+ value: {
195
+
196
+ color: theme.colors.brown[800],
197
+ textTransform: 'uppercase',
198
+ },
199
+ signature: {
200
+
201
+ color: theme.colors.brown[800],
202
+ ...Platform.select({
203
+ web: { fontStyle: 'italic' },
204
+ default: {},
205
+ }),
206
+ },
207
+ mrzContainer: {
208
+ backgroundColor: '#e6d6c8', // Slightly darker cream for MRZ background
209
+ borderBottomRightRadius: 16,
210
+ },
211
+ mrzText: {
212
+
213
+ color: theme.colors.brown[600],
214
+ letterSpacing: 1.5,
215
+ lineHeight: 18,
216
+ }
217
+ });
@@ -0,0 +1,253 @@
1
+
2
+ import { View, StyleSheet } from 'react-native';
3
+ import Svg, { Circle, Text as SvgText, Path, Defs, TextPath, G } from 'react-native-svg';
4
+ import { theme } from '../../../styles/theme';
5
+ import { Typography } from '../../Typography/Typography';
6
+ import { useResponsive } from '../../../hooks/useResponsive';
7
+ import { Flag } from '../../DataDisplay/Flag/Flag';
8
+ import { Check } from 'lucide-react-native';
9
+
10
+ export interface PassportModule {
11
+ id: string;
12
+ title: string;
13
+ }
14
+
15
+ export interface PassportStampPageProps {
16
+ countryCode: string;
17
+ countryName: string;
18
+ capital: string;
19
+ level: string;
20
+ date: string;
21
+ modules: PassportModule[];
22
+ // Allows rotating the stamp slightly for realism
23
+ stampRotation?: number;
24
+ }
25
+
26
+ export const PassportStampPage = ({
27
+ countryCode,
28
+ countryName,
29
+ capital,
30
+ level,
31
+ date,
32
+ modules,
33
+ stampRotation = -5 // Default slight rotation
34
+ }: PassportStampPageProps) => {
35
+ const { scale, scaleText } = useResponsive();
36
+
37
+ // Background pattern for passport page (faint horizontal lines)
38
+ const BackgroundPattern = () => (
39
+ <View style={StyleSheet.absoluteFill} pointerEvents="none">
40
+ <Svg width="100%" height="100%">
41
+ {Array.from({ length: 40 }).map((_, i) => (
42
+ <Path
43
+ key={i}
44
+ d={`M0 ${i * scale(14)} Q 150 ${i * scale(14) - 2} 300 ${i * scale(14)}`}
45
+ stroke="rgba(164, 139, 123, 0.15)"
46
+ strokeWidth="1"
47
+ fill="none"
48
+ />
49
+ ))}
50
+ </Svg>
51
+ </View>
52
+ );
53
+
54
+ // Circular stamp SVG
55
+ const Stamp = () => (
56
+ <View style={{ width: scale(80), height: scale(80), alignItems: 'center', justifyContent: 'center' }}>
57
+ <Svg width="100%" height="100%" viewBox="0 0 160 160" style={{ transform: [{ rotate: `${stampRotation}deg` }] }}>
58
+ <Defs>
59
+ <Path id="topCurve" d="M 30,80 A 50,50 0 0,1 130,80" fill="none" />
60
+ <Path id="bottomCurve" d="M 130,80 A 50,50 0 0,1 30,80" fill="none" />
61
+ </Defs>
62
+
63
+ {/* Outer Rings */}
64
+ <Circle cx="80" cy="80" r="70" stroke={theme.colors.brown[500]} strokeWidth="2" fill="none" />
65
+ <Circle cx="80" cy="80" r="62" stroke={theme.colors.brown[500]} strokeWidth="1" fill="none" />
66
+
67
+ {/* Curved Text - Top */}
68
+ <SvgText fill={theme.colors.brown[600]} fontSize="12" fontWeight="800" letterSpacing="2">
69
+ <TextPath href="#topCurve" startOffset="50%" textAnchor="middle">
70
+ RÉPUBLIQUE DE LA XETWA
71
+ </TextPath>
72
+ </SvgText>
73
+
74
+ {/* Curved Text - Bottom */}
75
+ <SvgText fill={theme.colors.brown[600]} fontSize="12" fontWeight="800" letterSpacing="1">
76
+ <TextPath href="#bottomCurve" startOffset="50%" textAnchor="middle">
77
+ * {date.toUpperCase()} *
78
+ </TextPath>
79
+ </SvgText>
80
+
81
+ {/* Inner Elements */}
82
+ <G x="80" y="80">
83
+ {/* City Name */}
84
+ <SvgText fill={theme.colors.brown[800]} fontSize="18" fontWeight="900" textAnchor="middle" y="-10">
85
+ {capital}
86
+ </SvgText>
87
+ {/* Validation text */}
88
+ <SvgText fill={theme.colors.brown[600]} fontSize="10" fontWeight="800" textAnchor="middle" y="10" letterSpacing="1">
89
+ VALIDÉ · {level}
90
+ </SvgText>
91
+ {/* Arrow */}
92
+ <Path d="M -15,18 L 15,18 M 10,14 L 16,18 L 10,22" stroke={theme.colors.brown[600]} strokeWidth="1.5" fill="none" />
93
+ </G>
94
+ </Svg>
95
+ </View>
96
+ );
97
+
98
+ return (
99
+ <View style={styles.container}>
100
+ <BackgroundPattern />
101
+
102
+ {/* Spine on the left */}
103
+ <View style={[styles.spine, { width: scale(32) }]} />
104
+
105
+ <View style={[styles.pageContent, { padding: scale(16) }]}>
106
+
107
+ {/* Header: Flag and Country Info */}
108
+ <View style={[styles.header, { paddingHorizontal: scale(16), paddingTop: scale(12), marginBottom: scale(8) }]}>
109
+ <View style={[styles.flagContainer, { width: scale(40), height: scale(40), borderRadius: scale(20), marginRight: scale(12) }]}>
110
+ <Flag countryCode={countryCode} size={scale(40)} />
111
+ </View>
112
+ <View style={styles.titleGroup}>
113
+ <Typography style={[styles.countryName, { fontSize: scaleText(20) }]}>{countryName}</Typography>
114
+ <Typography style={[styles.subtitle, { fontSize: scaleText(12) }]}>{capital} · Niveau {level}</Typography>
115
+ </View>
116
+ </View>
117
+
118
+ {/* Central Stamp Area */}
119
+ <View style={[styles.stampContainer, { marginVertical: scale(8) }]}>
120
+ <Stamp />
121
+ </View>
122
+
123
+ {/* Modules Section Header */}
124
+ <View style={[styles.modulesHeader, { marginBottom: scale(8) }]}>
125
+ <Typography style={[styles.modulesTitle, { fontSize: scaleText(10) }]}>MODULES</Typography>
126
+ <View style={[styles.badge, { paddingHorizontal: scale(8), paddingVertical: scale(4), borderRadius: scale(4) }]}>
127
+ <Typography style={[styles.badgeText, { fontSize: scaleText(9) }]}>PAÍS CARIMBADO</Typography>
128
+ </View>
129
+ </View>
130
+
131
+ {/* Separator line */}
132
+ <View style={[styles.separator, { marginBottom: scale(12) }]} />
133
+
134
+ {/* Modules Checklist */}
135
+ <View style={[styles.checklist, { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' }]}>
136
+ <Typography style={{ color: 'red' }}>Debug: {JSON.stringify(modules)}</Typography>
137
+ {modules.map((mod) => (
138
+ <View key={mod.id} style={[styles.checkItem, { width: '48%', marginBottom: scale(8) }]}>
139
+ <View style={[styles.checkCircle, { width: scale(16), height: scale(16), borderRadius: scale(8) }]}>
140
+ <Check size={scale(10)} color="#fff" strokeWidth={3} />
141
+ </View>
142
+ <Typography style={[styles.moduleText, { fontSize: scaleText(11), marginLeft: scale(8) }]}>
143
+ {mod.title}
144
+ </Typography>
145
+ </View>
146
+ ))}
147
+ </View>
148
+
149
+ </View>
150
+ </View>
151
+ );
152
+ };
153
+
154
+ const styles = StyleSheet.create({
155
+ container: {
156
+ flex: 1,
157
+ flexDirection: 'row',
158
+ backgroundColor: '#f6eedf', // Slightly lighter cream than ID page
159
+ borderRadius: 16,
160
+ overflow: 'hidden',
161
+ shadowColor: '#51332d',
162
+ shadowOffset: { width: 0, height: 12 },
163
+ shadowOpacity: 0.15,
164
+ shadowRadius: 24,
165
+ elevation: 8,
166
+ },
167
+ spine: {
168
+ backgroundColor: '#efe2d6',
169
+ borderRightWidth: 1,
170
+ borderRightColor: 'rgba(0,0,0,0.05)',
171
+ shadowColor: '#000',
172
+ shadowOffset: { width: 2, height: 0 },
173
+ shadowOpacity: 0.05,
174
+ shadowRadius: 4,
175
+ zIndex: 2,
176
+ },
177
+ pageContent: {
178
+ flex: 1,
179
+ zIndex: 1,
180
+ },
181
+ header: {
182
+ flexDirection: 'row',
183
+ alignItems: 'center',
184
+ },
185
+ flagContainer: {
186
+ backgroundColor: '#efe2d6',
187
+ alignItems: 'center',
188
+ justifyContent: 'center',
189
+ shadowColor: '#000',
190
+ shadowOffset: { width: 0, height: 2 },
191
+ shadowOpacity: 0.1,
192
+ shadowRadius: 4,
193
+ elevation: 2,
194
+ },
195
+ titleGroup: {
196
+ flex: 1,
197
+ },
198
+ countryName: {
199
+
200
+ color: theme.colors.brown[800],
201
+ },
202
+ subtitle: {
203
+
204
+ color: theme.colors.brown[500],
205
+ },
206
+ stampContainer: {
207
+ alignItems: 'center',
208
+ justifyContent: 'center',
209
+ },
210
+ modulesHeader: {
211
+ flexDirection: 'row',
212
+ alignItems: 'center',
213
+ justifyContent: 'space-between',
214
+ },
215
+ modulesTitle: {
216
+
217
+ color: theme.colors.brown[400],
218
+ letterSpacing: 1,
219
+ },
220
+ badge: {
221
+ backgroundColor: 'rgba(15, 142, 102, 0.1)', // Light green bg
222
+ borderWidth: 1,
223
+ borderColor: 'rgba(15, 142, 102, 0.3)',
224
+ },
225
+ badgeText: {
226
+
227
+ color: theme.colors.success,
228
+ letterSpacing: 0.5,
229
+ },
230
+ separator: {
231
+ height: 1,
232
+ borderStyle: 'dashed',
233
+ borderWidth: 1,
234
+ borderColor: "#c3b1a4",
235
+ borderRadius: 1,
236
+ },
237
+ checklist: {
238
+ width: '100%',
239
+ },
240
+ checkItem: {
241
+ flexDirection: 'row',
242
+ alignItems: 'center',
243
+ },
244
+ checkCircle: {
245
+ backgroundColor: theme.colors.success,
246
+ alignItems: 'center',
247
+ justifyContent: 'center',
248
+ },
249
+ moduleText: {
250
+ color: theme.colors.brown[700],
251
+ flexShrink: 1,
252
+ }
253
+ });
@@ -0,0 +1,95 @@
1
+
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { View } from 'react-native';
4
+ import { PassportViewer } from './PassportViewer';
5
+ import type { PassportUserData } from './PassportIDPage';
6
+ import type { PassportStampPageProps } from './PassportStampPage';
7
+
8
+ const meta = {
9
+ title: 'Cards/Passport',
10
+ component: PassportViewer,
11
+ parameters: {
12
+ layout: 'fullscreen',
13
+ docs: {
14
+ description: {
15
+ component: 'O **PassportViewer** é uma caderneta interativa que guarda o progresso global do utilizador. Ele suporta _swipe gestures_ nativas e animações 3D de dobragem para simular um passaporte real. Contém a página de Identidade e múltiplas páginas de carimbos dos países.',
16
+ },
17
+ },
18
+ },
19
+ tags: ['autodocs'],
20
+ decorators: [
21
+ (Story) => (
22
+ <View style={{ flex: 1, height: '100%', width: '100%', maxWidth: 600, marginHorizontal: 'auto', overflow: 'hidden' }}>
23
+ <Story />
24
+ </View>
25
+ ),
26
+ ],
27
+ } satisfies Meta<any>;
28
+
29
+ export default meta;
30
+ type Story = StoryObj<any>;
31
+
32
+ const mockUser: PassportUserData = {
33
+ surname: 'MARQUES',
34
+ givenName: 'Sofia',
35
+ nationality: 'FRANCOPHONE',
36
+ level: 'B2',
37
+ birthDate: '14 MARS 2001',
38
+ birthPlace: 'LISBONNE',
39
+ issueDate: '02 JUIN 2026',
40
+ passportNo: 'XT 042 6726',
41
+ signature: 'Sofia M.',
42
+ mrzLine1: 'P<XTWMARQUES<<SOFIA<<<<<<<<<<<<',
43
+ mrzLine2: 'XT0426726<9XTW0103148F<<<<<<<<<02'
44
+ };
45
+
46
+ const mockCountries: Omit<PassportStampPageProps, 'stampRotation'>[] = [
47
+ {
48
+ countryName: 'France',
49
+ capital: 'Paris',
50
+ level: 'A1',
51
+ date: 'MAI 2026',
52
+ countryCode: 'FR',
53
+ modules: [
54
+ { id: '1', title: 'Salutations & présentations' },
55
+ { id: '2', title: "Les nombres et l'heure" },
56
+ { id: '3', title: 'Au café' },
57
+ { id: '4', title: 'Demander son chemin' },
58
+ { id: '5', title: 'Ma famille' },
59
+ ]
60
+ },
61
+ {
62
+ countryName: 'Belgique',
63
+ capital: 'Bruxelles',
64
+ level: 'A2',
65
+ date: 'JUIN 2026',
66
+ countryCode: 'BE',
67
+ modules: [
68
+ { id: '1', title: 'Ma routine' },
69
+ { id: '2', title: 'Les courses & vêtements' },
70
+ { id: '3', title: 'Le passé composé' },
71
+ { id: '4', title: 'La santé' },
72
+ ]
73
+ },
74
+ {
75
+ countryName: 'Suisse',
76
+ capital: 'Genève',
77
+ level: 'A2',
78
+ date: 'JUIN 2026',
79
+ countryCode: 'CH',
80
+ modules: [
81
+ { id: '1', title: 'La maison & la ville' },
82
+ { id: '2', title: 'Le temps libre' },
83
+ { id: '3', title: 'Les projets' },
84
+ { id: '4', title: 'Septante & nonante' },
85
+ ]
86
+ }
87
+ ];
88
+
89
+ export const Default: Story = {
90
+ args: {
91
+ user: mockUser,
92
+ countries: mockCountries,
93
+ onBack: () => console.log('Back pressed')
94
+ },
95
+ };