@appsupportkit/support-sdk 0.1.1 → 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/lib/commonjs/api.js +29 -0
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/components/BottomSheet.js +67 -47
- package/lib/commonjs/components/BottomSheet.js.map +1 -1
- package/lib/commonjs/components/LauncherButton.js +177 -0
- package/lib/commonjs/components/LauncherButton.js.map +1 -0
- package/lib/commonjs/components/SupportButton.js +76 -146
- package/lib/commonjs/components/SupportButton.js.map +1 -1
- package/lib/commonjs/components/icons.js +74 -23
- package/lib/commonjs/components/icons.js.map +1 -1
- package/lib/commonjs/components/primitives.js +183 -38
- package/lib/commonjs/components/primitives.js.map +1 -1
- package/lib/commonjs/components/screens/ComposeScreen.js +115 -58
- package/lib/commonjs/components/screens/ComposeScreen.js.map +1 -1
- package/lib/commonjs/components/screens/HomeScreen.js +170 -115
- package/lib/commonjs/components/screens/HomeScreen.js.map +1 -1
- package/lib/commonjs/components/screens/ThreadScreen.js +222 -108
- package/lib/commonjs/components/screens/ThreadScreen.js.map +1 -1
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/push.js +103 -0
- package/lib/commonjs/push.js.map +1 -0
- package/lib/commonjs/theme.js +136 -17
- package/lib/commonjs/theme.js.map +1 -1
- package/lib/commonjs/types.js +7 -0
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/unread.js +92 -0
- package/lib/commonjs/unread.js.map +1 -0
- package/lib/commonjs/useSupportThreads.js +286 -0
- package/lib/commonjs/useSupportThreads.js.map +1 -0
- package/lib/commonjs/utils.js +33 -0
- package/lib/commonjs/utils.js.map +1 -1
- package/lib/module/api.js +27 -0
- package/lib/module/api.js.map +1 -1
- package/lib/module/components/BottomSheet.js +68 -48
- package/lib/module/components/BottomSheet.js.map +1 -1
- package/lib/module/components/LauncherButton.js +172 -0
- package/lib/module/components/LauncherButton.js.map +1 -0
- package/lib/module/components/SupportButton.js +79 -149
- package/lib/module/components/SupportButton.js.map +1 -1
- package/lib/module/components/icons.js +72 -22
- package/lib/module/components/icons.js.map +1 -1
- package/lib/module/components/primitives.js +179 -39
- package/lib/module/components/primitives.js.map +1 -1
- package/lib/module/components/screens/ComposeScreen.js +118 -61
- package/lib/module/components/screens/ComposeScreen.js.map +1 -1
- package/lib/module/components/screens/HomeScreen.js +172 -117
- package/lib/module/components/screens/HomeScreen.js.map +1 -1
- package/lib/module/components/screens/ThreadScreen.js +228 -112
- package/lib/module/components/screens/ThreadScreen.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/push.js +98 -0
- package/lib/module/push.js.map +1 -0
- package/lib/module/theme.js +135 -16
- package/lib/module/theme.js.map +1 -1
- package/lib/module/types.js +7 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/unread.js +83 -0
- package/lib/module/unread.js.map +1 -0
- package/lib/module/useSupportThreads.js +281 -0
- package/lib/module/useSupportThreads.js.map +1 -0
- package/lib/module/utils.js +30 -0
- package/lib/module/utils.js.map +1 -1
- package/lib/typescript/api.d.ts +17 -1
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/components/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/components/LauncherButton.d.ts +14 -0
- package/lib/typescript/components/LauncherButton.d.ts.map +1 -0
- package/lib/typescript/components/SupportButton.d.ts +3 -3
- package/lib/typescript/components/SupportButton.d.ts.map +1 -1
- package/lib/typescript/components/icons.d.ts +13 -2
- package/lib/typescript/components/icons.d.ts.map +1 -1
- package/lib/typescript/components/primitives.d.ts +35 -4
- package/lib/typescript/components/primitives.d.ts.map +1 -1
- package/lib/typescript/components/screens/ComposeScreen.d.ts.map +1 -1
- package/lib/typescript/components/screens/HomeScreen.d.ts +3 -2
- package/lib/typescript/components/screens/HomeScreen.d.ts.map +1 -1
- package/lib/typescript/components/screens/ThreadScreen.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/push.d.ts +18 -0
- package/lib/typescript/push.d.ts.map +1 -0
- package/lib/typescript/theme.d.ts +44 -3
- package/lib/typescript/theme.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +37 -4
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/unread.d.ts +22 -0
- package/lib/typescript/unread.d.ts.map +1 -0
- package/lib/typescript/useSupportThreads.d.ts +28 -0
- package/lib/typescript/useSupportThreads.d.ts.map +1 -0
- package/lib/typescript/utils.d.ts +5 -0
- package/lib/typescript/utils.d.ts.map +1 -1
- package/package.json +8 -2
- package/src/api.ts +41 -1
- package/src/components/BottomSheet.tsx +61 -43
- package/src/components/LauncherButton.tsx +217 -0
- package/src/components/SupportButton.tsx +81 -162
- package/src/components/icons.tsx +77 -19
- package/src/components/primitives.tsx +217 -36
- package/src/components/screens/ComposeScreen.tsx +131 -61
- package/src/components/screens/HomeScreen.tsx +168 -117
- package/src/components/screens/ThreadScreen.tsx +289 -119
- package/src/index.tsx +3 -0
- package/src/push.ts +97 -0
- package/src/theme.ts +165 -18
- package/src/types.ts +39 -4
- package/src/unread.ts +91 -0
- package/src/useSupportThreads.ts +372 -0
- package/src/utils.ts +33 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// Bottom sheet built on the built-in Modal + Animated APIs.
|
|
2
2
|
//
|
|
3
3
|
// Why no sheet library: the SDK needs exactly one detent, a drag handle, a
|
|
4
|
-
// backdrop fade, and a
|
|
4
|
+
// backdrop fade, and a short ease-out slide — all achievable with core APIs.
|
|
5
5
|
// Skipping reanimated/gesture-handler keeps the package free of native
|
|
6
6
|
// dependencies and install friction for consumers.
|
|
7
|
-
|
|
7
|
+
//
|
|
8
|
+
// `visible` drives a single animation path: the sheet stays mounted until
|
|
9
|
+
// its exit finishes, so closing from the header button, the backdrop, the
|
|
10
|
+
// hardware back button, or a drag all animate identically — and a reopen
|
|
11
|
+
// always starts from off-screen instead of appearing already in place.
|
|
12
|
+
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
|
|
8
13
|
import {
|
|
9
14
|
Animated,
|
|
10
15
|
Easing,
|
|
@@ -17,7 +22,8 @@ import {
|
|
|
17
22
|
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
|
18
23
|
import type { SupportTheme } from '../theme';
|
|
19
24
|
|
|
20
|
-
const SLIDE_MS =
|
|
25
|
+
const SLIDE_MS = 260;
|
|
26
|
+
const FADE_MS = 200;
|
|
21
27
|
|
|
22
28
|
export function BottomSheet({
|
|
23
29
|
visible,
|
|
@@ -31,57 +37,67 @@ export function BottomSheet({
|
|
|
31
37
|
children: ReactNode;
|
|
32
38
|
}) {
|
|
33
39
|
const { height: windowHeight } = useWindowDimensions();
|
|
34
|
-
const sheetHeight = Math.min(windowHeight * 0.
|
|
40
|
+
const sheetHeight = Math.min(windowHeight * 0.88, 720);
|
|
35
41
|
|
|
42
|
+
const [rendered, setRendered] = useState(false);
|
|
36
43
|
const translateY = useRef(new Animated.Value(sheetHeight)).current;
|
|
37
44
|
const backdrop = useRef(new Animated.Value(0)).current;
|
|
45
|
+
/** Whether the entrance has already played for the current open. */
|
|
46
|
+
const shown = useRef(false);
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
toValue: 0,
|
|
43
|
-
duration: SLIDE_MS,
|
|
44
|
-
easing: Easing.out(Easing.cubic),
|
|
45
|
-
useNativeDriver: true,
|
|
46
|
-
}),
|
|
47
|
-
Animated.timing(backdrop, {
|
|
48
|
-
toValue: 1,
|
|
49
|
-
duration: SLIDE_MS,
|
|
50
|
-
easing: Easing.out(Easing.cubic),
|
|
51
|
-
useNativeDriver: true,
|
|
52
|
-
}),
|
|
53
|
-
]).start();
|
|
54
|
-
}, [translateY, backdrop]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (visible) setRendered(true);
|
|
50
|
+
}, [visible]);
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!rendered) return;
|
|
54
|
+
|
|
55
|
+
if (visible && !shown.current) {
|
|
56
|
+
shown.current = true;
|
|
57
|
+
translateY.setValue(sheetHeight);
|
|
58
58
|
Animated.parallel([
|
|
59
59
|
Animated.timing(translateY, {
|
|
60
|
-
toValue:
|
|
60
|
+
toValue: 0,
|
|
61
61
|
duration: SLIDE_MS,
|
|
62
|
+
easing: Easing.bezier(0.16, 1, 0.3, 1),
|
|
63
|
+
useNativeDriver: true,
|
|
64
|
+
}),
|
|
65
|
+
Animated.timing(backdrop, {
|
|
66
|
+
toValue: 1,
|
|
67
|
+
duration: FADE_MS,
|
|
62
68
|
easing: Easing.out(Easing.cubic),
|
|
63
69
|
useNativeDriver: true,
|
|
64
70
|
}),
|
|
71
|
+
]).start();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!visible && shown.current) {
|
|
76
|
+
shown.current = false;
|
|
77
|
+
// Starts from wherever the sheet is now, so a drag that crosses the
|
|
78
|
+
// dismiss threshold continues rather than jumping.
|
|
79
|
+
Animated.parallel([
|
|
80
|
+
Animated.timing(translateY, {
|
|
81
|
+
toValue: sheetHeight,
|
|
82
|
+
duration: theme.motion.slow,
|
|
83
|
+
easing: Easing.in(Easing.cubic),
|
|
84
|
+
useNativeDriver: true,
|
|
85
|
+
}),
|
|
65
86
|
Animated.timing(backdrop, {
|
|
66
87
|
toValue: 0,
|
|
67
|
-
duration:
|
|
68
|
-
easing: Easing.
|
|
88
|
+
duration: theme.motion.slow,
|
|
89
|
+
easing: Easing.in(Easing.cubic),
|
|
69
90
|
useNativeDriver: true,
|
|
70
91
|
}),
|
|
71
92
|
]).start(({ finished }) => {
|
|
72
|
-
if (finished)
|
|
93
|
+
if (finished) setRendered(false);
|
|
73
94
|
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
if (visible) animateIn();
|
|
80
|
-
}, [visible, animateIn]);
|
|
95
|
+
}
|
|
96
|
+
}, [rendered, visible, sheetHeight, translateY, backdrop, theme.motion.slow]);
|
|
81
97
|
|
|
82
98
|
const requestClose = useCallback(() => {
|
|
83
|
-
|
|
84
|
-
}, [
|
|
99
|
+
onClose();
|
|
100
|
+
}, [onClose]);
|
|
85
101
|
|
|
86
102
|
// Drag handle: follow the finger downward; past 25% (or a flick), dismiss.
|
|
87
103
|
const panResponder = useRef(
|
|
@@ -97,7 +113,7 @@ export function BottomSheet({
|
|
|
97
113
|
} else {
|
|
98
114
|
Animated.timing(translateY, {
|
|
99
115
|
toValue: 0,
|
|
100
|
-
duration:
|
|
116
|
+
duration: theme.motion.slow,
|
|
101
117
|
easing: Easing.out(Easing.cubic),
|
|
102
118
|
useNativeDriver: true,
|
|
103
119
|
}).start();
|
|
@@ -106,7 +122,7 @@ export function BottomSheet({
|
|
|
106
122
|
}),
|
|
107
123
|
).current;
|
|
108
124
|
|
|
109
|
-
if (!
|
|
125
|
+
if (!rendered) return null;
|
|
110
126
|
|
|
111
127
|
return (
|
|
112
128
|
<Modal
|
|
@@ -125,7 +141,7 @@ export function BottomSheet({
|
|
|
125
141
|
right: 0,
|
|
126
142
|
bottom: 0,
|
|
127
143
|
left: 0,
|
|
128
|
-
backgroundColor: 'rgba(0,0,0,0.
|
|
144
|
+
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
129
145
|
opacity: backdrop,
|
|
130
146
|
}}
|
|
131
147
|
>
|
|
@@ -139,25 +155,27 @@ export function BottomSheet({
|
|
|
139
155
|
<Animated.View
|
|
140
156
|
style={{
|
|
141
157
|
height: sheetHeight,
|
|
142
|
-
backgroundColor: theme.colors.
|
|
158
|
+
backgroundColor: theme.colors.bg,
|
|
143
159
|
borderTopLeftRadius: theme.radii.sheet,
|
|
144
160
|
borderTopRightRadius: theme.radii.sheet,
|
|
145
161
|
borderWidth: 1,
|
|
146
162
|
borderBottomWidth: 0,
|
|
147
163
|
borderColor: theme.colors.border,
|
|
164
|
+
overflow: 'hidden',
|
|
148
165
|
transform: [{ translateY }],
|
|
166
|
+
...theme.shadows.sheet,
|
|
149
167
|
}}
|
|
150
168
|
>
|
|
151
169
|
{/* Drag handle */}
|
|
152
170
|
<View
|
|
153
171
|
{...panResponder.panHandlers}
|
|
154
|
-
style={{ alignItems: 'center',
|
|
172
|
+
style={{ alignItems: 'center', paddingTop: 10, paddingBottom: 6 }}
|
|
155
173
|
>
|
|
156
174
|
<View
|
|
157
175
|
style={{
|
|
158
|
-
width:
|
|
159
|
-
height:
|
|
160
|
-
borderRadius:
|
|
176
|
+
width: 40,
|
|
177
|
+
height: 5,
|
|
178
|
+
borderRadius: 3,
|
|
161
179
|
backgroundColor: theme.colors.borderStrong,
|
|
162
180
|
}}
|
|
163
181
|
/>
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// The launcher: the one piece of the SDK that lives inside the host app's
|
|
2
|
+
// own UI, so it has to look intentional next to anything. Three variants
|
|
3
|
+
// cover the placements that actually occur — a floating action button, a
|
|
4
|
+
// bordered button in a header, and a bare icon in a toolbar.
|
|
5
|
+
import { useEffect, useRef } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
Animated,
|
|
8
|
+
Easing,
|
|
9
|
+
Pressable,
|
|
10
|
+
Text,
|
|
11
|
+
View,
|
|
12
|
+
type StyleProp,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
import type { SupportTheme } from '../theme';
|
|
16
|
+
import type { LauncherVariant } from '../types';
|
|
17
|
+
import { ChatBubbleMark } from './icons';
|
|
18
|
+
|
|
19
|
+
const PRESS_SCALE = 0.93;
|
|
20
|
+
|
|
21
|
+
/** Counts stay legible: three digits in a pill is not a design. */
|
|
22
|
+
function badgeLabel(unread: number): string {
|
|
23
|
+
return unread > 99 ? '99+' : String(unread);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function LauncherButton({
|
|
27
|
+
theme,
|
|
28
|
+
unread,
|
|
29
|
+
variant = 'solid',
|
|
30
|
+
size = 52,
|
|
31
|
+
label,
|
|
32
|
+
style,
|
|
33
|
+
onPress,
|
|
34
|
+
}: {
|
|
35
|
+
theme: SupportTheme;
|
|
36
|
+
unread: number;
|
|
37
|
+
variant?: LauncherVariant;
|
|
38
|
+
size?: number;
|
|
39
|
+
/** Renders a pill with text next to the mark instead of a circle. */
|
|
40
|
+
label?: string;
|
|
41
|
+
style?: StyleProp<ViewStyle>;
|
|
42
|
+
onPress: () => void;
|
|
43
|
+
}) {
|
|
44
|
+
const scale = useRef(new Animated.Value(1)).current;
|
|
45
|
+
const pulse = useRef(new Animated.Value(0)).current;
|
|
46
|
+
const previousUnread = useRef(unread);
|
|
47
|
+
|
|
48
|
+
// A new reply pulses the halo twice, then stops. Enough to catch the eye
|
|
49
|
+
// on the way past; not a permanently animating element in someone's app.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (unread > previousUnread.current) {
|
|
52
|
+
pulse.setValue(0);
|
|
53
|
+
const animation = Animated.loop(
|
|
54
|
+
Animated.timing(pulse, {
|
|
55
|
+
toValue: 1,
|
|
56
|
+
duration: 1600,
|
|
57
|
+
easing: Easing.out(Easing.ease),
|
|
58
|
+
useNativeDriver: true,
|
|
59
|
+
}),
|
|
60
|
+
{ iterations: 2 },
|
|
61
|
+
);
|
|
62
|
+
animation.start(() => pulse.setValue(0));
|
|
63
|
+
previousUnread.current = unread;
|
|
64
|
+
return () => animation.stop();
|
|
65
|
+
}
|
|
66
|
+
previousUnread.current = unread;
|
|
67
|
+
return undefined;
|
|
68
|
+
}, [unread, pulse]);
|
|
69
|
+
|
|
70
|
+
const press = (to: number) =>
|
|
71
|
+
Animated.timing(scale, {
|
|
72
|
+
toValue: to,
|
|
73
|
+
duration: to === 1 ? theme.motion.base : theme.motion.fast,
|
|
74
|
+
easing: Easing.out(Easing.cubic),
|
|
75
|
+
useNativeDriver: true,
|
|
76
|
+
}).start();
|
|
77
|
+
|
|
78
|
+
const solid = variant === 'solid';
|
|
79
|
+
const soft = variant === 'soft';
|
|
80
|
+
const fill = solid
|
|
81
|
+
? theme.colors.accent
|
|
82
|
+
: soft
|
|
83
|
+
? theme.colors.surface
|
|
84
|
+
: 'transparent';
|
|
85
|
+
const markColor = solid
|
|
86
|
+
? theme.colors.accentForeground
|
|
87
|
+
: soft
|
|
88
|
+
? theme.colors.accent
|
|
89
|
+
: theme.launcher.minimalMark;
|
|
90
|
+
// Dots are punched out of the bubble in whatever sits behind the mark —
|
|
91
|
+
// the button's own fill, or the app background when there isn't one.
|
|
92
|
+
const markHoleColor = solid
|
|
93
|
+
? theme.colors.accent
|
|
94
|
+
: soft
|
|
95
|
+
? theme.colors.surface
|
|
96
|
+
: theme.colors.bg;
|
|
97
|
+
// Dark: a hairline separates the fill from a near-black app background.
|
|
98
|
+
// Light: the accent-tinted shadow already does that, so the ring only has
|
|
99
|
+
// to keep the edge from looking soft.
|
|
100
|
+
const border = solid
|
|
101
|
+
? { borderWidth: 1, borderColor: theme.launcher.solidRing }
|
|
102
|
+
: soft
|
|
103
|
+
? { borderWidth: 1, borderColor: theme.colors.border }
|
|
104
|
+
: { borderWidth: 0, borderColor: 'transparent' };
|
|
105
|
+
const badgeBg = solid ? theme.colors.accentForeground : theme.colors.accent;
|
|
106
|
+
const badgeFg = solid ? theme.colors.accent : theme.colors.accentForeground;
|
|
107
|
+
|
|
108
|
+
const height = label ? Math.max(44, size * 0.84) : size;
|
|
109
|
+
const markSize = Math.round(size * (label ? 0.36 : 0.42));
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Animated.View
|
|
113
|
+
style={[{ transform: [{ scale }] }, style]}
|
|
114
|
+
// The halo and badge sit outside the button's bounds.
|
|
115
|
+
pointerEvents="box-none"
|
|
116
|
+
>
|
|
117
|
+
{unread > 0 && (
|
|
118
|
+
<Animated.View
|
|
119
|
+
pointerEvents="none"
|
|
120
|
+
style={{
|
|
121
|
+
position: 'absolute',
|
|
122
|
+
top: 0,
|
|
123
|
+
left: 0,
|
|
124
|
+
width: label ? undefined : size,
|
|
125
|
+
right: label ? 0 : undefined,
|
|
126
|
+
height,
|
|
127
|
+
borderRadius: theme.radii.pill,
|
|
128
|
+
backgroundColor: theme.colors.accent,
|
|
129
|
+
opacity: pulse.interpolate({
|
|
130
|
+
inputRange: [0, 1],
|
|
131
|
+
outputRange: [0.28, 0],
|
|
132
|
+
}),
|
|
133
|
+
transform: [
|
|
134
|
+
{
|
|
135
|
+
scale: pulse.interpolate({
|
|
136
|
+
inputRange: [0, 1],
|
|
137
|
+
outputRange: [1, 1.5],
|
|
138
|
+
}),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
<Pressable
|
|
146
|
+
onPress={onPress}
|
|
147
|
+
onPressIn={() => press(PRESS_SCALE)}
|
|
148
|
+
onPressOut={() => press(1)}
|
|
149
|
+
hitSlop={8}
|
|
150
|
+
accessibilityRole="button"
|
|
151
|
+
accessibilityLabel={
|
|
152
|
+
unread > 0
|
|
153
|
+
? `Support, ${unread} unread ${unread === 1 ? 'reply' : 'replies'}`
|
|
154
|
+
: 'Support'
|
|
155
|
+
}
|
|
156
|
+
style={{
|
|
157
|
+
height,
|
|
158
|
+
width: label ? undefined : size,
|
|
159
|
+
paddingHorizontal: label ? 18 : 0,
|
|
160
|
+
flexDirection: 'row',
|
|
161
|
+
alignItems: 'center',
|
|
162
|
+
justifyContent: 'center',
|
|
163
|
+
gap: 9,
|
|
164
|
+
borderRadius: theme.radii.pill,
|
|
165
|
+
backgroundColor: fill,
|
|
166
|
+
...border,
|
|
167
|
+
...(variant === 'minimal' ? null : theme.shadows.launcher),
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<ChatBubbleMark color={markColor} dotColor={markHoleColor} size={markSize} />
|
|
171
|
+
{label !== undefined && (
|
|
172
|
+
<Text
|
|
173
|
+
style={{
|
|
174
|
+
fontSize: 15,
|
|
175
|
+
fontWeight: '600',
|
|
176
|
+
letterSpacing: -0.1,
|
|
177
|
+
color: solid ? theme.colors.accentForeground : theme.colors.textPrimary,
|
|
178
|
+
}}
|
|
179
|
+
>
|
|
180
|
+
{label}
|
|
181
|
+
</Text>
|
|
182
|
+
)}
|
|
183
|
+
</Pressable>
|
|
184
|
+
|
|
185
|
+
{unread > 0 && (
|
|
186
|
+
<View
|
|
187
|
+
pointerEvents="none"
|
|
188
|
+
style={{
|
|
189
|
+
position: 'absolute',
|
|
190
|
+
top: -5,
|
|
191
|
+
right: -5,
|
|
192
|
+
minWidth: 19,
|
|
193
|
+
height: 19,
|
|
194
|
+
borderRadius: theme.radii.pill,
|
|
195
|
+
paddingHorizontal: 5,
|
|
196
|
+
backgroundColor: badgeBg,
|
|
197
|
+
alignItems: 'center',
|
|
198
|
+
justifyContent: 'center',
|
|
199
|
+
...theme.shadows.badge,
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
<Text
|
|
203
|
+
style={{
|
|
204
|
+
fontSize: 11,
|
|
205
|
+
lineHeight: 14,
|
|
206
|
+
fontWeight: '700',
|
|
207
|
+
color: badgeFg,
|
|
208
|
+
fontVariant: ['tabular-nums'],
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{badgeLabel(unread)}
|
|
212
|
+
</Text>
|
|
213
|
+
</View>
|
|
214
|
+
)}
|
|
215
|
+
</Animated.View>
|
|
216
|
+
);
|
|
217
|
+
}
|