quick-bug-reporter-react 1.3.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,7 @@ Drop-in bug reporter for React apps — screenshot capture, video recording, ann
7
7
  - **Screenshot capture** — Full-page or region selection via `html2canvas-pro`
8
8
  - **Video recording** — Screen + microphone via `MediaRecorder` API
9
9
  - **Annotation** — Drag-to-highlight on captured screenshots
10
+ - **Structured bug reports** — Tab-based UI for Steps/Expected/Actual/Context (4000 char total)
10
11
  - **Network logging** — Automatic fetch interception during capture
11
12
  - **Console capture** — Automatic console log and JS error capture
12
13
  - **Integrations** — Linear and Jira (direct API or backend proxy)
package/dist/index.cjs CHANGED
@@ -882,42 +882,104 @@ var CloudIntegration = class {
882
882
  throw new Error("CloudIntegration: projectKey is required.");
883
883
  }
884
884
  this.projectKey = options.projectKey;
885
- this.endpoint = options.endpoint ?? "/api/ingest";
885
+ this.endpoint = options.ingestUrl ?? options.endpoint ?? "/api/ingest";
886
+ this.appVersion = options.appVersion;
887
+ this.environment = options.environment;
886
888
  this.fetchFn = options.fetchImpl ?? fetch.bind(globalThis);
887
889
  }
888
890
  async submit(payload, onProgress = noop3) {
889
891
  onProgress("Preparing report\u2026");
890
- const ua = payload.userAgent || navigator.userAgent;
892
+ const ua = payload.userAgent || getRuntimeUserAgent();
891
893
  const browserName = parseBrowserName(ua);
894
+ const browserVersion = parseBrowserVersion(ua);
892
895
  const osName = parseOsName(ua);
893
- const body = {
894
- project_key: this.projectKey,
895
- title: payload.title,
896
- provider: "cloud",
897
- capture_mode: payload.captureMode,
898
- has_screenshot: Boolean(payload.screenshotBlob),
899
- has_video: Boolean(payload.videoBlob),
900
- has_network_logs: payload.networkLogs.length > 0,
901
- has_console_logs: payload.consoleLogs.length > 0,
902
- js_error_count: payload.jsErrors.length,
903
- user_agent: ua,
904
- browser_name: browserName,
905
- os_name: osName,
906
- device_type: getDeviceType(),
907
- screen_resolution: getScreenResolution(),
908
- viewport: getViewport(),
909
- color_scheme: payload.metadata.colorScheme !== "unknown" ? payload.metadata.colorScheme : null,
910
- locale: payload.metadata.locale,
911
- timezone: payload.metadata.timezone,
912
- connection_type: payload.metadata.connection?.effectiveType ?? null,
913
- page_url: payload.pageUrl,
914
- environment: getEnvironment()
915
- };
896
+ const osVersion = parseOsVersion(ua, osName);
897
+ const fd = new FormData();
898
+ fd.set("project_key", this.projectKey);
899
+ fd.set("title", payload.title);
900
+ fd.set("description", payload.description || "");
901
+ if (payload.stepsToReproduce) fd.set("steps_to_reproduce", payload.stepsToReproduce);
902
+ if (payload.expectedResult) fd.set("expected_result", payload.expectedResult);
903
+ if (payload.actualResult) fd.set("actual_result", payload.actualResult);
904
+ if (payload.additionalContext) fd.set("additional_context", payload.additionalContext);
905
+ fd.set("provider", "cloud");
906
+ fd.set("capture_mode", payload.captureMode);
907
+ fd.set("has_screenshot", String(Boolean(payload.screenshotBlob)));
908
+ fd.set("has_video", String(Boolean(payload.videoBlob)));
909
+ fd.set("has_network_logs", String(payload.networkLogs.length > 0));
910
+ fd.set("has_console_logs", String(payload.consoleLogs.length > 0));
911
+ fd.set("js_error_count", String(payload.jsErrors.length));
912
+ fd.set("user_agent", ua);
913
+ fd.set("browser_name", browserName);
914
+ fd.set("browser_version", browserVersion ?? "");
915
+ fd.set("os_name", osName);
916
+ fd.set("os_version", osVersion ?? "");
917
+ fd.set("device_type", getDeviceType());
918
+ fd.set("screen_resolution", getScreenResolution());
919
+ fd.set("viewport", getViewport());
920
+ fd.set("color_scheme", payload.metadata.colorScheme !== "unknown" ? payload.metadata.colorScheme : "");
921
+ fd.set("locale", payload.metadata.locale ?? "");
922
+ fd.set("timezone", payload.metadata.timezone ?? "");
923
+ fd.set("connection_type", payload.metadata.connection?.effectiveType ?? "");
924
+ fd.set("page_url", payload.pageUrl || "");
925
+ fd.set("environment", this.environment ?? getEnvironment());
926
+ fd.set("app_version", this.appVersion ?? "");
927
+ fd.set("platform", payload.metadata.platform ?? "");
928
+ fd.set("duration_ms", String(payload.elapsedMs));
929
+ fd.set("stopped_at", payload.stoppedAt || "");
930
+ if (payload.metadata.mobile) {
931
+ fd.set("platform", payload.metadata.mobile.platform);
932
+ fd.set("device_model", payload.metadata.mobile.deviceModel ?? "");
933
+ fd.set("device_brand", payload.metadata.mobile.deviceBrand ?? "");
934
+ fd.set("os_version", payload.metadata.mobile.osVersion ?? "");
935
+ fd.set("app_build_number", payload.metadata.mobile.appBuildNumber ?? "");
936
+ fd.set("is_emulator", String(payload.metadata.mobile.isEmulator));
937
+ fd.set(
938
+ "battery_level",
939
+ payload.metadata.mobile.batteryLevel == null ? "" : String(payload.metadata.mobile.batteryLevel)
940
+ );
941
+ fd.set(
942
+ "free_storage_mb",
943
+ payload.metadata.mobile.freeStorageMb == null ? "" : String(payload.metadata.mobile.freeStorageMb)
944
+ );
945
+ fd.set("invocation_method", payload.metadata.mobile.invocationMethod);
946
+ }
947
+ if (payload.screenshotBlob) {
948
+ fd.append("screenshot", payload.screenshotBlob, "bug-screenshot.png");
949
+ }
950
+ if (payload.videoBlob) {
951
+ fd.append("video", payload.videoBlob, "bug-recording.webm");
952
+ }
953
+ fd.append(
954
+ "network_logs",
955
+ new Blob([formatNetworkLogs(payload.networkLogs)], { type: "text/plain" }),
956
+ "network-logs.txt"
957
+ );
958
+ const parts = [];
959
+ if (payload.jsErrors.length > 0) {
960
+ parts.push("=== JavaScript Errors ===\n" + formatJsErrors(payload.jsErrors));
961
+ } else {
962
+ parts.push("=== JavaScript Errors ===\nNo JavaScript errors captured.");
963
+ }
964
+ if (payload.consoleLogs.length > 0) {
965
+ parts.push("=== Console Output ===\n" + formatConsoleLogs(payload.consoleLogs));
966
+ } else {
967
+ parts.push("=== Console Output ===\nNo console output captured.");
968
+ }
969
+ fd.append(
970
+ "console_logs",
971
+ new Blob([parts.join("\n\n")], { type: "text/plain" }),
972
+ "console-logs.txt"
973
+ );
974
+ fd.append(
975
+ "metadata",
976
+ new Blob([JSON.stringify(payload.metadata, null, 2)], { type: "application/json" }),
977
+ "client-metadata.json"
978
+ );
916
979
  onProgress("Sending report\u2026");
917
980
  const response = await this.fetchFn(this.endpoint, {
918
981
  method: "POST",
919
- headers: { "Content-Type": "application/json" },
920
- body: JSON.stringify(body)
982
+ body: fd
921
983
  });
922
984
  if (!response.ok) {
923
985
  const errorBody = await response.json().catch(() => ({}));
@@ -926,12 +988,22 @@ var CloudIntegration = class {
926
988
  }
927
989
  const result = await response.json();
928
990
  onProgress("Report submitted.");
991
+ const fwd = result.forwarding;
992
+ const externalKey = fwd?.key;
993
+ const externalUrl = fwd?.url;
994
+ const warnings = [];
995
+ if (result.forwarding_status === "queued") {
996
+ warnings.push("Tracker forwarding is running in the background.");
997
+ }
998
+ if (fwd?.error) {
999
+ warnings.push(`Forwarding: ${fwd.error}`);
1000
+ }
929
1001
  return {
930
1002
  provider: "cloud",
931
1003
  issueId: result.id,
932
- issueKey: `QB-${result.id.slice(0, 8)}`,
933
- issueUrl: null,
934
- warnings: []
1004
+ issueKey: externalKey || `QB-${result.id.slice(0, 8)}`,
1005
+ issueUrl: externalUrl || null,
1006
+ warnings
935
1007
  };
936
1008
  }
937
1009
  };
@@ -943,14 +1015,50 @@ function parseBrowserName(ua) {
943
1015
  if (ua.includes("Safari/") && !ua.includes("Chrome/")) return "Safari";
944
1016
  return "Unknown";
945
1017
  }
1018
+ function parseBrowserVersion(ua) {
1019
+ return matchVersion(ua, /Edg\/([\d.]+)/) || matchVersion(ua, /OPR\/([\d.]+)/) || matchVersion(ua, /Opera\/([\d.]+)/) || matchVersion(ua, /Chrome\/([\d.]+)/) || matchVersion(ua, /Firefox\/([\d.]+)/) || matchVersion(ua, /Version\/([\d.]+).*Safari/);
1020
+ }
1021
+ function getRuntimeUserAgent() {
1022
+ if (typeof navigator !== "undefined" && typeof navigator.userAgent === "string") {
1023
+ return navigator.userAgent;
1024
+ }
1025
+ return "";
1026
+ }
946
1027
  function parseOsName(ua) {
947
1028
  if (ua.includes("Windows")) return "Windows";
948
- if (ua.includes("Mac OS")) return "macOS";
949
- if (ua.includes("Linux")) return "Linux";
950
1029
  if (ua.includes("Android")) return "Android";
951
1030
  if (ua.includes("iPhone") || ua.includes("iPad")) return "iOS";
1031
+ if (ua.includes("Mac OS")) return "macOS";
1032
+ if (ua.includes("Linux")) return "Linux";
952
1033
  return "Unknown";
953
1034
  }
1035
+ function parseOsVersion(ua, osName) {
1036
+ if (osName === "Windows") {
1037
+ const nt = matchVersion(ua, /Windows NT ([\d.]+)/);
1038
+ if (!nt) return null;
1039
+ if (nt.startsWith("10.0")) return "10/11";
1040
+ if (nt.startsWith("6.3")) return "8.1";
1041
+ if (nt.startsWith("6.2")) return "8";
1042
+ if (nt.startsWith("6.1")) return "7";
1043
+ return nt;
1044
+ }
1045
+ if (osName === "macOS") {
1046
+ const mac = matchVersion(ua, /Mac OS X ([0-9_]+)/);
1047
+ return mac ? mac.replace(/_/g, ".") : null;
1048
+ }
1049
+ if (osName === "Android") {
1050
+ return matchVersion(ua, /Android ([\d.]+)/);
1051
+ }
1052
+ if (osName === "iOS") {
1053
+ const ios = matchVersion(ua, /OS ([0-9_]+) like Mac OS X/);
1054
+ return ios ? ios.replace(/_/g, ".") : null;
1055
+ }
1056
+ return null;
1057
+ }
1058
+ function matchVersion(ua, pattern) {
1059
+ const match = ua.match(pattern);
1060
+ return match?.[1] ?? null;
1061
+ }
954
1062
  function getDeviceType() {
955
1063
  if (typeof window === "undefined") return "unknown";
956
1064
  const w = window.innerWidth;
@@ -1429,6 +1537,7 @@ var BugSession = class {
1429
1537
  this.autoStopTimeout = null;
1430
1538
  this.stopInFlight = null;
1431
1539
  this.lastArtifacts = null;
1540
+ this.screenshotLogsPendingSubmit = false;
1432
1541
  this.maxDurationMs = options.maxDurationMs ?? DEFAULT_MAX_RECORDING_MS;
1433
1542
  this.screenRecorder = options.screenRecorder ?? new ScreenRecorder();
1434
1543
  this.screenshotCapturer = options.screenshotCapturer ?? new ScreenshotCapturer();
@@ -1439,9 +1548,13 @@ var BugSession = class {
1439
1548
  if (this.recording) {
1440
1549
  return;
1441
1550
  }
1551
+ if (this.networkLogger.isRecording()) {
1552
+ this.networkLogger.stop();
1553
+ }
1442
1554
  this.clearAutoStopTimer();
1443
1555
  this.networkLogger.clear();
1444
1556
  this.lastArtifacts = null;
1557
+ this.screenshotLogsPendingSubmit = false;
1445
1558
  this.networkLogger.start();
1446
1559
  try {
1447
1560
  await this.screenRecorder.start({
@@ -1464,14 +1577,18 @@ var BugSession = class {
1464
1577
  if (this.recording) {
1465
1578
  await this.stop("manual");
1466
1579
  }
1580
+ if (this.networkLogger.isRecording()) {
1581
+ this.networkLogger.stop();
1582
+ }
1467
1583
  this.clearAutoStopTimer();
1468
1584
  this.networkLogger.clear();
1469
1585
  this.lastArtifacts = null;
1586
+ this.screenshotLogsPendingSubmit = false;
1470
1587
  const startedAtMs = Date.now();
1471
1588
  this.networkLogger.start();
1472
1589
  try {
1473
1590
  const screenshotBlob = region ? await this.screenshotCapturer.captureRegion(region) : await this.screenshotCapturer.capture();
1474
- const networkLogs = this.networkLogger.stop();
1591
+ const networkLogs = this.networkLogger.getLogs();
1475
1592
  const stoppedAtMs = Date.now();
1476
1593
  const artifacts = {
1477
1594
  videoBlob: null,
@@ -1484,10 +1601,12 @@ var BugSession = class {
1484
1601
  stopReason: "manual"
1485
1602
  };
1486
1603
  this.lastArtifacts = artifacts;
1604
+ this.screenshotLogsPendingSubmit = true;
1487
1605
  return artifacts;
1488
1606
  } catch (error) {
1489
1607
  this.networkLogger.stop();
1490
1608
  this.networkLogger.clear();
1609
+ this.screenshotLogsPendingSubmit = false;
1491
1610
  throw error;
1492
1611
  }
1493
1612
  }
@@ -1521,17 +1640,32 @@ var BugSession = class {
1521
1640
  getLastCaptureMode() {
1522
1641
  return this.lastArtifacts?.captureMode ?? null;
1523
1642
  }
1643
+ finalizeNetworkLogsForSubmit(captureMode) {
1644
+ if (captureMode === "screenshot" && this.screenshotLogsPendingSubmit && this.networkLogger.isRecording()) {
1645
+ const logs = this.networkLogger.stop();
1646
+ this.screenshotLogsPendingSubmit = false;
1647
+ return logs;
1648
+ }
1649
+ return this.networkLogger.getLogs();
1650
+ }
1524
1651
  resetArtifacts() {
1525
1652
  this.lastArtifacts = null;
1526
1653
  this.screenRecorder.clearLastBlob();
1654
+ if (this.networkLogger.isRecording()) {
1655
+ this.networkLogger.stop();
1656
+ }
1527
1657
  this.networkLogger.clear();
1658
+ this.screenshotLogsPendingSubmit = false;
1528
1659
  }
1529
1660
  async dispose() {
1530
1661
  await this.stop("manual");
1531
1662
  this.clearAutoStopTimer();
1532
1663
  this.screenRecorder.dispose();
1533
- this.networkLogger.stop();
1664
+ if (this.networkLogger.isRecording()) {
1665
+ this.networkLogger.stop();
1666
+ }
1534
1667
  this.networkLogger.clear();
1668
+ this.screenshotLogsPendingSubmit = false;
1535
1669
  }
1536
1670
  async stopInternal(reason) {
1537
1671
  this.clearAutoStopTimer();
@@ -1552,6 +1686,7 @@ var BugSession = class {
1552
1686
  stopReason: reason
1553
1687
  };
1554
1688
  this.lastArtifacts = artifacts;
1689
+ this.screenshotLogsPendingSubmit = false;
1555
1690
  return artifacts;
1556
1691
  }
1557
1692
  async handleForcedStop(reason) {
@@ -1702,9 +1837,13 @@ var BugReporter = class {
1702
1837
  const payload = {
1703
1838
  title: normalizedTitle,
1704
1839
  description: normalizedDescription,
1840
+ stepsToReproduce: options.stepsToReproduce,
1841
+ expectedResult: options.expectedResult,
1842
+ actualResult: options.actualResult,
1843
+ additionalContext: options.additionalContext,
1705
1844
  videoBlob: artifacts.videoBlob,
1706
1845
  screenshotBlob: options.screenshotBlob ?? artifacts.screenshotBlob,
1707
- networkLogs: artifacts.networkLogs,
1846
+ networkLogs: this.session.finalizeNetworkLogsForSubmit(artifacts.captureMode),
1708
1847
  consoleLogs: options.consoleLogs ?? [],
1709
1848
  jsErrors: options.jsErrors ?? [],
1710
1849
  captureMode: artifacts.captureMode,
@@ -2234,7 +2373,7 @@ function BugReporterProvider({
2234
2373
  setScreenshotAnnotation(annotation);
2235
2374
  }, []);
2236
2375
  const submitReport = react.useCallback(
2237
- async (title, description) => {
2376
+ async (title, structuredFields) => {
2238
2377
  const reporter = getOrCreateReporter();
2239
2378
  if (!reporter) {
2240
2379
  setError("No bug tracker integration is configured.");
@@ -2254,6 +2393,25 @@ function BugReporterProvider({
2254
2393
  setSubmissionProgress("Preparing submission\u2026");
2255
2394
  setError(null);
2256
2395
  setSuccess(null);
2396
+ const { stepsToReproduce, expectedResult, actualResult, additionalContext } = structuredFields;
2397
+ const sections = [];
2398
+ if (stepsToReproduce.trim()) {
2399
+ sections.push(`## Steps to Reproduce
2400
+ ${stepsToReproduce.trim()}`);
2401
+ }
2402
+ if (expectedResult.trim()) {
2403
+ sections.push(`## Expected Result
2404
+ ${expectedResult.trim()}`);
2405
+ }
2406
+ if (actualResult.trim()) {
2407
+ sections.push(`## Actual Result
2408
+ ${actualResult.trim()}`);
2409
+ }
2410
+ if (additionalContext.trim()) {
2411
+ sections.push(`## Additional Context
2412
+ ${additionalContext.trim()}`);
2413
+ }
2414
+ const description = sections.length > 0 ? sections.join("\n\n") : "No description provided";
2257
2415
  const screenshotBlobForSubmit = draftMode === "screenshot" ? screenshotAnnotation.annotatedBlob ?? screenshotBlob : null;
2258
2416
  const metadata = {
2259
2417
  annotation: draftMode === "screenshot" && screenshotAnnotation.highlights.length > 0 ? {
@@ -2268,13 +2426,21 @@ function BugReporterProvider({
2268
2426
  };
2269
2427
  try {
2270
2428
  const result = await reporter.submit(title, description, {
2429
+ stepsToReproduce,
2430
+ expectedResult,
2431
+ actualResult,
2432
+ additionalContext,
2271
2433
  screenshotBlob: screenshotBlobForSubmit,
2272
2434
  metadata,
2273
2435
  consoleLogs,
2274
2436
  jsErrors,
2275
2437
  onProgress: setSubmissionProgress
2276
2438
  });
2277
- setSuccess(`Submitted to ${getProviderLabel(result.provider)} (${result.issueKey}).`);
2439
+ if (result.provider === "cloud" && !result.issueUrl) {
2440
+ setSuccess("Report received by QuickBugs Cloud. Tracker forwarding is running in the background.");
2441
+ } else {
2442
+ setSuccess(`Submitted to ${getProviderLabel(result.provider)} (${result.issueKey}).`);
2443
+ }
2278
2444
  clearDraft();
2279
2445
  setIsOpen(false);
2280
2446
  return result;
@@ -3061,6 +3227,7 @@ function providerLabel(provider) {
3061
3227
  if (provider === "cloud") return "QuickBugs Cloud";
3062
3228
  return provider;
3063
3229
  }
3230
+ var CHAR_LIMIT = 4e3;
3064
3231
  function BugReporterModal() {
3065
3232
  const {
3066
3233
  autoStopNotice,
@@ -3090,9 +3257,39 @@ function BugReporterModal() {
3090
3257
  videoPreviewUrl
3091
3258
  } = useBugReporter();
3092
3259
  const [title, setTitle] = react.useState("");
3093
- const [description, setDescription] = react.useState("");
3260
+ const [stepsToReproduce, setStepsToReproduce] = react.useState("");
3261
+ const [expectedResult, setExpectedResult] = react.useState("");
3262
+ const [actualResult, setActualResult] = react.useState("");
3263
+ const [additionalContext, setAdditionalContext] = react.useState("");
3094
3264
  const [step, setStep] = react.useState("review");
3265
+ const [activeTab, setActiveTab] = react.useState("steps");
3266
+ const totalChars = stepsToReproduce.length + expectedResult.length + actualResult.length + additionalContext.length;
3267
+ const isOverLimit = totalChars > CHAR_LIMIT;
3095
3268
  const elapsedLabel = react.useMemo(() => formatElapsed2(elapsedMs), [elapsedMs]);
3269
+ const handleStepsKeyDown = (event) => {
3270
+ if (event.key === "Enter" && !event.shiftKey) {
3271
+ event.preventDefault();
3272
+ const textarea = event.currentTarget;
3273
+ const cursorPos = textarea.selectionStart;
3274
+ const textBeforeCursor = stepsToReproduce.substring(0, cursorPos);
3275
+ const textAfterCursor = stepsToReproduce.substring(cursorPos);
3276
+ const lines = textBeforeCursor.split("\n");
3277
+ const currentLine = lines[lines.length - 1];
3278
+ const numberMatch = currentLine.match(/^(\d+)\.\s/);
3279
+ const nextNumber = numberMatch ? parseInt(numberMatch[1]) + 1 : lines.length > 0 && stepsToReproduce.trim() === "" ? 1 : lines.length + 1;
3280
+ const newText = textBeforeCursor + "\n" + nextNumber + ". " + textAfterCursor;
3281
+ setStepsToReproduce(newText);
3282
+ setTimeout(() => {
3283
+ const newCursorPos = cursorPos + ("\n" + nextNumber + ". ").length;
3284
+ textarea.setSelectionRange(newCursorPos, newCursorPos);
3285
+ }, 0);
3286
+ }
3287
+ };
3288
+ const handleStepsFocus = () => {
3289
+ if (stepsToReproduce.trim() === "") {
3290
+ setStepsToReproduce("1. ");
3291
+ }
3292
+ };
3096
3293
  const handleDialogOpenChange = (open) => {
3097
3294
  if (open) {
3098
3295
  openModal();
@@ -3103,15 +3300,24 @@ function BugReporterModal() {
3103
3300
  };
3104
3301
  const handleSubmit = async (event) => {
3105
3302
  event.preventDefault();
3106
- const result = await submitReport(title, description);
3303
+ const result = await submitReport(title, {
3304
+ stepsToReproduce,
3305
+ expectedResult,
3306
+ actualResult,
3307
+ additionalContext
3308
+ });
3107
3309
  if (result) {
3108
3310
  setTitle("");
3109
- setDescription("");
3311
+ setStepsToReproduce("");
3312
+ setExpectedResult("");
3313
+ setActualResult("");
3314
+ setAdditionalContext("");
3110
3315
  setStep("review");
3316
+ setActiveTab("steps");
3111
3317
  }
3112
3318
  };
3113
3319
  const hasIntegrations = availableProviders.length > 0;
3114
- const canSubmit = !isSubmitting && !isCapturingScreenshot && hasIntegrations && !!selectedProvider && hasDraft && title.trim().length > 0;
3320
+ const canSubmit = !isSubmitting && !isCapturingScreenshot && hasIntegrations && !!selectedProvider && hasDraft && title.trim().length > 0 && !isOverLimit;
3115
3321
  return /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open: isOpen, onOpenChange: handleDialogOpenChange, children: /* @__PURE__ */ jsxRuntime.jsx(
3116
3322
  DialogContent,
3117
3323
  {
@@ -3277,18 +3483,91 @@ function BugReporterModal() {
3277
3483
  }
3278
3484
  )
3279
3485
  ] }),
3280
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5 sm:col-span-2", children: [
3281
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-sm font-medium", htmlFor: "bug-description", children: "Quick note" }),
3282
- /* @__PURE__ */ jsxRuntime.jsx(
3486
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 sm:col-span-2", children: [
3487
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-sm font-medium", children: "Bug Details" }),
3488
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1 border-b border-gray-200", children: [
3489
+ /* @__PURE__ */ jsxRuntime.jsx(
3490
+ "button",
3491
+ {
3492
+ type: "button",
3493
+ className: `px-3 py-2 text-sm font-medium transition-colors ${activeTab === "steps" ? "border-b-2 border-indigo-600 text-indigo-600" : "text-gray-500 hover:text-gray-700"}`,
3494
+ onClick: () => setActiveTab("steps"),
3495
+ children: "Steps"
3496
+ }
3497
+ ),
3498
+ /* @__PURE__ */ jsxRuntime.jsx(
3499
+ "button",
3500
+ {
3501
+ type: "button",
3502
+ className: `px-3 py-2 text-sm font-medium transition-colors ${activeTab === "expected" ? "border-b-2 border-indigo-600 text-indigo-600" : "text-gray-500 hover:text-gray-700"}`,
3503
+ onClick: () => setActiveTab("expected"),
3504
+ children: "Expected"
3505
+ }
3506
+ ),
3507
+ /* @__PURE__ */ jsxRuntime.jsx(
3508
+ "button",
3509
+ {
3510
+ type: "button",
3511
+ className: `px-3 py-2 text-sm font-medium transition-colors ${activeTab === "actual" ? "border-b-2 border-indigo-600 text-indigo-600" : "text-gray-500 hover:text-gray-700"}`,
3512
+ onClick: () => setActiveTab("actual"),
3513
+ children: "Actual"
3514
+ }
3515
+ ),
3516
+ /* @__PURE__ */ jsxRuntime.jsx(
3517
+ "button",
3518
+ {
3519
+ type: "button",
3520
+ className: `px-3 py-2 text-sm font-medium transition-colors ${activeTab === "context" ? "border-b-2 border-indigo-600 text-indigo-600" : "text-gray-500 hover:text-gray-700"}`,
3521
+ onClick: () => setActiveTab("context"),
3522
+ children: "Context"
3523
+ }
3524
+ )
3525
+ ] }),
3526
+ activeTab === "steps" && /* @__PURE__ */ jsxRuntime.jsx(
3283
3527
  Textarea,
3284
3528
  {
3285
- id: "bug-description",
3286
- maxLength: 4e3,
3287
- placeholder: "What did you expect, what happened, and any quick repro steps.",
3288
- value: description,
3289
- onChange: (event) => setDescription(event.target.value)
3529
+ id: "bug-steps",
3530
+ placeholder: "Press Enter to start numbering steps...",
3531
+ value: stepsToReproduce,
3532
+ onChange: (event) => setStepsToReproduce(event.target.value),
3533
+ onKeyDown: handleStepsKeyDown,
3534
+ onFocus: handleStepsFocus
3290
3535
  }
3291
- )
3536
+ ),
3537
+ activeTab === "expected" && /* @__PURE__ */ jsxRuntime.jsx(
3538
+ Textarea,
3539
+ {
3540
+ id: "bug-expected",
3541
+ placeholder: "Describe what should happen...",
3542
+ value: expectedResult,
3543
+ onChange: (event) => setExpectedResult(event.target.value)
3544
+ }
3545
+ ),
3546
+ activeTab === "actual" && /* @__PURE__ */ jsxRuntime.jsx(
3547
+ Textarea,
3548
+ {
3549
+ id: "bug-actual",
3550
+ placeholder: "Describe what actually happened...",
3551
+ value: actualResult,
3552
+ onChange: (event) => setActualResult(event.target.value)
3553
+ }
3554
+ ),
3555
+ activeTab === "context" && /* @__PURE__ */ jsxRuntime.jsx(
3556
+ Textarea,
3557
+ {
3558
+ id: "bug-context",
3559
+ placeholder: "Any additional information, workarounds, or notes...",
3560
+ value: additionalContext,
3561
+ onChange: (event) => setAdditionalContext(event.target.value)
3562
+ }
3563
+ ),
3564
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: `text-xs ${isOverLimit ? "text-red-600 font-medium" : "text-gray-500"}`, children: [
3565
+ totalChars,
3566
+ "/",
3567
+ CHAR_LIMIT,
3568
+ " characters ",
3569
+ isOverLimit && "(over limit)"
3570
+ ] })
3292
3571
  ] }),
3293
3572
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5", children: [
3294
3573
  /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-sm font-medium", htmlFor: "bug-provider", children: "Submit to" }),