@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,317 @@
1
+ import { View, StyleSheet } from 'react-native';
2
+ import type { ViewStyle } from 'react-native';
3
+ import { theme } from '../../../styles/theme';
4
+ import { BaseCard } from '../BaseCard/BaseCard';
5
+ import { Check, X } from 'lucide-react-native';
6
+ import { useResponsive } from '../../../hooks/useResponsive';
7
+ import { Typography } from '../../Typography/Typography';
8
+
9
+ export type PlanCardTier = 'decouverte' | 'explorateur' | 'fluent';
10
+
11
+ /**
12
+ * Propriedades para configurar o cartão de plano de assinatura.
13
+ */
14
+ export interface PlanCardProps {
15
+ /** Categoria do plano. */
16
+ tier: PlanCardTier;
17
+ /** Rótulo customizado do nível (e.g. "DÉCOUVERTE"). */
18
+ tierLabel?: string;
19
+ /** Nome do plano. */
20
+ planName: string;
21
+ /** Valor a ser exibido. */
22
+ priceText: string;
23
+ /** Período do preço (e.g. "/mês"). */
24
+ pricePeriod?: string;
25
+ /** Lista de itens incluídos no plano. */
26
+ includedItems?: string[];
27
+ /** Lista de itens não incluídos no plano. */
28
+ excludedItems?: string[];
29
+ /** Define se o cartão deve exibir o badge de "mais popular". */
30
+ isPopular?: boolean;
31
+ /** Estilização customizada do container. */
32
+ style?: ViewStyle | ViewStyle[];
33
+ /** Variante de layout do cartão (completo ou miniatura). */
34
+ variant?: 'default' | 'mini';
35
+ }
36
+
37
+ export const PlanCard = ({
38
+ tier,
39
+ tierLabel,
40
+ planName,
41
+ priceText,
42
+ pricePeriod = '/mês',
43
+ includedItems = [],
44
+ excludedItems = [],
45
+ isPopular = false,
46
+ style,
47
+ variant = 'default',
48
+ }: PlanCardProps) => {
49
+ const { scale, scaleText } = useResponsive();
50
+ const isMini = variant === 'mini';
51
+
52
+ // Determine container config based on plan tier
53
+ const getContainerConfig = () => {
54
+ switch (tier) {
55
+ case 'decouverte':
56
+ return {
57
+ borderWidth: 1.5,
58
+ borderColor: '#efe2d6',
59
+ backgroundColor: '#ffffff',
60
+ borderRadius: scale(18),
61
+ paddingVertical: scale(13),
62
+ paddingHorizontal: scale(14),
63
+ shadow: 'none' as const,
64
+ overflow: 'visible' as const,
65
+ flatShadow: false,
66
+ };
67
+ case 'explorateur':
68
+ return {
69
+ borderWidth: 2,
70
+ borderColor: '#f2974b',
71
+ backgroundColor: '#ffffff',
72
+ borderRadius: scale(18),
73
+ paddingVertical: scale(14),
74
+ paddingHorizontal: scale(15),
75
+ shadow: 'sm' as const,
76
+ overflow: 'visible' as const,
77
+ flatShadow: false,
78
+ };
79
+ case 'fluent':
80
+ return {
81
+ borderWidth: 0,
82
+ borderColor: 'transparent',
83
+ backgroundColor: '#51332d',
84
+ borderRadius: scale(18),
85
+ paddingVertical: scale(13),
86
+ paddingHorizontal: scale(14),
87
+ shadow: 'none' as const,
88
+ overflow: 'visible' as const,
89
+ flatShadow: true, // Uses border bottom for flat 3D shadow
90
+ };
91
+ }
92
+ };
93
+
94
+ const config = getContainerConfig();
95
+
96
+ // Resolve dynamic colors based on tier
97
+ const getDynamicColors = () => {
98
+ const isDark = tier === 'fluent';
99
+ if (isDark) {
100
+ return {
101
+ labelColor: '#f2974b',
102
+ titleColor: '#ffffff',
103
+ priceColor: '#fde9d5',
104
+ periodColor: '#cbb6a4',
105
+ itemTextColor: '#fde9d5',
106
+ checkColor: '#f2974b',
107
+ crossColor: '#c3b1a4',
108
+ };
109
+ }
110
+ return {
111
+ labelColor: tier === 'explorateur' ? '#cf7a30' : '#9a8478',
112
+ titleColor: '#51332d',
113
+ priceColor: tier === 'explorateur' ? '#f2974b' : '#51332d',
114
+ periodColor: '#9a8478',
115
+ itemTextColor: '#51332d',
116
+ checkColor: '#16915a',
117
+ crossColor: '#c3b1a4',
118
+ };
119
+ };
120
+
121
+ const colors = getDynamicColors();
122
+
123
+ return (
124
+ <View style={[styles.cardWrapper, { paddingTop: scale(10) }]}>
125
+ <BaseCard
126
+ borderWidth={config.borderWidth}
127
+ borderColor={config.borderColor}
128
+ backgroundColor={config.backgroundColor}
129
+ borderRadius={config.borderRadius}
130
+ paddingVertical={config.paddingVertical}
131
+ paddingHorizontal={config.paddingHorizontal}
132
+ shadow={config.shadow}
133
+ overflow={config.overflow}
134
+ style={[
135
+ config.flatShadow ? styles.fluentFlatShadow : null,
136
+ ...(Array.isArray(style) ? style : [style].filter(Boolean)),
137
+ ] as ViewStyle[]}
138
+ >
139
+ {/* Floating popular badge */}
140
+ {isPopular && (
141
+ <View style={[
142
+ styles.popularBadge,
143
+ {
144
+ top: scale(-10),
145
+ paddingVertical: scale(4),
146
+ paddingHorizontal: scale(12),
147
+ borderRadius: scale(20),
148
+ }
149
+ ]}>
150
+ <Typography style={[styles.popularBadgeText, { fontSize: scaleText(9), letterSpacing: scale(0.8) }]}>MAIS POPULAR</Typography>
151
+ </View>
152
+ )}
153
+
154
+ {isMini ? (
155
+ <View style={[styles.miniHeader, { marginTop: scale(8), marginBottom: scale(4) }]}>
156
+ <View style={styles.miniLeft}>
157
+ <Typography style={[styles.labelTier, { color: colors.labelColor, fontSize: scaleText(10), letterSpacing: scale(1.0) }]}>
158
+ {tierLabel || tier.toUpperCase()}
159
+ </Typography>
160
+ <Typography style={[styles.planName, { color: colors.titleColor, fontSize: scaleText(18), lineHeight: scaleText(20) }]}>
161
+ {planName}
162
+ </Typography>
163
+ </View>
164
+ <View style={styles.priceRow}>
165
+ <Typography style={[styles.priceText, { color: colors.priceColor, fontSize: scaleText(22) }]}>
166
+ {priceText}
167
+ </Typography>
168
+ <Typography style={[styles.periodText, { color: colors.periodColor, fontSize: scaleText(10), marginLeft: scale(2) }]}>
169
+ {pricePeriod}
170
+ </Typography>
171
+ </View>
172
+ </View>
173
+ ) : (
174
+ <View style={[styles.header, { gap: scale(4), marginTop: scale(8) }]}>
175
+ <Typography style={[styles.labelTier, { color: colors.labelColor, fontSize: scaleText(10), letterSpacing: scale(1.0) }]}>
176
+ {tierLabel || tier.toUpperCase()}
177
+ </Typography>
178
+ <Typography style={[styles.planName, { color: colors.titleColor, fontSize: scaleText(18), lineHeight: scaleText(20) }]}>
179
+ {planName}
180
+ </Typography>
181
+ <View style={styles.priceRow}>
182
+ <Typography style={[styles.priceText, { color: colors.priceColor, fontSize: scaleText(22) }]}>
183
+ {priceText}
184
+ </Typography>
185
+ <Typography style={[styles.periodText, { color: colors.periodColor, fontSize: scaleText(10), marginLeft: scale(2) }]}>
186
+ {pricePeriod}
187
+ </Typography>
188
+ </View>
189
+ </View>
190
+ )}
191
+
192
+ {!isMini && (
193
+ <>
194
+ <BaseCard.Divider
195
+ color={tier === 'fluent' ? 'rgba(255, 255, 255, 0.15)' : '#efe2d6'}
196
+ verticalMargin={scale(14)}
197
+ />
198
+
199
+ {/* Feature List */}
200
+ <View style={[styles.featureList, { gap: scale(10), marginBottom: scale(16) }]}>
201
+ {includedItems.map((item, idx) => (
202
+ <View key={`inc-${idx}`} style={styles.featureRow}>
203
+ <View style={[styles.iconBox, { width: scale(20) }]}>
204
+ <Check stroke={colors.checkColor} strokeWidth={2.6} size={scale(14)} />
205
+ </View>
206
+ <Typography style={[styles.featureText, { color: colors.itemTextColor, fontSize: scaleText(12), lineHeight: scaleText(14) }]}>
207
+ {item}
208
+ </Typography>
209
+ </View>
210
+ ))}
211
+
212
+ {excludedItems.map((item, idx) => (
213
+ <View key={`exc-${idx}`} style={styles.featureRow}>
214
+ <View style={[styles.iconBox, { width: scale(20) }]}>
215
+ <X stroke={colors.crossColor} strokeWidth={2.4} size={scale(14)} />
216
+ </View>
217
+ <Typography style={[styles.featureText, { color: '#9a8478', textDecorationLine: 'line-through', fontSize: scaleText(12), lineHeight: scaleText(14) }]}>
218
+ {item}
219
+ </Typography>
220
+ </View>
221
+ ))}
222
+ </View>
223
+ </>
224
+ )}
225
+
226
+ </BaseCard>
227
+ </View>
228
+ );
229
+ };
230
+
231
+ const styles = StyleSheet.create({
232
+ cardWrapper: {
233
+ position: 'relative',
234
+ paddingTop: 10, // Margin to accommodate the floating badge without clipping
235
+ },
236
+ popularBadge: {
237
+ position: 'absolute',
238
+ top: -10,
239
+ alignSelf: 'center',
240
+ backgroundColor: '#f2974b',
241
+ paddingVertical: 4,
242
+ paddingHorizontal: 12,
243
+ borderRadius: 20,
244
+ zIndex: 10,
245
+ ...theme.shadows.sm,
246
+ },
247
+ popularBadgeText: {
248
+
249
+ fontSize: 9,
250
+ letterSpacing: 0.8,
251
+ color: '#ffffff',
252
+ textTransform: 'uppercase',
253
+ },
254
+ fluentFlatShadow: {
255
+ borderBottomWidth: 4,
256
+ borderBottomColor: '#3a241f',
257
+ },
258
+ header: {
259
+ alignItems: 'center',
260
+ gap: 4,
261
+ marginTop: 8,
262
+ },
263
+ miniHeader: {
264
+ flexDirection: 'row',
265
+ justifyContent: 'space-between',
266
+ alignItems: 'center',
267
+ width: '100%',
268
+ },
269
+ miniLeft: {
270
+ alignItems: 'flex-start',
271
+ gap: 4,
272
+ },
273
+ labelTier: {
274
+
275
+ fontSize: 10,
276
+ letterSpacing: 1.0,
277
+ textTransform: 'uppercase',
278
+ },
279
+ planName: {
280
+
281
+ fontSize: 18,
282
+ lineHeight: 20,
283
+ },
284
+ priceRow: {
285
+ flexDirection: 'row',
286
+ alignItems: 'baseline',
287
+ marginTop: 2,
288
+ },
289
+ priceText: {
290
+
291
+ fontSize: 22,
292
+ },
293
+ periodText: {
294
+
295
+ fontSize: 10,
296
+ marginLeft: 2,
297
+ },
298
+ featureList: {
299
+ gap: 10,
300
+ marginBottom: 16,
301
+ },
302
+ featureRow: {
303
+ flexDirection: 'row',
304
+ alignItems: 'center',
305
+ },
306
+ iconBox: {
307
+ width: 20,
308
+ justifyContent: 'center',
309
+ alignItems: 'flex-start',
310
+ },
311
+ featureText: {
312
+
313
+ fontSize: 12,
314
+ lineHeight: 14,
315
+ flex: 1,
316
+ },
317
+ });
@@ -0,0 +1,83 @@
1
+ // eslint-disable-next-line storybook/no-renderer-packages
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { View } from 'react-native';
4
+ import { PricingCard } from './PricingCard';
5
+
6
+ const meta = {
7
+ title: 'Components/Cards/PricingCard',
8
+ component: PricingCard,
9
+ parameters: {
10
+ layout: 'padded',
11
+ docs: {
12
+ description: {
13
+ component: 'O **PricingCard** exibe opções de subscrição na paywall.\n\n### Exemplo de Uso\n```tsx\n<PricingCard \n title="Pro"\n price="9,99€"\n period="/mês"\n features={["Acesso ilimitado", "Professores nativos"]}\n isHighlighted={true}\n/>\n```',
14
+ }
15
+ }
16
+ },
17
+ tags: ['autodocs'],
18
+ argTypes: {
19
+ title: { control: 'text' },
20
+ subtitle: { control: 'text' },
21
+ priceValue: { control: 'text' },
22
+ pricePeriod: { control: 'text' },
23
+ badgeLabel: { control: 'text' },
24
+ selected: { control: 'boolean' },
25
+ },
26
+ decorators: [
27
+ (Story) => (
28
+ <View style={{ padding: 24, maxWidth: 350, backgroundColor: '#f5f0eb' }}>
29
+ <Story />
30
+ </View>
31
+ ),
32
+ ],
33
+ } satisfies Meta<any>;
34
+
35
+ export default meta;
36
+ type Story = StoryObj<any>;
37
+
38
+ export const Default: Story = {
39
+ args: {
40
+ title: 'Mensal',
41
+ subtitle: 'Flexível · cancela quando quiseres',
42
+ priceValue: '€12,99',
43
+ pricePeriod: '/mês',
44
+ selected: false,
45
+ },
46
+ };
47
+
48
+ export const SelectedWithBadge: Story = {
49
+ args: {
50
+ title: 'Anual',
51
+ subtitle: '€89/ano · faturado uma vez',
52
+ priceValue: '€7,42',
53
+ pricePeriod: '/mês',
54
+ badgeLabel: 'POUPA 40%',
55
+ selected: true,
56
+ },
57
+ };
58
+
59
+ export const Grid: Story = {
60
+ args: {
61
+ title: '',
62
+ priceValue: '',
63
+ pricePeriod: '',
64
+ },
65
+ render: () => (
66
+ <View style={{ gap: 24 }}>
67
+ <PricingCard
68
+ title="Anual"
69
+ subtitle="€89/ano · faturado uma vez"
70
+ priceValue="€7,42"
71
+ pricePeriod="/mês"
72
+ badgeLabel="POUPA 40%"
73
+ selected
74
+ />
75
+ <PricingCard
76
+ title="Mensal"
77
+ subtitle="Flexível · cancela quando quiseres"
78
+ priceValue="€12,99"
79
+ pricePeriod="/mês"
80
+ />
81
+ </View>
82
+ ),
83
+ };
@@ -0,0 +1,144 @@
1
+ import { View, StyleSheet, Pressable } from 'react-native';
2
+ import type { ViewStyle } from 'react-native';
3
+ import { theme } from '../../../styles/theme';
4
+ import { useResponsive } from '../../../hooks/useResponsive';
5
+ import { Typography } from '../../Typography/Typography';
6
+
7
+ /**
8
+ * Cartão para apresentar os diferentes planos e preços de subscrição.
9
+ */
10
+ export interface PricingCardProps {
11
+ /** Título do plano (ex: 'Pro'). */
12
+ title: string;
13
+ /** Subtítulo do plano. */
14
+ subtitle?: string;
15
+ /** Preço formatado (ex: '9,99€'). */
16
+ priceValue: string;
17
+ /** Período de faturação (ex: '/mês'). */
18
+ pricePeriod: string;
19
+ /** Label opcional para destaque. */
20
+ badgeLabel?: string;
21
+ /** Estado de seleção. */
22
+ selected?: boolean;
23
+ /** Função ao selecionar o plano. */
24
+ onPress?: () => void;
25
+ style?: ViewStyle | ViewStyle[];
26
+ }
27
+
28
+ export const PricingCard = ({
29
+ title,
30
+ subtitle,
31
+ priceValue,
32
+ pricePeriod,
33
+ badgeLabel,
34
+ selected = false,
35
+ onPress,
36
+ style,
37
+ }: PricingCardProps) => {
38
+ const { scale, scaleText } = useResponsive();
39
+
40
+ return (
41
+ <Pressable
42
+ style={[
43
+ styles.container,
44
+ selected ? styles.selectedContainer : styles.defaultContainer,
45
+ {
46
+ padding: scale(20),
47
+ borderRadius: scale(16),
48
+ },
49
+ style,
50
+ ]}
51
+ onPress={onPress}
52
+ >
53
+ {badgeLabel && (
54
+ <View style={[
55
+ styles.badgeWrapper,
56
+ {
57
+ top: scale(-12),
58
+ right: scale(20),
59
+ paddingHorizontal: scale(8),
60
+ paddingVertical: scale(4),
61
+ borderRadius: scale(8),
62
+ }
63
+ ]}>
64
+ <Typography style={[styles.badgeText, { fontSize: scaleText(10), letterSpacing: scale(0.5) }]}>{badgeLabel}</Typography>
65
+ </View>
66
+ )}
67
+
68
+ <View style={styles.leftCol}>
69
+ <Typography style={[styles.title, { fontSize: scaleText(18), marginBottom: scale(4) }]}>{title}</Typography>
70
+ {subtitle && <Typography style={[styles.subtitle, { fontSize: scaleText(12) }]}>{subtitle}</Typography>}
71
+ </View>
72
+
73
+ <View style={[styles.rightCol, { marginLeft: scale(16) }]}>
74
+ <Typography style={[styles.priceValue, { fontSize: scaleText(22) }]}>{priceValue}</Typography>
75
+ <Typography style={[styles.pricePeriod, { fontSize: scaleText(12) }]}>{pricePeriod}</Typography>
76
+ </View>
77
+ </Pressable>
78
+ );
79
+ };
80
+
81
+ const styles = StyleSheet.create({
82
+ container: {
83
+ flexDirection: 'row',
84
+ justifyContent: 'space-between',
85
+ alignItems: 'center',
86
+ padding: 20,
87
+ borderRadius: 16,
88
+ borderWidth: 2,
89
+ borderStyle: 'solid',
90
+ width: '100%',
91
+ },
92
+ defaultContainer: {
93
+ borderColor: theme.colors.brown[100],
94
+ backgroundColor: theme.colors.white,
95
+ },
96
+ selectedContainer: {
97
+ borderColor: theme.colors.primary,
98
+ backgroundColor: theme.colors.white,
99
+ },
100
+ badgeWrapper: {
101
+ position: 'absolute',
102
+ top: -12,
103
+ right: 20,
104
+ backgroundColor: theme.colors.primary,
105
+ paddingHorizontal: 8,
106
+ paddingVertical: 4,
107
+ borderRadius: 8,
108
+ },
109
+ badgeText: {
110
+
111
+ fontSize: 10,
112
+ textTransform: 'uppercase',
113
+ color: theme.colors.white,
114
+ letterSpacing: 0.5,
115
+ },
116
+ leftCol: {
117
+ flex: 1,
118
+ },
119
+ rightCol: {
120
+ alignItems: 'flex-end',
121
+ marginLeft: 16,
122
+ },
123
+ title: {
124
+
125
+ fontSize: 18,
126
+ color: theme.colors.brown[800],
127
+ marginBottom: 4,
128
+ },
129
+ subtitle: {
130
+
131
+ fontSize: 12,
132
+ color: theme.colors.brown[500],
133
+ },
134
+ priceValue: {
135
+
136
+ fontSize: 22,
137
+ color: theme.colors.brown[800],
138
+ },
139
+ pricePeriod: {
140
+
141
+ fontSize: 12,
142
+ color: theme.colors.brown[500],
143
+ },
144
+ });
@@ -0,0 +1,77 @@
1
+ // eslint-disable-next-line storybook/no-renderer-packages
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { View } from 'react-native';
4
+ import { SelectionCard } from './SelectionCard';
5
+ import { Plane, Briefcase, Star, GraduationCap } from 'lucide-react-native';
6
+ import { theme } from '../../../styles/theme';
7
+
8
+ const meta = {
9
+ title: 'Components/Cards/SelectionCard',
10
+ component: SelectionCard,
11
+ parameters: {
12
+ layout: 'padded',
13
+ docs: {
14
+ description: {
15
+ component: 'O **SelectionCard** é utilizado nos inquéritos ou ecrãs de onboarding onde o utilizador precisa de selecionar uma resposta (ex: "Qual é a tua motivação?").\n\n### Exemplo de Uso\n```tsx\n<SelectionCard \n title="Viagem"\n description="Para as tuas próximas férias"\n isSelected={true}\n onPress={() => selecionar("Viagem")}\n/>\n```',
16
+ }
17
+ }
18
+ },
19
+ tags: ['autodocs'],
20
+ argTypes: {
21
+ label: { control: 'text' },
22
+ selected: { control: 'boolean' },
23
+ },
24
+ decorators: [
25
+ (Story) => (
26
+ <View style={{ padding: 24, maxWidth: 350 }}>
27
+ <Story />
28
+ </View>
29
+ ),
30
+ ],
31
+ } satisfies Meta<any>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<any>;
35
+
36
+ export const Default: Story = {
37
+ args: {
38
+ label: 'Viajar para França',
39
+ iconNode: <Plane size={24} color={theme.colors.brown[500]} />,
40
+ selected: false,
41
+ },
42
+ };
43
+
44
+ export const Selected: Story = {
45
+ args: {
46
+ label: 'Trabalho e carreira',
47
+ iconNode: <Briefcase size={24} color={theme.colors.white} />,
48
+ selected: true,
49
+ },
50
+ };
51
+
52
+ export const Grid: Story = {
53
+ args: {
54
+ label: '',
55
+ },
56
+ render: () => (
57
+ <View style={{ gap: 16 }}>
58
+ <SelectionCard
59
+ label="Viajar para França"
60
+ iconNode={<Plane size={24} color={theme.colors.white} />}
61
+ selected
62
+ />
63
+ <SelectionCard
64
+ label="Trabalho e carreira"
65
+ iconNode={<Briefcase size={24} color={theme.colors.brown[500]} />}
66
+ />
67
+ <SelectionCard
68
+ label="Cultura e diversão"
69
+ iconNode={<Star size={24} color={theme.colors.brown[500]} />}
70
+ />
71
+ <SelectionCard
72
+ label="Estudos / exames"
73
+ iconNode={<GraduationCap size={24} color={theme.colors.brown[500]} />}
74
+ />
75
+ </View>
76
+ ),
77
+ };