@xetwa/design-system 1.0.42 → 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.42",
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
+ });
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';