@xetwa/design-system 1.0.10 → 1.0.11

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xetwa/design-system",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,74 @@
1
+ // eslint-disable-next-line storybook/no-renderer-packages
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { View, Text } from 'react-native';
4
+ import { BaseButton } from './BaseButton';
5
+
6
+ const meta = {
7
+ title: 'Components/Buttons/BaseButton',
8
+ component: BaseButton,
9
+ parameters: {
10
+ layout: 'padded',
11
+ docs: {
12
+ description: {
13
+ component: 'O **BaseButton** é idêntico ao MainButton mas aceita ReactNode genérico como children. Ideal para ícones combinados com texto ou outros layouts costumizados.\n\n### Exemplo de Uso\n```tsx\n<BaseButton \n variant="secondary"\n size="lg"\n onPress={() => console.log("Clicado!")}\n>\n <View><Text>Continuar com Google</Text></View>\n</BaseButton>\n```',
14
+ }
15
+ }
16
+ },
17
+ tags: ['autodocs'],
18
+ argTypes: {
19
+ variant: {
20
+ control: 'radio',
21
+ options: ['primary', 'secondary', 'ghost'],
22
+ },
23
+ size: {
24
+ control: 'radio',
25
+ options: ['sm', 'md', 'lg'],
26
+ },
27
+ fullWidth: {
28
+ control: 'boolean',
29
+ },
30
+ disabled: {
31
+ control: 'boolean',
32
+ },
33
+ loading: {
34
+ control: 'boolean',
35
+ },
36
+ },
37
+ decorators: [
38
+ (Story) => (
39
+ <View style={{ padding: 0, width: '100%', alignItems: 'center' }}>
40
+ <Story />
41
+ </View>
42
+ ),
43
+ ],
44
+ } satisfies Meta<any>;
45
+
46
+ export default meta;
47
+ type Story = StoryObj<any>;
48
+
49
+ export const Primary: Story = {
50
+ args: {
51
+ variant: 'primary',
52
+ children: <Text style={{ color: '#fff', fontWeight: 'bold' }}>Botão Base Primário</Text>,
53
+ },
54
+ };
55
+
56
+ export const SecondaryWithIcon: Story = {
57
+ args: {
58
+ variant: 'secondary',
59
+ children: (
60
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
61
+ <Text style={{ fontSize: 20 }}>🍎</Text>
62
+ <Text style={{ color: '#51332d', fontWeight: 'bold' }}>Continuar com Apple</Text>
63
+ </View>
64
+ ),
65
+ },
66
+ };
67
+
68
+ export const Loading: Story = {
69
+ args: {
70
+ variant: 'primary',
71
+ loading: true,
72
+ children: <Text>Oculto pelo loading</Text>,
73
+ },
74
+ };
@@ -0,0 +1,186 @@
1
+ import React, { useState } from 'react';
2
+ import { Pressable, ActivityIndicator, 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
+
7
+ /**
8
+ * BaseButton é um botão genérico que aplica o comportamento tátil 3D e cores do tema,
9
+ * permitindo passar conteúdo customizado (ReactNode) como children.
10
+ */
11
+ export interface BaseButtonProps {
12
+ /** Conteúdo livre a exibir no botão (ícones, texto, etc) */
13
+ children: React.ReactNode;
14
+ /** Variante visual do botão. */
15
+ variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
16
+ /** Tamanho do botão. */
17
+ size?: 'sm' | 'md' | 'lg';
18
+ /** Se true, o botão ocupa a largura total disponível. */
19
+ fullWidth?: boolean;
20
+ /** Se true, desativa a interação e aplica estilos de inativo. */
21
+ disabled?: boolean;
22
+ /** Se true, exibe um spinner de carregamento em vez do conteúdo. */
23
+ loading?: boolean;
24
+ /** Callback para o evento de clique. */
25
+ onPress?: () => void;
26
+ /** Cor de fundo customizada (sobrepõe a cor da variante) */
27
+ bgColor?: string;
28
+ /** Cor da sombra customizada (sobrepõe a cor da variante) */
29
+ shadowColor?: string;
30
+ /** Cor de fundo no hover (opcional) */
31
+ hoverColor?: string;
32
+ style?: ViewStyle | ViewStyle[];
33
+ }
34
+
35
+ export const BaseButton = React.forwardRef<React.ElementRef<typeof Pressable>, BaseButtonProps>(
36
+ (
37
+ {
38
+ style,
39
+ variant = 'primary',
40
+ size = 'md',
41
+ fullWidth = false,
42
+ loading = false,
43
+ children,
44
+ disabled,
45
+ onPress,
46
+ bgColor,
47
+ shadowColor,
48
+ hoverColor,
49
+ ...props
50
+ },
51
+ ref
52
+ ) => {
53
+ const [isHovered, setIsHovered] = useState(false);
54
+ const { scale } = useResponsive();
55
+
56
+ const getBgColor = () => {
57
+ if (disabled) {
58
+ if (variant === 'primary' || variant === 'secondary') return '#efe2d6';
59
+ return 'transparent';
60
+ }
61
+ if (bgColor) return isHovered ? (hoverColor || bgColor) : bgColor;
62
+ if (variant === 'primary') return isHovered ? '#e8883e' : theme.colors.primary;
63
+ if (variant === 'secondary') return theme.colors.white;
64
+ return 'transparent'; // ghost
65
+ };
66
+
67
+ const getBorderStyles = (): ViewStyle => {
68
+ if (variant === 'secondary') {
69
+ const borderColor = disabled ? '#efe2d6' : isHovered ? '#f2974b' : theme.colors.brown[100];
70
+ return {
71
+ borderWidth: 2,
72
+ borderColor,
73
+ };
74
+ }
75
+ return { borderWidth: 0 };
76
+ };
77
+
78
+ const getShadowStyles = (): ViewStyle => {
79
+ if (disabled || variant === 'ghost') {
80
+ return {};
81
+ }
82
+
83
+ if (Platform.OS === 'web') {
84
+ const resolvedShadowColor = shadowColor || (variant === 'primary' ? theme.colors.primaryDark : theme.colors.brown[100]);
85
+ return {
86
+ boxShadow: `0px ${scale(4)}px 0px ${resolvedShadowColor}`
87
+ };
88
+ } else {
89
+ if (shadowColor) {
90
+ return {
91
+ shadowColor,
92
+ shadowOffset: { width: 0, height: scale(4) },
93
+ shadowOpacity: 1,
94
+ shadowRadius: 0,
95
+ elevation: 4,
96
+ };
97
+ }
98
+ return variant === 'primary' ? theme.shadows.btnPrimary : theme.shadows.btnSecondary;
99
+ }
100
+ };
101
+
102
+ const sizeStyles = {
103
+ sm: { height: scale(36), paddingHorizontal: scale(16), borderRadius: scale(11) },
104
+ md: { height: scale(50), paddingHorizontal: scale(22), borderRadius: scale(16) },
105
+ lg: { height: scale(58), paddingHorizontal: scale(32), borderRadius: scale(18) },
106
+ }[size];
107
+
108
+ const buttonStyles: ViewStyle[] = [
109
+ styles.base,
110
+ sizeStyles,
111
+ { backgroundColor: getBgColor() },
112
+ getBorderStyles(),
113
+ getShadowStyles(),
114
+ fullWidth ? { width: '100%' } : { alignSelf: 'flex-start' },
115
+ disabled && variant === 'ghost' ? { opacity: 0.4 } : {},
116
+ style as ViewStyle,
117
+ ];
118
+
119
+ const hoverProps = Platform.OS === 'web' ? {
120
+ onHoverIn: () => !disabled && setIsHovered(true),
121
+ onHoverOut: () => !disabled && setIsHovered(false),
122
+ } : {};
123
+
124
+ return (
125
+ <Pressable
126
+ ref={ref}
127
+ disabled={disabled || loading}
128
+ onPress={onPress}
129
+ style={({ pressed }) => {
130
+ const stateStyles: ViewStyle[] = [...buttonStyles];
131
+
132
+ if (disabled) {
133
+ // @ts-expect-error - web only style
134
+ if (Platform.OS === 'web') stateStyles.push({ cursor: 'not-allowed' });
135
+ return stateStyles;
136
+ }
137
+
138
+ if (pressed && !loading) {
139
+ if (variant === 'ghost') {
140
+ stateStyles.push({ opacity: 0.7 });
141
+ } else {
142
+ stateStyles.push({
143
+ transform: [{ translateY: scale(4) }],
144
+ });
145
+ if (Platform.OS === 'web') {
146
+ stateStyles.push({ boxShadow: 'none' });
147
+ } else {
148
+ stateStyles.push({
149
+ shadowOpacity: 0,
150
+ elevation: 0,
151
+ });
152
+ }
153
+ }
154
+ } else if (isHovered && variant === 'primary' && !loading) {
155
+ stateStyles.push({
156
+ transform: [{ translateY: scale(-1) }],
157
+ });
158
+ }
159
+
160
+ return stateStyles;
161
+ }}
162
+ {...hoverProps}
163
+ {...props}
164
+ >
165
+ {loading ? (
166
+ <ActivityIndicator
167
+ size="small"
168
+ color={variant === 'primary' ? '#ffffff' : theme.colors.brown[500]}
169
+ />
170
+ ) : (
171
+ children
172
+ )}
173
+ </Pressable>
174
+ );
175
+ }
176
+ );
177
+
178
+ BaseButton.displayName = 'BaseButton';
179
+
180
+ const styles = StyleSheet.create({
181
+ base: {
182
+ alignItems: 'center',
183
+ justifyContent: 'center',
184
+ flexDirection: 'row',
185
+ },
186
+ });
@@ -18,6 +18,7 @@ export interface TextInputProps extends Omit<RNTextInputProps, 'style'> {
18
18
  style?: ViewStyle | ViewStyle[];
19
19
  inputStyle?: TextStyle | TextStyle[];
20
20
  containerStyle?: ViewStyle | ViewStyle[];
21
+ rightElement?: React.ReactNode;
21
22
  }
22
23
 
23
24
  export const TextInput = forwardRef<RNTextInput, TextInputProps>(
@@ -31,6 +32,7 @@ export const TextInput = forwardRef<RNTextInput, TextInputProps>(
31
32
  style,
32
33
  inputStyle,
33
34
  containerStyle,
35
+ rightElement,
34
36
  onFocus,
35
37
  onBlur,
36
38
  ...props
@@ -138,6 +140,7 @@ export const TextInput = forwardRef<RNTextInput, TextInputProps>(
138
140
  ]}
139
141
  {...props}
140
142
  />
143
+ {rightElement}
141
144
  </View>
142
145
 
143
146
  {messageText && (
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './components/Headers/CountryHeader/CountryHeader';
2
2
  export * from './components/Buttons/MainButton/MainButton';
3
+ export * from './components/Buttons/BaseButton/BaseButton';
3
4
  export * from './components/Buttons/BackButton/BackButton';
4
5
  export * from './components/Buttons/FabButton/FabButton';
5
6
  export * from './components/Cards/ModuleCard/ModuleCard';