rnnovaui 0.2.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnnovaui",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "react-native": "src/index.js",
@@ -1,7 +1,9 @@
1
- import React, { memo, useCallback, useMemo } from "react";
1
+ import React, { memo, useCallback, useMemo, useRef } from "react";
2
2
 
3
3
  import {
4
4
  ActivityIndicator,
5
+ Animated,
6
+ Easing,
5
7
  Pressable,
6
8
  StyleSheet,
7
9
  Text,
@@ -10,7 +12,7 @@ import {
10
12
 
11
13
  import { useUITheme } from "../../theme";
12
14
 
13
- const VARIANTS = {
15
+ const BUTTON_VARIANTS = {
14
16
  solid: "solid",
15
17
  outline: "outline",
16
18
  ghost: "ghost",
@@ -19,14 +21,14 @@ const VARIANTS = {
19
21
  success: "success",
20
22
  };
21
23
 
22
- const SIZES = {
24
+ const BUTTON_SIZES = {
23
25
  sm: "sm",
24
26
  md: "md",
25
27
  lg: "lg",
26
28
  xl: "xl",
27
29
  };
28
30
 
29
- const FALLBACK = {
31
+ const FALLBACK_COLORS = {
30
32
  primary: "#FF5A1F",
31
33
  primaryPressed: "#E94D17",
32
34
  primarySoft: "#FFF0EA",
@@ -41,9 +43,10 @@ const FALLBACK = {
41
43
 
42
44
  disabledBackground: "#E5E5E5",
43
45
  disabledText: "#A3A3A3",
46
+ disabledBorder: "#E5E5E5",
44
47
  };
45
48
 
46
- function UIButton({
49
+ function UIButtonComponent({
47
50
  title,
48
51
  children,
49
52
 
@@ -55,136 +58,146 @@ function UIButton({
55
58
 
56
59
  fullWidth = false,
57
60
 
58
- leftIcon,
59
- rightIcon,
61
+ leftIcon = null,
62
+ rightIcon = null,
60
63
 
61
64
  onPress,
62
65
  onLongPress,
66
+ onPressIn,
67
+ onPressOut,
63
68
 
64
69
  style,
65
70
  contentStyle,
66
71
  textStyle,
67
72
 
68
- testID,
69
-
70
- ...props
71
- }) {
72
- const { theme } = useUITheme();
73
+ activeOpacity = 0.82,
73
74
 
74
- const colors = theme?.colors || {};
75
-
76
- const primary = colors.primary || FALLBACK.primary;
77
-
78
- const primaryPressed = colors.primaryPressed || FALLBACK.primaryPressed;
75
+ pressAnimation = true,
79
76
 
80
- const primarySoft = colors.primarySoft || FALLBACK.primarySoft;
77
+ pressScale = 0.97,
81
78
 
82
- const success = colors.success || FALLBACK.success;
79
+ pressAnimationDuration = 100,
83
80
 
84
- const successPressed = colors.successPressed || FALLBACK.successPressed;
81
+ testID,
85
82
 
86
- const danger = colors.danger || FALLBACK.danger;
83
+ accessibilityLabel,
84
+ accessibilityHint,
87
85
 
88
- const dangerPressed = colors.dangerPressed || FALLBACK.dangerPressed;
86
+ ...props
87
+ }) {
88
+ const { theme } = useUITheme();
89
89
 
90
- const white = colors.onPrimary || FALLBACK.white;
90
+ const colors = theme?.colors || {};
91
91
 
92
92
  const isDisabled = disabled || loading;
93
93
 
94
- const buttonColors = useMemo(() => {
95
- if (isDisabled) {
96
- return {
97
- background:
98
- colors.buttonDisabledBackground || FALLBACK.disabledBackground,
99
-
100
- border: colors.buttonDisabledBorder || FALLBACK.disabledBackground,
101
-
102
- text: colors.buttonDisabledText || FALLBACK.disabledText,
103
-
104
- borderWidth: 1,
105
- };
106
- }
94
+ /*
95
+ * Native Animated value.
96
+ *
97
+ * Created once and reused.
98
+ * This avoids creating animation
99
+ * objects on every render.
100
+ */
101
+ const scale = useRef(new Animated.Value(1)).current;
107
102
 
108
- switch (variant) {
109
- case "outline":
110
- return {
111
- background: "transparent",
103
+ const animationRef = useRef(null);
112
104
 
113
- border: colors.buttonOutlineBorder || primary,
105
+ const safeVariant = BUTTON_VARIANTS[variant]
106
+ ? variant
107
+ : BUTTON_VARIANTS.solid;
114
108
 
115
- text: colors.buttonOutlineText || primary,
109
+ const safeSize = BUTTON_SIZES[size] ? size : BUTTON_SIZES.md;
116
110
 
117
- borderWidth: 1,
118
- };
111
+ const buttonColors = useMemo(
112
+ () => getVariantColors(safeVariant, colors, isDisabled),
113
+ [safeVariant, colors, isDisabled],
114
+ );
119
115
 
120
- case "ghost":
121
- return {
122
- background: "transparent",
116
+ const dimensions = useMemo(
117
+ () => getDimensions(safeSize, theme),
118
+ [safeSize, theme],
119
+ );
123
120
 
124
- border: "transparent",
121
+ const stopAnimation = useCallback(() => {
122
+ if (animationRef.current) {
123
+ animationRef.current.stop();
125
124
 
126
- text: colors.buttonGhostText || primary,
125
+ animationRef.current = null;
126
+ }
127
+ }, []);
127
128
 
128
- borderWidth: 0,
129
- };
129
+ const animateScale = useCallback(
130
+ (targetScale) => {
131
+ if (!pressAnimation || isDisabled) {
132
+ return;
133
+ }
130
134
 
131
- case "soft":
132
- return {
133
- background: colors.buttonSoftBackground || primarySoft,
135
+ stopAnimation();
134
136
 
135
- border: colors.buttonSoftBackground || primarySoft,
137
+ animationRef.current = Animated.timing(scale, {
138
+ toValue: targetScale,
136
139
 
137
- text: colors.buttonSoftText || primary,
140
+ duration: pressAnimationDuration,
138
141
 
139
- borderWidth: 1,
140
- };
142
+ easing: Easing.out(Easing.quad),
141
143
 
142
- case "danger":
143
- return {
144
- background: colors.buttonDangerBackground || danger,
144
+ useNativeDriver: true,
145
+ });
145
146
 
146
- border: colors.buttonDangerBackground || danger,
147
+ animationRef.current.start(() => {
148
+ animationRef.current = null;
149
+ });
150
+ },
151
+ [pressAnimation, isDisabled, stopAnimation, scale, pressAnimationDuration],
152
+ );
147
153
 
148
- text: colors.buttonDangerText || white,
154
+ const handlePressIn = useCallback(
155
+ (event) => {
156
+ if (isDisabled) {
157
+ return;
158
+ }
149
159
 
150
- borderWidth: 1,
151
- };
160
+ animateScale(pressScale);
152
161
 
153
- case "success":
154
- return {
155
- background: colors.buttonSuccessBackground || success,
162
+ onPressIn?.(event);
163
+ },
164
+ [isDisabled, animateScale, pressScale, onPressIn],
165
+ );
156
166
 
157
- border: colors.buttonSuccessBackground || success,
167
+ const handlePressOut = useCallback(
168
+ (event) => {
169
+ if (isDisabled) {
170
+ return;
171
+ }
158
172
 
159
- text: colors.buttonSuccessText || white,
173
+ animateScale(1);
160
174
 
161
- borderWidth: 1,
162
- };
175
+ onPressOut?.(event);
176
+ },
177
+ [isDisabled, animateScale, onPressOut],
178
+ );
163
179
 
164
- case "solid":
165
- default:
166
- return {
167
- background: colors.buttonPrimaryBackground || primary,
180
+ const handlePress = useCallback(
181
+ (event) => {
182
+ if (isDisabled) {
183
+ return;
184
+ }
168
185
 
169
- border: colors.buttonPrimaryBackground || primary,
186
+ onPress?.(event);
187
+ },
188
+ [isDisabled, onPress],
189
+ );
170
190
 
171
- text: colors.buttonPrimaryText || white,
191
+ const handleLongPress = useCallback(
192
+ (event) => {
193
+ if (isDisabled) {
194
+ return;
195
+ }
172
196
 
173
- borderWidth: 1,
174
- };
175
- }
176
- }, [
177
- variant,
178
- isDisabled,
179
- colors,
180
- primary,
181
- primarySoft,
182
- success,
183
- danger,
184
- white,
185
- ]);
186
-
187
- const dimensions = useMemo(() => getDimensions(size, theme), [size, theme]);
197
+ onLongPress?.(event);
198
+ },
199
+ [isDisabled, onLongPress],
200
+ );
188
201
 
189
202
  const buttonStyle = useMemo(
190
203
  () => [
@@ -208,88 +221,187 @@ function UIButton({
208
221
  width: fullWidth ? "100%" : undefined,
209
222
 
210
223
  opacity: isDisabled ? 0.55 : 1,
224
+
225
+ transform: [
226
+ {
227
+ scale,
228
+ },
229
+ ],
211
230
  },
212
231
 
213
232
  style,
214
233
  ],
215
- [dimensions, buttonColors, fullWidth, isDisabled, style],
234
+ [dimensions, buttonColors, fullWidth, isDisabled, scale, style],
216
235
  );
217
236
 
218
- const handlePress = useCallback(
219
- (event) => {
220
- if (isDisabled) {
221
- return;
222
- }
223
-
224
- onPress?.(event);
225
- },
226
- [isDisabled, onPress],
237
+ return (
238
+ <Animated.View style={buttonStyle}>
239
+ <Pressable
240
+ {...props}
241
+ testID={testID}
242
+ disabled={isDisabled}
243
+ onPress={handlePress}
244
+ onLongPress={handleLongPress}
245
+ onPressIn={handlePressIn}
246
+ onPressOut={handlePressOut}
247
+ accessibilityRole="button"
248
+ accessibilityLabel={
249
+ accessibilityLabel || (typeof title === "string" ? title : undefined)
250
+ }
251
+ accessibilityHint={accessibilityHint}
252
+ accessibilityState={{
253
+ disabled: isDisabled,
254
+
255
+ busy: loading,
256
+ }}
257
+ style={styles.pressable}
258
+ >
259
+ <View
260
+ style={[
261
+ styles.content,
262
+
263
+ {
264
+ gap: dimensions.gap,
265
+ },
266
+
267
+ contentStyle,
268
+ ]}
269
+ >
270
+ {loading ? (
271
+ <ActivityIndicator size="small" color={buttonColors.text} />
272
+ ) : (
273
+ leftIcon && <View style={styles.icon}>{leftIcon}</View>
274
+ )}
275
+
276
+ {children !== undefined && children !== null ? (
277
+ <View style={styles.children}>{children}</View>
278
+ ) : title !== undefined && title !== null ? (
279
+ <Text
280
+ numberOfLines={1}
281
+ style={[
282
+ styles.text,
283
+
284
+ {
285
+ color: buttonColors.text,
286
+
287
+ fontSize: dimensions.fontSize,
288
+
289
+ lineHeight: dimensions.lineHeight,
290
+ },
291
+
292
+ textStyle,
293
+ ]}
294
+ >
295
+ {title}
296
+ </Text>
297
+ ) : null}
298
+
299
+ {!loading && rightIcon && (
300
+ <View style={styles.icon}>{rightIcon}</View>
301
+ )}
302
+ </View>
303
+ </Pressable>
304
+ </Animated.View>
227
305
  );
306
+ }
228
307
 
229
- const handleLongPress = useCallback(
230
- (event) => {
231
- if (isDisabled) {
232
- return;
233
- }
308
+ function getVariantColors(variant, colors, disabled) {
309
+ const primary = colors.primary || FALLBACK_COLORS.primary;
234
310
 
235
- onLongPress?.(event);
236
- },
237
- [isDisabled, onLongPress],
238
- );
311
+ const primarySoft = colors.primarySoft || FALLBACK_COLORS.primarySoft;
239
312
 
240
- return (
241
- <Pressable
242
- {...props}
243
- testID={testID}
244
- disabled={isDisabled}
245
- onPress={handlePress}
246
- onLongPress={handleLongPress}
247
- style={buttonStyle}
248
- >
249
- <View
250
- style={[
251
- styles.content,
313
+ const success = colors.success || FALLBACK_COLORS.success;
252
314
 
253
- {
254
- gap: dimensions.gap,
255
- },
315
+ const successPressed =
316
+ colors.successPressed || FALLBACK_COLORS.successPressed;
256
317
 
257
- contentStyle,
258
- ]}
259
- >
260
- {loading ? (
261
- <ActivityIndicator size="small" color={buttonColors.text} />
262
- ) : (
263
- leftIcon && <View style={styles.icon}>{leftIcon}</View>
264
- )}
265
-
266
- {children !== undefined && children !== null ? (
267
- <View style={styles.children}>{children}</View>
268
- ) : title !== undefined && title !== null ? (
269
- <Text
270
- numberOfLines={1}
271
- style={[
272
- styles.text,
273
-
274
- {
275
- color: buttonColors.text,
276
-
277
- fontSize: dimensions.fontSize,
278
-
279
- lineHeight: dimensions.lineHeight,
280
- },
281
-
282
- textStyle,
283
- ]}
284
- >
285
- {title}
286
- </Text>
287
- ) : null}
288
-
289
- {!loading && rightIcon && <View style={styles.icon}>{rightIcon}</View>}
290
- </View>
291
- </Pressable>
292
- );
318
+ const danger = colors.danger || FALLBACK_COLORS.danger;
319
+
320
+ const dangerPressed = colors.dangerPressed || FALLBACK_COLORS.dangerPressed;
321
+
322
+ const white = colors.onPrimary || FALLBACK_COLORS.white;
323
+
324
+ if (disabled) {
325
+ return {
326
+ background:
327
+ colors.buttonDisabledBackground || FALLBACK_COLORS.disabledBackground,
328
+
329
+ border: colors.buttonDisabledBorder || FALLBACK_COLORS.disabledBorder,
330
+
331
+ borderWidth: 1,
332
+
333
+ text: colors.buttonDisabledText || FALLBACK_COLORS.disabledText,
334
+ };
335
+ }
336
+
337
+ switch (variant) {
338
+ case BUTTON_VARIANTS.outline:
339
+ return {
340
+ background: colors.buttonOutlineBackground || "transparent",
341
+
342
+ border: colors.buttonOutlineBorder || primary,
343
+
344
+ borderWidth: 1,
345
+
346
+ text: colors.buttonOutlineText || primary,
347
+ };
348
+
349
+ case BUTTON_VARIANTS.ghost:
350
+ return {
351
+ background: colors.buttonGhostBackground || "transparent",
352
+
353
+ border: "transparent",
354
+
355
+ borderWidth: 0,
356
+
357
+ text: colors.buttonGhostText || primary,
358
+ };
359
+
360
+ case BUTTON_VARIANTS.soft:
361
+ return {
362
+ background: colors.buttonSoftBackground || primarySoft,
363
+
364
+ border: colors.buttonSoftBackground || primarySoft,
365
+
366
+ borderWidth: 1,
367
+
368
+ text: colors.buttonSoftText || primary,
369
+ };
370
+
371
+ case BUTTON_VARIANTS.danger:
372
+ return {
373
+ background: colors.buttonDangerBackground || danger,
374
+
375
+ border: colors.buttonDangerBackground || danger,
376
+
377
+ borderWidth: 1,
378
+
379
+ text: colors.buttonDangerText || white,
380
+ };
381
+
382
+ case BUTTON_VARIANTS.success:
383
+ return {
384
+ background: colors.buttonSuccessBackground || success,
385
+
386
+ border: colors.buttonSuccessBackground || success,
387
+
388
+ borderWidth: 1,
389
+
390
+ text: colors.buttonSuccessText || white,
391
+ };
392
+
393
+ case BUTTON_VARIANTS.solid:
394
+ default:
395
+ return {
396
+ background: colors.buttonPrimaryBackground || primary,
397
+
398
+ border: colors.buttonPrimaryBackground || primary,
399
+
400
+ borderWidth: 1,
401
+
402
+ text: colors.buttonPrimaryText || white,
403
+ };
404
+ }
293
405
  }
294
406
 
295
407
  function getDimensions(size, theme) {
@@ -301,7 +413,7 @@ function getDimensions(size, theme) {
301
413
  const spacing = theme?.spacing || {};
302
414
 
303
415
  switch (size) {
304
- case "sm":
416
+ case BUTTON_SIZES.sm:
305
417
  return {
306
418
  height: buttonSizes.sm || 36,
307
419
 
@@ -316,7 +428,7 @@ function getDimensions(size, theme) {
316
428
  lineHeight: 18,
317
429
  };
318
430
 
319
- case "lg":
431
+ case BUTTON_SIZES.lg:
320
432
  return {
321
433
  height: buttonSizes.lg || 52,
322
434
 
@@ -331,7 +443,7 @@ function getDimensions(size, theme) {
331
443
  lineHeight: 22,
332
444
  };
333
445
 
334
- case "xl":
446
+ case BUTTON_SIZES.xl:
335
447
  return {
336
448
  height: buttonSizes.xl || 60,
337
449
 
@@ -346,7 +458,7 @@ function getDimensions(size, theme) {
346
458
  lineHeight: 24,
347
459
  };
348
460
 
349
- case "md":
461
+ case BUTTON_SIZES.md:
350
462
  default:
351
463
  return {
352
464
  height: buttonSizes.md || 44,
@@ -366,18 +478,28 @@ function getDimensions(size, theme) {
366
478
 
367
479
  const styles = StyleSheet.create({
368
480
  button: {
369
- flexDirection: "row",
481
+ flexShrink: 0,
370
482
 
371
483
  alignItems: "center",
372
484
 
373
485
  justifyContent: "center",
486
+ },
374
487
 
375
- overflow: "hidden",
488
+ pressable: {
489
+ width: "100%",
376
490
 
377
- flexShrink: 0,
491
+ height: "100%",
492
+
493
+ alignItems: "center",
494
+
495
+ justifyContent: "center",
378
496
  },
379
497
 
380
498
  content: {
499
+ width: "100%",
500
+
501
+ height: "100%",
502
+
381
503
  flexDirection: "row",
382
504
 
383
505
  alignItems: "center",
@@ -404,16 +526,20 @@ const styles = StyleSheet.create({
404
526
  },
405
527
 
406
528
  text: {
529
+ flexShrink: 1,
530
+
407
531
  fontWeight: "600",
408
532
 
409
533
  textAlign: "center",
410
534
 
411
535
  includeFontPadding: false,
412
-
413
- flexShrink: 1,
414
536
  },
415
537
  });
416
538
 
417
- export default memo(UIButton);
539
+ export const UIButton = memo(UIButtonComponent);
540
+
541
+ UIButton.displayName = "UIButton";
542
+
543
+ export { BUTTON_VARIANTS as UIButtonVariants, BUTTON_SIZES as UIButtonSizes };
418
544
 
419
- export { UIButton, VARIANTS as UIButtonVariants, SIZES as UIButtonSizes };
545
+ export default UIButton;