@upstep/react-native 0.6.15 → 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 }) {
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 ───────────────────────────────────────────────────────────────────
@@ -594,7 +834,11 @@ const styles = StyleSheet.create({
594
834
  justifyContent: "flex-end",
595
835
  },
596
836
  backdrop: {
597
- ...StyleSheet.absoluteFill,
837
+ position: "absolute",
838
+ top: 0,
839
+ right: 0,
840
+ bottom: 0,
841
+ left: 0,
598
842
  backgroundColor: "rgba(0,0,0,0.45)",
599
843
  },
600
844
 
@@ -636,7 +880,11 @@ const styles = StyleSheet.create({
636
880
  scrollContent: { flexGrow: 1, padding: 16, paddingBottom: 32 },
637
881
 
638
882
  // List
639
- centered: { alignItems: "center", justifyContent: "center", paddingVertical: 60 },
883
+ centered: {
884
+ alignItems: "center",
885
+ justifyContent: "center",
886
+ paddingVertical: 60,
887
+ },
640
888
  emptyText: { fontSize: 14 },
641
889
  card: {
642
890
  flexDirection: "row",
@@ -658,36 +906,85 @@ const styles = StyleSheet.create({
658
906
  voteArrow: { fontSize: 11, lineHeight: 13 },
659
907
  voteCount: { fontSize: 15, fontWeight: "700", lineHeight: 18 },
660
908
  cardBody: { flex: 1 },
661
- cardTitle: { fontSize: 14, fontWeight: "600", lineHeight: 19, marginBottom: 2 },
909
+ cardTitle: {
910
+ fontSize: 14,
911
+ fontWeight: "600",
912
+ lineHeight: 19,
913
+ marginBottom: 2,
914
+ },
662
915
  cardDesc: { fontSize: 12.5, lineHeight: 17, marginBottom: 6 },
663
916
  cardMeta: { flexDirection: "row", alignItems: "center", gap: 6 },
664
917
  cardDate: { fontSize: 11 },
665
918
  labelBadges: { flexDirection: "row", flexWrap: "wrap", gap: 6, marginTop: 7 },
666
- 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
+ },
667
929
  labelDot: { width: 6, height: 6, borderRadius: 3 },
668
930
  labelBadgeText: { fontSize: 11, fontWeight: "500", flexShrink: 1 },
669
931
  chevron: { fontSize: 22, fontWeight: "300", marginLeft: 4 },
670
932
 
671
933
  // Detail
672
934
  detailPad: { paddingTop: 4 },
673
- detailTitle: { fontSize: 22, fontWeight: "700", letterSpacing: -0.4, marginBottom: 10, lineHeight: 28 },
674
- 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
+ },
675
948
  detailContent: { fontSize: 15, lineHeight: 23, marginBottom: 24 },
676
949
  upvoteBtn: {
677
- flexDirection: "row", alignItems: "center", gap: 8,
678
- alignSelf: "flex-start", borderWidth: 1, borderRadius: 12,
679
- paddingHorizontal: 16, paddingVertical: 10, marginBottom: 8,
680
- 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",
681
961
  },
682
962
  upvoteArrow: { fontSize: 13, lineHeight: 15 },
683
963
  upvoteCount: { fontSize: 18, fontWeight: "700" },
684
964
  upvoteLabel: { fontSize: 13, fontWeight: "500" },
685
- commentsSection: { marginTop: 28, borderTopWidth: StyleSheet.hairlineWidth, paddingTop: 20 },
686
- 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
+ },
687
976
  commentBubble: {
688
- 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,
689
987
  },
690
- commentMeta: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 8 },
691
988
  ownerBadge: { borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3 },
692
989
  ownerBadgeText: { fontSize: 11, fontWeight: "700" },
693
990
  commentDate: { fontSize: 11 },
@@ -695,27 +992,54 @@ const styles = StyleSheet.create({
695
992
 
696
993
  // Create
697
994
  createPad: { paddingTop: 4 },
698
- 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
+ },
699
1001
  typeRow: { flexDirection: "row", gap: 8, marginBottom: 20, flexWrap: "wrap" },
700
- typeBtn: { paddingVertical: 9, paddingHorizontal: 14, borderRadius: 10, borderWidth: 1 },
1002
+ typeBtn: {
1003
+ paddingVertical: 9,
1004
+ paddingHorizontal: 14,
1005
+ borderRadius: 10,
1006
+ borderWidth: 1,
1007
+ },
701
1008
  typeBtnText: { fontSize: 13, fontWeight: "600" },
702
1009
  titleInput: {
703
- borderWidth: 1, borderRadius: 12,
704
- paddingHorizontal: 14, paddingVertical: 12,
705
- fontSize: 15, marginBottom: 20,
1010
+ borderWidth: 1,
1011
+ borderRadius: 12,
1012
+ paddingHorizontal: 14,
1013
+ paddingVertical: 12,
1014
+ fontSize: 15,
1015
+ marginBottom: 20,
706
1016
  ...(Platform.OS === "ios" ? {} : { paddingTop: 12 }),
707
1017
  },
708
1018
  descInput: {
709
- borderWidth: 1, borderRadius: 12,
710
- paddingHorizontal: 14, paddingVertical: 12,
711
- 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,
712
1026
  ...(Platform.OS === "ios" ? {} : { paddingTop: 12 }),
713
1027
  },
714
- errorText: { fontSize: 13, marginBottom: 12, fontWeight: "500", color: "#d6453d" },
1028
+ errorText: {
1029
+ fontSize: 13,
1030
+ marginBottom: 12,
1031
+ fontWeight: "500",
1032
+ color: "#d6453d",
1033
+ },
715
1034
  submitBtn: { borderRadius: 12, padding: 14, alignItems: "center" },
716
1035
  submitBtnDisabled: { opacity: 0.5 },
717
1036
  submitBtnText: { color: "#fff", fontSize: 15, fontWeight: "700" },
718
- successTitle: { fontSize: 20, fontWeight: "700", textAlign: "center", marginBottom: 6 },
1037
+ successTitle: {
1038
+ fontSize: 20,
1039
+ fontWeight: "700",
1040
+ textAlign: "center",
1041
+ marginBottom: 6,
1042
+ },
719
1043
  successSub: { fontSize: 14, textAlign: "center" },
720
1044
 
721
1045
  // Pending badge
@@ -755,9 +1079,17 @@ const styles = StyleSheet.create({
755
1079
  badgeText: { fontSize: 10, fontWeight: "700", letterSpacing: 0.2 },
756
1080
  statusDot: { width: 6, height: 6, borderRadius: 3 },
757
1081
  statusBadge: {
758
- flexDirection: "row", alignItems: "center", gap: 5,
759
- paddingHorizontal: 8, paddingVertical: 3,
760
- 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",
761
1094
  },
762
- statusBadgeText: { fontSize: 10, fontWeight: "600", textTransform: "capitalize" },
763
1095
  });