@bbearai/react 0.5.0 → 0.5.2

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.
Files changed (3) hide show
  1. package/dist/index.js +568 -72
  2. package/dist/index.mjs +542 -46
  3. package/package.json +4 -2
package/dist/index.mjs CHANGED
@@ -304,11 +304,80 @@ function BugBearProvider({ config, children, enabled = true }) {
304
304
  }
305
305
 
306
306
  // src/BugBearPanel.tsx
307
- import { useState as useState11, useRef as useRef3, useEffect as useEffect8, useCallback as useCallback5 } from "react";
307
+ import { useState as useState11, useRef as useRef4, useEffect as useEffect11, useCallback as useCallback6 } from "react";
308
308
  import { createPortal } from "react-dom";
309
309
 
310
310
  // src/widget/navigation.ts
311
- import { useReducer } from "react";
311
+ import { useReducer, useEffect as useEffect2, useRef as useRef2 } from "react";
312
+ var NAV_STORAGE_KEY = "bugbear-nav-screen";
313
+ function serializeScreen(screen) {
314
+ switch (screen.name) {
315
+ case "HOME":
316
+ case "TEST_LIST":
317
+ case "MESSAGE_LIST":
318
+ case "COMPOSE_MESSAGE":
319
+ case "PROFILE":
320
+ case "REPORT_SUCCESS":
321
+ return JSON.stringify({ name: screen.name });
322
+ case "TEST_DETAIL":
323
+ return JSON.stringify({ name: screen.name, testId: screen.testId });
324
+ case "REPORT":
325
+ return JSON.stringify({ name: screen.name });
326
+ case "ISSUE_LIST":
327
+ return JSON.stringify({ name: screen.name, category: screen.category });
328
+ // Complex screens — save their parent list instead
329
+ case "THREAD_DETAIL":
330
+ return JSON.stringify({ name: "MESSAGE_LIST" });
331
+ case "ISSUE_DETAIL":
332
+ return JSON.stringify({ name: "ISSUE_LIST", category: "open" });
333
+ case "TEST_FEEDBACK":
334
+ return JSON.stringify({ name: "TEST_LIST" });
335
+ default:
336
+ return null;
337
+ }
338
+ }
339
+ function deserializeScreen(json) {
340
+ try {
341
+ const parsed = JSON.parse(json);
342
+ if (!parsed || !parsed.name) return null;
343
+ switch (parsed.name) {
344
+ case "HOME":
345
+ return { name: "HOME" };
346
+ case "TEST_LIST":
347
+ return { name: "TEST_LIST" };
348
+ case "MESSAGE_LIST":
349
+ return { name: "MESSAGE_LIST" };
350
+ case "COMPOSE_MESSAGE":
351
+ return { name: "COMPOSE_MESSAGE" };
352
+ case "PROFILE":
353
+ return { name: "PROFILE" };
354
+ case "REPORT_SUCCESS":
355
+ return { name: "REPORT_SUCCESS" };
356
+ case "TEST_DETAIL":
357
+ return { name: "TEST_DETAIL", testId: parsed.testId };
358
+ case "REPORT":
359
+ return { name: "REPORT" };
360
+ case "ISSUE_LIST":
361
+ return { name: "ISSUE_LIST", category: parsed.category || "open" };
362
+ default:
363
+ return null;
364
+ }
365
+ } catch {
366
+ return null;
367
+ }
368
+ }
369
+ function getInitialScreen() {
370
+ if (typeof window === "undefined") return { name: "HOME" };
371
+ try {
372
+ const saved = localStorage.getItem(NAV_STORAGE_KEY);
373
+ if (saved) {
374
+ const screen = deserializeScreen(saved);
375
+ if (screen) return screen;
376
+ }
377
+ } catch {
378
+ }
379
+ return { name: "HOME" };
380
+ }
312
381
  function navReducer(state, action) {
313
382
  switch (action.type) {
314
383
  case "PUSH":
@@ -324,9 +393,21 @@ function navReducer(state, action) {
324
393
  }
325
394
  }
326
395
  function useNavigation() {
327
- const [state, dispatch] = useReducer(navReducer, { stack: [{ name: "HOME" }] });
396
+ const initialScreen = useRef2(getInitialScreen());
397
+ const [state, dispatch] = useReducer(navReducer, { stack: [initialScreen.current] });
398
+ const currentScreen = state.stack[state.stack.length - 1];
399
+ useEffect2(() => {
400
+ if (typeof window === "undefined") return;
401
+ try {
402
+ const serialized = serializeScreen(currentScreen);
403
+ if (serialized) {
404
+ localStorage.setItem(NAV_STORAGE_KEY, serialized);
405
+ }
406
+ } catch {
407
+ }
408
+ }, [currentScreen]);
328
409
  return {
329
- currentScreen: state.stack[state.stack.length - 1],
410
+ currentScreen,
330
411
  canGoBack: state.stack.length > 1,
331
412
  push: (screen) => dispatch({ type: "PUSH", screen }),
332
413
  pop: () => dispatch({ type: "POP" }),
@@ -420,8 +501,38 @@ function getThreadTypeIcon(type) {
420
501
  }
421
502
  }
422
503
 
504
+ // src/widget/useScreenCapture.ts
505
+ import { domToBlob } from "modern-screenshot";
506
+ async function capturePageScreenshot(options = {}) {
507
+ const { excludeElement, timeout = 3e3 } = options;
508
+ try {
509
+ const blob = await Promise.race([
510
+ domToBlob(document.documentElement, {
511
+ filter: (node) => {
512
+ if (excludeElement && node === excludeElement) return false;
513
+ return true;
514
+ },
515
+ scale: 1,
516
+ quality: 0.85
517
+ }),
518
+ new Promise(
519
+ (_, reject) => setTimeout(() => reject(new Error("Screenshot capture timed out")), timeout)
520
+ )
521
+ ]);
522
+ if (!blob) return null;
523
+ return new File(
524
+ [blob],
525
+ `screenshot-auto-${Date.now()}.png`,
526
+ { type: "image/png" }
527
+ );
528
+ } catch (err) {
529
+ console.warn("BugBear: Auto-capture failed, user can attach manually", err);
530
+ return null;
531
+ }
532
+ }
533
+
423
534
  // src/widget/screens/HomeScreen.tsx
424
- import { useEffect as useEffect2 } from "react";
535
+ import { useEffect as useEffect3 } from "react";
425
536
 
426
537
  // src/widget/Skeleton.tsx
427
538
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
@@ -562,9 +673,9 @@ function MessageListScreenSkeleton() {
562
673
 
563
674
  // src/widget/screens/HomeScreen.tsx
564
675
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
565
- function HomeScreen({ nav }) {
566
- const { assignments, unreadCount, threads, refreshAssignments, refreshThreads, issueCounts, refreshIssueCounts, isLoading } = useBugBear();
567
- useEffect2(() => {
676
+ function HomeScreen({ nav, onReportBug }) {
677
+ const { assignments, unreadCount, threads, refreshAssignments, refreshThreads, issueCounts, refreshIssueCounts, dashboardUrl, isLoading } = useBugBear();
678
+ useEffect3(() => {
568
679
  refreshAssignments();
569
680
  refreshThreads();
570
681
  refreshIssueCounts();
@@ -751,9 +862,11 @@ function HomeScreen({ nav }) {
751
862
  {
752
863
  role: "button",
753
864
  tabIndex: 0,
754
- onClick: () => nav.push({ name: "REPORT", prefill: { type: "bug" } }),
865
+ onClick: () => onReportBug ? onReportBug() : nav.push({ name: "REPORT", prefill: { type: "bug" } }),
755
866
  onKeyDown: (e) => {
756
- if (e.key === "Enter" || e.key === " ") nav.push({ name: "REPORT", prefill: { type: "bug" } });
867
+ if (e.key === "Enter" || e.key === " ") {
868
+ onReportBug ? onReportBug() : nav.push({ name: "REPORT", prefill: { type: "bug" } });
869
+ }
757
870
  },
758
871
  style: {
759
872
  backgroundColor: colors.card,
@@ -968,6 +1081,33 @@ function HomeScreen({ nav }) {
968
1081
  " tests completed"
969
1082
  ] })
970
1083
  ] }),
1084
+ dashboardUrl && /* @__PURE__ */ jsxs2(
1085
+ "a",
1086
+ {
1087
+ href: dashboardUrl,
1088
+ target: "_blank",
1089
+ rel: "noopener noreferrer",
1090
+ style: {
1091
+ display: "flex",
1092
+ alignItems: "center",
1093
+ backgroundColor: colors.card,
1094
+ border: `1px solid ${colors.border}`,
1095
+ borderRadius: 12,
1096
+ padding: 14,
1097
+ marginBottom: 16,
1098
+ textDecoration: "none",
1099
+ cursor: "pointer"
1100
+ },
1101
+ children: [
1102
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: 22, marginRight: 12 }, children: "\u{1F310}" }),
1103
+ /* @__PURE__ */ jsxs2("span", { style: { flex: 1 }, children: [
1104
+ /* @__PURE__ */ jsx3("span", { style: { display: "block", fontSize: 14, fontWeight: 600, color: colors.textPrimary }, children: "Open Dashboard" }),
1105
+ /* @__PURE__ */ jsx3("span", { style: { display: "block", fontSize: 12, color: colors.textMuted, marginTop: 2 }, children: "View analytics, history & more" })
1106
+ ] }),
1107
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: 16, color: colors.textMuted, marginLeft: 8 }, children: "\u2192" })
1108
+ ]
1109
+ }
1110
+ ),
971
1111
  /* @__PURE__ */ jsx3("div", { style: { display: "flex", justifyContent: "center", padding: "8px 0" }, children: /* @__PURE__ */ jsx3(
972
1112
  "button",
973
1113
  {
@@ -992,7 +1132,7 @@ function HomeScreen({ nav }) {
992
1132
  }
993
1133
 
994
1134
  // src/widget/screens/TestDetailScreen.tsx
995
- import { useState as useState2, useEffect as useEffect3, useCallback as useCallback2 } from "react";
1135
+ import { useState as useState2, useEffect as useEffect4, useCallback as useCallback2 } from "react";
996
1136
  import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
997
1137
  function TestDetailScreen({ testId, nav }) {
998
1138
  const { client, assignments, currentAssignment, refreshAssignments, onNavigate } = useBugBear();
@@ -1006,12 +1146,12 @@ function TestDetailScreen({ testId, nav }) {
1006
1146
  const [skipNotes, setSkipNotes] = useState2("");
1007
1147
  const [skipping, setSkipping] = useState2(false);
1008
1148
  const [isSubmitting, setIsSubmitting] = useState2(false);
1009
- useEffect3(() => {
1149
+ useEffect4(() => {
1010
1150
  setCriteriaResults({});
1011
1151
  setShowSteps(true);
1012
1152
  setShowDetails(false);
1013
1153
  }, [displayedAssignment?.id]);
1014
- useEffect3(() => {
1154
+ useEffect4(() => {
1015
1155
  const active = displayedAssignment?.status === "in_progress" ? displayedAssignment : null;
1016
1156
  if (!active?.startedAt) {
1017
1157
  setAssignmentElapsedTime(0);
@@ -1058,6 +1198,37 @@ function TestDetailScreen({ testId, nav }) {
1058
1198
  setIsSubmitting(false);
1059
1199
  }
1060
1200
  }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
1201
+ const handleReopen = useCallback2(async () => {
1202
+ if (!client || !displayedAssignment || isSubmitting) return;
1203
+ setIsSubmitting(true);
1204
+ try {
1205
+ await client.reopenAssignment(displayedAssignment.id);
1206
+ await refreshAssignments();
1207
+ } finally {
1208
+ setIsSubmitting(false);
1209
+ }
1210
+ }, [client, displayedAssignment, refreshAssignments, isSubmitting]);
1211
+ const handleChangeResult = useCallback2(async (newStatus) => {
1212
+ if (!client || !displayedAssignment || isSubmitting) return;
1213
+ setIsSubmitting(true);
1214
+ try {
1215
+ await client.reopenAssignment(displayedAssignment.id);
1216
+ await client.updateAssignmentStatus(displayedAssignment.id, newStatus);
1217
+ await refreshAssignments();
1218
+ if (newStatus === "failed") {
1219
+ nav.replace({
1220
+ name: "REPORT",
1221
+ prefill: {
1222
+ type: "test_fail",
1223
+ assignmentId: displayedAssignment.id,
1224
+ testCaseId: displayedAssignment.testCase.id
1225
+ }
1226
+ });
1227
+ }
1228
+ } finally {
1229
+ setIsSubmitting(false);
1230
+ }
1231
+ }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
1061
1232
  const handleSkip = useCallback2(async () => {
1062
1233
  if (!client || !displayedAssignment || !selectedSkipReason) return;
1063
1234
  setSkipping(true);
@@ -1565,6 +1736,11 @@ function TestDetailScreen({ testId, nav }) {
1565
1736
  ]
1566
1737
  }
1567
1738
  ),
1739
+ testCase.track && /* @__PURE__ */ jsxs3("div", { style: { fontSize: 12, color: colors.textSecondary, marginBottom: 6 }, children: [
1740
+ testCase.track.icon,
1741
+ " ",
1742
+ testCase.track.name
1743
+ ] }),
1568
1744
  testCase.description && /* @__PURE__ */ jsx4("div", { style: { fontSize: 13, color: colors.textSecondary, lineHeight: "18px" }, children: testCase.description }),
1569
1745
  testCase.group && /* @__PURE__ */ jsx4("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ jsxs3("span", { style: { fontSize: 12, color: colors.textMuted }, children: [
1570
1746
  "\u{1F4C1} ",
@@ -1573,7 +1749,113 @@ function TestDetailScreen({ testId, nav }) {
1573
1749
  ]
1574
1750
  }
1575
1751
  ),
1576
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 10, marginTop: 8 }, children: [
1752
+ displayedAssignment.status === "passed" || displayedAssignment.status === "failed" || displayedAssignment.status === "skipped" || displayedAssignment.status === "blocked" ? /* @__PURE__ */ jsxs3(Fragment, { children: [
1753
+ /* @__PURE__ */ jsxs3(
1754
+ "div",
1755
+ {
1756
+ style: {
1757
+ display: "flex",
1758
+ alignItems: "center",
1759
+ justifyContent: "center",
1760
+ gap: 6,
1761
+ padding: "8px 12px",
1762
+ borderRadius: 8,
1763
+ marginTop: 8,
1764
+ marginBottom: 8,
1765
+ backgroundColor: displayedAssignment.status === "passed" ? colors.greenDark : displayedAssignment.status === "failed" ? colors.redDark : colors.card,
1766
+ border: `1px solid ${displayedAssignment.status === "passed" ? colors.green : displayedAssignment.status === "failed" ? colors.red : colors.border}`
1767
+ },
1768
+ children: [
1769
+ /* @__PURE__ */ jsx4("span", { style: { fontSize: 14 }, children: displayedAssignment.status === "passed" ? "\u2705" : displayedAssignment.status === "failed" ? "\u274C" : displayedAssignment.status === "skipped" ? "\u23ED" : "\u{1F6AB}" }),
1770
+ /* @__PURE__ */ jsxs3(
1771
+ "span",
1772
+ {
1773
+ style: {
1774
+ fontSize: 13,
1775
+ fontWeight: 600,
1776
+ color: displayedAssignment.status === "passed" ? colors.green : displayedAssignment.status === "failed" ? "#fca5a5" : colors.textSecondary
1777
+ },
1778
+ children: [
1779
+ "Marked as ",
1780
+ displayedAssignment.status.charAt(0).toUpperCase() + displayedAssignment.status.slice(1)
1781
+ ]
1782
+ }
1783
+ )
1784
+ ]
1785
+ }
1786
+ ),
1787
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 10, marginTop: 4 }, children: [
1788
+ /* @__PURE__ */ jsx4(
1789
+ "button",
1790
+ {
1791
+ type: "button",
1792
+ onClick: handleReopen,
1793
+ disabled: isSubmitting,
1794
+ style: {
1795
+ flex: 1,
1796
+ paddingTop: 14,
1797
+ paddingBottom: 14,
1798
+ borderRadius: 12,
1799
+ textAlign: "center",
1800
+ backgroundColor: colors.card,
1801
+ border: `1px solid ${colors.blue}`,
1802
+ cursor: isSubmitting ? "not-allowed" : "pointer",
1803
+ fontSize: 14,
1804
+ fontWeight: 600,
1805
+ color: colors.blue,
1806
+ opacity: isSubmitting ? 0.5 : 1
1807
+ },
1808
+ children: isSubmitting ? "Reopening..." : "\u{1F504} Reopen Test"
1809
+ }
1810
+ ),
1811
+ displayedAssignment.status === "passed" && /* @__PURE__ */ jsx4(
1812
+ "button",
1813
+ {
1814
+ type: "button",
1815
+ onClick: () => handleChangeResult("failed"),
1816
+ disabled: isSubmitting,
1817
+ style: {
1818
+ flex: 1,
1819
+ paddingTop: 14,
1820
+ paddingBottom: 14,
1821
+ borderRadius: 12,
1822
+ textAlign: "center",
1823
+ backgroundColor: colors.redDark,
1824
+ border: `1px solid ${colors.red}`,
1825
+ cursor: isSubmitting ? "not-allowed" : "pointer",
1826
+ fontSize: 14,
1827
+ fontWeight: 600,
1828
+ color: "#fca5a5",
1829
+ opacity: isSubmitting ? 0.5 : 1
1830
+ },
1831
+ children: "Change to Fail"
1832
+ }
1833
+ ),
1834
+ displayedAssignment.status === "failed" && /* @__PURE__ */ jsx4(
1835
+ "button",
1836
+ {
1837
+ type: "button",
1838
+ onClick: () => handleChangeResult("passed"),
1839
+ disabled: isSubmitting,
1840
+ style: {
1841
+ flex: 1,
1842
+ paddingTop: 14,
1843
+ paddingBottom: 14,
1844
+ borderRadius: 12,
1845
+ textAlign: "center",
1846
+ backgroundColor: colors.greenDark,
1847
+ border: `1px solid ${colors.green}`,
1848
+ cursor: isSubmitting ? "not-allowed" : "pointer",
1849
+ fontSize: 14,
1850
+ fontWeight: 600,
1851
+ color: "#86efac",
1852
+ opacity: isSubmitting ? 0.5 : 1
1853
+ },
1854
+ children: "Change to Pass"
1855
+ }
1856
+ )
1857
+ ] })
1858
+ ] }) : /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 10, marginTop: 8 }, children: [
1577
1859
  /* @__PURE__ */ jsx4(
1578
1860
  "button",
1579
1861
  {
@@ -1792,10 +2074,11 @@ function TestDetailScreen({ testId, nav }) {
1792
2074
  import { useState as useState3, useMemo, useCallback as useCallback3 } from "react";
1793
2075
  import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1794
2076
  function TestListScreen({ nav }) {
1795
- const { assignments, currentAssignment, refreshAssignments, isLoading } = useBugBear();
2077
+ const { assignments, currentAssignment, refreshAssignments, dashboardUrl, isLoading } = useBugBear();
1796
2078
  const [filter, setFilter] = useState3("all");
1797
2079
  const [roleFilter, setRoleFilter] = useState3(null);
1798
2080
  const [trackFilter, setTrackFilter] = useState3(null);
2081
+ const [platformFilter, setPlatformFilter] = useState3("web");
1799
2082
  const [searchQuery, setSearchQuery] = useState3("");
1800
2083
  const [sortMode, setSortMode] = useState3("priority");
1801
2084
  const [collapsedFolders, setCollapsedFolders] = useState3(/* @__PURE__ */ new Set());
@@ -1873,6 +2156,7 @@ function TestListScreen({ nav }) {
1873
2156
  });
1874
2157
  }, []);
1875
2158
  const filterAssignment = useCallback3((a) => {
2159
+ if (platformFilter && a.testCase.platforms && !a.testCase.platforms.includes(platformFilter)) return false;
1876
2160
  if (roleFilter && a.testCase.role?.id !== roleFilter) return false;
1877
2161
  if (trackFilter && a.testCase.track?.id !== trackFilter) return false;
1878
2162
  if (searchQuery) {
@@ -1885,7 +2169,7 @@ function TestListScreen({ nav }) {
1885
2169
  if (filter === "done") return a.status === "passed";
1886
2170
  if (filter === "reopened") return a.status === "failed";
1887
2171
  return true;
1888
- }, [roleFilter, trackFilter, searchQuery, filter]);
2172
+ }, [platformFilter, roleFilter, trackFilter, searchQuery, filter]);
1889
2173
  const pendingCount = assignments.filter(
1890
2174
  (a) => a.status === "pending" || a.status === "in_progress"
1891
2175
  ).length;
@@ -2019,6 +2303,61 @@ function TestListScreen({ nav }) {
2019
2303
  }
2020
2304
  }
2021
2305
  ) }),
2306
+ /* @__PURE__ */ jsxs4("div", { style: { display: "flex", gap: 4, marginBottom: 8 }, children: [
2307
+ /* @__PURE__ */ jsx5(
2308
+ "button",
2309
+ {
2310
+ type: "button",
2311
+ onClick: () => setPlatformFilter(null),
2312
+ style: {
2313
+ padding: "3px 8px",
2314
+ borderRadius: 6,
2315
+ backgroundColor: !platformFilter ? colors.card : "transparent",
2316
+ border: !platformFilter ? `1px solid ${colors.border}` : "1px solid transparent",
2317
+ cursor: "pointer",
2318
+ fontSize: 11,
2319
+ color: !platformFilter ? colors.textPrimary : colors.textMuted,
2320
+ fontWeight: !platformFilter ? 600 : 400,
2321
+ whiteSpace: "nowrap"
2322
+ },
2323
+ children: "All"
2324
+ }
2325
+ ),
2326
+ [
2327
+ { key: "web", label: "Web", icon: "\u{1F310}" },
2328
+ { key: "ios", label: "iOS", icon: "\u{1F4F1}" },
2329
+ { key: "android", label: "Android", icon: "\u{1F916}" }
2330
+ ].map((p) => {
2331
+ const isActive = platformFilter === p.key;
2332
+ return /* @__PURE__ */ jsxs4(
2333
+ "button",
2334
+ {
2335
+ type: "button",
2336
+ onClick: () => setPlatformFilter(isActive ? null : p.key),
2337
+ style: {
2338
+ display: "flex",
2339
+ alignItems: "center",
2340
+ gap: 4,
2341
+ padding: "3px 8px",
2342
+ borderRadius: 6,
2343
+ backgroundColor: isActive ? colors.blue + "20" : "transparent",
2344
+ border: isActive ? `1px solid ${colors.blue}60` : "1px solid transparent",
2345
+ cursor: "pointer",
2346
+ fontSize: 11,
2347
+ color: isActive ? colors.blue : colors.textMuted,
2348
+ fontWeight: isActive ? 600 : 400,
2349
+ whiteSpace: "nowrap"
2350
+ },
2351
+ children: [
2352
+ p.icon,
2353
+ " ",
2354
+ p.label
2355
+ ]
2356
+ },
2357
+ p.key
2358
+ );
2359
+ })
2360
+ ] }),
2022
2361
  (availableTracks.length >= 2 || true) && /* @__PURE__ */ jsxs4("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 12 }, children: [
2023
2362
  availableTracks.length >= 2 && /* @__PURE__ */ jsxs4("div", { style: { display: "flex", gap: 4, flex: 1, overflow: "auto" }, children: [
2024
2363
  /* @__PURE__ */ jsx5(
@@ -2332,7 +2671,27 @@ function TestListScreen({ nav }) {
2332
2671
  ]
2333
2672
  }
2334
2673
  ),
2335
- /* @__PURE__ */ jsx5("div", { style: { display: "flex", justifyContent: "center", paddingTop: 12, paddingBottom: 8 }, children: /* @__PURE__ */ jsx5(
2674
+ dashboardUrl && /* @__PURE__ */ jsx5(
2675
+ "a",
2676
+ {
2677
+ href: `${dashboardUrl}/test-cases`,
2678
+ target: "_blank",
2679
+ rel: "noopener noreferrer",
2680
+ style: {
2681
+ display: "flex",
2682
+ alignItems: "center",
2683
+ justifyContent: "center",
2684
+ gap: 6,
2685
+ paddingTop: 12,
2686
+ fontSize: 13,
2687
+ fontWeight: 500,
2688
+ color: colors.blue,
2689
+ textDecoration: "none"
2690
+ },
2691
+ children: "\u{1F310} Manage on Dashboard \u2192"
2692
+ }
2693
+ ),
2694
+ /* @__PURE__ */ jsx5("div", { style: { display: "flex", justifyContent: "center", paddingTop: 8, paddingBottom: 8 }, children: /* @__PURE__ */ jsx5(
2336
2695
  "button",
2337
2696
  {
2338
2697
  type: "button",
@@ -2403,6 +2762,25 @@ function useImageAttachments(uploadFn, maxImages, bucket = "screenshots") {
2403
2762
  const pickFromCamera = useCallback4(() => {
2404
2763
  triggerFilePicker("environment");
2405
2764
  }, [triggerFilePicker]);
2765
+ const addFile = useCallback4((file) => {
2766
+ const id = `img-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
2767
+ const localUri = URL.createObjectURL(file);
2768
+ const name = file.name || `image-${id}.png`;
2769
+ setImages((prev) => {
2770
+ if (prev.length >= maxImages) return prev;
2771
+ return [...prev, { id, localUri, remoteUrl: null, name, status: "uploading" }];
2772
+ });
2773
+ uploadFn(file, bucket).then((url) => {
2774
+ setImages((prev) => prev.map(
2775
+ (img) => img.id === id ? { ...img, remoteUrl: url, status: url ? "done" : "error" } : img
2776
+ ));
2777
+ }).catch((err) => {
2778
+ console.error("BugBear: Image upload failed", err);
2779
+ setImages((prev) => prev.map(
2780
+ (img) => img.id === id ? { ...img, status: "error" } : img
2781
+ ));
2782
+ });
2783
+ }, [maxImages, uploadFn, bucket]);
2406
2784
  const removeImage = useCallback4((id) => {
2407
2785
  setImages((prev) => {
2408
2786
  const img = prev.find((i) => i.id === id);
@@ -2424,7 +2802,7 @@ function useImageAttachments(uploadFn, maxImages, bucket = "screenshots") {
2424
2802
  const getScreenshotUrls = useCallback4(() => {
2425
2803
  return images.filter((img) => img.status === "done" && img.remoteUrl).map((img) => img.remoteUrl);
2426
2804
  }, [images]);
2427
- return { images, pickFromGallery, pickFromCamera, removeImage, clear, isUploading, hasError, getAttachments, getScreenshotUrls };
2805
+ return { images, pickFromGallery, pickFromCamera, addFile, removeImage, clear, isUploading, hasError, getAttachments, getScreenshotUrls };
2428
2806
  }
2429
2807
 
2430
2808
  // src/widget/ImagePreviewStrip.tsx
@@ -2545,7 +2923,12 @@ function ImagePickerButtons({ images, maxImages, onPickGallery, onPickCamera, on
2545
2923
  maxImages
2546
2924
  ] })
2547
2925
  ] }),
2548
- /* @__PURE__ */ jsx7(ImagePreviewStrip, { images, onRemove })
2926
+ /* @__PURE__ */ jsx7(ImagePreviewStrip, { images, onRemove }),
2927
+ typeof navigator !== "undefined" && /* @__PURE__ */ jsxs6("span", { style: { fontSize: 11, color: colors.textDim, display: "block", marginTop: 4 }, children: [
2928
+ "Paste images with ",
2929
+ typeof navigator !== "undefined" && navigator.platform?.includes("Mac") ? "\u2318" : "Ctrl",
2930
+ "+V"
2931
+ ] })
2549
2932
  ] });
2550
2933
  }
2551
2934
 
@@ -2843,7 +3226,7 @@ var styles = {
2843
3226
  };
2844
3227
 
2845
3228
  // src/widget/screens/ReportScreen.tsx
2846
- import React6, { useState as useState6, useRef as useRef2 } from "react";
3229
+ import React6, { useState as useState6, useRef as useRef3, useEffect as useEffect6 } from "react";
2847
3230
 
2848
3231
  // src/widget/CategoryDropdown.tsx
2849
3232
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
@@ -2889,11 +3272,63 @@ function CategoryDropdown({ value, onChange, optional = true, disabled = false }
2889
3272
  );
2890
3273
  }
2891
3274
 
3275
+ // src/widget/useClipboardPaste.ts
3276
+ import { useEffect as useEffect5, useCallback as useCallback5 } from "react";
3277
+ function useClipboardPaste({
3278
+ containerRef,
3279
+ enabled,
3280
+ currentCount,
3281
+ maxImages,
3282
+ addFile
3283
+ }) {
3284
+ const handlePaste = useCallback5((event) => {
3285
+ if (!enabled || currentCount >= maxImages) return;
3286
+ const items = event.clipboardData?.items;
3287
+ if (!items) return;
3288
+ const imageFiles = [];
3289
+ for (let i = 0; i < items.length; i++) {
3290
+ const item = items[i];
3291
+ if (item.type.startsWith("image/")) {
3292
+ const file = item.getAsFile();
3293
+ if (file) imageFiles.push(file);
3294
+ }
3295
+ }
3296
+ if (imageFiles.length === 0) return;
3297
+ event.preventDefault();
3298
+ const remaining = maxImages - currentCount;
3299
+ for (const file of imageFiles.slice(0, remaining)) {
3300
+ addFile(file);
3301
+ }
3302
+ }, [enabled, currentCount, maxImages, addFile]);
3303
+ useEffect5(() => {
3304
+ const container = containerRef.current;
3305
+ if (!container || !enabled) return;
3306
+ container.addEventListener("paste", handlePaste);
3307
+ return () => container.removeEventListener("paste", handlePaste);
3308
+ }, [containerRef, enabled, handlePaste]);
3309
+ }
3310
+
2892
3311
  // src/widget/screens/ReportScreen.tsx
2893
3312
  import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2894
- function ReportScreen({ nav, prefill }) {
3313
+ function ReportScreen({ nav, prefill, autoCapture, onAutoCaptureConsumed }) {
2895
3314
  const { client, refreshAssignments, uploadImage } = useBugBear();
2896
3315
  const images = useImageAttachments(uploadImage, 5, "screenshots");
3316
+ const formRef = useRef3(null);
3317
+ const hasConsumedCapture = useRef3(false);
3318
+ useEffect6(() => {
3319
+ if (autoCapture && !hasConsumedCapture.current) {
3320
+ hasConsumedCapture.current = true;
3321
+ images.addFile(autoCapture);
3322
+ onAutoCaptureConsumed?.();
3323
+ }
3324
+ }, [autoCapture, onAutoCaptureConsumed]);
3325
+ useClipboardPaste({
3326
+ containerRef: formRef,
3327
+ enabled: true,
3328
+ currentCount: images.images.length,
3329
+ maxImages: 5,
3330
+ addFile: images.addFile
3331
+ });
2897
3332
  const [reportType, setReportType] = useState6(prefill?.type || "bug");
2898
3333
  const [severity, setSeverity] = useState6("medium");
2899
3334
  const [category, setCategory] = useState6(null);
@@ -2901,7 +3336,7 @@ function ReportScreen({ nav, prefill }) {
2901
3336
  const [affectedRoute, setAffectedRoute] = useState6("");
2902
3337
  const [submitting, setSubmitting] = useState6(false);
2903
3338
  const [error, setError] = useState6(null);
2904
- const submittingRef = useRef2(false);
3339
+ const submittingRef = useRef3(false);
2905
3340
  React6.useEffect(() => {
2906
3341
  if (reportType === "feedback" || reportType === "suggestion") {
2907
3342
  setCategory("other");
@@ -2909,7 +3344,7 @@ function ReportScreen({ nav, prefill }) {
2909
3344
  setCategory(null);
2910
3345
  }
2911
3346
  }, [reportType]);
2912
- const observedRoute = useRef2(
3347
+ const observedRoute = useRef3(
2913
3348
  typeof window !== "undefined" ? window.location.pathname : "unknown"
2914
3349
  );
2915
3350
  const isRetestFailure = prefill?.type === "test_fail";
@@ -2965,7 +3400,7 @@ function ReportScreen({ nav, prefill }) {
2965
3400
  { sev: "medium", color: "#eab308" },
2966
3401
  { sev: "low", color: "#6b7280" }
2967
3402
  ];
2968
- return /* @__PURE__ */ jsx10("div", { children: isRetestFailure ? /* @__PURE__ */ jsxs9(Fragment3, { children: [
3403
+ return /* @__PURE__ */ jsx10("div", { ref: formRef, tabIndex: -1, style: { outline: "none" }, children: isRetestFailure ? /* @__PURE__ */ jsxs9(Fragment3, { children: [
2969
3404
  /* @__PURE__ */ jsxs9("div", { style: styles2.retestBanner, children: [
2970
3405
  /* @__PURE__ */ jsx10("span", { style: { fontSize: 16 }, children: "\u{1F504}" }),
2971
3406
  /* @__PURE__ */ jsxs9("div", { children: [
@@ -3293,10 +3728,10 @@ var styles2 = {
3293
3728
  };
3294
3729
 
3295
3730
  // src/widget/screens/ReportSuccessScreen.tsx
3296
- import { useEffect as useEffect4 } from "react";
3731
+ import { useEffect as useEffect7 } from "react";
3297
3732
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
3298
3733
  function ReportSuccessScreen({ nav }) {
3299
- useEffect4(() => {
3734
+ useEffect7(() => {
3300
3735
  const timer = setTimeout(() => nav.reset(), 2e3);
3301
3736
  return () => clearTimeout(timer);
3302
3737
  }, [nav]);
@@ -3334,7 +3769,7 @@ var styles3 = {
3334
3769
  // src/widget/screens/MessageListScreen.tsx
3335
3770
  import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
3336
3771
  function MessageListScreen({ nav }) {
3337
- const { threads, unreadCount, refreshThreads, isLoading } = useBugBear();
3772
+ const { threads, unreadCount, refreshThreads, dashboardUrl, isLoading } = useBugBear();
3338
3773
  if (isLoading) return /* @__PURE__ */ jsx12(MessageListScreenSkeleton, {});
3339
3774
  return /* @__PURE__ */ jsxs11("div", { children: [
3340
3775
  /* @__PURE__ */ jsx12(
@@ -3523,6 +3958,26 @@ function MessageListScreen({ nav }) {
3523
3958
  },
3524
3959
  thread.id
3525
3960
  )) }),
3961
+ dashboardUrl && /* @__PURE__ */ jsx12(
3962
+ "a",
3963
+ {
3964
+ href: `${dashboardUrl}/discussions`,
3965
+ target: "_blank",
3966
+ rel: "noopener noreferrer",
3967
+ style: {
3968
+ display: "flex",
3969
+ alignItems: "center",
3970
+ justifyContent: "center",
3971
+ gap: 6,
3972
+ paddingTop: 12,
3973
+ fontSize: 13,
3974
+ fontWeight: 500,
3975
+ color: colors.blue,
3976
+ textDecoration: "none"
3977
+ },
3978
+ children: "\u{1F310} View on Dashboard \u2192"
3979
+ }
3980
+ ),
3526
3981
  /* @__PURE__ */ jsxs11(
3527
3982
  "div",
3528
3983
  {
@@ -3530,7 +3985,7 @@ function MessageListScreen({ nav }) {
3530
3985
  display: "flex",
3531
3986
  justifyContent: "space-between",
3532
3987
  alignItems: "center",
3533
- paddingTop: 12,
3988
+ paddingTop: 8,
3534
3989
  paddingLeft: 4,
3535
3990
  paddingRight: 4
3536
3991
  },
@@ -3566,7 +4021,7 @@ function MessageListScreen({ nav }) {
3566
4021
  }
3567
4022
 
3568
4023
  // src/widget/screens/ThreadDetailScreen.tsx
3569
- import { useState as useState7, useEffect as useEffect5 } from "react";
4024
+ import { useState as useState7, useEffect as useEffect8 } from "react";
3570
4025
  import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
3571
4026
  var inputStyle = {
3572
4027
  backgroundColor: "#27272a",
@@ -3588,7 +4043,7 @@ function ThreadDetailScreen({
3588
4043
  const [replyText, setReplyText] = useState7("");
3589
4044
  const [sending, setSending] = useState7(false);
3590
4045
  const [sendError, setSendError] = useState7(false);
3591
- useEffect5(() => {
4046
+ useEffect8(() => {
3592
4047
  let cancelled = false;
3593
4048
  setLoading(true);
3594
4049
  (async () => {
@@ -4013,7 +4468,7 @@ function ComposeMessageScreen({ nav }) {
4013
4468
  }
4014
4469
 
4015
4470
  // src/widget/screens/ProfileScreen.tsx
4016
- import { useState as useState9, useEffect as useEffect6 } from "react";
4471
+ import { useState as useState9, useEffect as useEffect9 } from "react";
4017
4472
  import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
4018
4473
  function ProfileScreen({ nav }) {
4019
4474
  const { testerInfo, assignments, updateTesterProfile, refreshTesterInfo } = useBugBear();
@@ -4026,7 +4481,7 @@ function ProfileScreen({ nav }) {
4026
4481
  const [saved, setSaved] = useState9(false);
4027
4482
  const [showDetails, setShowDetails] = useState9(false);
4028
4483
  const completedCount = assignments.filter((a) => a.status === "passed" || a.status === "failed").length;
4029
- useEffect6(() => {
4484
+ useEffect9(() => {
4030
4485
  if (testerInfo) {
4031
4486
  setName(testerInfo.name);
4032
4487
  setAdditionalEmails(testerInfo.additionalEmails || []);
@@ -4490,7 +4945,7 @@ var styles4 = {
4490
4945
  };
4491
4946
 
4492
4947
  // src/widget/screens/IssueListScreen.tsx
4493
- import { useState as useState10, useEffect as useEffect7 } from "react";
4948
+ import { useState as useState10, useEffect as useEffect10 } from "react";
4494
4949
  import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
4495
4950
  var CATEGORY_CONFIG = {
4496
4951
  open: { label: "Open Issues", accent: "#f97316", emptyIcon: "\u2705", emptyText: "No open issues" },
@@ -4508,7 +4963,7 @@ function IssueListScreen({ nav, category }) {
4508
4963
  const [issues, setIssues] = useState10([]);
4509
4964
  const [loading, setLoading] = useState10(true);
4510
4965
  const config = CATEGORY_CONFIG[category];
4511
- useEffect7(() => {
4966
+ useEffect10(() => {
4512
4967
  let cancelled = false;
4513
4968
  setLoading(true);
4514
4969
  (async () => {
@@ -4654,6 +5109,7 @@ var SEVERITY_CONFIG = {
4654
5109
  low: { label: "Low", color: "#71717a", bg: "#27272a" }
4655
5110
  };
4656
5111
  function IssueDetailScreen({ nav, issue }) {
5112
+ const { dashboardUrl } = useBugBear();
4657
5113
  const statusConfig = STATUS_LABELS[issue.status] || { label: issue.status, bg: "#27272a", color: "#a1a1aa" };
4658
5114
  const severityConfig = issue.severity ? SEVERITY_CONFIG[issue.severity] : null;
4659
5115
  return /* @__PURE__ */ jsxs16("div", { children: [
@@ -4771,7 +5227,29 @@ function IssueDetailScreen({ nav, issue }) {
4771
5227
  " \xB7 Updated ",
4772
5228
  formatRelativeTime(issue.updatedAt)
4773
5229
  ] })
4774
- ] })
5230
+ ] }),
5231
+ dashboardUrl && /* @__PURE__ */ jsx17(
5232
+ "a",
5233
+ {
5234
+ href: `${dashboardUrl}/reports`,
5235
+ target: "_blank",
5236
+ rel: "noopener noreferrer",
5237
+ style: {
5238
+ display: "flex",
5239
+ alignItems: "center",
5240
+ justifyContent: "center",
5241
+ gap: 6,
5242
+ marginTop: 12,
5243
+ padding: "10px 0",
5244
+ fontSize: 13,
5245
+ fontWeight: 500,
5246
+ color: colors.blue,
5247
+ textDecoration: "none",
5248
+ borderTop: `1px solid ${colors.border}`
5249
+ },
5250
+ children: "\u{1F310} View on Dashboard \u2192"
5251
+ }
5252
+ )
4775
5253
  ] });
4776
5254
  }
4777
5255
 
@@ -4827,11 +5305,12 @@ function BugBearPanel({
4827
5305
  const { shouldShowWidget, testerInfo, assignments, isLoading, unreadCount } = useBugBear();
4828
5306
  const { currentScreen, canGoBack, push, pop, replace, reset } = useNavigation();
4829
5307
  const [collapsed, setCollapsed] = useState11(defaultCollapsed);
5308
+ const autoCaptureRef = useRef4(null);
4830
5309
  const [panelPosition, setPanelPosition] = useState11(null);
4831
5310
  const [isDragging, setIsDragging] = useState11(false);
4832
- const dragStartRef = useRef3(null);
4833
- const panelRef = useRef3(null);
4834
- useEffect8(() => {
5311
+ const dragStartRef = useRef4(null);
5312
+ const panelRef = useRef4(null);
5313
+ useEffect11(() => {
4835
5314
  if (typeof window === "undefined") return;
4836
5315
  try {
4837
5316
  const saved = localStorage.getItem(STORAGE_KEY);
@@ -4845,7 +5324,7 @@ function BugBearPanel({
4845
5324
  setPanelPosition(getDefaultPosition(position));
4846
5325
  }
4847
5326
  }, [position]);
4848
- useEffect8(() => {
5327
+ useEffect11(() => {
4849
5328
  if (panelPosition && typeof window !== "undefined") {
4850
5329
  try {
4851
5330
  localStorage.setItem(STORAGE_KEY, JSON.stringify(panelPosition));
@@ -4853,7 +5332,7 @@ function BugBearPanel({
4853
5332
  }
4854
5333
  }
4855
5334
  }, [panelPosition]);
4856
- useEffect8(() => {
5335
+ useEffect11(() => {
4857
5336
  if (typeof window === "undefined") return;
4858
5337
  const handleResize = () => {
4859
5338
  setPanelPosition((prev) => prev ? clampPosition(prev) : getDefaultPosition(position));
@@ -4861,7 +5340,7 @@ function BugBearPanel({
4861
5340
  window.addEventListener("resize", handleResize);
4862
5341
  return () => window.removeEventListener("resize", handleResize);
4863
5342
  }, [position]);
4864
- const handleMouseDown = useCallback5((e) => {
5343
+ const handleMouseDown = useCallback6((e) => {
4865
5344
  if (!draggable || !panelPosition) return;
4866
5345
  const target = e.target;
4867
5346
  if (!target.closest("[data-drag-handle]")) return;
@@ -4874,7 +5353,7 @@ function BugBearPanel({
4874
5353
  panelY: panelPosition.y
4875
5354
  };
4876
5355
  }, [draggable, panelPosition]);
4877
- useEffect8(() => {
5356
+ useEffect11(() => {
4878
5357
  if (!isDragging) return;
4879
5358
  const handleMouseMove = (e) => {
4880
5359
  if (!dragStartRef.current) return;
@@ -4896,7 +5375,7 @@ function BugBearPanel({
4896
5375
  document.removeEventListener("mouseup", handleMouseUp);
4897
5376
  };
4898
5377
  }, [isDragging]);
4899
- const handleDoubleClick = useCallback5(() => {
5378
+ const handleDoubleClick = useCallback6(() => {
4900
5379
  if (!draggable) return;
4901
5380
  setPanelPosition(getDefaultPosition(position));
4902
5381
  try {
@@ -4904,6 +5383,14 @@ function BugBearPanel({
4904
5383
  } catch {
4905
5384
  }
4906
5385
  }, [draggable, position]);
5386
+ const handleReportBug = useCallback6(async () => {
5387
+ const file = await capturePageScreenshot({
5388
+ excludeElement: panelRef.current,
5389
+ timeout: 3e3
5390
+ });
5391
+ autoCaptureRef.current = file;
5392
+ push({ name: "REPORT", prefill: { type: "bug" } });
5393
+ }, [push]);
4907
5394
  if (isLoading || !shouldShowWidget) return null;
4908
5395
  if (!panelPosition) return null;
4909
5396
  const pendingCount = assignments.filter((a) => a.status === "pending" || a.status === "in_progress").length;
@@ -4940,13 +5427,12 @@ function BugBearPanel({
4940
5427
  };
4941
5428
  const handleClose = () => {
4942
5429
  setCollapsed(true);
4943
- reset();
4944
5430
  };
4945
5431
  const nav = { push, pop, replace, reset, canGoBack };
4946
5432
  const renderScreen = () => {
4947
5433
  switch (currentScreen.name) {
4948
5434
  case "HOME":
4949
- return /* @__PURE__ */ jsx18(HomeScreen, { nav });
5435
+ return /* @__PURE__ */ jsx18(HomeScreen, { nav, onReportBug: handleReportBug });
4950
5436
  case "TEST_DETAIL":
4951
5437
  return /* @__PURE__ */ jsx18(TestDetailScreen, { testId: currentScreen.testId, nav });
4952
5438
  case "TEST_LIST":
@@ -4954,7 +5440,17 @@ function BugBearPanel({
4954
5440
  case "TEST_FEEDBACK":
4955
5441
  return /* @__PURE__ */ jsx18(TestFeedbackScreen, { status: currentScreen.status, assignmentId: currentScreen.assignmentId, nav });
4956
5442
  case "REPORT":
4957
- return /* @__PURE__ */ jsx18(ReportScreen, { nav, prefill: currentScreen.prefill });
5443
+ return /* @__PURE__ */ jsx18(
5444
+ ReportScreen,
5445
+ {
5446
+ nav,
5447
+ prefill: currentScreen.prefill,
5448
+ autoCapture: autoCaptureRef.current,
5449
+ onAutoCaptureConsumed: () => {
5450
+ autoCaptureRef.current = null;
5451
+ }
5452
+ }
5453
+ );
4958
5454
  case "REPORT_SUCCESS":
4959
5455
  return /* @__PURE__ */ jsx18(ReportSuccessScreen, { nav });
4960
5456
  case "MESSAGE_LIST":