@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,118 @@
|
|
|
1
|
+
import { View, StyleSheet } from 'react-native';
|
|
2
|
+
import type { ViewStyle } from 'react-native';
|
|
3
|
+
import { Flag } from '../Flag/Flag';
|
|
4
|
+
import { Typography } from '../../Typography/Typography';
|
|
5
|
+
|
|
6
|
+
export type CEFRLevel = 'A1' | 'A2' | 'B1' | 'B2' | 'C1';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Badge específico para exibir o nível de proficiência linguística (A1, A2, B1, etc).
|
|
10
|
+
*/
|
|
11
|
+
export interface LevelBadgeProps {
|
|
12
|
+
levelCode: CEFRLevel;
|
|
13
|
+
size?: 'lg' | 'sm';
|
|
14
|
+
countryFlagName?: string;
|
|
15
|
+
style?: ViewStyle | ViewStyle[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface LevelConfig {
|
|
19
|
+
label: string;
|
|
20
|
+
color: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const LEVEL_CONFIGS: Record<CEFRLevel, LevelConfig> = {
|
|
24
|
+
A1: { label: 'Iniciante', color: '#cf7a30' },
|
|
25
|
+
A2: { label: 'Elementar', color: '#cf7a30' },
|
|
26
|
+
B1: { label: 'Intermédio', color: '#16915a' },
|
|
27
|
+
B2: { label: 'Independente', color: '#16915a' },
|
|
28
|
+
C1: { label: 'Avançado', color: '#2a6fc2' },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const LevelBadge = ({
|
|
32
|
+
levelCode,
|
|
33
|
+
size = 'lg',
|
|
34
|
+
countryFlagName = 'França',
|
|
35
|
+
style,
|
|
36
|
+
}: LevelBadgeProps) => {
|
|
37
|
+
const config = LEVEL_CONFIGS[levelCode] || { label: 'Iniciante', color: '#cf7a30' };
|
|
38
|
+
const isLarge = size === 'lg';
|
|
39
|
+
|
|
40
|
+
// Container style configurations based on spec
|
|
41
|
+
const containerStyle: ViewStyle = isLarge
|
|
42
|
+
? {
|
|
43
|
+
paddingVertical: 10,
|
|
44
|
+
paddingHorizontal: 18,
|
|
45
|
+
borderRadius: 16,
|
|
46
|
+
borderWidth: 2,
|
|
47
|
+
borderColor: '#f2974b', // Custom orange highlight border for large badge
|
|
48
|
+
backgroundColor: '#fdf3ea', // Warm light cream/orange highlight background
|
|
49
|
+
}
|
|
50
|
+
: {
|
|
51
|
+
paddingVertical: 5,
|
|
52
|
+
paddingHorizontal: 12,
|
|
53
|
+
borderRadius: 12,
|
|
54
|
+
borderWidth: 1.5,
|
|
55
|
+
borderColor: '#efe2d6',
|
|
56
|
+
backgroundColor: '#fdf3ea',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Flag box dimensions based on size
|
|
60
|
+
const flagWidth = isLarge ? 26 : 18;
|
|
61
|
+
const flagHeight = isLarge ? 18 : 12;
|
|
62
|
+
const flagRadius = isLarge ? 4 : 2;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<View style={[styles.container, containerStyle, style]}>
|
|
66
|
+
{/* Rectangular flag mapping */}
|
|
67
|
+
<Flag
|
|
68
|
+
countryCode={countryFlagName}
|
|
69
|
+
size={flagWidth}
|
|
70
|
+
style={{
|
|
71
|
+
width: flagWidth,
|
|
72
|
+
height: flagHeight,
|
|
73
|
+
borderRadius: flagRadius,
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
{/* Level Code */}
|
|
78
|
+
<Typography style={[styles.levelText, isLarge ? styles.levelTextLg : styles.levelTextSm]}>
|
|
79
|
+
{levelCode}
|
|
80
|
+
</Typography>
|
|
81
|
+
|
|
82
|
+
{/* Optional Descriptive Label for Large Badge */}
|
|
83
|
+
{isLarge && (
|
|
84
|
+
<Typography style={[styles.labelText, { color: config.color }]}>
|
|
85
|
+
{config.label}
|
|
86
|
+
</Typography>
|
|
87
|
+
)}
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const styles = StyleSheet.create({
|
|
93
|
+
container: {
|
|
94
|
+
flexDirection: 'row',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
justifyContent: 'center',
|
|
97
|
+
gap: 8,
|
|
98
|
+
alignSelf: 'flex-start',
|
|
99
|
+
},
|
|
100
|
+
levelText: {
|
|
101
|
+
|
|
102
|
+
color: '#51332d',
|
|
103
|
+
lineHeight: 28, // Matches large font alignment
|
|
104
|
+
},
|
|
105
|
+
levelTextLg: {
|
|
106
|
+
fontSize: 28,
|
|
107
|
+
lineHeight: 30,
|
|
108
|
+
},
|
|
109
|
+
levelTextSm: {
|
|
110
|
+
fontSize: 14,
|
|
111
|
+
lineHeight: 16,
|
|
112
|
+
},
|
|
113
|
+
labelText: {
|
|
114
|
+
|
|
115
|
+
fontSize: 14,
|
|
116
|
+
lineHeight: 16,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
// eslint-disable-next-line storybook/no-renderer-packages
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import { View, Button } from 'react-native';
|
|
5
|
+
import { ProgressBar } from './ProgressBar';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Components/DataDisplay/ProgressBar',
|
|
9
|
+
component: ProgressBar,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'padded',
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component: 'O **ProgressBar** é utilizado para mostrar o progresso ao longo de um curso ou lição.\n\n### Exemplo de Uso\n```tsx\n<ProgressBar \n progress={0.5}\n segments={4}\n size="md"\n/>\n```',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
tags: ['autodocs'],
|
|
19
|
+
argTypes: {
|
|
20
|
+
progress: {
|
|
21
|
+
control: { type: 'range', min: 0, max: 1, step: 0.05 },
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
control: 'radio',
|
|
25
|
+
options: ['xs', 'sm', 'md'],
|
|
26
|
+
},
|
|
27
|
+
variant: {
|
|
28
|
+
control: 'radio',
|
|
29
|
+
options: ['default', 'success'],
|
|
30
|
+
},
|
|
31
|
+
segments: {
|
|
32
|
+
control: { type: 'number', min: 1 },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
decorators: [
|
|
36
|
+
(Story) => (
|
|
37
|
+
<View style={{ padding: 24, width: '100%', maxWidth: 600 }}>
|
|
38
|
+
<Story />
|
|
39
|
+
</View>
|
|
40
|
+
),
|
|
41
|
+
],
|
|
42
|
+
} satisfies Meta<any>;
|
|
43
|
+
|
|
44
|
+
export default meta;
|
|
45
|
+
type Story = StoryObj<any>;
|
|
46
|
+
|
|
47
|
+
export const Default: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
progress: 0.45,
|
|
50
|
+
size: 'sm',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Success: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
progress: 1,
|
|
57
|
+
variant: 'success',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const Sizes: Story = {
|
|
62
|
+
args: { progress: 0 },
|
|
63
|
+
render: () => (
|
|
64
|
+
<View style={{ gap: 20 }}>
|
|
65
|
+
<ProgressBar progress={0.3} size="xs" />
|
|
66
|
+
<ProgressBar progress={0.5} size="sm" />
|
|
67
|
+
<ProgressBar progress={0.8} size="md" />
|
|
68
|
+
</View>
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const Segmented: Story = {
|
|
73
|
+
args: {
|
|
74
|
+
progress: 0.6,
|
|
75
|
+
segments: 5,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const Interactive: Story = {
|
|
80
|
+
args: { progress: 0 },
|
|
81
|
+
render: () => {
|
|
82
|
+
const [progress, setProgress] = useState(0);
|
|
83
|
+
return (
|
|
84
|
+
<View style={{ gap: 20 }}>
|
|
85
|
+
<ProgressBar progress={progress} segments={3} />
|
|
86
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 }}>
|
|
87
|
+
<Button title="0%" onPress={() => setProgress(0)} />
|
|
88
|
+
<Button title="33%" onPress={() => setProgress(0.333)} />
|
|
89
|
+
<Button title="66%" onPress={() => setProgress(0.666)} />
|
|
90
|
+
<Button title="100%" onPress={() => setProgress(1)} />
|
|
91
|
+
</View>
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { View, StyleSheet, Animated, Easing } from 'react-native';
|
|
3
|
+
import type { ViewStyle } from 'react-native';
|
|
4
|
+
import { theme } from '../../../styles/theme';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Barra de progresso para exibir a percentagem de conclusão de uma tarefa ou módulo.
|
|
8
|
+
*/
|
|
9
|
+
export interface ProgressBarProps {
|
|
10
|
+
/** Progresso atual de 0.0 a 1.0 */
|
|
11
|
+
progress: number;
|
|
12
|
+
/** Se passado, a barra de progresso será dividida no número de segmentos indicados. */
|
|
13
|
+
segments?: number;
|
|
14
|
+
/** Tamanho da barra. */
|
|
15
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
16
|
+
/** Cor da barra (opcional, assume a cor primária por defeito). */
|
|
17
|
+
color?: string;
|
|
18
|
+
/** Variantes nativas */
|
|
19
|
+
variant?: 'default' | 'success';
|
|
20
|
+
/** Cor customizada forçada (sobrepõe a variante) da barra cheia */
|
|
21
|
+
fillColor?: string;
|
|
22
|
+
/** Cor customizada forçada do trilho vazio */
|
|
23
|
+
trackColor?: string;
|
|
24
|
+
style?: ViewStyle | ViewStyle[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ProgressBar = ({
|
|
28
|
+
progress = 0,
|
|
29
|
+
size = 'sm',
|
|
30
|
+
variant = 'default',
|
|
31
|
+
segments = 1,
|
|
32
|
+
fillColor,
|
|
33
|
+
trackColor,
|
|
34
|
+
style,
|
|
35
|
+
}: ProgressBarProps) => {
|
|
36
|
+
const [animatedProgress] = useState(() => new Animated.Value(0));
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
// Easing smooth: [0.4, 0, 0.2, 1] as defined in design specs
|
|
40
|
+
Animated.timing(animatedProgress, {
|
|
41
|
+
toValue: Math.max(0, Math.min(progress, 1)),
|
|
42
|
+
duration: 600,
|
|
43
|
+
easing: Easing.bezier(0.4, 0, 0.2, 1),
|
|
44
|
+
useNativeDriver: false, // width animation does not support native driver
|
|
45
|
+
}).start();
|
|
46
|
+
}, [progress, animatedProgress]);
|
|
47
|
+
|
|
48
|
+
const sizeStyles = {
|
|
49
|
+
xs: { height: 8, borderRadius: 5 },
|
|
50
|
+
sm: { height: 12, borderRadius: 7 },
|
|
51
|
+
md: { height: 16, borderRadius: 9 },
|
|
52
|
+
}[size === 'lg' ? 'md' : size] || { height: 16, borderRadius: 9 };
|
|
53
|
+
|
|
54
|
+
const defaultFillColor = variant === 'success' ? theme.colors.success : theme.colors.primary;
|
|
55
|
+
const activeFillColor = fillColor || defaultFillColor;
|
|
56
|
+
const activeTrackColor = trackColor || '#f3e7da';
|
|
57
|
+
|
|
58
|
+
const safeSegments = Math.max(1, Math.floor(segments));
|
|
59
|
+
const segmentsArray = Array.from({ length: safeSegments });
|
|
60
|
+
const segmentRange = 1 / safeSegments;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<View style={[styles.container, style]}>
|
|
64
|
+
{segmentsArray.map((_, index) => {
|
|
65
|
+
const start = index * segmentRange;
|
|
66
|
+
const end = (index + 1) * segmentRange;
|
|
67
|
+
|
|
68
|
+
// Interpolate this specific segment's fill percentage based on global progress
|
|
69
|
+
const widthInterpolation = animatedProgress.interpolate({
|
|
70
|
+
inputRange: [start, end],
|
|
71
|
+
outputRange: ['0%', '100%'],
|
|
72
|
+
extrapolate: 'clamp',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<View
|
|
77
|
+
key={index}
|
|
78
|
+
style={[
|
|
79
|
+
styles.track,
|
|
80
|
+
{ backgroundColor: activeTrackColor },
|
|
81
|
+
sizeStyles,
|
|
82
|
+
index < safeSegments - 1 && { marginRight: 3 },
|
|
83
|
+
]}
|
|
84
|
+
>
|
|
85
|
+
<Animated.View
|
|
86
|
+
style={[
|
|
87
|
+
styles.fill,
|
|
88
|
+
{ borderRadius: sizeStyles.borderRadius },
|
|
89
|
+
{ backgroundColor: activeFillColor, width: widthInterpolation },
|
|
90
|
+
]}
|
|
91
|
+
/>
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
})}
|
|
95
|
+
</View>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const styles = StyleSheet.create({
|
|
100
|
+
container: {
|
|
101
|
+
flexDirection: 'row',
|
|
102
|
+
width: '100%',
|
|
103
|
+
alignItems: 'center',
|
|
104
|
+
},
|
|
105
|
+
track: {
|
|
106
|
+
flex: 1,
|
|
107
|
+
backgroundColor: '#f3e7da',
|
|
108
|
+
overflow: 'hidden',
|
|
109
|
+
},
|
|
110
|
+
fill: {
|
|
111
|
+
height: '100%',
|
|
112
|
+
position: 'absolute',
|
|
113
|
+
left: 0,
|
|
114
|
+
top: 0,
|
|
115
|
+
bottom: 0,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// eslint-disable-next-line storybook/no-renderer-packages
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { View } from 'react-native';
|
|
4
|
+
import { StatBadge } from './StatBadge';
|
|
5
|
+
import { theme } from '../../../styles/theme';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Components/DataDisplay/StatBadge',
|
|
9
|
+
component: StatBadge,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'padded',
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component: 'O **StatBadge** fica normalmente na `TopNavigationBar` para exibir as vidas, fogos e gemas do utilizador.\n\n### Exemplo de Uso\n```tsx\n<StatBadge \n value={5}\n variant="lives"\n onPress={() => abrirLojaVidas()}\n/>\n```',
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
tags: ['autodocs'],
|
|
19
|
+
argTypes: {
|
|
20
|
+
variant: { control: 'radio', options: ['streak', 'xp', 'lives'] },
|
|
21
|
+
value: { control: 'text' },
|
|
22
|
+
status: { control: 'radio', options: ['active', 'inactive', 'at-risk'] },
|
|
23
|
+
},
|
|
24
|
+
decorators: [
|
|
25
|
+
(Story) => (
|
|
26
|
+
<View style={{ padding: 24, backgroundColor: theme.colors.cream }}>
|
|
27
|
+
<Story />
|
|
28
|
+
</View>
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
} satisfies Meta<any>;
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<any>;
|
|
35
|
+
|
|
36
|
+
export const Streak: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
variant: 'streak',
|
|
39
|
+
value: 12,
|
|
40
|
+
status: 'active',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const StreakAtRisk: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
variant: 'streak',
|
|
47
|
+
value: 12,
|
|
48
|
+
status: 'at-risk',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const StreakInactive: Story = {
|
|
53
|
+
args: {
|
|
54
|
+
variant: 'streak',
|
|
55
|
+
value: 0,
|
|
56
|
+
status: 'inactive',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const XP: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
variant: 'xp',
|
|
63
|
+
value: 240,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const Lives: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
variant: 'lives',
|
|
70
|
+
value: 5,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const HeaderExample: Story = {
|
|
75
|
+
render: () => (
|
|
76
|
+
<View style={{ flexDirection: 'row', gap: 12 }}>
|
|
77
|
+
<StatBadge variant="streak" value={12} />
|
|
78
|
+
<StatBadge variant="xp" value={240} />
|
|
79
|
+
<StatBadge variant="lives" value={5} />
|
|
80
|
+
</View>
|
|
81
|
+
),
|
|
82
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { View, StyleSheet, Animated } from 'react-native';
|
|
3
|
+
import { theme } from '../../../styles/theme';
|
|
4
|
+
import { Flame, Heart } from 'lucide-react-native';
|
|
5
|
+
import { useResponsive } from '../../../hooks/useResponsive';
|
|
6
|
+
import { Typography } from '../../Typography/Typography';
|
|
7
|
+
|
|
8
|
+
export type StatBadgeVariant = 'streak' | 'xp' | 'lives';
|
|
9
|
+
export type StatBadgeStatus = 'active' | 'inactive' | 'at-risk';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Badge estatístico usado no topo do ecrã para mostrar "fogo" (streak), corações ou gemas.
|
|
13
|
+
*/
|
|
14
|
+
export interface StatBadgeProps {
|
|
15
|
+
/** Valor numérico a apresentar. */
|
|
16
|
+
value: number | string;
|
|
17
|
+
/** Variante do ícone e cor associada. */
|
|
18
|
+
variant: StatBadgeVariant;
|
|
19
|
+
/** Estado da badge. */
|
|
20
|
+
status?: StatBadgeStatus;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const StatBadge = ({
|
|
24
|
+
variant,
|
|
25
|
+
value,
|
|
26
|
+
status = 'active',
|
|
27
|
+
}: StatBadgeProps) => {
|
|
28
|
+
const { scaleText } = useResponsive();
|
|
29
|
+
const pulseAnim = useRef(new Animated.Value(1)).current;
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
let anim: Animated.CompositeAnimation | null = null;
|
|
33
|
+
|
|
34
|
+
if (variant === 'streak' && status === 'at-risk') {
|
|
35
|
+
anim = Animated.loop(
|
|
36
|
+
Animated.sequence([
|
|
37
|
+
Animated.timing(pulseAnim, {
|
|
38
|
+
toValue: 1.3,
|
|
39
|
+
duration: 900,
|
|
40
|
+
useNativeDriver: false,
|
|
41
|
+
}),
|
|
42
|
+
Animated.timing(pulseAnim, {
|
|
43
|
+
toValue: 1,
|
|
44
|
+
duration: 900,
|
|
45
|
+
useNativeDriver: false,
|
|
46
|
+
}),
|
|
47
|
+
])
|
|
48
|
+
);
|
|
49
|
+
anim.start();
|
|
50
|
+
} else {
|
|
51
|
+
pulseAnim.setValue(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
if (anim) {
|
|
56
|
+
anim.stop();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}, [variant, status, pulseAnim]);
|
|
60
|
+
|
|
61
|
+
const renderIcon = () => {
|
|
62
|
+
if (variant === 'streak') {
|
|
63
|
+
const color = status === 'inactive' || Number(value) === 0 ? theme.colors.brown[300] : theme.colors.primary;
|
|
64
|
+
const IconComp = (
|
|
65
|
+
<Flame size={16} fill={color} color={color} strokeWidth={2} />
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (status === 'at-risk') {
|
|
69
|
+
return (
|
|
70
|
+
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
|
|
71
|
+
{IconComp}
|
|
72
|
+
</Animated.View>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return IconComp;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (variant === 'xp') {
|
|
79
|
+
return (
|
|
80
|
+
<View
|
|
81
|
+
style={{
|
|
82
|
+
width: 11,
|
|
83
|
+
height: 11,
|
|
84
|
+
backgroundColor: theme.colors.primary,
|
|
85
|
+
transform: [{ rotate: '45deg' }],
|
|
86
|
+
borderRadius: 2,
|
|
87
|
+
marginHorizontal: 2, // to account for rotation bounds
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (variant === 'lives') {
|
|
94
|
+
return (
|
|
95
|
+
<Heart size={18} fill={theme.colors.primary} color={theme.colors.primary} />
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<View style={styles.boxContainer}>
|
|
103
|
+
<View style={styles.iconContainer}>
|
|
104
|
+
{renderIcon()}
|
|
105
|
+
</View>
|
|
106
|
+
<Typography style={[styles.text, { fontSize: scaleText(16) }]}>
|
|
107
|
+
{value}
|
|
108
|
+
</Typography>
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const styles = StyleSheet.create({
|
|
114
|
+
boxContainer: {
|
|
115
|
+
flexDirection: 'row',
|
|
116
|
+
alignItems: 'center',
|
|
117
|
+
backgroundColor: theme.colors.white,
|
|
118
|
+
borderColor: theme.colors.brown[100],
|
|
119
|
+
borderWidth: 1.5,
|
|
120
|
+
borderRadius: 12,
|
|
121
|
+
paddingVertical: 5,
|
|
122
|
+
paddingHorizontal: 10,
|
|
123
|
+
alignSelf: 'flex-start',
|
|
124
|
+
},
|
|
125
|
+
iconContainer: {
|
|
126
|
+
marginRight: 6,
|
|
127
|
+
justifyContent: 'center',
|
|
128
|
+
alignItems: 'center',
|
|
129
|
+
width: 18, // Fixed width to align nicely
|
|
130
|
+
},
|
|
131
|
+
text: {
|
|
132
|
+
|
|
133
|
+
color: theme.colors.brown[800],
|
|
134
|
+
},
|
|
135
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './StatBadge';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// eslint-disable-next-line storybook/no-renderer-packages
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { View } from 'react-native';
|
|
4
|
+
import { Tag } from './Tag';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Components/DataDisplay/Tag',
|
|
8
|
+
component: Tag,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'padded',
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component: 'O componente **Tag** é uma pequena pílula de texto para dar contexto aos cartões, por exemplo "EM CURSO", "NOVO" ou "ESGOTADO".\n\n### Exemplo de Uso\n```tsx\n<Tag \n label="NOVO"\n variant="success"\n size="sm"\n/>\n```',
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
argTypes: {
|
|
19
|
+
variant: { control: 'radio', options: ['neutro', 'primary', 'success', 'error', 'dark'] },
|
|
20
|
+
size: { control: 'radio', options: ['xs', 'sm', 'md', 'lg'] },
|
|
21
|
+
label: { control: 'text' },
|
|
22
|
+
},
|
|
23
|
+
decorators: [
|
|
24
|
+
(Story) => (
|
|
25
|
+
<View style={{ padding: 24 }}>
|
|
26
|
+
<Story />
|
|
27
|
+
</View>
|
|
28
|
+
),
|
|
29
|
+
],
|
|
30
|
+
} satisfies Meta<any>;
|
|
31
|
+
|
|
32
|
+
export default meta;
|
|
33
|
+
type Story = StoryObj<any>;
|
|
34
|
+
|
|
35
|
+
export const Default: Story = {
|
|
36
|
+
args: { label: 'Em Curso', variant: 'primary', size: 'sm' },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Variants: Story = {
|
|
40
|
+
args: { label: 'Variantes' },
|
|
41
|
+
render: () => (
|
|
42
|
+
<View style={{ gap: 10, flexDirection: 'row', flexWrap: 'wrap' }}>
|
|
43
|
+
<Tag label="A1" variant="neutro" />
|
|
44
|
+
<Tag label="Em Curso" variant="primary" />
|
|
45
|
+
<Tag label="Concluído" variant="success" />
|
|
46
|
+
<Tag label="Bloqueado" variant="error" />
|
|
47
|
+
<Tag label="30 min" variant="neutro" />
|
|
48
|
+
<Tag label="Dark Mode" variant="dark" />
|
|
49
|
+
</View>
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Sizes: Story = {
|
|
54
|
+
args: { label: 'Tamanhos' },
|
|
55
|
+
render: () => (
|
|
56
|
+
<View style={{ gap: 10, flexDirection: 'row', alignItems: 'center' }}>
|
|
57
|
+
<Tag label="Extra Small" size="xs" />
|
|
58
|
+
<Tag label="Small Tag" size="sm" />
|
|
59
|
+
<Tag label="Medium Tag" size="md" />
|
|
60
|
+
<Tag label="Large Tag" size="lg" />
|
|
61
|
+
</View>
|
|
62
|
+
),
|
|
63
|
+
};
|