glitchgrab 1.30.0 → 1.31.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/dist/index.js CHANGED
@@ -31,28 +31,37 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/index.ts
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
- GLITCHGRAB_SHORTCUT: () => GLITCHGRAB_SHORTCUT,
35
- GLITCHGRAB_SHORTCUT_MAC: () => GLITCHGRAB_SHORTCUT_MAC,
34
+ FeedbackButton: () => FeedbackButton,
35
+ GLITCHGRAB_SHORTCUT: () => GLITCHGRAB_SHORTCUT2,
36
+ GLITCHGRAB_SHORTCUT_MAC: () => GLITCHGRAB_SHORTCUT_MAC2,
36
37
  GlitchgrabErrorBoundary: () => GlitchgrabErrorBoundary,
37
38
  GlitchgrabProvider: () => GlitchgrabProvider,
38
39
  ReportButton: () => ReportButton,
39
40
  addBreadcrumb: () => addBreadcrumb,
40
41
  captureContext: () => captureContext,
41
42
  captureDeviceInfo: () => captureDeviceInfo,
43
+ captureError: () => captureError,
44
+ clearAppContext: () => clearAppContext,
42
45
  clearBreadcrumbs: () => clearBreadcrumbs,
43
46
  clearDedupCache: () => clearDedupCache,
44
47
  computeSignature: () => computeSignature,
45
48
  enhanceText: () => enhanceText,
49
+ fetchGlitchgrabFeedback: () => fetchGlitchgrabFeedback,
46
50
  fetchGlitchgrabReports: () => fetchGlitchgrabReports,
51
+ getAppContext: () => getAppContext,
47
52
  getBreadcrumbs: () => getBreadcrumbs,
48
- getShortcutLabel: () => getShortcutLabel,
53
+ getShortcutLabel: () => getShortcutLabel2,
49
54
  initBreadcrumbs: () => initBreadcrumbs,
50
55
  matchesShortcut: () => matchesShortcut,
51
56
  sanitizeUrl: () => sanitizeUrl,
57
+ sendFeedback: () => sendFeedback,
52
58
  sendReport: () => sendReport,
59
+ setContext: () => setContext,
60
+ setContexts: () => setContexts,
53
61
  shouldSkipDuplicate: () => shouldSkipDuplicate,
54
62
  useGlitchgrab: () => useGlitchgrab,
55
63
  useGlitchgrabActions: () => useGlitchgrabActions,
64
+ useGlitchgrabFeedback: () => useGlitchgrabFeedback,
56
65
  useGlitchgrabReports: () => useGlitchgrabReports
57
66
  });
58
67
  module.exports = __toCommonJS(index_exports);
@@ -63,18 +72,76 @@ var import_react3 = require("react");
63
72
  // src/error-boundary.tsx
64
73
  var React = __toESM(require("react"));
65
74
 
75
+ // src/redact.ts
76
+ var SENSITIVE_KEY_RE = /(pass|pwd|secret|token|auth|session|cookie|credential|apikey|api_key|access_key|private|signature|otp|pin|cvv|card|ssn|aadhaar|pan)/i;
77
+ var EMAIL_RE = /[\w.+-]+@[\w-]+\.[\w.-]+/g;
78
+ var JWT_RE = /\beyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]{8,}\b/g;
79
+ var BEARER_RE = /\b(bearer\s+)[\w-.~+/]{12,}=*/gi;
80
+ var MAX_BODY_LENGTH = 500;
81
+ function redactString(value) {
82
+ return value.replace(JWT_RE, "[REDACTED]").replace(BEARER_RE, "$1[REDACTED]").replace(EMAIL_RE, "[REDACTED_EMAIL]");
83
+ }
84
+ function redactValue(value, depth) {
85
+ if (depth > 4) return "[DEPTH]";
86
+ if (value === null || value === void 0) return value;
87
+ if (typeof value === "string") return redactString(value);
88
+ if (typeof value === "number" || typeof value === "boolean") return value;
89
+ if (Array.isArray(value)) return value.slice(0, 20).map((v) => redactValue(v, depth + 1));
90
+ if (typeof value === "object") {
91
+ const out = {};
92
+ for (const [key, inner] of Object.entries(value)) {
93
+ out[key] = SENSITIVE_KEY_RE.test(key) ? "[REDACTED]" : redactValue(inner, depth + 1);
94
+ }
95
+ return out;
96
+ }
97
+ return String(value);
98
+ }
99
+ function redactBody(body, maxLength = MAX_BODY_LENGTH) {
100
+ try {
101
+ const trimmed = body.trim();
102
+ if (!trimmed) return "";
103
+ let result;
104
+ try {
105
+ result = JSON.stringify(redactValue(JSON.parse(trimmed), 0));
106
+ } catch (e) {
107
+ result = redactString(trimmed);
108
+ }
109
+ return result.length > maxLength ? `${result.slice(0, maxLength)}\u2026[truncated]` : result;
110
+ } catch (e) {
111
+ return "";
112
+ }
113
+ }
114
+
66
115
  // src/breadcrumbs.ts
67
116
  var MAX_DEFAULT = 50;
68
117
  var breadcrumbs = [];
69
118
  var maxBreadcrumbs = MAX_DEFAULT;
70
119
  var initialized = false;
71
- function initBreadcrumbs(max) {
120
+ var responseBodyOrigins = [];
121
+ function mayCaptureBody(url) {
122
+ try {
123
+ if (typeof window === "undefined") return false;
124
+ const origin = new URL(url, window.location.href).origin;
125
+ if (origin === window.location.origin) return true;
126
+ return responseBodyOrigins.includes(origin);
127
+ } catch (e) {
128
+ return false;
129
+ }
130
+ }
131
+ function initBreadcrumbs(max, options) {
132
+ var _a;
133
+ if ((_a = options == null ? void 0 : options.responseBodyOrigins) == null ? void 0 : _a.length) {
134
+ responseBodyOrigins = Array.from(
135
+ /* @__PURE__ */ new Set([...responseBodyOrigins, ...options.responseBodyOrigins])
136
+ );
137
+ }
72
138
  if (initialized) return;
73
139
  maxBreadcrumbs = max != null ? max : MAX_DEFAULT;
74
140
  try {
75
141
  if (typeof window === "undefined") return;
76
142
  interceptConsole();
77
143
  interceptFetch();
144
+ interceptXhr();
78
145
  interceptNavigation();
79
146
  interceptClicks();
80
147
  initialized = true;
@@ -133,6 +200,16 @@ function argsToString(args) {
133
200
  return "[unknown]";
134
201
  }
135
202
  }
203
+ async function readErrorBody(response, url) {
204
+ try {
205
+ if (response.ok) return "";
206
+ if (!mayCaptureBody(url)) return "";
207
+ const text = await response.clone().text();
208
+ return redactBody(text);
209
+ } catch (e) {
210
+ return "";
211
+ }
212
+ }
136
213
  function interceptFetch() {
137
214
  const origFetch = window.fetch;
138
215
  window.fetch = async function(input, init) {
@@ -145,10 +222,12 @@ function interceptFetch() {
145
222
  input,
146
223
  init
147
224
  );
225
+ const body = await readErrorBody(response, url);
148
226
  addBreadcrumb("api", `${method} ${url.slice(0, 100)} \u2192 ${response.status}`, {
149
227
  method,
150
228
  status: String(response.status),
151
- duration: `${Date.now() - start}ms`
229
+ duration: `${Date.now() - start}ms`,
230
+ ...body ? { responseBody: body } : {}
152
231
  });
153
232
  return response;
154
233
  } catch (err) {
@@ -161,6 +240,67 @@ function interceptFetch() {
161
240
  }
162
241
  };
163
242
  }
243
+ function interceptXhr() {
244
+ if (typeof XMLHttpRequest === "undefined") return;
245
+ const origOpen = XMLHttpRequest.prototype.open;
246
+ const origSend = XMLHttpRequest.prototype.send;
247
+ XMLHttpRequest.prototype.open = function(method, url, ...rest) {
248
+ try {
249
+ this.__ggMethod = method;
250
+ this.__ggUrl = typeof url === "string" ? url : url.toString();
251
+ const record = () => {
252
+ var _a, _b, _c, _d;
253
+ try {
254
+ const recordedMethod = (_a = this.__ggMethod) != null ? _a : "GET";
255
+ const recordedUrl = ((_b = this.__ggUrl) != null ? _b : "").slice(0, 100);
256
+ const duration = `${Date.now() - ((_c = this.__ggStart) != null ? _c : Date.now())}ms`;
257
+ if (!this.status) {
258
+ addBreadcrumb("api", `${recordedMethod} ${recordedUrl} \u2192 FAILED`, {
259
+ method: recordedMethod,
260
+ duration
261
+ });
262
+ return;
263
+ }
264
+ const failed = this.status < 200 || this.status >= 300;
265
+ const body = failed && mayCaptureBody((_d = this.__ggUrl) != null ? _d : "") ? readXhrBody(this) : "";
266
+ addBreadcrumb("api", `${recordedMethod} ${recordedUrl} \u2192 ${this.status}`, {
267
+ method: recordedMethod,
268
+ status: String(this.status),
269
+ duration,
270
+ ...body ? { responseBody: body } : {}
271
+ });
272
+ } catch (e) {
273
+ }
274
+ };
275
+ if (!this.__ggListening) {
276
+ this.__ggListening = true;
277
+ this.addEventListener("loadend", record);
278
+ }
279
+ } catch (e) {
280
+ }
281
+ return origOpen.apply(this, [
282
+ method,
283
+ url,
284
+ ...rest
285
+ ]);
286
+ };
287
+ XMLHttpRequest.prototype.send = function(...args) {
288
+ try {
289
+ this.__ggStart = Date.now();
290
+ } catch (e) {
291
+ }
292
+ return origSend.apply(this, args);
293
+ };
294
+ }
295
+ function readXhrBody(xhr) {
296
+ try {
297
+ if (xhr.responseType && xhr.responseType !== "text" && xhr.responseType !== "json") return "";
298
+ const raw = xhr.responseType === "json" ? JSON.stringify(xhr.response) : xhr.responseText;
299
+ return raw ? redactBody(raw) : "";
300
+ } catch (e) {
301
+ return "";
302
+ }
303
+ }
164
304
  function interceptNavigation() {
165
305
  const origPush = history.pushState.bind(history);
166
306
  const origReplace = history.replaceState.bind(history);
@@ -195,6 +335,90 @@ function interceptClicks() {
195
335
  );
196
336
  }
197
337
 
338
+ // src/runtime.ts
339
+ var pageLoadedAt = Date.now();
340
+ var errorCount = 0;
341
+ function incrementErrorCount() {
342
+ errorCount += 1;
343
+ return errorCount;
344
+ }
345
+ var toMb = (bytes) => typeof bytes === "number" ? Math.round(bytes / 1048576 * 10) / 10 : void 0;
346
+ function captureRuntimeInfo() {
347
+ try {
348
+ if (typeof window === "undefined") return null;
349
+ const memory = performance == null ? void 0 : performance.memory;
350
+ const connection = navigator == null ? void 0 : navigator.connection;
351
+ return {
352
+ timeOnPageMs: Date.now() - pageLoadedAt,
353
+ errorCount,
354
+ visibility: typeof document !== "undefined" ? document.visibilityState : "unknown",
355
+ ...memory ? {
356
+ jsHeapUsedMb: toMb(memory.usedJSHeapSize),
357
+ jsHeapLimitMb: toMb(memory.jsHeapSizeLimit)
358
+ } : {},
359
+ ...connection ? {
360
+ connectionType: connection.effectiveType,
361
+ downlinkMbps: connection.downlink,
362
+ rttMs: connection.rtt,
363
+ saveData: connection.saveData
364
+ } : {}
365
+ };
366
+ } catch (e) {
367
+ return null;
368
+ }
369
+ }
370
+
371
+ // src/app-context.ts
372
+ var MAX_KEYS = 30;
373
+ var MAX_VALUE_LENGTH = 200;
374
+ var appContext = {};
375
+ var release;
376
+ function coerce(value) {
377
+ if (typeof value === "string") return value.slice(0, MAX_VALUE_LENGTH);
378
+ if (value === null || value === void 0) return "";
379
+ try {
380
+ return JSON.stringify(value).slice(0, MAX_VALUE_LENGTH);
381
+ } catch (e) {
382
+ return String(value).slice(0, MAX_VALUE_LENGTH);
383
+ }
384
+ }
385
+ function setContext(key, value) {
386
+ try {
387
+ if (!key) return;
388
+ if (value === null || value === void 0) {
389
+ delete appContext[key];
390
+ return;
391
+ }
392
+ if (!(key in appContext) && Object.keys(appContext).length >= MAX_KEYS) return;
393
+ appContext[key] = coerce(value);
394
+ } catch (e) {
395
+ }
396
+ }
397
+ function setContexts(values) {
398
+ try {
399
+ for (const [key, value] of Object.entries(values != null ? values : {})) setContext(key, value);
400
+ } catch (e) {
401
+ }
402
+ }
403
+ function getAppContext() {
404
+ return { ...appContext };
405
+ }
406
+ function clearAppContext() {
407
+ appContext = {};
408
+ }
409
+ function setRelease(value) {
410
+ if (value) release = value;
411
+ }
412
+ function getRelease() {
413
+ if (release) return release;
414
+ try {
415
+ const fromEnv = process.env.NEXT_PUBLIC_APP_VERSION || process.env.NEXT_PUBLIC_RELEASE || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA;
416
+ return fromEnv || void 0;
417
+ } catch (e) {
418
+ return void 0;
419
+ }
420
+ }
421
+
198
422
  // src/utils.ts
199
423
  var SENSITIVE_PARAMS = [
200
424
  "token",
@@ -263,7 +487,10 @@ function captureContext(visitedPages) {
263
487
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
264
488
  visitedPages: visitedPages.map(sanitizeUrl),
265
489
  breadcrumbs: getBreadcrumbs(),
266
- deviceInfo: captureDeviceInfo()
490
+ deviceInfo: captureDeviceInfo(),
491
+ runtime: captureRuntimeInfo(),
492
+ appContext: getAppContext(),
493
+ release: getRelease()
267
494
  };
268
495
  } catch (e) {
269
496
  return {
@@ -272,8 +499,40 @@ function captureContext(visitedPages) {
272
499
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
273
500
  visitedPages: [],
274
501
  breadcrumbs: [],
275
- deviceInfo: null
502
+ deviceInfo: null,
503
+ runtime: null,
504
+ appContext: {}
505
+ };
506
+ }
507
+ }
508
+ function contextMetadata(context) {
509
+ var _a;
510
+ try {
511
+ const runtime = context.runtime;
512
+ const appContext2 = Object.entries((_a = context.appContext) != null ? _a : {}).reduce(
513
+ (acc, [key, value]) => {
514
+ acc[`ctx_${key}`] = value;
515
+ return acc;
516
+ },
517
+ {}
518
+ );
519
+ return {
520
+ ...context.release ? { release: context.release } : {},
521
+ ...runtime ? {
522
+ timeOnPageMs: String(runtime.timeOnPageMs),
523
+ errorCount: String(runtime.errorCount),
524
+ visibility: runtime.visibility,
525
+ ...runtime.jsHeapUsedMb !== void 0 ? { jsHeapUsedMb: String(runtime.jsHeapUsedMb) } : {},
526
+ ...runtime.jsHeapLimitMb !== void 0 ? { jsHeapLimitMb: String(runtime.jsHeapLimitMb) } : {},
527
+ ...runtime.connectionType ? { connectionType: runtime.connectionType } : {},
528
+ ...runtime.downlinkMbps !== void 0 ? { downlinkMbps: String(runtime.downlinkMbps) } : {},
529
+ ...runtime.rttMs !== void 0 ? { rttMs: String(runtime.rttMs) } : {},
530
+ ...runtime.saveData !== void 0 ? { saveData: String(runtime.saveData) } : {}
531
+ } : {},
532
+ ...appContext2
276
533
  };
534
+ } catch (e) {
535
+ return {};
277
536
  }
278
537
  }
279
538
  var DEFAULT_BASE_URL = "https://glitchgrab.dev";
@@ -323,6 +582,41 @@ async function sendReport(payload, baseUrl) {
323
582
  return null;
324
583
  }
325
584
  }
585
+ async function sendFeedback(payload, baseUrl) {
586
+ var _a;
587
+ try {
588
+ const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/sdk/feedback`;
589
+ const body = JSON.stringify(payload);
590
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
591
+ try {
592
+ const response = await fetch(url, {
593
+ method: "POST",
594
+ headers: {
595
+ "Content-Type": "application/json",
596
+ Authorization: `Bearer ${payload.token}`
597
+ },
598
+ body
599
+ });
600
+ if (response.ok) {
601
+ const envelope = await response.json();
602
+ return { success: envelope.success, ...(_a = envelope.data) != null ? _a : {} };
603
+ }
604
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
605
+ return null;
606
+ }
607
+ } catch (e) {
608
+ }
609
+ if (attempt < MAX_RETRIES - 1) {
610
+ await new Promise(
611
+ (resolve) => setTimeout(resolve, RETRY_BASE_MS * Math.pow(2, attempt))
612
+ );
613
+ }
614
+ }
615
+ return null;
616
+ } catch (e) {
617
+ return null;
618
+ }
619
+ }
326
620
  async function enhanceText(text, token, baseUrl, screenshot, context) {
327
621
  var _a;
328
622
  try {
@@ -412,11 +706,12 @@ function stripUrlQuery(url) {
412
706
  }
413
707
  }
414
708
  function computeSignature(params) {
415
- var _a, _b;
709
+ var _a, _b, _c, _d;
416
710
  const msg = (_b = (_a = params.errorMessage) == null ? void 0 : _a.trim()) != null ? _b : "";
417
711
  const page = stripUrlQuery(params.pageUrl);
418
712
  const frame = topStackFrame(params.errorStack);
419
- return hashString(`${msg}|${page}|${frame}`);
713
+ const digest = (_d = (_c = params.digest) == null ? void 0 : _c.trim()) != null ? _d : "";
714
+ return hashString(digest ? `${msg}|${page}|${frame}|${digest}` : `${msg}|${page}|${frame}`);
420
715
  }
421
716
  function shouldSkipDuplicate(signature, windowMs = DEFAULT_WINDOW_MS, now = Date.now()) {
422
717
  const last = seen.get(signature);
@@ -441,6 +736,78 @@ function clearDedupCache() {
441
736
  seen.clear();
442
737
  }
443
738
 
739
+ // src/capture.ts
740
+ var config = null;
741
+ function registerCaptureConfig(next) {
742
+ config = next;
743
+ }
744
+ function matchesIgnorePatterns(message, patterns) {
745
+ if (!patterns || patterns.length === 0) return false;
746
+ return patterns.some(
747
+ (pattern) => pattern instanceof RegExp ? pattern.test(message) : message.includes(pattern)
748
+ );
749
+ }
750
+ function sessionMetadata(session) {
751
+ return {
752
+ ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
753
+ ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
754
+ ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
755
+ ...(session == null ? void 0 : session.phone) ? { sessionUserPhone: String(session.phone) } : {}
756
+ };
757
+ }
758
+ function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureError") {
759
+ var _a, _b, _c;
760
+ const err = error instanceof Error ? error : void 0;
761
+ const errorMessage = err ? err.message : String(error);
762
+ if (!errorMessage) return null;
763
+ if (matchesIgnorePatterns(errorMessage, cfg.ignoreErrors)) return null;
764
+ incrementErrorCount();
765
+ const context = captureContext(cfg.getVisitedPages());
766
+ const digest = (_a = options == null ? void 0 : options.digest) != null ? _a : typeof (error == null ? void 0 : error.digest) === "string" ? error.digest : void 0;
767
+ const sig = computeSignature({
768
+ errorMessage,
769
+ pageUrl: context.url,
770
+ errorStack: err == null ? void 0 : err.stack,
771
+ digest
772
+ });
773
+ if (shouldSkipDuplicate(sig)) return null;
774
+ return {
775
+ token: cfg.token,
776
+ source: "SDK_AUTO",
777
+ type: "BUG",
778
+ errorMessage,
779
+ errorStack: err == null ? void 0 : err.stack,
780
+ componentStack: (_b = options == null ? void 0 : options.componentStack) != null ? _b : void 0,
781
+ pageUrl: context.url,
782
+ userAgent: context.userAgent,
783
+ breadcrumbs: context.breadcrumbs,
784
+ deviceInfo: (_c = context.deviceInfo) != null ? _c : void 0,
785
+ metadata: {
786
+ timestamp: context.timestamp,
787
+ visitedPages: JSON.stringify(context.visitedPages),
788
+ capturedBy,
789
+ ...contextMetadata(context),
790
+ ...(options == null ? void 0 : options.boundary) ? { boundary: options.boundary } : {},
791
+ ...digest ? { digest } : {},
792
+ ...sessionMetadata(cfg.session),
793
+ ...options == null ? void 0 : options.metadata
794
+ }
795
+ };
796
+ }
797
+ function captureError(error, options) {
798
+ try {
799
+ const cfg = config;
800
+ if (!(cfg == null ? void 0 : cfg.token)) return;
801
+ const payload = buildCapturedErrorPayload(error, options, cfg);
802
+ if (!payload) return;
803
+ void sendReport(payload, cfg.baseUrl).then((result) => {
804
+ if (result && cfg.onReportSent) cfg.onReportSent(result);
805
+ });
806
+ if (cfg.onError && error instanceof Error) cfg.onError(error);
807
+ } catch (e) {
808
+ }
809
+ }
810
+
444
811
  // src/error-boundary.tsx
445
812
  var GlitchgrabErrorBoundary = class extends React.Component {
446
813
  constructor(props) {
@@ -451,40 +818,22 @@ var GlitchgrabErrorBoundary = class extends React.Component {
451
818
  return { hasError: true };
452
819
  }
453
820
  componentDidCatch(error, errorInfo) {
454
- var _a, _b;
821
+ var _a;
455
822
  try {
456
- const context = captureContext(this.props.visitedPages);
457
- const sig = computeSignature({
458
- errorMessage: error.message,
459
- pageUrl: context.url,
460
- errorStack: error.stack
461
- });
462
- if (shouldSkipDuplicate(sig)) {
463
- if (this.props.onError) this.props.onError(error);
464
- return;
823
+ const payload = buildCapturedErrorPayload(
824
+ error,
825
+ { componentStack: (_a = errorInfo.componentStack) != null ? _a : void 0 },
826
+ {
827
+ token: this.props.token,
828
+ baseUrl: this.props.baseUrl,
829
+ session: this.props.session,
830
+ getVisitedPages: () => this.props.visitedPages
831
+ },
832
+ "GlitchgrabErrorBoundary"
833
+ );
834
+ if (payload) {
835
+ sendReport(payload, this.props.baseUrl);
465
836
  }
466
- const session = this.props.session;
467
- const payload = {
468
- token: this.props.token,
469
- source: "SDK_AUTO",
470
- type: "BUG",
471
- errorMessage: error.message,
472
- errorStack: error.stack,
473
- componentStack: (_a = errorInfo.componentStack) != null ? _a : void 0,
474
- pageUrl: context.url,
475
- userAgent: context.userAgent,
476
- breadcrumbs: context.breadcrumbs,
477
- deviceInfo: (_b = context.deviceInfo) != null ? _b : void 0,
478
- metadata: {
479
- timestamp: context.timestamp,
480
- visitedPages: JSON.stringify(context.visitedPages),
481
- ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
482
- ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
483
- ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
484
- ...(session == null ? void 0 : session.phone) ? { sessionUserPhone: String(session.phone) } : {}
485
- }
486
- };
487
- sendReport(payload, this.props.baseUrl);
488
837
  if (this.props.onError) {
489
838
  this.props.onError(error);
490
839
  }
@@ -502,13 +851,41 @@ var GlitchgrabErrorBoundary = class extends React.Component {
502
851
  }
503
852
  };
504
853
 
505
- // src/report-dialog.tsx
506
- var import_react2 = require("react");
507
- var import_react_dom = require("react-dom");
508
-
509
- // src/annotation-canvas.tsx
854
+ // ../report-ui/dist/index.mjs
510
855
  var import_react = require("react");
856
+ var import_react_dom = require("react-dom");
857
+ var import_react2 = require("react");
511
858
  var import_jsx_runtime = require("react/jsx-runtime");
859
+ var import_jsx_runtime2 = require("react/jsx-runtime");
860
+ var MAX_SCREENSHOT_DATA_URL_LENGTH = 18e5;
861
+ var QUALITY_LADDER = [0.92, 0.85, 0.75];
862
+ var DOWNSCALE_FACTOR = 0.7;
863
+ function downscale(canvas, factor) {
864
+ const next = document.createElement("canvas");
865
+ next.width = Math.max(1, Math.round(canvas.width * factor));
866
+ next.height = Math.max(1, Math.round(canvas.height * factor));
867
+ const ctx = next.getContext("2d");
868
+ if (!ctx) return null;
869
+ ctx.imageSmoothingEnabled = true;
870
+ ctx.imageSmoothingQuality = "high";
871
+ ctx.drawImage(canvas, 0, 0, next.width, next.height);
872
+ return next;
873
+ }
874
+ function encodeScreenshot(canvas, maxLength = MAX_SCREENSHOT_DATA_URL_LENGTH) {
875
+ let current = canvas;
876
+ for (let round = 0; round < 2; round++) {
877
+ let encoded = "";
878
+ for (const quality of QUALITY_LADDER) {
879
+ encoded = current.toDataURL("image/jpeg", quality);
880
+ if (encoded.length <= maxLength) return encoded;
881
+ }
882
+ if (round === 1) return encoded;
883
+ const smaller = downscale(current, DOWNSCALE_FACTOR);
884
+ if (!smaller) return encoded;
885
+ current = smaller;
886
+ }
887
+ return current.toDataURL("image/jpeg", QUALITY_LADDER[0]);
888
+ }
512
889
  var PALETTE = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#a855f7", "#ffffff"];
513
890
  function drawStroke(ctx, s) {
514
891
  ctx.strokeStyle = s.color;
@@ -528,15 +905,15 @@ function drawStroke(ctx, s) {
528
905
  }
529
906
  }
530
907
  function AnnotationCanvas({ imageSrc, onSave, onCancel }) {
531
- const canvasRef = (0, import_react.useRef)(null);
532
- const imgRef = (0, import_react.useRef)(null);
533
- const [ready, setReady] = (0, import_react.useState)(false);
534
- const [tool, setTool] = (0, import_react.useState)("pen");
535
- const [color, setColor] = (0, import_react.useState)(PALETTE[0]);
536
- const [strokeWidth, setStrokeWidth] = (0, import_react.useState)(4);
537
- const [strokes, setStrokes] = (0, import_react.useState)([]);
538
- const [currentStroke, setCurrentStroke] = (0, import_react.useState)(null);
539
- (0, import_react.useEffect)(() => {
908
+ const canvasRef = (0, import_react2.useRef)(null);
909
+ const imgRef = (0, import_react2.useRef)(null);
910
+ const [ready, setReady] = (0, import_react2.useState)(false);
911
+ const [tool, setTool] = (0, import_react2.useState)("pen");
912
+ const [color, setColor] = (0, import_react2.useState)(PALETTE[0]);
913
+ const [strokeWidth, setStrokeWidth] = (0, import_react2.useState)(4);
914
+ const [strokes, setStrokes] = (0, import_react2.useState)([]);
915
+ const [currentStroke, setCurrentStroke] = (0, import_react2.useState)(null);
916
+ (0, import_react2.useEffect)(() => {
540
917
  const img = new Image();
541
918
  img.onload = () => {
542
919
  imgRef.current = img;
@@ -549,7 +926,7 @@ function AnnotationCanvas({ imageSrc, onSave, onCancel }) {
549
926
  };
550
927
  img.src = imageSrc;
551
928
  }, [imageSrc]);
552
- (0, import_react.useEffect)(() => {
929
+ (0, import_react2.useEffect)(() => {
553
930
  if (!ready) return;
554
931
  const canvas = canvasRef.current;
555
932
  const img = imgRef.current;
@@ -594,7 +971,7 @@ function AnnotationCanvas({ imageSrc, onSave, onCancel }) {
594
971
  const handleSave = () => {
595
972
  const canvas = canvasRef.current;
596
973
  if (!canvas) return;
597
- onSave(canvas.toDataURL("image/jpeg", 0.85));
974
+ onSave(encodeScreenshot(canvas));
598
975
  };
599
976
  const iconBtn = (active = false) => ({
600
977
  width: "30px",
@@ -784,8 +1161,6 @@ function AnnotationCanvas({ imageSrc, onSave, onCancel }) {
784
1161
  }
785
1162
  );
786
1163
  }
787
-
788
- // src/shortcut.ts
789
1164
  var GLITCHGRAB_SHORTCUT = "Ctrl+Shift+G";
790
1165
  var GLITCHGRAB_SHORTCUT_MAC = "\u2318\u21E7G";
791
1166
  function isMac() {
@@ -799,12 +1174,38 @@ function isMac() {
799
1174
  function getShortcutLabel() {
800
1175
  return isMac() ? GLITCHGRAB_SHORTCUT_MAC : GLITCHGRAB_SHORTCUT;
801
1176
  }
802
- function matchesShortcut(e) {
803
- return (e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "g";
804
- }
805
-
806
- // src/report-dialog.tsx
807
- var import_jsx_runtime2 = require("react/jsx-runtime");
1177
+ var ATTACHMENT_ACCEPT = [
1178
+ "image/*",
1179
+ // text — inlined into the issue body
1180
+ ".html",
1181
+ ".htm",
1182
+ ".txt",
1183
+ ".md",
1184
+ ".json",
1185
+ ".xml",
1186
+ ".log",
1187
+ ".yml",
1188
+ ".yaml",
1189
+ ".csv",
1190
+ ".har",
1191
+ ".js",
1192
+ ".mjs",
1193
+ ".cjs",
1194
+ ".ts",
1195
+ ".tsx",
1196
+ ".jsx",
1197
+ ".css",
1198
+ ".scss",
1199
+ ".py",
1200
+ ".sh",
1201
+ // binary documents
1202
+ ".pdf",
1203
+ ".doc",
1204
+ ".docx",
1205
+ ".xls",
1206
+ ".xlsx",
1207
+ ".zip"
1208
+ ].join(",");
808
1209
  function useIsDark() {
809
1210
  if (typeof window === "undefined") return true;
810
1211
  try {
@@ -917,6 +1318,98 @@ function detectHostAccent() {
917
1318
  return null;
918
1319
  }
919
1320
  }
1321
+ function initialsOf(name) {
1322
+ return name.trim().split(/\s+/).slice(0, 2).map((part) => {
1323
+ var _a, _b;
1324
+ return (_b = (_a = part[0]) == null ? void 0 : _a.toUpperCase()) != null ? _b : "";
1325
+ }).join("");
1326
+ }
1327
+ function ReporterChip({
1328
+ reporter,
1329
+ t
1330
+ }) {
1331
+ var _a;
1332
+ const [avatarFailed, setAvatarFailed] = (0, import_react.useState)(false);
1333
+ const avatarUrl = (reporter == null ? void 0 : reporter.avatarUrl) || "";
1334
+ (0, import_react.useEffect)(() => {
1335
+ setAvatarFailed(false);
1336
+ }, [avatarUrl]);
1337
+ const name = ((_a = reporter == null ? void 0 : reporter.name) == null ? void 0 : _a.trim()) || "";
1338
+ const isAnonymous = !name;
1339
+ const initials = isAnonymous ? "" : initialsOf(name);
1340
+ const label = isAnonymous ? "Anonymous" : name;
1341
+ const title = isAnonymous ? "No reporter identity was passed to Glitchgrab \u2014 this report won't be attributed to anyone" : [name, reporter == null ? void 0 : reporter.email, reporter == null ? void 0 : reporter.role].filter(Boolean).join(" \xB7 ");
1342
+ const showAvatarImage = !!avatarUrl && !avatarFailed;
1343
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1344
+ "span",
1345
+ {
1346
+ title,
1347
+ style: { display: "flex", alignItems: "center", gap: "6px", minWidth: 0 },
1348
+ children: [
1349
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1350
+ "span",
1351
+ {
1352
+ "aria-hidden": "true",
1353
+ style: {
1354
+ width: "18px",
1355
+ height: "18px",
1356
+ flexShrink: 0,
1357
+ borderRadius: "50%",
1358
+ overflow: "hidden",
1359
+ display: "flex",
1360
+ alignItems: "center",
1361
+ justifyContent: "center",
1362
+ fontSize: "8px",
1363
+ fontWeight: 700,
1364
+ letterSpacing: "0.02em",
1365
+ background: isAnonymous ? t.inputBg : t.accent,
1366
+ color: isAnonymous ? t.textMuted : t.accentText,
1367
+ border: `1px solid ${isAnonymous ? t.inputBorder : "transparent"}`
1368
+ },
1369
+ children: showAvatarImage ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1370
+ "img",
1371
+ {
1372
+ src: avatarUrl,
1373
+ alt: "",
1374
+ width: 18,
1375
+ height: 18,
1376
+ onError: () => setAvatarFailed(true),
1377
+ style: { width: "100%", height: "100%", objectFit: "cover" }
1378
+ }
1379
+ ) : initials ? initials : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "10", height: "10", viewBox: "0 0 16 16", fill: "none", children: [
1380
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "8", cy: "5.5", r: "2.75", stroke: "currentColor", strokeWidth: "1.4" }),
1381
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1382
+ "path",
1383
+ {
1384
+ d: "M2.75 13.5c0-2.35 2.35-3.75 5.25-3.75s5.25 1.4 5.25 3.75",
1385
+ stroke: "currentColor",
1386
+ strokeWidth: "1.4",
1387
+ strokeLinecap: "round"
1388
+ }
1389
+ )
1390
+ ] })
1391
+ }
1392
+ ),
1393
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1394
+ "span",
1395
+ {
1396
+ style: {
1397
+ fontSize: "11px",
1398
+ color: t.textMuted,
1399
+ overflow: "hidden",
1400
+ textOverflow: "ellipsis",
1401
+ whiteSpace: "nowrap"
1402
+ },
1403
+ children: [
1404
+ label,
1405
+ (reporter == null ? void 0 : reporter.role) ? ` \xB7 ${reporter.role}` : ""
1406
+ ]
1407
+ }
1408
+ )
1409
+ ]
1410
+ }
1411
+ );
1412
+ }
920
1413
  function getTheme(dark) {
921
1414
  const hostAccent = detectHostAccent();
922
1415
  const defaults = dark ? {
@@ -986,48 +1479,91 @@ function isLowQualityText(text) {
986
1479
  }
987
1480
  return null;
988
1481
  }
1482
+ async function captureViaHtml2Canvas() {
1483
+ try {
1484
+ const { default: html2canvas } = await import("html2canvas-pro");
1485
+ const scale = Math.min(Math.max(window.devicePixelRatio || 1, 1), 2);
1486
+ const canvas = await html2canvas(document.body, {
1487
+ scale,
1488
+ logging: false,
1489
+ useCORS: true,
1490
+ allowTaint: true,
1491
+ x: window.scrollX,
1492
+ y: window.scrollY,
1493
+ width: window.innerWidth,
1494
+ height: window.innerHeight,
1495
+ windowWidth: window.innerWidth,
1496
+ windowHeight: window.innerHeight
1497
+ });
1498
+ return encodeScreenshot(canvas);
1499
+ } catch (e) {
1500
+ return null;
1501
+ }
1502
+ }
989
1503
  function ReportDialog({
990
1504
  report,
1505
+ sendFeedback: sendFeedback2,
991
1506
  enhanceText: enhanceText2,
992
1507
  transcribeAudio: transcribeAudio2,
993
1508
  types,
994
- showSeverity = true
1509
+ showSeverity = true,
1510
+ captureScreenshot = captureViaHtml2Canvas,
1511
+ reporter
995
1512
  }) {
996
- const [isEnhancing, setIsEnhancing] = (0, import_react2.useState)(false);
997
- const [isEnhanced, setIsEnhanced] = (0, import_react2.useState)(false);
998
- const [originalDescription, setOriginalDescription] = (0, import_react2.useState)(
1513
+ const [isEnhancing, setIsEnhancing] = (0, import_react.useState)(false);
1514
+ const [isEnhanced, setIsEnhanced] = (0, import_react.useState)(false);
1515
+ const [originalDescription, setOriginalDescription] = (0, import_react.useState)(
999
1516
  null
1000
1517
  );
1001
- const [isListening, setIsListening] = (0, import_react2.useState)(false);
1002
- const [isTranscribing, setIsTranscribing] = (0, import_react2.useState)(false);
1003
- const [voiceError, setVoiceError] = (0, import_react2.useState)(null);
1004
- const [screenshotDragOver, setScreenshotDragOver] = (0, import_react2.useState)(false);
1005
- const [mounted, setMounted] = (0, import_react2.useState)(false);
1006
- (0, import_react2.useEffect)(() => {
1518
+ const [isListening, setIsListening] = (0, import_react.useState)(false);
1519
+ const [isTranscribing, setIsTranscribing] = (0, import_react.useState)(false);
1520
+ const [voiceError, setVoiceError] = (0, import_react.useState)(null);
1521
+ const [screenshotDragOver, setScreenshotDragOver] = (0, import_react.useState)(false);
1522
+ const [mounted, setMounted] = (0, import_react.useState)(false);
1523
+ (0, import_react.useEffect)(() => {
1007
1524
  setMounted(true);
1008
1525
  }, []);
1009
- const [isOpen, setIsOpen] = (0, import_react2.useState)(false);
1010
- const [description, setDescription] = (0, import_react2.useState)("");
1011
- const [isSubmitting, setIsSubmitting] = (0, import_react2.useState)(false);
1012
- const [submitted, setSubmitted] = (0, import_react2.useState)(false);
1013
- const [screenshots, setScreenshots] = (0, import_react2.useState)([]);
1014
- const [attachments, setAttachments] = (0, import_react2.useState)([]);
1015
- const [previewIndex, setPreviewIndex] = (0, import_react2.useState)(null);
1016
- const [annotatingIndex, setAnnotatingIndex] = (0, import_react2.useState)(null);
1017
- const fileInputRef = (0, import_react2.useRef)(null);
1018
- const modalRef = (0, import_react2.useRef)(null);
1019
- const previewImgRef = (0, import_react2.useRef)(null);
1020
- const textareaRef = (0, import_react2.useRef)(null);
1021
- const recognitionRef = (0, import_react2.useRef)(null);
1022
- const voiceBaseRef = (0, import_react2.useRef)("");
1023
- const usingWebSpeechRef = (0, import_react2.useRef)(false);
1024
- const textBeforeVoiceRef = (0, import_react2.useRef)("");
1025
- const mediaRecorderRef = (0, import_react2.useRef)(null);
1026
- const streamRef = (0, import_react2.useRef)(null);
1027
- const sarvamChunksRef = (0, import_react2.useRef)([]);
1028
- const spaceTimerRef = (0, import_react2.useRef)(null);
1029
- const isPushToTalkRef = (0, import_react2.useRef)(false);
1030
- (0, import_react2.useEffect)(() => {
1526
+ const [isOpen, setIsOpen] = (0, import_react.useState)(false);
1527
+ const [description, setDescription] = (0, import_react.useState)("");
1528
+ const [isSubmitting, setIsSubmitting] = (0, import_react.useState)(false);
1529
+ const [submitted, setSubmitted] = (0, import_react.useState)(false);
1530
+ const [screenshots, setScreenshots] = (0, import_react.useState)([]);
1531
+ const [attachments, setAttachments] = (0, import_react.useState)([]);
1532
+ const [previewIndex, setPreviewIndex] = (0, import_react.useState)(null);
1533
+ const [annotatingIndex, setAnnotatingIndex] = (0, import_react.useState)(null);
1534
+ const [previewZoomed, setPreviewZoomed] = (0, import_react.useState)(false);
1535
+ const [previewNatural, setPreviewNatural] = (0, import_react.useState)(null);
1536
+ const previewScrollRef = (0, import_react.useRef)(null);
1537
+ const previewImgElRef = (0, import_react.useRef)(null);
1538
+ const panRef = (0, import_react.useRef)(null);
1539
+ const zoomAnchorRef = (0, import_react.useRef)(null);
1540
+ const resetPreviewZoom = () => {
1541
+ setPreviewZoomed(false);
1542
+ setPreviewNatural(null);
1543
+ zoomAnchorRef.current = null;
1544
+ };
1545
+ const openPreview = (i) => {
1546
+ resetPreviewZoom();
1547
+ setPreviewIndex(i);
1548
+ };
1549
+ const closePreview = () => {
1550
+ resetPreviewZoom();
1551
+ setPreviewIndex(null);
1552
+ };
1553
+ const fileInputRef = (0, import_react.useRef)(null);
1554
+ const modalRef = (0, import_react.useRef)(null);
1555
+ const previewImgRef = (0, import_react.useRef)(null);
1556
+ const textareaRef = (0, import_react.useRef)(null);
1557
+ const recognitionRef = (0, import_react.useRef)(null);
1558
+ const voiceBaseRef = (0, import_react.useRef)("");
1559
+ const usingWebSpeechRef = (0, import_react.useRef)(false);
1560
+ const textBeforeVoiceRef = (0, import_react.useRef)("");
1561
+ const mediaRecorderRef = (0, import_react.useRef)(null);
1562
+ const streamRef = (0, import_react.useRef)(null);
1563
+ const sarvamChunksRef = (0, import_react.useRef)([]);
1564
+ const spaceTimerRef = (0, import_react.useRef)(null);
1565
+ const isPushToTalkRef = (0, import_react.useRef)(false);
1566
+ (0, import_react.useEffect)(() => {
1031
1567
  if (!isOpen) return;
1032
1568
  const dialogs = Array.from(
1033
1569
  document.querySelectorAll(
@@ -1037,22 +1573,69 @@ function ReportDialog({
1037
1573
  dialogs.forEach((d) => d.setAttribute("inert", ""));
1038
1574
  return () => dialogs.forEach((d) => d.removeAttribute("inert"));
1039
1575
  }, [isOpen]);
1040
- const [step, setStep] = (0, import_react2.useState)(1);
1041
- const [reportType, setReportType] = (0, import_react2.useState)("BUG");
1042
- const [severity, setSeverity] = (0, import_react2.useState)("medium");
1043
- const [validationError, setValidationError] = (0, import_react2.useState)(null);
1044
- const [placeholderIdx, setPlaceholderIdx] = (0, import_react2.useState)(0);
1576
+ const [step, setStep] = (0, import_react.useState)(1);
1577
+ const [reportType, setReportType] = (0, import_react.useState)("BUG");
1578
+ const [severity, setSeverity] = (0, import_react.useState)("medium");
1579
+ const [validationError, setValidationError] = (0, import_react.useState)(null);
1580
+ const [placeholderIdx, setPlaceholderIdx] = (0, import_react.useState)(0);
1581
+ const [rating, setRating] = (0, import_react.useState)(0);
1582
+ const [hoveredStar, setHoveredStar] = (0, import_react.useState)(0);
1583
+ const isRating = reportType === "RATING";
1584
+ const submitDisabled = isSubmitting || (isRating ? rating < 1 : !description.trim());
1045
1585
  const availableTypes = types != null ? types : [
1046
1586
  "BUG",
1047
1587
  "FEATURE_REQUEST",
1048
1588
  "UI_IMPROVEMENT",
1049
1589
  "PERFORMANCE",
1050
1590
  "SECURITY",
1051
- "QUESTION",
1052
- "OTHER"
1591
+ "QUESTION"
1053
1592
  ];
1593
+ const tileGridRef = (0, import_react.useRef)(null);
1594
+ (0, import_react.useEffect)(() => {
1595
+ if (!isOpen || step !== 1) return;
1596
+ const onKey = (e) => {
1597
+ const el = e.target;
1598
+ const tag = el == null ? void 0 : el.tagName;
1599
+ if (tag === "INPUT" || tag === "TEXTAREA" || (el == null ? void 0 : el.isContentEditable)) return;
1600
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
1601
+ if (sendFeedback2 && /^[1-5]$/.test(e.key)) {
1602
+ e.preventDefault();
1603
+ setRating(Number(e.key));
1604
+ setReportType("RATING");
1605
+ setStep(2);
1606
+ return;
1607
+ }
1608
+ const letter = e.key.toLowerCase();
1609
+ if (!/^[a-z]$/.test(letter)) return;
1610
+ const match = availableTypes.find(
1611
+ (type) => getTypeLabel(type).toLowerCase().startsWith(letter)
1612
+ );
1613
+ if (match) {
1614
+ e.preventDefault();
1615
+ setReportType(match);
1616
+ setStep(2);
1617
+ }
1618
+ };
1619
+ window.addEventListener("keydown", onKey);
1620
+ return () => window.removeEventListener("keydown", onKey);
1621
+ }, [isOpen, step, sendFeedback2, availableTypes.join(",")]);
1622
+ const handleTileGridKeyDown = (e) => {
1623
+ var _a, _b, _c;
1624
+ const keys = ["ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp"];
1625
+ if (!keys.includes(e.key)) return;
1626
+ const tiles = Array.from(
1627
+ (_b = (_a = tileGridRef.current) == null ? void 0 : _a.querySelectorAll("button[data-gg-type]")) != null ? _b : []
1628
+ );
1629
+ if (tiles.length === 0) return;
1630
+ e.preventDefault();
1631
+ const current = tiles.indexOf(document.activeElement);
1632
+ const delta = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : e.key === "ArrowDown" ? 2 : -2;
1633
+ const next = current === -1 ? 0 : Math.min(Math.max(current + delta, 0), tiles.length - 1);
1634
+ (_c = tiles[next]) == null ? void 0 : _c.focus();
1635
+ };
1054
1636
  const isDark = useIsDark();
1055
1637
  const t = getTheme(isDark);
1638
+ const canZoomPreview = previewNatural !== null && typeof window !== "undefined" && (previewNatural.w > window.innerWidth - 32 || previewNatural.h > window.innerHeight - 80);
1056
1639
  const handleFileUpload = (e) => {
1057
1640
  const files = e.target.files;
1058
1641
  if (!files || files.length === 0) return;
@@ -1077,56 +1660,17 @@ function ReportDialog({
1077
1660
  reader.readAsDataURL(file);
1078
1661
  });
1079
1662
  };
1080
- const retakeScreenshot = async () => {
1081
- setPreviewIndex(null);
1082
- setIsOpen(false);
1083
- setScreenshots([]);
1084
- await new Promise((r) => setTimeout(r, 300));
1085
- try {
1086
- const { default: html2canvas } = await import("html2canvas-pro");
1087
- const canvas = await html2canvas(document.body, {
1088
- scale: 0.5,
1089
- logging: false,
1090
- useCORS: true,
1091
- allowTaint: true,
1092
- x: window.scrollX,
1093
- y: window.scrollY,
1094
- width: window.innerWidth,
1095
- height: window.innerHeight,
1096
- windowWidth: window.innerWidth,
1097
- windowHeight: window.innerHeight
1098
- });
1099
- setScreenshots([canvas.toDataURL("image/jpeg", 0.6)]);
1100
- } catch (e) {
1101
- }
1102
- setIsOpen(true);
1103
- };
1104
1663
  const handleOpen = async () => {
1105
1664
  setSubmitted(false);
1106
1665
  if (availableTypes.length === 1) {
1107
1666
  setReportType(availableTypes[0]);
1108
1667
  setStep(2);
1109
1668
  }
1110
- try {
1111
- const { default: html2canvas } = await import("html2canvas-pro");
1112
- const canvas = await html2canvas(document.body, {
1113
- scale: 0.5,
1114
- logging: false,
1115
- useCORS: true,
1116
- allowTaint: true,
1117
- x: window.scrollX,
1118
- y: window.scrollY,
1119
- width: window.innerWidth,
1120
- height: window.innerHeight,
1121
- windowWidth: window.innerWidth,
1122
- windowHeight: window.innerHeight
1123
- });
1124
- setScreenshots([canvas.toDataURL("image/jpeg", 0.6)]);
1125
- } catch (e) {
1126
- }
1669
+ const shot = await captureScreenshot();
1670
+ if (shot) setScreenshots([shot]);
1127
1671
  setIsOpen(true);
1128
1672
  };
1129
- (0, import_react2.useEffect)(() => {
1673
+ (0, import_react.useEffect)(() => {
1130
1674
  const handler = (e) => {
1131
1675
  if (isOpen) return;
1132
1676
  const detail = e.detail;
@@ -1140,54 +1684,115 @@ function ReportDialog({
1140
1684
  window.addEventListener("glitchgrab:open-report", handler);
1141
1685
  return () => window.removeEventListener("glitchgrab:open-report", handler);
1142
1686
  }, [isOpen, handleOpen]);
1143
- (0, import_react2.useEffect)(() => {
1687
+ (0, import_react.useEffect)(() => {
1144
1688
  if (!isOpen) return;
1145
1689
  const handleEsc = (e) => {
1146
1690
  if (e.key === "Escape") {
1147
- if (previewIndex !== null) setPreviewIndex(null);
1691
+ if (previewZoomed) setPreviewZoomed(false);
1692
+ else if (previewIndex !== null) closePreview();
1148
1693
  else handleClose();
1149
1694
  }
1150
1695
  };
1151
1696
  document.addEventListener("keydown", handleEsc);
1152
1697
  return () => document.removeEventListener("keydown", handleEsc);
1153
- }, [isOpen, previewIndex]);
1154
- (0, import_react2.useEffect)(() => {
1698
+ }, [isOpen, previewIndex, previewZoomed]);
1699
+ (0, import_react.useEffect)(() => {
1155
1700
  if (previewIndex === null || annotatingIndex !== null) return;
1156
1701
  const handlePointerDown = (e) => {
1157
1702
  var _a;
1158
1703
  if (!((_a = previewImgRef.current) == null ? void 0 : _a.contains(e.target))) {
1159
- setPreviewIndex(null);
1704
+ closePreview();
1160
1705
  }
1161
1706
  };
1162
1707
  document.addEventListener("pointerdown", handlePointerDown, true);
1163
1708
  return () => document.removeEventListener("pointerdown", handlePointerDown, true);
1164
1709
  }, [previewIndex, annotatingIndex]);
1165
- (0, import_react2.useEffect)(() => {
1710
+ (0, import_react.useEffect)(() => {
1711
+ var _a;
1712
+ const el = previewScrollRef.current;
1713
+ if (!previewZoomed || !el) return;
1714
+ const anchor = (_a = zoomAnchorRef.current) != null ? _a : { fx: 0.5, fy: 0.5 };
1715
+ el.scrollLeft = anchor.fx * el.scrollWidth - el.clientWidth / 2;
1716
+ el.scrollTop = anchor.fy * el.scrollHeight - el.clientHeight / 2;
1717
+ }, [previewZoomed]);
1718
+ const togglePreviewZoom = (e) => {
1719
+ var _a;
1720
+ e.stopPropagation();
1721
+ if ((_a = panRef.current) == null ? void 0 : _a.moved) return;
1722
+ if (!previewZoomed && !canZoomPreview) return;
1723
+ const img = previewImgElRef.current;
1724
+ if (!previewZoomed && img) {
1725
+ const rect = img.getBoundingClientRect();
1726
+ zoomAnchorRef.current = {
1727
+ fx: (e.clientX - rect.left) / rect.width,
1728
+ fy: (e.clientY - rect.top) / rect.height
1729
+ };
1730
+ }
1731
+ setPreviewZoomed((z) => !z);
1732
+ };
1733
+ const handlePanStart = (e) => {
1734
+ const el = previewScrollRef.current;
1735
+ if (!previewZoomed || !el) return;
1736
+ panRef.current = {
1737
+ x: e.clientX,
1738
+ y: e.clientY,
1739
+ left: el.scrollLeft,
1740
+ top: el.scrollTop,
1741
+ moved: false
1742
+ };
1743
+ el.setPointerCapture(e.pointerId);
1744
+ };
1745
+ const handlePanMove = (e) => {
1746
+ const el = previewScrollRef.current;
1747
+ const start = panRef.current;
1748
+ if (!el || !start) return;
1749
+ const dx = e.clientX - start.x;
1750
+ const dy = e.clientY - start.y;
1751
+ if (Math.abs(dx) > 4 || Math.abs(dy) > 4) start.moved = true;
1752
+ el.scrollLeft = start.left - dx;
1753
+ el.scrollTop = start.top - dy;
1754
+ };
1755
+ const handlePanEnd = (e) => {
1756
+ var _a, _b;
1757
+ const el = previewScrollRef.current;
1758
+ const moved = (_b = (_a = panRef.current) == null ? void 0 : _a.moved) != null ? _b : false;
1759
+ try {
1760
+ el == null ? void 0 : el.releasePointerCapture(e.pointerId);
1761
+ } catch (e2) {
1762
+ }
1763
+ if (moved) {
1764
+ panRef.current = { x: 0, y: 0, left: 0, top: 0, moved: true };
1765
+ setTimeout(() => {
1766
+ panRef.current = null;
1767
+ }, 0);
1768
+ } else {
1769
+ panRef.current = null;
1770
+ }
1771
+ };
1772
+ (0, import_react.useEffect)(() => {
1166
1773
  if (!isOpen) return;
1167
1774
  const handlePaste = (e) => {
1168
1775
  var _a;
1169
1776
  try {
1170
1777
  const items = (_a = e.clipboardData) == null ? void 0 : _a.items;
1171
1778
  if (!items) return;
1172
- let matched = false;
1779
+ const pasted = [];
1173
1780
  for (let i = 0; i < items.length; i++) {
1174
- if (items[i].type.startsWith("image/")) {
1175
- const file = items[i].getAsFile();
1176
- if (!file) continue;
1177
- matched = true;
1178
- const reader = new FileReader();
1179
- reader.onload = () => setScreenshots((prev) => [...prev, reader.result]);
1180
- reader.readAsDataURL(file);
1181
- }
1781
+ if (items[i].kind !== "file") continue;
1782
+ const file = items[i].getAsFile();
1783
+ if (file) pasted.push(file);
1784
+ }
1785
+ if (pasted.length > 0) {
1786
+ e.preventDefault();
1787
+ addFiles(pasted);
1182
1788
  }
1183
- if (matched) e.preventDefault();
1184
1789
  } catch (e2) {
1185
1790
  }
1186
1791
  };
1187
1792
  window.addEventListener("paste", handlePaste);
1188
1793
  return () => window.removeEventListener("paste", handlePaste);
1189
1794
  }, [isOpen]);
1190
- (0, import_react2.useEffect)(() => {
1795
+ (0, import_react.useEffect)(() => {
1191
1796
  if (!isOpen || !transcribeAudio2) return;
1192
1797
  const id = setInterval(() => {
1193
1798
  setPlaceholderIdx((i) => (i + 1) % 2);
@@ -1226,6 +1831,8 @@ function ReportDialog({
1226
1831
  stopVoice();
1227
1832
  setIsOpen(false);
1228
1833
  setStep(1);
1834
+ setRating(0);
1835
+ setHoveredStar(0);
1229
1836
  setReportType("BUG");
1230
1837
  setSeverity("medium");
1231
1838
  setValidationError(null);
@@ -1371,7 +1978,30 @@ function ReportDialog({
1371
1978
  };
1372
1979
  const handleSubmit = async () => {
1373
1980
  try {
1374
- if (!description.trim() || isSubmitting) return;
1981
+ if (isSubmitting) return;
1982
+ if (isRating) {
1983
+ if (rating < 1) {
1984
+ setValidationError("Pick a star rating first.");
1985
+ return;
1986
+ }
1987
+ if (!sendFeedback2) return;
1988
+ setIsSubmitting(true);
1989
+ const result2 = await sendFeedback2(rating, description.trim() || void 0);
1990
+ if (result2 == null ? void 0 : result2.success) {
1991
+ setSubmitted(true);
1992
+ setDescription("");
1993
+ setRating(0);
1994
+ setTimeout(() => {
1995
+ setSubmitted(false);
1996
+ handleClose();
1997
+ }, 2e3);
1998
+ } else {
1999
+ setValidationError("Could not send \u2014 please try again.");
2000
+ }
2001
+ setIsSubmitting(false);
2002
+ return;
2003
+ }
2004
+ if (!description.trim()) return;
1375
2005
  const qualityError = isLowQualityText(description);
1376
2006
  if (qualityError) {
1377
2007
  setValidationError(qualityError);
@@ -1386,6 +2016,8 @@ function ReportDialog({
1386
2016
  metadata.severity = severity;
1387
2017
  }
1388
2018
  const result = await report(
2019
+ // The RATING branch returned above, so anything reaching here is a real
2020
+ // ReportType — the widened tile union can't leak into the report API.
1389
2021
  reportType,
1390
2022
  description.trim(),
1391
2023
  Object.keys(metadata).length > 0 ? metadata : void 0
@@ -1429,6 +2061,18 @@ function ReportDialog({
1429
2061
  button[data-gg-type="SECURITY"] > svg{animation:gg-shield-pulse 2.2s ease-in-out infinite;transform-origin:top}
1430
2062
  button[data-gg-type="QUESTION"] > svg{animation:gg-question-shake 2s ease-in-out infinite}
1431
2063
  button[data-gg-type="OTHER"] > svg{animation:gg-chat-tilt 2.2s ease-in-out infinite}
2064
+ /* Lift on hover, settle on press. Cheaper and steadier than doing it
2065
+ through React state, and :focus-visible keeps the ring off mouse users
2066
+ while leaving it for the arrow-key path. */
2067
+ button[data-gg-type]{transition:transform .15s ease,border-color .15s ease,box-shadow .15s ease,background-color .15s ease}
2068
+ button[data-gg-type]:hover{transform:translateY(-2px);box-shadow:0 6px 16px rgba(0,0,0,.18)}
2069
+ button[data-gg-type]:active{transform:translateY(0) scale(.985);box-shadow:none}
2070
+ button[data-gg-type]:focus-visible{outline:2px solid ${t.accent};outline-offset:2px}
2071
+ @keyframes gg-star-pop{0%{transform:scale(.6);opacity:0}60%{transform:scale(1.12)}100%{transform:scale(1);opacity:1}}
2072
+ button[data-gg-star]{transition:transform .12s ease}
2073
+ button[data-gg-star]:hover{transform:scale(1.18)}
2074
+ button[data-gg-star]:active{transform:scale(.94)}
2075
+ button[data-gg-star]:focus-visible{outline:2px solid #f59e0b;outline-offset:3px;border-radius:4px}
1432
2076
  ` }),
1433
2077
  document.body
1434
2078
  ),
@@ -1460,7 +2104,7 @@ function ReportDialog({
1460
2104
  style: {
1461
2105
  position: "relative",
1462
2106
  zIndex: 2147483647,
1463
- width: "340px",
2107
+ width: "420px",
1464
2108
  maxWidth: "calc(100% - 32px)",
1465
2109
  maxHeight: "calc(100dvh - 32px)",
1466
2110
  display: "flex",
@@ -1670,7 +2314,7 @@ function ReportDialog({
1670
2314
  "div",
1671
2315
  {
1672
2316
  style: {
1673
- padding: "16px",
2317
+ padding: "18px",
1674
2318
  overflowY: "auto",
1675
2319
  flex: "1 1 auto",
1676
2320
  minHeight: 0
@@ -1691,13 +2335,145 @@ function ReportDialog({
1691
2335
  ]
1692
2336
  }
1693
2337
  ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2338
+ step === 1 && sendFeedback2 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2339
+ "div",
2340
+ {
2341
+ style: {
2342
+ border: "1px solid rgba(245,158,11,0.35)",
2343
+ background: "rgba(245,158,11,0.07)",
2344
+ borderRadius: "10px",
2345
+ padding: "14px 16px",
2346
+ marginBottom: "16px",
2347
+ display: "flex",
2348
+ alignItems: "center",
2349
+ justifyContent: "space-between",
2350
+ gap: "12px",
2351
+ flexWrap: "wrap"
2352
+ },
2353
+ onMouseLeave: () => setHoveredStar(0),
2354
+ children: [
2355
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2356
+ "span",
2357
+ {
2358
+ style: { fontSize: "13px", fontWeight: 600, color: t.text },
2359
+ children: "How are we doing?"
2360
+ }
2361
+ ),
2362
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { display: "flex", gap: "4px" }, children: [1, 2, 3, 4, 5].map((star) => {
2363
+ const filled = star <= (hoveredStar || rating);
2364
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2365
+ "button",
2366
+ {
2367
+ type: "button",
2368
+ "data-gg-star": star,
2369
+ "aria-label": `${star} star${star > 1 ? "s" : ""}`,
2370
+ onMouseEnter: () => setHoveredStar(star),
2371
+ onFocus: () => setHoveredStar(star),
2372
+ onClick: () => {
2373
+ setRating(star);
2374
+ setReportType("RATING");
2375
+ setStep(2);
2376
+ },
2377
+ style: {
2378
+ border: "none",
2379
+ background: "transparent",
2380
+ padding: 0,
2381
+ cursor: "pointer",
2382
+ lineHeight: 0
2383
+ },
2384
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "26", height: "26", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2385
+ "path",
2386
+ {
2387
+ d: "M12 3l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.4 6.8 19.1l1-5.8L3.5 9.2l5.9-.9L12 3z",
2388
+ fill: filled ? "#f59e0b" : "transparent",
2389
+ stroke: "#f59e0b",
2390
+ strokeWidth: "1.5",
2391
+ strokeLinejoin: "round"
2392
+ }
2393
+ ) })
2394
+ },
2395
+ star
2396
+ );
2397
+ }) })
2398
+ ]
2399
+ }
2400
+ ),
2401
+ step === 1 && sendFeedback2 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2402
+ "div",
2403
+ {
2404
+ style: {
2405
+ display: "flex",
2406
+ alignItems: "center",
2407
+ gap: "10px",
2408
+ marginBottom: "16px"
2409
+ },
2410
+ children: [
2411
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { flex: 1, height: "1px", background: t.border } }),
2412
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: "11px", color: t.textMuted }, children: "or report something" }),
2413
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { flex: 1, height: "1px", background: t.border } })
2414
+ ]
2415
+ }
2416
+ ),
2417
+ step === 1 && screenshots.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2418
+ "div",
2419
+ {
2420
+ style: {
2421
+ display: "flex",
2422
+ alignItems: "center",
2423
+ gap: "10px",
2424
+ marginBottom: "14px",
2425
+ padding: "8px 10px",
2426
+ border: `1px solid ${t.inputBorder}`,
2427
+ borderRadius: "8px",
2428
+ background: t.inputBg
2429
+ },
2430
+ children: [
2431
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2432
+ "img",
2433
+ {
2434
+ src: screenshots[0],
2435
+ alt: "",
2436
+ style: {
2437
+ width: "34px",
2438
+ height: "24px",
2439
+ objectFit: "cover",
2440
+ borderRadius: "4px",
2441
+ border: `1px solid ${t.border}`,
2442
+ flexShrink: 0
2443
+ }
2444
+ }
2445
+ ),
2446
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: "11px", color: t.textMuted, flex: 1 }, children: "Screenshot attached" }),
2447
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2448
+ "button",
2449
+ {
2450
+ type: "button",
2451
+ onClick: () => setScreenshots([]),
2452
+ "aria-label": "Remove screenshot",
2453
+ style: {
2454
+ border: "none",
2455
+ background: "transparent",
2456
+ color: t.textMuted,
2457
+ cursor: "pointer",
2458
+ fontSize: "15px",
2459
+ lineHeight: 1,
2460
+ padding: "2px 4px"
2461
+ },
2462
+ children: "\xD7"
2463
+ }
2464
+ )
2465
+ ]
2466
+ }
2467
+ ),
1694
2468
  step === 1 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1695
2469
  "div",
1696
2470
  {
2471
+ ref: tileGridRef,
2472
+ onKeyDown: handleTileGridKeyDown,
1697
2473
  style: {
1698
2474
  display: "grid",
1699
- gridTemplateColumns: "1fr 1fr",
1700
- gap: "8px"
2475
+ gridTemplateColumns: "1fr 1fr 1fr",
2476
+ gap: "10px"
1701
2477
  },
1702
2478
  children: availableTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1703
2479
  "button",
@@ -1712,15 +2488,14 @@ function ReportDialog({
1712
2488
  display: "flex",
1713
2489
  flexDirection: "column",
1714
2490
  alignItems: "center",
1715
- gap: "8px",
1716
- padding: "16px 8px",
2491
+ gap: "9px",
2492
+ padding: "18px 10px",
1717
2493
  borderRadius: "8px",
1718
2494
  border: `1px solid ${t.inputBorder}`,
1719
2495
  background: t.inputBg,
1720
2496
  cursor: "pointer",
1721
2497
  color: t.text,
1722
- fontFamily: "inherit",
1723
- transition: "border-color 0.15s ease"
2498
+ fontFamily: "inherit"
1724
2499
  },
1725
2500
  onMouseEnter: (e) => {
1726
2501
  e.currentTarget.style.borderColor = t.accent;
@@ -1729,17 +2504,17 @@ function ReportDialog({
1729
2504
  e.currentTarget.style.borderColor = t.inputBorder;
1730
2505
  },
1731
2506
  children: [
1732
- getTypeIcon(type, t.accent),
1733
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: "13px", fontWeight: 600 }, children: getTypeLabel(type) }),
2507
+ getTypeIcon(type, t.accent, 22),
1734
2508
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1735
2509
  "span",
1736
2510
  {
1737
2511
  style: {
1738
- fontSize: "11px",
1739
- color: t.textMuted,
1740
- lineHeight: "1.3"
2512
+ fontSize: "12px",
2513
+ fontWeight: 600,
2514
+ textAlign: "center",
2515
+ lineHeight: "1.2"
1741
2516
  },
1742
- children: getTypeSubtitle(type)
2517
+ children: getTypeLabel(type)
1743
2518
  }
1744
2519
  )
1745
2520
  ]
@@ -1749,6 +2524,77 @@ function ReportDialog({
1749
2524
  }
1750
2525
  ),
1751
2526
  step === 2 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2527
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2528
+ "button",
2529
+ {
2530
+ type: "button",
2531
+ onClick: () => setStep(1),
2532
+ style: {
2533
+ display: "inline-flex",
2534
+ alignItems: "center",
2535
+ gap: "6px",
2536
+ marginBottom: "10px",
2537
+ padding: "4px 10px 4px 8px",
2538
+ borderRadius: "999px",
2539
+ border: `1px solid ${isRating ? "rgba(245,158,11,0.4)" : t.inputBorder}`,
2540
+ background: isRating ? "rgba(245,158,11,0.1)" : t.inputBg,
2541
+ color: t.text,
2542
+ fontSize: "11px",
2543
+ fontWeight: 600,
2544
+ fontFamily: "inherit",
2545
+ cursor: "pointer"
2546
+ },
2547
+ children: [
2548
+ getTypeIcon(reportType, isRating ? "#f59e0b" : t.accent, 13),
2549
+ getTypeLabel(reportType),
2550
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: t.textMuted, fontWeight: 400 }, children: "\xB7 change" })
2551
+ ]
2552
+ }
2553
+ ),
2554
+ isRating && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2555
+ "div",
2556
+ {
2557
+ style: {
2558
+ display: "flex",
2559
+ gap: "6px",
2560
+ marginBottom: "12px"
2561
+ },
2562
+ onMouseLeave: () => setHoveredStar(0),
2563
+ children: [1, 2, 3, 4, 5].map((star) => {
2564
+ const filled = star <= (hoveredStar || rating);
2565
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2566
+ "button",
2567
+ {
2568
+ type: "button",
2569
+ "aria-label": `${star} star${star > 1 ? "s" : ""}`,
2570
+ onClick: () => {
2571
+ setRating(star);
2572
+ if (validationError) setValidationError(null);
2573
+ },
2574
+ onMouseEnter: () => setHoveredStar(star),
2575
+ style: {
2576
+ border: "none",
2577
+ background: "transparent",
2578
+ padding: 0,
2579
+ cursor: "pointer",
2580
+ lineHeight: 0
2581
+ },
2582
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "30", height: "30", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2583
+ "path",
2584
+ {
2585
+ d: "M12 3l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.4 6.8 19.1l1-5.8L3.5 9.2l5.9-.9L12 3z",
2586
+ fill: filled ? "#f59e0b" : "transparent",
2587
+ stroke: filled ? "#f59e0b" : t.inputBorder,
2588
+ strokeWidth: "1.5",
2589
+ strokeLinejoin: "round"
2590
+ }
2591
+ ) })
2592
+ },
2593
+ star
2594
+ );
2595
+ })
2596
+ }
2597
+ ),
1752
2598
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { position: "relative" }, children: [
1753
2599
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1754
2600
  "textarea",
@@ -2147,7 +2993,7 @@ function ReportDialog({
2147
2993
  children: voiceError
2148
2994
  }
2149
2995
  ),
2150
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginTop: "10px" }, children: [
2996
+ !isRating && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginTop: "10px" }, children: [
2151
2997
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2152
2998
  "span",
2153
2999
  {
@@ -2187,7 +3033,7 @@ function ReportDialog({
2187
3033
  {
2188
3034
  src,
2189
3035
  alt: `Screenshot ${i + 1}`,
2190
- onClick: () => setPreviewIndex(i),
3036
+ onClick: () => openPreview(i),
2191
3037
  style: {
2192
3038
  width: "100%",
2193
3039
  height: "100%",
@@ -2591,37 +3437,54 @@ function ReportDialog({
2591
3437
  {
2592
3438
  type: "button",
2593
3439
  onClick: handleSubmit,
2594
- disabled: !description.trim() || isSubmitting,
3440
+ disabled: submitDisabled,
2595
3441
  style: {
2596
3442
  marginTop: "12px",
2597
3443
  width: "100%",
2598
3444
  padding: "10px",
2599
3445
  borderRadius: "8px",
2600
3446
  border: "none",
2601
- backgroundColor: !description.trim() || isSubmitting ? t.bgSecondary : t.accent,
2602
- color: !description.trim() || isSubmitting ? t.textMuted : t.accentText,
3447
+ backgroundColor: submitDisabled ? t.bgSecondary : t.accent,
3448
+ color: submitDisabled ? t.textMuted : t.accentText,
2603
3449
  fontSize: "14px",
2604
3450
  fontWeight: 600,
2605
- cursor: !description.trim() || isSubmitting ? "not-allowed" : "pointer",
3451
+ cursor: submitDisabled ? "not-allowed" : "pointer",
2606
3452
  fontFamily: "inherit",
2607
3453
  transition: "background-color 0.15s ease"
2608
3454
  },
2609
- children: isSubmitting ? "Sending..." : "Send Report"
3455
+ children: isSubmitting ? "Sending..." : isRating ? "Send Rating" : "Send Report"
2610
3456
  }
2611
3457
  )
2612
3458
  ] })
2613
3459
  ] })
2614
3460
  }
2615
3461
  ),
2616
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3462
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2617
3463
  "div",
2618
3464
  {
2619
3465
  style: {
2620
3466
  padding: "8px 16px 10px",
2621
3467
  borderTop: `1px solid ${t.border}`,
2622
- textAlign: "center"
3468
+ display: "flex",
3469
+ alignItems: "center",
3470
+ justifyContent: "space-between",
3471
+ gap: "12px"
2623
3472
  },
2624
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: "11px", color: t.textMuted }, children: "Powered by Glitchgrab" })
3473
+ children: [
3474
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ReporterChip, { reporter, t }),
3475
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3476
+ "span",
3477
+ {
3478
+ style: {
3479
+ fontSize: "11px",
3480
+ color: t.textMuted,
3481
+ whiteSpace: "nowrap",
3482
+ flexShrink: 0
3483
+ },
3484
+ children: "Powered by Glitchgrab"
3485
+ }
3486
+ )
3487
+ ]
2625
3488
  }
2626
3489
  )
2627
3490
  ]
@@ -2636,7 +3499,7 @@ function ReportDialog({
2636
3499
  {
2637
3500
  ref: fileInputRef,
2638
3501
  type: "file",
2639
- accept: "image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv",
3502
+ accept: ATTACHMENT_ACCEPT,
2640
3503
  multiple: true,
2641
3504
  onChange: handleFileUpload,
2642
3505
  style: { display: "none" }
@@ -2660,7 +3523,7 @@ function ReportDialog({
2660
3523
  },
2661
3524
  onClick: (e) => {
2662
3525
  e.stopPropagation();
2663
- setPreviewIndex(null);
3526
+ closePreview();
2664
3527
  },
2665
3528
  onMouseDown: (e) => e.stopPropagation(),
2666
3529
  children: [
@@ -2668,22 +3531,59 @@ function ReportDialog({
2668
3531
  "div",
2669
3532
  {
2670
3533
  ref: previewImgRef,
2671
- style: { position: "relative", maxWidth: "100%" },
3534
+ style: {
3535
+ position: "relative",
3536
+ display: "flex",
3537
+ maxWidth: "100%",
3538
+ maxHeight: "calc(100vh - 80px)"
3539
+ },
2672
3540
  onClick: (e) => e.stopPropagation(),
2673
3541
  children: [
2674
3542
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2675
- "img",
3543
+ "div",
2676
3544
  {
2677
- src: screenshots[previewIndex],
2678
- alt: "Screenshot preview",
2679
- style: {
2680
- maxWidth: "100%",
2681
- maxHeight: "calc(100vh - 80px)",
3545
+ ref: previewScrollRef,
3546
+ onPointerDown: handlePanStart,
3547
+ onPointerMove: handlePanMove,
3548
+ onPointerUp: handlePanEnd,
3549
+ onPointerCancel: handlePanEnd,
3550
+ onClick: togglePreviewZoom,
3551
+ style: previewZoomed ? {
3552
+ // Fill the overlay so there is room to pan around a
3553
+ // full-resolution screenshot.
3554
+ width: "calc(100vw - 32px)",
3555
+ height: "calc(100vh - 80px)",
3556
+ overflow: "auto",
2682
3557
  borderRadius: "8px",
2683
- objectFit: "contain",
2684
- cursor: "default",
2685
- display: "block"
2686
- }
3558
+ touchAction: "none",
3559
+ cursor: "grab"
3560
+ } : { maxWidth: "100%", lineHeight: 0 },
3561
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3562
+ "img",
3563
+ {
3564
+ ref: previewImgElRef,
3565
+ src: screenshots[previewIndex],
3566
+ alt: "Screenshot preview",
3567
+ onLoad: (e) => setPreviewNatural({
3568
+ w: e.currentTarget.naturalWidth,
3569
+ h: e.currentTarget.naturalHeight
3570
+ }),
3571
+ style: previewZoomed ? {
3572
+ // Natural size — one image pixel per CSS pixel.
3573
+ maxWidth: "none",
3574
+ maxHeight: "none",
3575
+ display: "block",
3576
+ cursor: "zoom-out"
3577
+ } : {
3578
+ maxWidth: "100%",
3579
+ maxHeight: "calc(100vh - 80px)",
3580
+ borderRadius: "8px",
3581
+ objectFit: "contain",
3582
+ cursor: canZoomPreview ? "zoom-in" : "default",
3583
+ display: "block"
3584
+ }
3585
+ }
3586
+ )
2687
3587
  }
2688
3588
  ),
2689
3589
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
@@ -2747,7 +3647,11 @@ function ReportDialog({
2747
3647
  color: t.textMuted,
2748
3648
  fontSize: "12px"
2749
3649
  },
2750
- children: screenshots.length > 1 ? `${previewIndex + 1} / ${screenshots.length} \xB7 Click outside to close` : "Click outside to close"
3650
+ children: [
3651
+ screenshots.length > 1 ? `${previewIndex + 1} / ${screenshots.length}` : null,
3652
+ canZoomPreview ? previewZoomed ? "Drag to pan \xB7 click image to fit" : "Click image to zoom to full size" : null,
3653
+ previewZoomed ? "Esc to fit" : "Click outside to close"
3654
+ ].filter(Boolean).join(" \xB7 ")
2751
3655
  }
2752
3656
  )
2753
3657
  }
@@ -2777,6 +3681,8 @@ function ReportDialog({
2777
3681
  }
2778
3682
  function getTypeLabel(type) {
2779
3683
  switch (type) {
3684
+ case "RATING":
3685
+ return "Rating";
2780
3686
  case "BUG":
2781
3687
  return "Bug Report";
2782
3688
  case "FEATURE_REQUEST":
@@ -2793,25 +3699,8 @@ function getTypeLabel(type) {
2793
3699
  return "Other";
2794
3700
  }
2795
3701
  }
2796
- function getTypeSubtitle(type) {
2797
- switch (type) {
2798
- case "BUG":
2799
- return "Something isn't working";
2800
- case "FEATURE_REQUEST":
2801
- return "Suggest an improvement";
2802
- case "UI_IMPROVEMENT":
2803
- return "Something looks off";
2804
- case "PERFORMANCE":
2805
- return "Something is slow";
2806
- case "SECURITY":
2807
- return "Report a security concern";
2808
- case "QUESTION":
2809
- return "Ask a question";
2810
- case "OTHER":
2811
- return "General feedback";
2812
- }
2813
- }
2814
3702
  function getPlaceholder(type, idx = 0, hasVoice = false) {
3703
+ if (type === "RATING") return "What made it good \u2014 or what let you down? (optional)";
2815
3704
  if (hasVoice && idx === 1) return "Hold Space to speak \u2014 we'll transcribe it";
2816
3705
  switch (type) {
2817
3706
  case "BUG":
@@ -2839,6 +3728,16 @@ function getTypeIcon(type, color, size = 24) {
2839
3728
  style: { flexShrink: 0 }
2840
3729
  };
2841
3730
  switch (type) {
3731
+ case "RATING":
3732
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3733
+ "path",
3734
+ {
3735
+ d: "M12 3l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.4 6.8 19.1l1-5.8L3.5 9.2l5.9-.9L12 3z",
3736
+ stroke: color,
3737
+ strokeWidth: "1.5",
3738
+ strokeLinejoin: "round"
3739
+ }
3740
+ ) });
2842
3741
  case "BUG":
2843
3742
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2844
3743
  "path",
@@ -2931,6 +3830,112 @@ function getTypeIcon(type, color, size = 24) {
2931
3830
  }
2932
3831
  }
2933
3832
 
3833
+ // src/rejection.ts
3834
+ var DETAILS_MAX_LENGTH = 1e3;
3835
+ var MESSAGE_MAX_LENGTH = 200;
3836
+ function isUnactionableRejection(description) {
3837
+ if (description.stack) return false;
3838
+ const message = description.message.trim();
3839
+ return message === "" || message === "[object Object]";
3840
+ }
3841
+ function readString(value) {
3842
+ if (typeof value !== "string") return void 0;
3843
+ const trimmed = value.trim();
3844
+ if (trimmed === "" || trimmed === "[object Object]") return void 0;
3845
+ return trimmed.slice(0, MESSAGE_MAX_LENGTH);
3846
+ }
3847
+ function readScalar(value) {
3848
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
3849
+ return readString(value);
3850
+ }
3851
+ function readStack(reason) {
3852
+ try {
3853
+ const stack = reason == null ? void 0 : reason.stack;
3854
+ return typeof stack === "string" && stack.trim() !== "" ? stack : void 0;
3855
+ } catch (e) {
3856
+ return void 0;
3857
+ }
3858
+ }
3859
+ function serialize(reason) {
3860
+ try {
3861
+ const seen2 = /* @__PURE__ */ new WeakSet();
3862
+ const json = JSON.stringify(reason, (_key, value) => {
3863
+ if (typeof value === "bigint") return `${value}n`;
3864
+ if (typeof value === "object" && value !== null) {
3865
+ if (seen2.has(value)) return "[Circular]";
3866
+ seen2.add(value);
3867
+ }
3868
+ return value;
3869
+ });
3870
+ if (!json || json === "{}" || json === "null") return void 0;
3871
+ return json.slice(0, DETAILS_MAX_LENGTH);
3872
+ } catch (e) {
3873
+ return void 0;
3874
+ }
3875
+ }
3876
+ var SHAPE_MAX_KEYS = 5;
3877
+ function describeShape(reason) {
3878
+ try {
3879
+ const keys = Object.keys(reason);
3880
+ if (keys.length === 0) return "";
3881
+ const listed = keys.slice(0, SHAPE_MAX_KEYS).join(", ");
3882
+ const suffix = keys.length > SHAPE_MAX_KEYS ? ", \u2026" : "";
3883
+ return `Rejected object: ${listed}${suffix}`.slice(0, MESSAGE_MAX_LENGTH);
3884
+ } catch (e) {
3885
+ return "";
3886
+ }
3887
+ }
3888
+ function describeRejection(reason) {
3889
+ var _a, _b, _c, _d, _e, _f, _g, _h;
3890
+ try {
3891
+ if (reason instanceof Error) {
3892
+ return {
3893
+ message: reason.message || reason.name || "",
3894
+ stack: reason.stack
3895
+ };
3896
+ }
3897
+ const stack = readStack(reason);
3898
+ if (typeof reason === "string") {
3899
+ return { message: (_a = readString(reason)) != null ? _a : "", stack };
3900
+ }
3901
+ if (typeof reason === "number" || typeof reason === "boolean") {
3902
+ return { message: String(reason), stack };
3903
+ }
3904
+ if (reason === null || reason === void 0) {
3905
+ return { message: "" };
3906
+ }
3907
+ const source = reason;
3908
+ const nestedError = source.error;
3909
+ const message = (_h = (_g = (_f = (_e = (_d = (_c = (_b = readString(source.message)) != null ? _b : readString(nestedError)) != null ? _c : readString(nestedError == null ? void 0 : nestedError.message)) != null ? _d : readScalar(source.code)) != null ? _e : readString(source.name)) != null ? _f : readScalar(source.status)) != null ? _g : readString(source.statusText)) != null ? _h : "";
3910
+ const details = serialize(reason);
3911
+ return {
3912
+ message: message || describeShape(reason),
3913
+ stack,
3914
+ details
3915
+ };
3916
+ } catch (e) {
3917
+ return { message: "" };
3918
+ }
3919
+ }
3920
+
3921
+ // src/shortcut.ts
3922
+ var GLITCHGRAB_SHORTCUT2 = "Ctrl+Shift+G";
3923
+ var GLITCHGRAB_SHORTCUT_MAC2 = "\u2318\u21E7G";
3924
+ function isMac2() {
3925
+ try {
3926
+ if (typeof navigator === "undefined") return false;
3927
+ return /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent);
3928
+ } catch (e) {
3929
+ return false;
3930
+ }
3931
+ }
3932
+ function getShortcutLabel2() {
3933
+ return isMac2() ? GLITCHGRAB_SHORTCUT_MAC2 : GLITCHGRAB_SHORTCUT2;
3934
+ }
3935
+ function matchesShortcut(e) {
3936
+ return (e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "g";
3937
+ }
3938
+
2934
3939
  // src/provider.tsx
2935
3940
  var import_jsx_runtime3 = require("react/jsx-runtime");
2936
3941
  var DEFAULT_BASE_URL2 = "https://glitchgrab.dev";
@@ -2954,14 +3959,28 @@ function GlitchgrabProviderInner({
2954
3959
  fallback,
2955
3960
  types,
2956
3961
  showSeverity,
2957
- ignoreErrors
3962
+ ignoreErrors,
3963
+ release: release2,
3964
+ context: appContext2,
3965
+ responseBodyOrigins: responseBodyOrigins2
2958
3966
  }) {
2959
3967
  const visitedPagesRef = (0, import_react3.useRef)([]);
3968
+ setRelease(release2);
3969
+ if (appContext2) setContexts(appContext2);
3970
+ registerCaptureConfig({
3971
+ token,
3972
+ baseUrl,
3973
+ session,
3974
+ ignoreErrors,
3975
+ getVisitedPages: () => visitedPagesRef.current,
3976
+ onError,
3977
+ onReportSent
3978
+ });
2960
3979
  (0, import_react3.useEffect)(() => {
2961
3980
  if (enableBreadcrumbs) {
2962
- initBreadcrumbs(maxBreadcrumbs2);
3981
+ initBreadcrumbs(maxBreadcrumbs2, { responseBodyOrigins: responseBodyOrigins2 });
2963
3982
  }
2964
- }, [enableBreadcrumbs, maxBreadcrumbs2]);
3983
+ }, [enableBreadcrumbs, maxBreadcrumbs2, responseBodyOrigins2 == null ? void 0 : responseBodyOrigins2.join(",")]);
2965
3984
  (0, import_react3.useEffect)(() => {
2966
3985
  try {
2967
3986
  if (typeof window === "undefined") return;
@@ -3009,10 +4028,7 @@ function GlitchgrabProviderInner({
3009
4028
  ];
3010
4029
  const matchesIgnore = (message) => {
3011
4030
  if (KNOWN_NOISE_RE.some((pattern) => pattern.test(message))) return true;
3012
- if (!ignoreErrors || ignoreErrors.length === 0) return false;
3013
- return ignoreErrors.some(
3014
- (pattern) => pattern instanceof RegExp ? pattern.test(message) : message.includes(pattern)
3015
- );
4031
+ return matchesIgnorePatterns(message, ignoreErrors);
3016
4032
  };
3017
4033
  const handleError = (event) => {
3018
4034
  var _a, _b, _c, _d, _e, _f, _g;
@@ -3030,6 +4046,7 @@ function GlitchgrabProviderInner({
3030
4046
  if (event.filename && EXTENSION_ORIGIN_RE.test(event.filename) || ((_a = event.error) == null ? void 0 : _a.stack) && EXTENSION_ORIGIN_RE.test(event.error.stack)) {
3031
4047
  return;
3032
4048
  }
4049
+ incrementErrorCount();
3033
4050
  const context = captureContext(visitedPagesRef.current);
3034
4051
  const sig = computeSignature({
3035
4052
  errorMessage: event.message,
@@ -3050,6 +4067,7 @@ function GlitchgrabProviderInner({
3050
4067
  metadata: {
3051
4068
  timestamp: context.timestamp,
3052
4069
  visitedPages: JSON.stringify(context.visitedPages),
4070
+ ...contextMetadata(context),
3053
4071
  filename: (_e = event.filename) != null ? _e : "",
3054
4072
  lineno: String((_f = event.lineno) != null ? _f : ""),
3055
4073
  colno: String((_g = event.colno) != null ? _g : ""),
@@ -3069,17 +4087,20 @@ function GlitchgrabProviderInner({
3069
4087
  const handleRejection = (event) => {
3070
4088
  var _a;
3071
4089
  try {
3072
- const context = captureContext(visitedPagesRef.current);
3073
4090
  const reason = event.reason;
3074
- const errMsg = reason instanceof Error ? reason.message : String(reason);
4091
+ const description = describeRejection(reason);
4092
+ const errMsg = description.message;
4093
+ const errStack = description.stack;
3075
4094
  if (errMsg === "Script error." || errMsg === "Script error") {
3076
4095
  return;
3077
4096
  }
3078
4097
  if (matchesIgnore(errMsg)) {
3079
4098
  return;
3080
4099
  }
3081
- const errStack = reason instanceof Error ? reason.stack : void 0;
3082
4100
  if (errStack && EXTENSION_ORIGIN_RE.test(errStack)) return;
4101
+ if (isUnactionableRejection(description)) return;
4102
+ incrementErrorCount();
4103
+ const context = captureContext(visitedPagesRef.current);
3083
4104
  const sig = computeSignature({
3084
4105
  errorMessage: errMsg,
3085
4106
  pageUrl: context.url,
@@ -3099,7 +4120,9 @@ function GlitchgrabProviderInner({
3099
4120
  metadata: {
3100
4121
  timestamp: context.timestamp,
3101
4122
  visitedPages: JSON.stringify(context.visitedPages),
4123
+ ...contextMetadata(context),
3102
4124
  type: "unhandledrejection",
4125
+ ...description.details ? { rejectionReason: description.details } : {},
3103
4126
  ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
3104
4127
  ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
3105
4128
  ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
@@ -3139,6 +4162,7 @@ function GlitchgrabProviderInner({
3139
4162
  metadata: {
3140
4163
  timestamp: context.timestamp,
3141
4164
  visitedPages: JSON.stringify(context.visitedPages),
4165
+ ...contextMetadata(context),
3142
4166
  ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
3143
4167
  ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
3144
4168
  ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
@@ -3159,6 +4183,53 @@ function GlitchgrabProviderInner({
3159
4183
  (description, metadata) => report("BUG", description, metadata),
3160
4184
  [report]
3161
4185
  );
4186
+ const feedback = (0, import_react3.useCallback)(
4187
+ async (rating, message, metadata) => {
4188
+ try {
4189
+ const context = captureContext(visitedPagesRef.current);
4190
+ return await sendFeedback(
4191
+ {
4192
+ token,
4193
+ rating,
4194
+ message,
4195
+ pageUrl: context.url,
4196
+ userAgent: context.userAgent,
4197
+ metadata: {
4198
+ timestamp: context.timestamp,
4199
+ ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
4200
+ ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
4201
+ ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
4202
+ ...(session == null ? void 0 : session.phone) ? { sessionUserPhone: String(session.phone) } : {},
4203
+ ...metadata
4204
+ }
4205
+ },
4206
+ baseUrl
4207
+ );
4208
+ } catch (e) {
4209
+ return null;
4210
+ }
4211
+ },
4212
+ [token, baseUrl, session]
4213
+ );
4214
+ const openFeedbackDialog = (0, import_react3.useCallback)(() => {
4215
+ if (typeof window !== "undefined") {
4216
+ window.dispatchEvent(
4217
+ new CustomEvent("glitchgrab:open-report", { detail: { type: "RATING" } })
4218
+ );
4219
+ }
4220
+ }, []);
4221
+ const captureError2 = (0, import_react3.useCallback)(
4222
+ (error, options) => {
4223
+ captureError(error, options);
4224
+ },
4225
+ []
4226
+ );
4227
+ const setContext2 = (0, import_react3.useCallback)((key, value) => {
4228
+ setContext(key, value);
4229
+ }, []);
4230
+ const setContexts2 = (0, import_react3.useCallback)((values) => {
4231
+ setContexts(values);
4232
+ }, []);
3162
4233
  const addBreadcrumb2 = (0, import_react3.useCallback)(
3163
4234
  (message, data) => {
3164
4235
  addBreadcrumb("custom", message, data);
@@ -3210,9 +4281,19 @@ function GlitchgrabProviderInner({
3210
4281
  } catch (e) {
3211
4282
  }
3212
4283
  }, [openReportDialog]);
3213
- const [shortcutLabel, setShortcutLabel] = (0, import_react3.useState)(GLITCHGRAB_SHORTCUT);
4284
+ const reporter = (0, import_react3.useMemo)(() => {
4285
+ var _a;
4286
+ if (!session) return null;
4287
+ const avatar = (_a = session.avatarUrl) != null ? _a : session.image;
4288
+ return {
4289
+ name: session.name,
4290
+ email: session.email,
4291
+ avatarUrl: typeof avatar === "string" ? avatar : null
4292
+ };
4293
+ }, [session]);
4294
+ const [shortcutLabel, setShortcutLabel] = (0, import_react3.useState)(GLITCHGRAB_SHORTCUT2);
3214
4295
  (0, import_react3.useEffect)(() => {
3215
- setShortcutLabel(getShortcutLabel());
4296
+ setShortcutLabel(getShortcutLabel2());
3216
4297
  }, []);
3217
4298
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3218
4299
  GlitchgrabContext.Provider,
@@ -3222,6 +4303,11 @@ function GlitchgrabProviderInner({
3222
4303
  baseUrl: baseUrl != null ? baseUrl : DEFAULT_BASE_URL2,
3223
4304
  reportBug,
3224
4305
  report,
4306
+ captureError: captureError2,
4307
+ setContext: setContext2,
4308
+ setContexts: setContexts2,
4309
+ sendFeedback: feedback,
4310
+ openFeedbackDialog,
3225
4311
  addBreadcrumb: addBreadcrumb2,
3226
4312
  openReportDialog,
3227
4313
  enhanceText: enhance,
@@ -3240,7 +4326,7 @@ function GlitchgrabProviderInner({
3240
4326
  children
3241
4327
  }
3242
4328
  ),
3243
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ReportDialog, { report, enhanceText: enhance, transcribeAudio: transcribe, types, showSeverity })
4329
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ReportDialog, { report, sendFeedback: feedback, enhanceText: enhance, transcribeAudio: transcribe, types, showSeverity, reporter })
3244
4330
  ]
3245
4331
  }
3246
4332
  );
@@ -3350,6 +4436,91 @@ function ReportButton({
3350
4436
  );
3351
4437
  }
3352
4438
 
4439
+ // src/feedback-button.tsx
4440
+ var import_jsx_runtime5 = require("react/jsx-runtime");
4441
+ function useGlitchgrabSafe2() {
4442
+ try {
4443
+ return useGlitchgrab();
4444
+ } catch (e) {
4445
+ return null;
4446
+ }
4447
+ }
4448
+ function FeedbackButton({
4449
+ position = "bottom-left",
4450
+ label = "Feedback",
4451
+ className,
4452
+ children
4453
+ }) {
4454
+ var _a, _b;
4455
+ const glitchgrab = useGlitchgrabSafe2();
4456
+ if (!glitchgrab) return null;
4457
+ const handleClick = () => glitchgrab.openFeedbackDialog();
4458
+ if (children) {
4459
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: children({ onClick: handleClick }) });
4460
+ }
4461
+ const isTop = (_a = position == null ? void 0 : position.startsWith("top")) != null ? _a : false;
4462
+ const isLeft = (_b = position == null ? void 0 : position.endsWith("left")) != null ? _b : false;
4463
+ const positionStyles = {
4464
+ ...isLeft ? { left: "16px" } : { right: "16px" },
4465
+ ...isTop ? { top: "16px" } : { bottom: "16px" }
4466
+ };
4467
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
4468
+ "button",
4469
+ {
4470
+ type: "button",
4471
+ onClick: handleClick,
4472
+ className,
4473
+ style: {
4474
+ position: "fixed",
4475
+ ...positionStyles,
4476
+ zIndex: 99999,
4477
+ padding: "10px 18px",
4478
+ borderRadius: "24px",
4479
+ border: "none",
4480
+ backgroundColor: "#18181b",
4481
+ color: "#fafafa",
4482
+ fontSize: "14px",
4483
+ fontWeight: 500,
4484
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
4485
+ cursor: "pointer",
4486
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15), 0 1px 3px rgba(0, 0, 0, 0.1)",
4487
+ transition: "transform 0.15s ease, box-shadow 0.15s ease",
4488
+ display: "flex",
4489
+ alignItems: "center",
4490
+ gap: "6px"
4491
+ },
4492
+ onMouseEnter: (e) => {
4493
+ e.target.style.transform = "scale(1.05)";
4494
+ },
4495
+ onMouseLeave: (e) => {
4496
+ e.target.style.transform = "scale(1)";
4497
+ },
4498
+ "aria-label": label,
4499
+ children: [
4500
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
4501
+ "svg",
4502
+ {
4503
+ width: "16",
4504
+ height: "16",
4505
+ viewBox: "0 0 24 24",
4506
+ fill: "none",
4507
+ style: { flexShrink: 0 },
4508
+ "aria-hidden": "true",
4509
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
4510
+ "path",
4511
+ {
4512
+ d: "M12 2.5l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3 1.1-6.5L2.6 9.3l6.5-.9L12 2.5z",
4513
+ fill: "currentColor"
4514
+ }
4515
+ )
4516
+ }
4517
+ ),
4518
+ label
4519
+ ]
4520
+ }
4521
+ );
4522
+ }
4523
+
3353
4524
  // src/use-reports.ts
3354
4525
  var import_react5 = require("react");
3355
4526
  var BASE_URL = "https://glitchgrab.dev";
@@ -3455,8 +4626,70 @@ function useGlitchgrabActions({
3455
4626
  error
3456
4627
  };
3457
4628
  }
4629
+
4630
+ // src/use-feedback.ts
4631
+ var import_react6 = require("react");
4632
+ var DEFAULT_BASE_URL3 = "https://glitchgrab.dev";
4633
+ function buildUrl({
4634
+ approvedOnly,
4635
+ userId,
4636
+ minRating,
4637
+ limit = 50,
4638
+ baseUrl
4639
+ }) {
4640
+ const params = new URLSearchParams({ limit: String(limit) });
4641
+ if (approvedOnly) params.set("approved", "true");
4642
+ if (userId) params.set("reporterPrimaryKey", userId);
4643
+ if (minRating) params.set("minRating", String(minRating));
4644
+ return `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL3}/api/v1/sdk/feedback?${params.toString()}`;
4645
+ }
4646
+ async function fetchGlitchgrabFeedback(query) {
4647
+ var _a, _b;
4648
+ const res = await fetch(buildUrl(query), {
4649
+ headers: { Authorization: `Bearer ${query.token}` }
4650
+ });
4651
+ const data = await res.json();
4652
+ if (!data.success) throw new Error((_a = data.error) != null ? _a : "Failed to fetch feedback");
4653
+ return (_b = data.data) != null ? _b : [];
4654
+ }
4655
+ function useGlitchgrabFeedback(query) {
4656
+ const { token, approvedOnly, userId, minRating, limit, baseUrl } = query;
4657
+ const [feedback, setFeedback] = (0, import_react6.useState)([]);
4658
+ const [isLoading, setIsLoading] = (0, import_react6.useState)(true);
4659
+ const [isFetching, setIsFetching] = (0, import_react6.useState)(false);
4660
+ const [error, setError] = (0, import_react6.useState)(null);
4661
+ const load = (0, import_react6.useCallback)(async () => {
4662
+ if (!token) {
4663
+ setIsLoading(false);
4664
+ return;
4665
+ }
4666
+ setIsFetching(true);
4667
+ setError(null);
4668
+ try {
4669
+ const data = await fetchGlitchgrabFeedback({
4670
+ token,
4671
+ approvedOnly,
4672
+ userId,
4673
+ minRating,
4674
+ limit,
4675
+ baseUrl
4676
+ });
4677
+ setFeedback(data);
4678
+ } catch (e) {
4679
+ setError(e instanceof Error ? e.message : "Network error");
4680
+ } finally {
4681
+ setIsLoading(false);
4682
+ setIsFetching(false);
4683
+ }
4684
+ }, [token, approvedOnly, userId, minRating, limit, baseUrl]);
4685
+ (0, import_react6.useEffect)(() => {
4686
+ load();
4687
+ }, [load]);
4688
+ return { feedback, isLoading, isFetching, error, refetch: load };
4689
+ }
3458
4690
  // Annotate the CommonJS export names for ESM import in node:
3459
4691
  0 && (module.exports = {
4692
+ FeedbackButton,
3460
4693
  GLITCHGRAB_SHORTCUT,
3461
4694
  GLITCHGRAB_SHORTCUT_MAC,
3462
4695
  GlitchgrabErrorBoundary,
@@ -3465,20 +4698,28 @@ function useGlitchgrabActions({
3465
4698
  addBreadcrumb,
3466
4699
  captureContext,
3467
4700
  captureDeviceInfo,
4701
+ captureError,
4702
+ clearAppContext,
3468
4703
  clearBreadcrumbs,
3469
4704
  clearDedupCache,
3470
4705
  computeSignature,
3471
4706
  enhanceText,
4707
+ fetchGlitchgrabFeedback,
3472
4708
  fetchGlitchgrabReports,
4709
+ getAppContext,
3473
4710
  getBreadcrumbs,
3474
4711
  getShortcutLabel,
3475
4712
  initBreadcrumbs,
3476
4713
  matchesShortcut,
3477
4714
  sanitizeUrl,
4715
+ sendFeedback,
3478
4716
  sendReport,
4717
+ setContext,
4718
+ setContexts,
3479
4719
  shouldSkipDuplicate,
3480
4720
  useGlitchgrab,
3481
4721
  useGlitchgrabActions,
4722
+ useGlitchgrabFeedback,
3482
4723
  useGlitchgrabReports
3483
4724
  });
3484
4725
  //# sourceMappingURL=index.js.map