@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,133 @@
|
|
|
1
|
+
|
|
2
|
+
import { View, StyleSheet, ScrollView, Pressable, Platform } from 'react-native';
|
|
3
|
+
import type { ViewStyle } from 'react-native';
|
|
4
|
+
import { theme } from '../../../styles/theme';
|
|
5
|
+
import { useResponsive } from '../../../hooks/useResponsive';
|
|
6
|
+
import { Typography } from '../../Typography/Typography';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Interface para os dados de um dia disponível no DaySelector.
|
|
10
|
+
*/
|
|
11
|
+
export interface DayOption {
|
|
12
|
+
/** Identificador único do dia (ex: "2024-03-23") */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Objeto Date correspondente ao dia */
|
|
15
|
+
dateObj: Date;
|
|
16
|
+
/** Etiqueta do dia da semana (ex: "SEG") */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Número do dia do mês (ex: "23") */
|
|
19
|
+
number: string;
|
|
20
|
+
/** Estado de disponibilidade do dia */
|
|
21
|
+
status: 'available' | 'unavailable';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Componente DaySelector
|
|
26
|
+
*
|
|
27
|
+
* Uma lista com scroll horizontal de dias, usada tipicamente para a marcação de aulas.
|
|
28
|
+
* O dia selecionado é destacado com as cores primárias e sombras suaves.
|
|
29
|
+
*/
|
|
30
|
+
export interface DaySelectorProps {
|
|
31
|
+
/** Array de dias a renderizar no seletor */
|
|
32
|
+
days: DayOption[];
|
|
33
|
+
/** ID do dia atualmente selecionado */
|
|
34
|
+
selectedId?: string;
|
|
35
|
+
/** Callback chamado quando o utilizador clica num dia disponível */
|
|
36
|
+
onSelectDay: (id: string, date: Date) => void;
|
|
37
|
+
/** Estilo para o container exterior */
|
|
38
|
+
style?: ViewStyle | ViewStyle[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const DaySelector = ({
|
|
42
|
+
days,
|
|
43
|
+
selectedId,
|
|
44
|
+
onSelectDay,
|
|
45
|
+
style,
|
|
46
|
+
}: DaySelectorProps) => {
|
|
47
|
+
const { scale, scaleText } = useResponsive();
|
|
48
|
+
|
|
49
|
+
// Transição suave para o Web
|
|
50
|
+
const transitionStyle = Platform.OS === 'web' ? { transition: 'all 0.15s ease-in-out' } as any : {};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<View style={style}>
|
|
54
|
+
<ScrollView
|
|
55
|
+
horizontal
|
|
56
|
+
showsHorizontalScrollIndicator={false}
|
|
57
|
+
contentContainerStyle={[styles.container, { gap: scale(8), paddingBottom: scale(6), paddingTop: scale(2) }]}
|
|
58
|
+
>
|
|
59
|
+
{days.map((day) => {
|
|
60
|
+
const isSelected = day.id === selectedId;
|
|
61
|
+
const isUnavailable = day.status === 'unavailable';
|
|
62
|
+
|
|
63
|
+
let bgColor = '#fff';
|
|
64
|
+
let borderColor = '#efe2d6';
|
|
65
|
+
let labelColor = '#9a8478';
|
|
66
|
+
let numColor = '#51332d';
|
|
67
|
+
|
|
68
|
+
if (isSelected) {
|
|
69
|
+
bgColor = theme.colors.primary; // #f2974b
|
|
70
|
+
borderColor = theme.colors.primary;
|
|
71
|
+
labelColor = '#fde9d5';
|
|
72
|
+
numColor = '#fff';
|
|
73
|
+
} else if (isUnavailable) {
|
|
74
|
+
labelColor = '#b9a392';
|
|
75
|
+
numColor = '#b9a392';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Pressable
|
|
80
|
+
key={day.id}
|
|
81
|
+
disabled={isUnavailable}
|
|
82
|
+
onPress={() => onSelectDay(day.id, day.dateObj)}
|
|
83
|
+
style={({ pressed }) => [
|
|
84
|
+
styles.item,
|
|
85
|
+
transitionStyle,
|
|
86
|
+
{
|
|
87
|
+
minWidth: scale(48),
|
|
88
|
+
paddingVertical: scale(8),
|
|
89
|
+
borderRadius: scale(13),
|
|
90
|
+
backgroundColor: bgColor,
|
|
91
|
+
borderWidth: scale(1.5),
|
|
92
|
+
borderColor,
|
|
93
|
+
},
|
|
94
|
+
isSelected && {
|
|
95
|
+
shadowColor: '#cf7a30',
|
|
96
|
+
shadowOffset: { width: 0, height: pressed ? 0 : 3 },
|
|
97
|
+
shadowOpacity: 1,
|
|
98
|
+
shadowRadius: 0,
|
|
99
|
+
elevation: 0,
|
|
100
|
+
transform: [{ translateY: pressed ? 3 : 0 }]
|
|
101
|
+
}
|
|
102
|
+
]}
|
|
103
|
+
>
|
|
104
|
+
<Typography style={[styles.label, { fontSize: scaleText(11), color: labelColor }, transitionStyle]}>
|
|
105
|
+
{day.label}
|
|
106
|
+
</Typography>
|
|
107
|
+
<Typography style={[styles.number, { fontSize: scaleText(17), color: numColor }, transitionStyle]}>
|
|
108
|
+
{day.number}
|
|
109
|
+
</Typography>
|
|
110
|
+
</Pressable>
|
|
111
|
+
);
|
|
112
|
+
})}
|
|
113
|
+
</ScrollView>
|
|
114
|
+
</View>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const styles = StyleSheet.create({
|
|
119
|
+
container: {
|
|
120
|
+
flexDirection: 'row',
|
|
121
|
+
},
|
|
122
|
+
item: {
|
|
123
|
+
alignItems: 'center',
|
|
124
|
+
justifyContent: 'center',
|
|
125
|
+
},
|
|
126
|
+
label: {
|
|
127
|
+
|
|
128
|
+
textTransform: 'uppercase',
|
|
129
|
+
},
|
|
130
|
+
number: {
|
|
131
|
+
|
|
132
|
+
}
|
|
133
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
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 { Radio } from './Radio';
|
|
6
|
+
import { theme } from '../../../styles/theme';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Components/Forms/Radio',
|
|
10
|
+
component: Radio,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'padded',
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component: 'Um botão de **Radio** é usado quando o utilizador tem de selecionar **apenas uma** opção de entre várias opções disponíveis.\n\n### Como usar em grupo\nComo não existe um componente `RadioGroup` isolado, o padrão correto em React Native é teres um estado que guarda o "valor selecionado" e usares vários `<Radio />` lado a lado. \n\nExemplo:\n```tsx\nconst [selected, setSelected] = useState("valor_a");\n\nreturn (\n <View>\n <Radio \n label="Opção A"\n checked={selected === "valor_a"}\n onChange={() => setSelected("valor_a")}\n />\n <Radio \n label="Opção B"\n checked={selected === "valor_b"}\n onChange={() => setSelected("valor_b")}\n />\n </View>\n)\n```\nIsto garante máxima flexibilidade para posicionares os Radios na horizontal, vertical, ou misturados com outro layout.',
|
|
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 InteractiveRadioGroup = () => {
|
|
33
|
+
const [selected, setSelected] = useState('diariamente');
|
|
34
|
+
return (
|
|
35
|
+
<View style={{ gap: 12 }}>
|
|
36
|
+
<Radio
|
|
37
|
+
checked={selected === 'diariamente'}
|
|
38
|
+
onChange={() => setSelected('diariamente')}
|
|
39
|
+
label="Diariamente"
|
|
40
|
+
/>
|
|
41
|
+
<Radio
|
|
42
|
+
checked={selected === '3x_semana'}
|
|
43
|
+
onChange={() => setSelected('3x_semana')}
|
|
44
|
+
label="3x por semana"
|
|
45
|
+
/>
|
|
46
|
+
</View>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const Default: Story = {
|
|
51
|
+
render: () => <InteractiveRadioGroup />,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Disabled: Story = {
|
|
55
|
+
render: () => (
|
|
56
|
+
<View style={{ gap: 12 }}>
|
|
57
|
+
<Radio label="Marcado inativo" checked={true} onChange={() => { }} disabled={true} />
|
|
58
|
+
<Radio label="Desmarcado inativo" checked={false} onChange={() => { }} disabled={true} />
|
|
59
|
+
</View>
|
|
60
|
+
),
|
|
61
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { View, StyleSheet, Pressable } from 'react-native';
|
|
2
|
+
import type { ViewStyle } from 'react-native';
|
|
3
|
+
import { theme } from '../../../styles/theme';
|
|
4
|
+
import { useResponsive } from '../../../hooks/useResponsive';
|
|
5
|
+
import { Typography } from '../../Typography/Typography';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Componente individual de Radio.
|
|
9
|
+
* Deve ser usado para opções mutuamente exclusivas (onde apenas uma pode ser selecionada).
|
|
10
|
+
*/
|
|
11
|
+
export interface RadioProps {
|
|
12
|
+
/** O texto a ser exibido ao lado do botão de rádio. A linha inteira será clicável. */
|
|
13
|
+
label?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Estado atual do rádio.
|
|
16
|
+
* Num grupo, apenas um componente deve ter `checked={true}` de cada vez.
|
|
17
|
+
*/
|
|
18
|
+
checked: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Função chamada quando o botão é pressionado.
|
|
21
|
+
* Geralmente usada para atualizar o estado que guarda o valor selecionado no grupo.
|
|
22
|
+
*/
|
|
23
|
+
onChange: (checked: boolean) => void;
|
|
24
|
+
/** Desativa a interação e diminui a opacidade do rádio. */
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
/** Estilos customizados opcionais aplicados ao container exterior. */
|
|
27
|
+
style?: ViewStyle | ViewStyle[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const Radio = ({
|
|
31
|
+
label,
|
|
32
|
+
checked,
|
|
33
|
+
onChange,
|
|
34
|
+
disabled = false,
|
|
35
|
+
style,
|
|
36
|
+
}: RadioProps) => {
|
|
37
|
+
const { scale, scaleText } = useResponsive();
|
|
38
|
+
|
|
39
|
+
const handlePress = () => {
|
|
40
|
+
if (!disabled) {
|
|
41
|
+
onChange(!checked);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Cores dinâmicas com base no estado
|
|
46
|
+
let borderColor = theme.colors.brown[100]; // #efe2d6
|
|
47
|
+
let dotColor = theme.colors.primary; // #f2974b
|
|
48
|
+
let borderWidth = scale(2);
|
|
49
|
+
|
|
50
|
+
if (disabled) {
|
|
51
|
+
borderColor = theme.colors.brown[100]; // #efe2d6
|
|
52
|
+
if (checked) {
|
|
53
|
+
dotColor = theme.colors.brown[400]; // #b9a392 (cor muted para disabled)
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
if (checked) {
|
|
57
|
+
borderColor = theme.colors.primary; // #f2974b
|
|
58
|
+
borderWidth = scale(2.5);
|
|
59
|
+
} else {
|
|
60
|
+
borderColor = theme.colors.brown[100];
|
|
61
|
+
borderWidth = scale(2);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Pressable
|
|
67
|
+
onPress={handlePress}
|
|
68
|
+
disabled={disabled}
|
|
69
|
+
style={() => [
|
|
70
|
+
styles.container,
|
|
71
|
+
{
|
|
72
|
+
paddingVertical: scale(4),
|
|
73
|
+
gap: scale(12),
|
|
74
|
+
opacity: disabled && !checked ? 0.5 : 1, // Opacity .5 para unchecked disabled
|
|
75
|
+
},
|
|
76
|
+
style
|
|
77
|
+
]}
|
|
78
|
+
accessibilityRole="radio"
|
|
79
|
+
accessibilityState={{ checked, disabled }}
|
|
80
|
+
>
|
|
81
|
+
<View
|
|
82
|
+
style={[
|
|
83
|
+
styles.circle,
|
|
84
|
+
{
|
|
85
|
+
width: scale(22),
|
|
86
|
+
height: scale(22),
|
|
87
|
+
borderRadius: scale(11),
|
|
88
|
+
borderWidth,
|
|
89
|
+
borderColor,
|
|
90
|
+
backgroundColor: theme.colors.white,
|
|
91
|
+
}
|
|
92
|
+
]}
|
|
93
|
+
>
|
|
94
|
+
{checked && (
|
|
95
|
+
<View
|
|
96
|
+
style={[
|
|
97
|
+
styles.dot,
|
|
98
|
+
{
|
|
99
|
+
width: scale(10),
|
|
100
|
+
height: scale(10),
|
|
101
|
+
borderRadius: scale(5),
|
|
102
|
+
backgroundColor: dotColor,
|
|
103
|
+
}
|
|
104
|
+
]}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
</View>
|
|
108
|
+
|
|
109
|
+
{label && (
|
|
110
|
+
<Typography
|
|
111
|
+
style={[
|
|
112
|
+
styles.label,
|
|
113
|
+
{
|
|
114
|
+
fontSize: scaleText(15),
|
|
115
|
+
color: theme.colors.brown[800], // #51332d
|
|
116
|
+
}
|
|
117
|
+
]}
|
|
118
|
+
>
|
|
119
|
+
{label}
|
|
120
|
+
</Typography>
|
|
121
|
+
)}
|
|
122
|
+
</Pressable>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const styles = StyleSheet.create({
|
|
127
|
+
container: {
|
|
128
|
+
flexDirection: 'row',
|
|
129
|
+
alignItems: 'center',
|
|
130
|
+
},
|
|
131
|
+
circle: {
|
|
132
|
+
justifyContent: 'center',
|
|
133
|
+
alignItems: 'center',
|
|
134
|
+
},
|
|
135
|
+
dot: {
|
|
136
|
+
// Definido dinamicamente no componente
|
|
137
|
+
},
|
|
138
|
+
label: {
|
|
139
|
+
|
|
140
|
+
},
|
|
141
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Radio';
|
|
@@ -0,0 +1,67 @@
|
|
|
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 { SegmentedToggle } from './SegmentedToggle';
|
|
6
|
+
import { theme } from '../../../styles/theme';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Components/Forms/SegmentedToggle',
|
|
10
|
+
component: SegmentedToggle,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'padded',
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component: 'O **SegmentedToggle** é uma alternativa muito elegante ao tradicional Dropdown (Select) ou ao grupo de Radios.\n\n### Exemplo de Uso\n```tsx\nconst [plan, setPlan] = useState("monthly");\n\nconst options = [\n { label: "Mensal", value: "monthly" },\n { label: "Anual (-20%)", value: "annual" }\n];\n\nreturn (\n <SegmentedToggle \n options={options}\n selectedValue={plan}\n onValueChange={setPlan}\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 InteractiveToggle = ({ options, initialValue }: { options: any[], initialValue: string }) => {
|
|
33
|
+
const [value, setValue] = useState(initialValue);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<SegmentedToggle
|
|
37
|
+
options={options}
|
|
38
|
+
selectedValue={value}
|
|
39
|
+
onValueChange={setValue}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const TwoOptions: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<InteractiveToggle
|
|
47
|
+
options={[
|
|
48
|
+
{ label: 'Mensal', value: 'mensal' },
|
|
49
|
+
{ label: 'Anual', value: 'anual', highlightText: '-20%' },
|
|
50
|
+
]}
|
|
51
|
+
initialValue="mensal"
|
|
52
|
+
/>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const ThreeOptions: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<InteractiveToggle
|
|
59
|
+
options={[
|
|
60
|
+
{ label: 'Fácil', value: 'facil' },
|
|
61
|
+
{ label: 'Médio', value: 'medio' },
|
|
62
|
+
{ label: 'Difícil', value: 'dificil' },
|
|
63
|
+
]}
|
|
64
|
+
initialValue="medio"
|
|
65
|
+
/>
|
|
66
|
+
),
|
|
67
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { View, StyleSheet, Pressable, Animated, Easing } from 'react-native';
|
|
3
|
+
import type { ViewStyle, LayoutChangeEvent } from 'react-native';
|
|
4
|
+
import { theme } from '../../../styles/theme';
|
|
5
|
+
import { useResponsive } from '../../../hooks/useResponsive';
|
|
6
|
+
import { Typography } from '../../Typography/Typography';
|
|
7
|
+
|
|
8
|
+
export interface SegmentedToggleOption {
|
|
9
|
+
label: string;
|
|
10
|
+
value: string;
|
|
11
|
+
highlightText?: string;
|
|
12
|
+
highlightColor?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Componente SegmentedToggle para alternar entre 2 ou 3 opções curtas (ex: Mensal vs Anual).
|
|
17
|
+
* Apresenta uma barra deslumbante que desliza debaixo da opção selecionada.
|
|
18
|
+
*/
|
|
19
|
+
export interface SegmentedToggleProps {
|
|
20
|
+
/** Array de opções a mostrar na barra. Recomenda-se entre 2 a 3 opções curtas. */
|
|
21
|
+
options: SegmentedToggleOption[];
|
|
22
|
+
/** O valor da opção atualmente selecionada. */
|
|
23
|
+
selectedValue: string;
|
|
24
|
+
/** Callback executado sempre que o utilizador escolhe uma opção diferente. */
|
|
25
|
+
onValueChange: (value: string) => void;
|
|
26
|
+
/** Estilos adicionais para a barra (ex: `maxWidth`). */
|
|
27
|
+
style?: ViewStyle | ViewStyle[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const SegmentedToggle = ({
|
|
31
|
+
options,
|
|
32
|
+
selectedValue,
|
|
33
|
+
onValueChange,
|
|
34
|
+
style,
|
|
35
|
+
}: SegmentedToggleProps) => {
|
|
36
|
+
const { scaleText, scale } = useResponsive();
|
|
37
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
38
|
+
|
|
39
|
+
const padding = scale(3);
|
|
40
|
+
const gap = scale(3);
|
|
41
|
+
|
|
42
|
+
// Encontrar índice atual
|
|
43
|
+
const selectedIndex = options.findIndex((opt) => opt.value === selectedValue) ?? 0;
|
|
44
|
+
|
|
45
|
+
const animValue = useRef(new Animated.Value(selectedIndex)).current;
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
Animated.timing(animValue, {
|
|
49
|
+
toValue: selectedIndex,
|
|
50
|
+
duration: 200,
|
|
51
|
+
easing: Easing.bezier(0.25, 0.1, 0.25, 1), // ease-smooth
|
|
52
|
+
useNativeDriver: false,
|
|
53
|
+
}).start();
|
|
54
|
+
}, [selectedIndex, animValue]);
|
|
55
|
+
|
|
56
|
+
const handleLayout = (e: LayoutChangeEvent) => {
|
|
57
|
+
setContainerWidth(e.nativeEvent.layout.width);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Se o containerWidth for 0 (ainda não medido), não mostramos o fundo deslizante
|
|
61
|
+
const availableWidth = containerWidth - (padding * 2);
|
|
62
|
+
const totalGap = (options.length - 1) * gap;
|
|
63
|
+
const itemWidth = (availableWidth - totalGap) / options.length;
|
|
64
|
+
|
|
65
|
+
const translateX = animValue.interpolate({
|
|
66
|
+
inputRange: options.map((_, i) => i),
|
|
67
|
+
outputRange: options.map((_, i) => i * (itemWidth + gap)),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<View
|
|
72
|
+
style={[styles.container, { padding, gap, borderRadius: scale(12), maxWidth: 400, width: '100%' }, style]}
|
|
73
|
+
onLayout={handleLayout}
|
|
74
|
+
accessibilityRole="radiogroup"
|
|
75
|
+
>
|
|
76
|
+
{containerWidth > 0 && (
|
|
77
|
+
<Animated.View
|
|
78
|
+
style={[
|
|
79
|
+
styles.activeBackground,
|
|
80
|
+
{
|
|
81
|
+
width: itemWidth,
|
|
82
|
+
borderRadius: scale(10),
|
|
83
|
+
transform: [{ translateX }],
|
|
84
|
+
top: padding,
|
|
85
|
+
left: padding,
|
|
86
|
+
bottom: padding,
|
|
87
|
+
}
|
|
88
|
+
]}
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{options.map((option, index) => {
|
|
93
|
+
const isActive = selectedIndex === index;
|
|
94
|
+
return (
|
|
95
|
+
<Pressable
|
|
96
|
+
key={option.value}
|
|
97
|
+
style={[styles.option]}
|
|
98
|
+
onPress={() => onValueChange(option.value)}
|
|
99
|
+
accessibilityRole="radio"
|
|
100
|
+
accessibilityState={{ checked: isActive }}
|
|
101
|
+
>
|
|
102
|
+
<Typography
|
|
103
|
+
style={[
|
|
104
|
+
styles.label,
|
|
105
|
+
{
|
|
106
|
+
fontSize: scaleText(13),
|
|
107
|
+
color: isActive ? '#51332d' : '#9a8478',
|
|
108
|
+
}
|
|
109
|
+
]}
|
|
110
|
+
>
|
|
111
|
+
{option.label}
|
|
112
|
+
{option.highlightText && (
|
|
113
|
+
<Typography style={{ color: option.highlightColor || theme.colors.success, fontWeight: '900' }}>
|
|
114
|
+
{' '}{option.highlightText}
|
|
115
|
+
</Typography>
|
|
116
|
+
)}
|
|
117
|
+
</Typography>
|
|
118
|
+
</Pressable>
|
|
119
|
+
);
|
|
120
|
+
})}
|
|
121
|
+
</View>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const styles = StyleSheet.create({
|
|
126
|
+
container: {
|
|
127
|
+
flexDirection: 'row',
|
|
128
|
+
backgroundColor: theme.colors.brown[100], // #efe2d6
|
|
129
|
+
position: 'relative',
|
|
130
|
+
},
|
|
131
|
+
activeBackground: {
|
|
132
|
+
position: 'absolute',
|
|
133
|
+
backgroundColor: theme.colors.white,
|
|
134
|
+
shadowColor: 'rgba(81, 51, 45, 1)',
|
|
135
|
+
shadowOffset: { width: 0, height: 2 },
|
|
136
|
+
shadowOpacity: 0.1,
|
|
137
|
+
shadowRadius: 4,
|
|
138
|
+
elevation: 2,
|
|
139
|
+
},
|
|
140
|
+
option: {
|
|
141
|
+
flex: 1,
|
|
142
|
+
justifyContent: 'center',
|
|
143
|
+
alignItems: 'center',
|
|
144
|
+
paddingVertical: 8,
|
|
145
|
+
zIndex: 1, // Para garantir que o texto fica por cima do activeBackground
|
|
146
|
+
},
|
|
147
|
+
label: {
|
|
148
|
+
|
|
149
|
+
},
|
|
150
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SegmentedToggle';
|
|
@@ -0,0 +1,59 @@
|
|
|
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 { Select } from './Select';
|
|
6
|
+
import { theme } from '../../../styles/theme';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Components/Forms/Select',
|
|
10
|
+
component: Select,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'padded',
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component: 'Um **Select** (Dropdown) permite que os utilizadores escolham um valor de uma lista através de um Bottom Sheet nativo.\n\n### Exemplo de Uso\n```tsx\nconst [language, setLanguage] = useState("");\n\nconst languages = [\n { label: "Português", value: "pt" },\n { label: "Inglês", value: "en" }\n];\n\nreturn (\n <Select \n options={languages}\n value={language}\n onChange={setLanguage}\n placeholder="Escolha um idioma..."\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 LANGUAGES = [
|
|
33
|
+
{ label: 'Português', value: 'pt' },
|
|
34
|
+
{ label: 'Inglês', value: 'en' },
|
|
35
|
+
{ label: 'Francês', value: 'fr' },
|
|
36
|
+
{ label: 'Espanhol', value: 'es' },
|
|
37
|
+
{ label: 'Alemão', value: 'de' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const InteractiveSelect = ({ disabled = false }: { disabled?: boolean }) => {
|
|
41
|
+
const [value, setValue] = useState('');
|
|
42
|
+
return (
|
|
43
|
+
<Select
|
|
44
|
+
options={LANGUAGES}
|
|
45
|
+
value={value}
|
|
46
|
+
onChange={setValue}
|
|
47
|
+
disabled={disabled}
|
|
48
|
+
placeholder="Escolhe um idioma"
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Default: Story = {
|
|
54
|
+
render: () => <InteractiveSelect />,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const Disabled: Story = {
|
|
58
|
+
render: () => <InteractiveSelect disabled={true} />,
|
|
59
|
+
};
|