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.mjs CHANGED
@@ -7,24 +7,83 @@ import {
7
7
  useRef as useRef3,
8
8
  useEffect as useEffect3,
9
9
  useCallback,
10
+ useMemo,
10
11
  useState as useState3
11
12
  } from "react";
12
13
 
13
14
  // src/error-boundary.tsx
14
15
  import * as React from "react";
15
16
 
17
+ // src/redact.ts
18
+ 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;
19
+ var EMAIL_RE = /[\w.+-]+@[\w-]+\.[\w.-]+/g;
20
+ var JWT_RE = /\beyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]{8,}\b/g;
21
+ var BEARER_RE = /\b(bearer\s+)[\w-.~+/]{12,}=*/gi;
22
+ var MAX_BODY_LENGTH = 500;
23
+ function redactString(value) {
24
+ return value.replace(JWT_RE, "[REDACTED]").replace(BEARER_RE, "$1[REDACTED]").replace(EMAIL_RE, "[REDACTED_EMAIL]");
25
+ }
26
+ function redactValue(value, depth) {
27
+ if (depth > 4) return "[DEPTH]";
28
+ if (value === null || value === void 0) return value;
29
+ if (typeof value === "string") return redactString(value);
30
+ if (typeof value === "number" || typeof value === "boolean") return value;
31
+ if (Array.isArray(value)) return value.slice(0, 20).map((v) => redactValue(v, depth + 1));
32
+ if (typeof value === "object") {
33
+ const out = {};
34
+ for (const [key, inner] of Object.entries(value)) {
35
+ out[key] = SENSITIVE_KEY_RE.test(key) ? "[REDACTED]" : redactValue(inner, depth + 1);
36
+ }
37
+ return out;
38
+ }
39
+ return String(value);
40
+ }
41
+ function redactBody(body, maxLength = MAX_BODY_LENGTH) {
42
+ try {
43
+ const trimmed = body.trim();
44
+ if (!trimmed) return "";
45
+ let result;
46
+ try {
47
+ result = JSON.stringify(redactValue(JSON.parse(trimmed), 0));
48
+ } catch (e) {
49
+ result = redactString(trimmed);
50
+ }
51
+ return result.length > maxLength ? `${result.slice(0, maxLength)}\u2026[truncated]` : result;
52
+ } catch (e) {
53
+ return "";
54
+ }
55
+ }
56
+
16
57
  // src/breadcrumbs.ts
17
58
  var MAX_DEFAULT = 50;
18
59
  var breadcrumbs = [];
19
60
  var maxBreadcrumbs = MAX_DEFAULT;
20
61
  var initialized = false;
21
- function initBreadcrumbs(max) {
62
+ var responseBodyOrigins = [];
63
+ function mayCaptureBody(url) {
64
+ try {
65
+ if (typeof window === "undefined") return false;
66
+ const origin = new URL(url, window.location.href).origin;
67
+ if (origin === window.location.origin) return true;
68
+ return responseBodyOrigins.includes(origin);
69
+ } catch (e) {
70
+ return false;
71
+ }
72
+ }
73
+ function initBreadcrumbs(max, options) {
74
+ var _a;
75
+ if ((_a = options == null ? void 0 : options.responseBodyOrigins) == null ? void 0 : _a.length) {
76
+ responseBodyOrigins = Array.from(
77
+ /* @__PURE__ */ new Set([...responseBodyOrigins, ...options.responseBodyOrigins])
78
+ );
79
+ }
22
80
  if (initialized) return;
23
81
  maxBreadcrumbs = max != null ? max : MAX_DEFAULT;
24
82
  try {
25
83
  if (typeof window === "undefined") return;
26
84
  interceptConsole();
27
85
  interceptFetch();
86
+ interceptXhr();
28
87
  interceptNavigation();
29
88
  interceptClicks();
30
89
  initialized = true;
@@ -83,6 +142,16 @@ function argsToString(args) {
83
142
  return "[unknown]";
84
143
  }
85
144
  }
145
+ async function readErrorBody(response, url) {
146
+ try {
147
+ if (response.ok) return "";
148
+ if (!mayCaptureBody(url)) return "";
149
+ const text = await response.clone().text();
150
+ return redactBody(text);
151
+ } catch (e) {
152
+ return "";
153
+ }
154
+ }
86
155
  function interceptFetch() {
87
156
  const origFetch = window.fetch;
88
157
  window.fetch = async function(input, init) {
@@ -95,10 +164,12 @@ function interceptFetch() {
95
164
  input,
96
165
  init
97
166
  );
167
+ const body = await readErrorBody(response, url);
98
168
  addBreadcrumb("api", `${method} ${url.slice(0, 100)} \u2192 ${response.status}`, {
99
169
  method,
100
170
  status: String(response.status),
101
- duration: `${Date.now() - start}ms`
171
+ duration: `${Date.now() - start}ms`,
172
+ ...body ? { responseBody: body } : {}
102
173
  });
103
174
  return response;
104
175
  } catch (err) {
@@ -111,6 +182,67 @@ function interceptFetch() {
111
182
  }
112
183
  };
113
184
  }
185
+ function interceptXhr() {
186
+ if (typeof XMLHttpRequest === "undefined") return;
187
+ const origOpen = XMLHttpRequest.prototype.open;
188
+ const origSend = XMLHttpRequest.prototype.send;
189
+ XMLHttpRequest.prototype.open = function(method, url, ...rest) {
190
+ try {
191
+ this.__ggMethod = method;
192
+ this.__ggUrl = typeof url === "string" ? url : url.toString();
193
+ const record = () => {
194
+ var _a, _b, _c, _d;
195
+ try {
196
+ const recordedMethod = (_a = this.__ggMethod) != null ? _a : "GET";
197
+ const recordedUrl = ((_b = this.__ggUrl) != null ? _b : "").slice(0, 100);
198
+ const duration = `${Date.now() - ((_c = this.__ggStart) != null ? _c : Date.now())}ms`;
199
+ if (!this.status) {
200
+ addBreadcrumb("api", `${recordedMethod} ${recordedUrl} \u2192 FAILED`, {
201
+ method: recordedMethod,
202
+ duration
203
+ });
204
+ return;
205
+ }
206
+ const failed = this.status < 200 || this.status >= 300;
207
+ const body = failed && mayCaptureBody((_d = this.__ggUrl) != null ? _d : "") ? readXhrBody(this) : "";
208
+ addBreadcrumb("api", `${recordedMethod} ${recordedUrl} \u2192 ${this.status}`, {
209
+ method: recordedMethod,
210
+ status: String(this.status),
211
+ duration,
212
+ ...body ? { responseBody: body } : {}
213
+ });
214
+ } catch (e) {
215
+ }
216
+ };
217
+ if (!this.__ggListening) {
218
+ this.__ggListening = true;
219
+ this.addEventListener("loadend", record);
220
+ }
221
+ } catch (e) {
222
+ }
223
+ return origOpen.apply(this, [
224
+ method,
225
+ url,
226
+ ...rest
227
+ ]);
228
+ };
229
+ XMLHttpRequest.prototype.send = function(...args) {
230
+ try {
231
+ this.__ggStart = Date.now();
232
+ } catch (e) {
233
+ }
234
+ return origSend.apply(this, args);
235
+ };
236
+ }
237
+ function readXhrBody(xhr) {
238
+ try {
239
+ if (xhr.responseType && xhr.responseType !== "text" && xhr.responseType !== "json") return "";
240
+ const raw = xhr.responseType === "json" ? JSON.stringify(xhr.response) : xhr.responseText;
241
+ return raw ? redactBody(raw) : "";
242
+ } catch (e) {
243
+ return "";
244
+ }
245
+ }
114
246
  function interceptNavigation() {
115
247
  const origPush = history.pushState.bind(history);
116
248
  const origReplace = history.replaceState.bind(history);
@@ -145,6 +277,90 @@ function interceptClicks() {
145
277
  );
146
278
  }
147
279
 
280
+ // src/runtime.ts
281
+ var pageLoadedAt = Date.now();
282
+ var errorCount = 0;
283
+ function incrementErrorCount() {
284
+ errorCount += 1;
285
+ return errorCount;
286
+ }
287
+ var toMb = (bytes) => typeof bytes === "number" ? Math.round(bytes / 1048576 * 10) / 10 : void 0;
288
+ function captureRuntimeInfo() {
289
+ try {
290
+ if (typeof window === "undefined") return null;
291
+ const memory = performance == null ? void 0 : performance.memory;
292
+ const connection = navigator == null ? void 0 : navigator.connection;
293
+ return {
294
+ timeOnPageMs: Date.now() - pageLoadedAt,
295
+ errorCount,
296
+ visibility: typeof document !== "undefined" ? document.visibilityState : "unknown",
297
+ ...memory ? {
298
+ jsHeapUsedMb: toMb(memory.usedJSHeapSize),
299
+ jsHeapLimitMb: toMb(memory.jsHeapSizeLimit)
300
+ } : {},
301
+ ...connection ? {
302
+ connectionType: connection.effectiveType,
303
+ downlinkMbps: connection.downlink,
304
+ rttMs: connection.rtt,
305
+ saveData: connection.saveData
306
+ } : {}
307
+ };
308
+ } catch (e) {
309
+ return null;
310
+ }
311
+ }
312
+
313
+ // src/app-context.ts
314
+ var MAX_KEYS = 30;
315
+ var MAX_VALUE_LENGTH = 200;
316
+ var appContext = {};
317
+ var release;
318
+ function coerce(value) {
319
+ if (typeof value === "string") return value.slice(0, MAX_VALUE_LENGTH);
320
+ if (value === null || value === void 0) return "";
321
+ try {
322
+ return JSON.stringify(value).slice(0, MAX_VALUE_LENGTH);
323
+ } catch (e) {
324
+ return String(value).slice(0, MAX_VALUE_LENGTH);
325
+ }
326
+ }
327
+ function setContext(key, value) {
328
+ try {
329
+ if (!key) return;
330
+ if (value === null || value === void 0) {
331
+ delete appContext[key];
332
+ return;
333
+ }
334
+ if (!(key in appContext) && Object.keys(appContext).length >= MAX_KEYS) return;
335
+ appContext[key] = coerce(value);
336
+ } catch (e) {
337
+ }
338
+ }
339
+ function setContexts(values) {
340
+ try {
341
+ for (const [key, value] of Object.entries(values != null ? values : {})) setContext(key, value);
342
+ } catch (e) {
343
+ }
344
+ }
345
+ function getAppContext() {
346
+ return { ...appContext };
347
+ }
348
+ function clearAppContext() {
349
+ appContext = {};
350
+ }
351
+ function setRelease(value) {
352
+ if (value) release = value;
353
+ }
354
+ function getRelease() {
355
+ if (release) return release;
356
+ try {
357
+ const fromEnv = process.env.NEXT_PUBLIC_APP_VERSION || process.env.NEXT_PUBLIC_RELEASE || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA;
358
+ return fromEnv || void 0;
359
+ } catch (e) {
360
+ return void 0;
361
+ }
362
+ }
363
+
148
364
  // src/utils.ts
149
365
  var SENSITIVE_PARAMS = [
150
366
  "token",
@@ -213,7 +429,10 @@ function captureContext(visitedPages) {
213
429
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
214
430
  visitedPages: visitedPages.map(sanitizeUrl),
215
431
  breadcrumbs: getBreadcrumbs(),
216
- deviceInfo: captureDeviceInfo()
432
+ deviceInfo: captureDeviceInfo(),
433
+ runtime: captureRuntimeInfo(),
434
+ appContext: getAppContext(),
435
+ release: getRelease()
217
436
  };
218
437
  } catch (e) {
219
438
  return {
@@ -222,10 +441,42 @@ function captureContext(visitedPages) {
222
441
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
223
442
  visitedPages: [],
224
443
  breadcrumbs: [],
225
- deviceInfo: null
444
+ deviceInfo: null,
445
+ runtime: null,
446
+ appContext: {}
226
447
  };
227
448
  }
228
449
  }
450
+ function contextMetadata(context) {
451
+ var _a;
452
+ try {
453
+ const runtime = context.runtime;
454
+ const appContext2 = Object.entries((_a = context.appContext) != null ? _a : {}).reduce(
455
+ (acc, [key, value]) => {
456
+ acc[`ctx_${key}`] = value;
457
+ return acc;
458
+ },
459
+ {}
460
+ );
461
+ return {
462
+ ...context.release ? { release: context.release } : {},
463
+ ...runtime ? {
464
+ timeOnPageMs: String(runtime.timeOnPageMs),
465
+ errorCount: String(runtime.errorCount),
466
+ visibility: runtime.visibility,
467
+ ...runtime.jsHeapUsedMb !== void 0 ? { jsHeapUsedMb: String(runtime.jsHeapUsedMb) } : {},
468
+ ...runtime.jsHeapLimitMb !== void 0 ? { jsHeapLimitMb: String(runtime.jsHeapLimitMb) } : {},
469
+ ...runtime.connectionType ? { connectionType: runtime.connectionType } : {},
470
+ ...runtime.downlinkMbps !== void 0 ? { downlinkMbps: String(runtime.downlinkMbps) } : {},
471
+ ...runtime.rttMs !== void 0 ? { rttMs: String(runtime.rttMs) } : {},
472
+ ...runtime.saveData !== void 0 ? { saveData: String(runtime.saveData) } : {}
473
+ } : {},
474
+ ...appContext2
475
+ };
476
+ } catch (e) {
477
+ return {};
478
+ }
479
+ }
229
480
  var DEFAULT_BASE_URL = "https://glitchgrab.dev";
230
481
  var MAX_RETRIES = 3;
231
482
  var RETRY_BASE_MS = 500;
@@ -273,6 +524,41 @@ async function sendReport(payload, baseUrl) {
273
524
  return null;
274
525
  }
275
526
  }
527
+ async function sendFeedback(payload, baseUrl) {
528
+ var _a;
529
+ try {
530
+ const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/sdk/feedback`;
531
+ const body = JSON.stringify(payload);
532
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
533
+ try {
534
+ const response = await fetch(url, {
535
+ method: "POST",
536
+ headers: {
537
+ "Content-Type": "application/json",
538
+ Authorization: `Bearer ${payload.token}`
539
+ },
540
+ body
541
+ });
542
+ if (response.ok) {
543
+ const envelope = await response.json();
544
+ return { success: envelope.success, ...(_a = envelope.data) != null ? _a : {} };
545
+ }
546
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
547
+ return null;
548
+ }
549
+ } catch (e) {
550
+ }
551
+ if (attempt < MAX_RETRIES - 1) {
552
+ await new Promise(
553
+ (resolve) => setTimeout(resolve, RETRY_BASE_MS * Math.pow(2, attempt))
554
+ );
555
+ }
556
+ }
557
+ return null;
558
+ } catch (e) {
559
+ return null;
560
+ }
561
+ }
276
562
  async function enhanceText(text, token, baseUrl, screenshot, context) {
277
563
  var _a;
278
564
  try {
@@ -362,11 +648,12 @@ function stripUrlQuery(url) {
362
648
  }
363
649
  }
364
650
  function computeSignature(params) {
365
- var _a, _b;
651
+ var _a, _b, _c, _d;
366
652
  const msg = (_b = (_a = params.errorMessage) == null ? void 0 : _a.trim()) != null ? _b : "";
367
653
  const page = stripUrlQuery(params.pageUrl);
368
654
  const frame = topStackFrame(params.errorStack);
369
- return hashString(`${msg}|${page}|${frame}`);
655
+ const digest = (_d = (_c = params.digest) == null ? void 0 : _c.trim()) != null ? _d : "";
656
+ return hashString(digest ? `${msg}|${page}|${frame}|${digest}` : `${msg}|${page}|${frame}`);
370
657
  }
371
658
  function shouldSkipDuplicate(signature, windowMs = DEFAULT_WINDOW_MS, now = Date.now()) {
372
659
  const last = seen.get(signature);
@@ -391,6 +678,78 @@ function clearDedupCache() {
391
678
  seen.clear();
392
679
  }
393
680
 
681
+ // src/capture.ts
682
+ var config = null;
683
+ function registerCaptureConfig(next) {
684
+ config = next;
685
+ }
686
+ function matchesIgnorePatterns(message, patterns) {
687
+ if (!patterns || patterns.length === 0) return false;
688
+ return patterns.some(
689
+ (pattern) => pattern instanceof RegExp ? pattern.test(message) : message.includes(pattern)
690
+ );
691
+ }
692
+ function sessionMetadata(session) {
693
+ return {
694
+ ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
695
+ ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
696
+ ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
697
+ ...(session == null ? void 0 : session.phone) ? { sessionUserPhone: String(session.phone) } : {}
698
+ };
699
+ }
700
+ function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureError") {
701
+ var _a, _b, _c;
702
+ const err = error instanceof Error ? error : void 0;
703
+ const errorMessage = err ? err.message : String(error);
704
+ if (!errorMessage) return null;
705
+ if (matchesIgnorePatterns(errorMessage, cfg.ignoreErrors)) return null;
706
+ incrementErrorCount();
707
+ const context = captureContext(cfg.getVisitedPages());
708
+ const digest = (_a = options == null ? void 0 : options.digest) != null ? _a : typeof (error == null ? void 0 : error.digest) === "string" ? error.digest : void 0;
709
+ const sig = computeSignature({
710
+ errorMessage,
711
+ pageUrl: context.url,
712
+ errorStack: err == null ? void 0 : err.stack,
713
+ digest
714
+ });
715
+ if (shouldSkipDuplicate(sig)) return null;
716
+ return {
717
+ token: cfg.token,
718
+ source: "SDK_AUTO",
719
+ type: "BUG",
720
+ errorMessage,
721
+ errorStack: err == null ? void 0 : err.stack,
722
+ componentStack: (_b = options == null ? void 0 : options.componentStack) != null ? _b : void 0,
723
+ pageUrl: context.url,
724
+ userAgent: context.userAgent,
725
+ breadcrumbs: context.breadcrumbs,
726
+ deviceInfo: (_c = context.deviceInfo) != null ? _c : void 0,
727
+ metadata: {
728
+ timestamp: context.timestamp,
729
+ visitedPages: JSON.stringify(context.visitedPages),
730
+ capturedBy,
731
+ ...contextMetadata(context),
732
+ ...(options == null ? void 0 : options.boundary) ? { boundary: options.boundary } : {},
733
+ ...digest ? { digest } : {},
734
+ ...sessionMetadata(cfg.session),
735
+ ...options == null ? void 0 : options.metadata
736
+ }
737
+ };
738
+ }
739
+ function captureError(error, options) {
740
+ try {
741
+ const cfg = config;
742
+ if (!(cfg == null ? void 0 : cfg.token)) return;
743
+ const payload = buildCapturedErrorPayload(error, options, cfg);
744
+ if (!payload) return;
745
+ void sendReport(payload, cfg.baseUrl).then((result) => {
746
+ if (result && cfg.onReportSent) cfg.onReportSent(result);
747
+ });
748
+ if (cfg.onError && error instanceof Error) cfg.onError(error);
749
+ } catch (e) {
750
+ }
751
+ }
752
+
394
753
  // src/error-boundary.tsx
395
754
  var GlitchgrabErrorBoundary = class extends React.Component {
396
755
  constructor(props) {
@@ -401,40 +760,22 @@ var GlitchgrabErrorBoundary = class extends React.Component {
401
760
  return { hasError: true };
402
761
  }
403
762
  componentDidCatch(error, errorInfo) {
404
- var _a, _b;
763
+ var _a;
405
764
  try {
406
- const context = captureContext(this.props.visitedPages);
407
- const sig = computeSignature({
408
- errorMessage: error.message,
409
- pageUrl: context.url,
410
- errorStack: error.stack
411
- });
412
- if (shouldSkipDuplicate(sig)) {
413
- if (this.props.onError) this.props.onError(error);
414
- return;
765
+ const payload = buildCapturedErrorPayload(
766
+ error,
767
+ { componentStack: (_a = errorInfo.componentStack) != null ? _a : void 0 },
768
+ {
769
+ token: this.props.token,
770
+ baseUrl: this.props.baseUrl,
771
+ session: this.props.session,
772
+ getVisitedPages: () => this.props.visitedPages
773
+ },
774
+ "GlitchgrabErrorBoundary"
775
+ );
776
+ if (payload) {
777
+ sendReport(payload, this.props.baseUrl);
415
778
  }
416
- const session = this.props.session;
417
- const payload = {
418
- token: this.props.token,
419
- source: "SDK_AUTO",
420
- type: "BUG",
421
- errorMessage: error.message,
422
- errorStack: error.stack,
423
- componentStack: (_a = errorInfo.componentStack) != null ? _a : void 0,
424
- pageUrl: context.url,
425
- userAgent: context.userAgent,
426
- breadcrumbs: context.breadcrumbs,
427
- deviceInfo: (_b = context.deviceInfo) != null ? _b : void 0,
428
- metadata: {
429
- timestamp: context.timestamp,
430
- visitedPages: JSON.stringify(context.visitedPages),
431
- ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
432
- ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
433
- ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
434
- ...(session == null ? void 0 : session.phone) ? { sessionUserPhone: String(session.phone) } : {}
435
- }
436
- };
437
- sendReport(payload, this.props.baseUrl);
438
779
  if (this.props.onError) {
439
780
  this.props.onError(error);
440
781
  }
@@ -452,17 +793,45 @@ var GlitchgrabErrorBoundary = class extends React.Component {
452
793
  }
453
794
  };
454
795
 
455
- // src/report-dialog.tsx
796
+ // ../report-ui/dist/index.mjs
456
797
  import {
457
798
  useState as useState2,
458
799
  useRef as useRef2,
459
800
  useEffect as useEffect2
460
801
  } from "react";
461
802
  import { createPortal } from "react-dom";
462
-
463
- // src/annotation-canvas.tsx
464
803
  import { useEffect, useRef, useState } from "react";
465
804
  import { jsx, jsxs } from "react/jsx-runtime";
805
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
806
+ var MAX_SCREENSHOT_DATA_URL_LENGTH = 18e5;
807
+ var QUALITY_LADDER = [0.92, 0.85, 0.75];
808
+ var DOWNSCALE_FACTOR = 0.7;
809
+ function downscale(canvas, factor) {
810
+ const next = document.createElement("canvas");
811
+ next.width = Math.max(1, Math.round(canvas.width * factor));
812
+ next.height = Math.max(1, Math.round(canvas.height * factor));
813
+ const ctx = next.getContext("2d");
814
+ if (!ctx) return null;
815
+ ctx.imageSmoothingEnabled = true;
816
+ ctx.imageSmoothingQuality = "high";
817
+ ctx.drawImage(canvas, 0, 0, next.width, next.height);
818
+ return next;
819
+ }
820
+ function encodeScreenshot(canvas, maxLength = MAX_SCREENSHOT_DATA_URL_LENGTH) {
821
+ let current = canvas;
822
+ for (let round = 0; round < 2; round++) {
823
+ let encoded = "";
824
+ for (const quality of QUALITY_LADDER) {
825
+ encoded = current.toDataURL("image/jpeg", quality);
826
+ if (encoded.length <= maxLength) return encoded;
827
+ }
828
+ if (round === 1) return encoded;
829
+ const smaller = downscale(current, DOWNSCALE_FACTOR);
830
+ if (!smaller) return encoded;
831
+ current = smaller;
832
+ }
833
+ return current.toDataURL("image/jpeg", QUALITY_LADDER[0]);
834
+ }
466
835
  var PALETTE = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#a855f7", "#ffffff"];
467
836
  function drawStroke(ctx, s) {
468
837
  ctx.strokeStyle = s.color;
@@ -548,7 +917,7 @@ function AnnotationCanvas({ imageSrc, onSave, onCancel }) {
548
917
  const handleSave = () => {
549
918
  const canvas = canvasRef.current;
550
919
  if (!canvas) return;
551
- onSave(canvas.toDataURL("image/jpeg", 0.85));
920
+ onSave(encodeScreenshot(canvas));
552
921
  };
553
922
  const iconBtn = (active = false) => ({
554
923
  width: "30px",
@@ -738,8 +1107,6 @@ function AnnotationCanvas({ imageSrc, onSave, onCancel }) {
738
1107
  }
739
1108
  );
740
1109
  }
741
-
742
- // src/shortcut.ts
743
1110
  var GLITCHGRAB_SHORTCUT = "Ctrl+Shift+G";
744
1111
  var GLITCHGRAB_SHORTCUT_MAC = "\u2318\u21E7G";
745
1112
  function isMac() {
@@ -753,12 +1120,38 @@ function isMac() {
753
1120
  function getShortcutLabel() {
754
1121
  return isMac() ? GLITCHGRAB_SHORTCUT_MAC : GLITCHGRAB_SHORTCUT;
755
1122
  }
756
- function matchesShortcut(e) {
757
- return (e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "g";
758
- }
759
-
760
- // src/report-dialog.tsx
761
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1123
+ var ATTACHMENT_ACCEPT = [
1124
+ "image/*",
1125
+ // text — inlined into the issue body
1126
+ ".html",
1127
+ ".htm",
1128
+ ".txt",
1129
+ ".md",
1130
+ ".json",
1131
+ ".xml",
1132
+ ".log",
1133
+ ".yml",
1134
+ ".yaml",
1135
+ ".csv",
1136
+ ".har",
1137
+ ".js",
1138
+ ".mjs",
1139
+ ".cjs",
1140
+ ".ts",
1141
+ ".tsx",
1142
+ ".jsx",
1143
+ ".css",
1144
+ ".scss",
1145
+ ".py",
1146
+ ".sh",
1147
+ // binary documents
1148
+ ".pdf",
1149
+ ".doc",
1150
+ ".docx",
1151
+ ".xls",
1152
+ ".xlsx",
1153
+ ".zip"
1154
+ ].join(",");
762
1155
  function useIsDark() {
763
1156
  if (typeof window === "undefined") return true;
764
1157
  try {
@@ -871,6 +1264,98 @@ function detectHostAccent() {
871
1264
  return null;
872
1265
  }
873
1266
  }
1267
+ function initialsOf(name) {
1268
+ return name.trim().split(/\s+/).slice(0, 2).map((part) => {
1269
+ var _a, _b;
1270
+ return (_b = (_a = part[0]) == null ? void 0 : _a.toUpperCase()) != null ? _b : "";
1271
+ }).join("");
1272
+ }
1273
+ function ReporterChip({
1274
+ reporter,
1275
+ t
1276
+ }) {
1277
+ var _a;
1278
+ const [avatarFailed, setAvatarFailed] = useState2(false);
1279
+ const avatarUrl = (reporter == null ? void 0 : reporter.avatarUrl) || "";
1280
+ useEffect2(() => {
1281
+ setAvatarFailed(false);
1282
+ }, [avatarUrl]);
1283
+ const name = ((_a = reporter == null ? void 0 : reporter.name) == null ? void 0 : _a.trim()) || "";
1284
+ const isAnonymous = !name;
1285
+ const initials = isAnonymous ? "" : initialsOf(name);
1286
+ const label = isAnonymous ? "Anonymous" : name;
1287
+ 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 ");
1288
+ const showAvatarImage = !!avatarUrl && !avatarFailed;
1289
+ return /* @__PURE__ */ jsxs2(
1290
+ "span",
1291
+ {
1292
+ title,
1293
+ style: { display: "flex", alignItems: "center", gap: "6px", minWidth: 0 },
1294
+ children: [
1295
+ /* @__PURE__ */ jsx2(
1296
+ "span",
1297
+ {
1298
+ "aria-hidden": "true",
1299
+ style: {
1300
+ width: "18px",
1301
+ height: "18px",
1302
+ flexShrink: 0,
1303
+ borderRadius: "50%",
1304
+ overflow: "hidden",
1305
+ display: "flex",
1306
+ alignItems: "center",
1307
+ justifyContent: "center",
1308
+ fontSize: "8px",
1309
+ fontWeight: 700,
1310
+ letterSpacing: "0.02em",
1311
+ background: isAnonymous ? t.inputBg : t.accent,
1312
+ color: isAnonymous ? t.textMuted : t.accentText,
1313
+ border: `1px solid ${isAnonymous ? t.inputBorder : "transparent"}`
1314
+ },
1315
+ children: showAvatarImage ? /* @__PURE__ */ jsx2(
1316
+ "img",
1317
+ {
1318
+ src: avatarUrl,
1319
+ alt: "",
1320
+ width: 18,
1321
+ height: 18,
1322
+ onError: () => setAvatarFailed(true),
1323
+ style: { width: "100%", height: "100%", objectFit: "cover" }
1324
+ }
1325
+ ) : initials ? initials : /* @__PURE__ */ jsxs2("svg", { width: "10", height: "10", viewBox: "0 0 16 16", fill: "none", children: [
1326
+ /* @__PURE__ */ jsx2("circle", { cx: "8", cy: "5.5", r: "2.75", stroke: "currentColor", strokeWidth: "1.4" }),
1327
+ /* @__PURE__ */ jsx2(
1328
+ "path",
1329
+ {
1330
+ d: "M2.75 13.5c0-2.35 2.35-3.75 5.25-3.75s5.25 1.4 5.25 3.75",
1331
+ stroke: "currentColor",
1332
+ strokeWidth: "1.4",
1333
+ strokeLinecap: "round"
1334
+ }
1335
+ )
1336
+ ] })
1337
+ }
1338
+ ),
1339
+ /* @__PURE__ */ jsxs2(
1340
+ "span",
1341
+ {
1342
+ style: {
1343
+ fontSize: "11px",
1344
+ color: t.textMuted,
1345
+ overflow: "hidden",
1346
+ textOverflow: "ellipsis",
1347
+ whiteSpace: "nowrap"
1348
+ },
1349
+ children: [
1350
+ label,
1351
+ (reporter == null ? void 0 : reporter.role) ? ` \xB7 ${reporter.role}` : ""
1352
+ ]
1353
+ }
1354
+ )
1355
+ ]
1356
+ }
1357
+ );
1358
+ }
874
1359
  function getTheme(dark) {
875
1360
  const hostAccent = detectHostAccent();
876
1361
  const defaults = dark ? {
@@ -940,12 +1425,36 @@ function isLowQualityText(text) {
940
1425
  }
941
1426
  return null;
942
1427
  }
1428
+ async function captureViaHtml2Canvas() {
1429
+ try {
1430
+ const { default: html2canvas } = await import("html2canvas-pro");
1431
+ const scale = Math.min(Math.max(window.devicePixelRatio || 1, 1), 2);
1432
+ const canvas = await html2canvas(document.body, {
1433
+ scale,
1434
+ logging: false,
1435
+ useCORS: true,
1436
+ allowTaint: true,
1437
+ x: window.scrollX,
1438
+ y: window.scrollY,
1439
+ width: window.innerWidth,
1440
+ height: window.innerHeight,
1441
+ windowWidth: window.innerWidth,
1442
+ windowHeight: window.innerHeight
1443
+ });
1444
+ return encodeScreenshot(canvas);
1445
+ } catch (e) {
1446
+ return null;
1447
+ }
1448
+ }
943
1449
  function ReportDialog({
944
1450
  report,
1451
+ sendFeedback: sendFeedback2,
945
1452
  enhanceText: enhanceText2,
946
1453
  transcribeAudio: transcribeAudio2,
947
1454
  types,
948
- showSeverity = true
1455
+ showSeverity = true,
1456
+ captureScreenshot = captureViaHtml2Canvas,
1457
+ reporter
949
1458
  }) {
950
1459
  const [isEnhancing, setIsEnhancing] = useState2(false);
951
1460
  const [isEnhanced, setIsEnhanced] = useState2(false);
@@ -968,6 +1477,25 @@ function ReportDialog({
968
1477
  const [attachments, setAttachments] = useState2([]);
969
1478
  const [previewIndex, setPreviewIndex] = useState2(null);
970
1479
  const [annotatingIndex, setAnnotatingIndex] = useState2(null);
1480
+ const [previewZoomed, setPreviewZoomed] = useState2(false);
1481
+ const [previewNatural, setPreviewNatural] = useState2(null);
1482
+ const previewScrollRef = useRef2(null);
1483
+ const previewImgElRef = useRef2(null);
1484
+ const panRef = useRef2(null);
1485
+ const zoomAnchorRef = useRef2(null);
1486
+ const resetPreviewZoom = () => {
1487
+ setPreviewZoomed(false);
1488
+ setPreviewNatural(null);
1489
+ zoomAnchorRef.current = null;
1490
+ };
1491
+ const openPreview = (i) => {
1492
+ resetPreviewZoom();
1493
+ setPreviewIndex(i);
1494
+ };
1495
+ const closePreview = () => {
1496
+ resetPreviewZoom();
1497
+ setPreviewIndex(null);
1498
+ };
971
1499
  const fileInputRef = useRef2(null);
972
1500
  const modalRef = useRef2(null);
973
1501
  const previewImgRef = useRef2(null);
@@ -996,17 +1524,64 @@ function ReportDialog({
996
1524
  const [severity, setSeverity] = useState2("medium");
997
1525
  const [validationError, setValidationError] = useState2(null);
998
1526
  const [placeholderIdx, setPlaceholderIdx] = useState2(0);
1527
+ const [rating, setRating] = useState2(0);
1528
+ const [hoveredStar, setHoveredStar] = useState2(0);
1529
+ const isRating = reportType === "RATING";
1530
+ const submitDisabled = isSubmitting || (isRating ? rating < 1 : !description.trim());
999
1531
  const availableTypes = types != null ? types : [
1000
1532
  "BUG",
1001
1533
  "FEATURE_REQUEST",
1002
1534
  "UI_IMPROVEMENT",
1003
1535
  "PERFORMANCE",
1004
1536
  "SECURITY",
1005
- "QUESTION",
1006
- "OTHER"
1537
+ "QUESTION"
1007
1538
  ];
1539
+ const tileGridRef = useRef2(null);
1540
+ useEffect2(() => {
1541
+ if (!isOpen || step !== 1) return;
1542
+ const onKey = (e) => {
1543
+ const el = e.target;
1544
+ const tag = el == null ? void 0 : el.tagName;
1545
+ if (tag === "INPUT" || tag === "TEXTAREA" || (el == null ? void 0 : el.isContentEditable)) return;
1546
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
1547
+ if (sendFeedback2 && /^[1-5]$/.test(e.key)) {
1548
+ e.preventDefault();
1549
+ setRating(Number(e.key));
1550
+ setReportType("RATING");
1551
+ setStep(2);
1552
+ return;
1553
+ }
1554
+ const letter = e.key.toLowerCase();
1555
+ if (!/^[a-z]$/.test(letter)) return;
1556
+ const match = availableTypes.find(
1557
+ (type) => getTypeLabel(type).toLowerCase().startsWith(letter)
1558
+ );
1559
+ if (match) {
1560
+ e.preventDefault();
1561
+ setReportType(match);
1562
+ setStep(2);
1563
+ }
1564
+ };
1565
+ window.addEventListener("keydown", onKey);
1566
+ return () => window.removeEventListener("keydown", onKey);
1567
+ }, [isOpen, step, sendFeedback2, availableTypes.join(",")]);
1568
+ const handleTileGridKeyDown = (e) => {
1569
+ var _a, _b, _c;
1570
+ const keys = ["ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp"];
1571
+ if (!keys.includes(e.key)) return;
1572
+ const tiles = Array.from(
1573
+ (_b = (_a = tileGridRef.current) == null ? void 0 : _a.querySelectorAll("button[data-gg-type]")) != null ? _b : []
1574
+ );
1575
+ if (tiles.length === 0) return;
1576
+ e.preventDefault();
1577
+ const current = tiles.indexOf(document.activeElement);
1578
+ const delta = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : e.key === "ArrowDown" ? 2 : -2;
1579
+ const next = current === -1 ? 0 : Math.min(Math.max(current + delta, 0), tiles.length - 1);
1580
+ (_c = tiles[next]) == null ? void 0 : _c.focus();
1581
+ };
1008
1582
  const isDark = useIsDark();
1009
1583
  const t = getTheme(isDark);
1584
+ const canZoomPreview = previewNatural !== null && typeof window !== "undefined" && (previewNatural.w > window.innerWidth - 32 || previewNatural.h > window.innerHeight - 80);
1010
1585
  const handleFileUpload = (e) => {
1011
1586
  const files = e.target.files;
1012
1587
  if (!files || files.length === 0) return;
@@ -1031,53 +1606,14 @@ function ReportDialog({
1031
1606
  reader.readAsDataURL(file);
1032
1607
  });
1033
1608
  };
1034
- const retakeScreenshot = async () => {
1035
- setPreviewIndex(null);
1036
- setIsOpen(false);
1037
- setScreenshots([]);
1038
- await new Promise((r) => setTimeout(r, 300));
1039
- try {
1040
- const { default: html2canvas } = await import("html2canvas-pro");
1041
- const canvas = await html2canvas(document.body, {
1042
- scale: 0.5,
1043
- logging: false,
1044
- useCORS: true,
1045
- allowTaint: true,
1046
- x: window.scrollX,
1047
- y: window.scrollY,
1048
- width: window.innerWidth,
1049
- height: window.innerHeight,
1050
- windowWidth: window.innerWidth,
1051
- windowHeight: window.innerHeight
1052
- });
1053
- setScreenshots([canvas.toDataURL("image/jpeg", 0.6)]);
1054
- } catch (e) {
1055
- }
1056
- setIsOpen(true);
1057
- };
1058
1609
  const handleOpen = async () => {
1059
1610
  setSubmitted(false);
1060
1611
  if (availableTypes.length === 1) {
1061
1612
  setReportType(availableTypes[0]);
1062
1613
  setStep(2);
1063
1614
  }
1064
- try {
1065
- const { default: html2canvas } = await import("html2canvas-pro");
1066
- const canvas = await html2canvas(document.body, {
1067
- scale: 0.5,
1068
- logging: false,
1069
- useCORS: true,
1070
- allowTaint: true,
1071
- x: window.scrollX,
1072
- y: window.scrollY,
1073
- width: window.innerWidth,
1074
- height: window.innerHeight,
1075
- windowWidth: window.innerWidth,
1076
- windowHeight: window.innerHeight
1077
- });
1078
- setScreenshots([canvas.toDataURL("image/jpeg", 0.6)]);
1079
- } catch (e) {
1080
- }
1615
+ const shot = await captureScreenshot();
1616
+ if (shot) setScreenshots([shot]);
1081
1617
  setIsOpen(true);
1082
1618
  };
1083
1619
  useEffect2(() => {
@@ -1098,24 +1634,87 @@ function ReportDialog({
1098
1634
  if (!isOpen) return;
1099
1635
  const handleEsc = (e) => {
1100
1636
  if (e.key === "Escape") {
1101
- if (previewIndex !== null) setPreviewIndex(null);
1637
+ if (previewZoomed) setPreviewZoomed(false);
1638
+ else if (previewIndex !== null) closePreview();
1102
1639
  else handleClose();
1103
1640
  }
1104
1641
  };
1105
1642
  document.addEventListener("keydown", handleEsc);
1106
1643
  return () => document.removeEventListener("keydown", handleEsc);
1107
- }, [isOpen, previewIndex]);
1644
+ }, [isOpen, previewIndex, previewZoomed]);
1108
1645
  useEffect2(() => {
1109
1646
  if (previewIndex === null || annotatingIndex !== null) return;
1110
1647
  const handlePointerDown = (e) => {
1111
1648
  var _a;
1112
1649
  if (!((_a = previewImgRef.current) == null ? void 0 : _a.contains(e.target))) {
1113
- setPreviewIndex(null);
1650
+ closePreview();
1114
1651
  }
1115
1652
  };
1116
1653
  document.addEventListener("pointerdown", handlePointerDown, true);
1117
1654
  return () => document.removeEventListener("pointerdown", handlePointerDown, true);
1118
1655
  }, [previewIndex, annotatingIndex]);
1656
+ useEffect2(() => {
1657
+ var _a;
1658
+ const el = previewScrollRef.current;
1659
+ if (!previewZoomed || !el) return;
1660
+ const anchor = (_a = zoomAnchorRef.current) != null ? _a : { fx: 0.5, fy: 0.5 };
1661
+ el.scrollLeft = anchor.fx * el.scrollWidth - el.clientWidth / 2;
1662
+ el.scrollTop = anchor.fy * el.scrollHeight - el.clientHeight / 2;
1663
+ }, [previewZoomed]);
1664
+ const togglePreviewZoom = (e) => {
1665
+ var _a;
1666
+ e.stopPropagation();
1667
+ if ((_a = panRef.current) == null ? void 0 : _a.moved) return;
1668
+ if (!previewZoomed && !canZoomPreview) return;
1669
+ const img = previewImgElRef.current;
1670
+ if (!previewZoomed && img) {
1671
+ const rect = img.getBoundingClientRect();
1672
+ zoomAnchorRef.current = {
1673
+ fx: (e.clientX - rect.left) / rect.width,
1674
+ fy: (e.clientY - rect.top) / rect.height
1675
+ };
1676
+ }
1677
+ setPreviewZoomed((z) => !z);
1678
+ };
1679
+ const handlePanStart = (e) => {
1680
+ const el = previewScrollRef.current;
1681
+ if (!previewZoomed || !el) return;
1682
+ panRef.current = {
1683
+ x: e.clientX,
1684
+ y: e.clientY,
1685
+ left: el.scrollLeft,
1686
+ top: el.scrollTop,
1687
+ moved: false
1688
+ };
1689
+ el.setPointerCapture(e.pointerId);
1690
+ };
1691
+ const handlePanMove = (e) => {
1692
+ const el = previewScrollRef.current;
1693
+ const start = panRef.current;
1694
+ if (!el || !start) return;
1695
+ const dx = e.clientX - start.x;
1696
+ const dy = e.clientY - start.y;
1697
+ if (Math.abs(dx) > 4 || Math.abs(dy) > 4) start.moved = true;
1698
+ el.scrollLeft = start.left - dx;
1699
+ el.scrollTop = start.top - dy;
1700
+ };
1701
+ const handlePanEnd = (e) => {
1702
+ var _a, _b;
1703
+ const el = previewScrollRef.current;
1704
+ const moved = (_b = (_a = panRef.current) == null ? void 0 : _a.moved) != null ? _b : false;
1705
+ try {
1706
+ el == null ? void 0 : el.releasePointerCapture(e.pointerId);
1707
+ } catch (e2) {
1708
+ }
1709
+ if (moved) {
1710
+ panRef.current = { x: 0, y: 0, left: 0, top: 0, moved: true };
1711
+ setTimeout(() => {
1712
+ panRef.current = null;
1713
+ }, 0);
1714
+ } else {
1715
+ panRef.current = null;
1716
+ }
1717
+ };
1119
1718
  useEffect2(() => {
1120
1719
  if (!isOpen) return;
1121
1720
  const handlePaste = (e) => {
@@ -1123,18 +1722,16 @@ function ReportDialog({
1123
1722
  try {
1124
1723
  const items = (_a = e.clipboardData) == null ? void 0 : _a.items;
1125
1724
  if (!items) return;
1126
- let matched = false;
1725
+ const pasted = [];
1127
1726
  for (let i = 0; i < items.length; i++) {
1128
- if (items[i].type.startsWith("image/")) {
1129
- const file = items[i].getAsFile();
1130
- if (!file) continue;
1131
- matched = true;
1132
- const reader = new FileReader();
1133
- reader.onload = () => setScreenshots((prev) => [...prev, reader.result]);
1134
- reader.readAsDataURL(file);
1135
- }
1727
+ if (items[i].kind !== "file") continue;
1728
+ const file = items[i].getAsFile();
1729
+ if (file) pasted.push(file);
1730
+ }
1731
+ if (pasted.length > 0) {
1732
+ e.preventDefault();
1733
+ addFiles(pasted);
1136
1734
  }
1137
- if (matched) e.preventDefault();
1138
1735
  } catch (e2) {
1139
1736
  }
1140
1737
  };
@@ -1180,6 +1777,8 @@ function ReportDialog({
1180
1777
  stopVoice();
1181
1778
  setIsOpen(false);
1182
1779
  setStep(1);
1780
+ setRating(0);
1781
+ setHoveredStar(0);
1183
1782
  setReportType("BUG");
1184
1783
  setSeverity("medium");
1185
1784
  setValidationError(null);
@@ -1325,7 +1924,30 @@ function ReportDialog({
1325
1924
  };
1326
1925
  const handleSubmit = async () => {
1327
1926
  try {
1328
- if (!description.trim() || isSubmitting) return;
1927
+ if (isSubmitting) return;
1928
+ if (isRating) {
1929
+ if (rating < 1) {
1930
+ setValidationError("Pick a star rating first.");
1931
+ return;
1932
+ }
1933
+ if (!sendFeedback2) return;
1934
+ setIsSubmitting(true);
1935
+ const result2 = await sendFeedback2(rating, description.trim() || void 0);
1936
+ if (result2 == null ? void 0 : result2.success) {
1937
+ setSubmitted(true);
1938
+ setDescription("");
1939
+ setRating(0);
1940
+ setTimeout(() => {
1941
+ setSubmitted(false);
1942
+ handleClose();
1943
+ }, 2e3);
1944
+ } else {
1945
+ setValidationError("Could not send \u2014 please try again.");
1946
+ }
1947
+ setIsSubmitting(false);
1948
+ return;
1949
+ }
1950
+ if (!description.trim()) return;
1329
1951
  const qualityError = isLowQualityText(description);
1330
1952
  if (qualityError) {
1331
1953
  setValidationError(qualityError);
@@ -1340,6 +1962,8 @@ function ReportDialog({
1340
1962
  metadata.severity = severity;
1341
1963
  }
1342
1964
  const result = await report(
1965
+ // The RATING branch returned above, so anything reaching here is a real
1966
+ // ReportType — the widened tile union can't leak into the report API.
1343
1967
  reportType,
1344
1968
  description.trim(),
1345
1969
  Object.keys(metadata).length > 0 ? metadata : void 0
@@ -1383,6 +2007,18 @@ function ReportDialog({
1383
2007
  button[data-gg-type="SECURITY"] > svg{animation:gg-shield-pulse 2.2s ease-in-out infinite;transform-origin:top}
1384
2008
  button[data-gg-type="QUESTION"] > svg{animation:gg-question-shake 2s ease-in-out infinite}
1385
2009
  button[data-gg-type="OTHER"] > svg{animation:gg-chat-tilt 2.2s ease-in-out infinite}
2010
+ /* Lift on hover, settle on press. Cheaper and steadier than doing it
2011
+ through React state, and :focus-visible keeps the ring off mouse users
2012
+ while leaving it for the arrow-key path. */
2013
+ button[data-gg-type]{transition:transform .15s ease,border-color .15s ease,box-shadow .15s ease,background-color .15s ease}
2014
+ button[data-gg-type]:hover{transform:translateY(-2px);box-shadow:0 6px 16px rgba(0,0,0,.18)}
2015
+ button[data-gg-type]:active{transform:translateY(0) scale(.985);box-shadow:none}
2016
+ button[data-gg-type]:focus-visible{outline:2px solid ${t.accent};outline-offset:2px}
2017
+ @keyframes gg-star-pop{0%{transform:scale(.6);opacity:0}60%{transform:scale(1.12)}100%{transform:scale(1);opacity:1}}
2018
+ button[data-gg-star]{transition:transform .12s ease}
2019
+ button[data-gg-star]:hover{transform:scale(1.18)}
2020
+ button[data-gg-star]:active{transform:scale(.94)}
2021
+ button[data-gg-star]:focus-visible{outline:2px solid #f59e0b;outline-offset:3px;border-radius:4px}
1386
2022
  ` }),
1387
2023
  document.body
1388
2024
  ),
@@ -1414,7 +2050,7 @@ function ReportDialog({
1414
2050
  style: {
1415
2051
  position: "relative",
1416
2052
  zIndex: 2147483647,
1417
- width: "340px",
2053
+ width: "420px",
1418
2054
  maxWidth: "calc(100% - 32px)",
1419
2055
  maxHeight: "calc(100dvh - 32px)",
1420
2056
  display: "flex",
@@ -1624,7 +2260,7 @@ function ReportDialog({
1624
2260
  "div",
1625
2261
  {
1626
2262
  style: {
1627
- padding: "16px",
2263
+ padding: "18px",
1628
2264
  overflowY: "auto",
1629
2265
  flex: "1 1 auto",
1630
2266
  minHeight: 0
@@ -1645,13 +2281,145 @@ function ReportDialog({
1645
2281
  ]
1646
2282
  }
1647
2283
  ) : /* @__PURE__ */ jsxs2(Fragment, { children: [
2284
+ step === 1 && sendFeedback2 && /* @__PURE__ */ jsxs2(
2285
+ "div",
2286
+ {
2287
+ style: {
2288
+ border: "1px solid rgba(245,158,11,0.35)",
2289
+ background: "rgba(245,158,11,0.07)",
2290
+ borderRadius: "10px",
2291
+ padding: "14px 16px",
2292
+ marginBottom: "16px",
2293
+ display: "flex",
2294
+ alignItems: "center",
2295
+ justifyContent: "space-between",
2296
+ gap: "12px",
2297
+ flexWrap: "wrap"
2298
+ },
2299
+ onMouseLeave: () => setHoveredStar(0),
2300
+ children: [
2301
+ /* @__PURE__ */ jsx2(
2302
+ "span",
2303
+ {
2304
+ style: { fontSize: "13px", fontWeight: 600, color: t.text },
2305
+ children: "How are we doing?"
2306
+ }
2307
+ ),
2308
+ /* @__PURE__ */ jsx2("span", { style: { display: "flex", gap: "4px" }, children: [1, 2, 3, 4, 5].map((star) => {
2309
+ const filled = star <= (hoveredStar || rating);
2310
+ return /* @__PURE__ */ jsx2(
2311
+ "button",
2312
+ {
2313
+ type: "button",
2314
+ "data-gg-star": star,
2315
+ "aria-label": `${star} star${star > 1 ? "s" : ""}`,
2316
+ onMouseEnter: () => setHoveredStar(star),
2317
+ onFocus: () => setHoveredStar(star),
2318
+ onClick: () => {
2319
+ setRating(star);
2320
+ setReportType("RATING");
2321
+ setStep(2);
2322
+ },
2323
+ style: {
2324
+ border: "none",
2325
+ background: "transparent",
2326
+ padding: 0,
2327
+ cursor: "pointer",
2328
+ lineHeight: 0
2329
+ },
2330
+ children: /* @__PURE__ */ jsx2("svg", { width: "26", height: "26", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx2(
2331
+ "path",
2332
+ {
2333
+ 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",
2334
+ fill: filled ? "#f59e0b" : "transparent",
2335
+ stroke: "#f59e0b",
2336
+ strokeWidth: "1.5",
2337
+ strokeLinejoin: "round"
2338
+ }
2339
+ ) })
2340
+ },
2341
+ star
2342
+ );
2343
+ }) })
2344
+ ]
2345
+ }
2346
+ ),
2347
+ step === 1 && sendFeedback2 && /* @__PURE__ */ jsxs2(
2348
+ "div",
2349
+ {
2350
+ style: {
2351
+ display: "flex",
2352
+ alignItems: "center",
2353
+ gap: "10px",
2354
+ marginBottom: "16px"
2355
+ },
2356
+ children: [
2357
+ /* @__PURE__ */ jsx2("span", { style: { flex: 1, height: "1px", background: t.border } }),
2358
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: "11px", color: t.textMuted }, children: "or report something" }),
2359
+ /* @__PURE__ */ jsx2("span", { style: { flex: 1, height: "1px", background: t.border } })
2360
+ ]
2361
+ }
2362
+ ),
2363
+ step === 1 && screenshots.length > 0 && /* @__PURE__ */ jsxs2(
2364
+ "div",
2365
+ {
2366
+ style: {
2367
+ display: "flex",
2368
+ alignItems: "center",
2369
+ gap: "10px",
2370
+ marginBottom: "14px",
2371
+ padding: "8px 10px",
2372
+ border: `1px solid ${t.inputBorder}`,
2373
+ borderRadius: "8px",
2374
+ background: t.inputBg
2375
+ },
2376
+ children: [
2377
+ /* @__PURE__ */ jsx2(
2378
+ "img",
2379
+ {
2380
+ src: screenshots[0],
2381
+ alt: "",
2382
+ style: {
2383
+ width: "34px",
2384
+ height: "24px",
2385
+ objectFit: "cover",
2386
+ borderRadius: "4px",
2387
+ border: `1px solid ${t.border}`,
2388
+ flexShrink: 0
2389
+ }
2390
+ }
2391
+ ),
2392
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: "11px", color: t.textMuted, flex: 1 }, children: "Screenshot attached" }),
2393
+ /* @__PURE__ */ jsx2(
2394
+ "button",
2395
+ {
2396
+ type: "button",
2397
+ onClick: () => setScreenshots([]),
2398
+ "aria-label": "Remove screenshot",
2399
+ style: {
2400
+ border: "none",
2401
+ background: "transparent",
2402
+ color: t.textMuted,
2403
+ cursor: "pointer",
2404
+ fontSize: "15px",
2405
+ lineHeight: 1,
2406
+ padding: "2px 4px"
2407
+ },
2408
+ children: "\xD7"
2409
+ }
2410
+ )
2411
+ ]
2412
+ }
2413
+ ),
1648
2414
  step === 1 && /* @__PURE__ */ jsx2(
1649
2415
  "div",
1650
2416
  {
2417
+ ref: tileGridRef,
2418
+ onKeyDown: handleTileGridKeyDown,
1651
2419
  style: {
1652
2420
  display: "grid",
1653
- gridTemplateColumns: "1fr 1fr",
1654
- gap: "8px"
2421
+ gridTemplateColumns: "1fr 1fr 1fr",
2422
+ gap: "10px"
1655
2423
  },
1656
2424
  children: availableTypes.map((type) => /* @__PURE__ */ jsxs2(
1657
2425
  "button",
@@ -1666,15 +2434,14 @@ function ReportDialog({
1666
2434
  display: "flex",
1667
2435
  flexDirection: "column",
1668
2436
  alignItems: "center",
1669
- gap: "8px",
1670
- padding: "16px 8px",
2437
+ gap: "9px",
2438
+ padding: "18px 10px",
1671
2439
  borderRadius: "8px",
1672
2440
  border: `1px solid ${t.inputBorder}`,
1673
2441
  background: t.inputBg,
1674
2442
  cursor: "pointer",
1675
2443
  color: t.text,
1676
- fontFamily: "inherit",
1677
- transition: "border-color 0.15s ease"
2444
+ fontFamily: "inherit"
1678
2445
  },
1679
2446
  onMouseEnter: (e) => {
1680
2447
  e.currentTarget.style.borderColor = t.accent;
@@ -1683,17 +2450,17 @@ function ReportDialog({
1683
2450
  e.currentTarget.style.borderColor = t.inputBorder;
1684
2451
  },
1685
2452
  children: [
1686
- getTypeIcon(type, t.accent),
1687
- /* @__PURE__ */ jsx2("span", { style: { fontSize: "13px", fontWeight: 600 }, children: getTypeLabel(type) }),
2453
+ getTypeIcon(type, t.accent, 22),
1688
2454
  /* @__PURE__ */ jsx2(
1689
2455
  "span",
1690
2456
  {
1691
2457
  style: {
1692
- fontSize: "11px",
1693
- color: t.textMuted,
1694
- lineHeight: "1.3"
2458
+ fontSize: "12px",
2459
+ fontWeight: 600,
2460
+ textAlign: "center",
2461
+ lineHeight: "1.2"
1695
2462
  },
1696
- children: getTypeSubtitle(type)
2463
+ children: getTypeLabel(type)
1697
2464
  }
1698
2465
  )
1699
2466
  ]
@@ -1703,6 +2470,77 @@ function ReportDialog({
1703
2470
  }
1704
2471
  ),
1705
2472
  step === 2 && /* @__PURE__ */ jsxs2(Fragment, { children: [
2473
+ /* @__PURE__ */ jsxs2(
2474
+ "button",
2475
+ {
2476
+ type: "button",
2477
+ onClick: () => setStep(1),
2478
+ style: {
2479
+ display: "inline-flex",
2480
+ alignItems: "center",
2481
+ gap: "6px",
2482
+ marginBottom: "10px",
2483
+ padding: "4px 10px 4px 8px",
2484
+ borderRadius: "999px",
2485
+ border: `1px solid ${isRating ? "rgba(245,158,11,0.4)" : t.inputBorder}`,
2486
+ background: isRating ? "rgba(245,158,11,0.1)" : t.inputBg,
2487
+ color: t.text,
2488
+ fontSize: "11px",
2489
+ fontWeight: 600,
2490
+ fontFamily: "inherit",
2491
+ cursor: "pointer"
2492
+ },
2493
+ children: [
2494
+ getTypeIcon(reportType, isRating ? "#f59e0b" : t.accent, 13),
2495
+ getTypeLabel(reportType),
2496
+ /* @__PURE__ */ jsx2("span", { style: { color: t.textMuted, fontWeight: 400 }, children: "\xB7 change" })
2497
+ ]
2498
+ }
2499
+ ),
2500
+ isRating && /* @__PURE__ */ jsx2(
2501
+ "div",
2502
+ {
2503
+ style: {
2504
+ display: "flex",
2505
+ gap: "6px",
2506
+ marginBottom: "12px"
2507
+ },
2508
+ onMouseLeave: () => setHoveredStar(0),
2509
+ children: [1, 2, 3, 4, 5].map((star) => {
2510
+ const filled = star <= (hoveredStar || rating);
2511
+ return /* @__PURE__ */ jsx2(
2512
+ "button",
2513
+ {
2514
+ type: "button",
2515
+ "aria-label": `${star} star${star > 1 ? "s" : ""}`,
2516
+ onClick: () => {
2517
+ setRating(star);
2518
+ if (validationError) setValidationError(null);
2519
+ },
2520
+ onMouseEnter: () => setHoveredStar(star),
2521
+ style: {
2522
+ border: "none",
2523
+ background: "transparent",
2524
+ padding: 0,
2525
+ cursor: "pointer",
2526
+ lineHeight: 0
2527
+ },
2528
+ children: /* @__PURE__ */ jsx2("svg", { width: "30", height: "30", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx2(
2529
+ "path",
2530
+ {
2531
+ 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",
2532
+ fill: filled ? "#f59e0b" : "transparent",
2533
+ stroke: filled ? "#f59e0b" : t.inputBorder,
2534
+ strokeWidth: "1.5",
2535
+ strokeLinejoin: "round"
2536
+ }
2537
+ ) })
2538
+ },
2539
+ star
2540
+ );
2541
+ })
2542
+ }
2543
+ ),
1706
2544
  /* @__PURE__ */ jsxs2("div", { style: { position: "relative" }, children: [
1707
2545
  /* @__PURE__ */ jsx2(
1708
2546
  "textarea",
@@ -2101,7 +2939,7 @@ function ReportDialog({
2101
2939
  children: voiceError
2102
2940
  }
2103
2941
  ),
2104
- /* @__PURE__ */ jsxs2("div", { style: { marginTop: "10px" }, children: [
2942
+ !isRating && /* @__PURE__ */ jsxs2("div", { style: { marginTop: "10px" }, children: [
2105
2943
  /* @__PURE__ */ jsxs2(
2106
2944
  "span",
2107
2945
  {
@@ -2141,7 +2979,7 @@ function ReportDialog({
2141
2979
  {
2142
2980
  src,
2143
2981
  alt: `Screenshot ${i + 1}`,
2144
- onClick: () => setPreviewIndex(i),
2982
+ onClick: () => openPreview(i),
2145
2983
  style: {
2146
2984
  width: "100%",
2147
2985
  height: "100%",
@@ -2545,37 +3383,54 @@ function ReportDialog({
2545
3383
  {
2546
3384
  type: "button",
2547
3385
  onClick: handleSubmit,
2548
- disabled: !description.trim() || isSubmitting,
3386
+ disabled: submitDisabled,
2549
3387
  style: {
2550
3388
  marginTop: "12px",
2551
3389
  width: "100%",
2552
3390
  padding: "10px",
2553
3391
  borderRadius: "8px",
2554
3392
  border: "none",
2555
- backgroundColor: !description.trim() || isSubmitting ? t.bgSecondary : t.accent,
2556
- color: !description.trim() || isSubmitting ? t.textMuted : t.accentText,
3393
+ backgroundColor: submitDisabled ? t.bgSecondary : t.accent,
3394
+ color: submitDisabled ? t.textMuted : t.accentText,
2557
3395
  fontSize: "14px",
2558
3396
  fontWeight: 600,
2559
- cursor: !description.trim() || isSubmitting ? "not-allowed" : "pointer",
3397
+ cursor: submitDisabled ? "not-allowed" : "pointer",
2560
3398
  fontFamily: "inherit",
2561
3399
  transition: "background-color 0.15s ease"
2562
3400
  },
2563
- children: isSubmitting ? "Sending..." : "Send Report"
3401
+ children: isSubmitting ? "Sending..." : isRating ? "Send Rating" : "Send Report"
2564
3402
  }
2565
3403
  )
2566
3404
  ] })
2567
3405
  ] })
2568
3406
  }
2569
3407
  ),
2570
- /* @__PURE__ */ jsx2(
3408
+ /* @__PURE__ */ jsxs2(
2571
3409
  "div",
2572
3410
  {
2573
3411
  style: {
2574
3412
  padding: "8px 16px 10px",
2575
3413
  borderTop: `1px solid ${t.border}`,
2576
- textAlign: "center"
3414
+ display: "flex",
3415
+ alignItems: "center",
3416
+ justifyContent: "space-between",
3417
+ gap: "12px"
2577
3418
  },
2578
- children: /* @__PURE__ */ jsx2("span", { style: { fontSize: "11px", color: t.textMuted }, children: "Powered by Glitchgrab" })
3419
+ children: [
3420
+ /* @__PURE__ */ jsx2(ReporterChip, { reporter, t }),
3421
+ /* @__PURE__ */ jsx2(
3422
+ "span",
3423
+ {
3424
+ style: {
3425
+ fontSize: "11px",
3426
+ color: t.textMuted,
3427
+ whiteSpace: "nowrap",
3428
+ flexShrink: 0
3429
+ },
3430
+ children: "Powered by Glitchgrab"
3431
+ }
3432
+ )
3433
+ ]
2579
3434
  }
2580
3435
  )
2581
3436
  ]
@@ -2590,7 +3445,7 @@ function ReportDialog({
2590
3445
  {
2591
3446
  ref: fileInputRef,
2592
3447
  type: "file",
2593
- accept: "image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv",
3448
+ accept: ATTACHMENT_ACCEPT,
2594
3449
  multiple: true,
2595
3450
  onChange: handleFileUpload,
2596
3451
  style: { display: "none" }
@@ -2614,7 +3469,7 @@ function ReportDialog({
2614
3469
  },
2615
3470
  onClick: (e) => {
2616
3471
  e.stopPropagation();
2617
- setPreviewIndex(null);
3472
+ closePreview();
2618
3473
  },
2619
3474
  onMouseDown: (e) => e.stopPropagation(),
2620
3475
  children: [
@@ -2622,22 +3477,59 @@ function ReportDialog({
2622
3477
  "div",
2623
3478
  {
2624
3479
  ref: previewImgRef,
2625
- style: { position: "relative", maxWidth: "100%" },
3480
+ style: {
3481
+ position: "relative",
3482
+ display: "flex",
3483
+ maxWidth: "100%",
3484
+ maxHeight: "calc(100vh - 80px)"
3485
+ },
2626
3486
  onClick: (e) => e.stopPropagation(),
2627
3487
  children: [
2628
3488
  /* @__PURE__ */ jsx2(
2629
- "img",
3489
+ "div",
2630
3490
  {
2631
- src: screenshots[previewIndex],
2632
- alt: "Screenshot preview",
2633
- style: {
2634
- maxWidth: "100%",
2635
- maxHeight: "calc(100vh - 80px)",
3491
+ ref: previewScrollRef,
3492
+ onPointerDown: handlePanStart,
3493
+ onPointerMove: handlePanMove,
3494
+ onPointerUp: handlePanEnd,
3495
+ onPointerCancel: handlePanEnd,
3496
+ onClick: togglePreviewZoom,
3497
+ style: previewZoomed ? {
3498
+ // Fill the overlay so there is room to pan around a
3499
+ // full-resolution screenshot.
3500
+ width: "calc(100vw - 32px)",
3501
+ height: "calc(100vh - 80px)",
3502
+ overflow: "auto",
2636
3503
  borderRadius: "8px",
2637
- objectFit: "contain",
2638
- cursor: "default",
2639
- display: "block"
2640
- }
3504
+ touchAction: "none",
3505
+ cursor: "grab"
3506
+ } : { maxWidth: "100%", lineHeight: 0 },
3507
+ children: /* @__PURE__ */ jsx2(
3508
+ "img",
3509
+ {
3510
+ ref: previewImgElRef,
3511
+ src: screenshots[previewIndex],
3512
+ alt: "Screenshot preview",
3513
+ onLoad: (e) => setPreviewNatural({
3514
+ w: e.currentTarget.naturalWidth,
3515
+ h: e.currentTarget.naturalHeight
3516
+ }),
3517
+ style: previewZoomed ? {
3518
+ // Natural size — one image pixel per CSS pixel.
3519
+ maxWidth: "none",
3520
+ maxHeight: "none",
3521
+ display: "block",
3522
+ cursor: "zoom-out"
3523
+ } : {
3524
+ maxWidth: "100%",
3525
+ maxHeight: "calc(100vh - 80px)",
3526
+ borderRadius: "8px",
3527
+ objectFit: "contain",
3528
+ cursor: canZoomPreview ? "zoom-in" : "default",
3529
+ display: "block"
3530
+ }
3531
+ }
3532
+ )
2641
3533
  }
2642
3534
  ),
2643
3535
  /* @__PURE__ */ jsxs2(
@@ -2701,7 +3593,11 @@ function ReportDialog({
2701
3593
  color: t.textMuted,
2702
3594
  fontSize: "12px"
2703
3595
  },
2704
- children: screenshots.length > 1 ? `${previewIndex + 1} / ${screenshots.length} \xB7 Click outside to close` : "Click outside to close"
3596
+ children: [
3597
+ screenshots.length > 1 ? `${previewIndex + 1} / ${screenshots.length}` : null,
3598
+ canZoomPreview ? previewZoomed ? "Drag to pan \xB7 click image to fit" : "Click image to zoom to full size" : null,
3599
+ previewZoomed ? "Esc to fit" : "Click outside to close"
3600
+ ].filter(Boolean).join(" \xB7 ")
2705
3601
  }
2706
3602
  )
2707
3603
  }
@@ -2731,6 +3627,8 @@ function ReportDialog({
2731
3627
  }
2732
3628
  function getTypeLabel(type) {
2733
3629
  switch (type) {
3630
+ case "RATING":
3631
+ return "Rating";
2734
3632
  case "BUG":
2735
3633
  return "Bug Report";
2736
3634
  case "FEATURE_REQUEST":
@@ -2747,25 +3645,8 @@ function getTypeLabel(type) {
2747
3645
  return "Other";
2748
3646
  }
2749
3647
  }
2750
- function getTypeSubtitle(type) {
2751
- switch (type) {
2752
- case "BUG":
2753
- return "Something isn't working";
2754
- case "FEATURE_REQUEST":
2755
- return "Suggest an improvement";
2756
- case "UI_IMPROVEMENT":
2757
- return "Something looks off";
2758
- case "PERFORMANCE":
2759
- return "Something is slow";
2760
- case "SECURITY":
2761
- return "Report a security concern";
2762
- case "QUESTION":
2763
- return "Ask a question";
2764
- case "OTHER":
2765
- return "General feedback";
2766
- }
2767
- }
2768
3648
  function getPlaceholder(type, idx = 0, hasVoice = false) {
3649
+ if (type === "RATING") return "What made it good \u2014 or what let you down? (optional)";
2769
3650
  if (hasVoice && idx === 1) return "Hold Space to speak \u2014 we'll transcribe it";
2770
3651
  switch (type) {
2771
3652
  case "BUG":
@@ -2793,6 +3674,16 @@ function getTypeIcon(type, color, size = 24) {
2793
3674
  style: { flexShrink: 0 }
2794
3675
  };
2795
3676
  switch (type) {
3677
+ case "RATING":
3678
+ return /* @__PURE__ */ jsx2("svg", { ...props, children: /* @__PURE__ */ jsx2(
3679
+ "path",
3680
+ {
3681
+ 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",
3682
+ stroke: color,
3683
+ strokeWidth: "1.5",
3684
+ strokeLinejoin: "round"
3685
+ }
3686
+ ) });
2796
3687
  case "BUG":
2797
3688
  return /* @__PURE__ */ jsx2("svg", { ...props, children: /* @__PURE__ */ jsx2(
2798
3689
  "path",
@@ -2885,6 +3776,112 @@ function getTypeIcon(type, color, size = 24) {
2885
3776
  }
2886
3777
  }
2887
3778
 
3779
+ // src/rejection.ts
3780
+ var DETAILS_MAX_LENGTH = 1e3;
3781
+ var MESSAGE_MAX_LENGTH = 200;
3782
+ function isUnactionableRejection(description) {
3783
+ if (description.stack) return false;
3784
+ const message = description.message.trim();
3785
+ return message === "" || message === "[object Object]";
3786
+ }
3787
+ function readString(value) {
3788
+ if (typeof value !== "string") return void 0;
3789
+ const trimmed = value.trim();
3790
+ if (trimmed === "" || trimmed === "[object Object]") return void 0;
3791
+ return trimmed.slice(0, MESSAGE_MAX_LENGTH);
3792
+ }
3793
+ function readScalar(value) {
3794
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
3795
+ return readString(value);
3796
+ }
3797
+ function readStack(reason) {
3798
+ try {
3799
+ const stack = reason == null ? void 0 : reason.stack;
3800
+ return typeof stack === "string" && stack.trim() !== "" ? stack : void 0;
3801
+ } catch (e) {
3802
+ return void 0;
3803
+ }
3804
+ }
3805
+ function serialize(reason) {
3806
+ try {
3807
+ const seen2 = /* @__PURE__ */ new WeakSet();
3808
+ const json = JSON.stringify(reason, (_key, value) => {
3809
+ if (typeof value === "bigint") return `${value}n`;
3810
+ if (typeof value === "object" && value !== null) {
3811
+ if (seen2.has(value)) return "[Circular]";
3812
+ seen2.add(value);
3813
+ }
3814
+ return value;
3815
+ });
3816
+ if (!json || json === "{}" || json === "null") return void 0;
3817
+ return json.slice(0, DETAILS_MAX_LENGTH);
3818
+ } catch (e) {
3819
+ return void 0;
3820
+ }
3821
+ }
3822
+ var SHAPE_MAX_KEYS = 5;
3823
+ function describeShape(reason) {
3824
+ try {
3825
+ const keys = Object.keys(reason);
3826
+ if (keys.length === 0) return "";
3827
+ const listed = keys.slice(0, SHAPE_MAX_KEYS).join(", ");
3828
+ const suffix = keys.length > SHAPE_MAX_KEYS ? ", \u2026" : "";
3829
+ return `Rejected object: ${listed}${suffix}`.slice(0, MESSAGE_MAX_LENGTH);
3830
+ } catch (e) {
3831
+ return "";
3832
+ }
3833
+ }
3834
+ function describeRejection(reason) {
3835
+ var _a, _b, _c, _d, _e, _f, _g, _h;
3836
+ try {
3837
+ if (reason instanceof Error) {
3838
+ return {
3839
+ message: reason.message || reason.name || "",
3840
+ stack: reason.stack
3841
+ };
3842
+ }
3843
+ const stack = readStack(reason);
3844
+ if (typeof reason === "string") {
3845
+ return { message: (_a = readString(reason)) != null ? _a : "", stack };
3846
+ }
3847
+ if (typeof reason === "number" || typeof reason === "boolean") {
3848
+ return { message: String(reason), stack };
3849
+ }
3850
+ if (reason === null || reason === void 0) {
3851
+ return { message: "" };
3852
+ }
3853
+ const source = reason;
3854
+ const nestedError = source.error;
3855
+ 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 : "";
3856
+ const details = serialize(reason);
3857
+ return {
3858
+ message: message || describeShape(reason),
3859
+ stack,
3860
+ details
3861
+ };
3862
+ } catch (e) {
3863
+ return { message: "" };
3864
+ }
3865
+ }
3866
+
3867
+ // src/shortcut.ts
3868
+ var GLITCHGRAB_SHORTCUT2 = "Ctrl+Shift+G";
3869
+ var GLITCHGRAB_SHORTCUT_MAC2 = "\u2318\u21E7G";
3870
+ function isMac2() {
3871
+ try {
3872
+ if (typeof navigator === "undefined") return false;
3873
+ return /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent);
3874
+ } catch (e) {
3875
+ return false;
3876
+ }
3877
+ }
3878
+ function getShortcutLabel2() {
3879
+ return isMac2() ? GLITCHGRAB_SHORTCUT_MAC2 : GLITCHGRAB_SHORTCUT2;
3880
+ }
3881
+ function matchesShortcut(e) {
3882
+ return (e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "g";
3883
+ }
3884
+
2888
3885
  // src/provider.tsx
2889
3886
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2890
3887
  var DEFAULT_BASE_URL2 = "https://glitchgrab.dev";
@@ -2908,14 +3905,28 @@ function GlitchgrabProviderInner({
2908
3905
  fallback,
2909
3906
  types,
2910
3907
  showSeverity,
2911
- ignoreErrors
3908
+ ignoreErrors,
3909
+ release: release2,
3910
+ context: appContext2,
3911
+ responseBodyOrigins: responseBodyOrigins2
2912
3912
  }) {
2913
3913
  const visitedPagesRef = useRef3([]);
3914
+ setRelease(release2);
3915
+ if (appContext2) setContexts(appContext2);
3916
+ registerCaptureConfig({
3917
+ token,
3918
+ baseUrl,
3919
+ session,
3920
+ ignoreErrors,
3921
+ getVisitedPages: () => visitedPagesRef.current,
3922
+ onError,
3923
+ onReportSent
3924
+ });
2914
3925
  useEffect3(() => {
2915
3926
  if (enableBreadcrumbs) {
2916
- initBreadcrumbs(maxBreadcrumbs2);
3927
+ initBreadcrumbs(maxBreadcrumbs2, { responseBodyOrigins: responseBodyOrigins2 });
2917
3928
  }
2918
- }, [enableBreadcrumbs, maxBreadcrumbs2]);
3929
+ }, [enableBreadcrumbs, maxBreadcrumbs2, responseBodyOrigins2 == null ? void 0 : responseBodyOrigins2.join(",")]);
2919
3930
  useEffect3(() => {
2920
3931
  try {
2921
3932
  if (typeof window === "undefined") return;
@@ -2963,10 +3974,7 @@ function GlitchgrabProviderInner({
2963
3974
  ];
2964
3975
  const matchesIgnore = (message) => {
2965
3976
  if (KNOWN_NOISE_RE.some((pattern) => pattern.test(message))) return true;
2966
- if (!ignoreErrors || ignoreErrors.length === 0) return false;
2967
- return ignoreErrors.some(
2968
- (pattern) => pattern instanceof RegExp ? pattern.test(message) : message.includes(pattern)
2969
- );
3977
+ return matchesIgnorePatterns(message, ignoreErrors);
2970
3978
  };
2971
3979
  const handleError = (event) => {
2972
3980
  var _a, _b, _c, _d, _e, _f, _g;
@@ -2984,6 +3992,7 @@ function GlitchgrabProviderInner({
2984
3992
  if (event.filename && EXTENSION_ORIGIN_RE.test(event.filename) || ((_a = event.error) == null ? void 0 : _a.stack) && EXTENSION_ORIGIN_RE.test(event.error.stack)) {
2985
3993
  return;
2986
3994
  }
3995
+ incrementErrorCount();
2987
3996
  const context = captureContext(visitedPagesRef.current);
2988
3997
  const sig = computeSignature({
2989
3998
  errorMessage: event.message,
@@ -3004,6 +4013,7 @@ function GlitchgrabProviderInner({
3004
4013
  metadata: {
3005
4014
  timestamp: context.timestamp,
3006
4015
  visitedPages: JSON.stringify(context.visitedPages),
4016
+ ...contextMetadata(context),
3007
4017
  filename: (_e = event.filename) != null ? _e : "",
3008
4018
  lineno: String((_f = event.lineno) != null ? _f : ""),
3009
4019
  colno: String((_g = event.colno) != null ? _g : ""),
@@ -3023,17 +4033,20 @@ function GlitchgrabProviderInner({
3023
4033
  const handleRejection = (event) => {
3024
4034
  var _a;
3025
4035
  try {
3026
- const context = captureContext(visitedPagesRef.current);
3027
4036
  const reason = event.reason;
3028
- const errMsg = reason instanceof Error ? reason.message : String(reason);
4037
+ const description = describeRejection(reason);
4038
+ const errMsg = description.message;
4039
+ const errStack = description.stack;
3029
4040
  if (errMsg === "Script error." || errMsg === "Script error") {
3030
4041
  return;
3031
4042
  }
3032
4043
  if (matchesIgnore(errMsg)) {
3033
4044
  return;
3034
4045
  }
3035
- const errStack = reason instanceof Error ? reason.stack : void 0;
3036
4046
  if (errStack && EXTENSION_ORIGIN_RE.test(errStack)) return;
4047
+ if (isUnactionableRejection(description)) return;
4048
+ incrementErrorCount();
4049
+ const context = captureContext(visitedPagesRef.current);
3037
4050
  const sig = computeSignature({
3038
4051
  errorMessage: errMsg,
3039
4052
  pageUrl: context.url,
@@ -3053,7 +4066,9 @@ function GlitchgrabProviderInner({
3053
4066
  metadata: {
3054
4067
  timestamp: context.timestamp,
3055
4068
  visitedPages: JSON.stringify(context.visitedPages),
4069
+ ...contextMetadata(context),
3056
4070
  type: "unhandledrejection",
4071
+ ...description.details ? { rejectionReason: description.details } : {},
3057
4072
  ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
3058
4073
  ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
3059
4074
  ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
@@ -3093,6 +4108,7 @@ function GlitchgrabProviderInner({
3093
4108
  metadata: {
3094
4109
  timestamp: context.timestamp,
3095
4110
  visitedPages: JSON.stringify(context.visitedPages),
4111
+ ...contextMetadata(context),
3096
4112
  ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
3097
4113
  ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
3098
4114
  ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
@@ -3113,6 +4129,53 @@ function GlitchgrabProviderInner({
3113
4129
  (description, metadata) => report("BUG", description, metadata),
3114
4130
  [report]
3115
4131
  );
4132
+ const feedback = useCallback(
4133
+ async (rating, message, metadata) => {
4134
+ try {
4135
+ const context = captureContext(visitedPagesRef.current);
4136
+ return await sendFeedback(
4137
+ {
4138
+ token,
4139
+ rating,
4140
+ message,
4141
+ pageUrl: context.url,
4142
+ userAgent: context.userAgent,
4143
+ metadata: {
4144
+ timestamp: context.timestamp,
4145
+ ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
4146
+ ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
4147
+ ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
4148
+ ...(session == null ? void 0 : session.phone) ? { sessionUserPhone: String(session.phone) } : {},
4149
+ ...metadata
4150
+ }
4151
+ },
4152
+ baseUrl
4153
+ );
4154
+ } catch (e) {
4155
+ return null;
4156
+ }
4157
+ },
4158
+ [token, baseUrl, session]
4159
+ );
4160
+ const openFeedbackDialog = useCallback(() => {
4161
+ if (typeof window !== "undefined") {
4162
+ window.dispatchEvent(
4163
+ new CustomEvent("glitchgrab:open-report", { detail: { type: "RATING" } })
4164
+ );
4165
+ }
4166
+ }, []);
4167
+ const captureError2 = useCallback(
4168
+ (error, options) => {
4169
+ captureError(error, options);
4170
+ },
4171
+ []
4172
+ );
4173
+ const setContext2 = useCallback((key, value) => {
4174
+ setContext(key, value);
4175
+ }, []);
4176
+ const setContexts2 = useCallback((values) => {
4177
+ setContexts(values);
4178
+ }, []);
3116
4179
  const addBreadcrumb2 = useCallback(
3117
4180
  (message, data) => {
3118
4181
  addBreadcrumb("custom", message, data);
@@ -3164,9 +4227,19 @@ function GlitchgrabProviderInner({
3164
4227
  } catch (e) {
3165
4228
  }
3166
4229
  }, [openReportDialog]);
3167
- const [shortcutLabel, setShortcutLabel] = useState3(GLITCHGRAB_SHORTCUT);
4230
+ const reporter = useMemo(() => {
4231
+ var _a;
4232
+ if (!session) return null;
4233
+ const avatar = (_a = session.avatarUrl) != null ? _a : session.image;
4234
+ return {
4235
+ name: session.name,
4236
+ email: session.email,
4237
+ avatarUrl: typeof avatar === "string" ? avatar : null
4238
+ };
4239
+ }, [session]);
4240
+ const [shortcutLabel, setShortcutLabel] = useState3(GLITCHGRAB_SHORTCUT2);
3168
4241
  useEffect3(() => {
3169
- setShortcutLabel(getShortcutLabel());
4242
+ setShortcutLabel(getShortcutLabel2());
3170
4243
  }, []);
3171
4244
  return /* @__PURE__ */ jsxs3(
3172
4245
  GlitchgrabContext.Provider,
@@ -3176,6 +4249,11 @@ function GlitchgrabProviderInner({
3176
4249
  baseUrl: baseUrl != null ? baseUrl : DEFAULT_BASE_URL2,
3177
4250
  reportBug,
3178
4251
  report,
4252
+ captureError: captureError2,
4253
+ setContext: setContext2,
4254
+ setContexts: setContexts2,
4255
+ sendFeedback: feedback,
4256
+ openFeedbackDialog,
3179
4257
  addBreadcrumb: addBreadcrumb2,
3180
4258
  openReportDialog,
3181
4259
  enhanceText: enhance,
@@ -3194,7 +4272,7 @@ function GlitchgrabProviderInner({
3194
4272
  children
3195
4273
  }
3196
4274
  ),
3197
- /* @__PURE__ */ jsx3(ReportDialog, { report, enhanceText: enhance, transcribeAudio: transcribe, types, showSeverity })
4275
+ /* @__PURE__ */ jsx3(ReportDialog, { report, sendFeedback: feedback, enhanceText: enhance, transcribeAudio: transcribe, types, showSeverity, reporter })
3198
4276
  ]
3199
4277
  }
3200
4278
  );
@@ -3304,6 +4382,91 @@ function ReportButton({
3304
4382
  );
3305
4383
  }
3306
4384
 
4385
+ // src/feedback-button.tsx
4386
+ import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
4387
+ function useGlitchgrabSafe2() {
4388
+ try {
4389
+ return useGlitchgrab();
4390
+ } catch (e) {
4391
+ return null;
4392
+ }
4393
+ }
4394
+ function FeedbackButton({
4395
+ position = "bottom-left",
4396
+ label = "Feedback",
4397
+ className,
4398
+ children
4399
+ }) {
4400
+ var _a, _b;
4401
+ const glitchgrab = useGlitchgrabSafe2();
4402
+ if (!glitchgrab) return null;
4403
+ const handleClick = () => glitchgrab.openFeedbackDialog();
4404
+ if (children) {
4405
+ return /* @__PURE__ */ jsx5(Fragment4, { children: children({ onClick: handleClick }) });
4406
+ }
4407
+ const isTop = (_a = position == null ? void 0 : position.startsWith("top")) != null ? _a : false;
4408
+ const isLeft = (_b = position == null ? void 0 : position.endsWith("left")) != null ? _b : false;
4409
+ const positionStyles = {
4410
+ ...isLeft ? { left: "16px" } : { right: "16px" },
4411
+ ...isTop ? { top: "16px" } : { bottom: "16px" }
4412
+ };
4413
+ return /* @__PURE__ */ jsxs5(
4414
+ "button",
4415
+ {
4416
+ type: "button",
4417
+ onClick: handleClick,
4418
+ className,
4419
+ style: {
4420
+ position: "fixed",
4421
+ ...positionStyles,
4422
+ zIndex: 99999,
4423
+ padding: "10px 18px",
4424
+ borderRadius: "24px",
4425
+ border: "none",
4426
+ backgroundColor: "#18181b",
4427
+ color: "#fafafa",
4428
+ fontSize: "14px",
4429
+ fontWeight: 500,
4430
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
4431
+ cursor: "pointer",
4432
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15), 0 1px 3px rgba(0, 0, 0, 0.1)",
4433
+ transition: "transform 0.15s ease, box-shadow 0.15s ease",
4434
+ display: "flex",
4435
+ alignItems: "center",
4436
+ gap: "6px"
4437
+ },
4438
+ onMouseEnter: (e) => {
4439
+ e.target.style.transform = "scale(1.05)";
4440
+ },
4441
+ onMouseLeave: (e) => {
4442
+ e.target.style.transform = "scale(1)";
4443
+ },
4444
+ "aria-label": label,
4445
+ children: [
4446
+ /* @__PURE__ */ jsx5(
4447
+ "svg",
4448
+ {
4449
+ width: "16",
4450
+ height: "16",
4451
+ viewBox: "0 0 24 24",
4452
+ fill: "none",
4453
+ style: { flexShrink: 0 },
4454
+ "aria-hidden": "true",
4455
+ children: /* @__PURE__ */ jsx5(
4456
+ "path",
4457
+ {
4458
+ 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",
4459
+ fill: "currentColor"
4460
+ }
4461
+ )
4462
+ }
4463
+ ),
4464
+ label
4465
+ ]
4466
+ }
4467
+ );
4468
+ }
4469
+
3307
4470
  // src/use-reports.ts
3308
4471
  import { useState as useState5, useEffect as useEffect4, useCallback as useCallback2 } from "react";
3309
4472
  var BASE_URL = "https://glitchgrab.dev";
@@ -3409,29 +4572,99 @@ function useGlitchgrabActions({
3409
4572
  error
3410
4573
  };
3411
4574
  }
4575
+
4576
+ // src/use-feedback.ts
4577
+ import { useState as useState6, useEffect as useEffect5, useCallback as useCallback3 } from "react";
4578
+ var DEFAULT_BASE_URL3 = "https://glitchgrab.dev";
4579
+ function buildUrl({
4580
+ approvedOnly,
4581
+ userId,
4582
+ minRating,
4583
+ limit = 50,
4584
+ baseUrl
4585
+ }) {
4586
+ const params = new URLSearchParams({ limit: String(limit) });
4587
+ if (approvedOnly) params.set("approved", "true");
4588
+ if (userId) params.set("reporterPrimaryKey", userId);
4589
+ if (minRating) params.set("minRating", String(minRating));
4590
+ return `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL3}/api/v1/sdk/feedback?${params.toString()}`;
4591
+ }
4592
+ async function fetchGlitchgrabFeedback(query) {
4593
+ var _a, _b;
4594
+ const res = await fetch(buildUrl(query), {
4595
+ headers: { Authorization: `Bearer ${query.token}` }
4596
+ });
4597
+ const data = await res.json();
4598
+ if (!data.success) throw new Error((_a = data.error) != null ? _a : "Failed to fetch feedback");
4599
+ return (_b = data.data) != null ? _b : [];
4600
+ }
4601
+ function useGlitchgrabFeedback(query) {
4602
+ const { token, approvedOnly, userId, minRating, limit, baseUrl } = query;
4603
+ const [feedback, setFeedback] = useState6([]);
4604
+ const [isLoading, setIsLoading] = useState6(true);
4605
+ const [isFetching, setIsFetching] = useState6(false);
4606
+ const [error, setError] = useState6(null);
4607
+ const load = useCallback3(async () => {
4608
+ if (!token) {
4609
+ setIsLoading(false);
4610
+ return;
4611
+ }
4612
+ setIsFetching(true);
4613
+ setError(null);
4614
+ try {
4615
+ const data = await fetchGlitchgrabFeedback({
4616
+ token,
4617
+ approvedOnly,
4618
+ userId,
4619
+ minRating,
4620
+ limit,
4621
+ baseUrl
4622
+ });
4623
+ setFeedback(data);
4624
+ } catch (e) {
4625
+ setError(e instanceof Error ? e.message : "Network error");
4626
+ } finally {
4627
+ setIsLoading(false);
4628
+ setIsFetching(false);
4629
+ }
4630
+ }, [token, approvedOnly, userId, minRating, limit, baseUrl]);
4631
+ useEffect5(() => {
4632
+ load();
4633
+ }, [load]);
4634
+ return { feedback, isLoading, isFetching, error, refetch: load };
4635
+ }
3412
4636
  export {
3413
- GLITCHGRAB_SHORTCUT,
3414
- GLITCHGRAB_SHORTCUT_MAC,
4637
+ FeedbackButton,
4638
+ GLITCHGRAB_SHORTCUT2 as GLITCHGRAB_SHORTCUT,
4639
+ GLITCHGRAB_SHORTCUT_MAC2 as GLITCHGRAB_SHORTCUT_MAC,
3415
4640
  GlitchgrabErrorBoundary,
3416
4641
  GlitchgrabProvider,
3417
4642
  ReportButton,
3418
4643
  addBreadcrumb,
3419
4644
  captureContext,
3420
4645
  captureDeviceInfo,
4646
+ captureError,
4647
+ clearAppContext,
3421
4648
  clearBreadcrumbs,
3422
4649
  clearDedupCache,
3423
4650
  computeSignature,
3424
4651
  enhanceText,
4652
+ fetchGlitchgrabFeedback,
3425
4653
  fetchGlitchgrabReports,
4654
+ getAppContext,
3426
4655
  getBreadcrumbs,
3427
- getShortcutLabel,
4656
+ getShortcutLabel2 as getShortcutLabel,
3428
4657
  initBreadcrumbs,
3429
4658
  matchesShortcut,
3430
4659
  sanitizeUrl,
4660
+ sendFeedback,
3431
4661
  sendReport,
4662
+ setContext,
4663
+ setContexts,
3432
4664
  shouldSkipDuplicate,
3433
4665
  useGlitchgrab,
3434
4666
  useGlitchgrabActions,
4667
+ useGlitchgrabFeedback,
3435
4668
  useGlitchgrabReports
3436
4669
  };
3437
4670
  //# sourceMappingURL=index.mjs.map