@triagly/sdk 0.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -51,26 +51,23 @@
51
51
  // Button orientation
52
52
  const orientation = this.config.orientation || 'horizontal';
53
53
  button.classList.add(`triagly-orientation-${orientation}`);
54
+ // Speech bubble SVG icon
55
+ const speechBubbleIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" class="triagly-icon"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>`;
56
+ const largeIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="currentColor" class="triagly-icon"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>`;
54
57
  // Handle button text based on shape
55
- const fullText = this.config.buttonText || '🐛 Feedback';
58
+ const fullText = this.config.buttonText || 'Feedback';
56
59
  if (shape === 'circular') {
57
- button.innerHTML = '🐛';
60
+ button.innerHTML = largeIcon;
58
61
  button.setAttribute('aria-label', fullText);
59
62
  }
60
63
  else if (shape === 'expandable') {
61
- // Expandable starts with emoji, expands to full text on hover
62
- button.innerHTML = '<span class="triagly-btn-icon">🐛</span><span class="triagly-btn-text"> Feedback</span>';
64
+ // Expandable starts with icon, expands to full text on hover
65
+ button.innerHTML = `<span class="triagly-btn-icon">${largeIcon}</span><span class="triagly-btn-text"> ${this.config.buttonText || 'Feedback'}</span>`;
63
66
  button.setAttribute('aria-label', fullText);
64
- // Store custom text if provided
65
- if (this.config.buttonText) {
66
- const textSpan = button.querySelector('.triagly-btn-text');
67
- if (textSpan) {
68
- textSpan.textContent = ' ' + this.config.buttonText.replace('🐛', '').trim();
69
- }
70
- }
71
67
  }
72
68
  else {
73
- button.innerHTML = fullText;
69
+ // Default: icon + text
70
+ button.innerHTML = `${speechBubbleIcon}<span class="triagly-btn-label">${fullText}</span>`;
74
71
  }
75
72
  button.onclick = () => this.toggle();
76
73
  // Position button
@@ -136,9 +133,9 @@
136
133
  this.setupKeyboardEvents();
137
134
  // Set up focus trap
138
135
  this.setupFocusTrap();
139
- // Focus on title field
140
- const titleInput = this.container?.querySelector('input[type="text"]');
141
- titleInput?.focus();
136
+ // Focus on description field
137
+ const descInput = this.container?.querySelector('#triagly-description');
138
+ descInput?.focus();
142
139
  }, 0);
143
140
  }
144
141
  /**
@@ -183,7 +180,7 @@
183
180
  overlay.className = 'triagly-overlay';
184
181
  overlay.setAttribute('role', 'dialog');
185
182
  overlay.setAttribute('aria-modal', 'true');
186
- overlay.setAttribute('aria-labelledby', 'triagly-modal-title');
183
+ overlay.setAttribute('aria-label', 'Send feedback');
187
184
  overlay.onclick = (e) => {
188
185
  if (e.target === overlay)
189
186
  this.close('overlay');
@@ -194,7 +191,6 @@
194
191
  const header = document.createElement('div');
195
192
  header.className = 'triagly-header';
196
193
  header.innerHTML = `
197
- <h3 id="triagly-modal-title">Send Feedback</h3>
198
194
  <button type="button" class="triagly-close" aria-label="Close feedback form">×</button>
199
195
  `;
200
196
  const closeBtn = header.querySelector('.triagly-close');
@@ -203,16 +199,7 @@
203
199
  form.className = 'triagly-form';
204
200
  form.innerHTML = `
205
201
  <div class="triagly-field">
206
- <label for="triagly-title">Title (optional)</label>
207
- <input
208
- type="text"
209
- id="triagly-title"
210
- placeholder="Brief summary of your feedback"
211
- />
212
- </div>
213
-
214
- <div class="triagly-field">
215
- <label for="triagly-description">Description *</label>
202
+ <label for="triagly-description">What's on your mind?</label>
216
203
  <textarea
217
204
  id="triagly-description"
218
205
  required
@@ -221,6 +208,15 @@
221
208
  ></textarea>
222
209
  </div>
223
210
 
211
+ <div class="triagly-field">
212
+ <label for="triagly-name">Name (optional)</label>
213
+ <input
214
+ type="text"
215
+ id="triagly-name"
216
+ placeholder="Your name"
217
+ />
218
+ </div>
219
+
224
220
  <div class="triagly-field">
225
221
  <label for="triagly-email">Email (optional)</label>
226
222
  <input
@@ -230,13 +226,6 @@
230
226
  />
231
227
  </div>
232
228
 
233
- <div class="triagly-field triagly-checkbox">
234
- <label>
235
- <input type="checkbox" id="triagly-screenshot" checked />
236
- <span>Include screenshot</span>
237
- </label>
238
- </div>
239
-
240
229
  ${this.config.turnstileSiteKey ? `
241
230
  <div class="triagly-field triagly-turnstile">
242
231
  <div class="cf-turnstile" data-sitekey="${this.config.turnstileSiteKey}" data-theme="light"></div>
@@ -260,8 +249,16 @@
260
249
  e.preventDefault();
261
250
  this.handleSubmit(form);
262
251
  };
252
+ const footer = document.createElement('div');
253
+ footer.className = 'triagly-footer';
254
+ footer.innerHTML = `
255
+ <a href="https://triagly.com" target="_blank" rel="noopener noreferrer" class="triagly-branding">
256
+ Powered by <strong>Triagly</strong>
257
+ </a>
258
+ `;
263
259
  modal.appendChild(header);
264
260
  modal.appendChild(form);
261
+ modal.appendChild(footer);
265
262
  overlay.appendChild(modal);
266
263
  // Render Turnstile widget if available
267
264
  if (this.config.turnstileSiteKey) {
@@ -303,17 +300,16 @@
303
300
  turnstileContainer.setAttribute('data-widget-id', widgetId);
304
301
  }
305
302
  catch (error) {
306
- console.error('Triagly: Failed to render Turnstile widget:', error);
303
+ console.error('Triagly: Failed to render Turnstile widget:', error instanceof Error ? error.message : 'Unknown error');
307
304
  }
308
305
  }
309
306
  /**
310
307
  * Handle form submission
311
308
  */
312
309
  async handleSubmit(form) {
313
- const titleInput = form.querySelector('#triagly-title');
314
310
  const descInput = form.querySelector('#triagly-description');
311
+ const nameInput = form.querySelector('#triagly-name');
315
312
  const emailInput = form.querySelector('#triagly-email');
316
- const screenshotCheckbox = form.querySelector('#triagly-screenshot');
317
313
  const statusDiv = form.querySelector('#triagly-status');
318
314
  const submitBtn = form.querySelector('button[type="submit"]');
319
315
  const turnstileContainer = form.querySelector('.cf-turnstile');
@@ -327,10 +323,9 @@
327
323
  turnstileToken = turnstileContainer.getAttribute('data-turnstile-response') || undefined;
328
324
  }
329
325
  const data = {
330
- title: titleInput.value.trim() || undefined,
331
326
  description: descInput.value.trim(),
327
+ reporterName: nameInput.value.trim() || undefined,
332
328
  reporterEmail: emailInput.value.trim() || undefined,
333
- includeScreenshot: screenshotCheckbox.checked,
334
329
  turnstileToken,
335
330
  };
336
331
  // Create a promise that waits for actual submission result
@@ -394,21 +389,32 @@
394
389
  position: fixed;
395
390
  z-index: 999999;
396
391
  padding: 12px 20px;
397
- background: var(--triagly-button-bg, #6366f1);
392
+ background: var(--triagly-button-bg, #18181b);
398
393
  color: var(--triagly-button-text, #ffffff);
399
394
  border: none;
400
395
  border-radius: var(--triagly-button-radius, 8px);
401
396
  font-size: 14px;
402
397
  font-weight: 500;
403
398
  cursor: pointer;
404
- box-shadow: var(--triagly-button-shadow, 0 4px 12px rgba(99, 102, 241, 0.3));
399
+ box-shadow: var(--triagly-button-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));
405
400
  transition: all 0.2s;
401
+ display: inline-flex;
402
+ align-items: center;
403
+ gap: 8px;
406
404
  }
407
405
 
408
406
  .triagly-button:hover {
409
- background: var(--triagly-button-bg-hover, #4f46e5);
407
+ background: var(--triagly-button-bg-hover, #27272a);
410
408
  transform: translateY(-2px);
411
- box-shadow: var(--triagly-button-shadow-hover, 0 6px 16px rgba(99, 102, 241, 0.4));
409
+ box-shadow: var(--triagly-button-shadow-hover, 0 6px 16px rgba(0, 0, 0, 0.2));
410
+ }
411
+
412
+ .triagly-button .triagly-icon {
413
+ flex-shrink: 0;
414
+ }
415
+
416
+ .triagly-button .triagly-btn-label {
417
+ line-height: 1;
412
418
  }
413
419
 
414
420
  /* Prevent expandable buttons from shifting on hover */
@@ -502,8 +508,8 @@
502
508
  min-width: auto;
503
509
  padding: 12px 20px;
504
510
  border-radius: 30px;
505
- background: var(--triagly-button-bg-hover, #4f46e5);
506
- box-shadow: var(--triagly-button-shadow-hover, 0 6px 16px rgba(99, 102, 241, 0.4));
511
+ background: var(--triagly-button-bg-hover, #27272a);
512
+ box-shadow: var(--triagly-button-shadow-hover, 0 6px 16px rgba(0, 0, 0, 0.2));
507
513
  }
508
514
  .triagly-shape-expandable:hover .triagly-btn-text {
509
515
  width: auto;
@@ -562,18 +568,10 @@
562
568
 
563
569
  .triagly-header {
564
570
  display: flex;
565
- justify-content: space-between;
571
+ justify-content: flex-end;
566
572
  align-items: center;
567
- padding: 20px 24px;
573
+ padding: 8px 12px 0;
568
574
  background: var(--triagly-header-bg, #ffffff);
569
- border-bottom: 1px solid var(--triagly-header-border, #e5e7eb);
570
- }
571
-
572
- .triagly-header h3 {
573
- margin: 0;
574
- font-size: 18px;
575
- font-weight: 600;
576
- color: var(--triagly-header-text, #111827);
577
575
  }
578
576
 
579
577
  .triagly-close {
@@ -631,46 +629,21 @@
631
629
  .triagly-field input:focus,
632
630
  .triagly-field textarea:focus {
633
631
  outline: none;
634
- border-color: var(--triagly-input-border-focus, #6366f1);
635
- box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
636
- }
637
-
638
- .triagly-checkbox label {
639
- display: flex;
640
- align-items: center;
641
- gap: 8px;
642
- cursor: pointer;
643
- font-weight: 400;
644
- }
645
-
646
- .triagly-checkbox label span {
647
- user-select: none;
648
- }
649
-
650
- .triagly-checkbox input {
651
- width: 16px;
652
- height: 16px;
653
- margin: 0;
654
- cursor: pointer;
632
+ border-color: var(--triagly-input-border-focus, #a1a1aa);
633
+ box-shadow: 0 0 0 2px rgba(161, 161, 170, 0.15);
655
634
  }
656
635
 
657
636
  /* Focus visible styles for accessibility */
658
637
  .triagly-button:focus-visible,
659
638
  .triagly-field input:focus-visible,
660
639
  .triagly-field textarea:focus-visible,
661
- .triagly-checkbox input:focus-visible,
662
640
  .triagly-btn-primary:focus-visible,
663
641
  .triagly-btn-secondary:focus-visible,
664
642
  .triagly-close:focus-visible {
665
- outline: 2px solid #6366f1;
643
+ outline: 2px solid #a1a1aa;
666
644
  outline-offset: 2px;
667
645
  }
668
646
 
669
- /* Checkbox label gets visual indicator when checkbox is focused */
670
- .triagly-checkbox input:focus-visible + span {
671
- text-decoration: underline;
672
- }
673
-
674
647
  .triagly-turnstile {
675
648
  display: flex;
676
649
  justify-content: center;
@@ -696,12 +669,12 @@
696
669
  }
697
670
 
698
671
  .triagly-btn-primary {
699
- background: var(--triagly-btn-primary-bg, #6366f1);
672
+ background: var(--triagly-btn-primary-bg, #18181b);
700
673
  color: var(--triagly-btn-primary-text, #ffffff);
701
674
  }
702
675
 
703
676
  .triagly-btn-primary:hover:not(:disabled) {
704
- background: var(--triagly-btn-primary-bg-hover, #4f46e5);
677
+ background: var(--triagly-btn-primary-bg-hover, #27272a);
705
678
  }
706
679
 
707
680
  .triagly-btn-primary:disabled {
@@ -737,6 +710,29 @@
737
710
  background: var(--triagly-error-bg, #fee2e2);
738
711
  color: var(--triagly-error-text, #991b1b);
739
712
  }
713
+
714
+ .triagly-footer {
715
+ padding: 12px 24px 16px;
716
+ text-align: right;
717
+ border-top: 1px solid var(--triagly-footer-border, #e5e7eb);
718
+ background: var(--triagly-footer-bg, #f9fafb);
719
+ border-radius: 0 0 var(--triagly-modal-radius, 12px) var(--triagly-modal-radius, 12px);
720
+ }
721
+
722
+ .triagly-branding {
723
+ font-size: 12px;
724
+ color: var(--triagly-footer-text, #6b7280);
725
+ text-decoration: none;
726
+ transition: color 0.2s;
727
+ }
728
+
729
+ .triagly-branding:hover {
730
+ color: var(--triagly-footer-text-hover, #18181b);
731
+ }
732
+
733
+ .triagly-branding strong {
734
+ font-weight: 600;
735
+ }
740
736
  `;
741
737
  document.head.appendChild(style);
742
738
  }
@@ -824,10 +820,14 @@
824
820
  }
825
821
 
826
822
  // API Client
827
- const DEFAULT_API_URL = 'https://bssghvinezdawvupcyci.supabase.co/functions/v1';
823
+ const API_URLS = {
824
+ production: 'https://iipkklhhafrjesryscjh.supabase.co/functions/v1',
825
+ staging: 'https://bssghvinezdawvupcyci.supabase.co/functions/v1',
826
+ };
828
827
  class TriaglyAPI {
829
- constructor(publishableKey, apiUrl, getToken, turnstileSiteKey) {
830
- this.apiUrl = (apiUrl || DEFAULT_API_URL).replace(/\/$/, ''); // Remove trailing slash
828
+ constructor(publishableKey, environment = 'production', apiUrl, getToken, turnstileSiteKey) {
829
+ // apiUrl override takes precedence, then environment-based URL
830
+ this.apiUrl = (apiUrl || API_URLS[environment]).replace(/\/$/, ''); // Remove trailing slash
831
831
  this.publishableKey = publishableKey;
832
832
  this.getToken = getToken;
833
833
  // Always use Triagly's Turnstile site key (can be overridden for testing)
@@ -865,7 +865,7 @@
865
865
  }
866
866
  }
867
867
  catch (error) {
868
- console.warn('Failed to get Turnstile token:', error);
868
+ console.warn('Failed to get Turnstile token:', error instanceof Error ? error.message : 'Unknown error');
869
869
  }
870
870
  }
871
871
  return null;
@@ -889,7 +889,7 @@
889
889
  hardenedToken = await this.getToken();
890
890
  }
891
891
  catch (error) {
892
- console.error('Failed to get hardened token:', error);
892
+ console.error('Failed to get hardened token:', error instanceof Error ? error.message : 'Unknown error');
893
893
  throw new Error('Failed to authenticate. Please try again.');
894
894
  }
895
895
  }
@@ -902,7 +902,6 @@
902
902
  consoleLogs: data.consoleLogs,
903
903
  },
904
904
  tags: data.tags,
905
- screenshot: data.screenshot,
906
905
  reporterEmail: data.reporterEmail,
907
906
  reporterName: data.reporterName,
908
907
  turnstileToken,
@@ -989,34 +988,6 @@
989
988
  }
990
989
  return browser;
991
990
  }
992
- /**
993
- * Capture screenshot of current page
994
- */
995
- async function captureScreenshot() {
996
- try {
997
- // Use html2canvas library if available
998
- if (typeof window.html2canvas !== 'undefined') {
999
- const canvas = await window.html2canvas(document.body, {
1000
- logging: false,
1001
- useCORS: true,
1002
- allowTaint: true,
1003
- });
1004
- return canvas.toDataURL('image/png');
1005
- }
1006
- // Fallback to native screenshot API if supported (limited browser support)
1007
- if ('mediaDevices' in navigator && 'getDisplayMedia' in navigator.mediaDevices) {
1008
- // This requires user interaction and shows a permission dialog
1009
- // Not ideal for automatic screenshots
1010
- console.warn('Screenshot capture requires html2canvas library');
1011
- return null;
1012
- }
1013
- return null;
1014
- }
1015
- catch (error) {
1016
- console.error('Screenshot capture failed:', error);
1017
- return null;
1018
- }
1019
- }
1020
991
  /**
1021
992
  * Simple rate limiter using localStorage
1022
993
  */
@@ -1069,7 +1040,7 @@
1069
1040
  localStorage.setItem(this.key, JSON.stringify(data));
1070
1041
  }
1071
1042
  catch (error) {
1072
- console.error('Failed to store rate limit data:', error);
1043
+ console.error('Failed to store rate limit data:', error instanceof Error ? error.message : 'Unknown error');
1073
1044
  }
1074
1045
  }
1075
1046
  }
@@ -1174,7 +1145,7 @@
1174
1145
  }
1175
1146
  catch (error) {
1176
1147
  // Don't let logging break the app
1177
- this.originalConsole.error('Failed to capture log:', error);
1148
+ this.originalConsole.error('Failed to capture log:', error instanceof Error ? error.message : 'Unknown error');
1178
1149
  }
1179
1150
  }
1180
1151
  /**
@@ -1237,7 +1208,7 @@
1237
1208
  theme: 'auto',
1238
1209
  position: 'bottom-right',
1239
1210
  buttonShape: 'rounded',
1240
- buttonText: '🐛 Feedback',
1211
+ buttonText: 'Feedback',
1241
1212
  placeholderText: 'Describe what happened...',
1242
1213
  successMessage: 'Feedback sent successfully!',
1243
1214
  errorMessage: 'Failed to send feedback. Please try again.',
@@ -1248,7 +1219,7 @@
1248
1219
  apiKey,
1249
1220
  publishableKey: apiKey, // Keep for backward compatibility
1250
1221
  };
1251
- this.api = new TriaglyAPI(apiKey, this.config.apiUrl, this.config.getToken, this.config.turnstileSiteKey);
1222
+ this.api = new TriaglyAPI(apiKey, this.config.environment || 'production', this.config.apiUrl, this.config.getToken, this.config.turnstileSiteKey);
1252
1223
  // Always pass Turnstile site key to widget (from API which has default)
1253
1224
  this.config.turnstileSiteKey = this.api.getTurnstileSiteKey();
1254
1225
  this.widget = new FeedbackWidget(this.config);
@@ -1292,17 +1263,11 @@
1292
1263
  }
1293
1264
  // Collect metadata
1294
1265
  const metadata = collectMetadata(this.config.metadata);
1295
- // Capture screenshot if requested
1296
- let screenshot = null;
1297
- if (data.includeScreenshot) {
1298
- screenshot = await captureScreenshot();
1299
- }
1300
1266
  // Prepare feedback data
1301
1267
  const feedbackData = {
1302
1268
  title: data.title,
1303
1269
  description: data.description,
1304
1270
  reporterEmail: data.reporterEmail,
1305
- screenshot: screenshot || undefined,
1306
1271
  consoleLogs: this.consoleLogger?.getLogs(),
1307
1272
  };
1308
1273
  // Submit to API with Turnstile token if provided
@@ -1317,10 +1282,9 @@
1317
1282
  if (this.config.onSuccess) {
1318
1283
  this.config.onSuccess(response.id);
1319
1284
  }
1320
- console.log('Feedback submitted successfully:', response.id);
1321
1285
  }
1322
1286
  catch (error) {
1323
- console.error('Failed to submit feedback:', error);
1287
+ console.error('Failed to submit feedback:', error instanceof Error ? error.message : 'Unknown error');
1324
1288
  // Dispatch error event for UI layer
1325
1289
  document.dispatchEvent(new CustomEvent('triagly:error', {
1326
1290
  detail: error