@xetwa/design-system 1.0.46 → 1.0.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xetwa/design-system",
3
- "version": "1.0.46",
3
+ "version": "1.0.49",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
- import { Pressable, StyleSheet } from 'react-native';
1
+ import React, { useRef } from 'react';
2
+ import { Pressable, StyleSheet, Animated } from 'react-native';
3
3
  import type { ViewStyle } from 'react-native';
4
4
  import { Typography } from '../../Typography/Typography';
5
5
  import { useResponsive } from '../../../hooks/useResponsive';
@@ -64,24 +64,50 @@ export const Chip = ({
64
64
  lg: 'bodyMdHeavy',
65
65
  }[size] as 'smallHeavy' | 'bodySmHeavy' | 'bodyMdHeavy';
66
66
 
67
+ const scaleAnim = useRef(new Animated.Value(1)).current;
68
+
69
+ const handlePressIn = () => {
70
+ Animated.spring(scaleAnim, {
71
+ toValue: 0.9,
72
+ speed: 20,
73
+ bounciness: 12,
74
+ useNativeDriver: true,
75
+ }).start();
76
+ };
77
+
78
+ const handlePressOut = () => {
79
+ Animated.spring(scaleAnim, {
80
+ toValue: 1,
81
+ speed: 20,
82
+ bounciness: 12,
83
+ useNativeDriver: true,
84
+ }).start();
85
+ };
86
+
67
87
  return (
68
88
  <Pressable
69
89
  onPress={onPress}
70
- style={({ pressed }) => [
71
- styles.container,
72
- sizeStyles,
73
- {
74
- backgroundColor,
75
- borderColor,
76
- borderWidth,
77
- opacity: pressed ? 0.7 : 1,
78
- },
79
- style,
80
- ]}
90
+ onPressIn={handlePressIn}
91
+ onPressOut={handlePressOut}
92
+ style={({ pressed }) => [{ opacity: pressed ? 0.8 : 1 }]}
81
93
  >
82
- <Typography variant={typographyVariant} color={textColor} style={{ textAlign: 'center' }}>
83
- {label}
84
- </Typography>
94
+ <Animated.View
95
+ style={[
96
+ styles.container,
97
+ sizeStyles,
98
+ {
99
+ backgroundColor,
100
+ borderColor,
101
+ borderWidth,
102
+ transform: [{ scale: scaleAnim }],
103
+ },
104
+ style,
105
+ ]}
106
+ >
107
+ <Typography variant={typographyVariant} color={textColor} style={{ textAlign: 'center' }}>
108
+ {label}
109
+ </Typography>
110
+ </Animated.View>
85
111
  </Pressable>
86
112
  );
87
113
  };
@@ -0,0 +1,153 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import React from 'react';
3
+ import { View } from 'react-native';
4
+ import { Stamp } from './Stamp';
5
+ import { theme } from '../../../styles/theme';
6
+
7
+ const meta = {
8
+ title: 'Components/DataDisplay/Stamp',
9
+ component: Stamp,
10
+ parameters: {
11
+ layout: 'centered',
12
+ docs: {
13
+ description: {
14
+ component: `O componente **Stamp** reproduz visualmente um "carimbo de passaporte". É usado no ecrã da rota para representar módulos que estão em curso, concluídos, ou localidades visitadas.
15
+
16
+ ### Características
17
+ - **Cores Livres:** Pode passar a propriedade \`color\` com qualquer cor (HEX, RGB) e o fundo é gerado automaticamente.
18
+ - **Textos Flexíveis:** O carimbo aceita \`topLabel\`, \`title\` e \`bottomLabels\` para escrever o que quiser.
19
+ ## Notas de Uso
20
+ - O componente possui três tamanhos pré-definidos: **sm** (100px), **md** (140px, padrão), e **lg** (180px).
21
+ - Cada tamanho tem a sua própria escala de tipografia matematicamente calculada para garantir que as letras nunca ficam desproporcionais ao círculo.
22
+ - Se a palavra do título for demasiado comprida, o tamanho de letra ajusta-se automaticamente (\`adjustsFontSizeToFit\`) sem quebrar o carimbo.
23
+
24
+ ### Exemplo de Uso
25
+ \`\`\`tsx
26
+ import { Stamp } from '@xetwa/design-system';
27
+
28
+ // Carimbo estilo Paris (Azul)
29
+ <Stamp
30
+ topLabel="REPUBLIQUE"
31
+ title="PARIS"
32
+ bottomLabels={['NIVEAU A1', 'MAR 2026']}
33
+ color="#2a6fc2"
34
+ />
35
+
36
+ // Carimbo estilo Dakar com inclinação à direita
37
+ <Stamp
38
+ topLabel="SÉNÉGAL"
39
+ title="DAKAR"
40
+ bottomLabels={['NIVEAU B1', 'EM CURSO']}
41
+ color="#f2974b"
42
+ position="right"
43
+ />
44
+ \`\`\`
45
+ `,
46
+ },
47
+ },
48
+ },
49
+ decorators: [
50
+ (Story) => (
51
+ <View style={{ padding: 40, backgroundColor: theme.colors.brown[50], alignItems: 'center', justifyContent: 'center' }}>
52
+ <Story />
53
+ </View>
54
+ ),
55
+ ],
56
+ tags: ['autodocs'],
57
+ argTypes: {
58
+ color: {
59
+ control: 'color',
60
+ description: 'Cor principal do carimbo (texto e borda).',
61
+ },
62
+ size: {
63
+ control: 'radio',
64
+ options: ['sm', 'md', 'lg'],
65
+ description: 'Tamanho pré-definido do carimbo.',
66
+ },
67
+ bgColor: {
68
+ control: 'color',
69
+ description: 'Cor de fundo do círculo interno (opcional).',
70
+ },
71
+ position: {
72
+ control: 'radio',
73
+ options: ['left', 'center', 'right'],
74
+ description: 'Inclinação do carimbo.',
75
+ },
76
+ },
77
+ } satisfies Meta<typeof Stamp>;
78
+
79
+ export default meta;
80
+ type Story = StoryObj<typeof meta>;
81
+
82
+ /**
83
+ * Carimbo Laranja, estilo Dakar (Em curso)
84
+ */
85
+ export const Dakar: Story = {
86
+ render: (args) => <Stamp {...args} />,
87
+ args: {
88
+ topLabel: 'SÉNÉGAL',
89
+ title: 'DAKAR',
90
+ bottomLabels: ['NIVEAU B1', 'EM CURSO'],
91
+ color: theme.colors.primary,
92
+ position: 'left',
93
+ },
94
+ };
95
+
96
+ /**
97
+ * Carimbo Azul, estilo Paris (Concluído antigo)
98
+ */
99
+ export const Paris: Story = {
100
+ render: (args) => <Stamp {...args} />,
101
+ args: {
102
+ topLabel: 'REPUBLIQUE',
103
+ title: 'PARIS',
104
+ bottomLabels: ['NIVEAU A1', 'MAR 2026'],
105
+ color: theme.colors.info,
106
+ position: 'right',
107
+ },
108
+ };
109
+
110
+ /**
111
+ * Carimbo Vermelho, estilo Lyon (Concluído recente)
112
+ */
113
+ export const Lyon: Story = {
114
+ render: (args) => <Stamp {...args} />,
115
+ args: {
116
+ topLabel: 'FRANCE',
117
+ title: 'LYON',
118
+ bottomLabels: ['NIVEAU A1', 'ABR 2026'],
119
+ color: theme.colors.error,
120
+ position: 'center',
121
+ },
122
+ };
123
+
124
+ /**
125
+ * Demonstra a capacidade do componente se ajustar perfeitamente a diferentes escalas.
126
+ */
127
+ export const Scalability: Story = {
128
+ render: () => (
129
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 24, justifyContent: 'center', alignItems: 'center' }}>
130
+ <Stamp
131
+ color="#8e44ad"
132
+ title="GENEVE"
133
+ topLabel="SUISSE"
134
+ size="sm"
135
+ bottomLabels={['NIVEAU A2']}
136
+ />
137
+ <Stamp
138
+ color="#d35400"
139
+ title="MONTREAL"
140
+ topLabel="CANADA"
141
+ size="md"
142
+ bottomLabels={['NIVEAU B1']}
143
+ />
144
+ <Stamp
145
+ color="#16a085"
146
+ title="KINSHASA"
147
+ topLabel="RD CONGO"
148
+ size="lg"
149
+ bottomLabels={['NIVEAU C2']}
150
+ />
151
+ </View>
152
+ ),
153
+ };
@@ -0,0 +1,173 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, Platform } from 'react-native';
3
+ import type { ViewStyle } from 'react-native';
4
+ import { Typography } from '../../Typography/Typography';
5
+
6
+ export interface StampProps {
7
+ /**
8
+ * Texto superior (ex: 'REPUBLIQUE', 'FRANCE', 'SÉNÉGAL').
9
+ */
10
+ topLabel?: string;
11
+ /**
12
+ * Título central em destaque (ex: 'PARIS', 'LYON', 'DAKAR').
13
+ */
14
+ title: string;
15
+ /**
16
+ * Textos inferiores, normalmente nível e data (ex: ['NIVEAU A1', 'MAR 2026']).
17
+ */
18
+ bottomLabels?: string[];
19
+ /**
20
+ * Cor principal do carimbo (aplicada ao texto e borda).
21
+ */
22
+ color: string;
23
+ /**
24
+ * Cor de fundo do círculo interno. Se não for fornecida,
25
+ * calculará automaticamente 10% de opacidade se a cor principal for HEX.
26
+ */
27
+ bgColor?: string;
28
+ /**
29
+ * Tamanho do carimbo.
30
+ * - `sm`: 100px (ideal para ícones/badges)
31
+ * - `md`: 140px (tamanho padrão)
32
+ * - `lg`: 180px (grande destaque)
33
+ * @default 'md'
34
+ */
35
+ size?: 'sm' | 'md' | 'lg';
36
+ /**
37
+ * Posição (inclinação) do carimbo.
38
+ * - `left`: Inclinado para a esquerda (-8 graus)
39
+ * - `center`: Direito (0 graus)
40
+ * - `right`: Inclinado para a direita (8 graus)
41
+ * @default 'left'
42
+ */
43
+ position?: 'left' | 'center' | 'right';
44
+ /**
45
+ * Estilos adicionais para o contentor exterior.
46
+ */
47
+ style?: ViewStyle | ViewStyle[];
48
+ }
49
+
50
+ /**
51
+ * **Stamp (Carimbo)**
52
+ *
53
+ * Componente visual para representar módulos ou cidades concluídas/em curso na rota de aprendizagem.
54
+ * Possui um aspeto autêntico de carimbo de passaporte, com borda tracejada e textos proporcionais.
55
+ */
56
+ export const Stamp = ({
57
+ topLabel,
58
+ title,
59
+ bottomLabels = [],
60
+ color,
61
+ bgColor,
62
+ size = 'md',
63
+ position = 'left',
64
+ style,
65
+ }: StampProps) => {
66
+
67
+ const SIZE_MAP = {
68
+ sm: { base: 100, top: 9, title: 18, bottom1: 7, bottom2: 6 },
69
+ md: { base: 140, top: 11, title: 22, bottom1: 9, bottom2: 7 },
70
+ lg: { base: 180, top: 14, title: 26, bottom1: 11, bottom2: 8 },
71
+ };
72
+
73
+ const currentSize = SIZE_MAP[size];
74
+ const BASE_SIZE = currentSize.base;
75
+
76
+ // Determinar inclinação: aplicamos a rotação ao carimbo inteiro!
77
+ const rotation = position === 'right' ? 12 : position === 'center' ? 0 : -12;
78
+
79
+ // Determinar cor de fundo (injetar 10% de opacidade se for HEX)
80
+ const isHex = color.startsWith('#') && (color.length === 7 || color.length === 4);
81
+ const calculatedBgColor = bgColor || (isHex ? `${color}1A` : 'transparent');
82
+
83
+ // Cálculos dinâmicos de proporção baseados no tamanho
84
+ const innerSize = BASE_SIZE * 0.82;
85
+ const borderWidth = Math.max(2.5, BASE_SIZE * 0.035);
86
+
87
+ return (
88
+ <View style={[{ alignSelf: 'flex-start', transform: [{ rotate: `${rotation}deg` }] }, style]}>
89
+ <View
90
+ style={[
91
+ styles.outerCircle,
92
+ {
93
+ width: BASE_SIZE,
94
+ height: BASE_SIZE,
95
+ borderRadius: BASE_SIZE / 2,
96
+ borderColor: color,
97
+ borderWidth: borderWidth,
98
+ },
99
+ ]}
100
+ >
101
+ <View style={[
102
+ styles.innerCircle,
103
+ {
104
+ width: innerSize,
105
+ height: innerSize,
106
+ borderRadius: innerSize / 2,
107
+ backgroundColor: calculatedBgColor,
108
+ }
109
+ ]}>
110
+
111
+ {topLabel ? (
112
+ <View style={{ position: 'absolute', top: '22%', width: '100%', alignItems: 'center' }}>
113
+ <Typography
114
+ color={color}
115
+ variant="stampTop"
116
+ style={{ textAlign: 'center', fontSize: currentSize.top, lineHeight: currentSize.top + 2 }}
117
+ >
118
+ {topLabel}
119
+ </Typography>
120
+ </View>
121
+ ) : null}
122
+
123
+ <View style={[styles.contentWrapper, { paddingHorizontal: BASE_SIZE * 0.2 }]}>
124
+ <Typography
125
+ color={color}
126
+ variant="stampTitle"
127
+ style={{ textAlign: 'center', fontSize: currentSize.title, lineHeight: currentSize.title + 4 }}
128
+ >
129
+ {title}
130
+ </Typography>
131
+ </View>
132
+
133
+ {bottomLabels.length > 0 ? (
134
+ <View style={{ position: 'absolute', bottom: '15%', width: '100%', alignItems: 'center' }}>
135
+ {bottomLabels.map((label, index) => (
136
+ <Typography
137
+ key={index}
138
+ color={color}
139
+ variant={index === 0 ? "stampBottom1" : "stampBottom2"}
140
+ style={{
141
+ marginTop: index > 0 ? 2 : 0,
142
+ textAlign: 'center',
143
+ fontSize: index === 0 ? currentSize.bottom1 : currentSize.bottom2,
144
+ lineHeight: index === 0 ? currentSize.bottom1 + 2 : currentSize.bottom2 + 2,
145
+ }}
146
+ >
147
+ {label}
148
+ </Typography>
149
+ ))}
150
+ </View>
151
+ ) : null}
152
+
153
+ </View>
154
+ </View>
155
+ </View>
156
+ );
157
+ };
158
+
159
+ const styles = StyleSheet.create({
160
+ outerCircle: {
161
+ borderStyle: 'dashed',
162
+ justifyContent: 'center',
163
+ alignItems: 'center',
164
+ },
165
+ innerCircle: {
166
+ overflow: 'hidden',
167
+ },
168
+ contentWrapper: {
169
+ flex: 1,
170
+ justifyContent: 'center',
171
+ alignItems: 'center',
172
+ }
173
+ });
@@ -0,0 +1,275 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { View } from 'react-native';
4
+ import { StampCard } from './StampCard';
5
+ import { theme } from '../../../styles/theme';
6
+
7
+ /**
8
+ * \`StampCard\` é um contentor elegante que exibe um \`Stamp\` no topo e
9
+ * informações de contexto (Bandeira, Local, e Subtítulo) na base.
10
+ * É perfeito para ser usado como card de conquista ou progresso numa grelha.
11
+ *
12
+ * ### Características
13
+ * - **Modular:** Recebe diretamente as propriedades do \`Stamp\` interno.
14
+ * - **Design Alinhado:** A cor do local mapeia automaticamente com a cor do carimbo.
15
+ * - **Container Clean:** Borda redonda e fundo sólido.
16
+ */
17
+ const meta = {
18
+ title: 'Components/Cards/StampCard',
19
+ component: StampCard,
20
+ parameters: {
21
+ layout: 'centered',
22
+ docs: {
23
+ description: {
24
+ component: `O \`StampCard\` é um contentor elegante que exibe um \`Stamp\` no topo e informações de contexto (Bandeira, Local, e Subtítulo) na base.
25
+ É perfeito para ser usado como card de conquista ou progresso numa grelha.
26
+
27
+ ### Características
28
+ - **Modular:** Recebe diretamente as propriedades do \`Stamp\` interno.
29
+ - **Design Alinhado:** A cor do local mapeia automaticamente com a cor do carimbo.
30
+ - **Container Clean:** Borda redonda e fundo sólido.
31
+
32
+ ### Exemplo de Uso
33
+ \`\`\`tsx
34
+ import { StampCard } from '@/components/DataDisplay/StampCard/StampCard';
35
+ import { theme } from '@/styles/theme';
36
+
37
+ <StampCard
38
+ stamp={{
39
+ color: theme.colors.error,
40
+ title: 'PARIS',
41
+ topLabel: 'FRANCE',
42
+ bottomLabels: ['NIVEAU A1', 'ABR 2026'],
43
+ size: 'md',
44
+ position: 'center',
45
+ }}
46
+ countryCode="FR"
47
+ locationName="Paris"
48
+ subtitle="A1 • concluído"
49
+ variant="warning"
50
+ warningText="Visto a expirar!"
51
+ />
52
+ \`\`\`
53
+ `
54
+ }
55
+ }
56
+ },
57
+ tags: ['autodocs'],
58
+ argTypes: {
59
+ width: {
60
+ control: 'text',
61
+ description: 'Largura do cartão (ex: 320 ou "100%").',
62
+ },
63
+ height: {
64
+ control: 'text',
65
+ description: 'Altura do cartão.',
66
+ },
67
+ variant: {
68
+ control: 'radio',
69
+ options: ['default', 'highlight', 'warning'],
70
+ description: 'Variante visual do cartão.',
71
+ },
72
+ warningText: {
73
+ control: 'text',
74
+ description: 'Texto exibido no topo quando variant="warning".',
75
+ if: { arg: 'variant', eq: 'warning' },
76
+ },
77
+ countryCode: {
78
+ control: 'text',
79
+ description: 'Código ISO ou nome do país para gerar a bandeira.',
80
+ },
81
+ locationName: {
82
+ control: 'text',
83
+ description: 'Nome do local em destaque.',
84
+ },
85
+ subtitle: {
86
+ control: 'text',
87
+ description: 'Subtítulo cinzento/castanho inferior.',
88
+ },
89
+ },
90
+ } satisfies Meta<typeof StampCard>;
91
+
92
+ export default meta;
93
+ type Story = StoryObj<typeof meta>;
94
+
95
+ /**
96
+ * Exemplo padrão conforme a especificação do design.
97
+ */
98
+ export const Default: Story = {
99
+ args: {
100
+ stamp: {
101
+ color: theme.colors.error,
102
+ title: 'LYON',
103
+ topLabel: 'FRANCE',
104
+ bottomLabels: ['NIVEAU A1', 'ABR 2026'],
105
+ size: 'md',
106
+ position: 'center',
107
+ },
108
+ countryCode: 'FR',
109
+ locationName: 'Lyon',
110
+ subtitle: 'A1 • concluído',
111
+ variant: 'default',
112
+ },
113
+ };
114
+
115
+ /**
116
+ * Exemplo de um cartão em destaque (highlight) com a borda espessa
117
+ * que herda a cor do carimbo interno.
118
+ */
119
+ export const Highlight: Story = {
120
+ args: {
121
+ stamp: {
122
+ color: theme.colors.primary,
123
+ title: 'DAKAR',
124
+ topLabel: 'SÉNÉGAL',
125
+ bottomLabels: ['NIVEAU B1', 'EM CURSO'],
126
+ size: 'md',
127
+ position: 'center',
128
+ },
129
+ countryCode: 'SN',
130
+ locationName: 'Dakar',
131
+ subtitle: 'B1 • em curso',
132
+ variant: 'highlight',
133
+ },
134
+ };
135
+
136
+ /**
137
+ * Exemplo de um cartão com aviso crítico (warning),
138
+ * sobrepondo uma badge no topo.
139
+ */
140
+ export const Warning: Story = {
141
+ args: {
142
+ stamp: {
143
+ color: theme.colors.primaryDark,
144
+ title: 'BRUXELLES',
145
+ topLabel: 'BELGIQUE',
146
+ bottomLabels: ['NIVEAU A2', 'JUL 2026'],
147
+ size: 'md',
148
+ position: 'left',
149
+ },
150
+ countryCode: 'BE',
151
+ locationName: 'Bruxelles',
152
+ subtitle: 'A2 • renovar visto',
153
+ variant: 'warning',
154
+ warningText: 'Visto a expirar!',
155
+ },
156
+ };
157
+
158
+ /**
159
+ * Exemplo demonstrando o card num estado de progresso contínuo
160
+ * (Ex: ainda em curso).
161
+ */
162
+ export const InProgress: Story = {
163
+ args: {
164
+ stamp: {
165
+ color: theme.colors.primary,
166
+ title: 'DAKAR',
167
+ topLabel: 'SÉNÉGAL',
168
+ bottomLabels: ['NIVEAU B1', 'EM CURSO'],
169
+ size: 'md',
170
+ position: 'left',
171
+ },
172
+ countryCode: 'SN',
173
+ locationName: 'Dakar',
174
+ subtitle: 'B1 • a decorrer',
175
+ },
176
+ };
177
+
178
+ /**
179
+ * Grelha de vários cards para testar a escalabilidade visual
180
+ * e mostrar como se comportam lado a lado.
181
+ */
182
+ export const Grid: Story = {
183
+ render: () => (
184
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 24, justifyContent: 'center' }}>
185
+ <StampCard
186
+ stamp={{
187
+ color: theme.colors.info,
188
+ title: 'PARIS',
189
+ topLabel: 'FRANCE',
190
+ bottomLabels: ['NIVEAU A2'],
191
+ size: 'md',
192
+ position: 'right',
193
+ }}
194
+ countryCode="FR"
195
+ locationName="Paris"
196
+ subtitle="A2 • concluído"
197
+ />
198
+ <StampCard
199
+ stamp={{
200
+ color: theme.colors.success,
201
+ title: 'GENEVE',
202
+ topLabel: 'SUISSE',
203
+ bottomLabels: ['NIVEAU C1'],
204
+ size: 'md',
205
+ position: 'left',
206
+ }}
207
+ countryCode="CH"
208
+ locationName="Geneve"
209
+ subtitle="C1 • em curso"
210
+ variant="highlight"
211
+ />
212
+ <StampCard
213
+ stamp={{
214
+ color: theme.colors.error,
215
+ title: 'ROMA',
216
+ topLabel: 'ITALIA',
217
+ bottomLabels: ['NIVEAU B2'],
218
+ size: 'md',
219
+ position: 'center',
220
+ }}
221
+ countryCode="IT"
222
+ locationName="Roma"
223
+ subtitle="B2 • atrasado"
224
+ variant="warning"
225
+ warningText="Atenção necessária"
226
+ />
227
+ </View>
228
+ ),
229
+ };
230
+
231
+ /**
232
+ * Exemplo demonstrando a passagem de uma \`width\` customizada.
233
+ * O cartão estica-se para preencher a largura fornecida, mantendo
234
+ * os elementos internos perfeitamente centrados.
235
+ */
236
+ export const CustomWidth: Story = {
237
+ args: {
238
+ width: 320,
239
+ stamp: {
240
+ color: theme.colors.primary,
241
+ title: 'LISBOA',
242
+ topLabel: 'PORTUGAL',
243
+ bottomLabels: ['NIVEAU C2'],
244
+ size: 'lg',
245
+ position: 'center',
246
+ },
247
+ countryCode: 'PT',
248
+ locationName: 'Lisboa',
249
+ subtitle: 'C2 • concluído',
250
+ variant: 'default',
251
+ },
252
+ };
253
+
254
+ /**
255
+ * Exemplo demonstrando a passagem de uma \`height\` customizada.
256
+ * O cartão estica-se verticalmente, e o carimbo afasta-se das informações
257
+ * graças ao \`justifyContent: 'space-between'\`.
258
+ */
259
+ export const CustomHeight: Story = {
260
+ args: {
261
+ height: 450,
262
+ stamp: {
263
+ color: theme.colors.success,
264
+ title: 'ZURICH',
265
+ topLabel: 'SUISSE',
266
+ bottomLabels: ['NIVEAU B2'],
267
+ size: 'lg',
268
+ position: 'right',
269
+ },
270
+ countryCode: 'CH',
271
+ locationName: 'Zurich',
272
+ subtitle: 'B2 • concluído',
273
+ variant: 'default',
274
+ },
275
+ };
@@ -0,0 +1,196 @@
1
+ import { View, StyleSheet } from 'react-native';
2
+ import type { ViewStyle, DimensionValue } from 'react-native';
3
+ import { Typography } from '../../Typography/Typography';
4
+ import { Stamp } from '../Stamp/Stamp';
5
+ import { Flag } from '../Flag/Flag';
6
+ import { BaseCard } from '../../Cards/BaseCard/BaseCard';
7
+ import { theme } from '../../../styles/theme';
8
+
9
+ /**
10
+ * O \`StampCard\` é um cartão desenhado para exibir o progresso ou conquista num destino (carimbo).
11
+ * Usa o \`BaseCard\` como base, herda bordas e fundos do Design System, e disponibiliza 3 variantes visuais.
12
+ *
13
+ * ### Exemplo de Uso
14
+ * \`\`\`tsx
15
+ * import { StampCard } from '@/components/DataDisplay/StampCard/StampCard';
16
+ * import { theme } from '@/styles/theme';
17
+ *
18
+ * <StampCard
19
+ * stamp={{
20
+ * color: theme.colors.error,
21
+ * title: 'PARIS',
22
+ * topLabel: 'FRANCE',
23
+ * bottomLabels: ['NIVEAU A1', 'ABR 2026'],
24
+ * size: 'md',
25
+ * position: 'center',
26
+ * }}
27
+ * countryCode="FR"
28
+ * locationName="Paris"
29
+ * subtitle="A1 • concluído"
30
+ * variant="warning"
31
+ * warningText="Visto a expirar!"
32
+ * />
33
+ * \`\`\`
34
+ */
35
+ export interface StampCardProps {
36
+ /**
37
+ * Propriedades para configurar o carimbo que aparece dentro do card.
38
+ */
39
+ stamp: {
40
+ color: string;
41
+ title: string;
42
+ topLabel?: string;
43
+ bottomLabels?: string[];
44
+ size?: 'sm' | 'md' | 'lg';
45
+ position?: 'left' | 'center' | 'right';
46
+ };
47
+ /**
48
+ * Código ISO do país ou nome para exibir a bandeira (ex: 'FR', 'France').
49
+ * Usa a componente Flag do sistema.
50
+ */
51
+ countryCode?: string;
52
+ /**
53
+ * Nome do local que aparece ao lado da bandeira (ex: 'Lyon').
54
+ * Fica com a mesma cor principal do carimbo.
55
+ */
56
+ locationName: string;
57
+ /**
58
+ * Subtítulo informativo (ex: 'A1 • concluído').
59
+ */
60
+ subtitle: string;
61
+ /**
62
+ * Largura customizada para o card.
63
+ */
64
+ width?: DimensionValue;
65
+ /**
66
+ * Altura customizada para o card.
67
+ */
68
+ height?: DimensionValue;
69
+ /**
70
+ * Variante visual do cartão:
71
+ * - `default`: Borda subtil neutra.
72
+ * - `highlight`: Borda espessa com a mesma cor do carimbo.
73
+ * - `warning`: Idêntico ao highlight, mas com badge a sobrepor no topo.
74
+ */
75
+ variant?: 'default' | 'highlight' | 'warning';
76
+ /**
77
+ * Texto para a badge de aviso (apenas renderizada se variant='warning').
78
+ * Opcional, por defeito será 'Visto a expirar!'.
79
+ */
80
+ warningText?: string;
81
+ /**
82
+ * Estilo customizado para o card (opcional).
83
+ */
84
+ style?: ViewStyle;
85
+ }
86
+
87
+ export const StampCard = ({
88
+ stamp,
89
+ countryCode,
90
+ locationName,
91
+ subtitle,
92
+ width,
93
+ height,
94
+ variant = 'default',
95
+ warningText = 'Visto a expirar!',
96
+ style,
97
+ }: StampCardProps) => {
98
+
99
+ const isHighlight = variant === 'highlight' || variant === 'warning';
100
+ const resolvedBorderColor = isHighlight ? stamp.color : theme.colors.brown[100];
101
+ const resolvedBorderWidth = isHighlight ? 4 : 2;
102
+
103
+ // Proteção contra inputs inválidos do painel do Storybook (ex: objetos vazios ou strings vazias)
104
+ const safeWidth = (typeof width === 'object' || width === '') ? undefined : width as DimensionValue;
105
+ const safeHeight = (typeof height === 'object' || height === '') ? undefined : height as DimensionValue;
106
+
107
+ return (
108
+ <BaseCard
109
+ width={safeWidth}
110
+ height={safeHeight}
111
+ borderColor={resolvedBorderColor}
112
+ borderWidth={resolvedBorderWidth}
113
+ borderRadius={theme.radius.xxl}
114
+ overflow="visible"
115
+ style={[
116
+ styles.card,
117
+ variant === 'warning' && { paddingTop: 44 }, // Espaço extra para compensar a tag flutuante
118
+ style
119
+ ]}
120
+ >
121
+
122
+ {variant === 'warning' ? (
123
+ <View style={[styles.warningBadge, { backgroundColor: stamp.color }]}>
124
+ <Typography variant="labelHeavy" color={theme.colors.white} style={{ textTransform: 'none' }}>
125
+ {warningText}
126
+ </Typography>
127
+ </View>
128
+ ) : null}
129
+
130
+ {/* Stamp Container */}
131
+ <View style={styles.stampContainer}>
132
+ <Stamp {...stamp} />
133
+ </View>
134
+
135
+ {/* Info Container */}
136
+ <View style={styles.infoContainer}>
137
+
138
+ <View style={styles.locationRow}>
139
+ {countryCode ? (
140
+ <Flag
141
+ countryCode={countryCode}
142
+ size={20}
143
+ style={{ marginRight: theme.spacing.xs }}
144
+ />
145
+ ) : null}
146
+ <Typography
147
+ variant="h5"
148
+ color={stamp.color}
149
+ >
150
+ {locationName}
151
+ </Typography>
152
+ </View>
153
+
154
+ <Typography
155
+ variant="subtitle1"
156
+ color={theme.colors.brown[500]}
157
+ style={{ marginTop: 2 }}
158
+ >
159
+ {subtitle}
160
+ </Typography>
161
+
162
+ </View>
163
+
164
+ </BaseCard>
165
+ );
166
+ };
167
+
168
+ const styles = StyleSheet.create({
169
+ card: {
170
+ padding: theme.spacing.space8,
171
+ alignItems: 'center',
172
+ justifyContent: 'space-between',
173
+ alignSelf: 'flex-start', // Adapta-se ao conteúdo por defeito
174
+ },
175
+ warningBadge: {
176
+ position: 'absolute',
177
+ top: -18, // Sobe para quebrar a borda superior (ajustado para a nova borda branca)
178
+ alignSelf: 'center',
179
+ paddingHorizontal: 16,
180
+ paddingVertical: 6,
181
+ borderRadius: theme.radius.full,
182
+ zIndex: 10,
183
+ borderWidth: 4,
184
+ borderColor: theme.colors.white,
185
+ },
186
+ stampContainer: {
187
+ marginBottom: theme.spacing.space6,
188
+ },
189
+ infoContainer: {
190
+ alignItems: 'center',
191
+ },
192
+ locationRow: {
193
+ flexDirection: 'row',
194
+ alignItems: 'center',
195
+ },
196
+ });
@@ -67,6 +67,13 @@ export const MapScreen = () => {
67
67
  control: 'radio',
68
68
  options: ['continuous', 'segmented'],
69
69
  },
70
+ variant: {
71
+ control: 'radio',
72
+ options: ['default', 'minimal'],
73
+ },
74
+ notificationCount: {
75
+ control: 'number',
76
+ },
70
77
  },
71
78
  } satisfies Meta<typeof RouteHeader>;
72
79
 
@@ -74,6 +81,7 @@ export default meta;
74
81
  type Story = StoryObj<typeof meta>;
75
82
 
76
83
  export const Continuous: Story = {
84
+ render: (args) => <RouteHeader {...args} />,
77
85
  args: {
78
86
  routeLabel: 'ROTA DA FRANCOFONIA',
79
87
  countryCode: 'SN',
@@ -88,6 +96,7 @@ export const Continuous: Story = {
88
96
  };
89
97
 
90
98
  export const Segmented: Story = {
99
+ render: (args) => <RouteHeader {...args} />,
91
100
  args: {
92
101
  routeLabel: 'ROTA DA FRANCOFONIA',
93
102
  stepLabel: 'paragem 5 de 8',
@@ -102,3 +111,22 @@ export const Segmented: Story = {
102
111
  safeAreaTop: 20,
103
112
  },
104
113
  };
114
+
115
+ export const Minimal: Story = {
116
+ render: (args) => <RouteHeader {...args} />,
117
+ args: {
118
+ routeLabel: 'Francofonia',
119
+ stepLabel: '5/8',
120
+ countryCode: 'SN',
121
+ title: 'Dakar',
122
+ subtitle: 'B2',
123
+ progressText: '',
124
+ progressSubText: '',
125
+ progress: 0.625,
126
+ progressType: 'segmented',
127
+ totalSegments: 8,
128
+ variant: 'minimal',
129
+ notificationCount: 3,
130
+ safeAreaTop: 20,
131
+ },
132
+ };
@@ -5,6 +5,7 @@ import { theme } from '../../../styles/theme';
5
5
  import { Flag } from '../../DataDisplay/Flag/Flag';
6
6
  import { ProgressBar } from '../../DataDisplay/ProgressBar/ProgressBar';
7
7
  import { Typography } from '../../Typography/Typography';
8
+ import { IconButton } from '../../Buttons/IconButton/IconButton';
8
9
 
9
10
  /**
10
11
  * Propriedades para o componente RouteHeader.
@@ -69,6 +70,19 @@ export interface RouteHeaderProps {
69
70
  * Deve receber o `insets.top` devolvido pelo hook `useSafeAreaInsets()`.
70
71
  */
71
72
  safeAreaTop?: number;
73
+ /**
74
+ * Variante visual do cabeçalho.
75
+ * - `default`: Mostra o progresso em texto gigante à direita (ex: 63%).
76
+ * - `minimal`: Oculta o progresso em texto e pode mostrar ações contextuais à direita (ex: sino e globo).
77
+ * @default 'default'
78
+ */
79
+ variant?: 'default' | 'minimal';
80
+ /** Contagem de notificações não lidas. Requer variant="minimal" */
81
+ notificationCount?: number;
82
+ /** Callback ao clicar no sino de notificações. Requer variant="minimal" */
83
+ onNotificationPress?: () => void;
84
+ /** Callback ao clicar no mapa/globo. Requer variant="minimal" */
85
+ onGlobePress?: () => void;
72
86
  /**
73
87
  * Estilos adicionais para o contentor exterior (ex: zIndex, position absolute).
74
88
  */
@@ -98,6 +112,10 @@ export const RouteHeader = ({
98
112
  progress,
99
113
  totalSegments = 1,
100
114
  progressType = 'continuous',
115
+ variant = 'default',
116
+ notificationCount = 0,
117
+ onNotificationPress,
118
+ onGlobePress,
101
119
  safeAreaTop = 0,
102
120
  style,
103
121
  }: RouteHeaderProps) => {
@@ -109,14 +127,21 @@ export const RouteHeader = ({
109
127
  <View style={[styles.container, { paddingTop: safeAreaTop + scale(12), paddingHorizontal: scale(20), paddingBottom: scale(20) }, style]}>
110
128
  {/* Top Row: Labels */}
111
129
  <View style={[styles.topRow, { marginBottom: scale(10) }]}>
112
- <Typography variant="bodySmHeavy" color={theme.colors.brown[500]} style={{ textTransform: 'uppercase', letterSpacing: 0.5 }}>
113
- {routeLabel}
114
- </Typography>
115
- {stepLabel ? (
116
- <Typography variant="smallHeavy" color={theme.colors.brown[500]}>
117
- {stepLabel}
130
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
131
+ <Typography variant="bodySmHeavy" color={theme.colors.brown[500]} style={{ textTransform: 'uppercase', letterSpacing: 0.5 }}>
132
+ {routeLabel}
118
133
  </Typography>
119
- ) : null}
134
+ {stepLabel ? (
135
+ <>
136
+ <Typography variant="bodySmHeavy" color={theme.colors.brown[400]} style={{ marginHorizontal: scale(6) }}>
137
+
138
+ </Typography>
139
+ <Typography variant="bodySmHeavy" color={theme.colors.brown[500]}>
140
+ {stepLabel}
141
+ </Typography>
142
+ </>
143
+ ) : null}
144
+ </View>
120
145
  </View>
121
146
 
122
147
  {/* Middle Row: Flag, Title, Progress Text */}
@@ -135,14 +160,42 @@ export const RouteHeader = ({
135
160
  </Typography>
136
161
  </View>
137
162
 
138
- <View style={styles.progressTextContainer}>
139
- <Typography variant="h4" color={theme.colors.primary}>
140
- {progressText}
141
- </Typography>
142
- <Typography variant="smallHeavy" color={theme.colors.brown[500]}>
143
- {progressSubText}
144
- </Typography>
145
- </View>
163
+ {variant === 'default' && (
164
+ <View style={styles.progressTextContainer}>
165
+ <Typography variant="h4" color={theme.colors.primary}>
166
+ {progressText}
167
+ </Typography>
168
+ <Typography variant="smallHeavy" color={theme.colors.brown[500]}>
169
+ {progressSubText}
170
+ </Typography>
171
+ </View>
172
+ )}
173
+
174
+ {variant === 'minimal' && (
175
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: scale(8) }}>
176
+ <View style={{ zIndex: 10, elevation: 10 }}>
177
+ <IconButton
178
+ iconName="Bell"
179
+ variant="secondary"
180
+ size="sm"
181
+ iconColor={theme.colors.primary}
182
+ onPress={onNotificationPress}
183
+ />
184
+ {!!notificationCount && notificationCount > 0 && (
185
+ <View style={styles.badgeContainer}>
186
+ <Typography variant="smallHeavy" color={theme.colors.white}>{notificationCount > 99 ? '99+' : notificationCount}</Typography>
187
+ </View>
188
+ )}
189
+ </View>
190
+ <IconButton
191
+ iconName="Globe"
192
+ variant="secondary"
193
+ size="sm"
194
+ iconColor={theme.colors.brown[800]}
195
+ onPress={onGlobePress}
196
+ />
197
+ </View>
198
+ )}
146
199
  </View>
147
200
 
148
201
  {/* Bottom Row: Progress Bar */}
@@ -217,4 +270,20 @@ const styles = StyleSheet.create({
217
270
  alignItems: 'flex-end',
218
271
  justifyContent: 'center',
219
272
  },
273
+ badgeContainer: {
274
+ position: 'absolute',
275
+ top: -4,
276
+ right: -4,
277
+ backgroundColor: theme.colors.error,
278
+ borderRadius: 10,
279
+ minWidth: 18,
280
+ height: 18,
281
+ justifyContent: 'center',
282
+ alignItems: 'center',
283
+ paddingHorizontal: 4,
284
+ borderWidth: 1.5,
285
+ borderColor: theme.colors.white,
286
+ zIndex: 10,
287
+ elevation: 10,
288
+ }
220
289
  });
@@ -17,6 +17,13 @@ const meta = {
17
17
  }
18
18
  },
19
19
  tags: ['autodocs'],
20
+ argTypes: {
21
+ variant: {
22
+ control: 'radio',
23
+ options: ['default', 'brown'],
24
+ description: 'Variante visual dos pontos.',
25
+ },
26
+ },
20
27
  decorators: [
21
28
  (Story) => (
22
29
  <View style={{ padding: 40, backgroundColor: theme.colors.cream }}>
@@ -30,16 +37,21 @@ export default meta;
30
37
  type Story = StoryObj<any>;
31
38
 
32
39
  export const Default: Story = {
33
- args: { totalPages: 4, currentPage: 1 } as any,
40
+ args: { totalPages: 4, currentPage: 1, variant: 'default' } as any,
41
+ render: (args: any) => <PaginationDots {...args} />,
42
+ };
43
+
44
+ export const Brown: Story = {
45
+ args: { totalPages: 4, currentPage: 1, variant: 'brown' } as any,
34
46
  render: (args: any) => <PaginationDots {...args} />,
35
47
  };
36
48
 
37
- const InteractiveDots = ({ totalPages }: { totalPages: number }) => {
49
+ const InteractiveDots = ({ totalPages, variant }: { totalPages: number, variant?: 'default' | 'brown' }) => {
38
50
  const [currentPage, setCurrentStep] = useState(0);
39
51
 
40
52
  return (
41
53
  <View style={{ alignItems: 'center' }}>
42
- <PaginationDots totalPages={totalPages} currentPage={currentPage} />
54
+ <PaginationDots totalPages={totalPages} currentPage={currentPage} variant={variant} />
43
55
 
44
56
  <View style={{ flexDirection: 'row', gap: 20, marginTop: 20 }}>
45
57
  <Button
@@ -58,5 +70,9 @@ const InteractiveDots = ({ totalPages }: { totalPages: number }) => {
58
70
  };
59
71
 
60
72
  export const Interactive: Story = {
61
- render: () => <InteractiveDots totalPages={4} />,
73
+ render: () => <InteractiveDots totalPages={4} variant="default" />,
74
+ };
75
+
76
+ export const InteractiveBrown: Story = {
77
+ render: () => <InteractiveDots totalPages={4} variant="brown" />,
62
78
  };
@@ -17,27 +17,41 @@ export interface PaginationDotsProps {
17
17
  inactiveColor?: string;
18
18
  /** Cor customizada do ponto ativo (opcional). */
19
19
  activeColor?: string;
20
+ /**
21
+ * Variante visual dos pontos.
22
+ * - `default`: Ponto ativo longo (primário), inativos redondos.
23
+ * - `brown`: Cores da paleta castanha, pontos inativos mais largos (em formato de traço curto).
24
+ */
25
+ variant?: 'default' | 'brown';
20
26
  style?: ViewStyle | ViewStyle[];
21
27
  }
22
28
 
23
29
  interface DotProps {
24
30
  isActive: boolean;
31
+ variant: 'default' | 'brown';
25
32
  }
26
33
 
27
- const Dot = ({ isActive }: DotProps) => {
34
+ const Dot = ({ isActive, variant }: DotProps) => {
28
35
  const { scale } = useResponsive();
29
36
 
30
- // Animação da largura: Inativo (6px) -> Ativo (20px)
31
- const widthAnim = useRef(new Animated.Value(isActive ? scale(20) : scale(6))).current;
37
+ // Se for 'brown', os inativos são mais largos (traços curtos em vez de círculos perfeitos)
38
+ const inactiveWidth = variant === 'brown' ? scale(12) : scale(6);
39
+ const activeWidth = scale(20);
40
+
41
+ const activeColor = variant === 'brown' ? theme.colors.brown[500] : theme.colors.primary;
42
+ const inactiveColor = theme.colors.brown[100]; // Bolinhas cinzas inativas mantêm a cor padrão (brown[100])
43
+
44
+ // Animação da largura
45
+ const widthAnim = useRef(new Animated.Value(isActive ? activeWidth : inactiveWidth)).current;
32
46
 
33
47
  useEffect(() => {
34
48
  Animated.timing(widthAnim, {
35
- toValue: isActive ? scale(20) : scale(6),
49
+ toValue: isActive ? activeWidth : inactiveWidth,
36
50
  duration: 250,
37
- easing: Easing.bezier(0.175, 0.885, 0.32, 1.275), // Efeito "ease-bounce" semelhante ao CSS
51
+ easing: Easing.bezier(0.175, 0.885, 0.32, 1.275), // Efeito "ease-bounce"
38
52
  useNativeDriver: false, // Animating width requires useNativeDriver: false
39
53
  }).start();
40
- }, [isActive, widthAnim, scale]);
54
+ }, [isActive, widthAnim, activeWidth, inactiveWidth]);
41
55
 
42
56
  return (
43
57
  <Animated.View
@@ -47,14 +61,14 @@ const Dot = ({ isActive }: DotProps) => {
47
61
  width: widthAnim,
48
62
  height: scale(6),
49
63
  borderRadius: scale(3),
50
- backgroundColor: isActive ? theme.colors.primary : theme.colors.brown[100],
64
+ backgroundColor: isActive ? activeColor : inactiveColor,
51
65
  },
52
66
  ]}
53
67
  />
54
68
  );
55
69
  };
56
70
 
57
- export const PaginationDots = ({ totalPages, currentPage, style }: PaginationDotsProps) => {
71
+ export const PaginationDots = ({ totalPages, currentPage, variant = 'default', style }: PaginationDotsProps) => {
58
72
  const { scale } = useResponsive();
59
73
 
60
74
  // Garantir que não crasha caso passem steps negativos
@@ -63,7 +77,7 @@ export const PaginationDots = ({ totalPages, currentPage, style }: PaginationDot
63
77
  return (
64
78
  <View style={[styles.container, { gap: scale(7), marginBottom: scale(24) }, style]}>
65
79
  {Array.from({ length: totalPages }).map((_, index) => (
66
- <Dot key={index} isActive={index === safeCurrentStep} />
80
+ <Dot key={index} isActive={index === safeCurrentStep} variant={variant} />
67
81
  ))}
68
82
  </View>
69
83
  );
package/src/index.ts CHANGED
@@ -27,6 +27,8 @@ export * from './components/DataDisplay/InstructionBubble/InstructionBubble';
27
27
  export * from './components/DataDisplay/LevelBadge/LevelBadge';
28
28
  export * from './components/DataDisplay/NumberBadge/NumberBadge';
29
29
  export * from './components/DataDisplay/ProgressBar/ProgressBar';
30
+ export * from './components/DataDisplay/Stamp/Stamp';
31
+ export * from './components/DataDisplay/StampCard/StampCard';
30
32
  export * from './components/DataDisplay/StatBadge/StatBadge';
31
33
  export * from './components/DataDisplay/Tag/Tag';
32
34
  export * from './components/Feedback/BottomSheet/BottomSheet';
@@ -234,6 +234,40 @@ export const theme = {
234
234
  desktopFontSize: 16,
235
235
  },
236
236
 
237
+ // FAMÍLIA SERIF (Elementos Clássicos, Carimbos)
238
+ stampTop: {
239
+ ...font('IM Fell English', '400'),
240
+ fontStyle: 'italic',
241
+ fontSize: 12,
242
+ lineHeight: 14,
243
+ letterSpacing: 2.5,
244
+ textTransform: 'uppercase',
245
+ desktopFontSize: 16,
246
+ },
247
+ stampTitle: {
248
+ ...font('Fredoka-SemiBold', '600'),
249
+ fontSize: 24,
250
+ lineHeight: 28,
251
+ textTransform: 'uppercase',
252
+ desktopFontSize: 28,
253
+ },
254
+ stampBottom1: {
255
+ ...font('Nunito-Regular', '400'),
256
+ fontSize: 9,
257
+ lineHeight: 12,
258
+ letterSpacing: 2,
259
+ textTransform: 'uppercase',
260
+ desktopFontSize: 10,
261
+ },
262
+ stampBottom2: {
263
+ ...font('Nunito-Regular', '400'),
264
+ fontSize: 7,
265
+ lineHeight: 10,
266
+ letterSpacing: 0.5,
267
+ textTransform: 'uppercase',
268
+ desktopFontSize: 8,
269
+ },
270
+
237
271
  // FAMÍLIA NUNITO (Corpo, Labels, Listas e Infos)
238
272
  bodyLgHeavy: {
239
273
  ...font('Nunito-Black', '900'),
@@ -1,4 +1,4 @@
1
- import { Play, Volume2, Volume1, ArrowRight, X, Check, ChevronRight, ChevronLeft, Info, AlertTriangle, Book, Mic, Home, Compass, GraduationCap, FolderOpen, User, ShoppingCart, FileText, Wallet } from 'lucide-react-native';
1
+ import { Play, Volume2, Volume1, ArrowRight, X, Check, ChevronRight, ChevronLeft, Info, AlertTriangle, Book, Mic, Home, Compass, GraduationCap, FolderOpen, User, ShoppingCart, FileText, Wallet, Bell, Globe } from 'lucide-react-native';
2
2
 
3
3
  /**
4
4
  * Para evitar que o Vite e o Metro (React Native) tentem empacotar os 1400+ ícones
@@ -29,4 +29,6 @@ export const IconMap: Record<string, any> = {
29
29
  ShoppingCart,
30
30
  FileText,
31
31
  Wallet,
32
+ Bell,
33
+ Globe,
32
34
  };