@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.
Files changed (155) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +148 -0
  3. package/lib/module/components/Accordion.js +242 -0
  4. package/lib/module/components/Accordion.js.map +1 -0
  5. package/lib/module/components/BottomSheet.js +183 -0
  6. package/lib/module/components/BottomSheet.js.map +1 -0
  7. package/lib/module/components/Button.js +64 -0
  8. package/lib/module/components/Button.js.map +1 -0
  9. package/lib/module/components/ButtonContainer.js +118 -0
  10. package/lib/module/components/ButtonContainer.js.map +1 -0
  11. package/lib/module/components/CalendarPicker.js +374 -0
  12. package/lib/module/components/CalendarPicker.js.map +1 -0
  13. package/lib/module/components/Card.js +43 -0
  14. package/lib/module/components/Card.js.map +1 -0
  15. package/lib/module/components/Checkbox.js +122 -0
  16. package/lib/module/components/Checkbox.js.map +1 -0
  17. package/lib/module/components/Chip.js +50 -0
  18. package/lib/module/components/Chip.js.map +1 -0
  19. package/lib/module/components/CopyToClipboard.js +98 -0
  20. package/lib/module/components/CopyToClipboard.js.map +1 -0
  21. package/lib/module/components/Dialog.js +232 -0
  22. package/lib/module/components/Dialog.js.map +1 -0
  23. package/lib/module/components/Header.js +94 -0
  24. package/lib/module/components/Header.js.map +1 -0
  25. package/lib/module/components/Icon.js +22 -0
  26. package/lib/module/components/Icon.js.map +1 -0
  27. package/lib/module/components/IconButton.js +57 -0
  28. package/lib/module/components/IconButton.js.map +1 -0
  29. package/lib/module/components/Input.js +111 -0
  30. package/lib/module/components/Input.js.map +1 -0
  31. package/lib/module/components/InputContainer.js +104 -0
  32. package/lib/module/components/InputContainer.js.map +1 -0
  33. package/lib/module/components/LoadingIndicator.js +62 -0
  34. package/lib/module/components/LoadingIndicator.js.map +1 -0
  35. package/lib/module/components/OtpInput.js +85 -0
  36. package/lib/module/components/OtpInput.js.map +1 -0
  37. package/lib/module/components/OtpInputContainer.js +148 -0
  38. package/lib/module/components/OtpInputContainer.js.map +1 -0
  39. package/lib/module/components/Select.js +189 -0
  40. package/lib/module/components/Select.js.map +1 -0
  41. package/lib/module/components/Switch.js +74 -0
  42. package/lib/module/components/Switch.js.map +1 -0
  43. package/lib/module/components/Tabs.js +99 -0
  44. package/lib/module/components/Tabs.js.map +1 -0
  45. package/lib/module/components/Text.js +66 -0
  46. package/lib/module/components/Text.js.map +1 -0
  47. package/lib/module/components/TextArea.js +106 -0
  48. package/lib/module/components/TextArea.js.map +1 -0
  49. package/lib/module/hooks/useTheme.js +20 -0
  50. package/lib/module/hooks/useTheme.js.map +1 -0
  51. package/lib/module/index.js +27 -0
  52. package/lib/module/index.js.map +1 -0
  53. package/lib/module/package.json +1 -0
  54. package/lib/module/store/themeStore.js +50 -0
  55. package/lib/module/store/themeStore.js.map +1 -0
  56. package/lib/module/theme/colors.js +25 -0
  57. package/lib/module/theme/colors.js.map +1 -0
  58. package/lib/module/theme/meassures.js +10 -0
  59. package/lib/module/theme/meassures.js.map +1 -0
  60. package/lib/module/utils/stringUtils.js +12 -0
  61. package/lib/module/utils/stringUtils.js.map +1 -0
  62. package/lib/module/utils/uiUtils.js +63 -0
  63. package/lib/module/utils/uiUtils.js.map +1 -0
  64. package/lib/typescript/package.json +1 -0
  65. package/lib/typescript/src/components/Accordion.d.ts +22 -0
  66. package/lib/typescript/src/components/Accordion.d.ts.map +1 -0
  67. package/lib/typescript/src/components/BottomSheet.d.ts +13 -0
  68. package/lib/typescript/src/components/BottomSheet.d.ts.map +1 -0
  69. package/lib/typescript/src/components/Button.d.ts +16 -0
  70. package/lib/typescript/src/components/Button.d.ts.map +1 -0
  71. package/lib/typescript/src/components/ButtonContainer.d.ts +30 -0
  72. package/lib/typescript/src/components/ButtonContainer.d.ts.map +1 -0
  73. package/lib/typescript/src/components/CalendarPicker.d.ts +19 -0
  74. package/lib/typescript/src/components/CalendarPicker.d.ts.map +1 -0
  75. package/lib/typescript/src/components/Card.d.ts +7 -0
  76. package/lib/typescript/src/components/Card.d.ts.map +1 -0
  77. package/lib/typescript/src/components/Checkbox.d.ts +11 -0
  78. package/lib/typescript/src/components/Checkbox.d.ts.map +1 -0
  79. package/lib/typescript/src/components/Chip.d.ts +9 -0
  80. package/lib/typescript/src/components/Chip.d.ts.map +1 -0
  81. package/lib/typescript/src/components/CopyToClipboard.d.ts +12 -0
  82. package/lib/typescript/src/components/CopyToClipboard.d.ts.map +1 -0
  83. package/lib/typescript/src/components/Dialog.d.ts +40 -0
  84. package/lib/typescript/src/components/Dialog.d.ts.map +1 -0
  85. package/lib/typescript/src/components/Header.d.ts +18 -0
  86. package/lib/typescript/src/components/Header.d.ts.map +1 -0
  87. package/lib/typescript/src/components/Icon.d.ts +12 -0
  88. package/lib/typescript/src/components/Icon.d.ts.map +1 -0
  89. package/lib/typescript/src/components/IconButton.d.ts +13 -0
  90. package/lib/typescript/src/components/IconButton.d.ts.map +1 -0
  91. package/lib/typescript/src/components/Input.d.ts +17 -0
  92. package/lib/typescript/src/components/Input.d.ts.map +1 -0
  93. package/lib/typescript/src/components/InputContainer.d.ts +22 -0
  94. package/lib/typescript/src/components/InputContainer.d.ts.map +1 -0
  95. package/lib/typescript/src/components/LoadingIndicator.d.ts +9 -0
  96. package/lib/typescript/src/components/LoadingIndicator.d.ts.map +1 -0
  97. package/lib/typescript/src/components/OtpInput.d.ts +3 -0
  98. package/lib/typescript/src/components/OtpInput.d.ts.map +1 -0
  99. package/lib/typescript/src/components/OtpInputContainer.d.ts +17 -0
  100. package/lib/typescript/src/components/OtpInputContainer.d.ts.map +1 -0
  101. package/lib/typescript/src/components/Select.d.ts +20 -0
  102. package/lib/typescript/src/components/Select.d.ts.map +1 -0
  103. package/lib/typescript/src/components/Switch.d.ts +10 -0
  104. package/lib/typescript/src/components/Switch.d.ts.map +1 -0
  105. package/lib/typescript/src/components/Tabs.d.ts +14 -0
  106. package/lib/typescript/src/components/Tabs.d.ts.map +1 -0
  107. package/lib/typescript/src/components/Text.d.ts +7 -0
  108. package/lib/typescript/src/components/Text.d.ts.map +1 -0
  109. package/lib/typescript/src/components/TextArea.d.ts +16 -0
  110. package/lib/typescript/src/components/TextArea.d.ts.map +1 -0
  111. package/lib/typescript/src/hooks/useTheme.d.ts +9 -0
  112. package/lib/typescript/src/hooks/useTheme.d.ts.map +1 -0
  113. package/lib/typescript/src/index.d.ts +27 -0
  114. package/lib/typescript/src/index.d.ts.map +1 -0
  115. package/lib/typescript/src/store/themeStore.d.ts +32 -0
  116. package/lib/typescript/src/store/themeStore.d.ts.map +1 -0
  117. package/lib/typescript/src/theme/colors.d.ts +14 -0
  118. package/lib/typescript/src/theme/colors.d.ts.map +1 -0
  119. package/lib/typescript/src/theme/meassures.d.ts +9 -0
  120. package/lib/typescript/src/theme/meassures.d.ts.map +1 -0
  121. package/lib/typescript/src/utils/stringUtils.d.ts +7 -0
  122. package/lib/typescript/src/utils/stringUtils.d.ts.map +1 -0
  123. package/lib/typescript/src/utils/uiUtils.d.ts +21 -0
  124. package/lib/typescript/src/utils/uiUtils.d.ts.map +1 -0
  125. package/package.json +173 -0
  126. package/src/components/Accordion.tsx +284 -0
  127. package/src/components/BottomSheet.tsx +259 -0
  128. package/src/components/Button.tsx +85 -0
  129. package/src/components/ButtonContainer.tsx +161 -0
  130. package/src/components/CalendarPicker.tsx +428 -0
  131. package/src/components/Card.tsx +55 -0
  132. package/src/components/Checkbox.tsx +160 -0
  133. package/src/components/Chip.tsx +58 -0
  134. package/src/components/CopyToClipboard.tsx +108 -0
  135. package/src/components/Dialog.tsx +263 -0
  136. package/src/components/Header.tsx +100 -0
  137. package/src/components/Icon.tsx +27 -0
  138. package/src/components/IconButton.tsx +71 -0
  139. package/src/components/Input.tsx +144 -0
  140. package/src/components/InputContainer.tsx +134 -0
  141. package/src/components/LoadingIndicator.tsx +78 -0
  142. package/src/components/OtpInput.tsx +109 -0
  143. package/src/components/OtpInputContainer.tsx +196 -0
  144. package/src/components/Select.tsx +219 -0
  145. package/src/components/Switch.tsx +104 -0
  146. package/src/components/Tabs.tsx +117 -0
  147. package/src/components/Text.tsx +64 -0
  148. package/src/components/TextArea.tsx +141 -0
  149. package/src/hooks/useTheme.tsx +23 -0
  150. package/src/index.tsx +38 -0
  151. package/src/store/themeStore.ts +57 -0
  152. package/src/theme/colors.ts +35 -0
  153. package/src/theme/meassures.ts +7 -0
  154. package/src/utils/stringUtils.ts +16 -0
  155. package/src/utils/uiUtils.ts +70 -0
@@ -0,0 +1,196 @@
1
+ import { forwardRef, useCallback, useRef, useState } from 'react';
2
+ import { Platform, StyleSheet } from 'react-native';
3
+ import {
4
+ View,
5
+ Keyboard,
6
+ type TextInputKeyPressEventData,
7
+ type NativeSyntheticEvent,
8
+ type ViewProps,
9
+ type TextInputProps,
10
+ type TextInput,
11
+ } from 'react-native';
12
+ import { OtpInput } from './OtpInput';
13
+ import useTheme from '../hooks/useTheme';
14
+ export type OtpInputContainerRef = {
15
+ clear: () => void;
16
+ };
17
+
18
+ type OtpInputContainerProps = {
19
+ inputProps?: Omit<
20
+ TextInputProps,
21
+ | 'style'
22
+ | 'onChangeText'
23
+ | 'onKeyPress'
24
+ | 'autoComplete'
25
+ | 'keyboardType'
26
+ | 'autoFocus'
27
+ >;
28
+ inputStyle?: TextInputProps['style'];
29
+ containerProps?: Omit<ViewProps, 'style'>;
30
+ containerStyle?: ViewProps['style'];
31
+ length?: number;
32
+ onFillEnded?: (otp: string) => void;
33
+ autoFocus?: boolean;
34
+ editable?: boolean;
35
+ };
36
+
37
+ export const OtpInputContainer = forwardRef<
38
+ OtpInputContainerRef,
39
+ OtpInputContainerProps
40
+ >(
41
+ (
42
+ {
43
+ length = 4,
44
+ inputProps,
45
+ inputStyle,
46
+ containerProps,
47
+ containerStyle,
48
+ onFillEnded,
49
+ autoFocus = true,
50
+ editable = true,
51
+ },
52
+ _
53
+ ) => {
54
+ const isIOS = Platform.OS === 'ios';
55
+ const pins = Array.from({ length }).map((__, i) => i);
56
+ const inputRefs = useRef<TextInput[]>([]);
57
+ const pinsValues = useRef<string[]>([]);
58
+ const iosOTP = useRef<{
59
+ key: string;
60
+ index: number | null;
61
+ }>({ key: '', index: null });
62
+ const timeoutRefs = useRef<ReturnType<typeof setTimeout>[]>([]);
63
+
64
+ const [maskedPins, setMaskedPins] = useState<boolean[]>(
65
+ new Array(length).fill(false)
66
+ );
67
+
68
+ const { colors } = useTheme();
69
+
70
+ const maskPin = useCallback((index: number) => {
71
+ setMaskedPins((prev) => {
72
+ const newMasked = [...prev];
73
+ newMasked[index] = true;
74
+ return newMasked;
75
+ });
76
+ }, []);
77
+
78
+ const handleOTP = useCallback(
79
+ (otp: string): boolean => {
80
+ const regexp = new RegExp(`[0-9]{${length}}`);
81
+ const otps = otp.match(regexp);
82
+ if (otps?.length) {
83
+ const otpSplits = otp.split('');
84
+ otpSplits.forEach((otpSplit, i) =>
85
+ inputRefs?.current[i]?.setNativeProps({ text: otpSplit })
86
+ );
87
+ onFillEnded?.(otp);
88
+ iosOTP.current = { key: '', index: null };
89
+ Keyboard.dismiss();
90
+ return true;
91
+ }
92
+ return false;
93
+ },
94
+ [length, onFillEnded]
95
+ );
96
+
97
+ const handleChangeText = useCallback(
98
+ async (text: string, index: number) => {
99
+ pinsValues.current[index] = text;
100
+
101
+ if (timeoutRefs.current[index]) {
102
+ clearTimeout(timeoutRefs.current[index]);
103
+ }
104
+
105
+ setMaskedPins((prev) => {
106
+ const newMasked = [...prev];
107
+ newMasked[index] = false;
108
+ return newMasked;
109
+ });
110
+
111
+ timeoutRefs.current[index] = setTimeout(() => {
112
+ maskPin(index);
113
+ }, 500);
114
+
115
+ if (index + 1 <= pins.length - 1) {
116
+ inputRefs?.current[index + 1]?.focus();
117
+ } else {
118
+ onFillEnded?.(pinsValues.current.join(''));
119
+ Keyboard.dismiss();
120
+ }
121
+ },
122
+ [maskPin, pins.length, onFillEnded]
123
+ );
124
+
125
+ const onKeyPress = useCallback(
126
+ (
127
+ event: NativeSyntheticEvent<TextInputKeyPressEventData>,
128
+ index: number
129
+ ) => {
130
+ event.persist();
131
+ if (isIOS && Number.isInteger(Number(event.nativeEvent.key))) {
132
+ if (iosOTP.current.index === null) {
133
+ iosOTP.current = { key: event.nativeEvent.key, index };
134
+ } else {
135
+ if (iosOTP.current.index === index) {
136
+ iosOTP.current = {
137
+ key: `${iosOTP.current.key}${event.nativeEvent.key}`,
138
+ index,
139
+ };
140
+ } else {
141
+ iosOTP.current = { key: '', index: null };
142
+ }
143
+ }
144
+ if (iosOTP.current.key.length === length) {
145
+ handleOTP(iosOTP.current.key);
146
+ return;
147
+ }
148
+ }
149
+
150
+ if (event.nativeEvent.key === 'Backspace') {
151
+ onFillEnded?.('');
152
+ iosOTP.current = { key: '', index: null };
153
+ if (index - 1 >= 0) {
154
+ inputRefs?.current[index - 1]?.focus();
155
+ }
156
+ }
157
+ },
158
+ [handleOTP, length, onFillEnded, isIOS]
159
+ );
160
+
161
+ return (
162
+ <View style={[styles.container, containerStyle]} {...containerProps}>
163
+ {pins.map((pin) => {
164
+ return (
165
+ <OtpInput
166
+ {...inputProps}
167
+ autoFocus={autoFocus && pin === 0}
168
+ ref={(input) => inputRefs?.current.push(input as TextInput)}
169
+ key={pin}
170
+ style={[
171
+ inputStyle,
172
+ { borderColor: colors.border, color: colors.foreground },
173
+ ]}
174
+ onChangeText={(text) => handleChangeText(text, pin)}
175
+ onKeyPress={(event) => onKeyPress(event, pin)}
176
+ autoComplete="sms-otp"
177
+ textContentType="oneTimeCode"
178
+ keyboardType="numeric"
179
+ secureTextEntry={maskedPins[pin]}
180
+ editable={editable}
181
+ />
182
+ );
183
+ })}
184
+ </View>
185
+ );
186
+ }
187
+ );
188
+
189
+ const styles = StyleSheet.create({
190
+ container: {
191
+ display: 'flex',
192
+ flexDirection: 'row',
193
+ alignItems: 'center',
194
+ justifyContent: 'center',
195
+ },
196
+ });
@@ -0,0 +1,219 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+ import {
3
+ View,
4
+ StyleSheet,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ Pressable,
8
+ } from 'react-native';
9
+ import Animated, {
10
+ useSharedValue,
11
+ withTiming,
12
+ useAnimatedStyle,
13
+ interpolateColor,
14
+ } from 'react-native-reanimated';
15
+ import { FlatList } from 'react-native-gesture-handler';
16
+
17
+ import InputContainer from './InputContainer';
18
+ import BottomSheet, { type BottomSheetProps } from './BottomSheet';
19
+ import Text from './Text';
20
+ import Icon from './Icon';
21
+
22
+ import useTheme from '../hooks/useTheme';
23
+
24
+ import { convertHexToRgba } from '../utils/uiUtils';
25
+
26
+ export type SelectOption = {
27
+ value: string;
28
+ label: string;
29
+ };
30
+
31
+ type Props = {
32
+ style?: StyleProp<ViewStyle>;
33
+ value?: SelectOption;
34
+ options: SelectOption[];
35
+ onChange: (option: SelectOption) => void;
36
+ title?: string;
37
+ placeholder?: string;
38
+ label?: string;
39
+ disabled?: boolean;
40
+ error?: boolean;
41
+ hint?: string;
42
+ };
43
+
44
+ const OptionItem = ({
45
+ style,
46
+ option,
47
+ selected,
48
+ onChange,
49
+ }: {
50
+ style?: StyleProp<ViewStyle>;
51
+ option: SelectOption;
52
+ selected: boolean;
53
+ onChange: (option: SelectOption) => void;
54
+ }) => {
55
+ const { theme, colors } = useTheme();
56
+ const pressed = useSharedValue(0);
57
+
58
+ const pressedColor = convertHexToRgba(colors.foreground, 0.1);
59
+
60
+ const animatedPress = useAnimatedStyle(() => ({
61
+ backgroundColor: interpolateColor(
62
+ pressed.value,
63
+ [0, 1],
64
+ [selected ? pressedColor : 'transparent', pressedColor]
65
+ ),
66
+ }));
67
+
68
+ return (
69
+ <Pressable
70
+ key={option.value}
71
+ style={style}
72
+ onPress={() => onChange(option)}
73
+ onPressIn={() => (pressed.value = 1)}
74
+ onPressOut={() => (pressed.value = 0)}
75
+ onHoverIn={() => (pressed.value = 1)}
76
+ onHoverOut={() => (pressed.value = 0)}
77
+ >
78
+ <Animated.View
79
+ style={[
80
+ styles.optionContent,
81
+ { borderRadius: theme.roundness - 2 },
82
+ animatedPress,
83
+ ]}
84
+ >
85
+ <Text
86
+ style={[styles.optionText, { color: colors.foreground }]}
87
+ variant="pm"
88
+ numberOfLines={1}
89
+ >
90
+ {option.label}
91
+ </Text>
92
+ {selected && <Icon name="Check" size={16} color={colors.foreground} />}
93
+ </Animated.View>
94
+ </Pressable>
95
+ );
96
+ };
97
+
98
+ export default function Select({
99
+ style,
100
+ value,
101
+ options,
102
+ onChange,
103
+ placeholder = 'Select',
104
+ label,
105
+ disabled = false,
106
+ error,
107
+ hint,
108
+ title,
109
+ }: Props) {
110
+ const [isOpen, setIsOpen] = useState(false);
111
+ const rotation = useSharedValue(0);
112
+ const { theme, colors } = useTheme();
113
+ const bottomSheetRef = useRef<BottomSheetProps>(null);
114
+
115
+ const handlePress = useCallback(() => {
116
+ if (disabled) return;
117
+ setIsOpen(true);
118
+ rotation.value = withTiming(1, { duration: 200 });
119
+ bottomSheetRef.current?.show();
120
+ }, [disabled, rotation]);
121
+
122
+ const handleClose = useCallback(() => {
123
+ rotation.value = withTiming(0, { duration: 200 });
124
+ setIsOpen(false);
125
+ bottomSheetRef.current?.hide();
126
+ }, [rotation]);
127
+
128
+ const animatedStyle = useAnimatedStyle(() => ({
129
+ transform: [{ rotate: `${rotation.value * 180}deg` }],
130
+ }));
131
+
132
+ const getInputTextColor = useCallback(() => {
133
+ if (disabled) return convertHexToRgba(colors.foreground, 0.5);
134
+ return colors.foreground;
135
+ }, [colors.foreground, disabled]);
136
+
137
+ return (
138
+ <>
139
+ <View style={style}>
140
+ <InputContainer
141
+ onPress={handlePress}
142
+ disabled={disabled}
143
+ focused={isOpen}
144
+ error={error}
145
+ hint={hint}
146
+ label={label}
147
+ >
148
+ <Text
149
+ variant="pm"
150
+ style={[
151
+ styles.selectText,
152
+ {
153
+ color: value
154
+ ? getInputTextColor()
155
+ : convertHexToRgba(colors.foreground, 0.5),
156
+ fontFamily: theme.fonts.regular,
157
+ },
158
+ ]}
159
+ numberOfLines={1}
160
+ >
161
+ {value?.label ?? placeholder}
162
+ </Text>
163
+ <Animated.View style={animatedStyle}>
164
+ <Icon name="ChevronDown" size={16} color={colors.foreground} />
165
+ </Animated.View>
166
+ </InputContainer>
167
+ </View>
168
+ <BottomSheet
169
+ ref={bottomSheetRef}
170
+ fullScreen={options.length > 20}
171
+ onBackdropPress={handleClose}
172
+ >
173
+ {title && (
174
+ <Text style={styles.title} variant="h3">
175
+ {title}
176
+ </Text>
177
+ )}
178
+ <FlatList
179
+ data={options}
180
+ contentContainerStyle={styles.list}
181
+ renderItem={({ item, index }) => (
182
+ <OptionItem
183
+ style={[
184
+ styles.option,
185
+ { borderRadius: theme.roundness - 2 },
186
+ index === options.length - 1 && {
187
+ marginBottom: theme.insets.bottom + 8,
188
+ },
189
+ ]}
190
+ option={item}
191
+ selected={value?.value === item.value}
192
+ onChange={(selectedOption) => {
193
+ onChange(selectedOption);
194
+ handleClose();
195
+ }}
196
+ />
197
+ )}
198
+ />
199
+ </BottomSheet>
200
+ </>
201
+ );
202
+ }
203
+
204
+ const styles = StyleSheet.create({
205
+ selectText: { fontSize: 14, flex: 1 },
206
+ optionContent: {
207
+ paddingVertical: 8,
208
+ justifyContent: 'space-between',
209
+ alignItems: 'center',
210
+ flexDirection: 'row',
211
+ paddingHorizontal: 8,
212
+ },
213
+ optionText: { flex: 1, marginEnd: 16 },
214
+ firstOption: { paddingTop: 4 },
215
+ lastOption: { paddingBottom: 6 },
216
+ option: { paddingBottom: 2 },
217
+ list: { paddingHorizontal: 8 },
218
+ title: { marginBottom: 8, marginHorizontal: 16 },
219
+ });
@@ -0,0 +1,104 @@
1
+ import { useEffect } from 'react';
2
+ import {
3
+ Pressable,
4
+ StyleSheet,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ } from 'react-native';
8
+ import Animated, {
9
+ Easing,
10
+ interpolate,
11
+ interpolateColor,
12
+ useAnimatedStyle,
13
+ useSharedValue,
14
+ withTiming,
15
+ } from 'react-native-reanimated';
16
+
17
+ import useTheme from '../hooks/useTheme';
18
+
19
+ type Props = {
20
+ style?: StyleProp<ViewStyle>;
21
+ value: boolean;
22
+ onValueChange: (value: boolean) => void;
23
+ disabled?: boolean;
24
+ };
25
+
26
+ const SWITCH_WIDTH = 44;
27
+ const SWITCH_HEIGHT = 24;
28
+ const PADDING = 2;
29
+ const THUMB_SIZE = SWITCH_HEIGHT - PADDING * 2;
30
+
31
+ export default function Switch({
32
+ value,
33
+ onValueChange,
34
+ disabled,
35
+ style,
36
+ }: Props) {
37
+ const { colors } = useTheme();
38
+ const active = useSharedValue(value ? 1 : 0);
39
+ const enabled = useSharedValue(disabled ? 0 : 1);
40
+
41
+ useEffect(() => {
42
+ active.value = withTiming(value ? 1 : 0, {
43
+ duration: 300,
44
+ easing: Easing.inOut(Easing.ease),
45
+ });
46
+ }, [value, active]);
47
+
48
+ useEffect(() => {
49
+ enabled.value = withTiming(disabled ? 0 : 1, { duration: 300 });
50
+ }, [disabled, enabled]);
51
+
52
+ const containerAnimatedStyle = useAnimatedStyle(() => {
53
+ return {
54
+ backgroundColor: interpolateColor(
55
+ active.value,
56
+ [0, 1],
57
+ [colors.border, colors.primary]
58
+ ),
59
+ opacity: interpolate(enabled.value, [0, 1], [0.5, 1]),
60
+ };
61
+ });
62
+
63
+ const thumbAnimatedStyle = useAnimatedStyle(() => {
64
+ return {
65
+ transform: [
66
+ {
67
+ translateX: interpolate(
68
+ active.value,
69
+ [0, 1],
70
+ [PADDING, SWITCH_WIDTH - (THUMB_SIZE + PADDING)]
71
+ ),
72
+ },
73
+ ],
74
+ };
75
+ });
76
+
77
+ return (
78
+ <Pressable
79
+ style={style}
80
+ onPress={() => onValueChange(!value)}
81
+ disabled={disabled}
82
+ >
83
+ <Animated.View style={[styles.container, containerAnimatedStyle]}>
84
+ <Animated.View
85
+ style={[
86
+ styles.thumb,
87
+ { backgroundColor: colors.background },
88
+ thumbAnimatedStyle,
89
+ ]}
90
+ />
91
+ </Animated.View>
92
+ </Pressable>
93
+ );
94
+ }
95
+
96
+ const styles = StyleSheet.create({
97
+ container: {
98
+ width: SWITCH_WIDTH,
99
+ height: SWITCH_HEIGHT,
100
+ borderRadius: SWITCH_HEIGHT,
101
+ justifyContent: 'center',
102
+ },
103
+ thumb: { width: THUMB_SIZE, height: THUMB_SIZE, borderRadius: THUMB_SIZE },
104
+ });
@@ -0,0 +1,117 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ View,
4
+ Pressable,
5
+ StyleSheet,
6
+ type ViewStyle,
7
+ type StyleProp,
8
+ } from 'react-native';
9
+ import useTheme from '../hooks/useTheme';
10
+ import { convertHexToRgba } from '../utils/uiUtils';
11
+ import Text from './Text';
12
+ import Animated, {
13
+ useAnimatedStyle,
14
+ useSharedValue,
15
+ withTiming,
16
+ Easing,
17
+ } from 'react-native-reanimated';
18
+
19
+ export type Tab = { value: string; label: string };
20
+
21
+ type Props = {
22
+ style?: StyleProp<ViewStyle>;
23
+ options: Tab[];
24
+ selected: Tab;
25
+ onChange: (value: Tab) => void;
26
+ };
27
+
28
+ export default function Tabs({ options, selected, onChange, style }: Props) {
29
+ const { theme, colors } = useTheme();
30
+ const [width, setWidth] = useState<number>(0);
31
+ const [height, setHeight] = useState<number>(0);
32
+ const index = useSharedValue<number>(0);
33
+ const tabWidth = (width - 8) / options.length;
34
+ const tabHeight = height - 8;
35
+
36
+ const tabAnimStyle = useAnimatedStyle(() => {
37
+ return {
38
+ transform: [{ translateX: index.value * tabWidth + 4 }],
39
+ };
40
+ }, [tabWidth, index]);
41
+
42
+ return (
43
+ <View style={[styles.container, style]}>
44
+ <View
45
+ onLayout={(event) => {
46
+ const { width: w, height: h } = event.nativeEvent.layout;
47
+ setWidth(w);
48
+ setHeight(h);
49
+ }}
50
+ style={[
51
+ styles.tabsList,
52
+ {
53
+ backgroundColor: convertHexToRgba(colors.border, 0.5),
54
+ borderRadius: theme.roundness,
55
+ },
56
+ ]}
57
+ >
58
+ <Animated.View
59
+ style={[
60
+ styles.tab,
61
+ {
62
+ height: tabHeight,
63
+ width: tabWidth,
64
+ backgroundColor: colors.background,
65
+ borderColor: colors.border,
66
+ },
67
+ theme.roundness >= 2 && { borderRadius: theme.roundness - 2 },
68
+ tabAnimStyle,
69
+ ]}
70
+ />
71
+ {options.map((tab, _index) => (
72
+ <Pressable
73
+ key={tab.value}
74
+ style={styles.tabTrigger}
75
+ onPress={() => {
76
+ index.value = withTiming(_index, {
77
+ duration: 250,
78
+ easing: Easing.ease,
79
+ });
80
+ onChange(tab);
81
+ }}
82
+ >
83
+ <Text
84
+ variant="h4"
85
+ numberOfLines={1}
86
+ style={{
87
+ color:
88
+ selected.value === tab.value
89
+ ? colors.foreground
90
+ : convertHexToRgba(colors.foreground, 0.6),
91
+ }}
92
+ >
93
+ {tab.label}
94
+ </Text>
95
+ </Pressable>
96
+ ))}
97
+ </View>
98
+ </View>
99
+ );
100
+ }
101
+
102
+ const styles = StyleSheet.create({
103
+ container: { width: '100%' },
104
+ tabsList: {
105
+ flexDirection: 'row',
106
+ alignItems: 'center',
107
+ height: 40,
108
+ },
109
+ tabTrigger: {
110
+ flex: 1,
111
+ paddingHorizontal: 16,
112
+ alignItems: 'center',
113
+ justifyContent: 'center',
114
+ height: 32,
115
+ },
116
+ tab: { position: 'absolute', top: 4, borderWidth: 1 },
117
+ });
@@ -0,0 +1,64 @@
1
+ import { useMemo } from 'react';
2
+ import { StyleSheet, Text as RNText, type TextProps } from 'react-native';
3
+
4
+ import useTheme from '../hooks/useTheme';
5
+
6
+ type Props = TextProps & {
7
+ variant: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'pl' | 'pm' | 'ps' | 'caption';
8
+ };
9
+
10
+ export default function Text({ variant, style, children, ...props }: Props) {
11
+ const { theme, colors } = useTheme();
12
+
13
+ const color = useMemo(
14
+ () => StyleSheet.flatten(style)?.color ?? colors.foreground,
15
+ [colors.foreground, style]
16
+ );
17
+
18
+ const textStyle = useMemo(() => {
19
+ return {
20
+ h1: {
21
+ fontSize: 24,
22
+ fontFamily: theme.fonts.bold,
23
+ },
24
+ h2: {
25
+ fontSize: 18,
26
+ fontFamily: theme.fonts.medium,
27
+ },
28
+ h3: {
29
+ fontSize: 16,
30
+ fontFamily: theme.fonts.medium,
31
+ },
32
+ h4: {
33
+ fontSize: 14,
34
+ fontFamily: theme.fonts.medium,
35
+ },
36
+ h5: {
37
+ fontSize: 12,
38
+ fontFamily: theme.fonts.medium,
39
+ },
40
+ pl: {
41
+ fontSize: 16,
42
+ fontFamily: theme.fonts.regular,
43
+ },
44
+ pm: {
45
+ fontSize: 14,
46
+ fontFamily: theme.fonts.regular,
47
+ },
48
+ ps: {
49
+ fontSize: 12,
50
+ fontFamily: theme.fonts.regular,
51
+ },
52
+ caption: {
53
+ fontSize: 10,
54
+ fontFamily: theme.fonts.regular,
55
+ },
56
+ }[variant];
57
+ }, [variant, theme.fonts.bold, theme.fonts.medium, theme.fonts.regular]);
58
+
59
+ return (
60
+ <RNText {...props} style={[style, textStyle, { color }]}>
61
+ {children}
62
+ </RNText>
63
+ );
64
+ }