@xetwa/design-system 1.0.38 → 1.0.40

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xetwa/design-system",
3
- "version": "1.0.38",
3
+ "version": "1.0.40",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,68 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { WordChipIcon } from './WordChipIcon';
3
+ import { Calendar, Bell } from 'lucide-react';
4
+ import React from 'react';
5
+
6
+ const meta = {
7
+ title: 'Components/Buttons/WordChipIcon',
8
+ component: WordChipIcon,
9
+ parameters: {
10
+ layout: 'centered',
11
+ docs: {
12
+ description: {
13
+ component: 'O **WordChipIcon** é um WordChip que permite renderizar um ícone no lado esquerdo ou direito.',
14
+ },
15
+ },
16
+ },
17
+ tags: ['autodocs'],
18
+ argTypes: {
19
+ word: { control: 'text' },
20
+ iconPosition: {
21
+ control: 'radio',
22
+ options: ['left', 'right'],
23
+ },
24
+ state: {
25
+ control: 'select',
26
+ options: ['default', 'selected', 'correct', 'wrong', 'used'],
27
+ },
28
+ disabled: { control: 'boolean' },
29
+ fullWidth: { control: 'boolean' },
30
+ size: {
31
+ control: 'select',
32
+ options: ['sm', 'md', 'lg'],
33
+ },
34
+ },
35
+ } satisfies Meta<typeof WordChipIcon>;
36
+
37
+ export default meta;
38
+ type Story = StoryObj<typeof meta>;
39
+
40
+ export const Default: Story = {
41
+ args: {
42
+ word: 'Calendário',
43
+ icon: <Calendar size={20} color="#cf7a30" />,
44
+ iconPosition: 'left',
45
+ state: 'default',
46
+ },
47
+ };
48
+
49
+ export const RightIcon: Story = {
50
+ args: {
51
+ word: 'Lembrete',
52
+ icon: <Bell size={20} color="#cf7a30" />,
53
+ iconPosition: 'right',
54
+ state: 'default',
55
+ },
56
+ };
57
+
58
+ export const AllStates: Story = {
59
+ render: () => (
60
+ <div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap', maxWidth: '600px' }}>
61
+ <WordChipIcon word="Calendário" icon={<Calendar size={20} color="#cf7a30" />} state="default" />
62
+ <WordChipIcon word="Lembrete" icon={<Bell size={20} color="#cf7a30" />} state="selected" />
63
+ <WordChipIcon word="Alarme" icon={<Bell size={20} color="#cf7a30" />} state="correct" />
64
+ <WordChipIcon word="Erro" icon={<Calendar size={20} color="#cf7a30" />} state="wrong" />
65
+ <WordChipIcon word="Usado" icon={<Calendar size={20} color="#cf7a30" />} state="used" />
66
+ </div>
67
+ ),
68
+ };
@@ -0,0 +1,186 @@
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
+ import type { WordChipState } from '../WordChip/WordChip';
8
+
9
+ export interface WordChipIconProps {
10
+ /** A palavra ou texto a apresentar. */
11
+ word: string;
12
+ /** O ícone a ser renderizado. */
13
+ icon: React.ReactNode;
14
+ /** A posição do ícone. Padrão: 'left' */
15
+ iconPosition?: 'left' | 'right';
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
+ /** Se verdadeiro, ocupa a largura total disponível. */
25
+ fullWidth?: boolean;
26
+ /** Tamanho do chip. */
27
+ size?: 'sm' | 'md' | 'lg';
28
+ /** Largura personalizada. Ignora max-width e align-self flex-start. */
29
+ width?: number | string;
30
+ }
31
+
32
+ export const WordChipIcon = ({
33
+ word,
34
+ icon,
35
+ iconPosition = 'left',
36
+ state = 'default',
37
+ onPress,
38
+ style,
39
+ disabled = false,
40
+ fullWidth = false,
41
+ size = 'md',
42
+ width,
43
+ }: WordChipIconProps) => {
44
+ const { scale } = useResponsive();
45
+ const [isPressed, setIsPressed] = useState(false);
46
+ const scaleAnim = useRef(new Animated.Value(state === 'selected' ? 0.96 : 1)).current;
47
+
48
+ // Animação ao entrar na resposta (bounce) quando o estado passa a 'selected'
49
+ useEffect(() => {
50
+ if (state === 'selected') {
51
+ scaleAnim.setValue(0.96);
52
+ Animated.spring(scaleAnim, {
53
+ toValue: 1,
54
+ friction: 4,
55
+ tension: 100,
56
+ useNativeDriver: true,
57
+ }).start();
58
+ } else {
59
+ scaleAnim.setValue(1);
60
+ }
61
+ }, [state, scaleAnim]);
62
+
63
+ const getStateStyles = () => {
64
+ switch (state) {
65
+ case 'selected':
66
+ return {
67
+ bg: theme.colors.primaryLight,
68
+ border: theme.colors.primary,
69
+ shadow: 'rgba(242, 151, 75, 0.25)',
70
+ text: theme.colors.brown[800],
71
+ };
72
+ case 'correct':
73
+ return {
74
+ bg: '#eafaf2',
75
+ border: theme.colors.success,
76
+ shadow: 'rgba(22, 145, 90, 0.2)',
77
+ text: theme.colors.success,
78
+ };
79
+ case 'wrong':
80
+ return {
81
+ bg: '#fdf0ee',
82
+ border: theme.colors.error,
83
+ shadow: 'rgba(207, 91, 72, 0.2)',
84
+ text: theme.colors.error,
85
+ };
86
+ case 'used':
87
+ return {
88
+ bg: theme.colors.white,
89
+ border: theme.colors.brown[100],
90
+ shadow: theme.colors.brown[100],
91
+ text: theme.colors.brown[300],
92
+ };
93
+ case 'default':
94
+ default:
95
+ return {
96
+ bg: theme.colors.white,
97
+ border: theme.colors.brown[100],
98
+ shadow: theme.colors.brown[100],
99
+ text: theme.colors.brown[800],
100
+ };
101
+ }
102
+ };
103
+
104
+ const currentStyles = getStateStyles();
105
+ const isDisabled = disabled || state === 'used';
106
+
107
+ // Press state visual adjustments
108
+ const showPressed = isPressed && !isDisabled;
109
+
110
+ const getTypographyVariant = () => {
111
+ switch (size) {
112
+ case 'lg': return 'btnLg';
113
+ case 'sm': return 'bodyMd';
114
+ case 'md':
115
+ default: return 'bodyLg';
116
+ }
117
+ };
118
+
119
+ return (
120
+ <Animated.View style={[{ transform: [{ scale: scaleAnim }] }, width ? { width } : null, style]}>
121
+ <Pressable
122
+ onPressIn={() => setIsPressed(true)}
123
+ onPressOut={() => setIsPressed(false)}
124
+ onPress={onPress}
125
+ disabled={isDisabled}
126
+ style={{ alignSelf: width ? 'stretch' : (fullWidth ? 'stretch' : 'flex-start') }}
127
+ >
128
+ <View style={[
129
+ styles.shadowContainer,
130
+ {
131
+ backgroundColor: currentStyles.shadow,
132
+ borderRadius: scale(12),
133
+ paddingBottom: showPressed ? 0 : scale(3),
134
+ marginTop: showPressed ? scale(3) : 0,
135
+ },
136
+ (fullWidth || width) ? { alignSelf: 'stretch', width: '100%' } : null
137
+ ]}>
138
+ <View style={[
139
+ styles.innerContainer,
140
+ {
141
+ backgroundColor: currentStyles.bg,
142
+ borderColor: currentStyles.border,
143
+ borderRadius: scale(12),
144
+ paddingVertical: scale(size === 'lg' ? 14 : size === 'sm' ? 6 : 9),
145
+ paddingHorizontal: scale(size === 'lg' ? 20 : size === 'sm' ? 10 : 14),
146
+ minHeight: scale(size === 'lg' ? 56 : size === 'sm' ? 36 : 44),
147
+ },
148
+ (fullWidth || width) ? { width: '100%' } : null
149
+ ]}>
150
+ <View style={[
151
+ styles.contentRow,
152
+ { flexDirection: iconPosition === 'right' ? 'row-reverse' : 'row' }
153
+ ]}>
154
+ {icon}
155
+ <Typography variant={getTypographyVariant()} style={[
156
+ styles.text,
157
+ { color: currentStyles.text }
158
+ ]}>
159
+ {word}
160
+ </Typography>
161
+ </View>
162
+ </View>
163
+ </View>
164
+ </Pressable>
165
+ </Animated.View>
166
+ );
167
+ };
168
+
169
+ const styles = StyleSheet.create({
170
+ shadowContainer: {
171
+ alignSelf: 'flex-start',
172
+ },
173
+ innerContainer: {
174
+ borderWidth: 2,
175
+ alignItems: 'center',
176
+ justifyContent: 'center',
177
+ },
178
+ contentRow: {
179
+ alignItems: 'center',
180
+ justifyContent: 'center',
181
+ gap: 8,
182
+ },
183
+ text: {
184
+ textAlign: 'center',
185
+ },
186
+ });
@@ -0,0 +1 @@
1
+ export * from './WordChipIcon';
@@ -1,9 +1,9 @@
1
1
  import type { ReactNode } from 'react';
2
- import { View, Pressable } from 'react-native';
3
- import type { ViewStyle, DimensionValue } from 'react-native';
2
+ import type { DimensionValue, ViewStyle } from 'react-native';
3
+ import { Pressable, View } from 'react-native';
4
4
  import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated';
5
- import { theme } from '../../../styles/theme';
6
5
  import { useResponsive } from '../../../hooks/useResponsive';
6
+ import { theme } from '../../../styles/theme';
7
7
 
8
8
  const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
9
9
 
@@ -15,41 +15,41 @@ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
15
15
  export interface BaseCardProps {
16
16
  /** O conteúdo a ser renderizado dentro do cartão. */
17
17
  children?: ReactNode;
18
-
18
+
19
19
  /** Variante de estilo do cartão */
20
20
  variant?: 'default' | 'soft';
21
-
21
+
22
22
  // Container styling
23
23
  backgroundColor?: string;
24
24
  borderRadius?: number;
25
25
  borderWidth?: number;
26
26
  borderColor?: string;
27
27
  borderStyle?: 'solid' | 'dashed';
28
-
28
+
29
29
  // Padding & Layout
30
30
  padding?: number;
31
31
  paddingVertical?: number;
32
32
  paddingHorizontal?: number;
33
-
33
+
34
34
  // Sizing
35
35
  width?: DimensionValue;
36
36
  height?: DimensionValue;
37
37
  minHeight?: DimensionValue;
38
-
38
+
39
39
  // Shadow presets: 'none' | 'sm' | 'md' | 'lg'
40
40
  shadow?: 'none' | 'sm' | 'md' | 'lg';
41
41
  shadowColor?: string;
42
-
42
+
43
43
  // Interactive / Pressable behavior
44
44
  onPress?: () => void;
45
45
  disabled?: boolean;
46
-
46
+
47
47
  // Selection state
48
48
  isSelected?: boolean;
49
49
  selectedBorderColor?: string;
50
50
  selectedBackgroundColor?: string;
51
51
  selectedShadowColor?: string;
52
-
52
+
53
53
  // Opacity & other styles
54
54
  opacity?: number;
55
55
  overflow?: 'visible' | 'hidden';
@@ -107,12 +107,12 @@ export const BaseCard = ({
107
107
  const activeBorderWidth = isSelected ? 2 : borderWidth;
108
108
  const activeShadow = isSelected
109
109
  ? {
110
- shadowColor: selectedShadowColor || '#f2974b',
111
- shadowOffset: { width: 0, height: scale(4) },
112
- shadowOpacity: 0.14,
113
- shadowRadius: scale(14),
114
- elevation: 4,
115
- }
110
+ shadowColor: selectedShadowColor || '#f2974b',
111
+ shadowOffset: { width: 0, height: scale(4) },
112
+ shadowOpacity: 0.14,
113
+ shadowRadius: scale(14),
114
+ elevation: 4,
115
+ }
116
116
  : getShadowStyle();
117
117
 
118
118
  // Consolidate static card styles with responsive scaling
@@ -16,7 +16,7 @@ const meta = {
16
16
  },
17
17
  tags: ['autodocs'],
18
18
  argTypes: {
19
- status: { control: 'radio', options: ['active', 'locked', 'completed', 'dark', 'active-dark'] },
19
+ status: { control: 'radio', options: ['active', 'locked', 'completed', 'dark', 'active-dark', 'completed-dark'] },
20
20
  number: { control: 'number' },
21
21
  title: { control: 'text' },
22
22
  subtitle: { control: 'text' },
@@ -75,6 +75,19 @@ export const ActiveDark: Story = {
75
75
  subtitle: 'Regatear, números, comida',
76
76
  subtext: '• 3/5',
77
77
  tagLabel: 'AULA MARCADA',
78
+ onSchedulePress: () => console.log('Agendar Aula'),
79
+ onReviewPress: () => console.log('Rever'),
80
+ },
81
+ };
82
+
83
+ export const ExamMode: Story = {
84
+ args: {
85
+ status: 'active-dark',
86
+ title: 'No mercado de Dakar',
87
+ subtitle: 'Exame de módulo',
88
+ subtext: '• 3/5',
89
+ tagLabel: 'POR FAZER',
90
+ onExamPress: () => console.log('Fazer Exame'),
78
91
  },
79
92
  };
80
93
 
@@ -88,6 +101,18 @@ export const Completed: Story = {
88
101
  },
89
102
  };
90
103
 
104
+ export const CompletedDark: Story = {
105
+ args: {
106
+ status: 'completed-dark',
107
+ number: 1,
108
+ title: 'Saudações & teranga (Dark)',
109
+ subtitle: 'Concluído',
110
+ subtext: '• 5/5',
111
+ onReviewPress: () => console.log('Rever módulo'),
112
+ onNextCountryPress: () => console.log('Ir para próximo país'),
113
+ },
114
+ };
115
+
91
116
  export const Grid: Story = {
92
117
  args: {
93
118
  status: 'active',
@@ -129,6 +154,23 @@ export const Grid: Story = {
129
154
  subtitle="Conversação e prática"
130
155
  subtext="• 4/4"
131
156
  tagLabel="AULA MARCADA"
157
+ onSchedulePress={() => {}}
158
+ onReviewPress={() => {}}
159
+ />
160
+ <ModuleCard
161
+ status="active-dark"
162
+ title="No mercado de Dakar"
163
+ subtitle="Exame de módulo"
164
+ subtext="• 3/5"
165
+ tagLabel="POR FAZER"
166
+ onExamPress={() => {}}
167
+ />
168
+ <ModuleCard
169
+ status="completed-dark"
170
+ number={4}
171
+ title="Módulo Concluído Dark"
172
+ subtitle="Concluído"
173
+ subtext="• 5/5"
132
174
  />
133
175
  </View>
134
176
  ),
@@ -4,20 +4,20 @@ import { theme } from '../../../styles/theme';
4
4
  import { Tag } from '../../DataDisplay/Tag/Tag';
5
5
  import { ProgressBar } from '../../DataDisplay/ProgressBar/ProgressBar';
6
6
  import { Badge } from '../../DataDisplay/Badge/Badge';
7
- import { Lock, Check, GraduationCap, RotateCcw, CalendarPlus } from 'lucide-react-native';
7
+ import { Lock, Check, GraduationCap, RotateCcw, CalendarPlus, PenTool } from 'lucide-react-native';
8
8
  import { useResponsive } from '../../../hooks/useResponsive';
9
9
  import { Typography } from '../../Typography/Typography';
10
10
  import React, { useRef } from 'react';
11
11
 
12
- export type ModuleCardStatus = 'active' | 'locked' | 'completed' | 'dark' | 'active-dark';
12
+ export type ModuleCardStatus = 'active' | 'locked' | 'completed' | 'dark' | 'active-dark' | 'completed-dark';
13
13
 
14
14
  /**
15
15
  * Cartão que representa uma lição ou módulo no caminho de aprendizagem.
16
16
  *
17
17
  * Regras de Animação e Clique:
18
18
  * - `active`: O cartão como um todo tem animação de escala e é clicável via `onPress`.
19
- * - `active-dark`: O cartão é estático. Apenas os botões internos de "Agendar" (`onSchedulePress`) e "Rever" (`onReviewPress`) têm animação individual.
20
- * - `completed`: O cartão é estático. Apenas o ícone/botão superior direito de "Rever" (`onReviewPress`) tem animação individual.
19
+ * - `active-dark`: O cartão é estático. Apenas os botões internos de "Agendar" (`onSchedulePress`), "Exame" (`onExamPress`) e "Rever" (`onReviewPress`) têm animação individual quando fornecidos.
20
+ * - `completed` / `completed-dark`: O cartão é estático. Apenas o ícone/botão superior direito de "Rever" (`onReviewPress`) tem animação individual.
21
21
  * - `locked` / `dark`: O cartão não é interativo.
22
22
  */
23
23
  export interface ModuleCardProps {
@@ -47,8 +47,14 @@ export interface ModuleCardProps {
47
47
  style?: ViewStyle | ViewStyle[];
48
48
  /** Função de clique no botão de Agendar (modo active/dark). */
49
49
  onSchedulePress?: () => void;
50
+ /** Função de clique no botão principal de Exame (modo active/dark). */
51
+ onExamPress?: () => void;
50
52
  /** Função de clique no botão de Rever (modo completed/active/dark). */
51
53
  onReviewPress?: () => void;
54
+ /** Função de clique no botão de Ir para o Próximo País (modo completed-dark). */
55
+ onNextCountryPress?: () => void;
56
+ /** Label do botão de próximo país. Padrão: "Seguir Viagem". */
57
+ nextCountryLabel?: string;
52
58
  }
53
59
 
54
60
  export const ModuleCard = ({
@@ -66,14 +72,19 @@ export const ModuleCard = ({
66
72
  onPress,
67
73
  style,
68
74
  onSchedulePress,
75
+ onExamPress,
69
76
  onReviewPress,
77
+ onNextCountryPress,
78
+ nextCountryLabel = 'Seguir Viagem',
70
79
  }: ModuleCardProps) => {
71
80
  const { scale, scaleText, width } = useResponsive();
72
81
  const isMobileScreen = width < 400;
73
82
 
74
83
  const scheduleScale = useRef(new Animated.Value(1)).current;
84
+ const examScale = useRef(new Animated.Value(1)).current;
75
85
  const reviewScale = useRef(new Animated.Value(1)).current;
76
86
  const completedReviewScale = useRef(new Animated.Value(1)).current;
87
+ const nextCountryScale = useRef(new Animated.Value(1)).current;
77
88
  const scaleAnim = useRef(new Animated.Value(1)).current;
78
89
 
79
90
  const handlePressIn = () => {
@@ -135,6 +146,7 @@ export const ModuleCard = ({
135
146
  };
136
147
  case 'dark':
137
148
  case 'active-dark':
149
+ case 'completed-dark':
138
150
  return {
139
151
  borderColor: theme.colors.brown[800],
140
152
  borderWidth: 0,
@@ -151,6 +163,7 @@ export const ModuleCard = ({
151
163
  return theme.colors.brown[400];
152
164
  case 'dark':
153
165
  case 'active-dark':
166
+ case 'completed-dark':
154
167
  return theme.colors.white;
155
168
  default:
156
169
  return theme.colors.brown[800];
@@ -159,7 +172,7 @@ export const ModuleCard = ({
159
172
 
160
173
  const containerStyle = getContainerStyle();
161
174
  const titleStyleColor = getTextColor();
162
- const subtitleStyleColor = subtitleColor || (status === 'completed' ? theme.colors.success : status === 'locked' || status === 'dark' || status === 'active-dark' ? theme.colors.brown[300] : theme.colors.brown[500]);
175
+ const subtitleStyleColor = subtitleColor || (status === 'completed' || status === 'completed-dark' ? theme.colors.success : status === 'locked' || status === 'dark' || status === 'active-dark' ? theme.colors.brown[300] : theme.colors.brown[500]);
163
176
 
164
177
  const activeShadow = status === 'active' ? {
165
178
  shadowColor: theme.colors.primary,
@@ -180,7 +193,7 @@ export const ModuleCard = ({
180
193
  />
181
194
  );
182
195
  }
183
- if (status === 'completed') {
196
+ if (status === 'completed' || status === 'completed-dark') {
184
197
  const checkSize = isMobileScreen ? scale(16) : scale(22);
185
198
  return (
186
199
  <Badge
@@ -290,9 +303,24 @@ export const ModuleCard = ({
290
303
  )}
291
304
  </View>
292
305
 
293
- {(status === 'locked' || status === 'dark' || status === 'completed') && (
306
+ {(status === 'locked' || status === 'dark' || status === 'completed' || status === 'completed-dark') && (
294
307
  <View style={[styles.rightContent, { marginLeft: colGap }]}>
295
- {status === 'completed' && (
308
+ {status === 'completed-dark' && onNextCountryPress ? (
309
+ <Animated.View style={{ transform: [{ scale: nextCountryScale }] }}>
310
+ <Pressable
311
+ onPress={onNextCountryPress}
312
+ onPressIn={() => animateBtnIn(nextCountryScale)}
313
+ onPressOut={() => animateBtnOut(nextCountryScale)}
314
+ hitSlop={10}
315
+ >
316
+ <Tag
317
+ label={nextCountryLabel}
318
+ variant="success"
319
+ size={isMobileScreen ? 'xs' : 'sm'}
320
+ />
321
+ </Pressable>
322
+ </Animated.View>
323
+ ) : (status === 'completed' || status === 'completed-dark') ? (
296
324
  <Animated.View style={{ transform: [{ scale: completedReviewScale }] }}>
297
325
  <Pressable
298
326
  style={styles.reviewIconButton}
@@ -305,7 +333,7 @@ export const ModuleCard = ({
305
333
  <Typography variant="label" style={[styles.reviewIconText, { fontSize: scaleText(9) }]}>REVER</Typography>
306
334
  </Pressable>
307
335
  </Animated.View>
308
- )}
336
+ ) : null}
309
337
  {(status === 'locked' || status === 'dark') && (
310
338
  <Lock
311
339
  stroke={status === 'dark' ? 'rgba(255,255,255,0.4)' : '#c3b1a4'}
@@ -329,31 +357,52 @@ export const ModuleCard = ({
329
357
 
330
358
  {status === 'active-dark' && (
331
359
  <View style={styles.compactDarkFooter}>
332
- <Animated.View style={{ transform: [{ scale: scheduleScale }] }}>
333
- <Pressable
334
- onPress={onSchedulePress || (() => {})}
335
- onPressIn={() => animateBtnIn(scheduleScale)}
336
- onPressOut={() => animateBtnOut(scheduleScale)}
337
- hitSlop={8}
338
- style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: theme.colors.primary, paddingHorizontal: scale(14), paddingVertical: scale(6), borderRadius: 100 }}
339
- >
340
- <CalendarPlus stroke={theme.colors.white} strokeWidth={2.5} size={scale(14)} />
341
- <Typography variant="label" style={{ color: theme.colors.white, fontSize: scaleText(12), fontWeight: 'bold', marginLeft: scale(6) }}>Agendar Aula</Typography>
342
- </Pressable>
343
- </Animated.View>
360
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: scale(8) }}>
361
+ {onSchedulePress && (
362
+ <Animated.View style={{ transform: [{ scale: scheduleScale }] }}>
363
+ <Pressable
364
+ onPress={onSchedulePress}
365
+ onPressIn={() => animateBtnIn(scheduleScale)}
366
+ onPressOut={() => animateBtnOut(scheduleScale)}
367
+ hitSlop={8}
368
+ style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: theme.colors.primary, paddingHorizontal: scale(14), paddingVertical: scale(6), borderRadius: 100 }}
369
+ >
370
+ <CalendarPlus stroke={theme.colors.white} strokeWidth={2.5} size={scale(14)} />
371
+ <Typography variant="label" style={{ color: theme.colors.white, fontSize: scaleText(12), fontWeight: 'bold', marginLeft: scale(6) }}>Agendar Aula</Typography>
372
+ </Pressable>
373
+ </Animated.View>
374
+ )}
344
375
 
345
- <Animated.View style={{ transform: [{ scale: reviewScale }] }}>
346
- <Pressable
347
- style={styles.compactReviewBtn}
348
- onPress={onReviewPress || (() => {})}
349
- onPressIn={() => animateBtnIn(reviewScale)}
350
- onPressOut={() => animateBtnOut(reviewScale)}
351
- hitSlop={8}
352
- >
353
- <RotateCcw stroke={theme.colors.primary} strokeWidth={2.5} size={scale(16)} />
354
- <Typography variant="label" style={[styles.reviewIconText, { color: theme.colors.primary, fontSize: scaleText(10), marginLeft: scale(4), marginTop: 0 }]}>REVER</Typography>
355
- </Pressable>
356
- </Animated.View>
376
+ {onExamPress && (
377
+ <Animated.View style={{ transform: [{ scale: examScale }] }}>
378
+ <Pressable
379
+ onPress={onExamPress}
380
+ onPressIn={() => animateBtnIn(examScale)}
381
+ onPressOut={() => animateBtnOut(examScale)}
382
+ hitSlop={8}
383
+ style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: theme.colors.primary, paddingHorizontal: scale(14), paddingVertical: scale(6), borderRadius: 100 }}
384
+ >
385
+ <PenTool stroke={theme.colors.white} strokeWidth={2.5} size={scale(14)} />
386
+ <Typography variant="label" style={{ color: theme.colors.white, fontSize: scaleText(12), fontWeight: 'bold', marginLeft: scale(6) }}>Fazer Exame</Typography>
387
+ </Pressable>
388
+ </Animated.View>
389
+ )}
390
+ </View>
391
+
392
+ {onReviewPress && (
393
+ <Animated.View style={{ transform: [{ scale: reviewScale }] }}>
394
+ <Pressable
395
+ style={styles.compactReviewBtn}
396
+ onPress={onReviewPress}
397
+ onPressIn={() => animateBtnIn(reviewScale)}
398
+ onPressOut={() => animateBtnOut(reviewScale)}
399
+ hitSlop={8}
400
+ >
401
+ <RotateCcw stroke={theme.colors.primary} strokeWidth={2.5} size={scale(16)} />
402
+ <Typography variant="label" style={[styles.reviewIconText, { color: theme.colors.primary, fontSize: scaleText(10), marginLeft: scale(4), marginTop: 0 }]}>REVER</Typography>
403
+ </Pressable>
404
+ </Animated.View>
405
+ )}
357
406
  </View>
358
407
  )}
359
408
 
@@ -0,0 +1,124 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { View, StyleSheet, Animated, Modal, Pressable } from 'react-native';
3
+ import { Typography } from '../../Typography/Typography';
4
+ import { theme } from '../../../styles/theme';
5
+ import { useResponsive } from '../../../hooks/useResponsive';
6
+ import { X } from 'lucide-react-native';
7
+
8
+ export interface BottomSheetProps {
9
+ /** Se o painel está visível */
10
+ isVisible: boolean;
11
+ /** Callback para fechar o painel */
12
+ onClose: () => void;
13
+ /** Título do painel */
14
+ title?: string;
15
+ /** Conteúdo do painel */
16
+ children: React.ReactNode;
17
+ }
18
+
19
+ /**
20
+ * Componente BottomSheet
21
+ *
22
+ * Um painel que desliza desde a parte inferior do ecrã para mostrar
23
+ * conteúdos adicionais, como seletores, opções extras ou formulários.
24
+ */
25
+ export const BottomSheet: React.FC<BottomSheetProps> = ({
26
+ isVisible,
27
+ onClose,
28
+ title,
29
+ children,
30
+ }) => {
31
+ const { scale } = useResponsive();
32
+ const [renderModal, setRenderModal] = useState(isVisible);
33
+ const translateY = useRef(new Animated.Value(1000)).current;
34
+
35
+ // Sincronizar montagem do Modal com a animação
36
+ useEffect(() => {
37
+ if (isVisible) {
38
+ setRenderModal(true);
39
+ Animated.timing(translateY, {
40
+ toValue: 0,
41
+ duration: 300,
42
+ useNativeDriver: true,
43
+ easing: (value) => 1 - Math.pow(1 - value, 3), // ease-out
44
+ }).start();
45
+ } else if (renderModal) {
46
+ Animated.timing(translateY, {
47
+ toValue: 1000,
48
+ duration: 250,
49
+ useNativeDriver: true,
50
+ }).start(() => {
51
+ setRenderModal(false);
52
+ });
53
+ }
54
+ }, [isVisible, translateY, renderModal]);
55
+
56
+ const handleClose = () => {
57
+ onClose();
58
+ };
59
+
60
+ if (!renderModal) return null;
61
+
62
+ return (
63
+ <Modal visible={renderModal} transparent animationType="fade" onRequestClose={handleClose}>
64
+ <View style={styles.overlay}>
65
+ <Pressable style={styles.backdrop} onPress={handleClose} />
66
+ <Animated.View style={[styles.sheet, { transform: [{ translateY }] }]}>
67
+
68
+ <View style={[styles.header, { paddingVertical: scale(16), paddingHorizontal: scale(20) }]}>
69
+ {title ? (
70
+ <Typography variant="h2" style={styles.title}>{title}</Typography>
71
+ ) : <View />}
72
+
73
+ <Pressable onPress={handleClose} style={({ pressed }) => [styles.closeBtn, pressed && { opacity: 0.5 }]}>
74
+ <X stroke="#9a8478" strokeWidth={2.5} size={scale(24)} />
75
+ </Pressable>
76
+ </View>
77
+
78
+ <View style={[styles.content, { paddingHorizontal: scale(20), paddingBottom: scale(32) }]}>
79
+ {children}
80
+ </View>
81
+
82
+ </Animated.View>
83
+ </View>
84
+ </Modal>
85
+ );
86
+ };
87
+
88
+ const styles = StyleSheet.create({
89
+ overlay: {
90
+ flex: 1,
91
+ justifyContent: 'flex-end',
92
+ backgroundColor: 'rgba(0, 0, 0, 0.4)',
93
+ },
94
+ backdrop: {
95
+ ...StyleSheet.absoluteFillObject,
96
+ },
97
+ sheet: {
98
+ backgroundColor: theme.colors.white,
99
+ borderTopLeftRadius: theme.radius.xxl,
100
+ borderTopRightRadius: theme.radius.xxl,
101
+ width: '100%',
102
+ shadowColor: '#000',
103
+ shadowOffset: { width: 0, height: -2 },
104
+ shadowOpacity: 0.1,
105
+ shadowRadius: 10,
106
+ elevation: 10,
107
+ },
108
+ header: {
109
+ flexDirection: 'row',
110
+ justifyContent: 'space-between',
111
+ alignItems: 'center',
112
+ borderBottomWidth: 1,
113
+ borderBottomColor: theme.colors.brown[50], // #fbf6f0
114
+ },
115
+ title: {
116
+ color: theme.colors.brown[800],
117
+ },
118
+ closeBtn: {
119
+ padding: 4,
120
+ },
121
+ content: {
122
+ paddingTop: 16,
123
+ },
124
+ });
@@ -0,0 +1,31 @@
1
+ import React, { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { CalendarPicker } from './CalendarPicker';
4
+ import { View } from 'react-native';
5
+
6
+ const meta = {
7
+ title: 'Forms/CalendarPicker',
8
+ component: CalendarPicker,
9
+ decorators: [
10
+ (Story) => (
11
+ <View style={{ padding: 20, backgroundColor: '#fbf6f0', flex: 1 }}>
12
+ <Story />
13
+ </View>
14
+ ),
15
+ ],
16
+ } satisfies Meta<typeof CalendarPicker>;
17
+
18
+ export default meta;
19
+ type Story = StoryObj<typeof meta>;
20
+
21
+ export const Default: Story = {
22
+ render: () => {
23
+ const [date, setDate] = useState<string | Date>(new Date());
24
+ return (
25
+ <CalendarPicker
26
+ selectedDate={date}
27
+ onSelectDate={(str) => setDate(str)}
28
+ />
29
+ );
30
+ }
31
+ };
@@ -0,0 +1,220 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import { View, StyleSheet, Pressable } from 'react-native';
3
+ import { Typography } from '../../Typography/Typography';
4
+ import { theme } from '../../../styles/theme';
5
+ import { useResponsive } from '../../../hooks/useResponsive';
6
+ import { ChevronLeft, ChevronRight } from 'lucide-react-native';
7
+
8
+ export interface CalendarPickerProps {
9
+ /** Data atualmente selecionada (ex: '2024-03-24' ou Date object) */
10
+ selectedDate?: string | Date;
11
+ /** Callback quando uma data é escolhida */
12
+ onSelectDate: (dateString: string, date: Date) => void;
13
+ }
14
+
15
+ const WEEKDAYS = ['DOM', 'SEG', 'TER', 'QUA', 'QUI', 'SEX', 'SÁB'];
16
+ const MONTHS = [
17
+ 'JANEIRO', 'FEVEREIRO', 'MARÇO', 'ABRIL', 'MAIO', 'JUNHO',
18
+ 'JULHO', 'AGOSTO', 'SETEMBRO', 'OUTUBRO', 'NOVEMBRO', 'DEZEMBRO'
19
+ ];
20
+
21
+ /**
22
+ * Componente CalendarPicker
23
+ * Permite a seleção de uma data num calendário nativo customizado.
24
+ */
25
+ export const CalendarPicker: React.FC<CalendarPickerProps> = ({
26
+ selectedDate,
27
+ onSelectDate,
28
+ }) => {
29
+ const { scale } = useResponsive();
30
+
31
+ // Normalize selected date
32
+ const parsedSelectedDate = selectedDate
33
+ ? (typeof selectedDate === 'string' ? new Date(selectedDate) : selectedDate)
34
+ : null;
35
+
36
+ // Estado para o mês e ano visualizados
37
+ const [currentViewDate, setCurrentViewDate] = useState(
38
+ parsedSelectedDate || new Date()
39
+ );
40
+
41
+ const viewMonth = currentViewDate.getMonth();
42
+ const viewYear = currentViewDate.getFullYear();
43
+
44
+ const handlePrevMonth = () => {
45
+ setCurrentViewDate(new Date(viewYear, viewMonth - 1, 1));
46
+ };
47
+
48
+ const handleNextMonth = () => {
49
+ setCurrentViewDate(new Date(viewYear, viewMonth + 1, 1));
50
+ };
51
+
52
+ // Gerar dias do calendário
53
+ const daysInMonth = useMemo(() => {
54
+ const year = viewYear;
55
+ const month = viewMonth;
56
+ const date = new Date(year, month, 1);
57
+ const days: (Date | null)[] = [];
58
+
59
+ // Preencher dias vazios antes do dia 1 (para alinhar com o dia da semana correto)
60
+ const firstDayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, etc.
61
+ for (let i = 0; i < firstDayOfWeek; i++) {
62
+ days.push(null);
63
+ }
64
+
65
+ // Preencher dias do mês
66
+ while (date.getMonth() === month) {
67
+ days.push(new Date(date));
68
+ date.setDate(date.getDate() + 1);
69
+ }
70
+
71
+ return days;
72
+ }, [viewYear, viewMonth]);
73
+
74
+ const isSameDay = (d1: Date, d2: Date) => {
75
+ return d1.getFullYear() === d2.getFullYear() &&
76
+ d1.getMonth() === d2.getMonth() &&
77
+ d1.getDate() === d2.getDate();
78
+ };
79
+
80
+ // Função para garantir formato YYYY-MM-DD local sem problema de fuso horário
81
+ const formatDateString = (date: Date) => {
82
+ const y = date.getFullYear();
83
+ const m = String(date.getMonth() + 1).padStart(2, '0');
84
+ const d = String(date.getDate()).padStart(2, '0');
85
+ return `${y}-${m}-${d}`;
86
+ };
87
+
88
+ return (
89
+ <View style={styles.container}>
90
+ {/* Header do Mês */}
91
+ <View style={styles.header}>
92
+ <Pressable onPress={handlePrevMonth} style={({ pressed }) => [styles.arrowBtn, pressed && { opacity: 0.5 }]}>
93
+ <ChevronLeft stroke={theme.colors.brown[800]} strokeWidth={2.5} size={scale(24)} />
94
+ </Pressable>
95
+ <Typography variant="h2" style={styles.monthText}>
96
+ {MONTHS[viewMonth]} {viewYear}
97
+ </Typography>
98
+ <Pressable onPress={handleNextMonth} style={({ pressed }) => [styles.arrowBtn, pressed && { opacity: 0.5 }]}>
99
+ <ChevronRight stroke={theme.colors.brown[800]} strokeWidth={2.5} size={scale(24)} />
100
+ </Pressable>
101
+ </View>
102
+
103
+ {/* Dias da Semana (D S T Q Q S S) */}
104
+ <View style={styles.weekdaysRow}>
105
+ {WEEKDAYS.map((day, index) => (
106
+ <View key={index} style={styles.weekdayCell}>
107
+ <Typography variant="label" style={styles.weekdayText}>
108
+ {day[0]}
109
+ </Typography>
110
+ </View>
111
+ ))}
112
+ </View>
113
+
114
+ {/* Grelha de Dias */}
115
+ <View style={styles.daysGrid}>
116
+ {daysInMonth.map((date, index) => {
117
+ if (!date) {
118
+ return <View key={`empty-${index}`} style={styles.dayCell} />;
119
+ }
120
+
121
+ const isSelected = parsedSelectedDate ? isSameDay(date, parsedSelectedDate) : false;
122
+ const isToday = isSameDay(date, new Date());
123
+ const isPast = date < new Date(new Date().setHours(0,0,0,0));
124
+
125
+ return (
126
+ <Pressable
127
+ key={index}
128
+ disabled={isPast}
129
+ onPress={() => onSelectDate(formatDateString(date), date)}
130
+ style={({ pressed }) => [
131
+ styles.dayCell,
132
+ pressed && !isPast && { opacity: 0.7 },
133
+ isPast && { opacity: 0.3 }
134
+ ]}
135
+ >
136
+ <View style={[
137
+ styles.dayCircle,
138
+ isSelected && styles.dayCircleSelected
139
+ ]}>
140
+ <Typography
141
+ variant="bodyLg"
142
+ weight={isSelected ? 'bold' : 'normal'}
143
+ style={[
144
+ styles.dayText,
145
+ isSelected && styles.dayTextSelected,
146
+ isToday && !isSelected && styles.dayTextToday
147
+ ]}
148
+ >
149
+ {date.getDate()}
150
+ </Typography>
151
+ </View>
152
+ </Pressable>
153
+ );
154
+ })}
155
+ </View>
156
+ </View>
157
+ );
158
+ };
159
+
160
+ const styles = StyleSheet.create({
161
+ container: {
162
+ width: '100%',
163
+ },
164
+ header: {
165
+ flexDirection: 'row',
166
+ alignItems: 'center',
167
+ justifyContent: 'space-between',
168
+ marginBottom: 20,
169
+ paddingHorizontal: 8,
170
+ },
171
+ monthText: {
172
+ color: theme.colors.brown[800],
173
+ textTransform: 'uppercase',
174
+ },
175
+ arrowBtn: {
176
+ padding: 4,
177
+ },
178
+ weekdaysRow: {
179
+ flexDirection: 'row',
180
+ marginBottom: 8,
181
+ },
182
+ weekdayCell: {
183
+ flex: 1,
184
+ alignItems: 'center',
185
+ justifyContent: 'center',
186
+ },
187
+ weekdayText: {
188
+ color: theme.colors.brown[400],
189
+ },
190
+ daysGrid: {
191
+ flexDirection: 'row',
192
+ flexWrap: 'wrap',
193
+ },
194
+ dayCell: {
195
+ width: `${100 / 7}%`,
196
+ height: 44, // Altura fixa para garantir que o círculo interior fique centrado
197
+ alignItems: 'center',
198
+ justifyContent: 'center',
199
+ },
200
+ dayCircle: {
201
+ width: 36,
202
+ height: 36,
203
+ borderRadius: 18,
204
+ alignItems: 'center',
205
+ justifyContent: 'center',
206
+ },
207
+ dayCircleSelected: {
208
+ backgroundColor: theme.colors.primary,
209
+ },
210
+ dayText: {
211
+ color: theme.colors.brown[700],
212
+ },
213
+ dayTextSelected: {
214
+ color: theme.colors.white,
215
+ },
216
+ dayTextToday: {
217
+ color: theme.colors.primary,
218
+ fontWeight: 'bold',
219
+ },
220
+ });
@@ -4,6 +4,7 @@ import type { ViewStyle } from 'react-native';
4
4
  import { theme } from '../../../styles/theme';
5
5
  import { useResponsive } from '../../../hooks/useResponsive';
6
6
  import { Typography } from '../../Typography/Typography';
7
+ import { Calendar } from 'lucide-react-native';
7
8
 
8
9
  /**
9
10
  * Interface para os dados de um dia disponível no DaySelector.
@@ -36,6 +37,8 @@ export interface DaySelectorProps {
36
37
  onSelectDay: (id: string, date: Date) => void;
37
38
  /** Estilo para o container exterior */
38
39
  style?: ViewStyle | ViewStyle[];
40
+ /** Callback chamado quando o utilizador clica no ícone de calendário (para ver mais datas) */
41
+ onCalendarPress?: () => void;
39
42
  }
40
43
 
41
44
  export const DaySelector = ({
@@ -43,6 +46,7 @@ export const DaySelector = ({
43
46
  selectedId,
44
47
  onSelectDay,
45
48
  style,
49
+ onCalendarPress,
46
50
  }: DaySelectorProps) => {
47
51
  const { scale, scaleText } = useResponsive();
48
52
 
@@ -110,6 +114,30 @@ export const DaySelector = ({
110
114
  </Pressable>
111
115
  );
112
116
  })}
117
+
118
+ {onCalendarPress && (
119
+ <Pressable
120
+ onPress={onCalendarPress}
121
+ style={({ pressed }) => [
122
+ styles.item,
123
+ transitionStyle,
124
+ {
125
+ minWidth: scale(48),
126
+ paddingVertical: scale(8),
127
+ borderRadius: scale(13),
128
+ backgroundColor: '#fff',
129
+ borderWidth: scale(1.5),
130
+ borderColor: '#efe2d6',
131
+ borderStyle: 'dashed',
132
+ },
133
+ pressed && {
134
+ opacity: 0.7
135
+ }
136
+ ]}
137
+ >
138
+ <Calendar stroke="#9a8478" strokeWidth={2} size={scale(20)} />
139
+ </Pressable>
140
+ )}
113
141
  </ScrollView>
114
142
  </View>
115
143
  );
@@ -3,6 +3,7 @@ import type { ViewStyle } from 'react-native';
3
3
  import { theme } from '../../../styles/theme';
4
4
  import { useResponsive } from '../../../hooks/useResponsive';
5
5
  import { Typography } from '../../Typography/Typography';
6
+ import { Plus } from 'lucide-react-native';
6
7
 
7
8
  /**
8
9
  * Interface para os dados de um horário disponível no TimeSlot.
@@ -31,6 +32,8 @@ export interface TimeSlotProps {
31
32
  onSelectTime: (id: string, time: string) => void;
32
33
  /** Estilo para o container exterior */
33
34
  style?: ViewStyle | ViewStyle[];
35
+ /** Callback chamado quando o utilizador clica em "Ver mais" */
36
+ onShowMorePress?: () => void;
34
37
  }
35
38
 
36
39
  export const TimeSlot = ({
@@ -38,6 +41,7 @@ export const TimeSlot = ({
38
41
  selectedId,
39
42
  onSelectTime,
40
43
  style,
44
+ onShowMorePress,
41
45
  }: TimeSlotProps) => {
42
46
  const { scale, scaleText } = useResponsive();
43
47
 
@@ -96,6 +100,31 @@ export const TimeSlot = ({
96
100
  </Pressable>
97
101
  );
98
102
  })}
103
+ {onShowMorePress && (
104
+ <Pressable
105
+ onPress={onShowMorePress}
106
+ style={({ pressed }) => [
107
+ styles.item,
108
+ transitionStyle,
109
+ {
110
+ paddingVertical: scale(9),
111
+ paddingHorizontal: scale(14),
112
+ borderRadius: scale(11),
113
+ backgroundColor: '#fff',
114
+ borderWidth: scale(1.5),
115
+ borderColor: '#efe2d6',
116
+ borderStyle: 'dashed',
117
+ minHeight: scale(44),
118
+ minWidth: scale(44),
119
+ },
120
+ pressed && {
121
+ opacity: 0.7
122
+ }
123
+ ]}
124
+ >
125
+ <Plus stroke="#9a8478" strokeWidth={2.5} size={scale(18)} />
126
+ </Pressable>
127
+ )}
99
128
  </View>
100
129
  </View>
101
130
  );
package/src/index.ts CHANGED
@@ -1,43 +1,36 @@
1
- export * from './components/Headers/CountryHeader/CountryHeader';
2
- export * from './components/Buttons/MainButton/MainButton';
3
- export * from './components/Buttons/BaseButton/BaseButton';
4
1
  export * from './components/Buttons/BackButton/BackButton';
2
+ export * from './components/Buttons/BaseButton/BaseButton';
5
3
  export * from './components/Buttons/FabButton/FabButton';
6
- export * from './components/Cards/ModuleCard/ModuleCard';
7
- export * from './components/Cards/ModuleCompletionCard/ModuleCompletionCard';
8
- export * from './components/Cards/AnswerOptionCard/AnswerOptionCard';
9
- export * from './components/DataDisplay/ProgressBar/ProgressBar';
10
- export * from './components/DataDisplay/Tag/Tag';
11
- export * from './components/DataDisplay/Badge/Badge';
12
- export * from './components/Inputs/TextInput/TextInput';
13
- export * from './components/Typography/Typography';
14
- export * from './components/Cards/BoardingPassCard/BoardingPassCard';
15
- export * from './components/Cards/SelectionCard/SelectionCard';
16
- export * from './components/Cards/PricingCard/PricingCard';
17
- export * from './components/DataDisplay/Flag/Flag';
18
- export * from './components/Cards/BaseCard/BaseCard';
19
- export * from './components/Cards/TeacherCard/TeacherCard';
20
- export * from './components/Cards/CountryCard/CountryCard';
21
- export * from './components/Cards/PlanCard/PlanCard';
22
- export * from './components/DataDisplay/LevelBadge/LevelBadge';
23
- export * from './hooks/useResponsive';
24
- export * from './components/Mascots/Vava/Vava';
25
- export * from './components/Mascots/Yaya/Yaya';
26
- export * from './components/Buttons/WordChip/WordChip';
27
- export * from './components/Feedback/FeedbackBottomSheet/FeedbackBottomSheet';
28
- export * from './components/Headers/ProfileHeader/ProfileHeader';
29
- export * from './components/Inputs/FillInTheBlank/FillInTheBlank';
30
- export * from './components/Charts/WeeklyProgressChart';
31
- export * from './components/Navigation/ProgressMap/ProgressMap';
32
4
  export * from './components/Buttons/IconButton/IconButton';
33
5
  export * from './components/Buttons/JourneyButton/JourneyButton';
6
+ export * from './components/Buttons/MainButton/MainButton';
34
7
  export * from './components/Buttons/WordChip/WordChip';
8
+ export * from './components/Buttons/WordChipIcon/WordChipIcon';
9
+ export * from './components/Cards/AnswerOptionCard/AnswerOptionCard';
10
+ export * from './components/Cards/BaseCard/BaseCard';
11
+ export * from './components/Cards/BoardingPassCard/BoardingPassCard';
12
+ export * from './components/Cards/CountryCard/CountryCard';
35
13
  export * from './components/Cards/LevelProgressCard/LevelProgressCard';
14
+ export * from './components/Cards/ModuleCard/ModuleCard';
15
+ export * from './components/Cards/ModuleCompletionCard/ModuleCompletionCard';
36
16
  export * from './components/Cards/Passport/PassportViewer';
17
+ export * from './components/Cards/PlanCard/PlanCard';
18
+ export * from './components/Cards/PricingCard/PricingCard';
19
+ export * from './components/Cards/SelectionCard/SelectionCard';
20
+ export * from './components/Cards/TeacherCard/TeacherCard';
21
+ export * from './components/Charts/WeeklyProgressChart';
37
22
  export * from './components/DataDisplay/Avatar/Avatar';
23
+ export * from './components/DataDisplay/Badge/Badge';
24
+ export * from './components/DataDisplay/Flag/Flag';
38
25
  export * from './components/DataDisplay/InstructionBubble/InstructionBubble';
39
- export * from './components/DataDisplay/StatBadge/StatBadge';
26
+ export * from './components/DataDisplay/LevelBadge/LevelBadge';
40
27
  export * from './components/DataDisplay/NumberBadge/NumberBadge';
28
+ export * from './components/DataDisplay/ProgressBar/ProgressBar';
29
+ export * from './components/DataDisplay/StatBadge/StatBadge';
30
+ export * from './components/DataDisplay/Tag/Tag';
31
+ export * from './components/Feedback/BottomSheet/BottomSheet';
32
+ export * from './components/Feedback/FeedbackBottomSheet/FeedbackBottomSheet';
33
+ export * from './components/Forms/CalendarPicker/CalendarPicker';
41
34
  export * from './components/Forms/Checkbox/Checkbox';
42
35
  export * from './components/Forms/DaySelector/DaySelector';
43
36
  export * from './components/Forms/Radio/Radio';
@@ -45,6 +38,16 @@ export * from './components/Forms/SegmentedToggle/SegmentedToggle';
45
38
  export * from './components/Forms/Select/Select';
46
39
  export * from './components/Forms/Switch/Switch';
47
40
  export * from './components/Forms/TimeSlot/TimeSlot';
41
+ export * from './components/Headers/CountryHeader/CountryHeader';
42
+ export * from './components/Headers/ProfileHeader/ProfileHeader';
43
+ export * from './components/Inputs/FillInTheBlank/FillInTheBlank';
44
+ export * from './components/Inputs/TextInput/TextInput';
45
+ export * from './components/Mascots/Vava/Vava';
46
+ export * from './components/Mascots/Yaya/Yaya';
48
47
  export * from './components/Navigation/BottomTabBar/BottomTabBar';
49
48
  export * from './components/Navigation/PaginationDots/PaginationDots';
49
+ export * from './components/Navigation/ProgressMap/ProgressMap';
50
50
  export * from './components/Navigation/WorldProgressMap/WorldProgressMap';
51
+ export * from './components/Typography/Typography';
52
+ export * from './hooks/useResponsive';
53
+
@@ -5,7 +5,7 @@ const font = (family: string, webWeight: '400' | '500' | '600' | '700' | '800' |
5
5
  const webFamily = family.split('-')[0];
6
6
  return Platform.select({
7
7
  web: { fontFamily: webFamily, fontWeight: webWeight },
8
- default: { fontFamily: family }
8
+ default: { fontFamily: family, fontWeight: webWeight }
9
9
  }) as { fontFamily: string; fontWeight?: '400' | '500' | '600' | '700' | '800' | '900' };
10
10
  };
11
11