@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,333 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { View } from 'react-native';
4
+ import { FillInTheBlank } from './FillInTheBlank';
5
+ import { WordChip } from '../../Buttons/WordChip/WordChip';
6
+ import { MainButton } from '../../Buttons/MainButton/MainButton';
7
+ import { useResponsive } from '../../../hooks/useResponsive';
8
+
9
+ const meta = {
10
+ title: 'Components/Inputs/FillInTheBlank',
11
+ component: FillInTheBlank,
12
+ parameters: {
13
+ layout: 'centered',
14
+ docs: {
15
+ description: {
16
+ component: `O **FillInTheBlank** é um componente interativo avançado para exercícios de preenchimento de lacunas (Cloze Tests), amplamente usados em apps de ensino de línguas.
17
+
18
+ Ele é focado apenas na renderização da frase e das lacunas. O banco de opções (Word Bank) é renderizado separadamente pelo ecrã usando o componente \`WordChip\`, o que permite organizar os botões da forma que desejar.
19
+
20
+ ### Exemplo de Uso (Seleção / Word Bank com chips externos)
21
+ \`\`\`tsx
22
+ import { useState } from 'react';
23
+ import { FillInTheBlank, WordChip } from '@xetlingo/design-system';
24
+
25
+ const parts = [
26
+ { type: 'text', text: 'Je suis' },
27
+ { type: 'blank', id: 'b1', placeholder: '...' },
28
+ { type: 'text', text: "et j'habite à Paris." }
29
+ ];
30
+
31
+ const MyScreen = () => {
32
+ const [values, setValues] = useState<Record<string, string>>({});
33
+ const [activeId, setActiveId] = useState('b1');
34
+
35
+ return (
36
+ <>
37
+ <FillInTheBlank
38
+ instruction="Completa a frase:"
39
+ parts={parts}
40
+ values={values}
41
+ activeBlankId={activeId}
42
+ onBlankPress={(id) => setActiveId(id)}
43
+ />
44
+
45
+ {/* WordChips gerenciados pelo ecrã */}
46
+ <WordChip
47
+ word="étudiant"
48
+ state={values.b1 === 'étudiant' ? 'used' : 'default'}
49
+ onPress={() => setValues({ b1: 'étudiant' })}
50
+ />
51
+ </>
52
+ );
53
+ };
54
+ \`\`\`
55
+
56
+ ### Exemplo de Uso (Modo Digitação)
57
+ \`\`\`tsx
58
+ import { useState } from 'react';
59
+ import { FillInTheBlank } from '@xetlingo/design-system';
60
+
61
+ const inputParts = [
62
+ { type: 'text', text: 'He' },
63
+ { type: 'blank', id: 'b1', placeholder: 'write', expectedLength: 6 },
64
+ { type: 'text', text: 'letters.' }
65
+ ];
66
+
67
+ const MyInputScreen = () => {
68
+ const [values, setValues] = useState<Record<string, string>>({});
69
+
70
+ return (
71
+ <FillInTheBlank
72
+ instruction="Digita a resposta correta:"
73
+ parts={inputParts}
74
+ values={values}
75
+ mode="input"
76
+ onChangeText={(id, text) => setValues(prev => ({ ...prev, [id]: text }))}
77
+ />
78
+ );
79
+ };
80
+ \`\`\`
81
+ `
82
+ }
83
+ }
84
+ },
85
+ tags: ['autodocs'],
86
+ decorators: [
87
+ (Story) => (
88
+ <View style={{ width: '100%', maxWidth: 500, alignSelf: 'center' }}>
89
+ <Story />
90
+ </View>
91
+ ),
92
+ ],
93
+ } satisfies Meta<any>;
94
+
95
+ export default meta;
96
+ type Story = StoryObj<any>;
97
+
98
+ // 1. Interactive Selection (Word Bank) wrapper
99
+ const InteractiveSelectionWrapper = (args: any) => {
100
+ const { width } = useResponsive();
101
+ const isSmallScreen = width < 480;
102
+
103
+ const [values, setValues] = useState<Record<string, string>>({});
104
+ const [usedOptions, setUsedOptions] = useState<string[]>([]);
105
+ const [activeBlankId, setActiveBlankId] = useState<string>('blank1');
106
+ const [validationStates, setValidationStates] = useState<Record<string, 'correct' | 'wrong'>>({});
107
+
108
+ const parts = [
109
+ { type: 'text' as const, text: 'Je suis' },
110
+ { type: 'blank' as const, id: 'blank1', placeholder: '...' },
111
+ { type: 'text' as const, text: "et j'habite à" },
112
+ { type: 'blank' as const, id: 'blank2', placeholder: '...' },
113
+ { type: 'text' as const, text: 'depuis dois anos.' },
114
+ ];
115
+
116
+ const options = ['étudiant', 'Paris', 'professeur', 'Saint-Louis', 'médecin'];
117
+
118
+ const handleOptionPress = (word: string) => {
119
+ if (!activeBlankId) return;
120
+
121
+ setValidationStates({});
122
+
123
+ const oldWord = values[activeBlankId];
124
+ let newUsed = [...usedOptions];
125
+ if (oldWord) {
126
+ newUsed = newUsed.filter(w => w !== oldWord);
127
+ }
128
+
129
+ setValues(prev => ({ ...prev, [activeBlankId]: word }));
130
+ setUsedOptions([...newUsed, word]);
131
+
132
+ if (activeBlankId === 'blank1' && !values['blank2']) {
133
+ setActiveBlankId('blank2');
134
+ } else {
135
+ setActiveBlankId('');
136
+ }
137
+ };
138
+
139
+ const handleBlankPress = (id: string) => {
140
+ setActiveBlankId(id);
141
+ setValidationStates({});
142
+
143
+ const currentWord = values[id];
144
+ if (currentWord) {
145
+ setValues(prev => ({ ...prev, [id]: '' }));
146
+ setUsedOptions(prev => prev.filter(w => w !== currentWord));
147
+ }
148
+ };
149
+
150
+ const handleReset = () => {
151
+ setValues({});
152
+ setUsedOptions([]);
153
+ setActiveBlankId('blank1');
154
+ setValidationStates({});
155
+ };
156
+
157
+ const handleVerify = () => {
158
+ const results: Record<string, 'correct' | 'wrong'> = {};
159
+ results.blank1 = (values.blank1 === 'étudiant' || values.blank1 === 'professeur' || values.blank1 === 'médecin') ? 'correct' : 'wrong';
160
+ results.blank2 = values.blank2 === 'Paris' ? 'correct' : 'wrong';
161
+ setValidationStates(results);
162
+ };
163
+
164
+ return (
165
+ <View style={{ width: '100%' }}>
166
+ <FillInTheBlank
167
+ instruction={args.instruction}
168
+ parts={args.parts || parts}
169
+ values={values}
170
+ activeBlankId={activeBlankId}
171
+ onBlankPress={handleBlankPress}
172
+ validationStates={validationStates}
173
+ mode="selection"
174
+ />
175
+
176
+ <View style={[styles.optionsContainer, { marginTop: isSmallScreen ? 12 : 20, marginBottom: isSmallScreen ? 12 : 20 }]}>
177
+ {options.map((option, idx) => {
178
+ const isUsed = usedOptions.includes(option);
179
+ const state = isUsed ? 'used' : 'default';
180
+
181
+ return (
182
+ <WordChip
183
+ key={`opt-${idx}`}
184
+ word={option}
185
+ state={state}
186
+ onPress={() => !isUsed && handleOptionPress(option)}
187
+ style={{ margin: 4 }}
188
+ />
189
+ );
190
+ })}
191
+ </View>
192
+
193
+ <View style={[styles.buttonRow, { flexDirection: isSmallScreen ? 'column' : 'row' }]}>
194
+ <MainButton
195
+ variant="secondary"
196
+ onPress={handleReset}
197
+ style={isSmallScreen ? { width: '100%', marginBottom: 8 } : { flex: 1, marginRight: 8 }}
198
+ >
199
+ Limpar
200
+ </MainButton>
201
+ <MainButton
202
+ variant="primary"
203
+ onPress={handleVerify}
204
+ style={isSmallScreen ? { width: '100%' } : { flex: 2 }}
205
+ disabled={!values.blank1 || !values.blank2}
206
+ >
207
+ Verificar Resposta
208
+ </MainButton>
209
+ </View>
210
+ </View>
211
+ );
212
+ };
213
+
214
+ export const SelectionMode: Story = {
215
+ name: 'Modo Seleção (Word Bank Interativo)',
216
+ args: {
217
+ parts: [], instruction: 'Completa a frase:',
218
+ },
219
+ render: (args) => <InteractiveSelectionWrapper {...args} />,
220
+ };
221
+
222
+ // 2. Interactive Input (Typing) wrapper
223
+ const InteractiveInputWrapper = (args: any) => {
224
+ const { width } = useResponsive();
225
+ const isSmallScreen = width < 480;
226
+
227
+ const [values, setValues] = useState<Record<string, string>>({});
228
+ const [validationStates, setValidationStates] = useState<Record<string, 'correct' | 'wrong'>>({});
229
+
230
+ const parts = [
231
+ { type: 'text' as const, text: 'He' },
232
+ { type: 'blank' as const, id: 'b1', placeholder: 'write', expectedLength: 6 },
233
+ { type: 'text' as const, text: 'letters to his family, and they' },
234
+ { type: 'blank' as const, id: 'b2', placeholder: 'send', expectedLength: 4 },
235
+ { type: 'text' as const, text: 'them back.' },
236
+ ];
237
+
238
+ const handleTextChange = (id: string, text: string) => {
239
+ setValues(prev => ({ ...prev, [id]: text }));
240
+ if (validationStates[id]) {
241
+ setValidationStates(prev => ({ ...prev, [id]: undefined as any }));
242
+ }
243
+ };
244
+
245
+ const handleVerify = () => {
246
+ const results: Record<string, 'correct' | 'wrong'> = {};
247
+ results.b1 = (values.b1 || '').trim().toLowerCase() === 'writes' ? 'correct' : 'wrong';
248
+ results.b2 = (values.b2 || '').trim().toLowerCase() === 'send' ? 'correct' : 'wrong';
249
+ setValidationStates(results);
250
+ };
251
+
252
+ const handleReset = () => {
253
+ setValues({});
254
+ setValidationStates({});
255
+ };
256
+
257
+ return (
258
+ <View style={{ width: '100%' }}>
259
+ <FillInTheBlank
260
+ instruction={args.instruction}
261
+ parts={args.parts || parts}
262
+ values={values}
263
+ onChangeText={handleTextChange}
264
+ validationStates={validationStates}
265
+ mode="input"
266
+ />
267
+
268
+ <View style={[styles.buttonRow, { flexDirection: isSmallScreen ? 'column' : 'row', marginTop: 16 }]}>
269
+ <MainButton
270
+ variant="secondary"
271
+ onPress={handleReset}
272
+ style={isSmallScreen ? { width: '100%', marginBottom: 8 } : { flex: 1, marginRight: 8 }}
273
+ >
274
+ Limpar
275
+ </MainButton>
276
+ <MainButton
277
+ variant="primary"
278
+ onPress={handleVerify}
279
+ style={isSmallScreen ? { width: '100%' } : { flex: 2 }}
280
+ disabled={!values.b1 || !values.b2}
281
+ >
282
+ Verificar Resposta
283
+ </MainButton>
284
+ </View>
285
+ </View>
286
+ );
287
+ };
288
+
289
+ export const InputMode: Story = {
290
+ name: 'Modo Digitação (Teclado)',
291
+ args: {
292
+ parts: [], instruction: 'Digita a resposta correta:',
293
+ },
294
+ render: (args) => <InteractiveInputWrapper {...args} />,
295
+ };
296
+
297
+ // 3. Validation States (Correct / Wrong) static preview
298
+ export const ValidationStates: Story = {
299
+ name: 'Estados de Validação (Resultado)',
300
+ args: {
301
+ parts: [
302
+ { type: 'text', text: 'Nous' },
303
+ { type: 'blank', id: 'b1', expectedLength: 6 },
304
+ { type: 'text', text: 'le français et vous' },
305
+ { type: 'blank', id: 'b2', expectedLength: 7 },
306
+ { type: 'text', text: 'le wolof.' },
307
+ ],
308
+ values: {
309
+ b1: 'parlons',
310
+ b2: 'parlezs', // incorreto
311
+ },
312
+ validationStates: {
313
+ b1: 'correct',
314
+ b2: 'wrong',
315
+ },
316
+ mode: 'selection',
317
+ },
318
+ };
319
+
320
+ const styles = {
321
+ optionsContainer: {
322
+ flexDirection: 'row' as const,
323
+ flexWrap: 'wrap' as const,
324
+ alignItems: 'center' as const,
325
+ justifyContent: 'flex-start' as const,
326
+ width: '100%' as const,
327
+ },
328
+ buttonRow: {
329
+ alignItems: 'center' as const,
330
+ justifyContent: 'space-between' as const,
331
+ width: '100%' as const,
332
+ },
333
+ };
@@ -0,0 +1,287 @@
1
+
2
+ import { View, StyleSheet, TextInput, TouchableOpacity } 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 FillInTheBlankPart =
9
+ | { type: 'text'; text: string; id?: string }
10
+ | { type: 'blank'; id: string; blankWidth?: number; expectedLength?: number; placeholder?: string };
11
+
12
+ export interface FillInTheBlankProps {
13
+ /** Segmentos da frase (texto ou lacunas) */
14
+ parts: FillInTheBlankPart[];
15
+ /** Valores atualmente preenchidos nas lacunas: { [blankId: string]: string } */
16
+ values?: Record<string, string>;
17
+ /** Callback ao clicar numa lacuna (modo seleção/word bank) */
18
+ onBlankPress?: (blankId: string) => void;
19
+ /** Callback ao digitar texto numa lacuna (modo input) */
20
+ onChangeText?: (blankId: string, text: string) => void;
21
+ /** ID da lacuna ativa/focada (ex: para destacar em modo seleção) */
22
+ activeBlankId?: string;
23
+ /** Modo de interação da lacuna: 'selection' (word bank) ou 'input' (digitação) */
24
+ mode?: 'selection' | 'input';
25
+ /** Estados de validação para cada lacuna: { [blankId: string]: 'correct' | 'wrong' } */
26
+ validationStates?: Record<string, 'correct' | 'wrong'>;
27
+ /** Instrução opcional exibida acima do componente (ex: "Complete a frase:") */
28
+ instruction?: string;
29
+ /** Estilos customizados para o container wrapper principal */
30
+ style?: ViewStyle | ViewStyle[];
31
+ }
32
+
33
+ export const FillInTheBlank = ({
34
+ parts,
35
+ values = {},
36
+ onBlankPress,
37
+ onChangeText,
38
+ activeBlankId,
39
+ mode = 'selection',
40
+ validationStates = {},
41
+ instruction,
42
+ style,
43
+ }: FillInTheBlankProps) => {
44
+ const { scale, scaleText } = useResponsive();
45
+
46
+ const renderBlank = (part: FillInTheBlankPart) => {
47
+ const blankId = part.id || "";
48
+ const value = values[blankId] || '';
49
+ const validation = validationStates[blankId];
50
+ const isActive = activeBlankId === blankId;
51
+ const isFilled = value !== '';
52
+
53
+ // Calcula largura com base nas propriedades fornecidas
54
+ const blankWidth = (part as any).blankWidth
55
+ ? scale((part as any).blankWidth)
56
+ : (part as any).expectedLength
57
+ ? scale((part as any).expectedLength * 12 + 28)
58
+ : scale(88);
59
+
60
+ // Definição de estilos base do slot consoante o estado
61
+ let borderColor = theme.colors.brown[100];
62
+ let backgroundColor = theme.colors.brown[50];
63
+ let textColor = theme.colors.brown[800];
64
+ let borderStyle: 'solid' | 'dashed' = 'dashed';
65
+ let shadowColor = 'transparent';
66
+ let show3DShadow = false;
67
+
68
+ if (validation === 'correct') {
69
+ borderColor = theme.colors.success;
70
+ backgroundColor = '#eafaf2'; // verde claro
71
+ textColor = theme.colors.success;
72
+ borderStyle = 'solid';
73
+ shadowColor = 'rgba(22, 145, 90, 0.2)';
74
+ } else if (validation === 'wrong') {
75
+ borderColor = theme.colors.error;
76
+ backgroundColor = '#fdf0ee'; // vermelho claro
77
+ textColor = theme.colors.error;
78
+ borderStyle = 'solid';
79
+ shadowColor = 'rgba(207, 91, 72, 0.2)';
80
+ } else if (isFilled) {
81
+ borderColor = isActive ? theme.colors.primary : theme.colors.brown[500];
82
+ backgroundColor = theme.colors.white;
83
+ textColor = theme.colors.brown[800];
84
+ borderStyle = 'solid';
85
+ shadowColor = isActive ? 'rgba(242, 151, 75, 0.2)' : theme.colors.brown[100];
86
+ show3DShadow = true;
87
+ } else if (isActive) {
88
+ borderColor = theme.colors.primary;
89
+ backgroundColor = theme.colors.primaryLight;
90
+ borderStyle = 'solid';
91
+ }
92
+
93
+ if (mode === 'input') {
94
+ return (
95
+ <View
96
+ key={`blank-${blankId}`}
97
+ style={[
98
+ styles.blankContainer,
99
+ {
100
+ width: isFilled ? undefined : blankWidth,
101
+ minWidth: blankWidth,
102
+ marginHorizontal: scale(4),
103
+ marginVertical: scale(4),
104
+ }
105
+ ]}
106
+ >
107
+ <TextInput
108
+ value={value}
109
+ onChangeText={(text) => onChangeText?.(part.id || "", text)}
110
+ placeholder={(part as any).placeholder || '...'}
111
+ placeholderTextColor={theme.colors.brown[400]}
112
+ editable={validation === undefined}
113
+ style={[
114
+ styles.input,
115
+ {
116
+ height: scale(40),
117
+ borderColor,
118
+ backgroundColor,
119
+ color: textColor,
120
+ borderRadius: scale(10),
121
+ fontSize: scaleText(16),
122
+ paddingHorizontal: scale(12),
123
+ borderWidth: 2,
124
+ }
125
+ ]}
126
+ />
127
+ </View>
128
+ );
129
+ } else {
130
+ const displayContent = (
131
+ <View style={[
132
+ styles.blankInner,
133
+ {
134
+ borderColor,
135
+ backgroundColor,
136
+ borderRadius: scale(10),
137
+ borderWidth: 2,
138
+ borderStyle,
139
+ minWidth: blankWidth,
140
+ height: scale(40),
141
+ paddingHorizontal: scale(12),
142
+ }
143
+ ]}>
144
+ <Typography style={[
145
+ styles.blankText,
146
+ {
147
+ color: isFilled ? textColor : theme.colors.brown[400],
148
+ fontSize: scaleText(16),
149
+ }
150
+ ]}>
151
+ {value || (part as any).placeholder || ''}
152
+ </Typography>
153
+ </View>
154
+ );
155
+
156
+ const handlePress = () => {
157
+ if (validation === undefined && onBlankPress) {
158
+ onBlankPress(blankId);
159
+ }
160
+ };
161
+
162
+ if (isFilled && show3DShadow && validation === undefined) {
163
+ return (
164
+ <TouchableOpacity
165
+ key={`blank-${blankId}`}
166
+ onPress={handlePress}
167
+ activeOpacity={0.85}
168
+ style={[
169
+ styles.blankContainer,
170
+ {
171
+ marginHorizontal: scale(4),
172
+ marginVertical: scale(4),
173
+ }
174
+ ]}
175
+ >
176
+ <View style={[
177
+ styles.shadowContainer,
178
+ {
179
+ backgroundColor: shadowColor,
180
+ borderRadius: scale(10),
181
+ paddingBottom: scale(3),
182
+ }
183
+ ]}>
184
+ {displayContent}
185
+ </View>
186
+ </TouchableOpacity>
187
+ );
188
+ }
189
+
190
+ return (
191
+ <TouchableOpacity
192
+ key={`blank-${blankId}`}
193
+ onPress={handlePress}
194
+ disabled={validation !== undefined || !onBlankPress}
195
+ activeOpacity={0.85}
196
+ style={[
197
+ styles.blankContainer,
198
+ {
199
+ marginHorizontal: scale(4),
200
+ marginVertical: scale(4),
201
+ }
202
+ ]}
203
+ >
204
+ {displayContent}
205
+ </TouchableOpacity>
206
+ );
207
+ }
208
+ };
209
+
210
+ const renderPart = (part: FillInTheBlankPart, index: number) => {
211
+ if (part.type === 'text') {
212
+ const words = (part.text || '').split(' ');
213
+ return words.map((word, wIdx) => {
214
+ if (word === '' && wIdx > 0 && wIdx < words.length - 1) return null;
215
+
216
+ return (
217
+ <Typography
218
+ key={`text-${index}-${wIdx}`}
219
+ style={[styles.staticText, { fontSize: scaleText(17.5), marginRight: scale(6), marginVertical: scale(4) }]}
220
+ >
221
+ {word}
222
+ </Typography>
223
+ );
224
+ });
225
+ } else {
226
+ return renderBlank(part);
227
+ }
228
+ };
229
+
230
+ return (
231
+ <View style={[styles.container, style]}>
232
+ {/* Opcional: Texto de instrução no topo */}
233
+ {!!instruction && (
234
+ <Typography style={[styles.instructionText, { fontSize: scaleText(16), marginBottom: scale(16) }]}>
235
+ {instruction}
236
+ </Typography>
237
+ )}
238
+
239
+ {/* Zona da Frase / Bloco das Lacunas */}
240
+ <View style={styles.sentenceWrapper}>
241
+ {parts.map((part, index) => renderPart(part, index))}
242
+ </View>
243
+ </View>
244
+ );
245
+ };
246
+
247
+ const styles = StyleSheet.create({
248
+ container: {
249
+ width: '100%',
250
+ alignSelf: 'center',
251
+ },
252
+ instructionText: {
253
+
254
+ color: theme.colors.brown[500],
255
+
256
+ },
257
+ sentenceWrapper: {
258
+ flexDirection: 'row',
259
+ flexWrap: 'wrap',
260
+ alignItems: 'center',
261
+ justifyContent: 'flex-start',
262
+ width: '100%',
263
+ },
264
+ staticText: {
265
+
266
+ color: theme.colors.brown[800], // cor do texto principal
267
+ },
268
+ blankContainer: {
269
+ alignSelf: 'center',
270
+ },
271
+ blankInner: {
272
+ alignItems: 'center',
273
+ justifyContent: 'center',
274
+ },
275
+ blankText: {
276
+
277
+ textAlign: 'center',
278
+ },
279
+ input: {
280
+
281
+ textAlign: 'center',
282
+ padding: 0,
283
+ },
284
+ shadowContainer: {
285
+ alignSelf: 'flex-start',
286
+ },
287
+ });
@@ -0,0 +1 @@
1
+ export * from './FillInTheBlank';