@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.
Files changed (168) hide show
  1. package/.github/workflows/publish.yml +30 -0
  2. package/.storybook/main.ts +17 -0
  3. package/.storybook/preview.tsx +65 -0
  4. package/eslint.config.js +22 -0
  5. package/index.html +13 -0
  6. package/package.json +55 -0
  7. package/public/favicon.svg +1 -0
  8. package/public/icons.svg +24 -0
  9. package/src/App.css +184 -0
  10. package/src/App.tsx +122 -0
  11. package/src/BordasSombras.mdx +343 -0
  12. package/src/Colors.mdx +393 -0
  13. package/src/GrelhaLayout.mdx +375 -0
  14. package/src/Icons.mdx +42 -0
  15. package/src/MovimentoBreakpoints.mdx +379 -0
  16. package/src/Spacing.mdx +231 -0
  17. package/src/Typography.mdx +321 -0
  18. package/src/assets/Mascots/vava/estaticos/vava-acenar.svg +3 -0
  19. package/src/assets/Mascots/vava/estaticos/vava-acertou.svg +3 -0
  20. package/src/assets/Mascots/vava/estaticos/vava-celebrar.svg +3 -0
  21. package/src/assets/Mascots/vava/estaticos/vava-com-autocolantes.svg +3 -0
  22. package/src/assets/Mascots/vava/estaticos/vava-dormir.svg +3 -0
  23. package/src/assets/Mascots/vava/estaticos/vava-errou.svg +3 -0
  24. package/src/assets/Mascots/vava/estaticos/vava-feliz.svg +3 -0
  25. package/src/assets/Mascots/vava/estaticos/vava-neutra.svg +3 -0
  26. package/src/assets/Mascots/vava/estaticos/vava-original.svg +3 -0
  27. package/src/assets/Mascots/vava/estaticos/vava-pensar.svg +3 -0
  28. package/src/assets/Mascots/vava/estaticos/vava-surpresa.svg +3 -0
  29. package/src/assets/Mascots/yaya/avatar/yaya-avatar-falar.svg +7 -0
  30. package/src/assets/Mascots/yaya/avatar/yaya-avatar-feliz.svg +7 -0
  31. package/src/assets/Mascots/yaya/avatar/yaya-avatar-transparente.svg +6 -0
  32. package/src/assets/Mascots/yaya/estaticos/yaya-acertou.svg +13 -0
  33. package/src/assets/Mascots/yaya/estaticos/yaya-celebrar.svg +13 -0
  34. package/src/assets/Mascots/yaya/estaticos/yaya-dormir.svg +13 -0
  35. package/src/assets/Mascots/yaya/estaticos/yaya-errou.svg +13 -0
  36. package/src/assets/Mascots/yaya/estaticos/yaya-falar.svg +13 -0
  37. package/src/assets/Mascots/yaya/estaticos/yaya-feliz.svg +13 -0
  38. package/src/assets/Mascots/yaya/estaticos/yaya-neutra.svg +13 -0
  39. package/src/assets/Mascots/yaya/estaticos/yaya-pensar.svg +13 -0
  40. package/src/assets/Mascots/yaya/estaticos/yaya-surpresa.svg +13 -0
  41. package/src/assets/hero.png +0 -0
  42. package/src/assets/react.svg +1 -0
  43. package/src/assets/vite.svg +1 -0
  44. package/src/components/Buttons/BackButton/BackButton.stories.tsx +86 -0
  45. package/src/components/Buttons/BackButton/BackButton.tsx +107 -0
  46. package/src/components/Buttons/FabButton/FabButton.stories.tsx +74 -0
  47. package/src/components/Buttons/FabButton/FabButton.tsx +161 -0
  48. package/src/components/Buttons/IconButton/IconButton.stories.tsx +99 -0
  49. package/src/components/Buttons/IconButton/IconButton.tsx +189 -0
  50. package/src/components/Buttons/JourneyButton/JourneyButton.stories.tsx +80 -0
  51. package/src/components/Buttons/JourneyButton/JourneyButton.tsx +194 -0
  52. package/src/components/Buttons/MainButton/MainButton.stories.tsx +144 -0
  53. package/src/components/Buttons/MainButton/MainButton.tsx +258 -0
  54. package/src/components/Buttons/WordChip/WordChip.stories.tsx +84 -0
  55. package/src/components/Buttons/WordChip/WordChip.tsx +157 -0
  56. package/src/components/Buttons/WordChip/index.ts +1 -0
  57. package/src/components/Cards/AnswerOptionCard/AnswerOptionCard.stories.tsx +181 -0
  58. package/src/components/Cards/AnswerOptionCard/AnswerOptionCard.tsx +201 -0
  59. package/src/components/Cards/BaseCard/BaseCard.stories.tsx +148 -0
  60. package/src/components/Cards/BaseCard/BaseCard.tsx +181 -0
  61. package/src/components/Cards/BoardingPassCard/BoardingPassCard.stories.tsx +205 -0
  62. package/src/components/Cards/BoardingPassCard/BoardingPassCard.tsx +384 -0
  63. package/src/components/Cards/BoardingPassCard/FlightIcon.tsx +21 -0
  64. package/src/components/Cards/BoardingPassCard/FlightIcon.web.tsx +18 -0
  65. package/src/components/Cards/CountryCard/CountryCard.stories.tsx +97 -0
  66. package/src/components/Cards/CountryCard/CountryCard.tsx +185 -0
  67. package/src/components/Cards/LevelProgressCard/LevelProgressCard.stories.tsx +61 -0
  68. package/src/components/Cards/LevelProgressCard/LevelProgressCard.tsx +143 -0
  69. package/src/components/Cards/ModuleCard/ModuleCard.stories.tsx +108 -0
  70. package/src/components/Cards/ModuleCard/ModuleCard.tsx +304 -0
  71. package/src/components/Cards/ModuleCompletionCard/ModuleCompletionCard.stories.tsx +53 -0
  72. package/src/components/Cards/ModuleCompletionCard/ModuleCompletionCard.tsx +167 -0
  73. package/src/components/Cards/Passport/PassportCover.tsx +114 -0
  74. package/src/components/Cards/Passport/PassportIDPage.tsx +217 -0
  75. package/src/components/Cards/Passport/PassportStampPage.tsx +253 -0
  76. package/src/components/Cards/Passport/PassportViewer.stories.tsx +95 -0
  77. package/src/components/Cards/Passport/PassportViewer.tsx +328 -0
  78. package/src/components/Cards/PlanCard/PlanCard.stories.tsx +159 -0
  79. package/src/components/Cards/PlanCard/PlanCard.tsx +317 -0
  80. package/src/components/Cards/PricingCard/PricingCard.stories.tsx +83 -0
  81. package/src/components/Cards/PricingCard/PricingCard.tsx +144 -0
  82. package/src/components/Cards/SelectionCard/SelectionCard.stories.tsx +77 -0
  83. package/src/components/Cards/SelectionCard/SelectionCard.tsx +111 -0
  84. package/src/components/Cards/TeacherCard/TeacherCard.stories.tsx +126 -0
  85. package/src/components/Cards/TeacherCard/TeacherCard.tsx +236 -0
  86. package/src/components/Charts/WeeklyProgressChart/WeeklyProgressChart.stories.tsx +89 -0
  87. package/src/components/Charts/WeeklyProgressChart/WeeklyProgressChart.tsx +179 -0
  88. package/src/components/Charts/WeeklyProgressChart/index.ts +1 -0
  89. package/src/components/DataDisplay/Avatar/Avatar.stories.tsx +85 -0
  90. package/src/components/DataDisplay/Avatar/Avatar.tsx +118 -0
  91. package/src/components/DataDisplay/Avatar/index.ts +1 -0
  92. package/src/components/DataDisplay/Badge/Badge.stories.tsx +62 -0
  93. package/src/components/DataDisplay/Badge/Badge.tsx +146 -0
  94. package/src/components/DataDisplay/Flag/Flag.stories.tsx +98 -0
  95. package/src/components/DataDisplay/Flag/Flag.tsx +347 -0
  96. package/src/components/DataDisplay/Flag/Flag.web.tsx +334 -0
  97. package/src/components/DataDisplay/InstructionBubble/InstructionBubble.stories.tsx +80 -0
  98. package/src/components/DataDisplay/InstructionBubble/InstructionBubble.tsx +86 -0
  99. package/src/components/DataDisplay/LevelBadge/LevelBadge.stories.tsx +99 -0
  100. package/src/components/DataDisplay/LevelBadge/LevelBadge.tsx +118 -0
  101. package/src/components/DataDisplay/ProgressBar/ProgressBar.stories.tsx +95 -0
  102. package/src/components/DataDisplay/ProgressBar/ProgressBar.tsx +117 -0
  103. package/src/components/DataDisplay/StatBadge/StatBadge.stories.tsx +82 -0
  104. package/src/components/DataDisplay/StatBadge/StatBadge.tsx +135 -0
  105. package/src/components/DataDisplay/StatBadge/index.ts +1 -0
  106. package/src/components/DataDisplay/Tag/Tag.stories.tsx +63 -0
  107. package/src/components/DataDisplay/Tag/Tag.tsx +148 -0
  108. package/src/components/Feedback/FeedbackBottomSheet/FeedbackBottomSheet.stories.tsx +74 -0
  109. package/src/components/Feedback/FeedbackBottomSheet/FeedbackBottomSheet.tsx +200 -0
  110. package/src/components/Forms/Checkbox/Checkbox.stories.tsx +60 -0
  111. package/src/components/Forms/Checkbox/Checkbox.tsx +127 -0
  112. package/src/components/Forms/Checkbox/index.ts +1 -0
  113. package/src/components/Forms/DaySelector/DaySelector.stories.tsx +62 -0
  114. package/src/components/Forms/DaySelector/DaySelector.tsx +133 -0
  115. package/src/components/Forms/Radio/Radio.stories.tsx +61 -0
  116. package/src/components/Forms/Radio/Radio.tsx +141 -0
  117. package/src/components/Forms/Radio/index.ts +1 -0
  118. package/src/components/Forms/SegmentedToggle/SegmentedToggle.stories.tsx +67 -0
  119. package/src/components/Forms/SegmentedToggle/SegmentedToggle.tsx +150 -0
  120. package/src/components/Forms/SegmentedToggle/index.ts +1 -0
  121. package/src/components/Forms/Select/Select.stories.tsx +59 -0
  122. package/src/components/Forms/Select/Select.tsx +255 -0
  123. package/src/components/Forms/Select/index.ts +1 -0
  124. package/src/components/Forms/Switch/Switch.stories.tsx +57 -0
  125. package/src/components/Forms/Switch/Switch.tsx +120 -0
  126. package/src/components/Forms/Switch/index.ts +1 -0
  127. package/src/components/Forms/TimeSlot/TimeSlot.stories.tsx +62 -0
  128. package/src/components/Forms/TimeSlot/TimeSlot.tsx +116 -0
  129. package/src/components/Headers/CountryHeader/CountryHeader.stories.tsx +114 -0
  130. package/src/components/Headers/CountryHeader/CountryHeader.tsx +147 -0
  131. package/src/components/Headers/ProfileHeader/ProfileHeader.stories.tsx +141 -0
  132. package/src/components/Headers/ProfileHeader/ProfileHeader.tsx +233 -0
  133. package/src/components/Inputs/FillInTheBlank/FillInTheBlank.stories.tsx +333 -0
  134. package/src/components/Inputs/FillInTheBlank/FillInTheBlank.tsx +287 -0
  135. package/src/components/Inputs/FillInTheBlank/index.ts +1 -0
  136. package/src/components/Inputs/TextInput/TextInput.stories.tsx +103 -0
  137. package/src/components/Inputs/TextInput/TextInput.tsx +187 -0
  138. package/src/components/Mascots/Vava/Vava.stories.tsx +114 -0
  139. package/src/components/Mascots/Vava/Vava.tsx +276 -0
  140. package/src/components/Mascots/Yaya/Yaya.stories.tsx +98 -0
  141. package/src/components/Mascots/Yaya/Yaya.tsx +233 -0
  142. package/src/components/Navigation/BottomTabBar/BottomTabBar.stories.tsx +68 -0
  143. package/src/components/Navigation/BottomTabBar/BottomTabBar.tsx +85 -0
  144. package/src/components/Navigation/BottomTabBar/BottomTabBarItem.tsx +68 -0
  145. package/src/components/Navigation/BottomTabBar/index.ts +2 -0
  146. package/src/components/Navigation/PaginationDots/PaginationDots.stories.tsx +62 -0
  147. package/src/components/Navigation/PaginationDots/PaginationDots.tsx +81 -0
  148. package/src/components/Navigation/PaginationDots/index.ts +1 -0
  149. package/src/components/Navigation/ProgressMap/ProgressMap.stories.tsx +116 -0
  150. package/src/components/Navigation/ProgressMap/ProgressMap.tsx +827 -0
  151. package/src/components/Navigation/WorldProgressMap/WorldProgressMap.stories.tsx +169 -0
  152. package/src/components/Navigation/WorldProgressMap/WorldProgressMap.tsx +790 -0
  153. package/src/components/Typography/Typography.stories.tsx +127 -0
  154. package/src/components/Typography/Typography.tsx +53 -0
  155. package/src/hooks/useResponsive.ts +41 -0
  156. package/src/hooks/useTheme.ts +32 -0
  157. package/src/index.css +355 -0
  158. package/src/index.ts +28 -0
  159. package/src/main.tsx +10 -0
  160. package/src/mocks/codegenNativeComponent.ts +5 -0
  161. package/src/styles/countryThemes.ts +79 -0
  162. package/src/styles/theme.ts +297 -0
  163. package/src/utils/iconMap.ts +23 -0
  164. package/tsconfig.app.json +25 -0
  165. package/tsconfig.json +7 -0
  166. package/tsconfig.node.json +24 -0
  167. package/vite.config.ts +51 -0
  168. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,189 @@
1
+ import React, { useState } from 'react';
2
+ import { Pressable, StyleSheet, 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 { IconMap } from '../../../utils/iconMap';
7
+ import { Typography } from '../../Typography/Typography';
8
+
9
+ export interface IconButtonProps {
10
+ /** Nome exato do ícone da biblioteca lucide-react-native (ex: 'Play', 'Volume2') */
11
+ iconName: string;
12
+ /** Texto opcional que aparece por baixo do ícone (ex: 'LENTO') */
13
+ text?: string;
14
+ /** Variante do botão para predefinição de cores */
15
+ variant?: 'primary' | 'secondary';
16
+ /** Cor de fundo customizada (sobrepõe a cor da variante) */
17
+ bgColor?: string;
18
+ /** Cor do ícone customizada (sobrepõe a cor da variante) */
19
+ iconColor?: string;
20
+ /** Cor do texto customizada (sobrepõe a cor da variante) */
21
+ textColor?: string;
22
+ /** Cor da sombra customizada (sobrepõe a cor da variante) */
23
+ shadowColor?: string;
24
+ /** Cor de fundo no hover (web) */
25
+ hoverColor?: string;
26
+ /** Tamanho do botão */
27
+ size?: 'sm' | 'md' | 'lg';
28
+ /** Se o botão está desativado */
29
+ disabled?: boolean;
30
+ /** Callback para o evento de clique */
31
+ onPress?: () => void;
32
+ /** Estilos customizados */
33
+ style?: ViewStyle | ViewStyle[];
34
+ }
35
+
36
+ export const IconButton = React.forwardRef<React.ElementRef<typeof Pressable>, IconButtonProps>(
37
+ (
38
+ {
39
+ iconName,
40
+ text,
41
+ variant = 'primary',
42
+ bgColor,
43
+ iconColor,
44
+ textColor,
45
+ shadowColor,
46
+ hoverColor,
47
+ size = 'md',
48
+ disabled = false,
49
+ onPress,
50
+ style,
51
+ ...props
52
+ },
53
+ ref
54
+ ) => {
55
+ const [isHovered, setIsHovered] = useState(false);
56
+ const { scale, scaleText } = useResponsive();
57
+
58
+ const Icon = IconMap[iconName] as React.ElementType;
59
+
60
+ const getBgColor = () => {
61
+ if (disabled) return '#efe2d6';
62
+ if (bgColor) return isHovered && hoverColor ? hoverColor : bgColor;
63
+ if (variant === 'primary') return isHovered ? '#e8883e' : theme.colors.primary; // laranja
64
+ if (variant === 'secondary') return theme.colors.white;
65
+ return theme.colors.primary;
66
+ };
67
+
68
+ const getIconColor = () => {
69
+ if (disabled) return '#b9a392';
70
+ if (iconColor) return iconColor;
71
+ if (variant === 'primary') return '#ffffff';
72
+ if (variant === 'secondary') return theme.colors.brown[500];
73
+ return '#ffffff';
74
+ };
75
+
76
+ const getTextColor = () => {
77
+ if (disabled) return '#b9a392';
78
+ if (textColor) return textColor;
79
+ if (variant === 'primary') return '#ffffff';
80
+ if (variant === 'secondary') return theme.colors.brown[800];
81
+ return '#ffffff';
82
+ };
83
+
84
+ const getShadowStyles = (): ViewStyle => {
85
+ if (disabled) return {};
86
+
87
+ if (Platform.OS === 'web') {
88
+ const resolvedShadowColor = shadowColor || (variant === 'primary' ? theme.colors.primaryDark : theme.colors.brown[100]);
89
+ return {
90
+ boxShadow: `0px ${scale(4)}px 0px ${resolvedShadowColor}`
91
+ };
92
+ } else {
93
+ if (shadowColor) {
94
+ return {
95
+ shadowColor,
96
+ shadowOffset: { width: 0, height: scale(4) },
97
+ shadowOpacity: 1,
98
+ shadowRadius: 0,
99
+ elevation: 4,
100
+ };
101
+ }
102
+ return variant === 'primary' ? theme.shadows.btnPrimary : theme.shadows.btnSecondary;
103
+ }
104
+ };
105
+
106
+ const sizeStyles = {
107
+ sm: { width: scale(40), height: scale(40), borderRadius: scale(12) },
108
+ md: { width: scale(60), height: scale(60), borderRadius: scale(16) },
109
+ lg: { width: scale(72), height: scale(72), borderRadius: scale(20) },
110
+ }[size];
111
+
112
+ const iconSizes = {
113
+ sm: scale(20),
114
+ md: scale(text ? 20 : 32),
115
+ lg: scale(text ? 24 : 40),
116
+ }[size];
117
+
118
+ const buttonStyles: ViewStyle[] = [
119
+ styles.base,
120
+ sizeStyles,
121
+ { backgroundColor: getBgColor() },
122
+ getShadowStyles(),
123
+ style as ViewStyle,
124
+ ];
125
+
126
+ const hoverProps = Platform.OS === 'web' ? {
127
+ onHoverIn: () => !disabled && setIsHovered(true),
128
+ onHoverOut: () => !disabled && setIsHovered(false),
129
+ } : {};
130
+
131
+ return (
132
+ <Pressable
133
+ ref={ref}
134
+ disabled={disabled}
135
+ onPress={onPress}
136
+ style={({ pressed }) => {
137
+ const stateStyles: ViewStyle[] = [...buttonStyles];
138
+
139
+ if (disabled) {
140
+ if (Platform.OS === 'web') stateStyles.push({ cursor: 'not-allowed' } as any);
141
+ return stateStyles;
142
+ }
143
+
144
+ if (pressed) {
145
+ stateStyles.push({ transform: [{ translateY: scale(4) }] });
146
+ if (Platform.OS === 'web') {
147
+ stateStyles.push({ boxShadow: 'none' } as any);
148
+ } else {
149
+ stateStyles.push({ shadowOpacity: 0, elevation: 0 });
150
+ }
151
+ }
152
+ return stateStyles;
153
+ }}
154
+ {...hoverProps}
155
+ {...props}
156
+ >
157
+ {Icon ? (
158
+ <Icon
159
+ size={iconSizes}
160
+ color={getIconColor()}
161
+ strokeWidth={text ? 2.5 : 3}
162
+ />
163
+ ) : (
164
+ <Typography style={{ color: 'red', fontSize: 10 }}>Missing</Typography>
165
+ )}
166
+
167
+ {!!text && (
168
+ <Typography style={[styles.text, { color: getTextColor(), fontSize: scaleText(size === 'sm' ? 10 : 12), marginTop: scale(4) }]}>
169
+ {text}
170
+ </Typography>
171
+ )}
172
+ </Pressable>
173
+ );
174
+ }
175
+ );
176
+
177
+ IconButton.displayName = 'IconButton';
178
+
179
+ const styles = StyleSheet.create({
180
+ base: {
181
+ alignItems: 'center',
182
+ justifyContent: 'center',
183
+ overflow: 'visible',
184
+ },
185
+ text: {
186
+
187
+ textTransform: 'uppercase',
188
+ }
189
+ });
@@ -0,0 +1,80 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { View } from 'react-native';
3
+ import { JourneyButton } from './JourneyButton';
4
+
5
+ const meta = {
6
+ title: 'Components/Buttons/JourneyButton',
7
+ component: JourneyButton,
8
+ parameters: {
9
+ layout: 'padded',
10
+ docs: {
11
+ description: {
12
+ component: `O **JourneyButton** é o botão de ação principal (Card-style) usado para continuar ou iniciar uma unidade didática no mapa.
13
+
14
+ Siga a Brand Guideline usando \`iconName="Play"\` ou qualquer outro nome da biblioteca \`lucide-react-native\`.
15
+
16
+ ### Exemplo de Uso
17
+ \`\`\`tsx
18
+ <JourneyButton
19
+ iconName="Play"
20
+ title="Continuar a viagem"
21
+ subtitle="ESTÁS EM DAKAR - B2"
22
+ variant="primary"
23
+ onPress={() => console.log('Iniciar Lição')}
24
+ />
25
+ \`\`\``
26
+ }
27
+ }
28
+ },
29
+ tags: ['autodocs'],
30
+ decorators: [
31
+ (Story) => (
32
+ <View style={{ padding: 0, backgroundColor: '#f5f0eb', alignItems: 'center', justifyContent: 'center' }}>
33
+ <View style={{ width: '100%', maxWidth: 400 }}>
34
+ <Story />
35
+ </View>
36
+ </View>
37
+ )
38
+ ]
39
+ } satisfies Meta<any>;
40
+
41
+ export default meta;
42
+ type Story = StoryObj<any>;
43
+
44
+ export const Default: Story = {
45
+ args: {
46
+ iconName: 'Play',
47
+ title: 'Continuar a viagem',
48
+ subtitle: 'ESTÁS EM DAKAR - B2',
49
+ variant: 'primary',
50
+ }
51
+ };
52
+
53
+ export const Disabled: Story = {
54
+ args: {
55
+ iconName: 'Play',
56
+ title: 'Bloqueado',
57
+ subtitle: 'COMPLETA A LIÇÃO 1',
58
+ disabled: true,
59
+ }
60
+ };
61
+
62
+ export const CustomCountryColor: Story = {
63
+ args: {
64
+ iconName: 'Play',
65
+ title: 'Continuar a viagem',
66
+ subtitle: 'ESTÁS EM LUANDA - B2',
67
+ bgColor: '#16915a',
68
+ shadowColor: '#0e6940',
69
+ }
70
+ };
71
+
72
+ export const InfoVariant: Story = {
73
+ args: {
74
+ iconName: 'Info',
75
+ title: 'Explorar secção',
76
+ subtitle: 'DICAS DE GRAMÁTICA',
77
+ bgColor: '#2a6fc2',
78
+ shadowColor: '#1d4f8f',
79
+ }
80
+ };
@@ -0,0 +1,194 @@
1
+ import React, { useState } from 'react';
2
+ import { Pressable, StyleSheet, Platform, View } from 'react-native';
3
+ import type { ViewStyle } from 'react-native';
4
+ import { theme } from '../../../styles/theme';
5
+ import { useResponsive } from '../../../hooks/useResponsive';
6
+ import { IconMap } from '../../../utils/iconMap';
7
+ import { Typography } from '../../Typography/Typography';
8
+
9
+ export interface JourneyButtonProps {
10
+ /** Título de ação (ex: 'Continuar a viagem') */
11
+ title: string;
12
+ /** Subtítulo do percurso (ex: 'ESTÁS EM DAKAR - B2') */
13
+ subtitle: string;
14
+ /** Nome exato do ícone da esquerda (ex: 'Play') */
15
+ iconName?: string;
16
+ /** Variante do botão para predefinição de cores */
17
+ variant?: 'primary' | 'secondary';
18
+ /** Cor de fundo customizada */
19
+ bgColor?: string;
20
+ /** Cor da sombra customizada */
21
+ shadowColor?: string;
22
+ /** Cor do texto e ícones */
23
+ textColor?: string;
24
+ /** Cor de fundo no hover (web) */
25
+ hoverColor?: string;
26
+ /** Se o botão está desativado */
27
+ disabled?: boolean;
28
+ /** Callback para o evento de clique */
29
+ onPress?: () => void;
30
+ /** Estilos customizados */
31
+ style?: ViewStyle | ViewStyle[];
32
+ }
33
+
34
+ export const JourneyButton = React.forwardRef<React.ElementRef<typeof Pressable>, JourneyButtonProps>(
35
+ (
36
+ {
37
+ title,
38
+ subtitle,
39
+ iconName = 'Play',
40
+ variant = 'primary',
41
+ bgColor,
42
+ shadowColor,
43
+ textColor,
44
+ hoverColor,
45
+ disabled = false,
46
+ onPress,
47
+ style,
48
+ ...props
49
+ },
50
+ ref
51
+ ) => {
52
+ const [isHovered, setIsHovered] = useState(false);
53
+ const { scale, scaleText } = useResponsive();
54
+
55
+ const LeftIcon = IconMap[iconName] as React.ElementType;
56
+ const RightIcon = IconMap['ArrowRight'] as React.ElementType;
57
+
58
+ const getBgColor = () => {
59
+ if (disabled) return '#efe2d6';
60
+ if (bgColor) return isHovered && hoverColor ? hoverColor : bgColor;
61
+ if (variant === 'primary') return isHovered ? '#e8883e' : theme.colors.primary;
62
+ return theme.colors.white;
63
+ };
64
+
65
+ const getTextColor = () => {
66
+ if (disabled) return '#b9a392';
67
+ if (textColor) return textColor;
68
+ if (variant === 'primary') return '#ffffff';
69
+ return theme.colors.brown[800];
70
+ };
71
+
72
+ const getShadowStyles = (): ViewStyle => {
73
+ if (disabled) return {};
74
+
75
+ if (Platform.OS === 'web') {
76
+ const resolvedShadowColor = shadowColor || (variant === 'primary' ? theme.colors.primaryDark : theme.colors.brown[100]);
77
+ return {
78
+ boxShadow: `0px ${scale(4)}px 0px ${resolvedShadowColor}`
79
+ };
80
+ } else {
81
+ if (shadowColor) {
82
+ return {
83
+ shadowColor,
84
+ shadowOffset: { width: 0, height: scale(4) },
85
+ shadowOpacity: 1,
86
+ shadowRadius: 0,
87
+ elevation: 4,
88
+ };
89
+ }
90
+ return variant === 'primary' ? theme.shadows.btnPrimary : theme.shadows.btnSecondary;
91
+ }
92
+ };
93
+
94
+ const buttonStyles: ViewStyle[] = [
95
+ styles.container,
96
+ { backgroundColor: getBgColor(), borderRadius: scale(20), padding: scale(16) },
97
+ getShadowStyles(),
98
+ style as ViewStyle,
99
+ ];
100
+
101
+ const hoverProps = Platform.OS === 'web' ? {
102
+ onHoverIn: () => !disabled && setIsHovered(true),
103
+ onHoverOut: () => !disabled && setIsHovered(false),
104
+ } : {};
105
+
106
+ return (
107
+ <Pressable
108
+ ref={ref}
109
+ disabled={disabled}
110
+ onPress={onPress}
111
+ style={({ pressed }) => {
112
+ const stateStyles: ViewStyle[] = [...buttonStyles];
113
+
114
+ if (disabled) {
115
+ if (Platform.OS === 'web') stateStyles.push({ cursor: 'not-allowed' } as any);
116
+ return stateStyles;
117
+ }
118
+
119
+ if (pressed) {
120
+ stateStyles.push({ transform: [{ translateY: scale(4) }] });
121
+ if (Platform.OS === 'web') {
122
+ stateStyles.push({ boxShadow: 'none' } as any);
123
+ } else {
124
+ stateStyles.push({ shadowOpacity: 0, elevation: 0 });
125
+ }
126
+ }
127
+ return stateStyles;
128
+ }}
129
+ {...hoverProps}
130
+ {...props}
131
+ >
132
+ <View style={styles.content}>
133
+ <View style={[styles.iconBox, { borderRadius: scale(14), width: scale(56), height: scale(56), marginRight: scale(16) }]}>
134
+ {LeftIcon ? (
135
+ <LeftIcon size={scale(24)} color={getTextColor()} strokeWidth={3} fill={getTextColor()} />
136
+ ) : null}
137
+ </View>
138
+
139
+ <View style={styles.textContainer}>
140
+ <Typography
141
+ numberOfLines={1}
142
+ adjustsFontSizeToFit
143
+ style={[styles.subtitle, { color: getTextColor(), fontSize: scaleText(10), marginBottom: scale(4) }]}
144
+ >
145
+ {subtitle}
146
+ </Typography>
147
+ <Typography style={[styles.title, { color: getTextColor(), fontSize: scaleText(20) }]}>
148
+ {title}
149
+ </Typography>
150
+ </View>
151
+
152
+ <View style={[styles.rightIconBox, { marginLeft: scale(16) }]}>
153
+ <RightIcon size={scale(24)} color={getTextColor()} strokeWidth={3} />
154
+ </View>
155
+ </View>
156
+ </Pressable>
157
+ );
158
+ }
159
+ );
160
+
161
+ JourneyButton.displayName = 'JourneyButton';
162
+
163
+ const styles = StyleSheet.create({
164
+ container: {
165
+ width: '100%',
166
+ overflow: 'visible',
167
+ },
168
+ content: {
169
+ flexDirection: 'row',
170
+ alignItems: 'center',
171
+ },
172
+ iconBox: {
173
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
174
+ alignItems: 'center',
175
+ justifyContent: 'center',
176
+ },
177
+ textContainer: {
178
+ flex: 1,
179
+ justifyContent: 'center',
180
+ },
181
+ subtitle: {
182
+
183
+ letterSpacing: 0.8,
184
+ textTransform: 'uppercase',
185
+ opacity: 0.9,
186
+ },
187
+ title: {
188
+
189
+ },
190
+ rightIconBox: {
191
+ justifyContent: 'center',
192
+ alignItems: 'center',
193
+ }
194
+ });
@@ -0,0 +1,144 @@
1
+
2
+ // eslint-disable-next-line storybook/no-renderer-packages
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { View } from 'react-native';
5
+ import { MainButton } from './MainButton';
6
+
7
+ const meta = {
8
+ title: 'Components/Buttons/MainButton',
9
+ component: MainButton,
10
+ parameters: {
11
+ layout: 'padded',
12
+ docs: {
13
+ description: {
14
+ component: 'O **MainButton** é o botão de ação principal (CTA) da aplicação.\n\n### Exemplo de Uso\n```tsx\n<MainButton \n variant="primary"\n size="md"\n onPress={() => console.log("Clicado!")}\n>\n Confirmar\n</MainButton>\n```',
15
+ }
16
+ }
17
+ },
18
+ tags: ['autodocs'],
19
+ argTypes: {
20
+ variant: {
21
+ control: 'radio',
22
+ options: ['primary', 'secondary', 'ghost'],
23
+ },
24
+ size: {
25
+ control: 'radio',
26
+ options: ['sm', 'md', 'lg'],
27
+ },
28
+ fullWidth: {
29
+ control: 'boolean',
30
+ },
31
+ disabled: {
32
+ control: 'boolean',
33
+ },
34
+ loading: {
35
+ control: 'boolean',
36
+ },
37
+ children: {
38
+ control: 'text',
39
+ },
40
+ },
41
+ decorators: [
42
+ (Story) => (
43
+ <View style={{ padding: 0, width: '100%', alignItems: 'center' }}>
44
+ <Story />
45
+ </View>
46
+ ),
47
+ ],
48
+ } satisfies Meta<any>;
49
+
50
+ export default meta;
51
+ type Story = StoryObj<any>;
52
+
53
+ // Basic Variants
54
+ export const Primary: Story = {
55
+ args: {
56
+ variant: 'primary',
57
+ children: 'Confirmar',
58
+ },
59
+ };
60
+
61
+ export const Secondary: Story = {
62
+ args: {
63
+ variant: 'secondary',
64
+ children: 'Cancelar',
65
+ },
66
+ };
67
+
68
+ export const Ghost: Story = {
69
+ args: {
70
+ variant: 'ghost',
71
+ children: 'Ver detalhes',
72
+ },
73
+ };
74
+
75
+ // Sizes
76
+ export const Small: Story = {
77
+ args: {
78
+ size: 'sm',
79
+ children: 'Ação Pequena',
80
+ },
81
+ };
82
+
83
+ export const Large: Story = {
84
+ args: {
85
+ size: 'lg',
86
+ children: 'Começar Agora',
87
+ },
88
+ };
89
+
90
+ // States
91
+ export const Loading: Story = {
92
+ args: {
93
+ loading: true,
94
+ children: 'A Carregar...',
95
+ },
96
+ };
97
+
98
+ export const DisabledPrimary: Story = {
99
+ args: {
100
+ variant: 'primary',
101
+ disabled: true,
102
+ children: 'Indisponível',
103
+ },
104
+ };
105
+
106
+ export const DisabledSecondary: Story = {
107
+ args: {
108
+ variant: 'secondary',
109
+ disabled: true,
110
+ children: 'Indisponível',
111
+ },
112
+ };
113
+
114
+ export const DisabledGhost: Story = {
115
+ args: {
116
+ variant: 'ghost',
117
+ disabled: true,
118
+ children: 'Indisponível',
119
+ },
120
+ };
121
+
122
+ export const FullWidth: Story = {
123
+ decorators: [
124
+ (Story) => (
125
+ <View style={{ width: 300 }}>
126
+ <Story />
127
+ </View>
128
+ ),
129
+ ],
130
+ args: {
131
+ fullWidth: true,
132
+ children: 'Largura Total',
133
+ },
134
+ };
135
+
136
+ // Custom Colors
137
+ export const CustomGreen: Story = {
138
+ args: {
139
+ variant: 'primary',
140
+ children: 'Sucesso',
141
+ bgColor: '#16915a', // var(--color-success)
142
+ shadowColor: '#0e6940',
143
+ },
144
+ };