@triagly/sdk 1.4.0-beta.2 → 1.4.0-beta.3
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/annotation.d.ts +9 -1
- package/dist/annotation.d.ts.map +1 -1
- package/dist/index.esm.js +229 -225
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +229 -225
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/types.d.ts +2 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +14 -2
- package/dist/ui.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/screenshot.d.ts +0 -42
- package/dist/screenshot.d.ts.map +0 -1
package/dist/annotation.d.ts
CHANGED
|
@@ -35,9 +35,17 @@ export declare class AnnotationEditor {
|
|
|
35
35
|
*/
|
|
36
36
|
private setupCanvas;
|
|
37
37
|
/**
|
|
38
|
-
* Set up event listeners
|
|
38
|
+
* Set up UI event listeners (buttons, color picker, etc.)
|
|
39
39
|
*/
|
|
40
40
|
private setupEventListeners;
|
|
41
|
+
/**
|
|
42
|
+
* Set up canvas drawing event listeners
|
|
43
|
+
*/
|
|
44
|
+
private setupCanvasEventListeners;
|
|
45
|
+
/**
|
|
46
|
+
* Remove canvas drawing event listeners
|
|
47
|
+
*/
|
|
48
|
+
private removeCanvasEventListeners;
|
|
41
49
|
private handleKeyDown;
|
|
42
50
|
/**
|
|
43
51
|
* Get canvas coordinates from mouse event
|
package/dist/annotation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"annotation.d.ts","sourceRoot":"","sources":["../src/annotation.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;AAezE,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,GAAG,CAAyC;IACpD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,iBAAiB,CAA2B;IACpD,OAAO,CAAC,UAAU,CAAmC;IACrD,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,gBAAgB,CAAsB;gBAG5C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,EAC5C,QAAQ,EAAE,MAAM,IAAI;IAOtB;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"annotation.d.ts","sourceRoot":"","sources":["../src/annotation.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;AAezE,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,GAAG,CAAyC;IACpD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,iBAAiB,CAA2B;IACpD,OAAO,CAAC,UAAU,CAAmC;IACrD,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,gBAAgB,CAAsB;gBAG5C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,EAC5C,QAAQ,EAAE,MAAM,IAAI;IAOtB;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB3B;;OAEG;IACH,OAAO,CAAC,SAAS;IASjB;;OAEG;IACH,OAAO,CAAC,aAAa;IAuErB;;OAEG;IACH,OAAO,CAAC,WAAW;IA4CnB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAkE3B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAwBjC;;OAEG;IACH,OAAO,CAAC,0BAA0B;IAUlC,OAAO,CAAC,aAAa,CAInB;IAEF;;OAEG;IACH,OAAO,CAAC,cAAc;IAUtB;;OAEG;IACH,OAAO,CAAC,eAAe;IAoBvB;;OAEG;IACH,OAAO,CAAC,aAAa;IAWrB;;OAEG;IACH,OAAO,CAAC,aAAa;IASrB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAiB7B;;OAEG;IACH,OAAO,CAAC,eAAe;IAoBvB;;OAEG;IACH,OAAO,CAAC,aAAa;IASrB;;OAEG;IACH,OAAO,CAAC,MAAM;IAoBd;;OAEG;IACH,OAAO,CAAC,cAAc;IAyBtB;;OAEG;IACH,OAAO,CAAC,aAAa;IAgBrB;;OAEG;IACH,OAAO,CAAC,SAAS;IA4BjB;;OAEG;IACH,OAAO,CAAC,YAAY;IAapB;;OAEG;IACH,OAAO,CAAC,QAAQ;IA2BhB;;OAEG;IACH,OAAO,CAAC,KAAK;IAoBb;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAoDxB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,cAAc;IAiBtB,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,aAAa;IAcrB;;OAEG;IACH,OAAO,CAAC,YAAY;CA2MrB"}
|
package/dist/index.esm.js
CHANGED
|
@@ -1,157 +1,3 @@
|
|
|
1
|
-
// Screenshot Capture Module
|
|
2
|
-
// Dynamically loads html2canvas and captures the page
|
|
3
|
-
// CDN URLs for html2canvas with fallbacks
|
|
4
|
-
const HTML2CANVAS_CDN_URLS = [
|
|
5
|
-
'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js',
|
|
6
|
-
'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js',
|
|
7
|
-
'https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js',
|
|
8
|
-
];
|
|
9
|
-
class ScreenshotCapture {
|
|
10
|
-
constructor(config = {}) {
|
|
11
|
-
this.html2canvasLoaded = false;
|
|
12
|
-
this.loadPromise = null;
|
|
13
|
-
this.quality = config.quality ?? 0.8;
|
|
14
|
-
this.maxWidth = config.maxWidth ?? 1920;
|
|
15
|
-
this.options = config.html2canvasOptions ?? {};
|
|
16
|
-
this.customCdnUrl = config.html2canvasCdnUrl;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Dynamically load html2canvas from CDN with fallbacks
|
|
20
|
-
* Returns true if loaded successfully, false otherwise
|
|
21
|
-
*/
|
|
22
|
-
async loadHtml2Canvas() {
|
|
23
|
-
// Return cached result if already attempted
|
|
24
|
-
if (this.html2canvasLoaded && window.html2canvas) {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
// Return existing promise if load is in progress
|
|
28
|
-
if (this.loadPromise) {
|
|
29
|
-
return this.loadPromise;
|
|
30
|
-
}
|
|
31
|
-
// Check if already available in window (user may have loaded it)
|
|
32
|
-
if (window.html2canvas) {
|
|
33
|
-
this.html2canvasLoaded = true;
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
// Build list of CDNs to try (custom URL first if provided)
|
|
37
|
-
const cdnUrls = this.customCdnUrl
|
|
38
|
-
? [this.customCdnUrl, ...HTML2CANVAS_CDN_URLS]
|
|
39
|
-
: HTML2CANVAS_CDN_URLS;
|
|
40
|
-
// Try loading from CDNs with fallback
|
|
41
|
-
this.loadPromise = this.tryLoadFromCdns(cdnUrls);
|
|
42
|
-
return this.loadPromise;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Try loading html2canvas from multiple CDN URLs
|
|
46
|
-
*/
|
|
47
|
-
async tryLoadFromCdns(urls) {
|
|
48
|
-
for (const url of urls) {
|
|
49
|
-
const success = await this.loadScriptFromUrl(url);
|
|
50
|
-
if (success && window.html2canvas) {
|
|
51
|
-
this.html2canvasLoaded = true;
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
console.error('[Triagly] Failed to load html2canvas from all CDN sources');
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Load a script from a specific URL
|
|
60
|
-
*/
|
|
61
|
-
loadScriptFromUrl(url) {
|
|
62
|
-
return new Promise((resolve) => {
|
|
63
|
-
const script = document.createElement('script');
|
|
64
|
-
script.src = url;
|
|
65
|
-
script.async = true;
|
|
66
|
-
const cleanup = () => {
|
|
67
|
-
script.onload = null;
|
|
68
|
-
script.onerror = null;
|
|
69
|
-
};
|
|
70
|
-
script.onload = () => {
|
|
71
|
-
cleanup();
|
|
72
|
-
resolve(true);
|
|
73
|
-
};
|
|
74
|
-
script.onerror = () => {
|
|
75
|
-
cleanup();
|
|
76
|
-
// Remove failed script from DOM
|
|
77
|
-
script.remove();
|
|
78
|
-
resolve(false);
|
|
79
|
-
};
|
|
80
|
-
document.head.appendChild(script);
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Capture screenshot of the current page
|
|
85
|
-
* Returns base64 data URL of the screenshot
|
|
86
|
-
*/
|
|
87
|
-
async capture() {
|
|
88
|
-
const loaded = await this.loadHtml2Canvas();
|
|
89
|
-
if (!loaded || !window.html2canvas) {
|
|
90
|
-
throw new Error('Screenshot capture requires html2canvas. ' +
|
|
91
|
-
'Include <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> ' +
|
|
92
|
-
'or ensure network access to CDN.');
|
|
93
|
-
}
|
|
94
|
-
// Hide the Triagly widget elements during capture
|
|
95
|
-
const widgetElements = document.querySelectorAll('.triagly-overlay, .triagly-button, #triagly-button');
|
|
96
|
-
const originalVisibility = new Map();
|
|
97
|
-
widgetElements.forEach((el) => {
|
|
98
|
-
const htmlEl = el;
|
|
99
|
-
originalVisibility.set(el, htmlEl.style.visibility);
|
|
100
|
-
htmlEl.style.visibility = 'hidden';
|
|
101
|
-
});
|
|
102
|
-
try {
|
|
103
|
-
const canvas = await window.html2canvas(document.body, {
|
|
104
|
-
useCORS: true,
|
|
105
|
-
allowTaint: false,
|
|
106
|
-
logging: false,
|
|
107
|
-
backgroundColor: null,
|
|
108
|
-
scale: Math.min(window.devicePixelRatio || 1, 2), // Cap at 2x for performance
|
|
109
|
-
...this.options,
|
|
110
|
-
});
|
|
111
|
-
// Resize if needed
|
|
112
|
-
const resized = this.resizeCanvas(canvas);
|
|
113
|
-
// Convert to JPEG data URL
|
|
114
|
-
return resized.toDataURL('image/jpeg', this.quality);
|
|
115
|
-
}
|
|
116
|
-
finally {
|
|
117
|
-
// Restore widget visibility
|
|
118
|
-
widgetElements.forEach((el) => {
|
|
119
|
-
const htmlEl = el;
|
|
120
|
-
htmlEl.style.visibility = originalVisibility.get(el) || '';
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Resize canvas if it exceeds maxWidth
|
|
126
|
-
*/
|
|
127
|
-
resizeCanvas(canvas) {
|
|
128
|
-
if (canvas.width <= this.maxWidth) {
|
|
129
|
-
return canvas;
|
|
130
|
-
}
|
|
131
|
-
const ratio = this.maxWidth / canvas.width;
|
|
132
|
-
const resized = document.createElement('canvas');
|
|
133
|
-
resized.width = this.maxWidth;
|
|
134
|
-
resized.height = Math.round(canvas.height * ratio);
|
|
135
|
-
const ctx = resized.getContext('2d');
|
|
136
|
-
if (ctx) {
|
|
137
|
-
// Use high-quality image smoothing
|
|
138
|
-
ctx.imageSmoothingEnabled = true;
|
|
139
|
-
ctx.imageSmoothingQuality = 'high';
|
|
140
|
-
ctx.drawImage(canvas, 0, 0, resized.width, resized.height);
|
|
141
|
-
}
|
|
142
|
-
return resized;
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Get approximate file size of a data URL in bytes
|
|
146
|
-
*/
|
|
147
|
-
static getDataUrlSize(dataUrl) {
|
|
148
|
-
// Remove data URL prefix to get base64 content
|
|
149
|
-
const base64 = dataUrl.split(',')[1] || '';
|
|
150
|
-
// Base64 encodes 3 bytes into 4 characters
|
|
151
|
-
return Math.round((base64.length * 3) / 4);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
1
|
// Annotation Editor Module
|
|
156
2
|
// Canvas-based annotation overlay for marking up screenshots
|
|
157
3
|
class AnnotationEditor {
|
|
@@ -189,6 +35,8 @@ class AnnotationEditor {
|
|
|
189
35
|
this.createOverlay();
|
|
190
36
|
// Set up the canvas
|
|
191
37
|
this.setupCanvas();
|
|
38
|
+
// Set up canvas drawing event listeners (must be after setupCanvas)
|
|
39
|
+
this.setupCanvasEventListeners();
|
|
192
40
|
// Draw the image
|
|
193
41
|
this.render();
|
|
194
42
|
}
|
|
@@ -314,10 +162,10 @@ class AnnotationEditor {
|
|
|
314
162
|
this.offsetY = (containerRect.height - canvasHeight) / 2;
|
|
315
163
|
}
|
|
316
164
|
/**
|
|
317
|
-
* Set up event listeners
|
|
165
|
+
* Set up UI event listeners (buttons, color picker, etc.)
|
|
318
166
|
*/
|
|
319
167
|
setupEventListeners() {
|
|
320
|
-
if (!this.overlay
|
|
168
|
+
if (!this.overlay)
|
|
321
169
|
return;
|
|
322
170
|
// Tool selection
|
|
323
171
|
const toolBtns = this.overlay.querySelectorAll('.triagly-tool-btn');
|
|
@@ -346,34 +194,16 @@ class AnnotationEditor {
|
|
|
346
194
|
this.render();
|
|
347
195
|
});
|
|
348
196
|
}
|
|
349
|
-
// Cancel button
|
|
350
|
-
const
|
|
351
|
-
if (
|
|
352
|
-
|
|
197
|
+
// Cancel button - use class selector to get the header cancel button only
|
|
198
|
+
const headerCancelBtn = this.overlay.querySelector('.triagly-annotation-actions .triagly-btn-cancel');
|
|
199
|
+
if (headerCancelBtn) {
|
|
200
|
+
headerCancelBtn.addEventListener('click', () => this.close(true));
|
|
353
201
|
}
|
|
354
202
|
// Done button
|
|
355
203
|
const doneBtn = this.overlay.querySelector('.triagly-btn-done');
|
|
356
204
|
if (doneBtn) {
|
|
357
205
|
doneBtn.addEventListener('click', () => this.close(false));
|
|
358
206
|
}
|
|
359
|
-
// Canvas drawing events
|
|
360
|
-
this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e));
|
|
361
|
-
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
|
|
362
|
-
this.canvas.addEventListener('mouseup', () => this.handleMouseUp());
|
|
363
|
-
this.canvas.addEventListener('mouseleave', () => this.handleMouseUp());
|
|
364
|
-
// Touch support
|
|
365
|
-
this.canvas.addEventListener('touchstart', (e) => {
|
|
366
|
-
e.preventDefault();
|
|
367
|
-
const touch = e.touches[0];
|
|
368
|
-
this.handleMouseDown(touch);
|
|
369
|
-
});
|
|
370
|
-
this.canvas.addEventListener('touchmove', (e) => {
|
|
371
|
-
e.preventDefault();
|
|
372
|
-
const touch = e.touches[0];
|
|
373
|
-
this.handleMouseMove(touch);
|
|
374
|
-
});
|
|
375
|
-
this.canvas.addEventListener('touchend', () => this.handleMouseUp());
|
|
376
|
-
this.canvas.addEventListener('touchcancel', () => this.handleMouseUp());
|
|
377
207
|
// Text modal event listeners
|
|
378
208
|
const textConfirmBtn = this.overlay.querySelector('#triagly-text-confirm');
|
|
379
209
|
const textCancelBtn = this.overlay.querySelector('#triagly-text-cancel');
|
|
@@ -393,6 +223,43 @@ class AnnotationEditor {
|
|
|
393
223
|
// Escape key to cancel
|
|
394
224
|
document.addEventListener('keydown', this.handleKeyDown);
|
|
395
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Set up canvas drawing event listeners
|
|
228
|
+
*/
|
|
229
|
+
setupCanvasEventListeners() {
|
|
230
|
+
if (!this.canvas)
|
|
231
|
+
return;
|
|
232
|
+
// Canvas drawing events
|
|
233
|
+
this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e));
|
|
234
|
+
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
|
|
235
|
+
this.canvas.addEventListener('mouseup', () => this.handleMouseUp());
|
|
236
|
+
this.canvas.addEventListener('mouseleave', () => this.handleMouseUp());
|
|
237
|
+
// Touch support (use passive: false to allow preventDefault)
|
|
238
|
+
this.canvas.addEventListener('touchstart', (e) => {
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
const touch = e.touches[0];
|
|
241
|
+
this.handleMouseDown(touch);
|
|
242
|
+
}, { passive: false });
|
|
243
|
+
this.canvas.addEventListener('touchmove', (e) => {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
const touch = e.touches[0];
|
|
246
|
+
this.handleMouseMove(touch);
|
|
247
|
+
}, { passive: false });
|
|
248
|
+
this.canvas.addEventListener('touchend', () => this.handleMouseUp());
|
|
249
|
+
this.canvas.addEventListener('touchcancel', () => this.handleMouseUp());
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Remove canvas drawing event listeners
|
|
253
|
+
*/
|
|
254
|
+
removeCanvasEventListeners() {
|
|
255
|
+
if (!this.canvas)
|
|
256
|
+
return;
|
|
257
|
+
// Remove all canvas event listeners to prevent memory leaks
|
|
258
|
+
// Note: We can't remove the exact listeners since they're arrow functions,
|
|
259
|
+
// but removing the overlay (which contains the canvas) will clean them up
|
|
260
|
+
this.canvas = null;
|
|
261
|
+
this.ctx = null;
|
|
262
|
+
}
|
|
396
263
|
/**
|
|
397
264
|
* Get canvas coordinates from mouse event
|
|
398
265
|
*/
|
|
@@ -624,7 +491,9 @@ class AnnotationEditor {
|
|
|
624
491
|
* Close the editor
|
|
625
492
|
*/
|
|
626
493
|
close(cancelled) {
|
|
494
|
+
// Clean up event listeners
|
|
627
495
|
document.removeEventListener('keydown', this.handleKeyDown);
|
|
496
|
+
this.removeCanvasEventListeners();
|
|
628
497
|
if (cancelled) {
|
|
629
498
|
this.onCancel();
|
|
630
499
|
}
|
|
@@ -754,7 +623,7 @@ class AnnotationEditor {
|
|
|
754
623
|
left: 0;
|
|
755
624
|
right: 0;
|
|
756
625
|
bottom: 0;
|
|
757
|
-
z-index:
|
|
626
|
+
z-index: 2000000;
|
|
758
627
|
background: rgba(0, 0, 0, 0.9);
|
|
759
628
|
display: flex;
|
|
760
629
|
flex-direction: column;
|
|
@@ -953,26 +822,66 @@ class FeedbackWidget {
|
|
|
953
822
|
this.previouslyFocusedElement = null;
|
|
954
823
|
this.focusableElements = [];
|
|
955
824
|
this.screenshotDataUrl = null;
|
|
956
|
-
this.
|
|
825
|
+
this.themeMediaQuery = null;
|
|
826
|
+
/**
|
|
827
|
+
* Update button theme based on current resolved theme
|
|
828
|
+
*/
|
|
829
|
+
this.updateButtonTheme = () => {
|
|
830
|
+
const button = document.getElementById('triagly-button');
|
|
831
|
+
if (!button)
|
|
832
|
+
return;
|
|
833
|
+
const newTheme = this.getResolvedTheme();
|
|
834
|
+
// Remove old theme class and add new one
|
|
835
|
+
button.classList.remove('triagly-theme-light', 'triagly-theme-dark');
|
|
836
|
+
button.classList.add(`triagly-theme-${newTheme}`);
|
|
837
|
+
};
|
|
957
838
|
this.config = config;
|
|
958
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* Get the resolved theme (resolves 'auto' to actual theme)
|
|
842
|
+
*/
|
|
843
|
+
getResolvedTheme() {
|
|
844
|
+
const theme = this.config.theme || 'auto';
|
|
845
|
+
if (theme === 'auto') {
|
|
846
|
+
// Check system preference
|
|
847
|
+
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
848
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
849
|
+
}
|
|
850
|
+
return 'light';
|
|
851
|
+
}
|
|
852
|
+
return theme;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Setup listener for system theme changes (when using 'auto' theme)
|
|
856
|
+
*/
|
|
857
|
+
setupThemeListener() {
|
|
858
|
+
// Only setup listener if theme is 'auto'
|
|
859
|
+
const theme = this.config.theme || 'auto';
|
|
860
|
+
if (theme !== 'auto') {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
864
|
+
this.themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
865
|
+
// Use addListener for older browsers, addEventListener for modern ones
|
|
866
|
+
if (this.themeMediaQuery.addEventListener) {
|
|
867
|
+
this.themeMediaQuery.addEventListener('change', this.updateButtonTheme);
|
|
868
|
+
}
|
|
869
|
+
else if (this.themeMediaQuery.addListener) {
|
|
870
|
+
this.themeMediaQuery.addListener(this.updateButtonTheme);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
959
874
|
/**
|
|
960
875
|
* Initialize the widget
|
|
961
876
|
*/
|
|
962
877
|
init() {
|
|
963
878
|
this.createButton();
|
|
964
879
|
this.injectStyles();
|
|
880
|
+
this.setupThemeListener();
|
|
965
881
|
// Load Turnstile script if configured
|
|
966
882
|
if (this.config.turnstileSiteKey) {
|
|
967
883
|
this.loadTurnstileScript();
|
|
968
884
|
}
|
|
969
|
-
// Initialize screenshot capture if enabled
|
|
970
|
-
if (this.config.enableScreenshot) {
|
|
971
|
-
this.screenshotCapture = new ScreenshotCapture({
|
|
972
|
-
quality: this.config.screenshotQuality,
|
|
973
|
-
maxWidth: this.config.screenshotMaxWidth,
|
|
974
|
-
});
|
|
975
|
-
}
|
|
976
885
|
}
|
|
977
886
|
/**
|
|
978
887
|
* Load Cloudflare Turnstile script dynamically
|
|
@@ -995,6 +904,9 @@ class FeedbackWidget {
|
|
|
995
904
|
const button = document.createElement('button');
|
|
996
905
|
button.id = 'triagly-button';
|
|
997
906
|
button.className = 'triagly-button';
|
|
907
|
+
// Apply theme class
|
|
908
|
+
const theme = this.getResolvedTheme();
|
|
909
|
+
button.classList.add(`triagly-theme-${theme}`);
|
|
998
910
|
// Button shape
|
|
999
911
|
const shape = this.config.buttonShape || 'rounded';
|
|
1000
912
|
button.classList.add(`triagly-shape-${shape}`);
|
|
@@ -1180,16 +1092,18 @@ class FeedbackWidget {
|
|
|
1180
1092
|
${this.config.enableScreenshot ? `
|
|
1181
1093
|
<div class="triagly-field triagly-screenshot-field">
|
|
1182
1094
|
<label>Screenshot (optional)</label>
|
|
1095
|
+
<input type="file" id="triagly-screenshot-input" accept="image/*" style="display: none;" />
|
|
1183
1096
|
<div class="triagly-screenshot-controls" id="triagly-screenshot-controls">
|
|
1184
|
-
<button type="button" class="triagly-btn-
|
|
1097
|
+
<button type="button" class="triagly-btn-upload" id="triagly-upload-btn">
|
|
1185
1098
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1186
|
-
<
|
|
1187
|
-
<
|
|
1188
|
-
<
|
|
1099
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
1100
|
+
<polyline points="17 8 12 3 7 8"/>
|
|
1101
|
+
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
1189
1102
|
</svg>
|
|
1190
|
-
${this.config.screenshotButtonText || '
|
|
1103
|
+
${this.config.screenshotButtonText || 'Upload Screenshot'}
|
|
1191
1104
|
</button>
|
|
1192
1105
|
</div>
|
|
1106
|
+
<div class="triagly-screenshot-error" id="triagly-screenshot-error" style="display: none;" role="alert"></div>
|
|
1193
1107
|
<div class="triagly-screenshot-preview" id="triagly-screenshot-preview" style="display: none;">
|
|
1194
1108
|
<img id="triagly-screenshot-img" alt="Screenshot preview" />
|
|
1195
1109
|
<div class="triagly-screenshot-actions">
|
|
@@ -1201,6 +1115,13 @@ class FeedbackWidget {
|
|
|
1201
1115
|
</svg>
|
|
1202
1116
|
</button>
|
|
1203
1117
|
` : ''}
|
|
1118
|
+
<button type="button" class="triagly-btn-icon" id="triagly-change-screenshot-btn" title="Upload different screenshot">
|
|
1119
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1120
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
1121
|
+
<polyline points="17 8 12 3 7 8"/>
|
|
1122
|
+
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
1123
|
+
</svg>
|
|
1124
|
+
</button>
|
|
1204
1125
|
<button type="button" class="triagly-btn-icon triagly-btn-danger" id="triagly-remove-screenshot-btn" title="Remove screenshot">
|
|
1205
1126
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1206
1127
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
@@ -1294,31 +1215,59 @@ class FeedbackWidget {
|
|
|
1294
1215
|
}
|
|
1295
1216
|
}
|
|
1296
1217
|
/**
|
|
1297
|
-
* Set up screenshot
|
|
1218
|
+
* Set up screenshot upload event handlers
|
|
1298
1219
|
*/
|
|
1299
1220
|
setupScreenshotHandlers(form) {
|
|
1300
|
-
const
|
|
1221
|
+
const fileInput = form.querySelector('#triagly-screenshot-input');
|
|
1222
|
+
const uploadBtn = form.querySelector('#triagly-upload-btn');
|
|
1301
1223
|
const annotateBtn = form.querySelector('#triagly-annotate-btn');
|
|
1224
|
+
const changeBtn = form.querySelector('#triagly-change-screenshot-btn');
|
|
1302
1225
|
const removeBtn = form.querySelector('#triagly-remove-screenshot-btn');
|
|
1303
1226
|
const preview = form.querySelector('#triagly-screenshot-preview');
|
|
1304
1227
|
const controls = form.querySelector('#triagly-screenshot-controls');
|
|
1305
1228
|
const previewImg = form.querySelector('#triagly-screenshot-img');
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1229
|
+
const errorDiv = form.querySelector('#triagly-screenshot-error');
|
|
1230
|
+
// Helper to show error message
|
|
1231
|
+
const showError = (message) => {
|
|
1232
|
+
if (errorDiv) {
|
|
1233
|
+
errorDiv.textContent = message;
|
|
1234
|
+
errorDiv.style.display = 'block';
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
// Helper to hide error message
|
|
1238
|
+
const hideError = () => {
|
|
1239
|
+
if (errorDiv) {
|
|
1240
|
+
errorDiv.textContent = '';
|
|
1241
|
+
errorDiv.style.display = 'none';
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
// Handle file selection
|
|
1245
|
+
const handleFileSelect = (file) => {
|
|
1246
|
+
// Clear any previous error
|
|
1247
|
+
hideError();
|
|
1248
|
+
// Validate file type
|
|
1249
|
+
if (!file.type.startsWith('image/')) {
|
|
1250
|
+
const errorMessage = 'Invalid file type. Please select an image.';
|
|
1251
|
+
showError(errorMessage);
|
|
1252
|
+
if (this.config.onScreenshotError) {
|
|
1253
|
+
this.config.onScreenshotError(new Error(errorMessage));
|
|
1254
|
+
}
|
|
1309
1255
|
return;
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1256
|
+
}
|
|
1257
|
+
// Validate file size (configurable, default 10MB)
|
|
1258
|
+
const maxSize = this.config.screenshotMaxFileSize ?? 10 * 1024 * 1024;
|
|
1259
|
+
if (file.size > maxSize) {
|
|
1260
|
+
const maxSizeMB = Math.round(maxSize / (1024 * 1024));
|
|
1261
|
+
const errorMessage = `File too large. Maximum size is ${maxSizeMB}MB.`;
|
|
1262
|
+
showError(errorMessage);
|
|
1263
|
+
if (this.config.onScreenshotError) {
|
|
1264
|
+
this.config.onScreenshotError(new Error(errorMessage));
|
|
1265
|
+
}
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const reader = new FileReader();
|
|
1269
|
+
reader.onload = (e) => {
|
|
1270
|
+
const dataUrl = e.target?.result;
|
|
1322
1271
|
this.screenshotDataUrl = dataUrl;
|
|
1323
1272
|
// Update UI
|
|
1324
1273
|
if (previewImg)
|
|
@@ -1328,20 +1277,37 @@ class FeedbackWidget {
|
|
|
1328
1277
|
if (controls)
|
|
1329
1278
|
controls.style.display = 'none';
|
|
1330
1279
|
// Call callback if provided
|
|
1331
|
-
if (this.config.
|
|
1332
|
-
this.config.
|
|
1280
|
+
if (this.config.onScreenshotUpload) {
|
|
1281
|
+
this.config.onScreenshotUpload(dataUrl);
|
|
1333
1282
|
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1283
|
+
};
|
|
1284
|
+
reader.onerror = () => {
|
|
1285
|
+
const errorMessage = 'Failed to read file. Please try again.';
|
|
1286
|
+
showError(errorMessage);
|
|
1337
1287
|
if (this.config.onScreenshotError) {
|
|
1338
|
-
this.config.onScreenshotError(
|
|
1288
|
+
this.config.onScreenshotError(new Error(errorMessage));
|
|
1339
1289
|
}
|
|
1290
|
+
};
|
|
1291
|
+
reader.readAsDataURL(file);
|
|
1292
|
+
};
|
|
1293
|
+
// File input change handler
|
|
1294
|
+
fileInput?.addEventListener('change', () => {
|
|
1295
|
+
const file = fileInput.files?.[0];
|
|
1296
|
+
if (file) {
|
|
1297
|
+
handleFileSelect(file);
|
|
1340
1298
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1299
|
+
// Reset input so same file can be selected again
|
|
1300
|
+
fileInput.value = '';
|
|
1301
|
+
});
|
|
1302
|
+
// Upload button triggers file input
|
|
1303
|
+
uploadBtn?.addEventListener('click', () => {
|
|
1304
|
+
hideError();
|
|
1305
|
+
fileInput?.click();
|
|
1306
|
+
});
|
|
1307
|
+
// Change screenshot - trigger file input again
|
|
1308
|
+
changeBtn?.addEventListener('click', () => {
|
|
1309
|
+
hideError();
|
|
1310
|
+
fileInput?.click();
|
|
1345
1311
|
});
|
|
1346
1312
|
// Annotate screenshot
|
|
1347
1313
|
annotateBtn?.addEventListener('click', () => {
|
|
@@ -1474,6 +1440,24 @@ class FeedbackWidget {
|
|
|
1474
1440
|
box-shadow: var(--triagly-button-shadow-hover, 0 6px 16px rgba(0, 0, 0, 0.2));
|
|
1475
1441
|
}
|
|
1476
1442
|
|
|
1443
|
+
/* Dark theme: white button with black text */
|
|
1444
|
+
.triagly-button.triagly-theme-dark {
|
|
1445
|
+
--triagly-button-bg: #ffffff;
|
|
1446
|
+
--triagly-button-text: #18181b;
|
|
1447
|
+
--triagly-button-bg-hover: #f4f4f5;
|
|
1448
|
+
--triagly-button-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
|
1449
|
+
--triagly-button-shadow-hover: 0 6px 16px rgba(0, 0, 0, 0.35);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/* Light theme: dark button with white text (default) */
|
|
1453
|
+
.triagly-button.triagly-theme-light {
|
|
1454
|
+
--triagly-button-bg: #18181b;
|
|
1455
|
+
--triagly-button-text: #ffffff;
|
|
1456
|
+
--triagly-button-bg-hover: #27272a;
|
|
1457
|
+
--triagly-button-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
1458
|
+
--triagly-button-shadow-hover: 0 6px 16px rgba(0, 0, 0, 0.2);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1477
1461
|
.triagly-button .triagly-icon {
|
|
1478
1462
|
flex-shrink: 0;
|
|
1479
1463
|
}
|
|
@@ -1573,8 +1557,8 @@ class FeedbackWidget {
|
|
|
1573
1557
|
min-width: auto;
|
|
1574
1558
|
padding: 12px 20px;
|
|
1575
1559
|
border-radius: 30px;
|
|
1576
|
-
background: var(--triagly-button-bg-hover
|
|
1577
|
-
box-shadow: var(--triagly-button-shadow-hover
|
|
1560
|
+
background: var(--triagly-button-bg-hover);
|
|
1561
|
+
box-shadow: var(--triagly-button-shadow-hover);
|
|
1578
1562
|
}
|
|
1579
1563
|
.triagly-shape-expandable:hover .triagly-btn-text {
|
|
1580
1564
|
width: auto;
|
|
@@ -1694,6 +1678,7 @@ class FeedbackWidget {
|
|
|
1694
1678
|
.triagly-field input:focus,
|
|
1695
1679
|
.triagly-field textarea:focus {
|
|
1696
1680
|
outline: none;
|
|
1681
|
+
background: var(--triagly-input-bg, #ffffff);
|
|
1697
1682
|
border-color: var(--triagly-input-border-focus, #a1a1aa);
|
|
1698
1683
|
box-shadow: 0 0 0 2px rgba(161, 161, 170, 0.15);
|
|
1699
1684
|
}
|
|
@@ -1809,7 +1794,7 @@ class FeedbackWidget {
|
|
|
1809
1794
|
gap: 8px;
|
|
1810
1795
|
}
|
|
1811
1796
|
|
|
1812
|
-
.triagly-btn-
|
|
1797
|
+
.triagly-btn-upload {
|
|
1813
1798
|
display: inline-flex;
|
|
1814
1799
|
align-items: center;
|
|
1815
1800
|
gap: 8px;
|
|
@@ -1826,14 +1811,23 @@ class FeedbackWidget {
|
|
|
1826
1811
|
justify-content: center;
|
|
1827
1812
|
}
|
|
1828
1813
|
|
|
1829
|
-
.triagly-btn-
|
|
1814
|
+
.triagly-btn-upload:hover:not(:disabled) {
|
|
1830
1815
|
background: var(--triagly-btn-secondary-bg-hover, #e5e7eb);
|
|
1831
1816
|
border-style: solid;
|
|
1832
1817
|
}
|
|
1833
1818
|
|
|
1834
|
-
.triagly-btn-
|
|
1819
|
+
.triagly-btn-upload:disabled {
|
|
1835
1820
|
opacity: 0.7;
|
|
1836
|
-
cursor:
|
|
1821
|
+
cursor: not-allowed;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
.triagly-screenshot-error {
|
|
1825
|
+
margin-top: 8px;
|
|
1826
|
+
padding: 8px 12px;
|
|
1827
|
+
background: var(--triagly-error-bg, #fee2e2);
|
|
1828
|
+
color: var(--triagly-error-text, #991b1b);
|
|
1829
|
+
border-radius: var(--triagly-input-radius, 6px);
|
|
1830
|
+
font-size: 13px;
|
|
1837
1831
|
}
|
|
1838
1832
|
|
|
1839
1833
|
.triagly-screenshot-preview {
|
|
@@ -1974,6 +1968,16 @@ class FeedbackWidget {
|
|
|
1974
1968
|
document.removeEventListener('keydown', tabHandler, true);
|
|
1975
1969
|
}
|
|
1976
1970
|
}
|
|
1971
|
+
// Clean up theme listener
|
|
1972
|
+
if (this.themeMediaQuery) {
|
|
1973
|
+
if (this.themeMediaQuery.removeEventListener) {
|
|
1974
|
+
this.themeMediaQuery.removeEventListener('change', this.updateButtonTheme);
|
|
1975
|
+
}
|
|
1976
|
+
else if (this.themeMediaQuery.removeListener) {
|
|
1977
|
+
this.themeMediaQuery.removeListener(this.updateButtonTheme);
|
|
1978
|
+
}
|
|
1979
|
+
this.themeMediaQuery = null;
|
|
1980
|
+
}
|
|
1977
1981
|
this.close();
|
|
1978
1982
|
document.getElementById('triagly-button')?.remove();
|
|
1979
1983
|
document.getElementById('triagly-styles')?.remove();
|