rnnovaui 0.7.0 → 0.8.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.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "react-native": "src/index.js",
@@ -0,0 +1,257 @@
1
+ import React, { memo, useMemo } from "react";
2
+
3
+ import { StyleSheet, View } from "react-native";
4
+
5
+ const BUTTON_GROUP_ORIENTATIONS = {
6
+ horizontal: "horizontal",
7
+ vertical: "vertical",
8
+ };
9
+
10
+ const BUTTON_GROUP_SIZES = {
11
+ sm: "sm",
12
+ md: "md",
13
+ lg: "lg",
14
+ };
15
+
16
+ const BUTTON_GROUP_VARIANTS = {
17
+ attached: "attached",
18
+ separated: "separated",
19
+ };
20
+
21
+ function UIButtonGroupComponent({
22
+ children,
23
+
24
+ orientation = "horizontal",
25
+
26
+ size,
27
+
28
+ variant = "attached",
29
+
30
+ spacing = 8,
31
+
32
+ fullWidth = false,
33
+
34
+ equalWidth = false,
35
+
36
+ disabled = false,
37
+
38
+ style,
39
+
40
+ contentContainerStyle,
41
+
42
+ testID,
43
+
44
+ ...props
45
+ }) {
46
+ const safeOrientation = BUTTON_GROUP_ORIENTATIONS[orientation]
47
+ ? orientation
48
+ : BUTTON_GROUP_ORIENTATIONS.horizontal;
49
+
50
+ const safeVariant = BUTTON_GROUP_VARIANTS[variant]
51
+ ? variant
52
+ : BUTTON_GROUP_VARIANTS.attached;
53
+
54
+ const childrenArray = useMemo(
55
+ () => React.Children.toArray(children),
56
+ [children],
57
+ );
58
+
59
+ const count = childrenArray.length;
60
+
61
+ const isHorizontal = safeOrientation === BUTTON_GROUP_ORIENTATIONS.horizontal;
62
+
63
+ const groupStyle = useMemo(
64
+ () => [
65
+ styles.container,
66
+
67
+ isHorizontal ? styles.horizontal : styles.vertical,
68
+
69
+ fullWidth ? styles.fullWidth : null,
70
+
71
+ safeVariant === BUTTON_GROUP_VARIANTS.separated
72
+ ? {
73
+ gap: spacing,
74
+ }
75
+ : null,
76
+
77
+ style,
78
+ ],
79
+ [isHorizontal, fullWidth, safeVariant, spacing, style],
80
+ );
81
+
82
+ return (
83
+ <View {...props} testID={testID} style={groupStyle}>
84
+ {childrenArray.map((child, index) => {
85
+ if (!React.isValidElement(child)) {
86
+ return child;
87
+ }
88
+
89
+ const isFirst = index === 0;
90
+
91
+ const isLast = index === count - 1;
92
+
93
+ const childStyle = getChildStyle({
94
+ isHorizontal,
95
+ safeVariant,
96
+ equalWidth,
97
+ fullWidth,
98
+ isFirst,
99
+ isLast,
100
+ index,
101
+ count,
102
+ spacing,
103
+ });
104
+
105
+ const existingStyle = child.props.style;
106
+
107
+ return React.cloneElement(child, {
108
+ key: child.key ?? index,
109
+
110
+ size: child.props.size ?? size,
111
+
112
+ disabled: disabled || child.props.disabled,
113
+
114
+ fullWidth: child.props.fullWidth ?? (equalWidth || fullWidth),
115
+
116
+ style: [childStyle, existingStyle],
117
+ });
118
+ })}
119
+ </View>
120
+ );
121
+ }
122
+
123
+ function getChildStyle({
124
+ isHorizontal,
125
+ safeVariant,
126
+ equalWidth,
127
+ fullWidth,
128
+ isFirst,
129
+ isLast,
130
+ index,
131
+ count,
132
+ spacing,
133
+ }) {
134
+ const stylesArray = [];
135
+
136
+ if (equalWidth || fullWidth) {
137
+ stylesArray.push({
138
+ flex: 1,
139
+ });
140
+ }
141
+
142
+ if (safeVariant === BUTTON_GROUP_VARIANTS.separated) {
143
+ return stylesArray;
144
+ }
145
+
146
+ /*
147
+ * Attached horizontal buttons
148
+ *
149
+ * Example:
150
+ *
151
+ * ┌────────┬────────┬────────┐
152
+ * │ One │ Two │ Three │
153
+ * └────────┴────────┴────────┘
154
+ */
155
+
156
+ if (isHorizontal) {
157
+ if (!isFirst) {
158
+ stylesArray.push({
159
+ marginLeft: -1,
160
+ });
161
+ }
162
+
163
+ if (isFirst) {
164
+ stylesArray.push({
165
+ borderTopRightRadius: 0,
166
+ borderBottomRightRadius: 0,
167
+ });
168
+ }
169
+
170
+ if (isLast) {
171
+ stylesArray.push({
172
+ borderTopLeftRadius: 0,
173
+ borderBottomLeftRadius: 0,
174
+ });
175
+ }
176
+
177
+ if (!isFirst && !isLast) {
178
+ stylesArray.push({
179
+ borderRadius: 0,
180
+ });
181
+ }
182
+ } else {
183
+ /*
184
+ * Attached vertical buttons
185
+ *
186
+ * ┌──────────────┐
187
+ * │ One │
188
+ * ├──────────────┤
189
+ * │ Two │
190
+ * ├──────────────┤
191
+ * │ Three │
192
+ * └──────────────┘
193
+ */
194
+
195
+ if (!isFirst) {
196
+ stylesArray.push({
197
+ marginTop: -1,
198
+ });
199
+ }
200
+
201
+ if (isFirst) {
202
+ stylesArray.push({
203
+ borderBottomLeftRadius: 0,
204
+ borderBottomRightRadius: 0,
205
+ });
206
+ }
207
+
208
+ if (isLast) {
209
+ stylesArray.push({
210
+ borderTopLeftRadius: 0,
211
+ borderTopRightRadius: 0,
212
+ });
213
+ }
214
+
215
+ if (!isFirst && !isLast) {
216
+ stylesArray.push({
217
+ borderRadius: 0,
218
+ });
219
+ }
220
+ }
221
+
222
+ return stylesArray;
223
+ }
224
+
225
+ const styles = StyleSheet.create({
226
+ container: {
227
+ alignSelf: "flex-start",
228
+ },
229
+
230
+ horizontal: {
231
+ flexDirection: "row",
232
+
233
+ alignItems: "stretch",
234
+ },
235
+
236
+ vertical: {
237
+ flexDirection: "column",
238
+
239
+ alignItems: "stretch",
240
+ },
241
+
242
+ fullWidth: {
243
+ width: "100%",
244
+ },
245
+ });
246
+
247
+ export const UIButtonGroup = memo(UIButtonGroupComponent);
248
+
249
+ UIButtonGroup.displayName = "UIButtonGroup";
250
+
251
+ export {
252
+ BUTTON_GROUP_ORIENTATIONS as UIButtonGroupOrientations,
253
+ BUTTON_GROUP_SIZES as UIButtonGroupSizes,
254
+ BUTTON_GROUP_VARIANTS as UIButtonGroupVariants,
255
+ };
256
+
257
+ export default UIButtonGroup;
@@ -0,0 +1,6 @@
1
+ export {
2
+ UIButtonGroup,
3
+ UIButtonGroupOrientations,
4
+ UIButtonGroupSizes,
5
+ UIButtonGroupVariants,
6
+ } from "./UIButtonGroup";
@@ -0,0 +1,389 @@
1
+ import React, { forwardRef, memo, useCallback, useMemo, useRef } from "react";
2
+
3
+ import { Animated, Easing, Pressable, StyleSheet, Text } from "react-native";
4
+
5
+ import { useUITheme } from "../../theme";
6
+
7
+ const BUTTON_LINK_SIZES = {
8
+ xs: "xs",
9
+ sm: "sm",
10
+ md: "md",
11
+ lg: "lg",
12
+ };
13
+
14
+ const BUTTON_LINK_VARIANTS = {
15
+ primary: "primary",
16
+ secondary: "secondary",
17
+ success: "success",
18
+ danger: "danger",
19
+ warning: "warning",
20
+ neutral: "neutral",
21
+ muted: "muted",
22
+ };
23
+
24
+ const BUTTON_LINK_UNDERLINES = {
25
+ none: "none",
26
+ always: "always",
27
+ pressed: "pressed",
28
+ };
29
+
30
+ const UIButtonLinkComponent = forwardRef(function UIButtonLink(
31
+ {
32
+ children,
33
+ title,
34
+
35
+ onPress,
36
+ onLongPress,
37
+
38
+ variant = "primary",
39
+ size = "md",
40
+
41
+ underline = "none",
42
+
43
+ disabled = false,
44
+ loading = false,
45
+
46
+ activeOpacity = 0.65,
47
+
48
+ leftIcon,
49
+ rightIcon,
50
+
51
+ gap = 5,
52
+
53
+ textStyle,
54
+ iconStyle,
55
+
56
+ hitSlop = 8,
57
+
58
+ pressAnimation = true,
59
+
60
+ accessibilityLabel,
61
+
62
+ testID,
63
+
64
+ style,
65
+
66
+ ...props
67
+ },
68
+ ref,
69
+ ) {
70
+ const { theme } = useUITheme();
71
+
72
+ const colors = theme?.colors || {};
73
+
74
+ const scale = useRef(new Animated.Value(1)).current;
75
+
76
+ const opacity = useRef(new Animated.Value(1)).current;
77
+
78
+ const dimensions = useMemo(() => getDimensions(size), [size]);
79
+
80
+ const resolvedVariant = BUTTON_LINK_VARIANTS[variant]
81
+ ? variant
82
+ : BUTTON_LINK_VARIANTS.primary;
83
+
84
+ const resolvedUnderline = BUTTON_LINK_UNDERLINES[underline]
85
+ ? underline
86
+ : BUTTON_LINK_UNDERLINES.none;
87
+
88
+ const color = getVariantColor(resolvedVariant, colors);
89
+
90
+ const content = title ?? children;
91
+
92
+ const animatePressIn = useCallback(() => {
93
+ if (!pressAnimation) {
94
+ return;
95
+ }
96
+
97
+ Animated.parallel([
98
+ Animated.timing(scale, {
99
+ toValue: 0.97,
100
+
101
+ duration: 80,
102
+
103
+ easing: Easing.out(Easing.quad),
104
+
105
+ useNativeDriver: true,
106
+ }),
107
+
108
+ Animated.timing(opacity, {
109
+ toValue: activeOpacity,
110
+
111
+ duration: 80,
112
+
113
+ easing: Easing.out(Easing.quad),
114
+
115
+ useNativeDriver: true,
116
+ }),
117
+ ]).start();
118
+ }, [pressAnimation, activeOpacity, scale, opacity]);
119
+
120
+ const animatePressOut = useCallback(() => {
121
+ if (!pressAnimation) {
122
+ return;
123
+ }
124
+
125
+ Animated.parallel([
126
+ Animated.spring(scale, {
127
+ toValue: 1,
128
+
129
+ friction: 7,
130
+
131
+ tension: 120,
132
+
133
+ useNativeDriver: true,
134
+ }),
135
+
136
+ Animated.timing(opacity, {
137
+ toValue: 1,
138
+
139
+ duration: 100,
140
+
141
+ easing: Easing.out(Easing.quad),
142
+
143
+ useNativeDriver: true,
144
+ }),
145
+ ]).start();
146
+ }, [pressAnimation, scale, opacity]);
147
+
148
+ const handlePress = useCallback(() => {
149
+ if (disabled || loading) {
150
+ return;
151
+ }
152
+
153
+ onPress?.();
154
+ }, [disabled, loading, onPress]);
155
+
156
+ const showUnderline =
157
+ resolvedUnderline === BUTTON_LINK_UNDERLINES.always ||
158
+ resolvedUnderline === BUTTON_LINK_UNDERLINES.pressed;
159
+
160
+ return (
161
+ <Animated.View
162
+ style={[
163
+ styles.wrapper,
164
+
165
+ {
166
+ opacity: disabled ? 0.45 : 1,
167
+
168
+ transform: [
169
+ {
170
+ scale,
171
+ },
172
+ ],
173
+ },
174
+
175
+ style,
176
+ ]}
177
+ >
178
+ <Pressable
179
+ {...props}
180
+ ref={ref}
181
+ testID={testID}
182
+ disabled={disabled || loading}
183
+ onPress={handlePress}
184
+ onLongPress={onLongPress}
185
+ onPressIn={animatePressIn}
186
+ onPressOut={animatePressOut}
187
+ hitSlop={hitSlop}
188
+ accessibilityRole="link"
189
+ accessibilityLabel={
190
+ accessibilityLabel ||
191
+ (typeof content === "string" ? content : undefined)
192
+ }
193
+ accessibilityState={{
194
+ disabled: disabled || loading,
195
+
196
+ busy: loading,
197
+ }}
198
+ style={styles.pressable}
199
+ >
200
+ {leftIcon ? (
201
+ <Animated.View
202
+ style={[
203
+ styles.icon,
204
+
205
+ {
206
+ marginRight: gap,
207
+ },
208
+
209
+ iconStyle,
210
+ ]}
211
+ >
212
+ {leftIcon}
213
+ </Animated.View>
214
+ ) : null}
215
+
216
+ {loading ? (
217
+ <LoadingIndicator color={color} size={dimensions.loaderSize} />
218
+ ) : (
219
+ <Text
220
+ numberOfLines={1}
221
+ style={[
222
+ styles.text,
223
+
224
+ {
225
+ color,
226
+
227
+ fontSize: dimensions.fontSize,
228
+
229
+ lineHeight: dimensions.lineHeight,
230
+
231
+ textDecorationLine: showUnderline ? "underline" : "none",
232
+ },
233
+
234
+ textStyle,
235
+ ]}
236
+ >
237
+ {content}
238
+ </Text>
239
+ )}
240
+
241
+ {rightIcon && !loading ? (
242
+ <Animated.View
243
+ style={[
244
+ styles.icon,
245
+
246
+ {
247
+ marginLeft: gap,
248
+ },
249
+
250
+ iconStyle,
251
+ ]}
252
+ >
253
+ {rightIcon}
254
+ </Animated.View>
255
+ ) : null}
256
+ </Pressable>
257
+ </Animated.View>
258
+ );
259
+ });
260
+
261
+ function getDimensions(size) {
262
+ switch (size) {
263
+ case BUTTON_LINK_SIZES.xs:
264
+ return {
265
+ fontSize: 11,
266
+ lineHeight: 15,
267
+ loaderSize: 13,
268
+ };
269
+
270
+ case BUTTON_LINK_SIZES.sm:
271
+ return {
272
+ fontSize: 12,
273
+ lineHeight: 17,
274
+ loaderSize: 14,
275
+ };
276
+
277
+ case BUTTON_LINK_SIZES.lg:
278
+ return {
279
+ fontSize: 16,
280
+ lineHeight: 22,
281
+ loaderSize: 18,
282
+ };
283
+
284
+ case BUTTON_LINK_SIZES.md:
285
+ default:
286
+ return {
287
+ fontSize: 14,
288
+ lineHeight: 20,
289
+ loaderSize: 16,
290
+ };
291
+ }
292
+ }
293
+
294
+ function getVariantColor(variant, colors) {
295
+ switch (variant) {
296
+ case BUTTON_LINK_VARIANTS.secondary:
297
+ return colors.onSecondary || colors.text || "#111111";
298
+
299
+ case BUTTON_LINK_VARIANTS.success:
300
+ return colors.success || "#16A34A";
301
+
302
+ case BUTTON_LINK_VARIANTS.danger:
303
+ return colors.danger || "#DC2626";
304
+
305
+ case BUTTON_LINK_VARIANTS.warning:
306
+ return colors.warning || "#D97706";
307
+
308
+ case BUTTON_LINK_VARIANTS.neutral:
309
+ return colors.text || "#111111";
310
+
311
+ case BUTTON_LINK_VARIANTS.muted:
312
+ return colors.textMuted || "#737373";
313
+
314
+ case BUTTON_LINK_VARIANTS.primary:
315
+ default:
316
+ return colors.primary || "#FF5A1F";
317
+ }
318
+ }
319
+
320
+ function LoadingIndicator({ color, size }) {
321
+ return (
322
+ <Animated.View
323
+ style={[
324
+ styles.loader,
325
+
326
+ {
327
+ width: size,
328
+ height: size,
329
+
330
+ borderRadius: size / 2,
331
+
332
+ borderTopColor: color,
333
+
334
+ borderRightColor: color,
335
+
336
+ borderBottomColor: "transparent",
337
+
338
+ borderLeftColor: "transparent",
339
+ },
340
+ ]}
341
+ />
342
+ );
343
+ }
344
+
345
+ const styles = StyleSheet.create({
346
+ wrapper: {
347
+ alignSelf: "flex-start",
348
+ },
349
+
350
+ pressable: {
351
+ flexDirection: "row",
352
+
353
+ alignItems: "center",
354
+
355
+ justifyContent: "center",
356
+
357
+ minHeight: 24,
358
+ },
359
+
360
+ text: {
361
+ fontWeight: "600",
362
+
363
+ includeFontPadding: false,
364
+
365
+ textAlign: "center",
366
+ },
367
+
368
+ icon: {
369
+ alignItems: "center",
370
+
371
+ justifyContent: "center",
372
+ },
373
+
374
+ loader: {
375
+ borderWidth: 2,
376
+ },
377
+ });
378
+
379
+ export const UIButtonLink = memo(UIButtonLinkComponent);
380
+
381
+ UIButtonLink.displayName = "UIButtonLink";
382
+
383
+ export {
384
+ BUTTON_LINK_SIZES as UIButtonLinkSizes,
385
+ BUTTON_LINK_VARIANTS as UIButtonLinkVariants,
386
+ BUTTON_LINK_UNDERLINES as UIButtonLinkUnderlines,
387
+ };
388
+
389
+ export default UIButtonLink;
@@ -0,0 +1,6 @@
1
+ export {
2
+ UIButtonLink,
3
+ UIButtonLinkSizes,
4
+ UIButtonLinkVariants,
5
+ UIButtonLinkUnderlines,
6
+ } from "./UIButtonLink";