@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,258 @@
1
+ import React, { useState } from 'react';
2
+ import { Pressable, ActivityIndicator, StyleSheet, Platform } from 'react-native';
3
+ import type { ViewStyle, TextStyle } from 'react-native';
4
+ import { theme } from '../../../styles/theme';
5
+ import { useResponsive } from '../../../hooks/useResponsive';
6
+ import { Typography } from '../../Typography/Typography';
7
+
8
+ /**
9
+ * Botão principal do sistema, usado para ações primárias, secundárias e destrutivas.
10
+ */
11
+ export interface MainButtonProps {
12
+ /** Texto a exibir no botão. */
13
+ children: React.ReactNode;
14
+ /** Variante visual do botão. */
15
+ variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
16
+ /** Tamanho do botão. */
17
+ size?: 'sm' | 'md' | 'lg';
18
+ /** Se true, o botão ocupa a largura total disponível. */
19
+ fullWidth?: boolean;
20
+ /** Se true, desativa a interação e aplica estilos de inativo. */
21
+ disabled?: boolean;
22
+ /** Se true, exibe um spinner de carregamento em vez do texto. */
23
+ loading?: boolean;
24
+ /** Callback para o evento de clique. */
25
+ onPress?: () => void;
26
+ /** Cor de fundo customizada (sobrepõe a cor da variante) */
27
+ bgColor?: string;
28
+ /** Cor do texto customizada (sobrepõe a cor da variante) */
29
+ textColor?: string;
30
+ /** Cor da sombra customizada (sobrepõe a cor da variante) */
31
+ shadowColor?: string;
32
+ /** Cor de fundo no hover (opcional) */
33
+ hoverColor?: string;
34
+ style?: ViewStyle | ViewStyle[];
35
+ }
36
+
37
+ export const MainButton = React.forwardRef<React.ElementRef<typeof Pressable>, MainButtonProps>(
38
+ (
39
+ {
40
+ style,
41
+ variant = 'primary',
42
+ size = 'md',
43
+ fullWidth = false,
44
+ loading = false,
45
+ children,
46
+ disabled,
47
+ onPress,
48
+ bgColor,
49
+ textColor,
50
+ shadowColor,
51
+ hoverColor,
52
+ ...props
53
+ },
54
+ ref
55
+ ) => {
56
+ const [isHovered, setIsHovered] = useState(false);
57
+ const { scale, scaleText } = useResponsive();
58
+
59
+ const getBgColor = () => {
60
+ if (disabled) {
61
+ if (variant === 'primary' || variant === 'secondary') return '#efe2d6';
62
+ return 'transparent';
63
+ }
64
+ if (bgColor) return isHovered ? (hoverColor || bgColor) : bgColor;
65
+ if (variant === 'primary') return isHovered ? '#e8883e' : theme.colors.primary;
66
+ if (variant === 'secondary') return theme.colors.white;
67
+ return 'transparent'; // ghost
68
+ };
69
+
70
+ const getTextColor = () => {
71
+ if (disabled) {
72
+ if (variant === 'primary' || variant === 'secondary') return '#b9a392';
73
+ return theme.colors.brown[500];
74
+ }
75
+ if (textColor) return textColor;
76
+ if (variant === 'primary') return '#ffffff';
77
+ if (variant === 'secondary') return isHovered ? '#cf7a30' : theme.colors.brown[800];
78
+ if (variant === 'ghost') return isHovered ? '#51332d' : theme.colors.brown[500];
79
+ return '#000000';
80
+ };
81
+
82
+ const getBorderStyles = (): ViewStyle => {
83
+ if (variant === 'secondary') {
84
+ const borderColor = disabled ? '#efe2d6' : isHovered ? '#f2974b' : theme.colors.brown[100];
85
+ return {
86
+ borderWidth: 2,
87
+ borderColor,
88
+ };
89
+ }
90
+ return { borderWidth: 0 };
91
+ };
92
+
93
+ const getShadowStyles = (): ViewStyle => {
94
+ if (disabled || variant === 'ghost') {
95
+ return {};
96
+ }
97
+
98
+ if (Platform.OS === 'web') {
99
+ // Web uses standard CSS box-shadow for better matching with design
100
+ const resolvedShadowColor = shadowColor || (variant === 'primary' ? theme.colors.primaryDark : theme.colors.brown[100]);
101
+ return {
102
+ boxShadow: `0px ${scale(4)}px 0px ${resolvedShadowColor}`
103
+ };
104
+ } else {
105
+ // Native shadow / elevation
106
+ if (shadowColor) {
107
+ return {
108
+ shadowColor,
109
+ shadowOffset: { width: 0, height: scale(4) },
110
+ shadowOpacity: 1,
111
+ shadowRadius: 0,
112
+ elevation: 4,
113
+ };
114
+ }
115
+ return variant === 'primary' ? theme.shadows.btnPrimary : theme.shadows.btnSecondary;
116
+ }
117
+ };
118
+
119
+ // Calculate dynamic height, padding and radius based on size and responsiveness
120
+ const sizeStyles = {
121
+ sm: { height: scale(36), paddingHorizontal: scale(16), borderRadius: scale(11) },
122
+ md: { height: scale(50), paddingHorizontal: scale(22), borderRadius: scale(16) },
123
+ lg: { height: scale(58), paddingHorizontal: scale(32), borderRadius: scale(18) },
124
+ }[size];
125
+
126
+ const buttonStyles: ViewStyle[] = [
127
+ styles.base,
128
+ sizeStyles,
129
+ { backgroundColor: getBgColor() },
130
+ getBorderStyles(),
131
+ getShadowStyles(),
132
+ fullWidth ? { width: '100%' } : { alignSelf: 'flex-start' },
133
+ disabled && variant === 'ghost' ? { opacity: 0.4 } : {},
134
+ style as ViewStyle,
135
+ ];
136
+
137
+ const textStyles: TextStyle[] = [
138
+ styles.textBase,
139
+ {
140
+ fontSize: {
141
+ sm: scaleText(12),
142
+ md: scaleText(16),
143
+ lg: scaleText(18),
144
+ }[size],
145
+ letterSpacing: {
146
+ sm: scale(0.48),
147
+ md: scale(0.64),
148
+ lg: scale(0.72),
149
+ }[size],
150
+ },
151
+ { color: getTextColor() },
152
+ loading ? styles.textHidden : {},
153
+ ];
154
+
155
+ // Handle React Native Web Hover (typing workaround)
156
+ const hoverProps = Platform.OS === 'web' ? {
157
+ onHoverIn: () => !disabled && setIsHovered(true),
158
+ onHoverOut: () => !disabled && setIsHovered(false),
159
+ } : {};
160
+
161
+ return (
162
+ <Pressable
163
+ ref={ref}
164
+ disabled={disabled || loading}
165
+ onPress={onPress}
166
+ style={({ pressed }) => {
167
+ const stateStyles: ViewStyle[] = [...buttonStyles];
168
+
169
+ if (disabled) {
170
+ // @ts-expect-error - web only style
171
+ if (Platform.OS === 'web') stateStyles.push({ cursor: 'not-allowed' });
172
+ return stateStyles;
173
+ }
174
+
175
+ if (pressed && !loading) {
176
+ if (variant === 'ghost') {
177
+ stateStyles.push({ opacity: 0.7 });
178
+ } else {
179
+ // 3D tactile pressing effect (translate Y)
180
+ stateStyles.push({
181
+ transform: [{ translateY: scale(4) }],
182
+ });
183
+ // Remove the shadow when pressed to mimic the physical push
184
+ if (Platform.OS === 'web') {
185
+ stateStyles.push({ boxShadow: 'none' });
186
+ } else {
187
+ stateStyles.push({
188
+ shadowOpacity: 0,
189
+ elevation: 0,
190
+ });
191
+ }
192
+ }
193
+ } else if (isHovered && variant === 'primary' && !loading) {
194
+ stateStyles.push({
195
+ transform: [{ translateY: scale(-1) }],
196
+ });
197
+ }
198
+
199
+ return stateStyles;
200
+ }}
201
+ {...hoverProps}
202
+ {...props}
203
+ >
204
+ {loading && (
205
+ <ActivityIndicator
206
+ size="small"
207
+ color={getTextColor()}
208
+ style={styles.spinner}
209
+ />
210
+ )}
211
+
212
+ <Typography style={textStyles}>
213
+ {children}
214
+ </Typography>
215
+ </Pressable>
216
+ );
217
+ }
218
+ );
219
+
220
+ MainButton.displayName = 'MainButton';
221
+
222
+ const styles = StyleSheet.create({
223
+ base: {
224
+ alignItems: 'center',
225
+ justifyContent: 'center',
226
+ flexDirection: 'row',
227
+ },
228
+
229
+ spinner: {
230
+ position: 'absolute',
231
+ },
232
+
233
+ // Text Sizes & Fonts
234
+ textBase: {
235
+
236
+ textAlign: 'center',
237
+ textTransform: 'uppercase',
238
+ },
239
+ text_sm: {
240
+ fontSize: 12,
241
+
242
+ letterSpacing: 12 * 0.04, // 0.48
243
+ },
244
+ text_md: {
245
+ fontSize: 16,
246
+
247
+ letterSpacing: 16 * 0.04, // 0.64
248
+ },
249
+ text_lg: {
250
+ fontSize: 18,
251
+
252
+ letterSpacing: 18 * 0.04, // 0.72
253
+ },
254
+
255
+ textHidden: {
256
+ color: 'transparent',
257
+ }
258
+ });
@@ -0,0 +1,84 @@
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 { WordChip } from './WordChip';
5
+ import { theme } from '../../../styles/theme';
6
+
7
+ const meta = {
8
+ title: 'Components/Buttons/WordChip',
9
+ component: WordChip,
10
+ parameters: {
11
+ layout: 'padded',
12
+ docs: {
13
+ description: {
14
+ component: 'O **WordChip** é usado maioritariamente nos exercícios interativos (ex: organizar frases) ou como tags filtráveis.\n\n### Exemplo de Uso\n```tsx\n<WordChip \n word="gato"\n isSelected={false}\n onPress={() => selecionar("gato")}\n/>\n```',
15
+ }
16
+ }
17
+ },
18
+ tags: ['autodocs'],
19
+ argTypes: {
20
+ word: { control: 'text' },
21
+ state: {
22
+ control: 'radio',
23
+ options: ['default', 'selected', 'correct', 'wrong', 'used']
24
+ },
25
+ disabled: { control: 'boolean' },
26
+ },
27
+ decorators: [
28
+ (Story) => (
29
+ <View style={{ padding: 24, backgroundColor: theme.colors.cream }}>
30
+ <Story />
31
+ </View>
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
+ word: 'de',
42
+ state: 'default',
43
+ },
44
+ };
45
+
46
+ export const Selected: Story = {
47
+ args: {
48
+ word: "l'eau",
49
+ state: 'selected',
50
+ },
51
+ };
52
+
53
+ export const Correct: Story = {
54
+ args: {
55
+ word: 'bois',
56
+ state: 'correct',
57
+ },
58
+ };
59
+
60
+ export const Wrong: Story = {
61
+ args: {
62
+ word: 'mange',
63
+ state: 'wrong',
64
+ },
65
+ };
66
+
67
+ export const Used: Story = {
68
+ args: {
69
+ word: 'Je',
70
+ state: 'used',
71
+ },
72
+ };
73
+
74
+ export const AllStates: Story = {
75
+ render: () => (
76
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 12 }}>
77
+ <WordChip word="de" state="default" />
78
+ <WordChip word="l'eau" state="selected" />
79
+ <WordChip word="bois" state="correct" />
80
+ <WordChip word="mange" state="wrong" />
81
+ <WordChip word="Je" state="used" />
82
+ </View>
83
+ ),
84
+ };
@@ -0,0 +1,157 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { View, StyleSheet, Pressable, 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 type WordChipState = 'default' | 'selected' | 'correct' | 'wrong' | 'used';
9
+
10
+ /**
11
+ * Componente WordChip usado em exercícios de ordenação de palavras ou tags interativas.
12
+ */
13
+ export interface WordChipProps {
14
+ /** A palavra ou texto a apresentar. */
15
+ word: string;
16
+ /** Estado atual do chip. */
17
+ state?: WordChipState;
18
+ /** Função disparada ao clicar. */
19
+ onPress?: () => void;
20
+ /** Estilos adicionais para o container. */
21
+ style?: ViewStyle | ViewStyle[];
22
+ /** Impede cliques. */
23
+ disabled?: boolean;
24
+ }
25
+
26
+ export const WordChip = ({
27
+ word,
28
+ state = 'default',
29
+ onPress,
30
+ style,
31
+ disabled = false,
32
+ }: WordChipProps) => {
33
+ const { scale, scaleText } = useResponsive();
34
+ const [isPressed, setIsPressed] = useState(false);
35
+ const scaleAnim = useRef(new Animated.Value(state === 'selected' ? 0.96 : 1)).current;
36
+
37
+ // Animação ao entrar na resposta (bounce) quando o estado passa a 'selected'
38
+ useEffect(() => {
39
+ if (state === 'selected') {
40
+ scaleAnim.setValue(0.96);
41
+ Animated.spring(scaleAnim, {
42
+ toValue: 1,
43
+ friction: 4,
44
+ tension: 100,
45
+ useNativeDriver: true, // we can use true here because it's just a simple spring scale
46
+ }).start();
47
+ } else {
48
+ scaleAnim.setValue(1);
49
+ }
50
+ }, [state, scaleAnim]);
51
+
52
+ const getStateStyles = () => {
53
+ switch (state) {
54
+ case 'selected':
55
+ return {
56
+ bg: theme.colors.primaryLight,
57
+ border: theme.colors.primary,
58
+ shadow: 'rgba(242, 151, 75, 0.25)',
59
+ text: theme.colors.brown[800],
60
+ };
61
+ case 'correct':
62
+ return {
63
+ bg: '#eafaf2',
64
+ border: theme.colors.success,
65
+ shadow: 'rgba(22, 145, 90, 0.2)',
66
+ text: theme.colors.success,
67
+ };
68
+ case 'wrong':
69
+ return {
70
+ bg: '#fdf0ee',
71
+ border: theme.colors.error,
72
+ shadow: 'rgba(207, 91, 72, 0.2)',
73
+ text: theme.colors.error,
74
+ };
75
+ case 'used':
76
+ return {
77
+ bg: theme.colors.white,
78
+ border: theme.colors.brown[100],
79
+ shadow: theme.colors.brown[100],
80
+ text: theme.colors.brown[300],
81
+ };
82
+ case 'default':
83
+ default:
84
+ return {
85
+ bg: theme.colors.white,
86
+ border: theme.colors.brown[100],
87
+ shadow: theme.colors.brown[100],
88
+ text: theme.colors.brown[800],
89
+ };
90
+ }
91
+ };
92
+
93
+ const currentStyles = getStateStyles();
94
+ const isDisabled = disabled || state === 'used';
95
+
96
+ // Press state visual adjustments
97
+ const showPressed = isPressed && !isDisabled;
98
+
99
+ return (
100
+ <Animated.View style={[{ transform: [{ scale: scaleAnim }] }, style]}>
101
+ <Pressable
102
+ onPressIn={() => setIsPressed(true)}
103
+ onPressOut={() => setIsPressed(false)}
104
+ onPress={onPress}
105
+ disabled={isDisabled}
106
+ style={{ alignSelf: 'flex-start' }}
107
+ >
108
+ <View style={[
109
+ styles.shadowContainer,
110
+ {
111
+ backgroundColor: currentStyles.shadow,
112
+ borderRadius: scale(12),
113
+ paddingBottom: showPressed ? 0 : scale(3),
114
+ marginTop: showPressed ? scale(3) : 0,
115
+ }
116
+ ]}>
117
+ <View style={[
118
+ styles.innerContainer,
119
+ {
120
+ backgroundColor: currentStyles.bg,
121
+ borderColor: currentStyles.border,
122
+ borderRadius: scale(12),
123
+ paddingVertical: scale(9),
124
+ paddingHorizontal: scale(14),
125
+ minHeight: scale(44),
126
+ }
127
+ ]}>
128
+ <Typography style={[
129
+ styles.text,
130
+ {
131
+ color: currentStyles.text,
132
+ fontSize: scaleText(15),
133
+ }
134
+ ]}>
135
+ {word}
136
+ </Typography>
137
+ </View>
138
+ </View>
139
+ </Pressable>
140
+ </Animated.View>
141
+ );
142
+ };
143
+
144
+ const styles = StyleSheet.create({
145
+ shadowContainer: {
146
+ alignSelf: 'flex-start',
147
+ },
148
+ innerContainer: {
149
+ borderWidth: 2,
150
+ alignItems: 'center',
151
+ justifyContent: 'center',
152
+ },
153
+ text: {
154
+
155
+ textAlign: 'center',
156
+ },
157
+ });
@@ -0,0 +1 @@
1
+ export * from './WordChip';
@@ -0,0 +1,181 @@
1
+ // eslint-disable-next-line storybook/no-renderer-packages
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { View, Pressable } from 'react-native';
4
+ import { AnswerOptionCard } from './AnswerOptionCard';
5
+ import type { AnswerOptionStatus } from './AnswerOptionCard';
6
+ import { theme } from '../../../styles/theme';
7
+ import { useState } from 'react';
8
+ import { Typography } from '../../Typography/Typography';
9
+
10
+ const meta = {
11
+ title: 'Components/Cards/AnswerOptionCard',
12
+ component: AnswerOptionCard,
13
+ parameters: {
14
+ layout: 'padded',
15
+ docs: {
16
+ description: {
17
+ component: 'O **AnswerOptionCard** é utilizado nos ecrãs de Lição/Quiz para exibir as múltiplas escolhas de um exercício.\n\n### Exemplo de Uso\n```tsx\n<AnswerOptionCard \n status="default"\n label="Boa noite"\n prefixNode={<CircleLetter letter="A" />}\n onPress={() => selecionar("A")}\n/>\n```',
18
+ }
19
+ }
20
+ },
21
+ tags: ['autodocs'],
22
+ argTypes: {
23
+ status: { control: 'radio', options: ['default', 'selected', 'correct', 'wrong', 'disabled'] },
24
+ label: { control: 'text' },
25
+ },
26
+ decorators: [
27
+ (Story) => (
28
+ <View style={{ padding: 24, maxWidth: 350 }}>
29
+ <Story />
30
+ </View>
31
+ ),
32
+ ],
33
+ } satisfies Meta<any>;
34
+
35
+ export default meta;
36
+ type Story = StoryObj<any>;
37
+
38
+ const CircleLetter = ({ letter, selected = false, correct = false }: { letter: string; selected?: boolean; correct?: boolean }) => {
39
+ let bgColor = theme.colors.brown[100];
40
+ let textColor = theme.colors.brown[500];
41
+
42
+ if (selected) {
43
+ bgColor = theme.colors.primary;
44
+ textColor = theme.colors.white;
45
+ } else if (correct) {
46
+ bgColor = theme.colors.success;
47
+ textColor = theme.colors.white;
48
+ }
49
+
50
+ return (
51
+ <View style={{
52
+ width: 28,
53
+ height: 28,
54
+ borderRadius: 14,
55
+ backgroundColor: bgColor,
56
+ alignItems: 'center',
57
+ justifyContent: 'center',
58
+ }}>
59
+ <Typography style={{
60
+
61
+ fontSize: 14,
62
+ color: textColor,
63
+ }}>
64
+ {letter}
65
+ </Typography>
66
+ </View>
67
+ );
68
+ };
69
+
70
+ export const Default: Story = {
71
+ args: {
72
+ status: 'default',
73
+ label: 'Boa noite',
74
+ prefixNode: <CircleLetter letter="A" />,
75
+ },
76
+ };
77
+
78
+ export const InteractiveDemo: Story = {
79
+ args: {
80
+ label: '',
81
+ },
82
+ render: () => {
83
+ const [selected, setSelected] = useState<string | null>(null);
84
+ const [statusMap, setStatusMap] = useState<Record<string, AnswerOptionStatus>>({
85
+ A: 'default',
86
+ B: 'default',
87
+ C: 'default',
88
+ });
89
+
90
+ const handlePress = (letter: string) => {
91
+ setSelected(letter);
92
+ };
93
+
94
+ const checkAnswer = () => {
95
+ if (!selected) return;
96
+ setStatusMap({
97
+ A: selected === 'A' ? 'wrong' : 'default',
98
+ B: selected === 'B' ? 'correct' : 'default',
99
+ C: selected === 'C' ? 'wrong' : 'default',
100
+ });
101
+ };
102
+
103
+ const reset = () => {
104
+ setSelected(null);
105
+ setStatusMap({ A: 'default', B: 'default', C: 'default' });
106
+ };
107
+
108
+ const getStatus = (letter: string) => {
109
+ if (statusMap[letter] !== 'default') {
110
+ return statusMap[letter];
111
+ }
112
+ return selected === letter ? 'selected' : 'default';
113
+ };
114
+
115
+ return (
116
+ <View style={{ gap: 16 }}>
117
+ <AnswerOptionCard
118
+ status={getStatus('A')}
119
+ label="Boa noite"
120
+ prefixNode={<CircleLetter letter="A" selected={selected === 'A'} correct={statusMap.A === 'correct'} />}
121
+ onPress={() => handlePress('A')}
122
+ />
123
+ <AnswerOptionCard
124
+ status={getStatus('B')}
125
+ label="Bom dia / Olá"
126
+ prefixNode={<CircleLetter letter="B" selected={selected === 'B'} correct={statusMap.B === 'correct'} />}
127
+ onPress={() => handlePress('B')}
128
+ />
129
+ <AnswerOptionCard
130
+ status={getStatus('C')}
131
+ label="Obrigado"
132
+ prefixNode={<CircleLetter letter="C" selected={selected === 'C'} correct={statusMap.C === 'correct'} />}
133
+ onPress={() => handlePress('C')}
134
+ />
135
+
136
+ <View style={{ marginTop: 16, flexDirection: 'row', gap: 12 }}>
137
+ <Pressable
138
+ style={{
139
+ flex: 2,
140
+ backgroundColor: theme.colors.primary,
141
+ padding: 12,
142
+ borderRadius: 12,
143
+ alignItems: 'center',
144
+ }}
145
+ onPress={checkAnswer}
146
+ >
147
+ <Typography style={{ color: 'white', fontWeight: 'bold' }}>Verificar Resposta</Typography>
148
+ </Pressable>
149
+
150
+ <Pressable
151
+ style={{
152
+ flex: 1,
153
+ backgroundColor: theme.colors.brown[100],
154
+ padding: 12,
155
+ borderRadius: 12,
156
+ alignItems: 'center',
157
+ }}
158
+ onPress={reset}
159
+ >
160
+ <Typography style={{ color: theme.colors.brown[800], fontWeight: 'bold' }}>Reset</Typography>
161
+ </Pressable>
162
+ </View>
163
+ </View>
164
+ );
165
+ },
166
+ };
167
+
168
+ export const Grid: Story = {
169
+ args: {
170
+ label: '',
171
+ },
172
+ render: () => (
173
+ <View style={{ gap: 16 }}>
174
+ <AnswerOptionCard status="default" label="Boa noite" prefixNode={<CircleLetter letter="A" />} />
175
+ <AnswerOptionCard status="selected" label="Bom dia / Olá" prefixNode={<CircleLetter letter="B" selected />} />
176
+ <AnswerOptionCard status="correct" label="Bom dia / Olá" prefixNode={<CircleLetter letter="B" correct />} />
177
+ <AnswerOptionCard status="wrong" label="Obrigado" prefixNode={<CircleLetter letter="C" />} />
178
+ <AnswerOptionCard status="disabled" label="Por favor" prefixNode={<CircleLetter letter="D" />} />
179
+ </View>
180
+ ),
181
+ };