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