@upstep/react-native 0.5.1 → 0.6.1

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": "@upstep/react-native",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "Upstep feedback SDK for React Native",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -15,11 +15,8 @@
15
15
  "@upstep/types": "0.5.0"
16
16
  },
17
17
  "peerDependencies": {
18
- "@gorhom/bottom-sheet": ">=4",
19
18
  "react": ">=18",
20
- "react-native": ">=0.73",
21
- "react-native-gesture-handler": ">=2",
22
- "react-native-reanimated": ">=3"
19
+ "react-native": ">=0.73"
23
20
  },
24
21
  "devDependencies": {
25
22
  "@types/react": "^19.0.7",
@@ -4,7 +4,6 @@ import React, {
4
4
  useEffect,
5
5
  useState,
6
6
  useCallback,
7
- useRef,
8
7
  type ReactNode,
9
8
  } from "react";
10
9
  import type {
@@ -15,7 +14,6 @@ import type {
15
14
  VoteValue,
16
15
  } from "@upstep/types";
17
16
  import { UpstepApiClient } from "./api";
18
- import type BottomSheet from "@gorhom/bottom-sheet";
19
17
 
20
18
  const DEFAULT_ACCENT = "#D97757";
21
19
 
@@ -27,11 +25,10 @@ interface UpstepContextValue {
27
25
  submit: (title: string, content: string, type?: FeedbackType) => Promise<void>;
28
26
  vote: (feedbackId: string, value: VoteValue) => Promise<void>;
29
27
  getItem: (feedbackId: string) => Promise<FeedbackWithComments>;
30
- /** Open the feedback sheet — call from your own UI (settings, menu, etc.). */
28
+ isOpen: boolean;
31
29
  openSheet: () => void;
32
- /** Set / update the end-user id at runtime (e.g. after the user logs in). */
30
+ closeSheet: () => void;
33
31
  identify: (userId: string | undefined) => void;
34
- sheetRef: React.RefObject<BottomSheet | null>;
35
32
  accentColor: string;
36
33
  theme: "light" | "dark" | "auto";
37
34
  }
@@ -45,7 +42,7 @@ export function FeedbackProvider({
45
42
  const [client] = useState(() => new UpstepApiClient(config));
46
43
  const [feedItems, setFeedItems] = useState<Feedback[]>([]);
47
44
  const [feedLoading, setFeedLoading] = useState(true);
48
- const sheetRef = useRef<BottomSheet>(null);
45
+ const [isOpen, setIsOpen] = useState(false);
49
46
 
50
47
  useEffect(() => {
51
48
  client.setUserId(config.userId);
@@ -87,7 +84,11 @@ export function FeedbackProvider({
87
84
  );
88
85
 
89
86
  const openSheet = useCallback(() => {
90
- sheetRef.current?.expand();
87
+ setIsOpen(true);
88
+ }, []);
89
+
90
+ const closeSheet = useCallback(() => {
91
+ setIsOpen(false);
91
92
  }, []);
92
93
 
93
94
  const identify = useCallback(
@@ -99,7 +100,7 @@ export function FeedbackProvider({
99
100
  <UpstepContext.Provider
100
101
  value={{
101
102
  client, feedItems, feedLoading, loadFeed, submit, vote, getItem,
102
- openSheet, identify, sheetRef,
103
+ isOpen, openSheet, closeSheet, identify,
103
104
  accentColor: config.accentColor ?? DEFAULT_ACCENT,
104
105
  theme: config.theme ?? "auto",
105
106
  }}
@@ -1,20 +1,20 @@
1
- import React, { useState, useMemo, useCallback, useEffect } from "react";
1
+ import React, { useState, useCallback, useEffect } from "react";
2
2
  import {
3
+ Modal,
3
4
  View,
4
5
  Text,
5
6
  TextInput,
6
7
  TouchableOpacity,
8
+ Pressable,
9
+ ScrollView,
10
+ KeyboardAvoidingView,
7
11
  StyleSheet,
8
12
  ActivityIndicator,
9
13
  useColorScheme,
10
14
  Platform,
15
+ SafeAreaView,
16
+ Linking,
11
17
  } from "react-native";
12
- import BottomSheet, {
13
- BottomSheetView,
14
- BottomSheetScrollView,
15
- BottomSheetBackdrop,
16
- } from "@gorhom/bottom-sheet";
17
- import type { BottomSheetBackdropProps } from "@gorhom/bottom-sheet";
18
18
  import type { Comment, Feedback, FeedbackType, FeedbackWithComments } from "@upstep/types";
19
19
  import { useUpstep } from "./FeedbackContext";
20
20
 
@@ -58,116 +58,130 @@ type Nav =
58
58
  | { screen: "create" };
59
59
 
60
60
  // ─── FeedbackSheet ────────────────────────────────────────────────────────────
61
- //
62
- // IMPORTANT: BottomSheet requires stable direct children — never conditionally
63
- // render <BottomSheetView> or <BottomSheetScrollView> as they set up gesture
64
- // handlers on mount. We keep ONE of each always mounted; only their contents
65
- // change based on `nav`. This also fixes TouchableOpacity not responding.
66
61
 
67
62
  export function FeedbackSheet() {
68
- const { sheetRef, accentColor, theme, loadFeed } = useUpstep();
63
+ const { isOpen, closeSheet, accentColor, theme, loadFeed } = useUpstep();
69
64
  const scheme = useColorScheme();
70
65
  const isDark = theme === "dark" || (theme === "auto" && scheme === "dark");
71
66
  const p = isDark ? DARK : LIGHT;
72
67
 
73
68
  const [nav, setNav] = useState<Nav>({ screen: "list" });
74
- const snapPoints = useMemo(() => ["90%"], []);
75
-
76
- // Dim backdrop — tap to close
77
- const renderBackdrop = useCallback(
78
- (props: BottomSheetBackdropProps) => (
79
- <BottomSheetBackdrop
80
- {...props}
81
- disappearsOnIndex={-1}
82
- appearsOnIndex={0}
83
- pressBehavior="close"
84
- />
85
- ),
86
- []
87
- );
69
+
70
+ // Refresh list every time the modal opens
71
+ useEffect(() => {
72
+ if (isOpen) loadFeed();
73
+ }, [isOpen, loadFeed]);
74
+
75
+ function handleClose() {
76
+ closeSheet();
77
+ // Reset to list when closed so next open starts fresh
78
+ setTimeout(() => setNav({ screen: "list" }), 300);
79
+ }
88
80
 
89
81
  return (
90
- <BottomSheet
91
- ref={sheetRef}
92
- index={-1}
93
- snapPoints={snapPoints}
94
- enablePanDownToClose
95
- backdropComponent={renderBackdrop}
96
- backgroundStyle={{ backgroundColor: p.bg, borderRadius: 24 }}
97
- handleIndicatorStyle={{ backgroundColor: p.handle, width: 40, height: 4 }}
98
- onChange={(index) => { if (index >= 0) loadFeed(); }}
99
- onClose={() => setNav({ screen: "list" })}
82
+ <Modal
83
+ visible={isOpen}
84
+ animationType="slide"
85
+ transparent
86
+ statusBarTranslucent
87
+ onRequestClose={handleClose}
100
88
  >
101
- {/*
102
- Wrap everything in ONE BottomSheetView so the library can measure the
103
- sheet correctly. A plain View header stays fixed; BottomSheetScrollView
104
- fills the remaining space below it — no overlap.
105
- */}
106
- <BottomSheetView style={styles.sheetContainer}>
107
- {/* ── Fixed header ─────────────────────────────── */}
108
- <View style={[styles.header, { borderBottomColor: p.border }]}>
109
- {nav.screen === "list" ? (
110
- <>
111
- <Text style={[styles.headerTitle, { color: p.text }]}>Feedback</Text>
112
- <TouchableOpacity
113
- onPress={() => setNav({ screen: "create" })}
114
- activeOpacity={0.8}
115
- style={[styles.newBtn, { backgroundColor: accentColor }]}
116
- >
117
- <Text style={styles.newBtnText}>+ New</Text>
118
- </TouchableOpacity>
119
- </>
120
- ) : nav.screen === "create" ? (
121
- <>
89
+ <KeyboardAvoidingView
90
+ style={styles.overlay}
91
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
92
+ >
93
+ {/* Tap backdrop to dismiss */}
94
+ <Pressable style={styles.backdrop} onPress={handleClose} />
95
+
96
+ {/* Sheet */}
97
+ <View style={[styles.sheet, { backgroundColor: p.bg }]}>
98
+ {/* Drag handle */}
99
+ <View style={styles.handleWrap}>
100
+ <View style={[styles.handle, { backgroundColor: p.handle }]} />
101
+ </View>
102
+
103
+ {/* Fixed header */}
104
+ <View style={[styles.header, { borderBottomColor: p.border }]}>
105
+ {nav.screen === "list" ? (
106
+ <>
107
+ <Text style={[styles.headerTitle, { color: p.text }]}>Feedback</Text>
108
+ <TouchableOpacity
109
+ onPress={() => setNav({ screen: "create" })}
110
+ activeOpacity={0.8}
111
+ style={[styles.newBtn, { backgroundColor: accentColor }]}
112
+ >
113
+ <Text style={styles.newBtnText}>+ New</Text>
114
+ </TouchableOpacity>
115
+ </>
116
+ ) : nav.screen === "create" ? (
117
+ <>
118
+ <TouchableOpacity
119
+ onPress={() => setNav({ screen: "list" })}
120
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 16 }}
121
+ >
122
+ <Text style={[styles.navBtn, { color: accentColor }]}>Cancel</Text>
123
+ </TouchableOpacity>
124
+ <Text style={[styles.headerTitle, { color: p.text }]}>New feedback</Text>
125
+ <View style={styles.navBtnSpacer} />
126
+ </>
127
+ ) : (
122
128
  <TouchableOpacity
123
129
  onPress={() => setNav({ screen: "list" })}
124
- hitSlop={{ top: 8, bottom: 8, left: 8, right: 16 }}
130
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 16 }}
125
131
  >
126
- <Text style={[styles.navBtn, { color: accentColor }]}>Cancel</Text>
132
+ <Text style={[styles.navBtn, { color: accentColor }]}>← Back</Text>
127
133
  </TouchableOpacity>
128
- <Text style={[styles.headerTitle, { color: p.text }]}>New feedback</Text>
129
- <View style={styles.navBtnPlaceholder} />
130
- </>
131
- ) : (
132
- <TouchableOpacity
133
- onPress={() => setNav({ screen: "list" })}
134
- hitSlop={{ top: 8, bottom: 8, left: 8, right: 16 }}
135
- >
136
- <Text style={[styles.navBtn, { color: accentColor }]}>← Back</Text>
137
- </TouchableOpacity>
138
- )}
139
- </View>
134
+ )}
135
+ </View>
136
+
137
+ {/* Scrollable body */}
138
+ <ScrollView
139
+ style={styles.scroll}
140
+ contentContainerStyle={styles.scrollContent}
141
+ keyboardShouldPersistTaps="handled"
142
+ showsVerticalScrollIndicator={false}
143
+ >
144
+ {nav.screen === "list" && (
145
+ <FeedListBody
146
+ p={p}
147
+ accent={accentColor}
148
+ onSelect={(id) => setNav({ screen: "detail", feedbackId: id })}
149
+ />
150
+ )}
151
+ {nav.screen === "detail" && "feedbackId" in nav && (
152
+ <FeedDetailBody
153
+ p={p}
154
+ accent={accentColor}
155
+ feedbackId={(nav as { screen: "detail"; feedbackId: string }).feedbackId}
156
+ />
157
+ )}
158
+ {nav.screen === "create" && (
159
+ <FeedCreateBody
160
+ p={p}
161
+ accent={accentColor}
162
+ onDone={() => {
163
+ setNav({ screen: "list" });
164
+ }}
165
+ />
166
+ )}
167
+ </ScrollView>
168
+
169
+ {/* Branding */}
170
+ <TouchableOpacity
171
+ onPress={() => Linking.openURL("https://upstep.dev")}
172
+ activeOpacity={0.6}
173
+ style={[styles.poweredBy, { borderTopColor: p.border }]}
174
+ >
175
+ <Text style={[styles.poweredByText, { color: p.textFaint }]}>
176
+ Powered by <Text style={{ fontWeight: "700" }}>Upstep.dev</Text>
177
+ </Text>
178
+ </TouchableOpacity>
140
179
 
141
- {/* ── Scrollable body ───────────────────────────── */}
142
- <BottomSheetScrollView
143
- style={styles.scrollView}
144
- contentContainerStyle={styles.scrollBody}
145
- keyboardShouldPersistTaps="handled"
146
- >
147
- {nav.screen === "list" && (
148
- <FeedListBody
149
- p={p}
150
- accent={accentColor}
151
- onSelect={(id) => setNav({ screen: "detail", feedbackId: id })}
152
- />
153
- )}
154
- {nav.screen === "detail" && "feedbackId" in nav && (
155
- <FeedDetailBody
156
- p={p}
157
- accent={accentColor}
158
- feedbackId={(nav as { screen: "detail"; feedbackId: string }).feedbackId}
159
- />
160
- )}
161
- {nav.screen === "create" && (
162
- <FeedCreateBody
163
- p={p}
164
- accent={accentColor}
165
- onDone={() => setNav({ screen: "list" })}
166
- />
167
- )}
168
- </BottomSheetScrollView>
169
- </BottomSheetView>
170
- </BottomSheet>
180
+ {/* Safe area spacer — fills the home-indicator / nav-bar zone */}
181
+ <SafeAreaView style={{ backgroundColor: p.bg }} />
182
+ </View>
183
+ </KeyboardAvoidingView>
184
+ </Modal>
171
185
  );
172
186
  }
173
187
 
@@ -176,8 +190,7 @@ export function FeedbackSheet() {
176
190
  function FeedListBody({
177
191
  p, accent, onSelect,
178
192
  }: {
179
- p: Palette; accent: string;
180
- onSelect: (id: string) => void;
193
+ p: Palette; accent: string; onSelect: (id: string) => void;
181
194
  }) {
182
195
  const { feedItems, feedLoading, vote } = useUpstep();
183
196
 
@@ -200,7 +213,7 @@ function FeedListBody({
200
213
  }
201
214
 
202
215
  return (
203
- <View style={styles.listPad}>
216
+ <>
204
217
  {feedItems.map((f) => (
205
218
  <FeedCard
206
219
  key={f.id}
@@ -211,7 +224,7 @@ function FeedListBody({
211
224
  onVote={() => vote(f.id, "UP")}
212
225
  />
213
226
  ))}
214
- </View>
227
+ </>
215
228
  );
216
229
  }
217
230
 
@@ -219,8 +232,7 @@ function FeedCard({
219
232
  item, p, accent, onPress, onVote,
220
233
  }: {
221
234
  item: Feedback; p: Palette; accent: string;
222
- onPress: () => void;
223
- onVote: () => void;
235
+ onPress: () => void; onVote: () => void;
224
236
  }) {
225
237
  const voted = item.userVote === "UP";
226
238
  const displayTitle = item.title ?? item.content;
@@ -259,9 +271,7 @@ function FeedCard({
259
271
  <View style={styles.cardMeta}>
260
272
  <TypeBadge type={item.type} p={p} />
261
273
  <StatusDot status={item.status} />
262
- <Text style={[styles.cardDate, { color: p.textFaint }]}>
263
- {fmtDate(item.createdAt)}
264
- </Text>
274
+ <Text style={[styles.cardDate, { color: p.textFaint }]}>{fmtDate(item.createdAt)}</Text>
265
275
  </View>
266
276
  </View>
267
277
 
@@ -350,9 +360,7 @@ function FeedDetailBody({
350
360
 
351
361
  {item.comments.length > 0 && (
352
362
  <View style={[styles.commentsSection, { borderTopColor: p.border }]}>
353
- <Text style={[styles.commentsLabel, { color: p.textFaint }]}>
354
- DEVELOPER RESPONSE
355
- </Text>
363
+ <Text style={[styles.commentsLabel, { color: p.textFaint }]}>DEVELOPER RESPONSE</Text>
356
364
  {item.comments.map((c) => (
357
365
  <CommentBubble key={c.id} comment={c} p={p} accent={accent} />
358
366
  ))}
@@ -371,9 +379,7 @@ function CommentBubble({ comment, p, accent }: { comment: Comment; p: Palette; a
371
379
  {comment.authorName ?? "Team"}
372
380
  </Text>
373
381
  </View>
374
- <Text style={[styles.commentDate, { color: p.textFaint }]}>
375
- {fmtDate(comment.createdAt)}
376
- </Text>
382
+ <Text style={[styles.commentDate, { color: p.textFaint }]}>{fmtDate(comment.createdAt)}</Text>
377
383
  </View>
378
384
  <Text style={[styles.commentContent, { color: p.textSoft }]}>{comment.content}</Text>
379
385
  </View>
@@ -409,7 +415,7 @@ function FeedCreateBody({
409
415
  try {
410
416
  await submit(title.trim(), description.trim() || title.trim(), type);
411
417
  setSuccess(true);
412
- setTimeout(onDone, 1400);
418
+ setTimeout(onDone, 1200);
413
419
  } catch {
414
420
  setError("Something went wrong. Please try again.");
415
421
  } finally {
@@ -419,7 +425,7 @@ function FeedCreateBody({
419
425
 
420
426
  if (success) {
421
427
  return (
422
- <View style={[styles.centered, { paddingTop: 60 }]}>
428
+ <View style={[styles.centered, { paddingTop: 40 }]}>
423
429
  <Text style={[styles.successTitle, { color: p.text }]}>Submitted</Text>
424
430
  <Text style={[styles.successSub, { color: p.textFaint }]}>
425
431
  Thanks — we received your feedback.
@@ -519,11 +525,8 @@ function TypeBadge({ type, p }: { type: FeedbackType; p: Palette }) {
519
525
  }
520
526
 
521
527
  const STATUS_COLOR: Record<string, string> = {
522
- OPEN: "#b45309",
523
- IN_PROGRESS: "#1d4ed8",
524
- DONE: "#15803d",
525
- PENDING: "#ea580c",
526
- CLOSED: "#6b7280",
528
+ OPEN: "#b45309", IN_PROGRESS: "#1d4ed8", DONE: "#15803d",
529
+ PENDING: "#ea580c", CLOSED: "#6b7280",
527
530
  };
528
531
 
529
532
  function StatusDot({ status }: { status: string }) {
@@ -534,9 +537,7 @@ function StatusBadge({ status, p }: { status: string; p: Palette }) {
534
537
  return (
535
538
  <View style={[styles.statusBadge, { backgroundColor: p.bgSoft, borderColor: p.border }]}>
536
539
  <StatusDot status={status} />
537
- <Text style={[styles.statusBadgeText, { color: p.textSoft }]}>
538
- {status.replace("_", " ")}
539
- </Text>
540
+ <Text style={[styles.statusBadgeText, { color: p.textSoft }]}>{status.replace("_", " ")}</Text>
540
541
  </View>
541
542
  );
542
543
  }
@@ -548,9 +549,35 @@ function fmtDate(iso: string) {
548
549
  // ─── Styles ───────────────────────────────────────────────────────────────────
549
550
 
550
551
  const styles = StyleSheet.create({
551
- // Sheet container — fills the sheet, stacks header above scroll
552
- sheetContainer: { flex: 1 },
552
+ // Modal overlay
553
+ overlay: {
554
+ flex: 1,
555
+ justifyContent: "flex-end",
556
+ },
557
+ backdrop: {
558
+ ...StyleSheet.absoluteFill,
559
+ backgroundColor: "rgba(0,0,0,0.45)",
560
+ },
553
561
 
562
+ // Sheet container — fixed height so it's always fully open regardless of content
563
+ sheet: {
564
+ height: "92%",
565
+ borderTopLeftRadius: 24,
566
+ borderTopRightRadius: 24,
567
+ overflow: "hidden",
568
+ },
569
+ handleWrap: {
570
+ alignItems: "center",
571
+ paddingTop: 10,
572
+ paddingBottom: 4,
573
+ },
574
+ handle: {
575
+ width: 40,
576
+ height: 4,
577
+ borderRadius: 2,
578
+ },
579
+
580
+ // Header
554
581
  header: {
555
582
  flexDirection: "row",
556
583
  alignItems: "center",
@@ -561,15 +588,15 @@ const styles = StyleSheet.create({
561
588
  },
562
589
  headerTitle: { fontSize: 16, fontWeight: "700", letterSpacing: -0.3 },
563
590
  navBtn: { fontSize: 14, fontWeight: "600" },
564
- navBtnPlaceholder: { width: 60 },
591
+ navBtnSpacer: { width: 60 },
565
592
  newBtn: { borderRadius: 20, paddingHorizontal: 14, paddingVertical: 7 },
566
593
  newBtnText: { color: "#fff", fontSize: 13, fontWeight: "700" },
567
594
 
568
- scrollView: { flex: 1 },
569
- scrollBody: { flexGrow: 1 },
595
+ // Scroll — flex: 1 fills all remaining space after the fixed header
596
+ scroll: { flex: 1 },
597
+ scrollContent: { flexGrow: 1, padding: 16, paddingBottom: 32 },
570
598
 
571
599
  // List
572
- listPad: { padding: 16, paddingBottom: 40 },
573
600
  centered: { alignItems: "center", justifyContent: "center", paddingVertical: 60 },
574
601
  emptyText: { fontSize: 14 },
575
602
  card: {
@@ -599,7 +626,7 @@ const styles = StyleSheet.create({
599
626
  chevron: { fontSize: 22, fontWeight: "300", marginLeft: 4 },
600
627
 
601
628
  // Detail
602
- detailPad: { padding: 20, paddingBottom: 50 },
629
+ detailPad: { paddingTop: 4 },
603
630
  detailTitle: { fontSize: 22, fontWeight: "700", letterSpacing: -0.4, marginBottom: 10, lineHeight: 28 },
604
631
  badgeRow: { flexDirection: "row", gap: 8, marginBottom: 16 },
605
632
  detailContent: { fontSize: 15, lineHeight: 23, marginBottom: 24 },
@@ -615,8 +642,7 @@ const styles = StyleSheet.create({
615
642
  commentsSection: { marginTop: 28, borderTopWidth: StyleSheet.hairlineWidth, paddingTop: 20 },
616
643
  commentsLabel: { fontSize: 10, fontWeight: "700", letterSpacing: 0.8, marginBottom: 12 },
617
644
  commentBubble: {
618
- borderRadius: 12, borderWidth: StyleSheet.hairlineWidth,
619
- padding: 14, marginBottom: 10,
645
+ borderRadius: 12, borderWidth: StyleSheet.hairlineWidth, padding: 14, marginBottom: 10,
620
646
  },
621
647
  commentMeta: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 8 },
622
648
  ownerBadge: { borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3 },
@@ -625,7 +651,7 @@ const styles = StyleSheet.create({
625
651
  commentContent: { fontSize: 14, lineHeight: 21 },
626
652
 
627
653
  // Create
628
- createPad: { padding: 20, paddingBottom: 50 },
654
+ createPad: { paddingTop: 4 },
629
655
  fieldLabel: { fontSize: 10, fontWeight: "700", letterSpacing: 0.8, marginBottom: 8 },
630
656
  typeRow: { flexDirection: "row", gap: 8, marginBottom: 20, flexWrap: "wrap" },
631
657
  typeBtn: { paddingVertical: 9, paddingHorizontal: 14, borderRadius: 10, borderWidth: 1 },
@@ -649,6 +675,14 @@ const styles = StyleSheet.create({
649
675
  successTitle: { fontSize: 20, fontWeight: "700", textAlign: "center", marginBottom: 6 },
650
676
  successSub: { fontSize: 14, textAlign: "center" },
651
677
 
678
+ // Branding
679
+ poweredBy: {
680
+ alignItems: "center",
681
+ paddingVertical: 10,
682
+ borderTopWidth: StyleSheet.hairlineWidth,
683
+ },
684
+ poweredByText: { fontSize: 11 },
685
+
652
686
  // Shared badges
653
687
  badge: { paddingHorizontal: 7, paddingVertical: 3, borderRadius: 6 },
654
688
  badgeText: { fontSize: 10, fontWeight: "700", letterSpacing: 0.2 },