@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,148 @@
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 TagVariant = 'neutro' | 'primary' | 'success' | 'error' | 'dark';
8
+ export type TagSize = 'xs' | 'sm' | 'md' | 'lg';
9
+
10
+ /**
11
+ * Etiqueta compacta usada em cima de cartões ou imagens para classificar conteúdos (ex: 'NOVO').
12
+ */
13
+ export interface TagProps {
14
+ /** Texto da etiqueta. */
15
+ label: string;
16
+ /** Variante de cor. */
17
+ variant?: TagVariant;
18
+ /** Tamanho. */
19
+ size?: TagSize;
20
+ icon?: ReactNode;
21
+ /** Estilos adicionais para o container da etiqueta. */
22
+ style?: ViewStyle | ViewStyle[];
23
+ }
24
+
25
+ export const Tag = ({
26
+ label,
27
+ variant = 'neutro',
28
+ size = 'sm',
29
+ icon,
30
+ style,
31
+ }: TagProps) => {
32
+ const getVariantStyles = () => {
33
+ switch (variant) {
34
+ case 'primary':
35
+ return {
36
+ backgroundColor: theme.colors.primaryLight,
37
+ borderColor: theme.colors.primary,
38
+ color: theme.colors.primaryDark,
39
+ borderWidth: 1,
40
+ };
41
+ case 'success':
42
+ return {
43
+ backgroundColor: '#eafaf2',
44
+ borderColor: theme.colors.success,
45
+ color: theme.colors.success,
46
+ borderWidth: 1,
47
+ };
48
+ case 'error':
49
+ return {
50
+ backgroundColor: '#fdf0ee',
51
+ borderColor: theme.colors.error,
52
+ color: theme.colors.error,
53
+ borderWidth: 1,
54
+ };
55
+ case 'dark':
56
+ return {
57
+ backgroundColor: theme.colors.brown[800],
58
+ borderColor: 'transparent',
59
+ color: theme.colors.white,
60
+ borderWidth: 0,
61
+ };
62
+ case 'neutro':
63
+ default:
64
+ return {
65
+ backgroundColor: theme.colors.brown[50],
66
+ borderColor: theme.colors.brown[100],
67
+ color: theme.colors.brown[800],
68
+ borderWidth: 1,
69
+ };
70
+ }
71
+ };
72
+
73
+ const vStyles = getVariantStyles();
74
+
75
+ return (
76
+ <View style={[
77
+ styles.container,
78
+ size === 'xs' ? styles.sizeXs : size === 'sm' ? styles.sizeSm : size === 'md' ? styles.sizeMd : styles.sizeLg,
79
+ { backgroundColor: vStyles.backgroundColor, borderColor: vStyles.borderColor, borderWidth: vStyles.borderWidth },
80
+ style
81
+ ]}>
82
+ {icon && <View style={styles.iconContainer}>{icon}</View>}
83
+ <Typography style={[
84
+ styles.text,
85
+ size === 'xs' ? styles.textXs : size === 'sm' ? styles.textSm : size === 'md' ? styles.textMd : styles.textLg,
86
+ { color: vStyles.color }
87
+ ]}>
88
+ {label}
89
+ </Typography>
90
+ </View>
91
+ );
92
+ };
93
+
94
+ const styles = StyleSheet.create({
95
+ container: {
96
+ flexDirection: 'row',
97
+ alignItems: 'center',
98
+ alignSelf: 'flex-start',
99
+ justifyContent: 'center',
100
+ },
101
+ sizeXs: {
102
+ paddingVertical: 2,
103
+ paddingHorizontal: 8,
104
+ borderRadius: 6,
105
+ },
106
+ sizeSm: {
107
+ paddingVertical: 4,
108
+ paddingHorizontal: 10,
109
+ borderRadius: 8,
110
+ },
111
+ sizeMd: {
112
+ paddingVertical: 5,
113
+ paddingHorizontal: 12,
114
+ borderRadius: 10,
115
+ },
116
+ sizeLg: {
117
+ paddingVertical: 6,
118
+ paddingHorizontal: 14,
119
+ borderRadius: 12,
120
+ },
121
+ text: {
122
+
123
+ textTransform: 'uppercase',
124
+ },
125
+ textXs: {
126
+ fontSize: 9,
127
+ lineHeight: 11,
128
+ letterSpacing: 0.54,
129
+ },
130
+ textSm: {
131
+ fontSize: 10,
132
+ lineHeight: 12,
133
+ letterSpacing: 0.6, // 10 * 0.06em
134
+ },
135
+ textMd: {
136
+ fontSize: 11,
137
+ lineHeight: 13,
138
+ letterSpacing: 0.66, // 11 * 0.06em
139
+ },
140
+ textLg: {
141
+ fontSize: 12,
142
+ lineHeight: 14,
143
+ letterSpacing: 0.72,
144
+ },
145
+ iconContainer: {
146
+ marginRight: 4,
147
+ }
148
+ });
@@ -0,0 +1,74 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { View } from 'react-native';
3
+ import { FeedbackBottomSheet } from './FeedbackBottomSheet';
4
+
5
+ const meta = {
6
+ title: 'Components/Feedback/FeedbackBottomSheet',
7
+ component: FeedbackBottomSheet,
8
+ parameters: {
9
+ layout: 'padded',
10
+ docs: {
11
+ description: {
12
+ component: `
13
+ O **FeedbackBottomSheet** é utilizado no final de um exercício para mostrar ao utilizador se a resposta está correta ou errada.
14
+ Ele renderiza automaticamente o estado visual adequado (cores, sombras, e a mascote Yaya em modo Avatar).
15
+
16
+ ### Casos de Uso
17
+
18
+ **1. Resposta Correta**
19
+ \`\`\`tsx
20
+ <FeedbackBottomSheet
21
+ status="correct"
22
+ title="Excellent !"
23
+ subtitle="+10 XP · resposta certa"
24
+ isVisible={true}
25
+ onContinue={() => console.log('Avançar')}
26
+ />
27
+ \`\`\`
28
+
29
+ **2. Resposta Errada**
30
+ \`\`\`tsx
31
+ <FeedbackBottomSheet
32
+ status="wrong"
33
+ title="Presque !"
34
+ subtitle="Quase lá — vê a resposta certa"
35
+ correctAnswer="Je bois de l'eau"
36
+ isVisible={true}
37
+ onContinue={() => console.log('Avançar')}
38
+ />
39
+ \`\`\`
40
+ `,
41
+ }
42
+ }
43
+ },
44
+ tags: ['autodocs'],
45
+ decorators: [
46
+ (Story) => (
47
+ <View style={{ width: '100%', maxWidth: 400, margin: 'auto', backgroundColor: '#f0f0f0', height: 400, justifyContent: 'flex-end', overflow: 'hidden' }}>
48
+ <Story />
49
+ </View>
50
+ ),
51
+ ],
52
+ } satisfies Meta<any>;
53
+
54
+ export default meta;
55
+ type Story = StoryObj<any>;
56
+
57
+ export const CorrectFeedback: Story = {
58
+ args: {
59
+ status: 'correct',
60
+ title: 'Excellent !',
61
+ subtitle: '+10 XP · resposta certa',
62
+ isVisible: true,
63
+ },
64
+ };
65
+
66
+ export const WrongFeedback: Story = {
67
+ args: {
68
+ status: 'wrong',
69
+ title: 'Presque !',
70
+ subtitle: 'Quase lá — vê a resposta certa',
71
+ correctAnswer: "Je bois de l'eau",
72
+ isVisible: true,
73
+ },
74
+ };
@@ -0,0 +1,200 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { View, StyleSheet, Animated, Platform } from 'react-native';
3
+ import { theme } from '../../../styles/theme';
4
+ import { Yaya } from '../../Mascots/Yaya/Yaya';
5
+ import { MainButton } from '../../Buttons/MainButton/MainButton';
6
+ import { Typography } from '../../Typography/Typography';
7
+
8
+ export interface FeedbackBottomSheetProps {
9
+ /** Define se a resposta foi correta ou errada */
10
+ status: 'correct' | 'wrong';
11
+ /** O estado/variante da mascote (ex: "acertou", "errou", "feliz") */
12
+ mascotVariant?: any;
13
+ /** Título principal (ex: "Correto!" ou "Atenção!") */
14
+ title: string;
15
+ /** Subtítulo ou texto complementar (ex: "+10 XP" ou "Resposta certa: ...") */
16
+ subtitle: string | React.ReactNode;
17
+ /** Campo da resposta certa, exibido na caixa branca quando status="wrong" */
18
+ correctAnswer?: string;
19
+ /** Texto do botão (ex: "CONTINUAR") */
20
+ buttonText?: string;
21
+ /** Callback ao clicar no botão */
22
+ onContinue?: () => void;
23
+ /** Se o sheet está visível ou não (para a animação) */
24
+ isVisible?: boolean;
25
+ }
26
+
27
+ /**
28
+ * Componente FeedbackBottomSheet
29
+ * Painel inferior que surge após o utilizador submeter uma resposta,
30
+ * apresentando a mascote (Avatar) em estado feliz ou triste.
31
+ */
32
+ export const FeedbackBottomSheet: React.FC<FeedbackBottomSheetProps> = ({
33
+ status,
34
+ mascotVariant,
35
+ title,
36
+ subtitle,
37
+ correctAnswer,
38
+ buttonText = 'CONTINUAR',
39
+ onContinue,
40
+ isVisible = true,
41
+ }) => {
42
+ const isCorrect = status === 'correct';
43
+ // Use Yaya as default since the images feature Yaya, and we just added avatarOnly to it.
44
+ const finalMascotVariant = mascotVariant || (isCorrect ? 'acertou' : 'errou');
45
+
46
+ const translateY = useRef(new Animated.Value(1000)).current;
47
+
48
+ useEffect(() => {
49
+ if (isVisible) {
50
+ Animated.timing(translateY, {
51
+ toValue: 0,
52
+ duration: 300,
53
+ useNativeDriver: true,
54
+ // Simple ease-out approximation
55
+ easing: (value) => 1 - Math.pow(1 - value, 3)
56
+ }).start();
57
+ } else {
58
+ Animated.timing(translateY, {
59
+ toValue: 1000,
60
+ duration: 300,
61
+ useNativeDriver: true,
62
+ }).start();
63
+ }
64
+ }, [isVisible, translateY]);
65
+
66
+ const bgSheet = isCorrect ? '#eafaf2' : '#fdf0ee';
67
+ const borderColor = isCorrect ? theme.colors.success : theme.colors.error;
68
+
69
+ return (
70
+ <Animated.View
71
+ style={[
72
+ styles.sheet,
73
+ { backgroundColor: bgSheet, borderTopColor: borderColor },
74
+ { transform: [{ translateY }] }
75
+ ]}
76
+ >
77
+ <View style={styles.header}>
78
+ <View style={styles.avatarContainer}>
79
+ {/* O componente sabe internamente que deve usar a mascote em modo Avatar */}
80
+ <Yaya variant={finalMascotVariant} avatarOnly size={64} />
81
+ </View>
82
+ <View style={styles.textContainer}>
83
+ <Typography style={[styles.title, { color: borderColor }]}>{title}</Typography>
84
+ <Typography style={[styles.subtitle, { color: borderColor }]}>{subtitle}</Typography>
85
+ </View>
86
+ </View>
87
+
88
+ {!isCorrect && correctAnswer && (
89
+ <View style={styles.correctAnswerBox}>
90
+ <Typography style={styles.correctAnswerLabel}>RESPOSTA CERTA</Typography>
91
+ <Typography style={styles.correctAnswerText}>{correctAnswer}</Typography>
92
+ </View>
93
+ )}
94
+
95
+ <MainButton
96
+ onPress={onContinue}
97
+ fullWidth
98
+ size="lg"
99
+ bgColor={borderColor}
100
+ textColor={theme.colors.white}
101
+ shadowColor={isCorrect ? '#0a633c' : '#a83c2d'}
102
+ >
103
+ {buttonText}
104
+ </MainButton>
105
+ </Animated.View>
106
+ );
107
+ };
108
+
109
+ const styles = StyleSheet.create({
110
+ sheet: {
111
+ paddingTop: theme.spacing.space4,
112
+ paddingBottom: theme.spacing.space6,
113
+ paddingHorizontal: 18,
114
+ borderTopLeftRadius: theme.radius.xxl,
115
+ borderTopRightRadius: theme.radius.xxl,
116
+ width: '100%',
117
+ minHeight: 120,
118
+ },
119
+ header: {
120
+ flexDirection: 'row',
121
+ alignItems: 'center',
122
+ marginBottom: theme.spacing.space4,
123
+ },
124
+ avatarContainer: {
125
+ marginRight: theme.spacing.space3,
126
+ },
127
+ textContainer: {
128
+ flex: 1,
129
+ },
130
+ title: {
131
+ ...theme.typography.h1,
132
+ marginBottom: 2,
133
+ },
134
+ subtitle: {
135
+
136
+ fontSize: 15,
137
+
138
+ marginTop: 2,
139
+ },
140
+ button: {
141
+ width: '100%',
142
+ paddingVertical: 14,
143
+ borderRadius: theme.radius.lg,
144
+ alignItems: 'center',
145
+ justifyContent: 'center',
146
+ },
147
+ correctAnswerBox: {
148
+ backgroundColor: '#fff',
149
+ borderRadius: theme.radius.md,
150
+ padding: theme.spacing.space4,
151
+ marginBottom: theme.spacing.space4,
152
+ borderWidth: 1.5,
153
+ borderColor: 'rgba(207, 91, 72, 0.15)', // light red tint border
154
+ },
155
+ correctAnswerLabel: {
156
+
157
+ fontSize: 11,
158
+
159
+ color: '#cf5b48',
160
+ letterSpacing: 0.8,
161
+ textTransform: 'uppercase',
162
+ marginBottom: 4,
163
+ },
164
+ correctAnswerText: {
165
+ ...theme.typography.h1,
166
+ color: theme.colors.brown[700],
167
+ },
168
+ buttonShadowCorrect: {
169
+ ...Platform.select({
170
+ web: {
171
+ boxShadow: `0px 4px 0px #0a633c`, // solid darker green shadow
172
+ },
173
+ default: {
174
+ shadowColor: '#0a633c',
175
+ shadowOffset: { width: 0, height: 4 },
176
+ shadowOpacity: 1,
177
+ shadowRadius: 0,
178
+ elevation: 2,
179
+ }
180
+ })
181
+ },
182
+ buttonShadowWrong: {
183
+ ...Platform.select({
184
+ web: {
185
+ boxShadow: `0px 4px 0px rgba(181, 74, 58, 1)`, // solid dark red shadow
186
+ },
187
+ default: {
188
+ shadowColor: '#a83c2d', // darker red
189
+ shadowOffset: { width: 0, height: 4 },
190
+ shadowOpacity: 1,
191
+ shadowRadius: 0,
192
+ elevation: 2,
193
+ }
194
+ })
195
+ },
196
+ buttonText: {
197
+ ...theme.typography.btnLg,
198
+ color: theme.colors.white,
199
+ }
200
+ });
@@ -0,0 +1,60 @@
1
+ // eslint-disable-next-line storybook/no-renderer-packages
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { useState } from 'react';
4
+ import { View } from 'react-native';
5
+ import { Checkbox } from './Checkbox';
6
+ import { theme } from '../../../styles/theme';
7
+
8
+ const meta = {
9
+ title: 'Components/Forms/Checkbox',
10
+ component: Checkbox,
11
+ parameters: {
12
+ layout: 'padded',
13
+ docs: {
14
+ description: {
15
+ component: 'Um **Checkbox** permite que o utilizador selecione uma ou mais opções de um conjunto. \n\n### Diferença para o Radio\nUsa o `Checkbox` quando queres permitir seleções independentes. Usa o `Radio` para escolhas mutuamente exclusivas.\n\n### Exemplo de Uso\n```tsx\nconst [acceptedTerms, setAcceptedTerms] = useState(false);\n\nreturn (\n <Checkbox \n label="Aceito os termos e condições"\n checked={acceptedTerms}\n onChange={setAcceptedTerms}\n />\n)\n```',
16
+ }
17
+ }
18
+ },
19
+ tags: ['autodocs'],
20
+ decorators: [
21
+ (Story) => (
22
+ <View style={{ padding: 40, backgroundColor: theme.colors.cream }}>
23
+ <Story />
24
+ </View>
25
+ ),
26
+ ],
27
+ } satisfies Meta<any>;
28
+
29
+ export default meta;
30
+ type Story = StoryObj<any>;
31
+
32
+ const InteractiveCheckbox = ({ initialChecked = false, label, disabled = false }: any) => {
33
+ const [checked, setChecked] = useState(initialChecked);
34
+ return (
35
+ <Checkbox
36
+ checked={checked}
37
+ onChange={setChecked}
38
+ label={label}
39
+ disabled={disabled}
40
+ />
41
+ );
42
+ };
43
+
44
+ export const Default: Story = {
45
+ render: () => (
46
+ <View style={{ gap: 12 }}>
47
+ <InteractiveCheckbox label="Viajar para França" initialChecked={true} />
48
+ <InteractiveCheckbox label="Trabalho e carreira" initialChecked={false} />
49
+ </View>
50
+ ),
51
+ };
52
+
53
+ export const Disabled: Story = {
54
+ render: () => (
55
+ <View style={{ gap: 12 }}>
56
+ <InteractiveCheckbox label="Marcado inativo" initialChecked={true} disabled={true} />
57
+ <InteractiveCheckbox label="Desmarcado inativo" initialChecked={false} disabled={true} />
58
+ </View>
59
+ ),
60
+ };
@@ -0,0 +1,127 @@
1
+ import { View, StyleSheet, Pressable } from 'react-native';
2
+ import type { ViewStyle } from 'react-native';
3
+ import { Check } from 'lucide-react-native';
4
+ import { theme } from '../../../styles/theme';
5
+ import { useResponsive } from '../../../hooks/useResponsive';
6
+ import { Typography } from '../../Typography/Typography';
7
+
8
+ /**
9
+ * Componente Checkbox para seleção múltipla.
10
+ * A área clicável inclui a caixa e o texto (label).
11
+ */
12
+ export interface CheckboxProps {
13
+ /** Texto a exibir ao lado do checkbox. */
14
+ label?: string;
15
+ /** Estado de marcação do checkbox. */
16
+ checked: boolean;
17
+ /** Callback disparado quando o estado deve mudar. */
18
+ onChange: (checked: boolean) => void;
19
+ /** Desativa o checkbox, aplicando opacidade. */
20
+ disabled?: boolean;
21
+ /** Estilos customizados para o container. */
22
+ style?: ViewStyle | ViewStyle[];
23
+ }
24
+
25
+ export const Checkbox = ({
26
+ label,
27
+ checked,
28
+ onChange,
29
+ disabled = false,
30
+ style,
31
+ }: CheckboxProps) => {
32
+ const { scale, scaleText } = useResponsive();
33
+
34
+ const handlePress = () => {
35
+ if (!disabled) {
36
+ onChange(!checked);
37
+ }
38
+ };
39
+
40
+ // Cores dinâmicas com base no estado
41
+ let backgroundColor = theme.colors.white;
42
+ let borderColor = theme.colors.brown[100]; // #efe2d6
43
+ let checkColor = theme.colors.white;
44
+
45
+ if (disabled) {
46
+ if (checked) {
47
+ backgroundColor = theme.colors.brown[100]; // #efe2d6
48
+ borderColor = theme.colors.brown[100];
49
+ checkColor = theme.colors.brown[400]; // #b9a392
50
+ } else {
51
+ backgroundColor = 'transparent';
52
+ borderColor = theme.colors.brown[100];
53
+ }
54
+ } else {
55
+ if (checked) {
56
+ backgroundColor = theme.colors.primary; // #f2974b
57
+ borderColor = theme.colors.primary; // esconder a borda ao pintar da mesma cor
58
+ } else {
59
+ backgroundColor = theme.colors.white;
60
+ borderColor = theme.colors.brown[100];
61
+ }
62
+ }
63
+
64
+ return (
65
+ <Pressable
66
+ onPress={handlePress}
67
+ disabled={disabled}
68
+ style={() => [
69
+ styles.container,
70
+ {
71
+ paddingVertical: scale(4),
72
+ gap: scale(12),
73
+ opacity: disabled && !checked ? 0.5 : 1, // Opacity .5 para unchecked disabled
74
+ },
75
+ style
76
+ ]}
77
+ accessibilityRole="checkbox"
78
+ accessibilityState={{ checked, disabled }}
79
+ >
80
+ <View
81
+ style={[
82
+ styles.box,
83
+ {
84
+ width: scale(22),
85
+ height: scale(22),
86
+ borderRadius: scale(7),
87
+ borderWidth: scale(2),
88
+ backgroundColor,
89
+ borderColor,
90
+ }
91
+ ]}
92
+ >
93
+ {checked && (
94
+ <Check stroke={checkColor} strokeWidth={3} size={scale(13)} />
95
+ )}
96
+ </View>
97
+
98
+ {label && (
99
+ <Typography
100
+ style={[
101
+ styles.label,
102
+ {
103
+ fontSize: scaleText(15),
104
+ color: theme.colors.brown[800], // #51332d
105
+ }
106
+ ]}
107
+ >
108
+ {label}
109
+ </Typography>
110
+ )}
111
+ </Pressable>
112
+ );
113
+ };
114
+
115
+ const styles = StyleSheet.create({
116
+ container: {
117
+ flexDirection: 'row',
118
+ alignItems: 'center',
119
+ },
120
+ box: {
121
+ justifyContent: 'center',
122
+ alignItems: 'center',
123
+ },
124
+ label: {
125
+
126
+ },
127
+ });
@@ -0,0 +1 @@
1
+ export * from './Checkbox';
@@ -0,0 +1,62 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { View } from 'react-native';
4
+ import { DaySelector } from './DaySelector';
5
+ import type { DayOption } from './DaySelector';
6
+ import { Typography } from '../../Typography/Typography';
7
+
8
+ const meta = {
9
+ title: 'Components/Forms/DaySelector',
10
+ component: DaySelector,
11
+ parameters: {
12
+ layout: 'padded',
13
+ docs: {
14
+ description: {
15
+ component: `O **DaySelector** é utilizado na marcação de aula. Consiste numa linha horizontal de dias com scroll horizontal se necessário (mobile). O dia selecionado fica destacado com a cor primária.
16
+
17
+ ### Exemplo de Uso
18
+ \`\`\`tsx
19
+ <DaySelector
20
+ days={[
21
+ { id: "1", dateObj: new Date(), label: "SEG", number: "23", status: "available" }
22
+ ]}
23
+ selectedId="1"
24
+ onSelectDay={(id, date) => console.log(id)}
25
+ />
26
+ \`\`\``,
27
+ }
28
+ }
29
+ },
30
+ tags: ['autodocs'],
31
+ } satisfies Meta<any>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<any>;
35
+
36
+ const mockDays: DayOption[] = [
37
+ { id: '1', dateObj: new Date(2024, 2, 23), label: 'SEG', number: '23', status: 'available' },
38
+ { id: '2', dateObj: new Date(2024, 2, 24), label: 'TER', number: '24', status: 'available' },
39
+ { id: '3', dateObj: new Date(2024, 2, 25), label: 'QUA', number: '25', status: 'unavailable' },
40
+ { id: '4', dateObj: new Date(2024, 2, 26), label: 'QUI', number: '26', status: 'available' },
41
+ ];
42
+
43
+ const DaySelectorDemo = () => {
44
+ const [selected, setSelected] = useState<string>('2');
45
+
46
+ return (
47
+ <View style={{ width: '100%', maxWidth: 400, backgroundColor: '#fdf6f0', padding: 24, borderRadius: 16 }}>
48
+ <Typography style={{ fontFamily: 'Nunito-Bold', marginBottom: 12, color: '#9a8478', textTransform: 'uppercase', letterSpacing: 1 }}>
49
+ Escolhe o Dia
50
+ </Typography>
51
+ <DaySelector
52
+ days={mockDays}
53
+ selectedId={selected}
54
+ onSelectDay={(id) => setSelected(id)}
55
+ />
56
+ </View>
57
+ );
58
+ };
59
+
60
+ export const Default: Story = {
61
+ render: () => <DaySelectorDemo />,
62
+ };