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