@triagly/sdk 1.4.0-beta.2 → 1.4.0-beta.4
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/api.d.ts +3 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +232 -232
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +232 -232
- 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/index.js
CHANGED
|
@@ -4,160 +4,6 @@
|
|
|
4
4
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Triagly = {}));
|
|
5
5
|
})(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
|
-
// Screenshot Capture Module
|
|
8
|
-
// Dynamically loads html2canvas and captures the page
|
|
9
|
-
// CDN URLs for html2canvas with fallbacks
|
|
10
|
-
const HTML2CANVAS_CDN_URLS = [
|
|
11
|
-
'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js',
|
|
12
|
-
'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js',
|
|
13
|
-
'https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js',
|
|
14
|
-
];
|
|
15
|
-
class ScreenshotCapture {
|
|
16
|
-
constructor(config = {}) {
|
|
17
|
-
this.html2canvasLoaded = false;
|
|
18
|
-
this.loadPromise = null;
|
|
19
|
-
this.quality = config.quality ?? 0.8;
|
|
20
|
-
this.maxWidth = config.maxWidth ?? 1920;
|
|
21
|
-
this.options = config.html2canvasOptions ?? {};
|
|
22
|
-
this.customCdnUrl = config.html2canvasCdnUrl;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Dynamically load html2canvas from CDN with fallbacks
|
|
26
|
-
* Returns true if loaded successfully, false otherwise
|
|
27
|
-
*/
|
|
28
|
-
async loadHtml2Canvas() {
|
|
29
|
-
// Return cached result if already attempted
|
|
30
|
-
if (this.html2canvasLoaded && window.html2canvas) {
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
// Return existing promise if load is in progress
|
|
34
|
-
if (this.loadPromise) {
|
|
35
|
-
return this.loadPromise;
|
|
36
|
-
}
|
|
37
|
-
// Check if already available in window (user may have loaded it)
|
|
38
|
-
if (window.html2canvas) {
|
|
39
|
-
this.html2canvasLoaded = true;
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
// Build list of CDNs to try (custom URL first if provided)
|
|
43
|
-
const cdnUrls = this.customCdnUrl
|
|
44
|
-
? [this.customCdnUrl, ...HTML2CANVAS_CDN_URLS]
|
|
45
|
-
: HTML2CANVAS_CDN_URLS;
|
|
46
|
-
// Try loading from CDNs with fallback
|
|
47
|
-
this.loadPromise = this.tryLoadFromCdns(cdnUrls);
|
|
48
|
-
return this.loadPromise;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Try loading html2canvas from multiple CDN URLs
|
|
52
|
-
*/
|
|
53
|
-
async tryLoadFromCdns(urls) {
|
|
54
|
-
for (const url of urls) {
|
|
55
|
-
const success = await this.loadScriptFromUrl(url);
|
|
56
|
-
if (success && window.html2canvas) {
|
|
57
|
-
this.html2canvasLoaded = true;
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
console.error('[Triagly] Failed to load html2canvas from all CDN sources');
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Load a script from a specific URL
|
|
66
|
-
*/
|
|
67
|
-
loadScriptFromUrl(url) {
|
|
68
|
-
return new Promise((resolve) => {
|
|
69
|
-
const script = document.createElement('script');
|
|
70
|
-
script.src = url;
|
|
71
|
-
script.async = true;
|
|
72
|
-
const cleanup = () => {
|
|
73
|
-
script.onload = null;
|
|
74
|
-
script.onerror = null;
|
|
75
|
-
};
|
|
76
|
-
script.onload = () => {
|
|
77
|
-
cleanup();
|
|
78
|
-
resolve(true);
|
|
79
|
-
};
|
|
80
|
-
script.onerror = () => {
|
|
81
|
-
cleanup();
|
|
82
|
-
// Remove failed script from DOM
|
|
83
|
-
script.remove();
|
|
84
|
-
resolve(false);
|
|
85
|
-
};
|
|
86
|
-
document.head.appendChild(script);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Capture screenshot of the current page
|
|
91
|
-
* Returns base64 data URL of the screenshot
|
|
92
|
-
*/
|
|
93
|
-
async capture() {
|
|
94
|
-
const loaded = await this.loadHtml2Canvas();
|
|
95
|
-
if (!loaded || !window.html2canvas) {
|
|
96
|
-
throw new Error('Screenshot capture requires html2canvas. ' +
|
|
97
|
-
'Include <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> ' +
|
|
98
|
-
'or ensure network access to CDN.');
|
|
99
|
-
}
|
|
100
|
-
// Hide the Triagly widget elements during capture
|
|
101
|
-
const widgetElements = document.querySelectorAll('.triagly-overlay, .triagly-button, #triagly-button');
|
|
102
|
-
const originalVisibility = new Map();
|
|
103
|
-
widgetElements.forEach((el) => {
|
|
104
|
-
const htmlEl = el;
|
|
105
|
-
originalVisibility.set(el, htmlEl.style.visibility);
|
|
106
|
-
htmlEl.style.visibility = 'hidden';
|
|
107
|
-
});
|
|
108
|
-
try {
|
|
109
|
-
const canvas = await window.html2canvas(document.body, {
|
|
110
|
-
useCORS: true,
|
|
111
|
-
allowTaint: false,
|
|
112
|
-
logging: false,
|
|
113
|
-
backgroundColor: null,
|
|
114
|
-
scale: Math.min(window.devicePixelRatio || 1, 2), // Cap at 2x for performance
|
|
115
|
-
...this.options,
|
|
116
|
-
});
|
|
117
|
-
// Resize if needed
|
|
118
|
-
const resized = this.resizeCanvas(canvas);
|
|
119
|
-
// Convert to JPEG data URL
|
|
120
|
-
return resized.toDataURL('image/jpeg', this.quality);
|
|
121
|
-
}
|
|
122
|
-
finally {
|
|
123
|
-
// Restore widget visibility
|
|
124
|
-
widgetElements.forEach((el) => {
|
|
125
|
-
const htmlEl = el;
|
|
126
|
-
htmlEl.style.visibility = originalVisibility.get(el) || '';
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Resize canvas if it exceeds maxWidth
|
|
132
|
-
*/
|
|
133
|
-
resizeCanvas(canvas) {
|
|
134
|
-
if (canvas.width <= this.maxWidth) {
|
|
135
|
-
return canvas;
|
|
136
|
-
}
|
|
137
|
-
const ratio = this.maxWidth / canvas.width;
|
|
138
|
-
const resized = document.createElement('canvas');
|
|
139
|
-
resized.width = this.maxWidth;
|
|
140
|
-
resized.height = Math.round(canvas.height * ratio);
|
|
141
|
-
const ctx = resized.getContext('2d');
|
|
142
|
-
if (ctx) {
|
|
143
|
-
// Use high-quality image smoothing
|
|
144
|
-
ctx.imageSmoothingEnabled = true;
|
|
145
|
-
ctx.imageSmoothingQuality = 'high';
|
|
146
|
-
ctx.drawImage(canvas, 0, 0, resized.width, resized.height);
|
|
147
|
-
}
|
|
148
|
-
return resized;
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Get approximate file size of a data URL in bytes
|
|
152
|
-
*/
|
|
153
|
-
static getDataUrlSize(dataUrl) {
|
|
154
|
-
// Remove data URL prefix to get base64 content
|
|
155
|
-
const base64 = dataUrl.split(',')[1] || '';
|
|
156
|
-
// Base64 encodes 3 bytes into 4 characters
|
|
157
|
-
return Math.round((base64.length * 3) / 4);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
7
|
// Annotation Editor Module
|
|
162
8
|
// Canvas-based annotation overlay for marking up screenshots
|
|
163
9
|
class AnnotationEditor {
|
|
@@ -195,6 +41,8 @@
|
|
|
195
41
|
this.createOverlay();
|
|
196
42
|
// Set up the canvas
|
|
197
43
|
this.setupCanvas();
|
|
44
|
+
// Set up canvas drawing event listeners (must be after setupCanvas)
|
|
45
|
+
this.setupCanvasEventListeners();
|
|
198
46
|
// Draw the image
|
|
199
47
|
this.render();
|
|
200
48
|
}
|
|
@@ -320,10 +168,10 @@
|
|
|
320
168
|
this.offsetY = (containerRect.height - canvasHeight) / 2;
|
|
321
169
|
}
|
|
322
170
|
/**
|
|
323
|
-
* Set up event listeners
|
|
171
|
+
* Set up UI event listeners (buttons, color picker, etc.)
|
|
324
172
|
*/
|
|
325
173
|
setupEventListeners() {
|
|
326
|
-
if (!this.overlay
|
|
174
|
+
if (!this.overlay)
|
|
327
175
|
return;
|
|
328
176
|
// Tool selection
|
|
329
177
|
const toolBtns = this.overlay.querySelectorAll('.triagly-tool-btn');
|
|
@@ -352,34 +200,16 @@
|
|
|
352
200
|
this.render();
|
|
353
201
|
});
|
|
354
202
|
}
|
|
355
|
-
// Cancel button
|
|
356
|
-
const
|
|
357
|
-
if (
|
|
358
|
-
|
|
203
|
+
// Cancel button - use class selector to get the header cancel button only
|
|
204
|
+
const headerCancelBtn = this.overlay.querySelector('.triagly-annotation-actions .triagly-btn-cancel');
|
|
205
|
+
if (headerCancelBtn) {
|
|
206
|
+
headerCancelBtn.addEventListener('click', () => this.close(true));
|
|
359
207
|
}
|
|
360
208
|
// Done button
|
|
361
209
|
const doneBtn = this.overlay.querySelector('.triagly-btn-done');
|
|
362
210
|
if (doneBtn) {
|
|
363
211
|
doneBtn.addEventListener('click', () => this.close(false));
|
|
364
212
|
}
|
|
365
|
-
// Canvas drawing events
|
|
366
|
-
this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e));
|
|
367
|
-
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
|
|
368
|
-
this.canvas.addEventListener('mouseup', () => this.handleMouseUp());
|
|
369
|
-
this.canvas.addEventListener('mouseleave', () => this.handleMouseUp());
|
|
370
|
-
// Touch support
|
|
371
|
-
this.canvas.addEventListener('touchstart', (e) => {
|
|
372
|
-
e.preventDefault();
|
|
373
|
-
const touch = e.touches[0];
|
|
374
|
-
this.handleMouseDown(touch);
|
|
375
|
-
});
|
|
376
|
-
this.canvas.addEventListener('touchmove', (e) => {
|
|
377
|
-
e.preventDefault();
|
|
378
|
-
const touch = e.touches[0];
|
|
379
|
-
this.handleMouseMove(touch);
|
|
380
|
-
});
|
|
381
|
-
this.canvas.addEventListener('touchend', () => this.handleMouseUp());
|
|
382
|
-
this.canvas.addEventListener('touchcancel', () => this.handleMouseUp());
|
|
383
213
|
// Text modal event listeners
|
|
384
214
|
const textConfirmBtn = this.overlay.querySelector('#triagly-text-confirm');
|
|
385
215
|
const textCancelBtn = this.overlay.querySelector('#triagly-text-cancel');
|
|
@@ -399,6 +229,43 @@
|
|
|
399
229
|
// Escape key to cancel
|
|
400
230
|
document.addEventListener('keydown', this.handleKeyDown);
|
|
401
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Set up canvas drawing event listeners
|
|
234
|
+
*/
|
|
235
|
+
setupCanvasEventListeners() {
|
|
236
|
+
if (!this.canvas)
|
|
237
|
+
return;
|
|
238
|
+
// Canvas drawing events
|
|
239
|
+
this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e));
|
|
240
|
+
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
|
|
241
|
+
this.canvas.addEventListener('mouseup', () => this.handleMouseUp());
|
|
242
|
+
this.canvas.addEventListener('mouseleave', () => this.handleMouseUp());
|
|
243
|
+
// Touch support (use passive: false to allow preventDefault)
|
|
244
|
+
this.canvas.addEventListener('touchstart', (e) => {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
const touch = e.touches[0];
|
|
247
|
+
this.handleMouseDown(touch);
|
|
248
|
+
}, { passive: false });
|
|
249
|
+
this.canvas.addEventListener('touchmove', (e) => {
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
const touch = e.touches[0];
|
|
252
|
+
this.handleMouseMove(touch);
|
|
253
|
+
}, { passive: false });
|
|
254
|
+
this.canvas.addEventListener('touchend', () => this.handleMouseUp());
|
|
255
|
+
this.canvas.addEventListener('touchcancel', () => this.handleMouseUp());
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Remove canvas drawing event listeners
|
|
259
|
+
*/
|
|
260
|
+
removeCanvasEventListeners() {
|
|
261
|
+
if (!this.canvas)
|
|
262
|
+
return;
|
|
263
|
+
// Remove all canvas event listeners to prevent memory leaks
|
|
264
|
+
// Note: We can't remove the exact listeners since they're arrow functions,
|
|
265
|
+
// but removing the overlay (which contains the canvas) will clean them up
|
|
266
|
+
this.canvas = null;
|
|
267
|
+
this.ctx = null;
|
|
268
|
+
}
|
|
402
269
|
/**
|
|
403
270
|
* Get canvas coordinates from mouse event
|
|
404
271
|
*/
|
|
@@ -630,7 +497,9 @@
|
|
|
630
497
|
* Close the editor
|
|
631
498
|
*/
|
|
632
499
|
close(cancelled) {
|
|
500
|
+
// Clean up event listeners
|
|
633
501
|
document.removeEventListener('keydown', this.handleKeyDown);
|
|
502
|
+
this.removeCanvasEventListeners();
|
|
634
503
|
if (cancelled) {
|
|
635
504
|
this.onCancel();
|
|
636
505
|
}
|
|
@@ -760,7 +629,7 @@
|
|
|
760
629
|
left: 0;
|
|
761
630
|
right: 0;
|
|
762
631
|
bottom: 0;
|
|
763
|
-
z-index:
|
|
632
|
+
z-index: 2000000;
|
|
764
633
|
background: rgba(0, 0, 0, 0.9);
|
|
765
634
|
display: flex;
|
|
766
635
|
flex-direction: column;
|
|
@@ -959,26 +828,66 @@
|
|
|
959
828
|
this.previouslyFocusedElement = null;
|
|
960
829
|
this.focusableElements = [];
|
|
961
830
|
this.screenshotDataUrl = null;
|
|
962
|
-
this.
|
|
831
|
+
this.themeMediaQuery = null;
|
|
832
|
+
/**
|
|
833
|
+
* Update button theme based on current resolved theme
|
|
834
|
+
*/
|
|
835
|
+
this.updateButtonTheme = () => {
|
|
836
|
+
const button = document.getElementById('triagly-button');
|
|
837
|
+
if (!button)
|
|
838
|
+
return;
|
|
839
|
+
const newTheme = this.getResolvedTheme();
|
|
840
|
+
// Remove old theme class and add new one
|
|
841
|
+
button.classList.remove('triagly-theme-light', 'triagly-theme-dark');
|
|
842
|
+
button.classList.add(`triagly-theme-${newTheme}`);
|
|
843
|
+
};
|
|
963
844
|
this.config = config;
|
|
964
845
|
}
|
|
846
|
+
/**
|
|
847
|
+
* Get the resolved theme (resolves 'auto' to actual theme)
|
|
848
|
+
*/
|
|
849
|
+
getResolvedTheme() {
|
|
850
|
+
const theme = this.config.theme || 'auto';
|
|
851
|
+
if (theme === 'auto') {
|
|
852
|
+
// Check system preference
|
|
853
|
+
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
854
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
855
|
+
}
|
|
856
|
+
return 'light';
|
|
857
|
+
}
|
|
858
|
+
return theme;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Setup listener for system theme changes (when using 'auto' theme)
|
|
862
|
+
*/
|
|
863
|
+
setupThemeListener() {
|
|
864
|
+
// Only setup listener if theme is 'auto'
|
|
865
|
+
const theme = this.config.theme || 'auto';
|
|
866
|
+
if (theme !== 'auto') {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
870
|
+
this.themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
871
|
+
// Use addListener for older browsers, addEventListener for modern ones
|
|
872
|
+
if (this.themeMediaQuery.addEventListener) {
|
|
873
|
+
this.themeMediaQuery.addEventListener('change', this.updateButtonTheme);
|
|
874
|
+
}
|
|
875
|
+
else if (this.themeMediaQuery.addListener) {
|
|
876
|
+
this.themeMediaQuery.addListener(this.updateButtonTheme);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
965
880
|
/**
|
|
966
881
|
* Initialize the widget
|
|
967
882
|
*/
|
|
968
883
|
init() {
|
|
969
884
|
this.createButton();
|
|
970
885
|
this.injectStyles();
|
|
886
|
+
this.setupThemeListener();
|
|
971
887
|
// Load Turnstile script if configured
|
|
972
888
|
if (this.config.turnstileSiteKey) {
|
|
973
889
|
this.loadTurnstileScript();
|
|
974
890
|
}
|
|
975
|
-
// Initialize screenshot capture if enabled
|
|
976
|
-
if (this.config.enableScreenshot) {
|
|
977
|
-
this.screenshotCapture = new ScreenshotCapture({
|
|
978
|
-
quality: this.config.screenshotQuality,
|
|
979
|
-
maxWidth: this.config.screenshotMaxWidth,
|
|
980
|
-
});
|
|
981
|
-
}
|
|
982
891
|
}
|
|
983
892
|
/**
|
|
984
893
|
* Load Cloudflare Turnstile script dynamically
|
|
@@ -1001,6 +910,9 @@
|
|
|
1001
910
|
const button = document.createElement('button');
|
|
1002
911
|
button.id = 'triagly-button';
|
|
1003
912
|
button.className = 'triagly-button';
|
|
913
|
+
// Apply theme class
|
|
914
|
+
const theme = this.getResolvedTheme();
|
|
915
|
+
button.classList.add(`triagly-theme-${theme}`);
|
|
1004
916
|
// Button shape
|
|
1005
917
|
const shape = this.config.buttonShape || 'rounded';
|
|
1006
918
|
button.classList.add(`triagly-shape-${shape}`);
|
|
@@ -1186,16 +1098,18 @@
|
|
|
1186
1098
|
${this.config.enableScreenshot ? `
|
|
1187
1099
|
<div class="triagly-field triagly-screenshot-field">
|
|
1188
1100
|
<label>Screenshot (optional)</label>
|
|
1101
|
+
<input type="file" id="triagly-screenshot-input" accept="image/*" style="display: none;" />
|
|
1189
1102
|
<div class="triagly-screenshot-controls" id="triagly-screenshot-controls">
|
|
1190
|
-
<button type="button" class="triagly-btn-
|
|
1103
|
+
<button type="button" class="triagly-btn-upload" id="triagly-upload-btn">
|
|
1191
1104
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1192
|
-
<
|
|
1193
|
-
<
|
|
1194
|
-
<
|
|
1105
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
1106
|
+
<polyline points="17 8 12 3 7 8"/>
|
|
1107
|
+
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
1195
1108
|
</svg>
|
|
1196
|
-
${this.config.screenshotButtonText || '
|
|
1109
|
+
${this.config.screenshotButtonText || 'Upload Screenshot'}
|
|
1197
1110
|
</button>
|
|
1198
1111
|
</div>
|
|
1112
|
+
<div class="triagly-screenshot-error" id="triagly-screenshot-error" style="display: none;" role="alert"></div>
|
|
1199
1113
|
<div class="triagly-screenshot-preview" id="triagly-screenshot-preview" style="display: none;">
|
|
1200
1114
|
<img id="triagly-screenshot-img" alt="Screenshot preview" />
|
|
1201
1115
|
<div class="triagly-screenshot-actions">
|
|
@@ -1207,6 +1121,13 @@
|
|
|
1207
1121
|
</svg>
|
|
1208
1122
|
</button>
|
|
1209
1123
|
` : ''}
|
|
1124
|
+
<button type="button" class="triagly-btn-icon" id="triagly-change-screenshot-btn" title="Upload different screenshot">
|
|
1125
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1126
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
1127
|
+
<polyline points="17 8 12 3 7 8"/>
|
|
1128
|
+
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
1129
|
+
</svg>
|
|
1130
|
+
</button>
|
|
1210
1131
|
<button type="button" class="triagly-btn-icon triagly-btn-danger" id="triagly-remove-screenshot-btn" title="Remove screenshot">
|
|
1211
1132
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1212
1133
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
@@ -1300,31 +1221,59 @@
|
|
|
1300
1221
|
}
|
|
1301
1222
|
}
|
|
1302
1223
|
/**
|
|
1303
|
-
* Set up screenshot
|
|
1224
|
+
* Set up screenshot upload event handlers
|
|
1304
1225
|
*/
|
|
1305
1226
|
setupScreenshotHandlers(form) {
|
|
1306
|
-
const
|
|
1227
|
+
const fileInput = form.querySelector('#triagly-screenshot-input');
|
|
1228
|
+
const uploadBtn = form.querySelector('#triagly-upload-btn');
|
|
1307
1229
|
const annotateBtn = form.querySelector('#triagly-annotate-btn');
|
|
1230
|
+
const changeBtn = form.querySelector('#triagly-change-screenshot-btn');
|
|
1308
1231
|
const removeBtn = form.querySelector('#triagly-remove-screenshot-btn');
|
|
1309
1232
|
const preview = form.querySelector('#triagly-screenshot-preview');
|
|
1310
1233
|
const controls = form.querySelector('#triagly-screenshot-controls');
|
|
1311
1234
|
const previewImg = form.querySelector('#triagly-screenshot-img');
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1235
|
+
const errorDiv = form.querySelector('#triagly-screenshot-error');
|
|
1236
|
+
// Helper to show error message
|
|
1237
|
+
const showError = (message) => {
|
|
1238
|
+
if (errorDiv) {
|
|
1239
|
+
errorDiv.textContent = message;
|
|
1240
|
+
errorDiv.style.display = 'block';
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
// Helper to hide error message
|
|
1244
|
+
const hideError = () => {
|
|
1245
|
+
if (errorDiv) {
|
|
1246
|
+
errorDiv.textContent = '';
|
|
1247
|
+
errorDiv.style.display = 'none';
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
// Handle file selection
|
|
1251
|
+
const handleFileSelect = (file) => {
|
|
1252
|
+
// Clear any previous error
|
|
1253
|
+
hideError();
|
|
1254
|
+
// Validate file type
|
|
1255
|
+
if (!file.type.startsWith('image/')) {
|
|
1256
|
+
const errorMessage = 'Invalid file type. Please select an image.';
|
|
1257
|
+
showError(errorMessage);
|
|
1258
|
+
if (this.config.onScreenshotError) {
|
|
1259
|
+
this.config.onScreenshotError(new Error(errorMessage));
|
|
1260
|
+
}
|
|
1315
1261
|
return;
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1262
|
+
}
|
|
1263
|
+
// Validate file size (configurable, default 10MB)
|
|
1264
|
+
const maxSize = this.config.screenshotMaxFileSize ?? 10 * 1024 * 1024;
|
|
1265
|
+
if (file.size > maxSize) {
|
|
1266
|
+
const maxSizeMB = Math.round(maxSize / (1024 * 1024));
|
|
1267
|
+
const errorMessage = `File too large. Maximum size is ${maxSizeMB}MB.`;
|
|
1268
|
+
showError(errorMessage);
|
|
1269
|
+
if (this.config.onScreenshotError) {
|
|
1270
|
+
this.config.onScreenshotError(new Error(errorMessage));
|
|
1271
|
+
}
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const reader = new FileReader();
|
|
1275
|
+
reader.onload = (e) => {
|
|
1276
|
+
const dataUrl = e.target?.result;
|
|
1328
1277
|
this.screenshotDataUrl = dataUrl;
|
|
1329
1278
|
// Update UI
|
|
1330
1279
|
if (previewImg)
|
|
@@ -1334,20 +1283,37 @@
|
|
|
1334
1283
|
if (controls)
|
|
1335
1284
|
controls.style.display = 'none';
|
|
1336
1285
|
// Call callback if provided
|
|
1337
|
-
if (this.config.
|
|
1338
|
-
this.config.
|
|
1286
|
+
if (this.config.onScreenshotUpload) {
|
|
1287
|
+
this.config.onScreenshotUpload(dataUrl);
|
|
1339
1288
|
}
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1289
|
+
};
|
|
1290
|
+
reader.onerror = () => {
|
|
1291
|
+
const errorMessage = 'Failed to read file. Please try again.';
|
|
1292
|
+
showError(errorMessage);
|
|
1343
1293
|
if (this.config.onScreenshotError) {
|
|
1344
|
-
this.config.onScreenshotError(
|
|
1294
|
+
this.config.onScreenshotError(new Error(errorMessage));
|
|
1345
1295
|
}
|
|
1296
|
+
};
|
|
1297
|
+
reader.readAsDataURL(file);
|
|
1298
|
+
};
|
|
1299
|
+
// File input change handler
|
|
1300
|
+
fileInput?.addEventListener('change', () => {
|
|
1301
|
+
const file = fileInput.files?.[0];
|
|
1302
|
+
if (file) {
|
|
1303
|
+
handleFileSelect(file);
|
|
1346
1304
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1305
|
+
// Reset input so same file can be selected again
|
|
1306
|
+
fileInput.value = '';
|
|
1307
|
+
});
|
|
1308
|
+
// Upload button triggers file input
|
|
1309
|
+
uploadBtn?.addEventListener('click', () => {
|
|
1310
|
+
hideError();
|
|
1311
|
+
fileInput?.click();
|
|
1312
|
+
});
|
|
1313
|
+
// Change screenshot - trigger file input again
|
|
1314
|
+
changeBtn?.addEventListener('click', () => {
|
|
1315
|
+
hideError();
|
|
1316
|
+
fileInput?.click();
|
|
1351
1317
|
});
|
|
1352
1318
|
// Annotate screenshot
|
|
1353
1319
|
annotateBtn?.addEventListener('click', () => {
|
|
@@ -1480,6 +1446,24 @@
|
|
|
1480
1446
|
box-shadow: var(--triagly-button-shadow-hover, 0 6px 16px rgba(0, 0, 0, 0.2));
|
|
1481
1447
|
}
|
|
1482
1448
|
|
|
1449
|
+
/* Dark theme: white button with black text */
|
|
1450
|
+
.triagly-button.triagly-theme-dark {
|
|
1451
|
+
--triagly-button-bg: #ffffff;
|
|
1452
|
+
--triagly-button-text: #18181b;
|
|
1453
|
+
--triagly-button-bg-hover: #f4f4f5;
|
|
1454
|
+
--triagly-button-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
|
1455
|
+
--triagly-button-shadow-hover: 0 6px 16px rgba(0, 0, 0, 0.35);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/* Light theme: dark button with white text (default) */
|
|
1459
|
+
.triagly-button.triagly-theme-light {
|
|
1460
|
+
--triagly-button-bg: #18181b;
|
|
1461
|
+
--triagly-button-text: #ffffff;
|
|
1462
|
+
--triagly-button-bg-hover: #27272a;
|
|
1463
|
+
--triagly-button-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
1464
|
+
--triagly-button-shadow-hover: 0 6px 16px rgba(0, 0, 0, 0.2);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1483
1467
|
.triagly-button .triagly-icon {
|
|
1484
1468
|
flex-shrink: 0;
|
|
1485
1469
|
}
|
|
@@ -1579,8 +1563,8 @@
|
|
|
1579
1563
|
min-width: auto;
|
|
1580
1564
|
padding: 12px 20px;
|
|
1581
1565
|
border-radius: 30px;
|
|
1582
|
-
background: var(--triagly-button-bg-hover
|
|
1583
|
-
box-shadow: var(--triagly-button-shadow-hover
|
|
1566
|
+
background: var(--triagly-button-bg-hover);
|
|
1567
|
+
box-shadow: var(--triagly-button-shadow-hover);
|
|
1584
1568
|
}
|
|
1585
1569
|
.triagly-shape-expandable:hover .triagly-btn-text {
|
|
1586
1570
|
width: auto;
|
|
@@ -1700,6 +1684,7 @@
|
|
|
1700
1684
|
.triagly-field input:focus,
|
|
1701
1685
|
.triagly-field textarea:focus {
|
|
1702
1686
|
outline: none;
|
|
1687
|
+
background: var(--triagly-input-bg, #ffffff);
|
|
1703
1688
|
border-color: var(--triagly-input-border-focus, #a1a1aa);
|
|
1704
1689
|
box-shadow: 0 0 0 2px rgba(161, 161, 170, 0.15);
|
|
1705
1690
|
}
|
|
@@ -1815,7 +1800,7 @@
|
|
|
1815
1800
|
gap: 8px;
|
|
1816
1801
|
}
|
|
1817
1802
|
|
|
1818
|
-
.triagly-btn-
|
|
1803
|
+
.triagly-btn-upload {
|
|
1819
1804
|
display: inline-flex;
|
|
1820
1805
|
align-items: center;
|
|
1821
1806
|
gap: 8px;
|
|
@@ -1832,14 +1817,23 @@
|
|
|
1832
1817
|
justify-content: center;
|
|
1833
1818
|
}
|
|
1834
1819
|
|
|
1835
|
-
.triagly-btn-
|
|
1820
|
+
.triagly-btn-upload:hover:not(:disabled) {
|
|
1836
1821
|
background: var(--triagly-btn-secondary-bg-hover, #e5e7eb);
|
|
1837
1822
|
border-style: solid;
|
|
1838
1823
|
}
|
|
1839
1824
|
|
|
1840
|
-
.triagly-btn-
|
|
1825
|
+
.triagly-btn-upload:disabled {
|
|
1841
1826
|
opacity: 0.7;
|
|
1842
|
-
cursor:
|
|
1827
|
+
cursor: not-allowed;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
.triagly-screenshot-error {
|
|
1831
|
+
margin-top: 8px;
|
|
1832
|
+
padding: 8px 12px;
|
|
1833
|
+
background: var(--triagly-error-bg, #fee2e2);
|
|
1834
|
+
color: var(--triagly-error-text, #991b1b);
|
|
1835
|
+
border-radius: var(--triagly-input-radius, 6px);
|
|
1836
|
+
font-size: 13px;
|
|
1843
1837
|
}
|
|
1844
1838
|
|
|
1845
1839
|
.triagly-screenshot-preview {
|
|
@@ -1980,6 +1974,16 @@
|
|
|
1980
1974
|
document.removeEventListener('keydown', tabHandler, true);
|
|
1981
1975
|
}
|
|
1982
1976
|
}
|
|
1977
|
+
// Clean up theme listener
|
|
1978
|
+
if (this.themeMediaQuery) {
|
|
1979
|
+
if (this.themeMediaQuery.removeEventListener) {
|
|
1980
|
+
this.themeMediaQuery.removeEventListener('change', this.updateButtonTheme);
|
|
1981
|
+
}
|
|
1982
|
+
else if (this.themeMediaQuery.removeListener) {
|
|
1983
|
+
this.themeMediaQuery.removeListener(this.updateButtonTheme);
|
|
1984
|
+
}
|
|
1985
|
+
this.themeMediaQuery = null;
|
|
1986
|
+
}
|
|
1983
1987
|
this.close();
|
|
1984
1988
|
document.getElementById('triagly-button')?.remove();
|
|
1985
1989
|
document.getElementById('triagly-styles')?.remove();
|
|
@@ -1997,11 +2001,11 @@
|
|
|
1997
2001
|
this.apiUrl = (apiUrl || API_URLS[environment]).replace(/\/$/, ''); // Remove trailing slash
|
|
1998
2002
|
this.publishableKey = publishableKey;
|
|
1999
2003
|
this.getToken = getToken;
|
|
2000
|
-
//
|
|
2001
|
-
this.turnstileSiteKey = turnstileSiteKey
|
|
2004
|
+
// Only enable Turnstile when explicitly configured
|
|
2005
|
+
this.turnstileSiteKey = turnstileSiteKey;
|
|
2002
2006
|
}
|
|
2003
2007
|
/**
|
|
2004
|
-
* Get the Turnstile site key
|
|
2008
|
+
* Get the Turnstile site key (undefined if not configured)
|
|
2005
2009
|
*/
|
|
2006
2010
|
getTurnstileSiteKey() {
|
|
2007
2011
|
return this.turnstileSiteKey;
|
|
@@ -2113,8 +2117,6 @@
|
|
|
2113
2117
|
return await response.json();
|
|
2114
2118
|
}
|
|
2115
2119
|
}
|
|
2116
|
-
// Triagly's public Turnstile site key (safe to hardcode - client-side only)
|
|
2117
|
-
TriaglyAPI.DEFAULT_TURNSTILE_SITE_KEY = '0x4AAAAAAB8Dc-Fl964Vp1Nn';
|
|
2118
2120
|
|
|
2119
2121
|
// Utility functions
|
|
2120
2122
|
/**
|
|
@@ -2388,8 +2390,6 @@
|
|
|
2388
2390
|
publishableKey: apiKey, // Keep for backward compatibility
|
|
2389
2391
|
};
|
|
2390
2392
|
this.api = new TriaglyAPI(apiKey, this.config.environment || 'production', this.config.apiUrl, this.config.getToken, this.config.turnstileSiteKey);
|
|
2391
|
-
// Always pass Turnstile site key to widget (from API which has default)
|
|
2392
|
-
this.config.turnstileSiteKey = this.api.getTurnstileSiteKey();
|
|
2393
2393
|
this.widget = new FeedbackWidget(this.config);
|
|
2394
2394
|
this.rateLimiter = new RateLimiter(apiKey, 3, 5 * 60 * 1000);
|
|
2395
2395
|
// Initialize console logger if enabled
|