@bbearai/react 0.5.1 → 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 +288 -81
  2. package/dist/index.mjs +262 -55
  3. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -343,11 +343,80 @@ function BugBearProvider({ config, children, enabled = true }) {
343
343
  }
344
344
 
345
345
  // src/BugBearPanel.tsx
346
- var import_react14 = require("react");
346
+ var import_react15 = require("react");
347
347
  var import_react_dom = require("react-dom");
348
348
 
349
349
  // src/widget/navigation.ts
350
350
  var import_react2 = require("react");
351
+ var NAV_STORAGE_KEY = "bugbear-nav-screen";
352
+ function serializeScreen(screen) {
353
+ switch (screen.name) {
354
+ case "HOME":
355
+ case "TEST_LIST":
356
+ case "MESSAGE_LIST":
357
+ case "COMPOSE_MESSAGE":
358
+ case "PROFILE":
359
+ case "REPORT_SUCCESS":
360
+ return JSON.stringify({ name: screen.name });
361
+ case "TEST_DETAIL":
362
+ return JSON.stringify({ name: screen.name, testId: screen.testId });
363
+ case "REPORT":
364
+ return JSON.stringify({ name: screen.name });
365
+ case "ISSUE_LIST":
366
+ return JSON.stringify({ name: screen.name, category: screen.category });
367
+ // Complex screens — save their parent list instead
368
+ case "THREAD_DETAIL":
369
+ return JSON.stringify({ name: "MESSAGE_LIST" });
370
+ case "ISSUE_DETAIL":
371
+ return JSON.stringify({ name: "ISSUE_LIST", category: "open" });
372
+ case "TEST_FEEDBACK":
373
+ return JSON.stringify({ name: "TEST_LIST" });
374
+ default:
375
+ return null;
376
+ }
377
+ }
378
+ function deserializeScreen(json) {
379
+ try {
380
+ const parsed = JSON.parse(json);
381
+ if (!parsed || !parsed.name) return null;
382
+ switch (parsed.name) {
383
+ case "HOME":
384
+ return { name: "HOME" };
385
+ case "TEST_LIST":
386
+ return { name: "TEST_LIST" };
387
+ case "MESSAGE_LIST":
388
+ return { name: "MESSAGE_LIST" };
389
+ case "COMPOSE_MESSAGE":
390
+ return { name: "COMPOSE_MESSAGE" };
391
+ case "PROFILE":
392
+ return { name: "PROFILE" };
393
+ case "REPORT_SUCCESS":
394
+ return { name: "REPORT_SUCCESS" };
395
+ case "TEST_DETAIL":
396
+ return { name: "TEST_DETAIL", testId: parsed.testId };
397
+ case "REPORT":
398
+ return { name: "REPORT" };
399
+ case "ISSUE_LIST":
400
+ return { name: "ISSUE_LIST", category: parsed.category || "open" };
401
+ default:
402
+ return null;
403
+ }
404
+ } catch {
405
+ return null;
406
+ }
407
+ }
408
+ function getInitialScreen() {
409
+ if (typeof window === "undefined") return { name: "HOME" };
410
+ try {
411
+ const saved = localStorage.getItem(NAV_STORAGE_KEY);
412
+ if (saved) {
413
+ const screen = deserializeScreen(saved);
414
+ if (screen) return screen;
415
+ }
416
+ } catch {
417
+ }
418
+ return { name: "HOME" };
419
+ }
351
420
  function navReducer(state, action) {
352
421
  switch (action.type) {
353
422
  case "PUSH":
@@ -363,9 +432,21 @@ function navReducer(state, action) {
363
432
  }
364
433
  }
365
434
  function useNavigation() {
366
- const [state, dispatch] = (0, import_react2.useReducer)(navReducer, { stack: [{ name: "HOME" }] });
435
+ const initialScreen = (0, import_react2.useRef)(getInitialScreen());
436
+ const [state, dispatch] = (0, import_react2.useReducer)(navReducer, { stack: [initialScreen.current] });
437
+ const currentScreen = state.stack[state.stack.length - 1];
438
+ (0, import_react2.useEffect)(() => {
439
+ if (typeof window === "undefined") return;
440
+ try {
441
+ const serialized = serializeScreen(currentScreen);
442
+ if (serialized) {
443
+ localStorage.setItem(NAV_STORAGE_KEY, serialized);
444
+ }
445
+ } catch {
446
+ }
447
+ }, [currentScreen]);
367
448
  return {
368
- currentScreen: state.stack[state.stack.length - 1],
449
+ currentScreen,
369
450
  canGoBack: state.stack.length > 1,
370
451
  push: (screen) => dispatch({ type: "PUSH", screen }),
371
452
  pop: () => dispatch({ type: "POP" }),
@@ -459,6 +540,36 @@ function getThreadTypeIcon(type) {
459
540
  }
460
541
  }
461
542
 
543
+ // src/widget/useScreenCapture.ts
544
+ var import_modern_screenshot = require("modern-screenshot");
545
+ async function capturePageScreenshot(options = {}) {
546
+ const { excludeElement, timeout = 3e3 } = options;
547
+ try {
548
+ const blob = await Promise.race([
549
+ (0, import_modern_screenshot.domToBlob)(document.documentElement, {
550
+ filter: (node) => {
551
+ if (excludeElement && node === excludeElement) return false;
552
+ return true;
553
+ },
554
+ scale: 1,
555
+ quality: 0.85
556
+ }),
557
+ new Promise(
558
+ (_, reject) => setTimeout(() => reject(new Error("Screenshot capture timed out")), timeout)
559
+ )
560
+ ]);
561
+ if (!blob) return null;
562
+ return new File(
563
+ [blob],
564
+ `screenshot-auto-${Date.now()}.png`,
565
+ { type: "image/png" }
566
+ );
567
+ } catch (err) {
568
+ console.warn("BugBear: Auto-capture failed, user can attach manually", err);
569
+ return null;
570
+ }
571
+ }
572
+
462
573
  // src/widget/screens/HomeScreen.tsx
463
574
  var import_react3 = require("react");
464
575
 
@@ -601,7 +712,7 @@ function MessageListScreenSkeleton() {
601
712
 
602
713
  // src/widget/screens/HomeScreen.tsx
603
714
  var import_jsx_runtime3 = require("react/jsx-runtime");
604
- function HomeScreen({ nav }) {
715
+ function HomeScreen({ nav, onReportBug }) {
605
716
  const { assignments, unreadCount, threads, refreshAssignments, refreshThreads, issueCounts, refreshIssueCounts, dashboardUrl, isLoading } = useBugBear();
606
717
  (0, import_react3.useEffect)(() => {
607
718
  refreshAssignments();
@@ -790,9 +901,11 @@ function HomeScreen({ nav }) {
790
901
  {
791
902
  role: "button",
792
903
  tabIndex: 0,
793
- onClick: () => nav.push({ name: "REPORT", prefill: { type: "bug" } }),
904
+ onClick: () => onReportBug ? onReportBug() : nav.push({ name: "REPORT", prefill: { type: "bug" } }),
794
905
  onKeyDown: (e) => {
795
- if (e.key === "Enter" || e.key === " ") nav.push({ name: "REPORT", prefill: { type: "bug" } });
906
+ if (e.key === "Enter" || e.key === " ") {
907
+ onReportBug ? onReportBug() : nav.push({ name: "REPORT", prefill: { type: "bug" } });
908
+ }
796
909
  },
797
910
  style: {
798
911
  backgroundColor: colors.card,
@@ -1662,6 +1775,11 @@ function TestDetailScreen({ testId, nav }) {
1662
1775
  ]
1663
1776
  }
1664
1777
  ),
1778
+ testCase.track && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontSize: 12, color: colors.textSecondary, marginBottom: 6 }, children: [
1779
+ testCase.track.icon,
1780
+ " ",
1781
+ testCase.track.name
1782
+ ] }),
1665
1783
  testCase.description && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 13, color: colors.textSecondary, lineHeight: "18px" }, children: testCase.description }),
1666
1784
  testCase.group && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { fontSize: 12, color: colors.textMuted }, children: [
1667
1785
  "\u{1F4C1} ",
@@ -2017,13 +2135,6 @@ function TestListScreen({ nav }) {
2017
2135
  }
2018
2136
  return Array.from(trackMap.values());
2019
2137
  }, [assignments]);
2020
- const availablePlatforms = (0, import_react5.useMemo)(() => {
2021
- const set = /* @__PURE__ */ new Set();
2022
- for (const a of assignments) {
2023
- if (a.testCase.platforms) a.testCase.platforms.forEach((p) => set.add(p));
2024
- }
2025
- return Array.from(set).sort();
2026
- }, [assignments]);
2027
2138
  const selectedRole = availableRoles.find((r) => r.id === roleFilter);
2028
2139
  const groupedAssignments = (0, import_react5.useMemo)(() => {
2029
2140
  const groups = /* @__PURE__ */ new Map();
@@ -2231,7 +2342,7 @@ function TestListScreen({ nav }) {
2231
2342
  }
2232
2343
  }
2233
2344
  ) }),
2234
- availablePlatforms.length >= 2 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", gap: 4, marginBottom: 8 }, children: [
2345
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", gap: 4, marginBottom: 8 }, children: [
2235
2346
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2236
2347
  "button",
2237
2348
  {
@@ -2248,18 +2359,20 @@ function TestListScreen({ nav }) {
2248
2359
  fontWeight: !platformFilter ? 600 : 400,
2249
2360
  whiteSpace: "nowrap"
2250
2361
  },
2251
- children: "All Platforms"
2362
+ children: "All"
2252
2363
  }
2253
2364
  ),
2254
- availablePlatforms.map((p) => {
2255
- const isActive = platformFilter === p;
2256
- const label = p === "ios" ? "iOS" : p === "android" ? "Android" : p === "web" ? "Web" : p;
2257
- const icon = p === "ios" ? "\u{1F4F1}" : p === "android" ? "\u{1F916}" : p === "web" ? "\u{1F310}" : "\u{1F4CB}";
2365
+ [
2366
+ { key: "web", label: "Web", icon: "\u{1F310}" },
2367
+ { key: "ios", label: "iOS", icon: "\u{1F4F1}" },
2368
+ { key: "android", label: "Android", icon: "\u{1F916}" }
2369
+ ].map((p) => {
2370
+ const isActive = platformFilter === p.key;
2258
2371
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2259
2372
  "button",
2260
2373
  {
2261
2374
  type: "button",
2262
- onClick: () => setPlatformFilter(isActive ? null : p),
2375
+ onClick: () => setPlatformFilter(isActive ? null : p.key),
2263
2376
  style: {
2264
2377
  display: "flex",
2265
2378
  alignItems: "center",
@@ -2275,12 +2388,12 @@ function TestListScreen({ nav }) {
2275
2388
  whiteSpace: "nowrap"
2276
2389
  },
2277
2390
  children: [
2278
- icon,
2391
+ p.icon,
2279
2392
  " ",
2280
- label
2393
+ p.label
2281
2394
  ]
2282
2395
  },
2283
- p
2396
+ p.key
2284
2397
  );
2285
2398
  })
2286
2399
  ] }),
@@ -2688,6 +2801,25 @@ function useImageAttachments(uploadFn, maxImages, bucket = "screenshots") {
2688
2801
  const pickFromCamera = (0, import_react6.useCallback)(() => {
2689
2802
  triggerFilePicker("environment");
2690
2803
  }, [triggerFilePicker]);
2804
+ const addFile = (0, import_react6.useCallback)((file) => {
2805
+ const id = `img-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
2806
+ const localUri = URL.createObjectURL(file);
2807
+ const name = file.name || `image-${id}.png`;
2808
+ setImages((prev) => {
2809
+ if (prev.length >= maxImages) return prev;
2810
+ return [...prev, { id, localUri, remoteUrl: null, name, status: "uploading" }];
2811
+ });
2812
+ uploadFn(file, bucket).then((url) => {
2813
+ setImages((prev) => prev.map(
2814
+ (img) => img.id === id ? { ...img, remoteUrl: url, status: url ? "done" : "error" } : img
2815
+ ));
2816
+ }).catch((err) => {
2817
+ console.error("BugBear: Image upload failed", err);
2818
+ setImages((prev) => prev.map(
2819
+ (img) => img.id === id ? { ...img, status: "error" } : img
2820
+ ));
2821
+ });
2822
+ }, [maxImages, uploadFn, bucket]);
2691
2823
  const removeImage = (0, import_react6.useCallback)((id) => {
2692
2824
  setImages((prev) => {
2693
2825
  const img = prev.find((i) => i.id === id);
@@ -2709,7 +2841,7 @@ function useImageAttachments(uploadFn, maxImages, bucket = "screenshots") {
2709
2841
  const getScreenshotUrls = (0, import_react6.useCallback)(() => {
2710
2842
  return images.filter((img) => img.status === "done" && img.remoteUrl).map((img) => img.remoteUrl);
2711
2843
  }, [images]);
2712
- return { images, pickFromGallery, pickFromCamera, removeImage, clear, isUploading, hasError, getAttachments, getScreenshotUrls };
2844
+ return { images, pickFromGallery, pickFromCamera, addFile, removeImage, clear, isUploading, hasError, getAttachments, getScreenshotUrls };
2713
2845
  }
2714
2846
 
2715
2847
  // src/widget/ImagePreviewStrip.tsx
@@ -2830,7 +2962,12 @@ function ImagePickerButtons({ images, maxImages, onPickGallery, onPickCamera, on
2830
2962
  maxImages
2831
2963
  ] })
2832
2964
  ] }),
2833
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ImagePreviewStrip, { images, onRemove })
2965
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ImagePreviewStrip, { images, onRemove }),
2966
+ typeof navigator !== "undefined" && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { style: { fontSize: 11, color: colors.textDim, display: "block", marginTop: 4 }, children: [
2967
+ "Paste images with ",
2968
+ typeof navigator !== "undefined" && navigator.platform?.includes("Mac") ? "\u2318" : "Ctrl",
2969
+ "+V"
2970
+ ] })
2834
2971
  ] });
2835
2972
  }
2836
2973
 
@@ -3128,7 +3265,7 @@ var styles = {
3128
3265
  };
3129
3266
 
3130
3267
  // src/widget/screens/ReportScreen.tsx
3131
- var import_react8 = __toESM(require("react"));
3268
+ var import_react9 = __toESM(require("react"));
3132
3269
 
3133
3270
  // src/widget/CategoryDropdown.tsx
3134
3271
  var import_jsx_runtime9 = require("react/jsx-runtime");
@@ -3174,27 +3311,79 @@ function CategoryDropdown({ value, onChange, optional = true, disabled = false }
3174
3311
  );
3175
3312
  }
3176
3313
 
3314
+ // src/widget/useClipboardPaste.ts
3315
+ var import_react8 = require("react");
3316
+ function useClipboardPaste({
3317
+ containerRef,
3318
+ enabled,
3319
+ currentCount,
3320
+ maxImages,
3321
+ addFile
3322
+ }) {
3323
+ const handlePaste = (0, import_react8.useCallback)((event) => {
3324
+ if (!enabled || currentCount >= maxImages) return;
3325
+ const items = event.clipboardData?.items;
3326
+ if (!items) return;
3327
+ const imageFiles = [];
3328
+ for (let i = 0; i < items.length; i++) {
3329
+ const item = items[i];
3330
+ if (item.type.startsWith("image/")) {
3331
+ const file = item.getAsFile();
3332
+ if (file) imageFiles.push(file);
3333
+ }
3334
+ }
3335
+ if (imageFiles.length === 0) return;
3336
+ event.preventDefault();
3337
+ const remaining = maxImages - currentCount;
3338
+ for (const file of imageFiles.slice(0, remaining)) {
3339
+ addFile(file);
3340
+ }
3341
+ }, [enabled, currentCount, maxImages, addFile]);
3342
+ (0, import_react8.useEffect)(() => {
3343
+ const container = containerRef.current;
3344
+ if (!container || !enabled) return;
3345
+ container.addEventListener("paste", handlePaste);
3346
+ return () => container.removeEventListener("paste", handlePaste);
3347
+ }, [containerRef, enabled, handlePaste]);
3348
+ }
3349
+
3177
3350
  // src/widget/screens/ReportScreen.tsx
3178
3351
  var import_jsx_runtime10 = require("react/jsx-runtime");
3179
- function ReportScreen({ nav, prefill }) {
3352
+ function ReportScreen({ nav, prefill, autoCapture, onAutoCaptureConsumed }) {
3180
3353
  const { client, refreshAssignments, uploadImage } = useBugBear();
3181
3354
  const images = useImageAttachments(uploadImage, 5, "screenshots");
3182
- const [reportType, setReportType] = (0, import_react8.useState)(prefill?.type || "bug");
3183
- const [severity, setSeverity] = (0, import_react8.useState)("medium");
3184
- const [category, setCategory] = (0, import_react8.useState)(null);
3185
- const [description, setDescription] = (0, import_react8.useState)("");
3186
- const [affectedRoute, setAffectedRoute] = (0, import_react8.useState)("");
3187
- const [submitting, setSubmitting] = (0, import_react8.useState)(false);
3188
- const [error, setError] = (0, import_react8.useState)(null);
3189
- const submittingRef = (0, import_react8.useRef)(false);
3190
- import_react8.default.useEffect(() => {
3355
+ const formRef = (0, import_react9.useRef)(null);
3356
+ const hasConsumedCapture = (0, import_react9.useRef)(false);
3357
+ (0, import_react9.useEffect)(() => {
3358
+ if (autoCapture && !hasConsumedCapture.current) {
3359
+ hasConsumedCapture.current = true;
3360
+ images.addFile(autoCapture);
3361
+ onAutoCaptureConsumed?.();
3362
+ }
3363
+ }, [autoCapture, onAutoCaptureConsumed]);
3364
+ useClipboardPaste({
3365
+ containerRef: formRef,
3366
+ enabled: true,
3367
+ currentCount: images.images.length,
3368
+ maxImages: 5,
3369
+ addFile: images.addFile
3370
+ });
3371
+ const [reportType, setReportType] = (0, import_react9.useState)(prefill?.type || "bug");
3372
+ const [severity, setSeverity] = (0, import_react9.useState)("medium");
3373
+ const [category, setCategory] = (0, import_react9.useState)(null);
3374
+ const [description, setDescription] = (0, import_react9.useState)("");
3375
+ const [affectedRoute, setAffectedRoute] = (0, import_react9.useState)("");
3376
+ const [submitting, setSubmitting] = (0, import_react9.useState)(false);
3377
+ const [error, setError] = (0, import_react9.useState)(null);
3378
+ const submittingRef = (0, import_react9.useRef)(false);
3379
+ import_react9.default.useEffect(() => {
3191
3380
  if (reportType === "feedback" || reportType === "suggestion") {
3192
3381
  setCategory("other");
3193
3382
  } else {
3194
3383
  setCategory(null);
3195
3384
  }
3196
3385
  }, [reportType]);
3197
- const observedRoute = (0, import_react8.useRef)(
3386
+ const observedRoute = (0, import_react9.useRef)(
3198
3387
  typeof window !== "undefined" ? window.location.pathname : "unknown"
3199
3388
  );
3200
3389
  const isRetestFailure = prefill?.type === "test_fail";
@@ -3250,7 +3439,7 @@ function ReportScreen({ nav, prefill }) {
3250
3439
  { sev: "medium", color: "#eab308" },
3251
3440
  { sev: "low", color: "#6b7280" }
3252
3441
  ];
3253
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { children: isRetestFailure ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
3442
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { ref: formRef, tabIndex: -1, style: { outline: "none" }, children: isRetestFailure ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
3254
3443
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: styles2.retestBanner, children: [
3255
3444
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { fontSize: 16 }, children: "\u{1F504}" }),
3256
3445
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { children: [
@@ -3578,10 +3767,10 @@ var styles2 = {
3578
3767
  };
3579
3768
 
3580
3769
  // src/widget/screens/ReportSuccessScreen.tsx
3581
- var import_react9 = require("react");
3770
+ var import_react10 = require("react");
3582
3771
  var import_jsx_runtime11 = require("react/jsx-runtime");
3583
3772
  function ReportSuccessScreen({ nav }) {
3584
- (0, import_react9.useEffect)(() => {
3773
+ (0, import_react10.useEffect)(() => {
3585
3774
  const timer = setTimeout(() => nav.reset(), 2e3);
3586
3775
  return () => clearTimeout(timer);
3587
3776
  }, [nav]);
@@ -3871,7 +4060,7 @@ function MessageListScreen({ nav }) {
3871
4060
  }
3872
4061
 
3873
4062
  // src/widget/screens/ThreadDetailScreen.tsx
3874
- var import_react10 = require("react");
4063
+ var import_react11 = require("react");
3875
4064
  var import_jsx_runtime13 = require("react/jsx-runtime");
3876
4065
  var inputStyle = {
3877
4066
  backgroundColor: "#27272a",
@@ -3888,12 +4077,12 @@ function ThreadDetailScreen({
3888
4077
  }) {
3889
4078
  const { getThreadMessages, sendMessage, markAsRead, uploadImage } = useBugBear();
3890
4079
  const replyImages = useImageAttachments(uploadImage, 3, "discussion-attachments");
3891
- const [messages, setMessages] = (0, import_react10.useState)([]);
3892
- const [loading, setLoading] = (0, import_react10.useState)(true);
3893
- const [replyText, setReplyText] = (0, import_react10.useState)("");
3894
- const [sending, setSending] = (0, import_react10.useState)(false);
3895
- const [sendError, setSendError] = (0, import_react10.useState)(false);
3896
- (0, import_react10.useEffect)(() => {
4080
+ const [messages, setMessages] = (0, import_react11.useState)([]);
4081
+ const [loading, setLoading] = (0, import_react11.useState)(true);
4082
+ const [replyText, setReplyText] = (0, import_react11.useState)("");
4083
+ const [sending, setSending] = (0, import_react11.useState)(false);
4084
+ const [sendError, setSendError] = (0, import_react11.useState)(false);
4085
+ (0, import_react11.useEffect)(() => {
3897
4086
  let cancelled = false;
3898
4087
  setLoading(true);
3899
4088
  (async () => {
@@ -4160,7 +4349,7 @@ function ThreadDetailScreen({
4160
4349
  }
4161
4350
 
4162
4351
  // src/widget/screens/ComposeMessageScreen.tsx
4163
- var import_react11 = require("react");
4352
+ var import_react12 = require("react");
4164
4353
  var import_jsx_runtime14 = require("react/jsx-runtime");
4165
4354
  var inputStyle2 = {
4166
4355
  backgroundColor: "#27272a",
@@ -4174,9 +4363,9 @@ var inputStyle2 = {
4174
4363
  function ComposeMessageScreen({ nav }) {
4175
4364
  const { createThread, uploadImage } = useBugBear();
4176
4365
  const images = useImageAttachments(uploadImage, 3, "discussion-attachments");
4177
- const [subject, setSubject] = (0, import_react11.useState)("");
4178
- const [message, setMessage] = (0, import_react11.useState)("");
4179
- const [sending, setSending] = (0, import_react11.useState)(false);
4366
+ const [subject, setSubject] = (0, import_react12.useState)("");
4367
+ const [message, setMessage] = (0, import_react12.useState)("");
4368
+ const [sending, setSending] = (0, import_react12.useState)(false);
4180
4369
  const canSend = subject.trim().length > 0 && message.trim().length > 0 && !sending && !images.isUploading;
4181
4370
  const handleSend = async () => {
4182
4371
  if (!canSend) return;
@@ -4318,20 +4507,20 @@ function ComposeMessageScreen({ nav }) {
4318
4507
  }
4319
4508
 
4320
4509
  // src/widget/screens/ProfileScreen.tsx
4321
- var import_react12 = require("react");
4510
+ var import_react13 = require("react");
4322
4511
  var import_jsx_runtime15 = require("react/jsx-runtime");
4323
4512
  function ProfileScreen({ nav }) {
4324
4513
  const { testerInfo, assignments, updateTesterProfile, refreshTesterInfo } = useBugBear();
4325
- const [editing, setEditing] = (0, import_react12.useState)(false);
4326
- const [name, setName] = (0, import_react12.useState)(testerInfo?.name || "");
4327
- const [additionalEmails, setAdditionalEmails] = (0, import_react12.useState)(testerInfo?.additionalEmails || []);
4328
- const [newEmailInput, setNewEmailInput] = (0, import_react12.useState)("");
4329
- const [platforms, setPlatforms] = (0, import_react12.useState)(testerInfo?.platforms || []);
4330
- const [saving, setSaving] = (0, import_react12.useState)(false);
4331
- const [saved, setSaved] = (0, import_react12.useState)(false);
4332
- const [showDetails, setShowDetails] = (0, import_react12.useState)(false);
4514
+ const [editing, setEditing] = (0, import_react13.useState)(false);
4515
+ const [name, setName] = (0, import_react13.useState)(testerInfo?.name || "");
4516
+ const [additionalEmails, setAdditionalEmails] = (0, import_react13.useState)(testerInfo?.additionalEmails || []);
4517
+ const [newEmailInput, setNewEmailInput] = (0, import_react13.useState)("");
4518
+ const [platforms, setPlatforms] = (0, import_react13.useState)(testerInfo?.platforms || []);
4519
+ const [saving, setSaving] = (0, import_react13.useState)(false);
4520
+ const [saved, setSaved] = (0, import_react13.useState)(false);
4521
+ const [showDetails, setShowDetails] = (0, import_react13.useState)(false);
4333
4522
  const completedCount = assignments.filter((a) => a.status === "passed" || a.status === "failed").length;
4334
- (0, import_react12.useEffect)(() => {
4523
+ (0, import_react13.useEffect)(() => {
4335
4524
  if (testerInfo) {
4336
4525
  setName(testerInfo.name);
4337
4526
  setAdditionalEmails(testerInfo.additionalEmails || []);
@@ -4795,7 +4984,7 @@ var styles4 = {
4795
4984
  };
4796
4985
 
4797
4986
  // src/widget/screens/IssueListScreen.tsx
4798
- var import_react13 = require("react");
4987
+ var import_react14 = require("react");
4799
4988
  var import_jsx_runtime16 = require("react/jsx-runtime");
4800
4989
  var CATEGORY_CONFIG = {
4801
4990
  open: { label: "Open Issues", accent: "#f97316", emptyIcon: "\u2705", emptyText: "No open issues" },
@@ -4810,10 +4999,10 @@ var SEVERITY_COLORS = {
4810
4999
  };
4811
5000
  function IssueListScreen({ nav, category }) {
4812
5001
  const { client } = useBugBear();
4813
- const [issues, setIssues] = (0, import_react13.useState)([]);
4814
- const [loading, setLoading] = (0, import_react13.useState)(true);
5002
+ const [issues, setIssues] = (0, import_react14.useState)([]);
5003
+ const [loading, setLoading] = (0, import_react14.useState)(true);
4815
5004
  const config = CATEGORY_CONFIG[category];
4816
- (0, import_react13.useEffect)(() => {
5005
+ (0, import_react14.useEffect)(() => {
4817
5006
  let cancelled = false;
4818
5007
  setLoading(true);
4819
5008
  (async () => {
@@ -5154,12 +5343,13 @@ function BugBearPanel({
5154
5343
  }) {
5155
5344
  const { shouldShowWidget, testerInfo, assignments, isLoading, unreadCount } = useBugBear();
5156
5345
  const { currentScreen, canGoBack, push, pop, replace, reset } = useNavigation();
5157
- const [collapsed, setCollapsed] = (0, import_react14.useState)(defaultCollapsed);
5158
- const [panelPosition, setPanelPosition] = (0, import_react14.useState)(null);
5159
- const [isDragging, setIsDragging] = (0, import_react14.useState)(false);
5160
- const dragStartRef = (0, import_react14.useRef)(null);
5161
- const panelRef = (0, import_react14.useRef)(null);
5162
- (0, import_react14.useEffect)(() => {
5346
+ const [collapsed, setCollapsed] = (0, import_react15.useState)(defaultCollapsed);
5347
+ const autoCaptureRef = (0, import_react15.useRef)(null);
5348
+ const [panelPosition, setPanelPosition] = (0, import_react15.useState)(null);
5349
+ const [isDragging, setIsDragging] = (0, import_react15.useState)(false);
5350
+ const dragStartRef = (0, import_react15.useRef)(null);
5351
+ const panelRef = (0, import_react15.useRef)(null);
5352
+ (0, import_react15.useEffect)(() => {
5163
5353
  if (typeof window === "undefined") return;
5164
5354
  try {
5165
5355
  const saved = localStorage.getItem(STORAGE_KEY);
@@ -5173,7 +5363,7 @@ function BugBearPanel({
5173
5363
  setPanelPosition(getDefaultPosition(position));
5174
5364
  }
5175
5365
  }, [position]);
5176
- (0, import_react14.useEffect)(() => {
5366
+ (0, import_react15.useEffect)(() => {
5177
5367
  if (panelPosition && typeof window !== "undefined") {
5178
5368
  try {
5179
5369
  localStorage.setItem(STORAGE_KEY, JSON.stringify(panelPosition));
@@ -5181,7 +5371,7 @@ function BugBearPanel({
5181
5371
  }
5182
5372
  }
5183
5373
  }, [panelPosition]);
5184
- (0, import_react14.useEffect)(() => {
5374
+ (0, import_react15.useEffect)(() => {
5185
5375
  if (typeof window === "undefined") return;
5186
5376
  const handleResize = () => {
5187
5377
  setPanelPosition((prev) => prev ? clampPosition(prev) : getDefaultPosition(position));
@@ -5189,7 +5379,7 @@ function BugBearPanel({
5189
5379
  window.addEventListener("resize", handleResize);
5190
5380
  return () => window.removeEventListener("resize", handleResize);
5191
5381
  }, [position]);
5192
- const handleMouseDown = (0, import_react14.useCallback)((e) => {
5382
+ const handleMouseDown = (0, import_react15.useCallback)((e) => {
5193
5383
  if (!draggable || !panelPosition) return;
5194
5384
  const target = e.target;
5195
5385
  if (!target.closest("[data-drag-handle]")) return;
@@ -5202,7 +5392,7 @@ function BugBearPanel({
5202
5392
  panelY: panelPosition.y
5203
5393
  };
5204
5394
  }, [draggable, panelPosition]);
5205
- (0, import_react14.useEffect)(() => {
5395
+ (0, import_react15.useEffect)(() => {
5206
5396
  if (!isDragging) return;
5207
5397
  const handleMouseMove = (e) => {
5208
5398
  if (!dragStartRef.current) return;
@@ -5224,7 +5414,7 @@ function BugBearPanel({
5224
5414
  document.removeEventListener("mouseup", handleMouseUp);
5225
5415
  };
5226
5416
  }, [isDragging]);
5227
- const handleDoubleClick = (0, import_react14.useCallback)(() => {
5417
+ const handleDoubleClick = (0, import_react15.useCallback)(() => {
5228
5418
  if (!draggable) return;
5229
5419
  setPanelPosition(getDefaultPosition(position));
5230
5420
  try {
@@ -5232,6 +5422,14 @@ function BugBearPanel({
5232
5422
  } catch {
5233
5423
  }
5234
5424
  }, [draggable, position]);
5425
+ const handleReportBug = (0, import_react15.useCallback)(async () => {
5426
+ const file = await capturePageScreenshot({
5427
+ excludeElement: panelRef.current,
5428
+ timeout: 3e3
5429
+ });
5430
+ autoCaptureRef.current = file;
5431
+ push({ name: "REPORT", prefill: { type: "bug" } });
5432
+ }, [push]);
5235
5433
  if (isLoading || !shouldShowWidget) return null;
5236
5434
  if (!panelPosition) return null;
5237
5435
  const pendingCount = assignments.filter((a) => a.status === "pending" || a.status === "in_progress").length;
@@ -5268,13 +5466,12 @@ function BugBearPanel({
5268
5466
  };
5269
5467
  const handleClose = () => {
5270
5468
  setCollapsed(true);
5271
- reset();
5272
5469
  };
5273
5470
  const nav = { push, pop, replace, reset, canGoBack };
5274
5471
  const renderScreen = () => {
5275
5472
  switch (currentScreen.name) {
5276
5473
  case "HOME":
5277
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(HomeScreen, { nav });
5474
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(HomeScreen, { nav, onReportBug: handleReportBug });
5278
5475
  case "TEST_DETAIL":
5279
5476
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(TestDetailScreen, { testId: currentScreen.testId, nav });
5280
5477
  case "TEST_LIST":
@@ -5282,7 +5479,17 @@ function BugBearPanel({
5282
5479
  case "TEST_FEEDBACK":
5283
5480
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(TestFeedbackScreen, { status: currentScreen.status, assignmentId: currentScreen.assignmentId, nav });
5284
5481
  case "REPORT":
5285
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ReportScreen, { nav, prefill: currentScreen.prefill });
5482
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5483
+ ReportScreen,
5484
+ {
5485
+ nav,
5486
+ prefill: currentScreen.prefill,
5487
+ autoCapture: autoCaptureRef.current,
5488
+ onAutoCaptureConsumed: () => {
5489
+ autoCaptureRef.current = null;
5490
+ }
5491
+ }
5492
+ );
5286
5493
  case "REPORT_SUCCESS":
5287
5494
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ReportSuccessScreen, { nav });
5288
5495
  case "MESSAGE_LIST":
@@ -5479,10 +5686,10 @@ function BugBearPanel({
5479
5686
  }
5480
5687
 
5481
5688
  // src/BugBearErrorBoundary.tsx
5482
- var import_react15 = require("react");
5689
+ var import_react16 = require("react");
5483
5690
  var import_core2 = require("@bbearai/core");
5484
5691
  var import_jsx_runtime19 = require("react/jsx-runtime");
5485
- var BugBearErrorBoundary = class extends import_react15.Component {
5692
+ var BugBearErrorBoundary = class extends import_react16.Component {
5486
5693
  constructor(props) {
5487
5694
  super(props);
5488
5695
  this.reset = () => {
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 }) {
676
+ function HomeScreen({ nav, onReportBug }) {
566
677
  const { assignments, unreadCount, threads, refreshAssignments, refreshThreads, issueCounts, refreshIssueCounts, dashboardUrl, isLoading } = useBugBear();
567
- useEffect2(() => {
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,
@@ -1019,7 +1132,7 @@ function HomeScreen({ nav }) {
1019
1132
  }
1020
1133
 
1021
1134
  // src/widget/screens/TestDetailScreen.tsx
1022
- 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";
1023
1136
  import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1024
1137
  function TestDetailScreen({ testId, nav }) {
1025
1138
  const { client, assignments, currentAssignment, refreshAssignments, onNavigate } = useBugBear();
@@ -1033,12 +1146,12 @@ function TestDetailScreen({ testId, nav }) {
1033
1146
  const [skipNotes, setSkipNotes] = useState2("");
1034
1147
  const [skipping, setSkipping] = useState2(false);
1035
1148
  const [isSubmitting, setIsSubmitting] = useState2(false);
1036
- useEffect3(() => {
1149
+ useEffect4(() => {
1037
1150
  setCriteriaResults({});
1038
1151
  setShowSteps(true);
1039
1152
  setShowDetails(false);
1040
1153
  }, [displayedAssignment?.id]);
1041
- useEffect3(() => {
1154
+ useEffect4(() => {
1042
1155
  const active = displayedAssignment?.status === "in_progress" ? displayedAssignment : null;
1043
1156
  if (!active?.startedAt) {
1044
1157
  setAssignmentElapsedTime(0);
@@ -1623,6 +1736,11 @@ function TestDetailScreen({ testId, nav }) {
1623
1736
  ]
1624
1737
  }
1625
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
+ ] }),
1626
1744
  testCase.description && /* @__PURE__ */ jsx4("div", { style: { fontSize: 13, color: colors.textSecondary, lineHeight: "18px" }, children: testCase.description }),
1627
1745
  testCase.group && /* @__PURE__ */ jsx4("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ jsxs3("span", { style: { fontSize: 12, color: colors.textMuted }, children: [
1628
1746
  "\u{1F4C1} ",
@@ -1978,13 +2096,6 @@ function TestListScreen({ nav }) {
1978
2096
  }
1979
2097
  return Array.from(trackMap.values());
1980
2098
  }, [assignments]);
1981
- const availablePlatforms = useMemo(() => {
1982
- const set = /* @__PURE__ */ new Set();
1983
- for (const a of assignments) {
1984
- if (a.testCase.platforms) a.testCase.platforms.forEach((p) => set.add(p));
1985
- }
1986
- return Array.from(set).sort();
1987
- }, [assignments]);
1988
2099
  const selectedRole = availableRoles.find((r) => r.id === roleFilter);
1989
2100
  const groupedAssignments = useMemo(() => {
1990
2101
  const groups = /* @__PURE__ */ new Map();
@@ -2192,7 +2303,7 @@ function TestListScreen({ nav }) {
2192
2303
  }
2193
2304
  }
2194
2305
  ) }),
2195
- availablePlatforms.length >= 2 && /* @__PURE__ */ jsxs4("div", { style: { display: "flex", gap: 4, marginBottom: 8 }, children: [
2306
+ /* @__PURE__ */ jsxs4("div", { style: { display: "flex", gap: 4, marginBottom: 8 }, children: [
2196
2307
  /* @__PURE__ */ jsx5(
2197
2308
  "button",
2198
2309
  {
@@ -2209,18 +2320,20 @@ function TestListScreen({ nav }) {
2209
2320
  fontWeight: !platformFilter ? 600 : 400,
2210
2321
  whiteSpace: "nowrap"
2211
2322
  },
2212
- children: "All Platforms"
2323
+ children: "All"
2213
2324
  }
2214
2325
  ),
2215
- availablePlatforms.map((p) => {
2216
- const isActive = platformFilter === p;
2217
- const label = p === "ios" ? "iOS" : p === "android" ? "Android" : p === "web" ? "Web" : p;
2218
- const icon = p === "ios" ? "\u{1F4F1}" : p === "android" ? "\u{1F916}" : p === "web" ? "\u{1F310}" : "\u{1F4CB}";
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;
2219
2332
  return /* @__PURE__ */ jsxs4(
2220
2333
  "button",
2221
2334
  {
2222
2335
  type: "button",
2223
- onClick: () => setPlatformFilter(isActive ? null : p),
2336
+ onClick: () => setPlatformFilter(isActive ? null : p.key),
2224
2337
  style: {
2225
2338
  display: "flex",
2226
2339
  alignItems: "center",
@@ -2236,12 +2349,12 @@ function TestListScreen({ nav }) {
2236
2349
  whiteSpace: "nowrap"
2237
2350
  },
2238
2351
  children: [
2239
- icon,
2352
+ p.icon,
2240
2353
  " ",
2241
- label
2354
+ p.label
2242
2355
  ]
2243
2356
  },
2244
- p
2357
+ p.key
2245
2358
  );
2246
2359
  })
2247
2360
  ] }),
@@ -2649,6 +2762,25 @@ function useImageAttachments(uploadFn, maxImages, bucket = "screenshots") {
2649
2762
  const pickFromCamera = useCallback4(() => {
2650
2763
  triggerFilePicker("environment");
2651
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]);
2652
2784
  const removeImage = useCallback4((id) => {
2653
2785
  setImages((prev) => {
2654
2786
  const img = prev.find((i) => i.id === id);
@@ -2670,7 +2802,7 @@ function useImageAttachments(uploadFn, maxImages, bucket = "screenshots") {
2670
2802
  const getScreenshotUrls = useCallback4(() => {
2671
2803
  return images.filter((img) => img.status === "done" && img.remoteUrl).map((img) => img.remoteUrl);
2672
2804
  }, [images]);
2673
- return { images, pickFromGallery, pickFromCamera, removeImage, clear, isUploading, hasError, getAttachments, getScreenshotUrls };
2805
+ return { images, pickFromGallery, pickFromCamera, addFile, removeImage, clear, isUploading, hasError, getAttachments, getScreenshotUrls };
2674
2806
  }
2675
2807
 
2676
2808
  // src/widget/ImagePreviewStrip.tsx
@@ -2791,7 +2923,12 @@ function ImagePickerButtons({ images, maxImages, onPickGallery, onPickCamera, on
2791
2923
  maxImages
2792
2924
  ] })
2793
2925
  ] }),
2794
- /* @__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
+ ] })
2795
2932
  ] });
2796
2933
  }
2797
2934
 
@@ -3089,7 +3226,7 @@ var styles = {
3089
3226
  };
3090
3227
 
3091
3228
  // src/widget/screens/ReportScreen.tsx
3092
- import React6, { useState as useState6, useRef as useRef2 } from "react";
3229
+ import React6, { useState as useState6, useRef as useRef3, useEffect as useEffect6 } from "react";
3093
3230
 
3094
3231
  // src/widget/CategoryDropdown.tsx
3095
3232
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
@@ -3135,11 +3272,63 @@ function CategoryDropdown({ value, onChange, optional = true, disabled = false }
3135
3272
  );
3136
3273
  }
3137
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
+
3138
3311
  // src/widget/screens/ReportScreen.tsx
3139
3312
  import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
3140
- function ReportScreen({ nav, prefill }) {
3313
+ function ReportScreen({ nav, prefill, autoCapture, onAutoCaptureConsumed }) {
3141
3314
  const { client, refreshAssignments, uploadImage } = useBugBear();
3142
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
+ });
3143
3332
  const [reportType, setReportType] = useState6(prefill?.type || "bug");
3144
3333
  const [severity, setSeverity] = useState6("medium");
3145
3334
  const [category, setCategory] = useState6(null);
@@ -3147,7 +3336,7 @@ function ReportScreen({ nav, prefill }) {
3147
3336
  const [affectedRoute, setAffectedRoute] = useState6("");
3148
3337
  const [submitting, setSubmitting] = useState6(false);
3149
3338
  const [error, setError] = useState6(null);
3150
- const submittingRef = useRef2(false);
3339
+ const submittingRef = useRef3(false);
3151
3340
  React6.useEffect(() => {
3152
3341
  if (reportType === "feedback" || reportType === "suggestion") {
3153
3342
  setCategory("other");
@@ -3155,7 +3344,7 @@ function ReportScreen({ nav, prefill }) {
3155
3344
  setCategory(null);
3156
3345
  }
3157
3346
  }, [reportType]);
3158
- const observedRoute = useRef2(
3347
+ const observedRoute = useRef3(
3159
3348
  typeof window !== "undefined" ? window.location.pathname : "unknown"
3160
3349
  );
3161
3350
  const isRetestFailure = prefill?.type === "test_fail";
@@ -3211,7 +3400,7 @@ function ReportScreen({ nav, prefill }) {
3211
3400
  { sev: "medium", color: "#eab308" },
3212
3401
  { sev: "low", color: "#6b7280" }
3213
3402
  ];
3214
- 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: [
3215
3404
  /* @__PURE__ */ jsxs9("div", { style: styles2.retestBanner, children: [
3216
3405
  /* @__PURE__ */ jsx10("span", { style: { fontSize: 16 }, children: "\u{1F504}" }),
3217
3406
  /* @__PURE__ */ jsxs9("div", { children: [
@@ -3539,10 +3728,10 @@ var styles2 = {
3539
3728
  };
3540
3729
 
3541
3730
  // src/widget/screens/ReportSuccessScreen.tsx
3542
- import { useEffect as useEffect4 } from "react";
3731
+ import { useEffect as useEffect7 } from "react";
3543
3732
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
3544
3733
  function ReportSuccessScreen({ nav }) {
3545
- useEffect4(() => {
3734
+ useEffect7(() => {
3546
3735
  const timer = setTimeout(() => nav.reset(), 2e3);
3547
3736
  return () => clearTimeout(timer);
3548
3737
  }, [nav]);
@@ -3832,7 +4021,7 @@ function MessageListScreen({ nav }) {
3832
4021
  }
3833
4022
 
3834
4023
  // src/widget/screens/ThreadDetailScreen.tsx
3835
- import { useState as useState7, useEffect as useEffect5 } from "react";
4024
+ import { useState as useState7, useEffect as useEffect8 } from "react";
3836
4025
  import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
3837
4026
  var inputStyle = {
3838
4027
  backgroundColor: "#27272a",
@@ -3854,7 +4043,7 @@ function ThreadDetailScreen({
3854
4043
  const [replyText, setReplyText] = useState7("");
3855
4044
  const [sending, setSending] = useState7(false);
3856
4045
  const [sendError, setSendError] = useState7(false);
3857
- useEffect5(() => {
4046
+ useEffect8(() => {
3858
4047
  let cancelled = false;
3859
4048
  setLoading(true);
3860
4049
  (async () => {
@@ -4279,7 +4468,7 @@ function ComposeMessageScreen({ nav }) {
4279
4468
  }
4280
4469
 
4281
4470
  // src/widget/screens/ProfileScreen.tsx
4282
- import { useState as useState9, useEffect as useEffect6 } from "react";
4471
+ import { useState as useState9, useEffect as useEffect9 } from "react";
4283
4472
  import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
4284
4473
  function ProfileScreen({ nav }) {
4285
4474
  const { testerInfo, assignments, updateTesterProfile, refreshTesterInfo } = useBugBear();
@@ -4292,7 +4481,7 @@ function ProfileScreen({ nav }) {
4292
4481
  const [saved, setSaved] = useState9(false);
4293
4482
  const [showDetails, setShowDetails] = useState9(false);
4294
4483
  const completedCount = assignments.filter((a) => a.status === "passed" || a.status === "failed").length;
4295
- useEffect6(() => {
4484
+ useEffect9(() => {
4296
4485
  if (testerInfo) {
4297
4486
  setName(testerInfo.name);
4298
4487
  setAdditionalEmails(testerInfo.additionalEmails || []);
@@ -4756,7 +4945,7 @@ var styles4 = {
4756
4945
  };
4757
4946
 
4758
4947
  // src/widget/screens/IssueListScreen.tsx
4759
- import { useState as useState10, useEffect as useEffect7 } from "react";
4948
+ import { useState as useState10, useEffect as useEffect10 } from "react";
4760
4949
  import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
4761
4950
  var CATEGORY_CONFIG = {
4762
4951
  open: { label: "Open Issues", accent: "#f97316", emptyIcon: "\u2705", emptyText: "No open issues" },
@@ -4774,7 +4963,7 @@ function IssueListScreen({ nav, category }) {
4774
4963
  const [issues, setIssues] = useState10([]);
4775
4964
  const [loading, setLoading] = useState10(true);
4776
4965
  const config = CATEGORY_CONFIG[category];
4777
- useEffect7(() => {
4966
+ useEffect10(() => {
4778
4967
  let cancelled = false;
4779
4968
  setLoading(true);
4780
4969
  (async () => {
@@ -5116,11 +5305,12 @@ function BugBearPanel({
5116
5305
  const { shouldShowWidget, testerInfo, assignments, isLoading, unreadCount } = useBugBear();
5117
5306
  const { currentScreen, canGoBack, push, pop, replace, reset } = useNavigation();
5118
5307
  const [collapsed, setCollapsed] = useState11(defaultCollapsed);
5308
+ const autoCaptureRef = useRef4(null);
5119
5309
  const [panelPosition, setPanelPosition] = useState11(null);
5120
5310
  const [isDragging, setIsDragging] = useState11(false);
5121
- const dragStartRef = useRef3(null);
5122
- const panelRef = useRef3(null);
5123
- useEffect8(() => {
5311
+ const dragStartRef = useRef4(null);
5312
+ const panelRef = useRef4(null);
5313
+ useEffect11(() => {
5124
5314
  if (typeof window === "undefined") return;
5125
5315
  try {
5126
5316
  const saved = localStorage.getItem(STORAGE_KEY);
@@ -5134,7 +5324,7 @@ function BugBearPanel({
5134
5324
  setPanelPosition(getDefaultPosition(position));
5135
5325
  }
5136
5326
  }, [position]);
5137
- useEffect8(() => {
5327
+ useEffect11(() => {
5138
5328
  if (panelPosition && typeof window !== "undefined") {
5139
5329
  try {
5140
5330
  localStorage.setItem(STORAGE_KEY, JSON.stringify(panelPosition));
@@ -5142,7 +5332,7 @@ function BugBearPanel({
5142
5332
  }
5143
5333
  }
5144
5334
  }, [panelPosition]);
5145
- useEffect8(() => {
5335
+ useEffect11(() => {
5146
5336
  if (typeof window === "undefined") return;
5147
5337
  const handleResize = () => {
5148
5338
  setPanelPosition((prev) => prev ? clampPosition(prev) : getDefaultPosition(position));
@@ -5150,7 +5340,7 @@ function BugBearPanel({
5150
5340
  window.addEventListener("resize", handleResize);
5151
5341
  return () => window.removeEventListener("resize", handleResize);
5152
5342
  }, [position]);
5153
- const handleMouseDown = useCallback5((e) => {
5343
+ const handleMouseDown = useCallback6((e) => {
5154
5344
  if (!draggable || !panelPosition) return;
5155
5345
  const target = e.target;
5156
5346
  if (!target.closest("[data-drag-handle]")) return;
@@ -5163,7 +5353,7 @@ function BugBearPanel({
5163
5353
  panelY: panelPosition.y
5164
5354
  };
5165
5355
  }, [draggable, panelPosition]);
5166
- useEffect8(() => {
5356
+ useEffect11(() => {
5167
5357
  if (!isDragging) return;
5168
5358
  const handleMouseMove = (e) => {
5169
5359
  if (!dragStartRef.current) return;
@@ -5185,7 +5375,7 @@ function BugBearPanel({
5185
5375
  document.removeEventListener("mouseup", handleMouseUp);
5186
5376
  };
5187
5377
  }, [isDragging]);
5188
- const handleDoubleClick = useCallback5(() => {
5378
+ const handleDoubleClick = useCallback6(() => {
5189
5379
  if (!draggable) return;
5190
5380
  setPanelPosition(getDefaultPosition(position));
5191
5381
  try {
@@ -5193,6 +5383,14 @@ function BugBearPanel({
5193
5383
  } catch {
5194
5384
  }
5195
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]);
5196
5394
  if (isLoading || !shouldShowWidget) return null;
5197
5395
  if (!panelPosition) return null;
5198
5396
  const pendingCount = assignments.filter((a) => a.status === "pending" || a.status === "in_progress").length;
@@ -5229,13 +5427,12 @@ function BugBearPanel({
5229
5427
  };
5230
5428
  const handleClose = () => {
5231
5429
  setCollapsed(true);
5232
- reset();
5233
5430
  };
5234
5431
  const nav = { push, pop, replace, reset, canGoBack };
5235
5432
  const renderScreen = () => {
5236
5433
  switch (currentScreen.name) {
5237
5434
  case "HOME":
5238
- return /* @__PURE__ */ jsx18(HomeScreen, { nav });
5435
+ return /* @__PURE__ */ jsx18(HomeScreen, { nav, onReportBug: handleReportBug });
5239
5436
  case "TEST_DETAIL":
5240
5437
  return /* @__PURE__ */ jsx18(TestDetailScreen, { testId: currentScreen.testId, nav });
5241
5438
  case "TEST_LIST":
@@ -5243,7 +5440,17 @@ function BugBearPanel({
5243
5440
  case "TEST_FEEDBACK":
5244
5441
  return /* @__PURE__ */ jsx18(TestFeedbackScreen, { status: currentScreen.status, assignmentId: currentScreen.assignmentId, nav });
5245
5442
  case "REPORT":
5246
- 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
+ );
5247
5454
  case "REPORT_SUCCESS":
5248
5455
  return /* @__PURE__ */ jsx18(ReportSuccessScreen, { nav });
5249
5456
  case "MESSAGE_LIST":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/react",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "BugBear React components for web apps",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -41,13 +41,15 @@
41
41
  "prepublishOnly": "npm run build"
42
42
  },
43
43
  "dependencies": {
44
- "@bbearai/core": "^0.5.0"
44
+ "@bbearai/core": "^0.5.0",
45
+ "modern-screenshot": "^4.6.8"
45
46
  },
46
47
  "peerDependencies": {
47
48
  "react": "^18.0.0 || ^19.0.0",
48
49
  "react-dom": "^18.0.0 || ^19.0.0"
49
50
  },
50
51
  "devDependencies": {
52
+ "@testing-library/dom": "^10.4.1",
51
53
  "@testing-library/jest-dom": "^6.9.1",
52
54
  "@testing-library/react": "^16.3.2",
53
55
  "@testing-library/user-event": "^14.6.1",