@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xetwa/design-system",
3
- "version": "1.0.48",
3
+ "version": "1.0.50",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,5 +1,5 @@
1
1
  import React, { useRef } from 'react';
2
- import { Pressable, StyleSheet, Animated } from 'react-native';
2
+ import { Pressable, StyleSheet, Animated, View } 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';
@@ -8,6 +8,8 @@ import { theme } from '../../../styles/theme';
8
8
  export interface ChipProps {
9
9
  /** Texto a ser exibido no chip */
10
10
  label: string;
11
+ /** Elemento extra à esquerda (ex: ícone, flag) */
12
+ leftElement?: React.ReactNode;
11
13
  /** Função a executar ao pressionar */
12
14
  onPress?: () => void;
13
15
  /** Tamanho do chip (afeta paddings e fonte) */
@@ -30,6 +32,7 @@ export interface ChipProps {
30
32
  */
31
33
  export const Chip = ({
32
34
  label,
35
+ leftElement,
33
36
  onPress,
34
37
  size = 'md',
35
38
  backgroundColor = theme.colors.white,
@@ -104,6 +107,7 @@ export const Chip = ({
104
107
  style,
105
108
  ]}
106
109
  >
110
+ {leftElement && <View style={{ marginRight: 6 }}>{leftElement}</View>}
107
111
  <Typography variant={typographyVariant} color={textColor} style={{ textAlign: 'center' }}>
108
112
  {label}
109
113
  </Typography>
@@ -0,0 +1,67 @@
1
+ // eslint-disable-next-line storybook/no-renderer-packages
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import React from 'react';
4
+ import { View } from 'react-native';
5
+ import { CircularProgress } from './CircularProgress';
6
+ import { Typography } from '../../Typography/Typography';
7
+
8
+ const meta = {
9
+ title: 'Components/DataDisplay/CircularProgress',
10
+ component: CircularProgress,
11
+ parameters: {
12
+ layout: 'centered',
13
+ docs: {
14
+ description: {
15
+ component: 'O **CircularProgress** exibe o progresso de uma determinada métrica num formato circular (usando SVG). Permite configurar cores, espessura da linha e incluir texto ou filhos personalizados no centro.\n\n### Exemplo de Uso\n```tsx\n<CircularProgress progress={50} size={120} strokeWidth={12} text="50%" />\n```',
16
+ }
17
+ }
18
+ },
19
+ tags: ['autodocs'],
20
+ argTypes: {
21
+ progress: { control: { type: 'range', min: 0, max: 100 } },
22
+ size: { control: 'number' },
23
+ strokeWidth: { control: 'number' },
24
+ color: { control: 'color' },
25
+ trackColor: { control: 'color' },
26
+ text: { control: 'text' },
27
+ },
28
+ } satisfies Meta<typeof CircularProgress>;
29
+
30
+ export default meta;
31
+ type Story = StoryObj<typeof meta>;
32
+
33
+ export const Default: Story = {
34
+ args: {
35
+ progress: 45,
36
+ size: 100,
37
+ strokeWidth: 10,
38
+ text: '45%',
39
+ },
40
+ };
41
+
42
+ export const CustomTypography: Story = {
43
+ args: {
44
+ progress: 75,
45
+ size: 140,
46
+ strokeWidth: 12,
47
+ },
48
+ render: (args) => (
49
+ <CircularProgress {...args}>
50
+ <View style={{ alignItems: 'center' }}>
51
+ <Typography variant="h2">75%</Typography>
52
+ <Typography variant="small" color="#9a8478">Concluído</Typography>
53
+ </View>
54
+ </CircularProgress>
55
+ ),
56
+ };
57
+
58
+ export const Sizes: Story = {
59
+ args: { progress: 30 },
60
+ render: () => (
61
+ <View style={{ flexDirection: 'row', gap: 20, alignItems: 'center' }}>
62
+ <CircularProgress progress={15} size={50} strokeWidth={6} text="15%" />
63
+ <CircularProgress progress={30} size={80} strokeWidth={8} text="30%" />
64
+ <CircularProgress progress={85} size={120} strokeWidth={12} text="85%" />
65
+ </View>
66
+ ),
67
+ };
@@ -0,0 +1,92 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import Svg, { Circle } from 'react-native-svg';
4
+ import { theme } from '../../../styles/theme';
5
+ import { Typography } from '../../Typography/Typography';
6
+
7
+ export interface CircularProgressProps {
8
+ /** O valor do progresso (0 a 100) */
9
+ progress: number;
10
+ /** Tamanho total (largura/altura) do círculo */
11
+ size?: number;
12
+ /** Grossura da linha */
13
+ strokeWidth?: number;
14
+ /** Cor da linha de progresso */
15
+ color?: string;
16
+ /** Cor da linha de fundo (track) */
17
+ trackColor?: string;
18
+ /** Texto opcional para aparecer no centro */
19
+ text?: string;
20
+ /** Permite customizar o conteúdo central, sobrepõe a prop text */
21
+ children?: React.ReactNode;
22
+ }
23
+
24
+ export const CircularProgress = ({
25
+ progress,
26
+ size = 100,
27
+ strokeWidth = 10,
28
+ color = theme.colors.primary,
29
+ trackColor = theme.colors.primaryLight,
30
+ text,
31
+ children,
32
+ }: CircularProgressProps) => {
33
+ const radius = (size - strokeWidth) / 2;
34
+ const circumference = radius * 2 * Math.PI;
35
+ // Limita o progresso entre 0 e 100
36
+ const safeProgress = Math.min(Math.max(progress, 0), 100);
37
+ const strokeDashoffset = circumference - (safeProgress / 100) * circumference;
38
+
39
+ return (
40
+ <View style={[styles.container, { width: size, height: size }]}>
41
+ <Svg width={size} height={size} style={styles.svg}>
42
+ {/* Fundo do Progresso (Track) */}
43
+ <Circle
44
+ stroke={trackColor}
45
+ fill="none"
46
+ cx={size / 2}
47
+ cy={size / 2}
48
+ r={radius}
49
+ strokeWidth={strokeWidth}
50
+ />
51
+ {/* Linha de Progresso */}
52
+ <Circle
53
+ stroke={color}
54
+ fill="none"
55
+ cx={size / 2}
56
+ cy={size / 2}
57
+ r={radius}
58
+ strokeWidth={strokeWidth}
59
+ strokeDasharray={circumference}
60
+ strokeDashoffset={strokeDashoffset}
61
+ strokeLinecap="round"
62
+ />
63
+ </Svg>
64
+
65
+ {/* Conteúdo Central */}
66
+ <View style={styles.contentContainer}>
67
+ {children ? (
68
+ children
69
+ ) : text ? (
70
+ <Typography variant="h3" color={theme.colors.brown[800]}>
71
+ {text}
72
+ </Typography>
73
+ ) : null}
74
+ </View>
75
+ </View>
76
+ );
77
+ };
78
+
79
+ const styles = StyleSheet.create({
80
+ container: {
81
+ alignItems: 'center',
82
+ justifyContent: 'center',
83
+ },
84
+ svg: {
85
+ transform: [{ rotate: '-90deg' }],
86
+ },
87
+ contentContainer: {
88
+ position: 'absolute',
89
+ alignItems: 'center',
90
+ justifyContent: 'center',
91
+ },
92
+ });
@@ -53,7 +53,7 @@ export const ProgressBar = ({
53
53
  }, [progress, animatedProgress]);
54
54
 
55
55
  const sizeStyles = {
56
- xs: { height: 8, borderRadius: 5 },
56
+ xs: { height: 8, borderRadius: 4 },
57
57
  sm: { height: 12, borderRadius: 7 },
58
58
  md: { height: 16, borderRadius: 9 },
59
59
  }[size === 'lg' ? 'md' : size] || { height: 16, borderRadius: 9 };
@@ -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
+ });