@xetwa/design-system 1.0.41 → 1.0.43

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.41",
3
+ "version": "1.0.43",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,104 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { View } from 'react-native';
3
+ import { RouteHeader } from './RouteHeader';
4
+ import { theme } from '../../../styles/theme';
5
+
6
+ const meta = {
7
+ title: 'Components/Headers/RouteHeader',
8
+ component: RouteHeader,
9
+ parameters: {
10
+ layout: 'fullscreen',
11
+ docs: {
12
+ description: {
13
+ component: `O **RouteHeader** é o cabeçalho principal (HUD) para ecrãs de percurso/navegação num mapa, substituindo a navegação clássica com setas de voltar.
14
+
15
+ ### Caso de Uso Principal
16
+ Utilizado no topo do mapa para informar o utilizador:
17
+ 1. Em que **país/módulo** se encontra (bandeira grande + nome da cidade).
18
+ 2. Qual o seu **nível e evolução** atual (ex: A1 → C1, 63%).
19
+ 3. Como está o seu **progresso**, que pode ser exibido de forma fluida (\`continuous\`) ou em paragens discretas (\`segmented\`).
20
+
21
+ ### Lidar com a SafeArea (Notch / Status Bar)
22
+ Este componente estende-se até ao limite superior físico do telemóvel. Deves passar a propriedade \`safeAreaTop\` com o valor gerado pelo hook \`useSafeAreaInsets()\` do \`react-native-safe-area-context\`.
23
+
24
+ ### Exemplo de Código
25
+ \`\`\`tsx
26
+ import { RouteHeader } from '@xetwa/design-system';
27
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
28
+ import { View } from 'react-native';
29
+
30
+ export const MapScreen = () => {
31
+ const insets = useSafeAreaInsets();
32
+
33
+ return (
34
+ <View style={{ flex: 1 }}>
35
+ <RouteHeader
36
+ routeLabel="ROTA DA FRANCOFONIA"
37
+ stepLabel="paragem 5 de 8"
38
+ countryCode="SN"
39
+ title="Dakar"
40
+ subtitle="Nível B2 · estás aqui"
41
+ progressText="63%"
42
+ progressSubText="A1 → C1"
43
+ progress={0.63}
44
+ progressType="segmented"
45
+ totalSegments={8}
46
+ safeAreaTop={insets.top}
47
+ />
48
+ {/* ... Mapa Embaixo ... */}
49
+ </View>
50
+ );
51
+ };
52
+ \`\`\`
53
+ `,
54
+ },
55
+ },
56
+ },
57
+ decorators: [
58
+ (Story) => (
59
+ <View style={{ flex: 1, backgroundColor: theme.colors.brown[100] }}>
60
+ <Story />
61
+ </View>
62
+ ),
63
+ ],
64
+ tags: ['autodocs'],
65
+ argTypes: {
66
+ progressType: {
67
+ control: 'radio',
68
+ options: ['continuous', 'segmented'],
69
+ },
70
+ },
71
+ } satisfies Meta<typeof RouteHeader>;
72
+
73
+ export default meta;
74
+ type Story = StoryObj<typeof meta>;
75
+
76
+ export const Continuous: Story = {
77
+ args: {
78
+ routeLabel: 'ROTA DA FRANCOFONIA',
79
+ countryCode: 'SN',
80
+ title: 'Dakar',
81
+ subtitle: 'Nível B2 · estás aqui',
82
+ progressText: '63%',
83
+ progressSubText: 'A1 → C1',
84
+ progress: 0.63,
85
+ progressType: 'continuous',
86
+ safeAreaTop: 20,
87
+ },
88
+ };
89
+
90
+ export const Segmented: Story = {
91
+ args: {
92
+ routeLabel: 'ROTA DA FRANCOFONIA',
93
+ stepLabel: 'paragem 5 de 8',
94
+ countryCode: 'SN',
95
+ title: 'Dakar',
96
+ subtitle: 'Nível B2 · estás aqui',
97
+ progressText: '63%',
98
+ progressSubText: 'A1 → C1',
99
+ progress: 0.63,
100
+ totalSegments: 8,
101
+ progressType: 'segmented',
102
+ safeAreaTop: 20,
103
+ },
104
+ };
@@ -0,0 +1,218 @@
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
+ import { Flag } from '../../DataDisplay/Flag/Flag';
6
+ import { ProgressBar } from '../../DataDisplay/ProgressBar/ProgressBar';
7
+ import { theme } from '../../../styles/theme';
8
+ import { useResponsive } from '../../../hooks/useResponsive';
9
+
10
+ /**
11
+ * Propriedades para o componente RouteHeader.
12
+ */
13
+ export interface RouteHeaderProps {
14
+ /**
15
+ * Label principal no topo esquerdo. Usado para indicar a rota global.
16
+ * @example "ROTA DA FRANCOFONIA"
17
+ */
18
+ routeLabel: string;
19
+ /**
20
+ * Label secundária no topo direito. Usada para indicar a etapa ou paragem atual no caminho.
21
+ * @example "paragem 5 de 8"
22
+ */
23
+ stepLabel?: string;
24
+ /**
25
+ * Código ISO Alpha-2 do país para a bandeira.
26
+ * A bandeira serve como o avatar principal ("estás aqui").
27
+ * @example "SN" (Senegal)
28
+ */
29
+ countryCode: string;
30
+ /**
31
+ * Título principal, geralmente o nome do destino ou cidade atual.
32
+ * @example "Dakar"
33
+ */
34
+ title: string;
35
+ /**
36
+ * Subtítulo por baixo do título. Mostra o nível ou estado do utilizador.
37
+ * @example "Nível B2 · estás aqui"
38
+ */
39
+ subtitle: string;
40
+ /**
41
+ * Percentagem ou valor métrico em formato de texto grande.
42
+ * @example "63%"
43
+ */
44
+ progressText: string;
45
+ /**
46
+ * Texto auxiliar debaixo da percentagem que descreve a evolução.
47
+ * @example "A1 → C1"
48
+ */
49
+ progressSubText: string;
50
+ /**
51
+ * Valor numérico absoluto do progresso total (entre 0.0 e 1.0).
52
+ * Controla a percentagem preenchida da barra (independente de ser segmentada).
53
+ */
54
+ progress: number;
55
+ /**
56
+ * Total de segmentos visíveis se a barra for do tipo "segmented".
57
+ * Representa o número total de paragens ou módulos de uma rota.
58
+ * @default 1
59
+ */
60
+ totalSegments?: number;
61
+ /**
62
+ * Determina a aparência da barra de progresso.
63
+ * - `continuous`: Barra lisa, ideal para indicar a percentagem de uma única etapa longa.
64
+ * - `segmented`: Barra dividida em secções, excelente para rotas com várias "paragens".
65
+ * @default 'continuous'
66
+ */
67
+ progressType?: 'continuous' | 'segmented';
68
+ /**
69
+ * Espaçamento extra no topo para evitar que o conteúdo fique por baixo da Notch/Status Bar.
70
+ * Deve receber o `insets.top` devolvido pelo hook `useSafeAreaInsets()`.
71
+ */
72
+ safeAreaTop?: number;
73
+ /**
74
+ * Estilos adicionais para o contentor exterior (ex: zIndex, position absolute).
75
+ */
76
+ style?: ViewStyle | ViewStyle[];
77
+ }
78
+
79
+ /**
80
+ * **RouteHeader**
81
+ *
82
+ * Cabeçalho principal para os ecrãs de percurso e navegação. Apresenta o progresso
83
+ * de aprendizagem do utilizador ao longo de uma "rota" cultural/geográfica.
84
+ *
85
+ * ### Casos de Uso
86
+ * - **Home do Percurso (Mapa Principal):** Usado sem setas de voltar, servindo de HUD de navegação global.
87
+ * Combina informações de localização (onde estou? -> Dakar) e informações de progresso (63%).
88
+ * - **Tipos de Progresso:** Pode apresentar o progresso fluido (`continuous`) para uma visão global, ou
89
+ * faseado por módulos/paragens (`segmented`) quando o foco é passar de um ponto para o outro num mapa.
90
+ */
91
+ export const RouteHeader = ({
92
+ routeLabel,
93
+ stepLabel,
94
+ countryCode,
95
+ title,
96
+ subtitle,
97
+ progressText,
98
+ progressSubText,
99
+ progress,
100
+ totalSegments = 1,
101
+ progressType = 'continuous',
102
+ safeAreaTop = 0,
103
+ style,
104
+ }: RouteHeaderProps) => {
105
+ const { scale } = useResponsive();
106
+
107
+ const segments = progressType === 'segmented' ? Math.max(1, totalSegments) : 1;
108
+
109
+ return (
110
+ <View style={[styles.container, { paddingTop: safeAreaTop + scale(20), paddingHorizontal: scale(24), paddingBottom: scale(24) }, style]}>
111
+ {/* Top Row: Labels */}
112
+ <View style={[styles.topRow, { marginBottom: scale(16) }]}>
113
+ <Typography variant="bodySm" color={theme.colors.brown[500]} style={{ textTransform: 'uppercase', letterSpacing: 0.5 }}>
114
+ {routeLabel}
115
+ </Typography>
116
+ {stepLabel ? (
117
+ <Typography variant="bodySmHeavy" color={theme.colors.brown[500]}>
118
+ {stepLabel}
119
+ </Typography>
120
+ ) : null}
121
+ </View>
122
+
123
+ {/* Middle Row: Flag, Title, Progress Text */}
124
+ <View style={[styles.middleRow, { marginBottom: scale(20) }]}>
125
+ {/* Bandeira com anel "estás aqui" e shadow 3D */}
126
+ <View style={[styles.flagWrapper, { width: scale(64), height: scale(64) }, styles.flagShadow]}>
127
+ <Flag countryCode={countryCode} size={scale(48)} />
128
+ </View>
129
+
130
+ <View style={[styles.titleContainer, { marginLeft: scale(16) }]}>
131
+ <Typography variant="h1" color={theme.colors.brown[800]}>
132
+ {title}
133
+ </Typography>
134
+ <Typography variant="bodyMdHeavy" color={theme.colors.primary}>
135
+ {subtitle}
136
+ </Typography>
137
+ </View>
138
+
139
+ <View style={styles.progressTextContainer}>
140
+ <Typography variant="h2" color={theme.colors.primary}>
141
+ {progressText}
142
+ </Typography>
143
+ <Typography variant="smallHeavy" color={theme.colors.brown[500]}>
144
+ {progressSubText}
145
+ </Typography>
146
+ </View>
147
+ </View>
148
+
149
+ {/* Bottom Row: Progress Bar */}
150
+ <ProgressBar
151
+ progress={progress}
152
+ segments={segments}
153
+ size="md"
154
+ variant="default" // Usa a cor primária (laranja)
155
+ />
156
+ </View>
157
+ );
158
+ };
159
+
160
+ const styles = StyleSheet.create({
161
+ container: {
162
+ backgroundColor: theme.colors.brown[50], // cream color matching the image
163
+ width: '100%',
164
+ zIndex: 10,
165
+ // Soft shadow to separate from content below
166
+ ...Platform.select({
167
+ web: {
168
+ boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.05)',
169
+ },
170
+ default: {
171
+ shadowColor: '#000',
172
+ shadowOffset: { width: 0, height: 4 },
173
+ shadowOpacity: 0.05,
174
+ shadowRadius: 8,
175
+ elevation: 4,
176
+ }
177
+ }),
178
+ },
179
+ topRow: {
180
+ flexDirection: 'row',
181
+ justifyContent: 'space-between',
182
+ alignItems: 'center',
183
+ },
184
+ middleRow: {
185
+ flexDirection: 'row',
186
+ alignItems: 'center',
187
+ },
188
+ flagWrapper: {
189
+ borderRadius: 999,
190
+ borderWidth: 2.5,
191
+ borderColor: theme.colors.primary, // Anel laranja que significa "estás aqui"
192
+ justifyContent: 'center',
193
+ alignItems: 'center',
194
+ backgroundColor: theme.colors.white,
195
+ },
196
+ flagShadow: {
197
+ ...Platform.select({
198
+ web: {
199
+ boxShadow: `0px 4px 0px ${theme.colors.brown[100]}`,
200
+ },
201
+ default: {
202
+ shadowColor: theme.colors.brown[100],
203
+ shadowOffset: { width: 0, height: 4 },
204
+ shadowOpacity: 1,
205
+ shadowRadius: 0,
206
+ elevation: 4,
207
+ }
208
+ })
209
+ },
210
+ titleContainer: {
211
+ flex: 1,
212
+ justifyContent: 'center',
213
+ },
214
+ progressTextContainer: {
215
+ alignItems: 'flex-end',
216
+ justifyContent: 'center',
217
+ },
218
+ });
@@ -135,7 +135,7 @@ const TabBarItemComponent = ({
135
135
  variant="smallHeavy"
136
136
  style={[
137
137
  styles.label,
138
- { color, fontSize: scaleText(9.5) },
138
+ { color, fontSize: scaleText(11.5) },
139
139
  variant === 'C' && { marginBottom: scale(4) }
140
140
  ]}
141
141
  >
@@ -162,7 +162,7 @@ export const TabBar = ({
162
162
  const { scale, scaleText, width } = useResponsive();
163
163
  const isDesktop = width >= 768;
164
164
 
165
- const iconSize = isDesktop ? scale(22) : scale(21);
165
+ const iconSize = isDesktop ? scale(30) : scale(26);
166
166
  const bottomPadding = Math.max(scale(14), safeAreaBottom);
167
167
 
168
168
  return (
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ export * from './components/Forms/Switch/Switch';
40
40
  export * from './components/Forms/TimeSlot/TimeSlot';
41
41
  export * from './components/Headers/CountryHeader/CountryHeader';
42
42
  export * from './components/Headers/ProfileHeader/ProfileHeader';
43
+ export * from './components/Headers/RouteHeader/RouteHeader';
43
44
  export * from './components/Inputs/FillInTheBlank/FillInTheBlank';
44
45
  export * from './components/Inputs/TextInput/TextInput';
45
46
  export * from './components/Mascots/Vava/Vava';
@@ -5,7 +5,7 @@ const font = (family: string, webWeight: '400' | '500' | '600' | '700' | '800' |
5
5
  const webFamily = family.split('-')[0];
6
6
  return Platform.select({
7
7
  web: { fontFamily: webFamily, fontWeight: webWeight },
8
- default: { fontFamily: family, fontWeight: webWeight }
8
+ default: { fontFamily: family }
9
9
  }) as { fontFamily: string; fontWeight?: '400' | '500' | '600' | '700' | '800' | '900' };
10
10
  };
11
11
 
@@ -236,31 +236,37 @@ export const theme = {
236
236
 
237
237
  // FAMÍLIA NUNITO (Corpo, Labels, Listas e Infos)
238
238
  bodyLgHeavy: {
239
- ...font('Nunito-ExtraBold', '800'),
239
+ ...font('Nunito-Black', '900'),
240
+ fontSize: 16,
241
+ lineHeight: 24,
242
+ desktopFontSize: 18,
243
+ },
244
+ bodyLg: {
245
+ ...font('Nunito-Black', '900'),
240
246
  fontSize: 16,
241
247
  lineHeight: 24,
242
248
  desktopFontSize: 18,
243
249
  },
244
250
  bodyMdHeavy: {
245
- ...font('Nunito-ExtraBold', '800'),
251
+ ...font('Nunito-Black', '900'),
246
252
  fontSize: 15,
247
253
  lineHeight: 22,
248
254
  desktopFontSize: 16,
249
255
  },
250
256
  bodyMd: {
251
- ...font('Nunito-SemiBold', '600'),
257
+ ...font('Nunito-Black', '900'),
252
258
  fontSize: 15,
253
259
  lineHeight: 22,
254
260
  desktopFontSize: 16,
255
261
  },
256
262
  bodySmHeavy: {
257
- ...font('Nunito-ExtraBold', '800'),
263
+ ...font('Nunito-Black', '900'),
258
264
  fontSize: 13.5,
259
265
  lineHeight: 20,
260
266
  desktopFontSize: 15,
261
267
  },
262
268
  bodySm: {
263
- ...font('Nunito-Bold', '700'),
269
+ ...font('Nunito-Black', '900'),
264
270
  fontSize: 13.5,
265
271
  lineHeight: 20,
266
272
  desktopFontSize: 15,
@@ -295,7 +301,7 @@ export const theme = {
295
301
  desktopFontSize: 12,
296
302
  },
297
303
  smallHeavy: {
298
- ...font('Nunito-ExtraBold', '800'),
304
+ ...font('Nunito-Black', '900'),
299
305
  fontSize: 10,
300
306
  lineHeight: 14,
301
307
  desktopFontSize: 11,
@@ -1,5 +1,4 @@
1
- import { FileText } from 'lucide-react';
2
- import { Play, Volume2, Volume1, ArrowRight, X, Check, ChevronRight, ChevronLeft, Info, AlertTriangle, Book, Mic, Home, Compass, GraduationCap, FolderOpen, User, ShoppingCart } 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 } from 'lucide-react-native';
3
2
 
4
3
  /**
5
4
  * Para evitar que o Vite e o Metro (React Native) tentem empacotar os 1400+ ícones
@@ -29,4 +28,5 @@ export const IconMap: Record<string, any> = {
29
28
  User,
30
29
  ShoppingCart,
31
30
  FileText,
31
+ Wallet,
32
32
  };