@xetwa/design-system 1.0.48 → 1.0.50

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.
@@ -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
+ });
@@ -1,4 +1,5 @@
1
- import { View, StyleSheet } from 'react-native';
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { View, StyleSheet, Animated, Easing } from 'react-native';
2
3
  import type { ViewStyle } from 'react-native';
3
4
  import { useResponsive } from '../../../hooks/useResponsive';
4
5
  import { getCountryTheme } from '../../../styles/countryThemes';
@@ -27,6 +28,8 @@ export interface CountryHeaderProps {
27
28
  * O developer deve passar o \`insets.top\` gerado pelo \`react-native-safe-area-context\`.
28
29
  */
29
30
  safeAreaTop?: number;
31
+ /** Se deve ocultar a barra de progresso com animação */
32
+ hideProgressBar?: boolean;
30
33
  /** Estilos customizados opcionais para o container principal */
31
34
  style?: ViewStyle | ViewStyle[];
32
35
  }
@@ -40,11 +43,23 @@ export const CountryHeader = ({
40
43
  progressSegments = 4,
41
44
  onBackPress,
42
45
  safeAreaTop = 0,
46
+ hideProgressBar = false,
43
47
  style,
44
48
  }: CountryHeaderProps) => {
45
49
  const { scale, scaleText } = useResponsive();
46
50
  const countryTheme = getCountryTheme(countryCode);
47
51
 
52
+ const progressAnim = useRef(new Animated.Value(hideProgressBar ? 0 : 1)).current;
53
+
54
+ useEffect(() => {
55
+ Animated.timing(progressAnim, {
56
+ toValue: hideProgressBar ? 0 : 1,
57
+ duration: 300,
58
+ easing: Easing.inOut(Easing.ease),
59
+ useNativeDriver: false,
60
+ }).start();
61
+ }, [hideProgressBar, progressAnim]);
62
+
48
63
  return (
49
64
  <View style={[styles.container, { backgroundColor: countryTheme.bg }, style]}>
50
65
  {/* Círculo decorativo de fundo */}
@@ -87,13 +102,24 @@ export const CountryHeader = ({
87
102
  </View>
88
103
 
89
104
  {/* Progress Bar */}
90
- <ProgressBar
91
- progress={progress}
92
- segments={progressSegments}
93
- size="sm"
94
- fillColor={countryTheme.progressFill}
95
- trackColor={countryTheme.progressTrack}
96
- />
105
+ <Animated.View
106
+ style={{
107
+ opacity: progressAnim,
108
+ height: progressAnim.interpolate({
109
+ inputRange: [0, 1],
110
+ outputRange: [0, 12],
111
+ }),
112
+ overflow: 'hidden',
113
+ }}
114
+ >
115
+ <ProgressBar
116
+ progress={progress}
117
+ segments={progressSegments}
118
+ size="sm"
119
+ fillColor={countryTheme.progressFill}
120
+ trackColor={countryTheme.progressTrack}
121
+ />
122
+ </Animated.View>
97
123
  </View>
98
124
  </View>
99
125
  );
@@ -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
+ };