@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/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 || !this.canvas)
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 cancelBtn = this.overlay.querySelector('.triagly-btn-cancel');
357
- if (cancelBtn) {
358
- cancelBtn.addEventListener('click', () => this.close(true));
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: 999999;
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.screenshotCapture = null;
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-capture" id="triagly-capture-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
- <rect x="3" y="3" width="18" height="18" rx="2"/>
1193
- <circle cx="8.5" cy="8.5" r="1.5"/>
1194
- <path d="M21 15l-5-5L5 21"/>
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 || 'Capture Screenshot'}
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 capture event handlers
1224
+ * Set up screenshot upload event handlers
1304
1225
  */
1305
1226
  setupScreenshotHandlers(form) {
1306
- const captureBtn = form.querySelector('#triagly-capture-btn');
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
- // Capture screenshot
1313
- captureBtn?.addEventListener('click', async () => {
1314
- if (!this.screenshotCapture)
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
- const btn = captureBtn;
1317
- const originalText = btn.innerHTML;
1318
- btn.disabled = true;
1319
- btn.innerHTML = `
1320
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" class="triagly-spin">
1321
- <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" opacity="0.3"/>
1322
- <path d="M12 2v4"/>
1323
- </svg>
1324
- Capturing...
1325
- `;
1326
- try {
1327
- const dataUrl = await this.screenshotCapture.capture();
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.onScreenshotCapture) {
1338
- this.config.onScreenshotCapture(dataUrl);
1286
+ if (this.config.onScreenshotUpload) {
1287
+ this.config.onScreenshotUpload(dataUrl);
1339
1288
  }
1340
- }
1341
- catch (error) {
1342
- console.error('Triagly: Screenshot capture failed:', error);
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(error instanceof Error ? error : new Error(String(error)));
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
- finally {
1348
- btn.disabled = false;
1349
- btn.innerHTML = originalText;
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, #27272a);
1583
- box-shadow: var(--triagly-button-shadow-hover, 0 6px 16px rgba(0, 0, 0, 0.2));
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-capture {
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-capture:hover:not(:disabled) {
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-capture:disabled {
1825
+ .triagly-btn-upload:disabled {
1841
1826
  opacity: 0.7;
1842
- cursor: wait;
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
- // Always use Triagly's Turnstile site key (can be overridden for testing)
2001
- this.turnstileSiteKey = turnstileSiteKey || TriaglyAPI.DEFAULT_TURNSTILE_SITE_KEY;
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