@koderlabs/tasks-sdk-web-reporter 0.1.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 ADDED
@@ -0,0 +1,1657 @@
1
+ import {
2
+ __name,
3
+ collectMetadata,
4
+ formatMetadataBlock,
5
+ patchConsole
6
+ } from "./chunk-LDKYNLXK.js";
7
+
8
+ // src/hotkey.ts
9
+ function parseHotkey(raw) {
10
+ if (!raw) return null;
11
+ const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/i.test(navigator.platform ?? "");
12
+ const parts = raw.toLowerCase().split("+").map((p) => p.trim());
13
+ const modifiers = /* @__PURE__ */ new Set();
14
+ let key = "";
15
+ for (const part of parts) {
16
+ if (part === "mod") {
17
+ modifiers.add(isMac ? "meta" : "ctrl");
18
+ } else if ([
19
+ "ctrl",
20
+ "control"
21
+ ].includes(part)) {
22
+ modifiers.add("ctrl");
23
+ } else if ([
24
+ "meta",
25
+ "cmd",
26
+ "command"
27
+ ].includes(part)) {
28
+ modifiers.add("meta");
29
+ } else if ([
30
+ "alt",
31
+ "option",
32
+ "opt"
33
+ ].includes(part)) {
34
+ modifiers.add("alt");
35
+ } else if (part === "shift") {
36
+ modifiers.add("shift");
37
+ } else {
38
+ key = part;
39
+ }
40
+ }
41
+ if (!key) return null;
42
+ return {
43
+ ctrl: modifiers.has("ctrl"),
44
+ meta: modifiers.has("meta"),
45
+ alt: modifiers.has("alt"),
46
+ shift: modifiers.has("shift"),
47
+ key
48
+ };
49
+ }
50
+ __name(parseHotkey, "parseHotkey");
51
+ function matchesHotkey(e, parsed) {
52
+ return e.ctrlKey === parsed.ctrl && e.metaKey === parsed.meta && e.altKey === parsed.alt && e.shiftKey === parsed.shift && e.key.toLowerCase() === parsed.key;
53
+ }
54
+ __name(matchesHotkey, "matchesHotkey");
55
+ function isTypingTarget(e) {
56
+ const target = e.target;
57
+ if (!target) return false;
58
+ const tag = target.tagName?.toLowerCase();
59
+ if (tag === "input" || tag === "textarea" || tag === "select") return true;
60
+ if (target.isContentEditable) return true;
61
+ const ceProp = target.contentEditable;
62
+ if (ceProp === "true" || ceProp === "plaintext-only") return true;
63
+ const ce = target.getAttribute?.("contenteditable");
64
+ if (ce === "" || ce === "true" || ce === "plaintext-only") return true;
65
+ return false;
66
+ }
67
+ __name(isTypingTarget, "isTypingTarget");
68
+ function registerHotkey(hotkeyStr, handler, target = window) {
69
+ const parsed = parseHotkey(hotkeyStr);
70
+ if (!parsed) return () => {
71
+ };
72
+ const hasModifier = parsed.ctrl || parsed.meta || parsed.alt;
73
+ const listener = /* @__PURE__ */ __name((e) => {
74
+ const ke = e;
75
+ if (!hasModifier && isTypingTarget(ke)) return;
76
+ if (matchesHotkey(ke, parsed)) {
77
+ e.preventDefault();
78
+ e.stopPropagation();
79
+ handler();
80
+ }
81
+ }, "listener");
82
+ target.addEventListener("keydown", listener, true);
83
+ return () => target.removeEventListener("keydown", listener, true);
84
+ }
85
+ __name(registerHotkey, "registerHotkey");
86
+
87
+ // src/modal/styles.ts
88
+ var WIDGET_STYLES = `
89
+ :host {
90
+ --accent-background: #ef4444;
91
+ --foreground: #111827;
92
+ --background: #ffffff;
93
+ --border-color: #e5e7eb;
94
+ --z-index: 2147483647;
95
+ --font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
96
+ all: initial;
97
+ font-family: var(--font-family);
98
+ color: var(--foreground);
99
+ }
100
+
101
+ *, *::before, *::after {
102
+ box-sizing: border-box;
103
+ }
104
+
105
+ .it-backdrop {
106
+ position: fixed;
107
+ inset: 0;
108
+ background: rgba(0, 0, 0, 0.35);
109
+ z-index: var(--z-index);
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ }
114
+
115
+ .it-modal {
116
+ background: var(--background);
117
+ border-radius: 12px;
118
+ box-shadow: 0 20px 60px rgba(0,0,0,0.25);
119
+ /* Big enough to annotate a screenshot comfortably without dominating
120
+ the entire viewport. Cap so it doesn't get unwieldy on 4K monitors. */
121
+ width: min(1280px, 88vw);
122
+ height: min(900px, 88vh);
123
+ max-width: 88vw;
124
+ max-height: 88vh;
125
+ overflow-y: auto;
126
+ padding: 24px;
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 16px;
130
+ position: relative;
131
+ font-family: var(--font-family);
132
+ color: var(--foreground);
133
+ }
134
+
135
+ .it-header {
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: space-between;
139
+ gap: 8px;
140
+ }
141
+
142
+ .it-title {
143
+ font-size: 16px;
144
+ font-weight: 600;
145
+ margin: 0;
146
+ }
147
+
148
+ .it-close-btn {
149
+ background: none;
150
+ border: none;
151
+ cursor: pointer;
152
+ color: var(--foreground);
153
+ opacity: 0.6;
154
+ padding: 4px;
155
+ border-radius: 6px;
156
+ line-height: 1;
157
+ font-size: 18px;
158
+ transition: opacity 0.15s;
159
+ }
160
+ .it-close-btn:hover { opacity: 1; }
161
+
162
+ .it-section { display: flex; flex-direction: column; gap: 6px; }
163
+
164
+ .it-label {
165
+ font-size: 13px;
166
+ font-weight: 500;
167
+ color: var(--foreground);
168
+ opacity: 0.8;
169
+ }
170
+
171
+ .it-textarea, .it-input {
172
+ width: 100%;
173
+ padding: 10px 12px;
174
+ border: 1px solid var(--border-color);
175
+ border-radius: 8px;
176
+ font-size: 14px;
177
+ font-family: var(--font-family);
178
+ color: var(--foreground);
179
+ background: var(--background);
180
+ resize: vertical;
181
+ outline: none;
182
+ transition: border-color 0.15s;
183
+ }
184
+ .it-textarea:focus, .it-input:focus {
185
+ border-color: var(--accent-background);
186
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-background) 20%, transparent);
187
+ }
188
+
189
+ .it-textarea { min-height: 100px; }
190
+
191
+ .it-error {
192
+ font-size: 12px;
193
+ color: var(--accent-background);
194
+ min-height: 16px;
195
+ }
196
+
197
+ .it-screenshot-area {
198
+ border: 2px dashed var(--border-color);
199
+ border-radius: 8px;
200
+ padding: 12px;
201
+ text-align: center;
202
+ background: #f9fafb;
203
+ /* Let the screenshot panel grow to fill the modal \u2014 description sits
204
+ below and keeps its natural size. */
205
+ flex: 1 1 auto;
206
+ min-height: 0;
207
+ display: flex;
208
+ flex-direction: column;
209
+ gap: 10px;
210
+ }
211
+
212
+ .it-screenshot-preview {
213
+ position: relative;
214
+ width: 100%;
215
+ flex: 1 1 auto;
216
+ min-height: 0;
217
+ overflow: hidden;
218
+ border-radius: 6px;
219
+ /* Center the screenshot vertically so smaller captures aren't pinned
220
+ to the top of a tall panel. */
221
+ display: flex;
222
+ align-items: center;
223
+ justify-content: center;
224
+ }
225
+
226
+ /* Stage = aspect-ratio-locked wrapper around bg + fg canvases.
227
+ Sized to fit the preview pane while preserving the screenshot's
228
+ aspect ratio \u2014 keeps fg overlay aligned with bg pixel-for-pixel. */
229
+ .it-screenshot-stage {
230
+ position: relative;
231
+ max-width: 100%;
232
+ max-height: 100%;
233
+ /* Default ratio prevents 0-height collapse before the first capture */
234
+ aspect-ratio: 16 / 10;
235
+ /* Lock width to the smaller of (parent width) or (parent height \xD7 ratio).
236
+ Browsers honor aspect-ratio for one axis only when the other is auto. */
237
+ width: 100%;
238
+ height: auto;
239
+ }
240
+
241
+ .it-background-canvas {
242
+ width: 100%;
243
+ height: 100%;
244
+ display: block;
245
+ border-radius: 6px;
246
+ }
247
+
248
+ .it-foreground-canvas {
249
+ position: absolute;
250
+ top: 0; left: 0;
251
+ width: 100% !important;
252
+ height: 100% !important;
253
+ cursor: crosshair;
254
+ }
255
+
256
+ .it-screenshot-placeholder {
257
+ color: var(--foreground);
258
+ opacity: 0.5;
259
+ font-size: 13px;
260
+ }
261
+
262
+ .it-toolbar {
263
+ display: flex;
264
+ gap: 8px;
265
+ align-items: center;
266
+ flex-wrap: wrap;
267
+ margin-top: 8px;
268
+ }
269
+
270
+ .it-btn {
271
+ padding: 7px 14px;
272
+ border-radius: 7px;
273
+ font-size: 13px;
274
+ font-weight: 500;
275
+ cursor: pointer;
276
+ border: 1px solid var(--border-color);
277
+ background: var(--background);
278
+ color: var(--foreground);
279
+ transition: background 0.15s, border-color 0.15s;
280
+ font-family: var(--font-family);
281
+ }
282
+ .it-btn:hover { background: #f3f4f6; }
283
+
284
+ .it-btn-primary {
285
+ background: var(--accent-background);
286
+ color: #fff;
287
+ border-color: var(--accent-background);
288
+ }
289
+ .it-btn-primary:hover { filter: brightness(0.9); background: var(--accent-background); }
290
+ .it-btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
291
+
292
+ .it-btn-tool.active {
293
+ background: color-mix(in srgb, var(--accent-background) 15%, var(--background));
294
+ border-color: var(--accent-background);
295
+ color: var(--accent-background);
296
+ }
297
+
298
+ .it-footer {
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: space-between;
302
+ gap: 8px;
303
+ }
304
+
305
+ .it-branding {
306
+ font-size: 11px;
307
+ color: var(--foreground);
308
+ opacity: 0.4;
309
+ text-decoration: none;
310
+ }
311
+ .it-branding:hover { opacity: 0.7; }
312
+
313
+ .it-actions {
314
+ display: flex;
315
+ gap: 8px;
316
+ }
317
+
318
+ .it-spinner {
319
+ display: inline-block;
320
+ width: 14px; height: 14px;
321
+ border: 2px solid rgba(255,255,255,0.3);
322
+ border-top-color: #fff;
323
+ border-radius: 50%;
324
+ animation: it-spin 0.7s linear infinite;
325
+ vertical-align: middle;
326
+ margin-right: 6px;
327
+ }
328
+ @keyframes it-spin { to { transform: rotate(360deg); } }
329
+
330
+ /* Modal collapses to a compact confirmation card after submit succeeds.
331
+ Overrides the wide annotation-mode sizing without re-mounting. */
332
+ .it-modal.it-modal--success {
333
+ width: min(420px, 92vw) !important;
334
+ height: auto !important;
335
+ max-height: 80vh !important;
336
+ }
337
+
338
+ .it-success {
339
+ text-align: center;
340
+ padding: 16px 8px 8px;
341
+ display: flex;
342
+ flex-direction: column;
343
+ align-items: center;
344
+ gap: 8px;
345
+ }
346
+
347
+ .it-success-icon {
348
+ width: 56px;
349
+ height: 56px;
350
+ border-radius: 50%;
351
+ background: #dcfce7;
352
+ color: #16a34a;
353
+ display: inline-flex;
354
+ align-items: center;
355
+ justify-content: center;
356
+ font-size: 32px;
357
+ font-weight: 700;
358
+ line-height: 1;
359
+ box-shadow: 0 0 0 6px rgba(22, 163, 74, 0.08);
360
+ }
361
+
362
+ .it-success-title {
363
+ font-size: 16px;
364
+ font-weight: 600;
365
+ color: var(--foreground);
366
+ }
367
+
368
+ .it-success-ticket {
369
+ font-size: 20px;
370
+ font-weight: 700;
371
+ color: var(--accent-background);
372
+ text-decoration: none;
373
+ letter-spacing: 0.02em;
374
+ }
375
+ .it-success-ticket:hover { text-decoration: underline; }
376
+
377
+ .it-success-hint {
378
+ font-size: 12px;
379
+ color: var(--foreground);
380
+ opacity: 0.55;
381
+ margin-top: 4px;
382
+ }
383
+
384
+ .it-warning {
385
+ font-size: 12px;
386
+ color: #d97706;
387
+ background: #fef3c7;
388
+ border-radius: 6px;
389
+ padding: 8px 10px;
390
+ }
391
+
392
+ .it-fab {
393
+ position: fixed;
394
+ bottom: 20px;
395
+ right: 20px;
396
+ z-index: calc(var(--z-index) - 1);
397
+ width: 48px; height: 48px;
398
+ border-radius: 50%;
399
+ background: var(--accent-background);
400
+ color: #fff;
401
+ border: none;
402
+ cursor: pointer;
403
+ box-shadow: 0 4px 16px rgba(0,0,0,0.2);
404
+ font-size: 22px;
405
+ display: flex;
406
+ align-items: center;
407
+ justify-content: center;
408
+ transition: transform 0.15s, box-shadow 0.15s;
409
+ }
410
+ .it-fab:hover {
411
+ transform: scale(1.1);
412
+ box-shadow: 0 6px 20px rgba(0,0,0,0.3);
413
+ }
414
+ `;
415
+
416
+ // src/modal/shell.ts
417
+ function getCspNonce() {
418
+ if (typeof document === "undefined") return null;
419
+ const meta = document.querySelector('meta[name="csp-nonce"]');
420
+ const v = meta?.getAttribute("content");
421
+ return v && v.length > 0 ? v : null;
422
+ }
423
+ __name(getCspNonce, "getCspNonce");
424
+ var ModalShell = class {
425
+ static {
426
+ __name(this, "ModalShell");
427
+ }
428
+ /** Outer host appended to document.body — exposed so callers can hide it
429
+ * from native screenshot capture without leaking the shadow DOM. */
430
+ host;
431
+ shadow;
432
+ _isOpen = false;
433
+ onCloseCb;
434
+ // Refs
435
+ backdropEl = null;
436
+ modalEl = null;
437
+ contentSlot = null;
438
+ // Cleanup
439
+ _escListener = null;
440
+ constructor(opts = {}) {
441
+ this.onCloseCb = opts.onClose;
442
+ this.host = document.createElement("div");
443
+ this.host.setAttribute("data-it-widget", "");
444
+ this.shadow = this.host.attachShadow({
445
+ mode: "open"
446
+ });
447
+ const styleEl = document.createElement("style");
448
+ styleEl.textContent = WIDGET_STYLES;
449
+ const nonce = getCspNonce();
450
+ if (nonce) {
451
+ styleEl.setAttribute("nonce", nonce);
452
+ styleEl.nonce = nonce;
453
+ }
454
+ this.shadow.appendChild(styleEl);
455
+ }
456
+ get shadowRoot() {
457
+ return this.shadow;
458
+ }
459
+ get isOpen() {
460
+ return this._isOpen;
461
+ }
462
+ get modalElement() {
463
+ return this.modalEl;
464
+ }
465
+ /**
466
+ * Open the modal with the given content element.
467
+ */
468
+ open(content) {
469
+ if (this._isOpen) this._teardown();
470
+ document.body.appendChild(this.host);
471
+ const backdrop = document.createElement("div");
472
+ backdrop.className = "it-backdrop";
473
+ backdrop.addEventListener("click", (e) => {
474
+ if (e.target === backdrop) {
475
+ e.stopPropagation();
476
+ }
477
+ });
478
+ const modal = document.createElement("div");
479
+ modal.className = "it-modal";
480
+ modal.setAttribute("role", "dialog");
481
+ modal.setAttribute("aria-modal", "true");
482
+ this.contentSlot = content;
483
+ modal.appendChild(content);
484
+ backdrop.appendChild(modal);
485
+ this.shadow.appendChild(backdrop);
486
+ this.backdropEl = backdrop;
487
+ this.modalEl = modal;
488
+ this._isOpen = true;
489
+ this._escListener = (e) => {
490
+ if (e.key === "Escape" && this._isOpen) {
491
+ this.close();
492
+ }
493
+ };
494
+ window.addEventListener("keydown", this._escListener);
495
+ modal.setAttribute("tabindex", "-1");
496
+ modal.focus();
497
+ }
498
+ /** Close and remove the modal. */
499
+ close() {
500
+ if (!this._isOpen) return;
501
+ this._teardown();
502
+ this.onCloseCb?.();
503
+ }
504
+ /** Destroy completely — remove host from DOM. */
505
+ destroy() {
506
+ this._teardown();
507
+ if (this.host.parentNode) this.host.parentNode.removeChild(this.host);
508
+ }
509
+ _teardown() {
510
+ if (this._escListener) {
511
+ window.removeEventListener("keydown", this._escListener);
512
+ this._escListener = null;
513
+ }
514
+ if (this.backdropEl && this.shadow.contains(this.backdropEl)) {
515
+ this.shadow.removeChild(this.backdropEl);
516
+ }
517
+ this.backdropEl = null;
518
+ this.modalEl = null;
519
+ this.contentSlot = null;
520
+ this._isOpen = false;
521
+ }
522
+ };
523
+
524
+ // src/annotator/tools.ts
525
+ var AnnotationTools = class {
526
+ static {
527
+ __name(this, "AnnotationTools");
528
+ }
529
+ _commands = [];
530
+ _activeTool = "highlight";
531
+ _onChange;
532
+ constructor(onChange) {
533
+ this._onChange = onChange;
534
+ }
535
+ get activeTool() {
536
+ return this._activeTool;
537
+ }
538
+ get commands() {
539
+ return this._commands;
540
+ }
541
+ /** Switch between highlight and hide tools. */
542
+ setTool(mode) {
543
+ this._activeTool = mode;
544
+ this._notify();
545
+ }
546
+ /** Toggle between the two tools. */
547
+ toggleTool() {
548
+ this._activeTool = this._activeTool === "highlight" ? "hide" : "highlight";
549
+ this._notify();
550
+ }
551
+ /** Add a new draw command (called when the user finishes drawing a rect). */
552
+ addCommand(cmd) {
553
+ this._commands = [
554
+ ...this._commands,
555
+ cmd
556
+ ];
557
+ this._notify();
558
+ }
559
+ /** Remove the last command (undo). */
560
+ undo() {
561
+ if (this._commands.length === 0) return;
562
+ this._commands = this._commands.slice(0, -1);
563
+ this._notify();
564
+ }
565
+ /** Remove a specific command by index. */
566
+ removeAt(index) {
567
+ this._commands = this._commands.filter((_, i) => i !== index);
568
+ this._notify();
569
+ }
570
+ /** Remove all commands. */
571
+ clear() {
572
+ this._commands = [];
573
+ this._notify();
574
+ }
575
+ /** Get a plain-object snapshot of the current state. */
576
+ getState() {
577
+ return {
578
+ commands: [
579
+ ...this._commands
580
+ ],
581
+ activeTool: this._activeTool
582
+ };
583
+ }
584
+ _notify() {
585
+ this._onChange?.(this.getState());
586
+ }
587
+ };
588
+
589
+ // src/annotator/draw.ts
590
+ var ACCENT_VAR = "--accent-background";
591
+ var ACCENT_DEFAULT = "#ef4444";
592
+ function getAccentColor(root) {
593
+ const el = root instanceof ShadowRoot ? root.host : root;
594
+ return getComputedStyle(el).getPropertyValue(ACCENT_VAR).trim() || ACCENT_DEFAULT;
595
+ }
596
+ __name(getAccentColor, "getAccentColor");
597
+ function paintForeground(foreground, commands, accent = ACCENT_DEFAULT) {
598
+ const ctx = foreground.getContext("2d");
599
+ if (!ctx) return;
600
+ const W = foreground.width;
601
+ const H = foreground.height;
602
+ ctx.clearRect(0, 0, W, H);
603
+ const highlights = commands.filter((c) => c.type === "highlight");
604
+ const hides = commands.filter((c) => c.type === "hide");
605
+ if (highlights.length > 0) {
606
+ ctx.save();
607
+ ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
608
+ ctx.fillRect(0, 0, W, H);
609
+ ctx.globalCompositeOperation = "destination-out";
610
+ for (const cmd of highlights) {
611
+ const rx = cmd.x * W;
612
+ const ry = cmd.y * H;
613
+ const rw = cmd.w * W;
614
+ const rh = cmd.h * H;
615
+ ctx.fillStyle = "rgba(0,0,0,1)";
616
+ ctx.fillRect(rx, ry, rw, rh);
617
+ }
618
+ ctx.restore();
619
+ ctx.save();
620
+ ctx.strokeStyle = accent;
621
+ ctx.lineWidth = 2;
622
+ for (const cmd of highlights) {
623
+ ctx.strokeRect(cmd.x * W, cmd.y * H, cmd.w * W, cmd.h * H);
624
+ }
625
+ ctx.restore();
626
+ }
627
+ if (hides.length > 0) {
628
+ ctx.save();
629
+ ctx.fillStyle = "#000000";
630
+ for (const cmd of hides) {
631
+ ctx.fillRect(cmd.x * W, cmd.y * H, cmd.w * W, cmd.h * H);
632
+ }
633
+ ctx.restore();
634
+ }
635
+ }
636
+ __name(paintForeground, "paintForeground");
637
+ function compositeCanvases(background, foreground, output) {
638
+ const ctx = output.getContext("2d");
639
+ if (!ctx) return;
640
+ output.width = background.width;
641
+ output.height = background.height;
642
+ ctx.clearRect(0, 0, output.width, output.height);
643
+ ctx.drawImage(background, 0, 0);
644
+ ctx.drawImage(foreground, 0, 0);
645
+ }
646
+ __name(compositeCanvases, "compositeCanvases");
647
+
648
+ // src/annotator/index.ts
649
+ var Annotator = class {
650
+ static {
651
+ __name(this, "Annotator");
652
+ }
653
+ tools;
654
+ background;
655
+ foreground;
656
+ cssRoot;
657
+ onChangeCb;
658
+ // In-progress drag state
659
+ dragging = false;
660
+ dragStart = null;
661
+ // Cleanup for event listeners
662
+ cleanupListeners = [];
663
+ constructor(opts) {
664
+ this.background = opts.background;
665
+ this.foreground = opts.foreground;
666
+ this.cssRoot = opts.cssRoot;
667
+ this.onChangeCb = opts.onChange;
668
+ this.tools = new AnnotationTools((state) => {
669
+ this._repaint();
670
+ this.onChangeCb?.(state.commands);
671
+ });
672
+ this._attachMouseListeners();
673
+ }
674
+ get activeTool() {
675
+ return this.tools.activeTool;
676
+ }
677
+ get commands() {
678
+ return this.tools.commands;
679
+ }
680
+ setTool(mode) {
681
+ this.tools.setTool(mode);
682
+ }
683
+ toggleTool() {
684
+ this.tools.toggleTool();
685
+ }
686
+ undo() {
687
+ this.tools.undo();
688
+ }
689
+ clear() {
690
+ this.tools.clear();
691
+ }
692
+ removeAt(i) {
693
+ this.tools.removeAt(i);
694
+ }
695
+ /** Composite background + foreground into an output canvas. */
696
+ composite(output) {
697
+ compositeCanvases(this.background, this.foreground, output);
698
+ }
699
+ /** Destroy event listeners. */
700
+ destroy() {
701
+ for (const fn of this.cleanupListeners) fn();
702
+ this.cleanupListeners = [];
703
+ }
704
+ // ── Private helpers ──────────────────────────────────────────────────────────
705
+ _repaint() {
706
+ const accent = getAccentColor(this.cssRoot);
707
+ paintForeground(this.foreground, [
708
+ ...this.tools.commands
709
+ ], accent);
710
+ }
711
+ /** Normalize a pixel coordinate within the foreground canvas to 0..1. */
712
+ _normalize(px, py) {
713
+ const W = this.foreground.width || 1;
714
+ const H = this.foreground.height || 1;
715
+ return {
716
+ nx: Math.max(0, Math.min(1, px / W)),
717
+ ny: Math.max(0, Math.min(1, py / H))
718
+ };
719
+ }
720
+ /**
721
+ * Mouse → internal canvas pixel coordinates.
722
+ *
723
+ * The foreground canvas has a HUGE internal resolution (matches the raw
724
+ * screenshot, e.g. 2880×1800 on retina) but is displayed at a small CSS
725
+ * size in the modal (~400px wide). `clientX - rect.left` returns CSS
726
+ * pixels, so we must scale up to internal pixels — otherwise a 100px CSS
727
+ * drag becomes a 100/2880 ≈ 3.5% rectangle pinned to the top-left.
728
+ */
729
+ _canvasPos(e) {
730
+ const rect = this.foreground.getBoundingClientRect();
731
+ const scaleX = rect.width > 0 ? this.foreground.width / rect.width : 1;
732
+ const scaleY = rect.height > 0 ? this.foreground.height / rect.height : 1;
733
+ return {
734
+ x: (e.clientX - rect.left) * scaleX,
735
+ y: (e.clientY - rect.top) * scaleY
736
+ };
737
+ }
738
+ _attachMouseListeners() {
739
+ const fg = this.foreground;
740
+ const onMouseDown = /* @__PURE__ */ __name((e) => {
741
+ if (e.button !== 0) return;
742
+ this.dragging = true;
743
+ this.dragStart = this._canvasPos(e);
744
+ }, "onMouseDown");
745
+ const onMouseMove = /* @__PURE__ */ __name((e) => {
746
+ if (!this.dragging || !this.dragStart) return;
747
+ const current = this._canvasPos(e);
748
+ const preview = this._buildCommand(this.dragStart, current);
749
+ const accent = getAccentColor(this.cssRoot);
750
+ paintForeground(this.foreground, [
751
+ ...this.tools.commands,
752
+ preview
753
+ ], accent);
754
+ }, "onMouseMove");
755
+ const onMouseUp = /* @__PURE__ */ __name((e) => {
756
+ if (!this.dragging || !this.dragStart) return;
757
+ this.dragging = false;
758
+ const end = this._canvasPos(e);
759
+ const cmd = this._buildCommand(this.dragStart, end);
760
+ const W = this.foreground.width || 1;
761
+ const H = this.foreground.height || 1;
762
+ if (Math.abs(cmd.w) * W > 4 && Math.abs(cmd.h) * H > 4) {
763
+ this.tools.addCommand(this._normalizeCommand(cmd));
764
+ } else {
765
+ this._repaint();
766
+ }
767
+ this.dragStart = null;
768
+ }, "onMouseUp");
769
+ fg.addEventListener("mousedown", onMouseDown);
770
+ fg.addEventListener("mousemove", onMouseMove);
771
+ fg.addEventListener("mouseup", onMouseUp);
772
+ this.cleanupListeners.push(() => fg.removeEventListener("mousedown", onMouseDown), () => fg.removeEventListener("mousemove", onMouseMove), () => fg.removeEventListener("mouseup", onMouseUp));
773
+ }
774
+ /** Build a raw (pixel-space) DrawCommand from two mouse positions. */
775
+ _buildCommand(start, end) {
776
+ const x = Math.min(start.x, end.x);
777
+ const y = Math.min(start.y, end.y);
778
+ const w = Math.abs(end.x - start.x);
779
+ const h = Math.abs(end.y - start.y);
780
+ const W = this.foreground.width || 1;
781
+ const H = this.foreground.height || 1;
782
+ return {
783
+ type: this.tools.activeTool,
784
+ x: x / W,
785
+ y: y / H,
786
+ w: w / W,
787
+ h: h / H
788
+ };
789
+ }
790
+ /** Ensure a command's coordinates are normalized (clamp to 0..1). */
791
+ _normalizeCommand(cmd) {
792
+ const clamp = /* @__PURE__ */ __name((v) => Math.max(0, Math.min(1, v)), "clamp");
793
+ return {
794
+ type: cmd.type,
795
+ x: clamp(cmd.x),
796
+ y: clamp(cmd.y),
797
+ w: clamp(cmd.w),
798
+ h: clamp(cmd.h)
799
+ };
800
+ }
801
+ };
802
+
803
+ // src/capture/support.ts
804
+ function isNativeCaptureSupported() {
805
+ if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
806
+ return false;
807
+ }
808
+ if (/Macintosh/i.test(navigator.userAgent) && navigator.maxTouchPoints > 1) {
809
+ return false;
810
+ }
811
+ if (!isSecureContext) {
812
+ return false;
813
+ }
814
+ return typeof navigator.mediaDevices?.getDisplayMedia === "function";
815
+ }
816
+ __name(isNativeCaptureSupported, "isNativeCaptureSupported");
817
+
818
+ // src/capture/native.ts
819
+ async function nativeCapture(opts = {}) {
820
+ const dpi = window.devicePixelRatio || 1;
821
+ const hiddenList = Array.isArray(opts.hideElement) ? opts.hideElement.filter((el) => !!el) : opts.hideElement ? [
822
+ opts.hideElement
823
+ ] : [];
824
+ const prev = hiddenList.map((el) => ({
825
+ el,
826
+ display: el.style.display,
827
+ visibility: el.style.visibility
828
+ }));
829
+ for (const el of hiddenList) {
830
+ el.style.visibility = "hidden";
831
+ el.style.display = "none";
832
+ }
833
+ const restore = /* @__PURE__ */ __name(() => {
834
+ for (const { el, display, visibility } of prev) {
835
+ el.style.display = display;
836
+ el.style.visibility = visibility;
837
+ }
838
+ }, "restore");
839
+ let stream;
840
+ try {
841
+ stream = await navigator.mediaDevices.getDisplayMedia({
842
+ video: {
843
+ width: window.innerWidth * dpi,
844
+ height: window.innerHeight * dpi
845
+ },
846
+ audio: false,
847
+ // @ts-expect-error — non-standard constraint flags (Chromium 111+)
848
+ monitorTypeSurfaces: "exclude",
849
+ preferCurrentTab: true,
850
+ selfBrowserSurface: "include",
851
+ surfaceSwitching: "exclude"
852
+ });
853
+ } catch (err) {
854
+ restore();
855
+ throw err;
856
+ }
857
+ try {
858
+ const video = document.createElement("video");
859
+ video.srcObject = stream;
860
+ video.muted = true;
861
+ await new Promise((resolve, reject) => {
862
+ video.onloadedmetadata = () => resolve();
863
+ video.onerror = () => reject(new Error("Video element error during screenshot"));
864
+ });
865
+ await video.play().catch(() => {
866
+ });
867
+ await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => r())));
868
+ const canvas = document.createElement("canvas");
869
+ canvas.width = video.videoWidth || window.innerWidth * dpi;
870
+ canvas.height = video.videoHeight || window.innerHeight * dpi;
871
+ const ctx = canvas.getContext("2d");
872
+ if (!ctx) throw new Error("Could not get 2D canvas context for screenshot");
873
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
874
+ for (const track of stream.getTracks()) track.stop();
875
+ video.srcObject = null;
876
+ return {
877
+ canvas,
878
+ dpi
879
+ };
880
+ } finally {
881
+ restore();
882
+ }
883
+ }
884
+ __name(nativeCapture, "nativeCapture");
885
+
886
+ // src/capture/fallback.ts
887
+ async function fallbackCapture(onTainted, opts = {}) {
888
+ let html2canvas;
889
+ try {
890
+ const mod = await import("html2canvas");
891
+ html2canvas = mod.default ?? mod;
892
+ } catch (importErr) {
893
+ throw new Error("html2canvas is not installed. Install it as a peer dependency or use a browser that supports getDisplayMedia.");
894
+ }
895
+ const hiddenList = Array.isArray(opts.hideElement) ? opts.hideElement.filter((el) => !!el) : opts.hideElement ? [
896
+ opts.hideElement
897
+ ] : [];
898
+ const prev = hiddenList.map((el) => ({
899
+ el,
900
+ display: el.style.display,
901
+ visibility: el.style.visibility
902
+ }));
903
+ for (const el of hiddenList) {
904
+ el.style.visibility = "hidden";
905
+ el.style.display = "none";
906
+ }
907
+ const restore = /* @__PURE__ */ __name(() => {
908
+ for (const { el, display, visibility } of prev) {
909
+ el.style.display = display;
910
+ el.style.visibility = visibility;
911
+ }
912
+ }, "restore");
913
+ let tainted = false;
914
+ try {
915
+ try {
916
+ const canvas = await html2canvas(document.body, {
917
+ allowTaint: false,
918
+ useCORS: true,
919
+ logging: false,
920
+ scale: window.devicePixelRatio || 1
921
+ });
922
+ return {
923
+ canvas,
924
+ tainted
925
+ };
926
+ } catch (err) {
927
+ if (err instanceof DOMException && err.name === "SecurityError") {
928
+ tainted = true;
929
+ onTainted?.();
930
+ const canvas = await html2canvas(document.body, {
931
+ allowTaint: true,
932
+ useCORS: false,
933
+ logging: false,
934
+ scale: window.devicePixelRatio || 1
935
+ }).catch(() => {
936
+ const blank = document.createElement("canvas");
937
+ blank.width = window.innerWidth;
938
+ blank.height = window.innerHeight;
939
+ return blank;
940
+ });
941
+ return {
942
+ canvas,
943
+ tainted
944
+ };
945
+ }
946
+ throw err;
947
+ }
948
+ } finally {
949
+ restore();
950
+ }
951
+ }
952
+ __name(fallbackCapture, "fallbackCapture");
953
+
954
+ // src/capture/index.ts
955
+ async function capture(opts = {}) {
956
+ const useNative = opts.useNativeScreenshot !== false;
957
+ if (useNative && isNativeCaptureSupported()) {
958
+ try {
959
+ const result = await nativeCapture({
960
+ hideElement: opts.hideElement
961
+ });
962
+ return {
963
+ ...result,
964
+ method: "native"
965
+ };
966
+ } catch (err) {
967
+ if (err instanceof DOMException && err.name === "NotAllowedError") {
968
+ return null;
969
+ }
970
+ opts.onFallback?.();
971
+ }
972
+ } else if (useNative) {
973
+ opts.onFallback?.();
974
+ }
975
+ try {
976
+ const result = await fallbackCapture(opts.onTainted, {
977
+ hideElement: opts.hideElement
978
+ });
979
+ return {
980
+ canvas: result.canvas,
981
+ dpi: window.devicePixelRatio || 1,
982
+ method: "fallback",
983
+ tainted: result.tainted
984
+ };
985
+ } catch {
986
+ return null;
987
+ }
988
+ }
989
+ __name(capture, "capture");
990
+
991
+ // src/modal/form.ts
992
+ function buildFormElement(opts) {
993
+ const container = document.createElement("div");
994
+ const header = document.createElement("div");
995
+ header.className = "it-header";
996
+ const title = document.createElement("h2");
997
+ title.className = "it-title";
998
+ title.textContent = "Report a Bug";
999
+ const closeBtn = document.createElement("button");
1000
+ closeBtn.className = "it-close-btn";
1001
+ closeBtn.setAttribute("aria-label", "Close");
1002
+ closeBtn.textContent = "\u2715";
1003
+ closeBtn.addEventListener("click", opts.onCancel);
1004
+ header.appendChild(title);
1005
+ header.appendChild(closeBtn);
1006
+ container.appendChild(header);
1007
+ let capturedCanvas = null;
1008
+ let annotator = null;
1009
+ let annotations = [];
1010
+ const screenshotArea = document.createElement("div");
1011
+ screenshotArea.className = "it-screenshot-area";
1012
+ const placeholder = document.createElement("div");
1013
+ placeholder.className = "it-screenshot-placeholder";
1014
+ placeholder.textContent = "No screenshot yet.";
1015
+ const captureBtn = document.createElement("button");
1016
+ captureBtn.className = "it-btn";
1017
+ captureBtn.textContent = "Add Screenshot";
1018
+ let taintedWarning = null;
1019
+ const screenshotPreview = document.createElement("div");
1020
+ screenshotPreview.className = "it-screenshot-preview";
1021
+ screenshotPreview.style.display = "none";
1022
+ const stage = document.createElement("div");
1023
+ stage.className = "it-screenshot-stage";
1024
+ const backgroundCanvas = document.createElement("canvas");
1025
+ backgroundCanvas.className = "it-background-canvas";
1026
+ const foregroundCanvas = document.createElement("canvas");
1027
+ foregroundCanvas.className = "it-foreground-canvas";
1028
+ stage.appendChild(backgroundCanvas);
1029
+ stage.appendChild(foregroundCanvas);
1030
+ screenshotPreview.appendChild(stage);
1031
+ const toolbar = document.createElement("div");
1032
+ toolbar.className = "it-toolbar";
1033
+ toolbar.style.display = "none";
1034
+ const highlightBtn = document.createElement("button");
1035
+ highlightBtn.className = "it-btn it-btn-tool active";
1036
+ highlightBtn.textContent = "\u25AD Highlight";
1037
+ const hideBtn = document.createElement("button");
1038
+ hideBtn.className = "it-btn it-btn-tool";
1039
+ hideBtn.textContent = "\u25A0 Hide";
1040
+ const undoBtn = document.createElement("button");
1041
+ undoBtn.className = "it-btn";
1042
+ undoBtn.textContent = "\u21A9 Undo";
1043
+ const clearBtn = document.createElement("button");
1044
+ clearBtn.className = "it-btn";
1045
+ clearBtn.textContent = "\u2715 Clear";
1046
+ toolbar.appendChild(highlightBtn);
1047
+ toolbar.appendChild(hideBtn);
1048
+ toolbar.appendChild(undoBtn);
1049
+ toolbar.appendChild(clearBtn);
1050
+ screenshotArea.appendChild(placeholder);
1051
+ screenshotArea.appendChild(captureBtn);
1052
+ screenshotArea.appendChild(screenshotPreview);
1053
+ screenshotArea.appendChild(toolbar);
1054
+ container.appendChild(screenshotArea);
1055
+ captureBtn.addEventListener("click", async () => {
1056
+ captureBtn.disabled = true;
1057
+ captureBtn.textContent = "Capturing\u2026";
1058
+ try {
1059
+ const result = await capture({
1060
+ useNativeScreenshot: opts.widgetOpts.useNativeScreenshot,
1061
+ hideElement: opts.hideForCapture ?? null,
1062
+ onTainted: /* @__PURE__ */ __name(() => {
1063
+ if (!taintedWarning) {
1064
+ taintedWarning = document.createElement("div");
1065
+ taintedWarning.className = "it-warning";
1066
+ taintedWarning.textContent = "Some content was blocked by browser security. You can still submit without a full screenshot.";
1067
+ screenshotArea.appendChild(taintedWarning);
1068
+ }
1069
+ }, "onTainted"),
1070
+ onFallback: /* @__PURE__ */ __name(() => {
1071
+ if (!opts.widgetOpts.silent) {
1072
+ console.info("[InstantTasks Widget] Using html2canvas fallback for screenshot.");
1073
+ }
1074
+ }, "onFallback")
1075
+ });
1076
+ if (result) {
1077
+ capturedCanvas = result.canvas;
1078
+ backgroundCanvas.width = result.canvas.width;
1079
+ backgroundCanvas.height = result.canvas.height;
1080
+ const bgCtx = backgroundCanvas.getContext("2d");
1081
+ bgCtx?.drawImage(result.canvas, 0, 0);
1082
+ foregroundCanvas.width = result.canvas.width;
1083
+ foregroundCanvas.height = result.canvas.height;
1084
+ stage.style.aspectRatio = `${result.canvas.width} / ${result.canvas.height}`;
1085
+ annotator?.destroy();
1086
+ annotator = new Annotator({
1087
+ background: backgroundCanvas,
1088
+ foreground: foregroundCanvas,
1089
+ cssRoot: opts.cssRoot,
1090
+ onChange: /* @__PURE__ */ __name((cmds) => {
1091
+ annotations = cmds;
1092
+ }, "onChange")
1093
+ });
1094
+ highlightBtn.addEventListener("click", () => {
1095
+ annotator?.setTool("highlight");
1096
+ highlightBtn.classList.add("active");
1097
+ hideBtn.classList.remove("active");
1098
+ });
1099
+ hideBtn.addEventListener("click", () => {
1100
+ annotator?.setTool("hide");
1101
+ hideBtn.classList.add("active");
1102
+ highlightBtn.classList.remove("active");
1103
+ });
1104
+ undoBtn.addEventListener("click", () => annotator?.undo());
1105
+ clearBtn.addEventListener("click", () => annotator?.clear());
1106
+ placeholder.style.display = "none";
1107
+ captureBtn.textContent = "Re-capture";
1108
+ screenshotPreview.style.display = "";
1109
+ toolbar.style.display = "";
1110
+ } else {
1111
+ captureBtn.textContent = "Add Screenshot";
1112
+ }
1113
+ } catch {
1114
+ captureBtn.textContent = "Add Screenshot";
1115
+ } finally {
1116
+ captureBtn.disabled = false;
1117
+ }
1118
+ });
1119
+ const descSection = document.createElement("div");
1120
+ descSection.className = "it-section";
1121
+ const descLabel = document.createElement("label");
1122
+ descLabel.className = "it-label";
1123
+ descLabel.textContent = "Description *";
1124
+ const descArea = document.createElement("textarea");
1125
+ descArea.className = "it-textarea";
1126
+ descArea.placeholder = "Describe the bug (at least 10 characters)\u2026";
1127
+ descArea.rows = 4;
1128
+ const descError = document.createElement("div");
1129
+ descError.className = "it-error";
1130
+ descSection.appendChild(descLabel);
1131
+ descSection.appendChild(descArea);
1132
+ descSection.appendChild(descError);
1133
+ container.appendChild(descSection);
1134
+ const titleSection = document.createElement("div");
1135
+ titleSection.className = "it-section";
1136
+ const titleLabel = document.createElement("label");
1137
+ titleLabel.className = "it-label";
1138
+ titleLabel.textContent = "Title (optional)";
1139
+ const titleInput = document.createElement("input");
1140
+ titleInput.className = "it-input";
1141
+ titleInput.type = "text";
1142
+ titleInput.placeholder = "Auto-derived from description if blank";
1143
+ titleInput.maxLength = 200;
1144
+ titleSection.appendChild(titleLabel);
1145
+ titleSection.appendChild(titleInput);
1146
+ container.appendChild(titleSection);
1147
+ let emailInput = null;
1148
+ if (opts.widgetOpts.collectEmail) {
1149
+ const emailSection = document.createElement("div");
1150
+ emailSection.className = "it-section";
1151
+ const emailLabel = document.createElement("label");
1152
+ emailLabel.className = "it-label";
1153
+ emailLabel.textContent = "Email (optional)";
1154
+ emailInput = document.createElement("input");
1155
+ emailInput.className = "it-input";
1156
+ emailInput.type = "email";
1157
+ emailInput.placeholder = "you@example.com";
1158
+ if (opts.widgetOpts.reporter?.email) {
1159
+ emailInput.value = opts.widgetOpts.reporter.email;
1160
+ }
1161
+ emailSection.appendChild(emailLabel);
1162
+ emailSection.appendChild(emailInput);
1163
+ container.appendChild(emailSection);
1164
+ }
1165
+ const submitError = document.createElement("div");
1166
+ submitError.className = "it-error";
1167
+ container.appendChild(submitError);
1168
+ const footer = document.createElement("div");
1169
+ footer.className = "it-footer";
1170
+ const brandingEl = document.createElement("a");
1171
+ brandingEl.className = "it-branding";
1172
+ brandingEl.href = "https://tasks.koderlabs.net";
1173
+ brandingEl.target = "_blank";
1174
+ brandingEl.rel = "noopener";
1175
+ brandingEl.textContent = "Powered by InstantTasks";
1176
+ brandingEl.style.display = opts.showBranding !== false ? "" : "none";
1177
+ const actions = document.createElement("div");
1178
+ actions.className = "it-actions";
1179
+ const cancelBtn = document.createElement("button");
1180
+ cancelBtn.className = "it-btn";
1181
+ cancelBtn.textContent = "Cancel";
1182
+ cancelBtn.addEventListener("click", opts.onCancel);
1183
+ const submitBtn = document.createElement("button");
1184
+ submitBtn.className = "it-btn it-btn-primary";
1185
+ submitBtn.textContent = "Submit";
1186
+ actions.appendChild(cancelBtn);
1187
+ actions.appendChild(submitBtn);
1188
+ footer.appendChild(brandingEl);
1189
+ footer.appendChild(actions);
1190
+ container.appendChild(footer);
1191
+ submitBtn.addEventListener("click", async () => {
1192
+ const desc = descArea.value.trim();
1193
+ if (desc.length < 10) {
1194
+ descError.textContent = "Description must be at least 10 characters.";
1195
+ descArea.focus();
1196
+ return;
1197
+ }
1198
+ descError.textContent = "";
1199
+ submitError.textContent = "";
1200
+ let screenshotForSubmit;
1201
+ if (capturedCanvas && annotator) {
1202
+ const out = document.createElement("canvas");
1203
+ annotator.composite(out);
1204
+ screenshotForSubmit = out;
1205
+ } else if (capturedCanvas) {
1206
+ screenshotForSubmit = capturedCanvas;
1207
+ }
1208
+ const derivedTitle = titleInput.value.trim() || (desc.length > 80 ? desc.slice(0, 80) + "\u2026" : desc);
1209
+ const payload = {
1210
+ title: derivedTitle,
1211
+ description: desc,
1212
+ email: emailInput?.value.trim() || void 0,
1213
+ screenshot: screenshotForSubmit,
1214
+ annotations
1215
+ };
1216
+ submitBtn.disabled = true;
1217
+ submitBtn.innerHTML = '<span class="it-spinner"></span>Submitting\u2026';
1218
+ try {
1219
+ const result = await opts.onSubmit(payload);
1220
+ container.innerHTML = "";
1221
+ const modalEl = container.closest(".it-modal");
1222
+ modalEl?.classList.add("it-modal--success");
1223
+ const successEl = document.createElement("div");
1224
+ successEl.className = "it-success";
1225
+ successEl.innerHTML = `
1226
+ <div class="it-success-icon">\u2713</div>
1227
+ <div class="it-success-title">Bug reported</div>
1228
+ <a class="it-success-ticket" href="${result.ticketUrl}" target="_blank" rel="noopener">${result.ticketKey}</a>
1229
+ <div class="it-success-hint">Closing in 4 seconds\u2026</div>
1230
+ `;
1231
+ container.appendChild(successEl);
1232
+ setTimeout(() => opts.onCancel(), 4e3);
1233
+ } catch (err) {
1234
+ submitError.textContent = err instanceof Error ? err.message : "Submission failed. Please try again.";
1235
+ submitBtn.disabled = false;
1236
+ submitBtn.textContent = "Submit";
1237
+ }
1238
+ });
1239
+ if (emailInput && opts.widgetOpts.reporter?.email) {
1240
+ emailInput.value = opts.widgetOpts.reporter.email;
1241
+ }
1242
+ setTimeout(() => descArea.focus(), 50);
1243
+ if (opts.widgetOpts.autoCapture !== false) {
1244
+ requestAnimationFrame(() => requestAnimationFrame(() => captureBtn.click()));
1245
+ }
1246
+ return container;
1247
+ }
1248
+ __name(buildFormElement, "buildFormElement");
1249
+
1250
+ // src/transport.ts
1251
+ function canvasToBlob(canvas) {
1252
+ return new Promise((resolve, reject) => {
1253
+ canvas.toBlob((blob) => {
1254
+ if (blob) resolve(blob);
1255
+ else reject(new Error("Failed to convert canvas to PNG blob"));
1256
+ }, "image/png");
1257
+ });
1258
+ }
1259
+ __name(canvasToBlob, "canvasToBlob");
1260
+ async function submitBugReport(client, payload) {
1261
+ const meta = await collectMetadata(payload.captureConsole, payload.customDataFn);
1262
+ const fullDescription = payload.description + formatMetadataBlock(meta);
1263
+ const event = {
1264
+ kind: "bug_report",
1265
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1266
+ url: meta.url,
1267
+ userAgent: meta.userAgent,
1268
+ viewport: meta.viewport,
1269
+ payload: {
1270
+ title: payload.title,
1271
+ description: fullDescription,
1272
+ email: payload.email ?? payload.reporter?.email,
1273
+ annotations: payload.annotations,
1274
+ reporter: payload.reporter,
1275
+ metadata: {
1276
+ appVersion: meta.appVersion,
1277
+ customData: {
1278
+ ...meta.customData,
1279
+ ...payload.customData
1280
+ },
1281
+ consoleTail: meta.consoleTail
1282
+ }
1283
+ }
1284
+ };
1285
+ const attachments = /* @__PURE__ */ new Map();
1286
+ if (payload.screenshot) {
1287
+ try {
1288
+ const pngBlob = await canvasToBlob(payload.screenshot);
1289
+ attachments.set("screenshot", pngBlob);
1290
+ } catch {
1291
+ }
1292
+ }
1293
+ const result = await client.send(event, attachments.size > 0 ? attachments : void 0);
1294
+ const ticketKey = result.ticketKey ?? "unknown";
1295
+ const baseUrl = client.options.endpoint;
1296
+ const ticketUrl = `${baseUrl}/tickets/${ticketKey}`;
1297
+ return {
1298
+ ticketKey,
1299
+ ticketUrl
1300
+ };
1301
+ }
1302
+ __name(submitBugReport, "submitBugReport");
1303
+
1304
+ // src/index.ts
1305
+ var WidgetIntegration = class WidgetIntegration2 {
1306
+ static {
1307
+ __name(this, "WidgetIntegration");
1308
+ }
1309
+ name = "widget";
1310
+ opts;
1311
+ /** Caller-supplied options, before defaults were applied. Used to decide
1312
+ * which fields can be overwritten by server-side project config. */
1313
+ _initialOpts;
1314
+ client = null;
1315
+ shell = null;
1316
+ fabBtn = null;
1317
+ /** Outer shadow-host element for the FAB. Tracked separately from the
1318
+ * button so capture flows can hide the *entire* widget chrome (not just
1319
+ * the modal) from screenshots. */
1320
+ fabHost = null;
1321
+ _visible = false;
1322
+ _reporter;
1323
+ _customData = {};
1324
+ /** Reserved for upcoming network capture filters (Phase I).
1325
+ * Public so the field stays type-checked and isn't tree-shaken; safe to
1326
+ * read but currently has no effect. */
1327
+ networkSettings = {};
1328
+ listeners = /* @__PURE__ */ new Map();
1329
+ cleanupHotkey = /* @__PURE__ */ __name(() => {
1330
+ }, "cleanupHotkey");
1331
+ cleanupConsole = /* @__PURE__ */ __name(() => {
1332
+ }, "cleanupConsole");
1333
+ constructor(opts) {
1334
+ this._initialOpts = {
1335
+ ...opts
1336
+ };
1337
+ this.opts = {
1338
+ hotkey: "mod+shift+b",
1339
+ autoInject: true,
1340
+ useNativeScreenshot: true,
1341
+ autoCapture: true,
1342
+ showBranding: true,
1343
+ ...opts
1344
+ };
1345
+ this._reporter = opts.reporter;
1346
+ this._customData = {
1347
+ ...opts.customData
1348
+ };
1349
+ }
1350
+ setup(client) {
1351
+ this.client = client;
1352
+ if (this.opts.captureConsole) {
1353
+ import("./metadata-BXXSPXC5.js").then(({ patchConsole: patchConsole2 }) => {
1354
+ this.cleanupConsole = patchConsole2();
1355
+ }).catch(() => {
1356
+ });
1357
+ }
1358
+ void this._applyRemoteConfig(client).finally(() => this._wireRuntime());
1359
+ }
1360
+ /**
1361
+ * GET /sdk/v1/config — uses the SDK's own configured endpoint + access
1362
+ * key (the client already has both). Merges into this.opts only for
1363
+ * fields the caller didn't explicitly set.
1364
+ */
1365
+ async _applyRemoteConfig(client) {
1366
+ try {
1367
+ const endpoint = client.options.endpoint?.replace(/\/+$/, "");
1368
+ const accessKey = client.options.accessKey;
1369
+ const projectId = client.options.projectId;
1370
+ if (!endpoint || !accessKey || !projectId) return;
1371
+ const res = await fetch(`${endpoint}/api/v1/sdk/v1/config`, {
1372
+ method: "GET",
1373
+ headers: {
1374
+ Authorization: `Bearer ${accessKey}`,
1375
+ "X-Project-Id": projectId
1376
+ }
1377
+ });
1378
+ if (!res.ok) return;
1379
+ const body = await res.json().catch(() => null);
1380
+ const cfg = body?.sdkConfig;
1381
+ if (!cfg) return;
1382
+ const init = this._initialOpts;
1383
+ if (init.hotkey === void 0 && typeof cfg.hotkey === "string") {
1384
+ this.opts.hotkey = cfg.hotkey;
1385
+ }
1386
+ if (init.autoInject === void 0 && typeof cfg.showFab === "boolean") {
1387
+ this.opts.autoInject = cfg.showFab;
1388
+ }
1389
+ if (cfg.enabled === false) {
1390
+ this.opts.hotkey = false;
1391
+ this.opts.autoInject = false;
1392
+ }
1393
+ } catch {
1394
+ }
1395
+ }
1396
+ /** Wires hotkey + FAB + debug surface. Called after _applyRemoteConfig. */
1397
+ _wireRuntime() {
1398
+ this.cleanupHotkey = registerHotkey(this.opts.hotkey, () => this.show());
1399
+ if (this.opts.autoInject) {
1400
+ this._injectFab();
1401
+ }
1402
+ const proc = globalThis.process;
1403
+ const isProd = proc?.env?.NODE_ENV === "production";
1404
+ const exposeGlobal = this.opts.exposeGlobal ?? !isProd;
1405
+ if (exposeGlobal) {
1406
+ try {
1407
+ const w = window;
1408
+ w.InstantTasks = {
1409
+ show: /* @__PURE__ */ __name(() => this.show(), "show"),
1410
+ hotkey: this.opts.hotkey
1411
+ };
1412
+ if (!this.opts.silent) {
1413
+ console.info(`[InstantTasks Widget] ready. Hotkey: ${this.opts.hotkey}. Type \`InstantTasks.show()\` in DevTools to open it.`);
1414
+ }
1415
+ } catch {
1416
+ }
1417
+ } else if (!this.opts.silent) {
1418
+ try {
1419
+ console.info(`[InstantTasks Widget] ready. Hotkey: ${this.opts.hotkey}.`);
1420
+ } catch {
1421
+ }
1422
+ }
1423
+ this._emit({
1424
+ name: "load"
1425
+ });
1426
+ }
1427
+ teardown() {
1428
+ this.cleanupHotkey();
1429
+ this.cleanupConsole();
1430
+ this.shell?.destroy();
1431
+ this.shell = null;
1432
+ if (this.fabBtn?.parentNode) this.fabBtn.parentNode.removeChild(this.fabBtn);
1433
+ this.fabBtn = null;
1434
+ this._visible = false;
1435
+ }
1436
+ // ── WidgetApi methods ──────────────────────────────────────────────────────
1437
+ show() {
1438
+ if (this._visible) return;
1439
+ if (!this.client) return;
1440
+ this._visible = true;
1441
+ this._openModal();
1442
+ this._emit({
1443
+ name: "show"
1444
+ });
1445
+ }
1446
+ hide() {
1447
+ if (!this._visible) return;
1448
+ this.shell?.close();
1449
+ this._visible = false;
1450
+ this._emit({
1451
+ name: "hide"
1452
+ });
1453
+ }
1454
+ isVisible() {
1455
+ return this._visible;
1456
+ }
1457
+ async capture(mode) {
1458
+ void mode;
1459
+ this.show();
1460
+ }
1461
+ cancelCapture() {
1462
+ this.hide();
1463
+ }
1464
+ setReporter(info) {
1465
+ this._reporter = info;
1466
+ }
1467
+ clearReporter() {
1468
+ this._reporter = void 0;
1469
+ }
1470
+ setCustomData(data) {
1471
+ this._customData = {
1472
+ ...this._customData,
1473
+ ...data
1474
+ };
1475
+ }
1476
+ setNetworkRecordingSettings(s) {
1477
+ this.networkSettings = s;
1478
+ }
1479
+ on(event, listener) {
1480
+ const arr = this.listeners.get(event) ?? [];
1481
+ arr.push(listener);
1482
+ this.listeners.set(event, arr);
1483
+ }
1484
+ off(event, listener) {
1485
+ const arr = this.listeners.get(event) ?? [];
1486
+ this.listeners.set(event, arr.filter((l) => l !== listener));
1487
+ }
1488
+ unload() {
1489
+ this.teardown();
1490
+ }
1491
+ // ── Private ────────────────────────────────────────────────────────────────
1492
+ _openModal() {
1493
+ const shell = new ModalShell({
1494
+ onClose: /* @__PURE__ */ __name(() => {
1495
+ this._visible = false;
1496
+ this._emit({
1497
+ name: "hide"
1498
+ });
1499
+ }, "onClose")
1500
+ });
1501
+ this.shell?.destroy();
1502
+ this.shell = shell;
1503
+ const formEl = buildFormElement({
1504
+ widgetOpts: this.opts,
1505
+ cssRoot: shell.shadowRoot,
1506
+ // Hide modal AND FAB during capture — both live as separate shadow
1507
+ // hosts on document.body and would otherwise show in the screenshot.
1508
+ hideForCapture: [
1509
+ shell.host,
1510
+ this.fabHost
1511
+ ].filter((el) => !!el),
1512
+ showBranding: this.opts.showBranding,
1513
+ onCancel: /* @__PURE__ */ __name(() => {
1514
+ this._emit({
1515
+ name: "feedbackdiscarded"
1516
+ });
1517
+ shell.close();
1518
+ this._visible = false;
1519
+ }, "onCancel"),
1520
+ onSubmit: /* @__PURE__ */ __name(async (payload) => {
1521
+ let cancelled = false;
1522
+ const mutableValues = {
1523
+ description: payload.description,
1524
+ title: payload.title,
1525
+ email: payload.email,
1526
+ labels: void 0,
1527
+ assignee: void 0,
1528
+ customFields: void 0,
1529
+ priority: void 0,
1530
+ issueType: void 0
1531
+ };
1532
+ const beforeSendListeners = this.listeners.get("feedbackbeforesend") ?? [];
1533
+ for (const listener of beforeSendListeners) {
1534
+ listener({
1535
+ name: "feedbackbeforesend",
1536
+ values: mutableValues,
1537
+ setValue: /* @__PURE__ */ __name((field, value) => {
1538
+ const MUTABLE = /* @__PURE__ */ new Set([
1539
+ "assignee",
1540
+ "labels",
1541
+ "customFields",
1542
+ "priority",
1543
+ "issueType"
1544
+ ]);
1545
+ if (MUTABLE.has(field)) mutableValues[field] = value;
1546
+ }, "setValue"),
1547
+ cancel: /* @__PURE__ */ __name(() => {
1548
+ cancelled = true;
1549
+ }, "cancel")
1550
+ });
1551
+ if (cancelled) break;
1552
+ }
1553
+ if (cancelled) {
1554
+ this._emit({
1555
+ name: "feedbackdiscarded"
1556
+ });
1557
+ shell.close();
1558
+ this._visible = false;
1559
+ throw new Error("Cancelled by feedbackbeforesend handler");
1560
+ }
1561
+ if (!this.client) throw new Error("Client not initialized");
1562
+ const result = await submitBugReport(this.client, {
1563
+ title: mutableValues.title ?? payload.title,
1564
+ description: mutableValues.description ?? payload.description,
1565
+ email: mutableValues.email ?? payload.email,
1566
+ screenshot: payload.screenshot,
1567
+ annotations: payload.annotations,
1568
+ reporter: this._reporter,
1569
+ customData: {
1570
+ ...this._customData,
1571
+ ...mutableValues.customFields
1572
+ }
1573
+ });
1574
+ this._emit({
1575
+ name: "feedbacksent",
1576
+ ...result
1577
+ });
1578
+ return result;
1579
+ }, "onSubmit")
1580
+ });
1581
+ shell.open(formEl);
1582
+ }
1583
+ _injectFab() {
1584
+ const fabHost = document.createElement("div");
1585
+ fabHost.setAttribute("data-it-fab", "");
1586
+ const shadow = fabHost.attachShadow({
1587
+ mode: "open"
1588
+ });
1589
+ const style = document.createElement("style");
1590
+ style.textContent = `
1591
+ .it-fab {
1592
+ position: fixed; bottom: 20px; right: 20px;
1593
+ z-index: 2147483646;
1594
+ width: 44px; height: 44px; border-radius: 50%;
1595
+ background: #4f46e5; color: #fff;
1596
+ border: none; cursor: pointer; padding: 0;
1597
+ box-shadow: 0 4px 12px rgba(0,0,0,0.18), 0 1px 2px rgba(0,0,0,0.12);
1598
+ display: inline-flex;
1599
+ align-items: center; justify-content: center;
1600
+ transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
1601
+ font-family: system-ui, -apple-system, sans-serif;
1602
+ }
1603
+ .it-fab:hover {
1604
+ transform: translateY(-1px) scale(1.04);
1605
+ background: #4338ca;
1606
+ box-shadow: 0 8px 20px rgba(0,0,0,0.22);
1607
+ }
1608
+ .it-fab:focus-visible {
1609
+ outline: 2px solid #c7d2fe;
1610
+ outline-offset: 2px;
1611
+ }
1612
+ .it-fab svg { width: 20px; height: 20px; display: block; }
1613
+ `;
1614
+ const fab = document.createElement("button");
1615
+ fab.className = "it-fab";
1616
+ fab.setAttribute("aria-label", "Report a bug");
1617
+ fab.setAttribute("title", "Report a bug");
1618
+ fab.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><line x1="12" y1="7" x2="12" y2="11"/><circle cx="12" cy="14.5" r="1"/></svg>';
1619
+ fab.addEventListener("click", () => this.show());
1620
+ shadow.appendChild(style);
1621
+ shadow.appendChild(fab);
1622
+ document.body.appendChild(fabHost);
1623
+ this.fabBtn = fab;
1624
+ this.fabHost = fabHost;
1625
+ }
1626
+ _emit(event) {
1627
+ const name = event.name;
1628
+ const listeners = this.listeners.get(name) ?? [];
1629
+ for (const l of listeners) {
1630
+ try {
1631
+ l(event);
1632
+ } catch (e) {
1633
+ console.error("[InstantTasks Widget] listener threw", e);
1634
+ }
1635
+ }
1636
+ }
1637
+ };
1638
+ function reporterIntegration(opts = {}) {
1639
+ return new WidgetIntegration(opts);
1640
+ }
1641
+ __name(reporterIntegration, "reporterIntegration");
1642
+ var widgetIntegration = reporterIntegration;
1643
+ var src_default = reporterIntegration;
1644
+ export {
1645
+ WidgetIntegration as ReporterIntegration,
1646
+ WidgetIntegration,
1647
+ collectMetadata,
1648
+ src_default as default,
1649
+ formatMetadataBlock,
1650
+ isNativeCaptureSupported,
1651
+ parseHotkey,
1652
+ patchConsole,
1653
+ registerHotkey,
1654
+ reporterIntegration,
1655
+ widgetIntegration
1656
+ };
1657
+ //# sourceMappingURL=index.js.map