@bbearai/react-native 0.8.1 → 0.8.3

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/dist/index.d.mts CHANGED
@@ -79,6 +79,11 @@ interface BugBearContextValue {
79
79
  refreshTesterInfo: () => Promise<void>;
80
80
  issueCounts: IssueCounts;
81
81
  refreshIssueCounts: () => Promise<void>;
82
+ /** Reopen a done issue with a required reason */
83
+ reopenReport: (reportId: string, reason: string) => Promise<{
84
+ success: boolean;
85
+ error?: string;
86
+ }>;
82
87
  /** Number of queued offline operations waiting to be sent. */
83
88
  queuedCount: number;
84
89
  /** URL to the BugBear web dashboard (for linking testers to the full web experience) */
package/dist/index.d.ts CHANGED
@@ -79,6 +79,11 @@ interface BugBearContextValue {
79
79
  refreshTesterInfo: () => Promise<void>;
80
80
  issueCounts: IssueCounts;
81
81
  refreshIssueCounts: () => Promise<void>;
82
+ /** Reopen a done issue with a required reason */
83
+ reopenReport: (reportId: string, reason: string) => Promise<{
84
+ success: boolean;
85
+ error?: string;
86
+ }>;
82
87
  /** Number of queued offline operations waiting to be sent. */
83
88
  queuedCount: number;
84
89
  /** URL to the BugBear web dashboard (for linking testers to the full web experience) */
package/dist/index.js CHANGED
@@ -12230,6 +12230,7 @@ var BugBearClient = class {
12230
12230
  this.navigationHistory = [];
12231
12231
  this.reportSubmitInFlight = false;
12232
12232
  this._queue = null;
12233
+ this._sessionStorage = new LocalStorageAdapter();
12233
12234
  this.realtimeChannels = [];
12234
12235
  this.monitor = null;
12235
12236
  this.initialized = false;
@@ -12307,6 +12308,10 @@ var BugBearClient = class {
12307
12308
  this.initOfflineQueue();
12308
12309
  this.initMonitoring();
12309
12310
  }
12311
+ /** Cache key scoped to the active project. */
12312
+ get sessionCacheKey() {
12313
+ return `bugbear_session_${this.config.projectId ?? "unknown"}`;
12314
+ }
12310
12315
  /** Initialize offline queue if configured. Shared by both init paths. */
12311
12316
  initOfflineQueue() {
12312
12317
  if (this.config.offlineQueue?.enabled) {
@@ -12370,6 +12375,26 @@ var BugBearClient = class {
12370
12375
  await this.pendingInit;
12371
12376
  if (this.initError) throw this.initError;
12372
12377
  }
12378
+ /**
12379
+ * Fire-and-forget call to a dashboard notification endpoint.
12380
+ * Only works when apiKey is configured (needed for API auth).
12381
+ * Failures are silently ignored — notifications are best-effort.
12382
+ */
12383
+ async notifyDashboard(path, body) {
12384
+ if (!this.config.apiKey) return;
12385
+ try {
12386
+ const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
12387
+ await fetch(`${baseUrl}/api/v1/notifications/${path}`, {
12388
+ method: "POST",
12389
+ headers: {
12390
+ "Content-Type": "application/json",
12391
+ "Authorization": `Bearer ${this.config.apiKey}`
12392
+ },
12393
+ body: JSON.stringify(body)
12394
+ });
12395
+ } catch {
12396
+ }
12397
+ }
12373
12398
  // ── Offline Queue ─────────────────────────────────────────
12374
12399
  /**
12375
12400
  * Access the offline queue (if enabled).
@@ -12396,6 +12421,48 @@ var BugBearClient = class {
12396
12421
  }
12397
12422
  await this._queue.load();
12398
12423
  }
12424
+ // ── Session Cache ──────────────────────────────────────────
12425
+ /**
12426
+ * Swap the session cache storage adapter (for React Native — pass AsyncStorage).
12427
+ * Must be called before getCachedSession() for persistence across app kills.
12428
+ * Web callers don't need this — LocalStorageAdapter is the default.
12429
+ */
12430
+ setSessionStorage(adapter) {
12431
+ this._sessionStorage = adapter;
12432
+ }
12433
+ /**
12434
+ * Cache the active QA session locally for instant restore on app restart.
12435
+ * Pass null to clear the cache (e.g. after ending a session).
12436
+ */
12437
+ async cacheSession(session) {
12438
+ try {
12439
+ if (session) {
12440
+ await this._sessionStorage.setItem(this.sessionCacheKey, JSON.stringify(session));
12441
+ } else {
12442
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
12443
+ }
12444
+ } catch {
12445
+ }
12446
+ }
12447
+ /**
12448
+ * Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
12449
+ * or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
12450
+ */
12451
+ async getCachedSession() {
12452
+ try {
12453
+ const raw = await this._sessionStorage.getItem(this.sessionCacheKey);
12454
+ if (!raw) return null;
12455
+ const session = JSON.parse(raw);
12456
+ const age = Date.now() - new Date(session.startedAt).getTime();
12457
+ if (age > 24 * 60 * 60 * 1e3) {
12458
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
12459
+ return null;
12460
+ }
12461
+ return session;
12462
+ } catch {
12463
+ return null;
12464
+ }
12465
+ }
12399
12466
  registerQueueHandlers() {
12400
12467
  if (!this._queue) return;
12401
12468
  this._queue.registerHandler("report", async (payload) => {
@@ -12413,6 +12480,11 @@ var BugBearClient = class {
12413
12480
  if (error) return { success: false, error: error.message };
12414
12481
  return { success: true };
12415
12482
  });
12483
+ this._queue.registerHandler("email_capture", async (payload) => {
12484
+ const { error } = await this.supabase.from("email_captures").insert(payload).select("id").single();
12485
+ if (error) return { success: false, error: error.message };
12486
+ return { success: true };
12487
+ });
12416
12488
  }
12417
12489
  // ── Realtime Subscriptions ─────────────────────────────────
12418
12490
  /** Whether realtime is enabled in config. */
@@ -12600,6 +12672,8 @@ var BugBearClient = class {
12600
12672
  if (this.config.onReportSubmitted) {
12601
12673
  this.config.onReportSubmitted(report);
12602
12674
  }
12675
+ this.notifyDashboard("report", { reportId: data.id }).catch(() => {
12676
+ });
12603
12677
  return { success: true, reportId: data.id };
12604
12678
  } catch (err) {
12605
12679
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -12612,6 +12686,44 @@ var BugBearClient = class {
12612
12686
  this.reportSubmitInFlight = false;
12613
12687
  }
12614
12688
  }
12689
+ /**
12690
+ * Capture an email for QA testing.
12691
+ * Called by the email interceptor — not typically called directly.
12692
+ */
12693
+ async captureEmail(payload) {
12694
+ try {
12695
+ await this.ready();
12696
+ if (!payload.subject || !payload.to || payload.to.length === 0) {
12697
+ return { success: false, error: "subject and to are required" };
12698
+ }
12699
+ const record = {
12700
+ project_id: this.config.projectId,
12701
+ to_addresses: payload.to,
12702
+ from_address: payload.from || null,
12703
+ subject: payload.subject,
12704
+ html_content: payload.html || null,
12705
+ text_content: payload.text || null,
12706
+ template_id: payload.templateId || null,
12707
+ metadata: payload.metadata || {},
12708
+ capture_mode: payload.captureMode,
12709
+ was_delivered: payload.wasDelivered,
12710
+ delivery_status: payload.wasDelivered ? "sent" : "pending"
12711
+ };
12712
+ const { data, error } = await this.supabase.from("email_captures").insert(record).select("id").single();
12713
+ if (error) {
12714
+ if (this._queue && isNetworkError(error.message)) {
12715
+ await this._queue.enqueue("email_capture", record);
12716
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
12717
+ }
12718
+ console.error("BugBear: Failed to capture email", error.message);
12719
+ return { success: false, error: error.message };
12720
+ }
12721
+ return { success: true, captureId: data.id };
12722
+ } catch (err) {
12723
+ const message = err instanceof Error ? err.message : "Unknown error";
12724
+ return { success: false, error: message };
12725
+ }
12726
+ }
12615
12727
  /**
12616
12728
  * Get assigned tests for current user
12617
12729
  * First looks up the tester by email, then fetches their assignments
@@ -13233,6 +13345,32 @@ var BugBearClient = class {
13233
13345
  return [];
13234
13346
  }
13235
13347
  }
13348
+ /**
13349
+ * Reopen a done issue that the tester believes isn't actually fixed.
13350
+ * Transitions the report from a done status back to 'confirmed'.
13351
+ */
13352
+ async reopenReport(reportId, reason) {
13353
+ try {
13354
+ const testerInfo = await this.getTesterInfo();
13355
+ if (!testerInfo) return { success: false, error: "Not authenticated as tester" };
13356
+ const { data, error } = await this.supabase.rpc("reopen_report", {
13357
+ p_report_id: reportId,
13358
+ p_tester_id: testerInfo.id,
13359
+ p_reason: reason
13360
+ });
13361
+ if (error) {
13362
+ console.error("BugBear: Failed to reopen report", formatPgError(error));
13363
+ return { success: false, error: error.message };
13364
+ }
13365
+ if (!data?.success) {
13366
+ return { success: false, error: data?.error || "Failed to reopen report" };
13367
+ }
13368
+ return { success: true };
13369
+ } catch (err) {
13370
+ console.error("BugBear: Error reopening report", err);
13371
+ return { success: false, error: "Unexpected error" };
13372
+ }
13373
+ }
13236
13374
  /**
13237
13375
  * Basic email format validation (defense in depth)
13238
13376
  */
@@ -13819,7 +13957,7 @@ var BugBearClient = class {
13819
13957
  insertData.attachments = safeAttachments;
13820
13958
  }
13821
13959
  }
13822
- const { error } = await this.supabase.from("discussion_messages").insert(insertData);
13960
+ const { data: msgData, error } = await this.supabase.from("discussion_messages").insert(insertData).select("id").single();
13823
13961
  if (error) {
13824
13962
  if (this._queue && isNetworkError(error.message)) {
13825
13963
  await this._queue.enqueue("message", insertData);
@@ -13828,6 +13966,10 @@ var BugBearClient = class {
13828
13966
  console.error("BugBear: Failed to send message", formatPgError(error));
13829
13967
  return false;
13830
13968
  }
13969
+ if (msgData?.id) {
13970
+ this.notifyDashboard("message", { threadId, messageId: msgData.id }).catch(() => {
13971
+ });
13972
+ }
13831
13973
  await this.markThreadAsRead(threadId);
13832
13974
  return true;
13833
13975
  } catch (err) {
@@ -13953,6 +14095,7 @@ var BugBearClient = class {
13953
14095
  if (!session) {
13954
14096
  return { success: false, error: "Session created but could not be fetched" };
13955
14097
  }
14098
+ await this.cacheSession(session);
13956
14099
  return { success: true, session };
13957
14100
  } catch (err) {
13958
14101
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -13976,6 +14119,7 @@ var BugBearClient = class {
13976
14119
  return { success: false, error: error.message };
13977
14120
  }
13978
14121
  const session = this.transformSession(data);
14122
+ await this.cacheSession(null);
13979
14123
  return { success: true, session };
13980
14124
  } catch (err) {
13981
14125
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -13991,8 +14135,13 @@ var BugBearClient = class {
13991
14135
  const testerInfo = await this.getTesterInfo();
13992
14136
  if (!testerInfo) return null;
13993
14137
  const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).eq("status", "active").order("started_at", { ascending: false }).limit(1).maybeSingle();
13994
- if (error || !data) return null;
13995
- return this.transformSession(data);
14138
+ if (error || !data) {
14139
+ await this.cacheSession(null);
14140
+ return null;
14141
+ }
14142
+ const session = this.transformSession(data);
14143
+ await this.cacheSession(session);
14144
+ return session;
13996
14145
  } catch (err) {
13997
14146
  console.error("BugBear: Error fetching active session", err);
13998
14147
  return null;
@@ -14510,6 +14659,7 @@ var BugBearContext = (0, import_react.createContext)({
14510
14659
  issueCounts: { open: 0, done: 0, reopened: 0 },
14511
14660
  refreshIssueCounts: async () => {
14512
14661
  },
14662
+ reopenReport: async () => ({ success: false }),
14513
14663
  queuedCount: 0,
14514
14664
  dashboardUrl: void 0,
14515
14665
  onError: void 0
@@ -14651,6 +14801,14 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14651
14801
  const counts = await client.getIssueCounts();
14652
14802
  setIssueCounts(counts);
14653
14803
  }, [client]);
14804
+ const reopenReport = (0, import_react.useCallback)(async (reportId, reason) => {
14805
+ if (!client) return { success: false, error: "Client not initialized" };
14806
+ const result = await client.reopenReport(reportId, reason);
14807
+ if (result.success) {
14808
+ await refreshIssueCounts();
14809
+ }
14810
+ return result;
14811
+ }, [client, refreshIssueCounts]);
14654
14812
  const initializeBugBear = (0, import_react.useCallback)(async (bugBearClient) => {
14655
14813
  setIsLoading(true);
14656
14814
  try {
@@ -14736,6 +14894,10 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14736
14894
  setClient(newClient);
14737
14895
  (async () => {
14738
14896
  try {
14897
+ const cachedSession = await newClient.getCachedSession();
14898
+ if (cachedSession) {
14899
+ setActiveSession(cachedSession);
14900
+ }
14739
14901
  await initializeBugBear(newClient);
14740
14902
  if (newClient.monitor && config.monitoring) {
14741
14903
  const getCurrentRoute = () => contextCapture.getCurrentRoute();
@@ -14870,6 +15032,7 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14870
15032
  // Issue tracking
14871
15033
  issueCounts,
14872
15034
  refreshIssueCounts,
15035
+ reopenReport,
14873
15036
  queuedCount,
14874
15037
  dashboardUrl: config.dashboardUrl,
14875
15038
  onError: config.onError
@@ -15601,6 +15764,17 @@ function TestDetailScreen({ testId, nav }) {
15601
15764
  setShowSteps(true);
15602
15765
  setShowDetails(false);
15603
15766
  }, [displayedAssignment?.id]);
15767
+ (0, import_react5.useEffect)(() => {
15768
+ if (!client || !displayedAssignment || displayedAssignment.status !== "pending") return;
15769
+ let cancelled = false;
15770
+ (async () => {
15771
+ await client.updateAssignmentStatus(displayedAssignment.id, "in_progress");
15772
+ if (!cancelled) await refreshAssignments();
15773
+ })();
15774
+ return () => {
15775
+ cancelled = true;
15776
+ };
15777
+ }, [client, displayedAssignment?.id, displayedAssignment?.status, refreshAssignments]);
15604
15778
  (0, import_react5.useEffect)(() => {
15605
15779
  const active = displayedAssignment?.status === "in_progress" ? displayedAssignment : null;
15606
15780
  if (!active?.startedAt) {
@@ -15651,17 +15825,6 @@ function TestDetailScreen({ testId, nav }) {
15651
15825
  setIsSubmitting(false);
15652
15826
  }
15653
15827
  }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
15654
- const handleStart = (0, import_react5.useCallback)(async () => {
15655
- if (!client || !displayedAssignment || isSubmitting) return;
15656
- import_react_native5.Keyboard.dismiss();
15657
- setIsSubmitting(true);
15658
- try {
15659
- await client.updateAssignmentStatus(displayedAssignment.id, "in_progress");
15660
- await refreshAssignments();
15661
- } finally {
15662
- setIsSubmitting(false);
15663
- }
15664
- }, [client, displayedAssignment, refreshAssignments, isSubmitting]);
15665
15828
  const handleReopen = (0, import_react5.useCallback)(async () => {
15666
15829
  if (!client || !displayedAssignment || isSubmitting) return;
15667
15830
  import_react_native5.Keyboard.dismiss();
@@ -15807,15 +15970,7 @@ function TestDetailScreen({ testId, nav }) {
15807
15970
  disabled: isSubmitting
15808
15971
  },
15809
15972
  /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.passBtnText }, "Change to Pass")
15810
- ))) : displayedAssignment.status === "pending" ? /* @__PURE__ */ import_react5.default.createElement(
15811
- import_react_native5.TouchableOpacity,
15812
- {
15813
- style: [styles5.startBtn, isSubmitting && { opacity: 0.5 }],
15814
- onPress: handleStart,
15815
- disabled: isSubmitting
15816
- },
15817
- /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.startBtnText }, isSubmitting ? "Starting..." : displayedAssignment.isVerification ? "\u{1F50D} Start Verification" : "\u25B6 Start Test")
15818
- ) : /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.actionButtons }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.failBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Reporting..." : "Failing..." : displayedAssignment.isVerification ? "\u2717 Still Broken" : "Fail")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.skipBtnText }, "Skip")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.passBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Verifying..." : "Passing..." : displayedAssignment.isVerification ? "\u2713 Fix Verified" : "Pass"))), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.modalOverlay }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.modalContent }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.modalTitle }, "Skip this test?"), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.modalSubtitle }, "Select a reason:"), [
15973
+ ))) : /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.actionButtons }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.failBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Reporting..." : "Failing..." : displayedAssignment.isVerification ? "\u2717 Still Broken" : "Fail")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.skipBtnText }, "Skip")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.passBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Verifying..." : "Passing..." : displayedAssignment.isVerification ? "\u2713 Fix Verified" : "Pass"))), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.modalOverlay }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.modalContent }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.modalTitle }, "Skip this test?"), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.modalSubtitle }, "Select a reason:"), [
15819
15974
  { reason: "blocked", label: "\u{1F6AB} Blocked by a bug" },
15820
15975
  { reason: "not_ready", label: "\u{1F6A7} Feature not ready" },
15821
15976
  { reason: "dependency", label: "\u{1F517} Needs another test first" },
@@ -15929,9 +16084,6 @@ function createStyles2() {
15929
16084
  completedLabel: { fontSize: 13, fontWeight: "600", color: colors.textSecondary },
15930
16085
  reopenBtn: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.blue },
15931
16086
  reopenBtnText: { fontSize: 14, fontWeight: "600", color: colors.blue },
15932
- // Start Test button
15933
- startBtn: { paddingVertical: 16, borderRadius: 12, alignItems: "center", backgroundColor: colors.blueSurface, borderWidth: 1, borderColor: colors.blueAccent, marginTop: 8 },
15934
- startBtnText: { fontSize: 16, fontWeight: "600", color: colors.blueText },
15935
16087
  // Action buttons
15936
16088
  actionButtons: { flexDirection: "row", gap: 10, marginTop: 8 },
15937
16089
  actionBtn: { flex: 1, paddingVertical: 14, borderRadius: 12, alignItems: "center" },
@@ -16044,17 +16196,23 @@ function TestListScreen({ nav }) {
16044
16196
  const keyMatch = a.testCase.testKey.toLowerCase().includes(q);
16045
16197
  if (!titleMatch && !keyMatch) return false;
16046
16198
  }
16047
- if (filter === "pending") return a.status === "pending" || a.status === "in_progress";
16048
- if (filter === "done") return a.status === "passed";
16049
- if (filter === "reopened") return a.status === "failed";
16199
+ if (filter === "todo") {
16200
+ return !a.isVerification && (a.status === "pending" || a.status === "in_progress" || a.status === "failed");
16201
+ }
16202
+ if (filter === "retest") {
16203
+ return !!a.isVerification && (a.status === "pending" || a.status === "in_progress");
16204
+ }
16205
+ if (filter === "done") {
16206
+ return a.status === "passed" || a.status === "skipped" || a.status === "blocked";
16207
+ }
16050
16208
  return true;
16051
16209
  }, [platformFilter, roleFilter, trackFilter, searchQuery, filter]);
16052
16210
  if (isLoading) return /* @__PURE__ */ import_react6.default.createElement(TestListScreenSkeleton, null);
16053
16211
  return /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, null, /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: styles5.filterBar }, [
16054
16212
  { key: "all", label: "All", count: assignments.length },
16055
- { key: "pending", label: "To Do", count: assignments.filter((a) => a.status === "pending" || a.status === "in_progress").length },
16056
- { key: "done", label: "Done", count: assignments.filter((a) => a.status === "passed").length },
16057
- { key: "reopened", label: "Re Opened", count: assignments.filter((a) => a.status === "failed").length }
16213
+ { key: "todo", label: "To Do", count: assignments.filter((a) => !a.isVerification && (a.status === "pending" || a.status === "in_progress" || a.status === "failed")).length },
16214
+ { key: "retest", label: "Retest", count: assignments.filter((a) => !!a.isVerification && (a.status === "pending" || a.status === "in_progress")).length },
16215
+ { key: "done", label: "Done", count: assignments.filter((a) => a.status === "passed" || a.status === "skipped" || a.status === "blocked").length }
16058
16216
  ].map((f) => /* @__PURE__ */ import_react6.default.createElement(import_react_native6.TouchableOpacity, { key: f.key, style: [styles5.filterBtn, filter === f.key && styles5.filterBtnActive], onPress: () => setFilter(f.key) }, /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: [styles5.filterBtnText, filter === f.key && styles5.filterBtnTextActive] }, f.label, " (", f.count, ")")))), availableRoles.length >= 2 && /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: styles5.roleSection }, /* @__PURE__ */ import_react6.default.createElement(import_react_native6.ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, style: styles5.roleBar }, /* @__PURE__ */ import_react6.default.createElement(
16059
16217
  import_react_native6.TouchableOpacity,
16060
16218
  {
@@ -16157,7 +16315,13 @@ function TestListScreen({ nav }) {
16157
16315
  onPress: () => nav.push({ name: "TEST_DETAIL", testId: assignment.id })
16158
16316
  },
16159
16317
  /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: styles5.testBadge }, badge.icon),
16160
- /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: styles5.testInfo }, /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: styles5.testTitle, numberOfLines: 1 }, assignment.testCase.title), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: styles5.testMetaRow }, assignment.isVerification && /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: styles5.retestTag }, /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: styles5.retestTagText }, "Retest")), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: styles5.testMeta }, assignment.testCase.testKey, " \xB7 ", assignment.testCase.priority), assignment.testCase.role && /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: styles5.roleBadgeRow }, /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: styles5.testMeta }, " \xB7 "), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: [styles5.roleBadgeDot, { backgroundColor: assignment.testCase.role.color }] }), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: [styles5.testMeta, { color: assignment.testCase.role.color, fontWeight: "500" }] }, assignment.testCase.role.name)))),
16318
+ /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: styles5.testInfo }, /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: styles5.testTitle, numberOfLines: 1 }, assignment.testCase.title), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: styles5.testMetaRow }, assignment.isVerification && /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: [
16319
+ styles5.retestTag,
16320
+ assignment.status === "passed" && { backgroundColor: colors.greenDark, borderColor: colors.greenBorder }
16321
+ ] }, /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: [
16322
+ styles5.retestTagText,
16323
+ assignment.status === "passed" && { color: colors.greenLight }
16324
+ ] }, assignment.status === "passed" ? "Verified" : "Retest")), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: styles5.testMeta }, assignment.testCase.testKey, " \xB7 ", assignment.testCase.priority), assignment.testCase.role && /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: styles5.roleBadgeRow }, /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: styles5.testMeta }, " \xB7 "), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: [styles5.roleBadgeDot, { backgroundColor: assignment.testCase.role.color }] }), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: [styles5.testMeta, { color: assignment.testCase.role.color, fontWeight: "500" }] }, assignment.testCase.role.name)))),
16161
16325
  /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: [
16162
16326
  styles5.statusPill,
16163
16327
  {
@@ -17673,9 +17837,15 @@ function createStyles11() {
17673
17837
  // src/widget/screens/IssueDetailScreen.tsx
17674
17838
  var import_react19 = __toESM(require("react"));
17675
17839
  var import_react_native18 = require("react-native");
17840
+ var DONE_STATUSES = ["verified", "resolved", "closed", "reviewed"];
17676
17841
  function IssueDetailScreen({ nav, issue }) {
17677
- const { dashboardUrl, widgetColorScheme } = useBugBear();
17842
+ const { dashboardUrl, widgetColorScheme, reopenReport } = useBugBear();
17678
17843
  const styles5 = (0, import_react19.useMemo)(() => createStyles12(), [widgetColorScheme]);
17844
+ const [showReopenForm, setShowReopenForm] = (0, import_react19.useState)(false);
17845
+ const [reopenReason, setReopenReason] = (0, import_react19.useState)("");
17846
+ const [isSubmitting, setIsSubmitting] = (0, import_react19.useState)(false);
17847
+ const [reopenError, setReopenError] = (0, import_react19.useState)(null);
17848
+ const [wasReopened, setWasReopened] = (0, import_react19.useState)(false);
17679
17849
  const STATUS_LABELS = (0, import_react19.useMemo)(() => ({
17680
17850
  new: { label: "New", bg: colors.blueDark, color: colors.blueLight },
17681
17851
  triaging: { label: "Triaging", bg: colors.blueDark, color: colors.blueLight },
@@ -17698,7 +17868,68 @@ function IssueDetailScreen({ nav, issue }) {
17698
17868
  }), [widgetColorScheme]);
17699
17869
  const statusConfig = STATUS_LABELS[issue.status] || { label: issue.status, bg: colors.card, color: colors.textSecondary };
17700
17870
  const severityConfig = issue.severity ? SEVERITY_CONFIG[issue.severity] : null;
17701
- return /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, null, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.badgeRow }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles5.badge, { backgroundColor: statusConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles5.badgeText, { color: statusConfig.color }] }, statusConfig.label)), severityConfig && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles5.badge, { backgroundColor: severityConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles5.badgeText, { color: severityConfig.color }] }, severityConfig.label))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.title }, issue.title), issue.route && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.route }, issue.route), issue.description && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.descriptionCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.descriptionText }, issue.description)), issue.verifiedByName && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.verifiedCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.verifiedHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedIcon }, "\u2705"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedTitle }, "Retesting Proof")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedBody }, "Verified by ", issue.verifiedByName, issue.verifiedAt && ` on ${new Date(issue.verifiedAt).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" })}`)), issue.originalBugTitle && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.originalBugCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.originalBugHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugIcon }, "\u{1F504}"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugTitle }, "Original Bug")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugBody }, "Retest of: ", issue.originalBugTitle)), issue.screenshotUrls && issue.screenshotUrls.length > 0 && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.screenshotSection }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.screenshotLabel }, "Screenshots (", issue.screenshotUrls.length, ")"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.screenshotRow }, issue.screenshotUrls.map((url, i) => /* @__PURE__ */ import_react19.default.createElement(import_react_native18.TouchableOpacity, { key: i, onPress: () => import_react_native18.Linking.openURL(url), activeOpacity: 0.7 }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Image, { source: { uri: url }, style: styles5.screenshotThumb }))))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.metaSection }, issue.reporterName && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.metaText }, "Reported by ", issue.reporterName), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.metaTextSmall }, "Created ", formatRelativeTime(issue.createdAt), " ", "\xB7", " Updated ", formatRelativeTime(issue.updatedAt))), dashboardUrl && /* @__PURE__ */ import_react19.default.createElement(
17871
+ const isDone = DONE_STATUSES.includes(issue.status);
17872
+ const handleReopen = (0, import_react19.useCallback)(async () => {
17873
+ if (!reopenReason.trim()) return;
17874
+ setIsSubmitting(true);
17875
+ setReopenError(null);
17876
+ const result = await reopenReport(issue.id, reopenReason.trim());
17877
+ setIsSubmitting(false);
17878
+ if (result.success) {
17879
+ setWasReopened(true);
17880
+ setShowReopenForm(false);
17881
+ } else {
17882
+ setReopenError(result.error || "Failed to reopen");
17883
+ }
17884
+ }, [reopenReason, reopenReport, issue.id]);
17885
+ return /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, null, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.badgeRow }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles5.badge, { backgroundColor: wasReopened ? colors.yellowDark : statusConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles5.badgeText, { color: wasReopened ? colors.yellowLight : statusConfig.color }] }, wasReopened ? "Reopened" : statusConfig.label)), severityConfig && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles5.badge, { backgroundColor: severityConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles5.badgeText, { color: severityConfig.color }] }, severityConfig.label))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.title }, issue.title), issue.route && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.route }, issue.route), issue.description && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.descriptionCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.descriptionText }, issue.description)), wasReopened && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.reopenedCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.reopenedRow }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenedIcon }, "\u{1F504}"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenedText }, "Issue reopened \u2014 your team has been notified"))), issue.verifiedByName && !wasReopened && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.verifiedCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.verifiedHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedIcon }, "\u2705"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedTitle }, "Retesting Proof")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedBody }, "Verified by ", issue.verifiedByName, issue.verifiedAt && ` on ${new Date(issue.verifiedAt).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" })}`)), issue.originalBugTitle && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.originalBugCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.originalBugHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugIcon }, "\u{1F504}"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugTitleText }, "Original Bug")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugBody }, "Retest of: ", issue.originalBugTitle)), isDone && !wasReopened && !showReopenForm && /* @__PURE__ */ import_react19.default.createElement(
17886
+ import_react_native18.TouchableOpacity,
17887
+ {
17888
+ style: styles5.reopenButton,
17889
+ onPress: () => setShowReopenForm(true),
17890
+ activeOpacity: 0.7
17891
+ },
17892
+ /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenButtonText }, "\u{1F504}", " Not Fixed \u2014 Reopen Issue")
17893
+ ), showReopenForm && !wasReopened && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.reopenForm }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenFormTitle }, "Why isn't this fixed?"), /* @__PURE__ */ import_react19.default.createElement(
17894
+ import_react_native18.TextInput,
17895
+ {
17896
+ value: reopenReason,
17897
+ onChangeText: setReopenReason,
17898
+ placeholder: "Describe what you're still seeing...",
17899
+ placeholderTextColor: colors.textMuted,
17900
+ style: styles5.reopenInput,
17901
+ multiline: true,
17902
+ autoFocus: true
17903
+ }
17904
+ ), reopenError && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenErrorText }, reopenError), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.reopenActions }, /* @__PURE__ */ import_react19.default.createElement(
17905
+ import_react_native18.TouchableOpacity,
17906
+ {
17907
+ style: [
17908
+ styles5.reopenSubmitButton,
17909
+ (!reopenReason.trim() || isSubmitting) && styles5.reopenSubmitDisabled
17910
+ ],
17911
+ onPress: handleReopen,
17912
+ disabled: isSubmitting || !reopenReason.trim(),
17913
+ activeOpacity: 0.7
17914
+ },
17915
+ isSubmitting ? /* @__PURE__ */ import_react19.default.createElement(import_react_native18.ActivityIndicator, { size: "small", color: "#fff" }) : /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [
17916
+ styles5.reopenSubmitText,
17917
+ !reopenReason.trim() && styles5.reopenSubmitTextDisabled
17918
+ ] }, "Reopen Issue")
17919
+ ), /* @__PURE__ */ import_react19.default.createElement(
17920
+ import_react_native18.TouchableOpacity,
17921
+ {
17922
+ style: styles5.reopenCancelButton,
17923
+ onPress: () => {
17924
+ setShowReopenForm(false);
17925
+ setReopenReason("");
17926
+ setReopenError(null);
17927
+ },
17928
+ disabled: isSubmitting,
17929
+ activeOpacity: 0.7
17930
+ },
17931
+ /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenCancelText }, "Cancel")
17932
+ ))), issue.screenshotUrls && issue.screenshotUrls.length > 0 && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.screenshotSection }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.screenshotLabel }, "Screenshots (", issue.screenshotUrls.length, ")"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.screenshotRow }, issue.screenshotUrls.map((url, i) => /* @__PURE__ */ import_react19.default.createElement(import_react_native18.TouchableOpacity, { key: i, onPress: () => import_react_native18.Linking.openURL(url), activeOpacity: 0.7 }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Image, { source: { uri: url }, style: styles5.screenshotThumb }))))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.metaSection }, issue.reporterName && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.metaText }, "Reported by ", issue.reporterName), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.metaTextSmall }, "Created ", formatRelativeTime(issue.createdAt), " ", "\xB7", " Updated ", formatRelativeTime(issue.updatedAt))), dashboardUrl && /* @__PURE__ */ import_react19.default.createElement(
17702
17933
  import_react_native18.TouchableOpacity,
17703
17934
  {
17704
17935
  style: styles5.dashboardLink,
@@ -17750,6 +17981,28 @@ function createStyles12() {
17750
17981
  color: colors.textSecondary,
17751
17982
  lineHeight: 19
17752
17983
  },
17984
+ reopenedCard: {
17985
+ backgroundColor: colors.yellowDark,
17986
+ borderWidth: 1,
17987
+ borderColor: colors.yellowBorder,
17988
+ borderRadius: 8,
17989
+ padding: 12,
17990
+ marginBottom: 12
17991
+ },
17992
+ reopenedRow: {
17993
+ flexDirection: "row",
17994
+ alignItems: "center",
17995
+ gap: 8
17996
+ },
17997
+ reopenedIcon: {
17998
+ fontSize: 14
17999
+ },
18000
+ reopenedText: {
18001
+ fontSize: 13,
18002
+ fontWeight: "600",
18003
+ color: colors.yellowLight,
18004
+ flex: 1
18005
+ },
17753
18006
  verifiedCard: {
17754
18007
  backgroundColor: colors.greenDark,
17755
18008
  borderWidth: 1,
@@ -17793,7 +18046,7 @@ function createStyles12() {
17793
18046
  originalBugIcon: {
17794
18047
  fontSize: 16
17795
18048
  },
17796
- originalBugTitle: {
18049
+ originalBugTitleText: {
17797
18050
  fontSize: 13,
17798
18051
  fontWeight: "600",
17799
18052
  color: colors.yellowLight
@@ -17802,6 +18055,89 @@ function createStyles12() {
17802
18055
  fontSize: 12,
17803
18056
  color: colors.yellowSubtle
17804
18057
  },
18058
+ reopenButton: {
18059
+ borderWidth: 1,
18060
+ borderColor: colors.orange,
18061
+ borderRadius: 8,
18062
+ paddingVertical: 10,
18063
+ paddingHorizontal: 16,
18064
+ alignItems: "center",
18065
+ marginBottom: 12
18066
+ },
18067
+ reopenButtonText: {
18068
+ fontSize: 13,
18069
+ fontWeight: "600",
18070
+ color: colors.orange
18071
+ },
18072
+ reopenForm: {
18073
+ backgroundColor: colors.card,
18074
+ borderWidth: 1,
18075
+ borderColor: colors.orange,
18076
+ borderRadius: 8,
18077
+ padding: 12,
18078
+ marginBottom: 12
18079
+ },
18080
+ reopenFormTitle: {
18081
+ fontSize: 13,
18082
+ fontWeight: "600",
18083
+ color: colors.orange,
18084
+ marginBottom: 8
18085
+ },
18086
+ reopenInput: {
18087
+ backgroundColor: colors.bg,
18088
+ borderWidth: 1,
18089
+ borderColor: colors.border,
18090
+ borderRadius: 6,
18091
+ padding: 8,
18092
+ color: colors.textPrimary,
18093
+ fontSize: 13,
18094
+ lineHeight: 18,
18095
+ minHeight: 60,
18096
+ textAlignVertical: "top"
18097
+ },
18098
+ reopenErrorText: {
18099
+ fontSize: 12,
18100
+ color: colors.red,
18101
+ marginTop: 6
18102
+ },
18103
+ reopenActions: {
18104
+ flexDirection: "row",
18105
+ gap: 8,
18106
+ marginTop: 8
18107
+ },
18108
+ reopenSubmitButton: {
18109
+ flex: 1,
18110
+ backgroundColor: colors.orange,
18111
+ borderRadius: 6,
18112
+ paddingVertical: 8,
18113
+ paddingHorizontal: 12,
18114
+ alignItems: "center",
18115
+ justifyContent: "center"
18116
+ },
18117
+ reopenSubmitDisabled: {
18118
+ backgroundColor: colors.card,
18119
+ opacity: 0.7
18120
+ },
18121
+ reopenSubmitText: {
18122
+ fontSize: 13,
18123
+ fontWeight: "600",
18124
+ color: "#fff"
18125
+ },
18126
+ reopenSubmitTextDisabled: {
18127
+ color: colors.textMuted
18128
+ },
18129
+ reopenCancelButton: {
18130
+ borderWidth: 1,
18131
+ borderColor: colors.border,
18132
+ borderRadius: 6,
18133
+ paddingVertical: 8,
18134
+ paddingHorizontal: 12,
18135
+ alignItems: "center"
18136
+ },
18137
+ reopenCancelText: {
18138
+ fontSize: 13,
18139
+ color: colors.textSecondary
18140
+ },
17805
18141
  screenshotSection: {
17806
18142
  marginBottom: 12
17807
18143
  },