@xetwa/design-system 1.0.39 → 1.0.41
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 +1 -1
- package/src/components/Buttons/WordChipIcon/WordChipIcon.stories.tsx +68 -0
- package/src/components/Buttons/WordChipIcon/WordChipIcon.tsx +186 -0
- package/src/components/Buttons/WordChipIcon/index.ts +1 -0
- package/src/components/Cards/BaseCard/BaseCard.tsx +17 -17
- package/src/components/Cards/BoardingPassCard/BoardingPassCard.tsx +25 -22
- package/src/components/Cards/ModuleCard/ModuleCard.stories.tsx +20 -1
- package/src/components/Cards/ModuleCard/ModuleCard.tsx +37 -14
- package/src/components/Feedback/BottomSheet/BottomSheet.tsx +124 -0
- package/src/components/Forms/CalendarPicker/CalendarPicker.stories.tsx +31 -0
- package/src/components/Forms/CalendarPicker/CalendarPicker.tsx +220 -0
- package/src/components/Forms/DaySelector/DaySelector.tsx +28 -0
- package/src/components/Forms/TimeSlot/TimeSlot.tsx +29 -0
- package/src/components/Navigation/TabBar/TabBar.stories.tsx +80 -0
- package/src/components/Navigation/TabBar/TabBar.tsx +245 -0
- package/src/index.ts +34 -30
- package/src/styles/theme.ts +107 -32
- package/src/utils/iconMap.ts +9 -1
package/package.json
CHANGED
|
@@ -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 {
|
|
3
|
-
import
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
@@ -132,17 +132,17 @@ export const BoardingPassCard = ({
|
|
|
132
132
|
styles.container,
|
|
133
133
|
{
|
|
134
134
|
borderRadius: scale(20),
|
|
135
|
-
minHeight: scale(
|
|
135
|
+
minHeight: scale(240),
|
|
136
136
|
},
|
|
137
137
|
style
|
|
138
138
|
]}>
|
|
139
139
|
{/* Top Section */}
|
|
140
140
|
<View style={[
|
|
141
|
-
styles.topSection,
|
|
142
|
-
{
|
|
141
|
+
styles.topSection,
|
|
142
|
+
{
|
|
143
143
|
backgroundColor: currentHeaderBg,
|
|
144
|
-
paddingTop: scale(
|
|
145
|
-
paddingBottom: scale(
|
|
144
|
+
paddingTop: scale(24),
|
|
145
|
+
paddingBottom: scale(24),
|
|
146
146
|
paddingHorizontal: scale(18),
|
|
147
147
|
}
|
|
148
148
|
]}>
|
|
@@ -158,7 +158,7 @@ export const BoardingPassCard = ({
|
|
|
158
158
|
]} />
|
|
159
159
|
|
|
160
160
|
<View style={[styles.headerRow, { marginBottom: scale(16) }]}>
|
|
161
|
-
<Typography variant="
|
|
161
|
+
<Typography variant="labelHeavy" style={[styles.headerText]}>CARTÃO DE EMBARQUE</Typography>
|
|
162
162
|
<View style={styles.headerPlaneContainer}>
|
|
163
163
|
<Plane stroke={theme.colors.white} strokeWidth={2.2} size={scale(22)} />
|
|
164
164
|
</View>
|
|
@@ -166,8 +166,8 @@ export const BoardingPassCard = ({
|
|
|
166
166
|
|
|
167
167
|
<View style={styles.routeRow}>
|
|
168
168
|
<View style={styles.cityCol}>
|
|
169
|
-
<Typography variant="
|
|
170
|
-
<Typography variant="
|
|
169
|
+
<Typography variant="display2" style={[styles.cityCode]}>{originCode}</Typography>
|
|
170
|
+
<Typography variant="small" style={[styles.cityName]}>{originName}</Typography>
|
|
171
171
|
</View>
|
|
172
172
|
|
|
173
173
|
<View style={[styles.flightLineContainer, { height: scale(30), marginHorizontal: scale(12) }]}>
|
|
@@ -178,8 +178,8 @@ export const BoardingPassCard = ({
|
|
|
178
178
|
</View>
|
|
179
179
|
|
|
180
180
|
<View style={[styles.cityCol, { alignItems: 'flex-end' }]}>
|
|
181
|
-
<Typography variant="
|
|
182
|
-
<Typography variant="
|
|
181
|
+
<Typography variant="display2" style={[styles.cityCode]}>{destCode}</Typography>
|
|
182
|
+
<Typography variant="small" style={[styles.cityName]}>{destName}</Typography>
|
|
183
183
|
</View>
|
|
184
184
|
</View>
|
|
185
185
|
</View>
|
|
@@ -188,7 +188,7 @@ export const BoardingPassCard = ({
|
|
|
188
188
|
<View style={[
|
|
189
189
|
styles.bottomSection,
|
|
190
190
|
{
|
|
191
|
-
paddingVertical: scale(
|
|
191
|
+
paddingVertical: scale(20),
|
|
192
192
|
paddingHorizontal: scale(18),
|
|
193
193
|
}
|
|
194
194
|
]}>
|
|
@@ -205,8 +205,8 @@ export const BoardingPassCard = ({
|
|
|
205
205
|
{flagIconNode ? flagIconNode : <Flag countryCode={countryName} size={scale(40)} />}
|
|
206
206
|
</View>
|
|
207
207
|
<View style={styles.countryContent}>
|
|
208
|
-
<Typography variant="
|
|
209
|
-
<Typography variant="
|
|
208
|
+
<Typography variant="subtitle1" style={[styles.countryName]}>{countryName}</Typography>
|
|
209
|
+
<Typography variant="label" style={[styles.countrySubtitle]}>{countrySubtitle}</Typography>
|
|
210
210
|
</View>
|
|
211
211
|
</View>
|
|
212
212
|
|
|
@@ -220,12 +220,12 @@ export const BoardingPassCard = ({
|
|
|
220
220
|
|
|
221
221
|
<View style={[styles.footerRow, { gap: scale(14) }]}>
|
|
222
222
|
<View style={styles.footerCol}>
|
|
223
|
-
<Typography variant="
|
|
224
|
-
<Typography variant="
|
|
223
|
+
<Typography variant="labelHeavy" style={[styles.footerLabel]}>MÓDULOS</Typography>
|
|
224
|
+
<Typography variant="subtitle1" style={[styles.footerValue]}>{modulesCount}</Typography>
|
|
225
225
|
</View>
|
|
226
226
|
<View style={styles.footerCol}>
|
|
227
|
-
<Typography variant="
|
|
228
|
-
<Typography variant="
|
|
227
|
+
<Typography variant="labelHeavy" style={[styles.footerLabel]}>FOCO</Typography>
|
|
228
|
+
<Typography variant="subtitle1" style={[styles.footerValue]}>{focusText}</Typography>
|
|
229
229
|
</View>
|
|
230
230
|
</View>
|
|
231
231
|
</View>
|
|
@@ -241,6 +241,10 @@ const styles = StyleSheet.create({
|
|
|
241
241
|
minHeight: 230,
|
|
242
242
|
...theme.shadows.lg,
|
|
243
243
|
},
|
|
244
|
+
headerText: {
|
|
245
|
+
color: theme.colors.white,
|
|
246
|
+
opacity: 0.9,
|
|
247
|
+
},
|
|
244
248
|
topSection: {
|
|
245
249
|
paddingTop: 16,
|
|
246
250
|
paddingBottom: 16,
|
|
@@ -266,11 +270,6 @@ const styles = StyleSheet.create({
|
|
|
266
270
|
headerPlaneContainer: {
|
|
267
271
|
transform: [{ rotate: '0deg' }],
|
|
268
272
|
},
|
|
269
|
-
headerText: {
|
|
270
|
-
letterSpacing: 1.2,
|
|
271
|
-
textTransform: 'uppercase',
|
|
272
|
-
color: 'rgba(255, 255, 255, 0.75)',
|
|
273
|
-
},
|
|
274
273
|
routeRow: {
|
|
275
274
|
flexDirection: 'row',
|
|
276
275
|
alignItems: 'center',
|
|
@@ -345,6 +344,10 @@ const styles = StyleSheet.create({
|
|
|
345
344
|
countrySubtitle: {
|
|
346
345
|
color: '#9a8478',
|
|
347
346
|
},
|
|
347
|
+
footerLabel: {
|
|
348
|
+
color: theme.colors.brown[500],
|
|
349
|
+
marginBottom: 4,
|
|
350
|
+
},
|
|
348
351
|
divider: {
|
|
349
352
|
borderTopWidth: 1.5,
|
|
350
353
|
borderColor: '#f3e7da',
|
|
@@ -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' },
|
|
@@ -101,6 +101,18 @@ export const Completed: Story = {
|
|
|
101
101
|
},
|
|
102
102
|
};
|
|
103
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
|
+
|
|
104
116
|
export const Grid: Story = {
|
|
105
117
|
args: {
|
|
106
118
|
status: 'active',
|
|
@@ -153,6 +165,13 @@ export const Grid: Story = {
|
|
|
153
165
|
tagLabel="POR FAZER"
|
|
154
166
|
onExamPress={() => {}}
|
|
155
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"
|
|
174
|
+
/>
|
|
156
175
|
</View>
|
|
157
176
|
),
|
|
158
177
|
};
|
|
@@ -9,7 +9,7 @@ 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.
|
|
@@ -17,7 +17,7 @@ export type ModuleCardStatus = 'active' | 'locked' | 'completed' | 'dark' | 'act
|
|
|
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
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`: O cartão é estático. Apenas o ícone/botão superior direito de "Rever" (`onReviewPress`) tem animação individual.
|
|
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 {
|
|
@@ -51,6 +51,10 @@ export interface ModuleCardProps {
|
|
|
51
51
|
onExamPress?: () => void;
|
|
52
52
|
/** Função de clique no botão de Rever (modo completed/active/dark). */
|
|
53
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;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
export const ModuleCard = ({
|
|
@@ -70,6 +74,8 @@ export const ModuleCard = ({
|
|
|
70
74
|
onSchedulePress,
|
|
71
75
|
onExamPress,
|
|
72
76
|
onReviewPress,
|
|
77
|
+
onNextCountryPress,
|
|
78
|
+
nextCountryLabel = 'Seguir Viagem',
|
|
73
79
|
}: ModuleCardProps) => {
|
|
74
80
|
const { scale, scaleText, width } = useResponsive();
|
|
75
81
|
const isMobileScreen = width < 400;
|
|
@@ -78,6 +84,7 @@ export const ModuleCard = ({
|
|
|
78
84
|
const examScale = useRef(new Animated.Value(1)).current;
|
|
79
85
|
const reviewScale = useRef(new Animated.Value(1)).current;
|
|
80
86
|
const completedReviewScale = useRef(new Animated.Value(1)).current;
|
|
87
|
+
const nextCountryScale = useRef(new Animated.Value(1)).current;
|
|
81
88
|
const scaleAnim = useRef(new Animated.Value(1)).current;
|
|
82
89
|
|
|
83
90
|
const handlePressIn = () => {
|
|
@@ -139,6 +146,7 @@ export const ModuleCard = ({
|
|
|
139
146
|
};
|
|
140
147
|
case 'dark':
|
|
141
148
|
case 'active-dark':
|
|
149
|
+
case 'completed-dark':
|
|
142
150
|
return {
|
|
143
151
|
borderColor: theme.colors.brown[800],
|
|
144
152
|
borderWidth: 0,
|
|
@@ -155,6 +163,7 @@ export const ModuleCard = ({
|
|
|
155
163
|
return theme.colors.brown[400];
|
|
156
164
|
case 'dark':
|
|
157
165
|
case 'active-dark':
|
|
166
|
+
case 'completed-dark':
|
|
158
167
|
return theme.colors.white;
|
|
159
168
|
default:
|
|
160
169
|
return theme.colors.brown[800];
|
|
@@ -163,7 +172,7 @@ export const ModuleCard = ({
|
|
|
163
172
|
|
|
164
173
|
const containerStyle = getContainerStyle();
|
|
165
174
|
const titleStyleColor = getTextColor();
|
|
166
|
-
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]);
|
|
167
176
|
|
|
168
177
|
const activeShadow = status === 'active' ? {
|
|
169
178
|
shadowColor: theme.colors.primary,
|
|
@@ -184,7 +193,7 @@ export const ModuleCard = ({
|
|
|
184
193
|
/>
|
|
185
194
|
);
|
|
186
195
|
}
|
|
187
|
-
if (status === 'completed') {
|
|
196
|
+
if (status === 'completed' || status === 'completed-dark') {
|
|
188
197
|
const checkSize = isMobileScreen ? scale(16) : scale(22);
|
|
189
198
|
return (
|
|
190
199
|
<Badge
|
|
@@ -226,9 +235,8 @@ export const ModuleCard = ({
|
|
|
226
235
|
const cardPaddingV = size === 'xl' ? scale(20) : size === 'lg' ? scale(16) : isMobileScreen ? scale(10) : scale(14);
|
|
227
236
|
const cardPaddingH = size === 'xl' ? scale(20) : size === 'lg' ? scale(18) : isMobileScreen ? scale(12) : scale(15);
|
|
228
237
|
|
|
229
|
-
const titleVariant = size === 'xl' ? '
|
|
230
|
-
|
|
231
|
-
const subtitleVariant = size === 'xl' ? 'bodyLg' : size === 'lg' ? 'bodyMd' : 'bodySm';
|
|
238
|
+
const titleVariant = size === 'xl' ? 'h4' : size === 'lg' ? 'h6' : 'subtitle1';
|
|
239
|
+
const subtitleVariant = size === 'xl' ? 'bodySm' : size === 'lg' ? 'caption' : 'label';
|
|
232
240
|
|
|
233
241
|
|
|
234
242
|
|
|
@@ -294,9 +302,24 @@ export const ModuleCard = ({
|
|
|
294
302
|
)}
|
|
295
303
|
</View>
|
|
296
304
|
|
|
297
|
-
{(status === 'locked' || status === 'dark' || status === 'completed') && (
|
|
305
|
+
{(status === 'locked' || status === 'dark' || status === 'completed' || status === 'completed-dark') && (
|
|
298
306
|
<View style={[styles.rightContent, { marginLeft: colGap }]}>
|
|
299
|
-
{status === 'completed' && (
|
|
307
|
+
{status === 'completed-dark' && onNextCountryPress ? (
|
|
308
|
+
<Animated.View style={{ transform: [{ scale: nextCountryScale }] }}>
|
|
309
|
+
<Pressable
|
|
310
|
+
onPress={onNextCountryPress}
|
|
311
|
+
onPressIn={() => animateBtnIn(nextCountryScale)}
|
|
312
|
+
onPressOut={() => animateBtnOut(nextCountryScale)}
|
|
313
|
+
hitSlop={10}
|
|
314
|
+
>
|
|
315
|
+
<Tag
|
|
316
|
+
label={nextCountryLabel}
|
|
317
|
+
variant="success"
|
|
318
|
+
size={isMobileScreen ? 'xs' : 'sm'}
|
|
319
|
+
/>
|
|
320
|
+
</Pressable>
|
|
321
|
+
</Animated.View>
|
|
322
|
+
) : (status === 'completed' || status === 'completed-dark') ? (
|
|
300
323
|
<Animated.View style={{ transform: [{ scale: completedReviewScale }] }}>
|
|
301
324
|
<Pressable
|
|
302
325
|
style={styles.reviewIconButton}
|
|
@@ -306,10 +329,10 @@ export const ModuleCard = ({
|
|
|
306
329
|
hitSlop={10}
|
|
307
330
|
>
|
|
308
331
|
<RotateCcw stroke={theme.colors.primary} strokeWidth={2.5} size={isMobileScreen ? scale(20) : scale(24)} />
|
|
309
|
-
<Typography variant="
|
|
332
|
+
<Typography variant="smallHeavy" style={[styles.reviewIconText, { fontSize: scaleText(9) }]}>REVER</Typography>
|
|
310
333
|
</Pressable>
|
|
311
334
|
</Animated.View>
|
|
312
|
-
)}
|
|
335
|
+
) : null}
|
|
313
336
|
{(status === 'locked' || status === 'dark') && (
|
|
314
337
|
<Lock
|
|
315
338
|
stroke={status === 'dark' ? 'rgba(255,255,255,0.4)' : '#c3b1a4'}
|
|
@@ -344,7 +367,7 @@ export const ModuleCard = ({
|
|
|
344
367
|
style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: theme.colors.primary, paddingHorizontal: scale(14), paddingVertical: scale(6), borderRadius: 100 }}
|
|
345
368
|
>
|
|
346
369
|
<CalendarPlus stroke={theme.colors.white} strokeWidth={2.5} size={scale(14)} />
|
|
347
|
-
<Typography variant="
|
|
370
|
+
<Typography variant="bodySmHeavy" style={{ color: theme.colors.white, marginLeft: scale(6) }}>Agendar Aula</Typography>
|
|
348
371
|
</Pressable>
|
|
349
372
|
</Animated.View>
|
|
350
373
|
)}
|
|
@@ -359,7 +382,7 @@ export const ModuleCard = ({
|
|
|
359
382
|
style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: theme.colors.primary, paddingHorizontal: scale(14), paddingVertical: scale(6), borderRadius: 100 }}
|
|
360
383
|
>
|
|
361
384
|
<PenTool stroke={theme.colors.white} strokeWidth={2.5} size={scale(14)} />
|
|
362
|
-
<Typography variant="
|
|
385
|
+
<Typography variant="bodySmHeavy" style={{ color: theme.colors.white, marginLeft: scale(6) }}>Fazer Exame</Typography>
|
|
363
386
|
</Pressable>
|
|
364
387
|
</Animated.View>
|
|
365
388
|
)}
|
|
@@ -375,7 +398,7 @@ export const ModuleCard = ({
|
|
|
375
398
|
hitSlop={8}
|
|
376
399
|
>
|
|
377
400
|
<RotateCcw stroke={theme.colors.primary} strokeWidth={2.5} size={scale(16)} />
|
|
378
|
-
<Typography variant="
|
|
401
|
+
<Typography variant="smallHeavy" style={[styles.reviewIconText, { color: theme.colors.primary, marginLeft: scale(4), marginTop: 0 }]}>REVER</Typography>
|
|
379
402
|
</Pressable>
|
|
380
403
|
</Animated.View>
|
|
381
404
|
)}
|