@space-uy/pulsar-ui 0.2.0
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/LICENSE +20 -0
- package/README.md +148 -0
- package/lib/module/components/Accordion.js +242 -0
- package/lib/module/components/Accordion.js.map +1 -0
- package/lib/module/components/BottomSheet.js +183 -0
- package/lib/module/components/BottomSheet.js.map +1 -0
- package/lib/module/components/Button.js +64 -0
- package/lib/module/components/Button.js.map +1 -0
- package/lib/module/components/ButtonContainer.js +118 -0
- package/lib/module/components/ButtonContainer.js.map +1 -0
- package/lib/module/components/CalendarPicker.js +374 -0
- package/lib/module/components/CalendarPicker.js.map +1 -0
- package/lib/module/components/Card.js +43 -0
- package/lib/module/components/Card.js.map +1 -0
- package/lib/module/components/Checkbox.js +122 -0
- package/lib/module/components/Checkbox.js.map +1 -0
- package/lib/module/components/Chip.js +50 -0
- package/lib/module/components/Chip.js.map +1 -0
- package/lib/module/components/CopyToClipboard.js +98 -0
- package/lib/module/components/CopyToClipboard.js.map +1 -0
- package/lib/module/components/Dialog.js +232 -0
- package/lib/module/components/Dialog.js.map +1 -0
- package/lib/module/components/Header.js +94 -0
- package/lib/module/components/Header.js.map +1 -0
- package/lib/module/components/Icon.js +22 -0
- package/lib/module/components/Icon.js.map +1 -0
- package/lib/module/components/IconButton.js +57 -0
- package/lib/module/components/IconButton.js.map +1 -0
- package/lib/module/components/Input.js +111 -0
- package/lib/module/components/Input.js.map +1 -0
- package/lib/module/components/InputContainer.js +104 -0
- package/lib/module/components/InputContainer.js.map +1 -0
- package/lib/module/components/LoadingIndicator.js +62 -0
- package/lib/module/components/LoadingIndicator.js.map +1 -0
- package/lib/module/components/OtpInput.js +85 -0
- package/lib/module/components/OtpInput.js.map +1 -0
- package/lib/module/components/OtpInputContainer.js +148 -0
- package/lib/module/components/OtpInputContainer.js.map +1 -0
- package/lib/module/components/Select.js +189 -0
- package/lib/module/components/Select.js.map +1 -0
- package/lib/module/components/Switch.js +74 -0
- package/lib/module/components/Switch.js.map +1 -0
- package/lib/module/components/Tabs.js +99 -0
- package/lib/module/components/Tabs.js.map +1 -0
- package/lib/module/components/Text.js +66 -0
- package/lib/module/components/Text.js.map +1 -0
- package/lib/module/components/TextArea.js +106 -0
- package/lib/module/components/TextArea.js.map +1 -0
- package/lib/module/hooks/useTheme.js +20 -0
- package/lib/module/hooks/useTheme.js.map +1 -0
- package/lib/module/index.js +27 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/store/themeStore.js +50 -0
- package/lib/module/store/themeStore.js.map +1 -0
- package/lib/module/theme/colors.js +25 -0
- package/lib/module/theme/colors.js.map +1 -0
- package/lib/module/theme/meassures.js +10 -0
- package/lib/module/theme/meassures.js.map +1 -0
- package/lib/module/utils/stringUtils.js +12 -0
- package/lib/module/utils/stringUtils.js.map +1 -0
- package/lib/module/utils/uiUtils.js +63 -0
- package/lib/module/utils/uiUtils.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/components/Accordion.d.ts +22 -0
- package/lib/typescript/src/components/Accordion.d.ts.map +1 -0
- package/lib/typescript/src/components/BottomSheet.d.ts +13 -0
- package/lib/typescript/src/components/BottomSheet.d.ts.map +1 -0
- package/lib/typescript/src/components/Button.d.ts +16 -0
- package/lib/typescript/src/components/Button.d.ts.map +1 -0
- package/lib/typescript/src/components/ButtonContainer.d.ts +30 -0
- package/lib/typescript/src/components/ButtonContainer.d.ts.map +1 -0
- package/lib/typescript/src/components/CalendarPicker.d.ts +19 -0
- package/lib/typescript/src/components/CalendarPicker.d.ts.map +1 -0
- package/lib/typescript/src/components/Card.d.ts +7 -0
- package/lib/typescript/src/components/Card.d.ts.map +1 -0
- package/lib/typescript/src/components/Checkbox.d.ts +11 -0
- package/lib/typescript/src/components/Checkbox.d.ts.map +1 -0
- package/lib/typescript/src/components/Chip.d.ts +9 -0
- package/lib/typescript/src/components/Chip.d.ts.map +1 -0
- package/lib/typescript/src/components/CopyToClipboard.d.ts +12 -0
- package/lib/typescript/src/components/CopyToClipboard.d.ts.map +1 -0
- package/lib/typescript/src/components/Dialog.d.ts +40 -0
- package/lib/typescript/src/components/Dialog.d.ts.map +1 -0
- package/lib/typescript/src/components/Header.d.ts +18 -0
- package/lib/typescript/src/components/Header.d.ts.map +1 -0
- package/lib/typescript/src/components/Icon.d.ts +12 -0
- package/lib/typescript/src/components/Icon.d.ts.map +1 -0
- package/lib/typescript/src/components/IconButton.d.ts +13 -0
- package/lib/typescript/src/components/IconButton.d.ts.map +1 -0
- package/lib/typescript/src/components/Input.d.ts +17 -0
- package/lib/typescript/src/components/Input.d.ts.map +1 -0
- package/lib/typescript/src/components/InputContainer.d.ts +22 -0
- package/lib/typescript/src/components/InputContainer.d.ts.map +1 -0
- package/lib/typescript/src/components/LoadingIndicator.d.ts +9 -0
- package/lib/typescript/src/components/LoadingIndicator.d.ts.map +1 -0
- package/lib/typescript/src/components/OtpInput.d.ts +3 -0
- package/lib/typescript/src/components/OtpInput.d.ts.map +1 -0
- package/lib/typescript/src/components/OtpInputContainer.d.ts +17 -0
- package/lib/typescript/src/components/OtpInputContainer.d.ts.map +1 -0
- package/lib/typescript/src/components/Select.d.ts +20 -0
- package/lib/typescript/src/components/Select.d.ts.map +1 -0
- package/lib/typescript/src/components/Switch.d.ts +10 -0
- package/lib/typescript/src/components/Switch.d.ts.map +1 -0
- package/lib/typescript/src/components/Tabs.d.ts +14 -0
- package/lib/typescript/src/components/Tabs.d.ts.map +1 -0
- package/lib/typescript/src/components/Text.d.ts +7 -0
- package/lib/typescript/src/components/Text.d.ts.map +1 -0
- package/lib/typescript/src/components/TextArea.d.ts +16 -0
- package/lib/typescript/src/components/TextArea.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useTheme.d.ts +9 -0
- package/lib/typescript/src/hooks/useTheme.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +27 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/store/themeStore.d.ts +32 -0
- package/lib/typescript/src/store/themeStore.d.ts.map +1 -0
- package/lib/typescript/src/theme/colors.d.ts +14 -0
- package/lib/typescript/src/theme/colors.d.ts.map +1 -0
- package/lib/typescript/src/theme/meassures.d.ts +9 -0
- package/lib/typescript/src/theme/meassures.d.ts.map +1 -0
- package/lib/typescript/src/utils/stringUtils.d.ts +7 -0
- package/lib/typescript/src/utils/stringUtils.d.ts.map +1 -0
- package/lib/typescript/src/utils/uiUtils.d.ts +21 -0
- package/lib/typescript/src/utils/uiUtils.d.ts.map +1 -0
- package/package.json +173 -0
- package/src/components/Accordion.tsx +284 -0
- package/src/components/BottomSheet.tsx +259 -0
- package/src/components/Button.tsx +85 -0
- package/src/components/ButtonContainer.tsx +161 -0
- package/src/components/CalendarPicker.tsx +428 -0
- package/src/components/Card.tsx +55 -0
- package/src/components/Checkbox.tsx +160 -0
- package/src/components/Chip.tsx +58 -0
- package/src/components/CopyToClipboard.tsx +108 -0
- package/src/components/Dialog.tsx +263 -0
- package/src/components/Header.tsx +100 -0
- package/src/components/Icon.tsx +27 -0
- package/src/components/IconButton.tsx +71 -0
- package/src/components/Input.tsx +144 -0
- package/src/components/InputContainer.tsx +134 -0
- package/src/components/LoadingIndicator.tsx +78 -0
- package/src/components/OtpInput.tsx +109 -0
- package/src/components/OtpInputContainer.tsx +196 -0
- package/src/components/Select.tsx +219 -0
- package/src/components/Switch.tsx +104 -0
- package/src/components/Tabs.tsx +117 -0
- package/src/components/Text.tsx +64 -0
- package/src/components/TextArea.tsx +141 -0
- package/src/hooks/useTheme.tsx +23 -0
- package/src/index.tsx +38 -0
- package/src/store/themeStore.ts +57 -0
- package/src/theme/colors.ts +35 -0
- package/src/theme/meassures.ts +7 -0
- package/src/utils/stringUtils.ts +16 -0
- package/src/utils/uiUtils.ts +70 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
View,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
type StyleProp,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { Check } from 'lucide-react-native';
|
|
11
|
+
import Animated, {
|
|
12
|
+
interpolateColor,
|
|
13
|
+
useAnimatedStyle,
|
|
14
|
+
useSharedValue,
|
|
15
|
+
withTiming,
|
|
16
|
+
} from 'react-native-reanimated';
|
|
17
|
+
|
|
18
|
+
import Text from './Text';
|
|
19
|
+
|
|
20
|
+
import useTheme from '../hooks/useTheme';
|
|
21
|
+
|
|
22
|
+
import { convertHexToRgba } from '../utils/uiUtils';
|
|
23
|
+
|
|
24
|
+
export type CheckboxProps = {
|
|
25
|
+
style?: StyleProp<ViewStyle>;
|
|
26
|
+
checked: boolean;
|
|
27
|
+
onCheckedChange: (checked: boolean) => void;
|
|
28
|
+
label: string;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
description?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default function Checkbox({
|
|
34
|
+
style,
|
|
35
|
+
checked,
|
|
36
|
+
onCheckedChange,
|
|
37
|
+
label,
|
|
38
|
+
disabled = false,
|
|
39
|
+
description,
|
|
40
|
+
}: CheckboxProps) {
|
|
41
|
+
const { colors, theme } = useTheme();
|
|
42
|
+
const checkedValue = useSharedValue(checked ? 1 : 0);
|
|
43
|
+
const pressedValue = useSharedValue(0);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
checkedValue.value = withTiming(checked ? 1 : 0, {
|
|
47
|
+
duration: 200,
|
|
48
|
+
});
|
|
49
|
+
}, [checked, checkedValue]);
|
|
50
|
+
|
|
51
|
+
const checkboxAnimStyle = useAnimatedStyle(() => {
|
|
52
|
+
return {
|
|
53
|
+
borderColor: interpolateColor(
|
|
54
|
+
checkedValue.value,
|
|
55
|
+
[0, 1],
|
|
56
|
+
[colors.border, colors.foreground]
|
|
57
|
+
),
|
|
58
|
+
backgroundColor: interpolateColor(
|
|
59
|
+
checkedValue.value,
|
|
60
|
+
[0, 1],
|
|
61
|
+
[colors.background, colors.foreground]
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const checkIconAnimStyle = useAnimatedStyle(() => {
|
|
67
|
+
return {
|
|
68
|
+
opacity: checkedValue.value,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const pressedColor = convertHexToRgba(colors.foreground, 0.05);
|
|
73
|
+
|
|
74
|
+
const pressableAnimStyle = useAnimatedStyle(() => {
|
|
75
|
+
return {
|
|
76
|
+
backgroundColor: interpolateColor(
|
|
77
|
+
pressedValue.value,
|
|
78
|
+
[0, 1],
|
|
79
|
+
['transparent', pressedColor]
|
|
80
|
+
),
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const updatePressAnimation = (value: number) => {
|
|
85
|
+
pressedValue.value = withTiming(value, { duration: 200 });
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handlePressIn = () => Platform.OS !== 'web' && updatePressAnimation(1);
|
|
89
|
+
|
|
90
|
+
const handlePressOut = () => Platform.OS !== 'web' && updatePressAnimation(0);
|
|
91
|
+
|
|
92
|
+
const handlePress = () => onCheckedChange(!checked);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Pressable
|
|
96
|
+
style={[style, disabled && styles.disabled]}
|
|
97
|
+
onPress={handlePress}
|
|
98
|
+
onPressIn={handlePressIn}
|
|
99
|
+
onPressOut={handlePressOut}
|
|
100
|
+
onHoverIn={() => updatePressAnimation(1)}
|
|
101
|
+
onHoverOut={() => updatePressAnimation(0)}
|
|
102
|
+
disabled={disabled}
|
|
103
|
+
>
|
|
104
|
+
<Animated.View
|
|
105
|
+
style={[
|
|
106
|
+
styles.container,
|
|
107
|
+
!description && styles.centered,
|
|
108
|
+
{ borderRadius: theme.roundness },
|
|
109
|
+
pressableAnimStyle,
|
|
110
|
+
]}
|
|
111
|
+
>
|
|
112
|
+
<Animated.View
|
|
113
|
+
style={[
|
|
114
|
+
styles.box,
|
|
115
|
+
{ borderRadius: theme.roundness },
|
|
116
|
+
checkboxAnimStyle,
|
|
117
|
+
]}
|
|
118
|
+
>
|
|
119
|
+
<Animated.View style={checkIconAnimStyle}>
|
|
120
|
+
<Check
|
|
121
|
+
size={14}
|
|
122
|
+
color={checked ? colors.background : colors.foreground}
|
|
123
|
+
strokeWidth={3}
|
|
124
|
+
/>
|
|
125
|
+
</Animated.View>
|
|
126
|
+
</Animated.View>
|
|
127
|
+
{(label || description) && (
|
|
128
|
+
<View style={styles.textContainer}>
|
|
129
|
+
<Text variant="h5">{label}</Text>
|
|
130
|
+
{description && (
|
|
131
|
+
<Text variant="ps" style={styles.description}>
|
|
132
|
+
{description}
|
|
133
|
+
</Text>
|
|
134
|
+
)}
|
|
135
|
+
</View>
|
|
136
|
+
)}
|
|
137
|
+
</Animated.View>
|
|
138
|
+
</Pressable>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const styles = StyleSheet.create({
|
|
143
|
+
disabled: { opacity: 0.5 },
|
|
144
|
+
centered: { alignItems: 'center' },
|
|
145
|
+
container: {
|
|
146
|
+
flexDirection: 'row',
|
|
147
|
+
alignItems: 'flex-start',
|
|
148
|
+
padding: 8,
|
|
149
|
+
margin: -8,
|
|
150
|
+
},
|
|
151
|
+
box: {
|
|
152
|
+
width: 24,
|
|
153
|
+
height: 24,
|
|
154
|
+
borderWidth: 2,
|
|
155
|
+
alignItems: 'center',
|
|
156
|
+
justifyContent: 'center',
|
|
157
|
+
},
|
|
158
|
+
textContainer: { marginLeft: 8, flex: 1 },
|
|
159
|
+
description: { marginTop: 2, opacity: 0.5 },
|
|
160
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Pressable, StyleSheet, View, type PressableProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import Text from './Text';
|
|
4
|
+
|
|
5
|
+
import useTheme from '../hooks/useTheme';
|
|
6
|
+
|
|
7
|
+
import { convertHexToRgba } from '../utils/uiUtils';
|
|
8
|
+
|
|
9
|
+
type Props = PressableProps & {
|
|
10
|
+
text: string;
|
|
11
|
+
size?: 'normal' | 'small';
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default function Chip({
|
|
16
|
+
text,
|
|
17
|
+
size = 'normal',
|
|
18
|
+
disabled = false,
|
|
19
|
+
...rest
|
|
20
|
+
}: Props) {
|
|
21
|
+
const { theme, colors } = useTheme();
|
|
22
|
+
|
|
23
|
+
const height = size === 'small' ? 24 : 32;
|
|
24
|
+
const textVariant = size === 'small' ? 'ps' : 'pm';
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Pressable disabled={disabled} {...rest}>
|
|
28
|
+
<View
|
|
29
|
+
style={[
|
|
30
|
+
styles.container,
|
|
31
|
+
{
|
|
32
|
+
backgroundColor: convertHexToRgba(colors.foreground, 0.08),
|
|
33
|
+
borderRadius: theme.roundness,
|
|
34
|
+
height,
|
|
35
|
+
},
|
|
36
|
+
disabled && styles.disabled,
|
|
37
|
+
]}
|
|
38
|
+
>
|
|
39
|
+
<Text
|
|
40
|
+
variant={textVariant}
|
|
41
|
+
style={{ color: colors.foreground }}
|
|
42
|
+
numberOfLines={1}
|
|
43
|
+
>
|
|
44
|
+
{text}
|
|
45
|
+
</Text>
|
|
46
|
+
</View>
|
|
47
|
+
</Pressable>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const styles = StyleSheet.create({
|
|
52
|
+
container: {
|
|
53
|
+
justifyContent: 'center',
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
paddingHorizontal: 12,
|
|
56
|
+
},
|
|
57
|
+
disabled: { opacity: 0.3 },
|
|
58
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, Alert, Clipboard, View } from 'react-native';
|
|
3
|
+
import Icon from './Icon';
|
|
4
|
+
import Text from './Text';
|
|
5
|
+
import useTheme from '../hooks/useTheme';
|
|
6
|
+
|
|
7
|
+
interface CopyToClipboardProps {
|
|
8
|
+
text: string;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
onCopy?: () => void;
|
|
11
|
+
showFeedback?: boolean;
|
|
12
|
+
style?: any;
|
|
13
|
+
iconColor?: string; // Color personalizado para el ícono
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const CopyToClipboard: React.FC<CopyToClipboardProps> = ({
|
|
17
|
+
text,
|
|
18
|
+
children,
|
|
19
|
+
onCopy,
|
|
20
|
+
showFeedback = true,
|
|
21
|
+
style,
|
|
22
|
+
iconColor,
|
|
23
|
+
}) => {
|
|
24
|
+
const [copied, setCopied] = useState(false);
|
|
25
|
+
const { colors } = useTheme();
|
|
26
|
+
|
|
27
|
+
const handleCopy = () => {
|
|
28
|
+
try {
|
|
29
|
+
Clipboard.setString(text);
|
|
30
|
+
setCopied(true);
|
|
31
|
+
|
|
32
|
+
if (showFeedback) {
|
|
33
|
+
Alert.alert('Copied!', `"${text}" has been copied to clipboard`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onCopy?.();
|
|
37
|
+
|
|
38
|
+
// Use setTimeout with reference for better control
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
setCopied(false);
|
|
41
|
+
}, 2500);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
Alert.alert('Error', 'Failed to copy to clipboard');
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Pressable
|
|
49
|
+
style={[
|
|
50
|
+
styles.container,
|
|
51
|
+
{
|
|
52
|
+
backgroundColor: colors.altBackground,
|
|
53
|
+
borderColor: colors.border,
|
|
54
|
+
},
|
|
55
|
+
style,
|
|
56
|
+
]}
|
|
57
|
+
onPress={handleCopy}
|
|
58
|
+
>
|
|
59
|
+
<View style={styles.content}>
|
|
60
|
+
{children || (
|
|
61
|
+
<Text
|
|
62
|
+
variant="pm"
|
|
63
|
+
style={[styles.text, { color: colors.foreground }]}
|
|
64
|
+
>
|
|
65
|
+
{text}
|
|
66
|
+
</Text>
|
|
67
|
+
)}
|
|
68
|
+
</View>
|
|
69
|
+
<View style={styles.iconContainer}>
|
|
70
|
+
<Icon
|
|
71
|
+
name={copied ? 'Check' : 'Copy'}
|
|
72
|
+
size={16}
|
|
73
|
+
color={iconColor || (copied ? colors.primary : colors.foreground)}
|
|
74
|
+
/>
|
|
75
|
+
</View>
|
|
76
|
+
</Pressable>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const styles = StyleSheet.create({
|
|
81
|
+
container: {
|
|
82
|
+
flexDirection: 'row',
|
|
83
|
+
alignItems: 'flex-start',
|
|
84
|
+
padding: 16,
|
|
85
|
+
borderRadius: 6,
|
|
86
|
+
borderWidth: 1,
|
|
87
|
+
minHeight: 48,
|
|
88
|
+
maxWidth: '100%', // Asegura que no se extienda más allá del contenedor padre
|
|
89
|
+
},
|
|
90
|
+
content: {
|
|
91
|
+
flex: 1,
|
|
92
|
+
marginRight: 12, // Aumenté para dar más espacio al ícono
|
|
93
|
+
minWidth: 0, // Permite que el contenido se comprima si es necesario
|
|
94
|
+
overflow: 'hidden', // Evita que el contenido se desborde
|
|
95
|
+
},
|
|
96
|
+
text: {
|
|
97
|
+
// Estilo para el texto por defecto cuando no hay children
|
|
98
|
+
},
|
|
99
|
+
iconContainer: {
|
|
100
|
+
flexShrink: 0, // Evita que el ícono se comprima
|
|
101
|
+
flexBasis: 16, // Base fija para el ícono
|
|
102
|
+
width: 16, // Ancho fijo del ícono
|
|
103
|
+
height: 16, // Alto fijo del ícono
|
|
104
|
+
justifyContent: 'center',
|
|
105
|
+
alignItems: 'center',
|
|
106
|
+
marginTop: 2, // Alineación con la primera línea del texto
|
|
107
|
+
},
|
|
108
|
+
});
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useState,
|
|
4
|
+
useCallback,
|
|
5
|
+
type PropsWithChildren,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import {
|
|
9
|
+
Modal,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
TouchableWithoutFeedback,
|
|
12
|
+
View,
|
|
13
|
+
type StyleProp,
|
|
14
|
+
type ViewStyle,
|
|
15
|
+
} from 'react-native';
|
|
16
|
+
import Animated, {
|
|
17
|
+
useSharedValue,
|
|
18
|
+
withTiming,
|
|
19
|
+
useAnimatedStyle,
|
|
20
|
+
runOnJS,
|
|
21
|
+
} from 'react-native-reanimated';
|
|
22
|
+
|
|
23
|
+
import useTheme from '../hooks/useTheme';
|
|
24
|
+
import { convertHexToRgba } from '../utils/uiUtils';
|
|
25
|
+
import Text from './Text';
|
|
26
|
+
import Button from './Button';
|
|
27
|
+
|
|
28
|
+
// Componente principal Dialog
|
|
29
|
+
interface DialogProps extends PropsWithChildren {
|
|
30
|
+
visible: boolean;
|
|
31
|
+
onDismiss: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const Dialog = ({ children, visible, onDismiss }: DialogProps) => {
|
|
35
|
+
const { colors, theme } = useTheme();
|
|
36
|
+
const [internalVisible, setInternalVisible] = useState(false);
|
|
37
|
+
|
|
38
|
+
const scale = useSharedValue(0);
|
|
39
|
+
const opacity = useSharedValue(0);
|
|
40
|
+
|
|
41
|
+
const runCloseAnimation = useCallback(() => {
|
|
42
|
+
scale.value = withTiming(0.7, { duration: 300 });
|
|
43
|
+
opacity.value = withTiming(0, { duration: 300 }, () => {
|
|
44
|
+
runOnJS(setInternalVisible)(false);
|
|
45
|
+
});
|
|
46
|
+
}, [scale, opacity]);
|
|
47
|
+
|
|
48
|
+
// Sync external visible prop with internal state
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (visible) {
|
|
51
|
+
setInternalVisible(true);
|
|
52
|
+
} else if (internalVisible) {
|
|
53
|
+
// Only start close animation if dialog is currently visible
|
|
54
|
+
runCloseAnimation();
|
|
55
|
+
}
|
|
56
|
+
}, [visible, internalVisible, runCloseAnimation]);
|
|
57
|
+
|
|
58
|
+
// Start open animation when internal state becomes true
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (internalVisible && visible) {
|
|
61
|
+
scale.value = withTiming(1, { duration: 300 });
|
|
62
|
+
opacity.value = withTiming(1, { duration: 300 });
|
|
63
|
+
}
|
|
64
|
+
}, [internalVisible, visible, scale, opacity]);
|
|
65
|
+
|
|
66
|
+
const animatedOverlayStyle = useAnimatedStyle(() => ({
|
|
67
|
+
opacity: opacity.value,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
const animatedContentStyle = useAnimatedStyle(() => ({
|
|
71
|
+
transform: [{ scale: scale.value }],
|
|
72
|
+
opacity: opacity.value,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
const handleBackdropPress = () => {
|
|
76
|
+
onDismiss();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Modal transparent visible={internalVisible} statusBarTranslucent>
|
|
81
|
+
<TouchableWithoutFeedback onPress={handleBackdropPress}>
|
|
82
|
+
<Animated.View style={[styles.overlay, animatedOverlayStyle]}>
|
|
83
|
+
<TouchableWithoutFeedback>
|
|
84
|
+
<Animated.View
|
|
85
|
+
style={[
|
|
86
|
+
styles.content,
|
|
87
|
+
{
|
|
88
|
+
backgroundColor: colors.background,
|
|
89
|
+
borderRadius: theme.roundness,
|
|
90
|
+
borderColor: colors.border,
|
|
91
|
+
},
|
|
92
|
+
animatedContentStyle,
|
|
93
|
+
]}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</Animated.View>
|
|
97
|
+
</TouchableWithoutFeedback>
|
|
98
|
+
</Animated.View>
|
|
99
|
+
</TouchableWithoutFeedback>
|
|
100
|
+
</Modal>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Dialog Header
|
|
105
|
+
interface DialogHeaderProps extends PropsWithChildren {
|
|
106
|
+
style?: StyleProp<ViewStyle>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const DialogHeader = ({ children, style }: DialogHeaderProps) => {
|
|
110
|
+
return <View style={[styles.header, style]}>{children}</View>;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Dialog Title
|
|
114
|
+
interface DialogTitleProps {
|
|
115
|
+
children: ReactNode;
|
|
116
|
+
style?: StyleProp<ViewStyle>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const DialogTitle = ({ children, style }: DialogTitleProps) => {
|
|
120
|
+
return (
|
|
121
|
+
<Text variant="h3" style={[styles.title, style]}>
|
|
122
|
+
{children}
|
|
123
|
+
</Text>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Dialog Description
|
|
128
|
+
interface DialogDescriptionProps {
|
|
129
|
+
children: ReactNode;
|
|
130
|
+
style?: StyleProp<ViewStyle>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const DialogDescription = ({ children, style }: DialogDescriptionProps) => {
|
|
134
|
+
const { colors } = useTheme();
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Text
|
|
138
|
+
variant="pm"
|
|
139
|
+
style={[
|
|
140
|
+
styles.description,
|
|
141
|
+
{ color: convertHexToRgba(colors.foreground, 0.7) },
|
|
142
|
+
style,
|
|
143
|
+
]}
|
|
144
|
+
>
|
|
145
|
+
{children}
|
|
146
|
+
</Text>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Dialog Footer
|
|
151
|
+
interface DialogFooterProps extends PropsWithChildren {
|
|
152
|
+
style?: StyleProp<ViewStyle>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const DialogFooter = ({ children, style }: DialogFooterProps) => {
|
|
156
|
+
return <View style={[styles.footer, style]}>{children}</View>;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Dialog Action (botón de acción)
|
|
160
|
+
interface DialogActionProps {
|
|
161
|
+
onPress: () => void;
|
|
162
|
+
children: ReactNode;
|
|
163
|
+
variant?: 'flat' | 'outline';
|
|
164
|
+
destructive?: boolean;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const DialogAction = ({
|
|
168
|
+
onPress,
|
|
169
|
+
children,
|
|
170
|
+
variant = 'flat',
|
|
171
|
+
destructive = false,
|
|
172
|
+
}: DialogActionProps) => {
|
|
173
|
+
const { colors } = useTheme();
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<Button
|
|
177
|
+
text={children as string}
|
|
178
|
+
variant={destructive ? 'destructive' : variant}
|
|
179
|
+
size="large"
|
|
180
|
+
onPress={onPress}
|
|
181
|
+
style={[
|
|
182
|
+
styles.actionButton,
|
|
183
|
+
destructive && { borderColor: colors.destructive },
|
|
184
|
+
]}
|
|
185
|
+
/>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Dialog Cancel (botón de cancelar)
|
|
190
|
+
interface DialogCancelProps {
|
|
191
|
+
onPress: () => void;
|
|
192
|
+
children: ReactNode;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const DialogCancel = ({ onPress, children }: DialogCancelProps) => {
|
|
196
|
+
return (
|
|
197
|
+
<Button
|
|
198
|
+
text={children as string}
|
|
199
|
+
variant="outline"
|
|
200
|
+
size="large"
|
|
201
|
+
onPress={onPress}
|
|
202
|
+
style={styles.cancelButton}
|
|
203
|
+
/>
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const styles = StyleSheet.create({
|
|
208
|
+
overlay: {
|
|
209
|
+
flex: 1,
|
|
210
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
211
|
+
justifyContent: 'center',
|
|
212
|
+
alignItems: 'center',
|
|
213
|
+
padding: 20,
|
|
214
|
+
},
|
|
215
|
+
content: {
|
|
216
|
+
width: '100%',
|
|
217
|
+
maxWidth: 400,
|
|
218
|
+
borderWidth: 1,
|
|
219
|
+
shadowColor: '#000',
|
|
220
|
+
shadowOffset: {
|
|
221
|
+
width: 0,
|
|
222
|
+
height: 4,
|
|
223
|
+
},
|
|
224
|
+
shadowOpacity: 0.25,
|
|
225
|
+
shadowRadius: 12,
|
|
226
|
+
elevation: 8,
|
|
227
|
+
},
|
|
228
|
+
header: {
|
|
229
|
+
padding: 20,
|
|
230
|
+
paddingBottom: 12,
|
|
231
|
+
},
|
|
232
|
+
title: {
|
|
233
|
+
marginBottom: 8,
|
|
234
|
+
},
|
|
235
|
+
description: {
|
|
236
|
+
lineHeight: 20,
|
|
237
|
+
},
|
|
238
|
+
footer: {
|
|
239
|
+
flexDirection: 'row',
|
|
240
|
+
justifyContent: 'flex-end',
|
|
241
|
+
gap: 12,
|
|
242
|
+
padding: 20,
|
|
243
|
+
paddingTop: 12,
|
|
244
|
+
},
|
|
245
|
+
actionButton: {
|
|
246
|
+
flex: 1,
|
|
247
|
+
minWidth: 80,
|
|
248
|
+
},
|
|
249
|
+
cancelButton: {
|
|
250
|
+
flex: 1,
|
|
251
|
+
minWidth: 80,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
export default Dialog;
|
|
256
|
+
export {
|
|
257
|
+
DialogHeader,
|
|
258
|
+
DialogTitle,
|
|
259
|
+
DialogDescription,
|
|
260
|
+
DialogFooter,
|
|
261
|
+
DialogAction,
|
|
262
|
+
DialogCancel,
|
|
263
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type PropsWithChildren } from 'react';
|
|
2
|
+
import { StyleSheet, View, type ViewStyle, type StyleProp } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import Text from './Text';
|
|
5
|
+
import IconButton from './IconButton';
|
|
6
|
+
import { type IconName } from './Icon';
|
|
7
|
+
|
|
8
|
+
import useTheme from '../hooks/useTheme';
|
|
9
|
+
|
|
10
|
+
import meassures from '../theme/meassures';
|
|
11
|
+
|
|
12
|
+
type HeaderButtonProps = {
|
|
13
|
+
iconName: IconName;
|
|
14
|
+
onPress?: () => void;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Props = PropsWithChildren & {
|
|
19
|
+
title: string;
|
|
20
|
+
leftButton?: HeaderButtonProps;
|
|
21
|
+
rightButton?: HeaderButtonProps;
|
|
22
|
+
style?: StyleProp<ViewStyle>;
|
|
23
|
+
useInsets?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const HEADER_BASE_HEIGHT = 52;
|
|
27
|
+
|
|
28
|
+
export default function Header({
|
|
29
|
+
title,
|
|
30
|
+
leftButton,
|
|
31
|
+
rightButton,
|
|
32
|
+
style,
|
|
33
|
+
useInsets = true,
|
|
34
|
+
children,
|
|
35
|
+
}: Props) {
|
|
36
|
+
const { colors, theme } = useTheme();
|
|
37
|
+
const topInset = useInsets ? theme.insets?.top || 0 : 0;
|
|
38
|
+
|
|
39
|
+
const renderButton = (button: HeaderButtonProps) => {
|
|
40
|
+
return (
|
|
41
|
+
<IconButton
|
|
42
|
+
iconName={button.iconName}
|
|
43
|
+
variant="transparent"
|
|
44
|
+
size="medium"
|
|
45
|
+
onPress={button.onPress}
|
|
46
|
+
disabled={button.disabled}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<View
|
|
53
|
+
style={[
|
|
54
|
+
styles.header,
|
|
55
|
+
{
|
|
56
|
+
backgroundColor: colors.background,
|
|
57
|
+
borderBottomColor: colors.border,
|
|
58
|
+
paddingTop: topInset,
|
|
59
|
+
},
|
|
60
|
+
!!children && styles.paddingBottom,
|
|
61
|
+
style,
|
|
62
|
+
]}
|
|
63
|
+
>
|
|
64
|
+
<View style={styles.headerContent}>
|
|
65
|
+
<View style={[styles.slot, styles.leftSlot]}>
|
|
66
|
+
{leftButton && renderButton(leftButton)}
|
|
67
|
+
</View>
|
|
68
|
+
<View style={styles.titleContainer}>
|
|
69
|
+
<Text
|
|
70
|
+
variant="h3"
|
|
71
|
+
style={[styles.title, { color: colors.foreground }]}
|
|
72
|
+
numberOfLines={1}
|
|
73
|
+
ellipsizeMode="tail"
|
|
74
|
+
>
|
|
75
|
+
{title}
|
|
76
|
+
</Text>
|
|
77
|
+
</View>
|
|
78
|
+
<View style={[styles.slot, styles.rightSlot]}>
|
|
79
|
+
{rightButton && renderButton(rightButton)}
|
|
80
|
+
</View>
|
|
81
|
+
</View>
|
|
82
|
+
{children}
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const styles = StyleSheet.create({
|
|
88
|
+
header: { borderBottomWidth: 1, paddingHorizontal: 16 },
|
|
89
|
+
headerContent: {
|
|
90
|
+
height: HEADER_BASE_HEIGHT,
|
|
91
|
+
flexDirection: 'row',
|
|
92
|
+
alignItems: 'center',
|
|
93
|
+
},
|
|
94
|
+
slot: { width: meassures.button.medium, justifyContent: 'center' },
|
|
95
|
+
leftSlot: { alignItems: 'flex-start' },
|
|
96
|
+
titleContainer: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
|
97
|
+
title: { textAlign: 'center', marginHorizontal: 8 },
|
|
98
|
+
rightSlot: { alignItems: 'flex-end' },
|
|
99
|
+
paddingBottom: { paddingBottom: 16 },
|
|
100
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import { icons } from 'lucide-react-native';
|
|
3
|
+
|
|
4
|
+
import useTheme from '../hooks/useTheme';
|
|
5
|
+
|
|
6
|
+
export type IconName = keyof typeof icons;
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
style?: StyleProp<ViewStyle>;
|
|
10
|
+
name: IconName;
|
|
11
|
+
size?: number;
|
|
12
|
+
color?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default function Icon({ name, size = 24, color, style }: Props) {
|
|
16
|
+
const { colors } = useTheme();
|
|
17
|
+
|
|
18
|
+
const IconComponent = icons[name];
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<IconComponent
|
|
22
|
+
size={size}
|
|
23
|
+
color={color ?? colors.foreground}
|
|
24
|
+
style={style}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|