@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,179 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { View, StyleSheet, Animated } from 'react-native';
3
+ import type { ViewStyle } from 'react-native';
4
+ import { theme } from '../../../styles/theme';
5
+ import { useResponsive } from '../../../hooks/useResponsive';
6
+ import { Typography } from '../../Typography/Typography';
7
+
8
+ export interface DailyProgress {
9
+ dayLabel: string;
10
+ value: number;
11
+ isCurrentDay?: boolean;
12
+ }
13
+
14
+ export interface WeeklyProgressChartProps {
15
+ title: string;
16
+ subtitle?: string;
17
+ data: DailyProgress[];
18
+ maxHeight?: number;
19
+ /** Size variant to automatically adjust dimensions */
20
+ size?: 'sm' | 'md' | 'lg';
21
+ style?: ViewStyle | ViewStyle[];
22
+ }
23
+
24
+ export const WeeklyProgressChart = ({
25
+ title,
26
+ subtitle,
27
+ data,
28
+ maxHeight,
29
+ size = 'md',
30
+ style,
31
+ }: WeeklyProgressChartProps) => {
32
+ const { scale, scaleText } = useResponsive();
33
+
34
+ // Compute dimensions based on size variant
35
+ let defaultMaxHeight = 80;
36
+ let barWidth = 24;
37
+ let titleFontSize = 14;
38
+ let labelFontSize = 12;
39
+
40
+ if (size === 'sm') {
41
+ defaultMaxHeight = 50;
42
+ barWidth = 16;
43
+ titleFontSize = 12;
44
+ labelFontSize = 10;
45
+ } else if (size === 'lg') {
46
+ defaultMaxHeight = 110;
47
+ barWidth = 32;
48
+ titleFontSize = 16;
49
+ labelFontSize = 14;
50
+ }
51
+
52
+ const finalMaxHeight = maxHeight ?? defaultMaxHeight;
53
+
54
+
55
+ // Animation value from 0 to 1
56
+ const animValue = useRef(new Animated.Value(0)).current;
57
+
58
+ useEffect(() => {
59
+ // Reset and animate on mount or data change
60
+ animValue.setValue(0);
61
+ Animated.timing(animValue, {
62
+ toValue: 1,
63
+ duration: 600,
64
+ easing: (value) => {
65
+ // Simple ease-out cubic
66
+ return 1 - Math.pow(1 - value, 3);
67
+ },
68
+ useNativeDriver: false, // Cannot use native driver for height/layout animations easily
69
+ }).start();
70
+ }, [animValue, data]);
71
+
72
+ // Find max value in data to scale bars relative to the highest value.
73
+ // If all values are 0, prevent division by zero by setting a default max of 1.
74
+ const maxValue = Math.max(...data.map(d => d.value), 1);
75
+
76
+ return (
77
+ <View style={[styles.container, style]}>
78
+ {/* Header / Title */}
79
+ <View style={styles.header}>
80
+ <Typography style={[styles.title, { fontSize: scaleText(titleFontSize) }]}>
81
+ {title.toUpperCase()}
82
+ {subtitle ? ` · ${subtitle.toUpperCase()}` : ''}
83
+ </Typography>
84
+ </View>
85
+
86
+ {/* Chart Area */}
87
+ <View style={styles.chartContainer}>
88
+ {data.map((item, index) => {
89
+ // Calculate the target height for this specific bar
90
+ const targetHeight = (item.value / maxValue) * scale(finalMaxHeight);
91
+
92
+ // Interpolate the animation value to the target height
93
+ const animatedHeight = animValue.interpolate({
94
+ inputRange: [0, 1],
95
+ outputRange: [0, targetHeight],
96
+ });
97
+
98
+ const isHighlighted = item.isCurrentDay;
99
+
100
+ // Using an opacity on primary to match the light peach/orange color
101
+ const barColor = isHighlighted
102
+ ? theme.colors.primary
103
+ : 'rgba(242, 151, 75, 0.35)'; // Light peach tone
104
+
105
+ const labelColor = isHighlighted
106
+ ? theme.colors.brown[800]
107
+ : theme.colors.brown[500];
108
+
109
+ return (
110
+ <View key={`bar-${index}`} style={styles.barWrapper}>
111
+ <View style={[styles.barTrack, { height: scale(finalMaxHeight) }]}>
112
+ <Animated.View
113
+ style={[
114
+ styles.barFill,
115
+ {
116
+ height: animatedHeight,
117
+ backgroundColor: barColor,
118
+ width: scale(barWidth),
119
+ borderRadius: scale(8),
120
+ }
121
+ ]}
122
+ />
123
+ </View>
124
+ <Typography
125
+ style={[
126
+ styles.dayLabel,
127
+ {
128
+ color: labelColor,
129
+ fontSize: scaleText(labelFontSize),
130
+
131
+ marginTop: scale(12),
132
+ }
133
+ ]}
134
+ >
135
+ {item.dayLabel}
136
+ </Typography>
137
+ </View>
138
+ );
139
+ })}
140
+ </View>
141
+ </View>
142
+ );
143
+ };
144
+
145
+ const styles = StyleSheet.create({
146
+ container: {
147
+ alignSelf: 'stretch',
148
+ paddingVertical: 16,
149
+ },
150
+ header: {
151
+ marginBottom: 24,
152
+ },
153
+ title: {
154
+
155
+ color: theme.colors.brown[500],
156
+
157
+ letterSpacing: 0.5,
158
+ },
159
+ chartContainer: {
160
+ flexDirection: 'row',
161
+ alignItems: 'flex-end',
162
+ justifyContent: 'space-between',
163
+ width: '100%',
164
+ },
165
+ barWrapper: {
166
+ flex: 1,
167
+ alignItems: 'center',
168
+ justifyContent: 'flex-end',
169
+ },
170
+ barTrack: {
171
+ justifyContent: 'flex-end', // Aligns the bar to the bottom
172
+ },
173
+ barFill: {
174
+ // animated styles applied inline
175
+ },
176
+ dayLabel: {
177
+
178
+ },
179
+ });
@@ -0,0 +1 @@
1
+ export * from './WeeklyProgressChart';
@@ -0,0 +1,85 @@
1
+ // eslint-disable-next-line storybook/no-renderer-packages
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { View } from 'react-native';
4
+ import { Avatar } from './Avatar';
5
+ import { theme } from '../../../styles/theme';
6
+
7
+ const meta = {
8
+ title: 'Components/DataDisplay/Avatar',
9
+ component: Avatar,
10
+ parameters: {
11
+ layout: 'padded',
12
+ docs: {
13
+ description: {
14
+ component: 'O **Avatar** é utilizado para representar utilizadores ou professores.\n\nPossui fallback automático: tenta carregar a imagem `src`. Se não existir ou der erro, mostra as `initials`. Se nem iniciais houver, mostra um ícone genérico de utilizador.\n\n### Exemplo de Uso\n```tsx\n<Avatar \n src="https://exemplo.com/foto.jpg"\n initials="AB"\n size="lg"\n variant="circle"\n/>\n```',
15
+ }
16
+ }
17
+ },
18
+ tags: ['autodocs'],
19
+ argTypes: {
20
+ size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] },
21
+ variant: { control: 'radio', options: ['circle', 'square'] },
22
+ src: { control: 'text' },
23
+ initials: { control: 'text' },
24
+ color: { control: 'color' },
25
+ },
26
+ decorators: [
27
+ (Story) => (
28
+ <View style={{ padding: 24, backgroundColor: theme.colors.cream, alignItems: 'center' }}>
29
+ <Story />
30
+ </View>
31
+ ),
32
+ ],
33
+ } satisfies Meta<any>;
34
+
35
+ export default meta;
36
+ type Story = StoryObj<any>;
37
+
38
+ export const DefaultCircle: Story = {
39
+ args: {
40
+ size: 'md',
41
+ variant: 'circle',
42
+ initials: 'V',
43
+ },
44
+ };
45
+
46
+ export const SquareVariant: Story = {
47
+ args: {
48
+ size: 'md',
49
+ variant: 'square',
50
+ initials: 'C',
51
+ color: theme.colors.brown[800],
52
+ },
53
+ };
54
+
55
+ export const WithImage: Story = {
56
+ args: {
57
+ size: 'lg',
58
+ variant: 'circle',
59
+ src: 'https://i.pravatar.cc/150?img=68',
60
+ },
61
+ };
62
+
63
+ export const AllSizesCircle: Story = {
64
+ render: () => (
65
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
66
+ <Avatar size="xs" initials="xs" />
67
+ <Avatar size="sm" initials="sm" />
68
+ <Avatar size="md" initials="md" />
69
+ <Avatar size="lg" initials="lg" />
70
+ <Avatar size="xl" initials="xl" />
71
+ </View>
72
+ ),
73
+ };
74
+
75
+ export const AllSizesSquare: Story = {
76
+ render: () => (
77
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
78
+ <Avatar size="xs" variant="square" initials="xs" color={theme.colors.success} />
79
+ <Avatar size="sm" variant="square" initials="sm" color={theme.colors.success} />
80
+ <Avatar size="md" variant="square" initials="md" color={theme.colors.success} />
81
+ <Avatar size="lg" variant="square" initials="lg" color={theme.colors.success} />
82
+ <Avatar size="xl" variant="square" initials="xl" color={theme.colors.success} />
83
+ </View>
84
+ ),
85
+ };
@@ -0,0 +1,118 @@
1
+
2
+ import { View, StyleSheet, Image } from 'react-native';
3
+ import type { ViewStyle, ImageSourcePropType } from 'react-native';
4
+ import { theme } from '../../../styles/theme';
5
+ import { useResponsive } from '../../../hooks/useResponsive';
6
+ import { Typography } from '../../Typography/Typography';
7
+
8
+ export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
9
+ export type AvatarVariant = 'circle' | 'square';
10
+
11
+ /**
12
+ * Componente Avatar para exibir imagens de perfil, iniciais do utilizador ou ícones de fallback.
13
+ */
14
+ export interface AvatarProps {
15
+ /** Fonte da imagem (URI remoto ou require local). */
16
+ src?: string | ImageSourcePropType;
17
+ /** Iniciais a mostrar caso não exista imagem (ex: 'WS'). */
18
+ initials?: string;
19
+ /** Tamanho predefinido do avatar. */
20
+ size?: AvatarSize;
21
+ /** O formato do avatar (circular ou com cantos arredondados). */
22
+ variant?: AvatarVariant;
23
+ /** Cor de fundo para o fallback de iniciais. */
24
+ color?: string;
25
+ /** Estilos adicionais para o contentor. */
26
+ style?: ViewStyle | ViewStyle[];
27
+ /** Função disparada ao clicar no avatar. */
28
+ onPress?: () => void;
29
+ }
30
+
31
+ const SIZE_MAP: Record<AvatarSize, number> = {
32
+ xs: 28,
33
+ sm: 36,
34
+ md: 48,
35
+ lg: 60,
36
+ xl: 80,
37
+ };
38
+
39
+ const SQUARE_RADIUS_MAP: Record<AvatarSize, number> = {
40
+ xs: 8,
41
+ sm: 10,
42
+ md: 14,
43
+ lg: 18,
44
+ xl: 22,
45
+ };
46
+
47
+ const FONT_SIZE_MAP: Record<AvatarSize, number> = {
48
+ xs: 12,
49
+ sm: 14,
50
+ md: 18,
51
+ lg: 22,
52
+ xl: 28,
53
+ };
54
+
55
+ export const Avatar = ({
56
+ size = 'md',
57
+ variant = 'circle',
58
+ src,
59
+ initials,
60
+ color = theme.colors.primary,
61
+ style,
62
+ }: AvatarProps) => {
63
+ const { scale, scaleText } = useResponsive();
64
+
65
+ const pxSize = scale(SIZE_MAP[size]);
66
+ const borderRadius = variant === 'circle'
67
+ ? pxSize / 2
68
+ : scale(SQUARE_RADIUS_MAP[size]);
69
+
70
+ const fontSize = scaleText(FONT_SIZE_MAP[size]);
71
+ const lineHeight = fontSize * 1.2;
72
+
73
+ const containerStyles = [
74
+ styles.container,
75
+ {
76
+ width: pxSize,
77
+ height: pxSize,
78
+ borderRadius,
79
+ backgroundColor: src ? 'transparent' : color,
80
+ },
81
+ style,
82
+ ];
83
+
84
+ if (src) {
85
+ return (
86
+ <View style={containerStyles}>
87
+ <Image
88
+ source={typeof src === 'string' ? { uri: src } : src}
89
+ style={{ width: pxSize, height: pxSize, borderRadius }}
90
+ resizeMode="cover"
91
+ />
92
+ </View>
93
+ );
94
+ }
95
+
96
+ return (
97
+ <View style={containerStyles}>
98
+ {initials ? (
99
+ <Typography style={[styles.text, { fontSize, lineHeight }]}>
100
+ {initials}
101
+ </Typography>
102
+ ) : null}
103
+ </View>
104
+ );
105
+ };
106
+
107
+ const styles = StyleSheet.create({
108
+ container: {
109
+ justifyContent: 'center',
110
+ alignItems: 'center',
111
+ overflow: 'hidden',
112
+ },
113
+ text: {
114
+
115
+ color: theme.colors.white,
116
+ textAlign: 'center',
117
+ },
118
+ });
@@ -0,0 +1 @@
1
+ export * from './Avatar';
@@ -0,0 +1,62 @@
1
+ // eslint-disable-next-line storybook/no-renderer-packages
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { View } from 'react-native';
4
+ import { Badge } from './Badge';
5
+
6
+ const meta = {
7
+ title: 'Components/DataDisplay/Badge',
8
+ component: Badge,
9
+ parameters: {
10
+ layout: 'padded',
11
+ docs: {
12
+ description: {
13
+ component: 'O **Badge** é utilizado para mostrar pequenas quantidades numéricas (ex: notificações), etiquetas curtas, ou pequenos ícones de status.\n\n### Exemplo de Uso\n```tsx\n<Badge \n content="3"\n variant="error"\n size="md"\n outline={true}\n/>\n```',
14
+ }
15
+ }
16
+ },
17
+ tags: ['autodocs'],
18
+ argTypes: {
19
+ variant: { control: 'radio', options: ['neutro', 'primary', 'success', 'error'] },
20
+ size: { control: 'radio', options: ['xs', 'sm', 'md', 'lg', 'xl'] },
21
+ content: { control: 'text' },
22
+ },
23
+ decorators: [
24
+ (Story) => (
25
+ <View style={{ padding: 24, flexDirection: 'row', gap: 16 }}>
26
+ <Story />
27
+ </View>
28
+ ),
29
+ ],
30
+ } satisfies Meta<any>;
31
+
32
+ export default meta;
33
+ type Story = StoryObj<any>;
34
+
35
+ export const Default: Story = {
36
+ args: { content: '3', variant: 'primary', size: 'sm' },
37
+ };
38
+
39
+ export const Variants: Story = {
40
+ args: { content: '' },
41
+ render: () => (
42
+ <View style={{ flexDirection: 'row', gap: 10 }}>
43
+ <Badge content="3" variant="primary" />
44
+ <Badge content="✓" variant="success" />
45
+ <Badge content="!" variant="error" />
46
+ <Badge content="12" variant="neutro" />
47
+ </View>
48
+ ),
49
+ };
50
+
51
+ export const Sizes: Story = {
52
+ args: { content: '' },
53
+ render: () => (
54
+ <View style={{ flexDirection: 'row', gap: 10, alignItems: 'center' }}>
55
+ <Badge content="1" size="xs" />
56
+ <Badge content="2" size="sm" />
57
+ <Badge content="3" size="md" />
58
+ <Badge content="4" size="lg" />
59
+ <Badge content="5" size="xl" />
60
+ </View>
61
+ ),
62
+ };
@@ -0,0 +1,146 @@
1
+ import { View, StyleSheet } from 'react-native';
2
+ import type { ViewStyle } from 'react-native';
3
+ import { theme } from '../../../styles/theme';
4
+ import type { ReactNode } from 'react';
5
+ import { Typography } from '../../Typography/Typography';
6
+
7
+ export type BadgeVariant = 'neutro' | 'primary' | 'success' | 'error' | 'disabled';
8
+ export type BadgeSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
9
+
10
+ /**
11
+ * Distintivo (Badge) usado para exibir notificações, números ou pequenos ícones.
12
+ */
13
+ export interface BadgeProps {
14
+ /** Conteúdo a exibir dentro do badge (texto, número, ou ícone). */
15
+ content?: string | number | ReactNode;
16
+ /** Variante de cor do badge. */
17
+ variant?: BadgeVariant;
18
+ /** Tamanho predefinido. */
19
+ size?: BadgeSize;
20
+ /** Estilização extra para o componente. */
21
+ style?: ViewStyle | ViewStyle[];
22
+ }
23
+
24
+ export const Badge = ({
25
+ content,
26
+ variant = 'primary',
27
+ size = 'sm',
28
+ style,
29
+ }: BadgeProps) => {
30
+ const getVariantStyles = () => {
31
+ switch (variant) {
32
+ case 'success':
33
+ return {
34
+ backgroundColor: theme.colors.success,
35
+ color: theme.colors.white,
36
+ borderWidth: 0,
37
+ borderColor: 'transparent',
38
+ };
39
+ case 'error':
40
+ return {
41
+ backgroundColor: theme.colors.error,
42
+ color: theme.colors.white,
43
+ borderWidth: 0,
44
+ borderColor: 'transparent',
45
+ };
46
+ case 'disabled':
47
+ return {
48
+ backgroundColor: '#ebdcd5',
49
+ color: theme.colors.white,
50
+ borderWidth: 0,
51
+ borderColor: 'transparent',
52
+ };
53
+ case 'neutro':
54
+ return {
55
+ backgroundColor: theme.colors.brown[50],
56
+ color: theme.colors.brown[800],
57
+ borderWidth: 1,
58
+ borderColor: theme.colors.brown[100],
59
+ };
60
+ case 'primary':
61
+ default:
62
+ return {
63
+ backgroundColor: theme.colors.primary,
64
+ color: theme.colors.white,
65
+ borderWidth: 0,
66
+ borderColor: 'transparent',
67
+ };
68
+ }
69
+ };
70
+
71
+ const vStyles = getVariantStyles();
72
+
73
+ return (
74
+ <View style={[
75
+ styles.container,
76
+ size === 'xs' ? styles.sizeXs : size === 'sm' ? styles.sizeSm : size === 'md' ? styles.sizeMd : size === 'lg' ? styles.sizeLg : size === 'xl' ? styles.sizeXl : styles.sizeXxl,
77
+ { backgroundColor: vStyles.backgroundColor, borderColor: vStyles.borderColor, borderWidth: vStyles.borderWidth },
78
+ style
79
+ ]}>
80
+ {typeof content === 'string' || typeof content === 'number' ? (
81
+ <Typography style={[
82
+ styles.text,
83
+ size === 'xs' ? styles.textXs : size === 'sm' ? styles.textSm : size === 'md' ? styles.textMd : size === 'lg' ? styles.textLg : size === 'xl' ? styles.textXl : styles.textXxl,
84
+ { color: vStyles.color }
85
+ ]}>
86
+ {content}
87
+ </Typography>
88
+ ) : (
89
+ content
90
+ )}
91
+ </View>
92
+ );
93
+ };
94
+
95
+ const styles = StyleSheet.create({
96
+ container: {
97
+ justifyContent: 'center',
98
+ alignItems: 'center',
99
+ borderRadius: 999,
100
+ },
101
+ sizeXs: {
102
+ width: 18,
103
+ height: 18,
104
+ },
105
+ sizeSm: {
106
+ width: 22,
107
+ height: 22,
108
+ },
109
+ sizeMd: {
110
+ width: 26,
111
+ height: 26,
112
+ },
113
+ sizeLg: {
114
+ width: 30,
115
+ height: 30,
116
+ },
117
+ sizeXl: {
118
+ width: 34,
119
+ height: 34,
120
+ },
121
+ sizeXxl: {
122
+ width: 44,
123
+ height: 44,
124
+ },
125
+ text: {
126
+
127
+ },
128
+ textXs: {
129
+ fontSize: 9,
130
+ },
131
+ textSm: {
132
+ fontSize: 10,
133
+ },
134
+ textMd: {
135
+ fontSize: 12,
136
+ },
137
+ textLg: {
138
+ fontSize: 14,
139
+ },
140
+ textXl: {
141
+ fontSize: 16,
142
+ },
143
+ textXxl: {
144
+ fontSize: 18,
145
+ },
146
+ });
@@ -0,0 +1,98 @@
1
+ // eslint-disable-next-line storybook/no-renderer-packages
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { View } from 'react-native';
4
+ import { Flag } from './Flag';
5
+ import { Typography } from '../../Typography/Typography';
6
+
7
+ const countries = [
8
+ { code: 'FR', name: 'França' },
9
+ { code: 'BE', name: 'Bélgica' },
10
+ { code: 'CH', name: 'Suíça' },
11
+ { code: 'LU', name: 'Luxemburgo' },
12
+ { code: 'MC', name: 'Mónaco' },
13
+ { code: 'CA', name: 'Canadá' },
14
+ { code: 'CI', name: "Côte d'Ivoire" },
15
+ { code: 'SN', name: 'Senegal' },
16
+ { code: 'CM', name: 'Camarões' },
17
+ { code: 'CD', name: 'RD Congo' },
18
+ { code: 'MG', name: 'Madagascar' },
19
+ { code: 'BF', name: 'Burkina Faso' },
20
+ { code: 'NE', name: 'Níger' },
21
+ { code: 'ML', name: 'Mali' },
22
+ { code: 'TD', name: 'Chade' },
23
+ { code: 'GN', name: 'Guiné' },
24
+ { code: 'RW', name: 'Ruanda' },
25
+ { code: 'BI', name: 'Burundi' },
26
+ { code: 'BJ', name: 'Benim' },
27
+ { code: 'TG', name: 'Togo' },
28
+ { code: 'CF', name: 'Rep. Centro-Africana' },
29
+ { code: 'CG', name: 'Congo' },
30
+ { code: 'GA', name: 'Gabão' },
31
+ { code: 'GQ', name: 'Guiné Equatorial' },
32
+ { code: 'DJ', name: 'Djibouti' },
33
+ { code: 'KM', name: 'Comores' },
34
+ { code: 'VU', name: 'Vanuatu' },
35
+ { code: 'SC', name: 'Seychelles' },
36
+ { code: 'HT', name: 'Haiti' },
37
+ ];
38
+
39
+ const meta = {
40
+ title: 'Components/DataDisplay/Flag',
41
+ component: Flag,
42
+ parameters: {
43
+ layout: 'padded',
44
+ docs: {
45
+ description: {
46
+ component: 'A **Flag** renderiza bandeiras SVG otimizadas com formato circular, dado o nome ou código de um país.\n\n### Exemplo de Uso\n```tsx\n<Flag \n countryCode="França"\n size={48}\n/>\n```',
47
+ }
48
+ }
49
+ },
50
+ tags: ['autodocs'],
51
+ argTypes: {
52
+ countryCode: {
53
+ control: 'select',
54
+ options: countries.map(c => c.code),
55
+ },
56
+ size: { control: 'number' },
57
+ },
58
+ decorators: [
59
+ (Story) => (
60
+ <View style={{ padding: 24, backgroundColor: '#f5f0eb', alignItems: 'center' }}>
61
+ <Story />
62
+ </View>
63
+ ),
64
+ ],
65
+ } satisfies Meta<any>;
66
+
67
+ export default meta;
68
+ type Story = StoryObj<any>;
69
+
70
+ export const InteractiveFlag: Story = {
71
+ name: 'Seletor de Bandeiras (Dropdown)',
72
+ args: {
73
+ countryCode: 'FR',
74
+ size: 80,
75
+ },
76
+ };
77
+
78
+ export const Catalog: Story = {
79
+ name: 'Catálogo Completo (29 Países)',
80
+ args: {
81
+ countryCode: 'FR',
82
+ size: 40,
83
+ },
84
+ render: () => {
85
+ return (
86
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 20, justifyContent: 'center', maxWidth: 650 }}>
87
+ {countries.map((c) => (
88
+ <View key={c.code} style={{ alignItems: 'center', width: 90, gap: 6 }}>
89
+ <Flag countryCode={c.code} size={48} />
90
+ <Typography style={{ fontSize: 10, fontWeight: '700', color: '#51332d', textAlign: 'center' }}>
91
+ {c.name}
92
+ </Typography>
93
+ </View>
94
+ ))}
95
+ </View>
96
+ );
97
+ },
98
+ };