@upstep/react-native 0.6.16 → 0.7.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.
@@ -15,7 +15,13 @@ import {
15
15
  Linking,
16
16
  } from "react-native";
17
17
  import { useSafeAreaInsets } from "react-native-safe-area-context";
18
- import type { Comment, Feedback, FeedbackType, FeedbackWithComments } from "@upstep/types";
18
+ import type {
19
+ Comment,
20
+ Feedback,
21
+ FeedbackType,
22
+ FeedbackWithComments,
23
+ } from "@upstep/types";
24
+ import { SupportView } from "./SupportView";
19
25
  import { useUpstep } from "./FeedbackContext";
20
26
 
21
27
  // ─── Theme ────────────────────────────────────────────────────────────────────
@@ -32,7 +38,7 @@ interface Palette {
32
38
 
33
39
  const LIGHT: Palette = {
34
40
  bg: "#ffffff",
35
- bgSoft: "#f6f5f2",
41
+ bgSoft: "#f5f5f5",
36
42
  text: "#1a1915",
37
43
  textSoft: "#56544d",
38
44
  textFaint: "#9b9890",
@@ -41,8 +47,8 @@ const LIGHT: Palette = {
41
47
  };
42
48
 
43
49
  const DARK: Palette = {
44
- bg: "#1c1b19",
45
- bgSoft: "#262522",
50
+ bg: "#000000",
51
+ bgSoft: "#171717",
46
52
  text: "#f5f4f0",
47
53
  textSoft: "#b4b1a8",
48
54
  textFaint: "#7d7a72",
@@ -53,6 +59,7 @@ const DARK: Palette = {
53
59
  // ─── Navigation ───────────────────────────────────────────────────────────────
54
60
 
55
61
  type Nav =
62
+ | { screen: "support" }
56
63
  | { screen: "list" }
57
64
  | { screen: "detail"; feedbackId: string }
58
65
  | { screen: "create" };
@@ -60,7 +67,17 @@ type Nav =
60
67
  // ─── FeedbackSheet ────────────────────────────────────────────────────────────
61
68
 
62
69
  export function FeedbackSheet() {
63
- const { isOpen, closeSheet, accentColor, theme, loadFeed, showBranding } = useUpstep();
70
+ const {
71
+ client,
72
+ isOpen,
73
+ closeSheet,
74
+ accentColor,
75
+ theme,
76
+ loadFeed,
77
+ showBranding,
78
+ supportConfig,
79
+ supportRevision,
80
+ } = useUpstep();
64
81
  const scheme = useColorScheme();
65
82
  const isDark = theme === "dark" || (theme === "auto" && scheme === "dark");
66
83
  const p = isDark ? DARK : LIGHT;
@@ -70,13 +87,14 @@ export function FeedbackSheet() {
70
87
 
71
88
  // Refresh list every time the modal opens
72
89
  useEffect(() => {
73
- if (isOpen) loadFeed();
74
- }, [isOpen, loadFeed]);
90
+ if (isOpen) {
91
+ loadFeed();
92
+ setNav({ screen: supportConfig?.enabled ? "support" : "list" });
93
+ }
94
+ }, [isOpen, loadFeed, supportConfig?.enabled]);
75
95
 
76
96
  function handleClose() {
77
97
  closeSheet();
78
- // Reset to list when closed so next open starts fresh
79
- setTimeout(() => setNav({ screen: "list" }), 300);
80
98
  }
81
99
 
82
100
  return (
@@ -103,9 +121,51 @@ export function FeedbackSheet() {
103
121
 
104
122
  {/* Fixed header */}
105
123
  <View style={[styles.header, { borderBottomColor: p.border }]}>
106
- {nav.screen === "list" ? (
124
+ {supportConfig ? (
125
+ <>
126
+ <View style={{ flexDirection: "row", gap: 8, flex: 1 }}>
127
+ {["support", "list"].map((screen) => (
128
+ <TouchableOpacity
129
+ key={screen}
130
+ onPress={() => setNav({ screen } as Nav)}
131
+ style={{
132
+ paddingVertical: 10,
133
+ paddingHorizontal: 14,
134
+ borderRadius: 10,
135
+ backgroundColor: (
136
+ screen === "support"
137
+ ? nav.screen === "support"
138
+ : nav.screen !== "support"
139
+ )
140
+ ? p.bgSoft
141
+ : "transparent",
142
+ }}
143
+ >
144
+ <Text
145
+ style={{
146
+ fontSize: 16,
147
+ fontWeight: "600",
148
+ color: p.text,
149
+ }}
150
+ >
151
+ {screen === "support" ? "Support" : "Feedback"}
152
+ </Text>
153
+ </TouchableOpacity>
154
+ ))}
155
+ </View>
156
+ <TouchableOpacity
157
+ accessibilityLabel="Close"
158
+ onPress={handleClose}
159
+ style={{ padding: 10 }}
160
+ >
161
+ <Text style={{ fontSize: 20, color: p.textSoft }}>×</Text>
162
+ </TouchableOpacity>
163
+ </>
164
+ ) : nav.screen === "list" ? (
107
165
  <>
108
- <Text style={[styles.headerTitle, { color: p.text }]}>Feedback</Text>
166
+ <Text style={[styles.headerTitle, { color: p.text }]}>
167
+ Feedback
168
+ </Text>
109
169
  <TouchableOpacity
110
170
  onPress={() => setNav({ screen: "create" })}
111
171
  activeOpacity={0.8}
@@ -120,9 +180,13 @@ export function FeedbackSheet() {
120
180
  onPress={() => setNav({ screen: "list" })}
121
181
  hitSlop={{ top: 10, bottom: 10, left: 10, right: 16 }}
122
182
  >
123
- <Text style={[styles.navBtn, { color: accentColor }]}>Cancel</Text>
183
+ <Text style={[styles.navBtn, { color: accentColor }]}>
184
+ Cancel
185
+ </Text>
124
186
  </TouchableOpacity>
125
- <Text style={[styles.headerTitle, { color: p.text }]}>New feedback</Text>
187
+ <Text style={[styles.headerTitle, { color: p.text }]}>
188
+ New feedback
189
+ </Text>
126
190
  <View style={styles.navBtnSpacer} />
127
191
  </>
128
192
  ) : (
@@ -130,51 +194,90 @@ export function FeedbackSheet() {
130
194
  onPress={() => setNav({ screen: "list" })}
131
195
  hitSlop={{ top: 10, bottom: 10, left: 10, right: 16 }}
132
196
  >
133
- <Text style={[styles.navBtn, { color: accentColor }]}>← Back</Text>
197
+ <Text style={[styles.navBtn, { color: accentColor }]}>
198
+ ← Back
199
+ </Text>
134
200
  </TouchableOpacity>
135
201
  )}
136
202
  </View>
137
203
 
138
204
  {/* Scrollable body */}
139
- <ScrollView
140
- style={styles.scroll}
141
- contentContainerStyle={styles.scrollContent}
142
- keyboardShouldPersistTaps="handled"
143
- showsVerticalScrollIndicator={false}
144
- >
145
- {nav.screen === "list" && (
146
- <FeedListBody
147
- p={p}
148
- accent={accentColor}
149
- onSelect={(id) => setNav({ screen: "detail", feedbackId: id })}
150
- />
151
- )}
152
- {nav.screen === "detail" && "feedbackId" in nav && (
153
- <FeedDetailBody
154
- p={p}
155
- accent={accentColor}
156
- feedbackId={(nav as { screen: "detail"; feedbackId: string }).feedbackId}
157
- />
158
- )}
159
- {nav.screen === "create" && (
160
- <FeedCreateBody
161
- p={p}
162
- accent={accentColor}
163
- onDone={() => {
164
- setNav({ screen: "list" });
165
- }}
166
- />
167
- )}
168
- </ScrollView>
205
+ {nav.screen === "support" && supportConfig ? (
206
+ <View style={{ flex: 1, paddingBottom: insets.bottom }}>
207
+ {isOpen && (
208
+ <SupportView
209
+ key={supportRevision}
210
+ client={client.support}
211
+ config={supportConfig}
212
+ accentColor={accentColor}
213
+ dark={isDark}
214
+ />
215
+ )}
216
+ </View>
217
+ ) : (
218
+ <ScrollView
219
+ style={styles.scroll}
220
+ contentContainerStyle={styles.scrollContent}
221
+ keyboardShouldPersistTaps="handled"
222
+ showsVerticalScrollIndicator={false}
223
+ >
224
+ {supportConfig && nav.screen === "list" && (
225
+ <TouchableOpacity
226
+ onPress={() => setNav({ screen: "create" })}
227
+ style={[
228
+ styles.newBtn,
229
+ {
230
+ backgroundColor: accentColor,
231
+ alignSelf: "flex-end",
232
+ marginBottom: 12,
233
+ },
234
+ ]}
235
+ >
236
+ <Text style={styles.newBtnText}>+ New feedback</Text>
237
+ </TouchableOpacity>
238
+ )}
239
+ {nav.screen === "list" && (
240
+ <FeedListBody
241
+ p={p}
242
+ accent={accentColor}
243
+ onSelect={(id) =>
244
+ setNav({ screen: "detail", feedbackId: id })
245
+ }
246
+ />
247
+ )}
248
+ {nav.screen === "detail" && "feedbackId" in nav && (
249
+ <FeedDetailBody
250
+ p={p}
251
+ accent={accentColor}
252
+ feedbackId={
253
+ (nav as { screen: "detail"; feedbackId: string }).feedbackId
254
+ }
255
+ />
256
+ )}
257
+ {nav.screen === "create" && (
258
+ <FeedCreateBody
259
+ p={p}
260
+ accent={accentColor}
261
+ onDone={() => {
262
+ setNav({ screen: "list" });
263
+ }}
264
+ />
265
+ )}
266
+ </ScrollView>
267
+ )}
169
268
 
170
269
  {/* Branding — hidden for Business plan */}
171
- {showBranding && (
270
+ {showBranding && nav.screen !== "support" && (
172
271
  <TouchableOpacity
173
272
  onPress={() => Linking.openURL("https://upstep.dev")}
174
273
  activeOpacity={0.6}
175
274
  style={[
176
275
  styles.poweredBy,
177
- { borderTopColor: p.border, paddingBottom: insets.bottom + 10, backgroundColor: p.bg },
276
+ {
277
+ borderTopColor: p.border,
278
+ paddingBottom: insets.bottom + 10,
279
+ backgroundColor: p.bg,
280
+ },
178
281
  ]}
179
282
  >
180
283
  <Text style={[styles.poweredByText, { color: p.textFaint }]}>
@@ -191,9 +294,13 @@ export function FeedbackSheet() {
191
294
  // ─── FeedListBody ─────────────────────────────────────────────────────────────
192
295
 
193
296
  function FeedListBody({
194
- p, accent, onSelect,
297
+ p,
298
+ accent,
299
+ onSelect,
195
300
  }: {
196
- p: Palette; accent: string; onSelect: (id: string) => void;
301
+ p: Palette;
302
+ accent: string;
303
+ onSelect: (id: string) => void;
197
304
  }) {
198
305
  const { feedItems, feedLoading, vote } = useUpstep();
199
306
 
@@ -234,10 +341,17 @@ function FeedListBody({
234
341
  }
235
342
 
236
343
  function FeedCard({
237
- item, p, accent, onPress, onVote,
344
+ item,
345
+ p,
346
+ accent,
347
+ onPress,
348
+ onVote,
238
349
  }: {
239
- item: Feedback; p: Palette; accent: string;
240
- onPress: () => void; onVote: () => void;
350
+ item: Feedback;
351
+ p: Palette;
352
+ accent: string;
353
+ onPress: () => void;
354
+ onVote: () => void;
241
355
  }) {
242
356
  const voted = item.userVote === "UP";
243
357
  const displayTitle = item.title ?? item.content;
@@ -260,8 +374,12 @@ function FeedCard({
260
374
  },
261
375
  ]}
262
376
  >
263
- <Text style={[styles.voteArrow, { color: voted ? "#fff" : accent }]}>▲</Text>
264
- <Text style={[styles.voteCount, { color: voted ? "#fff" : p.text }]}>{item.upvotes}</Text>
377
+ <Text style={[styles.voteArrow, { color: voted ? "#fff" : accent }]}>
378
+ ▲
379
+ </Text>
380
+ <Text style={[styles.voteCount, { color: voted ? "#fff" : p.text }]}>
381
+ {item.upvotes}
382
+ </Text>
265
383
  </TouchableOpacity>
266
384
 
267
385
  <View style={styles.cardBody}>
@@ -269,16 +387,23 @@ function FeedCard({
269
387
  {displayTitle}
270
388
  </Text>
271
389
  {item.title && (
272
- <Text style={[styles.cardDesc, { color: p.textFaint }]} numberOfLines={1}>
390
+ <Text
391
+ style={[styles.cardDesc, { color: p.textFaint }]}
392
+ numberOfLines={1}
393
+ >
273
394
  {item.content}
274
395
  </Text>
275
396
  )}
276
397
  <View style={styles.cardMeta}>
277
398
  <TypeBadge type={item.type} p={p} />
278
- {item.status === "PENDING"
279
- ? <PendingBadge />
280
- : <StatusDot status={item.status} color={item.boardStatus?.color} />}
281
- <Text style={[styles.cardDate, { color: p.textFaint }]}>{fmtDate(item.createdAt)}</Text>
399
+ {item.status === "PENDING" ? (
400
+ <PendingBadge />
401
+ ) : (
402
+ <StatusDot status={item.status} color={item.boardStatus?.color} />
403
+ )}
404
+ <Text style={[styles.cardDate, { color: p.textFaint }]}>
405
+ {fmtDate(item.createdAt)}
406
+ </Text>
282
407
  </View>
283
408
  <LabelBadges labels={item.labels} p={p} />
284
409
  </View>
@@ -291,9 +416,13 @@ function FeedCard({
291
416
  // ─── FeedDetailBody ───────────────────────────────────────────────────────────
292
417
 
293
418
  function FeedDetailBody({
294
- p, accent, feedbackId,
419
+ p,
420
+ accent,
421
+ feedbackId,
295
422
  }: {
296
- p: Palette; accent: string; feedbackId: string;
423
+ p: Palette;
424
+ accent: string;
425
+ feedbackId: string;
297
426
  }) {
298
427
  const { vote, getItem } = useUpstep();
299
428
  const [item, setItem] = useState<FeedbackWithComments | null>(null);
@@ -304,9 +433,18 @@ function FeedDetailBody({
304
433
  let cancelled = false;
305
434
  setLoading(true);
306
435
  getItem(feedbackId)
307
- .then((data) => { if (!cancelled) { setItem(data); setLoading(false); } })
308
- .catch(() => { if (!cancelled) setLoading(false); });
309
- return () => { cancelled = true; };
436
+ .then((data) => {
437
+ if (!cancelled) {
438
+ setItem(data);
439
+ setLoading(false);
440
+ }
441
+ })
442
+ .catch(() => {
443
+ if (!cancelled) setLoading(false);
444
+ });
445
+ return () => {
446
+ cancelled = true;
447
+ };
310
448
  }, [feedbackId, getItem]);
311
449
 
312
450
  async function handleVote() {
@@ -332,17 +470,27 @@ function FeedDetailBody({
332
470
  return (
333
471
  <View style={styles.detailPad}>
334
472
  {item.title && (
335
- <Text style={[styles.detailTitle, { color: p.text }]}>{item.title}</Text>
473
+ <Text style={[styles.detailTitle, { color: p.text }]}>
474
+ {item.title}
475
+ </Text>
336
476
  )}
337
477
  <View style={styles.badgeRow}>
338
478
  <TypeBadge type={item.type} p={p} />
339
- {item.status === "PENDING"
340
- ? <PendingBadge />
341
- : <StatusBadge status={item.status} boardStatus={item.boardStatus} p={p} />}
479
+ {item.status === "PENDING" ? (
480
+ <PendingBadge />
481
+ ) : (
482
+ <StatusBadge
483
+ status={item.status}
484
+ boardStatus={item.boardStatus}
485
+ p={p}
486
+ />
487
+ )}
342
488
  <LabelBadges labels={item.labels} p={p} />
343
489
  </View>
344
490
 
345
- <Text style={[styles.detailContent, { color: p.textSoft }]}>{item.content}</Text>
491
+ <Text style={[styles.detailContent, { color: p.textSoft }]}>
492
+ {item.content}
493
+ </Text>
346
494
 
347
495
  <TouchableOpacity
348
496
  onPress={handleVote}
@@ -361,7 +509,9 @@ function FeedDetailBody({
361
509
  ) : (
362
510
  <>
363
511
  <Text style={[styles.upvoteArrow, { color: accent }]}>▲</Text>
364
- <Text style={[styles.upvoteCount, { color: p.text }]}>{item.upvotes}</Text>
512
+ <Text style={[styles.upvoteCount, { color: p.text }]}>
513
+ {item.upvotes}
514
+ </Text>
365
515
  <Text style={[styles.upvoteLabel, { color: p.textFaint }]}>
366
516
  {item.userVote === "UP" ? "Upvoted" : "Upvote"}
367
517
  </Text>
@@ -371,7 +521,9 @@ function FeedDetailBody({
371
521
 
372
522
  {item.comments.length > 0 && (
373
523
  <View style={[styles.commentsSection, { borderTopColor: p.border }]}>
374
- <Text style={[styles.commentsLabel, { color: p.textFaint }]}>DEVELOPER RESPONSE</Text>
524
+ <Text style={[styles.commentsLabel, { color: p.textFaint }]}>
525
+ COMMENTS
526
+ </Text>
375
527
  {item.comments.map((c) => (
376
528
  <CommentBubble key={c.id} comment={c} p={p} accent={accent} />
377
529
  ))}
@@ -381,18 +533,35 @@ function FeedDetailBody({
381
533
  );
382
534
  }
383
535
 
384
- function CommentBubble({ comment, p, accent }: { comment: Comment; p: Palette; accent: string }) {
536
+ function CommentBubble({
537
+ comment,
538
+ p,
539
+ accent,
540
+ }: {
541
+ comment: Comment;
542
+ p: Palette;
543
+ accent: string;
544
+ }) {
385
545
  return (
386
- <View style={[styles.commentBubble, { backgroundColor: p.bgSoft, borderColor: p.border }]}>
546
+ <View
547
+ style={[
548
+ styles.commentBubble,
549
+ { backgroundColor: p.bgSoft, borderColor: p.border },
550
+ ]}
551
+ >
387
552
  <View style={styles.commentMeta}>
388
553
  <View style={[styles.ownerBadge, { backgroundColor: accent + "20" }]}>
389
554
  <Text style={[styles.ownerBadgeText, { color: accent }]}>
390
- Developer
555
+ {comment.authorName?.trim() || "UpStep Team"}
391
556
  </Text>
392
557
  </View>
393
- <Text style={[styles.commentDate, { color: p.textFaint }]}>{fmtDate(comment.createdAt)}</Text>
558
+ <Text style={[styles.commentDate, { color: p.textFaint }]}>
559
+ {fmtDate(comment.createdAt)}
560
+ </Text>
394
561
  </View>
395
- <Text style={[styles.commentContent, { color: p.textSoft }]}>{comment.content}</Text>
562
+ <Text style={[styles.commentContent, { color: p.textSoft }]}>
563
+ {comment.content}
564
+ </Text>
396
565
  </View>
397
566
  );
398
567
  }
@@ -407,9 +576,13 @@ const TYPE_LABELS: Record<FeedbackType, string> = {
407
576
  };
408
577
 
409
578
  function FeedCreateBody({
410
- p, accent, onDone,
579
+ p,
580
+ accent,
581
+ onDone,
411
582
  }: {
412
- p: Palette; accent: string; onDone: () => void;
583
+ p: Palette;
584
+ accent: string;
585
+ onDone: () => void;
413
586
  }) {
414
587
  const { submit } = useUpstep();
415
588
  const [title, setTitle] = useState("");
@@ -464,7 +637,12 @@ function FeedCreateBody({
464
637
  },
465
638
  ]}
466
639
  >
467
- <Text style={[styles.typeBtnText, { color: active ? "#fff" : p.textSoft }]}>
640
+ <Text
641
+ style={[
642
+ styles.typeBtnText,
643
+ { color: active ? "#fff" : p.textSoft },
644
+ ]}
645
+ >
468
646
  {TYPE_LABELS[t]}
469
647
  </Text>
470
648
  </TouchableOpacity>
@@ -480,10 +658,15 @@ function FeedCreateBody({
480
658
  placeholderTextColor={p.textFaint}
481
659
  maxLength={200}
482
660
  returnKeyType="next"
483
- style={[styles.titleInput, { borderColor: p.border, backgroundColor: p.bgSoft, color: p.text }]}
661
+ style={[
662
+ styles.titleInput,
663
+ { borderColor: p.border, backgroundColor: p.bgSoft, color: p.text },
664
+ ]}
484
665
  />
485
666
 
486
- <Text style={[styles.fieldLabel, { color: p.textFaint }]}>DESCRIPTION</Text>
667
+ <Text style={[styles.fieldLabel, { color: p.textFaint }]}>
668
+ DESCRIPTION
669
+ </Text>
487
670
  <TextInput
488
671
  value={description}
489
672
  onChangeText={setDescription}
@@ -493,7 +676,10 @@ function FeedCreateBody({
493
676
  numberOfLines={5}
494
677
  maxLength={2000}
495
678
  textAlignVertical="top"
496
- style={[styles.descInput, { borderColor: p.border, backgroundColor: p.bgSoft, color: p.text }]}
679
+ style={[
680
+ styles.descInput,
681
+ { borderColor: p.border, backgroundColor: p.bgSoft, color: p.text },
682
+ ]}
497
683
  />
498
684
 
499
685
  {error ? <Text style={styles.errorText}>{error}</Text> : null}
@@ -521,10 +707,14 @@ function FeedCreateBody({
521
707
  // ─── Shared sub-components ────────────────────────────────────────────────────
522
708
 
523
709
  const TYPE_BG: Record<FeedbackType, string> = {
524
- BUG: "#fdecec", FEATURE: "#e9f0fd", GENERAL: "",
710
+ BUG: "#fdecec",
711
+ FEATURE: "#e9f0fd",
712
+ GENERAL: "",
525
713
  };
526
714
  const TYPE_FG: Record<FeedbackType, string> = {
527
- BUG: "#d6453d", FEATURE: "#3b76d6", GENERAL: "",
715
+ BUG: "#d6453d",
716
+ FEATURE: "#3b76d6",
717
+ GENERAL: "",
528
718
  };
529
719
 
530
720
  function PendingBadge() {
@@ -538,43 +728,91 @@ function PendingBadge() {
538
728
 
539
729
  function TypeBadge({ type, p }: { type: FeedbackType; p: Palette }) {
540
730
  return (
541
- <View style={[styles.badge, { backgroundColor: TYPE_BG[type] || p.bgSoft }]}>
542
- <Text style={[styles.badgeText, { color: TYPE_FG[type] || p.textSoft }]}>{type}</Text>
731
+ <View
732
+ style={[styles.badge, { backgroundColor: TYPE_BG[type] || p.bgSoft }]}
733
+ >
734
+ <Text style={[styles.badgeText, { color: TYPE_FG[type] || p.textSoft }]}>
735
+ {type}
736
+ </Text>
543
737
  </View>
544
738
  );
545
739
  }
546
740
 
547
741
  const STATUS_COLOR: Record<string, string> = {
548
- OPEN: "#b45309", IN_PROGRESS: "#1d4ed8", DONE: "#15803d",
549
- PENDING: "#ea580c", CLOSED: "#6b7280",
742
+ OPEN: "#b45309",
743
+ IN_PROGRESS: "#1d4ed8",
744
+ DONE: "#15803d",
745
+ PENDING: "#ea580c",
746
+ CLOSED: "#6b7280",
550
747
  };
551
748
 
552
- function StatusDot({ status, color }: { status: string; color?: string | undefined }) {
553
- return <View style={[styles.statusDot, { backgroundColor: color ?? STATUS_COLOR[status] ?? "#9ca3af" }]} />;
749
+ function StatusDot({
750
+ status,
751
+ color,
752
+ }: {
753
+ status: string;
754
+ color?: string | undefined;
755
+ }) {
756
+ return (
757
+ <View
758
+ style={[
759
+ styles.statusDot,
760
+ { backgroundColor: color ?? STATUS_COLOR[status] ?? "#9ca3af" },
761
+ ]}
762
+ />
763
+ );
554
764
  }
555
765
 
556
766
  function StatusBadge({
557
- status, boardStatus, p,
767
+ status,
768
+ boardStatus,
769
+ p,
558
770
  }: {
559
- status: string; boardStatus: Feedback["boardStatus"]; p: Palette;
771
+ status: string;
772
+ boardStatus: Feedback["boardStatus"];
773
+ p: Palette;
560
774
  }) {
561
775
  return (
562
- <View style={[styles.statusBadge, { backgroundColor: p.bgSoft, borderColor: p.border }]}>
776
+ <View
777
+ style={[
778
+ styles.statusBadge,
779
+ { backgroundColor: p.bgSoft, borderColor: p.border },
780
+ ]}
781
+ >
563
782
  <StatusDot status={status} color={boardStatus?.color} />
564
- <Text style={[styles.statusBadgeText, { color: p.textSoft }]}>{boardStatus?.name ?? status.replace("_", " ")}</Text>
783
+ <Text style={[styles.statusBadgeText, { color: p.textSoft }]}>
784
+ {boardStatus?.name ?? status.replace("_", " ")}
785
+ </Text>
565
786
  </View>
566
787
  );
567
788
  }
568
789
 
569
- function LabelBadges({ labels, p }: { labels: Feedback["labels"]; p: Palette }) {
790
+ function LabelBadges({
791
+ labels,
792
+ p,
793
+ }: {
794
+ labels: Feedback["labels"];
795
+ p: Palette;
796
+ }) {
570
797
  if (!labels?.length) return null;
571
798
 
572
799
  return (
573
800
  <View style={styles.labelBadges}>
574
801
  {labels.map((label) => (
575
- <View key={label.id} style={[styles.labelBadge, { backgroundColor: p.bgSoft, borderColor: label.color }]}>
802
+ <View
803
+ key={label.id}
804
+ style={[
805
+ styles.labelBadge,
806
+ { backgroundColor: p.bgSoft, borderColor: label.color },
807
+ ]}
808
+ >
576
809
  <View style={[styles.labelDot, { backgroundColor: label.color }]} />
577
- <Text style={[styles.labelBadgeText, { color: p.textSoft }]} numberOfLines={1}>{label.name}</Text>
810
+ <Text
811
+ style={[styles.labelBadgeText, { color: p.textSoft }]}
812
+ numberOfLines={1}
813
+ >
814
+ {label.name}
815
+ </Text>
578
816
  </View>
579
817
  ))}
580
818
  </View>
@@ -582,7 +820,10 @@ function LabelBadges({ labels, p }: { labels: Feedback["labels"]; p: Palette })
582
820
  }
583
821
 
584
822
  function fmtDate(iso: string) {
585
- return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric" });
823
+ return new Date(iso).toLocaleDateString(undefined, {
824
+ month: "short",
825
+ day: "numeric",
826
+ });
586
827
  }
587
828
 
588
829
  // ─── Styles ───────────────────────────────────────────────────────────────────
@@ -640,7 +881,11 @@ const styles = StyleSheet.create({
640
881
  scrollContent: { flexGrow: 1, padding: 16, paddingBottom: 32 },
641
882
 
642
883
  // List
643
- centered: { alignItems: "center", justifyContent: "center", paddingVertical: 60 },
884
+ centered: {
885
+ alignItems: "center",
886
+ justifyContent: "center",
887
+ paddingVertical: 60,
888
+ },
644
889
  emptyText: { fontSize: 14 },
645
890
  card: {
646
891
  flexDirection: "row",
@@ -662,36 +907,85 @@ const styles = StyleSheet.create({
662
907
  voteArrow: { fontSize: 11, lineHeight: 13 },
663
908
  voteCount: { fontSize: 15, fontWeight: "700", lineHeight: 18 },
664
909
  cardBody: { flex: 1 },
665
- cardTitle: { fontSize: 14, fontWeight: "600", lineHeight: 19, marginBottom: 2 },
910
+ cardTitle: {
911
+ fontSize: 14,
912
+ fontWeight: "600",
913
+ lineHeight: 19,
914
+ marginBottom: 2,
915
+ },
666
916
  cardDesc: { fontSize: 12.5, lineHeight: 17, marginBottom: 6 },
667
917
  cardMeta: { flexDirection: "row", alignItems: "center", gap: 6 },
668
918
  cardDate: { fontSize: 11 },
669
919
  labelBadges: { flexDirection: "row", flexWrap: "wrap", gap: 6, marginTop: 7 },
670
- labelBadge: { flexDirection: "row", alignItems: "center", gap: 5, borderWidth: 1, borderRadius: 10, paddingHorizontal: 7, paddingVertical: 3, maxWidth: "100%" },
920
+ labelBadge: {
921
+ flexDirection: "row",
922
+ alignItems: "center",
923
+ gap: 5,
924
+ borderWidth: 1,
925
+ borderRadius: 10,
926
+ paddingHorizontal: 7,
927
+ paddingVertical: 3,
928
+ maxWidth: "100%",
929
+ },
671
930
  labelDot: { width: 6, height: 6, borderRadius: 3 },
672
931
  labelBadgeText: { fontSize: 11, fontWeight: "500", flexShrink: 1 },
673
932
  chevron: { fontSize: 22, fontWeight: "300", marginLeft: 4 },
674
933
 
675
934
  // Detail
676
935
  detailPad: { paddingTop: 4 },
677
- detailTitle: { fontSize: 22, fontWeight: "700", letterSpacing: -0.4, marginBottom: 10, lineHeight: 28 },
678
- badgeRow: { flexDirection: "row", flexWrap: "wrap", gap: 8, marginBottom: 16 },
936
+ detailTitle: {
937
+ fontSize: 22,
938
+ fontWeight: "700",
939
+ letterSpacing: -0.4,
940
+ marginBottom: 10,
941
+ lineHeight: 28,
942
+ },
943
+ badgeRow: {
944
+ flexDirection: "row",
945
+ flexWrap: "wrap",
946
+ gap: 8,
947
+ marginBottom: 16,
948
+ },
679
949
  detailContent: { fontSize: 15, lineHeight: 23, marginBottom: 24 },
680
950
  upvoteBtn: {
681
- flexDirection: "row", alignItems: "center", gap: 8,
682
- alignSelf: "flex-start", borderWidth: 1, borderRadius: 12,
683
- paddingHorizontal: 16, paddingVertical: 10, marginBottom: 8,
684
- minWidth: 80, justifyContent: "center",
951
+ flexDirection: "row",
952
+ alignItems: "center",
953
+ gap: 8,
954
+ alignSelf: "flex-start",
955
+ borderWidth: 1,
956
+ borderRadius: 12,
957
+ paddingHorizontal: 16,
958
+ paddingVertical: 10,
959
+ marginBottom: 8,
960
+ minWidth: 80,
961
+ justifyContent: "center",
685
962
  },
686
963
  upvoteArrow: { fontSize: 13, lineHeight: 15 },
687
964
  upvoteCount: { fontSize: 18, fontWeight: "700" },
688
965
  upvoteLabel: { fontSize: 13, fontWeight: "500" },
689
- commentsSection: { marginTop: 28, borderTopWidth: StyleSheet.hairlineWidth, paddingTop: 20 },
690
- commentsLabel: { fontSize: 10, fontWeight: "700", letterSpacing: 0.8, marginBottom: 12 },
966
+ commentsSection: {
967
+ marginTop: 28,
968
+ borderTopWidth: StyleSheet.hairlineWidth,
969
+ paddingTop: 20,
970
+ },
971
+ commentsLabel: {
972
+ fontSize: 10,
973
+ fontWeight: "700",
974
+ letterSpacing: 0.8,
975
+ marginBottom: 12,
976
+ },
691
977
  commentBubble: {
692
- borderRadius: 12, borderWidth: StyleSheet.hairlineWidth, padding: 14, marginBottom: 10,
978
+ borderRadius: 12,
979
+ borderWidth: StyleSheet.hairlineWidth,
980
+ padding: 14,
981
+ marginBottom: 10,
982
+ },
983
+ commentMeta: {
984
+ flexDirection: "row",
985
+ alignItems: "center",
986
+ gap: 8,
987
+ marginBottom: 8,
693
988
  },
694
- commentMeta: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 8 },
695
989
  ownerBadge: { borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3 },
696
990
  ownerBadgeText: { fontSize: 11, fontWeight: "700" },
697
991
  commentDate: { fontSize: 11 },
@@ -699,27 +993,54 @@ const styles = StyleSheet.create({
699
993
 
700
994
  // Create
701
995
  createPad: { paddingTop: 4 },
702
- fieldLabel: { fontSize: 10, fontWeight: "700", letterSpacing: 0.8, marginBottom: 8 },
996
+ fieldLabel: {
997
+ fontSize: 10,
998
+ fontWeight: "700",
999
+ letterSpacing: 0.8,
1000
+ marginBottom: 8,
1001
+ },
703
1002
  typeRow: { flexDirection: "row", gap: 8, marginBottom: 20, flexWrap: "wrap" },
704
- typeBtn: { paddingVertical: 9, paddingHorizontal: 14, borderRadius: 10, borderWidth: 1 },
1003
+ typeBtn: {
1004
+ paddingVertical: 9,
1005
+ paddingHorizontal: 14,
1006
+ borderRadius: 10,
1007
+ borderWidth: 1,
1008
+ },
705
1009
  typeBtnText: { fontSize: 13, fontWeight: "600" },
706
1010
  titleInput: {
707
- borderWidth: 1, borderRadius: 12,
708
- paddingHorizontal: 14, paddingVertical: 12,
709
- fontSize: 15, marginBottom: 20,
1011
+ borderWidth: 1,
1012
+ borderRadius: 12,
1013
+ paddingHorizontal: 14,
1014
+ paddingVertical: 12,
1015
+ fontSize: 15,
1016
+ marginBottom: 20,
710
1017
  ...(Platform.OS === "ios" ? {} : { paddingTop: 12 }),
711
1018
  },
712
1019
  descInput: {
713
- borderWidth: 1, borderRadius: 12,
714
- paddingHorizontal: 14, paddingVertical: 12,
715
- fontSize: 14, minHeight: 110, marginBottom: 20,
1020
+ borderWidth: 1,
1021
+ borderRadius: 12,
1022
+ paddingHorizontal: 14,
1023
+ paddingVertical: 12,
1024
+ fontSize: 14,
1025
+ minHeight: 110,
1026
+ marginBottom: 20,
716
1027
  ...(Platform.OS === "ios" ? {} : { paddingTop: 12 }),
717
1028
  },
718
- errorText: { fontSize: 13, marginBottom: 12, fontWeight: "500", color: "#d6453d" },
1029
+ errorText: {
1030
+ fontSize: 13,
1031
+ marginBottom: 12,
1032
+ fontWeight: "500",
1033
+ color: "#d6453d",
1034
+ },
719
1035
  submitBtn: { borderRadius: 12, padding: 14, alignItems: "center" },
720
1036
  submitBtnDisabled: { opacity: 0.5 },
721
1037
  submitBtnText: { color: "#fff", fontSize: 15, fontWeight: "700" },
722
- successTitle: { fontSize: 20, fontWeight: "700", textAlign: "center", marginBottom: 6 },
1038
+ successTitle: {
1039
+ fontSize: 20,
1040
+ fontWeight: "700",
1041
+ textAlign: "center",
1042
+ marginBottom: 6,
1043
+ },
723
1044
  successSub: { fontSize: 14, textAlign: "center" },
724
1045
 
725
1046
  // Pending badge
@@ -759,9 +1080,17 @@ const styles = StyleSheet.create({
759
1080
  badgeText: { fontSize: 10, fontWeight: "700", letterSpacing: 0.2 },
760
1081
  statusDot: { width: 6, height: 6, borderRadius: 3 },
761
1082
  statusBadge: {
762
- flexDirection: "row", alignItems: "center", gap: 5,
763
- paddingHorizontal: 8, paddingVertical: 3,
764
- borderRadius: 6, borderWidth: StyleSheet.hairlineWidth,
1083
+ flexDirection: "row",
1084
+ alignItems: "center",
1085
+ gap: 5,
1086
+ paddingHorizontal: 8,
1087
+ paddingVertical: 3,
1088
+ borderRadius: 6,
1089
+ borderWidth: StyleSheet.hairlineWidth,
1090
+ },
1091
+ statusBadgeText: {
1092
+ fontSize: 10,
1093
+ fontWeight: "600",
1094
+ textTransform: "capitalize",
765
1095
  },
766
- statusBadgeText: { fontSize: 10, fontWeight: "600", textTransform: "capitalize" },
767
1096
  });