@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/LICENSE +179 -0
- package/README.md +9 -0
- package/dist/chunk-LDKYNLXK.js +309 -0
- package/dist/chunk-LDKYNLXK.js.map +1 -0
- package/dist/index.cjs +2013 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +362 -0
- package/dist/index.d.ts +362 -0
- package/dist/index.js +1657 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.umd.js +385 -0
- package/dist/metadata-BXXSPXC5.js +13 -0
- package/dist/metadata-BXXSPXC5.js.map +1 -0
- package/package.json +72 -0
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
|