@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.
Files changed (168) hide show
  1. package/.github/workflows/publish.yml +30 -0
  2. package/.storybook/main.ts +17 -0
  3. package/.storybook/preview.tsx +65 -0
  4. package/eslint.config.js +22 -0
  5. package/index.html +13 -0
  6. package/package.json +55 -0
  7. package/public/favicon.svg +1 -0
  8. package/public/icons.svg +24 -0
  9. package/src/App.css +184 -0
  10. package/src/App.tsx +122 -0
  11. package/src/BordasSombras.mdx +343 -0
  12. package/src/Colors.mdx +393 -0
  13. package/src/GrelhaLayout.mdx +375 -0
  14. package/src/Icons.mdx +42 -0
  15. package/src/MovimentoBreakpoints.mdx +379 -0
  16. package/src/Spacing.mdx +231 -0
  17. package/src/Typography.mdx +321 -0
  18. package/src/assets/Mascots/vava/estaticos/vava-acenar.svg +3 -0
  19. package/src/assets/Mascots/vava/estaticos/vava-acertou.svg +3 -0
  20. package/src/assets/Mascots/vava/estaticos/vava-celebrar.svg +3 -0
  21. package/src/assets/Mascots/vava/estaticos/vava-com-autocolantes.svg +3 -0
  22. package/src/assets/Mascots/vava/estaticos/vava-dormir.svg +3 -0
  23. package/src/assets/Mascots/vava/estaticos/vava-errou.svg +3 -0
  24. package/src/assets/Mascots/vava/estaticos/vava-feliz.svg +3 -0
  25. package/src/assets/Mascots/vava/estaticos/vava-neutra.svg +3 -0
  26. package/src/assets/Mascots/vava/estaticos/vava-original.svg +3 -0
  27. package/src/assets/Mascots/vava/estaticos/vava-pensar.svg +3 -0
  28. package/src/assets/Mascots/vava/estaticos/vava-surpresa.svg +3 -0
  29. package/src/assets/Mascots/yaya/avatar/yaya-avatar-falar.svg +7 -0
  30. package/src/assets/Mascots/yaya/avatar/yaya-avatar-feliz.svg +7 -0
  31. package/src/assets/Mascots/yaya/avatar/yaya-avatar-transparente.svg +6 -0
  32. package/src/assets/Mascots/yaya/estaticos/yaya-acertou.svg +13 -0
  33. package/src/assets/Mascots/yaya/estaticos/yaya-celebrar.svg +13 -0
  34. package/src/assets/Mascots/yaya/estaticos/yaya-dormir.svg +13 -0
  35. package/src/assets/Mascots/yaya/estaticos/yaya-errou.svg +13 -0
  36. package/src/assets/Mascots/yaya/estaticos/yaya-falar.svg +13 -0
  37. package/src/assets/Mascots/yaya/estaticos/yaya-feliz.svg +13 -0
  38. package/src/assets/Mascots/yaya/estaticos/yaya-neutra.svg +13 -0
  39. package/src/assets/Mascots/yaya/estaticos/yaya-pensar.svg +13 -0
  40. package/src/assets/Mascots/yaya/estaticos/yaya-surpresa.svg +13 -0
  41. package/src/assets/hero.png +0 -0
  42. package/src/assets/react.svg +1 -0
  43. package/src/assets/vite.svg +1 -0
  44. package/src/components/Buttons/BackButton/BackButton.stories.tsx +86 -0
  45. package/src/components/Buttons/BackButton/BackButton.tsx +107 -0
  46. package/src/components/Buttons/FabButton/FabButton.stories.tsx +74 -0
  47. package/src/components/Buttons/FabButton/FabButton.tsx +161 -0
  48. package/src/components/Buttons/IconButton/IconButton.stories.tsx +99 -0
  49. package/src/components/Buttons/IconButton/IconButton.tsx +189 -0
  50. package/src/components/Buttons/JourneyButton/JourneyButton.stories.tsx +80 -0
  51. package/src/components/Buttons/JourneyButton/JourneyButton.tsx +194 -0
  52. package/src/components/Buttons/MainButton/MainButton.stories.tsx +144 -0
  53. package/src/components/Buttons/MainButton/MainButton.tsx +258 -0
  54. package/src/components/Buttons/WordChip/WordChip.stories.tsx +84 -0
  55. package/src/components/Buttons/WordChip/WordChip.tsx +157 -0
  56. package/src/components/Buttons/WordChip/index.ts +1 -0
  57. package/src/components/Cards/AnswerOptionCard/AnswerOptionCard.stories.tsx +181 -0
  58. package/src/components/Cards/AnswerOptionCard/AnswerOptionCard.tsx +201 -0
  59. package/src/components/Cards/BaseCard/BaseCard.stories.tsx +148 -0
  60. package/src/components/Cards/BaseCard/BaseCard.tsx +181 -0
  61. package/src/components/Cards/BoardingPassCard/BoardingPassCard.stories.tsx +205 -0
  62. package/src/components/Cards/BoardingPassCard/BoardingPassCard.tsx +384 -0
  63. package/src/components/Cards/BoardingPassCard/FlightIcon.tsx +21 -0
  64. package/src/components/Cards/BoardingPassCard/FlightIcon.web.tsx +18 -0
  65. package/src/components/Cards/CountryCard/CountryCard.stories.tsx +97 -0
  66. package/src/components/Cards/CountryCard/CountryCard.tsx +185 -0
  67. package/src/components/Cards/LevelProgressCard/LevelProgressCard.stories.tsx +61 -0
  68. package/src/components/Cards/LevelProgressCard/LevelProgressCard.tsx +143 -0
  69. package/src/components/Cards/ModuleCard/ModuleCard.stories.tsx +108 -0
  70. package/src/components/Cards/ModuleCard/ModuleCard.tsx +304 -0
  71. package/src/components/Cards/ModuleCompletionCard/ModuleCompletionCard.stories.tsx +53 -0
  72. package/src/components/Cards/ModuleCompletionCard/ModuleCompletionCard.tsx +167 -0
  73. package/src/components/Cards/Passport/PassportCover.tsx +114 -0
  74. package/src/components/Cards/Passport/PassportIDPage.tsx +217 -0
  75. package/src/components/Cards/Passport/PassportStampPage.tsx +253 -0
  76. package/src/components/Cards/Passport/PassportViewer.stories.tsx +95 -0
  77. package/src/components/Cards/Passport/PassportViewer.tsx +328 -0
  78. package/src/components/Cards/PlanCard/PlanCard.stories.tsx +159 -0
  79. package/src/components/Cards/PlanCard/PlanCard.tsx +317 -0
  80. package/src/components/Cards/PricingCard/PricingCard.stories.tsx +83 -0
  81. package/src/components/Cards/PricingCard/PricingCard.tsx +144 -0
  82. package/src/components/Cards/SelectionCard/SelectionCard.stories.tsx +77 -0
  83. package/src/components/Cards/SelectionCard/SelectionCard.tsx +111 -0
  84. package/src/components/Cards/TeacherCard/TeacherCard.stories.tsx +126 -0
  85. package/src/components/Cards/TeacherCard/TeacherCard.tsx +236 -0
  86. package/src/components/Charts/WeeklyProgressChart/WeeklyProgressChart.stories.tsx +89 -0
  87. package/src/components/Charts/WeeklyProgressChart/WeeklyProgressChart.tsx +179 -0
  88. package/src/components/Charts/WeeklyProgressChart/index.ts +1 -0
  89. package/src/components/DataDisplay/Avatar/Avatar.stories.tsx +85 -0
  90. package/src/components/DataDisplay/Avatar/Avatar.tsx +118 -0
  91. package/src/components/DataDisplay/Avatar/index.ts +1 -0
  92. package/src/components/DataDisplay/Badge/Badge.stories.tsx +62 -0
  93. package/src/components/DataDisplay/Badge/Badge.tsx +146 -0
  94. package/src/components/DataDisplay/Flag/Flag.stories.tsx +98 -0
  95. package/src/components/DataDisplay/Flag/Flag.tsx +347 -0
  96. package/src/components/DataDisplay/Flag/Flag.web.tsx +334 -0
  97. package/src/components/DataDisplay/InstructionBubble/InstructionBubble.stories.tsx +80 -0
  98. package/src/components/DataDisplay/InstructionBubble/InstructionBubble.tsx +86 -0
  99. package/src/components/DataDisplay/LevelBadge/LevelBadge.stories.tsx +99 -0
  100. package/src/components/DataDisplay/LevelBadge/LevelBadge.tsx +118 -0
  101. package/src/components/DataDisplay/ProgressBar/ProgressBar.stories.tsx +95 -0
  102. package/src/components/DataDisplay/ProgressBar/ProgressBar.tsx +117 -0
  103. package/src/components/DataDisplay/StatBadge/StatBadge.stories.tsx +82 -0
  104. package/src/components/DataDisplay/StatBadge/StatBadge.tsx +135 -0
  105. package/src/components/DataDisplay/StatBadge/index.ts +1 -0
  106. package/src/components/DataDisplay/Tag/Tag.stories.tsx +63 -0
  107. package/src/components/DataDisplay/Tag/Tag.tsx +148 -0
  108. package/src/components/Feedback/FeedbackBottomSheet/FeedbackBottomSheet.stories.tsx +74 -0
  109. package/src/components/Feedback/FeedbackBottomSheet/FeedbackBottomSheet.tsx +200 -0
  110. package/src/components/Forms/Checkbox/Checkbox.stories.tsx +60 -0
  111. package/src/components/Forms/Checkbox/Checkbox.tsx +127 -0
  112. package/src/components/Forms/Checkbox/index.ts +1 -0
  113. package/src/components/Forms/DaySelector/DaySelector.stories.tsx +62 -0
  114. package/src/components/Forms/DaySelector/DaySelector.tsx +133 -0
  115. package/src/components/Forms/Radio/Radio.stories.tsx +61 -0
  116. package/src/components/Forms/Radio/Radio.tsx +141 -0
  117. package/src/components/Forms/Radio/index.ts +1 -0
  118. package/src/components/Forms/SegmentedToggle/SegmentedToggle.stories.tsx +67 -0
  119. package/src/components/Forms/SegmentedToggle/SegmentedToggle.tsx +150 -0
  120. package/src/components/Forms/SegmentedToggle/index.ts +1 -0
  121. package/src/components/Forms/Select/Select.stories.tsx +59 -0
  122. package/src/components/Forms/Select/Select.tsx +255 -0
  123. package/src/components/Forms/Select/index.ts +1 -0
  124. package/src/components/Forms/Switch/Switch.stories.tsx +57 -0
  125. package/src/components/Forms/Switch/Switch.tsx +120 -0
  126. package/src/components/Forms/Switch/index.ts +1 -0
  127. package/src/components/Forms/TimeSlot/TimeSlot.stories.tsx +62 -0
  128. package/src/components/Forms/TimeSlot/TimeSlot.tsx +116 -0
  129. package/src/components/Headers/CountryHeader/CountryHeader.stories.tsx +114 -0
  130. package/src/components/Headers/CountryHeader/CountryHeader.tsx +147 -0
  131. package/src/components/Headers/ProfileHeader/ProfileHeader.stories.tsx +141 -0
  132. package/src/components/Headers/ProfileHeader/ProfileHeader.tsx +233 -0
  133. package/src/components/Inputs/FillInTheBlank/FillInTheBlank.stories.tsx +333 -0
  134. package/src/components/Inputs/FillInTheBlank/FillInTheBlank.tsx +287 -0
  135. package/src/components/Inputs/FillInTheBlank/index.ts +1 -0
  136. package/src/components/Inputs/TextInput/TextInput.stories.tsx +103 -0
  137. package/src/components/Inputs/TextInput/TextInput.tsx +187 -0
  138. package/src/components/Mascots/Vava/Vava.stories.tsx +114 -0
  139. package/src/components/Mascots/Vava/Vava.tsx +276 -0
  140. package/src/components/Mascots/Yaya/Yaya.stories.tsx +98 -0
  141. package/src/components/Mascots/Yaya/Yaya.tsx +233 -0
  142. package/src/components/Navigation/BottomTabBar/BottomTabBar.stories.tsx +68 -0
  143. package/src/components/Navigation/BottomTabBar/BottomTabBar.tsx +85 -0
  144. package/src/components/Navigation/BottomTabBar/BottomTabBarItem.tsx +68 -0
  145. package/src/components/Navigation/BottomTabBar/index.ts +2 -0
  146. package/src/components/Navigation/PaginationDots/PaginationDots.stories.tsx +62 -0
  147. package/src/components/Navigation/PaginationDots/PaginationDots.tsx +81 -0
  148. package/src/components/Navigation/PaginationDots/index.ts +1 -0
  149. package/src/components/Navigation/ProgressMap/ProgressMap.stories.tsx +116 -0
  150. package/src/components/Navigation/ProgressMap/ProgressMap.tsx +827 -0
  151. package/src/components/Navigation/WorldProgressMap/WorldProgressMap.stories.tsx +169 -0
  152. package/src/components/Navigation/WorldProgressMap/WorldProgressMap.tsx +790 -0
  153. package/src/components/Typography/Typography.stories.tsx +127 -0
  154. package/src/components/Typography/Typography.tsx +53 -0
  155. package/src/hooks/useResponsive.ts +41 -0
  156. package/src/hooks/useTheme.ts +32 -0
  157. package/src/index.css +355 -0
  158. package/src/index.ts +28 -0
  159. package/src/main.tsx +10 -0
  160. package/src/mocks/codegenNativeComponent.ts +5 -0
  161. package/src/styles/countryThemes.ts +79 -0
  162. package/src/styles/theme.ts +297 -0
  163. package/src/utils/iconMap.ts +23 -0
  164. package/tsconfig.app.json +25 -0
  165. package/tsconfig.json +7 -0
  166. package/tsconfig.node.json +24 -0
  167. package/vite.config.ts +51 -0
  168. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,103 @@
1
+
2
+ // eslint-disable-next-line storybook/no-renderer-packages
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { View } from 'react-native';
5
+ import { TextInput } from './TextInput';
6
+
7
+ const meta = {
8
+ title: 'Components/Inputs/TextInput',
9
+ component: TextInput,
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ tags: ['autodocs'],
14
+ argTypes: {
15
+ label: {
16
+ control: 'text',
17
+ },
18
+ error: {
19
+ control: 'text',
20
+ },
21
+ helperText: {
22
+ control: 'text',
23
+ },
24
+ size: {
25
+ control: 'radio',
26
+ options: ['sm', 'md', 'lg'],
27
+ },
28
+ disabled: {
29
+ control: 'boolean',
30
+ },
31
+ },
32
+ decorators: [
33
+ (Story) => (
34
+ <View style={{ padding: 24, width: '100%', minWidth: 320, alignItems: 'center' }}>
35
+ <Story />
36
+ </View>
37
+ ),
38
+ ],
39
+ } satisfies Meta<any>;
40
+
41
+ export default meta;
42
+ type Story = StoryObj<any>;
43
+
44
+ // Default State
45
+ export const Default: Story = {
46
+ args: {
47
+ label: 'Nome',
48
+ placeholder: 'Ex: Sofia',
49
+ },
50
+ };
51
+
52
+ // Filled State
53
+ export const Filled: Story = {
54
+ args: {
55
+ label: 'Email',
56
+ value: 'sofia@xetwa.com',
57
+ },
58
+ };
59
+
60
+ // Error State
61
+ export const Error: Story = {
62
+ args: {
63
+ label: 'Email',
64
+ value: 'sofia@',
65
+ error: 'Endereço de email inválido',
66
+ },
67
+ };
68
+
69
+ // Helper Text
70
+ export const WithHelperText: Story = {
71
+ args: {
72
+ label: 'Password',
73
+ placeholder: 'A tua senha secreta',
74
+ secureTextEntry: true,
75
+ helperText: 'A password deve ter pelo menos 8 caracteres.',
76
+ },
77
+ };
78
+
79
+ // Disabled
80
+ export const Disabled: Story = {
81
+ args: {
82
+ label: 'Nome de Utilizador',
83
+ value: 'sofia_xetwa',
84
+ disabled: true,
85
+ },
86
+ };
87
+
88
+ // Sizes
89
+ export const Small: Story = {
90
+ args: {
91
+ label: 'Pesquisa',
92
+ placeholder: 'O que procuras?',
93
+ size: 'sm',
94
+ },
95
+ };
96
+
97
+ export const Large: Story = {
98
+ args: {
99
+ label: 'Código de Convite',
100
+ placeholder: 'XET-1234',
101
+ size: 'lg',
102
+ },
103
+ };
@@ -0,0 +1,187 @@
1
+ import { useState, forwardRef } from 'react';
2
+ import { View, TextInput as RNTextInput, StyleSheet, Platform } from 'react-native';
3
+ import type {
4
+ ViewStyle,
5
+ TextStyle,
6
+ TextInputProps as RNTextInputProps,
7
+ } from 'react-native';
8
+ import { theme } from '../../../styles/theme';
9
+ import { useResponsive } from '../../../hooks/useResponsive';
10
+ import { Typography } from '../../Typography/Typography';
11
+
12
+ export interface TextInputProps extends Omit<RNTextInputProps, 'style'> {
13
+ label?: string;
14
+ error?: string;
15
+ helperText?: string;
16
+ size?: 'sm' | 'md' | 'lg';
17
+ disabled?: boolean;
18
+ style?: ViewStyle | ViewStyle[];
19
+ inputStyle?: TextStyle | TextStyle[];
20
+ containerStyle?: ViewStyle | ViewStyle[];
21
+ }
22
+
23
+ export const TextInput = forwardRef<RNTextInput, TextInputProps>(
24
+ (
25
+ {
26
+ label,
27
+ error,
28
+ helperText,
29
+ size = 'md',
30
+ disabled = false,
31
+ style,
32
+ inputStyle,
33
+ containerStyle,
34
+ onFocus,
35
+ onBlur,
36
+ ...props
37
+ },
38
+ ref
39
+ ) => {
40
+ const [isFocused, setIsFocused] = useState(false);
41
+ const { scale, scaleText } = useResponsive();
42
+
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ const handleFocus = (e: any) => {
45
+ setIsFocused(true);
46
+ if (onFocus) onFocus(e);
47
+ };
48
+
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ const handleBlur = (e: any) => {
51
+ setIsFocused(false);
52
+ if (onBlur) onBlur(e);
53
+ };
54
+
55
+ // Calculate dynamic styles
56
+ const hasError = !!error;
57
+
58
+ const getWrapperStyles = (): ViewStyle => {
59
+ let borderColor = theme.colors.brown[100];
60
+ let backgroundColor = theme.colors.white;
61
+ let shadowStyle: ViewStyle = {};
62
+
63
+ if (disabled) {
64
+ backgroundColor = theme.colors.brown[50];
65
+ } else if (hasError) {
66
+ borderColor = theme.colors.error;
67
+ backgroundColor = '#fff9f8';
68
+ if (Platform.OS === 'web') {
69
+ shadowStyle = { boxShadow: `0 0 0 ${scale(3)}px rgba(207, 91, 72, 0.18)` } as unknown as ViewStyle;
70
+ } else {
71
+ shadowStyle = { ...theme.shadows.sm, shadowColor: theme.colors.error };
72
+ }
73
+ } else if (isFocused) {
74
+ borderColor = theme.colors.primary;
75
+ if (Platform.OS === 'web') {
76
+ shadowStyle = { boxShadow: `0 0 0 ${scale(3)}px rgba(242, 151, 75, 0.22)` } as unknown as ViewStyle;
77
+ } else {
78
+ shadowStyle = theme.shadows.focus;
79
+ }
80
+ }
81
+
82
+ const sizeHeight = {
83
+ sm: scale(42),
84
+ md: scale(50),
85
+ lg: scale(58),
86
+ }[size];
87
+
88
+ return {
89
+ borderWidth: 2,
90
+ borderColor,
91
+ backgroundColor,
92
+ height: sizeHeight,
93
+ ...shadowStyle,
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ ...(disabled && Platform.OS === 'web' ? { cursor: 'not-allowed' as any } : {}),
96
+ };
97
+ };
98
+
99
+ const messageText = error || helperText;
100
+ const messageColor = error ? theme.colors.error : theme.colors.brown[500];
101
+
102
+ return (
103
+ <View style={[styles.root, style]}>
104
+ {label && (
105
+ <Typography style={[
106
+ styles.label,
107
+ { fontSize: scaleText(12), marginBottom: scale(6) },
108
+ disabled && { color: theme.colors.brown[400] }
109
+ ]}>
110
+ {label}
111
+ </Typography>
112
+ )}
113
+
114
+ <View style={[
115
+ styles.inputWrapper,
116
+ getWrapperStyles(),
117
+ {
118
+ borderRadius: scale(14),
119
+ paddingHorizontal: scale(16),
120
+ },
121
+ containerStyle
122
+ ]}>
123
+ <RNTextInput
124
+ ref={ref}
125
+ editable={!disabled}
126
+ onFocus={handleFocus}
127
+ onBlur={handleBlur}
128
+ placeholderTextColor={theme.colors.brown[300]}
129
+ style={[
130
+ styles.input,
131
+ {
132
+ fontSize: scaleText(15),
133
+ },
134
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
135
+ Platform.OS === 'web' && ({ outlineStyle: 'none' } as any),
136
+ disabled && { color: theme.colors.brown[400] },
137
+ inputStyle,
138
+ ]}
139
+ {...props}
140
+ />
141
+ </View>
142
+
143
+ {messageText && (
144
+ <Typography style={[styles.message, { color: messageColor, fontSize: scaleText(12), marginTop: scale(5) }]}>
145
+ {messageText}
146
+ </Typography>
147
+ )}
148
+ </View>
149
+ );
150
+ }
151
+ );
152
+
153
+ TextInput.displayName = 'TextInput';
154
+
155
+ const styles = StyleSheet.create({
156
+ root: {
157
+ width: '100%',
158
+ maxWidth: 480, // Default constraint for desktop
159
+ },
160
+ label: {
161
+
162
+ fontSize: 12,
163
+ color: theme.colors.brown[800],
164
+ marginBottom: 6,
165
+ },
166
+ inputWrapper: {
167
+ borderRadius: 14,
168
+ paddingHorizontal: 16,
169
+ justifyContent: 'center',
170
+ flexDirection: 'row',
171
+ alignItems: 'center',
172
+ overflow: 'hidden',
173
+ },
174
+ input: {
175
+ flex: 1,
176
+ height: '100%',
177
+
178
+ fontSize: 15,
179
+ color: theme.colors.brown[800],
180
+ padding: 0, // Reset RN default paddings
181
+ },
182
+ message: {
183
+
184
+ fontSize: 12,
185
+ marginTop: 5,
186
+ },
187
+ });
@@ -0,0 +1,114 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Vava } from './Vava';
3
+
4
+ const meta = {
5
+ title: 'Mascots/Vava',
6
+ component: Vava,
7
+ parameters: {
8
+ layout: 'centered',
9
+ },
10
+ tags: ['autodocs'],
11
+ argTypes: {
12
+ variant: {
13
+ control: 'select',
14
+ options: [
15
+ 'original',
16
+ 'com-autocolantes',
17
+ 'acenar',
18
+ 'feliz',
19
+ 'neutra',
20
+ 'surpresa',
21
+ 'pensar',
22
+ 'errou',
23
+ 'acertou',
24
+ 'dormir',
25
+ 'celebrar',
26
+ ],
27
+ description: 'A expressão ou estado da mascote Vava.',
28
+ },
29
+ size: {
30
+ control: { type: 'range', min: 50, max: 500, step: 10 },
31
+ description: 'O tamanho do componente (largura e altura em pixels).',
32
+ },
33
+ },
34
+ } satisfies Meta<any>;
35
+
36
+ export default meta;
37
+ type Story = StoryObj<any>;
38
+
39
+ export const Default: Story = {
40
+ args: {
41
+ variant: 'original',
42
+ size: 200,
43
+ },
44
+ };
45
+
46
+ export const ComAutocolantes: Story = {
47
+ args: {
48
+ variant: 'com-autocolantes',
49
+ size: 200,
50
+ },
51
+ };
52
+
53
+ export const Acenar: Story = {
54
+ args: {
55
+ variant: 'acenar',
56
+ size: 200,
57
+ },
58
+ };
59
+
60
+ export const Feliz: Story = {
61
+ args: {
62
+ variant: 'feliz',
63
+ size: 200,
64
+ },
65
+ };
66
+
67
+ export const Neutra: Story = {
68
+ args: {
69
+ variant: 'neutra',
70
+ size: 200,
71
+ },
72
+ };
73
+
74
+ export const Surpresa: Story = {
75
+ args: {
76
+ variant: 'surpresa',
77
+ size: 200,
78
+ },
79
+ };
80
+
81
+ export const Pensar: Story = {
82
+ args: {
83
+ variant: 'pensar',
84
+ size: 200,
85
+ },
86
+ };
87
+
88
+ export const Errou: Story = {
89
+ args: {
90
+ variant: 'errou',
91
+ size: 200,
92
+ },
93
+ };
94
+
95
+ export const Acertou: Story = {
96
+ args: {
97
+ variant: 'acertou',
98
+ size: 200,
99
+ },
100
+ };
101
+
102
+ export const Dormir: Story = {
103
+ args: {
104
+ variant: 'dormir',
105
+ size: 200,
106
+ },
107
+ };
108
+
109
+ export const Celebrar: Story = {
110
+ args: {
111
+ variant: 'celebrar',
112
+ size: 200,
113
+ },
114
+ };
@@ -0,0 +1,276 @@
1
+ import React from 'react';
2
+ import Svg, { Path, Rect, Circle, Ellipse, G, Text as SvgText } from 'react-native-svg';
3
+
4
+ export type VavaVariant =
5
+ | 'original'
6
+ | 'com-autocolantes'
7
+ | 'acenar'
8
+ | 'feliz'
9
+ | 'neutra'
10
+ | 'surpresa'
11
+ | 'pensar'
12
+ | 'errou'
13
+ | 'acertou'
14
+ | 'dormir'
15
+ | 'celebrar';
16
+
17
+ export interface VavaProps {
18
+ /**
19
+ * A expressão ou estado da mascote Vava.
20
+ * @default 'original'
21
+ */
22
+ variant?: VavaVariant;
23
+ /**
24
+ * O tamanho do componente (largura e altura em pixels).
25
+ * @default 200
26
+ */
27
+ size?: number;
28
+ /**
29
+ * Se verdadeiro, renderiza apenas o rosto/cabeça da mascote em formato Avatar.
30
+ * @default false
31
+ */
32
+ avatarOnly?: boolean;
33
+ }
34
+
35
+ /**
36
+ * Componente Vava - A mascote oficial do Xetlingo.
37
+ *
38
+ * Este componente renderiza de forma estática, vetorial e performática as diferentes expressões da Vava.
39
+ * É compatível tanto com React Native (iOS/Android) quanto com React Native Web.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * // Vava neutra com tamanho padrão (200px)
44
+ * <Vava variant="neutra" />
45
+ *
46
+ * // Vava celebrando com 150px de tamanho
47
+ * <Vava variant="celebrar" size={150} />
48
+ * ```
49
+ */
50
+ export const Vava: React.FC<VavaProps> = ({ variant = 'original', size = 200, avatarOnly = false }) => {
51
+ // Normalizar variantes equivalentes nos ficheiros de assets
52
+ const normalizedVariant = variant === 'com-autocolantes' ? 'original' : variant === 'acenar' ? 'feliz' : variant;
53
+
54
+ const renderFace = () => {
55
+ switch (normalizedVariant) {
56
+ case 'feliz':
57
+ return (
58
+ <>
59
+ {/* Olhos felizes fechados */}
60
+ <Path d="M73 95 Q80 86 87 95" stroke="#15100d" strokeWidth={3.2} fill="none" strokeLinecap="round" />
61
+ <Path d="M113 95 Q120 86 127 95" stroke="#15100d" strokeWidth={3.2} fill="none" strokeLinecap="round" />
62
+ {/* Boca feliz aberta */}
63
+ <Path d="M90 100 Q100 111 110 100 Z" fill="#fbe6cf" />
64
+ </>
65
+ );
66
+
67
+ case 'neutra':
68
+ return (
69
+ <>
70
+ {/* Olhos abertos com pupilas e brilho */}
71
+ <Ellipse cx={80} cy={92} rx={8} ry={10} fill="#fff" />
72
+ <Ellipse cx={120} cy={92} rx={8} ry={10} fill="#fff" />
73
+ <Circle cx={81} cy={94} r={4.2} fill="#15100d" />
74
+ <Circle cx={121} cy={94} r={4.2} fill="#15100d" />
75
+ <Circle cx={79} cy={90} r={1.6} fill="#fff" />
76
+ <Circle cx={119} cy={90} r={1.6} fill="#fff" />
77
+ {/* Boca sorridente fechada */}
78
+ <Path d="M92 102 Q100 109 108 102" fill="none" stroke="#fbe6cf" strokeWidth={3} strokeLinecap="round" />
79
+ </>
80
+ );
81
+
82
+ case 'surpresa':
83
+ return (
84
+ <>
85
+ {/* Olhos bem abertos e redondos */}
86
+ <Circle cx={80} cy={92} r={10} fill="#fff" />
87
+ <Circle cx={120} cy={92} r={10} fill="#fff" />
88
+ <Circle cx={80} cy={93} r={5} fill="#15100d" />
89
+ <Circle cx={120} cy={93} r={5} fill="#15100d" />
90
+ <Circle cx={78} cy={90} r={2} fill="#fff" />
91
+ <Circle cx={118} cy={90} r={2} fill="#fff" />
92
+ {/* Boca redonda aberta de surpresa */}
93
+ <Ellipse cx={100} cy={105} rx={6} ry={7} fill="#fbe6cf" />
94
+ </>
95
+ );
96
+
97
+ case 'pensar':
98
+ return (
99
+ <>
100
+ {/* Olhos a olhar para cima e para a direita */}
101
+ <Ellipse cx={80} cy={92} rx={8} ry={10} fill="#fff" />
102
+ <Ellipse cx={120} cy={92} rx={8} ry={10} fill="#fff" />
103
+ <Circle cx={82} cy={88} r={4} fill="#15100d" />
104
+ <Circle cx={122} cy={88} r={4} fill="#15100d" />
105
+ {/* Boca de dúvida (traço reto ligeiramente inclinado) */}
106
+ <Path d="M93 105 L105 103" stroke="#fbe6cf" strokeWidth={3} fill="none" strokeLinecap="round" />
107
+ {/* Ponto de interrogação flutuante */}
108
+ <SvgText x="50" y="50" fontFamily="sans-serif" fontWeight="700" fontSize={30} fill="#f2974b">?</SvgText>
109
+ </>
110
+ );
111
+
112
+ case 'errou':
113
+ return (
114
+ <>
115
+ {/* Sobrancelhas de preocupação */}
116
+ <Path d="M70 84 l11 4" stroke="#15100d" strokeWidth={2.4} strokeLinecap="round" />
117
+ <Path d="M130 84 l-11 4" stroke="#15100d" strokeWidth={2.4} strokeLinecap="round" />
118
+ {/* Olhos tensos a olhar para baixo */}
119
+ <Ellipse cx={80} cy={95} rx={7.5} ry={9} fill="#fff" />
120
+ <Ellipse cx={120} cy={95} rx={7.5} ry={9} fill="#fff" />
121
+ <Circle cx={81} cy={97} r={4} fill="#15100d" />
122
+ <Circle cx={121} cy={97} r={4} fill="#15100d" />
123
+ {/* Boca triste para baixo */}
124
+ <Path d="M94 107 Q100 102 106 107" stroke="#fbe6cf" strokeWidth={3} fill="none" strokeLinecap="round" />
125
+ {/* Gota de suor fria na bochecha */}
126
+ <Path d="M134 84 q3.6 5 0 9 q-3.6 -4 0 -9 Z" fill="#7ab8e0" />
127
+ {/* Ícone flutuante de Erro (Círculo Vermelho com X) */}
128
+ {!avatarOnly && (
129
+ <G transform="translate(150, 58)">
130
+ <Circle r={16} fill="#cf5b48" />
131
+ <Path d="M-6 -6 l12 12 M6 -6 l-12 12" stroke="#fff" strokeWidth={3.5} strokeLinecap="round" />
132
+ </G>
133
+ )}
134
+ </>
135
+ );
136
+
137
+ case 'acertou':
138
+ return (
139
+ <>
140
+ {/* Olhos felizes fechados */}
141
+ <Path d="M73 95 Q80 86 87 95" stroke="#15100d" strokeWidth={3.2} fill="none" strokeLinecap="round" />
142
+ <Path d="M113 95 Q120 86 127 95" stroke="#15100d" strokeWidth={3.2} fill="none" strokeLinecap="round" />
143
+ {/* Boca feliz aberta */}
144
+ <Path d="M90 100 Q100 112 110 100 Z" fill="#fbe6cf" />
145
+ {/* Ícone flutuante de Sucesso (Círculo Verde com Checkmark) */}
146
+ {!avatarOnly && (
147
+ <G transform="translate(150, 58)">
148
+ <Circle r={16} fill="#16915a" />
149
+ <Path d="M-7 0 l4 5 9 -10" stroke="#fff" strokeWidth={3.5} fill="none" strokeLinecap="round" strokeLinejoin="round" />
150
+ </G>
151
+ )}
152
+ </>
153
+ );
154
+
155
+ case 'dormir':
156
+ return (
157
+ <>
158
+ {/* Olhos fechados em curva suave de sono */}
159
+ <Path d="M72 93 Q80 99 88 93" stroke="#15100d" strokeWidth={3} fill="none" strokeLinecap="round" />
160
+ <Path d="M112 93 Q120 99 128 93" stroke="#15100d" strokeWidth={3} fill="none" strokeLinecap="round" />
161
+ {/* Boca em pequeno círculo aberto de sono */}
162
+ <Circle cx={100} cy={105} r={3} fill="none" stroke="#fbe6cf" strokeWidth={2.5} />
163
+ {/* Letras de sono flutuantes (z Z) */}
164
+ <SvgText x="50" y="50" fontFamily="sans-serif" fontWeight="700" fontSize={15} fill="#9a8478">z</SvgText>
165
+ <SvgText x="50" y="50" fontFamily="sans-serif" fontWeight="700" fontSize={20} fill="#9a8478">Z</SvgText>
166
+ </>
167
+ );
168
+
169
+ case 'celebrar':
170
+ return (
171
+ <>
172
+ {/* Olhos felizes fechados */}
173
+ <Path d="M73 95 Q80 86 87 95" stroke="#15100d" strokeWidth={3.2} fill="none" strokeLinecap="round" />
174
+ <Path d="M113 95 Q120 86 127 95" stroke="#15100d" strokeWidth={3.2} fill="none" strokeLinecap="round" />
175
+ {/* Boca aberta de celebração sorrindo com interior à mostra */}
176
+ <Path d="M87 99 Q100 116 113 99 Q100 106 87 99 Z" fill="#fff" />
177
+ <Path d="M89 100 Q100 113 111 100" fill="#fbe6cf" />
178
+ </>
179
+ );
180
+
181
+ case 'original':
182
+ default:
183
+ return (
184
+ <>
185
+ {/* Olhos felizes fechados (padrão) */}
186
+ <Path d="M73 95 Q80 86 87 95" stroke="#15100d" strokeWidth={3.2} fill="none" strokeLinecap="round" />
187
+ <Path d="M113 95 Q120 86 127 95" stroke="#15100d" strokeWidth={3.2} fill="none" strokeLinecap="round" />
188
+ {/* Boca feliz aberta */}
189
+ <Path d="M90 100 Q100 111 110 100 Z" fill="#fbe6cf" />
190
+
191
+ {/* Autocolantes/Stickers de Bandeiras espalhados pelo corpo */}
192
+ {/* Bandeira de Portugal (inferior esquerda) */}
193
+ <G transform="translate(60, 148) rotate(-10)">
194
+ <Rect x={-12} y={-9} width={24} height={18} rx={3} fill="#fff" />
195
+ <Rect x={-10} y={-7} width={6.5} height={14} fill="#2a4b9b" />
196
+ <Rect x={-3.5} y={-7} width={6.5} height={14} fill="#fff" />
197
+ <Rect x={3} y={-7} width={6.5} height={14} fill="#d34b3e" />
198
+ </G>
199
+ {/* Bandeira do Brasil (inferior direita) */}
200
+ <G transform="translate(140, 150) rotate(9)">
201
+ <Rect x={-12} y={-9} width={24} height={18} rx={3} fill="#fff" />
202
+ <Rect x={-10} y={-7} width={6.5} height={14} fill="#16915a" />
203
+ <Rect x={-3.5} y={-7} width={6.5} height={14} fill="#fcd116" />
204
+ <Rect x={3} y={-7} width={6.5} height={14} fill="#d12e2e" />
205
+ <SvgText x={-0.2} y={2} textAnchor="middle" fontSize={8} fill="#16915a">★</SvgText>
206
+ </G>
207
+ {/* Bandeira de Espanha (bochecha direita) */}
208
+ <G transform="translate(142, 82) rotate(8)">
209
+ <Rect x={-12} y={-9} width={24} height={18} rx={3} fill="#fff" />
210
+ <Rect x={-10} y={-7} width={6.5} height={14} fill="#15100d" />
211
+ <Rect x={-3.5} y={-7} width={6.5} height={14} fill="#f6c343" />
212
+ <Rect x={3} y={-7} width={6.5} height={14} fill="#d34b3e" />
213
+ </G>
214
+ {/* Bandeira de França (bochecha esquerda) */}
215
+ <G transform="translate(58, 82) rotate(-8)">
216
+ <Rect x={-11} y={-9} width={22} height={18} rx={3} fill="#fff" />
217
+ <Rect x={-9} y={-7} width={5} height={14} fill="#0050a4" />
218
+ <Rect x={-4} y={-7} width={8} height={14} fill="#fff" />
219
+ <Rect x={4} y={-7} width={5} height={14} fill="#d52b1e" />
220
+ <Path d="M0 -3 l2 3 -2 3 -2 -3 Z" fill="#d52b1e" />
221
+ </G>
222
+ </>
223
+ );
224
+ }
225
+ };
226
+
227
+ return (
228
+ <Svg
229
+ width={size}
230
+ height={size}
231
+ viewBox={avatarOnly ? "35 45 130 130" : "0 0 200 200"}
232
+ role="img"
233
+ aria-label={`Mascote Vava: ${variant}`}
234
+ >
235
+ {/* 1. Antena Superior */}
236
+ <Path
237
+ d="M84 64 q0 -20 16 -20 q16 0 16 20"
238
+ fill="none"
239
+ stroke="#3f2620"
240
+ strokeWidth={7}
241
+ strokeLinecap="round"
242
+ />
243
+ {/* Bases da Antena */}
244
+ <Rect x={84} y={58} width={12} height={10} rx={2} fill="#cf7a30" />
245
+ <Rect x={104} y={58} width={12} height={10} rx={2} fill="#cf7a30" />
246
+
247
+ {/* 2. Corpo do Tablet (Chassis) */}
248
+ <Rect x={44} y={64} width={112} height={104} rx={16} fill="#51332d" />
249
+
250
+ {/* 3. Moldura Interna do Ecrã */}
251
+ <Rect
252
+ x={50}
253
+ y={70}
254
+ width={100}
255
+ height={92}
256
+ rx={12}
257
+ fill="none"
258
+ stroke="#6b4a33"
259
+ strokeWidth={2.5}
260
+ />
261
+
262
+ {/* 4. Faixa Inferior Laranja do Tablet */}
263
+ <Rect x={44} y={116} width={112} height={15} fill="#f2974b" />
264
+
265
+ {/* 5. Botão Home Central */}
266
+ <Rect x={92} y={112} width={16} height={23} rx={3} fill="#cf7a30" />
267
+
268
+ {/* 6. Bochechas Coradas */}
269
+ <Circle cx={66} cy={106} r={6} fill="#f2974b" opacity={0.4} />
270
+ <Circle cx={134} cy={106} r={6} fill="#f2974b" opacity={0.4} />
271
+
272
+ {/* 7. Expressão Facial & Elementos de Expressão (Diferentes de acordo com a Variante) */}
273
+ {renderFace()}
274
+ </Svg>
275
+ );
276
+ };