@xetwa/design-system 1.0.2
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/.github/workflows/publish.yml +30 -0
- package/.storybook/main.ts +17 -0
- package/.storybook/preview.tsx +65 -0
- package/eslint.config.js +22 -0
- package/index.html +13 -0
- package/package.json +55 -0
- package/public/favicon.svg +1 -0
- package/public/icons.svg +24 -0
- package/src/App.css +184 -0
- package/src/App.tsx +122 -0
- package/src/BordasSombras.mdx +343 -0
- package/src/Colors.mdx +393 -0
- package/src/GrelhaLayout.mdx +375 -0
- package/src/Icons.mdx +42 -0
- package/src/MovimentoBreakpoints.mdx +379 -0
- package/src/Spacing.mdx +231 -0
- package/src/Typography.mdx +321 -0
- package/src/assets/Mascots/vava/estaticos/vava-acenar.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-acertou.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-celebrar.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-com-autocolantes.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-dormir.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-errou.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-feliz.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-neutra.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-original.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-pensar.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-surpresa.svg +3 -0
- package/src/assets/Mascots/yaya/avatar/yaya-avatar-falar.svg +7 -0
- package/src/assets/Mascots/yaya/avatar/yaya-avatar-feliz.svg +7 -0
- package/src/assets/Mascots/yaya/avatar/yaya-avatar-transparente.svg +6 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-acertou.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-celebrar.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-dormir.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-errou.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-falar.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-feliz.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-neutra.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-pensar.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-surpresa.svg +13 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/components/Buttons/BackButton/BackButton.stories.tsx +86 -0
- package/src/components/Buttons/BackButton/BackButton.tsx +107 -0
- package/src/components/Buttons/FabButton/FabButton.stories.tsx +74 -0
- package/src/components/Buttons/FabButton/FabButton.tsx +161 -0
- package/src/components/Buttons/IconButton/IconButton.stories.tsx +99 -0
- package/src/components/Buttons/IconButton/IconButton.tsx +189 -0
- package/src/components/Buttons/JourneyButton/JourneyButton.stories.tsx +80 -0
- package/src/components/Buttons/JourneyButton/JourneyButton.tsx +194 -0
- package/src/components/Buttons/MainButton/MainButton.stories.tsx +144 -0
- package/src/components/Buttons/MainButton/MainButton.tsx +258 -0
- package/src/components/Buttons/WordChip/WordChip.stories.tsx +84 -0
- package/src/components/Buttons/WordChip/WordChip.tsx +157 -0
- package/src/components/Buttons/WordChip/index.ts +1 -0
- package/src/components/Cards/AnswerOptionCard/AnswerOptionCard.stories.tsx +181 -0
- package/src/components/Cards/AnswerOptionCard/AnswerOptionCard.tsx +201 -0
- package/src/components/Cards/BaseCard/BaseCard.stories.tsx +148 -0
- package/src/components/Cards/BaseCard/BaseCard.tsx +181 -0
- package/src/components/Cards/BoardingPassCard/BoardingPassCard.stories.tsx +205 -0
- package/src/components/Cards/BoardingPassCard/BoardingPassCard.tsx +384 -0
- package/src/components/Cards/BoardingPassCard/FlightIcon.tsx +21 -0
- package/src/components/Cards/BoardingPassCard/FlightIcon.web.tsx +18 -0
- package/src/components/Cards/CountryCard/CountryCard.stories.tsx +97 -0
- package/src/components/Cards/CountryCard/CountryCard.tsx +185 -0
- package/src/components/Cards/LevelProgressCard/LevelProgressCard.stories.tsx +61 -0
- package/src/components/Cards/LevelProgressCard/LevelProgressCard.tsx +143 -0
- package/src/components/Cards/ModuleCard/ModuleCard.stories.tsx +108 -0
- package/src/components/Cards/ModuleCard/ModuleCard.tsx +304 -0
- package/src/components/Cards/ModuleCompletionCard/ModuleCompletionCard.stories.tsx +53 -0
- package/src/components/Cards/ModuleCompletionCard/ModuleCompletionCard.tsx +167 -0
- package/src/components/Cards/Passport/PassportCover.tsx +114 -0
- package/src/components/Cards/Passport/PassportIDPage.tsx +217 -0
- package/src/components/Cards/Passport/PassportStampPage.tsx +253 -0
- package/src/components/Cards/Passport/PassportViewer.stories.tsx +95 -0
- package/src/components/Cards/Passport/PassportViewer.tsx +328 -0
- package/src/components/Cards/PlanCard/PlanCard.stories.tsx +159 -0
- package/src/components/Cards/PlanCard/PlanCard.tsx +317 -0
- package/src/components/Cards/PricingCard/PricingCard.stories.tsx +83 -0
- package/src/components/Cards/PricingCard/PricingCard.tsx +144 -0
- package/src/components/Cards/SelectionCard/SelectionCard.stories.tsx +77 -0
- package/src/components/Cards/SelectionCard/SelectionCard.tsx +111 -0
- package/src/components/Cards/TeacherCard/TeacherCard.stories.tsx +126 -0
- package/src/components/Cards/TeacherCard/TeacherCard.tsx +236 -0
- package/src/components/Charts/WeeklyProgressChart/WeeklyProgressChart.stories.tsx +89 -0
- package/src/components/Charts/WeeklyProgressChart/WeeklyProgressChart.tsx +179 -0
- package/src/components/Charts/WeeklyProgressChart/index.ts +1 -0
- package/src/components/DataDisplay/Avatar/Avatar.stories.tsx +85 -0
- package/src/components/DataDisplay/Avatar/Avatar.tsx +118 -0
- package/src/components/DataDisplay/Avatar/index.ts +1 -0
- package/src/components/DataDisplay/Badge/Badge.stories.tsx +62 -0
- package/src/components/DataDisplay/Badge/Badge.tsx +146 -0
- package/src/components/DataDisplay/Flag/Flag.stories.tsx +98 -0
- package/src/components/DataDisplay/Flag/Flag.tsx +347 -0
- package/src/components/DataDisplay/Flag/Flag.web.tsx +334 -0
- package/src/components/DataDisplay/InstructionBubble/InstructionBubble.stories.tsx +80 -0
- package/src/components/DataDisplay/InstructionBubble/InstructionBubble.tsx +86 -0
- package/src/components/DataDisplay/LevelBadge/LevelBadge.stories.tsx +99 -0
- package/src/components/DataDisplay/LevelBadge/LevelBadge.tsx +118 -0
- package/src/components/DataDisplay/ProgressBar/ProgressBar.stories.tsx +95 -0
- package/src/components/DataDisplay/ProgressBar/ProgressBar.tsx +117 -0
- package/src/components/DataDisplay/StatBadge/StatBadge.stories.tsx +82 -0
- package/src/components/DataDisplay/StatBadge/StatBadge.tsx +135 -0
- package/src/components/DataDisplay/StatBadge/index.ts +1 -0
- package/src/components/DataDisplay/Tag/Tag.stories.tsx +63 -0
- package/src/components/DataDisplay/Tag/Tag.tsx +148 -0
- package/src/components/Feedback/FeedbackBottomSheet/FeedbackBottomSheet.stories.tsx +74 -0
- package/src/components/Feedback/FeedbackBottomSheet/FeedbackBottomSheet.tsx +200 -0
- package/src/components/Forms/Checkbox/Checkbox.stories.tsx +60 -0
- package/src/components/Forms/Checkbox/Checkbox.tsx +127 -0
- package/src/components/Forms/Checkbox/index.ts +1 -0
- package/src/components/Forms/DaySelector/DaySelector.stories.tsx +62 -0
- package/src/components/Forms/DaySelector/DaySelector.tsx +133 -0
- package/src/components/Forms/Radio/Radio.stories.tsx +61 -0
- package/src/components/Forms/Radio/Radio.tsx +141 -0
- package/src/components/Forms/Radio/index.ts +1 -0
- package/src/components/Forms/SegmentedToggle/SegmentedToggle.stories.tsx +67 -0
- package/src/components/Forms/SegmentedToggle/SegmentedToggle.tsx +150 -0
- package/src/components/Forms/SegmentedToggle/index.ts +1 -0
- package/src/components/Forms/Select/Select.stories.tsx +59 -0
- package/src/components/Forms/Select/Select.tsx +255 -0
- package/src/components/Forms/Select/index.ts +1 -0
- package/src/components/Forms/Switch/Switch.stories.tsx +57 -0
- package/src/components/Forms/Switch/Switch.tsx +120 -0
- package/src/components/Forms/Switch/index.ts +1 -0
- package/src/components/Forms/TimeSlot/TimeSlot.stories.tsx +62 -0
- package/src/components/Forms/TimeSlot/TimeSlot.tsx +116 -0
- package/src/components/Headers/CountryHeader/CountryHeader.stories.tsx +114 -0
- package/src/components/Headers/CountryHeader/CountryHeader.tsx +147 -0
- package/src/components/Headers/ProfileHeader/ProfileHeader.stories.tsx +141 -0
- package/src/components/Headers/ProfileHeader/ProfileHeader.tsx +233 -0
- package/src/components/Inputs/FillInTheBlank/FillInTheBlank.stories.tsx +333 -0
- package/src/components/Inputs/FillInTheBlank/FillInTheBlank.tsx +287 -0
- package/src/components/Inputs/FillInTheBlank/index.ts +1 -0
- package/src/components/Inputs/TextInput/TextInput.stories.tsx +103 -0
- package/src/components/Inputs/TextInput/TextInput.tsx +187 -0
- package/src/components/Mascots/Vava/Vava.stories.tsx +114 -0
- package/src/components/Mascots/Vava/Vava.tsx +276 -0
- package/src/components/Mascots/Yaya/Yaya.stories.tsx +98 -0
- package/src/components/Mascots/Yaya/Yaya.tsx +233 -0
- package/src/components/Navigation/BottomTabBar/BottomTabBar.stories.tsx +68 -0
- package/src/components/Navigation/BottomTabBar/BottomTabBar.tsx +85 -0
- package/src/components/Navigation/BottomTabBar/BottomTabBarItem.tsx +68 -0
- package/src/components/Navigation/BottomTabBar/index.ts +2 -0
- package/src/components/Navigation/PaginationDots/PaginationDots.stories.tsx +62 -0
- package/src/components/Navigation/PaginationDots/PaginationDots.tsx +81 -0
- package/src/components/Navigation/PaginationDots/index.ts +1 -0
- package/src/components/Navigation/ProgressMap/ProgressMap.stories.tsx +116 -0
- package/src/components/Navigation/ProgressMap/ProgressMap.tsx +827 -0
- package/src/components/Navigation/WorldProgressMap/WorldProgressMap.stories.tsx +169 -0
- package/src/components/Navigation/WorldProgressMap/WorldProgressMap.tsx +790 -0
- package/src/components/Typography/Typography.stories.tsx +127 -0
- package/src/components/Typography/Typography.tsx +53 -0
- package/src/hooks/useResponsive.ts +41 -0
- package/src/hooks/useTheme.ts +32 -0
- package/src/index.css +355 -0
- package/src/index.ts +28 -0
- package/src/main.tsx +10 -0
- package/src/mocks/codegenNativeComponent.ts +5 -0
- package/src/styles/countryThemes.ts +79 -0
- package/src/styles/theme.ts +297 -0
- package/src/utils/iconMap.ts +23 -0
- package/tsconfig.app.json +25 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +51 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { CountryHeader } from './CountryHeader';
|
|
3
|
+
import { countryThemes } from '../../../styles/countryThemes';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/Headers/CountryHeader',
|
|
7
|
+
component: CountryHeader,
|
|
8
|
+
argTypes: {
|
|
9
|
+
countryCode: {
|
|
10
|
+
control: 'select',
|
|
11
|
+
options: Object.keys(countryThemes),
|
|
12
|
+
description: 'Código do país para a paleta de cores e bandeira',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
parameters: {
|
|
16
|
+
layout: 'fullscreen',
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component: `O **CountryHeader** é o cabeçalho principal dos ecrãs de Módulo ou Etapa de um País.
|
|
20
|
+
Ele adapta inteligentemente a cor de fundo, o círculo decorativo e as cores da barra de progresso baseando-se unicamente no \`countryCode\` que recebe. Utiliza a paleta oficial da Xetwa para países francófonos.
|
|
21
|
+
|
|
22
|
+
### Lidar com a SafeArea (Notch / Status Bar)
|
|
23
|
+
Como este cabeçalho tem uma cor de fundo sólida e um círculo decorativo, ele deve ignorar as margens do ecrã e ir **até ao limite superior físico do telemóvel**.
|
|
24
|
+
Para isso, NÃO coloque este componente dentro de uma \`SafeAreaView\` convencional com margem no topo.
|
|
25
|
+
Em vez disso, passe a propriedade \`safeAreaTop\` com o valor gerado pelo hook \`useSafeAreaInsets()\` do \`react-native-safe-area-context\`.
|
|
26
|
+
|
|
27
|
+
\`\`\`tsx
|
|
28
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
29
|
+
|
|
30
|
+
export function Screen() {
|
|
31
|
+
const insets = useSafeAreaInsets();
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<View style={{ flex: 1 }}>
|
|
35
|
+
<CountryHeader
|
|
36
|
+
countryCode="SN"
|
|
37
|
+
title="Senegal"
|
|
38
|
+
// ...
|
|
39
|
+
safeAreaTop={insets.top}
|
|
40
|
+
/>
|
|
41
|
+
</View>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
\`\`\`
|
|
45
|
+
|
|
46
|
+
### Exemplo de Uso
|
|
47
|
+
\`\`\`tsx
|
|
48
|
+
<CountryHeader
|
|
49
|
+
countryCode="SN"
|
|
50
|
+
title="Senegal"
|
|
51
|
+
subtitle="Dakar · Saint-Louis · 2 de 4 módulos"
|
|
52
|
+
headerText="ETAPA 3 · A1"
|
|
53
|
+
progress={0.5}
|
|
54
|
+
progressSegments={4}
|
|
55
|
+
onBackPress={() => console.log('Voltar')}
|
|
56
|
+
/>
|
|
57
|
+
\`\`\`
|
|
58
|
+
`
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
tags: ['autodocs'],
|
|
63
|
+
} satisfies Meta<any>;
|
|
64
|
+
|
|
65
|
+
export default meta;
|
|
66
|
+
type Story = StoryObj<any>;
|
|
67
|
+
|
|
68
|
+
export const Senegal: Story = {
|
|
69
|
+
name: 'Senegal (SN)',
|
|
70
|
+
args: {
|
|
71
|
+
countryCode: 'SN',
|
|
72
|
+
title: 'Senegal',
|
|
73
|
+
subtitle: 'Dakar · Saint-Louis · 2 de 4 módulos',
|
|
74
|
+
headerText: 'ETAPA 3 · A1',
|
|
75
|
+
progress: 0.5,
|
|
76
|
+
progressSegments: 4,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const France: Story = {
|
|
81
|
+
name: 'França (FR)',
|
|
82
|
+
args: {
|
|
83
|
+
countryCode: 'FR',
|
|
84
|
+
title: 'França',
|
|
85
|
+
subtitle: 'Paris · Lyon · 1 de 4 módulos',
|
|
86
|
+
headerText: 'ETAPA 1 · A1',
|
|
87
|
+
progress: 0.25,
|
|
88
|
+
progressSegments: 4,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const Congo: Story = {
|
|
93
|
+
name: 'RD Congo (CD)',
|
|
94
|
+
args: {
|
|
95
|
+
countryCode: 'CD',
|
|
96
|
+
title: 'Congo',
|
|
97
|
+
subtitle: 'Kinshasa · 4 de 4 módulos',
|
|
98
|
+
headerText: 'ETAPA 5 · B1',
|
|
99
|
+
progress: 1.0,
|
|
100
|
+
progressSegments: 4,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const WithSafeArea: Story = {
|
|
105
|
+
name: 'Simulação de SafeArea (iPhone Notch)',
|
|
106
|
+
args: {
|
|
107
|
+
countryCode: 'SN',
|
|
108
|
+
title: 'Senegal (Com Notch)',
|
|
109
|
+
subtitle: 'Cabeçalho empurra o conteúdo para baixo',
|
|
110
|
+
headerText: 'SIMULAÇÃO DE INSETS.TOP',
|
|
111
|
+
progress: 0.5,
|
|
112
|
+
safeAreaTop: 47, // Valor comum para iPhone 12/13/14
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { View, StyleSheet } from 'react-native';
|
|
2
|
+
import type { ViewStyle } from 'react-native';
|
|
3
|
+
import { useResponsive } from '../../../hooks/useResponsive';
|
|
4
|
+
import { getCountryTheme } from '../../../styles/countryThemes';
|
|
5
|
+
import { Flag } from '../../DataDisplay/Flag/Flag';
|
|
6
|
+
import { ProgressBar } from '../../DataDisplay/ProgressBar/ProgressBar';
|
|
7
|
+
import { BackButton } from '../../Buttons/BackButton/BackButton';
|
|
8
|
+
import { Typography } from '../../Typography/Typography';
|
|
9
|
+
|
|
10
|
+
export interface CountryHeaderProps {
|
|
11
|
+
/** Código ISO do país (ex: 'SN') para a bandeira e o tema de cores */
|
|
12
|
+
countryCode: string;
|
|
13
|
+
/** Nome do país (ex: 'Senegal') */
|
|
14
|
+
title: string;
|
|
15
|
+
/** Subtítulo (ex: 'Dakar · Saint-Louis · 2 de 4 módulos') */
|
|
16
|
+
subtitle: string;
|
|
17
|
+
/** Texto do cabeçalho superior (ex: 'ETAPA 3 · A1') */
|
|
18
|
+
headerText: string;
|
|
19
|
+
/** Progresso atual de 0.0 a 1.0 */
|
|
20
|
+
progress: number;
|
|
21
|
+
/** Número de segmentos na barra de progresso */
|
|
22
|
+
progressSegments?: number;
|
|
23
|
+
/** Função ao clicar no botão de voltar */
|
|
24
|
+
onBackPress?: () => void;
|
|
25
|
+
/**
|
|
26
|
+
* Espaçamento extra no topo para acomodar a Notch/Status Bar do dispositivo.
|
|
27
|
+
* O developer deve passar o \`insets.top\` gerado pelo \`react-native-safe-area-context\`.
|
|
28
|
+
*/
|
|
29
|
+
safeAreaTop?: number;
|
|
30
|
+
/** Estilos customizados opcionais para o container principal */
|
|
31
|
+
style?: ViewStyle | ViewStyle[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const CountryHeader = ({
|
|
35
|
+
countryCode,
|
|
36
|
+
title,
|
|
37
|
+
subtitle,
|
|
38
|
+
headerText,
|
|
39
|
+
progress,
|
|
40
|
+
progressSegments = 4,
|
|
41
|
+
onBackPress,
|
|
42
|
+
safeAreaTop = 0,
|
|
43
|
+
style,
|
|
44
|
+
}: CountryHeaderProps) => {
|
|
45
|
+
const { scale, scaleText } = useResponsive();
|
|
46
|
+
const countryTheme = getCountryTheme(countryCode);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View style={[styles.container, { backgroundColor: countryTheme.bg }, style]}>
|
|
50
|
+
{/* Círculo decorativo de fundo */}
|
|
51
|
+
<View
|
|
52
|
+
style={[
|
|
53
|
+
styles.decorativeCircle,
|
|
54
|
+
{
|
|
55
|
+
backgroundColor: countryTheme.circle,
|
|
56
|
+
width: scale(230),
|
|
57
|
+
height: scale(230),
|
|
58
|
+
borderRadius: scale(120),
|
|
59
|
+
top: scale(-80),
|
|
60
|
+
right: scale(-80),
|
|
61
|
+
},
|
|
62
|
+
]}
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
<View style={[styles.content, { padding: scale(20), paddingTop: safeAreaTop + scale(24) }]}>
|
|
66
|
+
{/* Top Bar: Back Button e Header Text */}
|
|
67
|
+
<View style={[styles.topRow, { marginBottom: scale(16) }]}>
|
|
68
|
+
<BackButton onPress={onBackPress} variant="glass" />
|
|
69
|
+
<Typography style={[styles.headerText, { fontSize: scaleText(13), lineHeight: scaleText(16), marginLeft: scale(12) }]}>
|
|
70
|
+
{headerText}
|
|
71
|
+
</Typography>
|
|
72
|
+
</View>
|
|
73
|
+
|
|
74
|
+
{/* Flag e Título/Subtítulo */}
|
|
75
|
+
<View style={[styles.titleRow, { marginBottom: scale(16) }]}>
|
|
76
|
+
<View style={[styles.flagWrapper, { padding: scale(2) }]}>
|
|
77
|
+
<Flag countryCode={countryCode} size={scale(56)} />
|
|
78
|
+
</View>
|
|
79
|
+
<View style={[styles.textContainer, { marginLeft: scale(12) }]}>
|
|
80
|
+
<Typography style={[styles.title, { fontSize: scaleText(26), lineHeight: scaleText(30) }]}>
|
|
81
|
+
{title}
|
|
82
|
+
</Typography>
|
|
83
|
+
<Typography style={[styles.subtitle, { fontSize: scaleText(13), lineHeight: scaleText(16), marginTop: scale(2) }]}>
|
|
84
|
+
{subtitle}
|
|
85
|
+
</Typography>
|
|
86
|
+
</View>
|
|
87
|
+
</View>
|
|
88
|
+
|
|
89
|
+
{/* Progress Bar */}
|
|
90
|
+
<ProgressBar
|
|
91
|
+
progress={progress}
|
|
92
|
+
segments={progressSegments}
|
|
93
|
+
size="sm"
|
|
94
|
+
fillColor={countryTheme.progressFill}
|
|
95
|
+
trackColor={countryTheme.progressTrack}
|
|
96
|
+
/>
|
|
97
|
+
</View>
|
|
98
|
+
</View>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const styles = StyleSheet.create({
|
|
103
|
+
container: {
|
|
104
|
+
width: '100%',
|
|
105
|
+
overflow: 'hidden',
|
|
106
|
+
position: 'relative',
|
|
107
|
+
},
|
|
108
|
+
decorativeCircle: {
|
|
109
|
+
position: 'absolute',
|
|
110
|
+
},
|
|
111
|
+
content: {
|
|
112
|
+
width: '100%',
|
|
113
|
+
},
|
|
114
|
+
topRow: {
|
|
115
|
+
flexDirection: 'row',
|
|
116
|
+
alignItems: 'center',
|
|
117
|
+
zIndex: 2, // Garante que fica acima do círculo decorativo
|
|
118
|
+
},
|
|
119
|
+
headerText: {
|
|
120
|
+
color: 'rgba(255, 255, 255, 0.8)',
|
|
121
|
+
|
|
122
|
+
letterSpacing: 0.5,
|
|
123
|
+
},
|
|
124
|
+
titleRow: {
|
|
125
|
+
flexDirection: 'row',
|
|
126
|
+
alignItems: 'center',
|
|
127
|
+
zIndex: 2,
|
|
128
|
+
},
|
|
129
|
+
flagWrapper: {
|
|
130
|
+
backgroundColor: '#ffffff',
|
|
131
|
+
borderRadius: 999,
|
|
132
|
+
justifyContent: 'center',
|
|
133
|
+
alignItems: 'center',
|
|
134
|
+
},
|
|
135
|
+
textContainer: {
|
|
136
|
+
flex: 1,
|
|
137
|
+
},
|
|
138
|
+
title: {
|
|
139
|
+
|
|
140
|
+
color: '#ffffff',
|
|
141
|
+
},
|
|
142
|
+
subtitle: {
|
|
143
|
+
|
|
144
|
+
color: '#ffffff',
|
|
145
|
+
opacity: 0.9,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { ProfileHeader } from './ProfileHeader';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/Headers/ProfileHeader',
|
|
7
|
+
component: ProfileHeader,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'fullscreen',
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component: `O **ProfileHeader** é o cabeçalho principal do ecrã de perfil do utilizador. Apresenta os dados do utilizador (nome e avatar circular), assim como um resumo do progresso no país atual.
|
|
13
|
+
|
|
14
|
+
A responsividade e consistência visual são garantidas através do uso de escalas de fontes (\`scaleText\`), empacotamento em \`maxWidth: 500\`, e redimensionamento automático de texto para ecrãs mais pequenos ou nomes excessivamente longos.
|
|
15
|
+
|
|
16
|
+
### Lidar com a SafeArea (Notch / Status Bar)
|
|
17
|
+
Assim como o \`CountryHeader\`, 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\`.
|
|
18
|
+
|
|
19
|
+
\`\`\`tsx
|
|
20
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
21
|
+
import { View } from 'react-native';
|
|
22
|
+
import { ProfileHeader } from '@xetlingo/design-system';
|
|
23
|
+
|
|
24
|
+
export function ProfileScreen() {
|
|
25
|
+
const insets = useSafeAreaInsets();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<View style={{ flex: 1 }}>
|
|
29
|
+
<ProfileHeader
|
|
30
|
+
userName="Sofia"
|
|
31
|
+
userSubtitle="a caminho de C1 · entrou em jan 2026"
|
|
32
|
+
avatarInitials="SO"
|
|
33
|
+
countryCode="SN"
|
|
34
|
+
countryName="Senegal"
|
|
35
|
+
level="Nível B2"
|
|
36
|
+
moduleSubtitle="país atual · 2 de 4 módulos"
|
|
37
|
+
progressPercentage={50}
|
|
38
|
+
safeAreaTop={insets.top}
|
|
39
|
+
onSettingsPress={() => console.log('Definições')}
|
|
40
|
+
/>
|
|
41
|
+
</View>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
\`\`\`
|
|
45
|
+
|
|
46
|
+
### Casos de Uso Comuns
|
|
47
|
+
|
|
48
|
+
#### 1. Uso com Imagem no Avatar e Bandeira Nativa (Vetorizada)
|
|
49
|
+
Caso o utilizador tenha uma foto de perfil, podes passá-la diretamente no \`avatarSrc\`.
|
|
50
|
+
\`\`\`tsx
|
|
51
|
+
<ProfileHeader
|
|
52
|
+
userName="Mamadou"
|
|
53
|
+
userSubtitle="a caminho de B1 · entrou em fev 2026"
|
|
54
|
+
avatarSrc="https://exemplo.com/avatar.jpg"
|
|
55
|
+
countryCode="BE"
|
|
56
|
+
countryName="Bélgica"
|
|
57
|
+
level="Nível A2"
|
|
58
|
+
moduleSubtitle="país atual · 1 de 4 módulos"
|
|
59
|
+
progressPercentage={25}
|
|
60
|
+
onSettingsPress={() => abrirDefinicoes()}
|
|
61
|
+
/>
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
#### 2. Uso com Iniciais do Utilizador e Fallback de Imagem Local
|
|
65
|
+
Se o utilizador não tiver imagem, as iniciais serão apresentadas. Podes opcionalmente passar uma imagem local como bandeira utilizando \`countryFlagSrc\`.
|
|
66
|
+
\`\`\`tsx
|
|
67
|
+
<ProfileHeader
|
|
68
|
+
userName="Maria"
|
|
69
|
+
userSubtitle="a caminho do domínio completo do idioma"
|
|
70
|
+
avatarInitials="MA"
|
|
71
|
+
countryFlagSrc={require('./assets/bandeira_sn.png')}
|
|
72
|
+
countryName="Senegal"
|
|
73
|
+
level="Nível C1"
|
|
74
|
+
moduleSubtitle="país atual · 4 de 4 módulos"
|
|
75
|
+
progressPercentage={100}
|
|
76
|
+
/>
|
|
77
|
+
\`\`\`
|
|
78
|
+
`
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
tags: ['autodocs'],
|
|
83
|
+
decorators: [
|
|
84
|
+
(Story) => (
|
|
85
|
+
<View style={{ flex: 1, backgroundColor: '#ffffff' }}>
|
|
86
|
+
<Story />
|
|
87
|
+
</View>
|
|
88
|
+
),
|
|
89
|
+
],
|
|
90
|
+
} satisfies Meta<any>;
|
|
91
|
+
|
|
92
|
+
export default meta;
|
|
93
|
+
type Story = StoryObj<any>;
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
export const Default: Story = {
|
|
97
|
+
args: {
|
|
98
|
+
userName: 'Sofia',
|
|
99
|
+
userSubtitle: 'a caminho de C1 · entrou em jan 2026',
|
|
100
|
+
avatarInitials: 'SO',
|
|
101
|
+
countryCode: 'SN',
|
|
102
|
+
countryName: 'Senegal',
|
|
103
|
+
level: 'Nível B2',
|
|
104
|
+
moduleSubtitle: 'país atual · 2 de 4 módulos',
|
|
105
|
+
progressPercentage: 50,
|
|
106
|
+
safeAreaTop: 44, // Simula a notch para que o design pareça real
|
|
107
|
+
onSettingsPress: () => console.log('Definições clicadas'),
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const AvatarImage: Story = {
|
|
112
|
+
name: 'Com Imagem no Avatar',
|
|
113
|
+
args: {
|
|
114
|
+
userName: 'Mamadou',
|
|
115
|
+
userSubtitle: 'a caminho de B1 · entrou em fev 2026',
|
|
116
|
+
avatarSrc: 'https://i.pravatar.cc/150?img=11',
|
|
117
|
+
countryCode: 'BE',
|
|
118
|
+
countryName: 'Bélgica',
|
|
119
|
+
level: 'Nível A2',
|
|
120
|
+
moduleSubtitle: 'país atual · 1 de 4 módulos',
|
|
121
|
+
progressPercentage: 25,
|
|
122
|
+
safeAreaTop: 44,
|
|
123
|
+
onSettingsPress: () => console.log('Definições clicadas'),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const TextosLongos: Story = {
|
|
128
|
+
name: 'Com Textos Longos (Teste Responsivo)',
|
|
129
|
+
args: {
|
|
130
|
+
userName: 'Maria Francisca das Neves',
|
|
131
|
+
userSubtitle: 'a caminho do domínio completo do idioma local',
|
|
132
|
+
avatarInitials: 'MF',
|
|
133
|
+
countryCode: 'CD',
|
|
134
|
+
countryName: 'República Democrática do Congo',
|
|
135
|
+
level: 'Nível C1 - Avançado',
|
|
136
|
+
moduleSubtitle: 'país atual muito longo para testar a quebra ou encolhimento de texto em ecrãs',
|
|
137
|
+
progressPercentage: 100,
|
|
138
|
+
safeAreaTop: 44,
|
|
139
|
+
onSettingsPress: () => console.log('Definições clicadas'),
|
|
140
|
+
},
|
|
141
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { View, StyleSheet, Image, TouchableOpacity } from 'react-native';
|
|
2
|
+
import type { ViewStyle, ImageSourcePropType } from 'react-native';
|
|
3
|
+
import { Settings } from 'lucide-react-native';
|
|
4
|
+
import { theme } from '../../../styles/theme';
|
|
5
|
+
import { useResponsive } from '../../../hooks/useResponsive';
|
|
6
|
+
import { Avatar } from '../../DataDisplay/Avatar/Avatar';
|
|
7
|
+
import { Flag } from '../../DataDisplay/Flag/Flag';
|
|
8
|
+
import { Typography } from '../../Typography/Typography';
|
|
9
|
+
|
|
10
|
+
export interface ProfileHeaderProps {
|
|
11
|
+
/** Nome do utilizador */
|
|
12
|
+
userName: string;
|
|
13
|
+
/** Subtítulo do utilizador (ex: "a caminho de C1 · entrou em jan 2026") */
|
|
14
|
+
userSubtitle: string;
|
|
15
|
+
/** Fonte da imagem do avatar */
|
|
16
|
+
avatarSrc?: string | ImageSourcePropType;
|
|
17
|
+
/** Iniciais caso não haja avatar */
|
|
18
|
+
avatarInitials?: string;
|
|
19
|
+
|
|
20
|
+
/** Código ISO do país (ex: 'SN') para renderizar a bandeira de forma nativa e nítida */
|
|
21
|
+
countryCode?: string;
|
|
22
|
+
/** Bandeira do país atual (fallback caso não forneça countryCode) */
|
|
23
|
+
countryFlagSrc?: ImageSourcePropType;
|
|
24
|
+
/** Nome do país atual */
|
|
25
|
+
countryName: string;
|
|
26
|
+
/** Nível de aprendizagem atual no país */
|
|
27
|
+
level: string;
|
|
28
|
+
/** Subtítulo do módulo atual */
|
|
29
|
+
moduleSubtitle: string;
|
|
30
|
+
/** Percentagem de progresso (0-100) */
|
|
31
|
+
progressPercentage: number;
|
|
32
|
+
|
|
33
|
+
/** Espaçamento extra no topo para SafeArea/Notch */
|
|
34
|
+
safeAreaTop?: number;
|
|
35
|
+
/** Evento ao clicar nas definições */
|
|
36
|
+
onSettingsPress?: () => void;
|
|
37
|
+
/** Estilos opcionais */
|
|
38
|
+
style?: ViewStyle | ViewStyle[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const ProfileHeader = ({
|
|
42
|
+
userName,
|
|
43
|
+
userSubtitle,
|
|
44
|
+
avatarSrc,
|
|
45
|
+
avatarInitials,
|
|
46
|
+
countryCode,
|
|
47
|
+
countryFlagSrc,
|
|
48
|
+
countryName,
|
|
49
|
+
level,
|
|
50
|
+
moduleSubtitle,
|
|
51
|
+
progressPercentage,
|
|
52
|
+
safeAreaTop = 0,
|
|
53
|
+
onSettingsPress,
|
|
54
|
+
style,
|
|
55
|
+
}: ProfileHeaderProps) => {
|
|
56
|
+
const { scale, scaleText } = useResponsive();
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<View style={[styles.container, style]}>
|
|
60
|
+
{/* Background Decorativo - Círculo menor na direita */}
|
|
61
|
+
<View
|
|
62
|
+
style={[
|
|
63
|
+
styles.decorativeCircle,
|
|
64
|
+
{
|
|
65
|
+
width: scale(140),
|
|
66
|
+
height: scale(140),
|
|
67
|
+
borderRadius: scale(70),
|
|
68
|
+
top: -scale(30),
|
|
69
|
+
right: -scale(30)
|
|
70
|
+
}
|
|
71
|
+
]}
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
<View style={[styles.content, { paddingHorizontal: scale(20), paddingBottom: scale(20), paddingTop: safeAreaTop + scale(16) }]}>
|
|
75
|
+
{/* Top Section: Avatar & Info */}
|
|
76
|
+
<View style={styles.topSection}>
|
|
77
|
+
<View style={styles.userInfoRow}>
|
|
78
|
+
<View style={{ marginRight: scale(16) }}>
|
|
79
|
+
<Avatar
|
|
80
|
+
size="xl"
|
|
81
|
+
variant="circle"
|
|
82
|
+
src={avatarSrc as any}
|
|
83
|
+
initials={avatarInitials}
|
|
84
|
+
color={theme.colors.primary}
|
|
85
|
+
/>
|
|
86
|
+
</View>
|
|
87
|
+
|
|
88
|
+
<View style={styles.userTexts}>
|
|
89
|
+
<Typography
|
|
90
|
+
style={[styles.userName, { fontSize: scaleText(26), lineHeight: scaleText(30) }]}
|
|
91
|
+
numberOfLines={1}
|
|
92
|
+
adjustsFontSizeToFit
|
|
93
|
+
>
|
|
94
|
+
{userName}
|
|
95
|
+
</Typography>
|
|
96
|
+
<Typography
|
|
97
|
+
style={[styles.userSubtitle, { fontSize: scaleText(14.5), lineHeight: scaleText(18), marginTop: scale(4) }]}
|
|
98
|
+
>
|
|
99
|
+
{userSubtitle}
|
|
100
|
+
</Typography>
|
|
101
|
+
</View>
|
|
102
|
+
</View>
|
|
103
|
+
|
|
104
|
+
{onSettingsPress && (
|
|
105
|
+
<TouchableOpacity
|
|
106
|
+
style={[styles.settingsButton, { padding: scale(8) }]}
|
|
107
|
+
onPress={onSettingsPress}
|
|
108
|
+
activeOpacity={0.7}
|
|
109
|
+
>
|
|
110
|
+
<Settings color={theme.colors.brown[100]} size={scale(24)} strokeWidth={2.2} />
|
|
111
|
+
</TouchableOpacity>
|
|
112
|
+
)}
|
|
113
|
+
</View>
|
|
114
|
+
|
|
115
|
+
{/* Bottom Section: Progress Card */}
|
|
116
|
+
<View style={[styles.progressCard, { padding: scale(16), borderRadius: scale(20), marginTop: scale(20) }]}>
|
|
117
|
+
<View style={styles.progressCardLeft}>
|
|
118
|
+
{countryCode ? (
|
|
119
|
+
<Flag
|
|
120
|
+
countryCode={countryCode}
|
|
121
|
+
size={scale(36)}
|
|
122
|
+
style={{
|
|
123
|
+
height: scale(26),
|
|
124
|
+
borderRadius: scale(4),
|
|
125
|
+
marginRight: scale(16)
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
) : countryFlagSrc ? (
|
|
129
|
+
<Image
|
|
130
|
+
source={countryFlagSrc}
|
|
131
|
+
style={[styles.flag, { width: scale(36), height: scale(26), borderRadius: scale(4), marginRight: scale(16) }]}
|
|
132
|
+
resizeMode="cover"
|
|
133
|
+
/>
|
|
134
|
+
) : null}
|
|
135
|
+
<View style={styles.progressCardTexts}>
|
|
136
|
+
<Typography
|
|
137
|
+
style={[styles.progressTitle, { fontSize: scaleText(16.5), lineHeight: scaleText(20) }]}
|
|
138
|
+
numberOfLines={1}
|
|
139
|
+
adjustsFontSizeToFit
|
|
140
|
+
>
|
|
141
|
+
{countryName} · {level}
|
|
142
|
+
</Typography>
|
|
143
|
+
<Typography style={[styles.progressSubtitle, { fontSize: scaleText(13.5), lineHeight: scaleText(17), marginTop: scale(2) }]}>
|
|
144
|
+
{moduleSubtitle}
|
|
145
|
+
</Typography>
|
|
146
|
+
</View>
|
|
147
|
+
</View>
|
|
148
|
+
|
|
149
|
+
<Typography style={[styles.progressPercent, { fontSize: scaleText(22), lineHeight: scaleText(26) }]}>
|
|
150
|
+
{progressPercentage}%
|
|
151
|
+
</Typography>
|
|
152
|
+
</View>
|
|
153
|
+
</View>
|
|
154
|
+
</View>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const styles = StyleSheet.create({
|
|
159
|
+
container: {
|
|
160
|
+
backgroundColor: theme.colors.brown[700], // Fundo principal mais escuro (#3f2620)
|
|
161
|
+
width: '100%',
|
|
162
|
+
maxWidth: 500, // Limita o tamanho em Storybook / ecrãs largos
|
|
163
|
+
alignSelf: 'center',
|
|
164
|
+
overflow: 'hidden',
|
|
165
|
+
},
|
|
166
|
+
decorativeCircle: {
|
|
167
|
+
position: 'absolute',
|
|
168
|
+
backgroundColor: '#6b4332', // Cor customizada solicitada pelo utilizador
|
|
169
|
+
},
|
|
170
|
+
content: {
|
|
171
|
+
position: 'relative',
|
|
172
|
+
zIndex: 1,
|
|
173
|
+
width: '100%',
|
|
174
|
+
},
|
|
175
|
+
topSection: {
|
|
176
|
+
flexDirection: 'row',
|
|
177
|
+
alignItems: 'center',
|
|
178
|
+
justifyContent: 'space-between',
|
|
179
|
+
},
|
|
180
|
+
userInfoRow: {
|
|
181
|
+
flexDirection: 'row',
|
|
182
|
+
alignItems: 'center',
|
|
183
|
+
flex: 1,
|
|
184
|
+
paddingRight: 8,
|
|
185
|
+
},
|
|
186
|
+
userTexts: {
|
|
187
|
+
flex: 1,
|
|
188
|
+
justifyContent: 'center',
|
|
189
|
+
},
|
|
190
|
+
userName: {
|
|
191
|
+
|
|
192
|
+
color: theme.colors.white,
|
|
193
|
+
},
|
|
194
|
+
userSubtitle: {
|
|
195
|
+
|
|
196
|
+
color: theme.colors.brown[100], // Cor mais clara e legível
|
|
197
|
+
},
|
|
198
|
+
settingsButton: {
|
|
199
|
+
justifyContent: 'center',
|
|
200
|
+
alignItems: 'center',
|
|
201
|
+
},
|
|
202
|
+
progressCard: {
|
|
203
|
+
backgroundColor: theme.colors.brown[800], // Mesma cor do círculo (#51332d)
|
|
204
|
+
flexDirection: 'row',
|
|
205
|
+
alignItems: 'center',
|
|
206
|
+
justifyContent: 'space-between',
|
|
207
|
+
},
|
|
208
|
+
progressCardLeft: {
|
|
209
|
+
flexDirection: 'row',
|
|
210
|
+
alignItems: 'center',
|
|
211
|
+
flex: 1,
|
|
212
|
+
paddingRight: 8,
|
|
213
|
+
},
|
|
214
|
+
flag: {
|
|
215
|
+
backgroundColor: theme.colors.brown[600], // fallback if loading
|
|
216
|
+
},
|
|
217
|
+
progressCardTexts: {
|
|
218
|
+
flex: 1,
|
|
219
|
+
justifyContent: 'center',
|
|
220
|
+
},
|
|
221
|
+
progressTitle: {
|
|
222
|
+
|
|
223
|
+
color: theme.colors.white,
|
|
224
|
+
},
|
|
225
|
+
progressSubtitle: {
|
|
226
|
+
|
|
227
|
+
color: theme.colors.brown[300],
|
|
228
|
+
},
|
|
229
|
+
progressPercent: {
|
|
230
|
+
|
|
231
|
+
color: theme.colors.primary,
|
|
232
|
+
},
|
|
233
|
+
});
|