@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,97 @@
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 { CountryCard } from './CountryCard';
5
+
6
+ const meta = {
7
+ title: 'Components/Cards/CountryCard',
8
+ component: CountryCard,
9
+ parameters: {
10
+ layout: 'padded',
11
+ docs: {
12
+ description: {
13
+ component: 'O **CountryCard** é usado na secção de Onboarding para selecionar um país (destino de aprendizagem).\n\n### Exemplo de Uso\n```tsx\n<CountryCard \n name="Senegal"\n isSelected={true}\n onPress={() => selecionar("Senegal")}\n/>\n```',
14
+ }
15
+ }
16
+ },
17
+ tags: ['autodocs'],
18
+ argTypes: {
19
+ countryName: { control: 'text' },
20
+ levelCode: { control: 'text' },
21
+ status: { control: { type: 'select', options: ['completed', 'active', 'locked'] } },
22
+ subtitle: { control: 'text' },
23
+ },
24
+ decorators: [
25
+ (Story) => (
26
+ <View style={{ padding: 24, maxWidth: 400, backgroundColor: '#f5f0eb' }}>
27
+ <Story />
28
+ </View>
29
+ ),
30
+ ],
31
+ } satisfies Meta<any>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<any>;
35
+
36
+ export const Completed: Story = {
37
+ name: "Concluído (Carimbado)",
38
+ args: {
39
+ countryName: 'França',
40
+ levelCode: 'A1',
41
+ status: 'completed',
42
+ subtitle: '6 módulos · carimbado',
43
+ },
44
+ };
45
+
46
+ export const Active: Story = {
47
+ name: "Em Curso (Active)",
48
+ args: {
49
+ countryName: 'Senegal',
50
+ levelCode: 'B2',
51
+ status: 'active',
52
+ subtitle: 'a decorrer · 2/4 módulos',
53
+ },
54
+ };
55
+
56
+ export const Locked: Story = {
57
+ name: "Bloqueado (Locked)",
58
+ args: {
59
+ countryName: 'Canadá',
60
+ levelCode: 'B1',
61
+ status: 'locked',
62
+ subtitle: 'Bloqueado',
63
+ },
64
+ };
65
+
66
+ export const Grid: Story = {
67
+ args: {
68
+ countryName: '',
69
+ levelCode: '',
70
+ status: 'completed',
71
+ subtitle: '',
72
+ },
73
+ render: () => (
74
+ <View style={{ gap: 16 }}>
75
+ <CountryCard
76
+ countryName="França"
77
+ levelCode="A1"
78
+ status="completed"
79
+ subtitle="6 módulos · carimbado"
80
+ onPress={() => alert('Rever França')}
81
+ />
82
+ <CountryCard
83
+ countryName="Senegal"
84
+ levelCode="B2"
85
+ status="active"
86
+ subtitle="a decorrer · 2/4 módulos"
87
+ onPress={() => alert('Continuar Senegal')}
88
+ />
89
+ <CountryCard
90
+ countryName="Canadá"
91
+ levelCode="B1"
92
+ status="locked"
93
+ subtitle="Bloqueado"
94
+ />
95
+ </View>
96
+ ),
97
+ };
@@ -0,0 +1,185 @@
1
+ import { View, StyleSheet } from 'react-native';
2
+ import type { ViewStyle } from 'react-native';
3
+ import { BaseCard } from '../BaseCard/BaseCard';
4
+ import { Flag } from '../../DataDisplay/Flag/Flag';
5
+ import { RotateCcw, Lock } from 'lucide-react-native';
6
+ import { Typography } from '../../Typography/Typography';
7
+
8
+ export type CountryCardStatus = 'completed' | 'active' | 'locked';
9
+
10
+ /**
11
+ * Cartão que exibe informações de progresso de um país no módulo de exploração.
12
+ */
13
+ export interface CountryCardProps {
14
+ /** Nome do país (utilizado para carregar a bandeira correta). */
15
+ countryName: string;
16
+ /** Código identificador do nível (ex: 'L1'). */
17
+ levelCode: string;
18
+ /** Status atual de progresso do país. */
19
+ status: CountryCardStatus;
20
+ /** Texto descritivo do progresso atual. */
21
+ subtitle: string;
22
+ /** Função disparada ao clicar no cartão. */
23
+ onPress?: () => void;
24
+ /** Estilos adicionais para o componente. */
25
+ style?: ViewStyle | ViewStyle[];
26
+ }
27
+
28
+ export const CountryCard = ({
29
+ countryName,
30
+ levelCode,
31
+ status,
32
+ subtitle,
33
+ onPress,
34
+ style,
35
+ }: CountryCardProps) => {
36
+ // Resolve configuration based on status
37
+ const getStatusStyles = () => {
38
+ switch (status) {
39
+ case 'completed':
40
+ return {
41
+ borderStyle: 'solid' as const,
42
+ borderColor: '#efe2d6',
43
+ opacity: 1.0,
44
+ flagBorderColor: '#f2974b',
45
+ subtitleColor: '#9a8478',
46
+ };
47
+ case 'active':
48
+ return {
49
+ borderStyle: 'dashed' as const,
50
+ borderColor: '#d8c7b8',
51
+ opacity: 0.85,
52
+ flagBorderColor: '#efe2d6',
53
+ subtitleColor: '#cf7a30',
54
+ };
55
+ case 'locked':
56
+ return {
57
+ borderStyle: 'solid' as const,
58
+ borderColor: '#efe2d6',
59
+ opacity: 0.5,
60
+ flagBorderColor: '#efe2d6',
61
+ subtitleColor: '#b9a392',
62
+ };
63
+ }
64
+ };
65
+
66
+ const statusStyles = getStatusStyles();
67
+
68
+ const renderAction = () => {
69
+ switch (status) {
70
+ case 'completed':
71
+ return (
72
+ <View style={styles.actionCol}>
73
+ <RotateCcw stroke="#f2974b" strokeWidth={2.4} size={16} />
74
+ <Typography style={styles.reverText}>REVER</Typography>
75
+ </View>
76
+ );
77
+ case 'active':
78
+ return (
79
+ <View style={styles.actionCol}>
80
+ <Typography style={styles.emCursoText}>EM</Typography>
81
+ <Typography style={styles.emCursoText}>CURSO</Typography>
82
+ </View>
83
+ );
84
+ case 'locked':
85
+ return (
86
+ <View style={styles.actionCol}>
87
+ <Lock stroke="#c3b1a4" strokeWidth={2.2} size={18} />
88
+ </View>
89
+ );
90
+ }
91
+ };
92
+
93
+ return (
94
+ <BaseCard
95
+ onPress={status !== 'locked' ? onPress : undefined}
96
+ disabled={status === 'locked'}
97
+ borderStyle={statusStyles.borderStyle}
98
+ borderColor={statusStyles.borderColor}
99
+ opacity={statusStyles.opacity}
100
+ paddingVertical={12}
101
+ paddingHorizontal={14}
102
+ style={style}
103
+ >
104
+ <View style={styles.container}>
105
+ {/* Left: Flag Avatar */}
106
+ <View style={[styles.flagWrapper, { borderColor: statusStyles.flagBorderColor }]}>
107
+ <Flag countryCode={countryName} size={38} />
108
+ </View>
109
+
110
+ {/* Center: Content Info */}
111
+ <View style={styles.infoCol}>
112
+ <Typography style={styles.titleText}>
113
+ {countryName} · {levelCode}
114
+ </Typography>
115
+ <Typography style={[styles.subtitleText, { color: statusStyles.subtitleColor }]}>
116
+ {subtitle}
117
+ </Typography>
118
+ </View>
119
+
120
+ {/* Right: Action Area */}
121
+ <View style={styles.actionWrapper}>
122
+ {renderAction()}
123
+ </View>
124
+ </View>
125
+ </BaseCard>
126
+ );
127
+ };
128
+
129
+ const styles = StyleSheet.create({
130
+ container: {
131
+ flexDirection: 'row',
132
+ alignItems: 'center',
133
+ },
134
+ flagWrapper: {
135
+ width: 42,
136
+ height: 42,
137
+ borderRadius: 21,
138
+ borderWidth: 2,
139
+ justifyContent: 'center',
140
+ alignItems: 'center',
141
+ backgroundColor: '#fff',
142
+ overflow: 'hidden',
143
+ },
144
+ infoCol: {
145
+ flex: 1,
146
+ marginLeft: 12,
147
+ justifyContent: 'center',
148
+ },
149
+ titleText: {
150
+
151
+ fontSize: 16,
152
+ color: '#51332d',
153
+ lineHeight: 18,
154
+ marginBottom: 4,
155
+ },
156
+ subtitleText: {
157
+
158
+ fontSize: 11,
159
+ lineHeight: 13,
160
+ },
161
+ actionWrapper: {
162
+ marginLeft: 12,
163
+ justifyContent: 'center',
164
+ alignItems: 'center',
165
+ minWidth: 46,
166
+ },
167
+ actionCol: {
168
+ alignItems: 'center',
169
+ justifyContent: 'center',
170
+ gap: 2,
171
+ },
172
+ reverText: {
173
+
174
+ fontSize: 10,
175
+ color: '#f2974b',
176
+ letterSpacing: 0.5,
177
+ },
178
+ emCursoText: {
179
+
180
+ fontSize: 10,
181
+ color: '#cf7a30',
182
+ lineHeight: 11,
183
+ textAlign: 'center',
184
+ },
185
+ });
@@ -0,0 +1,61 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { View } from 'react-native';
3
+ import { LevelProgressCard } from './LevelProgressCard';
4
+
5
+ const meta = {
6
+ title: 'Components/Cards/LevelProgressCard',
7
+ component: LevelProgressCard,
8
+ parameters: {
9
+ layout: 'centered',
10
+ docs: {
11
+ description: {
12
+ component:
13
+ 'O **LevelProgressCard** é utilizado no ecrã de progresso para destacar o avanço atual do utilizador para a fluência, apresentando um gráfico circular, nível atual e texto motivacional, sobre um fundo castanho escuro.',
14
+ },
15
+ },
16
+ },
17
+ tags: ['autodocs'],
18
+ argTypes: {
19
+ level: { control: 'text' },
20
+ progressPercent: { control: { type: 'range', min: 0, max: 100, step: 1 } },
21
+ subtitle: { control: 'text' },
22
+ description: { control: 'text' },
23
+ },
24
+ decorators: [
25
+ (Story) => (
26
+ <View style={{ padding: 24, backgroundColor: '#fbf6f0', width: 400 }}>
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
+ level: 'B2',
39
+ progressPercent: 45,
40
+ subtitle: '45% ATÉ AO C1',
41
+ description: 'Já dominas o intermédio. Faltam 3 países para a fluência.',
42
+ },
43
+ };
44
+
45
+ export const Beginner: Story = {
46
+ args: {
47
+ level: 'A1',
48
+ progressPercent: 12,
49
+ subtitle: '12% ATÉ AO A2',
50
+ description: 'O início de uma grande viagem. Continua a explorar novos destinos!',
51
+ },
52
+ };
53
+
54
+ export const AlmostFluent: Story = {
55
+ args: {
56
+ level: 'C1',
57
+ progressPercent: 88,
58
+ subtitle: '88% ATÉ À FLUÊNCIA',
59
+ description: 'Estás a um passo de dominar completamente a francofonia!',
60
+ },
61
+ };
@@ -0,0 +1,143 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { View, StyleSheet, Animated, Easing, Platform } from 'react-native';
3
+ import type { ViewStyle } from 'react-native';
4
+ import Svg, { Circle } from 'react-native-svg';
5
+ import { theme } from '../../../styles/theme';
6
+ import { useResponsive } from '../../../hooks/useResponsive';
7
+ import { Typography } from '../../Typography/Typography';
8
+
9
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
10
+
11
+ export interface LevelProgressCardProps {
12
+ /** Nível atual a apresentar no centro do círculo (ex: "B2") */
13
+ level: string;
14
+ /** Percentagem de progresso de 0 a 100 */
15
+ progressPercent: number;
16
+ /** Texto destacado pequeno acima da descrição (ex: "45% ATÉ AO C1") */
17
+ subtitle: string;
18
+ /** Texto de descrição principal (ex: "Já dominas o intermédio...") */
19
+ description: string;
20
+ /** Estilos customizados para o container principal */
21
+ style?: ViewStyle | ViewStyle[];
22
+ }
23
+
24
+ export const LevelProgressCard = ({
25
+ level,
26
+ progressPercent,
27
+ subtitle,
28
+ description,
29
+ style,
30
+ }: LevelProgressCardProps) => {
31
+ const { scale } = useResponsive();
32
+ const animatedValue = useRef(new Animated.Value(0)).current;
33
+
34
+ // Circle configs
35
+ const size = scale(90);
36
+ const strokeWidth = scale(8);
37
+ const radius = (size - strokeWidth) / 2;
38
+ const circumference = radius * 2 * Math.PI;
39
+
40
+ useEffect(() => {
41
+ animatedValue.setValue(0);
42
+ Animated.timing(animatedValue, {
43
+ toValue: progressPercent,
44
+ duration: 1200,
45
+ delay: 300,
46
+ easing: Easing.out(Easing.cubic),
47
+ useNativeDriver: false,
48
+ }).start();
49
+ }, [progressPercent, animatedValue]);
50
+
51
+ const strokeDashoffset = animatedValue.interpolate({
52
+ inputRange: [0, 100],
53
+ outputRange: [circumference, 0],
54
+ extrapolate: 'clamp',
55
+ });
56
+
57
+ return (
58
+ <View style={[styles.cardContainer, { padding: scale(20), borderRadius: scale(24) }, style]}>
59
+ {/* Left side: Circular Progress */}
60
+ <View style={[styles.progressContainer, { width: size, height: size, marginRight: scale(20) }]}>
61
+ <Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
62
+ {/* Background Track */}
63
+ <Circle
64
+ stroke={theme.colors.brown[700]} // Dark brown track
65
+ cx={size / 2}
66
+ cy={size / 2}
67
+ r={radius}
68
+ strokeWidth={strokeWidth}
69
+ fill="none"
70
+ />
71
+ {/* Progress Circle */}
72
+ <AnimatedCircle
73
+ stroke={theme.colors.primary}
74
+ cx={size / 2}
75
+ cy={size / 2}
76
+ r={radius}
77
+ strokeWidth={strokeWidth}
78
+ strokeDasharray={circumference}
79
+ strokeDashoffset={strokeDashoffset}
80
+ strokeLinecap="round"
81
+ fill="none"
82
+ rotation="-90"
83
+ origin={`${size / 2}, ${size / 2}`}
84
+ />
85
+ </Svg>
86
+ {/* Inner Text */}
87
+ <View style={styles.innerTextContainer}>
88
+ <Typography variant="display2" style={{ color: theme.colors.white }}>{level}</Typography>
89
+ <Typography variant="caption" style={{ color: theme.colors.brown[300] }}>nível</Typography>
90
+ </View>
91
+ </View>
92
+
93
+ {/* Right side: Texts */}
94
+ <View style={styles.textContainer}>
95
+ <Typography variant="bodySm" style={[styles.subtitle, { marginBottom: scale(4) }]}>
96
+ {subtitle.toUpperCase()}
97
+ </Typography>
98
+ <Typography variant="bodyLg" style={styles.description}>
99
+ {description}
100
+ </Typography>
101
+ </View>
102
+ </View>
103
+ );
104
+ };
105
+
106
+ const styles = StyleSheet.create({
107
+ cardContainer: {
108
+ backgroundColor: theme.colors.brown[800], // The dark brown color from the design
109
+ flexDirection: 'row',
110
+ alignItems: 'center',
111
+ width: '100%',
112
+ ...Platform.select({
113
+ web: { boxShadow: `0px 6px 0px ${theme.colors.brown[900]}` },
114
+ default: {
115
+ shadowColor: theme.colors.brown[900],
116
+ shadowOffset: { width: 0, height: 6 },
117
+ shadowOpacity: 1,
118
+ shadowRadius: 0,
119
+ elevation: 0,
120
+ },
121
+ }),
122
+ },
123
+ progressContainer: {
124
+ alignItems: 'center',
125
+ justifyContent: 'center',
126
+ position: 'relative',
127
+ },
128
+ innerTextContainer: {
129
+ position: 'absolute',
130
+ alignItems: 'center',
131
+ justifyContent: 'center',
132
+ },
133
+ textContainer: {
134
+ flex: 1,
135
+ justifyContent: 'center',
136
+ },
137
+ subtitle: {
138
+ color: theme.colors.primary,
139
+ },
140
+ description: {
141
+ color: theme.colors.brown[100],
142
+ },
143
+ });
@@ -0,0 +1,108 @@
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 { ModuleCard } from './ModuleCard';
5
+
6
+ const meta = {
7
+ title: 'Components/Cards/ModuleCard',
8
+ component: ModuleCard,
9
+ parameters: {
10
+ layout: 'padded',
11
+ docs: {
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```',
14
+ }
15
+ }
16
+ },
17
+ tags: ['autodocs'],
18
+ argTypes: {
19
+ status: { control: 'radio', options: ['active', 'locked', 'completed', 'dark'] },
20
+ number: { control: 'number' },
21
+ title: { control: 'text' },
22
+ subtitle: { control: 'text' },
23
+ subtext: { control: 'text' },
24
+ tagLabel: { control: 'text' },
25
+ progress: { control: 'number' },
26
+ segments: { control: 'number' },
27
+ },
28
+ decorators: [
29
+ (Story) => (
30
+ <View style={{ width: '100%', backgroundColor: '#f5f0eb' }}>
31
+ <Story />
32
+ </View>
33
+ ),
34
+ ],
35
+ } satisfies Meta<any>;
36
+
37
+ export default meta;
38
+ type Story = StoryObj<any>;
39
+
40
+ export const Active: Story = {
41
+ args: {
42
+ status: 'active',
43
+ number: 1,
44
+ title: 'No mercado de Dakar',
45
+ subtitle: 'Regatear, números, comida',
46
+ subtext: '• 3/5',
47
+ tagLabel: 'EM CURSO',
48
+ progress: 0.6,
49
+ segments: 5,
50
+ },
51
+ };
52
+
53
+ export const Locked: Story = {
54
+ args: {
55
+ status: 'locked',
56
+ number: 3,
57
+ title: 'A minha família',
58
+ subtitle: '5 lições',
59
+ },
60
+ };
61
+
62
+ export const DarkLocked: Story = {
63
+ args: {
64
+ status: 'dark',
65
+ title: 'Aula ao vivo + Teste final',
66
+ subtitle: 'Acaba os 4 módulos para desbloquear',
67
+ },
68
+ };
69
+
70
+ export const Grid: Story = {
71
+ args: {
72
+ status: 'active',
73
+ number: 1,
74
+ title: '',
75
+ },
76
+ render: () => (
77
+ <View style={{ gap: 16, width: '100%' }}>
78
+ <ModuleCard
79
+ status="completed"
80
+ number={1}
81
+ title="Saudações & teranga"
82
+ subtitle="Concluído"
83
+ subtext="• 5/5"
84
+ />
85
+ <ModuleCard
86
+ status="active"
87
+ number={2}
88
+ title="No mercado de Dakar"
89
+ subtitle="Regatear, números, comida"
90
+ subtext="• 3/5"
91
+ tagLabel="EM CURSO"
92
+ progress={0.6}
93
+ segments={5}
94
+ />
95
+ <ModuleCard
96
+ status="locked"
97
+ number={3}
98
+ title="A minha família"
99
+ subtitle="5 lições"
100
+ />
101
+ <ModuleCard
102
+ status="dark"
103
+ title="Aula ao vivo + Teste final"
104
+ subtitle="Acaba os 4 módulos para desbloquear"
105
+ />
106
+ </View>
107
+ ),
108
+ };