@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.
@@ -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
@@ -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;IAc3B;;OAEG;IACH,OAAO,CAAC,SAAS;IASjB;;OAEG;IACH,OAAO,CAAC,aAAa;IAuErB;;OAEG;IACH,OAAO,CAAC,WAAW;IA4CnB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAsF3B,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;IAkBb;;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"}
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 || !this.canvas)
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 cancelBtn = this.overlay.querySelector('.triagly-btn-cancel');
351
- if (cancelBtn) {
352
- cancelBtn.addEventListener('click', () => this.close(true));
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: 999999;
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.screenshotCapture = null;
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-capture" id="triagly-capture-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
- <rect x="3" y="3" width="18" height="18" rx="2"/>
1187
- <circle cx="8.5" cy="8.5" r="1.5"/>
1188
- <path d="M21 15l-5-5L5 21"/>
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 || 'Capture Screenshot'}
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 capture event handlers
1218
+ * Set up screenshot upload event handlers
1298
1219
  */
1299
1220
  setupScreenshotHandlers(form) {
1300
- const captureBtn = form.querySelector('#triagly-capture-btn');
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
- // Capture screenshot
1307
- captureBtn?.addEventListener('click', async () => {
1308
- if (!this.screenshotCapture)
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
- const btn = captureBtn;
1311
- const originalText = btn.innerHTML;
1312
- btn.disabled = true;
1313
- btn.innerHTML = `
1314
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" class="triagly-spin">
1315
- <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"/>
1316
- <path d="M12 2v4"/>
1317
- </svg>
1318
- Capturing...
1319
- `;
1320
- try {
1321
- const dataUrl = await this.screenshotCapture.capture();
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.onScreenshotCapture) {
1332
- this.config.onScreenshotCapture(dataUrl);
1280
+ if (this.config.onScreenshotUpload) {
1281
+ this.config.onScreenshotUpload(dataUrl);
1333
1282
  }
1334
- }
1335
- catch (error) {
1336
- console.error('Triagly: Screenshot capture failed:', error);
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(error instanceof Error ? error : new Error(String(error)));
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
- finally {
1342
- btn.disabled = false;
1343
- btn.innerHTML = originalText;
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, #27272a);
1577
- box-shadow: var(--triagly-button-shadow-hover, 0 6px 16px rgba(0, 0, 0, 0.2));
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-capture {
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-capture:hover:not(:disabled) {
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-capture:disabled {
1819
+ .triagly-btn-upload:disabled {
1835
1820
  opacity: 0.7;
1836
- cursor: wait;
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();