@herca/rn-kit 0.1.26 → 0.1.28

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 (66) hide show
  1. package/lib/module/BottomSheet/BottomSheet.js +4 -1
  2. package/lib/module/BottomSheet/BottomSheet.js.map +1 -1
  3. package/lib/module/Button/tests/Button.test.js +70 -0
  4. package/lib/module/Button/tests/Button.test.js.map +1 -0
  5. package/lib/module/Button/type.js +1 -1
  6. package/lib/module/DatePicker/TimePicker.js +84 -39
  7. package/lib/module/DatePicker/TimePicker.js.map +1 -1
  8. package/lib/module/Icon/Icon.js +7 -1
  9. package/lib/module/Icon/Icon.js.map +1 -1
  10. package/lib/module/Icon/README.md +6 -0
  11. package/lib/module/Icon/icons/Bold.js +25 -0
  12. package/lib/module/Icon/icons/Bold.js.map +1 -0
  13. package/lib/module/Icon/icons/Italic.js +25 -0
  14. package/lib/module/Icon/icons/Italic.js.map +1 -0
  15. package/lib/module/Icon/icons/UnderLine.js +25 -0
  16. package/lib/module/Icon/icons/UnderLine.js.map +1 -0
  17. package/lib/module/Input/InputOtp.js +7 -5
  18. package/lib/module/Input/InputOtp.js.map +1 -1
  19. package/lib/module/Input/README_INPUT_OTP.md +8 -7
  20. package/lib/module/TextEditor/TextEditor.js +206 -4
  21. package/lib/module/TextEditor/TextEditor.js.map +1 -1
  22. package/lib/module/Toast/README.md +32 -16
  23. package/lib/module/Toast/Toast.js +31 -19
  24. package/lib/module/Toast/Toast.js.map +1 -1
  25. package/lib/module/Toast/ToastContext.js +2 -1
  26. package/lib/module/Toast/ToastContext.js.map +1 -1
  27. package/lib/typescript/src/Button/tests/Button.test.d.ts +2 -0
  28. package/lib/typescript/src/Button/tests/Button.test.d.ts.map +1 -0
  29. package/lib/typescript/src/DatePicker/TimePicker.d.ts.map +1 -1
  30. package/lib/typescript/src/Icon/Icon.d.ts.map +1 -1
  31. package/lib/typescript/src/Icon/icons/Bold.d.ts +9 -0
  32. package/lib/typescript/src/Icon/icons/Bold.d.ts.map +1 -0
  33. package/lib/typescript/src/Icon/icons/Italic.d.ts +9 -0
  34. package/lib/typescript/src/Icon/icons/Italic.d.ts.map +1 -0
  35. package/lib/typescript/src/Icon/icons/UnderLine.d.ts +9 -0
  36. package/lib/typescript/src/Icon/icons/UnderLine.d.ts.map +1 -0
  37. package/lib/typescript/src/Icon/type.d.ts +1 -1
  38. package/lib/typescript/src/Icon/type.d.ts.map +1 -1
  39. package/lib/typescript/src/Input/InputOtp.d.ts +1 -0
  40. package/lib/typescript/src/Input/InputOtp.d.ts.map +1 -1
  41. package/lib/typescript/src/TextEditor/TextEditor.d.ts +2 -1
  42. package/lib/typescript/src/TextEditor/TextEditor.d.ts.map +1 -1
  43. package/lib/typescript/src/TextEditor/types.d.ts +7 -0
  44. package/lib/typescript/src/TextEditor/types.d.ts.map +1 -1
  45. package/lib/typescript/src/Toast/Toast.d.ts +2 -1
  46. package/lib/typescript/src/Toast/Toast.d.ts.map +1 -1
  47. package/lib/typescript/src/Toast/ToastContext.d.ts +1 -0
  48. package/lib/typescript/src/Toast/ToastContext.d.ts.map +1 -1
  49. package/package.json +2 -2
  50. package/src/BottomSheet/BottomSheet.tsx +4 -1
  51. package/src/Button/tests/Button.test.tsx +49 -0
  52. package/src/Button/type.tsx +1 -1
  53. package/src/DatePicker/TimePicker.tsx +116 -52
  54. package/src/Icon/Icon.tsx +6 -0
  55. package/src/Icon/README.md +6 -0
  56. package/src/Icon/icons/Bold.tsx +22 -0
  57. package/src/Icon/icons/Italic.tsx +22 -0
  58. package/src/Icon/icons/UnderLine.tsx +26 -0
  59. package/src/Icon/type.tsx +3 -0
  60. package/src/Input/InputOtp.tsx +7 -3
  61. package/src/Input/README_INPUT_OTP.md +8 -7
  62. package/src/TextEditor/TextEditor.tsx +222 -3
  63. package/src/TextEditor/types.tsx +7 -0
  64. package/src/Toast/README.md +32 -16
  65. package/src/Toast/Toast.tsx +26 -16
  66. package/src/Toast/ToastContext.tsx +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"TextEditor.d.ts","sourceRoot":"","sources":["../../../../src/TextEditor/TextEditor.tsx"],"names":[],"mappings":"AAEA,MAAM,CAAC,OAAO,UAAU,UAAU,4CAEjC"}
1
+ {"version":3,"file":"TextEditor.d.ts","sourceRoot":"","sources":["../../../../src/TextEditor/TextEditor.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAK9C,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,MAAY,GACb,EAAE,cAAc,2CAyKhB"}
@@ -1 +1,8 @@
1
+ export interface TextEditorType {
2
+ label?: string;
3
+ hint?: string;
4
+ hasError?: boolean;
5
+ onChange?: (data: string) => void;
6
+ height?: number;
7
+ }
1
8
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/TextEditor/types.tsx"],"names":[],"mappings":""}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/TextEditor/types.tsx"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
@@ -8,7 +8,8 @@ interface ToastProps {
8
8
  icon?: IconNameProps;
9
9
  onClear?: (val: boolean) => void;
10
10
  onHide?: (val: boolean) => void;
11
+ children?: React.ReactNode;
11
12
  }
12
- declare const Toast: ({ visible, message, duration, onHide, color, icon, onClear, }: ToastProps) => import("react/jsx-runtime").JSX.Element | null;
13
+ declare const Toast: ({ visible, message, duration, onHide, color, icon, onClear, children, }: ToastProps) => import("react/jsx-runtime").JSX.Element | null;
13
14
  export default Toast;
14
15
  //# sourceMappingURL=Toast.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../../../src/Toast/Toast.tsx"],"names":[],"mappings":"AAIA,OAAa,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAInD,KAAK,UAAU,GACX,SAAS,GACT,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,MAAM,GACN,QAAQ,GACR,QAAQ,CAAC;AAoDb,UAAU,UAAU;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CACjC;AACD,QAAA,MAAM,KAAK,GAAI,+DAQZ,UAAU,mDAqEZ,CAAC;AAwBF,eAAe,KAAK,CAAC"}
1
+ {"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../../../src/Toast/Toast.tsx"],"names":[],"mappings":"AAIA,OAAa,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAEnD,KAAK,UAAU,GACX,SAAS,GACT,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,MAAM,GACN,QAAQ,GACR,QAAQ,CAAC;AAoDb,UAAU,UAAU;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AACD,QAAA,MAAM,KAAK,GAAI,yEASZ,UAAU,mDAmEZ,CAAC;AAoCF,eAAe,KAAK,CAAC"}
@@ -5,6 +5,7 @@ interface ToastOptions {
5
5
  color?: ColorVariantType;
6
6
  icon?: IconNameProps;
7
7
  duration?: number;
8
+ children?: React.ReactNode;
8
9
  }
9
10
  interface ToastContextType {
10
11
  show: (message: string, options?: ToastOptions) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"ToastContext.d.ts","sourceRoot":"","sources":["../../../../src/Toast/ToastContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,UAAU,YAAY;IACpB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,gBAAgB;IACxB,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;CACzD;AAID,eAAO,MAAM,aAAa,GAAI,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,4CA2BlE,CAAC;AAEF,eAAO,MAAM,QAAQ,QAAO,gBAI3B,CAAC"}
1
+ {"version":3,"file":"ToastContext.d.ts","sourceRoot":"","sources":["../../../../src/Toast/ToastContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,UAAU,YAAY;IACpB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED,UAAU,gBAAgB;IACxB,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;CACzD;AAID,eAAO,MAAM,aAAa,GAAI,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,4CA4BlE,CAAC;AAEF,eAAO,MAAM,QAAQ,QAAO,gBAI3B,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@herca/rn-kit",
3
- "private": false,
4
- "version": "0.1.26",
3
+ "version": "0.1.28",
5
4
  "description": "React Native UI Kit",
6
5
  "main": "./lib/module/index.js",
7
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -94,6 +93,7 @@
94
93
  "@react-native-documents/picker": "^10.1.3",
95
94
  "@react-native-documents/viewer": "^1.0.1",
96
95
  "@react-navigation/native": "^7.1.14",
96
+ "@testing-library/react-native": "^13.2.0",
97
97
  "react-native-blob-util": "^0.22.2",
98
98
  "react-native-image-picker": "^8.2.1",
99
99
  "react-native-pager-view": "^6.8.1",
@@ -249,9 +249,12 @@ const styles = StyleSheet.create({
249
249
  },
250
250
  contentWithFooter: {},
251
251
  footer: {
252
+ borderTopWidth: 1,
253
+ borderTopColor: Color.gray[100],
252
254
  backgroundColor: Color.base.white100,
253
255
  position: 'absolute',
254
- marginBottom: 20,
256
+ paddingTop: 10,
257
+ marginBottom: 32,
255
258
  bottom: 0,
256
259
  left: 0,
257
260
  right: 0,
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import { render, fireEvent } from '@testing-library/react-native';
3
+ import { Text } from 'react-native';
4
+ import Button from '../Button';
5
+
6
+ describe('Button Component', () => {
7
+ it('renders with title', () => {
8
+ const { getByText } = render(<Button title="Submit" onPress={() => {}} />);
9
+ expect(getByText('Submit')).toBeTruthy();
10
+ });
11
+
12
+ it('calls onPress when pressed', () => {
13
+ const mockFn = jest.fn();
14
+ const { getByText } = render(<Button title="Click me" onPress={mockFn} />);
15
+ fireEvent.press(getByText('Click me'));
16
+ expect(mockFn).toHaveBeenCalledTimes(1);
17
+ });
18
+
19
+ it('does not call onPress when disabled', () => {
20
+ const mockFn = jest.fn();
21
+ const { getByText } = render(
22
+ <Button title="Disabled" onPress={mockFn} disabled />
23
+ );
24
+ fireEvent.press(getByText('Disabled'));
25
+ expect(mockFn).not.toHaveBeenCalled();
26
+ });
27
+
28
+ it('renders loading indicator when loading', () => {
29
+ const { getByTestId } = render(
30
+ <Button title="Load" onPress={() => {}} loading />
31
+ );
32
+ // You might want to set `testID="loading-indicator"` in <ActivityIndicator /> in the component
33
+ // For now, we check for presence of ActivityIndicator via accessibilityRole
34
+ const indicator = getByTestId('ActivityIndicator');
35
+ expect(indicator).toBeTruthy();
36
+ });
37
+
38
+ it('renders custom child instead of title', () => {
39
+ const { queryByText, getByText } = render(
40
+ <Button onPress={() => {}}>
41
+ <React.Fragment>
42
+ <Text>Custom Child</Text>
43
+ </React.Fragment>
44
+ </Button>
45
+ );
46
+ expect(getByText('Custom Child')).toBeTruthy();
47
+ expect(queryByText('title')).toBeNull();
48
+ });
49
+ });
@@ -255,7 +255,7 @@ export const COLOR_MAP: Record<
255
255
  tertiaryFocusBorderColor: Color.info[200],
256
256
  tertiaryPressTextColor: Color.info[700],
257
257
  tertiaryPressBorderColor: 'transparent',
258
- tertiaryDisabledTextColor: Color.info[100],
258
+ tertiaryDisabledTextColor: Color.info[200],
259
259
  tertiaryDisabledBorderColor: Color.base.white100,
260
260
  },
261
261
  orange: {
@@ -1,12 +1,13 @@
1
- import React, { useRef, useState, useEffect } from 'react';
1
+ import React, { useRef, useState, useEffect, useMemo } from 'react';
2
2
  import {
3
3
  View,
4
- ScrollView,
5
4
  StyleSheet,
6
5
  Text,
7
- type NativeScrollEvent,
8
- type NativeSyntheticEvent,
9
6
  Dimensions,
7
+ Animated,
8
+ type NativeSyntheticEvent,
9
+ type NativeScrollEvent,
10
+ ScrollView,
10
11
  } from 'react-native';
11
12
  import BottomSheet from '../BottomSheet/BottomSheet';
12
13
  import Button from '../Button/Button';
@@ -17,6 +18,7 @@ const ITEM_HEIGHT = 40;
17
18
  const SCREEN_WIDTH = Dimensions.get('window').width;
18
19
  const VISIBLE_ITEMS = 5;
19
20
  const MIDDLE_INDEX = Math.floor(VISIBLE_ITEMS / 2);
21
+ const REPEAT_COUNT = 5;
20
22
 
21
23
  interface TimePickerProps {
22
24
  isOpen: boolean;
@@ -26,42 +28,61 @@ interface TimePickerProps {
26
28
 
27
29
  const generateItems = (max: number) => Array.from({ length: max }, (_, i) => i);
28
30
 
31
+ const repeatItems = (items: number[], repeatCount: number) =>
32
+ Array.from({ length: repeatCount }).flatMap(() => items);
33
+
29
34
  export default function TimePickerWheel({
30
35
  isOpen,
31
36
  onClose,
32
37
  onChange,
33
38
  }: TimePickerProps) {
34
- const [selectedHour, setSelectedHour] = useState(0);
35
- const [selectedMinute, setSelectedMinute] = useState(0);
39
+ const [selectedHour, setSelectedHour] = useState<number>(0);
40
+ const [selectedMinute, setSelectedMinute] = useState<number>(0);
36
41
 
37
42
  const hourRef = useRef<ScrollView>(null);
38
43
  const minuteRef = useRef<ScrollView>(null);
39
44
 
40
- const padItems = (items: number[], padCount: number) => {
41
- return [
42
- ...items.slice(-padCount).map((i) => i - items.length),
43
- ...items,
44
- ...items.slice(0, padCount).map((i) => i + items.length),
45
- ];
46
- };
45
+ const hourScrollY = useRef(new Animated.Value(0)).current;
46
+ const minuteScrollY = useRef(new Animated.Value(0)).current;
47
+
48
+ const hoursBase = generateItems(24);
49
+ const minutesBase = generateItems(60);
47
50
 
48
- const hours = padItems(generateItems(24), MIDDLE_INDEX);
49
- const minutes = padItems(generateItems(60), MIDDLE_INDEX);
51
+ const hours = useMemo(
52
+ () => repeatItems(hoursBase, REPEAT_COUNT),
53
+ [hoursBase]
54
+ );
55
+ const minutes = useMemo(
56
+ () => repeatItems(minutesBase, REPEAT_COUNT),
57
+ [minutesBase]
58
+ );
59
+
60
+ const CENTER_INDEX_HOUR =
61
+ Math.floor(hours.length / 2 / 24) * 24 + selectedHour;
62
+ const CENTER_INDEX_MINUTE =
63
+ Math.floor(minutes.length / 2 / 60) * 60 + selectedMinute;
50
64
 
51
65
  useEffect(() => {
52
66
  if (isOpen) {
53
67
  setTimeout(() => {
54
- scrollToIndex(hourRef, selectedHour + MIDDLE_INDEX);
55
- scrollToIndex(minuteRef, selectedMinute + MIDDLE_INDEX);
68
+ scrollToIndex(hourRef, CENTER_INDEX_HOUR);
69
+ scrollToIndex(minuteRef, CENTER_INDEX_MINUTE);
56
70
  }, 100);
57
71
  }
58
- }, [isOpen, selectedMinute, selectedHour]);
72
+ }, [
73
+ isOpen,
74
+ selectedMinute,
75
+ selectedHour,
76
+ CENTER_INDEX_HOUR,
77
+ CENTER_INDEX_MINUTE,
78
+ ]);
59
79
 
60
80
  const scrollToIndex = (
61
81
  ref: React.RefObject<ScrollView | null>,
62
- index: number
82
+ index: number,
83
+ animated: boolean = true
63
84
  ) => {
64
- ref.current?.scrollTo({ y: index * ITEM_HEIGHT, animated: true });
85
+ ref.current?.scrollTo({ y: index * ITEM_HEIGHT, animated });
65
86
  };
66
87
 
67
88
  const handleScrollEnd = (
@@ -70,14 +91,23 @@ export default function TimePickerWheel({
70
91
  ) => {
71
92
  const offsetY = event.nativeEvent.contentOffset.y;
72
93
  const index = Math.round(offsetY / ITEM_HEIGHT);
94
+
73
95
  if (type === 'hour') {
74
- const realIndex = (((index - MIDDLE_INDEX) % 24) + 24) % 24;
75
- setSelectedHour(realIndex);
76
- setTimeout(() => scrollToIndex(hourRef, realIndex + MIDDLE_INDEX), 50);
96
+ const realValue = hours[index % 24] as number;
97
+ setSelectedHour(realValue);
98
+
99
+ const newCenter = Math.floor(hours.length / 2 / 24) * 24 + realValue;
100
+ if (Math.abs(index - newCenter) > 12) {
101
+ scrollToIndex(hourRef, newCenter, false);
102
+ }
77
103
  } else {
78
- const realIndex = (((index - MIDDLE_INDEX) % 60) + 60) % 60;
79
- setSelectedMinute(realIndex);
80
- setTimeout(() => scrollToIndex(minuteRef, realIndex + MIDDLE_INDEX), 50);
104
+ const realValue = minutes[index % 60] as number;
105
+ setSelectedMinute(realValue);
106
+
107
+ const newCenter = Math.floor(minutes.length / 2 / 60) * 60 + realValue;
108
+ if (Math.abs(index - newCenter) > 12) {
109
+ scrollToIndex(minuteRef, newCenter, false);
110
+ }
81
111
  }
82
112
  };
83
113
 
@@ -86,22 +116,44 @@ export default function TimePickerWheel({
86
116
  onClose?.();
87
117
  };
88
118
 
89
- const renderItem = (value: number, type: 'hour' | 'minute') => {
90
- const isSelected =
91
- type === 'hour'
92
- ? ((value % 24) + 24) % 24 === selectedHour
93
- : (value % 60) + (60 % 60) === selectedMinute;
94
- const displayValue =
95
- type === 'hour'
96
- ? (((value % 24) + 24) % 24).toString().padStart(2, '0')
97
- : (((value % 60) + 60) % 60).toString().padStart(2, '0');
119
+ const renderAnimatedItem = (
120
+ value: number,
121
+ type: 'hour' | 'minute',
122
+ index: number,
123
+ scrollY: Animated.Value
124
+ ) => {
125
+ const normalizedValue =
126
+ type === 'hour' ? ((value % 24) + 24) % 24 : ((value % 60) + 60) % 60;
127
+
128
+ const inputRange = [
129
+ (index - 2) * ITEM_HEIGHT,
130
+ (index - 1) * ITEM_HEIGHT,
131
+ index * ITEM_HEIGHT,
132
+ (index + 1) * ITEM_HEIGHT,
133
+ (index + 2) * ITEM_HEIGHT,
134
+ ];
135
+
136
+ const scale = scrollY.interpolate({
137
+ inputRange,
138
+ outputRange: [0.7, 0.85, 1, 0.85, 0.7],
139
+ extrapolate: 'clamp',
140
+ });
141
+
142
+ const opacity = scrollY.interpolate({
143
+ inputRange,
144
+ outputRange: [0.3, 0.6, 1, 0.6, 0.3],
145
+ extrapolate: 'clamp',
146
+ });
98
147
 
99
148
  return (
100
- <View key={`${type}-${value}`} style={styles.item}>
101
- <Text style={[styles.itemText, isSelected && styles.selectedItemText]}>
102
- {displayValue}
149
+ <Animated.View
150
+ key={`${type}-${index}`}
151
+ style={[styles.item, { transform: [{ scale }], opacity }]}
152
+ >
153
+ <Text style={styles.itemText}>
154
+ {normalizedValue.toString().padStart(2, '0')}
103
155
  </Text>
104
- </View>
156
+ </Animated.View>
105
157
  );
106
158
  };
107
159
 
@@ -136,7 +188,7 @@ export default function TimePickerWheel({
136
188
  weight="semibold"
137
189
  color={Color.gray[900]}
138
190
  center
139
- style={{ marginBottom: 16 }}
191
+ style={styles.hintTime}
140
192
  >
141
193
  Pilih Waktu
142
194
  </Typography>
@@ -146,31 +198,46 @@ export default function TimePickerWheel({
146
198
  <View style={styles.highlightOverlay} pointerEvents="none">
147
199
  <View style={styles.highlight} />
148
200
  </View>
201
+
149
202
  <View style={styles.scrollContainer}>
150
203
  <View style={styles.pickerColumn}>
151
- <ScrollView
204
+ <Animated.ScrollView
152
205
  ref={hourRef}
153
206
  showsVerticalScrollIndicator={false}
154
207
  snapToInterval={ITEM_HEIGHT}
155
208
  decelerationRate="fast"
156
209
  onMomentumScrollEnd={(e) => handleScrollEnd(e, 'hour')}
210
+ onScroll={Animated.event(
211
+ [{ nativeEvent: { contentOffset: { y: hourScrollY } } }],
212
+ { useNativeDriver: true }
213
+ )}
214
+ scrollEventThrottle={16}
157
215
  contentContainerStyle={styles.scrollContent}
158
216
  >
159
- {hours.map((h) => renderItem(h, 'hour'))}
160
- </ScrollView>
217
+ {hours.map((h, i) =>
218
+ renderAnimatedItem(h, 'hour', i, hourScrollY)
219
+ )}
220
+ </Animated.ScrollView>
161
221
  </View>
162
222
 
163
223
  <View style={styles.pickerColumn}>
164
- <ScrollView
224
+ <Animated.ScrollView
165
225
  ref={minuteRef}
166
226
  showsVerticalScrollIndicator={false}
167
227
  snapToInterval={ITEM_HEIGHT}
168
228
  decelerationRate="fast"
169
229
  onMomentumScrollEnd={(e) => handleScrollEnd(e, 'minute')}
230
+ onScroll={Animated.event(
231
+ [{ nativeEvent: { contentOffset: { y: minuteScrollY } } }],
232
+ { useNativeDriver: true }
233
+ )}
234
+ scrollEventThrottle={16}
170
235
  contentContainerStyle={styles.scrollContent}
171
236
  >
172
- {minutes.map((m) => renderItem(m, 'minute'))}
173
- </ScrollView>
237
+ {minutes.map((m, i) =>
238
+ renderAnimatedItem(m, 'minute', i, minuteScrollY)
239
+ )}
240
+ </Animated.ScrollView>
174
241
  </View>
175
242
  </View>
176
243
  </View>
@@ -209,13 +276,9 @@ const styles = StyleSheet.create({
209
276
  alignItems: 'center',
210
277
  },
211
278
  itemText: {
212
- fontSize: 18,
213
- color: Color.gray[400],
214
- },
215
- selectedItemText: {
216
- color: Color.gray[900],
217
- fontWeight: 'bold',
218
279
  fontSize: 22,
280
+ color: Color.gray[900],
281
+ fontWeight: '600',
219
282
  },
220
283
  highlightOverlay: {
221
284
  position: 'absolute',
@@ -231,4 +294,5 @@ const styles = StyleSheet.create({
231
294
  zIndex: -1,
232
295
  backgroundColor: Color.primary[50],
233
296
  },
297
+ hintTime: { marginBottom: 16 },
234
298
  });
package/src/Icon/Icon.tsx CHANGED
@@ -56,6 +56,9 @@ import QuestionCircleOutline from './icons/QuestionCircleOutline';
56
56
  import ShieldFill from './icons/ShieldFill';
57
57
  import Users from './icons/Users';
58
58
  import WhatsAppFill from './icons/WhatsappFill';
59
+ import Bold from './icons/Bold';
60
+ import Italic from './icons/Italic';
61
+ import UnderLine from './icons/UnderLine';
59
62
 
60
63
  const icons: Record<
61
64
  IconNameProps,
@@ -116,6 +119,9 @@ const icons: Record<
116
119
  'shield-fill': ShieldFill,
117
120
  'Users': Users,
118
121
  'whatsapp-fill': WhatsAppFill,
122
+ 'Bold': Bold,
123
+ 'Italic': Italic,
124
+ 'UnderLine': UnderLine,
119
125
  };
120
126
 
121
127
  const Icon: React.FC<IconProps> = ({
@@ -53,6 +53,7 @@ export default function App() {
53
53
  - ArrowLeft
54
54
  - ArrowRight
55
55
  - ArrowUp
56
+ - Bold
56
57
  - Camera
57
58
  - Calendar
58
59
  - Check
@@ -64,12 +65,14 @@ export default function App() {
64
65
  - Filter
65
66
  - Home
66
67
  - Image
68
+ - Italic
67
69
  - Pdf
68
70
  - RadioButton
69
71
  - Search
70
72
  - StickyNote
71
73
  - Times
72
74
  - User
75
+ - UnderLine
73
76
  - x-circle
74
77
  - Plus
75
78
  - Bell
@@ -100,3 +103,6 @@ export default function App() {
100
103
  - shield-fill
101
104
  - Users
102
105
  - whatsapp-fill
106
+ - Bold
107
+ - UnderLine
108
+ - Italic
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import Svg, { Path } from 'react-native-svg';
3
+
4
+ interface Props {
5
+ size?: number;
6
+ color?: string;
7
+ style?: object;
8
+ }
9
+
10
+ const Bold: React.FC<Props> = ({ size = 24, color = '#000', style }) => (
11
+ <Svg width={size} height={size} viewBox="0 0 13 16" fill="none" style={style}>
12
+ <Path
13
+ d="M1 8.07682H7.66667C9.50762 8.07682 11 6.58444 11 4.74349C11 2.90254 9.50762 1.41016 7.66667 1.41016H1V8.07682ZM1 8.07682H8.5C10.3409 8.07682 11.8333 9.56921 11.8333 11.4102C11.8333 13.2511 10.3409 14.7435 8.5 14.7435H1V8.07682Z"
14
+ stroke={color}
15
+ stroke-width="2"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ />
19
+ </Svg>
20
+ );
21
+
22
+ export default Bold;
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import Svg, { Path } from 'react-native-svg';
3
+
4
+ interface Props {
5
+ size?: number;
6
+ color?: string;
7
+ style?: object;
8
+ }
9
+
10
+ const Italic: React.FC<Props> = ({ size = 24, color = '#909090', style }) => (
11
+ <Svg width={size} height={size} viewBox="0 0 14 16" fill="none" style={style}>
12
+ <Path
13
+ d="M12.8334 1.41016H5.33341M8.66675 14.7435H1.16675M9.50008 1.41016L4.50008 14.7435"
14
+ stroke={color}
15
+ stroke-width="2"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ />
19
+ </Svg>
20
+ );
21
+
22
+ export default Italic;
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import Svg, { Path } from 'react-native-svg';
3
+
4
+ interface Props {
5
+ size?: number;
6
+ color?: string;
7
+ style?: object;
8
+ }
9
+
10
+ const UnderLine: React.FC<Props> = ({
11
+ size = 24,
12
+ color = '#909090',
13
+ style,
14
+ }) => (
15
+ <Svg width={size} height={size} viewBox="0 0 16 17" fill="none" style={style}>
16
+ <Path
17
+ d="M12.9999 1.41016V7.24349C12.9999 10.0049 10.7613 12.2435 7.99992 12.2435C5.23849 12.2435 2.99992 10.0049 2.99992 7.24349V1.41016M1.33325 15.5768H14.6666"
18
+ stroke={color}
19
+ stroke-width="2"
20
+ stroke-linecap="round"
21
+ stroke-linejoin="round"
22
+ />
23
+ </Svg>
24
+ );
25
+
26
+ export default UnderLine;
package/src/Icon/type.tsx CHANGED
@@ -57,6 +57,9 @@ export type IconNameProps =
57
57
  | 'question-circle-outline'
58
58
  | 'shield-fill'
59
59
  | 'Users'
60
+ | 'Bold'
61
+ | 'Italic'
62
+ | 'UnderLine'
60
63
  | 'whatsapp-fill';
61
64
 
62
65
  export interface IconProps {
@@ -1,5 +1,5 @@
1
1
  import React, { useRef, useState } from 'react';
2
- import { View, TextInput, StyleSheet } from 'react-native';
2
+ import { StyleSheet, TextInput, View } from 'react-native';
3
3
  import Color from '../Color/Color';
4
4
  import LabelForm from '../LabelForm/LabelForm';
5
5
  import Typography from '../Typography/Typography';
@@ -10,6 +10,7 @@ type InputOtpProps = {
10
10
  label?: string;
11
11
  hint?: string;
12
12
  hasError?: boolean;
13
+ inputCenter?: boolean;
13
14
  };
14
15
 
15
16
  const InputOtp: React.FC<InputOtpProps> = ({
@@ -18,6 +19,7 @@ const InputOtp: React.FC<InputOtpProps> = ({
18
19
  label,
19
20
  hint,
20
21
  hasError,
22
+ inputCenter = false,
21
23
  }) => {
22
24
  const inputs = useRef<Array<TextInput | null>>([]);
23
25
  const values = useRef<string[]>(Array(length).fill(''));
@@ -50,7 +52,10 @@ const InputOtp: React.FC<InputOtpProps> = ({
50
52
  <View style={styles.gap4}>
51
53
  {label ? <LabelForm title={label} /> : null}
52
54
  <View
53
- style={styles.container}
55
+ style={[
56
+ styles.container,
57
+ { justifyContent: inputCenter ? 'center' : 'flex-start' },
58
+ ]}
54
59
  onLayout={(e) => {
55
60
  // cek apakah lebar container itu cukup untuk menampilkan semua input
56
61
  containerWidth.current = e.nativeEvent.layout.width;
@@ -101,7 +106,6 @@ const styles = StyleSheet.create({
101
106
  container: {
102
107
  flexDirection: 'row',
103
108
  gap: 10,
104
- justifyContent: 'flex-start',
105
109
  },
106
110
  input: {
107
111
  flexGrow: 1,
@@ -24,13 +24,14 @@ Salin file `InputOtp.tsx` ke proyek React Native Anda dan import sesuai kebutuha
24
24
 
25
25
  ## Props
26
26
 
27
- | Nama | Tipe | Default | Deskripsi |
28
- |------------|-----------------------------|---------|----------------------------------------------------------------|
29
- | `length` | `number` | 6 | Jumlah kotak input OTP yang ingin ditampilkan |
30
- | `onChange` | `(otp: string) => void` | - | Callback saat isi OTP berubah, menerima OTP lengkap sebagai string |
31
- | `label` | `string` | - | Label yang tampil di atas kotak input |
32
- | `hint` | `string` | - | Pesan kecil di bawah input (misal instruksi atau error) |
33
- | `hasError` | `boolean` | false | Jika `true`, border input berubah warna merah (indikasi error) |
27
+ | Nama | Tipe | Default | Deskripsi |
28
+ | ------------- | ----------------------- | ------- | ---------------------------------------------------------------------------------------- |
29
+ | `length` | `number` | 6 | Jumlah kotak input OTP yang ingin ditampilkan |
30
+ | `onChange` | `(otp: string) => void` | - | Callback saat isi OTP berubah, menerima OTP lengkap sebagai string |
31
+ | `label` | `string` | - | Label yang tampil di atas kotak input |
32
+ | `hint` | `string` | - | Pesan kecil di bawah input (misal instruksi atau error) |
33
+ | `hasError` | `boolean` | false | Jika `true`, border input berubah warna merah (indikasi error) |
34
+ | `inputCenter` | `boolean` | false | Jika `true`, maka seluruh input field akan memposisikan diri ke tengah secara horizontal |
34
35
 
35
36
  ---
36
37